@joshuafolkken/kit 1.1136.0 → 1.1166.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/.claude/settings.json +4 -4
  2. package/.claude/skills/epic-commands/SKILL.md +27 -239
  3. package/.claude/skills/epic-commands/epic-bundle.md +115 -0
  4. package/.claude/skills/epic-commands/execution-waves.md +126 -0
  5. package/.claude/skills/workflow-commands/SKILL.md +48 -194
  6. package/.claude/skills/workflow-commands/background-commands.md +1 -1
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +12 -123
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +11 -60
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +10 -8
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -173
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +64 -144
  13. package/.claude/skills/workflow-commands/backlogrun.md +5 -4
  14. package/.claude/skills/workflow-commands/chain-rule.md +3 -2
  15. package/.claude/skills/workflow-commands/delegation.md +2 -2
  16. package/.claude/skills/workflow-commands/entry-sequence.md +62 -0
  17. package/.claude/skills/workflow-commands/followup.md +104 -181
  18. package/.claude/skills/workflow-commands/fullrun-steps.md +8 -9
  19. package/.claude/skills/workflow-commands/fullrun.md +16 -73
  20. package/.claude/skills/workflow-commands/halfrun.md +10 -36
  21. package/.claude/skills/workflow-commands/into-target.md +4 -4
  22. package/.claude/skills/workflow-commands/issue-comments.md +7 -8
  23. package/.claude/skills/workflow-commands/issue-scout.md +6 -6
  24. package/.claude/skills/workflow-commands/kickoff.md +6 -6
  25. package/.claude/skills/workflow-commands/needs-human-review.md +6 -6
  26. package/.claude/skills/workflow-commands/observation-filing.md +21 -172
  27. package/.claude/skills/workflow-commands/observation-ledger.md +150 -0
  28. package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -93
  29. package/.claude/skills/workflow-commands/prerequisite.md +6 -6
  30. package/.claude/skills/workflow-commands/progress-watcher.md +90 -0
  31. package/.claude/skills/workflow-commands/prrun.md +5 -5
  32. package/.claude/skills/workflow-commands/retrospective.md +9 -9
  33. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  34. package/.claude/skills/workflow-commands/target-repository.md +2 -2
  35. package/.claude/skills/workflow-commands/working-tree-hold.md +7 -5
  36. package/.codex/hooks.json +4 -4
  37. package/AGENTS.md +1 -6
  38. package/CLAUDE.md +58 -120
  39. package/GEMINI.md +1 -6
  40. package/dist/CLAUDE.md +58 -120
  41. package/dist/hooks/batch-guard.js +4 -4
  42. package/dist/hooks/{chunk-NFZGFGOR.js → chunk-5KTCMCOT.js} +14 -1
  43. package/dist/hooks/{chunk-LJBLGHEH.js → chunk-A6P7PN7P.js} +4 -6
  44. package/dist/hooks/{chunk-FO2I7UT7.js → chunk-ANJ2WGFD.js} +1 -1
  45. package/dist/hooks/{chunk-GDU26K3J.js → chunk-DYAVAM6S.js} +563 -126
  46. package/dist/hooks/{chunk-RFWWUI5F.js → chunk-ENA7P5FB.js} +3 -3
  47. package/dist/hooks/{chunk-KBXZLMRH.js → chunk-GU5MATVE.js} +213 -65
  48. package/dist/hooks/chunk-GYNGP7HI.js +56 -0
  49. package/dist/hooks/{chunk-HBKZLITF.js → chunk-URJA6XY4.js} +4 -4
  50. package/dist/hooks/{chunk-KOKOAV6D.js → chunk-V2BTOZHT.js} +6 -4
  51. package/dist/hooks/{chunk-JKKTVDAG.js → chunk-ZDZ555YH.js} +100 -20
  52. package/dist/hooks/codex-hook-adapter.js +10 -10
  53. package/dist/hooks/duplicate-read-guard.js +5 -5
  54. package/dist/hooks/format-edited.js +2 -2
  55. package/dist/hooks/inputs.json +193 -192
  56. package/dist/hooks/investigation-guard.js +5 -5
  57. package/dist/hooks/pretool-guard.js +11 -10
  58. package/dist/hooks/stop-guard.js +5 -5
  59. package/dist/josh.js +25 -2
  60. package/dist/version/index.js +5 -1
  61. package/package.json +2 -1
  62. package/prompts/collaboration-workflow/file-edits.md +11 -10
  63. package/prompts/collaboration-workflow/gh-rest.md +2 -4
  64. package/prompts/collaboration-workflow/glossary.md +1 -1
  65. package/prompts/collaboration-workflow/issue-citation.md +5 -9
  66. package/prompts/collaboration-workflow/issue-template.md +40 -115
  67. package/prompts/collaboration-workflow/operating-rules.md +4 -5
  68. package/prompts/collaboration-workflow/output-bounds.md +12 -78
  69. package/prompts/collaboration-workflow/overview.md +7 -13
  70. package/prompts/collaboration-workflow/plan-comment.md +11 -39
  71. package/prompts/collaboration-workflow/principles.md +3 -4
  72. package/prompts/collaboration-workflow/report-format.md +28 -74
  73. package/prompts/collaboration-workflow/residency.md +12 -109
  74. package/prompts/collaboration-workflow/rule-delivery.md +98 -97
  75. package/prompts/collaboration-workflow/shell-body.md +3 -13
  76. package/prompts/collaboration-workflow/turn-batching.md +14 -88
  77. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -14
  78. package/prompts/collaboration-workflow/wip-cap.md +6 -45
  79. package/prompts/collaboration-workflow.md +1 -13
  80. package/prompts/review-rubric.md +8 -71
  81. package/prompts/review.md +25 -55
  82. package/prompts/testing-guide.md +18 -67
  83. package/scripts/agent/agent-role-profile.ts +1 -1
  84. package/scripts/agent/codex-hooks.ts +3 -12
  85. package/scripts/backlog/backlog-budget.ts +1 -1
  86. package/scripts/backlog/backlog-drive-cli.ts +4 -3
  87. package/scripts/backlog/backlog-drive-offer-argv.ts +10 -1
  88. package/scripts/backlog/backlog-offer.ts +6 -6
  89. package/scripts/behavior/behavior-rules.ts +11 -4
  90. package/scripts/delegation/investigation-reads.ts +5 -5
  91. package/scripts/discovery/repo-discovery.ts +21 -7
  92. package/scripts/discovery/repo-origin.ts +26 -0
  93. package/scripts/discovery/repo-party.ts +42 -1
  94. package/scripts/document/backlogrun-parent-read-set.ts +14 -15
  95. package/scripts/document/document-byte-budget.ts +34 -20
  96. package/scripts/document/entry-read-budget.ts +21 -9
  97. package/scripts/document/lane-child-read-set.ts +12 -14
  98. package/scripts/document/read-set-cli.ts +4 -4
  99. package/scripts/document/read-set-point-of-use.ts +10 -2
  100. package/scripts/document/resident-budget.ts +47 -0
  101. package/scripts/epic/epic-shape.ts +10 -13
  102. package/scripts/epic/epic-solo.ts +1 -1
  103. package/scripts/exports/unused-members-cli.ts +7 -3
  104. package/scripts/gh/git-gh-api-path.ts +7 -0
  105. package/scripts/hooks/pretool-guard-cli.ts +2 -2
  106. package/scripts/hooks/pretool-guard.ts +45 -2
  107. package/scripts/hooks/run-hook.sh +41 -0
  108. package/scripts/init/hook-command-rewrite.ts +18 -35
  109. package/scripts/init/hook-launch.ts +21 -32
  110. package/scripts/issue/issue-auto-ok.ts +95 -0
  111. package/scripts/issue/issue-file-cli.ts +53 -7
  112. package/scripts/issue/issue-file.ts +28 -5
  113. package/scripts/issue/issue-labels.ts +12 -12
  114. package/scripts/issue/issue-rank.ts +1 -1
  115. package/scripts/issue/issue-read-cli.ts +3 -3
  116. package/scripts/issue/issue-read.ts +2 -2
  117. package/scripts/issue/issue-scout-cli.ts +2 -2
  118. package/scripts/issue/issue-state-cli.ts +1 -1
  119. package/scripts/issue/issue-state.ts +2 -2
  120. package/scripts/issue/issue-wip.ts +70 -0
  121. package/scripts/josh/hook-decision.ts +17 -0
  122. package/scripts/josh/josh-commands-development.ts +10 -0
  123. package/scripts/josh/josh-commands-issue.ts +2 -2
  124. package/scripts/lines/bytes-command.ts +29 -6
  125. package/scripts/observations/observation-ledger-line.ts +1 -1
  126. package/scripts/release/release-progress.ts +167 -0
  127. package/scripts/release/release-publish.ts +19 -8
  128. package/scripts/release/release-tag.ts +22 -8
  129. package/scripts/rules/decision-oracle-stage.ts +1 -1
  130. package/scripts/rules/decision-oracle.ts +6 -6
  131. package/scripts/rules/delivered-rules.ts +11 -11
  132. package/scripts/rules/direct-pr-create.ts +110 -0
  133. package/scripts/rules/file-body.ts +98 -42
  134. package/scripts/rules/filing-cap.ts +2 -2
  135. package/scripts/rules/filing-offer.ts +1 -1
  136. package/scripts/rules/gh-api.ts +18 -1
  137. package/scripts/rules/implementation-cut.ts +3 -1
  138. package/scripts/rules/lane-background.ts +67 -10
  139. package/scripts/rules/permission-guards.ts +10 -2
  140. package/scripts/rules/stop-rules.ts +4 -4
  141. package/scripts/rules/test-declared-commit.ts +1 -1
  142. package/scripts/rules/third-party-write.ts +56 -12
  143. package/scripts/run/run-cut-handoff.ts +21 -1
  144. package/scripts/run/run-entry-cli.ts +11 -3
  145. package/scripts/run/run-hold-cli.ts +16 -2
  146. package/scripts/run/run-label.ts +50 -0
  147. package/scripts/run/run-merge-steps.ts +8 -3
  148. package/scripts/run/run-merge.ts +1 -1
  149. package/scripts/run/run-prep-cli.ts +6 -2
  150. package/scripts/run/run-prep.ts +17 -0
  151. package/scripts/run/run-ship-preflight.ts +34 -3
  152. package/scripts/run/run-ship-review-steps.ts +1 -1
  153. package/scripts/run/run-ship-review.ts +26 -5
  154. package/scripts/run/run-step-cli.ts +20 -7
  155. package/scripts/run/run-wake-driver.ts +24 -1
  156. package/scripts/run/run-wake-session.ts +2 -1
  157. package/scripts/test/test-declared-logic.ts +1 -1
  158. package/scripts/time-runtime/time-batch-guard.ts +52 -16
  159. package/scripts/time-runtime/time-density-hook.ts +20 -2
  160. package/scripts/time-runtime/time-density.ts +4 -3
  161. package/scripts/time-runtime/time-markers.ts +15 -4
  162. package/scripts/version/npm-registry.ts +11 -1
  163. package/templates/workflows/ci.yml +9 -1
  164. package/.claude/skills/workflow-commands/followup-reference.md +0 -118
  165. package/dist/hooks/chunk-HANSKHES.js +0 -464
