@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
|
@@ -4,6 +4,7 @@ import { filing_offer } from './filing-offer'
|
|
|
4
4
|
import { issue_citation } from './issue-citation'
|
|
5
5
|
import { lane_background } from './lane-background'
|
|
6
6
|
import { reply_language } from './reply-language'
|
|
7
|
+
import { stop_decision, type StopDecision } from './stop-decision'
|
|
7
8
|
|
|
8
9
|
// The stop-time rules, delivered on the `Stop` hook.
|
|
9
10
|
//
|
|
@@ -19,11 +20,14 @@ import { reply_language } from './reply-language'
|
|
|
19
20
|
// permission envelope. The stop guard is a second *entry* on the one foundation, exactly as
|
|
20
21
|
// `pretool-guard` is one — not a second copy of the plumbing.
|
|
21
22
|
//
|
|
22
|
-
// **
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
23
|
+
// **A rule whose answer is fixed acts; only a rule that needs judgement refuses** (joshuafolkken/kit#3919).
|
|
24
|
+
// The stop notification, the hold release and the citation lines are each one mechanical operation the
|
|
25
|
+
// hook can run itself, so they come back as `actions` and the stop goes through: a refusal is a whole
|
|
26
|
+
// model turn spent re-reading the context to run a command the hook already knew. `stop-actions.ts`
|
|
27
|
+
// runs them, and only an action that fails falls back to its refusal. The rest — the reply's language,
|
|
28
|
+
// an unattended filing offer, a lane's index request or running task, a headless parent's wait — need
|
|
29
|
+
// the model, so they still block with `{"decision":"block"}`, the one channel a `Stop` hook has to it;
|
|
30
|
+
// `stop_hook_active` caps a false positive at a single wasted turn.
|
|
27
31
|
|
|
28
32
|
const SWITCH_ENV_KEY = 'JOSH_STOP_GUARD'
|
|
29
33
|
const BLOCK_DECISION = 'block'
|
|
@@ -40,14 +44,22 @@ type StopPayload = z.infer<typeof stop_payload_schema>
|
|
|
40
44
|
|
|
41
45
|
// What one stop looks like to the rules, computed once by the CLI from the payload and the world.
|
|
42
46
|
interface StopContext {
|
|
43
|
-
// The `run:hold` record for this working tree
|
|
44
|
-
|
|
47
|
+
// The Issue the `run:hold` record for this working tree names (`held` or `stale`) — `new` for an
|
|
48
|
+
// unnumbered entry — or `undefined` when no record is in place.
|
|
49
|
+
hold_issue: string | undefined
|
|
50
|
+
// That record is this session's own run's, or its owner cannot be told (`run-hold-session.ts`). A
|
|
51
|
+
// hold another run took is that run's pause to announce and release, never this session's.
|
|
52
|
+
hold_owned: boolean
|
|
53
|
+
// The person opened this turn with a question: the reply answers someone at the session, not a
|
|
54
|
+
// pause waiting on them (`stop-decision.ts` → `is_question`).
|
|
55
|
+
answers_question: boolean
|
|
45
56
|
// `git status --porcelain` came back empty.
|
|
46
57
|
tree_clean: boolean
|
|
47
58
|
// That record carries a `prrun` stop mark: the run stopped at a green pull
|
|
48
59
|
// request and keeps the hold over a clean tree on purpose, so a person can look before it merges.
|
|
49
60
|
prrun_stopped: boolean
|
|
50
|
-
// A `confirmation` notify is on this run's transcript tail
|
|
61
|
+
// A `confirmation` notify is on this run's transcript tail, or the hook's own send is recorded on the
|
|
62
|
+
// hold since the pause began (`run-hold-session.ts` → `is_notified_since`).
|
|
51
63
|
notified: boolean
|
|
52
64
|
// The turn's session-facing reply text.
|
|
53
65
|
message: string
|
|
@@ -56,7 +68,8 @@ interface StopContext {
|
|
|
56
68
|
// This stop already forced a continuation earlier in the prompt — the loop-breaker Claude Code
|
|
57
69
|
// documents, so a run that will not comply is not blocked forever.
|
|
58
70
|
stop_hook_active: boolean
|
|
59
|
-
//
|
|
71
|
+
// This turn ended at a cut — its record is in place, or this turn relaunched the successor that may
|
|
72
|
+
// already have cleared it (`stop-guard.ts` → `is_cut_stop`). A dispatched lane child ends its turn at
|
|
60
73
|
// the cut holding the tree by design and hands off to a fresh process (`pre-gate-cut.md`), so that
|
|
61
74
|
// turn-end is an automatic continuation, not the person-waiting pause the block rules are for.
|
|
62
75
|
cut_pending: boolean
|
|
@@ -90,15 +103,26 @@ interface StopContext {
|
|
|
90
103
|
backlog_parent: boolean
|
|
91
104
|
}
|
|
92
105
|
|
|
106
|
+
// The operations the hook runs itself instead of refusing — each carries what it acts on.
|
|
107
|
+
type StopAction =
|
|
108
|
+
| { kind: 'notify'; issue: string; message: string; decision: StopDecision }
|
|
109
|
+
| { kind: 'release'; issue: string }
|
|
110
|
+
| { kind: 'cite'; references: ReadonlyArray<string> }
|
|
111
|
+
|
|
112
|
+
// `reason` refuses the stop; otherwise `actions` run, and `blocks_on_failure` says whether a failed one
|
|
113
|
+
// falls back to its refusal — never past the loop-breaker.
|
|
93
114
|
interface StopOutcome {
|
|
94
115
|
reason: string | undefined
|
|
116
|
+
actions: ReadonlyArray<StopAction>
|
|
117
|
+
blocks_on_failure: boolean
|
|
95
118
|
}
|
|
96
119
|
|
|
97
|
-
const NO_OUTCOME: StopOutcome = { reason: undefined }
|
|
120
|
+
const NO_OUTCOME: StopOutcome = { reason: undefined, actions: [], blocks_on_failure: false }
|
|
98
121
|
|
|
99
122
|
// **A mid-workflow stop is announced off-screen before it happens.** `CLAUDE.md` → "Mid-workflow stop
|
|
100
123
|
// notification" requires the `confirmation` Telegram before any pause; the hold is what says a run is
|
|
101
|
-
// mid-workflow, and the transcript is what says the notify was sent.
|
|
124
|
+
// mid-workflow, and the transcript is what says the notify was sent. The hook sends it; this reason is
|
|
125
|
+
// only the fallback for a send that failed.
|
|
102
126
|
const STOP_NOTIFY_REASON =
|
|
103
127
|
'⛔ mid-workflow stop notification: this working tree is still held by a run and no `confirmation` ' +
|
|
104
128
|
'Telegram was sent this turn. Send ' +
|
|
@@ -109,13 +133,14 @@ const STOP_NOTIFY_REASON =
|
|
|
109
133
|
// **A clean tree that still holds is a tree the next run will trample.** `working-tree-hold.md`: a stop that
|
|
110
134
|
// leaves the tree clean releases the hold; a `halfrun` pre-commit stop and a `needs-human-review` stop
|
|
111
135
|
// keep it because their tree is dirty, which is why this row is silent whenever the tree is not clean.
|
|
136
|
+
// The hook releases it; this reason is only the fallback for a release that failed.
|
|
112
137
|
const HOLD_RELEASE_REASON =
|
|
113
138
|
'⛔ working-tree hold not released: this working tree is clean but its `run:hold` record is still in ' +
|
|
114
139
|
'place. Release it with `pnpm josh run:release <N>` (bare for a `new` entry; ' +
|
|
115
140
|
'`working-tree-hold.md`), then confirm in one line that it was released — do not repeat your ' +
|
|
116
141
|
'previous reply.'
|
|
117
142
|
|
|
118
|
-
// **
|
|
143
|
+
// **The fallback when the hook could not print the citation lines itself**. The bare `#N` is already
|
|
119
144
|
// on screen and the hook cannot unsay it, so the reason does the one thing that helps the *next* reply:
|
|
120
145
|
// it names the numbers it detected and hands over the exact `issue:cite` call that prints the
|
|
121
146
|
// paste-ready lines, then asks for the corrected citation lines alone — not the whole reply reissued.
|
|
@@ -246,42 +271,52 @@ function needs_filing(context: StopContext): boolean {
|
|
|
246
271
|
//
|
|
247
272
|
// **A lane handed to the detached ship stands them down too**: the hold now
|
|
248
273
|
// belongs to the supervisor driving the merge, and the person hears from that supervisor's own notify.
|
|
274
|
+
//
|
|
275
|
+
// **Another run's hold stands them down as well**: the pause and the claim are that run's.
|
|
249
276
|
function is_held_idle(context: StopContext): boolean {
|
|
250
|
-
|
|
277
|
+
if (context.hold_issue === undefined || !context.hold_owned) return false
|
|
278
|
+
|
|
279
|
+
return !context.agent_pending && !context.handed_off
|
|
251
280
|
}
|
|
252
281
|
|
|
282
|
+
// **A stop that answers the person's question is no pause** (joshuafolkken/kit#3961): they are reading
|
|
283
|
+
// the reply where it was written, whatever the tree's hold says.
|
|
253
284
|
function needs_notify(context: StopContext): boolean {
|
|
254
|
-
return is_held_idle(context) && !context.notified
|
|
285
|
+
return is_held_idle(context) && !context.notified && !context.answers_question
|
|
255
286
|
}
|
|
256
287
|
|
|
257
|
-
|
|
258
|
-
|
|
288
|
+
// The notify a stop owes but cannot send: its reply names no decision to announce.
|
|
289
|
+
function needs_decision(context: StopContext): boolean {
|
|
290
|
+
return needs_notify(context) && stop_decision.decision_of(context.message) === undefined
|
|
259
291
|
}
|
|
260
292
|
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
if (references.length === 0) return undefined
|
|
264
|
-
|
|
265
|
-
return build_citation_reason(references)
|
|
293
|
+
function needs_release(context: StopContext): boolean {
|
|
294
|
+
return is_held_idle(context) && context.tree_clean && !context.prrun_stopped
|
|
266
295
|
}
|
|
267
296
|
|
|
268
|
-
// The
|
|
269
|
-
// its text lands in a stream-json log, so correcting its citations or its
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
function
|
|
273
|
-
if (context.headless_agent) return
|
|
297
|
+
// The bare references the citation lines are printed for. **A headless agent's reply has no reader**:
|
|
298
|
+
// its text lands in a stream-json log, so correcting its citations or its language costs a turn and
|
|
299
|
+
// reaches nobody. The artifacts a person does read — a Telegram, an Issue, a pull request — are held
|
|
300
|
+
// to the same rules by their own guards, not here.
|
|
301
|
+
function bare_references(context: StopContext): ReadonlyArray<string> {
|
|
302
|
+
if (context.headless_agent) return []
|
|
274
303
|
|
|
275
|
-
return
|
|
304
|
+
return issue_citation.unquoted_references(context.message, context.prompt)
|
|
276
305
|
}
|
|
277
306
|
|
|
278
|
-
// The
|
|
307
|
+
// The two rules that read the reply and need the model, the filing offer first. A drifted reply
|
|
279
308
|
// refused for another reason is not rewritten, but the drift still ends: `block_envelope` appends the
|
|
280
309
|
// session-language note to every refusal, so the turn it continues is written in the session language.
|
|
281
310
|
function reply_reason(context: StopContext): string | undefined {
|
|
282
311
|
if (needs_filing(context)) return FILING_OFFER_REASON
|
|
312
|
+
if (context.headless_agent) return undefined
|
|
283
313
|
|
|
284
|
-
return
|
|
314
|
+
return language_reason(context)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Sent back once; past the loop-breaker the run has said nothing is awaited, and nothing is sent.
|
|
318
|
+
function decision_reason(context: StopContext): string | undefined {
|
|
319
|
+
return needs_decision(context) ? stop_decision.DECISION_REASON : undefined
|
|
285
320
|
}
|
|
286
321
|
|
|
287
322
|
// The headless wait holds past the loop-breaker until the spin bound is reached.
|
|
@@ -306,24 +341,56 @@ function needs_background_wait(context: StopContext): boolean {
|
|
|
306
341
|
return context.lane_child && context.background_pending
|
|
307
342
|
}
|
|
308
343
|
|
|
309
|
-
// The two
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
function hold_reason(context: StopContext): string | undefined {
|
|
344
|
+
// The two lane rules that mean the run is not pausing at all — ahead of every action, since the notify
|
|
345
|
+
// would announce a pause the run has no reason to take.
|
|
346
|
+
function lane_reason(context: StopContext): string | undefined {
|
|
313
347
|
if (needs_index_route(context)) return LANE_INDEX_REASON
|
|
314
348
|
if (needs_background_wait(context)) return lane_background.LANE_BACKGROUND_STOP_REASON
|
|
315
|
-
if (needs_notify(context)) return STOP_NOTIFY_REASON
|
|
316
|
-
if (needs_release(context)) return HOLD_RELEASE_REASON
|
|
317
349
|
|
|
318
350
|
return undefined
|
|
319
351
|
}
|
|
320
352
|
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
353
|
+
// The refusals that need the model: the lane rules alone, else the reply's own and the decision
|
|
354
|
+
// together. **Joined, since the loop-breaker grants one continuation**: a decision refusal queued behind
|
|
355
|
+
// a language or filing refusal would never be sent, and its stop would pass with no notify.
|
|
356
|
+
function model_reason(context: StopContext): string | undefined {
|
|
357
|
+
const lane = lane_reason(context)
|
|
358
|
+
if (lane !== undefined) return lane
|
|
359
|
+
|
|
360
|
+
const reasons = [reply_reason(context), decision_reason(context)].filter(
|
|
361
|
+
(reason) => reason !== undefined,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
return reasons.length === 0 ? undefined : reasons.join('\n\n')
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// The notify carries the reply's own decision; a reply with none sends nothing (`needs_decision`).
|
|
368
|
+
function notify_actions(context: StopContext, issue: string): Array<StopAction> {
|
|
369
|
+
const decision = stop_decision.decision_of(context.message)
|
|
370
|
+
if (decision === undefined || !needs_notify(context)) return []
|
|
371
|
+
|
|
372
|
+
return [{ kind: 'notify', issue, message: context.message, decision }]
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function hold_actions(context: StopContext): Array<StopAction> {
|
|
376
|
+
const issue = context.hold_issue
|
|
377
|
+
if (issue === undefined) return []
|
|
378
|
+
|
|
379
|
+
const notify = notify_actions(context, issue)
|
|
380
|
+
|
|
381
|
+
return needs_release(context) ? [...notify, { kind: 'release', issue }] : notify
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// The actions in the order they run: the notify ahead of the release that ends the hold it reports.
|
|
385
|
+
function stop_actions(context: StopContext): ReadonlyArray<StopAction> {
|
|
386
|
+
const references = bare_references(context)
|
|
387
|
+
const cite: Array<StopAction> = references.length > 0 ? [{ kind: 'cite', references }] : []
|
|
388
|
+
|
|
389
|
+
return [...hold_actions(context), ...cite]
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// First-wins across the refusals. `stop_hook_active` is the loop-breaker, so a run that already got one
|
|
393
|
+
// continuation this prompt may stop; the lane rules go first, then the filing offer, then the language.
|
|
327
394
|
//
|
|
328
395
|
// **The headless wait comes first and outlasts `stop_hook_active` up to its spin bound**.
|
|
329
396
|
// The loop-breaker exists so a person is not wedged behind a rule the run will not satisfy; a headless
|
|
@@ -334,18 +401,49 @@ function block_reason(context: StopContext): string | undefined {
|
|
|
334
401
|
if (is_headless_held(context)) return HEADLESS_WAIT_REASON
|
|
335
402
|
if (context.stop_hook_active || context.cut_pending) return undefined
|
|
336
403
|
|
|
337
|
-
return
|
|
404
|
+
return model_reason(context)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// **The actions run only on the stop that goes through**, so a refused stop never notifies a pause its
|
|
408
|
+
// continuation then cancels, while the continuation's own stop, past the loop-breaker, still runs them
|
|
409
|
+
// and a failure there no longer refuses. A lane child still waiting on its own work runs none, and a
|
|
410
|
+
// pre-gate cut in flight stands everything but the headless wait down: it is a lane child's automatic
|
|
411
|
+
// turn-end, not a person-waiting pause.
|
|
412
|
+
function runs_actions(context: StopContext, reason: string | undefined): boolean {
|
|
413
|
+
return reason === undefined && !context.cut_pending && lane_reason(context) === undefined
|
|
338
414
|
}
|
|
339
415
|
|
|
340
416
|
function stop_outcome(context: StopContext): StopOutcome {
|
|
341
|
-
|
|
417
|
+
const reason = block_reason(context)
|
|
418
|
+
if (!runs_actions(context, reason)) return { ...NO_OUTCOME, reason }
|
|
419
|
+
|
|
420
|
+
const actions = stop_actions(context)
|
|
421
|
+
if (actions.length === 0) return NO_OUTCOME
|
|
422
|
+
|
|
423
|
+
return { reason, actions, blocks_on_failure: !context.stop_hook_active }
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// The refusal an action falls back to when the hook could not run it.
|
|
427
|
+
function fallback_reason(action: StopAction): string {
|
|
428
|
+
if (action.kind === 'notify') return STOP_NOTIFY_REASON
|
|
429
|
+
if (action.kind === 'release') return HOLD_RELEASE_REASON
|
|
430
|
+
|
|
431
|
+
return build_citation_reason(action.references)
|
|
342
432
|
}
|
|
343
433
|
|
|
344
434
|
// The documented shape a `Stop` hook blocks with: `reason` is fed back to Claude, which then continues
|
|
345
435
|
// instead of stopping. Plain stdout is not it — only this envelope holds the stop. Every reason leaves
|
|
346
436
|
// with the session-language note, so no rule can hand the model an English-only turn.
|
|
347
|
-
|
|
348
|
-
|
|
437
|
+
// `notice` is what the hook already did on this stop, shown to the person alongside.
|
|
438
|
+
function block_envelope(reason: string, lang: string, notice?: string): string {
|
|
439
|
+
const refusal = { decision: BLOCK_DECISION, reason: `${reason} ${language_note(lang)}` }
|
|
440
|
+
|
|
441
|
+
return JSON.stringify(notice === undefined ? refusal : { ...refusal, systemMessage: notice })
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// The shape a `Stop` hook shows the person a line in without holding the stop: `systemMessage` alone.
|
|
445
|
+
function notice_envelope(notice: string): string {
|
|
446
|
+
return JSON.stringify({ systemMessage: notice })
|
|
349
447
|
}
|
|
350
448
|
|
|
351
449
|
function parse_stop_payload(raw_payload: string): StopPayload | undefined {
|
|
@@ -371,10 +469,12 @@ const stop_rules = {
|
|
|
371
469
|
block_envelope,
|
|
372
470
|
build_language_reason,
|
|
373
471
|
count_headless_refusals,
|
|
472
|
+
fallback_reason,
|
|
374
473
|
is_enabled,
|
|
474
|
+
notice_envelope,
|
|
375
475
|
parse_stop_payload,
|
|
376
476
|
stop_outcome,
|
|
377
477
|
}
|
|
378
478
|
|
|
379
|
-
export type { StopContext, StopOutcome }
|
|
479
|
+
export type { StopAction, StopContext, StopOutcome }
|
|
380
480
|
export { stop_rules }
|
|
@@ -30,6 +30,20 @@ function prior_bash_commands(tail: string): ReadonlyArray<string> {
|
|
|
30
30
|
return tail.split('\n').flatMap((line) => bash_commands_of(line))
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
// **Whether a call `is_call` names answered with the `token` line** — the call and its own result
|
|
34
|
+
// paired off parsed blocks, so a transcript that merely quotes the command or the token does not
|
|
35
|
+
// count. An unreadable text reads as no answer.
|
|
36
|
+
function answered(text: string, is_call: (command: string) => boolean, token: string): boolean {
|
|
37
|
+
const blocks = time_transcript_line.parse_text(text).flatMap((line) => line.blocks)
|
|
38
|
+
const call_ids = new Set(
|
|
39
|
+
blocks
|
|
40
|
+
.filter((block) => is_call(time_shell.bash_command(block.input)))
|
|
41
|
+
.map((block) => block.id),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return blocks.some((block) => call_ids.has(block.result_id) && block.token_lines.includes(token))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tail_commands = { answered, is_bash_use, prior_bash_commands }
|
|
34
48
|
|
|
35
49
|
export { tail_commands }
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { styleText } from 'node:util'
|
|
5
|
+
import { core_budget } from '#scripts/gate/core-budget'
|
|
5
6
|
import { machine_capacity } from '#scripts/gate/machine-capacity'
|
|
6
7
|
import { telegram_notify } from '#scripts/notify/telegram-notify'
|
|
7
8
|
import { run_progress } from '#scripts/run/progress/run-progress'
|
|
@@ -51,6 +52,7 @@ const LIVE_PORTS: BoardPorts = {
|
|
|
51
52
|
read_plan: async (scope) => await run_board_plan.read_plan(scope),
|
|
52
53
|
read_local: run_board_read.read_local,
|
|
53
54
|
read_machine: machine_capacity.read_sample,
|
|
55
|
+
read_waits: () => core_budget.budget_waits(),
|
|
54
56
|
read_usage: run_board_usage_read.usage_reader(),
|
|
55
57
|
read_closed: async (issues) => await run_board_closed.read_all(issues),
|
|
56
58
|
read_thanks: async () => await run_board_thanks_read.read_thanks(),
|
|
@@ -18,10 +18,13 @@ interface ClosedIssue {
|
|
|
18
18
|
closed_ms: number | undefined
|
|
19
19
|
// Closed by a merged pull request, rather than closed by hand.
|
|
20
20
|
is_merged: boolean
|
|
21
|
+
// Closed as not planned — withdrawn rather than finished (joshuafolkken/kit#3958).
|
|
22
|
+
is_not_planned: boolean
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
const CLOSED_FIELDS = 'title,state,closedAt,labels'
|
|
25
|
+
const CLOSED_FIELDS = 'title,state,stateReason,closedAt,labels'
|
|
24
26
|
const CLOSED_STATE = 'CLOSED'
|
|
27
|
+
const NOT_PLANNED_REASON = 'not_planned'
|
|
25
28
|
const STILL_OPEN = 'open' as const
|
|
26
29
|
|
|
27
30
|
// What one issue answered: closed, still open, or `undefined` when it could not be read.
|
|
@@ -41,6 +44,7 @@ const READ_CONCURRENCY = 4
|
|
|
41
44
|
const closed_schema = z.looseObject({
|
|
42
45
|
title: z.string(),
|
|
43
46
|
state: z.string(),
|
|
47
|
+
stateReason: z.string().nullish(),
|
|
44
48
|
closedAt: z.string().nullish(),
|
|
45
49
|
labels: z.array(issue_label_schema).default([]),
|
|
46
50
|
})
|
|
@@ -69,7 +73,9 @@ async function read_closed(issue: number): Promise<ClosedRead> {
|
|
|
69
73
|
|
|
70
74
|
const labels = view.labels.map((label) => label.name)
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
const is_not_planned = view.stateReason === NOT_PLANNED_REASON
|
|
77
|
+
|
|
78
|
+
return { title: view.title, labels, closed_ms: closed_ms_of(view), is_merged, is_not_planned }
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
async function read_all(
|
|
@@ -73,12 +73,20 @@ function fold_plan(state: BoardState): BoardState {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// joshuafolkken/kit#3969: the planned issues whose open lane may be this run's though the stream never
|
|
77
|
+
// saw it launched. An earlier run's lane is not, so its issue waits until this run launches it.
|
|
78
|
+
function lanes_in_run(plan: BoardPlan, local: LocalRead): ReadonlySet<number> {
|
|
79
|
+
const earlier = new Set(local.earlier_lanes?.map(Number))
|
|
80
|
+
|
|
81
|
+
return new Set([...run_board_layout.numbers_of(plan)].filter((issue) => !earlier.has(issue)))
|
|
82
|
+
}
|
|
83
|
+
|
|
76
84
|
function statuses_for(
|
|
77
85
|
state: BoardState,
|
|
78
86
|
plan: BoardPlan,
|
|
79
87
|
local: LocalRead,
|
|
80
88
|
): ReadonlyMap<number, ItemStatus> {
|
|
81
|
-
const in_run =
|
|
89
|
+
const in_run = lanes_in_run(plan, local)
|
|
82
90
|
const stopped = run_board_idle.stopped_of(state.idle_since, state.read_ms)
|
|
83
91
|
|
|
84
92
|
return run_board_status.statuses_of(local.events, local.lanes, in_run, stopped)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { styleText } from 'node:util'
|
|
2
2
|
import { backlog_budget } from '#scripts/backlog/backlog-budget'
|
|
3
|
+
import type { BudgetWait } from '#scripts/gate/core-wait'
|
|
3
4
|
import { run_board_fit } from './run-board-fit'
|
|
4
5
|
import { run_board_fresh } from './run-board-fresh'
|
|
5
6
|
import { run_board_labels } from './run-board-labels'
|
|
@@ -65,6 +66,8 @@ interface BoardHeader {
|
|
|
65
66
|
machine: MachineGauges | undefined
|
|
66
67
|
// Each running lane's CPU and memory, `undefined` where none could be read.
|
|
67
68
|
usages?: LaneUsages | undefined
|
|
69
|
+
// The claims waiting in the machine-wide core budget (`run-board-render-waits.ts`).
|
|
70
|
+
budget_waits?: ReadonlyArray<BudgetWait> | undefined
|
|
68
71
|
// How long each phase usually takes; a running row's phase time is colored against it.
|
|
69
72
|
usual?: UsualTimes | undefined
|
|
70
73
|
// The spinner's frame a running run and a running row turn, `undefined` where the output is not a
|
|
@@ -107,7 +110,7 @@ function count_state(rows: ReadonlyArray<BoardRow>, state: BoardRow['state']): n
|
|
|
107
110
|
function counts_of(layout: BoardLayout): BoardCounts {
|
|
108
111
|
const total = run_board_layout.rows_of(layout).length
|
|
109
112
|
const parked = count_state(layout.active, 'parked')
|
|
110
|
-
const settled =
|
|
113
|
+
const settled = layout.active.filter((row) => run_board_layout.is_settled(row.state)).length
|
|
111
114
|
const running = count_state(layout.active, 'running')
|
|
112
115
|
|
|
113
116
|
return { total, settled, parked, running, remaining: total - settled - running }
|
|
@@ -43,6 +43,7 @@ const WORDS = {
|
|
|
43
43
|
merged: 'merged',
|
|
44
44
|
parked: 'parked',
|
|
45
45
|
done: 'done',
|
|
46
|
+
withdrawn: 'withdrawn',
|
|
46
47
|
in_progress: 'running',
|
|
47
48
|
stopped: 'stopped',
|
|
48
49
|
waiting: 'waiting',
|
|
@@ -104,6 +105,7 @@ const STATE_ICONS: Readonly<Record<ItemState, string>> = {
|
|
|
104
105
|
waiting: '⏳',
|
|
105
106
|
human: '🙋',
|
|
106
107
|
stopped: '🛑',
|
|
108
|
+
withdrawn: '🚫',
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
// The blocking edge, chosen on the same rule: ⛓ is a text symbol.
|
|
@@ -82,7 +82,7 @@ interface Drawn extends Overlay {
|
|
|
82
82
|
reason_of: ReasonOf | undefined
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const SETTLED_STATES: ReadonlySet<ItemState> = new Set(['merged', 'parked', 'done'])
|
|
85
|
+
const SETTLED_STATES: ReadonlySet<ItemState> = new Set(['merged', 'parked', 'done', 'withdrawn'])
|
|
86
86
|
|
|
87
87
|
function is_settled(state: ItemState): boolean {
|
|
88
88
|
return SETTLED_STATES.has(state)
|
|
@@ -37,9 +37,15 @@ interface LocalRead {
|
|
|
37
37
|
// The open lanes no child is working in at this read (`lane-spent.ts`); one is drawn stopped once it
|
|
38
38
|
// has stayed so (`run-board-idle.ts`).
|
|
39
39
|
idle_lanes?: ReadonlyArray<string> | undefined
|
|
40
|
+
// The open lanes that last took a child before the run started — an earlier run's, until this run
|
|
41
|
+
// dispatches into one again.
|
|
42
|
+
earlier_lanes?: ReadonlyArray<string> | undefined
|
|
40
43
|
// How many more lanes the limit allows beside the live ones, as `lane:limit` counts them;
|
|
41
44
|
// `undefined` where the limit could not be read, so nothing is said about it.
|
|
42
45
|
free_lanes?: number | undefined
|
|
46
|
+
// The issues whose lanes hold a place under the limit (`backlog-occupied.ts`), named beside a row
|
|
47
|
+
// that waits for a free lane.
|
|
48
|
+
occupied_lanes?: ReadonlyArray<string> | undefined
|
|
43
49
|
// How long each phase usually takes, from the repository's lane ledger.
|
|
44
50
|
usual?: UsualTimes | undefined
|
|
45
51
|
}
|
|
@@ -81,6 +87,13 @@ function sources_of(
|
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
// joshuafolkken/kit#3969: a lane whose last dispatch is unknown is not called earlier.
|
|
91
|
+
function earlier_lanes(lanes: ReadonlyArray<LaneInfo>, started_ms: number): Array<string> {
|
|
92
|
+
return lanes
|
|
93
|
+
.filter((lane) => lane.dispatched_ms !== undefined && lane.dispatched_ms < started_ms)
|
|
94
|
+
.map((lane) => lane.issue)
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
// `undefined` when no run has started here.
|
|
85
98
|
async function read_local(): Promise<LocalRead | undefined> {
|
|
86
99
|
const repository = await run_carry.repository_directory()
|
|
@@ -90,6 +103,7 @@ async function read_local(): Promise<LocalRead | undefined> {
|
|
|
90
103
|
|
|
91
104
|
const events = run_event_stream.read_events(run_event_stream.target_of(repository))
|
|
92
105
|
const lanes = await lane_registry.list_lanes()
|
|
106
|
+
const occupied = await backlog_ready.occupied_lanes(lanes)
|
|
93
107
|
|
|
94
108
|
return {
|
|
95
109
|
started_ms: span.started_ms,
|
|
@@ -101,7 +115,9 @@ async function read_local(): Promise<LocalRead | undefined> {
|
|
|
101
115
|
is_stranded: run_stranded_detect.is_stranded_at(repository),
|
|
102
116
|
operations: run_board_operation_read.read(sources_of(repository, lanes, transcript)),
|
|
103
117
|
idle_lanes: lane_spent.idle_lanes(lanes),
|
|
118
|
+
earlier_lanes: earlier_lanes(lanes, span.started_ms),
|
|
104
119
|
free_lanes: await backlog_ready.free_lanes_beside(lanes),
|
|
120
|
+
occupied_lanes: occupied.map((lane) => lane.issue),
|
|
105
121
|
usual: run_board_usual.read(repository),
|
|
106
122
|
}
|
|
107
123
|
}
|
|
@@ -36,6 +36,9 @@ interface ReasonRead extends Overlay {
|
|
|
36
36
|
events: ReadonlyArray<RunEvent>
|
|
37
37
|
// How many more lanes the limit allows; `undefined` where it could not be read.
|
|
38
38
|
free_lanes: number | undefined
|
|
39
|
+
// The issues whose lanes fill the limit, named after `no free lane` so a lane an earlier run left
|
|
40
|
+
// behind shows on the row it holds back.
|
|
41
|
+
occupied_lanes?: ReadonlyArray<string> | undefined
|
|
39
42
|
// A stopped run opens no lane, so a stage its stream ends on is not one any lane is at.
|
|
40
43
|
is_stopped: boolean
|
|
41
44
|
}
|
|
@@ -92,6 +95,12 @@ function overlap_of(child: EpicChild, read: ReasonRead, pool: Pool): string | un
|
|
|
92
95
|
: `${WORDS.overlaps} ${String(clash.holder)} (${clash.path})`
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
function no_lane_of(occupied: ReadonlyArray<string> = []): string {
|
|
99
|
+
if (occupied.length === 0) return WORDS.no_lane
|
|
100
|
+
|
|
101
|
+
return `${WORDS.no_lane} — ${occupied.join(' ')}`
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
// A candidate nothing blocks, in the order `backlog:next` answers: the pool held for a judgment, then
|
|
96
105
|
// a path a running issue declares, then a pool with no lane left.
|
|
97
106
|
function offer_wait(child: EpicChild, read: ReasonRead, pool: Pool): string | undefined {
|
|
@@ -101,7 +110,7 @@ function offer_wait(child: EpicChild, read: ReasonRead, pool: Pool): string | un
|
|
|
101
110
|
|
|
102
111
|
if (overlap !== undefined) return overlap
|
|
103
112
|
|
|
104
|
-
return read.free_lanes === NO_FREE_LANES ?
|
|
113
|
+
return read.free_lanes === NO_FREE_LANES ? no_lane_of(read.occupied_lanes) : undefined
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
function wait_of(child: EpicChild, read: ReasonRead, pool: Pool): RowReason | undefined {
|
|
@@ -9,7 +9,7 @@ import { run_board_usage_text } from './run-board-usage-text'
|
|
|
9
9
|
// blanks on any other row.
|
|
10
10
|
|
|
11
11
|
const { finish_of, text_of } = run_board_usage_text
|
|
12
|
-
const FINISHED: ReadonlySet<ItemState> = new Set(['merged', 'done'])
|
|
12
|
+
const FINISHED: ReadonlySet<ItemState> = new Set(['merged', 'done', 'withdrawn'])
|
|
13
13
|
const BLANK = ' '
|
|
14
14
|
|
|
15
15
|
// What one frame's column is drawn from: each lane's usage, and the running rows the frame draws —
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { core_wait, type BudgetWait } from '#scripts/gate/core-wait'
|
|
2
|
+
|
|
3
|
+
// The claims waiting in the machine-wide core budget, under the header (joshuafolkken/kit#3943): a lane
|
|
4
|
+
// held back by memory pressure otherwise reads the same as one that is stuck. Each line names the
|
|
5
|
+
// command, the lane it runs in — the main checkout where none, and another repository's checkout by
|
|
6
|
+
// name, unlinked — how long it has waited, and why.
|
|
7
|
+
|
|
8
|
+
const MS_PER_SECOND = 1000
|
|
9
|
+
// 🚧 rather than ⏳, which a waiting row already draws, or 🚦, the gate stage's; an emoji a terminal
|
|
10
|
+
// draws two columns wide.
|
|
11
|
+
const BUDGET_ICON = '🚧'
|
|
12
|
+
const GAP = ' '
|
|
13
|
+
const MAIN_CHECKOUT = 'main'
|
|
14
|
+
|
|
15
|
+
function place_text(wait: BudgetWait, link: (reference: string) => string): string {
|
|
16
|
+
if (wait.elsewhere !== undefined) return wait.elsewhere
|
|
17
|
+
|
|
18
|
+
return wait.lane === undefined ? MAIN_CHECKOUT : link(wait.lane)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function wait_text(wait: BudgetWait, link: (reference: string) => string): string {
|
|
22
|
+
const lane = place_text(wait, link)
|
|
23
|
+
const seconds = `${String(Math.round(wait.waited_ms / MS_PER_SECOND))}s`
|
|
24
|
+
const parts = [wait.command ?? 'a step', lane, seconds, core_wait.REASON_TEXT[wait.reason]]
|
|
25
|
+
|
|
26
|
+
return `${BUDGET_ICON} ${parts.join(GAP)}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Nothing is drawn while no claim waits.
|
|
30
|
+
function wait_lines(
|
|
31
|
+
waits: ReadonlyArray<BudgetWait> | undefined,
|
|
32
|
+
link: (reference: string) => string,
|
|
33
|
+
): Array<string> {
|
|
34
|
+
if (waits === undefined || waits.length === 0) return []
|
|
35
|
+
|
|
36
|
+
return ['', ...waits.map((wait) => wait_text(wait, link))]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const run_board_render_waits = { BUDGET_ICON, wait_lines }
|
|
40
|
+
|
|
41
|
+
export { run_board_render_waits }
|
|
@@ -24,6 +24,7 @@ import { run_board_render_slot, type UsageSlot } from './run-board-render-slot'
|
|
|
24
24
|
import { run_board_render_tail } from './run-board-render-tail'
|
|
25
25
|
import { run_board_render_thanks } from './run-board-render-thanks'
|
|
26
26
|
import { run_board_render_title } from './run-board-render-title'
|
|
27
|
+
import { run_board_render_waits } from './run-board-render-waits'
|
|
27
28
|
import { run_board_row_yield } from './run-board-row-yield'
|
|
28
29
|
import type { Thanks } from './run-board-thanks-csv'
|
|
29
30
|
|
|
@@ -388,7 +389,11 @@ function bounded(parts: FrameParts, bounds: FrameBounds): Array<string> {
|
|
|
388
389
|
|
|
389
390
|
function render(view: BoardView): Array<string> {
|
|
390
391
|
const { header, notes } = view
|
|
391
|
-
const above = [
|
|
392
|
+
const above = [
|
|
393
|
+
...run_board_header.header_lines(header),
|
|
394
|
+
...run_board_render_waits.wait_lines(header.budget_waits, header.link),
|
|
395
|
+
...resume_lines(view.resume),
|
|
396
|
+
]
|
|
392
397
|
const parts = {
|
|
393
398
|
above,
|
|
394
399
|
plan: plan_sections(header, view.size),
|