@joshuafolkken/kit 1.1373.0 → 1.1383.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 +6 -3
- package/.claude/skills/workflow-commands/backlogrun-progress.md +2 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -4
- package/.claude/skills/workflow-commands/retrospective.md +1 -2
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-UTHMB4O2.js → chunk-2X2ART5G.js} +4 -33
- package/dist/hooks/{chunk-NZN2UFI6.js → chunk-4MGHBTOM.js} +162 -233
- package/dist/hooks/{chunk-YLBBSSTF.js → chunk-6AC4C2IO.js} +91 -67
- package/dist/hooks/{chunk-AKGCP4LX.js → chunk-6MAW7BCM.js} +12 -12
- package/dist/hooks/{chunk-JCRDBQXB.js → chunk-7VSZ2N4B.js} +3 -3
- package/dist/hooks/{chunk-GG72ANKI.js → chunk-KC6LUZZ2.js} +1 -1
- package/dist/hooks/{chunk-BZMPVFDY.js → chunk-KCF2YE7O.js} +5 -5
- package/dist/hooks/{chunk-7WKYHEGU.js → chunk-LC56R53A.js} +3 -3
- package/dist/hooks/{chunk-V4TKZHER.js → chunk-LOJTBU5V.js} +3 -3
- package/dist/hooks/{chunk-JGW24KKM.js → chunk-RYP3TSUJ.js} +3 -3
- package/dist/hooks/{chunk-SLSFIWTX.js → chunk-TT742SIO.js} +8 -5
- package/dist/hooks/{chunk-Z5TMJIPW.js → chunk-Y5R47QHU.js} +1 -1
- package/dist/hooks/{chunk-7UBFU3CL.js → chunk-YKUITFT7.js} +2 -2
- 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 +190 -191
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +3 -3
- package/dist/hooks/stop-guard.js +19 -15
- package/dist/josh.js +40 -5
- package/package.json +4 -4
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/scripts/adopt/adopt-logic.ts +4 -1
- package/scripts/adopt/adopt.ts +7 -4
- package/scripts/backlog/backlog-budget-cli.ts +2 -20
- package/scripts/backlog/backlog-budget.ts +72 -109
- package/scripts/backlog/backlog-drive-cli.ts +10 -3
- package/scripts/backlog/backlog-drive.ts +5 -2
- package/scripts/backlog/backlog-offer-cli.ts +1 -15
- package/scripts/cost-runtime/context-cut-payback.ts +9 -4
- package/scripts/cost-runtime/cost-pricing.ts +94 -33
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/document/read-set-pricing.ts +7 -5
- package/scripts/epic/epic-candidate-confirm.ts +1 -1
- package/scripts/epic/epic-cross-repo.ts +4 -4
- package/scripts/epic/epic-next.ts +1 -1
- package/scripts/git/git-spawn-sync.ts +9 -1
- package/scripts/hooks/stop-guard.ts +20 -9
- package/scripts/init/init-ai-copy.ts +10 -3
- package/scripts/init/init-copy-content.ts +8 -1
- package/scripts/init/security-policy.ts +79 -0
- package/scripts/josh/josh-commands-ai.ts +1 -1
- package/scripts/josh/josh-commands-maintenance.ts +3 -0
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/josh/josh-commands-versioning.ts +2 -1
- package/scripts/lane/lane-cli.ts +1 -3
- package/scripts/propagate/propagate-run.ts +17 -1
- package/scripts/propagate/propagate-steps.ts +9 -1
- package/scripts/run/board/run-board-header.ts +107 -84
- package/scripts/run/board/run-board-labels.ts +18 -17
- package/scripts/run/board/run-board-machine.ts +21 -8
- package/scripts/run/board/run-board-render.ts +1 -5
- package/scripts/run/board/run-board-span.ts +0 -1
- package/scripts/run/board/run-board-status.ts +0 -17
- package/scripts/run/board/run-board-tick.ts +1 -0
- package/scripts/run/carry/run-carry-cli.ts +8 -62
- package/scripts/run/carry/run-carry-ended.ts +1 -1
- package/scripts/run/carry/run-carry.ts +22 -68
- package/scripts/run/cut/run-cut.ts +4 -4
- package/scripts/run/event/run-event-render.ts +0 -1
- package/scripts/run/event/run-event-scope.ts +4 -8
- package/scripts/run/event/run-event-stream-emit.ts +0 -25
- package/scripts/run/event/run-event-stream.ts +23 -31
- package/scripts/run/merge/run-merge-collect.ts +1 -1
- package/scripts/run/merge/run-merge-steps.ts +1 -1
- package/scripts/run/progress/run-progress-read.ts +1 -1
- package/scripts/run/run-headless.ts +2 -2
- package/scripts/run/run-status-cli.ts +1 -5
- package/scripts/run/run-step.ts +2 -9
- package/scripts/run/run-stop-notify.ts +1 -1
- package/scripts/run/run-stranded-detect.ts +3 -3
- package/scripts/run/run-stranded.ts +25 -17
- package/scripts/run/wake/run-wake-cli.ts +7 -9
- package/scripts/run/wake/run-wake-describe.ts +3 -5
- package/scripts/run/wake/run-wake-driver.ts +3 -3
- package/scripts/run/wake/run-wake-loop.ts +4 -21
- package/scripts/run/wake/run-wake-restart.ts +1 -1
- package/scripts/run/wake/run-wake.ts +6 -7
- package/scripts/sync/sync-ai-files.ts +2 -0
- package/scripts/version/latest-update.ts +20 -6
- package/scripts/version/peer-floor.ts +99 -0
- package/scripts/version/playwright-browsers.ts +109 -0
- package/scripts/version/publishable-range-check.ts +41 -4
- package/templates/SECURITY.md +23 -0
- package/scripts/backlog/backlog-idle.ts +0 -88
package/dist/hooks/stop-guard.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
run_event_stream,
|
|
17
17
|
run_event_stream_emit,
|
|
18
18
|
run_hold
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-6AC4C2IO.js";
|
|
20
20
|
import {
|
|
21
21
|
session_language
|
|
22
22
|
} from "./chunk-6V5OPIUU.js";
|
|
@@ -26,22 +26,22 @@ import {
|
|
|
26
26
|
issue_cite,
|
|
27
27
|
lane_registry,
|
|
28
28
|
run_headless
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-2X2ART5G.js";
|
|
30
30
|
import {
|
|
31
31
|
timed_fetch
|
|
32
32
|
} from "./chunk-FZ6FUSBM.js";
|
|
33
33
|
import "./chunk-CDY4NFQG.js";
|
|
34
34
|
import {
|
|
35
35
|
hook_decision
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-LC56R53A.js";
|
|
37
37
|
import {
|
|
38
38
|
time_density_hook,
|
|
39
39
|
time_hook_transcript,
|
|
40
40
|
time_reported_failure
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-TT742SIO.js";
|
|
42
42
|
import {
|
|
43
43
|
lane_child_marker
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-Y5R47QHU.js";
|
|
45
45
|
import {
|
|
46
46
|
backlog_budget,
|
|
47
47
|
issue_number_shape,
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
process_owner_schema,
|
|
51
51
|
run_carry,
|
|
52
52
|
run_invocation
|
|
53
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-4MGHBTOM.js";
|
|
54
54
|
import {
|
|
55
55
|
PROJECT_ROOT,
|
|
56
56
|
stamp_file
|
|
@@ -981,7 +981,6 @@ var PENDING_DECISION = { kind: "pending" };
|
|
|
981
981
|
var FAILED_DECISION = { kind: "failed" };
|
|
982
982
|
var STOP_REASONS = {
|
|
983
983
|
none: "ended",
|
|
984
|
-
expired: "expired",
|
|
985
984
|
unreadable: "unreadable"
|
|
986
985
|
};
|
|
987
986
|
var run_wake_schema = process_owner_schema.extend({
|
|
@@ -1157,14 +1156,17 @@ function supervisor_liveness(wake, same_process = process_identity.is_same_proce
|
|
|
1157
1156
|
if (is_same === void 0) return UNKNOWN;
|
|
1158
1157
|
return is_same ? LIVE : GONE;
|
|
1159
1158
|
}
|
|
1159
|
+
function had_driver(carry) {
|
|
1160
|
+
return carry.is_handed_off === true || carry.owner_pid !== void 0;
|
|
1161
|
+
}
|
|
1160
1162
|
function is_stranded(input) {
|
|
1161
|
-
if (input.read.kind !== "carried" || input.
|
|
1163
|
+
if (input.read.kind !== "carried" || input.is_owner_live || !had_driver(input.read.carry)) {
|
|
1162
1164
|
return false;
|
|
1163
1165
|
}
|
|
1164
1166
|
return input.supervisor === GONE;
|
|
1165
1167
|
}
|
|
1166
1168
|
function describe2(carry) {
|
|
1167
|
-
return `Run stranded \u2014 ${run_carry.describe_carry(carry)}. The owner is gone, no
|
|
1169
|
+
return `Run stranded \u2014 ${run_carry.describe_carry(carry)}. The owner is gone, no successor holds the budget, and no supervisor is watching.`;
|
|
1168
1170
|
}
|
|
1169
1171
|
function verdict_of(is_run_stranded) {
|
|
1170
1172
|
return is_run_stranded ? STRANDED : OK2;
|
|
@@ -1295,18 +1297,20 @@ async function outcome_of(raw_payload) {
|
|
|
1295
1297
|
return stop_rules.NO_OUTCOME;
|
|
1296
1298
|
}
|
|
1297
1299
|
}
|
|
1298
|
-
async function
|
|
1300
|
+
async function carried_owner_live() {
|
|
1299
1301
|
try {
|
|
1300
1302
|
const directory = await run_carry.repository_directory();
|
|
1301
|
-
if (directory === void 0) return
|
|
1302
|
-
|
|
1303
|
+
if (directory === void 0) return void 0;
|
|
1304
|
+
const read = run_carry.read_carry(run_carry.carry_path(directory));
|
|
1305
|
+
return read.kind === "carried" ? run_carry.is_owner_live(read.carry) : void 0;
|
|
1303
1306
|
} catch {
|
|
1304
|
-
return
|
|
1307
|
+
return void 0;
|
|
1305
1308
|
}
|
|
1306
1309
|
}
|
|
1307
1310
|
async function run_report_checks() {
|
|
1308
|
-
|
|
1309
|
-
|
|
1311
|
+
const is_owner_live = await carried_owner_live();
|
|
1312
|
+
if (is_owner_live === void 0) return;
|
|
1313
|
+
if (is_owner_live) await backlog_stalled_detect.run_stall_check(backlog_ready.DEFAULT_PORTS);
|
|
1310
1314
|
await run_stranded_detect.run_stranded_check();
|
|
1311
1315
|
}
|
|
1312
1316
|
async function write_stop_decision(raw_payload) {
|
package/dist/josh.js
CHANGED
|
@@ -46,6 +46,35 @@ var git_utilities = {
|
|
|
46
46
|
get_git_command_for_spawn
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
// scripts/git/git-location-environment.ts
|
|
50
|
+
var GIT_LOCATION_VARIABLES = [
|
|
51
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
52
|
+
"GIT_COMMON_DIR",
|
|
53
|
+
"GIT_DIR",
|
|
54
|
+
"GIT_INDEX_FILE",
|
|
55
|
+
"GIT_NAMESPACE",
|
|
56
|
+
"GIT_OBJECT_DIRECTORY",
|
|
57
|
+
"GIT_WORK_TREE"
|
|
58
|
+
];
|
|
59
|
+
function location_free_environment() {
|
|
60
|
+
return Object.fromEntries(GIT_LOCATION_VARIABLES.map((name) => [name, void 0]));
|
|
61
|
+
}
|
|
62
|
+
function forget(name) {
|
|
63
|
+
Reflect.deleteProperty(process.env, name);
|
|
64
|
+
}
|
|
65
|
+
function restore_one(name, value) {
|
|
66
|
+
if (value === void 0) forget(name);
|
|
67
|
+
else process.env[name] = value;
|
|
68
|
+
}
|
|
69
|
+
function clear_git_location_variables() {
|
|
70
|
+
const previous = GIT_LOCATION_VARIABLES.map((name) => [name, process.env[name]]);
|
|
71
|
+
for (const [name] of previous) forget(name);
|
|
72
|
+
return () => {
|
|
73
|
+
for (const [name, value] of previous) restore_one(name, value);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
var git_location_environment = { clear_git_location_variables, location_free_environment };
|
|
77
|
+
|
|
49
78
|
// scripts/git/git-spawn-sync.ts
|
|
50
79
|
var SUCCESS_EXIT_CODE = 0;
|
|
51
80
|
var C_LOCALE = { LC_ALL: "C", LANGUAGE: "C" };
|
|
@@ -67,7 +96,10 @@ function read(args, options = {}) {
|
|
|
67
96
|
return result.exit_code === SUCCESS_EXIT_CODE ? result.stdout.trim() : void 0;
|
|
68
97
|
}
|
|
69
98
|
function origin_url(cwd) {
|
|
70
|
-
return read(["config", "--get", "remote.origin.url"], {
|
|
99
|
+
return read(["config", "--get", "remote.origin.url"], {
|
|
100
|
+
cwd,
|
|
101
|
+
env: git_location_environment.location_free_environment()
|
|
102
|
+
});
|
|
71
103
|
}
|
|
72
104
|
function toplevel(timeout = GIT_TIMEOUT_MS) {
|
|
73
105
|
return run(["rev-parse", "--show-toplevel"], { env: C_LOCALE, timeout });
|
|
@@ -1835,7 +1867,7 @@ var AI_COMMANDS = {
|
|
|
1835
1867
|
// `.env` rather than the ambient environment: the strand notification needs the Telegram
|
|
1836
1868
|
// credentials, the same reason `run:wake` carries these flags.
|
|
1837
1869
|
tsx_arguments: OPTIONAL_ENV_FILE_FLAGS,
|
|
1838
|
-
description: "Report whether the run is stranded \u2014
|
|
1870
|
+
description: "Report whether the run is stranded \u2014 owner gone, no successor holding the budget, and no supervisor watching",
|
|
1839
1871
|
category: "AI tools",
|
|
1840
1872
|
reference: ["", "automation", ["processes", "notifications"]]
|
|
1841
1873
|
},
|
|
@@ -2448,7 +2480,10 @@ var MAINTENANCE_COMMANDS = {
|
|
|
2448
2480
|
script: "scripts/version/latest-update.ts",
|
|
2449
2481
|
description: "Update all dependencies to latest",
|
|
2450
2482
|
category: "Maintenance",
|
|
2451
|
-
reference: ["", "developer", ["files", "network"]]
|
|
2483
|
+
reference: ["", "developer", ["files", "network"]],
|
|
2484
|
+
// `JOSH_PLAYWRIGHT_BROWSERS` widens the browsers a Playwright bump installs, and a project
|
|
2485
|
+
// keeps that setting in its `.env`.
|
|
2486
|
+
tsx_arguments: OPTIONAL_ENV_FILE_FLAGS
|
|
2452
2487
|
}
|
|
2453
2488
|
};
|
|
2454
2489
|
|
|
@@ -2536,7 +2571,7 @@ var PROJECT_COMMANDS = {
|
|
|
2536
2571
|
},
|
|
2537
2572
|
adopt: {
|
|
2538
2573
|
script: "scripts/adopt/adopt.ts",
|
|
2539
|
-
description: "Upgrade
|
|
2574
|
+
description: "Upgrade the @joshuafolkken toolkits and other dependencies here, open the PR",
|
|
2540
2575
|
category: "Project",
|
|
2541
2576
|
reference: ["[--dry-run]", "developer", ["files", "git", "network", "processes"]]
|
|
2542
2577
|
}
|
|
@@ -2562,7 +2597,7 @@ var VERSIONING_COMMANDS = {
|
|
|
2562
2597
|
// before chaining here, exactly as it already does for `latest:update`.
|
|
2563
2598
|
ranges: {
|
|
2564
2599
|
script: "scripts/version/publishable-range-check.ts",
|
|
2565
|
-
description: "Check that every published dependency range still resolves for a consumer",
|
|
2600
|
+
description: "Check that every published dependency range still resolves for a consumer and no peer range falls below an installed toolkit",
|
|
2566
2601
|
category: "Versioning",
|
|
2567
2602
|
reference: ["", "developer", ["network"]]
|
|
2568
2603
|
},
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.1383.0",
|
|
3
3
|
"name": "@joshuafolkken/kit",
|
|
4
4
|
"description": "Development toolkit for AI-assisted projects: formatting, AI assistant rules and a Git/GitHub Issue workflow for any repository, plus lint, type check, tests and hooks for Node projects",
|
|
5
5
|
"keywords": [
|
|
@@ -264,10 +264,10 @@
|
|
|
264
264
|
"eslint_d": "^15.0.3",
|
|
265
265
|
"globals": "^17.13.0",
|
|
266
266
|
"lefthook": "^2.2.1",
|
|
267
|
-
"prettier": "^3.9.
|
|
267
|
+
"prettier": "^3.9.10",
|
|
268
268
|
"prettier-plugin-svelte": "^4.1.1",
|
|
269
269
|
"prettier-plugin-tailwindcss": "^0.8.1",
|
|
270
|
-
"rollup": "^4.64.
|
|
270
|
+
"rollup": "^4.64.5",
|
|
271
271
|
"rollup-plugin-dts": "^6.5.1",
|
|
272
272
|
"secretlint": "^13.0.7",
|
|
273
273
|
"typescript": "^6.0.3",
|
|
@@ -289,7 +289,7 @@
|
|
|
289
289
|
"devEngines": {
|
|
290
290
|
"packageManager": {
|
|
291
291
|
"name": "pnpm",
|
|
292
|
-
"version": "12.
|
|
292
|
+
"version": "12.12.0+sha512.2ee06ac86db554cf384421461220f0e336a487bfd41b2acd04e17a86b1487832fe45e8d1743f7fc52acc77de12d2356b536fedf8229fc483239e40b13d474449",
|
|
293
293
|
"onFail": "download"
|
|
294
294
|
}
|
|
295
295
|
},
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
1. **起票する** — `pnpm josh issue:file "<title>" --body-file <path> --depth <n> --route interrupt --label priority:high`。上限は効かない(`wip-cap.md` →「割り込み起票 — 上限が効かない側」の条件 2)
|
|
19
19
|
2. **止まらずに直す** — 壊れた機構を迂回しない(回避策の禁止と、検証ゲートを緩めない規則はここでも変わらない)。`backlogrun` の中では、その割り込みを次の子にし、修正が着地してから塞がれていた子へ戻る。プールから選ぶランでは `priority:high` が先頭に並べ、epic の下では `wip-cap.md` の手順 3 で挿入する。名前を指定したランのドライバーは指定された Issue しか出さないので、`pnpm josh lane:launch <N>` で自分で dispatch する。バッチの外では `wip-cap.md` の手順 4 が変わらず効く — 起票まで済ませ、打つべき `fullrun #<N>` を報告して止まる
|
|
20
20
|
3. **再開できるなら自分で再開し、動くことを確かめる** — 修正のマージ後、再開の手段がある答えには人の打ち直しを求めない。手渡し済みの記録、所有者が死んだ記録、監督プロセスの停止(`warning` Telegram)には `pnpm josh run:wake --start`、`run:carry` の `standing` には `pnpm josh run:carry --resume "<invocation>" --owner "$PPID"` を自分で打つ。そのあと `pnpm josh run:wake --list` が監督プロセスを示し、そのログに次の子の dispatch が現れることを確かめて報告する
|
|
21
|
-
4. **人に打ち直しを求めるのは再開の手段が無いときだけ** — `
|
|
21
|
+
4. **人に打ち直しを求めるのは再開の手段が無いときだけ** — `mismatch`(別の起動の記録)、別の生きた所有者の `busy`。新しい承認を宣言するのは人だけ、という規則も変わらない
|
|
22
22
|
5. **`needs-decision` を付けない** — ランが自分で直せる不具合は人の判断を要さない(`.claude/skills/workflow-commands/backlogrun-park.md` → "Only a person's judgement carries `needs-decision`")
|
|
23
23
|
|
|
24
24
|
手順:
|
|
@@ -8,7 +8,10 @@ import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
|
8
8
|
// The decisions `josh adopt` makes before any step runs: whether this repository may adopt at all,
|
|
9
9
|
// which repository the issue is opened in, and what the run is carrying.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// It names `josh latest` because the issue title names only the toolkits: without this sentence the
|
|
12
|
+
// other dependency bumps in the pull request have nothing in the issue that accounts for them.
|
|
13
|
+
const ADOPT_ORIGIN =
|
|
14
|
+
'Opened by `josh adopt` in this repository, which also ran `josh latest` — the other dependencies and the pnpm pin are updated alongside.'
|
|
12
15
|
const INCOMPLETE_HEADLINE =
|
|
13
16
|
'Refusing to adopt: these declared toolkits would fall out of the plan, and the run would sync the rest without them.'
|
|
14
17
|
const INCOMPLETE_EFFECT =
|
package/scripts/adopt/adopt.ts
CHANGED
|
@@ -13,9 +13,11 @@ import { adopt_toolkits } from './adopt-toolkits'
|
|
|
13
13
|
//
|
|
14
14
|
// It is `josh propagate` seen from the other end: propagation stands in the supplier and pushes one
|
|
15
15
|
// released version out to every consumer, adoption stands in the consumer and pulls whatever is
|
|
16
|
-
// newest in. The step sequence is
|
|
17
|
-
//
|
|
18
|
-
// default branch cannot drift between the
|
|
16
|
+
// newest in. The step sequence is the same one with `josh latest` added — `propagate_run
|
|
17
|
+
// .ADOPT_STEP_ORDER`, derived from `STEP_ORDER` and run through `propagate_run.run_target` — so the
|
|
18
|
+
// working-tree pre-check, the ordering and the return to the default branch cannot drift between the
|
|
19
|
+
// two. `josh latest` is adoption's alone: catching a consumer up means its other dependencies and its
|
|
20
|
+
// pnpm pin too, in the same pull request, where propagation carries one exact release and nothing else.
|
|
19
21
|
//
|
|
20
22
|
// It stops at the open pull request. Merging is what `pnpm josh followup` does, under an
|
|
21
23
|
// authorization a CLI does not have.
|
|
@@ -62,7 +64,7 @@ function run_sequence(target: PropagateTarget, plan: ReleasePlan | undefined): n
|
|
|
62
64
|
plan === undefined ? propagate_steps.describe_step : propagate_steps.create_step_runner(plan)
|
|
63
65
|
const reason = plan === undefined ? DRY_RUN_REASON : propagate_run.PROPAGATED_REASON
|
|
64
66
|
|
|
65
|
-
return report(propagate_run.run_target(target, step, reason))
|
|
67
|
+
return report(propagate_run.run_target(target, step, reason, propagate_run.ADOPT_STEP_ORDER))
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
// A declared toolkit that cannot be carried stops the run, whichever of the two ways it fell out —
|
|
@@ -158,6 +160,7 @@ const adopt = {
|
|
|
158
160
|
KNOWN_FLAGS,
|
|
159
161
|
NOTHING_TO_ADOPT,
|
|
160
162
|
parse_options,
|
|
163
|
+
run_sequence,
|
|
161
164
|
run_here,
|
|
162
165
|
run,
|
|
163
166
|
}
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
type BudgetDecision,
|
|
9
9
|
type BudgetInput,
|
|
10
10
|
} from './backlog-budget'
|
|
11
|
-
import { backlog_idle } from './backlog-idle'
|
|
12
11
|
|
|
13
12
|
// The thin half of `josh backlog:budget` (joshuafolkken/kit#1632): read the loop's state off the
|
|
14
13
|
// command line, hand it to the pure decision, print the verdict on stdout and the reason on stderr
|
|
@@ -18,7 +17,6 @@ const ARGV_OFFSET = 2
|
|
|
18
17
|
const SUCCESS_EXIT_CODE = 0
|
|
19
18
|
const FAILURE_EXIT_CODE = 1
|
|
20
19
|
const JSON_KEY = 'budget'
|
|
21
|
-
const EXHAUSTED_ANSWER: BacklogAnswer = 'exhausted'
|
|
22
20
|
|
|
23
21
|
const USAGE = `Usage: josh backlog:budget --answer <${backlog_budget.ANSWERS.join('|')}> --started <ISO-8601> [--active <ISO-8601>, required unless the watch is off] [--merged <count>] [--running <count>] [--max <count>] [--idle <minutes>, 0 to turn the watch off] [--json]`
|
|
24
22
|
|
|
@@ -71,7 +69,7 @@ function to_count(raw: string | undefined): number | undefined {
|
|
|
71
69
|
|
|
72
70
|
// A flag that was given but could not be read makes the whole invocation unreadable. Falling back to
|
|
73
71
|
// the default instead would answer a question nobody asked — a mistyped `--idle 0` would read as the
|
|
74
|
-
// default
|
|
72
|
+
// default watch and hold a run open that was asked to finish at its first empty backlog.
|
|
75
73
|
function is_readable(raw: string | undefined, parsed: number | undefined): boolean {
|
|
76
74
|
return raw === undefined || parsed !== undefined
|
|
77
75
|
}
|
|
@@ -165,27 +163,11 @@ function build_input(values: ParsedValues, now_ms: number): BudgetInput | undefi
|
|
|
165
163
|
return input_of(values, { answer, now_ms, started_at_ms })
|
|
166
164
|
}
|
|
167
165
|
|
|
168
|
-
// The idle window, carried only on the watch an empty backlog opens — the one `idle_decision` reaches,
|
|
169
|
-
// not the draining watch a reached bound returns while children still run — so `backlog:offer` can
|
|
170
|
-
// record when that wait ends (joshuafolkken/kit#3430).
|
|
171
|
-
function idle_of(input: BudgetInput, verdict: string): string | undefined {
|
|
172
|
-
const is_idle_watch =
|
|
173
|
-
verdict === backlog_budget.WATCH_VERDICT &&
|
|
174
|
-
input.answer === EXHAUSTED_ANSWER &&
|
|
175
|
-
backlog_budget.is_finish(input)
|
|
176
|
-
const window = is_idle_watch ? backlog_idle.window_of(input) : undefined
|
|
177
|
-
|
|
178
|
-
return window === undefined ? undefined : backlog_idle.text_of(window)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
166
|
function print_json(input: BudgetInput, decision: BudgetDecision): void {
|
|
182
167
|
const { verdict, reason } = decision
|
|
183
|
-
const idle = idle_of(input, verdict)
|
|
184
168
|
const is_finish = backlog_budget.is_finish(input)
|
|
185
169
|
|
|
186
|
-
console.info(
|
|
187
|
-
JSON.stringify({ [JSON_KEY]: verdict, reason, is_finish, ...(idle !== undefined && { idle }) }),
|
|
188
|
-
)
|
|
170
|
+
console.info(JSON.stringify({ [JSON_KEY]: verdict, reason, is_finish }))
|
|
189
171
|
}
|
|
190
172
|
|
|
191
173
|
function refuse(): number {
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
// `josh backlog:budget` — whether a `backlogrun` may start more work, keep watching, or finish.
|
|
2
2
|
//
|
|
3
3
|
// `backlog:next` says what may start; it says nothing about when the run itself should end. Without
|
|
4
|
-
// this a run would finish the moment
|
|
4
|
+
// this a run would finish the moment nothing could proceed, so an issue a person opted in minutes
|
|
5
5
|
// later would need a whole new session.
|
|
6
6
|
//
|
|
7
7
|
// **The idle watch is on by default and the maximum is not.** A `backlogrun` with neither flag given
|
|
8
|
-
// watches
|
|
9
|
-
// the backlog holds. `--idle 0` is how the watch is turned off.
|
|
8
|
+
// watches for `IDLE_WAIT_HOURS` once nothing can proceed before it finishes, and takes as many issues
|
|
9
|
+
// as the backlog holds. `--idle 0` is how the watch is turned off.
|
|
10
|
+
//
|
|
11
|
+
// **Every reason nothing can proceed is watched, not only an empty backlog.** Everything opted in
|
|
12
|
+
// waiting on a person, and a listing that could not be read, both change without this run doing
|
|
13
|
+
// anything — a person opts the prerequisite in, GitHub answers again — so ending on either would miss
|
|
14
|
+
// the issue opted in minutes later, which is the one thing the watch exists to catch.
|
|
10
15
|
//
|
|
11
16
|
// The decision is a command's rather than the loop's own arithmetic, for the reason `josh delegate`
|
|
12
17
|
// and `josh review:level` are commands: a count and an elapsed time kept in an agent's head are a
|
|
@@ -14,29 +19,23 @@
|
|
|
14
19
|
|
|
15
20
|
const MS_PER_MINUTE = 60_000
|
|
16
21
|
const MINUTES_PER_HOUR = 60
|
|
17
|
-
// The whole-run bound, decided here like the idle watch and the maximum rather than by an agent reading
|
|
18
|
-
// a clock.
|
|
19
|
-
const WHOLE_RUN_BUDGET_HOURS = 8
|
|
20
|
-
const WHOLE_RUN_BUDGET_MINUTES = WHOLE_RUN_BUDGET_HOURS * MINUTES_PER_HOUR
|
|
21
|
-
const WHOLE_RUN_BUDGET_MS = WHOLE_RUN_BUDGET_MINUTES * MS_PER_MINUTE
|
|
22
22
|
|
|
23
|
-
// **
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// - **It is about the length of one child** — 12 to 28 minutes measured — so a run that has emptied its backlog waits roughly as long as one more issue would have taken.
|
|
30
|
-
const DEFAULT_IDLE_MINUTES = 30
|
|
23
|
+
// **How long a run with nothing to do waits, and the only time bound a run has.** It is counted from
|
|
24
|
+
// when the run last had work, never from its start: a run still working, or still inside this wait,
|
|
25
|
+
// is not ended by its age. Eight hours is one unattended night — a person who opts an issue in before
|
|
26
|
+
// they sleep, or after they wake, finds the run still there to pick it up.
|
|
27
|
+
const IDLE_WAIT_HOURS = 8
|
|
28
|
+
const DEFAULT_IDLE_MINUTES = IDLE_WAIT_HOURS * MINUTES_PER_HOUR
|
|
31
29
|
const DEFAULT_IDLE_MS = DEFAULT_IDLE_MINUTES * MS_PER_MINUTE
|
|
32
30
|
|
|
33
|
-
// **
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// history — to learn nothing thirty times. `backlogrun-progress.md` → "Waiting, and never waiting forever" holds
|
|
31
|
+
// **The one interval between two asks for work, whether lanes are running or nothing is.** The driver
|
|
32
|
+
// and the wake supervisor both read it from here. The loop is a process, so an ask spends no AI turn,
|
|
33
|
+
// and a watch is polled as often as a running batch; a child collected in between is asked about at
|
|
34
|
+
// once, without waiting it out. `backlogrun-progress.md` → "Waiting, and never waiting forever" holds
|
|
38
35
|
// the row; the figure is here because `idle_watch_reason` below has to say it.
|
|
39
|
-
const
|
|
36
|
+
const OFFER_INTERVAL_SECONDS = 60
|
|
37
|
+
const MS_PER_SECOND = 1000
|
|
38
|
+
const OFFER_INTERVAL_MS = OFFER_INTERVAL_SECONDS * MS_PER_SECOND
|
|
40
39
|
|
|
41
40
|
const RUN_VERDICT = 'run'
|
|
42
41
|
const WATCH_VERDICT = 'watch'
|
|
@@ -69,7 +68,7 @@ interface BudgetInput {
|
|
|
69
68
|
// otherwise be measured against a maximum that cannot see it.
|
|
70
69
|
running: number
|
|
71
70
|
started_at_ms: number
|
|
72
|
-
// When this run last had work —
|
|
71
|
+
// When this run last had work — its most recent launch or collected child, or the run's start.
|
|
73
72
|
// Refreshing it is what restarts the idle watch when a candidate appears during one.
|
|
74
73
|
active_at_ms: number
|
|
75
74
|
now_ms: number
|
|
@@ -87,22 +86,28 @@ interface BudgetDecision {
|
|
|
87
86
|
reason: string
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
const WHOLE_RUN_REASON = `The whole-run bound of ${String(WHOLE_RUN_BUDGET_HOURS)} hours has passed. An unattended run that has not finished overnight needs a person, not more waiting.`
|
|
91
|
-
|
|
92
|
-
const PARKED_REASON =
|
|
93
|
-
'Nothing opted in can proceed without a person. Report the parked issues and finish.'
|
|
94
|
-
|
|
95
|
-
const UNREADABLE_REASON =
|
|
96
|
-
'The backlog listing could not be read, so nothing was offered. Report what the command printed and finish — it is not an empty backlog.'
|
|
97
|
-
|
|
98
89
|
const BLOCKED_REASON =
|
|
99
90
|
'Everything opted in is blocked or already running, so waiting can still change the answer. Poll again at the polling interval.'
|
|
100
91
|
|
|
101
92
|
const TRIAGE_REASON =
|
|
102
93
|
'An opted-in candidate carries neither `run:solo` nor `run:lane`, so nothing starts until it is judged. Read each issue named above, record its order and one of the two labels with a comment, then ask again.'
|
|
103
94
|
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
// What the run is waiting on, by the answer that opened the watch — the opening of every sentence a
|
|
96
|
+
// watch or its ending prints, so a person reads which of the three it was.
|
|
97
|
+
const EMPTY_BACKLOG = 'The backlog is empty'
|
|
98
|
+
const WAITING_ON: Readonly<Partial<Record<BacklogAnswer, string>>> = {
|
|
99
|
+
exhausted: EMPTY_BACKLOG,
|
|
100
|
+
parked: 'Nothing opted in can proceed without a person',
|
|
101
|
+
unreadable: 'The backlog listing could not be read',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function waiting_on(answer: BacklogAnswer): string {
|
|
105
|
+
return WAITING_ON[answer] ?? EMPTY_BACKLOG
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function no_idle_watch_reason(answer: BacklogAnswer): string {
|
|
109
|
+
return `${waiting_on(answer)} and the idle watch was turned off with \`--idle 0\`, so the run finishes here.`
|
|
110
|
+
}
|
|
106
111
|
|
|
107
112
|
function to_minutes(duration_ms: number): string {
|
|
108
113
|
return String(Math.ceil(duration_ms / MS_PER_MINUTE))
|
|
@@ -119,39 +124,29 @@ function draining_reason(running: number, reason: string): string {
|
|
|
119
124
|
return `Start nothing more and poll until the running children merge (running: ${String(running)}). Then report and finish — ${reason}`
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
function idle_expired_reason(idle_budget_ms: number): string {
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Never longer than the watch has left: "about 2 minutes left, ask again in 5 minutes" would overrun
|
|
127
|
-
// the budget a person declared, and a sentence that contradicts itself is read as neither half.
|
|
128
|
-
function next_ask_minutes(left_ms: number): string {
|
|
129
|
-
const left_minutes = Number(to_minutes(left_ms))
|
|
130
|
-
|
|
131
|
-
return String(Math.min(IDLE_POLL_MINUTES, left_minutes))
|
|
127
|
+
function idle_expired_reason(answer: BacklogAnswer, idle_budget_ms: number): string {
|
|
128
|
+
return `${waiting_on(answer)}, and stayed so for the whole ${to_minutes(idle_budget_ms)}-minute idle watch. Report what was done, including how many were picked up during the watch, and finish.`
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
function idle_watch_reason(left_ms: number): string {
|
|
135
|
-
return
|
|
131
|
+
function idle_watch_reason(answer: BacklogAnswer, left_ms: number): string {
|
|
132
|
+
return `${waiting_on(answer)} and the idle watch has about ${to_minutes(left_ms)} minutes left. Release the working tree, ask again in ${String(OFFER_INTERVAL_SECONDS)} seconds, and restart the watch the moment a candidate appears.`
|
|
136
133
|
}
|
|
137
134
|
|
|
138
135
|
// **A watch that opens while children are still in lanes is not the idle watch's ordinary shape**: the
|
|
139
|
-
// backlog can
|
|
140
|
-
//
|
|
141
|
-
//
|
|
136
|
+
// backlog can offer nothing while this run's own children are still merging. The sentence above
|
|
137
|
+
// would get one thing wrong there — the working tree is still held, because the hold is released at
|
|
138
|
+
// the *last* child's merge.
|
|
142
139
|
//
|
|
143
|
-
// **The watch runs down while they drain, and
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
function running_watch_reason(running: number, left_ms: number): string {
|
|
148
|
-
return `The backlog is empty and the idle watch has about ${to_minutes(left_ms)} minutes left, with ${String(running)} still running. Keep the working tree until they merge and poll at the polling interval, not the idle poll — the watch is running down meanwhile.`
|
|
140
|
+
// **The watch runs down while they drain, and restarts at each one collected.** A collected child is
|
|
141
|
+
// work, so it moves `active_at`; an ask that offered nothing never does.
|
|
142
|
+
function running_watch_reason(answer: BacklogAnswer, running: number, left_ms: number): string {
|
|
143
|
+
return `${waiting_on(answer)} and the idle watch has about ${to_minutes(left_ms)} minutes left, with ${String(running)} still running. Keep the working tree until they merge and poll at the polling interval — the watch is running down meanwhile.`
|
|
149
144
|
}
|
|
150
145
|
|
|
151
146
|
function watching_reason(input: BudgetInput, left_ms: number): string {
|
|
152
|
-
if (input.running === 0) return idle_watch_reason(left_ms)
|
|
147
|
+
if (input.running === 0) return idle_watch_reason(input.answer, left_ms)
|
|
153
148
|
|
|
154
|
-
return running_watch_reason(input.running, left_ms)
|
|
149
|
+
return running_watch_reason(input.answer, input.running, left_ms)
|
|
155
150
|
}
|
|
156
151
|
|
|
157
152
|
// What the maximum has already taken: merged issues and the children still running. Counting only
|
|
@@ -169,32 +164,10 @@ function run_reason(input: BudgetInput): string {
|
|
|
169
164
|
return `Start at most ${String(max_issues - taken_by(input))} more: this run's maximum is ${String(max_issues)}, with ${String(input.merged)} merged and ${String(input.running)} running.`
|
|
170
165
|
}
|
|
171
166
|
|
|
172
|
-
// The two answers that end a run whatever the budgets say. Neither is a budget decision — the graph
|
|
173
|
-
// said so — but both reach this command, because the loop asks one question rather than branching on
|
|
174
|
-
// `backlog:next`'s word itself and then asking a second.
|
|
175
|
-
const ANSWER_STOP_REASONS: Readonly<Partial<Record<BacklogAnswer, string>>> = {
|
|
176
|
-
parked: PARKED_REASON,
|
|
177
|
-
unreadable: UNREADABLE_REASON,
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function whole_run_reason(input: BudgetInput): string | undefined {
|
|
181
|
-
if (input.now_ms - input.started_at_ms < WHOLE_RUN_BUDGET_MS) return undefined
|
|
182
|
-
|
|
183
|
-
return WHOLE_RUN_REASON
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// The answer-level stops come first, ahead of every budget. Both of them say something the person
|
|
187
|
-
// has to act on — issues are parked, or the listing could not be read — and the reason printed here
|
|
188
|
-
// is quoted verbatim into the completion report, so a budget reason in front of one would report a
|
|
189
|
-
// tidy ending for a run whose backlog actually broke.
|
|
190
|
-
function stop_reason(input: BudgetInput): string | undefined {
|
|
191
|
-
return ANSWER_STOP_REASONS[input.answer] ?? whole_run_reason(input)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
167
|
// Every ending goes through here, so no ending abandons a lane. Children already started have to
|
|
195
|
-
// merge and be reported whatever ended the run — the maximum, a
|
|
196
|
-
//
|
|
197
|
-
//
|
|
168
|
+
// merge and be reported whatever ended the run — the maximum, or a watch that ran out or was turned
|
|
169
|
+
// off — so a `stop` while any of them is still running would leave open lanes behind and merged work
|
|
170
|
+
// unmentioned.
|
|
198
171
|
function stopping(input: BudgetInput, reason: string): BudgetDecision {
|
|
199
172
|
if (input.running === 0) return { verdict: STOP_VERDICT, reason }
|
|
200
173
|
|
|
@@ -212,30 +185,31 @@ function max_decision(input: BudgetInput): BudgetDecision | undefined {
|
|
|
212
185
|
return stopping(input, max_reached_reason(input.merged, max_issues))
|
|
213
186
|
}
|
|
214
187
|
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
// the other branch, where
|
|
188
|
+
// Nothing can proceed, so the run watches rather than ends — whichever of the three answers said so.
|
|
189
|
+
// The watch is the run's only time bound: it is counted from `active_at_ms`, so a run is never ended
|
|
190
|
+
// by how long ago it started. Only `--idle 0` takes the other branch, where the first such answer is
|
|
191
|
+
// the run's ending.
|
|
218
192
|
function idle_decision(input: BudgetInput): BudgetDecision {
|
|
219
193
|
const { idle_budget_ms } = input
|
|
220
194
|
|
|
221
|
-
if (idle_budget_ms === undefined) return stopping(input,
|
|
195
|
+
if (idle_budget_ms === undefined) return stopping(input, no_idle_watch_reason(input.answer))
|
|
222
196
|
|
|
223
197
|
const left_ms = idle_budget_ms - (input.now_ms - input.active_at_ms)
|
|
224
198
|
|
|
225
|
-
if (left_ms
|
|
199
|
+
if (left_ms > 0) return { verdict: WATCH_VERDICT, reason: watching_reason(input, left_ms) }
|
|
226
200
|
|
|
227
|
-
return
|
|
201
|
+
return stopping(input, idle_expired_reason(input.answer, idle_budget_ms))
|
|
228
202
|
}
|
|
229
203
|
|
|
230
|
-
// Whether a `stop` for this input is the run finishing — the
|
|
231
|
-
// rather than stopping on something a person must act on:
|
|
232
|
-
//
|
|
233
|
-
// `--end --stopped`), and `backlog:drive` reads which one from here.
|
|
204
|
+
// Whether a `stop` for this input is the run finishing — the watch ending on an empty backlog —
|
|
205
|
+
// rather than stopping on something a person must act on: the maximum, issues still waiting on a
|
|
206
|
+
// person, or a listing that never became readable. The two end the carry record differently (`--end`
|
|
207
|
+
// against `--end --stopped`), and `backlog:drive` reads which one from here.
|
|
234
208
|
function is_finish(input: BudgetInput): boolean {
|
|
235
|
-
return
|
|
209
|
+
return input.answer === 'exhausted' && max_decision(input) === undefined
|
|
236
210
|
}
|
|
237
211
|
|
|
238
|
-
// The answers whose decision no count changes, once the
|
|
212
|
+
// The answers whose decision no count changes, once the maximum above has been applied.
|
|
239
213
|
const FIXED_DECISIONS: Readonly<Partial<Record<BacklogAnswer, BudgetDecision>>> = {
|
|
240
214
|
blocked: { verdict: WATCH_VERDICT, reason: BLOCKED_REASON },
|
|
241
215
|
untriaged: { verdict: TRIAGE_VERDICT, reason: TRIAGE_REASON },
|
|
@@ -248,15 +222,7 @@ function answer_decision(input: BudgetInput): BudgetDecision {
|
|
|
248
222
|
}
|
|
249
223
|
|
|
250
224
|
function decide(input: BudgetInput): BudgetDecision {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (reason !== undefined) return stopping(input, reason)
|
|
254
|
-
|
|
255
|
-
const capped = max_decision(input)
|
|
256
|
-
|
|
257
|
-
if (capped !== undefined) return capped
|
|
258
|
-
|
|
259
|
-
return answer_decision(input)
|
|
225
|
+
return max_decision(input) ?? answer_decision(input)
|
|
260
226
|
}
|
|
261
227
|
|
|
262
228
|
const backlog_budget = {
|
|
@@ -264,25 +230,22 @@ const backlog_budget = {
|
|
|
264
230
|
BLOCKED_REASON,
|
|
265
231
|
DEFAULT_IDLE_MINUTES,
|
|
266
232
|
DEFAULT_IDLE_MS,
|
|
267
|
-
|
|
233
|
+
IDLE_WAIT_HOURS,
|
|
268
234
|
MS_PER_MINUTE,
|
|
269
|
-
|
|
270
|
-
PARKED_REASON,
|
|
235
|
+
OFFER_INTERVAL_MS,
|
|
271
236
|
RUN_VERDICT,
|
|
272
237
|
STOP_VERDICT,
|
|
273
238
|
TRIAGE_REASON,
|
|
274
239
|
TRIAGE_VERDICT,
|
|
275
|
-
|
|
240
|
+
WAITING_ON,
|
|
276
241
|
WATCH_VERDICT,
|
|
277
|
-
WHOLE_RUN_BUDGET_HOURS,
|
|
278
|
-
WHOLE_RUN_BUDGET_MS,
|
|
279
|
-
WHOLE_RUN_REASON,
|
|
280
242
|
decide,
|
|
281
243
|
draining_reason,
|
|
282
244
|
idle_expired_reason,
|
|
283
245
|
idle_watch_reason,
|
|
284
246
|
is_finish,
|
|
285
247
|
max_reached_reason,
|
|
248
|
+
no_idle_watch_reason,
|
|
286
249
|
run_reason,
|
|
287
250
|
running_watch_reason,
|
|
288
251
|
taken_by,
|