@joshuafolkken/kit 1.1136.0 → 1.1166.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/.claude/settings.json +4 -4
  2. package/.claude/skills/epic-commands/SKILL.md +27 -239
  3. package/.claude/skills/epic-commands/epic-bundle.md +115 -0
  4. package/.claude/skills/epic-commands/execution-waves.md +126 -0
  5. package/.claude/skills/workflow-commands/SKILL.md +48 -194
  6. package/.claude/skills/workflow-commands/background-commands.md +1 -1
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +12 -123
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +11 -60
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +10 -8
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -173
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +64 -144
  13. package/.claude/skills/workflow-commands/backlogrun.md +5 -4
  14. package/.claude/skills/workflow-commands/chain-rule.md +3 -2
  15. package/.claude/skills/workflow-commands/delegation.md +2 -2
  16. package/.claude/skills/workflow-commands/entry-sequence.md +62 -0
  17. package/.claude/skills/workflow-commands/followup.md +104 -181
  18. package/.claude/skills/workflow-commands/fullrun-steps.md +8 -9
  19. package/.claude/skills/workflow-commands/fullrun.md +16 -73
  20. package/.claude/skills/workflow-commands/halfrun.md +10 -36
  21. package/.claude/skills/workflow-commands/into-target.md +4 -4
  22. package/.claude/skills/workflow-commands/issue-comments.md +7 -8
  23. package/.claude/skills/workflow-commands/issue-scout.md +6 -6
  24. package/.claude/skills/workflow-commands/kickoff.md +6 -6
  25. package/.claude/skills/workflow-commands/needs-human-review.md +6 -6
  26. package/.claude/skills/workflow-commands/observation-filing.md +21 -172
  27. package/.claude/skills/workflow-commands/observation-ledger.md +150 -0
  28. package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -93
  29. package/.claude/skills/workflow-commands/prerequisite.md +6 -6
  30. package/.claude/skills/workflow-commands/progress-watcher.md +90 -0
  31. package/.claude/skills/workflow-commands/prrun.md +5 -5
  32. package/.claude/skills/workflow-commands/retrospective.md +9 -9
  33. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  34. package/.claude/skills/workflow-commands/target-repository.md +2 -2
  35. package/.claude/skills/workflow-commands/working-tree-hold.md +7 -5
  36. package/.codex/hooks.json +4 -4
  37. package/AGENTS.md +1 -6
  38. package/CLAUDE.md +58 -120
  39. package/GEMINI.md +1 -6
  40. package/dist/CLAUDE.md +58 -120
  41. package/dist/hooks/batch-guard.js +4 -4
  42. package/dist/hooks/{chunk-NFZGFGOR.js → chunk-5KTCMCOT.js} +14 -1
  43. package/dist/hooks/{chunk-LJBLGHEH.js → chunk-A6P7PN7P.js} +4 -6
  44. package/dist/hooks/{chunk-FO2I7UT7.js → chunk-ANJ2WGFD.js} +1 -1
  45. package/dist/hooks/{chunk-GDU26K3J.js → chunk-DYAVAM6S.js} +563 -126
  46. package/dist/hooks/{chunk-RFWWUI5F.js → chunk-ENA7P5FB.js} +3 -3
  47. package/dist/hooks/{chunk-KBXZLMRH.js → chunk-GU5MATVE.js} +213 -65
  48. package/dist/hooks/chunk-GYNGP7HI.js +56 -0
  49. package/dist/hooks/{chunk-HBKZLITF.js → chunk-URJA6XY4.js} +4 -4
  50. package/dist/hooks/{chunk-KOKOAV6D.js → chunk-V2BTOZHT.js} +6 -4
  51. package/dist/hooks/{chunk-JKKTVDAG.js → chunk-ZDZ555YH.js} +100 -20
  52. package/dist/hooks/codex-hook-adapter.js +10 -10
  53. package/dist/hooks/duplicate-read-guard.js +5 -5
  54. package/dist/hooks/format-edited.js +2 -2
  55. package/dist/hooks/inputs.json +193 -192
  56. package/dist/hooks/investigation-guard.js +5 -5
  57. package/dist/hooks/pretool-guard.js +11 -10
  58. package/dist/hooks/stop-guard.js +5 -5
  59. package/dist/josh.js +25 -2
  60. package/dist/version/index.js +5 -1
  61. package/package.json +2 -1
  62. package/prompts/collaboration-workflow/file-edits.md +11 -10
  63. package/prompts/collaboration-workflow/gh-rest.md +2 -4
  64. package/prompts/collaboration-workflow/glossary.md +1 -1
  65. package/prompts/collaboration-workflow/issue-citation.md +5 -9
  66. package/prompts/collaboration-workflow/issue-template.md +40 -115
  67. package/prompts/collaboration-workflow/operating-rules.md +4 -5
  68. package/prompts/collaboration-workflow/output-bounds.md +12 -78
  69. package/prompts/collaboration-workflow/overview.md +7 -13
  70. package/prompts/collaboration-workflow/plan-comment.md +11 -39
  71. package/prompts/collaboration-workflow/principles.md +3 -4
  72. package/prompts/collaboration-workflow/report-format.md +28 -74
  73. package/prompts/collaboration-workflow/residency.md +12 -109
  74. package/prompts/collaboration-workflow/rule-delivery.md +98 -97
  75. package/prompts/collaboration-workflow/shell-body.md +3 -13
  76. package/prompts/collaboration-workflow/turn-batching.md +14 -88
  77. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -14
  78. package/prompts/collaboration-workflow/wip-cap.md +6 -45
  79. package/prompts/collaboration-workflow.md +1 -13
  80. package/prompts/review-rubric.md +8 -71
  81. package/prompts/review.md +25 -55
  82. package/prompts/testing-guide.md +18 -67
  83. package/scripts/agent/agent-role-profile.ts +1 -1
  84. package/scripts/agent/codex-hooks.ts +3 -12
  85. package/scripts/backlog/backlog-budget.ts +1 -1
  86. package/scripts/backlog/backlog-drive-cli.ts +4 -3
  87. package/scripts/backlog/backlog-drive-offer-argv.ts +10 -1
  88. package/scripts/backlog/backlog-offer.ts +6 -6
  89. package/scripts/behavior/behavior-rules.ts +11 -4
  90. package/scripts/delegation/investigation-reads.ts +5 -5
  91. package/scripts/discovery/repo-discovery.ts +21 -7
  92. package/scripts/discovery/repo-origin.ts +26 -0
  93. package/scripts/discovery/repo-party.ts +42 -1
  94. package/scripts/document/backlogrun-parent-read-set.ts +14 -15
  95. package/scripts/document/document-byte-budget.ts +34 -20
  96. package/scripts/document/entry-read-budget.ts +21 -9
  97. package/scripts/document/lane-child-read-set.ts +12 -14
  98. package/scripts/document/read-set-cli.ts +4 -4
  99. package/scripts/document/read-set-point-of-use.ts +10 -2
  100. package/scripts/document/resident-budget.ts +47 -0
  101. package/scripts/epic/epic-shape.ts +10 -13
  102. package/scripts/epic/epic-solo.ts +1 -1
  103. package/scripts/exports/unused-members-cli.ts +7 -3
  104. package/scripts/gh/git-gh-api-path.ts +7 -0
  105. package/scripts/hooks/pretool-guard-cli.ts +2 -2
  106. package/scripts/hooks/pretool-guard.ts +45 -2
  107. package/scripts/hooks/run-hook.sh +41 -0
  108. package/scripts/init/hook-command-rewrite.ts +18 -35
  109. package/scripts/init/hook-launch.ts +21 -32
  110. package/scripts/issue/issue-auto-ok.ts +95 -0
  111. package/scripts/issue/issue-file-cli.ts +53 -7
  112. package/scripts/issue/issue-file.ts +28 -5
  113. package/scripts/issue/issue-labels.ts +12 -12
  114. package/scripts/issue/issue-rank.ts +1 -1
  115. package/scripts/issue/issue-read-cli.ts +3 -3
  116. package/scripts/issue/issue-read.ts +2 -2
  117. package/scripts/issue/issue-scout-cli.ts +2 -2
  118. package/scripts/issue/issue-state-cli.ts +1 -1
  119. package/scripts/issue/issue-state.ts +2 -2
  120. package/scripts/issue/issue-wip.ts +70 -0
  121. package/scripts/josh/hook-decision.ts +17 -0
  122. package/scripts/josh/josh-commands-development.ts +10 -0
  123. package/scripts/josh/josh-commands-issue.ts +2 -2
  124. package/scripts/lines/bytes-command.ts +29 -6
  125. package/scripts/observations/observation-ledger-line.ts +1 -1
  126. package/scripts/release/release-progress.ts +167 -0
  127. package/scripts/release/release-publish.ts +19 -8
  128. package/scripts/release/release-tag.ts +22 -8
  129. package/scripts/rules/decision-oracle-stage.ts +1 -1
  130. package/scripts/rules/decision-oracle.ts +6 -6
  131. package/scripts/rules/delivered-rules.ts +11 -11
  132. package/scripts/rules/direct-pr-create.ts +110 -0
  133. package/scripts/rules/file-body.ts +98 -42
  134. package/scripts/rules/filing-cap.ts +2 -2
  135. package/scripts/rules/filing-offer.ts +1 -1
  136. package/scripts/rules/gh-api.ts +18 -1
  137. package/scripts/rules/implementation-cut.ts +3 -1
  138. package/scripts/rules/lane-background.ts +67 -10
  139. package/scripts/rules/permission-guards.ts +10 -2
  140. package/scripts/rules/stop-rules.ts +4 -4
  141. package/scripts/rules/test-declared-commit.ts +1 -1
  142. package/scripts/rules/third-party-write.ts +56 -12
  143. package/scripts/run/run-cut-handoff.ts +21 -1
  144. package/scripts/run/run-entry-cli.ts +11 -3
  145. package/scripts/run/run-hold-cli.ts +16 -2
  146. package/scripts/run/run-label.ts +50 -0
  147. package/scripts/run/run-merge-steps.ts +8 -3
  148. package/scripts/run/run-merge.ts +1 -1
  149. package/scripts/run/run-prep-cli.ts +6 -2
  150. package/scripts/run/run-prep.ts +17 -0
  151. package/scripts/run/run-ship-preflight.ts +34 -3
  152. package/scripts/run/run-ship-review-steps.ts +1 -1
  153. package/scripts/run/run-ship-review.ts +26 -5
  154. package/scripts/run/run-step-cli.ts +20 -7
  155. package/scripts/run/run-wake-driver.ts +24 -1
  156. package/scripts/run/run-wake-session.ts +2 -1
  157. package/scripts/test/test-declared-logic.ts +1 -1
  158. package/scripts/time-runtime/time-batch-guard.ts +52 -16
  159. package/scripts/time-runtime/time-density-hook.ts +20 -2
  160. package/scripts/time-runtime/time-density.ts +4 -3
  161. package/scripts/time-runtime/time-markers.ts +15 -4
  162. package/scripts/version/npm-registry.ts +11 -1
  163. package/templates/workflows/ci.yml +9 -1
  164. package/.claude/skills/workflow-commands/followup-reference.md +0 -118
  165. package/dist/hooks/chunk-HANSKHES.js +0 -464
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  investigation_reads
3
- } from "./chunk-LJBLGHEH.js";
3
+ } from "./chunk-A6P7PN7P.js";
4
4
  import {
5
5
  lane_guard_policy
6
- } from "./chunk-HANSKHES.js";
6
+ } from "./chunk-GYNGP7HI.js";
7
7
  import {
8
8
  hook_decision
9
- } from "./chunk-NFZGFGOR.js";
9
+ } from "./chunk-5KTCMCOT.js";
10
10
 
