@joshuafolkken/kit 1.1128.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 (192) hide show
  1. package/.claude/agents/investigator.md +17 -0
  2. package/.claude/settings.json +5 -4
  3. package/.claude/skills/epic-commands/SKILL.md +27 -239
  4. package/.claude/skills/epic-commands/epic-bundle.md +115 -0
  5. package/.claude/skills/epic-commands/execution-waves.md +126 -0
  6. package/.claude/skills/workflow-commands/SKILL.md +48 -194
  7. package/.claude/skills/workflow-commands/background-commands.md +1 -1
  8. package/.claude/skills/workflow-commands/backlogrun-child.md +12 -123
  9. package/.claude/skills/workflow-commands/backlogrun-lanes.md +11 -60
  10. package/.claude/skills/workflow-commands/backlogrun-park.md +10 -8
  11. package/.claude/skills/workflow-commands/backlogrun-progress.md +19 -175
  12. package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
  13. package/.claude/skills/workflow-commands/backlogrun-steps.md +64 -144
  14. package/.claude/skills/workflow-commands/backlogrun.md +5 -4
  15. package/.claude/skills/workflow-commands/chain-rule.md +3 -2
  16. package/.claude/skills/workflow-commands/delegation.md +7 -3
  17. package/.claude/skills/workflow-commands/entry-sequence.md +62 -0
  18. package/.claude/skills/workflow-commands/followup.md +104 -181
  19. package/.claude/skills/workflow-commands/fullrun-steps.md +8 -9
  20. package/.claude/skills/workflow-commands/fullrun.md +16 -73
  21. package/.claude/skills/workflow-commands/halfrun.md +10 -36
  22. package/.claude/skills/workflow-commands/into-target.md +4 -4
  23. package/.claude/skills/workflow-commands/issue-comments.md +7 -8
  24. package/.claude/skills/workflow-commands/issue-scout.md +6 -6
  25. package/.claude/skills/workflow-commands/kickoff.md +6 -6
  26. package/.claude/skills/workflow-commands/needs-human-review.md +6 -6
  27. package/.claude/skills/workflow-commands/observation-filing.md +21 -172
  28. package/.claude/skills/workflow-commands/observation-ledger.md +150 -0
  29. package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -93
  30. package/.claude/skills/workflow-commands/prerequisite.md +6 -6
  31. package/.claude/skills/workflow-commands/progress-watcher.md +90 -0
  32. package/.claude/skills/workflow-commands/prrun.md +5 -5
  33. package/.claude/skills/workflow-commands/retrospective.md +9 -9
  34. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  35. package/.claude/skills/workflow-commands/target-repository.md +2 -2
  36. package/.claude/skills/workflow-commands/working-tree-hold.md +7 -5
  37. package/.codex/hooks.json +4 -4
  38. package/AGENTS.md +1 -6
  39. package/CLAUDE.md +58 -120
  40. package/GEMINI.md +1 -6
  41. package/SECURITY.md +1 -0
  42. package/dist/CLAUDE.md +58 -120
  43. package/dist/hooks/batch-guard.js +4 -4
  44. package/dist/hooks/{chunk-MKVGBQPA.js → chunk-5KTCMCOT.js} +14 -1
  45. package/dist/hooks/{chunk-NXYMNPUQ.js → chunk-A6P7PN7P.js} +59 -21
  46. package/dist/hooks/{chunk-77J7IEQM.js → chunk-ANJ2WGFD.js} +1 -1
  47. package/dist/hooks/{chunk-4JZDP5X5.js → chunk-DYAVAM6S.js} +572 -127
  48. package/dist/hooks/{chunk-7HVB7SZG.js → chunk-ENA7P5FB.js} +4 -4
  49. package/dist/hooks/{chunk-M7QKEKWO.js → chunk-GU5MATVE.js} +214 -85
  50. package/dist/hooks/chunk-GYNGP7HI.js +56 -0
  51. package/dist/hooks/{chunk-5SD2YM7Z.js → chunk-URJA6XY4.js} +4 -4
  52. package/dist/hooks/{chunk-XYUIQOW3.js → chunk-V2BTOZHT.js} +6 -4
  53. package/dist/hooks/{chunk-AIOIOMPX.js → chunk-ZDZ555YH.js} +225 -78
  54. package/dist/hooks/codex-hook-adapter.js +10 -10
  55. package/dist/hooks/duplicate-read-guard.js +5 -5
  56. package/dist/hooks/format-edited.js +2 -2
  57. package/dist/hooks/inputs.json +186 -184
  58. package/dist/hooks/investigation-guard.js +5 -5
  59. package/dist/hooks/pretool-guard.js +11 -10
  60. package/dist/hooks/stop-guard.js +5 -5
  61. package/dist/josh.js +33 -3
  62. package/dist/version/index.js +5 -1
  63. package/package.json +7 -4
  64. package/prompts/collaboration-workflow/file-edits.md +11 -10
  65. package/prompts/collaboration-workflow/gh-rest.md +2 -4
  66. package/prompts/collaboration-workflow/glossary.md +1 -1
  67. package/prompts/collaboration-workflow/issue-citation.md +5 -9
  68. package/prompts/collaboration-workflow/issue-template.md +40 -115
  69. package/prompts/collaboration-workflow/operating-rules.md +4 -5
  70. package/prompts/collaboration-workflow/output-bounds.md +12 -78
  71. package/prompts/collaboration-workflow/overview.md +7 -13
  72. package/prompts/collaboration-workflow/plan-comment.md +11 -39
  73. package/prompts/collaboration-workflow/principles.md +3 -4
  74. package/prompts/collaboration-workflow/report-format.md +28 -74
  75. package/prompts/collaboration-workflow/residency.md +12 -109
  76. package/prompts/collaboration-workflow/rule-delivery.md +98 -97
  77. package/prompts/collaboration-workflow/shell-body.md +3 -13
  78. package/prompts/collaboration-workflow/turn-batching.md +14 -88
  79. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -14
  80. package/prompts/collaboration-workflow/wip-cap.md +6 -45
  81. package/prompts/collaboration-workflow.md +1 -13
  82. package/prompts/review-rubric.md +8 -71
  83. package/prompts/review.md +25 -55
  84. package/prompts/testing-guide.md +18 -67
  85. package/scripts/agent/agent-role-profile.ts +1 -1
  86. package/scripts/agent/codex-hooks.ts +3 -12
  87. package/scripts/backlog/backlog-budget.ts +1 -1
  88. package/scripts/backlog/backlog-drive-cli.ts +4 -3
  89. package/scripts/backlog/backlog-drive-offer-argv.ts +10 -1
  90. package/scripts/backlog/backlog-offer.ts +6 -6
  91. package/scripts/behavior/behavior-rules.ts +11 -4
  92. package/scripts/delegation/delegation-policy.ts +9 -1
  93. package/scripts/delegation/investigation-guard.ts +1 -1
  94. package/scripts/delegation/investigation-reads.ts +109 -23
  95. package/scripts/discovery/repo-discovery.ts +21 -7
  96. package/scripts/discovery/repo-origin.ts +26 -0
  97. package/scripts/discovery/repo-party.ts +42 -1
  98. package/scripts/document/backlogrun-parent-read-set.ts +14 -15
  99. package/scripts/document/capped-print.ts +74 -0
  100. package/scripts/document/document-byte-budget.ts +34 -20
  101. package/scripts/document/document-section-cli.ts +4 -3
  102. package/scripts/document/entry-read-budget.ts +21 -9
  103. package/scripts/document/lane-child-read-set.ts +12 -14
  104. package/scripts/document/read-set-cli.ts +4 -4
  105. package/scripts/document/read-set-point-of-use.ts +10 -2
  106. package/scripts/document/resident-budget.ts +47 -0
  107. package/scripts/epic/epic-shape.ts +10 -13
  108. package/scripts/epic/epic-solo.ts +1 -1
  109. package/scripts/exports/unused-members-cli.ts +7 -3
  110. package/scripts/gate/scoped-green.ts +25 -14
  111. package/scripts/gh/git-gh-api-path.ts +7 -0
  112. package/scripts/hooks/pretool-guard-cli.ts +2 -2
  113. package/scripts/hooks/pretool-guard.ts +45 -2
  114. package/scripts/hooks/run-hook.sh +41 -0
  115. package/scripts/init/claude-kit-only-settings.ts +29 -0
  116. package/scripts/init/hook-command-rewrite.ts +18 -35
  117. package/scripts/init/hook-launch.ts +21 -32
  118. package/scripts/init/init-copy-content.ts +8 -2
  119. package/scripts/init/initial-commit.ts +24 -0
  120. package/scripts/init/start-exec.ts +16 -3
  121. package/scripts/init/start-steps.ts +8 -12
  122. package/scripts/issue/issue-auto-ok.ts +95 -0
  123. package/scripts/issue/issue-file-cli.ts +53 -7
  124. package/scripts/issue/issue-file.ts +28 -5
  125. package/scripts/issue/issue-labels.ts +12 -12
  126. package/scripts/issue/issue-rank.ts +1 -1
  127. package/scripts/issue/issue-read-cli.ts +4 -3
  128. package/scripts/issue/issue-read.ts +2 -2
  129. package/scripts/issue/issue-report-failures.ts +6 -2
  130. package/scripts/issue/issue-scout-cli.ts +2 -2
  131. package/scripts/issue/issue-state-cli.ts +2 -2
  132. package/scripts/issue/issue-state.ts +2 -2
  133. package/scripts/issue/issue-wip.ts +70 -0
  134. package/scripts/josh/hook-decision.ts +17 -0
  135. package/scripts/josh/josh-commands-development.ts +10 -0
  136. package/scripts/josh/josh-commands-issue.ts +2 -2
  137. package/scripts/josh/josh-commands-lane.ts +1 -1
  138. package/scripts/josh/josh-commands-project.ts +7 -0
  139. package/scripts/lane/lane-await.ts +46 -2
  140. package/scripts/lane/lane-cli.ts +11 -13
  141. package/scripts/lines/bytes-command.ts +29 -6
  142. package/scripts/observations/observation-ledger-line.ts +1 -1
  143. package/scripts/release/github-release-environment.ts +1 -0
  144. package/scripts/release/github-release.ts +103 -17
  145. package/scripts/release/release-progress.ts +167 -0
  146. package/scripts/release/release-publish.ts +19 -8
  147. package/scripts/release/release-tag.ts +22 -8
  148. package/scripts/rules/decision-oracle-stage.ts +1 -1
  149. package/scripts/rules/decision-oracle.ts +6 -6
  150. package/scripts/rules/delivered-rules.ts +11 -11
  151. package/scripts/rules/direct-pr-create.ts +110 -0
  152. package/scripts/rules/file-body.ts +98 -42
  153. package/scripts/rules/filing-cap.ts +2 -2
  154. package/scripts/rules/filing-offer.ts +1 -1
  155. package/scripts/rules/gh-api.ts +18 -1
  156. package/scripts/rules/implementation-cut.ts +3 -1
  157. package/scripts/rules/lane-background.ts +67 -10
  158. package/scripts/rules/permission-guards.ts +10 -2
  159. package/scripts/rules/stop-rules.ts +4 -4
  160. package/scripts/rules/test-declared-commit.ts +1 -1
  161. package/scripts/rules/third-party-write.ts +56 -12
  162. package/scripts/run/run-carry-cli.ts +3 -1
  163. package/scripts/run/run-carry-conversation.ts +94 -0
  164. package/scripts/run/run-carry-reclaim.ts +37 -0
  165. package/scripts/run/run-carry.ts +21 -5
  166. package/scripts/run/run-cut-handoff.ts +21 -1
  167. package/scripts/run/run-entry-cli.ts +11 -3
  168. package/scripts/run/run-hold-cli.ts +16 -2
  169. package/scripts/run/run-label.ts +50 -0
  170. package/scripts/run/run-merge-steps.ts +12 -4
  171. package/scripts/run/run-merge.ts +1 -1
  172. package/scripts/run/run-prep-cli.ts +6 -2
  173. package/scripts/run/run-prep.ts +17 -0
  174. package/scripts/run/run-ship-preflight.ts +34 -3
  175. package/scripts/run/run-ship-review-steps.ts +1 -1
  176. package/scripts/run/run-ship-review.ts +26 -5
  177. package/scripts/run/run-step-cli.ts +20 -7
  178. package/scripts/run/run-wake-driver.ts +24 -1
  179. package/scripts/run/run-wake-session.ts +2 -1
  180. package/scripts/test/test-declared-logic.ts +1 -1
  181. package/scripts/test/test-related.ts +1 -6
  182. package/scripts/test/test-unit-guard.ts +21 -7
  183. package/scripts/time-runtime/time-batch-guard.ts +52 -16
  184. package/scripts/time-runtime/time-density-hook.ts +20 -2
  185. package/scripts/time-runtime/time-density.ts +4 -3
  186. package/scripts/time-runtime/time-markers.ts +15 -4
  187. package/scripts/time-runtime/time-spans.ts +4 -0
  188. package/scripts/version/npm-registry.ts +11 -1
  189. package/templates/workflows/ci.yml +9 -1
  190. package/templates/workflows/github-release.yml +13 -25
  191. package/.claude/skills/workflow-commands/followup-reference.md +0 -118
  192. package/dist/hooks/chunk-QVLT52OV.js +0 -464
