@joshuafolkken/kit 1.1453.0 → 1.1471.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/dependency-update/SKILL.md +26 -55
- package/.claude/skills/epic-commands/SKILL.md +87 -148
- package/.claude/skills/epic-commands/epic-bundle.md +45 -77
- package/.claude/skills/epic-commands/execution-waves.md +44 -81
- package/.claude/skills/verify-ui/SKILL.md +25 -50
- package/.claude/skills/workflow-commands/background-commands.md +36 -47
- package/.claude/skills/workflow-commands/backlogrun-child.md +72 -85
- package/.claude/skills/workflow-commands/backlogrun-handoff-report.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +91 -121
- package/.claude/skills/workflow-commands/backlogrun-park.md +73 -117
- package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -9
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +11 -20
- package/.claude/skills/workflow-commands/backlogrun-steps.md +43 -52
- package/.claude/skills/workflow-commands/chain-rule.md +24 -34
- package/.claude/skills/workflow-commands/followup.md +57 -76
- package/.claude/skills/workflow-commands/halfrun.md +2 -1
- package/.claude/skills/workflow-commands/issue-comments.md +43 -60
- package/.claude/skills/workflow-commands/kickoff.md +65 -74
- package/.claude/skills/workflow-commands/observation-filing.md +62 -107
- package/.claude/skills/workflow-commands/observation-ledger.md +56 -111
- package/.claude/skills/workflow-commands/pre-gate-cut.md +89 -138
- package/.claude/skills/workflow-commands/prerequisite.md +10 -15
- package/.claude/skills/workflow-commands/progress-watcher.md +21 -26
- package/.claude/skills/workflow-commands/prrun.md +2 -1
- package/.claude/skills/workflow-commands/retrospective.md +31 -50
- package/.claude/skills/workflow-commands/split-assessment.md +34 -58
- package/.claude/skills/workflow-commands/working-tree-hold.md +54 -71
- package/CLAUDE.md +7 -6
- package/README.md +2 -9
- package/dist/CLAUDE.md +7 -6
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-Y3GKIKGW.js → chunk-43ZHT22P.js} +31 -15
- package/dist/hooks/{chunk-MBJYQPEK.js → chunk-AS74OU3D.js} +24 -8
- package/dist/hooks/{chunk-3C4XRYG6.js → chunk-E7OIKX5Z.js} +17 -12
- package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-EUMACUK2.js} +18 -12
- package/dist/hooks/{chunk-NO4AALP5.js → chunk-H5HVWYXK.js} +4 -4
- package/dist/hooks/{chunk-5C7L6FRC.js → chunk-IIGFYXKC.js} +346 -241
- package/dist/hooks/{chunk-W5ZKGTSY.js → chunk-J47HU5H5.js} +14 -3
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-K4EOONYM.js} +1 -1
- package/dist/hooks/{chunk-LBFYEOXJ.js → chunk-K4LMBQC2.js} +4 -4
- package/dist/hooks/{chunk-D3NJXN5M.js → chunk-KC5AK2GF.js} +8 -6
- package/dist/hooks/chunk-SHGPANFB.js +56 -0
- package/dist/hooks/{chunk-EF5GQ6WS.js → chunk-YHBZAJSV.js} +3 -3
- package/dist/hooks/{chunk-SHYUQOIS.js → chunk-YYPI7SJZ.js} +5 -5
- package/dist/hooks/codex-hook-adapter.js +13 -13
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +3 -3
- package/dist/hooks/inputs.json +251 -247
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +6 -5
- package/dist/hooks/stop-guard.js +75 -18
- package/dist/josh.js +6 -7
- package/dist/managed-marker/index.js +1 -2
- package/dist/version/index.js +15 -12
- package/eslint/base.js +2 -1
- package/eslint/rules/unicorn.js +16 -0
- package/package.json +3 -3
- package/prompts/coding-standards.md +12 -22
- package/prompts/collaboration-workflow/file-edits.md +26 -44
- package/prompts/collaboration-workflow/gh-rest.md +20 -36
- package/prompts/collaboration-workflow/glossary.md +5 -7
- package/prompts/collaboration-workflow/issue-citation.md +7 -13
- package/prompts/collaboration-workflow/issue-template.md +29 -29
- package/prompts/collaboration-workflow/operating-rules.md +35 -44
- package/prompts/collaboration-workflow/output-bounds.md +15 -15
- package/prompts/collaboration-workflow/overview.md +14 -19
- package/prompts/collaboration-workflow/plan-comment.md +13 -13
- package/prompts/collaboration-workflow/principles.md +50 -53
- package/prompts/collaboration-workflow/proposal-request.md +1 -9
- package/prompts/collaboration-workflow/report-format.md +32 -49
- package/prompts/collaboration-workflow/residency.md +10 -10
- package/prompts/collaboration-workflow/rule-delivery.md +9 -9
- package/prompts/collaboration-workflow/shell-body.md +11 -21
- package/prompts/collaboration-workflow/turn-batching.md +8 -14
- package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
- package/prompts/collaboration-workflow/wip-cap.md +24 -30
- package/prompts/collaboration-workflow.md +24 -25
- package/prompts/refactoring.md +33 -43
- package/prompts/review-rubric.md +84 -129
- package/prompts/review.md +111 -141
- package/prompts/sonar-hotspot-handling.md +22 -50
- package/prompts/testing-guide.md +48 -99
- package/scripts/adopt/adopt-toolkits.ts +2 -4
- package/scripts/agent/agent-exit-record.ts +23 -1
- package/scripts/agent/codex-agent-argv.ts +7 -2
- package/scripts/backlog/backlog-budget-cli.ts +1 -2
- package/scripts/backlog/backlog-drive-carried.ts +26 -0
- package/scripts/backlog/backlog-drive-cli.ts +10 -5
- package/scripts/backlog/backlog-drive-named.ts +2 -4
- package/scripts/backlog/backlog-drive-restore.ts +19 -1
- package/scripts/backlog/backlog-drive-seats.ts +27 -0
- package/scripts/backlog/backlog-drive.ts +11 -6
- package/scripts/backlog/backlog-ready.ts +13 -6
- package/scripts/behavior/behavior-rules.ts +3 -2
- package/scripts/cost-runtime/codex-usage.ts +4 -2
- package/scripts/cost-runtime/cost-cli.ts +1 -2
- package/scripts/delegation/investigation-guard.ts +1 -1
- package/scripts/delegation/investigation-reads.ts +1 -1
- package/scripts/discovery/repo-discovery.ts +4 -2
- package/scripts/discovery/repo-origin.ts +2 -4
- package/scripts/doctor/doctor-logic.ts +6 -3
- package/scripts/document/document-byte-budget.ts +27 -27
- package/scripts/document/document-byte-check.ts +1 -2
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/epic/epic-audit-rationale.ts +8 -2
- package/scripts/epic/epic-bundle-cli.ts +1 -2
- package/scripts/epic/epic-chains.ts +6 -2
- package/scripts/epic/epic-close-comment.ts +1 -2
- package/scripts/gate/verification-gate.ts +7 -2
- package/scripts/gh/git-pr-ai-review.ts +12 -1
- package/scripts/gh/git-pr-confirmation.ts +11 -7
- package/scripts/gh/git-pr.ts +6 -2
- package/scripts/git/git-common-directory.ts +3 -3
- package/scripts/git/git-diff-reads.ts +16 -9
- package/scripts/git/git-execa-error.ts +1 -2
- package/scripts/hooks/format-edited-file.ts +10 -4
- package/scripts/hooks/pre-push-unit.ts +15 -4
- package/scripts/hooks/pretool-guard.ts +23 -29
- package/scripts/hooks/step-zero-notice.ts +3 -2
- package/scripts/init/hook-command-rewrite.ts +6 -2
- package/scripts/init/init-logic-deploy-vps.ts +4 -2
- package/scripts/issue/behavior-change-lint.ts +12 -6
- package/scripts/issue/issue-scout-cli.ts +3 -2
- package/scripts/josh/file-map-stamp.ts +1 -3
- package/scripts/josh/hook-decision.ts +17 -2
- package/scripts/josh/josh-commands-workflow.ts +1 -1
- package/scripts/lane/lane-await.ts +3 -2
- package/scripts/lane/lane-child-invocation.ts +14 -1
- package/scripts/lane/lane-guard-policy.ts +4 -4
- package/scripts/lane/lane-ledger.ts +9 -0
- package/scripts/lane/lane-load.ts +9 -2
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-parked.ts +18 -0
- package/scripts/lane/lane-process-load.ts +125 -0
- package/scripts/lane/lane-process-stats.ts +93 -0
- package/scripts/lane/lane-registry.ts +8 -3
- package/scripts/lane/lane-session-broker.ts +29 -5
- package/scripts/lane/lane-spent.ts +14 -3
- package/scripts/lane/lane-stats.ts +1 -0
- package/scripts/managed-marker/managed-marker-logic.ts +1 -2
- package/scripts/notify/confirmation-options.ts +59 -0
- package/scripts/notify/notify-opt-in.ts +33 -0
- package/scripts/notify/telegram-notify.ts +25 -8
- package/scripts/notify/telegram-test-logic.ts +33 -1
- package/scripts/notify/telegram-test.ts +4 -2
- package/scripts/overrides/lockfile-overrides.ts +1 -3
- package/scripts/propagate/propagate.ts +1 -2
- package/scripts/release/release-progress.ts +3 -2
- package/scripts/report/report-lint.ts +7 -4
- package/scripts/review/review-record-cli.ts +6 -2
- package/scripts/rules/destructive-command.ts +6 -9
- package/scripts/rules/direct-filing.ts +3 -7
- package/scripts/rules/early-heartbeat.ts +7 -18
- package/scripts/rules/file-body.ts +6 -13
- package/scripts/rules/filing-cap.ts +3 -6
- package/scripts/rules/git-force.ts +5 -9
- package/scripts/rules/implementation-cut.ts +37 -35
- package/scripts/rules/index-guard.ts +4 -7
- package/scripts/rules/issue-comments.ts +5 -15
- package/scripts/rules/josh-git-bare.ts +3 -6
- package/scripts/rules/lane-background.ts +10 -21
- package/scripts/rules/lane-carry-conflict.ts +7 -17
- package/scripts/rules/lane-interactive-ask.ts +6 -11
- package/scripts/rules/lane-park.ts +5 -13
- package/scripts/rules/lane-split-park.ts +2 -7
- package/scripts/rules/lane-switch-main.ts +9 -12
- package/scripts/rules/oracle-consulted.ts +4 -5
- package/scripts/rules/piped-verification.ts +81 -31
- package/scripts/rules/poll-loop.ts +7 -17
- package/scripts/rules/pre-gate-cut.ts +7 -21
- package/scripts/rules/prior-comment-read.ts +7 -5
- package/scripts/rules/protected-files.ts +5 -9
- package/scripts/rules/raw-field-body.ts +3 -7
- package/scripts/rules/rule-body-guard.ts +4 -11
- package/scripts/rules/run-tail-rule.ts +6 -22
- package/scripts/rules/shell-body-trigger.ts +7 -8
- package/scripts/rules/shell-segments.ts +7 -0
- package/scripts/rules/stop-rules.ts +17 -29
- package/scripts/rules/test-declared-commit.ts +4 -10
- package/scripts/rules/third-party-write.ts +5 -10
- package/scripts/rules/worktree-guard.ts +5 -12
- package/scripts/run/board/run-board-github.ts +11 -3
- package/scripts/run/board/run-board-idle.ts +31 -0
- package/scripts/run/board/run-board-read.ts +9 -12
- package/scripts/run/board/run-board-reason.ts +7 -3
- package/scripts/run/board/run-board-state.ts +4 -0
- package/scripts/run/board/run-board-status.ts +18 -3
- package/scripts/run/board/run-board-thanks.ts +7 -2
- package/scripts/run/board/run-board-tick.ts +32 -7
- package/scripts/run/carry/run-carry-args.ts +1 -3
- package/scripts/run/carry/run-carry-conversation.ts +7 -2
- package/scripts/run/cut/run-cut-cli.ts +15 -20
- package/scripts/run/cut/run-cut-decline.ts +72 -0
- package/scripts/run/cut/run-cut-ready.ts +74 -0
- package/scripts/run/cut/run-cut-report.ts +13 -0
- package/scripts/run/cut/run-cut.ts +10 -1
- package/scripts/run/entry/run-entry-stop.ts +6 -3
- package/scripts/run/hold/run-hold-cli.ts +14 -3
- package/scripts/run/hold/run-hold.ts +6 -3
- package/scripts/run/merge/run-merge-cli.ts +30 -7
- package/scripts/run/merge/run-merge-ship.ts +59 -1
- package/scripts/run/merge/run-merge.ts +3 -2
- package/scripts/run/run-ending.ts +31 -3
- package/scripts/run/run-parent-cut-hook.ts +5 -10
- package/scripts/run/run-preflight.ts +1 -2
- package/scripts/run/run-stop-notify.ts +3 -2
- package/scripts/run/run-watcher-guard.ts +6 -3
- package/scripts/run/ship/run-ship-review-steps.ts +13 -4
- package/scripts/run/ship/run-ship-review.ts +15 -2
- package/scripts/run/tidy/run-tidy-cli.ts +3 -3
- package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
- package/scripts/safe-chain/project-config.ts +7 -2
- package/scripts/sync/sync-hook-safety.ts +8 -2
- package/scripts/test/unit-worker-share.ts +3 -2
- package/scripts/time-runtime/time-batch-guard.ts +3 -3
- package/scripts/time-runtime/time-hook-transcript.ts +3 -2
- package/scripts/time-runtime/time-shell.ts +10 -0
- package/scripts/version/effective-upstream.ts +3 -4
- package/scripts/version/kit-version-config.ts +8 -2
- package/scripts/version/latest-corepack.ts +1 -2
- package/scripts/version/release-hold.ts +11 -4
- package/scripts/version/version-commands.ts +12 -4
- package/dist/hooks/chunk-UT7YC3N2.js +0 -56
|
@@ -83,25 +83,15 @@ function is_poll_loop(command: string): boolean {
|
|
|
83
83
|
return LOOP_KEYWORD.test(plain) && SLEEP_COMMAND.test(plain)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// The instruction in the shape a refusal can carry:
|
|
87
|
-
//
|
|
88
|
-
//
|
|
86
|
+
// The instruction in the shape a refusal can carry: why the command's exit is the thing to wait on,
|
|
87
|
+
// and the one route that delivers that exit. The measurement behind it is in
|
|
88
|
+
// `docs/maintainers/rule-delivery-rationale.md`.
|
|
89
89
|
const POLL_LOOP_REASON =
|
|
90
90
|
"⛔ output poll loop: this hand-writes a `while`/`until` wait loop over a command's output, a " +
|
|
91
|
-
'marker file or a process
|
|
92
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
95
|
-
'(joshuafolkken/kit#2371 measured about 45 minutes, 13% of six lanes, lost to exactly this). Do ' +
|
|
96
|
-
'not poll the output file. A command known to take minutes — `pnpm josh gate`, `pnpm josh ' +
|
|
97
|
-
'followup`, `pnpm josh format` — is started detached from the first with `run_in_background` set, ' +
|
|
98
|
-
'and the harness re-invokes you when it exits: its completion notification is what ends the wait, ' +
|
|
99
|
-
'on the task exit rather than a regex or the block limit. So issue the long command in the ' +
|
|
100
|
-
'background and end the turn — do not arm a loop; the wake-up brings you back. To glance at ' +
|
|
101
|
-
'interim output, `Read` the output file once, never in a sleep loop. If you are waiting on CI, ' +
|
|
102
|
-
'`pnpm josh followup` waits on it for you. The procedure is ' +
|
|
103
|
-
'`.claude/skills/workflow-commands/background-commands.md`, "A command that can take minutes is ' +
|
|
104
|
-
'issued in the background". **This rule fires on every occurrence, not once per run.**'
|
|
91
|
+
'marker file or a process, and the loop does not know when the command finished. Issue the long ' +
|
|
92
|
+
'command with `run_in_background` set and end the turn — its exit re-invokes you; for CI, ' +
|
|
93
|
+
'`pnpm josh followup` (`.claude/skills/workflow-commands/background-commands.md`). It fires on ' +
|
|
94
|
+
'every occurrence.'
|
|
105
95
|
|
|
106
96
|
// The row itself, so `delivered-rules.ts` spreads one entry. `decide` returns true so it refuses every
|
|
107
97
|
// occurrence; it declares no `keeps` — waiting on the task exit is the *absence* of a loop, not a
|
|
@@ -133,9 +133,7 @@ function dispatched_issue(state: LaneCutState): string | undefined {
|
|
|
133
133
|
function uncut_lane_issue(state: LaneCutState): string | undefined {
|
|
134
134
|
const issue = lane_paths.lane_issue_of(state.directory)
|
|
135
135
|
|
|
136
|
-
if (issue === undefined) return undefined
|
|
137
|
-
|
|
138
|
-
if (dispatched_issue(state) !== issue) return undefined
|
|
136
|
+
if (issue === undefined || dispatched_issue(state) !== issue) return undefined
|
|
139
137
|
|
|
140
138
|
return state.carried()?.issue === issue ? undefined : issue
|
|
141
139
|
}
|
|
@@ -159,26 +157,14 @@ function is_uncut_gate(command: string, state: LaneCutState = current_state()):
|
|
|
159
157
|
return warrants_the_cut(state.context_verdict())
|
|
160
158
|
}
|
|
161
159
|
|
|
162
|
-
// The instruction in the shape a refusal can carry:
|
|
163
|
-
// the reissue sentence every delivery needs.
|
|
164
|
-
//
|
|
165
|
-
// to go and read which of the other five leave it holding the run — the same reason the WIP cap's
|
|
166
|
-
// delivery names its three interrupt tests instead of naming the file they live in.
|
|
160
|
+
// The instruction in the shape a refusal can carry: the cut, the one verdict that ends the turn, and
|
|
161
|
+
// the reissue sentence every delivery needs. `cut` is the only verdict that ends the turn, so the
|
|
162
|
+
// refusal says so rather than leaving a run to look up which of the others leave it holding the run.
|
|
167
163
|
const PRE_GATE_CUT_REASON =
|
|
168
164
|
'⛔ pre-gate cut: this checkout is a lane dispatched for this issue and the gate is the boundary to ' +
|
|
169
|
-
'cut at
|
|
170
|
-
'
|
|
171
|
-
'
|
|
172
|
-
'176K of 204K output riding across that boundary, and joshuafolkken/kit#1864 measured the step ' +
|
|
173
|
-
'itself firing 0 times in 6 lane children while it was carried as prose. Issue ' +
|
|
174
|
-
'`pnpm josh run:cut <N>` now, and read the verdict: on `cut`, **end the turn immediately** — the ' +
|
|
175
|
-
'fresh process owns the run from there and must not be waited for, and **no Telegram is owed** for a cut that resumes itself; on `not-a-lane`, `unready`, ' +
|
|
176
|
-
'`busy`, `failed` or `unknown`, this process carries the run on and the gate is simply the next ' +
|
|
177
|
-
'call. Never relaunch a second process after `busy`. This fired because the dispatch mark names ' +
|
|
178
|
-
'this lane; a person working here carries no mark and sees no refusal, so there is no ' +
|
|
179
|
-
'human-or-child judgement left to make. The procedure is ' +
|
|
180
|
-
'`.claude/skills/workflow-commands/pre-gate-cut.md`. Reissue the gate once the cut has answered — ' +
|
|
181
|
-
'this fires once per run, so it cannot repeat on the call in hand.'
|
|
165
|
+
'cut at. Issue `pnpm josh run:cut <N>`: on `cut`, end the turn immediately — no Telegram is owed; ' +
|
|
166
|
+
'on any other verdict (`not-a-lane`, `busy`, …), reissue the gate — this fires once per run ' +
|
|
167
|
+
'(`.claude/skills/workflow-commands/pre-gate-cut.md`).'
|
|
182
168
|
|
|
183
169
|
// **The pre-gate cut is two acts, one call apart**, so its `rule:value`
|
|
184
170
|
// denominator is only the runs that reached the boundary: a run that issues the cut's entry check and,
|
|
@@ -15,20 +15,22 @@ import { tail_commands } from './tail-commands'
|
|
|
15
15
|
// **It reads comments in the josh spelling too.** `pnpm josh issue:read <N>` prints the body and every
|
|
16
16
|
// comment in one call, so a run that opened an Issue with it has read the comments as surely as one
|
|
17
17
|
// that passed `--comments` — and that spelling is the one the workflow itself reaches for, so leaving
|
|
18
|
-
// it out would refuse the reader that obeyed the rule most directly.
|
|
18
|
+
// it out would refuse the reader that obeyed the rule most directly. **`run:entry <N>` and `run:prep <N>`
|
|
19
|
+
// count the same way**: both print the bundled report that carries the body and every comment, and a
|
|
20
|
+
// run that opened with one was refused for a read it had made (joshuafolkken/kit#3875).
|
|
19
21
|
|
|
20
22
|
// The path segment `…/issues/<N>` and `…/issues/<N>/comments` both name the Issue in the same capture.
|
|
21
23
|
const ISSUE_PATH_NUMBER = /issues\/(\d+)/gu
|
|
22
24
|
// The positional after `gh issue view`, past any flags that precede it.
|
|
23
25
|
const ISSUE_VIEW_NUMBER = /\bissue\s+view\s+(?:-{1,2}\S+\s+)*(\d+)/u
|
|
24
|
-
// The run of numbers
|
|
25
|
-
const ISSUE_READ_ARGUMENTS = /\
|
|
26
|
+
// The run of numbers a josh comment-reading command takes as its positional arguments.
|
|
27
|
+
const ISSUE_READ_ARGUMENTS = /\b(?:issue:read|run:entry|run:prep)\b([\s\d]*)/u
|
|
26
28
|
|
|
27
29
|
// A command that prints an Issue's comments: the comments endpoint, `gh issue view … --comments`, or
|
|
28
|
-
//
|
|
30
|
+
// a josh command that reads body and comments together.
|
|
29
31
|
const COMMENTS_ENDPOINT = /issues\/\d+\/comments\b/u
|
|
30
32
|
const VIEW_WITH_COMMENTS = /\bissue\s+view\b[^\n]*--comments\b/u
|
|
31
|
-
const ISSUE_READ_COMMAND = /\
|
|
33
|
+
const ISSUE_READ_COMMAND = /\b(?:issue:read|run:entry|run:prep)\b/u
|
|
32
34
|
|
|
33
35
|
function add_issue_read_numbers(command: string, numbers: Set<number>): void {
|
|
34
36
|
const match = ISSUE_READ_ARGUMENTS.exec(command)
|
|
@@ -119,21 +119,17 @@ function is_protected_file_call(
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
const PROTECTED_FILE_REASON =
|
|
122
|
-
'⛔ protected file: `.env
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'in their own process (the tracked `.env.example` and `.env.test` stay readable). ' +
|
|
126
|
-
'`.claude/settings.json` in a ' +
|
|
127
|
-
'consumer repository is the deny list and the hooks every other rule rests on, so a run must not ' +
|
|
128
|
-
'rewrite it; `josh sync` owns it, and a change belongs upstream in kit (`CLAUDE.md` → "Route ' +
|
|
129
|
-
'distributed-doc / config changes upstream to kit"). Leave these files alone, or ask the user. ' +
|
|
130
|
-
'**This rule fires on every occurrence, not once per run.**'
|
|
122
|
+
'⛔ protected file: `.env`, a `.env.*` variant and `.dev.vars` hold secrets, and a consumer ' +
|
|
123
|
+
'`.claude/settings.json` is owned by `josh sync` (a change belongs upstream in kit). Leave these ' +
|
|
124
|
+
'files alone, or ask the user. It fires on every occurrence.'
|
|
131
125
|
|
|
132
126
|
const ROW = {
|
|
133
127
|
id: 'protected-file',
|
|
134
128
|
is_trigger: is_protected_file_call,
|
|
135
129
|
reason: PROTECTED_FILE_REASON,
|
|
136
130
|
decide: (): boolean => true,
|
|
131
|
+
// The opening the reason carried before it was cut to the action and the next command.
|
|
132
|
+
former_reasons: ['⛔ protected file: `.env` holds the Telegram bot token'],
|
|
137
133
|
}
|
|
138
134
|
|
|
139
135
|
const protected_files = { ROW, is_protected_file_call }
|
|
@@ -35,13 +35,9 @@ function posts_at_path_literally(command: string): boolean {
|
|
|
35
35
|
// it reads as unbelievable — the call succeeds and returns a URL while posting the path as text.
|
|
36
36
|
const RAW_FIELD_BODY_REASON =
|
|
37
37
|
'⛔ literal @path body: this passes `body=@<path>` to `gh api` with a raw field flag (`-f` / ' +
|
|
38
|
-
'`--raw-field`), which
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'invisible until a person reads the issue (joshuafolkken/kit#2304 lost two park comments this way). ' +
|
|
42
|
-
'Post an issue or PR comment with `pnpm josh issue:comment <N> --body-file <path>` — the one ' +
|
|
43
|
-
'spelling, which cannot take the wrong flag. The rule and the safe forms are in ' +
|
|
44
|
-
'`prompts/collaboration-workflow/shell-body.md`. This rule fires on every occurrence, not once per run.'
|
|
38
|
+
'`--raw-field`), which posts the string `@<path>` rather than the file. Post the comment with ' +
|
|
39
|
+
'`pnpm josh issue:comment <N> --body-file <path>`; this rule fires on every occurrence ' +
|
|
40
|
+
'(`prompts/collaboration-workflow/shell-body.md`).'
|
|
45
41
|
|
|
46
42
|
const ROW = {
|
|
47
43
|
id: 'raw-field-body',
|
|
@@ -131,17 +131,10 @@ function writes_rule_prose(
|
|
|
131
131
|
// skips, the commands that answer them, and the reissue once they have run.
|
|
132
132
|
const RULE_BODY_REASON =
|
|
133
133
|
'⛔ rule written into prose: this edit adds rule text to CLAUDE.md, a `prompts/**` doc or a skill ' +
|
|
134
|
-
'body.
|
|
135
|
-
'
|
|
136
|
-
'
|
|
137
|
-
'`
|
|
138
|
-
'in the run driver, not prose — the ordering question; `pnpm josh run:step` is the state machine a ' +
|
|
139
|
-
'sequencing rule moves into. Only a rule that is neither — not computable, not ordering — stays ' +
|
|
140
|
-
'prose, and then as a trigger-plus-pointer line rather than a second copy of its procedure. The ' +
|
|
141
|
-
'criterion is `prompts/collaboration-workflow/residency.md` (question 0 and the ordering question); ' +
|
|
142
|
-
'the delivery enumeration is `prompts/collaboration-workflow/rule-delivery.md`. Every such edit is ' +
|
|
143
|
-
'refused until both commands are on the transcript — run them, confirm the rule can be neither an ' +
|
|
144
|
-
'oracle nor an ordering step, then reissue this edit.'
|
|
134
|
+
'body. Answer question 0 (`pnpm josh oracle:list`) and the ordering question (`pnpm josh ' +
|
|
135
|
+
'run:step`) of `prompts/collaboration-workflow/residency.md`; refused until both commands are on ' +
|
|
136
|
+
'the transcript, then reissue this as a trigger-plus-pointer line ' +
|
|
137
|
+
'(`prompts/collaboration-workflow/rule-delivery.md`).'
|
|
145
138
|
|
|
146
139
|
// The two placement oracles the residency questions name: question 0 is `pnpm josh oracle:list`, the
|
|
147
140
|
// ordering question is `pnpm josh run:step` (its degenerate pre-implementation form `run:next` counts
|
|
@@ -137,30 +137,14 @@ function decide(): boolean {
|
|
|
137
137
|
return true
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
// The instruction in the shape a refusal can carry:
|
|
141
|
-
//
|
|
142
|
-
// is what makes the rule believable — the work was 17 minutes and the run was 45.
|
|
140
|
+
// The instruction in the shape a refusal can carry: the reissue, and the half a run forgets once the
|
|
141
|
+
// push has landed. The measurement behind it is in `docs/maintainers/rule-delivery-rationale.md`.
|
|
143
142
|
const RUN_TAIL_REASON =
|
|
144
143
|
'⛔ run tail: this issues the commit-push-PR step in the foreground, where the harness decides ' +
|
|
145
|
-
'when to stop waiting. Reissue it with `run_in_background` set
|
|
146
|
-
'
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'cap decides, not the number. A backgrounded call re-invokes you when it exits, so the completion ' +
|
|
150
|
-
'notification is what resumes the run. **When it arrives, the turn that reads it goes straight ' +
|
|
151
|
-
'through any branch-2 filing and `pnpm josh epic:bundle` to `pnpm josh followup` — the turn never ' +
|
|
152
|
-
'ends at the push.** CI went green 6m06s before the merge started in that run, and it started ' +
|
|
153
|
-
'only because the person asked; joshuafolkken/kit#1333 had already settled that the merge is ' +
|
|
154
|
-
'issued in the turn the review closes. While the push runs, do the work that writes nothing — ' +
|
|
155
|
-
'drafting the completion body to a file, deciding the branch-2 disposition. `pnpm josh followup` ' +
|
|
156
|
-
'itself stays in the **foreground** — nearly every step after it reads its result. But a tail ' +
|
|
157
|
-
'does follow the merge, measured at 3.0 min and 5.9% of a run (joshuafolkken/kit#1462), so ' +
|
|
158
|
-
'empty it beforehand: the steps that read the merge result stay after `followup`, and the rest ' +
|
|
159
|
-
'are composed in the turn that issues it, bar the one exception background-commands.md names. The ' +
|
|
160
|
-
'procedure is `.claude/skills/workflow-commands/background-commands.md`, "A command that can take ' +
|
|
161
|
-
'minutes is issued in the background". **This rule fires on every foreground push, not once per ' +
|
|
162
|
-
'run**, so ' +
|
|
163
|
-
'reissuing the same call in the foreground will be refused again.'
|
|
144
|
+
'when to stop waiting. Reissue it with `run_in_background` set; when it exits, go straight on to ' +
|
|
145
|
+
'`pnpm josh followup` in the foreground — the turn never ends at the push ' +
|
|
146
|
+
'(`.claude/skills/workflow-commands/background-commands.md`). It fires on every foreground push, ' +
|
|
147
|
+
'not once per run.'
|
|
164
148
|
|
|
165
149
|
const run_tail_rule = {
|
|
166
150
|
RUN_TAIL_REASON,
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
// A body handed to a command as an inline double-quoted argument, in the spellings a run reaches for:
|
|
8
8
|
// `gh`'s field flags (`-f` / `-F` / `--field` / `--raw-field` with `body=`), `gh`'s own `--body`, and
|
|
9
|
-
// `josh`'s `--body` and `--notify-message
|
|
9
|
+
// `josh`'s `--body` and `--notify-message`, and `josh notify`'s `--recommended` / `--alternative` —
|
|
10
|
+
// free text every confirmation carries, naming the very commands a backtick would run. The value is captured so the decision can be made on what
|
|
10
11
|
// the body actually contains rather than on the flag alone.
|
|
11
12
|
//
|
|
12
13
|
// **`-b` is deliberately absent.** It is `gh`'s short `--body`, but it is also `git checkout -b`, and
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
// spellings end in `-` where this pattern needs whitespace or `=`, so `--body-file <path>` and
|
|
15
16
|
// `--notify-message-file <path>` cannot match it.
|
|
16
17
|
const FIELD_FLAGS = '(?:-f|-F|--field|--raw-field)'
|
|
17
|
-
const MESSAGE_FLAGS = '(?:--body|--notify-message)'
|
|
18
|
+
const MESSAGE_FLAGS = '(?:--body|--notify-message|--recommended|--alternative)'
|
|
18
19
|
|
|
19
20
|
// **The opening quote sits on either side of `body=`, and both spellings are the same command.** zsh
|
|
20
21
|
// strips the quotes before `gh` is started, so `-f body="… \`pnpm josh ms\` …"` and
|
|
@@ -186,12 +187,10 @@ function carries_a_body(command: string): boolean {
|
|
|
186
187
|
// (`git-force.ts`, `lane-carry-conflict.ts`).
|
|
187
188
|
const SHELL_BODY_REASON =
|
|
188
189
|
'⛔ shell-evaluated body: this command carries a body inline in double quotes, and that body ' +
|
|
189
|
-
'contains a backtick or a `$`, so the shell
|
|
190
|
-
'
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"trigger's blind spots are in `prompts/collaboration-workflow/shell-body.md`. Reissue this call " +
|
|
194
|
-
'once the body is in a file — it fires once per run and cannot repeat on the call in hand.'
|
|
190
|
+
'contains a backtick or a `$`, so the shell executes it. Reissue this call once the body is in a ' +
|
|
191
|
+
'file, passed by path (`pnpm josh issue:comment <N> --body-file <path>`, ' +
|
|
192
|
+
"`--notify-message-file <path>`), or use `$'…'` quoting " +
|
|
193
|
+
'(`prompts/collaboration-workflow/shell-body.md`).'
|
|
195
194
|
|
|
196
195
|
const shell_body_trigger = {
|
|
197
196
|
SHELL_BODY_REASON,
|
|
@@ -14,12 +14,18 @@ import { time_shell } from '#scripts/time-runtime/time-shell'
|
|
|
14
14
|
// because `sleep 1200 &` is an armed timer sent to the background and has to be seen as one. That is
|
|
15
15
|
// a different question about a different character, not a second copy of this answer.
|
|
16
16
|
const SEGMENT_SEPARATOR = /&&|\|\||;|\n/u
|
|
17
|
+
const SEGMENT_PARTS = new RegExp(`(${SEGMENT_SEPARATOR.source})`, 'u')
|
|
17
18
|
|
|
18
19
|
/** The line's commands, trimmed. Empty segments are kept: a caller anchoring at `^` rejects them. */
|
|
19
20
|
function segments_of(command: string): Array<string> {
|
|
20
21
|
return command.split(SEGMENT_SEPARATOR).map((segment) => segment.trim())
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/** The same cut, untrimmed and with each separator kept between its commands, so the parts join back to the line. */
|
|
25
|
+
function parts_of(command: string): Array<string> {
|
|
26
|
+
return command.split(SEGMENT_PARTS)
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
// **Both spellings match, and the expansion is not this file's.** `josh_command_of` expands the alias
|
|
24
30
|
// in the command it reads, so `pnpm josh ga` arrives as `josh gate` and a caller's canonical set
|
|
25
31
|
// matches it as it is. Widening as well would be a second copy of one rule, and the caller that
|
|
@@ -36,6 +42,7 @@ function is_josh_command(segment: string, names: ReadonlySet<string>): boolean {
|
|
|
36
42
|
|
|
37
43
|
const shell_segments = {
|
|
38
44
|
is_josh_command,
|
|
45
|
+
parts_of,
|
|
39
46
|
segments_of,
|
|
40
47
|
}
|
|
41
48
|
|
|
@@ -101,24 +101,19 @@ const NO_OUTCOME: StopOutcome = { reason: undefined }
|
|
|
101
101
|
// mid-workflow, and the transcript is what says the notify was sent.
|
|
102
102
|
const STOP_NOTIFY_REASON =
|
|
103
103
|
'⛔ mid-workflow stop notification: this working tree is still held by a run and no `confirmation` ' +
|
|
104
|
-
'Telegram was sent this turn
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'your previous reply. This fires only while the hold is held and no notify is on the transcript, and ' +
|
|
109
|
-
'`stop_hook_active` lets a second stop through so a run is never wedged.'
|
|
104
|
+
'Telegram was sent this turn. Send ' +
|
|
105
|
+
'`pnpm josh notify --task-type confirmation --issue-url "<url>" --body=$\'<reason>\' ' +
|
|
106
|
+
'--recommended "<choice>" --alternative "<choice>"` (`CLAUDE.md` → "Mid-workflow stop ' +
|
|
107
|
+
'notification"), then confirm in one line that it was sent — do not repeat your previous reply.'
|
|
110
108
|
|
|
111
109
|
// **A clean tree that still holds is a tree the next run will trample.** `working-tree-hold.md`: a stop that
|
|
112
110
|
// leaves the tree clean releases the hold; a `halfrun` pre-commit stop and a `needs-human-review` stop
|
|
113
111
|
// keep it because their tree is dirty, which is why this row is silent whenever the tree is not clean.
|
|
114
112
|
const HOLD_RELEASE_REASON =
|
|
115
113
|
'⛔ working-tree hold not released: this working tree is clean but its `run:hold` record is still in ' +
|
|
116
|
-
'place
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
'pre-commit stop and a `needs-human-review` stop keep the hold because their tree is dirty, and a ' +
|
|
120
|
-
'`prrun` stop keeps it by its stop mark — this row is silent there. Release it, then end with a one-line confirmation that it was released — do ' +
|
|
121
|
-
'not repeat your previous reply.'
|
|
114
|
+
'place. Release it with `pnpm josh run:release <N>` (bare for a `new` entry; ' +
|
|
115
|
+
'`working-tree-hold.md`), then confirm in one line that it was released — do not repeat your ' +
|
|
116
|
+
'previous reply.'
|
|
122
117
|
|
|
123
118
|
// **A refusal that corrects rather than advises**. The bare `#N` is already
|
|
124
119
|
// on screen and the hook cannot unsay it, so the reason does the one thing that helps the *next* reply:
|
|
@@ -148,13 +143,10 @@ function build_citation_reason(references: ReadonlyArray<string>): string {
|
|
|
148
143
|
// first**: there the offer is the correct reply, so `needs_filing` is silent.
|
|
149
144
|
const FILING_OFFER_REASON =
|
|
150
145
|
'⛔ filing offer: your reply offers to file an Issue instead of filing it, and this run is ' +
|
|
151
|
-
'unattended — nobody is there to answer.
|
|
152
|
-
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
'runs `epic:bundle` itself — then end with the one-line citation of what was filed; do not repeat ' +
|
|
156
|
-
'your previous reply. If it is not worth filing ' +
|
|
157
|
-
'after all, say so in one line instead.'
|
|
146
|
+
'unattended — nobody is there to answer. Run ' +
|
|
147
|
+
'`pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2>` (Tier A, ' +
|
|
148
|
+
'`observation-filing.md`), then end with the one-line citation of what was filed; do not repeat ' +
|
|
149
|
+
'your previous reply. If it is not worth filing after all, say so in one line instead.'
|
|
158
150
|
|
|
159
151
|
// **A headless parent's turn-end is its process's end**. Under `claude -p` the
|
|
160
152
|
// background waits die with the turn, so the lanes lose their parent and nothing re-invokes it. The
|
|
@@ -175,12 +167,10 @@ const HEADLESS_REFUSAL_CAP = 3
|
|
|
175
167
|
const HEADLESS_WAIT_CALLS = ['"command":"pnpm josh lane:await']
|
|
176
168
|
|
|
177
169
|
const HEADLESS_WAIT_BODY =
|
|
178
|
-
' this is a `claude -p` `backlogrun` parent
|
|
179
|
-
'
|
|
180
|
-
"lanes are left without a parent. Do not watch them yourself: the `run:wake` supervisor's driver " +
|
|
181
|
-
'watches the lanes and wakes a session only for a branch that needs judging. Hand the loop back with ' +
|
|
170
|
+
' this is a `claude -p` `backlogrun` parent with lanes in flight, and ending this turn kills the ' +
|
|
171
|
+
'process and its background waits. Hand the loop back with ' +
|
|
182
172
|
'`pnpm josh run:carry --cut --owner "$PPID"` (or `--end` when the run is over), then end the turn ' +
|
|
183
|
-
'(`backlogrun-progress.md`
|
|
173
|
+
'(`backlogrun-progress.md`).'
|
|
184
174
|
|
|
185
175
|
const HEADLESS_WAIT_REASON = `${HEADLESS_WAIT_MARKER}${HEADLESS_WAIT_BODY}`
|
|
186
176
|
|
|
@@ -194,11 +184,9 @@ const PERMISSION_PATTERN = /許可|承認|permission|approve|approval|authorizat
|
|
|
194
184
|
|
|
195
185
|
const LANE_INDEX_REASON =
|
|
196
186
|
'⛔ lane index permission: this is a dispatched lane child (`JOSH_LANE_CHILD`) and your reply asks ' +
|
|
197
|
-
|
|
198
|
-
'
|
|
199
|
-
'
|
|
200
|
-
'This is Tier A (`.claude/skills/workflow-commands/chain-rule.md` → "origin/main is merged in before ' +
|
|
201
|
-
'the gate"). Do it and continue the run — do not repeat your previous reply.'
|
|
187
|
+
'a person for permission to change the git index. That is Tier A here (`chain-rule.md`): remove any ' +
|
|
188
|
+
'conflict markers, run `pnpm josh git -y` (it stages and commits, a conflicted merge included) and ' +
|
|
189
|
+
'continue the run — do not repeat your previous reply.'
|
|
202
190
|
|
|
203
191
|
function asks_index_permission(message: string): boolean {
|
|
204
192
|
return INDEX_COMMAND_PATTERN.test(message) && PERMISSION_PATTERN.test(message)
|
|
@@ -30,16 +30,10 @@ function is_untested_commit(call: GuardedCall): boolean {
|
|
|
30
30
|
|
|
31
31
|
const REASON =
|
|
32
32
|
'⛔ test declared: this commits a runtime code change with no test beside it — `pnpm josh ' +
|
|
33
|
-
'test:declared` reads
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'E2E. Add the declared test and reissue this commit. **If the change genuinely touches no ' +
|
|
38
|
-
'executable runtime path** it is the pre-approved non-runtime exception — declare it in your Step ' +
|
|
39
|
-
'0 work summary with the reason it is exempt (non-executable config, a cosmetic asset swap) and ' +
|
|
40
|
-
'reissue; the exception is a presentation, not a stop. Run `pnpm josh test:declared` to see the ' +
|
|
41
|
-
'files it names. The procedure is `prompts/collaboration-workflow/rule-delivery.md` → ' +
|
|
42
|
-
'"配送されている規則". This fires once per run, so reissuing will not repeat on the call in hand.'
|
|
33
|
+
'test:declared` reads it `required` and names the files. Tests are required for ALL code changes: ' +
|
|
34
|
+
'add the test and reissue, or, for a change that touches no runtime path, declare the non-runtime ' +
|
|
35
|
+
'exception in your Step 0 work summary and reissue. It fires once per run (`CLAUDE.md` → Code ' +
|
|
36
|
+
'Change Rules).'
|
|
43
37
|
|
|
44
38
|
// The enumeration row itself, so `delivered-rules.ts` spreads one entry rather than restating the
|
|
45
39
|
// trigger and reason it already single-sources here. Once per run (the default delivery) for the
|
|
@@ -103,16 +103,11 @@ function writes_third_party(
|
|
|
103
103
|
// to do instead. The command that computes the party is handed over, because the whole point of this
|
|
104
104
|
// Issue is that the test is computed rather than judged.
|
|
105
105
|
const THIRD_PARTY_WRITE_REASON =
|
|
106
|
-
'⛔ third-party write: this `gh api` call writes to a repository whose owner is not this session
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'
|
|
110
|
-
'`prompts/collaboration-workflow/upstream-interrupt.md`).
|
|
111
|
-
'`pnpm josh repo:party <owner/repo>` — owner equality, computed not judged. A read (`-X GET`, or no ' +
|
|
112
|
-
'field flag) passes untouched; only a write (`-f`/`-F`/`--field`/`--raw-field`/`--input`, or ' +
|
|
113
|
-
'`-X POST|PATCH|PUT|DELETE`) is refused. Record the finding on our own Issue and draft the artifact ' +
|
|
114
|
-
'for a person to post — do not publish it yourself. **This rule fires on every occurrence, not once ' +
|
|
115
|
-
'per run.**'
|
|
106
|
+
'⛔ third-party write: this `gh api` call writes to a repository whose owner is not this session, ' +
|
|
107
|
+
'which is Tier C and needs explicit current-turn user instruction. Check with ' +
|
|
108
|
+
'`pnpm josh repo:party <owner/repo>`, then record the finding on our own Issue and draft the ' +
|
|
109
|
+
'artifact for a person to post. This rule fires on every occurrence ' +
|
|
110
|
+
'(`prompts/collaboration-workflow/upstream-interrupt.md`).'
|
|
116
111
|
|
|
117
112
|
// `decide` returns true so it refuses every occurrence; it declares no `keeps` — not writing to
|
|
118
113
|
// another owner's tracker is the absence of a call, not a call, so the row is reported unmeasured
|
|
@@ -124,18 +124,11 @@ function is_unauthorized_worktree_change(command: string): boolean {
|
|
|
124
124
|
// and — for a stash — the message form that is allowed. `pnpm josh git` and `pnpm josh stash:pop` are
|
|
125
125
|
// invisible to this trigger, so a run that uses them is never refused.
|
|
126
126
|
const WORKTREE_MUTATION_REASON =
|
|
127
|
-
'⛔ unauthorized working-tree change: `git checkout
|
|
128
|
-
'`git
|
|
129
|
-
'
|
|
130
|
-
'
|
|
131
|
-
'
|
|
132
|
-
'divert. A stash push is authorized only inside a documented flow and only with a findable message ' +
|
|
133
|
-
'— `git stash push -u -m "<message>"` — which this row allows; a bare `git stash`, a message-less ' +
|
|
134
|
-
'push, or `pop` / `apply` / `drop` is refused. A plan that restores a handed-over stash with `git ' +
|
|
135
|
-
'stash apply` / `git stash pop` is read as `pnpm josh stash:pop "<message>"`: apply it that way, ' +
|
|
136
|
-
'continue, and record the substitution on the Issue — Tier A, never a `needs-decision` park ' +
|
|
137
|
-
'unless the plan says why the owner keeps the stash: then stop and confirm (`operating-rules.md` ' +
|
|
138
|
-
'→ "no-self-staging"). **This rule fires on every occurrence, not once per run.**'
|
|
127
|
+
'⛔ unauthorized working-tree change: `git checkout --`, `git restore`, `git clean -f`, ' +
|
|
128
|
+
'`git stash`. A documented flow stashes with `git stash push -u -m "<message>"`; a handed-over ' +
|
|
129
|
+
'stash is `pnpm josh stash:pop "<message>"`; record the substitution on the Issue (Tier A) unless ' +
|
|
130
|
+
'the plan says why the owner keeps the stash: then stop and confirm. It fires on every occurrence ' +
|
|
131
|
+
'(`operating-rules.md`).'
|
|
139
132
|
|
|
140
133
|
const ROW = {
|
|
141
134
|
id: 'worktree-mutation',
|
|
@@ -2,6 +2,7 @@ import { run_progress } from '#scripts/run/progress/run-progress'
|
|
|
2
2
|
import { run_progress_cli } from '#scripts/run/progress/run-progress-cli'
|
|
3
3
|
import type { ClosedAnswer } from './run-board-closed'
|
|
4
4
|
import { run_board_fetch, type Fetch } from './run-board-fetch'
|
|
5
|
+
import { run_board_idle } from './run-board-idle'
|
|
5
6
|
import { run_board_layout, type BoardPlan } from './run-board-layout'
|
|
6
7
|
import type { LocalRead } from './run-board-read'
|
|
7
8
|
import { run_board_state, type BoardPorts, type BoardState, type Pace } from './run-board-state'
|
|
@@ -72,8 +73,15 @@ function fold_plan(state: BoardState): BoardState {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
function statuses_for(
|
|
76
|
-
|
|
76
|
+
function statuses_for(
|
|
77
|
+
state: BoardState,
|
|
78
|
+
plan: BoardPlan,
|
|
79
|
+
local: LocalRead,
|
|
80
|
+
): ReadonlyMap<number, ItemStatus> {
|
|
81
|
+
const in_run = run_board_layout.numbers_of(plan)
|
|
82
|
+
const stopped = run_board_idle.stopped_of(state.idle_since, state.read_ms)
|
|
83
|
+
|
|
84
|
+
return run_board_status.statuses_of(local.events, local.lanes, in_run, stopped)
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
// The children the run touched that the open listing no longer holds and the board has not read yet.
|
|
@@ -83,7 +91,7 @@ function unread_closed(state: BoardState, local: LocalRead): Array<number> {
|
|
|
83
91
|
|
|
84
92
|
if (open === undefined || state.plan === undefined) return []
|
|
85
93
|
|
|
86
|
-
const touched = [...statuses_for(state.plan, local).keys()]
|
|
94
|
+
const touched = [...statuses_for(state, state.plan, local).keys()]
|
|
87
95
|
|
|
88
96
|
return touched.filter((issue) => !open.has(issue) && !state.closed.has(issue))
|
|
89
97
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { lane_await } from '#scripts/lane/lane-await'
|
|
2
|
+
|
|
3
|
+
// A lane is drawn stopped only once it has read idle for `lane_await.RECONFIRM_MS`: a boundary cut drops
|
|
4
|
+
// the child's process for a moment before its resume takes over, and one bare probe cannot tell that
|
|
5
|
+
// handoff from a child that died — `lane:await` re-confirms a disappearance over the same window. A live
|
|
6
|
+
// board carries when each lane was first read idle across its reads; a single frame has no earlier read,
|
|
7
|
+
// so it reads the lanes again after the window (`run-board-tick.ts`).
|
|
8
|
+
|
|
9
|
+
/** When each lane the last read found idle was first read so. */
|
|
10
|
+
type IdleSince = ReadonlyMap<string, number>
|
|
11
|
+
|
|
12
|
+
const NO_IDLE: IdleSince = new Map()
|
|
13
|
+
|
|
14
|
+
// The lanes read idle now, each keeping the moment it was first read so; a lane read working drops out.
|
|
15
|
+
function since_of(before: IdleSince, idle: ReadonlyArray<string>, now_ms: number): IdleSince {
|
|
16
|
+
return new Map(idle.map((lane) => [lane, before.get(lane) ?? now_ms]))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The lanes idle for the whole window as of the read at `read_ms`; none before any read.
|
|
20
|
+
function stopped_of(since: IdleSince, read_ms: number | undefined): Array<string> {
|
|
21
|
+
if (read_ms === undefined) return []
|
|
22
|
+
|
|
23
|
+
const lasted = [...since].filter(([, at]) => read_ms - at >= lane_await.RECONFIRM_MS)
|
|
24
|
+
|
|
25
|
+
return lasted.map(([lane]) => lane)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const run_board_idle = { NO_IDLE, since_of, stopped_of }
|
|
29
|
+
|
|
30
|
+
export type { IdleSince }
|
|
31
|
+
export { run_board_idle }
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { NamedPlan } from '#scripts/backlog/backlog-plan'
|
|
2
|
+
import { backlog_ready } from '#scripts/backlog/backlog-ready'
|
|
2
3
|
import { backlogrun_launch } from '#scripts/backlog/backlogrun-launch'
|
|
3
|
-
import { lane_capacity } from '#scripts/lane/lane-capacity'
|
|
4
4
|
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
5
|
+
import { lane_spent } from '#scripts/lane/lane-spent'
|
|
5
6
|
import { run_carry } from '#scripts/run/carry/run-carry'
|
|
6
7
|
import { run_carry_ended } from '#scripts/run/carry/run-carry-ended'
|
|
7
8
|
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
@@ -33,21 +34,16 @@ interface LocalRead {
|
|
|
33
34
|
is_stranded: boolean
|
|
34
35
|
// The newest operation of each session of the run, read off the end of its log.
|
|
35
36
|
operations?: SessionOperations | undefined
|
|
36
|
-
//
|
|
37
|
-
//
|
|
37
|
+
// The open lanes no child is working in at this read (`lane-spent.ts`); one is drawn stopped once it
|
|
38
|
+
// has stayed so (`run-board-idle.ts`).
|
|
39
|
+
idle_lanes?: ReadonlyArray<string> | undefined
|
|
40
|
+
// How many more lanes the limit allows beside the live ones, as `lane:limit` counts them;
|
|
41
|
+
// `undefined` where the limit could not be read, so nothing is said about it.
|
|
38
42
|
free_lanes?: number | undefined
|
|
39
43
|
// How long each phase usually takes, from the repository's lane ledger.
|
|
40
44
|
usual?: UsualTimes | undefined
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
async function free_lanes_of(lanes: ReadonlyArray<LaneInfo>): Promise<number | undefined> {
|
|
44
|
-
const choice = await lane_capacity.lane_limit()
|
|
45
|
-
|
|
46
|
-
if (choice.kind !== 'limit') return undefined
|
|
47
|
-
|
|
48
|
-
return lane_capacity.free_lanes(choice.limit, lane_registry.live_count(lanes))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
const NO_SPAN: SpanHere = { span: undefined, resume: undefined, transcript: undefined }
|
|
52
48
|
|
|
53
49
|
interface SpanHere {
|
|
@@ -104,7 +100,8 @@ async function read_local(): Promise<LocalRead | undefined> {
|
|
|
104
100
|
resume,
|
|
105
101
|
is_stranded: run_stranded_detect.is_stranded_at(repository),
|
|
106
102
|
operations: run_board_operation_read.read(sources_of(repository, lanes, transcript)),
|
|
107
|
-
|
|
103
|
+
idle_lanes: lane_spent.idle_lanes(lanes),
|
|
104
|
+
free_lanes: await backlog_ready.free_lanes_beside(lanes),
|
|
108
105
|
usual: run_board_usual.read(repository),
|
|
109
106
|
}
|
|
110
107
|
}
|
|
@@ -5,6 +5,7 @@ import { run_event_setup } from '#scripts/run/event/run-event-setup'
|
|
|
5
5
|
import type { RunEvent } from '#scripts/run/event/run-event-stream'
|
|
6
6
|
import { run_board_layout, type Overlay, type ReasonOf, type RowReason } from './run-board-layout'
|
|
7
7
|
import { run_board_setup } from './run-board-setup'
|
|
8
|
+
import type { ItemStatus } from './run-board-status'
|
|
8
9
|
|
|
9
10
|
// Why each waiting row waits — pure, so the words are tested apart from the screen. A row whose lane is
|
|
10
11
|
// being opened says the stage that lane is at; any other says what `backlog:next` would answer for it,
|
|
@@ -63,10 +64,13 @@ function is_untriaged_candidate(child: EpicChild, read: ReasonRead): boolean {
|
|
|
63
64
|
return !epic_triage.is_triaged(child) && !run_board_layout.is_blocked(child, read)
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// A child whose lane stands with no one in it still holds the paths it declared, as its open lane does.
|
|
68
|
+
function holds_paths(status: ItemStatus): boolean {
|
|
69
|
+
return status.state === 'running' || status.lane !== undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
function running_of(read: ReasonRead): Array<number> {
|
|
67
|
-
return [...read.statuses]
|
|
68
|
-
.filter(([, status]) => status.state === 'running')
|
|
69
|
-
.map(([issue]) => issue)
|
|
73
|
+
return [...read.statuses].filter(([, status]) => holds_paths(status)).map(([issue]) => issue)
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
function pool_of(read: ReasonRead): Pool {
|