@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
@@ -106,12 +106,11 @@ the epic body; what merged the epic's task list.
106
106
 
107
107
  | Setting | Value | Meaning |
108
108
  | --- | --- | --- |
109
- | Polling interval | 60 s | **In flight, a floor between two asks, never a clock** — the wake is `lane:await`'s completion or the watcher's arrival exit, never a report. **With nothing in flight no wake comes: it is the parent's own clock.** |
110
- | `backlogrun` idle-watch poll | 5 min | Not the interval above, and a floor in the same sense. |
109
+ | Polling interval | 60 s | **In flight, a floor between two asks, never a clock** — the wake is `lane:await`'s completion or the watcher's arrival exit, never a report. **With nothing in flight no wake comes: it is the parent's own clock.** A `backlogrun` idle watch is polled at this same interval. |
111
110
  | Silent delegated unit | 30 min | Not the child's duration — the time its output has gone **unchanged**. Past it, ask `run:liveness` and book a stopped unit as a failure. |
112
111
  | Stale `in-progress` | 90 min | Past it, the other session is gone. |
113
112
  | Publish wait | 10 min | `josh propagate`'s own budget. A failed publish never appears. |
114
- | Whole run | 8 h | Past it, the run needs a person. |
113
+ | `backlogrun` idle watch | 8 h | Counted from when the run's hands became free, never from its start; `pnpm josh backlog:budget` decides it. |
115
114
 
116
115
  Each timeout **ends the wait and reports** — none is retried indefinitely. Rationale for the figures:
117
116
  `docs/maintainers/backlogrun-progress-rationale.md` → "Why the parent waits on classification, not a
@@ -29,7 +29,7 @@ provenance of each rule: `docs/maintainers/backlogrun-steps-rationale.md` → "W
29
29
  | The bound | What it limits | Counted by |
30
30
  | --- | --- | --- |
31
31
  | `--max` | merges per invocation — a run's own filing competes for it | `run:carry --merged` |
32
- | `--idle`, and the 8-hour whole-run bound | how long the run looks for more | the record's `started_at` |
32
+ | `--idle` (8 hours unless typed) | how long the run waits once nothing can proceed | `--active`, the moment its hands became free |
33
33
  | **Ten filings per invocation** | additions to the pool on **every** filing route (`prerequisite.md`) — `route:tier-a`, `route:interrupt` and a review branch-2 filing are exempt from the depth test (`observation-filing.md`), never from this ceiling | `run:carry --filed` |
34
34
  | The WIP cap on open issues | the pool across invocations | `prompts/collaboration-workflow/wip-cap.md` |
35
35
 
@@ -75,8 +75,6 @@ and `--filed 1`, and **`--cut` is the session's last write**.
75
75
  | `busy` / `mismatch` / `unreadable` / `unknown` | **Stop; do not open a lane** — report what it printed; never resume into another invocation's record |
76
76
  | `over` | **End the conversation**; retype the invocation in a fresh session |
77
77
  | `standing` | **Stop; do not open a lane**: report `pnpm josh run:carry --resume "<invocation>" --owner "$PPID"` and `pnpm josh run:carry --end` for the person to choose — a crash from a run-tooling defect this run has since fixed you resume yourself (`upstream-interrupt.md` → "実行中のリポジトリ自身のラン機構の不具合") |
78
- | `expired` | On standard output: report, stop and `--end` it. On standard error ahead of `began`: a new run replaced it |
79
-
80
78
  **End the record when the run ends** — `pnpm josh run:report`, then `pnpm josh run:carry --end`, **never
81
79
  batched with it**, and `pnpm josh run:wake --stop` in the same turn. **A run that ends by _stopping_**
