@joshuafolkken/kit 1.1471.0 → 1.1514.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/.claude/settings.json +6 -6
  2. package/.claude/skills/epic-commands/SKILL.md +2 -14
  3. package/.claude/skills/epic-commands/epic-bundle.md +9 -5
  4. package/.claude/skills/epic-commands/execution-waves.md +1 -2
  5. package/.claude/skills/verify-ui/SKILL.md +1 -2
  6. package/.claude/skills/workflow-commands/SKILL.md +7 -9
  7. package/.claude/skills/workflow-commands/background-commands.md +3 -7
  8. package/.claude/skills/workflow-commands/backlogrun-child.md +3 -10
  9. package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -15
  10. package/.claude/skills/workflow-commands/backlogrun-park.md +2 -8
  11. package/.claude/skills/workflow-commands/backlogrun-progress.md +10 -19
  12. package/.claude/skills/workflow-commands/backlogrun-recovery.md +8 -8
  13. package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -10
  14. package/.claude/skills/workflow-commands/backlogrun.md +31 -50
  15. package/.claude/skills/workflow-commands/chain-rule.md +7 -6
  16. package/.claude/skills/workflow-commands/delegation.md +12 -8
  17. package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
  18. package/.claude/skills/workflow-commands/followup.md +3 -6
  19. package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
  20. package/.claude/skills/workflow-commands/fullrun.md +4 -6
  21. package/.claude/skills/workflow-commands/halfrun.md +3 -7
  22. package/.claude/skills/workflow-commands/into-target.md +1 -2
  23. package/.claude/skills/workflow-commands/issue-comments.md +3 -3
  24. package/.claude/skills/workflow-commands/issue-scout.md +3 -2
  25. package/.claude/skills/workflow-commands/latest-gate.md +2 -6
  26. package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
  27. package/.claude/skills/workflow-commands/observation-filing.md +0 -14
  28. package/.claude/skills/workflow-commands/observation-ledger.md +4 -9
  29. package/.claude/skills/workflow-commands/pre-gate-cut.md +11 -16
  30. package/.claude/skills/workflow-commands/prerequisite.md +1 -2
  31. package/.claude/skills/workflow-commands/progress-watcher.md +2 -7
  32. package/.claude/skills/workflow-commands/prrun.md +3 -5
  33. package/.claude/skills/workflow-commands/retrospective.md +4 -4
  34. package/.claude/skills/workflow-commands/split-assessment.md +1 -3
  35. package/.claude/skills/workflow-commands/target-repository.md +1 -2
  36. package/.claude/skills/workflow-commands/working-tree-hold.md +2 -4
  37. package/.codex/hooks.json +6 -6
  38. package/.github/actions/setup-node/action.yml +1 -1
  39. package/.github/workflows/ci.yml +1 -1
  40. package/.github/workflows/publish.yml +2 -2
  41. package/CLAUDE.md +1 -1
  42. package/cspell/index.yaml +7 -0
  43. package/dist/CLAUDE.md +1 -1
  44. package/dist/commands/document-section-cli.js +67 -34
  45. package/dist/hooks/batch-guard.js +6 -6
  46. package/dist/hooks/chunk-4KKD2UTK.js +66 -0
  47. package/dist/hooks/{chunk-J47HU5H5.js → chunk-6TS2JD6D.js} +3 -3
  48. package/dist/hooks/chunk-7DSBJVFH.js +50 -0
  49. package/dist/hooks/{chunk-YHBZAJSV.js → chunk-A65Y4YOR.js} +3 -3
  50. package/dist/hooks/{chunk-E7OIKX5Z.js → chunk-APUO6U4J.js} +85 -7
  51. package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
  52. package/dist/hooks/chunk-GP4IEC4O.js +20 -0
  53. package/dist/hooks/{chunk-SHGPANFB.js → chunk-JP45SOCJ.js} +3 -3
  54. package/dist/hooks/{chunk-K4EOONYM.js → chunk-LAOV7NLM.js} +1 -1
  55. package/dist/hooks/chunk-M4HGZV7U.js +83 -0
  56. package/dist/hooks/{chunk-KC5AK2GF.js → chunk-O6GDRNJB.js} +3 -3
  57. package/dist/hooks/{chunk-YYPI7SJZ.js → chunk-PF2NZL2A.js} +5 -5
  58. package/dist/hooks/{chunk-H5HVWYXK.js → chunk-PTICX4OL.js} +3 -4
  59. package/dist/hooks/{chunk-IIGFYXKC.js → chunk-RT27QRJW.js} +581 -1438
  60. package/dist/hooks/{chunk-43ZHT22P.js → chunk-SYOTDYCI.js} +1127 -1089
  61. package/dist/hooks/{chunk-AS74OU3D.js → chunk-TNUMY6NL.js} +1507 -1336
  62. package/dist/hooks/chunk-UPUZF3AN.js +279 -0
  63. package/dist/hooks/{chunk-EUMACUK2.js → chunk-WK6SMBIC.js} +1948 -806
  64. package/dist/hooks/codex-hook-adapter.js +17 -14
  65. package/dist/hooks/duplicate-read-guard.js +7 -7
  66. package/dist/hooks/format-edited.js +5 -4
  67. package/dist/hooks/inputs.json +317 -264
  68. package/dist/hooks/investigation-guard.js +8 -11
  69. package/dist/hooks/pretool-guard.js +16 -13
  70. package/dist/hooks/session-lang.js +370 -25
  71. package/dist/hooks/stop-guard.js +2576 -176
  72. package/dist/josh.js +2124 -277
  73. package/lefthook/base.yml +5 -3
  74. package/package.json +12 -10
  75. package/prompts/collaboration-workflow/file-edits.md +1 -3
  76. package/prompts/collaboration-workflow/gh-rest.md +1 -2
  77. package/prompts/collaboration-workflow/issue-citation.md +2 -4
  78. package/prompts/collaboration-workflow/issue-template.md +10 -2
  79. package/prompts/collaboration-workflow/lane-instructions.md +35 -0
  80. package/prompts/collaboration-workflow/operating-rules.md +2 -6
  81. package/prompts/collaboration-workflow/output-bounds.md +3 -3
  82. package/prompts/collaboration-workflow/overview.md +1 -1
  83. package/prompts/collaboration-workflow/principles.md +14 -8
  84. package/prompts/collaboration-workflow/report-format.md +9 -0
  85. package/prompts/collaboration-workflow/residency.md +12 -8
  86. package/prompts/collaboration-workflow/rule-delivery.md +3 -11
  87. package/prompts/collaboration-workflow/shell-body.md +6 -6
  88. package/prompts/collaboration-workflow/turn-batching.md +5 -5
  89. package/prompts/collaboration-workflow/upstream-interrupt.md +2 -2
  90. package/prompts/collaboration-workflow/wip-cap.md +6 -6
  91. package/prompts/collaboration-workflow.md +1 -0
  92. package/prompts/review.md +4 -5
  93. package/prompts/testing-guide.md +2 -2
  94. package/scripts/agent/agent-argv.ts +20 -4
  95. package/scripts/agent/agent-role-profile.ts +101 -45
  96. package/scripts/agent/agent-safe-text.ts +60 -0
  97. package/scripts/agent/claude-agent-argv.ts +14 -0
  98. package/scripts/agent/codex-agent-argv.ts +2 -0
  99. package/scripts/agent/lane-instructions.ts +144 -0
  100. package/scripts/backlog/backlog-budget.ts +4 -4
  101. package/scripts/backlog/backlog-drive-named.ts +1 -1
  102. package/scripts/backlog/backlog-drive-restore.ts +9 -1
  103. package/scripts/backlog/backlog-occupied.ts +56 -0
  104. package/scripts/backlog/backlog-ready.ts +47 -10
  105. package/scripts/delegation/duplicate-read-guard.ts +2 -2
  106. package/scripts/delegation/investigation-guard.ts +15 -65
  107. package/scripts/delegation/investigation-reads.ts +0 -1
  108. package/scripts/document/document-byte-budget.ts +6 -5
  109. package/scripts/document/document-section-cli.ts +17 -5
  110. package/scripts/document/entry-read-budget.ts +2 -2
  111. package/scripts/document/read-set-cli.ts +4 -1
  112. package/scripts/epic/epic-audit-orphans.ts +5 -8
  113. package/scripts/epic/epic-audit.ts +52 -1
  114. package/scripts/epic/epic-bundle-evidence.ts +49 -26
  115. package/scripts/epic/epic-bundle-referenced.ts +14 -3
  116. package/scripts/epic/epic-bundle.ts +17 -9
  117. package/scripts/followup/git-followup-workflow.ts +19 -1
  118. package/scripts/gate/check-inputs.ts +129 -0
  119. package/scripts/gate/check-reuse.ts +162 -0
  120. package/scripts/gate/core-budget.ts +93 -44
  121. package/scripts/gate/core-wait.ts +130 -0
  122. package/scripts/gate/free-cores.ts +49 -0
  123. package/scripts/gate/gate-ledger.ts +17 -3
  124. package/scripts/gate/gate-plan.ts +23 -1
  125. package/scripts/gate/gate-report.ts +15 -2
  126. package/scripts/gate/gate-run-steps.ts +150 -0
  127. package/scripts/gate/held-mark.ts +55 -0
  128. package/scripts/gate/machine-capacity.ts +70 -11
  129. package/scripts/gate/pre-commit-type-check.ts +14 -1
  130. package/scripts/gate/verification-gate.ts +58 -125
  131. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  132. package/scripts/gh/git-gh-issue-write.ts +24 -2
  133. package/scripts/gh/git-pr.ts +13 -1
  134. package/scripts/git/main-merge-regenerate.ts +78 -0
  135. package/scripts/git/main-merge.ts +14 -5
  136. package/scripts/hooks/batch-guard.ts +5 -8
  137. package/scripts/hooks/pre-commit-lint.ts +91 -0
  138. package/scripts/hooks/pre-push-unit.ts +1 -1
  139. package/scripts/hooks/pretool-guard.ts +27 -52
  140. package/scripts/hooks/reserved-run.ts +1 -0
  141. package/scripts/hooks/run-hook.sh +10 -12
  142. package/scripts/hooks/stop-actions.ts +204 -0
  143. package/scripts/hooks/stop-guard.ts +62 -21
  144. package/scripts/init/hook-launch.ts +12 -7
  145. package/scripts/issue/expected-effect-lint.ts +59 -0
  146. package/scripts/issue/issue-cite-cli.ts +35 -15
  147. package/scripts/issue/issue-classification.ts +1 -1
  148. package/scripts/issue/issue-lint-cli.ts +5 -3
  149. package/scripts/issue/issue-scout-cli.ts +19 -14
  150. package/scripts/issue/markdown-section.ts +19 -5
  151. package/scripts/josh/command-usage.ts +110 -0
  152. package/scripts/josh/josh-command-map.ts +2 -0
  153. package/scripts/josh/josh-command-types.ts +55 -9
  154. package/scripts/josh/josh-commands-ai.ts +13 -0
  155. package/scripts/josh/josh-commands-development.ts +1 -1
  156. package/scripts/josh/josh-commands-hooks.ts +6 -0
  157. package/scripts/josh/josh-commands-lane.ts +10 -1
  158. package/scripts/josh/josh-commands-workflow.ts +5 -0
  159. package/scripts/josh/josh-logic.ts +6 -15
  160. package/scripts/josh/josh-usage-automation.ts +121 -0
  161. package/scripts/josh/josh-usage-backlog.ts +180 -0
  162. package/scripts/josh/josh-usage-developer.ts +230 -0
  163. package/scripts/josh/josh-usage-hooks.ts +38 -0
  164. package/scripts/josh/josh-usage-issue.ts +205 -0
  165. package/scripts/josh/josh-usage-review.ts +101 -0
  166. package/scripts/josh/josh-usage-run.ts +252 -0
  167. package/scripts/josh/josh-usage-session.ts +238 -0
  168. package/scripts/josh/josh-usage-workflow.ts +212 -0
  169. package/scripts/josh/josh.ts +14 -5
  170. package/scripts/josh/package-directory.ts +20 -0
  171. package/scripts/lane/lane-capacity.ts +8 -2
  172. package/scripts/lane/lane-check-stats.ts +56 -0
  173. package/scripts/lane/lane-child-invocation.ts +35 -4
  174. package/scripts/lane/lane-dispatch.ts +3 -3
  175. package/scripts/lane/lane-guard-policy.ts +2 -2
  176. package/scripts/lane/lane-handoff.ts +8 -1
  177. package/scripts/lane/lane-install.ts +12 -4
  178. package/scripts/lane/lane-ledger.ts +43 -1
  179. package/scripts/lane/lane-limit-cli.ts +8 -9
  180. package/scripts/lane/lane-measure-cli.ts +2 -0
  181. package/scripts/lane/lane-preserve.ts +29 -1
  182. package/scripts/lane/lane-registry.ts +9 -7
  183. package/scripts/lane/lane-restart.ts +5 -26
  184. package/scripts/lane/lane-session-action.ts +146 -0
  185. package/scripts/lane/lane-session-broker.ts +57 -138
  186. package/scripts/lane/lane-withdraw-cli.ts +71 -0
  187. package/scripts/lane/lane-withdraw.ts +182 -0
  188. package/scripts/lint/format.ts +4 -4
  189. package/scripts/lint/lint-parallel.ts +10 -3
  190. package/scripts/lint/lint-related-scope.ts +4 -1
  191. package/scripts/notify/telegram-test.ts +10 -5
  192. package/scripts/report/report-format-reference.ts +2 -0
  193. package/scripts/review/acceptance-map-check.ts +61 -0
  194. package/scripts/review/acceptance-map.ts +205 -0
  195. package/scripts/review/review-brief-cli.ts +1 -1
  196. package/scripts/rules/agent-model.ts +59 -0
  197. package/scripts/rules/delivered-rules.ts +11 -2
  198. package/scripts/rules/filing-cap.ts +1 -0
  199. package/scripts/rules/firing-point.ts +6 -5
  200. package/scripts/rules/guard-remedy.ts +117 -0
  201. package/scripts/rules/issue-comments.ts +67 -6
  202. package/scripts/rules/last-prompt.ts +50 -5
  203. package/scripts/rules/oracle-list-cli.ts +7 -0
  204. package/scripts/rules/piped-verification.ts +3 -1
  205. package/scripts/rules/rule-list.ts +4 -1
  206. package/scripts/rules/rule-registry-rows.ts +10 -0
  207. package/scripts/rules/rule-registry.ts +7 -4
  208. package/scripts/rules/rule-rewrite.ts +114 -0
  209. package/scripts/rules/rule-value.ts +7 -1
  210. package/scripts/rules/shell-segments.ts +15 -0
  211. package/scripts/rules/stop-decision.ts +77 -0
  212. package/scripts/rules/stop-rules.ts +147 -47
  213. package/scripts/rules/tail-commands.ts +15 -1
  214. package/scripts/run/board/run-board-cli.ts +2 -0
  215. package/scripts/run/board/run-board-closed.ts +8 -2
  216. package/scripts/run/board/run-board-github.ts +9 -1
  217. package/scripts/run/board/run-board-header.ts +4 -1
  218. package/scripts/run/board/run-board-labels.ts +2 -0
  219. package/scripts/run/board/run-board-layout.ts +1 -1
  220. package/scripts/run/board/run-board-read.ts +16 -0
  221. package/scripts/run/board/run-board-reason.ts +10 -1
  222. package/scripts/run/board/run-board-render-legend.ts +1 -0
  223. package/scripts/run/board/run-board-render-slot.ts +1 -1
  224. package/scripts/run/board/run-board-render-waits.ts +41 -0
  225. package/scripts/run/board/run-board-render.ts +6 -1
  226. package/scripts/run/board/run-board-row-yield.ts +1 -0
  227. package/scripts/run/board/run-board-state.ts +5 -0
  228. package/scripts/run/board/run-board-status.ts +72 -21
  229. package/scripts/run/board/run-board-tick.ts +5 -1
  230. package/scripts/run/cut/run-cut-cli.ts +3 -2
  231. package/scripts/run/cut/run-cut-report.ts +16 -0
  232. package/scripts/run/detached-launch.ts +23 -4
  233. package/scripts/run/entry/run-entry-cli.ts +8 -1
  234. package/scripts/run/event/run-event-render.ts +1 -0
  235. package/scripts/run/event/run-event-scope.ts +19 -1
  236. package/scripts/run/event/run-event-stream-emit.ts +13 -6
  237. package/scripts/run/event/run-event-stream.ts +4 -0
  238. package/scripts/run/hold/run-hold-session.ts +46 -0
  239. package/scripts/run/hold/run-hold.ts +7 -1
  240. package/scripts/run/launch-priority.ts +32 -0
  241. package/scripts/run/merge/run-merge-cli.ts +16 -65
  242. package/scripts/run/merge/run-merge-signals.ts +99 -0
  243. package/scripts/run/merge/run-merge-steps.ts +11 -1
  244. package/scripts/run/merge/run-merge.ts +21 -9
  245. package/scripts/run/run-liveness.ts +1 -1
  246. package/scripts/run/run-poll-hook.ts +157 -0
  247. package/scripts/run/run-step-cli.ts +22 -15
  248. package/scripts/run/run-step-section.ts +85 -0
  249. package/scripts/run/ship/run-ship-cli.ts +5 -12
  250. package/scripts/run/ship/run-ship-detach.ts +9 -0
  251. package/scripts/run/ship/run-ship-next.ts +10 -2
  252. package/scripts/run/ship/run-ship-overlap.ts +195 -0
  253. package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
  254. package/scripts/run/ship/run-ship-preflight.ts +28 -2
  255. package/scripts/run/ship/run-ship-return.ts +31 -7
  256. package/scripts/run/ship/run-ship-review-steps.ts +12 -10
  257. package/scripts/run/ship/run-ship-steps.ts +1 -1
  258. package/scripts/run/ship/run-ship-stop-context.ts +206 -0
  259. package/scripts/run/ship/run-ship-sync.ts +14 -6
  260. package/scripts/test/test-e2e-guard.ts +30 -5
  261. package/scripts/test/test-unit-guard.ts +16 -1
  262. package/scripts/test/unit-worker-share.ts +26 -4
  263. package/scripts/time-runtime/time-shell.ts +29 -4
  264. package/scripts/time-runtime/time-spans.ts +1 -0
  265. package/scripts/time-runtime/time-tool-call.ts +7 -0
  266. package/templates/gitignore +6 -2
  267. package/dist/hooks/chunk-K4LMBQC2.js +0 -68
