@joshuafolkken/kit 1.872.0 → 1.879.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 (232) hide show
  1. package/.github/workflows/publish.yml +39 -2
  2. package/README.md +3 -3
  3. package/package.json +6 -2
  4. package/scripts/init/distributed-paths.ts +14 -11
  5. package/scripts/init/hook-command-bootstrap.ts +51 -0
  6. package/scripts/init/hook-command-rewrite.ts +9 -1
  7. package/scripts/init/init-logic.ts +2 -8
  8. package/templates/workflows/ci.yml +6 -2
  9. package/dist/hooks/chunk-25JL6FMS.js +0 -4970
  10. package/dist/hooks/chunk-2ANGT3XM.js +0 -375
  11. package/dist/hooks/chunk-2EI5GBSW.js +0 -42
  12. package/dist/hooks/chunk-2OT5XE2N.js +0 -5014
  13. package/dist/hooks/chunk-33ELOKEV.js +0 -910
  14. package/dist/hooks/chunk-33YRTKM7.js +0 -230
  15. package/dist/hooks/chunk-36JQU5NN.js +0 -77
  16. package/dist/hooks/chunk-3EJBTWVS.js +0 -1769
  17. package/dist/hooks/chunk-3FK5SFAC.js +0 -432
  18. package/dist/hooks/chunk-3PWPYDD2.js +0 -240
  19. package/dist/hooks/chunk-3TQU7TDH.js +0 -375
  20. package/dist/hooks/chunk-3TVLSFZ6.js +0 -432
  21. package/dist/hooks/chunk-3VXYCB4E.js +0 -77
  22. package/dist/hooks/chunk-44M6JXGV.js +0 -3398
  23. package/dist/hooks/chunk-46EEEHC3.js +0 -240
  24. package/dist/hooks/chunk-4A7UVP2P.js +0 -364
  25. package/dist/hooks/chunk-4LFQEZS4.js +0 -137
  26. package/dist/hooks/chunk-4OCX3V2T.js +0 -469
  27. package/dist/hooks/chunk-4OHA5DE7.js +0 -49
  28. package/dist/hooks/chunk-4PMDW3YD.js +0 -68
  29. package/dist/hooks/chunk-4QHBNAI4.js +0 -68
  30. package/dist/hooks/chunk-4SSB3FEE.js +0 -3273
  31. package/dist/hooks/chunk-5A2WQ2CQ.js +0 -5390
  32. package/dist/hooks/chunk-5EVA2JQB.js +0 -5390
  33. package/dist/hooks/chunk-5P7VF5VE.js +0 -3230
  34. package/dist/hooks/chunk-5QWJGGZP.js +0 -49
  35. package/dist/hooks/chunk-5U5EZYG6.js +0 -77
  36. package/dist/hooks/chunk-5VUUF3ZV.js +0 -240
  37. package/dist/hooks/chunk-5WA7UBT3.js +0 -374
  38. package/dist/hooks/chunk-6EBGRQ3Q.js +0 -301
  39. package/dist/hooks/chunk-6TVHWMD3.js +0 -432
  40. package/dist/hooks/chunk-6VAGLEMB.js +0 -68
  41. package/dist/hooks/chunk-75BTXKFJ.js +0 -3424
  42. package/dist/hooks/chunk-75RHN67S.js +0 -137
  43. package/dist/hooks/chunk-76KC3BDD.js +0 -3414
  44. package/dist/hooks/chunk-76SLS7CZ.js +0 -898
  45. package/dist/hooks/chunk-7DHZ4DWT.js +0 -5015
  46. package/dist/hooks/chunk-7INSJDGE.js +0 -458
  47. package/dist/hooks/chunk-7VJ4FTDX.js +0 -268
  48. package/dist/hooks/chunk-7XCYNHQK.js +0 -77
  49. package/dist/hooks/chunk-A5YJ3H7E.js +0 -375
  50. package/dist/hooks/chunk-AAEXXUUL.js +0 -268
  51. package/dist/hooks/chunk-ABVEVV2M.js +0 -432
  52. package/dist/hooks/chunk-ACN3YXFL.js +0 -68
  53. package/dist/hooks/chunk-AD3XE3QZ.js +0 -397
  54. package/dist/hooks/chunk-AENFZPZH.js +0 -230
  55. package/dist/hooks/chunk-AFKRFQ6W.js +0 -68
  56. package/dist/hooks/chunk-AHD5IPWI.js +0 -375
  57. package/dist/hooks/chunk-AMK5JGV7.js +0 -77
  58. package/dist/hooks/chunk-ARN7NCQI.js +0 -425
  59. package/dist/hooks/chunk-AUFY653N.js +0 -3425
  60. package/dist/hooks/chunk-AXQBNFEF.js +0 -375
  61. package/dist/hooks/chunk-AYC7R6U4.js +0 -3316
  62. package/dist/hooks/chunk-AYEA2FJ7.js +0 -5015
  63. package/dist/hooks/chunk-B4HPNY2I.js +0 -375
  64. package/dist/hooks/chunk-B57S3VWT.js +0 -922
  65. package/dist/hooks/chunk-BINZ3PLI.js +0 -364
  66. package/dist/hooks/chunk-BODUSXKK.js +0 -410
  67. package/dist/hooks/chunk-BQBAT2YK.js +0 -375
  68. package/dist/hooks/chunk-BUJEBW75.js +0 -391
  69. package/dist/hooks/chunk-BVNWORKA.js +0 -72
  70. package/dist/hooks/chunk-BWI47LQS.js +0 -5205
  71. package/dist/hooks/chunk-C25HRMJX.js +0 -137
  72. package/dist/hooks/chunk-CBFZNC74.js +0 -375
  73. package/dist/hooks/chunk-CECYXS4Y.js +0 -3424
  74. package/dist/hooks/chunk-CELGUP5F.js +0 -49
  75. package/dist/hooks/chunk-CMHANU3F.js +0 -3442
  76. package/dist/hooks/chunk-CMN372LI.js +0 -68
  77. package/dist/hooks/chunk-CPGZLMMK.js +0 -3299
  78. package/dist/hooks/chunk-CTQKXDC7.js +0 -268
  79. package/dist/hooks/chunk-CYAT4RIT.js +0 -68
  80. package/dist/hooks/chunk-CYTWYCH2.js +0 -5014
  81. package/dist/hooks/chunk-CZSOLRBA.js +0 -77
  82. package/dist/hooks/chunk-D2PIDYJR.js +0 -3438
  83. package/dist/hooks/chunk-DE57HI6T.js +0 -137
  84. package/dist/hooks/chunk-DGXDGAPG.js +0 -5306
  85. package/dist/hooks/chunk-DNYRSYQG.js +0 -5014
  86. package/dist/hooks/chunk-DTAXH2A2.js +0 -240
  87. package/dist/hooks/chunk-E2IZUWI5.js +0 -375
  88. package/dist/hooks/chunk-E4L2K2BL.js +0 -397
  89. package/dist/hooks/chunk-ECXVCCVD.js +0 -3079
  90. package/dist/hooks/chunk-EPHO443D.js +0 -68
  91. package/dist/hooks/chunk-EUFK2BI2.js +0 -375
  92. package/dist/hooks/chunk-EYCD5ZAV.js +0 -137
  93. package/dist/hooks/chunk-F2SDYJU4.js +0 -240
  94. package/dist/hooks/chunk-F4B5D5TF.js +0 -68
  95. package/dist/hooks/chunk-FGEO3NDX.js +0 -432
  96. package/dist/hooks/chunk-FJYI52RE.js +0 -137
  97. package/dist/hooks/chunk-FYTZHIY4.js +0 -3085
  98. package/dist/hooks/chunk-G7JL2A5K.js +0 -375
  99. package/dist/hooks/chunk-GA3ITDBW.js +0 -42
  100. package/dist/hooks/chunk-GDR4H4SV.js +0 -469
  101. package/dist/hooks/chunk-GFKDXPSM.js +0 -2855
  102. package/dist/hooks/chunk-GIMRTKFU.js +0 -566
  103. package/dist/hooks/chunk-GOPHRLGO.js +0 -240
  104. package/dist/hooks/chunk-GX4HO7SJ.js +0 -230
  105. package/dist/hooks/chunk-GZ6FUVIK.js +0 -566
  106. package/dist/hooks/chunk-H5HPY5M5.js +0 -2869
  107. package/dist/hooks/chunk-HCF3OGEE.js +0 -1829
  108. package/dist/hooks/chunk-HDEXAOUM.js +0 -469
  109. package/dist/hooks/chunk-HM2TY3TN.js +0 -469
  110. package/dist/hooks/chunk-HOA7NXA2.js +0 -55
  111. package/dist/hooks/chunk-HOFEWH6H.js +0 -240
  112. package/dist/hooks/chunk-HUUI6S5J.js +0 -2669
  113. package/dist/hooks/chunk-I4D3NVUD.js +0 -469
  114. package/dist/hooks/chunk-I4Q6WAJE.js +0 -68
  115. package/dist/hooks/chunk-IB6N4CR2.js +0 -432
  116. package/dist/hooks/chunk-IFLDYJMD.js +0 -469
  117. package/dist/hooks/chunk-IGU466YD.js +0 -77
  118. package/dist/hooks/chunk-IJCQUHKY.js +0 -375
  119. package/dist/hooks/chunk-IKUMBDJB.js +0 -230
  120. package/dist/hooks/chunk-IM3BDIBJ.js +0 -5005
  121. package/dist/hooks/chunk-IMYMAODK.js +0 -3431
  122. package/dist/hooks/chunk-IN2ZVGXA.js +0 -5005
  123. package/dist/hooks/chunk-ITVJ2MKQ.js +0 -18
  124. package/dist/hooks/chunk-J5RIJYTM.js +0 -2669
  125. package/dist/hooks/chunk-JFLMH4K5.js +0 -3460
  126. package/dist/hooks/chunk-JR25IFML.js +0 -77
  127. package/dist/hooks/chunk-JT5AUXFI.js +0 -432
  128. package/dist/hooks/chunk-JY5YVN6N.js +0 -3234
  129. package/dist/hooks/chunk-K5Q4ETVI.js +0 -391
  130. package/dist/hooks/chunk-K67V2F24.js +0 -137
  131. package/dist/hooks/chunk-KATZESEC.js +0 -3273
  132. package/dist/hooks/chunk-KFH4H7W7.js +0 -469
  133. package/dist/hooks/chunk-KMUEI6RX.js +0 -910
  134. package/dist/hooks/chunk-KMV4OY77.js +0 -3438
  135. package/dist/hooks/chunk-KQAZNWEY.js +0 -137
  136. package/dist/hooks/chunk-KZZR6JER.js +0 -5015
  137. package/dist/hooks/chunk-LAR6DREP.js +0 -77
  138. package/dist/hooks/chunk-LNCJWEZD.js +0 -5015
  139. package/dist/hooks/chunk-LRYD2GLM.js +0 -6
  140. package/dist/hooks/chunk-LXBKYHDV.js +0 -77
  141. package/dist/hooks/chunk-LYLKIUNE.js +0 -5024
  142. package/dist/hooks/chunk-LZKCFOVC.js +0 -432
  143. package/dist/hooks/chunk-M4FCIYRI.js +0 -77
  144. package/dist/hooks/chunk-M5M7327Q.js +0 -3425
  145. package/dist/hooks/chunk-MF72MHSX.js +0 -3455
  146. package/dist/hooks/chunk-MK56LKDO.js +0 -397
  147. package/dist/hooks/chunk-N3MJADY7.js +0 -240
  148. package/dist/hooks/chunk-NFQSUDBN.js +0 -458
  149. package/dist/hooks/chunk-NHREKY45.js +0 -137
  150. package/dist/hooks/chunk-NP5TTAAW.js +0 -375
  151. package/dist/hooks/chunk-NQTLTKTJ.js +0 -2993
  152. package/dist/hooks/chunk-NVRCZPBZ.js +0 -391
  153. package/dist/hooks/chunk-O46HMMLJ.js +0 -469
  154. package/dist/hooks/chunk-OFJGH2L4.js +0 -240
  155. package/dist/hooks/chunk-OLFMCZH5.js +0 -240
  156. package/dist/hooks/chunk-OMCZS46I.js +0 -49
  157. package/dist/hooks/chunk-OQZEZW5G.js +0 -469
  158. package/dist/hooks/chunk-P2MDAT2Y.js +0 -1897
  159. package/dist/hooks/chunk-P3M2JJIF.js +0 -68
  160. package/dist/hooks/chunk-PDVHQITQ.js +0 -3453
  161. package/dist/hooks/chunk-PNT2AMDH.js +0 -391
  162. package/dist/hooks/chunk-PR2LVSY6.js +0 -3445
  163. package/dist/hooks/chunk-PRR5R4JK.js +0 -137
  164. package/dist/hooks/chunk-PXT44QVN.js +0 -566
  165. package/dist/hooks/chunk-PZLWJLAP.js +0 -469
  166. package/dist/hooks/chunk-Q2FEAZJ2.js +0 -77
  167. package/dist/hooks/chunk-Q32WETDO.js +0 -3265
  168. package/dist/hooks/chunk-Q45DUBLE.js +0 -3600
  169. package/dist/hooks/chunk-Q7NZSWQ5.js +0 -240
  170. package/dist/hooks/chunk-QH2JNAMK.js +0 -3085
  171. package/dist/hooks/chunk-QPU5H2OS.js +0 -68
  172. package/dist/hooks/chunk-QRAHZ5TO.js +0 -1829
  173. package/dist/hooks/chunk-QUZ5HPIE.js +0 -469
  174. package/dist/hooks/chunk-QVCRG34O.js +0 -2893
  175. package/dist/hooks/chunk-R3MY4E3S.js +0 -1675
  176. package/dist/hooks/chunk-R6TXJJXP.js +0 -77
  177. package/dist/hooks/chunk-REJU6VT7.js +0 -1829
  178. package/dist/hooks/chunk-REQAQLRC.js +0 -2993
  179. package/dist/hooks/chunk-RFCXPAHH.js +0 -391
  180. package/dist/hooks/chunk-RHI6Y22X.js +0 -137
  181. package/dist/hooks/chunk-RKZVOBBS.js +0 -469
  182. package/dist/hooks/chunk-RL57TT2Y.js +0 -566
  183. package/dist/hooks/chunk-RORDOD4S.js +0 -3321
  184. package/dist/hooks/chunk-RZKYW5P2.js +0 -77
  185. package/dist/hooks/chunk-SJPJ6VSI.js +0 -391
  186. package/dist/hooks/chunk-SKSIJQQQ.js +0 -432
  187. package/dist/hooks/chunk-SSQDZKXG.js +0 -268
  188. package/dist/hooks/chunk-SYHFKE4C.js +0 -77
  189. package/dist/hooks/chunk-TCGNI2ZY.js +0 -42
  190. package/dist/hooks/chunk-TG3WXJF5.js +0 -137
  191. package/dist/hooks/chunk-TH7IJAY5.js +0 -77
  192. package/dist/hooks/chunk-TLZ27PCY.js +0 -240
  193. package/dist/hooks/chunk-U652FXTN.js +0 -566
  194. package/dist/hooks/chunk-UEK47YUF.js +0 -268
  195. package/dist/hooks/chunk-UIO45H3O.js +0 -432
  196. package/dist/hooks/chunk-UNLZEEVH.js +0 -5005
  197. package/dist/hooks/chunk-UNRVF3SZ.js +0 -432
  198. package/dist/hooks/chunk-USLFAFOI.js +0 -3453
  199. package/dist/hooks/chunk-VAEELMLL.js +0 -442
  200. package/dist/hooks/chunk-VALSWLDQ.js +0 -3453
  201. package/dist/hooks/chunk-VC5JEPNY.js +0 -3398
  202. package/dist/hooks/chunk-VEZVK6C7.js +0 -230
  203. package/dist/hooks/chunk-VK5JKFAR.js +0 -49
  204. package/dist/hooks/chunk-VNF2KP7Z.js +0 -469
  205. package/dist/hooks/chunk-VWWCUMW6.js +0 -2669
  206. package/dist/hooks/chunk-W4JARUQV.js +0 -137
  207. package/dist/hooks/chunk-WEOBMNJG.js +0 -2993
  208. package/dist/hooks/chunk-WKJU3J5Y.js +0 -5390
  209. package/dist/hooks/chunk-WMN4TSVF.js +0 -3299
  210. package/dist/hooks/chunk-WNON54PT.js +0 -3469
  211. package/dist/hooks/chunk-WOVKBPPX.js +0 -137
  212. package/dist/hooks/chunk-WRVFORYJ.js +0 -432
  213. package/dist/hooks/chunk-WUBVYGQZ.js +0 -240
  214. package/dist/hooks/chunk-WWULTUOM.js +0 -68
  215. package/dist/hooks/chunk-WXHKKB45.js +0 -4962
  216. package/dist/hooks/chunk-XAI3YTHJ.js +0 -469
  217. package/dist/hooks/chunk-XFZH6JZJ.js +0 -77
  218. package/dist/hooks/chunk-XGPWX6IA.js +0 -137
  219. package/dist/hooks/chunk-XIXXIVGY.js +0 -566
  220. package/dist/hooks/chunk-XKDOYJGA.js +0 -922
  221. package/dist/hooks/chunk-YALTVBHM.js +0 -432
  222. package/dist/hooks/chunk-YEBEPBFK.js +0 -5014
  223. package/dist/hooks/chunk-YI2BQ3DK.js +0 -3441
  224. package/dist/hooks/chunk-YVNBCFGG.js +0 -1829
  225. package/dist/hooks/chunk-YXBN3RGG.js +0 -432
  226. package/dist/hooks/chunk-Z2OOJSW3.js +0 -3163
  227. package/dist/hooks/chunk-ZA3HVBOY.js +0 -2644
  228. package/dist/hooks/chunk-ZKID46U3.js +0 -137
  229. package/dist/hooks/chunk-ZPMU2JV6.js +0 -49
  230. package/dist/hooks/chunk-ZRKY4W7O.js +0 -432
  231. package/dist/hooks/chunk-ZTMRB7LJ.js +0 -268
  232. package/dist/hooks/chunk-ZVPPBQ3L.js +0 -3413