11
11
  // scripts/delegation/investigation-guard.ts
12
12
  import { text } from "node:stream/consumers";
@@ -30,36 +30,36 @@ import {
30
30
  repo_party,
31
31
  run_carry,
32
32
  run_cut,
33
+ run_cut_handoff,
33
34
  run_headless,
34
35
  run_hold,
35
36
  run_tail,
36
37
  shell_segments,
37
38
  tail_commands
38
- } from "./chunk-JKKTVDAG.js";
39
+ } from "./chunk-ZDZ555YH.js";
39
40
  import {
40
41
  find_local_bin_upwards
41
42
  } from "./chunk-3V37IID6.js";
42
43
  import {
43
44
  batch_outcome
44
- } from "./chunk-KOKOAV6D.js";
45
+ } from "./chunk-V2BTOZHT.js";
45
46
  import {
46
47
  investigation_refusal
47
- } from "./chunk-RFWWUI5F.js";
48
+ } from "./chunk-ENA7P5FB.js";
48
49
  import {
49
50
  duplicate_read_outcome
50
- } from "./chunk-HBKZLITF.js";
51
+ } from "./chunk-URJA6XY4.js";
51
52
  import {
52
53
  investigation_reads
53
- } from "./chunk-LJBLGHEH.js";
54
+ } from "./chunk-A6P7PN7P.js";
54
55
  import {
55
- lane_guard_policy,
56
- time_batch_guard
57
- } from "./chunk-HANSKHES.js";
56
+ lane_guard_policy
57
+ } from "./chunk-GYNGP7HI.js";
58
58
  import {
59
59
  hook_decision,
60
60
  lane_child_marker,
61
61
  run_issue_number
62
- } from "./chunk-NFZGFGOR.js";
62
+ } from "./chunk-5KTCMCOT.js";
63
63
  import {
64
64
  PLATFORM_TEMP_ROOT,
65
65
  cost_attribute,
@@ -70,8 +70,9 @@ import {
70
70
  json_value,
71
71
  lane_paths,
72
72
  stamp_file,
73
+ time_batch_guard,
73
74
  time_shell
74
- } from "./chunk-GDU26K3J.js";
75
+ } from "./chunk-DYAVAM6S.js";
75
76
 
76
77
  // scripts/rules/direct-filing.ts
77
78
  var DIRECT_FILING_REASON = '\u26D4 direct filing: an Issue is filed with `pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>]`, never with `gh api \u2026/issues` or `gh issue create`. The command runs every filing step in order \u2014 the duplicate scout, the body lint and its classification labels, the depth and `route:` labels, the `## Origin` check when filing into another repository, and `epic:bundle` after the create \u2014 so none of them is a step to remember. When the scout prints candidates it holds the filing: read each, then reissue with `--distinct <N,\u2026>` for the ones that are separate deliverables. Reference: `docs/josh-commands-automation.md` \u2192 "`josh issue:file`". **This rule fires on every occurrence, not once per run.**';
