@joshuafolkken/kit 1.1383.0 → 1.1435.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
- package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
- package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
- package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
- package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +262 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +124 -15
- package/dist/hooks/stop-guard.js +98 -55
- package/dist/josh.js +63 -10
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-cli.ts +2 -4
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +5 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-owner.ts +72 -5
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +80 -5
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +30 -4
- package/scripts/init/start-steps.ts +77 -3
- package/scripts/init/start.ts +21 -13
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +25 -1
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +139 -37
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +58 -5
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +121 -0
- package/scripts/run/board/run-board-span.ts +49 -7
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +76 -13
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +53 -16
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-added.ts +1 -1
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-change.ts +1 -1
- package/scripts/run/carry/run-carry-cli.ts +39 -52
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry-ended.ts +9 -1
- package/scripts/run/carry/run-carry.ts +24 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +3 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +91 -0
- package/scripts/run/event/run-event-stream.ts +46 -9
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/run-stranded-detect.ts +58 -12
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +108 -74
- package/scripts/run/wake/run-wake-describe.ts +36 -2
- package/scripts/run/wake/run-wake-driver.ts +21 -5
- package/scripts/run/wake/run-wake-handoff.ts +1 -1
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
2
|
+
import { fresh_machine_spec, type Scenario } from './fresh-machine-spec'
|
|
3
|
+
|
|
4
|
+
// kit's own six patterns: three projects (one Lua file, one `index.html`, a minimal TypeScript
|
|
5
|
+
// SvelteKit app from `sv`) by two commands (`josh init`, `josh start`), each from the starting state
|
|
6
|
+
// joshuafolkken/kit#2848 gave it. `josh init` installs the packed checkout, so a fix is retested
|
|
7
|
+
// without a release; `josh start` installs a published version, because its setup pull request
|
|
8
|
+
// would carry a tarball path CI cannot resolve.
|
|
9
|
+
|
|
10
|
+
const KIT = KIT_PACKAGE_NAME
|
|
11
|
+
const ALLOW_BUILDS = '--allow-build=esbuild --allow-build=unrs-resolver'
|
|
12
|
+
// What `pnpm dlx @joshuafolkken/kit init` writes before it installs kit.
|
|
13
|
+
const BARE_MANIFEST = String.raw`printf '{ "private": true }\n' > package.json`
|
|
14
|
+
const CREATE_SVELTEKIT =
|
|
15
|
+
'pnpm dlx sv create --template minimal --types ts --no-add-ons --install pnpm .'
|
|
16
|
+
// The `sv` that created the project is the one `pnpm dlx` resolved, so its version is recorded.
|
|
17
|
+
const SV_VERSION = 'pnpm dlx sv --version'
|
|
18
|
+
// The app builds and type-checks before kit is installed and still does after it.
|
|
19
|
+
const SVELTEKIT_CHECKS = 'pnpm build && pnpm check'
|
|
20
|
+
const INSTALL_TARBALL = `pnpm add -D ${ALLOW_BUILDS} "$PACKAGE_TARBALL"`
|
|
21
|
+
// A version published minutes ago is inside pnpm's release-age wait; kit alone is excluded from it
|
|
22
|
+
// (joshuafolkken/kit#2869), so every other dependency keeps the wait.
|
|
23
|
+
const EXCLUDE_RELEASE_AGE = String.raw`printf 'minimumReleaseAgeExclude:\n - "${KIT}"\n' >> pnpm-workspace.yaml`
|
|
24
|
+
const INSTALL_PUBLISHED = `pnpm add -D ${ALLOW_BUILDS} ${KIT}@"$PACKAGE_VERSION"`
|
|
25
|
+
const GATE = 'pnpm josh gate'
|
|
26
|
+
const INIT_COMMANDS = [INSTALL_TARBALL, 'pnpm exec josh init', GATE]
|
|
27
|
+
// `josh start` leaves the project on its setup branch, where the gate asks for the scoped checks
|
|
28
|
+
// first; this is the line its refusal hands back.
|
|
29
|
+
const SCOPED_CHECKS = 'pnpm josh lint:related && pnpm josh test:related'
|
|
30
|
+
const START_COMMANDS = [
|
|
31
|
+
EXCLUDE_RELEASE_AGE,
|
|
32
|
+
INSTALL_PUBLISHED,
|
|
33
|
+
'pnpm exec josh start --yes --github',
|
|
34
|
+
SCOPED_CHECKS,
|
|
35
|
+
GATE,
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
// `gh auth login` cannot run without a person, and it is what points git at gh for credentials; with
|
|
39
|
+
// the token in the environment this is the same step.
|
|
40
|
+
const SIGN_IN = 'gh auth setup-git'
|
|
41
|
+
// What the prerequisites guide has a person run once per machine. Only the pattern with no repository
|
|
42
|
+
// uses it: `josh start` makes the first commit there, and stops before writing without it (#3754).
|
|
43
|
+
const MACHINE_IDENTITY =
|
|
44
|
+
'git config --global user.name kit-test && git config --global user.email kit-test@example.com'
|
|
45
|
+
// The identity is the repository's own, so the machine stays one nobody has configured git on.
|
|
46
|
+
const FIRST_COMMIT = [
|
|
47
|
+
'git init --initial-branch main',
|
|
48
|
+
'git config user.name kit-test && git config user.email kit-test@example.com',
|
|
49
|
+
'git add --all && git commit --message "Initial commit"',
|
|
50
|
+
]
|
|
51
|
+
const CREATE_REPOSITORY = 'gh repo create "$RUN_NAME" --private --source . --push'
|
|
52
|
+
|
|
53
|
+
// The two states #3712 was found in: a shell that reads no rc file, and a PATH with `node` on it and
|
|
54
|
+
// no `pnpm`.
|
|
55
|
+
const BARE_SHELL =
|
|
56
|
+
'env -i HOME="$HOME" PATH=/usr/local/bin:/usr/bin:/bin bash --noprofile --norc -c'
|
|
57
|
+
const NODE_ONLY = '"$HOME/node-only"'
|
|
58
|
+
const PATH_PROBES = [
|
|
59
|
+
`${BARE_SHELL} '${GATE}'`,
|
|
60
|
+
`mkdir -p ${NODE_ONLY} && ln -sf "$(command -v node)" ${NODE_ONLY}/node && ${BARE_SHELL} 'PATH="$HOME/node-only:$PATH" ./node_modules/.bin/josh gate'`,
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
// What a `josh init` run is checked against after the gate: the profile it chose, the files it left
|
|
64
|
+
// (a `./.git` line is a repository it should not have made), and a second run that changes nothing.
|
|
65
|
+
const PROJECT_FILES =
|
|
66
|
+
'find . -name node_modules -prune -o -name .git -prune -o -type f -print | sort'
|
|
67
|
+
const CHECKSUMS = `${PROJECT_FILES} | xargs sha256sum`
|
|
68
|
+
const INIT_PROBES = [
|
|
69
|
+
'pnpm josh profile',
|
|
70
|
+
'find . -name node_modules -prune -o -print | sort',
|
|
71
|
+
`${CHECKSUMS} > "$HOME/before-second-init.sha" && pnpm exec josh init && ${CHECKSUMS} | diff "$HOME/before-second-init.sha" -`,
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
const LUA_FILE = 'main.lua'
|
|
75
|
+
const HTML_FILE = 'index.html'
|
|
76
|
+
const LUA_FILES = { [LUA_FILE]: 'print("Hello")\n' }
|
|
77
|
+
const HTML_FILES = { [HTML_FILE]: '<!doctype html><html><body><h1>Hello</h1></body></html>\n' }
|
|
78
|
+
|
|
79
|
+
function init_pattern(
|
|
80
|
+
project: string,
|
|
81
|
+
files: Record<string, string>,
|
|
82
|
+
setup: ReadonlyArray<string>,
|
|
83
|
+
project_probes: ReadonlyArray<string> = [],
|
|
84
|
+
): Scenario {
|
|
85
|
+
return fresh_machine_spec.define({
|
|
86
|
+
name: `kit-test-${project}-init`,
|
|
87
|
+
files,
|
|
88
|
+
commands: [...setup, ...INIT_COMMANDS],
|
|
89
|
+
probes: [...INIT_PROBES, ...project_probes, ...PATH_PROBES],
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function start_pattern(
|
|
94
|
+
project: string,
|
|
95
|
+
files: Record<string, string>,
|
|
96
|
+
setup: ReadonlyArray<string>,
|
|
97
|
+
): Scenario {
|
|
98
|
+
return fresh_machine_spec.define({
|
|
99
|
+
name: `kit-test-${project}-start`,
|
|
100
|
+
requires_github: true,
|
|
101
|
+
package_source: 'published',
|
|
102
|
+
files,
|
|
103
|
+
commands: [SIGN_IN, ...setup, ...START_COMMANDS],
|
|
104
|
+
probes: PATH_PROBES,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const PATTERNS: ReadonlyArray<Scenario> = [
|
|
109
|
+
init_pattern('lua', LUA_FILES, [BARE_MANIFEST]),
|
|
110
|
+
// No Git: `josh start` creates the repository, the first commit and the GitHub repository.
|
|
111
|
+
start_pattern('lua', LUA_FILES, [MACHINE_IDENTITY, BARE_MANIFEST]),
|
|
112
|
+
init_pattern('html', HTML_FILES, [BARE_MANIFEST]),
|
|
113
|
+
// Git with a first commit on `main`, no GitHub repository.
|
|
114
|
+
start_pattern('html', HTML_FILES, [...FIRST_COMMIT, BARE_MANIFEST]),
|
|
115
|
+
init_pattern('svelte', {}, [CREATE_SVELTEKIT, SV_VERSION, SVELTEKIT_CHECKS], [SVELTEKIT_CHECKS]),
|
|
116
|
+
// A GitHub repository with `main` already pushed.
|
|
117
|
+
start_pattern('svelte', {}, [CREATE_SVELTEKIT, ...FIRST_COMMIT, CREATE_REPOSITORY]),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
function names(): Array<string> {
|
|
121
|
+
return PATTERNS.map((pattern) => pattern.name)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function find(name: string): Scenario | undefined {
|
|
125
|
+
return PATTERNS.find((pattern) => pattern.name === name)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const fresh_machine_patterns = { names, find }
|
|
129
|
+
|
|
130
|
+
export { fresh_machine_patterns }
|
|
@@ -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
|
|