@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
|
@@ -4,6 +4,7 @@ import type { ClosedAnswer, ClosedIssue } from './run-board-closed'
|
|
|
4
4
|
import type { Fetch } from './run-board-fetch'
|
|
5
5
|
import type { TerminalSize } from './run-board-fit'
|
|
6
6
|
import type { BoardForm } from './run-board-header'
|
|
7
|
+
import { run_board_idle, type IdleSince } from './run-board-idle'
|
|
7
8
|
import type { BoardPlan } from './run-board-layout'
|
|
8
9
|
import type { Link } from './run-board-link'
|
|
9
10
|
import type { MachineGauges, MachineMark } from './run-board-machine'
|
|
@@ -56,6 +57,8 @@ interface BoardState {
|
|
|
56
57
|
// The last local read, drawn until the next is due; `undefined` when no run had started.
|
|
57
58
|
local: LocalRead | undefined
|
|
58
59
|
read_ms: number | undefined
|
|
60
|
+
// When each lane the reads found idle was first read so (`run-board-idle.ts`).
|
|
61
|
+
idle_since: IdleSince
|
|
59
62
|
// The start of the run the plan state was built for, so a run that begins draws nothing of the last.
|
|
60
63
|
run_started_ms: number | undefined
|
|
61
64
|
plan: BoardPlan | undefined
|
|
@@ -91,6 +94,7 @@ interface BoardState {
|
|
|
91
94
|
const FRESH_STATE: BoardState = {
|
|
92
95
|
local: undefined,
|
|
93
96
|
read_ms: undefined,
|
|
97
|
+
idle_since: run_board_idle.NO_IDLE,
|
|
94
98
|
run_started_ms: undefined,
|
|
95
99
|
plan: undefined,
|
|
96
100
|
plan_fetch: undefined,
|
|
@@ -66,6 +66,9 @@ const ENDING_STATES: Readonly<Partial<Record<string, ItemState>>> = {
|
|
|
66
66
|
const CONTINUING_KINDS: ReadonlySet<string> = new Set([KIND.CUT, KIND.RESUME])
|
|
67
67
|
// A person's restart starts the child over in a fresh session, so its track starts over too.
|
|
68
68
|
const LAUNCH_KINDS: ReadonlySet<string> = new Set([KIND.CHILD_LAUNCH, KIND.RESTART])
|
|
69
|
+
// What the GitHub read may still settle: a running child, and one whose lane has no child left in it —
|
|
70
|
+
// a child that merged and then died is drawn merged, not stopped.
|
|
71
|
+
const UNSETTLED_STATES: ReadonlySet<ItemState> = new Set(['running', 'stopped'])
|
|
69
72
|
|
|
70
73
|
function moment(event: RunEvent): number {
|
|
71
74
|
return Date.parse(event.at)
|
|
@@ -175,13 +178,25 @@ function is_running_lane(
|
|
|
175
178
|
return in_run.has(issue) && !run_board_setup.is_lane_pending(events, issue)
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
// A lane no child is working in is stopped whatever the stream last said: a child that died writes no
|
|
182
|
+
// ending, so its row would otherwise run — and its phase time grow — for as long as the lane stays.
|
|
183
|
+
function lane_status(status: ItemStatus | undefined, lane: string, is_idle: boolean): ItemStatus {
|
|
184
|
+
const held = { lane: `lane ${lane}` }
|
|
185
|
+
|
|
186
|
+
return is_idle
|
|
187
|
+
? { ...settled_as(status, 'stopped'), ...held }
|
|
188
|
+
: { state: 'running', ...status, ...held }
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
// Every child the run has touched, with the lane each running child holds. A lane the stream never saw
|
|
179
192
|
// launched — a run restored after the stream rolled — still reads as running while its issue is in the
|
|
180
|
-
// run's plan (`in_run`); one the run is still setting up for stays waiting until its launch.
|
|
193
|
+
// run's plan (`in_run`); one the run is still setting up for stays waiting until its launch. A lane in
|
|
194
|
+
// `idle` has no child working in it (`lane-spent.ts`).
|
|
181
195
|
function statuses_of(
|
|
182
196
|
events: ReadonlyArray<RunEvent>,
|
|
183
197
|
lanes: ReadonlyArray<string>,
|
|
184
198
|
in_run: ReadonlySet<number>,
|
|
199
|
+
idle: ReadonlyArray<string> = [],
|
|
185
200
|
): ReadonlyMap<number, ItemStatus> {
|
|
186
201
|
const statuses = fold_events(events)
|
|
187
202
|
|
|
@@ -190,7 +205,7 @@ function statuses_of(
|
|
|
190
205
|
const status = statuses.get(issue)
|
|
191
206
|
|
|
192
207
|
if (is_running_lane(issue, status, events, in_run)) {
|
|
193
|
-
statuses.set(issue,
|
|
208
|
+
statuses.set(issue, lane_status(status, lane, idle.includes(lane)))
|
|
194
209
|
}
|
|
195
210
|
}
|
|
196
211
|
|
|
@@ -260,7 +275,7 @@ function closed_status(
|
|
|
260
275
|
}
|
|
261
276
|
|
|
262
277
|
function settled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus {
|
|
263
|
-
if (status.state
|
|
278
|
+
if (!UNSETTLED_STATES.has(status.state)) return status
|
|
264
279
|
|
|
265
280
|
const from_closed = closed_status(status, read.closed.get(issue))
|
|
266
281
|
|
|
@@ -17,8 +17,13 @@ type ThanksState = Pick<BoardState, 'thanks' | 'thanks_fetch' | 'thanks_attempte
|
|
|
17
17
|
function launch_thanks(state: BoardState, ports: BoardPorts, now_ms: number): BoardState {
|
|
18
18
|
const { read_thanks } = ports
|
|
19
19
|
|
|
20
|
-
if (
|
|
21
|
-
|
|
20
|
+
if (
|
|
21
|
+
read_thanks === undefined ||
|
|
22
|
+
state.thanks_fetch !== undefined ||
|
|
23
|
+
!is_due(state.thanks_attempted_ms, TTL_MS, now_ms)
|
|
24
|
+
) {
|
|
25
|
+
return state
|
|
26
|
+
}
|
|
22
27
|
|
|
23
28
|
const thanks_fetch: Fetch<Thanks> = run_board_fetch.launch(read_thanks, state.thanks)
|
|
24
29
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { lane_await } from '#scripts/lane/lane-await'
|
|
1
2
|
import { run_progress } from '#scripts/run/progress/run-progress'
|
|
2
3
|
import type { ClosedIssue } from './run-board-closed'
|
|
3
4
|
import { run_board_github } from './run-board-github'
|
|
4
5
|
import { run_board_header, type BoardHeader } from './run-board-header'
|
|
6
|
+
import { run_board_idle } from './run-board-idle'
|
|
5
7
|
import { run_board_labels } from './run-board-labels'
|
|
6
8
|
import { run_board_layout, type BoardLayout, type BoardPlan } from './run-board-layout'
|
|
7
9
|
import { run_board_link } from './run-board-link'
|
|
@@ -34,10 +36,30 @@ const { FRESH_STATE, is_due } = run_board_state
|
|
|
34
36
|
const { PLAN_RETRY_MS, gather_github, statuses_for } = run_board_github
|
|
35
37
|
const { gather_thanks } = run_board_thanks
|
|
36
38
|
|
|
37
|
-
async function
|
|
39
|
+
async function read_at(state: BoardState, ports: BoardPorts, now_ms: number): Promise<BoardState> {
|
|
40
|
+
const local = await ports.read_local()
|
|
41
|
+
const idle_since = run_board_idle.since_of(state.idle_since, local?.idle_lanes ?? [], now_ms)
|
|
42
|
+
|
|
43
|
+
return { ...state, local, read_ms: now_ms, idle_since }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A single frame has no earlier read to confirm an idle lane against (`run-board-idle.ts`).
|
|
47
|
+
function is_unconfirmed(before: BoardState, read: BoardState, pace: Pace): boolean {
|
|
48
|
+
return pace === 'settled' && before.read_ms === undefined && read.idle_since.size > 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function reread(state: BoardState, ports: BoardPorts, pace: Pace): Promise<BoardState> {
|
|
52
|
+
const now_ms = ports.now()
|
|
53
|
+
|
|
38
54
|
if (!is_due(state.read_ms, LOCAL_READ_MS, now_ms)) return state
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
const read = await read_at(state, ports, now_ms)
|
|
57
|
+
|
|
58
|
+
if (!is_unconfirmed(state, read, pace)) return read
|
|
59
|
+
|
|
60
|
+
await ports.sleep(lane_await.RECONFIRM_MS)
|
|
61
|
+
|
|
62
|
+
return await read_at(read, ports, ports.now())
|
|
41
63
|
}
|
|
42
64
|
|
|
43
65
|
// Each lane's usage, read on the machine's second; a reading that failed draws no column.
|
|
@@ -69,9 +91,9 @@ async function resample(state: BoardState, ports: BoardPorts, now_ms: number): P
|
|
|
69
91
|
function state_for(state: BoardState, local: LocalRead): BoardState {
|
|
70
92
|
if (state.run_started_ms === local.started_ms) return state
|
|
71
93
|
|
|
72
|
-
const { local: kept, read_ms } = state
|
|
94
|
+
const { local: kept, read_ms, idle_since } = state
|
|
73
95
|
|
|
74
|
-
return { ...FRESH_STATE, local: kept, read_ms, run_started_ms: local.started_ms }
|
|
96
|
+
return { ...FRESH_STATE, local: kept, read_ms, idle_since, run_started_ms: local.started_ms }
|
|
75
97
|
}
|
|
76
98
|
|
|
77
99
|
// The plan with a closed child's title and labels under the listing's own.
|
|
@@ -108,7 +130,7 @@ function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): B
|
|
|
108
130
|
closed,
|
|
109
131
|
labels: plan.labels,
|
|
110
132
|
}
|
|
111
|
-
const statuses = run_board_status.settle_closed(statuses_for(plan, local), read)
|
|
133
|
+
const statuses = run_board_status.settle_closed(statuses_for(state, plan, local), read)
|
|
112
134
|
const reason_of = run_board_reason.reader({
|
|
113
135
|
plan,
|
|
114
136
|
statuses,
|
|
@@ -125,8 +147,10 @@ function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): B
|
|
|
125
147
|
function layout_for(state: BoardState, local: LocalRead): BoardLayout {
|
|
126
148
|
if (state.plan !== undefined) return planned_layout(state, state.plan, local)
|
|
127
149
|
|
|
150
|
+
const stopped = run_board_idle.stopped_of(state.idle_since, state.read_ms)
|
|
151
|
+
|
|
128
152
|
return run_board_layout.local_layout_of(
|
|
129
|
-
run_board_status.statuses_of(local.events, local.lanes, new Set()),
|
|
153
|
+
run_board_status.statuses_of(local.events, local.lanes, new Set(), stopped),
|
|
130
154
|
)
|
|
131
155
|
}
|
|
132
156
|
|
|
@@ -220,8 +244,9 @@ async function tick(
|
|
|
220
244
|
ports: BoardPorts,
|
|
221
245
|
pace: Pace = 'settled',
|
|
222
246
|
): Promise<BoardState> {
|
|
247
|
+
const read = await reread(state, ports, pace)
|
|
248
|
+
// Taken after the read, which a single frame may have waited on to confirm an idle lane.
|
|
223
249
|
const now_ms = ports.now()
|
|
224
|
-
const read = await reread(state, ports, now_ms)
|
|
225
250
|
const { local } = read
|
|
226
251
|
|
|
227
252
|
if (local === undefined) {
|
|
@@ -269,9 +269,7 @@ function to_named_request(values: ParsedValues, owner: CarryOwner): Request | un
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
function to_request(values: ParsedValues): Request | undefined {
|
|
272
|
-
if (group_count(values) > ONE_GROUP) return undefined
|
|
273
|
-
|
|
274
|
-
if (!is_summary_paired(values)) return undefined
|
|
272
|
+
if (group_count(values) > ONE_GROUP || !is_summary_paired(values)) return undefined
|
|
275
273
|
|
|
276
274
|
const owner = to_owner(values.owner)
|
|
277
275
|
|
|
@@ -76,8 +76,13 @@ function is_same_conversation(carry: RunCarry, owner: CarryOwner): boolean {
|
|
|
76
76
|
// a new one, so the record goes on naming a live process once the conversation falls quiet. A record a
|
|
77
77
|
// cut handed off is the successor's to take, never the cutting conversation's to take back.
|
|
78
78
|
function reclaim_owner(carry: RunCarry, owner: CarryOwner): RunCarry {
|
|
79
|
-
if (
|
|
80
|
-
|
|
79
|
+
if (
|
|
80
|
+
carry.is_handed_off === true ||
|
|
81
|
+
!is_same_conversation(carry, owner) ||
|
|
82
|
+
owner.pid === carry.owner_pid
|
|
83
|
+
) {
|
|
84
|
+
return carry
|
|
85
|
+
}
|
|
81
86
|
|
|
82
87
|
return { ...carry, owner_pid: owner.pid, owner_start: owner.start }
|
|
83
88
|
}
|
|
@@ -3,7 +3,6 @@ import { fileURLToPath } from 'node:url'
|
|
|
3
3
|
import { agent_session_role } from '#scripts/agent/agent-session-role'
|
|
4
4
|
import { cost_cli } from '#scripts/cost-runtime/cost-cli'
|
|
5
5
|
import { cost_verdict } from '#scripts/cost-runtime/cost-verdict'
|
|
6
|
-
import { git_command } from '#scripts/git/git-command'
|
|
7
6
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
8
7
|
import { session_cite } from '#scripts/issue/session-cite'
|
|
9
8
|
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
@@ -12,9 +11,11 @@ import { openai_lane_supervisor } from '#scripts/lane/openai-lane-supervisor'
|
|
|
12
11
|
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
13
12
|
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
14
13
|
import { run_cli_fault } from '#scripts/run/run-cli-fault'
|
|
15
|
-
import { run_cut, type
|
|
14
|
+
import { run_cut, type RunCut } from './run-cut'
|
|
16
15
|
import { run_cut_args, type Request } from './run-cut-args'
|
|
16
|
+
import { run_cut_decline } from './run-cut-decline'
|
|
17
17
|
import { run_cut_handoff } from './run-cut-handoff'
|
|
18
|
+
import { run_cut_ready } from './run-cut-ready'
|
|
18
19
|
import { run_cut_report } from './run-cut-report'
|
|
19
20
|
|
|
20
21
|
// `josh run:cut` — the record that lets a lane child end its process before the gate and a fresh one
|
|
@@ -57,6 +58,7 @@ const {
|
|
|
57
58
|
report_busy,
|
|
58
59
|
report_fault,
|
|
59
60
|
report_handed_off,
|
|
61
|
+
report_held_cut,
|
|
60
62
|
report_incomplete,
|
|
61
63
|
report_over,
|
|
62
64
|
report_reviewer,
|
|
@@ -125,11 +127,8 @@ function relaunch(target: string, lane: LaneInfo, phase: string): number {
|
|
|
125
127
|
return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return state.branch !== default_branch && state.is_dirty
|
|
132
|
-
}
|
|
130
|
+
const { is_lane_branch } = run_cut_ready
|
|
131
|
+
const { declined } = run_cut_decline
|
|
133
132
|
|
|
134
133
|
// An expired record is a fresh process that never started; it is cleared so the next cut can begin
|
|
135
134
|
// rather than being wedged behind a stale file the exclusive create would refuse forever.
|
|
@@ -204,16 +203,6 @@ async function finish_cut(target: string, lane: LaneInfo, request: CutRequest):
|
|
|
204
203
|
: relaunch(target, lane, request.phase)
|
|
205
204
|
}
|
|
206
205
|
|
|
207
|
-
// The cut outside a lane is taken, and nothing is relaunched: the session that took it is the one a
|
|
208
|
-
// person is watching, so the run is handed on the way `fullrun`'s entry `over` stop hands it on.
|
|
209
|
-
function report_held_cut(issue: string): number {
|
|
210
|
-
console.error(
|
|
211
|
-
`The cut is recorded for ${session_cite.issue(issue)} outside a lane, so nothing was relaunched. Keep the hold, send a \`confirmation\` Telegram whose body names the resume command \`fullrun ${issue_cite.plain(issue)}\`, and end the turn; the fresh session's \`pnpm josh run:cut --resume ${issue}\` answers \`resume-impl\` with the handoff.`,
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
206
|
// **A `fullrun` held in its own checkout takes the implementation cut too**, so a run a person started
|
|
218
207
|
// has a bound on its context mid-implementation. The hold naming this issue is what marks the run — a
|
|
219
208
|
// tree nobody holds for it, and the pre-gate phase (which a held run's gate reaches in the same
|
|
@@ -228,8 +217,11 @@ async function cut_outside_lane(
|
|
|
228
217
|
|
|
229
218
|
const state = await run_cut.current_state()
|
|
230
219
|
|
|
231
|
-
if (state.held_issue !== request.issue)
|
|
232
|
-
|
|
220
|
+
if (state.held_issue !== request.issue) {
|
|
221
|
+
return await declined(request, state, report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!(await is_lane_branch(state))) return await declined(request, state, refuse_unready(state))
|
|
233
225
|
|
|
234
226
|
clear_expired(target)
|
|
235
227
|
|
|
@@ -250,7 +242,10 @@ async function cut(
|
|
|
250
242
|
|
|
251
243
|
const state = await run_cut.current_state()
|
|
252
244
|
|
|
253
|
-
if (!(await is_lane_branch(state)))
|
|
245
|
+
if (!(await is_lane_branch(state))) {
|
|
246
|
+
return await declined({ issue, phase }, state, refuse_unready(state))
|
|
247
|
+
}
|
|
248
|
+
|
|
254
249
|
if (skips_pre_gate_cut(phase)) return report_under_threshold()
|
|
255
250
|
|
|
256
251
|
clear_expired(target)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
2
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { run_cut, type CutState } from './run-cut'
|
|
5
|
+
|
|
6
|
+
// The implementation cut a run asked for and was refused — `not-a-lane` or `unready` — recorded so the
|
|
7
|
+
// implementation-cut guard stops asking for it (joshuafolkken/kit#3881).
|
|
8
|
+
//
|
|
9
|
+
// **The reissue window alone re-asked every 90 seconds.** A run whose `run:cut --impl` can only answer
|
|
10
|
+
// `unready` was refused edit after edit, each refusal a round trip that cut nothing. Neither answer
|
|
11
|
+
// changes while the same hold stands over the tree, so the record names the issue and the hold it
|
|
12
|
+
// answered, and the guard stays silent while both still match; a fresh hold — a new run — asks again.
|
|
13
|
+
|
|
14
|
+
const DECLINE_PREFIX = 'josh-run-cut-decline-'
|
|
15
|
+
|
|
16
|
+
const decline_schema = z.object({ issue: z.string(), hold_taken_at: z.string().optional() })
|
|
17
|
+
|
|
18
|
+
type CutDecline = z.infer<typeof decline_schema>
|
|
19
|
+
|
|
20
|
+
function decline_path(git_directory: string): string {
|
|
21
|
+
return stamp_file.stamp_path(DECLINE_PREFIX, git_directory)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function record(git_directory: string, decline: CutDecline): void {
|
|
25
|
+
stamp_file.replace_stamp(decline_path(git_directory), decline)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function read(git_directory: string): CutDecline | undefined {
|
|
29
|
+
const raw = stamp_file.read_stamp_text(decline_path(git_directory))
|
|
30
|
+
|
|
31
|
+
return raw === undefined ? undefined : json_value.parse_with(raw, decline_schema)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function matches(recorded: CutDecline | undefined, current: CutDecline): boolean {
|
|
35
|
+
return recorded?.issue === current.issue && recorded.hold_taken_at === current.hold_taken_at
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The same read taken synchronously, for the `PreToolUse` guard. A checkout git cannot describe has no
|
|
39
|
+
// record, so the guard keeps speaking there.
|
|
40
|
+
function is_declined_sync(
|
|
41
|
+
issue: string,
|
|
42
|
+
directory: string | undefined = run_cut.worktree_git_directory_sync(),
|
|
43
|
+
): boolean {
|
|
44
|
+
if (directory === undefined) return false
|
|
45
|
+
|
|
46
|
+
const hold_taken_at = run_cut.hold_of(directory)?.taken_at
|
|
47
|
+
|
|
48
|
+
return matches(read(directory), { issue, hold_taken_at })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// **An implementation cut `run:cut` refuses is recorded for the guard that asked for it**, then its
|
|
52
|
+
// exit code is passed back unchanged. The pre-gate phase has its own guard and records nothing.
|
|
53
|
+
async function declined(
|
|
54
|
+
request: { issue: string; phase: string },
|
|
55
|
+
state: CutState,
|
|
56
|
+
code: number,
|
|
57
|
+
): Promise<number> {
|
|
58
|
+
if (request.phase !== run_cut.IMPLEMENTATION_PHASE) return code
|
|
59
|
+
|
|
60
|
+
const directory = await run_cut.worktree_directory()
|
|
61
|
+
|
|
62
|
+
if (directory !== undefined) {
|
|
63
|
+
record(directory, { issue: request.issue, hold_taken_at: state.hold_taken_at })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return code
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const run_cut_decline = { decline_path, declined, is_declined_sync, read, record }
|
|
70
|
+
|
|
71
|
+
export type { CutDecline }
|
|
72
|
+
export { run_cut_decline }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { UNTRACKED_FILES_FLAG } from '#scripts/git/constants'
|
|
2
|
+
import { git_command } from '#scripts/git/git-command'
|
|
3
|
+
import { git_diff_reads } from '#scripts/git/git-diff-reads'
|
|
4
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
5
|
+
import type { CutState } from './run-cut'
|
|
6
|
+
|
|
7
|
+
// Whether a cut can be taken on this tree — the one predicate `run:cut` refuses `unready` on and the
|
|
8
|
+
// implementation-cut guard asks before it refuses an edit (joshuafolkken/kit#3881).
|
|
9
|
+
//
|
|
10
|
+
// **The guard and the command must not disagree.** The guard once fired on every over-threshold edit
|
|
11
|
+
// of a run held on the default branch, where `run:cut --impl` can only ever answer `unready`; each
|
|
12
|
+
// refusal cost a round trip that cut nothing. Reading the same predicate keeps the guard silent
|
|
13
|
+
// exactly where the command would refuse.
|
|
14
|
+
|
|
15
|
+
interface CutTree {
|
|
16
|
+
branch: string
|
|
17
|
+
default_branch: string
|
|
18
|
+
is_dirty: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A cut needs an uncommitted implementation on a branch other than the default one: the resume
|
|
22
|
+
// verifies both, so a tree missing either has nothing a fresh process could resume.
|
|
23
|
+
function is_ready(tree: CutTree): boolean {
|
|
24
|
+
return tree.branch !== tree.default_branch && tree.is_dirty
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The same predicate over the state `run:cut` reads, against the default branch asked asynchronously.
|
|
28
|
+
async function is_lane_branch(state: CutState): Promise<boolean> {
|
|
29
|
+
const default_branch = await git_command.get_default_branch()
|
|
30
|
+
|
|
31
|
+
return is_ready({ branch: state.branch, default_branch, is_dirty: state.is_dirty })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// `--branch` puts the branch on a `# branch.head` header line, so one call answers both halves; every
|
|
35
|
+
// other line is a change.
|
|
36
|
+
const STATUS_ARGUMENTS = ['status', '--porcelain=v2', '--branch', UNTRACKED_FILES_FLAG] as const
|
|
37
|
+
const HEADER_PREFIX = '#'
|
|
38
|
+
const BRANCH_HEADER = '# branch.head '
|
|
39
|
+
|
|
40
|
+
function tree_of(status: string, default_branch: string): CutTree | undefined {
|
|
41
|
+
const lines = status.split('\n').filter((line) => line !== '')
|
|
42
|
+
const header = lines.find((line) => line.startsWith(BRANCH_HEADER))
|
|
43
|
+
|
|
44
|
+
if (header === undefined) return undefined
|
|
45
|
+
|
|
46
|
+
const branch = header.slice(BRANCH_HEADER.length)
|
|
47
|
+
const is_dirty = lines.some((line) => !line.startsWith(HEADER_PREFIX))
|
|
48
|
+
|
|
49
|
+
return { branch, default_branch, is_dirty }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The tree read synchronously, for the `PreToolUse` guard — `undefined` where git cannot answer.
|
|
53
|
+
function read_tree_sync(): CutTree | undefined {
|
|
54
|
+
const status = git_spawn_sync.read(STATUS_ARGUMENTS)
|
|
55
|
+
|
|
56
|
+
if (status === undefined) return undefined
|
|
57
|
+
|
|
58
|
+
const reference = git_spawn_sync.read(git_diff_reads.DEFAULT_BRANCH_ARGUMENTS)
|
|
59
|
+
|
|
60
|
+
return tree_of(status, git_diff_reads.default_branch_of(reference))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// **A tree git cannot describe reads as ready**, so the guard keeps speaking rather than going quiet
|
|
64
|
+
// on a read fault — the same direction `run_cut.carried_cut_sync` takes for an unreadable record.
|
|
65
|
+
function is_ready_sync(read: () => CutTree | undefined = read_tree_sync): boolean {
|
|
66
|
+
const tree = read()
|
|
67
|
+
|
|
68
|
+
return tree === undefined || is_ready(tree)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const run_cut_ready = { is_lane_branch, is_ready, is_ready_sync, tree_of }
|
|
72
|
+
|
|
73
|
+
export type { CutTree }
|
|
74
|
+
export { run_cut_ready }
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
1
3
|
import { run_cut, type CutState, type RunCut } from './run-cut'
|
|
2
4
|
import { run_cut_handoff } from './run-cut-handoff'
|
|
3
5
|
|
|
@@ -136,6 +138,16 @@ function report_reviewer(): number {
|
|
|
136
138
|
return report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE)
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
// The cut outside a lane is taken, and nothing is relaunched: the session that took it is the one a
|
|
142
|
+
// person is watching, so the run is handed on the way `fullrun`'s entry `over` stop hands it on.
|
|
143
|
+
function report_held_cut(issue: string): number {
|
|
144
|
+
console.error(
|
|
145
|
+
`The cut is recorded for ${session_cite.issue(issue)} outside a lane, so nothing was relaunched. Keep the hold, send a \`confirmation\` Telegram whose body names the resume command \`fullrun ${issue_cite.plain(issue)}\`, and end the turn; the fresh session's \`pnpm josh run:cut --resume ${issue}\` answers \`resume-impl\` with the handoff.`,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
// **An implementation cut is never resumed by a session over the threshold**.
|
|
140
152
|
// The cut outside a lane leaves the session that took it open, so a person retyping `fullrun #N` there
|
|
141
153
|
// would be answered `resume-impl`, keep implementing over the threshold, and be cut again on the next
|
|
@@ -176,6 +188,7 @@ const run_cut_report = {
|
|
|
176
188
|
report_busy,
|
|
177
189
|
report_fault,
|
|
178
190
|
report_handed_off,
|
|
191
|
+
report_held_cut,
|
|
179
192
|
report_incomplete,
|
|
180
193
|
report_over,
|
|
181
194
|
report_reviewer,
|
|
@@ -106,6 +106,8 @@ interface CutState {
|
|
|
106
106
|
is_held: boolean
|
|
107
107
|
// The issue a `fullrun` hold names, absent when the tree is not held by one.
|
|
108
108
|
held_issue?: string | undefined
|
|
109
|
+
// When the hold standing over the tree was taken — what keys a declined cut to the run it answered.
|
|
110
|
+
hold_taken_at?: string | undefined
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
// The fields a cut is written from, bundled so `begin_cut` stays within the parameter limit. `phase`
|
|
@@ -402,7 +404,13 @@ async function current_state(): Promise<CutState> {
|
|
|
402
404
|
const is_dirty = await run_hold.is_tree_dirty()
|
|
403
405
|
const hold = hold_of(await worktree_directory())
|
|
404
406
|
|
|
405
|
-
return {
|
|
407
|
+
return {
|
|
408
|
+
branch,
|
|
409
|
+
is_dirty,
|
|
410
|
+
is_held: hold !== undefined,
|
|
411
|
+
held_issue: fullrun_issue(hold),
|
|
412
|
+
hold_taken_at: hold?.taken_at,
|
|
413
|
+
}
|
|
406
414
|
}
|
|
407
415
|
|
|
408
416
|
function describe_cut(cut: RunCut): string {
|
|
@@ -466,6 +474,7 @@ const run_cut = {
|
|
|
466
474
|
handed_off_message,
|
|
467
475
|
has_required_handoff,
|
|
468
476
|
held_issue_sync,
|
|
477
|
+
hold_of,
|
|
469
478
|
incomplete_message,
|
|
470
479
|
invocation_for,
|
|
471
480
|
is_expired,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
2
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
3
|
-
import { telegram_notify } from '#scripts/notify/telegram-notify'
|
|
3
|
+
import { telegram_notify, type ConfirmInput } from '#scripts/notify/telegram-notify'
|
|
4
4
|
import { lane_park } from '#scripts/rules/lane-park'
|
|
5
5
|
|
|
6
6
|
// What `run:entry` does on the way out of a stop it decided itself.
|
|
@@ -26,14 +26,15 @@ interface StopNotice {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
interface StopPorts {
|
|
29
|
-
notify: (input:
|
|
29
|
+
notify: (input: ConfirmInput) => Promise<boolean>
|
|
30
30
|
release: (issue_number: string) => Promise<boolean>
|
|
31
31
|
is_parking_child: () => boolean
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const RECOVERY = 'If this did not arrive, the run:entry output in the session names the stop.'
|
|
35
|
+
const LATER_ALTERNATIVE = 'Leave the Issue for a later session — nothing was changed'
|
|
35
36
|
const NOTIFY_FAILED_NOTE =
|
|
36
|
-
'(the confirmation Telegram could not be sent — send it with `pnpm josh notify --task-type confirmation` before stopping)'
|
|
37
|
+
'(the confirmation Telegram could not be sent — send it with `pnpm josh notify --task-type confirmation --recommended "<choice>"` before stopping)'
|
|
37
38
|
const RELEASED_NOTE = '(hold released by run:entry — the tree is free for the resume)'
|
|
38
39
|
const RELEASE_FAILED_NOTE =
|
|
39
40
|
'(the hold could not be released — run `pnpm josh run:release` before stopping)'
|
|
@@ -78,6 +79,8 @@ async function notify_stop(notice: StopNotice, ports: StopPorts): Promise<void>
|
|
|
78
79
|
issue_title: `${notice.command} ${issue_cite.plain(notice.issue_number)} stopped`,
|
|
79
80
|
body: body_of(notice),
|
|
80
81
|
recovery: RECOVERY,
|
|
82
|
+
recommended: `Run \`${notice.command} ${issue_cite.plain(notice.issue_number)}\` again in a fresh session`,
|
|
83
|
+
alternatives: [LATER_ALTERNATIVE],
|
|
81
84
|
})
|
|
82
85
|
|
|
83
86
|
console.info(is_sent ? lane_park.COMMAND_NOTIFY_MARKER : NOTIFY_FAILED_NOTE)
|
|
@@ -176,14 +176,25 @@ function held_block(hold: RunHold, issue: string): string | undefined {
|
|
|
176
176
|
return run_hold.is_reentry(hold, issue) ? undefined : run_hold.held_message(hold)
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// An expired record of the claimant's own issue that no stop marked is the earlier run of the same work —
|
|
180
|
+
// a child killed and re-dispatched into its lane — so the claim adopts it with the work it left. A
|
|
181
|
+
// stopped `halfrun` or `prrun` keeps its own adopt path.
|
|
182
|
+
function is_own_unstopped(hold: RunHold, issue: string): boolean {
|
|
183
|
+
return run_hold.is_reentry(hold, issue) && !run_hold.is_stop_marked(hold)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function stale_block(hold: RunHold, issue: string): Promise<string | undefined> {
|
|
187
|
+
if (is_own_unstopped(hold, issue)) return undefined
|
|
188
|
+
|
|
189
|
+
return (await run_hold.is_tree_dirty()) ? run_hold.uncommitted_message(hold) : undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
179
192
|
async function blocking_message(read: HoldRead, issue: string): Promise<string | undefined> {
|
|
180
193
|
if (read.kind === 'held') return held_block(read.hold, issue)
|
|
181
194
|
|
|
182
195
|
if (read.kind === 'unreadable') return run_hold.unreadable_message()
|
|
183
196
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return is_expired_over_work ? run_hold.uncommitted_message(read.hold) : undefined
|
|
197
|
+
return read.kind === 'stale' ? await stale_block(read.hold, issue) : undefined
|
|
187
198
|
}
|
|
188
199
|
|
|
189
200
|
// A record already here is never overwritten by a claim: overwriting is what the guard exists to
|
|
@@ -129,10 +129,12 @@ function is_expired(hold: RunHold, now: Date): boolean {
|
|
|
129
129
|
// nothing. Only a recorded session that is provably gone counts — no owner, or a live pid whose start
|
|
130
130
|
// cannot be read, leaves the age as the whole test. **A stop-marked record is exempt**: a `halfrun` or
|
|
131
131
|
// `prrun` stop is held across a person's latency on purpose, long after its session ends.
|
|
132
|
-
function
|
|
133
|
-
|
|
132
|
+
function is_stop_marked(hold: RunHold): boolean {
|
|
133
|
+
return hold.is_halfrun_stop === true || hold.prrun_stop_head !== undefined
|
|
134
|
+
}
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
function is_owner_gone(hold: RunHold): boolean {
|
|
137
|
+
if (is_stop_marked(hold) || hold.owner_pid === undefined) return false
|
|
136
138
|
|
|
137
139
|
return process_identity.is_same_process(hold.owner_pid, hold.owner_start) === false
|
|
138
140
|
}
|
|
@@ -317,6 +319,7 @@ function unknown_message(): string {
|
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
const run_hold = {
|
|
322
|
+
is_stop_marked,
|
|
320
323
|
FORCE_RELEASE_COMMAND,
|
|
321
324
|
HOLD_MAX_AGE_HOURS,
|
|
322
325
|
HOLD_MAX_AGE_MS,
|