# Framework Agent Features — Technical Design Companion to `functional.md`. This file is the implementation contract: every line here is what the implementation tasks build. Deviations require a `[unreleased]` CHANGELOG entry and a design doc update. ## 1. File Map (what v1 adds) ``` ~/.automaton/ ├── models.json # NEW — model manifest (see §2) ├── scripts/ │ ├── detect_models.py # NEW — probes opencode.json + localhost endpoints │ └── status.py # EXTENDED — new flags (see §4) ├── prompts/ │ ├── rule-proposer.md # NEW — Rule Proposer session prompt │ ├── rule-reviewer.md # NEW — Rule Reviewer session prompt │ └── onboarding.md # EXTENDED — Step 2e (backlog check) ├── automaton/ │ └── dashboard/ │ ├── html/dashboard.js # EXTENDED — remove AGENT_TYPE_META, two-section render │ └── ui/app.py # EXTENDED — /api/phase-roles endpoint ├── .automaton/ # (framework self-hosting: this is ~/.automaton/.automaton/) │ ├── .state.rule-scan # NEW — Rule Proposer state (see §3) │ ├── .state.rule-review # NEW — Rule Reviewer state (see §3) │ ├── RULE_PROPOSALS.md # NEW — Rule Proposer output (append-per-run) │ ├── RULE_REVIEW.md # NEW — Rule Reviewer output (append-per-run) │ └── automaton-rule-scan.sh # NEW — generated by --install-rule-scan-schedule │ automaton-rule-review.sh # NEW — generated by --install-rule-review-schedule └── tests/ ├── test_model_divergence.py # NEW — manifest, conflict matrix, --transition --model ├── test_rule_agents.py # NEW — scan flows, state files, output schemas └── test_dashboard_phase_roles.py # NEW — /api/phase-roles, two-section render ``` Per-project paths mirror the loop convention: `{project}/.automaton/.state.rule-scan`, `{project}/.automaton/RULE_PROPOSALS.md`, etc. For framework self-hosting, the project is `~/.automaton/` itself. ## 2. `models.json` Schema ```json { "schema_version": 1, "default": "glm-4.6", "advised": true, "models": [ { "name": "glm-4.6", "provider": "opencode", "context_window": 131072, "location": "remote" }, { "name": "qwen3-coder", "provider": "opencode", "context_window": 131072, "location": "remote" }, { "name": "llama-3.3-70b", "provider": "localhost", "context_window": 32768, "location": "http://localhost:8080" } ] } ``` - `default`: model name used when no role-specific binding exists. Must be present in `models[]`. - `advised`: bool. If `true`, single-LLM mode prints a one-time advisory recommending a second model, then goes silent. - `models[]`: roster. `name` is the unique key. `provider` is informational. `context_window` is informational (framework never inspects capability, D8). `location` is `"remote"` or a localhost URL (for `detect_models.py` probing). - **Missing file** → single-LLM mode (backward compatible). All model-divergence commands are no-ops. - **0-1 models** → single-LLM mode. Advisory once if `advised: true`. - **2+ models** → multi-LLM mode. Hard-block on conflict matrix. ### 2.1 `detect_models.py` ``` python3 scripts/detect_models.py [--json] ``` 1. Parse `opencode.json` (or `opencode.jsonc`) for provider+model entries. 2. Probe localhost endpoints: `http://localhost:8080/v1/models`, `http://localhost:11434/api/tags` (Ollama), `http://localhost:1234/v1/models` (LM Studio), `http://localhost:8000/v1/models` (vLLM). 3. Merge results, emit a candidate `models.json` to stdout (or write if `--json` not set). 4. Used by `install.sh` / `update.sh` / `upgrade.sh` to bootstrap or refresh `models.json`. ## 3. State Schemas ### 3.1 `.state.rule-scan` ```json { "schema_version": 1, "last_scan_at": "2026-06-25T10:00:00Z", "last_scanned_task": "fix-context-sizing", "proposals_count": 3, "scanned_tasks_count": 12 } ``` - `last_scanned_task`: the most recent task name scanned. Next scan starts after this task (alphabetical or mtime order). - `proposals_count`: cumulative count of proposals written to `RULE_PROPOSALS.md`. - Stored at `{project}/.automaton/.state.rule-scan`. Missing file → first run scans all completed tasks. ### 3.2 `.state.rule-review` ```json { "schema_version": 1, "last_review_at": "2026-06-25T10:00:00Z", "contradictions_found": 2, "stale_rules": 5, "merges_suggested": 1 } ``` - Stored at `{project}/.automaton/.state.rule-review`. Missing file → first run reviews all rules. ### 3.3 `.state.models` (per-task) ```json { "schema_version": 1, "implement": "glm-4.6", "code_review": "qwen3-coder", "bug_find": "qwen3-coder", "adversarial_bug_find": "llama-3.3-70b", "referee": "llama-3.3-70b", "doc_review": null } ``` - Stored at `{task}/.state.models`. One file per task. - Written by `--transition --model ` when entering a phase. - Read by `--claim` (conflict-matrix check) and `--audit` (violation detection). - Roles not yet filled are `null` or absent. ## 4. `status.py` New Flags All model-divergence and rule-agent commands route through `status.py` — no second enforcement surface. ``` # Model-divergence status.py --transition --task [--model ] Records model in .state.models; checks conflict matrix status.py --claim --task --agent [--model ] Refuses if model conflicts with filled roles (multi-LLM mode) status.py --audit EXTENDED — +model_divergence category status.py --can-edit [...] UNCHANGED # Rule agents status.py --rule-scan [--project

] [--dry-run] Scan completed tasks, propose rules to RULE_PROPOSALS.md status.py --install-rule-scan-schedule [--interval S] Install OS-native unit for --rule-scan (default daily) status.py --rule-review [--project

] [--dry-run] Consolidate .rules.md, write RULE_REVIEW.md status.py --install-rule-review-schedule [--interval S] Install OS-native unit for --rule-review (default monthly) ``` ### 4.1 `--transition --model` flow 1. Load `models.json`. If missing or single-LLM mode → record model (advisory), no conflict check. 2. If multi-LLM mode: load `.state.models` for the task. Check the role being entered against the conflict matrix (§5). 3. If `--model` not provided: auto-assign next-available non-conflicting model from `models[]`. Refuse if none available. 4. If `--model` provided: verify it's in `models[]`. Check conflict matrix. Refuse on violation. 5. Write `role: model` to `.state.models`. Transition the phase. ### 4.2 `--claim --model` flow 1. Load `models.json`. If single-LLM mode → existing claim logic, no model check. 2. If multi-LLM mode: load `.state.models`. Determine the role for the phase being claimed. Check conflict matrix against already-filled roles. 3. Refuse if the claiming agent's model conflicts. Error message names the conflicting role and model. ### 4.3 `--audit` extension New audit category `model_divergence`: - For each task with `.state.models`: check all filled roles against the conflict matrix. - Flag violations as `severity: high` (conflict-of-interest is a correctness issue, not a style issue). - Output format mirrors existing audit categories. ## 5. Conflict Matrix (implementation) ```python CONFLICT_MATRIX = { "code_review": {"implement"}, "bug_find": {"implement"}, "adversarial_bug_find": {"implement", "bug_find"}, "referee": {"implement", "bug_find", "adversarial_bug_find"}, "loop-verify": {"loop-implement"}, } ``` - Key = role being entered. Value = set of roles that must have a different model. - `doc_review`, `code_review`, `bug_find` are NOT in conflict with each other (only `bug_find` ↔ `adversarial_bug_find` conflicts). - Check function: `def _check_conflict(state_models: dict, role: str, model: str, matrix: dict) -> Optional[str]` — returns the conflicting role name or `None`. ## 6. Rule Proposer Flow (`--rule-scan`) ``` 1. Load .state.rule-scan (or init if missing). 2. Find completed tasks since last_scanned_task: - Scan tasks/complete/ and tasks with .state phase=complete - Filter by mtime > last_scan_at (or all if first run) - Sort by mtime ascending 3. For each task: a. Read BUG_REPORT.md, ADVERSARIAL_BUG_REPORT.md, VERDICT.md (skip if none exist) b. Read current .rules.md (for dedup context — capped at 4k tokens) c. Build proposer prompt (see §7) d. Invoke harness via loop-runner._invoke_harness(prompt, model=rule_proposer_model) e. Parse LLM output for proposed rules (expect RULE_PROPOSALS.md format) f. Append proposals to RULE_PROPOSALS.md g. Update .state.rule-scan (last_scanned_task, proposals_count) 4. Write final .state.rule-scan with last_scan_at = now. ``` - `--dry-run`: list tasks that would be scanned, do not invoke harness. - `--project`: scope to a project (default: framework dir). - Errors during a single task scan do not abort the run; the scan continues to the next task and logs the error. ## 7. Rule Proposer Prompt Shape (`rule-proposer.md`) ``` # Rule Proposer — {date} You are scanning completed tasks for failure patterns that should become rules. ## Current rules (read-only, for dedup) {current_rules} # .rules.md content, capped at 4k tokens ## Task failure artifacts {bug_report} # BUG_REPORT.md content, capped at 2k tokens {adversarial_report} # ADVERSARIAL_BUG_REPORT.md, capped at 2k tokens {verdict} # VERDICT.md, capped at 2k tokens ## What to do For each distinct failure pattern you observe: 1. Check if a rule already exists in .rules.md that covers it. If so, skip. 2. If no existing rule covers it, propose a new rule with: - A concrete example from the task artifacts - The proposed rule text as it would appear in .rules.md ## Output (strict markdown, no JSON) ## Proposed Rule: {title} **Source**: tasks/{task-name}/VERDICT.md **Pattern**: {one-line description} **Example**: {concrete snippet} **Proposed rule text**: {rule text} --- ``` No `{model}` token in the prompt — the model is selected by the caller and passed to `_invoke_harness`. ## 8. Rule Reviewer Flow (`--rule-review`) ``` 1. Load .state.rule-review (or init if missing). 2. Read .rules.md (full file). 3. Read recent RULE_PROPOSALS.md entries (since last_review_at). 4. Read recent completed-task summaries (last 30 days) for staleness context. 5. Build reviewer prompt (see §9). 6. Invoke harness via loop-runner._invoke_harness(prompt, model=rule_reviewer_model). 7. Parse LLM output for review sections (contradictions, stale, missing examples, merges). 8. Append to RULE_REVIEW.md. 9. Update .state.rule-review. ``` - `--dry-run`: report what would be reviewed, do not invoke harness. ## 9. Rule Reviewer Prompt Shape (`rule-reviewer.md`) ``` # Rule Reviewer — {date} You are consolidating .rules.md for contradictions, staleness, and missing examples. ## Current rules (full) {rules_content} # .rules.md, full file ## Recent proposals (since last review) {recent_proposals} # RULE_PROPOSALS.md entries since last_review_at ## Recent completed tasks (last 30 days, for staleness context) {task_summaries} # one-line per task: name + phase + completion date ## What to check 1. Contradictions: rules that conflict with each other. 2. Stale rules: no observed instance in last 30 days. 3. Rules missing examples: any rule without a concrete example. 4. Merge candidates: overlapping rules that could be consolidated. ## Output (strict markdown, no JSON) ## Contradictions Found - ... ## Stale Rules - ... ## Rules Missing Examples - ... ## Merge Candidates - ... ``` ## 10. Harness Invocation (direct, not loop) Rule agents reuse `loop-runner._invoke_harness` directly — they are NOT loops. The function signature (from `loop-runner.py:366-404`): ```python def _invoke_harness(harness_command: str, prompt_content: str, cwd: str, env: dict = None) -> str: ``` `status.py --rule-scan` calls this as: ```python from loop_runner import _invoke_harness output = _invoke_harness( harness_command=rule_harness_command, # from schedule config or default prompt_content=resolved_prompt, # rule-proposer.md with tokens substituted cwd=str(project_dir), env={"AUTOMATON_RULE_ROLE": "proposer"} ) ``` `{model}` substitution: if the harness command contains `{model}`, it's replaced with the rule agent's configured model. Until model-divergence ships, this is the default model. ### 10.1 Schedule Config for Rule Agents Rule agents do not use `loop.json`. Their config is embedded in the schedule stub: ```bash #!/usr/bin/env bash cd "" python3 "/scripts/status.py" --rule-scan --model ``` The `--model` flag is optional and ignored in single-LLM mode. In multi-LLM mode it sets the rule agent's model (subject to conflict-of-interest checks once enforced). ## 11. Scheduler Unit Generation Mirrors `cmd_install_cleanup_schedule` (`status.py:2188-2260`) exactly: ### 11.1 `--install-rule-scan-schedule` ```python def cmd_install_rule_scan_schedule(args) -> int: interval = args.interval if args.interval else 86400 # daily # 1. Write stub: automaton-rule-scan.sh # 2. Platform dispatch: # Darwin → ~/Library/LaunchAgents/com.automaton.rule-scan.plist # Linux → crontab block via _install_cron_block_generic # Windows → schtasks /create /tn "AutomatonRuleScan" ``` ### 11.2 `--install-rule-review-schedule` ```python def cmd_install_rule_review_schedule(args) -> int: interval = args.interval if args.interval else 2592000 # monthly # Same pattern, labels: com.automaton.rule-review / AutomatonRuleReview ``` ### 11.3 `_list_scheduled_jobs` extension `_list_scheduled_jobs` (`status.py:2282`) gains recognition for new labels: ```python def _launchd_label_kind(label: str) -> tuple[str, str]: if label.startswith("com.automaton.loop."): return ("loop", label[len("com.automaton.loop."):]) if label in ("com.automaton.cleanup",): return ("cleanup", "") if label in ("com.automaton.rule-scan",): return ("rule-scan", "") if label in ("com.automaton.rule-review",): return ("rule-review", "") ... ``` This makes rule-scan and rule-review jobs appear in `/api/scheduled` with their real `kind`, which the Agent tab renders directly. ## 12. Agent Tab Data Flow ### 12.1 New endpoint: `/api/phase-roles` `app.py` gains a handler: ```python elif self.path == "/api/phase-roles": self._serve_phase_roles() ``` ```python def _serve_phase_roles(self): # 1. Parse .agent.md Agent Configuration for role definitions # 2. Load all tasks via status.py module # 3. For each role, count tasks in that role's phases # 4. Return JSON: { "roles": [ {"id": "researcher", "label": "Researcher", "icon": "🔬", "phases": ["research", "decomposition", "design", "test_design"], "active_tasks": 2, "status": "active"}, ... ], "available": True } ``` Role icons (self-documenting, per `.rules.md` Self-Documenting UI Names): | Role | Icon | |---|---| | researcher | 🔬 | | implementer | ⚙️ | | code-reviewer | 👁️ | | bug-hunter | 🐛 | | referee | ⚖️ | | orchestrator | 🎯 | ### 12.2 `dashboard.js` changes **Remove**: `AGENT_TYPE_META` (lines 330-334), `AGENT_TYPES` (337), `AGENT_TYPE_META_FALLBACK` (338), `_resolveAgentType` (351-354). **Replace `renderAgentTab`** with a two-section render: ```javascript async function renderAgentTab() { const panel = document.getElementById('agent-panel'); panel.innerHTML = '

