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