@joshuafolkken/kit 1.1387.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-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
- 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 +288 -229
- 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 +62 -9
- 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-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 +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 +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 +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 +15 -0
- package/scripts/init/start-steps.ts +46 -0
- package/scripts/init/start.ts +1 -0
- 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 +24 -0
- 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 +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/run/board/run-board-track.ts +0 -22
|
@@ -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 }
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { git_gh_api_path } from '#scripts/gh/git-gh-api-path'
|
|
2
|
+
import { git_gh_conditional } from '#scripts/gh/git-gh-conditional'
|
|
3
|
+
import { git_gh_issue_read } from '#scripts/gh/git-gh-issue-read'
|
|
4
|
+
import { git_command } from '#scripts/git/git-command'
|
|
5
|
+
import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
6
|
+
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
7
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { issue_watch_diff, type WatchSnapshot } from './issue-watch-diff'
|
|
10
|
+
|
|
11
|
+
// The record of what a run last read of its issue, and the poll that compares the issue against it.
|
|
12
|
+
//
|
|
13
|
+
// **The baseline is written where the run reads the issue** — `josh run:entry`, which prints the body
|
|
14
|
+
// and every comment — so it is the same for a dispatched lane child and a `fullrun` in a person's own
|
|
15
|
+
// checkout, and a resume that prints nothing keeps the baseline it had. One record per checkout,
|
|
16
|
+
// keyed on its root like the review attestation, carries the issue number, so the hook needs no other
|
|
17
|
+
// way to learn which issue the session works on.
|
|
18
|
+
//
|
|
19
|
+
// **A poll is a conditional request.** The issue object's ETag changes with its body and with its
|
|
20
|
+
// comment count, so a 304 there means neither moved and the comments are not read at all.
|
|
21
|
+
|
|
22
|
+
const RECORD_PREFIX = 'josh-issue-watch-'
|
|
23
|
+
const CLOSED_STATE = 'closed'
|
|
24
|
+
|
|
25
|
+
const record_schema = z.object({
|
|
26
|
+
issue: z.string(),
|
|
27
|
+
body: z.string(),
|
|
28
|
+
last_comment_id: z.number(),
|
|
29
|
+
acknowledged_at: z.string(),
|
|
30
|
+
checked_at: z.string(),
|
|
31
|
+
etag: z.string().optional(),
|
|
32
|
+
pending: z.string().optional(),
|
|
33
|
+
shown: z.string().optional(),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const issue_schema = z.looseObject({ body: z.string().nullish(), state: z.string() })
|
|
37
|
+
|
|
38
|
+
type WatchRecord = z.infer<typeof record_schema>
|
|
39
|
+
|
|
40
|
+
type Fetched =
|
|
41
|
+
| { kind: 'unchanged' }
|
|
42
|
+
| { kind: 'unreadable' }
|
|
43
|
+
| { kind: 'closed' }
|
|
44
|
+
| { kind: 'read'; snapshot: WatchSnapshot; etag: string | undefined }
|
|
45
|
+
|
|
46
|
+
const UNREADABLE: Fetched = { kind: 'unreadable' }
|
|
47
|
+
|
|
48
|
+
function record_path(root: string = PROJECT_ROOT): string {
|
|
49
|
+
return stamp_file.stamp_path(RECORD_PREFIX, root)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function read_record(root?: string): WatchRecord | undefined {
|
|
53
|
+
const raw = stamp_file.read_stamp_text(record_path(root))
|
|
54
|
+
|
|
55
|
+
return raw === undefined ? undefined : json_value.parse_with(raw, record_schema)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function write_record(record: WatchRecord, root?: string): WatchRecord {
|
|
59
|
+
stamp_file.replace_stamp(record_path(root), record)
|
|
60
|
+
|
|
61
|
+
return record
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function clear(root?: string): void {
|
|
65
|
+
stamp_file.remove_stamp(record_path(root))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface FetchRequest {
|
|
69
|
+
issue: string
|
|
70
|
+
etag?: string | undefined
|
|
71
|
+
timeout_ms?: number | undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function read_snapshot(
|
|
75
|
+
request: FetchRequest,
|
|
76
|
+
body: string,
|
|
77
|
+
etag: string | undefined,
|
|
78
|
+
): Promise<Fetched> {
|
|
79
|
+
const comments = issue_watch_diff.parse_comments(
|
|
80
|
+
await git_gh_issue_read.issue_list_comments(request.issue, request.timeout_ms),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return comments === undefined ? UNREADABLE : { kind: 'read', snapshot: { body, comments }, etag }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function fetch_issue(request: FetchRequest): Promise<Fetched> {
|
|
87
|
+
const read = await git_gh_conditional.read_conditional(
|
|
88
|
+
git_gh_api_path.issue_api_path(request.issue),
|
|
89
|
+
request.etag,
|
|
90
|
+
request.timeout_ms,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if (read.kind !== 'changed') return read
|
|
94
|
+
|
|
95
|
+
const fields = json_value.parse_with(read.body, issue_schema)
|
|
96
|
+
|
|
97
|
+
if (fields === undefined) return UNREADABLE
|
|
98
|
+
if (fields.state === CLOSED_STATE) return { kind: 'closed' }
|
|
99
|
+
|
|
100
|
+
return await read_snapshot(request, fields.body ?? '', read.etag)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// `acknowledged_at` is when a person's update was last taken in — the baseline's own read, or an
|
|
104
|
+
// acknowledgement that had something to take in. The review attestation compares it with the brief.
|
|
105
|
+
function fresh_record(
|
|
106
|
+
issue: string,
|
|
107
|
+
fetched: Extract<Fetched, { kind: 'read' }>,
|
|
108
|
+
now: Date,
|
|
109
|
+
acknowledged_at: string = now.toISOString(),
|
|
110
|
+
): WatchRecord {
|
|
111
|
+
const baseline = issue_watch_diff.baseline_of(fetched.snapshot)
|
|
112
|
+
const checked_at = now.toISOString()
|
|
113
|
+
|
|
114
|
+
return { issue, ...baseline, acknowledged_at, checked_at, etag: fetched.etag }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Called by `josh run:entry` once it has printed the issue. `false` when the issue could not be read,
|
|
118
|
+
// which leaves no record — a watch with no baseline would call the whole issue unread.
|
|
119
|
+
async function record_baseline(
|
|
120
|
+
issue: string,
|
|
121
|
+
root?: string,
|
|
122
|
+
now: Date = new Date(),
|
|
123
|
+
): Promise<boolean> {
|
|
124
|
+
const fetched = await fetch_issue({ issue })
|
|
125
|
+
|
|
126
|
+
if (fetched.kind !== 'read') return false
|
|
127
|
+
|
|
128
|
+
write_record(fresh_record(issue, fetched, now), root)
|
|
129
|
+
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The `_here` forms key the record on the repository root rather than `process.cwd()`, the same reason
|
|
134
|
+
// `review_attest.check_here` does: a run started from a subdirectory must find the record it wrote.
|
|
135
|
+
async function record_baseline_here(issue: string): Promise<boolean> {
|
|
136
|
+
return await record_baseline(issue, await git_command.repository_root())
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function clear_here(): Promise<void> {
|
|
140
|
+
clear(await git_command.repository_root())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function with_fetched(record: WatchRecord, fetched: Fetched, checked_at: string): WatchRecord {
|
|
144
|
+
if (fetched.kind !== 'read') return { ...record, checked_at }
|
|
145
|
+
|
|
146
|
+
const pending = issue_watch_diff.unread_diff(record, fetched.snapshot)
|
|
147
|
+
|
|
148
|
+
return { ...record, checked_at, etag: fetched.etag, pending }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface PollOptions {
|
|
152
|
+
interval_ms: number
|
|
153
|
+
timeout_ms?: number
|
|
154
|
+
now?: Date
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function is_due(record: WatchRecord, options: PollOptions, now: Date): boolean {
|
|
158
|
+
return now.getTime() - Date.parse(record.checked_at) >= options.interval_ms
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function refresh(
|
|
162
|
+
record: WatchRecord,
|
|
163
|
+
options: PollOptions,
|
|
164
|
+
now: Date,
|
|
165
|
+
root?: string,
|
|
166
|
+
): Promise<WatchRecord | undefined> {
|
|
167
|
+
const fetched = await fetch_issue({
|
|
168
|
+
issue: record.issue,
|
|
169
|
+
etag: record.etag,
|
|
170
|
+
timeout_ms: options.timeout_ms,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
if (fetched.kind === 'closed') {
|
|
174
|
+
clear(root)
|
|
175
|
+
|
|
176
|
+
return undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return write_record(with_fetched(record, fetched, now.toISOString()), root)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The checkout's record, polled when the last poll is at least `interval_ms` old. `undefined` when no
|
|
183
|
+
// run recorded a baseline here, or the issue has closed — a closed issue ends its watch.
|
|
184
|
+
async function poll(options: PollOptions, root?: string): Promise<WatchRecord | undefined> {
|
|
185
|
+
const record = read_record(root)
|
|
186
|
+
const now = options.now ?? new Date()
|
|
187
|
+
|
|
188
|
+
if (record === undefined || !is_due(record, options, now)) return record
|
|
189
|
+
|
|
190
|
+
return await refresh(record, options, now, root)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Marks a pending update shown, so the hook stops once per update rather than on every call.
|
|
194
|
+
function mark_shown(record: WatchRecord, root?: string): WatchRecord {
|
|
195
|
+
return write_record({ ...record, shown: record.pending }, root)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface Acknowledged {
|
|
199
|
+
issue: string
|
|
200
|
+
diff: string | undefined
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// `josh issue:ack`: read the issue as it is now, return what changed since the baseline, and make
|
|
204
|
+
// that the new baseline. **What is returned is exactly what is acknowledged** — the read and the new
|
|
205
|
+
// baseline are the same snapshot, so an update landing between the two cannot be acknowledged unseen.
|
|
206
|
+
async function acknowledge(
|
|
207
|
+
root?: string,
|
|
208
|
+
now: Date = new Date(),
|
|
209
|
+
): Promise<Acknowledged | undefined> {
|
|
210
|
+
const record = read_record(root)
|
|
211
|
+
|
|
212
|
+
if (record === undefined) return undefined
|
|
213
|
+
|
|
214
|
+
const fetched = await fetch_issue({ issue: record.issue })
|
|
215
|
+
|
|
216
|
+
if (fetched.kind !== 'read') return undefined
|
|
217
|
+
|
|
218
|
+
const diff = issue_watch_diff.unread_diff(record, fetched.snapshot)
|
|
219
|
+
|
|
220
|
+
const acknowledged_at = diff === undefined ? record.acknowledged_at : now.toISOString()
|
|
221
|
+
|
|
222
|
+
write_record(fresh_record(record.issue, fetched, now, acknowledged_at), root)
|
|
223
|
+
|
|
224
|
+
return { issue: record.issue, diff }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const issue_watch = {
|
|
228
|
+
acknowledge,
|
|
229
|
+
clear,
|
|
230
|
+
clear_here,
|
|
231
|
+
mark_shown,
|
|
232
|
+
poll,
|
|
233
|
+
read_record,
|
|
234
|
+
record_baseline,
|
|
235
|
+
record_baseline_here,
|
|
236
|
+
record_path,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export type { Acknowledged, PollOptions, WatchRecord }
|
|
240
|
+
export { issue_watch }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { execSync } from 'node:child_process'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { gh_author_association } from '#scripts/gh/gh-author-association'
|
|
4
5
|
import { git_gh_issue_read } from '#scripts/gh/git-gh-issue-read'
|
|
5
6
|
import { parse_json_object_or_undefined } from '#scripts/git/parse-json-array'
|
|
6
7
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
@@ -24,8 +25,6 @@ import { session_cite } from './session-cite'
|
|
|
24
25
|
const FAILURE_EXIT_CODE = 1
|
|
25
26
|
const ARGV_OFFSET = 2
|
|
26
27
|
const ISSUE_FIELDS = 'body,author_association'
|
|
27
|
-
const TRUSTED_ASSOCIATIONS: ReadonlySet<string> = new Set(['OWNER', 'MEMBER', 'COLLABORATOR'])
|
|
28
|
-
const TRUSTED_LIST = [...TRUSTED_ASSOCIATIONS].join(' / ')
|
|
29
28
|
const UNKNOWN_ASSOCIATION = 'unknown'
|
|
30
29
|
const USAGE = 'Usage: josh measure:rerun <issue-number>'
|
|
31
30
|
const NO_BASELINE = 'no `command → value` baseline found in ## ベースライン — nothing to re-run'
|
|
@@ -100,10 +99,10 @@ async function read_trusted_body(issue_number: string): Promise<BodyRead> {
|
|
|
100
99
|
|
|
101
100
|
const association = issue.author_association ?? UNKNOWN_ASSOCIATION
|
|
102
101
|
|
|
103
|
-
if (!
|
|
102
|
+
if (!gh_author_association.is_trusted(association)) {
|
|
104
103
|
return {
|
|
105
104
|
kind: 'refused',
|
|
106
|
-
reason: `refusing to run the baseline of ${session_cite.issue(issue_number)}: its author is ${association}, not ${TRUSTED_LIST}`,
|
|
105
|
+
reason: `refusing to run the baseline of ${session_cite.issue(issue_number)}: its author is ${association}, not ${gh_author_association.TRUSTED_LIST}`,
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
108
|
|
|
@@ -54,6 +54,7 @@ const ALIASES: Record<string, string> = {
|
|
|
54
54
|
pf: 'profile',
|
|
55
55
|
st: 'start',
|
|
56
56
|
sy: 'sync',
|
|
57
|
+
un: 'uninstall',
|
|
57
58
|
rmi: 'registry:migrate',
|
|
58
59
|
g: 'git',
|
|
59
60
|
gp: 'pr',
|
|
@@ -79,9 +80,12 @@ const ALIASES: Record<string, string> = {
|
|
|
79
80
|
rvf: 'review:findings',
|
|
80
81
|
ruv: 'rule:value',
|
|
81
82
|
ev: 'eval',
|
|
83
|
+
fr: 'fresh:run',
|
|
84
|
+
as: 'auth:setup',
|
|
82
85
|
lsm: 'lane:sample',
|
|
83
86
|
lst: 'lane:stats',
|
|
84
87
|
lli: 'lane:limit',
|
|
88
|
+
lrs: 'lane:restart',
|
|
85
89
|
blr: 'backlogrun',
|
|
86
90
|
}
|
|
87
91
|
|
|
@@ -11,10 +11,10 @@ const BACKLOG_COMMANDS: Record<string, CommandEntry> = {
|
|
|
11
11
|
backlogrun: {
|
|
12
12
|
script: 'scripts/backlog/backlogrun-cli.ts',
|
|
13
13
|
description:
|
|
14
|
-
'Start a backlogrun in the background and show its board; with one already running, only show the board',
|
|
14
|
+
'Start a backlogrun in the background and show its board; with one already running, only show the board, or replace it when it is idle and the invocation differs; --stop stops it',
|
|
15
15
|
category: 'AI tools',
|
|
16
16
|
reference: [
|
|
17
|
-
'[
|
|
17
|
+
'[<n>...] [--only] [--max <n>] [--idle <minutes>] [--agent claude|codex] | --stop',
|
|
18
18
|
'developer',
|
|
19
19
|
['files', 'network', 'processes'],
|
|
20
20
|
],
|
|
@@ -181,7 +181,7 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
|
|
|
181
181
|
'test:unit': {
|
|
182
182
|
script: 'scripts/test/test-unit-guard.ts',
|
|
183
183
|
description:
|
|
184
|
-
'Run unit tests with Vitest (
|
|
184
|
+
'Run unit tests with Vitest (without Vitest: a basic project runs its own test script, others skip; fails when Vitest has no tests)',
|
|
185
185
|
category: 'Development',
|
|
186
186
|
reference: [FILTER_ARGUMENTS, 'developer', ['processes']],
|
|
187
187
|
core_weight: gate_plan.direct_unit_weight,
|
|
@@ -11,6 +11,13 @@ const ISSUES_WITH_REPO = '<issue...> [--repo <owner/repo>]'
|
|
|
11
11
|
|
|
12
12
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
13
13
|
const ISSUE_COMMANDS: Record<string, CommandEntry> = {
|
|
14
|
+
'issue:ack': {
|
|
15
|
+
script: 'scripts/issue/issue-ack-cli.ts',
|
|
16
|
+
description:
|
|
17
|
+
"Record that the run read a person's update to its issue, printing the update it acknowledges",
|
|
18
|
+
category: 'AI tools',
|
|
19
|
+
reference: ['<issue>', 'automation', ['network']],
|
|
20
|
+
},
|
|
14
21
|
'issue:read': {
|
|
15
22
|
script: 'scripts/issue/issue-read-cli.ts',
|
|
16
23
|
description: "Print each issue's title, body and every comment on it, in one call",
|
|
@@ -40,7 +40,7 @@ const LANE_COMMANDS: Record<string, CommandEntry> = {
|
|
|
40
40
|
'lane:prune': {
|
|
41
41
|
...LANE_ARGUMENTS,
|
|
42
42
|
description:
|
|
43
|
-
'Close every lane an interruption left without its work tree, then sweep unregistered leftovers under the lanes root',
|
|
43
|
+
'Close every lane an interruption left without its work tree and every spent lane, then sweep unregistered leftovers under the lanes root',
|
|
44
44
|
category: 'AI tools',
|
|
45
45
|
reference: ['', 'automation', ['files', 'git']],
|
|
46
46
|
default_script_arguments: ['prune'],
|
|
@@ -74,6 +74,15 @@ const LANE_COMMANDS: Record<string, CommandEntry> = {
|
|
|
74
74
|
category: 'AI tools',
|
|
75
75
|
reference: ['<issue> [--stash <message>]', 'automation', ['files', 'git', 'processes']],
|
|
76
76
|
},
|
|
77
|
+
// `.env` is read for `JOSH_LANE_ROOT`, as every lane command reads it, so the lane is found.
|
|
78
|
+
'lane:restart': {
|
|
79
|
+
script: 'scripts/lane/lane-restart-cli.ts',
|
|
80
|
+
tsx_arguments: OPTIONAL_ENV_FILE_FLAGS,
|
|
81
|
+
description:
|
|
82
|
+
'Stop a running lane child, stash its uncommitted work and re-dispatch its issue in a fresh session — not counted as an outage or a failure',
|
|
83
|
+
category: 'AI tools',
|
|
84
|
+
reference: ['<issue>', 'developer', ['files', 'git', 'processes', 'network']],
|
|
85
|
+
},
|
|
77
86
|
// A live run's lane limit: `.env` is read so the bare form reports the
|
|
78
87
|
// `JOSH_LANE_LIMIT` a person set there when no override stands.
|
|
79
88
|
'lane:limit': {
|
|
@@ -97,7 +106,7 @@ const LANE_COMMANDS: Record<string, CommandEntry> = {
|
|
|
97
106
|
'lane:stats': {
|
|
98
107
|
...MEASURE_ARGUMENTS,
|
|
99
108
|
description:
|
|
100
|
-
'Print one table row of lane throughput, gate duration and machine load over a period,
|
|
109
|
+
'Print one table row of lane throughput, gate duration and machine load over a period, the per-stage durations of a lane under it, and the wait after each ship stop reason under those',
|
|
101
110
|
category: 'AI tools',
|
|
102
111
|
reference: ['--period <days> [--limit <lane-limit>]', 'maintainer', ['none']],
|
|
103
112
|
default_script_arguments: ['stats'],
|