@@ -1,14 +1,13 @@
1
1
  name: GitHub Release
2
2
 
3
3
  # Creates the GitHub Release, with notes generated from `.github/release.yml`, for each tag
4
- # `josh release` cuts (joshuafolkken/kit#3007). A repository with a Publish workflow gets its release
5
- # once that workflow has succeeded for the tag; one without it gets the release when auto-tag.yml
6
- # announces the tag. kit's own release is the `create-release` job of its publish.yml, which runs the
7
- # same `josh release:github`, so this file is distributed but never runs in kit.
4
+ # `josh release` cuts (joshuafolkken/kit#3007). It starts when auto-tag.yml announces the tag; a
5
+ # repository with a Publish workflow gets its release once that workflow has succeeded for the tag,
6
+ # waited for inside `josh release:github`. A `workflow_run` on Publish is not used: a Publish run
7
+ # started by the GITHUB_TOKEN never fires one (joshuafolkken/kit#3138). kit's own release is the
8
+ # `create-release` job of its publish.yml, which runs the same `josh release:github`, so this file is
9
+ # distributed but never runs in kit.
8
10
  on:
9
- workflow_run:
10
- workflows: [Publish]
11
- types: [completed]
12
11
  repository_dispatch:
13
12
  types: [new-tag-created]
