Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .eados-core/orchestrator/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
9 changes: 9 additions & 0 deletions .eados-core/orchestrator/commands/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions .eados-core/orchestrator/triage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 40 additions & 0 deletions .eados-core/tools/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand All @@ -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: <root>/ROADMAP.md)")
ap.add_argument("--links", help="traceability links file (default: <root>/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:
Expand All @@ -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


Expand Down
18 changes: 18 additions & 0 deletions .eados-core/tools/tests/test_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
44 changes: 44 additions & 0 deletions .issues/README.md
Original file line number Diff line number Diff line change
@@ -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<N>-<slug>-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: <name>.
```

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.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down