@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.
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +9 -7
- package/.claude/skills/workflow-commands/backlogrun.md +2 -2
- package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +1 -1
- package/.claude/skills/workflow-commands/kickoff.md +12 -13
- package/.claude/skills/workflow-commands/observation-filing.md +1 -1
- package/.claude/skills/workflow-commands/prerequisite.md +6 -0
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/README.md +15 -17
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-3LE6VLSR.js → chunk-3C4XRYG6.js} +18 -5
- package/dist/hooks/{chunk-JXQBK323.js → chunk-5C7L6FRC.js} +131 -104
- package/dist/hooks/{chunk-EMB55BDF.js → chunk-D3NJXN5M.js} +2 -2
- package/dist/hooks/{chunk-7KMN5QB5.js → chunk-EF5GQ6WS.js} +3 -3
- package/dist/hooks/{chunk-2G5F2YCD.js → chunk-LBFYEOXJ.js} +3 -3
- package/dist/hooks/{chunk-XER6SSIB.js → chunk-MBJYQPEK.js} +16 -6
- package/dist/hooks/{chunk-QL6QTPWN.js → chunk-NO4AALP5.js} +3 -3
- package/dist/hooks/{chunk-4BHUPFKF.js → chunk-OLMO72ON.js} +1 -1
- package/dist/hooks/{chunk-MSYSUSY6.js → chunk-SHYUQOIS.js} +5 -5
- package/dist/hooks/{chunk-5SJSSCJG.js → chunk-UT7YC3N2.js} +1 -1
- package/dist/hooks/{chunk-MU4LJFIV.js → chunk-W5ZKGTSY.js} +3 -3
- package/dist/hooks/{chunk-YZXYOAZ3.js → chunk-Y3GKIKGW.js} +11 -5
- package/dist/hooks/{chunk-YW452ZAX.js → chunk-Z4XI7CZS.js} +2 -2
- package/dist/hooks/codex-hook-adapter.js +13 -13
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +3 -3
- package/dist/hooks/inputs.json +241 -240
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +3 -3
- package/dist/hooks/stop-guard.js +6 -6
- package/dist/josh.js +33 -10
- package/package.json +1 -1
- package/prompts/collaboration-workflow/issue-template.md +10 -8
- package/prompts/review.md +8 -4
- package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
- package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
- package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
- package/scripts/auto-ok/auto-ok-judge.ts +67 -0
- package/scripts/backlog/backlog-drive-cli.ts +14 -0
- package/scripts/backlog/backlog-drive.ts +22 -3
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/fresh-machine/fresh-machine-patterns.ts +60 -15
- package/scripts/gate/project-checks.ts +5 -2
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue.ts +9 -0
- package/scripts/init/init-actions.ts +3 -3
- package/scripts/init/init-ai-copy.ts +1 -1
- package/scripts/init/init-basic.ts +1 -1
- package/scripts/init/init-bootstrap.ts +6 -2
- package/scripts/init/init-dry-run-report.ts +150 -0
- package/scripts/init/init-dry-run.ts +189 -0
- package/scripts/init/init-install.ts +8 -1
- package/scripts/init/init.ts +57 -19
- package/scripts/init/kit-setup-state.ts +11 -17
- package/scripts/init/own-formatter.ts +21 -0
- package/scripts/init/project-profile.ts +25 -6
- package/scripts/init/start-dry-run.ts +70 -0
- package/scripts/init/start-plan.ts +8 -3
- package/scripts/init/start-setup-pr.ts +13 -24
- package/scripts/init/start-steps.ts +16 -1
- package/scripts/init/start.ts +38 -18
- package/scripts/init/working-tree.ts +116 -0
- package/scripts/issue/issue-auto-ok.ts +68 -99
- package/scripts/issue/issue-file-cli.ts +17 -17
- package/scripts/issue/issue-file.ts +1 -6
- package/scripts/issue/issue-labels.ts +6 -2
- package/scripts/issue/issue-release-cli.ts +69 -23
- package/scripts/issue/issue-release.ts +3 -1
- package/scripts/josh/josh-commands-ai.ts +11 -0
- package/scripts/josh/josh-commands-issue.ts +3 -3
- package/scripts/josh/josh-commands-project.ts +2 -2
- package/scripts/josh/process-identity.ts +32 -6
- package/scripts/lane/lane-broker-server.ts +69 -0
- package/scripts/lane/lane-session-broker.ts +37 -14
- package/scripts/observations/observation-ledger-migrate.ts +3 -15
- package/scripts/release/release-issue-close.ts +133 -0
- package/scripts/release/release-publish.ts +12 -1
- package/scripts/repo/repository-labels.ts +11 -1
- package/scripts/review/comment-ranges.ts +199 -0
- package/scripts/review/review-brief-cli.ts +22 -5
- package/scripts/review/review-code-tree.ts +119 -0
- package/scripts/review/review-round2-cli.ts +16 -5
- package/scripts/review/review-round2.ts +30 -10
- package/scripts/review/review-stamps.ts +22 -1
- 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-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-5C7L6FRC.js";
|
|
5
|
+
import "./chunk-SHYUQOIS.js";
|
|
6
|
+
import "./chunk-Y3GKIKGW.js";
|
|
7
7
|
import "./chunk-IX6TTTI6.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-Z4XI7CZS.js";
|
|
9
9
|
import "./chunk-CDY4NFQG.js";
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
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-
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-
|
|
19
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
23
|
+
} from "./chunk-3C4XRYG6.js";
|
|
24
24
|
import {
|
|
25
25
|
stamp_file
|
|
26
26
|
} from "./chunk-DGT54IH5.js";
|
package/dist/hooks/stop-guard.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
repo_party,
|
|
14
14
|
run_cut,
|
|
15
15
|
run_hold
|
|
16
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
38
|
+
} from "./chunk-MBJYQPEK.js";
|
|
39
39
|
import {
|
|
40
40
|
lane_child_marker
|
|
41
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
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
|
|
417
|
-
} catch {
|
|
418
|
-
return
|
|
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] [--
|
|
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.
|
|
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 回の作成リクエストで行う。**宣言を選ぶのは起票者の判断であり、コマンドは単語から推測しない。**
|
|
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`
|
|
60
|
-
| `- 種別: 振る舞い変更` | なし
|
|
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** (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 }
|