@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,4 +1,5 @@
|
|
|
1
1
|
import type { NamedPlan } from '#scripts/backlog/backlog-plan'
|
|
2
|
+
import type { BudgetWait } from '#scripts/gate/core-wait'
|
|
2
3
|
import type { MachineSample } from '#scripts/gate/machine-capacity'
|
|
3
4
|
import type { ClosedAnswer, ClosedIssue } from './run-board-closed'
|
|
4
5
|
import type { Fetch } from './run-board-fetch'
|
|
@@ -21,6 +22,8 @@ interface BoardPorts {
|
|
|
21
22
|
// `undefined` when no run has started here.
|
|
22
23
|
read_local: () => Promise<LocalRead | undefined>
|
|
23
24
|
read_machine: () => Promise<MachineSample>
|
|
25
|
+
// The claims waiting in the core budget, read on the machine's second; none where it is not given.
|
|
26
|
+
read_waits?: (() => Array<BudgetWait>) | undefined
|
|
24
27
|
// Every process and its lane, the last reading's lanes reused (`run-board-usage-read.ts`).
|
|
25
28
|
read_usage: (before: UsageMark | undefined) => Promise<UsageMark | undefined>
|
|
26
29
|
// The issues that answered as closed, and whether every issue answered (`run-board-closed.ts`).
|
|
@@ -85,6 +88,7 @@ interface BoardState {
|
|
|
85
88
|
// them.
|
|
86
89
|
usage: UsageMark | undefined
|
|
87
90
|
usages: LaneUsages | undefined
|
|
91
|
+
budget_waits: ReadonlyArray<BudgetWait>
|
|
88
92
|
// The reviewers drawn above the legend, the read of them in flight, and when it was last launched.
|
|
89
93
|
thanks: Thanks
|
|
90
94
|
thanks_fetch: Fetch<Thanks> | undefined
|
|
@@ -112,6 +116,7 @@ const FRESH_STATE: BoardState = {
|
|
|
112
116
|
spots: [],
|
|
113
117
|
usage: undefined,
|
|
114
118
|
usages: undefined,
|
|
119
|
+
budget_waits: [],
|
|
115
120
|
thanks: [],
|
|
116
121
|
thanks_fetch: undefined,
|
|
117
122
|
thanks_attempted_ms: undefined,
|
|
@@ -17,11 +17,17 @@ import { run_board_setup, type SetupRead } from './run-board-setup'
|
|
|
17
17
|
// statuses are laid over is the slow GitHub half, and so is what a child that left the open listing
|
|
18
18
|
// answers when read closed.
|
|
19
19
|
|
|
20
|
-
type ItemState =
|
|
20
|
+
type ItemState =
|
|
21
|
+
'running' | 'merged' | 'parked' | 'done' | 'waiting' | 'human' | 'stopped' | 'withdrawn'
|
|
21
22
|
|
|
22
23
|
interface ItemStatus {
|
|
23
24
|
state: ItemState
|
|
25
|
+
// When the child first launched in this run — a re-dispatch or a restart does not move it, so the
|
|
26
|
+
// row's time is the lane's whole time.
|
|
24
27
|
started_ms?: number | undefined
|
|
28
|
+
// The child's newest launch, present only where it launched again after `started_ms` — what the
|
|
29
|
+
// GitHub read is weighed against.
|
|
30
|
+
launched_ms?: number | undefined
|
|
25
31
|
ended_ms?: number | undefined
|
|
26
32
|
// The seat-free lane label (`lane <N>`) of a running child, when it holds one — the running test reads
|
|
27
33
|
// it; the board does not draw it.
|
|
@@ -61,25 +67,40 @@ const ENDING_STATES: Readonly<Partial<Record<string, ItemState>>> = {
|
|
|
61
67
|
[KIND.PARK]: 'parked',
|
|
62
68
|
[KIND.SPLIT]: 'done',
|
|
63
69
|
[KIND.STOP]: 'done',
|
|
70
|
+
[KIND.WITHDRAW]: 'withdrawn',
|
|
64
71
|
}
|
|
65
72
|
// A cut or a resume says the child is still running, on whatever phase it had reached.
|
|
66
73
|
const CONTINUING_KINDS: ReadonlySet<string> = new Set([KIND.CUT, KIND.RESUME])
|
|
67
|
-
// A person's restart
|
|
74
|
+
// A person's restart, and a re-dispatch after an outage, start the child over in a fresh session, so
|
|
75
|
+
// its track starts over too; its time does not (`launched`).
|
|
68
76
|
const LAUNCH_KINDS: ReadonlySet<string> = new Set([KIND.CHILD_LAUNCH, KIND.RESTART])
|
|
69
77
|
// What the GitHub read may still settle: a running child, and one whose lane has no child left in it —
|
|
70
78
|
// a child that merged and then died is drawn merged, not stopped.
|
|
71
79
|
const UNSETTLED_STATES: ReadonlySet<ItemState> = new Set(['running', 'stopped'])
|
|
80
|
+
// A parked child waits on a person, who may close its issue after the park; only a closed read settles
|
|
81
|
+
// it again, so one still open stays parked whatever its labels say (joshuafolkken/kit#3958).
|
|
82
|
+
const PARKED_STATE: ItemState = 'parked'
|
|
72
83
|
|
|
73
84
|
function moment(event: RunEvent): number {
|
|
74
85
|
return Date.parse(event.at)
|
|
75
86
|
}
|
|
76
87
|
|
|
77
|
-
|
|
88
|
+
// The newest launch carried on, where the child launched more than once.
|
|
89
|
+
function relaunch_of(status: ItemStatus | undefined): Pick<ItemStatus, 'launched_ms'> {
|
|
90
|
+
const launched_ms = status?.launched_ms
|
|
91
|
+
|
|
92
|
+
return launched_ms === undefined ? {} : { launched_ms }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// joshuafolkken/kit#3971: a launch keeps the start of the child's first one in this run.
|
|
96
|
+
function launched(previous: ItemStatus | undefined, event: RunEvent): ItemStatus {
|
|
78
97
|
const at_ms = moment(event)
|
|
98
|
+
const started_ms = previous?.started_ms ?? at_ms
|
|
79
99
|
|
|
80
100
|
return {
|
|
81
101
|
state: 'running',
|
|
82
|
-
started_ms
|
|
102
|
+
started_ms,
|
|
103
|
+
...(started_ms !== at_ms && { launched_ms: at_ms }),
|
|
83
104
|
track: [run_board_phase.LAUNCHED_PHASE],
|
|
84
105
|
phase_ms: at_ms,
|
|
85
106
|
}
|
|
@@ -98,7 +119,12 @@ function moved(previous: ItemStatus, track: ReadonlyArray<Phase>, event: RunEven
|
|
|
98
119
|
function settled_as(previous: ItemStatus | undefined, state: ItemState): ItemStatus {
|
|
99
120
|
const track = previous?.track
|
|
100
121
|
|
|
101
|
-
return {
|
|
122
|
+
return {
|
|
123
|
+
state,
|
|
124
|
+
started_ms: previous?.started_ms,
|
|
125
|
+
...relaunch_of(previous),
|
|
126
|
+
...(track !== undefined && { track }),
|
|
127
|
+
}
|
|
102
128
|
}
|
|
103
129
|
|
|
104
130
|
function ended(previous: ItemStatus | undefined, state: ItemState, event: RunEvent): ItemStatus {
|
|
@@ -108,7 +134,7 @@ function ended(previous: ItemStatus | undefined, state: ItemState, event: RunEve
|
|
|
108
134
|
function continued(previous: ItemStatus | undefined): ItemStatus {
|
|
109
135
|
const { started_ms, track, phase_ms } = previous ?? {}
|
|
110
136
|
|
|
111
|
-
return { state: 'running', started_ms, track, phase_ms }
|
|
137
|
+
return { state: 'running', started_ms, ...relaunch_of(previous), track, phase_ms }
|
|
112
138
|
}
|
|
113
139
|
|
|
114
140
|
// A stage line carrying its attempt redraws the track of any child launched on this stream — a settled
|
|
@@ -143,7 +169,7 @@ function advanced(previous: ItemStatus | undefined, event: RunEvent): ItemStatus
|
|
|
143
169
|
|
|
144
170
|
// One event folded into the status of the child it names; an event the board does not track keeps it.
|
|
145
171
|
function next_status(previous: ItemStatus | undefined, event: RunEvent): ItemStatus | undefined {
|
|
146
|
-
if (LAUNCH_KINDS.has(event.kind)) return launched(event)
|
|
172
|
+
if (LAUNCH_KINDS.has(event.kind)) return launched(previous, event)
|
|
147
173
|
|
|
148
174
|
const ending = ENDING_STATES[event.kind]
|
|
149
175
|
|
|
@@ -165,8 +191,9 @@ function fold_events(events: ReadonlyArray<RunEvent>): Map<number, ItemStatus> {
|
|
|
165
191
|
return statuses
|
|
166
192
|
}
|
|
167
193
|
|
|
168
|
-
// A lane is this run's only when its issue is on the run's stream or in
|
|
169
|
-
//
|
|
194
|
+
// A lane is this run's only when its issue is on the run's stream or in `in_run`, so a lane left over
|
|
195
|
+
// from an earlier day is not drawn as this run's child. `in_run` leaves out a planned issue whose lane
|
|
196
|
+
// an earlier run dispatched (`run-board-github.ts`).
|
|
170
197
|
function is_running_lane(
|
|
171
198
|
issue: number,
|
|
172
199
|
status: ItemStatus | undefined,
|
|
@@ -227,9 +254,14 @@ function is_listed_closed(issue: number, read: OpenRead): boolean {
|
|
|
227
254
|
return read.open_numbers !== undefined && !read.open_numbers.has(issue)
|
|
228
255
|
}
|
|
229
256
|
|
|
230
|
-
//
|
|
257
|
+
// When the child last launched — what a GitHub read is weighed against.
|
|
258
|
+
function launch_ms(status: ItemStatus): number | undefined {
|
|
259
|
+
return status.launched_ms ?? status.started_ms
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The listing was read after the child last launched, so what it says about the child is current.
|
|
231
263
|
function is_read_after_launch(status: ItemStatus, read: OpenRead): boolean {
|
|
232
|
-
return read.read_ms !== undefined && (status
|
|
264
|
+
return read.read_ms !== undefined && (launch_ms(status) ?? 0) <= read.read_ms
|
|
233
265
|
}
|
|
234
266
|
|
|
235
267
|
// A child launched before the listing was read and missing from it has closed since.
|
|
@@ -257,26 +289,30 @@ function labelled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus
|
|
|
257
289
|
|
|
258
290
|
// A close dated before the launch is an earlier life of the issue, not how this run's child ended.
|
|
259
291
|
function is_closed_before_launch(status: ItemStatus, closed: ClosedIssue): boolean {
|
|
260
|
-
|
|
292
|
+
const launch = launch_ms(status)
|
|
293
|
+
|
|
294
|
+
if (launch === undefined || closed.closed_ms === undefined) return false
|
|
295
|
+
|
|
296
|
+
return closed.closed_ms < launch
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function closed_state(closed: ClosedIssue): ItemState {
|
|
300
|
+
if (closed.is_merged) return 'merged'
|
|
261
301
|
|
|
262
|
-
return closed.
|
|
302
|
+
return closed.is_not_planned ? 'withdrawn' : 'done'
|
|
263
303
|
}
|
|
264
304
|
|
|
265
|
-
// A
|
|
305
|
+
// A child read closed ends when GitHub says it closed — merged, withdrawn as not planned, or done.
|
|
266
306
|
function closed_status(
|
|
267
307
|
status: ItemStatus,
|
|
268
308
|
closed: ClosedIssue | undefined,
|
|
269
309
|
): ItemStatus | undefined {
|
|
270
310
|
if (closed === undefined || is_closed_before_launch(status, closed)) return undefined
|
|
271
311
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return { ...settled_as(status, state), ended_ms: closed.closed_ms }
|
|
312
|
+
return { ...settled_as(status, closed_state(closed)), ended_ms: closed.closed_ms }
|
|
275
313
|
}
|
|
276
314
|
|
|
277
|
-
function
|
|
278
|
-
if (!UNSETTLED_STATES.has(status.state)) return status
|
|
279
|
-
|
|
315
|
+
function unsettled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus {
|
|
280
316
|
const from_closed = closed_status(status, read.closed.get(issue))
|
|
281
317
|
|
|
282
318
|
if (from_closed !== undefined) return from_closed
|
|
@@ -286,11 +322,26 @@ function settled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus
|
|
|
286
322
|
return labelled(issue, status, read)
|
|
287
323
|
}
|
|
288
324
|
|
|
325
|
+
// The closed read is kept for the run, so a parked child a person reopened since is told apart by the
|
|
326
|
+
// listing holding it again.
|
|
327
|
+
function parked_settled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus {
|
|
328
|
+
if (read.open_numbers?.has(issue) === true) return status
|
|
329
|
+
|
|
330
|
+
return closed_status(status, read.closed.get(issue)) ?? status
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function settled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus {
|
|
334
|
+
if (status.state === PARKED_STATE) return parked_settled(issue, status, read)
|
|
335
|
+
|
|
336
|
+
return UNSETTLED_STATES.has(status.state) ? unsettled(issue, status, read) : status
|
|
337
|
+
}
|
|
338
|
+
|
|
289
339
|
// **The stream stays the source of truth; the listing only unsticks it.** A child whose merge never
|
|
290
340
|
// reached the stream would read as running forever, so a running child the plan's open listing no
|
|
291
341
|
// longer holds is drawn finished instead — merged and timed once GitHub was read for it, finished
|
|
292
342
|
// without a time while that read has not answered. One the listing still holds is drawn as its labels
|
|
293
|
-
// say, so a child that stopped with no parent to record it does not read as running.
|
|
343
|
+
// say, so a child that stopped with no parent to record it does not read as running. A parked child is
|
|
344
|
+
// settled again only by a closed read.
|
|
294
345
|
function settle_closed(
|
|
295
346
|
statuses: ReadonlyMap<number, ItemStatus>,
|
|
296
347
|
read: OpenRead,
|
|
@@ -82,8 +82,10 @@ async function resample(state: BoardState, ports: BoardPorts, now_ms: number): P
|
|
|
82
82
|
const at_ms = ports.now()
|
|
83
83
|
const machine = { sample: await ports.read_machine(), at_ms }
|
|
84
84
|
const gauges = run_board_machine.gauges_of(state.machine, machine)
|
|
85
|
+
const budget_waits = ports.read_waits?.() ?? []
|
|
86
|
+
const usages = await read_usages(state, ports)
|
|
85
87
|
|
|
86
|
-
return { ...state, ...
|
|
88
|
+
return { ...state, ...usages, machine, gauges, budget_waits, sampled_ms: now_ms }
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
// The state of the run on screen; a different run starts its plan state from nothing, so no row, count
|
|
@@ -136,6 +138,7 @@ function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): B
|
|
|
136
138
|
statuses,
|
|
137
139
|
events: local.events,
|
|
138
140
|
free_lanes: local.free_lanes,
|
|
141
|
+
occupied_lanes: local.occupied_lanes,
|
|
139
142
|
is_stopped: activity_here(local).is_stopped,
|
|
140
143
|
})
|
|
141
144
|
|
|
@@ -192,6 +195,7 @@ function header_of(
|
|
|
192
195
|
is_plan_loading: state.plan_fetch !== undefined,
|
|
193
196
|
machine: state.gauges,
|
|
194
197
|
usages: state.usages,
|
|
198
|
+
budget_waits: state.budget_waits,
|
|
195
199
|
form: redraw.ports.form,
|
|
196
200
|
repo: state.plan?.context.repo,
|
|
197
201
|
spinner: redraw.ports.is_tty ? SPOT : undefined,
|
|
@@ -61,6 +61,7 @@ const {
|
|
|
61
61
|
report_held_cut,
|
|
62
62
|
report_incomplete,
|
|
63
63
|
report_over,
|
|
64
|
+
report_relaunched_cut,
|
|
64
65
|
report_reviewer,
|
|
65
66
|
report_stale,
|
|
66
67
|
report_under_threshold,
|
|
@@ -124,7 +125,7 @@ function relaunch(target: string, lane: LaneInfo, phase: string): number {
|
|
|
124
125
|
|
|
125
126
|
if (notes.length > 0) console.error(notes.join('\n'))
|
|
126
127
|
|
|
127
|
-
return
|
|
128
|
+
return report_relaunched_cut()
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
const { is_lane_branch } = run_cut_ready
|
|
@@ -199,7 +200,7 @@ async function finish_cut(target: string, lane: LaneInfo, request: CutRequest):
|
|
|
199
200
|
if (refused !== undefined) return refused
|
|
200
201
|
|
|
201
202
|
return lane_relaunch.is_openai_lane(lane)
|
|
202
|
-
?
|
|
203
|
+
? report_relaunched_cut()
|
|
203
204
|
: relaunch(target, lane, request.phase)
|
|
204
205
|
}
|
|
205
206
|
|
|
@@ -148,6 +148,20 @@ function report_held_cut(issue: string): number {
|
|
|
148
148
|
return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// **A cut that relaunched its successor says so on this session's own transcript** (joshuafolkken/kit#3936).
|
|
152
|
+
// The successor's `run:cut --resume` can clear the record before this session's turn ends, so the
|
|
153
|
+
// `Stop` hook cannot tell a cut stop by the record alone; it reads this line off the turn instead, a
|
|
154
|
+
// fact no other process can remove. The held cut never prints it — a person resumes that one. **A bare
|
|
155
|
+
// token line**, so it is read off the `run:cut` call's own result (`cost-run-cut.ts`), and a turn that
|
|
156
|
+
// merely quotes it is not read as a relaunch.
|
|
157
|
+
const RELAUNCHED_CUT_MARKER = 'cut-relaunched'
|
|
158
|
+
|
|
159
|
+
function report_relaunched_cut(): number {
|
|
160
|
+
console.error(RELAUNCHED_CUT_MARKER)
|
|
161
|
+
|
|
162
|
+
return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
|
|
163
|
+
}
|
|
164
|
+
|
|
151
165
|
// **An implementation cut is never resumed by a session over the threshold**.
|
|
152
166
|
// The cut outside a lane leaves the session that took it open, so a person retyping `fullrun #N` there
|
|
153
167
|
// would be answered `resume-impl`, keep implementing over the threshold, and be cut again on the next
|
|
@@ -174,6 +188,7 @@ const run_cut_report = {
|
|
|
174
188
|
INCOMPLETE_VERDICT,
|
|
175
189
|
NOT_A_LANE_VERDICT,
|
|
176
190
|
OVER_VERDICT,
|
|
191
|
+
RELAUNCHED_CUT_MARKER,
|
|
177
192
|
RESUME_IMPL_VERDICT,
|
|
178
193
|
RESUME_VERDICT,
|
|
179
194
|
STALE_VERDICT,
|
|
@@ -191,6 +206,7 @@ const run_cut_report = {
|
|
|
191
206
|
report_held_cut,
|
|
192
207
|
report_incomplete,
|
|
193
208
|
report_over,
|
|
209
|
+
report_relaunched_cut,
|
|
194
210
|
report_reviewer,
|
|
195
211
|
report_stale,
|
|
196
212
|
report_under_threshold,
|
|
@@ -4,6 +4,7 @@ import { agent_launch_environment } from '#scripts/agent/agent-launch-environmen
|
|
|
4
4
|
import { agent_role_profile, type AgentProfile } from '#scripts/agent/agent-role-profile'
|
|
5
5
|
import { agent_session_environment } from '#scripts/josh/agent-session-environment'
|
|
6
6
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
7
|
+
import { launch_priority } from '#scripts/run/launch-priority'
|
|
7
8
|
|
|
8
9
|
// Starting a process that outlives this one, and keeping what it writes.
|
|
9
10
|
//
|
|
@@ -34,13 +35,18 @@ const UNSAFE_NOTE =
|
|
|
34
35
|
//
|
|
35
36
|
// A NUL byte is what `execve` treats as the end of a string, so a value carrying one executes as a
|
|
36
37
|
// prefix of itself; the other control characters have no business in a command line and their presence
|
|
37
|
-
// means the value did not come from where it was supposed to.
|
|
38
|
+
// means the value did not come from where it was supposed to. **An agent's prompt is the one element a
|
|
39
|
+
// line break belongs in** — a ship-stop prompt carries the failed output's lines — so a vector marked
|
|
40
|
+
// `ends_with_prompt` lets it into its last element alone (joshuafolkken/kit#3964).
|
|
38
41
|
//
|
|
39
42
|
// The shared value validation lives with the role policy so environment overrides and argv elements
|
|
40
43
|
// cannot disagree about which control characters or lengths are safe.
|
|
41
44
|
interface LaunchArgv {
|
|
42
45
|
command: string
|
|
43
46
|
args: ReadonlyArray<string>
|
|
47
|
+
// Set by an agent argv builder, whose prompt is the last element: that one element may carry line
|
|
48
|
+
// breaks (`is_safe_prompt`), and every other element is checked as before.
|
|
49
|
+
ends_with_prompt?: boolean
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
type LaunchResult = { kind: 'launched'; pid: number } | { kind: 'failed'; note: string }
|
|
@@ -65,12 +71,23 @@ interface LaunchRequest {
|
|
|
65
71
|
profile?: AgentProfile | undefined
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
const { is_safe_value } = agent_role_profile
|
|
74
|
+
const { is_safe_prompt, is_safe_value } = agent_role_profile
|
|
75
|
+
const LAST = -1
|
|
76
|
+
|
|
77
|
+
// An agent vector's prompt, the last element, may break lines; every other element is a value.
|
|
78
|
+
function is_safe_element(argv: LaunchArgv, value: string, index: number): boolean {
|
|
79
|
+
const is_prompt = argv.ends_with_prompt === true && index === argv.args.length + LAST
|
|
80
|
+
|
|
81
|
+
return is_prompt ? is_safe_prompt(value) : is_safe_value(value)
|
|
82
|
+
}
|
|
69
83
|
|
|
70
84
|
// The single gate every argument vector passes before it reaches the operating system, whoever
|
|
71
85
|
// composed it.
|
|
72
86
|
function is_safe_argv(argv: LaunchArgv): boolean {
|
|
73
|
-
return
|
|
87
|
+
return (
|
|
88
|
+
is_safe_value(argv.command) &&
|
|
89
|
+
argv.args.every((value, index) => is_safe_element(argv, value, index))
|
|
90
|
+
)
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
function note_of(error: unknown): string {
|
|
@@ -251,6 +268,7 @@ async function attached_spawn(
|
|
|
251
268
|
child.once('exit', (exit_code) => {
|
|
252
269
|
resolve({ kind: 'completed', pid: child.pid ?? 0, exit_code })
|
|
253
270
|
})
|
|
271
|
+
launch_priority.lower(child.pid)
|
|
254
272
|
notify_spawn(child.pid, on_spawn, on_error)
|
|
255
273
|
})
|
|
256
274
|
}
|
|
@@ -287,6 +305,7 @@ function spawned(
|
|
|
287
305
|
on_error(note_of(error))
|
|
288
306
|
})
|
|
289
307
|
child.unref()
|
|
308
|
+
launch_priority.lower(child.pid)
|
|
290
309
|
|
|
291
310
|
return child.pid === undefined ? { kind: 'failed', note: NO_PID_NOTE } : launched(child.pid)
|
|
292
311
|
}
|
|
@@ -341,5 +360,5 @@ const detached_launch = {
|
|
|
341
360
|
note_of,
|
|
342
361
|
}
|
|
343
362
|
|
|
344
|
-
export type { LaunchArgv, LaunchRequest, LaunchResult }
|
|
363
|
+
export type { AttachedLaunchResult, LaunchArgv, LaunchRequest, LaunchResult }
|
|
345
364
|
export { detached_launch }
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '#scripts/run/run-stage'
|
|
23
23
|
import { run_stage_read, type StageRead } from '#scripts/run/run-stage-read'
|
|
24
24
|
import { run_step } from '#scripts/run/run-step'
|
|
25
|
+
import { run_step_section } from '#scripts/run/run-step-section'
|
|
25
26
|
import { run_entry, type EntryParts } from './run-entry'
|
|
26
27
|
import { run_entry_stop } from './run-entry-stop'
|
|
27
28
|
|
|
@@ -259,7 +260,13 @@ async function proceed(issue_number: string, hold: string, cost: string): Promis
|
|
|
259
260
|
// update is measured against (`issue-watch.ts`).
|
|
260
261
|
await issue_watch.record_baseline_here(issue_number)
|
|
261
262
|
|
|
262
|
-
|
|
263
|
+
const code = emit({ issue_number, hold, cost, verdict: reads.verdict, report: reads.report })
|
|
264
|
+
|
|
265
|
+
// The entry's own read — the split assessment every run answers before its work — printed here so
|
|
266
|
+
// the reader does not spend a turn fetching it.
|
|
267
|
+
if (code !== SUCCESS_EXIT_CODE) return code
|
|
268
|
+
|
|
269
|
+
return run_step_section.print_section(run_step_section.ENTRY)
|
|
263
270
|
}
|
|
264
271
|
|
|
265
272
|
async function claim_run(
|
|
@@ -29,6 +29,7 @@ const KIND_LABELS: Readonly<Record<EventKind, KindLabel>> = {
|
|
|
29
29
|
cut: { ja: 'セッション交代', en: 'session cut' },
|
|
30
30
|
resume: { ja: '実装を再開', en: 'implementation resumed' },
|
|
31
31
|
restart: { ja: '止めて出し直し', en: 'restarted' },
|
|
32
|
+
withdraw: { ja: '取り下げ', en: 'withdrawn' },
|
|
32
33
|
drain: { ja: 'バックログが空', en: 'backlog drained' },
|
|
33
34
|
stop: { ja: '停止', en: 'stopped' },
|
|
34
35
|
'pr-opened': { ja: 'PR 作成', en: 'PR opened' },
|
|
@@ -94,6 +94,7 @@ const KIND = run_event_stream.EVENT_KIND
|
|
|
94
94
|
const BATCH_RECORD_KINDS: ReadonlySet<string> = new Set([
|
|
95
95
|
KIND.CHILD_LAUNCH,
|
|
96
96
|
KIND.RESTART,
|
|
97
|
+
KIND.WITHDRAW,
|
|
97
98
|
KIND.PARK,
|
|
98
99
|
KIND.SPLIT,
|
|
99
100
|
KIND.RETROSPECTIVE,
|
|
@@ -167,12 +168,14 @@ function last_issue_event(
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
// A child's launch and the endings `run:merge` settles it with — the ones `run:board` stops drawing it
|
|
170
|
-
// running on. A park or a split already ran `run:merge`, so a child closed after one owes nothing
|
|
171
|
+
// running on. A park or a split already ran `run:merge`, so a child closed after one owes nothing; a
|
|
172
|
+
// withdrawal settles the child itself, its issue closed by the person who withdrew it.
|
|
171
173
|
const LAUNCH_OR_SETTLE_KINDS: ReadonlySet<string> = new Set([
|
|
172
174
|
KIND.CHILD_LAUNCH,
|
|
173
175
|
KIND.MERGE,
|
|
174
176
|
KIND.PARK,
|
|
175
177
|
KIND.SPLIT,
|
|
178
|
+
KIND.WITHDRAW,
|
|
176
179
|
])
|
|
177
180
|
|
|
178
181
|
// Whether this invocation's newest launch of the issue has not yet been settled.
|
|
@@ -186,9 +189,24 @@ function is_merge_owed(events: ReadonlyArray<RunEvent>, scope: EventScope, issue
|
|
|
186
189
|
return newest?.kind === KIND.CHILD_LAUNCH
|
|
187
190
|
}
|
|
188
191
|
|
|
192
|
+
function is_launch_or_withdrawal(event: RunEvent, issue: string): boolean {
|
|
193
|
+
const is_kind = event.kind === KIND.CHILD_LAUNCH || event.kind === KIND.WITHDRAW
|
|
194
|
+
|
|
195
|
+
return is_kind && names_issue(event, issue)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Whether a person withdrew the issue's newest launch (`lane:withdraw`): its newest launch or withdrawal
|
|
199
|
+
// is a withdrawal. A launch after it is a fresh attempt, which the old withdrawal does not settle.
|
|
200
|
+
function is_withdrawn(events: ReadonlyArray<RunEvent>, issue: string): boolean {
|
|
201
|
+
const newest = events.findLast((event) => is_launch_or_withdrawal(event, issue))
|
|
202
|
+
|
|
203
|
+
return newest?.kind === KIND.WITHDRAW
|
|
204
|
+
}
|
|
205
|
+
|
|
189
206
|
const run_event_scope = {
|
|
190
207
|
UNKNOWN_EVENT_SCOPE,
|
|
191
208
|
is_merge_owed,
|
|
209
|
+
is_withdrawn,
|
|
192
210
|
issue_named,
|
|
193
211
|
last_issue_event,
|
|
194
212
|
last_scoped_event,
|
|
@@ -113,10 +113,9 @@ async function emit_heartbeat(line: string): Promise<void> {
|
|
|
113
113
|
await emit(run_event_stream.EVENT_KIND.HEARTBEAT, line)
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
// The events of the invocation now running,
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
async function current_events(): Promise<ReadonlyArray<RunEvent>> {
|
|
116
|
+
// The events of the invocation now running, `undefined` where the invocation's scope could not be told
|
|
117
|
+
// — for a reader that must not take "no events" for "nothing launched" (`backlog-occupied.ts`).
|
|
118
|
+
async function scoped_current_events(): Promise<ReadonlyArray<RunEvent> | undefined> {
|
|
120
119
|
try {
|
|
121
120
|
const repository = await run_carry.repository_directory()
|
|
122
121
|
|
|
@@ -126,12 +125,19 @@ async function current_events(): Promise<ReadonlyArray<RunEvent>> {
|
|
|
126
125
|
const scope = run_event_scope.scope_of(read)
|
|
127
126
|
const events = run_event_stream.read_events(run_event_stream.target_of(repository))
|
|
128
127
|
|
|
129
|
-
return run_event_scope.scoped_events(events, scope)
|
|
128
|
+
return run_event_scope.scoped_events(events, scope)
|
|
130
129
|
} catch {
|
|
131
|
-
return
|
|
130
|
+
return undefined
|
|
132
131
|
}
|
|
133
132
|
}
|
|
134
133
|
|
|
134
|
+
// The events of the invocation now running, for a reader restoring the run from its stream
|
|
135
|
+
// (`backlog:drive`). Fail-quiet like the writers: no repository, an unreadable stream or an
|
|
136
|
+
// undetermined scope is an empty invocation.
|
|
137
|
+
async function current_events(): Promise<ReadonlyArray<RunEvent>> {
|
|
138
|
+
return (await scoped_current_events()) ?? []
|
|
139
|
+
}
|
|
140
|
+
|
|
135
141
|
// The invocation's events for a writer deciding whether a marker is already there: unlike
|
|
136
142
|
// `current_events`, an undetermined scope keeps the whole stream, so a run with no carry record skips a
|
|
137
143
|
// marker it already wrote rather than writing it twice.
|
|
@@ -167,6 +173,7 @@ const run_event_stream_emit = {
|
|
|
167
173
|
emit_once,
|
|
168
174
|
emit_once_since,
|
|
169
175
|
invocation_or_all_events,
|
|
176
|
+
scoped_current_events,
|
|
170
177
|
stream_target,
|
|
171
178
|
}
|
|
172
179
|
|
|
@@ -64,6 +64,10 @@ const EVENT_KIND = {
|
|
|
64
64
|
// (`lane:restart`). It follows the fresh dispatch's `CHILD_LAUNCH`, so it is a launch to the parent and
|
|
65
65
|
// the board, and — like that launch — a batch record a lane child never reads as its own position.
|
|
66
66
|
RESTART: 'restart',
|
|
67
|
+
// A person withdrew a lane child — stopped it and closed its issue as not planned (`lane:withdraw`).
|
|
68
|
+
// It is written before the stop, so `run:merge` settles the child as withdrawn whatever GitHub says
|
|
69
|
+
// by the time the parent collects it, and counts it as neither a merge, a failure nor an outage.
|
|
70
|
+
WITHDRAW: 'withdraw',
|
|
67
71
|
// The backlog emptied while nothing of the run's own was in flight — the drain. It marks the position
|
|
68
72
|
// at which the end-of-run retrospective is owed, *before* the idle watch opens, so `run:step` fires
|
|
69
73
|
// the retrospective at the drain rather than after the watch runs its course. It is not itself a
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
2
|
+
import { run_halfrun_resume } from '#scripts/run/run-halfrun-resume'
|
|
3
|
+
import { run_hold, type RunHold, type SessionOwner } from './run-hold'
|
|
4
|
+
|
|
5
|
+
// What the `Stop` hook asks of a hold beyond "is one in place" (joshuafolkken/kit#3961): whether it is
|
|
6
|
+
// this session's own run's, and whether its pause was already announced.
|
|
7
|
+
//
|
|
8
|
+
// **Another run's hold is not this session's pause.** A `kickoff`, or a question answered while a
|
|
9
|
+
// `backlogrun` parent holds the same tree, stops in a session that holds nothing — announcing that stop
|
|
10
|
+
// as the held run's "confirmation required" reports a decision nobody is waiting on.
|
|
11
|
+
//
|
|
12
|
+
// **Unknown is this session's own.** A record written outside an agent session, or a hook that cannot
|
|
13
|
+
// read its own session, keeps the behavior the hold had before ownership was recorded: a missed notify
|
|
14
|
+
// costs a person a silent pause, a wrong one only a needless Telegram.
|
|
15
|
+
|
|
16
|
+
function is_same_start(recorded: string | undefined, own: string | undefined): boolean {
|
|
17
|
+
return recorded === undefined || own === undefined || recorded === own
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function is_session_hold(hold: RunHold, owner: SessionOwner = run_hold.session_owner()): boolean {
|
|
21
|
+
if (hold.owner_pid === undefined || owner.owner_pid === undefined) return true
|
|
22
|
+
|
|
23
|
+
return hold.owner_pid === owner.owner_pid && is_same_start(hold.owner_start, owner.owner_start)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// **A pause is announced once, and a person's prompt ends it.** The hook's own send is recorded on the
|
|
27
|
+
// hold, so a later stop of the same pause — a background notice that woke the session — finds it; a send before `pause_start` belongs to an earlier pause. An unknown start
|
|
28
|
+
// announces again: a needless Telegram costs less than a silent wait.
|
|
29
|
+
function is_notified_since(hold: RunHold, pause_start: string | undefined): boolean {
|
|
30
|
+
if (pause_start === undefined || hold.notified_at === undefined) return false
|
|
31
|
+
|
|
32
|
+
return Date.parse(hold.notified_at) >= Date.parse(pause_start)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Stamps the send on the record in place; a tree no longer held has nothing to stamp. A stale record is
|
|
36
|
+
// stamped too: the hook reads it as the hold (`hold_of`), so a run past the expiry age still finds its send.
|
|
37
|
+
function mark_notified(target: string, now: Date = new Date()): void {
|
|
38
|
+
const hold = run_halfrun_resume.hold_of(run_hold.read_hold(target, now))
|
|
39
|
+
if (hold === undefined) return
|
|
40
|
+
|
|
41
|
+
stamp_file.write_stamp(target, { ...hold, notified_at: now.toISOString() })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const run_hold_session = { is_notified_since, is_session_hold, mark_notified }
|
|
45
|
+
|
|
46
|
+
export { run_hold_session }
|
|
@@ -65,6 +65,10 @@ interface RunHold {
|
|
|
65
65
|
// fields, or by a claim made outside an agent session.
|
|
66
66
|
owner_pid?: number | undefined
|
|
67
67
|
owner_start?: string | undefined
|
|
68
|
+
// When the `Stop` hook last sent this run's `confirmation` Telegram — its own send leaves no command
|
|
69
|
+
// on the transcript, so the record is what keeps one pause from being announced twice
|
|
70
|
+
// (`run-hold-session.ts`).
|
|
71
|
+
notified_at?: string | undefined
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
type HoldRead =
|
|
@@ -107,6 +111,7 @@ const run_hold_schema = z.object({
|
|
|
107
111
|
prrun_stop_head: z.string().optional(),
|
|
108
112
|
owner_pid: z.number().optional(),
|
|
109
113
|
owner_start: z.string().optional(),
|
|
114
|
+
notified_at: z.string().optional(),
|
|
110
115
|
})
|
|
111
116
|
|
|
112
117
|
// `undefined` for anything that is not a well-formed record, so the caller decides what a broken one
|
|
@@ -342,6 +347,7 @@ const run_hold = {
|
|
|
342
347
|
race_message,
|
|
343
348
|
read_hold,
|
|
344
349
|
release_hold,
|
|
350
|
+
session_owner,
|
|
345
351
|
stale_message,
|
|
346
352
|
uncommitted_message,
|
|
347
353
|
unknown_message,
|
|
@@ -350,5 +356,5 @@ const run_hold = {
|
|
|
350
356
|
write_hold,
|
|
351
357
|
}
|
|
352
358
|
|
|
353
|
-
export type { HoldRead, RunHold, StopMarkFields }
|
|
359
|
+
export type { HoldRead, RunHold, SessionOwner, StopMarkFields }
|
|
354
360
|
export { run_hold }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { setPriority } from 'node:os'
|
|
2
|
+
|
|
3
|
+
// **A launched session runs below the interactive one, so a saturated machine still answers the
|
|
4
|
+
// user.** Every headless session and everything it starts — gates, tests, builds — used to share the
|
|
5
|
+
// interactive session's priority, and at a load of ~30 the lanes took the terminal's responsiveness
|
|
6
|
+
// with them (#3947). The niceness is set on the child right after `spawn` returns: a process inherits
|
|
7
|
+
// its parent's niceness at fork, and an agent CLI boots for far longer than this takes before it forks
|
|
8
|
+
// anything, so every descendant inherits it. Efficiency-core confinement (`taskpolicy -b`) is not
|
|
9
|
+
// used — it throttles a lane far beyond yielding to the user.
|
|
10
|
+
const LANE_NICENESS = 10
|
|
11
|
+
|
|
12
|
+
type SetPriority = (pid: number, priority: number) => void
|
|
13
|
+
|
|
14
|
+
// **A priority that cannot be lowered starts the session as before.** A platform without the call, or
|
|
15
|
+
// a child that already runs at a lower priority than this, is not a reason to fail the launch.
|
|
16
|
+
function lower(pid: number | undefined, set_priority: SetPriority = setPriority): void {
|
|
17
|
+
if (pid === undefined) return
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
set_priority(pid, LANE_NICENESS)
|
|
21
|
+
} catch {
|
|
22
|
+
// the session keeps the priority it was started with
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const launch_priority = {
|
|
27
|
+
LANE_NICENESS,
|
|
28
|
+
lower,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type { SetPriority }
|
|
32
|
+
export { launch_priority }
|