@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
|
@@ -1,34 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// `
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import {
|
|
2
|
+
BREAKING_CHANGE_LABEL,
|
|
3
|
+
DEFERRED_LABEL,
|
|
4
|
+
EPIC_LABEL,
|
|
5
|
+
has_label_name,
|
|
6
|
+
NEEDS_HUMAN_REVIEW_LABEL,
|
|
7
|
+
RELEASE_LABEL,
|
|
8
|
+
} from './issue-labels'
|
|
9
|
+
import { markdown_section } from './markdown-section'
|
|
10
|
+
|
|
11
|
+
// Whether `josh issue:file` applies `auto-ok` to the Issue it files, judged against the criteria
|
|
12
|
+
// `docs/how-to/run-backlog.md` records rather than the context the filing happens in, so a filing a
|
|
13
|
+
// person asked for (`kickoff new`) is judged the same as one an agent makes on its own. The answer is
|
|
14
|
+
// a pure reading of the filing — its labels, its body and its target — so `backlogrun`'s judgement
|
|
15
|
+
// of an Issue already open reuses `exclusion_of` rather than a second list (joshuafolkken/kit#3854).
|
|
16
|
+
// The order: `--no-auto-ok`, a filing to another repository (whose own `backlogrun` would implement
|
|
17
|
+
// and merge it though nobody opted that repository in), a label that keeps a person in the loop, and
|
|
18
|
+
// last the body's judgement declaration (`JUDGEMENT_NEEDED` / `JUDGEMENT_NOT_NEEDED`): whether a
|
|
19
|
+
// policy decision, an external account or a human visual check is needed.
|
|
20
|
+
|
|
21
|
+
interface AutoOkFiling {
|
|
22
|
+
// `--no-auto-ok`: the filer declared the Issue needs a person's judgement.
|
|
23
23
|
is_opted_out: boolean
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
is_carried: boolean
|
|
29
|
-
// The labels of the Issue the current branch names; `undefined` when the branch names none or the
|
|
30
|
-
// Issue could not be read.
|
|
31
|
-
branch_labels: ReadonlyArray<string> | undefined
|
|
24
|
+
is_cross_repository: boolean
|
|
25
|
+
// Every label the filing carries, including the ones the body's declarations add.
|
|
26
|
+
labels: ReadonlyArray<string>
|
|
27
|
+
body: string
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
interface AutoOkDecision {
|
|
@@ -36,98 +32,71 @@ interface AutoOkDecision {
|
|
|
36
32
|
reason: string
|
|
37
33
|
}
|
|
38
34
|
|
|
35
|
+
const BACKGROUND_HEADING = '## 背景'
|
|
36
|
+
const JUDGEMENT_NOT_NEEDED = '- 人の判断: 不要'
|
|
37
|
+
const JUDGEMENT_NEEDED = '- 人の判断: 要'
|
|
38
|
+
const DECLARATION_PROBLEM = `declare under \`${BACKGROUND_HEADING}\` whether a person's judgement (a policy decision, an external account, a human visual check) is needed: \`${JUDGEMENT_NOT_NEEDED}\` or \`${JUDGEMENT_NEEDED}(<which one>)\` — exactly one (\`issue-template.md\`)`
|
|
39
|
+
|
|
39
40
|
const OPTED_OUT: AutoOkDecision = { is_applied: false, reason: '--no-auto-ok declared' }
|
|
40
41
|
const CROSS_REPOSITORY: AutoOkDecision = {
|
|
41
42
|
is_applied: false,
|
|
42
43
|
reason: 'the Issue is filed to another repository',
|
|
43
44
|
}
|
|
44
|
-
const
|
|
45
|
-
is_applied: false,
|
|
46
|
-
reason: `a ${RELEASE_LABEL} Issue is opted in by a person only`,
|
|
47
|
-
}
|
|
48
|
-
const REQUESTED: AutoOkDecision = {
|
|
45
|
+
const JUDGEMENT: AutoOkDecision = {
|
|
49
46
|
is_applied: false,
|
|
50
|
-
reason:
|
|
47
|
+
reason: `the body declares \`${JUDGEMENT_NEEDED}\``,
|
|
51
48
|
}
|
|
52
|
-
const
|
|
53
|
-
const BRANCH_OPTED_IN: AutoOkDecision = {
|
|
49
|
+
const APPLIED: AutoOkDecision = {
|
|
54
50
|
is_applied: true,
|
|
55
|
-
reason: 'the
|
|
56
|
-
}
|
|
57
|
-
const NOT_OPTED_IN: AutoOkDecision = {
|
|
58
|
-
is_applied: false,
|
|
59
|
-
reason: 'no backlogrun carry record is live and the branch names no auto-ok Issue',
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// What the filing itself declares: `--no-auto-ok` and `--requested`.
|
|
63
|
-
type AutoOkDeclared = Pick<AutoOkSignals, 'is_opted_out' | 'is_requested'>
|
|
64
|
-
type Withholding = AutoOkDeclared & Pick<AutoOkSignals, 'is_release'>
|
|
65
|
-
|
|
66
|
-
// The three signals that withhold `auto-ok` before any other is read, or `undefined` when none holds.
|
|
67
|
-
function withheld(signals: Withholding): AutoOkDecision | undefined {
|
|
68
|
-
if (signals.is_opted_out) return OPTED_OUT
|
|
69
|
-
if (signals.is_release) return RELEASE
|
|
70
|
-
if (signals.is_requested) return REQUESTED
|
|
71
|
-
|
|
72
|
-
return undefined
|
|
51
|
+
reason: 'no exclusion holds and the body declares no judgement is needed',
|
|
73
52
|
}
|
|
74
53
|
|
|
75
|
-
// The
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
54
|
+
// The labels that keep a person in the loop, each with the reason it withholds `auto-ok`. A
|
|
55
|
+
// `breaking-change` declared in the body arrives here as its label, added before the decision.
|
|
56
|
+
const EXCLUDED_LABELS: ReadonlyArray<readonly [string, string]> = [
|
|
57
|
+
[RELEASE_LABEL, 'a release is Tier C'],
|
|
58
|
+
[BREAKING_CHANGE_LABEL, 'a breaking change needs a person to accept the migration'],
|
|
59
|
+
[DEFERRED_LABEL, 'the Issue is deferred'],
|
|
60
|
+
[NEEDS_HUMAN_REVIEW_LABEL, 'a person reviews the change before it lands'],
|
|
61
|
+
[EPIC_LABEL, 'an epic is opted in by a person, and that opts in every descendant'],
|
|
62
|
+
]
|
|
79
63
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
64
|
+
// The first label that withholds `auto-ok`, or `undefined` when none of them is present.
|
|
65
|
+
function exclusion_of(labels: ReadonlyArray<string>): AutoOkDecision | undefined {
|
|
66
|
+
const found = EXCLUDED_LABELS.find(([label]) => has_label_name(labels, label))
|
|
83
67
|
|
|
84
|
-
return
|
|
85
|
-
}
|
|
68
|
+
if (found === undefined) return undefined
|
|
86
69
|
|
|
87
|
-
|
|
88
|
-
return `auto-ok: ${decision.is_applied ? 'applied' : 'not applied'} — ${decision.reason}`
|
|
70
|
+
return { is_applied: false, reason: `a \`${found[0]}\` Issue — ${found[1]}` }
|
|
89
71
|
}
|
|
90
72
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
const
|
|
73
|
+
// `true` / `false` for a declared need, `undefined` when the background declares neither or both.
|
|
74
|
+
function judgement_of(body: string): boolean | undefined {
|
|
75
|
+
const background = markdown_section.section_lines(body, BACKGROUND_HEADING).join('\n')
|
|
76
|
+
const lines = markdown_section.unfenced_lines(background).map((line) => line.trim())
|
|
77
|
+
const is_not_needed = lines.some((line) => line.startsWith(JUDGEMENT_NOT_NEEDED))
|
|
78
|
+
const is_needed = lines.some((line) => line.startsWith(JUDGEMENT_NEEDED))
|
|
94
79
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return run_carry.read_carry(run_carry.carry_path(directory)).kind === 'carried'
|
|
80
|
+
return is_not_needed === is_needed ? undefined : is_needed
|
|
98
81
|
}
|
|
99
82
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const issue_number = git_branch.issue_from_branch(await git_command.branch())
|
|
83
|
+
// Why the filing is refused for its declaration, or `undefined` when it declares exactly one answer.
|
|
84
|
+
function declaration_problem(body: string): string | undefined {
|
|
85
|
+
return judgement_of(body) === undefined ? DECLARATION_PROBLEM : undefined
|
|
86
|
+
}
|
|
105
87
|
|
|
106
|
-
|
|
107
|
-
|
|
88
|
+
function decide(filing: AutoOkFiling): AutoOkDecision {
|
|
89
|
+
if (filing.is_opted_out) return OPTED_OUT
|
|
90
|
+
if (filing.is_cross_repository) return CROSS_REPOSITORY
|
|
108
91
|
|
|
109
|
-
return
|
|
92
|
+
return exclusion_of(filing.labels) ?? (judgement_of(filing.body) === false ? APPLIED : JUDGEMENT)
|
|
110
93
|
}
|
|
111
94
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
async function resolve(
|
|
115
|
-
declared: AutoOkDeclared,
|
|
116
|
-
target: string,
|
|
117
|
-
current: string,
|
|
118
|
-
labels: ReadonlyArray<string> = [],
|
|
119
|
-
): Promise<AutoOkDecision> {
|
|
120
|
-
const withholding = { ...declared, is_release: has_label_name(labels, RELEASE_LABEL) }
|
|
121
|
-
const decision = withheld(withholding)
|
|
122
|
-
|
|
123
|
-
if (decision !== undefined) return decision
|
|
124
|
-
if (!issue_file.is_same_repository(target, current)) return CROSS_REPOSITORY
|
|
125
|
-
if (await is_carried()) return CARRIED
|
|
126
|
-
|
|
127
|
-
return decide({ ...withholding, is_carried: false, branch_labels: await branch_labels(current) })
|
|
95
|
+
function line_of(decision: AutoOkDecision): string {
|
|
96
|
+
return `auto-ok: ${decision.is_applied ? 'applied' : 'not applied'} — ${decision.reason}`
|
|
128
97
|
}
|
|
129
98
|
|
|
130
|
-
const issue_auto_ok = { decide,
|
|
99
|
+
const issue_auto_ok = { declaration_problem, decide, exclusion_of, line_of }
|
|
131
100
|
|
|
132
|
-
export type {
|
|
101
|
+
export type { AutoOkDecision, AutoOkFiling }
|
|
133
102
|
export { issue_auto_ok }
|
|
@@ -24,7 +24,7 @@ import { issue_scout_cli } from './issue-scout-cli'
|
|
|
24
24
|
import { issue_wip } from './issue-wip'
|
|
25
25
|
|
|
26
26
|
// `josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>]…
|
|
27
|
-
// [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok] [--
|
|
27
|
+
// [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok] [--release]` — file an Issue
|
|
28
28
|
// with every filing step run in order: the third-party refusal, the body
|
|
29
29
|
// lint, the `## Origin` check for another repository, the `auto-ok` decision
|
|
30
30
|
// with the run label it owes, the fold question against the run's earlier
|
|
@@ -37,7 +37,7 @@ const SUCCESS_EXIT_CODE = 0
|
|
|
37
37
|
const FAILURE_EXIT_CODE = 1
|
|
38
38
|
const ARGV_OFFSET = 2
|
|
39
39
|
const USAGE =
|
|
40
|
-
'Usage: josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <tier-a|split|interrupt|review-cap>] [--label <name>]… [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok] [--
|
|
40
|
+
'Usage: josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <tier-a|split|interrupt|review-cap>] [--label <name>]… [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok] [--release]'
|
|
41
41
|
const UNKNOWN_REPO_MESSAGE =
|
|
42
42
|
'Could not read this repository from `git remote`, so the filing has no repository to compare against — check `gh auth status`.'
|
|
43
43
|
const THIRD_PARTY_MESSAGE =
|
|
@@ -74,10 +74,13 @@ function is_third_party(target: string): boolean {
|
|
|
74
74
|
// the filing may go on to the scout.
|
|
75
75
|
function refusals_of(filing: Filing): ReadonlyArray<string> {
|
|
76
76
|
if (is_third_party(filing.target)) return [THIRD_PARTY_MESSAGE]
|
|
77
|
-
const problems =
|
|
78
|
-
|
|
77
|
+
const problems = [
|
|
78
|
+
...issue_lint_cli.problems(filing.body),
|
|
79
|
+
issue_auto_ok.declaration_problem(filing.body),
|
|
80
|
+
issue_file.origin_problem(filing.body, filing.target, filing.current),
|
|
81
|
+
]
|
|
79
82
|
|
|
80
|
-
return
|
|
83
|
+
return problems.filter((problem) => problem !== undefined).map((problem) => `✖ ${problem}`)
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
function unacknowledged_message(numbers: ReadonlyArray<number>): string {
|
|
@@ -143,18 +146,15 @@ async function is_scout_clear(filing: Filing): Promise<boolean> {
|
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
// Every label the create call carries, or `undefined` when an `auto-ok` filing names neither run label.
|
|
146
|
-
// Decided before the WIP count and the scout, so a refusal costs no listing.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
// Decided before the WIP count and the scout, so a refusal costs no listing. The decision reads the
|
|
150
|
+
// labels the body's declarations add, so a declared breaking change is excluded like its label.
|
|
151
|
+
function labels_of(filing: Filing): ReadonlyArray<string> | undefined {
|
|
152
|
+
const auto_ok = issue_auto_ok.decide({
|
|
149
153
|
is_opted_out: filing.args.is_auto_ok_opted_out,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
filing.target,
|
|
155
|
-
filing.current,
|
|
156
|
-
filing.args.labels,
|
|
157
|
-
)
|
|
154
|
+
is_cross_repository: !issue_file.is_same_repository(filing.target, filing.current),
|
|
155
|
+
labels: issue_file.labels_of(filing.args, filing.body),
|
|
156
|
+
body: filing.body,
|
|
157
|
+
})
|
|
158
158
|
|
|
159
159
|
console.info(issue_auto_ok.line_of(auto_ok))
|
|
160
160
|
const labels = issue_file.labels_of(filing.args, filing.body, auto_ok.is_applied)
|
|
@@ -271,7 +271,7 @@ async function is_clear(filing: Filing): Promise<boolean> {
|
|
|
271
271
|
|
|
272
272
|
async function file(filing: Filing): Promise<number> {
|
|
273
273
|
if (!is_admitted(filing)) return FAILURE_EXIT_CODE
|
|
274
|
-
const labels =
|
|
274
|
+
const labels = labels_of(filing)
|
|
275
275
|
|
|
276
276
|
if (labels === undefined || !(await is_clear(filing))) return FAILURE_EXIT_CODE
|
|
277
277
|
|
|
@@ -41,9 +41,6 @@ interface FileArguments {
|
|
|
41
41
|
is_over_cap: boolean
|
|
42
42
|
// `--no-auto-ok`: the Issue needs a person's judgement, so `auto-ok` is not applied (`issue-auto-ok.ts`).
|
|
43
43
|
is_auto_ok_opted_out: boolean
|
|
44
|
-
// `--requested`: a person asked for this filing, so `auto-ok` is applied only when a `--label` names
|
|
45
|
-
// it (`issue-auto-ok.ts`).
|
|
46
|
-
is_requested: boolean
|
|
47
44
|
// `--release`: the change reaches a consumer only once published, so the Issue is linked to the
|
|
48
45
|
// repository's release Issue as a blocker (`issue-release-cli.ts`).
|
|
49
46
|
is_release: boolean
|
|
@@ -58,7 +55,6 @@ const OPTIONS = {
|
|
|
58
55
|
distinct: { type: 'string', multiple: true },
|
|
59
56
|
'over-cap': { type: 'boolean' },
|
|
60
57
|
'no-auto-ok': { type: 'boolean' },
|
|
61
|
-
requested: { type: 'boolean' },
|
|
62
58
|
release: { type: 'boolean' },
|
|
63
59
|
} as const
|
|
64
60
|
|
|
@@ -129,7 +125,6 @@ function arguments_of(values: ParsedValues, title: string | undefined): FileArgu
|
|
|
129
125
|
repo: values.repo,
|
|
130
126
|
is_over_cap: values['over-cap'] === true,
|
|
131
127
|
is_auto_ok_opted_out: values['no-auto-ok'] === true,
|
|
132
|
-
is_requested: values.requested === true,
|
|
133
128
|
is_release: values.release === true,
|
|
134
129
|
}
|
|
135
130
|
}
|
|
@@ -151,7 +146,7 @@ function is_undeclared_classification(label: string, body: string): boolean {
|
|
|
151
146
|
|
|
152
147
|
// Every label the filing carries: the depth, the route when there is one, the classification labels
|
|
153
148
|
// the body declares (`issue_classification.labels_for`, which merges case-insensitively), then any
|
|
154
|
-
// extra `--label`, then `auto-ok` when the filing
|
|
149
|
+
// extra `--label`, then `auto-ok` when the filing is judged in (`issue-auto-ok.ts`) and no `--label`
|
|
155
150
|
// already named it. One request applies them all, so no label is a separate step to forget.
|
|
156
151
|
function labels_of(args: FileArguments, body: string, is_auto_ok = false): ReadonlyArray<string> {
|
|
157
152
|
const extra = args.labels.filter((label) => !is_undeclared_classification(label, body))
|
|
@@ -67,9 +67,12 @@ const ALREADY_DONE_LABEL = 'already-done'
|
|
|
67
67
|
// Marks the Issue that tracks a repository's next release. An Issue whose
|
|
68
68
|
// change reaches a consumer only once published is linked to it as a `blocked_by` blocker
|
|
69
69
|
// (`josh issue:release`), so "merged but not yet released" is visible in the backlog. A run applies
|
|
70
|
-
// it; **`auto-ok` never rides on it** — `issue-auto-ok.ts`
|
|
71
|
-
//
|
|
70
|
+
// it; **`auto-ok` never rides on it** — `issue-auto-ok.ts` excludes it, since a release is Tier C —
|
|
71
|
+
// and only a person opts it into unattended execution.
|
|
72
72
|
const RELEASE_LABEL = 'release'
|
|
73
|
+
// A person's mark for work set aside for now; `issue-auto-ok.ts` reads it as an exclusion. Applied by
|
|
74
|
+
// hand and never provisioned, so it is not in `REPOSITORY_LABELS`.
|
|
75
|
+
const DEFERRED_LABEL = 'deferred'
|
|
73
76
|
// Applied to an Issue whose second review round was skipped, so the condition stays auditable
|
|
74
77
|
// (`prompts/review.md` → "When round 2 is skipped entirely, and when it is not").
|
|
75
78
|
const REVIEW_ROUND2_SKIPPED_LABEL = 'review-round2-skipped'
|
|
@@ -402,6 +405,7 @@ export {
|
|
|
402
405
|
BUG_LABEL,
|
|
403
406
|
BUGFIX_LABEL,
|
|
404
407
|
BREAKING_CHANGE_LABEL,
|
|
408
|
+
DEFERRED_LABEL,
|
|
405
409
|
depth_label_of,
|
|
406
410
|
DEPTH_0_LABEL,
|
|
407
411
|
DEPTH_1_LABEL,
|
|
@@ -18,16 +18,21 @@ import { session_cite } from './session-cite'
|
|
|
18
18
|
// --release` calls the same `link` once its Issue exists, so a new and an existing Issue are linked
|
|
19
19
|
// by one body of code.
|
|
20
20
|
//
|
|
21
|
+
// `--wait` points the edge the other way: `<N>` is blocked by the release Issue, so a run that needs
|
|
22
|
+
// the published version records a dependency rather than a `needs-decision` park, and the backlog
|
|
23
|
+
// offers `<N>` again once a person closes the release Issue (joshuafolkken/kit#3824).
|
|
24
|
+
//
|
|
21
25
|
// The relation is written through `issue_add_blocked_by`, which addresses the repository `gh`
|
|
22
26
|
// resolves — the current one, or the target a cross-repository `issue:file` has pointed `GH_REPO` at.
|
|
23
27
|
|
|
24
28
|
const SUCCESS_EXIT_CODE = 0
|
|
25
29
|
const FAILURE_EXIT_CODE = 1
|
|
26
30
|
const ARGV_OFFSET = 2
|
|
27
|
-
const USAGE = 'Usage: josh issue:release <issue-number>'
|
|
31
|
+
const USAGE = 'Usage: josh issue:release <issue-number> [--wait]'
|
|
28
32
|
const NONE = 'none'
|
|
29
33
|
const UNKNOWN_REPO_MESSAGE =
|
|
30
34
|
'Could not read this repository from `git remote` — check `gh auth status`.'
|
|
35
|
+
const OPTIONS = { wait: { type: 'boolean' } } as const
|
|
31
36
|
|
|
32
37
|
interface ReleaseIssue {
|
|
33
38
|
issue_number: number
|
|
@@ -35,7 +40,20 @@ interface ReleaseIssue {
|
|
|
35
40
|
is_filed: boolean
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
interface ReleaseRequest {
|
|
44
|
+
issue_number: number
|
|
45
|
+
// `<N>` waits on the release Issue, rather than blocking it.
|
|
46
|
+
is_wait: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The two ends of one `blocked_by` edge.
|
|
50
|
+
interface Edge {
|
|
51
|
+
blocked: number
|
|
52
|
+
blocker: number
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
// `undefined` when the listing is unreadable, `NONE` when it reads and names no open release Issue.
|
|
56
|
+
// `pnpm josh release` asks the same question to close the Issue it has just published.
|
|
39
57
|
async function find_open(target: string): Promise<number | typeof NONE | undefined> {
|
|
40
58
|
const outcome = await git_gh_issue_list.issue_list({
|
|
41
59
|
json_fields: 'number',
|
|
@@ -91,41 +109,59 @@ async function release_of(target: string): Promise<ReleaseIssue | undefined> {
|
|
|
91
109
|
return filed === undefined ? undefined : { issue_number: filed, is_filed: true }
|
|
92
110
|
}
|
|
93
111
|
|
|
94
|
-
function
|
|
112
|
+
function release_label_of(release: ReleaseIssue, target: string): string {
|
|
95
113
|
const how = release.is_filed ? 'filed' : 'open'
|
|
96
|
-
const release_cite = session_cite.issue(release.issue_number, undefined, target)
|
|
97
114
|
|
|
98
|
-
return
|
|
115
|
+
return `${session_cite.issue(release.issue_number, undefined, target)} (${how})`
|
|
99
116
|
}
|
|
100
117
|
|
|
101
|
-
function
|
|
102
|
-
const
|
|
118
|
+
function linked_line(release: ReleaseIssue, request: ReleaseRequest, target: string): string {
|
|
119
|
+
const release_label = release_label_of(release, target)
|
|
120
|
+
const issue_label = session_cite.issue(request.issue_number, undefined, target)
|
|
103
121
|
|
|
104
|
-
return
|
|
122
|
+
return request.is_wait
|
|
123
|
+
? `release: ${issue_label} waits on ${release_label}`
|
|
124
|
+
: `release: ${release_label} is blocked by ${issue_label}`
|
|
105
125
|
}
|
|
106
126
|
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
function unlinked_line(edge: Edge, target: string): string {
|
|
128
|
+
const blocker = session_cite.issue(edge.blocker, undefined, target)
|
|
129
|
+
|
|
130
|
+
return `✖ ${blocker} could not be recorded as a blocker of ${session_cite.issue(edge.blocked, undefined, target)}`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function edge_of(release: ReleaseIssue, request: ReleaseRequest): Edge {
|
|
134
|
+
return request.is_wait
|
|
135
|
+
? { blocked: request.issue_number, blocker: release.issue_number }
|
|
136
|
+
: { blocked: release.issue_number, blocker: request.issue_number }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Whether the edge between Issue `<N>` of `target` and the release Issue is now recorded. Every
|
|
140
|
+
// outcome is printed.
|
|
141
|
+
async function record(request: ReleaseRequest, target: string): Promise<boolean> {
|
|
109
142
|
const release = await release_of(target)
|
|
110
143
|
|
|
111
144
|
if (release === undefined) return false
|
|
112
|
-
const
|
|
113
|
-
const is_linked = await git_gh_issue_write.issue_add_blocked_by(
|
|
145
|
+
const edge = edge_of(release, request)
|
|
146
|
+
const is_linked = await git_gh_issue_write.issue_add_blocked_by(
|
|
147
|
+
String(edge.blocked),
|
|
148
|
+
String(edge.blocker),
|
|
149
|
+
)
|
|
114
150
|
|
|
115
|
-
if (is_linked) console.info(linked_line(release,
|
|
116
|
-
else console.error(unlinked_line(
|
|
151
|
+
if (is_linked) console.info(linked_line(release, request, target))
|
|
152
|
+
else console.error(unlinked_line(edge, target))
|
|
117
153
|
|
|
118
154
|
return is_linked
|
|
119
155
|
}
|
|
120
156
|
|
|
121
|
-
//
|
|
122
|
-
function
|
|
123
|
-
return
|
|
157
|
+
// Whether Issue `issue_number` of `target` now blocks the release Issue.
|
|
158
|
+
async function link(issue_number: number, target: string): Promise<boolean> {
|
|
159
|
+
return await record({ issue_number, is_wait: false }, target)
|
|
124
160
|
}
|
|
125
161
|
|
|
126
162
|
// The number is the one positional: a missing, malformed or second one refuses the whole call.
|
|
127
|
-
function issue_number_of(
|
|
128
|
-
const [issue_number, ...rest] =
|
|
163
|
+
function issue_number_of(positionals: ReadonlyArray<string>): number | undefined {
|
|
164
|
+
const [issue_number, ...rest] = positionals
|
|
129
165
|
const is_single = rest.length === 0 && issue_number !== undefined
|
|
130
166
|
|
|
131
167
|
return is_single && issue_number_shape.ISSUE_NUMBER_PATTERN.test(issue_number)
|
|
@@ -133,10 +169,20 @@ function issue_number_of(argv: ReadonlyArray<string>): number | undefined {
|
|
|
133
169
|
: undefined
|
|
134
170
|
}
|
|
135
171
|
|
|
172
|
+
// An unknown flag parses to no request at all, so it refuses the call like a missing number.
|
|
173
|
+
function request_of(argv: ReadonlyArray<string>): ReleaseRequest | undefined {
|
|
174
|
+
const parsed = cli_flags.arguments_of(argv, OPTIONS)
|
|
175
|
+
const issue_number = issue_number_of(parsed?.positionals ?? [])
|
|
176
|
+
|
|
177
|
+
if (parsed === undefined || issue_number === undefined) return undefined
|
|
178
|
+
|
|
179
|
+
return { issue_number, is_wait: parsed.values.wait === true }
|
|
180
|
+
}
|
|
181
|
+
|
|
136
182
|
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
137
|
-
const
|
|
183
|
+
const request = request_of(argv)
|
|
138
184
|
|
|
139
|
-
if (
|
|
185
|
+
if (request === undefined) {
|
|
140
186
|
console.error(USAGE)
|
|
141
187
|
|
|
142
188
|
return FAILURE_EXIT_CODE
|
|
@@ -150,7 +196,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
|
150
196
|
return FAILURE_EXIT_CODE
|
|
151
197
|
}
|
|
152
198
|
|
|
153
|
-
return (await
|
|
199
|
+
return (await record(request, current)) ? SUCCESS_EXIT_CODE : FAILURE_EXIT_CODE
|
|
154
200
|
}
|
|
155
201
|
|
|
156
202
|
// `process.exitCode` rather than `process.exit()`, so the report on standard output drains first.
|
|
@@ -158,8 +204,8 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
|
158
204
|
process.exitCode = await run(argv)
|
|
159
205
|
}
|
|
160
206
|
|
|
161
|
-
const issue_release_cli = { link, run }
|
|
207
|
+
const issue_release_cli = { find_open, link, run }
|
|
162
208
|
|
|
163
209
|
if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
|
|
164
210
|
|
|
165
|
-
export { issue_release_cli }
|
|
211
|
+
export { issue_release_cli, NONE }
|
|
@@ -42,7 +42,9 @@ function body_of(repo: string): string {
|
|
|
42
42
|
'',
|
|
43
43
|
'Every Issue this one is blocked by changes something a consumer can use only once it is published. Once they have merged, a person runs `pnpm josh release` in the primary checkout.',
|
|
44
44
|
'',
|
|
45
|
-
'
|
|
45
|
+
'`pnpm josh release` closes this Issue once that release is published: the next blocker files a new one, so an open release Issue names only unreleased work.',
|
|
46
|
+
'',
|
|
47
|
+
'Every Issue this one blocks needs the published version (`pnpm josh issue:release <N> --wait`); closing this Issue hands each of them back to the backlog.',
|
|
46
48
|
'',
|
|
47
49
|
'`auto-ok` is never applied to this Issue by a run; a person may apply it.',
|
|
48
50
|
'',
|
|
@@ -61,6 +61,17 @@ const AI_COMMANDS: Record<string, CommandEntry> = {
|
|
|
61
61
|
category: 'AI tools',
|
|
62
62
|
reference: ['[--exclude <n>[,<n>...]] [--label <name>]', 'automation', ['network']],
|
|
63
63
|
},
|
|
64
|
+
'auto-ok:judge': {
|
|
65
|
+
script: 'scripts/auto-ok/auto-ok-judge-cli.ts',
|
|
66
|
+
description:
|
|
67
|
+
'List the open issues backlogrun owes an auto-ok judgement, or record one (--apply / --decline)',
|
|
68
|
+
category: 'AI tools',
|
|
69
|
+
reference: [
|
|
70
|
+
'[--apply <n> (--lane | --solo) | --decline <n> --reason <text>]',
|
|
71
|
+
'automation',
|
|
72
|
+
['network'],
|
|
73
|
+
],
|
|
74
|
+
},
|
|
64
75
|
...BACKLOG_COMMANDS,
|
|
65
76
|
cost: {
|
|
66
77
|
script: 'scripts/cost-runtime/cost-cli.ts',
|
|
@@ -75,7 +75,7 @@ const ISSUE_COMMANDS: Record<string, CommandEntry> = {
|
|
|
75
75
|
'File an Issue with every filing step: lint, Origin, fold, WIP cap, duplicate scout, labels, then epic:bundle',
|
|
76
76
|
category: 'AI tools',
|
|
77
77
|
reference: [
|
|
78
|
-
'<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok] [--
|
|
78
|
+
'<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok] [--release]',
|
|
79
79
|
'automation',
|
|
80
80
|
['network'],
|
|
81
81
|
],
|
|
@@ -83,9 +83,9 @@ const ISSUE_COMMANDS: Record<string, CommandEntry> = {
|
|
|
83
83
|
'issue:release': {
|
|
84
84
|
script: 'scripts/issue/issue-release-cli.ts',
|
|
85
85
|
description:
|
|
86
|
-
"Link an Issue to the repository's open release Issue as its blocker, filing one when none is open",
|
|
86
|
+
"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",
|
|
87
87
|
category: 'AI tools',
|
|
88
|
-
reference: ['<issue>', 'automation', ['network']],
|
|
88
|
+
reference: ['<issue> [--wait]', 'automation', ['network']],
|
|
89
89
|
},
|
|
90
90
|
}
|
|
91
91
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
@@ -17,7 +17,7 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
17
17
|
description: 'Set a project up for the GitHub Issue workflow, from git init to the setup PR',
|
|
18
18
|
category: 'Project',
|
|
19
19
|
reference: [
|
|
20
|
-
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--init-command <command>] [--push-preflight-command <command>]`,
|
|
20
|
+
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--dry-run] [--init-command <command>] [--push-preflight-command <command>]`,
|
|
21
21
|
'developer',
|
|
22
22
|
['files', 'git', 'network', 'processes'],
|
|
23
23
|
],
|
|
@@ -52,7 +52,7 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
52
52
|
description: 'Initialize config in a new project',
|
|
53
53
|
category: 'Project',
|
|
54
54
|
reference: [
|
|
55
|
-
`${PROFILE_ARGUMENTS} [--no-install]`,
|
|
55
|
+
`${PROFILE_ARGUMENTS} [--no-install] [--dry-run]`,
|
|
56
56
|
'developer',
|
|
57
57
|
['files', 'network', 'processes'],
|
|
58
58
|
],
|
|
@@ -47,7 +47,7 @@ const WORKFLOW_COMMANDS: Record<string, CommandEntry> = {
|
|
|
47
47
|
description: 'Send Telegram notification',
|
|
48
48
|
category: 'Workflow',
|
|
49
49
|
reference: [
|
|
50
|
-
'[--task-type <type>] [--body <text> | --body-file <path>] [--issue-url <url>] [--pr-url <url>] [--issue-title <text>] [--repo-name <name>]',
|
|
50
|
+
'[--task-type <type>] [--body <text> | --body-file <path>] [--issue-url <url>] [--pr-url <url>] [--issue-title <text>] [--repo-name <name>] [--recommended <text>] [--alternative <text>]...',
|
|
51
51
|
'automation',
|
|
52
52
|
['notifications'],
|
|
53
53
|
],
|