@joshuafolkken/kit 1.1383.0 → 1.1435.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 (244) hide show
  1. package/.claude/skills/epic-commands/SKILL.md +1 -1
  2. package/.claude/skills/epic-commands/epic-bundle.md +5 -0
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
  4. package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
  5. package/.claude/skills/workflow-commands/backlogrun.md +1 -0
  6. package/.claude/skills/workflow-commands/chain-rule.md +4 -2
  7. package/.claude/skills/workflow-commands/issue-comments.md +6 -0
  8. package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
  9. package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
  10. package/README.md +25 -0
  11. package/dist/hooks/audit-provision.js +2 -2
  12. package/dist/hooks/batch-guard.js +7 -7
  13. package/dist/hooks/{chunk-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
  14. package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
  15. package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
  16. package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
  17. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  18. package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
  19. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  20. package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
  21. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  22. package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
  23. package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
  24. package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
  25. package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
  26. package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
  27. package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
  28. package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
  29. package/dist/hooks/codex-hook-adapter.js +15 -15
  30. package/dist/hooks/duplicate-read-guard.js +8 -8
  31. package/dist/hooks/format-edited.js +5 -5
  32. package/dist/hooks/inputs.json +262 -201
  33. package/dist/hooks/investigation-guard.js +8 -8
  34. package/dist/hooks/pretool-guard.js +14 -14
  35. package/dist/hooks/session-lang.js +124 -15
  36. package/dist/hooks/stop-guard.js +98 -55
  37. package/dist/josh.js +63 -10
  38. package/package.json +4 -3
  39. package/prompts/review.md +22 -0
  40. package/scripts/agent/claude-agent-argv.ts +13 -1
  41. package/scripts/auth/auth-setup-cli.ts +101 -0
  42. package/scripts/auth/claude-token-output.ts +78 -0
  43. package/scripts/auth/claude-token.ts +68 -0
  44. package/scripts/backlog/backlog-drive-cli.ts +2 -4
  45. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  46. package/scripts/backlog/backlog-drive-launch.ts +5 -0
  47. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  48. package/scripts/backlog/backlog-drive-named.ts +4 -2
  49. package/scripts/backlog/backlog-drive-owner.ts +72 -5
  50. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  51. package/scripts/backlog/backlog-drive.ts +16 -8
  52. package/scripts/backlog/backlog-overlap.ts +22 -5
  53. package/scripts/backlog/backlog-ready.ts +1 -3
  54. package/scripts/backlog/backlogrun-args.ts +13 -2
  55. package/scripts/backlog/backlogrun-cli.ts +91 -19
  56. package/scripts/backlog/backlogrun-launch.ts +80 -5
  57. package/scripts/backlog/backlogrun-replace.ts +166 -0
  58. package/scripts/backlog/backlogrun-stop.ts +178 -0
  59. package/scripts/behavior/behavior-cli.ts +6 -1
  60. package/scripts/doctor/doctor-io.ts +60 -9
  61. package/scripts/doctor/doctor.ts +20 -1
  62. package/scripts/document/document-byte-budget.ts +3 -3
  63. package/scripts/epic/epic-bundle-cli.ts +8 -8
  64. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  65. package/scripts/epic/epic-bundle.ts +39 -4
  66. package/scripts/exports/unused-members-cli.ts +2 -1
  67. package/scripts/followup/followup-checks-event.ts +82 -0
  68. package/scripts/followup/git-followup-finish.ts +16 -2
  69. package/scripts/followup/git-followup-workflow.ts +5 -2
  70. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  71. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  72. package/scripts/followup/git-pr-followup.ts +4 -1
  73. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  74. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  75. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  76. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  77. package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
  78. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  79. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  80. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  81. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  82. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  83. package/scripts/gate/gate-report.ts +70 -15
  84. package/scripts/gate/project-checks.ts +13 -2
  85. package/scripts/gate/scoped-green.ts +2 -1
  86. package/scripts/gate/verification-gate.ts +4 -3
  87. package/scripts/gh/gh-author-association.ts +14 -0
  88. package/scripts/gh/git-gh-conditional.ts +66 -0
  89. package/scripts/gh/git-gh-exec.ts +1 -1
  90. package/scripts/gh/git-gh-issue-read.ts +6 -3
  91. package/scripts/gh/git-gh-issue-write.ts +5 -1
  92. package/scripts/gh/git-gh-issue.ts +4 -1
  93. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  94. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  95. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  96. package/scripts/gh/git-pr-checks.ts +36 -2
  97. package/scripts/git/git-command.ts +16 -4
  98. package/scripts/git/git-schemas.ts +1 -0
  99. package/scripts/git/git-spawn-sync.ts +1 -1
  100. package/scripts/hooks/pretool-guard.ts +3 -1
  101. package/scripts/init/hook-command-rewrite.ts +31 -0
  102. package/scripts/init/init-actions.ts +4 -1
  103. package/scripts/init/init-ai-copy.ts +33 -10
  104. package/scripts/init/init-basic.ts +8 -3
  105. package/scripts/init/init-logic-toolchain.ts +76 -0
  106. package/scripts/init/init-logic.ts +2 -0
  107. package/scripts/init/init.ts +5 -23
  108. package/scripts/init/project-profile.ts +36 -7
  109. package/scripts/init/start-exec.ts +27 -1
  110. package/scripts/init/start-plan.ts +30 -4
  111. package/scripts/init/start-steps.ts +77 -3
  112. package/scripts/init/start.ts +21 -13
  113. package/scripts/init/workflow-mark.ts +35 -0
  114. package/scripts/issue/issue-ack-cli.ts +84 -0
  115. package/scripts/issue/issue-labels.ts +40 -8
  116. package/scripts/issue/issue-state-cli.ts +14 -4
  117. package/scripts/issue/issue-watch-diff.ts +152 -0
  118. package/scripts/issue/issue-watch-gate.ts +81 -0
  119. package/scripts/issue/issue-watch-hook.ts +54 -0
  120. package/scripts/issue/issue-watch.ts +240 -0
  121. package/scripts/issue/measure-rerun-cli.ts +3 -4
  122. package/scripts/josh/josh-command-map.ts +4 -0
  123. package/scripts/josh/josh-commands-backlog.ts +2 -2
  124. package/scripts/josh/josh-commands-development.ts +1 -1
  125. package/scripts/josh/josh-commands-issue.ts +7 -0
  126. package/scripts/josh/josh-commands-lane.ts +11 -2
  127. package/scripts/josh/josh-commands-project.ts +25 -1
  128. package/scripts/josh/josh-verdict.ts +33 -4
  129. package/scripts/lane/claude-lane-broker.ts +142 -0
  130. package/scripts/lane/lane-await.ts +6 -2
  131. package/scripts/lane/lane-cache.ts +37 -7
  132. package/scripts/lane/lane-capacity.ts +8 -6
  133. package/scripts/lane/lane-child-invocation.ts +1 -0
  134. package/scripts/lane/lane-cli.ts +15 -1
  135. package/scripts/lane/lane-close.ts +15 -6
  136. package/scripts/lane/lane-dispatch.ts +36 -10
  137. package/scripts/lane/lane-environment.ts +1 -1
  138. package/scripts/lane/lane-launch-cli.ts +13 -4
  139. package/scripts/lane/lane-ledger.ts +21 -3
  140. package/scripts/lane/lane-measure-cli.ts +10 -2
  141. package/scripts/lane/lane-open.ts +9 -1
  142. package/scripts/lane/lane-preserve.ts +49 -0
  143. package/scripts/lane/lane-reap.ts +20 -5
  144. package/scripts/lane/lane-registry.ts +6 -0
  145. package/scripts/lane/lane-restart-cli.ts +51 -0
  146. package/scripts/lane/lane-restart.ts +175 -0
  147. package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
  148. package/scripts/lane/lane-spent.ts +195 -0
  149. package/scripts/lane/lane-stage-stats.ts +27 -3
  150. package/scripts/lane/lane-stop-stats.ts +96 -0
  151. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  152. package/scripts/lane/process-tools.ts +31 -0
  153. package/scripts/lib/buffered-process.ts +36 -2
  154. package/scripts/lib/pnpm-reach.ts +27 -0
  155. package/scripts/lib/status-icons.ts +5 -1
  156. package/scripts/lint/lint-parallel.ts +19 -2
  157. package/scripts/observations/observation-ledger-home.ts +1 -0
  158. package/scripts/review/review-attest-cli.ts +21 -6
  159. package/scripts/review/review-brief-cli.ts +2 -2
  160. package/scripts/review/review-comment-round.ts +114 -0
  161. package/scripts/review/review-record-cli.ts +39 -17
  162. package/scripts/rules/implementation-cut.ts +3 -3
  163. package/scripts/rules/pre-gate-cut.ts +1 -1
  164. package/scripts/run/board/run-board-checks.ts +42 -0
  165. package/scripts/run/board/run-board-cli.ts +2 -0
  166. package/scripts/run/board/run-board-every.ts +3 -1
  167. package/scripts/run/board/run-board-fit.ts +16 -5
  168. package/scripts/run/board/run-board-fresh.ts +64 -0
  169. package/scripts/run/board/run-board-header.ts +139 -37
  170. package/scripts/run/board/run-board-labels.ts +47 -4
  171. package/scripts/run/board/run-board-lane-event.ts +66 -0
  172. package/scripts/run/board/run-board-layout.ts +57 -9
  173. package/scripts/run/board/run-board-now.ts +234 -0
  174. package/scripts/run/board/run-board-operation-read.ts +75 -0
  175. package/scripts/run/board/run-board-operation.ts +150 -0
  176. package/scripts/run/board/run-board-phase.ts +26 -1
  177. package/scripts/run/board/run-board-plan.ts +4 -6
  178. package/scripts/run/board/run-board-read.ts +58 -5
  179. package/scripts/run/board/run-board-reason.ts +124 -0
  180. package/scripts/run/board/run-board-render-checks.ts +55 -0
  181. package/scripts/run/board/run-board-render-legend.ts +12 -3
  182. package/scripts/run/board/run-board-render-slot.ts +2 -2
  183. package/scripts/run/board/run-board-render-tail.ts +128 -0
  184. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  185. package/scripts/run/board/run-board-render-title.ts +58 -0
  186. package/scripts/run/board/run-board-render.ts +96 -80
  187. package/scripts/run/board/run-board-row-yield.ts +32 -0
  188. package/scripts/run/board/run-board-setup.ts +121 -0
  189. package/scripts/run/board/run-board-span.ts +49 -7
  190. package/scripts/run/board/run-board-state.ts +10 -0
  191. package/scripts/run/board/run-board-status.ts +76 -13
  192. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  193. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  194. package/scripts/run/board/run-board-thanks.ts +53 -0
  195. package/scripts/run/board/run-board-tick.ts +53 -16
  196. package/scripts/run/board/run-board-usual.ts +44 -0
  197. package/scripts/run/carry/run-carry-added.ts +1 -1
  198. package/scripts/run/carry/run-carry-args.ts +9 -1
  199. package/scripts/run/carry/run-carry-change.ts +1 -1
  200. package/scripts/run/carry/run-carry-cli.ts +39 -52
  201. package/scripts/run/carry/run-carry-end.ts +73 -0
  202. package/scripts/run/carry/run-carry-ended.ts +9 -1
  203. package/scripts/run/carry/run-carry.ts +24 -1
  204. package/scripts/run/entry/run-entry-cli.ts +5 -0
  205. package/scripts/run/event/run-event-checks.ts +54 -0
  206. package/scripts/run/event/run-event-render.ts +3 -0
  207. package/scripts/run/event/run-event-scope.ts +1 -0
  208. package/scripts/run/event/run-event-setup.ts +91 -0
  209. package/scripts/run/event/run-event-stream.ts +46 -9
  210. package/scripts/run/merge/run-merge-cli.ts +7 -4
  211. package/scripts/run/merge/run-merge-ship.ts +61 -0
  212. package/scripts/run/merge/run-merge-steps.ts +7 -14
  213. package/scripts/run/merge/run-merge-token.ts +16 -2
  214. package/scripts/run/run-headless.ts +1 -0
  215. package/scripts/run/run-step.ts +2 -0
  216. package/scripts/run/run-stranded-detect.ts +58 -12
  217. package/scripts/run/ship/run-ship-cli.ts +22 -2
  218. package/scripts/run/ship/run-ship-return.ts +31 -13
  219. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  220. package/scripts/run/ship/run-ship-stage.ts +1 -0
  221. package/scripts/run/wake/run-wake-await.ts +130 -0
  222. package/scripts/run/wake/run-wake-cli.ts +108 -74
  223. package/scripts/run/wake/run-wake-describe.ts +36 -2
  224. package/scripts/run/wake/run-wake-driver.ts +21 -5
  225. package/scripts/run/wake/run-wake-handoff.ts +1 -1
  226. package/scripts/run/wake/run-wake-session.ts +20 -3
  227. package/scripts/run/wake/run-wake-stop.ts +37 -0
  228. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  229. package/scripts/sync/claude-settings-merge.ts +144 -0
  230. package/scripts/sync/sync-ai-files.ts +28 -3
  231. package/scripts/sync/sync.ts +15 -0
  232. package/scripts/test/project-test-script.ts +73 -0
  233. package/scripts/test/skip-marker.ts +14 -1
  234. package/scripts/test/test-related.ts +3 -2
  235. package/scripts/test/test-unit-guard.ts +78 -14
  236. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  237. package/scripts/uninstall/distributed-files.ts +92 -0
  238. package/scripts/uninstall/uninstall-plan.ts +119 -0
  239. package/scripts/uninstall/uninstall-report.ts +61 -0
  240. package/scripts/uninstall/uninstall.ts +206 -0
  241. package/templates/AGENTS.basic.md +4 -0
  242. package/templates/GEMINI.basic.md +6 -0
  243. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  244. package/scripts/run/board/run-board-track.ts +0 -22
@@ -3,10 +3,14 @@ import {
3
3
  IN_PROGRESS_LABEL,
4
4
  NEEDS_DECISION_LABEL,
5
5
  } from '#scripts/issue/issue-labels'
6
+ import type { PrChecks } from '#scripts/run/event/run-event-checks'
6
7
  import { run_event_scope } from '#scripts/run/event/run-event-scope'
7
8
  import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
9
+ import { run_board_checks } from './run-board-checks'
8
10
  import type { ClosedIssue } from './run-board-closed'
11
+ import { run_board_lane_event, type LaneEvent } from './run-board-lane-event'
9
12
  import { run_board_phase, type Phase } from './run-board-phase'
13
+ import { run_board_setup, type SetupRead } from './run-board-setup'
10
14
 
11
15
  // What each issue of a run is doing now, read from the run's own event stream and its open lanes.
12
16
  // Local reads only — no `gh` call — so `run:board` can redraw it every few seconds; the plan these
@@ -24,11 +28,29 @@ interface ItemStatus {
24
28
  lane?: string | undefined
25
29
  // Every phase the child passed while running, in order (`run-board-phase.ts`) — kept once it settles.
26
30
  track?: ReadonlyArray<Phase> | undefined
31
+ // When a running child entered the phase its track ends on — the event that moved it there.
32
+ phase_ms?: number | undefined
27
33
  }
28
34
 
29
- // The run as a whole, read off the same stream.
30
- interface RunActivity {
35
+ interface LatestEvent {
36
+ text: string
37
+ at_ms: number
38
+ }
39
+
40
+ // The run as a whole, read off the same stream. The setup half is `run-board-setup.ts`'s, absent
41
+ // where a stream carries no stage.
42
+ interface RunActivity extends Partial<SetupRead> {
31
43
  last_event_ms: number | undefined
44
+ // The newest thing the run did — its newest position, or its newest event where it has none yet —
45
+ // in the event's own words, and when.
46
+ latest?: LatestEvent | undefined
47
+ // When the run last did anything of its own — its newest position or stage, whichever is newer. A
48
+ // stage that has ended still counts: what follows it is counted from its end, not from before it.
49
+ idle_ms?: number | undefined
50
+ // Each child's newest `lane-phase` or `ship-stage`, by issue (`run-board-lane-event.ts`).
51
+ lane_events?: ReadonlyMap<number, LaneEvent> | undefined
52
+ // Each child's newest `pr-checks`, by issue (`run-board-checks.ts`).
53
+ pr_checks?: ReadonlyMap<number, PrChecks> | undefined
32
54
  // The run's last position is a `stop` that names no child — the run itself ended.
33
55
  is_stopped: boolean
34
56
  }
@@ -42,13 +64,30 @@ const ENDING_STATES: Readonly<Partial<Record<string, ItemState>>> = {
42
64
  }
43
65
  // A cut or a resume says the child is still running, on whatever phase it had reached.
44
66
  const CONTINUING_KINDS: ReadonlySet<string> = new Set([KIND.CUT, KIND.RESUME])
67
+ // A person's restart starts the child over in a fresh session, so its track starts over too.
68
+ const LAUNCH_KINDS: ReadonlySet<string> = new Set([KIND.CHILD_LAUNCH, KIND.RESTART])
45
69
 
46
70
  function moment(event: RunEvent): number {
47
71
  return Date.parse(event.at)
48
72
  }
49
73
 
50
74
  function launched(event: RunEvent): ItemStatus {
51
- return { state: 'running', started_ms: moment(event), track: [run_board_phase.LAUNCHED_PHASE] }
75
+ const at_ms = moment(event)
76
+
77
+ return {
78
+ state: 'running',
79
+ started_ms: at_ms,
80
+ track: [run_board_phase.LAUNCHED_PHASE],
81
+ phase_ms: at_ms,
82
+ }
83
+ }
84
+
85
+ // The status with its track moved on by `event`. The phase's start moves only where the phase the
86
+ // track ends on changes, so a second session's first edit does not restart the time of its phase.
87
+ function moved(previous: ItemStatus, track: ReadonlyArray<Phase>, event: RunEvent): ItemStatus {
88
+ const is_same = previous.phase_ms !== undefined && track.at(-1) === previous.track?.at(-1)
89
+
90
+ return { ...previous, track, phase_ms: is_same ? previous.phase_ms : moment(event) }
52
91
  }
53
92
 
54
93
  // The status a child settles into: its start and the track it had reached, so a settled row still
@@ -64,7 +103,9 @@ function ended(previous: ItemStatus | undefined, state: ItemState, event: RunEve
64
103
  }
65
104
 
66
105
  function continued(previous: ItemStatus | undefined): ItemStatus {
67
- return { state: 'running', started_ms: previous?.started_ms, track: previous?.track }
106
+ const { started_ms, track, phase_ms } = previous ?? {}
107
+
108
+ return { state: 'running', started_ms, track, phase_ms }
68
109
  }
69
110
 
70
111
  // A stage line carrying its attempt redraws the track of any child launched on this stream — a settled
@@ -73,11 +114,12 @@ function continued(previous: ItemStatus | undefined): ItemStatus {
73
114
  function replayed(
74
115
  previous: ItemStatus | undefined,
75
116
  attempt: ReadonlyArray<Phase>,
117
+ event: RunEvent,
76
118
  ): ItemStatus | undefined {
77
119
  if (previous === undefined) return previous
78
120
  if (previous.track === undefined && previous.state !== 'running') return previous
79
121
 
80
- return { ...previous, track: run_board_phase.replayed(previous.track ?? [], attempt) }
122
+ return moved(previous, run_board_phase.replayed(previous.track ?? [], attempt), event)
81
123
  }
82
124
 
83
125
  // A phase moves only a running child, so a three-word `report` stage after its merge — or a `plan`
@@ -87,18 +129,18 @@ function phased(previous: ItemStatus | undefined, event: RunEvent): ItemStatus |
87
129
 
88
130
  if (phase === undefined || previous?.state !== 'running') return previous
89
131
 
90
- return { ...previous, track: run_board_phase.history_after(previous.track ?? [], phase) }
132
+ return moved(previous, run_board_phase.history_after(previous.track ?? [], phase), event)
91
133
  }
92
134
 
93
135
  function advanced(previous: ItemStatus | undefined, event: RunEvent): ItemStatus | undefined {
94
136
  const attempt = run_board_phase.attempt_of(event)
95
137
 
96
- return attempt === undefined ? phased(previous, event) : replayed(previous, attempt)
138
+ return attempt === undefined ? phased(previous, event) : replayed(previous, attempt, event)
97
139
  }
98
140
 
99
141
  // One event folded into the status of the child it names; an event the board does not track keeps it.
100
142
  function next_status(previous: ItemStatus | undefined, event: RunEvent): ItemStatus | undefined {
101
- if (event.kind === KIND.CHILD_LAUNCH) return launched(event)
143
+ if (LAUNCH_KINDS.has(event.kind)) return launched(event)
102
144
 
103
145
  const ending = ENDING_STATES[event.kind]
104
146
 
@@ -125,16 +167,17 @@ function fold_events(events: ReadonlyArray<RunEvent>): Map<number, ItemStatus> {
125
167
  function is_running_lane(
126
168
  issue: number,
127
169
  status: ItemStatus | undefined,
170
+ events: ReadonlyArray<RunEvent>,
128
171
  in_run: ReadonlySet<number>,
129
172
  ): boolean {
130
- if (status === undefined) return in_run.has(issue)
173
+ if (status !== undefined) return status.state === 'running'
131
174
 
132
- return status.state === 'running'
175
+ return in_run.has(issue) && !run_board_setup.is_lane_pending(events, issue)
133
176
  }
134
177
 
135
178
  // Every child the run has touched, with the lane each running child holds. A lane the stream never saw
136
179
  // launched — a run restored after the stream rolled — still reads as running while its issue is in the
137
- // run's plan (`in_run`).
180
+ // run's plan (`in_run`); one the run is still setting up for stays waiting until its launch.
138
181
  function statuses_of(
139
182
  events: ReadonlyArray<RunEvent>,
140
183
  lanes: ReadonlyArray<string>,
@@ -146,7 +189,7 @@ function statuses_of(
146
189
  const issue = Number(lane)
147
190
  const status = statuses.get(issue)
148
191
 
149
- if (is_running_lane(issue, status, in_run)) {
192
+ if (is_running_lane(issue, status, events, in_run)) {
150
193
  statuses.set(issue, { state: 'running', ...status, lane: `lane ${lane}` })
151
194
  }
152
195
  }
@@ -244,6 +287,21 @@ function is_run_stop(event: RunEvent | undefined): boolean {
244
287
  return event?.kind === KIND.STOP && run_event_scope.issue_named(event) === undefined
245
288
  }
246
289
 
290
+ function latest_of(event: RunEvent | undefined): LatestEvent | undefined {
291
+ return event === undefined ? undefined : { text: event.text, at_ms: moment(event) }
292
+ }
293
+
294
+ function is_own(event: RunEvent): boolean {
295
+ return event.kind === KIND.SETUP || !run_event_stream.TRACE_KINDS.has(event.kind)
296
+ }
297
+
298
+ // A stream of traces alone has done nothing of its own yet, so its newest event stands in.
299
+ function idle_of(events: ReadonlyArray<RunEvent>): number | undefined {
300
+ const own = events.findLast((event) => is_own(event)) ?? events.at(-1)
301
+
302
+ return own === undefined ? undefined : moment(own)
303
+ }
304
+
247
305
  // `is_ended` is a run `run:carry --end` closed: its record says it stopped, whatever its last event
248
306
  // was.
249
307
  function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActivity {
@@ -251,7 +309,12 @@ function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActi
251
309
  const position = events.findLast((event) => !run_event_stream.TRACE_KINDS.has(event.kind))
252
310
 
253
311
  return {
312
+ ...run_board_setup.read(events),
254
313
  last_event_ms: last === undefined ? undefined : moment(last),
314
+ latest: latest_of(position ?? last),
315
+ idle_ms: idle_of(events),
316
+ lane_events: run_board_lane_event.newest_of(events),
317
+ pr_checks: run_board_checks.newest_of(events),
255
318
  is_stopped: is_ended || is_run_stop(position),
256
319
  }
257
320
  }
@@ -259,4 +322,4 @@ function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActi
259
322
  const run_board_status = { activity_of, settle_closed, statuses_of }
260
323
 
261
324
  export { run_board_status }
262
- export type { ItemState, ItemStatus, OpenRead, RunActivity }
325
+ export type { ItemState, ItemStatus, LatestEvent, OpenRead, RunActivity }
@@ -0,0 +1,71 @@
1
+ import { parse } from 'csv-parse/sync'
2
+ import { run_board_render_title } from './run-board-render-title'
3
+
4
+ // The reviewers who helped kit, read off the published roster's CSV. The roster is a spreadsheet anyone
5
+ // with edit access changes, so nothing in it is trusted as drawn: a sheet whose header is not `name,url`
6
+ // is not the roster, a row without a name or with a link that is not `https://` is dropped, and a name
7
+ // loses every control character — an escape in a cell would otherwise be written to the terminal as one.
8
+
9
+ const HEADER = ['name', 'url'] as const
10
+ // Enough for any person's or project's name, short enough that one cell cannot fill the line; a
11
+ // longer one is cut to an ellipsis, as a row's title is.
12
+ const NAME_LIMIT = 40
13
+ const CONTROL = /\p{Cc}/gu
14
+ const SAFE_URL = /^https:\/\/[^\s\p{Cc}]+$/u
15
+
16
+ interface Thanker {
17
+ name: string
18
+ // The page the name opens; `undefined` draws the name unlinked.
19
+ url: string | undefined
20
+ }
21
+
22
+ type Thanks = ReadonlyArray<Thanker>
23
+
24
+ function name_of(cell: string): string {
25
+ return run_board_render_title.title_of(cell.replaceAll(CONTROL, '').trim(), NAME_LIMIT)
26
+ }
27
+
28
+ // An empty cell is a name without a link; any other value has to be a whole `https://` URL.
29
+ function url_of(cell: string): string | undefined | false {
30
+ const url = cell.trim()
31
+
32
+ if (url === '') return undefined
33
+
34
+ return SAFE_URL.test(url) && URL.canParse(url) ? url : false
35
+ }
36
+
37
+ function thanker_of(row: ReadonlyArray<string>): Array<Thanker> {
38
+ const [name_cell = '', url_cell = ''] = row
39
+ const name = name_of(name_cell)
40
+ const url = url_of(url_cell)
41
+
42
+ if (name === '' || url === false) return []
43
+
44
+ return [{ name, url }]
45
+ }
46
+
47
+ function is_roster(header: ReadonlyArray<string> | undefined): boolean {
48
+ return header?.length === HEADER.length && HEADER.every((cell, index) => header[index] === cell)
49
+ }
50
+
51
+ function rows_of(csv: string): Array<Array<string>> | undefined {
52
+ try {
53
+ return parse(csv, { relax_column_count: true, skip_empty_lines: true, bom: true })
54
+ } catch {
55
+ return undefined
56
+ }
57
+ }
58
+
59
+ // `undefined` for a text that is not the roster, so a failed read is told apart from an empty roster.
60
+ function thanks_of(csv: string): Thanks | undefined {
61
+ const [header, ...rows] = rows_of(csv) ?? []
62
+
63
+ if (!is_roster(header)) return undefined
64
+
65
+ return rows.flatMap((row) => thanker_of(row))
66
+ }
67
+
68
+ const run_board_thanks_csv = { NAME_LIMIT, thanks_of }
69
+
70
+ export { run_board_thanks_csv }
71
+ export type { Thanker, Thanks }
@@ -0,0 +1,112 @@
1
+ import { PROJECT_ROOT } from '#scripts/init/init-paths'
2
+ import { stamp_file } from '#scripts/josh/stamp-file'
3
+ import { json_value } from '#scripts/lib/json-value'
4
+ import { timed_fetch } from '#scripts/lib/timed-fetch'
5
+ import { run_progress } from '#scripts/run/progress/run-progress'
6
+ import { z } from 'zod'
7
+ import { run_board_thanks_csv, type Thanks } from './run-board-thanks-csv'
8
+
9
+ // The roster `run:board` thanks its reviewers from: a Google spreadsheet published as CSV, changed
10
+ // apart from any build or release, so it is read at run time. Its answer is kept in the project's
11
+ // record beside the board's other state and reused for five minutes — Google's own cache holds it as
12
+ // long — so neither a redraw nor a fresh `--once` asks again inside that window. A read that fails
13
+ // keeps the last answer, and with none draws no line; nothing about the failure reaches the screen.
14
+
15
+ const ROSTER_URL =
16
+ 'https://docs.google.com/spreadsheets/d/e/2PACX-1vR8Ia38Wiw6Kfyk9Or594J8ZRS8okynV30CpOtq5B6_GyJG7A1-t8Au64jTYvmzgIOlbafcKKgrNFVJ/pub?gid=0&single=true&output=csv'
17
+ const RECORD_PREFIX = 'josh-run-board-thanks-'
18
+ const TTL_MINUTES = 5
19
+ const TTL_MS = TTL_MINUTES * run_progress.MS_PER_MINUTE
20
+
21
+ const thanker_schema = z.object({ name: z.string(), url: z.string().optional() })
22
+ const record_schema = z.object({ checked_ms: z.number(), thanks: z.array(thanker_schema) })
23
+
24
+ interface ThanksRecord {
25
+ checked_ms: number
26
+ thanks: Thanks
27
+ }
28
+
29
+ // What the read works through, injected in tests.
30
+ interface ThanksDependencies {
31
+ fetch_text: () => Promise<string>
32
+ record_path: string
33
+ now: () => number
34
+ }
35
+
36
+ function read_record(record_path: string): ThanksRecord | undefined {
37
+ const raw = stamp_file.read_stamp_text(record_path)
38
+ const parsed = raw === undefined ? undefined : json_value.parse_with(raw, record_schema)
39
+
40
+ if (parsed === undefined) return undefined
41
+
42
+ return {
43
+ checked_ms: parsed.checked_ms,
44
+ thanks: parsed.thanks.map(({ name, url }) => ({ name, url })),
45
+ }
46
+ }
47
+
48
+ // A record that cannot be written leaves the next read to ask again — never a reason to fail the board.
49
+ function write_record(record_path: string, record: ThanksRecord): void {
50
+ try {
51
+ stamp_file.replace_stamp(record_path, record)
52
+ } catch {
53
+ // The board draws what it read either way.
54
+ }
55
+ }
56
+
57
+ async function fetch_roster(read: () => Promise<string>): Promise<Thanks | undefined> {
58
+ try {
59
+ return run_board_thanks_csv.thanks_of(await read())
60
+ } catch {
61
+ return undefined
62
+ }
63
+ }
64
+
65
+ // The recorded roster while it is younger than the record's life; `undefined` once it is stale. A
66
+ // record dated after now — the clock stepped back — is stale too, or it would hold until the clock
67
+ // caught up with it.
68
+ function fresh_thanks(record: ThanksRecord | undefined, now_ms: number): Thanks | undefined {
69
+ if (record === undefined) return undefined
70
+
71
+ const age_ms = now_ms - record.checked_ms
72
+
73
+ return age_ms >= 0 && age_ms < TTL_MS ? record.thanks : undefined
74
+ }
75
+
76
+ // A response that is not a success is a failed read, so its body is never taken for the roster.
77
+ async function fetch_text(): Promise<string> {
78
+ const response = await timed_fetch(ROSTER_URL)
79
+
80
+ if (!response.ok) throw new Error(`roster read answered ${String(response.status)}`)
81
+
82
+ return await response.text()
83
+ }
84
+
85
+ const LIVE_DEPS: ThanksDependencies = {
86
+ fetch_text,
87
+ record_path: stamp_file.stamp_path(RECORD_PREFIX, PROJECT_ROOT),
88
+ now: () => Date.now(),
89
+ }
90
+
91
+ async function read_with(deps: ThanksDependencies): Promise<Thanks> {
92
+ const now_ms = deps.now()
93
+ const record = read_record(deps.record_path)
94
+ const fresh = fresh_thanks(record, now_ms)
95
+
96
+ if (fresh !== undefined) return fresh
97
+
98
+ const thanks = (await fetch_roster(deps.fetch_text)) ?? record?.thanks ?? []
99
+
100
+ write_record(deps.record_path, { checked_ms: now_ms, thanks })
101
+
102
+ return thanks
103
+ }
104
+
105
+ async function read_thanks(deps: ThanksDependencies = LIVE_DEPS): Promise<Thanks> {
106
+ return await read_with(deps)
107
+ }
108
+
109
+ const run_board_thanks_read = { TTL_MS, read_thanks }
110
+
111
+ export { run_board_thanks_read }
112
+ export type { ThanksDependencies }
@@ -0,0 +1,53 @@
1
+ import { run_board_fetch, type Fetch } from './run-board-fetch'
2
+ import { run_board_state, type BoardPorts, type BoardState, type Pace } from './run-board-state'
3
+ import type { Thanks } from './run-board-thanks-csv'
4
+ import { run_board_thanks_read } from './run-board-thanks-read'
5
+
6
+ // The reviewers' roster a `run:board` redraw draws above its legend. Like the GitHub reads
7
+ // (`run-board-github.ts`), **a live board never waits on it**: the read is launched and folded in on
8
+ // the redraw after it lands; one plain frame waits for it. It is launched again no sooner than the
9
+ // roster's own record goes stale. A board with no `read_thanks` port — the chat frame — reads none and
10
+ // draws none.
11
+
12
+ const { TTL_MS } = run_board_thanks_read
13
+ const { is_due } = run_board_state
14
+
15
+ type ThanksState = Pick<BoardState, 'thanks' | 'thanks_fetch' | 'thanks_attempted_ms'>
16
+
17
+ function launch_thanks(state: BoardState, ports: BoardPorts, now_ms: number): BoardState {
18
+ const { read_thanks } = ports
19
+
20
+ if (read_thanks === undefined || state.thanks_fetch !== undefined) return state
21
+ if (!is_due(state.thanks_attempted_ms, TTL_MS, now_ms)) return state
22
+
23
+ const thanks_fetch: Fetch<Thanks> = run_board_fetch.launch(read_thanks, state.thanks)
24
+
25
+ return { ...state, thanks_fetch, thanks_attempted_ms: now_ms }
26
+ }
27
+
28
+ function fold_thanks(state: BoardState): BoardState {
29
+ const answer = state.thanks_fetch?.answer()
30
+
31
+ if (answer === undefined) return state
32
+
33
+ return { ...state, thanks: answer.value, thanks_fetch: undefined }
34
+ }
35
+
36
+ // Folds what landed and launches what is due; at the `settled` pace, waits for it to land.
37
+ async function gather_thanks(
38
+ state: BoardState,
39
+ ports: BoardPorts,
40
+ now_ms: number,
41
+ pace: Pace,
42
+ ): Promise<BoardState> {
43
+ const launched = launch_thanks(fold_thanks(state), ports, now_ms)
44
+
45
+ if (pace === 'settled' && launched.thanks_fetch !== undefined) await launched.thanks_fetch.landed
46
+
47
+ return fold_thanks(launched)
48
+ }
49
+
50
+ const run_board_thanks = { gather_thanks }
51
+
52
+ export { run_board_thanks }
53
+ export type { ThanksState }
@@ -8,10 +8,12 @@ import { run_board_link } from './run-board-link'
8
8
  import { run_board_machine } from './run-board-machine'
9
9
  import { run_board_notes, type Titles } from './run-board-notes'
10
10
  import type { LocalRead } from './run-board-read'
11
+ import { run_board_reason } from './run-board-reason'
11
12
  import { run_board_render, type FrameBounds } from './run-board-render'
12
13
  import { run_board_spin, type Spun } from './run-board-spin'
13
14
  import { run_board_state, type BoardPorts, type BoardState, type Pace } from './run-board-state'
14
- import { run_board_status } from './run-board-status'
15
+ import { run_board_status, type RunActivity } from './run-board-status'
16
+ import { run_board_thanks } from './run-board-thanks'
15
17
  import { run_board_usage } from './run-board-usage'
16
18
 
17
19
  // One `run:board` redraw. **Three speeds**: the frame is redrawn once a second, on the second, so
@@ -30,6 +32,7 @@ const LOCAL_READ_SECONDS = 5
30
32
  const LOCAL_READ_MS = LOCAL_READ_SECONDS * run_progress.MS_PER_SECOND
31
33
  const { FRESH_STATE, is_due } = run_board_state
32
34
  const { PLAN_RETRY_MS, gather_github, statuses_for } = run_board_github
35
+ const { gather_thanks } = run_board_thanks
33
36
 
34
37
  async function reread(state: BoardState, ports: BoardPorts, now_ms: number): Promise<BoardState> {
35
38
  if (!is_due(state.read_ms, LOCAL_READ_MS, now_ms)) return state
@@ -88,17 +91,17 @@ function titles_of(state: BoardState): Titles {
88
91
  return with_closed(state.plan, state.closed).context.titles
89
92
  }
90
93
 
91
- // The statuses scoped to the run's plan, a running child the plan's listing no longer holds settled.
92
- // Before the plan is read, the run's own children alone.
93
- function layout_for(state: BoardState, local: LocalRead): BoardLayout {
94
- const { plan, closed } = state
95
-
96
- if (plan === undefined) {
97
- return run_board_layout.local_layout_of(
98
- run_board_status.statuses_of(local.events, local.lanes, new Set()),
99
- )
100
- }
94
+ // A run `run:carry --end` closed and one nobody drives any more are both drawn as stopped.
95
+ function activity_here(local: LocalRead): RunActivity {
96
+ return run_board_status.activity_of(
97
+ local.events,
98
+ local.ended_ms !== undefined || local.is_stranded,
99
+ )
100
+ }
101
101
 
102
+ // The plan laid under its statuses, each waiting row saying why it waits.
103
+ function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): BoardLayout {
104
+ const { closed } = state
102
105
  const read = {
103
106
  open_numbers: plan.context.open_numbers,
104
107
  read_ms: state.fetched_ms,
@@ -106,8 +109,25 @@ function layout_for(state: BoardState, local: LocalRead): BoardLayout {
106
109
  labels: plan.labels,
107
110
  }
108
111
  const statuses = run_board_status.settle_closed(statuses_for(plan, local), read)
112
+ const reason_of = run_board_reason.reader({
113
+ plan,
114
+ statuses,
115
+ events: local.events,
116
+ free_lanes: local.free_lanes,
117
+ is_stopped: activity_here(local).is_stopped,
118
+ })
119
+
120
+ return run_board_layout.layout_of(with_closed(plan, closed), statuses, reason_of)
121
+ }
122
+
123
+ // The statuses scoped to the run's plan, a running child the plan's listing no longer holds settled.
124
+ // Before the plan is read, the run's own children alone.
125
+ function layout_for(state: BoardState, local: LocalRead): BoardLayout {
126
+ if (state.plan !== undefined) return planned_layout(state, state.plan, local)
109
127
 
110
- return run_board_layout.layout_of(with_closed(plan, closed), statuses)
128
+ return run_board_layout.local_layout_of(
129
+ run_board_status.statuses_of(local.events, local.lanes, new Set()),
130
+ )
111
131
  }
112
132
 
113
133
  // What one redraw draws with.
@@ -116,6 +136,22 @@ interface Redraw {
116
136
  now_ms: number
117
137
  }
118
138
 
139
+ // What the header reads off the local read.
140
+ type LocalHeader = Pick<
141
+ BoardHeader,
142
+ 'started_ms' | 'ended_ms' | 'activity' | 'operations' | 'usual'
143
+ >
144
+
145
+ function local_header(local: LocalRead): LocalHeader {
146
+ return {
147
+ started_ms: local.started_ms,
148
+ ended_ms: local.ended_ms,
149
+ activity: activity_here(local),
150
+ operations: local.operations,
151
+ usual: local.usual,
152
+ }
153
+ }
154
+
119
155
  function header_of(
120
156
  state: BoardState,
121
157
  local: LocalRead,
@@ -124,9 +160,7 @@ function header_of(
124
160
  ): BoardHeader {
125
161
  return {
126
162
  now_ms: redraw.now_ms,
127
- started_ms: local.started_ms,
128
- ended_ms: local.ended_ms,
129
- activity: run_board_status.activity_of(local.events, local.ended_ms !== undefined),
163
+ ...local_header(local),
130
164
  layout,
131
165
  baseline_total: state.baseline_total,
132
166
  plan_fetched_ms: state.fetched_ms,
@@ -153,6 +187,8 @@ function frame_of(state: BoardState, local: LocalRead, layout: BoardLayout, redr
153
187
  header: header_of(state, local, layout, redraw),
154
188
  notes: run_board_notes.notes_of(local.events, titles_of(state)),
155
189
  resume: local.resume,
190
+ thanks: state.thanks,
191
+ link: redraw.ports.link,
156
192
  ...bounds,
157
193
  })
158
194
 
@@ -195,8 +231,9 @@ async function tick(
195
231
  }
196
232
 
197
233
  const gathered = await gather_github(state_for(read, local), { ports, local, now_ms, pace })
234
+ const thanked = await gather_thanks(gathered, ports, now_ms, pace)
198
235
 
199
- return draw_run(await resample(gathered, ports, now_ms), local, { ports, now_ms })
236
+ return draw_run(await resample(thanked, ports, now_ms), local, { ports, now_ms })
200
237
  }
201
238
 
202
239
  const run_board_tick = {
@@ -0,0 +1,44 @@
1
+ import { lane_ledger, type LedgerEntry } from '#scripts/lane/lane-ledger'
2
+ import { lane_stage_stats } from '#scripts/lane/lane-stage-stats'
3
+ import { run_board_phase, type Phase } from './run-board-phase'
4
+
5
+ // How long each phase usually takes: the lane ledger's median of the stage the phase is
6
+ // (`lane-stage-stats.ts`), so a running child that has stood at its phase far longer stands out
7
+ // (joshuafolkken/kit#3769). A phase is a `josh ship` stage under the names `run-board-phase.ts` gives
8
+ // them, and `implement` is the ledger's derived one. A phase no stage records — `investigate`, `plan`,
9
+ // `failed` — has no usual time, and is never drawn as long.
10
+
11
+ type UsualTimes = ReadonlyMap<Phase, number>
12
+
13
+ const IMPLEMENT_PHASE: Phase = 'implement'
14
+
15
+ // Each phase's ledger stage, from the stage-to-phase names a row's phase is drawn with. The ledger's
16
+ // `implement` runs from the dispatch, so it also holds the investigation and the plan the row draws
17
+ // before its `implement` phase: the usual time is the longer, which can only draw a phase as long
18
+ // later, never sooner.
19
+ const PHASE_STAGES: ReadonlyArray<readonly [string, Phase | undefined]> = [
20
+ [lane_stage_stats.IMPLEMENT, IMPLEMENT_PHASE],
21
+ ...Object.entries(run_board_phase.SHIP_PHASES),
22
+ ]
23
+
24
+ function usual_of(entries: ReadonlyArray<LedgerEntry>): UsualTimes {
25
+ const medians = lane_stage_stats.medians(entries)
26
+
27
+ return new Map(
28
+ PHASE_STAGES.flatMap(([stage, phase]) => {
29
+ const median = medians.get(stage)
30
+
31
+ return phase === undefined || median === undefined ? [] : [[phase, median] as const]
32
+ }),
33
+ )
34
+ }
35
+
36
+ // The repository's whole ledger, read where the board reads its stream; an absent one has no usual time.
37
+ function read(repository: string): UsualTimes {
38
+ return usual_of(lane_ledger.read_entries(lane_ledger.target_of(repository)))
39
+ }
40
+
41
+ const run_board_usual = { read, usual_of }
42
+
43
+ export { run_board_usual }
44
+ export type { UsualTimes }
@@ -51,7 +51,7 @@ function add_issues(
51
51
  ): RunCarry {
52
52
  const next: RunCarry = { ...carry, added: next_added(carry, additions) }
53
53
 
54
- stamp_file.write_stamp(target, next)
54
+ stamp_file.replace_stamp(target, next)
55
55
 
56
56
  return next
57
57
  }
@@ -1,5 +1,6 @@
1
1
  import { issue_number_shape } from '#scripts/issue/issue-number-shape'
2
2
  import { cli_flags } from '#scripts/lib/cli-flags'
3
+ import { run_headless } from '#scripts/run/run-headless'
3
4
  import { run_invocation } from '#scripts/run/run-invocation'
4
5
  import { run_carry, type CarryChange, type CarryClaimRequest, type CarryOwner } from './run-carry'
5
6
 
@@ -178,6 +179,13 @@ function group_count(values: ParsedValues): number {
178
179
  return groups.filter(Boolean).length
179
180
  }
180
181
 
182
+ // The mark rides the environment the run launched its session with; the record keeps it beside the
183
+ // owner, so `josh backlogrun --stop` knows that owner for the run's own. An attached owner is left as
184
+ // it is, so its record is the one it always was.
185
+ function marked(owner: CarryOwner): CarryOwner {
186
+ return run_headless.is_headless_parent(process.env) ? { ...owner, is_headless: true } : owner
187
+ }
188
+
181
189
  // **`--owner` is what makes liveness answerable at all.** The process that runs this command is a
182
190
  // short-lived `tsx`, so its own pid says nothing about whether the run is still going; the caller
183
191
  // names the process that is — its own session — and a `backlogrun` passes `--owner "$PPID"`. Absent,
@@ -192,7 +200,7 @@ function to_owner(value: OptionValue): CarryOwner | undefined {
192
200
 
193
201
  const pid = Number(text)
194
202
 
195
- return pid < MIN_PID ? undefined : run_carry.owner_of(pid)
203
+ return pid < MIN_PID ? undefined : marked(run_carry.owner_of(pid))
196
204
  }
197
205
 
198
206
  // A non-empty `--summary`, or nothing. An empty string is nothing here, so it reads as absent and the
@@ -83,7 +83,7 @@ function apply_change(
83
83
  retrospective: effective.retrospective === true || carry.retrospective,
84
84
  }
85
85
 
86
- stamp_file.write_stamp(target, next)
86
+ stamp_file.replace_stamp(target, next)
87
87
 
88
88
  return next
89
89
  }