@joshuafolkken/kit 1.1435.0 → 1.1455.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.
Files changed (105) hide show
  1. package/.claude/skills/workflow-commands/backlogrun-handoff-report.md +1 -1
  2. package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
  3. package/.claude/skills/workflow-commands/backlogrun-steps.md +9 -7
  4. package/.claude/skills/workflow-commands/backlogrun.md +2 -2
  5. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  6. package/.claude/skills/workflow-commands/halfrun.md +3 -2
  7. package/.claude/skills/workflow-commands/kickoff.md +12 -13
  8. package/.claude/skills/workflow-commands/observation-filing.md +1 -1
  9. package/.claude/skills/workflow-commands/prerequisite.md +6 -0
  10. package/.claude/skills/workflow-commands/prrun.md +2 -1
  11. package/.claude/skills/workflow-commands/retrospective.md +5 -5
  12. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  13. package/CLAUDE.md +1 -1
  14. package/README.md +10 -19
  15. package/dist/CLAUDE.md +1 -1
  16. package/dist/hooks/batch-guard.js +6 -6
  17. package/dist/hooks/{chunk-3LE6VLSR.js → chunk-3C4XRYG6.js} +18 -5
  18. package/dist/hooks/{chunk-7KMN5QB5.js → chunk-5H4SSDSD.js} +3 -3
  19. package/dist/hooks/{chunk-YZXYOAZ3.js → chunk-7WQBPLUN.js} +11 -5
  20. package/dist/hooks/{chunk-2G5F2YCD.js → chunk-D5QBTOCZ.js} +3 -3
  21. package/dist/hooks/{chunk-MU4LJFIV.js → chunk-GPLGZ6IC.js} +3 -3
  22. package/dist/hooks/{chunk-MSYSUSY6.js → chunk-HFGWYGUV.js} +5 -5
  23. package/dist/hooks/{chunk-JXQBK323.js → chunk-HHI6FO3R.js} +132 -105
  24. package/dist/hooks/{chunk-XER6SSIB.js → chunk-LIAFCXL7.js} +17 -7
  25. package/dist/hooks/{chunk-4BHUPFKF.js → chunk-OLMO72ON.js} +1 -1
  26. package/dist/hooks/{chunk-EMB55BDF.js → chunk-TBJFTSUY.js} +2 -2
  27. package/dist/hooks/{chunk-5SJSSCJG.js → chunk-UT7YC3N2.js} +1 -1
  28. package/dist/hooks/{chunk-QL6QTPWN.js → chunk-WNLVGT7E.js} +3 -3
  29. package/dist/hooks/{chunk-YW452ZAX.js → chunk-Z4XI7CZS.js} +2 -2
  30. package/dist/hooks/codex-hook-adapter.js +13 -13
  31. package/dist/hooks/duplicate-read-guard.js +7 -7
  32. package/dist/hooks/format-edited.js +3 -3
  33. package/dist/hooks/inputs.json +229 -226
  34. package/dist/hooks/investigation-guard.js +7 -7
  35. package/dist/hooks/pretool-guard.js +12 -12
  36. package/dist/hooks/session-lang.js +3 -3
  37. package/dist/hooks/stop-guard.js +64 -11
  38. package/dist/josh.js +34 -11
  39. package/package.json +1 -1
  40. package/prompts/collaboration-workflow/issue-template.md +10 -8
  41. package/prompts/collaboration-workflow/shell-body.md +1 -1
  42. package/prompts/review.md +8 -4
  43. package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
  44. package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
  45. package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
  46. package/scripts/auto-ok/auto-ok-judge.ts +67 -0
  47. package/scripts/backlog/backlog-drive-cli.ts +14 -0
  48. package/scripts/backlog/backlog-drive.ts +22 -3
  49. package/scripts/document/document-byte-budget.ts +1 -1
  50. package/scripts/fresh-machine/fresh-machine-patterns.ts +60 -15
  51. package/scripts/gate/project-checks.ts +5 -2
  52. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  53. package/scripts/gh/git-gh-issue.ts +9 -0
  54. package/scripts/gh/git-pr-ai-review.ts +12 -1
  55. package/scripts/gh/git-pr-confirmation.ts +11 -7
  56. package/scripts/init/init-actions.ts +3 -3
  57. package/scripts/init/init-ai-copy.ts +1 -1
  58. package/scripts/init/init-basic.ts +1 -1
  59. package/scripts/init/init-bootstrap.ts +6 -2
  60. package/scripts/init/init-dry-run-report.ts +150 -0
  61. package/scripts/init/init-dry-run.ts +189 -0
  62. package/scripts/init/init-install.ts +8 -1
  63. package/scripts/init/init.ts +57 -19
  64. package/scripts/init/kit-setup-state.ts +11 -17
  65. package/scripts/init/own-formatter.ts +21 -0
  66. package/scripts/init/project-profile.ts +25 -6
  67. package/scripts/init/start-dry-run.ts +70 -0
  68. package/scripts/init/start-plan.ts +8 -3
  69. package/scripts/init/start-setup-pr.ts +13 -24
  70. package/scripts/init/start-steps.ts +16 -1
  71. package/scripts/init/start.ts +38 -18
  72. package/scripts/init/working-tree.ts +116 -0
  73. package/scripts/issue/issue-auto-ok.ts +68 -99
  74. package/scripts/issue/issue-file-cli.ts +17 -17
  75. package/scripts/issue/issue-file.ts +1 -6
  76. package/scripts/issue/issue-labels.ts +6 -2
  77. package/scripts/issue/issue-release-cli.ts +69 -23
  78. package/scripts/issue/issue-release.ts +3 -1
  79. package/scripts/josh/josh-commands-ai.ts +11 -0
  80. package/scripts/josh/josh-commands-issue.ts +3 -3
  81. package/scripts/josh/josh-commands-project.ts +2 -2
  82. package/scripts/josh/josh-commands-workflow.ts +1 -1
  83. package/scripts/josh/process-identity.ts +32 -6
  84. package/scripts/lane/lane-broker-server.ts +69 -0
  85. package/scripts/lane/lane-session-broker.ts +37 -14
  86. package/scripts/notify/confirmation-options.ts +59 -0
  87. package/scripts/notify/notify-opt-in.ts +33 -0
  88. package/scripts/notify/telegram-notify.ts +25 -8
  89. package/scripts/notify/telegram-test-logic.ts +33 -1
  90. package/scripts/notify/telegram-test.ts +4 -2
  91. package/scripts/observations/observation-ledger-migrate.ts +3 -15
  92. package/scripts/release/release-issue-close.ts +133 -0
  93. package/scripts/release/release-publish.ts +12 -1
  94. package/scripts/repo/repository-labels.ts +11 -1
  95. package/scripts/review/comment-ranges.ts +199 -0
  96. package/scripts/review/review-brief-cli.ts +22 -5
  97. package/scripts/review/review-code-tree.ts +119 -0
  98. package/scripts/review/review-round2-cli.ts +16 -5
  99. package/scripts/review/review-round2.ts +30 -10
  100. package/scripts/review/review-stamps.ts +22 -1
  101. package/scripts/rules/shell-body-trigger.ts +3 -2
  102. package/scripts/rules/stop-rules.ts +2 -1
  103. package/scripts/run/entry/run-entry-stop.ts +6 -3
  104. package/scripts/run/run-stop-notify.ts +2 -0
  105. package/scripts/uninstall/distributed-files.ts +1 -1
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env tsx
2
2
  import {
3
3
  pretool_guard
4
- } from "./chunk-JXQBK323.js";
5
- import "./chunk-MSYSUSY6.js";
6
- import "./chunk-YZXYOAZ3.js";
4
+ } from "./chunk-HHI6FO3R.js";
5
+ import "./chunk-HFGWYGUV.js";
6
+ import "./chunk-7WQBPLUN.js";
7
7
  import "./chunk-IX6TTTI6.js";
