@joshuafolkken/kit 1.1471.0 → 1.1514.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 +6 -6
- package/.claude/skills/epic-commands/SKILL.md +2 -14
- package/.claude/skills/epic-commands/epic-bundle.md +9 -5
- package/.claude/skills/epic-commands/execution-waves.md +1 -2
- package/.claude/skills/verify-ui/SKILL.md +1 -2
- package/.claude/skills/workflow-commands/SKILL.md +7 -9
- package/.claude/skills/workflow-commands/background-commands.md +3 -7
- package/.claude/skills/workflow-commands/backlogrun-child.md +3 -10
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -15
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -8
- package/.claude/skills/workflow-commands/backlogrun-progress.md +10 -19
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +8 -8
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -10
- package/.claude/skills/workflow-commands/backlogrun.md +31 -50
- package/.claude/skills/workflow-commands/chain-rule.md +7 -6
- package/.claude/skills/workflow-commands/delegation.md +12 -8
- package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
- package/.claude/skills/workflow-commands/followup.md +3 -6
- package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
- package/.claude/skills/workflow-commands/fullrun.md +4 -6
- package/.claude/skills/workflow-commands/halfrun.md +3 -7
- package/.claude/skills/workflow-commands/into-target.md +1 -2
- package/.claude/skills/workflow-commands/issue-comments.md +3 -3
- package/.claude/skills/workflow-commands/issue-scout.md +3 -2
- package/.claude/skills/workflow-commands/latest-gate.md +2 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
- package/.claude/skills/workflow-commands/observation-filing.md +0 -14
- package/.claude/skills/workflow-commands/observation-ledger.md +4 -9
- package/.claude/skills/workflow-commands/pre-gate-cut.md +11 -16
- package/.claude/skills/workflow-commands/prerequisite.md +1 -2
- package/.claude/skills/workflow-commands/progress-watcher.md +2 -7
- package/.claude/skills/workflow-commands/prrun.md +3 -5
- package/.claude/skills/workflow-commands/retrospective.md +4 -4
- package/.claude/skills/workflow-commands/split-assessment.md +1 -3
- package/.claude/skills/workflow-commands/target-repository.md +1 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +2 -4
- package/.codex/hooks.json +6 -6
- package/.github/actions/setup-node/action.yml +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CLAUDE.md +1 -1
- package/cspell/index.yaml +7 -0
- package/dist/CLAUDE.md +1 -1
- package/dist/commands/document-section-cli.js +67 -34
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/chunk-4KKD2UTK.js +66 -0
- package/dist/hooks/{chunk-J47HU5H5.js → chunk-6TS2JD6D.js} +3 -3
- package/dist/hooks/chunk-7DSBJVFH.js +50 -0
- package/dist/hooks/{chunk-YHBZAJSV.js → chunk-A65Y4YOR.js} +3 -3
- package/dist/hooks/{chunk-E7OIKX5Z.js → chunk-APUO6U4J.js} +85 -7
- package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
- package/dist/hooks/chunk-GP4IEC4O.js +20 -0
- package/dist/hooks/{chunk-SHGPANFB.js → chunk-JP45SOCJ.js} +3 -3
- package/dist/hooks/{chunk-K4EOONYM.js → chunk-LAOV7NLM.js} +1 -1
- package/dist/hooks/chunk-M4HGZV7U.js +83 -0
- package/dist/hooks/{chunk-KC5AK2GF.js → chunk-O6GDRNJB.js} +3 -3
- package/dist/hooks/{chunk-YYPI7SJZ.js → chunk-PF2NZL2A.js} +5 -5
- package/dist/hooks/{chunk-H5HVWYXK.js → chunk-PTICX4OL.js} +3 -4
- package/dist/hooks/{chunk-IIGFYXKC.js → chunk-RT27QRJW.js} +581 -1438
- package/dist/hooks/{chunk-43ZHT22P.js → chunk-SYOTDYCI.js} +1127 -1089
- package/dist/hooks/{chunk-AS74OU3D.js → chunk-TNUMY6NL.js} +1507 -1336
- package/dist/hooks/chunk-UPUZF3AN.js +279 -0
- package/dist/hooks/{chunk-EUMACUK2.js → chunk-WK6SMBIC.js} +1948 -806
- package/dist/hooks/codex-hook-adapter.js +17 -14
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +5 -4
- package/dist/hooks/inputs.json +317 -264
- package/dist/hooks/investigation-guard.js +8 -11
- package/dist/hooks/pretool-guard.js +16 -13
- package/dist/hooks/session-lang.js +370 -25
- package/dist/hooks/stop-guard.js +2576 -176
- package/dist/josh.js +2124 -277
- package/lefthook/base.yml +5 -3
- package/package.json +12 -10
- package/prompts/collaboration-workflow/file-edits.md +1 -3
- package/prompts/collaboration-workflow/gh-rest.md +1 -2
- package/prompts/collaboration-workflow/issue-citation.md +2 -4
- package/prompts/collaboration-workflow/issue-template.md +10 -2
- package/prompts/collaboration-workflow/lane-instructions.md +35 -0
- package/prompts/collaboration-workflow/operating-rules.md +2 -6
- package/prompts/collaboration-workflow/output-bounds.md +3 -3
- package/prompts/collaboration-workflow/overview.md +1 -1
- package/prompts/collaboration-workflow/principles.md +14 -8
- package/prompts/collaboration-workflow/report-format.md +9 -0
- package/prompts/collaboration-workflow/residency.md +12 -8
- package/prompts/collaboration-workflow/rule-delivery.md +3 -11
- package/prompts/collaboration-workflow/shell-body.md +6 -6
- package/prompts/collaboration-workflow/turn-batching.md +5 -5
- package/prompts/collaboration-workflow/upstream-interrupt.md +2 -2
- package/prompts/collaboration-workflow/wip-cap.md +6 -6
- package/prompts/collaboration-workflow.md +1 -0
- package/prompts/review.md +4 -5
- package/prompts/testing-guide.md +2 -2
- package/scripts/agent/agent-argv.ts +20 -4
- package/scripts/agent/agent-role-profile.ts +101 -45
- package/scripts/agent/agent-safe-text.ts +60 -0
- package/scripts/agent/claude-agent-argv.ts +14 -0
- package/scripts/agent/codex-agent-argv.ts +2 -0
- package/scripts/agent/lane-instructions.ts +144 -0
- package/scripts/backlog/backlog-budget.ts +4 -4
- package/scripts/backlog/backlog-drive-named.ts +1 -1
- package/scripts/backlog/backlog-drive-restore.ts +9 -1
- package/scripts/backlog/backlog-occupied.ts +56 -0
- package/scripts/backlog/backlog-ready.ts +47 -10
- package/scripts/delegation/duplicate-read-guard.ts +2 -2
- package/scripts/delegation/investigation-guard.ts +15 -65
- package/scripts/delegation/investigation-reads.ts +0 -1
- package/scripts/document/document-byte-budget.ts +6 -5
- package/scripts/document/document-section-cli.ts +17 -5
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/document/read-set-cli.ts +4 -1
- package/scripts/epic/epic-audit-orphans.ts +5 -8
- package/scripts/epic/epic-audit.ts +52 -1
- package/scripts/epic/epic-bundle-evidence.ts +49 -26
- package/scripts/epic/epic-bundle-referenced.ts +14 -3
- package/scripts/epic/epic-bundle.ts +17 -9
- package/scripts/followup/git-followup-workflow.ts +19 -1
- package/scripts/gate/check-inputs.ts +129 -0
- package/scripts/gate/check-reuse.ts +162 -0
- package/scripts/gate/core-budget.ts +93 -44
- package/scripts/gate/core-wait.ts +130 -0
- package/scripts/gate/free-cores.ts +49 -0
- package/scripts/gate/gate-ledger.ts +17 -3
- package/scripts/gate/gate-plan.ts +23 -1
- package/scripts/gate/gate-report.ts +15 -2
- package/scripts/gate/gate-run-steps.ts +150 -0
- package/scripts/gate/held-mark.ts +55 -0
- package/scripts/gate/machine-capacity.ts +70 -11
- package/scripts/gate/pre-commit-type-check.ts +14 -1
- package/scripts/gate/verification-gate.ts +58 -125
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +24 -2
- package/scripts/gh/git-pr.ts +13 -1
- package/scripts/git/main-merge-regenerate.ts +78 -0
- package/scripts/git/main-merge.ts +14 -5
- package/scripts/hooks/batch-guard.ts +5 -8
- package/scripts/hooks/pre-commit-lint.ts +91 -0
- package/scripts/hooks/pre-push-unit.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +27 -52
- package/scripts/hooks/reserved-run.ts +1 -0
- package/scripts/hooks/run-hook.sh +10 -12
- package/scripts/hooks/stop-actions.ts +204 -0
- package/scripts/hooks/stop-guard.ts +62 -21
- package/scripts/init/hook-launch.ts +12 -7
- package/scripts/issue/expected-effect-lint.ts +59 -0
- package/scripts/issue/issue-cite-cli.ts +35 -15
- package/scripts/issue/issue-classification.ts +1 -1
- package/scripts/issue/issue-lint-cli.ts +5 -3
- package/scripts/issue/issue-scout-cli.ts +19 -14
- package/scripts/issue/markdown-section.ts +19 -5
- package/scripts/josh/command-usage.ts +110 -0
- package/scripts/josh/josh-command-map.ts +2 -0
- package/scripts/josh/josh-command-types.ts +55 -9
- package/scripts/josh/josh-commands-ai.ts +13 -0
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-hooks.ts +6 -0
- package/scripts/josh/josh-commands-lane.ts +10 -1
- package/scripts/josh/josh-commands-workflow.ts +5 -0
- package/scripts/josh/josh-logic.ts +6 -15
- package/scripts/josh/josh-usage-automation.ts +121 -0
- package/scripts/josh/josh-usage-backlog.ts +180 -0
- package/scripts/josh/josh-usage-developer.ts +230 -0
- package/scripts/josh/josh-usage-hooks.ts +38 -0
- package/scripts/josh/josh-usage-issue.ts +205 -0
- package/scripts/josh/josh-usage-review.ts +101 -0
- package/scripts/josh/josh-usage-run.ts +252 -0
- package/scripts/josh/josh-usage-session.ts +238 -0
- package/scripts/josh/josh-usage-workflow.ts +212 -0
- package/scripts/josh/josh.ts +14 -5
- package/scripts/josh/package-directory.ts +20 -0
- package/scripts/lane/lane-capacity.ts +8 -2
- package/scripts/lane/lane-check-stats.ts +56 -0
- package/scripts/lane/lane-child-invocation.ts +35 -4
- package/scripts/lane/lane-dispatch.ts +3 -3
- package/scripts/lane/lane-guard-policy.ts +2 -2
- package/scripts/lane/lane-handoff.ts +8 -1
- package/scripts/lane/lane-install.ts +12 -4
- package/scripts/lane/lane-ledger.ts +43 -1
- package/scripts/lane/lane-limit-cli.ts +8 -9
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-preserve.ts +29 -1
- package/scripts/lane/lane-registry.ts +9 -7
- package/scripts/lane/lane-restart.ts +5 -26
- package/scripts/lane/lane-session-action.ts +146 -0
- package/scripts/lane/lane-session-broker.ts +57 -138
- package/scripts/lane/lane-withdraw-cli.ts +71 -0
- package/scripts/lane/lane-withdraw.ts +182 -0
- package/scripts/lint/format.ts +4 -4
- package/scripts/lint/lint-parallel.ts +10 -3
- package/scripts/lint/lint-related-scope.ts +4 -1
- package/scripts/notify/telegram-test.ts +10 -5
- package/scripts/report/report-format-reference.ts +2 -0
- package/scripts/review/acceptance-map-check.ts +61 -0
- package/scripts/review/acceptance-map.ts +205 -0
- package/scripts/review/review-brief-cli.ts +1 -1
- package/scripts/rules/agent-model.ts +59 -0
- package/scripts/rules/delivered-rules.ts +11 -2
- package/scripts/rules/filing-cap.ts +1 -0
- package/scripts/rules/firing-point.ts +6 -5
- package/scripts/rules/guard-remedy.ts +117 -0
- package/scripts/rules/issue-comments.ts +67 -6
- package/scripts/rules/last-prompt.ts +50 -5
- package/scripts/rules/oracle-list-cli.ts +7 -0
- package/scripts/rules/piped-verification.ts +3 -1
- package/scripts/rules/rule-list.ts +4 -1
- package/scripts/rules/rule-registry-rows.ts +10 -0
- package/scripts/rules/rule-registry.ts +7 -4
- package/scripts/rules/rule-rewrite.ts +114 -0
- package/scripts/rules/rule-value.ts +7 -1
- package/scripts/rules/shell-segments.ts +15 -0
- package/scripts/rules/stop-decision.ts +77 -0
- package/scripts/rules/stop-rules.ts +147 -47
- package/scripts/rules/tail-commands.ts +15 -1
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-closed.ts +8 -2
- package/scripts/run/board/run-board-github.ts +9 -1
- package/scripts/run/board/run-board-header.ts +4 -1
- package/scripts/run/board/run-board-labels.ts +2 -0
- package/scripts/run/board/run-board-layout.ts +1 -1
- package/scripts/run/board/run-board-read.ts +16 -0
- package/scripts/run/board/run-board-reason.ts +10 -1
- package/scripts/run/board/run-board-render-legend.ts +1 -0
- package/scripts/run/board/run-board-render-slot.ts +1 -1
- package/scripts/run/board/run-board-render-waits.ts +41 -0
- package/scripts/run/board/run-board-render.ts +6 -1
- package/scripts/run/board/run-board-row-yield.ts +1 -0
- package/scripts/run/board/run-board-state.ts +5 -0
- package/scripts/run/board/run-board-status.ts +72 -21
- package/scripts/run/board/run-board-tick.ts +5 -1
- package/scripts/run/cut/run-cut-cli.ts +3 -2
- package/scripts/run/cut/run-cut-report.ts +16 -0
- package/scripts/run/detached-launch.ts +23 -4
- package/scripts/run/entry/run-entry-cli.ts +8 -1
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-scope.ts +19 -1
- package/scripts/run/event/run-event-stream-emit.ts +13 -6
- package/scripts/run/event/run-event-stream.ts +4 -0
- package/scripts/run/hold/run-hold-session.ts +46 -0
- package/scripts/run/hold/run-hold.ts +7 -1
- package/scripts/run/launch-priority.ts +32 -0
- package/scripts/run/merge/run-merge-cli.ts +16 -65
- package/scripts/run/merge/run-merge-signals.ts +99 -0
- package/scripts/run/merge/run-merge-steps.ts +11 -1
- package/scripts/run/merge/run-merge.ts +21 -9
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-poll-hook.ts +157 -0
- package/scripts/run/run-step-cli.ts +22 -15
- package/scripts/run/run-step-section.ts +85 -0
- package/scripts/run/ship/run-ship-cli.ts +5 -12
- package/scripts/run/ship/run-ship-detach.ts +9 -0
- package/scripts/run/ship/run-ship-next.ts +10 -2
- package/scripts/run/ship/run-ship-overlap.ts +195 -0
- package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
- package/scripts/run/ship/run-ship-preflight.ts +28 -2
- package/scripts/run/ship/run-ship-return.ts +31 -7
- package/scripts/run/ship/run-ship-review-steps.ts +12 -10
- package/scripts/run/ship/run-ship-steps.ts +1 -1
- package/scripts/run/ship/run-ship-stop-context.ts +206 -0
- package/scripts/run/ship/run-ship-sync.ts +14 -6
- package/scripts/test/test-e2e-guard.ts +30 -5
- package/scripts/test/test-unit-guard.ts +16 -1
- package/scripts/test/unit-worker-share.ts +26 -4
- package/scripts/time-runtime/time-shell.ts +29 -4
- package/scripts/time-runtime/time-spans.ts +1 -0
- package/scripts/time-runtime/time-tool-call.ts +7 -0
- package/templates/gitignore +6 -2
- package/dist/hooks/chunk-K4LMBQC2.js +0 -68
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { api_outage } from '#scripts/agent/api-outage'
|
|
4
3
|
import { CONTEXT_CUT_THRESHOLD } from '#scripts/cost-runtime/context-cut-threshold'
|
|
5
4
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
6
|
-
import { issue_closing_pr } from '#scripts/issue/issue-closing-pr'
|
|
7
5
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
8
6
|
import { issue_state_cli } from '#scripts/issue/issue-state-cli'
|
|
9
7
|
import { lane_handoff } from '#scripts/lane/lane-handoff'
|
|
10
8
|
import { lane_ledger } from '#scripts/lane/lane-ledger'
|
|
11
9
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
12
10
|
import { run_carry, type CarryOwner, type RunCarry } from '#scripts/run/carry/run-carry'
|
|
13
|
-
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
14
11
|
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
15
12
|
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
16
|
-
import { run_ending } from '#scripts/run/run-ending'
|
|
17
13
|
import { run_label } from '#scripts/run/run-label'
|
|
18
|
-
import { run_merge, type ChildOutcome
|
|
19
|
-
import {
|
|
14
|
+
import { run_merge, type ChildOutcome } from './run-merge'
|
|
15
|
+
import { run_merge_signals } from './run-merge-signals'
|
|
20
16
|
import { run_merge_steps, type FailedResult, type MergeContext } from './run-merge-steps'
|
|
21
17
|
import { run_merge_token, type MergeToken } from './run-merge-token'
|
|
22
18
|
|
|
@@ -191,61 +187,6 @@ async function offered(ctx: MergeContext): Promise<OfferVerdict> {
|
|
|
191
187
|
return { token: await run_merge_steps.ask_next(ctx), code: SUCCESS_EXIT_CODE, is_offer: true }
|
|
192
188
|
}
|
|
193
189
|
|
|
194
|
-
// Whether the child's session ended through its environment — read only when `--output` named the
|
|
195
|
-
// transcript, off otherwise: its exit record shows it could not reach the API, or the session was
|
|
196
|
-
// killed inside a turn. The path is validated inside `run_ending`, so a missing output is "no".
|
|
197
|
-
function read_is_outage(output: string | undefined): boolean {
|
|
198
|
-
if (output === undefined) return false
|
|
199
|
-
|
|
200
|
-
return api_outage.is_outage(run_ending.read_exit(output)) || run_ending.ended_mid_turn(output)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function child_outages(child: string): Promise<number> {
|
|
204
|
-
const events = await run_event_stream_emit.current_events()
|
|
205
|
-
|
|
206
|
-
return events.filter(
|
|
207
|
-
(event) =>
|
|
208
|
-
event.kind === run_event_stream.EVENT_KIND.OUTAGE &&
|
|
209
|
-
run_event_scope.issue_named(event) === child,
|
|
210
|
-
).length
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// An outage child is re-dispatched only so many times: one that ends the same way on every dispatch is
|
|
214
|
-
// parked, since merges in other lanes reset the run-wide outage streak and would never stop it.
|
|
215
|
-
async function read_is_retryable(child: string, output: string | undefined): Promise<boolean> {
|
|
216
|
-
const is_interrupted =
|
|
217
|
-
read_is_outage(output) || (await run_merge_ship.is_unanswered(child, output))
|
|
218
|
-
|
|
219
|
-
if (!is_interrupted) return false
|
|
220
|
-
|
|
221
|
-
return (await child_outages(child)) < run_merge.CONSECUTIVE_OUTAGE_LIMIT
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
type IssueRead = Awaited<ReturnType<typeof issue_state_cli.read_issue>>
|
|
225
|
-
|
|
226
|
-
// The timeline is read only for an OPEN, unparked child — the one classification a merged closing pull
|
|
227
|
-
// request changes. A CLOSED child was closed by GitHub, so it must carry no `merged_pr` for
|
|
228
|
-
// `do_merged` to close again.
|
|
229
|
-
async function read_merged_pr(child: string, read: IssueRead): Promise<string | undefined> {
|
|
230
|
-
if (read.kind !== 'state' || run_merge.classify_child(read.state) !== 'failed') return undefined
|
|
231
|
-
|
|
232
|
-
return await issue_closing_pr.read_closing_pr(child)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function read_signals(ctx: MergeContext, read: IssueRead): Promise<EndingSignals> {
|
|
236
|
-
const [is_cut, merged_pr, is_outage] = await Promise.all([
|
|
237
|
-
run_merge_steps.has_resumable_cut(ctx.child),
|
|
238
|
-
read_merged_pr(ctx.child, read),
|
|
239
|
-
read_is_retryable(ctx.child, ctx.output),
|
|
240
|
-
])
|
|
241
|
-
|
|
242
|
-
return { is_outage, is_cut, merged_pr }
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function outcome_of(read: IssueRead, signals: EndingSignals): ChildOutcome {
|
|
246
|
-
return read.kind === 'state' ? run_merge.classify_child(read.state, signals) : 'unresolved'
|
|
247
|
-
}
|
|
248
|
-
|
|
249
190
|
// A refused count means this session is not the carry record's owner, so the whole operation is
|
|
250
191
|
// rejected — the lane is not closed and no next child is offered.
|
|
251
192
|
function report_count_refused(carry: RunCarry | undefined): MergeVerdict {
|
|
@@ -397,6 +338,13 @@ async function on_skipped(ctx: MergeContext): Promise<MergeVerdict> {
|
|
|
397
338
|
return await offered(ctx)
|
|
398
339
|
}
|
|
399
340
|
|
|
341
|
+
// A child a person withdrew with `lane:withdraw`: the withdrawal already stopped it, closed its issue
|
|
342
|
+
// and lane and put itself on the stream, so nothing is counted or written here and its seat goes to the
|
|
343
|
+
// next child.
|
|
344
|
+
async function on_withdrawn(ctx: MergeContext): Promise<MergeVerdict> {
|
|
345
|
+
return await offered(ctx)
|
|
346
|
+
}
|
|
347
|
+
|
|
400
348
|
// The child's own ending: it stopped before its commit for a person to look at, so nothing is counted
|
|
401
349
|
// and no next child is offered.
|
|
402
350
|
async function on_human_review(): Promise<MergeVerdict> {
|
|
@@ -422,6 +370,7 @@ const HANDLERS: Readonly<Record<ChildOutcome, (ctx: MergeContext) => Promise<Mer
|
|
|
422
370
|
split: on_skipped,
|
|
423
371
|
unresolved: on_unresolved,
|
|
424
372
|
shipping: on_shipping,
|
|
373
|
+
withdrawn: on_withdrawn,
|
|
425
374
|
}
|
|
426
375
|
|
|
427
376
|
// The whole merge event for one returned child, in-process: read its state, classify it, and run the
|
|
@@ -432,10 +381,12 @@ async function merge_child(ctx: MergeContext): Promise<MergeResult> {
|
|
|
432
381
|
}
|
|
433
382
|
|
|
434
383
|
const read = await issue_state_cli.read_issue(ctx.child, ctx.repo)
|
|
435
|
-
const signals = await read_signals(ctx, read)
|
|
436
|
-
const outcome = outcome_of(read, signals)
|
|
384
|
+
const signals = await run_merge_signals.read_signals(ctx, read)
|
|
385
|
+
const outcome = run_merge_signals.outcome_of(read, signals)
|
|
386
|
+
|
|
387
|
+
const { merged_pr, is_unanswered } = signals
|
|
437
388
|
|
|
438
|
-
return { outcome, ...(await HANDLERS[outcome]({ ...ctx, merged_pr
|
|
389
|
+
return { outcome, ...(await HANDLERS[outcome]({ ...ctx, merged_pr, is_unanswered })) }
|
|
439
390
|
}
|
|
440
391
|
|
|
441
392
|
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
@@ -461,7 +412,7 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
|
461
412
|
const run_merge_cli = {
|
|
462
413
|
merge_child,
|
|
463
414
|
parse,
|
|
464
|
-
read_merged_pr,
|
|
415
|
+
read_merged_pr: run_merge_signals.read_merged_pr,
|
|
465
416
|
record_merged,
|
|
466
417
|
run,
|
|
467
418
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { api_outage } from '#scripts/agent/api-outage'
|
|
2
|
+
import { issue_closing_pr } from '#scripts/issue/issue-closing-pr'
|
|
3
|
+
import { issue_merged } from '#scripts/issue/issue-merged'
|
|
4
|
+
import type { issue_state_cli } from '#scripts/issue/issue-state-cli'
|
|
5
|
+
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
6
|
+
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
7
|
+
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
8
|
+
import { run_ending } from '#scripts/run/run-ending'
|
|
9
|
+
import { run_merge, type ChildOutcome, type EndingSignals } from './run-merge'
|
|
10
|
+
import { run_merge_ship } from './run-merge-ship'
|
|
11
|
+
import { run_merge_steps, type MergeContext } from './run-merge-steps'
|
|
12
|
+
|
|
13
|
+
// What `run:merge` reads beside a returned child's GitHub state before classifying it — the exit
|
|
14
|
+
// record, the lane's cut, a merged closing pull request and a person's withdrawal — and the
|
|
15
|
+
// classification those readings and the state decide (`run_merge.classify_child`).
|
|
16
|
+
|
|
17
|
+
type IssueRead = Awaited<ReturnType<typeof issue_state_cli.read_issue>>
|
|
18
|
+
|
|
19
|
+
// Whether the child's session ended through its environment — read only when `--output` named the
|
|
20
|
+
// transcript, off otherwise: its exit record shows it could not reach the API, or the session was
|
|
21
|
+
// killed inside a turn. The path is validated inside `run_ending`, so a missing output is "no".
|
|
22
|
+
function read_is_outage(output: string | undefined): boolean {
|
|
23
|
+
if (output === undefined) return false
|
|
24
|
+
|
|
25
|
+
return api_outage.is_outage(run_ending.read_exit(output)) || run_ending.ended_mid_turn(output)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function child_outages(child: string): Promise<number> {
|
|
29
|
+
const events = await run_event_stream_emit.current_events()
|
|
30
|
+
|
|
31
|
+
return events.filter(
|
|
32
|
+
(event) =>
|
|
33
|
+
event.kind === run_event_stream.EVENT_KIND.OUTAGE &&
|
|
34
|
+
run_event_scope.issue_named(event) === child,
|
|
35
|
+
).length
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// An outage child is re-dispatched only so many times: one that ends the same way on every dispatch is
|
|
39
|
+
// parked, since merges in other lanes reset the run-wide outage streak and would never stop it. **An
|
|
40
|
+
// unanswered ship stop is told apart from an API outage**: the child reached the API, so it is
|
|
41
|
+
// re-dispatched without adding to the run-wide streak (joshuafolkken/kit#3974).
|
|
42
|
+
async function read_retryable(
|
|
43
|
+
child: string,
|
|
44
|
+
output: string | undefined,
|
|
45
|
+
): Promise<{ is_outage: boolean; is_unanswered: boolean }> {
|
|
46
|
+
const is_api_outage = read_is_outage(output)
|
|
47
|
+
const is_unanswered = !is_api_outage && (await run_merge_ship.is_unanswered(child, output))
|
|
48
|
+
|
|
49
|
+
if (!is_api_outage && !is_unanswered) return { is_outage: false, is_unanswered: false }
|
|
50
|
+
|
|
51
|
+
const is_outage = (await child_outages(child)) < run_merge.CONSECUTIVE_OUTAGE_LIMIT
|
|
52
|
+
|
|
53
|
+
return { is_outage, is_unanswered: is_outage && is_unanswered }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The timeline is read only for an OPEN, unparked child — the one classification a merged closing pull
|
|
57
|
+
// request changes. A CLOSED child was closed by GitHub, so it must carry no `merged_pr` for
|
|
58
|
+
// `do_merged` to close again.
|
|
59
|
+
async function read_merged_pr(child: string, read: IssueRead): Promise<string | undefined> {
|
|
60
|
+
if (read.kind !== 'state' || run_merge.classify_child(read.state) !== 'failed') return undefined
|
|
61
|
+
|
|
62
|
+
return await issue_closing_pr.read_closing_pr(child)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A child that merged before `lane:withdraw` stopped it is merged, not withdrawn: the withdrawal's
|
|
66
|
+
// event is already written, so only the timeline tells its merged close from the not-planned one. An
|
|
67
|
+
// unread timeline answers `undefined`, and `outcome_of` asks again rather than guessing.
|
|
68
|
+
async function read_is_withdrawn(child: string, read: IssueRead): Promise<boolean | undefined> {
|
|
69
|
+
const events = await run_event_stream_emit.current_events()
|
|
70
|
+
|
|
71
|
+
if (!run_event_scope.is_withdrawn(events, child)) return false
|
|
72
|
+
if (read.kind !== 'state' || run_merge.classify_child(read.state) !== 'merged') return true
|
|
73
|
+
const is_merged = await issue_merged.read_merge_state(child)
|
|
74
|
+
|
|
75
|
+
return is_merged === undefined ? undefined : !is_merged
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function read_signals(ctx: MergeContext, read: IssueRead): Promise<EndingSignals> {
|
|
79
|
+
const [is_cut, merged_pr, retryable, is_withdrawn] = await Promise.all([
|
|
80
|
+
run_merge_steps.has_resumable_cut(ctx.child),
|
|
81
|
+
read_merged_pr(ctx.child, read),
|
|
82
|
+
read_retryable(ctx.child, ctx.output),
|
|
83
|
+
read_is_withdrawn(ctx.child, read),
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
return { ...retryable, is_cut, merged_pr, is_withdrawn }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// An unread state is `unresolved` unless a withdrawal already settled the child, which is read locally;
|
|
90
|
+
// a withdrawn child whose timeline could not tell its merge (`is_withdrawn` unset) is `unresolved` too.
|
|
91
|
+
function outcome_of(read: IssueRead, signals: EndingSignals): ChildOutcome {
|
|
92
|
+
if (signals.is_withdrawn === undefined) return 'unresolved'
|
|
93
|
+
|
|
94
|
+
return run_merge.classify_child(read.kind === 'state' ? read.state : undefined, signals)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const run_merge_signals = { outcome_of, read_merged_pr, read_signals }
|
|
98
|
+
|
|
99
|
+
export { run_merge_signals }
|
|
@@ -73,6 +73,8 @@ interface MergeContext {
|
|
|
73
73
|
// The merged pull request that closes the child, when GitHub left the child OPEN after the merge —
|
|
74
74
|
// the one `merged` child whose issue this command still has to close.
|
|
75
75
|
merged_pr?: string | undefined
|
|
76
|
+
// The outage is a ship stop no relaunch answered: re-dispatched, but not added to the streak.
|
|
77
|
+
is_unanswered?: boolean | undefined
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
interface FailedResult {
|
|
@@ -359,8 +361,16 @@ interface OutageResult {
|
|
|
359
361
|
// the child, so it is re-dispatchable in the same run. Returns the record so the caller can read the
|
|
360
362
|
// outage streak against its guard. Returns `is_refused: true` without touching labels when the carry
|
|
361
363
|
// record rejected the count.
|
|
364
|
+
async function count_outage(ctx: MergeContext): Promise<ApplyCarryResult> {
|
|
365
|
+
if (ctx.is_unanswered !== true) return await apply_carry(ctx, run_merge.change_of('outage'))
|
|
366
|
+
|
|
367
|
+
const refused = await refused_carry(ctx)
|
|
368
|
+
|
|
369
|
+
return refused === undefined ? { kind: 'none' } : { kind: 'refused', carry: refused }
|
|
370
|
+
}
|
|
371
|
+
|
|
362
372
|
async function do_outage(ctx: MergeContext): Promise<OutageResult> {
|
|
363
|
-
const result = await
|
|
373
|
+
const result = await count_outage(ctx)
|
|
364
374
|
|
|
365
375
|
if (result.kind === 'refused') return { carry: result.carry, is_refused: true }
|
|
366
376
|
|
|
@@ -24,10 +24,11 @@ const CLOSED_STATE = 'CLOSED'
|
|
|
24
24
|
// failing every child it is handed rather than on failures scattered across a long run.
|
|
25
25
|
const CONSECUTIVE_FAILURE_LIMIT = 3
|
|
26
26
|
// Three outages in a row stop the run too — the same shape as the failure
|
|
27
|
-
// guard, over its own streak. An outage is a child its environment ended — it could not reach the API
|
|
28
|
-
// its session was killed inside a turn
|
|
29
|
-
//
|
|
30
|
-
//
|
|
27
|
+
// guard, over its own streak. An outage is a child its environment ended — it could not reach the API
|
|
28
|
+
// or its session was killed inside a turn — so a run of them is the environment being down; stopping is
|
|
29
|
+
// what keeps the run from re-dispatching into a dead API forever. A ship that stopped with no relaunch
|
|
30
|
+
// is re-dispatched too but not counted: its child reached the API (joshuafolkken/kit#3974). A merge or
|
|
31
|
+
// a genuine child failure resets it (`run-carry.ts` → `next_outages`).
|
|
31
32
|
const CONSECUTIVE_OUTAGE_LIMIT = 3
|
|
32
33
|
const ONE = 1
|
|
33
34
|
|
|
@@ -45,9 +46,12 @@ const ONE = 1
|
|
|
45
46
|
// the child ended its session on purpose, so it is resumed rather than parked and
|
|
46
47
|
// counts nothing.
|
|
47
48
|
// - `failed` — OPEN and carrying neither, and not an outage; the child did not finish.
|
|
49
|
+
// - `withdrawn` — a person withdrew it with `lane:withdraw`, read from the event stream before the
|
|
50
|
+
// GitHub state: closed as not planned, or about to be, it is neither merged nor failed.
|
|
48
51
|
// - `unresolved` — the state could not be read; re-read before deciding.
|
|
49
52
|
type ChildOutcome =
|
|
50
53
|
| 'merged'
|
|
54
|
+
| 'withdrawn'
|
|
51
55
|
| 'human-review'
|
|
52
56
|
| 'parked'
|
|
53
57
|
| 'split'
|
|
@@ -65,6 +69,10 @@ interface EndingSignals {
|
|
|
65
69
|
// The merged pull request whose `closes #N` names the child, when GitHub left the child OPEN after
|
|
66
70
|
// merging it.
|
|
67
71
|
merged_pr?: string | undefined
|
|
72
|
+
// The newest launch of the child was withdrawn by a person (`run_event_scope.is_withdrawn`).
|
|
73
|
+
is_withdrawn?: boolean | undefined
|
|
74
|
+
// The outage is a ship stop no relaunch answered, not an API the child could not reach.
|
|
75
|
+
is_unanswered?: boolean | undefined
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
const NO_SIGNALS: EndingSignals = { is_outage: false, is_cut: false }
|
|
@@ -107,12 +115,16 @@ function open_outcome(state: IssueState, signals: EndingSignals): ChildOutcome {
|
|
|
107
115
|
return unfinished_outcome(signals)
|
|
108
116
|
}
|
|
109
117
|
|
|
110
|
-
// The signals are read by the CLI and matter only for an OPEN child — a CLOSED
|
|
111
|
-
// regardless of how the process exited.
|
|
118
|
+
// The signals are read by the CLI and, but for a withdrawal, matter only for an OPEN child — a CLOSED
|
|
119
|
+
// child is merged regardless of how the process exited. **A withdrawal is read first**: `lane:withdraw`
|
|
120
|
+
// records it before stopping the child, so the parent may collect it while the issue is still OPEN, and
|
|
121
|
+
// once it is CLOSED as not planned the closed state would otherwise read as a merge.
|
|
112
122
|
function classify_child(
|
|
113
123
|
state: IssueState | undefined,
|
|
114
124
|
signals: EndingSignals = NO_SIGNALS,
|
|
115
125
|
): ChildOutcome {
|
|
126
|
+
if (signals.is_withdrawn === true) return 'withdrawn'
|
|
127
|
+
|
|
116
128
|
if (state === undefined) return 'unresolved'
|
|
117
129
|
|
|
118
130
|
if (is_closed(state)) return 'merged'
|
|
@@ -121,9 +133,9 @@ function classify_child(
|
|
|
121
133
|
}
|
|
122
134
|
|
|
123
135
|
// The counter change a child's outcome implies. A merge counts a merge (which resets the failure
|
|
124
|
-
// streak); a failure counts a failure; a split, parked or human-review child counts nothing —
|
|
125
|
-
//
|
|
126
|
-
// untouched.
|
|
136
|
+
// streak); a failure counts a failure; a split, parked, withdrawn or human-review child counts nothing —
|
|
137
|
+
// a decision a person made or still owes is not the environment failing — and `undefined` says the
|
|
138
|
+
// carry record is left untouched.
|
|
127
139
|
function change_of(outcome: ChildOutcome): CarryChange | undefined {
|
|
128
140
|
if (outcome === 'merged') return { merged: ONE }
|
|
129
141
|
|
|
@@ -103,7 +103,7 @@ const REASONS: Record<LivenessVerdict, string> = {
|
|
|
103
103
|
'A trace could not be read, so this says nothing about the unit. A trace that could not be read answers `undetermined`, never `stopped`.',
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
const ALIVE_ADVICE = 'Keep
|
|
106
|
+
const ALIVE_ADVICE = 'Keep waiting on `pnpm josh lane:await`; do not touch the child.'
|
|
107
107
|
const SETTLED_ADVICE =
|
|
108
108
|
'Read the child with `pnpm josh issue:state <N>` and take the branch its state says.'
|
|
109
109
|
// The unit stopped, but *why* it stopped — abandoned mid-implementation, or dead because it could not
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
2
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
3
|
+
import { time_density_hook } from '#scripts/time-runtime/time-density-hook'
|
|
4
|
+
import { time_shell } from '#scripts/time-runtime/time-shell'
|
|
5
|
+
import { time_spans, type Span } from '#scripts/time-runtime/time-spans'
|
|
6
|
+
import { run_headless } from './run-headless'
|
|
7
|
+
import { run_watcher_hook } from './run-watcher-hook'
|
|
8
|
+
|
|
9
|
+
// The `backlogrun` parent's and the wake session's poll, refused at its first repeat.
|
|
10
|
+
//
|
|
11
|
+
// **The parent waits on a blocking command, never on a re-read.** `backlog:drive` and `lane:await` hold
|
|
12
|
+
// the turn until something the model has to read happens; a state read issued again with nothing
|
|
13
|
+
// decided in between is a poll, and each one is a model turn spent on an answer that has not changed.
|
|
14
|
+
//
|
|
15
|
+
// **"Nothing decided in between" is read off the transcript**: the last completed call before this one
|
|
16
|
+
// ran the same state read on the same operands, with no person's prompt after it. A different command, a person's prompt or
|
|
17
|
+
// any other tool call in between breaks the streak, so a progress question answered twice is not one.
|
|
18
|
+
//
|
|
19
|
+
// **The parent test is `run_headless.driving_carry`'s**, the one the parent hand-off guard reads, so a
|
|
20
|
+
// lane child, a person's session and a handed-off record are left alone. A wake session adopts the
|
|
21
|
+
// record, so it reads as the parent here.
|
|
22
|
+
|
|
23
|
+
const SWITCH_ENV_KEY = 'JOSH_POLL_GUARD'
|
|
24
|
+
const BASH_TOOL = 'Bash'
|
|
25
|
+
const STATE_READS: ReadonlySet<string> = new Set([
|
|
26
|
+
'josh lane:list',
|
|
27
|
+
'josh lane:output',
|
|
28
|
+
'josh run:board',
|
|
29
|
+
'josh run:carry',
|
|
30
|
+
'josh run:liveness',
|
|
31
|
+
'josh run:progress',
|
|
32
|
+
'josh run:wake',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
const POLL_REASON =
|
|
36
|
+
'⛔ poll: this `backlogrun` session repeats a state read with nothing decided in between, on every ' +
|
|
37
|
+
'occurrence. Wait on `pnpm josh backlog:drive`, or `pnpm josh lane:await <N...> --owner "$PPID"` in ' +
|
|
38
|
+
'the background, and end the turn; its exit is the wake.'
|
|
39
|
+
|
|
40
|
+
// The flags that make a state-read command write: `run:carry --filed` twice is two filings, and
|
|
41
|
+
// `run:carry --cut` after `run:carry --json` is the hand-off, so neither is a re-read.
|
|
42
|
+
const WRITING_FLAGS: ReadonlySet<string> = new Set([
|
|
43
|
+
'--begin',
|
|
44
|
+
'--cut',
|
|
45
|
+
'--done',
|
|
46
|
+
'--end',
|
|
47
|
+
'--every',
|
|
48
|
+
'--filed',
|
|
49
|
+
'--loop',
|
|
50
|
+
'--mark',
|
|
51
|
+
'--merged',
|
|
52
|
+
'--resume',
|
|
53
|
+
'--retrospective',
|
|
54
|
+
'--start',
|
|
55
|
+
'--stop',
|
|
56
|
+
'--wait',
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
const FLAG_PREFIX = '-'
|
|
60
|
+
const NAME_WORDS = 2
|
|
61
|
+
const SIGNATURE_SEPARATOR = ' '
|
|
62
|
+
|
|
63
|
+
// A state read keyed by the command and its operands, or `undefined` for a call that is none.
|
|
64
|
+
// **Operands, not flags**: `lane:output 1 <path>` then `lane:output 2 <path>` read two children, while
|
|
65
|
+
// `run:board` and `run:board --chat` print one answer.
|
|
66
|
+
function read_key(signature: string): string | undefined {
|
|
67
|
+
const words = signature.split(SIGNATURE_SEPARATOR)
|
|
68
|
+
const name = words.slice(0, NAME_WORDS).join(SIGNATURE_SEPARATOR)
|
|
69
|
+
|
|
70
|
+
if (!STATE_READS.has(name) || words.some((word) => WRITING_FLAGS.has(word))) return undefined
|
|
71
|
+
|
|
72
|
+
return words.filter((word) => !word.startsWith(FLAG_PREFIX)).join(SIGNATURE_SEPARATOR)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function read_keys(signatures: ReadonlyArray<string>): Array<string> {
|
|
76
|
+
return signatures.map((signature) => read_key(signature)).filter((key) => key !== undefined)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function state_reads_of(command: string): ReadonlyArray<string> {
|
|
80
|
+
return read_keys(time_shell.josh_signatures_of(command))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function command_of(input: unknown): string {
|
|
84
|
+
if (typeof input !== 'object' || input === null || !('command' in input)) return ''
|
|
85
|
+
|
|
86
|
+
return typeof input.command === 'string' ? input.command : ''
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The state reads the call runs, or none for a call this guard never judges: not a shell call, or a
|
|
90
|
+
// delegated unit's (it names the parent's transcript, so only `agent_id` tells it apart).
|
|
91
|
+
function candidate_reads(raw_payload: string): ReadonlyArray<string> {
|
|
92
|
+
const payload = hook_decision.parse_hook_payload(raw_payload)
|
|
93
|
+
|
|
94
|
+
if (payload?.tool_name !== BASH_TOOL || (payload.agent_id ?? '') !== '') return []
|
|
95
|
+
|
|
96
|
+
return state_reads_of(command_of(payload.tool_input))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The last completed call or person's prompt; a model span is the turn itself, never a decision.
|
|
100
|
+
function last_step(tail: string): Span | undefined {
|
|
101
|
+
return time_spans
|
|
102
|
+
.parse_timeline(tail)
|
|
103
|
+
.spans.findLast((span) => span.category !== time_spans.MODEL_CATEGORY)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function repeats_last_call(tail: string, reads: ReadonlyArray<string>): boolean {
|
|
107
|
+
const last = last_step(tail)
|
|
108
|
+
|
|
109
|
+
if (last?.category !== time_spans.TOOL_CATEGORY) return false
|
|
110
|
+
|
|
111
|
+
const last_reads = read_keys(last.josh_signatures)
|
|
112
|
+
|
|
113
|
+
return reads.some((key) => last_reads.includes(key))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Every cheap test first: the carry record is read only for a call that already repeats a state read.
|
|
117
|
+
function is_repeat(raw_payload: string): boolean {
|
|
118
|
+
const reads = hook_decision.is_switch_enabled(SWITCH_ENV_KEY) ? candidate_reads(raw_payload) : []
|
|
119
|
+
const transcript = reads.length === 0 ? undefined : run_watcher_hook.transcript_of(raw_payload)
|
|
120
|
+
|
|
121
|
+
if (transcript === undefined) return false
|
|
122
|
+
|
|
123
|
+
return repeats_last_call(time_density_hook.read_tail(transcript), reads)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The deny reason for a state read the driving `backlogrun` parent (or its wake session) issues again
|
|
128
|
+
* with nothing decided since the last one, or `undefined` when the call proceeds.
|
|
129
|
+
*/
|
|
130
|
+
async function poll_reason(
|
|
131
|
+
raw_payload: string,
|
|
132
|
+
is_parent: () => Promise<boolean> = run_headless.is_backlog_parent,
|
|
133
|
+
): Promise<string | undefined> {
|
|
134
|
+
if (!is_repeat(raw_payload)) return undefined
|
|
135
|
+
|
|
136
|
+
return (await is_parent()) ? POLL_REASON : undefined
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// It fails open: an unreadable transcript or carry record allows the call rather than wedging the run.
|
|
140
|
+
async function safe_poll_reason(raw_payload: string): Promise<string | undefined> {
|
|
141
|
+
try {
|
|
142
|
+
return await poll_reason(raw_payload)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
error_text.trace_swallowed('run_poll_hook.poll_reason', error)
|
|
145
|
+
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const run_poll_hook = {
|
|
151
|
+
POLL_REASON,
|
|
152
|
+
SWITCH_ENV_KEY,
|
|
153
|
+
poll_reason,
|
|
154
|
+
safe_poll_reason,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { run_poll_hook }
|
|
@@ -14,15 +14,16 @@ import { run_prep, type PrepParts } from './run-prep'
|
|
|
14
14
|
import { run_prep_cli } from './run-prep-cli'
|
|
15
15
|
import { run_retrospective } from './run-retrospective'
|
|
16
16
|
import { run_step, type ParentInput, type StepInput } from './run-step'
|
|
17
|
+
import { run_step_section } from './run-step-section'
|
|
17
18
|
|
|
18
|
-
// `josh run:step [<N>]` — print the run's next single action,
|
|
19
|
+
// `josh run:step [<N>]` — print the run's next single action, followed by the section that governs it
|
|
20
|
+
// (`run-step-section.ts`), computed from the event stream, the carry
|
|
19
21
|
// record and the issue state. It reads exactly those three, never the
|
|
20
22
|
// conversation: the issue facts through `run:prep`'s own gather (so this and `run:next` never answer
|
|
21
23
|
// from different reads), the newest event off the stream, and the carry record's kind. The reader runs
|
|
22
24
|
// the printed line, returns the result through `run:event`, and asks again — the loop `run:step`
|
|
23
25
|
// exists to close.
|
|
24
26
|
|
|
25
|
-
const SUCCESS_EXIT_CODE = 0
|
|
26
27
|
const FAILURE_EXIT_CODE = 1
|
|
27
28
|
const ARGV_OFFSET = 2
|
|
28
29
|
const USAGE =
|
|
@@ -154,16 +155,31 @@ function parent_input(directory: string | undefined): ParentInput {
|
|
|
154
155
|
// `unknown` is the one answer with nothing to run — no record this session drives — so it exits non-zero
|
|
155
156
|
// with the usage, exactly as a position missing its issue state does.
|
|
156
157
|
async function run_parent(): Promise<number> {
|
|
157
|
-
const
|
|
158
|
+
const action = run_step.parent_action(parent_input(await run_carry.repository_directory()))
|
|
158
159
|
|
|
159
|
-
console.info(line)
|
|
160
|
-
if (line !== run_step.UNKNOWN) return
|
|
160
|
+
console.info(action.line)
|
|
161
|
+
if (action.line !== run_step.UNKNOWN) return run_step_section.print_for(action)
|
|
161
162
|
|
|
162
163
|
console.error(USAGE)
|
|
163
164
|
|
|
164
165
|
return FAILURE_EXIT_CODE
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
function report({ input, ship_problems }: Gathered): number {
|
|
169
|
+
const action = run_step.next_action(input)
|
|
170
|
+
|
|
171
|
+
console.info(action.line)
|
|
172
|
+
// On stderr, so stdout stays the action line and its rule: what `josh ship` would
|
|
173
|
+
// refuse on is said at every step, while it is still cheap to meet.
|
|
174
|
+
if (ship_problems.length > 0) console.error(run_prep.ship_body(ship_problems))
|
|
175
|
+
|
|
176
|
+
// A state that could not be read exits non-zero, exactly as `run:prep` and `run:next` exit on the
|
|
177
|
+
// same failure, so a position missing its issue state is never read as a confident next step.
|
|
178
|
+
if (input.state === undefined) return FAILURE_EXIT_CODE
|
|
179
|
+
|
|
180
|
+
return run_step_section.print_for(action)
|
|
181
|
+
}
|
|
182
|
+
|
|
167
183
|
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
168
184
|
if (argv.length === 0) return await run_parent()
|
|
169
185
|
|
|
@@ -175,16 +191,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
|
175
191
|
return FAILURE_EXIT_CODE
|
|
176
192
|
}
|
|
177
193
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
console.info(run_step.next_action(input).line)
|
|
181
|
-
// On stderr, so stdout stays the one action line: what `josh ship` would
|
|
182
|
-
// refuse on is said at every step, while it is still cheap to meet.
|
|
183
|
-
if (ship_problems.length > 0) console.error(run_prep.ship_body(ship_problems))
|
|
184
|
-
|
|
185
|
-
// A state that could not be read exits non-zero, exactly as `run:prep` and `run:next` exit on the
|
|
186
|
-
// same failure, so a position missing its issue state is never read as a confident next step.
|
|
187
|
-
return input.state === undefined ? FAILURE_EXIT_CODE : SUCCESS_EXIT_CODE
|
|
194
|
+
return report(await gather(issue_number))
|
|
188
195
|
}
|
|
189
196
|
|
|
190
197
|
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { document_section_cli } from '#scripts/document/document-section-cli'
|
|
2
|
+
import { run_step, type StepAction } from './run-step'
|
|
3
|
+
|
|
4
|
+
// The section that governs each action `run:step` and `run:entry` print — the one table the action →
|
|
5
|
+
// rule mapping lives in, so the manifests no longer carry a route table the reader walks to find it.
|
|
6
|
+
// The command prints the section after its action line, which saves the `doc:section` turn the reader
|
|
7
|
+
// would otherwise spend fetching a section the action already determined.
|
|
8
|
+
//
|
|
9
|
+
// **A heading that does not resolve fails the command**, exactly as `doc:section` refuses it: a pointer
|
|
10
|
+
// renamed away would otherwise print the action without its rule, and the reader would act unread.
|
|
11
|
+
|
|
12
|
+
const JOSH_PREFIX = 'pnpm josh '
|
|
13
|
+
const COMMAND_SEPARATOR = ' '
|
|
14
|
+
const SUCCESS_EXIT_CODE = 0
|
|
15
|
+
|
|
16
|
+
// The key `run:entry` prints under — not a `run:step` action, but the entry's own read, kept in the
|
|
17
|
+
// same table so every mechanically-chosen section has one home.
|
|
18
|
+
const ENTRY = 'entry'
|
|
19
|
+
|
|
20
|
+
interface SectionReference {
|
|
21
|
+
document: string
|
|
22
|
+
heading: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function reference(document: string, heading: string): SectionReference {
|
|
26
|
+
return { document, heading }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The gate's chain governs both the implementation it follows and a stopped ship's report.
|
|
30
|
+
const CHAIN = reference('chain-rule.md', 'Run the review-to-merge chain')
|
|
31
|
+
const PROGRESS = 'backlogrun-progress.md'
|
|
32
|
+
|
|
33
|
+
// Keyed on a verdict token, or on the josh subcommand a command action runs. An action with no row —
|
|
34
|
+
// `wait`, `stop`, a `decide:` line — has no rule to read beyond its own line.
|
|
35
|
+
const GOVERNING_SECTIONS: ReadonlyMap<string, SectionReference> = new Map([
|
|
36
|
+
[ENTRY, reference('split-assessment.md', 'The question')],
|
|
37
|
+
[run_step.IMPLEMENT, CHAIN],
|
|
38
|
+
[run_step.HUMAN_REVIEW, reference('needs-human-review.md', '`needs-human-review`')],
|
|
39
|
+
[run_step.UPDATE_DEPS, reference('latest-gate.md', 'The dependency update')],
|
|
40
|
+
['followup', reference('followup.md', 'Run `pnpm josh followup`')],
|
|
41
|
+
['review:round2', reference('prompts/review.md', 'Review round cap')],
|
|
42
|
+
['run:merge', reference(PROGRESS, "Running a named epic's children")],
|
|
43
|
+
['backlog:next', reference('backlogrun-steps.md', 'The loop')],
|
|
44
|
+
['run:cut', reference('pre-gate-cut.md', 'Resuming')],
|
|
45
|
+
['ship', CHAIN],
|
|
46
|
+
['retrospective', reference('retrospective.md', 'The end-of-run retrospective')],
|
|
47
|
+
['run:carry', reference(PROGRESS, 'The hand-off')],
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
function key_of(action: StepAction): string {
|
|
51
|
+
if (action.kind !== 'command') return action.line
|
|
52
|
+
|
|
53
|
+
const [subcommand = ''] = action.line.slice(JOSH_PREFIX.length).split(COMMAND_SEPARATOR)
|
|
54
|
+
|
|
55
|
+
return subcommand
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function section_for(key: string): SectionReference | undefined {
|
|
59
|
+
return GOVERNING_SECTIONS.get(key)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Printed through `doc:section` itself, so the extent, the Bash-cap split and the refusal of an
|
|
63
|
+
// unresolved heading are that command's, never a second copy of them.
|
|
64
|
+
function print_section(key: string, root: string = process.cwd()): number {
|
|
65
|
+
const found = section_for(key)
|
|
66
|
+
|
|
67
|
+
if (found === undefined) return SUCCESS_EXIT_CODE
|
|
68
|
+
|
|
69
|
+
return document_section_cli.run([found.document, found.heading], root)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function print_for(action: StepAction, root: string = process.cwd()): number {
|
|
73
|
+
return print_section(key_of(action), root)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const run_step_section = {
|
|
77
|
+
ENTRY,
|
|
78
|
+
GOVERNING_SECTIONS,
|
|
79
|
+
key_of,
|
|
80
|
+
print_for,
|
|
81
|
+
print_section,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type { SectionReference }
|
|
85
|
+
export { run_step_section }
|