@@ -5,24 +5,30 @@ import { repo_party } from '#scripts/discovery/repo-party'
5
5
  import { epic_bundle_cli } from '#scripts/epic/epic-bundle-cli'
6
6
  import { git_gh_command } from '#scripts/gh/git-gh-command'
7
7
  import { git_gh_exec } from '#scripts/gh/git-gh-exec'
8
+ import { git_gh_issue_list, MAX_SCANNED } from '#scripts/gh/git-gh-issue-list'
8
9
  import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
9
10
  import { github_issue_url } from '#scripts/gh/github-issue-url'
10
11
  import { error_text } from '#scripts/lib/error-message'
12
+ import { repository_labels } from '#scripts/repo/repository-labels'
13
+ import { issue_auto_ok } from './issue-auto-ok'
11
14
  import { issue_file, type FileArguments } from './issue-file'
12
15
  import { issue_lint_cli } from './issue-lint-cli'
13
16
  import { issue_scout_cli } from './issue-scout-cli'
17
+ import { issue_wip } from './issue-wip'
14
18
 
15
19
  // `josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>]…
16
- // [--repo <owner/repo>] [--distinct <N,…>]` — file an Issue with every filing step run in order
17
- // (joshuafolkken/kit#2808): the third-party refusal, the body lint, the `## Origin` check for another
18
- // repository, the duplicate scout, the create call carrying every label, and `epic:bundle` after it.
20
+ // [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok]` — file an Issue with every
21
+ // filing step run in order (joshuafolkken/kit#2808): the third-party refusal, the body lint, the
22
+ // `## Origin` check for another repository, the WIP cap count (joshuafolkken/kit#3181), the duplicate
23
+ // scout, the missing workflow labels created (joshuafolkken/kit#3176), the `auto-ok` decision
24
+ // (joshuafolkken/kit#3213), the create call carrying every label, and `epic:bundle` after it.
19
25
  // A direct `gh api …/issues` filing is refused by the `direct-filing` delivered rule and pointed here.