82
80
  (a `stop` verdict or the consecutive-failure guard) **ends with `pnpm josh run:carry --end --stopped
@@ -140,7 +138,7 @@ section the row names:
140
138
 
141
139
  **A candidate only in another repository is never a token** — report it with its checkout. The budgets,
142
140
  verdicts and stops are the driver's own: `docs/maintainers/backlogrun-driver.md` (rationale:
143
- `docs/maintainers/backlogrun-steps-rationale.md` → "Why the idle watch defaults to 30 minutes";
141
+ `docs/maintainers/backlogrun-steps-rationale.md` → "Why the idle watch lasts 8 hours from when the hands became free";
144
142
  `docs/maintainers/backlogrun-steps-rationale.md` → "Why the cost check skips a watch").
145
143
 
146
144
  Rationale: `docs/maintainers/backlogrun-steps-rationale.md` → "Why the loop's contract is shaped this way"
@@ -60,8 +60,7 @@ Issue, as after any filing.
60
60
  `issue:file`'s default applies the label and the next run picks the improvements up without waiting
61
61
  for a person. **`backlogrun-steps.md` → "What one invocation approves" is that default's single
62
62
  source**; it is written there, beside the brakes that bound it, rather than restated here. Every brake still counts a retrospective's filings: the
63
- ten-per-invocation ceiling, the WIP cap, `--max`, the 150,000-token session budget and the 8-hour
64
- whole-run bound.
63
+ ten-per-invocation ceiling, the WIP cap, `--max` and the 150,000-token session budget.
65
64
 
66
65
  **Close with `pnpm josh run:carry --retrospective --summary "<result>" --owner "$PPID"`.** It marks the
67
66
  retrospective run on the carry record, so `run:step` prints `stop` from then on rather than the step a
@@ -9,12 +9,12 @@ import {
9
9
  load_environment_file,
10
10
  notice_refusal_path,
11
11
  refusal_path
12
- } from "./chunk-7ZM464OW.js";
13
- import "./chunk-DHSKBAQ2.js";
14
- import "./chunk-E7LZL5AN.js";
15
- import "./chunk-TORRSNDJ.js";
16
- import "./chunk-NBEAGPS4.js";
17
- import "./chunk-RW4XMF47.js";
12
+ } from "./chunk-LMWBRJIP.js";
13
+ import "./chunk-EAQEGME7.js";
14
+ import "./chunk-EBSHN25W.js";
15
+ import "./chunk-SL4GQ2XP.js";
16
+ import "./chunk-J3KSS7II.js";
17
+ import "./chunk-2YSW3CXH.js";
18
18
  import "./chunk-NEC4AT4Z.js";
19
19
  export {
20
20
  DISABLED_VALUES,
@@ -25,134 +25,6 @@ function is_record(value) {
25
25
  }
26
26
  var json_value = { parse_or_undefined, parse_with, is_record };
27
27
 
28
- // scripts/backlog/backlog-budget.ts
29
- var MS_PER_MINUTE = 6e4;
30
- var MINUTES_PER_HOUR = 60;
31
- var WHOLE_RUN_BUDGET_HOURS = 8;
32
- var WHOLE_RUN_BUDGET_MINUTES = WHOLE_RUN_BUDGET_HOURS * MINUTES_PER_HOUR;
33
- var WHOLE_RUN_BUDGET_MS = WHOLE_RUN_BUDGET_MINUTES * MS_PER_MINUTE;
34
- var DEFAULT_IDLE_MINUTES = 30;
35
- var DEFAULT_IDLE_MS = DEFAULT_IDLE_MINUTES * MS_PER_MINUTE;
36
- var IDLE_POLL_MINUTES = 5;
37
- var RUN_VERDICT = "run";
38
- var WATCH_VERDICT = "watch";
39
- var STOP_VERDICT = "stop";
40
- var TRIAGE_VERDICT = "triage";
41
- var ANSWERS = ["blocked", "candidates", "exhausted", "parked", "unreadable", "untriaged"];
42
- var WHOLE_RUN_REASON = `The whole-run bound of ${String(WHOLE_RUN_BUDGET_HOURS)} hours has passed. An unattended run that has not finished overnight needs a person, not more waiting.`;
43
- var PARKED_REASON = "Nothing opted in can proceed without a person. Report the parked issues and finish.";
44
- var UNREADABLE_REASON = "The backlog listing could not be read, so nothing was offered. Report what the command printed and finish \u2014 it is not an empty backlog.";
45
- var BLOCKED_REASON = "Everything opted in is blocked or already running, so waiting can still change the answer. Poll again at the polling interval.";
46
- var TRIAGE_REASON = "An opted-in candidate carries neither `run:solo` nor `run:lane`, so nothing starts until it is judged. Read each issue named above, record its order and one of the two labels with a comment, then ask again.";
47
- var NO_IDLE_WATCH_REASON = "The backlog is empty and the idle watch was turned off with `--idle 0`, so the run finishes here.";
48
- function to_minutes(duration_ms) {
49
- return String(Math.ceil(duration_ms / MS_PER_MINUTE));
50
- }
51
- function max_reached_reason(merged, max_issues) {
52
- return `This run's maximum of ${String(max_issues)} is reached, with ${String(merged)} merged. Report what was done, including how many were picked up during an idle watch, and finish.`;
53
- }
54
- function draining_reason(running, reason) {
55
- return `Start nothing more and poll until the running children merge (running: ${String(running)}). Then report and finish \u2014 ${reason}`;
56
- }
57
- function idle_expired_reason(idle_budget_ms) {
58
- return `The backlog stayed empty for the whole ${to_minutes(idle_budget_ms)}-minute idle watch. Report what was done, including how many were picked up during the watch, and finish.`;
59
- }
60
- function next_ask_minutes(left_ms) {
61
- const left_minutes = Number(to_minutes(left_ms));
62
- return String(Math.min(IDLE_POLL_MINUTES, left_minutes));
63
- }
64
- function idle_watch_reason(left_ms) {
65
- return `The backlog is empty and the idle watch has about ${to_minutes(left_ms)} minutes left. Release the working tree, ask again in ${next_ask_minutes(left_ms)} minutes, and restart the watch the moment a candidate appears.`;
66
- }
67
- function running_watch_reason(running, left_ms) {
68
- return `The backlog is empty and the idle watch has about ${to_minutes(left_ms)} minutes left, with ${String(running)} still running. Keep the working tree until they merge and poll at the polling interval, not the idle poll \u2014 the watch is running down meanwhile.`;
69
- }
70
- function watching_reason(input, left_ms) {
71
- if (input.running === 0) return idle_watch_reason(left_ms);
72
- return running_watch_reason(input.running, left_ms);
73
- }
74
- function taken_by(input) {
75
- return input.merged + input.running;
76
- }
77
- function run_reason(input) {
78
- const { max_issues } = input;
79
- if (max_issues === void 0) return "No maximum was given, so start every issue offered.";
80
- return `Start at most ${String(max_issues - taken_by(input))} more: this run's maximum is ${String(max_issues)}, with ${String(input.merged)} merged and ${String(input.running)} running.`;
81
- }
82
- var ANSWER_STOP_REASONS = {
83
- parked: PARKED_REASON,
84
- unreadable: UNREADABLE_REASON
85
- };
86
- function whole_run_reason(input) {
87
- if (input.now_ms - input.started_at_ms < WHOLE_RUN_BUDGET_MS) return void 0;
88
- return WHOLE_RUN_REASON;
89
- }
90
- function stop_reason(input) {
91
- return ANSWER_STOP_REASONS[input.answer] ?? whole_run_reason(input);
92
- }
93
- function stopping(input, reason) {
94
- if (input.running === 0) return { verdict: STOP_VERDICT, reason };
95
- return { verdict: WATCH_VERDICT, reason: draining_reason(input.running, reason) };
96
- }
97
- function max_decision(input) {
98
- const { max_issues } = input;
99
- if (max_issues === void 0 || taken_by(input) < max_issues) return void 0;
100
- return stopping(input, max_reached_reason(input.merged, max_issues));
101
- }
102
- function idle_decision(input) {
103
- const { idle_budget_ms } = input;
104
- if (idle_budget_ms === void 0) return stopping(input, NO_IDLE_WATCH_REASON);
105
- const left_ms = idle_budget_ms - (input.now_ms - input.active_at_ms);
106
- if (left_ms <= 0) return stopping(input, idle_expired_reason(idle_budget_ms));
107
- return { verdict: WATCH_VERDICT, reason: watching_reason(input, left_ms) };
108
- }
109
- function is_finish(input) {
110
- return stop_reason(input) === void 0 && max_decision(input) === void 0;
111
- }
112
- var FIXED_DECISIONS = {
113
- blocked: { verdict: WATCH_VERDICT, reason: BLOCKED_REASON },
114
- untriaged: { verdict: TRIAGE_VERDICT, reason: TRIAGE_REASON }
115
- };
116
- function answer_decision(input) {
117
- if (input.answer === "candidates") return { verdict: RUN_VERDICT, reason: run_reason(input) };
118
- return FIXED_DECISIONS[input.answer] ?? idle_decision(input);
119
- }
120
- function decide(input) {
121
- const reason = stop_reason(input);
122
- if (reason !== void 0) return stopping(input, reason);
123
- const capped = max_decision(input);
124
- if (capped !== void 0) return capped;
125
- return answer_decision(input);
126
- }
127
- var backlog_budget = {
128
- ANSWERS,
129
- BLOCKED_REASON,
130
- DEFAULT_IDLE_MINUTES,
131
- DEFAULT_IDLE_MS,
132
- IDLE_POLL_MINUTES,
133
- MS_PER_MINUTE,
134
- NO_IDLE_WATCH_REASON,
135
- PARKED_REASON,
136
- RUN_VERDICT,
137
- STOP_VERDICT,
138
- TRIAGE_REASON,
139
- TRIAGE_VERDICT,
140
- UNREADABLE_REASON,
141
- WATCH_VERDICT,
142
- WHOLE_RUN_BUDGET_HOURS,
143
- WHOLE_RUN_BUDGET_MS,
144
- WHOLE_RUN_REASON,
145
- decide,
146
- draining_reason,
147
- idle_expired_reason,
148
- idle_watch_reason,
149
- is_finish,
150
- max_reached_reason,
151
- run_reason,
152
- running_watch_reason,
153
- taken_by
154
- };
155
-
156
28
  // scripts/git/constants.ts
