@joshuafolkken/kit 1.1035.0 → 1.1050.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 (118) hide show
  1. package/.claude/skills/verify-ui/SKILL.md +1 -1
  2. package/.claude/skills/workflow-commands/SKILL.md +2 -2
  3. package/.claude/skills/workflow-commands/background-commands.md +1 -1
  4. package/.claude/skills/workflow-commands/backlogrun-child.md +2 -2
  5. package/.claude/skills/workflow-commands/backlogrun-lanes.md +1 -1
  6. package/.claude/skills/workflow-commands/backlogrun-park.md +6 -0
  7. package/.claude/skills/workflow-commands/backlogrun-progress.md +30 -36
  8. package/.claude/skills/workflow-commands/backlogrun-steps.md +16 -16
  9. package/.claude/skills/workflow-commands/chain-rule.md +2 -2
  10. package/.claude/skills/workflow-commands/followup-reference.md +2 -2
  11. package/.claude/skills/workflow-commands/issue-comments.md +1 -1
  12. package/.claude/skills/workflow-commands/observation-filing.md +41 -86
  13. package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -36
  14. package/.claude/skills/workflow-commands/working-tree-hold.md +2 -2
  15. package/.github/workflows/auto-tag.yml +21 -0
  16. package/.github/workflows/publish.yml +9 -10
  17. package/dist/hooks/batch-guard.js +4 -4
  18. package/dist/hooks/{chunk-HU66PWWL.js → chunk-5BK3K7V4.js} +89 -41
  19. package/dist/hooks/{chunk-GLBDBJOK.js → chunk-7VPDUBRN.js} +49 -151
  20. package/dist/hooks/{chunk-GJT4BNVE.js → chunk-DMQN45FN.js} +3 -3
  21. package/dist/hooks/{chunk-D67KVGGY.js → chunk-EJDX2TOB.js} +13 -3
  22. package/dist/hooks/{chunk-KG6PZD72.js → chunk-GQ7KP45M.js} +2 -2
  23. package/dist/hooks/{chunk-Y27M6A7X.js → chunk-HJIISEAC.js} +4 -4
  24. package/dist/hooks/{chunk-XPLP27WA.js → chunk-NAS4C7GP.js} +4 -26
  25. package/dist/hooks/{chunk-ZXNE6NY3.js → chunk-NJQOBE6L.js} +2 -2
  26. package/dist/hooks/{chunk-EIFCAJRB.js → chunk-RLDX5SOC.js} +1184 -946
  27. package/dist/hooks/{chunk-HOYJU5RM.js → chunk-WQ4OSBSS.js} +10 -3
  28. package/dist/hooks/codex-hook-adapter.js +10 -10
  29. package/dist/hooks/duplicate-read-guard.js +5 -5
  30. package/dist/hooks/format-edited.js +15 -34
  31. package/dist/hooks/investigation-guard.js +5 -5
  32. package/dist/hooks/pretool-guard.js +23 -16
  33. package/dist/hooks/stop-guard.js +13 -188
  34. package/dist/josh.js +4 -106
  35. package/dist/version/index.js +381 -9
  36. package/package.json +6 -6
  37. package/prompts/collaboration-workflow/file-edits.md +6 -53
  38. package/prompts/collaboration-workflow/residency.md +1 -1
  39. package/prompts/collaboration-workflow/rule-delivery.md +4 -35
  40. package/prompts/collaboration-workflow/shell-body.md +7 -9
  41. package/prompts/collaboration-workflow/turn-batching.md +21 -37
  42. package/prompts/collaboration-workflow/wip-cap.md +15 -18
  43. package/prompts/sonar-hotspot-handling.md +1 -1
  44. package/scripts/adopt/adopt-toolkits.ts +2 -0
  45. package/scripts/auto-ok/auto-ok-cli.ts +9 -9
  46. package/scripts/backlog/backlog-next.ts +24 -8
  47. package/scripts/backlog/backlog-plan-cli.ts +5 -1
  48. package/scripts/backlog/backlog-pool.ts +31 -11
  49. package/scripts/backlog/backlog-rank.ts +106 -0
  50. package/scripts/backlog/backlog-waves.ts +42 -29
  51. package/scripts/build/build-hooks.ts +7 -2
  52. package/scripts/delegation/delegation-policy.ts +11 -0
  53. package/scripts/document/document-byte-budget.ts +5 -5
  54. package/scripts/document/entry-read-budget.ts +5 -4
  55. package/scripts/epic/epic-lane-offer.ts +11 -22
  56. package/scripts/epic/epic-rank.ts +37 -0
  57. package/scripts/epic/epic-solo.ts +29 -28
  58. package/scripts/git/git-command.ts +1 -1
  59. package/scripts/git/git-execa-error.ts +6 -1
  60. package/scripts/git/git-next-issues.ts +34 -5
  61. package/scripts/git/git-preflight.ts +22 -4
  62. package/scripts/git/git-push-transport.ts +12 -69
  63. package/scripts/git/git-spawn.ts +79 -3
  64. package/scripts/git/git-ssh-keepalive.ts +74 -0
  65. package/scripts/git/gone-branch.ts +3 -2
  66. package/scripts/git/issue-labels.ts +12 -0
  67. package/scripts/git/issue-rank.ts +89 -0
  68. package/scripts/hooks/format-edited-cli.ts +15 -0
  69. package/scripts/hooks/format-edited-file.ts +2 -9
  70. package/scripts/hooks/pretool-guard-cli.ts +21 -0
  71. package/scripts/hooks/pretool-guard.ts +14 -15
  72. package/scripts/init/start-exec.ts +7 -1
  73. package/scripts/init/start-plan.ts +18 -5
  74. package/scripts/init/start-setup-pr.ts +46 -19
  75. package/scripts/init/start-steps.ts +41 -9
  76. package/scripts/init/start.ts +40 -10
  77. package/scripts/issue/issue-cite.ts +19 -1
  78. package/scripts/josh/josh-command-map.ts +0 -102
  79. package/scripts/josh/josh-commands-development.ts +2 -2
  80. package/scripts/josh/josh-commands-project.ts +1 -1
  81. package/scripts/lane/lane-child-marker.ts +16 -2
  82. package/scripts/lane/lane-cli.ts +32 -14
  83. package/scripts/lane/lane-report.ts +10 -4
  84. package/scripts/propagate/propagate-plan.ts +185 -0
  85. package/scripts/propagate/propagate-run.ts +5 -1
  86. package/scripts/propagate/propagate-steps.ts +16 -1
  87. package/scripts/propagate/propagate-targets.ts +13 -2
  88. package/scripts/propagate/propagate.ts +60 -42
  89. package/scripts/registry-migration/migrate-logic.ts +1 -0
  90. package/scripts/release/github-release.ts +1 -1
  91. package/scripts/review/review-attest.ts +44 -0
  92. package/scripts/review/review-brief-cli.ts +49 -2
  93. package/scripts/review/review-brief.ts +46 -1
  94. package/scripts/review/review-stamps.ts +7 -0
  95. package/scripts/rules/implementation-cut.ts +1 -1
  96. package/scripts/rules/issue-fold.ts +1 -2
  97. package/scripts/rules/lane-carry-conflict.ts +1 -2
  98. package/scripts/rules/lane-park.ts +1 -2
  99. package/scripts/rules/pre-gate-cut.ts +2 -2
  100. package/scripts/run/run-headless.ts +22 -6
  101. package/scripts/run/run-parent-cut-hook.ts +144 -0
  102. package/scripts/run/run-progress-read.ts +2 -1
  103. package/scripts/run/run-progress.ts +21 -8
  104. package/scripts/run/run-ship-cli.ts +6 -74
  105. package/scripts/run/run-ship-preflight.ts +87 -0
  106. package/scripts/run/run-ship-review-steps.ts +5 -26
  107. package/scripts/run/run-ship-review.ts +9 -0
  108. package/scripts/run/run-ship-scoped.ts +44 -0
  109. package/scripts/run/run-ship-stage.ts +5 -1
  110. package/scripts/run/run-ship-steps.ts +105 -0
  111. package/scripts/run/run-ship.ts +2 -0
  112. package/scripts/run/run-wake-cli.ts +2 -2
  113. package/scripts/run/run-wake-describe.ts +19 -1
  114. package/scripts/run/run-watcher-hook.ts +1 -1
  115. package/scripts/time-runtime/time-bundle-call.ts +2 -2
  116. package/scripts/version/npm-registry.ts +97 -0
  117. package/scripts/version/version-commands.ts +5 -4
  118. package/scripts/version/version-remote.ts +26 -12
