@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
@@ -14,31 +14,67 @@ import { shell_segments } from './shell-segments'
14
14
  // existing file, and a synchronous `stat` decides it — a new-file creation and a read-only heredoc stay
15
15
  // silent, an existing-file rewrite is refused.
16
16
  //
17
- // **Three shapes, one row.** (1) A heredoc whose redirect or `tee` target already exists. (2) an
17
+ // **Four shapes, one row.** (1) A heredoc whose redirect or `tee` target already exists. (2) an
18
18
  // interpreter reading a heredoc — or a `node -e` — whose body writes a file: the canonical `python3 -
19
19
  // <<'PY' … open(f,'w') … PY`, which has no shell redirect to stat, is caught by its body instead. (3)
20
- // an in-place `perl -0pi -e`. A read-only heredoc (`cat <<'EOF'`, a compute-only `python3 -`) writes
21
- // nothing and stays silent — the enumeration's rule that a non-firing turn is a kept turn.
20
+ // an in-place `perl -0pi -e`. (4) a heredoc creating an interpreter script whose body writes. Each is
21
+ // read on every command line, not only the first. A read-only heredoc (`cat <<'EOF'`, a compute-only
22
+ // `python3 -`) writes nothing and stays silent — the enumeration's rule that a non-firing turn is a
23
+ // kept turn.
22
24
  //
23
25
  // **It fires on every occurrence** (`git-force.ts`): each inline body is paid for again, so
24
26
  // refused-once-and-free-after would let the second rewrite through.
25
27
 
