@joshuafolkken/kit 1.1094.0 → 1.1136.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/agents/investigator.md +17 -0
- package/.claude/settings.json +1 -0
- package/.claude/skills/workflow-commands/SKILL.md +1 -1
- package/.claude/skills/workflow-commands/background-commands.md +36 -85
- package/.claude/skills/workflow-commands/backlogrun-progress.md +31 -34
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -2
- package/.claude/skills/workflow-commands/delegation.md +5 -1
- package/.claude/skills/workflow-commands/fullrun.md +5 -4
- package/.claude/skills/workflow-commands/halfrun.md +5 -4
- package/.claude/skills/workflow-commands/latest-gate.md +7 -15
- package/.claude/skills/workflow-commands/pre-gate-cut.md +4 -8
- package/.claude/skills/workflow-commands/split-assessment.md +4 -18
- package/.github/actions/setup-node/action.yml +33 -0
- package/.github/actions/setup-pnpm/action.yml +4 -4
- package/.github/workflows/auto-tag.yml +2 -3
- package/.github/workflows/ci.yml +7 -8
- package/.github/workflows/dependabot-auto-merge.yml +1 -0
- package/.github/workflows/pr-classification.yml +1 -3
- package/.github/workflows/production.yml +1 -0
- package/.github/workflows/publish.yml +10 -9
- package/.github/workflows/sonar-qube.yml +17 -13
- package/.pnpmfile.mjs +29 -0
- package/AGENTS.md +4 -11
- package/CLAUDE.md +24 -17
- package/GEMINI.md +4 -13
- package/README.md +8 -3
- package/SECURITY.md +2 -0
- package/cspell/index.yaml +4 -0
- package/dist/CLAUDE.basic.md +6 -6
- package/dist/CLAUDE.md +24 -17
- package/dist/CLAUDE.static.md +6 -6
- package/dist/config-merge/index.js +33 -9
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-OIZUHPJI.js → chunk-FO2I7UT7.js} +1 -1
- package/dist/hooks/{chunk-NKOG3XAV.js → chunk-GDU26K3J.js} +11 -3
- package/dist/hooks/{chunk-O4LL4HTI.js → chunk-HANSKHES.js} +2 -2
- package/dist/hooks/{chunk-3C4YCCEF.js → chunk-HBKZLITF.js} +4 -4
- package/dist/hooks/{chunk-UD24EAIL.js → chunk-JKKTVDAG.js} +1984 -146
- package/dist/hooks/{chunk-M2XDCUKD.js → chunk-KBXZLMRH.js} +89 -1803
- package/dist/hooks/{chunk-IL27YTPH.js → chunk-KOKOAV6D.js} +2 -2
- package/dist/hooks/{chunk-7ZPFAVR7.js → chunk-LJBLGHEH.js} +59 -19
- package/dist/hooks/{chunk-E54KGFEQ.js → chunk-NFZGFGOR.js} +1 -1
- package/dist/hooks/{chunk-KHWHKJOM.js → chunk-RFWWUI5F.js} +4 -4
- package/dist/hooks/codex-hook-adapter.js +12 -47
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/inputs.json +175 -173
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +17 -12
- package/dist/josh.js +87 -31
- package/dist/version/index.js +36 -20
- package/eslint/rules/unicorn.js +3 -2
- package/package.json +23 -5
- package/pnpm-workspace.yaml +5 -7
- package/prompts/collaboration-workflow/glossary.md +17 -0
- package/prompts/collaboration-workflow/issue-template.md +1 -1
- package/prompts/collaboration-workflow/operating-rules.md +1 -1
- package/prompts/collaboration-workflow/principles.md +9 -0
- package/prompts/collaboration-workflow.md +1 -0
- package/scripts/backlog/backlog-arrival.ts +30 -10
- package/scripts/backlog/backlog-budget-cli.ts +15 -16
- package/scripts/backlog/backlog-offer-cli.ts +6 -14
- package/scripts/backlog/backlog-plan.ts +29 -18
- package/scripts/backlog/backlog-ready.ts +20 -7
- package/scripts/config-merge/parse-jsonc.ts +33 -3
- package/scripts/config-merge/patch-json-key.ts +13 -6
- package/scripts/cost-runtime/cost-cli.ts +3 -7
- package/scripts/delegation/delegation-policy.ts +9 -1
- package/scripts/delegation/investigation-guard.ts +1 -1
- package/scripts/delegation/investigation-reads.ts +105 -19
- package/scripts/doctor/doctor-consumer.ts +3 -5
- package/scripts/doctor/doctor-io.ts +9 -13
- package/scripts/document/backlogrun-parent-read-set.ts +12 -1
- package/scripts/document/capped-print.ts +74 -0
- package/scripts/document/document-byte-budget.ts +1 -0
- package/scripts/document/document-section-cli.ts +4 -3
- package/scripts/document/entry-read-budget.ts +13 -7
- package/scripts/document/entry-read-set.ts +32 -8
- package/scripts/document/lane-child-read-set.ts +7 -0
- package/scripts/document/read-set-point-of-use.ts +8 -3
- package/scripts/document/read-set-trim.ts +3 -1
- package/scripts/epic/epic-audit.ts +3 -18
- package/scripts/gate/scoped-green.ts +25 -14
- package/scripts/gh/git-gh-issue-list.ts +12 -11
- package/scripts/gh/git-gh-issue-write.ts +4 -1
- package/scripts/gh/git-gh-pr-auto-merge.ts +4 -1
- package/scripts/git/git-command.ts +6 -1
- package/scripts/git/git-spawn-sync.ts +65 -0
- package/scripts/git/git-spawn.ts +5 -4
- package/scripts/git/stash/stash-pop-args.ts +1 -6
- package/scripts/init/claude-kit-only-settings.ts +29 -0
- package/scripts/init/init-copy-content.ts +8 -2
- package/scripts/init/init-file-action.ts +54 -0
- package/scripts/init/init-logic-migrate.ts +35 -17
- package/scripts/init/init-logic-secretlint.ts +1 -1
- package/scripts/init/init-logic.ts +6 -22
- package/scripts/init/init.ts +11 -68
- package/scripts/init/initial-commit.ts +24 -0
- package/scripts/init/kit-written-paths.ts +2 -0
- package/scripts/init/project-profile.ts +3 -6
- package/scripts/init/start-exec.ts +16 -3
- package/scripts/init/start-steps.ts +10 -13
- package/scripts/issue/defect-rate-cli.ts +61 -20
- package/scripts/issue/issue-backlinks.ts +7 -14
- package/scripts/issue/issue-cite.ts +12 -6
- package/scripts/issue/issue-file.ts +5 -12
- package/scripts/issue/issue-read-cli.ts +1 -0
- package/scripts/issue/issue-report-failures.ts +6 -2
- package/scripts/issue/issue-state-cli.ts +1 -1
- package/scripts/issue/markdown-section.ts +21 -6
- package/scripts/issue/measure-rerun-cli.ts +73 -14
- package/scripts/josh/josh-commands-lane.ts +1 -1
- package/scripts/josh/josh-commands-project.ts +8 -1
- package/scripts/josh/josh-commands-workflow.ts +1 -1
- package/scripts/josh/run-github-prerequisite.ts +3 -6
- package/scripts/lane/lane-await.ts +89 -15
- package/scripts/lane/lane-cli.ts +11 -13
- package/scripts/lane/lane-close.ts +4 -1
- package/scripts/lane/lane-launch-cli.ts +10 -16
- package/scripts/lane/openai-review-broker.ts +4 -1
- package/scripts/lib/cli-flags.ts +31 -2
- package/scripts/lib/error-message.ts +12 -1
- package/scripts/lines/line-targets.ts +0 -11
- package/scripts/lines/lines-command.ts +5 -3
- package/scripts/propagate/propagate-git.ts +3 -8
- package/scripts/refactor/refactor-scan.ts +2 -1
- package/scripts/registry-migration/migrate-logic.ts +28 -19
- package/scripts/registry-migration/migrate.ts +6 -5
- package/scripts/release/github-release-cli.ts +14 -3
- package/scripts/release/github-release-environment.ts +1 -0
- package/scripts/release/github-release.ts +103 -17
- package/scripts/release/release-publish.ts +36 -2
- package/scripts/release/release-worktree.ts +97 -2
- package/scripts/review/review-checkout.ts +3 -2
- package/scripts/review/review-record-cli.ts +9 -16
- package/scripts/rules/early-heartbeat.ts +4 -3
- package/scripts/rules/lane-park.ts +9 -0
- package/scripts/rules/stop-rules.ts +7 -4
- package/scripts/run/run-carry-args.ts +8 -12
- package/scripts/run/run-carry-cli.ts +3 -1
- package/scripts/run/run-carry-conversation.ts +94 -0
- package/scripts/run/run-carry-reclaim.ts +37 -0
- package/scripts/run/run-carry.ts +21 -5
- package/scripts/run/run-cut-args.ts +1 -6
- package/scripts/run/run-ending-cli.ts +1 -5
- package/scripts/run/run-entry-cli.ts +40 -9
- package/scripts/run/run-entry-stop.ts +104 -0
- package/scripts/run/run-event-stream-emit.ts +7 -2
- package/scripts/run/run-hold.ts +4 -1
- package/scripts/run/run-liveness-cli.ts +1 -5
- package/scripts/run/run-merge-cli.ts +2 -10
- package/scripts/run/run-merge-steps.ts +4 -1
- package/scripts/run/run-parent-cut-hook.ts +4 -1
- package/scripts/run/run-prep-cli.ts +4 -1
- package/scripts/run/run-progress-args.ts +20 -4
- package/scripts/run/run-progress-cli.ts +31 -29
- package/scripts/run/run-progress-clock.ts +4 -1
- package/scripts/run/run-ship-cli.ts +8 -16
- package/scripts/run/run-ship-probe.ts +4 -1
- package/scripts/run/run-status-cli.ts +2 -15
- package/scripts/run/run-tidy-lanes.ts +4 -1
- package/scripts/run/run-wake-cli.ts +2 -10
- package/scripts/run/run-watcher-guard.ts +9 -5
- package/scripts/safe-chain/pack-hook.ts +92 -0
- package/scripts/sync/sync-ai-files.ts +4 -0
- package/scripts/test/test-declared-changed.ts +13 -16
- package/scripts/test/test-declared.ts +3 -7
- package/scripts/test/test-related.ts +1 -6
- package/scripts/test/test-unit-guard.ts +21 -7
- package/scripts/time-runtime/time-spans.ts +4 -0
- package/scripts/version/version-commands.ts +11 -1
- package/templates/CLAUDE.basic.md +6 -6
- package/templates/workflows/ci.yml +16 -82
- package/templates/workflows/dependabot-auto-merge.yml +1 -0
- package/templates/workflows/github-release.yml +14 -28
- package/tsconfig.sonar.json +4 -22
|
@@ -76,17 +76,13 @@ type Request =
|
|
|
76
76
|
const READ_REQUEST: Request = { kind: 'read' }
|
|
77
77
|
|
|
78
78
|
function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
|
|
79
|
-
return cli_flags.
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function text_of(value: OptionValue): string | undefined {
|
|
83
|
-
return typeof value === 'string' ? value : undefined
|
|
79
|
+
return cli_flags.values_of(argv, OPTIONS)
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
// Absent is zero; present but not a count is `undefined`, which invalidates the whole invocation
|
|
87
83
|
// rather than quietly counting nothing.
|
|
88
84
|
function to_count(value: OptionValue): number | undefined {
|
|
89
|
-
const text =
|
|
85
|
+
const text = cli_flags.string_of(value)
|
|
90
86
|
|
|
91
87
|
if (text === undefined) return NO_INCREMENT
|
|
92
88
|
|
|
@@ -99,7 +95,7 @@ function to_count(value: OptionValue): number | undefined {
|
|
|
99
95
|
// than a zero: there is no issue to record, which is a different fact from recording issue zero. A
|
|
100
96
|
// present-but-malformed value invalidates the whole invocation, exactly as a bad `--merged` does.
|
|
101
97
|
function to_done(value: OptionValue): Pick<CarryChange, 'done'> | undefined {
|
|
102
|
-
const text =
|
|
98
|
+
const text = cli_flags.string_of(value)
|
|
103
99
|
|
|
104
100
|
if (text === undefined) return {}
|
|
105
101
|
|
|
@@ -165,7 +161,7 @@ function group_count(values: ParsedValues): number {
|
|
|
165
161
|
// the record simply declares no owner and every standing record reads as not provably live, which
|
|
166
162
|
// refuses rather than resumes.
|
|
167
163
|
function to_owner(value: OptionValue): CarryOwner | undefined {
|
|
168
|
-
const text =
|
|
164
|
+
const text = cli_flags.string_of(value)
|
|
169
165
|
|
|
170
166
|
if (text === undefined) return run_carry.NO_OWNER
|
|
171
167
|
|
|
@@ -179,7 +175,7 @@ function to_owner(value: OptionValue): CarryOwner | undefined {
|
|
|
179
175
|
// A non-empty `--summary`, or nothing. An empty string is nothing here, so it reads as absent and the
|
|
180
176
|
// pairing check below refuses it against `--retrospective`.
|
|
181
177
|
function summary_text(values: ParsedValues): string | undefined {
|
|
182
|
-
const summary =
|
|
178
|
+
const summary = cli_flags.string_of(values.summary)
|
|
183
179
|
|
|
184
180
|
return summary === undefined || summary === '' ? undefined : summary
|
|
185
181
|
}
|
|
@@ -220,17 +216,17 @@ function to_claim_request(
|
|
|
220
216
|
}
|
|
221
217
|
|
|
222
218
|
function to_other_request(values: ParsedValues, owner: CarryOwner): Request | undefined {
|
|
223
|
-
if (values.end === true) return { kind: 'end', stopped:
|
|
219
|
+
if (values.end === true) return { kind: 'end', stopped: cli_flags.string_of(values.stopped) }
|
|
224
220
|
|
|
225
221
|
return has_count(values) ? to_count_request(values, owner) : READ_REQUEST
|
|
226
222
|
}
|
|
227
223
|
|
|
228
224
|
function to_named_request(values: ParsedValues, owner: CarryOwner): Request | undefined {
|
|
229
|
-
const begun =
|
|
225
|
+
const begun = cli_flags.string_of(values.begin)
|
|
230
226
|
|
|
231
227
|
if (begun !== undefined) return to_claim_request(begun, owner, false)
|
|
232
228
|
|
|
233
|
-
const adopted =
|
|
229
|
+
const adopted = cli_flags.string_of(values.resume)
|
|
234
230
|
|
|
235
231
|
if (adopted !== undefined) return to_claim_request(adopted, owner, true)
|
|
236
232
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type RunCarry,
|
|
11
11
|
} from './run-carry'
|
|
12
12
|
import { run_carry_args, type CountRequest, type Request } from './run-carry-args'
|
|
13
|
+
import { run_carry_conversation } from './run-carry-conversation'
|
|
13
14
|
import { run_carry_stash } from './run-carry-stash'
|
|
14
15
|
import { run_event_stream } from './run-event-stream'
|
|
15
16
|
import { run_event_stream_emit } from './run-event-stream-emit'
|
|
@@ -331,7 +332,8 @@ async function count(
|
|
|
331
332
|
|
|
332
333
|
if (blocked !== undefined) return blocked
|
|
333
334
|
|
|
334
|
-
const
|
|
335
|
+
const owned = run_carry_conversation.reclaim_owner(read.carry, request.owner)
|
|
336
|
+
const carry = run_carry.apply_change(target, owned, request.change)
|
|
335
337
|
|
|
336
338
|
await record_retrospective(request)
|
|
337
339
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { cost_transcript } from '#scripts/cost-runtime/cost-transcript'
|
|
4
|
+
import { own_session } from '#scripts/cost-runtime/own-session'
|
|
5
|
+
import type { CarryOwner, RunCarry } from './run-carry'
|
|
6
|
+
|
|
7
|
+
// **A conversation outlives its process, so an owner named by a pid alone dies too early**
|
|
8
|
+
// (joshuafolkken/kit#3137). The carry record names the agent process spending the budget, and that is
|
|
9
|
+
// right while the process lives — but an interactive conversation is not its process. On 2026-10-04 a
|
|
10
|
+
// VS Code extension-host restart ended the session's process at 15:17:34 UTC, and the same
|
|
11
|
+
// conversation resumed eighteen seconds later in a new process (`--resume=<id>`), writing on into the
|
|
12
|
+
// same transcript. `run:wake` read the old pid as dead, recovered the record as a crash, and dispatched
|
|
13
|
+
// a second lane child for the issue the conversation was still running.
|
|
14
|
+
//
|
|
15
|
+
// **The transcript is what persists across the restart, so it is the second half of the identity.**
|
|
16
|
+
// Every resume path — the extension's `--resume=<id>`, a person's `claude -c` — appends to the one
|
|
17
|
+
// file named by the session id, and nothing else writes it. So a record also names its owner's
|
|
18
|
+
// transcript, and a dead pid whose transcript was written inside the quiet window below is a
|
|
19
|
+
// conversation still being continued rather than a crash. Matching the transcript is also how the
|
|
20
|
+
// resumed conversation is recognized as the record's own owner, so it can keep counting.
|
|
21
|
+
//
|
|
22
|
+
// **A hand-off ends the conversation's claim.** A record `--cut` handed off is waited on by the pid
|
|
23
|
+
// alone, because the cutting conversation is meant to end there — counted by its transcript, the
|
|
24
|
+
// supervisor would hold every cut for the whole window after the cutting process had already exited.
|
|
25
|
+
|
|
26
|
+
// How long after its last write a conversation whose process has gone still counts as continuing. It
|
|
27
|
+
// bridges a restart and a resume — measured at eighteen seconds — with room for a person reopening a
|
|
28
|
+
// window, and it is the order of the wake grace window, so a genuine crash is recovered no later than
|
|
29
|
+
// a lost wake would be retried.
|
|
30
|
+
const CONVERSATION_QUIET_MS = 600_000
|
|
31
|
+
|
|
32
|
+
type Environment = Readonly<Record<string, string | undefined>>
|
|
33
|
+
|
|
34
|
+
// The calling session's own transcript, or `undefined` outside a session or when its file cannot be
|
|
35
|
+
// found. **The id comes from the environment, which a detached supervisor never inherits**
|
|
36
|
+
// (`agent-session-environment.ts` strips it), so a record the supervisor adopts names no transcript.
|
|
37
|
+
function own_transcript(
|
|
38
|
+
environment: Environment = process.env,
|
|
39
|
+
directories: ReadonlyArray<string> = cost_transcript.transcript_directories(process.cwd()),
|
|
40
|
+
): string | undefined {
|
|
41
|
+
const session_id = own_session.session_id_of(environment)
|
|
42
|
+
|
|
43
|
+
if (session_id === undefined) return undefined
|
|
44
|
+
|
|
45
|
+
const name = `${session_id}${cost_transcript.TRANSCRIPT_EXTENSION}`
|
|
46
|
+
|
|
47
|
+
return directories.map((directory) => path.join(directory, name)).find((file) => existsSync(file))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function modified_ms(transcript: string): number | undefined {
|
|
51
|
+
try {
|
|
52
|
+
return statSync(transcript).mtimeMs
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function is_recent(modified: number | undefined, now: Date): boolean {
|
|
59
|
+
return modified !== undefined && now.getTime() - modified <= CONVERSATION_QUIET_MS
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function is_conversation_live(
|
|
63
|
+
carry: RunCarry,
|
|
64
|
+
now: Date = new Date(),
|
|
65
|
+
read_modified: (transcript: string) => number | undefined = modified_ms,
|
|
66
|
+
): boolean {
|
|
67
|
+
if (carry.is_handed_off === true || carry.owner_transcript === undefined) return false
|
|
68
|
+
|
|
69
|
+
return is_recent(read_modified(carry.owner_transcript), now)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function is_same_conversation(carry: RunCarry, owner: CarryOwner): boolean {
|
|
73
|
+
return owner.transcript !== undefined && owner.transcript === carry.owner_transcript
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The record with its owner moved to the caller's process when the caller is the same conversation in
|
|
77
|
+
// a new one, so the record goes on naming a live process once the conversation falls quiet. A record a
|
|
78
|
+
// cut handed off is the successor's to take, never the cutting conversation's to take back.
|
|
79
|
+
function reclaim_owner(carry: RunCarry, owner: CarryOwner): RunCarry {
|
|
80
|
+
if (carry.is_handed_off === true || !is_same_conversation(carry, owner)) return carry
|
|
81
|
+
if (owner.pid === carry.owner_pid) return carry
|
|
82
|
+
|
|
83
|
+
return { ...carry, owner_pid: owner.pid, owner_start: owner.start }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const run_carry_conversation = {
|
|
87
|
+
CONVERSATION_QUIET_MS,
|
|
88
|
+
own_transcript,
|
|
89
|
+
is_conversation_live,
|
|
90
|
+
is_same_conversation,
|
|
91
|
+
reclaim_owner,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { run_carry_conversation }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
2
|
+
import { run_carry, type CarryOwner } from './run-carry'
|
|
3
|
+
import { run_carry_conversation } from './run-carry-conversation'
|
|
4
|
+
|
|
5
|
+
// **A resumed conversation has to take its record back before it falls quiet** (joshuafolkken/kit#3137).
|
|
6
|
+
// A count moves the owner to the conversation's new process, but a parent waiting on a lane child
|
|
7
|
+
// counts nothing — `lane:await` blocks in the background and the transcript stays unwritten — so a
|
|
8
|
+
// restart followed by a long wait outlasts the quiet window with the record still naming the dead
|
|
9
|
+
// process, and `run:wake` recovers it as a crash. The wait itself is therefore where the parent
|
|
10
|
+
// declares its owner: `lane:await --owner "$PPID"` reclaims the record before it starts waiting.
|
|
11
|
+
|
|
12
|
+
// Writes the record back under the caller's process when the caller is the record's own conversation
|
|
13
|
+
// in a new one; answers whether it did.
|
|
14
|
+
function reclaim_at(target: string, owner: CarryOwner): boolean {
|
|
15
|
+
const read = run_carry.read_carry(target)
|
|
16
|
+
|
|
17
|
+
if (read.kind !== 'carried') return false
|
|
18
|
+
|
|
19
|
+
const reclaimed = run_carry_conversation.reclaim_owner(read.carry, owner)
|
|
20
|
+
|
|
21
|
+
if (reclaimed === read.carry) return false
|
|
22
|
+
|
|
23
|
+
// Replaced atomically: a `run:wake` reading between an unlink and a write would see no record.
|
|
24
|
+
stamp_file.replace_stamp(target, reclaimed)
|
|
25
|
+
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function reclaim_carry(owner: CarryOwner): Promise<boolean> {
|
|
30
|
+
const directory = await run_carry.repository_directory()
|
|
31
|
+
|
|
32
|
+
return directory !== undefined && reclaim_at(run_carry.carry_path(directory), owner)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const run_carry_reclaim = { reclaim_at, reclaim_carry }
|
|
36
|
+
|
|
37
|
+
export { run_carry_reclaim }
|
package/scripts/run/run-carry.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { process_identity } from '#scripts/josh/process-identity'
|
|
|
4
4
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
5
5
|
import { z } from 'zod'
|
|
6
6
|
import { run_carry_change } from './run-carry-change'
|
|
7
|
+
import { run_carry_conversation } from './run-carry-conversation'
|
|
7
8
|
import { run_invocation } from './run-invocation'
|
|
8
9
|
|
|
9
10
|
// joshuafolkken/kit#1714: a `backlogrun` declares a budget — `--max`, `--idle` and the 8-hour
|
|
@@ -106,6 +107,9 @@ interface RunCarry {
|
|
|
106
107
|
// them apart would be asserting a distinction nothing here can observe.
|
|
107
108
|
owner_pid?: number | undefined
|
|
108
109
|
owner_start?: string | undefined
|
|
110
|
+
// The owner conversation's transcript, which outlives a restart of its process
|
|
111
|
+
// (`run-carry-conversation.ts`, joshuafolkken/kit#3137). Optional for the same reason as the pid.
|
|
112
|
+
owner_transcript?: string | undefined
|
|
109
113
|
// Set by `--cut`, and by nothing else. A crash never reaches `--cut`, which is what makes a
|
|
110
114
|
// declared cut the only standing record carried without a person deciding.
|
|
111
115
|
is_handed_off?: boolean | undefined
|
|
@@ -129,10 +133,12 @@ interface RunCarry {
|
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
// The identity of a process, as `process-identity.ts` keeps it: the pid plus an opaque start-time
|
|
132
|
-
// token, so a reissued pid is never mistaken for the process that wrote the record
|
|
136
|
+
// token, so a reissued pid is never mistaken for the process that wrote the record — and the
|
|
137
|
+
// transcript of the conversation that process runs, which outlives it (`run-carry-conversation.ts`).
|
|
133
138
|
interface CarryOwner {
|
|
134
139
|
pid?: number | undefined
|
|
135
140
|
start?: string | undefined
|
|
141
|
+
transcript?: string | undefined
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
// **`resume` is the only one of the four that establishes anything.** The other three are refusals,
|
|
@@ -201,6 +207,7 @@ const run_carry_schema = z.object({
|
|
|
201
207
|
// which is the not-provably-live answer rather than a live one.
|
|
202
208
|
owner_pid: z.number().optional(),
|
|
203
209
|
owner_start: z.string().optional(),
|
|
210
|
+
owner_transcript: z.string().optional(),
|
|
204
211
|
is_handed_off: z.boolean().optional(),
|
|
205
212
|
done: z.array(z.number()).optional(),
|
|
206
213
|
// Optional, so a record written before the retrospective existed still parses (joshuafolkken/kit#2328).
|
|
@@ -256,12 +263,14 @@ function read_carry(target: string, now: Date = new Date()): CarryRead {
|
|
|
256
263
|
|
|
257
264
|
// The identity to record for a pid the caller declared. `read_start` is what makes the pair an
|
|
258
265
|
// identity rather than a number; injected in tests so the sandbox branch is exercised without
|
|
259
|
-
// changing the machine's process capabilities.
|
|
266
|
+
// changing the machine's process capabilities. The transcript is the calling session's own — the
|
|
267
|
+
// declared pid is that session's process (`--owner "$PPID"`).
|
|
260
268
|
function owner_of(
|
|
261
269
|
pid: number,
|
|
262
270
|
read: (owner_pid: number) => string | undefined = process_identity.read_start,
|
|
271
|
+
transcript: string | undefined = run_carry_conversation.own_transcript(),
|
|
263
272
|
): CarryOwner {
|
|
264
|
-
return { pid, start: read(pid) }
|
|
273
|
+
return { pid, start: read(pid), transcript }
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
function fresh_carry(invocation: string, owner: CarryOwner, now: Date): RunCarry {
|
|
@@ -275,6 +284,7 @@ function fresh_carry(invocation: string, owner: CarryOwner, now: Date): RunCarry
|
|
|
275
284
|
outages: NO_INCREMENT,
|
|
276
285
|
owner_pid: owner.pid,
|
|
277
286
|
owner_start: owner.start,
|
|
287
|
+
owner_transcript: owner.transcript,
|
|
278
288
|
}
|
|
279
289
|
}
|
|
280
290
|
|
|
@@ -329,6 +339,7 @@ function adopt_carry(
|
|
|
329
339
|
...carry,
|
|
330
340
|
owner_pid: owner.pid,
|
|
331
341
|
owner_start: owner.start,
|
|
342
|
+
owner_transcript: owner.transcript,
|
|
332
343
|
is_handed_off: false,
|
|
333
344
|
}
|
|
334
345
|
|
|
@@ -340,11 +351,16 @@ function adopt_carry(
|
|
|
340
351
|
// A live pid with no readable start token is conservatively held rather than replaced. The command
|
|
341
352
|
// cannot prove its generation inside that sandbox, but a false `busy` stops for a person while a
|
|
342
353
|
// false `standing` can put two parents on one budget. A record with no pid still resolves false.
|
|
343
|
-
|
|
344
|
-
|
|
354
|
+
// A gone process whose conversation is still being continued is live (`run-carry-conversation.ts`).
|
|
355
|
+
function is_owner_live(carry: RunCarry, now: Date = new Date()): boolean {
|
|
356
|
+
if (process_identity.is_same_process(carry.owner_pid, carry.owner_start) !== false) return true
|
|
357
|
+
|
|
358
|
+
return run_carry_conversation.is_conversation_live(carry, now)
|
|
345
359
|
}
|
|
346
360
|
|
|
347
361
|
function is_owned_by(carry: RunCarry, owner: CarryOwner): boolean {
|
|
362
|
+
if (run_carry_conversation.is_same_conversation(carry, owner)) return true
|
|
363
|
+
|
|
348
364
|
return carry.owner_pid === owner.pid && carry.owner_start === owner.start
|
|
349
365
|
}
|
|
350
366
|
|
|
@@ -47,12 +47,7 @@ type Request =
|
|
|
47
47
|
| { kind: 'end' }
|
|
48
48
|
|
|
49
49
|
function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
|
|
50
|
-
return cli_flags.
|
|
51
|
-
args: [...argv],
|
|
52
|
-
options: OPTIONS,
|
|
53
|
-
strict: true,
|
|
54
|
-
allowPositionals: true,
|
|
55
|
-
})
|
|
50
|
+
return cli_flags.arguments_of(argv, OPTIONS)
|
|
56
51
|
}
|
|
57
52
|
|
|
58
53
|
// The issue is the sole positional, and it must be a real issue number: a cut relaunches
|
|
@@ -56,10 +56,6 @@ interface ParsedArguments {
|
|
|
56
56
|
values: ParsedValues
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function read_arguments(argv: ReadonlyArray<string>): ParsedArguments | undefined {
|
|
60
|
-
return cli_flags.parse_or_undefined({ args: [...argv], options: OPTIONS, allowPositionals: true })
|
|
61
|
-
}
|
|
62
|
-
|
|
63
59
|
function is_valid(parsed: ParsedArguments): boolean {
|
|
64
60
|
return (
|
|
65
61
|
parsed.positionals.length === 1 &&
|
|
@@ -78,7 +74,7 @@ function to_request(parsed: ParsedArguments): EndingRequest {
|
|
|
78
74
|
}
|
|
79
75
|
|
|
80
76
|
function parse_request(argv: ReadonlyArray<string>): EndingRequest | undefined {
|
|
81
|
-
const parsed =
|
|
77
|
+
const parsed = cli_flags.arguments_of(argv, OPTIONS)
|
|
82
78
|
|
|
83
79
|
if (parsed === undefined || !is_valid(parsed)) return undefined
|
|
84
80
|
|
|
@@ -4,6 +4,7 @@ import { josh_command } from '#scripts/josh/josh-run'
|
|
|
4
4
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
5
5
|
import { run_cut_report } from './run-cut-report'
|
|
6
6
|
import { run_entry, type EntryParts } from './run-entry'
|
|
7
|
+
import { run_entry_stop } from './run-entry-stop'
|
|
7
8
|
import { run_halfrun_resume } from './run-halfrun-resume'
|
|
8
9
|
import { run_hold_cli } from './run-hold-cli'
|
|
9
10
|
import { run_next } from './run-next'
|
|
@@ -40,6 +41,7 @@ const NEWLINE = '\n'
|
|
|
40
41
|
const HOLD_STOP_NOTE =
|
|
41
42
|
'(tree not held — see the reason above; clean up and retry, or release with `pnpm josh run:release`)'
|
|
42
43
|
const COST_STOP_NOTE = '(session budget spent — see the figure above; resume in a fresh session)'
|
|
44
|
+
const { BUDGET_REASON, HOLD_REASON } = run_entry_stop
|
|
43
45
|
const HALFRUN_RESUME_TOKEN = 'halfrun'
|
|
44
46
|
|
|
45
47
|
// Exactly one issue number, or the call is refused — `run:entry` opens one run, and a second number
|
|
@@ -135,6 +137,23 @@ function stop_report(issue_number: string, hold: string, cost: string, report: s
|
|
|
135
137
|
return emit({ issue_number, hold, cost, verdict: run_entry.NO_VERDICT, report })
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
// A stop this command decided carries its own chores (joshuafolkken/kit#3099): the `confirmation`
|
|
141
|
+
// Telegram always, and the release where this call claimed the hold — `run_entry_stop` says why.
|
|
142
|
+
interface StopCall {
|
|
143
|
+
parts: Pick<EntryParts, 'issue_number' | 'hold' | 'cost' | 'report'>
|
|
144
|
+
command: StageCommand
|
|
145
|
+
reason: string
|
|
146
|
+
should_release: boolean
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function stop_run({ parts, command, reason, should_release }: StopCall): Promise<number> {
|
|
150
|
+
const code = stop_report(parts.issue_number, parts.hold, parts.cost, parts.report)
|
|
151
|
+
|
|
152
|
+
await run_entry_stop.stop({ issue_number: parts.issue_number, command, reason, should_release })
|
|
153
|
+
|
|
154
|
+
return code
|
|
155
|
+
}
|
|
156
|
+
|
|
138
157
|
interface Resume {
|
|
139
158
|
token: string
|
|
140
159
|
code: number
|
|
@@ -166,16 +185,22 @@ function resume_report(issue_number: string, resume: Resume): number {
|
|
|
166
185
|
// to and including implementation. **A `prrun` stopped before its merge is resumed the same way**
|
|
167
186
|
// (joshuafolkken/kit#3023), under the token that says what is left of it.
|
|
168
187
|
async function resume_stopped(
|
|
169
|
-
issue_number:
|
|
188
|
+
{ issue_number, command }: EntryRequest,
|
|
170
189
|
token: string,
|
|
171
190
|
stopped_run: { adopt: (issue: string) => Promise<boolean> },
|
|
172
191
|
): Promise<number> {
|
|
173
192
|
const cost = await check_cost()
|
|
174
193
|
|
|
175
|
-
if (cost === run_entry.COST_OVER)
|
|
194
|
+
if (cost === run_entry.COST_OVER) {
|
|
195
|
+
const parts = { issue_number, hold: token, cost, report: COST_STOP_NOTE }
|
|
196
|
+
|
|
197
|
+
return await stop_run({ parts, command, reason: BUDGET_REASON, should_release: false })
|
|
198
|
+
}
|
|
176
199
|
|
|
177
200
|
if (!(await stopped_run.adopt(issue_number))) {
|
|
178
|
-
|
|
201
|
+
const parts = { issue_number, hold: run_hold_cli.BUSY_VERDICT, cost, report: HOLD_STOP_NOTE }
|
|
202
|
+
|
|
203
|
+
return await stop_run({ parts, command, reason: HOLD_REASON, should_release: false })
|
|
179
204
|
}
|
|
180
205
|
|
|
181
206
|
console.info(`entry #${issue_number} — resume: ${token}`)
|
|
@@ -186,17 +211,17 @@ async function resume_stopped(
|
|
|
186
211
|
// The stopped run a decision picks up: a `halfrun` stop resumes at the gate, a `prrun` stop at
|
|
187
212
|
// `followup` under its own token. `undefined` for any other start, so the ordinary claim decides.
|
|
188
213
|
async function resume_any(
|
|
189
|
-
|
|
214
|
+
request: EntryRequest,
|
|
190
215
|
decision: StageDecision,
|
|
191
216
|
stage: StageRead,
|
|
192
217
|
): Promise<number | undefined> {
|
|
193
218
|
if (decision.state === run_stage.HALFRUN_STOPPED) {
|
|
194
|
-
return await resume_stopped(
|
|
219
|
+
return await resume_stopped(request, HALFRUN_RESUME_TOKEN, run_halfrun_resume)
|
|
195
220
|
}
|
|
196
221
|
|
|
197
222
|
return stage.prrun_token === undefined
|
|
198
223
|
? undefined
|
|
199
|
-
: await resume_stopped(
|
|
224
|
+
: await resume_stopped(request, stage.prrun_token, run_prrun_resume)
|
|
200
225
|
}
|
|
201
226
|
|
|
202
227
|
// **A command whose stopping point the issue has already reached redoes nothing** (joshuafolkken/kit#3042)
|
|
@@ -210,12 +235,18 @@ async function claim_run({ issue_number, command }: EntryRequest): Promise<numbe
|
|
|
210
235
|
const hold = await claim_hold(issue_number, command)
|
|
211
236
|
|
|
212
237
|
if (hold !== run_hold_cli.HOLD_VERDICT) {
|
|
213
|
-
|
|
238
|
+
const parts = { issue_number, hold, cost: run_entry.COST_SKIPPED, report: HOLD_STOP_NOTE }
|
|
239
|
+
|
|
240
|
+
return await stop_run({ parts, command, reason: HOLD_REASON, should_release: false })
|
|
214
241
|
}
|
|
215
242
|
|
|
216
243
|
const cost = await check_cost()
|
|
217
244
|
|
|
218
|
-
if (cost === run_entry.COST_OVER)
|
|
245
|
+
if (cost === run_entry.COST_OVER) {
|
|
246
|
+
const parts = { issue_number, hold, cost, report: COST_STOP_NOTE }
|
|
247
|
+
|
|
248
|
+
return await stop_run({ parts, command, reason: BUDGET_REASON, should_release: true })
|
|
249
|
+
}
|
|
219
250
|
|
|
220
251
|
const reads = await gather_reads(issue_number)
|
|
221
252
|
|
|
@@ -248,7 +279,7 @@ async function open_run(request: EntryRequest): Promise<number> {
|
|
|
248
279
|
|
|
249
280
|
if (command === run_stage.KICKOFF || is_settled(decision)) return SUCCESS_EXIT_CODE
|
|
250
281
|
|
|
251
|
-
return (await resume_any(
|
|
282
|
+
return (await resume_any(request, decision, stage)) ?? (await claim_run(request))
|
|
252
283
|
}
|
|
253
284
|
|
|
254
285
|
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { josh_command } from '#scripts/josh/josh-run'
|
|
2
|
+
import { telegram_notify } from '#scripts/notify/telegram-notify'
|
|
3
|
+
import { lane_park } from '#scripts/rules/lane-park'
|
|
4
|
+
|
|
5
|
+
// What `run:entry` does on the way out of a stop it decided itself (joshuafolkken/kit#3099).
|
|
6
|
+
//
|
|
7
|
+
// **The stop's chores used to be the agent's to remember, and the `Stop` hook was what reminded it.**
|
|
8
|
+
// `fullrun.md` step 3 asked the agent, on an `over` budget, to send the `confirmation` Telegram and then
|
|
9
|
+
// run `pnpm josh run:release` — two calls after a command had already decided the run was over. Across
|
|
10
|
+
// eleven measured sessions the hook sent a reply back five times for exactly those two calls, each one a
|
|
11
|
+
// further turn re-reading the whole context. The command that decides the stop knows everything the
|
|
12
|
+
// chores need, so it does them, and the hook stays a safety net that a normal run never trips.
|
|
13
|
+
//
|
|
14
|
+
// **The release is the claim's own undo, and only where this call made the claim.** A resume over a
|
|
15
|
+
// stopped `halfrun` / `prrun` stops on the budget before adopting anything, and the hold there is the
|
|
16
|
+
// stopped run's — kept on purpose over its verified diff — so that stop notifies and releases nothing.
|
|
17
|
+
// A `busy` / `unknown` hold is another run's, or none at all, so it is never released here either.
|
|
18
|
+
//
|
|
19
|
+
// **A dispatched lane child sends no Telegram from here.** Its stop must park the Issue before it
|
|
20
|
+
// notifies, and `lane_park`'s guard delivers that rule at the `josh notify` call — a Telegram sent from
|
|
21
|
+
// inside this command would pass that guard by and satisfy the `Stop` hook with no park behind it.
|
|
22
|
+
|
|
23
|
+
interface StopNotice {
|
|
24
|
+
issue_number: string
|
|
25
|
+
command: string
|
|
26
|
+
reason: string
|
|
27
|
+
should_release: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StopPorts {
|
|
31
|
+
notify: (input: { issue_title: string; body: string; recovery: string }) => Promise<boolean>
|
|
32
|
+
release: (issue_number: string) => Promise<boolean>
|
|
33
|
+
is_parking_child: () => boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const RECOVERY = 'If this did not arrive, the run:entry output in the session names the stop.'
|
|
37
|
+
const NOTIFY_FAILED_NOTE =
|
|
38
|
+
'(the confirmation Telegram could not be sent — send it with `pnpm josh notify --task-type confirmation` before stopping)'
|
|
39
|
+
const RELEASED_NOTE = '(hold released by run:entry — the tree is free for the resume)'
|
|
40
|
+
const RELEASE_FAILED_NOTE =
|
|
41
|
+
'(the hold could not be released — run `pnpm josh run:release` before stopping)'
|
|
42
|
+
const should_forward_stderr = true
|
|
43
|
+
const SUCCESS_EXIT_CODE = 0
|
|
44
|
+
|
|
45
|
+
const BUDGET_REASON = 'the session budget is spent'
|
|
46
|
+
const HOLD_REASON = 'the working tree is not held for this run — see the run:entry output'
|
|
47
|
+
|
|
48
|
+
async function release(issue_number: string): Promise<boolean> {
|
|
49
|
+
const released = await josh_command.josh_run(['run:release', issue_number], should_forward_stderr)
|
|
50
|
+
|
|
51
|
+
return released.code === SUCCESS_EXIT_CODE
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function is_parking_child(): boolean {
|
|
55
|
+
return lane_park.is_parking_child()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DEFAULT_PORTS: StopPorts = { notify: telegram_notify.confirm, release, is_parking_child }
|
|
59
|
+
|
|
60
|
+
function body_of(notice: StopNotice): string {
|
|
61
|
+
return `${notice.command} #${notice.issue_number} stopped at entry: ${notice.reason}.\nResume with \`${notice.command} #${notice.issue_number}\`.`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The release runs first, so the resume the Telegram names finds the tree free when a person acts on it.
|
|
65
|
+
// The marker line is printed only for a Telegram that went out: the `Stop` hook reads it as the notify,
|
|
66
|
+
// and a failed send has to leave the hook still asking for one. The released note, likewise, is printed
|
|
67
|
+
// only for a release that succeeded, so a failed one leaves the agent still asked to release.
|
|
68
|
+
async function release_claim(notice: StopNotice, ports: StopPorts): Promise<void> {
|
|
69
|
+
if (!notice.should_release) return
|
|
70
|
+
|
|
71
|
+
const is_released = await ports.release(notice.issue_number)
|
|
72
|
+
|
|
73
|
+
console.info(is_released ? RELEASED_NOTE : RELEASE_FAILED_NOTE)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function notify_stop(notice: StopNotice, ports: StopPorts): Promise<void> {
|
|
77
|
+
if (ports.is_parking_child()) return
|
|
78
|
+
|
|
79
|
+
const is_sent = await ports.notify({
|
|
80
|
+
issue_title: `${notice.command} #${notice.issue_number} stopped`,
|
|
81
|
+
body: body_of(notice),
|
|
82
|
+
recovery: RECOVERY,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
console.info(is_sent ? lane_park.COMMAND_NOTIFY_MARKER : NOTIFY_FAILED_NOTE)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function stop(notice: StopNotice, ports: StopPorts = DEFAULT_PORTS): Promise<void> {
|
|
89
|
+
await release_claim(notice, ports)
|
|
90
|
+
await notify_stop(notice, ports)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const run_entry_stop = {
|
|
94
|
+
BUDGET_REASON,
|
|
95
|
+
HOLD_REASON,
|
|
96
|
+
NOTIFY_FAILED_NOTE,
|
|
97
|
+
RELEASED_NOTE,
|
|
98
|
+
RELEASE_FAILED_NOTE,
|
|
99
|
+
body_of,
|
|
100
|
+
stop,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { run_entry_stop }
|
|
104
|
+
export type { StopNotice, StopPorts }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
1
2
|
import { run_carry } from './run-carry'
|
|
2
3
|
import { run_event_scope } from './run-event-scope'
|
|
3
4
|
import { run_event_stream, type RunEvent } from './run-event-stream'
|
|
@@ -62,8 +63,10 @@ async function emit_once(kind: string, text: string): Promise<boolean> {
|
|
|
62
63
|
if (target === undefined || run_event_stream.read_last(target)?.kind === kind) return false
|
|
63
64
|
|
|
64
65
|
return run_event_stream.append(target, kind, text, now_iso()).appended
|
|
65
|
-
} catch {
|
|
66
|
+
} catch (error) {
|
|
66
67
|
// Best-effort: a failed append is dropped rather than raised into the loop's work.
|
|
68
|
+
error_text.trace_swallowed('run_event_stream_emit.emit_once', error)
|
|
69
|
+
|
|
67
70
|
return false
|
|
68
71
|
}
|
|
69
72
|
}
|
|
@@ -98,8 +101,10 @@ async function emit_once_since(kind: string, text: string, reset_kind: string):
|
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
return run_event_stream.append(target, kind, text, now_iso()).appended
|
|
101
|
-
} catch {
|
|
104
|
+
} catch (error) {
|
|
102
105
|
// Best-effort: a failed append is dropped rather than raised into the caller's work.
|
|
106
|
+
error_text.trace_swallowed('run_event_stream_emit.emit_once_since', error)
|
|
107
|
+
|
|
103
108
|
return false
|
|
104
109
|
}
|
|
105
110
|
}
|
package/scripts/run/run-hold.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { git_command } from '#scripts/git/git-command'
|
|
2
2
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
3
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
3
4
|
import { z } from 'zod'
|
|
4
5
|
|
|
5
6
|
// joshuafolkken/kit#1091: `epicrun` asks `epic-busy.ts` whether a *repository* already has a child in
|
|
@@ -196,7 +197,9 @@ async function is_tree_dirty(directory?: string): Promise<boolean> {
|
|
|
196
197
|
const status = await git_command.status(directory)
|
|
197
198
|
|
|
198
199
|
return status.trim() !== ''
|
|
199
|
-
} catch {
|
|
200
|
+
} catch (error) {
|
|
201
|
+
error_text.trace_swallowed('run_hold.is_tree_dirty', error)
|
|
202
|
+
|
|
200
203
|
return true
|
|
201
204
|
}
|
|
202
205
|
}
|
|
@@ -89,10 +89,6 @@ interface ParsedArguments {
|
|
|
89
89
|
values: ParsedValues
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function read_arguments(argv: ReadonlyArray<string>): ParsedArguments | undefined {
|
|
93
|
-
return cli_flags.parse_or_undefined({ args: [...argv], options: OPTIONS, allowPositionals: true })
|
|
94
|
-
}
|
|
95
|
-
|
|
96
92
|
function is_valid_target(parsed: ParsedArguments): boolean {
|
|
97
93
|
return (
|
|
98
94
|
parsed.positionals.length === 1 &&
|
|
@@ -136,7 +132,7 @@ function to_request(parsed: ParsedArguments): LivenessRequest {
|
|
|
136
132
|
}
|
|
137
133
|
|
|
138
134
|
function parse_request(argv: ReadonlyArray<string>): LivenessRequest | undefined {
|
|
139
|
-
const parsed =
|
|
135
|
+
const parsed = cli_flags.arguments_of(argv, OPTIONS)
|
|
140
136
|
|
|
141
137
|
if (parsed === undefined || !is_valid(parsed)) return undefined
|
|
142
138
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
3
|
import { api_outage } from '#scripts/agent/api-outage'
|
|
5
4
|
import { CONTEXT_CUT_THRESHOLD } from '#scripts/cost-runtime/context-cut-threshold'
|
|
6
5
|
import { issue_closing_pr } from '#scripts/issue/issue-closing-pr'
|
|
7
6
|
import { issue_state_cli } from '#scripts/issue/issue-state-cli'
|
|
8
7
|
import { lane_handoff } from '#scripts/lane/lane-handoff'
|
|
8
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
9
9
|
import { run_carry, type CarryOwner, type RunCarry } from './run-carry'
|
|
10
10
|
import { run_ending } from './run-ending'
|
|
11
11
|
import { run_event_stream } from './run-event-stream'
|
|
@@ -69,14 +69,6 @@ interface ParsedArguments {
|
|
|
69
69
|
values: Partial<Record<'epic' | 'output' | 'over' | 'owner' | 'repo', string>>
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function read_args(argv: ReadonlyArray<string>): ParsedArguments | undefined {
|
|
73
|
-
try {
|
|
74
|
-
return parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true, strict: true })
|
|
75
|
-
} catch {
|
|
76
|
-
return undefined
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
72
|
// Absent owner is "no owner declared", which the carry guard reads as not provably foreign; a present
|
|
81
73
|
// but malformed pid invalidates the invocation rather than silently counting unguarded.
|
|
82
74
|
function to_owner(raw: string | undefined): CarryOwner | undefined {
|
|
@@ -170,7 +162,7 @@ function to_context(parsed: ParsedArguments): MergeContext | undefined {
|
|
|
170
162
|
}
|
|
171
163
|
|
|
172
164
|
function parse(argv: ReadonlyArray<string>): MergeContext | undefined {
|
|
173
|
-
const parsed =
|
|
165
|
+
const parsed = cli_flags.arguments_of(argv, OPTIONS)
|
|
174
166
|
|
|
175
167
|
return parsed === undefined ? undefined : to_context(parsed)
|
|
176
168
|
}
|