Skip to content

fix(vibe): symbol-level route rule and the 2026-10-21 lessons - #329

Merged
shafty023 merged 9 commits into
mainfrom
fix/vibe-lessons-20261021
Oct 21, 2026
Merged

shafty023 merged 9 commits into
mainfrom
fix/vibe-lessons-20261021

Conversation

@shafty023

@shafty023 shafty023 commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Purpose

vibe 1.0.73: one combined lessons PR for vibe and fleet-orchestrator, covering every open entry in the lessons ledger. Wherever a lesson had a checkable shape it got a mechanical check, with prose only where nothing can observe the mistake.

Route rule: symbol-level narrowing

routeSpecs used to follow helper imports at file level. As a result, a route named in one large shared fixture required every spec that imported that fixture. The rule now lives in route-specs.mjs, and module-symbols.mjs gives a text-only read of each helper's top-level statements. A spec is required only when it imports an export that reaches the endpoint. An export reaches it when its declaration names the endpoint, or references a constant, function or import that does. Re-exports (named, aliased and export *) are followed within the existing three-file hop limit. Type-only imports never count. The rule falls back to file level only for a namespace import, a dynamic import, a default import of a file with no default declaration, a helper whose top level runs on load, or a file the splitter cannot read. Each fallback prints its reason in the requirement.

Real tree: symphony-alpha origin/main at 2dd71fbab2, apps/api/app/artifact-links/batch-move/route.ts, file contents read with git show.

  • Before: 62 specs. After: 2 specs:
    • apps/desktop/test/e2e/sessions-list-project-edit.spec.ts: imports sessionsListProjectCloudRoutes from apps/desktop/test/e2e/helpers/sessions-list-project-cloud.ts
    • e2e/sessions-linked-entity-columns.spec.ts: imports installProjectWriteMocks from e2e/helpers/sessions-list-fixtures.ts
  • Checked by hand: these are the only specs that import an export reaching the endpoint (BATCH_MOVE_PATH feeds only installProjectWriteMocks; the Desktop helper names it inside sessionsListProjectCloudRoutes). The splitter read all 2,861 e2e and Desktop test files on that tree with no fallback.

Failing-first: the brief's case (a helper with two exports, only one naming the endpoint, and a spec importing the other) and the wider case (type, alias, re-export and namespace imports) both fail on origin/main and pass here.

Publication main-sync waits for QA

Operator ruling: QA the feature-complete head, conflicts included, then merge main once and enqueue. Before this change, vibe's "Main-sync before publication" merged main on every push. Now prepareMainSync (tools/vibe-workers/src/main-sync.ts, dist rebuilt with npm run build) still fetches main, but stages the merge only in final-merge mode: vibe-sessions.mjs touch --main-merge final, which is set once QA passes or when the person asks for a main merge now. Otherwise the branch's own merge-base with main stands in as its main, and that includes a branch that conflicts with main or fails a check (it is fixed on that base). The receipt records the waiting main as deferredMain, and preparation returns mainMerge: "deferred".

Tests:

  • New, failing first against the old bundle: a clean upstream change and a conflicting branch, neither in final mode, get no merge, and the receipt's main is the merge-base.
  • Existing tests now in final mode: a clean upstream gets one staged merge, and a conflict gets one merge with the conflict kept for the writer.

qa-merge-order.mjs checks the result before enqueue.

Shallow clones are deepened, not blocked

When a lookup by the restored-removal rule finds nothing in a shallow clone, the rule deepens the history in steps (git fetch --deepen=1000, then --deepen=10000, then --unshallow) and reruns until the lookups resolve or the clone is complete. It blocks only when a fetch fails, and it prints git's reason. The test clones with --depth 1: deepening finds the removal commit and leaves the clone complete, and an unreachable remote blocks with the fetch error.

Same-class spots #327 listed

Spot Decision Why
isRootTest / ROOT_TEST_PREFIXES (test-files.mjs) isRootTest also accepts isE2ePath; the prefixes stay A repair to an existing spec under e2e-* was not a test repair, and a spec added there for a requested PR was outside the allowed paths, though the same spec under e2e/ passed. Only spec and test files are widened; a config under an e2e root is still outside.
requiredLanes e2e lane (pr-facts.mjs) Fires on any isE2ePath change A Desktop or smoke e2e spec change owed no e2e run, while an e2e/ one did.
"rg ... under e2e/" (vibe-verify-worker.md) Searches every e2e root The same root definition; specs outside e2e/ that cover a touched screen were never searched.

Each spot has a failing-first test (the inventory phase tests and the lane test) except the prose.

Ledger entries

