@joshuafolkken/kit 1.1379.0 → 1.1387.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 (84) hide show
  1. package/.claude/skills/workflow-commands/backlogrun-progress.md +2 -3
  2. package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -4
  3. package/.claude/skills/workflow-commands/retrospective.md +1 -2
  4. package/dist/hooks/batch-guard.js +6 -6
  5. package/dist/hooks/{chunk-RW4XMF47.js → chunk-2YSW3CXH.js} +133 -234
  6. package/dist/hooks/{chunk-4RWRP3FF.js → chunk-DCY2GWHC.js} +5 -5
  7. package/dist/hooks/{chunk-DHSKBAQ2.js → chunk-EAQEGME7.js} +1 -1
  8. package/dist/hooks/{chunk-E7LZL5AN.js → chunk-EBSHN25W.js} +3 -3
  9. package/dist/hooks/{chunk-HGA4EG5S.js → chunk-GJ7JPIHN.js} +15 -15
  10. package/dist/hooks/{chunk-TG2AB3W6.js → chunk-GNDJ7NJ7.js} +88 -501
  11. package/dist/hooks/{chunk-546X4PX3.js → chunk-IYVOLN6A.js} +3 -3
  12. package/dist/hooks/{chunk-NBEAGPS4.js → chunk-J3KSS7II.js} +1 -1
  13. package/dist/hooks/{chunk-7ZM464OW.js → chunk-LMWBRJIP.js} +3 -3
  14. package/dist/hooks/{chunk-TORRSNDJ.js → chunk-SL4GQ2XP.js} +3 -3
  15. package/dist/hooks/{chunk-PV3FWDSQ.js → chunk-STDSEPWU.js} +2 -2
  16. package/dist/hooks/{chunk-YSDAH73S.js → chunk-U32VSRNG.js} +420 -4
  17. package/dist/hooks/{chunk-QARPP36N.js → chunk-WNFJLBXI.js} +3 -3
  18. package/dist/hooks/codex-hook-adapter.js +13 -13
  19. package/dist/hooks/duplicate-read-guard.js +7 -7
  20. package/dist/hooks/format-edited.js +3 -3
  21. package/dist/hooks/inputs.json +213 -212
  22. package/dist/hooks/investigation-guard.js +7 -7
  23. package/dist/hooks/pretool-guard.js +12 -12
  24. package/dist/hooks/session-lang.js +65 -9
  25. package/dist/hooks/stop-guard.js +104 -32
  26. package/dist/josh.js +2 -2
  27. package/package.json +1 -1
  28. package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
  29. package/scripts/backlog/backlog-budget-cli.ts +2 -20
  30. package/scripts/backlog/backlog-budget.ts +72 -109
  31. package/scripts/backlog/backlog-drive-cli.ts +12 -7
  32. package/scripts/backlog/backlog-drive-launch.ts +2 -0
  33. package/scripts/backlog/backlog-drive-owner.ts +72 -5
  34. package/scripts/backlog/backlog-drive.ts +5 -2
  35. package/scripts/backlog/backlog-offer-cli.ts +1 -15
  36. package/scripts/backlog/backlogrun-launch.ts +56 -5
  37. package/scripts/document/document-byte-budget.ts +1 -1
  38. package/scripts/epic/epic-candidate-confirm.ts +1 -1
  39. package/scripts/epic/epic-cross-repo.ts +4 -4
  40. package/scripts/epic/epic-next.ts +1 -1
  41. package/scripts/hooks/stop-guard.ts +20 -9
  42. package/scripts/init/start-plan.ts +15 -4
  43. package/scripts/init/start-steps.ts +31 -3
  44. package/scripts/init/start.ts +20 -13
  45. package/scripts/josh/josh-commands-ai.ts +1 -1
  46. package/scripts/josh/josh-commands-project.ts +1 -1
  47. package/scripts/lane/lane-cli.ts +1 -3
  48. package/scripts/run/board/run-board-header.ts +137 -88
  49. package/scripts/run/board/run-board-labels.ts +18 -17
  50. package/scripts/run/board/run-board-machine.ts +21 -8
  51. package/scripts/run/board/run-board-read.ts +9 -2
  52. package/scripts/run/board/run-board-render.ts +1 -5
  53. package/scripts/run/board/run-board-setup.ts +73 -0
  54. package/scripts/run/board/run-board-span.ts +49 -8
  55. package/scripts/run/board/run-board-status.ts +10 -23
  56. package/scripts/run/board/run-board-tick.ts +11 -2
  57. package/scripts/run/carry/run-carry-added.ts +1 -1
  58. package/scripts/run/carry/run-carry-change.ts +1 -1
  59. package/scripts/run/carry/run-carry-cli.ts +36 -60
  60. package/scripts/run/carry/run-carry-ended.ts +10 -2
  61. package/scripts/run/carry/run-carry.ts +38 -68
  62. package/scripts/run/cut/run-cut.ts +4 -4
  63. package/scripts/run/event/run-event-render.ts +1 -1
  64. package/scripts/run/event/run-event-scope.ts +4 -8
  65. package/scripts/run/event/run-event-setup.ts +47 -0
  66. package/scripts/run/event/run-event-stream-emit.ts +0 -25
  67. package/scripts/run/event/run-event-stream.ts +41 -28
  68. package/scripts/run/merge/run-merge-collect.ts +1 -1
  69. package/scripts/run/merge/run-merge-steps.ts +1 -1
  70. package/scripts/run/progress/run-progress-read.ts +1 -1
  71. package/scripts/run/run-headless.ts +2 -2
  72. package/scripts/run/run-status-cli.ts +1 -5
  73. package/scripts/run/run-step.ts +2 -9
  74. package/scripts/run/run-stop-notify.ts +1 -1
  75. package/scripts/run/run-stranded-detect.ts +61 -15
  76. package/scripts/run/run-stranded.ts +25 -17
  77. package/scripts/run/wake/run-wake-cli.ts +58 -15
  78. package/scripts/run/wake/run-wake-describe.ts +5 -5
  79. package/scripts/run/wake/run-wake-driver.ts +13 -4
  80. package/scripts/run/wake/run-wake-handoff.ts +1 -1
  81. package/scripts/run/wake/run-wake-loop.ts +4 -21
  82. package/scripts/run/wake/run-wake-restart.ts +1 -1
  83. package/scripts/run/wake/run-wake.ts +6 -7
  84. package/scripts/backlog/backlog-idle.ts +0 -88
