@joshuafolkken/kit 1.1387.0 → 1.1453.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
- package/.claude/skills/workflow-commands/backlogrun.md +3 -2
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +1 -1
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/kickoff.md +12 -13
- package/.claude/skills/workflow-commands/observation-filing.md +1 -1
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/.claude/skills/workflow-commands/prerequisite.md +6 -0
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/README.md +27 -4
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +261 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +95 -19
- package/package.json +4 -3
- package/prompts/collaboration-workflow/issue-template.md +10 -8
- package/prompts/review.md +30 -4
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- 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-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +38 -11
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +175 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +18 -4
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +13 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +7 -4
- package/scripts/init/init-ai-copy.ts +34 -11
- package/scripts/init/init-basic.ts +9 -4
- 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-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +62 -42
- package/scripts/init/kit-setup-state.ts +11 -17
- package/scripts/init/own-formatter.ts +21 -0
- package/scripts/init/project-profile.ts +60 -12
- package/scripts/init/start-dry-run.ts +70 -0
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +23 -3
- package/scripts/init/start-setup-pr.ts +13 -24
- package/scripts/init/start-steps.ts +62 -1
- package/scripts/init/start.ts +38 -17
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/init/working-tree.ts +116 -0
- package/scripts/issue/issue-ack-cli.ts +84 -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 +46 -10
- package/scripts/issue/issue-release-cli.ts +69 -23
- package/scripts/issue/issue-release.ts +3 -1
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-ai.ts +11 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +10 -3
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +26 -2
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/josh/process-identity.ts +32 -6
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-broker-server.ts +69 -0
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/lane-session-broker.ts +363 -0
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- 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-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +24 -7
- package/scripts/review/review-code-tree.ts +119 -0
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- 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/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/lane/openai-review-broker.ts +0 -262
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -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
|
'',
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
4
|
+
import type { IssueReadReason } from '#scripts/gh/git-gh-issue-read'
|
|
4
5
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
5
6
|
import { bounded_pool } from '#scripts/lib/bounded-pool'
|
|
6
7
|
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
7
|
-
import { issue_report_failures
|
|
8
|
+
import { issue_report_failures } from './issue-report-failures'
|
|
8
9
|
import { issue_state, type IssueState } from './issue-state'
|
|
9
10
|
|
|
10
11
|
// `josh issue:state <N> [<N> ...]` — print each issue's state and labels, in the spelling the
|
|
@@ -50,7 +51,14 @@ interface StateRequest {
|
|
|
50
51
|
// One issue's state, or the failure kind. Named for export because `run:prep` reads the state beside
|
|
51
52
|
// the body and the dependency-update scope, reusing this read rather than a
|
|
52
53
|
// second one that would drift from the `human_review` decision `parse_issue_state` owns.
|
|
53
|
-
|
|
54
|
+
//
|
|
55
|
+
// **Why a read failed rides along where the request said it**: `backlog:drive` names the reason when it
|
|
56
|
+
// hands an unreadable named issue back. Both are optional, so a caller that only separates the kinds
|
|
57
|
+
// needs no edit.
|
|
58
|
+
type StateRead =
|
|
59
|
+
| { kind: 'state'; state: IssueState }
|
|
60
|
+
| { kind: 'missing' }
|
|
61
|
+
| { kind: 'unreadable'; reason?: IssueReadReason | undefined; status?: number | undefined }
|
|
54
62
|
|
|
55
63
|
interface IssueReport {
|
|
56
64
|
issue_number: string
|
|
@@ -102,11 +110,13 @@ function parse_request(argv: ReadonlyArray<string>): StateRequest | undefined {
|
|
|
102
110
|
async function read_issue(issue_number: string, repo?: string): Promise<StateRead> {
|
|
103
111
|
const read = await git_gh_command.issue_view_json_classified(issue_number, STATE_FIELDS, repo)
|
|
104
112
|
|
|
105
|
-
if (read.kind !== 'read') return
|
|
113
|
+
if (read.kind !== 'read') return read
|
|
106
114
|
|
|
107
115
|
const parsed = issue_state.parse_issue_state(read.json)
|
|
108
116
|
|
|
109
|
-
return parsed === undefined
|
|
117
|
+
return parsed === undefined
|
|
118
|
+
? { kind: 'unreadable', reason: 'malformed' }
|
|
119
|
+
: { kind: 'state', state: parsed }
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
// Through the shared pool rather than a raw `Promise.all`: each read spawns a `gh` process, and a
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { read_json_listing } from '#scripts/git/parse-json-array'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
// What a person changed on an issue since a run last read it, as the one pure answer the hook, the
|
|
5
|
+
// review attestation, `josh issue:ack` and `josh followup` all print. Kept free of I/O so the four
|
|
6
|
+
// callers cannot each draw the line between "a person's update" and "noise" differently.
|
|
7
|
+
//
|
|
8
|
+
// **A person's update is a body edit or a comment nobody automated.** A bot's comment (`type: Bot`,
|
|
9
|
+
// or a `[bot]` login) is not one. A run's own comment is posted under the person's own `gh` login,
|
|
10
|
+
// so the login cannot tell the two apart; `git-gh-issue-write.ts` stamps every comment josh posts
|
|
11
|
+
// with `RUN_MARKER`, and a marked comment is not one either. An unmarked comment written by hand
|
|
12
|
+
// through a raw `gh api` counts as a person's — the direction that costs one `josh issue:ack`, never a
|
|
13
|
+
// missed correction.
|
|
14
|
+
//
|
|
15
|
+
// **Comment edits are out of scope.** A new comment carries an id above the baseline's; an edited
|
|
16
|
+
// one keeps its id and is not seen. The body is compared whole, so its edits are.
|
|
17
|
+
|
|
18
|
+
const RUN_MARKER = '<!-- josh:run -->'
|
|
19
|
+
const BOT_TYPE = 'Bot'
|
|
20
|
+
const BOT_SUFFIX = '[bot]'
|
|
21
|
+
const NO_COMMENT_ID = 0
|
|
22
|
+
const UNKNOWN_AUTHOR = '(unknown)'
|
|
23
|
+
const UNKNOWN_TIME = '(undated)'
|
|
24
|
+
|
|
25
|
+
const BODY_HEADING = 'Body edited:'
|
|
26
|
+
const REMOVED_PREFIX = '- '
|
|
27
|
+
const ADDED_PREFIX = '+ '
|
|
28
|
+
const COMMENT_HEADING = 'New comment by '
|
|
29
|
+
const AT = ' at '
|
|
30
|
+
const SECTION_SEPARATOR = '\n\n'
|
|
31
|
+
const LINE_SEPARATOR = '\n'
|
|
32
|
+
const CRLF = '\r\n'
|
|
33
|
+
|
|
34
|
+
const comment_schema = z.looseObject({
|
|
35
|
+
id: z.number(),
|
|
36
|
+
user: z.looseObject({ login: z.string(), type: z.string().optional() }).nullish(),
|
|
37
|
+
created_at: z.string().optional(),
|
|
38
|
+
body: z.string().nullish(),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
interface WatchComment {
|
|
42
|
+
id: number
|
|
43
|
+
author: string
|
|
44
|
+
is_automated: boolean
|
|
45
|
+
created_at: string
|
|
46
|
+
body: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface WatchSnapshot {
|
|
50
|
+
body: string
|
|
51
|
+
comments: ReadonlyArray<WatchComment>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface WatchBaseline {
|
|
55
|
+
body: string
|
|
56
|
+
last_comment_id: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function is_bot(user: z.infer<typeof comment_schema>['user']): boolean {
|
|
60
|
+
return user?.type === BOT_TYPE || (user?.login.endsWith(BOT_SUFFIX) ?? false)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function is_automated(user: z.infer<typeof comment_schema>['user'], body: string): boolean {
|
|
64
|
+
return is_bot(user) || body.includes(RUN_MARKER)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function to_comment(raw: z.infer<typeof comment_schema>): WatchComment {
|
|
68
|
+
const body = raw.body ?? ''
|
|
69
|
+
const author = raw.user?.login ?? UNKNOWN_AUTHOR
|
|
70
|
+
const created_at = raw.created_at ?? UNKNOWN_TIME
|
|
71
|
+
|
|
72
|
+
return { id: raw.id, author, is_automated: is_automated(raw.user, body), created_at, body }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// `undefined` — never `[]` — for a listing that could not be read, so a failed read is never taken
|
|
76
|
+
// for "nobody commented".
|
|
77
|
+
function parse_comments(json: string | undefined): ReadonlyArray<WatchComment> | undefined {
|
|
78
|
+
if (json === undefined) return undefined
|
|
79
|
+
|
|
80
|
+
const listing = read_json_listing(json, comment_schema)
|
|
81
|
+
|
|
82
|
+
return listing.kind === 'read' ? listing.rows.map((row) => to_comment(row)) : undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function last_comment_id(comments: ReadonlyArray<WatchComment>): number {
|
|
86
|
+
return Math.max(NO_COMMENT_ID, ...comments.map((comment) => comment.id))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function baseline_of(snapshot: WatchSnapshot): WatchBaseline {
|
|
90
|
+
return { body: snapshot.body, last_comment_id: last_comment_id(snapshot.comments) }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function lines_missing_from(source: string, other: string): Array<string> {
|
|
94
|
+
const present = new Set(other.split(LINE_SEPARATOR))
|
|
95
|
+
|
|
96
|
+
return source.split(LINE_SEPARATOR).filter((line) => line.trim() !== '' && !present.has(line))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// GitHub stores a body saved in its web editor with CRLF line endings, so one edited line to a body
|
|
100
|
+
// josh wrote over the API with LF would otherwise read as every line removed and added.
|
|
101
|
+
function to_lf(body: string): string {
|
|
102
|
+
return body.split(CRLF).join(LINE_SEPARATOR)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The lines that left and the lines that arrived — not a positional diff, which would need a
|
|
106
|
+
// dependency in a hook that runs on every tool call, and a reader comparing a spec only needs which
|
|
107
|
+
// sentences changed.
|
|
108
|
+
function body_section(previous_raw: string, current_raw: string): string | undefined {
|
|
109
|
+
const previous = to_lf(previous_raw)
|
|
110
|
+
const current = to_lf(current_raw)
|
|
111
|
+
|
|
112
|
+
if (previous === current) return undefined
|
|
113
|
+
|
|
114
|
+
const removed = lines_missing_from(previous, current).map((line) => `${REMOVED_PREFIX}${line}`)
|
|
115
|
+
const added = lines_missing_from(current, previous).map((line) => `${ADDED_PREFIX}${line}`)
|
|
116
|
+
|
|
117
|
+
return [BODY_HEADING, ...removed, ...added].join(LINE_SEPARATOR)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function comment_section(comment: WatchComment): string {
|
|
121
|
+
return `${COMMENT_HEADING}${comment.author}${AT}${comment.created_at}${LINE_SEPARATOR}${comment.body}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function new_person_comments(
|
|
125
|
+
baseline: WatchBaseline,
|
|
126
|
+
snapshot: WatchSnapshot,
|
|
127
|
+
): ReadonlyArray<WatchComment> {
|
|
128
|
+
return snapshot.comments.filter(
|
|
129
|
+
(comment) => comment.id > baseline.last_comment_id && !comment.is_automated,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The unread update as the text a run is shown, or `undefined` when a person changed nothing.
|
|
134
|
+
function unread_diff(baseline: WatchBaseline, snapshot: WatchSnapshot): string | undefined {
|
|
135
|
+
const body = body_section(baseline.body, snapshot.body)
|
|
136
|
+
const comments = new_person_comments(baseline, snapshot).map((comment) =>
|
|
137
|
+
comment_section(comment),
|
|
138
|
+
)
|
|
139
|
+
const sections = [...(body === undefined ? [] : [body]), ...comments]
|
|
140
|
+
|
|
141
|
+
return sections.length === 0 ? undefined : sections.join(SECTION_SEPARATOR)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const issue_watch_diff = {
|
|
145
|
+
RUN_MARKER,
|
|
146
|
+
baseline_of,
|
|
147
|
+
parse_comments,
|
|
148
|
+
unread_diff,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type { WatchBaseline, WatchComment, WatchSnapshot }
|
|
152
|
+
export { issue_watch_diff }
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { git_command } from '#scripts/git/git-command'
|
|
2
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
3
|
+
import { review_attest } from '#scripts/review/review-attest'
|
|
4
|
+
import { issue_watch, type WatchRecord } from './issue-watch'
|
|
5
|
+
|
|
6
|
+
// The refusals a person's unread issue update raises, composed once so the hook, `josh review:attest
|
|
7
|
+
// --check` and `josh followup` say the same thing about the same state.
|
|
8
|
+
//
|
|
9
|
+
// **The gates poll on every call** (`interval_ms: 0`): they run a handful of times per run, and the
|
|
10
|
+
// poll is a conditional request that a 304 answers for free. Only the hook, which runs on every tool
|
|
11
|
+
// call, spaces its polls out.
|
|
12
|
+
|
|
13
|
+
const EVERY_CALL_MS = 0
|
|
14
|
+
|
|
15
|
+
function ack_command(issue: string): string {
|
|
16
|
+
return `pnpm josh issue:ack ${issue}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function unread_message(record: Pick<WatchRecord, 'issue' | 'pending'>): string {
|
|
20
|
+
return [
|
|
21
|
+
`A person updated issue ${session_cite.issue(record.issue)} after this run read it, and the run has not taken the update in:`,
|
|
22
|
+
record.pending ?? '',
|
|
23
|
+
`Read it, compare it with the acceptance criteria, fix what it changes, then run \`${ack_command(record.issue)}\` to record that it was read.`,
|
|
24
|
+
].join('\n\n')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function review_message(record: WatchRecord, briefed_at: string): string {
|
|
28
|
+
return [
|
|
29
|
+
`Issue ${session_cite.issue(record.issue)} was updated after the review was briefed (taken in at ${record.acknowledged_at}, briefed at ${briefed_at}), so the review read the earlier specification.`,
|
|
30
|
+
'Re-run `/code-review` with a fresh `pnpm josh review:brief`, so the review reads the change against the updated issue.',
|
|
31
|
+
].join('\n')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function polled(root: string): Promise<WatchRecord | undefined> {
|
|
35
|
+
return await issue_watch.poll({ interval_ms: EVERY_CALL_MS }, root)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The merge-time refusal: a person's update the run has not acknowledged. `undefined` when there is
|
|
39
|
+
// none, when no run recorded a baseline in this checkout, or when the issue could not be read — an
|
|
40
|
+
// unreachable GitHub does not hold a merge `josh followup` would otherwise make.
|
|
41
|
+
async function unread_refusal(root: string): Promise<string | undefined> {
|
|
42
|
+
const record = await polled(root)
|
|
43
|
+
|
|
44
|
+
return record?.pending === undefined ? undefined : unread_message(record)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The review-time refusal: an unread update, or one taken in after the review was briefed — the fix
|
|
48
|
+
// it led to is then a change the review never saw.
|
|
49
|
+
async function review_refusal(root: string): Promise<string | undefined> {
|
|
50
|
+
const record = await polled(root)
|
|
51
|
+
|
|
52
|
+
if (record === undefined) return undefined
|
|
53
|
+
if (record.pending !== undefined) return unread_message(record)
|
|
54
|
+
|
|
55
|
+
const pointer = review_attest.read_pointer(root)
|
|
56
|
+
|
|
57
|
+
if (pointer === undefined || record.acknowledged_at <= pointer.taken_at) return undefined
|
|
58
|
+
|
|
59
|
+
return review_message(record, pointer.taken_at)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function review_refusal_here(): Promise<string | undefined> {
|
|
63
|
+
return await review_refusal(await git_command.repository_root())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Throws the refusal, for the `josh followup` seams that stop by throwing.
|
|
67
|
+
async function assert_read_here(): Promise<void> {
|
|
68
|
+
const refusal = await unread_refusal(await git_command.repository_root())
|
|
69
|
+
|
|
70
|
+
if (refusal !== undefined) throw new Error(refusal)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const issue_watch_gate = {
|
|
74
|
+
assert_read_here,
|
|
75
|
+
review_refusal,
|
|
76
|
+
review_refusal_here,
|
|
77
|
+
unread_message,
|
|
78
|
+
unread_refusal,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { issue_watch_gate }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
2
|
+
import { issue_watch, type WatchRecord } from './issue-watch'
|
|
3
|
+
import { issue_watch_gate } from './issue-watch-gate'
|
|
4
|
+
|
|
5
|
+
// The `PreToolUse` adapter that shows a run a person's update to its issue on the next tool call,
|
|
6
|
+
// composed into `pretool-guard.ts` — the one PreToolUse hook — rather than added as a second
|
|
7
|
+
// `.claude/settings.json` entry.
|
|
8
|
+
//
|
|
9
|
+
// **It polls at most once per interval.** Every tool call reads the checkout's record from disk; only
|
|
10
|
+
// one whose last poll is older than `POLL_INTERVAL_MS` asks GitHub, with the ETag of the previous
|
|
11
|
+
// answer, so an unchanged issue costs a 304. The request is bounded well under the hook's own timeout.
|
|
12
|
+
//
|
|
13
|
+
// **It refuses once per update.** The refusal is what puts the update in front of the run; repeating
|
|
14
|
+
// it on every call would wedge the run before it could act. A further update changes the pending text
|
|
15
|
+
// and is shown again. `josh issue:ack` clears it, and the review attestation and `josh followup` keep
|
|
16
|
+
// refusing until it does — the hook is the early warning, not the enforcement.
|
|
17
|
+
//
|
|
18
|
+
// **It fails open.** No record, an unreadable issue or a thrown error allows the call: a hook that
|
|
19
|
+
// stalls every tool call over a network blip would cost more than the update it watches for.
|
|
20
|
+
|
|
21
|
+
const POLL_INTERVAL_MS = 180_000
|
|
22
|
+
const POLL_TIMEOUT_MS = 5000
|
|
23
|
+
|
|
24
|
+
function is_new_to_run(record: WatchRecord | undefined): record is WatchRecord {
|
|
25
|
+
return record?.pending !== undefined && record.pending !== record.shown
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function new_update_reason(now: Date): Promise<string | undefined> {
|
|
29
|
+
const record = await issue_watch.poll({
|
|
30
|
+
interval_ms: POLL_INTERVAL_MS,
|
|
31
|
+
timeout_ms: POLL_TIMEOUT_MS,
|
|
32
|
+
now,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (!is_new_to_run(record)) return undefined
|
|
36
|
+
|
|
37
|
+
issue_watch.mark_shown(record)
|
|
38
|
+
|
|
39
|
+
return issue_watch_gate.unread_message(record)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function watch_reason(now: Date = new Date()): Promise<string | undefined> {
|
|
43
|
+
try {
|
|
44
|
+
return await new_update_reason(now)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
error_text.trace_swallowed('issue_watch_hook.watch_reason', error)
|
|
47
|
+
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const issue_watch_hook = { POLL_INTERVAL_MS, watch_reason }
|
|
53
|
+
|
|
54
|
+
export { issue_watch_hook }
|