@@ -7,7 +7,7 @@ from the gate onward. Rationale: `docs/maintainers/pre-gate-cut-rationale.md`
7
7
 
8
8
  This file is the single source of the boundary. `run:cut` takes it, and `run:cut --resume` is the
9
9
  check a fresh process makes at its own entry. **A second boundary is added below** — the
10
- implementation-phase cut (joshuafolkken/kit#1933), at a context threshold _during_ implementation.
10
+ implementation-phase cut, at a context threshold _during_ implementation.
11
11
  Everything through "What is carried" describes the pre-gate cut; the later section states only what
12
12
  differs.
13
13
 
@@ -23,7 +23,7 @@ verification the fresh process runs on the tree.
23
23
 
24
24
  The cut is for a **detached** `fullrun` a lane dispatched; a person's interactive `fullrun` never
25
25
  relaunches itself. A dispatched child is told apart **mechanically, by a mark the dispatch sets** — never
26
- by the model reading the shape of its own prompt (joshuafolkken/kit#1904).
26
+ by the model reading the shape of its own prompt.
27
27
 
28
28
  ### The dispatch mark
29
29
 
@@ -44,16 +44,15 @@ checkout, a cut in a lane.
44
44
 
45
45
  ## Taking the cut
46
46
 
47
- **The order is fixed in the chain** (joshuafolkken/kit#2177): `chain-rule.md` step 1 issues
47
+ **The order is fixed in the chain**: `chain-rule.md` step 1 issues
48
48
  `pnpm josh run:cut <N>` after `pnpm josh main:merge`, before the scoped pair and the gate. **The command
49
- is issued unconditionally**; `run:cut` itself decides whether the cut is warranted
50
- (joshuafolkken/kit#2312), so a short lane with nothing to drop is answered `under-threshold` and walks on
49
+ is issued unconditionally**; `run:cut` itself decides whether the cut is warranted, so a short lane with nothing to drop is answered `under-threshold` and walks on
51
50
  to the gate rather than relaunching.
52
51
 
53
52
  At the pre-gate boundary, issue:
54
53
 
55
54
  ```bash
56
- pnpm josh run:cut <N> # alias: josh rct
55
+ pnpm josh run:cut <N>
57
56
  ```
58
57
 
59
58
  - `cut` (0) — record written and handed to the lane's launch owner (OpenAI supervisor starts the
@@ -72,25 +71,25 @@ the current process to carry the run on itself.
72
71
 
73
72
  ### The gate refuses until the cut has been taken (joshuafolkken/kit#1864)
74
73
 
75
- **This step was carried as prose and fired exactly never.** Across six measured lane children the cut
76
- was taken **0 times**. So `pnpm josh rule:guard` **refuses `pnpm josh gate`** while this checkout is a
77
- lane and no cut record is carried, handing back the command above. With the ordered step above the
78
- refusal is insurance, not the primary trigger. Rationale:
74
+ **This step was carried as prose and fired exactly never.** In measured lane children the cut was taken **0 times**, so
75
+ `pnpm josh rule:guard` **refuses `pnpm josh gate`** while this checkout is a lane and no cut record is
76
+ carried, handing back the command above. Rationale:
79
77
  `docs/maintainers/pre-gate-cut-rationale.md` → "Why the rules are guards, not prose".
80
78
 
81
79
  - **It fires for a marked child and nowhere else**: the working directory is a lane —
82
80
  `<lane root>/<issue number>` — **and** `JOSH_LANE_CHILD` names that same issue. An interactive
83
81
  `fullrun` carries no mark and runs its gate untouched. `run:cut` uses an open-lane lookup instead, so
84
82
  the guard and the command need not agree byte for byte.
85
- - **It fires only when the recent-window context warrants a cut** (joshuafolkken/kit#2312). The guard
83
+ - **It fires only when the recent-window context warrants a cut**. The guard
86
84
  reads the same `cost_cli.session_verdict` `run:cut` reads, so an `under`-threshold lane is let through
87
85
  to the gate rather than refused.
88
86
  - **It is silent once the cut is carried**, so the resumed process goes straight to the gate.
89
- - **It fires once per run.** A refusal that repeated would wedge the runs that obeyed.
87
+ - **It fires once per run.**
90
88
  - **`--resume`, `--end` and `--json` do not count as taking the cut.**
91
89
 
92
90
  The row, its trigger and the enumeration it joins are
93
- `prompts/collaboration-workflow/rule-delivery.md`; `scripts/rules/pre-gate-cut.test.ts` pins it.
91
+ `prompts/collaboration-workflow/rule-delivery.md`. The suites that pin this and the three guards below:
92
+ `docs/maintainers/pre-gate-cut-rationale.md` → "Which suites pin each guard".
94
93
 
95
94
  ## Resuming — the fresh process's entry check
96
95
 
@@ -106,8 +105,7 @@ pnpm josh run:cut --resume <N>
106
105
  - `resume` (0) — a declared pre-gate cut whose tree matches was verified and taken over. **Skip the
107
106
  title, the plan, the fresh hold claim and the implementation**; re-read the issue body and comments
108
107
  for the plan and the recorded decisions, then go straight to `pnpm josh gate`.
109
- - `resume-impl` (0) — like `resume`, but the cut was taken **during** implementation
110
- (joshuafolkken/kit#1933). Skip the title, the plan, the fresh hold claim and the split assessment,
108
+ - `resume-impl` (0) — like `resume`, but the cut was taken **during** implementation. Skip the title, the plan, the fresh hold claim and the split assessment,
111
109
  re-read the plan, then **continue implementation** — do **not** go straight to the gate.
112
110
  - `handed-off` (0) — a successor has already adopted this cut, `is_handed_off` is spent
113
111
  (joshuafolkken/kit#1935). This process was woken **after** its own cut. **End the turn quietly and do
@@ -154,7 +152,7 @@ decisions are read back from the issue. Rationale:
154
152
 
155
153
  ## The implementation-phase cut — a lane child cuts before the gate too
156
154
 
157
- The **implementation-phase cut** (joshuafolkken/kit#1933) caps the context accumulated _during_
155
+ The **implementation-phase cut** caps the context accumulated _during_
158
156
  implementation — the child ends mid-implementation and a fresh one resumes the lane **back into
159
157
  implementation**.
160
158
 
@@ -162,15 +160,15 @@ implementation**.
162
160
 
163
161
  The guard below refuses on the same measurement the parent uses between children —
164
162
  `cost_verdict.per_request_cost`, read through `cost_cli.session_verdict` (the read-only form of
165
- `pnpm josh cost --cut`, joshuafolkken/kit#2312), single-sourced at `backlogrun-progress.md` → "The
163
+ `pnpm josh cost --cut`), single-sourced at `backlogrun-progress.md` → "The
166
164
  hand-off". The parent's seam and the child's `run_cut.IMPLEMENTATION_CONTEXT_THRESHOLD` share the
167
165
  **135_000** `CONTEXT_CUT_THRESHOLD` (aliased so the tests cannot drift). No separate measurement is
168
166
  built for the lane child.
169
167
 
170
168
  ### It is a guard, fired at the edit that crosses the threshold (joshuafolkken/kit#2310)
171
169
 
172
- **This step was carried as prose and fired exactly never** — measured across five lanes, the cut fired
173
- **0 times** while 33.9% of their requests ran past 200,000 tokens. Rationale:
170
+ **This step was carried as prose and fired exactly never** — the cut fired **0 times** while 33.9% of
171
+ lane requests ran past 200,000 tokens. Rationale:
174
172
  `docs/maintainers/pre-gate-cut-rationale.md` → "Why the rules are guards, not prose".
175
173
 
176
174
  So `pnpm josh rule:guard` **refuses an `Edit` / `Write`** while this checkout is a dispatched lane child
@@ -185,7 +183,7 @@ crossing one successor.
185
183
  - **It fires for a marked child, or a run held outside a lane on a measured `over`**
186
184
  (joshuafolkken/kit#2760); a person's tree holds no run. Outside a lane `cut` relaunches nothing: keep
187
185
  the hold, send a `confirmation` Telegram naming `fullrun #<N>`, end the turn.
188
- - **It fires on every threshold crossing, not once per run** (joshuafolkken/kit#2385) — it re-asks on
186
+ - **It fires on every threshold crossing, not once per run** — it re-asks on
189
187
  each over-threshold edit, including after a `busy` / `failed` / `unready` verdict.
190
188
  - **An edit reissued right after a refusal passes** — so a `busy` / `failed` cut cannot wedge the run:
191
189
  the reissue lands inside a short window and goes through, a genuinely new crossing past it refuses
@@ -196,8 +194,8 @@ crossing one successor.
196
194
  - **The verdict read is reused over a short window** — cached per checkout for a few seconds, so a
197
195
  turn's burst of edits shares one read.
198
196
 
199
- `scripts/rules/implementation-cut.ts` implements the trigger, `scripts/rules/implementation-cut.test.ts`
200
- pins it, and the row joins `prompts/collaboration-workflow/rule-delivery.md`. **A child that ended
197
+ `scripts/rules/implementation-cut.ts` implements the trigger, and the row joins
198
+ `prompts/collaboration-workflow/rule-delivery.md`. **A child that ended
201
199
  without cutting is still detected after the fact by `pnpm josh run:ending <N> --output <path>`**
202
200
  (joshuafolkken/kit#2139). **A single check (`lint:related` / `test:related`) is run after a batch of
203
201
  edits, not after each one.**
@@ -206,12 +204,10 @@ edits, not after each one.**
206
204
 
207
205
  **`CONTEXT_CUT_THRESHOLD` (135_000) is one value, and `cost_verdict.per_request_cost` — the one
208
206
  statistic the parent and the lane child both read — averages the most recent `RECENT_REQUEST_WINDOW`
209
- (10) requests, not the whole session** (joshuafolkken/kit#2295). The value is a derived break-even, not a
210
- chosen number (joshuafolkken/kit#2406): `scripts/cost-runtime/context-cut-payback.ts` computes it from a
211
- cut's one-time preamble rewrite (`POST_CUT_CONTEXT`, ~60_000 tokens — a cut carries a request from ~130k
212
- down to ~60k), the cache multipliers in `cost-pricing.ts`, and a 10-request horizon.
207
+ (10) requests, not the whole session**. The value is computed by
208
+ `scripts/cost-runtime/context-cut-payback.ts`, its single source.
213
209
 
214
- **The cut is conditional** (joshuafolkken/kit#2312): taken only when the recent-window per-request cost
210
+ **The cut is conditional**: taken only when the recent-window per-request cost
215
211
  is over `CONTEXT_CUT_THRESHOLD` — the same statistic and threshold the implementation-phase cut reads, so
216
212
  no second threshold exists. Below it `run:cut` answers `under-threshold` and the gate runs uncut; the
217
213
  guard reads the same verdict and stays silent. An **unmeasurable** session is always cut.
@@ -227,8 +223,8 @@ cut is the same pattern one boundary earlier.
227
223
  ## A lane child records its park before it stops
228
224
 
229
225
  **A dispatched lane child records its park on the Issue before it stops for a decision**
230
- (joshuafolkken/kit#2034). A headless child's only route to ask a person is to park the Issue — measured
231
- on #2012 and #2011, whose questions reached only their final message. Rationale:
226
+ (joshuafolkken/kit#2034). A headless child's only route to ask a person is to park the Issue (#2012,
227
+ #2011). Rationale:
232
228
  `docs/maintainers/pre-gate-cut-rationale.md` → "Why the rules are guards, not prose".
233
229
 
234
230
  **The record, before the Telegram, is the park procedure exactly** — `needs-decision` plus a comment
@@ -255,21 +251,22 @@ lane child — the mark `JOSH_LANE_CHILD` names this lane's own issue — handin
255
251
  on the compliant path is safe.
256
252
 
257
253
  This section is the single source of the lane-child park rule; `scripts/rules/lane-park.ts` implements
258
- the trigger, `scripts/rules/lane-park.test.ts` pins it, and the row joins the enumeration in
254
+ the trigger, and the row joins the enumeration in
259
255
  `prompts/collaboration-workflow/rule-delivery.md`.
260
256
 
261
257
  ### The interactive ask is refused one call earlier
262
258
 
263
259
  (joshuafolkken/kit#2201) The harness refuses an interactive ask in a headless session by **ending the
264
- turn**, so the notify guard never fires — #2178's child did exactly that, the failure
265
- joshuafolkken/kit#2034 set out to prevent, one call upstream. So `pnpm josh rule:guard` **refuses
266
- `AskUserQuestion` for a marked lane child**; the deny returns to the model, and the instruction is to
267
- park the question. It fires on **every occurrence** and stays silent for a person working in a lane.
260
+ turn**, so the notify guard never fires (#2178 — the failure joshuafolkken/kit#2034 set out to prevent,
261
+ one call upstream). So `pnpm josh rule:guard` **refuses `AskUserQuestion` for a marked lane child**; the
262
+ deny returns to the model, and the instruction is to park the question. It fires on **every occurrence**
263
+ and stays silent for a person working in a lane. Rationale:
264
+ `docs/maintainers/pre-gate-cut-rationale.md` → "Why the rules are guards, not prose".
268
265
 
269
266
  **A child that slips past it is still recoverable** — `pnpm josh run:ending` lifts the refused ask out
270
267
  of the exit record's `permission_denials` into the `abandoned` verdict's park basis, so the parent puts
271
- it into the park comment. `scripts/rules/lane-interactive-ask.ts` implements the trigger,
272
- `scripts/rules/lane-interactive-ask.test.ts` pins it, and `scripts/agent/interactive-ask.ts` is the
268
+ it into the park comment. `scripts/rules/lane-interactive-ask.ts` implements the trigger, and
269
+ `scripts/agent/interactive-ask.ts` is the
273
270
  shared extractor the guard and `run:ending` both read.
274
271
 
275
272
  This file is the single source of the rule.
@@ -11,9 +11,9 @@ a `new` entry files its Issue**, because a run stopped after the filing has alre
11
11
  artifact it should not have created.
12
12
 
13
13
  ```bash
14
- pnpm josh run:hold <N> # a `#N` entry point ; alias: josh rh
14
+ pnpm josh run:hold <N> # a `#N` entry point
15
15
  pnpm josh run:hold # a `new` entry point, before the issue exists
16
- pnpm josh run:release <N> # that same run releasing its own record ; alias: josh rr
16
+ pnpm josh run:release <N> # that same run releasing its own record
17
17
  pnpm josh run:release # the bare form releases the unnumbered run's own record
18
18
  pnpm josh run:release --force # a record left behind by a run that has ended
19
19
  ```
@@ -9,6 +9,7 @@ jobs:
9
9
  name: Create Git Tag if Version Changed
10
10
  runs-on: ubuntu-latest
11
11
  permissions:
12
+ actions: write
12
13
  contents: write
13
14
  steps:
14
15
  - name: Checkout code
@@ -109,3 +110,23 @@ jobs:
109
110
  with:
110
111
  event-type: new-tag-created
111
112
  client-payload: '{"tag": "${{ steps.create_tag.outputs.tag }}"}'
113
+
114
+ # A publish workflow run on the tag ref records the tag's commit in npm provenance; a
115
+ # repository_dispatch run records the default branch head, which may already be past the tag.
116
+ # A publish.yml that still listens for new-tag-created is left to the dispatch above, so one
117
+ # release never starts two publish runs. This step runs after that dispatch so its failure
118
+ # cannot keep the dispatch from reaching production.yml.
119
+ - name: Start publish workflow on the tag
120
+ if: steps.create_tag.outputs.completed == 'true'
121
+ env:
122
+ GH_TOKEN: ${{ github.token }}
123
+ GH_REPO: ${{ github.repository }}
124
+ TAG: ${{ steps.create_tag.outputs.tag }}
125
+ run: |
126
+ WORKFLOW=.github/workflows/publish.yml
127
+ if [ -f "$WORKFLOW" ] && grep -Eq '^[[:space:]]*workflow_dispatch:' "$WORKFLOW" && ! grep -q 'new-tag-created' "$WORKFLOW"; then
128
+ gh workflow run publish.yml --ref "$TAG"
129
+ echo "✅ Started publish.yml on $TAG"
130
+ else
131
+ echo "ℹ️ publish.yml is not a workflow_dispatch-only publisher, skipping"
132
+ fi
@@ -1,12 +1,15 @@
1
1
  name: Publish
2
- run-name: Publish ${{ github.event.client_payload.tag }}
2
+ run-name: Publish ${{ github.ref_name }}
3
3
 
4
+ # auto-tag.yml starts this workflow with `gh workflow run publish.yml --ref <tag>`, so the run's
5
+ # commit and ref are the release tag's. npm provenance records the run's commit from the OIDC token,
6
+ # and a repository_dispatch run is pinned to the default branch head, which can be newer than the tag.
4
7
  on:
5
- repository_dispatch:
6
- types: [new-tag-created]
8
+ workflow_dispatch:
7
9
 
8
10
  jobs:
9
11
  publish-github:
12
+ if: startsWith(github.ref, 'refs/tags/v')
10
13
  concurrency:
11
14
  group: package-publish
12
15
  queue: max
@@ -17,8 +20,6 @@ jobs:
17
20
  packages: write
18
21
  steps:
19
22
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
20
- with:
21
- ref: ${{ github.event.client_payload.tag }}
22
23
 
23
24
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
24
25
  with:
@@ -55,6 +56,7 @@ jobs:
55
56
  NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56
57
 
57
58
  publish-npm:
59
+ if: startsWith(github.ref, 'refs/tags/v')
58
60
  concurrency:
59
61
  group: package-publish
60
62
  queue: max
@@ -65,8 +67,6 @@ jobs:
65
67
  id-token: write
66
68
  steps:
67
69
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
68
- with:
69
- ref: ${{ github.event.client_payload.tag }}
70
70
 
71
71
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
72
72
  with:
@@ -106,7 +106,7 @@ jobs:
106
106
  contents: write
107
107
  uses: ./.github/workflows/production.yml
108
108
  with:
109
- tag: ${{ github.event.client_payload.tag }}
109
+ tag: ${{ github.ref_name }}
110
110
 
111
111
  create-release:
112
112
  needs: [publish-github, publish-npm, update-production]
@@ -118,7 +118,6 @@ jobs:
118
118
  steps:
119
119
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
120
120
  with:
121
- ref: ${{ github.event.client_payload.tag }}
122
121
  fetch-depth: 0
123
122
 
124
123
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
@@ -143,4 +142,4 @@ jobs:
143
142
  run: pnpm exec tsx scripts/release/github-release-cli.ts
144
143
  env:
145
144
  GH_TOKEN: ${{ github.token }}
146
- RELEASE_TAG: ${{ github.event.client_payload.tag }}
145
+ RELEASE_TAG: ${{ github.ref_name }}
@@ -9,10 +9,10 @@ import {
9
9
  load_environment_file,
10
10
  notice_refusal_path,
11
11
  refusal_path
12
- } from "./chunk-KG6PZD72.js";
13
- import "./chunk-ZXNE6NY3.js";
14
- import "./chunk-HOYJU5RM.js";
15
- import "./chunk-GLBDBJOK.js";
12
+ } from "./chunk-GQ7KP45M.js";
13
+ import "./chunk-NJQOBE6L.js";
14
+ import "./chunk-WQ4OSBSS.js";
15
+ import "./chunk-7VPDUBRN.js";
16
16
  import "./chunk-WPALEIRX.js";
17
17
  import "./chunk-RKUAD3I6.js";
18
18
  export {
@@ -21,6 +21,7 @@ import {
21
21
  git_gh_exec,
22
22
  has_any_label,
23
23
  has_stderr_field,
24
+ issue_cite,
24
25
  lane_background,
25
26
  lane_park,
26
27
  lane_registry,
@@ -31,37 +32,38 @@ import {
31
32
  repo_party,
32
33
  run_carry,
33
34
  run_cut,
35
+ run_headless,
34
36
  run_hold,
35
37
  run_progress_clock,
36
38
  run_tail,
37
39
  run_watcher_guard,
38
40
  shell_segments,
39
41
  tail_commands
40
- } from "./chunk-EIFCAJRB.js";
42
+ } from "./chunk-RLDX5SOC.js";
41
43
  import {
42
44
  find_local_bin_upwards
43
45
  } from "./chunk-3V37IID6.js";
44
46
  import {
45
47
  batch_outcome
46
- } from "./chunk-KG6PZD72.js";
48
+ } from "./chunk-GQ7KP45M.js";
47
49
  import {
48
50
  investigation_refusal
49
- } from "./chunk-GJT4BNVE.js";
51
+ } from "./chunk-DMQN45FN.js";
50
52
  import {
51
53
  duplicate_read_outcome
52
- } from "./chunk-Y27M6A7X.js";
54
+ } from "./chunk-HJIISEAC.js";
53
55
  import {
54
56
  investigation_reads
55
- } from "./chunk-D67KVGGY.js";
57
+ } from "./chunk-EJDX2TOB.js";
56
58
  import {
57
59
  lane_guard_policy,
58
60
  time_batch_guard
59
- } from "./chunk-ZXNE6NY3.js";
61
+ } from "./chunk-NJQOBE6L.js";
60
62
  import {
61
63
  hook_decision,
62
64
  lane_child_marker,
63
65
  run_issue_number
64
- } from "./chunk-HOYJU5RM.js";
66
+ } from "./chunk-WQ4OSBSS.js";
65
67
  import {
66
68
  PLATFORM_TEMP_ROOT,
67
69
  cost_attribute,
@@ -73,11 +75,7 @@ import {
73
75
  lane_paths,
74
76
  stamp_file,
75
77
  time_shell
76
- } from "./chunk-GLBDBJOK.js";
77
-
78
- // scripts/hooks/pretool-guard.ts
79
- import { text } from "node:stream/consumers";
80
- import { fileURLToPath as fileURLToPath3 } from "node:url";
78
+ } from "./chunk-7VPDUBRN.js";
81
79
 
82
80
  // scripts/rules/direct-filing.ts
83
81
  var DIRECT_FILING_REASON = '\u26D4 direct filing: an Issue is filed with `pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>]`, never with `gh api \u2026/issues` or `gh issue create`. The command runs every filing step in order \u2014 the duplicate scout, the body lint and its classification labels, the depth and `route:` labels, the `## Origin` check when filing into another repository, and `epic:bundle` after the create \u2014 so none of them is a step to remember. When the scout prints candidates it holds the filing: read each, then reissue with `--distinct <N,\u2026>` for the ones that are separate deliverables. Reference: `docs/josh-commands.md` \u2192 "`josh issue:file`". **This rule fires on every occurrence, not once per run.**';
@@ -138,17 +136,19 @@ function next_state(previous, key, now_ms) {
138
136
  if (previous?.key === key) return previous;
139
137
  return { key, unchanged_since_ms: now_ms };
140
138
  }
141
- function format_child(child) {
139
+ function format_child(child, cite) {
142
140
  const labels = child.labels.length > 0 ? child.labels.join(",") : "(no labels)";
143
- return `#${child.issue} ${labels} PR:${child.pr_state}`;
141
+ return `${cite(child.issue)} ${labels} PR:${child.pr_state}`;
144
142
  }
145
- function format_children(children) {
143
+ function format_children(children, repo) {
146
144
  if (children.length === 0) return NO_CHILD_YET;
147
- return children.map((child) => format_child(child)).join(" \xB7 ");
145
+ const cite = issue_cite.citer(repo, new Map(children.map((child) => [child.issue, child.title])));
146
+ return children.map((child) => format_child(child, cite)).join(" \xB7 ");
148
147
  }
149
- function format_lanes(lanes) {
148
+ function format_lanes(lanes, repo) {
150
149
  if (lanes.length === 0) return NO_LANES;
151
- return lanes.map((lane) => `${lane.issue}:${lane.state}`).join(" ");
150
+ const cite = issue_cite.citer(repo, /* @__PURE__ */ new Map());
151
+ return lanes.map((lane) => `${cite(lane.issue)}:${lane.state}`).join(" ");
152
152
  }
153
153
  function format_record(record_age_ms) {
154
154
  return record_age_ms === void 0 ? "unread" : `+${format_minutes(record_age_ms)}`;
@@ -173,8 +173,8 @@ function format_line(observations, timing) {
173
173
  observed_at: format_observed_at(timing.now_ms),
174
174
  quiet: format_minutes(timing.now_ms - timing.quiet_since_ms),
175
175
  unchanged: format_minutes(timing.now_ms - timing.unchanged_since_ms),
176
- children: format_children(observations.children),
177
- lanes: format_lanes(observations.lanes),
176
+ children: format_children(observations.children, observations.repo),
177
+ lanes: format_lanes(observations.lanes, observations.repo),
178
178
  load: observations.load_average.toFixed(1),
179
179
  record: format_record(observations.record_age_ms),
180
180
  next: format_next_report(timing.now_ms, timing.interval_ms)
@@ -1182,8 +1182,8 @@ function labels_of(question) {
1182
1182
  }
1183
1183
  function render_question(question) {
1184
1184
  const labels = labels_of(question);
1185
- const text2 = question.question ?? UNKNOWN_QUESTION;
1186
- return labels.length === 0 ? text2 : `${text2} [${labels.join(OPTION_SEPARATOR)}]`;
1185
+ const text = question.question ?? UNKNOWN_QUESTION;
1186
+ return labels.length === 0 ? text : `${text} [${labels.join(OPTION_SEPARATOR)}]`;
1187
1187
  }
1188
1188
  function interactive_input(raw) {
1189
1189
  const denial = DENIAL_SCHEMA.safeParse(raw);
@@ -1860,8 +1860,8 @@ function is_rule_document(file_path) {
1860
1860
  }
1861
1861
  var LINK_TARGET = /\]\([^)]*\)/gu;
1862
1862
  var BARE_URL = /https?:\/\/\S+/gu;
1863
- function prose_of(text2) {
1864
- return text2.replaceAll(LINK_TARGET, "]()").replaceAll(BARE_URL, "");
1863
+ function prose_of(text) {
1864
+ return text.replaceAll(LINK_TARGET, "]()").replaceAll(BARE_URL, "");
1865
1865
  }
1866
1866
  var MIN_APPENDED_PROSE = 120;
1867
1867
  function adds_rule_prose(old_text, new_text) {
@@ -4719,7 +4719,7 @@ function usable_text(value) {
4719
4719
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
4720
4720
  }
4721
4721
  function claude_progress(content) {
4722
- return content.map((block) => TEXT_BLOCK_SCHEMA.safeParse(block)).filter((parsed) => parsed.success).map((parsed) => parsed.data.text).findLast((text2) => text2.trim() !== "");
4722
+ return content.map((block) => TEXT_BLOCK_SCHEMA.safeParse(block)).filter((parsed) => parsed.success).map((parsed) => parsed.data.text).findLast((text) => text.trim() !== "");
4723
4723
  }
4724
4724
  function claude_result(value, state) {
4725
4725
  const result = claude_result_event.decode(value);
@@ -5041,9 +5041,10 @@ function event_column(lane) {
5041
5041
  const state = run_liveness.describe_agent_state(lane.output);
5042
5042
  return state ?? "agent -";
5043
5043
  }
5044
- function describe_lane(lane) {
5044
+ var PLAIN_CITE = issue_cite.citer(void 0, /* @__PURE__ */ new Map());
5045
+ function describe_lane(lane, cite = PLAIN_CITE) {
5045
5046
  return [
5046
- `#${lane.issue}`,
5047
+ cite(lane.issue),
5047
5048
  `seat ${value_or_unknown(lane.seat)}`,
5048
5049
  `dev ${value_or_unknown(lane.development_port)}`,
5049
5050
  `preview ${value_or_unknown(lane.preview_port)}`,
@@ -5055,9 +5056,9 @@ function describe_lane(lane) {
5055
5056
  event_column(lane)
5056
5057
  ].join(COLUMN_SEPARATOR);
5057
5058
  }
5058
- function describe_lanes(lanes) {
5059
+ function describe_lanes(lanes, cite = PLAIN_CITE) {
5059
5060
  if (lanes.length === 0) return NO_LANES2;
5060
- return lanes.map((lane) => describe_lane(lane)).join("\n");
5061
+ return lanes.map((lane) => describe_lane(lane, cite)).join("\n");
5061
5062
  }
5062
5063
  function describe_ports(lane) {
5063
5064
  if (lane.seat === void 0) return "no lane seat could be read";
@@ -5304,7 +5305,7 @@ function to_labels(issue) {
5304
5305
  async function to_child(issue) {
5305
5306
  const number = String(issue.number);
5306
5307
  const child = await run_preflight.read_child_state(number);
5307
- return { issue: number, labels: to_labels(issue), pr_state: child.pr_state };
5308
+ return { issue: number, title: issue.title, labels: to_labels(issue), pr_state: child.pr_state };
5308
5309
  }
5309
5310
  async function read_children(repo) {
5310
5311
  const read2 = await epic_busy.read_repository(repo);
@@ -5349,6 +5350,7 @@ async function read_observations(request) {
5349
5350
  if (children === void 0) return UNREADABLE_READ;
5350
5351
  if (children.length === 0 && !await has_run_started(lanes)) return IDLE_READ;
5351
5352
  const observations = {
5353
+ repo: request.repo,
5352
5354
  children,
5353
5355
  lanes,
5354
5356
  load_average: loadavg()[FIRST_LOAD_AVERAGE] ?? 0,
@@ -5403,7 +5405,58 @@ async function watcher_hook_reason(raw_payload, now_ms = Date.now()) {
5403
5405
  if (transcript === void 0) return void 0;
5404
5406
  return await stale_reason(transcript, now_ms);
5405
5407
  }
5406
- var run_watcher_hook = { SWITCH_ENV_KEY: SWITCH_ENV_KEY2, STAMP_PREFIX: STAMP_PREFIX2, watcher_hook_reason };
5408
+ var run_watcher_hook = { SWITCH_ENV_KEY: SWITCH_ENV_KEY2, STAMP_PREFIX: STAMP_PREFIX2, transcript_of, watcher_hook_reason };
5409
+
5410
+ // scripts/run/run-parent-cut-hook.ts
5411
+ var SWITCH_ENV_KEY3 = "JOSH_PARENT_CUT_GUARD";
5412
+ var STAMP_PREFIX3 = "josh-parent-cut-guard-";
5413
+ var QUIET_WINDOW_MS = 6e5;
5414
+ function current_state3() {
5415
+ return {
5416
+ carry: async () => await run_headless.driving_carry(),
5417
+ verdict: () => implementation_cut_verdict.reused_verdict(() => cost_cli.session_verdict())
5418
+ };
5419
+ }
5420
+ var THRESHOLD_TEXT2 = `${cost_format.format_tokens(CONTEXT_CUT_THRESHOLD)}-token`;
5421
+ var PARENT_CUT_REASON = `\u26D4 backlogrun parent hand-off: this session drives a \`backlogrun\` (it owns the live carry record) and its recent-context cost has crossed the shared ${THRESHOLD_TEXT2} threshold between merges, so every later request re-reads the accumulated context (joshuafolkken/kit#2947). Take the hand-off now rather than at the next merge: follow \`.claude/skills/workflow-commands/backlogrun-progress.md\` \u2192 "The hand-off" from its \`over\` branch \u2014 open no new lane and take no new child, read \`pnpm josh lane:list\`, make sure every lane in flight records an output path, then run \`pnpm josh run:carry --cut --owner "$PPID"\` and, unless it answers \`capped\`, \`pnpm josh run:wake\`, post the progress comment and end the turn. A lane nobody can poll means the cut does not happen; carry on and this asks again later. Reissue this call when the hand-off is done or cannot be taken \u2014 the guard stays quiet for a while after it refuses, so the hand-off calls themselves are not refused.`;
5422
+ async function is_due2(state) {
5423
+ const carry = await state.carry();
5424
+ if (carry === void 0 || run_carry.is_at_cut_cap(carry)) return false;
5425
+ return state.verdict() === cost_verdict.OVER_VERDICT;
5426
+ }
5427
+ function is_quiet(target, now_ms) {
5428
+ const last_ms = hook_decision.create_refusal_stamp(STAMP_PREFIX3).last_ms(target);
5429
+ return last_ms !== hook_decision.NEVER_MS && now_ms - last_ms < QUIET_WINDOW_MS;
5430
+ }
5431
+ async function refusal_for(transcript, now_ms, state) {
5432
+ const stamp = hook_decision.create_refusal_stamp(STAMP_PREFIX3);
5433
+ const target = stamp.path(transcript);
5434
+ if (is_quiet(target, now_ms) || !await is_due2(state)) return void 0;
5435
+ return stamp.record(target, now_ms) ? PARENT_CUT_REASON : void 0;
5436
+ }
5437
+ function is_unit_call(raw_payload) {
5438
+ try {
5439
+ return (hook_decision.parse_hook_payload(raw_payload)?.agent_id ?? "") !== "";
5440
+ } catch {
5441
+ return false;
5442
+ }
5443
+ }
5444
+ function is_exempt2(raw_payload) {
5445
+ return !hook_decision.is_switch_enabled(SWITCH_ENV_KEY3) || is_unit_call(raw_payload);
5446
+ }
5447
+ async function parent_cut_reason(raw_payload, now_ms = Date.now(), state = current_state3()) {
5448
+ if (is_exempt2(raw_payload)) return void 0;
5449
+ const transcript = run_watcher_hook.transcript_of(raw_payload);
5450
+ if (transcript === void 0) return void 0;
5451
+ return await refusal_for(transcript, now_ms, state);
5452
+ }
5453
+ var run_parent_cut_hook = {
5454
+ PARENT_CUT_REASON,
5455
+ QUIET_WINDOW_MS,
5456
+ STAMP_PREFIX: STAMP_PREFIX3,
5457
+ SWITCH_ENV_KEY: SWITCH_ENV_KEY3,
5458
+ parent_cut_reason
5459
+ };
5407
5460
 
5408
5461
  // scripts/hooks/pretool-guard.ts
5409
5462
  function combine_outcomes(batch, extra_reason) {
@@ -5429,24 +5482,19 @@ function pretool_outcome(raw_payload) {
5429
5482
  const combined = combine_outcomes(batch, investigation ?? duplicate_refusal ?? rule);
5430
5483
  return with_duplicate_notice(combined, duplicate.notice);
5431
5484
  }
5485
+ async function async_reason(raw_payload) {
5486
+ const watcher = await run_watcher_hook.watcher_hook_reason(raw_payload);
5487
+ return watcher ?? await run_parent_cut_hook.parent_cut_reason(raw_payload);
5488
+ }
5432
5489
  async function pretool_outcome_async(raw_payload) {
5433
5490
  const base = pretool_outcome(raw_payload);
5434
5491
  if (!is_clear(base)) return base;
5435
- const reason = await run_watcher_hook.watcher_hook_reason(raw_payload);
5492
+ const reason = await async_reason(raw_payload);
5436
5493
  return reason === void 0 ? base : { reason, notice: void 0, fault: void 0 };
5437
5494
  }
5438
5495
  var pretool_guard = { combine_outcomes, pretool_outcome, pretool_outcome_async };
5439
- if (process.argv[1] === fileURLToPath3(import.meta.url)) {
5440
- if (process.stdin.isTTY) hook_decision.report_no_payload("pretool:guard");
5441
- else {
5442
- const raw_payload = await text(process.stdin);
5443
- hook_decision.load_environment_file();
5444
- hook_decision.emit_outcome(await pretool_outcome_async(raw_payload));
5445
- }
5446
- }
5447
5496
 
5448
5497
  export {
5449
- pretool_outcome,
5450
5498
  pretool_outcome_async,
5451
5499
  pretool_guard
5452
5500
  };