@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,3 +1,5 @@
1
+ import { capped_output } from '#scripts/document/capped-print'
2
+
1
3
  // The failure report `issue:read` and `issue:state` share (joshuafolkken/kit#3046). Both read a batch
2
4
  // of numbers, print a block for each one that answered, and name every one that did not; only the
3
5
  // result kind that counts as an answer and what a gap must not be mistaken for differ between them.
@@ -22,10 +24,12 @@ interface FailureTerms<K extends string> {
22
24
  misreading: string
23
25
  }
24
26
 
25
- function print_blocks(blocks: ReadonlyArray<string>, separator: string): void {
27
+ // Under the Bash output cap, or as part files past it — a batch of long threads is the shape that
28
+ // overflowed (joshuafolkken/kit#3143).
29
+ function print_blocks(blocks: ReadonlyArray<string>, separator: string, label: string): void {
26
30
  if (blocks.length === 0) return
27
31
 
28
- console.info(blocks.join(separator))
32
+ capped_output.capped_print(blocks.join(separator), label)
29
33
  }
30
34
 
31
35
  // A number that resolves to nothing is an answer — a typo, or another repository's number quoted in
@@ -107,7 +107,7 @@ function format_duplicate(candidate: DuplicateCandidate): string {
107
107
  const epic = candidate.epic === undefined ? '' : ` (epic #${String(candidate.epic)})`
108
108
  // The reader does two different things with the two states, so the row has to say which it is: an
109
109
  // open candidate means somebody is already tracking this, a closed one means it may already be
110
- // done — and the second is the exit `SKILL.md` → §2g names, not a second filing.
110
+ // done — and the second is the exit `issue-comments.md` names, not a second filing.
111
111
  const closed = candidate.is_closed === true ? ' (closed)' : ''
112
112
 
113
113
  return ` #${String(candidate.number)} ${score} ${candidate.title}${closed}${epic}`
@@ -211,7 +211,7 @@ function named_epics(draft: BacklogIssue, issues: ReadonlyArray<BacklogIssue>):
211
211
  .map((issue) => issue.number)
212
212
  }
213
213
 
214
- // A person naming the epic is the strongest signal there is — it is what §2a's `into <target>` suffix
214
+ // A person naming the epic is the strongest signal there is — it is what `into-target.md`'s `into <target>` suffix
215
215
  // means — so it is reported ahead of whatever the candidate search concluded, with the candidates
216
216
  // still named beside it.
217
217
  function format_named_epics(epics: ReadonlyArray<number>, decision: BundleDecision): Array<string> {
@@ -170,7 +170,7 @@ async function read_all(request: StateRequest): Promise<ReadonlyArray<IssueRepor
170
170
  }
171
171
 
172
172
  // `[]` for a number that produced no state, so one unresolvable number costs the others nothing.
173
- // The heading is withheld for a single number: `.claude/skills/workflow-commands/SKILL.md` §2z and
173
+ // The heading is withheld for a single number: `.claude/skills/workflow-commands/needs-human-review.md` and
174
174
  // `.claude/skills/diag/SKILL.md` read that report's three lines verbatim.
175
175
  function state_blocks(report: IssueReport, should_attribute: boolean): ReadonlyArray<string> {
176
176
  if (report.result.kind !== 'state') return []
@@ -188,7 +188,7 @@ function print_states(reports: ReadonlyArray<IssueReport>): void {
188
188
  const should_attribute = reports.length > 1
189
189
  const blocks = reports.flatMap((report) => state_blocks(report, should_attribute))
190
190
 
191
- issue_report_failures.print_blocks(blocks, BLOCK_SEPARATOR)
191
+ issue_report_failures.print_blocks(blocks, BLOCK_SEPARATOR, 'issue:state')
192
192
  }
193
193
 
194
194
  async function run(argv: ReadonlyArray<string>): Promise<number> {
@@ -88,8 +88,8 @@ function format_issue_state(state: IssueState): string {
88
88
 
89
89
  // The same three lines, under the number they belong to. Kept as a separate function rather than a
90
90
  // flag on the one above, so the single-number report has no branch that could ever prepend a fourth
91
- // line to it: `.claude/skills/workflow-commands/SKILL.md` §2z and `.claude/skills/diag/SKILL.md`
92
- // read those three verbatim, and §2z's `needs-human-review` stop is decided from them.
91
+ // line to it: `.claude/skills/workflow-commands/needs-human-review.md` and `.claude/skills/diag/SKILL.md`
92
+ // read those three verbatim, and `needs-human-review.md`'s `needs-human-review` stop is decided from them.
93
93
  function format_attributed_issue_state(issue_number: string, state: IssueState): string {
94
94
  return `${ISSUE_LABEL}${issue_number}\n${format_issue_state(state)}`
95
95
  }
@@ -0,0 +1,70 @@
1
+ import { delivered_rules } from '#scripts/rules/delivered-rules'
2
+ import { INTERRUPT_ROUTE_LABEL, SPLIT_ROUTE_LABEL, TIER_A_ROUTE_LABEL } from './issue-labels'
3
+
4
+ // The WIP cap at the call that files (joshuafolkken/kit#3181). The count used to be a hand-run
5
+ // `gh api … | wc -l` the agent compared with the cap itself; `josh issue:file` now reads it and asks
6
+ // the exemption question before anything is sent, the same hold `--distinct` places on a duplicate.
7
+ // What stays judgement — whether a filing is exempt — is declared by the route or by `--over-cap`.
8
+
9
+ const WITHIN = 'within'
10
+ const EXEMPT = 'exempt'
11
+ const HELD = 'held' as const
12
+
13
+ type WipVerdict = typeof WITHIN | typeof EXEMPT | typeof HELD
14
+
15
+ // Each route is an exempt category by definition: a prerequisite or upstream defect (`tier-a`) and a
16
+ // split child block the run, and an interrupt is one of the three tests. `review-cap` is the review
17
+ // round cap's branch 2, which `wip-cap.md` counts as discretionary, so it is held like no route at all.
18
+ const EXEMPT_ROUTES: ReadonlySet<string> = new Set([
19
+ TIER_A_ROUTE_LABEL,
20
+ SPLIT_ROUTE_LABEL,
21
+ INTERRUPT_ROUTE_LABEL,
22
+ ])
23
+
24
+ const HELD_MESSAGE =
25
+ '✖ over the WIP cap: close one first; nothing honestly closable means do not file — a discretionary ' +
26
+ 'filing waits. An interrupt meeting one of three tests (a verification answers wrongly, a documented ' +
27
+ 'workflow cannot complete, or data is lost or written outside the repository) files with ' +
28
+ '`--route interrupt`; a filing the run is blocked by reissues with `--over-cap`. Either way, state ' +
29
+ 'the overage (`prompts/collaboration-workflow/wip-cap.md`).'
30
+
31
+ const UNREAD_MESSAGE =
32
+ '⚠ wip: could not count the open Issues — the filing goes on unchecked against the cap ' +
33
+ '(`prompts/collaboration-workflow/wip-cap.md`).'
34
+
35
+ interface WipFiling {
36
+ route: string | undefined
37
+ is_over_cap: boolean
38
+ }
39
+
40
+ function is_exempt(filing: WipFiling): boolean {
41
+ return filing.is_over_cap || (filing.route !== undefined && EXEMPT_ROUTES.has(filing.route))
42
+ }
43
+
44
+ // The cap is "more than `WIP_CAP` open", so exactly the cap is still within it.
45
+ function verdict_of(count: number, filing: WipFiling): WipVerdict {
46
+ if (count <= delivered_rules.WIP_CAP) return WITHIN
47
+
48
+ return is_exempt(filing) ? EXEMPT : HELD
49
+ }
50
+
51
+ function count_line(count: number, repo: string, verdict: WipVerdict): string {
52
+ return `wip: ${String(count)} open in ${repo} · cap ${String(delivered_rules.WIP_CAP)} · ${verdict}`
53
+ }
54
+
55
+ // The listing's JSON is an array of rows; anything else is unreadable rather than zero, because zero
56
+ // would pass every filing as within the cap.
57
+ function count_of(json: string): number | undefined {
58
+ try {
59
+ const parsed: unknown = JSON.parse(json)
60
+
61
+ return Array.isArray(parsed) ? parsed.length : undefined
62
+ } catch {
63
+ return undefined
64
+ }
65
+ }
66
+
67
+ const issue_wip = { HELD, HELD_MESSAGE, UNREAD_MESSAGE, verdict_of, count_line, count_of }
68
+
69
+ export type { WipVerdict }
70
+ export { issue_wip }
@@ -38,6 +38,7 @@ type HookPayload = z.infer<typeof payload_schema>
38
38
 
39
39
  const HOOK_EVENT_NAME = 'PreToolUse'
40
40
  const DENY_DECISION = 'deny'
41
+ const ALLOW_DECISION = 'allow'
41
42
  // No record means nothing has been refused yet, and every span began after instant zero — which is
42
43
  // the answer wanted: the first qualifying accumulation of a run is refused.
43
44
  const NEVER_MS = 0
@@ -84,6 +85,21 @@ function deny_envelope(reason: string): string {
84
85
  })
85
86
  }
86
87
 
88
+ // **A rewrite, not a refusal** (joshuafolkken/kit#3154): the call proceeds with `updated_input` merged
89
+ // over its own. `allow` is the decision the harness applies an `updatedInput` under; a settings `deny`
90
+ // rule still wins over it, so this never widens what the call may do.
91
+ function rewrite_envelope(updated_input: Record<string, unknown>, context: string): string {
92
+ return JSON.stringify({
93
+ systemMessage: context,
94
+ hookSpecificOutput: {
95
+ hookEventName: HOOK_EVENT_NAME,
96
+ permissionDecision: ALLOW_DECISION,
97
+ updatedInput: updated_input,
98
+ additionalContext: context,
99
+ },
100
+ })
101
+ }
102
+
87
103
  // **A notice, not a decision.** It carries no `permissionDecision`, so the call proceeds exactly as it
88
104
  // would have; `additionalContext` is what reaches the model, and `systemMessage` is what the person
89
105
  // watching sees. Both are filled because either channel alone leaves one of the two blind — and the
@@ -470,6 +486,7 @@ const hook_decision = {
470
486
  notice_envelope,
471
487
  parse_hook_payload,
472
488
  report_no_payload,
489
+ rewrite_envelope,
473
490
  stamp_fault,
474
491
  write_decision,
475
492
  write_outcome,
@@ -115,6 +115,16 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
115
115
  // `UserPromptSubmit`, so it must stay eligible for in-process dispatch rather than pay a second
116
116
  // tsx start each turn. It calls `process.loadEnvFile` itself through the shared loader.
117
117
  },
118
+ 'codex:hook-adapter': {
119
+ script: 'scripts/hooks/codex-hook-adapter.ts',
120
+ description:
121
+ 'Codex hook: run the pretool or posttool guard on a Codex payload (reads the tool call on stdin)',
122
+ category: 'Development',
123
+ reference: ['<pretool|posttool>', 'automation', ['none']],
124
+ // The live-source fallback `scripts/hooks/run-hook.sh` derives from the `codex-hook-adapter`
125
+ // bundle name (joshuafolkken/kit#3184). **No `tsx_arguments`**, the same as `pretool:guard`
126
+ // above: it runs before every guarded Codex call.
127
+ },
118
128
  'cspell:dot': {
119
129
  script: 'scripts/lint/cspell-cached.ts',
120
130
  description: 'Run spell check including dotfiles',
@@ -66,10 +66,10 @@ const ISSUE_COMMANDS: Record<string, CommandEntry> = {
66
66
  'issue:file': {
67
67
  script: 'scripts/issue/issue-file-cli.ts',
68
68
  description:
69
- 'File an Issue with every filing step: lint, Origin, duplicate scout, labels, then epic:bundle',
69
+ 'File an Issue with every filing step: lint, Origin, WIP cap, duplicate scout, labels, then epic:bundle',
70
70
  category: 'AI tools',
71
71
  reference: [
72
- '<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,…>]',
72
+ '<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok]',
73
73
  'automation',
74
74
  ['network'],
75
75
  ],
@@ -59,7 +59,7 @@ const LANE_COMMANDS: Record<string, CommandEntry> = {
59
59
  description:
60
60
  'Block until any of the named in-flight lane children completes; prints which one finished',
61
61
  category: 'AI tools',
62
- reference: ['<issue> [<issue>...]', 'automation', ['processes']],
62
+ reference: ['<issue> [<issue>...] [--owner <pid>]', 'automation', ['files', 'processes']],
63
63
  default_script_arguments: ['await'],
64
64
  },
65
65
  'lane:launch': {
@@ -22,6 +22,13 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
22
22
  ['files', 'git', 'network', 'processes'],
23
23
  ],
24
24
  },
25
+ 'dogfood:commit': {
26
+ script: 'scripts/dogfood/dogfood-commit-cli.ts',
27
+ description: "Make the first commit of a dogfood run's own kit-test-* project",
28
+ category: 'Project',
29
+ reference: ['<dir>', 'automation', ['files', 'git']],
30
+ is_kit_only: true,
31
+ },
25
32
  init: {
26
33
  script: 'scripts/init/init.ts',
27
34
  description: 'Initialize config in a new project',
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process'
2
2
  import { setTimeout as sleep } from 'node:timers/promises'
3
+ import { parseArgs } from 'node:util'
3
4
  import { run_liveness } from '#scripts/run/run-liveness'
4
5
  import { lane_child_invocation } from './lane-child-invocation'
5
6
  import { lane_handoff } from './lane-handoff'
@@ -33,6 +34,16 @@ interface AwaitState {
33
34
  is_settled?: boolean
34
35
  }
35
36
 
37
+ interface AwaitArguments {
38
+ issues: ReadonlyArray<string>
39
+ owner: number | undefined
40
+ }
41
+
42
+ interface RawAwaitArguments {
43
+ positionals: Array<string>
44
+ values: { owner?: string | undefined }
45
+ }
46
+
36
47
  // Injectable for testing -- the CLI never overrides these.
37
48
  interface AwaitOptions {
38
49
  is_running?: (issue: string) => boolean
@@ -67,6 +78,39 @@ async function is_settled_default(issue: string): Promise<boolean> {
67
78
  return (await run_liveness.read_child_settled(issue)) === true
68
79
  }
69
80
 
81
+ // An unknown flag or a flag without its value is `undefined`, which is usage.
82
+ function read_await_args(rest: ReadonlyArray<string>): RawAwaitArguments | undefined {
83
+ try {
84
+ return parseArgs({
85
+ args: [...rest],
86
+ allowPositionals: true,
87
+ strict: true,
88
+ options: { owner: { type: 'string' } },
89
+ })
90
+ } catch {
91
+ return undefined
92
+ }
93
+ }
94
+
95
+ // Every positional is an issue number, at least one is given, and `--owner` is a pid.
96
+ function is_valid(raw: RawAwaitArguments): boolean {
97
+ const { owner } = raw.values
98
+ const numbers = owner === undefined ? raw.positionals : [...raw.positionals, owner]
99
+
100
+ return raw.positionals.length > 0 && numbers.every((value) => ISSUE_PATTERN.test(value))
101
+ }
102
+
103
+ // The issues to wait on and the waiting session's pid, or `undefined` for anything else.
104
+ function parse_arguments(rest: ReadonlyArray<string>): AwaitArguments | undefined {
105
+ const raw = read_await_args(rest)
106
+
107
+ if (raw === undefined || !is_valid(raw)) return undefined
108
+
109
+ const { owner } = raw.values
110
+
111
+ return { issues: raw.positionals, owner: owner === undefined ? undefined : Number(owner) }
112
+ }
113
+
70
114
  function make_initial_state(is_settled: boolean): AwaitState {
71
115
  return { appeared: false, disappeared_at: undefined, first_polled_at: undefined, is_settled }
72
116
  }
@@ -187,13 +231,13 @@ async function wait_for_any(
187
231
  }
188
232
 
189
233
  const lane_await = {
190
- ISSUE_PATTERN,
191
234
  NEVER_APPEARED_TIMEOUT_MS,
192
235
  RECONFIRM_MS,
193
236
  check_issue,
194
237
  is_process_running_default,
238
+ parse_arguments,
195
239
  wait_for_any,
196
240
  }
197
241
 
198
- export type { AwaitState, CheckConfig }
242
+ export type { AwaitArguments, AwaitState, CheckConfig }
199
243
  export { lane_await }
@@ -6,6 +6,7 @@ import type { OpenIssueData } from '#scripts/git/schemas'
6
6
  import { issue_cite, type IssueCiter } from '#scripts/issue/issue-cite'
7
7
  import { error_text } from '#scripts/lib/error-message'
8
8
  import { run_carry } from '#scripts/run/run-carry'
9
+ import { run_carry_reclaim } from '#scripts/run/run-carry-reclaim'
9
10
  import { lane_await } from './lane-await'
10
11
  import { lane_close, type CloseOutcome, type SweepOutcome } from './lane-close'
11
12
  import { lane_dispatch, type DispatchOutcome } from './lane-dispatch'
@@ -40,7 +41,7 @@ const USAGE = [
40
41
  ' josh lane:prune',
41
42
  ' josh lane:output <issue-number> [<path>]',
42
43
  ' josh lane:dispatch <issue-number>',
43
- ' josh lane:await <issue-number> ...',
44
+ ' josh lane:await <issue-number> ... [--owner <pid>]',
44
45
  ].join('\n')
45
46
 
46
47
  type Handler = (rest: ReadonlyArray<string>) => Promise<number>
@@ -356,22 +357,19 @@ async function dispatch_command(rest: ReadonlyArray<string>): Promise<number> {
356
357
  return await report_dispatch(await lane_dispatch.dispatch_child(issue), issue)
357
358
  }
358
359
 
359
- // Each valid argument is an issue number; any non-number stops parsing and triggers usage.
360
- function parse_issues(rest: ReadonlyArray<string>): ReadonlyArray<string> | undefined {
361
- if (rest.length === 0) return undefined
362
- const issues = rest.filter((value) => lane_await.ISSUE_PATTERN.test(value))
363
-
364
- return issues.length === rest.length ? issues : undefined
365
- }
366
-
367
360
  // The completed issue number goes to standard output so `N=$(pnpm josh lane:await ...)` captures
368
- // which child finished without parsing stderr.
361
+ // which child finished without parsing stderr. `--owner` reclaims the carry record for a resumed
362
+ // conversation before the wait (`run-carry-reclaim.ts`).
369
363
  async function await_command(rest: ReadonlyArray<string>): Promise<number> {
370
- const issues = parse_issues(rest)
364
+ const parsed = lane_await.parse_arguments(rest)
371
365
 
372
- if (issues === undefined) return report_usage()
366
+ if (parsed === undefined) return report_usage()
367
+
368
+ if (parsed.owner !== undefined) {
369
+ await run_carry_reclaim.reclaim_carry(run_carry.owner_of(parsed.owner))
370
+ }
373
371
 
374
- const completed = await lane_await.wait_for_any(issues)
372
+ const completed = await lane_await.wait_for_any(parsed.issues)
375
373
 
376
374
  console.info(completed)
377
375
 
@@ -4,6 +4,7 @@ import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { document_byte_budget } from '#scripts/document/document-byte-budget'
6
6
  import { entry_read_budget } from '#scripts/document/entry-read-budget'
7
+ import { resident_budget } from '#scripts/document/resident-budget'
7
8
 
8
9
  // The package root, resolved from this file rather than the caller's working directory.
9
10
  const REPO_ROOT = fileURLToPath(new URL('../../', import.meta.url))
@@ -75,14 +76,40 @@ function normalized_path(argument: string): string {
75
76
  return argument.startsWith('./') ? argument.slice('./'.length) : argument
76
77
  }
77
78
 
79
+ function remaining_phrase(remaining: number): string {
80
+ return remaining < 0 ? `over by ${(-remaining).toString()}` : `${remaining.toString()} left`
81
+ }
82
+
83
+ // The resident document is held by a fixed limit rather than a block ratchet, and loosening it is
84
+ // Tier C, so its row names the headroom or the overage but never a value to raise it to
85
+ // (joshuafolkken/kit#3171).
86
+ function resident_row(status: DocumentStatus): string {
87
+ const counts = `${status.current.toString()}/${status.recorded.toString()} bytes`
88
+
89
+ return `${status.path}${ROW_GAP}${counts} · ${remaining_phrase(status.remaining)}`
90
+ }
91
+
92
+ function is_resident(relative_path: string): boolean {
93
+ return resident_budget.limit_bytes_for(relative_path) !== undefined
94
+ }
95
+
96
+ function recorded_for(relative_path: string): number | undefined {
97
+ return (
98
+ resident_budget.limit_bytes_for(relative_path) ??
99
+ document_byte_budget.recorded_bytes_for(relative_path)
100
+ )
101
+ }
102
+
78
103
  function argument_row(argument: string, root: string = REPO_ROOT): string {
79
104
  const relative_path = normalized_path(argument)
80
- const recorded = document_byte_budget.recorded_bytes_for(relative_path)
105
+ const recorded = recorded_for(relative_path)
81
106
 
82
107
  if (recorded === undefined) return `${relative_path}${ROW_GAP}${NOT_BUDGETED}`
83
108
  if (!existsSync(path.join(root, relative_path))) return `${relative_path}${ROW_GAP}${NOT_A_FILE}`
84
109
 
85
- return status_row(status_of(relative_path, recorded, root))
110
+ const status = status_of(relative_path, recorded, root)
111
+
112
+ return is_resident(relative_path) ? resident_row(status) : status_row(status)
86
113
  }
87
114
 
88
115
  // A document is "near its ceiling" once less than this is left before its recorded block ceiling —
@@ -118,10 +145,6 @@ function entry_status_of(entry: string, recorded: number): EntryStatus {
118
145
  return { entry, current, recorded, remaining }
119
146
  }
120
147
 
121
- function remaining_phrase(remaining: number): string {
122
- return remaining < 0 ? `over by ${(-remaining).toString()}` : `${remaining.toString()} left`
123
- }
124
-
125
148
  // The primary budget, one row per entry: the total each workflow entry reads against its recorded
126
149
  // ceiling and the headroom left — printed on every scan, so the main budget is no longer a number the
127
150
  // gate reveals only when it fails (joshuafolkken/kit#2271).
@@ -1,4 +1,4 @@
1
- // The observation ledger's line grammar (`.claude/skills/workflow-commands/observation-filing.md`)
1
+ // The observation ledger's line grammar (`.claude/skills/workflow-commands/observation-ledger.md`)
2
2
  // is fixed — `- k:<slug> | d<n> | <YYYY-MM-DD> | <where> | <what>` — but the path was collected in
3
3
  // `observation-ledger.ts` without a line ever being parsed, so a malformed append rode a flush
4
4
  // through unremarked. This validates a ledger entry line and reports why a broken one is broken
@@ -37,6 +37,7 @@ function read_release_input(environment: Environment): ReleaseInput {
37
37
  start_tag: optional(environment['RELEASE_START_TAG']),
38
38
  workflow: optional(environment['RELEASE_WORKFLOW']),
39
39
  jobs: job_names(environment['RELEASE_JOBS']),
40
+ await_publish: environment['RELEASE_AWAIT_PUBLISH'] === 'true',
40
41
  },
41
42
  }
42
43
  }
@@ -15,7 +15,12 @@ const POLL_INTERVAL_MS = 30_000
15
15
  const MAX_WAIT_ATTEMPTS = 120
16
16
  const RELEASE_SCHEMA = z.object({ tag_name: z.string().min(1) })
17
17
  const NOTES_SCHEMA = z.object({ name: z.string().min(1), body: z.string().min(1) })
18
- const RUN_SCHEMA = z.object({ id: z.number(), display_title: z.string() })
18
+ const RUN_SCHEMA = z.object({
19
+ id: z.number(),
20
+ display_title: z.string(),
21
+ status: z.string().optional(),
22
+ conclusion: z.string().nullable().optional(),
23
+ })
19
24
  const JOB_SCHEMA = z.object({
20
25
  name: z.string(),
21
26
  status: z.string(),
@@ -26,15 +31,19 @@ const JOBS_SCHEMA = z.object({ jobs: z.array(JOB_SCHEMA) })
26
31
 
27
32
  type ReleaseRequest = (url: string, init: RequestInit) => Promise<Response>
28
33
  type Wait = () => Promise<void>
34
+ type PollState = 'ready' | 'failed' | 'pending'
29
35
 
30
36
  // What differs between repositories. `start_tag` is the last tag that predates automatic releases;
31
37
  // without it the latest existing release is the floor. `workflow` names the publish workflow whose
32
38
  // failed run lets a lower tag be skipped, and `jobs` the jobs in it that count (empty: every job).
39
+ // `await_publish` holds the tag's own release until its run in that workflow has succeeded, for a
40
+ // release started beside the publication rather than from inside it.
33
41
  interface ReleaseSettings {
34
42
  repository: string
35
43
  start_tag?: string | undefined
36
44
  workflow?: string | undefined
37
45
  jobs?: ReadonlyArray<string> | undefined
46
+ await_publish?: boolean | undefined
38
47
  }
39
48
 
40
49
  interface PublishOptions extends ReleaseSettings {
@@ -56,7 +65,14 @@ interface Ladder {
56
65
  tags: ReadonlyArray<string>
57
66
  floor: string | undefined
58
67
  fallback?: () => Promise<string>
68
+ budget: Budget
69
+ }
70
+
71
+ // One allowance shared by every wait a release makes: waiting for its own publication and then for
72
+ // lower tags' releases must end inside the job's `timeout-minutes` together, not each on its own.
73
+ interface Budget {
59
74
  wait: Wait
75
+ remaining: number
60
76
  }
61
77
 
62
78
  function request_options(token: string, method: string, body?: object): RequestInit {
@@ -130,13 +146,22 @@ function is_failed_job(job: z.infer<typeof JOB_SCHEMA>, names: ReadonlyArray<str
130
146
  return names.some((name) => job.name === name || job.name.startsWith(`${name} /`))
131
147
  }
132
148
 
149
+ async function publish_run(
150
+ client: Client,
151
+ workflow_name: string,
152
+ tag: string,
153
+ ): Promise<z.infer<typeof RUN_SCHEMA> | undefined> {
154
+ const workflow = encodeURIComponent(workflow_name)
155
+ const runs_response = await api_get(client, `/actions/workflows/${workflow}/runs?per_page=100`)
156
+ const runs = RUNS_SCHEMA.parse(await read_response(runs_response)).workflow_runs
157
+
158
+ return runs.find((item) => item.display_title === `Publish ${tag}`)
159
+ }
160
+
133
161
  async function failed_publication(client: Client, tag: string): Promise<boolean> {
134
162
  if (client.workflow === undefined) return false
135
163
 
136
- const workflow = encodeURIComponent(client.workflow)
137
- const runs_response = await api_get(client, `/actions/workflows/${workflow}/runs?per_page=100`)
138
- const runs = RUNS_SCHEMA.parse(await read_response(runs_response)).workflow_runs
139
- const run = runs.find((item) => item.display_title === `Publish ${tag}`)
164
+ const run = await publish_run(client, client.workflow, tag)
140
165
  if (!run) return false
141
166
 
142
167
  const jobs_response = await api_get(client, `/actions/runs/${String(run.id)}/jobs?per_page=100`)
@@ -149,24 +174,72 @@ async function default_wait(): Promise<void> {
149
174
  await setTimeout(POLL_INTERVAL_MS)
150
175
  }
151
176
 
152
- async function release_state(client: Client, tag: string): Promise<'ready' | 'failed' | 'pending'> {
177
+ async function release_state(client: Client, tag: string): Promise<PollState> {
153
178
  if (await existing_release(client, tag)) return 'ready'
154
179
  if (await failed_publication(client, tag)) return 'failed'
155
180
 
156
181
  return 'pending'
157
182
  }
158
183
 
159
- async function wait_for_release(client: Client, tag: string, wait: Wait): Promise<boolean> {
160
- for (let attempt = 0; attempt < MAX_WAIT_ATTEMPTS; attempt += 1) {
184
+ // A run not listed yet is pending too: the announcement that starts this release also starts the
185
+ // publication, so the release can ask before GitHub has queued that run.
186
+ async function publication_state(
187
+ client: Client,
188
+ workflow: string,
189
+ tag: string,
190
+ ): Promise<PollState> {
191
+ const run = await publish_run(client, workflow, tag)
192
+ if (run?.status !== 'completed') return 'pending'
193
+
194
+ return run.conclusion === 'success' ? 'ready' : 'failed'
195
+ }
196
+
197
+ async function poll(
198
+ read: () => Promise<PollState>,
199
+ budget: Budget,
200
+ subject: string,
201
+ ): Promise<boolean> {
202
+ while (budget.remaining > 0) {
161
203
  // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
162
- const state = await release_state(client, tag)
204
+ const state = await read()
163
205
  if (state !== 'pending') return state === 'ready'
164
206
 
207
+ budget.remaining -= 1
165
208
  // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
166
- await wait()
209
+ await budget.wait()
167
210
  }
168
211
 
169
- throw new Error(`Timed out waiting for previous GitHub Release ${tag}`)
212
+ throw new Error(`Timed out waiting for ${subject}`)
213
+ }
214
+
215
+ async function wait_for_release(client: Client, tag: string, budget: Budget): Promise<boolean> {
216
+ return await poll(
217
+ async () => await release_state(client, tag),
218
+ budget,
219
+ `previous GitHub Release ${tag}`,
220
+ )
221
+ }
222
+
223
+ async function wait_for_publication(client: Client, tag: string, budget: Budget): Promise<boolean> {
224
+ const { workflow } = client
225
+ if (workflow === undefined) throw new Error('Waiting for the publication needs RELEASE_WORKFLOW')
226
+
227
+ return await poll(
228
+ async () => await publication_state(client, workflow, tag),
229
+ budget,
230
+ `Publish ${tag}`,
231
+ )
232
+ }
233
+
234
+ async function is_publication_ready(
235
+ client: Client,
236
+ tag: string,
237
+ options: PublishOptions,
238
+ budget: Budget,
239
+ ): Promise<boolean> {
240
+ if (options.await_publish !== true) return true
241
+
242
+ return await wait_for_publication(client, tag, budget)
170
243
  }
171
244
 
172
245
  async function ladder_end(ladder: Ladder): Promise<string | undefined> {
@@ -184,7 +257,7 @@ async function baseline_tag(
184
257
  if (ladder.floor === undefined) return prior
185
258
  if (!prior) return await ladder_end(ladder)
186
259
 
187
- return (await wait_for_release(client, prior, ladder.wait))
260
+ return (await wait_for_release(client, prior, ladder.budget))
188
261
  ? prior
189
262
  : await baseline_tag(client, prior, ladder)
190
263
  }
@@ -195,16 +268,19 @@ function assert_older(previous: string | undefined, tag: string): void {
195
268
  }
196
269
  }
197
270
 
198
- async function build_ladder(client: Client, options: PublishOptions): Promise<Ladder> {
271
+ async function build_ladder(
272
+ client: Client,
273
+ options: PublishOptions,
274
+ budget: Budget,
275
+ ): Promise<Ladder> {
199
276
  const tags = options.tags ?? []
200
- const wait = options.wait ?? default_wait
201
277
  const { start_tag } = options
202
278
 
203
279
  if (start_tag !== undefined) {
204
- return { tags, wait, floor: start_tag, fallback: async () => await previous_tag(client) }
280
+ return { tags, budget, floor: start_tag, fallback: async () => await previous_tag(client) }
205
281
  }
206
282
 
207
- return { tags, wait, floor: await latest_release(client) }
283
+ return { tags, budget, floor: await latest_release(client) }
208
284
  }
209
285
 
210
286
  async function generate_notes(
@@ -252,6 +328,10 @@ function create_client(request: ReleaseRequest, token: string, settings: Release
252
328
  }
253
329
  }
254
330
 
331
+ function create_budget(options: PublishOptions): Budget {
332
+ return { wait: options.wait ?? default_wait, remaining: MAX_WAIT_ATTEMPTS }
333
+ }
334
+
255
335
  async function publish(
256
336
  request: ReleaseRequest,
257
337
  token: string,
@@ -263,7 +343,13 @@ async function publish(
263
343
  const client = create_client(request, token, options)
264
344
  if (await existing_release(client, tag)) return 'already-published'
265
345
 
266
- const previous = await baseline_tag(client, tag, await build_ladder(client, options))
346
+ const budget = create_budget(options)
347
+
348
+ if (!(await is_publication_ready(client, tag, options, budget))) {
349
+ return `skipped ${tag}: Publish ${tag} did not succeed`
350
+ }
351
+
352
+ const previous = await baseline_tag(client, tag, await build_ladder(client, options, budget))
267
353
 
268
354
  assert_older(previous, tag)
269
355
  await create_release(client, tag, previous)