@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,3085 +0,0 @@
1
- import {
2
- CONTEXT_CUT_THRESHOLD,
3
- RECENT_REQUEST_WINDOW,
4
- git_utilities,
5
- lane_park,
6
- run_cut,
7
- shell_segments
8
- } from "./chunk-XKDOYJGA.js";
9
- import {
10
- batch_outcome
11
- } from "./chunk-RZKYW5P2.js";
12
- import {
13
- investigation_refusal
14
- } from "./chunk-TCGNI2ZY.js";
15
- import {
16
- duplicate_read_outcome
17
- } from "./chunk-C25HRMJX.js";
18
- import {
19
- investigation_reads
20
- } from "./chunk-BINZ3PLI.js";
21
- import {
22
- lane_guard_policy,
23
- time_batch_guard
24
- } from "./chunk-7INSJDGE.js";
25
- import {
26
- hook_decision,
27
- lane_child_marker
28
- } from "./chunk-IKUMBDJB.js";
29
- import {
30
- cost_attribute,
31
- cost_blocks,
32
- cost_transcript,
33
- cost_usage,
34
- json_value,
35
- lane_paths,
36
- stamp_file,
37
- time_density_hook,
38
- time_shell,
39
- time_transcript_line
40
- } from "./chunk-RORDOD4S.js";
41
-
42
- // scripts/hooks/pretool-guard.ts
43
- import { text } from "node:stream/consumers";
44
- import { fileURLToPath as fileURLToPath2 } from "node:url";
45
-
46
- // scripts/rules/bash-triggers.ts
47
- function on_bash_command(is_match) {
48
- return function is_trigger(call) {
49
- if (call.name !== cost_blocks.BASH_TOOL) return false;
50
- return is_match(time_shell.bash_command(call.input));
51
- };
52
- }
53
- var ISSUE_CREATE_COMMAND = /\bgh\s+(?:\S+\s+)*?issue\s+create\b/u;
54
- var ISSUES_ENDPOINT = /repos\/[^\s'"]*\/issues(?=$|["'\s])/u;
55
- var TITLE_FIELD = /(?:-f|-F|--field|--raw-field)\s*'?title=/u;
56
- function is_issue_filing(command) {
57
- if (ISSUE_CREATE_COMMAND.test(command)) return true;
58
- return ISSUES_ENDPOINT.test(command) && TITLE_FIELD.test(command);
59
- }
60
- var bash_triggers = { is_issue_filing, on_bash_command };
61
-
62
- // scripts/run/run-progress.ts
63
- var DEFAULT_INTERVAL_MINUTES = 20;
64
- var INTERVAL_KEY = "JOSH_PROGRESS_INTERVAL_MINUTES";
65
- var MS_PER_MINUTE = 6e4;
66
- var MS_PER_SECOND = 1e3;
67
- var DEFAULT_INTERVAL_MS = DEFAULT_INTERVAL_MINUTES * MS_PER_MINUTE;
68
- var NO_LANES = "none";
69
- var NO_CHILD_YET = "no in-progress child yet";
70
- var NO_NEXT = "none";
71
- var QUIET_MARKER = "\u23F3";
72
- var STAMP_END = 16;
73
- var PAD_WIDTH = 2;
74
- var MINUTES_PER_HOUR = 60;
75
- function to_minutes(elapsed_ms) {
76
- return Math.floor(elapsed_ms / MS_PER_MINUTE);
77
- }
78
- function format_minutes(elapsed_ms) {
79
- return `${String(to_minutes(elapsed_ms))}m`;
80
- }
81
- function pad(value) {
82
- return String(value).padStart(PAD_WIDTH, "0");
83
- }
84
- function offset_minutes(date) {
85
- return Math.round(-date.getTimezoneOffset());
86
- }
87
- function format_offset(total_minutes) {
88
- const sign = total_minutes < 0 ? "-" : "+";
89
- const absolute = Math.abs(total_minutes);
90
- return `${sign}${pad(Math.floor(absolute / MINUTES_PER_HOUR))}:${pad(absolute % MINUTES_PER_HOUR)}`;
91
- }
92
- function format_observed_at(now_ms) {
93
- const minutes = offset_minutes(new Date(now_ms));
94
- const local = new Date(now_ms + minutes * MS_PER_MINUTE);
95
- const day = `${String(local.getUTCFullYear())}-${pad(local.getUTCMonth() + 1)}-${pad(local.getUTCDate())}`;
96
- const clock = `${pad(local.getUTCHours())}:${pad(local.getUTCMinutes())}${format_offset(minutes)}`;
97
- return `${day} ${clock} / ${new Date(now_ms).toISOString().slice(0, STAMP_END)}Z`;
98
- }
99
- function observation_key(observations) {
100
- return JSON.stringify({ children: observations.children, lanes: observations.lanes });
101
- }
102
- function next_state(previous, key, now_ms) {
103
- if (previous?.key === key) return previous;
104
- return { key, unchanged_since_ms: now_ms };
105
- }
106
- function format_child(child) {
107
- const labels = child.labels.length > 0 ? child.labels.join(",") : "(no labels)";
108
- return `#${child.issue} ${labels} PR:${child.pr_state}`;
109
- }
110
- function format_children(children) {
111
- if (children.length === 0) return NO_CHILD_YET;
112
- return children.map((child) => format_child(child)).join(" \xB7 ");
113
- }
114
- function format_lanes(lanes) {
115
- if (lanes.length === 0) return NO_LANES;
116
- return lanes.map((lane) => `${lane.issue}:${lane.state}`).join(" ");
117
- }
118
- function format_record(record_age_ms) {
119
- return record_age_ms === void 0 ? "unread" : `+${format_minutes(record_age_ms)}`;
120
- }
121
- function format_next_report(now_ms, interval_ms) {
122
- return format_observed_at(now_ms + interval_ms);
123
- }
124
- function format_next_line(next) {
125
- return `next ${next ?? NO_NEXT}`;
126
- }
127
- function format_report(fields) {
128
- return [
129
- `${QUIET_MARKER} at ${fields.observed_at}`,
130
- `quiet ${fields.quiet} \xB7 unchanged ${fields.unchanged}`,
131
- `children ${fields.children}`,
132
- `lanes ${fields.lanes} \xB7 load ${fields.load} \xB7 record ${fields.record}`,
133
- format_next_line(fields.next)
134
- ].join("\n");
135
- }
136
- function format_line(observations, timing) {
137
- return format_report({
138
- observed_at: format_observed_at(timing.now_ms),
139
- quiet: format_minutes(timing.now_ms - timing.quiet_since_ms),
140
- unchanged: format_minutes(timing.now_ms - timing.unchanged_since_ms),
141
- children: format_children(observations.children),
142
- lanes: format_lanes(observations.lanes),
143
- load: observations.load_average.toFixed(1),
144
- record: format_record(observations.record_age_ms),
145
- next: format_next_report(timing.now_ms, timing.interval_ms)
146
- });
147
- }
148
- function is_due(last_report_ms, now_ms, interval_ms) {
149
- return now_ms - last_report_ms >= interval_ms;
150
- }
151
- function minutes_from(raw) {
152
- const minutes = Number(raw);
153
- if (raw === void 0 || raw.trim() === "" || !Number.isFinite(minutes) || minutes <= 0) {
154
- return void 0;
155
- }
156
- return minutes;
157
- }
158
- var run_progress = {
159
- DEFAULT_INTERVAL_MINUTES,
160
- DEFAULT_INTERVAL_MS,
161
- INTERVAL_KEY,
162
- MS_PER_MINUTE,
163
- MS_PER_SECOND,
164
- NO_CHILD_YET,
165
- NO_NEXT,
166
- format_child,
167
- format_children,
168
- format_lanes,
169
- format_line,
170
- format_next_line,
171
- format_next_report,
172
- format_record,
173
- format_report,
174
- is_due,
175
- minutes_from,
176
- next_state,
177
- observation_key
178
- };
179
-
180
- // scripts/run/run-progress-clock.ts
181
- import { execaSync } from "execa";
182
- import { z } from "zod";
183
- var PROGRESS_PREFIX = "josh-run-progress-";
184
- var LIFE_PREFIX = "josh-run-progress-life-";
185
- var LOG_SUFFIX = ".log";
186
- var LOG_KEEP = 12;
187
- var MS_PER_MINUTE2 = 6e4;
188
- var LOG_STALE_MINUTES = 90;
189
- var LOG_STALE_MS = LOG_STALE_MINUTES * MS_PER_MINUTE2;
190
- var BLOCK_SEPARATOR = "\n\n";
191
- var GIT_TIMEOUT_MS = 5e3;
192
- var GIT_DIRECTORY_ARGUMENTS = ["rev-parse", "--absolute-git-dir"];
193
- var report_stamp_schema = z.object({ reported_at: z.string(), line: z.string().optional() });
194
- function parse_stamp(raw) {
195
- const parsed = report_stamp_schema.safeParse(JSON.parse(raw));
196
- if (!parsed.success) return void 0;
197
- const reported_at = Date.parse(parsed.data.reported_at);
198
- return Number.isNaN(reported_at) ? void 0 : reported_at;
199
- }
200
- function read_last_line(target) {
201
- const raw = stamp_file.read_stamp_text(target);
202
- if (raw === void 0) return void 0;
203
- try {
204
- return report_stamp_schema.safeParse(JSON.parse(raw)).data?.line;
205
- } catch {
206
- return void 0;
207
- }
208
- }
209
- function read_last_report(target) {
210
- const raw = stamp_file.read_stamp_text(target);
211
- if (raw === void 0) return void 0;
212
- try {
213
- return parse_stamp(raw);
214
- } catch {
215
- return void 0;
216
- }
217
- }
218
- function log_path_of(target) {
219
- return `${target}${LOG_SUFFIX}`;
220
- }
221
- function is_log_stale(target, now_ms) {
222
- const previous_ms = read_last_report(target);
223
- return previous_ms === void 0 || now_ms - previous_ms > LOG_STALE_MS;
224
- }
225
- function recent_blocks(log_target, is_fresh) {
226
- if (is_fresh) return [];
227
- const raw = stamp_file.read_stamp_text(log_target);
228
- if (raw === void 0) return [];
229
- return raw.split(BLOCK_SEPARATOR).map((block) => block.trimEnd()).filter((block) => block.length > 0);
230
- }
231
- function append_line(log_target, reported_at, line, is_fresh) {
232
- const block = `${reported_at}
233
- ${line}`;
234
- const blocks = [...recent_blocks(log_target, is_fresh), block].slice(-LOG_KEEP);
235
- stamp_file.write_text_stamp(log_target, `${blocks.join(BLOCK_SEPARATOR)}
236
- `);
237
- }
238
- function mark(target, now_ms, line) {
239
- const reported_at = new Date(now_ms).toISOString();
240
- const kept = line ?? read_last_line(target);
241
- if (line !== void 0) {
242
- append_line(log_path_of(target), reported_at, line, is_log_stale(target, now_ms));
243
- }
244
- stamp_file.write_stamp(target, kept === void 0 ? { reported_at } : { reported_at, line: kept });
245
- }
246
- function stamp_target_of(git_directory) {
247
- return stamp_file.stamp_path(PROGRESS_PREFIX, git_directory);
248
- }
249
- function life_target_of(git_directory) {
250
- return stamp_file.stamp_path(LIFE_PREFIX, git_directory);
251
- }
252
- var life_schema = z.object({ alive: z.literal(true), pinged_at: z.string().optional() });
253
- function begin_life(target) {
254
- stamp_file.write_stamp(target, { alive: true, pinged_at: (/* @__PURE__ */ new Date()).toISOString() });
255
- }
256
- function ping_life(target) {
257
- begin_life(target);
258
- }
259
- function parse_pinged_at(raw) {
260
- const parsed = life_schema.safeParse(JSON.parse(raw));
261
- if (!parsed.success || parsed.data.pinged_at === void 0) return void 0;
262
- const pinged_ms = Date.parse(parsed.data.pinged_at);
263
- return Number.isNaN(pinged_ms) ? void 0 : pinged_ms;
264
- }
265
- function is_life_fresh(target, threshold_ms) {
266
- const raw = stamp_file.read_stamp_text(target);
267
- if (raw === void 0) return false;
268
- try {
269
- const pinged_ms = parse_pinged_at(raw);
270
- return pinged_ms !== void 0 && Date.now() - pinged_ms < threshold_ms;
271
- } catch {
272
- return false;
273
- }
274
- }
275
- function is_life_ended(target) {
276
- return stamp_file.read_stamp_text(target) === void 0;
277
- }
278
- function end_life(target) {
279
- stamp_file.remove_stamp(target);
280
- }
281
- function git_directory_sync() {
282
- const git_binary = git_utilities.get_git_command_for_spawn();
283
- const result = execaSync(git_binary, GIT_DIRECTORY_ARGUMENTS, {
284
- reject: false,
285
- timeout: GIT_TIMEOUT_MS
286
- });
287
- if (result.exitCode !== 0) return void 0;
288
- const output = result.stdout.trim();
289
- return output === "" ? void 0 : output;
290
- }
291
- function stamp_target_sync() {
292
- const directory = git_directory_sync();
293
- return directory === void 0 ? void 0 : stamp_target_of(directory);
294
- }
295
- function read_last_report_sync() {
296
- const target = stamp_target_sync();
297
- return target === void 0 ? void 0 : read_last_report(target);
298
- }
299
- var run_progress_clock = {
300
- LIFE_PREFIX,
301
- LOG_KEEP,
302
- LOG_STALE_MS,
303
- PROGRESS_PREFIX,
304
- append_line,
305
- begin_life,
306
- end_life,
307
- is_life_ended,
308
- is_life_fresh,
309
- life_target_of,
310
- log_path_of,
311
- mark,
312
- parse_stamp,
313
- ping_life,
314
- read_last_line,
315
- read_last_report,
316
- read_last_report_sync,
317
- stamp_target_of,
318
- stamp_target_sync
319
- };
320
-
321
- // scripts/run/run-progress-config.ts
322
- import { readFileSync } from "node:fs";
323
- import path from "node:path";
324
- import { z as z2 } from "zod";
325
- var PACKAGE_FILE = "package.json";
326
- var CONFIG_FIELD = "josh";
327
- var INTERVAL_FIELD = "progress_interval_minutes";
328
- var MAX_DEPTH = 10;
329
- var package_schema = z2.object({
330
- josh: z2.object({ progress_interval_minutes: z2.number().positive() }).optional()
331
- });
332
- function ancestors_of(start) {
333
- const directories = [];
334
- let current = path.resolve(start);
335
- while (directories.length < MAX_DEPTH && !directories.includes(current)) {
336
- directories.push(current);
337
- current = path.dirname(current);
338
- }
339
- return directories;
340
- }
341
- function read_declared_minutes(directory) {
342
- try {
343
- const raw = readFileSync(path.join(directory, PACKAGE_FILE), "utf8");
344
- const parsed = package_schema.safeParse(JSON.parse(raw));
345
- return parsed.success ? parsed.data.josh?.progress_interval_minutes : void 0;
346
- } catch {
347
- return void 0;
348
- }
349
- }
350
- function read_package_minutes(start) {
351
- for (const directory of ancestors_of(start)) {
352
- const minutes = read_declared_minutes(directory);
353
- if (minutes !== void 0) return minutes;
354
- }
355
- return void 0;
356
- }
357
- function to_ms(minutes) {
358
- return minutes === void 0 ? void 0 : minutes * run_progress.MS_PER_MINUTE;
359
- }
360
- function resolve_interval_ms(raw, directory = process.cwd()) {
361
- 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;
362
- }
363
- function configured_interval_ms(directory = process.cwd()) {
364
- return resolve_interval_ms(void 0, directory);
365
- }
366
- var run_progress_config = {
367
- CONFIG_FIELD,
368
- INTERVAL_FIELD,
369
- PACKAGE_FILE,
370
- configured_interval_ms,
371
- read_package_minutes,
372
- resolve_interval_ms
373
- };
374
-
375
- // scripts/rules/early-heartbeat.ts
376
- var ARM_PREFIX = "josh-heartbeat-arm-";
377
- var SEGMENT_SEPARATOR = /&&|\|\||;|&|\n/u;
378
- var SLEEP_SEGMENT = /^sleep\s+(\d+(?:\.\d+)?)([smhd]?)$/u;
379
- var INERT_SEGMENT = /^(?::|true|(?:echo|printf|date)\b.*)$/u;
380
- var LOOP_KEYWORD = /\b(?:while|until|for|do|done)\b/u;
381
- var SECONDS_PER_UNIT = { d: 86400, h: 3600, m: 60, s: 1 };
382
- var BARE_NUMBER_SECONDS = 1;
383
- var AMOUNT_GROUP = 1;
384
- var UNIT_GROUP = 2;
385
- var NO_SECONDS = 0;
386
- var ARM_RECORD = hook_decision.create_refusal_stamp(ARM_PREFIX);
387
- function segments_of(command) {
388
- return command.split(SEGMENT_SEPARATOR).map((segment) => segment.trim()).filter((segment) => segment !== "");
389
- }
390
- function sleep_seconds(segment) {
391
- const matched = SLEEP_SEGMENT.exec(segment);
392
- if (matched === null) return void 0;
393
- const unit = matched[UNIT_GROUP] ?? "";
394
- return Number(matched[AMOUNT_GROUP]) * (SECONDS_PER_UNIT[unit] ?? BARE_NUMBER_SECONDS);
395
- }
396
- function is_wait_or_inert(segment) {
397
- return sleep_seconds(segment) !== void 0 || INERT_SEGMENT.test(segment);
398
- }
399
- function is_wait_timer(command) {
400
- if (LOOP_KEYWORD.test(command)) return false;
401
- const segments = segments_of(command);
402
- if (segments.every((segment) => sleep_seconds(segment) === void 0)) return false;
403
- return segments.every((segment) => is_wait_or_inert(segment));
404
- }
405
- function wait_duration_ms(command) {
406
- const seconds = segments_of(command).reduce(
407
- (total, segment) => total + (sleep_seconds(segment) ?? NO_SECONDS),
408
- NO_SECONDS
409
- );
410
- return seconds * run_progress.MS_PER_SECOND;
411
- }
412
- function is_armed(target, now_ms) {
413
- return now_ms < ARM_RECORD.last_ms(target);
414
- }
415
- function landing_ms(command, now_ms, last_report_ms) {
416
- const interval_ms = run_progress_config.configured_interval_ms();
417
- const duration_ms = wait_duration_ms(command);
418
- if (duration_ms > interval_ms) return void 0;
419
- const lands_ms = now_ms + duration_ms;
420
- return run_progress.is_due(last_report_ms, lands_ms, interval_ms) ? lands_ms : void 0;
421
- }
422
- function decide(call, run2, can_record) {
423
- const last_report_ms = run_progress_clock.read_last_report_sync();
424
- if (last_report_ms === void 0) return false;
425
- const target = ARM_RECORD.path(run2.transcript);
426
- if (is_armed(target, run2.now_ms)) return true;
427
- const lands_ms = landing_ms(time_shell.bash_command(call.input), run2.now_ms, last_report_ms);
428
- if (lands_ms === void 0) return true;
429
- if (can_record) ARM_RECORD.record(target, lands_ms);
430
- return false;
431
- }
432
- 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.';
433
- var PROGRESS_NAMES = /* @__PURE__ */ new Set(["run:progress"]);
434
- var WATCH_FLAG = /(?:^|\s)--(?:wait|once)(?=\s|$)/u;
435
- function is_progress_watch_segment(segment) {
436
- return shell_segments.is_josh_command(segment, PROGRESS_NAMES) && WATCH_FLAG.test(segment);
437
- }
438
- function is_progress_watch(command) {
439
- return segments_of(time_shell.unquoted(command)).some(
440
- (segment) => is_progress_watch_segment(segment)
441
- );
442
- }
443
- function waits_for_progress(command) {
444
- return is_wait_timer(command) || is_progress_watch(command);
445
- }
446
- var early_heartbeat = {
447
- ARM_PREFIX,
448
- EARLY_HEARTBEAT_REASON,
449
- decide,
450
- is_progress_watch,
451
- is_wait_timer,
452
- wait_duration_ms,
453
- waits_for_progress
454
- };
455
-
456
- // scripts/rules/file-body.ts
457
- import { existsSync } from "node:fs";
458
- var HEREDOC = /<<-?\s*['"]?\w+/u;
459
- var LINE_BREAK = "\n";
460
- function first_line(command) {
461
- const [line] = command.split(LINE_BREAK);
462
- return line ?? "";
463
- }
464
- function has_heredoc(command) {
465
- return HEREDOC.test(command);
466
- }
467
- var FILE_REDIRECT = /(?:^|\s)>>?\s*(?!&)("[^"]+"|'[^']+'|\S+)/u;
468
- var TEE_TARGET = /\btee\s+(?:-a\s+)?(?!-)("[^"]+"|'[^']+'|\S+)/u;
469
- var QUOTES = /^['"]|['"]$/gu;
470
- function redirect_target(line) {
471
- const target = FILE_REDIRECT.exec(line)?.[1] ?? TEE_TARGET.exec(line)?.[1];
472
- return target === void 0 ? void 0 : target.replaceAll(QUOTES, "");
473
- }
474
- function writes_existing_file(command, has_file) {
475
- if (!has_heredoc(command)) return false;
476
- const target = redirect_target(first_line(command));
477
- return target !== void 0 && has_file(target);
478
- }
479
- var BODY_INTERPRETERS = /* @__PURE__ */ new Set(["python", "python3", "ruby", "perl", "php"]);
480
- var NODE = "node";
481
- var NODE_EVAL = /(?:^|\s)(?:-e|--eval)\b/u;
482
- var WRITE_CALL = /writeFile|appendFile|createWriteStream|\.write(?:lines)?\s*\(/u;
483
- var WRITE_MODE = /open\s*\([^)]*['"][rbt+]*[wax]|os\.replace|shutil\.(?:copy|move)/u;
484
- function body_writes(command) {
485
- return WRITE_CALL.test(command) || WRITE_MODE.test(command);
486
- }
487
- function is_interpreter_write(segment, command) {
488
- const leading = time_shell.leading_word(segment);
489
- if (BODY_INTERPRETERS.has(leading)) return has_heredoc(command) && body_writes(command);
490
- return leading === NODE && NODE_EVAL.test(segment) && body_writes(segment);
491
- }
492
- var PERL = "perl";
493
- var PERL_IN_PLACE = /^-[A-Za-z0-9]*i/u;
494
- var WHITESPACE = /\s+/u;
495
- function is_in_place_perl(segment) {
496
- if (time_shell.leading_word(segment) !== PERL) return false;
497
- return segment.split(WHITESPACE).some((word) => PERL_IN_PLACE.test(word));
498
- }
499
- function has_body_interpreter(command) {
500
- return shell_segments.segments_of(first_line(command)).some((segment) => is_interpreter_write(segment, command) || is_in_place_perl(segment));
501
- }
502
- function carries_a_file_body(command, has_file = existsSync) {
503
- return writes_existing_file(command, has_file) || has_body_interpreter(command);
504
- }
505
- 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.**";
506
- var ROW = {
507
- id: "file-body",
508
- is_trigger: bash_triggers.on_bash_command((command) => carries_a_file_body(command)),
509
- reason: FILE_BODY_REASON,
510
- decide: () => true
511
- };
512
- var file_body = {
513
- FILE_BODY_REASON,
514
- ROW,
515
- carries_a_file_body,
516
- is_in_place_perl,
517
- is_interpreter_write,
518
- writes_existing_file
519
- };
520
-
521
- // scripts/rules/tail-commands.ts
522
- function is_bash_use(block) {
523
- return block.type === cost_blocks.TOOL_USE_TYPE && block.name === cost_blocks.BASH_TOOL;
524
- }
525
- function bash_commands_of(line) {
526
- const parsed = time_transcript_line.parse_line(line);
527
- if (parsed === void 0) return [];
528
- return parsed.blocks.filter((block) => is_bash_use(block)).map((block) => time_shell.bash_command(block.input));
529
- }
530
- function prior_bash_commands(tail) {
531
- return tail.split("\n").flatMap((line) => bash_commands_of(line));
532
- }
533
- var tail_commands = { bash_commands_of, is_bash_use, prior_bash_commands };
534
-
535
- // scripts/rules/filing-cap.ts
536
- var FILING_CAP = 10;
537
- var CAP_REACHED = FILING_CAP;
538
- function is_filing_use(block) {
539
- if (!tail_commands.is_bash_use(block)) return false;
540
- return bash_triggers.is_issue_filing(time_shell.bash_command(block.input));
541
- }
542
- function collect_block(block, found) {
543
- if (block.result_id !== "") found.refusals.set(block.result_id, block.refusal_guard);
544
- if (is_filing_use(block)) found.filing_ids.push(block.id);
545
- }
546
- function collect_line(line, found) {
547
- const parsed = time_transcript_line.parse_line(line);
548
- if (parsed === void 0) return;
549
- for (const block of parsed.blocks) collect_block(block, found);
550
- }
551
- function scan_tail(tail) {
552
- const found = { filing_ids: [], refusals: /* @__PURE__ */ new Map() };
553
- for (const line of tail.split("\n")) collect_line(line, found);
554
- return found;
555
- }
556
- function prior_filing_count(tail) {
557
- const { filing_ids, refusals } = scan_tail(tail);
558
- return filing_ids.filter((id) => (refusals.get(id) ?? "") === "").length;
559
- }
560
- function decide2(_call, run2) {
561
- return prior_filing_count(time_density_hook.read_tail(run2.transcript)) >= CAP_REACHED;
562
- }
563
- 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.`;
564
- var ROW2 = {
565
- id: "filing-cap",
566
- is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
567
- reason: FILING_CAP_REASON,
568
- decide: decide2
569
- };
570
- var filing_cap = { FILING_CAP, FILING_CAP_REASON, ROW: ROW2, decide: decide2, prior_filing_count };
571
-
572
- // scripts/rules/gh-api.ts
573
- var GH_FLAGS = String.raw`(?:-{1,2}[\w-]+(?:[= ][^\s]+)?\s+)*`;
574
- var GH_API_COMMAND = new RegExp(String.raw`^gh\s+${GH_FLAGS}api\s`, "u");
575
- var API_METHOD = /(?:^|\s)(?:--method|-X)[= ]([A-Za-z]+)/u;
576
- var API_FIELD = /(?:^|\s)(?:--raw-field|--field|--input|-F|-f)(?:[= ]|$)/u;
577
- var READ_METHOD = "GET";
578
- var REPO_PATH = /repos\/([^/\s'"?]+)\/([^/\s'"?]+)/u;
579
- var OWNER_GROUP = 1;
580
- var REPO_GROUP = 2;
581
- function is_gh_api(segment) {
582
- return GH_API_COMMAND.test(segment);
583
- }
584
- function is_read(segment) {
585
- const method = API_METHOD.exec(segment)?.[1];
586
- if (method !== void 0) return method.toUpperCase() === READ_METHOD;
587
- return !API_FIELD.test(segment);
588
- }
589
- function is_write(segment) {
590
- return !is_read(segment);
591
- }
592
- function repo_target(segment) {
593
- const match = REPO_PATH.exec(segment);
594
- if (match === null) return void 0;
595
- return { owner: match[OWNER_GROUP] ?? "", repo: match[REPO_GROUP] ?? "" };
596
- }
597
- var gh_api = { GH_API_COMMAND, GH_FLAGS, is_gh_api, is_read, is_write, repo_target };
598
-
599
- // scripts/rules/git-argv.ts
600
- var ENV_ASSIGNMENT = /^[A-Za-z_]\w*=/u;
601
- var WRAPPER_WORDS = /* @__PURE__ */ new Set([
602
- "pnpm",
603
- "npx",
604
- "exec",
605
- "run",
606
- "sudo",
607
- "command"
608
- ]);
609
- var SUBSHELL_OPEN = "(";
610
- var VALUE_TAKING_GLOBALS = /* @__PURE__ */ new Set([
611
- "-C",
612
- "-c",
613
- "--git-dir",
614
- "--work-tree",
615
- "--namespace",
616
- "--exec-path",
617
- "--super-prefix"
618
- ]);
619
- var FLAG_PREFIX = "-";
620
- var GIT_COMMAND = "git";
621
- var NEXT = 1;
622
- var AFTER_VALUE = 2;
623
- var WHITESPACE2 = /\s+/u;
624
- function words_of(segment) {
625
- return segment.split(WHITESPACE2).filter((word) => word !== "");
626
- }
627
- function is_wrapper_token(word) {
628
- return word === SUBSHELL_OPEN || ENV_ASSIGNMENT.test(word) || WRAPPER_WORDS.has(word);
629
- }
630
- function after_wrapper(words) {
631
- let index = 0;
632
- while (index < words.length && is_wrapper_token(words[index] ?? "")) index += NEXT;
633
- return index;
634
- }
635
- function global_step(flag) {
636
- return VALUE_TAKING_GLOBALS.has(flag) ? AFTER_VALUE : NEXT;
637
- }
638
- function after_globals(words, start) {
639
- let index = start;
640
- while (index < words.length) {
641
- const flag = words[index] ?? "";
642
- if (!flag.startsWith(FLAG_PREFIX)) return index;
643
- index += global_step(flag);
644
- }
645
- return index;
646
- }
647
- function parse(segment) {
648
- const words = words_of(segment);
649
- const git_index = after_wrapper(words);
650
- if (words[git_index] !== GIT_COMMAND) return void 0;
651
- const subcommand_index = after_globals(words, git_index + NEXT);
652
- return { subcommand: words[subcommand_index], args: words.slice(subcommand_index + NEXT) };
653
- }
654
- var SHORT_CLUSTER = /^-[A-Za-z]+$/u;
655
- function short_cluster_has(token, letters) {
656
- if (!SHORT_CLUSTER.test(token)) return false;
657
- const cluster = token.slice(FLAG_PREFIX.length);
658
- for (let index = 0; index < letters.length; index += 1) {
659
- if (cluster.includes(letters.charAt(index))) return true;
660
- }
661
- return false;
662
- }
663
- var git_argv = { parse, short_cluster_has, words_of };
664
-
665
- // scripts/rules/git-force.ts
666
- var FORCE_LONG_PREFIX = "--force";
667
- var DELETE_LONG = "--delete";
668
- var PUSH_SHORT_LETTERS = "fd";
669
- var FORCE_OR_DELETE_REFSPEC = /^[+:]/u;
670
- function is_force_or_delete_push(args) {
671
- return args.some(
672
- (argument) => argument.startsWith(FORCE_LONG_PREFIX) || argument === DELETE_LONG || git_argv.short_cluster_has(argument, PUSH_SHORT_LETTERS) || FORCE_OR_DELETE_REFSPEC.test(argument)
673
- );
674
- }
675
- var BRANCH_DELETE_LETTERS = "dD";
676
- function is_branch_delete(args) {
677
- return args.some(
678
- (argument) => argument === DELETE_LONG || git_argv.short_cluster_has(argument, BRANCH_DELETE_LETTERS)
679
- );
680
- }
681
- var PUSH_SUBCOMMAND = "push";
682
- var BRANCH_SUBCOMMAND = "branch";
683
- function is_destructive_segment(segment) {
684
- const call = git_argv.parse(segment);
685
- if (call === void 0) return false;
686
- if (call.subcommand === PUSH_SUBCOMMAND) return is_force_or_delete_push(call.args);
687
- return call.subcommand === BRANCH_SUBCOMMAND && is_branch_delete(call.args);
688
- }
689
- function is_force_or_delete(command) {
690
- return shell_segments.segments_of(command).some((segment) => is_destructive_segment(segment));
691
- }
692
- 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.**";
693
- var ROW3 = {
694
- id: "git-force",
695
- is_trigger: bash_triggers.on_bash_command(is_force_or_delete),
696
- reason: GIT_FORCE_REASON,
697
- decide: () => true
698
- };
699
- var git_force = {
700
- GIT_FORCE_REASON,
701
- ROW: ROW3,
702
- is_branch_delete,
703
- is_force_or_delete,
704
- is_force_or_delete_push
705
- };
706
-
707
- // scripts/cost-runtime/cost-cli.ts
708
- import { fileURLToPath } from "node:url";
709
- import { parseArgs } from "node:util";
710
-
711
- // scripts/josh/agent-session-environment.ts
712
- var PARENT_SESSION_KEYS = [
713
- "CLAUDE_CODE_MESSAGING_SOCKET",
714
- "CLAUDE_CODE_MESSAGING_TOKEN",
715
- "CLAUDE_CODE_SESSION_ID",
716
- "CLAUDE_CODE_CHILD_SESSION"
717
- ];
718
- var PROXY_KEYS = [
719
- "ALL_PROXY",
720
- "HTTP_PROXY",
721
- "HTTPS_PROXY",
722
- "all_proxy",
723
- "http_proxy",
724
- "https_proxy",
725
- "GLOBAL_AGENT_HTTP_PROXY",
726
- "GLOBAL_AGENT_HTTPS_PROXY"
727
- ];
728
- var PROXY_CERTIFICATE_KEY = "NODE_EXTRA_CA_CERTS";
729
- var LOOPBACK_HOSTS = ["localhost", "ip6-localhost", "[::1]"];
730
- var LOOPBACK_PREFIX = "127.";
731
- var NO_HOST = "";
732
- function parsed_host(value) {
733
- try {
734
- return new URL(value).hostname;
735
- } catch {
736
- return NO_HOST;
737
- }
738
- }
739
- function proxy_host(value) {
740
- const direct = parsed_host(value);
741
- return direct === NO_HOST ? parsed_host(`http://${value}`) : direct;
742
- }
743
- function is_loopback_host(host) {
744
- return host.startsWith(LOOPBACK_PREFIX) || LOOPBACK_HOSTS.includes(host);
745
- }
746
- function is_loopback_proxy(value) {
747
- if (value === void 0 || value === NO_HOST) return false;
748
- return is_loopback_host(proxy_host(value));
749
- }
750
- function is_declared(value) {
751
- return value !== void 0 && value !== NO_HOST;
752
- }
753
- function loopback_proxy_keys(source) {
754
- const declared = PROXY_KEYS.filter((key) => is_declared(source[key]));
755
- const matched = declared.filter((key) => is_loopback_proxy(source[key]));
756
- const is_certificate_included = matched.length > 0 && matched.length === declared.length;
757
- return is_certificate_included ? [...matched, PROXY_CERTIFICATE_KEY] : matched;
758
- }
759
- function removed_environment(source = process.env) {
760
- const keys = [...PARENT_SESSION_KEYS, ...loopback_proxy_keys(source)];
761
- return Object.fromEntries(keys.map((key) => [key, void 0]));
762
- }
763
- var agent_session_environment = {
764
- LOOPBACK_HOSTS,
765
- PARENT_SESSION_KEYS,
766
- PROXY_CERTIFICATE_KEY,
767
- PROXY_KEYS,
768
- removed_environment
769
- };
770
-
771
- // scripts/agent/agent-role-profile.ts
772
- import { z as z3 } from "zod";
773
- var FIRST_PRINTABLE_CODE = 32;
774
- var DELETE_CODE = 127;
775
- var FIRST_CODE_POINT = 0;
776
- var EMPTY_LENGTH = 0;
777
- var MAX_VALUE_LENGTH = 4096;
778
- var ROLE_SCHEMA = z3.enum(["scheduler", "worker", "reviewer"]);
779
- var PROVIDER_SCHEMA = z3.enum(["anthropic", "openai"]);
780
- var EFFORT_SCHEMA = z3.enum(["low", "medium", "high", "xhigh", "max"]);
781
- var PROFILE_SCHEMA = z3.object({
782
- provider: PROVIDER_SCHEMA.default("anthropic"),
783
- role: ROLE_SCHEMA,
784
- model: z3.string(),
785
- effort: EFFORT_SCHEMA
786
- });
787
- var SCHEDULER = "scheduler";
788
- var WORKER = "worker";
789
- var REVIEWER = "reviewer";
790
- var ANTHROPIC_PROVIDER = "anthropic";
791
- var OPENAI_PROVIDER = "openai";
792
- var OPENAI_MODEL = "gpt-5.6-sol";
793
- var CODEX_SESSION_KEY = "CODEX_THREAD_ID";
794
- var DEFAULT_PROFILES = {
795
- scheduler: { provider: ANTHROPIC_PROVIDER, role: SCHEDULER, model: "opus", effort: "medium" },
796
- worker: { provider: ANTHROPIC_PROVIDER, role: WORKER, model: "opus", effort: "medium" },
797
- reviewer: { provider: ANTHROPIC_PROVIDER, role: REVIEWER, model: "opus", effort: "high" }
798
- };
799
- var OPENAI_PROFILES = {
800
- scheduler: { provider: OPENAI_PROVIDER, role: SCHEDULER, model: OPENAI_MODEL, effort: "medium" },
801
- worker: { provider: OPENAI_PROVIDER, role: WORKER, model: OPENAI_MODEL, effort: "medium" },
802
- reviewer: { provider: OPENAI_PROVIDER, role: REVIEWER, model: OPENAI_MODEL, effort: "high" }
803
- };
804
- var PROVIDER_PROFILES = { anthropic: DEFAULT_PROFILES, openai: OPENAI_PROFILES };
805
- var ENV_KEYS = {
806
- scheduler: { model: "JOSH_SCHEDULER_MODEL", effort: "JOSH_SCHEDULER_EFFORT" },
807
- worker: { model: "JOSH_WORKER_MODEL", effort: "JOSH_WORKER_EFFORT" },
808
- reviewer: { model: "JOSH_REVIEWER_MODEL", effort: "JOSH_REVIEWER_EFFORT" }
809
- };
810
- var LEGACY_WORKER_KEYS = { model: "JOSH_LANE_MODEL", effort: "JOSH_LANE_EFFORT" };
811
- function is_control_character(character) {
812
- const code = character.codePointAt(FIRST_CODE_POINT) ?? FIRST_PRINTABLE_CODE;
813
- return code < FIRST_PRINTABLE_CODE || code === DELETE_CODE;
814
- }
815
- function is_safe_value(value) {
816
- if (value.length === EMPTY_LENGTH || value.length > MAX_VALUE_LENGTH) return false;
817
- for (const character of value) if (is_control_character(character)) return false;
818
- return true;
819
- }
820
- function trimmed(value) {
821
- const result = value?.trim();
822
- return result === void 0 || result.length === EMPTY_LENGTH ? void 0 : result;
823
- }
824
- function override_value(role, field, environment) {
825
- const key = ENV_KEYS[role][field];
826
- const value = trimmed(environment[key]);
827
- if (value !== void 0) return { key, value };
828
- if (role !== WORKER) return { key, value: void 0 };
829
- const legacy = LEGACY_WORKER_KEYS[field];
830
- const legacy_value = trimmed(environment[legacy]);
831
- return legacy_value === void 0 ? { key, value: void 0 } : { key: legacy, value: legacy_value };
832
- }
833
- function model_override(role, environment, provider) {
834
- const key = ENV_KEYS[role].model;
835
- return provider === OPENAI_PROVIDER ? { key, value: void 0 } : override_value(role, "model", environment);
836
- }
837
- function rejection(key, value, expected) {
838
- return { kind: "rejected", note: `${key}=${value} ${expected}, so the agent was not started` };
839
- }
840
- function resolved_effort(override, fallback) {
841
- const parsed = EFFORT_SCHEMA.safeParse(override.value ?? fallback);
842
- return parsed.success ? { kind: "effort", effort: parsed.data } : rejection(override.key, override.value ?? "", "is not an allowed effort");
843
- }
844
- function has_session(environment, keys) {
845
- return keys.some((key) => trimmed(environment[key]) !== void 0);
846
- }
847
- function session_rejection(is_conflicting) {
848
- const note = is_conflicting ? "both Codex and Claude Code sessions were detected" : "no Codex or Claude Code session was detected";
849
- return { kind: "rejected", note };
850
- }
851
- function resolve_provider(environment = process.env) {
852
- const has_codex = has_session(environment, [CODEX_SESSION_KEY]);
853
- const has_claude = has_session(environment, agent_session_environment.PARENT_SESSION_KEYS);
854
- if (has_codex === has_claude) return session_rejection(has_codex);
855
- return { kind: "provider", provider: has_codex ? OPENAI_PROVIDER : ANTHROPIC_PROVIDER };
856
- }
857
- function validate(profile, model_key) {
858
- if (!is_safe_value(profile.model)) {
859
- return rejection(model_key, profile.model, "is not a safe model");
860
- }
861
- return { kind: "profile", profile };
862
- }
863
- function resolve(role, environment = process.env) {
864
- const selected = resolve_provider(environment);
865
- if (selected.kind === "rejected") return selected;
866
- const defaults = PROVIDER_PROFILES[selected.provider][role];
867
- const model = model_override(role, environment, selected.provider);
868
- const effort = override_value(role, "effort", environment);
869
- const resolved = resolved_effort(effort, defaults.effort);
870
- if (resolved.kind === "rejected") return resolved;
871
- const profile = {
872
- provider: selected.provider,
873
- role,
874
- model: model.value ?? defaults.model,
875
- effort: resolved.effort
876
- };
877
- return validate(profile, model.key);
878
- }
879
- function describe(profile) {
880
- return `provider=${profile.provider} role=${profile.role} model=${profile.model} effort=${profile.effort}`;
881
- }
882
- function parse2(value) {
883
- const parsed = PROFILE_SCHEMA.safeParse(value);
884
- return parsed.success && is_safe_value(parsed.data.model) ? parsed.data : void 0;
885
- }
886
- var agent_role_profile = {
887
- DEFAULT_PROFILES,
888
- EFFORT_SCHEMA,
889
- ENV_KEYS,
890
- LEGACY_WORKER_KEYS,
891
- MAX_VALUE_LENGTH,
892
- OPENAI_PROFILES,
893
- PROVIDER_SCHEMA,
894
- PROFILE_SCHEMA,
895
- REVIEWER,
896
- SCHEDULER,
897
- WORKER,
898
- describe,
899
- is_safe_value,
900
- parse: parse2,
901
- resolve,
902
- resolve_provider
903
- };
904
-
905
- // scripts/cost-runtime/codex-usage.ts
906
- import { globSync, readFileSync as readFileSync2 } from "node:fs";
907
- import path2 from "node:path";
908
- import { z as z4 } from "zod";
909
- var THREAD_ID_PATTERN = /^[A-Za-z0-9-]+$/u;
910
- var SESSION_META_SCHEMA = z4.looseObject({
911
- type: z4.literal("session_meta"),
912
- payload: z4.looseObject({ id: z4.string(), cwd: z4.string() })
913
- });
914
- var TOTAL_USAGE_SCHEMA = z4.looseObject({ input_tokens: z4.number().nonnegative() });
915
- var TOKEN_COUNT_SCHEMA = z4.looseObject({
916
- type: z4.literal("event_msg"),
917
- payload: z4.looseObject({
918
- type: z4.literal("token_count"),
919
- info: z4.looseObject({
920
- total_token_usage: TOTAL_USAGE_SCHEMA,
921
- last_token_usage: TOTAL_USAGE_SCHEMA
922
- })
923
- })
924
- });
925
- function decode(line) {
926
- try {
927
- return JSON.parse(line);
928
- } catch {
929
- return void 0;
930
- }
931
- }
932
- function same_project(cwd, target) {
933
- return cost_transcript.session_cwd(cwd) === cost_transcript.session_cwd(target);
934
- }
935
- function billed_per_request(usages) {
936
- return usages.map((usage) => usage.last_token_usage.input_tokens);
937
- }
938
- function measurement_from(content, target, thread_id) {
939
- const values = content.split("\n").map((line) => decode(line));
940
- const metadata = values.flatMap((value) => {
941
- const parsed = SESSION_META_SCHEMA.safeParse(value);
942
- return parsed.success ? [parsed.data] : [];
943
- });
944
- const usages = values.flatMap((value) => {
945
- const parsed = TOKEN_COUNT_SCHEMA.safeParse(value);
946
- return parsed.success ? [parsed.data.payload.info] : [];
947
- });
948
- const session = metadata.find((entry) => entry.payload.id === thread_id);
949
- if (session === void 0 || usages.length === 0) return void 0;
950
- if (!same_project(session.payload.cwd, target)) return void 0;
951
- return { billed_input_per_request: billed_per_request(usages) };
952
- }
953
- function rollout_files(home, thread_id) {
954
- if (!THREAD_ID_PATTERN.test(thread_id)) return [];
955
- return globSync(`sessions/**/*${thread_id}.jsonl`, { cwd: home }).map(
956
- (file) => path2.join(home, file)
957
- );
958
- }
959
- function read_measurement(file, target, thread_id) {
960
- try {
961
- return measurement_from(readFileSync2(file, "utf8"), target, thread_id);
962
- } catch {
963
- return void 0;
964
- }
965
- }
966
- function latest_measurement(files, target, thread_id) {
967
- for (const file of files.toReversed()) {
968
- const measured = read_measurement(file, target, thread_id);
969
- if (measured !== void 0) return measured;
970
- }
971
- return void 0;
972
- }
973
- function measurement(target, default_home, environment = process.env) {
974
- const thread_id = environment["CODEX_THREAD_ID"];
975
- if (thread_id === void 0) return void 0;
976
- const home = environment["CODEX_HOME"] ?? path2.join(default_home, ".codex");
977
- return latest_measurement(rollout_files(home, thread_id), target, thread_id);
978
- }
979
- var codex_usage = { measurement };
980
-
981
- // scripts/cost-runtime/cost-corpus.ts
982
- function accumulate_missing(sessions) {
983
- return {
984
- no_usage_lines: sessions.reduce((sum, session) => sum + session.no_usage_lines, 0),
985
- malformed_lines: sessions.reduce((sum, session) => sum + session.malformed_lines, 0),
986
- unreadable_sessions: sessions.filter((session) => !session.is_readable).length,
987
- // Filled by the attribution, which is the only step that knows a unit could not follow its
988
- // parent; a per-session tally cannot see it, so it is zero here and set for an issue scope below.
989
- unattributed_sessions: 0
990
- };
991
- }
992
- function load_corpus(cwd) {
993
- const files = cost_transcript.list_sessions_across(cost_transcript.transcript_directories(cwd));
994
- const sessions = files.map((file) => cost_transcript.read_session(file));
995
- return { sessions, files, missing: accumulate_missing(sessions) };
996
- }
997
- function session_meta(corpus) {
998
- return new Map(
999
- corpus.files.map((file) => [
1000
- file.session_id,
1001
- { is_delegated: file.is_delegated, owner: cost_transcript.owning_session_id(file) }
1002
- ])
1003
- );
1004
- }
1005
- function issue_set(records) {
1006
- const issues = cost_attribute.group_by_issue(records).map((group) => group.issue).filter((issue) => issue !== cost_attribute.UNATTRIBUTED_KEY);
1007
- return new Set(issues);
1008
- }
1009
- function sole_of(issues) {
1010
- if (issues.size !== 1) return cost_attribute.UNATTRIBUTED_KEY;
1011
- return [...issues][0] ?? cost_attribute.UNATTRIBUTED_KEY;
1012
- }
1013
- function owner_sets(corpus, meta) {
1014
- const own = corpus.sessions.filter(
1015
- (session) => meta.get(session.session_id)?.is_delegated !== true
1016
- );
1017
- return new Map(own.map((session) => [session.session_id, issue_set(session.records)]));
1018
- }
1019
- function pairs_with_inherited(session, inherited, is_delegated) {
1020
- return cost_attribute.group_by_issue(session.records).flatMap(
1021
- (group) => group.records.map((record) => ({
1022
- record,
1023
- issue: group.issue === cost_attribute.UNATTRIBUTED_KEY ? inherited : group.issue,
1024
- baseline_tokens: session.baseline_tokens,
1025
- session_id: session.session_id,
1026
- is_delegated
1027
- }))
1028
- );
1029
- }
1030
- var NO_ISSUES = /* @__PURE__ */ new Set();
1031
- function parent_issues(session, meta, owners) {
1032
- const entry = meta.get(session.session_id);
1033
- if (entry?.is_delegated !== true) return NO_ISSUES;
1034
- return owners.get(entry.owner) ?? NO_ISSUES;
1035
- }
1036
- function pairs_of_session(session, meta, owners) {
1037
- const is_delegated = meta.get(session.session_id)?.is_delegated === true;
1038
- const candidates = parent_issues(session, meta, owners);
1039
- const inherited = is_delegated ? sole_of(candidates) : cost_attribute.UNATTRIBUTED_KEY;
1040
- const pairs = pairs_with_inherited(session, inherited, is_delegated);
1041
- const has_unattributed = pairs.some((pair) => pair.issue === cost_attribute.UNATTRIBUTED_KEY);
1042
- if (!is_delegated || !has_unattributed) return { pairs };
1043
- return { pairs, unattributed_candidates: candidates };
1044
- }
1045
- function attribute_corpus(corpus) {
1046
- const meta = session_meta(corpus);
1047
- const owners = owner_sets(corpus, meta);
1048
- const per_session = corpus.sessions.map((session) => pairs_of_session(session, meta, owners));
1049
- return {
1050
- pairs: per_session.flatMap((one) => one.pairs),
1051
- unattributed: per_session.flatMap(
1052
- (one) => one.unattributed_candidates === void 0 ? [] : [one.unattributed_candidates]
1053
- )
1054
- };
1055
- }
1056
- function floor_for_issue(unattributed, issue_number) {
1057
- return unattributed.filter((set) => set.size === 0 || set.has(issue_number)).length;
1058
- }
1059
- function is_better(candidate, existing) {
1060
- const { UNATTRIBUTED_KEY } = cost_attribute;
1061
- return existing.issue === UNATTRIBUTED_KEY && candidate.issue !== UNATTRIBUTED_KEY;
1062
- }
1063
- function values_of(best) {
1064
- const collected = [];
1065
- for (const [, pair] of best) collected.push(pair);
1066
- return collected;
1067
- }
1068
- function dedupe_across_sessions(pairs) {
1069
- const best = /* @__PURE__ */ new Map();
1070
- for (const pair of pairs) {
1071
- const existing = best.get(pair.record.request_id);
1072
- if (existing === void 0 || is_better(pair, existing)) {
1073
- best.set(pair.record.request_id, pair);
1074
- }
1075
- }
1076
- return values_of(best);
1077
- }
1078
- function attributed(corpus) {
1079
- return dedupe_across_sessions(attribute_corpus(corpus).pairs);
1080
- }
1081
- function mainline_records(pairs) {
1082
- const groups = /* @__PURE__ */ new Map();
1083
- for (const pair of pairs) {
1084
- if (pair.is_delegated) continue;
1085
- groups.set(pair.session_id, [...groups.get(pair.session_id) ?? [], pair.record]);
1086
- }
1087
- return [...groups.values()];
1088
- }
1089
- var cost_corpus = {
1090
- accumulate_missing,
1091
- load_corpus,
1092
- attribute_corpus,
1093
- floor_for_issue,
1094
- dedupe_across_sessions,
1095
- attributed,
1096
- mainline_records
1097
- };
1098
-
1099
- // scripts/cost-runtime/cost-verdict.ts
1100
- var FAILURE_EXIT_CODE = 1;
1101
- var OVER_VERDICT = "over";
1102
- var UNDER_VERDICT = "under";
1103
- function recent_requests(measurement2) {
1104
- return measurement2.billed_input_per_request.slice(-RECENT_REQUEST_WINDOW);
1105
- }
1106
- function per_request_cost(measurement2) {
1107
- const window = recent_requests(measurement2);
1108
- if (window.length === 0) return 0;
1109
- const total = window.reduce((sum, billed) => sum + billed, 0);
1110
- return Math.round(total / window.length);
1111
- }
1112
- function classify(measurement2, limit) {
1113
- return per_request_cost(measurement2) > limit ? OVER_VERDICT : UNDER_VERDICT;
1114
- }
1115
- function over_summary(measurement2, limit) {
1116
- const total = measurement2.billed_input_per_request.length;
1117
- const window = recent_requests(measurement2).length;
1118
- return `${String(per_request_cost(measurement2))} billed input tokens per request over the last ${String(window)} of ${String(total)} request(s); limit ${String(limit)}`;
1119
- }
1120
- function report_over(measurement2, limit) {
1121
- if (measurement2 === void 0 || measurement2.billed_input_per_request.length === 0) {
1122
- console.error("No requests in this session; there is nothing to hand off.");
1123
- return FAILURE_EXIT_CODE;
1124
- }
1125
- console.info(classify(measurement2, limit));
1126
- console.error(over_summary(measurement2, limit));
1127
- return 0;
1128
- }
1129
- var cost_verdict = {
1130
- OVER_VERDICT,
1131
- UNDER_VERDICT,
1132
- classify,
1133
- per_request_cost,
1134
- report_over
1135
- };
1136
-
1137
- // scripts/cost-runtime/transcript-cwd.ts
1138
- import path3 from "node:path";
1139
- function resolve2(target, cwd) {
1140
- return target === void 0 ? cwd : path3.resolve(target);
1141
- }
1142
- var transcript_cwd = { resolve: resolve2 };
1143
-
1144
- // scripts/cost-runtime/cost-cli.ts
1145
- var ARGV_OFFSET = 2;
1146
- var FAILURE_EXIT_CODE2 = 1;
1147
- var USAGE = "Usage: josh cost (--cut | --over <tokens-per-request>) [--path <dir>]";
1148
- var UNMEASURABLE_VERDICT = "unmeasurable";
1149
- function to_threshold(raw) {
1150
- if (raw === void 0) return void 0;
1151
- if (raw.trim() === "") return void 0;
1152
- const parsed = Number(raw);
1153
- return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
1154
- }
1155
- function is_unparsed(raw, parsed) {
1156
- return parsed === void 0 && raw !== void 0;
1157
- }
1158
- function selected_threshold(values) {
1159
- if (values.cut === true) return CONTEXT_CUT_THRESHOLD;
1160
- return to_threshold(values.over);
1161
- }
1162
- function threshold_option(over) {
1163
- return over === void 0 ? {} : { over };
1164
- }
1165
- function path_option(path5) {
1166
- return path5 === void 0 ? {} : { path: path5 };
1167
- }
1168
- function to_options(values) {
1169
- if (values.cut === true && values.over !== void 0) return void 0;
1170
- const over = selected_threshold(values);
1171
- if (is_unparsed(values.over, over)) return void 0;
1172
- return { ...threshold_option(over), ...path_option(values.path) };
1173
- }
1174
- var PARSE_ARGS_OPTIONS = {
1175
- cut: { type: "boolean" },
1176
- over: { type: "string" },
1177
- path: { type: "string" }
1178
- };
1179
- function parse_options(argv) {
1180
- try {
1181
- const { values } = parseArgs({ args: [...argv], options: PARSE_ARGS_OPTIONS, strict: true });
1182
- return to_options(values);
1183
- } catch {
1184
- return void 0;
1185
- }
1186
- }
1187
- function report_empty(cwd) {
1188
- const searched = cost_transcript.searched_directories(cost_transcript.transcript_directories(cwd));
1189
- for (const line of cost_transcript.missing_message(searched, void 0)) console.error(line);
1190
- return FAILURE_EXIT_CODE2;
1191
- }
1192
- function provider_of(environment) {
1193
- const resolved = agent_role_profile.resolve_provider(environment);
1194
- return resolved.kind === "provider" ? resolved.provider : void 0;
1195
- }
1196
- function anthropic_measurement(target) {
1197
- const corpus = cost_corpus.load_corpus(target);
1198
- const session = corpus.sessions[cost_transcript.latest_own_index(corpus.files)];
1199
- if (session === void 0) return void 0;
1200
- return {
1201
- billed_input_per_request: session.records.map(
1202
- (record) => cost_usage.billed_input(record.totals)
1203
- )
1204
- };
1205
- }
1206
- function report_missing(target, provider) {
1207
- if (provider === "anthropic") return report_empty(target);
1208
- console.error(`No Codex usage found for the current OpenAI thread under ${target}.`);
1209
- return FAILURE_EXIT_CODE2;
1210
- }
1211
- function same_openai_project(target, cwd) {
1212
- return cost_transcript.session_cwd(target) === cost_transcript.session_cwd(cwd);
1213
- }
1214
- function reject_openai_cross_project(context, target, cwd) {
1215
- if (context.provider !== "openai" || context.path === void 0 || same_openai_project(target, cwd)) {
1216
- return false;
1217
- }
1218
- console.error(
1219
- "OpenAI --path cannot select a thread from another project; run josh cost in that project."
1220
- );
1221
- return true;
1222
- }
1223
- function measure(target, provider, environment) {
1224
- return provider === "openai" ? codex_usage.measurement(target, cost_transcript.home_directory(), environment) : anthropic_measurement(target);
1225
- }
1226
- function run_over(target, limit, provider, environment) {
1227
- const measurement2 = measure(target, provider, environment);
1228
- if (measurement2 === void 0) return report_missing(target, provider);
1229
- return cost_verdict.report_over(measurement2, limit);
1230
- }
1231
- function verdict_of(measurement2) {
1232
- if (measurement2 === void 0 || measurement2.billed_input_per_request.length === 0) {
1233
- return UNMEASURABLE_VERDICT;
1234
- }
1235
- return cost_verdict.classify(measurement2, CONTEXT_CUT_THRESHOLD);
1236
- }
1237
- function session_verdict(cwd = process.cwd(), environment = process.env) {
1238
- const provider = provider_of(environment);
1239
- if (provider === void 0) return UNMEASURABLE_VERDICT;
1240
- return verdict_of(measure(cwd, provider, environment));
1241
- }
1242
- function run_context(argv, environment) {
1243
- const options = parse_options(argv);
1244
- if (options?.over === void 0) return void 0;
1245
- const provider = provider_of(environment);
1246
- if (provider === void 0) return void 0;
1247
- return { over: options.over, path: options.path, provider };
1248
- }
1249
- function run(argv, cwd = process.cwd(), environment = process.env) {
1250
- const context = run_context(argv, environment);
1251
- if (context === void 0) {
1252
- console.error(USAGE);
1253
- return FAILURE_EXIT_CODE2;
1254
- }
1255
- const target = transcript_cwd.resolve(context.path, cwd);
1256
- if (reject_openai_cross_project(context, target, cwd)) return FAILURE_EXIT_CODE2;
1257
- return run_over(target, context.over, context.provider, environment);
1258
- }
1259
- function main(argv) {
1260
- process.exitCode = run(argv);
1261
- }
1262
- var cost_cli = {
1263
- UNMEASURABLE_VERDICT,
1264
- USAGE,
1265
- parse_options,
1266
- to_threshold,
1267
- load_corpus: cost_corpus.load_corpus,
1268
- attributed: cost_corpus.attributed,
1269
- run,
1270
- session_verdict,
1271
- main
1272
- };
1273
- if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv.slice(ARGV_OFFSET));
1274
-
1275
- // scripts/rules/implementation-cut.ts
1276
- var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write"]);
1277
- function is_edit_tool(name) {
1278
- return EDIT_TOOLS.has(name);
1279
- }
1280
- var CUT_COMMANDS = /* @__PURE__ */ new Set(["run:cut"]);
1281
- var IMPL_FLAG = /(?:^|\s)--impl(?:[=\s]|$)/u;
1282
- function is_impl_cut_segment(segment) {
1283
- return shell_segments.is_josh_command(segment, CUT_COMMANDS) && IMPL_FLAG.test(segment);
1284
- }
1285
- function takes_the_impl_cut(command) {
1286
- return shell_segments.segments_of(command).some((segment) => is_impl_cut_segment(segment));
1287
- }
1288
- function current_state() {
1289
- return {
1290
- directory: process.cwd(),
1291
- source: process.env,
1292
- carried: run_cut.carried_cut_sync,
1293
- verdict: () => cost_cli.session_verdict()
1294
- };
1295
- }
1296
- function uncut_lane_child_issue(state) {
1297
- if (!lane_child_marker.is_child_of(state.directory, state.source)) return void 0;
1298
- const issue = lane_paths.lane_issue_of(state.directory, { ...state.source });
1299
- if (issue === void 0) return void 0;
1300
- return state.carried()?.issue === issue ? void 0 : issue;
1301
- }
1302
- function is_over_threshold_edit(call, state = current_state()) {
1303
- if (!is_edit_tool(call.name) || uncut_lane_child_issue(state) === void 0) return false;
1304
- return state.verdict() === cost_verdict.OVER_VERDICT;
1305
- }
1306
- var IMPLEMENTATION_CUT_REASON = "\u26D4 implementation-phase cut: this checkout is a lane dispatched for this issue and its recent-context cost has crossed the shared 200,000-token threshold mid-implementation, so the thinking accumulated so far is now re-read on every later request. Take the cut before this edit. `pnpm josh run:cut --impl <N>` ends this process and relaunches a fresh one that resumes back into implementation (`resume-impl`), dropping the accumulated context rather than carrying it \u2014 joshuafolkken/kit#1933 built the boundary and joshuafolkken/kit#2310 measured it firing 0 times because the verdict was only ever read at session entry, where the context has not yet grown. The verdict is `pnpm josh cost --cut`'s exactly \u2014 the same per-request billed-input statistic against the same threshold, never a second measurement. Issue `pnpm josh run:cut --impl <N>` now and read the verdict: on `cut`, **end the turn immediately** \u2014 the fresh process owns the run and continues implementing, so it must not be waited for; on `not-a-lane`, `unready`, `busy`, `failed` or `unknown`, this process carries the run on and the edit is simply the next call. Never relaunch a second process after `busy`. This fired because the dispatch mark names this lane; 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 this edit once the cut has answered \u2014 it fires once per run, so it cannot repeat on the call in hand.";
1307
- var ROW4 = {
1308
- id: "implementation-cut",
1309
- is_trigger: is_over_threshold_edit,
1310
- reason: IMPLEMENTATION_CUT_REASON,
1311
- keeps: bash_triggers.on_bash_command(takes_the_impl_cut)
1312
- };
1313
- var implementation_cut = {
1314
- IMPLEMENTATION_CUT_REASON,
1315
- ROW: ROW4,
1316
- is_over_threshold_edit,
1317
- takes_the_impl_cut,
1318
- uncut_lane_child_issue
1319
- };
1320
-
1321
- // scripts/rules/issue-fold.ts
1322
- var FOLD_NAMES = /* @__PURE__ */ new Set(["issue:fold"]);
1323
- function runs_the_fold(command) {
1324
- return shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, FOLD_NAMES));
1325
- }
1326
- function already_folded(tail, _call) {
1327
- if (filing_cap.prior_filing_count(tail) === 0) return true;
1328
- return tail_commands.prior_bash_commands(tail).some((command) => runs_the_fold(command));
1329
- }
1330
- 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.';
1331
- var ROW5 = {
1332
- id: "issue-fold",
1333
- is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
1334
- reason: ISSUE_FOLD_REASON,
1335
- already_satisfied: already_folded,
1336
- keeps: bash_triggers.on_bash_command(runs_the_fold)
1337
- };
1338
- var issue_fold = { ISSUE_FOLD_REASON, ROW: ROW5, already_folded, runs_the_fold };
1339
-
1340
- // scripts/rules/issue-scout.ts
1341
- var SCOUT_NAMES = /* @__PURE__ */ new Set(["issue:scout"]);
1342
- function runs_the_scout(command) {
1343
- return shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, SCOUT_NAMES));
1344
- }
1345
- function already_scouted(tail, _call) {
1346
- return tail_commands.prior_bash_commands(tail).some((command) => runs_the_scout(command));
1347
- }
1348
- 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.';
1349
- var ROW6 = {
1350
- id: "issue-scout",
1351
- is_trigger: bash_triggers.on_bash_command(bash_triggers.is_issue_filing),
1352
- reason: ISSUE_SCOUT_REASON,
1353
- already_satisfied: already_scouted,
1354
- keeps: bash_triggers.on_bash_command(runs_the_scout)
1355
- };
1356
- var issue_scout = { ISSUE_SCOUT_REASON, ROW: ROW6, already_scouted, runs_the_scout };
1357
-
1358
- // scripts/rules/josh-git-bare.ts
1359
- var GIT_COMMAND2 = /* @__PURE__ */ new Set(["git"]);
1360
- var CONFIRM_FLAGS = /* @__PURE__ */ new Set(["-y", "--yes"]);
1361
- function has_confirm_flag(args) {
1362
- return args.some((argument) => CONFIRM_FLAGS.has(argument));
1363
- }
1364
- function is_bare_segment(segment) {
1365
- if (!shell_segments.is_josh_command(segment, GIT_COMMAND2)) return false;
1366
- return !has_confirm_flag(time_shell.josh_arguments(segment));
1367
- }
1368
- function is_bare_josh_git(command) {
1369
- return shell_segments.segments_of(command).some((segment) => is_bare_segment(segment));
1370
- }
1371
- var JOSH_GIT_BARE_REASON = '\u26D4 bare `pnpm josh git`: this prompts to confirm the staging, and with no TTY the prompt falls to its cancel branch \u2014 `Checking unstaged files... Found` then `Operation cancelled.` \u2014 so the call throws away the time it takes to fail (about 31 seconds in one measured lane) and you reissue it with `-y` anyway (joshuafolkken/kit#2297). Run it unattended from the start: `pnpm josh git -y "<title> #<N>"`. The recovery form that opens the PR for a push that already landed is `pnpm josh git -y --skip-commit --skip-push`. This rule fires on every occurrence, not once per run.';
1372
- var ROW7 = {
1373
- id: "josh-git-bare",
1374
- is_trigger: bash_triggers.on_bash_command(is_bare_josh_git),
1375
- reason: JOSH_GIT_BARE_REASON,
1376
- decide: () => true
1377
- };
1378
- var josh_git_bare = {
1379
- JOSH_GIT_BARE_REASON,
1380
- ROW: ROW7,
1381
- is_bare_josh_git
1382
- };
1383
-
1384
- // scripts/rules/lane-carry-conflict.ts
1385
- var RUN_BUDGET_COMMANDS = /* @__PURE__ */ new Set(["run:merge", "run:carry"]);
1386
- function invokes_budget_command(command) {
1387
- return shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, RUN_BUDGET_COMMANDS));
1388
- }
1389
- function is_carry_conflict(command, is_lane_child = lane_child_marker.is_child_of(process.cwd())) {
1390
- if (!invokes_budget_command(command)) return false;
1391
- return is_lane_child;
1392
- }
1393
- 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.';
1394
- var ROW8 = {
1395
- id: "lane-carry-conflict",
1396
- is_trigger: bash_triggers.on_bash_command(is_carry_conflict),
1397
- reason: LANE_CARRY_CONFLICT_REASON,
1398
- decide: () => true
1399
- };
1400
- var lane_carry_conflict = {
1401
- LANE_CARRY_CONFLICT_REASON,
1402
- ROW: ROW8,
1403
- invokes_budget_command,
1404
- is_carry_conflict
1405
- };
1406
-
1407
- // scripts/agent/interactive-ask.ts
1408
- import { z as z5 } from "zod";
1409
- var INTERACTIVE_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion"]);
1410
- function is_interactive_tool(name) {
1411
- return INTERACTIVE_TOOLS.has(name);
1412
- }
1413
- var OPTION_SCHEMA = z5.looseObject({ label: z5.string().optional() });
1414
- var QUESTION_SCHEMA = z5.looseObject({
1415
- question: z5.string().optional(),
1416
- options: z5.array(OPTION_SCHEMA).optional()
1417
- });
1418
- var ASK_INPUT_SCHEMA = z5.looseObject({ questions: z5.array(QUESTION_SCHEMA).optional() });
1419
- var DENIAL_SCHEMA = z5.looseObject({
1420
- tool_name: z5.string().optional(),
1421
- tool_input: z5.unknown().optional()
1422
- });
1423
- var UNKNOWN_QUESTION = "(question text unavailable)";
1424
- var OPTION_SEPARATOR = " / ";
1425
- var QUESTION_SEPARATOR = "; ";
1426
- function labels_of(question) {
1427
- return (question.options ?? []).map((option) => option.label ?? "").filter((label) => label !== "");
1428
- }
1429
- function render_question(question) {
1430
- const labels = labels_of(question);
1431
- const text2 = question.question ?? UNKNOWN_QUESTION;
1432
- return labels.length === 0 ? text2 : `${text2} [${labels.join(OPTION_SEPARATOR)}]`;
1433
- }
1434
- function interactive_input(raw) {
1435
- const denial = DENIAL_SCHEMA.safeParse(raw);
1436
- if (!denial.success || !is_interactive_tool(denial.data.tool_name ?? "")) return void 0;
1437
- const input = ASK_INPUT_SCHEMA.safeParse(denial.data.tool_input);
1438
- return input.success ? input.data : void 0;
1439
- }
1440
- function refused_ask_of(permission_denials) {
1441
- if (!Array.isArray(permission_denials)) return void 0;
1442
- const input = permission_denials.map((raw) => interactive_input(raw)).find((one) => one !== void 0);
1443
- const questions = input?.questions ?? [];
1444
- if (questions.length === 0) return void 0;
1445
- return questions.map((question) => render_question(question)).join(QUESTION_SEPARATOR);
1446
- }
1447
- var interactive_ask = { INTERACTIVE_TOOLS, is_interactive_tool, refused_ask_of };
1448
-
1449
- // scripts/rules/lane-interactive-ask.ts
1450
- function is_interactive_ask(call, is_lane_child = lane_child_marker.is_child_of(process.cwd())) {
1451
- if (!interactive_ask.is_interactive_tool(call.name)) return false;
1452
- return is_lane_child;
1453
- }
1454
- var LANE_INTERACTIVE_ASK_REASON = '\u26D4 lane child interactive ask: this session is a dispatched lane child and cannot ask a person, so this call is refused. Left to run it would end the turn with nothing on the Issue \u2014 the parent then finds an OPEN Issue with `in-progress` and no question (joshuafolkken/kit#2201; joshuafolkken/kit#2034 placed the same rule one tool-call too late). Do not ask: park the decision instead. Apply `needs-decision` and post a comment carrying the question, the options and whether work was stashed \u2014 `gh api repos/{owner}/{repo}/issues/<N>/labels -f \'labels[]=needs-decision\'` then `pnpm josh issue:comment <N> --body-file <path>` \u2014 then send the `confirmation` Telegram and stop. The procedure is `.claude/skills/workflow-commands/pre-gate-cut.md` \u2192 "A lane child records its park before it stops", and the park itself is `backlogrun-park.md` \u2192 "park and continue". This rule fires on every occurrence, not once per run.';
1455
- var ROW9 = {
1456
- id: "lane-interactive-ask",
1457
- is_trigger: is_interactive_ask,
1458
- reason: LANE_INTERACTIVE_ASK_REASON,
1459
- decide: () => true
1460
- };
1461
- var lane_interactive_ask = { LANE_INTERACTIVE_ASK_REASON, ROW: ROW9, is_interactive_ask };
1462
-
1463
- // scripts/rules/lane-switch-main.ts
1464
- var SWITCH_SUBCOMMAND = "switch";
1465
- var NON_COLLIDING_FLAGS = /* @__PURE__ */ new Set([
1466
- "-c",
1467
- "-C",
1468
- "--create",
1469
- "--force-create",
1470
- "-d",
1471
- "--detach"
1472
- ]);
1473
- var FLAG_PREFIX2 = "-";
1474
- function switch_target(segment) {
1475
- const call = git_argv.parse(segment);
1476
- if (call?.subcommand !== SWITCH_SUBCOMMAND) return void 0;
1477
- if (call.args.some((argument) => NON_COLLIDING_FLAGS.has(argument))) return void 0;
1478
- return call.args.find((argument) => !argument.startsWith(FLAG_PREFIX2));
1479
- }
1480
- function switch_targets(command) {
1481
- return shell_segments.segments_of(command).map((segment) => switch_target(segment)).filter((target) => target !== void 0);
1482
- }
1483
- function own_lane_branch(directory) {
1484
- const issue = lane_paths.lane_issue_of(directory);
1485
- return issue === void 0 ? void 0 : lane_paths.lane_branch(issue);
1486
- }
1487
- function is_switch_away_from_lane(command, is_lane_child = lane_child_marker.is_child_of(process.cwd()), own_branch = own_lane_branch(process.cwd())) {
1488
- if (!is_lane_child || own_branch === void 0) return false;
1489
- return switch_targets(command).some((target) => target !== own_branch);
1490
- }
1491
- var LANE_SWITCH_MAIN_REASON = "\u26D4 lane child on `git switch main`: this session is a dispatched lane child, and switching to the default branch here always fails \u2014 a lane is a linked work tree, the primary checkout holds the default branch, and git allows one branch in one work tree at a time (`fatal: main is already used by worktree at <the primary checkout>`, measured 11 times in the 2026-09-21 backlogrun). Do not switch: the lane is already on its own issue branch, branched from a fresh default before it opened, and the latest default is brought in during the gate by `pnpm josh main:merge`. So skip this step and keep implementing on the current branch \u2014 no `git switch`, no `git pull`. `pnpm josh main:sync` (`josh ms`) is refused in a lane for the same reason, and a lane is finished with `pnpm josh lane:close <issue-number>`. The parent runs `git switch main && git pull` in the primary checkout before each lane opens, never the child \u2014 `backlogrun-lanes.md` and `backlogrun-child.md` are the single sources. This rule fires on every occurrence, not once per run.";
1492
- var ROW10 = {
1493
- id: "lane-switch-main",
1494
- is_trigger: bash_triggers.on_bash_command(is_switch_away_from_lane),
1495
- reason: LANE_SWITCH_MAIN_REASON,
1496
- decide: () => true
1497
- };
1498
- var lane_switch_main = {
1499
- LANE_SWITCH_MAIN_REASON,
1500
- ROW: ROW10,
1501
- is_switch_away_from_lane,
1502
- switch_target,
1503
- switch_targets
1504
- };
1505
-
1506
- // scripts/rules/decision-oracle.ts
1507
- var CHAIN_RULE_MD = ".claude/skills/workflow-commands/chain-rule.md";
1508
- var BACKLOGRUN_PROGRESS_MD = ".claude/skills/workflow-commands/backlogrun-progress.md";
1509
- var SKILL_2E = ".claude/skills/workflow-commands/SKILL.md \u2192 \xA72e";
1510
- var SPLIT_ASSESSMENT_QUESTION = ".claude/skills/workflow-commands/split-assessment.md \u2192 The question";
1511
- var PRE_GATE_CUT_MD = ".claude/skills/workflow-commands/pre-gate-cut.md";
1512
- var BACKLOGRUN_MD = ".claude/skills/workflow-commands/backlogrun.md";
1513
- var RUN_MERGE_REFERENCE = "docs/josh-commands.md \u2192 `josh run:merge`";
1514
- var EPIC_NEXT_REFERENCE = "docs/josh-commands.md \u2192 `josh epic:next`";
1515
- var BACKLOG_BUDGET_REFERENCE = "docs/josh-commands.md \u2192 `josh backlog:budget`";
1516
- var RUN_LIVENESS_REFERENCE = "docs/josh-commands.md \u2192 `josh run:liveness`";
1517
- var AUTO_OK_NEXT_REFERENCE = "docs/josh-commands.md \u2192 `josh auto-ok:next`";
1518
- var RUN_CUT_CMD = "run:cut";
1519
- var REQUIRED = "required";
1520
- var SKIP = "skip";
1521
- var STOP = "stop";
1522
- var WAIT = "wait";
1523
- var RETRY = "retry";
1524
- var OVER = "over";
1525
- var UNKNOWN = "unknown";
1526
- var NONE = "none";
1527
- var NOT_A_LANE = "not-a-lane";
1528
- var BUSY = "busy";
1529
- var HUMAN_REVIEW = "human-review";
1530
- var LIVE = "live";
1531
- var STOPPED = "stopped";
1532
- var RECONCILED = "reconciled";
1533
- var NOTHING_TO_RECONCILE = "nothing to reconcile";
1534
- var ISSUE_N_ARG = "<N>";
1535
- var EXCLUDE_ARG = "[--exclude <n>...]";
1536
- var DECISION_ORACLES = [
1537
- {
1538
- name: "delegate",
1539
- decision: "Whether a run step may be delegated to a cheaper execution tier",
1540
- args: "<step>",
1541
- vocabulary: ["delegate", "keep"],
1542
- single_source: ".claude/skills/workflow-commands/SKILL.md \u2192 \xA72b"
1543
- },
1544
- {
1545
- name: "review:level",
1546
- decision: "The code-review level for the changed paths",
1547
- command: "review:brief",
1548
- args: "--level-only",
1549
- vocabulary: ["low", "medium", "high", "max"],
1550
- single_source: "prompts/review.md"
1551
- },
1552
- {
1553
- name: "review:round2",
1554
- decision: "Whether the second /code-review round is due",
1555
- args: "[--round-1-closed]",
1556
- vocabulary: [REQUIRED, SKIP],
1557
- single_source: CHAIN_RULE_MD
1558
- },
1559
- {
1560
- name: "disposition",
1561
- decision: "Whether a review finding reaches a runtime path, so it may be filed",
1562
- args: "<path...>",
1563
- vocabulary: ["runtime", "non-runtime"],
1564
- single_source: "prompts/review.md \u2192 Three-way disposition after the cap"
1565
- },
1566
- {
1567
- name: "review:attest",
1568
- decision: "Whether a /code-review attested the briefed checkout",
1569
- args: "--check",
1570
- vocabulary: ["ok", "missing", "mismatch", "not-required"],
1571
- single_source: CHAIN_RULE_MD
1572
- },
1573
- {
1574
- name: "latest:scope",
1575
- decision: "Whether this run has to update dependencies first",
1576
- args: "",
1577
- vocabulary: [REQUIRED, SKIP],
1578
- single_source: ".claude/skills/workflow-commands/latest-gate.md"
1579
- },
1580
- {
1581
- name: "run:hold",
1582
- decision: "Whether this working tree is free for a run to claim",
1583
- args: `[${ISSUE_N_ARG}]`,
1584
- vocabulary: ["hold", BUSY, UNKNOWN],
1585
- single_source: ".claude/skills/workflow-commands/SKILL.md \u2192 \xA72f"
1586
- },
1587
- {
1588
- name: "cost:cut",
1589
- command: "cost",
1590
- decision: "Whether the session cost exceeds the hand-off threshold",
1591
- args: "--cut",
1592
- vocabulary: [OVER, "under"],
1593
- single_source: BACKLOGRUN_PROGRESS_MD
1594
- },
1595
- {
1596
- name: "release:scope",
1597
- decision: "Whether a release is owed after this merge",
1598
- args: "",
1599
- vocabulary: [REQUIRED, SKIP, UNKNOWN],
1600
- single_source: ".claude/skills/workflow-commands/followup-reference.md"
1601
- },
1602
- {
1603
- name: "epic:bundle",
1604
- decision: "Which epic a newly filed issue belongs to",
1605
- args: ISSUE_N_ARG,
1606
- vocabulary: ["add_to_epic", "create_epic", "ask", "nothing_to_bundle"],
1607
- single_source: SKILL_2E
1608
- },
1609
- {
1610
- name: "issue:scout",
1611
- decision: "Whether a matching open issue already exists before filing",
1612
- args: "<title>",
1613
- vocabulary: ["Duplicates:", "Epic:"],
1614
- single_source: SKILL_2E
1615
- },
1616
- {
1617
- name: "issue:fold",
1618
- decision: "Whether findings filed from one session fold into one issue or stay separate",
1619
- args: "<title...>",
1620
- vocabulary: ["fold", "separate", "no-fold-needed"],
1621
- single_source: SPLIT_ASSESSMENT_QUESTION
1622
- },
1623
- {
1624
- name: "pkg:scout",
1625
- decision: "Whether the top package candidate is clearly best (Tier A) or a near-tie (Tier B)",
1626
- args: "<keywords>",
1627
- vocabulary: ["clear", "close"],
1628
- single_source: "CLAUDE.md \u2192 Package-First Development"
1629
- },
1630
- {
1631
- name: "issue:lint",
1632
- decision: "Whether a behavior-change issue declares a deliverable firing point and a re-runnable baseline",
1633
- args: "<path>",
1634
- vocabulary: ["ok", "\u2716 missing heading", "\u2716 firing point", "\u2716 baseline"],
1635
- single_source: "prompts/collaboration-workflow/issue-template.md"
1636
- },
1637
- {
1638
- name: "cases",
1639
- decision: "Each I/O boundary a change crosses (network, process, fs) and the abnormal cases it owes",
1640
- args: "<path...>",
1641
- vocabulary: ["network", "process", "fs", NONE],
1642
- single_source: "prompts/collaboration-workflow/report-format.md \u2192 \u5909\u66F4\u3068\u30C6\u30B9\u30C8"
1643
- },
1644
- {
1645
- name: "issue:state",
1646
- decision: "An issue's state, labels, and human-review flag",
1647
- args: ISSUE_N_ARG,
1648
- vocabulary: ["human_review: yes", "human_review: no"],
1649
- single_source: ".claude/skills/workflow-commands/SKILL.md \u2192 \xA72z"
1650
- },
1651
- {
1652
- name: "run:cut:resume",
1653
- command: RUN_CUT_CMD,
1654
- decision: "Whether a lane child is resuming prior work or starting fresh",
1655
- args: `--resume ${ISSUE_N_ARG}`,
1656
- vocabulary: ["fresh", "resume", NOT_A_LANE],
1657
- single_source: PRE_GATE_CUT_MD
1658
- },
1659
- {
1660
- name: "run:cut:gate",
1661
- command: RUN_CUT_CMD,
1662
- decision: "Whether a lane child should take its pre-gate cut now",
1663
- args: ISSUE_N_ARG,
1664
- vocabulary: ["cut", NOT_A_LANE],
1665
- single_source: PRE_GATE_CUT_MD
1666
- },
1667
- {
1668
- name: "backlog:budget",
1669
- decision: "Whether a backlogrun may start more work, keep watching, or finish",
1670
- args: "",
1671
- vocabulary: ["run", "watch", STOP],
1672
- single_source: BACKLOG_BUDGET_REFERENCE
1673
- },
1674
- {
1675
- name: "run:liveness",
1676
- decision: "Whether a delegated unit is still working or has stopped without reporting",
1677
- args: `${ISSUE_N_ARG} --output <path>`,
1678
- vocabulary: ["alive", "stopped", "settled", "undetermined"],
1679
- single_source: RUN_LIVENESS_REFERENCE
1680
- },
1681
- {
1682
- name: "stash:pop",
1683
- decision: "Whether a named stash entry exists and can be applied",
1684
- args: "<message>",
1685
- vocabulary: ["popped", "conflicted", "no-match", "ambiguous"],
1686
- single_source: ".claude/skills/workflow-commands/SKILL.md \u2192 \xA72d"
1687
- },
1688
- {
1689
- name: "backlog:next",
1690
- decision: "Which issue the backlog offers next, or whether to wait or stop",
1691
- args: EXCLUDE_ARG,
1692
- vocabulary: [WAIT, STOP, RETRY, "error", NONE],
1693
- single_source: BACKLOGRUN_MD
1694
- },
1695
- {
1696
- name: "auto-ok:next",
1697
- decision: "The next opted-in issue outside any epic, or none",
1698
- args: EXCLUDE_ARG,
1699
- vocabulary: [NONE],
1700
- single_source: AUTO_OK_NEXT_REFERENCE
1701
- },
1702
- {
1703
- name: "epic:next",
1704
- decision: "The next runnable child of an epic, or the epic's status",
1705
- args: "<epic>",
1706
- vocabulary: [WAIT, STOP, "complete"],
1707
- single_source: EPIC_NEXT_REFERENCE
1708
- },
1709
- {
1710
- name: "run:merge",
1711
- decision: "The post-merge batch step: confirm child, advance, or report a stop condition",
1712
- args: ISSUE_N_ARG,
1713
- vocabulary: [OVER, HUMAN_REVIEW, STOP, RETRY, BUSY],
1714
- single_source: RUN_MERGE_REFERENCE
1715
- },
1716
- {
1717
- name: "run:step",
1718
- decision: "The run\u2019s next single action, computed from the event stream, carry and issue state",
1719
- args: ISSUE_N_ARG,
1720
- // The non-command answers: `run:step` prints a runnable command for a phase with one to run, and
1721
- // one of these verdicts otherwise. A command line is not a vocabulary token, exactly as an issue
1722
- // number is not one for `backlog:next`.
1723
- vocabulary: ["implement", HUMAN_REVIEW, "update-deps", "already-done", WAIT, STOP, UNKNOWN],
1724
- single_source: BACKLOGRUN_MD
1725
- },
1726
- {
1727
- name: "refactor:scan",
1728
- decision: "Whether the refactoring scope still holds high- or medium-priority candidates",
1729
- args: "",
1730
- vocabulary: ["clear", "candidates", "error"],
1731
- single_source: "prompts/refactoring.md"
1732
- },
1733
- {
1734
- name: "split:assess",
1735
- decision: "Whether a change size clears the split guide (the split assessment size question)",
1736
- args: "[--json]",
1737
- vocabulary: ["split", "single"],
1738
- single_source: SPLIT_ASSESSMENT_QUESTION
1739
- },
1740
- {
1741
- name: "sonar:hotspots",
1742
- decision: "The Step B disposition for each SonarCloud hotspot on a pull request",
1743
- args: "<PR>",
1744
- vocabulary: ["excluded", "local", "fix", "defer", "unreadable"],
1745
- single_source: "prompts/sonar-hotspot-handling.md"
1746
- },
1747
- {
1748
- name: "clone:scan",
1749
- decision: "Whether cross-file or cross-repository code duplication exists",
1750
- args: "",
1751
- vocabulary: ["clean", "clones:"],
1752
- single_source: "prompts/collaboration-workflow/no-clones.md"
1753
- },
1754
- {
1755
- name: "epic:reconcile",
1756
- command: "epic",
1757
- decision: "Whether an epic's declaration matches its recorded relations, and the repair when it does not",
1758
- args: "--reconcile <E>",
1759
- vocabulary: [RECONCILED, NOTHING_TO_RECONCILE],
1760
- single_source: "docs/josh-commands.md \u2192 `josh epic --reconcile`"
1761
- },
1762
- {
1763
- name: "lane:list",
1764
- decision: "Whether each in-progress issue holds a live lane, and the two-directional difference",
1765
- args: "",
1766
- vocabulary: [LIVE, STOPPED, UNKNOWN],
1767
- single_source: "docs/josh-commands.md \u2192 The `in-progress` / lane difference"
1768
- }
1769
- ];
1770
- function get_command(oracle) {
1771
- return oracle.command ?? oracle.name;
1772
- }
1773
- function find_oracle(name) {
1774
- return DECISION_ORACLES.find((oracle) => oracle.name === name.trim());
1775
- }
1776
- var decision_oracle = {
1777
- DECISION_ORACLES,
1778
- find_oracle,
1779
- get_command
1780
- };
1781
-
1782
- // scripts/rules/oracle-firing.ts
1783
- var PACKAGE_ADD = /^(?:pnpm|yarn|bun)\s+add\s+\S|^npm\s+(?:install|i)\s+[^-\s]/u;
1784
- function is_package_add(command) {
1785
- return shell_segments.segments_of(command).some((segment) => PACKAGE_ADD.test(segment));
1786
- }
1787
- var FIRING = /* @__PURE__ */ new Map([
1788
- [
1789
- "pkg:scout",
1790
- {
1791
- describes: "a package-add command (`pnpm add \u2026`, `npm install <pkg>`)",
1792
- governs: is_package_add
1793
- }
1794
- ],
1795
- [
1796
- "issue:lint",
1797
- {
1798
- describes: "an Issue-filing call (`gh api \u2026/issues`, `gh issue create`)",
1799
- governs: bash_triggers.is_issue_filing
1800
- }
1801
- ]
1802
- ]);
1803
- var PHASE = "the act it governs is a phase of a run, not a single shell call a trigger can anchor on";
1804
- var READ = "it answers a status question and governs no action of its own to gate before";
1805
- var SELF = "the command is itself the governed act; there is no earlier call to gate before it";
1806
- var LATEST_SCOPE_REASON = "the run it governs (`pnpm josh latest`) is already gated at its point of use by the latest-gate; a firing here would double-gate it";
1807
- var EPIC_BUNDLE_REASON = "it runs after a filing to place it, and that filing is already gated by `issue-scout`; it has no earlier call of its own";
1808
- var RELEASE_SCOPE_REASON = "it reads the release owed *after* `pnpm josh followup` has merged (`git_followup_pending`), so it trails the merge rather than gating it; a firing on `followup` would gate a check that must run once the merge is done, reading a pre-merge state that has not yet arrived";
1809
- var NOT_NAMED = /* @__PURE__ */ new Map([
1810
- ["release:scope", RELEASE_SCOPE_REASON],
1811
- ["delegate", PHASE],
1812
- ["review:level", READ],
1813
- ["review:round2", READ],
1814
- ["disposition", READ],
1815
- ["review:attest", READ],
1816
- ["latest:scope", LATEST_SCOPE_REASON],
1817
- ["run:hold", SELF],
1818
- ["cost:cut", READ],
1819
- ["epic:bundle", EPIC_BUNDLE_REASON],
1820
- ["issue:scout", "the filing it governs is already gated by the `issue-scout` delivered rule"],
1821
- [
1822
- "issue:fold",
1823
- "the second filing it governs is already gated by the `issue-fold` delivered rule"
1824
- ],
1825
- ["cases", READ],
1826
- ["issue:state", READ],
1827
- ["run:cut:resume", SELF],
1828
- ["run:cut:gate", SELF],
1829
- ["backlog:budget", READ],
1830
- ["run:liveness", READ],
1831
- ["stash:pop", SELF],
1832
- ["backlog:next", READ],
1833
- ["auto-ok:next", READ],
1834
- ["epic:next", READ],
1835
- ["run:merge", PHASE],
1836
- ["run:step", SELF],
1837
- ["refactor:scan", PHASE],
1838
- ["split:assess", PHASE],
1839
- ["sonar:hotspots", READ],
1840
- ["clone:scan", PHASE],
1841
- ["epic:reconcile", SELF],
1842
- ["lane:list", READ]
1843
- ]);
1844
- function declaration_for(name) {
1845
- const firing_point = FIRING.get(name);
1846
- if (firing_point !== void 0) return { firing_point };
1847
- const not_firing_reason = NOT_NAMED.get(name);
1848
- return not_firing_reason === void 0 ? {} : { not_firing_reason };
1849
- }
1850
- function firing_oracles() {
1851
- return decision_oracle.DECISION_ORACLES.flatMap((oracle) => {
1852
- const firing_point = FIRING.get(oracle.name);
1853
- return firing_point === void 0 ? [] : [{ oracle, firing_point }];
1854
- });
1855
- }
1856
- var oracle_firing = {
1857
- FIRING,
1858
- NOT_NAMED,
1859
- declaration_for,
1860
- firing_oracles,
1861
- is_package_add
1862
- };
1863
-
1864
- // scripts/rules/oracle-consulted.ts
1865
- function command_names(oracle) {
1866
- return /* @__PURE__ */ new Set([decision_oracle.get_command(oracle)]);
1867
- }
1868
- function runs_the_oracle(command, names) {
1869
- return shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, names));
1870
- }
1871
- function command_line(oracle) {
1872
- const command = decision_oracle.get_command(oracle);
1873
- return oracle.args ? `pnpm josh ${command} ${oracle.args}` : `pnpm josh ${command}`;
1874
- }
1875
- function reason_for(oracle, firing_point) {
1876
- return `\u26D4 decision oracle not consulted: this is ${firing_point.describes}, but the run has not run \`${command_line(oracle)}\` first \u2014 the oracle that decides ${oracle.decision.toLowerCase()}. It answers ${oracle.vocabulary.join(" | ")}; single source ${oracle.single_source}. Reissue after running it and reading its verdict. It fires once per run and cannot repeat on the call in hand.`;
1877
- }
1878
- function row_for(oracle, firing_point) {
1879
- const names = command_names(oracle);
1880
- return {
1881
- id: `oracle-consulted:${oracle.name}`,
1882
- is_trigger: bash_triggers.on_bash_command(firing_point.governs),
1883
- reason: reason_for(oracle, firing_point),
1884
- already_satisfied: (tail, _call) => tail_commands.prior_bash_commands(tail).some((command) => runs_the_oracle(command, names)),
1885
- keeps: bash_triggers.on_bash_command((command) => runs_the_oracle(command, names))
1886
- };
1887
- }
1888
- var ROWS = oracle_firing.firing_oracles().map(({ oracle, firing_point }) => row_for(oracle, firing_point));
1889
- var oracle_consulted = { ROWS, command_line, reason_for, row_for, runs_the_oracle };
1890
-
1891
- // scripts/rules/piped-verification.ts
1892
- var VERIFICATION_COMMANDS = /* @__PURE__ */ new Set([
1893
- "check",
1894
- "cspell",
1895
- "cspell:dot",
1896
- "eval",
1897
- "gate",
1898
- "lint",
1899
- "lint:eslint",
1900
- "lint:prettier",
1901
- "lint:related",
1902
- "overrides",
1903
- "ranges",
1904
- "test",
1905
- "test:e2e",
1906
- "test:related",
1907
- "test:unit"
1908
- ]);
1909
- function is_verification_command(segment) {
1910
- return shell_segments.is_josh_command(segment, VERIFICATION_COMMANDS);
1911
- }
1912
- function is_masked_verification(command) {
1913
- return time_shell.discarded_commands(command).some((segment) => is_verification_command(segment));
1914
- }
1915
- 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.";
1916
- function command_pieces(command) {
1917
- return shell_segments.segments_of(time_shell.unquoted(command)).flatMap((segment) => segment.split("|")).map((piece) => piece.trim());
1918
- }
1919
- function runs_verification(command) {
1920
- return command_pieces(command).some((piece) => is_verification_command(piece));
1921
- }
1922
- function keeps_verdict_intact(command) {
1923
- return runs_verification(command) && !is_masked_verification(command);
1924
- }
1925
- var piped_verification = {
1926
- PIPED_VERIFICATION_REASON,
1927
- VERIFICATION_COMMANDS,
1928
- is_masked_verification,
1929
- keeps_verdict_intact,
1930
- runs_verification
1931
- };
1932
-
1933
- // scripts/rules/pre-gate-cut.ts
1934
- var GATE_COMMANDS = /* @__PURE__ */ new Set(["gate"]);
1935
- var CUT_COMMANDS2 = /* @__PURE__ */ new Set(["run:cut"]);
1936
- var CUT_MODE_FLAG = /(?:^|\s)--(?:resume|end|json)(?:[=\s]|$)/u;
1937
- var HOLD_COMMANDS = /* @__PURE__ */ new Set(["run:hold"]);
1938
- var HOLD_MODE_FLAG = /(?:^|\s)--release(?:[=\s]|$)/u;
1939
- function invokes_josh(command, names, mode_flag) {
1940
- return shell_segments.segments_of(command).some(
1941
- (segment) => shell_segments.is_josh_command(segment, names) && mode_flag?.test(segment) !== true
1942
- );
1943
- }
1944
- function runs_the_gate(command) {
1945
- return invokes_josh(command, GATE_COMMANDS);
1946
- }
1947
- function takes_the_cut(command) {
1948
- return invokes_josh(command, CUT_COMMANDS2, CUT_MODE_FLAG);
1949
- }
1950
- function claims_the_hold(command) {
1951
- return invokes_josh(command, HOLD_COMMANDS, HOLD_MODE_FLAG);
1952
- }
1953
- function asks_about_the_cut(command) {
1954
- return invokes_josh(command, CUT_COMMANDS2);
1955
- }
1956
- function current_state2() {
1957
- return {
1958
- directory: process.cwd(),
1959
- carried: run_cut.carried_cut_sync,
1960
- marked_issue: lane_child_marker.marked_issue(),
1961
- context_verdict: cost_cli.session_verdict
1962
- };
1963
- }
1964
- function uncut_lane_issue(state) {
1965
- const issue = lane_paths.lane_issue_of(state.directory);
1966
- if (issue === void 0) return void 0;
1967
- if (state.marked_issue !== issue) return void 0;
1968
- return state.carried()?.issue === issue ? void 0 : issue;
1969
- }
1970
- function warrants_the_cut(verdict) {
1971
- return verdict !== cost_verdict.UNDER_VERDICT;
1972
- }
1973
- function is_uncut_gate(command, state = current_state2()) {
1974
- if (!runs_the_gate(command) || uncut_lane_issue(state) === void 0) return false;
1975
- return warrants_the_cut(state.context_verdict());
1976
- }
1977
- 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.";
1978
- var pre_gate_cut = {
1979
- PRE_GATE_CUT_REASON,
1980
- asks_about_the_cut,
1981
- claims_the_hold,
1982
- is_uncut_gate,
1983
- runs_the_gate,
1984
- takes_the_cut,
1985
- uncut_lane_issue
1986
- };
1987
-
1988
- // scripts/rules/prior-comment-read.ts
1989
- var ISSUE_PATH_NUMBER = /issues\/(\d+)/gu;
1990
- var ISSUE_VIEW_NUMBER = /\bissue\s+view\s+(?:-{1,2}\S+\s+)*(\d+)/u;
1991
- var ISSUE_READ_ARGUMENTS = /\bissue:read\b([\s\d]*)/u;
1992
- var A_NUMBER = /^\d+$/u;
1993
- var COMMENTS_ENDPOINT = /issues\/\d+\/comments\b/u;
1994
- var VIEW_WITH_COMMENTS = /\bissue\s+view\b[^\n]*--comments\b/u;
1995
- var ISSUE_READ_COMMAND = /\bissue:read\b/u;
1996
- function add_issue_read_numbers(command, numbers) {
1997
- const match = ISSUE_READ_ARGUMENTS.exec(command);
1998
- if (match === null) return;
1999
- const tokens = (match[1] ?? "").split(/\s+/u);
2000
- for (const token of tokens) {
2001
- if (A_NUMBER.test(token)) numbers.add(Number(token));
2002
- }
2003
- }
2004
- function issue_numbers_of(command) {
2005
- const numbers = /* @__PURE__ */ new Set();
2006
- for (const match of command.matchAll(ISSUE_PATH_NUMBER)) numbers.add(Number(match[1]));
2007
- const view = ISSUE_VIEW_NUMBER.exec(command);
2008
- if (view !== null) numbers.add(Number(view[1]));
2009
- add_issue_read_numbers(command, numbers);
2010
- return numbers;
2011
- }
2012
- function fetches_comments(command) {
2013
- return COMMENTS_ENDPOINT.test(command) || VIEW_WITH_COMMENTS.test(command) || ISSUE_READ_COMMAND.test(command);
2014
- }
2015
- function reads_target_comments(command, target) {
2016
- if (!fetches_comments(command)) return false;
2017
- for (const number of issue_numbers_of(command)) {
2018
- if (target.has(number)) return true;
2019
- }
2020
- return false;
2021
- }
2022
- function already_read_comments_for(tail, command) {
2023
- const target = issue_numbers_of(command);
2024
- if (target.size === 0) return false;
2025
- return tail_commands.prior_bash_commands(tail).some((prior) => reads_target_comments(prior, target));
2026
- }
2027
- function already_read_for_call(tail, call) {
2028
- if (call.name !== cost_blocks.BASH_TOOL) return false;
2029
- return already_read_comments_for(tail, time_shell.bash_command(call.input));
2030
- }
2031
- var prior_comment_read = {
2032
- already_read_comments_for,
2033
- already_read_for_call,
2034
- fetches_comments,
2035
- issue_numbers_of
2036
- };
2037
-
2038
- // scripts/rules/raw-field-body.ts
2039
- var RAW_FIELD_BODY_AT = /(?:^|\s)(?:-f|--raw-field)[\s=]+['"]?body=['"]?@/u;
2040
- function posts_at_path_literally(command) {
2041
- return RAW_FIELD_BODY_AT.test(command);
2042
- }
2043
- var RAW_FIELD_BODY_REASON = "\u26D4 literal @path body: this passes `body=@<path>` to `gh api` with a raw field flag (`-f` / `--raw-field`), which sends the value verbatim \u2014 the string `@<path>` is posted as the comment, not the file it points at. `-F` / `--field` is the flag that reads the file, and the two spellings differ by one character while `gh` exits 0 and returns a comment URL either way, so the break is invisible until a person reads the issue (joshuafolkken/kit#2304 lost two park comments this way). Post an issue or PR comment with `pnpm josh issue:comment <N> --body-file <path>` \u2014 the one spelling, which cannot take the wrong flag. The rule and the safe forms are in `prompts/collaboration-workflow/shell-body.md`. This rule fires on every occurrence, not once per run.";
2044
- var ROW11 = {
2045
- id: "raw-field-body",
2046
- is_trigger: bash_triggers.on_bash_command(posts_at_path_literally),
2047
- reason: RAW_FIELD_BODY_REASON,
2048
- decide: () => true
2049
- };
2050
- var raw_field_body = { RAW_FIELD_BODY_REASON, ROW: ROW11, posts_at_path_literally };
2051
-
2052
- // scripts/rules/rule-body-guard.ts
2053
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
2054
- var EDIT_TOOL = "Edit";
2055
- var WRITE_TOOL = "Write";
2056
- var CLAUDE_DOCUMENT = /(?:^|\/)CLAUDE\.md$/u;
2057
- var PROMPT_DOCUMENT = /(?:^|\/)prompts\/.+\.md$/u;
2058
- var SKILL_DOCUMENT = /(?:^|\/)\.claude\/skills\/.+\.md$/u;
2059
- function is_rule_document(file_path) {
2060
- return CLAUDE_DOCUMENT.test(file_path) || PROMPT_DOCUMENT.test(file_path) || SKILL_DOCUMENT.test(file_path);
2061
- }
2062
- var LINK_TARGET = /\]\([^)]*\)/gu;
2063
- var BARE_URL = /https?:\/\/\S+/gu;
2064
- function prose_of(text2) {
2065
- return text2.replaceAll(LINK_TARGET, "]()").replaceAll(BARE_URL, "");
2066
- }
2067
- var MIN_APPENDED_PROSE = 120;
2068
- function adds_rule_prose(old_text, new_text) {
2069
- return prose_of(new_text).length - prose_of(old_text).length >= MIN_APPENDED_PROSE;
2070
- }
2071
- function string_field(input, key) {
2072
- const value = input[key];
2073
- return typeof value === "string" ? value : "";
2074
- }
2075
- function read_existing(file_path) {
2076
- return existsSync2(file_path) ? readFileSync3(file_path, "utf8") : "";
2077
- }
2078
- function texts_of(name, input, read) {
2079
- if (name === WRITE_TOOL) {
2080
- return {
2081
- old_text: read(string_field(input, "file_path")),
2082
- new_text: string_field(input, "content")
2083
- };
2084
- }
2085
- return {
2086
- old_text: string_field(input, "old_string"),
2087
- new_text: string_field(input, "new_string")
2088
- };
2089
- }
2090
- function is_edit_tool2(name) {
2091
- return name === EDIT_TOOL || name === WRITE_TOOL;
2092
- }
2093
- function rule_document_input(call) {
2094
- if (!is_edit_tool2(call.name) || !json_value.is_record(call.input)) return void 0;
2095
- return is_rule_document(string_field(call.input, "file_path")) ? call.input : void 0;
2096
- }
2097
- function writes_rule_prose(call, read = read_existing) {
2098
- const input = rule_document_input(call);
2099
- if (input === void 0) return false;
2100
- const { old_text, new_text } = texts_of(call.name, input, read);
2101
- return adds_rule_prose(old_text, new_text);
2102
- }
2103
- 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.";
2104
- var PLACEMENT_ORACLES = [
2105
- /* @__PURE__ */ new Set(["oracle:list"]),
2106
- /* @__PURE__ */ new Set(["run:step", "run:next"])
2107
- ];
2108
- function ran_oracle(tail_commands_run, names) {
2109
- return tail_commands_run.some(
2110
- (command) => shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, names))
2111
- );
2112
- }
2113
- function answered_placement(tail, _call) {
2114
- const commands = tail_commands.prior_bash_commands(tail);
2115
- return PLACEMENT_ORACLES.every((names) => ran_oracle(commands, names));
2116
- }
2117
- function runs_placement_oracle(command) {
2118
- return PLACEMENT_ORACLES.some(
2119
- (names) => shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, names))
2120
- );
2121
- }
2122
- var ROW12 = {
2123
- id: "rule-body",
2124
- is_trigger: writes_rule_prose,
2125
- reason: RULE_BODY_REASON,
2126
- already_satisfied: answered_placement,
2127
- keeps: bash_triggers.on_bash_command(runs_placement_oracle)
2128
- };
2129
- var rule_body_guard = {
2130
- RULE_BODY_REASON,
2131
- ROW: ROW12,
2132
- adds_rule_prose,
2133
- answered_placement,
2134
- is_rule_document,
2135
- prose_of,
2136
- runs_placement_oracle,
2137
- writes_rule_prose
2138
- };
2139
-
2140
- // scripts/rules/run-tail.ts
2141
- var SEGMENT_PREFIX = String.raw`(?:\(\s*)?(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:npx\s+|pnpm\s+(?:exec\s+|run\s+)?)?`;
2142
- var WORD_END = String.raw`(?=\s|$|\))`;
2143
- var PUSH_STEP_SEGMENT = new RegExp(String.raw`^${SEGMENT_PREFIX}josh\s+(?:git|g)${WORD_END}`, "u");
2144
- var CONFIRMED_FLAG = new RegExp(String.raw`(?:^|\s)(?:-y|--yes)${WORD_END}`, "u");
2145
- var SKIPS_THE_PUSH = new RegExp(String.raw`(?:^|\s)--skip-push${WORD_END}`, "u");
2146
- var BACKGROUND_KEY = "run_in_background";
2147
- var QUOTED_SPAN = /"[^"]*"|'[^']*'/gu;
2148
- function flags_of(segment) {
2149
- return segment.replaceAll(QUOTED_SPAN, " ");
2150
- }
2151
- function is_push_step_segment(segment) {
2152
- if (!PUSH_STEP_SEGMENT.test(segment)) return false;
2153
- const flags = flags_of(segment);
2154
- if (SKIPS_THE_PUSH.test(flags)) return false;
2155
- return CONFIRMED_FLAG.test(flags);
2156
- }
2157
- function is_push_step(command) {
2158
- return shell_segments.segments_of(command).some((segment) => is_push_step_segment(segment));
2159
- }
2160
- function is_backgrounded(input) {
2161
- if (!json_value.is_record(input)) return false;
2162
- return input[BACKGROUND_KEY] === true;
2163
- }
2164
- function is_push_step_call(call) {
2165
- if (call.name !== cost_blocks.BASH_TOOL) return false;
2166
- return is_push_step(time_shell.bash_command(call.input));
2167
- }
2168
- function is_backgrounded_push_step(call) {
2169
- if (!is_backgrounded(call.input)) return false;
2170
- return is_push_step_call(call);
2171
- }
2172
- function is_foreground_push_step(call) {
2173
- if (is_backgrounded(call.input)) return false;
2174
- return is_push_step_call(call);
2175
- }
2176
- function decide3() {
2177
- return true;
2178
- }
2179
- 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.';
2180
- var run_tail = {
2181
- RUN_TAIL_REASON,
2182
- decide: decide3,
2183
- is_backgrounded_push_step,
2184
- is_foreground_push_step,
2185
- is_push_step,
2186
- is_push_step_call
2187
- };
2188
-
2189
- // scripts/rules/shell-body-trigger.ts
2190
- var FIELD_FLAGS = "(?:-f|-F|--field|--raw-field)";
2191
- var MESSAGE_FLAGS = "(?:--body|--notify-message)";
2192
- var FIELD_OPENING = String.raw`${FIELD_FLAGS}[\s=]*(?:'?body="|"body=)`;
2193
- var MESSAGE_OPENING = String.raw`${MESSAGE_FLAGS}[\s=]+"`;
2194
- var NESTED_SUBSTITUTION = String.raw`\$\([^()]*\)`;
2195
- var SUBSTITUTION = String.raw`\$\((?:[^()$]|\$(?!\()|${NESTED_SUBSTITUTION})*\)`;
2196
- var QUOTED_VALUE = String.raw`(?:[^"\\$]|\\.|\$(?!\()|${SUBSTITUTION})*`;
2197
- var INLINE_BODY_VALUE = new RegExp(
2198
- `(?:${FIELD_OPENING}|${MESSAGE_OPENING})(${QUOTED_VALUE})"`,
2199
- "gu"
2200
- );
2201
- var SHELL_EVALUATED = /[`$]/u;
2202
- var WHOLE_VALUE_SUBSTITUTION = new RegExp(`^${SUBSTITUTION}$`, "u");
2203
- var BACKTICK = "`";
2204
- var WHOLE_VALUE_FILE_REFERENCE = /^@\S+$/u;
2205
- var ESCAPED_PAIR = /\\./gu;
2206
- function is_whole_value_substitution(literal) {
2207
- return WHOLE_VALUE_SUBSTITUTION.test(literal) && !literal.includes(BACKTICK);
2208
- }
2209
- function is_body_already_in_a_file(literal) {
2210
- return WHOLE_VALUE_FILE_REFERENCE.test(literal) || is_whole_value_substitution(literal);
2211
- }
2212
- function is_evaluated_value(raw_value) {
2213
- const literal = raw_value.replaceAll(ESCAPED_PAIR, "");
2214
- if (is_body_already_in_a_file(literal)) return false;
2215
- return SHELL_EVALUATED.test(literal);
2216
- }
2217
- function is_shell_evaluated_body(command) {
2218
- for (const match of command.matchAll(INLINE_BODY_VALUE)) {
2219
- if (is_evaluated_value(match[1] ?? "")) return true;
2220
- }
2221
- return false;
2222
- }
2223
- var FILE_FLAG = /(?:--body-file|--notify-message-file)(?:[\s=]|$)/u;
2224
- var FIELD_FILE_REFERENCE = new RegExp(String.raw`${FIELD_FLAGS}[\s=]*body=@\S`, "u");
2225
- var ANSI_C_BODY = /(?:(?:--body|--notify-message)[\s=]+|body=)\$'/u;
2226
- var DOUBLE_QUOTED_SPAN = /"[^"]*"/gu;
2227
- var BODY_FLAG = new RegExp(
2228
- String.raw`${FIELD_FLAGS}[\s=]*'?"?body=|${MESSAGE_FLAGS}[\s=]|--body-file[\s=]|--notify-message-file[\s=]`,
2229
- "u"
2230
- );
2231
- var QUOTED_SPAN2 = /"[^"]*"|'[^']*'/gu;
2232
- function inline_body_literals(command) {
2233
- return Array.from(
2234
- command.matchAll(INLINE_BODY_VALUE),
2235
- (match) => (match[1] ?? "").replaceAll(ESCAPED_PAIR, "")
2236
- );
2237
- }
2238
- function is_safe_body_form(command) {
2239
- if (ANSI_C_BODY.test(command.replaceAll(DOUBLE_QUOTED_SPAN, " "))) return true;
2240
- const flags = command.replaceAll(QUOTED_SPAN2, " ");
2241
- if (FILE_FLAG.test(flags) || FIELD_FILE_REFERENCE.test(flags)) return true;
2242
- return inline_body_literals(command).some((literal) => is_body_already_in_a_file(literal));
2243
- }
2244
- function keeps_body_safe(command) {
2245
- return is_safe_body_form(command) && !is_shell_evaluated_body(command);
2246
- }
2247
- function carries_a_body(command) {
2248
- if (BODY_FLAG.test(command.replaceAll(QUOTED_SPAN2, " "))) return true;
2249
- return inline_body_literals(command).length > 0;
2250
- }
2251
- var SHELL_BODY_REASON = "\u26D4 shell-evaluated body: this command carries a body inline in double quotes, and that body contains a backtick or a `$`, so the shell runs it \u2014 the text is executed rather than merely mangled (joshuafolkken/kit#1198). Write the body to a file and pass it by path: `pnpm josh issue:comment <N> --body-file <path>`, `pnpm josh followup --notify-message-file <path>`, or `--body-file <path>` wherever offered. `$'\u2026'` quoting is the other safe form; the rule and the trigger's blind spots are in `prompts/collaboration-workflow/shell-body.md`. Reissue this call once the body is in a file \u2014 it fires once per run and cannot repeat on the call in hand.";
2252
- var shell_body_trigger = {
2253
- SHELL_BODY_REASON,
2254
- carries_a_body,
2255
- is_safe_body_form,
2256
- is_shell_evaluated_body,
2257
- keeps_body_safe
2258
- };
2259
-
2260
- // scripts/test/test-declared-changed.ts
2261
- import { execFileSync } from "node:child_process";
2262
-
2263
- // scripts/test/test-declared-logic.ts
2264
- var TEST_SUFFIXES = [".e2e.ts", ".test.ts"];
2265
- var EXEMPT_PATHS = [".editorconfig"];
2266
- var EXEMPT_PREFIXES = [".idea/", ".vscode/", "prompts/"];
2267
- var EXEMPT_SUFFIXES = [".md"];
2268
- function normalize(paths) {
2269
- return paths.map((path5) => path5.trim()).filter((path5) => path5 !== "");
2270
- }
2271
- function is_test_file(path5) {
2272
- return TEST_SUFFIXES.some((suffix) => path5.endsWith(suffix));
2273
- }
2274
- function is_exempt(path5) {
2275
- if (EXEMPT_PATHS.includes(path5)) return true;
2276
- return EXEMPT_SUFFIXES.some((suffix) => path5.endsWith(suffix)) || EXEMPT_PREFIXES.some((prefix) => path5.startsWith(prefix));
2277
- }
2278
- function is_runtime(path5) {
2279
- return !is_test_file(path5) && !is_exempt(path5);
2280
- }
2281
- function verdict_for(paths) {
2282
- const changed = normalize(paths);
2283
- if (changed.some((path5) => is_test_file(path5))) return "satisfied";
2284
- if (changed.some((path5) => is_runtime(path5))) return "required";
2285
- return "exempt";
2286
- }
2287
- function runtime_files(paths) {
2288
- return normalize(paths).filter((path5) => is_runtime(path5));
2289
- }
2290
- function exempt_files(paths) {
2291
- return normalize(paths).filter((path5) => is_exempt(path5));
2292
- }
2293
- var test_declared_logic = {
2294
- EXEMPT_PATHS,
2295
- EXEMPT_PREFIXES,
2296
- EXEMPT_SUFFIXES,
2297
- TEST_SUFFIXES,
2298
- exempt_files,
2299
- is_exempt,
2300
- is_runtime,
2301
- is_test_file,
2302
- runtime_files,
2303
- verdict_for
2304
- };
2305
-
2306
- // scripts/test/test-declared-changed.ts
2307
- var RENAME_ARROW = " -> ";
2308
- var STATUS_PREFIX_LENGTH = 3;
2309
- function path_of(line) {
2310
- const body = line.slice(STATUS_PREFIX_LENGTH);
2311
- const arrow = body.indexOf(RENAME_ARROW);
2312
- return arrow === -1 ? body : body.slice(arrow + RENAME_ARROW.length);
2313
- }
2314
- function read_changed_paths_sync() {
2315
- try {
2316
- const output = execFileSync("git", ["status", "--porcelain"], {
2317
- encoding: "utf8",
2318
- stdio: ["ignore", "pipe", "ignore"]
2319
- });
2320
- return output.split("\n").filter((line) => line.length > STATUS_PREFIX_LENGTH).map((line) => path_of(line));
2321
- } catch {
2322
- return [];
2323
- }
2324
- }
2325
- var injected = {};
2326
- function source_paths() {
2327
- return injected.paths ?? read_changed_paths_sync();
2328
- }
2329
- function current_verdict(paths = source_paths()) {
2330
- return test_declared_logic.verdict_for(paths);
2331
- }
2332
- function restore_injection(previous) {
2333
- if (previous === void 0) {
2334
- delete injected.paths;
2335
- return;
2336
- }
2337
- injected.paths = previous;
2338
- }
2339
- function with_paths(paths, body) {
2340
- const previous = injected.paths;
2341
- injected.paths = paths;
2342
- try {
2343
- return body();
2344
- } finally {
2345
- restore_injection(previous);
2346
- }
2347
- }
2348
- var test_declared_changed = { current_verdict, path_of, read_changed_paths_sync, with_paths };
2349
-
2350
- // scripts/rules/test-declared-commit.ts
2351
- function is_untested_commit(call) {
2352
- if (!run_tail.is_push_step_call(call)) return false;
2353
- return test_declared_changed.current_verdict() === "required";
2354
- }
2355
- 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.';
2356
- var ROW13 = { id: "test-declared", is_trigger: is_untested_commit, reason: REASON };
2357
- var test_declared_commit = { REASON, ROW: ROW13, is_untested_commit };
2358
-
2359
- // scripts/discovery/repo-discovery.ts
2360
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
2361
- import path4 from "node:path";
2362
-
2363
- // scripts/discovery/repo-origin.ts
2364
- var GITHUB_HOST = "github.com";
2365
- var SSH_ALIAS_PATTERN = /^github(?:-\w+)*$/u;
2366
- var GIT_SUFFIX = ".git";
2367
- var PATH_SEPARATOR = "/";
2368
- var USER_SEPARATOR = "@";
2369
- var HOST_SEPARATOR = ":";
2370
- var OWNER_INDEX = 0;
2371
- var REPO_INDEX = 1;
2372
- var REPO_PATH_SEGMENTS = 2;
2373
- var NOT_FOUND = -1;
2374
- var SECTION_PREFIX = "[";
2375
- var ORIGIN_SECTION = /^\[remote\s+"origin"\]$/u;
2376
- var URL_KEY = "url";
2377
- function is_github_hostname(host) {
2378
- return host.toLowerCase() === GITHUB_HOST;
2379
- }
2380
- function is_github_ssh_host(host) {
2381
- const normalized = host.toLowerCase();
2382
- return is_github_hostname(normalized) || SSH_ALIAS_PATTERN.test(normalized);
2383
- }
2384
- function strip_git_suffix(name) {
2385
- return name.endsWith(GIT_SUFFIX) ? name.slice(0, -GIT_SUFFIX.length) : name;
2386
- }
2387
- function to_identity(repository_path) {
2388
- const segments = repository_path.split(PATH_SEPARATOR).filter((segment) => segment !== "");
2389
- if (segments.length !== REPO_PATH_SEGMENTS) return void 0;
2390
- const owner = segments[OWNER_INDEX] ?? "";
2391
- const repo = strip_git_suffix(segments[REPO_INDEX] ?? "");
2392
- return repo === "" ? void 0 : { owner, repo };
2393
- }
2394
- function parse_scp_like(url) {
2395
- const user_end = url.indexOf(USER_SEPARATOR);
2396
- const authority = user_end === NOT_FOUND ? url : url.slice(user_end + 1);
2397
- const host_end = authority.indexOf(HOST_SEPARATOR);
2398
- if (host_end === NOT_FOUND) return void 0;
2399
- if (!is_github_ssh_host(authority.slice(0, host_end))) return void 0;
2400
- return to_identity(authority.slice(host_end + 1));
2401
- }
2402
- function parse_url_like(url) {
2403
- try {
2404
- const parsed = new URL(url);
2405
- return is_github_hostname(parsed.hostname) ? to_identity(parsed.pathname) : void 0;
2406
- } catch {
2407
- return void 0;
2408
- }
2409
- }
2410
- function parse_origin_url(url) {
2411
- const trimmed2 = url.trim();
2412
- if (trimmed2 === "") return void 0;
2413
- return parse_url_like(trimmed2) ?? parse_scp_like(trimmed2);
2414
- }
2415
- function is_section_header(line) {
2416
- return line.startsWith(SECTION_PREFIX);
2417
- }
2418
- function origin_section_lines(lines) {
2419
- const start = lines.findIndex((line) => ORIGIN_SECTION.test(line));
2420
- if (start === NOT_FOUND) return [];
2421
- const rest = lines.slice(start + 1);
2422
- const end = rest.findIndex((line) => is_section_header(line));
2423
- return end === NOT_FOUND ? rest : rest.slice(0, end);
2424
- }
2425
- function entry_value(line, key) {
2426
- const separator = line.indexOf("=");
2427
- if (separator === NOT_FOUND) return void 0;
2428
- if (line.slice(0, separator).trim() !== key) return void 0;
2429
- return line.slice(separator + 1).trim();
2430
- }
2431
- function parse_origin_from_config(content) {
2432
- const lines = content.split("\n").map((line) => line.trim());
2433
- return origin_section_lines(lines).map((line) => entry_value(line, URL_KEY)).find((url) => url !== void 0);
2434
- }
2435
- function format_identity(identity) {
2436
- return `${identity.owner}${PATH_SEPARATOR}${identity.repo}`.toLowerCase();
2437
- }
2438
- var repo_origin = {
2439
- GITHUB_HOST,
2440
- is_github_hostname,
2441
- is_github_ssh_host,
2442
- parse_origin_from_config,
2443
- parse_origin_url,
2444
- format_identity
2445
- };
2446
-
2447
- // scripts/discovery/repo-map-logic.ts
2448
- var OVERRIDE_ENTRY_SEPARATOR = ",";
2449
- var OVERRIDE_PAIR_SEPARATOR = "=";
2450
- var OVERRIDE_PAIR_PARTS = 2;
2451
- var KEY_INDEX = 0;
2452
- var PATH_INDEX = 1;
2453
- function is_same_owner(identity, current_owner2) {
2454
- return identity.owner.toLowerCase() === current_owner2.toLowerCase();
2455
- }
2456
- function add_entry(target, origin_url, repository_path, current_owner2) {
2457
- if (origin_url === void 0) return;
2458
- const identity = repo_origin.parse_origin_url(origin_url);
2459
- if (identity === void 0 || !is_same_owner(identity, current_owner2)) return;
2460
- target.set(repo_origin.format_identity(identity), repository_path);
2461
- }
2462
- function parse_override_entry(entry) {
2463
- const parts = entry.split(OVERRIDE_PAIR_SEPARATOR).map((part) => part.trim());
2464
- if (parts.length !== OVERRIDE_PAIR_PARTS) return void 0;
2465
- const key = parts[KEY_INDEX] ?? "";
2466
- const repository_path = parts[PATH_INDEX] ?? "";
2467
- if (repository_path === "") return void 0;
2468
- return { path: repository_path, origin_url: `https://${repo_origin.GITHUB_HOST}/${key}` };
2469
- }
2470
- function parse_overrides(raw) {
2471
- if (raw === void 0) return [];
2472
- return raw.split(OVERRIDE_ENTRY_SEPARATOR).map((entry) => parse_override_entry(entry)).filter((entry) => entry !== void 0);
2473
- }
2474
- function build_repository_map(discovered, current_owner2, override_value2) {
2475
- const result = /* @__PURE__ */ new Map();
2476
- for (const entry of discovered) add_entry(result, entry.origin_url, entry.path, current_owner2);
2477
- for (const entry of parse_overrides(override_value2)) {
2478
- add_entry(result, entry.origin_url, entry.path, current_owner2);
2479
- }
2480
- return result;
2481
- }
2482
- var repo_map_logic = {
2483
- is_same_owner,
2484
- parse_overrides,
2485
- build_repository_map
2486
- };
2487
-
2488
- // scripts/discovery/repo-discovery.ts
2489
- var GIT_DIRECTORY = ".git";
2490
- var GIT_CONFIG = "config";
2491
- var GITDIR_PREFIX = "gitdir:";
2492
- var COMMON_DIRECTORY_FILE = "commondir";
2493
- var OVERRIDE_ENV_KEY = "JOSH_REPO_PATHS";
2494
- function resolve_common_directory(git_directory) {
2495
- const pointer = path4.join(git_directory, COMMON_DIRECTORY_FILE);
2496
- if (!existsSync3(pointer)) return git_directory;
2497
- try {
2498
- return path4.resolve(git_directory, readFileSync4(pointer, "utf8").trim());
2499
- } catch {
2500
- return git_directory;
2501
- }
2502
- }
2503
- function follow_git_file(repository_path, git_file) {
2504
- const pointer = readFileSync4(git_file, "utf8").trim();
2505
- if (!pointer.startsWith(GITDIR_PREFIX)) return void 0;
2506
- return path4.resolve(repository_path, pointer.slice(GITDIR_PREFIX.length).trim());
2507
- }
2508
- function resolve_git_directory(repository_path) {
2509
- const candidate = path4.join(repository_path, GIT_DIRECTORY);
2510
- if (!existsSync3(candidate)) return void 0;
2511
- try {
2512
- if (statSync(candidate).isDirectory()) return candidate;
2513
- const linked = follow_git_file(repository_path, candidate);
2514
- return linked === void 0 ? void 0 : resolve_common_directory(linked);
2515
- } catch {
2516
- return void 0;
2517
- }
2518
- }
2519
- function read_origin_url(repository_path) {
2520
- const git_directory = resolve_git_directory(repository_path);
2521
- if (git_directory === void 0) return void 0;
2522
- try {
2523
- return repo_origin.parse_origin_from_config(
2524
- readFileSync4(path4.join(git_directory, GIT_CONFIG), "utf8")
2525
- );
2526
- } catch {
2527
- return void 0;
2528
- }
2529
- }
2530
- function sibling_directories(parent) {
2531
- try {
2532
- return readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path4.join(parent, entry.name)).toSorted((left, right) => left.localeCompare(right));
2533
- } catch {
2534
- return [];
2535
- }
2536
- }
2537
- function scan_siblings(parent) {
2538
- return sibling_directories(parent).map((repository_path) => ({
2539
- path: repository_path,
2540
- origin_url: read_origin_url(repository_path)
2541
- }));
2542
- }
2543
- function resolve_current_owner(repository_path) {
2544
- const origin_url = read_origin_url(repository_path);
2545
- if (origin_url === void 0) return void 0;
2546
- return repo_origin.parse_origin_url(origin_url)?.owner;
2547
- }
2548
- function main_worktree(repository_path) {
2549
- const git_directory = resolve_git_directory(repository_path);
2550
- if (git_directory === void 0) return repository_path;
2551
- if (path4.basename(git_directory) !== GIT_DIRECTORY) return repository_path;
2552
- return path4.dirname(git_directory);
2553
- }
2554
- function discover_repositories(repository_path, environment = process.env) {
2555
- const main2 = main_worktree(repository_path);
2556
- const current_owner2 = resolve_current_owner(main2);
2557
- if (current_owner2 === void 0) return /* @__PURE__ */ new Map();
2558
- return repo_map_logic.build_repository_map(
2559
- scan_siblings(path4.dirname(main2)),
2560
- current_owner2,
2561
- environment[OVERRIDE_ENV_KEY]
2562
- );
2563
- }
2564
- var repo_discovery = {
2565
- OVERRIDE_ENV_KEY,
2566
- main_worktree,
2567
- read_origin_url,
2568
- resolve_current_owner,
2569
- scan_siblings,
2570
- discover_repositories
2571
- };
2572
-
2573
- // scripts/discovery/repo-party.ts
2574
- var FIRST_PARTY = "first-party";
2575
- var THIRD_PARTY = "third-party";
2576
- var UNKNOWN2 = "unknown";
2577
- function classify2(session_owner, target) {
2578
- if (session_owner === void 0 || target === void 0) return UNKNOWN2;
2579
- return session_owner.toLowerCase() === target.toLowerCase() ? FIRST_PARTY : THIRD_PARTY;
2580
- }
2581
- function target_owner(owner_repo) {
2582
- return repo_origin.parse_origin_url(`https://${repo_origin.GITHUB_HOST}/${owner_repo}`)?.owner;
2583
- }
2584
- function current_owner(repository_path = process.cwd()) {
2585
- return repo_discovery.resolve_current_owner(repository_path);
2586
- }
2587
- var repo_party = { FIRST_PARTY, THIRD_PARTY, UNKNOWN: UNKNOWN2, classify: classify2, current_owner, target_owner };
2588
-
2589
- // scripts/rules/third-party-write.ts
2590
- function is_third_party_write_segment(segment, session_owner) {
2591
- if (!gh_api.is_gh_api(segment) || !gh_api.is_write(segment)) return false;
2592
- const target = gh_api.repo_target(segment);
2593
- if (target === void 0) return false;
2594
- return repo_party.classify(session_owner, target.owner) === repo_party.THIRD_PARTY;
2595
- }
2596
- function writes_third_party(command, resolve_owner = repo_party.current_owner) {
2597
- const targets = shell_segments.segments_of(command).filter((segment) => gh_api.is_gh_api(segment) && gh_api.is_write(segment));
2598
- if (targets.length === 0) return false;
2599
- const session_owner = resolve_owner();
2600
- return targets.some((segment) => is_third_party_write_segment(segment, session_owner));
2601
- }
2602
- 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.**';
2603
- var ROW14 = {
2604
- id: "third-party-write",
2605
- is_trigger: bash_triggers.on_bash_command((command) => writes_third_party(command)),
2606
- reason: THIRD_PARTY_WRITE_REASON,
2607
- decide: () => true
2608
- };
2609
- var third_party_write = {
2610
- ROW: ROW14,
2611
- THIRD_PARTY_WRITE_REASON,
2612
- is_third_party_write_segment,
2613
- writes_third_party
2614
- };
2615
-
2616
- // scripts/rules/worktree-guard.ts
2617
- var CHECKOUT_SUBCOMMAND = "checkout";
2618
- var RESTORE_SUBCOMMAND = "restore";
2619
- var STASH_SUBCOMMAND = "stash";
2620
- var PATHSPEC_SEPARATOR = "--";
2621
- function is_worktree_checkout(args) {
2622
- return args.includes(PATHSPEC_SEPARATOR);
2623
- }
2624
- var STAGED_LONG = "--staged";
2625
- var STAGED_LETTER = "S";
2626
- var WORKTREE_LONG = "--worktree";
2627
- var WORKTREE_LETTER = "W";
2628
- function touches_worktree(args) {
2629
- const is_staged = args.some(
2630
- (argument) => argument === STAGED_LONG || git_argv.short_cluster_has(argument, STAGED_LETTER)
2631
- );
2632
- const is_worktree = args.some(
2633
- (argument) => argument === WORKTREE_LONG || git_argv.short_cluster_has(argument, WORKTREE_LETTER)
2634
- );
2635
- return !is_staged || is_worktree;
2636
- }
2637
- var PUSH_STASH = "push";
2638
- var MESSAGE_LONG = "--message";
2639
- var MESSAGE_SHORT_LETTER = "m";
2640
- var READ_ONLY_STASH = /* @__PURE__ */ new Set(["list", "show"]);
2641
- function has_message(args) {
2642
- return args.some(
2643
- (argument) => argument === MESSAGE_LONG || argument.startsWith(`${MESSAGE_LONG}=`) || git_argv.short_cluster_has(argument, MESSAGE_SHORT_LETTER)
2644
- );
2645
- }
2646
- function is_unauthorized_stash(args) {
2647
- const [stash_subcommand, ...rest] = args;
2648
- if (stash_subcommand === void 0) return true;
2649
- if (READ_ONLY_STASH.has(stash_subcommand)) return false;
2650
- if (stash_subcommand === PUSH_STASH) return !has_message(rest);
2651
- return true;
2652
- }
2653
- function is_unauthorized_segment(segment) {
2654
- const call = git_argv.parse(segment);
2655
- if (call === void 0) return false;
2656
- if (call.subcommand === CHECKOUT_SUBCOMMAND) return is_worktree_checkout(call.args);
2657
- if (call.subcommand === RESTORE_SUBCOMMAND) return touches_worktree(call.args);
2658
- return call.subcommand === STASH_SUBCOMMAND && is_unauthorized_stash(call.args);
2659
- }
2660
- function is_unauthorized_worktree_change(command) {
2661
- return shell_segments.segments_of(command).some((segment) => is_unauthorized_segment(segment));
2662
- }
2663
- 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.**';
2664
- var ROW15 = {
2665
- id: "worktree-mutation",
2666
- is_trigger: bash_triggers.on_bash_command(is_unauthorized_worktree_change),
2667
- reason: WORKTREE_MUTATION_REASON,
2668
- decide: () => true
2669
- };
2670
- var worktree_guard = {
2671
- ROW: ROW15,
2672
- WORKTREE_MUTATION_REASON,
2673
- is_unauthorized_stash,
2674
- is_unauthorized_worktree_change,
2675
- is_worktree_checkout,
2676
- touches_worktree
2677
- };
2678
-
2679
- // scripts/rules/delivered-rules.ts
2680
- var SWITCH_ENV_KEY = "JOSH_RULE_GUARD";
2681
- var STAMP_PREFIX = "josh-rule-guard-";
2682
- var { is_issue_filing: is_issue_filing2, on_bash_command: on_bash_command2 } = bash_triggers;
2683
- 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.";
2684
- var ISSUE_VIEW_COMMAND = new RegExp(String.raw`^gh\s+${gh_api.GH_FLAGS}issue\s+view\s`, "u");
2685
- var ISSUE_BODY_PATH = /repos\/[^\s'"]*\/issues\/\d+(?=$|["'\s])/u;
2686
- var FETCHES_COMMENTS = /--comments\b|--json\s[\w,]*\bcomments\b|\/comments\b/u;
2687
- var ISSUES_PATH = /repos\/[^\s'"]*\/issues\//u;
2688
- var FIELD_PROJECTION = /(?:--jq|--json)[= ]\s*('[^']*'|"[^"]*"|\S+)/u;
2689
- var NAMES_THE_BODY = /\bbody\b/u;
2690
- function projects_away_body(segment) {
2691
- const projection = FIELD_PROJECTION.exec(segment);
2692
- return projection !== null && !NAMES_THE_BODY.test(projection[1] ?? "");
2693
- }
2694
- function is_body_read_segment(segment) {
2695
- if (projects_away_body(segment)) return false;
2696
- if (ISSUE_VIEW_COMMAND.test(segment)) return true;
2697
- return gh_api.is_gh_api(segment) && ISSUE_BODY_PATH.test(segment) && gh_api.is_read(segment);
2698
- }
2699
- function fetches_issue_comments(segment) {
2700
- if (!FETCHES_COMMENTS.test(segment)) return false;
2701
- return ISSUE_VIEW_COMMAND.test(segment) || gh_api.is_gh_api(segment) && ISSUES_PATH.test(segment);
2702
- }
2703
- function is_body_only_issue_read(command) {
2704
- const segments = shell_segments.segments_of(command);
2705
- if (segments.some((segment) => fetches_issue_comments(segment))) return false;
2706
- return segments.some((segment) => is_body_read_segment(segment));
2707
- }
2708
- 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.";
2709
- var { SHELL_BODY_REASON: SHELL_BODY_REASON2, carries_a_body: carries_a_body2, is_shell_evaluated_body: is_shell_evaluated_body2, keeps_body_safe: keeps_body_safe2 } = shell_body_trigger;
2710
- var ISSUE_LISTING = /^gh\s+(?:-{1,2}[\w-]+(?:[= ]\S+)?\s+)*issue\s+list\b/u;
2711
- var ISSUES_QUERY = /repos\/[^\s'"]*\/issues\?[^\s'"]*state=open/u;
2712
- var OPEN_STATE = /--state[= ]open|state=open/u;
2713
- var LABEL_FILTER = /--label\b|[?&]labels=/u;
2714
- function counts_open_issues(command) {
2715
- return shell_segments.segments_of(command).some((segment) => {
2716
- if (LABEL_FILTER.test(segment)) return false;
2717
- return ISSUE_LISTING.test(segment) && OPEN_STATE.test(segment) || ISSUES_QUERY.test(segment);
2718
- });
2719
- }
2720
- function reads_issue_comments(command) {
2721
- return shell_segments.segments_of(command).some((segment) => fetches_issue_comments(segment));
2722
- }
2723
- function reaches_the_pre_gate_boundary(call, _turn, run2) {
2724
- if (!on_bash_command2(pre_gate_cut.asks_about_the_cut)(call)) return false;
2725
- return run2.some((issued) => on_bash_command2(pre_gate_cut.claims_the_hold)(issued));
2726
- }
2727
- var DELIVERED_RULES = [
2728
- // **Listed first, ahead of the filing trigger it overlaps** (joshuafolkken/kit#2122). A `gh api`
2729
- // that files an Issue into a repository we do not own trips both this row and `wip-cap` /
2730
- // `issue-scout` / `filing-cap` — but the backlog count, the scout and the per-run cap are all about
2731
- // filing into *our own* backlog, so on a third-party target the right answer is to stop the write
2732
- // entirely rather than to count anything. Placing it first means a third-party write is refused
2733
- // before any of those speak; on a first-party filing it is silent (the owners match), so `wip-cap`
2734
- // still speaks first, exactly as before. It fires on every occurrence (`git-force.ts`).
2735
- third_party_write.ROW,
2736
- {
2737
- id: "wip-cap",
2738
- is_trigger: on_bash_command2(is_issue_filing2),
2739
- reason: WIP_CAP_REASON,
2740
- keeps: on_bash_command2(counts_open_issues)
2741
- },
2742
- // **Three rows share the filing trigger** (joshuafolkken/kit#2119), listed after `wip-cap` so the
2743
- // backlog count still speaks first: `issue-scout` refuses a filing the run has not scouted (once per
2744
- // run, stood down once the scout is on the tail), and `filing-cap` refuses every filing past the
2745
- // per-run ceiling. Each is a real instance of the admissible overlap the comment on `delivery`
2746
- // describes — the losing rule's delivery is still correct one reissue later — so a scout-less,
2747
- // over-cap filing is delivered `wip-cap`, then `issue-scout`, then `filing-cap` across its reissues.
2748
- issue_scout.ROW,
2749
- filing_cap.ROW,
2750
- // **`issue-fold` shares the same filing trigger** (joshuafolkken/kit#2213), listed after
2751
- // `filing-cap`: it refuses a *second* filing the run has not folded, once per run, stood down off
2752
- // the tail when the run has already folded or has filed nothing earlier. It is another instance of
2753
- // the admissible overlap — the losing rule's delivery is still correct one reissue later — so a
2754
- // scout-less, unfolded, over-cap second filing is delivered `wip-cap`, then `issue-scout`, then
2755
- // `filing-cap`, then `issue-fold` across its reissues.
2756
- issue_fold.ROW,
2757
- {
2758
- id: "issue-comments",
2759
- is_trigger: on_bash_command2(is_body_only_issue_read),
2760
- reason: ISSUE_COMMENTS_REASON,
2761
- already_satisfied: prior_comment_read.already_read_for_call,
2762
- keeps: on_bash_command2(reads_issue_comments)
2763
- },
2764
- {
2765
- id: "shell-body",
2766
- is_trigger: on_bash_command2(is_shell_evaluated_body2),
2767
- reason: SHELL_BODY_REASON2,
2768
- keeps: on_bash_command2(keeps_body_safe2),
2769
- reaches: on_bash_command2(carries_a_body2)
2770
- },
2771
- // **The sibling of `shell-body`, one call further along** (joshuafolkken/kit#2304). `shell-body`
2772
- // refuses a body passed inline so the shell evaluates it; this refuses a body passed by path with
2773
- // the raw field flag (`-f` / `--raw-field body=@<path>`), which posts the literal `@<path>`. It
2774
- // claims no command any row above does — `shell-body` triggers on a backtick or `$`, which the
2775
- // `@<path>` misfire carries neither, and `is_issue_filing` leaves the comment endpoint alone — so
2776
- // the exactly-one-rule invariant over its fixtures holds. It fires on every occurrence (`git-force.ts`).
2777
- raw_field_body.ROW,
2778
- {
2779
- id: "piped-verification",
2780
- is_trigger: on_bash_command2(piped_verification.is_masked_verification),
2781
- reason: piped_verification.PIPED_VERIFICATION_REASON,
2782
- keeps: on_bash_command2(piped_verification.keeps_verdict_intact),
2783
- reaches: on_bash_command2(piped_verification.runs_verification)
2784
- },
2785
- {
2786
- id: "early-heartbeat",
2787
- is_trigger: on_bash_command2(early_heartbeat.is_wait_timer),
2788
- reason: early_heartbeat.EARLY_HEARTBEAT_REASON,
2789
- decide: early_heartbeat.decide,
2790
- keeps: on_bash_command2(early_heartbeat.is_progress_watch),
2791
- reaches: on_bash_command2(early_heartbeat.waits_for_progress)
2792
- },
2793
- // **Listed before `run-tail`, the one row it deliberately overlaps** (joshuafolkken/kit#2118). A
2794
- // foreground `pnpm josh git -y` with no test beside the change is claimed by both: `test-declared`
2795
- // decides whether the commit should happen at all, so it is listed first, exactly as `wip-cap`
2796
- // precedes `shell-body` for deciding whether an Issue should exist. Nothing is lost by `run-tail`
2797
- // losing the race — the stamps are keyed per `id`, so the reissued push is delivered `run-tail`,
2798
- // which is asserted rather than assumed. The overlap only arises in a real checkout where the
2799
- // verdict is `required`; in the hermetic non-repo suite the git read fails to `exempt`, so
2800
- // `test-declared` claims nothing and the "exactly one rule" invariant over `run-tail`'s fixture holds.
2801
- test_declared_commit.ROW,
2802
- // **The one row whose trigger reads a field of the input beside the command**, so it supplies its
2803
- // own tool-name check rather than going through `on_bash_command`: a push step already issued with
2804
- // `run_in_background` is the rule obeyed, and refusing it would charge a run for doing the right
2805
- // thing. It supplies `decide` because a push is a recurring act — one per child in a batch, and a
2806
- // second inside one `fullrun` when round 2 fixes a finding in place.
2807
- {
2808
- id: "run-tail",
2809
- is_trigger: run_tail.is_foreground_push_step,
2810
- reason: run_tail.RUN_TAIL_REASON,
2811
- decide: run_tail.decide,
2812
- keeps: run_tail.is_backgrounded_push_step,
2813
- reaches: run_tail.is_push_step_call
2814
- },
2815
- // **The one row whose trigger consults the world beside the command**, because "is this a lane that
2816
- // has not cut" is not readable from the call: it is the working directory's own name and the cut
2817
- // record on disk. Both reads are synchronous, and the command match runs first, so a run that never
2818
- // types `pnpm josh gate` pays nothing for it.
2819
- //
2820
- // **Once per run rather than `decide`, and the direction of the error is why.** Taking the cut ends
2821
- // the process, so this is not the recurring act joshuafolkken/kit#1570 wrote `decide` for — and the
2822
- // five verdicts that legitimately leave a run at the gate (`not-a-lane`, `unready`, `busy`,
2823
- // `failed`, `unknown`) all need the reissue to go through. A row that refused every time would wedge exactly
2824
- // those runs; a row that refuses once cannot.
2825
- //
2826
- // **`pnpm josh rule:value` read this row with a ceiling of about 50%, and joshuafolkken/kit#1867 is
2827
- // what removed it** (the ceiling was recorded here by joshuafolkken/kit#1864's review round 2). A
2828
- // cut relaunches a *new session*, which the measurement groups as a run of its own, and that
2829
- // resumed run issues the entry check — so scored on the asking alone it joined the denominator
2830
- // while never being able to satisfy `keeps`, which only the cutting run can, and ten perfectly
2831
- // obedient children read as ten kept out of twenty. **The two halves are separable, just not from
2832
- // one call**: both processes issue byte-identical `--resume` strings, but the `fresh` verdict sends
2833
- // a run on to claim the working-tree hold while the `resume` verdict tells its counterpart to skip
2834
- // that claim. `reaches_the_pre_gate_boundary` is that conjunction, and it is why `CallTest` is
2835
- // handed the run's calls beside the turn's rather than a second predicate slot of its own.
2836
- // Dropping `reaches` altogether is still strictly worse — the trigger evaluates against the
2837
- // *measuring* process's working directory, so it is always false from the main checkout and the row
2838
- // would read as no runs at all.
2839
- //
2840
- // **It overlaps `piped-verification` on a piped gate call, and the order is safe in both
2841
- // directions.** `pnpm josh gate | tail` in an uncut lane is a real instance of both defects; the
2842
- // earlier row speaks, its own reason says to reissue, and this one delivers on the reissue — the
2843
- // reading "the losing rule's delivery is still correct one call later" that the comment below
2844
- // requires of any admissible overlap.
2845
- {
2846
- id: "pre-gate-cut",
2847
- is_trigger: on_bash_command2(pre_gate_cut.is_uncut_gate),
2848
- reason: pre_gate_cut.PRE_GATE_CUT_REASON,
2849
- keeps: on_bash_command2(pre_gate_cut.takes_the_cut),
2850
- reaches: reaches_the_pre_gate_boundary
2851
- },
2852
- // **The second row whose trigger consults the world beside the command** (joshuafolkken/kit#2034):
2853
- // "is this a dispatched lane child" is the dispatch mark against this checkout's own issue, read
2854
- // synchronously by `lane_child_marker.is_child_of`, and the command match runs first so an ordinary
2855
- // run pays nothing for it.
2856
- //
2857
- // **Once per run rather than `decide`, and no `already_satisfied`.** The stop ends the turn, so it
2858
- // is not the recurring act joshuafolkken/kit#1570 wrote `decide` for. It carries no
2859
- // `already_satisfied` because the park it asks for is a GitHub label a synchronous guard cannot
2860
- // read, and the transcript-tail alternative would read a child that merely *read*
2861
- // `backlogrun-park.md` — whose prose carries the `labels[]=needs-decision` command — as compliant
2862
- // and fall silent on a real violation. `lane-park.ts` states why one wasted reissue on the compliant
2863
- // path is the safe direction. `keeps` reads an actual label-application `Bash` command, never the
2864
- // tail, so it is not fooled the same way; `reaches` stays `is_trigger`, since a compliant run issues
2865
- // the same confirmation notify and belongs in the denominator.
2866
- {
2867
- id: "lane-park",
2868
- is_trigger: on_bash_command2(lane_park.is_unparked_stop),
2869
- reason: lane_park.LANE_PARK_REASON,
2870
- keeps: on_bash_command2(lane_park.records_the_park)
2871
- },
2872
- // **joshuafolkken/kit#2034's rule, one tool-call earlier** (joshuafolkken/kit#2201). `lane-park`
2873
- // fires on the `confirmation` notify a routed child reaches; a child that reaches for
2874
- // `AskUserQuestion` never gets there, because the harness ends its turn at the refused ask. This
2875
- // row's trigger is that ask, so the refusal becomes an instruction — park the question — instead of
2876
- // the child's death. Its trigger reads the input's tool name and consults the dispatch mark, like
2877
- // `lane-park`, and shares no command with any Bash row, so it claims no `gh` / `git` / `josh` call.
2878
- // It fires on every occurrence (`decide` returns true), the disposition `git-force.ts` takes: an
2879
- // interactive ask must never succeed in a child, and the route is always the park, never a reissue.
2880
- lane_interactive_ask.ROW,
2881
- // **The parent budget commands a lane child must never run** (joshuafolkken/kit#2267). A dispatched
2882
- // child that ran `pnpm josh run:merge` read its `busy` as a competing session and parked its Issue
2883
- // unimplemented — but the live carry record is the parent that launched it, never a competitor. This
2884
- // row refuses `run:merge` / `run:carry` in a lane child and hands back the mechanical branch: the
2885
- // record is the parent's, so continue implementing rather than park or ask. Its trigger claims a
2886
- // `josh` subcommand no other row does (the filing rows are `gh`, `run-tail` is `git push`), so it
2887
- // overlaps nothing. It fires on every occurrence (`decide` returns true): a child must never run
2888
- // these, and the route is always "continue", never a reissue (`git-force.ts`).
2889
- lane_carry_conflict.ROW,
2890
- // **The `git switch main` a lane child can never succeed at** (joshuafolkken/kit#2313), the fifth of
2891
- // the "always fails" misfires joshuafolkken/kit#2297 catalogued. A lane is a linked work tree and the
2892
- // primary checkout holds the default branch, so a child's `git switch main` fails structurally
2893
- // (`already used by worktree`); this refuses it before it fails and hands back the next command —
2894
- // skip the step, `pnpm josh main:merge` brings the default in during the gate. Its trigger reads the
2895
- // dispatch mark and the lane branch like `lane-carry-conflict`, and claims a `git switch` no row
2896
- // above matches — `git-force` is `git push` / `git branch`, `worktree-mutation` is `git checkout --`
2897
- // / `restore` / `stash` — so no two rows claim one command. It fires on every occurrence (`decide`
2898
- // returns true): a child must never run this, and the route is always "continue" (`git-force.ts`).
2899
- lane_switch_main.ROW,
2900
- // **A `pnpm josh git` with no `-y` cannot succeed from an agent** (joshuafolkken/kit#2297). It
2901
- // prompts to confirm the staging, the no-TTY prompt cancels, and the run reissues with `-y` after
2902
- // throwing away the time it took to fail. This row refuses the bare call and hands back the `-y`
2903
- // form. It is disjoint from `run-tail` by the flag — that row requires `-y`, this refuses its
2904
- // absence — so no two rows claim one command. It fires on every occurrence (`decide` returns true):
2905
- // a bare call wastes the same time each time, so the route is always the `-y` reissue
2906
- // (`git-force.ts`).
2907
- josh_git_bare.ROW,
2908
- // **The three Bash-string gaps the deny glob cannot express** (joshuafolkken/kit#2120), each reading
2909
- // the command's argv rather than a literal a glob keys on. They share no trigger with any row above —
2910
- // force/delete is `git push` / `git branch`, the worktree change is `git checkout` / `restore` /
2911
- // `stash`, and the file body is a heredoc / `node -e` / `perl -i` — so no two rows claim one command,
2912
- // which `delivered-rules-bash.test.ts` pins. Each declares `decide` returning true: a destructive or
2913
- // billed act is refused on every occurrence, never once per run (`git-force.ts`).
2914
- git_force.ROW,
2915
- worktree_guard.ROW,
2916
- file_body.ROW,
2917
- // **The second row whose trigger is an `Edit` / `Write` rather than a shell call**
2918
- // (joshuafolkken/kit#2310), and the third that consults the world beside the call: "is this a lane
2919
- // child whose recent-context cost has crossed the threshold" reads the dispatch mark, the cut record
2920
- // on disk and the synchronous `pnpm josh cost --cut` verdict — the same measurement the parent
2921
- // hand-off uses, never a second one (joshuafolkken/kit#1933). The `Edit` / `Write` name match runs
2922
- // first, then the lane read, then the transcript-priced verdict, so a run that is not a dispatched
2923
- // child in an uncut lane pays nothing for it. It self-gates on the tool name, so it claims no `Bash`
2924
- // command any row above matches — the Bash `rules_claiming` invariant is untouched.
2925
- //
2926
- // **Listed before `rule-body`, the one row it can overlap.** Both trigger on an `Edit` / `Write`; a
2927
- // rule-document edit made in a lane child that is already over threshold trips both. The cut is the
2928
- // right thing to do first — end the process and drop the accumulated context — and on the reissue in
2929
- // the fresh, under-threshold process this row is silent and `rule-body` delivers, the "losing rule's
2930
- // delivery still correct one reissue later" reading any admissible overlap needs. It carries no
2931
- // `decide`, so it is silent only **between a cut and its resume**: the implementation resume clears
2932
- // the record (joshuafolkken/kit#2310, `run-cut-cli.ts`), so the carried-record check stops silencing
2933
- // the guard, and the fresh session's own transcript reads under threshold — the row re-arms per
2934
- // resume rather than per edit, instead of a lingering record silencing it for the rest of the run.
2935
- //
2936
- // **No `reaches`, because the occasion is live-only.** `pre-gate-cut` gives a `reaches` because both
2937
- // its halves leave a command-string trace (`run:cut --resume` at entry); this row's occasion —
2938
- // crossing the threshold mid-implementation — is the working directory, the cut record and the cost
2939
- // verdict, none of which a transcript records. `pnpm josh rule:value` therefore reads it as no runs
2940
- // rather than as 0% kept (`keeps` stays, so the compliance act is named for a future measurement
2941
- // redesign), the same honest-unmeasured stance the batching and investigation rows take for the same
2942
- // reason: a trigger no transcript can reconstruct. The row itself lives in `implementation-cut.ts`.
2943
- implementation_cut.ROW,
2944
- // **The first row whose trigger is an `Edit` / `Write` rather than a shell call**
2945
- // (joshuafolkken/kit#2272). It delivers the residency questions at the edit that writes a rule into
2946
- // prose, and self-gates on the tool name and the file path, so it claims no `Bash` command any row
2947
- // above matches — the `rules_claiming` invariant over Bash fixtures is untouched. It carries no
2948
- // `decide`, so it is once per run and takes the batching stand-aside `is_first_delivery` gives every
2949
- // default row: the batching guard treats an `Edit` as a candidate, exactly as it treats an Issue
2950
- // read, so this row stands aside on a batched turn and delivers on the reissue.
2951
- rule_body_guard.ROW,
2952
- // **The generic oracle-consulted rows, one per oracle that declared a firing point**
2953
- // (joshuafolkken/kit#2324). Each refuses the action the oracle governs when the run has not run that
2954
- // oracle's command first, stood down once it has — `issue-scout`'s shape read from the registry
2955
- // rather than hand-written. Listed last so an oracle whose firing point overlaps a filing
2956
- // (`issue:lint`) delivers on the reissue, after `wip-cap` / `issue-scout` / `filing-cap` /
2957
- // `issue-fold` have each had their turn. The non-overlapping firing point (`pkg:scout` on a package
2958
- // add) claims a command no row above matches.
2959
- ...oracle_consulted.ROWS
2960
- ];
2961
- var ALONE_IN_TURN = 1;
2962
- function batches_the_turn(call, turn) {
2963
- return time_batch_guard.is_guarded_call(call) && turn.length > ALONE_IN_TURN;
2964
- }
2965
- var BATCHING_RULE = {
2966
- id: "batching",
2967
- reason: time_batch_guard.REASON,
2968
- keeps: batches_the_turn,
2969
- reaches: time_batch_guard.is_guarded_call
2970
- };
2971
- var INVESTIGATION_RULE = {
2972
- id: "investigation",
2973
- reason: investigation_reads.REASON,
2974
- reaches: investigation_reads.is_refusable_call
2975
- };
2976
- var MEASURED_RULES = [
2977
- ...DELIVERED_RULES,
2978
- BATCHING_RULE,
2979
- INVESTIGATION_RULE
2980
- ];
2981
- var BATCH_STAMP = hook_decision.create_refusal_stamp(time_batch_guard.STAMP_PREFIX);
2982
- var BATCH_REFUSAL_WINDOW_MS = 1e4;
2983
- function will_batch_guard_refuse(tail, call, run2) {
2984
- if (lane_guard_policy.mode_here("batching") !== "refuse") return false;
2985
- const refused_at_ms = BATCH_STAMP.last_ms(BATCH_STAMP.path(run2.transcript));
2986
- if (run2.now_ms - refused_at_ms < BATCH_REFUSAL_WINDOW_MS) return true;
2987
- return time_batch_guard.should_block(tail, call, refused_at_ms);
2988
- }
2989
- function is_first_delivery(tail, call, delivered_at_ms, run2) {
2990
- return delivered_at_ms === hook_decision.NEVER_MS && !will_batch_guard_refuse(tail, call, run2);
2991
- }
2992
- function delivery_decision(rule) {
2993
- const { decide: decide4, already_satisfied } = rule;
2994
- if (decide4 === void 0 && already_satisfied === void 0) return is_first_delivery;
2995
- return function should_block(tail, call, delivered_at_ms, run2) {
2996
- if (already_satisfied?.(tail, call) === true) return false;
2997
- if (decide4 === void 0) return is_first_delivery(tail, call, delivered_at_ms, run2);
2998
- return decide4(call, run2, !will_batch_guard_refuse(tail, call, run2));
2999
- };
3000
- }
3001
- function guard_of(rule) {
3002
- return hook_decision.create_transcript_guard({
3003
- prefix: `${STAMP_PREFIX}${rule.id}-`,
3004
- switch_key: SWITCH_ENV_KEY,
3005
- is_candidate: rule.is_trigger,
3006
- should_block: delivery_decision(rule),
3007
- reason: rule.reason
3008
- });
3009
- }
3010
- var GUARDS = new Map(DELIVERED_RULES.map((rule) => [rule.id, guard_of(rule)]));
3011
- function delivery_path(rule_id, transcript_path) {
3012
- return GUARDS.get(rule_id)?.refusal_path(transcript_path) ?? "";
3013
- }
3014
- function delivery(raw_payload, now_ms = Date.now()) {
3015
- for (const guard of GUARDS.values()) {
3016
- const reason = guard.refusal(raw_payload, now_ms);
3017
- if (reason !== void 0) return reason;
3018
- }
3019
- return void 0;
3020
- }
3021
- function is_enabled() {
3022
- return hook_decision.is_switch_enabled(SWITCH_ENV_KEY);
3023
- }
3024
- var delivered_rules = {
3025
- DELIVERED_RULES,
3026
- EARLY_HEARTBEAT_REASON: early_heartbeat.EARLY_HEARTBEAT_REASON,
3027
- FILE_BODY_REASON: file_body.FILE_BODY_REASON,
3028
- FILING_CAP_REASON: filing_cap.FILING_CAP_REASON,
3029
- GIT_FORCE_REASON: git_force.GIT_FORCE_REASON,
3030
- ISSUE_COMMENTS_REASON,
3031
- ISSUE_FOLD_REASON: issue_fold.ISSUE_FOLD_REASON,
3032
- ISSUE_SCOUT_REASON: issue_scout.ISSUE_SCOUT_REASON,
3033
- LANE_INTERACTIVE_ASK_REASON: lane_interactive_ask.LANE_INTERACTIVE_ASK_REASON,
3034
- LANE_PARK_REASON: lane_park.LANE_PARK_REASON,
3035
- MEASURED_RULES,
3036
- PIPED_VERIFICATION_REASON: piped_verification.PIPED_VERIFICATION_REASON,
3037
- PRE_GATE_CUT_REASON: pre_gate_cut.PRE_GATE_CUT_REASON,
3038
- RAW_FIELD_BODY_REASON: raw_field_body.RAW_FIELD_BODY_REASON,
3039
- RUN_TAIL_REASON: run_tail.RUN_TAIL_REASON,
3040
- SHELL_BODY_REASON: SHELL_BODY_REASON2,
3041
- SWITCH_ENV_KEY,
3042
- THIRD_PARTY_WRITE_REASON: third_party_write.THIRD_PARTY_WRITE_REASON,
3043
- WIP_CAP_REASON,
3044
- WORKTREE_MUTATION_REASON: worktree_guard.WORKTREE_MUTATION_REASON,
3045
- delivery,
3046
- delivery_path,
3047
- is_body_only_issue_read,
3048
- is_enabled,
3049
- is_issue_filing: is_issue_filing2
3050
- };
3051
-
3052
- // scripts/hooks/pretool-guard.ts
3053
- function combine_outcomes(batch, extra_reason) {
3054
- if (batch.reason !== void 0) return batch;
3055
- if (extra_reason !== void 0) {
3056
- return { reason: extra_reason, notice: void 0, fault: void 0 };
3057
- }
3058
- return batch;
3059
- }
3060
- function is_clear(outcome) {
3061
- return outcome.reason === void 0 && outcome.notice === void 0 && outcome.fault === void 0;
3062
- }
3063
- function with_duplicate_notice(combined, notice) {
3064
- if (notice === void 0 || !is_clear(combined)) return combined;
3065
- return { reason: void 0, notice, fault: void 0 };
3066
- }
3067
- function pretool_outcome(raw_payload) {
3068
- const batch = batch_outcome(raw_payload);
3069
- const investigation = investigation_refusal(raw_payload);
3070
- const duplicate = duplicate_read_outcome(raw_payload);
3071
- const rule = delivered_rules.delivery(raw_payload);
3072
- const duplicate_refusal = duplicate.reason;
3073
- const combined = combine_outcomes(batch, investigation ?? duplicate_refusal ?? rule);
3074
- return with_duplicate_notice(combined, duplicate.notice);
3075
- }
3076
- var pretool_guard = { combine_outcomes, pretool_outcome };
3077
- if (process.argv[1] === fileURLToPath2(import.meta.url)) {
3078
- if (process.stdin.isTTY) hook_decision.report_no_payload("pretool:guard");
3079
- else hook_decision.write_outcome(await text(process.stdin), pretool_outcome);
3080
- }
3081
-
3082
- export {
3083
- pretool_outcome,
3084
- pretool_guard
3085
- };