20
26
 
21
27
  const SUCCESS_EXIT_CODE = 0
22
28
  const FAILURE_EXIT_CODE = 1
23
29
  const ARGV_OFFSET = 2
24
30
  const USAGE =
25
- 'Usage: josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <tier-a|split|interrupt|review-cap>] [--label <name>]… [--repo <owner/repo>] [--distinct <N,…>]'
31
+ 'Usage: josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <tier-a|split|interrupt|review-cap>] [--label <name>]… [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok]'
26
32
  const UNKNOWN_REPO_MESSAGE =
27
33
  'Could not read this repository from `git remote`, so the filing has no repository to compare against — check `gh auth status`.'
28
34
  const THIRD_PARTY_MESSAGE =
@@ -68,6 +74,37 @@ function unacknowledged_message(numbers: ReadonlyArray<number>): string {
68
74
  return `✖ duplicate candidate(s) ${listed} not declared separate — read each (\`issue-fold-existing.md\`); fold into a duplicate, or reissue with \`--distinct ${listed}\` when each is a separate deliverable.`
69
75
  }
70
76
 
77
+ // The target's open Issues, pull requests excluded by the listing itself; `undefined` when unreadable.
78
+ async function open_count(target: string): Promise<number | undefined> {
79
+ const outcome = await git_gh_issue_list.issue_list({
80
+ json_fields: 'number',
81
+ limit: MAX_SCANNED,
82
+ repo: target,
83
+ })
84
+
85
+ return outcome.json === undefined ? undefined : issue_wip.count_of(outcome.json)
86
+ }
87
+
88
+ // The count is printed whatever it answers, and the filing is held only past the cap with no
89
+ // exemption declared. An unreadable count warns and goes on: the cap makes growth visible, and a
90
+ // listing outage is not a reason to stop the run a blocked filing exists for.
91
+ async function is_wip_clear(filing: Filing): Promise<boolean> {
92
+ const count = await open_count(filing.target)
93
+
94
+ if (count === undefined) {
95
+ console.error(issue_wip.UNREAD_MESSAGE)
96
+
97
+ return true
98
+ }
99
+
100
+ const verdict = issue_wip.verdict_of(count, filing.args)
101
+
102
+ console.info(issue_wip.count_line(count, filing.target, verdict))
103
+ if (verdict === issue_wip.HELD) console.error(issue_wip.HELD_MESSAGE)
104
+
105
+ return verdict !== issue_wip.HELD
106
+ }
107
+
71
108
  // Set before the scout and left set for `epic:bundle`, so both read the target's backlog.