@@ -466,15 +467,28 @@ var early_heartbeat = {
466
467
 
467
468
  // scripts/rules/file-body.ts
468
469
  import { existsSync } from "node:fs";
469
- var HEREDOC = /<<-?\s*['"]?\w+/u;
470
+ var HEREDOC = /(?<!<)<<(?!<)-?\s*['"]?([A-Z_a-z]\w*)/u;
470
471
  var LINE_BREAK = "\n";
471
- function first_line(command) {
472
- const [line] = command.split(LINE_BREAK);
473
- return line ?? "";
474
- }
475
472
  function has_heredoc(command) {
476
473
  return HEREDOC.test(command);
477
474
  }
475
+ function with_body_line(lines, line) {
476
+ const opener = lines.at(-1);
477
+ if (opener === void 0) return [...lines];
478
+ return [...lines.slice(0, -1), { ...opener, body: `${opener.body}${line}${LINE_BREAK}` }];
479
+ }
480
+ function scan_line(scan, line) {
481
+ if (scan.delimiter === void 0) {
482
+ return { lines: [...scan.lines, { text: line, body: "" }], delimiter: HEREDOC.exec(line)?.[1] };
483
+ }
484
+ if (line.trim() === scan.delimiter) return { ...scan, delimiter: void 0 };
485
+ return { ...scan, lines: with_body_line(scan.lines, line) };
486
+ }
487
+ function command_lines(command) {
488
+ let scan = { lines: [], delimiter: void 0 };
489
+ for (const line of command.split(LINE_BREAK)) scan = scan_line(scan, line);
490
+ return scan.lines;
491
+ }
478
492
  var FILE_REDIRECT = /(?:^|\s)>>?\s*(?!&)("[^"]+"|'[^']+'|\S+)/u;
479
493
  var TEE_TARGET = /\btee\s+(?:-a\s+)?(?!-)("[^"]+"|'[^']+'|\S+)/u;
480
494
  var QUOTES = /^['"]|['"]$/gu;
@@ -482,22 +496,32 @@ function redirect_target(line) {
482
496
  const target = FILE_REDIRECT.exec(line)?.[1] ?? TEE_TARGET.exec(line)?.[1];
483
497
  return target === void 0 ? void 0 : target.replaceAll(QUOTES, "");
484
498
  }
485
- function writes_existing_file(command, has_file) {
486
- if (!has_heredoc(command)) return false;
487
- const target = redirect_target(first_line(command));
488
- return target !== void 0 && has_file(target);
489
- }
490
- var BODY_INTERPRETERS = /* @__PURE__ */ new Set(["python", "python3", "ruby", "perl", "php"]);
499
+ var BODY_INTERPRETERS = /* @__PURE__ */ new Set([
500
+ "python",
501
+ "python3",
502
+ "ruby",
503
+ "perl",
504
+ "php",
505
+ "node"
506
+ ]);
491
507
  var NODE = "node";
492
508
  var NODE_EVAL = /(?:^|\s)(?:-e|--eval)\b/u;
493
- var WRITE_CALL = /writeFile|appendFile|createWriteStream|\.write(?:lines)?\s*\(/u;
509
+ var WRITE_CALL = /writeFile|appendFile|createWriteStream|\.write(?:lines|_text|_bytes)?\s*\(/u;
494
510
  var WRITE_MODE = /open\s*\([^)]*['"][rbt+]*[wax]|os\.replace|shutil\.(?:copy|move)/u;
495
- function body_writes(command) {
496
- return WRITE_CALL.test(command) || WRITE_MODE.test(command);
511
+ function body_writes(body) {
512
+ return WRITE_CALL.test(body) || WRITE_MODE.test(body);
497
513
  }
498
- function is_interpreter_write(segment, command) {
514
+ var SCRIPT_FILE = /\.(?:py|rb|pl|php|js|mjs|cjs)$/u;
515
+ function is_heredoc_write(line, has_file) {
516
+ const target = has_heredoc(line.text) ? redirect_target(line.text) : void 0;
517
+ if (target === void 0) return false;
518
+ if (has_file(target)) return true;
519
+ return SCRIPT_FILE.test(target) && body_writes(line.body);
520
+ }
521
+ function is_interpreter_write(segment, body) {
499
522
  const leading = time_shell.leading_word(segment);
500
- if (BODY_INTERPRETERS.has(leading)) return has_heredoc(command) && body_writes(command);
523
+ if (!BODY_INTERPRETERS.has(leading)) return false;
524
+ if (has_heredoc(segment)) return body_writes(body);
501
525
  return leading === NODE && NODE_EVAL.test(segment) && body_writes(segment);
502
526
  }
503
527
  var PERL = "perl";
@@ -507,13 +531,15 @@ function is_in_place_perl(segment) {
507
531
  if (time_shell.leading_word(segment) !== PERL) return false;
508
532
  return segment.split(WHITESPACE).some((word) => PERL_IN_PLACE.test(word));
509
533
  }
510
- function has_body_interpreter(command) {
511
- return shell_segments.segments_of(first_line(command)).some((segment) => is_interpreter_write(segment, command) || is_in_place_perl(segment));
534
+ function has_body_interpreter(line) {
535
+ return shell_segments.segments_of(line.text).some((segment) => is_interpreter_write(segment, line.body) || is_in_place_perl(segment));
512
536
  }
513
537
  function carries_a_file_body(command, has_file = existsSync) {
514
- return writes_existing_file(command, has_file) || has_body_interpreter(command);
538
+ return command_lines(command).some(
539
+ (line) => is_heredoc_write(line, has_file) || has_body_interpreter(line)
540
+ );
515
541
  }
516
- 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.**";
542
+ var FILE_BODY_REASON = "\u26D4 file body in a shell command: this carries a file's new text inline \u2014 a heredoc rewriting an existing file, an interpreter (`python3 - <<`, `node -e`) whose body writes, a script file (`cat > edit.py <<EOF`) whose body writes, or an in-place `perl -0pi -e`. `CLAUDE.md` forbids it because the command body is re-read on every later request of the run, so an editing script written early is billed for the rest of it (`file-edits.md` measured one run at ~2m45s spent retyping existing lines). Edit a region with the Edit tool (old / new); a change scattered so widely that the partial edits would carry more than the file is rewritten whole with the Write tool, never a heredoc. Pass a body by path (`--field body=@<path>`, `--body-file <path>`). A new-file creation (`cat > <new-file> <<EOF`), a read-only heredoc (`cat <<EOF`), and a short `sed -i` stay allowed. **This rule fires on every occurrence, not once per run.**";
517
543
  var ROW2 = {
518
544
  id: "file-body",
519
545
  is_trigger: bash_triggers.on_bash_command((command) => carries_a_file_body(command)),
@@ -535,6 +561,7 @@ var READ_METHOD = "GET";
535
561
  var REPO_PATH = /repos\/([^/\s'"?]+)\/([^/\s'"?]+)/u;
536
562
  var OWNER_GROUP = 1;
537
563
  var REPO_GROUP = 2;
564
+ var OWNER_PLACEHOLDERS = /* @__PURE__ */ new Set(["{owner}", ":owner"]);
538
565
  function is_gh_api(segment) {
539
566
  return GH_API_COMMAND.test(segment);
540
567
  }
@@ -554,7 +581,18 @@ function repo_target(segment) {
554
581
  if (match === null) return void 0;
555
582
  return { owner: match[OWNER_GROUP] ?? "", repo: match[REPO_GROUP] ?? "" };
556
583
  }
557
- var gh_api = { GH_FLAGS, is_gh_api, is_read, is_write, method_of, repo_target };
584
+ function is_owner_placeholder(owner) {
585
+ return OWNER_PLACEHOLDERS.has(owner);
586
+ }
587
+ var gh_api = {
588
+ GH_FLAGS,
589
+ is_gh_api,
590
+ is_owner_placeholder,
591
+ is_read,
592
+ is_write,
593
+ method_of,
594
+ repo_target
595
+ };
558
596
 
559
597
  // scripts/rules/git-argv.ts
560
598
  var ENV_ASSIGNMENT = /^[A-Za-z_]\w*=/u;
@@ -1144,7 +1182,7 @@ function is_over_threshold_edit(call, state = current_state()) {
1144
1182
  return warrants_the_cut(state);
1145
1183
  }
1146
1184
  var THRESHOLD_TEXT = `${cost_format.format_tokens(CONTEXT_CUT_THRESHOLD)}-token`;
1147
- var IMPLEMENTATION_CUT_REASON = `\u26D4 implementation-phase cut: this checkout is a lane dispatched for this issue, or a run holds it for this issue (joshuafolkken/kit#2760), and its recent-context cost has crossed the shared ${THRESHOLD_TEXT} threshold mid-implementation, so the thinking accumulated so far is now re-read on every later request. Take the cut before this edit. First write a handoff file with the Write tool \u2014 the user\u2019s instruction verbatim, what you have completed, what remains, and what you deliberately did not touch \u2014 and pass it as \`--handoff <path>\`, so the fresh process resumes on the original instruction rather than the working tree alone (joshuafolkken/kit#2354); a resume that finds no instruction is refused \`incomplete\` rather than continuing blind. \`pnpm josh run:cut --impl <N> --handoff <path>\` ends this process and relaunches a fresh one that resumes back into implementation (\`resume-impl\`), dropping the accumulated context rather than carrying it \u2014 joshuafolkken/kit#1933 built the boundary and joshuafolkken/kit#2310 measured it firing 0 times because the verdict was only ever read at session entry, where the context has not yet grown. The verdict is \`pnpm josh cost --cut\`'s exactly \u2014 the same per-request billed-input statistic against the same threshold, never a second measurement. Issue \`pnpm josh run:cut --impl <N> --handoff <path>\` now and read the verdict: on \`cut\`, **end the turn immediately** \u2014 in a lane the fresh process owns the run and continues implementing, so it must not be waited for; outside a lane nothing is relaunched, so first send the \`confirmation\` Telegram naming the resume command \`fullrun #<N>\` and keep the hold; on \`not-a-lane\`, \`unready\`, \`busy\`, \`failed\` or \`unknown\`, this process carries the run on and the edit is simply the next call. Never relaunch a second process after \`busy\`. This fired because the dispatch mark names this lane or the run hold names this issue; a person working here carries no mark and holds no run, so sees no refusal, so there is no human-or-child judgement left to make. The procedure is \`.claude/skills/workflow-commands/pre-gate-cut.md\`. Reissue this edit once the cut has answered \u2014 an edit reissued right after this refusal passes, so \`busy\` / \`failed\` cannot wedge the run edit after edit; this fires again on the next threshold crossing rather than once per run.`;
1185
+ var IMPLEMENTATION_CUT_REASON = `\u26D4 implementation-phase cut: this checkout is a lane dispatched for this issue, or a run holds it for this issue (joshuafolkken/kit#2760), and its recent-context cost has crossed the shared ${THRESHOLD_TEXT} threshold mid-implementation, so the thinking accumulated so far is now re-read on every later request. Take the cut before this edit. First write a handoff file with the Write tool \u2014 the user\u2019s instruction verbatim, what you have completed, what remains, and what you deliberately did not touch, as ${run_cut_handoff.HANDOFF_FORMAT} (Markdown is refused) \u2014 and pass it as \`--handoff <path>\`, so the fresh process resumes on the original instruction rather than the working tree alone (joshuafolkken/kit#2354); a resume that finds no instruction is refused \`incomplete\` rather than continuing blind. \`pnpm josh run:cut --impl <N> --handoff <path>\` ends this process and relaunches a fresh one that resumes back into implementation (\`resume-impl\`), dropping the accumulated context rather than carrying it \u2014 joshuafolkken/kit#1933 built the boundary and joshuafolkken/kit#2310 measured it firing 0 times because the verdict was only ever read at session entry, where the context has not yet grown. The verdict is \`pnpm josh cost --cut\`'s exactly \u2014 the same per-request billed-input statistic against the same threshold, never a second measurement. Issue \`pnpm josh run:cut --impl <N> --handoff <path>\` now and read the verdict: on \`cut\`, **end the turn immediately** \u2014 in a lane the fresh process owns the run and continues implementing, so it must not be waited for; outside a lane nothing is relaunched, so first send the \`confirmation\` Telegram naming the resume command \`fullrun #<N>\` and keep the hold; on \`not-a-lane\`, \`unready\`, \`busy\`, \`failed\` or \`unknown\`, this process carries the run on and the edit is simply the next call. Never relaunch a second process after \`busy\`. This fired because the dispatch mark names this lane or the run hold names this issue; a person working here carries no mark and holds no run, so sees no refusal, so there is no human-or-child judgement left to make. The procedure is \`.claude/skills/workflow-commands/pre-gate-cut.md\`. Reissue this edit once the cut has answered \u2014 an edit reissued right after this refusal passes, so \`busy\` / \`failed\` cannot wedge the run edit after edit; this fires again on the next threshold crossing rather than once per run.`;
1148
1186
  var REISSUE_WINDOW_MS = 9e4;
1149
1187
  function is_reissued_refusal(now_ms, delivered_at_ms) {
1150
1188
  return now_ms - delivered_at_ms < REISSUE_WINDOW_MS;
@@ -1436,13 +1474,13 @@ var RUN_ENTRY_ORACLE = {
1436
1474
  decision: "Where a run on an issue starts, and whether the typed command has already been reached",
1437
1475
  args: "<N> [--to kickoff|halfrun|prrun|fullrun]",
1438
1476
  vocabulary: run_stage.START_TOKENS,
1439
- single_source: ".claude/skills/workflow-commands/SKILL.md \u2192 \xA72k"
1477
+ single_source: "docs/how-to/run-issues.md"
1440
1478
  };
1441
1479
 
1442
1480
  // scripts/rules/decision-oracle.ts
1443
1481
  var CHAIN_RULE_MD = ".claude/skills/workflow-commands/chain-rule.md";
1444
1482
  var BACKLOGRUN_PROGRESS_MD = ".claude/skills/workflow-commands/backlogrun-progress.md";
1445
- var SKILL_2E = ".claude/skills/workflow-commands/SKILL.md \u2192 \xA72e";
1483
+ var SKILL_2E = ".claude/skills/workflow-commands/issue-scout.md";
1446
1484
  var SPLIT_ASSESSMENT_QUESTION = ".claude/skills/workflow-commands/split-assessment.md \u2192 The question";
1447
1485
  var PRE_GATE_CUT_MD = ".claude/skills/workflow-commands/pre-gate-cut.md";
1448
1486
  var BACKLOGRUN_MD = ".claude/skills/workflow-commands/backlogrun.md";
@@ -1475,7 +1513,7 @@ var DECISION_ORACLES = [
1475
1513
  decision: "Whether a run step may be delegated to a cheaper execution tier",
1476
1514
  args: "<step>",
1477
1515
  vocabulary: ["delegate", "keep"],
1478
- single_source: ".claude/skills/workflow-commands/SKILL.md \u2192 \xA72b"
1516
+ single_source: ".claude/skills/workflow-commands/delegation.md"
1479
1517
  },
1480
1518
  {
1481
1519
  name: "review:level",
@@ -1518,7 +1556,7 @@ var DECISION_ORACLES = [
1518
1556
  decision: "Whether this working tree is free for a run to claim",
1519
1557
  args: `[${ISSUE_N_ARG}]`,
1520
1558
  vocabulary: ["hold", BUSY, UNKNOWN],
1521
- single_source: ".claude/skills/workflow-commands/SKILL.md \u2192 \xA72f"
1559
+ single_source: ".claude/skills/workflow-commands/working-tree-hold.md"
1522
1560
  },
1523
1561
  {
1524
1562
  name: "cost:cut",
@@ -1533,7 +1571,7 @@ var DECISION_ORACLES = [
1533
1571
  decision: "Whether a release is owed after this merge",
1534
1572
  args: "",
1535
1573
  vocabulary: [REQUIRED, SKIP, UNKNOWN],
1536
- single_source: ".claude/skills/workflow-commands/followup-reference.md"
1574
+ single_source: ".claude/skills/workflow-commands/followup.md"
1537
1575
  },
1538
1576
  {
1539
1577
  name: "epic:bundle",
@@ -1582,7 +1620,7 @@ var DECISION_ORACLES = [
1582
1620
  decision: "An issue's state, labels, and human-review flag",
1583
1621
  args: ISSUE_N_ARG,
1584
1622
  vocabulary: ["human_review: yes", "human_review: no"],
1585
- single_source: ".claude/skills/workflow-commands/SKILL.md \u2192 \xA72z"
1623
+ single_source: ".claude/skills/workflow-commands/needs-human-review.md"
1586
1624
  },
1587
1625
  {
1588
1626
  name: "run:cut:resume",
@@ -1619,7 +1657,7 @@ var DECISION_ORACLES = [
1619
1657
  decision: "Whether a named stash entry exists and can be applied",
1620
1658
  args: "<message>",
1621
1659
  vocabulary: ["popped", "conflicted", "no-match", "ambiguous"],
1622
- single_source: ".claude/skills/workflow-commands/SKILL.md \u2192 \xA72d"
1660
+ single_source: ".claude/skills/workflow-commands/prerequisite.md"
1623
1661
  },
1624
1662
  {
1625
1663
  name: "backlog:next",
@@ -1889,6 +1927,67 @@ var ROW10 = {
1889
1927
  };
1890
1928
  var destructive_command = { DESTRUCTIVE_COMMAND_REASON, ROW: ROW10, is_destructive_command };
1891
1929
 
1930
+ // scripts/rules/direct-pr-create.ts
1931
+ var GH_COMMAND2 = "gh";
1932
+ var PR_GROUP2 = "pr";
1933
+ var PR_CREATE_ACTIONS = /* @__PURE__ */ new Set(["create", "new"]);
1934
+ var FLAG_PREFIX3 = "-";
1935
+ var VALUE_TAKING_FLAGS = /* @__PURE__ */ new Set(["-R", "--repo"]);
1936
+ var API_VALUE_TAKING_FLAGS = /* @__PURE__ */ new Set([
1937
+ ...VALUE_TAKING_FLAGS,
1938
+ "-X",
1939
+ "--method",
1940
+ "-f",
1941
+ "--raw-field",
1942
+ "-F",
1943
+ "--field",
1944
+ "-H",
1945
+ "--header",
1946
+ "--input",
1947
+ "-q",
1948
+ "--jq",
1949
+ "-t",
1950
+ "--template",
1951
+ "--hostname",
1952
+ "-p",
1953
+ "--preview",
1954
+ "--cache"
1955
+ ]);
1956
+ var API_SUBCOMMAND = "api";
1957
+ var PULLS_PATH = /^['"]?(?:https:\/\/api\.github\.com)?\/?repos\/[^/\s'"]+\/[^/\s'"]+\/pulls\/?['"]?$/u;
1958
+ function positional_words(args, value_taking_flags) {
1959
+ return args.filter((argument, index) => {
1960
+ if (argument.startsWith(FLAG_PREFIX3)) return false;
1961
+ return !value_taking_flags.has(args[index - 1] ?? "");
1962
+ });
1963
+ }
1964
+ function is_pr_create_api_call(args) {
1965
+ const call = [GH_COMMAND2, ...args].join(" ");
1966
+ if (!gh_api.is_gh_api(call) || !gh_api.is_write(call)) return false;
1967
+ const [subcommand, endpoint = ""] = positional_words(args, API_VALUE_TAKING_FLAGS);
1968
+ return subcommand === API_SUBCOMMAND && PULLS_PATH.test(endpoint);
1969
+ }
1970
+ function is_pr_create_cli_call(args) {
1971
+ const [group, action = ""] = positional_words(args, VALUE_TAKING_FLAGS);
1972
+ return group === PR_GROUP2 && PR_CREATE_ACTIONS.has(action);
1973
+ }
1974
+ function is_pr_create_segment(segment) {
1975
+ const args = git_argv.arguments_of(segment, GH_COMMAND2);
1976
+ if (args === void 0) return false;
1977
+ return is_pr_create_cli_call(args) || is_pr_create_api_call(args);
1978
+ }
1979
+ function is_direct_pr_create(command) {
1980
+ return shell_segments.segments_of(command).some((segment) => is_pr_create_segment(segment));
1981
+ }
1982
+ var DIRECT_PR_CREATE_REASON = "\u26D4 direct PR creation: `gh pr create` (or a `gh api` POST to `repos/<o>/<r>/pulls`) bypasses `build_body`, which writes the `closes #N` line \u2014 the Issue would stay open after the merge (`CLAUDE.md` \u2192 Git Rules). Open the PR with `pnpm josh pr` instead; after a failed push, fix, push, then run `pnpm josh pr`. **This rule fires on every occurrence, not once per run.**";
1983
+ var ROW11 = {
1984
+ id: "direct-pr-create",
1985
+ is_trigger: bash_triggers.on_bash_command(is_direct_pr_create),
1986
+ reason: DIRECT_PR_CREATE_REASON,
1987
+ decide: () => true
1988
+ };
1989
+ var direct_pr_create = { DIRECT_PR_CREATE_REASON, ROW: ROW11, is_direct_pr_create };
1990
+
1892
1991
  // scripts/rules/worktree-guard.ts
1893
1992
  var CHECKOUT_SUBCOMMAND = "checkout";
1894
1993
  var RESTORE_SUBCOMMAND = "restore";
@@ -1949,14 +2048,14 @@ function is_unauthorized_worktree_change(command) {
1949
2048
  return shell_segments.segments_of(command).some((segment) => is_unauthorized_segment(segment));
1950
2049
  }
1951
2050
  var WORKTREE_MUTATION_REASON = '\u26D4 unauthorized working-tree change: `git checkout -- <path>`, `git restore <path>`, a forced `git clean` and an unauthorized `git stash` discard work or mutate the repository-wide stash every work tree shares, and none is on the `.claude/settings.json` deny list (`operating-rules.md`). Do not run them on your own judgement. A commit goes through `pnpm josh git`; a stash pop goes through `pnpm josh stash:pop "<message>"`, never a positional `git stash pop` a shared stack lets another lane divert. A stash push is authorized only inside a documented flow and only with a findable message \u2014 `git stash push -u -m "<message>"` \u2014 which this row allows; a bare `git stash`, a message-less push, or `pop` / `apply` / `drop` is refused. **This rule fires on every occurrence, not once per run.**';
1952
- var ROW11 = {
2051
+ var ROW12 = {
1953
2052
  id: "worktree-mutation",
1954
2053
  is_trigger: bash_triggers.on_bash_command(is_unauthorized_worktree_change),
1955
2054
  reason: WORKTREE_MUTATION_REASON,
1956
2055
  decide: () => true
1957
2056
  };
1958
2057
  var worktree_guard = {
1959
- ROW: ROW11,
2058
+ ROW: ROW12,
1960
2059
  WORKTREE_MUTATION_REASON,
1961
2060
  is_unauthorized_worktree_change,
1962
2061
  touches_worktree
@@ -1985,13 +2084,13 @@ function is_index_mutation(command) {
1985
2084
  return shell_segments.segments_of(command).some((segment) => is_index_segment(segment));
1986
2085
  }
1987
2086
  var INDEX_MUTATION_REASON = '\u26D4 git index mutation: `git add` / `stage` / `commit` / `reset` / `rm` / `mv` and `git restore --staged` overwrite the user\'s staged snapshot, which `CLAUDE.md` \u2192 Git Rules forbids on your own judgement (`prompts/collaboration-workflow/operating-rules.md` \u2192 "git index \u3092\u52DD\u624B\u306B\u5909\u66F4\u3057\u306A\u3044"). The `.claude/settings.json` deny list matches prefix globs, so it misses `git -C . commit`, `git -c k=v commit` and `env git commit`; this row reads the argv instead, so the spelling does not matter. A commit goes through `pnpm josh git -y`; any other staging needs explicit current-turn user instruction, run by the user in their own terminal. **This rule fires on every occurrence, not once per run.**';
1988
- var ROW12 = {
2087
+ var ROW13 = {
1989
2088
  id: "index-mutation",
1990
2089
  is_trigger: bash_triggers.on_bash_command(is_index_mutation),
1991
2090
  reason: INDEX_MUTATION_REASON,
1992
2091
  decide: () => true
1993
2092
  };
1994
- var index_guard = { INDEX_MUTATION_REASON, ROW: ROW12, is_index_mutation };
2093
+ var index_guard = { INDEX_MUTATION_REASON, ROW: ROW13, is_index_mutation };
1995
2094
 
1996
2095
  // scripts/rules/protected-files.ts
1997
2096
  import path5 from "node:path";
@@ -2140,16 +2239,21 @@ function is_protected_file_call(call, is_kit_consumer2 = doctor_consumer.is_kit_
2140
2239
  return is_environment_read(call) || is_consumer_settings_edit(call, is_kit_consumer2);
2141
2240
  }
2142
2241
  var PROTECTED_FILE_REASON = '\u26D4 protected file: `.env` holds the Telegram bot token, so reading it puts a secret in the transcript \u2014 the scripts that need it load it in their own process. `.claude/settings.json` in a consumer repository is the deny list and the hooks every other rule rests on, so a run must not rewrite it; `josh sync` owns it, and a change belongs upstream in kit (`CLAUDE.md` \u2192 "Route distributed-doc / config changes upstream to kit"). Leave both files alone, or ask the user. **This rule fires on every occurrence, not once per run.**';
2143
- var ROW13 = {
2242
+ var ROW14 = {
2144
2243
  id: "protected-file",
2145
2244
  is_trigger: is_protected_file_call,
2146
2245
  reason: PROTECTED_FILE_REASON,
2147
2246
  decide: () => true
2148
2247
  };
2149
- var protected_files = { ROW: ROW13, is_protected_file_call };
2248
+ var protected_files = { ROW: ROW14, is_protected_file_call };
2150
2249
 
2151
2250
  // scripts/rules/permission-guards.ts
2152
- var ROWS2 = [index_guard.ROW, destructive_command.ROW, protected_files.ROW];
2251
+ var ROWS2 = [
2252
+ index_guard.ROW,
2253
+ destructive_command.ROW,
2254
+ protected_files.ROW,
2255
+ direct_pr_create.ROW
2256
+ ];
2153
2257
  var permission_guards = { ROWS: ROWS2 };
2154
2258
 
2155
2259
  // scripts/rules/piped-verification.ts
@@ -2220,7 +2324,7 @@ function is_poll_loop(command) {
2220
2324
  return LOOP_KEYWORD2.test(plain) && SLEEP_COMMAND.test(plain);
2221
2325
  }
2222
2326
  var POLL_LOOP_REASON = '\u26D4 output poll loop: this hand-writes a `while`/`until` wait loop over a command\'s output, a marker file or a process \u2014 with a `sleep` or, worse, without one \u2014 and the loop does not know when the command finished. A regex that never matches waits to the harness block limit; one that matches a mid-run line exits too early \u2014 either way the command exit is not what ends the wait, and each pass spends a full-context round trip (joshuafolkken/kit#2371 measured about 45 minutes, 13% of six lanes, lost to exactly this). Do not poll the output file. A command known to take minutes \u2014 `pnpm josh gate`, `pnpm josh followup`, `pnpm josh format` \u2014 is started detached from the first with `run_in_background` set, and the harness re-invokes you when it exits: its completion notification is what ends the wait, on the task exit rather than a regex or the block limit. So issue the long command in the background and end the turn \u2014 do not arm a loop; the wake-up brings you back. To glance at interim output, `Read` the output file once, never in a sleep loop. If you are waiting on CI, `pnpm josh followup` waits on it for you. The procedure is `.claude/skills/workflow-commands/background-commands.md`, "A command that can take minutes is issued in the background". **This rule fires on every occurrence, not once per run.**';
2223
- var ROW14 = {
2327
+ var ROW15 = {
2224
2328
  id: "poll-loop",
2225
2329
  is_trigger: bash_triggers.on_bash_command(is_poll_loop),
2226
2330
  reason: POLL_LOOP_REASON,
@@ -2228,7 +2332,7 @@ var ROW14 = {
2228
2332
  };
2229
2333
  var poll_loop = {
2230
2334
  POLL_LOOP_REASON,
2231
- ROW: ROW14,
2335
+ ROW: ROW15,
2232
2336
  is_poll_loop
2233
2337
  };
2234
2338
 
@@ -2348,13 +2452,13 @@ function posts_at_path_literally(command) {
2348
2452
  return RAW_FIELD_BODY_AT.test(command);
2349
2453
  }
2350
2454
  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.";
2351
- var ROW15 = {
2455
+ var ROW16 = {
2352
2456
  id: "raw-field-body",
2353
2457
  is_trigger: bash_triggers.on_bash_command(posts_at_path_literally),
2354
2458
  reason: RAW_FIELD_BODY_REASON,
2355
2459
  decide: () => true
2356
2460
  };
2357
- var raw_field_body = { RAW_FIELD_BODY_REASON, ROW: ROW15, posts_at_path_literally };
2461
+ var raw_field_body = { RAW_FIELD_BODY_REASON, ROW: ROW16, posts_at_path_literally };
2358
2462
 
2359
2463
  // scripts/rules/rule-body-guard.ts
2360
2464
  var EDIT_TOOL = "Edit";
@@ -2425,7 +2529,7 @@ function runs_placement_oracle(command) {
2425
2529
  (names) => shell_segments.segments_of(command).some((segment) => shell_segments.is_josh_command(segment, names))
2426
2530
  );
2427
2531
  }
2428
- var ROW16 = {
2532
+ var ROW17 = {
2429
2533
  id: "rule-body",
2430
2534
  is_trigger: writes_rule_prose,
2431
2535
  reason: RULE_BODY_REASON,
@@ -2434,7 +2538,7 @@ var ROW16 = {
2434
2538
  };
2435
2539
  var rule_body_guard = {
2436
2540
  RULE_BODY_REASON,
2437
- ROW: ROW16,
2541
+ ROW: ROW17,
2438
2542
  answered_placement,
2439
2543
  is_rule_document,
2440
2544
  writes_rule_prose
@@ -2869,31 +2973,53 @@ function is_untested_commit(call) {
2869
2973
  return test_declared_changed.current_verdict() === "required";
2870
2974
  }
2871
2975
  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.';
2872
- var ROW17 = { id: "test-declared", is_trigger: is_untested_commit, reason: REASON };
2873
- var test_declared_commit = { REASON, ROW: ROW17 };
2976
+ var ROW18 = { id: "test-declared", is_trigger: is_untested_commit, reason: REASON };
2977
+ var test_declared_commit = { REASON, ROW: ROW18 };
2874
2978
 
2875
2979
  // scripts/rules/third-party-write.ts
2876
- function is_third_party_write_segment(segment, session_owner) {
2980
+ function is_third_party_write_segment(segment, session_owner, resolve_placeholder_owners) {
2877
2981
  if (!gh_api.is_gh_api(segment) || !gh_api.is_write(segment)) return false;
2878
2982
  const target = gh_api.repo_target(segment);
2879
2983
  if (target === void 0) return false;
2880
- return repo_party.classify(session_owner, target.owner) === repo_party.THIRD_PARTY;
2984
+ if (!gh_api.is_owner_placeholder(target.owner)) {
2985
+ return repo_party.classify(session_owner, target.owner) === repo_party.THIRD_PARTY;
2986
+ }
2987
+ return resolve_placeholder_owners().some(
2988
+ (owner) => owner === void 0 || repo_party.classify(session_owner, owner) === repo_party.THIRD_PARTY
2989
+ );
2990
+ }
2991
+ var GH_CONTEXT_CHANGE = /(?:^|\s)(?:cd|pushd)(?:\s|$)|(?:^|\s)(?:export\s+)?GH_REPO=/u;
2992
+ function unreadable_candidates() {
2993
+ return [void 0];
2881
2994
  }
2882
- function writes_third_party(command, resolve_owner = repo_party.current_owner) {
2883
- const targets = shell_segments.segments_of(command).filter((segment) => gh_api.is_gh_api(segment) && gh_api.is_write(segment));
2884
- if (targets.length === 0) return false;
2995
+ function candidates_at(segments, index, resolve_placeholder_owners) {
2996
+ const has_moved = segments.slice(0, index).some((segment) => GH_CONTEXT_CHANGE.test(segment));
2997
+ return has_moved ? unreadable_candidates : resolve_placeholder_owners;
2998
+ }
2999
+ function writes_third_party(command, resolve_owner = repo_party.current_owner, resolve_placeholder_owners = repo_party.placeholder_owners) {
3000
+ const segments = shell_segments.segments_of(command);
3001
+ const has_write = segments.some(
3002
+ (segment) => gh_api.is_gh_api(segment) && gh_api.is_write(segment)
3003
+ );
3004
+ if (!has_write) return false;
2885
3005
  const session_owner = resolve_owner();
2886
- return targets.some((segment) => is_third_party_write_segment(segment, session_owner));
3006
+ return segments.some(
3007
+ (segment, index) => is_third_party_write_segment(
3008
+ segment,
3009
+ session_owner,
3010
+ candidates_at(segments, index, resolve_placeholder_owners)
3011
+ )
3012
+ );
2887
3013
  }
2888
3014
  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.**';
2889
- var ROW18 = {
3015
+ var ROW19 = {
2890
3016
  id: "third-party-write",
2891
3017
  is_trigger: bash_triggers.on_bash_command((command) => writes_third_party(command)),
2892
3018
  reason: THIRD_PARTY_WRITE_REASON,
2893
3019
  decide: () => true
2894
3020
  };
2895
3021
  var third_party_write = {
2896
- ROW: ROW18,
3022
+ ROW: ROW19,
2897
3023
  THIRD_PARTY_WRITE_REASON,
2898
3024
  writes_third_party
2899
3025
  };
@@ -2903,7 +3029,7 @@ var SWITCH_ENV_KEY = "JOSH_RULE_GUARD";
2903
3029
  var STAMP_PREFIX = "josh-rule-guard-";
2904
3030
  var { is_issue_filing, on_bash_command } = bash_triggers;
2905
3031
  var WIP_CAP = 30;
2906
- var WIP_CAP_REASON = `\u26D4 backlog WIP cap: count the target repository's open Issues before filing. With more than ${String(WIP_CAP)} 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.`;
3032
+ var WIP_CAP_REASON = `\u26D4 backlog WIP cap: \`pnpm josh issue:file\` counts the target repository's open Issues before filing. With more than ${String(WIP_CAP)} open, close one first; nothing honestly closable means do not file \u2014 the command holds such a filing. Two filings are exempt and proceed while stating the overage \u2014 one the run is blocked by (\`--over-cap\`, or the \`tier-a\` / \`split\` route), and an interrupt (\`--route 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. Both procedures are in \`prompts/collaboration-workflow/wip-cap.md\`. Reissue this call and let the command count \u2014 it fires once per run and cannot repeat on the call in hand.`;
2907
3033
  var ISSUE_VIEW_COMMAND = new RegExp(String.raw`^gh\s+${gh_api.GH_FLAGS}issue\s+view\s`, "u");
2908
3034
  var ISSUE_BODY_PATH = /repos\/[^\s'"]*\/issues\/\d+(?=$|["'\s])/u;
2909
3035
  var FETCHES_COMMENTS = /--comments\b|--json\s[\w,]*\bcomments\b|\/comments\b/u;
@@ -2928,7 +3054,7 @@ function is_body_only_issue_read(command) {
2928
3054
  if (segments.some((segment) => fetches_issue_comments(segment))) return false;
2929
3055
  return segments.some((segment) => is_body_read_segment(segment));
2930
3056
  }
2931
- 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\". Every body-only read is refused until the comments are read.";
3057
+ 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/issue-comments.md`. Every body-only read is refused until the comments are read.";
2932
3058
  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;
2933
3059
  var ISSUE_LISTING = /^gh\s+(?:-{1,2}[\w-]+(?:[= ]\S+)?\s+)*issue\s+list\b/u;
2934
3060
  var ISSUES_QUERY = /repos\/[^\s'"]*\/issues\?[^\s'"]*state=open/u;
@@ -4297,7 +4423,29 @@ async function pretool_outcome_async(raw_payload) {
4297
4423
  if (reason !== void 0) return { reason, notice: void 0, fault: void 0 };
4298
4424
  return with_step_zero_notice(base, raw_payload);
4299
4425
  }
4300
- var pretool_guard = { combine_outcomes, pretool_outcome };
4426
+ function guarded_call(raw_payload) {
4427
+ try {
4428
+ const payload = hook_decision.parse_hook_payload(raw_payload);
4429
+ return payload === void 0 ? void 0 : { name: payload.tool_name, input: payload.tool_input };
4430
+ } catch {
4431
+ return void 0;
4432
+ }
4433
+ }
4434
+ function foreground_rewrite(outcome, raw_payload, is_lane_child) {
4435
+ if (outcome.reason !== void 0) return void 0;
4436
+ const call = guarded_call(raw_payload);
4437
+ const input = call === void 0 ? void 0 : lane_background.foreground_input(call, is_lane_child);
4438
+ if (input === void 0) return void 0;
4439
+ const context = [lane_background.FOREGROUND_NOTE, outcome.notice ?? outcome.fault];
4440
+ return hook_decision.rewrite_envelope(input, context.filter(Boolean).join("\n\n"));
4441
+ }
4442
+ function emit(raw_payload, outcome) {
4443
+ const rewrite = foreground_rewrite(outcome, raw_payload);
4444
+ if (rewrite === void 0) hook_decision.emit_outcome(outcome);
4445
+ else process.stdout.write(`${rewrite}
4446
+ `);
4447
+ }
4448
+ var pretool_guard = { combine_outcomes, emit, foreground_rewrite, pretool_outcome };
4301
4449
 
4302
4450
  export {
4303
4451
  pretool_outcome_async,
@@ -0,0 +1,56 @@
1
+ import {
2
+ lane_child_marker
3
+ } from "./chunk-5KTCMCOT.js";
4
+
5
+ // scripts/lane/lane-guard-policy.ts
6
+ var DEFAULT_MODE = "refuse";
7
+ var OFF_MODE = "off";
8
+ var LANE_GUARD_POLICY = [
9
+ {
10
+ id: "investigation",
11
+ mode_in_lane_child: "notice",
12
+ because: 'a refusal ends a child turn so it cannot fire as one (kit#2138), which is why it was `off` from #2138 to #2382. The exemption reason \u2014 "the child is itself the delegated unit, so it cannot dispatch a sub-unit to read its own edit targets" \u2014 did not hold in measurement: kit#2382 re-measured the six lane children after #2347 (#2366 #2370 #2371 #2372 #2374 #2375) and every one dispatched sub-agents (10 launches, 2 of them investigation fan-outs) and read 4\u201317 files it never edited, all six over the threshold of 3. So the reading the guard exists to send out was there to send. It takes `notice`, not `off`: the child is nudged to delegate rather than killed, on the `additionalContext` path batching and duplicate-read already use, and the notice names the concrete unedited files this run read (the #2276 precedent) rather than the generic "delegate" #2164 \u2192 #2178 measured not to move the number. The density is a genuine experiment \u2014 re-measured on the next backlogrun, and a notice that still does not move it is redesigned rather than kept'
13
+ },
14
+ {
15
+ id: "batching",
16
+ mode_in_lane_child: "off",
17
+ because: "a refusal ends a child turn so it cannot fire as one (kit#2138); kit#2164 replaced it with a notice, kit#2178 took that notice off after it did not move the density, and kit#2276 restored it \u2014 naming the concrete recent calls and recurring every single-call turn \u2014 as a genuine experiment to be redesigned rather than kept if the next backlogrun still missed 1.40. kit#2405 is that re-measurement, and it cuts the notice off (`off`). `josh time:density` read 1.13 calls per round trip over the ten most recent lanes (1.04 in the Issue\u2019s per-message frame), short of the 1.40 target as every advisory attempt before it was (kit#1304/#1329/#1337/#1390/#2164/#2276). The cause is structural rather than wording: a `PreToolUse` hook cannot see the turn it is in \u2014 a turn\u2019s later `tool_use` lines arrive 1.4\u201315s after its first \u2014 so it judges only closed turns, and a notice at turn close still asks for the parallel `tool_use` blocks the model resists. The one lever measured to move the density is a composite command the model emits in a single call (`read:files`/`edit:files`, kit#2202/#2311), which needs no notice. So per the issue\u2019s instruction to discontinue advisory means when the target is missed, the notice is cut off rather than rewritten a seventh time; the density stays readable through `josh time:density`"
18
+ },
19
+ {
20
+ id: "duplicate-read",
21
+ mode_in_lane_child: "notice",
22
+ because: "a refusal ends a child turn so it cannot fire as one (kit#2138), and the unchanged re-reads the guard catches are measured in lane children above all (kit#2298 baseline: 8.0 per lane run) \u2014 so silencing it there (`off`) would neuter it exactly where the count lives. It takes `notice` instead: the child is nudged to scroll up to the earlier read rather than killed. The mode is a genuine experiment, as the batching notice is \u2014 a notice that does not reduce the duplicate reads on the next backlogrun is redesigned rather than kept"
23
+ },
24
+ {
25
+ id: "rule",
26
+ mode_in_lane_child: "refuse",
27
+ because: "it carries the lane-only rules a child depends on (`pre-gate-cut`, `implementation-cut`, `lane-park`) and the safety rules a child must still obey (`shell-body`, `piped-verification`); a blanket suppression would disarm exactly the guards written for the child. `implementation-cut` (joshuafolkken/kit#2310) needs `refuse` for the same reason the pre-gate cut does \u2014 kit#1864 and kit#2310 both measured a cut carried as prose firing 0 times, so a notice, being prose in front of the run, would drop enforcement without a procedural replacement. The pre-gate-cut refusal does collateral a sibling call batched in the same turn \u2014 a `PreToolUse` deny cancels the turn's other parallel calls, which is the harness behavior kit#2177 measured on #2160 (gate refused, its batched `review:brief` cancelled) \u2014 and that collateral is not avoidable at the guard level. `notice` was considered for the pre-gate-cut point and rejected: kit#1864 measured the cut taken 0 times while it was carried as prose, and a notice is prose in front of the run (kit#2178 measured a lane-child notice not moving the number), so a notice would drop enforcement without a procedural replacement. kit#2177 supplies that replacement in the procedure instead \u2014 the chain orders the cut before the gate, so the cut is issued on its own (its `cut` verdict ends the turn, batching with nothing) and the gate runs in the fresh process where the carried cut keeps this guard silent; the refusal therefore stays `refuse` as insurance and the collateral is gone from the default path rather than from the guard"
28
+ }
29
+ ];
30
+ var KNOWN_GUARD_IDS = LANE_GUARD_POLICY.map((entry) => entry.id);
31
+ function entry_for(id) {
32
+ return LANE_GUARD_POLICY.find((entry) => entry.id === id.trim());
33
+ }
34
+ function mode_in_lane_child(id) {
35
+ return entry_for(id)?.mode_in_lane_child ?? DEFAULT_MODE;
36
+ }
37
+ function mode_here(id, directory = process.cwd(), source = process.env) {
38
+ const lane_mode = mode_in_lane_child(id);
39
+ if (lane_mode === DEFAULT_MODE) return DEFAULT_MODE;
40
+ return lane_child_marker.is_child_of(directory, source) ? lane_mode : DEFAULT_MODE;
41
+ }
42
+ function is_suppressed_here(id, directory = process.cwd(), source = process.env) {
43
+ return mode_here(id, directory, source) === OFF_MODE;
44
+ }
45
+ var lane_guard_policy = {
46
+ LANE_GUARD_POLICY,
47
+ KNOWN_GUARD_IDS,
48
+ entry_for,
49
+ mode_in_lane_child,
50
+ mode_here,
51
+ is_suppressed_here
52
+ };
53
+
54
+ export {
55
+ lane_guard_policy
56
+ };