@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
@@ -1,22 +1,25 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { randomUUID } from 'node:crypto'
3
- import path from 'node:path'
4
3
  import { fileURLToPath } from 'node:url'
5
- import type { AgentProfile } from '#scripts/agent/agent-role-profile'
4
+ import type { AgentProfile, AgentProvider } from '#scripts/agent/agent-role-profile'
6
5
  import { backlog_budget } from '#scripts/backlog/backlog-budget'
7
6
  import { cli_flags } from '#scripts/lib/cli-flags'
8
- import { poll } from '#scripts/lib/poll'
9
7
  import { telegram_notify } from '#scripts/notify/telegram-notify'
10
8
  import { run_carry, type CarryRead } from '#scripts/run/carry/run-carry'
9
+ import { run_event_setup } from '#scripts/run/event/run-event-setup'
11
10
  import { run_event_stream } from '#scripts/run/event/run-event-stream'
11
+ import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
12
12
  import { run_headless } from '#scripts/run/run-headless'
13
- import { run_wake, type RunWake, type WakeStopReason, type WakeTidyResult } from './run-wake'
13
+ import { run_stranded_detect, type WatchedRun } from '#scripts/run/run-stranded-detect'
14
+ import { run_wake, type WakeStopReason, type WakeTidyResult } from './run-wake'
15
+ import { run_wake_await } from './run-wake-await'
14
16
  import { run_wake_describe, type WakeContext } from './run-wake-describe'
15
17
  import { run_wake_driver } from './run-wake-driver'
16
18
  import { run_wake_handoff } from './run-wake-handoff'
17
19
  import { run_wake_loop, type LoopPorts, type LoopStop } from './run-wake-loop'
18
- import { run_wake_restart, type RestartPorts } from './run-wake-restart'
19
20
  import { run_wake_session, type LaunchResult } from './run-wake-session'
21
+ import { run_wake_stop } from './run-wake-stop'
22
+ import { run_wake_supervise } from './run-wake-supervise'
20
23
 
21
24
  // `josh run:wake --start | --list | --stop | --loop` — the supervisor that continues a cut
22
25
  // `backlogrun` or `queue` without a person retyping the keyword. **Which invocations it may continue
@@ -83,6 +86,7 @@ const STOP_BODIES: Record<WakeStopReason, string | undefined> = {
83
86
  unreadable:
84
87
  'The carry record could not be read, so the supervisor stopped rather than guess at it.',
85
88
  }
89
+ const NO_PROFILE_NOTE = 'the scheduler profile could not be resolved'
86
90
  const SUPERSEDED_RESULT: WakeTidyResult = 'superseded'
87
91
  const SUPERSEDED_NOTE =
88
92
  'another supervisor now holds the record, so it was left alone; this loop is the superseded one'
@@ -131,25 +135,14 @@ function group_count(values: ParsedValues): number {
131
135
  return verbs.filter((flag) => flag === true).length
132
136
  }
133
137
 
138
+ function context_of(directory: string): WakeContext {
139
+ return run_wake_describe.context_of(directory, note_to_stderr)
140
+ }
141
+
134
142
  async function resolve_context(): Promise<WakeContext | undefined> {
135
143
  const directory = await run_carry.repository_directory()
136
144
 
137
- if (directory === undefined) return undefined
138
-
139
- const log_target = run_wake.wake_log_path(directory)
140
-
141
- // Every verb reaches this one function, so creating the log here is what makes the path each of
142
- // them prints a path that exists — including `--list` and the already-running branch of `--start`,
143
- // neither of which launches anything.
144
- run_wake_session.ensure_log(log_target, note_to_stderr)
145
-
146
- return {
147
- carry_target: run_carry.carry_path(directory),
148
- wake_target: run_wake.wake_path(directory),
149
- log_target,
150
- event_target: run_event_stream.target_of(directory),
151
- worktree: path.dirname(directory),
152
- }
145
+ return directory === undefined ? undefined : context_of(directory)
153
146
  }
154
147
 
155
148
  // There is nothing to supervise unless a run is actually carrying a budget. Refusing here rather than
@@ -161,6 +154,12 @@ function refuse_without_carry(kind: CarryRead['kind']): number {
161
154
  return report(NONE_VERDICT, FAILURE_EXIT_CODE)
162
155
  }
163
156
 
