fix(orchestrator): land M3 -- switch knobs, anti-keyword case, atomic prefs, bounded audit, symlink scan#11
Merged
Merged
Conversation
…d audit, symlink-safe scan Four correctness fixes the M3 selection work shipped with: - policy.rs: anti-keyword matching is now case-insensitive. Task tokens are lowercased during tokenization, but anti_keywords were compared with a case-sensitive contains(), so an uppercase manifest entry (e.g. "CSS") never matched and the penalty silently never fired. - feedback.rs: preference save is now atomic (temp file in the same dir + rename) so a crash or concurrent writer cannot leave a half-written JSON file that load() would then fail to parse. - audit.rs: load streams the JSONL line-by-line via BufReader and keeps only the most recent MAX_AUDIT_ENTRIES (50k) in a VecDeque, instead of reading the entire file into memory -- a long-lived log no longer exhausts memory. - context.rs: the project scan uses DirEntry::file_type() (which does not follow symlinks) and skips symlinks, so a planted symlink cannot redirect the walk outside the project tree. Each fix has a regression test. clippy -D warnings clean; 69 orchestrator tests pass.
…switch_margin SwitchPolicy exposed min_confidence and switch_margin but decide() never read them -- they were hardcoded to 0.0 in from_sensitivity and ignored in the decision logic, so the documented knobs did nothing. Now from_sensitivity maps them from sensitivity (0.0 at the 0.5 midpoint and above, rising to a 0.3 confidence floor / 0.15 margin at the conservative end), and decide() applies them: a candidate below min_confidence never activates, and a challenger that does not beat the current active by switch_margin holds. Default and high-sensitivity behavior is unchanged because both floors are 0.0 there; the gates only bite at low sensitivity. Adds two regression tests (low-confidence activation blocked, marginal challenger held). clippy -D warnings clean; 71 orchestrator tests pass.
Two more spots where local Arch rustfmt and CI upstream rustfmt diverge at the formatting boundary: audit.rs imported VecDeque so the declaration fits one line for both builds; the feedback.rs temp-file assertion uses a single-arg assert + comment (upstream explodes a 2-arg assert with a message that Arch keeps inline). Behavior unchanged; 71 orchestrator tests still pass.
These hold raw multi-model audit findings with local filesystem paths, unpublished vulnerability locations, and internal tool identifiers that do not belong in a public repo.
Clears all 8 cargo-audit vulnerabilities that were gating the Security
audit job, and ratchets that job from continue-on-error to blocking.
- diesel 2.2 -> 2.3.10, diesel-async 0.5 -> 0.9.2, diesel_migrations
2.2 -> 2.3, bb8 0.8 -> 0.9 (diesel-async 0.9 implements
bb8::ManageConnection only for bb8 0.9). Clears RUSTSEC-2026-0136 and
-0137 plus the diesel/diesel-async unsound advisories.
- cargo update pulls postgres-protocol 0.6.12 (RUSTSEC-2026-0179/-0180),
tokio-postgres 0.7.18 (RUSTSEC-2026-0178), quinn-proto 0.11.15
(RUSTSEC-2026-0185).
- frameshift-server: prometheus default-features=false drops protobuf
2.28 (RUSTSEC-2024-0437); the server only emits the text format.
- dev: testcontainers 0.23 -> 0.27, testcontainers-modules 0.11 -> 0.15;
drops unmaintained tokio-tar (RUSTSEC-2025-0111, no upstream fix) for
the maintained astral-tokio-tar fork, and rustls-pemfile.
- ci: audit job now blocking, ignoring only RUSTSEC-2024-0370
(proc-macro-error, build-only via age, removable only by the age 0.11
beta that backs vault crypto).
diesel-async 0.9 migration: transaction() now takes an AsyncFnOnce, so
register_pack_version uses an `async move |conn|` closure instead of the
removed `|conn| Box::pin(async move { .. })` form; ManagerConfig is now
generic, so pool.rs names AsyncPgConnection explicitly. rust-version
1.75 -> 1.86 to match the new floor (diesel 2.3 + async closures).
clippy -D warnings clean; cargo test --workspace 616 passed, 0 failed.
`git push --mirror` deletes refs absent from the local checkout. Because actions/checkout only fetches the triggering branch, the mirror push tried to delete every other ref on the target -- and the remote rejects deleting its default branch `main`, so the job failed on every push. Fetch all source branches into remote-tracking refs, then force-push branches and tags with explicit refspecs (additive, no deletion).
Context sensing previously ranked personas against a static whole-repo file-extension census, which a file-count-dominant language can swamp. sense() now also consults the active git working set (staged, unstaged, and untracked changes via `git status --porcelain`) and boosts those languages above the census, so selection favors what is being edited right now: a polyglot repo where you are editing Rust no longer ranks a JavaScript-heavy persona first. Best-effort and dependency-free. A non-git directory or a missing git binary yields no boost and prior behavior is unchanged. The boost weight (1.5) sits between the census ceiling (1.0) and the prose sentinel (2.0), so it never overrides an explicit prose-writing signal. Dependency-manifest parsing (dep names to task tokens) is the next increment; it needs toml/serde_json on the currently serde-only crate. cargo test -p frameshift-orchestrator: 77 passed, 0 failed (6 new); clippy -D warnings clean.
Local rustfmt wraps the success arm into a block; CI rustfmt collapses it onto one line. A two-statement block with a named stdout binding is stable across both rustfmt versions and reads clearer.
Context sensing now parses root manifests (Cargo.toml, package.json) for dependency names and exposes them as ContextSignal.context_tokens. The policy scores them as a small capped additive bonus when they match a persona's keywords, so framework-level signals (axum vs actix, react vs vue) sharpen selection. Dependency-free line-based parsing keeps the orchestrator crate serde-only. The bonus is deliberately kept out of the lexical IDF channel: folding dependency names into task_tokens would inflate the IDF denominator with high-weight non-matching deps and dilute genuine task-token matches. As an independent additive term (cap 0.12) it can only raise a persona that matches project dependencies, never redistribute the primary weights. cargo test -p frameshift-orchestrator: 84 passed, 0 failed (7 new); clippy -D warnings clean.
ContextSignal.frameworks (cargo/npm/go/python, detected from marker files) was populated by context sensing but never read by the scorer. Route it through the existing capped context-signal bonus alongside dependency tokens, so a persona whose keywords name a project's build framework gets the same small boost. No new fields, no weight change. cargo test -p frameshift-orchestrator: 85 passed, 0 failed (1 new); clippy -D warnings clean.
`frameshift use <name>` activated a persona but never recorded a preference, so the orchestrator's bias machinery never learned from manual choices. After a successful activation, record the persona into the shared automate-prefs.json (the same store `select`, the daemon, and the `prefs` command read), nudging future automatic selection toward personas the user actually picks. Best-effort: a preferences load/save failure warns but does not fail the command, since activation has already succeeded. cargo test -p frameshift-cli use_persona: 2 passed; clippy and fmt clean.
The catalog `total_downloads` counter shown on the marketplace was never
incremented by any route: the download handler only recorded a `pack_downloads`
event (which feeds the 7-day Trending ranking), while the counter that backs
`total_downloads` is bumped exclusively by `increment_download_counter`, which
no handler called. As a result every pack reported 0 total downloads.
Wire `increment_download_counter` into `download_pack_bytes` alongside the
existing `record_download` call, as a best-effort step that warns and continues
on failure so a download the client already received is never failed. Document
why the signed `/dl/{hash}` path cannot increment the counter (it has only the
content hash, not name/version; the official client uses the direct route).
Add an integration test asserting a successful download increments the counter.
…ing scaffold Two sharpen-arc slices for automate-mode persona selection. A1 -- daemon learns from manual overrides. evaluate_and_apply now compares the SwitchController's tracked auto-pick against the on-disk active marker before deciding. When they diverge (the user switched by hand), it rewards the chosen persona and decays the rejected auto-pick via Preferences::record_override, persists the shared automate-prefs.json, and re-baselines the controller through the new SwitchController::adopt_active so the same override is not re-learned on every tick. A new SwitchController::active_persona exposes the tracked pick. A2 phase 1 -- dependency-free semantic scoring scaffold. A new embed module defines the Embedder trait plus cosine_similarity/semantic_similarity. The scorer gains a semantic ScoreComponents channel: rank_with_embedder folds a cosine bonus (task text vs persona text, scaled by SEMANTIC_WEIGHT) additively into the blend, and select_with_embedder threads an optional embedder through selection. rank/select keep their signatures and delegate with no embedder, so the semantic channel is 0.0 by default and there is zero behavior change. A real embedding engine and model distribution are phase 2, deferred pending sign-off. clippy -D warnings clean; 92 orchestrator + 13 daemon tests pass, including a manual-override regression test and semantic-channel tests with a mock embedder.
… call The CI Format job (cargo fmt --all -- --check) was failing on feat/land-m3: b39816d landed the download-counter integration test with a method-chain state.packs.insert that rustfmt rewrites to multi-line call args, and aa90d8d added a manually-wrapped rank_with_embedder call that fits on one line. Apply the canonical rustfmt forms for both. No behavior change.
…cally CI was catching formatting and security-advisory failures that should never have been pushed. scripts/preflight.sh mirrors the CI gates (fmt, clippy, test, cargo audit with the same RUSTSEC-2024-0370 ignore); .githooks/pre-push runs the cheap no-compile subset (fmt + audit) on every push so the two failure modes that have actually slipped through are blocked before they reach CI. Enable per clone with: git config core.hooksPath .githooks Run the full local mirror before a PR with: scripts/preflight.sh
This was referenced Jul 5, 2026
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.
Summary
Five correctness fixes the M3 intelligent-selection work shipped with, each with a regression test. Stacked on #10 (CI foundation), so this PR's own checks run the new CI.
Fixes
anti_keywordswere compared with a case-sensitivecontains(), so an uppercase manifest entry never matched and the penalty silently never fired.load()would then fail to parse.loadstreams the JSONL line-by-line viaBufReaderand retains only the most recentMAX_AUDIT_ENTRIES(50k) in aVecDeque, instead of reading the whole file into memory.DirEntry::file_type()(which does not follow symlinks) and skips symlinks, so a planted symlink cannot redirect the walk outside the project tree.SwitchPolicy::min_confidenceandswitch_marginwere advertised but ignored.from_sensitivitynow maps them (0.0 at the 0.5 midpoint and above, rising to a 0.3 confidence floor / 0.15 margin at the conservative end), anddecide()applies them: a candidate belowmin_confidencenever activates, and a challenger that does not beat the current active byswitch_marginholds. Default/high-sensitivity behavior is unchanged.Verification (local)
cargo clippy -p frameshift-orchestrator --all-targets -- -D warnings: cleancargo test -p frameshift-orchestrator: 71 passed, 0 failed (6 new tests; no existing test broken)