14
13
 
@@ -17,12 +16,6 @@ on:
17
16
 
18
17
  jobs:
19
18
  release:
20
- # The Publish run is titled `Publish <tag>`; its head branch is not the tag when it was started by
21
- # repository_dispatch, so the title is where the tag is read from.
22
- if: >-
23
- (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' &&
24
- startsWith(github.event.workflow_run.display_title, 'Publish v')) ||
25
- github.event_name == 'repository_dispatch'
26
19
  runs-on: ubuntu-latest
27
20
  timeout-minutes: 65
28
21
  permissions:
@@ -36,23 +29,17 @@ jobs:
36
29
  fetch-depth: 0
37
30
  persist-credentials: false
38
31
 
39
- # A repository with a Publish workflow is released from the workflow_run above, after its
40
- # publication, so the tag announcement is left alone there and one tag never starts two releases.
32
+ # A repository with a Publish workflow waits for that workflow's `Publish <tag>` run to succeed
33
+ # before releasing; the announcement is the only trigger, so one tag never starts two releases.
41
34
  - name: Select release tag
42
35
  id: release
43
36
  env:
44
- EVENT_NAME: ${{ github.event_name }}
45
- RUN_TITLE: ${{ github.event.workflow_run.display_title }}
46
- RUN_WORKFLOW: ${{ github.event.workflow_run.workflow_id }}
47
37
  DISPATCH_TAG: ${{ github.event.client_payload.tag }}
48
38
  run: |
49
- if [ "$EVENT_NAME" = workflow_run ]; then
50
- echo "tag=${RUN_TITLE#Publish }" >> "$GITHUB_OUTPUT"
51
- echo "workflow=$RUN_WORKFLOW" >> "$GITHUB_OUTPUT"
52
- elif [ -f .github/workflows/publish.yml ]; then
53
- echo "ℹ️ publish.yml releases this tag once it has published, skipping"
54
- else
55
- echo "tag=$DISPATCH_TAG" >> "$GITHUB_OUTPUT"
39
+ echo "tag=$DISPATCH_TAG" >> "$GITHUB_OUTPUT"
40
+ if [ -f .github/workflows/publish.yml ]; then
41
+ echo "workflow=publish.yml" >> "$GITHUB_OUTPUT"
42
+ echo "await=true" >> "$GITHUB_OUTPUT"
56
43
  fi
57
44
 
58
45
  - name: Setup Node.js
@@ -77,3 +64,4 @@ jobs:
77
64
  GH_TOKEN: ${{ github.token }}
78
65
  RELEASE_TAG: ${{ steps.release.outputs.tag }}
79
66
  RELEASE_WORKFLOW: ${{ steps.release.outputs.workflow }}
