@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
@@ -0,0 +1,167 @@
1
+ import path from 'node:path'
2
+ import { git_gh_api_path } from '#scripts/gh/git-gh-api-path'
3
+ import { git_gh_exec } from '#scripts/gh/git-gh-exec'
4
+ import { json_value } from '#scripts/lib/json-value'
5
+ import { poll } from '#scripts/lib/poll'
6
+ import { file_reader } from '#scripts/lib/read-file'
7
+ import { npm_registry } from '#scripts/version/npm-registry'
8
+ import { version_targets } from '#scripts/version/version-targets'
9
+ import { z } from 'zod'
10
+ import { release_tag } from './release-tag'
11
+
12
+ // The stage lines `pnpm josh release` prints, and the two stages it waits for after the tag
13
+ // (joshuafolkken/kit#3193).
14
+ //
15
+ // **The tag starts the distribution, it does not finish it.** `publish.yml` runs off the tag:
16
+ // `publish-npm` puts the version on npm and `create-release` opens the GitHub Release once the
17
+ // publishing jobs pass. The command used to stop at the tag and call the release published, ahead of
18
+ // both. It now waits for each, prints one line per stage reached, and prints the completion line only
19
+ // when every stage has — a stage that times out is named with ❌ and the run exits non-zero.
20
+
21
+ const { PACKAGE_JSON } = version_targets
22
+ const SUCCESS_EXIT_CODE = 0
23
+ const FAILURE_EXIT_CODE = 1
24
+ const NPM_TIMEOUT_ENV = 'JOSH_RELEASE_NPM_TIMEOUT_SECONDS'
25
+ const GITHUB_RELEASE_TIMEOUT_ENV = 'JOSH_RELEASE_GITHUB_RELEASE_TIMEOUT_SECONDS'
26
+ const NPM_PACKAGE_PAGE = 'https://www.npmjs.com/package'
27
+ const HTML_URL_FILTER = '.html_url'
28
+
29
+ // `private: true` is the manifest's own "never published to npm" — `josh release` is distributed to
30
+ // consumers whose packages are not on npm, so the npm stage is skipped for them rather than timed out.
31
+ // Only the npm stage reads the name, so a private manifest needs none — the basic profile's
32
+ // `josh init` writes `{ "private": true }` without one.
33
+ const manifest_schema = z.looseObject({
34
+ name: z.string().optional(),
35
+ private: z.boolean().optional(),
36
+ })
37
+
38
+ type ReleaseManifest = { is_private: true } | { is_private: false; name: string }
39
+
40
+ function parse_manifest(cwd: string): z.infer<typeof manifest_schema> {
41
+ const raw = file_reader.read_optional(path.join(cwd, PACKAGE_JSON)) ?? ''
42
+ const parsed = manifest_schema.safeParse(json_value.parse_or_undefined(raw))
43
+
44
+ return parsed.success ? parsed.data : {}
45
+ }
46
+
47
+ function read_manifest(cwd: string): ReleaseManifest {
48
+ const manifest = parse_manifest(cwd)
49
+
50
+ if (manifest.private === true) return { is_private: true }
51
+ if (manifest.name === undefined) throw new Error(`${PACKAGE_JSON} in ${cwd} has no package name.`)
52
+
53
+ return { is_private: false, name: manifest.name }
54
+ }
55
+
56
+ function pr_opened_line(pr_url: string): string {
57
+ return `📝 Release PR opened: ${pr_url}`
58
+ }
59
+
60
+ function pr_merged_line(pr_url: string): string {
61
+ return `🔀 Release PR merged: ${pr_url}`
62
+ }
63
+
64
+ function npm_package_url(package_name: string, version: string): string {
65
+ return `${NPM_PACKAGE_PAGE}/${package_name}/v/${version}`
66
+ }
67
+
68
+ function npm_line(package_name: string, version: string): string {
69
+ return `📦 Published to npm: ${npm_package_url(package_name, version)}`
70
+ }
71
+
72
+ function github_release_line(release_url: string): string {
73
+ return `📰 GitHub Release created: ${release_url}`
74
+ }
75
+
76
+ function complete_line(version: string): string {
77
+ return `🎉 Release ${release_tag.tag_name(version)} complete`
78
+ }
79
+
80
+ // Like the tag's failure text, these say which stage was not reached rather than guess at why: the
81
+ // command sees only that the thing it waited for never appeared.
82
+ function missed_npm_line(package_name: string, version: string): string {
83
+ return [
84
+ `❌ Not published to npm: ${package_name}@${version} never appeared on the registry.`,
85
+ ` Check the Publish run for ${release_tag.tag_name(version)}: its publish-npm job did not finish.`,
86
+ ].join('\n')
87
+ }
88
+
89
+ function missed_release_line(version: string): string {
90
+ return [
91
+ `❌ No GitHub Release: ${release_tag.tag_name(version)} never got one.`,
92
+ ` Check the Publish run for ${release_tag.tag_name(version)}: its create-release job did not finish.`,
93
+ ].join('\n')
94
+ }
95
+
96
+ // REST answers 404 until the release exists, which `exec_gh_api` throws on — so a failure here is
97
+ // "not yet", the answer the poll asks again on.
98
+ async function read_release_url(version: string): Promise<string | undefined> {
99
+ const tag = release_tag.tag_name(version)
100
+
101
+ try {
102
+ const url = await git_gh_exec.exec_gh_api({
103
+ path: git_gh_api_path.release_by_tag_api_path(tag),
104
+ jq_filter: HTML_URL_FILTER,
105
+ })
106
+
107
+ return url === '' ? undefined : url
108
+ } catch {
109
+ return undefined
110
+ }
111
+ }
112
+
113
+ async function wait_for_npm(manifest: ReleaseManifest, version: string): Promise<boolean> {
114
+ if (manifest.is_private) return true
115
+ const is_published = await poll.poll_until(
116
+ async () => npm_registry.has_public_version(manifest.name, version),
117
+ release_tag.poll_options_for(NPM_TIMEOUT_ENV),
118
+ )
119
+
120
+ console.info(
121
+ is_published ? npm_line(manifest.name, version) : missed_npm_line(manifest.name, version),
122
+ )
123
+
124
+ return is_published
125
+ }
126
+
127
+ async function wait_for_github_release(version: string): Promise<boolean> {
128
+ // `poll_until` answers only whether it succeeded, so the URL the last read found is kept here.
129
+ const found: { url: string | undefined } = { url: undefined }
130
+
131
+ await poll.poll_until(async () => {
132
+ found.url = await read_release_url(version)
133
+
134
+ return found.url !== undefined
135
+ }, release_tag.poll_options_for(GITHUB_RELEASE_TIMEOUT_ENV))
136
+
137
+ console.info(
138
+ found.url === undefined ? missed_release_line(version) : github_release_line(found.url),
139
+ )
140
+
141
+ return found.url !== undefined
142
+ }
143
+
144
+ // Runs in the release work tree, whose manifest names the package being released. The stages run in
145
+ // `publish.yml`'s order, and a stage that is not reached ends the run: what follows it is gated on it.
146
+ async function wait_for_distribution(version: string): Promise<number> {
147
+ const manifest = read_manifest(process.cwd())
148
+
149
+ if (!(await wait_for_npm(manifest, version))) return FAILURE_EXIT_CODE
150
+ if (!(await wait_for_github_release(version))) return FAILURE_EXIT_CODE
151
+
152
+ console.info(complete_line(version))
153
+
154
+ return SUCCESS_EXIT_CODE
155
+ }
156
+
157
+ const release_progress = {
158
+ npm_package_url,
159
+ pr_merged_line,
160
+ pr_opened_line,
161
+ read_manifest,
162
+ wait_for_distribution,
163
+ GITHUB_RELEASE_TIMEOUT_ENV,
164
+ NPM_TIMEOUT_ENV,
165
+ }
166
+
167
+ export { release_progress }
@@ -6,11 +6,13 @@ import { repository_lock } from '#scripts/git/repository-lock'
6
6
  import { write_version } from '#scripts/version/bump-version'