26
- // A heredoc marker in any of its spellings: `<<EOF`, `<< 'EOF'`, `<<-"EOF"`. Its presence is what
27
- // separates a body-carrying write from an ordinary redirect a run legitimately uses to extract text.
28
- const HEREDOC = /<<-?\s*['"]?\w+/u
29
- // The first physical line carries the command, its redirect and the heredoc marker; the body follows on
30
- // the lines after it. The two are read separately so the redirect target is found on the command line
31
- // and the write indicators are scanned in the body.
28
+ // A heredoc marker in any of its spellings: `<<EOF`, `<< 'EOF'`, `<<-"EOF"`, with its delimiter captured.
29
+ // Its presence is what separates a body-carrying write from an ordinary redirect a run legitimately uses
30
+ // to extract text. A here-string (`<<<`) and an arithmetic shift (`$((1<<2))`) carry no body, so a third
31
+ // `<` on either side and a digit delimiter are excluded.
32
+ const HEREDOC = /(?<!<)<<(?!<)-?\s*['"]?([A-Z_a-z]\w*)/u
32
33
  const LINE_BREAK = '\n'
33
34
 
34
- function first_line(command: string): string {
35
- const [line] = command.split(LINE_BREAK)
35
+ function has_heredoc(command: string): boolean {
36
+ return HEREDOC.test(command)
37
+ }
36
38
 
37
- return line ?? ''
39
+ // The lines the shell reads as commands — every physical line outside a heredoc body — each paired with
40
+ // the heredoc body it opened. A command, its redirect and its heredoc marker share one of these; the body
41
+ // follows up to the delimiter line. Reading only the first physical line missed the interpreter a run
42
+ // opened after a `cd <lane>` line (joshuafolkken/kit#3155), so every command line is read, a body line is
43
+ // never mistaken for one, and a write is judged only in the body its own command opened.
44
+ interface CommandLine {
45
+ text: string
46
+ body: string
38
47
  }
39
48
 
40
- function has_heredoc(command: string): boolean {
41
- return HEREDOC.test(command)
49
+ interface LineScan {
50
+ lines: ReadonlyArray<CommandLine>
51
+ delimiter: string | undefined
52
+ }
53
+
54
+ function with_body_line(lines: ReadonlyArray<CommandLine>, line: string): Array<CommandLine> {
55
+ const opener = lines.at(-1)
56
+
57
+ if (opener === undefined) return [...lines]
58
+
59
+ return [...lines.slice(0, -1), { ...opener, body: `${opener.body}${line}${LINE_BREAK}` }]
60
+ }
61
+
62
+ function scan_line(scan: LineScan, line: string): LineScan {
63
+ if (scan.delimiter === undefined) {
64
+ return { lines: [...scan.lines, { text: line, body: '' }], delimiter: HEREDOC.exec(line)?.[1] }
65
+ }
66
+
67
+ if (line.trim() === scan.delimiter) return { ...scan, delimiter: undefined }
68
+
69
+ return { ...scan, lines: with_body_line(scan.lines, line) }
70
+ }
71
+
72
+ function command_lines(command: string): ReadonlyArray<CommandLine> {
73
+ let scan: LineScan = { lines: [], delimiter: undefined }
74
+
75
+ for (const line of command.split(LINE_BREAK)) scan = scan_line(scan, line)
76
+
77
+ return scan.lines
42
78
  }
43
79
 
44
80
  // A file redirect (`> path`, `>> path`) or a `tee path`, with the target captured. A descriptor
@@ -53,40 +89,56 @@ function redirect_target(line: string): string | undefined {
53
89
  return target === undefined ? undefined : target.replaceAll(QUOTES, '')
54
90
  }
55
91
 
56
- // Shape 1: a heredoc write whose target already exists. The existence check is injected so the suite
57
- // controls it without touching the real filesystem, the same way `lane-park.ts` injects the lane fact.
58
- function writes_existing_file(command: string, has_file: (path: string) => boolean): boolean {
59
- if (!has_heredoc(command)) return false
60
-
61
- const target = redirect_target(first_line(command))
62
-
63
- return target !== undefined && has_file(target)
64
- }
65
-
66
92
  // The interpreters that carry a body to run rather than a region to read. `cat` / `grep` / `sed -n`
67
93
  // are deliberately absent — a heredoc fed to them is the read-only form `file-edits.md` allows.
68
- const BODY_INTERPRETERS: ReadonlySet<string> = new Set(['python', 'python3', 'ruby', 'perl', 'php'])
94
+ const BODY_INTERPRETERS: ReadonlySet<string> = new Set([
95
+ 'python',
96
+ 'python3',
97
+ 'ruby',
98
+ 'perl',
99
+ 'php',
100
+ 'node',
101
+ ])
69
102
  const NODE = 'node'
70
103
  const NODE_EVAL = /(?:^|\s)(?:-e|--eval)\b/u
71
104
 
72
105
  // A body that writes a file, in Python and Node spellings — split across two patterns so neither
73
106
  // exceeds the regex-complexity ceiling. The named write calls: `writeFile` covers `writeFileSync`,
74
- // `appendFile` covers its sync form, and `.write(` covers `.writelines(`.
75
- const WRITE_CALL = /writeFile|appendFile|createWriteStream|\.write(?:lines)?\s*\(/u
107
+ // `appendFile` covers its sync form, and `.write(` covers `.writelines(` and Python's path-object
108
+ // `.write_text(` / `.write_bytes(`.
109
+ const WRITE_CALL = /writeFile|appendFile|createWriteStream|\.write(?:lines|_text|_bytes)?\s*\(/u
76
110
  // The mode-string and relocation forms: an `open(…, "w"|"a"|"x")`, `os.replace`, `shutil.copy` / `move`.
77
111
  // A mode string of `r` alone (a read) matches none of these, so a compute- or read-only heredoc stays
78
112
  // silent.
79
113
  const WRITE_MODE = /open\s*\([^)]*['"][rbt+]*[wax]|os\.replace|shutil\.(?:copy|move)/u
80
114
 
81
- function body_writes(command: string): boolean {
82
- return WRITE_CALL.test(command) || WRITE_MODE.test(command)
115
+ function body_writes(body: string): boolean {
116
+ return WRITE_CALL.test(body) || WRITE_MODE.test(body)
117
+ }
118
+
119
+ // A script file for one of the body interpreters. Creating one is a new-file creation, but a script
120
+ // whose body writes is the interpreter rewrite moved one call later — `cat > edit.py <<EOF` then
121
+ // `python3 edit.py` carries the same replacement text (joshuafolkken/kit#3155).
122
+ const SCRIPT_FILE = /\.(?:py|rb|pl|php|js|mjs|cjs)$/u
123
+
124
+ // Shapes 1 and 4: a heredoc write whose target already exists, or whose target is a script that writes.
125
+ // The existence check is injected so the suite controls it without touching the real filesystem, the
126
+ // same way `lane-park.ts` injects the lane fact.
127
+ function is_heredoc_write(line: CommandLine, has_file: (path: string) => boolean): boolean {
128
+ const target = has_heredoc(line.text) ? redirect_target(line.text) : undefined
129
+
130
+ if (target === undefined) return false
131
+ if (has_file(target)) return true
132
+
133
+ return SCRIPT_FILE.test(target) && body_writes(line.body)
83
134
  }
84
135
 
85
136
  // Shape 2: an interpreter reading a heredoc, or a `node -e`, whose body writes.
86
- function is_interpreter_write(segment: string, command: string): boolean {
137
+ function is_interpreter_write(segment: string, body: string): boolean {
87
138
  const leading = time_shell.leading_word(segment)
88
139
 
89
- if (BODY_INTERPRETERS.has(leading)) return has_heredoc(command) && body_writes(command)
140
+ if (!BODY_INTERPRETERS.has(leading)) return false
141
+ if (has_heredoc(segment)) return body_writes(body)
90
142
 
91
143
  return leading === NODE && NODE_EVAL.test(segment) && body_writes(segment)
92
144
  }
@@ -103,19 +155,21 @@ function is_in_place_perl(segment: string): boolean {
103
155
  return segment.split(WHITESPACE).some((word) => PERL_IN_PLACE.test(word))
104
156
  }
105
157
 
106
- // The interpreter and perl shapes are read per first-line segment, so a heredoc marker on the same line
107
- // is judged against the command that opened it rather than against a later one in a chain.
108
- function has_body_interpreter(command: string): boolean {
158
+ // The interpreter and perl shapes are read per command-line segment, so a heredoc marker is judged
159
+ // against the command that opened it rather than against a later one in a chain.
160
+ function has_body_interpreter(line: CommandLine): boolean {
109
161
  return shell_segments
110
- .segments_of(first_line(command))
111
- .some((segment) => is_interpreter_write(segment, command) || is_in_place_perl(segment))
162
+ .segments_of(line.text)
163
+ .some((segment) => is_interpreter_write(segment, line.body) || is_in_place_perl(segment))
112
164
  }
113
165
 
114
166
  function carries_a_file_body(
115
167
  command: string,
116
168
  has_file: (path: string) => boolean = existsSync,
117
169
  ): boolean {
118
- return writes_existing_file(command, has_file) || has_body_interpreter(command)
170
+ return command_lines(command).some(
171
+ (line) => is_heredoc_write(line, has_file) || has_body_interpreter(line),
172
+ )
119
173
  }
120
174
 
121
175
  // The instruction in the shape a refusal can carry: what the command is doing, the safe tool, and the
@@ -123,11 +177,13 @@ function carries_a_file_body(
123
177
  // surprising — the command body is billed on every later request of the run.
124
178
  const FILE_BODY_REASON =
125
179
  "⛔ file body in a shell command: this carries a file's new text inline — a heredoc rewriting an " +
126
- 'existing file, an interpreter (`python3 - <<`, `node -e`) whose body writes, or an in-place `perl ' +
127
- '-0pi -e`. `CLAUDE.md` forbids it because the command body is re-read on every later request of the ' +
128
- 'run, so an editing script written early is billed for the rest of it (`file-edits.md` measured one ' +
129
- 'run at ~2m45s spent retyping existing lines). Edit a region with the Edit tool (old / new), or pass ' +
130
- 'a body by path (`--field body=@<path>`, `--body-file <path>`). A new-file creation (`cat > ' +
180
+ 'existing file, an interpreter (`python3 - <<`, `node -e`) whose body writes, a script file (`cat > ' +
181
+ 'edit.py <<EOF`) whose body writes, or an in-place `perl -0pi -e`. `CLAUDE.md` forbids it because ' +
182
+ 'the command body is re-read on every later request of the run, so an editing script written early ' +
183
+ 'is billed for the rest of it (`file-edits.md` measured one run at ~2m45s spent retyping existing ' +
184
+ 'lines). Edit a region with the Edit tool (old / new); a change scattered so widely that the ' +
185
+ 'partial edits would carry more than the file is rewritten whole with the Write tool, never a ' +
186
+ 'heredoc. Pass a body by path (`--field body=@<path>`, `--body-file <path>`). A new-file creation (`cat > ' +
131
187
  '<new-file> <<EOF`), a read-only heredoc (`cat <<EOF`), and a short `sed -i` stay allowed. **This ' +
132
188
  'rule fires on every occurrence, not once per run.**'
133
189
 
@@ -8,7 +8,7 @@ import { tail_commands } from './tail-commands'
8
8
  // The trigger and the decision behind the `filing-cap` row of `delivered-rules.ts`
9
9
  // (joshuafolkken/kit#2119).
10
10
  //
11
- // **Automatic filing is capped at 10 Issues per run, refused at the eleventh filing.** §2d and
11
+ // **Automatic filing is capped at 10 Issues per run, refused at the eleventh filing.** `prerequisite.md` and
12
12
  // `observation-filing.md` state the ceiling; nothing counted it, so a run that over-filed was held
13
13
  // only by self-restraint. The input is mechanical — how many Issues this run has already filed — so
14
14
  // it is a delivered rule rather than resident prose. **No new predicate**: the filing is
@@ -108,7 +108,7 @@ const FILING_CAP_REASON =
108
108
  `⛔ filing cap: this run has already filed ${String(FILING_CAP)} Issues, the per-run ceiling — on ` +
109
109
  'reaching it, stop and report rather than filing more. Automatic filing is capped so a run cannot ' +
110
110
  'manufacture Issues faster than they can be closed (`.claude/skills/workflow-commands/SKILL.md` → ' +
111
- "§2d, `observation-filing.md`). A further Issue that genuinely must exist is a person's call, not " +
111
+ "`prerequisite.md`, `observation-filing.md`). A further Issue that genuinely must exist is a person's call, not " +
112
112
  "this run's. **This rule fires on every filing past the cap, not once per run**, so reissuing the " +
113
113
  'same filing will be refused again.'
114
114
 
@@ -4,7 +4,7 @@ import { issue_citation } from './issue-citation'
4
4
  // The predicates behind the `filing-offer` row of the stop guard (joshuafolkken/kit#2422).
5
5
  //
6
6
  // **A first-party filing is Tier A, and an offer to file is that filing deferred to the user.**
7
- // `SKILL.md` → §2i says a run that judges something worth filing files it without asking, yet a reply
7
+ // `observation-filing.md` says a run that judges something worth filing files it without asking, yet a reply
8
8
  // can still end on "it is worth filing — say so if you want it filed". The rule was resident; what was
9
9
  // missing was anything that fired at the moment of hesitation. That moment is one `Stop` payload: a
10
10
  // reply that offers to file, on a turn that filed nothing.
@@ -30,6 +30,11 @@ const REPO_PATH = /repos\/([^/\s'"?]+)\/([^/\s'"?]+)/u
30
30
  const OWNER_GROUP = 1
31
31
  const REPO_GROUP = 2
32
32
 
33
+ // The placeholders `gh api` expands to its base repository's owner before it sends the request
34
+ // (joshuafolkken/kit#3188) — `{owner}`, and the legacy `:owner` it still accepts. Which owner that is
35
+ // depends on the checkout's remotes, so the caller resolves it rather than this parser.
36
+ const OWNER_PLACEHOLDERS: ReadonlySet<string> = new Set(['{owner}', ':owner'])
37
+
33
38
  interface RepoTarget {
34
39
  owner: string
35
40
  repo: string
@@ -65,6 +70,18 @@ function repo_target(segment: string): RepoTarget | undefined {
65
70
  return { owner: match[OWNER_GROUP] ?? '', repo: match[REPO_GROUP] ?? '' }
66
71
  }
67
72
 
68
- const gh_api = { GH_FLAGS, is_gh_api, is_read, is_write, method_of, repo_target }
73
+ function is_owner_placeholder(owner: string): boolean {
74
+ return OWNER_PLACEHOLDERS.has(owner)
75
+ }
76
+
77
+ const gh_api = {
78
+ GH_FLAGS,
79
+ is_gh_api,
80
+ is_owner_placeholder,
81
+ is_read,
82
+ is_write,
83
+ method_of,
84
+ repo_target,
85
+ }
69
86
 
70
87
  export { gh_api }
@@ -6,6 +6,7 @@ import type { GuardRun } from '#scripts/josh/hook-decision'
6
6
  import { lane_child_marker, type MarkerSource } from '#scripts/lane/lane-child-marker'
7
7
  import { lane_paths } from '#scripts/lane/lane-paths'
8
8
  import { run_cut, type RunCut } from '#scripts/run/run-cut'
9
+ import { run_cut_handoff } from '#scripts/run/run-cut-handoff'
9
10
  import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
10
11
  import { bash_triggers } from './bash-triggers'
11
12
  import { implementation_cut_verdict } from './implementation-cut-verdict'
@@ -161,7 +162,8 @@ const IMPLEMENTATION_CUT_REASON =
161
162
  `cost has crossed the shared ${THRESHOLD_TEXT} threshold mid-implementation, so the thinking accumulated ` +
162
163
  'so far is now re-read on every later request. Take the cut before this edit. ' +
163
164
  'First write a handoff file with the Write tool — the user’s instruction verbatim, what you have ' +
164
- 'completed, what remains, and what you deliberately did not touch — and pass it as `--handoff <path>`, ' +
165
+ 'completed, what remains, and what you deliberately did not touch, as ' +
166
+ `${run_cut_handoff.HANDOFF_FORMAT} (Markdown is refused) — and pass it as \`--handoff <path>\`, ` +
165
167
  'so the fresh process resumes on the original instruction rather than the working tree alone ' +
166
168
  '(joshuafolkken/kit#2354); a resume that finds no instruction is refused `incomplete` rather than ' +
167
169
  'continuing blind. ' +
@@ -26,7 +26,9 @@ import { run_tail } from './run-tail'
26
26
  // but only after its preflight — the scoped checks — has passed in the calling process, so a
27
27
  // backgrounded `ship` is killed at the turn's end before any supervisor exists: the #3022 child did
28
28
  // exactly that and was parked as a failure. Issued in the foreground it returns at `launched` within the
29
- // tool timeout, and the supervisor carries the rest.
29
+ // tool timeout, and the supervisor carries the rest. A backgrounded `ship` alone is no longer refused but
30
+ // rewritten into that foreground call (`foreground_input`, joshuafolkken/kit#3154); one chained after
31
+ // another long command is still refused.
30
32
  //
31
33
  // **Its stop-time half reads the same fact from the other side.** A backgrounded task the transcript
32
34
  // launched but never saw finish is still running, and in a child the stop about to happen kills it —
@@ -35,29 +37,61 @@ import { run_tail } from './run-tail'
35
37
 
36
38
  // The josh subcommands that run long enough to be backgrounded, as `time_shell` names them — canonical,
37
39
  // so the `ga` / `g` / `fu` aliases are covered by the one spelling.
38
- const LONG_RUNNING_COMMANDS: ReadonlySet<string> = new Set(
39
- ['gate', 'git', 'followup', 'ship'].map((name) => `${time_shell.JOSH_PREFIX}${name}`),
40
- )
40
+ const SHIP_COMMAND = `${time_shell.JOSH_PREFIX}ship`
41
+ const LONG_RUNNING_COMMANDS: ReadonlySet<string> = new Set([
42
+ ...['gate', 'git', 'followup'].map((name) => `${time_shell.JOSH_PREFIX}${name}`),
43
+ SHIP_COMMAND,
44
+ ])
41
45
 
42
46
  const BACKGROUND_KEY = 'run_in_background'
47
+ const TIMEOUT_KEY = 'timeout'
48
+ // The longest a foreground Bash call may run. A backgrounded call rarely carries a `timeout`, and one that
49
+ // does may exceed the foreground cap, so the rewrite sets it: left at the two-minute default, a preflight
50
+ // whose scoped pair outruns it is cut before `launched`, the #3027 failure the rewrite exists to avoid.
51
+ const FOREGROUND_TIMEOUT_MS = 600_000
43
52
 
44
53
  function is_backgrounded(input: unknown): boolean {
45
54
  return json_value.is_record(input) && input[BACKGROUND_KEY] === true
46
55
  }
47
56
 
48
- function runs_long_command(command: string): boolean {
49
- return time_shell.josh_commands_of(command).some((name) => LONG_RUNNING_COMMANDS.has(name))
57
+ function long_commands_of(command: string): Array<string> {
58
+ return time_shell.josh_commands_of(command).filter((name) => LONG_RUNNING_COMMANDS.has(name))
59
+ }
60
+
61
+ // A call whose long-running work is `ship` alone. In a child a foreground `ship` already detaches
62
+ // itself after its preflight (`run-ship-cli.ts` → `should_detach`), so the only fault in backgrounding
63
+ // it is the flag — and the flag is what the hook rewrites (joshuafolkken/kit#3154).
64
+ function ships_alone(command: string): boolean {
65
+ const names = long_commands_of(command)
66
+
67
+ return names.length > 0 && names.every((name) => name === SHIP_COMMAND)
68
+ }
69
+
70
+ function backgrounded_bash_command(call: GuardedCall): string | undefined {
71
+ if (call.name !== cost_blocks.BASH_TOOL || !is_backgrounded(call.input)) return undefined
72
+
73
+ return time_shell.bash_command(call.input)
74
+ }
75
+
76
+ function is_backgrounded_ship(call: GuardedCall): boolean {
77
+ const command = backgrounded_bash_command(call)
78
+
79
+ return command !== undefined && ships_alone(command)
50
80
  }
51
81
 
52
82
  // The call's own fields alone: a backgrounded Bash call that runs a long josh command, or the push step
53
83
  // in either spelling. A child's foreground push is claimed too, because `run-tail` would otherwise answer
54
84
  // it with "reissue it backgrounded" — the one reissue this row then refuses — so the child is handed the
55
- // detached route on its first refusal rather than two contradictory ones.
85
+ // detached route on its first refusal rather than two contradictory ones. A backgrounded `ship` alone is
86
+ // not refused: `foreground_input` turns it into the foreground call the refusal used to ask for.
56
87
  function is_candidate(call: GuardedCall): boolean {
57
88
  if (run_tail.is_push_step_call(call)) return true
58
- if (call.name !== cost_blocks.BASH_TOOL || !is_backgrounded(call.input)) return false
59
89
 
60
- return runs_long_command(time_shell.bash_command(call.input))
90
+ const command = backgrounded_bash_command(call)
91
+
92
+ if (command === undefined || is_backgrounded_ship(call)) return false
93
+
94
+ return long_commands_of(command).length > 0
61
95
  }
62
96
 
63
97
  // True when this session is a dispatched lane child backgrounding a long-running josh command. The call's
@@ -69,6 +103,26 @@ function is_background_long_run(
69
103
  return is_candidate(call) && is_lane_child()
70
104
  }
71
105
 
106
+ // **A backgrounded `ship` in a child is rewritten, not refused** (joshuafolkken/kit#3154). The refusal
107
+ // asked for nothing but the same call in the foreground — 14 of 41 lane `ship` refusals in one measured
108
+ // backlogrun were that round trip, paid at the run's largest context. The input with the flag cleared,
109
+ // or `undefined` for every call this does not apply to.
110
+ function foreground_input(
111
+ call: GuardedCall,
112
+ is_lane_child: () => boolean = () => lane_child_marker.is_child_of(process.cwd()),
113
+ ): Record<string, unknown> | undefined {
114
+ if (!is_backgrounded_ship(call) || !json_value.is_record(call.input)) return undefined
115
+
116
+ if (!is_lane_child()) return undefined
117
+
118
+ return { ...call.input, [BACKGROUND_KEY]: false, [TIMEOUT_KEY]: FOREGROUND_TIMEOUT_MS }
119
+ }
120
+
121
+ const FOREGROUND_NOTE =
122
+ 'lane background: this lane child backgrounded `josh ship`, which would die with the headless turn ' +
123
+ 'before its supervisor exists — so it was run in the foreground instead, where it detaches itself ' +
124
+ 'after the preflight. End the turn on `launched` / `busy` (joshuafolkken/kit#3154).'
125
+
72
126
  // The launches `launched_ids` reads off the transcript tail that never saw their finish. A launch is
73
127
  // read off its own tool result and a finish off the harness notice, both by `time_transcript_line`.
74
128
  function unfinished_ids(
@@ -103,7 +157,8 @@ const LANE_BACKGROUND_REASON =
103
157
  'verification done, the run parked instead of merged). Hand the gate-to-merge region to the detached ' +
104
158
  'supervisor instead: `pnpm josh ship --detach --review "<title> #<N>"` (add `--cite <N>`) issued in ' +
105
159
  'the foreground — its preflight runs in this process before the supervisor exists, so a backgrounded ' +
106
- '`ship` dies with the turn (joshuafolkken/kit#3027) — then end the turn on `launched` / `busy`. ' +
160
+ '`ship` dies with the turn (joshuafolkken/kit#3027; a backgrounded `ship` issued alone is moved to ' +
161
+ 'the foreground for you, joshuafolkken/kit#3154) — then end the turn on `launched` / `busy`. ' +
107
162
  '`.claude/skills/workflow-commands/chain-rule.md` step 0 is the single source. A ' +
108
163
  'single check you must read before continuing runs in the foreground within the tool timeout; the ' +
109
164
  'commit-push step does not — in a child it too belongs to the detached ship, in either spelling. This ' +
@@ -128,9 +183,11 @@ const ROW = {
128
183
  }
129
184
 
130
185
  const lane_background = {
186
+ FOREGROUND_NOTE,
131
187
  LANE_BACKGROUND_REASON,
132
188
  LANE_BACKGROUND_STOP_REASON,
133
189
  ROW,
190
+ foreground_input,
134
191
  is_background_long_run,
135
192
  pending_agent_ids,
136
193
  pending_background_ids,
@@ -1,4 +1,5 @@
1
1
  import { destructive_command } from './destructive-command'
2
+ import { direct_pr_create } from './direct-pr-create'
2
3
  import { index_guard } from './index-guard'
3
4
  import { protected_files } from './protected-files'
4
5
 
@@ -6,8 +7,15 @@ import { protected_files } from './protected-files'
6
7
  // `delivered-rules.ts` spreads one entry. Each reads argv or a file path rather than a glob: an index
7
8
  // mutation in any git spelling, a recursive forced `rm` or a destructive `gh` call, and a Read of
8
9
  // `.env` or an edit of a consumer's `.claude/settings.json`. Their triggers are disjoint from every
9
- // other row's — `git restore` is split with `worktree-mutation` by `--staged` alone.
10
- const ROWS = [index_guard.ROW, destructive_command.ROW, protected_files.ROW] as const
10
+ // other row's — `git restore` is split with `worktree-mutation` by `--staged` alone. The fourth row,
11
+ // `direct-pr-create` (joshuafolkken/kit#3183), closes the same kind of gap for `gh pr create`, which
12
+ // the deny list never listed: it claims `gh pr create` and a `gh api` write to the pulls collection.
13
+ const ROWS = [
14
+ index_guard.ROW,
15
+ destructive_command.ROW,
16
+ protected_files.ROW,
17
+ direct_pr_create.ROW,
18
+ ] as const
11
19
 
12
20
  const permission_guards = { ROWS }
13
21
 
@@ -103,13 +103,13 @@ const STOP_NOTIFY_REASON =
103
103
  'your previous reply. This fires only while the hold is held and no notify is on the transcript, and ' +
104
104
  '`stop_hook_active` lets a second stop through so a run is never wedged.'
105
105
 
106
- // **A clean tree that still holds is a tree the next run will trample.** `SKILL.md` → §2f: a stop that
106
+ // **A clean tree that still holds is a tree the next run will trample.** `working-tree-hold.md`: a stop that
107
107
  // leaves the tree clean releases the hold; a `halfrun` pre-commit stop and a `needs-human-review` stop
108
108
  // keep it because their tree is dirty, which is why this row is silent whenever the tree is not clean.
109
109
  const HOLD_RELEASE_REASON =
110
110
  '⛔ working-tree hold not released: this working tree is clean but its `run:hold` record is still in ' +
111
111
  'place, so the next run here runs `git switch main && git pull` believing the tree is free while ' +
112
- 'you hold it. `.claude/skills/workflow-commands/SKILL.md` → §2f: a stop that leaves the tree clean ' +
112
+ 'you hold it. The workflow-commands skill → `working-tree-hold.md`: a stop that leaves the tree clean ' +
113
113
  'releases the hold with `pnpm josh run:release <N>` (bare for a `new` entry). A `halfrun` ' +
114
114
  'pre-commit stop and a `needs-human-review` stop keep the hold because their tree is dirty, and a ' +
115
115
  '`prrun` stop keeps it by its stop mark — this row is silent there. Release it, then end with a one-line confirmation that it was released — do ' +
@@ -139,11 +139,11 @@ function build_citation_reason(references: ReadonlyArray<string>): string {
139
139
 
140
140
  // **An offer to file is a Tier A filing deferred to the user** (joshuafolkken/kit#2422). The judgement
141
141
  // is already made, so the reason hands over the filing chain rather than a question; the rule itself
142
- // is resident in `SKILL.md` → §2i and only pointed at.
142
+ // is resident in `observation-filing.md` and only pointed at.
143
143
  const FILING_OFFER_REASON =
144
144
  '⛔ filing offer: your reply offers to file an Issue instead of filing it. Filing into a ' +
145
145
  'first-party repository is Tier A — the trigger is the judgement that it is worth filing, not the ' +
146
- "run's progress (`.claude/skills/workflow-commands/SKILL.md` → §2i, `observation-filing.md`). Run " +
146
+ "run's progress (the workflow-commands skill → `observation-filing.md`). Run " +
147
147
  '`pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2>` — it scouts, lints, labels and ' +
148
148
  'runs `epic:bundle` itself — then end with the one-line citation of what was filed; do not repeat ' +
149
149
  'your previous reply. If it is not worth filing ' +
@@ -17,7 +17,7 @@ import { run_tail } from './run-tail'
17
17
  // pays nothing and only a `pnpm josh git -y` spawns `git status`.
18
18
  //
19
19
  // **Once per run, because the exemption is a person's judgement a synchronous guard cannot read.** The
20
- // non-executable-config and cosmetic-asset arms of `CLAUDE.md`'s "Non-runtime updates" exception stay
20
+ // non-executable-config and cosmetic-asset arms of `prompts/testing-guide.md`'s "Non-runtime updates" exception stay
21
21
  // `required` by the classifier, and a person declares them in the Step 0 work summary — which no label
22
22
  // or transcript tail records. So a row that fired every time would wedge exactly that declared-exempt
23
23
  // commit; refusing once and passing the reissue is the same safe direction `issue-comments` takes, and
@@ -25,34 +25,78 @@ import { shell_segments } from './shell-segments'
25
25
  // A third-party write means all three: a `gh api` call, a write, and a repository target whose owner
26
26
  // the classification calls third-party. `unknown` (an unreadable session remote, an unparseable path)
27
27
  // is not third-party — the rule stays silent rather than refusing a write it cannot prove is external.
28
- function is_third_party_write_segment(segment: string, session_owner: string | undefined): boolean {
28
+ // The `{owner}` placeholder (joshuafolkken/kit#3188) is judged by every owner `gh` could expand it to:
29
+ // first-party only when all of them are, so a fork checkout whose `upstream` is another owner's still
30
+ // refuses the write `gh` may send there. A candidate whose owner could not be read counts as
31
+ // third-party here — unlike an unreadable session owner, it is a remote `gh` may really write to.
32
+ function is_third_party_write_segment(
33
+ segment: string,
34
+ session_owner: string | undefined,
35
+ resolve_placeholder_owners: () => Array<string | undefined>,
36
+ ): boolean {
29
37
  if (!gh_api.is_gh_api(segment) || !gh_api.is_write(segment)) return false
30
38
 
31
39
  const target = gh_api.repo_target(segment)
32
-
33
40
  if (target === undefined) return false
34
41
 
35
- return repo_party.classify(session_owner, target.owner) === repo_party.THIRD_PARTY
42
+ if (!gh_api.is_owner_placeholder(target.owner)) {
43
+ return repo_party.classify(session_owner, target.owner) === repo_party.THIRD_PARTY
44
+ }
45
+
46
+ return resolve_placeholder_owners().some(
47
+ (owner) =>
48
+ owner === undefined || repo_party.classify(session_owner, owner) === repo_party.THIRD_PARTY,
49
+ )
50
+ }
51
+
52
+ // A segment that makes `gh` resolve the placeholder against something other than this process's
53
+ // checkout: a directory change, or a `GH_REPO` assignment. The guard reads the hook's own cwd and
54
+ // environment, so after either one its candidate set no longer describes what `gh` will expand.
55
+ const GH_CONTEXT_CHANGE = /(?:^|\s)(?:cd|pushd)(?:\s|$)|(?:^|\s)(?:export\s+)?GH_REPO=/u
56
+
57
+ function unreadable_candidates(): Array<string | undefined> {
58
+ return [undefined]
59
+ }
60
+
61
+ // The placeholder candidates for the segment at `index`: the checkout's own, unless an earlier
62
+ // segment of the same line moved `gh` elsewhere — then one unreadable candidate, refused.
63
+ function candidates_at(
64
+ segments: ReadonlyArray<string>,
65
+ index: number,
66
+ resolve_placeholder_owners: () => Array<string | undefined>,
67
+ ): () => Array<string | undefined> {
68
+ const has_moved = segments.slice(0, index).some((segment) => GH_CONTEXT_CHANGE.test(segment))
69
+
70
+ return has_moved ? unreadable_candidates : resolve_placeholder_owners
36
71
  }
37
72
 
38
73
  // The session owner is read only when a segment is actually a `gh api` write to a repository — so an
39
- // ordinary shell line, and every read, pays nothing for the local config read. `resolve_owner` is
40
- // injected so the suite fixes the owner without touching the real remote, the way `file-body.ts`
41
- // injects `has_file`. Each segment is judged on its own, so a `gh api` write quoted inside another
42
- // command's argument is read as the write it is not, exactly as every other trigger here reads its own.
74
+ // ordinary shell line, and every read, pays nothing for the local config read. `resolve_owner` and
75
+ // `resolve_placeholder_owners` are injected so the suite fixes the owners without touching the real
76
+ // remotes, the way `file-body.ts` injects `has_file`. Each segment is judged on its own, so a `gh api`
77
+ // write quoted inside another command's argument is read as the write it is not, exactly as every
78
+ // other trigger here reads its own.
43
79
  function writes_third_party(
44
80
  command: string,
45
81
  resolve_owner: () => string | undefined = repo_party.current_owner,
82
+ resolve_placeholder_owners: () => Array<string | undefined> = repo_party.placeholder_owners,
46
83
  ): boolean {
47
- const targets = shell_segments
48
- .segments_of(command)
49
- .filter((segment) => gh_api.is_gh_api(segment) && gh_api.is_write(segment))
84
+ const segments = shell_segments.segments_of(command)
85
+ const has_write = segments.some(
86
+ (segment) => gh_api.is_gh_api(segment) && gh_api.is_write(segment),
87
+ )
50
88
 
51
- if (targets.length === 0) return false
89
+ if (!has_write) return false
52
90
 
53
91
  const session_owner = resolve_owner()
54
92
 
55
- return targets.some((segment) => is_third_party_write_segment(segment, session_owner))
93
+ return segments.some((segment, index) =>
94
+ is_third_party_write_segment(
95
+ segment,
96
+ session_owner,
97
+ candidates_at(segments, index, resolve_placeholder_owners),
98
+ ),
99
+ )
56
100
  }
57
101
 
58
102
  // The instruction in the shape a refusal can carry: what the command does, why it is Tier C, and what
@@ -41,6 +41,22 @@ const UNTOUCHED_LABEL = 'Deliberately untouched'
41
41
  const INSTRUCTION_LABEL = 'Instruction (verbatim)'
42
42
  const EMPTY_LIST_NOTE = '(none)'
43
43
 
44
+ type HandoffKey = keyof Handoff
45
+
46
+ function describe_key(key: HandoffKey): string {
47
+ const value = handoff_schema.shape[key] instanceof z.ZodArray ? '["…"]' : '"…"'
48
+
49
+ return `"${key}": ${value}`
50
+ }
51
+
52
+ // The shape a handoff file must have, derived from `handoff_schema` so the refusal that asks for the file
53
+ // and the error that rejects it can never name a key the parser does not read (joshuafolkken/kit#3195).
54
+ // A refusal that listed the four items in prose was answered with Markdown, which `parse_handoff` rejects.
55
+ const HANDOFF_FORMAT = `a JSON object {${handoff_schema
56
+ .keyof()
57
+ .options.map((key) => describe_key(key))
58
+ .join(', ')}}`
59
+
44
60
  function parse_handoff(raw: string): Handoff | undefined {
45
61
  try {
46
62
  const parsed = handoff_schema.safeParse(JSON.parse(raw))
@@ -84,7 +100,10 @@ function load_handoff(handoff_path: string | undefined, max_bytes: number): Hand
84
100
  const handoff = read_handoff_file(handoff_path)
85
101
 
86
102
  if (handoff === undefined) {
87
- return { kind: 'bad', note: `--handoff ${handoff_path} is not a readable handoff file` }
103
+ return {
104
+ kind: 'bad',
105
+ note: `--handoff ${handoff_path} is not a readable handoff file — write ${HANDOFF_FORMAT}`,
106
+ }
88
107
  }
89
108
 
90
109
  if (!is_within_bound(handoff, max_bytes)) {
@@ -114,6 +133,7 @@ function describe_handoff(handoff: Handoff): string {
114
133
  }
115
134
 
116
135
  const run_cut_handoff = {
136
+ HANDOFF_FORMAT,
117
137
  describe_handoff,
118
138
  handoff_schema,
119
139
  is_complete_handoff,