@joshuafolkken/kit 1.1435.0 → 1.1453.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 (88) hide show
  1. package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
  2. package/.claude/skills/workflow-commands/backlogrun-steps.md +9 -7
  3. package/.claude/skills/workflow-commands/backlogrun.md +2 -2
  4. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  5. package/.claude/skills/workflow-commands/halfrun.md +1 -1
  6. package/.claude/skills/workflow-commands/kickoff.md +12 -13
  7. package/.claude/skills/workflow-commands/observation-filing.md +1 -1
  8. package/.claude/skills/workflow-commands/prerequisite.md +6 -0
  9. package/.claude/skills/workflow-commands/retrospective.md +5 -5
  10. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  11. package/README.md +15 -17
  12. package/dist/hooks/batch-guard.js +6 -6
  13. package/dist/hooks/{chunk-3LE6VLSR.js → chunk-3C4XRYG6.js} +18 -5
  14. package/dist/hooks/{chunk-JXQBK323.js → chunk-5C7L6FRC.js} +131 -104
  15. package/dist/hooks/{chunk-EMB55BDF.js → chunk-D3NJXN5M.js} +2 -2
  16. package/dist/hooks/{chunk-7KMN5QB5.js → chunk-EF5GQ6WS.js} +3 -3
  17. package/dist/hooks/{chunk-2G5F2YCD.js → chunk-LBFYEOXJ.js} +3 -3
  18. package/dist/hooks/{chunk-XER6SSIB.js → chunk-MBJYQPEK.js} +16 -6
  19. package/dist/hooks/{chunk-QL6QTPWN.js → chunk-NO4AALP5.js} +3 -3
  20. package/dist/hooks/{chunk-4BHUPFKF.js → chunk-OLMO72ON.js} +1 -1
  21. package/dist/hooks/{chunk-MSYSUSY6.js → chunk-SHYUQOIS.js} +5 -5
  22. package/dist/hooks/{chunk-5SJSSCJG.js → chunk-UT7YC3N2.js} +1 -1
  23. package/dist/hooks/{chunk-MU4LJFIV.js → chunk-W5ZKGTSY.js} +3 -3
  24. package/dist/hooks/{chunk-YZXYOAZ3.js → chunk-Y3GKIKGW.js} +11 -5
  25. package/dist/hooks/{chunk-YW452ZAX.js → chunk-Z4XI7CZS.js} +2 -2
  26. package/dist/hooks/codex-hook-adapter.js +13 -13
  27. package/dist/hooks/duplicate-read-guard.js +7 -7
  28. package/dist/hooks/format-edited.js +3 -3
  29. package/dist/hooks/inputs.json +241 -240
  30. package/dist/hooks/investigation-guard.js +7 -7
  31. package/dist/hooks/pretool-guard.js +12 -12
  32. package/dist/hooks/session-lang.js +3 -3
  33. package/dist/hooks/stop-guard.js +6 -6
  34. package/dist/josh.js +33 -10
  35. package/package.json +1 -1
  36. package/prompts/collaboration-workflow/issue-template.md +10 -8
  37. package/prompts/review.md +8 -4
  38. package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
  39. package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
  40. package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
  41. package/scripts/auto-ok/auto-ok-judge.ts +67 -0
  42. package/scripts/backlog/backlog-drive-cli.ts +14 -0
  43. package/scripts/backlog/backlog-drive.ts +22 -3
  44. package/scripts/document/document-byte-budget.ts +1 -1
  45. package/scripts/fresh-machine/fresh-machine-patterns.ts +60 -15
  46. package/scripts/gate/project-checks.ts +5 -2
  47. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  48. package/scripts/gh/git-gh-issue.ts +9 -0
  49. package/scripts/init/init-actions.ts +3 -3
  50. package/scripts/init/init-ai-copy.ts +1 -1
  51. package/scripts/init/init-basic.ts +1 -1
  52. package/scripts/init/init-bootstrap.ts +6 -2
  53. package/scripts/init/init-dry-run-report.ts +150 -0
  54. package/scripts/init/init-dry-run.ts +189 -0
  55. package/scripts/init/init-install.ts +8 -1
  56. package/scripts/init/init.ts +57 -19
  57. package/scripts/init/kit-setup-state.ts +11 -17
  58. package/scripts/init/own-formatter.ts +21 -0
  59. package/scripts/init/project-profile.ts +25 -6
  60. package/scripts/init/start-dry-run.ts +70 -0
  61. package/scripts/init/start-plan.ts +8 -3
  62. package/scripts/init/start-setup-pr.ts +13 -24
  63. package/scripts/init/start-steps.ts +16 -1
  64. package/scripts/init/start.ts +38 -18
  65. package/scripts/init/working-tree.ts +116 -0
  66. package/scripts/issue/issue-auto-ok.ts +68 -99
  67. package/scripts/issue/issue-file-cli.ts +17 -17
  68. package/scripts/issue/issue-file.ts +1 -6
  69. package/scripts/issue/issue-labels.ts +6 -2
  70. package/scripts/issue/issue-release-cli.ts +69 -23
  71. package/scripts/issue/issue-release.ts +3 -1
  72. package/scripts/josh/josh-commands-ai.ts +11 -0
  73. package/scripts/josh/josh-commands-issue.ts +3 -3
  74. package/scripts/josh/josh-commands-project.ts +2 -2
  75. package/scripts/josh/process-identity.ts +32 -6
  76. package/scripts/lane/lane-broker-server.ts +69 -0
  77. package/scripts/lane/lane-session-broker.ts +37 -14
  78. package/scripts/observations/observation-ledger-migrate.ts +3 -15
  79. package/scripts/release/release-issue-close.ts +133 -0
  80. package/scripts/release/release-publish.ts +12 -1
  81. package/scripts/repo/repository-labels.ts +11 -1
  82. package/scripts/review/comment-ranges.ts +199 -0
  83. package/scripts/review/review-brief-cli.ts +22 -5
  84. package/scripts/review/review-code-tree.ts +119 -0
  85. package/scripts/review/review-round2-cli.ts +16 -5
  86. package/scripts/review/review-round2.ts +30 -10
  87. package/scripts/review/review-stamps.ts +22 -1
  88. 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-5C7L6FRC.js";