8
- import "./chunk-YW452ZAX.js";
8
+ import "./chunk-Z4XI7CZS.js";
9
9
  import "./chunk-CDY4NFQG.js";
10
- import "./chunk-7KMN5QB5.js";
11
- import "./chunk-2G5F2YCD.js";
12
- import "./chunk-QL6QTPWN.js";
13
- import "./chunk-5SJSSCJG.js";
10
+ import "./chunk-5H4SSDSD.js";
11
+ import "./chunk-D5QBTOCZ.js";
12
+ import "./chunk-WNLVGT7E.js";
13
+ import "./chunk-UT7YC3N2.js";
14
14
  import {
15
15
  hook_decision
16
- } from "./chunk-MU4LJFIV.js";
17
- import "./chunk-XER6SSIB.js";
18
- import "./chunk-4BHUPFKF.js";
19
- import "./chunk-3LE6VLSR.js";
16
+ } from "./chunk-GPLGZ6IC.js";
17
+ import "./chunk-LIAFCXL7.js";
18
+ import "./chunk-OLMO72ON.js";
19
+ import "./chunk-3C4XRYG6.js";
20
20
  import "./chunk-DGT54IH5.js";
21
21
 
22
22
  // scripts/hooks/pretool-guard-cli.ts
@@ -8,19 +8,19 @@ import {
8
8
  run_event_stream_emit,
9
9
  run_headless,
10
10
  session_language
11
- } from "./chunk-YW452ZAX.js";
11
+ } from "./chunk-Z4XI7CZS.js";
12
12
  import {
13
13
  error_text
14
14
  } from "./chunk-CDY4NFQG.js";
15
15
  import {
16
16
  josh_environment_file
17
- } from "./chunk-4BHUPFKF.js";
17
+ } from "./chunk-OLMO72ON.js";
18
18
  import {
19
19
  agent_session_environment,
20
20
  json_value,
21
21
  process_identity,
22
22
  run_carry
23
- } from "./chunk-3LE6VLSR.js";
23
+ } from "./chunk-3C4XRYG6.js";
24
24
  import {
25
25
  stamp_file
26
26
  } from "./chunk-DGT54IH5.js";
@@ -13,7 +13,7 @@ import {
13
13
  repo_party,
14
14
  run_cut,
15
15
  run_hold
16
- } from "./chunk-YZXYOAZ3.js";
16
+ } from "./chunk-7WQBPLUN.js";
17
17
  import {
18
18
  agent_headless,
19
19
  agent_role_profile,
@@ -23,22 +23,22 @@ import {
23
23
  run_event_stream_emit,
24
24
  run_headless,
25
25
  session_language
26
- } from "./chunk-YW452ZAX.js";
26
+ } from "./chunk-Z4XI7CZS.js";
27
27
  import {
28
28
  timed_fetch
29
29
  } from "./chunk-723HKZO5.js";
30
30
  import "./chunk-CDY4NFQG.js";