72
109
  function point_listings_at(filing: Filing): void {
73
110
  if (issue_file.is_same_repository(filing.target, filing.current)) return
@@ -94,12 +131,21 @@ async function is_scout_clear(filing: Filing): Promise<boolean> {
94
131
  }
95
132
 
96
133
  // `undefined` when the create call failed. `gh`'s standard error is captured into the thrown error
97
- // rather than printed, so it is printed here — otherwise the filing fails with no reason given.
134
+ // rather than printed, so it is printed here — otherwise the filing fails with no reason given. A label
135
+ // the create applies is provisioned first, so it never arrives with a generated color.
98
136
  async function create(filing: Filing): Promise<string | undefined> {
137
+ repository_labels.ensure_labels(filing.target)
138
+ const auto_ok = await issue_auto_ok.resolve(
139
+ filing.args.is_auto_ok_opted_out,
140
+ filing.target,
141
+ filing.current,
142
+ )
143
+
144
+ console.info(issue_auto_ok.line_of(auto_ok))
99
145
  const request = git_gh_issue_write.issue_create_request({
100
146
  title: filing.args.title,
101
147
  body: filing.body,
102
- labels: issue_file.labels_of(filing.args, filing.body),
148
+ labels: issue_file.labels_of(filing.args, filing.body, auto_ok.is_applied),
103
149
  repo: filing.target,
104
150
  })
105
151
 
@@ -136,7 +182,7 @@ async function file(filing: Filing): Promise<number> {
136
182
  return FAILURE_EXIT_CODE
137
183
  }
138
184
 
139
- if (!(await is_scout_clear(filing))) return FAILURE_EXIT_CODE
185
+ if (!(await is_wip_clear(filing)) || !(await is_scout_clear(filing))) return FAILURE_EXIT_CODE
140
186
  const url = await create(filing)
141
187
 
142
188
  if (url === undefined) return FAILURE_EXIT_CODE
@@ -4,7 +4,12 @@ import { github_issue_url } from '#scripts/gh/github-issue-url'
4
4
  import { cli_flags } from '#scripts/lib/cli-flags'
5
5
  import { issue_backlinks } from './issue-backlinks'
6
6
  import { issue_classification } from './issue-classification'
7
- import { DEPTH_LABEL_ORDER, FILING_ROUTE_LABELS, has_label_name } from './issue-labels'
7
+ import {
8
+ AUTO_OK_LABEL,
9
+ DEPTH_LABEL_ORDER,
10
+ FILING_ROUTE_LABELS,
11
+ has_label_name,
12
+ } from './issue-labels'
8
13
  import { markdown_section } from './markdown-section'
9
14
 
10
15
  // The decisions behind `josh issue:file` (joshuafolkken/kit#2808), kept apart from the command that
@@ -31,6 +36,10 @@ interface FileArguments {
31
36
  labels: ReadonlyArray<string>
32
37
  repo: string | undefined
33
38
  distinct: ReadonlyArray<number>
39
+ // `--over-cap`: the run is blocked by this filing, so the WIP cap does not hold it (`issue-wip.ts`).
40
+ is_over_cap: boolean
41
+ // `--no-auto-ok`: the Issue needs a person's judgement, so `auto-ok` is not applied (`issue-auto-ok.ts`).
42
+ is_auto_ok_opted_out: boolean
34
43
  }
35
44
 
36
45
  const OPTIONS = {
@@ -40,6 +49,8 @@ const OPTIONS = {
40
49
  label: { type: 'string', multiple: true },
41
50
  repo: { type: 'string' },
42
51
  distinct: { type: 'string', multiple: true },
52
+ 'over-cap': { type: 'boolean' },
53
+ 'no-auto-ok': { type: 'boolean' },
43
54
  } as const
44
55
 
45
56
  // The label a `--depth` / `--route` value names, or `undefined` when it names none — read against the
@@ -101,7 +112,15 @@ function arguments_of(values: ParsedValues, title: string | undefined): FileArgu
101
112
 
102
113
  if (labelled === undefined || !has_text(title) || !has_text(body_file)) return undefined
103
114
 
104
- return { title, body_file, ...labelled, labels: values.label ?? [], repo: values.repo }
115
+ return {
116
+ title,
117
+ body_file,
118
+ ...labelled,
119
+ labels: values.label ?? [],
120
+ repo: values.repo,
121
+ is_over_cap: values['over-cap'] === true,
122
+ is_auto_ok_opted_out: values['no-auto-ok'] === true,
123
+ }
105
124
  }
106
125
 
107
126
  // An unknown flag is `undefined` rather than a throw, so the answer is the usage line, not a stack trace.
@@ -121,12 +140,16 @@ function is_undeclared_classification(label: string, body: string): boolean {
121
140
 
122
141
  // Every label the filing carries: the depth, the route when there is one, the classification labels
123
142
  // the body declares (`issue_classification.labels_for`, which merges case-insensitively), then any
124
- // extra `--label`. One request applies them all, so no label is a separate step to forget.
125
- function labels_of(args: FileArguments, body: string): ReadonlyArray<string> {
143
+ // extra `--label`, then `auto-ok` when the filing opted in (`issue-auto-ok.ts`) and no `--label`
144
+ // already named it. One request applies them all, so no label is a separate step to forget.
145
+ function labels_of(args: FileArguments, body: string, is_auto_ok = false): ReadonlyArray<string> {
126
146
  const extra = args.labels.filter((label) => !is_undeclared_classification(label, body))
127
147
  const declared = [args.depth, ...(args.route === undefined ? [] : [args.route]), ...extra]
148
+ const labels = issue_classification.labels_for(body, declared)
128
149
 
129
- return issue_classification.labels_for(body, declared)
150
+ if (!is_auto_ok || has_label_name(labels, AUTO_OK_LABEL)) return labels
151
+
152
+ return [...labels, AUTO_OK_LABEL]
130
153
  }
131
154
 
132
155
  function is_same_repository(target: string, current: string): boolean {
@@ -16,7 +16,7 @@ const PLANNED_LABEL = 'run:planned'
16
16
  // forever (joshuafolkken/kit#860).
17
17
  const NEEDS_DECISION_LABEL = 'needs-decision'
18
18
  // Marks an issue whose defect reaches the verification path, so it runs with nothing beside it
19
- // (joshuafolkken/kit#2776). A run applies it from the verification-path enumeration in `wip-cap.md`;
19
+ // (joshuafolkken/kit#2776). A run applies it from the verification-path enumeration in `backlogrun-lanes.md`;
20
20
  // `backlog:next` enforces it (`backlog-solo.ts`), so the rule no longer rests on a judgement at
21
21
  // dispatch time.
22
22
  const RUN_SOLO_LABEL = 'run:solo'
@@ -36,11 +36,12 @@ const PRIORITY_HIGH_LABEL = 'priority:high'
36
36
  const AUTO_OK_LABEL = 'auto-ok'
37
37
  // Degrades one issue's run to a `halfrun`-shaped stop: it is implemented and taken through the
38
38
  // verification gate, and then nothing is committed, pushed, opened as a pull request or merged
39
- // (joshuafolkken/kit#1125). **Only a person applies it**, exactly as strongly as `auto-ok` — a mark a
40
- // run could clear for itself is not a mark.
39
+ // (joshuafolkken/kit#1125). **Only a person applies it** — a mark a run could clear for itself is not
40
+ // a mark.
41
41
  //
42
- // It is the opposite of `auto-ok` in what it does and its twin in who may apply it: one widens
43
- // unattended execution past an epic's edge, the other withholds the last step of it. `auto-ok`
42
+ // It is the opposite of `auto-ok` in what it does: one widens unattended execution past an epic's
43
+ // edge, the other withholds the last step of it. Unlike `auto-ok`, which `issue:file` applies by
44
+ // default to work found by opted-in work (joshuafolkken/kit#3213), no run ever applies it. `auto-ok`
44
45
  // answers "may this run at all", this one answers "may its result ship without a person looking".
45
46
  //
46
47
  // **Not `needs-decision`, and the difference is what the two sets below encode.** A parked issue is
@@ -154,7 +155,7 @@ const FILING_ROUTE_LABELS: ReadonlyArray<{
154
155
  //
155
156
  // **The depth is read off the subject, so a run applies it.** Unlike `auto-ok` and
156
157
  // `needs-human-review` this is not a person's judgement about authorization; it is the same reading
157
- // §2i already asks a run to make before it files, and none of the three withholds or widens anything
158
+ // `observation-filing.md` already asks a run to make before it files, and none of the three withholds or widens anything
158
159
  // a run may do.
159
160
  const DEPTH_0_LABEL = 'depth:0'
160
161
  const DEPTH_1_LABEL = 'depth:1'
@@ -171,9 +172,8 @@ const DEPTH_LABEL_ORDER: ReadonlyArray<string> = [DEPTH_0_LABEL, DEPTH_1_LABEL,
171
172
  // description, so provisioning from here is what gives each one a stable color a reader can scan by.
172
173
  // Green, amber and pale blue, so the consumer-facing depth is the one that stands out in a listing.
173
174
  //
174
- // **The provisioning command lives in `observation-filing.md`**, because prose cannot import this module —
175
- // the same split `FILING_ROUTE_LABELS` above lives with. `scripts/rules/issue-depth-label.test.ts` keys
176
- // those three command lines to this array, so a color changed here without the document fails.
175
+ // **`josh issue:file` provisions them** through `repository_labels.ensure_labels` before its create
176
+ // call (joshuafolkken/kit#3176), so no document carries a creation command to keep in step.
177
177
  const DEPTH_LABELS: ReadonlyArray<{
178
178
  name: string
179
179
  color: string
@@ -182,17 +182,17 @@ const DEPTH_LABELS: ReadonlyArray<{
182
182
  {
183
183
  name: DEPTH_0_LABEL,
184
184
  color: '0e8a16',
185
- description: 'Depth 0 — what a consumer of this package touches (SKILL.md §2i)',
185
+ description: 'Depth 0 — what a consumer of this package touches (observation-filing.md)',
186
186
  },
187
187
  {
188
188
  name: DEPTH_1_LABEL,
189
189
  color: 'fbc02d',
190
- description: 'Depth 1 — the run orchestration that executes an Issue (SKILL.md §2i)',
190
+ description: 'Depth 1 — the run orchestration that executes an Issue (observation-filing.md)',
191
191
  },
192
192
  {
193
193
  name: DEPTH_2_LABEL,
194
194
  color: 'c5def5',
195
- description: 'Depth 2 — what measures a run (SKILL.md §2i)',
195
+ description: 'Depth 2 — what measures a run (observation-filing.md)',
196
196
  },
197
197
  ]
198
198
 
@@ -15,7 +15,7 @@ import {
15
15
  //
16
16
  // **`run:solo` alone is not a key.** It means "runs alone", not "runs first": tidying that only touches
17
17
  // the verification path carries it too, and a low-priority one is not owed a place ahead of the queue.
18
- // Only a defect on that path is, because `wip-cap.md` holds a batch until it has merged.
18
+ // Only a defect on that path is, because `backlogrun-lanes.md` holds a batch until it has merged.
19
19
 
20
20
  interface RankRow {
21
21
  labels: ReadonlyArray<string>
@@ -8,7 +8,7 @@ import { issue_report_failures, type ReadFailureKind } from './issue-report-fail
8
8
  // `josh issue:read <N> [<N> ...]` — the body *and* the comments of every issue named, in one call
9
9
  // (joshuafolkken/kit#1715).
10
10
  //
11
- // It replaces the two `gh api` reads `.claude/skills/workflow-commands/SKILL.md` §2g tells an agent
11
+ // It replaces the two `gh api` reads `.claude/skills/workflow-commands/issue-comments.md` tells an agent
12
12
  // to type per issue. Measured over four recorded `backlogrun` parents, `issue bookkeeping` was the
13
13
  // largest single contributor to the parent's turn count — 110 of 414 turns, 26.6% — and one issue
14
14
  // read at a time was its dominant shape: 19 `…/issues/<N>` calls and 5 `…/comments` calls, each its
@@ -23,7 +23,7 @@ import { issue_report_failures, type ReadFailureKind } from './issue-report-fail
23
23
  // prevent.
24
24
  //
25
25
  // **A failed read is never printed as an issue**, and neither is a failed comment listing printed as
26
- // "no comments" — the block says which of the two it holds, because §2g's rule is that the later text
26
+ // "no comments" — the block says which of the two it holds, because `issue-comments.md`'s rule is that the later text
27
27
  // wins, and a comment nobody read cannot win anything.
28
28
 
29
29
  const FAILURE_EXIT_CODE = 1
@@ -88,7 +88,7 @@ async function read_issue(issue_number: string): Promise<NumberResult> {
88
88
 
89
89
  // One issue's block for a caller that wants the text rather than the printing — `run:prep` composes it
90
90
  // with the state block and the dependency-update line. A failed read is returned as its kind, never as
91
- // an empty block, so §2g's "a comment nobody read cannot win anything" holds through the bundle too.
91
+ // an empty block, so `issue-comments.md`'s "a comment nobody read cannot win anything" holds through the bundle too.
92
92
  async function read_block(issue_number: string): Promise<BlockRead> {
93
93
  const result = await read_issue(issue_number)
94
94
 
@@ -6,7 +6,7 @@ import { z } from 'zod'
6
6
  // (joshuafolkken/kit#1715).
7
7
  //
8
8
  // **It exists because reading one issue costs a parent two turns.** `.claude/skills/workflow-commands/SKILL.md`
9
- // §2g requires the body *and* the comments, since a decision recorded after the body was written
9
+ // `issue-comments.md` requires the body *and* the comments, since a decision recorded after the body was written
10
10
  // lives only in a comment; typed by hand that is `gh api repos/{owner}/{repo}/issues/<N>` followed by
11
11
  // `gh api repos/{owner}/{repo}/issues/<N>/comments`. Measured over four recorded `backlogrun`
12
12
  // parents, `issue bookkeeping` was the single largest contributor to the parent's turn count — 110 of
@@ -15,7 +15,7 @@ import { z } from 'zod'
15
15
  // distribution this collapses.
16
16
  //
17
17
  // **The comments are never silently absent.** A listing that could not be read prints a line saying
18
- // so, because a block showing no comment where the read failed is exactly the misread §2g exists to
18
+ // so, because a block showing no comment where the read failed is exactly the misread `issue-comments.md` exists to
19
19
  // prevent: the agreement in force would be taken from a body a comment had already overturned.
20
20
 
21
21
  const ISSUE_LABEL = 'issue: '
@@ -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 []
@@ -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
  ],
@@ -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