@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.
- package/.claude/skills/workflow-commands/backlogrun-handoff-report.md +1 -1
- 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 +3 -2
- 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/prrun.md +2 -1
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/CLAUDE.md +1 -1
- package/README.md +10 -19
- package/dist/CLAUDE.md +1 -1
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-3LE6VLSR.js → chunk-3C4XRYG6.js} +18 -5
- package/dist/hooks/{chunk-7KMN5QB5.js → chunk-5H4SSDSD.js} +3 -3
- package/dist/hooks/{chunk-YZXYOAZ3.js → chunk-7WQBPLUN.js} +11 -5
- package/dist/hooks/{chunk-2G5F2YCD.js → chunk-D5QBTOCZ.js} +3 -3
- package/dist/hooks/{chunk-MU4LJFIV.js → chunk-GPLGZ6IC.js} +3 -3
- package/dist/hooks/{chunk-MSYSUSY6.js → chunk-HFGWYGUV.js} +5 -5
- package/dist/hooks/{chunk-JXQBK323.js → chunk-HHI6FO3R.js} +132 -105
- package/dist/hooks/{chunk-XER6SSIB.js → chunk-LIAFCXL7.js} +17 -7
- package/dist/hooks/{chunk-4BHUPFKF.js → chunk-OLMO72ON.js} +1 -1
- package/dist/hooks/{chunk-EMB55BDF.js → chunk-TBJFTSUY.js} +2 -2
- package/dist/hooks/{chunk-5SJSSCJG.js → chunk-UT7YC3N2.js} +1 -1
- package/dist/hooks/{chunk-QL6QTPWN.js → chunk-WNLVGT7E.js} +3 -3
- 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 +229 -226
- 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 +64 -11
- package/dist/josh.js +34 -11
- package/package.json +1 -1
- package/prompts/collaboration-workflow/issue-template.md +10 -8
- package/prompts/collaboration-workflow/shell-body.md +1 -1
- 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/gh/git-pr-ai-review.ts +12 -1
- package/scripts/gh/git-pr-confirmation.ts +11 -7
- 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/josh-commands-workflow.ts +1 -1
- 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/notify/confirmation-options.ts +59 -0
- package/scripts/notify/notify-opt-in.ts +33 -0
- package/scripts/notify/telegram-notify.ts +25 -8
- package/scripts/notify/telegram-test-logic.ts +33 -1
- package/scripts/notify/telegram-test.ts +4 -2
- 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/rules/shell-body-trigger.ts +3 -2
- package/scripts/rules/stop-rules.ts +2 -1
- package/scripts/run/entry/run-entry-stop.ts +6 -3
- package/scripts/run/run-stop-notify.ts +2 -0
- package/scripts/uninstall/distributed-files.ts +1 -1
|
@@ -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 }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { issue_auto_ok, type AutoOkDecision } from '#scripts/issue/issue-auto-ok'
|
|
3
|
+
import { AUTO_OK_LABEL, has_label_name } from '#scripts/issue/issue-labels'
|
|
4
|
+
import type { WatchComment } from '#scripts/issue/issue-watch-diff'
|
|
5
|
+
|
|
6
|
+
// The pure half of `josh auto-ok:judge`: which open issues `backlogrun` judges for `auto-ok` when a lane
|
|
7
|
+
// is free and nothing is opted in, and whether one was already judged (joshuafolkken/kit#3854).
|
|
8
|
+
//
|
|
9
|
+
// **The mechanical exclusions are `issue_auto_ok.exclusion_of`**, the list a filing is judged by
|
|
10
|
+
// (joshuafolkken/kit#3853), never a second one. What a label cannot answer — whether a policy decision,
|
|
11
|
+
// an external account or a human visual check is needed — is the agent's reading of the body and the
|
|
12
|
+
// comments, so this module only says which issues still owe that reading.
|
|
13
|
+
//
|
|
14
|
+
// **The record is the judgement comment itself.** It carries a digest of the body it judged, so an
|
|
15
|
+
// issue is judged again only when its body changed or a person commented after it — the same line
|
|
16
|
+
// `issue_watch_diff` draws between a person's update and an automated one.
|
|
17
|
+
|
|
18
|
+
interface JudgeIssue {
|
|
19
|
+
number: number
|
|
20
|
+
labels: ReadonlyArray<string>
|
|
21
|
+
body: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MARKER_PREFIX = '<!-- josh:auto-ok-judged body='
|
|
25
|
+
const MARKER_SUFFIX = ' -->'
|
|
26
|
+
const MARKER_PATTERN = /<!-- josh:auto-ok-judged body=(?<digest>[\da-f]+) -->/u
|
|
27
|
+
const DIGEST_LENGTH = 16
|
|
28
|
+
const CRLF = '\r\n'
|
|
29
|
+
const LF = '\n'
|
|
30
|
+
|
|
31
|
+
function is_candidate(issue: JudgeIssue): boolean {
|
|
32
|
+
if (has_label_name(issue.labels, AUTO_OK_LABEL)) return false
|
|
33
|
+
|
|
34
|
+
return issue_auto_ok.exclusion_of(issue.labels) === undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// GitHub stores a body saved in its web editor with CRLF, so the digest is taken over LF alone.
|
|
38
|
+
function body_digest(body: string): string {
|
|
39
|
+
const normalized = body.split(CRLF).join(LF)
|
|
40
|
+
|
|
41
|
+
return createHash('sha256').update(normalized).digest('hex').slice(0, DIGEST_LENGTH)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function digest_of(comment: WatchComment): string | undefined {
|
|
45
|
+
return MARKER_PATTERN.exec(comment.body)?.groups?.['digest']
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Judged when the newest judgement comment covers the body as it stands and no person has commented
|
|
49
|
+
// since. A failed comment read never reaches here — the caller treats it as nothing learned.
|
|
50
|
+
function is_judged(body: string, comments: ReadonlyArray<WatchComment>): boolean {
|
|
51
|
+
const judgement = comments.findLast((comment) => digest_of(comment) !== undefined)
|
|
52
|
+
|
|
53
|
+
if (judgement === undefined || digest_of(judgement) !== body_digest(body)) return false
|
|
54
|
+
|
|
55
|
+
return comments.every((comment) => comment.id <= judgement.id || comment.is_automated)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function comment_of(decision: AutoOkDecision, body: string): string {
|
|
59
|
+
const marker = `${MARKER_PREFIX}${body_digest(body)}${MARKER_SUFFIX}`
|
|
60
|
+
|
|
61
|
+
return `${issue_auto_ok.line_of(decision)}${LF}${LF}${marker}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const auto_ok_judge = { comment_of, is_candidate, is_judged }
|
|
65
|
+
|
|
66
|
+
export type { JudgeIssue }
|
|
67
|
+
export { auto_ok_judge }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { auto_ok_judge_read, type JudgedCache } from '#scripts/auto-ok/auto-ok-judge-read'
|
|
4
5
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
5
6
|
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
6
7
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
@@ -166,6 +167,18 @@ function end_line(end: DriveEnd): string {
|
|
|
166
167
|
return end.detail === undefined ? base : `${base} ${end.detail}`
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
// The count of open issues owing an `auto-ok` judgement, over a cache kept for the process so a watch
|
|
171
|
+
// minute reads the comments only of the issues that moved (`auto-ok-judge-read.ts`).
|
|
172
|
+
function judgement_reader(): () => Promise<number> {
|
|
173
|
+
const cache: JudgedCache = new Map()
|
|
174
|
+
|
|
175
|
+
return async () => {
|
|
176
|
+
const owed = await auto_ok_judge_read.owed(cache)
|
|
177
|
+
|
|
178
|
+
return owed?.length ?? 0
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
169
182
|
function ports_of(
|
|
170
183
|
context: DriveContext,
|
|
171
184
|
seeded: ReadonlyArray<string>,
|
|
@@ -177,6 +190,7 @@ function ports_of(
|
|
|
177
190
|
offer: async (state) => await read_offer(state, context),
|
|
178
191
|
launch: async (issue) => await backlog_drive_launch.launch(issue, context.owner),
|
|
179
192
|
free_lanes: backlog_ready.drive_free_lane_count,
|
|
193
|
+
judgement_owed: judgement_reader(),
|
|
180
194
|
now: () => new Date(),
|
|
181
195
|
sleep: async (milliseconds) => {
|
|
182
196
|
await sleep(milliseconds)
|
|
@@ -65,6 +65,8 @@ const KNOWN_VERDICTS: ReadonlySet<string> = new Set([
|
|
|
65
65
|
])
|
|
66
66
|
const NO_RETRIES = 0
|
|
67
67
|
const NO_FREE_LANES = 0
|
|
68
|
+
const EXHAUSTED_ANSWER = 'exhausted'
|
|
69
|
+
const JUDGE_REASON = 'judge'
|
|
68
70
|
const LAUNCHED = 'launched'
|
|
69
71
|
const UNOPENED = 'unopened'
|
|
70
72
|
|
|
@@ -98,7 +100,8 @@ interface DriveState {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
interface DriveEnd {
|
|
101
|
-
// Why the loop handed control back: `merge`, `launch`, `offer`, `watch`, `stop`, `window`
|
|
103
|
+
// Why the loop handed control back: `merge`, `launch`, `offer`, `judge`, `watch`, `stop`, `window`
|
|
104
|
+
// or a verdict.
|
|
102
105
|
reason: string
|
|
103
106
|
// The token the parent reads, where one command printed it.
|
|
104
107
|
token: string | undefined
|
|
@@ -121,6 +124,9 @@ interface DrivePorts {
|
|
|
121
124
|
// The lanes free right now — `backlog_ready.drive_free_lane_count` in production, the limit less the
|
|
122
125
|
// open lanes.
|
|
123
126
|
free_lanes: () => Promise<number>
|
|
127
|
+
// How many open issues lacking `auto-ok` still owe a judgement — `auto_ok_judge_read.owed` in
|
|
128
|
+
// production, `0` when the read failed.
|
|
129
|
+
judgement_owed: () => Promise<number>
|
|
124
130
|
now: () => Date
|
|
125
131
|
}
|
|
126
132
|
|
|
@@ -149,7 +155,7 @@ function ended(reason: string, state: DriveState, token?: string, issue?: string
|
|
|
149
155
|
function is_drain(read: OfferRead, state: DriveState): boolean {
|
|
150
156
|
return (
|
|
151
157
|
read.verdict === WATCH_VERDICT &&
|
|
152
|
-
read.answer ===
|
|
158
|
+
read.answer === EXHAUSTED_ANSWER &&
|
|
153
159
|
state.in_flight.length === 0 &&
|
|
154
160
|
read.is_retrospective_owed === true
|
|
155
161
|
)
|
|
@@ -158,7 +164,7 @@ function is_drain(read: OfferRead, state: DriveState): boolean {
|
|
|
158
164
|
function needs_retrospective(read: OfferRead, state: DriveState): boolean {
|
|
159
165
|
return (
|
|
160
166
|
read.verdict === STOP_VERDICT &&
|
|
161
|
-
read.answer ===
|
|
167
|
+
read.answer === EXHAUSTED_ANSWER &&
|
|
162
168
|
state.in_flight.length === 0 &&
|
|
163
169
|
read.is_retrospective_owed === true
|
|
164
170
|
)
|
|
@@ -318,6 +324,18 @@ function on_verdict(read: OfferRead, state: DriveState): PassResult {
|
|
|
318
324
|
return { kind: 'continue', state }
|
|
319
325
|
}
|
|
320
326
|
|
|
327
|
+
// **A free lane with nothing opted in is the open backlog's turn to be judged**
|
|
328
|
+
// (joshuafolkken/kit#3854): a watch over an exhausted pool hands back `judge` while an open issue
|
|
329
|
+
// lacking `auto-ok` still owes a judgement, ahead of the drain it would otherwise be. Once every one
|
|
330
|
+
// carries its judgement comment the count is zero and the watch is the driver's again.
|
|
331
|
+
async function is_judgement_owed(read: OfferRead, ports: DrivePorts): Promise<boolean> {
|
|
332
|
+
const is_exhausted = read.verdict === WATCH_VERDICT && read.answer === EXHAUSTED_ANSWER
|
|
333
|
+
|
|
334
|
+
if (!is_exhausted || (await ports.free_lanes()) <= NO_FREE_LANES) return false
|
|
335
|
+
|
|
336
|
+
return (await ports.judgement_owed()) > 0
|
|
337
|
+
}
|
|
338
|
+
|
|
321
339
|
async function ask_offer(state: DriveState, ports: DrivePorts): Promise<PassResult> {
|
|
322
340
|
const read = await ports.offer(state)
|
|
323
341
|
|
|
@@ -326,6 +344,7 @@ async function ask_offer(state: DriveState, ports: DrivePorts): Promise<PassResu
|
|
|
326
344
|
const next = { ...state, retries: read.retries }
|
|
327
345
|
|
|
328
346
|
if (read.verdict === RUN_VERDICT) return await launch_all(read.issues, next, ports)
|
|
347
|
+
if (await is_judgement_owed(read, ports)) return ended(JUDGE_REASON, next, JUDGE_REASON)
|
|
329
348
|
|
|
330
349
|
return on_verdict(read, next)
|
|
331
350
|
}
|
|
@@ -76,7 +76,7 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
|
|
|
76
76
|
{ path: 'docs/josh-commands-automation.md', bytes: 53_248 },
|
|
77
77
|
{ path: 'docs/josh-commands-run.md', bytes: 77_824 },
|
|
78
78
|
{ path: 'docs/josh-commands-backlog.md', bytes: 61_440 },
|
|
79
|
-
{ path: 'docs/josh-commands.md', bytes:
|
|
79
|
+
{ path: 'docs/josh-commands.md', bytes: 57_344 },
|
|
80
80
|
{ path: 'docs/labels-and-run-states.md', bytes: 8192 },
|
|
81
81
|
{ path: 'prompts/coding-standards.md', bytes: 8192 },
|
|
82
82
|
{ path: 'prompts/collaboration-workflow.md', bytes: 8192 },
|
|
@@ -27,13 +27,18 @@ const INIT_COMMANDS = [INSTALL_TARBALL, 'pnpm exec josh init', GATE]
|
|
|
27
27
|
// `josh start` leaves the project on its setup branch, where the gate asks for the scoped checks
|
|
28
28
|
// first; this is the line its refusal hands back.
|
|
29
29
|
const SCOPED_CHECKS = 'pnpm josh lint:related && pnpm josh test:related'
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
const START = 'pnpm exec josh start --yes --github'
|
|
31
|
+
|
|
32
|
+
// An agent names the profile rather than leaving it to detection, so every pattern passes its own.
|
|
33
|
+
function start_commands(profile: string): Array<string> {
|
|
34
|
+
return [
|
|
35
|
+
EXCLUDE_RELEASE_AGE,
|
|
36
|
+
INSTALL_PUBLISHED,
|
|
37
|
+
`${START} --profile ${profile}`,
|
|
38
|
+
SCOPED_CHECKS,
|
|
39
|
+
GATE,
|
|
40
|
+
]
|
|
41
|
+
}
|
|
37
42
|
|
|
38
43
|
// `gh auth login` cannot run without a person, and it is what points git at gh for credentials; with
|
|
39
44
|
// the token in the environment this is the same step.
|
|
@@ -49,6 +54,11 @@ const FIRST_COMMIT = [
|
|
|
49
54
|
'git add --all && git commit --message "Initial commit"',
|
|
50
55
|
]
|
|
51
56
|
const CREATE_REPOSITORY = 'gh repo create "$RUN_NAME" --private --source . --push'
|
|
57
|
+
// The labels a repository already had, recorded before `josh start` so the probe after it shows none
|
|
58
|
+
// was removed or changed.
|
|
59
|
+
const LABELS = 'gh label list --limit 100'
|
|
60
|
+
// The setup pull request's CI, waited for and recorded; the pull request itself is never merged.
|
|
61
|
+
const SETUP_CI = 'gh pr checks --watch --interval 60'
|
|
52
62
|
|
|
53
63
|
// The two states #3712 was found in: a shell that reads no rc file, and a PATH with `node` on it and
|
|
54
64
|
// no `pnpm`.
|
|
@@ -60,13 +70,29 @@ const PATH_PROBES = [
|
|
|
60
70
|
`mkdir -p ${NODE_ONLY} && ln -sf "$(command -v node)" ${NODE_ONLY}/node && ${BARE_SHELL} 'PATH="$HOME/node-only:$PATH" ./node_modules/.bin/josh gate'`,
|
|
61
71
|
]
|
|
62
72
|
|
|
73
|
+
// What a `josh start` run is checked against before the PATH states: the profile it recorded, the
|
|
74
|
+
// repository's visibility and branch, its labels, and the files the first commit on `main` holds. Where
|
|
75
|
+
// `main` already had a commit, `josh start` opens a setup Issue and pull request instead of pushing to
|
|
76
|
+
// it; the last four record those, the files the pull request carries and the `main` GitHub holds.
|
|
77
|
+
const PROFILE = 'pnpm josh profile'
|
|
78
|
+
const START_PROBES = [
|
|
79
|
+
PROFILE,
|
|
80
|
+
'gh repo view --json visibility,defaultBranchRef',
|
|
81
|
+
LABELS,
|
|
82
|
+
'git show --name-only --format= "$(git rev-list --max-parents=0 main)"',
|
|
83
|
+
'gh issue list --state all --json number,title,state,labels',
|
|
84
|
+
'gh pr list --state all --json number,title,state,headRefName,labels,closingIssuesReferences',
|
|
85
|
+
'gh pr diff --name-only',
|
|
86
|
+
'git log --oneline origin/main',
|
|
87
|
+
]
|
|
88
|
+
|
|
63
89
|
// What a `josh init` run is checked against after the gate: the profile it chose, the files it left
|
|
64
90
|
// (a `./.git` line is a repository it should not have made), and a second run that changes nothing.
|
|
65
91
|
const PROJECT_FILES =
|
|
66
92
|
'find . -name node_modules -prune -o -name .git -prune -o -type f -print | sort'
|
|
67
93
|
const CHECKSUMS = `${PROJECT_FILES} | xargs sha256sum`
|
|
68
94
|
const INIT_PROBES = [
|
|
69
|
-
|
|
95
|
+
PROFILE,
|
|
70
96
|
'find . -name node_modules -prune -o -print | sort',
|
|
71
97
|
`${CHECKSUMS} > "$HOME/before-second-init.sha" && pnpm exec josh init && ${CHECKSUMS} | diff "$HOME/before-second-init.sha" -`,
|
|
72
98
|
]
|
|
@@ -90,31 +116,50 @@ function init_pattern(
|
|
|
90
116
|
})
|
|
91
117
|
}
|
|
92
118
|
|
|
119
|
+
// What a `josh start` pattern adds to the shared steps: the state before the install, the profile it
|
|
120
|
+
// names, the commands that must pass after the gate, and the probes recorded before the PATH states.
|
|
121
|
+
interface StartProject {
|
|
122
|
+
setup: ReadonlyArray<string>
|
|
123
|
+
profile: string
|
|
124
|
+
checks?: ReadonlyArray<string>
|
|
125
|
+
probes?: ReadonlyArray<string>
|
|
126
|
+
}
|
|
127
|
+
|
|
93
128
|
function start_pattern(
|
|
94
129
|
project: string,
|
|
95
130
|
files: Record<string, string>,
|
|
96
|
-
setup:
|
|
131
|
+
{ setup, profile, checks = [], probes = [] }: StartProject,
|
|
97
132
|
): Scenario {
|
|
98
133
|
return fresh_machine_spec.define({
|
|
99
134
|
name: `kit-test-${project}-start`,
|
|
100
135
|
requires_github: true,
|
|
101
136
|
package_source: 'published',
|
|
102
137
|
files,
|
|
103
|
-
commands: [SIGN_IN, ...setup, ...
|
|
104
|
-
probes: PATH_PROBES,
|
|
138
|
+
commands: [SIGN_IN, ...setup, ...start_commands(profile), ...checks],
|
|
139
|
+
probes: [...START_PROBES, ...probes, ...PATH_PROBES],
|
|
105
140
|
})
|
|
106
141
|
}
|
|
107
142
|
|
|
108
143
|
const PATTERNS: ReadonlyArray<Scenario> = [
|
|
109
144
|
init_pattern('lua', LUA_FILES, [BARE_MANIFEST]),
|
|
110
145
|
// No Git: `josh start` creates the repository, the first commit and the GitHub repository.
|
|
111
|
-
start_pattern('lua', LUA_FILES, [MACHINE_IDENTITY, BARE_MANIFEST]),
|
|
146
|
+
start_pattern('lua', LUA_FILES, { setup: [MACHINE_IDENTITY, BARE_MANIFEST], profile: 'basic' }),
|
|
112
147
|
init_pattern('html', HTML_FILES, [BARE_MANIFEST]),
|
|
113
148
|
// Git with a first commit on `main`, no GitHub repository.
|
|
114
|
-
start_pattern('html', HTML_FILES, [...FIRST_COMMIT, BARE_MANIFEST]),
|
|
149
|
+
start_pattern('html', HTML_FILES, { setup: [...FIRST_COMMIT, BARE_MANIFEST], profile: 'basic' }),
|
|
115
150
|
init_pattern('svelte', {}, [CREATE_SVELTEKIT, SV_VERSION, SVELTEKIT_CHECKS], [SVELTEKIT_CHECKS]),
|
|
116
|
-
// A GitHub repository with `main` already pushed
|
|
117
|
-
|
|
151
|
+
// A GitHub repository with `main` already pushed, its labels recorded before the install; the app
|
|
152
|
+
// still builds and type-checks on the setup branch, and the setup pull request's CI is waited for.
|
|
153
|
+
start_pattern(
|
|
154
|
+
'svelte',
|
|
155
|
+
{},
|
|
156
|
+
{
|
|
157
|
+
setup: [CREATE_SVELTEKIT, SV_VERSION, ...FIRST_COMMIT, CREATE_REPOSITORY, LABELS],
|
|
158
|
+
profile: 'full',
|
|
159
|
+
checks: [SVELTEKIT_CHECKS],
|
|
160
|
+
probes: [SETUP_CI],
|
|
161
|
+
},
|
|
162
|
+
),
|
|
118
163
|
]
|
|
119
164
|
|
|
120
165
|
function names(): Array<string> {
|
|
@@ -2,6 +2,7 @@ import { existsSync, globSync, readFileSync } from 'node:fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { find_local_bin_upwards } from '#scripts/build/local-bin'
|
|
4
4
|
import { doctor_consumer } from '#scripts/doctor/doctor-consumer'
|
|
5
|
+
import { own_formatter } from '#scripts/init/own-formatter'
|
|
5
6
|
import { project_profile } from '#scripts/init/project-profile'
|
|
6
7
|
import { ancestor_directories } from '#scripts/lib/ancestor-directories'
|
|
7
8
|
import { json_value } from '#scripts/lib/json-value'
|
|
@@ -150,10 +151,12 @@ function type_check_skip_reason(directory: string): string | undefined {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
// Shared by `josh lint`, `josh lint:related` and `josh format`, so a basic project without Web
|
|
153
|
-
// files — a Python or Rust project, which `josh init` gives no Prettier —
|
|
154
|
-
// reason by all three.
|
|
154
|
+
// files — a Python or Rust project, which `josh init` gives no Prettier — or with a formatter of its
|
|
155
|
+
// own is skipped for the same reason by all three.
|
|
155
156
|
function prettier_skip_reason(directory: string): string | undefined {
|
|
156
157
|
if (!is_basic(directory)) return undefined
|
|
158
|
+
const formatter = own_formatter.find_config(project_root(directory))
|
|
159
|
+
if (formatter !== undefined) return `${formatter} configures the project's own formatter`
|
|
157
160
|
if (!has_files(directory, WEB_FILES)) return 'no HTML, CSS or JavaScript files were found'
|
|
158
161
|
|
|
159
162
|
return missing_tool_reason(directory, 'prettier', 'prettier')
|
|
@@ -71,6 +71,7 @@ const REST_FIELD_NAMES = new Map<string, string>([
|
|
|
71
71
|
['url', 'html_url'],
|
|
72
72
|
['createdAt', 'created_at'],
|
|
73
73
|
['closedAt', 'closed_at'],
|
|
74
|
+
['updatedAt', 'updated_at'],
|
|
74
75
|
])
|
|
75
76
|
|
|
76
77
|
const NOT_AN_ISSUE_MESSAGE = 'gh api answered something other than an issue object'
|
|
@@ -150,6 +150,14 @@ async function issue_list_by_label_in_repo(
|
|
|
150
150
|
return await issue_list({ json_fields: SUMMARY_FIELDS, limit, label, repo })
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// Every open issue with what `josh auto-ok:judge` judges it by — its labels and body — and `updatedAt`,
|
|
154
|
+
// so the backlog driver's resident check reads an issue's comments again only once it has moved.
|
|
155
|
+
const JUDGEMENT_FIELDS = 'number,title,body,labels,updatedAt'
|
|
156
|
+
|
|
157
|
+
async function issue_list_open_for_judgement(limit: number): Promise<IssueListOutcome> {
|
|
158
|
+
return await issue_list({ json_fields: JUDGEMENT_FIELDS, limit })
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
const git_gh_issue = {
|
|
154
162
|
...git_gh_issue_read,
|
|
155
163
|
...git_gh_issue_write,
|
|
@@ -159,6 +167,7 @@ const git_gh_issue = {
|
|
|
159
167
|
issue_list_by_label_summary,
|
|
160
168
|
issue_search_body,
|
|
161
169
|
issue_list_open_bodies,
|
|
170
|
+
issue_list_open_for_judgement,
|
|
162
171
|
issue_list_recently_closed,
|
|
163
172
|
}
|
|
164
173
|
|
|
@@ -89,6 +89,10 @@ async function fetch_review_comments(
|
|
|
89
89
|
return parsed?.map((comment) => to_review_comment(comment))
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// The other choice both AI-review stops offer: a person who has looked records why and moves on.
|
|
93
|
+
const IGNORE_ALTERNATIVE =
|
|
94
|
+
'Re-run `pnpm josh followup --ai-review-ignore-reason "<reason>"` if the gate does not apply'
|
|
95
|
+
|
|
92
96
|
async function handle_blockers(input: {
|
|
93
97
|
branch_name: string
|
|
94
98
|
ignore_reason: string | undefined
|
|
@@ -105,7 +109,12 @@ async function handle_blockers(input: {
|
|
|
105
109
|
return
|
|
106
110
|
}
|
|
107
111
|
|
|
108
|
-
await git_pr_confirmation.notify_confirmation({
|
|
112
|
+
await git_pr_confirmation.notify_confirmation({
|
|
113
|
+
context: input.context,
|
|
114
|
+
body,
|
|
115
|
+
recommended: 'Fix the findings and re-run `pnpm josh followup`',
|
|
116
|
+
alternatives: [IGNORE_ALTERNATIVE],
|
|
117
|
+
})
|
|
109
118
|
|
|
110
119
|
throw new Error(body)
|
|
111
120
|
}
|
|
@@ -140,6 +149,8 @@ async function handle_unreadable_comments(input: {
|
|
|
140
149
|
await git_pr_confirmation.notify_confirmation({
|
|
141
150
|
context: input.context,
|
|
142
151
|
body: UNREADABLE_COMMENTS_BODY,
|
|
152
|
+
recommended: 'Re-run `pnpm josh followup` once the comments can be read',
|
|
153
|
+
alternatives: [IGNORE_ALTERNATIVE],
|
|
143
154
|
})
|
|
144
155
|
|
|
145
156
|
throw new Error(UNREADABLE_COMMENTS_BODY)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
confirmation_options,
|
|
3
|
+
type ConfirmationOptions,
|
|
4
|
+
} from '#scripts/notify/confirmation-options'
|
|
1
5
|
import { telegram_notify, type TelegramSendInput } from '#scripts/notify/telegram-notify'
|
|
2
6
|
|
|
3
7
|
// What to type when the confirmation notification itself could not be delivered.
|
|
@@ -5,7 +9,7 @@ import { telegram_notify, type TelegramSendInput } from '#scripts/notify/telegra
|
|
|
5
9
|
// commands and a backtick inside shell double quotes is evaluated before the command runs.
|
|
6
10
|
const CONFIRMATION_RECOVERY =
|
|
7
11
|
'send it by hand — `pnpm josh notify --task-type confirmation --issue-url <url> ' +
|
|
8
|
-
'--body-file <path
|
|
12
|
+
'--body-file <path> --recommended "<choice>"`'
|
|
9
13
|
|
|
10
14
|
interface TelegramContext {
|
|
11
15
|
repo_name: string | undefined
|
|
@@ -14,15 +18,18 @@ interface TelegramContext {
|
|
|
14
18
|
pr_url: string | undefined
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
interface ConfirmationInput extends ConfirmationOptions {
|
|
18
22
|
context: TelegramContext
|
|
19
23
|
body: string
|
|
20
|
-
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function build_confirmation_input(input: ConfirmationInput): TelegramSendInput {
|
|
21
27
|
return {
|
|
22
28
|
task_type: 'confirmation',
|
|
23
29
|
repo_name: input.context.repo_name,
|
|
24
30
|
issue_title: input.context.issue_title,
|
|
25
31
|
body: input.body,
|
|
32
|
+
choices: confirmation_options.block(input),
|
|
26
33
|
issue_url: input.context.issue_url,
|
|
27
34
|
pr_url: input.context.pr_url,
|
|
28
35
|
}
|
|
@@ -34,10 +41,7 @@ function build_confirmation_input(input: {
|
|
|
34
41
|
//
|
|
35
42
|
// Unlike the completion notification, this one *has* a recovery: a `confirmation` is exactly what
|
|
36
43
|
// `josh notify` is for, and the run stops here anyway, so re-sending it by hand loses nothing.
|
|
37
|
-
async function notify_confirmation(input: {
|
|
38
|
-
context: TelegramContext
|
|
39
|
-
body: string
|
|
40
|
-
}): Promise<void> {
|
|
44
|
+
async function notify_confirmation(input: ConfirmationInput): Promise<void> {
|
|
41
45
|
await telegram_notify.send_or_report(build_confirmation_input(input), CONFIRMATION_RECOVERY)
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -106,7 +106,7 @@ function build_basic_settings_action(): FileAction {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function build_basic_vscode_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
|
|
109
|
-
const recommendations = shape.
|
|
109
|
+
const recommendations = shape.should_use_prettier
|
|
110
110
|
? [...BASIC_EXTENSIONS, PRETTIER_EXTENSION]
|
|
111
111
|
: BASIC_EXTENSIONS
|
|
112
112
|
const actions: Array<FileAction> = [
|
|
@@ -117,7 +117,7 @@ function build_basic_vscode_actions(shape: ProjectShape): ReadonlyArray<FileActi
|
|
|
117
117
|
),
|
|
118
118
|
]
|
|
119
119
|
|
|
120
|
-
if (shape.
|
|
120
|
+
if (shape.should_use_prettier) actions.push(build_basic_settings_action())
|
|
121
121
|
|
|
122
122
|
return actions
|
|
123
123
|
}
|
|
@@ -210,7 +210,7 @@ function build_basic_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
|
|
|
210
210
|
if (shape.has_git) actions.push(build_gitignore_action())
|
|
211
211
|
if (shape.has_typescript) actions.push(build_basic_tsconfig_action())
|
|
212
212
|
|
|
213
|
-
if (shape.
|
|
213
|
+
if (shape.should_use_prettier) {
|
|
214
214
|
actions.push({
|
|
215
215
|
dest: BASIC_PRETTIER_CONFIG,
|
|
216
216
|
create: () =>
|
|
@@ -348,7 +348,7 @@ function should_copy_github_file(filename: string, shape?: ProjectShape): boolea
|
|
|
348
348
|
function should_copy_basic_file(filename: string, shape?: ProjectShape): boolean {
|
|
349
349
|
if (filename !== PRETTIER_IGNORE || shape?.profile !== 'basic') return true
|
|
350
350
|
|
|
351
|
-
return shape.
|
|
351
|
+
return shape.should_use_prettier
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
function should_copy_ai_file(filename: string, shape?: ProjectShape): boolean {
|
|
@@ -45,7 +45,7 @@ function merge_basic_manifest(
|
|
|
45
45
|
const with_kit = init_logic.merge_development_dependencies(with_script, {
|
|
46
46
|
[KIT_PACKAGE_NAME]: versions.kit,
|
|
47
47
|
})
|
|
48
|
-
const with_prettier = shape.
|
|
48
|
+
const with_prettier = shape.should_use_prettier
|
|
49
49
|
? init_logic.merge_development_dependencies(with_kit, { prettier: versions.prettier })
|
|
50
50
|
: with_kit
|
|
51
51
|
|
|
@@ -82,13 +82,17 @@ function ensure_project_manifest(project_root: string): void {
|
|
|
82
82
|
if (!existsSync(manifest_path)) writeFileSync(manifest_path, init_basic.initial_manifest())
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function is_handed_off(): boolean {
|
|
86
|
+
return process.env[HANDOFF_ENV] !== undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
85
89
|
// Returns the failure to report, or undefined once the project-installed kit finished the setup.
|
|
86
90
|
function hand_off(
|
|
87
91
|
package_directory: string,
|
|
88
92
|
project_root: string,
|
|
89
93
|
args: ReadonlyArray<string>,
|
|
90
94
|
): string | undefined {
|
|
91
|
-
if (
|
|
95
|
+
if (is_handed_off()) return REPEATED_HANDOFF
|
|
92
96
|
console.info('\n🚀 Installing @joshuafolkken/kit into the project, then running its josh init')
|
|
93
97
|
const template = readFileSync(path.join(package_directory, KIT_BUILDS_TEMPLATE), 'utf8')
|
|
94
98
|
const install = kit_install_step(has_kit_dependency(project_root), template)
|
|
@@ -100,5 +104,5 @@ function hand_off(
|
|
|
100
104
|
return undefined
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
const init_bootstrap = { is_project_kit, kit_build_flags, hand_off }
|
|
107
|
+
const init_bootstrap = { HANDOFF_ENV, is_project_kit, kit_build_flags, hand_off, is_handed_off }
|
|
104
108
|
export { init_bootstrap }
|