31
31
  import {
32
32
  hook_decision
33
- } from "./chunk-MU4LJFIV.js";
33
+ } from "./chunk-GPLGZ6IC.js";
34
34
  import {
35
35
  time_density_hook,
36
36
  time_hook_transcript,
37
37
  time_reported_failure
38
- } from "./chunk-XER6SSIB.js";
38
+ } from "./chunk-LIAFCXL7.js";
39
39
  import {
40
40
  lane_child_marker
41
- } from "./chunk-4BHUPFKF.js";
41
+ } from "./chunk-OLMO72ON.js";
42
42
  import {
43
43
  backlog_budget,
44
44
  issue_number_shape,
@@ -47,7 +47,7 @@ import {
47
47
  process_owner_schema,
48
48
  run_carry,
49
49
  run_invocation
50
- } from "./chunk-3LE6VLSR.js";
50
+ } from "./chunk-3C4XRYG6.js";
51
51
  import {
52
52
  stamp_file
53
53
  } from "./chunk-DGT54IH5.js";
@@ -317,6 +317,52 @@ var backlog_ready = {
317
317
 
318
318
  // scripts/notify/telegram-notify.ts
319
319
  import { z } from "zod";
320
+
321
+ // scripts/notify/confirmation-options.ts
322
+ var JAPANESE_LANG_PREFIX = "ja";
323
+ var JAPANESE_HEADINGS = {
324
+ recommended: "\u63A8\u5968:",
325
+ alternatives: "\u4ED6\u306E\u9078\u629E\u80A2:",
326
+ none: "\u306A\u3057"
327
+ };
328
+ var ENGLISH_HEADINGS = {
329
+ recommended: "Recommended:",
330
+ alternatives: "Alternatives:",
331
+ none: "none"
332
+ };
333
+ function headings() {
334
+ const { lang } = session_language.resolve_session_lang();
335
+ return lang.toLowerCase().startsWith(JAPANESE_LANG_PREFIX) ? JAPANESE_HEADINGS : ENGLISH_HEADINGS;
336
+ }
337
+ function alternatives_block(options, labels) {
338
+ if (options.alternatives.length === 0) return `${labels.alternatives} ${labels.none}`;
339
+ const lines = options.alternatives.map((alternative) => `- ${alternative}`);
340
+ return [labels.alternatives, ...lines].join("\n");
341
+ }
342
+ function block(options) {
343
+ const labels = headings();
344
+ return `${labels.recommended} ${options.recommended}
345
+ ${alternatives_block(options, labels)}`;
346
+ }
347
+ var confirmation_options = { block };
348
+
349
+ // scripts/notify/notify-opt-in.ts
350
+ var OPT_IN_KEY = "JOSH_NOTIFY_OPT_IN";
351
+ var OPTIONAL_TASK_TYPES = /* @__PURE__ */ new Set(["planning", "completion"]);
352
+ var LIST_SEPARATOR = ",";
353
+ function opted_in_types() {
354
+ return (process.env[OPT_IN_KEY] ?? "").split(LIST_SEPARATOR).map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
355
+ }
356
+ function is_skipped(task_type) {
357
+ if (!OPTIONAL_TASK_TYPES.has(task_type)) return false;
358
+ return !opted_in_types().includes(task_type);
359
+ }
360
+ function skip_line(task_type) {
361
+ return `\u{1F515} ${task_type} notifications are not opted in (${OPT_IN_KEY}).`;
362
+ }
363
+ var notify_opt_in = { is_skipped, skip_line };
364
+
365
+ // scripts/notify/telegram-notify.ts
320
366
  var TELEGRAM_API_BASE = "https://api.telegram.org";
321
367
  var TELEGRAM_TEXT_LIMIT = 4096;
322
368
  var BODY_TRUNCATION_MARK = "\n...";
@@ -377,6 +423,12 @@ function load_config() {
377
423
  function is_notify_disabled() {
378
424
  return parse_environment_string(process.env[NOTIFY_SWITCH_KEY]).toLowerCase() === NOTIFY_OFF_VALUE;
379
425
  }
426
+ function skip_line2(task_type) {
427
+ if (is_notify_disabled()) {
428
+ return `\u{1F515} Telegram notifications are disabled (${NOTIFY_SWITCH_KEY}=${NOTIFY_OFF_VALUE}).`;
429
+ }
430
+ return notify_opt_in.is_skipped(task_type) ? notify_opt_in.skip_line(task_type) : void 0;
431
+ }
380
432
  function build_header(task_type, repo_name) {
381
433
  const { icon, label } = TASK_DEFINITIONS[task_type];
382
434
  if (repo_name === void 0 || repo_name.length === 0) return `${icon} ${label}`;
@@ -402,6 +454,7 @@ function build_url_parts(input) {
402
454
  function build_blocks(input) {
403
455
  const blocks = [build_title_block(input)];
404
456
  push_if_present(blocks, input.body);
457
+ push_if_present(blocks, input.choices);
405
458
  blocks.push(...build_url_parts(input));
406
459
  return blocks;
407
460
  }
@@ -435,10 +488,9 @@ function build_send_failure(error, config) {
435
488
  return new Error(`${SEND_FAILED_PREFIX}: ${reason}`, { cause: new Error(reason) });
436
489
  }
437
490
  async function send(input) {
438
- if (is_notify_disabled()) {
439
- console.info(
440
- `\u{1F515} Telegram notifications are disabled (${NOTIFY_SWITCH_KEY}=${NOTIFY_OFF_VALUE}).`
441
- );
491
+ const skipped = skip_line2(input.task_type);
492
+ if (skipped !== void 0) {
493
+ console.info(skipped);
442
494
  return;
443
495
  }
444
496
  const config = load_config();
@@ -487,6 +539,7 @@ async function confirm(input) {
487
539
  repo_name: gh_spawn.get_repo_name_with_owner_within(REPO_LOOKUP_TIMEOUT_MS),
488
540
  issue_title: input.issue_title,
489
541
  body: input.body,
542
+ choices: confirmation_options.block(input),
490
543
  issue_url: void 0,
491
544
  pr_url: void 0
492
545
  },
@@ -762,7 +815,7 @@ var stop_payload_schema = z3.object({
762
815
  last_assistant_message: z3.string().nullish()
763
816
  });
764
817
  var NO_OUTCOME = { reason: void 0 };
765
- var STOP_NOTIFY_REASON = '\u26D4 mid-workflow stop notification: this working tree is still held by a run and no `confirmation` Telegram was sent this turn, so a person is being left to wait off-screen without knowing the run paused. `CLAUDE.md` \u2192 "Mid-workflow stop notification" requires `pnpm josh notify --task-type confirmation --issue-url "<url>" --body=$\'<reason>\'` before any mid-workflow pause. Send it, then end with a one-line confirmation that it was sent \u2014 do not repeat your previous reply. This fires only while the hold is held and no notify is on the transcript, and `stop_hook_active` lets a second stop through so a run is never wedged.';
818
+ var STOP_NOTIFY_REASON = '\u26D4 mid-workflow stop notification: this working tree is still held by a run and no `confirmation` Telegram was sent this turn, so a person is being left to wait off-screen without knowing the run paused. `CLAUDE.md` \u2192 "Mid-workflow stop notification" requires `pnpm josh notify --task-type confirmation --issue-url "<url>" --body=$\'<reason>\' --recommended "<choice>" --alternative "<choice>"` before any mid-workflow pause. Send it, then end with a one-line confirmation that it was sent \u2014 do not repeat your previous reply. This fires only while the hold is held and no notify is on the transcript, and `stop_hook_active` lets a second stop through so a run is never wedged.';
766
819
  var HOLD_RELEASE_REASON = "\u26D4 working-tree hold not released: this working tree is clean but its `run:hold` record is still in place, so the next run here runs `pnpm josh ms` believing the tree is free while you hold it. The workflow-commands skill \u2192 `working-tree-hold.md`: a stop that leaves the tree clean releases the hold with `pnpm josh run:release <N>` (bare for a `new` entry). A `halfrun` pre-commit stop and a `needs-human-review` stop keep the hold because their tree is dirty, and a `prrun` stop keeps it by its stop mark \u2014 this row is silent there. Release it, then end with a one-line confirmation that it was released \u2014 do not repeat your previous reply.";
767
820
  var ISSUE_CITATION_REASON = "Session-facing output cites an Issue as a number-link so the reader can click through and see which repository it is (`CLAUDE.md`, `prompts/collaboration-workflow/issue-citation.md`).";
768
821
  function build_citation_reason(references) {
package/dist/josh.js CHANGED
@@ -395,6 +395,7 @@ var START_TIME_FORMAT = "lstart=";
395
395
  var PID_FLAG = "-p";
396
396
  var PROBE_ENVIRONMENT = { TZ: "UTC", LC_ALL: "C" };
397
397
  var LIVENESS_SIGNAL = 0;
398
+ var NO_SUCH_PROCESS = "ESRCH";
398
399
  var LOWEST_REAL_PID = 1;
399
400
  var PROC_START_FIELD_INDEX = 19;
400
401
  var BEACON_SCHEME = "socket:";
@@ -409,15 +410,20 @@ var BEACON_GONE_STATUS = 2;
409
410
  var SOCKET_PROBE_SOURCE = `const net=require('node:net');const socket=net.createConnection(process.argv[1]);socket.once('connect',()=>process.exit(${String(BEACON_LIVE_STATUS)}));socket.once('error',(error)=>process.exit(['ECONNREFUSED','ENOENT'].includes(error.code)?${String(BEACON_GONE_STATUS)}:${String(BEACON_UNKNOWN_STATUS)}));socket.setTimeout(1000,()=>process.exit(${String(BEACON_UNKNOWN_STATUS)}));`;
410
411
  var beacons = /* @__PURE__ */ new Map();
411
412
  var exit_cleanup = { is_registered: false };
412
- function is_live_pid(pid) {
413
- if (pid < LOWEST_REAL_PID) return false;
413
+ function signal_refusal(pid) {
414
414
  try {
415
415
  process.kill(pid, LIVENESS_SIGNAL);
416
- return true;
417
- } catch {
418
- return false;
416
+ return void 0;
417
+ } catch (error) {
418
+ return error.code ?? NO_SUCH_PROCESS;
419
419
  }
420
420
  }
421
+ function is_live_pid(pid) {
422
+ return pid >= LOWEST_REAL_PID && signal_refusal(pid) === void 0;
423
+ }
424
+ function is_present_pid(pid) {
425
+ return pid >= LOWEST_REAL_PID && signal_refusal(pid) !== NO_SUCH_PROCESS;
426
+ }
421
427
  function read_proc_start(pid) {
422
428
  try {
423
429
  const stat = readFileSync3(`/proc/${String(pid)}/stat`, "utf8");
@@ -543,6 +549,11 @@ function is_same_process(pid, recorded_start, read2 = read_start) {
543
549
  if (recorded_start === void 0) return void 0;
544
550
  return matches_recorded_start(pid, recorded_start, read2);
545
551
  }
552
+ function is_present_process(pid, recorded_start, read2 = read_start) {
553
+ if (!is_present_pid(pid)) return false;
554
+ if (recorded_start === void 0) return void 0;
555
+ return matches_recorded_start(pid, recorded_start, read2);
556
+ }
546
557
  function is_own_process(pid, recorded_start) {
547
558
  return pid === process.pid && recorded_start === own_start();
548
559
  }
@@ -551,6 +562,8 @@ var process_identity = {
551
562
  is_live_beacon,
552
563
  is_live_pid,
553
564
  is_own_process,
565
+ is_present_pid,
566
+ is_present_process,
554
567
  is_same_process,
555
568
  open_beacon,
556
569
  own_fields,
@@ -1447,16 +1460,16 @@ var ISSUE_COMMANDS = {
1447
1460
  description: "File an Issue with every filing step: lint, Origin, fold, WIP cap, duplicate scout, labels, then epic:bundle",
1448
1461
  category: "AI tools",
1449
1462
  reference: [
1450
- "<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,\u2026>] [--over-cap] [--no-auto-ok] [--requested] [--release]",
1463
+ "<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,\u2026>] [--over-cap] [--no-auto-ok] [--release]",
1451
1464
  "automation",
1452
1465
  ["network"]
1453
1466
  ]
1454
1467
  },
1455
1468
  "issue:release": {
1456
1469
  script: "scripts/issue/issue-release-cli.ts",
1457
- description: "Link an Issue to the repository's open release Issue as its blocker, filing one when none is open",
1470
+ description: "Link an Issue to the repository's open release Issue as its blocker (or, with --wait, as waiting on it), filing one when none is open",
1458
1471
  category: "AI tools",
1459
- reference: ["<issue>", "automation", ["network"]]
1472
+ reference: ["<issue> [--wait]", "automation", ["network"]]
1460
1473
  }
1461
1474
  };
1462
1475
 
@@ -1741,6 +1754,16 @@ var AI_COMMANDS = {
1741
1754
  category: "AI tools",
1742
1755
  reference: ["[--exclude <n>[,<n>...]] [--label <name>]", "automation", ["network"]]
1743
1756
  },
1757
+ "auto-ok:judge": {
1758
+ script: "scripts/auto-ok/auto-ok-judge-cli.ts",
1759
+ description: "List the open issues backlogrun owes an auto-ok judgement, or record one (--apply / --decline)",
1760
+ category: "AI tools",
1761
+ reference: [
1762
+ "[--apply <n> (--lane | --solo) | --decline <n> --reason <text>]",
1763
+ "automation",
1764
+ ["network"]
1765
+ ]
1766
+ },
1744
1767
  ...BACKLOG_COMMANDS,
1745
1768
  cost: {
1746
1769
  script: "scripts/cost-runtime/cost-cli.ts",
@@ -2528,7 +2551,7 @@ var PROJECT_COMMANDS = {
2528
2551
  description: "Set a project up for the GitHub Issue workflow, from git init to the setup PR",
2529
2552
  category: "Project",
2530
2553
  reference: [
2531
- `${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--init-command <command>] [--push-preflight-command <command>]`,
2554
+ `${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--dry-run] [--init-command <command>] [--push-preflight-command <command>]`,
2532
2555
  "developer",
2533
2556
  ["files", "git", "network", "processes"]
2534
2557
  ]
@@ -2561,7 +2584,7 @@ var PROJECT_COMMANDS = {
2561
2584
  description: "Initialize config in a new project",
2562
2585
  category: "Project",
2563
2586
  reference: [
2564
- `${PROFILE_ARGUMENTS} [--no-install]`,
2587
+ `${PROFILE_ARGUMENTS} [--no-install] [--dry-run]`,
2565
2588
  "developer",
2566
2589
  ["files", "network", "processes"]
2567
2590
  ]
@@ -2724,7 +2747,7 @@ var WORKFLOW_COMMANDS = {
2724
2747
  description: "Send Telegram notification",
2725
2748
  category: "Workflow",
2726
2749
  reference: [
2727
- "[--task-type <type>] [--body <text> | --body-file <path>] [--issue-url <url>] [--pr-url <url>] [--issue-title <text>] [--repo-name <name>]",
2750
+ "[--task-type <type>] [--body <text> | --body-file <path>] [--issue-url <url>] [--pr-url <url>] [--issue-title <text>] [--repo-name <name>] [--recommended <text>] [--alternative <text>]...",
2728
2751
  "automation",
2729
2752
  ["notifications"]
2730
2753
  ],
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1435.0",
2
+ "version": "1.1455.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": [
@@ -6,6 +6,7 @@
6
6
  ## 背景
7
7
 
8
8
  <不具合なら `- 種別: 不具合`、そうでなければ `- 種別: 非不具合` を宣言する>
9
+ <人の判断が要らなければ `- 人の判断: 不要`、要るなら `- 人の判断: 要(<方針の判断 / 外部アカウント / 目視確認>)` を宣言する>
9
10
 
10
11
  <なぜ必要か>
11
12
 
@@ -49,15 +50,16 @@
49
50
 
50
51
  ### `## 背景` 直下の宣言と分類ラベル
51
52
 
52
- 起票は `pnpm josh issue:file "<title>" --body-file <本文ファイル> --depth <n>` で行う。本文の検査(`pnpm josh issue:lint <path>` と同じ)と、下表の宣言から決まる分類ラベルの付与は、このコマンドが 1 回の作成リクエストで行う。**宣言を選ぶのは起票者の判断であり、コマンドは単語から推測しない。** 人が依頼した起票(会話での依頼も含む)には `--requested` を付ける(`kickoff.md` → "Words typed after `new`")。
53
+ 起票は `pnpm josh issue:file "<title>" --body-file <本文ファイル> --depth <n>` で行う。本文の検査(`pnpm josh issue:lint <path>` と同じ)と、下表の宣言から決まる分類ラベルの付与は、このコマンドが 1 回の作成リクエストで行う。**宣言を選ぶのは起票者の判断であり、コマンドは単語から推測しない。** `- 人の判断:` の行は必須で、`issue:file` だけが検査し(`issue:lint` は見ない)、`auto-ok` の判定に使う。判定は人が依頼した起票も含めてすべて同じ(`scripts/issue/issue-auto-ok.ts`、`docs/how-to/run-backlog.md`)。**`不要` を宣言する起票には `--label run:lane`(kit 自身の検証の不具合なら `run:solo`)も付ける** — 付いていなければ `issue:file` が拒否する。
53
54
 
54
- | 宣言 | 付くラベル | 選ぶ基準 |
55
- | ---------------------- | ----------------- | ------------------------------------------------------------------------------------------ |
56
- | `- 種別: 不具合` | `bug` | 報告された症状を直す。`- 種別: 非不具合` とどちらか一方を必ず置く |
57
- | `- 目的: 機能追加` | `enhancement` | 新しい機能。不具合修正・文書のみ・依存更新・保守作業には書かない |
58
- | `- 目的: 機能改善` | `enhancement` | 既存機能の改善。目的が曖昧なら期待結果と受け入れ条件を具体化してから選ぶ |
59
- | `- 互換性: 破壊的変更` | `breaking-change` | 既存の利用者・API・設定の契約を変える。移行措置があっても書き、廃止対象と影響を説明する |
60
- | `- 種別: 振る舞い変更` | なし | AI の振る舞いを変える。`## 発火点` / `## ベースライン` / `## 再現` の 3 見出しが必須になる |
55
+ | 宣言 | 付くラベル | 選ぶ基準 |
56
+ | ---------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------- |
57
+ | `- 種別: 不具合` | `bug` | 報告された症状を直す。`- 種別: 非不具合` とどちらか一方を必ず置く |
58
+ | `- 目的: 機能追加` | `enhancement` | 新しい機能。不具合修正・文書のみ・依存更新・保守作業には書かない |
59
+ | `- 目的: 機能改善` | `enhancement` | 既存機能の改善。目的が曖昧なら期待結果と受け入れ条件を具体化してから選ぶ |
60
+ | `- 互換性: 破壊的変更` | `breaking-change` | 既存の利用者・API・設定の契約を変える。移行措置があっても書き、廃止対象と影響を説明する |
61
+ | `- 種別: 振る舞い変更` | なし | AI の振る舞いを変える。`## 発火点` / `## ベースライン` / `## 再現` の 3 見出しが必須になる |
62
+ | `- 人の判断: 不要` | `auto-ok`(除外ラベルがなければ) | 方針の判断・外部アカウント・人の目視確認のどれも要らない。`- 人の判断: 要(<どれか>)` とどちらか一方を必ず置く |
61
63
 
62
64
  - **持ち主の意図と違う動きは、コード・コメント・テストに書かれた設計どおりでも `- 種別: 不具合` とする。** コードは意図を写したもので、意図そのものではない。変更の形(コードか文書か)でも決めない。例: コメントとテストに固定されたまま意図より広く働く自動付与、文書だけでも意図と違う分類を生む基準
63
65
  - **`- 種別: 不具合` の Issue の回帰テストは、利用者が症状を見たのと同じ粒度で書く。** 修正前のツリーでも緑になるテストは `pnpm josh git -y` がコミットを拒否する(joshuafolkken/kit#2448)
@@ -25,7 +25,7 @@ zsh は `pnpm josh ms` を実行し、その標準出力を本文に埋め込む
25
25
  pnpm josh issue:comment <N> --body-file <path>
26
26
  gh api -X PATCH repos/{owner}/{repo}/issues/<N> --field body=@<path>
27
27
  pnpm josh followup "<title> #<N>" --notify-message-file <path>
28
- pnpm josh notify --task-type confirmation --issue-url "<url>" --body-file <path>
28
+ pnpm josh notify --task-type confirmation --issue-url "<url>" --body-file <path> --recommended "<choice>"
29
29
  ```
30
30
 
31
31
  **PR コメントも同じ経路である** — REST では pull request のコメントは issue のコメントであり、`pnpm josh issue:comment <N> --body-file <path>` がそのまま使える。`gh issue comment` / `gh pr comment` は使わない(GraphQL 経由。理由は `docs/maintainers/shell-body-rationale.md` → "Why no `gh` subcommand is written in an executable block")。
package/prompts/review.md CHANGED
@@ -100,10 +100,14 @@ round-1 High/Medium finding actually closed — by a fix in this working tree, o
100
100
  positive — and none was filed or deferred. **Without the flag the answer is `required`**, which is also
101
101
  what the command answers to every uncertainty it meets, a missing round-1 snapshot included.
102
102
 
103
- **`skip` has exactly two arms** — **A** (no fix code: the fix delta is empty) and **B** (inert fix
104
- code: every path in the fix delta is inert by the review-level classification above); the reason line
105
- names which fired. Why neither weakens the standard: `docs/maintainers/review-history.md` → "The two
106
- skip arms".
103
+ **`skip` has exactly two arms** — **A** (no fix code: the fix delta is empty) and **B** (a fix that
104
+ leaves what executes unchanged: every path in the fix delta is inert by the review-level classification
105
+ above, `docs/**/*.md`, the root `README.md`, under `prompts/`, or a `.ts` / `.js` / `.svelte` file
106
+ outside `.claude/` whose only change is in comments, a directive comment — lint, type-check, coverage
107
+ or bundler — counting as code); the reason line names which fired. **One path
108
+ outside that set makes the round `required`** — `.claude/**`, `CLAUDE.md`, a test, configuration, a
109
+ code line, or a source file whose comment-free text cannot be read. Why neither arm weakens the
110
+ standard: `docs/maintainers/review-history.md` → "The two skip arms".
107
111
 
108
112
  **A skipped round is recorded on the Issue**, so the condition stays auditable and can be withdrawn if
109
113
  a defect is later traced to a skipped delta:
@@ -0,0 +1,125 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import { RUN_LANE_LABEL, RUN_SOLO_LABEL } from '#scripts/issue/issue-labels'
3
+ import { issue_number_shape } from '#scripts/issue/issue-number-shape'
4
+ import { session_cite } from '#scripts/issue/session-cite'
5
+ import { cli_flags } from '#scripts/lib/cli-flags'
6
+ import { auto_ok_judge_read } from './auto-ok-judge-read'
7
+ import { auto_ok_judge_write } from './auto-ok-judge-write'
8
+
9
+ // `josh auto-ok:judge` — the open issues `backlogrun` judges for `auto-ok` when a lane is free and
10
+ // nothing is opted in (joshuafolkken/kit#3854), and the record of each judgement.
11
+ //
12
+ // Bare, it lists the issues that still owe a judgement: one number per line on stdout, or `none`, with
13
+ // each title on stderr. The agent reads each one's body and comments (`josh issue:read`) and answers
14
+ // with `--apply <N> --lane|--solo` or `--decline <N> --reason <text>`; which of the two is a reading of
15
+ // the issue, never this command's.
16
+
17
+ const ARGV_OFFSET = 2
18
+ const SUCCESS_EXIT_CODE = 0
19
+ const FAILURE_EXIT_CODE = 1
20
+ const NONE_TOKEN = 'none'
21
+ const USAGE =
22
+ 'Usage: josh auto-ok:judge [--apply <N> (--lane | --solo) | --decline <N> --reason <text>]'
23
+
24
+ const OPTIONS = {
25
+ apply: { type: 'string' },
26
+ decline: { type: 'string' },
27
+ lane: { type: 'boolean' },
28
+ solo: { type: 'boolean' },
29
+ reason: { type: 'string' },
30
+ } as const
31
+
32
+ type Values = NonNullable<ReturnType<typeof cli_flags.values_of<typeof OPTIONS>>>
33
+
34
+ type JudgeRequest =
35
+ | { kind: 'list' }
36
+ | { kind: 'apply'; issue: string; run_label: string }
37
+ | { kind: 'decline'; issue: string; reason: string }
38
+
39
+ function run_label_of(values: Values): string | undefined {
40
+ if (values.lane === values.solo) return undefined
41
+
42
+ return values.lane === true ? RUN_LANE_LABEL : RUN_SOLO_LABEL
43
+ }
44
+
45
+ function apply_request(issue: string, values: Values): JudgeRequest | undefined {
46
+ const run_label = run_label_of(values)
47
+
48
+ return run_label === undefined ? undefined : { kind: 'apply', issue, run_label }
49
+ }
50
+
51
+ function decline_request(issue: string, values: Values): JudgeRequest | undefined {
52
+ const reason = values.reason?.trim() ?? ''
53
+
54
+ return reason === '' ? undefined : { kind: 'decline', issue, reason }
55
+ }
56
+
57
+ function request_of(values: Values): JudgeRequest | undefined {
58
+ const issue = values.apply ?? values.decline
59
+
60
+ if (issue === undefined) return { kind: 'list' }
61
+ if (!issue_number_shape.is_issue_number(issue)) return undefined
62
+
63
+ return values.apply === undefined ? decline_request(issue, values) : apply_request(issue, values)
64
+ }
65
+
66
+ function parse_request(argv: ReadonlyArray<string>): JudgeRequest | undefined {
67
+ const values = cli_flags.values_of(argv, OPTIONS)
68
+
69
+ if (values === undefined || (values.apply !== undefined && values.decline !== undefined)) {
70
+ return undefined
71
+ }
72
+
73
+ return request_of(values)
74
+ }
75
+
76
+ async function list(): Promise<number> {
77
+ const owed = await auto_ok_judge_read.owed()
78
+
79
+ if (owed === undefined) {
80
+ console.error('the open issues could not be read')
81
+
82
+ return FAILURE_EXIT_CODE
83
+ }
84
+
85
+ if (owed.length === 0) console.info(NONE_TOKEN)
86
+
87
+ for (const row of owed) {
88
+ console.error(session_cite.issue(row.number, row.title))
89
+ console.info(String(row.number))
90
+ }
91
+
92
+ return SUCCESS_EXIT_CODE
93
+ }
94
+
95
+ async function dispatch(request: JudgeRequest): Promise<number> {
96
+ const { apply, decline } = auto_ok_judge_write
97
+
98
+ if (request.kind === 'list') return await list()
99
+ if (request.kind === 'decline') return await decline(request.issue, request.reason)
100
+
101
+ return await apply(request.issue, request.run_label)
102
+ }
103
+
104
+ async function run(argv: ReadonlyArray<string>): Promise<number> {
105
+ const request = parse_request(argv)
106
+
107
+ if (request === undefined) {
108
+ console.error(USAGE)
109
+
110
+ return FAILURE_EXIT_CODE
111
+ }
112
+
113
+ return await dispatch(request)
114
+ }
115
+
116
+ // `process.exitCode` rather than `process.exit()`, so the report on standard output drains first.
117
+ async function main(argv: ReadonlyArray<string>): Promise<void> {
118
+ process.exitCode = await run(argv)
119
+ }
120
+
121
+ const auto_ok_judge_cli = { NONE_TOKEN, run }
122
+
123
+ if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
124
+
125
+ export { auto_ok_judge_cli }
@@ -0,0 +1,95 @@
1
+ import { git_gh_command } from '#scripts/gh/git-gh-command'
2
+ import { issue_label_schema } from '#scripts/git/git-schemas'
3
+ import { read_json_listing } from '#scripts/git/parse-json-array'
4
+ import { issue_watch_diff } from '#scripts/issue/issue-watch-diff'
5
+ import { bounded_pool } from '#scripts/lib/bounded-pool'
6
+ import { z } from 'zod'
7
+ import { auto_ok_judge, type JudgeIssue } from './auto-ok-judge'
8
+
9
+ // The reading half of `josh auto-ok:judge`: the open issues that still owe a judgement, shared by the
10
+ // command's listing and the backlog driver's check for a free lane with nothing opted in.
11
+ //
12
+ // **The driver asks once a minute while it watches**, so it hands in a cache it keeps for its process:
13
+ // an issue seen judged is remembered with its `updatedAt`, and its comments are read again only once
14
+ // that moves — a body edit, a comment or a label change. Without it every watch minute would read the
15
+ // comments of every candidate in the repository.
16
+
17
+ // The listing cap `auto-ok:next` reads the opted-in pool under; the open backlog is read under the same.
18
+ const LISTING_LIMIT = 200
19
+ // How many comment listings run at once — a handful of `gh` processes, never one per candidate.
20
+ const COMMENT_READ_WIDTH = 4
21
+
22
+ const row_schema = z.looseObject({
23
+ number: z.number(),
24
+ title: z.string(),
25
+ body: z.string().nullish(),
26
+ labels: z.array(issue_label_schema),
27
+ updatedAt: z.string(),
28
+ })
29
+
30
+ interface JudgeRow extends JudgeIssue {
31
+ title: string
32
+ updated_at: string
33
+ }
34
+
35
+ // An issue's number to the `updatedAt` it was last seen judged at.
36
+ type JudgedCache = Map<number, string>
37
+
38
+ function to_row(raw: z.infer<typeof row_schema>): JudgeRow {
39
+ return {
40
+ number: raw.number,
41
+ title: raw.title,
42
+ body: raw.body ?? '',
43
+ labels: raw.labels.map((label) => label.name),
44
+ updated_at: raw.updatedAt,
45
+ }
46
+ }
47
+
48
+ // `undefined` when the listing could not be read, which is never "nothing to judge".
49
+ async function read_candidates(): Promise<ReadonlyArray<JudgeRow> | undefined> {
50
+ const { json } = await git_gh_command.issue_list_open_for_judgement(LISTING_LIMIT)
51
+
52
+ if (json === undefined) return undefined
53
+
54
+ const listing = read_json_listing(json, row_schema)
55
+
56
+ if (listing.kind !== 'read') return undefined
57
+
58
+ return listing.rows.map((raw) => to_row(raw)).filter((row) => auto_ok_judge.is_candidate(row))
59
+ }
60
+
61
+ // `true` only for an issue known to owe a judgement: a failed comment read learns nothing, so it is
62
+ // neither counted nor remembered.
63
+ async function owes_judgement(row: JudgeRow, cache: JudgedCache): Promise<boolean> {
64
+ if (cache.get(row.number) === row.updated_at) return false
65
+
66
+ const comments = issue_watch_diff.parse_comments(
67
+ await git_gh_command.issue_list_comments(String(row.number)),
68
+ )
69
+
70
+ if (comments === undefined) return false
71
+ if (!auto_ok_judge.is_judged(row.body, comments)) return true
72
+
73
+ cache.set(row.number, row.updated_at)
74
+
75
+ return false
76
+ }
77
+
78
+ async function owed(cache: JudgedCache = new Map()): Promise<ReadonlyArray<JudgeRow> | undefined> {
79
+ const candidates = await read_candidates()
80
+
81
+ if (candidates === undefined) return undefined
82
+
83
+ const owing = await bounded_pool.bounded_map(
84
+ candidates,
85
+ COMMENT_READ_WIDTH,
86
+ async (row) => await owes_judgement(row, cache),
87
+ )
88
+
89
+ return candidates.filter((_row, index) => owing[index] === true)
90
+ }
91
+
92
+ const auto_ok_judge_read = { owed }
93
+
94
+ export type { JudgedCache, JudgeRow }
95
+ export { auto_ok_judge_read }