67
+ RELEASE_AWAIT_PUBLISH: ${{ steps.release.outputs.await }}
@@ -1,118 +0,0 @@
1
- # `pnpm josh followup` — the post-execution reference
2
-
3
- The parts of the `followup` procedure a run reaches **after** it has issued `pnpm josh followup`: how
4
- to read the stage-timing block the command prints, the AI-reviewer comment scan and the config-file
5
- report it runs, and the release ask that closes the completion summary. `followup.md` carries what a
6
- run needs **before** it issues the command and points here for the rest, so the read that lands in the
7
- turn that issues `pnpm josh followup` stays small (joshuafolkken/kit#1905).
8
-
9
- This file is the single source of the sections below; `followup.md`, `fullrun.md`, `backlogrun.md` and
10
- `backlogrun.md` point here rather than restating them.
11
-
12
- ## Reading the stage-timing block
13
-
14
- ### It prints how long each of those stages took
15
-
16
- Since joshuafolkken/kit#1349 the command closes with one row per stage and a total, so "where did
17
- `followup` spend its time" is read rather than guessed. The shape, with illustrative durations:
18
-
19
- ```
20
- followup stage: closes-and-context 2.1 s
21
- followup stage: checks-wait 28.4 s
22
- followup stage: coderabbit-comments 1.3 s
23
- followup stage: ai-review-comments 2.0 s
24
- followup stage: telegram 1.1 s
25
- followup stage: merge 2.6 s
26
- followup stage: completion-and-epic-close 1.4 s
27
- followup stages total: 38.9 s
28
- ```
29
-
30
- - **The block is printed on a failed run too**, up to and including the stage that threw, which is
31
- named `interrupted`. A `followup` that exits non-zero on an AI-review blocker or a red check is the
32
- invocation whose wait was longest, and one that printed nothing would leave the measurement blind to
33
- exactly those. **A short block there is the run stopping early, not the printer breaking**: the
34
- stages after the failure never ran, so they have no duration to report.
35
- - **The `merge` row appears only on a run that merged**, so a `--no-merge` block is a row shorter.
36
- - **Two rows say `and` because the requests behind them go out together** (joshuafolkken/kit#1446).
37
- `closes-and-context` is the former `closes-check` plus `context` — the pull request body, the
38
- repository name, the pull request URL and the issue title, four reads that need nothing from one
39
- another; `completion-and-epic-close` is the former `completion-comment` plus `epic-close`. A lap
40
- records an interval rather than a call, so a batch is one row, and the name is what says which
41
- stages it holds. **The one dependency inside the batch is kept**: with no issue number on the
42
- command line the title read waits for the body that names it, and only that read waits.
43
- **`checks-wait` is untouched**, which is the point — what was overlapped was never a wait.
44
- - **The total is the sum of the stages, not the command's whole wall clock.** What sits outside it is
45
- the tail the workflow script runs afterwards — the review-record clears, the hold
46
- release, the next-issue listing and the version line — so a run-timing reading of the same
47
- span is longer, and that gap is the tail. It is normally a second or two.
48
- - **What was measured is on joshuafolkken/kit#1349**, and cutting any of it is deliberately not this
49
- block's business: the required-check wait and the AI-review scan are the merge gate, and narrowing
50
- either to make a number smaller is the workaround `CLAUDE.md` prohibits.
51
-
52
- A `failure` Telegram is sent **by hand, exactly once**, and only when the agent has finally given up
53
- recovering — never once per retry:
54
-
55
- ```bash
56
- pnpm josh notify --task-type failure --issue-url "<issue-url>" --body "<the reason and what is unresolved>"
57
- ```
58
-
59
- ## AI reviewer comment scan (automatic in `pnpm josh followup`)
60
-
61
- `pnpm josh followup` scans top-level PR comments from AI reviewers (Claude Review, CodeRabbit summary comments) **independently of CI status**. This scan runs after CI is green and after the existing CodeRabbit line-comment check. The goal is to ensure substantive findings posted by AI reviewers _after_ CI goes green are not silently shipped.
62
-
63
- **Temporary (kit#753)**: while CodeRabbit reviews are slow, CodeRabbit is non-blocking end to end — it is excluded from the default required checks (restore via `JOSH_REQUIRED_CHECKS`), `Actionable comments posted: N` is downgraded to an informational log, and unresolved CodeRabbit line comments no longer require an ignore reason. Every skip is printed to the console and appended to the completion Telegram body. Claude Review blockers are unchanged. Revert together with kit#752.
64
-
65
- - Blocker heuristics (conservative, structural — not NLP):
66
- - **Claude Review** (`author.login = claude`): body contains `### Issues`, `### Problem`, `#### Logic bug`, or a numbered finding heading like `### 1. ...`
67
- - **CodeRabbit** (`author.login = coderabbitai` / `coderabbitai[bot]`): body contains `Actionable comments posted: N` with N > 0. Rate-limit notices (`rate limited by coderabbit.ai` / `Rate limit exceeded`) and "No actionable comments" summaries are ignored.
68
- - If blockers exist and **no** ignore reason is supplied: `pnpm josh followup` sends a `confirmation` Telegram and exits non-zero. Fix the findings (or provide an ignore reason) and re-run.
69
- - If blockers exist and `--ai-review-ignore-reason "<reason>"` is supplied: the workflow posts an ignore-reason comment to the PR (mirroring the CodeRabbit ignore-reason flow) and proceeds to completion.
70
- - Acknowledgment-only Claude comments (`All issues resolved ✓`, `Everything else looks good`) do not match the blocker heuristics, so rounds where the AI reviewer explicitly signs off do not trigger a false positive.
71
- - **A comment listing that could not be read is treated exactly like a standing blocker.** A rate limit, expired auth, a dropped connection — every one of them used to arrive as an empty listing, so the gate passed without having read anything (joshuafolkken/kit#973). `confirmation` Telegram, non-zero exit, re-run once the read succeeds. `--ai-review-ignore-reason` still gets past it, because what it means is unchanged: a person has looked — and the run then carries an audit note saying the scan was bypassed.
72
- - **The CodeRabbit line-comment listing is the exception**: unreadable there is reported as an audit note rather than blocking, because kit#753 has CodeRabbit not blocking the merge at all. That reader also answers unreadable when the PR number itself would not resolve, which the top-level scan never has to do. Revert with kit#752.
73
-
74
- ## Config file reporting (inside `pnpm josh followup`)
75
-
76
- **Nothing here asks you to compare anything by eye** (joshuafolkken/kit#1578). `pnpm josh followup` reads the tracked branch diff itself and matches every changed path against everything `josh sync` distributes — the three lists `AI_COPY_FILES`, `AI_COPY_FILE_MAPPINGS`, `AI_COPY_DIRECTORIES`, plus `SYNCED_PATHS`, the destinations `sync` writes directly (`playwright.config.ts`, `eslint.config.js`, the rest).
77
-
78
- **It stops nothing** (joshuafolkken/kit#1592). What it produces is a report: the claimed paths and the list that claimed each one go into **the completion notification and the completion report on the Issue**, and the run continues to the CI wait and the merge exactly as a run that distributes nothing does. A run passing `--no-merge` is not reported on, because nothing has been distributed yet.
79
-
80
- **It was a confirmation stop until joshuafolkken/kit#1592, and measurement is what removed it rather than preference.** In kit the condition is nearly always true, because kit is the distribution source: every change to `CLAUDE.md`, to `prompts/`, to `.claude/skills/` or to the distributed part of `docs/` is by definition a change to a distributed path. Measured on `epicrun #1413`, **two of three children stopped here** — and in both, the distributed file was the one that child's own acceptance criteria had ordered changed. The gate was not catching an unintended edit; it was stopping the work, and ending unattended execution until a person retyped one command.
81
-
82
- **There is deliberately no branch on which repository this is.** Stopping in a consumer project — where editing a distributed file really is the mistake `CLAUDE.md` → "Route distributed-doc / config changes upstream to kit" names, since the next `josh sync` overwrites it silently — while staying quiet in kit was weighed and rejected on joshuafolkken/kit#1592: it needs a distribution-source test this package does not have, no case is recorded of the gate having saved a consumer's work, and the report below reaches a consumer's reader just as well.
83
-
84
- **The matching itself is unchanged, and so is the reason it is mechanical.** The instruction the mechanical read replaced was skipped in two runs out of three on the day it was measured, and one of those two could not have succeeded by eye at all: `AI_COPY_DIRECTORIES` holds directories, so a distributed path such as `.claude/skills/workflow-commands/backlogrun.md` appears in no list textually.
85
-
86
- ```bash
87
- pnpm josh sync:scope # managed | clean, naming which list claimed each path
88
- ```
89
-
90
- - **There is no flag to pass and nothing to approve.** `--managed-config-ignore-reason` is gone with the stop it existed to get past.
91
- - This report is produced independently of AI reviewer comment scanning, which still stops the run on a standing blocker
92
-
93
- ## When `pnpm josh release` runs
94
-
95
- **The release point is a position plus a command's answer, never a judgement** (joshuafolkken/kit#1582). joshuafolkken/kit#1169 took the version off the branch and put it behind one command a person types; nothing said *when* to type it, and because nothing fails when nobody does — CI green, every pull request merged, every Issue closed — 53 merges reached main unreleased and no consumer of this package saw one of them.
96
-
97
- **The position: once per invocation, after the last merge.** Ask when `pnpm josh followup` has merged the last pull request *this invocation* authorized — a lone `fullrun`'s only one, a `backlogrun`'s **last** child, never once per child. In a lane the parent asks it, in the primary checkout, after the last lane is closed. Asked per child it would cut a release in the middle of a batch whose remaining children are still moving main.
98
-
99
- **The answer: `pnpm josh release:scope`.**
100
-
101
- ```bash
102
- pnpm josh release:scope # → required | skip | unknown
103
- pnpm josh release:scope --json # the same answer as one JSON object
104
- ```
105
-
106
- | It answers | What it means | What the run does |
107
- | ---------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------- |
108
- | `required` | main has taken at least one merge since the version last changed | Close the completion report with the release request, naming `pnpm josh release` |
109
- | `skip` | the count is zero — nothing is waiting to ship | Say so in one line and finish |
110
- | `unknown` | the count could not be read | **Report it as `unknown`** — it is never read as `skip` |
111
-
112
- **The threshold is one, and that is deliberate.** joshuafolkken/kit#1582's own complaint is that accumulating makes a single version larger and its contents harder to trace afterwards, so a release per invocation is the cadence rather than a compromise — and it is the cadence that already held while every child bumped, before joshuafolkken/kit#1486 removed that. What keeps it from firing per child is the position above, not a larger number.
113
-
114
- **The run never types `pnpm josh release` itself.** That command opens a pull request of its own, merges it, and starts the tag → publish → `production` chain: outward-facing and effectively irreversible, so it is Tier C (`CLAUDE.md` → "Decision autonomy"). Typing `fullrun` authorizes merging *this Issue's* pull request and nothing past it. On `required` the run reports the request as its closing line and stops there; a person types the command in the primary checkout, on the default branch, with a clean tree.
115
-
116
- **`pnpm josh release --dry-run` was checked first and does not answer this.** It refuses off the default branch and on a dirty working tree, and it counts against `HEAD` rather than `origin/<default>` — and every position above is a feature branch or a lane, which is exactly where it throws. So `release:scope` adds **no counting of its own**: it reads `git_followup_pending.read_pending`, the same fetch-then-count `pnpm josh followup` already uses for the Telegram line, and the two therefore cannot disagree.
117
-
118
- **This section is the single source.** `fullrun.md`, `backlogrun.md` and `backlogrun.md` point here rather than restating it, and `docs/josh-commands-automation.md` → "`josh release:scope`" documents the command itself.
@@ -1,464 +0,0 @@
1
- import {
2
- lane_child_marker
3
- } from "./chunk-MKVGBQPA.js";
4
- import {
5
- time_bundle_call,
6
- time_format,
7
- time_round_trips,
8
- time_spans,
9
- time_transcript_line
10
- } from "./chunk-4JZDP5X5.js";
11
-
12
- // scripts/lane/lane-guard-policy.ts
13
- var DEFAULT_MODE = "refuse";
14
- var OFF_MODE = "off";
15
- var LANE_GUARD_POLICY = [
16
- {
17
- id: "investigation",
18
- mode_in_lane_child: "notice",
19
- 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'
20
- },
21
- {
22
- id: "batching",
23
- mode_in_lane_child: "off",
24
- 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`"
25
- },
26
- {
27
- id: "duplicate-read",
28
- mode_in_lane_child: "notice",
29
- 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"
30
- },
31
- {
32
- id: "rule",
33
- mode_in_lane_child: "refuse",
34
- 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"
35
- }
36
- ];
37
- var KNOWN_GUARD_IDS = LANE_GUARD_POLICY.map((entry) => entry.id);
38
- function entry_for(id) {
39
- return LANE_GUARD_POLICY.find((entry) => entry.id === id.trim());
40
- }
41
- function mode_in_lane_child(id) {
42
- return entry_for(id)?.mode_in_lane_child ?? DEFAULT_MODE;
43
- }
44
- function mode_here(id, directory = process.cwd(), source = process.env) {
45
- const lane_mode = mode_in_lane_child(id);
46
- if (lane_mode === DEFAULT_MODE) return DEFAULT_MODE;
47
- return lane_child_marker.is_child_of(directory, source) ? lane_mode : DEFAULT_MODE;
48
- }
49
- function is_suppressed_here(id, directory = process.cwd(), source = process.env) {
50
- return mode_here(id, directory, source) === OFF_MODE;
51
- }
52
- var lane_guard_policy = {
53
- LANE_GUARD_POLICY,
54
- KNOWN_GUARD_IDS,
55
- entry_for,
56
- mode_in_lane_child,
57
- mode_here,
58
- is_suppressed_here
59
- };
60
-
61
- // scripts/time-runtime/time-agent-bundles.ts
62
- var NONE = 0;
63
- var ONE = 1;
64
- var MIN_GROUP = 2;
65
- function is_launch(span) {
66
- return span.category === time_spans.TOOL_CATEGORY && !span.is_continuation && time_bundle_call.is_launch_tool(span.label);
67
- }
68
- function new_walk() {
69
- return { closed: [], open: NONE, wrote_since: false, turn_id: time_spans.NO_MESSAGE_ID };
70
- }
71
- function close_group(walk) {
72
- if (walk.open >= MIN_GROUP) walk.closed.push(walk.open);
73
- walk.open = NONE;
74
- }
75
- function is_same_turn(walk, span) {
76
- return span.message_id !== time_spans.NO_MESSAGE_ID && span.message_id === walk.turn_id;
77
- }
78
- function open_turn(walk, span) {
79
- if (walk.wrote_since || span.has_prior_reference) close_group(walk);
80
- walk.open += ONE;
81
- walk.turn_id = span.message_id;
82
- walk.wrote_since = false;
83
- }
84
- function step(walk, span) {
85
- if (span.is_writing) {
86
- walk.wrote_since = true;
87
- return;
88
- }
89
- if (!is_launch(span) || is_same_turn(walk, span)) return;
90
- open_turn(walk, span);
91
- }
92
- function agent_bundles(spans) {
93
- const walk = new_walk();
94
- for (const span of time_round_trips.in_time_order(spans)) step(walk, span);
95
- close_group(walk);
96
- return walk.closed;
97
- }
98
- var time_agent_bundles = { agent_bundles };
99
-
100
- // scripts/time-runtime/time-bundles.ts
101
- var { unmeasured_row } = time_format;
102
- var HEADING = "Bundling:";
103
- var SEQUENCE_LABEL = "bundleable sequences";
104
- var RECOVERABLE_LABEL = "recoverable round trips";
105
- var SAVING_LABEL = "recoverable wait";
106
- var BY_TOOL_LABEL = "recoverable by tool";
107
- var AGENT_ROW_LABEL = "Agent";
108
- var ROW_INDENT = " ";
109
- var MIN_SEQUENCE = 2;
110
- var ONE_CALL = 1;
111
- var NONE2 = 0;
112
- var PATH_SEPARATOR = "/";
113
- var FIRST_CALL = 1;
114
- var NO_LABEL = "";
115
- var NO_BUNDLES = {
116
- sequence_count: 0,
117
- longest_sequence: 0,
118
- recoverable_round_trips: 0,
119
- by_tool: [],
120
- unattributed_round_trips: 0,
121
- is_measured: false
122
- };
123
- function contains_segments(whole, part) {
124
- if (whole.startsWith(`${part}${PATH_SEPARATOR}`)) return true;
125
- return whole.endsWith(`${PATH_SEPARATOR}${part}`) || whole.includes(`${PATH_SEPARATOR}${part}${PATH_SEPARATOR}`);
126
- }
127
- function is_related(left, right) {
128
- if (left === right) return true;
129
- return contains_segments(left, right) || contains_segments(right, left);
130
- }
131
- function shares_target(left, right) {
132
- return left.some((one) => right.some((other) => is_related(one, other)));
133
- }
134
- function is_dependent(earlier, later) {
135
- if (earlier.is_writing && later.is_writing) return false;
136
- return shares_target(earlier.targets, later.targets);
137
- }
138
- function conflicts(span, sequence) {
139
- return sequence.some((earlier) => is_dependent(earlier, span));
140
- }
141
- function is_call(span) {
142
- return span.category === time_spans.TOOL_CATEGORY && !span.is_continuation;
143
- }
144
- function is_turn_boundary(span) {
145
- return span.category === time_spans.MODEL_CATEGORY;
146
- }
147
- function new_walk2() {
148
- return { pending: [], sequence: [], closed: [] };
149
- }
150
- function flush(walk) {
151
- if (walk.sequence.length >= MIN_SEQUENCE) walk.closed.push(walk.sequence);
152
- walk.sequence = [];
153
- }
154
- function extend(walk, span) {
155
- if (conflicts(span, walk.sequence)) flush(walk);
156
- walk.sequence.push(span);
157
- }
158
- function close_trip(walk) {
159
- const [only] = walk.pending;
160
- if (walk.pending.length === ONE_CALL && only?.is_bundleable === true) extend(walk, only);
161
- else if (walk.pending.length > NONE2) flush(walk);
162
- walk.pending = [];
163
- }
164
- function is_same_turn2(span, pending) {
165
- const [first] = pending;
166
- if (first === void 0 || span.is_continuation || span.message_id === time_spans.NO_MESSAGE_ID) {
167
- return false;
168
- }
169
- return span.message_id === first.message_id;
170
- }
171
- function step2(walk, span) {
172
- if (is_call(span)) {
173
- walk.pending.push(span);
174
- return;
175
- }
176
- if (is_same_turn2(span, walk.pending)) return;
177
- close_trip(walk);
178
- if (!is_turn_boundary(span)) flush(walk);
179
- }
180
- function longest_of(sizes) {
181
- return sizes.length === NONE2 ? NONE2 : Math.max(...sizes);
182
- }
183
- function recoverable_of(sizes) {
184
- return sizes.reduce((sum, size) => sum + size - ONE_CALL, NONE2);
185
- }
186
- function sizes_of(closed) {
187
- return closed.map((sequence) => sequence.length);
188
- }
189
- function tally_for(tallies, label) {
190
- const found = tallies.get(label);
191
- if (found !== void 0) return found;
192
- const created = { sequence_count: NONE2, recoverable_round_trips: NONE2 };
193
- tallies.set(label, created);
194
- return created;
195
- }
196
- function count_trip(tallies, counted, label) {
197
- const tally = tally_for(tallies, label);
198
- tally.recoverable_round_trips += ONE_CALL;
199
- if (!counted.has(label)) tally.sequence_count += ONE_CALL;
200
- counted.add(label);
201
- }
202
- function tally_sequence(tallies, sequence) {
203
- const counted = /* @__PURE__ */ new Set();
204
- for (const span of sequence.slice(FIRST_CALL)) {
205
- if (span.label !== NO_LABEL) count_trip(tallies, counted, span.label);
206
- }
207
- }
208
- function by_weight(left, right) {
209
- if (left.recoverable_round_trips !== right.recoverable_round_trips) {
210
- return right.recoverable_round_trips - left.recoverable_round_trips;
211
- }
212
- return left.label.localeCompare(right.label);
213
- }
214
- function tool_rows(closed) {
215
- const tallies = /* @__PURE__ */ new Map();
216
- for (const sequence of closed) tally_sequence(tallies, sequence);
217
- return [...tallies].map(([label, tally]) => ({ label, ...tally })).toSorted(by_weight);
218
- }
219
- function attributed_of(rows) {
220
- return rows.reduce((sum, row) => sum + row.recoverable_round_trips, NONE2);
221
- }
222
- function with_agent_row(rows, agent_sizes) {
223
- const recoverable_round_trips = recoverable_of(agent_sizes);
224
- if (recoverable_round_trips === NONE2) return [...rows];
225
- const row = {
226
- label: AGENT_ROW_LABEL,
227
- sequence_count: agent_sizes.length,
228
- recoverable_round_trips
229
- };
230
- return [...rows, row].toSorted(by_weight);
231
- }
232
- function build_bundles(spans) {
233
- const walk = new_walk2();
234
- for (const span of time_round_trips.in_time_order(spans)) step2(walk, span);
235
- close_trip(walk);
236
- flush(walk);
237
- const sizes = sizes_of(walk.closed);
238
- const agent_sizes = time_agent_bundles.agent_bundles(spans);
239
- const by_tool = with_agent_row(tool_rows(walk.closed), agent_sizes);
240
- const recoverable_round_trips = recoverable_of(sizes) + recoverable_of(agent_sizes);
241
- return {
242
- sequence_count: sizes.length + agent_sizes.length,
243
- longest_sequence: Math.max(longest_of(sizes), longest_of(agent_sizes)),
244
- recoverable_round_trips,
245
- by_tool,
246
- unattributed_round_trips: recoverable_round_trips - attributed_of(by_tool),
247
- is_measured: time_spans.has_transcript_data(spans.length)
248
- };
249
- }
250
- function open_sequence(spans) {
251
- const walk = new_walk2();
252
- for (const span of time_round_trips.in_time_order(spans)) step2(walk, span);
253
- return walk.sequence;
254
- }
255
- function sequence_suffix(totals) {
256
- return `longest ${String(totals.longest_sequence)} turn(s)`;
257
- }
258
- function recoverable_suffix(totals, round_trip_count) {
259
- const share = time_format.format_share(totals.recoverable_round_trips, round_trip_count);
260
- return `${share} of ${String(round_trip_count)} round trip(s)`;
261
- }
262
- function saving_line(totals, model_ms_per_round_trip) {
263
- const saved_ms = totals.recoverable_round_trips * model_ms_per_round_trip;
264
- const rate = `at ${time_format.format_seconds(model_ms_per_round_trip)} model time per round trip`;
265
- return time_format.format_row(SAVING_LABEL, saved_ms, rate);
266
- }
267
- function by_tool_suffix(totals) {
268
- const attributed = totals.recoverable_round_trips - totals.unattributed_round_trips;
269
- return `${String(attributed)} of ${String(totals.recoverable_round_trips)} attributed`;
270
- }
271
- function tool_row(row) {
272
- return time_format.format_columns(
273
- `${ROW_INDENT}${row.label}`,
274
- String(row.recoverable_round_trips),
275
- `in ${String(row.sequence_count)} sequence(s)`
276
- );
277
- }
278
- function by_tool_lines(totals) {
279
- const heading = time_format.format_columns(
280
- BY_TOOL_LABEL,
281
- String(totals.by_tool.length),
282
- by_tool_suffix(totals)
283
- );
284
- const rows = totals.by_tool.slice(NONE2, time_format.MAX_ROWS).map((row) => tool_row(row));
285
- return [heading, ...rows, ...time_format.overflow_line(totals.by_tool.length)];
286
- }
287
- function measured_lines(totals, price) {
288
- return [
289
- time_format.format_columns(
290
- SEQUENCE_LABEL,
291
- String(totals.sequence_count),
292
- sequence_suffix(totals)
293
- ),
294
- time_format.format_columns(
295
- RECOVERABLE_LABEL,
296
- String(totals.recoverable_round_trips),
297
- recoverable_suffix(totals, price.round_trip_count)
298
- ),
299
- saving_line(totals, price.model_ms_per_round_trip),
300
- ...by_tool_lines(totals)
301
- ];
302
- }
303
- var LABELS = [SEQUENCE_LABEL, RECOVERABLE_LABEL, SAVING_LABEL, BY_TOOL_LABEL];
304
- function no_divisor_lines() {
305
- return LABELS.map((label) => time_format.format_columns(label, "", time_format.NO_CALLS));
306
- }
307
- function bundle_lines(totals, price) {
308
- const heading = ["", HEADING];
309
- if (!totals.is_measured) return [...heading, ...LABELS.map((label) => unmeasured_row(label))];
310
- if (price.round_trip_count === NONE2) return [...heading, ...no_divisor_lines()];
311
- return [...heading, ...measured_lines(totals, price)];
312
- }
313
- var time_bundles = {
314
- HEADING,
315
- BY_TOOL_LABEL,
316
- NO_BUNDLES,
317
- build_bundles,
318
- bundle_lines,
319
- is_dependent,
320
- open_sequence,
321
- shares_target
322
- };
323
-
324
- // scripts/time-runtime/time-command-key.ts
325
- var UNNAMED_KEY = "";
326
- function command_key(span) {
327
- if (span.label === time_spans.UNKNOWN_TOOL) return UNNAMED_KEY;
328
- return span.josh_command === "" ? span.label : span.josh_command;
329
- }
330
- var time_command_key = { UNNAMED_KEY, command_key };
331
-
332
- // scripts/time-runtime/time-call-identity.ts
333
- var IDENTITY_SEPARATOR = "\0";
334
- function identity_of(call) {
335
- return [
336
- time_command_key.command_key(call),
337
- call.check_key,
338
- [...call.targets].toSorted((left, right) => left.localeCompare(right)).join(",")
339
- ].join(IDENTITY_SEPARATOR);
340
- }
341
- var time_call_identity = { identity_of };
342
-
343
- // scripts/time-runtime/time-batch-guard.ts
344
- var CONSECUTIVE_LIMIT = 3;
345
- var REFIRE_EVERY = CONSECUTIVE_LIMIT;
346
- var ONE_TURN = 1;
347
- var NONE3 = 0;
348
- var NOTICE_REFIRE_EVERY = ONE_TURN;
349
- var NAMED_CANDIDATE_COUNT = 3;
350
- var SEQUENCE_BEFORE_LIMIT = CONSECUTIVE_LIMIT - ONE_TURN;
351
- var REASON = `\u26D4 batching: the last ${String(SEQUENCE_BEFORE_LIMIT)} turns each issued a single tool call, so this one would make ${String(CONSECUTIVE_LIMIT)} in a row. Reissue it in one turn together with the calls meant to follow it that do not need its result. **The criterion is whether this call's input needs another call's result, not what kind of call it is** \u2014 edits are covered exactly as reads are, and it never authorizes weakening a verification gate or a review: fewer turns, never less work. The measured cost and the rejected mechanisms are in \`prompts/collaboration-workflow/turn-batching.md\`. If this turn was already batching, or the call genuinely has nothing to go beside it, reissue it as it was: this fires once per run of single-call turns and cannot repeat on the call in hand.`;
352
- var NOTICE = `\u{1F4A1} batching: the last ${String(SEQUENCE_BEFORE_LIMIT)} turns each issued a single tool call, so this Write makes ${String(CONSECUTIVE_LIMIT)} in a row. This is a notice, not a refusal \u2014 the write proceeds. Where the writes meant to follow it do not need its result, issue them in one turn together. **The criterion is whether a call's input needs another call's result, not what kind of call it is** \u2014 it never authorizes weakening a verification gate or a review: fewer turns, never less work. The measured cost and the rejected mechanisms are in \`prompts/collaboration-workflow/turn-batching.md\`. If this run was already batching, or the write genuinely has nothing to go beside it, carry on: this recurs every ${String(NOTICE_REFIRE_EVERY)} further single-call turn(s).`;
353
- var LANE_NOTICE = `\u{1F4A1} batching: the last ${String(SEQUENCE_BEFORE_LIMIT)} turns each issued a single tool call, so this one makes ${String(CONSECUTIVE_LIMIT)} in a row. This is a notice, not a refusal \u2014 the call proceeds, because a dispatched lane child ends its turn on a denial. Where the calls meant to follow it do not need its result, issue them in one turn together. **The criterion is whether a call's input needs another call's result, not what kind of call it is** \u2014 it never authorizes weakening a verification gate or a review: fewer turns, never less work. The measured cost and the rejected mechanisms are in \`prompts/collaboration-workflow/turn-batching.md\`. If this run was already batching, or the call genuinely has nothing to go beside it, carry on: this recurs every ${String(NOTICE_REFIRE_EVERY)} further single-call turn(s).`;
354
- var WHOLE_FILE_WRITE_TOOL = "Write";
355
- var EDIT_TOOL = "Edit";
356
- function is_guarded_call(call) {
357
- if (call.name === WHOLE_FILE_WRITE_TOOL) return false;
358
- return time_bundle_call.call_facts(call.name, call.input).is_bundleable;
359
- }
360
- function is_notice_call(call) {
361
- return call.name === WHOLE_FILE_WRITE_TOOL;
362
- }
363
- function is_read_only_call(call) {
364
- const facts = time_bundle_call.call_facts(call.name, call.input);
365
- return facts.is_bundleable && !facts.may_write;
366
- }
367
- function depends_on_sequence(sequence, facts) {
368
- return sequence.some((span) => time_bundles.shares_target(span.targets, facts.targets));
369
- }
370
- function sequence_started_ms(sequence) {
371
- return sequence[0]?.ended_ms ?? NONE3;
372
- }
373
- function turns_since(sequence, last_fired_ms) {
374
- return sequence.filter((span) => span.ended_ms > last_fired_ms).length;
375
- }
376
- function is_sequence_at_limit(sequence, facts, last_fired_ms, refire_every) {
377
- if (sequence.length < SEQUENCE_BEFORE_LIMIT || depends_on_sequence(sequence, facts)) return false;
378
- if (last_fired_ms < sequence_started_ms(sequence)) return true;
379
- return turns_since(sequence, last_fired_ms) >= refire_every;
380
- }
381
- function is_at_limit(spans, call, last_fired_ms, refire_every) {
382
- return is_sequence_at_limit(
383
- time_bundles.open_sequence(spans),
384
- time_bundle_call.call_facts(call.name, call.input),
385
- last_fired_ms,
386
- refire_every
387
- );
388
- }
389
- var GUARD_LABEL = time_transcript_line.guard_from_refusal(REASON);
390
- function is_reissued_refusal(spans, call) {
391
- const identity = time_call_identity.identity_of(
392
- time_spans.to_tool_call(call.name, call.input, time_spans.NO_MESSAGE_ID)
393
- );
394
- return spans.some(
395
- (span) => span.refusal_guard === GUARD_LABEL && time_call_identity.identity_of(span) === identity
396
- );
397
- }
398
- function should_block(text, call, refused_at_ms, refire_every = REFIRE_EVERY) {
399
- if (!is_guarded_call(call)) return false;
400
- const { spans } = time_spans.parse_timeline(text);
401
- if (is_reissued_refusal(spans, call)) return false;
402
- return is_at_limit(spans, call, refused_at_ms, refire_every);
403
- }
404
- function should_notify(text, call, notified_at_ms) {
405
- if (!is_notice_call(call)) return false;
406
- const { spans } = time_spans.parse_timeline(text);
407
- return is_at_limit(spans, call, notified_at_ms, NOTICE_REFIRE_EVERY);
408
- }
409
- function describe_span(span) {
410
- const target = span.targets[0] ?? "";
411
- if (target === "" || span.label.includes(target)) return span.label;
412
- return `${span.label} ${target}`;
413
- }
414
- var FOLD_MINIMUM = 2;
415
- function read_targets(sequence) {
416
- const paths = sequence.filter((span) => span.is_bundleable && !span.is_writing).map((span) => span.targets[0] ?? "").filter((path) => path !== "" && time_bundle_call.has_extension(path));
417
- return [...new Set(paths)];
418
- }
419
- function read_fold_directive(sequence) {
420
- const paths = read_targets(sequence);
421
- if (paths.length < FOLD_MINIMUM) return "";
422
- return ` Fold them into one call: \`pnpm josh read:files ${paths.join(" ")}\`.`;
423
- }
424
- function write_targets(sequence) {
425
- const paths = sequence.filter((span) => span.label === EDIT_TOOL).map((span) => span.targets[0] ?? "").filter((path) => path !== "" && time_bundle_call.has_extension(path));
426
- return [...new Set(paths)];
427
- }
428
- function write_fold_directive(sequence) {
429
- const paths = write_targets(sequence);
430
- if (paths.length < FOLD_MINIMUM) return "";
431
- return ` Fold them into one \`pnpm josh edit:files\` call with a plan over: ${paths.join(" ")}.`;
432
- }
433
- function recent_candidates(tail) {
434
- const recent = time_bundles.open_sequence(time_spans.parse_timeline(tail).spans).slice(-NAMED_CANDIDATE_COUNT);
435
- if (recent.length === NONE3) return "";
436
- const named = recent.map((span) => describe_span(span)).join(", ");
437
- const folds = `${read_fold_directive(recent)}${write_fold_directive(recent)}`;
438
- return ` Just issued one per turn, so at least these could have shared a turn: ${named}.${folds}`;
439
- }
440
- var STAMP_PREFIX = "josh-batch-guard-";
441
- var NOTICE_STAMP_PREFIX = "josh-batch-guard-notice-";
442
- var time_batch_guard = {
443
- CONSECUTIVE_LIMIT,
444
- GUARD_LABEL,
445
- LANE_NOTICE,
446
- NOTICE,
447
- NOTICE_REFIRE_EVERY,
448
- NOTICE_STAMP_PREFIX,
449
- REASON,
450
- REFIRE_EVERY,
451
- SEQUENCE_BEFORE_LIMIT,
452
- STAMP_PREFIX,
453
- is_guarded_call,
454
- is_notice_call,
455
- is_read_only_call,
456
- recent_candidates,
457
- should_block,
458
- should_notify
459
- };
460
-
461
- export {
462
- lane_guard_policy,
463
- time_batch_guard
464
- };