@joshuafolkken/kit 1.1383.0 → 1.1387.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/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-4MGHBTOM.js → chunk-2YSW3CXH.js} +8 -2
- package/dist/hooks/{chunk-KCF2YE7O.js → chunk-DCY2GWHC.js} +5 -5
- package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-EAQEGME7.js} +1 -1
- package/dist/hooks/{chunk-LC56R53A.js → chunk-EBSHN25W.js} +3 -3
- package/dist/hooks/{chunk-6MAW7BCM.js → chunk-GJ7JPIHN.js} +14 -14
- package/dist/hooks/{chunk-6AC4C2IO.js → chunk-GNDJ7NJ7.js} +59 -462
- package/dist/hooks/{chunk-LOJTBU5V.js → chunk-IYVOLN6A.js} +3 -3
- package/dist/hooks/{chunk-Y5R47QHU.js → chunk-J3KSS7II.js} +1 -1
- package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-LMWBRJIP.js} +3 -3
- package/dist/hooks/{chunk-TT742SIO.js → chunk-SL4GQ2XP.js} +2 -2
- package/dist/hooks/{chunk-YKUITFT7.js → chunk-STDSEPWU.js} +2 -2
- package/dist/hooks/{chunk-2X2ART5G.js → chunk-U32VSRNG.js} +420 -4
- package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-WNFJLBXI.js} +3 -3
- 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 +208 -206
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +65 -9
- package/dist/hooks/stop-guard.js +91 -23
- package/dist/josh.js +1 -1
- package/package.json +1 -1
- package/scripts/backlog/backlog-drive-cli.ts +2 -4
- package/scripts/backlog/backlog-drive-launch.ts +2 -0
- package/scripts/backlog/backlog-drive-owner.ts +72 -5
- package/scripts/backlog/backlogrun-launch.ts +56 -5
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/init/start-plan.ts +15 -4
- package/scripts/init/start-steps.ts +31 -3
- package/scripts/init/start.ts +20 -13
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/run/board/run-board-header.ts +39 -13
- package/scripts/run/board/run-board-read.ts +9 -2
- package/scripts/run/board/run-board-setup.ts +73 -0
- package/scripts/run/board/run-board-span.ts +49 -7
- package/scripts/run/board/run-board-status.ts +10 -6
- package/scripts/run/board/run-board-tick.ts +10 -2
- package/scripts/run/carry/run-carry-added.ts +1 -1
- package/scripts/run/carry/run-carry-change.ts +1 -1
- package/scripts/run/carry/run-carry-cli.ts +37 -7
- package/scripts/run/carry/run-carry-ended.ts +9 -1
- package/scripts/run/carry/run-carry.ts +16 -0
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +47 -0
- package/scripts/run/event/run-event-stream.ts +26 -5
- package/scripts/run/run-stranded-detect.ts +58 -12
- package/scripts/run/wake/run-wake-cli.ts +51 -6
- package/scripts/run/wake/run-wake-describe.ts +2 -0
- package/scripts/run/wake/run-wake-driver.ts +10 -1
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
|
@@ -6,17 +6,23 @@ import {
|
|
|
6
6
|
agent_role_profile,
|
|
7
7
|
detached_launch,
|
|
8
8
|
git_common_directory,
|
|
9
|
+
issue_cite,
|
|
10
|
+
run_event_stream,
|
|
11
|
+
run_event_stream_emit,
|
|
9
12
|
run_headless
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-U32VSRNG.js";
|
|
14
|
+
import {
|
|
15
|
+
error_text
|
|
16
|
+
} from "./chunk-CDY4NFQG.js";
|
|
11
17
|
import {
|
|
12
18
|
josh_environment_file
|
|
13
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-J3KSS7II.js";
|
|
14
20
|
import {
|
|
15
21
|
agent_session_environment,
|
|
16
22
|
json_value,
|
|
17
23
|
process_identity,
|
|
18
24
|
run_carry
|
|
19
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-2YSW3CXH.js";
|
|
20
26
|
import {
|
|
21
27
|
stamp_file
|
|
22
28
|
} from "./chunk-NEC4AT4Z.js";
|
|
@@ -298,6 +304,32 @@ var agent_argv = {
|
|
|
298
304
|
with_resume_in
|
|
299
305
|
};
|
|
300
306
|
|
|
307
|
+
// scripts/run/event/run-event-setup.ts
|
|
308
|
+
var STAGE = {
|
|
309
|
+
// `josh backlogrun` started the parent session, which is reading its procedure.
|
|
310
|
+
STARTING: "starting session",
|
|
311
|
+
// `run:carry --begin` claimed the run, and the parent is planning it.
|
|
312
|
+
PLANNING: "planning",
|
|
313
|
+
// The supervisor took the run over from the parent and is asking for the first offer.
|
|
314
|
+
HANDING_OFF: "handing off"
|
|
315
|
+
};
|
|
316
|
+
var OPENING = "opening lane";
|
|
317
|
+
function opening_lane(issue) {
|
|
318
|
+
return `${OPENING} ${issue_cite.plain(issue)}`;
|
|
319
|
+
}
|
|
320
|
+
async function emit(text) {
|
|
321
|
+
await run_event_stream_emit.emit(run_event_stream.EVENT_KIND.SETUP, text);
|
|
322
|
+
}
|
|
323
|
+
function emit_in(repository, text, at) {
|
|
324
|
+
try {
|
|
325
|
+
const target = run_event_stream.target_of(repository);
|
|
326
|
+
run_event_stream.append(target, run_event_stream.EVENT_KIND.SETUP, text, at.toISOString());
|
|
327
|
+
} catch (error) {
|
|
328
|
+
error_text.trace_swallowed("run_event_setup.emit_in", error);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
var run_event_setup = { STAGE, emit, emit_in, opening_lane };
|
|
332
|
+
|
|
301
333
|
// scripts/backlog/backlogrun-launch.ts
|
|
302
334
|
import { z } from "zod";
|
|
303
335
|
var LAUNCH_PREFIX = "josh-backlogrun-launch-";
|
|
@@ -306,7 +338,11 @@ var LOG_SUFFIX = ".log";
|
|
|
306
338
|
var launch_schema = z.object({
|
|
307
339
|
pid: z.number(),
|
|
308
340
|
start: z.string().optional(),
|
|
309
|
-
session: z.string().optional()
|
|
341
|
+
session: z.string().optional(),
|
|
342
|
+
// What the parent was started to run and when, so `run:board` has a run to draw and a clock to count
|
|
343
|
+
// before the parent claims the carry record. Absent on a record an earlier version wrote.
|
|
344
|
+
invocation: z.string().optional(),
|
|
345
|
+
started_at: z.string().optional()
|
|
310
346
|
});
|
|
311
347
|
function launch_path(git_directory) {
|
|
312
348
|
return stamp_file.stamp_path(LAUNCH_PREFIX, git_directory);
|
|
@@ -348,9 +384,27 @@ function argv_of(invocation, provider, worktree, session) {
|
|
|
348
384
|
function note_to_stderr(note) {
|
|
349
385
|
console.error(`backlogrun: ${note}`);
|
|
350
386
|
}
|
|
351
|
-
function record_launch(git_directory,
|
|
352
|
-
const
|
|
353
|
-
|
|
387
|
+
function record_launch(git_directory, launch) {
|
|
388
|
+
const now = /* @__PURE__ */ new Date();
|
|
389
|
+
const identity = process_identity.read_start(launch.pid);
|
|
390
|
+
const record = { ...launch, start: identity, started_at: now.toISOString() };
|
|
391
|
+
stamp_file.replace_stamp(launch_path(git_directory), record);
|
|
392
|
+
run_event_setup.emit_in(git_directory, run_event_setup.STAGE.STARTING, now);
|
|
393
|
+
}
|
|
394
|
+
function live_launch(git_directory) {
|
|
395
|
+
const record = read_launch(launch_path(git_directory));
|
|
396
|
+
return is_launch_live(record) ? record : void 0;
|
|
397
|
+
}
|
|
398
|
+
function pending_launch(git_directory) {
|
|
399
|
+
const { invocation, started_at } = live_launch(git_directory) ?? {};
|
|
400
|
+
if (invocation === void 0 || started_at === void 0) return void 0;
|
|
401
|
+
return { invocation, started_at };
|
|
402
|
+
}
|
|
403
|
+
function started_for(git_directory, owner_pid) {
|
|
404
|
+
const record = live_launch(git_directory);
|
|
405
|
+
if (record?.started_at === void 0 || record.pid !== owner_pid) return void 0;
|
|
406
|
+
const started_ms = Date.parse(record.started_at);
|
|
407
|
+
return Number.isNaN(started_ms) ? void 0 : new Date(started_ms);
|
|
354
408
|
}
|
|
355
409
|
function spawn_parent(argv, provider, target) {
|
|
356
410
|
const environment = {
|
|
@@ -376,7 +430,7 @@ function start(invocation, provider, target) {
|
|
|
376
430
|
if (built.kind === "rejected") return { kind: "failed", note: built.note };
|
|
377
431
|
const result = spawn_parent(built, provider, target);
|
|
378
432
|
if (result.kind === "failed") return result;
|
|
379
|
-
record_launch(target.git_directory, result.pid, session);
|
|
433
|
+
record_launch(target.git_directory, { pid: result.pid, session, invocation });
|
|
380
434
|
const launched = { pid: result.pid, session, log_path: log_path(target.git_directory) };
|
|
381
435
|
return { kind: "launched", launched };
|
|
382
436
|
}
|
|
@@ -384,7 +438,9 @@ var backlogrun_launch = {
|
|
|
384
438
|
argv_of,
|
|
385
439
|
chosen_environment,
|
|
386
440
|
is_running,
|
|
387
|
-
|
|
441
|
+
pending_launch,
|
|
442
|
+
start,
|
|
443
|
+
started_for
|
|
388
444
|
};
|
|
389
445
|
|
|
390
446
|
// scripts/josh/session-language-cli.ts
|
package/dist/hooks/stop-guard.js
CHANGED
|
@@ -13,10 +13,8 @@ import {
|
|
|
13
13
|
lane_park,
|
|
14
14
|
repo_party,
|
|
15
15
|
run_cut,
|
|
16
|
-
run_event_stream,
|
|
17
|
-
run_event_stream_emit,
|
|
18
16
|
run_hold
|
|
19
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-GNDJ7NJ7.js";
|
|
20
18
|
import {
|
|
21
19
|
session_language
|
|
22
20
|
} from "./chunk-6V5OPIUU.js";
|
|
@@ -25,23 +23,25 @@ import {
|
|
|
25
23
|
agent_role_profile,
|
|
26
24
|
issue_cite,
|
|
27
25
|
lane_registry,
|
|
26
|
+
run_event_stream,
|
|
27
|
+
run_event_stream_emit,
|
|
28
28
|
run_headless
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-U32VSRNG.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-EBSHN25W.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-SL4GQ2XP.js";
|
|
42
42
|
import {
|
|
43
43
|
lane_child_marker
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-J3KSS7II.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-2YSW3CXH.js";
|
|
54
54
|
import {
|
|
55
55
|
PROJECT_ROOT,
|
|
56
56
|
stamp_file
|
|
@@ -964,8 +964,53 @@ var run_halfrun_resume = {
|
|
|
964
964
|
take_over
|
|
965
965
|
};
|
|
966
966
|
|
|
967
|
-
// scripts/run/
|
|
967
|
+
// scripts/run/carry/run-carry-ended.ts
|
|
968
|
+
import path from "node:path";
|
|
968
969
|
import { z as z4 } from "zod";
|
|
970
|
+
var ENDED_PREFIX = "josh-run-ended-";
|
|
971
|
+
var TRANSCRIPT_SUFFIX = ".jsonl";
|
|
972
|
+
var ended_run_schema = z4.object({
|
|
973
|
+
invocation: z4.string(),
|
|
974
|
+
started_at: z4.string(),
|
|
975
|
+
ended_at: z4.string(),
|
|
976
|
+
stopped: z4.string().optional(),
|
|
977
|
+
session: z4.string().optional()
|
|
978
|
+
});
|
|
979
|
+
function ended_path(git_directory) {
|
|
980
|
+
return stamp_file.stamp_path(ENDED_PREFIX, git_directory);
|
|
981
|
+
}
|
|
982
|
+
function session_of(transcript) {
|
|
983
|
+
return transcript === void 0 ? void 0 : path.basename(transcript, TRANSCRIPT_SUFFIX);
|
|
984
|
+
}
|
|
985
|
+
function stop_of(transcript, stopped) {
|
|
986
|
+
return stopped === void 0 ? {} : { stopped, session: session_of(transcript) };
|
|
987
|
+
}
|
|
988
|
+
function record_ended(target, read, now = /* @__PURE__ */ new Date(), stopped) {
|
|
989
|
+
if (read.kind !== "carried") return;
|
|
990
|
+
const { invocation, started_at, owner_transcript } = read.carry;
|
|
991
|
+
const ended_at = now.toISOString();
|
|
992
|
+
stamp_file.replace_stamp(target, {
|
|
993
|
+
invocation,
|
|
994
|
+
started_at,
|
|
995
|
+
ended_at,
|
|
996
|
+
...stop_of(owner_transcript, stopped)
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
function parse_ended(raw) {
|
|
1000
|
+
return json_value.parse_with(raw, ended_run_schema);
|
|
1001
|
+
}
|
|
1002
|
+
function read_ended(target) {
|
|
1003
|
+
const raw = stamp_file.read_stamp_text(target);
|
|
1004
|
+
return raw === void 0 ? void 0 : parse_ended(raw);
|
|
1005
|
+
}
|
|
1006
|
+
function has_ended(target, started_at) {
|
|
1007
|
+
const ended = read_ended(target);
|
|
1008
|
+
return ended !== void 0 && ended.started_at === started_at;
|
|
1009
|
+
}
|
|
1010
|
+
var run_carry_ended = { ended_path, has_ended, read_ended, record_ended };
|
|
1011
|
+
|
|
1012
|
+
// scripts/run/wake/run-wake.ts
|
|
1013
|
+
import { z as z5 } from "zod";
|
|
969
1014
|
var WAKE_PREFIX = "josh-run-wake-";
|
|
970
1015
|
var WAKE_LOG_PREFIX = "josh-run-wake-log-";
|
|
971
1016
|
var WAKE_LOG_SUFFIX = ".log";
|
|
@@ -984,15 +1029,15 @@ var STOP_REASONS = {
|
|
|
984
1029
|
unreadable: "unreadable"
|
|
985
1030
|
};
|
|
986
1031
|
var run_wake_schema = process_owner_schema.extend({
|
|
987
|
-
invocation:
|
|
988
|
-
started_at:
|
|
989
|
-
woke:
|
|
990
|
-
woke_at:
|
|
1032
|
+
invocation: z5.string(),
|
|
1033
|
+
started_at: z5.string(),
|
|
1034
|
+
woke: z5.number(),
|
|
1035
|
+
woke_at: z5.string().optional(),
|
|
991
1036
|
// Every optional `RunWake` field must be listed: `z.object` strips undeclared ones on the round trip.
|
|
992
|
-
attempts:
|
|
993
|
-
woke_pid:
|
|
994
|
-
spawned:
|
|
995
|
-
idle_since:
|
|
1037
|
+
attempts: z5.number().optional(),
|
|
1038
|
+
woke_pid: z5.number().optional(),
|
|
1039
|
+
spawned: z5.array(z5.string()).optional(),
|
|
1040
|
+
idle_since: z5.string().optional(),
|
|
996
1041
|
profile: agent_role_profile.PROFILE_SCHEMA.optional()
|
|
997
1042
|
});
|
|
998
1043
|
function is_overdue(marked_at, now, limit_ms = WAKE_GRACE_MS) {
|
|
@@ -1186,10 +1231,12 @@ var run_stranded = {
|
|
|
1186
1231
|
|
|
1187
1232
|
// scripts/run/run-stranded-detect.ts
|
|
1188
1233
|
var STRAND_RECOVERY = `Restart supervision with \`${run_stranded.RECOVERY_COMMAND}\` \u2014 it wakes a fresh successor to claim the budget; the cutting session must not resume it itself.`;
|
|
1234
|
+
function targets_of(directory) {
|
|
1235
|
+
return { carry: run_carry.carry_path(directory), wake: run_wake.wake_path(directory) };
|
|
1236
|
+
}
|
|
1189
1237
|
async function real_resolve_targets() {
|
|
1190
1238
|
const directory = await run_carry.repository_directory();
|
|
1191
|
-
|
|
1192
|
-
return { carry: run_carry.carry_path(directory), wake: run_wake.wake_path(directory) };
|
|
1239
|
+
return directory === void 0 ? void 0 : targets_of(directory);
|
|
1193
1240
|
}
|
|
1194
1241
|
async function real_emit_stranded(text2) {
|
|
1195
1242
|
return await run_event_stream_emit.emit_once(run_event_stream.EVENT_KIND.STRANDED, text2);
|
|
@@ -1201,14 +1248,13 @@ var DEFAULT_PORTS3 = {
|
|
|
1201
1248
|
resolve_targets: real_resolve_targets,
|
|
1202
1249
|
read_carry: (target) => run_carry.read_carry(target),
|
|
1203
1250
|
is_owner_live: run_carry.is_owner_live,
|
|
1251
|
+
has_ended: run_carry_ended.has_ended,
|
|
1204
1252
|
read_wake: (target) => run_wake.read_wake(target),
|
|
1205
1253
|
supervisor_liveness: (wake) => run_stranded.supervisor_liveness(wake),
|
|
1206
1254
|
emit_stranded: real_emit_stranded,
|
|
1207
1255
|
notify_stranded: real_notify_stranded
|
|
1208
1256
|
};
|
|
1209
|
-
|
|
1210
|
-
const targets = await ports.resolve_targets();
|
|
1211
|
-
if (targets === void 0) return void 0;
|
|
1257
|
+
function input_at(ports, targets) {
|
|
1212
1258
|
const read = ports.read_carry(targets.carry);
|
|
1213
1259
|
if (read.kind !== "carried") return void 0;
|
|
1214
1260
|
return {
|
|
@@ -1217,6 +1263,10 @@ async function gather2(ports) {
|
|
|
1217
1263
|
supervisor: ports.supervisor_liveness(ports.read_wake(targets.wake))
|
|
1218
1264
|
};
|
|
1219
1265
|
}
|
|
1266
|
+
async function gather2(ports) {
|
|
1267
|
+
const targets = await ports.resolve_targets();
|
|
1268
|
+
return targets === void 0 ? void 0 : input_at(ports, targets);
|
|
1269
|
+
}
|
|
1220
1270
|
async function report2(ports, input) {
|
|
1221
1271
|
if (input.read.kind !== "carried") return;
|
|
1222
1272
|
const text2 = run_stranded.describe(input.read.carry);
|
|
@@ -1235,7 +1285,25 @@ async function run_stranded_check() {
|
|
|
1235
1285
|
} catch {
|
|
1236
1286
|
}
|
|
1237
1287
|
}
|
|
1238
|
-
|
|
1288
|
+
function is_stranded_at(directory, ports = DEFAULT_PORTS3) {
|
|
1289
|
+
const input = input_at(ports, targets_of(directory));
|
|
1290
|
+
return input !== void 0 && run_stranded.is_stranded(input);
|
|
1291
|
+
}
|
|
1292
|
+
function is_left_stranded(run, ports = DEFAULT_PORTS3) {
|
|
1293
|
+
const read = ports.read_carry(run.carry_target);
|
|
1294
|
+
if (read.kind !== "carried") return !ports.has_ended(run.ended_target, run.started_at);
|
|
1295
|
+
return run_stranded.is_stranded({
|
|
1296
|
+
read,
|
|
1297
|
+
is_owner_live: run_carry.is_held_by_another(read.carry, ports.is_owner_live),
|
|
1298
|
+
supervisor: run_stranded.GONE
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
var run_stranded_detect = {
|
|
1302
|
+
detect_and_report: detect_and_report2,
|
|
1303
|
+
is_left_stranded,
|
|
1304
|
+
is_stranded_at,
|
|
1305
|
+
run_stranded_check
|
|
1306
|
+
};
|
|
1239
1307
|
|
|
1240
1308
|
// scripts/hooks/stop-guard.ts
|
|
1241
1309
|
async function hold_facts() {
|
package/dist/josh.js
CHANGED
|
@@ -2501,7 +2501,7 @@ var PROJECT_COMMANDS = {
|
|
|
2501
2501
|
description: "Set a project up for the GitHub Issue workflow, from git init to the setup PR",
|
|
2502
2502
|
category: "Project",
|
|
2503
2503
|
reference: [
|
|
2504
|
-
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--init-command <command>]`,
|
|
2504
|
+
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--init-command <command>] [--push-preflight-command <command>]`,
|
|
2505
2505
|
"developer",
|
|
2506
2506
|
["files", "git", "network", "processes"]
|
|
2507
2507
|
]
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.1387.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": [
|
|
@@ -79,10 +79,7 @@ async function read_offer(
|
|
|
79
79
|
state: DriveState,
|
|
80
80
|
context: DriveContext,
|
|
81
81
|
): Promise<OfferRead | undefined> {
|
|
82
|
-
await backlog_drive_owner.assert_current(context.owner)
|
|
83
|
-
const carry = await read_record()
|
|
84
|
-
|
|
85
|
-
if (carry === undefined) return undefined
|
|
82
|
+
const carry = await backlog_drive_owner.assert_current(context.owner)
|
|
86
83
|
const named = await backlog_drive_named_offer.read(carry, state, context)
|
|
87
84
|
if (named !== undefined) return named
|
|
88
85
|
|
|
@@ -269,6 +266,7 @@ const backlog_drive_cli = {
|
|
|
269
266
|
merge_token,
|
|
270
267
|
offer_argv: backlog_drive_offer_argv.offer_argv,
|
|
271
268
|
parse: backlog_drive_args.parse,
|
|
269
|
+
read_offer,
|
|
272
270
|
resume_line: backlog_drive_args.resume_line,
|
|
273
271
|
run,
|
|
274
272
|
to_offer,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { git_stash } from '#scripts/git/stash/git-stash'
|
|
2
2
|
import { lane_launch_cli, type LaunchOutcome } from '#scripts/lane/lane-launch-cli'
|
|
3
|
+
import { run_event_setup } from '#scripts/run/event/run-event-setup'
|
|
3
4
|
import { backlog_drive_owner } from './backlog-drive-owner'
|
|
4
5
|
|
|
5
6
|
// The driver's lane launch. The parent that ran `josh latest` in the primary
|
|
@@ -28,6 +29,7 @@ async function latest_stash(): Promise<string | undefined> {
|
|
|
28
29
|
|
|
29
30
|
async function launch(issue: string, owner: string): Promise<LaunchOutcome['kind']> {
|
|
30
31
|
await backlog_drive_owner.assert_current(owner)
|
|
32
|
+
await run_event_setup.emit(run_event_setup.opening_lane(issue))
|
|
31
33
|
|
|
32
34
|
const outcome = await lane_launch_cli.launch_lane({ issue, stash: await latest_stash() })
|
|
33
35
|
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { poll } from '#scripts/lib/poll'
|
|
3
|
+
import { run_carry, type CarryRead, type RunCarry } from '#scripts/run/carry/run-carry'
|
|
4
|
+
import { run_carry_ended } from '#scripts/run/carry/run-carry-ended'
|
|
5
|
+
import { backlog_budget } from './backlog-budget'
|
|
6
|
+
|
|
7
|
+
// **A record that does not read is asked again before the driver gives up on it.** The driver is the
|
|
8
|
+
// only thing looking for work during the idle watch, so ending on one failed read throws away whatever
|
|
9
|
+
// is left of the watch. The read is repeated at the offer's own interval, and
|
|
10
|
+
// the count bounds it: a record that stays unreadable still ends the driver, with the reason said.
|
|
11
|
+
const READ_ATTEMPTS = 6
|
|
12
|
+
const ENDED_MESSAGE = 'The carry record is no longer active.'
|
|
13
|
+
|
|
14
|
+
// The run this driver last read, so a record that is gone can be told from one `run:carry --end` ended.
|
|
15
|
+
const seen: { started_at?: string } = {}
|
|
2
16
|
|
|
3
17
|
function owns_record(carry: RunCarry, owner: string): boolean {
|
|
4
18
|
const current = run_carry.owner_of(Number(owner))
|
|
@@ -7,16 +21,69 @@ function owns_record(carry: RunCarry, owner: string): boolean {
|
|
|
7
21
|
return has_owner && run_carry.is_owner_live(carry) && !carry.is_handed_off
|
|
8
22
|
}
|
|
9
23
|
|
|
10
|
-
|
|
24
|
+
// `--end` records the run it removes, so an ended run answers at once rather than after the re-reads.
|
|
25
|
+
function is_ended(directory: string): boolean {
|
|
26
|
+
return run_carry_ended.has_ended(run_carry_ended.ended_path(directory), seen.started_at)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Whether the file is there is said beside the read, because `none` is the answer both to a record
|
|
30
|
+
// that is gone and to one that is present and could not be opened.
|
|
31
|
+
function unread_note(read: CarryRead, directory: string): string {
|
|
32
|
+
const presence = existsSync(run_carry.carry_path(directory)) ? 'present' : 'absent'
|
|
33
|
+
|
|
34
|
+
return `The carry record read \`${read.kind}\` with its file ${presence}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function is_settled(read: CarryRead, directory: string): boolean {
|
|
38
|
+
if (read.kind === 'carried' || is_ended(directory)) return true
|
|
39
|
+
|
|
40
|
+
console.error(`${unread_note(read, directory)}; reading it again.`)
|
|
41
|
+
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function read_record(directory: string): Promise<CarryRead> {
|
|
46
|
+
const target = run_carry.carry_path(directory)
|
|
47
|
+
let read: CarryRead = { kind: 'none' }
|
|
48
|
+
|
|
49
|
+
await poll.poll_until(
|
|
50
|
+
async () => {
|
|
51
|
+
read = run_carry.read_carry(target)
|
|
52
|
+
|
|
53
|
+
return is_settled(read, directory)
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
attempts: READ_ATTEMPTS,
|
|
57
|
+
interval_ms: backlog_budget.OFFER_INTERVAL_MS,
|
|
58
|
+
sleeper: async (duration_ms) => {
|
|
59
|
+
await poll.sleep(duration_ms)
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return read
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function unread_message(read: CarryRead, directory: string): string {
|
|
68
|
+
if (is_ended(directory)) return ENDED_MESSAGE
|
|
69
|
+
|
|
70
|
+
return `${unread_note(read, directory)}, ${String(READ_ATTEMPTS)} times in a row.`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function assert_current(owner: string): Promise<RunCarry> {
|
|
11
74
|
const directory = await run_carry.repository_directory()
|
|
12
75
|
if (directory === undefined) throw new Error('The carry record cannot be located.')
|
|
13
76
|
|
|
14
|
-
const read =
|
|
15
|
-
if (read.kind !== 'carried') throw new Error(
|
|
77
|
+
const read = await read_record(directory)
|
|
78
|
+
if (read.kind !== 'carried') throw new Error(unread_message(read, directory))
|
|
16
79
|
|
|
17
80
|
if (!owns_record(read.carry, owner)) {
|
|
18
81
|
throw new Error('The driver no longer owns the carry record.')
|
|
19
82
|
}
|
|
83
|
+
|
|
84
|
+
seen.started_at = read.carry.started_at
|
|
85
|
+
|
|
86
|
+
return read.carry
|
|
20
87
|
}
|
|
21
88
|
|
|
22
|
-
export const backlog_drive_owner = { assert_current }
|
|
89
|
+
export const backlog_drive_owner = { READ_ATTEMPTS, assert_current }
|
|
@@ -11,6 +11,7 @@ import { stamp_file } from '#scripts/josh/stamp-file'
|
|
|
11
11
|
import { json_value } from '#scripts/lib/json-value'
|
|
12
12
|
import { run_carry, type CarryRead } from '#scripts/run/carry/run-carry'
|
|
13
13
|
import { detached_launch, type LaunchResult } from '#scripts/run/detached-launch'
|
|
14
|
+
import { run_event_setup } from '#scripts/run/event/run-event-setup'
|
|
14
15
|
import { run_headless } from '#scripts/run/run-headless'
|
|
15
16
|
import { z } from 'zod'
|
|
16
17
|
|
|
@@ -34,10 +35,20 @@ const launch_schema = z.object({
|
|
|
34
35
|
pid: z.number(),
|
|
35
36
|
start: z.string().optional(),
|
|
36
37
|
session: z.string().optional(),
|
|
38
|
+
// What the parent was started to run and when, so `run:board` has a run to draw and a clock to count
|
|
39
|
+
// before the parent claims the carry record. Absent on a record an earlier version wrote.
|
|
40
|
+
invocation: z.string().optional(),
|
|
41
|
+
started_at: z.string().optional(),
|
|
37
42
|
})
|
|
38
43
|
|
|
39
44
|
type LaunchRecord = z.infer<typeof launch_schema>
|
|
40
45
|
|
|
46
|
+
// A launched run the parent has not claimed yet, as far as its record says.
|
|
47
|
+
interface PendingLaunch {
|
|
48
|
+
invocation: string
|
|
49
|
+
started_at: string
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
interface LaunchTarget {
|
|
42
53
|
// The checkout the parent runs in, and its common git directory the records are keyed on.
|
|
43
54
|
worktree: string
|
|
@@ -117,10 +128,48 @@ function note_to_stderr(note: string): void {
|
|
|
117
128
|
console.error(`backlogrun: ${note}`)
|
|
118
129
|
}
|
|
119
130
|
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
// **The record and the run's first stage carry one moment**, so the `starting session` event is never
|
|
132
|
+
// dated before the start the board measures from.
|
|
133
|
+
function record_launch(
|
|
134
|
+
git_directory: string,
|
|
135
|
+
launch: Pick<LaunchRecord, 'pid' | 'session' | 'invocation'>,
|
|
136
|
+
): void {
|
|
137
|
+
const now = new Date()
|
|
138
|
+
const identity = process_identity.read_start(launch.pid)
|
|
139
|
+
const record: LaunchRecord = { ...launch, start: identity, started_at: now.toISOString() }
|
|
140
|
+
|
|
141
|
+
stamp_file.replace_stamp(launch_path(git_directory), record)
|
|
142
|
+
run_event_setup.emit_in(git_directory, run_event_setup.STAGE.STARTING, now)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// The live launch record here, `undefined` once the parent it names is gone.
|
|
146
|
+
function live_launch(git_directory: string): LaunchRecord | undefined {
|
|
147
|
+
const record = read_launch(launch_path(git_directory))
|
|
148
|
+
|
|
149
|
+
return is_launch_live(record) ? record : undefined
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The launch `run:board` draws while no carry record stands: one whose record says what it runs and
|
|
153
|
+
// since when.
|
|
154
|
+
function pending_launch(git_directory: string): PendingLaunch | undefined {
|
|
155
|
+
const { invocation, started_at } = live_launch(git_directory) ?? {}
|
|
156
|
+
|
|
157
|
+
if (invocation === undefined || started_at === undefined) return undefined
|
|
158
|
+
|
|
159
|
+
return { invocation, started_at }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// When the launch that started `owner_pid` began, for `run:carry --begin` to start the run's clock
|
|
163
|
+
// from: **only the parent this launch started inherits it**, so a run typed inside a session — which has
|
|
164
|
+
// no launch record, or another parent's — still starts at its own `--begin`.
|
|
165
|
+
function started_for(git_directory: string, owner_pid: number | undefined): Date | undefined {
|
|
166
|
+
const record = live_launch(git_directory)
|
|
167
|
+
|
|
168
|
+
if (record?.started_at === undefined || record.pid !== owner_pid) return undefined
|
|
169
|
+
|
|
170
|
+
const started_ms = Date.parse(record.started_at)
|
|
122
171
|
|
|
123
|
-
|
|
172
|
+
return Number.isNaN(started_ms) ? undefined : new Date(started_ms)
|
|
124
173
|
}
|
|
125
174
|
|
|
126
175
|
// The headless mark holds the parent's turn open while its lanes run, as `run:wake`'s does. The launch
|
|
@@ -160,7 +209,7 @@ function start(invocation: string, provider: AgentProvider, target: LaunchTarget
|
|
|
160
209
|
|
|
161
210
|
if (result.kind === 'failed') return result
|
|
162
211
|
|
|
163
|
-
record_launch(target.git_directory, result.pid, session)
|
|
212
|
+
record_launch(target.git_directory, { pid: result.pid, session, invocation })
|
|
164
213
|
|
|
165
214
|
const launched = { pid: result.pid, session, log_path: log_path(target.git_directory) }
|
|
166
215
|
|
|
@@ -171,8 +220,10 @@ const backlogrun_launch = {
|
|
|
171
220
|
argv_of,
|
|
172
221
|
chosen_environment,
|
|
173
222
|
is_running,
|
|
223
|
+
pending_launch,
|
|
174
224
|
start,
|
|
225
|
+
started_for,
|
|
175
226
|
}
|
|
176
227
|
|
|
177
|
-
export type { Launched, LaunchTarget, StartResult }
|
|
228
|
+
export type { Launched, LaunchTarget, PendingLaunch, StartResult }
|
|
178
229
|
export { backlogrun_launch }
|
|
@@ -74,7 +74,7 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
|
|
|
74
74
|
{ path: '.claude/skills/workflow-commands/target-repository.md', bytes: 4096 },
|
|
75
75
|
{ path: '.claude/skills/workflow-commands/working-tree-hold.md', bytes: 8192 },
|
|
76
76
|
{ path: 'docs/josh-commands-automation.md', bytes: 53_248 },
|
|
77
|
-
{ path: 'docs/josh-commands-run.md', bytes:
|
|
77
|
+
{ path: 'docs/josh-commands-run.md', bytes: 69_632 },
|
|
78
78
|
{ path: 'docs/josh-commands-backlog.md', bytes: 57_344 },
|
|
79
79
|
{ path: 'docs/josh-commands.md', bytes: 49_152 },
|
|
80
80
|
{ path: 'docs/labels-and-run-states.md', bytes: 8192 },
|
|
@@ -9,6 +9,7 @@ interface StartOptions {
|
|
|
9
9
|
is_github: boolean
|
|
10
10
|
visibility: Visibility
|
|
11
11
|
init_command: string | undefined
|
|
12
|
+
push_preflight_command: string | undefined
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
type StartStep = 'git_init' | 'initialize' | 'commit' | 'repository' | 'labels' | 'setup_pr'
|
|
@@ -53,7 +54,8 @@ const STEP_LABELS: Readonly<Record<StartStep, string>> = {
|
|
|
53
54
|
setup_pr: 'Open an Issue and a pull request that add the kit setup to main',
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
// The GitHub write steps `--yes` alone never consents to.
|
|
57
|
+
// The GitHub write steps `--yes` alone never consents to. Each one pushes — main, or the setup branch —
|
|
58
|
+
// so they are also the steps a caller's push preflight guards (#3702).
|
|
57
59
|
const GITHUB_WRITE_STEPS: ReadonlySet<StartStep> = new Set(['repository', 'setup_pr'])
|
|
58
60
|
|
|
59
61
|
function refused(refusal: string): StepPlan {
|
|
@@ -107,6 +109,10 @@ function plan_steps(state: GitState): StepPlan {
|
|
|
107
109
|
return { steps: FULL_STEPS, refusal: undefined }
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
function has_push(steps: ReadonlyArray<StartStep>): boolean {
|
|
113
|
+
return steps.some((step) => GITHUB_WRITE_STEPS.has(step))
|
|
114
|
+
}
|
|
115
|
+
|
|
110
116
|
function interaction_of(options: StartOptions, is_tty: boolean): Verdict<Interaction> {
|
|
111
117
|
if (options.is_yes) return { value: 'unattended', refusal: undefined }
|
|
112
118
|
if (is_tty) return { value: 'interactive', refusal: undefined }
|
|
@@ -126,9 +132,7 @@ function github_consent(
|
|
|
126
132
|
options: StartOptions,
|
|
127
133
|
interaction: Interaction,
|
|
128
134
|
): Verdict<Consent> {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!is_github_write || options.is_github) return { value: 'granted', refusal: undefined }
|
|
135
|
+
if (!has_push(steps) || options.is_github) return { value: 'granted', refusal: undefined }
|
|
132
136
|
if (interaction === 'interactive') return { value: 'ask', refusal: undefined }
|
|
133
137
|
|
|
134
138
|
return {
|
|
@@ -158,6 +162,11 @@ function progress_report(
|
|
|
158
162
|
return `josh start stopped at: ${labels[failed]}\nCompleted: ${done}\nCause: ${cause}`
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
// What a run prints when the caller's push preflight fails: no step has run yet, so nothing changed.
|
|
166
|
+
function preflight_report(command: string, cause: string): string {
|
|
167
|
+
return `josh start stopped before its first step: the push preflight (${command}) failed. Nothing was changed.\nCause: ${cause}`
|
|
168
|
+
}
|
|
169
|
+
|
|
161
170
|
function plan_summary(
|
|
162
171
|
steps: ReadonlyArray<StartStep>,
|
|
163
172
|
labels: Readonly<Record<StartStep, string>> = STEP_LABELS,
|
|
@@ -169,9 +178,11 @@ function plan_summary(
|
|
|
169
178
|
|
|
170
179
|
const start_plan = {
|
|
171
180
|
plan_steps,
|
|
181
|
+
has_push,
|
|
172
182
|
interaction_of,
|
|
173
183
|
github_consent,
|
|
174
184
|
progress_report,
|
|
185
|
+
preflight_report,
|
|
175
186
|
plan_summary,
|
|
176
187
|
step_labels,
|
|
177
188
|
STEP_LABELS,
|