From cd91ddabb2960f4f261c5828bc6446d81e946806 Mon Sep 17 00:00:00 2001 From: Daniel Polo <106583643+danielPoloWork@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:43:33 +0200 Subject: [PATCH] feat(commands): surface routing advice - triage, status, planning protocol (M16 16.3) Closes #254. - triage.yaml: routing_advice block - once a request routes to a focused-change or the five-step-loop, the host states the recommended tier/effort (advisory, ADR-0017); answer-directly needs none - doctor.py: opt-in --routing-milestone TITLE readout - one advisory line per open issue via route_advice (pure routing_lines helper); SKIP offline, never changes the exit code - .issues/README.md (new): the planning protocol - plan-doc Routing column + the Routing: body line + the sets-pattern/decision-heavy flags; commands/README + status.md updated - test_doctor.py: routing_lines checks (47/47 suite green) M15 backfill (#234-#250 body lines) runs as a tracker action alongside this PR. Co-Authored-By: Claude Fable 5 --- .eados-core/orchestrator/commands/README.md | 6 +++ .eados-core/orchestrator/commands/status.md | 9 +++++ .eados-core/orchestrator/triage.yaml | 10 +++++ .eados-core/tools/doctor.py | 40 +++++++++++++++++++ .eados-core/tools/tests/test_doctor.py | 18 +++++++++ .issues/README.md | 44 +++++++++++++++++++++ CHANGELOG.md | 12 ++++++ 7 files changed, 139 insertions(+) create mode 100644 .issues/README.md diff --git a/.eados-core/orchestrator/commands/README.md b/.eados-core/orchestrator/commands/README.md index 38775c2..28bb3f6 100644 --- a/.eados-core/orchestrator/commands/README.md +++ b/.eados-core/orchestrator/commands/README.md @@ -23,6 +23,12 @@ to the **minimal** path — the ordered, **stop-at-first-match** procedure in gate (#224). The point is to avoid firing the whole pipeline at a one-line doc fix while keeping a maintenance edit under the same ownership + human-gate rules as everything else. +Once the route is decided (a `focused-change` or the `five-step-loop`), the host also **states the +recommended model tier + effort** for the work ahead — `triage.yaml` `routing_advice`, the +[`os/routing`](../os/routing/_schema.md) policy (ADR-0017) evaluated by +[`route_advice.py`](../../tools/route_advice.py). Advisory only: the human keeps final model +authority, and the session model is never switched by the agent. + | Command | Phase | Status | Procedure | |---------|-------|--------|-----------| | `/eados init` | init | **available** (M1) | [`init.md`](init.md) | diff --git a/.eados-core/orchestrator/commands/status.md b/.eados-core/orchestrator/commands/status.md index 723bdeb..9e5814c 100644 --- a/.eados-core/orchestrator/commands/status.md +++ b/.eados-core/orchestrator/commands/status.md @@ -24,6 +24,15 @@ By default it looks for `ROADMAP.md` and `links.yaml` next to the manifest; `--r `--links` override. It **exits non-zero when it finds an actionable problem** (an undeclared phase, an uncovered RFC, or a dangling traceability edge), so it doubles as a pre-flight check. +### Routing advice (optional) + +`--routing-milestone "TITLE"` appends one **advisory** line per open issue of that milestone — +`#N -> tier/effort (model)` — from the [`os/routing`](../os/routing/_schema.md) policy (ADR-0017) +via [`route_advice.py`](../../tools/route_advice.py). Label-only: asserted flags (`sets-pattern` / +`decision-heavy`) may raise a route further; the reviewed call lives as the `Routing:` line in the +issue body (see [`.issues/README.md`](../../../.issues/README.md)). It needs `gh` and degrades to +`SKIP` offline; **advice never changes the exit code** — the human keeps final model authority. + ## Boundary Assessment only — `status` makes no change, proposes no transition, and runs no phase tool that diff --git a/.eados-core/orchestrator/triage.yaml b/.eados-core/orchestrator/triage.yaml index 4a33ee3..54944aa 100644 --- a/.eados-core/orchestrator/triage.yaml +++ b/.eados-core/orchestrator/triage.yaml @@ -25,6 +25,16 @@ steps: route: five-step-loop # interview -> resolve profile -> manifest -> render -> verify/PR enters_pipeline: true +# M16 16.3 (#254): once the route is decided, ALSO state the recommended model tier + effort for +# the work ahead - the os/routing policy (ADR-0017) evaluated by tools/route_advice.py +# (offline: --labels "a,b" [--flags sets-pattern,decision-heavy]). Advisory only: the host states +# tier/effort + today's catalog model and moves on; it never switches the session model - that +# stays the human's call. `answer-directly` needs no advice (no work is being routed). +routing_advice: + applies_to: [focused-change, five-step-loop] + tool: tools/route_advice.py + boundary: "advisory only - the human keeps final model authority (ADR-0017)" + # #225 / #224: worked examples of the classification, shape-checked by eados_lint's `examples` gate. # `verdicts` are the `steps[].route` names, so the examples stay congruent with the routes they teach. examples: diff --git a/.eados-core/tools/doctor.py b/.eados-core/tools/doctor.py index 9b7879f..c447449 100644 --- a/.eados-core/tools/doctor.py +++ b/.eados-core/tools/doctor.py @@ -24,6 +24,7 @@ import render # noqa: E402 — the hand-rolled YAML loader import phase_runner # noqa: E402 — current phase + legal transitions (the deterministic engine) import traceability # noqa: E402 — coverage + dangling-edge checks +import route_advice # noqa: E402 — advisory model/effort routing (M16 16.3; opt-in readout) def _state(workflow, phase): @@ -100,6 +101,24 @@ def status_report(manifest, workflow, roadmap_text=None, links=None): return lines, healthy +def routing_lines(issues, spec, host=None): + """Advisory model/effort routing for a milestone's open issues (M16 16.3 / #254). Pure: + `issues` are {number, title, labels[]} dicts. One line per issue from the label-only advice — + asserted flags (`sets-pattern` / `decision-heavy`) may raise a route further; the reviewed + call lives as the `Routing:` line in the issue body. Advisory only (ADR-0017): this readout + never touches health or the exit code.""" + lines = [] + for it in issues: + adv = route_advice.advise( + route_advice.signals_for(it.get("labels") or [], [], spec), spec, host=host) + lines.append(f"#{it.get('number')} -> {adv['tier']}/{adv['effort']} ({adv['model']}) " + f"{it.get('title')}") + if lines: + lines.append("advisory only - the human keeps final model authority (ADR-0017); " + "asserted flags may raise a route") + return lines + + def _read(path): with open(path, encoding="utf-8") as handle: return handle.read() @@ -116,6 +135,9 @@ def main(argv=None): ap.add_argument("--root", help="project root for ROADMAP.md / links (default: the manifest's dir)") ap.add_argument("--roadmap", help="path to ROADMAP.md (default: /ROADMAP.md)") ap.add_argument("--links", help="traceability links file (default: /links.yaml if present)") + ap.add_argument("--routing-milestone", metavar="TITLE", + help="append advisory model/effort routing for the milestone's open issues " + "(needs gh; degrades to SKIP offline; never changes the exit code)") args = ap.parse_args(argv) try: @@ -136,6 +158,24 @@ def main(argv=None): print(f"EADOS status - {args.manifest}") for line in lines: print(f" {line}") + + # M16 16.3 (#254): opt-in advisory routing column. Advice is a readout, never a gate — a gh + # outage or a broken routing spec is reported loudly but does not change the health verdict + # (the spec's own integrity is eados_lint's + route_advice's loud-rejection job). + if args.routing_milestone: + print(f" routing advice - milestone '{args.routing_milestone}':") + try: + spec = route_advice.load_routing() + issues = route_advice.fetch_milestone_issues(args.routing_milestone) + out = routing_lines(issues, spec) or \ + [f"no open issues in milestone '{args.routing_milestone}'"] + except RuntimeError as exc: + out = [f"SKIP - {exc}"] + except (OSError, ValueError) as exc: + out = [f"ERROR - {exc}"] + for line in out: + print(f" {line}") + return 0 if healthy else 1 diff --git a/.eados-core/tools/tests/test_doctor.py b/.eados-core/tools/tests/test_doctor.py index 17c88fb..e147602 100644 --- a/.eados-core/tools/tests/test_doctor.py +++ b/.eados-core/tools/tests/test_doctor.py @@ -93,6 +93,24 @@ def main(): check("audit -> refactor is shown", has(lines, "-> refactor"), failures) check("no roadmap stays healthy", healthy, failures) + # --- M16 16.3 (#254): the advisory routing readout (pure; the gh shell is not touched) --- + import route_advice + spec = route_advice.load_routing() + issues = [ + {"number": 1, "title": "ratify the ADR", "labels": ["adr", "severity:high"]}, + {"number": 2, "title": "fix a doc typo", "labels": ["documentation", "severity:low"]}, + ] + rl = doctor.routing_lines(issues, spec) + check("one advisory line per issue (+ the boundary line)", len(rl) == 3, failures) + check("an ADR-labelled issue routes to frontier-reasoning", + has(rl, "#1 -> frontier-reasoning/"), failures) + check("a small doc fix stays on the floor", + has(rl, f"#2 -> {spec['defaults']['tier']}/{spec['defaults']['effort']}"), failures) + check("the advice names today's catalog model", has(rl, "("), failures) + check("the advisory boundary is stated", has(rl, "advisory only"), failures) + check("no issues -> no lines (nothing to advise)", + doctor.routing_lines([], spec) == [], failures) + if failures: print("test-doctor: FAIL\n") for f in failures: diff --git a/.issues/README.md b/.issues/README.md new file mode 100644 index 0000000..2fde1e2 --- /dev/null +++ b/.issues/README.md @@ -0,0 +1,44 @@ +# `.issues/` — milestone plans & issue drafts + +The factory's planning surface: draft tickets (`NNNN-*.md`) and one **canonical milestone plan** +per milestone (`M--milestone.md` — precedent: M15 #233, M16 #256). GitHub is the tracker +of record; a plan doc is the reviewed intent the tracker items are cut from, and it records the +tracker mapping once the issues are filed. Allow-listed as human-reviewed prose (`gate-coverage`). + +## Milestone plan format + +A plan doc carries: **Status/Owner header · Theme · Ratified decisions (dated) · the +wave/sequence table · Out of scope (invariants)**. Since M16 (ADR-0017) the sequence table has a +**`Routing` column** next to `Effort`: + +| Item | Issue | Title | Effort | Routing | +|---|---|---|---|---| +| 16.1 | #252 | … | M | frontier-reasoning / high (decision-heavy, sets-pattern) | + +`Routing` speaks the [`os/routing`](../.eados-core/orchestrator/os/routing/_schema.md) vocabulary: +a capability **tier** (`fast` / `standard` / `frontier-reasoning`) + an **effort** +(`low`–`max`), with the asserted flags in parentheses. Model names stay out of the table — the +dated catalog in `routing.yaml` owns them. + +## Filing an issue + +Every filed issue carries the tracker metadata (labels incl. a `severity:*`, the milestone, the +assignee — the PR-metadata parity rule) **and a `Routing:` line in its body**: + +```markdown +**Routing** (ADR-0017, advisory — reviewed YYYY-MM-DD): `tier` / `effort` — flags: `…`. Today's claude-code model: . +``` + +The flags record what labels cannot say: `sets-pattern` (first of its class — followers inherit a +cheaper route) and `decision-heavy` (the decision is the deliverable). The line is the +**maintainer-reviewed call**; the policy's label-only advice is the starting point, checkable any +time with: + +```bash +python .eados-core/tools/route_advice.py --issue N # one issue, from its labels +python .eados-core/tools/route_advice.py --milestone "M16 — …" # one line per open issue +python .eados-core/tools/route_advice.py --labels "adr,severity:high" --flags decision-heavy +``` + +Advisory only, always: the human keeps final model authority (ADR-0017); an agent never switches +the session model. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c4075a..fea92d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,18 @@ in the same PR. Releases follow Semantic Versioning; the latest is **v2.8.0**. ### Added +- **Routing advice at the OS's read-points (#254, M16 16.3).** The advisory model/effort route + (ADR-0017) now surfaces where work is actually picked up: **Step-0 triage** states the + recommended tier + effort once a request routes to a `focused-change` or the `five-step-loop` + (a new `routing_advice:` block in [`triage.yaml`](.eados-core/orchestrator/triage.yaml)); + **`/eados status`** gains an opt-in `--routing-milestone "TITLE"` readout — one advisory line + per open issue (`#N -> tier/effort (model)`), degrading to SKIP offline and **never changing + the exit code**; and the **planning protocol** is now written down in + [`.issues/README.md`](.issues/README.md) — plan-doc `Routing` column + a `Routing:` line in + every filed issue body, with the `sets-pattern` / `decision-heavy` flags recording what labels + cannot say. The 17 open M15 issues (#234–#250) are backfilled with their reviewed 2026-07-09 + routing lines. Covered by new `routing_lines` checks in `test_doctor.py`. + - **`tools/route_advice.py` — the routing policy as a tool (#253, M16 16.2).** A pure evaluator over [`os/routing/routing.yaml`](.eados-core/orchestrator/os/routing/routing.yaml): tracker labels + asserted flags in → tier / effort / current model (per catalog host) + the matched