@joshuafolkken/kit 1.1373.0 → 1.1383.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 (92) hide show
  1. package/.claude/skills/dependency-update/SKILL.md +6 -3
  2. package/.claude/skills/workflow-commands/backlogrun-progress.md +2 -3
  3. package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -4
  4. package/.claude/skills/workflow-commands/retrospective.md +1 -2
  5. package/dist/hooks/batch-guard.js +6 -6
  6. package/dist/hooks/{chunk-UTHMB4O2.js → chunk-2X2ART5G.js} +4 -33
  7. package/dist/hooks/{chunk-NZN2UFI6.js → chunk-4MGHBTOM.js} +162 -233
  8. package/dist/hooks/{chunk-YLBBSSTF.js → chunk-6AC4C2IO.js} +91 -67
  9. package/dist/hooks/{chunk-AKGCP4LX.js → chunk-6MAW7BCM.js} +12 -12
  10. package/dist/hooks/{chunk-JCRDBQXB.js → chunk-7VSZ2N4B.js} +3 -3
  11. package/dist/hooks/{chunk-GG72ANKI.js → chunk-KC6LUZZ2.js} +1 -1
  12. package/dist/hooks/{chunk-BZMPVFDY.js → chunk-KCF2YE7O.js} +5 -5
  13. package/dist/hooks/{chunk-7WKYHEGU.js → chunk-LC56R53A.js} +3 -3
  14. package/dist/hooks/{chunk-V4TKZHER.js → chunk-LOJTBU5V.js} +3 -3
  15. package/dist/hooks/{chunk-JGW24KKM.js → chunk-RYP3TSUJ.js} +3 -3
  16. package/dist/hooks/{chunk-SLSFIWTX.js → chunk-TT742SIO.js} +8 -5
  17. package/dist/hooks/{chunk-Z5TMJIPW.js → chunk-Y5R47QHU.js} +1 -1
  18. package/dist/hooks/{chunk-7UBFU3CL.js → chunk-YKUITFT7.js} +2 -2
  19. package/dist/hooks/codex-hook-adapter.js +13 -13
  20. package/dist/hooks/duplicate-read-guard.js +7 -7
  21. package/dist/hooks/format-edited.js +3 -3
  22. package/dist/hooks/inputs.json +190 -191
  23. package/dist/hooks/investigation-guard.js +7 -7
  24. package/dist/hooks/pretool-guard.js +12 -12
  25. package/dist/hooks/session-lang.js +3 -3
  26. package/dist/hooks/stop-guard.js +19 -15
  27. package/dist/josh.js +40 -5
  28. package/package.json +4 -4
  29. package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
  30. package/scripts/adopt/adopt-logic.ts +4 -1
  31. package/scripts/adopt/adopt.ts +7 -4
  32. package/scripts/backlog/backlog-budget-cli.ts +2 -20
  33. package/scripts/backlog/backlog-budget.ts +72 -109
  34. package/scripts/backlog/backlog-drive-cli.ts +10 -3
  35. package/scripts/backlog/backlog-drive.ts +5 -2
  36. package/scripts/backlog/backlog-offer-cli.ts +1 -15
  37. package/scripts/cost-runtime/context-cut-payback.ts +9 -4
  38. package/scripts/cost-runtime/cost-pricing.ts +94 -33
  39. package/scripts/document/document-byte-budget.ts +1 -1
  40. package/scripts/document/read-set-pricing.ts +7 -5
  41. package/scripts/epic/epic-candidate-confirm.ts +1 -1
  42. package/scripts/epic/epic-cross-repo.ts +4 -4
  43. package/scripts/epic/epic-next.ts +1 -1
  44. package/scripts/git/git-spawn-sync.ts +9 -1
  45. package/scripts/hooks/stop-guard.ts +20 -9
  46. package/scripts/init/init-ai-copy.ts +10 -3
  47. package/scripts/init/init-copy-content.ts +8 -1
  48. package/scripts/init/security-policy.ts +79 -0
  49. package/scripts/josh/josh-commands-ai.ts +1 -1
  50. package/scripts/josh/josh-commands-maintenance.ts +3 -0
  51. package/scripts/josh/josh-commands-project.ts +1 -1
  52. package/scripts/josh/josh-commands-versioning.ts +2 -1
  53. package/scripts/lane/lane-cli.ts +1 -3
  54. package/scripts/propagate/propagate-run.ts +17 -1
  55. package/scripts/propagate/propagate-steps.ts +9 -1
  56. package/scripts/run/board/run-board-header.ts +107 -84
  57. package/scripts/run/board/run-board-labels.ts +18 -17
  58. package/scripts/run/board/run-board-machine.ts +21 -8
  59. package/scripts/run/board/run-board-render.ts +1 -5
  60. package/scripts/run/board/run-board-span.ts +0 -1
  61. package/scripts/run/board/run-board-status.ts +0 -17
  62. package/scripts/run/board/run-board-tick.ts +1 -0
  63. package/scripts/run/carry/run-carry-cli.ts +8 -62
  64. package/scripts/run/carry/run-carry-ended.ts +1 -1
  65. package/scripts/run/carry/run-carry.ts +22 -68
  66. package/scripts/run/cut/run-cut.ts +4 -4
  67. package/scripts/run/event/run-event-render.ts +0 -1
  68. package/scripts/run/event/run-event-scope.ts +4 -8
  69. package/scripts/run/event/run-event-stream-emit.ts +0 -25
  70. package/scripts/run/event/run-event-stream.ts +23 -31
  71. package/scripts/run/merge/run-merge-collect.ts +1 -1
  72. package/scripts/run/merge/run-merge-steps.ts +1 -1
  73. package/scripts/run/progress/run-progress-read.ts +1 -1
  74. package/scripts/run/run-headless.ts +2 -2
  75. package/scripts/run/run-status-cli.ts +1 -5
  76. package/scripts/run/run-step.ts +2 -9
  77. package/scripts/run/run-stop-notify.ts +1 -1
  78. package/scripts/run/run-stranded-detect.ts +3 -3
  79. package/scripts/run/run-stranded.ts +25 -17
  80. package/scripts/run/wake/run-wake-cli.ts +7 -9
  81. package/scripts/run/wake/run-wake-describe.ts +3 -5
  82. package/scripts/run/wake/run-wake-driver.ts +3 -3
  83. package/scripts/run/wake/run-wake-loop.ts +4 -21
  84. package/scripts/run/wake/run-wake-restart.ts +1 -1
  85. package/scripts/run/wake/run-wake.ts +6 -7
  86. package/scripts/sync/sync-ai-files.ts +2 -0
  87. package/scripts/version/latest-update.ts +20 -6
  88. package/scripts/version/peer-floor.ts +99 -0
  89. package/scripts/version/playwright-browsers.ts +109 -0
  90. package/scripts/version/publishable-range-check.ts +41 -4
  91. package/templates/SECURITY.md +23 -0
  92. package/scripts/backlog/backlog-idle.ts +0 -88
