@joshuafolkken/kit 1.1435.0 → 1.1455.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.claude/skills/workflow-commands/backlogrun-handoff-report.md +1 -1
  2. package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
  3. package/.claude/skills/workflow-commands/backlogrun-steps.md +9 -7
  4. package/.claude/skills/workflow-commands/backlogrun.md +2 -2
  5. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  6. package/.claude/skills/workflow-commands/halfrun.md +3 -2
  7. package/.claude/skills/workflow-commands/kickoff.md +12 -13
  8. package/.claude/skills/workflow-commands/observation-filing.md +1 -1
  9. package/.claude/skills/workflow-commands/prerequisite.md +6 -0
  10. package/.claude/skills/workflow-commands/prrun.md +2 -1
  11. package/.claude/skills/workflow-commands/retrospective.md +5 -5
  12. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  13. package/CLAUDE.md +1 -1
  14. package/README.md +10 -19
  15. package/dist/CLAUDE.md +1 -1
  16. package/dist/hooks/batch-guard.js +6 -6
  17. package/dist/hooks/{chunk-3LE6VLSR.js → chunk-3C4XRYG6.js} +18 -5
  18. package/dist/hooks/{chunk-7KMN5QB5.js → chunk-5H4SSDSD.js} +3 -3
  19. package/dist/hooks/{chunk-YZXYOAZ3.js → chunk-7WQBPLUN.js} +11 -5
  20. package/dist/hooks/{chunk-2G5F2YCD.js → chunk-D5QBTOCZ.js} +3 -3
  21. package/dist/hooks/{chunk-MU4LJFIV.js → chunk-GPLGZ6IC.js} +3 -3
  22. package/dist/hooks/{chunk-MSYSUSY6.js → chunk-HFGWYGUV.js} +5 -5
  23. package/dist/hooks/{chunk-JXQBK323.js → chunk-HHI6FO3R.js} +132 -105
  24. package/dist/hooks/{chunk-XER6SSIB.js → chunk-LIAFCXL7.js} +17 -7
  25. package/dist/hooks/{chunk-4BHUPFKF.js → chunk-OLMO72ON.js} +1 -1
  26. package/dist/hooks/{chunk-EMB55BDF.js → chunk-TBJFTSUY.js} +2 -2
  27. package/dist/hooks/{chunk-5SJSSCJG.js → chunk-UT7YC3N2.js} +1 -1
  28. package/dist/hooks/{chunk-QL6QTPWN.js → chunk-WNLVGT7E.js} +3 -3
  29. package/dist/hooks/{chunk-YW452ZAX.js → chunk-Z4XI7CZS.js} +2 -2
  30. package/dist/hooks/codex-hook-adapter.js +13 -13
  31. package/dist/hooks/duplicate-read-guard.js +7 -7
  32. package/dist/hooks/format-edited.js +3 -3
  33. package/dist/hooks/inputs.json +229 -226
  34. package/dist/hooks/investigation-guard.js +7 -7
  35. package/dist/hooks/pretool-guard.js +12 -12
  36. package/dist/hooks/session-lang.js +3 -3
  37. package/dist/hooks/stop-guard.js +64 -11
  38. package/dist/josh.js +34 -11
  39. package/package.json +1 -1
  40. package/prompts/collaboration-workflow/issue-template.md +10 -8
  41. package/prompts/collaboration-workflow/shell-body.md +1 -1
  42. package/prompts/review.md +8 -4
  43. package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
  44. package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
  45. package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
  46. package/scripts/auto-ok/auto-ok-judge.ts +67 -0
  47. package/scripts/backlog/backlog-drive-cli.ts +14 -0
  48. package/scripts/backlog/backlog-drive.ts +22 -3
  49. package/scripts/document/document-byte-budget.ts +1 -1
  50. package/scripts/fresh-machine/fresh-machine-patterns.ts +60 -15
  51. package/scripts/gate/project-checks.ts +5 -2
  52. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  53. package/scripts/gh/git-gh-issue.ts +9 -0
  54. package/scripts/gh/git-pr-ai-review.ts +12 -1
  55. package/scripts/gh/git-pr-confirmation.ts +11 -7
  56. package/scripts/init/init-actions.ts +3 -3
  57. package/scripts/init/init-ai-copy.ts +1 -1
  58. package/scripts/init/init-basic.ts +1 -1
  59. package/scripts/init/init-bootstrap.ts +6 -2
  60. package/scripts/init/init-dry-run-report.ts +150 -0
  61. package/scripts/init/init-dry-run.ts +189 -0
  62. package/scripts/init/init-install.ts +8 -1
  63. package/scripts/init/init.ts +57 -19
  64. package/scripts/init/kit-setup-state.ts +11 -17
  65. package/scripts/init/own-formatter.ts +21 -0
  66. package/scripts/init/project-profile.ts +25 -6
  67. package/scripts/init/start-dry-run.ts +70 -0
  68. package/scripts/init/start-plan.ts +8 -3
  69. package/scripts/init/start-setup-pr.ts +13 -24
  70. package/scripts/init/start-steps.ts +16 -1
  71. package/scripts/init/start.ts +38 -18
  72. package/scripts/init/working-tree.ts +116 -0
  73. package/scripts/issue/issue-auto-ok.ts +68 -99
  74. package/scripts/issue/issue-file-cli.ts +17 -17
  75. package/scripts/issue/issue-file.ts +1 -6
  76. package/scripts/issue/issue-labels.ts +6 -2
  77. package/scripts/issue/issue-release-cli.ts +69 -23
  78. package/scripts/issue/issue-release.ts +3 -1
  79. package/scripts/josh/josh-commands-ai.ts +11 -0
  80. package/scripts/josh/josh-commands-issue.ts +3 -3
  81. package/scripts/josh/josh-commands-project.ts +2 -2
  82. package/scripts/josh/josh-commands-workflow.ts +1 -1
  83. package/scripts/josh/process-identity.ts +32 -6
  84. package/scripts/lane/lane-broker-server.ts +69 -0
  85. package/scripts/lane/lane-session-broker.ts +37 -14
  86. package/scripts/notify/confirmation-options.ts +59 -0
  87. package/scripts/notify/notify-opt-in.ts +33 -0
  88. package/scripts/notify/telegram-notify.ts +25 -8
  89. package/scripts/notify/telegram-test-logic.ts +33 -1
  90. package/scripts/notify/telegram-test.ts +4 -2
  91. package/scripts/observations/observation-ledger-migrate.ts +3 -15
  92. package/scripts/release/release-issue-close.ts +133 -0
  93. package/scripts/release/release-publish.ts +12 -1
  94. package/scripts/repo/repository-labels.ts +11 -1
  95. package/scripts/review/comment-ranges.ts +199 -0
  96. package/scripts/review/review-brief-cli.ts +22 -5
  97. package/scripts/review/review-code-tree.ts +119 -0
  98. package/scripts/review/review-round2-cli.ts +16 -5
  99. package/scripts/review/review-round2.ts +30 -10
  100. package/scripts/review/review-stamps.ts +22 -1
  101. package/scripts/rules/shell-body-trigger.ts +3 -2
  102. package/scripts/rules/stop-rules.ts +2 -1
  103. package/scripts/run/entry/run-entry-stop.ts +6 -3
  104. package/scripts/run/run-stop-notify.ts +2 -0
  105. package/scripts/uninstall/distributed-files.ts +1 -1