157
29
  import { platform } from "node:os";
158
30
  var GIT_COMMAND_UNIX = "/usr/bin/git";
@@ -952,23 +824,6 @@ var process_identity = {
952
824
  resolve_own_start
953
825
  };
954
826
 
955
- // scripts/josh/stamp-record.ts
956
- var NONE_READ = { kind: "none" };
957
- var UNREADABLE_READ = { kind: "unreadable" };
958
- function is_older_than(iso, max_age_ms, now) {
959
- const at = Date.parse(iso);
960
- if (Number.isNaN(at)) return true;
961
- return now.getTime() - at > max_age_ms;
962
- }
963
- function classify(raw, spec, now) {
964
- if (raw === void 0) return NONE_READ;
965
- const record = spec.parse(raw);
966
- if (record === void 0) return UNREADABLE_READ;
967
- const is_expired2 = is_older_than(spec.timestamp_of(record), spec.max_age_ms, now);
968
- return { kind: is_expired2 ? "expired" : "carried", record };
969
- }
970
- var stamp_record = { classify, is_older_than };
971
-
972
827
  // scripts/backlog/backlog-budget-cli.ts
973
828
  import { fileURLToPath } from "node:url";
974
829
 
@@ -1116,55 +971,125 @@ var cli_flags = {
1116
971
  is_value_unusable
1117
972
  };