157
+ // **A session that started is said on the stream.** The driver's `waiting on judgment` is the moment it
158
+ // asked; from here a session is at work on the answer, until the supervisor's next hand-off.
159
+ function say_session_running(context: WakeContext): void {
160
+ run_event_setup.emit_to(context.event_target, run_event_setup.STAGE.SESSION, new Date())
161
+ }
162
+
164
163
  function wake_session(
165
164
  context: WakeContext,
166
165
  invocation: string,
@@ -176,14 +175,20 @@ function wake_session(
176
175
  // under `claude -p` a turn-end is the process's end (`run-headless.ts`).
177
176
  const environment = run_headless.environment()
178
177
  const target = { cwd: context.worktree, env: environment, log_path: context.log_target }
178
+ const result = run_wake_session.launch({ argv: built.argv, ...target, profile }, note_to_stderr)
179
+
180
+ if (result.kind === 'launched') say_session_running(context)
179
181
 
180
- return run_wake_session.launch({ argv: built.argv, ...target, profile }, note_to_stderr)
182
+ return result
181
183
  }
182
184
 
185
+ // **A record this supervisor owns itself is not a session at work.** A failed driver leaves the record
186
+ // adopted for this process, so reading that owner as live would have every restarted pass wait on
187
+ // itself and never drive again.
183
188
  function ports_for(context: WakeContext, profile: AgentProfile): LoopPorts {
184
189
  return {
185
190
  read_carry: () => run_carry.read_carry(context.carry_target),
186
- is_owner_live: (read) => read.kind === 'carried' && run_carry.is_owner_live(read.carry),
191
+ is_owner_live: (read) => read.kind === 'carried' && run_carry.is_held_by_another(read.carry),
187
192
  has_work: async () => true,
188
193
  new_session_id: () => randomUUID(),
189
194
  hand_off: () => {
@@ -211,14 +216,45 @@ function warning_body(stop: LoopStop, body: string, context: WakeContext): strin
211
216
  return [body, log_hint(context), stop.note].filter(Boolean).join('\n')
212
217
  }
213
218
 
214
- async function warn_of_stop(stop: LoopStop, body: string, context: WakeContext): Promise<void> {
215
- await telegram_notify.warn({
219
+ // Whether the warning reached a person — the answer `record_failure` leaves its marker on.
220
+ async function warn_of_stop(stop: LoopStop, body: string, context: WakeContext): Promise<boolean> {
221
+ return await telegram_notify.warn({
216
222
  issue_title: WARNING_TITLE,
217
223
  body: warning_body(stop, body, context),
218
224
  recovery: WARNING_RECOVERY,
219
225
  })
220
226
  }
221
227
 
228
+ // **A failed supervisor that leaves the run stranded says so on the stream, with its cause.** Nothing
229
+ // else writes there when the supervisor gives up, so a reader of the events would see the last merge
230
+ // and a run that looks as if it were still watching. The cause is the note's first line — the
231
+ // driver's own `error …` — because the rest is a stderr tail the log already holds.
232
+ //
233
+ // **Only a strand, and only one a person was told of.** A `failed` stop is not always a strand: a
234
+ // `run:carry --end` under a live driver fails it too, and that run ended cleanly. And the event is the
235
+ // marker `run_stranded_detect` holds its own notification back on, so it is left only when the warning
236
+ // was sent — after a failed send the stop-time detector still finds no marker, and notifies.
237
+ function is_strand_told(stop: LoopStop, watched: WatchedRun, is_warned: boolean): boolean {
238
+ if (!is_warned || stop.reason !== run_wake_loop.FAILED_REASON) return false
239
+
240
+ return run_stranded_detect.is_left_stranded(watched)
241
+ }
242
+
243
+ async function record_failure(
244
+ stop: LoopStop,
245
+ watched: WatchedRun,
246
+ is_warned: boolean,
247
+ ): Promise<void> {
248
+ if (!is_strand_told(stop, watched, is_warned)) return
249
+
250
+ const [cause = ''] = (stop.note ?? '').split('\n', 1)
251
+
252
+ await run_event_stream_emit.emit(
253
+ run_event_stream.EVENT_KIND.STRANDED,
254
+ `The supervisor failed and stopped watching the run — ${cause}`,
255
+ )
256
+ }
257
+
222
258
  // **Only a failed wake exits non-zero; `unreadable` warns but exits zero.** The
223
259
  // verdict-and-exit-code table in `docs/josh-commands-automation.md` is a contract callers branch on,
224
260
  // and what that one needs is the notification, not a different exit code.
@@ -234,25 +270,33 @@ function stop_body(reason: WakeStopReason): string | undefined {
234
270
  return STOP_BODIES[reason]
235
271
  }
236
272
 
237
- async function finish(stop: LoopStop, context: WakeContext): Promise<number> {
273
+ // `started_at` is the run this supervisor took on — what `record_failure` tells an ended run by once
274
+ // the carry record that held it no longer reads.
275
+ async function finish(stop: LoopStop, context: WakeContext, started_at: string): Promise<number> {
238
276
  const body = stop_body(stop.reason)
239
277
 
240
278
  if (body === undefined) return report(stop.reason)
241
279
 
242
280
  console.error(stop.note ?? body)
243
- await warn_of_stop(stop, body, context)
281
+ const is_warned = await warn_of_stop(stop, body, context)
282
+
283
+ await record_failure(stop, { ...context, started_at }, is_warned)
244
284
 
245
285
  return report(stop.reason, exit_code_of(stop.reason))
246
286
  }
247
287
 
248
- function spawn_supervisor(context: WakeContext, interval: string | undefined): number {
249
- const profile = run_wake_session.scheduler_profile()
250
- if (profile === undefined) return report(FAILED_VERDICT, FAILURE_EXIT_CODE)
288
+ function launch_supervisor(
289
+ context: WakeContext,
290
+ interval: string | undefined,
291
+ profile: AgentProfile | undefined,
292
+ ): LaunchResult {
293
+ if (profile === undefined) return { kind: 'failed', note: NO_PROFILE_NOTE }
251
294
  const argv = run_wake_session.supervisor_argv(SCRIPT_PATH, interval)
295
+
252
296
  // **The supervisor's own output goes to the same file as the sessions it starts.** It is detached
253
297
  // with the same call, so otherwise its `console.error` — the launch note that says the agent CLI is
254
298
  // not on `PATH`, and the stop note behind every warning — would go nowhere at all.
255
- const result = run_wake_session.launch(
299
+ return run_wake_session.launch(
256
300
  {
257
301
  argv,
258
302
  cwd: context.worktree,
@@ -261,6 +305,20 @@ function spawn_supervisor(context: WakeContext, interval: string | undefined): n
261
305
  },
262
306
  note_to_stderr,
263
307
  )
308
+ }
309
+
310
+ // The supervisor `josh backlogrun` starts beside its launched parent, on the parent's provider, from its
311
+ // own process so it inherits the credential the parent's Bash tool does not pass on (kit#3804). No carry
312
+ // record stands yet, so `start`'s check is skipped; its `--loop` waits for the parent to claim one.
313
+ function start_for_launch(git_directory: string, provider: AgentProvider): LaunchResult {
314
+ const resolved = run_wake_session.chosen_profile(provider)
315
+ if (resolved.kind === 'rejected') return { kind: 'failed', note: resolved.note }
316
+
317
+ return launch_supervisor(context_of(git_directory), undefined, resolved.profile)
318
+ }
319
+
320
+ function spawn_supervisor(context: WakeContext, interval: string | undefined): number {
321
+ const result = launch_supervisor(context, interval, run_wake_session.scheduler_profile())
264
322
 
265
323
  if (result.kind === 'failed') {
266
324
  console.error(`Could not start the supervisor: ${result.note}`)
@@ -303,40 +361,32 @@ function tidy_up(target: string): void {
303
361
  if (run_wake.tidy_own_wake(target) === SUPERSEDED_RESULT) note_to_stderr(SUPERSEDED_NOTE)
304
362
  }
305
363
 
306
- function restart_ports(context: WakeContext, ports: LoopPorts, interval_ms: number): RestartPorts {
307
- return {
308
- pass: async () => await run_wake_loop.run_loop(context.wake_target, ports, interval_ms),
309
- is_resumable: () => run_carry.read_carry(context.carry_target).kind === 'carried',
310
- pause: async () => {
311
- await poll.sleep(interval_ms)
312
- },
313
- note: note_to_stderr,
314
- }
315
- }
316
-
317
364
  // A failed pass is restarted while the carry record is still resumable (`run-wake-restart.ts`), so
318
- // only a failure past the restart bound reaches `finish` and its warning.
365
+ // only a failure past the restart bound reaches `finish` and its warning; the Claude lanes are brokered
366
+ // for as long as it supervises (`run-wake-supervise.ts`).
319
367
  // **The record is claimed once and held across every restart**, so a `--stop` landing in the pause
320
368
  // between two passes still finds it, signals this process, and the next pass reads the removal as the
321
369
  // person's stop rather than claiming a fresh record and resuming.
322
370
  async function loop(context: WakeContext, interval_ms: number): Promise<number> {
323
- const read = run_carry.read_carry(context.carry_target)
324
-
325
- if (read.kind !== 'carried') return refuse_without_carry(read.kind)
326
-
327
371
  const profile = run_wake_session.scheduler_profile()
328
372
  if (profile === undefined) return report(FAILED_VERDICT, FAILURE_EXIT_CODE)
329
373
 
330
- const claimed = run_wake.claim(context.wake_target, read.carry.invocation, new Date(), profile)
374
+ const claimed = await run_wake_await.claim_run(context, profile, interval_ms)
331
375
 
332
- if (claimed === undefined) return report(RUNNING_VERDICT)
376
+ if (claimed.kind === 'running') return report(RUNNING_VERDICT)
377
+ if (claimed.kind === 'refused') return refuse_without_carry(claimed.carry_kind)
333
378
 
334
- const ports = ports_for(context, claimed.profile ?? profile)
335
- const stop = await run_wake_restart.supervise(restart_ports(context, ports, interval_ms))
379
+ const ports = ports_for(context, claimed.wake.profile ?? profile)
380
+ const stop = await run_wake_supervise.supervise({
381
+ context,
382
+ ports,
383
+ interval_ms,
384
+ note: note_to_stderr,
385
+ })
336
386
 
337
387
  tidy_up(context.wake_target)
338
388
 
339
- return await finish(stop, context)
389
+ return await finish(stop, context, claimed.carry.started_at)
340
390
  }
341
391
 
342
392
  // **The verdict tracks whether the supervisor is actually running, not merely whether a record is
@@ -352,30 +402,10 @@ function list(context: WakeContext): number {
352
402
  return report(run_wake.is_supervisor_live(wake) ? SUPERVISING_VERDICT : STALE_VERDICT)
353
403
  }
354
404
 
355
- // **Removing the record is the stop; the signal only shortens the wait.** A pid already gone, or one
356
- // this process may not signal, is not an error — the loop ends at its next pass either way, because it
357
- // re-reads the record it no longer finds and `run_wake.update_wake` refuses to write one back.
358
- function signal_supervisor(wake: RunWake): void {
359
- if (!run_wake.is_supervisor_live(wake)) return
360
-
361
- try {
362
- process.kill(wake.pid, 'SIGTERM')
363
- } catch {
364
- console.error(
365
- `Process ${String(wake.pid)} could not be signalled; the record is removed regardless.`,
366
- )
367
- }
368
- }
369
-
370
405
  function stop_supervisor(context: WakeContext): number {
371
- const wake = run_wake.read_wake(context.wake_target)
372
-
373
- if (wake === undefined) return report(NONE_VERDICT)
374
-
375
- run_wake.remove_wake(context.wake_target)
376
- signal_supervisor(wake)
406
+ const stopped = run_wake_stop.stop(context.wake_target)
377
407
 
378
- return report(STOPPED_VERDICT)
408
+ return report(stopped === undefined ? NONE_VERDICT : STOPPED_VERDICT)
379
409
  }
380
410
 
381
411
  async function dispatch(
@@ -419,7 +449,11 @@ const run_wake_cli = {
419
449
  SUPERVISING_VERDICT,
420
450
  UNKNOWN_VERDICT,
421
451
  USAGE,
452
+ finish,
453
+ ports_for,
454
+ record_failure,
422
455
  run,
456
+ start_for_launch,
423
457
  stop_body,
424
458
  warning_body,
425
459
  }
@@ -1,17 +1,22 @@
1
+ import path from 'node:path'
1
2
  import { agent_role_profile } from '#scripts/agent/agent-role-profile'
2
3
  import { gh_spawn } from '#scripts/gh/gh-spawn'
3
4
  import { issue_citation } from '#scripts/rules/issue-citation'
4
5
  import { run_carry, type CarryRead, type RunCarry } from '#scripts/run/carry/run-carry'
6
+ import { run_carry_ended } from '#scripts/run/carry/run-carry-ended'
5
7
  import { run_event_stream } from '#scripts/run/event/run-event-stream'
6
8
  import { run_liveness } from '#scripts/run/run-liveness'
7
9
  import { run_wake, type RunWake, type WakeDecision, type WakeDecisionInput } from './run-wake'
10
+ import { run_wake_session } from './run-wake-session'
8
11
 
9
12
  // The `--list` presentation of a wake record. What the command *does* stays in `run-wake-cli.ts`; how
10
13
  // a supervisor's record reads back to a person lives here, one cohesive block.
11
14
 
12
15
  const UNKNOWN_CUTS = 'an unreadable number of'
13
16
  const REPO_LOOKUP_TIMEOUT_MS = 10_000
14
- const STOP_COMMAND = 'pnpm josh run:wake --stop'
17
+ // The whole run, not the supervisor alone: `run:wake --stop` leaves the run's record behind, and a
18
+ // record with no owner is what the next `josh backlogrun` stands on without starting.
19
+ const STOP_COMMAND = 'pnpm josh backlogrun --stop'
15
20
  const UNREADABLE_TIME = 'an unreadable time'
16
21
 
17
22
  // The three paths every verb needs: the carry record it reads, its own record, and the work tree a
@@ -19,6 +24,8 @@ const UNREADABLE_TIME = 'an unreadable time'
19
24
  // checkout — a lane must never be where the next session resumes, because a lane belongs to one child.
20
25
  interface WakeContext {
21
26
  carry_target: string
27
+ // Where `run:carry --end` records the run it removes — what tells an ended run from an unread one.
28
+ ended_target: string
22
29
  wake_target: string
23
30
  // Where everything this supervisor starts writes its output. It is named
24
31
  // in `--list` and in every warning, because a path nobody is told is a file nobody reads.
@@ -28,6 +35,8 @@ interface WakeContext {
28
35
  // side uses. `--list` relays the newest event from here: the report surface belongs to the run, and
29
36
  // `--list` is the degenerate last-event read of the same stream the attached session follows.
30
37
  event_target: string
38
+ // The common git directory every record is keyed on — what finds the launch `josh backlogrun` made.
39
+ git_directory: string
31
40
  worktree: string
32
41
  }
33
42
 
@@ -154,7 +163,32 @@ function describe_wake_cited(wake: RunWake, context: WakeContext): string {
154
163
  return cite_description(describe_wake(wake, context), repo)
155
164
  }
156
165
 
157
- const run_wake_describe = { STOP_COMMAND, cite_description, describe_wake, describe_wake_cited }
166
+ function context_of(directory: string, note: (line: string) => void): WakeContext {
167
+ const log_target = run_wake.wake_log_path(directory)
168
+
169
+ // Every verb reaches this one function, so creating the log here is what makes the path each of
170
+ // them prints a path that exists — including `--list` and the already-running branch of `--start`,
171
+ // neither of which launches anything.
172
+ run_wake_session.ensure_log(log_target, note)
173
+
174
+ return {
175
+ carry_target: run_carry.carry_path(directory),
176
+ ended_target: run_carry_ended.ended_path(directory),
177
+ wake_target: run_wake.wake_path(directory),
178
+ log_target,
179
+ event_target: run_event_stream.target_of(directory),
180
+ git_directory: directory,
181
+ worktree: path.dirname(directory),
182
+ }
183
+ }
184
+
185
+ const run_wake_describe = {
186
+ STOP_COMMAND,
187
+ cite_description,
188
+ context_of,
189
+ describe_wake,
190
+ describe_wake_cited,
191
+ }
158
192
 
159
193
  export type { WakeContext }
160
194
  export { run_wake_describe }
@@ -1,5 +1,6 @@
1
1
  import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
2
2
  import { run_carry, type CarryRead } from '#scripts/run/carry/run-carry'
3
+ import { run_event_setup } from '#scripts/run/event/run-event-setup'
3
4
  import { run_invocation } from '#scripts/run/run-invocation'
4
5
  import type { DriveResult } from './run-wake-loop'
5
6
 
@@ -125,10 +126,16 @@ function read_result(
125
126
  return driver_result(result.out, run_carry.read_carry(target), invocation, result.err)
126
127
  }
127
128
 
129
+ // **A record this process already owns is claimed again.** A driver that failed leaves the record
130
+ // adopted for the supervisor and not handed off, and the restarted pass re-drives it from here; only a
131
+ // record another live process holds is refused.
128
132
  function claim_record(target: string, invocation: string): boolean {
129
133
  const current = run_carry.read_carry(target)
130
134
  if (current.kind !== 'carried' || current.carry.invocation !== invocation) return false
131
- if (current.carry.is_handed_off !== true && run_carry.is_owner_live(current.carry)) return false
135
+
136
+ if (current.carry.is_handed_off !== true && run_carry.is_held_by_another(current.carry)) {
137
+ return false
138
+ }
132
139
 
133
140
  return run_carry.adopt_carry(target, current.carry, run_carry.owner_of(process.pid)) !== undefined
134
141
  }
@@ -177,6 +184,15 @@ async function run_driver(
177
184
  return await run_driver(target, args, wait, remaining - 1)
178
185
  }
179
186
 
187
+ // **A result handed to a session is said on the stream first.** The driver has stopped asking, so
188
+ // without it the board keeps the `handing off` this pass wrote and a run waiting on a session's
189
+ // judgment reads as one still getting ready.
190
+ async function announced(result: DriveResult): Promise<DriveResult> {
191
+ if (result.kind === 'judgment') await run_event_setup.emit(run_event_setup.STAGE.JUDGING)
192
+
193
+ return result
194
+ }
195
+
180
196
  async function drive(target: string, wait: Pause = pause): Promise<DriveResult> {
181
197
  const read = run_carry.read_carry(target)
182
198
  if (read.kind !== 'carried') return { kind: 'failed', note: 'The carry record changed.' }
@@ -188,10 +204,10 @@ async function drive(target: string, wait: Pause = pause): Promise<DriveResult>
188
204
  return { kind: 'failed', note: 'The driver could not claim the carry record.' }
189
205
  }
190
206
 
191
- return read_result(
192
- await run_driver(target, args, wait, RERUN_LIMIT),
193
- target,
194
- read.carry.invocation,
207
+ await run_event_setup.emit(run_event_setup.STAGE.HANDING_OFF)
208
+
209
+ return await announced(
210
+ read_result(await run_driver(target, args, wait, RERUN_LIMIT), target, read.carry.invocation),
195
211
  )
196
212
  }
197
213
 
@@ -21,7 +21,7 @@ function hand_off(target: string): void {
21
21
 
22
22
  if (read.kind !== 'carried' || read.carry.is_handed_off === true) return
23
23
 
24
- stamp_file.write_stamp(target, { ...read.carry, is_handed_off: true })
24
+ stamp_file.replace_stamp(target, { ...read.carry, is_handed_off: true })
25
25
  }
26
26
 
27
27
  const run_wake_handoff = { hand_off }
@@ -1,6 +1,12 @@
1
1
  import { agent_argv, type AgentArgvResult } from '#scripts/agent/agent-argv'
2
- import { agent_role_profile, type AgentProfile } from '#scripts/agent/agent-role-profile'
2
+ import {
3
+ agent_role_profile,
4
+ type AgentProfile,
5
+ type AgentProvider,
6
+ type ProfileResult,
7
+ } from '#scripts/agent/agent-role-profile'
3
8
  import { claude_agent_argv } from '#scripts/agent/claude-agent-argv'
9
+ import { backlogrun_launch } from '#scripts/backlog/backlogrun-launch'
4
10
  import { detached_launch, type LaunchArgv } from '#scripts/run/detached-launch'
5
11
  import { run_invocation } from '#scripts/run/run-invocation'
6
12
 
@@ -46,10 +52,20 @@ function scheduler_profile(): AgentProfile | undefined {
46
52
  return undefined
47
53
  }
48
54
 
49
- function supervisor_environment(profile: AgentProfile): Record<string, string> {
55
+ // The scheduler profile of the provider a run was started on, whatever session the command was typed in
56
+ // (joshuafolkken/kit#3804).
57
+ function chosen_profile(provider: AgentProvider): ProfileResult {
58
+ const environment = backlogrun_launch.chosen_environment(provider)
59
+
60
+ return agent_role_profile.resolve(agent_role_profile.SCHEDULER, environment)
61
+ }
62
+
63
+ // **A Claude supervisor clears an inherited Codex session key**, so one started from a command typed in
64
+ // a Codex session does not read itself as both.
65
+ function supervisor_environment(profile: AgentProfile): Record<string, string | undefined> {
50
66
  return profile.provider === 'openai'
51
67
  ? { CODEX_THREAD_ID: SUPERVISOR_SESSION }
52
- : { CLAUDE_CODE_CHILD_SESSION: SUPERVISOR_SESSION }
68
+ : { CLAUDE_CODE_CHILD_SESSION: SUPERVISOR_SESSION, CODEX_THREAD_ID: undefined }
53
69
  }
54
70
 
55
71
  // **What comes back is always the rebuilt text, and it is refused unless it matches the record.** The
@@ -149,6 +165,7 @@ const run_wake_session = {
149
165
  ensure_log: detached_launch.ensure_log,
150
166
  is_safe_argv: detached_launch.is_safe_argv,
151
167
  launch: detached_launch.launch,
168
+ chosen_profile,
152
169
  scheduler_profile,
153
170
  supervisor_argv,
154
171
  supervisor_environment,
@@ -0,0 +1,37 @@
1
+ import { run_wake, type RunWake } from './run-wake'
2
+
3
+ // Stopping the supervisor, apart from the command that prints the verdict: `run:wake --stop` and
4
+ // `josh backlogrun --stop` both stop it here, so the one stop is never written twice.
5
+
6
+ // **Removing the record is the stop; the signal only shortens the wait.** A pid already gone, or one
7
+ // this process may not signal, is not an error — the loop ends at its next pass either way, because it
8
+ // re-reads the record it no longer finds and `run_wake.update_wake` refuses to write one back.
9
+ function signal(pid: number): void {
10
+ try {
11
+ process.kill(pid, 'SIGTERM')
12
+ } catch {
13
+ console.error(
14
+ `Process ${String(pid)} could not be signalled; the record is removed regardless.`,
15
+ )
16
+ }
17
+ }
18
+
19
+ function signal_supervisor(wake: RunWake): void {
20
+ if (run_wake.is_supervisor_live(wake)) signal(wake.pid)
21
+ }
22
+
23
+ // The record that was stopped, `undefined` where there was none to stop.
24
+ function stop(wake_target: string): RunWake | undefined {
25
+ const wake = run_wake.read_wake(wake_target)
26
+
27
+ if (wake === undefined) return undefined
28
+
29
+ run_wake.remove_wake(wake_target)
30
+ signal_supervisor(wake)
31
+
32
+ return wake
33
+ }
34
+
35
+ const run_wake_stop = { signal, stop }
36
+
37
+ export { run_wake_stop }
@@ -0,0 +1,44 @@
1
+ import { claude_lane_broker } from '#scripts/lane/claude-lane-broker'
2
+ import { poll } from '#scripts/lib/poll'
3
+ import { run_carry } from '#scripts/run/carry/run-carry'
4
+ import type { WakeContext } from './run-wake-describe'
5
+ import { run_wake_loop, type LoopPorts, type LoopStop } from './run-wake-loop'
6
+ import { run_wake_restart, type RestartPorts } from './run-wake-restart'
7
+
8
+ // The supervisor's whole life: the restarting loop (`run-wake-restart.ts`), with the Claude lanes
9
+ // brokered beside it.
10
+ //
11
+ // **The lanes are brokered for the supervisor's life, not one drive's**: a lane's ship asks for its
12
+ // reviewer or its relaunch whenever it gets there, a hand-back to a session included, and only this
13
+ // process holds the token those sessions sign in with (joshuafolkken/kit#3817, `claude-lane-broker.ts`).
14
+
15
+ interface SuperviseTarget {
16
+ context: WakeContext
17
+ ports: LoopPorts
18
+ interval_ms: number
19
+ note: (text: string) => void
20
+ }
21
+
22
+ function restart_ports(target: SuperviseTarget): RestartPorts {
23
+ const { context, ports, interval_ms } = target
24
+
25
+ return {
26
+ pass: async () => await run_wake_loop.run_loop(context.wake_target, ports, interval_ms),
27
+ is_resumable: () => run_carry.read_carry(context.carry_target).kind === 'carried',
28
+ pause: async () => {
29
+ await poll.sleep(interval_ms)
30
+ },
31
+ note: target.note,
32
+ }
33
+ }
34
+
35
+ async function supervise(target: SuperviseTarget): Promise<LoopStop> {
36
+ return await claude_lane_broker.serve_during(
37
+ async () => await run_wake_restart.supervise(restart_ports(target)),
38
+ )
39
+ }
40
+
41
+ const run_wake_supervise = { supervise }
42
+
43
+ export { run_wake_supervise }
44
+ export type { SuperviseTarget }