Taken (ledger line, lesson → what changed):

  • L239: matching a prototype removed a shipped item → capability-removal rule in diff-rules.mjs.
  • L240: a capability list became new UI → change worker: every new element needs a source that names it; record absorbed tickets.
  • L241: lessons PRs encode general rules only → fleet Lessons loop.
  • L242: PR body edits dropped ## Test plan → body gate checks the repo template's headings.
  • L243 to L247: targeted tests skipped the PR's own spec; graph misses branch-only files and e2e → evidence gate for whole-branch selection and runs; verify worker records it. L244 (fix lives in vibe first) → fleet Lessons loop.
  • L248: per-spec slot entries → e2e-lock.mjs refuses a second entry from the same worktree.
  • L252 to L254: follow-up ticket placement; IN_REVIEW without a PR → ticket-write-check.mjs and the children gate (awaitingPullRequest).
  • L257 to L258: executed PRDs need the production visual-QA flag section → executed-check.mjs, ticket worker, fleet.
  • L261: idle helpers → fleet helpers.mjs status prints STOP lines (replaces "reuse an idle reviewer").
  • L264 to L265: specs asserting a changed request body → route rule (fix(vibe): route rule requires specs behind helpers in every e2e root #327 plus this PR) and e2e-reference selection by request field.
  • L268: shallow clone misattributed history → restored-removal deepens a shallow clone automatically; fleet history-origin.mjs refuses to attribute without full history.
  • L274: QA before the final main merge → publication main-sync waits for final-merge mode; qa-merge-order.mjs state check before enqueue.
  • L277 to L280: route narrowing and the same-class spots → above.

Skipped:

Rungs

Each lesson takes the highest rung that can catch it (types or architecture, then a script or gate, then a guard test, then prose).

Lesson (ledger line) Rung Check
Route rule over-selects through shared fixtures (L277 to L280) Gate route-specs.mjs, module-symbols.mjs (diffRules.requiredTests)
e2e root hard-coded to e2e/ (#327 spots) Gate isRootTest, requiredLanes use isE2ePath
Selected tests not run; branch-only files; e2e by reference; body-asserting specs (L243 to L247, L264 to L265) Gate selectionProblems (blocking.prEvidence)
PR body drops a template heading (L242) Gate templateHeadingProblems (blocking.prBody)
IN_REVIEW without a PR (L254) Gate checkChildEvidence (awaitingPullRequest); ticket-write-check.mjs --status IN_REVIEW
Ticket under an EXECUTED, OBSOLETE, DONE or CANCELED parent (L252, L253) Gate ticket-write-check.mjs (ticket worker create mode, fleet ticket filing)
Ticket in a past sprint (L253) Gate where the project carries an end date or an M/D-M/D name; otherwise prose Sprint naming is repo-specific, so a project without either cannot be dated by a check.
EXECUTED without the flag section (L257, L258) Gate executed-check.mjs (ticket worker merge check, fleet PRD close-out)
History attributed from a shallow clone (L268) Gate restored-removal rule deepens automatically, blocking only on a failed fetch; fleet history-origin.mjs refuses
Matching a prototype removed a shipped item (L239) Gate capability-removal rule: a removed mount or shipped string needs Removal: <source>
Per-spec e2e lock entries (L248) Gate e2e-lock.mjs refuses a second entry from the same worktree (exit 76)
Idle helpers (L261) Script in the orchestrator status fleet helpers.mjs status prints STOP lines
QA, then one main merge, then enqueue (L274) Architecture plus a state check main-sync merges only in final-merge mode; qa-merge-order.mjs before enqueue
New UI from a capability list (L240) Prose plus the existing copy-provenance gate Whether a cited source names an element is a reading of ticket text, which no check can judge; the gate already forces a cited source for each new label.
Lessons stay general (L241) Prose plus the existing generic.test.mjs The guard test covers fleet-orchestrator text. vibe deliberately targets one repo, so no test can tell a general rule from a repo rule in its text.
A lesson's fix lives in vibe first (L244) Prose This rule is about where future fixes go, and no check can see a fix that was never written.

Instruction length (agents and SKILL files): +56 / -15 lines. The prose for the lock, ticket and helper rules now points at their checks instead of restating them.

Tests

node --test <every plugins/vibe *.test.{mjs,cjs,js}>   # green in CI (local load average above 200 timed out unrelated suites)
tools/vibe-workers: tsc --noEmit, vitest run, dist up to date  # green in CI; the new deferral case passes locally
uv run pytest plugins/vibe                             # 370 passed
uv run ruff check .                                    # clean
uv run pyright                                         # 0 errors

Two prose contract tests changed with the wording they pin (only the tests selected for each file of the whole branch diff; Every other child ... Never set IN_REVIEW without a pull request). The children-gate fixture now passes pullRequest, because the ruling made a delivered child without one stay open.

Prevention

Prevention
- Class: test selection by text or file-level imports over-selects or misses specs (route rule, e2e roots, whole-branch selection)
- Instances: fixed routeSpecs (symbol level), isRootTest, requiredLanes e2e, verify-worker e2e search; selection over the whole branch diff is gated
- Search: grep -rnE "e2e/|startsWith\(.e2e|code_tests_for" plugins/vibe
- Rung: mechanical rule and evidence gate with failing-first tests
- Proof: diff-rules, inventory and pr-facts tests fail on origin/main and pass here
- Why not higher: no type or lint can see which tests a diff needs
- Event recorded: no log defined in this repository

Prevention
- Class: a status or artifact claimed without the evidence the ruling requires (IN_REVIEW without a PR, EXECUTED without the flag section, a body missing template headings)
- Instances: children gate, executed-check.mjs, body gate
- Search: grep -rn "IN_REVIEW\|EXECUTED\|pull_request_template" plugins/vibe
- Rung: mechanical gates the ticket and environment workers already run
- Proof: new tests fail without the change (missing exports, or inReview set with no PR)
- Why not higher: ClosedLoop status writes go through MCP calls no script intercepts
- Event recorded: no log defined in this repository

Prevention
- Class: attributing history from a shallow clone
- Instances: restored-removal rule; fleet research rule
- Search: grep -rn "git log" plugins/vibe/skills plugins/vibe/agents
- Rung: mechanical (rule blocks when shallow and a lookup found nothing)
- Proof: the shallow-clone test fails on the previous diff-rules.mjs
- Why not higher: an ad hoc git log by an agent is prose-governed
- Event recorded: no log defined in this repository

Prevention
- Class: orchestration and ticket habits (QA order, idle helpers, ticket placement, lock entries, prototype matching)
- Instances: qa-merge-order.mjs, helpers.mjs, ticket-write-check.mjs, e2e-lock.mjs, capability-removal rule
- Search: grep -n "idle\|main merge\|sprint\|prototype\|lock" plugins/vibe/skills/fleet-orchestrator/SKILL.md
- Rung: scripts and gates with tests; the skill prose names each check
- Proof: each new test fails without its change (missing module, or exit 75 where 76 is now required, or no capability-removal finding)
- Why not higher: ticket writes and pane lifecycles happen outside any process the plugin owns, so the checks run at the point of action rather than as types
- Event recorded: no log defined in this repository

…s that reach the endpoint

routeSpecs followed helper imports at file level, so a route named in a large shared fixture required every spec importing that fixture (62 for batch-move). It now reads each helper's top-level statements and requires a spec only when it imports an export that names the endpoint or references a constant, function or import that does, across re-exports within three files. Namespace, dynamic and indirect default imports, a top-level statement that runs on load, and a file the splitter cannot read fall back to file level, with the reason in the requirement.
…nce lane

isRootTest and the e2e evidence lane matched only e2e/, so a repair to a spec under another e2e root was not a test repair and a change there needed no e2e run. Both now use isE2ePath, and the verify worker searches every e2e root for specs covering a touched screen.
…D and shallow history

The pull-request evidence gate now needs an import and an e2e by-reference selection for every changed source file and a passing run on the published commit for every selected, required or edited test. The body gate refuses a body missing a heading of the repo's pull request template. A delivered PRD child moves to IN_REVIEW only with its pull request. executed-check.mjs refuses EXECUTED without the production visual-QA flag section checked after the last merge. The restored-removal rule blocks in a shallow clone, where the removal it looks for can be cut off.
Writer, verify, ticket and handoff instructions point at the new gates; fleet-orchestrator gains the QA-then-one-main-merge order, stopping idle helpers, follow-up ticket placement, the executed-PRD flag section, the shallow-clone check, one e2e lock entry per head, the shipped-items rule for prototype matching and the general-lessons rule.
Removed shipped mounts and strings need a recorded source, the e2e lock takes one entry per worktree, ticket writes are checked for sprint, parent and pull request, the QA-then-one-merge order is a state check, and fleet-orchestrator gains a helper ledger that flags idle helpers and a history lookup that refuses a shallow clone. Prose that these checks now enforce points at them instead.
…n, ticket parents widen

Publication main-sync merges fresh main only in final-merge mode (vibe-sessions.mjs touch --main-merge final, set once QA passes), when the branch no longer merges cleanly onto main, or when its last validation failed; otherwise the branch's base stands in as its main and the waiting main is recorded as deferredMain. The dist bundle is rebuilt. The restored-removal rule deepens a shallow clone in steps instead of blocking, and blocks only when a fetch fails. ticket-write-check also refuses a DONE or CANCELED parent.
A conflicting branch is QA'd as it is and a failing check is fixed on the branch's own base, so neither triggers a main merge any more; only final-merge mode, set once QA passes or when the person asks for a merge now, does. A conflicting branch outside final mode is covered by a new test.
@shafty023
shafty023 merged commit 10086b0 into main Oct 21, 2026
10 checks passed
@shafty023
shafty023 deleted the fix/vibe-lessons-20261021 branch October 21, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant