chore(memory): BDR-116 + LRN-212 + EVAL-043 + journal/TODO/contract w3a — feat model-router wave 3-A
This commit is contained in:
@@ -133,6 +133,7 @@ rules:
|
||||
| BDR-109 | 2026-09-30 | Higgsfield pack: npm CLI `latest` + 8 upstream skills git-cloned into gitignored `skills-external/higgsfield-*`, OFF by default, in no profile; two toggles (`higgsfield` = allowlist of 7 media skills, `higgsfield-websites` = landing-page aid, never website create/deploy/publish); CLI presence by probe; routing on explicit ask | accepted |
|
||||
| BDR-110 | 2026-10-06 | Shell portability doctrine: native userland on macOS AND Linux, no Homebrew GNU tools on PATH; `lib/tests/portability-census.test.sh` locks deterministic GNU-only idioms | accepted |
|
||||
| BDR-115 | 2026-10-08 | model-router mod: pin = entry default, sub-tasks route finer; one writer per axis; full ids from the mod table; built-ins-only agents table until frontmatter pins go; state in closure | accepted |
|
||||
| BDR-116 | 2026-10-11 | model-router first-use confirmation: tracked routing.json = source of phases + rows + decisions; engine dialog at first use; project exceptions in user scope keyed by normalized remote; model never writes; census tolerates a decided row | accepted |
|
||||
|
||||
---
|
||||
|
||||
@@ -1403,3 +1404,11 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
|
||||
- **Refs**: plan `.claude/tasks/plans/2026-10-08-model-router-mod.md`, contract `.claude/tasks/contracts/2026-10-08-model-router-w1a-1533.md`, [[BDR-107]], [[BDR-108]], [[BLK-029]], [[LRN-205]], [[LRN-206]].
|
||||
- **Amendment (2026-10-09, user decisions 2026-10-08 evening)**: (a) LOAD supersedes the "Load:" line: tracked relative symlink `skills/model-router` → `../mods/model-router`, loaded in place as `model-router@skills-dir` wherever link.sh links `~/.claude/skills`; `CLAUDE_CODE_PLUGIN_DIRS` dropped (absolute path, settings `env` has no `$HOME` expansion, settings.json tracked), local marketplace dropped (`add` writes an absolute path into settings.json). Proven by fresh-process `claude plugin list --json`. (b) PRECEDENCE amended: `ultrathink` and a typed `/effort-<l>` are the main turn's DEFAULT and MINIMUM (floor slot `turnFloor`): sticky `/route` effort > turn route effort > floor > engine, then floored; per axis; mid-turn prompt floors the running turn and the next (`wait` ignored). Rationale: user "un choix explicite bat la phase déduite"; a pure floor made `/effort-low` a no-op (challenge finding). (c) Per-machine kill switch `"enabled": false` in the untracked `~/.claude/model-router.json` (survives `/clear`, a failed reload keeps the previous config); `enabledPlugins` would dirty the tracked settings.json on every machine. (d) Hardening: `/route` composer-only; agent loops effort-only (model fixed at spawn); config caps; typed slash attested at `prompt.submit`. Commits 346d6ae, 1ff608a, 6430ac6; contracts `2026-10-08-model-router-floor-1835`, `2026-10-08-model-router-wiring-1835`; residuals parked in TODO.
|
||||
- **Amendment 2 (2026-10-10, wave 2 closed, commits bb56f3e + 1f2d33b)**: (a) rule 4 closed: rows for every repo skill (56) + agent (21) + Explore/Plan, PHASES by role (plan/reflect/orchestrate/escalate best · judge big · implement/write/verify/explore/apply work · mechanical cheap; `write` work/high + `apply` work/low added). User: pins "deleted or reworked", not copied → rows are the live source, tracked `model:`/`effort:` frontmatter KEPT as off-state floor, census-locked equal to rows (`lib/tests/effort-routing.test.sh`). Robustness BLOCKER closed: mod off → agents would inherit parent model. (b) Rule 5 amended: unrowed skill load changes nothing; best-tier skill row lives in `runMain` slot surviving turn end (precedence userMain > turnMain > runMain > floor > engine), dropped by `/route clear`, `/route off`, user `/model`, typed non-best skill; turn writers (route tool, prompt rules) never touch it. (c) Agents: model written at spawn WITHIN tier, upward only (never below frontmatter alias); explicit Agent params win; project-defined agent (agent.offer source projectSettings|localSettings) skipped. (d) Typed slash: name-bound marker at prompt.submit (composer|sdk|bridge) + pending slot + idle fallback (no live/spawning loop). (e) Shifters `effort-*`, `effort-pins.*`, `model-check.sh` DELETED; orchestrators call `mcp__model-router__route` per phase; gate witness = route answer id, remedy `/route on`; builtin `/effort` not a lever inside a run (levers `ultrathink`, `/route effort=max`). (f) SemVer: typed `/effort-*` removal = breaking → next release 3.0.0. Plan `.claude/tasks/plans/2026-10-09-model-router-w2-1546.md` r4, contracts `2026-10-09-model-router-w2a-1546`, `2026-10-10-model-router-w2b-1045`. Links [[LRN-210]], [[LRN-211]], [[EVAL-042]].
|
||||
|
||||
## BDR-116 — model-router first-use confirmation: tracked `routing.json` = source of phases, rows and decisions; engine dialog once per row/phase; project exceptions in user scope; model never writes [accepted] (2026-10-11)
|
||||
- **Decision**: (1) `mods/model-router/routing.json` (tracked, reached from every project through the plugin dir `$.plugin.root` = the existing `~/.claude/skills/model-router` link) is the single source: 11 phases (full routes), 56 skill rows, 23 agent rows, `confirmed` (kind/name → phase), `changed` (from/to for an Everywhere change), `projects[<key>]` exceptions, `ask`. `DEFAULT_CONFIG` keeps only the phases as fallback; rows `{}`. (2) First use of a rowed typed skill (T1), a rowed agent spawn with no explicit model (T2) or a main-loop phase declared via the route tool (T3) opens `$.ui.ask`: options Later / Keep / 2 alt phases (Other = phase name; T3 Later/Keep); a change asks Everywhere (row moved + `changed`) or This project only (`projects[key]`, confirmed = base row so no other project asks). Keep endorses the phase too. One dialog in flight; concurrent uses route unasked; headless/dismissed/garbage = Later; never inside a sub-agent; pre-ask re-read of the file (other sessions' decisions seen). (3) Project key = origin remote normalized (`new URL` host+path or strict scp regex; userinfo never read; any remaining `@`/`:`/empty host → no key); no `local:` path keys; no remote → Everywhere/Later only. The project tree's `.claude/model-router.json` is NEVER read (a cloned repo must not re-route the user's gate agents). (4) Writers = dialog answers + composer `/route ask on|off` only; serialized chain; output capped 64 KB; file never created; later unreadable → previous config kept (kill-switch rule); layers routing.json < `~/.claude/model-router.json` (its `ask` wins). (5) Census reads rows + phases from the file, locks DEFAULT phases == file, tolerates a drift only for a `changed` row whose frontmatter == `from` and row == `to` (WARN); Keep-only drift still FAILs. (6) `/route pending`, `/route ask on|off`; `set`/`confirm`/show suffix deferred.
|
||||
- **Why**: user 2026-10-10: see in practice whether routing fits ("prompt qui demande de confirmer… mémoire de ce qu'on décide… met à jour la table… exception pour ce projet… persistant sur tous les projets, se redéploie comme le mod"). Tracked file = deploys with the mod via git; user-scope exceptions = security (robustness lens: project-tree layer let a cloned repo downgrade security-auditor to haiku).
|
||||
- **Alternatives rejected**: `$.store` (machine-local, re-asks per machine); project file in the tree (security hole + writes into worktrees); shared-promise dedupe for parallel spawns (hook budget: awaiters time out); `confirmed` dates (git log dates them); dialog edits of phases (blast radius: a phase is shared by many rows); per-layer degrade after first load (one transient read failure wiped 79 rows + the kill switch); `local:<realpath>` keys (home path in a tracked file).
|
||||
- **Gates**: plan r1 → r4 (simplicity CONCERNS(3), correctness FATAL(8) with the census BLOCKER, robustness CONCERNS(8) after a network-killed first run, confirmation CONCERNS(6)); feater DONE + 4 rounds; GATE 0 MET; verifier ECARTS(3)/(2) → CONFORME, re-verify after security ECARTS(1) → fixtures; security BLOCK(1) real (password with `@`/`/` stored in the key) → fixed, PASS. Kit 86 → 190 tests. Live T2 dialogs answered by the user from the hot-loaded working-tree mod ([[LRN-212]]).
|
||||
- **Refs**: contract `.claude/tasks/contracts/2026-10-10-model-router-w3a-confirm-1201.md`, plan `.claude/tasks/plans/2026-10-10-model-router-w3a-confirm-1201.md`, commit 22455c0, [[BDR-115]], [[LRN-210]], [[LRN-211]], [[EVAL-043]].
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ rules:
|
||||
| EVAL-040 | 2026-10-08 | model-router w1a plan: 3 challengers + 1 confirmation found 2 BLOCKER + 14 MAJOR on a plan judged closed; executor then passed every gate first time | keep the round, never dispatch a mod plan without it |
|
||||
| EVAL-041 | 2026-10-09 | model-router W1-C plan: 3 lenses FATAL (4 BLOCKER + 20 MAJOR) then 2 confirmations each FATAL with a NEW BLOCKER in my own revision; executor DONE first pass, 3 short text/hardening rounds | one confirmation is not enough when a revision removes a whole mechanism; the plan carried the risk, the code almost none |
|
||||
| EVAL-042 | 2026-10-10 | model-router W2 plan r1 → r4: 3 lenses (1 BLOCKER), 2 confirmations (1 BLOCKER then 0); W2-A verifier 3× ECARTS on coverage clauses only, W2-B ECARTS(7) → CONFORME; gate A→B read from engine records | second confirmation paid again (BLOCKER on my own r2 slot); compound coverage criterion = endless ECARTS; engine jsonl replaces the live log |
|
||||
| EVAL-043 | 2026-10-11 | model-router W3-A: 3 lenses + 1 confirmation (census BLOCKER, project-tree layer dropped), feater DONE + 4 rounds, verifier 3× then re-verify after a REAL security BLOCK (credential fragment in the tracked key) | challenge + security gates both earned their cost; coverage-shaped criteria still cost 3 verifier rounds; live mod side effects misread as a test leak |
|
||||
|
||||
---
|
||||
|
||||
@@ -394,3 +395,11 @@ Dogfood: 3 blind lenses attacked the v1 plan for the plan-challenge feature itse
|
||||
- **Method**: 3 blind lenses (simplicity CONCERNS(5), correctness CONCERNS(10), robustness FATAL(8) → BLOCKER: mod off = agents inherit parent model); confirmation 1 robustness FATAL(8) → BLOCKER introduced by r2 (route calls wiped the sticky slot); confirmation 2 correctness CONCERNS(3), no BLOCKER → r4. W2-A: feater DONE + 4 rounds (1 internal decision, 3 coverage), GATE 0 MET, verifier ECARTS(3)/(1)/(1) all coverage, user accepted at cap; security PASS. W2-B: feater DONE first pass, verifier ECARTS(7) (5 FLOOR items = planned deletions needing a CLARIFICATIONS line, 2 prose, 1 scope add) → CONFORME 10/10; security PASS; full `make test` once (env red only). Gate A→B: 3/4 probes answered from engine jsonl, probe 4 (typed skill with a live agent) unobserved, recorded as a limit.
|
||||
- **Anomaly**: the user's rework answer ("delete or rework, not copy") changed the design mid-plan; r2's own fix carried a BLOCKER again (as in EVAL-041). Coverage criterion: 3 verifiers, 0 defects. FLOOR guard needs the test deletions named in CLARIFICATIONS, not only in criteria.
|
||||
- **Action**: keep the "second confirmation after a mechanism change" rule; write coverage criteria one clause each (LRN-210); when a plan deletes tests, write the authorizing CLARIFICATIONS line BEFORE the first verifier. Links [[EVAL-041]], [[LRN-210]], [[LRN-211]], [[BDR-115]].
|
||||
|
||||
## EVAL-043 — model-router W3-A: the gates caught two real defects, the coverage criteria still cost three verifier rounds
|
||||
- **Date**: 2026-10-11
|
||||
- **Output checked**: plan `.claude/tasks/plans/2026-10-10-model-router-w3a-confirm-1201.md` r1 → r4; diff 22455c0 (register.ts, routing.json, 190 kit tests, census).
|
||||
- **Method**: 3 lenses (simplicity CONCERNS(3), correctness FATAL(8): BLOCKER = an Everywhere decision turns the census red; robustness: first run died on a DNS error, fresh re-dispatch on r2 CONCERNS(8): project-tree layer = security hole, shared-promise dedupe burns hook budgets, per-layer degrade wipes rows); confirmation correctness CONCERNS(6) → r4. Feater DONE first pass (169 tests); verifier ECARTS(3) → feater → ECARTS(2) → feater → CONFORME; security BLOCK(1): `normalizeRemote` kept a password tail when it held `@` or `/` → fixed (strict URL/scp parsing, no `local:` keys, output cap) → re-verify ECARTS(1) (guard fixtures) → feater → re-scan PASS. Full `make test` once (env red only).
|
||||
- **Anomaly**: the live hot-loaded mod answered-by-user dialogs were first misread as a kit write leak (LRN-212). A GATE 0 CHECK written as a multi-line heredoc is not runnable by gates.sh (one line or a script). Criterion 3 again bundled ~15 clauses: three verifier rounds on coverage, zero code defects from them (LRN-210 not yet applied by me).
|
||||
- **Action**: write GATE 0 oracles as scripts from the start; split coverage criteria per clause BEFORE the first verifier; when a mod is under development, announce the live side effects at each dispatch. Links [[EVAL-042]], [[BDR-116]], [[LRN-212]].
|
||||
|
||||
|
||||
@@ -594,3 +594,6 @@ rules:
|
||||
|
||||
## 2026-10-10
|
||||
- model-router W2-B landed (1f2d33b) + gate A→B. User /reload-plugins + typed /status; I read the engine jsonl instead of the UI log: main low on typed /status, analyzer opus/xhigh at step 0 → ordering + row-over-frontmatter proven; probe 4 unobserved (user away), limit recorded. Route tool checked live (`mcp__model-router__route`, deferred → ToolSearch once; answer names the id); session itself routed through it (reflect → orchestrate). W2-B: contract 10 criteria, feater DONE first pass (43 files), verifier ECARTS(7): 5 FLOOR items (planned test deletions → CLARIFICATIONS line), `/route on` wording, false CLAUDE.global.md sentence, plan-challenger scope add → CONFORME 10/10; I folded 3 observations by hand (SDD sonnet implementers `effort="medium"`, run-slot droppers, 80-col). Security PASS (push-guard false positive on a grep pattern; it also bit my registry heredoc). Full make test green (env red only). Doc audit SIGNIFICANT → user: apply all P1-P8, SemVer BREAKING → 3.0.0, registries all → BDR-115 amendment 2, LRN-210/211, EVAL-042. Pending: doc commit, memory commit, user merge decision (gitflow finish, human signal), publish by hand.
|
||||
|
||||
## 2026-10-11
|
||||
- model-router W3-A (first-use confirmation) landed 22455c0 on feature/model-router-confirm. User asked for it after the W2 merge; 3 pass-B answers. Plan r1→r4: correctness BLOCKER (Everywhere decision = census red → `changed` WARN exemption), robustness (first challenger killed by DNS, fresh one: project-tree layer dropped for security, single dialog in flight, keep-previous after first load), confirmation CONCERNS(6). Feater DONE (169 tests) + 4 rounds. Verifier ECARTS(3)/(2)/CONFORME; security BLOCK(1) REAL: password with `@`/`/` landed in the tracked key → strict parsing, no `local:` keys, output cap → re-verify ECARTS(1) fixtures → PASS. The working-tree mod was hot-loaded by the engine: my own dispatches opened T2 dialogs the user answered (verifier Keep, feater Keep, security-auditor → implement → user reset to verify); first misread as a test leak (LRN-212). GATE 0 oracle as heredoc not runnable → script. Full make test green (env red only). Docs P1-P13 user-approved (patch in flight); BDR-116, LRN-212, EVAL-043 written. Next: doc commit, memory commit, user merge + publish by hand; T1/T3 live checks open.
|
||||
|
||||
@@ -1784,3 +1784,8 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
|
||||
## LRN-211 — Typed-slash routing: name-bound marker + idle fallback; run slot separate from turn routes; engine records are the live oracle
|
||||
- **Context**: mod needs "user typed /feat" from `skill.prompt`, which carries no origin. `prompt.submit` sees the raw `/name` first (composer|sdk|bridge): store the NAME (not a boolean; a bare flag leaked to the next preload), pending slot when mid-turn, consume only on the matching `skill.prompt`; fallback = no live/spawning loop AND allowed origin. Sticky run route in the SAME slot as turn routes was wiped by the first `route()` call (confirmation BLOCKER) → separate `runMain`, best-tier rows only (work/cheap rows leak low effort across turns). Live facts read from `~/.claude/projects/<repo>/<session>.jsonl` (+ `subagents/agent-*.jsonl`): `effort` + `message.model` per step. Typed `/status` → main low; analyzer step 0 opus/xhigh with frontmatter high (spawn bookkeeping precedes step 0; row beats frontmatter).
|
||||
- **Apply**: hook-side user-intent markers: bind to a name, add a pending slot for mid-turn, keep an ordering-independent fallback. Two lifetimes = two slots, never one slot with a source tag. Verify engine behaviour in the transcript jsonl, not in `$.ui.log`. Links [[BDR-115]], [[LRN-206]].
|
||||
|
||||
## LRN-212 — The engine hot-reloads a mod's hooks module from the working tree: unverified code runs live during its own feature run; dialogs answered there are facts; the kit never touches the disk
|
||||
- **Context**: W3-A, 2026-10-10. Mid-run, `routing.json` gained decisions nobody expected (verifier → judge, feater → judge). First read: a kit test wrote the real file. Truth (feater, transcript timestamps = file mtimes to the second): the engine had reloaded `register.ts` from the working tree through the `skills/model-router` link, the live mod opened the T2 dialog on MY verifier/feater spawns, the user answered them in the terminal. The kit cannot write: an unmocked `fs.write` is refused ("no implementation"), a throwing mock lands on the same refusal; the real file's sha was stable across 10+ suite runs.
|
||||
- **Apply**: while a mod is under development in this repo, its working-tree code is LIVE in every session (no `/reload-plugins` needed): expect its side effects (dialogs, writes) during the gates; tell the user what dialogs will pop and what to answer; reset the data file deliberately at the gate. Blame the kit last: check mtimes against the transcript before assuming a test leak. Live answers = criterion evidence (record them `[gated]`). Links [[BDR-116]], [[LRN-206]], [[LRN-211]].
|
||||
|
||||
|
||||
@@ -23,6 +23,13 @@ migration of shifters/pins/model-gate in wave 2 after proof; names model-router
|
||||
- [ ] (was) W2 gate A→B (user): `/reload-plugins`, then the live probe of plan § Gate (4 points: typed `/status` marker vs fallback in the verbose log; a real rowed spawn line + `step 0 agent` effort = spawn/first-step ordering; sonnet session typed `/feat` self-check + route answer; probe 1 again with a background agent alive). `typed-marker` never seen → W2-B blocked, A4 re-planned.
|
||||
- [x] (was) W2-B repo migration (plan § W2-B B0-B7 + STEP 6/7): bridge removal, `lib/effort-shift.md` rewrite, 15 citers → `route`, `effort=` on opus general-purpose dispatches, slim `lib/model-gate.md`, delete `model-check.sh` + `effort-pins.*` + their tests + install/update blocks, delete `skills/effort-*`, analyzer `effort: xhigh`, census rewrite (drift lock rows ↔ frontmatter), docs + registries (BDR-115 amendment, LRN typed-slash/run slot, EVAL)
|
||||
- [ ] W2-A residuals (security, accepted): first-load failure of the override activates the router despite `enabled:false` (fix = treat a failed first load as off); ReDoS on a self-authored prompt pattern (size-bounded); error text in the local log; model alias keys unvalidated (PHASE_KEY would do); `offers` map uncapped; `__proto__`/`constructor` override keys untested. Known limits: `skillCalls`/`spawning` counters are global; offers keyed by name only; builtin `/effort` is not a lever inside a run.
|
||||
## 2026-10-10 — model-router wave 3-A: first-use route confirmation + decision memory (feature/model-router-confirm)
|
||||
Plan `.claude/tasks/plans/2026-10-10-model-router-w3a-confirm-1201.md` r4, contract `2026-10-10-model-router-w3a-confirm-1201`. User decisions 2026-10-10: tracked routing.json reached through the plugin dir (no new link); blocking `$.ui.ask` dialog at first use; per skill row, agent row, main phase.
|
||||
- [x] W3-A landed (22455c0, 2026-10-11): routing.json = phases + rows + decisions; T1/T2/T3 dialogs; project exceptions `projects[<normalized remote>]` in the tracked file (project-tree layer dropped: security); writers = dialog + `/route ask`; `/route pending`; census from the file with the `changed` WARN exemption; kit 86 → 190. Gates: 3 lenses + 1 confirmation (BLOCKER census), feater + 4 rounds, verifier CONFORME + re-verify, security BLOCK(1) credential leak fixed → PASS, full make test green (design-tool-gate env red). Live: T2 dialogs answered by the user (verifier Keep, feater Keep, security-auditor → implement then reset to verify on user go).
|
||||
- [ ] W3-A live checks still open: T1 (typed `/feat` etc.) and T3 (first `route(phase=…)` call) dialogs; a parallel same-agent dispatch answered after > 10 s; what an unanswered dialog resolves to on the terminal (must be Later or nothing written). Record in the contract as `[gated]` when seen.
|
||||
- [ ] W3-A residuals (security LOW, accepted): non-atomic cross-process write (two sessions, crash mid-write → file reads as failed, previous config kept); `host/path` of a private remote in the tracked file; a persistent write failure re-asks every use (`asked.delete` in the catch); `out.length` vs byte size at the cap; `changePatch` scope 'project' with a vanished key writes Everywhere (cwd change mid-dialog). Deferred: `/route set`, `/route confirm`, `(unconfirmed)` in show, dialog edits of phases, frontmatter auto-alignment, session-start warning when routing.json is dirty, per-machine `projects`.
|
||||
- [ ] routing.json ships `confirmed` for verifier/feater (user Keeps) and phases verify/implement: every clone inherits them (by design: decisions travel). Revisit at release if unwanted.
|
||||
|
||||
- [ ] W2 residuals: probe 4 unobserved (above); the push-guard hook denies a read-only grep (or a heredoc) whose TEXT contains the push verb next to `git` (security-auditor + orchestrator 2026-10-10, false positives, reworded); ~27 loose "sonnet pin"/"opus pin" shorthand sites kept (true by census); README config key list omits `tiers`/`fallback`/`cooldownMinutes`/`mainUpgrade`/`upgradeMaxTokens` (doc audit item 6, pre-existing); MIGRATION.md "Upgrading to 3.0.0" at release time; `skillCalls`/`spawning` counters global; offers keyed by name.
|
||||
- [ ] W3 optional: step heuristics, haiku classifier, quota-aware downgrade, A/B
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# CONTRACT — model-router-w3a-confirm
|
||||
- date: 2026-10-10 | flow: feat | branch: feature/model-router-confirm (to start off develop)
|
||||
- status: active
|
||||
|
||||
## REQUEST (verbatim — IMMUTABLE)
|
||||
une fois fini j'aimerais que pour les premiere fois, les premiers switch de model et de'effort, on est un prompt qui demqnde de confirmer si on utilise bien ce model ou si moi j'en recommande un autre. Ca permet de voir si le routing correspond bien a nos besoin dans la pratique. sois faire un truc qui se souvient en userscop (si ca demande de confirmer la route sur un autre projet pour tel tache, que ca ne ele redemande jamais sur un autre projet. une memoire de ce qu'on decide, et ca met a jour la table de routing existante si il y a des changements, et si on fait un changement demander si c'est une exception pour ce projet ou non. Faire un truc qui demande au debut, mais qui est persistant une fois demander sur tout les projet et qui se redploi automatiquement comment tout le mod.
|
||||
|
||||
## CLARIFICATIONS
|
||||
Q: where does the decision memory live? / A: "le 1 [fichier suivi dans le repo], mais il faut qu'il soit lisible et écrivable par tous les projets, donc le déployer (ln -s) dans le .claude du home à l'install, comme le reste" → tracked `mods/model-router/routing.json`, reached from every project through the EXISTING link `~/.claude/skills/model-router` → `../mods/model-router` (no new link: the plugin's own directory, `$.plugin`, resolves to it) [gated 2026-10-10]
|
||||
Q: how is the question asked? / A: blocking dialog at the moment of the switch (`$.ui.ask`, the engine's AskUserQuestion), options Keep / Change / Later; never in headless; `/route ask off` cuts it [gated 2026-10-10]
|
||||
Q: granularity? / A: per skill row, per agent row, per main-loop phase declared through the route tool; each once, across projects [gated 2026-10-10]
|
||||
Q: public names (orchestrator default, user may veto): `/route pending`, `/route ask on|off` (`/route set` and `/route confirm` deferred after the simplicity lens); dialog texts in English like the rest of the mod; project exceptions live in `routing.json` under `projects[<normalized repo key>]` (r3: the project-tree file `<project>/.claude/model-router.json` was dropped after the robustness lens showed a cloned repo could re-route the user's gate agents and that writes would land in foreign trees) [stated 2026-10-10, user may veto]
|
||||
|
||||
Q: live T2 dialogs answered during the run (verifier → judge, feater → judge, Everywhere; the working-tree mod was hot-loaded by the engine without /reload-plugins) / A: user "Restaurer les deux" → routing.json reset to the shipped rows, confirmed/changed emptied; criterion 9 evidence: T2 dialog seen and written twice (texts and scope question confirmed live) [gated 2026-10-10]
|
||||
|
||||
Q: live decisions during the gates (verifier Keep, feater Keep, security-auditor → implement Everywhere) / A: user "Revenir à verify" for security-auditor (row reset, its confirmed/changed entries removed); the two Keeps stay. Criterion 9 evidence: T2 dialog seen 5 times live (Keep, Change + scope, dismissed/Later), the mod hot-loaded from the working tree by the engine [gated 2026-10-11]
|
||||
Q: security gate BLOCK(1) credential fragment in the remote key / A: fixed (strict URL/scp parsing, credentials never read), `local:` path keys removed (no remote → Everywhere/Later only), output size cap; re-verify ECARTS(1) on guard fixtures → closed; re-scan PASS [gated 2026-10-11]
|
||||
|
||||
## ACCEPTANCE CRITERIA
|
||||
1. `mods/model-router/routing.json` (tracked) is the single source of the phase table (11 full routes) and of the skill and agent rows (56 skill rows, 21 agent rows + Explore/Plan of plan r4 § Row tables), plus `confirmed` (skills/agents/phases → the confirmed phase) and `ask` (boolean); every row value is a phase key of that table; `DEFAULT_CONFIG.skills` and `.agents` in `register.ts` are `{}` (its `phases` stay as the fallback when the file is unreadable); rows and phases arrive from the file at load (session.start, or lazily once after a `/reload-plugins`), on `/route reload`, after each write, after `/clear` and on a cwd change.
|
||||
CHECK: bash .claude/tasks/contracts/w3a-routing-file.sh
|
||||
EXPECT: W3A-ROUTING-FILE
|
||||
EVIDENCE: MET exit=0 marker-found :: W3A-ROUTING-FILE
|
||||
2. Config layers, later wins: `routing.json` (phases, rows, then its `projects[<repo key>]` rows for the current repo) < `~/.claude/model-router.json` (machine override, its `ask: false` honored); a `.claude/model-router.json` inside the project tree is NEVER read; phases merge first (an invalid or missing routing.json phase falls back to the code default by name, logged), rows validate against the final table; a failed layer is skipped only at the first load, afterwards a failed read keeps the whole previous config; session toggles survive a rebuild; `/clear` keeps the config and re-reads at the next prompt. One kit test per clause.
|
||||
3. First use asks once, one dialog: a typed skill with a row (main, allowed origin), a rowed agent spawn without an explicit `model` (`parentAgentId` undefined), a main-loop phase declared through the route tool → `$.ui.ask` whose question carries the REAL next model id and effort (decideFor/mainEffort on main, spawnTarget + explicit effort on spawn) and the options Later / Keep / <alt phase> / <alt phase> (T3: Later / Keep, only for a phase-key call); the file is re-read right before the dialog and the key re-checked as decided (another session's decision is seen); Keep writes `confirmed.<kind>.<name> = phase` and `confirmed.phases[phase]` and applies the row; an alt or a valid "Other" phase asks the scope (Everywhere → `routing.json` row + `changed.<kind>.<name> = {from, to}`; This project only → `routing.json` `projects[<normalized repo key>]` row, base row untouched, `confirmed` = the base row so no other project asks; one serialized write; a key failure offers Everywhere only), the new route applies to the current decision at once; any other answer (Later, dismissed, rejected in headless, unknown phase) → default applied, nothing written, not asked again this session; at most one dialog in flight: a concurrent use (same or another key) applies its current route unasked; a phase endorsed by a Keep is not asked at T3; a row that exists in `projects[key]` or in the machine override counts as decided; never asked inside a sub-agent (`parentAgentId` set), with an explicit `model` param, with the mod off, with `ask` false, or while routing.json is unreadable. One kit test per clause.
|
||||
4. Writes happen only from a dialog answer or the composer `/route ask` command (never from the route tool, a prompt rule, or any model-originated event); only to `${$.plugin.root}/routing.json`, never into a project tree; the writer refuses when the file is absent or unparsable (toast, answer = Later) and never creates it; read-modify-write of the whole JSON (2-space, key order phases, skills, agents, projects, confirmed, changed, ask), serialized through one promise chain; a write or rebuild failure logs once, applies the default and leaves the key unasked; after a write the config is rebuilt (swapped only on a successful read) and a toast says "routing.json updated: commit it from the config repo (chore branch)". One kit test per clause + reading.
|
||||
5. `/route pending` lists the rows and phases not yet confirmed (asked this session first, then the rest); `/route ask on|off` toggles asking and writes `ask`; `/route reload` re-reads the three layers. (`set`, `confirm`, a show suffix: deferred.) One kit test per command.
|
||||
6. The kit suite and the mods suite are green; `claude plugin validate` passes.
|
||||
CHECK: cd mods/model-router && out="$(claude plugin test . 2>&1)" && printf '%s\n' "$out" | grep -qE '[0-9]+ pass' && ! printf '%s\n' "$out" | grep -qE '[1-9][0-9]* fail' && claude plugin validate . 2>&1 | grep -q 'passed' && cd ../.. && make test suite=lib/tests/mods.test.sh 2>&1 | grep -q 'all suites green' && echo W3A-MOD-GREEN
|
||||
EXPECT: W3A-MOD-GREEN
|
||||
EVIDENCE: MET exit=0 marker-found :: W3A-MOD-GREEN
|
||||
7. `lib/tests/effort-routing.test.sh` reads rows AND phases from `routing.json` (only the tier heads still come from `register.ts`), locks `DEFAULT_CONFIG.phases` equal to the file's phases, keeps the drift lock except for a row with a `changed.<kind>.<name>` entry whose `from` route equals the frontmatter and whose `to` equals the row (then a `WARN floor drift` line, no FAIL; a Keep-only drift, an empty or unknown frontmatter value, a hand edit after a change still FAIL; flip-tested), and is green; `lib/effort-shift.md` names the first-use dialog, `/route pending` and `/route ask` in ≤ 6 added lines.
|
||||
CHECK: grep -q 'routing.json' lib/tests/effort-routing.test.sh && [ "$(grep -c 'register.ts' lib/tests/effort-routing.test.sh)" -le 3 ] && grep -q 'floor drift' lib/tests/effort-routing.test.sh && bash lib/tests/effort-routing.test.sh >/dev/null 2>&1 && grep -q '/route pending' lib/effort-shift.md && [ "$(wc -l < lib/effort-shift.md)" -le 66 ] && echo W3A-CENSUS-DOC
|
||||
EXPECT: W3A-CENSUS-DOC
|
||||
EVIDENCE: MET exit=0 marker-found :: W3A-CENSUS-DOC
|
||||
8. Hook budget: no dialog or file write on the `turn.step` path; a `$.ui.ask` in flight never blocks a second, unrelated spawn of another agent name beyond the dialog itself; the ask is awaited outside `safely` in the owning hook with a `.catch` → Later. Judged by reading.
|
||||
9. Live checks after the user's `/reload-plugins` (EVIDENCE lines added by the orchestrator; the answers committed on the branch before finish): one dialog at each of the three sites; a parallel same-agent dispatch answered after more than 10 s routes the unowned spawn without a timeout; an unanswered dialog on the terminal resolves to "Later" or to nothing written.
|
||||
|
||||
## FILE SCOPE
|
||||
mods/model-router/routing.json (new), mods/model-router/hooks/register.ts, mods/model-router/hooks/register.test.ts, lib/tests/effort-routing.test.sh, lib/effort-shift.md; .claude/tasks/contracts/w3a-routing-file.sh (oracle, orchestrator)
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# GATE 0 oracle, contract 2026-10-10-model-router-w3a-confirm: routing.json
|
||||
# is the single source of phases + rows; register.ts rows are empty.
|
||||
set -u
|
||||
python3 - <<'PY'
|
||||
import json,re,sys
|
||||
r=json.load(open('mods/model-router/routing.json'))
|
||||
src=open('mods/model-router/hooks/register.ts').read()
|
||||
ph=set(re.findall(r"^\s+([a-z]+): \{ tier:", re.search(r"phases: \{(.*?)\n \}", src, re.S).group(1), re.M))
|
||||
jp=set(r.get('phases',{}).keys())
|
||||
ok = jp==ph and len(r['skills'])==56 and len(r['agents'])==23
|
||||
ok = ok and all(v in jp for v in list(r['skills'].values())+list(r['agents'].values()))
|
||||
ok = ok and isinstance(r.get('confirmed'),dict) and isinstance(r.get('ask'),bool) and 'version' not in r
|
||||
ok = ok and bool(re.search(r"\n skills: \{\},", src)) and bool(re.search(r"\n agents: \{\},", src))
|
||||
print('W3A-ROUTING-FILE' if ok else 'W3A-ROUTING-BAD'); sys.exit(0 if ok else 1)
|
||||
PY
|
||||
@@ -0,0 +1,288 @@
|
||||
# PLAN r4 — model-router wave 3-A: first-use route confirmation + decision memory + project exceptions (2026-10-10)
|
||||
|
||||
r1 → r2 after simplicity CONCERNS(3) + correctness FATAL(8); r2 → r3 after
|
||||
robustness CONCERNS(8) (fresh dispatch on r2 after the first died on a
|
||||
network error); r3 → r4 after the confirmation pass (correctness
|
||||
CONCERNS(6), no BLOCKER). Precedence: r4 > r3 > r2 > r1 where they differ. Contract
|
||||
`.claude/tasks/contracts/2026-10-10-model-router-w3a-confirm-1201.md`.
|
||||
Facts (kit types, checked 2026-10-10): `$.ui.ask(question, {options,
|
||||
header})` opens the engine's AskUserQuestion dialog, resolves to the chosen
|
||||
label or the "Other" free text, REJECTS when dismissed and in a `-p` run;
|
||||
`$.fs.read/stat/exists/write` take absolute paths, write creates
|
||||
directories; `$.session.root()` = the project root (follows /cd);
|
||||
`$.plugin.root` = the plugin directory, absolute (routing.json =
|
||||
`${$.plugin.root}/routing.json`, reached through the existing
|
||||
`~/.claude/skills/model-router` link); `$.ui.toast`; `$` calls stop the
|
||||
hook budget clock. The kit has no fs: tests mock bottom `fs.*`, `env.get`,
|
||||
`session.root` and `tool.call AskUserQuestion` hooks.
|
||||
|
||||
## Data (one tracked source for rows AND phases; r3)
|
||||
- `mods/model-router/routing.json` (tracked):
|
||||
`{ "phases": {11 full routes}, "skills": {56}, "agents": {23},
|
||||
"projects": { "<repo key>": { "skills": {}, "agents": {} } },
|
||||
"confirmed": { "skills": {}, "agents": {}, "phases": {} },
|
||||
"changed": { "skills": {}, "agents": {} }, "ask": true }`
|
||||
`confirmed.<kind>.<name>` = the routing.json phase endorsed by a Keep;
|
||||
`changed.<kind>.<name>` = `{ "from": <shipped phase>, "to": <phase> }` for
|
||||
an "Everywhere" change (the census reads `from`). `projects[key]` = the
|
||||
"This project only" exceptions. Key (r4) = the origin remote normalized:
|
||||
scheme and userinfo stripped, host lowercased, `.git` and trailing slash
|
||||
removed (`github.com/acme/app`); no remote → `local:<root realpath>`
|
||||
(meaningful on that machine only, stated in the toast); `session.repo`
|
||||
or the realpath failing → the `projects` layer is skipped and ASK2
|
||||
offers Everywhere only. Resolved once per decision and re-resolved on
|
||||
`classic.CwdChanged` (rebuild). No version key, no dates. Never a URL
|
||||
with credentials in the file.
|
||||
- NEVER read from the project tree: `<root>/.claude/model-router.json` is
|
||||
not a layer (a cloned repo must not route the user's agents). Layers,
|
||||
later wins: routing.json (phases, rows, then `projects[key]` rows) <
|
||||
`~/.claude/model-router.json` (machine override; its `ask: false` also
|
||||
honored). Phases merge first, rows validate against the final table.
|
||||
- Phases: routing.json `phases` replaces `DEFAULT_CONFIG.phases` route by
|
||||
route; an invalid or missing phase falls back to `DEFAULT_CONFIG.phases
|
||||
[name]` with one log line (a typo never removes a phase); alts offered
|
||||
by the dialog are filtered against the final table.
|
||||
- Loading: full read at session.start, and LAZILY once when the State was
|
||||
never loaded (first prompt.submit / skill.prompt / agent.spawn / route
|
||||
tool after a `/reload-plugins`, which re-runs register() without
|
||||
session.start); a failed layer is skipped and logged at that first load
|
||||
only; afterwards any failed read, and a routing.json that went missing,
|
||||
keeps the WHOLE previous config (today's kill-switch rule), and no ask
|
||||
runs while routing.json is unreadable. `/route show` prints `config:
|
||||
routing.json` once loaded. `resetSession` keeps its current set (cfg
|
||||
included); the layers are re-read at the next `prompt.submit` and the
|
||||
cfg swapped only after a fully successful read. Session toggles
|
||||
(`/route switch`, `verbose`) live in State, re-applied after any rebuild,
|
||||
and added to resetSession's kept set (they survive /clear as today).
|
||||
- `DEFAULT_CONFIG.skills/.agents` = `{}`; the file is the source. A
|
||||
missing routing.json → DEFAULT phases, no rows, one log, no asks, no
|
||||
writes (the writer never creates the file).
|
||||
|
||||
## Dialog (main loop only; `ask` true; mod on; never inside an agent)
|
||||
- Trigger sites and guards:
|
||||
T1 typed skill with a row: `applyTypedSkill` made async, awaited in the
|
||||
skill.prompt hook OUTSIDE `safely`: route FIRST (`routeMainBySkill`,
|
||||
typed=true), build the text from the route now in force, ask, and on
|
||||
a change re-run `routeMainBySkill` with the recomputed `skillRow`
|
||||
(the model's `Skill` path never asks).
|
||||
T2 rowed agent spawn: in `registerSpawn` before `spawnTarget`, awaited;
|
||||
guards `e.parentAgentId === undefined`, not frozen, not shadowed, and
|
||||
`e.model === undefined` (an explicit model is not a routing decision).
|
||||
T3 route tool phase on main (`handleRouteTool`, no agentId, and only
|
||||
when `e.phase` is a phase key — never for an effort-only call):
|
||||
confirm-only.
|
||||
- One dialog (ASK1), 4 options: question text built from the REAL decision:
|
||||
main → `decideFor`/`mainEffort` like `mainRoutedText` ("First route for
|
||||
/feat: reflect, next step claude-fable-5-1 at high. Keep it?"); spawn →
|
||||
`spawnTarget` + the explicit `effort` param when given ("First dispatch
|
||||
of feater: implement, claude-sonnet-5-5 at medium. Keep it?"); T3 →
|
||||
"First use of orchestrate on the main loop: claude-fable-5-1 at medium.
|
||||
Keep it?" with options ["Later", "Keep"] only.
|
||||
Options T1/T2: ["Later", "Keep", <altA>, <altB>] (Later FIRST: an idle
|
||||
auto-pick, if any surface does one, must never write), header
|
||||
"model-router";
|
||||
alts = the first two of [plan, reflect, implement, apply] (skills) or
|
||||
[judge, implement, verify, apply] (agents) minus the current phase;
|
||||
"Other" free text = a phase key of the final table (else toast "unknown
|
||||
phase <x>, default kept" → Later); an Other equal to the current phase
|
||||
= Keep. Any answer that is not exactly Keep, an alt, or a valid Other →
|
||||
Later (covers dismissed, rejected, auto-resolved idle answers). ASK2
|
||||
accepts only its two labels; anything else = Later.
|
||||
Before opening any dialog the mod re-reads routing.json (the pre-ask
|
||||
read) and re-checks `ask` AND "decided" for the key from that fresh
|
||||
content, so a decision taken in another live session is seen.
|
||||
- Keep → `confirmed.<kind>.<name> = <routing.json phase>` AND
|
||||
`confirmed.phases[<phase>] = <phase>` (a Keep endorses the phase: no
|
||||
duplicate T3 later); the row applies. "Decided" = `confirmed` equals the
|
||||
routing.json row, OR a `projects[key]` / machine-override row exists for
|
||||
that name (presence = decided; never compared across layers).
|
||||
- Change (alt/Other) → ASK2 scope ["Everywhere", "This project only"].
|
||||
Everywhere → routing.json row + `changed.<kind>.<name> = {from, to}`
|
||||
(`from` kept from the FIRST entry when one exists, `to` = the new row) +
|
||||
`confirmed.<kind>.<name> = to`; This project only → `projects[key]
|
||||
.<kind>.<name> = phase` (routing.json row untouched) + `confirmed.<kind>
|
||||
.<name> = the BASE routing.json row` (so no other project asks again). One file, one serialized write.
|
||||
Then rebuild; the new route applies to the current decision at once
|
||||
(T1: recompute `skillRow`; T2: recompute `spawnRoute` and pass it to the
|
||||
spawn bookkeeping). A write or rebuild failure → log once, default row
|
||||
applied, key left unasked (never escapes to the hook's `.catch`).
|
||||
- Later → default applies, nothing written.
|
||||
- One dialog in flight per session, ever (`st.asking: string | null`):
|
||||
a use that finds a dialog open (same key or another) applies its current
|
||||
route and stays unasked (asked later); the owner alone runs ASK1, ASK2
|
||||
and the write. A parallel spawn never awaits a promise it did not create
|
||||
(hook budget: only the owner's `$` call stops the clock). Asked-this-
|
||||
session = `Set<key>` in State, dropped by resetSession.
|
||||
- Never on `turn.step`. The ask is awaited in the owning hook with
|
||||
`.catch(() => 'Later')`.
|
||||
|
||||
## Commands (composer only, as `/route` today)
|
||||
- `/route pending` → keys not yet confirmed (asked-this-session first).
|
||||
- `/route ask on|off` → `ask` in routing.json (write path below); the
|
||||
flag is re-read (one `$.fs.read`) right before each ask so another
|
||||
session's change is seen.
|
||||
- `/route reload` → re-reads the three layers. (`set`, `confirm`, a show
|
||||
suffix: deferred to TODO; the dialog is the writer.)
|
||||
|
||||
## Writes
|
||||
- `writeRouting($, st, patch)`: read-modify-write of the whole JSON
|
||||
(2-space; key order phases, skills, agents, projects, confirmed, changed,
|
||||
ask), serialized through ONE promise chain in State; refuses (toast,
|
||||
answer treated as Later) when routing.json is absent or unparsable:
|
||||
never creates the file; size cap as the override, `readCapped` takes
|
||||
the file label; after a write: rebuild (swap only on a successful read)
|
||||
+ toast "routing.json updated: commit it from the config repo (chore
|
||||
branch, `gitflow.sh start chore …`)".
|
||||
- Writers: dialog answers and composer `/route ask` ONLY. The route tool
|
||||
(model), prompt rules and sub-agents never write. Nothing is ever
|
||||
written into a project tree.
|
||||
|
||||
## Census and floors (BLOCKER closed)
|
||||
- `lib/tests/effort-routing.test.sh`: rows AND phases read from
|
||||
routing.json (python3 json); tier heads still parsed from `register.ts`
|
||||
`tiers` (unchanged by this wave; the only register.ts read left); a new
|
||||
lock: `DEFAULT_CONFIG.phases` values == routing.json phases (the fallback
|
||||
never applies a stale route). The drift lock stays for every row EXCEPT
|
||||
one with a `changed.<kind>.<name>` entry whose `from` phase route equals
|
||||
the frontmatter AND whose `to` equals the current row: then one `WARN
|
||||
floor drift: <file> <value> vs row <to> (<value>)` line, no FAIL. A
|
||||
Keep-only row, an empty or unknown frontmatter value, a hand edit after
|
||||
a change (`to` ≠ row), or a drift not matching `from` still FAILs.
|
||||
- BDR-115 amendment 2(a) "census-locked equal" → amended to "equal unless
|
||||
the row is a confirmed user decision (floor may lag; WARN)" at STEP 7.
|
||||
|
||||
## Steps
|
||||
- [ ] S1 routing.json from the current DEFAULT_CONFIG (phases + rows +
|
||||
Explore/Plan), `projects`/`confirmed`/`changed` empty, `ask` true;
|
||||
DEFAULT rows → `{}`; `loadLayers` (two files + `projects[key]`),
|
||||
first-load degrade then keep-previous rule, phase fallback by name,
|
||||
session toggles in State, resetSession kept set unchanged + re-read
|
||||
at the next prompt.submit.
|
||||
- [ ] S2 ask engine: `askFirst($, st, kind, name, text, options)` with the
|
||||
single-dialog guard + asked set; `decide(answer)`; `applyDecision`
|
||||
(Keep / change + scope) → serialized write → rebuild → recompute,
|
||||
wrapped so a failure logs once and applies the default.
|
||||
- [ ] S3 wiring T1 (async applyTypedSkill), T2 (registerSpawn), T3
|
||||
(handleRouteTool, confirm-only).
|
||||
- [ ] S4 commands `pending`, `ask on|off`; `reload` reads three layers.
|
||||
- [ ] S5 census (rows + phases from JSON, drift lock with the decision
|
||||
exception) + `lib/effort-shift.md` ≤ 6 added lines (dialog, `/route
|
||||
pending`, `/route ask`).
|
||||
- [ ] S6 kit tests. First: `boot()`/`bootRun()` gain a path-aware fixture
|
||||
(bottom `fs.exists`/`fs.stat` (+ `realPath`)/`fs.read`/`fs.write`,
|
||||
`env.get` HOME, `session.root`, `session.repo`) serving an inline
|
||||
routing.json with `ask: false`
|
||||
unless a test opts in (`boot($, on, {ask: true, project: {...},
|
||||
home: {...}})`); the existing `{verifier: null}` test becomes
|
||||
path-aware; all 86 tests green again. Then one test per clause:
|
||||
layer precedence (routing.json rows < projects[key] < machine
|
||||
override, null drop; a `.claude/model-router.json` in the project
|
||||
tree is NEVER read; machine `ask:false` honored); typed `/feat` first use → AskUserQuestion mock
|
||||
sees the id + "high" → "Keep" → fs.write of routing.json captured
|
||||
with confirmed.skills.feat = reflect AND confirmed.phases.reflect →
|
||||
second typed `/feat` → no ask; alt "implement" → ASK2 → "Everywhere"
|
||||
→ routing.json row + main routed implement now; "This project only"
|
||||
→ project file written, routing.json row untouched, confirmed in
|
||||
routing.json; "Later"/reject/free text garbage → default, no write,
|
||||
no second ask this session; agent first spawn → ask → Keep → model
|
||||
written; explicit `model` param → no ask; two parallel spawns → one
|
||||
ask, the second spawn routed on the current row without waiting; parentAgentId set → no ask; route tool phase first use → ask
|
||||
(Keep/Later) → Keep → confirmed.phases; phase already endorsed by a
|
||||
T1 Keep → no T3 ask; `/route ask off` → no asks + write; `/route
|
||||
pending` text; routing.json unreadable at start → DEFAULT phases, empty
|
||||
rows, one log, no asks; unreadable at a later rebuild → previous cfg
|
||||
kept; missing file → `/route ask off` refused with a toast, file not
|
||||
created; a phase typo in routing.json → DEFAULT phase by name + log,
|
||||
alts filtered; `/route switch on` survives a rebuild; writes are
|
||||
serialized (two decisions, one file, both present); row edited by
|
||||
hand after a Keep → asked again; census flip-tests: confirmed-only
|
||||
drift FAILs, `changed.from` drift WARNs, empty `model:` FAILs.
|
||||
- [ ] S7 live checks after the user's /reload-plugins (EVIDENCE lines,
|
||||
answers committed on the branch before finish): `/route show`
|
||||
prints `config: routing.json` right after the reload (lazy load);
|
||||
one dialog at each of the three sites; a parallel same-agent dispatch answered after
|
||||
more than 10 s (the unowned spawn must be routed, not timed out);
|
||||
what an unanswered dialog resolves to on the terminal (and on sdk/
|
||||
bridge if reachable): any auto-pick must land on "Later".
|
||||
- Disposition: honors BDR-115 (user writers only; full ids in texts;
|
||||
amendment 2(a) to be amended), BDR-107/108 (phases unchanged), LRN-206
|
||||
(kit facts), LRN-210 (one clause per test), LRN-211 (typed path only).
|
||||
- Deferred (TODO): `/route set`, `/route confirm`, show suffix, dialog
|
||||
edits of phases, frontmatter auto-alignment, a session-start warning
|
||||
when routing.json is dirty, per-machine `projects` (today one tracked
|
||||
map keyed by repo).
|
||||
|
||||
## Challenge ledger (r1 → r2)
|
||||
- correctness 1 BLOCKER + simplicity 3 (census red on a decision) → drift
|
||||
lock with the confirmed-decision exception (WARN), BDR-115 2(a) amended.
|
||||
- correctness 2/5, simplicity 2 (partial phase overrides, blast radius) →
|
||||
phases move to routing.json as a full table, dialog never edits phases,
|
||||
T3 confirm-only, project layer rows only.
|
||||
- correctness 3, simplicity 10 (kit loses rows) → S6 path-aware fixture first.
|
||||
- correctness 4 (texts from the real decision) → ASK1 texts from
|
||||
decideFor/mainEffort and spawnTarget; T2 skipped on explicit model.
|
||||
- correctness 6, simplicity 9 (sync site) → async applyTypedSkill awaited
|
||||
in the hook.
|
||||
- correctness 7 (root == HOME) → layer 3 skipped, ASK2 skipped.
|
||||
- correctness 8/9/10/16/17 (layers, /clear, validation order, stale root,
|
||||
concurrent writes) → re-read from disk on every rebuild, per-layer
|
||||
degrade, phases first, root re-resolved, one write chain.
|
||||
- correctness 11/12 (Other, counts), simplicity 4/7 → one 4-option dialog,
|
||||
2 alts, non-matching answers = Later.
|
||||
- correctness 13 (confirmed location) → always routing.json.
|
||||
- correctness 14/15 (parentAgentId, fs.stat label) → written in.
|
||||
- correctness 18 (no live proof) → S7.
|
||||
- simplicity 1 (commands) → pending + ask only; set/confirm deferred.
|
||||
- simplicity 5/6/8 → confirmed = phase, no version, one Map, Keep endorses
|
||||
the phase.
|
||||
- simplicity 11 → project layer rows only.
|
||||
|
||||
## Challenge ledger (r2 → r3, robustness)
|
||||
- rob 1/9 (shared promise burns the awaiters' budget; stacked dialogs) →
|
||||
one dialog in flight, owner-only; concurrent uses apply the current
|
||||
route unasked; S7 >10 s check.
|
||||
- rob 2 (per-layer degrade wipes rows, reopens the kill switch) → degrade
|
||||
at first load only, then keep-previous; no asks while unreadable.
|
||||
- rob 3 (a phase typo removes the phase, gate STOPs everywhere) → fallback
|
||||
to DEFAULT phase by name + log; alts filtered.
|
||||
- rob 4 (writer creates a missing file) → writer refuses, never creates.
|
||||
- rob 5 (global confirmed vs layered rows re-asks forever) → confirmed
|
||||
compared with the routing.json row only; layer rows = decided by
|
||||
presence; one file, one write.
|
||||
- rob 6 (project-tree layer = security hole + foreign writes) → layer
|
||||
removed; exceptions in routing.json `projects[key]`; nothing written in
|
||||
a project tree.
|
||||
- rob 7 (drift exception too wide) → exemption only for `changed.from`
|
||||
matches; Keep-only, empty and unknown values FAIL.
|
||||
- rob 8 (/clear drops cfg/kill switch/breaker) → kept set unchanged;
|
||||
re-read at next prompt.submit, swap on success.
|
||||
- rob 10 (session toggles reverted by rebuilds) → toggles in State.
|
||||
- rob 11 (dirty tracked repo, S7 answers) → toast names the chore flow;
|
||||
S7 answers committed before finish; session-start warning deferred.
|
||||
- rob 12 (idle auto-pick) → "Later" first; S7 verifies.
|
||||
- rob 13 (`ask` only in the tracked file) → machine override `ask`
|
||||
honored; flag re-read before each ask.
|
||||
- rob 14 (write failure escapes) → applyDecision wrapped.
|
||||
|
||||
## Confirmation ledger (r3 → r4, correctness)
|
||||
- conf 1 (tier heads) → census keeps a tiers parser on register.ts; AC7
|
||||
CHECK narrowed to the rows/phases parser.
|
||||
- conf 2 (rows lost after /reload-plugins) → lazy load once; S7 line.
|
||||
- conf 3 (T1 text before routing) → route first, text, ask, re-route.
|
||||
- conf 4 (exception re-asks elsewhere) → confirmed = base row on "This
|
||||
project only"; test X then Y.
|
||||
- conf 5 (multi-session re-ask) → pre-ask read re-checks decided.
|
||||
- conf 6 (raw remote URL / machine path as key) → normalized key, no
|
||||
userinfo, `local:` fallback, key failure skips projects only.
|
||||
- conf 7 (leftovers, un-gated move of the exception file) → S6/contract
|
||||
fixed; named to the user at the gate.
|
||||
- conf 8 (fixture) → session.repo + realPath mocked; key failure scoped.
|
||||
- conf 9 (/cd) → rebuild on `classic.CwdChanged`.
|
||||
- conf 10 (changed.from on a second change) → first `from` kept, exempt
|
||||
only when row == to.
|
||||
- conf 11 (unspecified answers) → ASK2 two labels only; Other == current
|
||||
= Keep.
|
||||
- conf 12 (DEFAULT phases drift) → census lock DEFAULT == routing.json.
|
||||
- conf 13 (effort-only route call) → T3 only for a phase key.
|
||||
- conf 14 (later absence) → treated as failed, previous kept.
|
||||
- conf 15 (toggles across /clear) → added to the kept set.
|
||||
Reference in New Issue
Block a user