@@ -1,1897 +0,0 @@
1
- import {
2
- git_utilities,
3
- lane_park,
4
- run_cut,
5
- shell_segments
6
- } from "./chunk-KMUEI6RX.js";
7
- import {
8
- batch_outcome
9
- } from "./chunk-AMK5JGV7.js";
10
- import {
11
- investigation_reads,
12
- investigation_refusal
13
- } from "./chunk-E4L2K2BL.js";
14
- import {
15
- lane_guard_policy,
16
- time_batch_guard
17
- } from "./chunk-VAEELMLL.js";
18
- import {
19
- hook_decision,
20
- lane_child_marker
21
- } from "./chunk-AENFZPZH.js";
22
- import {
23
- cost_blocks,
24
- json_value,
25
- lane_paths,
26
- stamp_file,
27
- time_density_hook,
28
- time_shell,
29
- time_transcript_line
30
- } from "./chunk-Q32WETDO.js";
31
-
32
- // scripts/hooks/pretool-guard.ts
33
- import { text } from "node:stream/consumers";
34
- import { fileURLToPath } from "node:url";
35
-
36
- // scripts/rules/bash-triggers.ts
37
- function on_bash_command(is_match) {
38
- return function is_trigger(call) {
39
- if (call.name !== cost_blocks.BASH_TOOL) return false;
40
- return is_match(time_shell.bash_command(call.input));
41
- };
42
- }
43
- var ISSUE_CREATE_COMMAND = /\bgh\s+(?:\S+\s+)*?issue\s+create\b/u;
44
- var ISSUES_ENDPOINT = /repos\/[^\s'"]*\/issues(?=$|["'\s])/u;
45
- var TITLE_FIELD = /(?:-f|-F|--field|--raw-field)\s*'?title=/u;
46
- function is_issue_filing(command) {
47
- if (ISSUE_CREATE_COMMAND.test(command)) return true;
48
- return ISSUES_ENDPOINT.test(command) && TITLE_FIELD.test(command);
49
- }
50
- var bash_triggers = { is_issue_filing, on_bash_command };
51
-
52
- // scripts/run/run-progress.ts
53
- var DEFAULT_INTERVAL_MINUTES = 20;
54
- var INTERVAL_KEY = "JOSH_PROGRESS_INTERVAL_MINUTES";
55
- var MS_PER_MINUTE = 6e4;
56
- var MS_PER_SECOND = 1e3;
57
- var DEFAULT_INTERVAL_MS = DEFAULT_INTERVAL_MINUTES * MS_PER_MINUTE;
58
- var NO_LANES = "none";
59
- var NO_CHILD_YET = "no in-progress child yet";
60
- var NO_NEXT = "none";
61
- var QUIET_MARKER = "\u23F3";
62
- var STAMP_END = 16;
63
- var PAD_WIDTH = 2;
64
- var MINUTES_PER_HOUR = 60;
65
- function to_minutes(elapsed_ms) {
66
- return Math.floor(elapsed_ms / MS_PER_MINUTE);
67
- }
68
- function format_minutes(elapsed_ms) {
69
- return `${String(to_minutes(elapsed_ms))}m`;
70
- }
71
- function pad(value) {
72
- return String(value).padStart(PAD_WIDTH, "0");
73
- }
74
- function offset_minutes(date) {
75
- return Math.round(-date.getTimezoneOffset());
76
- }
77
- function format_offset(total_minutes) {
78
- const sign = total_minutes < 0 ? "-" : "+";
79
- const absolute = Math.abs(total_minutes);
80
- return `${sign}${pad(Math.floor(absolute / MINUTES_PER_HOUR))}:${pad(absolute % MINUTES_PER_HOUR)}`;
81
- }
82
- function format_observed_at(now_ms) {
83
- const minutes = offset_minutes(new Date(now_ms));
84
- const local = new Date(now_ms + minutes * MS_PER_MINUTE);
85
- const day = `${String(local.getUTCFullYear())}-${pad(local.getUTCMonth() + 1)}-${pad(local.getUTCDate())}`;
86
- const clock = `${pad(local.getUTCHours())}:${pad(local.getUTCMinutes())}${format_offset(minutes)}`;
87
- return `${day} ${clock} / ${new Date(now_ms).toISOString().slice(0, STAMP_END)}Z`;
88
- }
89
- function observation_key(observations) {
90
- return JSON.stringify({ children: observations.children, lanes: observations.lanes });
91
- }
92
- function next_state(previous, key, now_ms) {
93
- if (previous?.key === key) return previous;
94
- return { key, unchanged_since_ms: now_ms };
95
- }
96
- function format_child(child) {
97
- const labels = child.labels.length > 0 ? child.labels.join(",") : "(no labels)";
98
- return `#${child.issue} ${labels} PR:${child.pr_state}`;
99
- }
100
- function format_children(children) {
101
- if (children.length === 0) return NO_CHILD_YET;
102
- return children.map((child) => format_child(child)).join(" \xB7 ");
103
- }
104
- function format_lanes(lanes) {
105
- if (lanes.length === 0) return NO_LANES;
106
- return lanes.map((lane) => `${lane.issue}:${lane.state}`).join(" ");
107
- }
108
- function format_record(record_age_ms) {
109
- return record_age_ms === void 0 ? "unread" : `+${format_minutes(record_age_ms)}`;
110
- }
111
- function format_next_report(now_ms, interval_ms) {
112
- return format_observed_at(now_ms + interval_ms);
113
- }
114
- function format_next_line(next) {
115
- return `next ${next ?? NO_NEXT}`;
116
- }
117
- function format_report(fields) {
118
- return [
119
- `${QUIET_MARKER} at ${fields.observed_at}`,
120
- `quiet ${fields.quiet} \xB7 unchanged ${fields.unchanged}`,
121
- `children ${fields.children}`,
122
- `lanes ${fields.lanes} \xB7 load ${fields.load} \xB7 record ${fields.record}`,
123
- format_next_line(fields.next)
124
- ].join("\n");
125
- }
126
- function format_line(observations, timing) {
127
- return format_report({
128
- observed_at: format_observed_at(timing.now_ms),
129
- quiet: format_minutes(timing.now_ms - timing.quiet_since_ms),
130
- unchanged: format_minutes(timing.now_ms - timing.unchanged_since_ms),
131
- children: format_children(observations.children),
132
- lanes: format_lanes(observations.lanes),
133
- load: observations.load_average.toFixed(1),
134
- record: format_record(observations.record_age_ms),
135
- next: format_next_report(timing.now_ms, timing.interval_ms)
136
- });
137
- }
138
- function is_due(last_report_ms, now_ms, interval_ms) {
139
- return now_ms - last_report_ms >= interval_ms;
140
- }
141
- function minutes_from(raw) {
142
- const minutes = Number(raw);
143
- if (raw === void 0 || raw.trim() === "" || !Number.isFinite(minutes) || minutes <= 0) {
144
- return void 0;
145
- }
146
- return minutes;
147
- }
148
- var run_progress = {
149
- DEFAULT_INTERVAL_MINUTES,
150
- DEFAULT_INTERVAL_MS,
151
- INTERVAL_KEY,
152
- MS_PER_MINUTE,
153
- MS_PER_SECOND,
154
- NO_CHILD_YET,
155
- NO_NEXT,
156
- format_child,
157
- format_children,
158
- format_lanes,
159
- format_line,
160
- format_next_line,
161
- format_next_report,
162
- format_record,
163
- format_report,
164
- is_due,
165
- minutes_from,
166
- next_state,
167
- observation_key
168
- };
169
-
170
- // scripts/run/run-progress-clock.ts
171
- import { execaSync } from "execa";
172
- import { z } from "zod";
173
- var PROGRESS_PREFIX = "josh-run-progress-";
174
- var LIFE_PREFIX = "josh-run-progress-life-";
175
- var LOG_SUFFIX = ".log";
176
- var LOG_KEEP = 12;
177
- var MS_PER_MINUTE2 = 6e4;
178
- var LOG_STALE_MINUTES = 90;
179
- var LOG_STALE_MS = LOG_STALE_MINUTES * MS_PER_MINUTE2;
180
- var BLOCK_SEPARATOR = "\n\n";
181
- var GIT_TIMEOUT_MS = 5e3;
182
- var GIT_DIRECTORY_ARGUMENTS = ["rev-parse", "--absolute-git-dir"];
183
- var report_stamp_schema = z.object({ reported_at: z.string(), line: z.string().optional() });
184
- function parse_stamp(raw) {
185
- const parsed = report_stamp_schema.safeParse(JSON.parse(raw));
186
- if (!parsed.success) return void 0;
187
- const reported_at = Date.parse(parsed.data.reported_at);
188
- return Number.isNaN(reported_at) ? void 0 : reported_at;
189
- }
190
- function read_last_line(target) {
191
- const raw = stamp_file.read_stamp_text(target);
192
- if (raw === void 0) return void 0;
193
- try {
194
- return report_stamp_schema.safeParse(JSON.parse(raw)).data?.line;
195
- } catch {
196
- return void 0;
197
- }
198
- }
199
- function read_last_report(target) {
200
- const raw = stamp_file.read_stamp_text(target);
201
- if (raw === void 0) return void 0;
202
- try {
203
- return parse_stamp(raw);
204
- } catch {
205
- return void 0;
206
- }
207
- }
208
- function log_path_of(target) {
209
- return `${target}${LOG_SUFFIX}`;
210
- }
211
- function is_log_stale(target, now_ms) {
212
- const previous_ms = read_last_report(target);
213
- return previous_ms === void 0 || now_ms - previous_ms > LOG_STALE_MS;
214
- }
215
- function recent_blocks(log_target, is_fresh) {
216
- if (is_fresh) return [];
217
- const raw = stamp_file.read_stamp_text(log_target);
218
- if (raw === void 0) return [];
219
- return raw.split(BLOCK_SEPARATOR).map((block) => block.trimEnd()).filter((block) => block.length > 0);
220
- }
221
- function append_line(log_target, reported_at, line, is_fresh) {
222
- const block = `${reported_at}
223
- ${line}`;
224
- const blocks = [...recent_blocks(log_target, is_fresh), block].slice(-LOG_KEEP);
225
- stamp_file.write_text_stamp(log_target, `${blocks.join(BLOCK_SEPARATOR)}
226
- `);
227
- }
228
- function mark(target, now_ms, line) {
229
- const reported_at = new Date(now_ms).toISOString();
230
- const kept = line ?? read_last_line(target);
231
- if (line !== void 0) {
232
- append_line(log_path_of(target), reported_at, line, is_log_stale(target, now_ms));
233
- }
234
- stamp_file.write_stamp(target, kept === void 0 ? { reported_at } : { reported_at, line: kept });
235
- }
236
- function stamp_target_of(git_directory) {
237
- return stamp_file.stamp_path(PROGRESS_PREFIX, git_directory);
238
- }
239
- function life_target_of(git_directory) {
240
- return stamp_file.stamp_path(LIFE_PREFIX, git_directory);
241
- }
242
- var life_schema = z.object({ alive: z.literal(true), pinged_at: z.string().optional() });
243
- function begin_life(target) {
244
- stamp_file.write_stamp(target, { alive: true, pinged_at: (/* @__PURE__ */ new Date()).toISOString() });
245
- }
246
- function ping_life(target) {
247
- begin_life(target);
248
- }
249
- function parse_pinged_at(raw) {
250
- const parsed = life_schema.safeParse(JSON.parse(raw));
251
- if (!parsed.success || parsed.data.pinged_at === void 0) return void 0;
252
- const pinged_ms = Date.parse(parsed.data.pinged_at);
253
- return Number.isNaN(pinged_ms) ? void 0 : pinged_ms;
254
- }
255
- function is_life_fresh(target, threshold_ms) {
256
- const raw = stamp_file.read_stamp_text(target);
257
- if (raw === void 0) return false;
258
- try {
259
- const pinged_ms = parse_pinged_at(raw);
260
- return pinged_ms !== void 0 && Date.now() - pinged_ms < threshold_ms;
261
- } catch {
262
- return false;
263
- }
264
- }
265
- function is_life_ended(target) {
266
- return stamp_file.read_stamp_text(target) === void 0;
267
- }
268
- function end_life(target) {
269
- stamp_file.remove_stamp(target);
270
- }
271
- function git_directory_sync() {
272
- const git_binary = git_utilities.get_git_command_for_spawn();
273
- const result = execaSync(git_binary, GIT_DIRECTORY_ARGUMENTS, {
274
- reject: false,
275
- timeout: GIT_TIMEOUT_MS
276
- });
277
- if (result.exitCode !== 0) return void 0;
278
- const output = result.stdout.trim();
279
- return output === "" ? void 0 : output;
280
- }
281
- function stamp_target_sync() {
282
- const directory = git_directory_sync();
283
- return directory === void 0 ? void 0 : stamp_target_of(directory);
284
- }
285
- function read_last_report_sync() {
286
- const target = stamp_target_sync();
287
- return target === void 0 ? void 0 : read_last_report(target);
288
- }
289
- var run_progress_clock = {
290
- LIFE_PREFIX,
291
- LOG_KEEP,
292
- LOG_STALE_MS,
293
- PROGRESS_PREFIX,
294
- append_line,
295
- begin_life,
296
- end_life,
297
- is_life_ended,
298
- is_life_fresh,
299
- life_target_of,
300
- log_path_of,
301
- mark,
302
- parse_stamp,
303
- ping_life,
304
- read_last_line,
305
- read_last_report,
306
- read_last_report_sync,
307
- stamp_target_of,
308
- stamp_target_sync
309
- };
310
-
311
- // scripts/run/run-progress-config.ts
312
- import { readFileSync } from "node:fs";
313
- import path from "node:path";
314
- import { z as z2 } from "zod";
315
- var PACKAGE_FILE = "package.json";
316
- var CONFIG_FIELD = "josh";
317
- var INTERVAL_FIELD = "progress_interval_minutes";
318
- var MAX_DEPTH = 10;
319
- var package_schema = z2.object({
320
- josh: z2.object({ progress_interval_minutes: z2.number().positive() }).optional()
321
- });
322
- function ancestors_of(start) {
323
- const directories = [];
324
- let current = path.resolve(start);
325
- while (directories.length < MAX_DEPTH && !directories.includes(current)) {
326
- directories.push(current);
327
- current = path.dirname(current);
328
- }
329
- return directories;
330
- }
331
- function read_declared_minutes(directory) {
332
- try {
333
- const raw = readFileSync(path.join(directory, PACKAGE_FILE), "utf8");
334
- const parsed = package_schema.safeParse(JSON.parse(raw));
335
- return parsed.success ? parsed.data.josh?.progress_interval_minutes : void 0;
336
- } catch {
337
- return void 0;
338
- }
339
- }
340
- function read_package_minutes(start) {
341
- for (const directory of ancestors_of(start)) {
342
- const minutes = read_declared_minutes(directory);
343
- if (minutes !== void 0) return minutes;
344
- }
345
- return void 0;
346
- }
347
- function to_ms(minutes) {
348
- return minutes === void 0 ? void 0 : minutes * run_progress.MS_PER_MINUTE;
349
- }
350
- function resolve_interval_ms(raw, directory = process.cwd()) {
351
- return to_ms(run_progress.minutes_from(raw)) ?? to_ms(run_progress.minutes_from(process.env[run_progress.INTERVAL_KEY])) ?? to_ms(read_package_minutes(directory)) ?? run_progress.DEFAULT_INTERVAL_MS;
352
- }
353
- function configured_interval_ms(directory = process.cwd()) {
354
- return resolve_interval_ms(void 0, directory);
355
- }
356
- var run_progress_config = {
357
- CONFIG_FIELD,
358
- INTERVAL_FIELD,
359
- PACKAGE_FILE,
360
- configured_interval_ms,
361
- read_package_minutes,
362
- resolve_interval_ms
363
- };
364
-
365
- // scripts/rules/early-heartbeat.ts
366
- var ARM_PREFIX = "josh-heartbeat-arm-";
367
- var SEGMENT_SEPARATOR = /&&|\|\||;|&|\n/u;
368
- var SLEEP_SEGMENT = /^sleep\s+(\d+(?:\.\d+)?)([smhd]?)$/u;
369
- var INERT_SEGMENT = /^(?::|true|(?:echo|printf|date)\b.*)$/u;
370
- var LOOP_KEYWORD = /\b(?:while|until|for|do|done)\b/u;
371
- var SECONDS_PER_UNIT = { d: 86400, h: 3600, m: 60, s: 1 };
372
- var BARE_NUMBER_SECONDS = 1;
373
- var AMOUNT_GROUP = 1;
374
- var UNIT_GROUP = 2;
375
- var NO_SECONDS = 0;
376
- var ARM_RECORD = hook_decision.create_refusal_stamp(ARM_PREFIX);
377
- function segments_of(command) {
378
- return command.split(SEGMENT_SEPARATOR).map((segment) => segment.trim()).filter((segment) => segment !== "");
379
- }
380
- function sleep_seconds(segment) {
381
- const matched = SLEEP_SEGMENT.exec(segment);
382
- if (matched === null) return void 0;
383
- const unit = matched[UNIT_GROUP] ?? "";
384
- return Number(matched[AMOUNT_GROUP]) * (SECONDS_PER_UNIT[unit] ?? BARE_NUMBER_SECONDS);
385
- }
386
- function is_wait_or_inert(segment) {
387
- return sleep_seconds(segment) !== void 0 || INERT_SEGMENT.test(segment);
388
- }
389
- function is_wait_timer(command) {
390
- if (LOOP_KEYWORD.test(command)) return false;
391
- const segments = segments_of(command);
392
- if (segments.every((segment) => sleep_seconds(segment) === void 0)) return false;
393
- return segments.every((segment) => is_wait_or_inert(segment));
394
- }
395
- function wait_duration_ms(command) {
396
- const seconds = segments_of(command).reduce(
397
- (total, segment) => total + (sleep_seconds(segment) ?? NO_SECONDS),
398
- NO_SECONDS
399
- );
400
- return seconds * run_progress.MS_PER_SECOND;
401
- }
402
- function is_armed(target, now_ms) {
403
- return now_ms < ARM_RECORD.last_ms(target);
404
- }
405
- function landing_ms(command, now_ms, last_report_ms) {
406
- const interval_ms = run_progress_config.configured_interval_ms();
407
- const duration_ms = wait_duration_ms(command);
408
- if (duration_ms > interval_ms) return void 0;
409
- const lands_ms = now_ms + duration_ms;
410
- return run_progress.is_due(last_report_ms, lands_ms, interval_ms) ? lands_ms : void 0;
411
- }
412
- function decide(call, run, can_record) {
413
- const last_report_ms = run_progress_clock.read_last_report_sync();
414
- if (last_report_ms === void 0) return false;
415
- const target = ARM_RECORD.path(run.transcript);
416
- if (is_armed(target, run.now_ms)) return true;
417
- const lands_ms = landing_ms(time_shell.bash_command(call.input), run.now_ms, last_report_ms);
418
- if (lands_ms === void 0) return true;
419
- if (can_record) ARM_RECORD.record(target, lands_ms);
420
- return false;
421
- }
422
- 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` waits the interval out for you (`JOSH_PROGRESS_INTERVAL_MINUTES`, or `josh.progress_interval_minutes` in `package.json`, default 20 minutes), prints the five labelled lines and exits \u2014 start it in the background, relay the lines it printed when it exits, and start the next one. 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. 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.';
423
- var PROGRESS_NAMES = /* @__PURE__ */ new Set(["run:progress"]);
424
- var WATCH_FLAG = /(?:^|\s)--(?:wait|once)(?=\s|$)/u;
425
- function is_progress_watch_segment(segment) {
426
- return shell_segments.is_josh_command(segment, PROGRESS_NAMES) && WATCH_FLAG.test(segment);
427
- }
428
- function is_progress_watch(command) {
429
- return segments_of(time_shell.unquoted(command)).some(
430
- (segment) => is_progress_watch_segment(segment)
431
- );
432
- }
433
- function waits_for_progress(command) {
434
- return is_wait_timer(command) || is_progress_watch(command);
435
- }
436
- var early_heartbeat = {
437
- ARM_PREFIX,
438
- EARLY_HEARTBEAT_REASON,
439
- decide,
440
- is_progress_watch,
441
- is_wait_timer,
442
- wait_duration_ms,
443
- waits_for_progress
444
- };
445
-
446
- // scripts/rules/file-body.ts
447
- import { existsSync } from "node:fs";
448
- var HEREDOC = /<<-?\s*['"]?\w+/u;
449
- var LINE_BREAK = "\n";
450
- function first_line(command) {
451
- const [line] = command.split(LINE_BREAK);
452
- return line ?? "";
453
- }
454
- function has_heredoc(command) {
455
- return HEREDOC.test(command);
456
- }
457
- var FILE_REDIRECT = /(?:^|\s)>>?\s*(?!&)("[^"]+"|'[^']+'|\S+)/u;
458
- var TEE_TARGET = /\btee\s+(?:-a\s+)?(?!-)("[^"]+"|'[^']+'|\S+)/u;
459
- var QUOTES = /^['"]|['"]$/gu;
460
- function redirect_target(line) {
461
- const target = FILE_REDIRECT.exec(line)?.[1] ?? TEE_TARGET.exec(line)?.[1];
462
- return target === void 0 ? void 0 : target.replaceAll(QUOTES, "");
463
- }
464
- function writes_existing_file(command, has_file) {
465
- if (!has_heredoc(command)) return false;
466
- const target = redirect_target(first_line(command));
467
- return target !== void 0 && has_file(target);
468
- }
469
- var BODY_INTERPRETERS = /* @__PURE__ */ new Set(["python", "python3", "ruby", "perl", "php"]);
470
- var NODE = "node";
471
- var NODE_EVAL = /(?:^|\s)(?:-e|--eval)\b/u;
472
- var WRITE_CALL = /writeFile|appendFile|createWriteStream|\.write(?:lines)?\s*\(/u;
473
- var WRITE_MODE = /open\s*\([^)]*['"][rbt+]*[wax]|os\.replace|shutil\.(?:copy|move)/u;
474
- function body_writes(command) {
475
- return WRITE_CALL.test(command) || WRITE_MODE.test(command);
476
- }
477
- function is_interpreter_write(segment, command) {
478
- const leading = time_shell.leading_word(segment);
479
- if (BODY_INTERPRETERS.has(leading)) return has_heredoc(command) && body_writes(command);
480
- return leading === NODE && NODE_EVAL.test(segment) && body_writes(segment);
481
- }
482
- var PERL = "perl";
483
- var PERL_IN_PLACE = /^-[A-Za-z0-9]*i/u;
484
- var WHITESPACE = /\s+/u;
485
- function is_in_place_perl(segment) {
486
- if (time_shell.leading_word(segment) !== PERL) return false;
487
- return segment.split(WHITESPACE).some((word) => PERL_IN_PLACE.test(word));
488
- }
489
- function has_body_interpreter(command) {
490
- return shell_segments.segments_of(first_line(command)).some((segment) => is_interpreter_write(segment, command) || is_in_place_perl(segment));
491
- }
492
- function carries_a_file_body(command, has_file = existsSync) {
493
- return writes_existing_file(command, has_file) || has_body_interpreter(command);
494
- }
495
- 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, 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), or 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.**";
496
- var ROW = {
497
- id: "file-body",
498
- is_trigger: bash_triggers.on_bash_command((command) => carries_a_file_body(command)),
499
- reason: FILE_BODY_REASON,
500
- decide: () => true
501
- };
502
- var file_body = {
503
- FILE_BODY_REASON,
504
- ROW,
505
- carries_a_file_body,
506
- is_in_place_perl,
507
- is_interpreter_write,
508
- writes_existing_file
509
- };
510
-
511
- // scripts/rules/tail-commands.ts
512
- function is_bash_use(block) {
513
- return block.type === cost_blocks.TOOL_USE_TYPE && block.name === cost_blocks.BASH_TOOL;
514
- }
515
- function bash_commands_of(line) {
516
- const parsed = time_transcript_line.parse_line(line);
517
- if (parsed === void 0) return [];
518
- return parsed.blocks.filter((block) => is_bash_use(block)).map((block) => time_shell.bash_command(block.input));
519
- }
520
- function prior_bash_commands(tail) {
521
- return tail.split("\n").flatMap((line) => bash_commands_of(line));
522
- }
523
- var tail_commands = { bash_commands_of, is_bash_use, prior_bash_commands };
524
-
525
- // scripts/rules/filing-cap.ts
526
- var FILING_CAP = 10;
527
- var CAP_REACHED = FILING_CAP;
528
- function is_filing_use(block) {
529
- if (!tail_commands.is_bash_use(block)) return false;
530
- return bash_triggers.is_issue_filing(time_shell.bash_command(block.input));
531
- }
532
- function collect_block(block, found) {
533
- if (block.result_id !== "") found.refusals.set(block.result_id, block.refusal_guard);
534
- if (is_filing_use(block)) found.filing_ids.push(block.id);
535
- }
536
- function collect_line(line, found) {
537
- const parsed = time_transcript_line.parse_line(line);
538
- if (parsed === void 0) return;
539
- for (const block of parsed.blocks) collect_block(block, found);
540
- }
541
- function scan_tail(tail) {
542
- const found = { filing_ids: [], refusals: /* @__PURE__ */ new Map() };
543
- for (const line of tail.split("\n")) collect_line(line, found);
544
- return found;
545
- }
546
- function prior_filing_count(tail) {
547
- const { filing_ids, refusals } = scan_tail(tail);
548
- return filing_ids.filter((id) => (refusals.get(id) ?? "") === "").length;
549
- }
550
- function decide2(_call, run) {
551
- return prior_filing_count(time_density_hook.read_tail(run.transcript)) >= CAP_REACHED;
552
- }
553
- var FILING_CAP_REASON = `\u26D4 filing cap: this run has already filed ${String(FILING_CAP)} Issues, the per-run ceiling \u2014 on reaching it, stop and report rather than filing more. Automatic filing is capped so a run cannot manufacture Issues faster than they can be closed (\`.claude/skills/workflow-commands/SKILL.md\` \u2192 \xA72d, \`observation-filing.md\`). A further Issue that genuinely must exist is a person's call, not this run's. **This rule fires on every filing past the cap, not once per run**, so reissuing the same filing will be refused again.`;
554
- var ROW2 = {
555
- id: "filing-cap",
556
- is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
557
- reason: FILING_CAP_REASON,
558
- decide: decide2
559
- };
560
- var filing_cap = { FILING_CAP, FILING_CAP_REASON, ROW: ROW2, decide: decide2, prior_filing_count };
561
-
562
- // scripts/rules/gh-api.ts
563
- var GH_FLAGS = String.raw`(?:-{1,2}[\w-]+(?:[= ][^\s]+)?\s+)*`;
564
- var GH_API_COMMAND = new RegExp(String.raw`^gh\s+${GH_FLAGS}api\s`, "u");
565
- var API_METHOD = /(?:^|\s)(?:--method|-X)[= ]([A-Za-z]+)/u;
566
- var API_FIELD = /(?:^|\s)(?:--raw-field|--field|--input|-F|-f)(?:[= ]|$)/u;
567
- var READ_METHOD = "GET";
568
- var REPO_PATH = /repos\/([^/\s'"?]+)\/([^/\s'"?]+)/u;
569
- var OWNER_GROUP = 1;
570
- var REPO_GROUP = 2;
571
- function is_gh_api(segment) {
572
- return GH_API_COMMAND.test(segment);
573
- }
574
- function is_read(segment) {
575
- const method = API_METHOD.exec(segment)?.[1];
576
- if (method !== void 0) return method.toUpperCase() === READ_METHOD;
577
- return !API_FIELD.test(segment);
578
- }
579
- function is_write(segment) {
580
- return !is_read(segment);
581
- }
582
- function repo_target(segment) {
583
- const match = REPO_PATH.exec(segment);
584
- if (match === null) return void 0;
585
- return { owner: match[OWNER_GROUP] ?? "", repo: match[REPO_GROUP] ?? "" };
586
- }
587
- var gh_api = { GH_API_COMMAND, GH_FLAGS, is_gh_api, is_read, is_write, repo_target };
588
-
589
- // scripts/rules/git-argv.ts
590
- var ENV_ASSIGNMENT = /^[A-Za-z_]\w*=/u;
591
- var WRAPPER_WORDS = /* @__PURE__ */ new Set([
592
- "pnpm",
593
- "npx",
594
- "exec",
595
- "run",
596
- "sudo",
597
- "command"
598
- ]);
599
- var SUBSHELL_OPEN = "(";
600
- var VALUE_TAKING_GLOBALS = /* @__PURE__ */ new Set([
601
- "-C",
602
- "-c",
603
- "--git-dir",
604
- "--work-tree",
605
- "--namespace",
606
- "--exec-path",
607
- "--super-prefix"
608
- ]);
609
- var FLAG_PREFIX = "-";
610
- var GIT_COMMAND = "git";
611
- var NEXT = 1;
612
- var AFTER_VALUE = 2;
613
- var WHITESPACE2 = /\s+/u;
614
- function words_of(segment) {
615
- return segment.split(WHITESPACE2).filter((word) => word !== "");
616
- }
617
- function is_wrapper_token(word) {
618
- return word === SUBSHELL_OPEN || ENV_ASSIGNMENT.test(word) || WRAPPER_WORDS.has(word);
619
- }
620
- function after_wrapper(words) {
621
- let index = 0;
622
- while (index < words.length && is_wrapper_token(words[index] ?? "")) index += NEXT;
623
- return index;
624
- }
625
- function global_step(flag) {
626
- return VALUE_TAKING_GLOBALS.has(flag) ? AFTER_VALUE : NEXT;
627
- }
628
- function after_globals(words, start) {
629
- let index = start;
630
- while (index < words.length) {
631
- const flag = words[index] ?? "";
632
- if (!flag.startsWith(FLAG_PREFIX)) return index;
633
- index += global_step(flag);
634
- }
635
- return index;
636
- }
637
- function parse(segment) {
638
- const words = words_of(segment);
639
- const git_index = after_wrapper(words);
640
- if (words[git_index] !== GIT_COMMAND) return void 0;
641
- const subcommand_index = after_globals(words, git_index + NEXT);
642
- return { subcommand: words[subcommand_index], args: words.slice(subcommand_index + NEXT) };
643
- }
644
- var SHORT_CLUSTER = /^-[A-Za-z]+$/u;
645
- function short_cluster_has(token, letters) {
646
- if (!SHORT_CLUSTER.test(token)) return false;
647
- const cluster = token.slice(FLAG_PREFIX.length);
648
- for (let index = 0; index < letters.length; index += 1) {
649
- if (cluster.includes(letters.charAt(index))) return true;
650
- }
651
- return false;
652
- }
653
- var git_argv = { parse, short_cluster_has, words_of };
654
-
655
- // scripts/rules/git-force.ts
656
- var FORCE_LONG_PREFIX = "--force";
657
- var DELETE_LONG = "--delete";
658
- var PUSH_SHORT_LETTERS = "fd";
659
- var FORCE_OR_DELETE_REFSPEC = /^[+:]/u;
660
- function is_force_or_delete_push(args) {
661
- return args.some(
662
- (argument) => argument.startsWith(FORCE_LONG_PREFIX) || argument === DELETE_LONG || git_argv.short_cluster_has(argument, PUSH_SHORT_LETTERS) || FORCE_OR_DELETE_REFSPEC.test(argument)
663
- );
664
- }
665
- var BRANCH_DELETE_LETTERS = "dD";
666
- function is_branch_delete(args) {
667
- return args.some(
668
- (argument) => argument === DELETE_LONG || git_argv.short_cluster_has(argument, BRANCH_DELETE_LETTERS)
669
- );
670
- }
671
- var PUSH_SUBCOMMAND = "push";
672
- var BRANCH_SUBCOMMAND = "branch";
673
- function is_destructive_segment(segment) {
674
- const call = git_argv.parse(segment);
675
- if (call === void 0) return false;
676
- if (call.subcommand === PUSH_SUBCOMMAND) return is_force_or_delete_push(call.args);
677
- return call.subcommand === BRANCH_SUBCOMMAND && is_branch_delete(call.args);
678
- }
679
- function is_force_or_delete(command) {
680
- return shell_segments.segments_of(command).some((segment) => is_destructive_segment(segment));
681
- }
682
- 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.**";
683
- var ROW3 = {
684
- id: "git-force",
685
- is_trigger: bash_triggers.on_bash_command(is_force_or_delete),
686
- reason: GIT_FORCE_REASON,
687
- decide: () => true
688
- };
689
- var git_force = {
690
- GIT_FORCE_REASON,
691
- ROW: ROW3,
692
- is_branch_delete,
693
- is_force_or_delete,
694
- is_force_or_delete_push
695
- };
696
-
697
- // scripts/rules/issue-fold.ts
698
- var FOLD_NAMES = /* @__PURE__ */ new Set(["issue:fold"]);
699
- function runs_the_fold(command) {
700
- return shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, FOLD_NAMES));
701
- }
702
- function already_folded(tail, _call) {
703
- if (filing_cap.prior_filing_count(tail) === 0) return true;
704
- return tail_commands.prior_bash_commands(tail).some((command) => runs_the_fold(command));
705
- }
706
- var ISSUE_FOLD_REASON = '\u26D4 issue fold: this run has filed an Issue before and is filing another with no `pnpm josh issue:fold` between them. Several findings from one session fold into one Issue by default \u2014 the filing-time counterpart to the split assessment, whose two questions it reuses: are the findings separable, and does the whole clearly exceed one verification gate (the size half is `split:assess`, called not recomputed). Reissue after folding: `pnpm josh issue:fold "<title>" "<title>" \u2026` \u2014 `fold` means file one Issue covering them, `separate` means file them apart (separable findings whose combined size clears the split guide), and the default is `fold`. Add `--not-separable` when they are really one deliverable. The procedure is `.claude/skills/workflow-commands/split-assessment.md` \u2192 "The question". It fires once per run and cannot repeat on the call in hand.';
707
- var ROW4 = {
708
- id: "issue-fold",
709
- is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
710
- reason: ISSUE_FOLD_REASON,
711
- already_satisfied: already_folded,
712
- keeps: bash_triggers.on_bash_command(runs_the_fold)
713
- };
714
- var issue_fold = { ISSUE_FOLD_REASON, ROW: ROW4, already_folded, runs_the_fold };
715
-
716
- // scripts/rules/issue-scout.ts
717
- var SCOUT_NAMES = /* @__PURE__ */ new Set(["issue:scout"]);
718
- function runs_the_scout(command) {
719
- return shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, SCOUT_NAMES));
720
- }
721
- function already_scouted(tail, _call) {
722
- return tail_commands.prior_bash_commands(tail).some((command) => runs_the_scout(command));
723
- }
724
- var ISSUE_SCOUT_REASON = '\u26D4 issue scout: this files an Issue with no `pnpm josh issue:scout` earlier in the run. A duplicate is likeliest for the work that just finished, which is exactly where self-restraint is weakest. Reissue after scouting: `pnpm josh issue:scout "<title>"` (add `--body "<one-line summary, citing #N where the work follows one>"` so the epic half can answer), then read its `Duplicates:` \u2014 an **open** candidate covering the same work stops the run rather than filing a second Issue, and a `(closed)` one means the work is already done \u2014 before reissuing this filing. The procedure is `.claude/skills/workflow-commands/SKILL.md` \u2192 \xA72e. It fires once per run and cannot repeat on the call in hand.';
725
- var ROW5 = {
726
- id: "issue-scout",
727
- is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
728
- reason: ISSUE_SCOUT_REASON,
729
- already_satisfied: already_scouted,
730
- keeps: bash_triggers.on_bash_command(runs_the_scout)
731
- };
732
- var issue_scout = { ISSUE_SCOUT_REASON, ROW: ROW5, already_scouted, runs_the_scout };
733
-
734
- // scripts/rules/lane-carry-conflict.ts
735
- var RUN_BUDGET_COMMANDS = /* @__PURE__ */ new Set(["run:merge", "run:carry"]);
736
- function invokes_budget_command(command) {
737
- return shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, RUN_BUDGET_COMMANDS));
738
- }
739
- function is_carry_conflict(command, is_lane_child = lane_child_marker.is_child_of(process.cwd())) {
740
- if (!invokes_budget_command(command)) return false;
741
- return is_lane_child;
742
- }
743
- 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". This rule fires on every occurrence, not once per run.';
744
- var ROW6 = {
745
- id: "lane-carry-conflict",
746
- is_trigger: bash_triggers.on_bash_command(is_carry_conflict),
747
- reason: LANE_CARRY_CONFLICT_REASON,
748
- decide: () => true
749
- };
750
- var lane_carry_conflict = {
751
- LANE_CARRY_CONFLICT_REASON,
752
- ROW: ROW6,
753
- invokes_budget_command,
754
- is_carry_conflict
755
- };
756
-
757
- // scripts/agent/interactive-ask.ts
758
- import { z as z3 } from "zod";
759
- var INTERACTIVE_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion"]);
760
- function is_interactive_tool(name) {
761
- return INTERACTIVE_TOOLS.has(name);
762
- }
763
- var OPTION_SCHEMA = z3.looseObject({ label: z3.string().optional() });
764
- var QUESTION_SCHEMA = z3.looseObject({
765
- question: z3.string().optional(),
766
- options: z3.array(OPTION_SCHEMA).optional()
767
- });
768
- var ASK_INPUT_SCHEMA = z3.looseObject({ questions: z3.array(QUESTION_SCHEMA).optional() });
769
- var DENIAL_SCHEMA = z3.looseObject({
770
- tool_name: z3.string().optional(),
771
- tool_input: z3.unknown().optional()
772
- });
773
- var UNKNOWN_QUESTION = "(question text unavailable)";
774
- var OPTION_SEPARATOR = " / ";
775
- var QUESTION_SEPARATOR = "; ";
776
- function labels_of(question) {
777
- return (question.options ?? []).map((option) => option.label ?? "").filter((label) => label !== "");
778
- }
779
- function render_question(question) {
780
- const labels = labels_of(question);
781
- const text2 = question.question ?? UNKNOWN_QUESTION;
782
- return labels.length === 0 ? text2 : `${text2} [${labels.join(OPTION_SEPARATOR)}]`;
783
- }
784
- function interactive_input(raw) {
785
- const denial = DENIAL_SCHEMA.safeParse(raw);
786
- if (!denial.success || !is_interactive_tool(denial.data.tool_name ?? "")) return void 0;
787
- const input = ASK_INPUT_SCHEMA.safeParse(denial.data.tool_input);
788
- return input.success ? input.data : void 0;
789
- }
790
- function refused_ask_of(permission_denials) {
791
- if (!Array.isArray(permission_denials)) return void 0;
792
- const input = permission_denials.map((raw) => interactive_input(raw)).find((one) => one !== void 0);
793
- const questions = input?.questions ?? [];
794
- if (questions.length === 0) return void 0;
795
- return questions.map((question) => render_question(question)).join(QUESTION_SEPARATOR);
796
- }
797
- var interactive_ask = { INTERACTIVE_TOOLS, is_interactive_tool, refused_ask_of };
798
-
799
- // scripts/rules/lane-interactive-ask.ts
800
- function is_interactive_ask(call, is_lane_child = lane_child_marker.is_child_of(process.cwd())) {
801
- if (!interactive_ask.is_interactive_tool(call.name)) return false;
802
- return is_lane_child;
803
- }
804
- 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 `gh api repos/{owner}/{repo}/issues/<N>/comments --field body=@<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.';
805
- var ROW7 = {
806
- id: "lane-interactive-ask",
807
- is_trigger: is_interactive_ask,
808
- reason: LANE_INTERACTIVE_ASK_REASON,
809
- decide: () => true
810
- };
811
- var lane_interactive_ask = { LANE_INTERACTIVE_ASK_REASON, ROW: ROW7, is_interactive_ask };
812
-
813
- // scripts/rules/piped-verification.ts
814
- var VERIFICATION_COMMANDS = /* @__PURE__ */ new Set([
815
- "check",
816
- "cspell",
817
- "cspell:dot",
818
- "eval",
819
- "gate",
820
- "lint",
821
- "lint:eslint",
822
- "lint:prettier",
823
- "lint:related",
824
- "overrides",
825
- "ranges",
826
- "test",
827
- "test:e2e",
828
- "test:related",
829
- "test:unit"
830
- ]);
831
- function is_verification_command(segment) {
832
- return shell_segments.is_josh_command(segment, VERIFICATION_COMMANDS);
833
- }
834
- function is_masked_verification(command) {
835
- return time_shell.discarded_commands(command).some((segment) => is_verification_command(segment));
836
- }
837
- 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.";
838
- function command_pieces(command) {
839
- return shell_segments.segments_of(time_shell.unquoted(command)).flatMap((segment) => segment.split("|")).map((piece) => piece.trim());
840
- }
841
- function runs_verification(command) {
842
- return command_pieces(command).some((piece) => is_verification_command(piece));
843
- }
844
- function keeps_verdict_intact(command) {
845
- return runs_verification(command) && !is_masked_verification(command);
846
- }
847
- var piped_verification = {
848
- PIPED_VERIFICATION_REASON,
849
- VERIFICATION_COMMANDS,
850
- is_masked_verification,
851
- keeps_verdict_intact,
852
- runs_verification
853
- };
854
-
855
- // scripts/rules/pre-gate-cut.ts
856
- var GATE_COMMANDS = /* @__PURE__ */ new Set(["gate"]);
857
- var CUT_COMMANDS = /* @__PURE__ */ new Set(["run:cut"]);
858
- var CUT_MODE_FLAG = /(?:^|\s)--(?:resume|end|json)(?:[=\s]|$)/u;
859
- var HOLD_COMMANDS = /* @__PURE__ */ new Set(["run:hold"]);
860
- var HOLD_MODE_FLAG = /(?:^|\s)--release(?:[=\s]|$)/u;
861
- function invokes_josh(command, names, mode_flag) {
862
- return shell_segments.segments_of(command).some(
863
- (segment) => shell_segments.is_josh_command(segment, names) && mode_flag?.test(segment) !== true
864
- );
865
- }
866
- function runs_the_gate(command) {
867
- return invokes_josh(command, GATE_COMMANDS);
868
- }
869
- function takes_the_cut(command) {
870
- return invokes_josh(command, CUT_COMMANDS, CUT_MODE_FLAG);
871
- }
872
- function claims_the_hold(command) {
873
- return invokes_josh(command, HOLD_COMMANDS, HOLD_MODE_FLAG);
874
- }
875
- function asks_about_the_cut(command) {
876
- return invokes_josh(command, CUT_COMMANDS);
877
- }
878
- function current_state() {
879
- return {
880
- directory: process.cwd(),
881
- carried: run_cut.carried_cut_sync,
882
- marked_issue: lane_child_marker.marked_issue()
883
- };
884
- }
885
- function uncut_lane_issue(state) {
886
- const issue = lane_paths.lane_issue_of(state.directory);
887
- if (issue === void 0) return void 0;
888
- if (state.marked_issue !== issue) return void 0;
889
- return state.carried()?.issue === issue ? void 0 : issue;
890
- }
891
- function is_uncut_gate(command, state = current_state()) {
892
- if (!runs_the_gate(command)) return false;
893
- return uncut_lane_issue(state) !== void 0;
894
- }
895
- 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; 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.";
896
- var pre_gate_cut = {
897
- PRE_GATE_CUT_REASON,
898
- asks_about_the_cut,
899
- claims_the_hold,
900
- is_uncut_gate,
901
- runs_the_gate,
902
- takes_the_cut,
903
- uncut_lane_issue
904
- };
905
-
906
- // scripts/rules/prior-comment-read.ts
907
- var ISSUE_PATH_NUMBER = /issues\/(\d+)/gu;
908
- var ISSUE_VIEW_NUMBER = /\bissue\s+view\s+(?:-{1,2}\S+\s+)*(\d+)/u;
909
- var ISSUE_READ_ARGUMENTS = /\bissue:read\b([\s\d]*)/u;
910
- var A_NUMBER = /^\d+$/u;
911
- var COMMENTS_ENDPOINT = /issues\/\d+\/comments\b/u;
912
- var VIEW_WITH_COMMENTS = /\bissue\s+view\b[^\n]*--comments\b/u;
913
- var ISSUE_READ_COMMAND = /\bissue:read\b/u;
914
- function add_issue_read_numbers(command, numbers) {
915
- const match = ISSUE_READ_ARGUMENTS.exec(command);
916
- if (match === null) return;
917
- const tokens = (match[1] ?? "").split(/\s+/u);
918
- for (const token of tokens) {
919
- if (A_NUMBER.test(token)) numbers.add(Number(token));
920
- }
921
- }
922
- function issue_numbers_of(command) {
923
- const numbers = /* @__PURE__ */ new Set();
924
- for (const match of command.matchAll(ISSUE_PATH_NUMBER)) numbers.add(Number(match[1]));
925
- const view = ISSUE_VIEW_NUMBER.exec(command);
926
- if (view !== null) numbers.add(Number(view[1]));
927
- add_issue_read_numbers(command, numbers);
928
- return numbers;
929
- }
930
- function fetches_comments(command) {
931
- return COMMENTS_ENDPOINT.test(command) || VIEW_WITH_COMMENTS.test(command) || ISSUE_READ_COMMAND.test(command);
932
- }
933
- function reads_target_comments(command, target) {
934
- if (!fetches_comments(command)) return false;
935
- for (const number of issue_numbers_of(command)) {
936
- if (target.has(number)) return true;
937
- }
938
- return false;
939
- }
940
- function already_read_comments_for(tail, command) {
941
- const target = issue_numbers_of(command);
942
- if (target.size === 0) return false;
943
- return tail_commands.prior_bash_commands(tail).some((prior) => reads_target_comments(prior, target));
944
- }
945
- function already_read_for_call(tail, call) {
946
- if (call.name !== cost_blocks.BASH_TOOL) return false;
947
- return already_read_comments_for(tail, time_shell.bash_command(call.input));
948
- }
949
- var prior_comment_read = {
950
- already_read_comments_for,
951
- already_read_for_call,
952
- fetches_comments,
953
- issue_numbers_of
954
- };
955
-
956
- // scripts/rules/rule-body-guard.ts
957
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
958
- var EDIT_TOOL = "Edit";
959
- var WRITE_TOOL = "Write";
960
- var CLAUDE_DOCUMENT = /(?:^|\/)CLAUDE\.md$/u;
961
- var PROMPT_DOCUMENT = /(?:^|\/)prompts\/.+\.md$/u;
962
- var SKILL_DOCUMENT = /(?:^|\/)\.claude\/skills\/.+\.md$/u;
963
- function is_rule_document(file_path) {
964
- return CLAUDE_DOCUMENT.test(file_path) || PROMPT_DOCUMENT.test(file_path) || SKILL_DOCUMENT.test(file_path);
965
- }
966
- var LINK_TARGET = /\]\([^)]*\)/gu;
967
- var BARE_URL = /https?:\/\/\S+/gu;
968
- function prose_of(text2) {
969
- return text2.replaceAll(LINK_TARGET, "]()").replaceAll(BARE_URL, "");
970
- }
971
- var MIN_APPENDED_PROSE = 120;
972
- function adds_rule_prose(old_text, new_text) {
973
- return prose_of(new_text).length - prose_of(old_text).length >= MIN_APPENDED_PROSE;
974
- }
975
- function string_field(input, key) {
976
- const value = input[key];
977
- return typeof value === "string" ? value : "";
978
- }
979
- function read_existing(file_path) {
980
- return existsSync2(file_path) ? readFileSync2(file_path, "utf8") : "";
981
- }
982
- function texts_of(name, input, read) {
983
- if (name === WRITE_TOOL) {
984
- return {
985
- old_text: read(string_field(input, "file_path")),
986
- new_text: string_field(input, "content")
987
- };
988
- }
989
- return {
990
- old_text: string_field(input, "old_string"),
991
- new_text: string_field(input, "new_string")
992
- };
993
- }
994
- function is_edit_tool(name) {
995
- return name === EDIT_TOOL || name === WRITE_TOOL;
996
- }
997
- function rule_document_input(call) {
998
- if (!is_edit_tool(call.name) || !json_value.is_record(call.input)) return void 0;
999
- return is_rule_document(string_field(call.input, "file_path")) ? call.input : void 0;
1000
- }
1001
- function writes_rule_prose(call, read = read_existing) {
1002
- const input = rule_document_input(call);
1003
- if (input === void 0) return false;
1004
- const { old_text, new_text } = texts_of(call.name, input, read);
1005
- return adds_rule_prose(old_text, new_text);
1006
- }
1007
- 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`. This fires once per run \u2014 once you have confirmed the rule can be neither an oracle nor an ordering step, reissue this edit and it will go through.";
1008
- var ROW8 = {
1009
- id: "rule-body",
1010
- is_trigger: writes_rule_prose,
1011
- reason: RULE_BODY_REASON
1012
- };
1013
- var rule_body_guard = {
1014
- RULE_BODY_REASON,
1015
- ROW: ROW8,
1016
- adds_rule_prose,
1017
- is_rule_document,
1018
- prose_of,
1019
- writes_rule_prose
1020
- };
1021
-
1022
- // scripts/rules/run-tail.ts
1023
- var SEGMENT_PREFIX = String.raw`(?:\(\s*)?(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:npx\s+|pnpm\s+(?:exec\s+|run\s+)?)?`;
1024
- var WORD_END = String.raw`(?=\s|$|\))`;
1025
- var PUSH_STEP_SEGMENT = new RegExp(String.raw`^${SEGMENT_PREFIX}josh\s+(?:git|g)${WORD_END}`, "u");
1026
- var CONFIRMED_FLAG = new RegExp(String.raw`(?:^|\s)(?:-y|--yes)${WORD_END}`, "u");
1027
- var SKIPS_THE_PUSH = new RegExp(String.raw`(?:^|\s)--skip-push${WORD_END}`, "u");
1028
- var BACKGROUND_KEY = "run_in_background";
1029
- var QUOTED_SPAN = /"[^"]*"|'[^']*'/gu;
1030
- function flags_of(segment) {
1031
- return segment.replaceAll(QUOTED_SPAN, " ");
1032
- }
1033
- function is_push_step_segment(segment) {
1034
- if (!PUSH_STEP_SEGMENT.test(segment)) return false;
1035
- const flags = flags_of(segment);
1036
- if (SKIPS_THE_PUSH.test(flags)) return false;
1037
- return CONFIRMED_FLAG.test(flags);
1038
- }
1039
- function is_push_step(command) {
1040
- return shell_segments.segments_of(command).some((segment) => is_push_step_segment(segment));
1041
- }
1042
- function is_backgrounded(input) {
1043
- if (!json_value.is_record(input)) return false;
1044
- return input[BACKGROUND_KEY] === true;
1045
- }
1046
- function is_push_step_call(call) {
1047
- if (call.name !== cost_blocks.BASH_TOOL) return false;
1048
- return is_push_step(time_shell.bash_command(call.input));
1049
- }
1050
- function is_backgrounded_push_step(call) {
1051
- if (!is_backgrounded(call.input)) return false;
1052
- return is_push_step_call(call);
1053
- }
1054
- function is_foreground_push_step(call) {
1055
- if (is_backgrounded(call.input)) return false;
1056
- return is_push_step_call(call);
1057
- }
1058
- function decide3() {
1059
- return true;
1060
- }
1061
- var RUN_TAIL_REASON = '\u26D4 run tail: this issues the commit-push-PR step in the foreground, where the harness decides when to stop waiting. Reissue it with `run_in_background` set. In `fullrun #1501` this call was given a 900-second timeout, the harness detached it at its own 600-second cap, and nothing read the output for a further 6m28s \u2014 45m03s of wall clock on about 17 minutes of work (joshuafolkken/kit#1510). **Never give a foreground call a timeout above the harness cap**: the cap decides, not the number. A backgrounded call re-invokes you when it exits, so the completion notification is what resumes the run. **When it arrives, the turn that reads it goes straight through any branch-2 filing and `pnpm josh epic:bundle` to `pnpm josh followup` \u2014 the turn never ends at the push.** CI went green 6m06s before the merge started in that run, and it started only because the person asked; joshuafolkken/kit#1333 had already settled that the merge is issued in the turn the review closes. While the push runs, do the work that writes nothing \u2014 drafting the completion body to a file, deciding the branch-2 disposition. `pnpm josh followup` itself stays in the **foreground** \u2014 nearly every step after it reads its result. But a tail does follow the merge, measured at 3.0 min and 5.9% of a run (joshuafolkken/kit#1462), so empty it beforehand: the steps that read the merge result stay after `followup`, and the rest are composed in the turn that issues it, bar the one exception background-commands.md names. 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 foreground push, not once per run**, so reissuing the same call in the foreground will be refused again.';
1062
- var run_tail = {
1063
- RUN_TAIL_REASON,
1064
- decide: decide3,
1065
- is_backgrounded_push_step,
1066
- is_foreground_push_step,
1067
- is_push_step,
1068
- is_push_step_call
1069
- };
1070
-
1071
- // scripts/rules/shell-body-trigger.ts
1072
- var FIELD_FLAGS = "(?:-f|-F|--field|--raw-field)";
1073
- var MESSAGE_FLAGS = "(?:--body|--notify-message)";
1074
- var FIELD_OPENING = String.raw`${FIELD_FLAGS}[\s=]*(?:'?body="|"body=)`;
1075
- var MESSAGE_OPENING = String.raw`${MESSAGE_FLAGS}[\s=]+"`;
1076
- var NESTED_SUBSTITUTION = String.raw`\$\([^()]*\)`;
1077
- var SUBSTITUTION = String.raw`\$\((?:[^()$]|\$(?!\()|${NESTED_SUBSTITUTION})*\)`;
1078
- var QUOTED_VALUE = String.raw`(?:[^"\\$]|\\.|\$(?!\()|${SUBSTITUTION})*`;
1079
- var INLINE_BODY_VALUE = new RegExp(
1080
- `(?:${FIELD_OPENING}|${MESSAGE_OPENING})(${QUOTED_VALUE})"`,
1081
- "gu"
1082
- );
1083
- var SHELL_EVALUATED = /[`$]/u;
1084
- var WHOLE_VALUE_SUBSTITUTION = new RegExp(`^${SUBSTITUTION}$`, "u");
1085
- var BACKTICK = "`";
1086
- var WHOLE_VALUE_FILE_REFERENCE = /^@\S+$/u;
1087
- var ESCAPED_PAIR = /\\./gu;
1088
- function is_whole_value_substitution(literal) {
1089
- return WHOLE_VALUE_SUBSTITUTION.test(literal) && !literal.includes(BACKTICK);
1090
- }
1091
- function is_body_already_in_a_file(literal) {
1092
- return WHOLE_VALUE_FILE_REFERENCE.test(literal) || is_whole_value_substitution(literal);
1093
- }
1094
- function is_evaluated_value(raw_value) {
1095
- const literal = raw_value.replaceAll(ESCAPED_PAIR, "");
1096
- if (is_body_already_in_a_file(literal)) return false;
1097
- return SHELL_EVALUATED.test(literal);
1098
- }
1099
- function is_shell_evaluated_body(command) {
1100
- for (const match of command.matchAll(INLINE_BODY_VALUE)) {
1101
- if (is_evaluated_value(match[1] ?? "")) return true;
1102
- }
1103
- return false;
1104
- }
1105
- var FILE_FLAG = /(?:--body-file|--notify-message-file)(?:[\s=]|$)/u;
1106
- var FIELD_FILE_REFERENCE = new RegExp(String.raw`${FIELD_FLAGS}[\s=]*body=@\S`, "u");
1107
- var ANSI_C_BODY = /(?:(?:--body|--notify-message)[\s=]+|body=)\$'/u;
1108
- var DOUBLE_QUOTED_SPAN = /"[^"]*"/gu;
1109
- var BODY_FLAG = new RegExp(
1110
- String.raw`${FIELD_FLAGS}[\s=]*'?"?body=|${MESSAGE_FLAGS}[\s=]|--body-file[\s=]|--notify-message-file[\s=]`,
1111
- "u"
1112
- );
1113
- var QUOTED_SPAN2 = /"[^"]*"|'[^']*'/gu;
1114
- function inline_body_literals(command) {
1115
- return Array.from(
1116
- command.matchAll(INLINE_BODY_VALUE),
1117
- (match) => (match[1] ?? "").replaceAll(ESCAPED_PAIR, "")
1118
- );
1119
- }
1120
- function is_safe_body_form(command) {
1121
- if (ANSI_C_BODY.test(command.replaceAll(DOUBLE_QUOTED_SPAN, " "))) return true;
1122
- const flags = command.replaceAll(QUOTED_SPAN2, " ");
1123
- if (FILE_FLAG.test(flags) || FIELD_FILE_REFERENCE.test(flags)) return true;
1124
- return inline_body_literals(command).some((literal) => is_body_already_in_a_file(literal));
1125
- }
1126
- function keeps_body_safe(command) {
1127
- return is_safe_body_form(command) && !is_shell_evaluated_body(command);
1128
- }
1129
- function carries_a_body(command) {
1130
- if (BODY_FLAG.test(command.replaceAll(QUOTED_SPAN2, " "))) return true;
1131
- return inline_body_literals(command).length > 0;
1132
- }
1133
- var shell_body_trigger = {
1134
- carries_a_body,
1135
- is_safe_body_form,
1136
- is_shell_evaluated_body,
1137
- keeps_body_safe
1138
- };
1139
-
1140
- // scripts/test/test-declared-changed.ts
1141
- import { execFileSync } from "node:child_process";
1142
-
1143
- // scripts/test/test-declared-logic.ts
1144
- var TEST_SUFFIXES = [".e2e.ts", ".test.ts"];
1145
- var EXEMPT_PATHS = [".editorconfig"];
1146
- var EXEMPT_PREFIXES = [".idea/", ".vscode/", "prompts/"];
1147
- var EXEMPT_SUFFIXES = [".md"];
1148
- function normalize(paths) {
1149
- return paths.map((path3) => path3.trim()).filter((path3) => path3 !== "");
1150
- }
1151
- function is_test_file(path3) {
1152
- return TEST_SUFFIXES.some((suffix) => path3.endsWith(suffix));
1153
- }
1154
- function is_exempt(path3) {
1155
- if (EXEMPT_PATHS.includes(path3)) return true;
1156
- return EXEMPT_SUFFIXES.some((suffix) => path3.endsWith(suffix)) || EXEMPT_PREFIXES.some((prefix) => path3.startsWith(prefix));
1157
- }
1158
- function is_runtime(path3) {
1159
- return !is_test_file(path3) && !is_exempt(path3);
1160
- }
1161
- function verdict_for(paths) {
1162
- const changed = normalize(paths);
1163
- if (changed.some((path3) => is_test_file(path3))) return "satisfied";
1164
- if (changed.some((path3) => is_runtime(path3))) return "required";
1165
- return "exempt";
1166
- }
1167
- function runtime_files(paths) {
1168
- return normalize(paths).filter((path3) => is_runtime(path3));
1169
- }
1170
- function exempt_files(paths) {
1171
- return normalize(paths).filter((path3) => is_exempt(path3));
1172
- }
1173
- var test_declared_logic = {
1174
- EXEMPT_PATHS,
1175
- EXEMPT_PREFIXES,
1176
- EXEMPT_SUFFIXES,
1177
- TEST_SUFFIXES,
1178
- exempt_files,
1179
- is_exempt,
1180
- is_runtime,
1181
- is_test_file,
1182
- runtime_files,
1183
- verdict_for
1184
- };
1185
-
1186
- // scripts/test/test-declared-changed.ts
1187
- var RENAME_ARROW = " -> ";
1188
- var STATUS_PREFIX_LENGTH = 3;
1189
- function path_of(line) {
1190
- const body = line.slice(STATUS_PREFIX_LENGTH);
1191
- const arrow = body.indexOf(RENAME_ARROW);
1192
- return arrow === -1 ? body : body.slice(arrow + RENAME_ARROW.length);
1193
- }
1194
- function read_changed_paths_sync() {
1195
- try {
1196
- const output = execFileSync("git", ["status", "--porcelain"], {
1197
- encoding: "utf8",
1198
- stdio: ["ignore", "pipe", "ignore"]
1199
- });
1200
- return output.split("\n").filter((line) => line.length > STATUS_PREFIX_LENGTH).map((line) => path_of(line));
1201
- } catch {
1202
- return [];
1203
- }
1204
- }
1205
- var injected = {};
1206
- function source_paths() {
1207
- return injected.paths ?? read_changed_paths_sync();
1208
- }
1209
- function current_verdict(paths = source_paths()) {
1210
- return test_declared_logic.verdict_for(paths);
1211
- }
1212
- function restore_injection(previous) {
1213
- if (previous === void 0) {
1214
- delete injected.paths;
1215
- return;
1216
- }
1217
- injected.paths = previous;
1218
- }
1219
- function with_paths(paths, body) {
1220
- const previous = injected.paths;
1221
- injected.paths = paths;
1222
- try {
1223
- return body();
1224
- } finally {
1225
- restore_injection(previous);
1226
- }
1227
- }
1228
- var test_declared_changed = { current_verdict, path_of, read_changed_paths_sync, with_paths };
1229
-
1230
- // scripts/rules/test-declared-commit.ts
1231
- function is_untested_commit(call) {
1232
- if (!run_tail.is_push_step_call(call)) return false;
1233
- return test_declared_changed.current_verdict() === "required";
1234
- }
1235
- 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.';
1236
- var ROW9 = { id: "test-declared", is_trigger: is_untested_commit, reason: REASON };
1237
- var test_declared_commit = { REASON, ROW: ROW9, is_untested_commit };
1238
-
1239
- // scripts/discovery/repo-discovery.ts
1240
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
1241
- import path2 from "node:path";
1242
-
1243
- // scripts/discovery/repo-origin.ts
1244
- var GITHUB_HOST = "github.com";
1245
- var SSH_ALIAS_PATTERN = /^github(?:-\w+)*$/u;
1246
- var GIT_SUFFIX = ".git";
1247
- var PATH_SEPARATOR = "/";
1248
- var USER_SEPARATOR = "@";
1249
- var HOST_SEPARATOR = ":";
1250
- var OWNER_INDEX = 0;
1251
- var REPO_INDEX = 1;
1252
- var REPO_PATH_SEGMENTS = 2;
1253
- var NOT_FOUND = -1;
1254
- var SECTION_PREFIX = "[";
1255
- var ORIGIN_SECTION = /^\[remote\s+"origin"\]$/u;
1256
- var URL_KEY = "url";
1257
- function is_github_hostname(host) {
1258
- return host.toLowerCase() === GITHUB_HOST;
1259
- }
1260
- function is_github_ssh_host(host) {
1261
- const normalized = host.toLowerCase();
1262
- return is_github_hostname(normalized) || SSH_ALIAS_PATTERN.test(normalized);
1263
- }
1264
- function strip_git_suffix(name) {
1265
- return name.endsWith(GIT_SUFFIX) ? name.slice(0, -GIT_SUFFIX.length) : name;
1266
- }
1267
- function to_identity(repository_path) {
1268
- const segments = repository_path.split(PATH_SEPARATOR).filter((segment) => segment !== "");
1269
- if (segments.length !== REPO_PATH_SEGMENTS) return void 0;
1270
- const owner = segments[OWNER_INDEX] ?? "";
1271
- const repo = strip_git_suffix(segments[REPO_INDEX] ?? "");
1272
- return repo === "" ? void 0 : { owner, repo };
1273
- }
1274
- function parse_scp_like(url) {
1275
- const user_end = url.indexOf(USER_SEPARATOR);
1276
- const authority = user_end === NOT_FOUND ? url : url.slice(user_end + 1);
1277
- const host_end = authority.indexOf(HOST_SEPARATOR);
1278
- if (host_end === NOT_FOUND) return void 0;
1279
- if (!is_github_ssh_host(authority.slice(0, host_end))) return void 0;
1280
- return to_identity(authority.slice(host_end + 1));
1281
- }
1282
- function parse_url_like(url) {
1283
- try {
1284
- const parsed = new URL(url);
1285
- return is_github_hostname(parsed.hostname) ? to_identity(parsed.pathname) : void 0;
1286
- } catch {
1287
- return void 0;
1288
- }
1289
- }
1290
- function parse_origin_url(url) {
1291
- const trimmed = url.trim();
1292
- if (trimmed === "") return void 0;
1293
- return parse_url_like(trimmed) ?? parse_scp_like(trimmed);
1294
- }
1295
- function is_section_header(line) {
1296
- return line.startsWith(SECTION_PREFIX);
1297
- }
1298
- function origin_section_lines(lines) {
1299
- const start = lines.findIndex((line) => ORIGIN_SECTION.test(line));
1300
- if (start === NOT_FOUND) return [];
1301
- const rest = lines.slice(start + 1);
1302
- const end = rest.findIndex((line) => is_section_header(line));
1303
- return end === NOT_FOUND ? rest : rest.slice(0, end);
1304
- }
1305
- function entry_value(line, key) {
1306
- const separator = line.indexOf("=");
1307
- if (separator === NOT_FOUND) return void 0;
1308
- if (line.slice(0, separator).trim() !== key) return void 0;
1309
- return line.slice(separator + 1).trim();
1310
- }
1311
- function parse_origin_from_config(content) {
1312
- const lines = content.split("\n").map((line) => line.trim());
1313
- return origin_section_lines(lines).map((line) => entry_value(line, URL_KEY)).find((url) => url !== void 0);
1314
- }
1315
- function format_identity(identity) {
1316
- return `${identity.owner}${PATH_SEPARATOR}${identity.repo}`.toLowerCase();
1317
- }
1318
- var repo_origin = {
1319
- GITHUB_HOST,
1320
- is_github_hostname,
1321
- is_github_ssh_host,
1322
- parse_origin_from_config,
1323
- parse_origin_url,
1324
- format_identity
1325
- };
1326
-
1327
- // scripts/discovery/repo-map-logic.ts
1328
- var OVERRIDE_ENTRY_SEPARATOR = ",";
1329
- var OVERRIDE_PAIR_SEPARATOR = "=";
1330
- var OVERRIDE_PAIR_PARTS = 2;
1331
- var KEY_INDEX = 0;
1332
- var PATH_INDEX = 1;
1333
- function is_same_owner(identity, current_owner2) {
1334
- return identity.owner.toLowerCase() === current_owner2.toLowerCase();
1335
- }
1336
- function add_entry(target, origin_url, repository_path, current_owner2) {
1337
- if (origin_url === void 0) return;
1338
- const identity = repo_origin.parse_origin_url(origin_url);
1339
- if (identity === void 0 || !is_same_owner(identity, current_owner2)) return;
1340
- target.set(repo_origin.format_identity(identity), repository_path);
1341
- }
1342
- function parse_override_entry(entry) {
1343
- const parts = entry.split(OVERRIDE_PAIR_SEPARATOR).map((part) => part.trim());
1344
- if (parts.length !== OVERRIDE_PAIR_PARTS) return void 0;
1345
- const key = parts[KEY_INDEX] ?? "";
1346
- const repository_path = parts[PATH_INDEX] ?? "";
1347
- if (repository_path === "") return void 0;
1348
- return { path: repository_path, origin_url: `https://${repo_origin.GITHUB_HOST}/${key}` };
1349
- }
1350
- function parse_overrides(raw) {
1351
- if (raw === void 0) return [];
1352
- return raw.split(OVERRIDE_ENTRY_SEPARATOR).map((entry) => parse_override_entry(entry)).filter((entry) => entry !== void 0);
1353
- }
1354
- function build_repository_map(discovered, current_owner2, override_value) {
1355
- const result = /* @__PURE__ */ new Map();
1356
- for (const entry of discovered) add_entry(result, entry.origin_url, entry.path, current_owner2);
1357
- for (const entry of parse_overrides(override_value)) {
1358
- add_entry(result, entry.origin_url, entry.path, current_owner2);
1359
- }
1360
- return result;
1361
- }
1362
- var repo_map_logic = {
1363
- is_same_owner,
1364
- parse_overrides,
1365
- build_repository_map
1366
- };
1367
-
1368
- // scripts/discovery/repo-discovery.ts
1369
- var GIT_DIRECTORY = ".git";
1370
- var GIT_CONFIG = "config";
1371
- var GITDIR_PREFIX = "gitdir:";
1372
- var COMMON_DIRECTORY_FILE = "commondir";
1373
- var OVERRIDE_ENV_KEY = "JOSH_REPO_PATHS";
1374
- function resolve_common_directory(git_directory) {
1375
- const pointer = path2.join(git_directory, COMMON_DIRECTORY_FILE);
1376
- if (!existsSync3(pointer)) return git_directory;
1377
- try {
1378
- return path2.resolve(git_directory, readFileSync3(pointer, "utf8").trim());
1379
- } catch {
1380
- return git_directory;
1381
- }
1382
- }
1383
- function follow_git_file(repository_path, git_file) {
1384
- const pointer = readFileSync3(git_file, "utf8").trim();
1385
- if (!pointer.startsWith(GITDIR_PREFIX)) return void 0;
1386
- return path2.resolve(repository_path, pointer.slice(GITDIR_PREFIX.length).trim());
1387
- }
1388
- function resolve_git_directory(repository_path) {
1389
- const candidate = path2.join(repository_path, GIT_DIRECTORY);
1390
- if (!existsSync3(candidate)) return void 0;
1391
- try {
1392
- if (statSync(candidate).isDirectory()) return candidate;
1393
- const linked = follow_git_file(repository_path, candidate);
1394
- return linked === void 0 ? void 0 : resolve_common_directory(linked);
1395
- } catch {
1396
- return void 0;
1397
- }
1398
- }
1399
- function read_origin_url(repository_path) {
1400
- const git_directory = resolve_git_directory(repository_path);
1401
- if (git_directory === void 0) return void 0;
1402
- try {
1403
- return repo_origin.parse_origin_from_config(
1404
- readFileSync3(path2.join(git_directory, GIT_CONFIG), "utf8")
1405
- );
1406
- } catch {
1407
- return void 0;
1408
- }
1409
- }
1410
- function sibling_directories(parent) {
1411
- try {
1412
- return readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path2.join(parent, entry.name)).toSorted((left, right) => left.localeCompare(right));
1413
- } catch {
1414
- return [];
1415
- }
1416
- }
1417
- function scan_siblings(parent) {
1418
- return sibling_directories(parent).map((repository_path) => ({
1419
- path: repository_path,
1420
- origin_url: read_origin_url(repository_path)
1421
- }));
1422
- }
1423
- function resolve_current_owner(repository_path) {
1424
- const origin_url = read_origin_url(repository_path);
1425
- if (origin_url === void 0) return void 0;
1426
- return repo_origin.parse_origin_url(origin_url)?.owner;
1427
- }
1428
- function main_worktree(repository_path) {
1429
- const git_directory = resolve_git_directory(repository_path);
1430
- if (git_directory === void 0) return repository_path;
1431
- if (path2.basename(git_directory) !== GIT_DIRECTORY) return repository_path;
1432
- return path2.dirname(git_directory);
1433
- }
1434
- function discover_repositories(repository_path, environment = process.env) {
1435
- const main = main_worktree(repository_path);
1436
- const current_owner2 = resolve_current_owner(main);
1437
- if (current_owner2 === void 0) return /* @__PURE__ */ new Map();
1438
- return repo_map_logic.build_repository_map(
1439
- scan_siblings(path2.dirname(main)),
1440
- current_owner2,
1441
- environment[OVERRIDE_ENV_KEY]
1442
- );
1443
- }
1444
- var repo_discovery = {
1445
- OVERRIDE_ENV_KEY,
1446
- main_worktree,
1447
- read_origin_url,
1448
- resolve_current_owner,
1449
- scan_siblings,
1450
- discover_repositories
1451
- };
1452
-
1453
- // scripts/discovery/repo-party.ts
1454
- var FIRST_PARTY = "first-party";
1455
- var THIRD_PARTY = "third-party";
1456
- var UNKNOWN = "unknown";
1457
- function classify(session_owner, target) {
1458
- if (session_owner === void 0 || target === void 0) return UNKNOWN;
1459
- return session_owner.toLowerCase() === target.toLowerCase() ? FIRST_PARTY : THIRD_PARTY;
1460
- }
1461
- function target_owner(owner_repo) {
1462
- return repo_origin.parse_origin_url(`https://${repo_origin.GITHUB_HOST}/${owner_repo}`)?.owner;
1463
- }
1464
- function current_owner(repository_path = process.cwd()) {
1465
- return repo_discovery.resolve_current_owner(repository_path);
1466
- }
1467
- var repo_party = { FIRST_PARTY, THIRD_PARTY, UNKNOWN, classify, current_owner, target_owner };
1468
-
1469
- // scripts/rules/third-party-write.ts
1470
- function is_third_party_write_segment(segment, session_owner) {
1471
- if (!gh_api.is_gh_api(segment) || !gh_api.is_write(segment)) return false;
1472
- const target = gh_api.repo_target(segment);
1473
- if (target === void 0) return false;
1474
- return repo_party.classify(session_owner, target.owner) === repo_party.THIRD_PARTY;
1475
- }
1476
- function writes_third_party(command, resolve_owner = repo_party.current_owner) {
1477
- const targets = shell_segments.segments_of(command).filter((segment) => gh_api.is_gh_api(segment) && gh_api.is_write(segment));
1478
- if (targets.length === 0) return false;
1479
- const session_owner = resolve_owner();
1480
- return targets.some((segment) => is_third_party_write_segment(segment, session_owner));
1481
- }
1482
- 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.**';
1483
- var ROW10 = {
1484
- id: "third-party-write",
1485
- is_trigger: bash_triggers.on_bash_command((command) => writes_third_party(command)),
1486
- reason: THIRD_PARTY_WRITE_REASON,
1487
- decide: () => true
1488
- };
1489
- var third_party_write = {
1490
- ROW: ROW10,
1491
- THIRD_PARTY_WRITE_REASON,
1492
- is_third_party_write_segment,
1493
- writes_third_party
1494
- };
1495
-
1496
- // scripts/rules/worktree-guard.ts
1497
- var CHECKOUT_SUBCOMMAND = "checkout";
1498
- var RESTORE_SUBCOMMAND = "restore";
1499
- var STASH_SUBCOMMAND = "stash";
1500
- var PATHSPEC_SEPARATOR = "--";
1501
- function is_worktree_checkout(args) {
1502
- return args.includes(PATHSPEC_SEPARATOR);
1503
- }
1504
- var STAGED_LONG = "--staged";
1505
- var STAGED_LETTER = "S";
1506
- var WORKTREE_LONG = "--worktree";
1507
- var WORKTREE_LETTER = "W";
1508
- function touches_worktree(args) {
1509
- const is_staged = args.some(
1510
- (argument) => argument === STAGED_LONG || git_argv.short_cluster_has(argument, STAGED_LETTER)
1511
- );
1512
- const is_worktree = args.some(
1513
- (argument) => argument === WORKTREE_LONG || git_argv.short_cluster_has(argument, WORKTREE_LETTER)
1514
- );
1515
- return !is_staged || is_worktree;
1516
- }
1517
- var PUSH_STASH = "push";
1518
- var MESSAGE_LONG = "--message";
1519
- var MESSAGE_SHORT_LETTER = "m";
1520
- var READ_ONLY_STASH = /* @__PURE__ */ new Set(["list", "show"]);
1521
- function has_message(args) {
1522
- return args.some(
1523
- (argument) => argument === MESSAGE_LONG || argument.startsWith(`${MESSAGE_LONG}=`) || git_argv.short_cluster_has(argument, MESSAGE_SHORT_LETTER)
1524
- );
1525
- }
1526
- function is_unauthorized_stash(args) {
1527
- const [stash_subcommand, ...rest] = args;
1528
- if (stash_subcommand === void 0) return true;
1529
- if (READ_ONLY_STASH.has(stash_subcommand)) return false;
1530
- if (stash_subcommand === PUSH_STASH) return !has_message(rest);
1531
- return true;
1532
- }
1533
- function is_unauthorized_segment(segment) {
1534
- const call = git_argv.parse(segment);
1535
- if (call === void 0) return false;
1536
- if (call.subcommand === CHECKOUT_SUBCOMMAND) return is_worktree_checkout(call.args);
1537
- if (call.subcommand === RESTORE_SUBCOMMAND) return touches_worktree(call.args);
1538
- return call.subcommand === STASH_SUBCOMMAND && is_unauthorized_stash(call.args);
1539
- }
1540
- function is_unauthorized_worktree_change(command) {
1541
- return shell_segments.segments_of(command).some((segment) => is_unauthorized_segment(segment));
1542
- }
1543
- var WORKTREE_MUTATION_REASON = '\u26D4 unauthorized working-tree change: `git checkout -- <path>`, `git restore <path>` and an unauthorized `git stash` mutate the working tree or 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. **This rule fires on every occurrence, not once per run.**';
1544
- var ROW11 = {
1545
- id: "worktree-mutation",
1546
- is_trigger: bash_triggers.on_bash_command(is_unauthorized_worktree_change),
1547
- reason: WORKTREE_MUTATION_REASON,
1548
- decide: () => true
1549
- };
1550
- var worktree_guard = {
1551
- ROW: ROW11,
1552
- WORKTREE_MUTATION_REASON,
1553
- is_unauthorized_stash,
1554
- is_unauthorized_worktree_change,
1555
- is_worktree_checkout,
1556
- touches_worktree
1557
- };
1558
-
1559
- // scripts/rules/delivered-rules.ts
1560
- var SWITCH_ENV_KEY = "JOSH_RULE_GUARD";
1561
- var STAMP_PREFIX = "josh-rule-guard-";
1562
- var { is_issue_filing: is_issue_filing2, on_bash_command: on_bash_command2 } = bash_triggers;
1563
- var WIP_CAP_REASON = "\u26D4 backlog WIP cap: count the target repository's open Issues before filing. With more than 30 open, close one first; nothing honestly closable means do not file. Two filings are exempt and proceed while stating the overage \u2014 one the run is blocked by, and an interrupt, decided by three tests rather than judgement: a verification answers wrongly, a documented workflow cannot complete, or data is lost or written outside the repository. Meeting none of the three, the finding is discretionary and waits. The count command and both procedures are in `prompts/collaboration-workflow/wip-cap.md`. Reissue this call once you have counted \u2014 it fires once per run and cannot repeat on the call in hand.";
1564
- var ISSUE_VIEW_COMMAND = new RegExp(String.raw`^gh\s+${gh_api.GH_FLAGS}issue\s+view\s`, "u");
1565
- var ISSUE_BODY_PATH = /repos\/[^\s'"]*\/issues\/\d+(?=$|["'\s])/u;
1566
- var FETCHES_COMMENTS = /--comments\b|--json\s[\w,]*\bcomments\b|\/comments\b/u;
1567
- var ISSUES_PATH = /repos\/[^\s'"]*\/issues\//u;
1568
- var FIELD_PROJECTION = /(?:--jq|--json)[= ]\s*('[^']*'|"[^"]*"|\S+)/u;
1569
- var NAMES_THE_BODY = /\bbody\b/u;
1570
- function projects_away_body(segment) {
1571
- const projection = FIELD_PROJECTION.exec(segment);
1572
- return projection !== null && !NAMES_THE_BODY.test(projection[1] ?? "");
1573
- }
1574
- function is_body_read_segment(segment) {
1575
- if (projects_away_body(segment)) return false;
1576
- if (ISSUE_VIEW_COMMAND.test(segment)) return true;
1577
- return gh_api.is_gh_api(segment) && ISSUE_BODY_PATH.test(segment) && gh_api.is_read(segment);
1578
- }
1579
- function fetches_issue_comments(segment) {
1580
- if (!FETCHES_COMMENTS.test(segment)) return false;
1581
- return ISSUE_VIEW_COMMAND.test(segment) || gh_api.is_gh_api(segment) && ISSUES_PATH.test(segment);
1582
- }
1583
- function is_body_only_issue_read(command) {
1584
- const segments = shell_segments.segments_of(command);
1585
- if (segments.some((segment) => fetches_issue_comments(segment))) return false;
1586
- return segments.some((segment) => is_body_read_segment(segment));
1587
- }
1588
- var ISSUE_COMMENTS_REASON = "\u26D4 an Issue's comments are part of the Issue: read them before implementing, not only the body. 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. Reissue this read with the comments included: `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. 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/SKILL.md` \u2192 \"An Issue's comments are part of the Issue\". It fires once per run and cannot repeat on the call in hand.";
1589
- var { carries_a_body: carries_a_body2, is_shell_evaluated_body: is_shell_evaluated_body2, keeps_body_safe: keeps_body_safe2 } = shell_body_trigger;
1590
- 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 `$`. The shell evaluates both before the command runs, so the text is executed rather than merely mangled \u2014 joshuafolkken/kit#1198 recorded a PR comment whose own words ran as git commands and switched a lane's work tree onto main. Write the body to a file and pass it by path: `gh api repos/{owner}/{repo}/issues/<N>/comments --field body=@<path>` (a PR comment is an issue comment), `pnpm josh followup --notify-message-file <path>`, `pnpm josh notify --body-file <path>`, and `--body-file <path>` wherever a command offers it. `$'\u2026'` quoting is the other safe form and is unchanged. The rule and what the trigger cannot see 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.";
1591
- var ISSUE_LISTING = /^gh\s+(?:-{1,2}[\w-]+(?:[= ]\S+)?\s+)*issue\s+list\b/u;
1592
- var ISSUES_QUERY = /repos\/[^\s'"]*\/issues\?[^\s'"]*state=open/u;
1593
- var OPEN_STATE = /--state[= ]open|state=open/u;
1594
- var LABEL_FILTER = /--label\b|[?&]labels=/u;
1595
- function counts_open_issues(command) {
1596
- return shell_segments.segments_of(command).some((segment) => {
1597
- if (LABEL_FILTER.test(segment)) return false;
1598
- return ISSUE_LISTING.test(segment) && OPEN_STATE.test(segment) || ISSUES_QUERY.test(segment);
1599
- });
1600
- }
1601
- function reads_issue_comments(command) {
1602
- return shell_segments.segments_of(command).some((segment) => fetches_issue_comments(segment));
1603
- }
1604
- function reaches_the_pre_gate_boundary(call, _turn, run) {
1605
- if (!on_bash_command2(pre_gate_cut.asks_about_the_cut)(call)) return false;
1606
- return run.some((issued) => on_bash_command2(pre_gate_cut.claims_the_hold)(issued));
1607
- }
1608
- var DELIVERED_RULES = [
1609
- // **Listed first, ahead of the filing trigger it overlaps** (joshuafolkken/kit#2122). A `gh api`
1610
- // that files an Issue into a repository we do not own trips both this row and `wip-cap` /
1611
- // `issue-scout` / `filing-cap` — but the backlog count, the scout and the per-run cap are all about
1612
- // filing into *our own* backlog, so on a third-party target the right answer is to stop the write
1613
- // entirely rather than to count anything. Placing it first means a third-party write is refused
1614
- // before any of those speak; on a first-party filing it is silent (the owners match), so `wip-cap`
1615
- // still speaks first, exactly as before. It fires on every occurrence (`git-force.ts`).
1616
- third_party_write.ROW,
1617
- {
1618
- id: "wip-cap",
1619
- is_trigger: on_bash_command2(is_issue_filing2),
1620
- reason: WIP_CAP_REASON,
1621
- keeps: on_bash_command2(counts_open_issues)
1622
- },
1623
- // **Three rows share the filing trigger** (joshuafolkken/kit#2119), listed after `wip-cap` so the
1624
- // backlog count still speaks first: `issue-scout` refuses a filing the run has not scouted (once per
1625
- // run, stood down once the scout is on the tail), and `filing-cap` refuses every filing past the
1626
- // per-run ceiling. Each is a real instance of the admissible overlap the comment on `delivery`
1627
- // describes — the losing rule's delivery is still correct one reissue later — so a scout-less,
1628
- // over-cap filing is delivered `wip-cap`, then `issue-scout`, then `filing-cap` across its reissues.
1629
- issue_scout.ROW,
1630
- filing_cap.ROW,
1631
- // **`issue-fold` shares the same filing trigger** (joshuafolkken/kit#2213), listed after
1632
- // `filing-cap`: it refuses a *second* filing the run has not folded, once per run, stood down off
1633
- // the tail when the run has already folded or has filed nothing earlier. It is another instance of
1634
- // the admissible overlap — the losing rule's delivery is still correct one reissue later — so a
1635
- // scout-less, unfolded, over-cap second filing is delivered `wip-cap`, then `issue-scout`, then
1636
- // `filing-cap`, then `issue-fold` across its reissues.
1637
- issue_fold.ROW,
1638
- {
1639
- id: "issue-comments",
1640
- is_trigger: on_bash_command2(is_body_only_issue_read),
1641
- reason: ISSUE_COMMENTS_REASON,
1642
- already_satisfied: prior_comment_read.already_read_for_call,
1643
- keeps: on_bash_command2(reads_issue_comments)
1644
- },
1645
- {
1646
- id: "shell-body",
1647
- is_trigger: on_bash_command2(is_shell_evaluated_body2),
1648
- reason: SHELL_BODY_REASON,
1649
- keeps: on_bash_command2(keeps_body_safe2),
1650
- reaches: on_bash_command2(carries_a_body2)
1651
- },
1652
- {
1653
- id: "piped-verification",
1654
- is_trigger: on_bash_command2(piped_verification.is_masked_verification),
1655
- reason: piped_verification.PIPED_VERIFICATION_REASON,
1656
- keeps: on_bash_command2(piped_verification.keeps_verdict_intact),
1657
- reaches: on_bash_command2(piped_verification.runs_verification)
1658
- },
1659
- {
1660
- id: "early-heartbeat",
1661
- is_trigger: on_bash_command2(early_heartbeat.is_wait_timer),
1662
- reason: early_heartbeat.EARLY_HEARTBEAT_REASON,
1663
- decide: early_heartbeat.decide,
1664
- keeps: on_bash_command2(early_heartbeat.is_progress_watch),
1665
- reaches: on_bash_command2(early_heartbeat.waits_for_progress)
1666
- },
1667
- // **Listed before `run-tail`, the one row it deliberately overlaps** (joshuafolkken/kit#2118). A
1668
- // foreground `pnpm josh git -y` with no test beside the change is claimed by both: `test-declared`
1669
- // decides whether the commit should happen at all, so it is listed first, exactly as `wip-cap`
1670
- // precedes `shell-body` for deciding whether an Issue should exist. Nothing is lost by `run-tail`
1671
- // losing the race — the stamps are keyed per `id`, so the reissued push is delivered `run-tail`,
1672
- // which is asserted rather than assumed. The overlap only arises in a real checkout where the
1673
- // verdict is `required`; in the hermetic non-repo suite the git read fails to `exempt`, so
1674
- // `test-declared` claims nothing and the "exactly one rule" invariant over `run-tail`'s fixture holds.
1675
- test_declared_commit.ROW,
1676
- // **The one row whose trigger reads a field of the input beside the command**, so it supplies its
1677
- // own tool-name check rather than going through `on_bash_command`: a push step already issued with
1678
- // `run_in_background` is the rule obeyed, and refusing it would charge a run for doing the right
1679
- // thing. It supplies `decide` because a push is a recurring act — one per child in a batch, and a
1680
- // second inside one `fullrun` when round 2 fixes a finding in place.
1681
- {
1682
- id: "run-tail",
1683
- is_trigger: run_tail.is_foreground_push_step,
1684
- reason: run_tail.RUN_TAIL_REASON,
1685
- decide: run_tail.decide,
1686
- keeps: run_tail.is_backgrounded_push_step,
1687
- reaches: run_tail.is_push_step_call
1688
- },
1689
- // **The one row whose trigger consults the world beside the command**, because "is this a lane that
1690
- // has not cut" is not readable from the call: it is the working directory's own name and the cut
1691
- // record on disk. Both reads are synchronous, and the command match runs first, so a run that never
1692
- // types `pnpm josh gate` pays nothing for it.
1693
- //
1694
- // **Once per run rather than `decide`, and the direction of the error is why.** Taking the cut ends
1695
- // the process, so this is not the recurring act joshuafolkken/kit#1570 wrote `decide` for — and the
1696
- // five verdicts that legitimately leave a run at the gate (`not-a-lane`, `unready`, `busy`,
1697
- // `failed`, `unknown`) all need the reissue to go through. A row that refused every time would wedge exactly
1698
- // those runs; a row that refuses once cannot.
1699
- //
1700
- // **`pnpm josh rule:value` read this row with a ceiling of about 50%, and joshuafolkken/kit#1867 is
1701
- // what removed it** (the ceiling was recorded here by joshuafolkken/kit#1864's review round 2). A
1702
- // cut relaunches a *new session*, which the measurement groups as a run of its own, and that
1703
- // resumed run issues the entry check — so scored on the asking alone it joined the denominator
1704
- // while never being able to satisfy `keeps`, which only the cutting run can, and ten perfectly
1705
- // obedient children read as ten kept out of twenty. **The two halves are separable, just not from
1706
- // one call**: both processes issue byte-identical `--resume` strings, but the `fresh` verdict sends
1707
- // a run on to claim the working-tree hold while the `resume` verdict tells its counterpart to skip
1708
- // that claim. `reaches_the_pre_gate_boundary` is that conjunction, and it is why `CallTest` is
1709
- // handed the run's calls beside the turn's rather than a second predicate slot of its own.
1710
- // Dropping `reaches` altogether is still strictly worse — the trigger evaluates against the
1711
- // *measuring* process's working directory, so it is always false from the main checkout and the row
1712
- // would read as no runs at all.
1713
- //
1714
- // **It overlaps `piped-verification` on a piped gate call, and the order is safe in both
1715
- // directions.** `pnpm josh gate | tail` in an uncut lane is a real instance of both defects; the
1716
- // earlier row speaks, its own reason says to reissue, and this one delivers on the reissue — the
1717
- // reading "the losing rule's delivery is still correct one call later" that the comment below
1718
- // requires of any admissible overlap.
1719
- {
1720
- id: "pre-gate-cut",
1721
- is_trigger: on_bash_command2(pre_gate_cut.is_uncut_gate),
1722
- reason: pre_gate_cut.PRE_GATE_CUT_REASON,
1723
- keeps: on_bash_command2(pre_gate_cut.takes_the_cut),
1724
- reaches: reaches_the_pre_gate_boundary
1725
- },
1726
- // **The second row whose trigger consults the world beside the command** (joshuafolkken/kit#2034):
1727
- // "is this a dispatched lane child" is the dispatch mark against this checkout's own issue, read
1728
- // synchronously by `lane_child_marker.is_child_of`, and the command match runs first so an ordinary
1729
- // run pays nothing for it.
1730
- //
1731
- // **Once per run rather than `decide`, and no `already_satisfied`.** The stop ends the turn, so it
1732
- // is not the recurring act joshuafolkken/kit#1570 wrote `decide` for. It carries no
1733
- // `already_satisfied` because the park it asks for is a GitHub label a synchronous guard cannot
1734
- // read, and the transcript-tail alternative would read a child that merely *read*
1735
- // `backlogrun-park.md` — whose prose carries the `labels[]=needs-decision` command — as compliant
1736
- // and fall silent on a real violation. `lane-park.ts` states why one wasted reissue on the compliant
1737
- // path is the safe direction. `keeps` reads an actual label-application `Bash` command, never the
1738
- // tail, so it is not fooled the same way; `reaches` stays `is_trigger`, since a compliant run issues
1739
- // the same confirmation notify and belongs in the denominator.
1740
- {
1741
- id: "lane-park",
1742
- is_trigger: on_bash_command2(lane_park.is_unparked_stop),
1743
- reason: lane_park.LANE_PARK_REASON,
1744
- keeps: on_bash_command2(lane_park.records_the_park)
1745
- },
1746
- // **joshuafolkken/kit#2034's rule, one tool-call earlier** (joshuafolkken/kit#2201). `lane-park`
1747
- // fires on the `confirmation` notify a routed child reaches; a child that reaches for
1748
- // `AskUserQuestion` never gets there, because the harness ends its turn at the refused ask. This
1749
- // row's trigger is that ask, so the refusal becomes an instruction — park the question — instead of
1750
- // the child's death. Its trigger reads the input's tool name and consults the dispatch mark, like
1751
- // `lane-park`, and shares no command with any Bash row, so it claims no `gh` / `git` / `josh` call.
1752
- // It fires on every occurrence (`decide` returns true), the disposition `git-force.ts` takes: an
1753
- // interactive ask must never succeed in a child, and the route is always the park, never a reissue.
1754
- lane_interactive_ask.ROW,
1755
- // **The parent budget commands a lane child must never run** (joshuafolkken/kit#2267). A dispatched
1756
- // child that ran `pnpm josh run:merge` read its `busy` as a competing session and parked its Issue
1757
- // unimplemented — but the live carry record is the parent that launched it, never a competitor. This
1758
- // row refuses `run:merge` / `run:carry` in a lane child and hands back the mechanical branch: the
1759
- // record is the parent's, so continue implementing rather than park or ask. Its trigger claims a
1760
- // `josh` subcommand no other row does (the filing rows are `gh`, `run-tail` is `git push`), so it
1761
- // overlaps nothing. It fires on every occurrence (`decide` returns true): a child must never run
1762
- // these, and the route is always "continue", never a reissue (`git-force.ts`).
1763
- lane_carry_conflict.ROW,
1764
- // **The three Bash-string gaps the deny glob cannot express** (joshuafolkken/kit#2120), each reading
1765
- // the command's argv rather than a literal a glob keys on. They share no trigger with any row above —
1766
- // force/delete is `git push` / `git branch`, the worktree change is `git checkout` / `restore` /
1767
- // `stash`, and the file body is a heredoc / `node -e` / `perl -i` — so no two rows claim one command,
1768
- // which `delivered-rules-bash.test.ts` pins. Each declares `decide` returning true: a destructive or
1769
- // billed act is refused on every occurrence, never once per run (`git-force.ts`).
1770
- git_force.ROW,
1771
- worktree_guard.ROW,
1772
- file_body.ROW,
1773
- // **The first row whose trigger is an `Edit` / `Write` rather than a shell call**
1774
- // (joshuafolkken/kit#2272). It delivers the residency questions at the edit that writes a rule into
1775
- // prose, and self-gates on the tool name and the file path, so it claims no `Bash` command any row
1776
- // above matches — the `rules_claiming` invariant over Bash fixtures is untouched. It carries no
1777
- // `decide`, so it is once per run and takes the batching stand-aside `is_first_delivery` gives every
1778
- // default row: the batching guard treats an `Edit` as a candidate, exactly as it treats an Issue
1779
- // read, so this row stands aside on a batched turn and delivers on the reissue.
1780
- rule_body_guard.ROW
1781
- ];
1782
- var ALONE_IN_TURN = 1;
1783
- function batches_the_turn(call, turn) {
1784
- return time_batch_guard.is_guarded_call(call) && turn.length > ALONE_IN_TURN;
1785
- }
1786
- var BATCHING_RULE = {
1787
- id: "batching",
1788
- reason: time_batch_guard.REASON,
1789
- keeps: batches_the_turn,
1790
- reaches: time_batch_guard.is_guarded_call
1791
- };
1792
- var INVESTIGATION_RULE = {
1793
- id: "investigation",
1794
- reason: investigation_reads.REASON,
1795
- reaches: investigation_reads.is_refusable_call
1796
- };
1797
- var MEASURED_RULES = [
1798
- ...DELIVERED_RULES,
1799
- BATCHING_RULE,
1800
- INVESTIGATION_RULE
1801
- ];
1802
- var BATCH_STAMP = hook_decision.create_refusal_stamp(time_batch_guard.STAMP_PREFIX);
1803
- var BATCH_REFUSAL_WINDOW_MS = 1e4;
1804
- function will_batch_guard_refuse(tail, call, run) {
1805
- if (lane_guard_policy.mode_here("batching") !== "refuse") return false;
1806
- const refused_at_ms = BATCH_STAMP.last_ms(BATCH_STAMP.path(run.transcript));
1807
- if (run.now_ms - refused_at_ms < BATCH_REFUSAL_WINDOW_MS) return true;
1808
- return time_batch_guard.should_block(tail, call, refused_at_ms);
1809
- }
1810
- function is_first_delivery(tail, call, delivered_at_ms, run) {
1811
- return delivered_at_ms === hook_decision.NEVER_MS && !will_batch_guard_refuse(tail, call, run);
1812
- }
1813
- function delivery_decision(rule) {
1814
- const { decide: decide4, already_satisfied } = rule;
1815
- if (decide4 === void 0 && already_satisfied === void 0) return is_first_delivery;
1816
- return function should_block(tail, call, delivered_at_ms, run) {
1817
- if (already_satisfied?.(tail, call) === true) return false;
1818
- if (decide4 === void 0) return is_first_delivery(tail, call, delivered_at_ms, run);
1819
- return decide4(call, run, !will_batch_guard_refuse(tail, call, run));
1820
- };
1821
- }
1822
- function guard_of(rule) {
1823
- return hook_decision.create_transcript_guard({
1824
- prefix: `${STAMP_PREFIX}${rule.id}-`,
1825
- switch_key: SWITCH_ENV_KEY,
1826
- is_candidate: rule.is_trigger,
1827
- should_block: delivery_decision(rule),
1828
- reason: rule.reason
1829
- });
1830
- }
1831
- var GUARDS = new Map(DELIVERED_RULES.map((rule) => [rule.id, guard_of(rule)]));
1832
- function delivery_path(rule_id, transcript_path) {
1833
- return GUARDS.get(rule_id)?.refusal_path(transcript_path) ?? "";
1834
- }
1835
- function delivery(raw_payload, now_ms = Date.now()) {
1836
- for (const guard of GUARDS.values()) {
1837
- const reason = guard.refusal(raw_payload, now_ms);
1838
- if (reason !== void 0) return reason;
1839
- }
1840
- return void 0;
1841
- }
1842
- function is_enabled() {
1843
- return hook_decision.is_switch_enabled(SWITCH_ENV_KEY);
1844
- }
1845
- var delivered_rules = {
1846
- DELIVERED_RULES,
1847
- EARLY_HEARTBEAT_REASON: early_heartbeat.EARLY_HEARTBEAT_REASON,
1848
- FILE_BODY_REASON: file_body.FILE_BODY_REASON,
1849
- FILING_CAP_REASON: filing_cap.FILING_CAP_REASON,
1850
- GIT_FORCE_REASON: git_force.GIT_FORCE_REASON,
1851
- ISSUE_COMMENTS_REASON,
1852
- ISSUE_FOLD_REASON: issue_fold.ISSUE_FOLD_REASON,
1853
- ISSUE_SCOUT_REASON: issue_scout.ISSUE_SCOUT_REASON,
1854
- LANE_INTERACTIVE_ASK_REASON: lane_interactive_ask.LANE_INTERACTIVE_ASK_REASON,
1855
- LANE_PARK_REASON: lane_park.LANE_PARK_REASON,
1856
- MEASURED_RULES,
1857
- PIPED_VERIFICATION_REASON: piped_verification.PIPED_VERIFICATION_REASON,
1858
- PRE_GATE_CUT_REASON: pre_gate_cut.PRE_GATE_CUT_REASON,
1859
- RUN_TAIL_REASON: run_tail.RUN_TAIL_REASON,
1860
- SHELL_BODY_REASON,
1861
- SWITCH_ENV_KEY,
1862
- THIRD_PARTY_WRITE_REASON: third_party_write.THIRD_PARTY_WRITE_REASON,
1863
- WIP_CAP_REASON,
1864
- WORKTREE_MUTATION_REASON: worktree_guard.WORKTREE_MUTATION_REASON,
1865
- delivery,
1866
- delivery_path,
1867
- is_body_only_issue_read,
1868
- is_enabled,
1869
- is_issue_filing: is_issue_filing2,
1870
- is_masked_verification: piped_verification.is_masked_verification,
1871
- is_wait_timer: early_heartbeat.is_wait_timer
1872
- };
1873
-
1874
- // scripts/hooks/pretool-guard.ts
1875
- function combine_outcomes(batch, extra_reason) {
1876
- if (batch.reason !== void 0) return batch;
1877
- if (extra_reason !== void 0) {
1878
- return { reason: extra_reason, notice: void 0, fault: void 0 };
1879
- }
1880
- return batch;
1881
- }
1882
- function pretool_outcome(raw_payload) {
1883
- const batch = batch_outcome(raw_payload);
1884
- const investigation = investigation_refusal(raw_payload);
1885
- const rule = delivered_rules.delivery(raw_payload);
1886
- return combine_outcomes(batch, investigation ?? rule);
1887
- }
1888
- var pretool_guard = { combine_outcomes, pretool_outcome };
1889
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
1890
- if (process.stdin.isTTY) hook_decision.report_no_payload("pretool:guard");
1891
- else hook_decision.write_outcome(await text(process.stdin), pretool_outcome);
1892
- }
1893
-
1894
- export {
1895
- pretool_outcome,
1896
- pretool_guard
1897
- };