@@ -157,28 +157,40 @@ function alert_of(spec: GaugeSpec, value: number, pressure: number | undefined):
157
157
  return verdict ?? threshold_alert(value, spec)
158
158
  }
159
159
 
160
+ // One gauge's reading: its value, the width its figure is right-aligned to where that is wider than
161
+ // the figure's own, and the kernel's pressure verdict where the gauge takes one.
162
+ interface Reading {
163
+ value: number | undefined
164
+ width: number
165
+ pressure?: number | undefined
166
+ }
167
+
160
168
  // Icon, figure, then bar: the figure is what a person reads, and its fixed
161
169
  // width keeps every bar starting in one column. The bar stops full past `spec.full`; the figure still
162
170
  // says how far past, and takes the warning colors only, so a quiet machine's figures stay the terminal's
163
171
  // own.
164
- function gauge(spec: GaugeSpec, value: number | undefined, pressure?: number): string | undefined {
172
+ function gauge(spec: GaugeSpec, reading: Reading): string | undefined {
173
+ const { value, width } = reading
174
+
165
175
  if (value === undefined) return undefined
166
176
 
167
- const alert = alert_of(spec, value, pressure)
177
+ const alert = alert_of(spec, value, reading.pressure)
168
178
  const warning = alert === 'normal' ? undefined : GAUGE_SHADES[alert]
169
179
  const bar = bar_of(value, spec.full, undefined, warning ?? spec.normal)
170
180
 
171
- return `${spec.icon} ${painted(warning, spec.format(value))} ${bar}`
181
+ return `${spec.icon} ${painted(warning, spec.format(value).padStart(width))} ${bar}`
172
182
  }
173
183
 
174
- // `undefined` when not one gauge could be read, so the header draws no line at all.
175
- function line_of(gauges: MachineGauges | undefined): string | undefined {
184
+ // `undefined` when not one gauge could be read, so the header draws no line at all. `width` is the
185
+ // header's figure column: a figure narrower than it is right-aligned to it, so the first gauge's bar
186
+ // starts under the progress bar whichever gauge that is.
187
+ function line_of(gauges: MachineGauges | undefined, width = 0): string | undefined {
176
188
  if (gauges === undefined) return undefined
177
189
 
178
190
  const parts = [
179
- gauge(SPECS.cpu, gauges.cpu_percent),
180
- gauge(SPECS.memory, gauges.memory_percent, gauges.memory_pressure),
181
- gauge(SPECS.swap, gauges.swap_mb_per_s),
191
+ gauge(SPECS.cpu, { value: gauges.cpu_percent, width }),
192
+ gauge(SPECS.memory, { value: gauges.memory_percent, width, pressure: gauges.memory_pressure }),
193
+ gauge(SPECS.swap, { value: gauges.swap_mb_per_s, width }),
182
194
  ].filter((part) => part !== undefined)
183
195
 
184
196
  return parts.length === 0 ? undefined : parts.join(GAP)
@@ -188,6 +200,7 @@ const run_board_machine = {
188
200
  CPU_YELLOW,
189
201
  GAP,
190
202
  MEMORY_YELLOW,
203
+ PERCENT_WIDTH,
191
204
  gauges_of,
192
205
  line_of,
193
206
  percent_text,
@@ -373,11 +373,7 @@ function bounded(parts: FrameParts, bounds: FrameBounds): Array<string> {
373
373
 
374
374
  function render(view: BoardView): Array<string> {
375
375
  const { header, notes } = view
376
- const above = [
377
- ...run_board_header.header_lines(header),
378
- ...resume_lines(view.resume),
379
- ...run_board_header.idle_lines(header),
380
- ]
376
+ const above = [...run_board_header.header_lines(header), ...resume_lines(view.resume)]
381
377
  const parts = {
382
378
  above,
383
379
  plan: plan_sections(header, view.size),
@@ -28,7 +28,6 @@ function ended_span(ended: EndedRun | undefined): BoardSpan | undefined {
28
28
  }
29
29
 
30
30
  // The running run wins over the ended one, so a run that begins replaces the finished board at once.
31
- // An expired carry is still the running run's budget spent, not a run that ended, so it draws nothing.
32
31
  function span_of(read: CarryRead, ended: EndedRun | undefined): BoardSpan | undefined {
33
32
  if (read.kind !== 'carried') return read.kind === 'none' ? ended_span(ended) : undefined
34
33
 
@@ -1,4 +1,3 @@
1
- import { backlog_idle, type IdleWindow } from '#scripts/backlog/backlog-idle'
2
1
  import {
3
2
  has_label_name,
4
3
  IN_PROGRESS_LABEL,
@@ -30,8 +29,6 @@ interface ItemStatus {
30
29
  // The run as a whole, read off the same stream.
31
30
  interface RunActivity {
32
31
  last_event_ms: number | undefined
33
- // The idle watch's window, when the newest one opened after the newest launch.
34
- idle: IdleWindow | undefined
35
32
  // The run's last position is a `stop` that names no child — the run itself ended.
36
33
  is_stopped: boolean
37
34
  }
@@ -243,19 +240,6 @@ function settle_closed(
243
240
  return new Map([...statuses].map(([issue, status]) => [issue, settled(issue, status, read)]))
244
241
  }
245
242
 
246
- function newest_of(events: ReadonlyArray<RunEvent>, kind: string): RunEvent | undefined {
247
- return events.findLast((event) => event.kind === kind)
248
- }
249
-
250
- function idle_of(events: ReadonlyArray<RunEvent>): IdleWindow | undefined {
251
- const idle = newest_of(events, KIND.IDLE)
252
- const launch = newest_of(events, KIND.CHILD_LAUNCH)
253
-
254
- if (idle === undefined || (launch !== undefined && launch.pos > idle.pos)) return undefined
255
-
256
- return backlog_idle.parse(idle.text)
257
- }
258
-
259
243
  function is_run_stop(event: RunEvent | undefined): boolean {
260
244
  return event?.kind === KIND.STOP && run_event_scope.issue_named(event) === undefined
261
245
  }
@@ -268,7 +252,6 @@ function activity_of(events: ReadonlyArray<RunEvent>, is_ended = false): RunActi
268
252
 
269
253
  return {
270
254
  last_event_ms: last === undefined ? undefined : moment(last),
271
- idle: idle_of(events),
272
255
  is_stopped: is_ended || is_run_stop(position),
273
256
  }
274
257
  }
@@ -135,6 +135,7 @@ function header_of(
135
135
  machine: state.gauges,
136
136
  usages: state.usages,
137
137
  form: redraw.ports.form,
138
+ repo: state.plan?.context.repo,
138
139
  spinner: redraw.ports.is_tty ? SPOT : undefined,
139
140
  link: run_board_link.linker(state.plan?.context.repo, redraw.ports.link),
140
141
  }
@@ -39,7 +39,6 @@ const RESUMED_VERDICT = 'resumed'
39
39
  const CARRIED_VERDICT = 'carried'
40
40
  const COUNTED_VERDICT = 'counted'
41
41
  const ENDED_VERDICT = 'ended'
42
- const EXPIRED_VERDICT = 'expired'
43
42
  const NONE_VERDICT = 'none'
44
43
  const UNREADABLE_VERDICT = 'unreadable'
45
44
  const MISMATCH_VERDICT = 'mismatch'
@@ -94,28 +93,14 @@ function report_unreadable(is_json: boolean): number {
94
93
  return report(UNREADABLE_VERDICT, undefined, is_json, FAILURE_EXIT_CODE)
95
94
  }
96
95
 
97
- function report_expired(carry: RunCarry, is_json: boolean): number {
98
- console.error(run_carry.expired_message(carry))
99
-
100
- return report_carry(EXPIRED_VERDICT, carry, is_json)
101
- }
102
-
103
96
  function report_read(read: CarryRead, is_json: boolean): number {
104
97
  if (read.kind === 'none') return report(NONE_VERDICT, undefined, is_json, SUCCESS_EXIT_CODE)
105
98
 
106
99
  if (read.kind === 'unreadable') return report_unreadable(is_json)
107
100
 
108
- if (read.kind === 'expired') return report_expired(read.carry, is_json)
109
-
110
101
  return report_carry(CARRIED_VERDICT, read.carry, is_json)
111
102
  }
112
103
 
113
- function report_busy(carry: RunCarry, is_json: boolean): number {
114
- console.error(run_carry.busy_message(carry))
115
-
116
- return report(BUSY_VERDICT, carry, is_json, FAILURE_EXIT_CODE)
117
- }
118
-
119
104
  function refusal_message(claim: CarryClaim, carry: RunCarry, invocation: string): string {
120
105
  if (claim === 'busy') return run_carry.busy_message(carry)
121
106
 
@@ -149,7 +134,7 @@ function report_adoption(
149
134
 
150
135
  // **Whose record it is, is the classifier's answer and not this command's guess.** `--end` is only
151
136
  // reached on the clean-finish path, so a run that crashed or stopped on a guard leaves its record
152
- // standing for up to the whole-run bound; `resumed` is reached only where the run itself declared the
137
+ // standing until a person carries or discards it; `resumed` is reached only where the run itself declared the
153
138
  // cut, or where `--resume` says the reader has decided to carry it.
154
139
  function report_claim(
155
140
  target: string,
@@ -166,49 +151,17 @@ function report_claim(
166
151
  return report(CLAIM_VERDICTS[claim], carry, is_json, FAILURE_EXIT_CODE)
167
152
  }
168
153
 
169
- function start_fresh(
170
- target: string,
171
- request: CarryClaimRequest,
172
- is_replacement: boolean,
173
- is_json: boolean,
174
- ): number {
175
- const carry = is_replacement
176
- ? run_carry.replace_carry(target, request.invocation, request.owner)
177
- : run_carry.begin_carry(target, request.invocation, request.owner)
154
+ function start_fresh(target: string, request: CarryClaimRequest, is_json: boolean): number {
155
+ const carry = run_carry.begin_carry(target, request.invocation, request.owner)
178
156
 
179
157
  if (carry === undefined) return report_lost(is_json)
180
158
 
181
159
  return report_carry(BEGAN_VERDICT, carry, is_json)
182
160
  }
183
161
 
184
- // **The bound ends *that* run, so a spent record whose owner is still running is not replaced
185
- // either.** Replacing it would delete a live parent's budget and answer `began` — two parents on one
186
- // record. Only that run, or a person who knows it is over, ends it.
187
- function begin_over_expired(
188
- target: string,
189
- carry: RunCarry,
190
- request: CarryClaimRequest,
191
- is_json: boolean,
192
- ): number {
193
- if (run_carry.is_foreign_live_owner(carry, request.owner)) return report_busy(carry, is_json)
194
-
195
- // **A hand-off says the resumption *is* that run, so the spent bound is this session's too.**
196
- // Replaced here, the record would come back with `started_at` set to now — and `backlog:budget
197
- // --started` reads that field, so the 8-hour bound would restart at every cut and never end the
198
- // run. The answer is `expired`, and a person who really is starting a new run clears the record
199
- // with `--end`, which is what the expiry message says.
200
- if (carry.is_handed_off === true) return report_expired(carry, is_json)
201
-
202
- console.error(run_carry.expired_message(carry))
203
-
204
- return start_fresh(target, request, true, is_json)
205
- }
206
-
207
- // **A live record is never replaced, and never resumed into by something else.** Replacing one would
208
- // restart the budget the cut exists to carry, and resuming into one would put two parents on a single
209
- // budget. An expired one is replaced,
210
- // because its run has spent the whole-run bound and a person typing the keyword again is starting a
211
- // new run — unless its owner is still running, which is the case above.
162
+ // **A standing record is never replaced, and never resumed into by something else.** Replacing one
163
+ // would restart the budget the cut exists to carry, and resuming into one would put two parents on a
164
+ // single budget. Its age changes none of that: a record is replaced by nothing but a person's `--end`.
212
165
  function begin(target: string, request: CarryClaimRequest, is_json: boolean): number {
213
166
  const read = run_carry.read_carry(target)
214
167
 
@@ -216,9 +169,7 @@ function begin(target: string, request: CarryClaimRequest, is_json: boolean): nu
216
169
 
217
170
  if (read.kind === 'carried') return report_claim(target, read.carry, request, is_json)
218
171
 
219
- if (read.kind === 'expired') return begin_over_expired(target, read.carry, request, is_json)
220
-
221
- return start_fresh(target, request, false, is_json)
172
+ return start_fresh(target, request, is_json)
222
173
  }
223
174
 
224
175
  function report_nothing_to_resume(is_json: boolean): number {
@@ -237,8 +188,6 @@ function adopt(target: string, request: CarryClaimRequest, is_json: boolean): nu
237
188
 
238
189
  if (read.kind === 'unreadable') return report_unreadable(is_json)
239
190
 
240
- if (read.kind === 'expired') return report_expired(read.carry, is_json)
241
-
242
191
  return report_claim(target, read.carry, request, is_json)
243
192
  }
244
193
 
@@ -334,9 +283,7 @@ async function count(
334
283
 
335
284
  await record_retrospective(request)
336
285
 
337
- return read.kind === 'expired'
338
- ? report_expired(carry, is_json)
339
- : report_carry(COUNTED_VERDICT, carry, is_json)
286
+ return report_carry(COUNTED_VERDICT, carry, is_json)
340
287
  }
341
288
 
342
289
  // **A stop over a record that is still there pushes one ⏸️ confirmation before the record goes**.
@@ -463,7 +410,6 @@ const run_carry_cli = {
463
410
  CARRIED_VERDICT,
464
411
  COUNTED_VERDICT,
465
412
  ENDED_VERDICT,
466
- EXPIRED_VERDICT,
467
413
  MISMATCH_VERDICT,
468
414
  NONE_VERDICT,
469
415
  OVER_VERDICT,
@@ -55,7 +55,7 @@ function record_ended(
55
55
  now: Date = new Date(),
56
56
  stopped?: string,
57
57
  ): void {
58
- if (read.kind !== 'carried' && read.kind !== 'expired') return
58
+ if (read.kind !== 'carried') return
59
59
 
60
60
  const { invocation, started_at, owner_transcript } = read.carry
61
61
  const ended_at = now.toISOString()
@@ -1,8 +1,6 @@
1
- import { backlog_budget } from '#scripts/backlog/backlog-budget'
2
1
  import { git_command } from '#scripts/git/git-command'
3
2
  import { process_identity } from '#scripts/josh/process-identity'
4
3
  import { stamp_file } from '#scripts/josh/stamp-file'
5
- import { stamp_record, type StampRecordSpec } from '#scripts/josh/stamp-record'
6
4
  import { json_value } from '#scripts/lib/json-value'
7
5
  import { run_invocation } from '#scripts/run/run-invocation'
8
6
  import { z } from 'zod'
@@ -10,7 +8,7 @@ import { run_carry_added, type CarryAddition } from './run-carry-added'
10
8
  import { run_carry_change } from './run-carry-change'
11
9
  import { run_carry_conversation } from './run-carry-conversation'
12
10
 
13
- // A `backlogrun` declares a budget — `--max`, `--idle` and the 8-hour whole-run bound — and the session
11
+ // A `backlogrun` declares a budget — `--max` and `--idle` — and the session
14
12
  // cut ends the session, so the next one would start from nothing. **The cut is an execution detail of
15
13
  // the same authorization**, so the budget has to survive it: this record carries it across sessions,
16
14
  // where there is no epic to read progress back off when the run began from the backlog.
@@ -20,9 +18,10 @@ import { run_carry_conversation } from './run-carry-conversation'
20
18
  // one *invocation's* budget, and that invocation opens lanes — each a work tree of its own — so the
21
19
  // key is the common git directory, which every lane of one repository shares.
22
20
  //
23
- // **The 8-hour bound is the record's expiry.** Held as an age rather than as prose, the bound is
24
- // decided the same way `backlog:budget` decides the other two: the command answers, and the run
25
- // obeys. A run that reads `expired` has spent the bound whatever any count says.
21
+ // **The record has no expiry of its own.** A run is bounded by its idle wait, which `backlog:budget`
22
+ // counts from when the run last had work; an age counted from `started_at` would end a run that is
23
+ // still working or still inside that wait. So a record a run never ended — a crash — stays `standing`
24
+ // until a person carries it with `--resume` or discards it with `--end`.
26
25
  //
27
26
  // **Whose record it is, is decided by liveness and by a declared hand-off — never by the invocation
28
27
  // text**. Comparing the `invocation` string alone walks straight through the case that happens most: a
@@ -38,21 +37,14 @@ import { run_carry_conversation } from './run-carry-conversation'
38
37
 
39
38
  const CARRY_PREFIX = 'josh-run-carry-'
40
39
  const REPOSITORY_DIRECTORY_INDEX = 1
41
- // **The whole-run bound is `backlog-budget.ts`'s, imported rather than restated.** `backlogrun.md` →
42
- // "Waiting, and never waiting forever" states the figure and that module applies it; a second copy
43
- // here would drift, and the drift is silent in exactly the direction that matters — raised there and
44
- // still 8 here, this record answers `expired`, `--begin` replaces it, and the budget restarts at
45
- // zero — the loss this record exists to prevent.
46
- const CARRY_MAX_AGE_HOURS = backlog_budget.WHOLE_RUN_BUDGET_HOURS
47
- const CARRY_MAX_AGE_MS = backlog_budget.WHOLE_RUN_BUDGET_MS
48
40
  const NO_INCREMENT = 0
49
41
 
50
42
  // The most session cuts one invocation may take. This bounds `cuts`, which
51
43
  // only `run:carry --cut` increments — a `backlogrun`'s session hand-off at a child's merge (~one per
52
- // 50 minutes over the 8-hour whole-run budget), not the `run:cut` phase cuts a lane child takes. Each
44
+ // 50 minutes of work), not the `run:cut` phase cuts a lane child takes. Each
53
45
  // cut drops the context it accumulated but pays a cold preamble to resume, so past a point the
54
- // re-establishment costs more than the accumulation it sheds; six sits well under the ~10 an 8-hour run
55
- // could otherwise take and refuses a run that has begun to churn. `is_at_cut_cap` reads it, and the
46
+ // re-establishment costs more than the accumulation it sheds; six sits well under the ~10 a night of
47
+ // work could otherwise take and refuses a run that has begun to churn. `is_at_cut_cap` reads it, and the
56
48
  // `--cut` count is refused at it rather than incrementing past it — except a headless judgment
57
49
  // session's hand-back to the driver, which `run_headless.is_cut_capped` exempts.
58
50
  const MAX_CUTS = 6
@@ -185,11 +177,7 @@ interface CarryChange {
185
177
  retrospective?: boolean
186
178
  }
187
179
 
188
- type CarryRead =
189
- | { kind: 'none' }
190
- | { kind: 'carried'; carry: RunCarry }
191
- | { kind: 'expired'; carry: RunCarry }
192
- | { kind: 'unreadable' }
180
+ type CarryRead = { kind: 'none' } | { kind: 'carried'; carry: RunCarry } | { kind: 'unreadable' }
193
181
 
194
182
  const NO_OWNER: CarryOwner = {}
195
183
 
@@ -234,32 +222,23 @@ function parse_carry(raw: string): RunCarry | undefined {
234
222
  return json_value.parse_with(raw, run_carry_schema)
235
223
  }
236
224
 
237
- function started_at_of(carry: RunCarry): string {
238
- return carry.started_at
239
- }
240
-
241
- // The expiry and the classification are `stamp-record.ts`'s, shared with `run-cut.ts` and
242
- // `run-hold.ts`; what a `started_at` that is not a date means is decided there.
243
- const CARRY_SPEC: StampRecordSpec<RunCarry> = {
244
- parse: parse_carry,
245
- timestamp_of: started_at_of,
246
- max_age_ms: CARRY_MAX_AGE_MS,
247
- }
248
-
249
- function is_expired(carry: RunCarry, now: Date): boolean {
250
- return stamp_record.is_older_than(started_at_of(carry), CARRY_MAX_AGE_MS, now)
251
- }
225
+ // **A record that cannot be read is `unreadable`, never `none`** — absent is none; present and
226
+ // unparseable is the state a guard must not fall open on. A `started_at` that is not a date is the
227
+ // same state: it passes the schema as a string, and every reader of the record measures from it.
228
+ function classify(raw: string | undefined): CarryRead {
229
+ if (raw === undefined) return { kind: 'none' }
252
230
 
253
- function classify(raw: string | undefined, now: Date): CarryRead {
254
- const read = stamp_record.classify(raw, CARRY_SPEC, now)
231
+ const carry = parse_carry(raw)
255
232
 
256
- if (!('record' in read)) return read
233
+ if (carry === undefined || Number.isNaN(Date.parse(carry.started_at))) {
234
+ return { kind: 'unreadable' }
235
+ }
257
236
 
258
- return { kind: read.kind, carry: read.record }
237
+ return { kind: 'carried', carry }
259
238
  }
260
239
 
261
- function read_carry(target: string, now: Date = new Date()): CarryRead {
262
- return classify(stamp_file.read_stamp_text(target), now)
240
+ function read_carry(target: string): CarryRead {
241
+ return classify(stamp_file.read_stamp_text(target))
263
242
  }
264
243
 
265
244
  // The identity to record for a pid the caller declared. `read_start` is what makes the pair an
@@ -304,20 +283,6 @@ function begin_carry(
304
283
  return stamp_file.create_stamp(target, carry) ? carry : undefined
305
284
  }
306
285
 
307
- // Replacing a record whose bound is spent. It removes and then creates exclusively rather than
308
- // writing over the old bytes, for the reason `run-hold.ts` replaces a stale hold the same way: a
309
- // plain write would put the race back one branch over.
310
- function replace_carry(
311
- target: string,
312
- invocation: string,
313
- owner: CarryOwner = NO_OWNER,
314
- now: Date = new Date(),
315
- ): RunCarry | undefined {
316
- stamp_file.remove_stamp(target)
317
-
318
- return begin_carry(target, invocation, owner, now)
319
- }
320
-
321
286
  // Taking over a standing record: the counters and `started_at` are the run's and stay, the owner
322
287
  // becomes this session, and the hand-off is spent. **`cuts` is not incremented here** — `--cut`
323
288
  // counts the cut, and a record adopted after a crash crossed no cut at all.
@@ -424,9 +389,7 @@ function end_carry(target: string): void {
424
389
  // no record — `none` or `unreadable` — is not a run whose retrospective has run, so it answers `false`;
425
390
  // `run:step` reads it to decide whether the stop position still owes a retrospective.
426
391
  function retrospective_done_of(read: CarryRead): boolean {
427
- if (read.kind !== 'carried' && read.kind !== 'expired') return false
428
-
429
- return read.carry.retrospective === true
392
+ return read.kind === 'carried' && read.carry.retrospective === true
430
393
  }
431
394
 
432
395
  // **What the resumed session has to be told, computed rather than stored.** A stored remainder would
@@ -458,10 +421,6 @@ function describe_carry(carry: RunCarry): string {
458
421
  return `${carry.invocation} started ${carry.started_at}; ${String(carry.merged)} merged, ${String(carry.filed)} filed, ${String(carry.failures)} failed in a row, ${String(carry.outages)} outage(s) in a row, ${String(carry.cuts)} cut(s) crossed${done_note(carry)}`
459
422
  }
460
423
 
461
- function expired_message(carry: RunCarry): string {
462
- return `The ${String(CARRY_MAX_AGE_HOURS)}-hour whole-run bound is spent: ${describe_carry(carry)}. Clear it with \`${END_COMMAND}\`.`
463
- }
464
-
465
424
  // A record left standing by a run that never reached `--end`. Naming both invocations is the point:
466
425
  // the reader has to see that the budget in the way belongs to something else before deciding whether
467
426
  // that run is over.
@@ -525,8 +484,6 @@ function unknown_message(): string {
525
484
  }
526
485
 
527
486
  const run_carry = {
528
- CARRY_MAX_AGE_HOURS,
529
- CARRY_MAX_AGE_MS,
530
487
  END_COMMAND,
531
488
  MAX_CUTS,
532
489
  NO_OWNER,
@@ -543,11 +500,9 @@ const run_carry = {
543
500
  is_at_cut_cap,
544
501
  describe_carry,
545
502
  end_carry,
546
- expired_message,
547
503
  mismatch_message,
548
504
  fresh_carry,
549
505
  is_count_refused,
550
- is_expired,
551
506
  is_foreign_live_owner,
552
507
  is_owner_live,
553
508
  lost_message,
@@ -556,7 +511,6 @@ const run_carry = {
556
511
  parse_carry,
557
512
  read_carry,
558
513
  remaining_of,
559
- replace_carry,
560
514
  repository_directory,
561
515
  retrospective_done_of,
562
516
  standing_message,
@@ -50,12 +50,12 @@ const { PRE_GATE_PHASE, IMPLEMENTATION_PHASE } = agent_role_profile
50
50
  // `pnpm josh cost --cut` hand-off uses (`backlogrun-progress.md` → "The hand-off"). The shared
51
51
  // threshold prevents the scheduler and worker boundaries from drifting apart.
52
52
  const IMPLEMENTATION_CONTEXT_THRESHOLD = CONTEXT_CUT_THRESHOLD
53
- // **The whole-run bound is `backlog-budget.ts`'s, imported rather than restated** — the same reuse
54
- // `run-carry.ts` makes. A resume is expected within seconds of the cut, so this age is only a backstop
53
+ // **The bound is `backlog-budget.ts`'s idle wait, imported rather than restated**, so the eight hours
54
+ // stay written once. A resume is expected within seconds of the cut, so this age is only a backstop
55
55
  // against a fresh process that never started; an expired record is cleared and replaced by the next
56
56
  // cut rather than blocking it.
57
- const CUT_MAX_AGE_MS = backlog_budget.WHOLE_RUN_BUDGET_MS
58
- const CUT_MAX_AGE_HOURS = backlog_budget.WHOLE_RUN_BUDGET_HOURS
57
+ const CUT_MAX_AGE_MS = backlog_budget.DEFAULT_IDLE_MS
58
+ const CUT_MAX_AGE_HOURS = backlog_budget.IDLE_WAIT_HOURS
59
59
 
60
60
  const END_COMMAND = 'pnpm josh run:cut --end'
61
61
  const READ_COMMAND = 'pnpm josh run:cut --json'
@@ -39,7 +39,6 @@ const KIND_LABELS: Readonly<Record<EventKind, KindLabel>> = {
39
39
  heartbeat: { ja: '生存確認', en: 'heartbeat' },
40
40
  'ship-launch': { ja: '出荷を開始', en: 'ship launched' },
41
41
  'ship-stop': { ja: '出荷が停止', en: 'ship stopped' },
42
- idle: { ja: '新着待ち', en: 'idle watch' },
43
42
  filed: { ja: '起票', en: 'filed' },
44
43
  note: { ja: '意見', en: 'note' },
45
44
  'lane-phase': { ja: 'レーンの段階', en: 'lane phase' },
@@ -55,16 +55,12 @@ function scoped_events(
55
55
  return events.filter((event) => is_within(event, floor))
56
56
  }
57
57
 
58
- // The scope the run's own carry record implies. A record that reads at all names the invocation's start, and
59
- // **an expired one names it just as well**: expiry is the budget's verdict on a run, not a gap in the record,
60
- // so rounding it to unknown would withhold the scope from exactly the long runs that most need one. Only an
61
- // absent or unreadable record leaves the scope undetermined.
58
+ // The scope the run's own carry record implies. A record that reads at all names the invocation's start,
59
+ // however long ago that was; only an absent or unreadable record leaves the scope undetermined.
62
60
  function scope_of(read: CarryRead): EventScope {
63
- if (read.kind === 'carried' || read.kind === 'expired') {
64
- return { kind: SINCE_SCOPE, started_at: read.carry.started_at }
65
- }
61
+ if (read.kind !== 'carried') return UNKNOWN_EVENT_SCOPE
66
62
 
67
- return UNKNOWN_EVENT_SCOPE
63
+ return { kind: SINCE_SCOPE, started_at: read.carry.started_at }
68
64
  }
69
65
 
70
66
  // The newest event within scope, or `undefined` when the scope could not be determined or nothing falls
@@ -70,30 +70,6 @@ async function emit_once(kind: string, text: string): Promise<boolean> {
70
70
  }
71
71
  }
72
72
 
73
- /**
74
- * Append one event unless the newest event of this kind already carries this text — for a marker whose
75
- * value, not its presence, is the news. The idle window is re-derived on every
76
- * offer of a watching loop; only a window that moved is worth a line. Returns whether it appended.
77
- */
78
- async function emit_changed(kind: string, text: string): Promise<boolean> {
79
- try {
80
- const target = await stream_target()
81
-
82
- if (target === undefined) return false
83
-
84
- const newest = run_event_stream.read_events(target).findLast((event) => event.kind === kind)
85
-
86
- if (newest?.text === text) return false
87
-
88
- return run_event_stream.append(target, kind, text, now_iso()).appended
89
- } catch (error) {
90
- // Best-effort: a failed append is dropped rather than raised into the loop's work.
91
- error_text.trace_swallowed('run_event_stream_emit.emit_changed', error)
92
-
93
- return false
94
- }
95
- }
96
-
97
73
  // The events of the invocation now running: the stream outlives every invocation, so a marker a previous
98
74
  // run left — a stall that ended with no dispatch after it — must not hold back this run's first one. An
99
75
  // undetermined scope (no carry record) keeps the whole stream, the side that under-notifies rather than
@@ -187,7 +163,6 @@ const run_event_stream_emit = {
187
163
  all_events,
188
164
  current_events,
189
165
  emit,
190
- emit_changed,
191
166
  emit_heartbeat,
192
167
  emit_once,
193
168
  emit_once_since,