@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
|
@@ -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
|
|
|
@@ -61,6 +61,17 @@ const AI_COMMANDS: Record<string, CommandEntry> = {
|
|
|
61
61
|
category: 'AI tools',
|
|
62
62
|
reference: ['[--exclude <n>[,<n>...]] [--label <name>]', 'automation', ['network']],
|
|
63
63
|
},
|
|
64
|
+
'auto-ok:judge': {
|
|
65
|
+
script: 'scripts/auto-ok/auto-ok-judge-cli.ts',
|
|
66
|
+
description:
|
|
67
|
+
'List the open issues backlogrun owes an auto-ok judgement, or record one (--apply / --decline)',
|
|
68
|
+
category: 'AI tools',
|
|
69
|
+
reference: [
|
|
70
|
+
'[--apply <n> (--lane | --solo) | --decline <n> --reason <text>]',
|
|
71
|
+
'automation',
|
|
72
|
+
['network'],
|
|
73
|
+
],
|
|
74
|
+
},
|
|
64
75
|
...BACKLOG_COMMANDS,
|
|
65
76
|
cost: {
|
|
66
77
|
script: 'scripts/cost-runtime/cost-cli.ts',
|
|
@@ -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",
|
|
@@ -68,7 +75,7 @@ const ISSUE_COMMANDS: Record<string, CommandEntry> = {
|
|
|
68
75
|
'File an Issue with every filing step: lint, Origin, fold, WIP cap, duplicate scout, labels, then epic:bundle',
|
|
69
76
|
category: 'AI tools',
|
|
70
77
|
reference: [
|
|
71
|
-
'<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok] [--
|
|
78
|
+
'<title> --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok] [--release]',
|
|
72
79
|
'automation',
|
|
73
80
|
['network'],
|
|
74
81
|
],
|
|
@@ -76,9 +83,9 @@ const ISSUE_COMMANDS: Record<string, CommandEntry> = {
|
|
|
76
83
|
'issue:release': {
|
|
77
84
|
script: 'scripts/issue/issue-release-cli.ts',
|
|
78
85
|
description:
|
|
79
|
-
"Link an Issue to the repository's open release Issue as its blocker, filing one when none is open",
|
|
86
|
+
"Link an Issue to the repository's open release Issue as its blocker (or, with --wait, as waiting on it), filing one when none is open",
|
|
80
87
|
category: 'AI tools',
|
|
81
|
-
reference: ['<issue>', 'automation', ['network']],
|
|
88
|
+
reference: ['<issue> [--wait]', 'automation', ['network']],
|
|
82
89
|
},
|
|
83
90
|
}
|
|
84
91
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
@@ -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'],
|
|
@@ -17,7 +17,7 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
17
17
|
description: 'Set a project up for the GitHub Issue workflow, from git init to the setup PR',
|
|
18
18
|
category: 'Project',
|
|
19
19
|
reference: [
|
|
20
|
-
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--init-command <command>] [--push-preflight-command <command>]`,
|
|
20
|
+
`${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--dry-run] [--init-command <command>] [--push-preflight-command <command>]`,
|
|
21
21
|
'developer',
|
|
22
22
|
['files', 'git', 'network', 'processes'],
|
|
23
23
|
],
|
|
@@ -29,12 +29,30 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
29
29
|
reference: ['<dir>', 'automation', ['files', 'git']],
|
|
30
30
|
is_kit_only: true,
|
|
31
31
|
},
|
|
32
|
+
'fresh:run': {
|
|
33
|
+
script: 'scripts/fresh-machine/fresh-machine-cli.ts',
|
|
34
|
+
description:
|
|
35
|
+
'Run an install scenario in a Docker container of a machine with nothing installed, and keep the log',
|
|
36
|
+
category: 'Project',
|
|
37
|
+
reference: [
|
|
38
|
+
'<pattern> | --spec <file.json> [--package-version <version>] [--log-dir <dir>] [--rebuild] | --list',
|
|
39
|
+
'maintainer',
|
|
40
|
+
['files', 'network', 'processes'],
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
'auth:setup': {
|
|
44
|
+
script: 'scripts/auth/auth-setup-cli.ts',
|
|
45
|
+
description:
|
|
46
|
+
'Save the long-lived Claude Code token `claude setup-token` prints in the macOS keychain, for josh fresh:run',
|
|
47
|
+
category: 'Project',
|
|
48
|
+
reference: ['[--check]', 'maintainer', ['network', 'processes']],
|
|
49
|
+
},
|
|
32
50
|
init: {
|
|
33
51
|
script: 'scripts/init/init.ts',
|
|
34
52
|
description: 'Initialize config in a new project',
|
|
35
53
|
category: 'Project',
|
|
36
54
|
reference: [
|
|
37
|
-
`${PROFILE_ARGUMENTS} [--no-install]`,
|
|
55
|
+
`${PROFILE_ARGUMENTS} [--no-install] [--dry-run]`,
|
|
38
56
|
'developer',
|
|
39
57
|
['files', 'network', 'processes'],
|
|
40
58
|
],
|
|
@@ -45,6 +63,12 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
45
63
|
category: 'Project',
|
|
46
64
|
reference: ['', 'developer', ['files']],
|
|
47
65
|
},
|
|
66
|
+
uninstall: {
|
|
67
|
+
script: 'scripts/uninstall/uninstall.ts',
|
|
68
|
+
description: 'Remove what kit added to a project and list what is left to review by hand',
|
|
69
|
+
category: 'Project',
|
|
70
|
+
reference: [`${PROFILE_ARGUMENTS} [--yes]`, 'developer', ['files', 'processes']],
|
|
71
|
+
},
|
|
48
72
|
'registry:migrate': {
|
|
49
73
|
script: 'scripts/registry-migration/migrate.ts',
|
|
50
74
|
description: 'Migrate a project from GitHub Packages to public npm',
|
|
@@ -35,7 +35,7 @@ import { status_icons } from '#scripts/lib/status-icons'
|
|
|
35
35
|
// gate's plan line is what says "the gate starts here", and it is single-sourced for the reason the
|
|
36
36
|
// verdict is: the printer builds it and the detector matches it from one place.
|
|
37
37
|
|
|
38
|
-
const { FAIL_ICON, PASS_ICON } = status_icons
|
|
38
|
+
const { FAIL_ICON, PASS_ICON, WARN_ICON } = status_icons
|
|
39
39
|
const GATE_SUBJECT = 'verification gate'
|
|
40
40
|
|
|
41
41
|
// The prefixes are what the detector matches **and** what the printer builds its line from, so a
|
|
@@ -43,16 +43,41 @@ const GATE_SUBJECT = 'verification gate'
|
|
|
43
43
|
// this module exists to prevent.
|
|
44
44
|
const GATE_PASSED_PREFIX = `${PASS_ICON} ${GATE_SUBJECT} passed`
|
|
45
45
|
const GATE_FAILED_PREFIX = `${FAIL_ICON} ${GATE_SUBJECT} failed`
|
|
46
|
+
// **A gate that ran no check has not passed, and it has not failed either.** Every check of a `basic`
|
|
47
|
+
// project with nothing to check exits 0 with a skip notice, which is a legitimate state — the exit code
|
|
48
|
+
// stays 0, or such a project could never commit — but a line reading `passed` made it indistinguishable
|
|
49
|
+
// from a gate that ran every check. It opens with neither icon and neither word, so no reader of the
|
|
50
|
+
// other two prefixes can take it for one of them.
|
|
51
|
+
const GATE_UNVERIFIED_PREFIX = `${WARN_ICON} ${GATE_SUBJECT} not verified`
|
|
46
52
|
|
|
47
|
-
type JoshVerdict = 'passed' | 'failed'
|
|
53
|
+
type JoshVerdict = 'passed' | 'failed' | 'unverified'
|
|
48
54
|
|
|
49
55
|
// Typed rather than left to inference: the namespace object widens a bare literal to `string`, and a
|
|
50
56
|
// caller comparing `read_verdict(line)` against it would then be comparing two unrelated types.
|
|
51
57
|
const PASSED_VERDICT: JoshVerdict = 'passed'
|
|
52
58
|
const FAILED_VERDICT: JoshVerdict = 'failed'
|
|
59
|
+
const UNVERIFIED_VERDICT: JoshVerdict = 'unverified'
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
// How many of a gate's checks ran, and how many passed without running.
|
|
62
|
+
interface GateCounts {
|
|
63
|
+
ran: number
|
|
64
|
+
skipped: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The split is printed only when there is one. A gate that skipped nothing — kit's own, and an ordinary
|
|
68
|
+
// consumer's — keeps the line it always printed.
|
|
69
|
+
function format_counts(counts: GateCounts): string {
|
|
70
|
+
if (counts.skipped === 0) return `${String(counts.ran)} checks`
|
|
71
|
+
|
|
72
|
+
return `${String(counts.ran)} ran, ${String(counts.skipped)} skipped`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function format_gate_passed(counts: GateCounts, total: string): string {
|
|
76
|
+
return `${GATE_PASSED_PREFIX} (${format_counts(counts)}) in ${total}.`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function format_gate_unverified(skipped: number, total: string): string {
|
|
80
|
+
return `${GATE_UNVERIFIED_PREFIX} (0 ran, ${String(skipped)} skipped) in ${total}.`
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
function format_gate_failed(failed_labels: string, total: string): string {
|
|
@@ -91,14 +116,18 @@ function read_verdict(line: string): JoshVerdict | undefined {
|
|
|
91
116
|
|
|
92
117
|
if (trimmed.startsWith(GATE_PASSED_PREFIX)) return PASSED_VERDICT
|
|
93
118
|
|
|
119
|
+
if (trimmed.startsWith(GATE_UNVERIFIED_PREFIX)) return UNVERIFIED_VERDICT
|
|
120
|
+
|
|
94
121
|
return undefined
|
|
95
122
|
}
|
|
96
123
|
|
|
97
124
|
const josh_verdict = {
|
|
98
125
|
PASSED_VERDICT,
|
|
99
126
|
FAILED_VERDICT,
|
|
127
|
+
UNVERIFIED_VERDICT,
|
|
100
128
|
format_gate_passed,
|
|
101
129
|
format_gate_failed,
|
|
130
|
+
format_gate_unverified,
|
|
102
131
|
GATE_OPENING_MARK,
|
|
103
132
|
GATE_OPENING_PREFIX,
|
|
104
133
|
is_gate_opening,
|
|
@@ -52,6 +52,7 @@ const PROBE_ENVIRONMENT = { TZ: 'UTC', LC_ALL: 'C' }
|
|
|
52
52
|
// `process.kill(pid, 0)` runs every permission check and delivers nothing, so it is the standard
|
|
53
53
|
// liveness probe: it throws `ESRCH` where the process is gone.
|
|
54
54
|
const LIVENESS_SIGNAL = 0
|
|
55
|
+
const NO_SUCH_PROCESS = 'ESRCH'
|
|
55
56
|
// Below this a pid is not a process: `0` addresses the caller's own process group and anything
|
|
56
57
|
// negative addresses another group, so a truncated or hand-edited record carrying either would
|
|
57
58
|
// otherwise answer "alive" forever.
|
|
@@ -79,18 +80,28 @@ interface StartProbes {
|
|
|
79
80
|
read_ps_start: (pid: number) => string | undefined
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
// The code the liveness probe was refused with, or nothing where the caller may signal the process.
|
|
84
|
+
function signal_refusal(pid: number): string | undefined {
|
|
85
85
|
try {
|
|
86
86
|
process.kill(pid, LIVENESS_SIGNAL)
|
|
87
87
|
|
|
88
|
-
return
|
|
89
|
-
} catch {
|
|
90
|
-
return
|
|
88
|
+
return undefined
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return (error as NodeJS.ErrnoException).code ?? NO_SUCH_PROCESS
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function is_live_pid(pid: number): boolean {
|
|
95
|
+
return pid >= LOWEST_REAL_PID && signal_refusal(pid) === undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// **Whether a pid names any process at all, for a reader that may not be allowed to signal it.** Only
|
|
99
|
+
// `ESRCH` proves the process gone: `EPERM` is the kernel refusing a signal to a process it has just
|
|
100
|
+
// found — one under another user, or outside a sandbox that may only signal its own — so it is present.
|
|
101
|
+
function is_present_pid(pid: number): boolean {
|
|
102
|
+
return pid >= LOWEST_REAL_PID && signal_refusal(pid) !== NO_SUCH_PROCESS
|
|
103
|
+
}
|
|
104
|
+
|
|
94
105
|
// Linux exposes field 22 as the process start time in clock ticks since boot. The command name in
|
|
95
106
|
// field 2 may itself contain spaces or `)`, so parsing starts after its final closing parenthesis;
|
|
96
107
|
// `state` is then index 0 and `starttime` index 19. A direct file read avoids process enumeration and
|
|
@@ -315,6 +326,19 @@ function is_same_process(
|
|
|
315
326
|
return matches_recorded_start(pid, recorded_start, read)
|
|
316
327
|
}
|
|
317
328
|
|
|
329
|
+
// `is_same_process` for a reader that may not be allowed to signal the writer (`is_present_pid`): a
|
|
330
|
+
// refused signal still compares the start time, so a reissued pid separates from its writer here too.
|
|
331
|
+
function is_present_process(
|
|
332
|
+
pid: number,
|
|
333
|
+
recorded_start: string | undefined,
|
|
334
|
+
read: (pid: number) => string | undefined = read_start,
|
|
335
|
+
): boolean | undefined {
|
|
336
|
+
if (!is_present_pid(pid)) return false
|
|
337
|
+
if (recorded_start === undefined) return undefined
|
|
338
|
+
|
|
339
|
+
return matches_recorded_start(pid, recorded_start, read)
|
|
340
|
+
}
|
|
341
|
+
|
|
318
342
|
// **Whether a record is the caller's own, which is a different question from whether its writer is
|
|
319
343
|
// alive**. `is_same_process` asks about the recorded process; this asks
|
|
320
344
|
// whether that process *is this one*. A supervisor that has been replaced reads its successor's
|
|
@@ -339,6 +363,8 @@ const process_identity = {
|
|
|
339
363
|
is_live_beacon,
|
|
340
364
|
is_live_pid,
|
|
341
365
|
is_own_process,
|
|
366
|
+
is_present_pid,
|
|
367
|
+
is_present_process,
|
|
342
368
|
is_same_process,
|
|
343
369
|
open_beacon,
|
|
344
370
|
own_fields,
|