Repository navigation
fix(ci): fetch each commit's workflow once in the Bun crash census and skip comment lines in jobs - #489
Merged
Conversation
…le CI test job docs Since #442 the `test` job is only the required fan-in of the four `test shard i/4` jobs. It runs no Bun, but `ci:bun-crash-census` still read it by default and reported zero crashes without saying so, and `workflowJobBunVersion(ci.yml, "test")` found the fan-in block, which has no setup-bun. - With no --job, the census reads every `test shard i/n` job of a run, or the `test` job of a run that has no shard job (a run from before the split, whose `test` job ran the suite). - --job accepts `/` and a `*` pattern, so the documented `--job "test shard 1/4"` and `--job "test shard *"` work; before, both were refused as an invalid argument. - The workflow lookup matches a job by its `name:` template, so `test shard 2/4` finds the `test-shard` matrix job's Bun pin. - The report prints a closed `warning:` line (also on stderr, and in --json) when no job matched or a selected job shows no Bun setup in its log or workflow block. - docs/security/env-secrets-threat-model.md and docs/reference/shared-host-lifecycle.md now say the tests run in a `test shard i/4` job and that `test` is the fan-in. Refs HACK-1424 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d skip comment lines in jobs The census resolved a job's Bun version from the workflow file at the run's head commit through a cache keyed by (sha, job), so one commit cost one contents GET per distinct job name. It now caches the decoded workflow text per sha and keeps the per-(sha, job) version memo on top, with a comment on why the key must not be sha alone. workflowJobBunVersion ended the jobs scan at any column-0 line, so a column-0 YAML comment inside jobs: hid every later job. Blank lines and column-0 comments are now skipped; any other column-0 line still ends jobs. Tests pin the (sha, job) key with a wildcard selector over the fan-in and shards at one sha, count contents fetches across runs and concurrency, and cover comment, blank, commented-out-job and later top-level-key lines. Refs HACK-1441 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
marked this pull request as ready for review
October 24, 2026 21:45
roodboi
added a commit
that referenced
this pull request
Oct 24, 2026
…ow parser (#503) ## Summary Two small follow-ups to #489 in `workflowJobBunVersion` (`scripts/ci-bun-crash-census.ts`). ### Root cause 1. **Commented `jobs:` line.** The parser found the jobs section with `line.trimEnd() === "jobs:"`. A valid workflow whose `jobs:` line has a trailing YAML comment (`jobs: # all jobs`) therefore had no jobs, and every job read as having no Bun version. 2. **Dead blank-line clause.** The job scan skipped lines with `line.trim() === "" || line.startsWith("#")`. Removing the blank-line half changed no result: a blank or whitespace-only line has no non-space first character (so it does not end the jobs) and matches none of the job-key, `name:` or `bun- version` patterns (so it does not change a block). No test can fail without it, so the JSDoc claim that blank lines are "skipped" described code that did nothing. ### Fix - Strip a whitespace-led trailing comment (`/\s+#.*$/`) before matching `jobs:`. A `#` with no space before it (`jobs:#`) is not a YAML comment and still does not match. - Remove the redundant blank-line clause and correct the JSDoc to say why blank lines need no skip. ### Tests - New: `a trailing comment on the jobs: line still opens the jobs (HACK-1462)`. It covers `jobs: # all jobs` with a whitespace-only line inside the job, plus the `jobs:#` negative case. - Existing: `blank and column-0 comment lines inside jobs: do not end the job scan` already has blank lines between jobs and still holds the documented behavior. ### Evidence Focused suite `tests/ci-bun-crash-census.test.ts`: 20 pass, 0 fail. Mutation results: | Mutation | Result | | --- | --- | | M1: match `jobs:` with plain `trimEnd()` (pre-fix) | new test fails (19 pass, 1 fail) | | M2: comment regex `\s*#` (no whitespace needed) | new test fails on the `jobs:#` case | | M3: a blank line ends the jobs scan | 4 tests fail, including the blank-line test and the real `ci.yml` test | | M4: put back the removed blank-line clause | 20 pass: the clause makes no difference to behavior | Gates: root `bun run check` (includes `privacy:check`) passes with exit 0. `bun run --cwd packages/cli typecheck` has 0 errors. Biome reports no issues on both changed files. gitleaks 8.30.1 found no leaks in `origin/next..HEAD`. The CLI surface and agent guidance did not change, so neither needs regeneration. For the full suite, see hosted CI `test` at this head. ### Gaps - Job-key lines (` test: # note`) and `name:` lines with trailing comments are still not handled. They are outside this unit's scope and are reported separately. Refs HACK-1462 🤖 Generated with [Claude Code](https://claude.com/claude-code)
roodboi
added a commit
that referenced
this pull request
Oct 24, 2026
… or non-ASCII task programs (HACK-1438, HACK-1461) (#490) Refs HACK-1438 ## Root cause The task-catalog denylist (`isInlineCommandArgv` in `src/lib/task-catalog-recipe.ts`) did not model the `git send-email` options that run a command. On `next` (1281184), every one of these was accepted: - `--to-cmd`, `--cc-cmd`, `--header-cmd` and `--sendmail-cmd`, in both the `--opt cmd` and `--opt=cmd` forms. - `--smtp-server=/abs/path`. send-email runs an absolute path given here as a sendmail program. A second reason it was open: `git send-email` is a Perl script that reads its options with Getopt::Long's defaults (`pass_through` only). Getopt::Long's grammar is wider than Git's parse- options. It accepts `--`, `-` or `+` before a long name, ignores letter case, and accepts any unique prefix. So even a rule written for `--to-cmd` would miss `-to-cmd=cmd`, `+to-cmd=cmd` and `--CC-CMD=cmd`. I confirmed each spelling against real Git 2.56 in a scratch repo. Each one ran a marker command: `--to-cmd X`, `--to-cmd=X`, `--cc-cmd=X`, `--header-cmd=X`, `--sendmail- cmd=X`, `--smtp-server=/abs`, `--cc-cm=X`, `--hea=X`, `--sendm=X`, `--to-cm=X`, `--CC-CMD=X`, `-cc-cmd=X`, `-to-cmd=X`, `-header-cmd X`, `-sendmail-cmd=X`, `+to-cmd=X`, `-smtp-server=/abs`, and `git Send-Email --cc-cmd=X`. The last one works because APFS is case-insensitive, so Git finds the `git-send-email` file for `Send-Email`. The `-c sendemail.ccCmd=…` / `toCmd` / `headerCmd` / `sendmailCmd` / `smtpServer` config forms also ran, including identity-scoped `sendemail.<id>.ccCmd`. They were already refused, because `git -c` and `--config-env` are refused whatever the key. This PR adds tests for them. ## Fix (`src/lib/task-catalog-recipe.ts`) - **New `subcommandGetoptLong` table** on the evaluator, plus a `getoptLongSelects` matcher. The matcher strips a `--`, `-` or `+` prefix, takes the name up to `=`, and lowercases it. It refuses the argument when the name is a prefix of an inline option. The one exception is a name that exactly matches another option (`to`, `cc`, `h`), because Getopt::Long takes an exact name as that option. `git send-email` gets `to-cmd`, `cc-cmd`, `header-cmd`, `sendmail- cmd` and `smtp-server`. - **Subcommand scoping.** The table is scoped to the `send-email` operand the same way `subcommandInline` is, so other commands are not refused (`git rev-list --header`, `git status -s`, `git format-patch --to=…`, `git log --cc`). The dashed form (`git-send-email`, `/usr/lib/git-core/git-send-email`) goes through the existing dashed-builtin mapping. - **Case-insensitive lookup.** Both subcommand-keyed tables now look up operands lowercased, because of the case-insensitive filesystem point above. - **Sweep additions**, each refused the same way and each confirmed against real Git where it is installed (see the sweep below): - `grep -O` / `--open-files-in-pager` (scoped to `grep`) - `daemon --access-hook` (scoped to `daemon`) - `instaweb -d` / `--httpd` (scoped to `instaweb`) - `svn --authors-prog` (scoped to `svn`) - the `remote-ext` word - `ext::` URLs (new `extUrls` flag) - the `archimport` word ## Tests (`tests/project-task-catalog.test.ts`) - **`HACK-1438: git send-email options that run a command`** (gap case). - Refused: every spelling listed above, plus the dashed, absolute-path, behind-`--git-dir` and mixed-case forms. Also refused: the `-c` config forms in any key case, identity-scoped and `smtpServer` keys, `--config-env`, and `-c sendemail.ccCmd=… -c alias.se=send-email se`, which only the `-c` rule catches. - Accepted: `--to=… --cc …`, `--suppress-cc=all`, `--sender=…`, `--no-header-cmd`, `--smtp- server-port=587`, `--no-to`, `-h`, and the same option names under other subcommands. - **`HACK-1438: Git porcelain options that run a command`** (gap case). Covers the sweep additions, with scoped accepted controls: `git grep -n`, `git grep --only-matching --or`, `git branch -d`, `git svn fetch --authors-file=…`, `git daemon --export-all`, `git instaweb --stop`, an `https://` remote and `git add extension.txt`. - **`HACK-1438: documented send-email and grep fail-closed collateral stays refused`.** Covers `send-email -s`, `--smtp`, `--smtp-server=mail`, `git grep -o`, and a path operand `Grep` scoping `-O`. ## Evidence - **Before and after.** I probed `isInlineCommandArgv` directly with 24 argvs that should be refused. On `next`, 22 of them were accepted; only the 2 `-c`/`--config-env` forms were refused. After the change, all 24 are refused, and the probed controls (`--to=…`, `--cc=…`, `git log --cc`, `git format-patch --to=…`) are still accepted. - **Mutation testing.** I applied 25 mutations one at a time, ran `-t HACK-1438` after each, and then restored the source. Every mutation fails at least one new test: | Mutation | Failing tests | | --- | --- | | Drop the Getopt::Long check | 2 | | Accept only the `--` prefix | 2 | | Drop the `+` prefix | 1 | | No abbreviations | 2 | | Case- sensitive option name | 1 | | Drop the exact list | 1 | | Case-sensitive subcommand lookup | 2 | | Unscoped (every `git` argv) | 1 | | Drop each of `to-cmd`, `cc-cmd`, `header-cmd`, `sendmail-cmd` | 1 each | | Drop `smtp-server` | 2 | | Drop `git -c` | 1 | | Drop the `grep` scope | 2 | | Drop `--open-files-in-pager` | 1 | | Drop `daemon` | 1 | | Drop `instaweb -d` | 1 | | Drop `instaweb --httpd` | 1 | | Drop `svn` | 1 | | Drop the `remote- ext` word | 1 | | Drop the `archimport` word | 1 | | Drop `ext::` URLs | 1 | | Case- sensitive `ext::` | 1 | | Unscoped `subcommandInline` | 1 | - **Focused tests.** `bun test tests/project-task-catalog.test.ts` reported 40 pass, 0 fail. It ran with a private HOME, HACK_HOME and DOCKER_CONFIG and `HACK_SECRETS_DISABLE_KEYCHAIN_FALLBACK=1`, and with `--timeout 60000`. The run with the default 5 s timeout hit 3 timeouts in slow existing domain tests (tracked pins, stale service definition, edit during read) while the host load average was about 106. None of the timeouts were in the changed tests. - **Gates.** Root `bun run check` passed, including `privacy-check: ok`, with only existing warnings and none in the changed files. `bunx biome check` on the changed files was clean. `bun run --cwd packages/cli typecheck` reported 0 errors. gitleaks 8.30.1 over `origin/next..HEAD` found no leaks. - **Not regenerated.** The CLI surface and consumer agent guidance did not change, so the CLI reference and plugin bundles were not regenerated. - **Full suite.** The hosted CI `test` job at this head. ## Git porcelain sweep Sources: `--git-completion-helper-all` for every command that `git --list- cmds=builtins,main,others,nohelpers` lists on Git 2.56, plus the script sources on `git/git` master (`git-send-email.perl`, `git-svn.perl`, `perl/Git/SVN.pm`, `git-instaweb.sh`, `git-p4.py`, `git-cvsimport.perl`, `git-cvsexportcommit.perl`, `git-archimport.perl`, `git- cvsserver.perl`, `git-web--browse.sh`, `git-mergetool.sh`, `git-difftool--helper.sh`, `git- submodule.sh`, `daemon.c`, `builtin/grep.c`, `imap-send.c`). Version history (2.19 and later) comes from the existing table and #463/#475. | Option or form | Runs | Status | | --- | --- | --- | | `send-email --to-cmd/--cc-cmd/--header- cmd/--sendmail-cmd` | shell command | **added** (Getopt::Long spellings) | | `send-email --smtp- server=/abs` | sendmail program | **added** (any value, fail-closed) | | `-c`/`--config-env` `sendemail.*Cmd`, `sendemail.smtpServer`, `core.sshCommand`, `core.pager`, `core.fsmonitor`, ... | config | already refused (`-c`/`--config-env` refused whatever the key) | | `grep -O<pager>` / `--open-files-in-pager=<pager>` | shell (`use_shell`) | **added**, scoped to `grep` | | `daemon --access-hook=<cmd>` | shell (`use_shell`) | **added**, scoped to `daemon` | | `instaweb -d/--httpd=<cmd>` | the daemon command line | **added**, scoped to `instaweb` | | `svn --authors-prog=<cmd>` | backticks in `Git::SVN` | **added**, scoped to `svn` (git-svn is not installed here; read from source) | | `git remote-ext <remote> <url>`, `ext::<cmd>` URLs | the URL as a command | **added**. Git's default `protocol.ext.allow=never` blocks it, but config can allow it. `EXT::` is also blocked by Git ("cannot handle remote-EXT as a builtin"). | | `git archimport <archive/branch>` | operand pasted into `tla abrowse … $limit \|` | **added**. `git archimport 'x/y;cmd #'` ran `cmd` even without tla. | | `--upload-pack`, `--receive-pack`, `--exec` (`clone`, `fetch`, `pull`, `ls-remote`, `push`, `archive`, `fetch-pack`, `send-pack`), `clone -u`, `rebase --exec/-x`, `difftool --extcmd/-x`, `submodule foreach`, `bisect run`, `filter-branch`, `rebase--helper --add-exec-commands` | shell | already modeled. `ls-remote` has no `-u` (its `--exec` is a hidden alias and is covered). | | `rebase--interactive --cmd` (2.20–2.33) | shell | in #475 (HACK-1423), not on `next` yet | | `column --command` | config lookup only | not a command | | `for-each-ref --shell`, `cat-file --batch-command` | quoting / stdin protocol | not a command | | `difftool/mergetool -t/--tool`, `web--browse -b`, `instaweb -b` | name must be a known tool or a configured `*.cmd` | config only | | `git hook run`, `clone/init --template` (template hooks run during clone), `--exec-path`, `merge/rebase/pull -s <name>` (`git-merge-<name>` on `PATH`), `transport::address` URLs (`git-remote-<transport>`) | a program file, not a string | not modeled (documented gap) | | `git config <key> <cmd>` writes (`core.pager`, `sendemail.ccCmd`, `include.path`, ...) | config read later | not modeled (existing documented gap) | | `cvsimport -d :ext:host` (runs `$CVS_RSH host cvs server`), `p4`, `cvsserver`, `cvsexportcommit`, `imap-send` (`imap.tunnel`) | env or config | not modeled. `cvsexportcommit` rev-parses its commit operands before it uses them in shell strings. | ## Gaps - **Repository aliases.** A repository alias still hides a subcommand from its scoped flags (`git se --cc-cmd cmd` with `alias.se = send-email`). This is the existing documented limit and is out of scope. - **Fail-closed collateral, documented.** - Any prefix of a refused send-email option is refused, even one Getopt::Long finds ambiguous and passes on to `format-patch` (`send-email -s`, `-t`, `--smtp`). - `--smtp-server` is refused whatever its value. - `git grep -o` is refused, because flags are case-folded; use `--only-matching`. - Subcommand names now match in any letter case at any operand, so a path named `Grep` scopes `-O`. The same applies to the existing `clone -u` and `rebase --add-exec-commands`. - **Interaction with #475 (HACK-1423, still a draft).** #475 changes `subcommandInline` to count only after the subcommand's operand. When it is rebased onto this change (or this onto it), it should apply the same after-the-operand rule to `subcommandGetoptLong`, and keep the lowercased lookup from `scopedEntries`. The two PRs touch the same lines in `evaluatesCode` and the docs paragraph, so expect a textual conflict. - **Not modeled.** Config writes and program-file runners (table above) are not modeled. ## Round 2: merged with `next`, one scoping rule (HACK-1423 + HACK-1438) The verifier found that this PR conflicted with `next` after #475 (HACK-1423) and #480 landed. The conflict was not only textual. #475 made subcommand-scoped Git flags count only **after** the operand that names the subcommand. This PR counted them at **any** operand. Its collateral test asserted that `git log -o -- Grep` is refused, which contradicts #475. **Rule chosen: #475's.** A scoped flag counts only after the operand that names its subcommand, because Git reads a subcommand's options only after the subcommand's name. This PR's lowercased subcommand lookup is kept, so `git Send-Email --cc-cmd=cmd` is still refused. What changed (merge commit 6cf5296, then a clean merge of 78487f7 in 0556dd9): - **`src/lib/task-catalog-recipe.ts`.** `scopedFlags` (from #475) and `scopedEntries` (from this PR) are now one helper, `scopedEntries({ table, positional, at })`. It returns the table entries for operands **before** argument `at`, and it looks up each operand lowercased. Both `subcommandInline` and `subcommandGetoptLong` use it, so the send-email Getopt::Long options follow the same after-the-subcommand rule as `clone -u`. #475's `--cmd` inline flag and its comments are kept. - **Tests.** The collateral test now asserts `git log -- Grep -o` is refused and `git log -o -- Grep` passes. New accepted controls where the flag comes before the subcommand word: `git rev- list --header send-email`, `git log -s -- Send-Email`, `git diff -Oorderfile -- grep`, `git push -d origin instaweb`. Every send-email spelling after `send-email` is still refused. New cases kill the two mutations that survived round 1: `git send-email --to=… -- p` and `git send-email --to=… -` pass (`--` and `-` name no option), `git push --repo=ext::cmd` is refused (`ext::` after `=`), and `git add docs/ext::notes.md` passes (`ext::` only at the start of an argument or after `=`). - **Docs (`docs/reference/ax-task-catalog.md`).** The contract table keeps both #475's rows and this PR's rows. The scoping row now lists `git rev-list --header send-email`, `git push -d origin instaweb` and `git log -o -- Grep` as accepted, and `git log -- Grep -o` as refused. The limitations paragraph uses the single after-the-subcommand rule, says the lookup ignores letter case, and drops the round-1 text ("matched at any operand ... a path named `Grep` scopes those flags to it"). The "Interaction with #475" gap and the "any operand" collateral bullet above are now resolved by this round. ### Evidence (round 2) - **Focused tests.** `bun test tests/project-task-catalog.test.ts`: 42 pass, 0 fail. Run with a private HOME, HACK_HOME and DOCKER_CONFIG and `HACK_SECRETS_DISABLE_KEYCHAIN_FALLBACK=1`. - **Mutation testing.** I applied each mutation alone, ran the focused file, and restored the source: | Mutation | Failing tests | | --- | --- | | Scoped flags count at any operand (drop the `at` check) | 5 | | Case-sensitive subcommand lookup | 2 | | `subcommandGetoptLong` counts at any operand | 1 | | `subcommandInline` counts at any operand | 4 | | Allow an empty Getopt::Long name (`--`, `-`) | 1 (survived round 1) | | `ext::` matched only at the start of an argument | 1 (survived round 1) | | `ext::` matched anywhere in an argument | 1 | | Drop `h` from the send-email exact list | 1 | | Getopt::Long without the one-dash prefix | 2 | | `index > at` instead of `index >= at` | 0, an equivalent mutation: the subcommand word itself cannot match a flag | - **Gates.** Root `bun run check` exit 0, with `privacy-check: ok`. `bun run --cwd packages/cli typecheck` exit 0, with 0 errors. gitleaks 8.30.1 `dir` over a `git log -p origin/next..HEAD` export (next = 78487f7): no leaks. The CLI surface and consumer agent guidance did not change, so nothing was regenerated. - **Full suite.** Hosted CI at 0556dd9, which contains `next` 78487f7. ## Round 3: Unicode case folding no longer bypasses the denylist (HACK-1461), merged with `next` Refs HACK-1461 The round-2 verifier found that APFS (and NTFS) look up names with Unicode case folding, but the denylist folded names with JavaScript's `toLowerCase()`. So `git ſend-email --to-cmd=cmd` (U+017F long s), `git-ſend-email`, `git inſtaweb --httpd=cmd`, `git ſvn clone --authors- prog=cmd` and `git filter-branch --tree-filter cmd` (U+FB01 fi ligature) were accepted. HACK-1461 records that two of them ran a marker command live on macOS. What changed (7483db3, on top of the merge of `next` cd35034 in 84d0379): - **`src/lib/task-catalog-recipe.ts`.** - A new `foldName` replaces `toLowerCase()` everywhere the denylist compares a name: program basenames (`programName`), Git helper porcelains (`gitSubcommand`), the subcommand-scoped tables (`scopedEntries`), the `words` check, first-operand `subcommands`, and Python REPL/trampoline modules (`isListedModule`). - `foldName` over-folds on purpose (fail-closed). It applies NFKC (`fi` becomes `fi`, `ſ` becomes `s`, the Kelvin sign becomes `K`), removes default-ignorable code points such as a zero-width space, then upper-cases before lower-casing (so `ß` becomes `ss` and dotless `ı` becomes `i`). - Option names keep ASCII-only folding. Git's parse-options and Getopt::Long compare options bytewise apart from ASCII letter case, and option values are left unfolded. - Two outright refusals in `evaluatesCode`, so the result does not depend on any one filesystem's folding table: - A listed program whose basename contains any non-ASCII character is refused whatever follows (`ſh scripts/x.sh`, `git-ſtatus`). - A Git subcommand (the first operand) that contains any non-ASCII character is refused (`git ſtatus`, `git --work-tree dir pack-refſ`). - Non-ASCII operands and values (`git commit -m 'café'`, `git log --author=Zoë`) and unlisted programs (`cat ſend-email`) still pass. - **Tests (`tests/project-task-catalog.test.ts`).** New test `HACK-1461: names that a case- insensitive filesystem folds to a listed name are refused`: - 18 refused spellings covering the long s, the fi ligature, the Kelvin sign (`Ksh -c`, `awK`), dotless i (`git ınstaweb`, `ıex`), folding after the first operand (`git log filter- branch`, `git log bıſect`, `git help ſend-email --to-cmd=…`, `git log filter-branch`, `python3 -m code`) and the non-ASCII refusals. - 8 accepted controls, plus the ASCII `filter-branch` and `send-email --to-cmd` refusals. - The collateral comment no longer claims a subcommand counts "at any operand". It now says the name is case-folded and scopes its flags from any operand that names it. - **Docs (`docs/reference/ax-task-catalog.md`).** - The refusal list describes the Unicode folding and the non-ASCII rule, and the contract table has a new row for it. - The limitations paragraph documents `help.autocorrect` next to the repository-alias limit. With autocorrect set to run the correction, `git send-emai --to-cmd=cmd` runs `send-email`, but the catalog sees an unknown subcommand and does not scope the flag to it. ### Evidence (round 3) - **Focused tests.** `bun test tests/project-task-catalog.test.ts`: 43 pass, 0 fail, run with `HACK_SECRETS_DISABLE_KEYCHAIN_FALLBACK=1`. The file's fixtures use their own private HOME, HACK_HOME and registry. - **Mutation testing.** I applied each mutation alone, then ran the HACK-1461 test (it failed every time) and a pure `isInlineCommandArgv` probe over the same 30 cases, then restored the source. | Mutation | Cases that flip to accepted | | --- | --- | | `foldName` is plain `toLowerCase()` (the pre-fix folding) | 7: `ıex`, `git log filter-branch`, `git log bıſect`, `git help ſend-email --to-cmd=id`, `git log filter-branch`, `python3 -m code`, `ſh scripts/x.sh` | | No non-ASCII listed-program refusal | 2: `ſh scripts/x.sh`, `git-ſtatus` | | No non-ASCII Git subcommand refusal | 2: `git ſtatus`, `git --work-tree dir pack-refſ` | | No upper-case step | 2: `ıex`, `git log bıſect` | | NFC instead of NFKC | 1: `python3 -m code` | | Default-ignorables kept | 1: `git log filter-branch` | | `words` and `scopedEntries` back on `toLowerCase()` | 4 | The Kelvin cases do not flip under the `toLowerCase()` mutation, because JavaScript already lowercases U+212A to `k`. They stay in the test as regression cases. - **Live macOS check (built CLI).** - Setup: I ran `bun run build`. In a scratch project with a private HOME, HACK_HOME and DOCKER_CONFIG, `.hack/tasks.json` held the HACK-1461 argv, each with a `touch <marker>` command. I then ran `./dist/hack project task list --json --path=<scratch>`. - At this head, every Unicode recipe (long s, dashed long s, fi ligature, dotless i, Kelvin) is refused as `inline_command`, as is the ASCII `send-email --to-cmd` control. The benign `lint` control gets past the inline check and is refused later as `input_path_invalid` (the scratch project is not a Git repository). - A CLI built the same way from the round-2 `task-catalog-recipe.ts` (0556dd9) let the long-s, dashed, fi and dotless-i recipes through the inline check; they too stopped only at `input_path_invalid`. - The marker file was never created. The catalog has no execution path. - **Gates.** - Root `bun run check`: exit 0, with `privacy-check: ok`. Its 34 warnings were there before this change, and none is in the changed files. - `bun run --cwd packages/cli typecheck`: 0 errors. - gitleaks 8.30.1 `dir` over a `git log -p origin/next..HEAD` export: no leaks. - The CLI surface and the consumer agent guidance did not change, so nothing was regenerated. - **Merge.** `next` cd35034 is merged in 84d0379 with no conflicts, so the branch is no longer behind. - **Full suite.** Hosted CI run 40134680598 (pull_request, headSha 7483db3) passed every check: the `test` aggregate and test shards 1/4 to 4/4, docker-e2e shards 1/4 to 4/4, secret-scan, Candidate core on Ubuntu and macOS, the Config compiler jobs, Runtime state models, runtime- images and linux-process-lifetime. While CI ran, `next` moved to 191382d (#489, #497). Neither touches this PR's files, and `git merge-tree` merges it cleanly, so GitHub shows BEHIND, not a conflict. ### Gaps (round 3) - I did not repeat the direct `git ſend-email --to-cmd` execution in this round, because the agent harness blocks a private-HOME `git` run outside its worktree. HACK-1461 already records that the bypass executes. This round shows that the catalog now refuses those argv. - `help.autocorrect` and repository aliases are documented limits. They are not detected, because the check does not read Git config. ### Round 4 (312de30): capital sharp s and non-ASCII programs The G109 verifier found a HIGH gap: `ẞh` (U+1E9E, capital sharp s) resolves to `/usr/bin/ssh` on the case-insensitive macOS filesystem, but `foldName` did not fold it, because JavaScript lowercases U+1E9E to `ß`, not `ss`. - **Folding.** `foldName` now runs NFKC, strips default-ignorables, then applies lower, upper, lower. The upper step maps `ß` to `SS`, so `ẞh` and `ßh` fold to `ssh`. - **argv[0] rule.** A recipe whose program (argv[0]) has any non-ASCII character in its file name is now refused, listed or not (`ẞh`, `/usr/bin/ẞh`, `./scripts/ünïcode.sh`, `café`). A case-insensitive filesystem may run such a name as an ASCII program that `foldName` does not predict, so the check fails closed. Reviewed scripts must use ASCII file names. Later arguments are still checked through `foldName` against the listed programs (`cat ẞh` is refused). - **Tests.** `tests/project-task-catalog.test.ts` adds `ẞh host id`, `/usr/bin/ẞh -o ProxyCommand=id x`, `cat ẞh`, `./scripts/ünïcode.sh` and `café --version` as refused cases. The accepted controls did not change. 43 pass. - **Mutations.** Restoring the old fold fails the `cat ẞh` case. Removing the argv[0] rule fails the HACK-1461 test. - **Docs.** `docs/reference/ax-task-catalog.md` documents the `ẞ` fold, the argv[0] refusal and the ASCII-name requirement. - **Gates.** Biome is clean on the changed files (complexity is back under the limit), `packages/cli` typecheck has 0 errors, `privacy:check` is ok, and gitleaks found no leaks in the commit patch. Hosted CI at 312de30 is the full-suite evidence. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs HACK-1441
Follow-ups from the #476 verifier on
scripts/ci-bun-crash-census.ts. Stacked on #476: #476 is still open, so this branch merges its head (e3814e87) on top ofnext(1281184b). Until #476 lands, this PR's diff also shows #476's changes. Only commit159df1e0belongs to this PR.Root cause
const cacheKey = `${sha}\n${job}`toconst cacheKey = sha, all 16 existing tests still pass. With a wildcard selector, a sha-only key gives every job at that sha the version of whichever job resolves first. For example, thetestfan-in (no Bun) and a shard (pinned Bun) would share one result.workflowJobBunVersionstopped at the first non-indented line afterjobs:. A column-0# commentinsidejobs:therefore hid every later job, and the function returned null for those jobs.ci.ymlwas fetched once for each distinct job name at a commit. That is up to 4 GETs per sha for the shards, plus 1 for the fan-in.Fix
workflowJobBunVersionnow skips blank lines and lines that start with#in column 0. Any other column-0 line is still the next top-level key and still ends the jobs.Tests (
tests/ci-bun-crash-census.test.ts, newworkflow reads (HACK-1441)block)env:key afterjobs:. Shard, lint, fan-in, commented-out-job and post-jobs:lookups each return the right value.--job "test*": three runs, two of them at the same sha. In one run the fan-in is listed first; in another a shard is listed first. Every shard gets1.4.2 workflow, the fan-in getsunknown, and the expected warning isno_bun_setup 2/9.Evidence
Focused tests:
bun test tests/ci-bun-crash-census.test.tsgives 19 pass, 0 fail (16 existing + 3 new), on Bun 1.4.2.Mutation results. Each mutation was applied to the fixed script, tested, and then the file was restored:
cacheKey = shajobs:)latelookup)Gates: root
bun run checkpasses (bun-version, privacy:check, turbo check; existing warnings only).bun run --cwd packages/cli typecheckhas 0 errors and coverstests/, which imports the script. Biome is clean on both changed files. gitleaks 8.30.1 (gitleaks dirover agit log -p origin/next..HEADexport) found no leaks.No CLI surface or consumer agent guidance changed, so I did not run
docs:cli-referenceorgenerate:agent-plugins.Full suite: hosted CI
testshards at this PR's exact head.Gaps
ghcensus was run against GitHub. The behavior is proven with the fakeGhReaderonly.#476is squash-merged before this PR,nexthas to be merged into this branch again. Expect a possible conflict at the end of the test file, because both sides add lines after the same last block.Round 2: merge
nextafter #476 landed#476 was squash-merged into
nextas101d2638. This branch still carried #476 as the merge commit0482673b, so the PR conflicted withnext(d09e1b73) inscripts/ci-bun-crash-census.tsandtests/ci-bun-crash-census.test.ts.499e8019mergesnext(d09e1b73) into the branch and keeps the PR side of both census files. Before resolving, I checked that fix(ci): read the test shard jobs in the Bun crash census and fix stale CI test job docs #476's five files are byte-identical between0482673bandd09e1b73(git diffis empty), so the PR side differs fromnextonly by the HACK-1441 changes.git diff next HEADis only the two HACK-1441 files (+177/-20). The "Stacked on fix(ci): read the test shard jobs in the Bun crash census and fix stale CI test job docs #476" note above no longer applies, because fix(ci): read the test shard jobs in the Bun crash census and fix stale CI test job docs #476's changes are already innext.499e8019: 19 pass, 0 fail, 87 expects (Bun 1.4.2).499e8019: each was applied, tested and then restored. M1 (cacheKey = sha) was killed by the per-job version test. M2 (skip block removed) was killed by the comment test and the per-job test. M3 (per-sha text cache never hits) was killed by the fetch-count test.499e8019: rootbun run checkexits 0 (privacy-check ok).bun run --cwd packages/cli typecheckexits 0 with 0 errors. Biome is clean on both files. gitleaks 8.30.1 (gitleaks dirover agit log -p origin/next..HEADexport plus the net diff) found no leaks.499e8019: run 40120172542 passed all 20 checks, includingtestshards 1/4 to 4/4,docker-e2eshards 1/4 to 4/4, secret-scan, Candidate core, Config compiler (all platforms), Runtime state models, linux-process-lifetime and runtime-images. This replaces the earlier evidence from159df1e0.nextgained one more commit,78487f72(test(native): import adopted refusal messages and size binding test budgets for load (HACK-1432) #487).git merge-treereports a clean merge, and that commit touches no census files. GitHub showsBEHIND, not a conflict.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.