@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.
- package/.github/workflows/publish.yml +39 -2
- package/README.md +3 -3
- package/package.json +6 -2
- package/scripts/init/distributed-paths.ts +14 -11
- package/scripts/init/hook-command-bootstrap.ts +51 -0
- package/scripts/init/hook-command-rewrite.ts +9 -1
- package/scripts/init/init-logic.ts +2 -8
- package/templates/workflows/ci.yml +6 -2
- package/dist/hooks/chunk-25JL6FMS.js +0 -4970
- package/dist/hooks/chunk-2ANGT3XM.js +0 -375
- package/dist/hooks/chunk-2EI5GBSW.js +0 -42
- package/dist/hooks/chunk-2OT5XE2N.js +0 -5014
- package/dist/hooks/chunk-33ELOKEV.js +0 -910
- package/dist/hooks/chunk-33YRTKM7.js +0 -230
- package/dist/hooks/chunk-36JQU5NN.js +0 -77
- package/dist/hooks/chunk-3EJBTWVS.js +0 -1769
- package/dist/hooks/chunk-3FK5SFAC.js +0 -432
- package/dist/hooks/chunk-3PWPYDD2.js +0 -240
- package/dist/hooks/chunk-3TQU7TDH.js +0 -375
- package/dist/hooks/chunk-3TVLSFZ6.js +0 -432
- package/dist/hooks/chunk-3VXYCB4E.js +0 -77
- package/dist/hooks/chunk-44M6JXGV.js +0 -3398
- package/dist/hooks/chunk-46EEEHC3.js +0 -240
- package/dist/hooks/chunk-4A7UVP2P.js +0 -364
- package/dist/hooks/chunk-4LFQEZS4.js +0 -137
- package/dist/hooks/chunk-4OCX3V2T.js +0 -469
- package/dist/hooks/chunk-4OHA5DE7.js +0 -49
- package/dist/hooks/chunk-4PMDW3YD.js +0 -68
- package/dist/hooks/chunk-4QHBNAI4.js +0 -68
- package/dist/hooks/chunk-4SSB3FEE.js +0 -3273
- package/dist/hooks/chunk-5A2WQ2CQ.js +0 -5390
- package/dist/hooks/chunk-5EVA2JQB.js +0 -5390
- package/dist/hooks/chunk-5P7VF5VE.js +0 -3230
- package/dist/hooks/chunk-5QWJGGZP.js +0 -49
- package/dist/hooks/chunk-5U5EZYG6.js +0 -77
- package/dist/hooks/chunk-5VUUF3ZV.js +0 -240
- package/dist/hooks/chunk-5WA7UBT3.js +0 -374
- package/dist/hooks/chunk-6EBGRQ3Q.js +0 -301
- package/dist/hooks/chunk-6TVHWMD3.js +0 -432
- package/dist/hooks/chunk-6VAGLEMB.js +0 -68
- package/dist/hooks/chunk-75BTXKFJ.js +0 -3424
- package/dist/hooks/chunk-75RHN67S.js +0 -137
- package/dist/hooks/chunk-76KC3BDD.js +0 -3414
- package/dist/hooks/chunk-76SLS7CZ.js +0 -898
- package/dist/hooks/chunk-7DHZ4DWT.js +0 -5015
- package/dist/hooks/chunk-7INSJDGE.js +0 -458
- package/dist/hooks/chunk-7VJ4FTDX.js +0 -268
- package/dist/hooks/chunk-7XCYNHQK.js +0 -77
- package/dist/hooks/chunk-A5YJ3H7E.js +0 -375
- package/dist/hooks/chunk-AAEXXUUL.js +0 -268
- package/dist/hooks/chunk-ABVEVV2M.js +0 -432
- package/dist/hooks/chunk-ACN3YXFL.js +0 -68
- package/dist/hooks/chunk-AD3XE3QZ.js +0 -397
- package/dist/hooks/chunk-AENFZPZH.js +0 -230
- package/dist/hooks/chunk-AFKRFQ6W.js +0 -68
- package/dist/hooks/chunk-AHD5IPWI.js +0 -375
- package/dist/hooks/chunk-AMK5JGV7.js +0 -77
- package/dist/hooks/chunk-ARN7NCQI.js +0 -425
- package/dist/hooks/chunk-AUFY653N.js +0 -3425
- package/dist/hooks/chunk-AXQBNFEF.js +0 -375
- package/dist/hooks/chunk-AYC7R6U4.js +0 -3316
- package/dist/hooks/chunk-AYEA2FJ7.js +0 -5015
- package/dist/hooks/chunk-B4HPNY2I.js +0 -375
- package/dist/hooks/chunk-B57S3VWT.js +0 -922
- package/dist/hooks/chunk-BINZ3PLI.js +0 -364
- package/dist/hooks/chunk-BODUSXKK.js +0 -410
- package/dist/hooks/chunk-BQBAT2YK.js +0 -375
- package/dist/hooks/chunk-BUJEBW75.js +0 -391
- package/dist/hooks/chunk-BVNWORKA.js +0 -72
- package/dist/hooks/chunk-BWI47LQS.js +0 -5205
- package/dist/hooks/chunk-C25HRMJX.js +0 -137
- package/dist/hooks/chunk-CBFZNC74.js +0 -375
- package/dist/hooks/chunk-CECYXS4Y.js +0 -3424
- package/dist/hooks/chunk-CELGUP5F.js +0 -49
- package/dist/hooks/chunk-CMHANU3F.js +0 -3442
- package/dist/hooks/chunk-CMN372LI.js +0 -68
- package/dist/hooks/chunk-CPGZLMMK.js +0 -3299
- package/dist/hooks/chunk-CTQKXDC7.js +0 -268
- package/dist/hooks/chunk-CYAT4RIT.js +0 -68
- package/dist/hooks/chunk-CYTWYCH2.js +0 -5014
- package/dist/hooks/chunk-CZSOLRBA.js +0 -77
- package/dist/hooks/chunk-D2PIDYJR.js +0 -3438
- package/dist/hooks/chunk-DE57HI6T.js +0 -137
- package/dist/hooks/chunk-DGXDGAPG.js +0 -5306
- package/dist/hooks/chunk-DNYRSYQG.js +0 -5014
- package/dist/hooks/chunk-DTAXH2A2.js +0 -240
- package/dist/hooks/chunk-E2IZUWI5.js +0 -375
- package/dist/hooks/chunk-E4L2K2BL.js +0 -397
- package/dist/hooks/chunk-ECXVCCVD.js +0 -3079
- package/dist/hooks/chunk-EPHO443D.js +0 -68
- package/dist/hooks/chunk-EUFK2BI2.js +0 -375
- package/dist/hooks/chunk-EYCD5ZAV.js +0 -137
- package/dist/hooks/chunk-F2SDYJU4.js +0 -240
- package/dist/hooks/chunk-F4B5D5TF.js +0 -68
- package/dist/hooks/chunk-FGEO3NDX.js +0 -432
- package/dist/hooks/chunk-FJYI52RE.js +0 -137
- package/dist/hooks/chunk-FYTZHIY4.js +0 -3085
- package/dist/hooks/chunk-G7JL2A5K.js +0 -375
- package/dist/hooks/chunk-GA3ITDBW.js +0 -42
- package/dist/hooks/chunk-GDR4H4SV.js +0 -469
- package/dist/hooks/chunk-GFKDXPSM.js +0 -2855
- package/dist/hooks/chunk-GIMRTKFU.js +0 -566
- package/dist/hooks/chunk-GOPHRLGO.js +0 -240
- package/dist/hooks/chunk-GX4HO7SJ.js +0 -230
- package/dist/hooks/chunk-GZ6FUVIK.js +0 -566
- package/dist/hooks/chunk-H5HPY5M5.js +0 -2869
- package/dist/hooks/chunk-HCF3OGEE.js +0 -1829
- package/dist/hooks/chunk-HDEXAOUM.js +0 -469
- package/dist/hooks/chunk-HM2TY3TN.js +0 -469
- package/dist/hooks/chunk-HOA7NXA2.js +0 -55
- package/dist/hooks/chunk-HOFEWH6H.js +0 -240
- package/dist/hooks/chunk-HUUI6S5J.js +0 -2669
- package/dist/hooks/chunk-I4D3NVUD.js +0 -469
- package/dist/hooks/chunk-I4Q6WAJE.js +0 -68
- package/dist/hooks/chunk-IB6N4CR2.js +0 -432
- package/dist/hooks/chunk-IFLDYJMD.js +0 -469
- package/dist/hooks/chunk-IGU466YD.js +0 -77
- package/dist/hooks/chunk-IJCQUHKY.js +0 -375
- package/dist/hooks/chunk-IKUMBDJB.js +0 -230
- package/dist/hooks/chunk-IM3BDIBJ.js +0 -5005
- package/dist/hooks/chunk-IMYMAODK.js +0 -3431
- package/dist/hooks/chunk-IN2ZVGXA.js +0 -5005
- package/dist/hooks/chunk-ITVJ2MKQ.js +0 -18
- package/dist/hooks/chunk-J5RIJYTM.js +0 -2669
- package/dist/hooks/chunk-JFLMH4K5.js +0 -3460
- package/dist/hooks/chunk-JR25IFML.js +0 -77
- package/dist/hooks/chunk-JT5AUXFI.js +0 -432
- package/dist/hooks/chunk-JY5YVN6N.js +0 -3234
- package/dist/hooks/chunk-K5Q4ETVI.js +0 -391
- package/dist/hooks/chunk-K67V2F24.js +0 -137
- package/dist/hooks/chunk-KATZESEC.js +0 -3273
- package/dist/hooks/chunk-KFH4H7W7.js +0 -469
- package/dist/hooks/chunk-KMUEI6RX.js +0 -910
- package/dist/hooks/chunk-KMV4OY77.js +0 -3438
- package/dist/hooks/chunk-KQAZNWEY.js +0 -137
- package/dist/hooks/chunk-KZZR6JER.js +0 -5015
- package/dist/hooks/chunk-LAR6DREP.js +0 -77
- package/dist/hooks/chunk-LNCJWEZD.js +0 -5015
- package/dist/hooks/chunk-LRYD2GLM.js +0 -6
- package/dist/hooks/chunk-LXBKYHDV.js +0 -77
- package/dist/hooks/chunk-LYLKIUNE.js +0 -5024
- package/dist/hooks/chunk-LZKCFOVC.js +0 -432
- package/dist/hooks/chunk-M4FCIYRI.js +0 -77
- package/dist/hooks/chunk-M5M7327Q.js +0 -3425
- package/dist/hooks/chunk-MF72MHSX.js +0 -3455
- package/dist/hooks/chunk-MK56LKDO.js +0 -397
- package/dist/hooks/chunk-N3MJADY7.js +0 -240
- package/dist/hooks/chunk-NFQSUDBN.js +0 -458
- package/dist/hooks/chunk-NHREKY45.js +0 -137
- package/dist/hooks/chunk-NP5TTAAW.js +0 -375
- package/dist/hooks/chunk-NQTLTKTJ.js +0 -2993
- package/dist/hooks/chunk-NVRCZPBZ.js +0 -391
- package/dist/hooks/chunk-O46HMMLJ.js +0 -469
- package/dist/hooks/chunk-OFJGH2L4.js +0 -240
- package/dist/hooks/chunk-OLFMCZH5.js +0 -240
- package/dist/hooks/chunk-OMCZS46I.js +0 -49
- package/dist/hooks/chunk-OQZEZW5G.js +0 -469
- package/dist/hooks/chunk-P2MDAT2Y.js +0 -1897
- package/dist/hooks/chunk-P3M2JJIF.js +0 -68
- package/dist/hooks/chunk-PDVHQITQ.js +0 -3453
- package/dist/hooks/chunk-PNT2AMDH.js +0 -391
- package/dist/hooks/chunk-PR2LVSY6.js +0 -3445
- package/dist/hooks/chunk-PRR5R4JK.js +0 -137
- package/dist/hooks/chunk-PXT44QVN.js +0 -566
- package/dist/hooks/chunk-PZLWJLAP.js +0 -469
- package/dist/hooks/chunk-Q2FEAZJ2.js +0 -77
- package/dist/hooks/chunk-Q32WETDO.js +0 -3265
- package/dist/hooks/chunk-Q45DUBLE.js +0 -3600
- package/dist/hooks/chunk-Q7NZSWQ5.js +0 -240
- package/dist/hooks/chunk-QH2JNAMK.js +0 -3085
- package/dist/hooks/chunk-QPU5H2OS.js +0 -68
- package/dist/hooks/chunk-QRAHZ5TO.js +0 -1829
- package/dist/hooks/chunk-QUZ5HPIE.js +0 -469
- package/dist/hooks/chunk-QVCRG34O.js +0 -2893
- package/dist/hooks/chunk-R3MY4E3S.js +0 -1675
- package/dist/hooks/chunk-R6TXJJXP.js +0 -77
- package/dist/hooks/chunk-REJU6VT7.js +0 -1829
- package/dist/hooks/chunk-REQAQLRC.js +0 -2993
- package/dist/hooks/chunk-RFCXPAHH.js +0 -391
- package/dist/hooks/chunk-RHI6Y22X.js +0 -137
- package/dist/hooks/chunk-RKZVOBBS.js +0 -469
- package/dist/hooks/chunk-RL57TT2Y.js +0 -566
- package/dist/hooks/chunk-RORDOD4S.js +0 -3321
- package/dist/hooks/chunk-RZKYW5P2.js +0 -77
- package/dist/hooks/chunk-SJPJ6VSI.js +0 -391
- package/dist/hooks/chunk-SKSIJQQQ.js +0 -432
- package/dist/hooks/chunk-SSQDZKXG.js +0 -268
- package/dist/hooks/chunk-SYHFKE4C.js +0 -77
- package/dist/hooks/chunk-TCGNI2ZY.js +0 -42
- package/dist/hooks/chunk-TG3WXJF5.js +0 -137
- package/dist/hooks/chunk-TH7IJAY5.js +0 -77
- package/dist/hooks/chunk-TLZ27PCY.js +0 -240
- package/dist/hooks/chunk-U652FXTN.js +0 -566
- package/dist/hooks/chunk-UEK47YUF.js +0 -268
- package/dist/hooks/chunk-UIO45H3O.js +0 -432
- package/dist/hooks/chunk-UNLZEEVH.js +0 -5005
- package/dist/hooks/chunk-UNRVF3SZ.js +0 -432
- package/dist/hooks/chunk-USLFAFOI.js +0 -3453
- package/dist/hooks/chunk-VAEELMLL.js +0 -442
- package/dist/hooks/chunk-VALSWLDQ.js +0 -3453
- package/dist/hooks/chunk-VC5JEPNY.js +0 -3398
- package/dist/hooks/chunk-VEZVK6C7.js +0 -230
- package/dist/hooks/chunk-VK5JKFAR.js +0 -49
- package/dist/hooks/chunk-VNF2KP7Z.js +0 -469
- package/dist/hooks/chunk-VWWCUMW6.js +0 -2669
- package/dist/hooks/chunk-W4JARUQV.js +0 -137
- package/dist/hooks/chunk-WEOBMNJG.js +0 -2993
- package/dist/hooks/chunk-WKJU3J5Y.js +0 -5390
- package/dist/hooks/chunk-WMN4TSVF.js +0 -3299
- package/dist/hooks/chunk-WNON54PT.js +0 -3469
- package/dist/hooks/chunk-WOVKBPPX.js +0 -137
- package/dist/hooks/chunk-WRVFORYJ.js +0 -432
- package/dist/hooks/chunk-WUBVYGQZ.js +0 -240
- package/dist/hooks/chunk-WWULTUOM.js +0 -68
- package/dist/hooks/chunk-WXHKKB45.js +0 -4962
- package/dist/hooks/chunk-XAI3YTHJ.js +0 -469
- package/dist/hooks/chunk-XFZH6JZJ.js +0 -77
- package/dist/hooks/chunk-XGPWX6IA.js +0 -137
- package/dist/hooks/chunk-XIXXIVGY.js +0 -566
- package/dist/hooks/chunk-XKDOYJGA.js +0 -922
- package/dist/hooks/chunk-YALTVBHM.js +0 -432
- package/dist/hooks/chunk-YEBEPBFK.js +0 -5014
- package/dist/hooks/chunk-YI2BQ3DK.js +0 -3441
- package/dist/hooks/chunk-YVNBCFGG.js +0 -1829
- package/dist/hooks/chunk-YXBN3RGG.js +0 -432
- package/dist/hooks/chunk-Z2OOJSW3.js +0 -3163
- package/dist/hooks/chunk-ZA3HVBOY.js +0 -2644
- package/dist/hooks/chunk-ZKID46U3.js +0 -137
- package/dist/hooks/chunk-ZPMU2JV6.js +0 -49
- package/dist/hooks/chunk-ZRKY4W7O.js +0 -432
- package/dist/hooks/chunk-ZTMRB7LJ.js +0 -268
- 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-B57S3VWT.js";
|
|
9
|
-
import {
|
|
10
|
-
batch_outcome
|
|
11
|
-
} from "./chunk-Q2FEAZJ2.js";
|
|
12
|
-
import {
|
|
13
|
-
investigation_refusal
|
|
14
|
-
} from "./chunk-2EI5GBSW.js";
|
|
15
|
-
import {
|
|
16
|
-
duplicate_read_outcome
|
|
17
|
-
} from "./chunk-DE57HI6T.js";
|
|
18
|
-
import {
|
|
19
|
-
investigation_reads
|
|
20
|
-
} from "./chunk-4A7UVP2P.js";
|
|
21
|
-
import {
|
|
22
|
-
lane_guard_policy,
|
|
23
|
-
time_batch_guard
|
|
24
|
-
} from "./chunk-NFQSUDBN.js";
|
|
25
|
-
import {
|
|
26
|
-
hook_decision,
|
|
27
|
-
lane_child_marker
|
|
28
|
-
} from "./chunk-33YRTKM7.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-AYC7R6U4.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
|
-
};
|