@joshuafolkken/kit 1.1383.0 → 1.1435.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-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
- package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
- package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
- package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
- package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
- 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 +262 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +124 -15
- package/dist/hooks/stop-guard.js +98 -55
- package/dist/josh.js +63 -10
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- 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/backlog/backlog-drive-cli.ts +2 -4
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +5 -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-owner.ts +72 -5
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- 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 +80 -5
- 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 +130 -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 +13 -2
- 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-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -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 +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +30 -4
- package/scripts/init/start-steps.ts +77 -3
- package/scripts/init/start.ts +21 -13
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- 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-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +25 -1
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- 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/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- 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/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- 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 +139 -37
- 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 +58 -5
- 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 +121 -0
- package/scripts/run/board/run-board-span.ts +49 -7
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +76 -13
- 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 +53 -16
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-added.ts +1 -1
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-change.ts +1 -1
- package/scripts/run/carry/run-carry-cli.ts +39 -52
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry-ended.ts +9 -1
- package/scripts/run/carry/run-carry.ts +24 -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 +3 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +91 -0
- package/scripts/run/event/run-event-stream.ts +46 -9
- 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/run-stranded-detect.ts +58 -12
- 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 +108 -74
- package/scripts/run/wake/run-wake-describe.ts +36 -2
- package/scripts/run/wake/run-wake-driver.ts +21 -5
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
- 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/run/board/run-board-track.ts +0 -22
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
2
|
+
|
|
3
|
+
// `josh start` sets a project up for the GitHub Issue workflow and `josh init` alone does not. The
|
|
4
|
+
// difference decides whether a `basic` project receives `.claude/settings.json`, whose permissions,
|
|
5
|
+
// hooks and plugin an unattended `backlogrun` agent runs on (#3796).
|
|
6
|
+
//
|
|
7
|
+
// `josh start` hands the mark to the setup it runs through this variable, not an argument: that setup
|
|
8
|
+
// may be the project's own kit it hands off to or a toolkit's command (`--init-command`), and an older
|
|
9
|
+
// one refuses an argument it does not know but ignores a variable. The setup records the mark in
|
|
10
|
+
// `package.json`, where every later `josh init` and `josh sync` reads it.
|
|
11
|
+
const WORKFLOW_ENV = 'JOSH_WORKFLOW'
|
|
12
|
+
const GITHUB_WORKFLOW = 'github'
|
|
13
|
+
|
|
14
|
+
function is_requested(environment: NodeJS.ProcessEnv = process.env): boolean {
|
|
15
|
+
return environment[WORKFLOW_ENV] === GITHUB_WORKFLOW
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function is_recorded(manifest: Record<string, unknown> | undefined): boolean {
|
|
19
|
+
const josh = manifest?.['josh']
|
|
20
|
+
|
|
21
|
+
return json_value.is_record(josh) && josh['workflow'] === GITHUB_WORKFLOW
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function request(environment: NodeJS.ProcessEnv = process.env): void {
|
|
25
|
+
environment[WORKFLOW_ENV] = GITHUB_WORKFLOW
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The `josh` record's field for the mark — none for a project `josh start` did not set up, so a
|
|
29
|
+
// `josh init` never writes the mark of its own accord.
|
|
30
|
+
function recorded_fields(has_workflow: boolean): Record<string, string> {
|
|
31
|
+
return has_workflow ? { workflow: GITHUB_WORKFLOW } : {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const workflow_mark = { is_requested, is_recorded, request, recorded_fields }
|
|
35
|
+
export { workflow_mark }
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { git_command } from '#scripts/git/git-command'
|
|
4
|
+
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
5
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
6
|
+
import { issue_watch, type Acknowledged } from './issue-watch'
|
|
7
|
+
|
|
8
|
+
// `josh issue:ack <N>` — record that the run has read a person's update to its issue, so the hook,
|
|
9
|
+
// the review attestation and `josh followup` stop raising it. It prints the update it acknowledges:
|
|
10
|
+
// the same read becomes the new baseline (`issue-watch.ts` → `acknowledge`), so nothing that arrived
|
|
11
|
+
// after the hook spoke can be acknowledged without being printed here.
|
|
12
|
+
//
|
|
13
|
+
// **The number is asked for, though the record already carries it**, so an acknowledgement typed for
|
|
14
|
+
// one issue cannot silently clear the watch on another.
|
|
15
|
+
|
|
16
|
+
const SUCCESS_EXIT_CODE = 0
|
|
17
|
+
const FAILURE_EXIT_CODE = 1
|
|
18
|
+
const ARGV_OFFSET = 2
|
|
19
|
+
const USAGE = 'Usage: josh issue:ack <issue-number>'
|
|
20
|
+
const NO_WATCH_LINE =
|
|
21
|
+
'No run recorded a baseline for that issue in this checkout (`josh run:entry` writes it), or the issue could not be read — nothing was acknowledged.'
|
|
22
|
+
const NOTHING_NEW_LINE = 'No person has updated the issue since the run last read it.'
|
|
23
|
+
|
|
24
|
+
function parse_number(argv: ReadonlyArray<string>): string | undefined {
|
|
25
|
+
const [first] = argv
|
|
26
|
+
|
|
27
|
+
if (first === undefined || argv.length !== 1) return undefined
|
|
28
|
+
|
|
29
|
+
return issue_number_shape.ISSUE_NUMBER_PATTERN.test(first) ? first : undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function report(acknowledged: Acknowledged): void {
|
|
33
|
+
const heading = `Acknowledged issue ${session_cite.issue(acknowledged.issue)}.`
|
|
34
|
+
|
|
35
|
+
console.info(
|
|
36
|
+
acknowledged.diff === undefined
|
|
37
|
+
? `${heading} ${NOTHING_NEW_LINE}`
|
|
38
|
+
: `${heading}\n\n${acknowledged.diff}`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function acknowledge(issue_number: string): Promise<number> {
|
|
43
|
+
const root = await git_command.repository_root()
|
|
44
|
+
|
|
45
|
+
if (issue_watch.read_record(root)?.issue !== issue_number) {
|
|
46
|
+
console.error(NO_WATCH_LINE)
|
|
47
|
+
|
|
48
|
+
return FAILURE_EXIT_CODE
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const acknowledged = await issue_watch.acknowledge(root)
|
|
52
|
+
|
|
53
|
+
if (acknowledged === undefined) {
|
|
54
|
+
console.error(NO_WATCH_LINE)
|
|
55
|
+
|
|
56
|
+
return FAILURE_EXIT_CODE
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
report(acknowledged)
|
|
60
|
+
|
|
61
|
+
return SUCCESS_EXIT_CODE
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
65
|
+
const issue_number = parse_number(argv)
|
|
66
|
+
|
|
67
|
+
if (issue_number === undefined) {
|
|
68
|
+
console.error(USAGE)
|
|
69
|
+
|
|
70
|
+
return FAILURE_EXIT_CODE
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return await acknowledge(issue_number)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
77
|
+
process.exitCode = await run(argv)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const issue_ack_cli = { run }
|
|
81
|
+
|
|
82
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
|
|
83
|
+
|
|
84
|
+
export { issue_ack_cli }
|
|
@@ -70,6 +70,9 @@ const ALREADY_DONE_LABEL = 'already-done'
|
|
|
70
70
|
// it; **`auto-ok` never rides on it** — `issue-auto-ok.ts` refuses the default for it on every path,
|
|
71
71
|
// since a release is Tier C — and only a person opts it into unattended execution.
|
|
72
72
|
const RELEASE_LABEL = 'release'
|
|
73
|
+
// Applied to an Issue whose second review round was skipped, so the condition stays auditable
|
|
74
|
+
// (`prompts/review.md` → "When round 2 is skipped entirely, and when it is not").
|
|
75
|
+
const REVIEW_ROUND2_SKIPPED_LABEL = 'review-round2-skipped'
|
|
73
76
|
|
|
74
77
|
// The four labels that mean an open issue must not be handed to a run as it stands: an `epic`
|
|
75
78
|
// tracks a batch and is never run directly (its children are), `in-progress` is already claimed by
|
|
@@ -205,8 +208,11 @@ interface LabelDefinition {
|
|
|
205
208
|
}
|
|
206
209
|
|
|
207
210
|
// What `josh start` provisions on a new repository so the first `kickoff` finds every label a run
|
|
208
|
-
// applies with its intended color. The
|
|
209
|
-
//
|
|
211
|
+
// applies with its intended color. The metadata is the one the workflow documents create each label
|
|
212
|
+
// with. **The labels only a person applies are provisioned too**: a person cannot apply a label the
|
|
213
|
+
// repository does not have, so leaving one out made the first `auto-ok` a manual creation step.
|
|
214
|
+
// `bug` is GitHub's default label, kept because `issue:file` applies it to a bug fix and a
|
|
215
|
+
// repository may have been created without the defaults.
|
|
210
216
|
const WORKFLOW_LABELS: ReadonlyArray<LabelDefinition> = [
|
|
211
217
|
{
|
|
212
218
|
name: EPIC_LABEL,
|
|
@@ -235,6 +241,32 @@ const WORKFLOW_LABELS: ReadonlyArray<LabelDefinition> = [
|
|
|
235
241
|
color: 'fef2c0',
|
|
236
242
|
description: 'Tracks the next release; its blockers are the merged work it publishes',
|
|
237
243
|
},
|
|
244
|
+
{
|
|
245
|
+
name: AUTO_OK_LABEL,
|
|
246
|
+
color: '0e8a16',
|
|
247
|
+
description: 'Opted in to unattended execution outside an epic',
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: NEEDS_DECISION_LABEL,
|
|
251
|
+
color: 'd93f0b',
|
|
252
|
+
description: 'Parked: needs a decision before it can run',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: NEEDS_HUMAN_REVIEW_LABEL,
|
|
256
|
+
color: 'd93f0b',
|
|
257
|
+
description: 'Implement and verify, but stop before committing so a person can look',
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: ALREADY_DONE_LABEL,
|
|
261
|
+
color: '6f42c1',
|
|
262
|
+
description: 'Verified already merged — a person closes it',
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: REVIEW_ROUND2_SKIPPED_LABEL,
|
|
266
|
+
color: 'c5def5',
|
|
267
|
+
description: 'Review round 2 was skipped under the condition in prompts/review.md',
|
|
268
|
+
},
|
|
269
|
+
{ name: BUG_LABEL, color: 'd73a4a', description: "Something isn't working" },
|
|
238
270
|
...FILING_ROUTE_LABELS,
|
|
239
271
|
...DEPTH_LABELS,
|
|
240
272
|
]
|
|
@@ -329,12 +361,6 @@ function depth_label_of(labels: ReadonlyArray<LabelReference> | undefined): stri
|
|
|
329
361
|
// document operates on a label name that does not exist here — a stale or mistyped `labels[]=…`
|
|
330
362
|
// otherwise fails silently at run time. One source, so a label added above joins the scan without a
|
|
331
363
|
// second edit.
|
|
332
|
-
// Applied to an Issue whose second review round was skipped, so the condition stays auditable
|
|
333
|
-
// (`prompts/review.md` → "When round 2 is skipped entirely, and when it is not"). Provisioned inline
|
|
334
|
-
// by the run that applies it rather than through `FILING_ROUTE_LABELS`, but still a managed label the
|
|
335
|
-
// documents operate on, so the label-reference scan has to know it.
|
|
336
|
-
const REVIEW_ROUND2_SKIPPED_LABEL = 'review-round2-skipped'
|
|
337
|
-
|
|
338
364
|
const ALL_LABELS: ReadonlySet<string> = new Set([
|
|
339
365
|
EPIC_LABEL,
|
|
340
366
|
BUG_LABEL,
|
|
@@ -363,6 +389,11 @@ const ALL_LABELS: ReadonlySet<string> = new Set([
|
|
|
363
389
|
IGNORE_FOR_RELEASE_LABEL,
|
|
364
390
|
])
|
|
365
391
|
|
|
392
|
+
// The managed labels `REPOSITORY_LABELS` deliberately does not provision, each with its reason. Every
|
|
393
|
+
// other member of `ALL_LABELS` is provisioned — `issue-labels.test.ts` fails on one that is in
|
|
394
|
+
// neither list, so a label added above cannot silently become one a repository lacks. Empty today.
|
|
395
|
+
const EXEMPT_FROM_PROVISIONING: ReadonlyMap<string, string> = new Map<string, string>()
|
|
396
|
+
|
|
366
397
|
export type { LabelDefinition, ReleaseClassification }
|
|
367
398
|
export {
|
|
368
399
|
ALL_LABELS,
|
|
@@ -379,6 +410,7 @@ export {
|
|
|
379
410
|
DEPTH_LABELS,
|
|
380
411
|
ENHANCEMENT_LABEL,
|
|
381
412
|
EPIC_LABEL,
|
|
413
|
+
EXEMPT_FROM_PROVISIONING,
|
|
382
414
|
FILING_ROUTE_LABELS,
|
|
383
415
|
has_any_label,
|
|
384
416
|
has_label_name,
|
|
@@ -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 }
|