@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,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
chmodSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs'
|
|
11
|
+
import os from 'node:os'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
14
|
+
import { execa, type Options } from 'execa'
|
|
15
|
+
import { z } from 'zod'
|
|
16
|
+
import { fresh_machine_docker, type ContainerRun } from './fresh-machine-docker'
|
|
17
|
+
import { fresh_machine_image } from './fresh-machine-image'
|
|
18
|
+
import { fresh_machine_log } from './fresh-machine-log'
|
|
19
|
+
import { fresh_machine_script } from './fresh-machine-script'
|
|
20
|
+
import type { Scenario } from './fresh-machine-spec'
|
|
21
|
+
|
|
22
|
+
// The I/O of `josh fresh:run`: build the machine, stage what the container may read, run the
|
|
23
|
+
// scenario, and put every line of all three into one log.
|
|
24
|
+
|
|
25
|
+
const DOCKER = 'docker'
|
|
26
|
+
const PNPM = 'pnpm'
|
|
27
|
+
const FAILURE_EXIT_CODE = 1
|
|
28
|
+
const STAGE_PREFIX = 'josh-fresh-machine-'
|
|
29
|
+
// The container's user is not the host's, so what it reads has to be readable by anyone — whatever
|
|
30
|
+
// the host's umask gave the seed files and the tarball `pnpm pack` wrote.
|
|
31
|
+
const SHARED_DIRECTORY_MODE = 0o755
|
|
32
|
+
const SHARED_FILE_MODE = 0o644
|
|
33
|
+
const TARBALL_EXTENSION = '.tgz'
|
|
34
|
+
const MANIFEST_SCHEMA = z.object({ name: z.string() })
|
|
35
|
+
|
|
36
|
+
interface RunRequest {
|
|
37
|
+
scenario: Scenario
|
|
38
|
+
run_name: string
|
|
39
|
+
log_path: string
|
|
40
|
+
package_version: string | undefined
|
|
41
|
+
// Variable name → value, each passed into the container by name and replaced in every line.
|
|
42
|
+
secrets: Readonly<Record<string, string>>
|
|
43
|
+
is_rebuild: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Stage {
|
|
47
|
+
root: string
|
|
48
|
+
seed_directory: string
|
|
49
|
+
pack_directory: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Write = (line: string) => void
|
|
53
|
+
|
|
54
|
+
function logger(request: RunRequest): Write {
|
|
55
|
+
const secrets = Object.values(request.secrets)
|
|
56
|
+
|
|
57
|
+
return function write(line: string): void {
|
|
58
|
+
const safe = fresh_machine_log.redact(line, secrets)
|
|
59
|
+
|
|
60
|
+
console.info(safe)
|
|
61
|
+
appendFileSync(request.log_path, `${safe}\n`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// One host command, its output streamed line by line into the log. A command that could not start
|
|
66
|
+
// (no docker on the machine) has no exit code, and its reason is the only line there is to log.
|
|
67
|
+
async function stream(
|
|
68
|
+
file: string,
|
|
69
|
+
arguments_: Array<string>,
|
|
70
|
+
options: Options,
|
|
71
|
+
write: Write,
|
|
72
|
+
): Promise<number> {
|
|
73
|
+
const subprocess = execa(file, arguments_, { ...options, all: true, reject: false })
|
|
74
|
+
const lines = subprocess.iterable({ from: 'all' })
|
|
75
|
+
|
|
76
|
+
for await (const line of lines) write(String(line))
|
|
77
|
+
|
|
78
|
+
const result = await subprocess
|
|
79
|
+
|
|
80
|
+
if (result.exitCode === undefined) write(result.shortMessage ?? `${file} did not start`)
|
|
81
|
+
|
|
82
|
+
return result.exitCode ?? FAILURE_EXIT_CODE
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function staged_directory(root: string, name: string): string {
|
|
86
|
+
const directory = path.join(root, name)
|
|
87
|
+
|
|
88
|
+
mkdirSync(directory)
|
|
89
|
+
|
|
90
|
+
return directory
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stage(scenario: Scenario): Stage {
|
|
94
|
+
const root = mkdtempSync(path.join(os.tmpdir(), STAGE_PREFIX))
|
|
95
|
+
const seed_directory = staged_directory(root, 'seed')
|
|
96
|
+
|
|
97
|
+
for (const [file, content] of Object.entries(scenario.files)) {
|
|
98
|
+
const target = path.join(seed_directory, file)
|
|
99
|
+
|
|
100
|
+
mkdirSync(path.dirname(target), { recursive: true })
|
|
101
|
+
writeFileSync(target, content)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { root, seed_directory, pack_directory: staged_directory(root, 'pack') }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Run once everything is staged, so the tarball is covered as well as the seed. The root itself stays
|
|
108
|
+
// as private as `mkdtemp` made it: a bind mount never enters a source through its parent, and on a
|
|
109
|
+
// shared host an open root would let every other user read the tarball for the length of the run.
|
|
110
|
+
function share(root: string): void {
|
|
111
|
+
const entries = readdirSync(root, { withFileTypes: true, recursive: true })
|
|
112
|
+
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const mode = entry.isDirectory() ? SHARED_DIRECTORY_MODE : SHARED_FILE_MODE
|
|
115
|
+
|
|
116
|
+
chmodSync(path.join(entry.parentPath, entry.name), mode)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// `pnpm pack` runs the checkout's own `prepack`, so the tarball holds what a release of this tree
|
|
121
|
+
// would.
|
|
122
|
+
async function pack(pack_directory: string, write: Write): Promise<string> {
|
|
123
|
+
const pack_arguments = ['pack', '--pack-destination', pack_directory]
|
|
124
|
+
|
|
125
|
+
write(`$ ${PNPM} ${pack_arguments.join(' ')}`)
|
|
126
|
+
await stream(PNPM, pack_arguments, { cwd: PROJECT_ROOT }, write)
|
|
127
|
+
|
|
128
|
+
const tarball = readdirSync(pack_directory).find((file) => file.endsWith(TARBALL_EXTENSION))
|
|
129
|
+
|
|
130
|
+
if (tarball === undefined) throw new Error('pnpm pack produced no tarball')
|
|
131
|
+
|
|
132
|
+
return tarball
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function package_name(): string {
|
|
136
|
+
const manifest = readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8')
|
|
137
|
+
|
|
138
|
+
return MANIFEST_SCHEMA.parse(JSON.parse(manifest)).name
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The published version is always written out in the log and the install command, so a run can be
|
|
142
|
+
// repeated against the same release; without `--package-version` it is the registry's latest.
|
|
143
|
+
async function published_version(requested: string | undefined): Promise<string> {
|
|
144
|
+
if (requested !== undefined) return requested
|
|
145
|
+
|
|
146
|
+
const { stdout } = await execa(PNPM, ['view', package_name(), 'version'], { cwd: PROJECT_ROOT })
|
|
147
|
+
|
|
148
|
+
return stdout.trim()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function container_run(
|
|
152
|
+
request: RunRequest,
|
|
153
|
+
staged: Stage,
|
|
154
|
+
write: Write,
|
|
155
|
+
): Promise<ContainerRun> {
|
|
156
|
+
const { scenario } = request
|
|
157
|
+
const is_tarball = scenario.package_source === 'tarball'
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
image: fresh_machine_image.tag(scenario.requires_github),
|
|
161
|
+
run_name: request.run_name,
|
|
162
|
+
script: fresh_machine_script.build(scenario),
|
|
163
|
+
seed_directory: staged.seed_directory,
|
|
164
|
+
pack_directory: staged.pack_directory,
|
|
165
|
+
tarball: is_tarball ? await pack(staged.pack_directory, write) : undefined,
|
|
166
|
+
package_version: is_tarball ? undefined : await published_version(request.package_version),
|
|
167
|
+
secret_names: Object.keys(request.secrets),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function build_image(request: RunRequest, write: Write): Promise<number> {
|
|
172
|
+
const { scenario } = request
|
|
173
|
+
const image = fresh_machine_image.tag(scenario.requires_github)
|
|
174
|
+
const build_arguments = fresh_machine_docker.build_arguments(image, request.is_rebuild)
|
|
175
|
+
const input = fresh_machine_image.dockerfile(scenario.requires_github)
|
|
176
|
+
|
|
177
|
+
write(`$ ${DOCKER} ${build_arguments.join(' ')}`)
|
|
178
|
+
|
|
179
|
+
return await stream(DOCKER, build_arguments, { input }, write)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function run_container(request: RunRequest, staged: Stage, write: Write): Promise<number> {
|
|
183
|
+
const container = await container_run(request, staged, write)
|
|
184
|
+
|
|
185
|
+
share(staged.root)
|
|
186
|
+
write(`$ ${DOCKER} run ${container.image} — ${request.run_name}`)
|
|
187
|
+
write(`# package: ${container.tarball ?? container.package_version ?? ''}`)
|
|
188
|
+
|
|
189
|
+
return await stream(
|
|
190
|
+
DOCKER,
|
|
191
|
+
fresh_machine_docker.run_arguments(container),
|
|
192
|
+
{ env: request.secrets },
|
|
193
|
+
write,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function run(request: RunRequest): Promise<number> {
|
|
198
|
+
const write = logger(request)
|
|
199
|
+
const build_code = await build_image(request, write)
|
|
200
|
+
|
|
201
|
+
if (build_code !== 0) return build_code
|
|
202
|
+
|
|
203
|
+
const staged = stage(request.scenario)
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
return await run_container(request, staged, write)
|
|
207
|
+
} finally {
|
|
208
|
+
rmSync(staged.root, { recursive: true, force: true })
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const fresh_machine_run = { run }
|
|
213
|
+
|
|
214
|
+
export { fresh_machine_run }
|
|
215
|
+
export type { RunRequest }
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Scenario } from './fresh-machine-spec'
|
|
2
|
+
|
|
3
|
+
// The script one container runs. Every command is printed before it runs and its exit status after,
|
|
4
|
+
// so the log reads as a terminal session; the first failing command ends the run, and the probes
|
|
5
|
+
// that follow a clean run are recorded whatever they exit with.
|
|
6
|
+
|
|
7
|
+
const SEED_MOUNT = '/seed'
|
|
8
|
+
const PACK_MOUNT = '/pack'
|
|
9
|
+
const COPY_SEED = `cp -R ${SEED_MOUNT}/. .`
|
|
10
|
+
const PROBES_HEADING = '# probes — recorded, never failing the run'
|
|
11
|
+
const ESCAPED_QUOTE = String.raw`'\''`
|
|
12
|
+
const PREAMBLE = [
|
|
13
|
+
String.raw`step() { printf '\n$ %s\n' "$1"; eval "$1"; local code=$?; printf '[exit %s]\n' "$code"; return "$code"; }`,
|
|
14
|
+
'mkdir -p "$HOME/$RUN_NAME" && cd "$HOME/$RUN_NAME" || exit 1',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
function quote(command: string): string {
|
|
18
|
+
const escaped = command.split("'").join(ESCAPED_QUOTE)
|
|
19
|
+
|
|
20
|
+
return `'${escaped}'`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function required(command: string): string {
|
|
24
|
+
return `step ${quote(command)} || exit $?`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function probe_lines(probes: ReadonlyArray<string>): Array<string> {
|
|
28
|
+
if (probes.length === 0) return []
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
String.raw`printf '\n%s\n' ${quote(PROBES_HEADING)}`,
|
|
32
|
+
...probes.map((probe) => `step ${quote(probe)}`),
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function build(scenario: Scenario): string {
|
|
37
|
+
const seed = Object.keys(scenario.files).length > 0 ? [COPY_SEED] : []
|
|
38
|
+
const commands = [...seed, ...scenario.commands].map((command) => required(command))
|
|
39
|
+
|
|
40
|
+
return [...PREAMBLE, ...commands, ...probe_lines(scenario.probes), 'exit 0', ''].join('\n')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fresh_machine_script = { build, SEED_MOUNT, PACK_MOUNT }
|
|
44
|
+
|
|
45
|
+
export { fresh_machine_script }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
// A scenario is everything one `josh fresh:run` needs to know: the files that are in the project
|
|
5
|
+
// before anything is installed, the commands a person would type, and the probes recorded after
|
|
6
|
+
// them. kit's own six patterns and a consumer toolkit's spec file are the same shape, so the machine
|
|
7
|
+
// and the log are shared and nothing about them is kit's alone (joshuafolkken/kit#3714).
|
|
8
|
+
//
|
|
9
|
+
// Inside the container a command reads `$RUN_NAME` (the numbered project and repository name),
|
|
10
|
+
// `$PACKAGE_TARBALL` (the packed checkout, for `package_source: tarball`) and `$PACKAGE_VERSION`
|
|
11
|
+
// (the published version, for `package_source: published`).
|
|
12
|
+
|
|
13
|
+
function is_project_relative(file: string): boolean {
|
|
14
|
+
const normalized = path.posix.normalize(file)
|
|
15
|
+
|
|
16
|
+
return !path.posix.isAbsolute(normalized) && !normalized.startsWith('..')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const COMMAND_SCHEMA = z.string().min(1)
|
|
20
|
+
const SCENARIO_SCHEMA = z.strictObject({
|
|
21
|
+
name: z.string().regex(/^[a-z0-9][a-z0-9-]*$/u),
|
|
22
|
+
// A GitHub scenario runs on the image that also has git and gh, and receives the host's token.
|
|
23
|
+
requires_github: z.boolean().default(false),
|
|
24
|
+
// A Claude Code scenario receives the host's long-lived token as `CLAUDE_CODE_OAUTH_TOKEN`.
|
|
25
|
+
requires_claude: z.boolean().default(false),
|
|
26
|
+
package_source: z.enum(['tarball', 'published']).default('tarball'),
|
|
27
|
+
files: z.record(z.string().refine(is_project_relative), z.string()).default({}),
|
|
28
|
+
// A failing command ends the run; a failing probe is only recorded.
|
|
29
|
+
commands: z.array(COMMAND_SCHEMA).min(1),
|
|
30
|
+
probes: z.array(COMMAND_SCHEMA).default([]),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
type Scenario = z.infer<typeof SCENARIO_SCHEMA>
|
|
34
|
+
type ScenarioInput = z.input<typeof SCENARIO_SCHEMA>
|
|
35
|
+
|
|
36
|
+
function define(input: ScenarioInput): Scenario {
|
|
37
|
+
return SCENARIO_SCHEMA.parse(input)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function problems(error: z.ZodError): string {
|
|
41
|
+
return error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A spec file is written by hand in another repository, so a mistake is reported as the fields that
|
|
45
|
+
// are wrong rather than as a stack trace.
|
|
46
|
+
function parse(json: string): Scenario {
|
|
47
|
+
const result = SCENARIO_SCHEMA.safeParse(JSON.parse(json))
|
|
48
|
+
|
|
49
|
+
if (!result.success) throw new Error(`Invalid scenario spec — ${problems(result.error)}`)
|
|
50
|
+
|
|
51
|
+
return result.data
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fresh_machine_spec = { define, parse }
|
|
55
|
+
|
|
56
|
+
export { fresh_machine_spec }
|
|
57
|
+
export type { Scenario, ScenarioInput }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kit-test-backlogrun-python",
|
|
3
|
+
"requires_github": true,
|
|
4
|
+
"package_source": "tarball",
|
|
5
|
+
"requires_claude": true,
|
|
6
|
+
"files": {
|
|
7
|
+
"pyproject.toml": "[project]\nname = \"greeting\"\nversion = \"0.1.0\"\nrequires-python = \">=3.9\"\n",
|
|
8
|
+
"main.py": "def main() -> None:\n print(\"Hello\")\n\n\nif __name__ == \"__main__\":\n main()\n"
|
|
9
|
+
},
|
|
10
|
+
"commands": [
|
|
11
|
+
"gh auth setup-git",
|
|
12
|
+
"git config --global user.name kit-test && git config --global user.email kit-test@example.com",
|
|
13
|
+
"printf '{ \"private\": true }\\n' > package.json",
|
|
14
|
+
"cp \"$PACKAGE_TARBALL\" . && pnpm add -D --allow-build=esbuild --allow-build=unrs-resolver \"./$(basename \"$PACKAGE_TARBALL\")\"",
|
|
15
|
+
"pnpm exec josh start --yes --github",
|
|
16
|
+
"pnpm josh profile | grep -x 'profile: basic (package.json josh.profile)'",
|
|
17
|
+
"grep -q '\"workflow\": \"github\"' package.json && test -f .claude/settings.json",
|
|
18
|
+
"curl -fsSL https://claude.ai/install.sh | bash && export PATH=\"$HOME/.local/bin:$PATH\" && claude --version",
|
|
19
|
+
"gh issue create --title 'Print a greeting with a name in main.py' --body 'Make main.py print \"Hello, Josh\" instead of \"Hello\".' --label auto-ok --label run:lane",
|
|
20
|
+
"timeout 60 pnpm josh backlogrun > \"$HOME/backlogrun.out\" 2>&1; cat \"$HOME/backlogrun.out\"; grep -q '^backlogrun: started' \"$HOME/backlogrun.out\"",
|
|
21
|
+
"pnpm josh run:event --from 0 | grep 'starting session'",
|
|
22
|
+
"for i in $(seq 1 90); do pnpm josh run:event --from 0 | grep -q 'dispatching' && break; sleep 10; done; pnpm josh run:event --from 0 | grep 'dispatching'",
|
|
23
|
+
"for i in $(seq 1 120); do test \"$(gh pr list --state all --json number --jq length)\" -gt 0 && break; sleep 10; done; pnpm josh run:board --chat",
|
|
24
|
+
"pnpm josh backlogrun --stop"
|
|
25
|
+
],
|
|
26
|
+
"probes": [
|
|
27
|
+
"tail -n 40 \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\" | cut -c1-400",
|
|
28
|
+
"grep -c 'requires approval' \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\"",
|
|
29
|
+
"grep -c 'run:carry --begin' \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\"",
|
|
30
|
+
"tail -n 40 /tmp/josh-lane-dispatch-*.log | cut -c1-400",
|
|
31
|
+
"cat /tmp/josh-lane-dispatch-*.log | grep -c 'requires approval'",
|
|
32
|
+
"gh pr list --state all"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kit-test-backlogrun",
|
|
3
|
+
"requires_github": true,
|
|
4
|
+
"package_source": "tarball",
|
|
5
|
+
"requires_claude": true,
|
|
6
|
+
"files": {
|
|
7
|
+
"index.html": "<!doctype html><html><body><h1>Hello</h1></body></html>\n"
|
|
8
|
+
},
|
|
9
|
+
"commands": [
|
|
10
|
+
"gh auth setup-git",
|
|
11
|
+
"git config --global user.name kit-test && git config --global user.email kit-test@example.com",
|
|
12
|
+
"printf '{ \"private\": true }\\n' > package.json",
|
|
13
|
+
"cp \"$PACKAGE_TARBALL\" . && pnpm add -D --allow-build=esbuild --allow-build=unrs-resolver \"./$(basename \"$PACKAGE_TARBALL\")\"",
|
|
14
|
+
"pnpm exec josh start --yes --github",
|
|
15
|
+
"gh label list --limit 100 --json name --jq '.[].name' > \"$HOME/labels.txt\"; grep -xE 'auto-ok|needs-decision|needs-human-review|already-done|review-round2-skipped|bug' \"$HOME/labels.txt\"; test \"$(grep -cxE 'auto-ok|needs-decision|needs-human-review|already-done|review-round2-skipped|bug' \"$HOME/labels.txt\")\" -eq 6",
|
|
16
|
+
"curl -fsSL https://claude.ai/install.sh | bash && export PATH=\"$HOME/.local/bin:$PATH\" && claude --version",
|
|
17
|
+
"gh issue create --title 'Add a heading to index.html' --body 'Add an h2 under the h1.' --label auto-ok --label run:lane",
|
|
18
|
+
"timeout 60 pnpm josh backlogrun > \"$HOME/backlogrun.out\" 2>&1; cat \"$HOME/backlogrun.out\"; grep -q '^backlogrun: started' \"$HOME/backlogrun.out\"",
|
|
19
|
+
"pnpm josh run:event --from 0 | grep 'starting session'",
|
|
20
|
+
"for i in $(seq 1 90); do pnpm josh run:event --from 0 | grep -q 'dispatching' && break; sleep 10; done; pnpm josh run:event --from 0 | grep 'dispatching'",
|
|
21
|
+
"for i in $(seq 1 120); do test \"$(gh pr list --state all --json number --jq length)\" -gt 0 && break; sleep 10; done; pnpm josh run:board --chat",
|
|
22
|
+
"pnpm josh backlogrun --stop"
|
|
23
|
+
],
|
|
24
|
+
"probes": [
|
|
25
|
+
"tail -n 40 \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\" | cut -c1-400",
|
|
26
|
+
"grep -c 'requires approval' \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\"",
|
|
27
|
+
"tail -n 40 /tmp/josh-lane-dispatch-*.log | cut -c1-400",
|
|
28
|
+
"cat /tmp/josh-lane-dispatch-*.log | grep -c 'requires approval'",
|
|
29
|
+
"gh pr list --state all"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -2,6 +2,7 @@ import { GATE_COMMAND } from '#scripts/josh/josh-command-types'
|
|
|
2
2
|
import { josh_verdict } from '#scripts/josh/josh-verdict'
|
|
3
3
|
import { buffered_process, type BufferedProcessResult } from '#scripts/lib/buffered-process'
|
|
4
4
|
import { status_icons } from '#scripts/lib/status-icons'
|
|
5
|
+
import { NOT_USED_MARKER, NOTHING_TO_CHECK_MARKER } from '#scripts/test/skip-marker'
|
|
5
6
|
import { test_unit_guard } from '#scripts/test/test-unit-guard'
|
|
6
7
|
import { gate_log } from './gate-log'
|
|
7
8
|
import { gate_plan } from './gate-plan'
|
|
@@ -11,7 +12,7 @@ import { gate_plan } from './gate-plan'
|
|
|
11
12
|
// about what is printed changes in the move: the four blocks, the log notice and the verdict come out
|
|
12
13
|
// exactly as before, with one addition — the per-failure action lines below.
|
|
13
14
|
|
|
14
|
-
const { FAIL_ICON, PASS_ICON } = status_icons
|
|
15
|
+
const { FAIL_ICON, PASS_ICON, WARN_ICON } = status_icons
|
|
15
16
|
|
|
16
17
|
interface GateStep {
|
|
17
18
|
label: string
|
|
@@ -76,16 +77,56 @@ function has_warnings(result: GateStepResult): boolean {
|
|
|
76
77
|
// other line still withholds.
|
|
77
78
|
const SAFE_CHAIN_BANNER = 'safe-chain is not available'
|
|
78
79
|
|
|
80
|
+
function checker_lines(result: GateStepResult): ReadonlyArray<string> {
|
|
81
|
+
return result.output.split('\n').filter((line) => !line.includes(SAFE_CHAIN_BANNER))
|
|
82
|
+
}
|
|
83
|
+
|
|
79
84
|
function has_checker_warning(result: GateStepResult): boolean {
|
|
80
85
|
if (!gate_plan.WARNING_CHECKER_LABELS.includes(result.label)) return false
|
|
81
86
|
|
|
82
|
-
return result.
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
return checker_lines(result).some((line) =>
|
|
88
|
+
WARNING_MARKERS.some((marker) => line.includes(marker)),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The record's counterpart to `is_skip_notice`, narrower for the reason `has_checker_warning` is
|
|
93
|
+
// narrower than `has_warnings`: a skip of a tool the project does not use (`NOT_USED_MARKER`) left
|
|
94
|
+
// nothing unverified, so only another skip line withholds the green record.
|
|
95
|
+
function is_unverified_skip(result: GateStepResult): boolean {
|
|
96
|
+
return checker_lines(result).some(
|
|
97
|
+
(line) => line.includes(test_unit_guard.SKIP_MARKER) && !line.includes(NOT_USED_MARKER),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// **A check the verdict counts as skipped said nothing but that it checked nothing.** `is_skip_notice`
|
|
102
|
+
// asks whether a check carries a skip notice, which is the right question for printing its body; it is
|
|
103
|
+
// the wrong one for a count, twice over. A check can skip half of
|
|
104
|
+
// itself — a `basic` project's `lint` runs Prettier and skips eslint — and calling it skipped would
|
|
105
|
+
// have the verdict say nothing ran on a gate that checked the formatting. And a check can run with
|
|
106
|
+
// nothing of the project's to look at — `exports` in a consumer, `behavior` with no transcript — which
|
|
107
|
+
// is no skip, and still left an empty `basic` project reading `2 ran`. A line that is neither notice is
|
|
108
|
+
// something a tool printed, so the check ran. A tool that ran clean and silent beside a skipped one is
|
|
109
|
+
// given a line of its own by the check (`project_checks.ran_notice`), so the reading holds for it too.
|
|
110
|
+
//
|
|
111
|
+
// **pnpm's script header is not something the check printed.** Every check is spawned as
|
|
112
|
+
// `pnpm josh <target>`, and where the project declares a `josh` script — `josh init` writes one — pnpm
|
|
113
|
+
// opens the captured output with `$ josh <target>`. Counted as a line, it made every spawned check one
|
|
114
|
+
// that ran, and `not verified` unreachable in the very project it was written for.
|
|
115
|
+
const IDLE_MARKERS: ReadonlyArray<string> = [test_unit_guard.SKIP_MARKER, NOTHING_TO_CHECK_MARKER]
|
|
116
|
+
const PNPM_SCRIPT_HEADER = '$ '
|
|
117
|
+
|
|
118
|
+
function is_idle_line(line: string): boolean {
|
|
119
|
+
return IDLE_MARKERS.some((marker) => line.includes(marker))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function is_tool_line(line: string): boolean {
|
|
123
|
+
return line.trim() !== '' && !line.startsWith(PNPM_SCRIPT_HEADER)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function is_idle(result: GateStepResult): boolean {
|
|
127
|
+
const lines = checker_lines(result).filter((line) => is_tool_line(line))
|
|
128
|
+
|
|
129
|
+
return lines.length > 0 && lines.every((line) => is_idle_line(line))
|
|
89
130
|
}
|
|
90
131
|
|
|
91
132
|
function should_print_body(result: GateStepResult, is_verbose: boolean): boolean {
|
|
@@ -144,10 +185,7 @@ function format_failure_actions(failed: ReadonlyArray<GateStepResult>): string {
|
|
|
144
185
|
|
|
145
186
|
// **A check that started past the core budget is said in the summary, not only in the log**.
|
|
146
187
|
// The wait cap admits a check that waited two minutes whatever the budget says,
|
|
147
|
-
// so a gate that ran over-subscribed would otherwise look exactly like one that fitted.
|
|
148
|
-
// local rather than in `status-icons.ts` because nothing reads this line back.
|
|
149
|
-
const WARN_ICON = '⚠'
|
|
150
|
-
|
|
188
|
+
// so a gate that ran over-subscribed would otherwise look exactly like one that fitted.
|
|
151
189
|
function format_budget_notes(results: ReadonlyArray<GateStepResult>): string {
|
|
152
190
|
const lines = results.flatMap((result) =>
|
|
153
191
|
result.budget_note === undefined
|
|
@@ -158,6 +196,24 @@ function format_budget_notes(results: ReadonlyArray<GateStepResult>): string {
|
|
|
158
196
|
return lines.length === 0 ? '' : `${lines.join('\n')}\n`
|
|
159
197
|
}
|
|
160
198
|
|
|
199
|
+
// **The verdict counts what ran apart from what was skipped, and a gate that ran nothing does not say
|
|
200
|
+
// `passed`.** It printed the planned number of checks whatever happened to them, so a `basic` project
|
|
201
|
+
// whose every check skipped read `passed (6 checks)` — the same last line as a gate that ran them all,
|
|
202
|
+
// and the skip this module keeps visible in the bodies was invisible in the one line a `tail` keeps.
|
|
203
|
+
//
|
|
204
|
+
// **A gate with no skip keeps the line it always printed.** A check with nothing to check is counted
|
|
205
|
+
// apart only beside a skip: `exports` says so in every consumer, and counting it alone would reword the
|
|
206
|
+
// verdict of every gate that skipped nothing.
|
|
207
|
+
function format_green_verdict(results: ReadonlyArray<GateStepResult>, total: string): string {
|
|
208
|
+
const has_skip = results.some((result) => is_skip_notice(result))
|
|
209
|
+
const skipped = has_skip ? results.filter((result) => is_idle(result)).length : 0
|
|
210
|
+
const ran = results.length - skipped
|
|
211
|
+
|
|
212
|
+
if (ran === 0) return josh_verdict.format_gate_unverified(skipped, total)
|
|
213
|
+
|
|
214
|
+
return josh_verdict.format_gate_passed({ ran, skipped }, total)
|
|
215
|
+
}
|
|
216
|
+
|
|
161
217
|
// The total is wall-clock for the whole command, not the sum of the four — they run concurrently,
|
|
162
218
|
// so a sum would report about three times what the caller waited.
|
|
163
219
|
//
|
|
@@ -172,7 +228,7 @@ function print_gate_summary(
|
|
|
172
228
|
const total = format_seconds(report.elapsed_ms)
|
|
173
229
|
const verdict =
|
|
174
230
|
failed.length === 0
|
|
175
|
-
?
|
|
231
|
+
? format_green_verdict(results, total)
|
|
176
232
|
: josh_verdict.format_gate_failed(failed.map((result) => result.label).join(', '), total)
|
|
177
233
|
const notes = format_budget_notes(results)
|
|
178
234
|
|
|
@@ -185,7 +241,6 @@ interface GateReport {
|
|
|
185
241
|
is_verbose: boolean
|
|
186
242
|
log_path: string | undefined
|
|
187
243
|
elapsed_ms: number
|
|
188
|
-
step_count: string
|
|
189
244
|
}
|
|
190
245
|
|
|
191
246
|
// Everything a finished run says, in the order a reader meets it: the four blocks, then the log the
|
|
@@ -213,7 +268,7 @@ const gate_report = {
|
|
|
213
268
|
format_failure_actions,
|
|
214
269
|
has_checker_warning,
|
|
215
270
|
is_gate_step_failed,
|
|
216
|
-
|
|
271
|
+
is_unverified_skip,
|
|
217
272
|
report_gate_steps,
|
|
218
273
|
}
|
|
219
274
|
|
|
@@ -5,7 +5,7 @@ import { doctor_consumer } from '#scripts/doctor/doctor-consumer'
|
|
|
5
5
|
import { project_profile } from '#scripts/init/project-profile'
|
|
6
6
|
import { ancestor_directories } from '#scripts/lib/ancestor-directories'
|
|
7
7
|
import { json_value } from '#scripts/lib/json-value'
|
|
8
|
-
import { SKIP_MARKER } from '#scripts/test/skip-marker'
|
|
8
|
+
import { NOT_USED_MARKER, SKIP_MARKER } from '#scripts/test/skip-marker'
|
|
9
9
|
import { z } from 'zod'
|
|
10
10
|
|
|
11
11
|
const PACKAGE_JSON = 'package.json'
|
|
@@ -126,8 +126,17 @@ function missing_tool_reason(
|
|
|
126
126
|
return `${package_name} is not installed`
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// Every skip reason here names a tool the project does not use — undeclared, or with no files or no
|
|
130
|
+
// configuration to read — so the notice says so (`NOT_USED_MARKER`).
|
|
129
131
|
function skip_notice(check: string, reason: string): string {
|
|
130
|
-
return `josh ${check}: ${reason} ${SKIP_MARKER} ${check}.`
|
|
132
|
+
return `josh ${check}: ${reason} ${SKIP_MARKER} ${check}, ${NOT_USED_MARKER}.`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// **A tool that ran clean beside a skipped one says that it ran.** A clean eslint prints nothing, so a
|
|
136
|
+
// `lint` whose Prettier was skipped read as nothing but a skip notice — the same output as a `lint`
|
|
137
|
+
// that ran neither — and the gate's verdict counted a check that linted the whole tree as skipped.
|
|
138
|
+
function ran_notice(check: string): string {
|
|
139
|
+
return `josh ${check}: ran and reported no problems.`
|
|
131
140
|
}
|
|
132
141
|
|
|
133
142
|
// Shared by `josh check` and the gate's type-check step, so neither reaches `tsc` on a basic
|
|
@@ -173,6 +182,8 @@ const project_checks = {
|
|
|
173
182
|
is_kit_repository,
|
|
174
183
|
prettier_skip_reason,
|
|
175
184
|
project_root,
|
|
185
|
+
ran_notice,
|
|
186
|
+
read_manifest,
|
|
176
187
|
skip_notice,
|
|
177
188
|
type_check_skip_reason,
|
|
178
189
|
}
|
|
@@ -64,7 +64,8 @@ interface ScopedSources {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// **`test:related` is required only where it can ever answer**. In a project
|
|
67
|
-
// whose unit guard skips — vitest absent
|
|
67
|
+
// whose unit guard skips — vitest absent with no project test script to run in its place, or a basic
|
|
68
|
+
// project with no test file — `test:related` runs
|
|
68
69
|
// nothing and so writes no record, and requiring one refused the gate forever with no way through.
|
|
69
70
|
// The guard's own decision is asked rather than re-derived, so the writer and this reader agree; a
|
|
70
71
|
// project with a unit suite still needs the record, and a run that executed nothing still writes none.
|
|
@@ -63,7 +63,9 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
|
|
|
63
63
|
// passed *without running* — `test-unit-guard` exits 0 with a notice when vitest is absent or the
|
|
64
64
|
// project has no tests — must not become "the unit tests all passed": the gate keeps that skip
|
|
65
65
|
// visible on the console, and a record erasing it would have the brief tell a review agent not to
|
|
66
|
-
// re-run tests that never ran. A
|
|
66
|
+
// re-run tests that never ran. A skip of a tool the project does not use (`NOT_USED_MARKER` — eslint
|
|
67
|
+
// with no ESLint configuration) is not one: nothing went unverified, so it withholds nothing
|
|
68
|
+
// (`gate_report.is_unverified_skip`). A step that passed **with a checker's warnings** is withheld for the
|
|
67
69
|
// same reason one place further on: since the record is reused instead of the checks being re-run, a
|
|
68
70
|
// warning printed once would never be printed again on that tree, and hiding an eslint or
|
|
69
71
|
// svelte-check finding is the same failure as hiding a skip. It is `has_checker_warning`, not
|
|
@@ -80,7 +82,7 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
|
|
|
80
82
|
// The destination is a parameter so a test can exercise the record without overwriting the one a
|
|
81
83
|
// real run may be relying on — `josh gate` and `josh review:brief` share one path by design.
|
|
82
84
|
function has_nothing_to_say(result: GateStepResult): boolean {
|
|
83
|
-
return !gate_report.
|
|
85
|
+
return !gate_report.is_unverified_skip(result) && !gate_report.has_checker_warning(result)
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
async function record_green_gate(
|
|
@@ -362,7 +364,6 @@ async function settle_gate(
|
|
|
362
364
|
is_verbose: options.is_verbose ?? false,
|
|
363
365
|
log_path: options.log_path,
|
|
364
366
|
elapsed_ms,
|
|
365
|
-
step_count: String(plan.checks.length),
|
|
366
367
|
})
|
|
367
368
|
const is_passed = failed_labels.length === 0
|
|
368
369
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Whose words kit acts on: the `author_association` REST reports with an issue or a comment, and the
|
|
2
|
+
// ones that can write to the repository. On a public repository anyone can file an issue or comment,
|
|
3
|
+
// so a body kit runs (`josh measure:rerun`) or copies into the ledger (`review:record`) is taken only
|
|
4
|
+
// from an author with write access — and an unknown author is not a trusted one.
|
|
5
|
+
const TRUSTED_ASSOCIATIONS: ReadonlySet<string> = new Set(['OWNER', 'MEMBER', 'COLLABORATOR'])
|
|
6
|
+
const TRUSTED_LIST = [...TRUSTED_ASSOCIATIONS].join(' / ')
|
|
7
|
+
|
|
8
|
+
function is_trusted(association: string | null | undefined): boolean {
|
|
9
|
+
return typeof association === 'string' && TRUSTED_ASSOCIATIONS.has(association)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const gh_author_association = { TRUSTED_LIST, is_trusted }
|
|
13
|
+
|
|
14
|
+
export { gh_author_association }
|