@joshuafolkken/kit 1.1455.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-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/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/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 +6 -5
- package/dist/CLAUDE.md +6 -5
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-7WQBPLUN.js → chunk-43ZHT22P.js} +31 -15
- package/dist/hooks/{chunk-LIAFCXL7.js → chunk-AS74OU3D.js} +23 -7
- 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-WNLVGT7E.js → chunk-H5HVWYXK.js} +4 -4
- package/dist/hooks/{chunk-HHI6FO3R.js → chunk-IIGFYXKC.js} +345 -240
- package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-J47HU5H5.js} +14 -3
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-K4EOONYM.js} +1 -1
- package/dist/hooks/{chunk-D5QBTOCZ.js → chunk-K4LMBQC2.js} +4 -4
- package/dist/hooks/{chunk-TBJFTSUY.js → chunk-KC5AK2GF.js} +8 -6
- package/dist/hooks/chunk-SHGPANFB.js +56 -0
- package/dist/hooks/{chunk-5H4SSDSD.js → chunk-YHBZAJSV.js} +3 -3
- package/dist/hooks/{chunk-HFGWYGUV.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 +216 -214
- 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 +18 -14
- package/dist/josh.js +5 -6
- 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 +10 -20
- 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.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/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/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 +4 -6
- package/scripts/rules/shell-segments.ts +7 -0
- package/scripts/rules/stop-rules.ts +16 -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/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 +1 -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
|
@@ -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 {
|
|
@@ -4,6 +4,7 @@ import type { ClosedAnswer, ClosedIssue } from './run-board-closed'
|
|
|
4
4
|
import type { Fetch } from './run-board-fetch'
|
|
5
5
|
import type { TerminalSize } from './run-board-fit'
|
|
6
6
|
import type { BoardForm } from './run-board-header'
|
|
7
|
+
import { run_board_idle, type IdleSince } from './run-board-idle'
|
|
7
8
|
import type { BoardPlan } from './run-board-layout'
|
|
8
9
|
import type { Link } from './run-board-link'
|
|
9
10
|
import type { MachineGauges, MachineMark } from './run-board-machine'
|
|
@@ -56,6 +57,8 @@ interface BoardState {
|
|
|
56
57
|
// The last local read, drawn until the next is due; `undefined` when no run had started.
|
|
57
58
|
local: LocalRead | undefined
|
|
58
59
|
read_ms: number | undefined
|
|
60
|
+
// When each lane the reads found idle was first read so (`run-board-idle.ts`).
|
|
61
|
+
idle_since: IdleSince
|
|
59
62
|
// The start of the run the plan state was built for, so a run that begins draws nothing of the last.
|
|
60
63
|
run_started_ms: number | undefined
|
|
61
64
|
plan: BoardPlan | undefined
|
|
@@ -91,6 +94,7 @@ interface BoardState {
|
|
|
91
94
|
const FRESH_STATE: BoardState = {
|
|
92
95
|
local: undefined,
|
|
93
96
|
read_ms: undefined,
|
|
97
|
+
idle_since: run_board_idle.NO_IDLE,
|
|
94
98
|
run_started_ms: undefined,
|
|
95
99
|
plan: undefined,
|
|
96
100
|
plan_fetch: undefined,
|
|
@@ -66,6 +66,9 @@ const ENDING_STATES: Readonly<Partial<Record<string, ItemState>>> = {
|
|
|
66
66
|
const CONTINUING_KINDS: ReadonlySet<string> = new Set([KIND.CUT, KIND.RESUME])
|
|
67
67
|
// A person's restart starts the child over in a fresh session, so its track starts over too.
|
|
68
68
|
const LAUNCH_KINDS: ReadonlySet<string> = new Set([KIND.CHILD_LAUNCH, KIND.RESTART])
|
|
69
|
+
// What the GitHub read may still settle: a running child, and one whose lane has no child left in it —
|
|
70
|
+
// a child that merged and then died is drawn merged, not stopped.
|
|
71
|
+
const UNSETTLED_STATES: ReadonlySet<ItemState> = new Set(['running', 'stopped'])
|
|
69
72
|
|
|
70
73
|
function moment(event: RunEvent): number {
|
|
71
74
|
return Date.parse(event.at)
|
|
@@ -175,13 +178,25 @@ function is_running_lane(
|
|
|
175
178
|
return in_run.has(issue) && !run_board_setup.is_lane_pending(events, issue)
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
// A lane no child is working in is stopped whatever the stream last said: a child that died writes no
|
|
182
|
+
// ending, so its row would otherwise run — and its phase time grow — for as long as the lane stays.
|
|
183
|
+
function lane_status(status: ItemStatus | undefined, lane: string, is_idle: boolean): ItemStatus {
|
|
184
|
+
const held = { lane: `lane ${lane}` }
|
|
185
|
+
|
|
186
|
+
return is_idle
|
|
187
|
+
? { ...settled_as(status, 'stopped'), ...held }
|
|
188
|
+
: { state: 'running', ...status, ...held }
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
// Every child the run has touched, with the lane each running child holds. A lane the stream never saw
|
|
179
192
|
// launched — a run restored after the stream rolled — still reads as running while its issue is in the
|
|
180
|
-
// run's plan (`in_run`); one the run is still setting up for stays waiting until its launch.
|
|
193
|
+
// run's plan (`in_run`); one the run is still setting up for stays waiting until its launch. A lane in
|
|
194
|
+
// `idle` has no child working in it (`lane-spent.ts`).
|
|
181
195
|
function statuses_of(
|
|
182
196
|
events: ReadonlyArray<RunEvent>,
|
|
183
197
|
lanes: ReadonlyArray<string>,
|
|
184
198
|
in_run: ReadonlySet<number>,
|
|
199
|
+
idle: ReadonlyArray<string> = [],
|
|
185
200
|
): ReadonlyMap<number, ItemStatus> {
|
|
186
201
|
const statuses = fold_events(events)
|
|
187
202
|
|
|
@@ -190,7 +205,7 @@ function statuses_of(
|
|
|
190
205
|
const status = statuses.get(issue)
|
|
191
206
|
|
|
192
207
|
if (is_running_lane(issue, status, events, in_run)) {
|
|
193
|
-
statuses.set(issue,
|
|
208
|
+
statuses.set(issue, lane_status(status, lane, idle.includes(lane)))
|
|
194
209
|
}
|
|
195
210
|
}
|
|
196
211
|
|
|
@@ -260,7 +275,7 @@ function closed_status(
|
|
|
260
275
|
}
|
|
261
276
|
|
|
262
277
|
function settled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus {
|
|
263
|
-
if (status.state
|
|
278
|
+
if (!UNSETTLED_STATES.has(status.state)) return status
|
|
264
279
|
|
|
265
280
|
const from_closed = closed_status(status, read.closed.get(issue))
|
|
266
281
|
|
|
@@ -17,8 +17,13 @@ type ThanksState = Pick<BoardState, 'thanks' | 'thanks_fetch' | 'thanks_attempte
|
|
|
17
17
|
function launch_thanks(state: BoardState, ports: BoardPorts, now_ms: number): BoardState {
|
|
18
18
|
const { read_thanks } = ports
|
|
19
19
|
|
|
20
|
-
if (
|
|
21
|
-
|
|
20
|
+
if (
|
|
21
|
+
read_thanks === undefined ||
|
|
22
|
+
state.thanks_fetch !== undefined ||
|
|
23
|
+
!is_due(state.thanks_attempted_ms, TTL_MS, now_ms)
|
|
24
|
+
) {
|
|
25
|
+
return state
|
|
26
|
+
}
|
|
22
27
|
|
|
23
28
|
const thanks_fetch: Fetch<Thanks> = run_board_fetch.launch(read_thanks, state.thanks)
|
|
24
29
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { lane_await } from '#scripts/lane/lane-await'
|
|
1
2
|
import { run_progress } from '#scripts/run/progress/run-progress'
|
|
2
3
|
import type { ClosedIssue } from './run-board-closed'
|
|
3
4
|
import { run_board_github } from './run-board-github'
|
|
4
5
|
import { run_board_header, type BoardHeader } from './run-board-header'
|
|
6
|
+
import { run_board_idle } from './run-board-idle'
|
|
5
7
|
import { run_board_labels } from './run-board-labels'
|
|
6
8
|
import { run_board_layout, type BoardLayout, type BoardPlan } from './run-board-layout'
|
|
7
9
|
import { run_board_link } from './run-board-link'
|
|
@@ -34,10 +36,30 @@ const { FRESH_STATE, is_due } = run_board_state
|
|
|
34
36
|
const { PLAN_RETRY_MS, gather_github, statuses_for } = run_board_github
|
|
35
37
|
const { gather_thanks } = run_board_thanks
|
|
36
38
|
|
|
37
|
-
async function
|
|
39
|
+
async function read_at(state: BoardState, ports: BoardPorts, now_ms: number): Promise<BoardState> {
|
|
40
|
+
const local = await ports.read_local()
|
|
41
|
+
const idle_since = run_board_idle.since_of(state.idle_since, local?.idle_lanes ?? [], now_ms)
|
|
42
|
+
|
|
43
|
+
return { ...state, local, read_ms: now_ms, idle_since }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A single frame has no earlier read to confirm an idle lane against (`run-board-idle.ts`).
|
|
47
|
+
function is_unconfirmed(before: BoardState, read: BoardState, pace: Pace): boolean {
|
|
48
|
+
return pace === 'settled' && before.read_ms === undefined && read.idle_since.size > 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function reread(state: BoardState, ports: BoardPorts, pace: Pace): Promise<BoardState> {
|
|
52
|
+
const now_ms = ports.now()
|
|
53
|
+
|
|
38
54
|
if (!is_due(state.read_ms, LOCAL_READ_MS, now_ms)) return state
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
const read = await read_at(state, ports, now_ms)
|
|
57
|
+
|
|
58
|
+
if (!is_unconfirmed(state, read, pace)) return read
|
|
59
|
+
|
|
60
|
+
await ports.sleep(lane_await.RECONFIRM_MS)
|
|
61
|
+
|
|
62
|
+
return await read_at(read, ports, ports.now())
|
|
41
63
|
}
|
|
42
64
|
|
|
43
65
|
// Each lane's usage, read on the machine's second; a reading that failed draws no column.
|
|
@@ -69,9 +91,9 @@ async function resample(state: BoardState, ports: BoardPorts, now_ms: number): P
|
|
|
69
91
|
function state_for(state: BoardState, local: LocalRead): BoardState {
|
|
70
92
|
if (state.run_started_ms === local.started_ms) return state
|
|
71
93
|
|
|
72
|
-
const { local: kept, read_ms } = state
|
|
94
|
+
const { local: kept, read_ms, idle_since } = state
|
|
73
95
|
|
|
74
|
-
return { ...FRESH_STATE, local: kept, read_ms, run_started_ms: local.started_ms }
|
|
96
|
+
return { ...FRESH_STATE, local: kept, read_ms, idle_since, run_started_ms: local.started_ms }
|
|
75
97
|
}
|
|
76
98
|
|
|
77
99
|
// The plan with a closed child's title and labels under the listing's own.
|
|
@@ -108,7 +130,7 @@ function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): B
|
|
|
108
130
|
closed,
|
|
109
131
|
labels: plan.labels,
|
|
110
132
|
}
|
|
111
|
-
const statuses = run_board_status.settle_closed(statuses_for(plan, local), read)
|
|
133
|
+
const statuses = run_board_status.settle_closed(statuses_for(state, plan, local), read)
|
|
112
134
|
const reason_of = run_board_reason.reader({
|
|
113
135
|
plan,
|
|
114
136
|
statuses,
|
|
@@ -125,8 +147,10 @@ function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): B
|
|
|
125
147
|
function layout_for(state: BoardState, local: LocalRead): BoardLayout {
|
|
126
148
|
if (state.plan !== undefined) return planned_layout(state, state.plan, local)
|
|
127
149
|
|
|
150
|
+
const stopped = run_board_idle.stopped_of(state.idle_since, state.read_ms)
|
|
151
|
+
|
|
128
152
|
return run_board_layout.local_layout_of(
|
|
129
|
-
run_board_status.statuses_of(local.events, local.lanes, new Set()),
|
|
153
|
+
run_board_status.statuses_of(local.events, local.lanes, new Set(), stopped),
|
|
130
154
|
)
|
|
131
155
|
}
|
|
132
156
|
|
|
@@ -220,8 +244,9 @@ async function tick(
|
|
|
220
244
|
ports: BoardPorts,
|
|
221
245
|
pace: Pace = 'settled',
|
|
222
246
|
): Promise<BoardState> {
|
|
247
|
+
const read = await reread(state, ports, pace)
|
|
248
|
+
// Taken after the read, which a single frame may have waited on to confirm an idle lane.
|
|
223
249
|
const now_ms = ports.now()
|
|
224
|
-
const read = await reread(state, ports, now_ms)
|
|
225
250
|
const { local } = read
|
|
226
251
|
|
|
227
252
|
if (local === undefined) {
|
|
@@ -269,9 +269,7 @@ function to_named_request(values: ParsedValues, owner: CarryOwner): Request | un
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
function to_request(values: ParsedValues): Request | undefined {
|
|
272
|
-
if (group_count(values) > ONE_GROUP) return undefined
|
|
273
|
-
|
|
274
|
-
if (!is_summary_paired(values)) return undefined
|
|
272
|
+
if (group_count(values) > ONE_GROUP || !is_summary_paired(values)) return undefined
|
|
275
273
|
|
|
276
274
|
const owner = to_owner(values.owner)
|
|
277
275
|
|
|
@@ -76,8 +76,13 @@ function is_same_conversation(carry: RunCarry, owner: CarryOwner): boolean {
|
|
|
76
76
|
// a new one, so the record goes on naming a live process once the conversation falls quiet. A record a
|
|
77
77
|
// cut handed off is the successor's to take, never the cutting conversation's to take back.
|
|
78
78
|
function reclaim_owner(carry: RunCarry, owner: CarryOwner): RunCarry {
|
|
79
|
-
if (
|
|
80
|
-
|
|
79
|
+
if (
|
|
80
|
+
carry.is_handed_off === true ||
|
|
81
|
+
!is_same_conversation(carry, owner) ||
|
|
82
|
+
owner.pid === carry.owner_pid
|
|
83
|
+
) {
|
|
84
|
+
return carry
|
|
85
|
+
}
|
|
81
86
|
|
|
82
87
|
return { ...carry, owner_pid: owner.pid, owner_start: owner.start }
|
|
83
88
|
}
|
|
@@ -3,7 +3,6 @@ import { fileURLToPath } from 'node:url'
|
|
|
3
3
|
import { agent_session_role } from '#scripts/agent/agent-session-role'
|
|
4
4
|
import { cost_cli } from '#scripts/cost-runtime/cost-cli'
|
|
5
5
|
import { cost_verdict } from '#scripts/cost-runtime/cost-verdict'
|
|
6
|
-
import { git_command } from '#scripts/git/git-command'
|
|
7
6
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
8
7
|
import { session_cite } from '#scripts/issue/session-cite'
|
|
9
8
|
import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
|
|
@@ -12,9 +11,11 @@ import { openai_lane_supervisor } from '#scripts/lane/openai-lane-supervisor'
|
|
|
12
11
|
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
13
12
|
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
14
13
|
import { run_cli_fault } from '#scripts/run/run-cli-fault'
|
|
15
|
-
import { run_cut, type
|
|
14
|
+
import { run_cut, type RunCut } from './run-cut'
|
|
16
15
|
import { run_cut_args, type Request } from './run-cut-args'
|
|
16
|
+
import { run_cut_decline } from './run-cut-decline'
|
|
17
17
|
import { run_cut_handoff } from './run-cut-handoff'
|
|
18
|
+
import { run_cut_ready } from './run-cut-ready'
|
|
18
19
|
import { run_cut_report } from './run-cut-report'
|
|
19
20
|
|
|
20
21
|
// `josh run:cut` — the record that lets a lane child end its process before the gate and a fresh one
|
|
@@ -57,6 +58,7 @@ const {
|
|
|
57
58
|
report_busy,
|
|
58
59
|
report_fault,
|
|
59
60
|
report_handed_off,
|
|
61
|
+
report_held_cut,
|
|
60
62
|
report_incomplete,
|
|
61
63
|
report_over,
|
|
62
64
|
report_reviewer,
|
|
@@ -125,11 +127,8 @@ function relaunch(target: string, lane: LaneInfo, phase: string): number {
|
|
|
125
127
|
return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return state.branch !== default_branch && state.is_dirty
|
|
132
|
-
}
|
|
130
|
+
const { is_lane_branch } = run_cut_ready
|
|
131
|
+
const { declined } = run_cut_decline
|
|
133
132
|
|
|
134
133
|
// An expired record is a fresh process that never started; it is cleared so the next cut can begin
|
|
135
134
|
// rather than being wedged behind a stale file the exclusive create would refuse forever.
|
|
@@ -204,16 +203,6 @@ async function finish_cut(target: string, lane: LaneInfo, request: CutRequest):
|
|
|
204
203
|
: relaunch(target, lane, request.phase)
|
|
205
204
|
}
|
|
206
205
|
|
|
207
|
-
// The cut outside a lane is taken, and nothing is relaunched: the session that took it is the one a
|
|
208
|
-
// person is watching, so the run is handed on the way `fullrun`'s entry `over` stop hands it on.
|
|
209
|
-
function report_held_cut(issue: string): number {
|
|
210
|
-
console.error(
|
|
211
|
-
`The cut is recorded for ${session_cite.issue(issue)} outside a lane, so nothing was relaunched. Keep the hold, send a \`confirmation\` Telegram whose body names the resume command \`fullrun ${issue_cite.plain(issue)}\`, and end the turn; the fresh session's \`pnpm josh run:cut --resume ${issue}\` answers \`resume-impl\` with the handoff.`,
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
206
|
// **A `fullrun` held in its own checkout takes the implementation cut too**, so a run a person started
|
|
218
207
|
// has a bound on its context mid-implementation. The hold naming this issue is what marks the run — a
|
|
219
208
|
// tree nobody holds for it, and the pre-gate phase (which a held run's gate reaches in the same
|
|
@@ -228,8 +217,11 @@ async function cut_outside_lane(
|
|
|
228
217
|
|
|
229
218
|
const state = await run_cut.current_state()
|
|
230
219
|
|
|
231
|
-
if (state.held_issue !== request.issue)
|
|
232
|
-
|
|
220
|
+
if (state.held_issue !== request.issue) {
|
|
221
|
+
return await declined(request, state, report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!(await is_lane_branch(state))) return await declined(request, state, refuse_unready(state))
|
|
233
225
|
|
|
234
226
|
clear_expired(target)
|
|
235
227
|
|
|
@@ -250,7 +242,10 @@ async function cut(
|
|
|
250
242
|
|
|
251
243
|
const state = await run_cut.current_state()
|
|
252
244
|
|
|
253
|
-
if (!(await is_lane_branch(state)))
|
|
245
|
+
if (!(await is_lane_branch(state))) {
|
|
246
|
+
return await declined({ issue, phase }, state, refuse_unready(state))
|
|
247
|
+
}
|
|
248
|
+
|
|
254
249
|
if (skips_pre_gate_cut(phase)) return report_under_threshold()
|
|
255
250
|
|
|
256
251
|
clear_expired(target)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
2
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { run_cut, type CutState } from './run-cut'
|
|
5
|
+
|
|
6
|
+
// The implementation cut a run asked for and was refused — `not-a-lane` or `unready` — recorded so the
|
|
7
|
+
// implementation-cut guard stops asking for it (joshuafolkken/kit#3881).
|
|
8
|
+
//
|
|
9
|
+
// **The reissue window alone re-asked every 90 seconds.** A run whose `run:cut --impl` can only answer
|
|
10
|
+
// `unready` was refused edit after edit, each refusal a round trip that cut nothing. Neither answer
|
|
11
|
+
// changes while the same hold stands over the tree, so the record names the issue and the hold it
|
|
12
|
+
// answered, and the guard stays silent while both still match; a fresh hold — a new run — asks again.
|
|
13
|
+
|
|
14
|
+
const DECLINE_PREFIX = 'josh-run-cut-decline-'
|
|
15
|
+
|
|
16
|
+
const decline_schema = z.object({ issue: z.string(), hold_taken_at: z.string().optional() })
|
|
17
|
+
|
|
18
|
+
type CutDecline = z.infer<typeof decline_schema>
|
|
19
|
+
|
|
20
|
+
function decline_path(git_directory: string): string {
|
|
21
|
+
return stamp_file.stamp_path(DECLINE_PREFIX, git_directory)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function record(git_directory: string, decline: CutDecline): void {
|
|
25
|
+
stamp_file.replace_stamp(decline_path(git_directory), decline)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function read(git_directory: string): CutDecline | undefined {
|
|
29
|
+
const raw = stamp_file.read_stamp_text(decline_path(git_directory))
|
|
30
|
+
|
|
31
|
+
return raw === undefined ? undefined : json_value.parse_with(raw, decline_schema)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function matches(recorded: CutDecline | undefined, current: CutDecline): boolean {
|
|
35
|
+
return recorded?.issue === current.issue && recorded.hold_taken_at === current.hold_taken_at
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The same read taken synchronously, for the `PreToolUse` guard. A checkout git cannot describe has no
|
|
39
|
+
// record, so the guard keeps speaking there.
|
|
40
|
+
function is_declined_sync(
|
|
41
|
+
issue: string,
|
|
42
|
+
directory: string | undefined = run_cut.worktree_git_directory_sync(),
|
|
43
|
+
): boolean {
|
|
44
|
+
if (directory === undefined) return false
|
|
45
|
+
|
|
46
|
+
const hold_taken_at = run_cut.hold_of(directory)?.taken_at
|
|
47
|
+
|
|
48
|
+
return matches(read(directory), { issue, hold_taken_at })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// **An implementation cut `run:cut` refuses is recorded for the guard that asked for it**, then its
|
|
52
|
+
// exit code is passed back unchanged. The pre-gate phase has its own guard and records nothing.
|
|
53
|
+
async function declined(
|
|
54
|
+
request: { issue: string; phase: string },
|
|
55
|
+
state: CutState,
|
|
56
|
+
code: number,
|
|
57
|
+
): Promise<number> {
|
|
58
|
+
if (request.phase !== run_cut.IMPLEMENTATION_PHASE) return code
|
|
59
|
+
|
|
60
|
+
const directory = await run_cut.worktree_directory()
|
|
61
|
+
|
|
62
|
+
if (directory !== undefined) {
|
|
63
|
+
record(directory, { issue: request.issue, hold_taken_at: state.hold_taken_at })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return code
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const run_cut_decline = { decline_path, declined, is_declined_sync, read, record }
|
|
70
|
+
|
|
71
|
+
export type { CutDecline }
|
|
72
|
+
export { run_cut_decline }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { UNTRACKED_FILES_FLAG } from '#scripts/git/constants'
|
|
2
|
+
import { git_command } from '#scripts/git/git-command'
|
|
3
|
+
import { git_diff_reads } from '#scripts/git/git-diff-reads'
|
|
4
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
5
|
+
import type { CutState } from './run-cut'
|
|
6
|
+
|
|
7
|
+
// Whether a cut can be taken on this tree — the one predicate `run:cut` refuses `unready` on and the
|
|
8
|
+
// implementation-cut guard asks before it refuses an edit (joshuafolkken/kit#3881).
|
|
9
|
+
//
|
|
10
|
+
// **The guard and the command must not disagree.** The guard once fired on every over-threshold edit
|
|
11
|
+
// of a run held on the default branch, where `run:cut --impl` can only ever answer `unready`; each
|
|
12
|
+
// refusal cost a round trip that cut nothing. Reading the same predicate keeps the guard silent
|
|
13
|
+
// exactly where the command would refuse.
|
|
14
|
+
|
|
15
|
+
interface CutTree {
|
|
16
|
+
branch: string
|
|
17
|
+
default_branch: string
|
|
18
|
+
is_dirty: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A cut needs an uncommitted implementation on a branch other than the default one: the resume
|
|
22
|
+
// verifies both, so a tree missing either has nothing a fresh process could resume.
|
|
23
|
+
function is_ready(tree: CutTree): boolean {
|
|
24
|
+
return tree.branch !== tree.default_branch && tree.is_dirty
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The same predicate over the state `run:cut` reads, against the default branch asked asynchronously.
|
|
28
|
+
async function is_lane_branch(state: CutState): Promise<boolean> {
|
|
29
|
+
const default_branch = await git_command.get_default_branch()
|
|
30
|
+
|
|
31
|
+
return is_ready({ branch: state.branch, default_branch, is_dirty: state.is_dirty })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// `--branch` puts the branch on a `# branch.head` header line, so one call answers both halves; every
|
|
35
|
+
// other line is a change.
|
|
36
|
+
const STATUS_ARGUMENTS = ['status', '--porcelain=v2', '--branch', UNTRACKED_FILES_FLAG] as const
|
|
37
|
+
const HEADER_PREFIX = '#'
|
|
38
|
+
const BRANCH_HEADER = '# branch.head '
|
|
39
|
+
|
|
40
|
+
function tree_of(status: string, default_branch: string): CutTree | undefined {
|
|
41
|
+
const lines = status.split('\n').filter((line) => line !== '')
|
|
42
|
+
const header = lines.find((line) => line.startsWith(BRANCH_HEADER))
|
|
43
|
+
|
|
44
|
+
if (header === undefined) return undefined
|
|
45
|
+
|
|
46
|
+
const branch = header.slice(BRANCH_HEADER.length)
|
|
47
|
+
const is_dirty = lines.some((line) => !line.startsWith(HEADER_PREFIX))
|
|
48
|
+
|
|
49
|
+
return { branch, default_branch, is_dirty }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The tree read synchronously, for the `PreToolUse` guard — `undefined` where git cannot answer.
|
|
53
|
+
function read_tree_sync(): CutTree | undefined {
|
|
54
|
+
const status = git_spawn_sync.read(STATUS_ARGUMENTS)
|
|
55
|
+
|
|
56
|
+
if (status === undefined) return undefined
|
|
57
|
+
|
|
58
|
+
const reference = git_spawn_sync.read(git_diff_reads.DEFAULT_BRANCH_ARGUMENTS)
|
|
59
|
+
|
|
60
|
+
return tree_of(status, git_diff_reads.default_branch_of(reference))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// **A tree git cannot describe reads as ready**, so the guard keeps speaking rather than going quiet
|
|
64
|
+
// on a read fault — the same direction `run_cut.carried_cut_sync` takes for an unreadable record.
|
|
65
|
+
function is_ready_sync(read: () => CutTree | undefined = read_tree_sync): boolean {
|
|
66
|
+
const tree = read()
|
|
67
|
+
|
|
68
|
+
return tree === undefined || is_ready(tree)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const run_cut_ready = { is_lane_branch, is_ready, is_ready_sync, tree_of }
|
|
72
|
+
|
|
73
|
+
export type { CutTree }
|
|
74
|
+
export { run_cut_ready }
|