@@ -1,34 +1,30 @@
1
- import { git_branch } from '#scripts/git/git-branch'
2
- import { git_command } from '#scripts/git/git-command'
3
- import { run_carry } from '#scripts/run/carry/run-carry'
4
- import { issue_file } from './issue-file'
5
- import { AUTO_OK_LABEL, has_label_name, RELEASE_LABEL } from './issue-labels'
6
- import { issue_state_cli } from './issue-state-cli'
7
-
8
- // Whether `josh issue:file` applies `auto-ok` to the Issue it files. So that work found during an
9
- // unattended run does not wait in the backlog until a person labels it, the filing computes the
10
- // answer instead of leaving it to the agent's memory: an Issue filed while a
11
- // `backlogrun` carry record is live, or from a branch whose Issue carries `auto-ok`, is opted in by
12
- // default. `--no-auto-ok` is the one exception, declared for an Issue that needs a person's judgement
13
- // (a Tier B toss-up or a Tier C action). A filing to another repository is never opted in: that
14
- // repository's own `backlogrun` would implement and merge it though nobody opted that repository in.
15
- // A `release` Issue is never opted in either: a release is Tier C, so only a
16
- // person applies `auto-ok` to it, whatever the carry record or the branch's Issue says.
17
- // `--requested` withholds the default the same way: a person asked for this filing (a `new` entry, or
18
- // a request in conversation), so the run's own opt-in does not speak for them — `auto-ok` comes only
19
- // from their own `--label auto-ok`. A filing without the declaration — an observation, a
20
- // retrospective, a routed filing — keeps the default above.
21
-
22
- interface AutoOkSignals {
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
- // `--requested`: a person asked for the filing, so neither the carry record nor the branch speaks.
25
- is_requested: boolean
26
- // The filing carries the `release` label.
27
- is_release: boolean
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 RELEASE: AutoOkDecision = {
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: 'a person asked for this filing; auto-ok only when they declare it',
47
+ reason: `the body declares \`${JUDGEMENT_NEEDED}\``,
51
48
  }
52
- const CARRIED: AutoOkDecision = { is_applied: true, reason: 'a backlogrun carry record is live' }
53
- const BRANCH_OPTED_IN: AutoOkDecision = {
49
+ const APPLIED: AutoOkDecision = {
54
50
  is_applied: true,
55
- reason: 'the Issue this branch works on carries auto-ok',
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 opt-out, the `release` label and `--requested` win over every signal, then the carry record,
76
- // then the branch's Issue.
77
- function decide(signals: AutoOkSignals): AutoOkDecision {
78
- const decision = withheld(signals)
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
- if (decision !== undefined) return decision
81
- if (signals.is_carried) return CARRIED
82
- if (has_label_name(signals.branch_labels ?? [], AUTO_OK_LABEL)) return BRANCH_OPTED_IN
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 NOT_OPTED_IN
85
- }
68
+ if (found === undefined) return undefined
86
69
 
87
- function line_of(decision: AutoOkDecision): string {
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
- // Read from the common git directory, so a lane sees the record its batch wrote in the main tree.
92
- async function is_carried(): Promise<boolean> {
93
- const directory = await run_carry.repository_directory()
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
- if (directory === undefined) return false
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
- // An unreadable Issue is read as no labels: the filing goes on without `auto-ok` rather than failing.
101
- // The branch names an Issue of `current`, read there by name: a filing to another repository has
102
- // already pointed `GH_REPO` at its target, which would otherwise answer with the target's Issue.
103
- async function branch_labels(current: string): Promise<ReadonlyArray<string> | undefined> {
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
- if (issue_number === undefined) return undefined
107
- const read = await issue_state_cli.read_issue(String(issue_number), current)
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 read.kind === 'state' ? read.state.labels : undefined
92
+ return exclusion_of(filing.labels) ?? (judgement_of(filing.body) === false ? APPLIED : JUDGEMENT)
110
93
  }
111
94
 
112
- // Each read is skipped once an earlier signal has already decided the answer; a cross-repository
113
- // filing is refused before any signal is read. `labels` are the ones the filing declares.
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, line_of, resolve }
99
+ const issue_auto_ok = { declaration_problem, decide, exclusion_of, line_of }
131
100
 
132
- export type { AutoOkDeclared, AutoOkDecision, AutoOkSignals }
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] [--requested] [--release]` — file an Issue
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] [--requested] [--release]'
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 = issue_lint_cli.problems(filing.body).map((problem) => `✖ ${problem}`)
78
- const origin = issue_file.origin_problem(filing.body, filing.target, filing.current)
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 origin === undefined ? problems : [...problems, `✖ ${origin}`]
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
- async function labels_of(filing: Filing): Promise<ReadonlyArray<string> | undefined> {
148
- const declared = {
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
- is_requested: filing.args.is_requested,
151
- }
152
- const auto_ok = await issue_auto_ok.resolve(
153
- declared,
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 = await labels_of(filing)
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 opted in (`issue-auto-ok.ts`) and no `--label`
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` refuses the default for it on every path,
71
- // since a release is Tier C — and only a person opts it into unattended execution.
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 linked_line(release: ReleaseIssue, issue_number: number, target: string): string {
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 `release: ${release_cite} (${how}) is blocked by ${session_cite.issue(issue_number, undefined, target)}`
115
+ return `${session_cite.issue(release.issue_number, undefined, target)} (${how})`
99
116
  }
100
117
 
101
- function unlinked_line(issue_number: number, blocked: string, target: string): string {
102
- const blocker = session_cite.issue(issue_number, undefined, target)
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 `✖ ${blocker} could not be recorded as a blocker of ${session_cite.issue(blocked, undefined, target)}`
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
- // Whether Issue `issue_number` of `target` now blocks the release Issue. Every outcome is printed.
108
- async function link(issue_number: number, target: string): Promise<boolean> {
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 blocked = String(release.issue_number)
113
- const is_linked = await git_gh_issue_write.issue_add_blocked_by(blocked, String(issue_number))
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, issue_number, target))
116
- else console.error(unlinked_line(issue_number, blocked, target))
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
- // An unknown flag parses to no positionals, so it refuses the call like a missing number.
122
- function positionals_of(argv: ReadonlyArray<string>): ReadonlyArray<string> {
123
- return cli_flags.arguments_of(argv, {})?.positionals ?? []
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(argv: ReadonlyArray<string>): number | undefined {
128
- const [issue_number, ...rest] = positionals_of(argv)
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 issue_number = issue_number_of(argv)
183
+ const request = request_of(argv)
138
184
 
139
- if (issue_number === undefined) {
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 link(issue_number, current)) ? SUCCESS_EXIT_CODE : FAILURE_EXIT_CODE
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
- 'Close this Issue once that release is published: the next blocker files a new one, so an open release Issue names only unreleased work.',
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] [--requested] [--release]',
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
  ],