1118
973
 
1119
- // scripts/backlog/backlog-idle.ts
1120
- var TEXT_PATTERN = /^idle since (?<since>\S+) until (?<until>\S+) \((?<bound>idle|whole-run)\) asked (?<asked>\S+)$/u;
1121
- var IDLE_BOUND = "idle";
1122
- var WHOLE_RUN_BOUND = "whole-run";
1123
- function window_of(input) {
1124
- if (input.idle_budget_ms === void 0) return void 0;
1125
- const idle_until = input.active_at_ms + input.idle_budget_ms;
1126
- const run_until = input.started_at_ms + backlog_budget.WHOLE_RUN_BUDGET_MS;
1127
- const bound = run_until < idle_until ? WHOLE_RUN_BOUND : IDLE_BOUND;
1128
- return {
1129
- since_ms: input.active_at_ms,
1130
- until_ms: Math.min(idle_until, run_until),
1131
- bound,
1132
- asked_ms: input.now_ms
1133
- };
974
+ // scripts/backlog/backlog-budget.ts
975
+ var MS_PER_MINUTE = 6e4;
976
+ var MINUTES_PER_HOUR = 60;
977
+ var IDLE_WAIT_HOURS = 8;
978
+ var DEFAULT_IDLE_MINUTES = IDLE_WAIT_HOURS * MINUTES_PER_HOUR;
979
+ var DEFAULT_IDLE_MS = DEFAULT_IDLE_MINUTES * MS_PER_MINUTE;
980
+ var OFFER_INTERVAL_SECONDS = 60;
981
+ var MS_PER_SECOND3 = 1e3;
982
+ var OFFER_INTERVAL_MS = OFFER_INTERVAL_SECONDS * MS_PER_SECOND3;
983
+ var RUN_VERDICT = "run";
984
+ var WATCH_VERDICT = "watch";
985
+ var STOP_VERDICT = "stop";
986
+ var TRIAGE_VERDICT = "triage";
987
+ var ANSWERS = ["blocked", "candidates", "exhausted", "parked", "unreadable", "untriaged"];
988
+ var BLOCKED_REASON = "Everything opted in is blocked or already running, so waiting can still change the answer. Poll again at the polling interval.";
989
+ var TRIAGE_REASON = "An opted-in candidate carries neither `run:solo` nor `run:lane`, so nothing starts until it is judged. Read each issue named above, record its order and one of the two labels with a comment, then ask again.";
990
+ var EMPTY_BACKLOG = "The backlog is empty";
991
+ var WAITING_ON = {
992
+ exhausted: EMPTY_BACKLOG,
993
+ parked: "Nothing opted in can proceed without a person",
994
+ unreadable: "The backlog listing could not be read"
995
+ };
996
+ function waiting_on(answer) {
997
+ return WAITING_ON[answer] ?? EMPTY_BACKLOG;
998
+ }
999
+ function no_idle_watch_reason(answer) {
1000
+ return `${waiting_on(answer)} and the idle watch was turned off with \`--idle 0\`, so the run finishes here.`;
1001
+ }
1002
+ function to_minutes(duration_ms) {
1003
+ return String(Math.ceil(duration_ms / MS_PER_MINUTE));
1004
+ }
1005
+ function max_reached_reason(merged, max_issues) {
1006
+ return `This run's maximum of ${String(max_issues)} is reached, with ${String(merged)} merged. Report what was done, including how many were picked up during an idle watch, and finish.`;
1007
+ }
1008
+ function draining_reason(running, reason) {
1009
+ return `Start nothing more and poll until the running children merge (running: ${String(running)}). Then report and finish \u2014 ${reason}`;
1134
1010
  }
1135
- function iso_of(ms) {
1136
- return new Date(ms).toISOString();
1011
+ function idle_expired_reason(answer, idle_budget_ms) {
1012
+ return `${waiting_on(answer)}, and stayed so for the whole ${to_minutes(idle_budget_ms)}-minute idle watch. Report what was done, including how many were picked up during the watch, and finish.`;
1137
1013
  }
1138
- function text_of(window) {
1139
- const { since_ms, until_ms, bound, asked_ms } = window;
1140
- return `idle since ${iso_of(since_ms)} until ${iso_of(until_ms)} (${bound}) asked ${iso_of(asked_ms)}`;
1014
+ function idle_watch_reason(answer, left_ms) {
1015
+ return `${waiting_on(answer)} and the idle watch has about ${to_minutes(left_ms)} minutes left. Release the working tree, ask again in ${String(OFFER_INTERVAL_SECONDS)} seconds, and restart the watch the moment a candidate appears.`;
1141
1016
  }
1142
- function is_bound(value) {
1143
- return value === IDLE_BOUND || value === WHOLE_RUN_BOUND;
1017
+ function running_watch_reason(answer, running, left_ms) {
1018
+ return `${waiting_on(answer)} and the idle watch has about ${to_minutes(left_ms)} minutes left, with ${String(running)} still running. Keep the working tree until they merge and poll at the polling interval \u2014 the watch is running down meanwhile.`;
1144
1019
  }
1145
- function window_from(groups, bound) {
1146
- const since_ms = Date.parse(String(groups["since"]));
1147
- const until_ms = Date.parse(String(groups["until"]));
1148
- const asked_ms = Date.parse(String(groups["asked"]));
1149
- return Number.isNaN(since_ms + until_ms + asked_ms) ? void 0 : { since_ms, until_ms, bound, asked_ms };
1020
+ function watching_reason(input, left_ms) {
1021
+ if (input.running === 0) return idle_watch_reason(input.answer, left_ms);
1022
+ return running_watch_reason(input.answer, input.running, left_ms);
1023
+ }
1024
+ function taken_by(input) {
1025
+ return input.merged + input.running;
1026
+ }
1027
+ function run_reason(input) {
1028
+ const { max_issues } = input;
1029
+ if (max_issues === void 0) return "No maximum was given, so start every issue offered.";
1030
+ return `Start at most ${String(max_issues - taken_by(input))} more: this run's maximum is ${String(max_issues)}, with ${String(input.merged)} merged and ${String(input.running)} running.`;
1031
+ }
1032
+ function stopping(input, reason) {
1033
+ if (input.running === 0) return { verdict: STOP_VERDICT, reason };
1034
+ return { verdict: WATCH_VERDICT, reason: draining_reason(input.running, reason) };
1150
1035
  }
1151
- function parse(text) {
1152
- const groups = TEXT_PATTERN.exec(text)?.groups;
1153
- if (groups === void 0 || !is_bound(groups["bound"])) return void 0;
1154
- return window_from(groups, groups["bound"]);
1036
+ function max_decision(input) {
1037
+ const { max_issues } = input;
1038
+ if (max_issues === void 0 || taken_by(input) < max_issues) return void 0;
1039
+ return stopping(input, max_reached_reason(input.merged, max_issues));
1155
1040
  }
1156
- function next_check_ms(window) {
1157
- const poll_ms = backlog_budget.IDLE_POLL_MINUTES * backlog_budget.MS_PER_MINUTE;
1158
- return Math.min(window.asked_ms + poll_ms, window.until_ms);
1041
+ function idle_decision(input) {
1042
+ const { idle_budget_ms } = input;
1043
+ if (idle_budget_ms === void 0) return stopping(input, no_idle_watch_reason(input.answer));
1044
+ const left_ms = idle_budget_ms - (input.now_ms - input.active_at_ms);
1045
+ if (left_ms > 0) return { verdict: WATCH_VERDICT, reason: watching_reason(input, left_ms) };
1046
+ return stopping(input, idle_expired_reason(input.answer, idle_budget_ms));
1047
+ }
1048
+ function is_finish(input) {
1049
+ return input.answer === "exhausted" && max_decision(input) === void 0;
1159
1050
  }
1160
- var backlog_idle = { next_check_ms, parse, text_of, window_of };
1051
+ var FIXED_DECISIONS = {
1052
+ blocked: { verdict: WATCH_VERDICT, reason: BLOCKED_REASON },
1053
+ untriaged: { verdict: TRIAGE_VERDICT, reason: TRIAGE_REASON }
1054
+ };
1055
+ function answer_decision(input) {
1056
+ if (input.answer === "candidates") return { verdict: RUN_VERDICT, reason: run_reason(input) };
1057
+ return FIXED_DECISIONS[input.answer] ?? idle_decision(input);
1058
+ }
1059
+ function decide(input) {
1060
+ return max_decision(input) ?? answer_decision(input);
1061
+ }
1062
+ var backlog_budget = {
1063
+ ANSWERS,
1064
+ BLOCKED_REASON,
1065
+ DEFAULT_IDLE_MINUTES,
1066
+ DEFAULT_IDLE_MS,
1067
+ IDLE_WAIT_HOURS,
1068
+ MS_PER_MINUTE,
1069
+ OFFER_INTERVAL_MS,
1070
+ RUN_VERDICT,
1071
+ STOP_VERDICT,
1072
+ TRIAGE_REASON,
1073
+ TRIAGE_VERDICT,
1074
+ WAITING_ON,
1075
+ WATCH_VERDICT,
1076
+ decide,
1077
+ draining_reason,
1078
+ idle_expired_reason,
1079
+ idle_watch_reason,
1080
+ is_finish,
1081
+ max_reached_reason,
1082
+ no_idle_watch_reason,
1083
+ run_reason,
1084
+ running_watch_reason,
1085
+ taken_by
1086
+ };
1161
1087
 
1162
1088
  // scripts/backlog/backlog-budget-cli.ts
1163
1089
  var ARGV_OFFSET = 2;
1164
1090
  var SUCCESS_EXIT_CODE2 = 0;
1165
1091
  var FAILURE_EXIT_CODE2 = 1;
1166
1092
  var JSON_KEY = "budget";
1167
- var EXHAUSTED_ANSWER = "exhausted";
1168
1093
  var USAGE = `Usage: josh backlog:budget --answer <${backlog_budget.ANSWERS.join("|")}> --started <ISO-8601> [--active <ISO-8601>, required unless the watch is off] [--merged <count>] [--running <count>] [--max <count>] [--idle <minutes>, 0 to turn the watch off] [--json]`;
1169
1094
  var OPTIONS = {
1170
1095
  active: { type: "string" },
@@ -1243,18 +1168,10 @@ function build_input(values, now_ms) {
1243
1168
  if (!are_values_readable(values)) return void 0;
1244
1169
  return input_of(values, { answer, now_ms, started_at_ms });
1245
1170
  }
1246
- function idle_of(input, verdict) {
1247
- const is_idle_watch = verdict === backlog_budget.WATCH_VERDICT && input.answer === EXHAUSTED_ANSWER && backlog_budget.is_finish(input);
1248
- const window = is_idle_watch ? backlog_idle.window_of(input) : void 0;
1249
- return window === void 0 ? void 0 : backlog_idle.text_of(window);
1250
- }
1251
1171
  function print_json(input, decision) {
1252
1172
  const { verdict, reason } = decision;
1253
- const idle = idle_of(input, verdict);
1254
1173
  const is_finish2 = backlog_budget.is_finish(input);
1255
- console.info(
1256
- JSON.stringify({ [JSON_KEY]: verdict, reason, is_finish: is_finish2, ...idle !== void 0 && { idle } })
1257
- );
1174
+ console.info(JSON.stringify({ [JSON_KEY]: verdict, reason, is_finish: is_finish2 }));
1258
1175
  }
1259
1176
  function refuse() {
1260
1177
  console.error(USAGE);
@@ -1363,7 +1280,7 @@ function argument_tokens(invocation) {
1363
1280
  tokens: true
1364
1281
  })?.tokens;
1365
1282
  }
1366
- function parse2(invocation) {
1283
+ function parse(invocation) {
1367
1284
  const tokens = argument_tokens(invocation);
1368
1285
  if (tokens === void 0) return void 0;
1369
1286
  const stop = tokens.findIndex((token) => token.kind !== "positional");
@@ -1380,7 +1297,7 @@ function joined(command, rendered) {
1380
1297
  return [command, ...rendered].join(TOKEN_SEPARATOR);
1381
1298
  }
1382
1299
  function rebuild(invocation) {
1383
- const parsed = parse2(invocation);
1300
+ const parsed = parse(invocation);
1384
1301
  if (parsed === void 0) return void 0;
1385
1302
  return joined(BACKLOG_COMMAND, [
1386
1303
  ...parsed.issues.map((issue) => issue_token(issue)),
@@ -1391,12 +1308,12 @@ function issue_numbers(invocation) {
1391
1308
  const tokens = tokens_of(invocation);
1392
1309
  const stop = tokens.findIndex((token) => token.startsWith(FLAG_PREFIX));
1393
1310
  const leading = stop === NOT_FOUND ? tokens : tokens.slice(FIRST_TOKEN, stop);
1394
- const issues = parse2(leading.join(TOKEN_SEPARATOR))?.issues;
1311
+ const issues = parse(leading.join(TOKEN_SEPARATOR))?.issues;
1395
1312
  if (issues === void 0 || issues.length === EMPTY_LENGTH) return void 0;
1396
1313
  return issues;
1397
1314
  }
1398
1315
  function has_only(invocation) {
1399
- return parse2(invocation)?.flags.includes(ONLY_FLAG) === true;
1316
+ return parse(invocation)?.flags.includes(ONLY_FLAG) === true;
1400
1317
  }
1401
1318
  var run_invocation = {
1402
1319
  has_only,
@@ -1424,7 +1341,7 @@ function next_added(carry, additions) {
1424
1341
  }
1425
1342
  function add_issues(target, carry, additions) {
1426
1343
  const next = { ...carry, added: next_added(carry, additions) };
1427
- stamp_file.write_stamp(target, next);
1344
+ stamp_file.replace_stamp(target, next);
1428
1345
  return next;
1429
1346
  }
1430
1347
  var run_carry_added = { add_issues, ordered };
@@ -1517,7 +1434,7 @@ function apply_change(target, carry, change, now = /* @__PURE__ */ new Date()) {
1517
1434
  done: next_done(carry, effective.done),
1518
1435
  retrospective: effective.retrospective === true || carry.retrospective
1519
1436
  };
1520
- stamp_file.write_stamp(target, next);
1437
+ stamp_file.replace_stamp(target, next);
1521
1438
  return next;
1522
1439
  }
1523
1440
  var run_carry_change = { apply_change };
@@ -1695,7 +1612,7 @@ var ASSISTANT_TYPE = "assistant";
1695
1612
  function is_priced_line(data) {
1696
1613
  return data.type === ASSISTANT_TYPE && data.message?.model !== SYNTHETIC_MODEL;
1697
1614
  }
1698
- function classify2(data) {
1615
+ function classify(data) {
1699
1616
  if (!is_priced_line(data)) return { kind: "skipped" };
1700
1617
  const usage = data.message?.usage;
1701
1618
  return usage ? { kind: "record", record: to_record(data, usage) } : { kind: "no_usage" };
@@ -1703,7 +1620,7 @@ function classify2(data) {
1703
1620
  function parse_line(line) {
1704
1621
  if (line.trim() === "") return { kind: "skipped" };
1705
1622
  const parsed = LINE_SCHEMA.safeParse(json_value.parse_or_undefined(line));
1706
- return parsed.success ? classify2(parsed.data) : { kind: "malformed" };
1623
+ return parsed.success ? classify(parsed.data) : { kind: "malformed" };
1707
1624
  }
1708
1625
  function later_instant(left, right) {
1709
1626
  if (left === void 0) return right;
@@ -2108,8 +2025,6 @@ var run_carry_conversation = {
2108
2025
  // scripts/run/carry/run-carry.ts
2109
2026
  var CARRY_PREFIX = "josh-run-carry-";
2110
2027
  var REPOSITORY_DIRECTORY_INDEX = 1;
2111
- var CARRY_MAX_AGE_HOURS = backlog_budget.WHOLE_RUN_BUDGET_HOURS;
2112
- var CARRY_MAX_AGE_MS = backlog_budget.WHOLE_RUN_BUDGET_MS;
2113
2028
  var NO_INCREMENT3 = 0;
2114
2029
  var MAX_CUTS = 6;
2115
2030
  var END_COMMAND = "pnpm josh run:carry --end";
@@ -2150,24 +2065,16 @@ async function repository_directory() {
2150
2065
  function parse_carry(raw) {
2151
2066
  return json_value.parse_with(raw, run_carry_schema);
2152
2067
  }
2153
- function started_at_of(carry) {
2154
- return carry.started_at;
2155
- }
2156
- var CARRY_SPEC = {
2157
- parse: parse_carry,
2158
- timestamp_of: started_at_of,
2159
- max_age_ms: CARRY_MAX_AGE_MS
2160
- };
2161
- function is_expired(carry, now) {
2162
- return stamp_record.is_older_than(started_at_of(carry), CARRY_MAX_AGE_MS, now);
2163
- }
2164
- function classify3(raw, now) {
2165
- const read3 = stamp_record.classify(raw, CARRY_SPEC, now);
2166
- if (!("record" in read3)) return read3;
2167
- return { kind: read3.kind, carry: read3.record };
2068
+ function classify2(raw) {
2069
+ if (raw === void 0) return { kind: "none" };
2070
+ const carry = parse_carry(raw);
2071
+ if (carry === void 0 || Number.isNaN(Date.parse(carry.started_at))) {
2072
+ return { kind: "unreadable" };
2073
+ }
2074
+ return { kind: "carried", carry };
2168
2075
  }
2169
- function read_carry(target, now = /* @__PURE__ */ new Date()) {
2170
- return classify3(stamp_file.read_stamp_text(target), now);
2076
+ function read_carry(target) {
2077
+ return classify2(stamp_file.read_stamp_text(target));
2171
2078
  }
2172
2079
  function owner_of(pid, read3 = process_identity.read_start, transcript = run_carry_conversation.own_transcript()) {
2173
2080
  return { pid, start: read3(pid), transcript };
@@ -2190,10 +2097,6 @@ function begin_carry(target, invocation, owner = NO_OWNER, now = /* @__PURE__ */
2190
2097
  const carry = fresh_carry(invocation, owner, now);
2191
2098
  return stamp_file.create_stamp(target, carry) ? carry : void 0;
2192
2099
  }
2193
- function replace_carry(target, invocation, owner = NO_OWNER, now = /* @__PURE__ */ new Date()) {
2194
- stamp_file.remove_stamp(target);
2195
- return begin_carry(target, invocation, owner, now);
2196
- }
2197
2100
  function adopt_carry(target, carry, owner = NO_OWNER) {
2198
2101
  const next = {
2199
2102
  ...carry,
@@ -2216,6 +2119,11 @@ function is_owned_by(carry, owner) {
2216
2119
  function is_foreign_live_owner(carry, owner) {
2217
2120
  return is_owner_live(carry) && !is_owned_by(carry, owner);
2218
2121
  }
2122
+ function is_held_by_another(carry, is_live = is_owner_live) {
2123
+ const caller = owner_of(process.pid);
2124
+ const is_own = carry.owner_pid === caller.pid && carry.owner_start === caller.start;
2125
+ return !is_own && is_live(carry);
2126
+ }
2219
2127
  function is_handed_off_to(carry, invocation) {
2220
2128
  return carry.is_handed_off === true && carry.invocation === invocation;
2221
2129
  }
@@ -2235,8 +2143,7 @@ function end_carry(target) {
2235
2143
  stamp_file.remove_stamp(target);
2236
2144
  }
2237
2145
  function retrospective_done_of(read3) {
2238
- if (read3.kind !== "carried" && read3.kind !== "expired") return false;
2239
- return read3.carry.retrospective === true;
2146
+ return read3.kind === "carried" && read3.carry.retrospective === true;
2240
2147
  }
2241
2148
  function remaining_of(carry) {
2242
2149
  if (carry === void 0) return void 0;
@@ -2252,9 +2159,6 @@ function done_note(carry) {
2252
2159
  function describe_carry(carry) {
2253
2160
  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)}`;
2254
2161
  }
2255
- function expired_message(carry) {
2256
- return `The ${String(CARRY_MAX_AGE_HOURS)}-hour whole-run bound is spent: ${describe_carry(carry)}. Clear it with \`${END_COMMAND}\`.`;
2257
- }
2258
2162
  function mismatch_message(carry, invocation) {
2259
2163
  return `A different invocation is carried here \u2014 ${describe_carry(carry)}. This one is \`${invocation}\`. End the standing record with \`${END_COMMAND}\` once you know its run is over, then begin again.`;
2260
2164
  }
@@ -2286,8 +2190,6 @@ function unknown_message() {
2286
2190
  return "This repository\u2019s git directory could not be read, so no run record was established.";
2287
2191
  }
2288
2192
  var run_carry = {
2289
- CARRY_MAX_AGE_HOURS,
2290
- CARRY_MAX_AGE_MS,
2291
2193
  END_COMMAND,
2292
2194
  MAX_CUTS,
2293
2195
  NO_OWNER,
@@ -2298,18 +2200,17 @@ var run_carry = {
2298
2200
  busy_message,
2299
2201
  carry_path,
2300
2202
  claim_command,
2301
- classify: classify3,
2203
+ classify: classify2,
2302
2204
  classify_claim,
2303
2205
  count_refused_message,
2304
2206
  is_at_cut_cap,
2305
2207
  describe_carry,
2306
2208
  end_carry,
2307
- expired_message,
2308
2209
  mismatch_message,
2309
2210
  fresh_carry,
2310
2211
  is_count_refused,
2311
- is_expired,
2312
2212
  is_foreign_live_owner,
2213
+ is_held_by_another,
2313
2214
  is_owner_live,
2314
2215
  lost_message,
2315
2216
  nothing_to_resume_message,
@@ -2317,7 +2218,6 @@ var run_carry = {
2317
2218
  parse_carry,
2318
2219
  read_carry,
2319
2220
  remaining_of,
2320
- replace_carry,
2321
2221
  repository_directory,
2322
2222
  retrospective_done_of,
2323
2223
  standing_message,
@@ -2421,7 +2321,6 @@ export {
2421
2321
  ports,
2422
2322
  process_identity,
2423
2323
  process_owner_schema,
2424
- backlog_budget,
2425
2324
  PORCELAIN_FLAG,
2426
2325
  git_utilities,
2427
2326
  has_timed_out,
@@ -2430,8 +2329,8 @@ export {
2430
2329
  git_location_environment,
2431
2330
  git_spawn_sync,
2432
2331
  git_command,
2433
- stamp_record,
2434
2332
  cli_flags,
2333
+ backlog_budget,
2435
2334
  issue_number_shape,
2436
2335
  run_invocation,
2437
2336
  lane_paths,
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  investigation_reads
3
- } from "./chunk-QARPP36N.js";
3
+ } from "./chunk-WNFJLBXI.js";
4
4
  import {
5
5
  lane_guard_policy
6
- } from "./chunk-DHSKBAQ2.js";
6
+ } from "./chunk-EAQEGME7.js";
7
7
  import {
8
8
  hook_decision
9
- } from "./chunk-E7LZL5AN.js";
9
+ } from "./chunk-EBSHN25W.js";
10
10
  import {
11
11
  time_spans
12
- } from "./chunk-TORRSNDJ.js";
12
+ } from "./chunk-SL4GQ2XP.js";
13
13
  import {
14
14
  json_value
15
- } from "./chunk-RW4XMF47.js";
15
+ } from "./chunk-2YSW3CXH.js";
16
16
 
17
17
  // scripts/delegation/duplicate-read-guard.ts
18
18
  import { statSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  lane_child_marker
3
- } from "./chunk-NBEAGPS4.js";
3
+ } from "./chunk-J3KSS7II.js";
4
4
 
5
5
  // scripts/lane/lane-guard-policy.ts
6
6
  var DEFAULT_MODE = "refuse";