* ci: audit closed-issue delivery claims — cited PRs must actually be merged
Follow-up to #3223: an issue closed as 'fixed in #N' while #N was closed
without merging (or does not exist) leaves a completion claim that does not
hold. The weekly maintenance workflow now runs a read-only audit over issues
closed in the last 30 days, extracts delivery-claim PR references from issue
bodies and close-window comments, verifies each cited PR's merge state, and
reports findings to the step summary.
Design constraints:
- Read-only: no comments, no closes, no labels; findings are advisory and
the run always stays green so the weekly audit never blocks maintenance.
- Unverifiable counts as unmerged: a 404 PR or an API failure fails the
verdict, because an unverifiable claim is exactly the failure mode.
- Pure decision functions are exported for tests; a first live run against
this repository surfaced one real finding (#3408 cites #3060, which does
not resolve), included here as the motivating sample.
8 unit/contract tests: extraction, windowing (close comments included),
the #3223 unmerged-citation failure mode, the all-clear summary, workflow
contract, and a no-write guard.
* ci: rework audit evidence contract — claims vs context, four PR states, visible completeness
Revision per the #3522 review (reviewer ran the CLI through 19 controlled
cases plus a bounded live check of #3408):
- Bind findings to asserted delivery claims. Closing wording ('Delivered
through #N'), claim verbs (fixed/shipped/landed/resolved/...), and bare
'PR #N' inside closing comments are claims; historical mentions, quoted
lines, fenced examples, negated claims, and tracking-only references are
reported as contextual references and never produce findings. On the live
#3408 the audit now recognizes the closing claim #3500 (merged) instead of
flagging the historical #3060 text.
- Separate observed states: merged / open-unmerged / closed-unmerged /
unknown (404, rate limit, transient). Unknown claims are advisory manual-
verification items, not failed claims; each row carries the claim snippet,
its source link, and the observation time. Checked claims are listed even
when passing, so the report is traceable rather than an opaque all-clear.
- Paginate issue and comment collection (--paginate --slurp), bound comment
requests (200) and PR checks (100), and surface skipped pages, rate limits,
and retrieval failures as an explicit incomplete audit instead of an empty
all-clear. A live-run maxBuffer overflow on the paginated issue list was
caught and fixed (64MB); the audit reported it as incomplete before the fix.
- Scope the job to read access: job-level contents/issues/pull-requests read.
13 contract/unit tests including a CLI fixture (stub gh) that places the
relevant issue on page two and forces a comment-fetch failure.
* ci: report incomplete audit at PR check cap
---------
Co-authored-by: chenhz01 <chenhz01@users.noreply.github.com>
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
Prettier's config searcher also reads .prettierrc.json5, .prettierrc.ts/.mts/.cts and prettier.config.ts/.mts/.cts. resolve-formatter knew none of them, so a project configured only through prettier.config.ts was never formatted by post-edit-format, quality-gate or the Stop batch. config-protection's rc pattern stopped at .yaml/.toml, so an existing .prettierrc.ts or .prettierrc.json5 could be edited freely.
The Stop typecheck reported every tsc line that contained the edited path anywhere. A root index.ts has the relative path `index.ts`, which every `src/index.ts(...)` diagnostic contains, so a clean root file was reported with the errors of src/index.ts. The same happened to src/utils.ts with web/src/utils.ts, and to any file named in another file's message.
Compare the path that starts the diagnostic (`path(line,col): `), keeping the slash normalization for Windows.
With diff.relative=true, `git diff --name-only` reports paths relative to the working directory, so joining them to the repository root pointed at files that do not exist, and a session in a subdirectory again got no console.log warning. getGitModifiedFiles now runs `git -c diff.relative=false diff --name-only HEAD`. The -c form also works on Git versions older than --no-relative.
check-console-log took the paths from `git diff --name-only HEAD`, which are relative to the repository root, and checked them with fs.existsSync from the hook's working directory. In a session started in a subdirectory, such as a monorepo package, no path existed from there, so the hook never warned. Resolve each path from the repository root. The exclusion patterns and the warning still use the root-relative path.
The platform-configs module lists `.pi`, but PLATFORM_SOURCE_PATH_OWNERS
had no entry for it, so isForeignPlatformPath() returned false and every
install-apply target (e.g. `--target opencode`) copied the Pi harness
adapter (.pi/README.md, .pi/extensions/*) into its config root and
recorded it as ECC-managed.
Pi consumes ECC through `pi install`, not install-apply, so map `.pi` to
the `pi` owner. No install target has that id, so the directory is now
filtered out everywhere, like `.codex`, `.cursor`, and `.zed`.
Add regression tests for the opencode plan and for every registered
target.
Fixes#3558
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
extractQualifiedTarGzip walked the archive, validated the terminator, read each header and classified the entry in one loop; installNasiko and uninstallNasiko each resolved the release and paths, then ran the whole download or staging sequence inside one try with rollback flags. The extractor now uses gunzipArchive, assertTarTerminator, readTarEntry, isBinaryEntry and isSidecarEntry. The two lifecycle calls share qualifiedReleaseFor and installPathsFor; install fetches through fetchVerifiedBinary and publishes through publishBinary, and uninstall stages and deletes through removeInstalledFiles with removeOrRetain.
The rollback rules are the same: a failed publish removes only the files this call created, a failed rename puts the staged file back, and a failed delete keeps the tombstone and reports it in cleanupPending. The plan still takes its version from the requested release, not from releaseOverride, and every dependency hook is called in the same order. Exports are unchanged, and a run of the old and new extractor over 20k generated archives covering sidecars, bad padding, bad size fields, truncation and trailing data produced identical results and messages.
searchEvidence built the Glob, Grep and LS evidence inline before falling through to the shell path, and findCreditingSearch and findClosestMiss each nested the per-search and per-item loops with the stem and directory checks in one body. The tool builders are now globEvidence, grepEvidence and lsEvidence behind a small Map, the item loop of findCreditingSearch is creditingItem, and findClosestMiss hands the miss reasons to missReason and the Read scan to considerReads. searchHasMessage replaces the repeated messageId check.
Results are unchanged: the same first crediting search wins, the same lowest-ranked miss is reported, and errors still resolve to null inside the two public functions. Checked by running the old and new factory side by side over 40k generated scans and targets with identical output.
lexCodeLines ran every state (code, line comment, block comment, string) through one loop body with three closures over its locals, and shCodeLines and psCodeLines each inlined their escape, quote and comment readers. The C-family lexer now keeps its partial line on a small lx record and dispatches by state to lexNewline, lexLineComment, lexBlockComment, lexString and lexCode, with lexCommentStart, startsJsx and lexStringStart under lexCode. The shell lexers share pushShellLine, readEscapedPair, readQuotedSpan and readLineComment; shCodeLines looks its readers up in a table and psCodeLines splits into psBlockComment and psToken.
Every reader returns the index of the last character it consumed, so the loops advance exactly as before, and the sentinel for an unlexable edit is still null from the C-family path and a null result from the shell paths. Exports are unchanged. A differential run against the previous version over the unit test strings plus 40k generated inputs, across all nine spec shapes change-profile builds, produced identical lines, normalized signatures and isTrivialEdit verdicts.
heredocDelimiter, executionRegion and scanShell each carried one long character loop with every quote, escape, heredoc, case and scope rule inline, which is where the three complexity warnings came from. The delimiter reader now has skipDelimiterBlanks, delimiterEscape and readDelimiterWord; the $(...) walker dispatches to regionQuotedEnd, regionSubstitution, openRegionQuote, regionHeredocOperator, regionHeredocBodies, casePhaseStep and closeRegionParen; and the top-level scanner keeps its position and pending word on a scan object that scanQuotedChar, scanEscape, scanSubstitution, openQuote, scanOperator and scanRedirect advance.
Every budget.spend call keeps its amount and order, so the work quota tests still land in the same window. The module still has no requires and exports createBudget, scanShell and scanExpansions unchanged; a differential run of the old and new scanner over the hook test corpus plus 58k generated inputs produced identical commands, nested regions and spend totals.
parseArgs now reads flags from two lookup tables, one for flags that take a
value in either form and one for standalone switches, instead of thirty-odd
if blocks. The ECC Tools evidence and gap text live in ordered tier tables
keyed by roadmap markers, with the shared marker lists named once.
buildRequirements reads its docs and readiness checks up front, then
assembles the list from four requirement groups. The PR and issue budget
rows share one builder.
Flag errors, requirement order, and every message string are unchanged.
Verified by comparing parseArgs over a flag matrix and buildReport over the
repo, an empty root, and one synthetic roadmap per marker. Clears the four
complexity warnings on this file.
cFamilyStartsFresh ran one 130-line loop over template literals, comments,
regex-vs-division, C# and Rust raw strings, quotes, words, and punctuation.
It now keeps scanner state in one object and walks a fixed list of step
functions, each returning consumed, abort, or not-mine. The Python f-string
field scan, the shell heredoc and quote tokens, and the PowerShell
here-string, block comment, and hash tokens get the same treatment.
Verified byte-for-byte against the recorded test inputs, four hundred real
source files from the repo, random prefixes, and single-character mutations
across every flavor spec. Clears the five complexity warnings on this file.
executeRepairOperation, executeUninstallOperation, and inspectManagedOperation
now dispatch on operation kind to one small function per kind, with the
repeated uninstall result shapes and previous-content restore pulled into
helpers. executeRepairOperation takes an options object instead of four
trailing positionals. analyzeRecord is a sequence of collect* steps, and the
per-record repair closure inside repairInstalledStates hands off to
runRecordRepair with the legacy OpenCode, build-only dry run, and managed
repair paths as separate functions.
Result objects keep the same keys in the same order, and lock, build, and
require ordering is unchanged. Clears the max-params and complexity warnings
on this file.
Six summary-line replacers in catalog.js each took seven to nine positional
capture groups and rebuilt the line by hand. They now go through one
catalogCountReplacer factory that takes the regex group count and the groups
holding the agents, skills, and commands counts. Every other group is written
back verbatim.
Output is byte-identical across the real docs and several synthetic catalogs.
Clears the six max-params warnings on this file.
The here-string normalizer, comment stripper, and balanced-group reader each
carried their own copy of the same inert-region state machine. They now share
createLexState, consumeInertChar, and openInertRegion. The container
extractor, statement tokenizer, nested pwsh scanner, cmd scanner, Start-Process
scanner, and top-level statement walk are split into small named helpers that
each handle one region or one command family.
No classification change. Verified against the recorded test inputs plus
mutated variants with an order-sensitive comparison of findings. Clears the
eleven complexity and prefer-const warnings on this file.
validateInlineJs unescaped the shell body with four chained replaces,
so a backslash pair followed by n or t was unescaped twice: the pair
became one backslash, then the new backslash-n became a newline. A
valid hook command such as console.log('\\n') failed validation with
an unterminated string. CodeQL flags the chain as double unescaping.
One regex pass maps each escape exactly once. The regression test
feeds the escaped-backslash program through the validator CLI.
validateHookEntry handled command, http and prompt hooks in one function; it now dispatches to validateCommandHook, validateHttpHook and validatePromptHook with the inline node -e check in validateInlineJs. validateHooks reads the file, loads the sidecar and validates the object or legacy array shape through readHooksFile, loadMetadata, validateObjectFormat and validateArrayFormat, so the deepest nesting is gone.
Error messages, their order, exit codes and the --update-fingerprints flow are unchanged.
extractSessionSummary did user-message filtering and two kinds of tool_use collection in one loop body six blocks deep. The per-entry work now lives in userMessageText, recordToolUse and collectEntry. main() is down to the sequence of steps: resolveTranscriptPath, sessionShortId, maybeGenerateLlmSummary, then updateSessionFile or createSessionFile, with the shared footer and empty-session template as constants.
Call order, log lines, file names and the written markdown are unchanged, including the exact summary block replacement and the migration path for files without markers.
gate() handled Edit, MultiEdit and shell in one 220-line function. It now builds a small context and dispatches to gateSingleEdit, gateMultiEdit and gateShell, with the first-touch, sibling-collapse and routine/destructive shell steps as their own functions. isDestructiveGit and isReadOnlyGitIntrospection became subcommand tables; unwrapLeadWrappers got one handler per wrapper; ddLauncherCommandIndex, isDestructiveFindExec and sanitizePath lost their inline option and code point ladders. condensedGateMsg takes an options object instead of seven positional parameters.
Decision order, recorded metrics reasons, state keys and every denial message are unchanged, and the module exports are the same.