@joshuafolkken/kit 1.1136.0 → 1.1178.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 +14 -63
- package/.claude/skills/workflow-commands/backlogrun-park.md +16 -15
- 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 +65 -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 +35 -95
- 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/.github/workflows/ci.yml +15 -2
- 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-32GQTL4F.js} +14 -1
- package/dist/hooks/{chunk-HBKZLITF.js → chunk-DO6NWWHU.js} +4 -4
- package/dist/hooks/{chunk-KBXZLMRH.js → chunk-G6KRCKEC.js} +216 -76
- package/dist/hooks/{chunk-JKKTVDAG.js → chunk-GDS5N2KB.js} +186 -309
- package/dist/hooks/chunk-I4XSL4TT.js +56 -0
- package/dist/hooks/{chunk-GDU26K3J.js → chunk-S6X6WYKR.js} +886 -174
- package/dist/hooks/{chunk-FO2I7UT7.js → chunk-UAGLLIGZ.js} +1 -1
- package/dist/hooks/{chunk-RFWWUI5F.js → chunk-VESKRQOF.js} +3 -3
- package/dist/hooks/{chunk-KOKOAV6D.js → chunk-VQLLNFNT.js} +6 -4
- package/dist/hooks/{chunk-LJBLGHEH.js → chunk-ZAL3NRGL.js} +4 -6
- 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 +178 -177
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +11 -10
- package/dist/hooks/stop-guard.js +20 -6
- package/dist/josh.js +29 -3
- 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-next.ts +15 -4
- package/scripts/backlog/backlog-offer.ts +6 -6
- package/scripts/backlog/backlog-plan-cli.ts +6 -5
- package/scripts/backlog/backlog-rank.ts +18 -9
- package/scripts/backlog/backlog-restructure.ts +152 -0
- package/scripts/backlog/backlog-waves.ts +15 -17
- package/scripts/behavior/behavior-rules.ts +11 -4
- package/scripts/cost-runtime/context-cut-threshold.ts +1 -9
- package/scripts/cost-runtime/cost-verdict.ts +15 -28
- 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/followup/git-followup-stages.ts +9 -0
- package/scripts/followup/git-pr-followup.ts +1 -1
- package/scripts/gh/git-gh-api-path.ts +7 -0
- package/scripts/gh/git-gh-issue.ts +5 -3
- package/scripts/gh/git-gh-pr-read.ts +4 -0
- package/scripts/gh/git-pr-checks-eval.ts +8 -0
- package/scripts/gh/git-pr-checks.ts +11 -1
- package/scripts/git/constants.ts +10 -0
- package/scripts/git/main-merge.ts +57 -21
- package/scripts/git/schemas.ts +2 -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 +11 -1
- package/scripts/josh/josh-commands-issue.ts +2 -2
- package/scripts/lane/lane-launch-cli.ts +21 -3
- package/scripts/lines/bytes-command.ts +29 -6
- package/scripts/notify/telegram-notify.ts +26 -1
- 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/pre-gate-cut.ts +2 -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.ts +11 -5
- package/scripts/run/run-cut-cli.ts +1 -1
- package/scripts/run/run-cut-handoff.ts +21 -1
- package/scripts/run/run-cut-report.ts +2 -2
- 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-cli.ts +27 -6
- package/scripts/run/run-ship-next.ts +13 -0
- package/scripts/run/run-ship-pre-detach.ts +66 -0
- package/scripts/run/run-ship-preflight.ts +34 -3
- package/scripts/run/run-ship-return.ts +7 -4
- package/scripts/run/run-ship-review-steps.ts +1 -1
- package/scripts/run/run-ship-review.ts +26 -5
- package/scripts/run/run-ship-stage.ts +13 -0
- package/scripts/run/run-ship-steps.ts +10 -2
- package/scripts/run/run-ship-sync.ts +114 -0
- package/scripts/run/run-ship.ts +6 -0
- package/scripts/run/run-step-cli.ts +20 -7
- package/scripts/run/run-wake-cli.ts +8 -1
- package/scripts/run/run-wake-driver.ts +62 -8
- package/scripts/run/run-wake-session.ts +2 -1
- package/scripts/test/test-declared-logic.ts +1 -1
- package/scripts/test/test-red-logic.ts +17 -2
- package/scripts/test/test-red.ts +8 -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-transcript-line.ts +27 -1
- 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
|
@@ -60,7 +60,7 @@ const {
|
|
|
60
60
|
} = run_cut_report
|
|
61
61
|
|
|
62
62
|
// **The pre-gate cut is conditional on the same statistic the implementation-phase cut reads**
|
|
63
|
-
// (joshuafolkken/kit#2312). `cost_cli.session_verdict` prices the
|
|
63
|
+
// (joshuafolkken/kit#2312). `cost_cli.session_verdict` prices the newest request
|
|
64
64
|
// against `CONTEXT_CUT_THRESHOLD`, so no second threshold is introduced — an `under` session skips the
|
|
65
65
|
// cut, and an unmeasurable one keeps the old unconditional cut as the safety net joshuafolkken/kit#1933
|
|
66
66
|
// relies on. Only the pre-gate phase is checked here; the implementation-phase caller gates its own
|
|
@@ -41,6 +41,22 @@ const UNTOUCHED_LABEL = 'Deliberately untouched'
|
|
|
41
41
|
const INSTRUCTION_LABEL = 'Instruction (verbatim)'
|
|
42
42
|
const EMPTY_LIST_NOTE = '(none)'
|
|
43
43
|
|
|
44
|
+
type HandoffKey = keyof Handoff
|
|
45
|
+
|
|
46
|
+
function describe_key(key: HandoffKey): string {
|
|
47
|
+
const value = handoff_schema.shape[key] instanceof z.ZodArray ? '["…"]' : '"…"'
|
|
48
|
+
|
|
49
|
+
return `"${key}": ${value}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The shape a handoff file must have, derived from `handoff_schema` so the refusal that asks for the file
|
|
53
|
+
// and the error that rejects it can never name a key the parser does not read (joshuafolkken/kit#3195).
|
|
54
|
+
// A refusal that listed the four items in prose was answered with Markdown, which `parse_handoff` rejects.
|
|
55
|
+
const HANDOFF_FORMAT = `a JSON object {${handoff_schema
|
|
56
|
+
.keyof()
|
|
57
|
+
.options.map((key) => describe_key(key))
|
|
58
|
+
.join(', ')}}`
|
|
59
|
+
|
|
44
60
|
function parse_handoff(raw: string): Handoff | undefined {
|
|
45
61
|
try {
|
|
46
62
|
const parsed = handoff_schema.safeParse(JSON.parse(raw))
|
|
@@ -84,7 +100,10 @@ function load_handoff(handoff_path: string | undefined, max_bytes: number): Hand
|
|
|
84
100
|
const handoff = read_handoff_file(handoff_path)
|
|
85
101
|
|
|
86
102
|
if (handoff === undefined) {
|
|
87
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
kind: 'bad',
|
|
105
|
+
note: `--handoff ${handoff_path} is not a readable handoff file — write ${HANDOFF_FORMAT}`,
|
|
106
|
+
}
|
|
88
107
|
}
|
|
89
108
|
|
|
90
109
|
if (!is_within_bound(handoff, max_bytes)) {
|
|
@@ -114,6 +133,7 @@ function describe_handoff(handoff: Handoff): string {
|
|
|
114
133
|
}
|
|
115
134
|
|
|
116
135
|
const run_cut_handoff = {
|
|
136
|
+
HANDOFF_FORMAT,
|
|
117
137
|
describe_handoff,
|
|
118
138
|
handoff_schema,
|
|
119
139
|
is_complete_handoff,
|
|
@@ -14,7 +14,7 @@ const RESUME_VERDICT = 'resume'
|
|
|
14
14
|
// re-reading the record.
|
|
15
15
|
const RESUME_IMPL_VERDICT = 'resume-impl'
|
|
16
16
|
const FRESH_VERDICT = 'fresh'
|
|
17
|
-
// The
|
|
17
|
+
// The current context is under the shared cut threshold, so the pre-gate cut was not worth its
|
|
18
18
|
// resume and nothing was cut (joshuafolkken/kit#2312). A benign, non-failing answer: the run carries
|
|
19
19
|
// on to the gate itself, exactly as `not-a-lane` does.
|
|
20
20
|
const UNDER_THRESHOLD_VERDICT = 'under-threshold'
|
|
@@ -111,7 +111,7 @@ function refuse_unready(state: CutState): number {
|
|
|
111
111
|
// relaunch costs more than it saves; the current process carries the run on to the gate uncut.
|
|
112
112
|
function report_under_threshold(): number {
|
|
113
113
|
console.error(
|
|
114
|
-
'The
|
|
114
|
+
'The current context is under the shared cut threshold, so nothing was cut; a resume would cost more than the accumulation it would drop. Continue to the gate.',
|
|
115
115
|
)
|
|
116
116
|
|
|
117
117
|
return report(UNDER_THRESHOLD_VERDICT, SUCCESS_EXIT_CODE)
|
|
@@ -7,6 +7,7 @@ import { run_entry, type EntryParts } from './run-entry'
|
|
|
7
7
|
import { run_entry_stop } from './run-entry-stop'
|
|
8
8
|
import { run_halfrun_resume } from './run-halfrun-resume'
|
|
9
9
|
import { run_hold_cli } from './run-hold-cli'
|
|
10
|
+
import { run_label } from './run-label'
|
|
10
11
|
import { run_next } from './run-next'
|
|
11
12
|
import { run_prep } from './run-prep'
|
|
12
13
|
import { run_prep_cli } from './run-prep-cli'
|
|
@@ -231,6 +232,15 @@ function is_settled(decision: StageDecision): boolean {
|
|
|
231
232
|
return decision.is_reached && decision.state !== run_stage.MERGED
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
// The tree is held and the budget allows the run, so the issue is marked as running before its reads
|
|
236
|
+
// (joshuafolkken/kit#3182) — the label then shows in the `labels:` line the report below carries.
|
|
237
|
+
async function proceed(issue_number: string, hold: string, cost: string): Promise<number> {
|
|
238
|
+
await run_label.mark(issue_number)
|
|
239
|
+
const reads = await gather_reads(issue_number)
|
|
240
|
+
|
|
241
|
+
return emit({ issue_number, hold, cost, verdict: reads.verdict, report: reads.report })
|
|
242
|
+
}
|
|
243
|
+
|
|
234
244
|
async function claim_run({ issue_number, command }: EntryRequest): Promise<number> {
|
|
235
245
|
const hold = await claim_hold(issue_number, command)
|
|
236
246
|
|
|
@@ -248,9 +258,7 @@ async function claim_run({ issue_number, command }: EntryRequest): Promise<numbe
|
|
|
248
258
|
return await stop_run({ parts, command, reason: BUDGET_REASON, should_release: true })
|
|
249
259
|
}
|
|
250
260
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return emit({ issue_number, hold, cost, verdict: reads.verdict, report: reads.report })
|
|
261
|
+
return await proceed(issue_number, hold, cost)
|
|
254
262
|
}
|
|
255
263
|
|
|
256
264
|
// A carried cut is asked first, before anything is read (`run:cut --resume`); `kickoff` claims and cuts
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { run_halfrun_resume } from './run-halfrun-resume'
|
|
4
4
|
import { run_hold, type HoldRead, type RunHold } from './run-hold'
|
|
5
|
+
import { run_label } from './run-label'
|
|
5
6
|
import { run_preflight, type PreflightDecision } from './run-preflight'
|
|
6
7
|
import { run_prrun_resume } from './run-prrun-resume'
|
|
7
8
|
import { run_tidy_cli } from './run-tidy-cli'
|
|
@@ -267,7 +268,7 @@ function report_unreadable_release(): number {
|
|
|
267
268
|
// Until then this removed whatever was there, and the `busy` stop message sent a person here to clear
|
|
268
269
|
// a record they had judged stale from outside the run that wrote it — so the guard's own recovery
|
|
269
270
|
// instruction was a way to free a live run's tree, which is the incident it was written after.
|
|
270
|
-
function
|
|
271
|
+
function release_record(target: string, claimant: string): number {
|
|
271
272
|
const read = run_hold.read_hold(target)
|
|
272
273
|
|
|
273
274
|
if (read.kind === 'free') return remove_record(target, false)
|
|
@@ -279,6 +280,19 @@ function release(target: string, claimant: string): number {
|
|
|
279
280
|
return remove_record(target, true)
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
// **A numbered run's own release takes its `in-progress` marker back off** (joshuafolkken/kit#3182),
|
|
284
|
+
// whether or not a record was still there — the stop that releases is the run ending its hold on the
|
|
285
|
+
// issue. A refused release (another run's record, an unreadable one) touches no label, and an
|
|
286
|
+
// unnumbered run names no issue to touch.
|
|
287
|
+
async function release(target: string, claimant: string): Promise<number> {
|
|
288
|
+
const code = release_record(target, claimant)
|
|
289
|
+
const is_own_end = code === SUCCESS_EXIT_CODE && claimant !== run_hold.UNNUMBERED_ISSUE
|
|
290
|
+
|
|
291
|
+
if (is_own_end) await run_label.unmark(claimant)
|
|
292
|
+
|
|
293
|
+
return code
|
|
294
|
+
}
|
|
295
|
+
|
|
282
296
|
// The one path that removes a record without matching it, which is why it says what it removed: a
|
|
283
297
|
// record genuinely abandoned by a crashed session has no run left to release it, and the eight-hour
|
|
284
298
|
// expiry alone leaves a dirty tree held until a person acts.
|
|
@@ -335,7 +349,7 @@ async function dispatch(request: HoldRequest, target: string, is_linked: boolean
|
|
|
335
349
|
|
|
336
350
|
if (request.kind === FORCE_RELEASE_KIND) return force_release(target)
|
|
337
351
|
|
|
338
|
-
return release(target, request.claimant)
|
|
352
|
+
return await release(target, request.claimant)
|
|
339
353
|
}
|
|
340
354
|
|
|
341
355
|
async function answer(request: HoldRequest): Promise<number> {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { git_followup_label } from '#scripts/followup/git-followup-label'
|
|
2
|
+
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
3
|
+
import { IN_PROGRESS_LABEL } from '#scripts/issue/issue-labels'
|
|
4
|
+
|
|
5
|
+
// **The `in-progress` marker belongs to the commands that open and release a run**
|
|
6
|
+
// (joshuafolkken/kit#3182). Every manifest used to spell the label's create, apply and removal as raw
|
|
7
|
+
// `gh api` calls, so each copy was a place the marker could be forgotten; `run:entry` applies it the
|
|
8
|
+
// moment the tree is held and the budget allows the run, and `run:release` takes it back off.
|
|
9
|
+
//
|
|
10
|
+
// **Neither half ever fails the command it rides on.** The hold is the guard and the label only
|
|
11
|
+
// reports it, so a refused write is warned about by name — with the call that finishes it by hand —
|
|
12
|
+
// and the run's own verdict stands.
|
|
13
|
+
|
|
14
|
+
const MARK_RECOVERY = "gh api repos/{owner}/{repo}/issues/<N>/labels -f 'labels[]=in-progress'"
|
|
15
|
+
const UNMARK_RECOVERY = 'gh api -X DELETE repos/{owner}/{repo}/issues/<N>/labels/in-progress'
|
|
16
|
+
|
|
17
|
+
function warn(action: string, issue: string, recovery: string): void {
|
|
18
|
+
console.warn(`⚠ could not ${action} \`in-progress\` on #${issue} — run by hand: ${recovery}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// `issue_add_label` creates the label when the repository lacks it (`lane-dispatch.ts`), so no
|
|
22
|
+
// separate provisioning call is made here.
|
|
23
|
+
async function mark(issue: string): Promise<boolean> {
|
|
24
|
+
const is_marked = await git_gh_command.issue_add_label(issue, IN_PROGRESS_LABEL)
|
|
25
|
+
|
|
26
|
+
if (!is_marked) warn('apply', issue, MARK_RECOVERY)
|
|
27
|
+
|
|
28
|
+
return is_marked
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The removal is `followup`'s own — read first, removed only when present, under the stored
|
|
32
|
+
// casing — so an issue that never carried the label is never written to.
|
|
33
|
+
async function unmark(issue: string): Promise<boolean> {
|
|
34
|
+
try {
|
|
35
|
+
await git_followup_label.strip_in_progress(issue)
|
|
36
|
+
|
|
37
|
+
return true
|
|
38
|
+
} catch {
|
|
39
|
+
warn('remove', issue, UNMARK_RECOVERY)
|
|
40
|
+
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const run_label = {
|
|
46
|
+
mark,
|
|
47
|
+
unmark,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { run_label }
|
|
@@ -48,7 +48,10 @@ interface MergeContext {
|
|
|
48
48
|
child: string
|
|
49
49
|
epic: string | undefined
|
|
50
50
|
repo: string | undefined
|
|
51
|
-
|
|
51
|
+
// The hand-off threshold, or `undefined` where no AI session owns the loop — the supervisor's
|
|
52
|
+
// `backlog:drive` has no context to cut, so a merge there is never handed back as `over`
|
|
53
|
+
// (joshuafolkken/kit#3156).
|
|
54
|
+
over: number | undefined
|
|
52
55
|
owner: CarryOwner
|
|
53
56
|
// The child's transcript output path, when the caller passed `--output`. Read to tell an API-outage
|
|
54
57
|
// ending apart from a genuine child failure (joshuafolkken/kit#2240); absent leaves outage detection
|
|
@@ -327,8 +330,10 @@ async function resume_cut(child: string): Promise<boolean> {
|
|
|
327
330
|
}
|
|
328
331
|
|
|
329
332
|
// The hand-off check, asked at a merge alone. A subprocess that cannot measure exits non-zero, which
|
|
330
|
-
// is read as `over` — "could not measure" is never "still cheap".
|
|
331
|
-
async function is_over_budget(over: number): Promise<boolean> {
|
|
333
|
+
// is read as `over` — "could not measure" is never "still cheap". No threshold is no session to measure.
|
|
334
|
+
async function is_over_budget(over: number | undefined): Promise<boolean> {
|
|
335
|
+
if (over === undefined) return false
|
|
336
|
+
|
|
332
337
|
const result = await josh(['cost', '--over', String(over)])
|
|
333
338
|
|
|
334
339
|
return result.code !== 0 || result.out === OVER
|
package/scripts/run/run-merge.ts
CHANGED
|
@@ -35,7 +35,7 @@ const ONE = 1
|
|
|
35
35
|
// - `merged` — CLOSED; the child finished and its pull request merged. Also an OPEN, unparked
|
|
36
36
|
// child a merged pull request's `closes #N` names — GitHub merged it without closing
|
|
37
37
|
// the issue (joshuafolkken/kit#2769).
|
|
38
|
-
// - `human-review` — OPEN and carrying `needs-human-review`; the run's own ending (
|
|
38
|
+
// - `human-review` — OPEN and carrying `needs-human-review`; the run's own ending (needs-human-review.md).
|
|
39
39
|
// - `parked` — OPEN and carrying `needs-decision` or `already-done`; a person still owns it.
|
|
40
40
|
// - `split` — OPEN and carrying `epic`; its work was divided into new children.
|
|
41
41
|
// - `outage` — OPEN and carrying neither, but the exit record shows it could not reach the API; not
|
|
@@ -8,6 +8,7 @@ import { error_text } from '#scripts/lib/error-message'
|
|
|
8
8
|
import { latest_scope_cli } from '#scripts/version/latest-scope-cli'
|
|
9
9
|
import { run_prep, type PrepParts } from './run-prep'
|
|
10
10
|
import { run_prep_locate } from './run-prep-locate'
|
|
11
|
+
import { run_ship_preflight } from './run-ship-preflight'
|
|
11
12
|
|
|
12
13
|
// `josh run:prep <N>` — one call for the reads a `fullrun` makes before its first edit
|
|
13
14
|
// (joshuafolkken/kit#1978): the issue body and comments (`issue:read`), the state, labels and
|
|
@@ -41,6 +42,7 @@ interface PrepReads {
|
|
|
41
42
|
latest: LatestDecision
|
|
42
43
|
has_changes: boolean
|
|
43
44
|
locations: string
|
|
45
|
+
ship_problems: ReadonlyArray<string>
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
// Exactly one issue number, or the call is refused: `run:prep` prepares one run, and a second number
|
|
@@ -91,15 +93,16 @@ async function lane_has_changes(): Promise<boolean> {
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
async function gather(issue_number: string): Promise<PrepReads> {
|
|
94
|
-
const [content, state, has_changes] = await Promise.all([
|
|
96
|
+
const [content, state, has_changes, ship_problems] = await Promise.all([
|
|
95
97
|
issue_read_cli.read_block(issue_number),
|
|
96
98
|
issue_state_cli.read_issue(issue_number),
|
|
97
99
|
lane_has_changes(),
|
|
100
|
+
run_ship_preflight.ahead(issue_number),
|
|
98
101
|
])
|
|
99
102
|
// Located from the body just read, so it waits for that read rather than joining the batch above.
|
|
100
103
|
const locations = await run_prep_locate.locate(content.kind === 'ok' ? content.block : '')
|
|
101
104
|
|
|
102
|
-
return { content, state, latest: latest_decision(), has_changes, locations }
|
|
105
|
+
return { content, state, latest: latest_decision(), has_changes, locations, ship_problems }
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
function content_body(issue_number: string, content: BlockRead): string {
|
|
@@ -119,6 +122,7 @@ function to_parts(issue_number: string, reads: PrepReads): PrepParts {
|
|
|
119
122
|
latest_reason: reads.latest.reason,
|
|
120
123
|
has_changes: reads.has_changes,
|
|
121
124
|
locations: reads.locations,
|
|
125
|
+
ship_problems: reads.ship_problems,
|
|
122
126
|
}
|
|
123
127
|
}
|
|
124
128
|
|
package/scripts/run/run-prep.ts
CHANGED
|
@@ -24,6 +24,10 @@ const CONTENT_HEADER = '=== issue ==='
|
|
|
24
24
|
const STATE_HEADER = '=== state ==='
|
|
25
25
|
const LATEST_HEADER = '=== dependency update ==='
|
|
26
26
|
const LOCATIONS_HEADER = '=== code locations ==='
|
|
27
|
+
const SHIP_HEADER = '=== ship preconditions ==='
|
|
28
|
+
const SHIP_MET = 'met — nothing `josh ship` refuses on as the tree stands'
|
|
29
|
+
const SHIP_UNMET =
|
|
30
|
+
'unmet — `josh ship` will refuse until these are fixed; fix them before the gate:'
|
|
27
31
|
const SECTION_SEPARATOR = '\n\n'
|
|
28
32
|
const LATEST_JOINER = ' — '
|
|
29
33
|
|
|
@@ -41,6 +45,9 @@ interface PrepParts {
|
|
|
41
45
|
// Whether a lane child's tree holds uncommitted work — read for the verdict, not printed in the
|
|
42
46
|
// report (joshuafolkken/kit#2476).
|
|
43
47
|
has_changes: boolean
|
|
48
|
+
// What `josh ship` would refuse on as the tree stands — `run_ship_preflight.ahead`
|
|
49
|
+
// (joshuafolkken/kit#3154).
|
|
50
|
+
ship_problems: ReadonlyArray<string>
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
function human_review_value(state: IssueState | undefined): string {
|
|
@@ -72,6 +79,12 @@ function section(header: string, body: string): string {
|
|
|
72
79
|
return `${header}\n${body}`
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
function ship_body(problems: ReadonlyArray<string>): string {
|
|
83
|
+
if (problems.length === 0) return SHIP_MET
|
|
84
|
+
|
|
85
|
+
return [SHIP_UNMET, ...problems.map((problem) => ` - ${problem}`)].join('\n')
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
function format_report(parts: PrepParts): string {
|
|
76
89
|
return [
|
|
77
90
|
summary(parts),
|
|
@@ -79,6 +92,7 @@ function format_report(parts: PrepParts): string {
|
|
|
79
92
|
section(STATE_HEADER, state_body(parts)),
|
|
80
93
|
section(LATEST_HEADER, `${parts.latest_scope}${LATEST_JOINER}${parts.latest_reason}`),
|
|
81
94
|
section(LOCATIONS_HEADER, parts.locations),
|
|
95
|
+
section(SHIP_HEADER, ship_body(parts.ship_problems)),
|
|
82
96
|
].join(SECTION_SEPARATOR)
|
|
83
97
|
}
|
|
84
98
|
|
|
@@ -86,9 +100,12 @@ const run_prep = {
|
|
|
86
100
|
CONTENT_HEADER,
|
|
87
101
|
LATEST_HEADER,
|
|
88
102
|
LOCATIONS_HEADER,
|
|
103
|
+
SHIP_HEADER,
|
|
104
|
+
SHIP_UNMET,
|
|
89
105
|
STATE_HEADER,
|
|
90
106
|
SUMMARY_PREFIX,
|
|
91
107
|
format_report,
|
|
108
|
+
ship_body,
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
export type { PrepParts }
|
|
@@ -7,6 +7,7 @@ import { run_event_stream_emit } from './run-event-stream-emit'
|
|
|
7
7
|
import { run_ship, type ShipSection } from './run-ship'
|
|
8
8
|
import { run_ship_detach } from './run-ship-detach'
|
|
9
9
|
import { run_ship_next } from './run-ship-next'
|
|
10
|
+
import { run_ship_pre_detach } from './run-ship-pre-detach'
|
|
10
11
|
import { run_ship_probe } from './run-ship-probe'
|
|
11
12
|
import { run_ship_return } from './run-ship-return'
|
|
12
13
|
import { run_ship_stage, type Phase, type ShipState, type Stage } from './run-ship-stage'
|
|
@@ -171,8 +172,9 @@ function parse(argv: ReadonlyArray<string>): ShipCommand | undefined {
|
|
|
171
172
|
|
|
172
173
|
async function run_step(step: Step, args: ShipArguments, state: ShipState): Promise<ShipSection> {
|
|
173
174
|
const result = await step.run(args, state)
|
|
175
|
+
const section = { header: step.header, body: result.out, code: result.code }
|
|
174
176
|
|
|
175
|
-
return
|
|
177
|
+
return result.conflicts === undefined ? section : { ...section, conflicts: result.conflicts }
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
async function emit_phase(args: ShipArguments, stage: Stage, phase: Phase): Promise<void> {
|
|
@@ -249,7 +251,14 @@ async function stopped(
|
|
|
249
251
|
stage: Stage,
|
|
250
252
|
): Promise<ReadonlyArray<ShipSection>> {
|
|
251
253
|
if (run_ship_detach.is_supervised()) {
|
|
252
|
-
|
|
254
|
+
const conflicts = sections.at(-1)?.conflicts
|
|
255
|
+
const resume = run_ship_next.resume_of(args)
|
|
256
|
+
|
|
257
|
+
await run_ship_return.return_control(
|
|
258
|
+
args.number,
|
|
259
|
+
stage,
|
|
260
|
+
conflicts === undefined ? resume : { ...resume, conflicts },
|
|
261
|
+
)
|
|
253
262
|
}
|
|
254
263
|
|
|
255
264
|
return sections
|
|
@@ -324,14 +333,26 @@ function should_detach(args: ShipArguments): boolean {
|
|
|
324
333
|
// joshuafolkken/kit#2966: the preflight is deterministic and fast, so it runs here, in the agent's own
|
|
325
334
|
// turn, before the hand-off — a stop it finds is fixed by the same session instead of relaunching one
|
|
326
335
|
// from the supervisor. Only the slow stages go to the supervisor, which re-asks the preflight itself.
|
|
336
|
+
// joshuafolkken/kit#3222: the type check and the document tests follow it here for the same reason; they
|
|
337
|
+
// are not a stage, so the supervisor's gate runs them again rather than passing over them.
|
|
338
|
+
async function pre_detach_sections(args: ShipArguments): Promise<ReadonlyArray<ShipSection>> {
|
|
339
|
+
const preflight = await run_stage(run_ship_steps.PREFLIGHT_STEP, args, await open_context(args))
|
|
340
|
+
if (preflight.code !== SUCCESS_EXIT_CODE) return [preflight]
|
|
341
|
+
|
|
342
|
+
const result = await run_ship_pre_detach.checks()
|
|
343
|
+
|
|
344
|
+
return [preflight, { header: run_ship.PRE_DETACH_HEADER, body: result.out, code: result.code }]
|
|
345
|
+
}
|
|
346
|
+
|
|
327
347
|
async function detach_after_preflight(args: ShipArguments): Promise<number> {
|
|
328
|
-
const
|
|
348
|
+
const sections = await pre_detach_sections(args)
|
|
349
|
+
const code = run_ship.exit_code(sections)
|
|
329
350
|
|
|
330
|
-
if (
|
|
351
|
+
if (code === SUCCESS_EXIT_CODE) return await detach(args)
|
|
331
352
|
|
|
332
|
-
console.info(run_ship.format_report(
|
|
353
|
+
console.info(run_ship.format_report(sections))
|
|
333
354
|
|
|
334
|
-
return
|
|
355
|
+
return code
|
|
335
356
|
}
|
|
336
357
|
|
|
337
358
|
async function supervised_repository(): Promise<string | undefined> {
|
|
@@ -42,6 +42,8 @@ interface ShipResume {
|
|
|
42
42
|
title: string
|
|
43
43
|
flags: ReadonlyArray<string>
|
|
44
44
|
is_review: boolean
|
|
45
|
+
// The paths a merge of the default branch left unmerged (joshuafolkken/kit#3221).
|
|
46
|
+
conflicts?: ReadonlyArray<string>
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
// Single quotes, so a `$` or a backtick in a title is never expanded by the shell that runs it.
|
|
@@ -90,14 +92,25 @@ const AFTER_FIX: Record<Stage, (resume: ShipResume) => string> = {
|
|
|
90
92
|
[STAGE.PREFLIGHT]: detach_as_started,
|
|
91
93
|
[STAGE.REVIEW]: detach_as_started,
|
|
92
94
|
[STAGE.GATE]: detach_as_started,
|
|
95
|
+
[STAGE.SYNC]: detach_as_started,
|
|
93
96
|
[STAGE.COMMIT]: detach_as_started,
|
|
94
97
|
[STAGE.ROUND_TWO]: detach_again,
|
|
95
98
|
[STAGE.FOLLOWUP]: detach_again,
|
|
96
99
|
[STAGE.REPORT]: detach_again,
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
// A conflict names its paths (joshuafolkken/kit#3221), so the resumed session resolves them without
|
|
103
|
+
// reading the report or asking git which files are unmerged.
|
|
104
|
+
function conflict_step(conflicts: ReadonlyArray<string>): string {
|
|
105
|
+
return `Resolve the conflict with origin/main in ${conflicts.join(', ')} (the merge is left in progress: remove the conflict markers in those files), then`
|
|
106
|
+
}
|
|
107
|
+
|
|
99
108
|
// Two lines' worth: read the stopped report, then the one command that resumes after the fix.
|
|
100
109
|
function next_step(issue: string, stage: Stage, resume: ShipResume): string {
|
|
110
|
+
const conflicts = resume.conflicts ?? []
|
|
111
|
+
|
|
112
|
+
if (conflicts.length > 0) return `${conflict_step(conflicts)} ${AFTER_FIX[stage](resume)}`
|
|
113
|
+
|
|
101
114
|
return `Read the stopped report with \`${COMMAND.LOG} ${issue}\`, fix the ${stage} failure, then ${AFTER_FIX[stage](resume)}`
|
|
102
115
|
}
|
|
103
116
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { type_check_step } from '#scripts/gate/type-check-step'
|
|
4
|
+
import type { JoshResult } from '#scripts/josh/josh-run'
|
|
5
|
+
import { buffered_process, type BufferedProcessResult } from '#scripts/lib/buffered-process'
|
|
6
|
+
|
|
7
|
+
// The fast checks a detached `josh ship` runs in the agent's own turn, after the preflight and before
|
|
8
|
+
// the hand-off (joshuafolkken/kit#3222). In one measured backlogrun about 7 of 31 lane resumes were a
|
|
9
|
+
// gate failure first seen after the detach — a type error in a file the related-only checks never
|
|
10
|
+
// touched, or a document test (a SKILL.md pointer, a byte budget, the command list) that only the full
|
|
11
|
+
// suite runs — each paid for with a relaunched session. These two answer in seconds, so the same
|
|
12
|
+
// session fixes what they find and ships again. **Nothing moves out of the gate**: the supervised gate
|
|
13
|
+
// stage still runs every check, these included.
|
|
14
|
+
//
|
|
15
|
+
// The test targets are the directories those resumes failed in. A consumer has none of them, so only
|
|
16
|
+
// the ones present run, and a consumer meets the type check alone.
|
|
17
|
+
|
|
18
|
+
const SUCCESS_EXIT_CODE = 0
|
|
19
|
+
const FAILURE_EXIT_CODE = 1
|
|
20
|
+
const SECTION_SEPARATOR = '\n\n'
|
|
21
|
+
const PASSED = 'type check and document tests passed'
|
|
22
|
+
const DOCUMENT_TEST_TARGETS: ReadonlyArray<string> = [
|
|
23
|
+
'scripts/document',
|
|
24
|
+
'scripts/claude',
|
|
25
|
+
'scripts/rules',
|
|
26
|
+
'scripts/josh/josh-command-map.test.ts',
|
|
27
|
+
]
|
|
28
|
+
// A present directory need not hold a test the project's Vitest includes — a consumer's own
|
|
29
|
+
// `scripts/claude` of shell helpers, say — and a filter that matches nothing would stop every ship.
|
|
30
|
+
const DOCUMENT_TEST_COMMAND: ReadonlyArray<string> = ['josh', 'test:unit', '--passWithNoTests']
|
|
31
|
+
|
|
32
|
+
function present_targets(directory: string): ReadonlyArray<string> {
|
|
33
|
+
return DOCUMENT_TEST_TARGETS.filter((target) => existsSync(path.join(directory, target)))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function command_lists(directory: string): Promise<Array<ReadonlyArray<string>>> {
|
|
37
|
+
const type_check = await type_check_step.resolve_type_check_args(directory)
|
|
38
|
+
const targets = present_targets(directory)
|
|
39
|
+
|
|
40
|
+
return targets.length === 0 ? [type_check] : [type_check, [...DOCUMENT_TEST_COMMAND, ...targets]]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function run_in(
|
|
44
|
+
directory: string,
|
|
45
|
+
list: ReadonlyArray<string>,
|
|
46
|
+
): Promise<BufferedProcessResult> {
|
|
47
|
+
return await buffered_process.run_buffered_process(list, { cwd: directory })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Both run at once, buffered, and every failure is reported, so one ship shows all there is to fix.
|
|
51
|
+
async function checks(directory: string = process.cwd()): Promise<JoshResult> {
|
|
52
|
+
const lists = await command_lists(directory)
|
|
53
|
+
const results = await Promise.all(lists.map(async (list) => await run_in(directory, list)))
|
|
54
|
+
const failed = results.filter((result) => buffered_process.is_process_failed(result))
|
|
55
|
+
|
|
56
|
+
if (failed.length === 0) return { code: SUCCESS_EXIT_CODE, out: PASSED }
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
code: FAILURE_EXIT_CODE,
|
|
60
|
+
out: failed.map((result) => result.output.trim()).join(SECTION_SEPARATOR),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const run_ship_pre_detach = { DOCUMENT_TEST_TARGETS, PASSED, checks }
|
|
65
|
+
|
|
66
|
+
export { run_ship_pre_detach }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
3
|
+
import { git_pr } from '#scripts/gh/git-pr'
|
|
3
4
|
import { changed_paths } from '#scripts/git/changed-paths'
|
|
4
5
|
import { git_branch } from '#scripts/git/git-branch'
|
|
5
6
|
import { git_preflight } from '#scripts/git/git-preflight'
|
|
@@ -22,6 +23,7 @@ import { run_ship_scoped } from './run-ship-scoped'
|
|
|
22
23
|
const SUCCESS_EXIT_CODE = 0
|
|
23
24
|
const FAILURE_EXIT_CODE = 1
|
|
24
25
|
const EVIDENCE_PROBLEM = `This change touches runtime code, but the PR body has no ${live_evidence.EVIDENCE_HEADING} section: run the acceptance criteria for real and pass the body with --body-file <path>.`
|
|
26
|
+
const EVIDENCE_PENDING = `This change touches runtime code and no PR is open yet: write the ${live_evidence.EVIDENCE_HEADING} section to a file and hand it to josh ship with --body-file <path>, or the ship refuses.`
|
|
25
27
|
|
|
26
28
|
interface PreflightRequest {
|
|
27
29
|
title: string
|
|
@@ -34,13 +36,20 @@ async function body_text(body_path: string | undefined): Promise<string | undefi
|
|
|
34
36
|
return await git_gh_command.pr_get_body(await git_branch.current())
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
// `no_body_problem` answers a runtime change with no body to read at all — no `--body-file` and no open
|
|
40
|
+
// PR, which is every run before its ship opens one.
|
|
41
|
+
async function evidence_problems(
|
|
42
|
+
body_path: string | undefined,
|
|
43
|
+
no_body_problem: string = EVIDENCE_PROBLEM,
|
|
44
|
+
): Promise<Array<string>> {
|
|
38
45
|
const [paths, body] = await Promise.all([
|
|
39
46
|
changed_paths.read_changed_paths(false),
|
|
40
47
|
body_text(body_path),
|
|
41
48
|
])
|
|
42
49
|
|
|
43
|
-
|
|
50
|
+
if (live_evidence.verdict_for(paths, body) !== 'required') return []
|
|
51
|
+
|
|
52
|
+
return [body === undefined ? no_body_problem : EVIDENCE_PROBLEM]
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
function refusal(problems: ReadonlyArray<string>): JoshResult {
|
|
@@ -81,7 +90,29 @@ async function stage(request: PreflightRequest): Promise<JoshResult> {
|
|
|
81
90
|
return scoped.code === SUCCESS_EXIT_CODE ? { code: SUCCESS_EXIT_CODE, out: 'ready' } : scoped
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
|
|
93
|
+
async function classification_problems(issue_number: string): Promise<Array<string>> {
|
|
94
|
+
await git_pr.release_classification(issue_number, await git_branch.current())
|
|
95
|
+
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// **The same two refusals, asked before the ship rather than by it** (joshuafolkken/kit#3154). In one
|
|
100
|
+
// measured backlogrun 20 of 41 lane `ship` refusals were a missing classification or missing evidence —
|
|
101
|
+
// facts fixed long before the ship, refused at the run's largest context. `run:prep` (and so `run:entry`)
|
|
102
|
+
// and `run:step` print this, so the Issue body and the evidence are met while they are cheap. Nothing
|
|
103
|
+
// here replaces `stage`: the ship and `followup` still refuse on both. Before the PR exists the evidence
|
|
104
|
+
// can only live in the file the ship will be handed, so it is asked for as that file, not reported as a
|
|
105
|
+
// body that already failed.
|
|
106
|
+
async function ahead(issue_number: string): Promise<Array<string>> {
|
|
107
|
+
const [classification, evidence] = await Promise.all([
|
|
108
|
+
caught(classification_problems(issue_number)),
|
|
109
|
+
caught(evidence_problems(undefined, EVIDENCE_PENDING)),
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
return [...classification, ...evidence]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const run_ship_preflight = { EVIDENCE_PENDING, EVIDENCE_PROBLEM, ahead, stage }
|
|
85
116
|
|
|
86
117
|
export type { PreflightRequest }
|
|
87
118
|
export { run_ship_preflight }
|
|
@@ -6,7 +6,7 @@ import { lane_relaunch } from '#scripts/lane/lane-relaunch'
|
|
|
6
6
|
import { run_event_stream } from './run-event-stream'
|
|
7
7
|
import { run_event_stream_emit } from './run-event-stream-emit'
|
|
8
8
|
import type { ShipResume } from './run-ship-next'
|
|
9
|
-
import type
|
|
9
|
+
import { run_ship_stage, type Stage } from './run-ship-stage'
|
|
10
10
|
|
|
11
11
|
// How a detached `josh ship` supervisor hands a stopped stage back (joshuafolkken/kit#2428). The agent
|
|
12
12
|
// ended when it handed the region over, so a red gate, a High/Medium review, a failed push or red CI has
|
|
@@ -28,8 +28,8 @@ const OPENAI_PROVIDER = 'openai'
|
|
|
28
28
|
|
|
29
29
|
type ReturnOutcome = 'relaunched' | 'recorded'
|
|
30
30
|
|
|
31
|
-
function stop_text(issue: string, stage: Stage): string {
|
|
32
|
-
return `#${issue} ${stage} failed — pnpm josh ship --log ${issue}`
|
|
31
|
+
function stop_text(issue: string, stage: Stage, resume: ShipResume): string {
|
|
32
|
+
return `#${issue} ${run_ship_stage.stop_reason(stage, resume.conflicts)} failed — pnpm josh ship --log ${issue}`
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function can_relaunch(lane: LaneInfo | undefined): lane is LaneInfo {
|
|
@@ -53,7 +53,10 @@ async function return_control(
|
|
|
53
53
|
stage: Stage,
|
|
54
54
|
resume: ShipResume,
|
|
55
55
|
): Promise<ReturnOutcome> {
|
|
56
|
-
await run_event_stream_emit.emit(
|
|
56
|
+
await run_event_stream_emit.emit(
|
|
57
|
+
run_event_stream.EVENT_KIND.SHIP_STOP,
|
|
58
|
+
stop_text(issue, stage, resume),
|
|
59
|
+
)
|
|
57
60
|
|
|
58
61
|
const lane = await lane_to_relaunch(issue)
|
|
59
62
|
|
|
@@ -155,7 +155,7 @@ async function record_and_route(
|
|
|
155
155
|
if (recorded.code !== SUCCESS_EXIT_CODE) return recorded
|
|
156
156
|
|
|
157
157
|
const outcome = outcome_of(verdict)
|
|
158
|
-
const out = [...verdict.
|
|
158
|
+
const out = [...verdict.report, outcome.note].join('\n')
|
|
159
159
|
|
|
160
160
|
return outcome.is_passing ? { code: SUCCESS_EXIT_CODE, out } : failure(out)
|
|
161
161
|
}
|