@joshuafolkken/kit 1.1455.0 → 1.1471.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 (211) hide show
  1. package/.claude/skills/dependency-update/SKILL.md +26 -55
  2. package/.claude/skills/epic-commands/SKILL.md +87 -148
  3. package/.claude/skills/epic-commands/epic-bundle.md +45 -77
  4. package/.claude/skills/epic-commands/execution-waves.md +44 -81
  5. package/.claude/skills/verify-ui/SKILL.md +25 -50
  6. package/.claude/skills/workflow-commands/background-commands.md +36 -47
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +72 -85
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +91 -121
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +73 -117
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -9
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +11 -20
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +43 -52
  13. package/.claude/skills/workflow-commands/chain-rule.md +24 -34
  14. package/.claude/skills/workflow-commands/followup.md +57 -76
  15. package/.claude/skills/workflow-commands/issue-comments.md +43 -60
  16. package/.claude/skills/workflow-commands/kickoff.md +65 -74
  17. package/.claude/skills/workflow-commands/observation-filing.md +62 -107
  18. package/.claude/skills/workflow-commands/observation-ledger.md +56 -111
  19. package/.claude/skills/workflow-commands/pre-gate-cut.md +89 -138
  20. package/.claude/skills/workflow-commands/prerequisite.md +10 -15
  21. package/.claude/skills/workflow-commands/progress-watcher.md +21 -26
  22. package/.claude/skills/workflow-commands/retrospective.md +31 -50
  23. package/.claude/skills/workflow-commands/split-assessment.md +34 -58
  24. package/.claude/skills/workflow-commands/working-tree-hold.md +54 -71
  25. package/CLAUDE.md +6 -5
  26. package/dist/CLAUDE.md +6 -5
  27. package/dist/hooks/batch-guard.js +6 -6
  28. package/dist/hooks/{chunk-7WQBPLUN.js → chunk-43ZHT22P.js} +31 -15
  29. package/dist/hooks/{chunk-LIAFCXL7.js → chunk-AS74OU3D.js} +23 -7
  30. package/dist/hooks/{chunk-3C4XRYG6.js → chunk-E7OIKX5Z.js} +17 -12
  31. package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-EUMACUK2.js} +18 -12
  32. package/dist/hooks/{chunk-WNLVGT7E.js → chunk-H5HVWYXK.js} +4 -4
  33. package/dist/hooks/{chunk-HHI6FO3R.js → chunk-IIGFYXKC.js} +345 -240
  34. package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-J47HU5H5.js} +14 -3
  35. package/dist/hooks/{chunk-OLMO72ON.js → chunk-K4EOONYM.js} +1 -1
  36. package/dist/hooks/{chunk-D5QBTOCZ.js → chunk-K4LMBQC2.js} +4 -4
  37. package/dist/hooks/{chunk-TBJFTSUY.js → chunk-KC5AK2GF.js} +8 -6
  38. package/dist/hooks/chunk-SHGPANFB.js +56 -0
  39. package/dist/hooks/{chunk-5H4SSDSD.js → chunk-YHBZAJSV.js} +3 -3
  40. package/dist/hooks/{chunk-HFGWYGUV.js → chunk-YYPI7SJZ.js} +5 -5
  41. package/dist/hooks/codex-hook-adapter.js +13 -13
  42. package/dist/hooks/duplicate-read-guard.js +7 -7
  43. package/dist/hooks/format-edited.js +3 -3
  44. package/dist/hooks/inputs.json +216 -214
  45. package/dist/hooks/investigation-guard.js +7 -7
  46. package/dist/hooks/pretool-guard.js +12 -12
  47. package/dist/hooks/session-lang.js +6 -5
  48. package/dist/hooks/stop-guard.js +18 -14
  49. package/dist/josh.js +5 -6
  50. package/dist/managed-marker/index.js +1 -2
  51. package/dist/version/index.js +15 -12
  52. package/eslint/base.js +2 -1
  53. package/eslint/rules/unicorn.js +16 -0
  54. package/package.json +3 -3
  55. package/prompts/coding-standards.md +12 -22
  56. package/prompts/collaboration-workflow/file-edits.md +26 -44
  57. package/prompts/collaboration-workflow/gh-rest.md +20 -36
  58. package/prompts/collaboration-workflow/glossary.md +5 -7
  59. package/prompts/collaboration-workflow/issue-citation.md +7 -13
  60. package/prompts/collaboration-workflow/issue-template.md +29 -29
  61. package/prompts/collaboration-workflow/operating-rules.md +35 -44
  62. package/prompts/collaboration-workflow/output-bounds.md +15 -15
  63. package/prompts/collaboration-workflow/overview.md +14 -19
  64. package/prompts/collaboration-workflow/plan-comment.md +13 -13
  65. package/prompts/collaboration-workflow/principles.md +50 -53
  66. package/prompts/collaboration-workflow/proposal-request.md +1 -9
  67. package/prompts/collaboration-workflow/report-format.md +32 -49
  68. package/prompts/collaboration-workflow/residency.md +10 -10
  69. package/prompts/collaboration-workflow/rule-delivery.md +9 -9
  70. package/prompts/collaboration-workflow/shell-body.md +10 -20
  71. package/prompts/collaboration-workflow/turn-batching.md +8 -14
  72. package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
  73. package/prompts/collaboration-workflow/wip-cap.md +24 -30
  74. package/prompts/collaboration-workflow.md +24 -25
  75. package/prompts/refactoring.md +33 -43
  76. package/prompts/review-rubric.md +84 -129
  77. package/prompts/review.md +111 -141
  78. package/prompts/sonar-hotspot-handling.md +22 -50
  79. package/prompts/testing-guide.md +48 -99
  80. package/scripts/adopt/adopt-toolkits.ts +2 -4
  81. package/scripts/agent/agent-exit-record.ts +23 -1
  82. package/scripts/agent/codex-agent-argv.ts +7 -2
  83. package/scripts/backlog/backlog-budget-cli.ts +1 -2
  84. package/scripts/backlog/backlog-drive-carried.ts +26 -0
  85. package/scripts/backlog/backlog-drive-cli.ts +10 -5
  86. package/scripts/backlog/backlog-drive-named.ts +2 -4
  87. package/scripts/backlog/backlog-drive-restore.ts +19 -1
  88. package/scripts/backlog/backlog-drive-seats.ts +27 -0
  89. package/scripts/backlog/backlog-drive.ts +11 -6
  90. package/scripts/backlog/backlog-ready.ts +13 -6
  91. package/scripts/behavior/behavior-rules.ts +3 -2
  92. package/scripts/cost-runtime/codex-usage.ts +4 -2
  93. package/scripts/cost-runtime/cost-cli.ts +1 -2
  94. package/scripts/delegation/investigation-guard.ts +1 -1
  95. package/scripts/delegation/investigation-reads.ts +1 -1
  96. package/scripts/discovery/repo-discovery.ts +4 -2
  97. package/scripts/discovery/repo-origin.ts +2 -4
  98. package/scripts/doctor/doctor-logic.ts +6 -3
  99. package/scripts/document/document-byte-budget.ts +27 -27
  100. package/scripts/document/document-byte-check.ts +1 -2
  101. package/scripts/document/entry-read-budget.ts +2 -2
  102. package/scripts/epic/epic-audit-rationale.ts +8 -2
  103. package/scripts/epic/epic-bundle-cli.ts +1 -2
  104. package/scripts/epic/epic-chains.ts +6 -2
  105. package/scripts/epic/epic-close-comment.ts +1 -2
  106. package/scripts/gate/verification-gate.ts +7 -2
  107. package/scripts/gh/git-pr.ts +6 -2
  108. package/scripts/git/git-common-directory.ts +3 -3
  109. package/scripts/git/git-diff-reads.ts +16 -9
  110. package/scripts/git/git-execa-error.ts +1 -2
  111. package/scripts/hooks/format-edited-file.ts +10 -4
  112. package/scripts/hooks/pre-push-unit.ts +15 -4
  113. package/scripts/hooks/pretool-guard.ts +23 -29
  114. package/scripts/hooks/step-zero-notice.ts +3 -2
  115. package/scripts/init/hook-command-rewrite.ts +6 -2
  116. package/scripts/init/init-logic-deploy-vps.ts +4 -2
  117. package/scripts/issue/behavior-change-lint.ts +12 -6
  118. package/scripts/issue/issue-scout-cli.ts +3 -2
  119. package/scripts/josh/file-map-stamp.ts +1 -3
  120. package/scripts/josh/hook-decision.ts +17 -2
  121. package/scripts/lane/lane-await.ts +3 -2
  122. package/scripts/lane/lane-child-invocation.ts +14 -1
  123. package/scripts/lane/lane-guard-policy.ts +4 -4
  124. package/scripts/lane/lane-ledger.ts +9 -0
  125. package/scripts/lane/lane-load.ts +9 -2
  126. package/scripts/lane/lane-measure-cli.ts +2 -0
  127. package/scripts/lane/lane-parked.ts +18 -0
  128. package/scripts/lane/lane-process-load.ts +125 -0
  129. package/scripts/lane/lane-process-stats.ts +93 -0
  130. package/scripts/lane/lane-registry.ts +8 -3
  131. package/scripts/lane/lane-session-broker.ts +29 -5
  132. package/scripts/lane/lane-spent.ts +14 -3
  133. package/scripts/lane/lane-stats.ts +1 -0
  134. package/scripts/managed-marker/managed-marker-logic.ts +1 -2
  135. package/scripts/overrides/lockfile-overrides.ts +1 -3
  136. package/scripts/propagate/propagate.ts +1 -2
  137. package/scripts/release/release-progress.ts +3 -2
  138. package/scripts/report/report-lint.ts +7 -4
  139. package/scripts/review/review-record-cli.ts +6 -2
  140. package/scripts/rules/destructive-command.ts +6 -9
  141. package/scripts/rules/direct-filing.ts +3 -7
  142. package/scripts/rules/early-heartbeat.ts +7 -18
  143. package/scripts/rules/file-body.ts +6 -13
  144. package/scripts/rules/filing-cap.ts +3 -6
  145. package/scripts/rules/git-force.ts +5 -9
  146. package/scripts/rules/implementation-cut.ts +37 -35
  147. package/scripts/rules/index-guard.ts +4 -7
  148. package/scripts/rules/issue-comments.ts +5 -15
  149. package/scripts/rules/josh-git-bare.ts +3 -6
  150. package/scripts/rules/lane-background.ts +10 -21
  151. package/scripts/rules/lane-carry-conflict.ts +7 -17
  152. package/scripts/rules/lane-interactive-ask.ts +6 -11
  153. package/scripts/rules/lane-park.ts +5 -13
  154. package/scripts/rules/lane-split-park.ts +2 -7
  155. package/scripts/rules/lane-switch-main.ts +9 -12
  156. package/scripts/rules/oracle-consulted.ts +4 -5
  157. package/scripts/rules/piped-verification.ts +81 -31
  158. package/scripts/rules/poll-loop.ts +7 -17
  159. package/scripts/rules/pre-gate-cut.ts +7 -21
  160. package/scripts/rules/prior-comment-read.ts +7 -5
  161. package/scripts/rules/protected-files.ts +5 -9
  162. package/scripts/rules/raw-field-body.ts +3 -7
  163. package/scripts/rules/rule-body-guard.ts +4 -11
  164. package/scripts/rules/run-tail-rule.ts +6 -22
  165. package/scripts/rules/shell-body-trigger.ts +4 -6
  166. package/scripts/rules/shell-segments.ts +7 -0
  167. package/scripts/rules/stop-rules.ts +16 -29
  168. package/scripts/rules/test-declared-commit.ts +4 -10
  169. package/scripts/rules/third-party-write.ts +5 -10
  170. package/scripts/rules/worktree-guard.ts +5 -12
  171. package/scripts/run/board/run-board-github.ts +11 -3
  172. package/scripts/run/board/run-board-idle.ts +31 -0
  173. package/scripts/run/board/run-board-read.ts +9 -12
  174. package/scripts/run/board/run-board-reason.ts +7 -3
  175. package/scripts/run/board/run-board-state.ts +4 -0
  176. package/scripts/run/board/run-board-status.ts +18 -3
  177. package/scripts/run/board/run-board-thanks.ts +7 -2
  178. package/scripts/run/board/run-board-tick.ts +32 -7
  179. package/scripts/run/carry/run-carry-args.ts +1 -3
  180. package/scripts/run/carry/run-carry-conversation.ts +7 -2
  181. package/scripts/run/cut/run-cut-cli.ts +15 -20
  182. package/scripts/run/cut/run-cut-decline.ts +72 -0
  183. package/scripts/run/cut/run-cut-ready.ts +74 -0
  184. package/scripts/run/cut/run-cut-report.ts +13 -0
  185. package/scripts/run/cut/run-cut.ts +10 -1
  186. package/scripts/run/hold/run-hold-cli.ts +14 -3
  187. package/scripts/run/hold/run-hold.ts +6 -3
  188. package/scripts/run/merge/run-merge-cli.ts +30 -7
  189. package/scripts/run/merge/run-merge-ship.ts +59 -1
  190. package/scripts/run/merge/run-merge.ts +3 -2
  191. package/scripts/run/run-ending.ts +31 -3
  192. package/scripts/run/run-parent-cut-hook.ts +5 -10
  193. package/scripts/run/run-preflight.ts +1 -2
  194. package/scripts/run/run-stop-notify.ts +1 -2
  195. package/scripts/run/run-watcher-guard.ts +6 -3
  196. package/scripts/run/ship/run-ship-review-steps.ts +13 -4
  197. package/scripts/run/ship/run-ship-review.ts +15 -2
  198. package/scripts/run/tidy/run-tidy-cli.ts +3 -3
  199. package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
  200. package/scripts/safe-chain/project-config.ts +7 -2
  201. package/scripts/sync/sync-hook-safety.ts +8 -2
  202. package/scripts/test/unit-worker-share.ts +3 -2
  203. package/scripts/time-runtime/time-batch-guard.ts +3 -3
  204. package/scripts/time-runtime/time-hook-transcript.ts +3 -2
  205. package/scripts/time-runtime/time-shell.ts +10 -0
  206. package/scripts/version/effective-upstream.ts +3 -4
  207. package/scripts/version/kit-version-config.ts +8 -2
  208. package/scripts/version/latest-corepack.ts +1 -2
  209. package/scripts/version/release-hold.ts +11 -4
  210. package/scripts/version/version-commands.ts +12 -4
  211. package/dist/hooks/chunk-UT7YC3N2.js +0 -56
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  duplicate_read_outcome
3
- } from "./chunk-HFGWYGUV.js";
3
+ } from "./chunk-YYPI7SJZ.js";
4
4
  import {
5
5
  ALREADY_DONE_LABEL,
6
6
  CONTEXT_CUT_THRESHOLD,
@@ -29,7 +29,7 @@ import {
29
29
  session_cite,
30
30
  shell_segments,
31
31
  tail_commands
32
- } from "./chunk-7WQBPLUN.js";
32
+ } from "./chunk-43ZHT22P.js";
33
33
  import {
34
34
  ancestor_directories,
35
35
  find_local_bin_upwards,
@@ -50,25 +50,25 @@ import {
50
50
  run_event_stream_emit,
51
51
  run_headless,
52
52
  session_language
53
- } from "./chunk-Z4XI7CZS.js";
53
+ } from "./chunk-EUMACUK2.js";
54
54
  import {
55
55
  error_text
56
56
  } from "./chunk-CDY4NFQG.js";
