@joshuafolkken/kit 1.921.0 → 1.947.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/workflow-commands/background-commands.md +6 -0
- package/.claude/skills/workflow-commands/backlogrun-child.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun-progress.md +13 -12
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -1
- package/.claude/skills/workflow-commands/observation-filing.md +3 -3
- package/.claude/skills/workflow-commands/prerequisite.md +1 -1
- package/.github/workflows/ci.yml +26 -8
- package/.github/workflows/pr-classification.yml +76 -0
- package/README.md +47 -21
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-LC765SUA.js → chunk-3FWV6CFC.js} +119 -20
- package/dist/hooks/{chunk-UULARQ5Q.js → chunk-EYFJ74MJ.js} +13 -15
- package/dist/hooks/{chunk-O2FBZPSW.js → chunk-GIX5SAAB.js} +248 -231
- package/dist/hooks/{chunk-OLTAGGHH.js → chunk-IA7WC4NS.js} +1 -1
- package/dist/hooks/{chunk-UANRLZV7.js → chunk-IBFC25IX.js} +4 -4
- package/dist/hooks/{chunk-XMR3BS35.js → chunk-M4U4YNRE.js} +3 -3
- package/dist/hooks/{chunk-NWD47MW3.js → chunk-NKVLBB65.js} +3 -3
- package/dist/hooks/{chunk-OYU6KPRA.js → chunk-O3PEDF3M.js} +2 -2
- package/dist/hooks/{chunk-3HZ5MJIW.js → chunk-OM5VDM6F.js} +2 -2
- package/dist/hooks/{chunk-SSX23SJ2.js → chunk-RKUAD3I6.js} +1 -0
- package/dist/hooks/{chunk-77RLKZ5V.js → chunk-V7RH56HU.js} +2 -2
- package/dist/hooks/{chunk-FXAKJKTB.js → chunk-WPALEIRX.js} +1 -1
- package/dist/hooks/codex-hook-adapter.js +12 -12
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +3 -3
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +11 -11
- package/dist/hooks/session-lang.js +2 -2
- package/dist/hooks/stop-guard.js +11 -5
- package/dist/josh.js +12 -14
- package/eslint/base.js +4 -1
- package/package.json +21 -6
- package/playwright.config.ts +9 -17
- package/ports/index.d.ts +1 -0
- package/ports/index.js +1 -0
- package/ports/web-server.d.ts +5 -0
- package/ports/web-server.js +73 -0
- package/prompts/collaboration-workflow/issue-template.md +1 -1
- package/scripts/agent/agent-role-profile.ts +11 -0
- package/scripts/ci/pr-classification.ts +27 -6
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/gate/hook-gate-reuse.ts +1 -1
- package/scripts/gate/project-checks.ts +60 -6
- package/scripts/gate/type-check-command.ts +43 -0
- package/scripts/git/constants.ts +4 -0
- package/scripts/git/git-command.ts +11 -5
- package/scripts/git/git-gh-pr-rollup.ts +56 -16
- package/scripts/git/git-staging.ts +6 -4
- package/scripts/git/git-stash.ts +73 -1
- package/scripts/git/stash-orphans.ts +24 -2
- package/scripts/git/stash-sweep-lock.ts +141 -0
- package/scripts/hooks/stop-guard.ts +2 -0
- package/scripts/init/init-actions.ts +3 -2
- package/scripts/init/init-ai-copy.ts +8 -5
- package/scripts/init/init-logic-migrate.ts +10 -1
- package/scripts/init/init-logic-templates.ts +27 -7
- package/scripts/init/init-logic-workspace.ts +89 -5
- package/scripts/init/init-logic.ts +7 -1
- package/scripts/init/init-static.ts +4 -1
- package/scripts/init/init.ts +60 -19
- package/scripts/init/project-profile.ts +14 -5
- package/scripts/issue/issue-merged.ts +75 -0
- package/scripts/issue/measure-rerun-cli.ts +2 -2
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +10 -0
- package/scripts/josh/josh-command-map.ts +1 -0
- package/scripts/josh/josh-command-types.ts +0 -3
- package/scripts/josh/josh-commands-ai.ts +6 -0
- package/scripts/josh/josh-commands-development.ts +6 -19
- package/scripts/lint/format.ts +68 -0
- package/scripts/lint/lint-parallel.ts +23 -36
- package/scripts/observations/observation-ledger-home.ts +22 -2
- package/scripts/observations/observation-ledger-line.ts +11 -0
- package/scripts/observations/observation-ledger-migrate.ts +146 -0
- package/scripts/observations/observation-ledger.ts +51 -3
- package/scripts/observations/observations-flush.ts +38 -7
- package/scripts/review/review-finding-ledger.ts +1 -1
- package/scripts/review/review-record-cli.ts +1 -7
- package/scripts/rules/delivered-rules.ts +10 -0
- package/scripts/rules/lane-background.ts +111 -0
- package/scripts/rules/stop-rules.ts +15 -2
- package/scripts/run/run-carry-flush.ts +2 -7
- package/scripts/run/run-hold-cli.ts +10 -2
- package/scripts/run/run-step-cli.ts +11 -1
- package/scripts/run/run-step.ts +36 -8
- package/scripts/run/run-tidy-cli.ts +50 -0
- package/scripts/run/run-tidy-lanes.ts +110 -0
- package/scripts/run/run-tidy-stashes.ts +160 -0
- package/scripts/run/run-tidy.ts +101 -0
- package/scripts/safe-chain/ci-installer-pin.ts +90 -0
- package/scripts/safe-chain/preinstall-command.ts +39 -0
- package/scripts/sync/sync.ts +5 -2
- package/scripts/test/pilot-files.ts +0 -1
- package/scripts/version/preinstall-version-update.ts +2 -0
- package/templates/pnpm-workspace.static.yaml +4 -0
- package/templates/workflows/ci.yml +21 -7
- package/tsconfig/base.json +4 -1
|
@@ -4,7 +4,21 @@
|
|
|
4
4
|
// `observations-flush.ts` is the only thing that stages it — so a literal in each would be three
|
|
5
5
|
// answers to one question, and a rename reaching only some of them would silently restore the
|
|
6
6
|
// contamination the exclusion exists to prevent.
|
|
7
|
-
|
|
7
|
+
//
|
|
8
|
+
// **It sits under `docs/maintainers/`, apart from the reader-facing documents** (joshuafolkken/kit#2724).
|
|
9
|
+
const OBSERVATION_LEDGER_PATH = 'docs/maintainers/observations.md'
|
|
10
|
+
|
|
11
|
+
// Where the ledger lived before joshuafolkken/kit#2724, kept as a migration source rather than
|
|
12
|
+
// forgotten: a run still on the old code, a stash cut before the move and a consumer repository's
|
|
13
|
+
// existing ledger all write here, and `observation-ledger-migrate.ts` moves what it finds to the path
|
|
14
|
+
// above. Every place that recognizes the ledger recognizes both, so a line on either path is never
|
|
15
|
+
// staged into an ordinary commit. Remove it once every repository holding a ledger has migrated.
|
|
16
|
+
const LEGACY_OBSERVATION_LEDGER_PATH = 'docs/observations.md'
|
|
17
|
+
|
|
18
|
+
const OBSERVATION_LEDGER_PATHS: ReadonlyArray<string> = [
|
|
19
|
+
OBSERVATION_LEDGER_PATH,
|
|
20
|
+
LEGACY_OBSERVATION_LEDGER_PATH,
|
|
21
|
+
]
|
|
8
22
|
|
|
9
23
|
// `git status --porcelain` prints `XY PATH`, so the path starts at the fourth character.
|
|
10
24
|
const STATUS_PATH_INDEX = 3
|
|
@@ -13,8 +27,34 @@ function status_path(line: string): string {
|
|
|
13
27
|
return line.slice(STATUS_PATH_INDEX).trim()
|
|
14
28
|
}
|
|
15
29
|
|
|
30
|
+
// The suffix of the file a migration renames the old ledger to while it copies it
|
|
31
|
+
// (`observation-ledger-migrate.ts`): `docs/observations.md.<pid>.migrating`.
|
|
32
|
+
const MIGRATION_CLAIM_SUFFIX = '.migrating'
|
|
33
|
+
|
|
34
|
+
// A claim left by a process killed mid-migration holds ledger lines, so it is the ledger too: never
|
|
35
|
+
// staged by an ordinary commit, and absorbed by the next migration.
|
|
36
|
+
function is_migration_claim(file_path: string): boolean {
|
|
37
|
+
return (
|
|
38
|
+
file_path.startsWith(`${LEGACY_OBSERVATION_LEDGER_PATH}.`) &&
|
|
39
|
+
file_path.endsWith(MIGRATION_CLAIM_SUFFIX)
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function is_ledger_path(file_path: string): boolean {
|
|
44
|
+
return OBSERVATION_LEDGER_PATHS.includes(file_path) || is_migration_claim(file_path)
|
|
45
|
+
}
|
|
46
|
+
|
|
16
47
|
function is_ledger_line(line: string): boolean {
|
|
17
|
-
return status_path(line)
|
|
48
|
+
return is_ledger_path(status_path(line))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The ledger paths a porcelain status names — what a flush stages after a migration, which may be the
|
|
52
|
+
// new file together with the old one's deletion.
|
|
53
|
+
function ledger_paths(status_output: string): ReadonlyArray<string> {
|
|
54
|
+
return status_output
|
|
55
|
+
.split('\n')
|
|
56
|
+
.filter((line) => is_ledger_line(line))
|
|
57
|
+
.map((line) => status_path(line))
|
|
18
58
|
}
|
|
19
59
|
|
|
20
60
|
// **The one place that answers "does this working tree hold a pending observation append?"**
|
|
@@ -29,7 +69,15 @@ function has_pending_append(status_output: string): boolean {
|
|
|
29
69
|
const observation_ledger = {
|
|
30
70
|
has_pending_append,
|
|
31
71
|
is_ledger_line,
|
|
72
|
+
is_ledger_path,
|
|
73
|
+
ledger_paths,
|
|
32
74
|
status_path,
|
|
33
75
|
}
|
|
34
76
|
|
|
35
|
-
export {
|
|
77
|
+
export {
|
|
78
|
+
LEGACY_OBSERVATION_LEDGER_PATH,
|
|
79
|
+
MIGRATION_CLAIM_SUFFIX,
|
|
80
|
+
OBSERVATION_LEDGER_PATH,
|
|
81
|
+
OBSERVATION_LEDGER_PATHS,
|
|
82
|
+
observation_ledger,
|
|
83
|
+
}
|
|
@@ -4,6 +4,7 @@ import { git_gh_command } from '#scripts/git/git-gh-command'
|
|
|
4
4
|
import { main_sync } from '#scripts/git/main-sync'
|
|
5
5
|
import { observation_ledger, OBSERVATION_LEDGER_PATH } from './observation-ledger'
|
|
6
6
|
import { observation_ledger_line, type BrokenLine } from './observation-ledger-line'
|
|
7
|
+
import { observation_ledger_migrate } from './observation-ledger-migrate'
|
|
7
8
|
import { observations_flush_landing } from './observations-flush-landing'
|
|
8
9
|
|
|
9
10
|
// The commit path the observation ledger did not have (joshuafolkken/kit#1756). The parent session
|
|
@@ -78,7 +79,9 @@ function has_ledger_change(status_output: string): boolean {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function other_changed_paths(status_output: string): ReadonlyArray<string> {
|
|
81
|
-
return status_paths(status_output).filter(
|
|
82
|
+
return status_paths(status_output).filter(
|
|
83
|
+
(file_path) => !observation_ledger.is_ledger_path(file_path),
|
|
84
|
+
)
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
function message_of(error: unknown): string {
|
|
@@ -295,9 +298,21 @@ function rejection_message(
|
|
|
295
298
|
return roll_back_failure_message(branch_name, reason, roll_back_reason)
|
|
296
299
|
}
|
|
297
300
|
|
|
301
|
+
// **Every ledger path the status names is staged, not the one constant** (joshuafolkken/kit#2724).
|
|
302
|
+
// After a migration the change is the new file together with the old one's deletion, and a commit
|
|
303
|
+
// holding only the first would leave the deletion behind as a change no later flush could tell apart
|
|
304
|
+
// from a fresh append.
|
|
305
|
+
async function stage_ledger(): Promise<void> {
|
|
306
|
+
const ledger_paths = observation_ledger.ledger_paths(await git_command.status())
|
|
307
|
+
|
|
308
|
+
for (const file_path of ledger_paths) {
|
|
309
|
+
await git_command.add_path(file_path)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
298
313
|
async function commit_ledger(branch_name: string, default_branch: string): Promise<void> {
|
|
299
314
|
try {
|
|
300
|
-
await
|
|
315
|
+
await stage_ledger()
|
|
301
316
|
await git_command.commit(COMMIT_MESSAGE)
|
|
302
317
|
} catch (error) {
|
|
303
318
|
const roll_back_reason = await roll_back(branch_name, default_branch)
|
|
@@ -316,7 +331,7 @@ async function open_pull_request(branch_name: string, default_branch: string): P
|
|
|
316
331
|
}
|
|
317
332
|
|
|
318
333
|
// **The checkout goes back to the default branch only on the way that merged.** A `finally` here
|
|
319
|
-
// would check the default branch out after a red check too — and
|
|
334
|
+
// would check the default branch out after a red check too — and the ledger would then
|
|
320
335
|
// revert to main's content, leaving the appended lines on a branch nothing in this checkout points
|
|
321
336
|
// at any more. The next flush would read `has_ledger_change` as false and report `clean`, which is
|
|
322
337
|
// the silent loss the refusals above exist to prevent. Left on the branch, the refusal fires.
|
|
@@ -359,14 +374,30 @@ function broken_lines_message(broken: ReadonlyArray<BrokenLine>): string {
|
|
|
359
374
|
// **A malformed line is refused before a branch is cut, not after.** The ledger path was collected
|
|
360
375
|
// without a line ever being parsed (joshuafolkken/kit#2123), so a broken append could ride a flush
|
|
361
376
|
// through; validating here keeps the committed ledger to its grammar.
|
|
377
|
+
//
|
|
378
|
+
// **A ledger that is not there has no line to break** — the change the status saw is then its
|
|
379
|
+
// deletion, and a flush commits that like any other change to it.
|
|
380
|
+
async function read_ledger(): Promise<string> {
|
|
381
|
+
try {
|
|
382
|
+
return await readFile(OBSERVATION_LEDGER_PATH, 'utf8')
|
|
383
|
+
} catch {
|
|
384
|
+
return ''
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
362
388
|
async function refuse_broken_ledger(): Promise<void> {
|
|
363
|
-
const broken = observation_ledger_line.broken_ledger_lines(
|
|
364
|
-
await readFile(OBSERVATION_LEDGER_PATH, 'utf8'),
|
|
365
|
-
)
|
|
389
|
+
const broken = observation_ledger_line.broken_ledger_lines(await read_ledger())
|
|
366
390
|
|
|
367
391
|
if (broken.length > 0) throw new Error(broken_lines_message(broken))
|
|
368
392
|
}
|
|
369
393
|
|
|
394
|
+
// Lines still on the ledger's old path are moved before anything is validated or staged
|
|
395
|
+
// (joshuafolkken/kit#2724), so the flush commits the move and the lines together.
|
|
396
|
+
async function prepare_ledger(): Promise<void> {
|
|
397
|
+
observation_ledger_migrate.migrate(process.cwd())
|
|
398
|
+
await refuse_broken_ledger()
|
|
399
|
+
}
|
|
400
|
+
|
|
370
401
|
// **Nothing pulls in front of the branch, and that is deliberate.** The ledger is dirty by
|
|
371
402
|
// definition at this point, so `git pull --ff-only` aborts on it in exactly the case a pull would
|
|
372
403
|
// have been for — an upstream flush that already advanced the ledger — and reports a failure about
|
|
@@ -382,7 +413,7 @@ async function flush(now: Date): Promise<string> {
|
|
|
382
413
|
|
|
383
414
|
if (!has_ledger_change(status_output)) return CLEAN_MESSAGE
|
|
384
415
|
|
|
385
|
-
await
|
|
416
|
+
await prepare_ledger()
|
|
386
417
|
|
|
387
418
|
const deferred = await observations_flush_landing.refuse_open_flush(BRANCH_PREFIX)
|
|
388
419
|
if (deferred !== undefined) return deferred
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The review-finding line's grammar, its category vocabulary and the aggregation over it — the single
|
|
2
2
|
// place all three are defined (joshuafolkken/kit#2325). A `/code-review` round's findings evaporated
|
|
3
3
|
// the moment they were fixed, so no one could say which category recurred. They now land in the same
|
|
4
|
-
// append-only ledger the observation lines use (`docs/observations.md`), under a distinct `- rf:`
|
|
4
|
+
// append-only ledger the observation lines use (`docs/maintainers/observations.md`), under a distinct `- rf:`
|
|
5
5
|
// prefix so `observation-ledger-line.ts`'s `- k:` grammar never treats them as its own — one ledger
|
|
6
6
|
// file, one flush path, a second line *type* rather than a second ledger.
|
|
7
7
|
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
-
import { appendFile, mkdir } from 'node:fs/promises'
|
|
3
|
-
import path from 'node:path'
|
|
4
2
|
import { fileURLToPath } from 'node:url'
|
|
5
3
|
import { parseArgs } from 'node:util'
|
|
6
4
|
import { observation_ledger_home } from '#scripts/observations/observation-ledger-home'
|
|
@@ -154,11 +152,7 @@ async function run_record(parsed: Parsed, now: Date, ledger_path: string): Promi
|
|
|
154
152
|
|
|
155
153
|
const lines = build_lines(request, today(now))
|
|
156
154
|
|
|
157
|
-
|
|
158
|
-
// below would fail with ENOENT and leave the merge gate unsatisfiable (joshuafolkken/kit#2402).
|
|
159
|
-
// Creating the parent first is the one write path that can bring the ledger into existence there.
|
|
160
|
-
await mkdir(path.dirname(ledger_path), { recursive: true })
|
|
161
|
-
await appendFile(ledger_path, `${lines.join('\n')}\n`, 'utf8')
|
|
155
|
+
await observation_ledger_home.append(ledger_path, lines)
|
|
162
156
|
console.info(confirmation(lines.length, ledger_path))
|
|
163
157
|
|
|
164
158
|
return 0
|
|
@@ -13,6 +13,7 @@ import { issue_bug_label_rule } from './issue-bug-label'
|
|
|
13
13
|
import { issue_fold } from './issue-fold'
|
|
14
14
|
import { issue_scout } from './issue-scout'
|
|
15
15
|
import { josh_git_bare } from './josh-git-bare'
|
|
16
|
+
import { lane_background } from './lane-background'
|
|
16
17
|
import { lane_carry_conflict } from './lane-carry-conflict'
|
|
17
18
|
import { lane_interactive_ask } from './lane-interactive-ask'
|
|
18
19
|
import { lane_park } from './lane-park'
|
|
@@ -360,6 +361,15 @@ const DELIVERED_RULES: ReadonlyArray<DeliveredRule> = [
|
|
|
360
361
|
// verdict is `required`; in the hermetic non-repo suite the git read fails to `exempt`, so
|
|
361
362
|
// `test-declared` claims nothing and the "exactly one rule" invariant over `run-tail`'s fixture holds.
|
|
362
363
|
test_declared_commit.ROW,
|
|
364
|
+
// **The long-running josh command a lane child backgrounds and loses** (joshuafolkken/kit#2704). A
|
|
365
|
+
// headless child's background tasks die with its turn, so a backgrounded `josh gate` / `git` /
|
|
366
|
+
// `followup` is killed before it reports; this refuses the launch and hands back `josh ship
|
|
367
|
+
// --detach`, whose supervisor outlives the turn. **Listed before `run-tail`, whose push it claims in
|
|
368
|
+
// a child**: that row answers a foreground push with "reissue it backgrounded", the reissue this row
|
|
369
|
+
// refuses, so a child is sent to the detached ship on its first refusal instead of through both.
|
|
370
|
+
// Outside a child it claims nothing. A backgrounded bare `josh git` also trips `josh-git-bare`
|
|
371
|
+
// below; this row wins, and `ship --detach` satisfies both. It fires on every occurrence.
|
|
372
|
+
lane_background.ROW,
|
|
363
373
|
// **The one row whose trigger reads a field of the input beside the command**, so it supplies its
|
|
364
374
|
// own tool-name check rather than going through `on_bash_command`: a push step already issued with
|
|
365
375
|
// `run_in_background` is the rule obeyed, and refusing it would charge a run for doing the right
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { cost_blocks } from '#scripts/cost-runtime/cost-blocks'
|
|
2
|
+
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
3
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
4
|
+
import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
|
|
5
|
+
import { time_shell } from '#scripts/time-runtime/time-shell'
|
|
6
|
+
import { time_transcript_line } from '#scripts/time-runtime/time-transcript-line'
|
|
7
|
+
import { run_tail } from './run-tail'
|
|
8
|
+
|
|
9
|
+
// A dispatched lane child is stopped from backgrounding a long-running josh command (joshuafolkken/kit#2704).
|
|
10
|
+
//
|
|
11
|
+
// **A headless child's turn-end is its process's end.** A lane child runs as `claude -p`, and there the
|
|
12
|
+
// background Bash tasks die with the turn — so "a background command's exit re-invokes the session"
|
|
13
|
+
// (`background-commands.md`) does not hold. joshuafolkken/kit#2457 refused a non-detached `josh ship`
|
|
14
|
+
// for exactly that reason, but only `ship`: the #2606 child backgrounded `pnpm josh gate && pnpm josh git
|
|
15
|
+
// -y …` directly, ended its turn, and the gate died at `exit code 143` with the implementation, the
|
|
16
|
+
// scoped checks, a review round and the live verification all done — parked instead of merged.
|
|
17
|
+
//
|
|
18
|
+
// **So the refusal is put at the launch, and it names the detached route.** The region from the gate to
|
|
19
|
+
// the merge belongs to `pnpm josh ship --detach` in a child (`chain-rule.md` step 0), whose supervisor
|
|
20
|
+
// outlives the turn. `ship` itself is not in the set: #2457 already detaches it implicitly in a child.
|
|
21
|
+
//
|
|
22
|
+
// **Its stop-time half reads the same fact from the other side.** A backgrounded task the transcript
|
|
23
|
+
// launched but never saw finish is still running, and in a child the stop about to happen kills it —
|
|
24
|
+
// so the `Stop` hook sends the child back to wait or detach rather than asking for a notify
|
|
25
|
+
// (`stop-rules.ts`). The launch and finish ids are `time_transcript_line`'s, never re-parsed here.
|
|
26
|
+
|
|
27
|
+
// The josh subcommands that run long enough to be backgrounded, as `time_shell` names them — canonical,
|
|
28
|
+
// so the `ga` / `g` / `fu` aliases are covered by the one spelling.
|
|
29
|
+
const LONG_RUNNING_COMMANDS: ReadonlySet<string> = new Set(
|
|
30
|
+
['gate', 'git', 'followup'].map((name) => `${time_shell.JOSH_PREFIX}${name}`),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const BACKGROUND_KEY = 'run_in_background'
|
|
34
|
+
|
|
35
|
+
function is_backgrounded(input: unknown): boolean {
|
|
36
|
+
return json_value.is_record(input) && input[BACKGROUND_KEY] === true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runs_long_command(command: string): boolean {
|
|
40
|
+
return time_shell.josh_commands_of(command).some((name) => LONG_RUNNING_COMMANDS.has(name))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The call's own fields alone: a backgrounded Bash call that runs a long josh command, or the push step
|
|
44
|
+
// in either spelling. A child's foreground push is claimed too, because `run-tail` would otherwise answer
|
|
45
|
+
// it with "reissue it backgrounded" — the one reissue this row then refuses — so the child is handed the
|
|
46
|
+
// detached route on its first refusal rather than two contradictory ones.
|
|
47
|
+
function is_candidate(call: GuardedCall): boolean {
|
|
48
|
+
if (run_tail.is_push_step_call(call)) return true
|
|
49
|
+
if (call.name !== cost_blocks.BASH_TOOL || !is_backgrounded(call.input)) return false
|
|
50
|
+
|
|
51
|
+
return runs_long_command(time_shell.bash_command(call.input))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// True when this session is a dispatched lane child backgrounding a long-running josh command. The call's
|
|
55
|
+
// own fields are tested first, so the dispatch mark is read only on the handful of calls that match.
|
|
56
|
+
function is_background_long_run(
|
|
57
|
+
call: GuardedCall,
|
|
58
|
+
is_lane_child: () => boolean = () => lane_child_marker.is_child_of(process.cwd()),
|
|
59
|
+
): boolean {
|
|
60
|
+
return is_candidate(call) && is_lane_child()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The background tasks the transcript tail launched and never saw finish. A launch is read off its own
|
|
64
|
+
// tool result and a finish off the harness notice, both by `time_transcript_line`.
|
|
65
|
+
function pending_background_ids(tail: string): ReadonlyArray<string> {
|
|
66
|
+
const lines = tail.split('\n').map((line) => time_transcript_line.parse_line(line))
|
|
67
|
+
const finished = new Set(lines.map((line) => line?.finished_background ?? ''))
|
|
68
|
+
const launched = lines.flatMap((line) => line?.blocks.map((block) => block.background_id) ?? [])
|
|
69
|
+
|
|
70
|
+
return launched.filter((id) => id !== '' && !finished.has(id))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const LANE_BACKGROUND_REASON =
|
|
74
|
+
'⛔ lane background: this session is a dispatched lane child (`JOSH_LANE_CHILD`), a headless ' +
|
|
75
|
+
'`claude -p` process whose background Bash tasks are killed when the turn ends — so a backgrounded ' +
|
|
76
|
+
'`josh gate` / `josh git` / `josh followup` never re-invokes you; it dies at `exit code 143` ' +
|
|
77
|
+
'(joshuafolkken/kit#2704, measured on the #2606 child: implementation, review and live verification ' +
|
|
78
|
+
'done, the run parked instead of merged). Hand the gate-to-merge region to the detached supervisor ' +
|
|
79
|
+
'instead: `pnpm josh ship --detach --review "<title> #<N>"` (add `--cite <N>`), then end the turn on ' +
|
|
80
|
+
'`launched` / `busy` — `.claude/skills/workflow-commands/chain-rule.md` step 0 is the single source. A ' +
|
|
81
|
+
'single check you must read before continuing runs in the foreground within the tool timeout; the ' +
|
|
82
|
+
'commit-push step does not — in a child it too belongs to the detached ship, in either spelling. This ' +
|
|
83
|
+
'rule fires on every occurrence, not once per run.'
|
|
84
|
+
|
|
85
|
+
const LANE_BACKGROUND_STOP_REASON =
|
|
86
|
+
'⛔ lane background stop: this is a dispatched lane child (`JOSH_LANE_CHILD`) and a background task ' +
|
|
87
|
+
'it launched has not finished — ending this headless turn kills it, and nothing re-invokes you ' +
|
|
88
|
+
'afterwards (joshuafolkken/kit#2704). Do not send a notification and do not stop: wait for the task ' +
|
|
89
|
+
'in the foreground and act on its result, or re-issue the work through `pnpm josh ship --detach`, ' +
|
|
90
|
+
'whose supervisor outlives the turn (`.claude/skills/workflow-commands/chain-rule.md` step 0). Do ' +
|
|
91
|
+
'not repeat your previous reply.'
|
|
92
|
+
|
|
93
|
+
// The row itself, so `delivered-rules.ts` spreads one entry. It fires on every occurrence (`decide`
|
|
94
|
+
// returns true): a child must never background these, so the route is always the detached ship, never
|
|
95
|
+
// a reissue. It declares no `keeps`: the detached ship is a different command, not this call made right.
|
|
96
|
+
const ROW = {
|
|
97
|
+
id: 'lane-background',
|
|
98
|
+
is_trigger: (call: GuardedCall): boolean => is_background_long_run(call),
|
|
99
|
+
reason: LANE_BACKGROUND_REASON,
|
|
100
|
+
decide: (): boolean => true,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const lane_background = {
|
|
104
|
+
LANE_BACKGROUND_REASON,
|
|
105
|
+
LANE_BACKGROUND_STOP_REASON,
|
|
106
|
+
ROW,
|
|
107
|
+
is_background_long_run,
|
|
108
|
+
pending_background_ids,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { lane_background }
|
|
@@ -2,6 +2,7 @@ import { hook_decision } from '#scripts/josh/hook-decision'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { filing_offer } from './filing-offer'
|
|
4
4
|
import { issue_citation } from './issue-citation'
|
|
5
|
+
import { lane_background } from './lane-background'
|
|
5
6
|
import { reply_language } from './reply-language'
|
|
6
7
|
|
|
7
8
|
// The stop-time rules, delivered on the `Stop` hook (joshuafolkken/kit#2121, joshuafolkken/kit#2422,
|
|
@@ -66,6 +67,9 @@ interface StopContext {
|
|
|
66
67
|
headless_refusals: number
|
|
67
68
|
// This session is a dispatched lane child for this checkout — `lane_child_marker.is_child_of`.
|
|
68
69
|
lane_child: boolean
|
|
70
|
+
// A background task this run launched has not finished — `lane_background.pending_background_ids`
|
|
71
|
+
// over the transcript tail (joshuafolkken/kit#2704).
|
|
72
|
+
background_pending: boolean
|
|
69
73
|
// The resolved `JOSH_SESSION_LANG` — `session_language.resolve_session_lang` (joshuafolkken/kit#2470).
|
|
70
74
|
session_lang: string
|
|
71
75
|
}
|
|
@@ -256,10 +260,19 @@ function count_headless_refusals(tail: string): number {
|
|
|
256
260
|
return tail.slice(Math.max(last_wait, 0)).split(HEADLESS_WAIT_MARKER).length - 1
|
|
257
261
|
}
|
|
258
262
|
|
|
259
|
-
//
|
|
260
|
-
//
|
|
263
|
+
// **A lane child with a task still running is sent back to wait, not to notify** (joshuafolkken/kit#2704).
|
|
264
|
+
// Its turn-end kills the task, so the notify the hold rules would ask for announces a stop that loses
|
|
265
|
+
// the work; the #2606 child obeyed exactly that and its gate died at `exit code 143`.
|
|
266
|
+
function needs_background_wait(context: StopContext): boolean {
|
|
267
|
+
return context.lane_child && context.background_pending
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// The two hold rules, notify ahead of release — behind the lane index route (joshuafolkken/kit#2445)
|
|
271
|
+
// and the lane background wait (joshuafolkken/kit#2704), since the notify they would ask for announces
|
|
272
|
+
// a pause the run has no reason to take.
|
|
261
273
|
function hold_reason(context: StopContext): string | undefined {
|
|
262
274
|
if (needs_index_route(context)) return LANE_INDEX_REASON
|
|
275
|
+
if (needs_background_wait(context)) return lane_background.LANE_BACKGROUND_STOP_REASON
|
|
263
276
|
if (needs_notify(context)) return STOP_NOTIFY_REASON
|
|
264
277
|
if (needs_release(context)) return HOLD_RELEASE_REASON
|
|
265
278
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PORCELAIN_FLAG, UNTRACKED_FILES_FLAG } from '#scripts/git/constants'
|
|
1
2
|
import { git_spawn } from '#scripts/git/git-spawn'
|
|
2
3
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
3
4
|
import { observation_ledger } from '#scripts/observations/observation-ledger'
|
|
@@ -31,13 +32,7 @@ const FAILURE_NOTE =
|
|
|
31
32
|
|
|
32
33
|
async function has_pending_append(): Promise<boolean> {
|
|
33
34
|
const root = observation_ledger_home.ledger_root()
|
|
34
|
-
const status = await git_spawn.read([
|
|
35
|
-
'-C',
|
|
36
|
-
root,
|
|
37
|
-
'status',
|
|
38
|
-
'--porcelain',
|
|
39
|
-
'--untracked-files=normal',
|
|
40
|
-
])
|
|
35
|
+
const status = await git_spawn.read(['-C', root, 'status', PORCELAIN_FLAG, UNTRACKED_FILES_FLAG])
|
|
41
36
|
|
|
42
37
|
return observation_ledger.has_pending_append(status)
|
|
43
38
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { run_hold, type HoldRead, type RunHold } from './run-hold'
|
|
4
4
|
import { run_preflight, type PreflightDecision } from './run-preflight'
|
|
5
|
+
import { run_tidy_cli } from './run-tidy-cli'
|
|
5
6
|
|
|
6
7
|
// `josh run:hold [<N>]` and `josh run:release [<N> | --force]` — the working-tree guard the typed
|
|
7
8
|
// entry points ask before they start (joshuafolkken/kit#1091).
|
|
@@ -28,6 +29,10 @@ import { run_preflight, type PreflightDecision } from './run-preflight'
|
|
|
28
29
|
// The contract is `epic:next`'s: **standard output carries exactly one token** — `hold`, `busy` or
|
|
29
30
|
// `unknown` — and every explanation goes to standard error, so `answer=$(pnpm josh run:hold 1091)`
|
|
30
31
|
// captures something a loop can branch on.
|
|
32
|
+
//
|
|
33
|
+
// **A successful claim then sweeps merged residue** — `run:tidy` (joshuafolkken/kit#2701). After the
|
|
34
|
+
// claim rather than before it: the sweep may append to the primary checkout's observation ledger, and
|
|
35
|
+
// doing that ahead of the preflight would turn a clean primary checkout into a `reclaim`.
|
|
31
36
|
|
|
32
37
|
const SUCCESS_EXIT_CODE = 0
|
|
33
38
|
const FAILURE_EXIT_CODE = 1
|
|
@@ -181,9 +186,12 @@ async function claim(target: string, issue: string, is_linked: boolean): Promise
|
|
|
181
186
|
|
|
182
187
|
if (blocked !== undefined) return report_busy(blocked)
|
|
183
188
|
|
|
184
|
-
|
|
189
|
+
const code =
|
|
190
|
+
read.kind === 'stale' ? replace_stale(target, issue, read.hold) : take_free_tree(target, issue)
|
|
185
191
|
|
|
186
|
-
|
|
192
|
+
if (code === SUCCESS_EXIT_CODE) await run_tidy_cli.sweep()
|
|
193
|
+
|
|
194
|
+
return code
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
function remove_record(target: string, was_present: boolean): number {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { agent_role_profile } from '#scripts/agent/agent-role-profile'
|
|
3
4
|
import { doctor_consumer } from '#scripts/doctor/doctor-consumer'
|
|
4
5
|
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
5
6
|
import { find_package_directory } from '#scripts/josh/josh-logic'
|
|
@@ -29,6 +30,7 @@ const RETROSPECTIVE_ENV_KEY = 'JOSH_RETROSPECTIVE'
|
|
|
29
30
|
interface RunReads {
|
|
30
31
|
carry_kind: CarryRead['kind']
|
|
31
32
|
is_retrospective_done: boolean
|
|
33
|
+
is_at_cut_cap: boolean
|
|
32
34
|
last_event: string | undefined
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -36,7 +38,12 @@ interface RunReads {
|
|
|
36
38
|
// directory that cannot be read leaves the position unknowable, which `next_action` answers `unknown`.
|
|
37
39
|
function read_run(directory: string | undefined, issue_number: string): RunReads {
|
|
38
40
|
if (directory === undefined) {
|
|
39
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
carry_kind: 'unreadable',
|
|
43
|
+
is_retrospective_done: false,
|
|
44
|
+
is_at_cut_cap: false,
|
|
45
|
+
last_event: undefined,
|
|
46
|
+
}
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
const carry = run_carry.read_carry(run_carry.carry_path(directory))
|
|
@@ -55,6 +62,7 @@ function read_run(directory: string | undefined, issue_number: string): RunReads
|
|
|
55
62
|
return {
|
|
56
63
|
carry_kind: carry.kind,
|
|
57
64
|
is_retrospective_done: run_carry.retrospective_done_of(carry),
|
|
65
|
+
is_at_cut_cap: carry.kind === 'carried' && run_carry.is_at_cut_cap(carry.carry),
|
|
58
66
|
last_event: last?.kind,
|
|
59
67
|
}
|
|
60
68
|
}
|
|
@@ -72,10 +80,12 @@ async function gather(issue_number: string): Promise<StepInput> {
|
|
|
72
80
|
last_event: run_reads.last_event,
|
|
73
81
|
carry_kind: run_reads.carry_kind,
|
|
74
82
|
is_retrospective_done: run_reads.is_retrospective_done,
|
|
83
|
+
is_at_cut_cap: run_reads.is_at_cut_cap,
|
|
75
84
|
is_lane_child: lane_child_marker.is_child_of(process.cwd()),
|
|
76
85
|
is_consumer: doctor_consumer.is_kit_consumer(find_package_directory(process.cwd())),
|
|
77
86
|
is_retrospective_enabled: hook_decision.is_switch_opt_in(RETROSPECTIVE_ENV_KEY),
|
|
78
87
|
has_changes: parts.has_changes,
|
|
88
|
+
has_completion_callback: agent_role_profile.has_completion_callback(),
|
|
79
89
|
}
|
|
80
90
|
}
|
|
81
91
|
|
package/scripts/run/run-step.ts
CHANGED
|
@@ -118,6 +118,13 @@ interface StepInput extends PreInput {
|
|
|
118
118
|
// are opt-in — so a run that leaves the variable unset never prints the retrospective step, while the
|
|
119
119
|
// retrospective's own logic is untouched and `pnpm josh retrospective` still runs by hand.
|
|
120
120
|
is_retrospective_enabled: boolean
|
|
121
|
+
// Whether this session is woken when a background command it started completes, read by the CLI from
|
|
122
|
+
// the session's own environment (joshuafolkken/kit#2653). A parent without the callback cannot wait
|
|
123
|
+
// on its lanes except by polling, so it hands the run to the supervisor instead of waiting.
|
|
124
|
+
has_completion_callback: boolean
|
|
125
|
+
// Whether the carried invocation has already taken its maximum cuts, read off the carry record by the
|
|
126
|
+
// CLI (joshuafolkken/kit#2346). At the cap `run:carry --cut` refuses, so the hand-off is not offered.
|
|
127
|
+
is_at_cut_cap: boolean
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
interface StepAction {
|
|
@@ -134,10 +141,9 @@ function command(line: string): StepAction {
|
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
// The action for each event a run can be positioned at, keyed on the newest one. Each dispatches to the
|
|
137
|
-
// command that owns that phase
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
// classifies both, including the outage's stop condition.
|
|
144
|
+
// command that owns that phase. `stop`, `drain` and `child-launch` are handled apart in
|
|
145
|
+
// `FULL_INPUT_ACTIONS`, because each needs more than the issue number. A merge and an outage share a
|
|
146
|
+
// next step: `run:merge` classifies both, including the outage's stop condition.
|
|
141
147
|
const KIND = run_event_stream.EVENT_KIND
|
|
142
148
|
// A park and a stall share this next step: both mean runnable work is waiting to be offered
|
|
143
149
|
// (joshuafolkken/kit#2359), so both are pointed at the backlog.
|
|
@@ -149,7 +155,6 @@ const EVENT_ACTIONS: Record<string, (issue_number: string) => StepAction> = {
|
|
|
149
155
|
[KIND.OUTAGE]: (issue_number) => command(`pnpm josh run:merge ${issue_number}`),
|
|
150
156
|
[KIND.PARK]: () => command(OFFER_BACKLOG),
|
|
151
157
|
[KIND.CUT]: (issue_number) => command(`pnpm josh run:cut --resume ${issue_number}`),
|
|
152
|
-
[KIND.CHILD_LAUNCH]: () => verdict(WAIT),
|
|
153
158
|
// A stall is undispatched ready work with a free lane, so its next step is exactly a park's: offer
|
|
154
159
|
// the backlog. The detector only reports the stall; reading it here as a dispatch is what turns the
|
|
155
160
|
// report into the action that resolves it.
|
|
@@ -197,12 +202,34 @@ function drain_action(input: StepInput): StepAction {
|
|
|
197
202
|
return is_retrospective_owed(input) ? command(RETROSPECTIVE_COMMAND) : verdict(WAIT)
|
|
198
203
|
}
|
|
199
204
|
|
|
200
|
-
// The
|
|
201
|
-
//
|
|
202
|
-
//
|
|
205
|
+
// The hand-off a `backlogrun` parent without a completion callback takes right after dispatching
|
|
206
|
+
// (joshuafolkken/kit#2653). It is the existing merge-time cut, so the ownership check and the cut cap
|
|
207
|
+
// apply unchanged, and the supervisor's driver adopts the in-flight lanes from their `child-launch`
|
|
208
|
+
// events.
|
|
209
|
+
const HAND_OFF_COMMAND = 'pnpm josh run:carry --cut --owner "$PPID"'
|
|
210
|
+
|
|
211
|
+
// After a dispatch the parent waits for its child — where waiting is free. A session the completion
|
|
212
|
+
// does not re-invoke could only wait by polling, each poll a model call over its whole context, so a
|
|
213
|
+
// `backlogrun` parent there hands the carry record to the supervisor instead. A lane child and a run
|
|
214
|
+
// with no carry record have nothing to hand off, so both still wait; so does a run at the cut cap, whose
|
|
215
|
+
// cut would be refused on every ask and leave the carry record with nobody.
|
|
216
|
+
function can_hand_off(input: StepInput): boolean {
|
|
217
|
+
if (input.is_lane_child || input.is_at_cut_cap) return false
|
|
218
|
+
|
|
219
|
+
return input.carry_kind === 'carried' && !input.has_completion_callback
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function child_launch_action(input: StepInput): StepAction {
|
|
223
|
+
return can_hand_off(input) ? command(HAND_OFF_COMMAND) : verdict(WAIT)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// The positions whose action needs the whole input rather than the issue number — a stop and a drain
|
|
227
|
+
// both turn on whether the retrospective is still owed, a child launch on whether waiting is free. Kept
|
|
228
|
+
// in a map so `next_action` dispatches them in one branch rather than one `if` apiece.
|
|
203
229
|
const FULL_INPUT_ACTIONS: Record<string, (input: StepInput) => StepAction> = {
|
|
204
230
|
[KIND.STOP]: stop_action,
|
|
205
231
|
[KIND.DRAIN]: drain_action,
|
|
232
|
+
[KIND.CHILD_LAUNCH]: child_launch_action,
|
|
206
233
|
}
|
|
207
234
|
|
|
208
235
|
// An event the table does not name leaves the position unknown rather than guessing a next step. A
|
|
@@ -275,6 +302,7 @@ function next_action(input: StepInput): StepAction {
|
|
|
275
302
|
const run_step = {
|
|
276
303
|
ALREADY_DONE,
|
|
277
304
|
EXPIRED_DECISION,
|
|
305
|
+
HAND_OFF_COMMAND,
|
|
278
306
|
HUMAN_REVIEW,
|
|
279
307
|
IMPLEMENT,
|
|
280
308
|
KEEP_WORK,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { issue_merged } from '#scripts/issue/issue-merged'
|
|
4
|
+
import { run_tidy } from './run-tidy'
|
|
5
|
+
import { run_tidy_lanes, type IsMerged } from './run-tidy-lanes'
|
|
6
|
+
import { run_tidy_stashes } from './run-tidy-stashes'
|
|
7
|
+
|
|
8
|
+
// `josh run:tidy` — sweep what merged work left behind at a run's start (joshuafolkken/kit#2701): the
|
|
9
|
+
// lanes still holding a seat after their issue merged, and the stashes whose issues are all merged.
|
|
10
|
+
// Both used to wait for a person — `lane:list` flagged a merged lane, `run:carry --end` printed the
|
|
11
|
+
// closed-issue stashes — and a batch ran with two of six seats held by finished work until someone did.
|
|
12
|
+
//
|
|
13
|
+
// `run:hold` calls `sweep` once a claim succeeds, so every `fullrun` / `halfrun` start runs it; a
|
|
14
|
+
// `backlogrun` runs the command itself in its once-per-repository preparation, beside `lane:prune`.
|
|
15
|
+
// **The sweep never fails the run that called it**: the report goes to standard error, where
|
|
16
|
+
// `run:hold`'s one-token standard output is not disturbed, and a failed read is a note, not an exit.
|
|
17
|
+
|
|
18
|
+
const FAILURE_NOTE = 'run:tidy could not finish; merged lanes and stashes may remain:'
|
|
19
|
+
|
|
20
|
+
// One read per issue across both halves — a lane and its stash usually name the same one.
|
|
21
|
+
function memoized(read: IsMerged): IsMerged {
|
|
22
|
+
const answers = new Map<string, Promise<boolean>>()
|
|
23
|
+
|
|
24
|
+
return async function read_once(issue: string): Promise<boolean> {
|
|
25
|
+
const cached = answers.get(issue) ?? read(issue)
|
|
26
|
+
|
|
27
|
+
answers.set(issue, cached)
|
|
28
|
+
|
|
29
|
+
return await cached
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function sweep(): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
const read_merged = memoized(issue_merged.read_merged)
|
|
36
|
+
const lanes = await run_tidy_lanes.tidy_lanes(read_merged)
|
|
37
|
+
const stashes = await run_tidy_stashes.tidy_stashes(read_merged)
|
|
38
|
+
const report = run_tidy.format_report([...lanes, ...stashes])
|
|
39
|
+
|
|
40
|
+
if (report !== undefined) console.error(report)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(FAILURE_NOTE, error)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const run_tidy_cli = { sweep }
|
|
47
|
+
|
|
48
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) await sweep()
|
|
49
|
+
|
|
50
|
+
export { run_tidy_cli }
|