@@ -74,7 +74,7 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
74
74
  { path: '.claude/skills/workflow-commands/target-repository.md', bytes: 4096 },
75
75
  { path: '.claude/skills/workflow-commands/working-tree-hold.md', bytes: 8192 },
76
76
  { path: 'docs/josh-commands-automation.md', bytes: 53_248 },
77
- { path: 'docs/josh-commands-run.md', bytes: 65_536 },
77
+ { path: 'docs/josh-commands-run.md', bytes: 69_632 },
78
78
  { path: 'docs/josh-commands-backlog.md', bytes: 57_344 },
79
79
  { path: 'docs/josh-commands.md', bytes: 49_152 },
80
80
  { path: 'docs/labels-and-run-states.md', bytes: 8192 },
@@ -111,7 +111,7 @@ function is_still_runnable(
111
111
  // A read that failed withholds the candidate rather than confirming it. "Could not tell" is not
112
112
  // "nothing blocks it", and this is the one direction the guard may not fail in, because that answer
113
113
  // *starts* work — the same reason `epic-busy.ts` answers `wait` for a listing it could not read. The
114
- // caller then offers the next candidate or waits, and the whole-run timeout bounds the wait.
114
+ // caller then offers the next candidate or waits, and the idle wait bounds the wait.
115
115
  async function read_or_withhold(
116
116
  candidate: EpicChild,
117
117
  read_blockers: BlockersReader,
@@ -80,7 +80,7 @@ const NOT_FOUND_STATUS = 404
80
80
  //
81
81
  // **`absent` is not `unreadable`.** A repository with no
82
82
  // manifest, or one whose manifest says `private`, publishes nothing — so a closed blocker there has
83
- // no release anyone could wait for, and waiting means waiting until the run's own eight-hour timeout
83
+ // no release anyone could wait for, and waiting means waiting until the run's idle wait runs out
84
84
  // with nothing an operator can edit to clear it. A read that merely failed says none of that.
85
85
  //
86
86
  // **It is read from the repository rather than from the registry, and that is the whole design.** The
@@ -123,7 +123,7 @@ type WorkspaceCheck = 'workspace' | 'single' | 'unreadable'
123
123
  //
124
124
  // **The file's existence proves nothing.** `josh sync` distributes it to every consumer regardless of
125
125
  // layout — it carries `allowBuilds`, `overrides` and the like — so probing for its presence would read
126
- // every private first-party repository as a workspace and wait the eight-hour timeout on each. What
126
+ // every private first-party repository as a workspace and wait the whole idle wait on each. What
127
127
  // makes a repository a workspace is a non-empty `packages`
128
128
  // key, so that is what is read.
129
129
  function declares_packages(yaml: string): boolean {
@@ -271,7 +271,7 @@ function read_default_branch_version(repo: string): string | undefined {
271
271
  // would start a dependent before its blocker's release existed. What is asked is only whether the
272
272
  // repository *is* a workspace — the members are not enumerated and nothing looks for which of them
273
273
  // publishes, so a workspace whose members are all private waits when it need not. That is the safe
274
- // direction: waiting ends at the run's own timeout, resolving early does not end at all.
274
+ // direction: waiting ends when the run's idle wait runs out, resolving early does not end at all.
275
275
  function publishes_nothing(repo: string): boolean {
276
276
  const answer = read_manifest(repo)
277
277
 
@@ -285,7 +285,7 @@ function publishes_nothing(repo: string): boolean {
285
285
  //
286
286
  // **"Ships nothing" is asked first, and it is a different question.** Waiting is right only where
287
287
  // something could still arrive; a closed blocker in a repository that publishes no package has no
288
- // release for `is_published` to ever find, so it would wait until the run's own eight-hour timeout
288
+ // release for `is_published` to ever find, so it would wait until the run's idle wait runs out
289
289
  // with nothing an operator could edit to clear it. It is answered from that repository's own manifest and never from
290
290
  // the registry, for the reason `ManifestAnswer` records: a registry 404 also means "this token may
291
291
  // not see it", and resolving on one would start a consumer child before its blocker's release
@@ -194,7 +194,7 @@ function refuse(reason: string): number {
194
194
 
195
195
  // The verdict as it applies to *this* repository. `run` never reaches a caller here: it means some
196
196
  // other repository has work, which for this session is something to wait on rather than a state its
197
- // loop has a branch for. The whole-run timeout is what bounds that wait.
197
+ // loop has a branch for. The idle wait is what bounds that wait.
198
198
  function repo_verdict(verdict: LaneOffer['verdict']): LaneOffer['verdict'] {
199
199
  return verdict === 'run' ? 'wait' : verdict
200
200
  }
@@ -126,20 +126,22 @@ async function outcome_of(raw_payload: string): Promise<StopOutcome> {
126
126
 
127
127
  // A live carry record is in place — `carried` alone reads as a run. Both report checks below are about
128
128
  // a carried run: the strand judge reads this very record and returns on anything but `carried`, and a
129
- // stall needs a backlog driver, which begins one before its plan. A spent (`expired`) or `unreadable`
130
- // record a crashed run left behind is no run either, so it does not re-arm the stall's backlog probe
131
- // on every stop.
129
+ // stall needs a backlog driver, which begins one before its plan. An `unreadable`
130
+ // record a crashed run left behind is no run either.
131
+ // The answer is whether that run's owner is still live, and `undefined` where there is no run at all.
132
132
  // A failed git read — outside a repository, `git rev-parse` exits non-zero — reads as no run, since this
133
133
  // gate sits ahead of the checks' own swallowing and must not throw into the stop decision either.
134
- async function has_run_record(): Promise<boolean> {
134
+ async function carried_owner_live(): Promise<boolean | undefined> {
135
135
  try {
136
136
  const directory = await run_carry.repository_directory()
137
137
 
138
- if (directory === undefined) return false
138
+ if (directory === undefined) return undefined
139
139
 
140
- return run_carry.read_carry(run_carry.carry_path(directory)).kind === 'carried'
140
+ const read = run_carry.read_carry(run_carry.carry_path(directory))
141
+
142
+ return read.kind === 'carried' ? run_carry.is_owner_live(read.carry) : undefined
141
143
  } catch {
142
- return false
144
+ return undefined
143
145
  }
144
146
  }
145
147
 
@@ -149,10 +151,19 @@ async function has_run_record(): Promise<boolean> {
149
151
  // — the budget handed off, the owner dead, no supervisor watching. Both only report and swallow their
150
152
  // own failures, so neither can change the stop decision. An ordinary stop with no run record skips
151
153
  // both, so it pays one directory read rather than their reads and a stale stream's backlog probe.
154
+ //
155
+ // **A stall is a live driver not dispatching, so a record whose owner is gone is not asked.** A record
156
+ // stands until a person carries or discards it, so a crashed run's would otherwise re-arm the stall's
157
+ // `git worktree list` and network backlog probe on every stop in the repository, for a run nobody
158
+ // drives. A gone driver is the strand check's question — handed off or not (`run-stranded.ts` →
159
+ // `had_driver`) — and that one reads local files only.
152
160
  async function run_report_checks(): Promise<void> {
153
- if (!(await has_run_record())) return
161
+ const is_owner_live = await carried_owner_live()
162
+
163
+ if (is_owner_live === undefined) return
164
+
165
+ if (is_owner_live) await backlog_stalled_detect.run_stall_check(backlog_ready.DEFAULT_PORTS)
154
166
 
155
- await backlog_stalled_detect.run_stall_check(backlog_ready.DEFAULT_PORTS)
156
167
  await run_stranded_detect.run_stranded_check()
157
168
  }
158
169
 
@@ -9,6 +9,7 @@ interface StartOptions {
9
9
  is_github: boolean
10
10
  visibility: Visibility
11
11
  init_command: string | undefined
12
+ push_preflight_command: string | undefined
12
13
  }
13
14
 
14
15
  type StartStep = 'git_init' | 'initialize' | 'commit' | 'repository' | 'labels' | 'setup_pr'
@@ -53,7 +54,8 @@ const STEP_LABELS: Readonly<Record<StartStep, string>> = {
53
54
  setup_pr: 'Open an Issue and a pull request that add the kit setup to main',
54
55
  }
55
56
 
56
- // The GitHub write steps `--yes` alone never consents to.
57
+ // The GitHub write steps `--yes` alone never consents to. Each one pushes — main, or the setup branch —
58
+ // so they are also the steps a caller's push preflight guards (#3702).
57
59
  const GITHUB_WRITE_STEPS: ReadonlySet<StartStep> = new Set(['repository', 'setup_pr'])
58
60
 
59
61
  function refused(refusal: string): StepPlan {
@@ -107,6 +109,10 @@ function plan_steps(state: GitState): StepPlan {
107
109
  return { steps: FULL_STEPS, refusal: undefined }
108
110
  }
109
111
 
112
+ function has_push(steps: ReadonlyArray<StartStep>): boolean {
113
+ return steps.some((step) => GITHUB_WRITE_STEPS.has(step))
114
+ }
115
+
110
116
  function interaction_of(options: StartOptions, is_tty: boolean): Verdict<Interaction> {
111
117
  if (options.is_yes) return { value: 'unattended', refusal: undefined }
112
118
  if (is_tty) return { value: 'interactive', refusal: undefined }
@@ -126,9 +132,7 @@ function github_consent(
126
132
  options: StartOptions,
127
133
  interaction: Interaction,
128
134
  ): Verdict<Consent> {
129
- const is_github_write = steps.some((step) => GITHUB_WRITE_STEPS.has(step))
130
-
131
- if (!is_github_write || options.is_github) return { value: 'granted', refusal: undefined }
135
+ if (!has_push(steps) || options.is_github) return { value: 'granted', refusal: undefined }
132
136
  if (interaction === 'interactive') return { value: 'ask', refusal: undefined }
133
137
 
134
138
  return {
@@ -158,6 +162,11 @@ function progress_report(
158
162
  return `josh start stopped at: ${labels[failed]}\nCompleted: ${done}\nCause: ${cause}`
159
163
  }
160
164
 
165
+ // What a run prints when the caller's push preflight fails: no step has run yet, so nothing changed.
166
+ function preflight_report(command: string, cause: string): string {
167
+ return `josh start stopped before its first step: the push preflight (${command}) failed. Nothing was changed.\nCause: ${cause}`
168
+ }
169
+
161
170
  function plan_summary(
162
171
  steps: ReadonlyArray<StartStep>,
163
172
  labels: Readonly<Record<StartStep, string>> = STEP_LABELS,
@@ -169,9 +178,11 @@ function plan_summary(
169
178
 
170
179
  const start_plan = {
171
180
  plan_steps,
181
+ has_push,
172
182
  interaction_of,
173
183
  github_consent,
174
184
  progress_report,
185
+ preflight_report,
175
186
  plan_summary,
176
187
  step_labels,
177
188
  STEP_LABELS,
@@ -18,6 +18,7 @@ interface StepContext {
18
18
  profile: ProjectProfile
19
19
  visibility: Visibility
20
20
  init_command: string | undefined
21
+ push_preflight_command: string | undefined
21
22
  }
22
23
 
23
24
  interface RunContext extends StepContext {
@@ -69,6 +70,13 @@ function self_run_refusal(root: string): string | undefined {
69
70
  return self_sync_guard.self_sync_refusal(PACKAGE_DIR, root)
70
71
  }
71
72
 
73
+ // A caller's command line runs without a shell, split the way execa splits one.
74
+ function run_command_line(command_line: string, extra: ReadonlyArray<string>, root: string): void {
75
+ const [command = '', ...args] = parseCommandString(command_line)
76
+
77
+ run_local(command, [...args, ...extra], root)
78
+ }
79
+
72
80
  function create_git_repository(context: StepContext): void {
73
81
  initial_commit.create_repository(context.root)
74
82
  }
@@ -84,9 +92,7 @@ function initialize(context: StepContext): void {
84
92
  return
85
93
  }
86
94
 
87
- const [command = '', ...args] = parseCommandString(context.init_command)
88
-
89
- run_local(command, [...args, '--profile', context.profile], context.root)
95
+ run_command_line(context.init_command, ['--profile', context.profile], context.root)
90
96
  }
91
97
 
92
98
  function commit_all(context: StepContext): void {
@@ -122,6 +128,26 @@ function baseline_of(
122
128
  return start_setup_pr.changed_paths(context.root)
123
129
  }
124
130
 
131
+ // A toolkit's own pre-push hook can need something kit knows nothing about (app-kit's needs a Docker
132
+ // daemon), and a push it refuses lands after `gh repo create` has made the repository. So the caller
133
+ // names a check that runs only when the plan pushes, before the first step: a run that does not push
134
+ // is never stopped by it, and one it stops has written nothing (#3702).
135
+ function run_push_preflight(steps: ReadonlyArray<StartStep>, context: StepContext): void {
136
+ const command_line = context.push_preflight_command
137
+
138
+ if (command_line === undefined || !start_plan.has_push(steps)) return
139
+
140
+ console.info(`\nPush preflight: ${command_line}`)
141
+
142
+ try {
143
+ run_command_line(command_line, [], context.root)
144
+ } catch (error) {
145
+ const cause = error_text.message_of(error)
146
+
147
+ throw new Error(start_plan.preflight_report(command_line, cause), { cause: error })
148
+ }
149
+ }
150
+
125
151
  type StepAction = (context: RunContext) => void | Promise<void>
126
152
 
127
153
  const STEP_ACTIONS: Readonly<Record<StartStep, StepAction>> = {
@@ -150,6 +176,8 @@ async function run_step(
150
176
 
151
177
  async function run_steps(steps: ReadonlyArray<StartStep>, context: StepContext): Promise<void> {
152
178
  const labels = start_plan.step_labels(context.init_command)
179
+
180
+ run_push_preflight(steps, context)
153
181
  const run_context: RunContext = { ...context, baseline: baseline_of(steps, context) }
154
182
 
155
183
  for (const [index, step] of steps.entries()) {
@@ -15,8 +15,9 @@ import { start_steps } from './start-steps'
15
15
  const ARGUMENT_START_INDEX = 2
16
16
  const READY_MESSAGE = 'GitHub workflow ready. Use kickoff new in your assistant to plan an Issue.'
17
17
  const START_USAGE =
18
- 'josh start [--profile basic|full] [--yes] [--github] [--public] [--init-command <command>]'
18
+ 'josh start [--profile basic|full] [--yes] [--github] [--public] [--init-command <command>] [--push-preflight-command <command>]'
19
19
  const INIT_COMMAND_FLAG = '--init-command'
20
+ const PUSH_PREFLIGHT_COMMAND_FLAG = '--push-preflight-command'
20
21
  const YES_FLAG = '--yes'
21
22
  const GITHUB_FLAG = '--github'
22
23
  const PUBLIC_FLAG = '--public'
@@ -28,34 +29,38 @@ interface Prepared {
28
29
  choices: Choices
29
30
  }
30
31
 
31
- interface InitCommandSplit {
32
- init_command: string | undefined
32
+ interface CommandSplit {
33
+ command: string | undefined
33
34
  rest: ReadonlyArray<string>
34
35
  }
35
36
 
36
37
  // A value that is blank or another flag means the command itself was left out, which would otherwise
37
- // swallow the next switch or hand the initialize step nothing to run.
38
+ // swallow the next switch or hand its step nothing to run.
38
39
  function is_command_value(value: string | undefined): value is string {
39
40
  return value !== undefined && value.trim() !== '' && !value.startsWith('-')
40
41
  }
41
42
 
42
43
  // The command arrives as the one argument after its flag and is removed together with it.
43
- function split_init_command(args: ReadonlyArray<string>): InitCommandSplit {
44
- const index = args.indexOf(INIT_COMMAND_FLAG)
44
+ function split_command(args: ReadonlyArray<string>, flag: string): CommandSplit {
45
+ const index = args.indexOf(flag)
45
46
 
46
- if (index === -1) return { init_command: undefined, rest: args }
47
+ if (index === -1) return { command: undefined, rest: args }
47
48
 
48
49
  const value = args[index + 1]
49
50
 
50
51
  if (!is_command_value(value)) throw new Error(`Usage: ${START_USAGE}`)
51
52
 
52
- return { init_command: value, rest: args.filter((_, at) => at !== index && at !== index + 1) }
53
+ return { command: value, rest: args.filter((_, at) => at !== index && at !== index + 1) }
53
54
  }
54
55
 
55
- // The switches and the initialize command are peeled off first, so what remains is exactly the
56
+ // The switches and the caller's commands are peeled off first, so what remains is exactly the
56
57
  // `--profile` pair `josh init` accepts — one parser for the profile in both commands.
57
58
  function parse_start_options(args: ReadonlyArray<string>): StartOptions {
58
- const { init_command, rest } = split_init_command(args)
59
+ const init = split_command(args, INIT_COMMAND_FLAG)
60
+ const { command: push_preflight_command, rest } = split_command(
61
+ init.rest,
62
+ PUSH_PREFLIGHT_COMMAND_FLAG,
63
+ )
59
64
  const profile_args = rest.filter((argument) => !SWITCHES.has(argument))
60
65
 
61
66
  return {
@@ -63,7 +68,8 @@ function parse_start_options(args: ReadonlyArray<string>): StartOptions {
63
68
  is_yes: rest.includes(YES_FLAG),
64
69
  is_github: rest.includes(GITHUB_FLAG),
65
70
  visibility: rest.includes(PUBLIC_FLAG) ? 'public' : 'private',
66
- init_command,
71
+ init_command: init.command,
72
+ push_preflight_command,
67
73
  }
68
74
  }
69
75
 
@@ -112,12 +118,13 @@ async function main(args: ReadonlyArray<string>, is_tty: boolean): Promise<void>
112
118
  const { steps, interaction, choices } = prepare(args, is_tty, process.cwd())
113
119
 
114
120
  const { root, options } = choices
115
- const { visibility, init_command } = options
121
+ const { visibility, init_command, push_preflight_command } = options
116
122
 
117
123
  console.info(start_plan.plan_summary(steps, start_plan.step_labels(init_command)))
118
124
  const profile = await resolve_profile(choices, interaction)
125
+ const context = { root, profile, visibility, init_command, push_preflight_command }
119
126
 
120
- await start_steps.run_steps(steps, { root, profile, visibility, init_command })
127
+ await start_steps.run_steps(steps, context)
121
128
  console.info(`\n${READY_MESSAGE}`)
122
129
  }
123
130
 
@@ -214,7 +214,7 @@ const AI_COMMANDS: Record<string, CommandEntry> = {
214
214
  // credentials, the same reason `run:wake` carries these flags.
215
215
  tsx_arguments: OPTIONAL_ENV_FILE_FLAGS,
216
216
  description:
217
- 'Report whether the run is stranded — budget handed off, owner gone, and no supervisor watching',
217
+ 'Report whether the run is stranded — owner gone, no successor holding the budget, and no supervisor watching',
218
218
  category: 'AI tools',
219
219
  reference: ['', 'automation', ['processes', 'notifications']],
220
220
  },
@@ -17,7 +17,7 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
17
17
  description: 'Set a project up for the GitHub Issue workflow, from git init to the setup PR',
18
18
  category: 'Project',
19
19
  reference: [
20
- `${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--init-command <command>]`,
20
+ `${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--init-command <command>] [--push-preflight-command <command>]`,
21
21
  'developer',
22
22
  ['files', 'git', 'network', 'processes'],
23
23
  ],
@@ -62,9 +62,7 @@ async function guard_cut_session(): Promise<string | undefined> {
62
62
 
63
63
  const read = run_carry.read_carry(run_carry.carry_path(directory))
64
64
 
65
- if ((read.kind !== 'carried' && read.kind !== 'expired') || read.carry.is_handed_off !== true) {
66
- return undefined
67
- }
65
+ if (read.kind !== 'carried' || read.carry.is_handed_off !== true) return undefined
68
66
 
69
67
  return run_carry.count_refused_message(read.carry)
70
68
  }