57
57
  import {
58
58
  batch_outcome
59
- } from "./chunk-5H4SSDSD.js";
59
+ } from "./chunk-YHBZAJSV.js";
60
60
  import {
61
- investigation_refusal
62
- } from "./chunk-D5QBTOCZ.js";
61
+ investigation_outcome
62
+ } from "./chunk-K4LMBQC2.js";
63
63
  import {
64
64
  investigation_reads
65
- } from "./chunk-WNLVGT7E.js";
65
+ } from "./chunk-H5HVWYXK.js";
66
66
  import {
67
67
  lane_guard_policy
68
- } from "./chunk-UT7YC3N2.js";
68
+ } from "./chunk-SHGPANFB.js";
69
69
  import {
70
70
  hook_decision
71
- } from "./chunk-GPLGZ6IC.js";
71
+ } from "./chunk-J47HU5H5.js";
72
72
  import {
73
73
  cost_attribute,
74
74
  cost_blocks,
@@ -76,25 +76,27 @@ import {
76
76
  time_batch_guard,
77
77
  time_density_hook,
78
78
  time_shell
79
- } from "./chunk-LIAFCXL7.js";
79
+ } from "./chunk-AS74OU3D.js";
80
80
  import {
81
81
  lane_child_marker
82
- } from "./chunk-OLMO72ON.js";
82
+ } from "./chunk-K4EOONYM.js";
83
83
  import {
84
84
  GIT_LOCATION_VARIABLES,
85
85
  PORCELAIN_FLAG,
86
+ UNTRACKED_FILES_FLAG,
86
87
  cli_flags,
87
88
  cost_transcript,
88
89
  cost_usage,
89
90
  file_reader,
90
91
  git_command,
92
+ git_diff_reads,
91
93
  git_spawn_sync,
92
94
  issue_number_shape,
93
95
  json_value,
94
96
  lane_paths,
95
97
  own_session,
96
98
  run_carry
97
- } from "./chunk-3C4XRYG6.js";
99
+ } from "./chunk-E7OIKX5Z.js";
98
100
  import {
99
101
  PLATFORM_TEMP_ROOT,
100
102
  PROBE_TIMEOUT_MS,
@@ -162,9 +164,9 @@ function read_record(root) {
162
164
  const raw = stamp_file.read_stamp_text(record_path(root));
163
165
  return raw === void 0 ? void 0 : json_value.parse_with(raw, record_schema);
164
166
  }
165
- function write_record(record, root) {
166
- stamp_file.replace_stamp(record_path(root), record);
167
- return record;
167
+ function write_record(record2, root) {
168
+ stamp_file.replace_stamp(record_path(root), record2);
169
+ return record2;
168
170
  }
169
171
  function clear(root) {
170
172
  stamp_file.remove_stamp(record_path(root));
@@ -176,16 +178,16 @@ async function read_snapshot(request2, body, etag) {
176
178
  return comments === void 0 ? UNREADABLE2 : { kind: "read", snapshot: { body, comments }, etag };
177
179
  }
178
180
  async function fetch_issue(request2) {
179
- const read2 = await git_gh_conditional.read_conditional(
181
+ const read3 = await git_gh_conditional.read_conditional(
180
182
  git_gh_api_path.issue_api_path(request2.issue),
181
183
  request2.etag,
182
184
  request2.timeout_ms
183
185
  );
184
- if (read2.kind !== "changed") return read2;
185
- const fields = json_value.parse_with(read2.body, issue_schema);
186
+ if (read3.kind !== "changed") return read3;
187
+ const fields = json_value.parse_with(read3.body, issue_schema);
186
188
  if (fields === void 0) return UNREADABLE2;
187
189
  if (fields.state === CLOSED_STATE) return { kind: "closed" };
188
- return await read_snapshot(request2, fields.body ?? "", read2.etag);
190
+ return await read_snapshot(request2, fields.body ?? "", read3.etag);
189
191
  }
190
192
  function fresh_record(issue, fetched, now, acknowledged_at = now.toISOString()) {
191
193
  const baseline = issue_watch_diff.baseline_of(fetched.snapshot);
@@ -204,44 +206,44 @@ async function record_baseline_here(issue) {
204
206
  async function clear_here() {
205
207
  clear(await git_command.repository_root());
206
208
  }
207
- function with_fetched(record, fetched, checked_at) {
208
- if (fetched.kind !== "read") return { ...record, checked_at };
209
- const pending = issue_watch_diff.unread_diff(record, fetched.snapshot);
210
- return { ...record, checked_at, etag: fetched.etag, pending };
209
+ function with_fetched(record2, fetched, checked_at) {
210
+ if (fetched.kind !== "read") return { ...record2, checked_at };
211
+ const pending = issue_watch_diff.unread_diff(record2, fetched.snapshot);
212
+ return { ...record2, checked_at, etag: fetched.etag, pending };
211
213
  }
212
- function is_due(record, options, now) {
213
- return now.getTime() - Date.parse(record.checked_at) >= options.interval_ms;
214
+ function is_due(record2, options, now) {
215
+ return now.getTime() - Date.parse(record2.checked_at) >= options.interval_ms;
214
216
  }
215
- async function refresh(record, options, now, root) {
217
+ async function refresh(record2, options, now, root) {
216
218
  const fetched = await fetch_issue({
217
- issue: record.issue,
218
- etag: record.etag,
219
+ issue: record2.issue,
220
+ etag: record2.etag,
219
221
  timeout_ms: options.timeout_ms
220
222
  });
221
223
  if (fetched.kind === "closed") {
222
224
  clear(root);
223
225
  return void 0;
224
226
  }
225
- return write_record(with_fetched(record, fetched, now.toISOString()), root);
227
+ return write_record(with_fetched(record2, fetched, now.toISOString()), root);
226
228
  }
227
229
  async function poll(options, root) {
228
- const record = read_record(root);
230
+ const record2 = read_record(root);
229
231
  const now = options.now ?? /* @__PURE__ */ new Date();
230
- if (record === void 0 || !is_due(record, options, now)) return record;
231
- return await refresh(record, options, now, root);
232
+ if (record2 === void 0 || !is_due(record2, options, now)) return record2;
233
+ return await refresh(record2, options, now, root);
232
234
  }
233
- function mark_shown(record, root) {
234
- return write_record({ ...record, shown: record.pending }, root);
235
+ function mark_shown(record2, root) {
236
+ return write_record({ ...record2, shown: record2.pending }, root);
235
237
  }
236
238
  async function acknowledge(root, now = /* @__PURE__ */ new Date()) {
237
- const record = read_record(root);
238
- if (record === void 0) return void 0;
239
- const fetched = await fetch_issue({ issue: record.issue });
239
+ const record2 = read_record(root);
240
+ if (record2 === void 0) return void 0;
241
+ const fetched = await fetch_issue({ issue: record2.issue });
240
242
  if (fetched.kind !== "read") return void 0;
241
- const diff = issue_watch_diff.unread_diff(record, fetched.snapshot);
242
- const acknowledged_at = diff === void 0 ? record.acknowledged_at : now.toISOString();
243
- write_record(fresh_record(record.issue, fetched, now, acknowledged_at), root);
244
- return { issue: record.issue, diff };
243
+ const diff = issue_watch_diff.unread_diff(record2, fetched.snapshot);
244
+ const acknowledged_at = diff === void 0 ? record2.acknowledged_at : now.toISOString();
245
+ write_record(fresh_record(record2.issue, fetched, now, acknowledged_at), root);
246
+ return { issue: record2.issue, diff };
245
247
  }
246
248
  var issue_watch = {
247
249
  acknowledge,
@@ -444,16 +446,16 @@ var EVERY_CALL_MS = 0;
444
446
  function ack_command(issue) {
445
447
  return `pnpm josh issue:ack ${issue}`;
446
448
  }
447
- function unread_message(record) {
449
+ function unread_message(record2) {
448
450
  return [
449
- `A person updated issue ${session_cite.issue(record.issue)} after this run read it, and the run has not taken the update in:`,
450
- record.pending ?? "",
451
- `Read it, compare it with the acceptance criteria, fix what it changes, then run \`${ack_command(record.issue)}\` to record that it was read.`
451
+ `A person updated issue ${session_cite.issue(record2.issue)} after this run read it, and the run has not taken the update in:`,
452
+ record2.pending ?? "",
453
+ `Read it, compare it with the acceptance criteria, fix what it changes, then run \`${ack_command(record2.issue)}\` to record that it was read.`
452
454
  ].join("\n\n");
453
455
  }
454
- function review_message(record, briefed_at) {
456
+ function review_message(record2, briefed_at) {
455
457
  return [
456
- `Issue ${session_cite.issue(record.issue)} was updated after the review was briefed (taken in at ${record.acknowledged_at}, briefed at ${briefed_at}), so the review read the earlier specification.`,
458
+ `Issue ${session_cite.issue(record2.issue)} was updated after the review was briefed (taken in at ${record2.acknowledged_at}, briefed at ${briefed_at}), so the review read the earlier specification.`,
457
459
  "Re-run `/code-review` with a fresh `pnpm josh review:brief`, so the review reads the change against the updated issue."
458
460
  ].join("\n");
459
461
  }
@@ -461,16 +463,16 @@ async function polled(root) {
461
463
  return await issue_watch.poll({ interval_ms: EVERY_CALL_MS }, root);
462
464
  }
463
465
  async function unread_refusal(root) {
464
- const record = await polled(root);
465
- return record?.pending === void 0 ? void 0 : unread_message(record);
466
+ const record2 = await polled(root);
467
+ return record2?.pending === void 0 ? void 0 : unread_message(record2);
466
468
  }
467
469
  async function review_refusal(root) {
468
- const record = await polled(root);
469
- if (record === void 0) return void 0;
470
- if (record.pending !== void 0) return unread_message(record);
470
+ const record2 = await polled(root);
471
+ if (record2 === void 0) return void 0;
472
+ if (record2.pending !== void 0) return unread_message(record2);
471
473
  const pointer = review_attest.read_pointer(root);
472
- if (pointer === void 0 || record.acknowledged_at <= pointer.taken_at) return void 0;
473
- return review_message(record, pointer.taken_at);
474
+ if (pointer === void 0 || record2.acknowledged_at <= pointer.taken_at) return void 0;
475
+ return review_message(record2, pointer.taken_at);
474
476
  }
475
477
  async function review_refusal_here() {
476
478
  return await review_refusal(await git_command.repository_root());
@@ -490,18 +492,18 @@ var issue_watch_gate = {
490
492
  // scripts/issue/issue-watch-hook.ts
491
493
  var POLL_INTERVAL_MS = 18e4;
492
494
  var POLL_TIMEOUT_MS = 5e3;
493
- function is_new_to_run(record) {
494
- return record?.pending !== void 0 && record.pending !== record.shown;
495
+ function is_new_to_run(record2) {
496
+ return record2?.pending !== void 0 && record2.pending !== record2.shown;
495
497
  }
496
498
  async function new_update_reason(now) {
497
- const record = await issue_watch.poll({
499
+ const record2 = await issue_watch.poll({
498
500
  interval_ms: POLL_INTERVAL_MS,
499
501
  timeout_ms: POLL_TIMEOUT_MS,
500
502
  now
501
503
  });
502
- if (!is_new_to_run(record)) return void 0;
503
- issue_watch.mark_shown(record);
504
- return issue_watch_gate.unread_message(record);
504
+ if (!is_new_to_run(record2)) return void 0;
505
+ issue_watch.mark_shown(record2);
506
+ return issue_watch_gate.unread_message(record2);
505
507
  }
506
508
  async function watch_reason(now = /* @__PURE__ */ new Date()) {
507
509
  try {
@@ -541,7 +543,7 @@ async function mark_implement(directory = process.cwd(), source = process.env) {
541
543
  var lane_phase = { mark_implement };
542
544
 
543
545
  // scripts/rules/direct-filing.ts
544
- var DIRECT_FILING_REASON = '\u26D4 direct filing: an Issue is filed with `pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>]`, never with `gh api \u2026/issues` or `gh issue create`. The command runs every filing step in order \u2014 the duplicate scout, the body lint and its classification labels, the depth and `route:` labels, the `## Origin` check when filing into another repository, and `epic:bundle` after the create \u2014 so none of them is a step to remember. When the scout prints candidates it holds the filing: read each, then reissue with `--distinct <N,\u2026>` for the ones that are separate deliverables. Reference: `docs/josh-commands-backlog.md` \u2192 "`josh issue:file`". **This rule fires on every occurrence, not once per run.**';
546
+ var DIRECT_FILING_REASON = '\u26D4 direct filing: an Issue is filed with `pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2>`, never with `gh api \u2026/issues` or `gh issue create` \u2014 it runs the scout, the lint, the labels and `epic:bundle` itself. It fires on every occurrence (`docs/josh-commands-backlog.md` \u2192 "`josh issue:file`").';
545
547
  function is_any_filing(command) {
546
548
  return bash_triggers.is_direct_filing(command) || bash_triggers.is_issue_filing(command);
547
549
  }
@@ -900,7 +902,7 @@ function decide(call, run2, can_record) {
900
902
  if (can_record) ARM_RECORD.record(target, lands_ms);
901
903
  return false;
902
904
  }
903
- var EARLY_HEARTBEAT_REASON = "\u26D4 early heartbeat: this call arms a wait timer of its own, and the progress clock is not yours to keep. `pnpm josh run:progress --wait` keeps it for you (`JOSH_PROGRESS_INTERVAL_MINUTES`, or `josh.progress_interval_minutes` in `package.json`, default 20 minutes): start it once in the background and it reports the five labelled lines to the run's event stream on its own, without waking you \u2014 relay nothing, and start it again only after it exits, which it does when newly runnable work arrives. Two timers armed at once is what produced reports 3\u20135 minutes apart on a 15-minute setting (joshuafolkken/kit#1570): a new one was armed on the turn a timer fired and again on the turn a child's completion woke the run, and `--mark` recorded each report without anything refusing the early ones. **An explicit ask is not a heartbeat**: if the person asked for progress now, run `pnpm josh run:progress --once`, which prints the five labelled lines whatever the clock says \u2014 under `backlogrun`, `pnpm josh run:board --chat` instead. If you are waiting on something rather than on a clock, wait on the thing itself \u2014 `pnpm josh followup` waits for CI. The procedure is `.claude/skills/workflow-commands/backlogrun.md` \u2192 \"Progress while the run is quiet\". **This rule fires on every early arm, not once per run**, so reissuing the same sleep will be refused again.";
905
+ var EARLY_HEARTBEAT_REASON = "\u26D4 early heartbeat: this call arms a wait timer of its own. Start `pnpm josh run:progress --wait` once in the background: it reports to the run's event stream on its own \u2014 relay nothing. When asked, `pnpm josh run:progress --once` \u2014 under `backlogrun`, `pnpm josh run:board --chat`; for CI, `pnpm josh followup`. It fires on every occurrence (`.claude/skills/workflow-commands/progress-watcher.md`).";
904
906
  var PROGRESS_NAMES = /* @__PURE__ */ new Set(["run:progress"]);
905
907
  var WATCH_FLAG = /(?:^|\s)--(?:wait|once)(?=\s|$)/u;
906
908
  function is_progress_watch_segment(segment) {
@@ -998,7 +1000,7 @@ function carries_a_file_body(command, has_file = existsSync) {
998
1000
  (line) => is_heredoc_write(line, has_file) || has_body_interpreter(line)
999
1001
  );
1000
1002
  }
1001
- var FILE_BODY_REASON = "\u26D4 file body in a shell command: this carries a file's new text inline \u2014 a heredoc rewriting an existing file, an interpreter (`python3 - <<`, `node -e`) whose body writes, a script file (`cat > edit.py <<EOF`) whose body writes, or an in-place `perl -0pi -e`. `CLAUDE.md` forbids it because the command body is re-read on every later request of the run, so an editing script written early is billed for the rest of it (`file-edits.md` measured one run at ~2m45s spent retyping existing lines). Edit a region with the Edit tool (old / new); a change scattered so widely that the partial edits would carry more than the file is rewritten whole with the Write tool, never a heredoc. Pass a body by path (`--field body=@<path>`, `--body-file <path>`). A new-file creation (`cat > <new-file> <<EOF`), a read-only heredoc (`cat <<EOF`), and a short `sed -i` stay allowed. **This rule fires on every occurrence, not once per run.**";
1003
+ var FILE_BODY_REASON = "\u26D4 file body in a shell command: this carries a file's new text inline, which is re-read on every later request. Edit a region with the Edit tool, rewrite a whole file with the Write tool, and pass a body by path (`--body-file <path>`); a new-file `cat > <new-file> <<EOF` stays allowed. This rule fires on every occurrence (`prompts/collaboration-workflow/file-edits.md`).";
1002
1004
  var ROW2 = {
1003
1005
  id: "file-body",
1004
1006
  is_trigger: bash_triggers.on_bash_command((command) => carries_a_file_body(command)),
@@ -1109,7 +1111,7 @@ function is_destructive_segment(segment) {
1109
1111
  function is_force_or_delete(command) {
1110
1112
  return shell_segments.segments_of(command).some((segment) => is_destructive_segment(segment));
1111
1113
  }
1112
- var GIT_FORCE_REASON = "\u26D4 force push or branch delete: this rewrites or removes shared history, which is Tier C and needs explicit current-turn user instruction \u2014 never an AI decision, even to fix a problem you caused (`prompts/collaboration-workflow/operating-rules.md`). The `.claude/settings.json` deny list matches globs, so it misses this spelling: a combined short cluster (`git push -uf`), a `git -C <path>` prefix, and the colon-form delete (`git push origin :branch`) all pass it. This row reads the argv instead, so the spelling does not matter. For an ordinary push use `pnpm josh git`; a force push, a `git push --delete` / `:branch`, or a `git branch -d` / `-D` is the user's to run in their own terminal. **This rule fires on every occurrence, not once per run.**";
1114
+ var GIT_FORCE_REASON = "\u26D4 force push or branch delete: this rewrites or removes shared history, which is Tier C and needs explicit current-turn user instruction. Push with `pnpm josh git`; leave a force push or branch delete to the user's own terminal. This rule fires on every occurrence (`prompts/collaboration-workflow/operating-rules.md`).";
1113
1115
  var ROW3 = {
1114
1116
  id: "git-force",
1115
1117
  is_trigger: bash_triggers.on_bash_command(is_force_or_delete),
@@ -1162,8 +1164,9 @@ function measurement_from(content, target, thread_id) {
1162
1164
  return parsed.success ? [parsed.data.payload.info] : [];
1163
1165
  });
1164
1166
  const session = metadata.find((entry) => entry.payload.id === thread_id);
1165
- if (session === void 0 || usages.length === 0) return void 0;
1166
- if (!same_project(session.payload.cwd, target)) return void 0;
1167
+ if (session === void 0 || usages.length === 0 || !same_project(session.payload.cwd, target)) {
1168
+ return void 0;
1169
+ }
1167
1170
  return { billed_input_per_request: billed_per_request(usages) };
1168
1171
  }
1169
1172
  function rollout_files(home, thread_id) {
@@ -1234,8 +1237,8 @@ function owner_sets(corpus, meta) {
1234
1237
  }
1235
1238
  function pairs_with_inherited(session, inherited, is_delegated) {
1236
1239
  return cost_attribute.group_by_issue(session.records).flatMap(
1237
- (group) => group.records.map((record) => ({
1238
- record,
1240
+ (group) => group.records.map((record2) => ({
1241
+ record: record2,
1239
1242
  issue: group.issue === cost_attribute.UNATTRIBUTED_KEY ? inherited : group.issue,
1240
1243
  baseline_tokens: session.baseline_tokens,
1241
1244
  session_id: session.session_id,
@@ -1354,8 +1357,7 @@ var FAILURE_EXIT_CODE2 = 1;
1354
1357
  var USAGE = "Usage: josh cost (--cut | --over <tokens-per-request>) [--path <dir>]";
1355
1358
  var UNMEASURABLE_VERDICT = "unmeasurable";
1356
1359
  function to_threshold(raw) {
1357
- if (raw === void 0) return void 0;
1358
- if (raw.trim() === "") return void 0;
1360
+ if (raw === void 0 || raw.trim() === "") return void 0;
1359
1361
  const parsed = Number(raw);
1360
1362
  return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
1361
1363
  }
@@ -1400,7 +1402,7 @@ function provider_of(environment) {
1400
1402
  function measurement_of(session) {
1401
1403
  return {
1402
1404
  billed_input_per_request: session.records.map(
1403
- (record) => cost_usage.billed_input(record.totals)
1405
+ (record2) => cost_usage.billed_input(record2.totals)
1404
1406
  )
1405
1407
  };
1406
1408
  }
@@ -1508,8 +1510,71 @@ var cost_format = {
1508
1510
  format_share
1509
1511
  };
1510
1512
 
1511
- // scripts/rules/implementation-cut-verdict.ts
1513
+ // scripts/run/cut/run-cut-decline.ts
1512
1514
  import { z as z5 } from "zod";
1515
+ var DECLINE_PREFIX = "josh-run-cut-decline-";
1516
+ var decline_schema = z5.object({ issue: z5.string(), hold_taken_at: z5.string().optional() });
1517
+ function decline_path(git_directory) {
1518
+ return stamp_file.stamp_path(DECLINE_PREFIX, git_directory);
1519
+ }
1520
+ function record(git_directory, decline) {
1521
+ stamp_file.replace_stamp(decline_path(git_directory), decline);
1522
+ }
1523
+ function read(git_directory) {
1524
+ const raw = stamp_file.read_stamp_text(decline_path(git_directory));
1525
+ return raw === void 0 ? void 0 : json_value.parse_with(raw, decline_schema);
1526
+ }
1527
+ function matches(recorded, current) {
1528
+ return recorded?.issue === current.issue && recorded.hold_taken_at === current.hold_taken_at;
1529
+ }
1530
+ function is_declined_sync(issue, directory = run_cut.worktree_git_directory_sync()) {
1531
+ if (directory === void 0) return false;
1532
+ const hold_taken_at = run_cut.hold_of(directory)?.taken_at;
1533
+ return matches(read(directory), { issue, hold_taken_at });
1534
+ }
1535
+ async function declined(request2, state, code) {
1536
+ if (request2.phase !== run_cut.IMPLEMENTATION_PHASE) return code;
1537
+ const directory = await run_cut.worktree_directory();
1538
+ if (directory !== void 0) {
1539
+ record(directory, { issue: request2.issue, hold_taken_at: state.hold_taken_at });
1540
+ }
1541
+ return code;
1542
+ }
1543
+ var run_cut_decline = { decline_path, declined, is_declined_sync, read, record };
1544
+
1545
+ // scripts/run/cut/run-cut-ready.ts
1546
+ function is_ready(tree) {
1547
+ return tree.branch !== tree.default_branch && tree.is_dirty;
1548
+ }
1549
+ async function is_lane_branch(state) {
1550
+ const default_branch = await git_command.get_default_branch();
1551
+ return is_ready({ branch: state.branch, default_branch, is_dirty: state.is_dirty });
1552
+ }
1553
+ var STATUS_ARGUMENTS = ["status", "--porcelain=v2", "--branch", UNTRACKED_FILES_FLAG];
1554
+ var HEADER_PREFIX = "#";
1555
+ var BRANCH_HEADER = "# branch.head ";
1556
+ function tree_of(status, default_branch) {
1557
+ const lines = status.split("\n").filter((line) => line !== "");
1558
+ const header = lines.find((line) => line.startsWith(BRANCH_HEADER));
1559
+ if (header === void 0) return void 0;
1560
+ const branch = header.slice(BRANCH_HEADER.length);
1561
+ const is_dirty = lines.some((line) => !line.startsWith(HEADER_PREFIX));
1562
+ return { branch, default_branch, is_dirty };
1563
+ }
1564
+ function read_tree_sync() {
1565
+ const status = git_spawn_sync.read(STATUS_ARGUMENTS);
1566
+ if (status === void 0) return void 0;
1567
+ const reference = git_spawn_sync.read(git_diff_reads.DEFAULT_BRANCH_ARGUMENTS);
1568
+ return tree_of(status, git_diff_reads.default_branch_of(reference));
1569
+ }
1570
+ function is_ready_sync(read3 = read_tree_sync) {
1571
+ const tree = read3();
1572
+ return tree === void 0 || is_ready(tree);
1573
+ }
1574
+ var run_cut_ready = { is_lane_branch, is_ready, is_ready_sync, tree_of };
1575
+
1576
+ // scripts/rules/implementation-cut-verdict.ts
1577
+ import { z as z6 } from "zod";
1513
1578
  var VERDICT_CACHE_PREFIX = "josh-impl-cut-verdict-";
1514
1579
  var VERDICT_REUSE_MS = 5e3;
1515
1580
  var VERDICT_VALUES = [
@@ -1517,7 +1582,7 @@ var VERDICT_VALUES = [
1517
1582
  "under",
1518
1583
  "unmeasurable"
1519
1584
  ];
1520
- var cache_schema = z5.object({ read_at_ms: z5.number(), verdict: z5.enum(VERDICT_VALUES) });
1585
+ var cache_schema = z6.object({ read_at_ms: z6.number(), verdict: z6.enum(VERDICT_VALUES) });
1521
1586
  function cache_path(directory) {
1522
1587
  return stamp_file.stamp_path(VERDICT_CACHE_PREFIX, directory);
1523
1588
  }
@@ -1528,11 +1593,11 @@ function cached_verdict(target, now_ms) {
1528
1593
  if (cached === void 0) return void 0;
1529
1594
  return now_ms - cached.read_at_ms < VERDICT_REUSE_MS ? cached.verdict : void 0;
1530
1595
  }
1531
- function reused_verdict(read2, directory = process.cwd(), now_ms = Date.now()) {
1596
+ function reused_verdict(read3, directory = process.cwd(), now_ms = Date.now()) {
1532
1597
  const target = cache_path(directory);
1533
1598
  const cached = cached_verdict(target, now_ms);
1534
1599
  if (cached !== void 0) return cached;
1535
- const value = read2();
1600
+ const value = read3();
1536
1601
  stamp_file.replace_stamp(target, { read_at_ms: now_ms, verdict: value });
1537
1602
  return value;
1538
1603
  }
@@ -1555,6 +1620,9 @@ function is_impl_cut_segment(segment) {
1555
1620
  function takes_the_impl_cut(command) {
1556
1621
  return shell_segments.segments_of(command).some((segment) => is_impl_cut_segment(segment));
1557
1622
  }
1623
+ function can_cut_sync(issue) {
1624
+ return !run_cut_decline.is_declined_sync(issue) && run_cut_ready.is_ready_sync();
1625
+ }
1558
1626
  function current_state() {
1559
1627
  return {
1560
1628
  directory: process.cwd(),
@@ -1564,7 +1632,8 @@ function current_state() {
1564
1632
  // **Read through the short reuse window**. The row now fires per
1565
1633
  // threshold crossing rather than once per run, so this predicate is a candidate on every edit;
1566
1634
  // the window collapses one turn's burst of edits to a single whole-transcript price.
1567
- verdict: () => implementation_cut_verdict.reused_verdict(() => cost_cli.session_verdict())
1635
+ verdict: () => implementation_cut_verdict.reused_verdict(() => cost_cli.session_verdict()),
1636
+ can_cut: can_cut_sync
1568
1637
  };
1569
1638
  }
1570
1639
  function lane_child_issue(state) {
@@ -1590,11 +1659,13 @@ function warrants_the_cut(state) {
1590
1659
  return verdict !== cost_verdict.UNDER_VERDICT;
1591
1660
  }
1592
1661
  function is_over_threshold_edit(call, state = current_state()) {
1593
- if (!is_edit_tool(call.name) || uncut_run_issue(state) === void 0) return false;
1594
- return warrants_the_cut(state);
1662
+ if (!is_edit_tool(call.name)) return false;
1663
+ const issue = uncut_run_issue(state);
1664
+ if (issue === void 0) return false;
1665
+ return warrants_the_cut(state) && state.can_cut(issue);
1595
1666
  }
1596
1667
  var THRESHOLD_TEXT = `${cost_format.format_tokens(CONTEXT_CUT_THRESHOLD)}-token`;
1597
- var IMPLEMENTATION_CUT_REASON = `\u26D4 implementation-phase cut: this checkout is a lane dispatched for this issue, or a run holds it for this issue (joshuafolkken/kit#2760), and its recent-context cost has crossed the shared ${THRESHOLD_TEXT} threshold mid-implementation, so the thinking accumulated so far is now re-read on every later request. Take the cut before this edit. First write a handoff file at \`${run_cut_handoff.HANDOFF_PATH}\` (ignored, so it is never committed) with the Write tool \u2014 the user\u2019s instruction verbatim, what you have completed, what remains, and what you deliberately did not touch, as ${run_cut_handoff.HANDOFF_FORMAT} (Markdown is refused) \u2014 and pass it as \`--handoff <path>\`, so the fresh process resumes on the original instruction rather than the working tree alone (joshuafolkken/kit#2354); a resume that finds no instruction is refused \`incomplete\` rather than continuing blind. \`pnpm josh run:cut --impl <N> --handoff <path>\` ends this process and relaunches a fresh one that resumes back into implementation (\`resume-impl\`), dropping the accumulated context rather than carrying it \u2014 joshuafolkken/kit#1933 built the boundary and joshuafolkken/kit#2310 measured it firing 0 times because the verdict was only ever read at session entry, where the context has not yet grown. The verdict is \`pnpm josh cost --cut\`'s exactly \u2014 the same per-request billed-input statistic against the same threshold, never a second measurement. Issue \`pnpm josh run:cut --impl <N> --handoff <path>\` now and read the verdict: on \`cut\`, read what the command printed. Where it relaunched a fresh process, **end the turn immediately** \u2014 that process owns the run and continues implementing, so it must not be waited for, and **no Telegram is owed**: a cut that resumes itself leaves a person nothing to act on. Where it printed that nothing was relaunched, do what it prints, then end the turn; on \`not-a-lane\`, \`unready\`, \`busy\`, \`failed\` or \`unknown\`, this process carries the run on and the edit is simply the next call. Never relaunch a second process after \`busy\`. This fired because the dispatch mark names this lane or the run hold names this issue; a person working here carries no mark and holds no run, so sees no refusal, so there is no human-or-child judgement left to make. The procedure is \`.claude/skills/workflow-commands/pre-gate-cut.md\`. Reissue this edit once the cut has answered \u2014 an edit reissued right after this refusal passes, so \`busy\` / \`failed\` cannot wedge the run edit after edit; this fires again on the next threshold crossing rather than once per run.`;
1668
+ var IMPLEMENTATION_CUT_REASON = `\u26D4 implementation-phase cut: refused on every edit past the ${THRESHOLD_TEXT} line. Write \`${run_cut_handoff.HANDOFF_PATH}\` as ${run_cut_handoff.HANDOFF_FORMAT}, run \`pnpm josh run:cut --impl <N> --handoff <path>\`; \`cut\`: end the turn immediately, no Telegram is owed; else reissue the edit (\`.claude/skills/workflow-commands/pre-gate-cut.md\`).`;
1598
1669
  var REISSUE_WINDOW_MS = 9e4;
1599
1670
  function is_reissued_refusal(now_ms, delivered_at_ms) {
1600
1671
  return now_ms - delivered_at_ms < REISSUE_WINDOW_MS;
@@ -1607,7 +1678,12 @@ var ROW4 = {
1607
1678
  is_trigger: is_over_threshold_edit,
1608
1679
  reason: IMPLEMENTATION_CUT_REASON,
1609
1680
  decide: decide2,
1610
- keeps: bash_triggers.on_bash_command(takes_the_impl_cut)
1681
+ keeps: bash_triggers.on_bash_command(takes_the_impl_cut),
1682
+ // The opening the reason carried before it was cut to the action and the next command.
1683
+ former_reasons: [
1684
+ "\u26D4 implementation-phase cut: this checkout is a lane dispatched",
1685
+ "\u26D4 implementation-phase cut: this run's context crossed the"
1686
+ ]
1611
1687
  };
1612
1688
  var implementation_cut = {
1613
1689
  IMPLEMENTATION_CUT_REASON,
@@ -1665,10 +1741,10 @@ var gh_api = {
1665
1741
  // scripts/rules/prior-comment-read.ts
1666
1742
  var ISSUE_PATH_NUMBER = /issues\/(\d+)/gu;
1667
1743
  var ISSUE_VIEW_NUMBER = /\bissue\s+view\s+(?:-{1,2}\S+\s+)*(\d+)/u;
1668
- var ISSUE_READ_ARGUMENTS = /\bissue:read\b([\s\d]*)/u;
1744
+ var ISSUE_READ_ARGUMENTS = /\b(?:issue:read|run:entry|run:prep)\b([\s\d]*)/u;
1669
1745
  var COMMENTS_ENDPOINT = /issues\/\d+\/comments\b/u;
1670
1746
  var VIEW_WITH_COMMENTS = /\bissue\s+view\b[^\n]*--comments\b/u;
1671
- var ISSUE_READ_COMMAND = /\bissue:read\b/u;
1747
+ var ISSUE_READ_COMMAND = /\b(?:issue:read|run:entry|run:prep)\b/u;
1672
1748
  function add_issue_read_numbers(command, numbers) {
1673
1749
  const match = ISSUE_READ_ARGUMENTS.exec(command);
1674
1750
  if (match === null) return;
@@ -1739,7 +1815,7 @@ function is_body_only_issue_read(command) {
1739
1815
  function reads_issue_comments(command) {
1740
1816
  return shell_segments.segments_of(command).some((segment) => fetches_issue_comments(segment));
1741
1817
  }
1742
- var ISSUE_COMMENTS_REASON = "\u26D4 issue comments: pnpm josh issue:read <N>\nRun that in place of this read: <N> is the Issue it names, and an Issue's comments are part of the Issue, so the body and every comment come back together. Without josh, add `gh api repos/{owner}/{repo}/issues/<N>/comments --jq '.[] | {user: .user.login, created_at, body}'` beside the body read, or `gh issue view <N> --comments` where GraphQL is reachable.\nWhy: a decision recorded after the body was written lives only in a comment \u2014 a corrected diagnosis (joshuafolkken/kit#1537), a changed default and an added acceptance criterion (joshuafolkken/kit#1520), a scope handed to another Issue (joshuafolkken/kit#1304) \u2014 and nothing in the body says it was superseded, so a body-only reader builds the wrong thing and sees no contradiction. Then the later text is the agreement in force \u2014 a comment supersedes the body it contradicts \u2014 except for two answers that are not the run's to make: work a comment reassigns to another Issue is out of scope and is not implemented, and a comment saying the Issue no longer has a reason to exist stops the run with a `confirmation` Telegram. The procedure is `.claude/skills/workflow-commands/issue-comments.md`. Every body-only read is refused until the comments are read.";
1818
+ var ISSUE_COMMENTS_REASON = "\u26D4 issue comments: pnpm josh issue:read <N>\nRun that in place of this read \u2014 an Issue's comments are part of it, and a later comment supersedes the body it contradicts; refused until that command is on the transcript. Without josh, read `gh api repos/{owner}/{repo}/issues/<N>/comments` beside the body (`.claude/skills/workflow-commands/issue-comments.md`).";
1743
1819
  var ROW5 = {
1744
1820
  id: "issue-comments",
1745
1821
  is_trigger: bash_triggers.on_bash_command(is_body_only_issue_read),
@@ -1768,7 +1844,7 @@ function is_bare_segment(segment) {
1768
1844
  function is_bare_josh_git(command) {
1769
1845
  return shell_segments.segments_of(command).some((segment) => is_bare_segment(segment));
1770
1846
  }
1771
- var JOSH_GIT_BARE_REASON = '\u26D4 bare `pnpm josh git`: this prompts to confirm the staging, and with no TTY the prompt falls to its cancel branch \u2014 `Checking unstaged files... Found` then `Operation cancelled.` \u2014 so the call throws away the time it takes to fail (about 31 seconds in one measured lane) and you reissue it with `-y` anyway (joshuafolkken/kit#2297). Run it unattended from the start: `pnpm josh git -y "<title> #<N>"`. The recovery form that opens the PR for a push that already landed is `pnpm josh git -y --skip-commit --skip-push`. This rule fires on every occurrence, not once per run.';
1847
+ var JOSH_GIT_BARE_REASON = '\u26D4 bare `pnpm josh git`: this prompts to confirm the staging, and with no TTY the prompt falls to its cancel branch. Run `pnpm josh git -y "<title> #<N>"`, or `pnpm josh git -y --skip-commit --skip-push` to open the PR for a push that already landed. It fires on every occurrence.';
1772
1848
  var ROW6 = {
1773
1849
  id: "josh-git-bare",
1774
1850
  is_trigger: bash_triggers.on_bash_command(is_bare_josh_git),
@@ -1801,7 +1877,7 @@ function is_carry_conflict(command, is_lane_child = lane_child_marker.is_child_o
1801
1877
  if (!invokes_budget_command(command)) return false;
1802
1878
  return is_lane_child;
1803
1879
  }
1804
- var LANE_CARRY_CONFLICT_REASON = '\u26D4 lane child carry conflict: this session is a dispatched lane child, and `pnpm josh run:merge` / `pnpm josh run:carry` are the parent backlogrun budget commands \u2014 a child must never run them. The live carry record here belongs to the parent that launched this child, not to a competing run: the parent holds the one run budget for the repository, and every lane shares the common git directory it is keyed on, so a live owner a lane child reads is always its parent (joshuafolkken/kit#2267, measured 2026-09-21 in backlogrun #2252 \u2014 a child read run:merge `busy` as a competitor and parked #2258 unimplemented). Do not run this command, and do not treat it as a conflict: no `needs-decision`, no `confirmation` Telegram, no question to a person \u2014 a dispatched child cannot ask, and there is nothing to decide. Continue implementing this Issue; the parent owns the budget and counts the outcome at the return. The procedure is `.claude/skills/workflow-commands/pre-gate-cut.md` and `backlogrun-progress.md` \u2192 "The hand-off". Live evidence of `run:carry` is taken with a fresh `JOSH_TEMP_ROOT` prefixed to the call, which this rule lets through (joshuafolkken/kit#3458); `run:merge` is refused under any root, because it also syncs main, closes lanes and writes to GitHub. This rule fires on every occurrence, not once per run.';
1880
+ var LANE_CARRY_CONFLICT_REASON = "\u26D4 lane child carry conflict: this session is a dispatched lane child, and `run:merge` / `run:carry` are the parent budget commands; the live record belongs to the parent, not to a competing run. Continue implementing this Issue: no `needs-decision`, no Telegram. Live `run:carry` evidence takes a fresh `JOSH_TEMP_ROOT`; `run:merge` is refused under any root, on every occurrence.";
1805
1881
  var ROW7 = {
1806
1882
  id: "lane-carry-conflict",
1807
1883
  is_trigger: bash_triggers.on_bash_command(is_carry_conflict),
@@ -1816,20 +1892,20 @@ var lane_carry_conflict = {
1816
1892
  };
1817
1893
 
1818
1894
  // scripts/agent/interactive-ask.ts
1819
- import { z as z6 } from "zod";
1895
+ import { z as z7 } from "zod";
1820
1896
  var INTERACTIVE_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion"]);
1821
1897
  function is_interactive_tool(name) {
1822
1898
  return INTERACTIVE_TOOLS.has(name);
1823
1899
  }
1824
- var OPTION_SCHEMA = z6.looseObject({ label: z6.string().optional() });
1825
- var QUESTION_SCHEMA = z6.looseObject({
1826
- question: z6.string().optional(),
1827
- options: z6.array(OPTION_SCHEMA).optional()
1900
+ var OPTION_SCHEMA = z7.looseObject({ label: z7.string().optional() });
1901
+ var QUESTION_SCHEMA = z7.looseObject({
1902
+ question: z7.string().optional(),
1903
+ options: z7.array(OPTION_SCHEMA).optional()
1828
1904
  });
1829
- var ASK_INPUT_SCHEMA = z6.looseObject({ questions: z6.array(QUESTION_SCHEMA).optional() });
1830
- var DENIAL_SCHEMA = z6.looseObject({
1831
- tool_name: z6.string().optional(),
1832
- tool_input: z6.unknown().optional()
1905
+ var ASK_INPUT_SCHEMA = z7.looseObject({ questions: z7.array(QUESTION_SCHEMA).optional() });
1906
+ var DENIAL_SCHEMA = z7.looseObject({
1907
+ tool_name: z7.string().optional(),
1908
+ tool_input: z7.unknown().optional()
1833
1909
  });
1834
1910
  var UNKNOWN_QUESTION = "(question text unavailable)";
1835
1911
  var OPTION_SEPARATOR = " / ";
@@ -1862,7 +1938,7 @@ function is_interactive_ask(call, is_lane_child = lane_child_marker.is_child_of(
1862
1938
  if (!interactive_ask.is_interactive_tool(call.name)) return false;
1863
1939
  return is_lane_child;
1864
1940
  }
1865
- var LANE_INTERACTIVE_ASK_REASON = '\u26D4 lane child interactive ask: this session is a dispatched lane child and cannot ask a person, so this call is refused. Left to run it would end the turn with nothing on the Issue \u2014 the parent then finds an OPEN Issue with `in-progress` and no question (joshuafolkken/kit#2201; joshuafolkken/kit#2034 placed the same rule one tool-call too late). Do not ask: park the decision instead. Apply `needs-decision` and post a comment carrying the question, the options and whether work was stashed \u2014 `gh api repos/{owner}/{repo}/issues/<N>/labels -f \'labels[]=needs-decision\'` then `pnpm josh issue:comment <N> --body-file <path>` \u2014 then send the `confirmation` Telegram and stop. The procedure is `.claude/skills/workflow-commands/pre-gate-cut.md` \u2192 "A lane child records its park before it stops", and the park itself is `backlogrun-park.md` \u2192 "park and continue". This rule fires on every occurrence, not once per run.';
1941
+ var LANE_INTERACTIVE_ASK_REASON = "\u26D4 lane child interactive ask: this session is a dispatched lane child and cannot ask a person, so park instead: `gh api repos/{owner}/{repo}/issues/<N>/labels -f 'labels[]=needs-decision'`, `pnpm josh issue:comment <N> --body-file <path>`, the `confirmation` Telegram, stop. It fires on every occurrence (`.claude/skills/workflow-commands/pre-gate-cut.md`, `backlogrun-park.md`).";
1866
1942
  var ROW8 = {
1867
1943
  id: "lane-interactive-ask",
1868
1944
  is_trigger: is_interactive_ask,
@@ -1918,7 +1994,7 @@ function decide3(_call, run2) {
1918
1994
  const commands = tail_commands.prior_bash_commands(time_density_hook.read_tail(run2.transcript));
1919
1995
  return issue !== void 0 && promoted_issues(commands).has(issue);
1920
1996
  }
1921
- var LANE_SPLIT_PARK_REASON = '\u26D4 lane split park: this session is a dispatched lane child that promoted its own Issue to an epic this run, and a split is not a park \u2014 do not apply `needs-decision` to the promoted epic and do not send a `confirmation` Telegram. The parent reads an open Issue carrying `epic` as a split and continues through its children; a question here has nobody to answer it (joshuafolkken/kit#3296, measured 2026-10-06 when the child for #3255 parked its promoted epic). If what remains of the original work needs a person, park that child issue instead, then end the turn. The procedure is `.claude/skills/workflow-commands/backlogrun-park.md` \u2192 "Splitting a child mid-run". This rule fires on every occurrence, not once per run.';
1997
+ var LANE_SPLIT_PARK_REASON = '\u26D4 lane split park: this session is a dispatched lane child that promoted its own Issue to an epic this run, and a split is not a park: no `needs-decision` on the epic, no `confirmation` Telegram, on every occurrence. Park a child issue if the rest needs a person, else end the turn (`.claude/skills/workflow-commands/backlogrun-park.md` \u2192 "Splitting a child mid-run").';
1922
1998
  var ROW9 = {
1923
1999
  id: "lane-split-park",
1924
2000
  is_trigger: bash_triggers.on_bash_command(is_candidate),
@@ -1944,8 +2020,9 @@ var NON_COLLIDING_FLAGS = /* @__PURE__ */ new Set([
1944
2020
  var FLAG_PREFIX2 = "-";
1945
2021
  function switch_target(segment) {
1946
2022
  const call = git_argv.parse(segment);
1947
- if (call?.subcommand !== SWITCH_SUBCOMMAND) return void 0;
1948
- if (call.args.some((argument) => NON_COLLIDING_FLAGS.has(argument))) return void 0;
2023
+ if (call?.subcommand !== SWITCH_SUBCOMMAND || call.args.some((argument) => NON_COLLIDING_FLAGS.has(argument))) {
2024
+ return void 0;
2025
+ }
1949
2026
  return call.args.find((argument) => !argument.startsWith(FLAG_PREFIX2));
1950
2027
  }
1951
2028
  function switch_targets(command) {
@@ -1959,7 +2036,7 @@ function is_switch_away_from_lane(command, is_lane_child = lane_child_marker.is_
1959
2036
  if (!is_lane_child || own_branch === void 0) return false;
1960
2037
  return switch_targets(command).some((target) => target !== own_branch);
1961
2038
  }
1962
- var LANE_SWITCH_MAIN_REASON = "\u26D4 lane child on `git switch main`: this session is a dispatched lane child, and switching to the default branch here always fails \u2014 a lane is a linked work tree, the primary checkout holds the default branch, and git allows one branch in one work tree at a time (`fatal: main is already used by worktree at <the primary checkout>`, measured 11 times in the 2026-09-21 backlogrun). Do not switch: the lane is already on its own issue branch, branched from a fresh default before it opened, and the latest default is brought in during the gate by `pnpm josh main:merge`. So skip this step and keep implementing on the current branch \u2014 no `git switch`, no `git pull`. `pnpm josh main:sync` (`josh ms`) is refused in a lane for the same reason, and a lane is finished with `pnpm josh lane:close <issue-number>`. The parent runs `pnpm josh ms` in the primary checkout before each lane opens, never the child \u2014 `backlogrun-lanes.md` and `backlogrun-child.md` are the single sources. This rule fires on every occurrence, not once per run.";
2039
+ var LANE_SWITCH_MAIN_REASON = "\u26D4 lane child on `git switch main`: this session is a dispatched lane child, and switching to the default branch here always fails (`already used by worktree`: the primary checkout holds it). Skip this step and keep implementing on the current branch; `pnpm josh main:merge` brings the default in during the gate. Refused on every occurrence (`backlogrun-lanes.md`, `backlogrun-child.md`).";
1963
2040
  var ROW10 = {
1964
2041
  id: "lane-switch-main",
1965
2042
  is_trigger: bash_triggers.on_bash_command(is_switch_away_from_lane),
@@ -2471,7 +2548,7 @@ function command_line(oracle) {
2471
2548
  return oracle.args ? `pnpm josh ${command} ${oracle.args}` : `pnpm josh ${command}`;
2472
2549
  }
2473
2550
  function reason_for(oracle, firing_point) {
2474
- return `\u26D4 decision oracle not consulted: this is ${firing_point.describes}, but the run has not run \`${command_line(oracle)}\` first \u2014 the oracle that decides ${oracle.decision.toLowerCase()}. It answers ${oracle.vocabulary.join(" | ")}; single source ${oracle.single_source}. Reissue after running it and reading its verdict. Every such call is refused until that command is on the transcript.`;
2551
+ return `\u26D4 decision oracle not consulted: this is ${firing_point.describes}. Run \`${command_line(oracle)}\` first \u2014 it decides ${oracle.decision.toLowerCase()} and answers ${oracle.vocabulary.join(" | ")} (${oracle.single_source}). Every such call is refused until that command is on the transcript.`;
2475
2552
  }
2476
2553
  function row_for(oracle, firing_point) {
2477
2554
  const names = command_names(oracle);
@@ -2537,12 +2614,14 @@ function is_destructive_segment2(segment) {
2537
2614
  function is_destructive_command(command) {
2538
2615
  return shell_segments.segments_of(command).some((segment) => is_destructive_segment2(segment));
2539
2616
  }
2540
- var DESTRUCTIVE_COMMAND_REASON = "\u26D4 destructive command: a recursive forced `rm` (`rm -rf`, `rm -fr`, `rm -r -f`, `rm --recursive --force`), `gh repo delete` / `archive`, `gh repo edit --visibility`, `gh pr close`, or a `gh api -X DELETE repos/\u2026` call deletes or closes something that cannot be restored, which is Tier C and needs explicit current-turn user instruction (`prompts/collaboration-workflow/operating-rules.md`). The `.claude/settings.json` deny list matches prefix globs, so it misses these spellings; this row reads the argv instead. Remove a file or directory one path at a time without `-f`, or ask the user to run the command in their own terminal. `gh issue close` and removing a label from an Issue (`DELETE repos/<o>/<r>/issues/<N>/labels/<name>`) are not refused. **This rule fires on every occurrence, not once per run.**";
2617
+ var DESTRUCTIVE_COMMAND_REASON = "\u26D4 destructive command: a recursive forced `rm`, a `gh repo delete` / `archive` / visibility edit, `gh pr close` or a `gh api -X DELETE` cannot be undone, which is Tier C. Remove one path at a time without `-f`, or ask the user to run it. It fires on every occurrence (`prompts/collaboration-workflow/operating-rules.md`).";
2541
2618
  var ROW11 = {
2542
2619
  id: "destructive-command",
2543
2620
  is_trigger: bash_triggers.on_bash_command(is_destructive_command),
2544
2621
  reason: DESTRUCTIVE_COMMAND_REASON,
2545
- decide: () => true
2622
+ decide: () => true,
2623
+ // The opening the reason carried before it was cut to the action and the next command.
2624
+ former_reasons: ["\u26D4 destructive command: a recursive forced `rm` (`rm -rf`"]
2546
2625
  };
2547
2626
  var destructive_command = { DESTRUCTIVE_COMMAND_REASON, ROW: ROW11, is_destructive_command };
2548
2627
 
@@ -2666,7 +2745,7 @@ function is_unauthorized_segment(segment) {
2666
2745
  function is_unauthorized_worktree_change(command) {
2667
2746
  return shell_segments.segments_of(command).some((segment) => is_unauthorized_segment(segment));
2668
2747
  }
2669
- var WORKTREE_MUTATION_REASON = '\u26D4 unauthorized working-tree change: `git checkout -- <path>`, `git restore <path>`, a forced `git clean` and an unauthorized `git stash` discard work or mutate the repository-wide stash every work tree shares, and none is on the `.claude/settings.json` deny list (`operating-rules.md`). Do not run them on your own judgement. A commit goes through `pnpm josh git`; a stash pop goes through `pnpm josh stash:pop "<message>"`, never a positional `git stash pop` a shared stack lets another lane divert. A stash push is authorized only inside a documented flow and only with a findable message \u2014 `git stash push -u -m "<message>"` \u2014 which this row allows; a bare `git stash`, a message-less push, or `pop` / `apply` / `drop` is refused. A plan that restores a handed-over stash with `git stash apply` / `git stash pop` is read as `pnpm josh stash:pop "<message>"`: apply it that way, continue, and record the substitution on the Issue \u2014 Tier A, never a `needs-decision` park unless the plan says why the owner keeps the stash: then stop and confirm (`operating-rules.md` \u2192 "no-self-staging"). **This rule fires on every occurrence, not once per run.**';
2748
+ var WORKTREE_MUTATION_REASON = '\u26D4 unauthorized working-tree change: `git checkout --`, `git restore`, `git clean -f`, `git stash`. A documented flow stashes with `git stash push -u -m "<message>"`; a handed-over stash is `pnpm josh stash:pop "<message>"`; record the substitution on the Issue (Tier A) unless the plan says why the owner keeps the stash: then stop and confirm. It fires on every occurrence (`operating-rules.md`).';
2670
2749
  var ROW13 = {
2671
2750
  id: "worktree-mutation",
2672
2751
  is_trigger: bash_triggers.on_bash_command(is_unauthorized_worktree_change),
@@ -2702,7 +2781,7 @@ function is_index_segment(segment) {
2702
2781
  function is_index_mutation(command) {
2703
2782
  return shell_segments.segments_of(command).some((segment) => is_index_segment(segment));
2704
2783
  }
2705
- var INDEX_MUTATION_REASON = '\u26D4 git index mutation: `git add` / `stage` / `commit` / `reset` / `rm` / `mv` and `git restore --staged` overwrite the user\'s staged snapshot, which `CLAUDE.md` \u2192 Git Rules forbids on your own judgement (`prompts/collaboration-workflow/operating-rules.md` \u2192 "no-self-staging"). The `.claude/settings.json` deny list matches prefix globs, so it misses `git -C . commit`, `git -c k=v commit` and `env git commit`; this row reads the argv instead, so the spelling does not matter. A commit goes through `pnpm josh git -y`; any other staging needs explicit current-turn user instruction, run by the user in their own terminal. **This rule fires on every occurrence, not once per run.**';
2784
+ var INDEX_MUTATION_REASON = "\u26D4 git index mutation: `git add` / `stage` / `commit` / `reset` / `rm` / `mv` and `git restore --staged` overwrite the user's staged snapshot. Commit with `pnpm josh git -y`; any other staging is the user's, on explicit instruction. It fires on every occurrence (`prompts/collaboration-workflow/operating-rules.md` \u2192 \"no-self-staging\").";
2706
2785
  var ROW14 = {
2707
2786
  id: "index-mutation",
2708
2787
  is_trigger: bash_triggers.on_bash_command(is_index_mutation),
@@ -2722,20 +2801,20 @@ import path4 from "node:path";
2722
2801
  var KIT_PACKAGE_NAME = "@joshuafolkken/kit";
2723
2802
 
2724
2803
  // scripts/doctor/doctor-consumer.ts
2725
- import { z as z7 } from "zod";
2804
+ import { z as z8 } from "zod";
2726
2805
  var NODE_MODULES = "node_modules";
2727
2806
  var SETTINGS_PATH = path4.join(".claude", "settings.json");
2728
2807
  var CLAUDE_MD = "CLAUDE.md";
2729
2808
  var PLUGIN_ID = "kit@kit";
2730
2809
  var CLAUDE_MD_POINTER = `@${NODE_MODULES}/${KIT_PACKAGE_NAME}/dist/${CLAUDE_MD}`;
2731
2810
  var SECRETLINT_BIN = path4.join(NODE_MODULES, ".bin", "secretlint");
2732
- var manifest_schema = z7.object({
2733
- name: z7.string().optional(),
2734
- dependencies: z7.record(z7.string(), z7.string()).optional(),
2735
- devDependencies: z7.record(z7.string(), z7.string()).optional()
2811
+ var manifest_schema = z8.object({
2812
+ name: z8.string().optional(),
2813
+ dependencies: z8.record(z8.string(), z8.string()).optional(),
2814
+ devDependencies: z8.record(z8.string(), z8.string()).optional()
2736
2815
  });
2737
- var settings_schema = z7.object({
2738
- enabledPlugins: z7.record(z7.string(), z7.boolean()).optional()
2816
+ var settings_schema = z8.object({
2817
+ enabledPlugins: z8.record(z8.string(), z8.boolean()).optional()
2739
2818
  });
2740
2819
  var STATE_OK = "ok";
2741
2820
  var STATE_TARGET_MISSING = "target-missing";
@@ -2898,12 +2977,14 @@ function is_consumer_settings_edit(call, is_kit_consumer2) {
2898
2977
  function is_protected_file_call(call, is_kit_consumer2 = doctor_consumer.is_kit_consumer) {
2899
2978
  return is_environment_read(call) || is_consumer_settings_edit(call, is_kit_consumer2);
2900
2979
  }
2901
- var PROTECTED_FILE_REASON = '\u26D4 protected file: `.env` holds the Telegram bot token, and a `.env.*` variant or `.dev.vars` holds the same kind of secret, so reading one \u2014 with the Read tool or a shell reader such as `cat` / `grep` / `source` \u2014 puts a secret in the transcript \u2014 the scripts that need it load it in their own process (the tracked `.env.example` and `.env.test` stay readable). `.claude/settings.json` in a consumer repository is the deny list and the hooks every other rule rests on, so a run must not rewrite it; `josh sync` owns it, and a change belongs upstream in kit (`CLAUDE.md` \u2192 "Route distributed-doc / config changes upstream to kit"). Leave these files alone, or ask the user. **This rule fires on every occurrence, not once per run.**';
2980
+ var PROTECTED_FILE_REASON = "\u26D4 protected file: `.env`, a `.env.*` variant and `.dev.vars` hold secrets, and a consumer `.claude/settings.json` is owned by `josh sync` (a change belongs upstream in kit). Leave these files alone, or ask the user. It fires on every occurrence.";
2902
2981
  var ROW15 = {
2903
2982
  id: "protected-file",
2904
2983
  is_trigger: is_protected_file_call,
2905
2984
  reason: PROTECTED_FILE_REASON,
2906
- decide: () => true
2985
+ decide: () => true,
2986
+ // The opening the reason carried before it was cut to the action and the next command.
2987
+ former_reasons: ["\u26D4 protected file: `.env` holds the Telegram bot token"]
2907
2988
  };
2908
2989
  var protected_files = { ROW: ROW15, is_protected_file_call };
2909
2990
 
@@ -2940,7 +3021,7 @@ function is_verification_command(segment) {
2940
3021
  function is_masked_verification(command) {
2941
3022
  return time_shell.discarded_commands(command).some((segment) => is_verification_command(segment));
2942
3023
  }
2943
- var PIPED_VERIFICATION_REASON = "\u26D4 piped verification: a pipeline exits with its last command's status, so `pnpm josh gate | tail` reports success on a gate that failed, and the verdict is discarded before anything reads it. Run the check without the pipe \u2014 josh prints its verdict line last, so the harness output cap keeps it even when the middle is elided. Where the output genuinely has to be narrowed, redirect it to a file and read ranges from that file, or prefix `set -o pipefail` so the pipeline carries the check's status. Read the printed verdict either way, never the exit code alone. Read-only listings are untouched \u2014 this fires only on a command whose result means pass or fail. The rule is in `prompts/collaboration-workflow/output-bounds.md`. Reissue this call with no pipe \u2014 it fires once per run and cannot repeat on the call in hand.";
3024
+ var PIPED_VERIFICATION_REASON = "\u26D4 piped verification: a pipeline exits with its last command's status, so `pnpm josh gate | tail` reports success on a gate that failed. Reissue this call with no pipe (josh prints its verdict last), or redirect it to a file and read ranges from that file, or prefix `set -o pipefail`; read the verdict line, never the exit code alone (`prompts/collaboration-workflow/output-bounds.md`).";
2944
3025
  function pipelines_of(command) {
2945
3026
  return shell_segments.segments_of(time_shell.unquoted(command)).map((segment) => segment.split("|").map((piece) => piece.trim()));
2946
3027
  }
@@ -2953,12 +3034,15 @@ function runs_verification(command) {
2953
3034
  function keeps_verdict_intact(command) {
2954
3035
  return runs_verification(command) && !is_masked_verification(command);
2955
3036
  }
2956
- var PIPEFAIL_PREFIX = "set -o pipefail; ";
3037
+ var PIPEFAIL_OPEN = "(set -o pipefail; ";
3038
+ var PIPEFAIL_CLOSE = ")";
3039
+ var COMMENT_START = /(?:^|\s)#/u;
2957
3040
  var FULL_READING_FILTER = /^(?:tail|grep)\b/u;
2958
3041
  var EARLY_EXIT_GREP = /^grep\b.*\s(?:-[a-zA-Z]*[lLmq]|--(?:quiet|silent|max-count|files-with(?:out)?-match))/u;
2959
3042
  var ALLOWED_OPERATORS = /2>&1|&&/gu;
2960
3043
  var SIDE_EFFECT_SYNTAX = /[&<>`]|\$\(/u;
2961
- var PIPEFAIL_NOTE = "\u21BB piped verification: the josh check was piped, so `set -o pipefail;` was prefixed and the call ran with the check's status carried to the exit code. Read the printed verdict, never the exit code alone \u2014 `prompts/collaboration-workflow/output-bounds.md` (joshuafolkken/kit#3570).";
3044
+ var GROUP_SYNTAX = /[()]/u;
3045
+ var PIPEFAIL_NOTE = "\u21BB piped verification: the josh check was piped, so its pipeline was wrapped in `(set -o pipefail; \u2026)` to carry the check's status, and the rest of the line ran as typed. Read the printed verdict, never the exit code alone \u2014 `prompts/collaboration-workflow/output-bounds.md`.";
2962
3046
  function reads_to_the_end(piece) {
2963
3047
  return FULL_READING_FILTER.test(piece) && !EARLY_EXIT_GREP.test(piece);
2964
3048
  }
@@ -2969,14 +3053,40 @@ function is_filtered_check(pipeline) {
2969
3053
  function has_side_effect_syntax(command) {
2970
3054
  return SIDE_EFFECT_SYNTAX.test(command.replaceAll(ALLOWED_OPERATORS, " "));
2971
3055
  }
2972
- function is_pipefail_rewritable(command) {
2973
- return is_masked_verification(command) && !has_side_effect_syntax(command) && pipelines_of(command).every((pipeline) => is_filtered_check(pipeline));
3056
+ function is_rewritable_part(raw, judged) {
3057
+ const pipeline = judged.split("|").map((piece) => piece.trim());
3058
+ return !has_side_effect_syntax(raw) && !GROUP_SYNTAX.test(judged) && is_filtered_check(pipeline);
3059
+ }
3060
+ function code_end(judged) {
3061
+ const comment = COMMENT_START.exec(judged);
3062
+ return comment === null ? judged.length : comment.index;
3063
+ }
3064
+ function wrapped(raw, judged) {
3065
+ const code = raw.slice(0, code_end(judged));
3066
+ const core = code.trim();
3067
+ const lead = code.slice(0, code.indexOf(core));
3068
+ return `${lead}${PIPEFAIL_OPEN}${core}${PIPEFAIL_CLOSE}${raw.slice(lead.length + core.length)}`;
3069
+ }
3070
+ function rewritten_part(raw, judged) {
3071
+ if (!is_masked_verification(judged)) return raw;
3072
+ return is_rewritable_part(raw, judged) ? wrapped(raw, judged) : void 0;
3073
+ }
3074
+ function pipefail_command(command) {
3075
+ const judged = time_shell.blanked(command);
3076
+ if (judged === void 0 || !is_masked_verification(command)) return void 0;
3077
+ const offset = { at: 0 };
3078
+ const parts = shell_segments.parts_of(judged).map((part) => {
3079
+ const raw = command.slice(offset.at, offset.at + part.length);
3080
+ offset.at += part.length;
3081
+ return rewritten_part(raw, part);
3082
+ });
3083
+ const rewritten = parts.includes(void 0) ? void 0 : parts.join("");
3084
+ return rewritten === command ? void 0 : rewritten;
2974
3085
  }
2975
3086
  function pipefail_input(call) {
2976
3087
  if (call.name !== cost_blocks.BASH_TOOL || !json_value.is_record(call.input)) return void 0;
2977
- const command = time_shell.bash_command(call.input);
2978
- if (!is_pipefail_rewritable(command)) return void 0;
2979
- return { ...call.input, command: `${PIPEFAIL_PREFIX}${command}` };
3088
+ const command = pipefail_command(time_shell.bash_command(call.input));
3089
+ return command === void 0 ? void 0 : { ...call.input, command };
2980
3090
  }
2981
3091
  var piped_verification = {
2982
3092
  PIPED_VERIFICATION_REASON,
@@ -3013,7 +3123,7 @@ function is_poll_loop(command) {
3013
3123
  if (is_state_wait(plain)) return true;
3014
3124
  return LOOP_KEYWORD2.test(plain) && SLEEP_COMMAND.test(plain);
3015
3125
  }
3016
- var POLL_LOOP_REASON = '\u26D4 output poll loop: this hand-writes a `while`/`until` wait loop over a command\'s output, a marker file or a process \u2014 with a `sleep` or, worse, without one \u2014 and the loop does not know when the command finished. A regex that never matches waits to the harness block limit; one that matches a mid-run line exits too early \u2014 either way the command exit is not what ends the wait, and each pass spends a full-context round trip (joshuafolkken/kit#2371 measured about 45 minutes, 13% of six lanes, lost to exactly this). Do not poll the output file. A command known to take minutes \u2014 `pnpm josh gate`, `pnpm josh followup`, `pnpm josh format` \u2014 is started detached from the first with `run_in_background` set, and the harness re-invokes you when it exits: its completion notification is what ends the wait, on the task exit rather than a regex or the block limit. So issue the long command in the background and end the turn \u2014 do not arm a loop; the wake-up brings you back. To glance at interim output, `Read` the output file once, never in a sleep loop. If you are waiting on CI, `pnpm josh followup` waits on it for you. The procedure is `.claude/skills/workflow-commands/background-commands.md`, "A command that can take minutes is issued in the background". **This rule fires on every occurrence, not once per run.**';
3126
+ var POLL_LOOP_REASON = "\u26D4 output poll loop: this hand-writes a `while`/`until` wait loop over a command's output, a marker file or a process, and the loop does not know when the command finished. Issue the long command with `run_in_background` set and end the turn \u2014 its exit re-invokes you; for CI, `pnpm josh followup` (`.claude/skills/workflow-commands/background-commands.md`). It fires on every occurrence.";
3017
3127
  var ROW16 = {
3018
3128
  id: "poll-loop",
3019
3129
  is_trigger: bash_triggers.on_bash_command(is_poll_loop),
@@ -3063,8 +3173,7 @@ function dispatched_issue(state) {
3063
3173
  }
3064
3174
  function uncut_lane_issue(state) {
3065
3175
  const issue = lane_paths.lane_issue_of(state.directory);
3066
- if (issue === void 0) return void 0;
3067
- if (dispatched_issue(state) !== issue) return void 0;
3176
+ if (issue === void 0 || dispatched_issue(state) !== issue) return void 0;
3068
3177
  return state.carried()?.issue === issue ? void 0 : issue;
3069
3178
  }
3070
3179
  function warrants_the_cut2(verdict) {
@@ -3074,7 +3183,7 @@ function is_uncut_gate(command, state = current_state2()) {
3074
3183
  if (!runs_the_gate(command) || uncut_lane_issue(state) === void 0) return false;
3075
3184
  return warrants_the_cut2(state.context_verdict());
3076
3185
  }
3077
- var PRE_GATE_CUT_REASON = "\u26D4 pre-gate cut: this checkout is a lane dispatched for this issue and the gate is the boundary to cut at, so take the cut before running it. `pnpm josh run:cut <N>` ends this process and relaunches a fresh one that resumes from the gate, dropping the thinking built up while implementing rather than carrying it on every later request \u2014 joshuafolkken/kit#1839 measured 176K of 204K output riding across that boundary, and joshuafolkken/kit#1864 measured the step itself firing 0 times in 6 lane children while it was carried as prose. Issue `pnpm josh run:cut <N>` now, and read the verdict: on `cut`, **end the turn immediately** \u2014 the fresh process owns the run from there and must not be waited for, and **no Telegram is owed** for a cut that resumes itself; on `not-a-lane`, `unready`, `busy`, `failed` or `unknown`, this process carries the run on and the gate is simply the next call. Never relaunch a second process after `busy`. This fired because the dispatch mark names this lane; a person working here carries no mark and sees no refusal, so there is no human-or-child judgement left to make. The procedure is `.claude/skills/workflow-commands/pre-gate-cut.md`. Reissue the gate once the cut has answered \u2014 this fires once per run, so it cannot repeat on the call in hand.";
3186
+ var PRE_GATE_CUT_REASON = "\u26D4 pre-gate cut: this checkout is a lane dispatched for this issue and the gate is the boundary to cut at. Issue `pnpm josh run:cut <N>`: on `cut`, end the turn immediately \u2014 no Telegram is owed; on any other verdict (`not-a-lane`, `busy`, \u2026), reissue the gate \u2014 this fires once per run (`.claude/skills/workflow-commands/pre-gate-cut.md`).";
3078
3187
  function reaches_the_pre_gate_boundary(call, _turn, run2) {
3079
3188
  if (!bash_triggers.on_bash_command(asks_about_the_cut)(call)) return false;
3080
3189
  return run2.some((issued) => bash_triggers.on_bash_command(claims_the_hold)(issued));
@@ -3095,7 +3204,7 @@ var RAW_FIELD_BODY_AT = /(?:^|\s)(?:-f|--raw-field)[\s=]+['"]?body=['"]?@/u;
3095
3204
  function posts_at_path_literally(command) {
3096
3205
  return RAW_FIELD_BODY_AT.test(command);
3097
3206
  }
3098
- var RAW_FIELD_BODY_REASON = "\u26D4 literal @path body: this passes `body=@<path>` to `gh api` with a raw field flag (`-f` / `--raw-field`), which sends the value verbatim \u2014 the string `@<path>` is posted as the comment, not the file it points at. `-F` / `--field` is the flag that reads the file, and the two spellings differ by one character while `gh` exits 0 and returns a comment URL either way, so the break is invisible until a person reads the issue (joshuafolkken/kit#2304 lost two park comments this way). Post an issue or PR comment with `pnpm josh issue:comment <N> --body-file <path>` \u2014 the one spelling, which cannot take the wrong flag. The rule and the safe forms are in `prompts/collaboration-workflow/shell-body.md`. This rule fires on every occurrence, not once per run.";
3207
+ var RAW_FIELD_BODY_REASON = "\u26D4 literal @path body: this passes `body=@<path>` to `gh api` with a raw field flag (`-f` / `--raw-field`), which posts the string `@<path>` rather than the file. Post the comment with `pnpm josh issue:comment <N> --body-file <path>`; this rule fires on every occurrence (`prompts/collaboration-workflow/shell-body.md`).";
3099
3208
  var ROW17 = {
3100
3209
  id: "raw-field-body",
3101
3210
  is_trigger: bash_triggers.on_bash_command(posts_at_path_literally),
@@ -3129,10 +3238,10 @@ function string_field2(input, key) {
3129
3238
  function read_existing(file_path) {
3130
3239
  return file_reader.read_file_or_empty(file_path);
3131
3240
  }
3132
- function texts_of(name, input, read2) {
3241
+ function texts_of(name, input, read3) {
3133
3242
  if (name === WRITE_TOOL) {
3134
3243
  return {
3135
- old_text: read2(string_field2(input, "file_path")),
3244
+ old_text: read3(string_field2(input, "file_path")),
3136
3245
  new_text: string_field2(input, "content")
3137
3246
  };
3138
3247
  }
@@ -3148,13 +3257,13 @@ function rule_document_input(call) {
3148
3257
  if (!is_edit_tool2(call.name) || !json_value.is_record(call.input)) return void 0;
3149
3258
  return is_rule_document(string_field2(call.input, "file_path")) ? call.input : void 0;
3150
3259
  }
3151
- function writes_rule_prose(call, read2 = read_existing) {
3260
+ function writes_rule_prose(call, read3 = read_existing) {
3152
3261
  const input = rule_document_input(call);
3153
3262
  if (input === void 0) return false;
3154
- const { old_text, new_text } = texts_of(call.name, input, read2);
3263
+ const { old_text, new_text } = texts_of(call.name, input, read3);
3155
3264
  return adds_rule_prose(old_text, new_text);
3156
3265
  }
3157
- var RULE_BODY_REASON = "\u26D4 rule written into prose: this edit adds rule text to CLAUDE.md, a `prompts/**` doc or a skill body. Before it lands, run the residency questions the addition skips. A rule whose answer is computable from mechanically readable inputs should be a decision oracle, not prose \u2014 question 0; `pnpm josh oracle:list` lists the existing oracles a new one joins (single source `scripts/rules/decision-oracle.ts`). A rule that decides *when* or *in what order* to act belongs in the run driver, not prose \u2014 the ordering question; `pnpm josh run:step` is the state machine a sequencing rule moves into. Only a rule that is neither \u2014 not computable, not ordering \u2014 stays prose, and then as a trigger-plus-pointer line rather than a second copy of its procedure. The criterion is `prompts/collaboration-workflow/residency.md` (question 0 and the ordering question); the delivery enumeration is `prompts/collaboration-workflow/rule-delivery.md`. Every such edit is refused until both commands are on the transcript \u2014 run them, confirm the rule can be neither an oracle nor an ordering step, then reissue this edit.";
3266
+ var RULE_BODY_REASON = "\u26D4 rule written into prose: this edit adds rule text to CLAUDE.md, a `prompts/**` doc or a skill body. Answer question 0 (`pnpm josh oracle:list`) and the ordering question (`pnpm josh run:step`) of `prompts/collaboration-workflow/residency.md`; refused until both commands are on the transcript, then reissue this as a trigger-plus-pointer line (`prompts/collaboration-workflow/rule-delivery.md`).";
3158
3267
  var PLACEMENT_ORACLES = [
3159
3268
  /* @__PURE__ */ new Set(["oracle:list"]),
3160
3269
  /* @__PURE__ */ new Set(["run:step", "run:next"])
@@ -3250,7 +3359,7 @@ function carries_a_body(command) {
3250
3359
  if (BODY_FLAG.test(command.replaceAll(QUOTED_SPAN, " "))) return true;
3251
3360
  return inline_body_literals(command).length > 0;
3252
3361
  }
3253
- var SHELL_BODY_REASON = "\u26D4 shell-evaluated body: this command carries a body inline in double quotes, and that body contains a backtick or a `$`, so the shell runs it \u2014 the text is executed rather than merely mangled (joshuafolkken/kit#1198). Write the body to a file and pass it by path: `pnpm josh issue:comment <N> --body-file <path>`, `pnpm josh followup --notify-message-file <path>`, or `--body-file <path>` wherever offered. `$'\u2026'` quoting is the other safe form; the rule and the trigger's blind spots are in `prompts/collaboration-workflow/shell-body.md`. Reissue this call once the body is in a file \u2014 it fires once per run and cannot repeat on the call in hand.";
3362
+ var SHELL_BODY_REASON = "\u26D4 shell-evaluated body: this command carries a body inline in double quotes, and that body contains a backtick or a `$`, so the shell executes it. Reissue this call once the body is in a file, passed by path (`pnpm josh issue:comment <N> --body-file <path>`, `--notify-message-file <path>`), or use `$'\u2026'` quoting (`prompts/collaboration-workflow/shell-body.md`).";
3254
3363
  var shell_body_trigger = {
3255
3364
  SHELL_BODY_REASON,
3256
3365
  carries_a_body,
@@ -3283,9 +3392,9 @@ import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync5 }
3283
3392
  import path12 from "node:path";
3284
3393
 
3285
3394
  // scripts/safe-chain/pack-hook.ts
3286
- import { z as z8 } from "zod";
3395
+ import { z as z9 } from "zod";
3287
3396
  var PACK_HOOK_FILE = ".pnpmfile.mjs";
3288
- var manifest_schema2 = z8.looseObject({ private: z8.boolean().optional() });
3397
+ var manifest_schema2 = z9.looseObject({ private: z9.boolean().optional() });
3289
3398
  var MESSAGES = {
3290
3399
  written: ` \u2714 synced ${PACK_HOOK_FILE} (run \`pnpm install\` to refresh pnpmfileChecksum)`,
3291
3400
  unchanged: ` \u2714 unchanged ${PACK_HOOK_FILE}`,
@@ -3321,15 +3430,15 @@ var yaml_document = { is_mapping_document, parse_yaml, parse_yaml_documents };
3321
3430
 
3322
3431
  // scripts/version/release-age.ts
3323
3432
  import semver from "semver";
3324
- import { z as z9 } from "zod";
3325
- var workspace_age_schema = z9.object({ minimumReleaseAge: z9.number().int().nonnegative() });
3326
- var release_times_schema = z9.record(z9.string(), z9.string());
3433
+ import { z as z10 } from "zod";
3434
+ var workspace_age_schema = z10.object({ minimumReleaseAge: z10.number().int().nonnegative() });
3435
+ var release_times_schema = z10.record(z10.string(), z10.string());
3327
3436
 
3328
3437
  // scripts/safe-chain/project-config.ts
3329
3438
  import { dump } from "js-yaml";
3330
- import { z as z10 } from "zod";
3439
+ import { z as z11 } from "zod";
3331
3440
  var PROJECT_CONFIG_FILE = ".aikido";
3332
- var workspace_schema = z10.object({ minimumReleaseAgeExclude: z10.array(z10.string()).optional() });
3441
+ var workspace_schema = z11.object({ minimumReleaseAgeExclude: z11.array(z11.string()).optional() });
3333
3442
 
3334
3443
  // scripts/config-merge/json-format.ts
3335
3444
  var PRINT_WIDTH = 100;
@@ -3518,8 +3627,9 @@ function patch_deploy_vps_pnpm(content) {
3518
3627
  PNPM_INSTALL_PATTERN,
3519
3628
  (command) => replace_outdated_install(command)
3520
3629
  );
3521
- if (with_pnpm12.includes(VERSION_CHECK_MARKER)) return with_pnpm12;
3522
- if (!OLD_PNPM_CHECK_PATTERN.test(with_pnpm12)) return with_pnpm12;
3630
+ if (with_pnpm12.includes(VERSION_CHECK_MARKER) || !OLD_PNPM_CHECK_PATTERN.test(with_pnpm12)) {
3631
+ return with_pnpm12;
3632
+ }
3523
3633
  return with_pnpm12.replace(OLD_MAJOR_LINE_PATTERN, "").replace(OLD_PNPM_CHECK_PATTERN, (_, indent) => build_version_check(indent));
3524
3634
  }
3525
3635
  var init_logic_deploy_vps = { patch_deploy_vps_pnpm };
@@ -4272,11 +4382,11 @@ var init_logic_json_merge = {
4272
4382
 
4273
4383
  // scripts/init/kit-development-versions.ts
4274
4384
  import { readFileSync as readFileSync3 } from "node:fs";
4275
- import { z as z11 } from "zod";
4385
+ import { z as z12 } from "zod";
4276
4386
  var PACKAGE_JSON = "package.json";
4277
- var kit_manifest_schema = z11.object({
4278
- peerDependencies: z11.record(z11.string(), z11.string()).default({}),
4279
- devDependencies: z11.record(z11.string(), z11.string())
4387
+ var kit_manifest_schema = z12.object({
4388
+ peerDependencies: z12.record(z12.string(), z12.string()).default({}),
4389
+ devDependencies: z12.record(z12.string(), z12.string())
4280
4390
  });
4281
4391
  function read_kit_manifest() {
4282
4392
  const content = readFileSync3(package_path(PACKAGE_JSON), "utf8");
@@ -5133,8 +5243,8 @@ var KEY_LINE_PATTERN = new RegExp(
5133
5243
  import { findNodeAtLocation as findNodeAtLocation2, getNodeValue as getNodeValue2, parseTree as parseTree3 } from "jsonc-parser";
5134
5244
 
5135
5245
  // scripts/init/vscode-tasks.ts
5136
- import { z as z12 } from "zod";
5137
- var task_array_schema = z12.array(json_object_schema);
5246
+ import { z as z13 } from "zod";
5247
+ var task_array_schema = z13.array(json_object_schema);
5138
5248
 
5139
5249
  // scripts/init/init-actions.ts
5140
5250
  var BASIC_PRETTIER_CONFIG = "prettier.config.mjs";
@@ -5461,7 +5571,7 @@ var SKIP_MARKER = "\u2014 skipping";
5461
5571
  var NOT_USED_MARKER = "which this project does not use";
5462
5572
 
5463
5573
  // scripts/gate/project-checks.ts
5464
- import { z as z13 } from "zod";
5574
+ import { z as z14 } from "zod";
5465
5575
  var PACKAGE_JSON2 = "package.json";
5466
5576
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
5467
5577
  "node_modules",
@@ -5496,7 +5606,7 @@ var CSPELL_CONFIGS = [
5496
5606
  ];
5497
5607
  var TYPE_CONFIGS = ["tsconfig.json", "jsconfig.json"];
5498
5608
  var DEPENDENCY_FIELDS = ["dependencies", "devDependencies"];
5499
- var object_schema = z13.record(z13.string(), z13.unknown());
5609
+ var object_schema = z14.record(z14.string(), z14.unknown());
5500
5610
  function project_root(directory) {
5501
5611
  return ancestor_directories.nearest(
5502
5612
  directory,
@@ -5709,7 +5819,7 @@ function is_untested_commit(call) {
5709
5819
  if (!run_tail_rule.is_push_step_call(call)) return false;
5710
5820
  return test_declared_changed.current_verdict() === "required";
5711
5821
  }
5712
- var REASON = '\u26D4 test declared: this commits a runtime code change with no test beside it \u2014 `pnpm josh test:declared` reads the working tree as `required` (a changed file that is neither a test \u2014 `*.test.ts` / `*.e2e.ts` \u2014 nor a mechanically-exempt non-runtime path). `CLAUDE.md` \u2192 Code Change Rules requires a test for ALL code changes: a bug fix gets a regression test, a refactor gets tests pinning behavior first, logic gets a unit test, an observable UI/timing change gets an E2E. Add the declared test and reissue this commit. **If the change genuinely touches no executable runtime path** it is the pre-approved non-runtime exception \u2014 declare it in your Step 0 work summary with the reason it is exempt (non-executable config, a cosmetic asset swap) and reissue; the exception is a presentation, not a stop. Run `pnpm josh test:declared` to see the files it names. The procedure is `prompts/collaboration-workflow/rule-delivery.md` \u2192 "\u914D\u9001\u3055\u308C\u3066\u3044\u308B\u898F\u5247". This fires once per run, so reissuing will not repeat on the call in hand.';
5822
+ var REASON = "\u26D4 test declared: this commits a runtime code change with no test beside it \u2014 `pnpm josh test:declared` reads it `required` and names the files. Tests are required for ALL code changes: add the test and reissue, or, for a change that touches no runtime path, declare the non-runtime exception in your Step 0 work summary and reissue. It fires once per run (`CLAUDE.md` \u2192 Code Change Rules).";
5713
5823
  var ROW19 = { id: "test-declared", is_trigger: is_untested_commit, reason: REASON };
5714
5824
  var test_declared_commit = { REASON, ROW: ROW19 };
5715
5825
 
@@ -5748,7 +5858,7 @@ function writes_third_party(command, resolve_owner = repo_party.current_owner, r
5748
5858
  )
5749
5859
  );
5750
5860
  }
5751
- var THIRD_PARTY_WRITE_REASON = '\u26D4 third-party write: this `gh api` call writes to a repository whose owner is not this session \u2014 a tracker we do not own is Tier C, and every write there (Issue, comment, PR, Discussion, review) needs explicit current-turn user instruction; typing the target, or holding a correct diagnosis, is not that instruction (`CLAUDE.md` \u2192 "Third-party repositories are Tier C", `prompts/collaboration-workflow/upstream-interrupt.md`). Confirm the party with `pnpm josh repo:party <owner/repo>` \u2014 owner equality, computed not judged. A read (`-X GET`, or no field flag) passes untouched; only a write (`-f`/`-F`/`--field`/`--raw-field`/`--input`, or `-X POST|PATCH|PUT|DELETE`) is refused. Record the finding on our own Issue and draft the artifact for a person to post \u2014 do not publish it yourself. **This rule fires on every occurrence, not once per run.**';
5861
+ var THIRD_PARTY_WRITE_REASON = "\u26D4 third-party write: this `gh api` call writes to a repository whose owner is not this session, which is Tier C and needs explicit current-turn user instruction. Check with `pnpm josh repo:party <owner/repo>`, then record the finding on our own Issue and draft the artifact for a person to post. This rule fires on every occurrence (`prompts/collaboration-workflow/upstream-interrupt.md`).";
5752
5862
  var ROW20 = {
5753
5863
  id: "third-party-write",
5754
5864
  is_trigger: bash_triggers.on_bash_command((command) => writes_third_party(command)),
@@ -5949,8 +6059,8 @@ function format_holder(issue, repo, ages) {
5949
6059
  function format_holders(issues, repo, ages) {
5950
6060
  return issues.map((issue) => format_holder(issue, repo, ages)).join(", ");
5951
6061
  }
5952
- function occupied_lanes(read2) {
5953
- return read2.kind === "busy" ? read2.issues.length : 0;
6062
+ function occupied_lanes(read3) {
6063
+ return read3.kind === "busy" ? read3.issues.length : 0;
5954
6064
  }
5955
6065
  function occupancy_message(issues, repo, limit, ages = NO_AGES) {
5956
6066
  return `${String(issues.length)} of ${String(limit)} lanes in use in ${repo}: ${format_holders(issues, repo, ages)}.`;
@@ -5968,18 +6078,18 @@ var BUSY_REASONS = {
5968
6078
  unreadable: unreadable_message,
5969
6079
  truncated: truncated_message
5970
6080
  };
5971
- function busy_reason(read2, repo, limit, ages = NO_AGES) {
5972
- if (read2.kind === "busy") return lanes_full_message(read2.issues, repo, limit, ages);
5973
- if (read2.kind === "idle") return "";
5974
- return BUSY_REASONS[read2.kind](repo);
6081
+ function busy_reason(read3, repo, limit, ages = NO_AGES) {
6082
+ if (read3.kind === "busy") return lanes_full_message(read3.issues, repo, limit, ages);
6083
+ if (read3.kind === "idle") return "";
6084
+ return BUSY_REASONS[read3.kind](repo);
5975
6085
  }
5976
6086
  function is_parked(issue) {
5977
6087
  return has_any_label(issue.labels, PARKED_LABELS);
5978
6088
  }
5979
6089
  function parse_listing(raw, is_capped) {
5980
- const read2 = read_json_listing(raw, open_issue_schema);
5981
- if (read2.kind !== "read") return { kind: "unreadable" };
5982
- const holders = read2.rows.filter((row) => !is_parked(row));
6090
+ const read3 = read_json_listing(raw, open_issue_schema);
6091
+ if (read3.kind !== "read") return { kind: "unreadable" };
6092
+ const holders = read3.rows.filter((row) => !is_parked(row));
5983
6093
  if (holders.length > 0) return { kind: "busy", issues: holders };
5984
6094
  return { kind: is_capped ? "truncated" : "idle" };
5985
6095
  }
@@ -6011,24 +6121,24 @@ import path14 from "node:path";
6011
6121
  import { setTimeout as sleep } from "node:timers/promises";
6012
6122
 
6013
6123
  // scripts/agent/agent-event.ts
6014
- import { z as z15 } from "zod";
6124
+ import { z as z16 } from "zod";
6015
6125
 
6016
6126
  // scripts/agent/claude-result-event.ts
6017
- import { z as z14 } from "zod";
6018
- var RESULT_EVENT_SCHEMA = z14.looseObject({
6019
- type: z14.literal("result"),
6020
- is_error: z14.unknown().optional(),
6021
- subtype: z14.unknown().optional(),
6022
- num_turns: z14.unknown().optional(),
6023
- permission_denials: z14.unknown().optional(),
6024
- result: z14.unknown().optional(),
6025
- session_id: z14.unknown().optional(),
6026
- usage: z14.unknown().optional()
6127
+ import { z as z15 } from "zod";
6128
+ var RESULT_EVENT_SCHEMA = z15.looseObject({
6129
+ type: z15.literal("result"),
6130
+ is_error: z15.unknown().optional(),
6131
+ subtype: z15.unknown().optional(),
6132
+ num_turns: z15.unknown().optional(),
6133
+ permission_denials: z15.unknown().optional(),
6134
+ result: z15.unknown().optional(),
6135
+ session_id: z15.unknown().optional(),
6136
+ usage: z15.unknown().optional()
6027
6137
  });
6028
- var USAGE_SCHEMA = z14.looseObject({
6029
- input_tokens: z14.unknown().optional(),
6030
- cache_read_input_tokens: z14.unknown().optional(),
6031
- output_tokens: z14.unknown().optional()
6138
+ var USAGE_SCHEMA = z15.looseObject({
6139
+ input_tokens: z15.unknown().optional(),
6140
+ cache_read_input_tokens: z15.unknown().optional(),
6141
+ output_tokens: z15.unknown().optional()
6032
6142
  });
6033
6143
  function usable_reason(value) {
6034
6144
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
@@ -6067,31 +6177,31 @@ function decode(value) {
6067
6177
  var claude_result_event = { decode };
6068
6178
 
6069
6179
  // scripts/agent/agent-event.ts
6070
- var TEXT_BLOCK_SCHEMA = z15.looseObject({ type: z15.literal("text"), text: z15.string() });
6071
- var CLAUDE_MESSAGE_SCHEMA = z15.looseObject({ content: z15.array(z15.unknown()).optional() });
6072
- var CLAUDE_INIT_SCHEMA = z15.looseObject({ type: z15.literal("system"), subtype: z15.literal("init") });
6073
- var CLAUDE_ASSISTANT_SCHEMA = z15.looseObject({
6074
- type: z15.literal("assistant"),
6180
+ var TEXT_BLOCK_SCHEMA = z16.looseObject({ type: z16.literal("text"), text: z16.string() });
6181
+ var CLAUDE_MESSAGE_SCHEMA = z16.looseObject({ content: z16.array(z16.unknown()).optional() });
6182
+ var CLAUDE_INIT_SCHEMA = z16.looseObject({ type: z16.literal("system"), subtype: z16.literal("init") });
6183
+ var CLAUDE_ASSISTANT_SCHEMA = z16.looseObject({
6184
+ type: z16.literal("assistant"),
6075
6185
  message: CLAUDE_MESSAGE_SCHEMA
6076
6186
  });
6077
- var USAGE_SCHEMA2 = z15.looseObject({
6078
- input_tokens: z15.number().optional(),
6079
- cached_input_tokens: z15.number().optional(),
6080
- output_tokens: z15.number().optional()
6187
+ var USAGE_SCHEMA2 = z16.looseObject({
6188
+ input_tokens: z16.number().optional(),
6189
+ cached_input_tokens: z16.number().optional(),
6190
+ output_tokens: z16.number().optional()
6081
6191
  });
6082
- var CODEX_START_SCHEMA = z15.looseObject({ type: z15.enum(["thread.started", "turn.started"]) });
6083
- var CODEX_PROGRESS_SCHEMA = z15.looseObject({
6084
- type: z15.enum(["item.started", "item.updated", "item.completed"]),
6085
- item: z15.looseObject({ type: z15.string(), text: z15.string().optional() }).optional()
6192
+ var CODEX_START_SCHEMA = z16.looseObject({ type: z16.enum(["thread.started", "turn.started"]) });
6193
+ var CODEX_PROGRESS_SCHEMA = z16.looseObject({
6194
+ type: z16.enum(["item.started", "item.updated", "item.completed"]),
6195
+ item: z16.looseObject({ type: z16.string(), text: z16.string().optional() }).optional()
6086
6196
  });
6087
- var CODEX_COMPLETE_SCHEMA = z15.looseObject({
6088
- type: z15.literal("turn.completed"),
6197
+ var CODEX_COMPLETE_SCHEMA = z16.looseObject({
6198
+ type: z16.literal("turn.completed"),
6089
6199
  usage: USAGE_SCHEMA2.optional()
6090
6200
  });
6091
- var CODEX_FAILURE_SCHEMA = z15.looseObject({
6092
- type: z15.enum(["turn.failed", "error"]),
6093
- error: z15.looseObject({ message: z15.string().optional() }).optional(),
6094
- message: z15.string().optional()
6201
+ var CODEX_FAILURE_SCHEMA = z16.looseObject({
6202
+ type: z16.enum(["turn.failed", "error"]),
6203
+ error: z16.looseObject({ message: z16.string().optional() }).optional(),
6204
+ message: z16.string().optional()
6095
6205
  });
6096
6206
  var EMPTY_STATE = {
6097
6207
  provider: void 0,
@@ -6169,7 +6279,7 @@ function next_state2(line, state) {
6169
6279
  if (is_header) return EMPTY_STATE;
6170
6280
  return normalized(json_value.parse_or_undefined(line), state);
6171
6281
  }
6172
- function read(transcript) {
6282
+ function read2(transcript) {
6173
6283
  let state = EMPTY_STATE;
6174
6284
  for (const line of transcript.split("\n")) state = next_state2(line, state);
6175
6285
  return state;
@@ -6179,10 +6289,10 @@ function describe(state) {
6179
6289
  const reason = state.reason === void 0 ? "" : ` reason=${state.reason}`;
6180
6290
  return `launch=${state.launch} result=${state.result}${progress}${reason}`;
6181
6291
  }
6182
- var agent_event = { describe, read };
6292
+ var agent_event = { describe, read: read2 };
6183
6293
 
6184
6294
  // scripts/issue/issue-state.ts
6185
- import { z as z16 } from "zod";
6295
+ import { z as z17 } from "zod";
6186
6296
  var NO_LABELS = "(none)";
6187
6297
  var LABEL_SEPARATOR = ", ";
6188
6298
  var STATE_LABEL = "state: ";
@@ -6192,9 +6302,9 @@ var YES = "yes";
6192
6302
  var NO = "no";
6193
6303
  var ISSUE_LABEL = "issue: ";
6194
6304
  var HUMAN_REVIEW_LABELS = /* @__PURE__ */ new Set([NEEDS_HUMAN_REVIEW_LABEL]);
6195
- var label_schema = z16.looseObject({ name: z16.string() });
6196
- var issue_state_schema = z16.looseObject({
6197
- state: z16.string(),
6305
+ var label_schema = z17.looseObject({ name: z17.string() });
6306
+ var issue_state_schema = z17.looseObject({
6307
+ state: z17.string(),
6198
6308
  labels: label_schema.array().optional()
6199
6309
  });
6200
6310
  function parse_issue_state(json) {
@@ -6462,7 +6572,7 @@ var lane_report = {
6462
6572
  };
6463
6573
 
6464
6574
  // scripts/run/run-preflight.ts
6465
- import { z as z17 } from "zod";
6575
+ import { z as z18 } from "zod";
6466
6576
  var CLEAN_VERDICT = "clean";
6467
6577
  var RECLAIM_VERDICT = "reclaim";
6468
6578
  var RESUME_VERDICT = "resume";
@@ -6472,7 +6582,7 @@ var OPEN_PR = "open";
6472
6582
  var MERGED_PR = "merged";
6473
6583
  var CLOSED_PR = "closed";
6474
6584
  var NO_CHILD_WORK = { branch_name: void 0, pr_state: NO_PR };
6475
- var pr_info_schema = z17.object({ state: z17.string().optional() });
6585
+ var pr_info_schema = z18.object({ state: z18.string().optional() });
6476
6586
  var GH_PR_STATES = /* @__PURE__ */ new Map([
6477
6587
  ["CLOSED", CLOSED_PR],
6478
6588
  ["MERGED", MERGED_PR],
@@ -6577,8 +6687,8 @@ async function read_pr_of_branch(branch_name) {
6577
6687
  return { branch_name, pr_state: await read_pr_state(branch_name) };
6578
6688
  }
6579
6689
  function pick_child_state(reads) {
6580
- const decided = reads.find((read2) => is_pr_decided(read2.pr_state));
6581
- const open = reads.find((read2) => is_pr_open(read2.pr_state));
6690
+ const decided = reads.find((read3) => is_pr_decided(read3.pr_state));
6691
+ const open = reads.find((read3) => is_pr_open(read3.pr_state));
6582
6692
  return decided ?? open ?? { branch_name: reads[0]?.branch_name, pr_state: NO_PR };
6583
6693
  }
6584
6694
  var ISSUE_BRANCH_SUFFIX = "-*";
@@ -6617,8 +6727,7 @@ async function is_held_dirty(name, lane) {
6617
6727
  }
6618
6728
  async function read_candidate(name, references, context) {
6619
6729
  const state = await read_pr_of_branch(name);
6620
- if (state.pr_state !== NO_PR) return state;
6621
- if (await has_commits_beyond(context.base, references)) return state;
6730
+ if (state.pr_state !== NO_PR || await has_commits_beyond(context.base, references)) return state;
6622
6731
  return await is_held_dirty(name, context.lane) ? state : void 0;
6623
6732
  }
6624
6733
  async function read_child_state(issue) {
@@ -6633,7 +6742,7 @@ async function read_child_state(issue) {
6633
6742
  async ([name, references]) => await read_candidate(name, references, { base, lane })
6634
6743
  )
6635
6744
  );
6636
- return pick_child_state(reads.filter((read2) => read2 !== void 0));
6745
+ return pick_child_state(reads.filter((read3) => read3 !== void 0));
6637
6746
  }
6638
6747
  async function check3(issue) {
6639
6748
  require_issue_number(issue);
@@ -6680,10 +6789,10 @@ async function to_child(issue) {
6680
6789
  return { issue: number, title: issue.title, labels: to_labels(issue), pr_state: child.pr_state };
6681
6790
  }
6682
6791
  async function read_children(repo) {
6683
- const read2 = await epic_busy.read_repository(repo);
6684
- if (read2.kind === "idle") return [];
6685
- if (read2.kind !== "busy") return void 0;
6686
- return await Promise.all(read2.issues.map(async (issue) => await to_child(issue)));
6792
+ const read3 = await epic_busy.read_repository(repo);
6793
+ if (read3.kind === "idle") return [];
6794
+ if (read3.kind !== "busy") return void 0;
6795
+ return await Promise.all(read3.issues.map(async (issue) => await to_child(issue)));
6687
6796
  }
6688
6797
  async function read_lanes() {
6689
6798
  const lanes = await lane_registry.list_lanes();
@@ -6697,11 +6806,11 @@ function read_record_age(paths, now_ms) {
6697
6806
  if (times.length === 0) return void 0;
6698
6807
  return now_ms - Math.max(...times);
6699
6808
  }
6700
- function is_hold_present(read2) {
6701
- return read2.kind === "held" || read2.kind === "stale";
6809
+ function is_hold_present(read3) {
6810
+ return read3.kind === "held" || read3.kind === "stale";
6702
6811
  }
6703
- function is_carry_present(read2) {
6704
- return read2.kind === "carried";
6812
+ function is_carry_present(read3) {
6813
+ return read3.kind === "carried";
6705
6814
  }
6706
6815
  function hold_present(worktree) {
6707
6816
  return worktree !== void 0 && is_hold_present(run_hold.read_hold(run_hold.hold_path(worktree)));
@@ -6745,7 +6854,7 @@ var run_progress_read = {
6745
6854
  };
6746
6855
 
6747
6856
  // scripts/run/run-watcher-hook.ts
6748
- import { z as z18 } from "zod";
6857
+ import { z as z19 } from "zod";
6749
6858
 
6750
6859
  // scripts/run/run-watcher-guard.ts
6751
6860
  var WATCHER_TICK_MS = 3e4;
@@ -6760,8 +6869,9 @@ async function owes_watcher() {
6760
6869
  return await lane_registry.has_lanes_in_flight();
6761
6870
  }
6762
6871
  async function check4(life_target) {
6763
- if (!await owes_watcher()) return OK_RESULT;
6764
- if (run_progress_clock.is_life_fresh(life_target, STALE_THRESHOLD_MS)) return OK_RESULT;
6872
+ if (!await owes_watcher() || run_progress_clock.is_life_fresh(life_target, STALE_THRESHOLD_MS)) {
6873
+ return OK_RESULT;
6874
+ }
6765
6875
  return { kind: "stale", note: STALE_NOTE };
6766
6876
  }
6767
6877
  var run_watcher_guard = {
@@ -6773,7 +6883,7 @@ var run_watcher_guard = {
6773
6883
  // scripts/run/run-watcher-hook.ts
6774
6884
  var SWITCH_ENV_KEY2 = "JOSH_WATCHER_GUARD";
6775
6885
  var STAMP_PREFIX2 = "josh-watcher-guard-";
6776
- var payload_schema = z18.object({ transcript_path: z18.string().min(1) });
6886
+ var payload_schema = z19.object({ transcript_path: z19.string().min(1) });
6777
6887
  function transcript_of(raw_payload) {
6778
6888
  return json_value.parse_with(raw_payload, payload_schema)?.transcript_path;
6779
6889
  }
@@ -6810,7 +6920,7 @@ function current_state3() {
6810
6920
  };
6811
6921
  }
6812
6922
  var THRESHOLD_TEXT2 = `${cost_format.format_tokens(CONTEXT_CUT_THRESHOLD)}-token`;
6813
- var PARENT_CUT_REASON = `\u26D4 backlogrun parent hand-off: this session drives a \`backlogrun\` (it owns the live carry record) and its recent-context cost has crossed the shared ${THRESHOLD_TEXT2} threshold between merges, so every later request re-reads the accumulated context (joshuafolkken/kit#2947). Take the hand-off now rather than at the next merge: follow \`.claude/skills/workflow-commands/backlogrun-progress.md\` \u2192 "The hand-off" from its \`over\` branch \u2014 open no new lane and take no new child, read \`pnpm josh lane:list\`, make sure every lane in flight records an output path, then run \`pnpm josh run:carry --cut --owner "$PPID"\` and, unless it answers \`capped\`, \`pnpm josh run:wake\`, post the progress comment and end the turn. A lane nobody can poll means the cut does not happen; carry on and this asks again later. Reissue this call when the hand-off is done or cannot be taken \u2014 the guard stays quiet for a while after it refuses, so the hand-off calls themselves are not refused.`;
6923
+ var PARENT_CUT_REASON = `\u26D4 backlogrun parent hand-off: this session drives a \`backlogrun\` and its recent-context cost crossed the ${THRESHOLD_TEXT2} threshold between merges. Take the hand-off now: open no new lane, follow \`backlogrun-progress.md\` \u2192 "The hand-off" (\`pnpm josh run:carry --cut --owner "$PPID"\`, then \`pnpm josh run:wake\` unless \`capped\`) and end the turn. Reissue this call once done or if it cannot be taken.`;
6814
6924
  async function is_due3(state) {
6815
6925
  const carry = await state.carry();
6816
6926
  if (carry === void 0 || run_headless.is_cut_capped(carry)) return false;
@@ -6882,26 +6992,22 @@ function notice(raw_payload, root = process.cwd()) {
6882
6992
  const transcript = is_candidate2(raw_payload, root);
6883
6993
  if (transcript === void 0) return void 0;
6884
6994
  const target = STAMP.path(transcript);
6885
- if (STAMP.last_ms(target) !== hook_decision.NEVER_MS) return void 0;
6886
- if (!STAMP.record(target, Date.now())) return void 0;
6995
+ if (STAMP.last_ms(target) !== hook_decision.NEVER_MS || !STAMP.record(target, Date.now())) {
6996
+ return void 0;
6997
+ }
6887
6998
  return NOTICE;
6888
6999
  }
6889
7000
  var step_zero_notice = { NOTICE, notice, stamp_path: STAMP.path };
6890
7001
 
6891
7002
  // scripts/hooks/pretool-guard.ts
6892
- function combine_outcomes(batch, extra_reason) {
6893
- if (batch.reason !== void 0) return batch;
6894
- if (extra_reason !== void 0) {
6895
- return { reason: extra_reason, notice: void 0, fault: void 0 };
6896
- }
6897
- return batch;
6898
- }
7003
+ var CLEAR = { reason: void 0, notice: void 0, fault: void 0 };
6899
7004
  function is_clear(outcome) {
6900
7005
  return outcome.reason === void 0 && outcome.notice === void 0 && outcome.fault === void 0;
6901
7006
  }
6902
- function with_duplicate_notice(combined, notice2) {
6903
- if (notice2 === void 0 || !is_clear(combined)) return combined;
6904
- return { reason: void 0, notice: notice2, fault: void 0 };
7007
+ function combine_outcomes(outcomes, rule_reason) {
7008
+ const reason = outcomes.find((outcome) => outcome.reason !== void 0)?.reason ?? rule_reason;
7009
+ if (reason !== void 0) return { reason, notice: void 0, fault: void 0 };
7010
+ return outcomes.find((outcome) => !is_clear(outcome)) ?? CLEAR;
6905
7011
  }
6906
7012
  function with_step_zero_notice(outcome, raw_payload) {
6907
7013
  if (!is_clear(outcome)) return outcome;
@@ -6909,13 +7015,12 @@ function with_step_zero_notice(outcome, raw_payload) {
6909
7015
  return notice2 === void 0 ? outcome : { reason: void 0, notice: notice2, fault: void 0 };
6910
7016
  }
6911
7017
  function guard_outcome(raw_payload) {
6912
- const batch = batch_outcome(raw_payload);
6913
- const investigation = investigation_refusal(raw_payload);
6914
- const duplicate = duplicate_read_outcome(raw_payload);
6915
- const rule = delivered_rules.delivery(raw_payload);
6916
- const duplicate_refusal = duplicate.reason;
6917
- const combined = combine_outcomes(batch, investigation ?? duplicate_refusal ?? rule);
6918
- return with_duplicate_notice(combined, duplicate.notice);
7018
+ const outcomes = [
7019
+ batch_outcome(raw_payload),
7020
+ investigation_outcome(raw_payload),
7021
+ duplicate_read_outcome(raw_payload)
7022
+ ];
7023
+ return combine_outcomes(outcomes, delivered_rules.delivery(raw_payload));
6919
7024
  }
6920
7025
  function pretool_outcome(raw_payload) {
6921
7026
  return with_step_zero_notice(guard_outcome(raw_payload), raw_payload);
@@ -6968,7 +7073,7 @@ function pipefail_rewrite(raw_payload) {
6968
7073
  }
6969
7074
  function pipefail_envelope(outcome, input) {
6970
7075
  if (input === void 0 || outcome.reason !== void 0) return void 0;
6971
- return hook_decision.rewrite_envelope(
7076
+ return hook_decision.flow_rewrite_envelope(
6972
7077
  input,
6973
7078
  rewrite_context(piped_verification.PIPEFAIL_NOTE, outcome)
6974
7079
  );