Loading…
'; const [rolesRes, schedRes] = await Promise.all([ fetch('/api/phase-roles').then(r => r.json()).catch(() => ({roles: [], available: false})), fetchSchedule(), ]); // Section 1: Phase Roles const rolesHtml = rolesRes.available ? renderPhaseRoles(rolesRes.roles) : '
Phase roles require .agent.md Agent Configuration.
'; // Section 2: Scheduled Jobs const jobsHtml = renderScheduledJobs(schedRes.jobs || []); panel.innerHTML = `

Phase Roles

${rolesHtml}

Scheduled Jobs

${jobsHtml}
`; } ``` **`renderScheduledJobs`** uses `job.kind` directly (no fake type resolution): ```javascript const JOB_META = { cleanup: { icon: '🧹', label: 'Cleanup Archiver' }, loop: { icon: '🔄', label: (j) => `Loop: ${j.name}` }, 'rule-scan': { icon: '📝', label: 'Rule Proposer' }, 'rule-review':{ icon: '📋', label: 'Rule Reviewer' }, }; ``` ## 13. Loop Integration (model-divergence) ### 13.1 `loop.json` per-role model ```json "roles": { "implement": {"prompt": "loop-implement.md", "model": "glm-4.6"}, "verify": {"prompt": "loop-verifier.md", "model": "qwen3-coder"}, "orchestrate":{"prompt": "loop-orchestrate.md", "model": "glm-4.6"} } ``` - `model` is optional. If absent, uses `models.json` `default`. - `loop-verify` model is checked against `loop-implement` model in `--check-gate` (multi-LLM mode). ### 13.2 `{model}` substitution in `_invoke_harness` `loop-runner.py:366-404` `_invoke_harness` gains `{model}` token substitution: ```python def _invoke_harness(harness_command, prompt_content, cwd, env=None, model=None): if model and "{model}" in harness_command: harness_command = harness_command.replace("{model}", model) ... ``` The caller passes `model` from the role config. If the harness command has no `{model}` token, the model is informational only (the harness picks its own). ### 13.3 `--check-gate` model-divergence check In multi-LLM mode, `--check-gate` adds: - Load `loop.json` roles. Compare `verify.model` vs `implement.model`. - If same model and multi-LLM mode → halt as `model_conflict` (new halt reason, or reuse `human_intervention` with a descriptive message). ## 14. Test Coverage ### 14.1 `test_model_divergence.py` - `test_models_json_missing_single_llm_mode` — no file → advisory, no blocks. - `test_single_model_advisory_once` — 1 model, `advised: true` → advisory printed once, then silent. - `test_multi_llm_conflict_matrix` — 2+ models, `--transition --model` records, `--claim` refuses conflict. - `test_auto_assign_next_available` — no `--model` flag → auto-assigns non-conflicting model. - `test_auto_assign_exhausted` — all models conflict → refuse. - `test_audit_model_divergence` — `--audit` flags conflict-matrix violations. - `test_loop_verify_neq_implement` — `--check-gate` halts on same model in multi-LLM mode. ### 14.2 `test_rule_agents.py` - `test_rule_scan_finds_completed_tasks` — seeded completed task with VERDICT.md → proposal written. - `test_rule_scan_state_tracking` — `.state.rule-scan` updated with last_scanned_task + count. - `test_rule_scan_dedup` — existing rule in `.rules.md` → not re-proposed. - `test_rule_scan_dry_run` — no harness invocation, lists candidates. - `test_rule_review_finds_contradictions` — seeded `.rules.md` with contradiction → review written. - `test_rule_review_state_tracking` — `.state.rule-review` updated. - `test_install_rule_scan_schedule` — stub + plist created with correct labels. - `test_install_rule_review_schedule` — stub + plist created with correct labels. ### 14.3 `test_dashboard_phase_roles.py` - `test_api_phase_roles` — `/api/phase-roles` returns 6 roles with correct phases. - `test_phase_roles_active_count` — tasks in phases → correct active_tasks count. - `test_scheduled_jobs_new_kinds` — rule-scan and rule-review jobs appear with correct `kind`. - `test_agent_type_meta_removed` — `AGENT_TYPE_META` no longer in dashboard.js (grep test). ## 15. Rollout (3 sequential tasks for model-divergence) The model-divergence-enforcement parent task decomposes into 3 subtasks: 1. **manifest+detection**: `models.json` schema, `detect_models.py`, `install.sh`/`update.sh`/`upgrade.sh` integration, `config.md` section, onboarding Step 2d. 2. **interactive enforcement+audit**: `.state.models`, `--transition --model`, `--claim --model` conflict check, `--audit` model_divergence category, dashboard badges. 3. **loop enforcement+dashboard**: `loop.json` per-role model, `{model}` substitution, `--check-gate` model check, loop dashboard badges. Rule agents (FW-2, FW-3) and Agent tab (FW-1) are backlog items, picked up after model-divergence ships (for FW-2/FW-3) or independently (for FW-1). ## 16. Locked Decision Index All decisions referenced by `(Fn)` are in `README.md` § "Locked decisions". Implementation must conform. Deviations require a design doc update + `[unreleased]` CHANGELOG entry.