@joshuafolkken/kit 1.1453.0 → 1.1471.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/skills/dependency-update/SKILL.md +26 -55
- package/.claude/skills/epic-commands/SKILL.md +87 -148
- package/.claude/skills/epic-commands/epic-bundle.md +45 -77
- package/.claude/skills/epic-commands/execution-waves.md +44 -81
- package/.claude/skills/verify-ui/SKILL.md +25 -50
- package/.claude/skills/workflow-commands/background-commands.md +36 -47
- package/.claude/skills/workflow-commands/backlogrun-child.md +72 -85
- package/.claude/skills/workflow-commands/backlogrun-handoff-report.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +91 -121
- package/.claude/skills/workflow-commands/backlogrun-park.md +73 -117
- package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -9
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +11 -20
- package/.claude/skills/workflow-commands/backlogrun-steps.md +43 -52
- package/.claude/skills/workflow-commands/chain-rule.md +24 -34
- package/.claude/skills/workflow-commands/followup.md +57 -76
- package/.claude/skills/workflow-commands/halfrun.md +2 -1
- package/.claude/skills/workflow-commands/issue-comments.md +43 -60
- package/.claude/skills/workflow-commands/kickoff.md +65 -74
- package/.claude/skills/workflow-commands/observation-filing.md +62 -107
- package/.claude/skills/workflow-commands/observation-ledger.md +56 -111
- package/.claude/skills/workflow-commands/pre-gate-cut.md +89 -138
- package/.claude/skills/workflow-commands/prerequisite.md +10 -15
- package/.claude/skills/workflow-commands/progress-watcher.md +21 -26
- package/.claude/skills/workflow-commands/prrun.md +2 -1
- package/.claude/skills/workflow-commands/retrospective.md +31 -50
- package/.claude/skills/workflow-commands/split-assessment.md +34 -58
- package/.claude/skills/workflow-commands/working-tree-hold.md +54 -71
- package/CLAUDE.md +7 -6
- package/README.md +2 -9
- package/dist/CLAUDE.md +7 -6
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-Y3GKIKGW.js → chunk-43ZHT22P.js} +31 -15
- package/dist/hooks/{chunk-MBJYQPEK.js → chunk-AS74OU3D.js} +24 -8
- package/dist/hooks/{chunk-3C4XRYG6.js → chunk-E7OIKX5Z.js} +17 -12
- package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-EUMACUK2.js} +18 -12
- package/dist/hooks/{chunk-NO4AALP5.js → chunk-H5HVWYXK.js} +4 -4
- package/dist/hooks/{chunk-5C7L6FRC.js → chunk-IIGFYXKC.js} +346 -241
- package/dist/hooks/{chunk-W5ZKGTSY.js → chunk-J47HU5H5.js} +14 -3
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-K4EOONYM.js} +1 -1
- package/dist/hooks/{chunk-LBFYEOXJ.js → chunk-K4LMBQC2.js} +4 -4
- package/dist/hooks/{chunk-D3NJXN5M.js → chunk-KC5AK2GF.js} +8 -6
- package/dist/hooks/chunk-SHGPANFB.js +56 -0
- package/dist/hooks/{chunk-EF5GQ6WS.js → chunk-YHBZAJSV.js} +3 -3
- package/dist/hooks/{chunk-SHYUQOIS.js → chunk-YYPI7SJZ.js} +5 -5
- package/dist/hooks/codex-hook-adapter.js +13 -13
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +3 -3
- package/dist/hooks/inputs.json +251 -247
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +6 -5
- package/dist/hooks/stop-guard.js +75 -18
- package/dist/josh.js +6 -7
- package/dist/managed-marker/index.js +1 -2
- package/dist/version/index.js +15 -12
- package/eslint/base.js +2 -1
- package/eslint/rules/unicorn.js +16 -0
- package/package.json +3 -3
- package/prompts/coding-standards.md +12 -22
- package/prompts/collaboration-workflow/file-edits.md +26 -44
- package/prompts/collaboration-workflow/gh-rest.md +20 -36
- package/prompts/collaboration-workflow/glossary.md +5 -7
- package/prompts/collaboration-workflow/issue-citation.md +7 -13
- package/prompts/collaboration-workflow/issue-template.md +29 -29
- package/prompts/collaboration-workflow/operating-rules.md +35 -44
- package/prompts/collaboration-workflow/output-bounds.md +15 -15
- package/prompts/collaboration-workflow/overview.md +14 -19
- package/prompts/collaboration-workflow/plan-comment.md +13 -13
- package/prompts/collaboration-workflow/principles.md +50 -53
- package/prompts/collaboration-workflow/proposal-request.md +1 -9
- package/prompts/collaboration-workflow/report-format.md +32 -49
- package/prompts/collaboration-workflow/residency.md +10 -10
- package/prompts/collaboration-workflow/rule-delivery.md +9 -9
- package/prompts/collaboration-workflow/shell-body.md +11 -21
- package/prompts/collaboration-workflow/turn-batching.md +8 -14
- package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
- package/prompts/collaboration-workflow/wip-cap.md +24 -30
- package/prompts/collaboration-workflow.md +24 -25
- package/prompts/refactoring.md +33 -43
- package/prompts/review-rubric.md +84 -129
- package/prompts/review.md +111 -141
- package/prompts/sonar-hotspot-handling.md +22 -50
- package/prompts/testing-guide.md +48 -99
- package/scripts/adopt/adopt-toolkits.ts +2 -4
- package/scripts/agent/agent-exit-record.ts +23 -1
- package/scripts/agent/codex-agent-argv.ts +7 -2
- package/scripts/backlog/backlog-budget-cli.ts +1 -2
- package/scripts/backlog/backlog-drive-carried.ts +26 -0
- package/scripts/backlog/backlog-drive-cli.ts +10 -5
- package/scripts/backlog/backlog-drive-named.ts +2 -4
- package/scripts/backlog/backlog-drive-restore.ts +19 -1
- package/scripts/backlog/backlog-drive-seats.ts +27 -0
- package/scripts/backlog/backlog-drive.ts +11 -6
- package/scripts/backlog/backlog-ready.ts +13 -6
- package/scripts/behavior/behavior-rules.ts +3 -2
- package/scripts/cost-runtime/codex-usage.ts +4 -2
- package/scripts/cost-runtime/cost-cli.ts +1 -2
- package/scripts/delegation/investigation-guard.ts +1 -1
- package/scripts/delegation/investigation-reads.ts +1 -1
- package/scripts/discovery/repo-discovery.ts +4 -2
- package/scripts/discovery/repo-origin.ts +2 -4
- package/scripts/doctor/doctor-logic.ts +6 -3
- package/scripts/document/document-byte-budget.ts +27 -27
- package/scripts/document/document-byte-check.ts +1 -2
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/epic/epic-audit-rationale.ts +8 -2
- package/scripts/epic/epic-bundle-cli.ts +1 -2
- package/scripts/epic/epic-chains.ts +6 -2
- package/scripts/epic/epic-close-comment.ts +1 -2
- package/scripts/gate/verification-gate.ts +7 -2
- package/scripts/gh/git-pr-ai-review.ts +12 -1
- package/scripts/gh/git-pr-confirmation.ts +11 -7
- package/scripts/gh/git-pr.ts +6 -2
- package/scripts/git/git-common-directory.ts +3 -3
- package/scripts/git/git-diff-reads.ts +16 -9
- package/scripts/git/git-execa-error.ts +1 -2
- package/scripts/hooks/format-edited-file.ts +10 -4
- package/scripts/hooks/pre-push-unit.ts +15 -4
- package/scripts/hooks/pretool-guard.ts +23 -29
- package/scripts/hooks/step-zero-notice.ts +3 -2
- package/scripts/init/hook-command-rewrite.ts +6 -2
- package/scripts/init/init-logic-deploy-vps.ts +4 -2
- package/scripts/issue/behavior-change-lint.ts +12 -6
- package/scripts/issue/issue-scout-cli.ts +3 -2
- package/scripts/josh/file-map-stamp.ts +1 -3
- package/scripts/josh/hook-decision.ts +17 -2
- package/scripts/josh/josh-commands-workflow.ts +1 -1
- package/scripts/lane/lane-await.ts +3 -2
- package/scripts/lane/lane-child-invocation.ts +14 -1
- package/scripts/lane/lane-guard-policy.ts +4 -4
- package/scripts/lane/lane-ledger.ts +9 -0
- package/scripts/lane/lane-load.ts +9 -2
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-parked.ts +18 -0
- package/scripts/lane/lane-process-load.ts +125 -0
- package/scripts/lane/lane-process-stats.ts +93 -0
- package/scripts/lane/lane-registry.ts +8 -3
- package/scripts/lane/lane-session-broker.ts +29 -5
- package/scripts/lane/lane-spent.ts +14 -3
- package/scripts/lane/lane-stats.ts +1 -0
- package/scripts/managed-marker/managed-marker-logic.ts +1 -2
- package/scripts/notify/confirmation-options.ts +59 -0
- package/scripts/notify/notify-opt-in.ts +33 -0
- package/scripts/notify/telegram-notify.ts +25 -8
- package/scripts/notify/telegram-test-logic.ts +33 -1
- package/scripts/notify/telegram-test.ts +4 -2
- package/scripts/overrides/lockfile-overrides.ts +1 -3
- package/scripts/propagate/propagate.ts +1 -2
- package/scripts/release/release-progress.ts +3 -2
- package/scripts/report/report-lint.ts +7 -4
- package/scripts/review/review-record-cli.ts +6 -2
- package/scripts/rules/destructive-command.ts +6 -9
- package/scripts/rules/direct-filing.ts +3 -7
- package/scripts/rules/early-heartbeat.ts +7 -18
- package/scripts/rules/file-body.ts +6 -13
- package/scripts/rules/filing-cap.ts +3 -6
- package/scripts/rules/git-force.ts +5 -9
- package/scripts/rules/implementation-cut.ts +37 -35
- package/scripts/rules/index-guard.ts +4 -7
- package/scripts/rules/issue-comments.ts +5 -15
- package/scripts/rules/josh-git-bare.ts +3 -6
- package/scripts/rules/lane-background.ts +10 -21
- package/scripts/rules/lane-carry-conflict.ts +7 -17
- package/scripts/rules/lane-interactive-ask.ts +6 -11
- package/scripts/rules/lane-park.ts +5 -13
- package/scripts/rules/lane-split-park.ts +2 -7
- package/scripts/rules/lane-switch-main.ts +9 -12
- package/scripts/rules/oracle-consulted.ts +4 -5
- package/scripts/rules/piped-verification.ts +81 -31
- package/scripts/rules/poll-loop.ts +7 -17
- package/scripts/rules/pre-gate-cut.ts +7 -21
- package/scripts/rules/prior-comment-read.ts +7 -5
- package/scripts/rules/protected-files.ts +5 -9
- package/scripts/rules/raw-field-body.ts +3 -7
- package/scripts/rules/rule-body-guard.ts +4 -11
- package/scripts/rules/run-tail-rule.ts +6 -22
- package/scripts/rules/shell-body-trigger.ts +7 -8
- package/scripts/rules/shell-segments.ts +7 -0
- package/scripts/rules/stop-rules.ts +17 -29
- package/scripts/rules/test-declared-commit.ts +4 -10
- package/scripts/rules/third-party-write.ts +5 -10
- package/scripts/rules/worktree-guard.ts +5 -12
- package/scripts/run/board/run-board-github.ts +11 -3
- package/scripts/run/board/run-board-idle.ts +31 -0
- package/scripts/run/board/run-board-read.ts +9 -12
- package/scripts/run/board/run-board-reason.ts +7 -3
- package/scripts/run/board/run-board-state.ts +4 -0
- package/scripts/run/board/run-board-status.ts +18 -3
- package/scripts/run/board/run-board-thanks.ts +7 -2
- package/scripts/run/board/run-board-tick.ts +32 -7
- package/scripts/run/carry/run-carry-args.ts +1 -3
- package/scripts/run/carry/run-carry-conversation.ts +7 -2
- package/scripts/run/cut/run-cut-cli.ts +15 -20
- package/scripts/run/cut/run-cut-decline.ts +72 -0
- package/scripts/run/cut/run-cut-ready.ts +74 -0
- package/scripts/run/cut/run-cut-report.ts +13 -0
- package/scripts/run/cut/run-cut.ts +10 -1
- package/scripts/run/entry/run-entry-stop.ts +6 -3
- package/scripts/run/hold/run-hold-cli.ts +14 -3
- package/scripts/run/hold/run-hold.ts +6 -3
- package/scripts/run/merge/run-merge-cli.ts +30 -7
- package/scripts/run/merge/run-merge-ship.ts +59 -1
- package/scripts/run/merge/run-merge.ts +3 -2
- package/scripts/run/run-ending.ts +31 -3
- package/scripts/run/run-parent-cut-hook.ts +5 -10
- package/scripts/run/run-preflight.ts +1 -2
- package/scripts/run/run-stop-notify.ts +3 -2
- package/scripts/run/run-watcher-guard.ts +6 -3
- package/scripts/run/ship/run-ship-review-steps.ts +13 -4
- package/scripts/run/ship/run-ship-review.ts +15 -2
- package/scripts/run/tidy/run-tidy-cli.ts +3 -3
- package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
- package/scripts/safe-chain/project-config.ts +7 -2
- package/scripts/sync/sync-hook-safety.ts +8 -2
- package/scripts/test/unit-worker-share.ts +3 -2
- package/scripts/time-runtime/time-batch-guard.ts +3 -3
- package/scripts/time-runtime/time-hook-transcript.ts +3 -2
- package/scripts/time-runtime/time-shell.ts +10 -0
- package/scripts/version/effective-upstream.ts +3 -4
- package/scripts/version/kit-version-config.ts +8 -2
- package/scripts/version/latest-corepack.ts +1 -2
- package/scripts/version/release-hold.ts +11 -4
- package/scripts/version/version-commands.ts +12 -4
- package/dist/hooks/chunk-UT7YC3N2.js +0 -56
|
@@ -10,11 +10,13 @@ import { lane_handoff } from '#scripts/lane/lane-handoff'
|
|
|
10
10
|
import { lane_ledger } from '#scripts/lane/lane-ledger'
|
|
11
11
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
12
12
|
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'
|
|
13
14
|
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
14
15
|
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
15
16
|
import { run_ending } from '#scripts/run/run-ending'
|
|
16
17
|
import { run_label } from '#scripts/run/run-label'
|
|
17
18
|
import { run_merge, type ChildOutcome, type EndingSignals } from './run-merge'
|
|
19
|
+
import { run_merge_ship } from './run-merge-ship'
|
|
18
20
|
import { run_merge_steps, type FailedResult, type MergeContext } from './run-merge-steps'
|
|
19
21
|
import { run_merge_token, type MergeToken } from './run-merge-token'
|
|
20
22
|
|
|
@@ -189,14 +191,34 @@ async function offered(ctx: MergeContext): Promise<OfferVerdict> {
|
|
|
189
191
|
return { token: await run_merge_steps.ask_next(ctx), code: SUCCESS_EXIT_CODE, is_offer: true }
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
// Whether the child's
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
// output is simply "not an outage".
|
|
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".
|
|
196
197
|
function read_is_outage(output: string | undefined): boolean {
|
|
197
198
|
if (output === undefined) return false
|
|
198
199
|
|
|
199
|
-
return api_outage.is_outage(run_ending.read_exit(output))
|
|
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
|
|
200
222
|
}
|
|
201
223
|
|
|
202
224
|
type IssueRead = Awaited<ReturnType<typeof issue_state_cli.read_issue>>
|
|
@@ -211,12 +233,13 @@ async function read_merged_pr(child: string, read: IssueRead): Promise<string |
|
|
|
211
233
|
}
|
|
212
234
|
|
|
213
235
|
async function read_signals(ctx: MergeContext, read: IssueRead): Promise<EndingSignals> {
|
|
214
|
-
const [is_cut, merged_pr] = await Promise.all([
|
|
236
|
+
const [is_cut, merged_pr, is_outage] = await Promise.all([
|
|
215
237
|
run_merge_steps.has_resumable_cut(ctx.child),
|
|
216
238
|
read_merged_pr(ctx.child, read),
|
|
239
|
+
read_is_retryable(ctx.child, ctx.output),
|
|
217
240
|
])
|
|
218
241
|
|
|
219
|
-
return { is_outage
|
|
242
|
+
return { is_outage, is_cut, merged_pr }
|
|
220
243
|
}
|
|
221
244
|
|
|
222
245
|
function outcome_of(read: IssueRead, signals: EndingSignals): ChildOutcome {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
2
|
+
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
3
|
+
import { lane_relaunch } from '#scripts/lane/lane-relaunch'
|
|
2
4
|
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
3
5
|
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
6
|
+
import { run_ending } from '#scripts/run/run-ending'
|
|
4
7
|
import { run_ship_detach } from '#scripts/run/ship/run-ship-detach'
|
|
5
8
|
|
|
6
9
|
// Why `run:merge` parks a child whose detached ship has already ended (joshuafolkken/kit#3808).
|
|
@@ -56,6 +59,61 @@ function cause(child: string, directory: string = process.cwd()): string | undef
|
|
|
56
59
|
return cause_of(run_ship_detach.read_result(repository, child), events, child)
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
// Whether the ended ship was never answered: a session relaunched to fix a stop writes to the
|
|
63
|
+
// transcript after it, so a transcript last written before the newest stop means no fix-up ran — its
|
|
64
|
+
// broker or relaunch could not start. A ship with no stop at all ended abnormally only when its supervisor
|
|
65
|
+
// died before reaching a stage.
|
|
66
|
+
function is_stop_unanswered(
|
|
67
|
+
ending: ShipEnding,
|
|
68
|
+
attempt: Events,
|
|
69
|
+
written_at: number | undefined,
|
|
70
|
+
): boolean {
|
|
71
|
+
const stop = attempt.findLast((event) => event.kind === run_event_stream.EVENT_KIND.SHIP_STOP)
|
|
72
|
+
|
|
73
|
+
if (stop === undefined) return ending?.result === 'abnormal'
|
|
74
|
+
|
|
75
|
+
return written_at !== undefined && written_at < Date.parse(stop.at)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Whether the child's ship ended through an interruption rather than its work, so the child is
|
|
79
|
+
// re-dispatched instead of parked.
|
|
80
|
+
function interrupted_of(
|
|
81
|
+
ending: ShipEnding,
|
|
82
|
+
events: Events,
|
|
83
|
+
child: string,
|
|
84
|
+
written_at: number | undefined,
|
|
85
|
+
): boolean {
|
|
86
|
+
if (ending === undefined || !ENDED_RESULTS.has(ending.result)) return false
|
|
87
|
+
|
|
88
|
+
const attempt = since_ship_launch(events, child)
|
|
89
|
+
|
|
90
|
+
return attempt !== undefined && is_stop_unanswered(ending, attempt, written_at)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function is_interrupted(
|
|
94
|
+
child: string,
|
|
95
|
+
written_at: number | undefined,
|
|
96
|
+
directory: string = process.cwd(),
|
|
97
|
+
): boolean {
|
|
98
|
+
const repository = git_common_directory.repository(directory)
|
|
99
|
+
if (repository === undefined) return false
|
|
100
|
+
|
|
101
|
+
const events = run_event_stream.read_events(run_event_stream.target_of(repository))
|
|
102
|
+
|
|
103
|
+
return interrupted_of(run_ship_detach.read_result(repository, child), events, child, written_at)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Whether the child's ship stopped and the relaunch meant to fix it never ran, read against its
|
|
107
|
+
// transcript at `output`. An OpenAI lane is never relaunched — its parent books every stop as an ended
|
|
108
|
+
// child — and nothing relaunches a child whose lane is gone, so only an open non-OpenAI lane reads as one.
|
|
109
|
+
async function is_unanswered(child: string, output: string | undefined): Promise<boolean> {
|
|
110
|
+
if (output === undefined || !is_interrupted(child, run_ending.written_at(output))) return false
|
|
111
|
+
|
|
112
|
+
const lane = await lane_registry.find_open_lane(child)
|
|
113
|
+
|
|
114
|
+
return lane !== undefined && !lane_relaunch.is_openai_lane(lane)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const run_merge_ship = { cause, cause_of, interrupted_of, is_unanswered }
|
|
60
118
|
|
|
61
119
|
export { run_merge_ship }
|
|
@@ -23,8 +23,9 @@ const CLOSED_STATE = 'CLOSED'
|
|
|
23
23
|
// resets the streak (`run-carry.ts` → `next_failures`), so the guard trips only on an environment
|
|
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
|
-
// Three
|
|
27
|
-
// guard, over its own streak. An outage is a child
|
|
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, or its ship stopped with no relaunch — so a run of them is
|
|
28
29
|
// the environment being down; stopping is what keeps the run from re-dispatching into a dead API
|
|
29
30
|
// forever. A merge or a genuine child failure resets it (`run-carry.ts` → `next_outages`).
|
|
30
31
|
const CONSECUTIVE_OUTAGE_LIMIT = 3
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs'
|
|
2
2
|
import { agent_exit_record } from '#scripts/agent/agent-exit-record'
|
|
3
3
|
import { api_outage } from '#scripts/agent/api-outage'
|
|
4
4
|
import type { ClaudeResultEvent } from '#scripts/agent/claude-result-event'
|
|
@@ -179,7 +179,7 @@ async function read_cut_taken(issue: string): Promise<boolean> {
|
|
|
179
179
|
return read.kind === 'carried' && read.cut.invocation === run_cut.invocation_for(issue)
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
function
|
|
182
|
+
function read_transcript(output_path: string): string | undefined {
|
|
183
183
|
const safe_path = run_liveness.to_safe_path(output_path)
|
|
184
184
|
|
|
185
185
|
if (safe_path === undefined) return undefined
|
|
@@ -188,12 +188,38 @@ function read_exit(output_path: string): ClaudeResultEvent | undefined {
|
|
|
188
188
|
// Validated by `run_liveness.to_safe_path` — normalized, required absolute, confined to the
|
|
189
189
|
// allowed roots — the same check `josh lane:output` gates a transcript path with, reused rather
|
|
190
190
|
// than re-spelled so the two cannot disagree.
|
|
191
|
-
return
|
|
191
|
+
return readFileSync(safe_path, 'utf8') // NOSONAR
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function read_exit(output_path: string): ClaudeResultEvent | undefined {
|
|
198
|
+
const transcript = read_transcript(output_path)
|
|
199
|
+
|
|
200
|
+
return transcript === undefined ? undefined : agent_exit_record.read_exit_record(transcript)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// When the child's transcript was last written, in epoch milliseconds — `undefined` when unreadable.
|
|
204
|
+
function written_at(output_path: string): number | undefined {
|
|
205
|
+
const safe_path = run_liveness.to_safe_path(output_path)
|
|
206
|
+
|
|
207
|
+
if (safe_path === undefined) return undefined
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
return statSync(safe_path).mtimeMs // NOSONAR — validated as `read_transcript` validates it
|
|
192
211
|
} catch {
|
|
193
212
|
return undefined
|
|
194
213
|
}
|
|
195
214
|
}
|
|
196
215
|
|
|
216
|
+
// The child's session was killed inside a turn rather than finishing it.
|
|
217
|
+
function ended_mid_turn(output_path: string): boolean {
|
|
218
|
+
const transcript = read_transcript(output_path)
|
|
219
|
+
|
|
220
|
+
return transcript !== undefined && agent_exit_record.is_mid_turn(transcript)
|
|
221
|
+
}
|
|
222
|
+
|
|
197
223
|
async function read_traces(request: EndingRequest): Promise<EndingTraces> {
|
|
198
224
|
const [is_child_closed, is_cut_taken, is_tree_dirty] = await Promise.all([
|
|
199
225
|
read_child_closed(request.issue, request.repo),
|
|
@@ -218,7 +244,9 @@ async function check(request: EndingRequest): Promise<EndingDecision> {
|
|
|
218
244
|
const run_ending = {
|
|
219
245
|
check,
|
|
220
246
|
decide,
|
|
247
|
+
ended_mid_turn,
|
|
221
248
|
read_exit,
|
|
249
|
+
written_at,
|
|
222
250
|
}
|
|
223
251
|
|
|
224
252
|
export type { EndingDecision, EndingRequest, EndingTraces, EndingVerdict }
|
|
@@ -53,16 +53,11 @@ function current_state(): ParentCutState {
|
|
|
53
53
|
const THRESHOLD_TEXT = `${cost_format.format_tokens(CONTEXT_CUT_THRESHOLD)}-token`
|
|
54
54
|
|
|
55
55
|
const PARENT_CUT_REASON =
|
|
56
|
-
'⛔ backlogrun parent hand-off: this session drives a `backlogrun`
|
|
57
|
-
`
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'make sure every lane in flight records an output path, then run `pnpm josh run:carry --cut --owner ' +
|
|
62
|
-
'"$PPID"` and, unless it answers `capped`, `pnpm josh run:wake`, post the progress comment and end the ' +
|
|
63
|
-
'turn. A lane nobody can poll means the cut does not happen; carry on and this asks again later. ' +
|
|
64
|
-
'Reissue this call when the hand-off is done or cannot be taken — the guard stays quiet for a while ' +
|
|
65
|
-
'after it refuses, so the hand-off calls themselves are not refused.'
|
|
56
|
+
'⛔ backlogrun parent hand-off: this session drives a `backlogrun` and its recent-context cost ' +
|
|
57
|
+
`crossed the ${THRESHOLD_TEXT} threshold between merges. Take the hand-off now: open no new lane, ` +
|
|
58
|
+
'follow `backlogrun-progress.md` → "The hand-off" (`pnpm josh run:carry --cut --owner "$PPID"`, then ' +
|
|
59
|
+
'`pnpm josh run:wake` unless `capped`) and end the turn. Reissue this call once done or if it ' +
|
|
60
|
+
'cannot be taken.'
|
|
66
61
|
|
|
67
62
|
// Neither an uncapped parent's own record nor a measured `over` is optional: a capped record has no cut
|
|
68
63
|
// left to take (`run:carry --cut` answers `capped`), and an unmeasurable session is left alone here —
|
|
@@ -343,8 +343,7 @@ async function read_candidate(
|
|
|
343
343
|
): Promise<ChildState | undefined> {
|
|
344
344
|
const state = await read_pr_of_branch(name)
|
|
345
345
|
|
|
346
|
-
if (state.pr_state !== NO_PR) return state
|
|
347
|
-
if (await has_commits_beyond(context.base, references)) return state
|
|
346
|
+
if (state.pr_state !== NO_PR || (await has_commits_beyond(context.base, references))) return state
|
|
348
347
|
|
|
349
348
|
return (await is_held_dirty(name, context.lane)) ? state : undefined
|
|
350
349
|
}
|
|
@@ -27,8 +27,7 @@ const STOP_RECOVERY =
|
|
|
27
27
|
// this type is for the run that halted needing a person. An `unreadable` record is not announced: with
|
|
28
28
|
// nothing readable there is no run this can honestly say stopped.
|
|
29
29
|
function plan(read: CarryRead, reason: string | undefined): StopNotice | undefined {
|
|
30
|
-
if (reason === undefined || reason.length === 0) return undefined
|
|
31
|
-
if (read.kind !== 'carried') return undefined
|
|
30
|
+
if (reason === undefined || reason.length === 0 || read.kind !== 'carried') return undefined
|
|
32
31
|
|
|
33
32
|
return { reason }
|
|
34
33
|
}
|
|
@@ -38,6 +37,8 @@ async function announce(notice: StopNotice): Promise<boolean> {
|
|
|
38
37
|
issue_title: STOP_TITLE,
|
|
39
38
|
body: notice.reason,
|
|
40
39
|
recovery: STOP_RECOVERY,
|
|
40
|
+
recommended: 'Resolve the reason above, then run `backlogrun` again',
|
|
41
|
+
alternatives: ['Leave the backlog stopped until a later session'],
|
|
41
42
|
})
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -49,9 +49,12 @@ async function owes_watcher(): Promise<boolean> {
|
|
|
49
49
|
// `run_progress_clock.life_target_of(git_directory)`. Passed in rather than resolved here so the
|
|
50
50
|
// caller can use whichever resolution fits its sync/async context.
|
|
51
51
|
async function check(life_target: string): Promise<GuardResult> {
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (
|
|
53
|
+
!(await owes_watcher()) ||
|
|
54
|
+
run_progress_clock.is_life_fresh(life_target, STALE_THRESHOLD_MS)
|
|
55
|
+
) {
|
|
56
|
+
return OK_RESULT
|
|
57
|
+
}
|
|
55
58
|
|
|
56
59
|
return { kind: 'stale', note: STALE_NOTE }
|
|
57
60
|
}
|
|
@@ -59,7 +59,7 @@ const ROUND_ONE_RECORDED_NOTE =
|
|
|
59
59
|
// single-sourced with the preflight and gate stages.
|
|
60
60
|
const { run_phases, scoped_pair } = run_ship_scoped
|
|
61
61
|
|
|
62
|
-
type PromptOf = (brief_path: string, findings_path: string) => string
|
|
62
|
+
type PromptOf = (brief_path: string, findings_path: string, issue: string) => string
|
|
63
63
|
|
|
64
64
|
function failure(out: string): JoshResult {
|
|
65
65
|
return { code: FAILURE_EXIT_CODE, out }
|
|
@@ -138,7 +138,12 @@ async function launch_reviewer(
|
|
|
138
138
|
stamp_file.write_text_stamp(target.brief, brief)
|
|
139
139
|
stamp_file.remove_stamp(target.findings)
|
|
140
140
|
|
|
141
|
-
return await run_session(
|
|
141
|
+
return await run_session(
|
|
142
|
+
prompt_of(target.brief, target.findings, issue),
|
|
143
|
+
target.log,
|
|
144
|
+
issue,
|
|
145
|
+
round,
|
|
146
|
+
)
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
function current_verdict(): ReturnType<typeof run_ship_review.read_verdict> {
|
|
@@ -153,8 +158,12 @@ function current_verdict(): ReturnType<typeof run_ship_review.read_verdict> {
|
|
|
153
158
|
async function join_gate(): Promise<JoshResult> {
|
|
154
159
|
const joined = await josh([RUN_REVIEW, '--join'])
|
|
155
160
|
|
|
156
|
-
if (
|
|
157
|
-
|
|
161
|
+
if (
|
|
162
|
+
joined.code === SUCCESS_EXIT_CODE ||
|
|
163
|
+
!run_ship_review.has_fixes(stamp_file.read_stamp_text(paths().findings))
|
|
164
|
+
) {
|
|
165
|
+
return joined
|
|
166
|
+
}
|
|
158
167
|
|
|
159
168
|
note(FIXED_JOIN_NOTE)
|
|
160
169
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
1
2
|
import { review_finding_ledger } from '#scripts/review/review-finding-ledger'
|
|
2
3
|
import { review_record_cli } from '#scripts/review/review-record-cli'
|
|
3
4
|
|
|
@@ -83,12 +84,23 @@ function findings_instruction(findings_path: string, fixed_clause: string): stri
|
|
|
83
84
|
].join(' ')
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// The rubric's requirement check sends the reviewer to the Issue, and a body-only read there is refused
|
|
88
|
+
// by the issue-comments rule — so the prompt names the read that carries the comments with the body
|
|
89
|
+
// (joshuafolkken/kit#3875).
|
|
90
|
+
function issue_read_instruction(issue: string): string {
|
|
91
|
+
return [
|
|
92
|
+
`Read Issue ${issue_cite.plain(issue)} with \`pnpm josh issue:read ${issue}\`, which prints the body and every`,
|
|
93
|
+
`comment together — never a body-only \`gh api …/issues/${issue}\` read.`,
|
|
94
|
+
].join(' ')
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
// One line, because `detached_launch` refuses an argument carrying a control character: the multi-line
|
|
87
98
|
// brief travels by path, never inline.
|
|
88
|
-
function reviewer_prompt(brief_path: string, findings_path: string): string {
|
|
99
|
+
function reviewer_prompt(brief_path: string, findings_path: string, issue: string): string {
|
|
89
100
|
return [
|
|
90
101
|
`Run a /code-review of this checkout: read the review brief at ${brief_path} and follow it exactly —`,
|
|
91
102
|
'the level on its first line, the rubric it names, the checkout it pins and its review:attest step.',
|
|
103
|
+
issue_read_instruction(issue),
|
|
92
104
|
'Apply a fix only for a Medium finding whose fix is small and local — a one-function refactor, an',
|
|
93
105
|
'added test, an added guard; never fix a High or a change that reaches beyond one place, and never',
|
|
94
106
|
'commit, stage or push. After any fix, run `pnpm josh lint:related && pnpm josh test:related` and',
|
|
@@ -98,11 +110,12 @@ function reviewer_prompt(brief_path: string, findings_path: string): string {
|
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
// The round-2 verification pass: a fresh session over the round-1 fix delta, reading only.
|
|
101
|
-
function verification_prompt(brief_path: string, findings_path: string): string {
|
|
113
|
+
function verification_prompt(brief_path: string, findings_path: string, issue: string): string {
|
|
102
114
|
return [
|
|
103
115
|
`Run a /code-review of this checkout — the round-2 verification pass over the round-1 fixes: read the`,
|
|
104
116
|
`review brief at ${brief_path} and follow it exactly — the level on its first line, the rubric it`,
|
|
105
117
|
'names, the checkout it pins and its review:attest step. Do not edit any file.',
|
|
118
|
+
issue_read_instruction(issue),
|
|
106
119
|
findings_instruction(findings_path, ''),
|
|
107
120
|
].join(' ')
|
|
108
121
|
}
|
|
@@ -33,12 +33,12 @@ function memoized(read: IsMerged): IsMerged {
|
|
|
33
33
|
|
|
34
34
|
async function sweep(): Promise<void> {
|
|
35
35
|
try {
|
|
36
|
-
const
|
|
36
|
+
const is_merged = memoized(issue_merged.read_merged)
|
|
37
37
|
const in_flight = backlog_drive_restore.active_issues(
|
|
38
38
|
await run_event_stream_emit.current_events(),
|
|
39
39
|
)
|
|
40
|
-
const lanes = await run_tidy_lanes.tidy_lanes(
|
|
41
|
-
const stashes = await run_tidy_stashes.tidy_stashes(
|
|
40
|
+
const lanes = await run_tidy_lanes.tidy_lanes(is_merged, in_flight)
|
|
41
|
+
const stashes = await run_tidy_stashes.tidy_stashes(is_merged)
|
|
42
42
|
const report = run_tidy.format_report([...lanes, ...stashes])
|
|
43
43
|
|
|
44
44
|
if (report !== undefined) console.error(report)
|
|
@@ -95,8 +95,9 @@ async function tidy_lane(
|
|
|
95
95
|
is_merged: IsMerged,
|
|
96
96
|
in_flight: ReadonlySet<string>,
|
|
97
97
|
): Promise<Outcome | undefined> {
|
|
98
|
-
if (in_flight.has(lane.issue))
|
|
99
|
-
|
|
98
|
+
if (in_flight.has(lane.issue) || lane.is_stranded || !(await is_merged(lane.issue))) {
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
100
101
|
|
|
101
102
|
return { target: `lane ${session_cite.issue(lane.issue)}`, verdict: await settle(lane) }
|
|
102
103
|
}
|
|
@@ -126,8 +126,13 @@ function merge_project_config(existing: string, workspace: string): string {
|
|
|
126
126
|
const aikido = yaml_document.parse_yaml(existing)
|
|
127
127
|
const safe_chain = read_mapping(aikido['safe-chain'], 'safe-chain')
|
|
128
128
|
const merged = merge_age(merge_exclusions(safe_chain, exclusions), workspace)
|
|
129
|
-
|
|
130
|
-
if (
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
JSON.stringify(merged) === JSON.stringify(safe_chain) ||
|
|
132
|
+
has_nothing_to_write(existing, exclusions, workspace)
|
|
133
|
+
) {
|
|
134
|
+
return existing
|
|
135
|
+
}
|
|
131
136
|
|
|
132
137
|
return replace_safe_chain_block(existing, merged)
|
|
133
138
|
}
|
|
@@ -65,8 +65,14 @@ function hook_write_warning(
|
|
|
65
65
|
filename: string,
|
|
66
66
|
): string | undefined {
|
|
67
67
|
const installed_version = installed_consumer_version(project_root)
|
|
68
|
-
|
|
69
|
-
if (
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
writing_version === undefined ||
|
|
71
|
+
installed_version === undefined ||
|
|
72
|
+
is_safe_to_write_hooks(writing_version, installed_version)
|
|
73
|
+
) {
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
70
76
|
|
|
71
77
|
return outdated_install_warning(writing_version, installed_version, filename)
|
|
72
78
|
}
|
|
@@ -186,8 +186,9 @@ function worker_arguments(
|
|
|
186
186
|
vitest_arguments: ReadonlyArray<string>,
|
|
187
187
|
share: number | undefined,
|
|
188
188
|
): Array<string> {
|
|
189
|
-
if (share === undefined)
|
|
190
|
-
|
|
189
|
+
if (share === undefined || vitest_arguments.some((argument) => is_worker_flag(argument))) {
|
|
190
|
+
return []
|
|
191
|
+
}
|
|
191
192
|
|
|
192
193
|
return [`${WORKER_FLAG}=${String(share)}`]
|
|
193
194
|
}
|
|
@@ -51,12 +51,12 @@ type NamedCall = Pick<Span, 'is_bundleable' | 'is_writing' | 'label' | 'targets'
|
|
|
51
51
|
const RULE_POINTER = '`prompts/collaboration-workflow/turn-batching.md`'
|
|
52
52
|
const STREAK = `${String(CONSECUTIVE_LIMIT)} single-call turns in a row`
|
|
53
53
|
|
|
54
|
-
// Includes what to do when
|
|
54
|
+
// Includes what to do when there is nothing to pair — the guard cannot see the turn it interrupted.
|
|
55
55
|
const REASON =
|
|
56
56
|
`⛔ batching: ${STREAK}. Reissue this with the calls that do not need its result — the test is ` +
|
|
57
57
|
`whether a call's input needs another call's result, not what kind of call it is (edits are covered ` +
|
|
58
|
-
`exactly as reads)
|
|
59
|
-
`
|
|
58
|
+
`exactly as reads); it never authorizes weakening a verification gate or a review. Nothing to ` +
|
|
59
|
+
`pair? Reissue it unchanged. ${RULE_POINTER}`
|
|
60
60
|
|
|
61
61
|
// A whole-file write earns a notice, not a refusal: a `Write` cannot be refused safely.
|
|
62
62
|
const NOTICE =
|
|
@@ -68,8 +68,9 @@ function fork_path(transcript_path: string, agent_id: string): string {
|
|
|
68
68
|
// **A path that is not a transcript is answered with itself**, since nothing can be derived from it and
|
|
69
69
|
// guessing would name a file in some other layout entirely.
|
|
70
70
|
function transcript_of(transcript_path: string, agent_id: string | null | undefined): string {
|
|
71
|
-
if (!is_agent_id(agent_id)
|
|
72
|
-
|
|
71
|
+
if (!is_agent_id(agent_id) || !transcript_path.endsWith(TRANSCRIPT_EXTENSION)) {
|
|
72
|
+
return transcript_path
|
|
73
|
+
}
|
|
73
74
|
|
|
74
75
|
return fork_path(transcript_path, agent_id)
|
|
75
76
|
}
|
|
@@ -189,6 +189,15 @@ function unquoted(command: string): string {
|
|
|
189
189
|
return without_heredoc_bodies(command).replaceAll(QUOTED_SPAN_PATTERN, ' ')
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// Quoted text replaced by spaces of its own length, so an offset found here is the same offset in the
|
|
193
|
+
// command — what a caller cutting the command where the unquoted reading says to needs. `undefined`
|
|
194
|
+
// for a line carrying a heredoc, whose body lines a cut would read as commands of their own.
|
|
195
|
+
function blanked(command: string): string | undefined {
|
|
196
|
+
if (HEREDOC_MARKER.test(command)) return undefined
|
|
197
|
+
|
|
198
|
+
return command.replaceAll(QUOTED_SPAN_PATTERN, (span) => ' '.repeat(span.length))
|
|
199
|
+
}
|
|
200
|
+
|
|
192
201
|
function discarded_commands(command: string): Array<string> {
|
|
193
202
|
const plain = unquoted(command)
|
|
194
203
|
|
|
@@ -308,6 +317,7 @@ const time_shell = {
|
|
|
308
317
|
JOSH_PREFIX,
|
|
309
318
|
bash_command,
|
|
310
319
|
bash_label,
|
|
320
|
+
blanked,
|
|
311
321
|
command_segment,
|
|
312
322
|
discarded_commands,
|
|
313
323
|
josh_arguments,
|
|
@@ -23,8 +23,7 @@ function read_matching_version(manifest_path: string, package_name: string): str
|
|
|
23
23
|
const parsed = package_named_version_schema.safeParse(
|
|
24
24
|
json_value.parse_or_undefined(readFileSync(manifest_path, 'utf8')),
|
|
25
25
|
)
|
|
26
|
-
if (!parsed.success) return undefined
|
|
27
|
-
if (parsed.data.name !== package_name) return undefined
|
|
26
|
+
if (!parsed.success || parsed.data.name !== package_name) return undefined
|
|
28
27
|
|
|
29
28
|
return parsed.data.version
|
|
30
29
|
}
|
|
@@ -45,10 +44,10 @@ function walk_up_for_version(start_directory: string, package_name: string): str
|
|
|
45
44
|
function resolve_from_base(
|
|
46
45
|
base_url: string,
|
|
47
46
|
package_name: string,
|
|
48
|
-
resolve_marker
|
|
47
|
+
resolve_marker = package_name,
|
|
49
48
|
): string | undefined {
|
|
50
49
|
try {
|
|
51
|
-
return createRequire(base_url).resolve(resolve_marker
|
|
50
|
+
return createRequire(base_url).resolve(resolve_marker)
|
|
52
51
|
} catch {
|
|
53
52
|
return undefined
|
|
54
53
|
}
|
|
@@ -13,8 +13,14 @@ const SELF_DIRECTORY = path.dirname(fileURLToPath(import.meta.url))
|
|
|
13
13
|
function read_shadow_warning(): string | undefined {
|
|
14
14
|
const path_josh = doctor_io.resolve_path_josh()
|
|
15
15
|
const global_josh = doctor_io.resolve_pnpm_global_josh()
|
|
16
|
-
|
|
17
|
-
if (
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
path_josh === undefined ||
|
|
19
|
+
global_josh === undefined ||
|
|
20
|
+
!doctor_logic.is_shadowed(path_josh, global_josh)
|
|
21
|
+
) {
|
|
22
|
+
return undefined
|
|
23
|
+
}
|
|
18
24
|
|
|
19
25
|
return doctor_logic.format_shadow_warning(path_josh, global_josh)
|
|
20
26
|
}
|
|
@@ -259,8 +259,7 @@ function restore_package_json(
|
|
|
259
259
|
|
|
260
260
|
function decode_integrity(encoded: string): string | undefined {
|
|
261
261
|
const bytes = Buffer.from(encoded, 'base64')
|
|
262
|
-
if (bytes.length !== SHA512_BYTES) return undefined
|
|
263
|
-
if (bytes.toString('base64') !== encoded) return undefined
|
|
262
|
+
if (bytes.length !== SHA512_BYTES || bytes.toString('base64') !== encoded) return undefined
|
|
264
263
|
|
|
265
264
|
return `sha512.${bytes.toString('hex')}`
|
|
266
265
|
}
|
|
@@ -51,8 +51,10 @@ function resolve_installable(
|
|
|
51
51
|
minimum_age_minutes: number,
|
|
52
52
|
now_ms: number,
|
|
53
53
|
): string | undefined {
|
|
54
|
-
if (times === undefined)
|
|
55
|
-
|
|
54
|
+
if (times === undefined || !is_latest_withheld(times, latest, minimum_age_minutes, now_ms)) {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
56
58
|
const installable = release_age.select_aged_version(times, undefined, minimum_age_minutes, now_ms)
|
|
57
59
|
|
|
58
60
|
return installable === latest ? undefined : installable
|
|
@@ -122,8 +124,13 @@ function build_release_hold_notes(
|
|
|
122
124
|
installable: string | undefined,
|
|
123
125
|
minimum_age_minutes: number,
|
|
124
126
|
): Array<string> {
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
+
if (
|
|
128
|
+
version === undefined ||
|
|
129
|
+
installable === undefined ||
|
|
130
|
+
!is_release_age_hold(version, latest, installable)
|
|
131
|
+
) {
|
|
132
|
+
return []
|
|
133
|
+
}
|
|
127
134
|
|
|
128
135
|
return [format_release_hold_note(version, latest, installable, minimum_age_minutes)]
|
|
129
136
|
}
|
|
@@ -101,8 +101,10 @@ function build_upstream_effective(
|
|
|
101
101
|
context: UpstreamHookContext,
|
|
102
102
|
): UpstreamEffective | undefined {
|
|
103
103
|
const { resolve_effective_version, resolve_global_upgrade_command } = upstream
|
|
104
|
-
|
|
105
|
-
if (resolve_global_upgrade_command === undefined)
|
|
104
|
+
|
|
105
|
+
if (resolve_effective_version === undefined || resolve_global_upgrade_command === undefined) {
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
106
108
|
|
|
107
109
|
return {
|
|
108
110
|
version: resolve_effective_version(context),
|
|
@@ -264,8 +266,14 @@ function describe_effective_outcome(
|
|
|
264
266
|
snapshot: VersionSnapshot,
|
|
265
267
|
): Array<string> {
|
|
266
268
|
const { resolve_effective_version } = upstream
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
+
|
|
270
|
+
if (
|
|
271
|
+
resolve_effective_version === undefined ||
|
|
272
|
+
report === undefined ||
|
|
273
|
+
version_check_logic.build_effective_upgrade_commands(report).length === 0
|
|
274
|
+
) {
|
|
275
|
+
return []
|
|
276
|
+
}
|
|
269
277
|
|
|
270
278
|
const after = resolve_effective_version({
|
|
271
279
|
latest: snapshot.latest,
|