@@ -1,23 +1,14 @@
1
1
  import { randomUUID } from 'node:crypto'
2
- import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
2
+ import { mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { setTimeout as sleep } from 'node:timers/promises'
5
- import { agent_argv } from '#scripts/agent/agent-argv'
6
- import { agent_role_profile, type AgentProvider } from '#scripts/agent/agent-role-profile'
7
5
  import { git_common_directory } from '#scripts/git/git-common-directory'
8
6
  import { stamp_file } from '#scripts/josh/stamp-file'
9
- import { error_text } from '#scripts/lib/error-message'
10
7
  import { json_value } from '#scripts/lib/json-value'
11
- import { detached_launch } from '#scripts/run/detached-launch'
12
- import { run_ship_review } from '#scripts/run/ship/run-ship-review'
13
- import { run_ship_stage } from '#scripts/run/ship/run-ship-stage'
14
8
  import { z } from 'zod'
15
9
  import { lane_broker_server, type BrokerServer } from './lane-broker-server'
16
- import { lane_child_invocation, type ShipStop } from './lane-child-invocation'
17
- import { lane_child_marker } from './lane-child-marker'
18
- import { lane_parked } from './lane-parked'
19
10
  import type { LaneInfo } from './lane-registry'
20
- import { lane_relaunch } from './lane-relaunch'
11
+ import { lane_session_action, type BrokerAction, type BrokerAnswer } from './lane-session-action'
21
12
 
22
13
  // The sessions a lane child's detached `josh ship` cannot start itself, started for it by a process
23
14
  // that can: the round-1 and round-2 reviewer, and the fresh child a stopped ship hands back to. An
@@ -25,13 +16,11 @@ import { lane_relaunch } from './lane-relaunch'
25
16
  // Claude lane is served by the `run:wake` supervisor (`claude-lane-broker.ts`), because Claude Code removes its
26
17
  // token from what its Bash tool starts, so a token-only credential never reaches the ship the child
27
18
  // launched (joshuafolkken/kit#3817). **The request names an action, never a command**: the server
28
- // rebuilds the argv from the registered lane, so a child can ask for nothing but these two sessions.
19
+ // rebuilds the argv from the registered lane, so a child can ask for nothing but these two sessions
20
+ // (`lane-session-action.ts`).
29
21
 
30
22
  const REQUEST_PREFIX = '.josh-review-request-'
31
23
  const RESPONSE_PREFIX = '.josh-review-response-'
32
- const BRIEF_PREFIX = 'josh-ship-review-brief-'
33
- const FINDINGS_PREFIX = 'josh-ship-review-findings-'
34
- const LOG_PREFIX = 'josh-ship-review-log-'
35
24
  const CACHE_DIRECTORY = ['node_modules', '.cache']
36
25
  const POLL_MS = 100
37
26
  // The heartbeat is kept for a ship branched before the server record, and read only where a
@@ -41,22 +30,8 @@ const LEGACY_HEARTBEAT_LIMIT_MS = 3000
41
30
  const MINUTE_MS = 60_000
42
31
  const REQUEST_LIMIT_MINUTES = 30
43
32
  const REQUEST_LIMIT_MS = REQUEST_LIMIT_MINUTES * MINUTE_MS
44
- const ANTHROPIC_PROVIDER: AgentProvider = 'anthropic'
45
- const stop_schema = z
46
- .object({
47
- stage: z.enum(run_ship_stage.STAGE),
48
- title: z.string(),
49
- flags: z.array(z.string()).readonly(),
50
- is_review: z.boolean(),
51
- conflicts: z.array(z.string()).readonly().default([]),
52
- })
53
- .strict()
54
- const action_schema = z.discriminatedUnion('kind', [
55
- z.object({ kind: z.literal('review'), round: z.enum(['1', '2']) }).strict(),
56
- z.object({ kind: z.literal('relaunch'), stop: stop_schema }).strict(),
57
- ])
58
33
  const request_schema = z
59
- .object({ session: z.uuid(), nonce: z.uuid(), action: action_schema })
34
+ .object({ session: z.uuid(), nonce: z.uuid(), action: lane_session_action.action_schema })
60
35
  .strict()
61
36
  const response_schema = z
62
37
  .object({
@@ -66,18 +41,13 @@ const response_schema = z
66
41
  heartbeat_at: z.number().int().positive(),
67
42
  })
68
43
  .strict()
44
+ // **A failure's reason sits beside the response, never in it**: a ship branched before the reason parses
45
+ // the response strictly, so a new field would read as no server (joshuafolkken/kit#3974).
46
+ const reason_schema = z.object({ nonce: z.string(), reason: z.string() }).strict()
69
47
  type BrokerRequest = z.infer<typeof request_schema>
70
48
  type BrokerResponse = z.infer<typeof response_schema>
71
- type ReviewRound = Extract<z.infer<typeof action_schema>, { kind: 'review' }>['round']
72
- type BrokerAction = { kind: 'review'; round: ReviewRound } | { kind: 'relaunch'; stop: ShipStop }
73
-
74
- function review_paths(directory: string): { brief: string; findings: string; log: string } {
75
- return {
76
- brief: stamp_file.stamp_path(BRIEF_PREFIX, directory, '.md'),
77
- findings: stamp_file.stamp_path(FINDINGS_PREFIX, directory, '.txt'),
78
- log: stamp_file.stamp_path(LOG_PREFIX, directory, '.log'),
79
- }
80
- }
49
+
50
+ const { failure } = lane_session_action
81
51
 
82
52
  function request_path(directory: string, issue: string): string {
83
53
  return path.join(directory, ...CACHE_DIRECTORY, `${REQUEST_PREFIX}${issue}.json`)
@@ -90,6 +60,10 @@ function response_path(directory: string, issue: string): string | undefined {
90
60
  return path.join(path.dirname(common), ...CACHE_DIRECTORY, `${RESPONSE_PREFIX}${issue}.json`)
91
61
  }
92
62
 
63
+ function reason_path(response: string): string {
64
+ return `${response}.reason`
65
+ }
66
+
93
67
  function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
94
68
  try {
95
69
  return json_value.parse_with(readFileSync(target, 'utf8'), schema)
@@ -98,102 +72,25 @@ function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
98
72
  }
99
73
  }
100
74
 
101
- // The reviewer runs on the lane's own provider, so a Claude lane's server never starts a Codex reviewer
102
- // and an OpenAI lane's never starts a Claude one.
103
- function provider_of(lane: LaneInfo): AgentProvider {
104
- return lane.profile?.provider ?? ANTHROPIC_PROVIDER
105
- }
106
-
107
- // **The reviewer carries the lane's mark explicitly**: the supervisor serving a Claude lane is no lane child,
108
- // so a reviewer inheriting its environment would descend from the carry record's owner unmarked and be
109
- // read as the `backlogrun` parent by every hook. **Its prompt ends with the review invocation**, so the
110
- // lane's liveness read and the park's reap find it as they find the child (joshuafolkken/kit#3878).
111
-
112
- function review_prompt(lane: LaneInfo, round: ReviewRound): string {
113
- const target = review_paths(lane.directory)
114
- const prompt_of =
115
- round === '1' ? run_ship_review.reviewer_prompt : run_ship_review.verification_prompt
116
- const tail = lane_child_invocation.review_invocation(lane.issue)
117
-
118
- return `${prompt_of(target.brief, target.findings, lane.issue)} This is ${tail}`
119
- }
120
-
121
- async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<boolean> {
122
- const target = review_paths(lane.directory)
123
- const built = agent_argv.resolve_in(
124
- review_prompt(lane, round),
125
- agent_role_profile.REVIEWER,
126
- lane.directory,
127
- )
128
- if (built.kind === 'rejected' || built.profile.provider !== provider_of(lane)) return false
129
-
130
- const result = await detached_launch.launch_attached(
131
- {
132
- argv: built.argv,
133
- cwd: lane.directory,
134
- log_path: target.log,
135
- profile: built.profile,
136
- env: lane_child_marker.env_for(lane.issue),
137
- },
138
- console.error,
139
- )
140
-
141
- return result.kind === 'completed' && result.exit_code === 0
142
- }
143
-
144
- // An OpenAI lane's successor is its supervisor's to start, never a relaunch's.
145
- function relaunch(lane: LaneInfo, stop: ShipStop): boolean {
146
- if (lane_relaunch.is_openai_lane(lane)) return false
147
-
148
- const invocation = lane_child_invocation.ship_stop_invocation(lane.issue, stop)
149
- const phase = agent_role_profile.IMPLEMENTATION_PHASE
150
- const result = lane_relaunch.relaunch(lane, invocation, phase, console.error)
151
-
152
- return result.kind === 'launched'
153
- }
154
-
155
- // **A parked issue gets no session**: a request can be read long after it was written, so the park is
156
- // read when the action runs, never when it was asked for (joshuafolkken/kit#3878).
157
- async function perform(lane: LaneInfo, action: BrokerAction): Promise<boolean> {
158
- if (await lane_parked.is_parked(lane.issue)) return false
159
-
160
- if (action.kind === 'relaunch') return relaunch(lane, action.stop)
161
-
162
- return await launch_review(lane, action.round)
163
- }
164
-
165
- function write_response(target: string, body: BrokerResponse): void {
75
+ function write_record(target: string, body: BrokerResponse | z.infer<typeof reason_schema>): void {
166
76
  const temporary = `${target}.${String(process.pid)}.tmp`
167
77
 
168
78
  writeFileSync(temporary, JSON.stringify(body))
169
79
  renameSync(temporary, target)
170
80
  }
171
81
 
172
- async function launched(lane: LaneInfo, action: BrokerAction): Promise<boolean> {
173
- try {
174
- return await perform(lane, action)
175
- } catch (error) {
176
- error_text.trace_swallowed('lane_session_broker.launched', error)
177
-
178
- return false
179
- }
180
- }
181
-
182
82
  async function process_request(
183
83
  lane: LaneInfo,
184
84
  response: string,
185
85
  received: BrokerRequest,
186
86
  state: { current: BrokerResponse },
187
87
  ): Promise<void> {
188
- const is_success = await launched(lane, received.action)
88
+ const answer = await lane_session_action.answer(lane, received.action)
89
+ const { session, nonce } = received
189
90
 
190
- state.current = {
191
- session: received.session,
192
- nonce: received.nonce,
193
- is_success,
194
- heartbeat_at: Date.now(),
195
- }
196
- write_response(response, state.current)
91
+ if (!answer.is_success) write_record(reason_path(response), { nonce, reason: answer.reason })
92
+ state.current = { session, nonce, is_success: answer.is_success, heartbeat_at: Date.now() }
93
+ write_record(response, state.current)
197
94
  }
198
95
 
199
96
  async function serve(
@@ -236,7 +133,7 @@ function prepare(lane: LaneInfo): { response: string; current: BrokerResponse }
236
133
  heartbeat_at: Date.now(),
237
134
  }
238
135
 
239
- write_response(response, current)
136
+ write_record(response, current)
240
137
 
241
138
  return { response, current }
242
139
  }
@@ -244,7 +141,7 @@ function prepare(lane: LaneInfo): { response: string; current: BrokerResponse }
244
141
  function start_heartbeat(state: { current: BrokerResponse }, response: string): NodeJS.Timeout {
245
142
  const heartbeat = setInterval(() => {
246
143
  state.current = { ...state.current, heartbeat_at: Date.now() }
247
- write_response(response, state.current)
144
+ write_record(response, state.current)
248
145
  }, LEGACY_HEARTBEAT_MS)
249
146
 
250
147
  heartbeat.unref()
@@ -264,6 +161,7 @@ async function stop_server(
264
161
  await serving
265
162
  clearInterval(heartbeat)
266
163
  unlinkSync(response)
164
+ rmSync(reason_path(response), { force: true })
267
165
  lane_broker_server.remove(response)
268
166
  }
269
167
 
@@ -306,31 +204,47 @@ function session_response(target: string, session: string): BrokerResponse | und
306
204
  return response?.session === session ? response : undefined
307
205
  }
308
206
 
207
+ // A failure from a server that predates the reason leaves none.
208
+ function answer_of(target: string, response: BrokerResponse): BrokerAnswer {
209
+ if (response.is_success) return lane_session_action.SUCCESS
210
+
211
+ const record = read_record(reason_path(target), reason_schema)
212
+ const reason = record?.nonce === response.nonce ? record.reason : 'it gave no reason'
213
+
214
+ return failure(`the broker answered a failure: ${reason}`)
215
+ }
216
+
217
+ // The server's answer to this request, or `undefined` while it is still being served.
309
218
  function response_verdict(
310
219
  target: string,
311
220
  session: string,
312
221
  nonce: string,
313
- ): 'wait' | 'success' | 'failure' {
222
+ ): BrokerAnswer | undefined {
314
223
  const response = session_response(target, session)
315
224
 
316
- if (!is_live_response(target, response, lane_broker_server.is_running)) return 'failure'
317
- if (response.nonce !== nonce) return 'wait'
225
+ if (!is_live_response(target, response, lane_broker_server.is_running)) {
226
+ return failure('the broker stopped serving before it answered')
227
+ }
318
228
 
319
- return response.is_success ? 'success' : 'failure'
229
+ return response.nonce === nonce ? answer_of(target, response) : undefined
320
230
  }
321
231
 
322
- async function wait_response(target: string, session: string, nonce: string): Promise<boolean> {
232
+ async function wait_response(
233
+ target: string,
234
+ session: string,
235
+ nonce: string,
236
+ ): Promise<BrokerAnswer> {
323
237
  const deadline = Date.now() + REQUEST_LIMIT_MS
324
238
 
325
239
  while (Date.now() < deadline) {
326
240
  const verdict = response_verdict(target, session, nonce)
327
- if (verdict !== 'wait') return verdict === 'success'
241
+ if (verdict !== undefined) return verdict
328
242
 
329
243
  // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
330
244
  await sleep(POLL_MS)
331
245
  }
332
246
 
333
- return false
247
+ return failure(`the broker gave no answer within ${String(REQUEST_LIMIT_MINUTES)} minutes`)
334
248
  }
335
249
 
336
250
  function live_response(target: string): BrokerResponse | undefined {
@@ -356,22 +270,26 @@ function withdraw(pending: string, nonce: string): void {
356
270
  if (read_record(pending, request_schema)?.nonce === nonce) stamp_file.remove_stamp(pending)
357
271
  }
358
272
 
359
- async function request(directory: string, issue: string, action: BrokerAction): Promise<boolean> {
273
+ async function request(
274
+ directory: string,
275
+ issue: string,
276
+ action: BrokerAction,
277
+ ): Promise<BrokerAnswer> {
360
278
  const target = response_path(directory, issue)
361
279
  const response = target === undefined ? undefined : live_response(target)
362
280
 
363
- if (target === undefined || response === undefined) return false
281
+ if (target === undefined || response === undefined) return failure('no broker serves this lane')
364
282
 
365
283
  const nonce = randomUUID()
366
284
  const body = { session: response.session, nonce, action }
367
285
  const pending = request_path(directory, issue)
368
286
 
369
287
  stamp_file.replace_stamp(pending, body)
370
- const is_success = await wait_response(target, response.session, nonce)
288
+ const answer = await wait_response(target, response.session, nonce)
371
289
 
372
290
  withdraw(pending, nonce)
373
291
 
374
- return is_success
292
+ return answer
375
293
  }
376
294
 
377
295
  const lane_session_broker = {
@@ -379,9 +297,10 @@ const lane_session_broker = {
379
297
  request,
380
298
  request_path,
381
299
  response_path,
382
- review_paths,
300
+ review_paths: lane_session_action.review_paths,
383
301
  with_server,
384
302
  }
385
303
 
386
- export type { BrokerAction, ReviewRound }
387
304
  export { lane_session_broker }
305
+
306
+ export { type ReviewRound, type BrokerAction, type BrokerAnswer } from './lane-session-action'
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env tsx
2
+ import { fileURLToPath } from 'node:url'
3
+ import { issue_number_shape } from '#scripts/issue/issue-number-shape'
4
+ import { cli_flags } from '#scripts/lib/cli-flags'
5
+ import { lane_withdraw, type WithdrawOutcome } from './lane-withdraw'
6
+
7
+ // `josh lane:withdraw <issue> --reason <text>` — every explanation on standard error, and a withdrawal
8
+ // that did not end with the Issue closed exits non-zero, so a caller never takes a half-done one as done.
9
+
10
+ const ARGV_OFFSET = 2
11
+ const SUCCESS_EXIT_CODE = 0
12
+ const FAILURE_EXIT_CODE = 1
13
+ const USAGE = 'Usage: josh lane:withdraw <issue-number> --reason <text>'
14
+ const OPTIONS = { reason: { type: 'string' } } as const
15
+
16
+ interface Request {
17
+ issue: string
18
+ reason: string
19
+ }
20
+
21
+ function issue_of(positionals: ReadonlyArray<string>): string | undefined {
22
+ const [issue, ...rest] = positionals
23
+
24
+ return rest.length === 0 && issue_number_shape.is_issue_number(issue ?? '') ? issue : undefined
25
+ }
26
+
27
+ // A reason of only whitespace is no reason: the Issue would close with an empty `Why:` line.
28
+ function reason_of(value: unknown): string | undefined {
29
+ const reason = cli_flags.string_of(value)?.trim()
30
+
31
+ return reason === '' ? undefined : reason
32
+ }
33
+
34
+ function parse_request(argv: ReadonlyArray<string>): Request | undefined {
35
+ const parsed = cli_flags.arguments_of(argv, OPTIONS)
36
+ if (parsed === undefined) return undefined
37
+ const issue = issue_of(parsed.positionals)
38
+ const reason = reason_of(parsed.values.reason)
39
+
40
+ return issue === undefined || reason === undefined ? undefined : { issue, reason }
41
+ }
42
+
43
+ function exit_code_of(outcome: WithdrawOutcome): number {
44
+ return outcome.kind === 'withdrawn' && outcome.is_closed ? SUCCESS_EXIT_CODE : FAILURE_EXIT_CODE
45
+ }
46
+
47
+ async function run(argv: ReadonlyArray<string>): Promise<number> {
48
+ const request = parse_request(argv)
49
+
50
+ if (request === undefined) {
51
+ console.error(USAGE)
52
+
53
+ return FAILURE_EXIT_CODE
54
+ }
55
+
56
+ const outcome = await lane_withdraw.withdraw(request.issue, request.reason)
57
+
58
+ console.error(lane_withdraw.describe(outcome, request.issue))
59
+
60
+ return exit_code_of(outcome)
61
+ }
62
+
63
+ async function main(argv: ReadonlyArray<string>): Promise<void> {
64
+ process.exitCode = await run(argv)
65
+ }
66
+
67
+ const lane_withdraw_cli = { run }
68
+
69
+ if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
70
+
71
+ export { lane_withdraw_cli }
@@ -0,0 +1,182 @@
1
+ import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
2
+ import { issue_cite } from '#scripts/issue/issue-cite'
3
+ import { issue_number_shape } from '#scripts/issue/issue-number-shape'
4
+ import { issue_state_cli } from '#scripts/issue/issue-state-cli'
5
+ import { session_cite } from '#scripts/issue/session-cite'
6
+ import { run_event_stream } from '#scripts/run/event/run-event-stream'
7
+ import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
8
+ import { run_label } from '#scripts/run/run-label'
9
+ import { lane_close, type CloseKind } from './lane-close'
10
+ import { lane_preserve, type KeptWork } from './lane-preserve'
11
+ import { lane_reap } from './lane-reap'
12
+ import { lane_registry, type LaneInfo } from './lane-registry'
13
+
14
+ // `josh lane:withdraw <N> --reason <text>` — a person takes a running lane child's issue out of the run
15
+ // for good: the child is stopped, its work stashed, the Issue closed as not planned and the lane closed.
16
+ //
17
+ // **The `withdraw` event is written first, before anything is stopped.** Killing the child alone would
18
+ // let `lane:await` book it stopped and `run:merge` classify it from GitHub — an open issue resumed as an
19
+ // `outage` or parked `needs-decision`. `run:merge` reads the event stream before GitHub, so a child whose
20
+ // newest launch-or-withdraw event is a withdraw is booked `withdrawn` whatever the Issue's state reads
21
+ // at that moment, and the race between the kill and the close cannot book it as anything else.
22
+ //
23
+ // **The issue is read again once the child is stopped.** It may merge between the first read and the
24
+ // reap; the reap stopped it, so the second read is final, and an issue it already closed keeps
25
+ // GitHub's close — `run:merge` books a merged close as merged whatever the event stream says.
26
+ //
27
+ // A tree that could not be stashed is left in place with the Issue open: closing the lane would delete
28
+ // the work, and the person decides what to keep.
29
+
30
+ const WITHDRAW_OCCASION = 'lane withdraw'
31
+ const WITHDRAW_CAUSE = 'a person withdrew it with `lane:withdraw`'
32
+ const CLOSED_STATE = 'CLOSED'
33
+ const CLOSED_KIND = 'closed'
34
+ const NO_LANE_KIND = 'no-lane'
35
+ const UNREADABLE_KIND = 'unreadable'
36
+
37
+ type Unsaved = Extract<KeptWork, { kind: 'unsaved' }>
38
+
39
+ interface Withdrawn {
40
+ kind: 'withdrawn'
41
+ // How many of the child's processes the withdrawal stopped — 0 for a child that had already ended.
42
+ reaped: number
43
+ stash: string | undefined
44
+ is_closed: boolean
45
+ // The issue had closed on its own — merged — before the withdrawal reached it, so it was left as is.
46
+ had_closed: boolean
47
+ lane: CloseKind
48
+ }
49
+
50
+ type IssueSettled = Pick<Withdrawn, 'had_closed' | 'is_closed'>
51
+
52
+ type Refused =
53
+ { kind: typeof CLOSED_KIND } | { kind: typeof NO_LANE_KIND } | { kind: typeof UNREADABLE_KIND }
54
+ type WithdrawOutcome = Withdrawn | Unsaved | Refused
55
+
56
+ function close_comment(reason: string): string {
57
+ return [
58
+ 'Withdrawn by a person with `pnpm josh lane:withdraw`.',
59
+ 'Its lane child was stopped and the lane closed.',
60
+ `- Why: ${reason}`,
61
+ ].join('\n')
62
+ }
63
+
64
+ async function record_withdraw(issue: string, reason: string): Promise<void> {
65
+ await run_event_stream_emit.emit(
66
+ run_event_stream.EVENT_KIND.WITHDRAW,
67
+ `${issue_cite.plain(issue)} withdrawn by a person: ${reason}`,
68
+ )
69
+ }
70
+
71
+ // A closed issue has nothing to withdraw, and an unread one might be: neither writes anything.
72
+ async function refusal_of(issue: string): Promise<Refused | undefined> {
73
+ const read = await issue_state_cli.read_issue(issue)
74
+
75
+ if (read.kind !== 'state') return { kind: UNREADABLE_KIND }
76
+ if (read.state.state.toUpperCase() === CLOSED_STATE) return { kind: CLOSED_KIND }
77
+
78
+ return undefined
79
+ }
80
+
81
+ async function close_open_issue(issue: string, reason: string): Promise<IssueSettled> {
82
+ const refusal = await refusal_of(issue)
83
+
84
+ // An unread issue may have merged during the reap, so it is left open for a person to close.
85
+ if (refusal !== undefined) {
86
+ const is_closed = refusal.kind === CLOSED_KIND
87
+
88
+ return { is_closed, had_closed: is_closed }
89
+ }
90
+
91
+ const is_closed = await git_gh_issue_write.issue_close_not_planned(issue, close_comment(reason))
92
+
93
+ return { is_closed, had_closed: false }
94
+ }
95
+
96
+ async function close_issue(
97
+ issue: string,
98
+ reason: string,
99
+ stash: string | undefined,
100
+ ): Promise<IssueSettled> {
101
+ const settled = await close_open_issue(issue, reason)
102
+
103
+ if (stash !== undefined) await lane_preserve.announce(issue, stash, WITHDRAW_CAUSE)
104
+ await run_label.unmark(issue)
105
+
106
+ return settled
107
+ }
108
+
109
+ async function stop_and_close(lane: LaneInfo, reason: string): Promise<WithdrawOutcome> {
110
+ await record_withdraw(lane.issue, reason)
111
+ const reaped = lane_reap.reap_child(lane.issue).length
112
+ const saved = await lane_preserve.keep_work(lane.issue, lane.directory, WITHDRAW_OCCASION)
113
+
114
+ if (saved.kind === 'unsaved') return saved
115
+ const settled = await close_issue(lane.issue, reason, saved.stash)
116
+ const closed = await lane_close.close_lane(lane.issue)
117
+
118
+ return { kind: 'withdrawn', reaped, stash: saved.stash, ...settled, lane: closed.kind }
119
+ }
120
+
121
+ /** Stop `#<N>`'s lane child, stash its work, close the Issue as not planned and close the lane. */
122
+ async function withdraw(issue: string, reason: string): Promise<WithdrawOutcome> {
123
+ issue_number_shape.require_issue_number(issue)
124
+ const refusal = await refusal_of(issue)
125
+
126
+ if (refusal !== undefined) return refusal
127
+ const lane = await lane_registry.find_open_lane(issue)
128
+
129
+ if (lane === undefined) return { kind: NO_LANE_KIND }
130
+
131
+ return await stop_and_close(lane, reason)
132
+ }
133
+
134
+ function stash_sentence(stash: string | undefined): string {
135
+ if (stash === undefined) return 'The lane held no uncommitted work.'
136
+
137
+ return `Its uncommitted work was stashed — recover with \`pnpm josh stash:pop "${stash}"\`.`
138
+ }
139
+
140
+ function withdrawn_text(outcome: Withdrawn, issue: string): string {
141
+ const cited = session_cite.issue(issue)
142
+ const stopped = outcome.reaped === 0 ? 'Its child had already ended.' : 'Stopped its child.'
143
+ const head = `Withdrew ${cited}. ${stopped} ${stash_sentence(outcome.stash)}`
144
+ const lane = outcome.lane === 'incomplete' ? ' The lane could not be fully removed.' : ''
145
+
146
+ if (outcome.had_closed) return `${head} The Issue had already closed and was left as is.${lane}`
147
+
148
+ if (outcome.is_closed) return `${head} The Issue is closed as not planned.${lane}`
149
+
150
+ return `${head} The Issue could not be closed — close ${cited} by hand; \`run:merge\` books it withdrawn either way.${lane}`
151
+ }
152
+
153
+ function unsaved_text(outcome: Unsaved, issue: string): string {
154
+ return `The child for ${session_cite.issue(issue)} was stopped, but its uncommitted work could not be stashed (${outcome.reason}), so the Issue is left open and the lane at ${outcome.directory}; \`run:merge\` books it withdrawn.`
155
+ }
156
+
157
+ function refused_text(outcome: Refused, issue: string): string {
158
+ const cited = session_cite.issue(issue)
159
+
160
+ if (outcome.kind === CLOSED_KIND) {
161
+ return `${cited} is already closed, so there is nothing to withdraw.`
162
+ }
163
+
164
+ if (outcome.kind === NO_LANE_KIND) {
165
+ return `No lane is open for ${cited}, so there is no child to withdraw.`
166
+ }
167
+
168
+ return `The state of ${cited} could not be read, so nothing was stopped or closed — try again.`
169
+ }
170
+
171
+ function describe(outcome: WithdrawOutcome, issue: string): string {
172
+ if (outcome.kind === 'unsaved') return unsaved_text(outcome, issue)
173
+
174
+ return outcome.kind === 'withdrawn'
175
+ ? withdrawn_text(outcome, issue)
176
+ : refused_text(outcome, issue)
177
+ }
178
+
179
+ const lane_withdraw = { describe, withdraw }
180
+
181
+ export type { WithdrawOutcome }
182
+ export { lane_withdraw }
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { project_checks } from '#scripts/gate/project-checks'
4
- import { ESLINT_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
4
+ import { ESLINT_CACHE_FLAGS, PRETTIER_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
5
5
  import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-composite-arguments'
6
6
  import { execa } from 'execa'
7
7
 
8
8
  const COMMAND_NAME = 'format'
9
9
  const ARGV_OFFSET = 2
10
10
  const FAIL_EXIT_CODE = 1
11
- const PRETTIER_ARGS = ['exec', 'prettier', '--write', '.'] as const
11
+ const PRETTIER_WRITE_ARGS = ['exec', 'prettier', '--write', '.', ...PRETTIER_CACHE_FLAGS] as const
12
12
  const ESLINT_FIX_ARGS = ['exec', 'eslint', '.', '--fix', ...ESLINT_CACHE_FLAGS] as const
13
13
 
14
14
  async function run_step(args: ReadonlyArray<string>, directory: string): Promise<number> {
@@ -37,7 +37,7 @@ async function run_format(directory: string): Promise<number> {
37
37
  const prettier_exit_code = await run_unless_skipped(
38
38
  'prettier',
39
39
  prettier_reason,
40
- PRETTIER_ARGS,
40
+ PRETTIER_WRITE_ARGS,
41
41
  directory,
42
42
  )
43
43
  if (prettier_exit_code !== 0) return prettier_exit_code
@@ -64,4 +64,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
64
64
 
65
65
  const format = { run, run_format }
66
66
 
67
- export { ESLINT_FIX_ARGS, format }
67
+ export { ESLINT_FIX_ARGS, format, PRETTIER_WRITE_ARGS }
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { project_checks } from '#scripts/gate/project-checks'
4
- import { ESLINT_CACHE_FILE, ESLINT_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
4
+ import {
5
+ ESLINT_CACHE_FILE,
6
+ ESLINT_CACHE_FLAGS,
7
+ PRETTIER_CACHE_FLAGS,
8
+ } from '#scripts/josh/josh-command-types'
5
9
  import { lane_cache_run } from '#scripts/lane/lane-cache-run'
6
10
  import {
7
11
  buffered_process,
@@ -11,7 +15,6 @@ import {
11
15
  import { SKIP_MARKER } from '#scripts/test/skip-marker'
12
16
  import { cross_file_recheck, type CrossFileRecheckOptions } from './cross-file-recheck'
13
17
 
14
- const PRETTIER_ARGS = ['exec', 'prettier', '--check', '.'] as const
15
18
  // This — not the `lint:eslint` map entry — is the eslint invocation `josh gate` reaches, so the
16
19
  // cache flags come from the same constant the ignore rules are asserted against.
17
20
  // A copy here would keep writing the old location the day that constant
@@ -23,6 +26,10 @@ const WHOLE_TREE_RECHECK: CrossFileRecheckOptions = {
23
26
  patterns: [WHOLE_TREE],
24
27
  }
25
28
 
29
+ // Prettier's cache stays this checkout's own, never shared through `lane_cache_run` as eslint's is:
30
+ // its entries do not survive a move (`josh-command-types.ts` → `GATE_CACHE_SPECS`).
31
+ const PRETTIER_ARGS = ['exec', 'prettier', '--check', WHOLE_TREE, ...PRETTIER_CACHE_FLAGS] as const
32
+
26
33
  // What one eslint run is made of: the child's arguments, and what re-reading a cached cross-file report
27
34
  // (`cross-file-recheck.ts`) needs to ask the same question again — the cache file and the targets.
28
35
  interface EslintRun {
@@ -132,4 +139,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
132
139
 
133
140
  const lint_parallel = { run_lint_checks, run_lint_parallel_checks }
134
141
 
135
- export { ESLINT_ARGS, lint_parallel }
142
+ export { ESLINT_ARGS, lint_parallel, PRETTIER_ARGS }
@@ -8,6 +8,7 @@ import {
8
8
  import {
9
9
  ESLINT_RELATED_CACHE_FILE,
10
10
  ESLINT_RELATED_CACHE_FLAGS,
11
+ PRETTIER_RELATED_CACHE_FLAGS,
11
12
  } from '#scripts/josh/josh-command-types'
12
13
  import type { CrossFileRecheckOptions } from './cross-file-recheck'
13
14
 
@@ -94,8 +95,10 @@ function describe_scope(scope: ChangedFileScope, root: string): string {
94
95
  return changed_file_scope.describe_scope(scope, root, VOCABULARY)
95
96
  }
96
97
 
98
+ // The scoped run's own cache file, for the reason eslint's below gives: prettier too rewrites its
99
+ // cache file whole, so a narrowed run sharing the gate's would discard entries a concurrent gate wrote.
97
100
  function prettier_arguments(files: ReadonlyArray<string>): ReadonlyArray<string> {
98
- return [...PRETTIER_ARGS, ...files]
101
+ return [...PRETTIER_ARGS, ...files, ...PRETTIER_RELATED_CACHE_FLAGS]
99
102
  }
100
103
 
101
104
  // The cache file is this command's own rather than the gate's, because the two run at the same time.