@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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
1
3
|
import { run_cut, type CutState, type RunCut } from './run-cut'
|
|
2
4
|
import { run_cut_handoff } from './run-cut-handoff'
|
|
3
5
|
|
|
@@ -136,6 +138,16 @@ function report_reviewer(): number {
|
|
|
136
138
|
return report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE)
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
// The cut outside a lane is taken, and nothing is relaunched: the session that took it is the one a
|
|
142
|
+
// person is watching, so the run is handed on the way `fullrun`'s entry `over` stop hands it on.
|
|
143
|
+
function report_held_cut(issue: string): number {
|
|
144
|
+
console.error(
|
|
145
|
+
`The cut is recorded for ${session_cite.issue(issue)} outside a lane, so nothing was relaunched. Keep the hold, send a \`confirmation\` Telegram whose body names the resume command \`fullrun ${issue_cite.plain(issue)}\`, and end the turn; the fresh session's \`pnpm josh run:cut --resume ${issue}\` answers \`resume-impl\` with the handoff.`,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
// **An implementation cut is never resumed by a session over the threshold**.
|
|
140
152
|
// The cut outside a lane leaves the session that took it open, so a person retyping `fullrun #N` there
|
|
141
153
|
// would be answered `resume-impl`, keep implementing over the threshold, and be cut again on the next
|
|
@@ -176,6 +188,7 @@ const run_cut_report = {
|
|
|
176
188
|
report_busy,
|
|
177
189
|
report_fault,
|
|
178
190
|
report_handed_off,
|
|
191
|
+
report_held_cut,
|
|
179
192
|
report_incomplete,
|
|
180
193
|
report_over,
|
|
181
194
|
report_reviewer,
|
|
@@ -106,6 +106,8 @@ interface CutState {
|
|
|
106
106
|
is_held: boolean
|
|
107
107
|
// The issue a `fullrun` hold names, absent when the tree is not held by one.
|
|
108
108
|
held_issue?: string | undefined
|
|
109
|
+
// When the hold standing over the tree was taken — what keys a declined cut to the run it answered.
|
|
110
|
+
hold_taken_at?: string | undefined
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
// The fields a cut is written from, bundled so `begin_cut` stays within the parameter limit. `phase`
|
|
@@ -402,7 +404,13 @@ async function current_state(): Promise<CutState> {
|
|
|
402
404
|
const is_dirty = await run_hold.is_tree_dirty()
|
|
403
405
|
const hold = hold_of(await worktree_directory())
|
|
404
406
|
|
|
405
|
-
return {
|
|
407
|
+
return {
|
|
408
|
+
branch,
|
|
409
|
+
is_dirty,
|
|
410
|
+
is_held: hold !== undefined,
|
|
411
|
+
held_issue: fullrun_issue(hold),
|
|
412
|
+
hold_taken_at: hold?.taken_at,
|
|
413
|
+
}
|
|
406
414
|
}
|
|
407
415
|
|
|
408
416
|
function describe_cut(cut: RunCut): string {
|
|
@@ -466,6 +474,7 @@ const run_cut = {
|
|
|
466
474
|
handed_off_message,
|
|
467
475
|
has_required_handoff,
|
|
468
476
|
held_issue_sync,
|
|
477
|
+
hold_of,
|
|
469
478
|
incomplete_message,
|
|
470
479
|
invocation_for,
|
|
471
480
|
is_expired,
|
|
@@ -176,14 +176,25 @@ function held_block(hold: RunHold, issue: string): string | undefined {
|
|
|
176
176
|
return run_hold.is_reentry(hold, issue) ? undefined : run_hold.held_message(hold)
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// An expired record of the claimant's own issue that no stop marked is the earlier run of the same work —
|
|
180
|
+
// a child killed and re-dispatched into its lane — so the claim adopts it with the work it left. A
|
|
181
|
+
// stopped `halfrun` or `prrun` keeps its own adopt path.
|
|
182
|
+
function is_own_unstopped(hold: RunHold, issue: string): boolean {
|
|
183
|
+
return run_hold.is_reentry(hold, issue) && !run_hold.is_stop_marked(hold)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function stale_block(hold: RunHold, issue: string): Promise<string | undefined> {
|
|
187
|
+
if (is_own_unstopped(hold, issue)) return undefined
|
|
188
|
+
|
|
189
|
+
return (await run_hold.is_tree_dirty()) ? run_hold.uncommitted_message(hold) : undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
179
192
|
async function blocking_message(read: HoldRead, issue: string): Promise<string | undefined> {
|
|
180
193
|
if (read.kind === 'held') return held_block(read.hold, issue)
|
|
181
194
|
|
|
182
195
|
if (read.kind === 'unreadable') return run_hold.unreadable_message()
|
|
183
196
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return is_expired_over_work ? run_hold.uncommitted_message(read.hold) : undefined
|
|
197
|
+
return read.kind === 'stale' ? await stale_block(read.hold, issue) : undefined
|
|
187
198
|
}
|
|
188
199
|
|
|
189
200
|
// A record already here is never overwritten by a claim: overwriting is what the guard exists to
|
|
@@ -129,10 +129,12 @@ function is_expired(hold: RunHold, now: Date): boolean {
|
|
|
129
129
|
// nothing. Only a recorded session that is provably gone counts — no owner, or a live pid whose start
|
|
130
130
|
// cannot be read, leaves the age as the whole test. **A stop-marked record is exempt**: a `halfrun` or
|
|
131
131
|
// `prrun` stop is held across a person's latency on purpose, long after its session ends.
|
|
132
|
-
function
|
|
133
|
-
|
|
132
|
+
function is_stop_marked(hold: RunHold): boolean {
|
|
133
|
+
return hold.is_halfrun_stop === true || hold.prrun_stop_head !== undefined
|
|
134
|
+
}
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
function is_owner_gone(hold: RunHold): boolean {
|
|
137
|
+
if (is_stop_marked(hold) || hold.owner_pid === undefined) return false
|
|
136
138
|
|
|
137
139
|
return process_identity.is_same_process(hold.owner_pid, hold.owner_start) === false
|
|
138
140
|
}
|
|
@@ -317,6 +319,7 @@ function unknown_message(): string {
|
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
const run_hold = {
|
|
322
|
+
is_stop_marked,
|
|
320
323
|
FORCE_RELEASE_COMMAND,
|
|
321
324
|
HOLD_MAX_AGE_HOURS,
|
|
322
325
|
HOLD_MAX_AGE_MS,
|
|
@@ -10,11 +10,13 @@ import { lane_handoff } from '#scripts/lane/lane-handoff'
|
|
|
10
10
|
import { lane_ledger } from '#scripts/lane/lane-ledger'
|
|
11
11
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
12
12
|
import { run_carry, type CarryOwner, type RunCarry } from '#scripts/run/carry/run-carry'
|
|
13
|
+
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
13
14
|
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
14
15
|
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
15
16
|
import { run_ending } from '#scripts/run/run-ending'
|
|
16
17
|
import { run_label } from '#scripts/run/run-label'
|
|
17
18
|
import { run_merge, type ChildOutcome, type EndingSignals } from './run-merge'
|
|
19
|
+
import { run_merge_ship } from './run-merge-ship'
|
|
18
20
|
import { run_merge_steps, type FailedResult, type MergeContext } from './run-merge-steps'
|
|
19
21
|
import { run_merge_token, type MergeToken } from './run-merge-token'
|
|
20
22
|
|
|
@@ -189,14 +191,34 @@ async function offered(ctx: MergeContext): Promise<OfferVerdict> {
|
|
|
189
191
|
return { token: await run_merge_steps.ask_next(ctx), code: SUCCESS_EXIT_CODE, is_offer: true }
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
// Whether the child's
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
// output is simply "not an outage".
|
|
194
|
+
// Whether the child's session ended through its environment — read only when `--output` named the
|
|
195
|
+
// transcript, off otherwise: its exit record shows it could not reach the API, or the session was
|
|
196
|
+
// killed inside a turn. The path is validated inside `run_ending`, so a missing output is "no".
|
|
196
197
|
function read_is_outage(output: string | undefined): boolean {
|
|
197
198
|
if (output === undefined) return false
|
|
198
199
|
|
|
199
|
-
return api_outage.is_outage(run_ending.read_exit(output))
|
|
200
|
+
return api_outage.is_outage(run_ending.read_exit(output)) || run_ending.ended_mid_turn(output)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function child_outages(child: string): Promise<number> {
|
|
204
|
+
const events = await run_event_stream_emit.current_events()
|
|
205
|
+
|
|
206
|
+
return events.filter(
|
|
207
|
+
(event) =>
|
|
208
|
+
event.kind === run_event_stream.EVENT_KIND.OUTAGE &&
|
|
209
|
+
run_event_scope.issue_named(event) === child,
|
|
210
|
+
).length
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// An outage child is re-dispatched only so many times: one that ends the same way on every dispatch is
|
|
214
|
+
// parked, since merges in other lanes reset the run-wide outage streak and would never stop it.
|
|
215
|
+
async function read_is_retryable(child: string, output: string | undefined): Promise<boolean> {
|
|
216
|
+
const is_interrupted =
|
|
217
|
+
read_is_outage(output) || (await run_merge_ship.is_unanswered(child, output))
|
|
218
|
+
|
|
219
|
+
if (!is_interrupted) return false
|
|
220
|
+
|
|
221
|
+
return (await child_outages(child)) < run_merge.CONSECUTIVE_OUTAGE_LIMIT
|
|
200
222
|
}
|
|
201
223
|
|
|
202
224
|
type IssueRead = Awaited<ReturnType<typeof issue_state_cli.read_issue>>
|
|
@@ -211,12 +233,13 @@ async function read_merged_pr(child: string, read: IssueRead): Promise<string |
|
|
|
211
233
|
}
|
|
212
234
|
|
|
213
235
|
async function read_signals(ctx: MergeContext, read: IssueRead): Promise<EndingSignals> {
|
|
214
|
-
const [is_cut, merged_pr] = await Promise.all([
|
|
236
|
+
const [is_cut, merged_pr, is_outage] = await Promise.all([
|
|
215
237
|
run_merge_steps.has_resumable_cut(ctx.child),
|
|
216
238
|
read_merged_pr(ctx.child, read),
|
|
239
|
+
read_is_retryable(ctx.child, ctx.output),
|
|
217
240
|
])
|
|
218
241
|
|
|
219
|
-
return { is_outage
|
|
242
|
+
return { is_outage, is_cut, merged_pr }
|
|
220
243
|
}
|
|
221
244
|
|
|
222
245
|
function outcome_of(read: IssueRead, signals: EndingSignals): ChildOutcome {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
2
|
+
import { lane_registry } from '#scripts/lane/lane-registry'
|
|
3
|
+
import { lane_relaunch } from '#scripts/lane/lane-relaunch'
|
|
2
4
|
import { run_event_scope } from '#scripts/run/event/run-event-scope'
|
|
3
5
|
import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
|
|
6
|
+
import { run_ending } from '#scripts/run/run-ending'
|
|
4
7
|
import { run_ship_detach } from '#scripts/run/ship/run-ship-detach'
|
|
5
8
|
|
|
6
9
|
// Why `run:merge` parks a child whose detached ship has already ended (joshuafolkken/kit#3808).
|
|
@@ -56,6 +59,61 @@ function cause(child: string, directory: string = process.cwd()): string | undef
|
|
|
56
59
|
return cause_of(run_ship_detach.read_result(repository, child), events, child)
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
// Whether the ended ship was never answered: a session relaunched to fix a stop writes to the
|
|
63
|
+
// transcript after it, so a transcript last written before the newest stop means no fix-up ran — its
|
|
64
|
+
// broker or relaunch could not start. A ship with no stop at all ended abnormally only when its supervisor
|
|
65
|
+
// died before reaching a stage.
|
|
66
|
+
function is_stop_unanswered(
|
|
67
|
+
ending: ShipEnding,
|
|
68
|
+
attempt: Events,
|
|
69
|
+
written_at: number | undefined,
|
|
70
|
+
): boolean {
|
|
71
|
+
const stop = attempt.findLast((event) => event.kind === run_event_stream.EVENT_KIND.SHIP_STOP)
|
|
72
|
+
|
|
73
|
+
if (stop === undefined) return ending?.result === 'abnormal'
|
|
74
|
+
|
|
75
|
+
return written_at !== undefined && written_at < Date.parse(stop.at)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Whether the child's ship ended through an interruption rather than its work, so the child is
|
|
79
|
+
// re-dispatched instead of parked.
|
|
80
|
+
function interrupted_of(
|
|
81
|
+
ending: ShipEnding,
|
|
82
|
+
events: Events,
|
|
83
|
+
child: string,
|
|
84
|
+
written_at: number | undefined,
|
|
85
|
+
): boolean {
|
|
86
|
+
if (ending === undefined || !ENDED_RESULTS.has(ending.result)) return false
|
|
87
|
+
|
|
88
|
+
const attempt = since_ship_launch(events, child)
|
|
89
|
+
|
|
90
|
+
return attempt !== undefined && is_stop_unanswered(ending, attempt, written_at)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function is_interrupted(
|
|
94
|
+
child: string,
|
|
95
|
+
written_at: number | undefined,
|
|
96
|
+
directory: string = process.cwd(),
|
|
97
|
+
): boolean {
|
|
98
|
+
const repository = git_common_directory.repository(directory)
|
|
99
|
+
if (repository === undefined) return false
|
|
100
|
+
|
|
101
|
+
const events = run_event_stream.read_events(run_event_stream.target_of(repository))
|
|
102
|
+
|
|
103
|
+
return interrupted_of(run_ship_detach.read_result(repository, child), events, child, written_at)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Whether the child's ship stopped and the relaunch meant to fix it never ran, read against its
|
|
107
|
+
// transcript at `output`. An OpenAI lane is never relaunched — its parent books every stop as an ended
|
|
108
|
+
// child — and nothing relaunches a child whose lane is gone, so only an open non-OpenAI lane reads as one.
|
|
109
|
+
async function is_unanswered(child: string, output: string | undefined): Promise<boolean> {
|
|
110
|
+
if (output === undefined || !is_interrupted(child, run_ending.written_at(output))) return false
|
|
111
|
+
|
|
112
|
+
const lane = await lane_registry.find_open_lane(child)
|
|
113
|
+
|
|
114
|
+
return lane !== undefined && !lane_relaunch.is_openai_lane(lane)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const run_merge_ship = { cause, cause_of, interrupted_of, is_unanswered }
|
|
60
118
|
|
|
61
119
|
export { run_merge_ship }
|
|
@@ -23,8 +23,9 @@ const CLOSED_STATE = 'CLOSED'
|
|
|
23
23
|
// resets the streak (`run-carry.ts` → `next_failures`), so the guard trips only on an environment
|
|
24
24
|
// failing every child it is handed rather than on failures scattered across a long run.
|
|
25
25
|
const CONSECUTIVE_FAILURE_LIMIT = 3
|
|
26
|
-
// Three
|
|
27
|
-
// guard, over its own streak. An outage is a child
|
|
26
|
+
// Three outages in a row stop the run too — the same shape as the failure
|
|
27
|
+
// guard, over its own streak. An outage is a child its environment ended — it could not reach the API,
|
|
28
|
+
// its session was killed inside a turn, or its ship stopped with no relaunch — so a run of them is
|
|
28
29
|
// the environment being down; stopping is what keeps the run from re-dispatching into a dead API
|
|
29
30
|
// forever. A merge or a genuine child failure resets it (`run-carry.ts` → `next_outages`).
|
|
30
31
|
const CONSECUTIVE_OUTAGE_LIMIT = 3
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs'
|
|
2
2
|
import { agent_exit_record } from '#scripts/agent/agent-exit-record'
|
|
3
3
|
import { api_outage } from '#scripts/agent/api-outage'
|
|
4
4
|
import type { ClaudeResultEvent } from '#scripts/agent/claude-result-event'
|
|
@@ -179,7 +179,7 @@ async function read_cut_taken(issue: string): Promise<boolean> {
|
|
|
179
179
|
return read.kind === 'carried' && read.cut.invocation === run_cut.invocation_for(issue)
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
function
|
|
182
|
+
function read_transcript(output_path: string): string | undefined {
|
|
183
183
|
const safe_path = run_liveness.to_safe_path(output_path)
|
|
184
184
|
|
|
185
185
|
if (safe_path === undefined) return undefined
|
|
@@ -188,12 +188,38 @@ function read_exit(output_path: string): ClaudeResultEvent | undefined {
|
|
|
188
188
|
// Validated by `run_liveness.to_safe_path` — normalized, required absolute, confined to the
|
|
189
189
|
// allowed roots — the same check `josh lane:output` gates a transcript path with, reused rather
|
|
190
190
|
// than re-spelled so the two cannot disagree.
|
|
191
|
-
return
|
|
191
|
+
return readFileSync(safe_path, 'utf8') // NOSONAR
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function read_exit(output_path: string): ClaudeResultEvent | undefined {
|
|
198
|
+
const transcript = read_transcript(output_path)
|
|
199
|
+
|
|
200
|
+
return transcript === undefined ? undefined : agent_exit_record.read_exit_record(transcript)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// When the child's transcript was last written, in epoch milliseconds — `undefined` when unreadable.
|
|
204
|
+
function written_at(output_path: string): number | undefined {
|
|
205
|
+
const safe_path = run_liveness.to_safe_path(output_path)
|
|
206
|
+
|
|
207
|
+
if (safe_path === undefined) return undefined
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
return statSync(safe_path).mtimeMs // NOSONAR — validated as `read_transcript` validates it
|
|
192
211
|
} catch {
|
|
193
212
|
return undefined
|
|
194
213
|
}
|
|
195
214
|
}
|
|
196
215
|
|
|
216
|
+
// The child's session was killed inside a turn rather than finishing it.
|
|
217
|
+
function ended_mid_turn(output_path: string): boolean {
|
|
218
|
+
const transcript = read_transcript(output_path)
|
|
219
|
+
|
|
220
|
+
return transcript !== undefined && agent_exit_record.is_mid_turn(transcript)
|
|
221
|
+
}
|
|
222
|
+
|
|
197
223
|
async function read_traces(request: EndingRequest): Promise<EndingTraces> {
|
|
198
224
|
const [is_child_closed, is_cut_taken, is_tree_dirty] = await Promise.all([
|
|
199
225
|
read_child_closed(request.issue, request.repo),
|
|
@@ -218,7 +244,9 @@ async function check(request: EndingRequest): Promise<EndingDecision> {
|
|
|
218
244
|
const run_ending = {
|
|
219
245
|
check,
|
|
220
246
|
decide,
|
|
247
|
+
ended_mid_turn,
|
|
221
248
|
read_exit,
|
|
249
|
+
written_at,
|
|
222
250
|
}
|
|
223
251
|
|
|
224
252
|
export type { EndingDecision, EndingRequest, EndingTraces, EndingVerdict }
|
|
@@ -53,16 +53,11 @@ function current_state(): ParentCutState {
|
|
|
53
53
|
const THRESHOLD_TEXT = `${cost_format.format_tokens(CONTEXT_CUT_THRESHOLD)}-token`
|
|
54
54
|
|
|
55
55
|
const PARENT_CUT_REASON =
|
|
56
|
-
'⛔ backlogrun parent hand-off: this session drives a `backlogrun`
|
|
57
|
-
`
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'make sure every lane in flight records an output path, then run `pnpm josh run:carry --cut --owner ' +
|
|
62
|
-
'"$PPID"` and, unless it answers `capped`, `pnpm josh run:wake`, post the progress comment and end the ' +
|
|
63
|
-
'turn. A lane nobody can poll means the cut does not happen; carry on and this asks again later. ' +
|
|
64
|
-
'Reissue this call when the hand-off is done or cannot be taken — the guard stays quiet for a while ' +
|
|
65
|
-
'after it refuses, so the hand-off calls themselves are not refused.'
|
|
56
|
+
'⛔ backlogrun parent hand-off: this session drives a `backlogrun` and its recent-context cost ' +
|
|
57
|
+
`crossed the ${THRESHOLD_TEXT} threshold between merges. Take the hand-off now: open no new lane, ` +
|
|
58
|
+
'follow `backlogrun-progress.md` → "The hand-off" (`pnpm josh run:carry --cut --owner "$PPID"`, then ' +
|
|
59
|
+
'`pnpm josh run:wake` unless `capped`) and end the turn. Reissue this call once done or if it ' +
|
|
60
|
+
'cannot be taken.'
|
|
66
61
|
|
|
67
62
|
// Neither an uncapped parent's own record nor a measured `over` is optional: a capped record has no cut
|
|
68
63
|
// left to take (`run:carry --cut` answers `capped`), and an unmeasurable session is left alone here —
|
|
@@ -343,8 +343,7 @@ async function read_candidate(
|
|
|
343
343
|
): Promise<ChildState | undefined> {
|
|
344
344
|
const state = await read_pr_of_branch(name)
|
|
345
345
|
|
|
346
|
-
if (state.pr_state !== NO_PR) return state
|
|
347
|
-
if (await has_commits_beyond(context.base, references)) return state
|
|
346
|
+
if (state.pr_state !== NO_PR || (await has_commits_beyond(context.base, references))) return state
|
|
348
347
|
|
|
349
348
|
return (await is_held_dirty(name, context.lane)) ? state : undefined
|
|
350
349
|
}
|
|
@@ -27,8 +27,7 @@ const STOP_RECOVERY =
|
|
|
27
27
|
// this type is for the run that halted needing a person. An `unreadable` record is not announced: with
|
|
28
28
|
// nothing readable there is no run this can honestly say stopped.
|
|
29
29
|
function plan(read: CarryRead, reason: string | undefined): StopNotice | undefined {
|
|
30
|
-
if (reason === undefined || reason.length === 0) return undefined
|
|
31
|
-
if (read.kind !== 'carried') return undefined
|
|
30
|
+
if (reason === undefined || reason.length === 0 || read.kind !== 'carried') return undefined
|
|
32
31
|
|
|
33
32
|
return { reason }
|
|
34
33
|
}
|
|
@@ -49,9 +49,12 @@ async function owes_watcher(): Promise<boolean> {
|
|
|
49
49
|
// `run_progress_clock.life_target_of(git_directory)`. Passed in rather than resolved here so the
|
|
50
50
|
// caller can use whichever resolution fits its sync/async context.
|
|
51
51
|
async function check(life_target: string): Promise<GuardResult> {
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (
|
|
53
|
+
!(await owes_watcher()) ||
|
|
54
|
+
run_progress_clock.is_life_fresh(life_target, STALE_THRESHOLD_MS)
|
|
55
|
+
) {
|
|
56
|
+
return OK_RESULT
|
|
57
|
+
}
|
|
55
58
|
|
|
56
59
|
return { kind: 'stale', note: STALE_NOTE }
|
|
57
60
|
}
|
|
@@ -59,7 +59,7 @@ const ROUND_ONE_RECORDED_NOTE =
|
|
|
59
59
|
// single-sourced with the preflight and gate stages.
|
|
60
60
|
const { run_phases, scoped_pair } = run_ship_scoped
|
|
61
61
|
|
|
62
|
-
type PromptOf = (brief_path: string, findings_path: string) => string
|
|
62
|
+
type PromptOf = (brief_path: string, findings_path: string, issue: string) => string
|
|
63
63
|
|
|
64
64
|
function failure(out: string): JoshResult {
|
|
65
65
|
return { code: FAILURE_EXIT_CODE, out }
|
|
@@ -138,7 +138,12 @@ async function launch_reviewer(
|
|
|
138
138
|
stamp_file.write_text_stamp(target.brief, brief)
|
|
139
139
|
stamp_file.remove_stamp(target.findings)
|
|
140
140
|
|
|
141
|
-
return await run_session(
|
|
141
|
+
return await run_session(
|
|
142
|
+
prompt_of(target.brief, target.findings, issue),
|
|
143
|
+
target.log,
|
|
144
|
+
issue,
|
|
145
|
+
round,
|
|
146
|
+
)
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
function current_verdict(): ReturnType<typeof run_ship_review.read_verdict> {
|
|
@@ -153,8 +158,12 @@ function current_verdict(): ReturnType<typeof run_ship_review.read_verdict> {
|
|
|
153
158
|
async function join_gate(): Promise<JoshResult> {
|
|
154
159
|
const joined = await josh([RUN_REVIEW, '--join'])
|
|
155
160
|
|
|
156
|
-
if (
|
|
157
|
-
|
|
161
|
+
if (
|
|
162
|
+
joined.code === SUCCESS_EXIT_CODE ||
|
|
163
|
+
!run_ship_review.has_fixes(stamp_file.read_stamp_text(paths().findings))
|
|
164
|
+
) {
|
|
165
|
+
return joined
|
|
166
|
+
}
|
|
158
167
|
|
|
159
168
|
note(FIXED_JOIN_NOTE)
|
|
160
169
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
1
2
|
import { review_finding_ledger } from '#scripts/review/review-finding-ledger'
|
|
2
3
|
import { review_record_cli } from '#scripts/review/review-record-cli'
|
|
3
4
|
|
|
@@ -83,12 +84,23 @@ function findings_instruction(findings_path: string, fixed_clause: string): stri
|
|
|
83
84
|
].join(' ')
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// The rubric's requirement check sends the reviewer to the Issue, and a body-only read there is refused
|
|
88
|
+
// by the issue-comments rule — so the prompt names the read that carries the comments with the body
|
|
89
|
+
// (joshuafolkken/kit#3875).
|
|
90
|
+
function issue_read_instruction(issue: string): string {
|
|
91
|
+
return [
|
|
92
|
+
`Read Issue ${issue_cite.plain(issue)} with \`pnpm josh issue:read ${issue}\`, which prints the body and every`,
|
|
93
|
+
`comment together — never a body-only \`gh api …/issues/${issue}\` read.`,
|
|
94
|
+
].join(' ')
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
// One line, because `detached_launch` refuses an argument carrying a control character: the multi-line
|
|
87
98
|
// brief travels by path, never inline.
|
|
88
|
-
function reviewer_prompt(brief_path: string, findings_path: string): string {
|
|
99
|
+
function reviewer_prompt(brief_path: string, findings_path: string, issue: string): string {
|
|
89
100
|
return [
|
|
90
101
|
`Run a /code-review of this checkout: read the review brief at ${brief_path} and follow it exactly —`,
|
|
91
102
|
'the level on its first line, the rubric it names, the checkout it pins and its review:attest step.',
|
|
103
|
+
issue_read_instruction(issue),
|
|
92
104
|
'Apply a fix only for a Medium finding whose fix is small and local — a one-function refactor, an',
|
|
93
105
|
'added test, an added guard; never fix a High or a change that reaches beyond one place, and never',
|
|
94
106
|
'commit, stage or push. After any fix, run `pnpm josh lint:related && pnpm josh test:related` and',
|
|
@@ -98,11 +110,12 @@ function reviewer_prompt(brief_path: string, findings_path: string): string {
|
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
// The round-2 verification pass: a fresh session over the round-1 fix delta, reading only.
|
|
101
|
-
function verification_prompt(brief_path: string, findings_path: string): string {
|
|
113
|
+
function verification_prompt(brief_path: string, findings_path: string, issue: string): string {
|
|
102
114
|
return [
|
|
103
115
|
`Run a /code-review of this checkout — the round-2 verification pass over the round-1 fixes: read the`,
|
|
104
116
|
`review brief at ${brief_path} and follow it exactly — the level on its first line, the rubric it`,
|
|
105
117
|
'names, the checkout it pins and its review:attest step. Do not edit any file.',
|
|
118
|
+
issue_read_instruction(issue),
|
|
106
119
|
findings_instruction(findings_path, ''),
|
|
107
120
|
].join(' ')
|
|
108
121
|
}
|
|
@@ -33,12 +33,12 @@ function memoized(read: IsMerged): IsMerged {
|
|
|
33
33
|
|
|
34
34
|
async function sweep(): Promise<void> {
|
|
35
35
|
try {
|
|
36
|
-
const
|
|
36
|
+
const is_merged = memoized(issue_merged.read_merged)
|
|
37
37
|
const in_flight = backlog_drive_restore.active_issues(
|
|
38
38
|
await run_event_stream_emit.current_events(),
|
|
39
39
|
)
|
|
40
|
-
const lanes = await run_tidy_lanes.tidy_lanes(
|
|
41
|
-
const stashes = await run_tidy_stashes.tidy_stashes(
|
|
40
|
+
const lanes = await run_tidy_lanes.tidy_lanes(is_merged, in_flight)
|
|
41
|
+
const stashes = await run_tidy_stashes.tidy_stashes(is_merged)
|
|
42
42
|
const report = run_tidy.format_report([...lanes, ...stashes])
|
|
43
43
|
|
|
44
44
|
if (report !== undefined) console.error(report)
|
|
@@ -95,8 +95,9 @@ async function tidy_lane(
|
|
|
95
95
|
is_merged: IsMerged,
|
|
96
96
|
in_flight: ReadonlySet<string>,
|
|
97
97
|
): Promise<Outcome | undefined> {
|
|
98
|
-
if (in_flight.has(lane.issue))
|
|
99
|
-
|
|
98
|
+
if (in_flight.has(lane.issue) || lane.is_stranded || !(await is_merged(lane.issue))) {
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
100
101
|
|
|
101
102
|
return { target: `lane ${session_cite.issue(lane.issue)}`, verdict: await settle(lane) }
|
|
102
103
|
}
|
|
@@ -126,8 +126,13 @@ function merge_project_config(existing: string, workspace: string): string {
|
|
|
126
126
|
const aikido = yaml_document.parse_yaml(existing)
|
|
127
127
|
const safe_chain = read_mapping(aikido['safe-chain'], 'safe-chain')
|
|
128
128
|
const merged = merge_age(merge_exclusions(safe_chain, exclusions), workspace)
|
|
129
|
-
|
|
130
|
-
if (
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
JSON.stringify(merged) === JSON.stringify(safe_chain) ||
|
|
132
|
+
has_nothing_to_write(existing, exclusions, workspace)
|
|
133
|
+
) {
|
|
134
|
+
return existing
|
|
135
|
+
}
|
|
131
136
|
|
|
132
137
|
return replace_safe_chain_block(existing, merged)
|
|
133
138
|
}
|
|
@@ -65,8 +65,14 @@ function hook_write_warning(
|
|
|
65
65
|
filename: string,
|
|
66
66
|
): string | undefined {
|
|
67
67
|
const installed_version = installed_consumer_version(project_root)
|
|
68
|
-
|
|
69
|
-
if (
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
writing_version === undefined ||
|
|
71
|
+
installed_version === undefined ||
|
|
72
|
+
is_safe_to_write_hooks(writing_version, installed_version)
|
|
73
|
+
) {
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
70
76
|
|
|
71
77
|
return outdated_install_warning(writing_version, installed_version, filename)
|
|
72
78
|
}
|
|
@@ -186,8 +186,9 @@ function worker_arguments(
|
|
|
186
186
|
vitest_arguments: ReadonlyArray<string>,
|
|
187
187
|
share: number | undefined,
|
|
188
188
|
): Array<string> {
|
|
189
|
-
if (share === undefined)
|
|
190
|
-
|
|
189
|
+
if (share === undefined || vitest_arguments.some((argument) => is_worker_flag(argument))) {
|
|
190
|
+
return []
|
|
191
|
+
}
|
|
191
192
|
|
|
192
193
|
return [`${WORKER_FLAG}=${String(share)}`]
|
|
193
194
|
}
|
|
@@ -51,12 +51,12 @@ type NamedCall = Pick<Span, 'is_bundleable' | 'is_writing' | 'label' | 'targets'
|
|
|
51
51
|
const RULE_POINTER = '`prompts/collaboration-workflow/turn-batching.md`'
|
|
52
52
|
const STREAK = `${String(CONSECUTIVE_LIMIT)} single-call turns in a row`
|
|
53
53
|
|
|
54
|
-
// Includes what to do when
|
|
54
|
+
// Includes what to do when there is nothing to pair — the guard cannot see the turn it interrupted.
|
|
55
55
|
const REASON =
|
|
56
56
|
`⛔ batching: ${STREAK}. Reissue this with the calls that do not need its result — the test is ` +
|
|
57
57
|
`whether a call's input needs another call's result, not what kind of call it is (edits are covered ` +
|
|
58
|
-
`exactly as reads)
|
|
59
|
-
`
|
|
58
|
+
`exactly as reads); it never authorizes weakening a verification gate or a review. Nothing to ` +
|
|
59
|
+
`pair? Reissue it unchanged. ${RULE_POINTER}`
|
|
60
60
|
|
|
61
61
|
// A whole-file write earns a notice, not a refusal: a `Write` cannot be refused safely.
|
|
62
62
|
const NOTICE =
|
|
@@ -68,8 +68,9 @@ function fork_path(transcript_path: string, agent_id: string): string {
|
|
|
68
68
|
// **A path that is not a transcript is answered with itself**, since nothing can be derived from it and
|
|
69
69
|
// guessing would name a file in some other layout entirely.
|
|
70
70
|
function transcript_of(transcript_path: string, agent_id: string | null | undefined): string {
|
|
71
|
-
if (!is_agent_id(agent_id)
|
|
72
|
-
|
|
71
|
+
if (!is_agent_id(agent_id) || !transcript_path.endsWith(TRANSCRIPT_EXTENSION)) {
|
|
72
|
+
return transcript_path
|
|
73
|
+
}
|
|
73
74
|
|
|
74
75
|
return fork_path(transcript_path, agent_id)
|
|
75
76
|
}
|
|
@@ -189,6 +189,15 @@ function unquoted(command: string): string {
|
|
|
189
189
|
return without_heredoc_bodies(command).replaceAll(QUOTED_SPAN_PATTERN, ' ')
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// Quoted text replaced by spaces of its own length, so an offset found here is the same offset in the
|
|
193
|
+
// command — what a caller cutting the command where the unquoted reading says to needs. `undefined`
|
|
194
|
+
// for a line carrying a heredoc, whose body lines a cut would read as commands of their own.
|
|
195
|
+
function blanked(command: string): string | undefined {
|
|
196
|
+
if (HEREDOC_MARKER.test(command)) return undefined
|
|
197
|
+
|
|
198
|
+
return command.replaceAll(QUOTED_SPAN_PATTERN, (span) => ' '.repeat(span.length))
|
|
199
|
+
}
|
|
200
|
+
|
|
192
201
|
function discarded_commands(command: string): Array<string> {
|
|
193
202
|
const plain = unquoted(command)
|
|
194
203
|
|
|
@@ -308,6 +317,7 @@ const time_shell = {
|
|
|
308
317
|
JOSH_PREFIX,
|
|
309
318
|
bash_command,
|
|
310
319
|
bash_label,
|
|
320
|
+
blanked,
|
|
311
321
|
command_segment,
|
|
312
322
|
discarded_commands,
|
|
313
323
|
josh_arguments,
|
|
@@ -23,8 +23,7 @@ function read_matching_version(manifest_path: string, package_name: string): str
|
|
|
23
23
|
const parsed = package_named_version_schema.safeParse(
|
|
24
24
|
json_value.parse_or_undefined(readFileSync(manifest_path, 'utf8')),
|
|
25
25
|
)
|
|
26
|
-
if (!parsed.success) return undefined
|
|
27
|
-
if (parsed.data.name !== package_name) return undefined
|
|
26
|
+
if (!parsed.success || parsed.data.name !== package_name) return undefined
|
|
28
27
|
|
|
29
28
|
return parsed.data.version
|
|
30
29
|
}
|
|
@@ -45,10 +44,10 @@ function walk_up_for_version(start_directory: string, package_name: string): str
|
|
|
45
44
|
function resolve_from_base(
|
|
46
45
|
base_url: string,
|
|
47
46
|
package_name: string,
|
|
48
|
-
resolve_marker
|
|
47
|
+
resolve_marker = package_name,
|
|
49
48
|
): string | undefined {
|
|
50
49
|
try {
|
|
51
|
-
return createRequire(base_url).resolve(resolve_marker
|
|
50
|
+
return createRequire(base_url).resolve(resolve_marker)
|
|
52
51
|
} catch {
|
|
53
52
|
return undefined
|
|
54
53
|
}
|