@joshuafolkken/kit 1.1136.0 → 1.1178.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/settings.json +4 -4
- package/.claude/skills/epic-commands/SKILL.md +27 -239
- package/.claude/skills/epic-commands/epic-bundle.md +115 -0
- package/.claude/skills/epic-commands/execution-waves.md +126 -0
- package/.claude/skills/workflow-commands/SKILL.md +48 -194
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-child.md +12 -123
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +14 -63
- package/.claude/skills/workflow-commands/backlogrun-park.md +16 -15
- package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -173
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +155 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +65 -144
- package/.claude/skills/workflow-commands/backlogrun.md +5 -4
- package/.claude/skills/workflow-commands/chain-rule.md +3 -2
- package/.claude/skills/workflow-commands/delegation.md +2 -2
- package/.claude/skills/workflow-commands/entry-sequence.md +62 -0
- package/.claude/skills/workflow-commands/followup.md +104 -181
- package/.claude/skills/workflow-commands/fullrun-steps.md +8 -9
- package/.claude/skills/workflow-commands/fullrun.md +16 -73
- package/.claude/skills/workflow-commands/halfrun.md +10 -36
- package/.claude/skills/workflow-commands/into-target.md +4 -4
- package/.claude/skills/workflow-commands/issue-comments.md +7 -8
- package/.claude/skills/workflow-commands/issue-scout.md +6 -6
- package/.claude/skills/workflow-commands/kickoff.md +6 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +6 -6
- package/.claude/skills/workflow-commands/observation-filing.md +21 -172
- package/.claude/skills/workflow-commands/observation-ledger.md +150 -0
- package/.claude/skills/workflow-commands/pre-gate-cut.md +35 -95
- package/.claude/skills/workflow-commands/prerequisite.md +6 -6
- package/.claude/skills/workflow-commands/progress-watcher.md +90 -0
- package/.claude/skills/workflow-commands/prrun.md +5 -5
- package/.claude/skills/workflow-commands/retrospective.md +9 -9
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/.claude/skills/workflow-commands/target-repository.md +2 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +7 -5
- package/.codex/hooks.json +4 -4
- package/.github/workflows/ci.yml +15 -2
- package/AGENTS.md +1 -6
- package/CLAUDE.md +58 -120
- package/GEMINI.md +1 -6
- package/dist/CLAUDE.md +58 -120
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-NFZGFGOR.js → chunk-32GQTL4F.js} +14 -1
- package/dist/hooks/{chunk-HBKZLITF.js → chunk-DO6NWWHU.js} +4 -4
- package/dist/hooks/{chunk-KBXZLMRH.js → chunk-G6KRCKEC.js} +216 -76
- package/dist/hooks/{chunk-JKKTVDAG.js → chunk-GDS5N2KB.js} +186 -309
- package/dist/hooks/chunk-I4XSL4TT.js +56 -0
- package/dist/hooks/{chunk-GDU26K3J.js → chunk-S6X6WYKR.js} +886 -174
- package/dist/hooks/{chunk-FO2I7UT7.js → chunk-UAGLLIGZ.js} +1 -1
- package/dist/hooks/{chunk-RFWWUI5F.js → chunk-VESKRQOF.js} +3 -3
- package/dist/hooks/{chunk-KOKOAV6D.js → chunk-VQLLNFNT.js} +6 -4
- package/dist/hooks/{chunk-LJBLGHEH.js → chunk-ZAL3NRGL.js} +4 -6
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/inputs.json +178 -177
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +11 -10
- package/dist/hooks/stop-guard.js +20 -6
- package/dist/josh.js +29 -3
- package/dist/version/index.js +5 -1
- package/package.json +2 -1
- package/prompts/collaboration-workflow/file-edits.md +11 -10
- package/prompts/collaboration-workflow/gh-rest.md +2 -4
- package/prompts/collaboration-workflow/glossary.md +1 -1
- package/prompts/collaboration-workflow/issue-citation.md +5 -9
- package/prompts/collaboration-workflow/issue-template.md +40 -115
- package/prompts/collaboration-workflow/operating-rules.md +4 -5
- package/prompts/collaboration-workflow/output-bounds.md +12 -78
- package/prompts/collaboration-workflow/overview.md +7 -13
- package/prompts/collaboration-workflow/plan-comment.md +11 -39
- package/prompts/collaboration-workflow/principles.md +3 -4
- package/prompts/collaboration-workflow/report-format.md +28 -74
- package/prompts/collaboration-workflow/residency.md +12 -109
- package/prompts/collaboration-workflow/rule-delivery.md +98 -97
- package/prompts/collaboration-workflow/shell-body.md +3 -13
- package/prompts/collaboration-workflow/turn-batching.md +14 -88
- package/prompts/collaboration-workflow/upstream-interrupt.md +12 -14
- package/prompts/collaboration-workflow/wip-cap.md +6 -45
- package/prompts/collaboration-workflow.md +1 -13
- package/prompts/review-rubric.md +8 -71
- package/prompts/review.md +25 -55
- package/prompts/testing-guide.md +18 -67
- package/scripts/agent/agent-role-profile.ts +1 -1
- package/scripts/agent/codex-hooks.ts +3 -12
- package/scripts/backlog/backlog-budget.ts +1 -1
- package/scripts/backlog/backlog-drive-cli.ts +4 -3
- package/scripts/backlog/backlog-drive-offer-argv.ts +10 -1
- package/scripts/backlog/backlog-next.ts +15 -4
- package/scripts/backlog/backlog-offer.ts +6 -6
- package/scripts/backlog/backlog-plan-cli.ts +6 -5
- package/scripts/backlog/backlog-rank.ts +18 -9
- package/scripts/backlog/backlog-restructure.ts +152 -0
- package/scripts/backlog/backlog-waves.ts +15 -17
- package/scripts/behavior/behavior-rules.ts +11 -4
- package/scripts/cost-runtime/context-cut-threshold.ts +1 -9
- package/scripts/cost-runtime/cost-verdict.ts +15 -28
- package/scripts/delegation/investigation-reads.ts +5 -5
- package/scripts/discovery/repo-discovery.ts +21 -7
- package/scripts/discovery/repo-origin.ts +26 -0
- package/scripts/discovery/repo-party.ts +42 -1
- package/scripts/document/backlogrun-parent-read-set.ts +14 -15
- package/scripts/document/document-byte-budget.ts +34 -20
- package/scripts/document/entry-read-budget.ts +21 -9
- package/scripts/document/lane-child-read-set.ts +12 -14
- package/scripts/document/read-set-cli.ts +4 -4
- package/scripts/document/read-set-point-of-use.ts +10 -2
- package/scripts/document/resident-budget.ts +47 -0
- package/scripts/epic/epic-shape.ts +10 -13
- package/scripts/epic/epic-solo.ts +1 -1
- package/scripts/exports/unused-members-cli.ts +7 -3
- package/scripts/followup/git-followup-stages.ts +9 -0
- package/scripts/followup/git-pr-followup.ts +1 -1
- package/scripts/gh/git-gh-api-path.ts +7 -0
- package/scripts/gh/git-gh-issue.ts +5 -3
- package/scripts/gh/git-gh-pr-read.ts +4 -0
- package/scripts/gh/git-pr-checks-eval.ts +8 -0
- package/scripts/gh/git-pr-checks.ts +11 -1
- package/scripts/git/constants.ts +10 -0
- package/scripts/git/main-merge.ts +57 -21
- package/scripts/git/schemas.ts +2 -0
- package/scripts/hooks/pretool-guard-cli.ts +2 -2
- package/scripts/hooks/pretool-guard.ts +45 -2
- package/scripts/hooks/run-hook.sh +41 -0
- package/scripts/init/hook-command-rewrite.ts +18 -35
- package/scripts/init/hook-launch.ts +21 -32
- package/scripts/issue/issue-auto-ok.ts +95 -0
- package/scripts/issue/issue-file-cli.ts +53 -7
- package/scripts/issue/issue-file.ts +28 -5
- package/scripts/issue/issue-labels.ts +12 -12
- package/scripts/issue/issue-rank.ts +1 -1
- package/scripts/issue/issue-read-cli.ts +3 -3
- package/scripts/issue/issue-read.ts +2 -2
- package/scripts/issue/issue-scout-cli.ts +2 -2
- package/scripts/issue/issue-state-cli.ts +1 -1
- package/scripts/issue/issue-state.ts +2 -2
- package/scripts/issue/issue-wip.ts +70 -0
- package/scripts/josh/hook-decision.ts +17 -0
- package/scripts/josh/josh-commands-development.ts +11 -1
- package/scripts/josh/josh-commands-issue.ts +2 -2
- package/scripts/lane/lane-launch-cli.ts +21 -3
- package/scripts/lines/bytes-command.ts +29 -6
- package/scripts/notify/telegram-notify.ts +26 -1
- package/scripts/observations/observation-ledger-line.ts +1 -1
- package/scripts/release/release-progress.ts +167 -0
- package/scripts/release/release-publish.ts +19 -8
- package/scripts/release/release-tag.ts +22 -8
- package/scripts/rules/decision-oracle-stage.ts +1 -1
- package/scripts/rules/decision-oracle.ts +6 -6
- package/scripts/rules/delivered-rules.ts +11 -11
- package/scripts/rules/direct-pr-create.ts +110 -0
- package/scripts/rules/file-body.ts +98 -42
- package/scripts/rules/filing-cap.ts +2 -2
- package/scripts/rules/filing-offer.ts +1 -1
- package/scripts/rules/gh-api.ts +18 -1
- package/scripts/rules/implementation-cut.ts +3 -1
- package/scripts/rules/lane-background.ts +67 -10
- package/scripts/rules/permission-guards.ts +10 -2
- package/scripts/rules/pre-gate-cut.ts +2 -2
- package/scripts/rules/stop-rules.ts +4 -4
- package/scripts/rules/test-declared-commit.ts +1 -1
- package/scripts/rules/third-party-write.ts +56 -12
- package/scripts/run/run-carry.ts +11 -5
- package/scripts/run/run-cut-cli.ts +1 -1
- package/scripts/run/run-cut-handoff.ts +21 -1
- package/scripts/run/run-cut-report.ts +2 -2
- package/scripts/run/run-entry-cli.ts +11 -3
- package/scripts/run/run-hold-cli.ts +16 -2
- package/scripts/run/run-label.ts +50 -0
- package/scripts/run/run-merge-steps.ts +8 -3
- package/scripts/run/run-merge.ts +1 -1
- package/scripts/run/run-prep-cli.ts +6 -2
- package/scripts/run/run-prep.ts +17 -0
- package/scripts/run/run-ship-cli.ts +27 -6
- package/scripts/run/run-ship-next.ts +13 -0
- package/scripts/run/run-ship-pre-detach.ts +66 -0
- package/scripts/run/run-ship-preflight.ts +34 -3
- package/scripts/run/run-ship-return.ts +7 -4
- package/scripts/run/run-ship-review-steps.ts +1 -1
- package/scripts/run/run-ship-review.ts +26 -5
- package/scripts/run/run-ship-stage.ts +13 -0
- package/scripts/run/run-ship-steps.ts +10 -2
- package/scripts/run/run-ship-sync.ts +114 -0
- package/scripts/run/run-ship.ts +6 -0
- package/scripts/run/run-step-cli.ts +20 -7
- package/scripts/run/run-wake-cli.ts +8 -1
- package/scripts/run/run-wake-driver.ts +62 -8
- package/scripts/run/run-wake-session.ts +2 -1
- package/scripts/test/test-declared-logic.ts +1 -1
- package/scripts/test/test-red-logic.ts +17 -2
- package/scripts/test/test-red.ts +8 -7
- package/scripts/time-runtime/time-batch-guard.ts +52 -16
- package/scripts/time-runtime/time-density-hook.ts +20 -2
- package/scripts/time-runtime/time-density.ts +4 -3
- package/scripts/time-runtime/time-markers.ts +15 -4
- package/scripts/time-runtime/time-transcript-line.ts +27 -1
- package/scripts/version/npm-registry.ts +11 -1
- package/templates/workflows/ci.yml +9 -1
- package/.claude/skills/workflow-commands/followup-reference.md +0 -118
- package/dist/hooks/chunk-HANSKHES.js +0 -464
|
@@ -44,44 +44,79 @@ function merge_message(default_branch: string, current_branch: string): string |
|
|
|
44
44
|
return `Merge ${default_branch} into ${current_branch} #${issue_number}`
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
// What one merge of the default branch came to (joshuafolkken/kit#3221). `josh ship` merges it again
|
|
48
|
+
// before the commit and on a conflicting pull request, and branches on the answer rather than on an
|
|
49
|
+
// exit code: `current` brought nothing in, so the tree is unchanged; `merged` changed it; `refused`
|
|
50
|
+
// is the guard's answer, nothing attempted; `conflict` names the paths git left unmerged, so a resumed
|
|
51
|
+
// session goes straight to them instead of asking git again.
|
|
52
|
+
type MergeOutcome =
|
|
53
|
+
| { kind: 'current'; branch: string }
|
|
54
|
+
| { kind: 'merged'; branch: string }
|
|
55
|
+
| { kind: 'refused'; message: string }
|
|
56
|
+
| { kind: 'conflict'; files: ReadonlyArray<string> }
|
|
57
|
+
|
|
58
|
+
interface MergeTarget {
|
|
59
|
+
default_branch: string
|
|
60
|
+
current_branch: string
|
|
61
|
+
is_current: boolean
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
// A
|
|
57
|
-
//
|
|
58
|
-
async function
|
|
64
|
+
// A failure that left no unmerged path — a remote that cannot be reached, a hook that refused — is not
|
|
65
|
+
// a conflict, so it is rethrown rather than reported as one with nothing to resolve.
|
|
66
|
+
async function merge_into(target: MergeTarget): Promise<MergeOutcome> {
|
|
67
|
+
const { default_branch, current_branch } = target
|
|
68
|
+
|
|
59
69
|
try {
|
|
60
|
-
await git_command.merge_branch(default_branch,
|
|
70
|
+
await git_command.merge_branch(default_branch, merge_message(default_branch, current_branch))
|
|
61
71
|
} catch (error) {
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
const files = main_merge_guard.unmerged_paths(await git_command.status())
|
|
73
|
+
|
|
74
|
+
if (files.length === 0) throw error
|
|
75
|
+
|
|
76
|
+
return { kind: 'conflict', files }
|
|
64
77
|
}
|
|
78
|
+
|
|
79
|
+
return { kind: target.is_current ? 'current' : 'merged', branch: default_branch }
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
// The fetch is what makes `origin/<default>` current before the merge reads it. `git pull` did the
|
|
68
|
-
// two together, which is the only thing it was doing here that is worth keeping.
|
|
69
|
-
|
|
83
|
+
// two together, which is the only thing it was doing here that is worth keeping. The refusal is read
|
|
84
|
+
// after the fetch, so the incoming paths are the ones the merge would bring in (joshuafolkken/kit#2445).
|
|
85
|
+
async function merge(): Promise<MergeOutcome> {
|
|
70
86
|
const default_branch = await git_command.get_default_branch()
|
|
71
87
|
const current_branch = await git_command.branch()
|
|
72
88
|
|
|
73
89
|
await git_command.fetch_branch(default_branch)
|
|
74
90
|
|
|
75
|
-
const
|
|
91
|
+
const status = await git_command.status()
|
|
92
|
+
const incoming = await main_merge_guard.incoming_paths(default_branch)
|
|
93
|
+
const message = main_merge_guard.refusal(status, incoming, default_branch)
|
|
94
|
+
|
|
95
|
+
if (message !== undefined) return { kind: 'refused', message }
|
|
96
|
+
|
|
97
|
+
return await merge_into({ default_branch, current_branch, is_current: incoming.length === 0 })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A merge that stops on a conflict keeps git's report and adds how to finish it, so the next step is
|
|
101
|
+
// the sanctioned commit rather than a request for `git add`.
|
|
102
|
+
function failure_text(outcome: MergeOutcome): string | undefined {
|
|
103
|
+
if (outcome.kind === 'refused') return outcome.message
|
|
104
|
+
if (outcome.kind === 'conflict') return main_merge_guard.CONFLICT_HINT
|
|
105
|
+
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function merge_default_branch(): Promise<number> {
|
|
110
|
+
const outcome = await merge()
|
|
111
|
+
const failure = failure_text(outcome)
|
|
76
112
|
|
|
77
|
-
if (
|
|
78
|
-
console.error(
|
|
113
|
+
if (failure !== undefined) {
|
|
114
|
+
console.error(failure)
|
|
79
115
|
|
|
80
116
|
return FAILURE_EXIT_CODE
|
|
81
117
|
}
|
|
82
118
|
|
|
83
|
-
|
|
84
|
-
console.info(default_branch)
|
|
119
|
+
if ('branch' in outcome) console.info(outcome.branch)
|
|
85
120
|
|
|
86
121
|
return SUCCESS_EXIT_CODE
|
|
87
122
|
}
|
|
@@ -114,8 +149,9 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
|
114
149
|
process.exitCode = await run(argv)
|
|
115
150
|
}
|
|
116
151
|
|
|
117
|
-
const main_merge = { run }
|
|
152
|
+
const main_merge = { merge, run }
|
|
118
153
|
|
|
119
154
|
if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
|
|
120
155
|
|
|
121
156
|
export { main_merge }
|
|
157
|
+
export type { MergeOutcome }
|
package/scripts/git/schemas.ts
CHANGED
|
@@ -140,6 +140,8 @@ const open_issue_schema = z.object({
|
|
|
140
140
|
labels: z.array(issue_label_schema).optional(),
|
|
141
141
|
createdAt: z.string(),
|
|
142
142
|
blockedBy: blocked_by_schema,
|
|
143
|
+
// Present only on the pickup listing, which reads it for the restructures each issue declares.
|
|
144
|
+
body: z.string().optional(),
|
|
143
145
|
})
|
|
144
146
|
|
|
145
147
|
type RollupItemData = z.infer<typeof rollup_item_schema>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { text } from 'node:stream/consumers'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
5
|
-
import { pretool_outcome_async } from './pretool-guard'
|
|
5
|
+
import { pretool_guard, pretool_outcome_async } from './pretool-guard'
|
|
6
6
|
|
|
7
7
|
// The `PreToolUse` hook's entry, kept apart from the composition in `pretool-guard.ts`
|
|
8
8
|
// (joshuafolkken/kit#2922). The hook bundles are built with `splitting: true`, which moves a module
|
|
@@ -16,6 +16,6 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
16
16
|
const raw_payload = await text(process.stdin)
|
|
17
17
|
|
|
18
18
|
hook_decision.load_environment_file()
|
|
19
|
-
|
|
19
|
+
pretool_guard.emit(raw_payload, await pretool_outcome_async(raw_payload))
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { duplicate_read_outcome } from '#scripts/delegation/duplicate-read-guard'
|
|
2
2
|
import { investigation_refusal } from '#scripts/delegation/investigation-guard'
|
|
3
|
-
import type
|
|
3
|
+
import { hook_decision, type GuardOutcome } from '#scripts/josh/hook-decision'
|
|
4
4
|
import { delivered_rules } from '#scripts/rules/delivered-rules'
|
|
5
|
+
import { lane_background } from '#scripts/rules/lane-background'
|
|
5
6
|
import { run_parent_cut_hook } from '#scripts/run/run-parent-cut-hook'
|
|
6
7
|
import { run_watcher_hook } from '#scripts/run/run-watcher-hook'
|
|
8
|
+
import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
|
|
7
9
|
import { batch_outcome } from './batch-guard'
|
|
8
10
|
import { step_zero_notice } from './step-zero-notice'
|
|
9
11
|
|
|
@@ -108,6 +110,47 @@ async function pretool_outcome_async(raw_payload: string): Promise<GuardOutcome>
|
|
|
108
110
|
return with_step_zero_notice(base, raw_payload)
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
|
|
113
|
+
function guarded_call(raw_payload: string): GuardedCall | undefined {
|
|
114
|
+
try {
|
|
115
|
+
const payload = hook_decision.parse_hook_payload(raw_payload)
|
|
116
|
+
|
|
117
|
+
return payload === undefined
|
|
118
|
+
? undefined
|
|
119
|
+
: { name: payload.tool_name, input: payload.tool_input }
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The envelope that replaces the outcome's own when a lane child backgrounded `josh ship` alone
|
|
126
|
+
// (joshuafolkken/kit#3154): the call is run in the foreground instead of being refused. A refusal from
|
|
127
|
+
// any guard still wins, and a notice the outcome carried rides along in the same context.
|
|
128
|
+
function foreground_rewrite(
|
|
129
|
+
outcome: GuardOutcome,
|
|
130
|
+
raw_payload: string,
|
|
131
|
+
is_lane_child?: () => boolean,
|
|
132
|
+
): string | undefined {
|
|
133
|
+
if (outcome.reason !== undefined) return undefined
|
|
134
|
+
|
|
135
|
+
const call = guarded_call(raw_payload)
|
|
136
|
+
const input =
|
|
137
|
+
call === undefined ? undefined : lane_background.foreground_input(call, is_lane_child)
|
|
138
|
+
|
|
139
|
+
if (input === undefined) return undefined
|
|
140
|
+
|
|
141
|
+
const context = [lane_background.FOREGROUND_NOTE, outcome.notice ?? outcome.fault]
|
|
142
|
+
|
|
143
|
+
return hook_decision.rewrite_envelope(input, context.filter(Boolean).join('\n\n'))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The hook's one write: the foreground rewrite where it applies, the outcome's own envelope otherwise.
|
|
147
|
+
function emit(raw_payload: string, outcome: GuardOutcome): void {
|
|
148
|
+
const rewrite = foreground_rewrite(outcome, raw_payload)
|
|
149
|
+
|
|
150
|
+
if (rewrite === undefined) hook_decision.emit_outcome(outcome)
|
|
151
|
+
else process.stdout.write(`${rewrite}\n`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const pretool_guard = { combine_outcomes, emit, foreground_rewrite, pretool_outcome }
|
|
112
155
|
|
|
113
156
|
export { pretool_guard, pretool_outcome, pretool_outcome_async }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Launch one kit hook by name: `sh scripts/hooks/run-hook.sh <name> [arguments...]`
|
|
3
|
+
# (joshuafolkken/kit#3184).
|
|
4
|
+
#
|
|
5
|
+
# A hook command in `.claude/settings.json` / `.codex/hooks.json` only moves to the project root
|
|
6
|
+
# (`scripts/init/hook-launch.ts`) and calls this script, so everything every hook shares lives here once:
|
|
7
|
+
#
|
|
8
|
+
# - The git location variables a git hook exports are cleared when the root resolves without them, so
|
|
9
|
+
# the hook's own git calls act on the checkout it stands in. They are kept when the checkout needs
|
|
10
|
+
# them — git metadata outside the work tree.
|
|
11
|
+
# - The pre-built bundle `dist/hooks/<name>.js` runs once its gate passes; otherwise the live source
|
|
12
|
+
# runs through josh, whose command is the name with its first `-` turned into `:`
|
|
13
|
+
# (`pretool-guard` → `pretool:guard`).
|
|
14
|
+
# - In kit's own checkout the gate is `hook-bundle-ready.ts`, which rebuilds stale bundles
|
|
15
|
+
# (joshuafolkken/kit#2984). In an installed package it is a presence check: the published bundles
|
|
16
|
+
# cannot be stale against a source the consumer never edits, and Node strips no types under
|
|
17
|
+
# `node_modules`.
|
|
18
|
+
# - An `if`/`else`, never `&&`/`||`: a guard's refusal is a non-zero exit, and a chain would read it as
|
|
19
|
+
# a missing bundle and run the hook twice.
|
|
20
|
+
|
|
21
|
+
location_variables='GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_COMMON_DIR GIT_DIR GIT_INDEX_FILE GIT_NAMESPACE GIT_OBJECT_DIRECTORY GIT_WORK_TREE'
|
|
22
|
+
unset_options=''
|
|
23
|
+
for variable in $location_variables; do unset_options="$unset_options -u $variable"; done
|
|
24
|
+
# shellcheck disable=SC2086
|
|
25
|
+
if env $unset_options git rev-parse --show-toplevel >/dev/null 2>&1; then unset $location_variables; fi
|
|
26
|
+
|
|
27
|
+
hook_name=$1
|
|
28
|
+
shift
|
|
29
|
+
hooks_dir=$(dirname "$0")
|
|
30
|
+
package_dir="$hooks_dir/../.."
|
|
31
|
+
bundle="$package_dir/dist/hooks/$hook_name.js"
|
|
32
|
+
josh_command="${hook_name%%-*}:${hook_name#*-}"
|
|
33
|
+
|
|
34
|
+
case $0 in
|
|
35
|
+
*node_modules/*)
|
|
36
|
+
if [ -d "$package_dir/dist/hooks" ]; then node "$bundle" "$@"; else node "$package_dir/dist/josh.js" "$josh_command" "$@"; fi
|
|
37
|
+
;;
|
|
38
|
+
*)
|
|
39
|
+
if node --disable-warning=ExperimentalWarning "$hooks_dir/hook-bundle-ready.ts"; then node "$bundle" "$@"; else pnpm josh "$josh_command" "$@"; fi
|
|
40
|
+
;;
|
|
41
|
+
esac
|
|
@@ -3,60 +3,43 @@ import { hook_launch } from './hook-launch'
|
|
|
3
3
|
|
|
4
4
|
// The consumer's `.claude/settings.json` runs its hooks off the published package directly, not
|
|
5
5
|
// through `pnpm josh` (joshuafolkken/kit#1930, joshuafolkken/kit#2023). Two paths inside a hook
|
|
6
|
-
// command are rebased onto the installed package: the
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// and a `pnpm` launch would only add the wrapper this exists to remove.
|
|
6
|
+
// command are rebased onto the installed package: the hook launcher `scripts/hooks/run-hook.sh`
|
|
7
|
+
// (joshuafolkken/kit#3184), which picks the bundle or the dispatcher from where it is installed, and a
|
|
8
|
+
// plain `pnpm josh <command>`, which becomes the `dist/josh.js` dispatcher — the published package
|
|
9
|
+
// always ships `dist`, and a `pnpm` launch would only add the wrapper this exists to remove.
|
|
10
10
|
//
|
|
11
|
-
// kit's own settings file keeps the
|
|
12
|
-
//
|
|
13
|
-
// copy destination, exactly as the plugin injection is — kit's source tree is never
|
|
14
|
-
// place.
|
|
11
|
+
// kit's own settings file keeps the launcher's live-source fallback (`pnpm josh` against
|
|
12
|
+
// `scripts/josh/josh.ts`), so a fresh clone with no `dist/` still runs the current guards. The rewrite
|
|
13
|
+
// is gated on the copy destination, exactly as the plugin injection is — kit's source tree is never
|
|
14
|
+
// transformed in place.
|
|
15
15
|
|
|
16
16
|
const PNPM_JOSH_PREFIX = 'pnpm josh '
|
|
17
|
-
const CODEX_ADAPTER_SOURCE = 'pnpm exec tsx scripts/hooks/codex-hook-adapter.ts'
|
|
18
17
|
// Relative to the consumer's project root, which hook commands select before using this path.
|
|
19
18
|
// The path is the plugin marketplace's `node_modules` location plus the built entry `bin.josh` names.
|
|
20
19
|
const BUNDLE_INVOCATION = `node ${claude_plugin_config.MARKETPLACE_PATH}/dist/josh.js `
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
const
|
|
20
|
+
// Anchored on the `sh ` that launches it: the installed path still ends in the source path, so only the
|
|
21
|
+
// anchor keeps a second pass from rebasing an already-rebased launcher onto itself.
|
|
22
|
+
const RUN_HOOK_INVOCATION = `sh ${hook_launch.RUN_HOOK_SCRIPT} `
|
|
23
|
+
const CONSUMER_RUN_HOOK_INVOCATION = `sh ${claude_plugin_config.MARKETPLACE_PATH}/${hook_launch.RUN_HOOK_SCRIPT} `
|
|
24
24
|
const CONSUMER_PACKAGE_MANIFEST = `${claude_plugin_config.MARKETPLACE_PATH}/package.json`
|
|
25
25
|
const MISSING_INSTALL_NOTICE = 'kit hooks inactive: run pnpm install, then reread CLAUDE.md'
|
|
26
26
|
const MISSING_INSTALL_GUARD = `if [ ! -f ${CONSUMER_PACKAGE_MANIFEST} ]; then echo '${MISSING_INSTALL_NOTICE}' >&2; exit 0; fi; `
|
|
27
|
-
const CODEX_ADAPTER_BUNDLE = `node ${CONSUMER_HOOK_BUNDLE_DIR}codex-hook-adapter.js`
|
|
28
27
|
const CODEX_HOOKS_DESTINATION = '.codex/hooks.json'
|
|
29
28
|
// The values below are matched inside raw JSON text, so the root prefix is taken in its escaped form.
|
|
30
29
|
const PROJECT_ROOT_PREFIX = JSON.stringify(hook_launch.PROJECT_ROOT_PREFIX).slice(1, -1)
|
|
31
|
-
// The installed package ships its bundles built, so they cannot be stale against a source the consumer
|
|
32
|
-
// never has: the ready gate kit's own commands pass (joshuafolkken/kit#2984) becomes a presence check.
|
|
33
|
-
const CONSUMER_READY_GATE = `[ -d ${CONSUMER_HOOK_BUNDLE_DIR} ]`
|
|
34
30
|
// Capture the value of every `"command"` field, escapes and all, so the rewrites below touch command
|
|
35
31
|
// values alone — never an echo reminder's prose or a `"description"` that merely mentions the string.
|
|
36
32
|
const COMMAND_FIELD = /("command":\s*")((?:[^"\\]|\\.)*)(")/gu
|
|
37
33
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// rewrite had for free.
|
|
42
|
-
function rebase_bundle_directory(value: string): string {
|
|
43
|
-
return value.includes(CONSUMER_HOOK_BUNDLE_DIR)
|
|
44
|
-
? value
|
|
45
|
-
: value.split(HOOK_BUNDLE_DIR).join(CONSUMER_HOOK_BUNDLE_DIR)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Rebase both paths a hook command can carry: the bundle it prefers, then the `pnpm josh` fallback.
|
|
49
|
-
// The bundle rewrite runs first — its replacement never contains `pnpm josh`, so the two are
|
|
50
|
-
// independent whichever order they run. The fallback rewrite is idempotent too (`BUNDLE_INVOCATION`
|
|
51
|
-
// holds no `pnpm josh `), so the whole rewrite is safe to run more than once.
|
|
34
|
+
// Rebase both paths a hook command can carry: the launcher, then a plain `pnpm josh`. Neither
|
|
35
|
+
// replacement contains the other's anchor, so the two are independent whichever order they run, and
|
|
36
|
+
// the whole rewrite is safe to run more than once.
|
|
52
37
|
function rewrite_command_value(value: string): string {
|
|
53
|
-
const rewritten =
|
|
54
|
-
.split(
|
|
55
|
-
.join(
|
|
38
|
+
const rewritten = value
|
|
39
|
+
.split(RUN_HOOK_INVOCATION)
|
|
40
|
+
.join(CONSUMER_RUN_HOOK_INVOCATION)
|
|
56
41
|
.split(PNPM_JOSH_PREFIX)
|
|
57
42
|
.join(BUNDLE_INVOCATION)
|
|
58
|
-
.split(CODEX_ADAPTER_SOURCE)
|
|
59
|
-
.join(CODEX_ADAPTER_BUNDLE)
|
|
60
43
|
|
|
61
44
|
if (!rewritten.includes(claude_plugin_config.MARKETPLACE_PATH)) return rewritten
|
|
62
45
|
if (rewritten.includes(MISSING_INSTALL_GUARD)) return rewritten
|
|
@@ -7,51 +7,40 @@ import { GIT_LOCATION_VARIABLES } from '#scripts/git/git-location-environment'
|
|
|
7
7
|
//
|
|
8
8
|
// **The command starts from the project root, never from wherever the session happens to be**
|
|
9
9
|
// (joshuafolkken/kit#2984). Every path after the prefix is relative, so a hook fired from a
|
|
10
|
-
// subdirectory would otherwise miss its
|
|
11
|
-
// hook exports cleared first
|
|
12
|
-
//
|
|
10
|
+
// subdirectory would otherwise miss its script. The root is git's, resolved with the location
|
|
11
|
+
// variables a git hook exports cleared first, and resolved with them only when that fails — git
|
|
12
|
+
// metadata outside the work tree. The consumer rewrite (`hook-command-rewrite.ts`) keeps this prefix,
|
|
13
|
+
// so both sides stay one mechanism for Claude Code and Codex alike.
|
|
13
14
|
//
|
|
14
|
-
// **
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// source. **An `if`/`else`, never `&&`/`||`**: a guard's refusal is a non-zero exit, and a
|
|
18
|
-
// `node … || pnpm josh …` chain would read that refusal as a missing bundle and run the hook twice.
|
|
15
|
+
// **Everything after the root is one script** (`scripts/hooks/run-hook.sh`, joshuafolkken/kit#3184):
|
|
16
|
+
// clearing the location variables for the hook itself, the bundle-ready gate and the live-source
|
|
17
|
+
// fallback live there once, so each hook command names only its hook.
|
|
19
18
|
|
|
20
19
|
const HOOK_DIST_DIR = 'dist/hooks'
|
|
20
|
+
const RUN_HOOK_SCRIPT = 'scripts/hooks/run-hook.sh'
|
|
21
21
|
const UNSET_OPTIONS = GIT_LOCATION_VARIABLES.map((name) => `-u ${name}`).join(' ')
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
// every hook's stderr.
|
|
25
|
-
const BUNDLE_READY_GATE =
|
|
26
|
-
'node --disable-warning=ExperimentalWarning scripts/hooks/hook-bundle-ready.ts'
|
|
22
|
+
const ROOT_LOOKUP = 'git rev-parse --show-toplevel'
|
|
23
|
+
const PROJECT_ROOT_PREFIX = `project_root="$(env ${UNSET_OPTIONS} ${ROOT_LOOKUP} 2>/dev/null || ${ROOT_LOOKUP})" && cd "$project_root" && `
|
|
27
24
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
function
|
|
31
|
-
return
|
|
25
|
+
// `name` is the hook's bundle under `dist/hooks` without `.js` (e.g. `pretool-guard`), followed by any
|
|
26
|
+
// arguments it takes (e.g. `codex-hook-adapter pretool`).
|
|
27
|
+
function hook_launch_command(name: string): string {
|
|
28
|
+
return `${PROJECT_ROOT_PREFIX}sh ${RUN_HOOK_SCRIPT} ${name}`
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
//
|
|
35
|
-
// `
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return `${PROJECT_ROOT_PREFIX}${launch_command(BUNDLE_READY_GATE, primary, fallback_command)}`
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// `bundle` is the hook's file under `dist/hooks` (e.g. `pretool-guard.js`); `command` is the josh
|
|
43
|
-
// subcommand the fallback runs (e.g. `pretool:guard`).
|
|
44
|
-
function hook_launch_command(bundle: string, command: string): string {
|
|
45
|
-
return bundle_launch_command(bundle, `pnpm josh ${command}`)
|
|
31
|
+
// The josh subcommand the launcher falls back to for hook `name`: its first `-` becomes `:`
|
|
32
|
+
// (`pretool-guard` → `pretool:guard`). The launcher derives it in shell; this is that rule for the tests
|
|
33
|
+
// that hold every wired hook to a subcommand josh has.
|
|
34
|
+
function fallback_command(name: string): string {
|
|
35
|
+
return name.replace('-', ':')
|
|
46
36
|
}
|
|
47
37
|
|
|
48
38
|
const hook_launch = {
|
|
49
|
-
|
|
50
|
-
bundle_launch_command,
|
|
39
|
+
fallback_command,
|
|
51
40
|
HOOK_DIST_DIR,
|
|
52
41
|
hook_launch_command,
|
|
53
|
-
launch_command,
|
|
54
42
|
PROJECT_ROOT_PREFIX,
|
|
43
|
+
RUN_HOOK_SCRIPT,
|
|
55
44
|
}
|
|
56
45
|
|
|
57
46
|
export { hook_launch }
|
|
@@ -0,0 +1,95 @@
|
|
|
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/run-carry'
|
|
4
|
+
import { issue_file } from './issue-file'
|
|
5
|
+
import { AUTO_OK_LABEL, has_label_name } 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 (joshuafolkken/kit#3213). Work
|
|
9
|
+
// found during an unattended run used to wait in the backlog until a person labelled it, so the
|
|
10
|
+
// filing now computes the 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
|
+
|
|
16
|
+
interface AutoOkSignals {
|
|
17
|
+
is_opted_out: boolean
|
|
18
|
+
is_carried: boolean
|
|
19
|
+
// The labels of the Issue the current branch names; `undefined` when the branch names none or the
|
|
20
|
+
// Issue could not be read.
|
|
21
|
+
branch_labels: ReadonlyArray<string> | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface AutoOkDecision {
|
|
25
|
+
is_applied: boolean
|
|
26
|
+
reason: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const OPTED_OUT: AutoOkDecision = { is_applied: false, reason: '--no-auto-ok declared' }
|
|
30
|
+
const CROSS_REPOSITORY: AutoOkDecision = {
|
|
31
|
+
is_applied: false,
|
|
32
|
+
reason: 'the Issue is filed to another repository',
|
|
33
|
+
}
|
|
34
|
+
const CARRIED: AutoOkDecision = { is_applied: true, reason: 'a backlogrun carry record is live' }
|
|
35
|
+
const BRANCH_OPTED_IN: AutoOkDecision = {
|
|
36
|
+
is_applied: true,
|
|
37
|
+
reason: 'the Issue this branch works on carries auto-ok',
|
|
38
|
+
}
|
|
39
|
+
const NOT_OPTED_IN: AutoOkDecision = {
|
|
40
|
+
is_applied: false,
|
|
41
|
+
reason: 'no backlogrun carry record is live and the branch names no auto-ok Issue',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The opt-out wins over every signal, then the carry record, then the branch's Issue.
|
|
45
|
+
function decide(signals: AutoOkSignals): AutoOkDecision {
|
|
46
|
+
if (signals.is_opted_out) return OPTED_OUT
|
|
47
|
+
if (signals.is_carried) return CARRIED
|
|
48
|
+
if (has_label_name(signals.branch_labels ?? [], AUTO_OK_LABEL)) return BRANCH_OPTED_IN
|
|
49
|
+
|
|
50
|
+
return NOT_OPTED_IN
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function line_of(decision: AutoOkDecision): string {
|
|
54
|
+
return `auto-ok: ${decision.is_applied ? 'applied' : 'not applied'} — ${decision.reason}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Read from the common git directory, so a lane sees the record its batch wrote in the main tree.
|
|
58
|
+
async function is_carried(): Promise<boolean> {
|
|
59
|
+
const directory = await run_carry.repository_directory()
|
|
60
|
+
|
|
61
|
+
if (directory === undefined) return false
|
|
62
|
+
|
|
63
|
+
return run_carry.read_carry(run_carry.carry_path(directory)).kind === 'carried'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// An unreadable Issue is read as no labels: the filing goes on without `auto-ok` rather than failing.
|
|
67
|
+
// The branch names an Issue of `current`, read there by name: a filing to another repository has
|
|
68
|
+
// already pointed `GH_REPO` at its target, which would otherwise answer with the target's Issue.
|
|
69
|
+
async function branch_labels(current: string): Promise<ReadonlyArray<string> | undefined> {
|
|
70
|
+
const issue_number = git_branch.issue_from_branch(await git_command.branch())
|
|
71
|
+
|
|
72
|
+
if (issue_number === undefined) return undefined
|
|
73
|
+
const read = await issue_state_cli.read_issue(String(issue_number), current)
|
|
74
|
+
|
|
75
|
+
return read.kind === 'state' ? read.state.labels : undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Each read is skipped once an earlier signal has already decided the answer; a cross-repository
|
|
79
|
+
// filing is refused before any signal is read.
|
|
80
|
+
async function resolve(
|
|
81
|
+
is_opted_out: boolean,
|
|
82
|
+
target: string,
|
|
83
|
+
current: string,
|
|
84
|
+
): Promise<AutoOkDecision> {
|
|
85
|
+
if (is_opted_out) return OPTED_OUT
|
|
86
|
+
if (!issue_file.is_same_repository(target, current)) return CROSS_REPOSITORY
|
|
87
|
+
if (await is_carried()) return CARRIED
|
|
88
|
+
|
|
89
|
+
return decide({ is_opted_out, is_carried: false, branch_labels: await branch_labels(current) })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const issue_auto_ok = { decide, line_of, resolve }
|
|
93
|
+
|
|
94
|
+
export type { AutoOkDecision, AutoOkSignals }
|
|
95
|
+
export { issue_auto_ok }
|
|
@@ -5,24 +5,30 @@ import { repo_party } from '#scripts/discovery/repo-party'
|
|
|
5
5
|
import { epic_bundle_cli } from '#scripts/epic/epic-bundle-cli'
|
|
6
6
|
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
7
7
|
import { git_gh_exec } from '#scripts/gh/git-gh-exec'
|
|
8
|
+
import { git_gh_issue_list, MAX_SCANNED } from '#scripts/gh/git-gh-issue-list'
|
|
8
9
|
import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
|
|
9
10
|
import { github_issue_url } from '#scripts/gh/github-issue-url'
|
|
10
11
|
import { error_text } from '#scripts/lib/error-message'
|
|
12
|
+
import { repository_labels } from '#scripts/repo/repository-labels'
|
|
13
|
+
import { issue_auto_ok } from './issue-auto-ok'
|
|
11
14
|
import { issue_file, type FileArguments } from './issue-file'
|
|
12
15
|
import { issue_lint_cli } from './issue-lint-cli'
|
|
13
16
|
import { issue_scout_cli } from './issue-scout-cli'
|
|
17
|
+
import { issue_wip } from './issue-wip'
|
|
14
18
|
|
|
15
19
|
// `josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>]…
|
|
16
|
-
// [--repo <owner/repo>] [--distinct <N,…>]` — file an Issue with every
|
|
17
|
-
// (joshuafolkken/kit#2808): the third-party refusal, the body lint, the
|
|
18
|
-
//
|
|
20
|
+
// [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok]` — file an Issue with every
|
|
21
|
+
// filing step run in order (joshuafolkken/kit#2808): the third-party refusal, the body lint, the
|
|
22
|
+
// `## Origin` check for another repository, the WIP cap count (joshuafolkken/kit#3181), the duplicate
|
|
23
|
+
// scout, the missing workflow labels created (joshuafolkken/kit#3176), the `auto-ok` decision
|
|
24
|
+
// (joshuafolkken/kit#3213), the create call carrying every label, and `epic:bundle` after it.
|
|
19
25
|
// A direct `gh api …/issues` filing is refused by the `direct-filing` delivered rule and pointed here.
|
|
20
26
|
|
|
21
27
|
const SUCCESS_EXIT_CODE = 0
|
|
22
28
|
const FAILURE_EXIT_CODE = 1
|
|
23
29
|
const ARGV_OFFSET = 2
|
|
24
30
|
const USAGE =
|
|
25
|
-
'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,…>]'
|
|
31
|
+
'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]'
|
|
26
32
|
const UNKNOWN_REPO_MESSAGE =
|
|
27
33
|
'Could not read this repository from `git remote`, so the filing has no repository to compare against — check `gh auth status`.'
|
|
28
34
|
const THIRD_PARTY_MESSAGE =
|
|
@@ -68,6 +74,37 @@ function unacknowledged_message(numbers: ReadonlyArray<number>): string {
|
|
|
68
74
|
return `✖ duplicate candidate(s) ${listed} not declared separate — read each (\`issue-fold-existing.md\`); fold into a duplicate, or reissue with \`--distinct ${listed}\` when each is a separate deliverable.`
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
// The target's open Issues, pull requests excluded by the listing itself; `undefined` when unreadable.
|
|
78
|
+
async function open_count(target: string): Promise<number | undefined> {
|
|
79
|
+
const outcome = await git_gh_issue_list.issue_list({
|
|
80
|
+
json_fields: 'number',
|
|
81
|
+
limit: MAX_SCANNED,
|
|
82
|
+
repo: target,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return outcome.json === undefined ? undefined : issue_wip.count_of(outcome.json)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The count is printed whatever it answers, and the filing is held only past the cap with no
|
|
89
|
+
// exemption declared. An unreadable count warns and goes on: the cap makes growth visible, and a
|
|
90
|
+
// listing outage is not a reason to stop the run a blocked filing exists for.
|
|
91
|
+
async function is_wip_clear(filing: Filing): Promise<boolean> {
|
|
92
|
+
const count = await open_count(filing.target)
|
|
93
|
+
|
|
94
|
+
if (count === undefined) {
|
|
95
|
+
console.error(issue_wip.UNREAD_MESSAGE)
|
|
96
|
+
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const verdict = issue_wip.verdict_of(count, filing.args)
|
|
101
|
+
|
|
102
|
+
console.info(issue_wip.count_line(count, filing.target, verdict))
|
|
103
|
+
if (verdict === issue_wip.HELD) console.error(issue_wip.HELD_MESSAGE)
|
|
104
|
+
|
|
105
|
+
return verdict !== issue_wip.HELD
|
|
106
|
+
}
|
|
107
|
+
|
|
71
108
|
// Set before the scout and left set for `epic:bundle`, so both read the target's backlog.
|
|
72
109
|
function point_listings_at(filing: Filing): void {
|
|
73
110
|
if (issue_file.is_same_repository(filing.target, filing.current)) return
|
|
@@ -94,12 +131,21 @@ async function is_scout_clear(filing: Filing): Promise<boolean> {
|
|
|
94
131
|
}
|
|
95
132
|
|
|
96
133
|
// `undefined` when the create call failed. `gh`'s standard error is captured into the thrown error
|
|
97
|
-
// rather than printed, so it is printed here — otherwise the filing fails with no reason given.
|
|
134
|
+
// rather than printed, so it is printed here — otherwise the filing fails with no reason given. A label
|
|
135
|
+
// the create applies is provisioned first, so it never arrives with a generated color.
|
|
98
136
|
async function create(filing: Filing): Promise<string | undefined> {
|
|
137
|
+
repository_labels.ensure_labels(filing.target)
|
|
138
|
+
const auto_ok = await issue_auto_ok.resolve(
|
|
139
|
+
filing.args.is_auto_ok_opted_out,
|
|
140
|
+
filing.target,
|
|
141
|
+
filing.current,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
console.info(issue_auto_ok.line_of(auto_ok))
|
|
99
145
|
const request = git_gh_issue_write.issue_create_request({
|
|
100
146
|
title: filing.args.title,
|
|
101
147
|
body: filing.body,
|
|
102
|
-
labels: issue_file.labels_of(filing.args, filing.body),
|
|
148
|
+
labels: issue_file.labels_of(filing.args, filing.body, auto_ok.is_applied),
|
|
103
149
|
repo: filing.target,
|
|
104
150
|
})
|
|
105
151
|
|
|
@@ -136,7 +182,7 @@ async function file(filing: Filing): Promise<number> {
|
|
|
136
182
|
return FAILURE_EXIT_CODE
|
|
137
183
|
}
|
|
138
184
|
|
|
139
|
-
if (!(await is_scout_clear(filing))) return FAILURE_EXIT_CODE
|
|
185
|
+
if (!(await is_wip_clear(filing)) || !(await is_scout_clear(filing))) return FAILURE_EXIT_CODE
|
|
140
186
|
const url = await create(filing)
|
|
141
187
|
|
|
142
188
|
if (url === undefined) return FAILURE_EXIT_CODE
|