7
7
  import { version_targets } from '#scripts/version/version-targets'
8
8
  import type { ReleasePlan } from './release-plan'
9
+ import { release_progress } from './release-progress'
9
10
  import { release_tag } from './release-tag'
10
11
  import { release_worktree } from './release-worktree'
11
12
 
12
- // The write half of `pnpm josh release`: the branch, the commit, the pull request, the merge and the
13
- // wait for the tag (joshuafolkken/kit#1169).
13
+ // The write half of `pnpm josh release`: the branch, the commit, the pull request, the merge, the
14
+ // wait for the tag (joshuafolkken/kit#1169) and the watch on what the tag publishes
15
+ // (`release-progress.ts`, joshuafolkken/kit#3193).
14
16
  //
15
17
  // **The merge goes through the same gate every other pull request does.** `wait_for_pr_success` is
16
18
  // the merge gate `pnpm josh followup` waits on, and it is called here rather than reimplemented —
@@ -104,7 +106,7 @@ async function clear_the_way(branch_name: string): Promise<void> {
104
106
  // before that creation, because `worktree_add` makes the branch local and `branch_exists` would then
105
107
  // report every release as already taken. This runs after `process.chdir` into the work tree, so the
106
108
  // version write and the git commands all act on it rather than on the root.
107
- async function commit_release(plan: ReleasePlan): Promise<void> {
109
+ async function commit_release(plan: ReleasePlan): Promise<string> {
108
110
  write_version(plan.next_version)
109
111
  await git_command.add_path(PACKAGE_JSON)
110
112
  await git_command.commit(commit_message(plan.next_version))
@@ -115,18 +117,27 @@ async function commit_release(plan: ReleasePlan): Promise<void> {
115
117
  IGNORE_FOR_RELEASE_LABEL,
116
118
  )
117
119
 
118
- console.info(pr_url)
120
+ console.info(release_progress.pr_opened_line(pr_url))
121
+
122
+ return pr_url
119
123
  }
120
124
 
121
- async function merge_and_tag(plan: ReleasePlan, branch_name: string): Promise<number> {
125
+ // No tag means nothing downstream ran, so the npm and GitHub Release watches are not started.
126
+ async function merge_and_tag(
127
+ plan: ReleasePlan,
128
+ branch_name: string,
129
+ pr_url: string,
130
+ ): Promise<number> {
122
131
  await git_pr_checks.wait_for_pr_success(branch_name)
123
132
  await git_gh_command.pr_merge(branch_name)
133
+ console.info(release_progress.pr_merged_line(pr_url))
124
134
 
125
135
  const is_tagged = await release_tag.wait_for_tag(plan.next_version)
126
136
 
127
137
  console.info(release_tag.format_result(plan.next_version, is_tagged))
138
+ if (!is_tagged) return FAILURE_EXIT_CODE
128
139
 
129
- return is_tagged ? SUCCESS_EXIT_CODE : FAILURE_EXIT_CODE
140
+ return await release_progress.wait_for_distribution(plan.next_version)
130
141
  }
131
142
 
132
143
  // The whole release happens inside a work tree cut for it, so the root checkout is never touched: the
@@ -143,9 +154,9 @@ async function publish_locked(plan: ReleasePlan): Promise<number> {
143
154
 
144
155
  try {
145
156
  process.chdir(directory)
146
- await commit_release(plan)
157
+ const pr_url = await commit_release(plan)
147
158
 
148
- return await merge_and_tag(plan, branch_name)
159
+ return await merge_and_tag(plan, branch_name, pr_url)
149
160
  } finally {
150
161
  process.chdir(previous_cwd)
151
162
  await release_worktree.remove(directory, branch_name)
@@ -49,27 +49,40 @@ async function tag_exists(version: string): Promise<boolean> {
49
49
  return status === HTTP_OK
50
50
  }
51
51
 
52
- async function wait_for_tag(
53
- version: string,
52
+ // The poll budget for one watched stage, read from that stage's environment variable. Every stage
53
+ // `josh release` waits on — the tag, npm and the GitHub Release — is tuned the same way
54
+ // (joshuafolkken/kit#3193).
55
+ function poll_options_for(
56
+ timeout_environment: string,
54
57
  sleeper?: (duration_ms: number) => Promise<void>,
55
- ): Promise<boolean> {
56
- const timeout_seconds = configured_timeout_seconds(process.env[TIMEOUT_ENV])
58
+ ): PollOptions {
59
+ const timeout_seconds = configured_timeout_seconds(process.env[timeout_environment])
60
+
57
61
  // Spread rather than a conditional call: `exactOptionalPropertyTypes` refuses an explicit
58
62
  // `sleeper: undefined`, and writing the call twice to satisfy it would duplicate the predicate.
59
- const options: PollOptions = {
63
+ return {
60
64
  attempts: attempts_for(timeout_seconds),
61
65
  interval_ms: TAG_POLL_INTERVAL_MS,
62
66
  ...(sleeper !== undefined && { sleeper }),
63
67
  }
68
+ }
64
69
 
65
- return await poll.poll_until(async () => await tag_exists(version), options)
70
+ async function wait_for_tag(
71
+ version: string,
72
+ sleeper?: (duration_ms: number) => Promise<void>,
73
+ ): Promise<boolean> {
74
+ return await poll.poll_until(
75
+ async () => await tag_exists(version),
76
+ poll_options_for(TIMEOUT_ENV, sleeper),
77
+ )
66
78
  }
67
79
 
68
80
  // **The failure text says what did not happen rather than what went wrong**, because the command
69
81
  // cannot know which link broke — only that the tag it was waiting for is not there, and that nothing
70
- // downstream of the tag can have run.
82
+ // downstream of the tag can have run. **The tag is not the release** (joshuafolkken/kit#3193): npm and
83
+ // the GitHub Release follow it, so the success line names only the tag.
71
84
  function format_result(version: string, is_tagged: boolean): string {
72
- if (is_tagged) return `🏷 ${tag_name(version)} exists — the release is published.`
85
+ if (is_tagged) return `🏷 Tag ${tag_name(version)} created`
73
86
 
74
87
  return [
75
88
  `❌ ${tag_name(version)} never appeared.`,
@@ -82,6 +95,7 @@ const release_tag = {
82
95
  attempts_for,
83
96
  configured_timeout_seconds,
84
97
  format_result,
98
+ poll_options_for,
85
99
  tag_name,
86
100
  wait_for_tag,
87
101
  DEFAULT_TIMEOUT_SECONDS,
@@ -12,7 +12,7 @@ const RUN_ENTRY_ORACLE: DecisionOracle = {
12
12
  'Where a run on an issue starts, and whether the typed command has already been reached',
13
13
  args: '<N> [--to kickoff|halfrun|prrun|fullrun]',
14
14
  vocabulary: run_stage.START_TOKENS,
15
- single_source: '.claude/skills/workflow-commands/SKILL.md → §2k',
15
+ single_source: 'docs/how-to/run-issues.md',
16
16
  }
17
17
 
18
18
  export { RUN_ENTRY_ORACLE }
@@ -11,7 +11,7 @@ import { RUN_ENTRY_ORACLE } from './decision-oracle-stage'
11
11
  // Paths that appear in more than one entry's `single_source` field.
12
12
  const CHAIN_RULE_MD = '.claude/skills/workflow-commands/chain-rule.md'
13
13
  const BACKLOGRUN_PROGRESS_MD = '.claude/skills/workflow-commands/backlogrun-progress.md'
14
- const SKILL_2E = '.claude/skills/workflow-commands/SKILL.md → §2e'
14
+ const SKILL_2E = '.claude/skills/workflow-commands/issue-scout.md'
15
15
  const SPLIT_ASSESSMENT_QUESTION =
16
16
  '.claude/skills/workflow-commands/split-assessment.md → The question'
17
17
  const PRE_GATE_CUT_MD = '.claude/skills/workflow-commands/pre-gate-cut.md'
@@ -82,7 +82,7 @@ const DECISION_ORACLES: ReadonlyArray<DecisionOracle> = [
82
82
  decision: 'Whether a run step may be delegated to a cheaper execution tier',
83
83
  args: '<step>',
84
84
  vocabulary: ['delegate', 'keep'],
85
- single_source: '.claude/skills/workflow-commands/SKILL.md → §2b',
85
+ single_source: '.claude/skills/workflow-commands/delegation.md',
86
86
  },
87
87
  {
88
88
  name: 'review:level',
@@ -125,7 +125,7 @@ const DECISION_ORACLES: ReadonlyArray<DecisionOracle> = [
125
125
  decision: 'Whether this working tree is free for a run to claim',
126
126
  args: `[${ISSUE_N_ARG}]`,
127
127
  vocabulary: ['hold', BUSY, UNKNOWN],
128
- single_source: '.claude/skills/workflow-commands/SKILL.md → §2f',
128
+ single_source: '.claude/skills/workflow-commands/working-tree-hold.md',
129
129
  },
130
130
  {
131
131
  name: 'cost:cut',
@@ -140,7 +140,7 @@ const DECISION_ORACLES: ReadonlyArray<DecisionOracle> = [
140
140
  decision: 'Whether a release is owed after this merge',
141
141
  args: '',
142
142
  vocabulary: [REQUIRED, SKIP, UNKNOWN],
143
- single_source: '.claude/skills/workflow-commands/followup-reference.md',
143
+ single_source: '.claude/skills/workflow-commands/followup.md',
144
144
  },
145
145
  {
146
146
  name: 'epic:bundle',
@@ -191,7 +191,7 @@ const DECISION_ORACLES: ReadonlyArray<DecisionOracle> = [
191
191
  decision: "An issue's state, labels, and human-review flag",
192
192
  args: ISSUE_N_ARG,
193
193
  vocabulary: ['human_review: yes', 'human_review: no'],
194
- single_source: '.claude/skills/workflow-commands/SKILL.md → §2z',
194
+ single_source: '.claude/skills/workflow-commands/needs-human-review.md',
195
195
  },
196
196
  {
197
197
  name: 'run:cut:resume',
@@ -228,7 +228,7 @@ const DECISION_ORACLES: ReadonlyArray<DecisionOracle> = [
228
228
  decision: 'Whether a named stash entry exists and can be applied',
229
229
  args: '<message>',
230
230
  vocabulary: ['popped', 'conflicted', 'no-match', 'ambiguous'],
231
- single_source: '.claude/skills/workflow-commands/SKILL.md → §2d',
231
+ single_source: '.claude/skills/workflow-commands/prerequisite.md',
232
232
  },
233
233
  {
234
234
  name: 'backlog:next',
@@ -147,14 +147,14 @@ const { is_issue_filing, on_bash_command } = bash_triggers
147
147
  // `WIP_CAP` is the number's single source; `wip-cap.md` states it once and a test pins the two equal.
148
148
  const WIP_CAP = 30
149
149
  const WIP_CAP_REASON =
150
- `⛔ backlog WIP cap: count the target repository's open Issues before filing. With more than ${String(WIP_CAP)} ` +
151
- 'open, close one first; nothing honestly closable means do not file. Two filings are exempt and ' +
152
- 'proceed while stating the overage — one the run is blocked by, and an interrupt, decided by ' +
153
- 'three tests rather than judgement: a verification answers wrongly, a documented workflow cannot ' +
154
- 'complete, or data is lost or written outside the repository. Meeting none of the three, the ' +
155
- 'finding is discretionary and waits. The count command and both procedures are in ' +
156
- '`prompts/collaboration-workflow/wip-cap.md`. Reissue this call once you have counted — it fires ' +
157
- 'once per run and cannot repeat on the call in hand.'
150
+ `⛔ backlog WIP cap: \`pnpm josh issue:file\` counts the target repository's open Issues before filing. With more than ${String(WIP_CAP)} ` +
151
+ 'open, close one first; nothing honestly closable means do not file — the command holds such a filing. ' +
152
+ 'Two filings are exempt and proceed while stating the overage — one the run is blocked by (`--over-cap`, ' +
153
+ 'or the `tier-a` / `split` route), and an interrupt (`--route interrupt`), decided by three tests rather ' +
154
+ 'than judgement: a verification answers wrongly, a documented workflow cannot complete, or data is lost ' +
155
+ 'or written outside the repository. Meeting none of the three, the finding is discretionary and waits. ' +
156
+ 'Both procedures are in `prompts/collaboration-workflow/wip-cap.md`. Reissue this call and let the ' +
157
+ 'command count — it fires once per run and cannot repeat on the call in hand.'
158
158
 
159
159
  // **A shell line carries several commands, and the subcommand has to be the one being invoked.**
160
160
  // Each segment is judged on its own, anchored at its start, so `gh issue comment <N> -b "… gh issue
@@ -241,7 +241,7 @@ const ISSUE_COMMENTS_REASON =
241
241
  "except for two answers that are not the run's to make: work a comment reassigns to another " +
242
242
  'Issue is out of scope and is not implemented, and a comment saying the Issue no longer has a ' +
243
243
  'reason to exist stops the run with a `confirmation` Telegram. The procedure is ' +
244
- '`.claude/skills/workflow-commands/SKILL.md` → "An Issue\'s comments are part of the Issue". ' +
244
+ '`.claude/skills/workflow-commands/issue-comments.md`. ' +
245
245
  'Every body-only read is refused until the comments are read.'
246
246
 
247
247
  // The reading of the call itself — which spellings carry a body inline, and what the shell does to
@@ -266,8 +266,8 @@ const OPEN_STATE = /--state[= ]open|state=open/u
266
266
  // `…/issues?labels=epic&state=open` ask which epics are open, which is what `epic:bundle` and the
267
267
  // Issue template do; counting either as the WIP count would credit the cap as kept by a run that
268
268
  // never counted the backlog. The residual the pattern cannot separate is named in
269
- // `docs/josh-commands.md`: the inventory command in the `diag` skill is byte-identical to the count
270
- // command in `wip-cap.md`, so no pattern can tell those two apart.
269
+ // `docs/josh-commands.md`: the inventory command in the `diag` skill is byte-identical to a hand count
270
+ // of the backlog, so no pattern can tell those two apart.
271
271
  const LABEL_FILTER = /--label\b|[?&]labels=/u
272
272
 
273
273
  function counts_open_issues(command: string): boolean {
@@ -0,0 +1,110 @@
1
+ import { bash_triggers } from './bash-triggers'
2
+ import { gh_api } from './gh-api'
3
+ import { git_argv } from './git-argv'
4
+ import { shell_segments } from './shell-segments'
5
+
6
+ // The trigger and the delivered text behind the `direct-pr-create` row of `delivered-rules.ts`
7
+ // (joshuafolkken/kit#3183). A PR opened outside `pnpm josh pr` skips `build_body`, which writes the
8
+ // `closes #N` line — so the Issue stays open after the merge. `CLAUDE.md` forbade the call in prose
9
+ // only; this row reads the argv instead, through `git-argv.ts`'s wrapper cut, so `env gh pr create`
10
+ // is seen too. `pnpm josh pr` itself is never claimed: its first word after the wrapper is `pnpm`,
11
+ // and the `gh api` call it makes runs inside the script, not as a `Bash` command.
12
+ //
13
+ // **It fires on every occurrence**, for the reason `git-force.ts` does.
14
+
15
+ const GH_COMMAND = 'gh'
16
+ const PR_GROUP = 'pr'
17
+ // `new` is gh's own alias for `pr create`.
18
+ const PR_CREATE_ACTIONS: ReadonlySet<string> = new Set(['create', 'new'])
19
+ const FLAG_PREFIX = '-'
20
+ // The repository selector gh accepts before `pr` or between `pr` and its action; with no `=` it
21
+ // swallows the next word, so that word is not read as the group or the action.
22
+ const VALUE_TAKING_FLAGS: ReadonlySet<string> = new Set(['-R', '--repo'])
23
+ // `gh api`'s own flags that take a value as the next word, beside the repository selector — skipped
24
+ // so `-X POST` or `-f body=…` is not read as the endpoint.
25
+ const API_VALUE_TAKING_FLAGS: ReadonlySet<string> = new Set([
26
+ ...VALUE_TAKING_FLAGS,
27
+ '-X',
28
+ '--method',
29
+ '-f',
30
+ '--raw-field',
31
+ '-F',
32
+ '--field',
33
+ '-H',
34
+ '--header',
35
+ '--input',
36
+ '-q',
37
+ '--jq',
38
+ '-t',
39
+ '--template',
40
+ '--hostname',
41
+ '-p',
42
+ '--preview',
43
+ '--cache',
44
+ ])
45
+ const API_SUBCOMMAND = 'api'
46
+ // The pull-request collection itself — a write there opens a PR. Anchored on the whole endpoint
47
+ // argument, so `…/pulls/5/comments` is not read as the collection. A full API URL names the same
48
+ // endpoint.
49
+ const PULLS_PATH =
50
+ /^['"]?(?:https:\/\/api\.github\.com)?\/?repos\/[^/\s'"]+\/[^/\s'"]+\/pulls\/?['"]?$/u
51
+
52
+ // The words that are neither a flag nor a value-taking flag's value, so `gh -R o/r pr create` and
53
+ // `gh pr --repo o/r create` reach the same `pr` / `create` pair as the bare spelling.
54
+ function positional_words(
55
+ args: ReadonlyArray<string>,
56
+ value_taking_flags: ReadonlySet<string>,
57
+ ): Array<string> {
58
+ return args.filter((argument, index) => {
59
+ if (argument.startsWith(FLAG_PREFIX)) return false
60
+
61
+ return !value_taking_flags.has(args[index - 1] ?? '')
62
+ })
63
+ }
64
+
65
+ // Only the endpoint — the first positional word after `api` — is tested, so a field value that
66
+ // happens to spell the collection path (`-f body='… repos/o/r/pulls'`) is not read as the target.
67
+ function is_pr_create_api_call(args: ReadonlyArray<string>): boolean {
68
+ const call = [GH_COMMAND, ...args].join(' ')
69
+
70
+ if (!gh_api.is_gh_api(call) || !gh_api.is_write(call)) return false
71
+
72
+ const [subcommand, endpoint = ''] = positional_words(args, API_VALUE_TAKING_FLAGS)
73
+
74
+ return subcommand === API_SUBCOMMAND && PULLS_PATH.test(endpoint)
75
+ }
76
+
77
+ function is_pr_create_cli_call(args: ReadonlyArray<string>): boolean {
78
+ const [group, action = ''] = positional_words(args, VALUE_TAKING_FLAGS)
79
+
80
+ return group === PR_GROUP && PR_CREATE_ACTIONS.has(action)
81
+ }
82
+
83
+ function is_pr_create_segment(segment: string): boolean {
84
+ const args = git_argv.arguments_of(segment, GH_COMMAND)
85
+
86
+ if (args === undefined) return false
87
+
88
+ return is_pr_create_cli_call(args) || is_pr_create_api_call(args)
89
+ }
90
+
91
+ function is_direct_pr_create(command: string): boolean {
92
+ return shell_segments.segments_of(command).some((segment) => is_pr_create_segment(segment))
93
+ }
94
+
95
+ const DIRECT_PR_CREATE_REASON =
96
+ '⛔ direct PR creation: `gh pr create` (or a `gh api` POST to `repos/<o>/<r>/pulls`) bypasses ' +
97
+ '`build_body`, which writes the `closes #N` line — the Issue would stay open after the merge ' +
98
+ '(`CLAUDE.md` → Git Rules). Open the PR with `pnpm josh pr` instead; after a failed push, fix, push, ' +
99
+ 'then run `pnpm josh pr`. **This rule fires on every occurrence, not once per run.**'
100
+
101
+ const ROW = {
102
+ id: 'direct-pr-create',
103
+ is_trigger: bash_triggers.on_bash_command(is_direct_pr_create),
104
+ reason: DIRECT_PR_CREATE_REASON,
105
+ decide: (): boolean => true,
106
+ }
107
+
108
+ const direct_pr_create = { DIRECT_PR_CREATE_REASON, ROW, is_direct_pr_create }
109
+
110
+ export { direct_pr_create }