@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.
- package/.claude/settings.json +4 -4
- package/.claude/skills/epic-commands/SKILL.md +27 -239
- package/.claude/skills/epic-commands/epic-bundle.md +115 -0
- package/.claude/skills/epic-commands/execution-waves.md +126 -0
- package/.claude/skills/workflow-commands/SKILL.md +48 -194
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-child.md +12 -123
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +11 -60
- package/.claude/skills/workflow-commands/backlogrun-park.md +10 -8
- package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -173
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +64 -144
- package/.claude/skills/workflow-commands/backlogrun.md +5 -4
- package/.claude/skills/workflow-commands/chain-rule.md +3 -2
- package/.claude/skills/workflow-commands/delegation.md +2 -2
- package/.claude/skills/workflow-commands/entry-sequence.md +62 -0
- package/.claude/skills/workflow-commands/followup.md +104 -181
- package/.claude/skills/workflow-commands/fullrun-steps.md +8 -9
- package/.claude/skills/workflow-commands/fullrun.md +16 -73
- package/.claude/skills/workflow-commands/halfrun.md +10 -36
- package/.claude/skills/workflow-commands/into-target.md +4 -4
- package/.claude/skills/workflow-commands/issue-comments.md +7 -8
- package/.claude/skills/workflow-commands/issue-scout.md +6 -6
- package/.claude/skills/workflow-commands/kickoff.md +6 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +6 -6
- package/.claude/skills/workflow-commands/observation-filing.md +21 -172
- package/.claude/skills/workflow-commands/observation-ledger.md +150 -0
- package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -93
- package/.claude/skills/workflow-commands/prerequisite.md +6 -6
- package/.claude/skills/workflow-commands/progress-watcher.md +90 -0
- package/.claude/skills/workflow-commands/prrun.md +5 -5
- package/.claude/skills/workflow-commands/retrospective.md +9 -9
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/.claude/skills/workflow-commands/target-repository.md +2 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +7 -5
- package/.codex/hooks.json +4 -4
- package/AGENTS.md +1 -6
- package/CLAUDE.md +58 -120
- package/GEMINI.md +1 -6
- package/dist/CLAUDE.md +58 -120
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-NFZGFGOR.js → chunk-5KTCMCOT.js} +14 -1
- package/dist/hooks/{chunk-LJBLGHEH.js → chunk-A6P7PN7P.js} +4 -6
- package/dist/hooks/{chunk-FO2I7UT7.js → chunk-ANJ2WGFD.js} +1 -1
- package/dist/hooks/{chunk-GDU26K3J.js → chunk-DYAVAM6S.js} +563 -126
- package/dist/hooks/{chunk-RFWWUI5F.js → chunk-ENA7P5FB.js} +3 -3
- package/dist/hooks/{chunk-KBXZLMRH.js → chunk-GU5MATVE.js} +213 -65
- package/dist/hooks/chunk-GYNGP7HI.js +56 -0
- package/dist/hooks/{chunk-HBKZLITF.js → chunk-URJA6XY4.js} +4 -4
- package/dist/hooks/{chunk-KOKOAV6D.js → chunk-V2BTOZHT.js} +6 -4
- package/dist/hooks/{chunk-JKKTVDAG.js → chunk-ZDZ555YH.js} +100 -20
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/inputs.json +193 -192
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +11 -10
- package/dist/hooks/stop-guard.js +5 -5
- package/dist/josh.js +25 -2
- package/dist/version/index.js +5 -1
- package/package.json +2 -1
- package/prompts/collaboration-workflow/file-edits.md +11 -10
- package/prompts/collaboration-workflow/gh-rest.md +2 -4
- package/prompts/collaboration-workflow/glossary.md +1 -1
- package/prompts/collaboration-workflow/issue-citation.md +5 -9
- package/prompts/collaboration-workflow/issue-template.md +40 -115
- package/prompts/collaboration-workflow/operating-rules.md +4 -5
- package/prompts/collaboration-workflow/output-bounds.md +12 -78
- package/prompts/collaboration-workflow/overview.md +7 -13
- package/prompts/collaboration-workflow/plan-comment.md +11 -39
- package/prompts/collaboration-workflow/principles.md +3 -4
- package/prompts/collaboration-workflow/report-format.md +28 -74
- package/prompts/collaboration-workflow/residency.md +12 -109
- package/prompts/collaboration-workflow/rule-delivery.md +98 -97
- package/prompts/collaboration-workflow/shell-body.md +3 -13
- package/prompts/collaboration-workflow/turn-batching.md +14 -88
- package/prompts/collaboration-workflow/upstream-interrupt.md +12 -14
- package/prompts/collaboration-workflow/wip-cap.md +6 -45
- package/prompts/collaboration-workflow.md +1 -13
- package/prompts/review-rubric.md +8 -71
- package/prompts/review.md +25 -55
- package/prompts/testing-guide.md +18 -67
- package/scripts/agent/agent-role-profile.ts +1 -1
- package/scripts/agent/codex-hooks.ts +3 -12
- package/scripts/backlog/backlog-budget.ts +1 -1
- package/scripts/backlog/backlog-drive-cli.ts +4 -3
- package/scripts/backlog/backlog-drive-offer-argv.ts +10 -1
- package/scripts/backlog/backlog-offer.ts +6 -6
- package/scripts/behavior/behavior-rules.ts +11 -4
- package/scripts/delegation/investigation-reads.ts +5 -5
- package/scripts/discovery/repo-discovery.ts +21 -7
- package/scripts/discovery/repo-origin.ts +26 -0
- package/scripts/discovery/repo-party.ts +42 -1
- package/scripts/document/backlogrun-parent-read-set.ts +14 -15
- package/scripts/document/document-byte-budget.ts +34 -20
- package/scripts/document/entry-read-budget.ts +21 -9
- package/scripts/document/lane-child-read-set.ts +12 -14
- package/scripts/document/read-set-cli.ts +4 -4
- package/scripts/document/read-set-point-of-use.ts +10 -2
- package/scripts/document/resident-budget.ts +47 -0
- package/scripts/epic/epic-shape.ts +10 -13
- package/scripts/epic/epic-solo.ts +1 -1
- package/scripts/exports/unused-members-cli.ts +7 -3
- package/scripts/gh/git-gh-api-path.ts +7 -0
- package/scripts/hooks/pretool-guard-cli.ts +2 -2
- package/scripts/hooks/pretool-guard.ts +45 -2
- package/scripts/hooks/run-hook.sh +41 -0
- package/scripts/init/hook-command-rewrite.ts +18 -35
- package/scripts/init/hook-launch.ts +21 -32
- package/scripts/issue/issue-auto-ok.ts +95 -0
- package/scripts/issue/issue-file-cli.ts +53 -7
- package/scripts/issue/issue-file.ts +28 -5
- package/scripts/issue/issue-labels.ts +12 -12
- package/scripts/issue/issue-rank.ts +1 -1
- package/scripts/issue/issue-read-cli.ts +3 -3
- package/scripts/issue/issue-read.ts +2 -2
- package/scripts/issue/issue-scout-cli.ts +2 -2
- package/scripts/issue/issue-state-cli.ts +1 -1
- package/scripts/issue/issue-state.ts +2 -2
- package/scripts/issue/issue-wip.ts +70 -0
- package/scripts/josh/hook-decision.ts +17 -0
- package/scripts/josh/josh-commands-development.ts +10 -0
- package/scripts/josh/josh-commands-issue.ts +2 -2
- package/scripts/lines/bytes-command.ts +29 -6
- package/scripts/observations/observation-ledger-line.ts +1 -1
- package/scripts/release/release-progress.ts +167 -0
- package/scripts/release/release-publish.ts +19 -8
- package/scripts/release/release-tag.ts +22 -8
- package/scripts/rules/decision-oracle-stage.ts +1 -1
- package/scripts/rules/decision-oracle.ts +6 -6
- package/scripts/rules/delivered-rules.ts +11 -11
- package/scripts/rules/direct-pr-create.ts +110 -0
- package/scripts/rules/file-body.ts +98 -42
- package/scripts/rules/filing-cap.ts +2 -2
- package/scripts/rules/filing-offer.ts +1 -1
- package/scripts/rules/gh-api.ts +18 -1
- package/scripts/rules/implementation-cut.ts +3 -1
- package/scripts/rules/lane-background.ts +67 -10
- package/scripts/rules/permission-guards.ts +10 -2
- package/scripts/rules/stop-rules.ts +4 -4
- package/scripts/rules/test-declared-commit.ts +1 -1
- package/scripts/rules/third-party-write.ts +56 -12
- package/scripts/run/run-cut-handoff.ts +21 -1
- package/scripts/run/run-entry-cli.ts +11 -3
- package/scripts/run/run-hold-cli.ts +16 -2
- package/scripts/run/run-label.ts +50 -0
- package/scripts/run/run-merge-steps.ts +8 -3
- package/scripts/run/run-merge.ts +1 -1
- package/scripts/run/run-prep-cli.ts +6 -2
- package/scripts/run/run-prep.ts +17 -0
- package/scripts/run/run-ship-preflight.ts +34 -3
- package/scripts/run/run-ship-review-steps.ts +1 -1
- package/scripts/run/run-ship-review.ts +26 -5
- package/scripts/run/run-step-cli.ts +20 -7
- package/scripts/run/run-wake-driver.ts +24 -1
- package/scripts/run/run-wake-session.ts +2 -1
- package/scripts/test/test-declared-logic.ts +1 -1
- package/scripts/time-runtime/time-batch-guard.ts +52 -16
- package/scripts/time-runtime/time-density-hook.ts +20 -2
- package/scripts/time-runtime/time-density.ts +4 -3
- package/scripts/time-runtime/time-markers.ts +15 -4
- package/scripts/version/npm-registry.ts +11 -1
- package/templates/workflows/ci.yml +9 -1
- package/.claude/skills/workflow-commands/followup-reference.md +0 -118
- 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
|
|
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<
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
):
|
|
56
|
-
const timeout_seconds = configured_timeout_seconds(process.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
|
-
|
|
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
|
-
|
|
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)}
|
|
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: '
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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/
|
|
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/
|
|
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:
|
|
151
|
-
'open, close one first; nothing honestly closable means do not file
|
|
152
|
-
'proceed while stating the overage — one the run is blocked by
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'`prompts/collaboration-workflow/wip-cap.md`. Reissue this call
|
|
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/
|
|
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
|
|
270
|
-
//
|
|
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 }
|