5
+ import "./chunk-SHYUQOIS.js";
6
+ import "./chunk-Y3GKIKGW.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-EF5GQ6WS.js";
11
+ import "./chunk-LBFYEOXJ.js";
12
+ import "./chunk-NO4AALP5.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-W5ZKGTSY.js";
17
+ import "./chunk-MBJYQPEK.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-Y3GKIKGW.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-W5ZKGTSY.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-MBJYQPEK.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";
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
  ]
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1435.0",
2
+ "version": "1.1453.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)
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 }
@@ -0,0 +1,109 @@
1
+ import { git_gh_command } from '#scripts/gh/git-gh-command'
2
+ import { issue_label_schema } from '#scripts/git/git-schemas'
3
+ import { parse_json_object_or_undefined } from '#scripts/git/parse-json-array'
4
+ import { issue_auto_ok, type AutoOkDecision } from '#scripts/issue/issue-auto-ok'
5
+ import { issue_cite } from '#scripts/issue/issue-cite'
6
+ import { AUTO_OK_LABEL } from '#scripts/issue/issue-labels'
7
+ import { session_cite } from '#scripts/issue/session-cite'
8
+ import { run_event_stream } from '#scripts/run/event/run-event-stream'
9
+ import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
10
+ import { z } from 'zod'
11
+ import { auto_ok_judge } from './auto-ok-judge'
12
+
13
+ // The writing half of `josh auto-ok:judge`: the agent's judgement of one open issue, recorded on it.
14
+ //
15
+ // **An applied judgement still passes the mechanical exclusions** — `issue_auto_ok.exclusion_of` is
16
+ // re-read against the labels as they stand, so a label added since the listing refuses the write
17
+ // rather than racing it. **The run label lands before `auto-ok`**, so the pool never holds an opted-in
18
+ // issue that `backlog:next` would withhold as untriaged. Both answers leave the judgement comment the
19
+ // next listing reads as the record, and an applied one is a `note` on the board's 📌.
20
+
21
+ const SUCCESS_EXIT_CODE = 0
22
+ const FAILURE_EXIT_CODE = 1
23
+ const UNREADABLE = 'the issue could not be read'
24
+
25
+ const issue_schema = z.object({ labels: z.array(issue_label_schema), body: z.string().nullish() })
26
+
27
+ interface IssueRead {
28
+ labels: ReadonlyArray<string>
29
+ body: string
30
+ }
31
+
32
+ async function read_issue(issue: string): Promise<IssueRead | undefined> {
33
+ const raw = await git_gh_command.issue_get_labels_and_body(issue)
34
+ const parsed = raw === undefined ? undefined : parse_json_object_or_undefined(raw, issue_schema)
35
+
36
+ if (parsed === undefined) return undefined
37
+
38
+ return { labels: parsed.labels.map((label) => label.name), body: parsed.body ?? '' }
39
+ }
40
+
41
+ async function apply_labels(issue: string, run_label: string): Promise<string | undefined> {
42
+ for (const label of [run_label, AUTO_OK_LABEL]) {
43
+ // eslint-disable-next-line no-await-in-loop -- the run label must land before `auto-ok`
44
+ const write = await git_gh_command.issue_apply_label(issue, label)
45
+
46
+ if (!write.is_applied) return `could not apply \`${label}\`: ${write.reason}`
47
+ }
48
+
49
+ return undefined
50
+ }
51
+
52
+ async function record(issue: string, decision: AutoOkDecision, body: string): Promise<number> {
53
+ try {
54
+ await git_gh_command.issue_comment(issue, auto_ok_judge.comment_of(decision, body))
55
+ } catch {
56
+ console.error(`could not comment the judgement on ${session_cite.issue(issue)}`)
57
+
58
+ return FAILURE_EXIT_CODE
59
+ }
60
+
61
+ console.info(issue_auto_ok.line_of(decision))
62
+
63
+ return SUCCESS_EXIT_CODE
64
+ }
65
+
66
+ function refused(issue: string, problem: string): number {
67
+ console.error(`${session_cite.issue(issue)}: ${problem}`)
68
+
69
+ return FAILURE_EXIT_CODE
70
+ }
71
+
72
+ async function apply_to(issue: string, run_label: string, read: IssueRead): Promise<number> {
73
+ const exclusion = issue_auto_ok.exclusion_of(read.labels)
74
+
75
+ if (exclusion !== undefined) return refused(issue, issue_auto_ok.line_of(exclusion))
76
+
77
+ const problem = await apply_labels(issue, run_label)
78
+
79
+ if (problem !== undefined) return refused(issue, problem)
80
+
81
+ const reason = `judged by backlogrun: no exclusion holds and no person's judgement is needed (${run_label})`
82
+
83
+ await run_event_stream_emit.emit(
84
+ run_event_stream.EVENT_KIND.NOTE,
85
+ `${issue_cite.plain(issue)} auto-ok applied (${run_label})`,
86
+ )
87
+
88
+ return await record(issue, { is_applied: true, reason }, read.body)
89
+ }
90
+
91
+ async function apply(issue: string, run_label: string): Promise<number> {
92
+ const read = await read_issue(issue)
93
+
94
+ if (read === undefined) return refused(issue, UNREADABLE)
95
+
96
+ return await apply_to(issue, run_label, read)
97
+ }
98
+
99
+ async function decline(issue: string, reason: string): Promise<number> {
100
+ const read = await read_issue(issue)
101
+
102
+ if (read === undefined) return refused(issue, UNREADABLE)
103
+
104
+ return await record(issue, { is_applied: false, reason }, read.body)
105
+ }
106
+
107
+ const auto_ok_judge_write = { apply, decline }
108
+
109
+ export { auto_ok_judge_write }