From 960036eb87d36ec656120df5d691ccadf9821718 Mon Sep 17 00:00:00 2001 From: agentfactory Date: Sun, 28 Jun 2026 17:05:34 +0000 Subject: [PATCH 1/9] analysis: root cause analysis for issue 73 (default dispatch workflow) Co-Authored-By: Claude Opus 4.8 Co-authored-by: agentfactory-cli <293373236+agentfactory-cli@users.noreply.github.com> --- .analysis/73/rootcause_analysis.md | 304 ++++++++++++++++++++++++++++ .analysis/73/rootcause_concern_1.md | 58 ++++++ .analysis/73/rootcause_concern_2.md | 79 ++++++++ .analysis/73/rootcause_concern_3.md | 140 +++++++++++++ .analysis/73/rootcause_concern_4.md | 155 ++++++++++++++ .analysis/73/rootcause_concern_5.md | 134 ++++++++++++ .analysis/73/rootcause_concern_6.md | 125 ++++++++++++ .analysis/73/rootcause_concern_7.md | 147 ++++++++++++++ .analysis/73/rootcause_concern_8.md | 150 ++++++++++++++ .analysis/73/rootcause_concern_9.md | 130 ++++++++++++ 10 files changed, 1422 insertions(+) create mode 100644 .analysis/73/rootcause_analysis.md create mode 100644 .analysis/73/rootcause_concern_1.md create mode 100644 .analysis/73/rootcause_concern_2.md create mode 100644 .analysis/73/rootcause_concern_3.md create mode 100644 .analysis/73/rootcause_concern_4.md create mode 100644 .analysis/73/rootcause_concern_5.md create mode 100644 .analysis/73/rootcause_concern_6.md create mode 100644 .analysis/73/rootcause_concern_7.md create mode 100644 .analysis/73/rootcause_concern_8.md create mode 100644 .analysis/73/rootcause_concern_9.md diff --git a/.analysis/73/rootcause_analysis.md b/.analysis/73/rootcause_analysis.md new file mode 100644 index 0000000..814faaa --- /dev/null +++ b/.analysis/73/rootcause_analysis.md @@ -0,0 +1,304 @@ +# Root Cause Analysis: Default dispatch workflow for fresh agentfactory installs (Issue #73) + +**Date**: 2026-06-28 +**Status**: Synthesized — solution documented +**Problem File**: GitHub issue [stempeck/agentfactory#73](https://github.com/stempeck/agentfactory/issues/73); design contract `.designs/73/problem-summary-73.md` + +## Problem Statement + +A freshly bootstrapped agentfactory does **not** support label-driven dispatch out of the +box. `af install --init` writes a placeholder `dispatch.json` with empty `repos` and empty +`mappings` (`internal/cmd/install.go:145`), so tagging a GitHub issue dispatches nothing and +the repo name is never injected. A new user must visit the manager and hand-configure +dispatch before any label does anything. + +Issue #73 asks for a **baked-in default `dispatch.json`**, populated with the **actual +`org/repository`** at install time plus sensible default label→agent mappings and a workflow, +so a new user can dispatch work purely by labeling GitHub issues — without ever visiting the +manager. The acceptance criterion is broader: a clean setup must let `af sling --agent +"task"` (and the label path) run **autonomously to a pushed PR**, with no `doctor --fix` and +no human intervention, each agent following the rigid step-by-step formula that IS its +identity. The issue explicitly asks for **systemic improvements while addressing the +scenario**. + +## Concerns from Problem File + +All concerns were independently investigated by a dedicated sub-agent (Phase 2). Verdicts are +evidence-based; full per-concern investigations are in the linked files. + +| # | Concern | Verdict | Evidence Link | +|---|---------|---------|---------------| +| 1 | Install-time default `dispatch.json` ships empty `repos` + empty `mappings` (`install.go:145`), so a fresh factory dispatches nothing by label. | **VALIDATED** | [rootcause_concern_1.md](rootcause_concern_1.md) | +| 2 | The actual `org/repository` is never discovered/injected at setup (static literal; no env interrogation). | **VALIDATED** | [rootcause_concern_2.md](rootcause_concern_2.md) | +| 3 | No default label→agent `mappings`; the proposed mappings are struct-valid + semantically correct but the agent names must be **registered in agents.json**. | **VALIDATED** | [rootcause_concern_3.md](rootcause_concern_3.md) | +| 4 | A fresh `af install --init` registers only `manager`+`supervisor`; the four mapped specialist agents are **not registered/provisioned**, so dispatch aborts on validation. | **VALIDATED** | [rootcause_concern_4.md](rootcause_concern_4.md) | +| 5 | Dispatch input-compatibility: do mapped formulas accept a single issue/PR input? | **INVALIDATED** | [rootcause_concern_5.md](rootcause_concern_5.md) | +| 6 | Workflow block validity: does the proposed `feature-workflow` pass `dispatch.go` validation? | **VALIDATED** | [rootcause_concern_6.md](rootcause_concern_6.md) | +| 7 | Dispatcher auto-activation: empty default friendly-skips; a populated default auto-starts on **bare** `af up`, but `af up ` (the documented `af up manager`) is gated out. | **VALIDATED** | [rootcause_concern_7.md](rootcause_concern_7.md) | +| 8 | Default semantics of `trigger_label:"agentic"` AND-matching + `remove_trigger_after_dispatch:true` are safe; caveats: issue JSON has a syntax error; dual-label requirement is a UX trap. | **VALIDATED** | [rootcause_concern_8.md](rootcause_concern_8.md) | +| 9 | Broader acceptance — systemic gaps to a **pushed PR** (origin remote + `gh`/push auth) live at the container layer, not in `af install --init`; `doctor` is not a real command. | **VALIDATED** | [rootcause_concern_9.md](rootcause_concern_9.md) | + +## Synthesized Root Cause(s) + +Based on investigation of 9 concerns: +- **8 concerns VALIDATED** as contributing factors (1, 2, 3, 4, 6, 7, 8, 9) +- **1 concern INVALIDATED** (5 — input-compatibility is a non-issue: all four mapped formulas + accept a single issue/PR URL input, so none needs an extra `--var`; the label path can run + them as-is). + +The eight validated concerns are **not eight independent bugs** — they form a single causal +chain rooted in one architectural decision, plus two registration/activation gaps that any +"populated default" must close to be coherent. + +### Primary Root Cause — The fresh-install config set is a static, structurally-incomplete literal + +`af install --init` emits `dispatch.json` from a **hardcoded compile-time string** in the +`starterConfigs` map (`internal/cmd/install.go:145`): +```go +"dispatch.json": `{"repos":[],"trigger_label":"agentic","notify_on_complete":"manager","mappings":[],"interval_seconds":300,"retry_after_seconds":1800}`, +``` +A constant string has no access to runtime state, which forces two failures at once and a +cascade behind them: + +1. **It cannot contain the repo** → `repos` is empty even though the init cwd is the cloned + target repo with `origin` set (`quickstart.sh:428`→`:442`, `getWd()`/`os.Getwd()`), and the + factory already ships a read-only git/gh detection idiom (`runGitDetect`/`detectDefaultBranch`, + `internal/cmd/detect_default_branch.go:34-73`) that was never generalized to repo-slug + detection (Concern 2; `grep nameWithOwner` → zero hits). +2. **It is empty (`repos:[]`, `mappings:[]`)** → `validateDispatchConfig` rejects it with + `ErrMissingField` on both the empty-repos and empty-mappings checks + (`internal/config/dispatch.go:142-150`) (Concern 1). +3. **Cascade → the dispatcher never starts.** `startup.json` correctly ships `start_dispatch:true` + (`install.go:147`), so a bare `af up` calls `startDispatch` (`up.go:330-331`), but + `startDispatch` folds `ErrMissingField` into a friendly "not configured" skip and returns + without launching the polling loop (`dispatch.go:1327-1339`), proven by + `TestStartDispatch_EmptyDefaultConfigFriendlySkip` (Concern 7). Net effect: labeling a GitHub + issue causes **zero dispatch** until an operator hand-edits the file (Concern 1). + +This single static literal is the substrate the issue is really about: the default is shipped +*intentionally incomplete* because a static string cannot be personalized, and everything +downstream skips/aborts on that incompleteness. + +### Contributing Factor A — Mapped specialist agents are not registered on the fresh-install path + +Even if `dispatch.json` were populated with the proposed mappings, dispatch would still **fail**, +not no-op. `af install --init` registers only `manager`+`supervisor` in `agents.json` +(`install.go:143`); the four mapped agents (`rapid-soldesign-plan`, `rapid-implement`, +`ultra-review`, `rapid-increment`) are never registered — the only registrar, +`af formula agent-gen` (via `agent-gen-all.sh`), runs only under `af install --agents`, which the +fresh path never invokes (Concern 4). The cross-file validator `ValidateDispatchConfig` rejects +any mapping referencing an unknown agent and **aborts the entire dispatch cycle** +(`internal/config/dispatch.go:101-102`, `internal/cmd/dispatch.go:146-148`), and even past that, +`af sling --agent` fails at `internal/cmd/sling.go:261`. The mappings are otherwise correct — +struct-valid, right issue/PR source, agent-name == formula-name by convention (Concern 3), and +the proposed `feature-workflow` passes every workflow rule **once the agents exist** (Concern 6). +So the dispatch.json default and the agent roster must be made consistent **by construction**: +shipping the mappings *requires* seeding the four agents into the default `agents.json`. + +### Contributing Factor B — Dispatcher auto-start is gated to the bare `af up` path + +Auto-start of the dispatcher fires **only** on the blanket `af up` (no positional args); the +block is inside `if blanket {` (`internal/cmd/up.go:92,306,330`). The documented setup flow uses +`af up manager` (positional), which is gated out — so even with a valid populated config and +registered agents, a user who follows the documented step gets **no dispatcher** and must still +visit the manager. To honor "tag issues without visiting the manager" via the documented flow, +auto-start must fire on that path too (the launch is idempotent — already-running is a benign +no-op, `dispatch.go:1322-1325`) (Concern 7). + +### Validated supporting facts (defaults are safe, with two caveats) + +- The default **semantics are correct and safe** (Concern 8): `trigger_label:"agentic"` gates the + GitHub query and mapping labels AND-match (so a user applies `agentic` + a mapping label); + `remove_trigger_after_dispatch:true` removes only the trigger as a safe one-shot; all field + names map 1:1 to the `DispatchConfig` json tags. **Two caveats**: (a) the issue's proposed JSON + literal is malformed (`"labels": ["rapid-plan` is missing its closing quote/bracket) and must + be corrected; (b) tagging only a mapping label *without* `agentic` does nothing — a non-obvious + UX trap to document. + +### Out-of-scope-but-named systemic gaps (per the "systemic improvements" mandate — Concern 9) + +Fixing `dispatch.json` is **necessary but not sufficient** for the broadest reading of acceptance. +The formula tail steps reach a PR via `git push -u origin` + `gh pr create` +(`rapid-implement.formula.toml:803/:881`, `rapid-soldesign-plan.formula.toml:496/:527`), which +hard-depend on an `origin` remote and `gh`/push auth. These are provisioned at the **container +layer** by `quickdocker.sh` (`gh auth login --with-token` + `gh auth setup-git`, `:557/:567`), +**not** by `af install --init`. On the *intended* quickdocker fresh-container path these are +already satisfied, so the only remaining gap is the dispatch config + agent registration (this +fix). On a bare `af install --init`-only repo (no remote, no auth), autonomy to a PR is also +blocked by those two gaps — named here, recommended as follow-ups, not solved by this change. +Note: **"doctor" is not a real command** in the codebase; the acceptance clause "no `doctor +--fix`" is a test-guarded property (`e2e_sling_test.go:67-69`), not a dependency to remove — so +that worry is moot. + +## Fishbone Diagram + +``` + [Fresh agentfactory cannot dispatch + work by GitHub label out of the box] + ▲ + ┌──────────────────────────┬──────────────────────┼───────────────────────┬─────────────────────────┐ + │ │ │ │ │ + [VALIDATED] [VALIDATED] [VALIDATED] [VALIDATED] [INVALIDATED] + C1/C2 Static, empty C3/C4 Mapped agents C7 Auto-start gated C8 Default semantics C5 Input-compatibility + dispatch.json literal not registered in to bare `af up` only safe (2 caveats) (ruled out — all 4 + (install.go:145): agents.json (up.go:92/306/330); • issue JSON malformed formulas take a single + • cannot inject repo (install.go:143; documented `af up • agentic+label dual issue/PR URL input; + (Concern 2) only `--agents` manager` skips it requirement is a UX sling.go:472-483) + • empty → ErrMissingField registers specialists) → no dispatcher on trap to document + (dispatch.go:142-150) → ValidateDispatchConfig the documented path + • startDispatch friendly- aborts cycle + skips, never launches (config/dispatch.go: + (dispatch.go:1327-1339) 101-102; sling.go:261) + │ │ │ + └─ root: config born from a static compile-time string that cannot be personalized ─┘ [ruled out — not a blocker] + + [Out-of-scope-but-named — Concern 9]: origin remote + gh/push auth provisioned only at the + container layer (quickdocker.sh:557/567), not by `af install --init`. "doctor" is not a real command. +``` + +## Solution + +**One approach (chosen).** Bootstrap a **complete, repo-personalized** dispatch configuration at +factory-init, keep it consistent with a **seeded agent roster** by construction, and **activate** +it on the documented startup path — using the in-code drift-proof constructor idiom already +established by `factory.json` (`DefaultFactoryConfigJSON()`). Repo detection degrades safely to +today's empty `repos:[]` when there is no GitHub remote, so non-GitHub repos see no regression. + +This is the single solution; rejected alternatives are noted inline (e.g., making `quickstart.sh` +run `af install --agents` to register the specialists is rejected because it only fixes the +quickstart path, not bare `af install --init`, and is heavier; threading the slug through +`quickdocker.sh → quickstart.sh → af install --init` is rejected because the binary can detect it +from a cwd it already resolves — fewer files, covers every init path). + +### Files to Modify + +| File | Change | +|------|--------| +| `internal/config/dispatch.go` | Add `DefaultDispatchConfigJSON(repoSlug string) string` — marshals a `DispatchConfig` with the default 4 mappings + `feature-workflow` + `remove_trigger_after_dispatch:true`, and `repos:[repoSlug]` (or `[]` if empty). Single on-disk-default source (mirrors `DefaultFactoryConfigJSON`). | +| `internal/cmd/detect_default_branch.go` | Add `var detectRepoSlug = func(workDir string) string` on the existing `runGitDetect` ADR-009 seam: `gh repo view --json nameWithOwner -q .nameWithOwner`, fallback to parsing `git remote get-url origin`; validate `owner/repo` shape, else `""`. | +| `internal/cmd/install.go` | In `runInstallInit` (`:139-157`): replace the static `dispatch.json` literal (`:145`) with `config.DefaultDispatchConfigJSON(detectRepoSlug(getWd()))`; replace the `agents.json` literal (`:143`) with a `config.DefaultAgentsConfigJSON()` that also registers the four dispatch-referenced specialists. Keep write-if-absent idempotency. | +| `internal/cmd/up.go` | Move the `if startupCfg.StartDispatch { startDispatch(...) }` block (`~:330`) out of the `blanket`-only gate (`:92,306`) so the dispatcher auto-starts on **any** `af up` (incl. documented `af up manager`); launch is idempotent (`dispatch.go:1322-1325`). | +| `internal/cmd/install_integration_test.go` (or new `dispatch_default_test.go`) | Drift-interlock test (see Enforcement). | +| `USING_AGENTFACTORY.md` | Document the populated default + the **dual-label** requirement (`agentic` + a mapping label) to defuse the UX trap; document the label set and `feature-workflow`. | + +### Implementation Steps + +**1. Drift-proof default-dispatch constructor** — `internal/config/dispatch.go` (verify field +names against the `DispatchConfig`/`DispatchMapping`/`Workflow` json tags at `:19-45`): +```go +// DefaultDispatchConfigJSON returns the on-disk default dispatch.json, personalized with the +// detected repo slug. An empty slug yields repos:[] so a non-GitHub repo still loads as the +// friendly "not configured" skip (no regression vs. today's behavior). +func DefaultDispatchConfigJSON(repoSlug string) string { + repos := []string{} + if repoSlug != "" { + repos = []string{repoSlug} + } + cfg := DispatchConfig{ + Repos: repos, TriggerLabel: "agentic", NotifyOnComplete: "manager", + IntervalSeconds: 300, RetryAfterSeconds: 1800, RemoveTriggerAfterDispatch: true, + Mappings: []DispatchMapping{ + {Labels: []string{"rapid-plan"}, Source: "issue", Agent: "rapid-soldesign-plan"}, + {Labels: []string{"rapid-engineer"}, Source: "issue", Agent: "rapid-implement"}, + {Labels: []string{"pr-review"}, Source: "pr", Agent: "ultra-review"}, + {Labels: []string{"pr-iterate"}, Source: "pr", Agent: "rapid-increment"}, + }, + Workflows: []Workflow{{Label: "feature-workflow", Phases: []string{"rapid-plan", "rapid-engineer"}}}, + } + b, _ := json.Marshal(cfg) // struct is statically valid; marshal cannot fail + return string(b) +} +``` +This also fixes the issue's malformed JSON (`"labels": ["rapid-plan` → proper `["rapid-plan"]`) +by construction — the literal no longer exists. + +**2. Repo-slug detection** — `internal/cmd/detect_default_branch.go`, mirroring `detectDefaultBranch` +(`:52-73`); the parse step reuses the exact pattern already in `quickdocker.sh:41-50`: +```go +var detectRepoSlug = func(workDir string) string { + if s := runGitDetect(workDir, "gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"); isRepoSlug(s) { + return s // canonical org/repo + } + s := parseRepoSlug(runGitDetect(workDir, "git", "remote", "get-url", "origin")) // strip scheme/host/.git + if isRepoSlug(s) { + return s + } + return "" // no GitHub remote → caller ships repos:[] (friendly-skip) +} +``` + +**3. Init wiring** — `internal/cmd/install.go`, in the `starterConfigs` block (`:139-157`): +```go +"agents.json": config.DefaultAgentsConfigJSON(), // manager+supervisor + the 4 dispatch specialists +"dispatch.json": config.DefaultDispatchConfigJSON(detectRepoSlug(getWd())), +``` +where `DefaultAgentsConfigJSON()` adds, alongside manager/supervisor, four `{"type":"autonomous", +"formula":""}` entries named exactly `rapid-soldesign-plan`, `rapid-implement`, +`ultra-review`, `rapid-increment` (agent-name == formula-name per the `agent-gen` convention). +Their role templates are already embedded (`internal/templates/roles/{rapid-soldesign-plan, +rapid-implement,ultra-review,rapid-increment}.md.tmpl`), so `af prime` renders each specialist +identity with no further provisioning. (Optional: add the four to `messaging.json`'s `all` group.) + +**4. Activate on the documented path** — `internal/cmd/up.go`: hoist the dispatch-start out of the +`blanket` gate so `af up manager` (and any `af up`) starts the dispatcher when `start_dispatch:true`: +```go +// was inside `if blanket { ... }`; now runs for every `af up` (idempotent — already-running no-ops) +if startupCfg.StartDispatch { + if dErr := startDispatch(cmd, root, t); dErr != nil { allOK = false } +} +``` + +**5. Drift-interlock test + docs** — see Enforcement and Files to Modify. + +### Enforcement Level + +| Step | Level | Notes | +|------|-------|-------| +| Build `dispatch.json` from `DefaultDispatchConfigJSON` (no static literal) | **Interlock** | Struct + `json.Marshal` enforce shape; a typo'd field or malformed JSON cannot ship | +| Repo-slug detection → empty-slug falls back to `repos:[]` | **Runtime guard** | Best-effort; degrades to today's friendly-skip — no regression for non-GitHub repos | +| Seed the 4 specialists into default `agents.json` | **Interlock (by construction)** | dispatch.json mappings resolve because the agents exist in the same install output | +| Drift-interlock unit test | **Interlock (Poka-yoke)** | Test asserts every mapping/phase agent in `DefaultDispatchConfigJSON` exists in `DefaultAgentsConfigJSON`, and that the populated default round-trips `LoadDispatchConfig`+`ValidateDispatchConfig`; and empty-slug still yields the friendly-skip. Makes it impossible to merge a default that references an unregistered agent or fails validation. | +| Ungate dispatcher auto-start on positional `af up` | **Interlock** | Documented `af up manager` flow now activates dispatch deterministically | +| Document dual-label requirement | **Advisory** | Defuses the UX trap (see code-level enforcement below) | + +**Code-level enforcement for the one Advisory item (the dual-label UX trap):** rather than relying +only on docs, add a runtime hint in the dispatch cycle — when an item carries a known *mapping* +label but lacks `trigger_label`, log `issue #N has label 'rapid-plan' but not 'agentic' — not +dispatched` (a Poka-yoke nudge), or optionally treat a configured mapping label as a sufficient +trigger. The interlock above already guarantees the *config* is valid; this guards the *operator's +labeling* mistake. + +### Verification Steps + +1. `make build && make test` — all pass, including the new `detectRepoSlug`, `DefaultDispatchConfigJSON`, + and drift-interlock tests; existing `TestStartDispatch_EmptyDefaultConfigFriendlySkip` and the + install-integration "dispatch.json is valid JSON" test stay green. +2. Unit (mirror `detectDefaultBranch` tests): canned `runGitDetect` outputs → `detectRepoSlug` + returns the slug / `""` correctly across `gh` and `git remote` fallback paths. +3. Unit: `DefaultDispatchConfigJSON("org/repo")` → `LoadDispatchConfig` + `ValidateDispatchConfig` + succeed against `DefaultAgentsConfigJSON()`; `DefaultDispatchConfigJSON("")` → `repos:[]` → + friendly-skip preserved. +4. Behavioral (quickdocker/quickstart fresh container): after `af up`, `af dispatch status --json` + shows the loop running with `repos:[""]`; labeling an issue `agentic`+`rapid-plan` + slings `rapid-soldesign-plan`; `agentic`+`feature-workflow` walks `rapid-plan`→`rapid-engineer`. +5. Confirm the documented `af up manager` step now starts the dispatcher (previously gated out). + +### Code Convention Issues + +- **Inconsistent default-config sourcing.** The fresh-install set mixes a drift-proof constructor + (`factory.json` via `DefaultFactoryConfigJSON()`) with brittle inline literals (`agents.json`, + `dispatch.json`, `messaging.json`, `startup.json`). This solution migrates `agents.json` and + `dispatch.json` to `config.Default*JSON()` constructors (same issue #371 Gap-6 rationale already + cited at `install.go:140-141`); `messaging.json`/`startup.json` are recommended follow-ups. +- **`LoadDispatchConfig` lacks `DisallowUnknownFields`** (Concern 8): a future typo'd key is silently + dropped. One-line robustness follow-up; not required for this fix. + +### Out-of-scope follow-ups (named per the "systemic improvements" mandate — Concern 9) + +Reaching a *pushed PR* additionally needs an `origin` GitHub remote and `gh`/push auth, which are +provisioned at the **container layer** (`quickdocker.sh:557/:567`), **not** by `af install --init`. +On the intended quickdocker path these are already satisfied — so this fix completes the +out-of-the-box label→autonomy chain there. On a bare `af install --init`-only repo, file follow-ups +to surface a clear preflight error (or provisioning) for missing remote/auth. **"doctor" is not a +command** in this codebase (the "no `doctor --fix`" acceptance clause is a test-guarded property, +`e2e_sling_test.go:67-69`), so there is no doctor dependency to remove. diff --git a/.analysis/73/rootcause_concern_1.md b/.analysis/73/rootcause_concern_1.md new file mode 100644 index 0000000..d7aa1a8 --- /dev/null +++ b/.analysis/73/rootcause_concern_1.md @@ -0,0 +1,58 @@ +# Concern #1 Investigation: Install-time default dispatch.json is empty (repos/mappings) + +**Investigated by**: Sub-agent +**Date**: 2026-06-28 + +## Verdict: VALIDATED + +## Summary +A fresh `af install --init` writes a dispatch.json with empty `repos` and empty `mappings` (`internal/cmd/install.go:145`). On every startup path, `config.LoadDispatchConfig` runs `validateDispatchConfig`, which rejects empty `repos` (line 142-143) and empty `mappings` (line 148-149) with `ErrMissingField`. The `af up` startup path (`startDispatch`, `internal/cmd/dispatch.go:1321-1340`) explicitly catches `ErrMissingField` and friendly-skips with "skipping dispatch (dispatch.json not configured)" — the dispatcher session is never launched. The strict CLI path (`runDispatchStart`, line 1258-1280) hard-errors on the same config. Either way, the dispatch loop (line 172 `for _, repo := range dispatchCfg.Repos`) never even runs because the config never loads. The net effect: on a fresh install, labeling a GitHub issue causes ZERO dispatch — there is no repo to query and no mapping to match, and the dispatcher process is not even started. The gap between "fresh install" and "label dispatches work" is total: a new user must hand-edit dispatch.json (add at least one repo and one label→agent mapping) before any label can dispatch anything. This is exactly the problem issue #73 describes. + +## 5-Whys Analysis + +### Why #1: On a fresh install, does labeling a GitHub issue dispatch any work? +No. The dispatcher never runs against any repo. The startup path that would launch the dispatcher refuses to launch it because the install-default config is invalid. Evidence: `startDispatch` calls `config.LoadDispatchConfig`, and on `ErrMissingField` it prints "skipping dispatch (dispatch.json not configured)" and returns nil without launching (`internal/cmd/dispatch.go:1327-1332`). Empirically confirmed: `TestStartDispatch_EmptyDefaultConfigFriendlySkip` passes and emits that skip message, and asserts `NewSession af-dispatch` is NOT recorded (`internal/cmd/startdispatch_test.go:70-84`). + +### Why #2: Why does the dispatcher refuse to launch / never query a repo? +Because the dispatch config fails validation at load time. `config.LoadDispatchConfig` always calls `validateDispatchConfig` before returning (`internal/config/dispatch.go:61-63`), and that validator rejects the install default. Two independent failures fire: empty `repos` (`internal/config/dispatch.go:142-143`: `if len(cfg.Repos) == 0 { return ...ErrMissingField... }`) and empty `mappings` (`internal/config/dispatch.go:148-149`: `if len(cfg.Mappings) == 0 { return ...ErrMissingField... }`). So `LoadDispatchConfig` returns an `ErrMissingField`-wrapped error and a nil config. The dispatch loop at `internal/cmd/dispatch.go:172` (`for _, repo := range dispatchCfg.Repos`) is never reached because the cfg never loads. + +### Why #3: Why is the dispatch config empty / invalid at install time? +Because that is literally what install writes. `internal/cmd/install.go:145` seeds the starter config map with: +`"dispatch.json": `{"repos":[],"trigger_label":"agentic","notify_on_complete":"manager","mappings":[],"interval_seconds":300,"retry_after_seconds":1800}`` +The `repos` array is `[]` and the `mappings` array is `[]`. The trigger label IS set ("agentic"), but with no repo to query and no label→agent mapping, that is inert. Even if validation passed, `matchItemToAgent` (`internal/cmd/dispatch.go:337-355`) iterates `mappings` and returns "" when the slice is empty, so every item would be skipped (`internal/cmd/dispatch.go:222-226`). + +### Why #4: Why does install write empty `repos` and empty `mappings` rather than real values? +Because install has no install-time mechanism to discover the user's org/repository or to bake sensible label→agent mappings. The starter-config map (`internal/cmd/install.go:139-148`) hard-codes literal JSON strings for each config file; `dispatch.json` is a static empty-skeleton literal with no substitution of a detected `origin` remote and no default mappings/workflows. It is only written if the file does not already exist (`internal/cmd/install.go:150-157` — idempotent stat-gate), so it is purely a placeholder the user is expected to fill in by hand. This absence of org/repo detection + default mappings is the ROOT CAUSE. + +### Why #5 (root cause): Why is there no install-time population of repo + mappings? +Because the design treats dispatch.json as opt-in / manually-configured rather than self-bootstrapping. The friendly-skip logic (`internal/cmd/dispatch.go:1313-1315`, 1328-1332) and its tests (`startdispatch_test.go:70-84`) intentionally model the empty default as "not configured" and degrade gracefully so `af up` never aborts. The contract is "ship a valid-JSON placeholder, skip dispatch until a human fills it in." There is no code path that detects the current GitHub repo (e.g. via `git remote get-url origin` / `gh repo view`) at install time, nor any baked-in default `mappings`/`workflows`. ROOT CAUSE: install.go ships a deliberately-empty placeholder dispatch.json with no auto-population of `repos` (from the detected origin) and no default label→agent mappings, so a fresh factory dispatches nothing by label until the operator hand-edits the file. + +## Evidence Gathered +| Finding | Source | Evidence | +|---------|--------|----------| +| Install writes empty repos + empty mappings | `internal/cmd/install.go:145` | `"dispatch.json": `{"repos":[],"trigger_label":"agentic","notify_on_complete":"manager","mappings":[],"interval_seconds":300,"retry_after_seconds":1800}`` | +| Starter config only written if absent (idempotent placeholder) | `internal/cmd/install.go:150-157` | `if _, err := os.Stat(path); os.IsNotExist(err) { ... os.WriteFile(path, []byte(content), 0644) }` | +| Load always validates | `internal/config/dispatch.go:61-63` | `if err := validateDispatchConfig(&cfg); err != nil { return nil, err }` | +| Empty repos rejected with ErrMissingField | `internal/config/dispatch.go:142-143` | `if len(cfg.Repos) == 0 { return fmt.Errorf("%w: dispatch config must have at least one repo", ErrMissingField) }` | +| Empty mappings rejected with ErrMissingField | `internal/config/dispatch.go:148-149` | `if len(cfg.Mappings) == 0 { return fmt.Errorf("%w: dispatch config must have at least one mapping", ErrMissingField) }` | +| ErrMissingField is the sentinel | `internal/config/config.go:19` | `ErrMissingField = errors.New("missing required field")` | +| af-up path friendly-skips on ErrNotFound/ErrMissingField | `internal/cmd/dispatch.go:1327-1332` | `cfg, err := config.LoadDispatchConfig(root); if errors.Is(err, config.ErrNotFound) || errors.Is(err, config.ErrMissingField) { fmt.Fprintf(..., "skipping dispatch (dispatch.json not configured)\n"); return nil }` | +| Skip happens BEFORE launchDispatchSession | `internal/cmd/dispatch.go:1338-1339` | launch only reached after the skip guard returns; empty default never reaches it | +| Strict CLI path hard-errors on same config | `internal/cmd/dispatch.go:1273-1276` | `dispatchCfg, err := config.LoadDispatchConfig(root); if err != nil { return fmt.Errorf("loading dispatch config: %w", err) }` | +| Dispatch loop keyed on Repos (never runs when empty) | `internal/cmd/dispatch.go:172` | `for _, repo := range dispatchCfg.Repos {` | +| Empty mappings ⇒ no agent match (defense in depth) | `internal/cmd/dispatch.go:337-355` | `matchItemToAgent` loops `mappings`; returns "" when empty ⇒ `stats.skipped++; continue` (lines 222-226) | +| Comment confirms empty install default is treated as "not configured" | `internal/cmd/dispatch.go:1313-1314` | "An absent dispatch.json or the empty install default skips with a friendly message" | + +## Tests Performed +| Test | Command | Result | +|------|---------|--------| +| af-up startup friendly-skips empty default, never launches session | `go test ./internal/cmd/ -run TestStartDispatch_EmptyDefaultConfigFriendlySkip -v` | PASS — emits "skipping dispatch (dispatch.json not configured)"; asserts no `NewSession af-dispatch` | +| Absent config also friendly-skips | `go test ./internal/cmd/ -run TestStartDispatch_AbsentConfigFriendlySkip -v` | PASS | +| Load rejects empty repos | `go test ./internal/config/ -run TestLoadDispatchConfig_MissingRepos -v` | PASS | +| Load rejects empty mappings | `go test ./internal/config/ -run TestLoadDispatchConfig_EmptyMappings -v` | PASS | +| Confirmed install literal | `Read internal/cmd/install.go:145` | repos=[], mappings=[] verified | + +(Note: tests required `GOTMPDIR`/`TMPDIR` redirected to a worktree-local dir and sandbox disabled because `/tmp` is mounted noexec; temp build dir was removed after the run. No source code was modified.) + +## Conclusion +VALIDATED. The concern is confirmed end-to-end with code evidence. Install (`install.go:145`) ships a dispatch.json with `repos:[]` and `mappings:[]`. That config fails `validateDispatchConfig` on BOTH the empty-repos check (`dispatch.go:142-143`) and the empty-mappings check (`dispatch.go:148-149`), each producing `ErrMissingField`. On the `af up` startup path, `startDispatch` (`dispatch.go:1327-1332`) catches `ErrMissingField` and friendly-skips without launching the dispatcher — proven by `TestStartDispatch_EmptyDefaultConfigFriendlySkip` (passes, emits the skip line, asserts no session is created). On the strict `af dispatch start` CLI path, the same load hard-errors (`dispatch.go:1273-1276`). In neither case does the dispatch loop (`dispatch.go:172`, keyed on `Repos`) ever execute, and even if it did, the empty `mappings` would make `matchItemToAgent` (`dispatch.go:337-355`) return "" for every item. Therefore, with the install default in place, labeling a GitHub issue causes ZERO dispatch. The gap between "fresh install" and "label dispatches work" is complete and requires manual editing of dispatch.json (add ≥1 repo and ≥1 label→agent mapping) — precisely the problem issue #73 raises. diff --git a/.analysis/73/rootcause_concern_2.md b/.analysis/73/rootcause_concern_2.md new file mode 100644 index 0000000..c1a959b --- /dev/null +++ b/.analysis/73/rootcause_concern_2.md @@ -0,0 +1,79 @@ +# Concern #2 Investigation: Repo name not auto-discovered/injected at setup + +**Investigated by**: Sub-agent +**Date**: 2026-06-28 + +## Verdict: VALIDATED + +## Summary +At factory-init time `af install --init` writes `dispatch.json` from a hardcoded static literal string with an empty `repos:[]` array (`internal/cmd/install.go:145`). The actual `org/repository` is never discovered or injected, even though every realistic install path runs with the repo trivially available: `quickstart.sh` does `cd "$repo_dir"` into the cloned target repo (which has `origin` configured) *before* invoking `af install --init` (`quickstart.sh:428` then `:442`), and `quickdocker.sh` receives the repo explicitly as its `` argument (`quickdocker.sh:356`). The literal cannot know the repo because it is a compile-time constant inside a `map[string]string`, and no code path in `runInstallInit` (`install.go:97-297`) ever runs `git remote get-url`, `gh repo view`, or reads its own argument for a repo. The factory already ships a clean, read-only, ADR-009-seam'd git/gh detection idiom (`detectDefaultBranch`/`runGitDetect` in `internal/cmd/detect_default_branch.go`) that is the exact precedent for the fix. Recommended mechanism: detect `org/repo` from the git remote of `getWd()` at init time using a new `detectRepoSlug` built on the existing `runGitDetect` seam; recommended injection point: `runInstallInit`, building the `dispatch.json` content programmatically instead of from the static literal. + +## 5-Whys Analysis + +### Why #1: Why does a fresh install write an empty `repos:[]` in dispatch.json? +Because `dispatch.json` is produced from a hardcoded compile-time literal in the `starterConfigs` map, with `repos` set to an empty array: +`internal/cmd/install.go:145`: +```go +"dispatch.json": `{"repos":[],"trigger_label":"agentic","notify_on_complete":"manager","mappings":[],"interval_seconds":300,"retry_after_seconds":1800}`, +``` +The map values are written verbatim (write-if-absent) at `install.go:150-157`. A constant string has no access to runtime state, so it structurally *cannot* contain the repo. + +### Why #2: Why is the literal static instead of computed? +Because `runInstallInit` treats all starter configs uniformly as static blobs (`install.go:139-157`). Only `factory.json` is built from in-code defaults (`config.DefaultFactoryConfigJSON()`, `install.go:142`); every other config — including `dispatch.json` — is an inline string. There is no per-config "compute then write" branch, so dispatch.json was never given a chance to interrogate the environment. + +### Why #3: Why does no code path during setup learn the org/repo? +`runInstallInit` (`install.go:97-297`) never shells out to git or gh for repo identity. The ONLY git interaction is `ensureGitExclude` (`install.go:270-271` -> `install.go:842-878`), which just appends paths to `.git/info/exclude` and never reads the remote. `runInstall` (`install.go:76-95`) takes no repo argument. So even though the info is reachable, the init flow simply never asks for it. + +### Why #4: Why is this a missed opportunity rather than an impossibility — is the repo actually available at init time? +Yes, the repo is available at init time on every real path: +- **quickstart path**: `configure_factory` finds the cloned target repo and `cd`s into it (`quickstart.sh:417-428`) *before* `af install --init` (`quickstart.sh:441-445`). The target was cloned via `gh repo clone "$REPO_PATH"` (`quickdocker.sh:582`), so its `origin` remote is set. `runInstallInit` resolves its working dir via `getWd()` (`install.go:98` -> `helpers.go:176-182`, a thin `os.Getwd()`), which is therefore that repo dir — a `git remote get-url origin` / `gh repo view` there would return the slug. +- **quickdocker path**: the slug is handed in *explicitly* as `` and normalized to `owner/repo` form (`quickdocker.sh:355-362`, stored in `REPO_PATH`). quickdocker even already parses its OWN remote into `owner/repo` for the AF source clone (`quickdocker.sh:41-50`), proving the parsing pattern is trivial. But `REPO_PATH` is only threaded to `gh repo clone` and the container name — never forwarded to `af install --init`, so the slug is dropped before init runs. + +### Why #5: Why hasn't a detection mechanism been wired in, given one already exists? +Because the existing git/gh detection idiom was built for a *different* concern (default branch) and never generalized. `internal/cmd/detect_default_branch.go` already provides: +- `runGitDetect(workDir, name, args...)` — a bounded, read-only, ADR-009-seam'd (`var`) exec wrapper returning trimmed stdout or `""` on error (`detect_default_branch.go:34-44`). +- `detectDefaultBranch(workDir)` — a layered chain calling `git symbolic-ref`, `git ls-remote`, and `gh repo view --json ... -q ...` (`detect_default_branch.go:52-73`). +The only missing piece is a sibling `detectRepoSlug` using the same seam (e.g. `gh repo view --json nameWithOwner -q .nameWithOwner`, or `git remote get-url origin` + parse). No such helper exists today (grep for `nameWithOwner` returns zero hits anywhere in the repo), so the wiring was simply never done. The root cause is a static literal that predates — and was never reconciled with — the detection idiom added for default-branch resolution. + +## Evidence Gathered + +| Finding | Source | Evidence | +|---------|--------|----------| +| dispatch.json is a static literal with empty `repos` | `internal/cmd/install.go:145` | ``"dispatch.json": `{"repos":[],"trigger_label":"agentic",...}` `` inside the `starterConfigs` map | +| Literal cannot know the repo (compile-time const in a map) | `internal/cmd/install.go:139-157` | starter configs are static strings written verbatim; only `factory.json` is computed (`config.DefaultFactoryConfigJSON()`, line 142) | +| init flow never queries git/gh for repo identity | `internal/cmd/install.go:97-297` | no `git remote`, `gh repo view`, or arg-read for repo; only git touch is `ensureGitExclude` (write-only, lines 270-271, 842-878) | +| init runs with cwd = cloned repo (has origin) | `quickstart.sh:417-428,441-445` + `install.go:98` | `cd "$repo_dir"` then `af install --init`; `getWd()` (`helpers.go:176-182`) returns that dir | +| target repo cloned with origin set | `quickdocker.sh:582` | `gh repo clone "$REPO_PATH"` populates `origin` in the cloned repo | +| quickdocker KNOWS the slug explicitly but drops it | `quickdocker.sh:355-362,398,582` | `REPO_PATH` normalized to `owner/repo`; passed only to clone/container-name, never to `af install --init` | +| quickdocker already parses owner/repo from a remote | `quickdocker.sh:41-50` | strips `https://github.com/`, `git@github.com:`, `.git` from `git remote get-url origin` for the AF source repo — the exact parse pattern needed | +| Existing read-only git/gh detection seam | `internal/cmd/detect_default_branch.go:34-73` | `runGitDetect` (ADR-009 `var` seam) + `detectDefaultBranch` chain using `git symbolic-ref`, `git ls-remote`, `gh repo view --json` | +| No org/repo-slug helper exists anywhere | `grep nameWithOwner` (repo-wide) | zero matches in `.go`/`.sh`; the parse helper must be added | +| `repos` is consumed as `owner/repo` (`org/repository`) form | `internal/cmd/dispatch.go:172,300,537-539` | dispatch iterates `dispatchCfg.Repos`, passes each as `gh --repo `; `ghLinkedPRs` does `strings.Cut(repo, "/")` and errors if not `owner/name` | +| empty `repos:[]` is actually INVALID for the dispatcher | `internal/config/dispatch.go:142-144` | `validateDispatchConfig` returns an error: "dispatch config must have at least one repo" — so the shipped default cannot even pass its own validator | + +## Tests Performed + +| Test | Command | Result | +|------|---------|--------| +| Confirm dispatch.json literal and empty repos | `grep -n 'dispatch.json' internal/cmd/install.go` | Single hit at line 145 with `"repos":[]` | +| Confirm no nameWithOwner / repo-slug helper exists | `grep -rni "nameWithOwner" . --include="*.go" --include="*.sh"` | Zero matches repo-wide | +| Confirm init flow git interactions | `grep -niE "git remote\|get-url\|gh repo view" internal/cmd/install.go` | No matches (no repo detection in install.go) | +| Confirm cwd at init = cloned repo with origin | Read `quickstart.sh:414-445`, `quickdocker.sh:573-584`, `helpers.go:176-182` | `cd "$repo_dir"` -> `af install --init`; repo cloned via `gh repo clone`; `getWd`=`os.Getwd()` | +| Confirm quickdocker has the slug but drops it | `grep -niE "REPO_PATH\|install --init" quickdocker.sh` | `REPO_PATH` used for clone+container name only; no `af install --init` call in quickdocker (it delegates to quickstart, passing nothing repo-specific) | +| Confirm `repos` consumed as owner/repo | Read `internal/cmd/dispatch.go:172,537-539` | `strings.Cut(repo, "/")`; errors if not `owner/name` form | +| Confirm empty repos fails dispatch validation | Read `internal/config/dispatch.go:142-144` | `validateDispatchConfig` errors when `len(cfg.Repos)==0` | +| Confirm existing detection idiom to mirror | Read `internal/cmd/detect_default_branch.go:34-73` | `runGitDetect` seam + `detectDefaultBranch` layered chain | + +## Conclusion + +**VALIDATED.** The org/repository is never discovered or injected at setup. The mechanism gap is concrete and proven: `dispatch.json` is born from a static compile-time literal (`internal/cmd/install.go:145`) that ships `repos:[]`, and nothing in `runInstallInit` (`install.go:97-297`) ever interrogates the environment for the repo slug — despite the slug being trivially available (the init cwd is the cloned target repo with `origin` set, per `quickstart.sh:428`/`:442` and `getWd()`/`os.Getwd()`). The empty default is not merely unhelpful: `validateDispatchConfig` (`internal/config/dispatch.go:142-144`) actively rejects a zero-length `repos`, so the shipped literal cannot pass the dispatcher's own validator — the dispatcher is dead until an operator manually edits the file. + +**Recommended mechanism — detect the git remote at init time (do NOT rely on quickdocker/quickstart to pass it in).** Mirror the existing, already-blessed read-only detection idiom in `internal/cmd/detect_default_branch.go`: add a sibling `var detectRepoSlug = func(workDir string) string` built on the existing `runGitDetect` ADR-009 seam, using a layered chain: +1. `gh repo view --json nameWithOwner -q .nameWithOwner` (canonical `org/repo`, network/GitHub), then +2. `git remote get-url origin` parsed by stripping `https://github.com/` / `git@github.com:` / `.git` (the exact, already-proven parse from `quickdocker.sh:41-50`), as the offline fallback. +Validate the result against an `owner/repo` shape before accepting it (so a non-GitHub or missing remote yields `""` and the install gracefully falls back to today's empty `repos:[]` rather than baking garbage). + +**Recommended injection point — `runInstallInit` in `internal/cmd/install.go`.** Stop emitting `dispatch.json` from the static `starterConfigs` literal; instead build its content programmatically right before the write loop (`install.go:139-157`), substituting the detected slug into `repos`. This keeps the change in ONE place, reuses the existing detection seam (so it is unit-testable with canned command output exactly like `detectDefaultBranch`'s tests), preserves the write-if-absent idempotency, and degrades safely to the current behavior when no GitHub remote is present. Threading the slug down through `quickdocker.sh -> quickstart.sh -> af install --init` as a new flag is feasible but inferior: it touches three files, leaves the bare `af install --init` (and `agent-gen-all`) paths uncovered, and duplicates parsing logic the binary can do itself from a cwd it already resolves. + +## VERDICT: VALIDATED +The actual `org/repository` is never discovered/injected at setup because `dispatch.json` comes from a static literal at `internal/cmd/install.go:145` with no env interrogation; recommended fix is to add a `detectRepoSlug` helper on the existing `runGitDetect` seam (`gh repo view --json nameWithOwner` with a `git remote get-url origin` parse fallback) and inject the detected slug into `repos` by building `dispatch.json` programmatically inside `runInstallInit` (`internal/cmd/install.go`). diff --git a/.analysis/73/rootcause_concern_3.md b/.analysis/73/rootcause_concern_3.md new file mode 100644 index 0000000..d73cf47 --- /dev/null +++ b/.analysis/73/rootcause_concern_3.md @@ -0,0 +1,140 @@ +# Concern #3 Investigation: Default label→agent mappings — correctness & validation + +**Investigated by**: Sub-agent +**Date**: 2026-06-28 + +## Verdict: VALIDATED + +## Summary +The four proposed mappings are **structurally well-formed** and pass the struct-level +validator `validateDispatchConfig` (`internal/config/dispatch.go:141-185`): each has a +non-empty `labels`, an `agent`, and a `source` of `"issue"` or `"pr"` — the only two +allowed values. The source-vs-formula semantics are also correct: `rapid-soldesign-plan` +is genuinely issue-driven, `rapid-implement` accepts an issue, and both `ultra-review` and +`rapid-increment` are genuinely PR-driven (they take a `pr_uri`). **However, the mappings +do NOT pass the cross-file validator `ValidateDispatchConfig` (dispatch.go:93-138) on a +fresh install, and would fail at dispatch time**, because `mappings[].agent` must name a +registered agent in `agents.json` — and a fresh install ships `agents.json` with only +`manager` and `supervisor` (`internal/cmd/install.go:144`). The values +`rapid-soldesign-plan` / `rapid-implement` / `ultra-review` / `rapid-increment` are +**formula names**, not registered agent names. Unless issue #73's implementation ALSO seeds +agents.json with agents bearing those exact names (the `af formula agent-gen` convention +defaults agent name = formula name, so this is achievable), every proposed mapping +references an unknown agent. The mappings are therefore correct in shape and semantics but +incomplete as a standalone change: they require a paired agents.json seeding to be valid. + +## 5-Whys Analysis + +### Why #1: Why do the proposed mappings pass struct-level validation? +Because `validateDispatchConfig` (`dispatch.go:151-171`) only enforces: not both `label` and +`labels` (`:152`), at least one label (`:159`), `source` ∈ {"" , "issue", "pr"} with "" +defaulting to "issue" (`:162-167`), and a non-empty `agent` (`:168`). All four proposed +mappings satisfy every clause: each supplies `labels` (singular array), an explicit +`source` that is exactly `"issue"` or `"pr"`, and a non-empty `agent`. There is no +duplicate-label rule and no per-mapping label-uniqueness rule at the struct level (the only +duplicate checks are for `workflows[].label`, `dispatch.go:198`), so four distinct +single-label mappings collide with nothing. + +### Why #2: Why are `source:"issue"` and `source:"pr"` both accepted? +Because the source whitelist at `dispatch.go:162` permits exactly `"issue"` and `"pr"` +(empty defaults to `"issue"` at `:165-166`). The dispatcher consumes `source` in +`groupMappingsBySource` (`internal/cmd/dispatch.go:357-366`): `source=="pr"` routes the +mapping to the PR query (`queryGitHubPRs`), everything else to the issue query +(`queryGitHubIssues`). So `source` is the issue-vs-PR selector, and both proposed source +values are valid and meaningful. + +### Why #3: Why are the source assignments semantically correct per formula? +Cross-checked each formula's input contract in `internal/cmd/install_formulas/`: +- `rapid-soldesign-plan` — "rapid design refinement **from a GitHub issue** URI" + (`rapid-soldesign-plan.formula.toml:3`); its sling line takes `--var issue_uri=` + (`internal/templates/roles/rapid-soldesign-plan.md.tmpl:19`). → **issue-source correct.** +- `rapid-implement` — "Requirements come from the assigned bead — which may contain ... a + link to a GitHub issue, or a link to a GitHub pull request" (`rapid-implement.formula.toml:6`); + variable `issue` "The issue/PR ID you're assigned to work on" (`:23`). It accepts an issue, + so `source:"issue"` is valid. → **issue-source correct (flexible; issue is a legitimate input).** +- `ultra-review` — "Multi-agent **pull request** review"; variable `pr_uri` "Pull request to + review" (`ultra-review.formula.toml:33`). → **pr-source correct.** +- `rapid-increment` — "addresses the UNRESOLVED review comments on a **pull request**"; + variable `pr_uri` (`rapid-increment.formula.toml:27`). → **pr-source correct.** + +### Why #4: Why do the mappings nonetheless fail the FULL validator / dispatch? +Because the second validator, `ValidateDispatchConfig` (`dispatch.go:93-138`), cross-checks +every `mapping.agent` against `agents.json`: `if _, ok := agents.Agents[m.Agent]; !ok { +return ... "dispatch mapping references unknown agent %q" }` (`dispatch.go:100-104`). At +dispatch time the same gate is enforced harder: `matchItemToAgent` returns the raw +`m.Agent` string (`internal/cmd/dispatch.go:351`), the dispatcher slings it via +`af sling --agent ` (`dispatch.go:422`), and `resolveSpecialistAgent` +(`sling.go:252-269`) fails with `agent %q not found in agents.json` (`:261`) if the name is +not a registered agent — and with `not a specialist (no formula field)` (`:265`) if it has +no formula. A fresh install's `agents.json` contains only `manager` and `supervisor` +(`internal/cmd/install.go:144`), neither of which matches any proposed `agent`. So all four +mappings reference unknown agents on a fresh install. + +### Why #5: Why is the `agent` field a name-not-formula mismatch, and is it fixable? +The `agent` field is an agents.json key (a registered agent), NOT a formula name +(`DispatchMapping.Agent`, `dispatch.go:34`; consumed as a sling `--agent` value). The four +proposed values are formula names. They become valid agent names only if agents bearing +those exact names are registered — which is exactly what `af formula agent-gen ` +does: it defaults the agent name to the formula name (`formula.go:62`, `:145`, `:235`) and +writes an AgentEntry with `Formula: f.Name` into agents.json. So issue #73 is correct in +INTENT (the names follow the agent-gen convention), but the dispatch.json mappings alone are +insufficient: the change must ALSO seed agents.json with the four agents (or document that +the operator runs `af formula agent-gen` for each), or the dispatcher will reject every item. + +## Evidence Gathered +| Finding | Source | Evidence | +|---------|--------|----------| +| `source` whitelist is exactly {issue, pr}; empty→issue | `internal/config/dispatch.go:162-167` | `if m.Source != "" && m.Source != "issue" && m.Source != "pr" { return ... }` | +| Mapping struct rules: not both label+labels; ≥1 label; non-empty agent | `internal/config/dispatch.go:152,159,168` | three returns under the mappings loop | +| Singular `label` is migrated to `labels` and cleared | `internal/config/dispatch.go:155-158` | `cfg.Mappings[i].Labels = []string{m.Label}` | +| NO struct-level duplicate-label / label-uniqueness rule for mappings | `internal/config/dispatch.go:151-171` | only workflow labels get a `seen` dup check (`:198`) | +| Cross-file validator rejects mapping agent not in agents.json | `internal/config/dispatch.go:100-104` | `"dispatch mapping references unknown agent %q"` | +| `source` drives issue-vs-PR query routing | `internal/cmd/dispatch.go:357-366` | `if m.Source == "pr" { prs = append(...) } else { issues = ... }` | +| Dispatch slings the raw agent name; must be a registered specialist | `internal/cmd/dispatch.go:351,422`; `internal/cmd/sling.go:259-266` | `return m.Agent`; `"sling","--agent",agent`; `"agent %q not found in agents.json"` / `"not a specialist"` | +| Fresh-install agents.json = only manager + supervisor | `internal/cmd/install.go:144` | `"agents.json": {"agents":{"manager":...,"supervisor":...}}` | +| Fresh-install dispatch.json = empty mappings | `internal/cmd/install.go:145` | `"dispatch.json": {... "mappings":[], ...}` | +| rapid-soldesign-plan is issue-sourced | `internal/cmd/install_formulas/rapid-soldesign-plan.formula.toml:3`; `.../roles/rapid-soldesign-plan.md.tmpl:19` | "from a GitHub issue URI"; `--var issue_uri=` | +| rapid-implement accepts an issue input | `internal/cmd/install_formulas/rapid-implement.formula.toml:6,23` | "a link to a GitHub issue, or ... pull request"; var `issue` | +| ultra-review is PR-sourced | `internal/cmd/install_formulas/ultra-review.formula.toml:33` | var `pr_uri` "Pull request to review" | +| rapid-increment is PR-sourced | `internal/cmd/install_formulas/rapid-increment.formula.toml:27` | var `pr_uri`; "review comments on a pull request" | +| agent-gen defaults agent name = formula name | `internal/cmd/formula.go:62,145,235` | `"Override agent name (default: formula name)"`; `agentName := formulaName`; `Formula: f.Name` | + +## Tests Performed +| Test | Command | Result | +|------|---------|--------| +| Dispatch config validation unit tests | `GOTMPDIR=$PWD/.gotmp go test ./internal/config/ -run Dispatch` | `ok` (PASS) — validators behave as documented | +| Confirm source whitelist values | read `dispatch.go:162` | only `"issue"`/`"pr"`/`""` accepted; all 4 proposed sources valid | +| Confirm agent field is cross-checked vs agents.json | read `dispatch.go:100-104` + `sling.go:259-266` | unknown agent → error at validate AND at dispatch time | +| Confirm fresh-install agents.json contents | read `install.go:144` | only `manager`, `supervisor` — none of the 4 proposed agents present | + +## Conclusion + +**Verdict: VALIDATED** — the concern (no default mappings exist; assess proposed mappings' +correctness and validation) is real and the investigation produces an actionable per-mapping +assessment. + +Per-mapping assessment: + +| # | Proposed mapping | Struct validation (`validateDispatchConfig`) | Source semantics | Cross-file validation (`ValidateDispatchConfig` + dispatch) | +|---|---|---|---|---| +| 1 | `{labels:[rapid-plan], source:issue, agent:rapid-soldesign-plan}` | PASS | CORRECT — issue-driven planner | **FAILS on fresh install** — `rapid-soldesign-plan` not in agents.json | +| 2 | `{labels:[rapid-engineer], source:issue, agent:rapid-implement}` | PASS | CORRECT — rapid-implement accepts an issue | **FAILS on fresh install** — `rapid-implement` not in agents.json | +| 3 | `{labels:[pr-review], source:pr, agent:ultra-review}` | PASS | CORRECT — PR reviewer | **FAILS on fresh install** — `ultra-review` not in agents.json | +| 4 | `{labels:[pr-iterate], source:pr, agent:rapid-increment}` | PASS | CORRECT — addresses PR comments | **FAILS on fresh install** — `rapid-increment` not in agents.json | + +Key findings: +1. **All four mappings are structurally valid and semantically correct** on source (issue vs + PR) and agent-intent. No mapping has a wrong source, no nonexistent-as-a-formula name, and + no duplicate-label collision. +2. **The blocker is the agent/formula-name distinction.** `mappings[].agent` is a registered + agents.json key (slung as `af sling --agent `), not a formula name. The proposed + values are formula names. On a fresh install agents.json holds only `manager` and + `supervisor`, so `ValidateDispatchConfig` rejects all four with + `"dispatch mapping references unknown agent"`, and even if struct-saved, the dispatcher + would error `agent ... not found in agents.json` at dispatch time. +3. **The fix is paired, not standalone.** Issue #73 must ALSO seed agents.json with four + formula-bearing agents named exactly `rapid-soldesign-plan`, `rapid-implement`, + `ultra-review`, `rapid-increment` (the `af formula agent-gen` convention already makes + agent name = formula name, so the names chosen are right), OR the proposed mappings must + reference whatever agent names the install flow actually registers. Shipping the mappings + without the agents would make a fresh install's dispatch.json fail validation/dispatch. diff --git a/.analysis/73/rootcause_concern_4.md b/.analysis/73/rootcause_concern_4.md new file mode 100644 index 0000000..61cacf4 --- /dev/null +++ b/.analysis/73/rootcause_concern_4.md @@ -0,0 +1,155 @@ +# Concern #4 Investigation: Mapped agents may not be provisioned on a fresh install + +**Investigated by**: Sub-agent +**Date**: 2026-06-28 + +## Verdict: VALIDATED + +## Summary +A fresh `af install --init` registers ONLY `manager` and `supervisor` in `agents.json` +and lists only `manager` in `startup.json`. The four agents the proposed default +dispatch.json maps labels to — `rapid-soldesign-plan`, `rapid-implement`, `ultra-review`, +`rapid-increment` — are NOT registered in agents.json on a fresh factory. The dispatcher +gates on agent presence at TWO points: (1) `ValidateDispatchConfig` runs at the very start +of every dispatch cycle and **aborts the entire cycle** if any mapping references an agent +absent from agents.json (`internal/config/dispatch.go:101-102`), and (2) even past that gate, +`af sling --agent ` calls `resolveSpecialistAgent`, which fails with +`agent %q not found in agents.json` (`internal/cmd/sling.go:261`). Neither `af sling` +nor the dispatcher auto-provisions or auto-registers an absent agent. The only code path that +registers a formula-derived agent into agents.json is `af formula agent-gen` (driven by +`agent-gen-all.sh`, which is invoked only by `af install --agents` — NOT by `af install --init` +and NOT by `quickstart.sh`). Therefore, with the proposed default mappings, dispatch would +FAIL on a fresh factory before it ever queries GitHub or slings anything. + +## 5-Whys Analysis + +### Why #1: Why might dispatch fail to sling the mapped agents on a fresh install? +Because the mapped agents are not present in `agents.json`. `af install --init` writes a +literal starter `agents.json` containing only `manager` and `supervisor` +(`internal/cmd/install.go:143`): +``` +"agents.json": `{"agents":{"manager":{"type":"interactive",...},"supervisor":{"type":"autonomous",...}}}` +``` +`startup.json` lists only `["manager"]` in its agents array (`internal/cmd/install.go:147`): +``` +"startup.json": `{"agents":["manager"],"quality":"default","fidelity":"default","start_dispatch":true,"watchdog_agents":["manager","supervisor"]}` +``` +The default dispatch.json on `--init` has empty mappings (`internal/cmd/install.go:145`); +the proposed mappings to rapid-soldesign-plan / rapid-implement / ultra-review / rapid-increment +are the change being evaluated, and none of those four agents is in the default agents.json. + +### Why #2: Why does an absent mapped agent break dispatch (rather than being skipped)? +Because the dispatcher validates agent presence as a hard precondition. At the start of every +dispatch cycle, `runDispatch` loads agents.json and calls `ValidateDispatchConfig` +(`internal/cmd/dispatch.go:138-148`). That validator iterates every mapping and returns an error +the moment one references an unknown agent (`internal/config/dispatch.go:100-103`): +``` +for _, m := range disp.Mappings { + if _, ok := agents.Agents[m.Agent]; !ok { + return fmt.Errorf("dispatch mapping references unknown agent %q", m.Agent) + } +``` +`runDispatch` returns this error immediately (`internal/cmd/dispatch.go:146-148`), BEFORE the +gh-auth check and BEFORE acquiring the dispatch-cycle lock — so the whole cycle aborts; no issue +is queried and nothing is slung. + +### Why #3: Why doesn't `af sling --agent` create/register the agent on demand? +Because sling is a strict lookup, not a provisioner. `af sling --agent --reset ` +(the exact argv the dispatcher runs, `internal/cmd/dispatch.go:422`) routes to +`dispatchToSpecialist` → `resolveSpecialistAgent`, which loads agents.json and fails if the +agent is missing (`internal/cmd/sling.go:259-266`): +``` +entry, ok := agentsCfg.Agents[agentName] +if !ok { + return ..., fmt.Errorf("agent %q not found in agents.json", agentName) +} +if entry.Formula == "" { + return ..., fmt.Errorf("agent %q is not a specialist (no formula field in agents.json)", agentName) +} +``` +There is no auto-add, auto-provision, or auto-up branch. (Sling does NOT require a running tmux +session — it launches one — and does NOT require a pre-existing on-disk agent dir — it creates the +worktree/agent dir. The ONE thing it strictly requires is an agents.json entry with a `formula` field.) + +### Why #4: Why isn't the agents.json entry created during a fresh install / quickstart? +Because the only writer of formula-derived agents.json entries is `af formula agent-gen`, and it +is never invoked by `--init` or by the quickstart bootstrap. +- `af install --init` writes the formula TOMLs into `store/formulas/` (`internal/cmd/install.go:252-257`, + `writeFormulas` from the embedded `install_formulas/` set, which DOES include all four formulas), + but a formula sitting in the store does NOT register an agent. Registration is a separate step. +- `af formula agent-gen ` is what loads agents.json, adds an entry with `Formula: f.Name`, + and saves it back (`internal/cmd/formula.go:214-246`). It also writes the role template. +- `agent-gen-all.sh` is the only thing that runs `af formula agent-gen` for every formula in the + store (`agent-gen-all.sh:134-153`), and it is invoked only by `af install --agents` + (`runInstallAgents` → `runAgentGenScript`, `internal/cmd/install.go:692-696`). +- `quickstart.sh` runs `af install --init` then provisions ONLY manager and supervisor via + `af install manager` / `af install supervisor` (`quickstart.sh:441-470`). It does NOT run + `agent-gen-all.sh` and does NOT call `af install --agents`. + +### Why #5: Why can't the user just run `af install rapid-implement` to provision it? +Because `af install ` is a provisioner that itself REQUIRES the role to already exist in +agents.json. `runInstallRole` loads agents.json and aborts if the role is absent +(`internal/cmd/install.go:509-512`): +``` +entry, ok := agents.Agents[role] +if !ok { + return fmt.Errorf("agent %q not found in agents.json", role) +} +``` +So on a fresh factory `af install rapid-implement` fails. The user must FIRST register the agent +(via `af formula agent-gen rapid-implement` or `af install --agents`, which registers all of them), +and only then can dispatch's mapping resolve. The root cause is that registration of the mapped +specialist agents into agents.json is a manual/`--agents` step that is NOT part of the `--init` +or quickstart fresh-install path. + +## Evidence Gathered + +| Finding | Source | Evidence | +|---------|--------|----------| +| `--init` agents.json contains only manager + supervisor | `internal/cmd/install.go:143` | `"agents.json": {"agents":{"manager":...,"supervisor":...}}` (no rapid-* / ultra-review) | +| `--init` startup.json agents list is `["manager"]` | `internal/cmd/install.go:147` | `"startup.json": {"agents":["manager"],...,"watchdog_agents":["manager","supervisor"]}` | +| `--init` dispatch.json default has empty mappings | `internal/cmd/install.go:145` | `"dispatch.json": {...,"mappings":[],...}` | +| `--init` DOES write all formula TOMLs (incl. the 4 mapped) to the store | `internal/cmd/install.go:252-257`, `install_formulas/` listing | writeFormulas copies embedded `install_formulas/*.formula.toml`; dir contains rapid-soldesign-plan, rapid-implement, ultra-review, rapid-increment | +| Formula in store ≠ registered agent; agent-gen is the registrar | `internal/cmd/formula.go:214-246` | LoadAgentConfig → build entry with `Formula:` → `AddAgentEntry` → `SaveAgentConfig` | +| Dispatch cycle aborts if a mapping agent is absent | `internal/config/dispatch.go:100-103`; `internal/cmd/dispatch.go:146-148` | `dispatch mapping references unknown agent %q`; returned before gh-auth/lock | +| ValidateDispatchConfig runs on the dispatch path (not just config-write) | `internal/cmd/dispatch.go:144-148` | same validator as `config_set.go:89` — "rule cannot drift" | +| `af sling --agent` requires agents.json entry; no auto-provision | `internal/cmd/sling.go:259-266` | `agent %q not found in agents.json` / not a specialist if no `formula` field | +| Dispatcher invokes exactly `af sling --agent --reset ` | `internal/cmd/dispatch.go:422` | `args := []string{"sling", "--agent", agent, "--reset"}` | +| `af install ` requires the role to pre-exist in agents.json | `internal/cmd/install.go:509-512` | `agent %q not found in agents.json` | +| Only `af install --agents` runs agent-gen-all.sh (registers all formula agents) | `internal/cmd/install.go:692-696`; `agent-gen-all.sh:134-153` | runInstallAgents → runAgentGenScript; loop runs `af formula agent-gen` per formula | +| quickstart.sh provisions ONLY manager + supervisor; no --agents / agent-gen-all | `quickstart.sh:441-470` | `af install --init`, then `af install manager`, `af install supervisor` only | + +## Tests Performed + +| Test | Command | Result | +|------|---------|--------| +| What `--init` writes to agents.json | Read `internal/cmd/install.go:139-157` (starterConfigs map) | Only manager + supervisor; confirmed | +| What `--init` writes to startup.json | Read `internal/cmd/install.go:147` | `agents:["manager"]`; confirmed | +| Whether the 4 mapped formulas ship in the embedded set | `ls internal/cmd/install_formulas/` | All 4 present (rapid-soldesign-plan, rapid-implement, ultra-review, rapid-increment) | +| Whether sling auto-provisions an absent agent | Read `internal/cmd/sling.go:128-266` | No; hard fail at resolveSpecialistAgent (sling.go:261) | +| Whether dispatch pre-validates mapping agents | grep `ValidateDispatchConfig` + read `internal/config/dispatch.go:93-103` & `internal/cmd/dispatch.go:144-148` | Yes; aborts cycle on unknown agent | +| Whether quickstart registers the mapped agents | Read `quickstart.sh:414-471` | No; only manager + supervisor provisioned | +| Whether agent-gen-all.sh registers them and what invokes it | Read `agent-gen-all.sh:134-153`, `internal/cmd/install.go:692-696` | Yes via `af formula agent-gen`; invoked only by `af install --agents` | + +## Conclusion + +**Verdict: VALIDATED.** + +On a fresh `af install --init` (the path quickstart.sh drives), the four agents the proposed +default dispatch mappings target — `rapid-soldesign-plan`, `rapid-implement`, `ultra-review`, +`rapid-increment` — are NOT registered in `agents.json` and NOT provisioned on disk. Only +`manager` and `supervisor` are. Although `--init` does copy all four formula TOMLs into +`.agentfactory/store/formulas/`, a formula in the store does not register an agent. + +**Exact dependency for dispatch to sling the mapped agents on a fresh factory:** each mapped +agent must have an entry in `.agentfactory/agents.json` with a non-empty `formula` field. The +ONLY code path that creates such entries is `af formula agent-gen ` — run en masse by +`agent-gen-all.sh`, which is itself invoked only by `af install --agents`. Until that runs, +`ValidateDispatchConfig` (`internal/config/dispatch.go:101-102`) rejects the dispatch.json with +`dispatch mapping references unknown agent`, aborting the dispatch cycle at the top +(`internal/cmd/dispatch.go:146-148`) before any GitHub query — and even past validation, +`af sling --agent` would fail at `internal/cmd/sling.go:261`. So with the proposed default +mappings, dispatch would FAIL (not silently no-op) on a fresh factory. The proposed default +dispatch.json is only viable if the fresh-install path also registers/provisions those four +agents (e.g., by adding their agents.json entries to the `--init` starter config, or by making +quickstart run `af install --agents`). diff --git a/.analysis/73/rootcause_concern_5.md b/.analysis/73/rootcause_concern_5.md new file mode 100644 index 0000000..e61b2b5 --- /dev/null +++ b/.analysis/73/rootcause_concern_5.md @@ -0,0 +1,134 @@ +# Concern #5 Investigation: Mapped formulas' dispatch input-compatibility + +**Investigated by**: Sub-agent +**Date**: 2026-06-28 + +## Verdict: INVALIDATED + +## Summary +All four formulas mapped in the proposed issue-#73 dispatch.json +(`rapid-soldesign-plan`, `rapid-implement`, `ultra-review`, `rapid-increment`) +accept exactly ONE required input that has no default, and that single input is +satisfiable from the labeled issue/PR URL that the dispatcher supplies as the +positional `task` argument. The dispatcher slings `af sling --agent +--reset ` (`internal/cmd/dispatch.go:422-426`), and the sling +input-bridging logic fills the one unsatisfied required input with that URL +(`internal/cmd/sling.go:472-484` for `[inputs]`-bearing formulas; the +auto-assignment-bead path at `internal/cmd/sling.go:432-444` for rapid-implement, +which has no `[inputs]` table and consumes the URL through its bead). No mapped +formula has more than one required-without-default input, so none of these +mappings is broken by the "multiple required inputs" failure mode. The concern's +hypothesis — that a mapped formula (e.g. rapid-implement needing `outline_path` / +a plan artifact) requires more than the issue/PR and would fail to dispatch +autonomously — is NOT borne out by the code. The concern is INVALIDATED. + +## 5-Whys Analysis + +### Why #1: Why might a label-dispatch fail to run a mapped formula autonomously? +Because a label-dispatch supplies ONLY the labeled issue/PR URL. If the mapped +formula declares MORE than one required input with no default, sling cannot fill +them all and errors out before instantiation. This is enforced explicitly: +`instantiateFormulaWorkflow` returns an error when +`findUnsatisfiedRequiredInputs` returns >1 (`internal/cmd/sling.go:477-483`): +`"formula %q has %d required inputs not provided ... provide all but one via +--var flags, the remaining one receives the positional text argument"`. + +### Why #2: Why would a formula have multiple required inputs that the URL can't fill? +Because a formula could require both the problem item AND a separately-produced +artifact (the concern's example: rapid-implement needing `outline_path` or a plan +artifact from rapid-soldesign-plan). So the question reduces to: does ANY mapped +formula declare a second required-without-default input? + +### Why #3: Why does each mapped formula end up with only ONE unsatisfiable input? +Because `findUnsatisfiedRequiredInputs` (`internal/cmd/sling.go:872-895`) skips an +input when it has a non-empty `Default` (line 875: `if !inp.Required || +inp.Default != ""`). rapid-soldesign-plan declares four `required=true` inputs, +but three of them (`analyst_name`, `designer_name`, `impl_name`) carry defaults, +so only `issue_uri` remains unsatisfied — exactly one. ultra-review and +rapid-increment declare a single required `pr_uri` (their other input, +`min_confidence`, is `required=false` with a default). rapid-implement has NO +`[inputs]` table at all; its `issue` comes from a `[vars.issue]` with +`source=cli`, satisfied by the auto-created assignment bead, not the inputs-bridge. + +### Why #4: Why does rapid-implement (no `[inputs]`) still receive the URL correctly? +Because the inputs-bridge block is guarded by `len(f.Inputs) > 0` +(`internal/cmd/sling.go:473`), which is false for rapid-implement. Instead, the +earlier auto-bead path fires: when `TaskDescription != "" && cliVars["issue"] == +""` (`internal/cmd/sling.go:432`), sling creates an assignment bead carrying the +URL in its description and sets `cliVars["issue"] = iss.ID` +(`internal/cmd/sling.go:442`). rapid-implement's `load-context` step then reads +the bead and classifies the input mode (issue vs PR link) from its content +(rapid-implement.formula.toml:88-110). So the single `issue` var is satisfied and +the URL is delivered. Both bridging mechanisms converge on "single input gets the +URL." + +### Why #5: Why doesn't the feature-workflow (rapid-plan → rapid-engineer) need an +explicit plan-artifact `--var` to bridge the two phases? +Because rapid-implement does NOT take a `plan`/`outline_path` input at all — it is +self-classifying. The proposed dispatch.json wires the two-phase pipeline through +GitHub labels + the workflow engine, not through a CLI var. The `feature-workflow` +workflow (`label: feature-workflow`, `phases: [rapid-plan, rapid-engineer]`) +hands the SAME issue URL to BOTH phases: after `rapid-plan` (rapid-soldesign-plan) +completes, the workflow advances the label and re-slings `rapid-engineer` +(rapid-implement) with the same `item.URL` +(`internal/cmd/dispatch.go:1159-1177` `slingPhase`; URL re-used at line 1161). +The handoff of the produced plan happens through the GitHub PR that +rapid-soldesign-plan opens (its `commit-and-pr` step opens a design PR and its +`dispatch-impl` step pushes `implementation_plan_outline.md` onto that PR branch), +which the next consumer reads from GitHub — NOT through a formula `--var`. So no +multi-required-input bridging is needed, and the concern's worry that +rapid-implement needs an extra `outline_path` var is unfounded. + +## Evidence Gathered + +| Finding | Source | Evidence | +|---------|--------|----------| +| Dispatcher slings the URL as the single positional task arg | `internal/cmd/dispatch.go:422-426` | `args := []string{"sling", "--agent", agent, "--reset"}` ... `args = append(args, itemURL)` | +| dispatchToSpecialist appends `task=` and routes to instantiation | `internal/cmd/sling.go:220-226` | `CLIVars: append(slingVars, fmt.Sprintf("task=%s", task))` | +| Input-bridging rule: text fills the SINGLE unsatisfied required input | `internal/cmd/sling.go:472-484` | `if len(unsatisfied)==1 { cliVars[unsatisfied[0]] = params.TaskDescription }` | +| Multiple unsatisfied required inputs => hard error (not autonomous) | `internal/cmd/sling.go:477-483` | `else if len(unsatisfied) > 1 { return ... "%d required inputs not provided" }` | +| Inputs with a default are NOT counted as unsatisfied | `internal/cmd/sling.go:875` | `if !inp.Required \|\| inp.Default != "" { continue }` | +| Auto-bead path satisfies `issue` for formulas without `[inputs]` | `internal/cmd/sling.go:432-444` | `if params.TaskDescription != "" && cliVars["issue"] == "" { ... cliVars["issue"] = iss.ID }` | +| rapid-soldesign-plan: one required-no-default input `issue_uri`; other 3 required inputs carry defaults | `rapid-soldesign-plan.formula.toml:83-105` | `issue_uri` required, no default; `analyst_name`/`designer_name`/`impl_name` required WITH `default=` | +| rapid-implement: no `[inputs]`; single `[vars.issue]` source=cli | `rapid-implement.formula.toml:913-917` | `[vars.issue] ... required = true ... source = "cli"` (grep `^\[inputs` count = 0) | +| rapid-implement is self-classifying (issue OR pr link), needs no plan var | `rapid-implement.formula.toml:88-110` | load-context classifies MODE=pr/issue from the bead input; no `outline_path` input exists | +| ultra-review: one required-no-default input `pr_uri`; `min_confidence` optional+default | `ultra-review.formula.toml:112-122` | `pr_uri` required; `min_confidence` `required=false` `default="70"` | +| rapid-increment: single required-no-default input `pr_uri` | `rapid-increment.formula.toml:995-999` | `pr_uri` required, no default; no other cli vars | +| All four formulas infer `TypeWorkflow` (have `[[steps]]`) so inputs-bridge path applies | `internal/formula/parser.go:37-51` | `if len(f.Steps) > 0 { f.Type = TypeWorkflow }` | +| Each mapped agent name resolves 1:1 to the same-named formula in agents.json | `.agentfactory/agents.json` | `rapid-soldesign-plan→rapid-soldesign-plan`, `rapid-implement→rapid-implement`, `ultra-review→ultra-review`, `rapid-increment→rapid-increment` | +| Workflow engine re-uses the SAME item URL across phases (no var handoff) | `internal/cmd/dispatch.go:1159-1177` | `slingPhase` calls `dispatchItem(w.root, agent, w.item.URL, ...)` for every phase | +| Plan→impl artifact handoff is via the GitHub PR, not a CLI var | `rapid-soldesign-plan.formula.toml:585-654, 656-734` | `dispatch-impl` slings impl agent with the PR link; `finalize` verifies `implementation_plan_outline.md` on the PR branch | + +## Tests Performed + +| Test | Command | Result | +|------|---------|--------| +| Enumerate required-no-default inputs + cli vars per formula | python TOML scan over the 4 `.formula.toml` files | rapid-soldesign-plan: only `issue_uri`; rapid-implement: only `issue` (var); ultra-review: only `pr_uri`; rapid-increment: only `pr_uri` — each exactly ONE | +| Confirm dispatcher argv passes URL positionally | `grep -n 'args := \[\]string{"sling"' internal/cmd/dispatch.go` | dispatch.go:422 `{"sling","--agent",agent,"--reset"}` + `append(args, itemURL)` | +| Confirm agent→formula 1:1 mapping | `jq '.agents[$a].formula' .agentfactory/agents.json` for all 4 | each maps to its same-named formula | +| Confirm formula type inference (workflow) | read `internal/formula/parser.go:37-51` | `[[steps]]` ⇒ TypeWorkflow for all 4 | +| Confirm default-bearing required inputs are skipped by bridging | read `findUnsatisfiedRequiredInputs` (`sling.go:872-895`) | line 875 short-circuits on non-empty Default | + +## Conclusion + +**Verdict: INVALIDATED.** The concern hypothesizes that one or more mapped +formulas (specifically rapid-implement, suspected of needing an `outline_path` / +plan artifact) require MORE than the labeled issue/PR and would therefore fail to +run autonomously under label-dispatch. The codebase does not support this: every +mapped formula has exactly one required-without-default input, all four are +satisfiable from the single issue/PR URL the dispatcher supplies, and the +plan→impl handoff is mediated by GitHub PRs (label-driven workflow advance), +never by a second required CLI var. + +### Per-agent input-compatibility table + +| Mapped agent (→ formula) | Source | Required inputs WITHOUT default | Optional / defaulted inputs | URL-only dispatch satisfies all required? | Label-dispatchable as-is? | +|--------------------------|--------|---------------------------------|------------------------------|--------------------------------------------|----------------------------| +| rapid-soldesign-plan → rapid-soldesign-plan | issue | `issue_uri` (1) | `analyst_name`, `designer_name`, `impl_name` (required but defaulted) | YES — `issue_uri` ← URL via inputs-bridge | YES | +| rapid-implement → rapid-implement | issue | `issue` (1, via `[vars]` source=cli; no `[inputs]`) | — | YES — `issue` ← auto-assignment bead carrying the URL | YES | +| ultra-review → ultra-review | pr | `pr_uri` (1) | `min_confidence` (`required=false`, default 70) | YES — `pr_uri` ← URL via inputs-bridge | YES | +| rapid-increment → rapid-increment | pr | `pr_uri` (1) | — | YES — `pr_uri` ← URL via inputs-bridge | YES | + +**No mapped formula needs more than the issue/PR to dispatch.** The +"multiple-required-inputs" failure mode (`sling.go:477-483`) is NOT triggered by +any of the four proposed mappings. diff --git a/.analysis/73/rootcause_concern_6.md b/.analysis/73/rootcause_concern_6.md new file mode 100644 index 0000000..8dab576 --- /dev/null +++ b/.analysis/73/rootcause_concern_6.md @@ -0,0 +1,125 @@ +# Concern #6 Investigation: Proposed feature-workflow validity + +**Investigated by**: Sub-agent +**Date**: 2026-06-28 + +## Verdict: VALIDATED + +## Summary +The proposed `{label:"feature-workflow", phases:["rapid-plan","rapid-engineer"]}` workflow +PASSES every workflow-validation rule in `internal/config/dispatch.go` against the proposed +mappings. Both phases back single-label mappings (`rapid-plan`→`rapid-soldesign-plan`, +`rapid-engineer`→`rapid-implement`); both mappings are `source:"issue"`, so they share a +source; the workflow label `"feature-workflow"` is distinct from `trigger_label:"agentic"` +and from every mapping label (`rapid-plan`, `rapid-engineer`, `pr-review`, `pr-iterate`); +neither phase equals `trigger_label`; and both phase agents are formula-bearing in +agents.json (`rapid-soldesign-plan`→formula `rapid-soldesign-plan`, `rapid-implement`→formula +`rapid-implement`), so the cross-file formula-bearing check also passes. The codebase even +ships a `TestUsingDoc_WorkflowsExampleValidates` test whose documented example uses the SAME +`feature-workflow` label with a structurally identical two-phase issue-source pipeline, and +all 12 workflow tests pass in this worktree. "Validates as written" is established by both +hand-tracing every rule with file:line evidence and by running the existing test suite. + +## 5-Whys Analysis + +### Why #1: Why might the proposed feature-workflow fail validation? +Because `validateWorkflows` (dispatch.go:192-249) enforces seven distinct rules, and a +workflow that trips any one is rejected at load time. So I must trace each rule against the +exact proposed config. + +### Why #2: Why does the workflow label "feature-workflow" pass the label rules? +- Non-empty (dispatch.go:195-197): "feature-workflow" is non-empty. PASS. +- Not duplicated (dispatch.go:198-201, `seen` map): only one workflow in the proposed config. PASS. +- Label != `trigger_label` (dispatch.go:202-204): "feature-workflow" != "agentic". PASS. +- Label not in its own phases (LOW-2, dispatch.go:210-214): phases are `["rapid-plan","rapid-engineer"]`; neither is "feature-workflow". PASS. +- Label not also a mapping label (HIGH-B, dispatch.go:219-221, via `phaseInAnyMapping`): the + four mapping labels are `rapid-plan`, `rapid-engineer`, `pr-review`, `pr-iterate`; + "feature-workflow" is none of them. PASS. + +### Why #3: Why does each phase pass the per-phase rules? +Loop at dispatch.go:223-246 over `["rapid-plan","rapid-engineer"]`: +- Phase != `trigger_label` (LOW-2, dispatch.go:225-227): neither "rapid-plan" nor + "rapid-engineer" equals "agentic". PASS. +- Phase resolves on the label ALONE (CRITICAL-2, dispatch.go:232-238, via + `phaseResolvesAlone` dispatch.go:256-267): `phaseResolvesAlone` returns the mapping whose + `Labels` is exactly `[phase]`. Mapping 1 is `{labels:["rapid-plan"], agent:"rapid-soldesign-plan"}` + (single-label, exact match) and mapping 2 is `{labels:["rapid-engineer"], agent:"rapid-implement"}` + (single-label, exact match). Both resolve. PASS. (No multi-label ANDed mapping is involved, + so the "on the phase label alone" rejection branch is not reached.) + +### Why #4: Why do the phases pass the same-source rule (HIGH-2)? +dispatch.go:239-245 records `workflowSource` from the first phase's resolved mapping and +rejects any later phase whose mapping source differs. The proposed mappings set +`source:"issue"` explicitly on both `rapid-plan` and `rapid-engineer`. Even if `source` were +omitted, `validateDispatchConfig` defaults an empty source to `"issue"` (dispatch.go:165-167) +BEFORE `validateWorkflows` runs (ordering guaranteed by dispatch.go:172 calling +`validateWorkflows` after the mapping loop, as documented at dispatch.go:187-191). Both phases +resolve to `source:"issue"` → identical. PASS. (The `pr`-source mappings `pr-review` and +`pr-iterate` are NOT referenced by this workflow's phases, so they are irrelevant to its +same-source check.) + +### Why #5: Why does the cross-file formula-bearing check pass? +`ValidateDispatchConfig` (dispatch.go:93-138) re-resolves each phase via `phaseResolvesAlone` +(dispatch.go:115-129) and rejects a phase whose mapped agent has an empty `Formula` +(dispatch.go:125-127). In `.agentfactory/agents.json`, `rapid-soldesign-plan` has +`formula:"rapid-soldesign-plan"` and `rapid-implement` has `formula:"rapid-implement"` — both +non-empty. Both formula files exist (`internal/cmd/install_formulas/rapid-soldesign-plan.formula.toml` +and `rapid-implement.formula.toml`, also mirrored under `.agentfactory/store/formulas/`). +So the formula-bearing check passes. Every rule passes → the workflow VALIDATES as written. + +## Evidence Gathered + +| Finding | Source | Evidence | +|---------|--------|----------| +| Workflow struct is `{Label, Phases}` (bare v1 shape) | internal/config/dispatch.go:42-45 | `Label string`; `Phases []string` | +| Rule: workflow label non-empty | internal/config/dispatch.go:195-197 | `if wf.Label == "" { return ... "workflow must have a label" }` | +| Rule: no duplicate workflow label | internal/config/dispatch.go:198-201 | `seen` map; "has duplicate label" | +| Rule: workflow label != trigger_label | internal/config/dispatch.go:202-204 | `if wf.Label == cfg.TriggerLabel` → "collides with trigger_label" | +| Rule: workflow must have ≥1 phase | internal/config/dispatch.go:205-207 | "must have at least one phase" | +| Rule (LOW-2): label not in its own phases | internal/config/dispatch.go:210-214 | `if phase == wf.Label` → "also appears in its phases" | +| Rule (HIGH-B): workflow label != any mapping label | internal/config/dispatch.go:219-221 | `phaseInAnyMapping(cfg.Mappings, wf.Label)` → "must not also be a mapping label" | +| Rule (LOW-2): phase != trigger_label | internal/config/dispatch.go:225-227 | `if phase == cfg.TriggerLabel` → "collides with trigger_label" | +| Rule (CRITICAL-2): phase backs a SINGLE-label mapping | internal/config/dispatch.go:232-238 + 256-267 | `phaseResolvesAlone` requires `len(Labels)==1 && Labels[0]==phase` | +| Rule (HIGH-2): all phases same source (v1) | internal/config/dispatch.go:239-245 | records `workflowSource` from phase 0, rejects mismatch | +| Source defaults to "issue" before workflow check | internal/config/dispatch.go:165-167, 172, 187-191 | empty `Source` set to `"issue"`; `validateWorkflows` runs after mapping loop | +| Cross-file rule: phase agent must have a formula | internal/config/dispatch.go:110-129 | `if entry.Formula == "" { return ... "no formula (cannot signal completion)" }` | +| `rapid-soldesign-plan` agent has formula binding | .agentfactory/agents.json | `formula="rapid-soldesign-plan"`, type=autonomous | +| `rapid-implement` agent has formula binding | .agentfactory/agents.json | `formula="rapid-implement"`, type=autonomous | +| Both phase formulas exist | internal/cmd/install_formulas/{rapid-soldesign-plan,rapid-implement}.formula.toml | files present (also in .agentfactory/store/formulas/) | +| Docs ship the same `feature-workflow` label, structurally identical pipeline | USING_AGENTFACTORY.md:216-238 | `{"label":"feature-workflow","phases":["design","build"]}`; both phases single-label issue mappings | +| Documented example is test-enforced to load+validate | internal/config/using_workflow_doc_test.go:13-41 | `TestUsingDoc_WorkflowsExampleValidates` loads the doc's JSON via `LoadDispatchConfig` | + +## Tests Performed + +| Test | Command | Result | +|------|---------|--------| +| Existing workflow validation suite passes in this worktree | `go test ./internal/config/ -run 'Workflow\|MixedSource\|PhaseNotResolvable\|DuplicateWorkflow\|LabelPhaseTrigger' -v` (TMPDIR/GOCACHE redirected inside worktree) | PASS — 12 tests incl. `TestLoadDispatchConfig_ValidWorkflow_Loads`, `TestValidateDispatchConfig_WorkflowPhaseAgentWithFormula_OK`, `TestUsingDoc_WorkflowsExampleValidates` | +| Confirm formula bindings for proposed phase agents | `python3` read of `.agentfactory/agents.json` | `rapid-soldesign-plan`→`formula="rapid-soldesign-plan"`; `rapid-implement`→`formula="rapid-implement"` (both non-empty) | +| Confirm doc example uses same workflow shape | `grep -A30 '"workflows"' USING_AGENTFACTORY.md` | `feature-workflow` with two single-label issue-source phases | +| Attempted standalone external-module driver | (abandoned) | Go blocks importing `internal/config` from an external module; the in-package tests above cover the exact shapes — chosen instead of modifying source (read-only constraint) | + +## Conclusion + +**Verdict: VALIDATED.** The proposed `{label:"feature-workflow", phases:["rapid-plan","rapid-engineer"]}` +workflow passes `dispatch.go` workflow validation as written against the proposed mappings. + +Per-rule pass/fail for the proposed workflow: + +| Rule | dispatch.go | Result | +|------|-------------|--------| +| Workflow label non-empty | :195-197 | PASS ("feature-workflow") | +| No duplicate workflow label | :198-201 | PASS (single workflow) | +| Workflow label != trigger_label ("agentic") | :202-204 | PASS | +| ≥1 phase | :205-207 | PASS (2 phases) | +| Label not in its own phases | :210-214 | PASS | +| Workflow label != any mapping label | :219-221 | PASS (not rapid-plan/rapid-engineer/pr-review/pr-iterate) | +| Each phase != trigger_label | :225-227 | PASS (both) | +| Each phase backs a single-label mapping (resolves alone) | :232-238, 256-267 | PASS (rapid-plan→rapid-soldesign-plan; rapid-engineer→rapid-implement) | +| All phases same source (v1) | :239-245 | PASS (both source="issue") | +| Each phase agent has a formula (cross-file) | :110-129 | PASS (both formula-bearing in agents.json) | + +No rule is violated. Caveat: the cross-file formula-bearing check (`ValidateDispatchConfig`) +is only run by the CLI/external-write path, not by `LoadDispatchConfig` itself; for the +proposed config it passes regardless, because both phase agents have non-empty formula +bindings in agents.json. The structural validity of this workflow is independently corroborated +by the codebase's own `feature-workflow` documentation example and the test that enforces it. diff --git a/.analysis/73/rootcause_concern_7.md b/.analysis/73/rootcause_concern_7.md new file mode 100644 index 0000000..b9ec559 --- /dev/null +++ b/.analysis/73/rootcause_concern_7.md @@ -0,0 +1,147 @@ +# Concern #7 Investigation: Dispatcher auto-activation on fresh setup + +**Investigated by**: Sub-agent +**Date**: 2026-06-28 + +## Verdict: VALIDATED + +## Summary +On a fresh `af install --init`, the shipped `dispatch.json` default has **empty +`repos` and empty `mappings`** (`internal/cmd/install.go:145`). `startup.json` +correctly ships `start_dispatch:true` (`internal/cmd/install.go:147`), and a +bare `af up` DOES call `startDispatch` because of it +(`internal/cmd/up.go:330-331`). However, `startDispatch` loads and validates +`dispatch.json`; the empty-`repos`/empty-`mappings` default fails +`validateDispatchConfig` with `ErrMissingField` +(`internal/config/dispatch.go:142-150`), and `startDispatch` treats +`ErrMissingField` as the "not configured" friendly-skip — it prints +`skipping dispatch (dispatch.json not configured)` and returns `nil` WITHOUT +ever creating the dispatcher tmux session or starting the polling loop +(`internal/cmd/dispatch.go:1327-1339`). Therefore the goal in issue #73 ("a new +user can dispatch by labeling GitHub issues without visiting the manager") is +NOT met by the current defaults: the dispatcher is not polling after a fresh +setup. To close the gap the ship-default `dispatch.json` must be POPULATED +(non-empty `repos` AND at least one `mapping`, with a non-empty +`trigger_label`). Once populated, `af up` (blanket) would auto-launch the +polling loop with no manual step. Two residual gaps remain even with a populated +default: (a) the dispatcher only auto-starts on the **blanket** `af up` path — +`af up ` (e.g. `af up manager`) is gated out by the `blanket` flag +(`internal/cmd/up.go:92,306,330`), so it would NOT start dispatch; and (b) a +shipped repo string and label-→-agent mapping cannot be known at install time +(the repo is user-specific), so a literal populated default risks pointing at a +wrong/placeholder repo unless install personalizes it. + +## 5-Whys Analysis + +### Why #1: Why is the dispatcher not polling after a fresh setup, even though `start_dispatch:true`? +Because `af up` honors `start_dispatch` and calls `startDispatch` +(`internal/cmd/up.go:330-331`), but `startDispatch` refuses to launch the +session when the dispatch config is unconfigured. + +### Why #2: Why does `startDispatch` refuse to launch with the fresh default config? +Because it loads `dispatch.json` and, when the loader returns `ErrNotFound` OR +`ErrMissingField`, it prints `skipping dispatch (dispatch.json not configured)` +and returns `nil` without launching +(`internal/cmd/dispatch.go:1327-1332`). + +### Why #3: Why does loading the fresh-default `dispatch.json` return `ErrMissingField`? +Because the install default is +`{"repos":[],...,"mappings":[],...}` (`internal/cmd/install.go:145`) and +`validateDispatchConfig` rejects a config with zero repos (and would also reject +zero mappings) with `ErrMissingField` +(`internal/config/dispatch.go:142-150`). `LoadDispatchConfig` runs this +validator and propagates the error (`internal/config/dispatch.go:61-63`). + +### Why #4: Why does the install ship an EMPTY (placeholder) dispatch.json rather than a populated one? +Because the dispatcher target — the GitHub `repos` and the label→agent +`mappings` — is user/repo-specific and cannot be known generically at install +time; the scaffold ships a structurally-valid-but-unfilled template +(`internal/cmd/install.go:145`) intended to be edited (the comment at +`internal/cmd/install.go:140-141` notes the configs are starter templates). The +empty default is precisely the "HIGH-1" case pinned by +`TestStartDispatch_EmptyDefaultConfigFriendlySkip` +(`internal/cmd/startdispatch_test.go:70-84`). + +### Why #5: Why does an empty dispatch config friendly-skip instead of erroring (and is that the right design root)? +By deliberate design: the empty install default must not turn every fresh +`af up` into a noisy error or abort, so `ErrNotFound`/`ErrMissingField` are +folded into a single friendly "not configured" skip +(`internal/cmd/dispatch.go:1328-1332`), distinct from real misconfiguration +(malformed JSON / `ErrInvalidType`) which warns +(`internal/cmd/dispatch.go:1333-1336`; tests at +`internal/cmd/startdispatch_test.go:90-142`). ROOT CAUSE for issue #73: the +skip discriminator keys on config *completeness*, and the shipped default is +intentionally INCOMPLETE — so auto-activation is structurally impossible until +the shipped default is made complete (populated). Populating the config flips +the load result from `ErrMissingField` to a valid `*DispatchConfig`, which makes +`startDispatch` fall through to `launchDispatchSession` and actually start the +polling loop (`internal/cmd/dispatch.go:1338-1339`, +`internal/cmd/dispatch.go:1297-1307`). + +## Evidence Gathered + +| Finding | Source | Evidence | +|---------|--------|----------| +| `start_dispatch` ships `true` in default startup.json | `internal/cmd/install.go:147` | `"startup.json": {... "start_dispatch":true ...}` | +| Default dispatch.json ships EMPTY repos + EMPTY mappings | `internal/cmd/install.go:145` | `"dispatch.json": {"repos":[],"trigger_label":"agentic","notify_on_complete":"manager","mappings":[],"interval_seconds":300,"retry_after_seconds":1800}` | +| `start_dispatch` is read in exactly one place | grep `\.StartDispatch` (non-test) | only `internal/cmd/up.go:330` | +| `af up` calls `startDispatch` when `start_dispatch:true` | `internal/cmd/up.go:330-335` | `if startupCfg.StartDispatch { if dErr := startDispatch(cmd, root, t); ... }` | +| Auto-start is gated to the **blanket** `af up` path only | `internal/cmd/up.go:92,306,330` | `blanket := len(args) == 0`; the dispatch block is inside `if blanket {`; so `af up manager` (positional) skips it | +| `startDispatch` friendly-skips on `ErrNotFound` OR `ErrMissingField` | `internal/cmd/dispatch.go:1327-1332` | `if errors.Is(err, ErrNotFound) || errors.Is(err, ErrMissingField) { print "not configured"; return nil }` | +| Empty `repos` → `ErrMissingField` (also empty mappings, empty trigger_label) | `internal/config/dispatch.go:142-150` | `if len(cfg.Repos)==0 { return ErrMissingField }`; same for trigger_label and mappings | +| `LoadDispatchConfig` runs the validator and propagates its error | `internal/config/dispatch.go:48-64` | reads file, unmarshals, `if err := validateDispatchConfig(&cfg); err != nil { return nil, err }` | +| A VALID config falls through to actually launch the loop | `internal/cmd/dispatch.go:1338-1339` + `1297-1307` | `return launchDispatchSession(...)` → `t.NewSession` + `t.SendKeys(loopCmd)` (the polling loop) | +| Already-running dispatcher is a benign no-op | `internal/cmd/dispatch.go:1322-1325` | `if running { print "already running"; return nil }` | +| Test pins empty-default friendly-skip (no session created) | `internal/cmd/startdispatch_test.go:72-84` | `{"repos":[],...,"mappings":[]}` → no error, asserts NO `NewSession` op | +| Test pins valid-config launch (session + send-keys) | `internal/cmd/startdispatch_test.go:202-217` | populated config → asserts `NewSession` AND `SendKeys` ops | +| The dispatch block is best-effort (never aborts af up) | `internal/cmd/up.go:330-335`, `dispatch.go:1311-1320` | all config outcomes return nil; only a real launch failure sets `allOK=false` | + +## Tests Performed + +| Test | Command | Result | +|------|---------|--------| +| Confirm `start_dispatch` read sites | `grep -rn "\.StartDispatch" internal/ (non-test)` | Exactly one: `internal/cmd/up.go:330` | +| Confirm `startDispatch`/`launchDispatchSession` callers | `grep -rn "launchDispatchSession\|startDispatch(" internal/cmd` | `startDispatch` called only from `up.go:331`; launch only from `startDispatch` (`dispatch.go:1339`) and `runDispatchStart` (`dispatch.go:1279`) | +| Confirm shipped install defaults | Read `internal/cmd/install.go:139-148` | dispatch.json empty repos+mappings; startup.json `start_dispatch:true` | +| Confirm validation thresholds | Read `internal/config/dispatch.go:140-185` | empty repos / empty trigger_label / empty mappings → `ErrMissingField` | +| Run `TestStartDispatch_EmptyDefaultConfigFriendlySkip` + `..._ValidConfigLaunches` | `go test ./internal/cmd -run ...` | BLOCKED: environment denies test-binary exec (`fork/exec ...: permission denied`, noexec build tmp). Behavior verified by reading the tests, which assert exactly the conclusions above. | + +## Conclusion + +**VALIDATED.** The concern is real and is the central blocker for issue #73's +goal. + +- With the CURRENT shipped defaults, the dispatcher does NOT auto-start polling + after a fresh setup. `start_dispatch:true` causes a bare `af up` to call + `startDispatch`, but the empty-`repos`/empty-`mappings` default + (`internal/cmd/install.go:145`) fails validation with `ErrMissingField` + (`internal/config/dispatch.go:142-150`), which `startDispatch` treats as the + friendly "not configured" skip — no session, no polling + (`internal/cmd/dispatch.go:1327-1332`). +- With a POPULATED default (non-empty `repos`, non-empty `mappings`, non-empty + `trigger_label`), the load succeeds, `startDispatch` falls through to + `launchDispatchSession`, and the polling loop IS started by a bare `af up` + with NO manual `af dispatch start` step + (`internal/cmd/dispatch.go:1338-1339`, `1297-1307`; + `internal/cmd/startdispatch_test.go:202-217`). Populating the config does NOT + change the skip-vs-error machinery — it simply moves the config out of the + `ErrMissingField` bucket and into the valid bucket. + +**Remaining gaps between "populated default config exists" and "dispatcher is +actually polling after fresh setup":** +1. **Path gating** — auto-start fires only on the blanket `af up` (no + positional args). `af up manager` / `af up ` is gated out by the + `blanket` flag (`internal/cmd/up.go:92,306,330`) and would NOT start + dispatch. A fresh user who runs `af up manager` instead of `af up` still gets + no dispatcher. +2. **Repo/mapping cannot be generically shipped** — the `repos` value and the + label→agent `mappings` are user/repo-specific. A literal populated default in + `install.go` would point at a placeholder/wrong repo unless the install flow + personalizes `dispatch.json` (e.g., detects the origin repo) or prompts for + it. So "populated default" likely requires install-time population, not just + editing the static literal at `internal/cmd/install.go:145`. +3. **Cross-file agent validity** — a populated mapping must reference agents + that exist in `agents.json`; `ValidateDispatchConfig` + (`internal/config/dispatch.go:93-104`) enforces this at the CLI/write path, + so a shipped mapping must use agents present in the default `agents.json` + (`manager`/`supervisor`). diff --git a/.analysis/73/rootcause_concern_8.md b/.analysis/73/rootcause_concern_8.md new file mode 100644 index 0000000..70ebd8e --- /dev/null +++ b/.analysis/73/rootcause_concern_8.md @@ -0,0 +1,150 @@ +# Concern #8 Investigation: trigger_label / AND-matching / remove_trigger_after_dispatch semantics + +**Investigated by**: Sub-agent +**Date**: 2026-06-28 + +## Verdict: VALIDATED + +## Summary +The proposed default's semantics are mechanically CORRECT and SAFE: the dispatcher +queries GitHub for ONLY items carrying `trigger_label:"agentic"`, then AND-matches each +mapping's `labels` against the item's labels, so a user must apply BOTH `agentic` AND a +mapping label (e.g. `rapid-plan`) for anything to dispatch. `remove_trigger_after_dispatch:true` +removes ONLY the `agentic` trigger label (not the mapping label) after a successful +non-workflow dispatch, which is the safe choice — it converts the dispatch into a +one-shot (no re-dispatch), eliminating the duplicate-dispatch window entirely while the +`.runtime/dispatch-state.json` 24h TTL + `retry_after_seconds` retry path remain as the +fallback when the trigger label persists. All field names/values in the proposed JSON +match the `DispatchConfig`/`DispatchMapping`/`Workflow` struct json tags exactly (verified +by round-tripping the JSON through the real struct). The concern is VALIDATED in the sense +that the design intent is real and worth confirming, and the implementation honors it — +with TWO caveats that belong in the issue: (1) the proposed JSON has two literal syntax +errors (an unterminated string + missing closing bracket on the first mapping's `"rapid-plan`), +and (2) the requirement "a new user could just tag issues to start work" has a genuine UX +trap — tagging ONLY a mapping label (e.g. `rapid-plan`) WITHOUT `agentic` does NOTHING, +because the trigger label is what makes the item appear in the query at all. + +## 5-Whys Analysis + +### Why #1: Does `trigger_label` gate dispatch such that an item needs BOTH `agentic` AND a mapping label? +YES. The dispatcher's GitHub query filters by the trigger label BEFORE any mapping is +considered. `queryGitHubIssues`/`queryGitHubPRs` pass `--label ` to +`gh issue list`/`gh pr list` (`internal/cmd/dispatch.go:178,194,299-305,317-324`). The +returned items are then matched by `matchItemToAgent`, which requires ALL of a mapping's +labels to be present (AND semantics) (`internal/cmd/dispatch.go:335-355`). So an item that +carries `rapid-plan` but NOT `agentic` is never even fetched; an item with `agentic` but no +mapping label is fetched but matches no mapping and is skipped. Effective requirement: +`agentic` AND `rapid-plan`. + +### Why #2: Why AND, and is a single-label mapping affected by AND? +`matchItemToAgent` loops a mapping's `m.Labels` and sets `allMatch=false` on the first +absent label (`dispatch.go:342-349`); it returns the agent only if `allMatch && len(m.Labels) > 0` +(`dispatch.go:350-352`). For the proposed mappings (each has exactly ONE label, e.g. +`["rapid-plan"]`), the AND degenerates to "is `rapid-plan` present?". Combined with the +trigger-label pre-filter, the user-facing rule is exactly "`agentic` + one mapping label". +First-match-wins across mappings (`dispatch.go:350` returns on the first satisfied mapping). + +### Why #3: What exactly does `remove_trigger_after_dispatch:true` remove, and when? +ONLY the trigger label (`agentic`), and ONLY on the NON-workflow path AFTER a successful +`dispatchItem`. `runDispatch` writes the state record, then `if dispatchCfg.RemoveTriggerAfterDispatch +{ removeTriggerLabel(repo, item.Number, dispatchCfg.TriggerLabel, ...) }` +(`dispatch.go:274-278`). `removeTriggerLabel` runs `gh {issue|pr} edit --remove-label ` +— the mapping label is untouched (`dispatch.go:370-380`). A failure to remove is a warning, +not an error (`dispatch.go:276`), so dispatch is not rolled back. For WORKFLOWS the trigger +label is NOT removed per-phase; it is removed only at terminal completion alongside the last +phase label (`terminal()`, `dispatch.go:1077-1106`), and phase labels are swapped add+remove +in one atomic `gh edit` by `editItemLabels` during `advance()` (`dispatch.go:1036-1075,389-402`). + +### Why #4: Is removing the trigger after dispatch SAFE as a default (re-dispatch / retry interaction)? +YES, and it is the safer of the two options. With removal ON, a dispatched item drops out of +the next query (its `agentic` label is gone), so it cannot be double-dispatched and cannot be +re-tried via the query — it is one-shot. With removal OFF (the field absent, as in today's +install default), the item KEEPS `agentic` and re-dispatch is governed by the +`.runtime/dispatch-state.json` record: a re-dispatch only happens if the agent is idle AND +`time.Since(entry.DispatchedAt) >= retry_after_seconds` (default 1800s) +(`dispatch.go:232-245`), and the record is pruned after 24h (`pruneDispatchState`, +`dispatch.go:1225-1235`). So removal=true trades away the auto-retry-on-failure path for +guaranteed no-duplicate; the explicit `retry_after_seconds:1800` in the proposed default is +effectively dead for non-workflow items once the trigger is gone, but it still governs the +workflow re-sling path (`resling`, `dispatch.go:1120-1126`). Net: safe; the only behavioral +nuance is that a dispatched-but-failed agent run will NOT be auto-retried by re-querying — +the operator/manager (notified via `notify_on_complete`) re-tags. This is a defensible +default for a "fire once, notify a human" UX. + +### Why #5: Are the proposed JSON field names/values exactly what the loader expects? +YES for field names. Round-tripping the proposed JSON (with the two literal syntax errors +corrected) through the real `DispatchConfig` struct parses cleanly: `trigger_label`, +`notify_on_complete`, `interval_seconds`, `retry_after_seconds`, +`remove_trigger_after_dispatch`, `mappings[].labels`, `mappings[].source`, `mappings[].agent`, +`workflows[].label`, `workflows[].phases` ALL match the json tags at +`internal/config/dispatch.go:19-45`. The loader uses plain `json.Unmarshal` with NO +`DisallowUnknownFields` (`dispatch.go:58`), so a stray/misspelled field would be silently +ignored rather than erroring — worth knowing, but the proposed fields are all correct. The +four referenced agents (`rapid-soldesign-plan`, `rapid-implement`, `ultra-review`, +`rapid-increment`) all have formulas in `.agentfactory/store/formulas/`, so the cross-file +`ValidateDispatchConfig` agent-existence check passes given matching `agents.json` entries. + +## Evidence Gathered +| Finding | Source | Evidence | +|---------|--------|----------| +| Query fetches ONLY trigger-labeled items | `internal/cmd/dispatch.go:178,194` then `:299-305,317-324` | `gh issue/pr list --label --state open` | +| Mapping labels AND-matched | `internal/cmd/dispatch.go:337-355` | loop `m.Labels`, `allMatch=false` on first absent; return agent iff `allMatch && len(m.Labels)>0` | +| User needs BOTH agentic + mapping label | composition of query filter + AND match | item w/o `agentic` never fetched; item w/ `agentic` but no mapping label → skipped (`:223-226`) | +| remove_trigger removes ONLY trigger label, post-dispatch, non-workflow | `internal/cmd/dispatch.go:274-278`, `:370-380` | `removeTriggerLabel(... dispatchCfg.TriggerLabel ...)`; `gh edit --remove-label ` | +| remove failure is warn-not-error | `internal/cmd/dispatch.go:276` | `warning: failed to remove trigger label ...` | +| Non-workflow retry gated by state record + retry_after_seconds (24h prune) | `internal/cmd/dispatch.go:232-245`, `:1225-1235` | retry only if idle AND `time.Since(DispatchedAt) >= retryAfter` | +| Workflow removes trigger only at terminal, swaps phase labels atomically | `internal/cmd/dispatch.go:1059,1097`, `:389-402` | `editItemLabels(... add[next] remove[phase] ...)`; terminal removes `[phase, triggerLabel]` | +| struct json tags match proposed JSON exactly | `internal/config/dispatch.go:19-45` | all 11 proposed keys map to tags; round-trip parse OK | +| Loader tolerates unknown fields (no DisallowUnknownFields) | `internal/config/dispatch.go:58` | plain `json.Unmarshal(data, &cfg)` | +| trigger_label is REQUIRED; mappings REQUIRED; defaults filled | `internal/config/dispatch.go:142-184` | interval→300, retry→1800, notify→"manager" defaults | +| CURRENT install default differs from proposed | `internal/cmd/install.go:145` | `{"repos":[],"trigger_label":"agentic","notify_on_complete":"manager","mappings":[],"interval_seconds":300,"retry_after_seconds":1800}` — empty repos/mappings, NO remove_trigger, NO workflows | +| Empty install default is intentionally "not configured" | `internal/cmd/dispatch.go:1327-1332` | empty mappings → `ErrMissingField` → "skipping dispatch (dispatch.json not configured)" | +| Proposed agents have formulas | `.agentfactory/store/formulas/` | rapid-soldesign-plan, rapid-implement, ultra-review, rapid-increment all present | +| Proposed JSON has literal syntax errors | issue #73 body | `"labels": ["rapid-plan` — unterminated string + missing `]` | + +## Tests Performed +| Test | Command | Result | +|------|---------|--------| +| Parse proposed JSON (errors fixed) vs real struct | `go run` of struct round-trip inside worktree | Parsed OK; trigger=agentic remove_trigger=true interval=300 retry=1800; 4 mappings, 1 workflow; all fields match | +| AND-matching semantics | `go test ./internal/cmd -run TestMatchItemToAgent` | PASS (AllLabelsMatch, PartialMatchFails, SingleLabelBackwardCompat, FirstMatchWins, NoMatch, MultiLabel_AND) | +| Workflow cursor / phase swap | `go test ./internal/cmd -run TestWorkflowCursor` | PASS (bootstrap, single, first, ambiguous cases) | +| Config validation | `go test ./internal/config -run 'Dispatch\|Workflow'` | ok (PASS) | +| Full cmd suite | `go test ./internal/cmd` | 2 FAILs, both environmental ("insufficient disk to create worktree: 1GB available") in sling worktree tests — UNRELATED to dispatch matching/trigger logic | + +## Conclusion +**Verdict: VALIDATED.** The default semantics are correct and safe as implemented: + +1. **trigger_label gating + AND-match is correct.** The dispatcher fetches ONLY `agentic`-labeled + items (query-level `--label agentic`) and then AND-matches each mapping's `labels`. With + single-label mappings this means the user applies `agentic` + one mapping label + (`dispatch.go:178/299/337-355`). This is the intended "both labels required" behavior and is + test-covered. + +2. **remove_trigger_after_dispatch:true is safe** and is the more conservative default: it + removes ONLY the `agentic` trigger (not the mapping label), makes the dispatch one-shot + (no double-dispatch, no re-query retry), and degrades gracefully (remove failure is a + warning). The `retry_after_seconds:1800` value is largely inert for non-workflow items once + the trigger is removed, but it is harmless and still governs the workflow re-sling ceiling + path (`dispatch.go:1120-1126`). + +3. **No schema field-name or type mismatches.** Every key/value in the proposed JSON maps + 1:1 to the `DispatchConfig`/`DispatchMapping`/`Workflow` json tags + (`internal/config/dispatch.go:19-45`) and round-trips cleanly. NOTE: the loader has no + `DisallowUnknownFields`, so a future typo would be silently dropped — not a defect in this + proposal, but a robustness gap worth a one-line callout. + +4. **UX caveats for the issue (recommend documenting, not blocking):** + - The proposed JSON literal in the issue is malformed: `"labels": ["rapid-plan` is missing + the closing quote and bracket. Must be fixed to `"labels": ["rapid-plan"]` before it + parses. + - The requirement narrative ("a new user could just tag issues to start work") has a real + trap: tagging ONLY a mapping label (e.g. `rapid-plan`) WITHOUT `agentic` does NOTHING, + because the trigger label is the query gate. New users will likely expect a single + `rapid-plan` tag to suffice. This should be called out in docs/onboarding (the two-label + requirement is correct and intentional, but non-obvious). + - Today's shipped install default (`install.go:145`) is the EMPTY/"not configured" form + (empty repos+mappings, no `remove_trigger_after_dispatch`, no workflows). Adopting the + proposed richer default requires changing `install.go` AND making the literal + repo-name-substituted at install time, as the issue itself notes. The proposed default + is schema-valid and will load and validate cleanly once `repos`/`mappings` are populated + and the JSON syntax is fixed. diff --git a/.analysis/73/rootcause_concern_9.md b/.analysis/73/rootcause_concern_9.md new file mode 100644 index 0000000..dec4a3e --- /dev/null +++ b/.analysis/73/rootcause_concern_9.md @@ -0,0 +1,130 @@ +# Concern #9 Investigation: Systemic gaps blocking end-to-end autonomy to a pushed PR + +**Investigated by**: Sub-agent +**Date**: 2026-06-28 + +## Verdict: VALIDATED + +## Summary +Fixing `dispatch.json` ALONE does not satisfy issue #73's acceptance criterion. The +dispatchable formulas (`rapid-implement`, `rapid-soldesign-plan`) reach a pushed PR only +by invoking `git push -u origin ...` and `gh pr create` in their tail steps, both of which +hard-depend on infrastructure that NO `af` command provisions: (a) a configured `origin` +remote pointing at GitHub, and (b) authenticated `gh` / git push credentials. On the +documented fresh-container path these are provisioned at the CONTAINER layer by +`quickdocker.sh` (`gh auth login --with-token` + `gh auth setup-git`, lines 557-567), not +by `af install --init`. So a fresh repo created only via `af install --init` (no GitHub +remote, no `gh` auth) cannot reach a pushed PR autonomously — `detectDefaultBranch` degrades +to the "main" sentinel with a warning (`sling.go:506`), and the final push/PR steps fail. +Critically, there is NO `doctor` command in the codebase at all — "doctor --fix" exists ONLY +as a negative guard in `e2e_sling_test.go:67-69`, so acceptance's "no doctor --fix" clause is +about asserting that property in tests, not about an existing tool. The systemic gaps to NAME +are: (1) origin remote provisioning + (2) `gh`/push auth provisioning happen outside `af`, and +(3) the default `dispatch.json` shipped by `install.go:145` is empty (no repos/mappings). + +## 5-Whys Analysis + +### Why #1: Why might `af sling --agent "task"` fail to reach a pushed PR on a fresh setup? +Because the formula's final steps run `git push -u origin ` and `gh pr create` +(`rapid-implement.formula.toml:803, :881`; `rapid-soldesign-plan.formula.toml:496, :527`), +which require an `origin` remote and authenticated push/`gh` credentials. + +### Why #2: Why would those credentials/remote be missing on a fresh setup? +Because `af install --init` provisions NO git remote and NO `gh` auth. Searching install.go +for `git remote`/`origin`/`gh auth` returns nothing relevant (only `.git/info/exclude` hook +plumbing). The default branch detection chain (`detect_default_branch.go:52-73`) requires an +`origin` remote in all three methods (local `origin/HEAD`, `ls-remote origin`, `gh repo view`). + +### Why #3: Why is the remote/auth not provisioned by `af`? +By design, these live at the container/setup layer. `quickdocker.sh` injects the PAT via +`gh auth login --with-token` (line 557) and wires git credentials via `gh auth setup-git` +(line 567), and derives the repo from `git remote get-url origin` of the host clone (line 41). +So the documented fresh-container path DOES have auth+remote — but a bare `af install --init` +in an arbitrary repo does not, and nothing in `af` repairs that. + +### Why #4: Why doesn't `dispatch.json` alone close the gap? +The default `dispatch.json` written by `install.go:145` ships with empty `repos` and empty +`mappings`, so label-dispatch matches nothing (the existing rootcause_analysis.md confirms +this, concerns #1-#8). Even a fully populated `dispatch.json` only routes a labeled issue to +an agent+formula; it does not create an `origin` remote or `gh` auth. The push/PR steps still +depend on the container-layer provisioning. So `dispatch.json` is necessary but not sufficient. + +### Why #5: Why does the "no doctor --fix" clause matter, and is any run dependent on doctor? +There is NO `doctor` command in this codebase (no `cmdDoctor`, no `"doctor"` registration; grep +of internal/cmd for `doctor` outside tests returns nothing). "doctor --fix" appears ONLY as a +negative assertion in `e2e_sling_test.go:67-69` ("sling output must not contain 'doctor --fix'"). +Therefore NO agent run is dependent on `doctor --fix`; the acceptance clause is a property to +hold (and is already test-guarded), not a dependency to remove. This is an INVALIDATED sub-claim +within an otherwise VALIDATED concern. + +## Evidence Gathered +| Finding | Source | Evidence | +|---------|--------|----------| +| Final step pushes branch & opens PR via `gh pr create` | `internal/cmd/install_formulas/rapid-implement.formula.toml:803, :881` | `git push -u origin $(git branch --show-current)` then `gh pr create --title ... --body ...` | +| rapid-soldesign-plan pushes & creates PR | `internal/cmd/install_formulas/rapid-soldesign-plan.formula.toml:496, :527` | `git push origin HEAD`; `PR_URL=$(gh pr create --title ... --body ...)`; aborts/escalates if PR_URL not a URL (`:536-540`) | +| Formula HARD-requires `gh` auth, fails fast if missing | `internal/cmd/install_formulas/rapid-soldesign-plan.formula.toml:146-149` | `gh auth status \|\| { echo "ERROR: gh CLI not authenticated. Run 'gh auth login' first."; exit 1; }` — explicit human-action error | +| Worktree branch is LOCAL with NO upstream | `internal/worktree/worktree.go:500` | `git worktree add --quiet -b ` creates a fresh local branch; no `--set-upstream`, hence formulas use `push -u origin` | +| NO `doctor` command exists | grep `internal/cmd/*.go` (non-test) for `doctor`/`cmdDoctor`/`--fix` | Zero hits; only reference is the negative guard in `e2e_sling_test.go:67-69` | +| "doctor --fix" is a test guard, not a tool | `internal/cmd/e2e_sling_test.go:67-69` | `if strings.Contains(slingOut, "doctor --fix") { t.Fatalf(...) }` | +| No agent run depends on doctor --fix | (consequence of above) | Nothing to remove; clause is a property assertion | +| `af install --init` provisions NO git remote / gh auth | `internal/cmd/install.go` | grep for `git remote`/`origin`/`gh auth`/`remote add` returns only `.git/info/exclude` hook plumbing; no remote/auth setup | +| Default `dispatch.json` is empty (no repos/mappings) | `internal/cmd/install.go:145` | `{"repos":[],...,"mappings":[],...}` — label-dispatch matches nothing out of the box | +| Default-branch detection requires an `origin` remote | `internal/cmd/detect_default_branch.go:52-73` | All 3 methods key off `origin` (symbolic-ref `refs/remotes/origin/HEAD`, `ls-remote --symref origin`, `gh repo view`); returns "" otherwise | +| sling fails LOUD then baits "main" if no remote | `internal/cmd/sling.go:504-507` | `warning: could not detect repository default branch (origin/HEAD unset, no GitHub remote); re-run with --var default_branch=` then sets sentinel `main` | +| Container path DOES provision auth + git creds | `quickdocker.sh:557, :567` | `gh auth login --with-token`; `gh auth setup-git` (configures git credential helper) | +| Container path derives repo from host remote | `quickdocker.sh:41` | `AF_REMOTE="$(git -C "$SCRIPT_DIR" remote get-url origin ...)"` — provisioning knows the repo, `install.go` does not | +| Dispatcher itself gates on gh auth | `internal/cmd/dispatch.go:150-153, :292-295` | `checkGHAuth()` runs `gh auth status`; dispatch errors "GitHub CLI not authenticated" if it fails | + +## Tests Performed +| Test | Command | Result | +|------|---------|--------| +| Is `doctor` a real command? | `grep -rn '"doctor"\|cmdDoctor\|Doctor\|--fix' internal/cmd/ (non-test)` | No hits — `doctor` does not exist as a command | +| Where does "doctor --fix" string live? | `grep -rn 'doctor' internal/cmd/` | Only `e2e_sling_test.go:67-68` (a negative guard) | +| Do formulas push + open PR in tail steps? | Read `rapid-implement.formula.toml` :785-911, `rapid-soldesign-plan.formula.toml` :485-541 | Yes: `git push -u origin` / `git push origin HEAD` + `gh pr create` | +| Does any formula require `gh` auth / a remote? | Read `rapid-soldesign-plan.formula.toml:146-149` | Yes — explicit `gh auth status \|\| exit 1` | +| Does `af install --init` set up a remote / gh auth? | grep `install.go` for remote/origin/gh auth | No | +| Does default-branch detection need a remote? | Read `detect_default_branch.go:52-73` | Yes — all 3 methods require `origin` | +| Does the fresh-container path provision auth? | Read `quickdocker.sh:557, :567` | Yes — `gh auth login --with-token` + `gh auth setup-git` | +| Is default `dispatch.json` populated? | Read `install.go:145` | No — empty repos + mappings | + +## Conclusion + +**Verdict: VALIDATED.** Real systemic gaps exist beyond `dispatch.json`. Fixing +`dispatch.json` alone is necessary but NOT sufficient for issue #73's acceptance criterion. + +Named systemic gaps (with scope tags): + +1. **`origin` GitHub remote provisioning** — `[likely OUT-OF-SCOPE for the immediate + dispatch.json fix, but MUST be named]`. The push/PR tail steps + (`rapid-implement.formula.toml:803/:881`, `rapid-soldesign-plan.formula.toml:496/:527`) + and default-branch detection (`detect_default_branch.go:52-73`) all require an `origin` + remote that `af install --init` never creates. Provisioned only at the container layer + (`quickdocker.sh:41`). A fresh repo without a GitHub remote cannot reach a pushed PR. + +2. **`gh` / git-push authentication provisioning** — `[OUT-OF-SCOPE for dispatch.json, but + MUST be named]`. `gh pr create` and `git push` need authenticated credentials; one formula + even fails fast on `gh auth status` (`rapid-soldesign-plan.formula.toml:146-149`), and the + dispatcher gates on `checkGHAuth` (`dispatch.go:150-153`). Provisioned only by + `quickdocker.sh:557/:567` (`gh auth login --with-token` + `gh auth setup-git`), NOT by `af`. + On a bare `af install --init` setup with no auth, autonomy to a PR is impossible. + +3. **Empty default `dispatch.json`** — `[IN-SCOPE — this is the primary issue #73 fix]`. + `install.go:145` ships empty `repos`/`mappings`, so label-dispatch matches nothing. (Covered + in depth by concerns #1-#8; named here for completeness of the end-to-end chain.) + +4. **No declared `origin` upstream on the worktree branch** — `[IN-SCOPE as a documentation/ + formula concern; LOW severity]`. `worktree.go:500` creates a local branch with no upstream; + the formulas correctly compensate with `push -u origin`, so this is handled, but it MEANS the + push step strictly depends on gaps #1 and #2 — confirming the chain. + +Sub-claim INVALIDATED: there is NO ongoing `doctor --fix` dependency to remove — `doctor` is +not a command in this codebase; "doctor --fix" exists only as a negative test guard +(`e2e_sling_test.go:67-69`). The acceptance clause is already a test-enforced property. + +**Needs review (uncertain):** Whether issue #73 intends gaps #1/#2 to be solved within this +change (e.g., by extending `quickstart.sh`/`quickdocker.sh` and/or `install --init` to bake a +remote/auth check), or whether the fresh-container path already satisfies them and only +`dispatch.json` population is in scope. The codebase shows the container path provisions +auth+remote, so on the INTENDED (quickdocker) fresh setup the only gap is the empty +`dispatch.json`; on a bare `af install --init`-only setup, gaps #1/#2 also bite. This scope +boundary should be confirmed with the issue author / Supervisor. From c21be0f355f9719a85c9adddcba0b322ca7f7ce3 Mon Sep 17 00:00:00 2001 From: agentfactory Date: Sun, 28 Jun 2026 17:22:52 +0000 Subject: [PATCH 2/9] design: design-v7 artifacts for 73 - source.md (verbatim requirements) - codebase-snapshot.md (verified ground truth for sub-agents) - verification.md (Gate A: forced-enumeration AC/constraint table) - dimension files (api/data/ux/scale/security/integration) - audit.md, conflicts.md, dependencies.md (dimension analysis) - elevation_assessment.md (architecture elevation verdict) - six_sigma_gaps.md (six-sigma gap analysis) - verification-report.md (fidelity verification of all codebase claims) - synthesis-checklist.md (Gate B: pre-synthesis re-grounding) - design-doc.md (AC Traceability, elevation verdict, six-sigma caveats) Co-Authored-By: Claude Opus 4.8 Co-authored-by: agentfactory-cli <293373236+agentfactory-cli@users.noreply.github.com> --- .designs/73/api.md | 193 +++++++++++++ .designs/73/audit.md | 79 ++++++ .designs/73/codebase-snapshot.md | 238 ++++++++++++++++ .designs/73/conflicts.md | 250 +++++++++++++++++ .designs/73/data.md | 189 +++++++++++++ .designs/73/dependencies.md | 105 +++++++ .designs/73/design-doc.md | 319 ++++++++++++++++++++++ .designs/73/design-refinement-progress.md | 27 ++ .designs/73/elevation_assessment.md | 119 ++++++++ .designs/73/integration.md | 221 +++++++++++++++ .designs/73/problem-summary-73.md | 104 +++++++ .designs/73/scale.md | 112 ++++++++ .designs/73/security.md | 144 ++++++++++ .designs/73/six_sigma_gaps.md | 88 ++++++ .designs/73/source.md | 159 +++++++++++ .designs/73/synthesis-checklist.md | 56 ++++ .designs/73/ux.md | 147 ++++++++++ .designs/73/verification-report.md | 59 ++++ .designs/73/verification.md | 35 +++ 19 files changed, 2644 insertions(+) create mode 100644 .designs/73/api.md create mode 100644 .designs/73/audit.md create mode 100644 .designs/73/codebase-snapshot.md create mode 100644 .designs/73/conflicts.md create mode 100644 .designs/73/data.md create mode 100644 .designs/73/dependencies.md create mode 100644 .designs/73/design-doc.md create mode 100644 .designs/73/design-refinement-progress.md create mode 100644 .designs/73/elevation_assessment.md create mode 100644 .designs/73/integration.md create mode 100644 .designs/73/problem-summary-73.md create mode 100644 .designs/73/scale.md create mode 100644 .designs/73/security.md create mode 100644 .designs/73/six_sigma_gaps.md create mode 100644 .designs/73/source.md create mode 100644 .designs/73/synthesis-checklist.md create mode 100644 .designs/73/ux.md create mode 100644 .designs/73/verification-report.md create mode 100644 .designs/73/verification.md diff --git a/.designs/73/api.md b/.designs/73/api.md new file mode 100644 index 0000000..b0b8ef8 --- /dev/null +++ b/.designs/73/api.md @@ -0,0 +1,193 @@ +# D1 — API & Interface Design (api.md) + +Owner of (verification.md): C-1 (with Integration, Data), C-3 (with Integration, Security). +This dimension governs: the Go function surface for the baked-in default, the +repo-discovery interface, and any CLI surface/error messages introduced. + +The design adds NO new user-facing CLI subcommand or flag. `af install --init` +already exists (`internal/cmd/install.go:88-90`, verified) and is the sole entry +point; the work is internal-API + behavior. This is itself an API decision (see +Option A1.3) and is constraint-driven by C-2 (bootstrap at first-create, not a new +command). + +--- + +## A1. Default-config interface (how the baked-in default is expressed in Go) + +### Option A1.1 — `DefaultDispatchConfigJSON(repo string) string` in `internal/config` — RECOMMENDED + +A package-level function in `internal/config` (mirroring `DefaultFactoryConfigJSON()` +at `internal/config/config.go:111`, verified) that builds a `DispatchConfig` value +from in-code constants, sets `Repos` to `[]string{repo}` (or `[]string{}` if repo +is ""), `json.Marshal`s it, and returns the string. `runInstallInit` calls it for +the `dispatch.json` starter entry (replacing the inline literal at +`internal/cmd/install.go:145`, verified). + +Signature: +```go +func DefaultDispatchConfigJSON(repo string) string +``` + +- Trade-offs: Exactly mirrors the established single-source pattern + (`DefaultFactoryConfigJSON`, config.go:111). The default is built from the + `DispatchConfig` struct (dispatch.go:18-27, verified), so it cannot drift from + the schema — field renames break compilation. Takes the discovered repo as a + parameter, keeping discovery (an install-time, git-touching concern) OUT of the + config package (which has no git dependency today). +- Reversibility: Easy (delete the func, restore the literal). +- Constraints: satisfies C-1 (baked into the tool as Go code), supports C-3 (repo + is a parameter populated by the caller), aligns with ADR-008 (a Go func is + inherently single-source — no embed-vs-source drift; codebase-snapshot Decision + History). Recommended. + +### Option A1.2 — Inline literal string in `runInstallInit` (status-quo shape extended) — REJECTED + +Keep the current inline-literal approach (`internal/cmd/install.go:145`, verified) +but expand it to the non-empty default with the repo string interpolated via +`fmt.Sprintf`. + +- REJECTED: reverses [Decision History: issue #371 Gap-6 single-source pattern]. + codebase-snapshot.md §6 records that `dispatch.json` is "the ONLY starter config + still an inline literal; `factory.json` uses `DefaultFactoryConfigJSON()` + (install.go:142) — established single-source pattern (issue #371 Gap-6)." A + hand-typed literal with a non-trivial mappings/workflows body is precisely the + drift risk that pattern exists to remove (a `fmt.Sprintf` body cannot be schema- + checked by the compiler). Extending the literal entrenches the one exception the + prior change set out to eliminate. + +### Option A1.3 — New `af dispatch init` / `af config dispatch default` subcommand — REJECTED + +Add a dedicated CLI command the operator runs after install to populate the default. + +- REJECTED: fails [AC-2], violates [C-2]. C-2 (verification.md) requires the + default to be written "when dispatch.json is FIRST created during init — not + lazily, not later." A separate command is a later, manual step; it reintroduces + the human interaction AC-2 ("without ever needing to visit the manager") and C-5 + ("without human interaction as an ongoing dependency") forbid. Also adds CLI + surface for no behavioral gain over A1.1. + +--- + +## A2. Repo-discovery interface (how the actual org/repository reaches the default) + +C-3 (verification.md) requires `repos` populated with the ACTUAL org/repository +"discovered during install, not a literal placeholder." codebase-snapshot.md §6 +confirms `runInstallInit` "takes NO repo argument and does NO git-remote lookup." +So this is net-new code. ADR-014 (Decision History) forbids interactive prompting +in `cmd/`/`internal/` Go paths: discovery MUST be non-interactive (git remote) or +fail-loud-with-flag — never a prompt. + +The dispatcher consumes `repos[]` as `owner/name` strings: it passes each entry +directly to `gh --repo ` (`internal/cmd/dispatch.go:300`, verified) and +splits it via `strings.Cut(repo, "/")` (dispatch.go:537, verified). So discovery +MUST yield the `owner/name` form, matching the proposed JSON `"org/repository"`. + +### Option A2.1 — `gh repo view --json nameWithOwner` at init, best-effort — RECOMMENDED + +In `runInstallInit`, before building the dispatch default, shell out to +`gh repo view --json nameWithOwner -q .nameWithOwner` in the install CWD. `gh` is a +verified hard prerequisite (quickstart.sh `check_gh`, lines 128-138, verified, and +the dispatcher already depends on `gh` for every query). `gh repo view` resolves +the `owner/repo` from the local git remote AND the GitHub API, returning canonical +`nameWithOwner` directly in the dispatcher's required form. + +- Trade-offs: Returns the canonical `owner/name` with no SSH/HTTPS URL parsing. + Requires `gh auth` at install time; the dispatcher already requires `gh auth` + (codebase-snapshot §4: "check gh auth"), so this adds no NEW operational + dependency for the autonomous path. On failure (no remote / not authed) fall + back to A2.3 (empty repos) rather than aborting install. +- Reversibility: Easy (discovery is one call site in runInstallInit). +- Constraints: satisfies C-3, complies with ADR-014 (non-interactive; no prompt), + ADR-017 (read-only git/gh access — no writes outside af dirs). Recommended. + +### Option A2.2 — Parse `git remote get-url origin` and normalize to `owner/name` — RECOMMENDED (fallback / no-gh-auth) + +Run `git remote get-url origin`, then normalize the result +(`git@github.com:org/repo.git`, `https://github.com/org/repo.git`, +`https://github.com/org/repo`) to `owner/name` by stripping scheme/host/`.git`. +quickstart.sh `cd`s into the repo before `af install --init` (quickstart.sh:428, +442, verified), so a git remote IS present in CWD at that moment +(codebase-snapshot §6 confirms this). + +- Trade-offs: Works WITHOUT `gh auth` (pure git), so it covers the window before + `claude`/`gh` auth in the setup flow (source.md setup steps 1-3 precede manager + start). Cost: URL-normalization is hand-rolled string handling — a real but + bounded surface (3 URL shapes). Best used as the FALLBACK when A2.1's `gh` call + fails, or as the PRIMARY if we prefer zero gh-auth coupling at init. +- Reversibility: Easy. +- Constraints: satisfies C-3, complies with ADR-014 (non-interactive), ADR-017 + (read-only). Recommended as the layered fallback to A2.1. + +### Option A2.3 — Leave repos empty; require operator to fill it later — REJECTED + +Ship the non-empty mappings/workflows default but keep `repos: []`, expecting the +operator to add the repo by hand or via `af config dispatch set`. + +- REJECTED: fails [AC-3], violates [C-3]. C-3 explicitly forbids leaving `repos` + as a placeholder; AC-3 requires the actual repo-name in `repos` at first create. + Additionally, `validateDispatchConfig` REQUIRES `len(cfg.Repos) > 0` + (`internal/config/dispatch.go:142-144`, verified): a default with empty repos but + non-empty mappings would FAIL to load via `LoadDispatchConfig` (dispatch.go:48-65, + verified), breaking the dispatcher on a fresh repo. (Retained ONLY as the + graceful-degradation target when discovery genuinely fails — see A2.1 fallback — + in which case mappings must ALSO be empty to keep the file loadable; that + degraded shape is the status quo, acceptable only as a non-default failure path.) + +--- + +## A3. Error-message / observability surface at discovery failure + +### Option A3.1 — Warn-don't-abort: discovery failure prints a warning, writes degraded-but-loadable default — RECOMMENDED + +If both A2.1 and A2.2 fail (no remote, no auth), `runInstallInit` prints a +structured warning to stderr naming the manual remedy +(`af config dispatch set`, verified at config_set.go:24-32) and writes the +status-quo loadable default (`repos:[]`, `mappings:[]`) so install still succeeds. + +- Trade-offs: install never hard-fails on a missing remote (matches install's + current robustness posture and quickstart's "warn-don't-abort" idiom, e.g. + webui launch guard quickstart.sh:690-697, verified). The autonomous path is + simply unavailable until the operator names the repo — an honest degradation. +- Reversibility: Easy. +- Constraints: complies with ADR-014 (a structured stderr message naming the flag + is exactly the ADR-014 "fail loud with a structured error naming the exact flag" + shape — here non-fatal because install has more to do). Recommended. + +### Option A3.2 — Hard-fail install when repo cannot be discovered — REJECTED + +Abort `af install --init` with a non-zero exit if no git remote is found. + +- REJECTED: violates [C-5] (would block the no-human-interaction bootstrap when a + repo has no remote yet, e.g. a brand-new local repo), and regresses the existing + robustness of `af install --init`, which today writes a loadable default + unconditionally (install.go:145, verified). A1/A2 already cover the happy path; + hardening install into a hard-fail on a missing remote trades a recoverable + degradation for a setup-blocking error. + +--- + +## Reversibility (this dimension): Easy + +All changes are additive and localized to `runInstallInit` plus one new +`internal/config` function. No schema change, no migration, no CLI surface change. + +## Dependencies produced + +- PROVIDES to **Data (D2)**: the `DefaultDispatchConfigJSON(repo)` function shape — + Data owns WHAT mappings/workflows the default contains; API owns the function + signature and the repo-injection point. +- PROVIDES to **Integration (D6)**: the single call site in `runInstallInit` + (install.go starterConfigs map, :139-148, verified) and the discovery call + ordering (discover repo → build default → write). +- REQUIRES from **Security (D5)**: validation of the discovered repo string + (owner/name shape; reject shell-meta) before it is written into the config. +- REQUIRES from **Data (D2)**: the exact field set the default must populate so the + marshaled output passes `validateDispatchConfig` (dispatch.go:141-185). + +## Risks identified + +| Risk | Severity | Mitigation | +|------|----------|------------| +| `gh repo view` requires `gh auth`, which may not be present at the install moment (auth happens at setup step 2, source.md) | Medium | Layer A2.2 (`git remote`, no auth needed) as the primary or fallback; A3.1 degrades gracefully | +| Discovered repo string contains an unexpected URL form, yielding a malformed `owner/name` | Medium | Security (D5) validates owner/name shape; on validation failure fall to A3.1 degraded default | +| Adding a `gh`/`git` shell-out to `runInstallInit` introduces a new failure mode in a previously git-agnostic function | Low | Best-effort + warn-don't-abort (A3.1); install proceeds regardless | diff --git a/.designs/73/audit.md b/.designs/73/audit.md new file mode 100644 index 0000000..edb98d5 --- /dev/null +++ b/.designs/73/audit.md @@ -0,0 +1,79 @@ +# audit.md — Pre-Synthesis Audit (re-grounded against source.md) + +Re-read source.md top to bottom before producing these tables. The constraint +set (C-1..C-6) and AC set (AC-1..AC-6) below are re-copied verbatim from source.md, +NOT from verification.md. + +For the constraint columns, this audit tracks the THREE constraints with the +broadest cross-dimension reach as C1/C2/C3 columns, since 6 constraints don't fit a +compact table — but every constraint is covered in the prose and Table C: +- **C1 column = C-1** "baked-in default … with agentfactory" (source.md:45) +- **C2 column = C-2** "bootstrapped when … first created during initial setup" (source.md:50) +- **C3 column = C-3** "we know the repository-name and … include that appropriately" (source.md:55-57) +- (C-4 codebase-truth, C-5 no-doctor/no-human, C-6 agents-must-exist are audited in prose + Table C rows.) + +--- + +## Table A — Constraint Audit + +| Dimension | Recommendation | C1 (C-1 baked-in) | C2 (C-2 first-create) | C3 (C-3 actual repo) | Status | +|-----------|----------------|-------------------|-----------------------|----------------------|--------| +| API (D1) | A1.1 `DefaultDispatchConfigJSON(repo)` + A2.1/A2.2 discovery + A3.1 warn-don't-abort | PASS — Go func, baked into tool | PASS — called from runInstallInit's first-create write (install.go:152) | PASS — repo is a parameter populated by discovery | PASS | +| Data (D2) | D2.2-A (4 mappings + workflow, intent-corrected) + C-6 rider; D2.3-A write-once | PASS — default value of an existing baked file | PASS — write-if-absent guard = "first created" | PASS — `repos:[""]` | PASS (C-6 deferred to D6) | +| UX (D3) | U1.1 zero-touch + U1.2 banner + U2.1 actionable errors | PASS — no hand-authoring needed | PASS — present right after init | PASS — banner echoes the discovered repo | PASS | +| Scale (D4) | S1.1 single discovery call + S2.1 inherit cadence + S3.1 one repo | PASS — no extra infra | PASS — discovery once at first-create | PASS — the one discovered repo | PASS | +| Security (D5) | SEC1.1 allowlist repo + SEC2.1 provision (+SEC2.2 tolerate) + SEC3.1 write-if-absent | PASS | PASS — write-if-absent preserves C-2 + ADR-017 | PASS — validates owner/name before write | PASS | +| Integration (D6) | I1.1 build in config + I2.1 discover in init + I3.1 provision (+I3.2 tolerate) + I4.1 route to specialists | PASS | PASS — runInstallInit starterConfigs map | PASS — discovery feeds repos | PASS (resolves C-6) | + +C-4 (codebase-truth): PASS for all dimensions — every cited path/line/function is +verified against the code this session (install.go, dispatch.go, config.go, +paths.go, config_set.go, quickstart.sh, agent-gen-all.sh) or in codebase-snapshot.md. +C-5 (no doctor / no human): PASS — no option depends on `doctor --fix`; the only +human touchpoints are genuine failure paths (degraded default), never the happy +path; SEC2.3 (rely on doctor) is REJECTED. C-6 (referenced agents must exist): the +crux; RESOLVED by D6/I3.1 (provision via `af install --agents` in quickstart) + +I3.2 (dispatcher skip-and-warn). All Table A rows PASS. + +--- + +## Table B — AC Coverage Audit (clause-by-clause) + +| AC-id | Verbatim text (re-copied from source.md) | Owner dimension(s) | Option chosen | Clause-by-clause: does it satisfy EVERY clause? | Evidence | +|-------|------------------------------------------|--------------------|---------------|--------------------------------------------------|----------| +| AC-1 | "We need to update dispatch.json to include a baked-in default for dispatch with agentfactory" | Data, Integration | D2.2-A + I1.1 | **Clause 1** "update dispatch.json" → YES (runInstallInit starter write, install.go:145→DefaultDispatchConfigJSON). **Clause 2** "baked-in default" → YES (Go function, single-source). **Clause 3** "for dispatch with agentfactory" → YES (mappings/workflows wire the dispatch system). | `DefaultDispatchConfigJSON` mirrors `DefaultFactoryConfigJSON` (config.go:111, verified); replaces literal at install.go:145 | +| AC-2 | "they could opt to just start tagging their github issues with tags instead to kick off work without ever needing to visit the manager." | UX, Integration | U1.1 + I3.1 | **Clause 1** "start tagging github issues with tags" → YES (trigger_label "agentic" + mapping labels rapid-plan/rapid-engineer/pr-review/pr-iterate). **Clause 2** "to kick off work" → YES (dispatcher slings the mapped specialist, dispatch.go cycle step 6). **Clause 3** "without ever needing to visit the manager" → YES (dispatcher auto-starts on `af up`, startup.json start_dispatch:true, install.go:147; no manager interaction). REQUIRES C-6 rider (specialists provisioned) — resolved I3.1. | dispatch cycle (codebase-snapshot §4); start_dispatch:true (install.go:147, verified) | +| AC-3 | "The dispatch.json should be boostrapped when the dispatch.json is first created during initial setup of the repository so we know the repository-name and can include that appropriately in the dispatch.json" | Integration, Data | I2.1 + D2.3-A | **Clause 1** "bootstrapped when … first created" → YES (write-if-absent guard, install.go:152 = "first created"). **Clause 2** "during initial setup of the repository" → YES (runInstallInit, called by quickstart configure_factory:442). **Clause 3** "so we know the repository-name" → YES (discovery via gh/git remote in CWD). **Clause 4** "include that appropriately in the dispatch.json" → YES (validated owner/name written to repos). | install.go:152 (verified); quickstart cd's into repo before init (:428,442, verified) | +| AC-4 | "when we get to the step where we ask the manager,`run af sling --agent \"task description\"`, we should have the work executed autonomously using the step-by-step formula that represents the IDENTITY of that agent and respects the formulas rigid step-by-step process up to the point where human interaction is necessary for next steps" | Integration, UX | I4.1 + U3.1 | **Clause 1** "af sling --agent 'task'" → YES (existing specialist dispatch, sling.go resolveSpecialistAgent, codebase-snapshot §4). **Clause 2** "executed autonomously using the step-by-step formula" → YES (formula instantiated, run via af prime/af done). **Clause 3** "represents the IDENTITY of that agent" → YES (formula-bearing specialists, codebase-snapshot §3). **Clause 4** "respects the formulas rigid step-by-step process" → YES (formula step machinery, unchanged). **Clause 5** "up to the point where human interaction is necessary" → YES (gates; existing behavior). | `af sling --agent` specialist mode (codebase-snapshot §4, sling.go verified) — EXISTING; this change only routes to such agents | +| AC-5 | "all the code branches created should have been pushed as PR's against the main branch without doctor fixes or human interaction (unless absolutely necessary, or in the case of doctor - a `doctor --fix` is acceptable only as a bandaid/fix for legacy broken behaviors, not as an ongoing operational dependency)." | Integration, Security | I4.1 + SEC2.1 | **Clause 1** "branches pushed as PRs against main" → YES (property of the rapid-*/ultra-review formulas the default routes to; routing verified, formula-internal push step is [UNVERIFIED] at step level but out of this change's scope). **Clause 2** "without doctor fixes … as an ongoing dependency" → YES (SEC2.3 rejected; default is valid-by-construction via I3.1, no doctor needed). **Clause 3** "without human interaction (unless absolutely necessary)" → YES (zero human step on happy path; degraded path is the only-when-necessary exception). | Default valid-by-construction (I3.1); SEC2.3 REJECTED on C-5; routing to existing formulas (I4.1) | +| AC-6 | "The agents should follow the known working formula process that IS their IDENTITY to perform their work so that we have consistent successful outcomes out of each agent. Your mission when addressing any problem scenario is to seek to understand how to achieve this desired outcome with systemic improvements while addressing the scenario." | Integration, UX, Scale | I4.1 + U3.1 + S2.1 | **Clause 1** "follow the known working formula process that IS their IDENTITY" → YES (routes to existing formula-bearing specialists, no formula edits). **Clause 2** "consistent successful outcomes out of each agent" → YES (valid-by-construction default + provisioned specialists + inherited stable cadence). **Clause 3** "systemic improvements while addressing the scenario" → YES (single-source default builder + repo discovery + provisioning fix = systemic, not a one-off patch). | I4.1 (formulas unchanged); D2/I1.1 single-source; I3.1 systemic provisioning | + +ALL 6 ACs: every clause = YES. No clause unsatisfied. The single conditional is the +C-6 rider on AC-2 and AC-5 (referenced specialists must be provisioned), which is +explicitly RESOLVED by D6/I3.1 + I3.2. + +--- + +## Table C — Additional Context Coverage + +| Context item (verbatim from source.md) | Reflected in dimension(s) / dismissed with rationale | +|-----------------------------------------|------------------------------------------------------| +| C-4: "review the codebase as the only real source-of-truth … *.md documents might have outdated information" (source.md:62) | Reflected in EVERY dimension: all claims anchored to verified file:line (install.go, dispatch.go, config.go, quickstart.sh) per the Codebase Fidelity Rule; markdown used only as search aid. | +| C-5: "without doctor fixes or human interaction … doctor --fix … only as a bandaid … not as an ongoing operational dependency" (source.md:67) | Reflected in D5/SEC2.3 (REJECTED — relying on doctor violates C-5), D3/U1.1 (zero-touch happy path), D6/I3.1 (valid-by-construction so no doctor needed), Table A C-5 prose. | +| C-6 [inferred]: mappings reference rapid-soldesign-plan/rapid-implement/ultra-review/rapid-increment; labels rapid-plan/rapid-engineer/pr-review/pr-iterate; workflow phases [rapid-plan, rapid-engineer] (source.md:71-74) | Reflected in D2 (the 4 mappings + workflow content), D6/I3 (the C-6 resolution — provision via `af install --agents` + dispatcher tolerance), D5/SEC2. THE central finding: fresh agents.json lacks these 4 specialists (quickstart.sh provisions only manager+supervisor, :448-470, verified). | +| "Before You Proceed" directive: "Read USING_AGENTFACTORY.md first … state the Vision, Mission … how agents start, how they receive work, how formulas drive execution" (source.md:79-83) | Directed at the human/agent investigator's process, not a design deliverable. Reflected operationally: the design respects formula-driven execution (AC-4/AC-6) and the agents-receive-work-via-dispatch flow. Not a config artifact; dismissed as process guidance, honored in framing. | +| Setup flow steps 1-7 (quickdocker → claude → quickstart.sh → af up manager → attach … OR af sling --agent) (source.md:85-98) | Reflected in D6 (the integration map anchors discovery to quickstart's cd-then-init, :428/442) and D3 (the happy path = quickstart → af up → tag issue). The "recommended" step 6 (`af sling --agent`) is exactly AC-4's path. | +| Proposed default dispatch.json with "the source's typos/unclosed brackets; the *intent* matters, not the literal JSON" (source.md:100-148) | Reflected in D2/D2.1 (the schema-vs-source consistency findings: emit from the `DispatchConfig` struct, NOT the malformed literal; the unclosed bracket at source.md:114 is fixed by struct marshaling). Intent honored, literal corrected. | +| Scope: "medium — all 6 dimensions … narrow in surface … touches install/setup code, dispatch config schema, repo-name discovery, and correctness of agent/label references" (source.md:150-154) | Reflected as the scope frame of all 6 dimension files; heavyweight scale machinery (S1.2/S2.2) REJECTED on scope grounds. | + +NO context item is unaddressed. Every Additional Context block from source.md maps +to at least one dimension or is dismissed with explicit rationale. + +--- + +## Audit result: PASS + +No Table A row fails. No AC clause is unsatisfied (the C-6 conditional on AC-2/AC-5 +is resolved by D6/I3.1+I3.2). No Additional Context item is unaddressed. The single +HIGH-severity finding (C-6: fresh agents.json lacks the 4 referenced specialists) +is surfaced in D2/D5/D6 and resolved by I3.1 (provision in quickstart) + I3.2 +(dispatcher skip-and-warn). No return-to-dimension required. diff --git a/.designs/73/codebase-snapshot.md b/.designs/73/codebase-snapshot.md new file mode 100644 index 0000000..0d01e17 --- /dev/null +++ b/.designs/73/codebase-snapshot.md @@ -0,0 +1,238 @@ +# codebase-snapshot.md — Verified Ground Truth (Phase 1) + +Captured by the main context for issue #73. Sub-agents MUST treat this as +authoritative. Claims here were verified by Read/Grep/Bash against the actual +codebase at `/home/dev/af/agentfactory/.agentfactory/worktrees/wt-7605ca`. + +Dynamic (runtime) values are marked ⚡ — they change over time; do not state them +as fixed facts. + +--- + +## 1. Package tree (Go, root module) + +Total Go files (root module, excl. vendor): **267**. Top-level package dirs: + +``` +./cmd/af +./internal/checkpoint +./internal/claude +./internal/cmd # CLI commands (install.go, dispatch.go, sling.go, config_set.go, ...) +./internal/config # config schema + loaders (dispatch.go, config.go, paths.go, startup.go, agents.go) +./internal/formula +./internal/fsutil # WriteFileAtomic +./internal/issuestore +./internal/issuestore/mcpstore +./internal/issuestore/memstore +./internal/lock +./internal/mail +./internal/session +./internal/templates +./internal/tmux +./internal/worktree +web/ # SEPARATE Go module (web/go.mod) — not in root `make test` +``` + +Most-relevant files for this design: +- `internal/cmd/install.go` — `af install --init` / `` / `--agents` +- `internal/config/dispatch.go` — DispatchConfig schema, loader, writer, validators +- `internal/config/config.go` — `DefaultFactoryConfigJSON()` (the single-source default pattern) +- `internal/config/paths.go` — config path helpers +- `internal/cmd/dispatch.go` — dispatcher loop + workflow engine +- `internal/cmd/sling.go` — `af sling --agent` specialist dispatch +- `internal/cmd/config_set.go` — `af config dispatch set` / `af config startup set` +- `quickstart.sh`, `quickdocker.sh` — bootstrap scripts (repo root) + +## 2. Module identity + +``` +module github.com/stempeck/agentfactory +go 1.24.2 +``` + +## 3. Referenced formulas / agents (existence verified) + +The proposed default `dispatch.json` references 4 agents in `mappings[].agent`. +Each EXISTS as a formula in BOTH `.agentfactory/store/formulas/` and +`internal/cmd/install_formulas/` (version 1), and is a configured specialist +(has a `formula` field) in THIS factory's `.agentfactory/agents.json`: + +| Agent (mappings[].agent) | Formula file (both dirs) | Version | In this factory's agents.json? | +|--------------------------|--------------------------|---------|-------------------------------| +| `rapid-soldesign-plan` | `rapid-soldesign-plan.formula.toml` | 1 | YES (type autonomous, formula=rapid-soldesign-plan) | +| `rapid-implement` | `rapid-implement.formula.toml` | 1 | YES | +| `ultra-review` | `ultra-review.formula.toml` | 1 | YES | +| `rapid-increment` | `rapid-increment.formula.toml` | 1 | YES | + +Full formula dir listing (both locations identical, 15 formulas): design, +design-plan-impl, design-v3, design-v7, factoryworker, gherkin-breakdown, +investigate, mergepatrol, minimalworker, rapid-implement, rapid-increment, +rapid-soldesign-plan, rootcause-all, ultra-review, web-design. + +**CRITICAL CAVEAT (verified):** the existence above is for THIS already-fully- +provisioned factory. The DEFAULT `agents.json` written by `af install --init` +contains ONLY `manager` + `supervisor` (see §6, install.go:143). A FRESH install +does NOT have the 4 specialists in agents.json. The issue's setup flow mentions +`design-v3`; the proposed mappings reference `rapid-*` agents — both classes of +specialist are absent from a fresh agents.json until separately provisioned. + +`design-v3` formula EXISTS (version 1). No name mismatches were found between the +proposed `mappings[].agent` values and existing formula/agent names. + +## 4. Referenced CLI commands (verified via --help) + +### `af install --help` (key facts) +- `--init` — Initialize a new factory workspace (creates config dir, starter configs, issue store, hooks). +- `[role]` — Provision a single agent role (renders CLAUDE.md, writes settings.json). +- `--agents` — Regenerate+reinstall all formula-derived agents (runs `agent-gen-all.sh` then `quickstart.sh`); refuses to run from a worktree; requires an already-initialized factory. +- `--no-build` — skip agent-gen-all.sh's duplicate rebuild. +- `runInstallInit` takes NO repo argument (verified §6). + +### `af config dispatch set --help` +> "Read a complete DispatchConfig as JSON on stdin, validate it (struct-level plus a cross-file check that every referenced agent exists in agents.json), and write it atomically to dispatch.json. On any validation failure the command exits non-zero and leaves the existing file untouched." +Flags: `-h, --help` (reads stdin; no other flags). + +### `af dispatch --help` (key facts) +Dispatch cycle: (1) Load `.agentfactory/dispatch.json` and `.agentfactory/agents.json`; (2) check `gh auth`; (3) query each repo for open issues/PRs with the trigger label; (4) match labels to mappings; (5) skip already-dispatched (24h TTL) or busy agents; (6) dispatch via `af sling --agent --reset `; (7) save state to `.runtime/dispatch-state.json`. Subcommands: `start`, `stop`, `status`. Flag: `--dry-run`. + +### `af sling --help` (key facts) +> "Specialist dispatch mode: when --agent names a specialist (an agent with a formula field in agents.json), the agent's formula is instantiated with the task injected as a variable…" +Resolution (sling.go): `resolveSpecialistAgent(root, agentName)` loads agents.json; errors `"agent %q not found in agents.json"` if absent, or `"agent %q is not a specialist (no formula field…)"` if no formula. + +## 5. Referenced file verification + +| Path | Status | +|------|--------| +| `internal/cmd/install.go` | EXISTS | +| `internal/config/dispatch.go` | EXISTS | +| `internal/config/config.go` | EXISTS (`DefaultFactoryConfigJSON` at line 111) | +| `internal/config/paths.go` | EXISTS | +| `internal/cmd/dispatch.go` | EXISTS | +| `internal/cmd/sling.go` | EXISTS | +| `internal/cmd/config_set.go` | EXISTS | +| `.agentfactory/dispatch.json` (runtime path) | `DispatchConfigPath(root)=/.agentfactory/dispatch.json` (paths.go:19) | +| `quickstart.sh` | EXISTS (repo root) — calls `af install --init` after `cd` into discovered repo | +| `quickdocker.sh` | EXISTS (repo root) — takes `` arg; delegates to quickstart.sh | +| `docs/architecture/adrs/` | EXISTS (19 ADRs) | + +## 6. Referenced types and functions (verified signatures, file:line) + +**`DispatchConfig`** — `internal/config/dispatch.go:17-27`: +```go +type DispatchConfig struct { + Repos []string `json:"repos"` + TriggerLabel string `json:"trigger_label"` + NotifyOnComplete string `json:"notify_on_complete"` + Mappings []DispatchMapping `json:"mappings"` + IntervalSecs int `json:"interval_seconds"` + RetryAfterSecs int `json:"retry_after_seconds"` + RemoveTriggerAfterDispatch bool `json:"remove_trigger_after_dispatch"` + Workflows []Workflow `json:"workflows,omitempty"` +} +``` +Every field name in the proposed JSON matches EXACTLY. Defaults applied in +`validateDispatchConfig`: `interval_seconds`→300, `retry_after_seconds`→1800, +`notify_on_complete`→"manager" (const `defaultNotifyAgent`, dispatch.go:15), +`mappings[].source`→"issue". + +**`DispatchMapping`** — `internal/config/dispatch.go:30-35`: +```go +type DispatchMapping struct { + Label string `json:"label,omitempty"` // deprecated singular; auto-migrates to Labels + Labels []string `json:"labels,omitempty"` + Source string `json:"source,omitempty"` // "issue"|"pr", default "issue" + Agent string `json:"agent"` +} +``` + +**`Workflow`** — `internal/config/dispatch.go:42-45`: +```go +type Workflow struct { + Label string `json:"label"` + Phases []string `json:"phases"` +} +``` + +**`LoadDispatchConfig(root) (*DispatchConfig, error)`** — dispatch.go:48-65. Reads +`/.agentfactory/dispatch.json`; returns `ErrNotFound` if missing; runs +**struct-level** `validateDispatchConfig` ONLY (NOT the cross-file agents.json check). + +**`SaveDispatchConfig(path, cfg)`** — dispatch.go:72-82. Struct-validates then +`fsutil.WriteFileAtomic` (temp+rename). Does NOT do the cross-file check. + +**`ValidateDispatchConfig(disp, agents)`** — dispatch.go:93+. **Cross-file**: every +`Mapping.Agent` and an explicitly-set `NotifyOnComplete` must exist in agents.json. +Empty `NotifyOnComplete` left unvalidated (defaults to "manager"). Called by +`af config dispatch set` (config_set.go) and the dispatcher path — NOT by +`LoadDispatchConfig`. + +**`runInstallInit`** — `internal/cmd/install.go`. Verified at install.go:138-157, +the `starterConfigs` map (write-if-absent, idempotent): +```go +starterConfigs := map[string]string{ + "factory.json": config.DefaultFactoryConfigJSON(), + "agents.json": `{"agents":{"manager":{"type":"interactive",...},"supervisor":{"type":"autonomous",...}}}`, + "messaging.json": `{"groups":{"all":["manager","supervisor"]}}`, + "dispatch.json": `{"repos":[],"trigger_label":"agentic","notify_on_complete":"manager","mappings":[],"interval_seconds":300,"retry_after_seconds":1800}`, + "startup.json": `{"agents":["manager"],"quality":"default","fidelity":"default","start_dispatch":true,"watchdog_agents":["manager","supervisor"]}`, +} +``` +- dispatch.json default today: **empty `repos`, empty `mappings`, no `workflows`** (install.go:145). +- `runInstallInit` takes NO repo argument and does NO git-remote lookup — repo identity is unknown to it today. +- Write is idempotent write-if-absent (`os.Stat … os.IsNotExist`, install.go:152) → "first created" (C-2) == this write. +- dispatch.json is the ONLY starter config still an inline literal; `factory.json` uses `DefaultFactoryConfigJSON()` (install.go:142) — established single-source pattern (issue #371 Gap-6). + +**`DefaultFactoryConfigJSON() string`** — `internal/config/config.go:111`. The model +to mirror for a `DefaultDispatchConfigJSON()`. + +**Path helpers** — `internal/config/paths.go`: `dotDir = ".agentfactory"` (:10); +`ConfigDir(root)` (:13); `AgentsConfigPath(root)` (:17); `DispatchConfigPath(root)` (:19). + +**Bootstrap scripts (verified by Agent B):** +- `quickdocker.sh`: takes `` as `$1`; derives container name; clones repo into `$WORKSPACE_DIR/$REPO_NAME`; delegates bootstrap to quickstart.sh. +- `quickstart.sh`: discovers the repo dir by finding the first `.git` under `$WORKSPACE_DIR`, `cd`s into it, then calls `af install --init` (so a git remote IS present in CWD at that moment, but install doesn't read it), then `af install manager` / `af install supervisor`. + +## 7. Dynamic values ⚡ + +- ⚡ `af agents list --json` at snapshot time: this factory has 17 configured + agents incl. all 4 specialists. Running now: `design-v7` (me, Phase 1), + `rapid-soldesign-plan` (orchestrator, awaiting analysis mail), `rootcause-all` + (Phase 2), `manager` (idle). `rapid-increment` shows `all_complete` for PR #67. + These statuses change continuously — do NOT cite as fixed. +- ⚡ This is a fully-provisioned dev factory; a fresh `af install --init` factory + has only manager+supervisor. Reason about the FRESH-install default, not this tree. + +--- + +## Decision History + +### ADRs (searched `docs/architecture/adrs/`; 19 ADRs present) + +**ADR-014 — No interactive prompting in agent-runtime code paths** (Accepted 2026-04-22). Key decision (verbatim): +> "No code path under `cmd/` or `internal/` (Go) or `py/` (Python) may prompt for user input at runtime." +> Required shape for caller discretion: "fail loud with a structured error naming the exact flag that expresses the intent. No prompt, no fallback." +> Exemption (verbatim): "One-time bootstrap / installation scripts invoked manually by a human operator at setup time: `quickdocker.sh`… The operator persona is real at the bootstrap moment. These scripts are exempt but should be minimized over time and not proliferated." +**Implication:** repo-name substitution during `af install --init` (Go code) MUST be non-interactive — discover via git remote, or fail-loud-with-flag — never prompt. A prompt in quickdocker.sh/quickstart.sh is permitted (operator present) but discouraged. + +**ADR-017 — af infrastructure commands must not delete customer data** (Accepted 2026-05-07). Key decision (verbatim): +> "af infrastructure commands (`af install`, `af up/down`, `af formula agent-gen`, `agent-gen-all.sh`, `make sync-formulas`, `quickstart.sh`) must not delete customer data." +> "Outside af directories: read-only. No creates, modifies, or deletes." "Inside af directories: af may manage its own artifacts." +**Implication:** writing/overwriting `.agentfactory/dispatch.json` is INSIDE an af directory → permitted. Reading `git remote` is read-only → permitted. The existing idempotent write-if-absent must be preserved so a customer-edited dispatch.json is never clobbered. + +**ADR-008 — `go:embed` source-of-truth with mechanical drift test** (Accepted 2026-04-10). Key decision: embedded assets (`internal/cmd/install_formulas/`, role templates) must stay byte-parity with source via a mechanical drift test. +**Implication:** a baked-in dispatch default expressed as a Go string/func (`DefaultDispatchConfigJSON()`) is inherently single-source (no embed-vs-source drift). If a default references formula names, those formula files are themselves embedded with a drift test — the names must match real formula files. + +**ADR-015 — formula three-location lifecycle** (Accepted). Formulas live/flow across `internal/cmd/install_formulas/` (embed/ship), `.agentfactory/store/formulas/` (installed), and customer edits; shipped formulas are edited in `install_formulas/`. +**Implication:** any agent named by the default dispatch must correspond to a formula shipped in `install_formulas/`. All 4 referenced formulas are shipped there (§3) ✓. + +**ADR-002 — actor-scoping / `IncludeAllAgents`** — SEARCHED, NOT relevant to this domain (RBAC visibility at the issue-store boundary; unrelated to dispatch.json bootstrapping). + +(Other ADRs searched by title — 001,003,004,005,006,007,009,010,011,012,013,016,018,019 — none govern dispatch-config bootstrapping directly. ADR-019 "no-container-recreation" and ADR-012 "python-preflight" touch setup but do not constrain dispatch defaults.) + +### Prior Designs (`ls .designs/`) +- Only `.designs/73/` exists (this issue). No prior design-doc.md for the dispatch-bootstrap domain. The sibling orchestration produced `problem-summary-73.md` (verbatim issue capture) and `design-refinement-progress.md` (orchestration tracker) — inputs, not prior designs. + +### Recent Deliberate Changes (git log on affected files) +- `internal/config/dispatch.go`: **#38** (`555f97d3`) "Extend af dispatch to support PR label matching alongside issues, add multi-label AND semantics to mappings, introduce source-based grouping with `remove_trigger_after_dispatch`, dispatch cycle locking (Fixes #37)" — established the current mappings/source schema. The `Workflow` type comment cites **issue #378** as the origin of the multi-phase `workflows` feature (dispatch.go:38). So the proposed JSON's `workflows`/`mappings`/`source`/`remove_trigger_after_dispatch` are all backed by deliberate recent additions — the design must use them as-defined, not redefine them. +- `internal/cmd/install.go`: **#58** (`9a27609d`) "Add startup.json-driven `af up`… dispatcher auto-start…" — `startup.json` default already sets `start_dispatch:true`, so the dispatcher auto-starts on `af up`. Reversing that is a deliberate-change reversal and must be justified. +- No commit has yet added a non-empty default to dispatch.json — this issue is the first to do so. diff --git a/.designs/73/conflicts.md b/.designs/73/conflicts.md new file mode 100644 index 0000000..30a5bf7 --- /dev/null +++ b/.designs/73/conflicts.md @@ -0,0 +1,250 @@ +# conflicts.md — Cross-Dimension Conflict Matrix + +Dimensions: D1 API, D2 Data, D3 UX, D4 Scale, D5 Security, D6 Integration. +Legend: **(none)** no conflict · **T** tension (trade-off needed) · **X** direct +conflict (resolution required). + +## NxN Matrix (upper triangle; symmetric) + +| | D1 API | D2 Data | D3 UX | D4 Scale | D5 Security | D6 Integration | +|--------|--------|---------|-------|----------|-------------|----------------| +| **D1 API** | — | T | (none)| (none) | T | T | +| **D2 Data**| | — | (none)| (none) | T (via C-6) | X (C-6) | +| **D3 UX** | | | — | (none) | T | T | +| **D4 Scale**| | | | — | (none) | (none) | +| **D5 Sec** | | | | | — | X (C-6 resolution) | +| **D6 Int** | | | | | | — | + +--- + +## Cell-by-cell justification + +### D1 API × D2 Data — **T** (tension) +- **Nature:** API owns the `DefaultDispatchConfigJSON(repo)` SIGNATURE and the + discovery/injection point; Data owns the CONTENT (which mappings/workflows). The + tension: the function's output must EXACTLY satisfy `validateDispatchConfig` + (dispatch.go:141-185, verified) — if API marshals a shape Data didn't fully + specify (e.g. omits a required label), the default fails to load. +- **Impact:** A mismatch produces an unloadable default (struct validation error). +- **Resolution options:** (a) API builds from the `DispatchConfig` struct so the + compiler enforces the field set; (b) a golden test pins the exact output. +- **Chosen resolution:** Both — A1.1 builds from the struct (compile-time field + safety) AND a golden round-trip test (D6 test list) pins content. Rationale: the + struct guarantees well-formedness; the golden test guarantees the specific + mappings Data chose are present. Tension resolved by construction. + +### D1 API × D3 UX — **(none)** +The API change (a new Go function + one call site) is invisible to the operator; UX +recommends an install banner and stderr hints that consume the SAME default the API +writes. They operate on disjoint surfaces (internal function vs operator-visible +output) and the banner derives from the written default, so they cannot diverge. + +### D1 API × D4 Scale — **(none)** +API adds one bounded install-time discovery call; Scale's only ask of API is that +the call have a timeout (S1.1). That is a complementary refinement, not a conflict — +no AC, constraint, or recommendation pulls them in opposite directions. + +### D1 API × D5 Security — **T** (tension) +- **Nature:** API wants discovery to be frictionless (just take whatever + `gh`/`git remote` returns and write it); Security requires the discovered string + be validated against a strict `owner/name` allowlist before it is written + (SEC1.1), adding a rejection path. +- **Impact:** Without validation, a crafted remote URL injects a bad `repos` value + (flag-injection into `gh --repo`, dispatch.go:300; terminal-escape into the UX + banner). +- **Resolution options:** (a) validate at the write boundary in API's discovery + helper; (b) validate later in the dispatcher. +- **Chosen resolution:** Validate at the WRITE boundary (SEC1.1) inside the + discovery step API owns. Rationale: a bad value never reaches disk or the + dispatcher; the dispatcher's own `strings.Cut` guard (dispatch.go:537-539) becomes + a second line of defense, not the only one. Tension resolved. + +### D1 API × D6 Integration — **T** (tension) +- **Nature:** API proposes the function lives in `internal/config`; Integration + proposes the call site is `runInstallInit` and the discovery ordering + (discover → validate → build → write). The tension is sequencing: discovery must + complete (and validate) BEFORE the starterConfigs map is built (install.go:139). +- **Impact:** Wrong ordering writes an empty/placeholder repos. +- **Resolution options:** (a) discover before building the map; (b) build the map + with a placeholder then patch — rejected (two writes). +- **Chosen resolution:** Discover-then-build (I2.1): the discovery+validation runs + before the starterConfigs map literal so the default carries the real repo in one + write. Tension resolved by ordering. + +### D2 Data × D3 UX — **(none)** +Data defines the mapping labels; UX's banner (U1.2) simply lists those labels for +discoverability. UX consumes Data's output read-only; there is no competing +requirement. The only shared fact (the label set) flows one direction. + +### D2 Data × D4 Scale — **(none)** +Data picks the mappings/workflows content; Scale picks the cadence (300/1800) and +single-repo default. They touch different fields of the same struct +(`mappings`/`workflows` vs `interval_seconds`/`repos`) and both defer to the +verified struct defaults, so there is no contention. + +### D2 Data × D5 Security — **T** (tension via C-6) +- **Nature:** Data wants to ship the faithful 4-mapping default (D2.2-A); Security + flags that those 4 agents are absent from a fresh agents.json, making the default + a cross-file-validation FAILURE (C-6) — an availability concern Security owns. +- **Impact:** Dispatch-start fails on a fresh factory until specialists exist. +- **Resolution options:** (a) provision specialists (SEC2.1); (b) dispatcher + tolerate unknown agents (SEC2.2); (c) ship a manager/supervisor-only default + (rejected — fails AC-2). +- **Chosen resolution:** SEC2.1 (provision) primary + SEC2.2 (tolerate) defense-in- + depth. Rationale: provisioning makes the default valid-by-construction (C-5); + tolerance backstops a partial provision. Data keeps its faithful default; Security + gets validity. Tension resolved — escalated to D6 for sequencing (see D2×D6). + +### D2 Data × D6 Integration — **X** (direct conflict — C-6, THE design crux) +- **Nature:** DIRECT conflict. Data's recommended default (D2.2-A) references 4 + specialists. Integration's verified finding: the bootstrap (`quickstart.sh`) + provisions ONLY manager+supervisor (quickstart.sh:448-470, verified), so those 4 + agents do NOT exist in a fresh agents.json (install.go:143, verified), and + `ValidateDispatchConfig` (dispatch.go:100-104, verified) FAILS on the first + unknown agent at dispatch-start. Shipping Data's default WITHOUT an Integration + change is a broken-on-arrival autonomous path — a real, not theoretical, conflict. +- **Impact:** HIGH. AC-2 ("kick off work without visiting the manager") and AC-4/ + AC-5 silently fail on every fresh install — the exact scenario the issue targets. +- **Resolution options:** (1) Integration adds `af install --agents` to quickstart + to provision all shipped specialists before dispatch (I3.1). (2) Integration + changes the dispatch loop to skip-and-warn on unknown agents (I3.2). (3) Data + ships a degraded default referencing only manager/supervisor (D2.2-B / I3.3) — + REJECTED (fails AC-2/AC-4). (4) Data ships mappings-only without the workflow + (D2.2-C) — does NOT resolve C-6 (the mappings themselves reference the absent + agents), so insufficient alone. +- **Chosen resolution:** **I3.1 (provision specialists in quickstart) as primary + + I3.2 (dispatcher skip-and-warn) as defense-in-depth.** Rationale: I3.1 makes the + default valid-by-construction (satisfies C-5/C-6 cleanly), and I3.2 ensures a + partially-provisioned factory degrades gracefully rather than failing the whole + cycle. Data ships its faithful default (D2.2-A) unchanged. THIS is the resolution + the synthesis must carry — it is the single most important decision in the design. + +### D3 UX × D4 Scale — **(none)** +UX's banner and error messages are install-time/operator-facing text; Scale's +cadence and single-repo default are runtime-loop concerns. They share no surface and +no competing requirement. + +### D3 UX × D5 Security — **T** (tension) +- **Nature:** UX wants to ECHO the discovered repo in a friendly "next steps" + banner (U1.2); Security requires that any echoed value be escape-safe to prevent + terminal-escape injection from a crafted remote URL. +- **Impact:** An unsanitized repo string in the banner could inject terminal + escapes. +- **Resolution options:** (a) validate the repo before storing (SEC1.1) so anything + echoed is already `owner/name`-clean; (b) sanitize at echo time. +- **Chosen resolution:** (a) — SEC1.1 validates at the write boundary, so the value + UX echoes is already a strict `owner/name` (no escapes possible). UX echoes the + stored value, not the raw discovery output. Tension resolved upstream. + +### D3 UX × D6 Integration — **T** (tension) +- **Nature:** UX's zero-touch happy path (U1.1) ASSUMES the dispatcher auto-starts + and the specialists exist; Integration owns whether both are true. If Integration + does NOT resolve C-6 (D2×D6), UX's "just tag an issue" promise is false. +- **Impact:** UX's headline ergonomic claim depends entirely on Integration's C-6 + resolution and the verified `start_dispatch:true` (install.go:147). +- **Resolution options:** (a) Integration resolves C-6 (I3.1+I3.2) so UX's promise + holds, with U2.1 actionable errors as the fallback when it doesn't; (b) UX softens + the promise. +- **Chosen resolution:** (a) — Integration's I3.1+I3.2 makes the zero-touch path + real; U2.1 actionable errors (naming `af install --agents`) cover the residual + failure. Tension resolved by Integration delivering the precondition UX assumes. + +### D4 Scale × D5 Security — **(none)** +Scale's single bounded discovery call and inherited cadence introduce no new attack +surface; Security's validation/provisioning concerns are orthogonal to performance. +Neither pulls against the other. + +### D4 Scale × D6 Integration — **(none)** +Scale defers entirely to the existing dispatch cadence, 50-result cap +(dispatch.go:182, verified), and 24h TTL — all of which Integration leaves +unchanged. Scale asks Integration only to confirm these are untouched (they are). +No competing requirement. + +### D5 Security × D6 Integration — **X** (direct conflict — the C-6 RESOLUTION mechanism) +- **Nature:** DIRECT conflict over HOW to resolve C-6. Security's SEC2.1 says + "provision specialists so the default is valid-by-construction"; SEC2.2 says + "make the dispatcher tolerate unknown agents." Integration must decide WHERE this + lives and in what ORDER, AND there is a sub-conflict: SEC2.2 (relax + `ValidateDispatchConfig`) collides with the WRITE-path guarantee — `af config + dispatch set` (config_set.go:85-91, verified) MUST stay strict so a human typo is + caught, while the dispatch LOOP may tolerate. A blanket relaxation would weaken + the write-path contract. +- **Impact:** HIGH. Getting this wrong either leaves the default broken (no + resolution) or weakens validation everywhere (over-broad relaxation). +- **Resolution options:** (1) Provision-only (SEC2.1/I3.1) — strict validation + everywhere, default valid-by-construction. (2) Tolerate-only (SEC2.2/I3.2) — relax + the dispatch loop. (3) Both, with the relaxation scoped ONLY to the dispatch-loop + caller, write path stays strict. +- **Chosen resolution:** **(3)** — I3.1 provisions specialists (primary), AND I3.2 + scopes the skip-and-warn tolerance to the DISPATCH-LOOP caller only; `af config + dispatch set` keeps the strict `ValidateDispatchConfig` (config_set.go unchanged). + Rationale: the default is valid-by-construction via provisioning; the dispatch + loop degrades gracefully on a partial provision; the human write path retains its + typo-catching strictness. This split is the precise mechanism that resolves both + the C-6 conflict and its write-path sub-conflict. Resolution required and chosen. + +--- + +## ELEVATION-LEVEL CONFLICTS + +After the NxN matrix, examine whether any NEW abstraction/component proposed by one +dimension conflicts with another dimension's recommendation. + +New abstractions/components proposed across dimensions: +1. **`DefaultDispatchConfigJSON(repo)`** (D1/API) — a new Go function. +2. **A repo-discovery helper** (`gh repo view` / `git remote` normalizer) (D1/API). +3. **A bootstrap provisioning step** (`af install --agents` in quickstart) (D6/I3.1). +4. **A dispatcher unknown-agent tolerance** (skip-and-warn) (D6/I3.2, D5/SEC2.2). +5. **An install "next steps" banner** (D3/U1.2). + +### EL-1 — `DefaultDispatchConfigJSON` (new fn) vs the single-source pattern — NO conflict +The new function is the OPPOSITE of an abstraction-conflict: it removes the one +remaining inline-literal exception (codebase-snapshot Decision History, issue #371 +Gap-6) by making dispatch.json's default match the `factory.json` pattern +(`DefaultFactoryConfigJSON`, config.go:111, verified). It aligns every dimension; no +dimension is contradicted. + +### EL-2 — Repo-discovery helper (new component) vs ADR-014 and Security — RESOLVED, NO residual conflict +The discovery helper introduces git/gh I/O into a previously git-agnostic Go path. +This could conflict with ADR-014 (no interactive prompting) — resolved because +discovery is NON-interactive (git remote / gh) with fail-loud degradation, never a +prompt. It could conflict with Security (untrusted input) — resolved by SEC1.1 +validation at the write boundary. No dimension's recommendation is left +contradicted. + +### EL-3 — `af install --agents` provisioning step (D6/I3.1) vs Scale (bootstrap cost) — TENSION, resolved +Provisioning all shipped specialists at bootstrap is a NEW, heavier setup step that +Scale (D4) would otherwise want minimal. Tension: heavier one-time bootstrap vs +valid-by-construction default. Resolution: the cost is one-time at setup (not a hot +path), and it is the ONLY clean way to satisfy C-6/AC-2; Scale's concern is bounded +because discovery and provisioning both run once. No runtime-scale impact. The +elevation choice (provision) wins over the minimal-bootstrap preference because the +AC requires it. + +### EL-4 — Dispatcher unknown-agent tolerance (D6/I3.2) vs the write-path validation contract (D5/D6) — DIRECT, resolved by scoping +This is the elevation-level form of the D5×D6 X-conflict: introducing a TOLERANCE +behavior in the dispatcher conflicts with the existing STRICT validation that +`af config dispatch set` relies on (config_set.go:85-91, verified). A new +"tolerate" abstraction must NOT leak into the write path. Resolution (carried from +D5×D6): scope the tolerance to the dispatch-loop caller ONLY; the write-path +validator stays strict. This keeps two callers of `ValidateDispatchConfig` with +deliberately different strictness — a documented split, not an accidental +divergence. + +### EL-5 — Install banner (D3/U1.2) vs no-new-output minimalism — NO conflict +The banner is additive operator-facing text derived from the just-written default; +no dimension proposes suppressing install output, and Security's only concern (echo +safety) is resolved by SEC1.1. No conflict. + +--- + +## Summary of required resolutions (for synthesis to carry) + +| Conflict | Type | Resolution | +|----------|------|------------| +| D2×D6 (C-6: default references unprovisioned specialists) | X | I3.1 provision via `af install --agents` in quickstart (primary) + I3.2 dispatcher skip-and-warn (defense-in-depth) | +| D5×D6 (C-6 resolution mechanism + write-path strictness) | X | Provision + scope dispatcher tolerance to the dispatch-loop caller ONLY; write path stays strict | +| D1×D5 / D3×D5 (untrusted repo string) | T | SEC1.1 validate owner/name at the write boundary | +| D1×D2 (function output must satisfy validation) | T | Build from the struct + golden test | +| D1×D6 (discovery ordering) | T | Discover→validate→build→write in runInstallInit | +| D3×D6 (zero-touch path depends on C-6 + auto-start) | T | Integration delivers C-6 resolution + verified start_dispatch:true; U2.1 fallback errors | diff --git a/.designs/73/data.md b/.designs/73/data.md new file mode 100644 index 0000000..031988a --- /dev/null +++ b/.designs/73/data.md @@ -0,0 +1,189 @@ +# D2 — Data Model (data.md) + +Owner of (verification.md): AC-1 (with Integration), AC-3 (with Integration), +C-6 (with Integration, Security). This dimension governs the SHAPE and CONTENT of +the baked-in default `dispatch.json`: which fields, which mappings, which +workflows, and whether any storage/schema/migration change is needed. + +CONSTRAINT-SENSITIVE NOTE (storage): There is NO new storage format, NO database, +NO schema migration, and NO SQL in any option below. The `DispatchConfig` JSON +schema already exists (`internal/config/dispatch.go:18-45`, verified) and the +file is already written by the install flow (`internal/cmd/install.go:145`, +verified). This dimension only changes the DEFAULT VALUE written into an +existing file with an existing schema. C-4 (codebase-only truth) is honored by +anchoring every field below to the verified struct. + +--- + +## D2.0 Established facts (verified, anchored) + +- `DispatchConfig` fields and JSON tags: `repos`, `trigger_label`, + `notify_on_complete`, `mappings`, `interval_seconds`, `retry_after_seconds`, + `remove_trigger_after_dispatch`, `workflows` (dispatch.go:18-27, verified). + **Every field name in the proposed JSON matches the struct exactly** + (codebase-snapshot §6). +- `DispatchMapping`: `label` (deprecated singular, auto-migrates to `labels`), + `labels`, `source` ("issue"|"pr", default "issue"), `agent` (dispatch.go:30-35, + verified). +- `Workflow`: `label`, `phases` (dispatch.go:42-45, verified). +- Struct validation (`validateDispatchConfig`, dispatch.go:141-185, verified) + requires: `len(repos) > 0`, non-empty `trigger_label`, `len(mappings) > 0`, + each mapping has ≥1 label and a non-empty `agent`. Workflow phases must each + resolve to a single-label mapping on the phase label ALONE + (`phaseResolvesAlone`, dispatch.go:256-267; `validateWorkflows`, dispatch.go:192-249, + verified), and a workflow's phases must share one source (dispatch.go:239-245). +- Cross-file validation (`ValidateDispatchConfig`, dispatch.go:93-138, verified): + every `mapping.agent` must exist in agents.json; every workflow-phase agent must + be formula-bearing. + +## D2.1 Critical schema-vs-source consistency findings (block naive copy of the proposed JSON) + +These are DATA-model defects in the source's *proposed* JSON (source.md:100-148) +that any chosen option MUST fix — the source itself says "the *intent* matters, +not the literal JSON" (source.md:100): + +1. The proposed mappings use `"labels"` (plural, dispatch.go:32 `Labels`), but the + workflow `phases` are `["rapid-plan", "rapid-engineer"]` while the mapping + labels are `rapid-plan` and `rapid-engineer`. For `validateWorkflows` to pass, + each phase must equal a single-label mapping's lone label + (`phaseResolvesAlone`, dispatch.go:256-267). The proposed mappings ARE + single-label (`["rapid-plan"]`, `["rapid-engineer"]`) — so this resolves, but + ONLY if the workflow phases reference the *mapping labels* `rapid-plan` / + `rapid-engineer` (they do). The `pr-review` / `pr-iterate` mappings are NOT in + any workflow, which is legal. +2. `validateWorkflows` HIGH-2 (dispatch.go:239-245) requires all phases of a + workflow to share a source. The `feature-workflow` phases `rapid-plan` + (source "issue") and `rapid-engineer` (source "issue") share source "issue" ✓. +3. The proposed `workflows[].label` is `"feature-workflow"`. `validateWorkflows` + rejects a workflow label that collides with `trigger_label` ("agentic") + (dispatch.go:202-204) — no collision ✓ — and rejects a workflow label that is + ALSO a mapping label (HIGH-B, dispatch.go:219-221) — `feature-workflow` is not a + mapping label ✓. + +These are not "options"; they are constraints any default must satisfy. The +options below differ in WHAT agents/workflows the default ships, not whether it is +schema-valid. + +## D2.2 — Default content: which mappings and workflows ship + +### Option D2.2-A — Ship the proposed 4 mappings + feature-workflow VERBATIM (intent-corrected) — RECOMMENDED, with the C-6 provisioning rider + +Bake the default exactly as the source proposes (source.md:100-148), corrected to +valid JSON: `trigger_label:"agentic"`, the 4 mappings (`rapid-plan`→ +`rapid-soldesign-plan`, `rapid-engineer`→`rapid-implement`, `pr-review`→ +`ultra-review`/source pr, `pr-iterate`→`rapid-increment`/source pr), and the +`feature-workflow` workflow `["rapid-plan","rapid-engineer"]`. `repos` is injected +by D1/A2 discovery. All other scalars match the proposed values and the struct +defaults (interval 300, retry 1800, notify "manager", remove_trigger true). + +- Trade-offs: Maximally faithful to the source intent (AC-1). All 4 referenced + formulas EXIST in `install_formulas/` and `store/formulas/` (codebase-snapshot + §3, re-verified: `rapid-soldesign-plan`, `rapid-implement`, `ultra-review`, + `rapid-increment` all present). The risk is purely C-6 cross-file: a FRESH + `agents.json` has only `manager`+`supervisor` (codebase-snapshot §6, + install.go:143, verified), so these 4 agents are ABSENT until provisioned → + `ValidateDispatchConfig` would fail (dispatch.go:100-104). +- THE C-6 RIDER (mandatory companion, owned jointly with Integration): the default + is only safe IFF the 4 agents are present in agents.json by the time + `ValidateDispatchConfig` runs. Note that `LoadDispatchConfig` does NOT run the + cross-file check (dispatch.go:48-65, verified — struct-only); only the dispatcher + path and `af config dispatch set` do (codebase-snapshot §6). So the file LOADS + fine; the cross-file failure surfaces at dispatch-start. The rider: bootstrap + must provision the referenced specialists (via `af install --agents`, which runs + `af formula agent-gen` for EVERY shipped formula — agent-gen-all.sh:134-153, + verified) before the autonomous path is exercised, OR the dispatcher must skip + unknown-agent mappings (a dispatcher change owned by Integration). This rider is + the crux of C-6 and is escalated to Integration/D6. +- Reversibility: Easy (the default is one Go function body). +- Constraints: satisfies C-1, AC-1, and (with the rider) C-6. Recommended. + +### Option D2.2-B — Ship a MINIMAL default that references only provisioned agents (manager/supervisor), with placeholder mappings the operator edits — REJECTED + +Bake a default whose mappings reference only `manager`/`supervisor` (the two +agents guaranteed present in a fresh agents.json), or ship empty mappings with a +comment. + +- REJECTED: fails [AC-1], fails [AC-2]. AC-1 requires a "baked-in default for + dispatch with agentfactory" that is useful out of the box; AC-2 requires that + applying the trigger/mapping labels actually dispatches WORK (specialist + formulas), "without ever needing to visit the manager." `manager`/`supervisor` + are not specialists with formulas (codebase-snapshot §6: fresh agents.json has + `manager` type interactive, `supervisor` type autonomous — neither is a + formula-bearing specialist). Mapping the trigger labels to them dispatches no + meaningful formula work, so the autonomous outcome AC-2/AC-4 demand never occurs. + Also, an empty/placeholder mappings default fails `validateDispatchConfig` + (`len(mappings) > 0`, dispatch.go:148-150) or loads but does nothing. + +### Option D2.2-C — Ship the 4 mappings WITHOUT the `feature-workflow` workflow (mappings-only default) — RECOMMENDED (fallback / conservative) + +Identical to D2.2-A but omit the `workflows` block. Mappings alone already satisfy +AC-1/AC-2 (label→agent dispatch); the `workflows` multi-phase pipeline (issue #378, +dispatch.go:38) is an additional convenience the source proposes but no AC strictly +requires. + +- Trade-offs: Smaller blast radius — drops the `validateWorkflows` surface + (dispatch.go:192-249) and the HIGH-2 same-source / formula-bearing constraints + entirely. Loses the one-label `feature-workflow` convenience the source proposes. + No AC names "feature-workflow" specifically; AC-1 ("a baked-in default") and AC-2 + (label-tagging dispatches work) are fully met by mappings alone. +- Reversibility: Easy. +- Constraints: satisfies C-1, AC-1, AC-2 (with the same C-6 rider as D2.2-A). + Recommended as the conservative fallback if the workflow adds validation risk on + a fresh factory. NOTE: this does NOT reverse a deliberate change — issue #378's + `workflows` feature (dispatch.go:38, Decision History) remains in the schema and + usable; we simply don't ship one BY DEFAULT. So no ADR/recent-change reversal. + +## D2.3 — `repos` data lifecycle + +### Option D2.3-A — `repos: [""]` written once at first-create, never re-written — RECOMMENDED + +The discovered repo (D1/A2) is written into `repos` exactly once, by the existing +idempotent write-if-absent path (`os.Stat … os.IsNotExist`, install.go:152, +verified). A re-run of `af install --init` does NOT clobber an operator-edited +`repos` (the file already exists → skipped). + +- Trade-offs: Honors C-2 ("first created") and ADR-017 (never clobber customer + data — codebase-snapshot Decision History). If the operator later moves the repo + or adds repos, their edit survives. Cost: if discovery yielded the wrong repo at + first-create, only a manual edit (or delete+reinit) fixes it — acceptable, and + symmetric with every other starter config. +- Reversibility: Easy. +- Constraints: satisfies C-2, C-3, complies with ADR-017. Recommended. + +### Option D2.3-B — Re-derive and overwrite `repos` on every `af install --init` — REJECTED + +- REJECTED: violates [C-2] and contradicts [ADR-017]. C-2 ties the bootstrap to + "first created"; the existing write-if-absent guard (install.go:152, verified) + is precisely the "first created == this write" semantics codebase-snapshot §6 + records. Overwriting on every run would clobber a customer-edited `repos`, + which ADR-017 ("af infrastructure commands must not delete customer data") + forbids for an inside-af-dir file that the customer may have curated. + +## Reversibility (this dimension): Easy + +The change is the body of one default-config builder plus the repo value. No +schema change, no migration, no on-disk format change. A revert restores the +empty-default literal. + +## Dependencies produced + +- PROVIDES to **API (D1)**: the exact field/value set the `DefaultDispatchConfigJSON` + function must emit so its output passes `validateDispatchConfig` (the 4 mappings, + the workflow or not, the scalar defaults). +- PROVIDES to **Integration (D6)**: the C-6 RIDER — the list of agents the default + references (`rapid-soldesign-plan`, `rapid-implement`, `ultra-review`, + `rapid-increment`) that MUST be provisioned (or skipped) before + `ValidateDispatchConfig` runs. +- REQUIRES from **API (D1)**: the discovered `owner/name` repo string to place in + `repos`. +- REQUIRES from **Integration (D6)**: confirmation of WHEN cross-file validation + runs relative to specialist provisioning in the bootstrap sequence. + +## Risks identified + +| Risk | Severity | Mitigation | +|------|----------|------------| +| Default references 4 specialists absent from a fresh agents.json → `ValidateDispatchConfig` fails at dispatch-start (C-6) | HIGH | The C-6 rider: bootstrap provisions specialists via `af install --agents` (agent-gen-all.sh:134-153) before dispatch, OR Integration makes the dispatcher skip unknown-agent mappings | +| Naively copying the source's malformed JSON (unclosed bracket, source.md:114) | HIGH | D2.1: emit from the `DispatchConfig` struct, not from the literal; the struct guarantees well-formed JSON | +| Shipping `feature-workflow` adds `validateWorkflows` failure surface on edge factories | Low | D2.2-C fallback (mappings-only) removes the workflow surface entirely | +| A formula referenced by the default is renamed/removed in a future release, breaking the default | Medium | ADR-008 drift test ties shipped formula names to embedded files; a golden test asserting the default validates against the shipped agents catches a rename | diff --git a/.designs/73/dependencies.md b/.designs/73/dependencies.md new file mode 100644 index 0000000..bc3dae5 --- /dev/null +++ b/.designs/73/dependencies.md @@ -0,0 +1,105 @@ +# dependencies.md — Dependency Graph + +Code-level component dependencies for the dispatch.json baked-in default, plus the +critical-path build order. All cited paths/lines are verified this session or in +codebase-snapshot.md. + +--- + +## Components (the units this design adds or touches) + +| Id | Component | Location (verified) | New / Modified | +|----|-----------|---------------------|----------------| +| K1 | `DefaultDispatchConfigJSON(repo string) string` | `internal/config` (new fn, beside `DefaultFactoryConfigJSON`, config.go:111) | NEW | +| K2 | Repo-discovery helper (`gh repo view` / `git remote` → validated `owner/name`) | `internal/cmd/install.go` (new, inside/near `runInstallInit`, :97+) | NEW | +| K3 | Repo-string validator (strict `owner/name` allowlist) | `internal/cmd` or `internal/config` (new) | NEW | +| K4 | `runInstallInit` starter-config wiring | `internal/cmd/install.go:139-157` (modify :145 to call K1) | MODIFIED | +| K5 | quickstart specialist provisioning (`af install --agents`) | `quickstart.sh` `configure_factory` (:414-471) | MODIFIED | +| K6 | Dispatcher unknown-agent tolerance (skip-and-warn, dispatch-loop caller only) | `internal/cmd/dispatch.go` dispatch loop (:160+) + `ValidateDispatchConfig` caller boundary | MODIFIED (optional, defense-in-depth) | +| K7 | Golden/round-trip + cross-file tests | `internal/config/*_test.go`, `internal/cmd/*_test.go` | NEW | +| EX1 | `validateDispatchConfig` (struct validation) | `internal/config/dispatch.go:141-185` | EXISTING (consumed, unchanged) | +| EX2 | `ValidateDispatchConfig` (cross-file) | `internal/config/dispatch.go:93-138` | EXISTING (consumed; strict at write path, tolerant at dispatch loop via K6) | +| EX3 | `af install --agents` / `agent-gen-all.sh` | `internal/cmd/install.go:620+`, `agent-gen-all.sh:134-153` | EXISTING (invoked by K5) | +| EX4 | write-if-absent guard | `internal/cmd/install.go:150-157` | EXISTING (reused by K4) | + +--- + +## Component dependencies (code-level) + +Arrow `A → B` reads "A depends on / requires B": + +``` +K4 (install wiring) → K1 (default builder) +K4 → K2 (repo discovery) +K2 (discovery) → K3 (repo validator) +K1 (default builder) → EX1 (validateDispatchConfig — output must pass struct validation) +K4 → EX4 (write-if-absent guard — reused, unchanged) +K5 (quickstart provision) → EX3 (af install --agents / agent-gen-all.sh) +K6 (dispatcher tolerance) → EX2 (ValidateDispatchConfig — relaxes only the dispatch-loop caller) +K7 (tests) → K1, K4, K5, K6, EX1, EX2 (asserts behavior of all) + +Runtime (not build) ordering at install/dispatch time: + K2 → K3 → K1 → K4 (discover → validate → build default → write) [install] + K5 (provision specialists) MUST complete before the dispatcher runs EX2 [bootstrap] + EX2 (cross-file) consumes the result of K4 (the written default) + K5 (provisioned agents.json) +``` + +Note: K5 and K1/K4 have NO compile-time dependency on each other (K5 is a shell +script; K1/K4 are Go). Their dependency is a RUNTIME SEQUENCING one: the default +(K4) and the provisioned agents (K5) must BOTH be in place before the dispatcher +runs the cross-file check (EX2). This is the C-6 sequencing the synthesis must pin. + +--- + +## Component build order (critical path) + +| Order | Component | Depends on (must exist first) | On critical path? | Risk | +|-------|-----------|-------------------------------|-------------------|------| +| 1 | K3 repo validator (allowlist) | — (pure function) | YES | LOW — pure regex/string check | +| 2 | K1 `DefaultDispatchConfigJSON` | EX1 (struct + validator, existing) | YES | LOW — mirrors existing `DefaultFactoryConfigJSON` | +| 3 | K2 repo discovery | K3 (validates its output) | YES | MED — git/gh I/O, URL-shape handling, gh-auth timing | +| 4 | K4 install wiring | K1, K2, EX4 | YES | LOW — one call-site change in an existing map | +| 5 | K5 quickstart provisioning | EX3 (existing `af install --agents`) | **YES — C-6 critical** | MED — heavier bootstrap; must run before dispatch; relies on `af install --agents` succeeding | +| 6 | K6 dispatcher tolerance (optional) | EX2 | NO (defense-in-depth) | MED — must NOT weaken the write-path validator | +| 7 | K7 tests | K1, K4, K5, (K6) | YES — gates AC-1/AC-3/C-6 | LOW | + +**Critical path:** K3 → K1 → K4 (the default itself), in parallel with K5 (C-6 +provisioning). The two strands converge at the dispatcher's cross-file check (EX2) +at runtime: the default (K4) and the provisioned agents.json (K5) must both be +present. K6 is off the critical path (the design is correct without it; it only adds +graceful degradation). + +--- + +## Circular-dependency check + +NO cycles. Verified by inspection of the arrows above: +- K1 depends only on EX1 (existing); EX1 depends on nothing in this set. +- K2 → K3; K3 depends on nothing. +- K4 → {K1, K2, EX4}; none of K1/K2/EX4 depend back on K4. +- K5 → EX3; EX3 (the existing `af install --agents`) does not depend on K1/K4/K5. +- K6 → EX2; EX2 does not depend on K6 (K6 changes how a CALLER uses EX2, not EX2's + own dependencies). +- K7 depends on everything but nothing depends on K7. + +A topological order exists: **K3, K1, K2, K4, K5, K6, K7** — a DAG, no cycle. + +Potential cycle that was AVOIDED (verified): `ValidateDispatchConfig` deliberately +does NOT import `internal/formula` to inspect phase agents' formulas, because +"internal/formula imports internal/config, so importing it here would create an +import cycle" (dispatch.go:130-136, verified). Any new validation in K1/K6 MUST +likewise avoid importing `internal/formula` from `internal/config` to preserve the +acyclic package graph. + +--- + +## Critical-path risk flags + +| Risk on critical path | Component | Severity | Mitigation | +|-----------------------|-----------|----------|------------| +| C-6 sequencing: K5 (provision) must finish before the dispatcher's EX2 runs, or the autonomous path fails on a fresh factory | K5 ↔ EX2 | HIGH | Run `af install --agents` in quickstart's configure_factory (after init, before `af up`/dispatch); K6 backstops a partial provision with skip-and-warn | +| K2 discovery depends on gh-auth/git-remote timing (auth may post-date init in the setup flow, source.md:90) | K2 | MED | Layer `git remote` (no auth) under `gh repo view`; warn-don't-abort (A3.1) degrades to a loadable default | +| K1 output must pass EX1 exactly, or the default is unloadable | K1 → EX1 | LOW (caught early) | Build K1 from the `DispatchConfig` struct (compile-time field safety) + K7 golden test | +| K6 over-broad relaxation weakens `af config dispatch set` strictness | K6 → EX2 | MED | Scope tolerance to the dispatch-loop caller ONLY; write path keeps strict EX2 (config_set.go unchanged) | +| New `internal/config` validation accidentally imports `internal/formula` → import cycle | K1 / K6 | MED | Preserve the existing no-formula-import discipline (dispatch.go:130-136, verified) | +| Future formula rename breaks the default's agent references | K1 + EX3 | MED | ADR-008 drift test ties formula names to embedded files; K7 cross-file test pins the default against shipped agents | diff --git a/.designs/73/design-doc.md b/.designs/73/design-doc.md new file mode 100644 index 0000000..69a0539 --- /dev/null +++ b/.designs/73/design-doc.md @@ -0,0 +1,319 @@ +# Design: Baked-in default `dispatch.json` for zero-touch label-triggered autonomy + +## Executive Summary + +Today `af install --init` creates `.agentfactory/dispatch.json` with an EMPTY +default (`repos:[]`, `mappings:[]`, no `workflows` — `internal/cmd/install.go:145`, +verified), and `runInstallInit` performs no repo-name discovery. So a freshly +bootstrapped factory cannot drive autonomous work from GitHub labels until a human +hand-edits the file — the gap issue #73 targets. This design bakes a useful, +schema-valid default into the tool (a `DefaultDispatchConfigJSON(repo)` function in +`internal/config`, mirroring the established `DefaultFactoryConfigJSON()` at +`config.go:111`), populated with the four label→agent mappings and the +`feature-workflow` the issue proposes, and with `repos` filled from the actual +`owner/name` discovered non-interactively at install time (`git remote` / `gh`). + +The pivotal finding from three independent analyses (Dimensions, Architecture +Elevation, Six-Sigma Gap) and firsthand re-verification: shipping that default is +**necessary but not sufficient**. The default's mappings reference four specialist +agents (`rapid-soldesign-plan`, `rapid-implement`, `ultra-review`, +`rapid-increment`) that a FRESH `agents.json` does not contain (install.go:143 ships +only `manager`+`supervisor`, verified), and `ValidateDispatchConfig` +(`internal/config/dispatch.go:93+`) **hard-fails the entire dispatch cycle** on the +first unknown mapped agent — invoked unconditionally at `internal/cmd/dispatch.go:146` +(verified firsthand). So the default alone would break dispatch on every fresh +factory. The design therefore couples three moves into one systemic change: (1) the +baked-in default builder, (2) non-interactive repo discovery feeding `repos`, and +(3) provisioning the referenced specialists during bootstrap so the default is +**valid-by-construction**, backed by a defense-in-depth dispatcher tolerance and a +golden test that mechanically gates drift. + +The Architecture Elevation verdict is **Frame correct** (the dispatch fields must +exist; deleting `dispatch.json` only relocates them) with **one Frame-lift OFFERED** +— repo self-derivation — which this design adopts as the repo-discovery component. + +## Constraints Respected + +All proposals respect the constraints captured verbatim in `source.md`: +- C-1 (baked-in default): the default ships in the `af` binary as `DefaultDispatchConfigJSON(repo)` (a Go function), not authored by a script — mirrors `DefaultFactoryConfigJSON()` (config.go:111). +- C-2 (bootstrapped at first creation): written by the existing idempotent write-if-absent path in `runInstallInit` (install.go:152) — "first created == this write"; a customer-edited file is never clobbered. +- C-3 (actual repository-name): `repos` is populated from the real `owner/name` discovered at install via `git remote get-url origin` / `gh repo view`, validated, then written. +- C-4 (codebase is source of truth): every claim in this design is anchored to a verified `file:line` (re-read this session); docs used only as search aids. +- C-5 (no doctor fixes / no human as ongoing dependency): the default is valid-by-construction (provisioned specialists), so no `doctor --fix` is needed; the only human touchpoints are genuine failure paths (e.g. unparseable remote), never the happy path. +- C-6 (referenced agents must exist): resolved by provisioning the four specialists during bootstrap (primary) plus a dispatch-loop skip-and-warn tolerance (defense-in-depth); the write path (`af config dispatch set`) stays strict. + +## AC Traceability (REQUIRED) + +| AC ref | Verbatim quote from source.md | Clause breakdown | Addressed by | Verified by | +|--------|-------------------------------|------------------|--------------|-------------| +| AC-1 | "We need to update dispatch.json to include a baked-in default for dispatch with agentfactory" | (i) update dispatch.json (ii) baked-in default (iii) for dispatch | K1 `DefaultDispatchConfigJSON` + K4 wiring at install.go:145 | `TestDefaultDispatchConfigJSON_*` golden test that the shipped default parses + equals expected mappings/workflow (model: `internal/config/dispatch_workflow_test.go`) | +| AC-2 | "they could opt to just start tagging their github issues with tags instead to kick off work without ever needing to visit the manager." | (i) tag issues with labels (ii) kick off work (iii) without visiting the manager | K1 mappings + `trigger_label`; EX dispatcher auto-start (`start_dispatch:true`, install.go:147) + K5 provisioned specialists | Integration test: fresh-init temp factory + provisioned specialists → `ValidateDispatchConfig` passes; `af dispatch --dry-run` matches a `agentic`+`rapid-plan` issue to `rapid-soldesign-plan` | +| AC-3 | "The dispatch.json should be boostrapped when the dispatch.json is first created during initial setup of the repository so we know the repository-name and can include that appropriately in the dispatch.json" | (i) first created (ii) during initial setup (iii) know repo-name (iv) include appropriately | K2 discovery + K3 validation + K4 write-if-absent (install.go:152) | Unit test: temp repo with known `git remote origin` → `af install --init` writes `repos:["/"]`; re-run does not clobber | +| AC-4 | "when we get to the step where we ask the manager,`run af sling --agent \"task description\"`, we should have the work executed autonomously using the step-by-step formula that represents the IDENTITY of that agent and respects the formulas rigid step-by-step process up to the point where human interaction is necessary for next steps" | (i) `af sling --agent` (ii) autonomous via formula (iii) formula = identity (iv) rigid steps (v) up to human gate | EX `af sling --agent` specialist dispatch (sling.go, unchanged) + K5 (routed agents are formula-bearing) | Existing sling/formula behavior (unchanged); K5 ensures the 4 agents resolve as specialists with formulas | +| AC-5 | "all the code branches created should have been pushed as PR's against the main branch without doctor fixes or human interaction (unless absolutely necessary, or in the case of doctor - a `doctor --fix` is acceptable only as a bandaid/fix for legacy broken behaviors, not as an ongoing operational dependency)." | (i) branches → PRs against main (ii) no doctor as ongoing dep (iii) no human (unless necessary) | (i) property of the routed FORMULAS (K5 routes; scoped to formula layer — see Six-Sigma Caveats) (ii) K1+K5 valid-by-construction → no doctor (iii) EX zero-touch happy path | Clause (i) owned by the formula layer (out of this change's scope, traceably noted); (ii)/(iii) by the valid-by-construction default + no-doctor-on-happy-path | +| AC-6 | "The agents should follow the known working formula process that IS their IDENTITY to perform their work so that we have consistent successful outcomes out of each agent. Your mission when addressing any problem scenario is to seek to understand how to achieve this desired outcome with systemic improvements while addressing the scenario." | (i) formula process IS identity (ii) consistent outcomes (iii) systemic improvements | K5 routes to existing formulas unchanged; K1 single-source + K5 provisioning + K7 drift/golden test = repeatable validity; K1–K7 is systemic | Design review: no formula edits; default cannot drift (single-source + golden test); fix is structural not one-off | + +## Architecture Elevation Verdict + +**Verdict: Frame correct (with grounded constraint) — one Frame-lift OFFERED.** + +From `elevation_assessment.md`: the concern ("a fresh dispatch.json is empty/ +placeholder so label-triggering doesn't work out of box") is NOT a symptom of a +removable abstraction. Candidate 0 (delete `dispatch.json`, fold fields into +`agents.json` / derive from the formula registry) FAILS the subtraction gate — it +relocates the same `repos`/`mappings`/`workflows` decisions and breaks the +deliberate L-1 cross-file validation seam (dispatch.go:84-92). So the file must +exist and the right move is to populate the default well (this design's core), +validating the in-frame dimension analysis. + +**The OFFERED lift — adopted by this design:** self-derive the home repo from +`git remote get-url origin` at `af install --init`. Today NO layer in `internal/` +reads a git remote (verified, 0 grep hits); the C-3 placeholder exists only because +`runInstallInit` discards a fact already in the environment (quickstart `cd`s into +the repo at ~:428 before `af install --init` at ~:442). Deriving it eliminates the +entire "operator hand-edits `repos`" category for the single-home-repo case. It is +OFFERED (not required) because empty-`repos` remains a valid fallback and the +multi-repo edge requires keeping `Repos []string` editable. **This design adopts the +lift as component K2** while keeping the field editable and degrading (write empty +`repos`, warn loudly) on an unparseable/missing remote — honoring ADR-014 +(non-interactive) and ADR-017 (read-only, write-if-absent). + +## Problem Statement + +(verbatim from `source.md`): + +> We need to update dispatch.json to include a baked-in default for dispatch with +> agentfactory, so that when someone new to the project uses +> `./quickdocker.sh repo-link` and gets their container bootstrapped and lands in +> `/af/repo` with their newly bootstrapped repository ready to use agentfactory, +> they could opt to just start tagging their github issues with tags instead to +> kick off work without ever needing to visit the manager. +> +> The dispatch.json should be boostrapped when the dispatch.json is first created +> during initial setup of the repository so we know the repository-name and can +> include that appropriately in the dispatch.json [...]. + +## Proposed Design + +### Overview + +Replace the lone inline `dispatch.json` literal in `runInstallInit` with a +single-source `DefaultDispatchConfigJSON(repo string)` builder in `internal/config`; +discover and validate the home repo non-interactively at install time and inject it +into `repos`; and make the default's four referenced specialists present-and-valid +on a fresh factory by provisioning them during bootstrap, with a dispatch-loop +skip-and-warn tolerance and a golden cross-file test as backstops. + +### Key Components + +| Id | Component | Location (verified) | New/Modified | +|----|-----------|---------------------|--------------| +| K1 | `DefaultDispatchConfigJSON(repo string) string` — builds the default from the `DispatchConfig` struct (compile-time field safety), emitting the 4 mappings + `feature-workflow` + `trigger_label:"agentic"` + struct-default cadence | `internal/config` (beside `DefaultFactoryConfigJSON`, config.go:111) | NEW | +| K2 | Repo-discovery helper: `gh repo view --json nameWithOwner` (primary) with `git remote get-url origin` normalization (no-auth fallback); warn-don't-abort | `internal/cmd/install.go` (`runInstallInit`) | NEW | +| K3 | Strict `owner/name` validator (allowlist regex) applied at the write boundary — guards `gh --repo` flag-injection and terminal-escape in the install banner | `internal/cmd` or `internal/config` | NEW | +| K4 | Wire `runInstallInit` starter-config map (install.go:139-148) to call K2→K3→K1; reuse write-if-absent (install.go:150-157) | `internal/cmd/install.go:145` | MODIFIED | +| K5 | Provision the referenced specialists during bootstrap so `agents.json` contains them before the dispatcher runs — invoke `agent-gen-all.sh` directly (or targeted `af formula agent-gen` for the 4), NOT `af install --agents` | `quickstart.sh` `configure_factory` (~:414-471) | MODIFIED | +| K6 | Dispatch-loop unknown-agent tolerance: skip-and-warn instead of hard-fail, scoped to the dispatch-loop caller ONLY (`af config dispatch set` stays strict) | `internal/cmd/dispatch.go` (caller of `ValidateDispatchConfig`, :146) | MODIFIED (defense-in-depth) | +| K7 | Golden + cross-file tests: the shipped default parses (`validateDispatchConfig`) AND cross-validates (`ValidateDispatchConfig`) against the default+provisioned `agents.json` | `internal/config/*_test.go`, `internal/cmd/*_test.go` | NEW | + +### Component Dependency Graph + +(from `dependencies.md`; DAG verified acyclic, topo order K3 → K1 → K2 → K4 → K5 → K6 → K7) + +``` +K4 (install wiring) → K1 (default builder) → EX1 validateDispatchConfig (output must pass struct validation) +K4 → K2 (repo discovery) → K3 (repo validator) +K4 → EX4 write-if-absent guard (reused, unchanged) +K5 (provisioning) → EX3 agent-gen-all.sh / af formula agent-gen +K6 (dispatch tolerance) → EX2 ValidateDispatchConfig (relaxes ONLY the dispatch-loop caller) +K7 (tests) → K1, K4, K5, K6, EX1, EX2 + +Runtime sequencing: K2→K3→K1→K4 at install; K5 must complete before the dispatcher +runs EX2; EX2 consumes K4's written default + K5's provisioned agents.json. +``` +No cycles. **Constraint:** any new validation in `internal/config` MUST NOT import +`internal/formula` (it imports `internal/config` → cycle; dispatch.go:130-136). + +### Interface (from api.md) + +- `func DefaultDispatchConfigJSON(repo string) string` in `internal/config` — + returns the marshaled default; `repo` is the validated `owner/name` (empty string + → `repos:[]` fallback). Built by marshaling a `DispatchConfig` value, never a hand + string, so the field set is compiler-checked. +- No new CLI flags. `runInstallInit` keeps its signature; discovery is internal. + The operator-facing surface is unchanged except an additive install banner line + echoing the discovered repo (escape-safe via K3). + +### Data Model (from data.md — MUST match storage constraints) + +No new storage format, no DB, no schema migration, no SQL. The `DispatchConfig` JSON +schema already exists (dispatch.go:17-45). This change only sets the DEFAULT VALUE +written into the existing file. The intent-corrected default (the source's malformed +literal fixed by struct marshaling): + +```json +{ + "repos": [""], + "trigger_label": "agentic", + "interval_seconds": 300, + "retry_after_seconds": 1800, + "remove_trigger_after_dispatch": true, + "mappings": [ + { "labels": ["rapid-plan"], "source": "issue", "agent": "rapid-soldesign-plan" }, + { "labels": ["rapid-engineer"], "source": "issue", "agent": "rapid-implement" }, + { "labels": ["pr-review"], "source": "pr", "agent": "ultra-review" }, + { "labels": ["pr-iterate"], "source": "pr", "agent": "rapid-increment" } + ], + "workflows": [ + { "label": "feature-workflow", "phases": ["rapid-plan", "rapid-engineer"] } + ] +} +``` + +`notify_on_complete` is **omitted** (it defaults to `"manager"` at runtime via +`validateDispatchConfig`; an explicit value would add a brittle cross-file existence +check — Six-Sigma Gap-7). This default satisfies `validateWorkflows`: each phase is a +single-label mapping (`phaseResolvesAlone`, dispatch.go:256), both phases share +source `"issue"`, and `feature-workflow` collides with neither `trigger_label` nor a +mapping label. + +## Cross-Dimension Trade-offs + +(from `conflicts.md`; every conflict has a resolution — none unresolved) + +| Conflict | Resolution | Rationale | +|----------|-----------|-----------| +| D2×D6 (X) — default references 4 unprovisioned specialists → cross-file validation hard-fails (THE crux) | K5 provision specialists in bootstrap (primary) + K6 dispatcher skip-and-warn (defense-in-depth); Data ships the faithful default unchanged | Provisioning makes the default valid-by-construction (C-5/C-6); tolerance backstops a partial provision | +| D5×D6 (X) — how to resolve C-6 vs write-path strictness | Provision + scope the K6 tolerance to the dispatch-LOOP caller ONLY; `af config dispatch set` keeps strict `ValidateDispatchConfig` | The default is valid-by-construction; the dispatch loop degrades gracefully; human edits still catch typos | +| D1×D5 / D3×D5 (T) — untrusted repo string from a crafted remote | K3 validate `owner/name` at the WRITE boundary before storing/echoing | A bad value never reaches disk, `gh --repo`, or the banner; dispatcher's `strings.Cut` becomes a 2nd line of defense | +| D1×D2 (T) — function output must satisfy `validateDispatchConfig` | Build K1 from the `DispatchConfig` struct (compile-time field safety) + K7 golden test pins content | Struct guarantees well-formedness; golden test guarantees the specific mappings | +| D1×D6 (T) — discovery ordering | Discover→validate→build→write in `runInstallInit` before the starterConfigs map is built (one write) | Wrong ordering would write a placeholder repos | +| D3×D6 (T) — zero-touch path depends on C-6 + auto-start | K5 delivers the C-6 precondition; EX `start_dispatch:true` (install.go:147) auto-starts; U2.1 actionable errors as fallback | The "just tag an issue" promise holds only if specialists exist and dispatch auto-starts | + +## Cross-Perspective Conflicts + +(Step 3.1b — mining across the independent analyses) + +| Finding Source | Finding | Conflicts With | Nature | Resolution | +|----------------|---------|----------------|--------|------------| +| Elevation | Repo self-derivation is a Frame-lift OFFERED (not required) | Dimensions/Integration treat repo discovery (K2) as a core recommended component | tension (status vs adoption) | Adopt K2 as a design component AND document its "offered" elevation status; keep `Repos` editable (multi-repo edge) — both views reconciled | +| Six-Sigma Gap-1 + Integration I3.1 | Provision specialists via `af install --agents` in quickstart | Integration's own quickstart call site | **DIRECT (synthesis-discovered)** — `af install --agents` re-invokes `quickstart.sh` (USING_AGENTFACTORY.md:634), so calling it FROM quickstart RECURSES | **Correction:** K5 invokes `agent-gen-all.sh` directly (or targeted `af formula agent-gen` for the 4), NOT `af install --agents`. Same provisioning effect, no recursion | +| Six-Sigma Gap-5 | AC-5 (PRs without doctor/human) is a property of the 4 FORMULAS, not dispatch.json | AC-5's apparent ownership by this change | scoping | Scope AC-5 clause (i) to the formula layer (necessary-but-not-sufficient); this change routes to those formulas and removes the doctor dependency, but does not (and cannot) verify formula-internal PR-push | +| Six-Sigma Gap-2 | `trigger_label` is a hard query pre-filter (dispatch.go:301) — tagging ONLY a mapping label dispatches nothing | AC-2's natural reading ("just tag rapid-plan") | escape path | Document the two-label requirement (`agentic` + mapping label) in the default and setup docs; widening the query is out of scope (expands blast radius) | +| Decision History (ADR-017) | Write-if-absent never upgrades existing empty-default installs | C-2 "first created" for the installed base | scope boundary | Scope to NET-NEW installs (ADR-017-honest); existing factories opt in via `af config dispatch set` — named, not silently accepted | + +No cross-perspective conflict is left unresolved. Pairs checked: +elevation-vs-dimensions (reconciled), gaps-vs-dimensions (recursion correction + +AC-5 scoping), elevation-vs-gaps (both agree dispatch.json must exist), +decision-history-vs-dimensions (ADR-014/017 shape K2/K4/K5, no contradiction). + +## Decisions Made + +| Decision | Options Considered | Chosen | Rationale | Reversibility | +|----------|--------------------|--------|-----------|---------------| +| Default builder location | Inline literal (status quo) / `DefaultDispatchConfigJSON()` in `internal/config` | `DefaultDispatchConfigJSON(repo)` | Mirrors `DefaultFactoryConfigJSON` single-source (config.go:111); removes the last inline-literal drift surface (issue #371 Gap-6) | Easy | +| Default content | 4 mappings + `feature-workflow` (D2.2-A) / mappings-only (D2.2-C) / manager-supervisor-only (D2.2-B, rejected: fails AC-2) | 4 mappings + `feature-workflow`, intent-corrected from the struct | Maximally faithful to source intent; D2.2-C is the conservative fallback if the workflow surface is deemed risky | Easy | +| Repo name | hand-authored placeholder (status quo) / `--init --repo` flag (I2.2) / self-derive in Go (I2.1) | Self-derive via `gh`/`git remote` in `runInstallInit`, validated | Adopts the elevation Frame-lift; ADR-014 prefers the Go path self-sufficient over a script-fed flag | Easy | +| `notify_on_complete` | explicit `"manager"` (source) / omit | Omit (defaults to manager) | Strictly smaller validation surface; identical runtime behavior (Gap-7) | Easy | +| C-6 provisioning mechanism | `af install --agents` in quickstart (I3.1 as written) / `agent-gen-all.sh` direct / targeted `af formula agent-gen` | `agent-gen-all.sh` direct (or targeted agent-gen) | `af install --agents` re-invokes quickstart → RECURSION (USING:634); direct invocation provisions specialists without recursion | Moderate | +| Dispatcher tolerance | none (hard-fail) / skip-and-warn everywhere / skip-and-warn dispatch-loop only | Skip-and-warn scoped to the dispatch loop; write path strict | Graceful degradation on partial provision without weakening the human write-path typo check | Moderate | +| Installed base | blind overwrite (rejected, ADR-017) / scope to net-new | Net-new installs; existing factories opt in | ADR-017 forbids clobbering customer-edited config | n/a (policy) | + +## Risk Registry + +| Risk | Severity | Likelihood | Mitigation | Owner | Source | +|------|----------|-----------|------------|-------|--------| +| Default references 4 specialists absent from fresh agents.json → `ValidateDispatchConfig` hard-fails dispatch cycle | HIGH | Certain (without K5) | K5 provision specialists in bootstrap + K6 dispatch-loop skip-and-warn | Integration | dimensions / elevation / gap | +| `af install --agents` from quickstart recurses | HIGH | Certain (if K5 used `--agents`) | K5 invokes `agent-gen-all.sh` directly / targeted `agent-gen`, never `af install --agents` | Integration | synthesis cross-perspective | +| Repo discovery fails (no/non-GitHub/non-`origin` remote) → empty `repos`, dispatcher silently skips (looks like benign "not configured", dispatch.go:1330) | HIGH | Possible | K2 warn-don't-abort writes loadable default; surface a distinct fail-loud message (ADR-014); `af up` pre-flight warns (K8 observability, below) | Security/UX | gap-3 / gap-8 | +| Crafted remote URL injects bad `repos` (gh flag-injection / terminal escape in banner) | MED | Low | K3 strict `owner/name` allowlist at the write boundary | Security | security | +| K1 output fails `validateDispatchConfig` (drift) | MED | Low | Build from struct + K7 golden test | API/Data | conflicts | +| K6 over-broad relaxation weakens `af config dispatch set` strictness | MED | Low | Scope tolerance to dispatch-loop caller only; config_set.go unchanged | Integration/Security | conflicts | +| Future formula rename breaks the default's agent references | MED | Low | ADR-008 drift test ties formula names to embedded files; K7 cross-file test pins the default | Integration | dependencies | +| User tags only a mapping label (not `agentic`) → nothing dispatched, no signal | MED | Likely | Document two-label requirement in default + docs (query-widening out of scope) | UX | gap-2 | +| Fresh-install dispatch validation failure is low-visibility (loops in tmux pane) | MED | Possible | K8 (below): `af up`/`af dispatch status` surface a "config invalid: " state; pre-flight warns, never aborts `af up` | UX/Integration | gap-8 | + +(K8 = observability hardening: a pre-flight `ValidateDispatchConfig` at `af up`/ +`af dispatch status` that warns loudly and distinguishes "empty by design" from +"discovery failed" from "references unprovisioned agents". Feasible; additive to the +existing `af dispatch status --json` contract.) + +## Six-Sigma Caveats + +| Gap | Category | Impact | Feasibility | Constraint | +|-----|----------|--------|-------------|-----------| +| PR-push (AC-5) is a formula-layer property | scope gap | MED | Partially feasible | Full AC-5 verification is E2E/live-gh, out of config-bootstrap scope; this change routes correctly and removes the doctor dependency, but formula-internal push is verified separately | +| "Tagged a mapping label but not `agentic`" miss | escape path | HIGH (UX) | Partially feasible | Diagnosing the miss is architecturally ceilinged — the trigger-label query never fetches the item; only documentation or query-widening helps, the latter out of scope | +| Installed base not auto-migrated | version drift | MED | Partially feasible | ADR-017 forbids clobbering customer config; existing factories opt in via `af config dispatch set` | +| Non-`origin`/non-GitHub remote | dependency fragility | LOW | Infeasible to auto-resolve | Legitimately cannot derive `org/repo`; MUST fail loud (ADR-014), operator supplies the repo | + +**Practical ceiling:** the achievable six-sigma target for THIS change is "the +shipped default is valid-by-construction, the repo is correctly discovered or fails +loud, and the validity is mechanically test-gated against drift" — Gaps 1, 3, 4, 7, 8 +closed; Gaps 2, 5, 6 named-and-scoped. The end-to-end autonomous OUTCOME (AC-5) is a +multi-component property; this design owns the config-and-provisioning component and +says so. Residual accepted risk: (a) exotic remotes fail discovery and require an +explicit repo; (b) AC-5 is a downstream-formula guarantee verified separately; +(c) existing factories are excluded by design. + +## Implementation Plan + +### Phase 1: Single-source default builder + repo discovery (Effort: M) +**Deliverables:** +1. K1 `DefaultDispatchConfigJSON(repo string) string` in `internal/config` (build from `DispatchConfig` struct; omit `notify_on_complete`; `remove_trigger_after_dispatch:true`). +2. K2 repo-discovery helper in `runInstallInit` (`gh repo view --json nameWithOwner` → fallback `git remote get-url origin`), warn-don't-abort. +3. K3 strict `owner/name` validator applied before write/echo. +4. K4 replace install.go:145 literal with `config.DefaultDispatchConfigJSON(discoveredRepo)`; reuse write-if-absent. + +**Source ACs satisfied**: AC-1, AC-3 (and the substrate of AC-2). +**Dependencies**: none new (uses existing struct, write path). +**Risks addressed**: drift (single-source), injection (K3), discovery failure (warn-don't-abort). +**Six-Sigma gaps closed**: Gap-7 (omit notify), partial Gap-3 (discovery exists). + +**Phase acceptance criteria:** +- [ ] `DefaultDispatchConfigJSON("org/repo")` output passes `validateDispatchConfig` (unit test). +- [ ] In a temp repo with a known `git remote origin`, `af install --init` writes `dispatch.json` with `repos:["org/repo"]` and the 4 mappings + `feature-workflow`; a re-run does not clobber it. +- [ ] A crafted/garbage remote URL is rejected by K3 and yields `repos:[]` + a loud warning, not a malformed write. + +### Phase 2: C-6 resolution — provision specialists + dispatcher tolerance (Effort: M) +**Deliverables:** +1. K5 bootstrap provisioning in `quickstart.sh` `configure_factory` via `agent-gen-all.sh` (or targeted `af formula agent-gen` for the 4 referenced agents) — explicitly NOT `af install --agents` (recursion). +2. K6 dispatch-loop skip-and-warn on unknown-agent mappings, scoped to the dispatcher caller of `ValidateDispatchConfig` (`internal/cmd/dispatch.go:146`); `af config dispatch set` unchanged (strict). +3. K8 observability: pre-flight validation surfaced at `af up` / `af dispatch status` (warn, never abort). + +**Source ACs satisfied**: AC-2, AC-4, AC-6 (and the no-doctor clause of AC-5). +**Dependencies**: Phase 1's default (the mappings that name the specialists). +**Risks addressed**: hard-fail-on-fresh-factory (HIGH), recursion (HIGH), low-visibility failure (MED). +**Six-Sigma gaps closed**: Gap-1, Gap-8; Gap-2 documented. + +**Phase acceptance criteria:** +- [ ] After a fresh bootstrap (init + K5 provisioning), `agents.json` contains the 4 specialists and `ValidateDispatchConfig(default, agents)` returns nil. +- [ ] With one mapped agent absent, the dispatch LOOP skips that mapping with a warning and still dispatches the others; `af config dispatch set` with the same config still exits non-zero (strict). +- [ ] `af up`/`af dispatch status` reports a distinct, actionable state for "references unprovisioned agents" vs "empty by design" vs "discovery failed". + +### Phase 3: Drift/golden test gate + docs (Effort: S) +**Deliverables:** +1. K7 golden test: the shipped `DefaultDispatchConfigJSON()` output parses (`validateDispatchConfig`) AND cross-validates (`ValidateDispatchConfig`) against the default+provisioned `agents.json` (model: `internal/config/dispatch_workflow_test.go`). +2. Docs: the two-label requirement (`agentic` + mapping/workflow label) for AC-2; the net-new-install scope for the installed base. + +**Source ACs satisfied**: AC-1 (drift-gated), AC-6 (systemic + repeatable). +**Dependencies**: Phases 1 and 2 (the default + provisioned agents). +**Risks addressed**: future drift (formula rename / label edit), AC-2 mis-use, installed-base confusion. +**Six-Sigma gaps closed**: Gap-4; Gap-2 and Gap-6 named-and-scoped. + +**Phase acceptance criteria:** +- [ ] A test fails CI if any future edit makes the shipped default fail struct OR cross-file validation against the default+provisioned agents.json. +- [ ] Setup docs state the two-label requirement and that the default ships for net-new installs (existing factories opt in via `af config dispatch set`). + +## Appendix: Analysis Artifacts +- [source.md](source.md) +- [verification.md](verification.md) +- [codebase-snapshot.md](codebase-snapshot.md) +- Dimension analyses: api.md, data.md, ux.md, scale.md, security.md, integration.md +- [audit.md](audit.md) +- [conflicts.md](conflicts.md) +- [dependencies.md](dependencies.md) +- [elevation_assessment.md](elevation_assessment.md) +- [six_sigma_gaps.md](six_sigma_gaps.md) +- [verification-report.md](verification-report.md) +- [synthesis-checklist.md](synthesis-checklist.md) diff --git a/.designs/73/design-refinement-progress.md b/.designs/73/design-refinement-progress.md new file mode 100644 index 0000000..068d53d --- /dev/null +++ b/.designs/73/design-refinement-progress.md @@ -0,0 +1,27 @@ +# Rapid Design Refinement Progress: New dispatch workflow default to be included with agentfactory + +**Status**: In Progress +**Issue**: https://github.com/stempeck/agentfactory/issues/73 +**Started**: 2026-06-28T16:35:12Z + +## Agents +| Role | Agent | Status | Started | Completed | +|------|---------|--------|---------|-----------| +| Analyst | rootcause-all | Dispatched | 2026-06-28T16:44Z | - | +| Designer | design-v7 | Dispatched | 2026-06-28T16:44Z | - | +| Impl | design-plan-impl | Not dispatched | - | - | + +**Beads:** analyst=af-8fe67d60, designer=af-cf3cee4b, gate=af-bec549ca (gate blocks analyst bead close → premature-af-done detection) + +## Cross-Review Progress +| Round | Exchange | Status | Timestamp | +|-------|----------|--------|-----------| +| 1 | Analyst reviews design-doc | Pending | - | +| 1 | Designer incorporates HIGH/CRIT | Pending | - | + +## Implementation Plan +| Stage | Status | Timestamp | +|-------|--------|-----------| +| PR opened | Pending | - | +| implementation-plan agent dispatched | Pending | - | +| implementation_plan_outline.md created | Pending | - | diff --git a/.designs/73/elevation_assessment.md b/.designs/73/elevation_assessment.md new file mode 100644 index 0000000..a451364 --- /dev/null +++ b/.designs/73/elevation_assessment.md @@ -0,0 +1,119 @@ +# Architecture Elevation Assessment + +**Date**: 2026-06-28 +**Target**: .designs/73/source.md +**Mode**: advisory +**Verdict**: Frame correct (with grounded constraint) — one Frame-lift OFFERED (C-3 repo-name self-derivation) + +--- + +## Phase 0 — Concerning Abstractions + +The "concern" of issue #73 is: *a fresh repo's `dispatch.json` is created empty/placeholder, so the autonomous label-trigger path does not work out-of-box and requires a human to hand-edit (repo name, mappings) or run `doctor --fix`.* Phase 0 interrogates whether that concern exists because of an abstraction that could be deleted, rather than configured. + +| Abstraction | file:line | Would removal eliminate the concern? (YES/NO + why) | Simpler system lacking this pattern | +|-------------|-----------|------------------------------------------------------|--------------------------------------| +| `dispatch.json` as a standalone on-disk config file separate from `agents.json` | `internal/config/dispatch.go:17-27` (`DispatchConfig`); written at `install.go:145` | **NO.** The dispatcher still must know WHICH repos to poll, WHICH labels map to WHICH agents. That data must live somewhere. Deleting the file just relocates the same fields; the concern (it's empty on a fresh repo) moves with it. | git's `.git/config` — repo identity is self-derived from `[remote "origin"] url`, not a separately-authored file. (Verified concept; `git config --get remote.origin.url`.) This is the relevant lift for the `repos` FIELD only (Candidate 1), not for the whole file. | +| `Repos []string` as a hand-authored field requiring the org/repo literal | `internal/config/dispatch.go:19`; consumed at `dispatch.go:172` (`for _, repo := range dispatchCfg.Repos`) | **PARTIALLY (this is the strongest candidate).** The repo the factory lives in is already determinable from `git remote get-url origin` at the CWD `af install --init` runs in (quickstart.sh `cd "$repo_dir"` at quickstart.sh:428 happens BEFORE `af install --init` at quickstart.sh:442). The *placeholder/manual-edit* sub-concern (C-3) exists ONLY because `runInstallInit` discards an available fact. **Verified: no git-remote read exists anywhere in `internal/` today** (grep for `git remote`/`remote.origin`/`RepoName` returned 0 hits). So the substrate of the C-3 concern is a *missing derivation*, and adding it eliminates a category of manual edit. | git: `git status` never asks you to type the repo name — it reads `.git`. The dispatcher could self-derive the single home repo the same way. | +| The split between `validateDispatchConfig` (struct-level) and `ValidateDispatchConfig` (cross-file agents.json) | `internal/config/dispatch.go:140-185` and `:93-138` | **NO.** This split is a deliberate seam (L-1, comment at dispatch.go:84-92) so the CLI write path and the dispatcher share one cross-file validator. It does not CREATE the empty-default concern; it is the mechanism that would REJECT a bad default. Removing it would make the concern *worse* (silent dispatch of unknown agents). | n/a — removal regresses. | +| `Mappings`/`Workflows` referencing agent NAMES by string rather than the agents being implied by the formula registry | `dispatch.go:30-45`; cross-checked at `dispatch.go:101` (`agents.Agents[m.Agent]`) | **NO.** Name-based binding is intentional indirection: one label → one agent, decoupled from formula internals (Workflow doc-comment dispatch.go:37-41, "mappings[] owns the agent binding, single source of truth, no formula edits"). The concern is that the *referenced agents are absent from a fresh `agents.json`*, which is a provisioning-ordering problem (Candidate 2), not an abstraction that can be deleted. | n/a within domain. | + +**Phase 0 conclusion:** The concern is NOT a pure artifact of a removable abstraction. The dispatch fields must exist somewhere. EXACTLY ONE sub-concern — the C-3 repo-name placeholder — is a removable manual-decision category, because the fact is already present in the environment (git remote) and is currently discarded. That becomes Candidate 1 (Frame-lift candidate). + +--- + +## Ownership Map + +Where the relevant policy ("what does a fresh factory dispatch, against which repo, validated how") is currently enforced. Built from source, not summaries. + +| Layer | Location (file:line) | Enforcement | Policy carried | +|-------|----------------------|-------------|----------------| +| Install-time default (the concern's origin) | `internal/cmd/install.go:145` — inline literal `{"repos":[],...,"mappings":[],...}` | mechanical (write-if-absent, install.go:150-156) | "A fresh dispatch.json has EMPTY repos, EMPTY mappings, no workflows." This is the literal substrate of the concern. | +| Repo identity | **NOWHERE** in `internal/` (verified: 0 grep hits for git-remote read) | absent | "Which repo does this factory poll?" — today hand-authored into `Repos`; never derived. `runInstallInit` takes no repo arg (install.go, verified). | +| Repo consumption | `internal/cmd/dispatch.go:172` (`for _, repo := range dispatchCfg.Repos`) | runtime | Each `Repos[]` entry is queried via `gh` (dispatch.go:178, :194). Empty `Repos` ⇒ loop body never runs ⇒ dispatcher polls nothing. | +| Cross-file agent existence | `internal/config/dispatch.go:93-138` `ValidateDispatchConfig`; invoked at `internal/cmd/dispatch.go:146` and `internal/cmd/config_set.go:89` | runtime (detects & **blocks** — `return err`) | "Every `mappings[].agent` MUST exist in agents.json." On a fresh install (agents.json = manager+supervisor only, install.go:143), the proposed 4-specialist default makes dispatch.go:146 **hard-fail the whole cycle**. | +| Struct-level shape | `internal/config/dispatch.go:140-185` `validateDispatchConfig`; default-fill interval/retry/notify/source | runtime (detects & blocks at load) | "repos≥1, trigger_label set, mappings≥1, workflow phases resolvable" (dispatch.go:142-171). The CURRENT empty default would FAIL these — but install writes the literal without loading it, so it is never checked at install time. | +| Default-as-Go-function pattern | `internal/config/config.go:108-124` `DefaultFactoryConfigJSON()` | mechanical (single-source; `TestDefaultFactoryConfigJSON_*` pins it) | "factory.json default is built from in-code constants, no on-disk literal to drift." dispatch.json is the ONLY starter config still an inline literal (install.go:145 vs :142) — duplicated-ownership candidate. | +| Provisioning order | `quickstart.sh:442` (`af install --init`) → `:451`,`:463` (`af install manager`/`supervisor`) only | instruction (shell script) | A fresh factory provisions ONLY manager+supervisor; the 4 dispatch specialists are NOT provisioned by the bootstrap path. | + +**Duplicated-ownership flags:** +1. The default config literal at install.go:145 duplicates the schema/intent that `internal/config/dispatch.go` owns — every other starter config (`factory.json`) was migrated to a `Default…JSON()` function (config.go:111); dispatch.json was left behind. **N>1 carriers of the same invariant** → flag (this is the AC-1 design target, not an elevation). +2. Repo identity is owned by NO layer (the gap that produces the C-3 placeholder concern). + +--- + +## Elimination Candidates + +Candidate 0 is the Phase-0 frame-removal hypothesis (delete the abstraction so the concern cannot exist). Candidate 1 is the strongest *field-level* lift. Candidate 2 documents the provisioning sub-concern. + +### Candidate 0 — Delete `dispatch.json` as a separate config; fold dispatch fields into `agents.json` / derive entirely from the formula registry +- **Altitude**: abstraction removal +- **Target boundary**: `internal/config/dispatch.go` (`DispatchConfig`), `install.go:145` +- **Invariant carried**: "which repos/labels/agents the dispatcher acts on" +- **Deletion ledger**: would remove `DispatchConfig` (dispatch.go:17-27), the install.go:145 literal, `LoadDispatchConfig`/`SaveDispatchConfig` (dispatch.go:48-82). +- **Addition ledger**: would ADD equivalent fields to `agents.json` schema + new loader/validator + migration of `dispatch.go:134,146,1273,1327` and `config_set.go` callers; ADD re-derivation logic to infer labels→agents from formulas. +- **Category elimination**: NO. The label→agent→repo decisions still must be made at runtime; they just move files. The empty-on-fresh-repo concern moves with them. +- **Subtraction gate**: Dim 1 (artifacts) deletions ≈ additions, **not** deletions>additions (net relocation). Dim 2 (categories) NO. **FAILS subtraction gate.** + +### Candidate 1 — Self-derive the home repo from `git remote get-url origin` at `af install --init`, eliminating the hand-authored `Repos` literal for the common single-repo case ★ STRONGEST +- **Altitude**: schema invariant + category elimination (turn a hand-authored field into a derived one) +- **Target boundary**: `internal/cmd/install.go` `runInstallInit` (writes dispatch.json at install.go:145) + a new `internal/config` derivation (mirroring `DefaultGitIdentity()`, config.go:89) +- **Invariant carried**: "the repo this factory polls == the repo it was installed into." Currently NO layer owns this (Ownership Map row 2). +- **Deletion ledger**: removes the user-decision category "type your org/repo into `Repos`" and removes the failure mode where `repos:[]` ⇒ dispatcher polls nothing (dispatch.go:172 loop is empty). Removes the C-3 placeholder `"org/repository"` as a manual-edit surface. +- **Addition ledger**: ADDS one git-remote read (e.g. `git remote get-url origin`, parsed to `org/repo`) called from `runInstallInit`; ADDS a `DefaultDispatchConfigJSON(repo string)`-style function in `internal/config` (mirrors `DefaultFactoryConfigJSON`, config.go:111); ADDS a unit test (temp repo w/ known origin → `repos:["org/repo"]`). Per ADR-014: on missing/unparseable remote it MUST fail-loud-with-flag or write empty `repos`, **never prompt**. +- **Category elimination**: **YES.** Eliminates the entire runtime category "operator manually edits `repos`" for the single-home-repo case — the dispatcher self-grounds in the repo it lives in, exactly like `git status` self-grounds in `.git`. +- **Subtraction gate**: Dim 2 (categories) **TRUE** → **PASSES** (a category of manual decision is eliminated even though artifact count rises slightly). + +### Candidate 2 — Provision the 4 default-dispatch specialists into the fresh `agents.json` (or make the dispatcher SKIP, not hard-fail, unknown-agent mappings) +- **Altitude**: single-interface invariant (align the default's referenced agents with what install provisions) +- **Target boundary**: `internal/cmd/install.go:143` (default agents.json) and/or `internal/config/dispatch.go:100-104` (the hard-fail loop) and/or `internal/cmd/dispatch.go:146` +- **Invariant carried**: "a shipped default dispatch.json must not reference agents that a fresh agents.json lacks" (else dispatch.go:146 returns err and the whole cycle dies). +- **Deletion ledger**: if the dispatcher SKIPPED unknown-agent mappings instead of erroring, it removes a category of total-cycle-failure. But that WEAKENS a deliberate validator (ValidateDispatchConfig, L-1 single-source) → see Self-Challenge Q4. +- **Addition ledger**: ADDS 4 agent entries to the install.go:143 literal (or a default-agents function), OR ADDS skip-and-warn logic at dispatch.go:146. +- **Category elimination**: NO new abstraction removed; this is a *correctness/consistency fix* the design MUST make (C-6), but it is not an elevation — it adds config, it doesn't subtract a concern. +- **Subtraction gate**: Dim 1 additions>deletions; Dim 2 NO (skip-variant eliminates a failure category but at the cost of weakening validation). **FAILS subtraction gate as an elevation** (belongs in the design as a required correctness item, owned by Dimensions/Data, not as a frame-lift). + +--- + +## Self-Challenge + +Surviving candidate after the subtraction gate: **Candidate 1** (Candidate 0 failed the gate; Candidate 2 is a correctness item, not an elevation). I attempt to reject Candidate 1 on all five questions. Candidate 0 is also audited to formally settle the "Frame artificial" question. + +### Candidate 1 — repo self-derivation grounding audit + +| # | Question | Rejection succeeds? | Grounding provided | Grounding passes? | +|---|----------|---------------------|--------------------|-------------------| +| Q1 | Does the target boundary exist in this codebase's topology? | NO | `runInstallInit` exists (install.go, writes dispatch.json at :145); `internal/config` already houses derived-default funcs (`DefaultGitIdentity` config.go:89, `DefaultFactoryConfigJSON` config.go:111). quickstart.sh `cd "$repo_dir"` (:428) runs BEFORE `af install --init` (:442), so a git remote is present in CWD. | YES — boundary is real and the env fact is available. Rejection FAILS. | +| Q2 | Does the language/framework/runtime support the proposed invariant? | NO | Go can shell `git remote get-url origin` (the codebase already shells `gh` throughout dispatch.go, e.g. queryGitHubIssues). Parsing org/repo from a remote URL is trivial string work. | YES — fully supported. Rejection FAILS. | +| Q3 | Does an ADR or user-signed decision decline this migration? | NO (it CONSTRAINS, does not decline) | **ADR-014** (no interactive prompting in `cmd/`/`internal/`): the derivation MUST be non-interactive — read git remote, else fail-loud-with-flag, never prompt. **ADR-017** (no deleting customer data; reading git remote is read-only; writing inside `.agentfactory/` is permitted; preserve write-if-absent so a customer-edited dispatch.json is never clobbered — install.go:150-156 already does). **Neither ADR declines the lift**; both shape its non-interactive, idempotent form. | YES — ADRs permit it under a stated shape. Rejection FAILS. | +| Q4 | Does the lift create a worse defect class? | PARTIAL | Risk: a multi-repo factory, or a factory whose origin ≠ the repo to poll, would get a wrong/narrow `repos`. Mitigation: write the derived single repo as the *default*, keep `Repos []string` editable (don't delete the field), and on no/unparseable remote write empty `repos` (today's behavior) — strictly a superset of current capability. So no NEW defect class for existing users; only an added correct-by-default for the common case. | Rejection PARTIALLY succeeds (multi-repo edge) but is fully mitigated by keeping the field editable → counts as a **constraint**, not a kill. | +| Q5 | Does the lift step OUTSIDE the concern's named domain (dispatch-config bootstrap)? | NO | Reading the home repo's git remote at install time to populate the dispatch `repos` field is squarely inside "bootstrap dispatch.json with the repository-name" — it IS the literal text of C-3 / AC-3. | YES — in-domain. Rejection FAILS. | + +**Result:** 4 of 5 rejections FAIL; Q4 succeeds only as a mitigated edge-case constraint. Candidate 1 survives as a **Frame-lift OFFERED** (≥1 rejection counts → "offered", not "required"). It is offered, not mandated, because today's empty-`repos` behavior is a valid fallback and the multi-repo edge requires the field to stay editable. + +### Candidate 0 — frame-removal grounding audit (to settle "Frame artificial") + +| # | Question | Rejection succeeds? | Grounding provided | Grounding passes? | +|---|----------|---------------------|--------------------|-------------------| +| Q1 | Target boundary exists? | YES (rejection succeeds) | The dispatch fields would have to be re-homed into `agents.json` (config.go) or re-derived from formulas — but that boundary does NOT today carry repo/label/trigger semantics; building it is net-new, not a deletion. | Rejection SUCCEEDS. | +| Q4 | Worse defect class? | YES | Folding dispatch into agents.json couples two independently-validated configs (dispatch.go vs agents.json loaders) and breaks the deliberate L-1 cross-file seam (dispatch.go:84-92). | Rejection SUCCEEDS. | + +**Result:** Candidate 0 fails the subtraction gate AND its rejections succeed. The dispatch.json abstraction is NOT artificial — the concern is real and the file must exist. **Frame is NOT artificial.** + +--- + +## Verdict + +**Frame correct (with grounded constraint), and one Frame-lift OFFERED.** + +Rationale, grounded: + +1. **The frame is correct, not artificial.** The concern ("a fresh dispatch.json is empty/placeholder so autonomous label-triggering doesn't work out of box") is NOT the symptom of a removable abstraction. The dispatch fields (`Repos`, `Mappings`, `Workflows`, `TriggerLabel`) must live somewhere; deleting `dispatch.json` (Candidate 0) only relocates them (subtraction gate FAILS; Q1/Q4 rejections succeed). The right shape is therefore to *populate* the default well, which is exactly AC-1/AC-2/AC-3. This is a design-WITHIN-the-frame problem, validating Phase 2's dimension analysis rather than redirecting it. + +2. **One genuine elevation is OFFERED — Candidate 1 (repo self-derivation).** Today NO layer owns repo identity (Ownership Map row 2: 0 grep hits for any git-remote read in `internal/`). The C-3/AC-3 placeholder (`"org/repository" <-update this...`) exists ONLY because `runInstallInit` discards a fact that is already in the environment (quickstart.sh `cd "$repo_dir"` at :428 precedes `af install --init` at :442, so `git remote get-url origin` is available). Deriving it ELIMINATES the entire runtime category "operator hand-edits `repos`" for the single-home-repo case (subtraction gate Dimension 2 = TRUE). It survives 4/5 self-challenge rejections; ADR-014 and ADR-017 (snapshot Decision History) permit it under a non-interactive, read-only, write-if-absent shape, which the existing `runInstallInit` already satisfies. It is OFFERED rather than REQUIRED because empty-`repos` remains a valid fallback and the multi-repo edge (Q4) requires keeping `Repos []string` editable — so the lift must *add a smart default*, never *remove the field*. + +3. **Required correctness item flagged for the synthesis (not an elevation): Candidate 2.** The proposed default references 4 specialist agents (`rapid-soldesign-plan`, `rapid-implement`, `ultra-review`, `rapid-increment`), but a fresh `agents.json` (install.go:143) has only `manager`+`supervisor`. The dispatcher cross-validates at `dispatch.go:146` via `ValidateDispatchConfig`, which **`return err`s and hard-fails the entire cycle** when a mapped agent is missing (dispatch.go:100-104). So the proposed default, shipped as-is against a fresh factory, would BREAK dispatch entirely until those agents are provisioned. The design MUST either (a) provision the 4 specialists into the default `agents.json`/bootstrap, or (b) have the dispatcher skip-and-warn on unknown-agent mappings (which weakens the L-1 validator — see Q4). This is owned by the Data/Integration dimensions and is the single highest-severity correctness constraint for AC-2/AC-4/C-6; it is NOT a frame elevation (it adds config; it subtracts no concern). + +4. **Single-source constraint (not an elevation, but a duplicated-ownership flag):** dispatch.json is the ONLY starter config still an inline literal at install.go:145, whereas `factory.json` was migrated to `DefaultFactoryConfigJSON()` (config.go:111, issue #371 Gap-6). The default should be expressed as a `DefaultDispatchConfigJSON(repo string)` function in `internal/config` (mirroring the established pattern) so the on-disk default cannot drift from the schema, and ADR-008's drift discipline applies to any formula names it references. + +The single strongest elimination candidate is **Candidate 1: self-derive the home repo from `git remote get-url origin` at `af install --init`**, eliminating the C-3 manual-edit category while honoring ADR-014 (non-interactive) and ADR-017 (read-only/write-if-absent). diff --git a/.designs/73/integration.md b/.designs/73/integration.md new file mode 100644 index 0000000..25d1fad --- /dev/null +++ b/.designs/73/integration.md @@ -0,0 +1,221 @@ +# D6 — Integration (integration.md) + +Owner of (verification.md): EVERY AC and EVERY constraint lists Integration as an +owner — this is the central dimension. It governs how the baked-in default fits the +existing install/dispatch system, which call sites change, and which tests change. + +This dimension surfaces THE pivotal finding of the whole design (verified this +session): the proposed default references 4 specialists that a FRESH bootstrap does +NOT provision. `quickstart.sh` runs only `af install --init`, `af install manager`, +`af install supervisor` (quickstart.sh:442, 451, 463, verified) — it NEVER runs +`af install --agents`. So on a fresh setup, agents.json has only manager+supervisor +(install.go:143, verified), and the default's mappings reference absent agents → +`ValidateDispatchConfig` fails (dispatch.go:100-104, verified) the moment the +dispatcher starts. Resolving this (C-6) is the integration crux. + +--- + +## I0. Verified integration map (call sites that change / are touched) + +| Site | File:line (verified) | Change | +|------|----------------------|--------| +| Starter-config map | `internal/cmd/install.go:139-148` | Replace the inline `dispatch.json` literal (:145) with `config.DefaultDispatchConfigJSON(repo)` | +| Repo discovery | `internal/cmd/install.go` (`runInstallInit`, :97+) | NEW: discover `owner/name` before building starter configs | +| Default builder | `internal/config/dispatch.go` or `config.go` | NEW: `DefaultDispatchConfigJSON(repo)` (mirrors `DefaultFactoryConfigJSON`, config.go:111) | +| Bootstrap specialist provisioning | `quickstart.sh` `configure_factory` (:414-471) | C-6: add specialist provisioning, OR rely on a dispatcher-tolerance change | +| Dispatcher cross-file check | `internal/config/dispatch.go:93-138` (`ValidateDispatchConfig`) | OPTIONAL (SEC2.2): skip-and-warn on unknown agent instead of failing | +| Write path (unchanged) | `internal/cmd/config_set.go:67-99` | No change — already runs cross-file validation before writing (verified) | + +The write path `af config dispatch set` already does the right cross-file check +(config_set.go:85-91, verified) and is the operator's repair command — no change +needed there. + +--- + +## I1. WHERE the default is produced and written + +### Option I1.1 — Build via `DefaultDispatchConfigJSON(repo)` in `internal/config`, called from `runInstallInit` — RECOMMENDED + +Mirror the established `factory.json` pattern: `runInstallInit` discovers the repo, +calls `config.DefaultDispatchConfigJSON(repo)`, and uses the result as the +`dispatch.json` value in the `starterConfigs` map (install.go:139-148, verified). +The existing write-if-absent loop (install.go:150-157, verified) writes it. + +- Trade-offs: One call site changes (the map literal), one new function added. + Reuses the entire existing write/idempotency machinery. Honors C-2 (written at + first-create by the same path) and the single-source pattern (codebase-snapshot + Decision History, issue #371 Gap-6). +- Reversibility: Easy. +- Constraints: satisfies C-1, C-2, AC-1, AC-3, complies with ADR-008. Recommended. + +### Option I1.2 — Write the default from quickstart.sh via `af config dispatch set` after init — REJECTED + +Have quickstart pipe a JSON default into `af config dispatch set` (config_set.go, +verified) after `af install --init`. + +- REJECTED: violates [C-1], [C-2]. C-1 requires the default baked into the TOOL + (Go), not authored by a bootstrap script; C-2 requires it written when + dispatch.json is FIRST created during init, not by a later command. A + quickstart-authored default also reintroduces a doc/script that can drift from + the schema (the opposite of the single-source pattern). Reject. + +--- + +## I2. Repo discovery integration point (ADR-014 compliant) + +### Option I2.1 — Discover inside `runInstallInit` (Go), non-interactive, before building starter configs — RECOMMENDED + +`runInstallInit` runs after quickstart `cd`s into the repo (quickstart.sh:428, 442, +verified), so the git remote is present in CWD. Discover via `gh repo view` / `git +remote` (D1/A2), validate (D5/SEC1.1), inject into the default. + +- Trade-offs: Keeps discovery in the Go path where the install logic already lives, + non-interactively (ADR-014). The git remote is guaranteed present at this point + (codebase-snapshot §6 confirms quickstart cd's in before init). Best-effort: on + failure, degrade (A3.1). +- Reversibility: Easy. +- Constraints: satisfies C-3, complies with ADR-014 (no prompt), ADR-017 + (read-only). Recommended. + +### Option I2.2 — Pass the repo to `af install --init` as a new flag/arg from quickstart — REJECTED + +Add `af install --init --repo ` and have quickstart compute and pass it. + +- REJECTED: reverses [Decision History: install.go #58 / runInstallInit takes NO + repo argument] without need, and pushes discovery into a bootstrap SCRIPT. + codebase-snapshot §6 states "runInstallInit takes NO repo argument and does NO + git-remote lookup." Adding a flag is more surface than I2.1, and ADR-014 prefers + the Go path to be self-sufficient (discover or fail-loud) rather than depending + on a script to feed it. The Go path can discover the remote itself (I2.1) since + CWD is the repo. (Not strictly forbidden, but strictly worse than I2.1.) + +--- + +## I3. THE C-6 RESOLUTION — making the default's agent references resolvable on a fresh factory + +This is the decisive integration choice. Three resolutions; the design needs at +least one (and the recommended posture combines I3.1 + I3.2 as primary + +defense-in-depth). + +### Option I3.1 — Provision the referenced specialists during bootstrap (run `af install --agents` in quickstart) — RECOMMENDED (primary) + +Add a step to `quickstart.sh` `configure_factory` (after manager/supervisor +provisioning, :448-470) that runs `af install --agents`, which regenerates+installs +EVERY shipped formula's agent (agent-gen-all.sh iterates all `install_formulas/*.formula.toml`, +:134-153, verified), placing the 4 referenced specialists (and all others) into +agents.json. `ValidateDispatchConfig` then passes. + +- Trade-offs: Makes the default valid-by-construction (C-5). `af install --agents` + "refuses to run from a worktree; requires an already-initialized factory" + (codebase-snapshot §4) — both conditions are met at the quickstart moment (real + repo root, post-init). Cost: heavier bootstrap (rebuilds af, provisions ~15 + agents) — a one-time setup cost, acceptable for the "land and go" scenario the + source describes. This is the cleanest path to AC-2/AC-4 (the specialists the + default routes to actually exist and are formula-bearing). +- Reversibility: Moderate (bootstrap-sequence change in quickstart.sh; the ADR-014 + bootstrap-script exemption applies — quickstart is an operator setup script). +- Constraints: satisfies C-6, C-5, AC-2, AC-4, AC-5; complies with ADR-015 + (formulas flow through `install_formulas/` → installed agents) and ADR-014 + (quickstart is an exempt bootstrap script). Recommended as primary. + +### Option I3.2 — Make the dispatcher skip unknown-agent mappings (skip-and-warn) — RECOMMENDED (defense-in-depth) + +Change the dispatch path so a mapping whose agent is absent from agents.json is +SKIPPED with a loud warning, instead of failing the whole cycle. Today +`ValidateDispatchConfig` returns an error on the first unknown agent +(dispatch.go:102, verified) — this option relaxes that for the dispatch-loop caller +(NOT for `af config dispatch set`, which should stay strict so a human typo is +caught at write time). + +- Trade-offs: The autonomous path degrades per-mapping rather than all-or-nothing, + so a partially-provisioned factory still dispatches the agents it has. Risk: + could mask a genuine config typo — mitigated by the loud warning. Requires + splitting the validation contract: strict at the write boundary (config_set.go), + tolerant at the dispatch boundary. This is a non-trivial behavior change and must + be carefully scoped to avoid weakening the write-path guarantee. +- Reversibility: Moderate (alters a validation contract used by the dispatch loop). +- Constraints: supports C-6/AC-2 robustness, must preserve C-5's "valid by + construction" intent (the warning names `af install --agents` as the remedy, + per U2.1). Recommended as defense-in-depth atop I3.1. + +### Option I3.3 — Ship a default whose mappings reference ONLY manager/supervisor — REJECTED + +(Same as Data/D2.2-B.) REJECTED: fails [AC-2], [AC-4]. Routing trigger labels to +non-formula agents (manager/supervisor are not formula-bearing specialists, +codebase-snapshot §6) dispatches no formula work, so the autonomous outcome AC-2/ +AC-4 require never happens. Reject. + +--- + +## I4. AC-5 — slung formulas push PRs without doctor/human (verification this dimension owns) + +AC-5 requires that slung agents push branches as PRs against main without +`doctor --fix` or human interaction as an ongoing dependency. This is a PROPERTY of +the referenced formulas (`rapid-implement`, `rapid-increment`, etc.), not of this +change directly. This dimension's integration obligation: confirm the default +routes only to formulas that have a push-PR terminal behavior. + +### Option I4.1 — Default routes to the rapid-* / ultra-review formulas (which carry the formula-driven PR-push behavior); no change to those formulas — RECOMMENDED + +The 4 referenced formulas are shipped specialists (codebase-snapshot §3) and are +the SAME formulas the factory already uses for autonomous PR work. This change does +not modify them; it only ensures the dispatch default routes to them. AC-5/AC-6's +"known working formula process that IS their IDENTITY" is satisfied by using the +existing formulas unchanged. + +- Trade-offs: Zero formula edits; leans on the existing, verified specialist- + dispatch + formula-step machinery (`af sling --agent`, codebase-snapshot §4). + This dimension does NOT independently verify each formula's internal PR-push step + (that is a formula-content concern outside this config-bootstrap change and + [UNVERIFIED] here at the step level) — but the routing is correct, and the + scope (source.md:150-154) is the config bootstrap, not formula internals. +- Reversibility: Easy (routing only). +- Constraints: satisfies AC-5, AC-6 (uses the identity formulas unchanged). + Recommended. + +--- + +## Reversibility (this dimension): Easy–Moderate + +I1/I2/I4 are Easy (additive, localized). I3.1 (bootstrap provisioning) and I3.2 +(dispatcher tolerance) are Moderate. + +## Tests that change (verified test surfaces) + +- A golden/unit test asserting `af install --init` (in a temp repo with a known + `git remote`) writes a dispatch.json whose `repos` is `["/"]` and + whose mappings/workflows match `DefaultDispatchConfigJSON` (verification.md AC-1, + AC-3 evidence rows). +- A test asserting `DefaultDispatchConfigJSON(repo)` output passes + `validateDispatchConfig` AND (given an agents.json containing the 4 specialists) + `ValidateDispatchConfig` — pinning C-6. +- If I3.2 is taken: a dispatch-loop test asserting an unknown-agent mapping is + skipped-with-warning, not fatal, while `af config dispatch set` still rejects it. +- The existing `config_test.go` patterns (Save/Load round-trips, e.g. the + BuildHostConfig round-trip at config_test.go:1163-1189, verified) are the model + for the new default round-trip test. + +## Dependencies produced + +- PROVIDES to **Data (D2)** and **Security (D5)**: the resolved C-6 sequencing — + specialists provisioned by `af install --agents` in quickstart BEFORE the + dispatcher runs `ValidateDispatchConfig`. +- PROVIDES to **API (D1)**: the single call site (`runInstallInit` starterConfigs + map) and the discovery ordering. +- PROVIDES to **UX (D3)**: confirmation that the dispatcher auto-starts on `af up` + (startup.json `start_dispatch:true`, install.go:147, verified), making the + zero-touch path (U1.1) real. +- REQUIRES from **Data (D2)**: the exact agent list the default references. +- REQUIRES from **API (D1)**: the `DefaultDispatchConfigJSON` signature. +- REQUIRES from **Security (D5)**: the validated repo string and the C-6 resolution + choice (provision vs tolerate). + +## Risks identified + +| Risk | Severity | Mitigation | +|------|----------|------------| +| C-6: default references 4 specialists absent from fresh agents.json → `ValidateDispatchConfig` fails at dispatch-start | HIGH | I3.1 run `af install --agents` in quickstart (primary) + I3.2 dispatcher skip-and-warn (defense-in-depth) | +| Adding `af install --agents` to quickstart lengthens/heavies the bootstrap and could fail (rebuilds af) | Medium | It is an existing, verified command with worktree/init guards (codebase-snapshot §4); run it best-effort with a clear failure message; I3.2 backstops a partial provision | +| I3.2 dispatcher tolerance masks a real config typo | Medium | Loud per-mapping warning naming the remedy; keep `af config dispatch set` strict (config_set.go unchanged) so human edits are still validated | +| Discovery runs in runInstallInit but git remote absent (local-only repo) | Medium | Best-effort + degrade (A3.1); install still succeeds with loadable status-quo default | +| Changing `ValidateDispatchConfig` behavior risks the write-path guarantee | Medium | Scope the tolerance to the dispatch-loop caller ONLY; the write path (config_set.go:85-91) keeps the strict check | diff --git a/.designs/73/problem-summary-73.md b/.designs/73/problem-summary-73.md new file mode 100644 index 0000000..5664b56 --- /dev/null +++ b/.designs/73/problem-summary-73.md @@ -0,0 +1,104 @@ +# Problem Summary — Issue #73 + +**Issue:** [New dispatch workflow default to be included with agentfactory](https://github.com/stempeck/agentfactory/issues/73) +**Number:** 73 +**Labels:** (none) +**Fetched:** 2026-06-28T16:35:12Z + +--- + +## Before You Proceed (issue author's framing) + +Read `USING_AGENTFACTORY.md` first. Before doing any investigation or code +changes, state the Vision, Mission, and summarize the current usage flow in +your own words — including how agents start, how they receive work, and how +formulas drive execution. Do not proceed until you have done this. + +## Requirements / Context + +When the agentfactory project is newly installed in a docker container, the +typical setup-and-start flow is: + +1. `./quickdocker.sh ` is run (with appropriate parameters); + the docker image is built and the user lands in a bash with immediate access + to agentfactory. +2. `claude` is run and authenticated manually. +3. `~/projects/agentfactory/quickstart.sh` is executed to build, install & + initialize the factory. +4. `af up manager` starts the manager. +5. `af attach manager` enters and issues commands. + +Then either: + +6. `af up design-v3` starts an agent (for direct interaction), and +7. `af attach design-v3` attaches to interact directly with the agent. + +OR (recommended): + +6. `af sling --agent design-v3 ""` initiates autonomous agent work. + +Problems are encountered at various points; the scenario below is the next one +to solve. + +## Scenario (the core problem) + +We need to update `dispatch.json` to include a **baked-in default** for dispatch +with agentfactory, so that when someone new to the project uses +`./quickdocker.sh repo-link` and gets their container bootstrapped — landing in +`/af/repo` with their newly bootstrapped repository ready to use agentfactory — +they could opt to **just start tagging their GitHub issues with labels** to kick +off work, **without ever needing to visit the manager**. + +The `dispatch.json` should be bootstrapped **when the dispatch.json is first +created during initial setup of the repository**, so we know the repository name +and can include it appropriately in the dispatch.json. The author proposes +something like the following default (verbatim from the issue, including its +typos/unclosed brackets — the *intent* is what matters, not the literal JSON): + +```json +{ + "repos": [ + "org/repository" // <- update this with the ACTUAL org/repository during the install + ], + "trigger_label": "agentic", + "notify_on_complete": "manager", + "interval_seconds": 300, + "retry_after_seconds": 1800, + "remove_trigger_after_dispatch": true, + "mappings": [ + { "labels": ["rapid-plan"], "source": "issue", "agent": "rapid-soldesign-plan" }, + { "labels": ["rapid-engineer"], "source": "issue", "agent": "rapid-implement" }, + { "labels": ["pr-review"], "source": "pr", "agent": "ultra-review" }, + { "labels": ["pr-iterate"], "source": "pr", "agent": "rapid-increment" } + ], + "workflows": [ + { "label": "feature-workflow", "phases": ["rapid-plan", "rapid-engineer"] } + ] +} +``` + +## Source-of-truth guidance + +- Assume `*.md` documents may contain outdated information. Treat the **codebase + as the only real source of truth**; use docs only to aid the search. +- Rely on `./USING_AGENTFACTORY.md` and other `./.designs/*.md` files to + understand the **architectural intentions** of agentfactory before making + recommendations. + +## Acceptance Criteria + +The ONLY successful outcome of agentfactory is that we can follow the documented +setup steps on a fresh agentfactory with a clean repo, and when we reach the +step where we ask the manager to `run af sling --agent "task +description"`, the work is executed **autonomously** using the step-by-step +formula that represents the **IDENTITY** of that agent, respecting the formula's +rigid step-by-step process up to the point where human interaction is genuinely +necessary for next steps. All code branches created must be **pushed as PRs +against the main branch** without doctor fixes or human interaction (unless +absolutely necessary; a `doctor --fix` is acceptable only as a bandaid for +legacy broken behaviors, not an ongoing operational dependency). + +Agents should follow the known-working formula process that IS their IDENTITY, +so we get consistent successful outcomes from each agent. The mission when +addressing any problem scenario is to seek to understand how to achieve this +desired outcome with **systemic improvements** while addressing the scenario. diff --git a/.designs/73/scale.md b/.designs/73/scale.md new file mode 100644 index 0000000..a7e9969 --- /dev/null +++ b/.designs/73/scale.md @@ -0,0 +1,112 @@ +# D4 — Scalability (scale.md) + +Owner of (verification.md): AC-6 (with Integration, UX). AC-6: "consistent +successful outcomes out of each agent … systemic improvements." The scalability +question for THIS change is narrow: a one-time bootstrap of a single config file +adds essentially no runtime hot path. The relevant "scale" dimensions are +(a) bootstrap-time cost (one git/gh call at install), and (b) the steady-state +dispatch-loop load that the now-populated default ENABLES — i.e. the dispatcher +will, for the first time on a fresh repo, actually poll and dispatch. + +CONSTRAINT-SENSITIVE NOTE (simplicity): No option below adds a caching layer, a +queue, a daemon, or any new dependency. C-4 (codebase is truth) and the overall +"narrow surface" scope (source.md:150-154) make heavyweight scale machinery +inappropriate and out of scope. Any such proposal is REJECTED on scope grounds. + +--- + +## S1. Bootstrap-time cost of repo discovery + +### Option S1.1 — Single best-effort `gh repo view` / `git remote` call at install — RECOMMENDED + +D1/A2 adds exactly ONE subprocess call (`gh repo view --json nameWithOwner`, or +`git remote get-url origin`) to `runInstallInit`. This runs once, at install, never +on a hot path. + +- Trade-offs: Negligible cost (one process spawn during a setup that already spawns + `gh`, builds Go, and starts an MCP server — quickstart.sh phases, verified). No + steady-state impact. Bounded by a timeout so a hung `gh` (e.g. network) cannot + stall install indefinitely. +- Reversibility: Easy. +- Constraints: satisfies AC-6 (systemic, one-time); no simplicity violation. + Recommended. + +### Option S1.2 — Cache the discovered repo and re-validate it on every install run — REJECTED + +Persist discovery results and re-check/refresh them across runs. + +- REJECTED: violates simplicity (no constraint requires it) and conflicts with + [C-2]/[ADR-017]. Discovery is a once-at-first-create concern (C-2); a caching/ + refresh layer is unjustified complexity for a value written once + (install.go:152 write-if-absent, verified) and risks re-writing a customer-edited + `repos` (ADR-017). No AC or constraint asks for repeated re-discovery. + +--- + +## S2. Steady-state dispatch load enabled by the populated default + +### Option S2.1 — Inherit the existing poll cadence and TTLs unchanged (interval 300s, retry 1800s, 24h dedup TTL) — RECOMMENDED + +The default ships `interval_seconds:300`, `retry_after_seconds:1800` (matching the +struct defaults, dispatch.go:176,179, verified), and the dispatcher's 24h +already-dispatched TTL (codebase-snapshot §4, dispatch.go cycle step 5) is +unchanged. The populated default simply means the dispatcher now has repos+mappings +to act on, instead of polling nothing. + +- Trade-offs: No new tuning surface; the cadence is the same the system already + uses everywhere. A 5-minute poll against one repo via `gh` is trivial load. The + query is capped at 50 results per repo (dispatch.go:182, verified) with a + truncation warning, so a label-flood on one repo degrades gracefully rather than + unboundedly. +- Reversibility: Easy (the values are just defaults the operator can override). +- Constraints: satisfies AC-6 (consistent, predictable cadence). Recommended. + +### Option S2.2 — Lower the default interval for snappier first-run feedback — REJECTED + +Ship a smaller `interval_seconds` so the new operator sees dispatch happen sooner. + +- REJECTED: violates simplicity and [C-1]'s "default … with agentfactory" intent + of a sensible, baked-in default. The struct default IS 300s + (dispatch.go:176, verified); shipping a different value creates a second source + of truth for the cadence and a faster poll multiplies `gh` API calls for no AC + benefit (no AC mentions latency). Diverging the baked default from the validated + struct default is exactly the drift the single-source pattern (D1/A1.1) avoids. + +--- + +## S3. Multi-repo / fleet scaling of the default + +### Option S3.1 — Default `repos` holds exactly the one discovered repo; multi-repo is operator-extended — RECOMMENDED + +The discovered `owner/name` is the sole entry (D2/D2.3-A). The dispatcher already +iterates `dispatchCfg.Repos` (dispatch.go:172, verified), so an operator who later +adds repos gets fan-out for free with no code change. + +- Trade-offs: The baked-in default is correct for the single-repo bootstrap + scenario the source describes (one container, one cloned repo — source.md:87-92). + Scaling to many repos is a deliberate operator action, not a default — keeping + the default minimal and correct (AC-3: the ACTUAL repo). +- Reversibility: Easy. +- Constraints: satisfies AC-3, C-3 (the actual discovered repo). Recommended. + +## Reversibility (this dimension): Easy + +No hot-path code, no new infra; one bounded install-time call and reuse of existing +dispatch cadence. + +## Dependencies produced + +- PROVIDES to **API (D1)**: the requirement that the discovery call be bounded by a + timeout (so install cannot hang). +- PROVIDES to **Data (D2)**: confirmation that the struct-default cadence values + (300/1800) should be the shipped values (no divergence). +- REQUIRES from **Integration (D6)**: confirmation that the dispatcher's existing + 50-result cap and 24h TTL remain unchanged (they do — codebase-snapshot §4). + +## Risks identified + +| Risk | Severity | Mitigation | +|------|----------|------------| +| `gh repo view` hangs at install (network), stalling `af install --init` | Medium | Bound the discovery call with a context timeout; on timeout, fall to A3.1 degraded default (warn-don't-abort) | +| Populated default + dispatcher auto-start means a fresh repo now polls `gh` every 5 min where before it polled nothing | Low | This is the intended AC-2 behavior; 300s cadence + 50-result cap (dispatch.go:182) bound the load | +| Many labeled issues on first run dispatch a burst of slings | Low | Dispatcher's busy-agent skip + 24h dedup TTL (codebase-snapshot §4) throttle re-dispatch; worktree cap (factory.json MaxWorktrees=4, config.go:116 verified) bounds concurrent agent work | diff --git a/.designs/73/security.md b/.designs/73/security.md new file mode 100644 index 0000000..793302e --- /dev/null +++ b/.designs/73/security.md @@ -0,0 +1,144 @@ +# D5 — Security (security.md) + +Owner of (verification.md): C-3 (with Integration, API), C-5 (with UX, +Integration), C-6 (with Data, Integration), AC-5 (with Integration). The security +surface of this change is small but real: it introduces, for the first time, +UNTRUSTED INPUT (a git remote URL / `gh` output) into the install path, and that +input is written into a config file the dispatcher later feeds to `gh`. + +Threat model context: install runs in a per-user docker container +(quickdocker.sh, source.md:87) — not a multi-tenant boundary. The realistic threats +are (a) a malformed/malicious git remote producing a bad `repos` entry, and (b) the +default mappings causing a cross-file validation failure (a correctness/DoS-of- +dispatch issue, C-6). There is no credential or PII handling in this change. + +--- + +## SEC1. Trust of the discovered repo string + +The repo string comes from `git remote get-url origin` or `gh repo view` (D1/A2). +In the container scenario the remote was set by `quickdocker.sh ` +(operator-supplied, source.md:87-88), so it is semi-trusted — but the value flows +into a config that is later passed to `gh --repo ` (dispatch.go:300, +verified) and split on `/` (dispatch.go:537, verified). A crafted value could +inject unexpected `gh` flags or terminal escapes. + +### Option SEC1.1 — Validate the discovered repo against a strict `owner/name` allowlist regex before writing — RECOMMENDED + +Before placing the discovered string in `repos`, require it to match a strict +pattern (e.g. `^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$` — one slash, no whitespace, no +shell metacharacters, no `..`). On mismatch, do NOT write it; fall to the degraded +default (A3.1) with a warning. + +- Trade-offs: Cheap, deterministic, and matches the EXACT shape the dispatcher + requires (`strings.Cut(repo,"/")` expecting non-empty owner and name, + dispatch.go:537-539, verified — it already errors on a non-`owner/name` form). + Validating at the WRITE boundary means a bad value never reaches the dispatcher. + Rejects URL-parse failures, multi-slash paths, and any embedded shell/escape + characters. +- Reversibility: Easy. +- Constraints: satisfies C-3 (a VALID actual repo, not just any string), supports + AC-5 (the dispatched work targets the right repo). Recommended. + +### Option SEC1.2 — Pass the raw discovered string through unvalidated — REJECTED + +Write whatever `git remote`/`gh` returns directly into `repos`. + +- REJECTED: fails [C-3] (a malformed value is not "the repository-name … included + appropriately") and introduces an injection surface. `gh` is invoked with the + repo as `--repo ` via exec (dispatch.go:300, verified) — argument-vector + exec mitigates shell injection, but a crafted value (e.g. containing a leading + `-`) could still be mis-parsed as a `gh` flag, and an unvalidated value would be + echoed into the UX banner (U1.2), enabling terminal-escape injection. Reject. + +--- + +## SEC2. Cross-file validity of the default (C-6) as an availability concern + +A default that references agents absent from agents.json causes +`ValidateDispatchConfig` to fail (dispatch.go:100-104, verified) at dispatch-start — +a self-inflicted denial of the dispatch service on a fresh factory. This is the +C-6 security/availability surface. + +### Option SEC2.1 — Guarantee the default is cross-file-valid by construction: provision referenced specialists during bootstrap — RECOMMENDED + +Ensure the 4 referenced specialists are in agents.json before +`ValidateDispatchConfig` runs, by having the bootstrap run `af install --agents` +(which provisions every shipped formula's agent via `af formula agent-gen`, +agent-gen-all.sh:134-153, verified). The cross-file validator then passes. + +- Trade-offs: The default is valid-by-construction (C-5: "valid-by-construction … + no doctor --fix"). This is owned jointly with Data (C-6 rider) and Integration + (the bootstrap-sequence change). Cost: `af install --agents` rebuilds af and + provisions ~15 agents — a heavier bootstrap, but a one-time setup cost. +- Reversibility: Moderate (touches the bootstrap sequence, not just one function). +- Constraints: satisfies C-6, C-5 (no doctor dependency), AC-5. Recommended; + see Integration (D6) for the precise sequencing. + +### Option SEC2.2 — Make the dispatcher TOLERATE unknown-agent mappings (skip-and-warn instead of fail) — RECOMMENDED (defense-in-depth, but a behavior change owned by Integration) + +Change the dispatch path so an unknown-agent mapping is SKIPPED with a warning +rather than failing the whole cycle. The known-good mappings still dispatch. + +- Trade-offs: Resilient even if provisioning is incomplete; the autonomous path + degrades per-mapping instead of all-or-nothing. BUT this is a behavior change to + `ValidateDispatchConfig`/the dispatch cycle (dispatch.go:93-138 currently RETURNS + an error on unknown agent, dispatch.go:102, verified) and could MASK a genuine + config typo. Must be paired with a loud per-mapping warning. This is a real + cross-dimension decision: SEC2.1 (provision) vs SEC2.2 (tolerate). They are not + mutually exclusive; SEC2.1 is the primary, SEC2.2 is defense-in-depth. +- Reversibility: Moderate (alters a validation contract). +- Constraints: supports C-6/AC-2 robustness; must NOT silently swallow real errors + (pair with warning). Recommended as defense-in-depth, decision deferred to D6. + +### Option SEC2.3 — Ship the default but let it fail validation, expecting `doctor --fix` to repair it — REJECTED + +- REJECTED: violates [C-5]. C-5 explicitly states `doctor --fix` is "acceptable + only as a bandaid/fix for legacy broken behaviors, not as an ongoing operational + dependency." Designing a default that REQUIRES `doctor --fix` on every fresh + install makes doctor an ongoing operational dependency — the precise thing C-5 + prohibits. Reject. + +--- + +## SEC3. Preserving customer data (ADR-017) + +### Option SEC3.1 — Reuse the existing idempotent write-if-absent guard; never overwrite an existing dispatch.json — RECOMMENDED + +The default is written only when dispatch.json does NOT yet exist +(`os.Stat … os.IsNotExist`, install.go:152, verified). An operator-edited file is +never clobbered. + +- Trade-offs: Honors ADR-017 ("af infrastructure commands must not delete customer + data") and C-2 ("first created"). Zero new write-path risk. The repo-discovery + read (`git remote`/`gh`) is read-only, also ADR-017-clean. +- Reversibility: Easy. +- Constraints: satisfies C-2, complies with ADR-017. Recommended. + +## Reversibility (this dimension): Easy–Moderate + +Repo validation and the write guard are Easy. The cross-file-validity guarantee +(SEC2.1) touches the bootstrap sequence and is Moderate; the dispatcher-tolerance +change (SEC2.2) alters a validation contract and is Moderate. + +## Dependencies produced + +- PROVIDES to **API (D1)**: the `owner/name` allowlist regex the discovered string + must pass before being written. +- PROVIDES to **UX (D3)**: the guarantee that the repo string echoed in the banner + is escape-safe (post-validation). +- PROVIDES to **Integration (D6)**: the C-6 resolution choice (SEC2.1 provision vs + SEC2.2 tolerate) — escalated to D6 to sequence. +- REQUIRES from **Data (D2)**: the list of agents the default references (to + guarantee they are provisioned / handled). +- REQUIRES from **Integration (D6)**: the exact point in the bootstrap where + `ValidateDispatchConfig` runs relative to specialist provisioning. + +## Risks identified + +| Risk | Severity | Mitigation | +|------|----------|------------| +| Crafted git remote URL injects a bad `repos` value (flag-injection into `gh`, terminal-escape into banner) | Medium | SEC1.1 strict `owner/name` allowlist at the write boundary; reject + degrade on mismatch | +| Default references unprovisioned agents → dispatch-start validation fails (self-inflicted DoS of dispatch, C-6) | HIGH | SEC2.1 provision specialists in bootstrap (primary) + SEC2.2 dispatcher skip-and-warn (defense-in-depth) | +| A future design papers over C-6 by mandating `doctor --fix` on every install | Medium | SEC2.3 REJECTED on C-5; encode the valid-by-construction requirement in a golden test (D2) | +| Re-running install overwrites an operator-curated dispatch.json | Low | SEC3.1 write-if-absent guard (install.go:152) preserves the existing file (ADR-017) | diff --git a/.designs/73/six_sigma_gaps.md b/.designs/73/six_sigma_gaps.md new file mode 100644 index 0000000..a58d747 --- /dev/null +++ b/.designs/73/six_sigma_gaps.md @@ -0,0 +1,88 @@ +# Six-Sigma Gap Analysis + +**Date**: 2026-06-28 +**Source**: .designs/73/source.md + +## Gap Discovery Method +Independent analysis of escape paths, test type mismatches, sibling vulnerabilities, version/config drift risks, and unstated assumptions. Every code claim below was verified by Read/Grep against the worktree at `/home/dev/af/agentfactory/.agentfactory/worktrees/wt-7605ca` (primarily `internal/cmd/dispatch.go`, `internal/config/dispatch.go`, `internal/cmd/install.go`, `internal/cmd/sling.go`, `quickstart.sh`, `quickdocker.sh`) and cross-checked against the Decision History in codebase-snapshot.md. + +## Gaps Identified + +### Gap 1: Default mappings reference 4 agents that a fresh install does NOT provision → the WHOLE dispatch cycle hard-fails +**Category**: escape path / scope gap +**Current state**: The proposed default (C-6) names `rapid-soldesign-plan`, `rapid-implement`, `ultra-review`, `rapid-increment` in `mappings[].agent`. The documented fresh-setup flow (`quickstart.sh:441-464`, VERIFIED) runs only `af install --init`, `af install manager`, `af install supervisor`. It does NOT run `af install --agents` and does NOT provision any of the 4 specialists. The default `agents.json` written by `af install --init` contains ONLY `manager` + `supervisor` (install.go:143, VERIFIED). At the first dispatch cycle, `runDispatch` calls `config.ValidateDispatchConfig(dispatchCfg, agentsCfg)` UNCONDITIONALLY (dispatch.go:146, VERIFIED), which returns `"dispatch mapping references unknown agent %q"` for the FIRST unknown agent (dispatch.go:100-104, VERIFIED). That error aborts the ENTIRE cycle — not just the offending mapping. So on a genuinely fresh setup, tagging ANY issue (even with a valid label) dispatches NOTHING: AC-2 and AC-4 both fail out-of-box. This is the single highest-impact gap; verification.md's C-6 row already flags the relaxation but treats provisioning as one of three equal options. +**Ideal state**: Six-sigma requires that the shipped default be valid-by-construction against the shipped default agents.json — i.e. either (a) `af install --init` (or the documented setup flow) also provisions the 4 specialist agents into agents.json before the first dispatch cycle, OR (b) `ValidateDispatchConfig` / the dispatch loop skips (with a warning) mappings whose agent is absent rather than hard-failing the whole cycle, OR (c) the default mappings reference only `manager`/`supervisor` (defeats the purpose). Option (a) is the only one that satisfies AC-2/AC-4 literally. +**Feasibility**: Feasible +**Feasibility rationale**: `af install --agents` already exists (regenerates+reinstalls all formula-derived agents; install.go help, VERIFIED) and all 4 formulas are shipped in `install_formulas/` (codebase-snapshot §3, VERIFIED) — so provisioning at init is a wiring change, not new capability. ADR-017 permits writes inside af directories (agents.json is one), so adding agents at init is allowed. The only caveat: `--agents` "refuses to run from a worktree" and "runs agent-gen-all.sh then quickstart.sh" (install.go help) — a heavier path than `af install `; the design must pick the lightweight per-role provisioning, not the full `--agents` rebuild, to avoid recursion during quickstart. Option (b) (skip-unknown) is also feasible and smaller but partially violates AC-6's "consistent successful outcomes" because it silently degrades. + +### Gap 2: AC-2 escape path — the trigger_label is a HARD query pre-filter; tagging only a mapping label dispatches nothing +**Category**: escape path / unstated assumption +**Current state**: `queryGitHubIssues`/`queryGitHubPRs` filter GitHub by `--label triggerLabel` ("agentic") at query time (dispatch.go:301,320, VERIFIED). An item that carries a mapping label (e.g. `rapid-plan`) but NOT `agentic` is NEVER returned by the query, so it is never matched and never dispatched — silently. AC-2 says the user "could opt to just start tagging their github issues with tags instead to kick off work." The natural reading ("tag with rapid-plan") does NOT work: the user must apply BOTH `agentic` AND `rapid-plan`. The workflow path has the same constraint — `feature-workflow` items also need `agentic` to be queried (bootstrap adds the phase label but the item must already be in the trigger-label result set). The design as stated (just ship the default JSON) does not address this two-label requirement; a fresh user will tag `rapid-plan`, see nothing happen, and have no signal why. +**Ideal state**: Either (a) document/surface that the trigger label is mandatory alongside the mapping/workflow label (a usability contract the default ships with), or (b) make the dispatcher also query each mapping/workflow label directly (a query-shape change), or (c) at minimum emit an observable signal when an issue carries a mapping label but lacks the trigger label (the "almost-triggered" miss). Six-sigma requires the user's most likely tagging action to either work or produce a visible diagnostic. +**Feasibility**: Partially feasible +**Feasibility rationale**: (a) is trivially feasible (doc/default comment). (b) is feasible but a real behavior change to the query layer (N label queries per repo, dedup) and risks the 50-result limit warning (dispatch.go:182,198) firing more often — moderate effort, expands scope beyond "bootstrap one config file." (c) — diagnosing the "tagged a mapping label but not the trigger label" miss — is INFEASIBLE without removing the trigger-label pre-filter, because the issue is invisible to the dispatcher precisely because the trigger-label query excludes it (the thing that would detect the miss is the thing being gated). So observability of this specific miss is architecturally ceilinged unless the query itself widens. Net: documentation is the only zero-risk fix; the rest trade scope. + +### Gap 3: Repo-name discovery does not exist anywhere in the Go init path and can silently produce an empty/placeholder `repos` +**Category**: scope gap / observability gap +**Current state**: C-3/AC-3 require `repos` populated with the ACTUAL org/repository at init. `runInstallInit` takes NO repo argument and does NO git-remote lookup (install.go:181 region, codebase-snapshot §6, VERIFIED). The ONLY git-remote parsing in the Go tree is `detect_default_branch.go` (branch detection, NOT repo identity; VERIFIED) and the ONLY org/repo parsing is `ghLinkedPRs` splitting an already-known `repo` on `/` (dispatch.go:537-539, VERIFIED). `quickstart.sh` `cd`s into the discovered repo before `af install --init` (codebase-snapshot §6), so a git remote IS present in CWD — but nothing reads it. New code must run `git remote get-url origin` (or `gh repo view --json nameWithOwner`) and parse `org/repo` from it. The failure mode the design must cover: detached/missing/non-GitHub/SSH-vs-HTTPS-form remote, multiple remotes, or a remote URL that doesn't parse to `org/repo`. If discovery fails and `repos` is left empty, `validateDispatchConfig` returns `ErrMissingField` ("must have at least one repo", dispatch.go:142-143, VERIFIED) — which the `af up` auto-start path (`startDispatch`) treats as "not configured" and SILENTLY skips (dispatch.go:1328-1332, VERIFIED). The user sees "skipping dispatch (dispatch.json not configured)" — indistinguishable from a deliberate empty default — and never learns repo discovery failed. +**Ideal state**: Six-sigma requires (a) robust URL parsing covering `git@github.com:org/repo.git`, `https://github.com/org/repo`, `.git` suffix, and trailing slashes; (b) a fail-LOUD signal (ADR-014 mandates fail-loud-with-flag, never prompt) when discovery cannot determine `org/repo`, distinct from the legitimate empty-default skip message; and (c) a test matrix over the URL forms. The `startDispatch` skip message must NOT swallow a discovery failure as "not configured." +**Feasibility**: Feasible +**Feasibility rationale**: `quickdocker.sh:41` already does `git remote get-url origin` for the agentfactory repo itself (VERIFIED) — the technique is proven in-tree. ADR-014 explicitly requires non-interactive discovery or fail-loud-with-flag at init (no prompt). The only ceiling: if the user clones via a remote name other than `origin` or with a non-GitHub host, discovery legitimately cannot produce `org/repo` and MUST fail loud — that residual is acceptable and ADR-014-compliant. Distinguishing "discovery failed" from "empty by design" in `startDispatch` is a message/branch change (feasible). + +### Gap 4: `workflows[].phases` vs `mappings[].labels` is a brittle cross-reference with no install-time golden test +**Category**: version/config drift / test type mismatch +**Current state**: The proposed `feature-workflow` has phases `["rapid-plan", "rapid-engineer"]` (C-6). These MUST exactly equal single-label mappings: `phaseResolvesAlone` requires a mapping whose label set is EXACTLY `{phaseLabel}` (dispatch.go:256-267 / config/dispatch.go:256-267, VERIFIED), and `validateWorkflows` REJECTS a phase that only appears inside a multi-label mapping or is absent (config/dispatch.go:232-238, VERIFIED). The proposed default happens to satisfy this (each phase = one single-label mapping). But this is an invariant maintained by hand in a literal default. If a future edit renames a mapping label, adds a second label to a phase's mapping, or makes `feature-workflow` collide with a mapping label (HIGH-B, config/dispatch.go:219-221), the default silently becomes invalid — and there is NO golden/round-trip test asserting the SHIPPED default parses+cross-validates. Existing dispatch tests construct ad-hoc configs (`dispatch_workflow_test.go`, `dispatch_test.go`, VERIFIED) — none asserts the install-time default. This is a structural (does-it-parse) gap where a behavioral (does-the-shipped-default-survive-ValidateDispatchConfig-against-the-default-agents.json) test is needed. +**Ideal state**: A test that loads the EXACT default `DefaultDispatchConfigJSON()` string, runs `validateDispatchConfig` AND `ValidateDispatchConfig` against the default `agents.json` (with the 4 specialists, per Gap 1's fix), and asserts no error — so any future drift in labels/phases/agents fails CI, not the customer's first dispatch. Mirror the `using_workflow_doc_test.go` pattern (which already parses a documented dispatch example, VERIFIED). +**Feasibility**: Feasible +**Feasibility rationale**: The validators are exported/callable (`config.ValidateDispatchConfig`) and the default-agents.json content is known. Following ADR-008's "single-source default expressed as a Go func with a mechanical drift test" makes this the established idiom. The only dependency: this test presupposes Gap 1's fix (the default agents.json must contain the 4 agents) — otherwise the golden test would itself assert a config that hard-fails, which is the contradiction the test exists to prevent. So the test is the verification artifact for Gap 1. + +### Gap 5: AC-5 (PRs pushed without doctor/human) is a property of the slung AGENT FORMULAS, not of dispatch.json — and is unobservable from the dispatch layer +**Category**: scope gap / failure mode gap +**Current state**: AC-5 requires that slung agents push branches as PRs against main with no `doctor --fix` and no human step (C-5). This is entirely a property of the four agents' FORMULAS (rapid-soldesign-plan, rapid-implement, ultra-review, rapid-increment), not of dispatch.json. The dispatch layer only slings; whether the agent reaches a PR is the formula's responsibility. The dispatcher's own completion gates (issue→pr handoff requires EXACTLY ONE linked PR, dispatch.go:856-867, VERIFIED) actually DEPEND on the producing formula emitting a closing keyword (`Resolves #N`) in the PR BODY — and the codebase explicitly notes that `rapid-soldesign-plan`/`soldesign-plan` put `(#N)` only in the PR TITLE (a cross-reference, NOT a closing keyword) → resolve to 0 linked PRs → a documented STALL, never an advance (dispatch.go:529-535, VERIFIED). So even with a perfect dispatch.json default, a `feature-workflow` whose first phase is `rapid-soldesign-plan` (issue source) handing off to `rapid-engineer` (also issue source here) could stall at the issue→pr boundary IF a phase is pr-sourced — and more importantly, the design "ship a dispatch.json default" does NOT and CANNOT by itself guarantee AC-5. AC-5's evidence (a PR appears, no doctor in the run log) is invisible to the dispatch.json change under test. +**Ideal state**: The design must EITHER (a) explicitly scope AC-5 OUT of "the dispatch.json default" deliverable and assign it to the agent-formula layer (with a traceability note), OR (b) verify, as a separate check, that each of the 4 default-referenced formulas actually reaches a PR-against-main terminal without doctor/human — a formula-level behavioral test, not a config test. Shipping the default without this leaves AC-5 asserted-but-unverified. +**Feasibility**: Partially feasible +**Feasibility rationale**: (a) is feasible (a scoping/traceability statement in the design doc — the AC is owned by the formula layer). (b) — proving the formulas push PRs without doctor — is a much larger, E2E, live-gh-dependent verification that is OUT of scope for a config-bootstrap change and would require running each formula to completion. The design can HONESTLY satisfy AC-5 only by acknowledging it is a downstream-formula property and that the dispatch default is necessary-but-not-sufficient. Claiming AC-5 is "covered" by shipping dispatch.json would be the paraphrase-validates-paraphrase failure mode. + +### Gap 6: Idempotent write-if-absent means a customer with an OLD empty dispatch.json never receives the new default +**Category**: failure mode gap / version drift +**Current state**: The starter-config write is `os.Stat … os.IsNotExist` write-if-absent (install.go:152, VERIFIED) and ADR-017 mandates not clobbering customer data, so an existing `.agentfactory/dispatch.json` is preserved. C-2 says bootstrap occurs when dispatch.json is "FIRST created." Correct for a truly fresh repo. But the LARGE existing population — every repo that already ran `af install --init` under today's empty-default code — already HAS a dispatch.json with empty `repos`/`mappings`/no `workflows`. Re-running `af install --init` will NOT upgrade them (write-if-absent skips). These users get none of AC-1/AC-2/AC-4 unless they hand-edit or delete-and-reinit — which contradicts C-5's "without human interaction." The design as stated (bootstrap at first creation) silently excludes already-installed factories. The issue text is fresh-install-framed, but six-sigma asks: what about the installed base? +**Ideal state**: Either (a) explicitly scope this to NET-NEW installs only (documented residual — existing users keep their config, must opt in), which is ADR-017-honest, OR (b) provide a non-destructive, opt-in upgrade path (e.g. `af doctor --fix` to fill an empty default — but C-5 tolerates doctor --fix ONLY as a legacy bandaid, so this is acceptable as a one-time migration, not an ongoing dependency). Six-sigma requires the residual be NAMED, not silently accepted. +**Feasibility**: Partially feasible +**Feasibility rationale**: Auto-upgrading an existing dispatch.json is constrained by ADR-017 (must not clobber/modify customer data outside the "manage its own artifacts" boundary — and a customer may have deliberately emptied it). A blind overwrite is INFEASIBLE under ADR-017. A targeted "fill only if it exactly equals the historical empty default" migration is feasible but fragile (must byte-match the old literal). The honest fix is (a) — name the residual: this default ships for NEW installs; existing factories opt in. That is feasible and ADR-compliant. + +### Gap 7: `notify_on_complete: "manager"` is valid on a fresh factory, but ONLY because manager is always provisioned — an unstated coupling +**Category**: unstated assumption / dependency fragility +**Current state**: The proposed default sets `notify_on_complete: "manager"` (also the const default, `defaultNotifyAgent`, config/dispatch.go:15, VERIFIED). `ValidateDispatchConfig` validates an EXPLICITLY set `NotifyOnComplete` against agents.json (dispatch.go:105-108, VERIFIED). The fresh default agents.json DOES contain `manager` (install.go:143, VERIFIED), so this passes. But the design now depends on `manager` always being present in the default agents.json. If a future change removes `manager` from the default agents.json (or a user prunes it), an EXPLICIT `notify_on_complete: "manager"` becomes a hard validation failure — whereas leaving it EMPTY would default to "manager" at runtime WITHOUT the explicit-existence check (the empty case is intentionally left unvalidated, config/dispatch.go:88-92, VERIFIED). So writing the literal `"manager"` is strictly MORE brittle than omitting it, for zero functional gain on a fresh install. +**Ideal state**: The default should OMIT `notify_on_complete` (let it default to "manager" via `validateDispatchConfig`, dodging the explicit-existence cross-check) UNLESS there is a reason to pin it — minimizing the coupling to manager's presence in agents.json. Six-sigma prefers the form with the smaller failure surface. +**Feasibility**: Feasible +**Feasibility rationale**: Trivial — omit the field (it is `omitempty`-eligible in behavior: empty → defaulted). The runtime behavior is identical (notify routes to manager) but the validation surface shrinks. No architectural constraint. This is a pure hardening choice within the design's own scope. + +### Gap 8: No observability when a dispatch cycle hard-fails on the unknown-agent (Gap 1) or repo-missing (Gap 3) path — the user sees nothing actionable +**Category**: observability gap +**Current state**: When `ValidateDispatchConfig` fails in `runDispatch` (Gap 1), the function returns the error (dispatch.go:147, VERIFIED). In the `af up` auto-start path, the dispatcher runs in a tmux loop (`launchDispatchSession` sends a looped `af dispatch` command, dispatch.go:1297-1305, VERIFIED) — the error prints to that session's pane/log, NOT to the user's foreground. The user who ran `af up` and tagged an issue sees no foreground signal that dispatch is failing validation every cycle; they must `af attach af-dispatch` or read `.runtime/dispatch.log`. Worse, the `startDispatch` path swallows `ErrMissingField` as a benign "not configured" skip (dispatch.go:1328-1332, VERIFIED), so the empty-repos case (Gap 3) produces a message that LOOKS intentional. Net: the two most likely fresh-install failure modes (unknown agent, empty repos) are both low-visibility. AC-6 demands "consistent successful outcomes"; a silently-looping-failing dispatcher is the opposite. +**Ideal state**: Six-sigma requires that a fresh-install dispatch validation failure be surfaced where the user is looking — e.g. `af up` runs a one-shot validation of dispatch.json against agents.json BEFORE launching the loop and warns loudly if it would fail, and `af dispatch status` reports a "config invalid: " state. Distinguish "empty by design" from "discovery failed" (Gap 3) and from "references unprovisioned agents" (Gap 1). +**Feasibility**: Feasible +**Feasibility rationale**: `ValidateDispatchConfig` is already callable at the `af up`/startup site, and `af dispatch status --json` already exists as the machine-readable contract (codebase-snapshot §4, VERIFIED) — adding a config-validity field is additive. The only ceiling: a pre-flight validation at `af up` time must not ABORT `af up` (the existing design folds dispatch failures into a warn+non-zero-exit, dispatch.go:1316-1320, VERIFIED) — so it must warn, not block, consistent with the established "dispatcher failure never aborts af up" rule. + +## Summary Table +| # | Gap | Category | Impact | Feasibility | Specific constraint (if infeasible) | +|---|-----|----------|--------|-------------|-------------------------------------| +| 1 | Default mappings reference 4 agents absent from fresh agents.json → whole cycle hard-fails | escape path / scope gap | CRITICAL — AC-2 & AC-4 fail out-of-box | Feasible | — (`af install --agents` exists; ADR-017 permits) | +| 2 | trigger_label is a hard query pre-filter; tagging only a mapping label dispatches nothing | escape path / unstated assumption | HIGH — AC-2 likely mis-used by new users | Partially feasible | Detecting the "mapping-label-only" miss is ceilinged by the trigger-label pre-filter itself | +| 3 | No repo-name discovery in Go init; empty `repos` silently skips dispatch | scope gap / observability | HIGH — AC-3 unmet; failure looks like benign skip | Feasible | Non-`origin`/non-GitHub remotes legitimately fail-loud (ADR-014) | +| 4 | No golden test that the SHIPPED default parses + cross-validates against default agents.json | config drift / test mismatch | HIGH — silent drift reaches customer's first dispatch | Feasible | Presupposes Gap 1 fix (else test asserts a failing config) | +| 5 | AC-5 (PRs w/o doctor/human) is a formula-layer property, not a dispatch.json property | scope gap / failure mode | MEDIUM — AC-5 asserted-but-unverified by this change | Partially feasible | Full AC-5 verification is E2E/live-gh, out of config-bootstrap scope | +| 6 | Write-if-absent never upgrades existing (empty-default) installs | failure mode / version drift | MEDIUM — installed base excluded | Partially feasible | Blind overwrite infeasible under ADR-017; residual must be named | +| 7 | Explicit `notify_on_complete:"manager"` is more brittle than omitting it | unstated assumption / dependency fragility | LOW — hardening only | Feasible | — | +| 8 | Fresh-install dispatch hard-fail (Gap 1/3) is low-visibility (loops in tmux/log) | observability | MEDIUM — silent failing dispatcher contradicts AC-6 | Feasible | Pre-flight must warn, not abort `af up` | + +## Practical Ceiling +The practical ceiling for "ship a baked-in dispatch.json default that makes label-tagged autonomous work succeed out-of-box" is bounded by THREE things that no amount of config polish can dissolve: + +1. **The default is necessary but not sufficient.** AC-2/AC-4 require the referenced agents to EXIST and be dispatchable (Gap 1), and AC-5 requires those agents' FORMULAS to reach a clean PR (Gap 5). The dispatch.json change can guarantee the config is valid-by-construction (Gaps 1, 4, 7) and the repo is discovered (Gap 3), but the autonomous OUTCOME is a property of the four formulas, which this change does not touch. Six-sigma for the END-TO-END flow is therefore a multi-component property; this design owns only the config-and-provisioning component and must say so. + +2. **One residual is architecturally ceilinged**: the "user tagged a mapping label but not the trigger label" miss (Gap 2c) is invisible to the dispatcher precisely because the trigger-label query excludes it — you cannot diagnose what you never fetch without widening the query. The only zero-risk mitigation is documentation/default-comment making the two-label requirement explicit. + +3. **The installed base cannot be auto-migrated** without violating ADR-017 (Gap 6). The honest ceiling is: this default ships for NET-NEW installs; existing factories keep their config and opt in. + +Residual risk that must be ACCEPTED even with all Feasible gaps closed: (a) non-`origin`/non-GitHub remotes will fail repo discovery and require an explicit flag (ADR-014-compliant fail-loud); (b) AC-5 remains a downstream-formula guarantee, verified separately, not by this change; (c) existing factories are excluded by design. The achievable six-sigma target for THIS change is "the shipped default is valid-by-construction, the repo is correctly discovered or fails loud, and the validity is mechanically test-gated against drift" — Gaps 1, 3, 4, 7, 8 closed; Gaps 2, 5, 6 named-and-scoped. diff --git a/.designs/73/source.md b/.designs/73/source.md new file mode 100644 index 0000000..1e6cf3a --- /dev/null +++ b/.designs/73/source.md @@ -0,0 +1,159 @@ +# source.md — Verbatim Source Capture + +## Source References +- [x] Source 1: `https://github.com/stempeck/agentfactory/issues/73` — fetched at `2026-06-28T16:35:12Z` (re-fetched verbatim via `gh issue view 73 --json body` on 2026-06-28) +- Title: **New dispatch workflow default to be included with agentfactory** +- Labels: (none) + +## Problem Statement (verbatim) + +``` +### scenario + +We need to update dispatch.json to include a baked-in default for dispatch with agentfactory, so that when someone new to the project uses `./quickdocker.sh repo-link` and gets their container bootstrapped and lands in `/af/repo` with their newly bootstrapped repository ready to use agentfactory, they could opt to just start tagging their github issues with tags instead to kick off work without ever needing to visit the manager. + +The dispatch.json should be boostrapped when the dispatch.json is first created during initial setup of the repository so we know the repository-name and can include that appropriately in the dispatch.json, with something like this: +``` + +## Acceptance Criteria (verbatim) + +Each AC below is a clause extracted verbatim from the source. The source presents +AC-1..AC-3 in the `### scenario` section (the concrete deliverable) and AC-4..AC-6 +in the `### Acceptance criteria` section (the end-to-end success condition). + +### AC-1 +> "We need to update dispatch.json to include a baked-in default for dispatch with agentfactory" + +### AC-2 +> "they could opt to just start tagging their github issues with tags instead to kick off work without ever needing to visit the manager." + +### AC-3 +> "The dispatch.json should be boostrapped when the dispatch.json is first created during initial setup of the repository so we know the repository-name and can include that appropriately in the dispatch.json" + +### AC-4 +> "when we get to the step where we ask the manager,`run af sling --agent \"task description\"`, we should have the work executed autonomously using the step-by-step formula that represents the IDENTITY of that agent and respects the formulas rigid step-by-step process up to the point where human interaction is necessary for next steps" + +### AC-5 +> "all the code branches created should have been pushed as PR's against the main branch without doctor fixes or human interaction (unless absolutely necessary, or in the case of doctor - a `doctor --fix` is acceptable only as a bandaid/fix for legacy broken behaviors, not as an ongoing operational dependency)." + +### AC-6 +> "The agents should follow the known working formula process that IS their IDENTITY to perform their work so that we have consistent successful outcomes out of each agent. Your mission when addressing any problem scenario is to seek to understand how to achieve this desired outcome with systemic improvements while addressing the scenario." + +## Constraints (verbatim) + +### C-1 +> "update dispatch.json to include a baked-in default for dispatch with agentfactory" +> +> Inference: the default dispatch configuration must ship *with agentfactory* (baked in to the tool), not be hand-authored by the new user. + +### C-2 +> "The dispatch.json should be boostrapped when the dispatch.json is first created during initial setup of the repository" +> +> Inference: the bootstrap of the default MUST occur at the moment dispatch.json is first created during initial repository setup — not lazily, not on a later command. + +### C-3 +> "so we know the repository-name and can include that appropriately in the dispatch.json" +> +> and from the proposed JSON: `"org/repository" <-update this with the ACTUAL org/repository during the install` +> +> Inference: the `repos` entry must be populated with the ACTUAL `org/repository` discovered during install, not left as a literal placeholder. + +### C-4 +> "Assume *.md documents might have outdated information and you should review the codebase as the only real source-of-truth and only use them to help in your search. You should also rely on ./USING_AGENT_FACTORY.md and other ./.designs/*.md files for understanding the archtictural intentions of agentfactory before making recommendations." +> +> Inference: the codebase is the only authoritative source of truth; markdown docs may be stale and are search aids only. Architectural intentions are drawn from USING_AGENTFACTORY.md and .designs/*.md. + +### C-5 +> "without doctor fixes or human interaction (unless absolutely necessary, or in the case of doctor - a `doctor --fix` is acceptable only as a bandaid/fix for legacy broken behaviors, not as an ongoing operational dependency)." +> +> Inference: the autonomous path must work without `af doctor --fix` and without human intervention as an ongoing operational dependency. `doctor --fix` is tolerated only as a temporary bandaid for legacy broken behaviors. + +### C-6 [inferred] +> from the proposed JSON `mappings` / `workflows`: agents `rapid-soldesign-plan`, `rapid-implement`, `ultra-review`, `rapid-increment`; labels `rapid-plan`, `rapid-engineer`, `pr-review`, `pr-iterate`; workflow phases `["rapid-plan", "rapid-engineer"]`. +> +> Inference: every agent named in a `mappings[].agent` and every label/phase referenced by `workflows` must correspond to an agent/formula that actually exists and is dispatchable, or label-triggered dispatch will fail at runtime (this is required for AC-4, which demands the slung work actually execute). + +## Additional Context (verbatim) + +### "Before You Proceed" directive (verbatim) +``` +## Before You Proceed + +Read `USING_AGENTFACTORY.md` first. Before doing any investigation or code changes, state the Vision, Mission, and summarize the current usage flow in your own words — including how agents start, how they receive work, and how formulas drive execution. Do not proceed until you have done this. +``` + +### Setup flow described in the issue (verbatim) +``` +When the /agentfactory/ project is newly installed in a docker container the process usually goes something like this to setup and start working on a new repository: +1. `./quickdocker.sh ` is run (with appropriate parameters), and then the docker image is ready we're put into a bash with immediate access to agentfactory +2. `claude` is run and authenticated manually +3. `~/projects/agentfactory/quickstart.sh` is executed to build, install & initialize the factory +4. `af up manager` is run to start the manager +5. `af attach manager` is run to enter and issue commands +Like +6. `af up design-v3` is run to start an agent like design-v3 (for direct interaction) +7. `af attach design-v3` is run (for direct interact with an agent) +OR (recommended) +6. `af sling --agent design-v3 ""` is run to initiate the autonomous agent work +``` + +### Proposed default dispatch.json (verbatim — includes the source's typos/unclosed brackets; the *intent* matters, not the literal JSON) +``` +{ + "repos": [ + "org/repository" <-update this with the ACTUAL org/repository during the install + ], + "trigger_label": "agentic", + "notify_on_complete": "manager", + "interval_seconds": 300, + "retry_after_seconds": 1800, + "remove_trigger_after_dispatch": true, + "mappings": [ + { + "labels": [ + "rapid-plan + ], + "source": "issue", + "agent": "rapid-soldesign-plan" + }, + { + "labels": [ + "rapid-engineer" + ], + "source": "issue", + "agent": "rapid-implement" + }, + { + "labels": [ + "pr-review" + ], + "source": "pr", + "agent": "ultra-review" + }, + { + "labels": [ + "pr-iterate" + ], + "source": "pr", + "agent": "rapid-increment" + } + ], + "workflows": [ + { + "label": "feature-workflow", + "phases": ["rapid-plan", "rapid-engineer"] + } + ] +} +``` + +## Scope +**Determined scope: medium** — all 6 dimensions, standard depth (2-3 options each). +Rationale: the change is narrow in surface (a bootstrap of one config file) but +touches install/setup code, the dispatch config schema, repo-name discovery, and +correctness of agent/label references — multiple dimensions with real trade-offs. + +## Sign-off +- [x] Every AC from the source is quoted above under its own heading. +- [x] Every constraint from the source is quoted or marked [inferred] with its source text. +- [x] Nothing in this file has been summarized or paraphrased (verbatim blocks only; inferences explicitly labelled). diff --git a/.designs/73/synthesis-checklist.md b/.designs/73/synthesis-checklist.md new file mode 100644 index 0000000..1a5b654 --- /dev/null +++ b/.designs/73/synthesis-checklist.md @@ -0,0 +1,56 @@ +# synthesis-checklist.md — GATE B: Pre-Synthesis Source Re-grounding + Fidelity + +Re-opened `source.md` top to bottom RIGHT NOW and re-copied each AC verbatim below +(not via verification.md or any sub-agent output). Every clause is enumerated and +mapped to a specific design component. + +Component legend (from dependencies.md / synthesis): +- **K1** `DefaultDispatchConfigJSON(repo)` in `internal/config` (the baked-in default builder) +- **K2** repo-discovery helper in `runInstallInit` (`gh repo view` / `git remote get-url origin`) +- **K3** strict `owner/name` repo validator (security) +- **K4** `runInstallInit` starter-config wiring (replace install.go:145 literal; reuse write-if-absent EX4) +- **K5** quickstart specialist provisioning (the 4 referenced agents into agents.json) +- **K6** dispatch-loop unknown-agent skip-and-warn (defense-in-depth; write path stays strict) +- **K7** golden + cross-file tests +- **EX** existing machinery: `af sling --agent` specialist dispatch, formula step engine, `start_dispatch:true` auto-start (unchanged) + +## Gate 3.0 — Source Re-grounding (AC clause map) + +| AC key | Verbatim text (RE-COPIED from source.md right now) | Clauses (enumerated) | Each clause satisfied by which component? | +|--------|----------------------------------------------------|----------------------|-------------------------------------------| +| AC-1 | "We need to update dispatch.json to include a baked-in default for dispatch with agentfactory" | (i) update dispatch.json; (ii) a baked-in default; (iii) for dispatch with agentfactory | (i) K4 (replace install.go:145 literal); (ii) K1 (`DefaultDispatchConfigJSON`, single-source Go func); (iii) K1 content (the 4 mappings + feature-workflow + trigger_label "agentic") | +| AC-2 | "they could opt to just start tagging their github issues with tags instead to kick off work without ever needing to visit the manager." | (i) tag github issues with tags; (ii) to kick off work; (iii) without ever needing to visit the manager | (i) K1 mappings + trigger_label (NOTE: requires BOTH `agentic` AND a mapping label — see Risk/Gap-2); (ii) EX dispatcher sling + K5 (specialists must exist); (iii) EX `start_dispatch:true` auto-start (install.go:147) + K5 provisioning makes it real | +| AC-3 | "The dispatch.json should be boostrapped when the dispatch.json is first created during initial setup of the repository so we know the repository-name and can include that appropriately in the dispatch.json" | (i) bootstrapped when first created; (ii) during initial setup of the repository; (iii) so we know the repository-name; (iv) include that appropriately | (i) K4 + EX4 write-if-absent (install.go:152 = "first created"); (ii) K4 runInstallInit (called by quickstart configure_factory); (iii) K2 discovery (`git remote`/`gh`); (iv) K3 validate owner/name → K1 writes it into `repos` | +| AC-4 | "when we get to the step where we ask the manager,`run af sling --agent \"task description\"`, we should have the work executed autonomously using the step-by-step formula that represents the IDENTITY of that agent and respects the formulas rigid step-by-step process up to the point where human interaction is necessary for next steps" | (i) `af sling --agent "task"`; (ii) executed autonomously via the formula; (iii) the formula that IS the agent's IDENTITY; (iv) respects the rigid step-by-step process; (v) up to where human interaction is necessary | (i) EX `af sling --agent` specialist dispatch (sling.go, unchanged); (ii) EX formula instantiation + af prime/af done; (iii) K5 (the routed agents are formula-bearing specialists); (iv) EX formula step engine; (v) EX gate machinery | +| AC-5 | "all the code branches created should have been pushed as PR's against the main branch without doctor fixes or human interaction (unless absolutely necessary, or in the case of doctor - a `doctor --fix` is acceptable only as a bandaid/fix for legacy broken behaviors, not as an ongoing operational dependency)." | (i) branches pushed as PRs against main; (ii) without doctor fixes as ongoing dependency; (iii) without human interaction (unless absolutely necessary) | (i) property of the 4 routed FORMULAS (K5 routes; formula-internal push is OUT of this change's scope — see Six-Sigma Gap-5 scoping); (ii) K1 valid-by-construction + K5 provision → no `doctor --fix` needed (SEC2.3 rejected); (iii) EX zero-touch happy path; degraded path is the only-when-necessary exception | +| AC-6 | "The agents should follow the known working formula process that IS their IDENTITY to perform their work so that we have consistent successful outcomes out of each agent. Your mission when addressing any problem scenario is to seek to understand how to achieve this desired outcome with systemic improvements while addressing the scenario." | (i) follow the known working formula process that IS their identity; (ii) consistent successful outcomes out of each agent; (iii) systemic improvements while addressing the scenario | (i) K5 routes to existing formula-bearing specialists, NO formula edits; (ii) K1 single-source default + K5 provisioned specialists + K7 drift/golden test = repeatable validity; (iii) the whole design (K1–K7: single-source builder + repo discovery + provisioning fix + test gate) is systemic, not a one-off patch | + +**Gate 3.0 result:** Every AC in source.md (AC-1..AC-6) is listed; every clause is +individually enumerated; every clause maps to a specific component. No cell reads +"covered generally". One clause (AC-5 clause i) is explicitly scoped to the +formula layer (necessary-but-not-sufficient), NOT silently claimed — see Six-Sigma +Caveats in design-doc. + +## Gate 3.0b — Fidelity Gate (corrections to apply during synthesis) + +From `verification-report.md`: +- **23 of 24 load-bearing claims VERIFIED.** The single most load-bearing claim — + `ValidateDispatchConfig` hard-fails the entire dispatch cycle on the first unknown + mapped agent, invoked at `internal/cmd/dispatch.go:146` — is **VERIFIED firsthand**. + The C-6 resolution (K5 provision + K6 scoped tolerance) rests on solid ground. +- **1 INACCURATE (minor, non-load-bearing):** the existing workflow test is at + **`internal/config/dispatch_workflow_test.go`**, NOT `internal/cmd/`. When the + design references the model for the new golden test (K7), it MUST cite + `internal/config/dispatch_workflow_test.go`. No design decision changes. +- No other INACCURATE claims. design-doc.md must therefore contain ZERO claims + flagged INACCURATE — the only correction needed is the test path above. + +**Addendum — line-number fidelity note for synthesis:** quickstart.sh line numbers +(~:428/:442/:448-470) and a few deep dispatcher lines were read by the Phase-2.5 +sub-agent (approximate); the load-bearing struct/validation/install lines were +re-read firsthand by the main context and are exact. Synthesis should phrase +quickstart line numbers as approximate ("~") and the dispatch.go/install.go/config.go +lines as exact. + +**Gate B result: PASS** (Gate 3.0 AC clause-map complete + count-equal; Gate 3.0b +corrections recorded). diff --git a/.designs/73/ux.md b/.designs/73/ux.md new file mode 100644 index 0000000..128627c --- /dev/null +++ b/.designs/73/ux.md @@ -0,0 +1,147 @@ +# D3 — User Experience (ux.md) + +Owner of (verification.md): AC-2 (with Integration), AC-4 (with Integration), +AC-6 (with Integration, Scale), C-2 (with Integration), C-5 (with Integration, +Security). The "user" here is the NEW operator in the source's scenario: they run +`./quickdocker.sh `, land in `/af/repo`, and want to "just start tagging +their github issues with tags … without ever needing to visit the manager" +(source.md:13, AC-2). + +The happy path this dimension must protect (verbatim setup flow, source.md:85-98): +quickdocker → claude auth → quickstart.sh → `af up` → tag an issue → work runs +autonomously. Today the gap is that after quickstart, dispatch.json is EMPTY +(install.go:145, verified), so tagging an issue dispatches nothing. + +--- + +## U1. First-run discoverability of the baked-in default + +### Option U1.1 — Zero-touch: default is present after `quickstart.sh`; operator just tags an issue — RECOMMENDED + +After `quickstart.sh` runs `af install --init` (quickstart.sh:442, verified), the +non-empty default (D2) with the discovered repo (D1) is already on disk, and the +dispatcher auto-starts on `af up` (startup.json `start_dispatch:true`, +install.go:147, verified; codebase-snapshot Decision History notes #58 made the +dispatcher auto-start). The operator's ONLY action is to apply a mapping label +(e.g. `rapid-plan`) to a GitHub issue. + +- Trade-offs: This is the literal AC-2 outcome — "without ever needing to visit the + manager." No new command to learn, no config to edit. The discoverability cost + shifts to "how does the operator know which labels to apply" — addressed by U2. +- Reversibility: Easy. +- Constraints: satisfies AC-2, C-2, C-5 (no human config step, no doctor). The + dispatcher auto-start is a deliberate recent change (#58, Decision History) that + this option RELIES ON rather than reverses. Recommended. + +### Option U1.2 — Print a "next steps" banner after install listing the dispatch labels — RECOMMENDED (complementary) + +After writing the default, `runInstallInit` (or quickstart's completion banner, +quickstart.sh:700-713, verified) prints the active trigger label and mapping labels +("Tag an issue with `rapid-plan`, `rapid-engineer`, `pr-review`, or `pr-iterate` to +dispatch work autonomously"). + +- Trade-offs: Closes the "which labels" discoverability gap from U1.1 at near-zero + cost (a few `fmt.Fprintln`). The banner content is derived from the default the + same run wrote, so it cannot drift. Slightly more install output. +- Reversibility: Easy. +- Constraints: satisfies AC-2 ergonomics; no constraint conflict. Recommended as a + complement to U1.1. + +### Option U1.3 — Interactive post-install prompt asking the operator to confirm/choose labels — REJECTED + +- REJECTED: contradicts [ADR-014]. ADR-014 (Decision History) forbids interactive + prompting in `cmd/`/`internal/` Go paths at runtime; `runInstallInit` is Go in + `internal/cmd/`. A prompt here is exactly the shape ADR-014 prohibits. (A prompt + in quickstart.sh is technically ADR-014-exempt as an operator bootstrap script, + but the ADR says such exemptions "should be minimized over time and not + proliferated" — and U1.1/U1.2 deliver the outcome with no prompt at all.) + +--- + +## U2. Failure-message ergonomics when the autonomous path can't run + +The new operator must not be left silently stuck. Three failure surfaces exist: +(a) repo discovery failed at install (D1/A3); (b) the trigger label was applied but +the mapped specialist isn't provisioned (C-6); (c) `gh auth` missing at dispatch. + +### Option U2.1 — Each failure emits a structured, actionable stderr message naming the exact remedy flag/command — RECOMMENDED + +(a) discovery fail → warn naming `af config dispatch set` (config_set.go:24-32, +verified). (b) unknown-agent mapping → the dispatcher's existing +`ValidateDispatchConfig` error already names the agent +("dispatch mapping references unknown agent %q", dispatch.go:102, verified); the +remedy message should add "run `af install --agents` to provision specialists." +(c) `gh auth` missing → the dispatcher already checks `gh auth` +(codebase-snapshot §4) and the existing query path warns per-repo +("warning: failed to query issues for %s", dispatch.go:180, verified). + +- Trade-offs: Reuses existing error sites; the only NEW text is the remedy hint. + Matches ADR-014's required shape ("fail loud with a structured error naming the + exact flag that expresses the intent"). Honest about what's missing. +- Reversibility: Easy. +- Constraints: satisfies C-5 (no human-fix-as-dependency for the HAPPY path; these + messages only fire on a genuinely broken setup), complies with ADR-014. + Recommended. + +### Option U2.2 — Silent best-effort: skip what can't run, no message — REJECTED + +- REJECTED: fails [AC-6], violates [C-5]. AC-6 demands "consistent successful + outcomes" and "systemic improvements"; a silent skip leaves the operator unable + to diagnose why tagging an issue produced nothing, turning a one-time bootstrap + gap into a recurring "why isn't this working" support burden — the opposite of + C-5's "without human interaction as an ongoing operational dependency." + +--- + +## U3. Keeping the autonomous formula path (AC-4) ergonomic end-to-end + +AC-4: `af sling --agent "task"` must run the agent's formula autonomously +"up to the point where human interaction is necessary." This is EXISTING behavior +(`af sling --agent` specialist dispatch, codebase-snapshot §4, sling.go verified: +`resolveSpecialistAgent` instantiates the formula). This dimension's UX job is to +ensure the dispatch.json default ROUTES to specialists that actually have this +behavior — which D2's 4 referenced agents do (all are formula-bearing specialists, +codebase-snapshot §3). + +### Option U3.1 — Default routes only to formula-bearing specialists (no behavior change to sling) — RECOMMENDED + +The 4 mapped agents are all formula-bearing (codebase-snapshot §3), so a dispatched +label drives `af sling --agent --reset ` (dispatch.go cycle step 6, +codebase-snapshot §4), which instantiates the formula and runs it via the agent's +own `af prime`/`af done` loop until a gate. No change to sling/done/prime; the UX +is "tag → formula runs → PR appears." + +- Trade-offs: Zero new UX surface; leans entirely on the verified existing + specialist-dispatch + formula-step machinery, which is exactly what AC-6 wants + ("the known working formula process that IS their IDENTITY"). The only + requirement is the C-6 rider (D2): those specialists must be provisioned. +- Reversibility: Easy. +- Constraints: satisfies AC-4, AC-5 (formula path pushes PRs without doctor/human — + an existing property of the rapid-* formulas, which Integration/D6 confirms), + AC-6. Recommended. + +## Reversibility (this dimension): Easy + +UX changes are an install-time banner and stderr remedy hints — additive text, +no behavioral lock-in. + +## Dependencies produced + +- PROVIDES to **Integration (D6)**: the requirement that the bootstrap banner / + remedy messages name `af install --agents` and `af config dispatch set` (so D6 + must confirm those are the real provisioning/repair commands — both verified). +- REQUIRES from **Data (D2)**: the mapping labels to print in the U1.2 banner. +- REQUIRES from **Integration (D6)**: confirmation that the dispatcher auto-starts + on `af up` (startup.json `start_dispatch:true`, install.go:147, verified) so U1.1 + is true. +- REQUIRES from **Security (D5)**: the discovered repo string is safe to echo in a + banner (no terminal-escape injection from a crafted remote URL). + +## Risks identified + +| Risk | Severity | Mitigation | +|------|----------|------------| +| Operator tags an issue but the specialist isn't provisioned (C-6) → silent no-op confuses them | HIGH | U2.1 actionable error naming `af install --agents`; D2/D6 C-6 rider provisions specialists in bootstrap | +| Operator doesn't know which labels to apply | Medium | U1.2 next-steps banner lists the active labels, derived from the just-written default | +| `gh auth` not yet done when the dispatcher first polls | Medium | Existing per-repo warning (dispatch.go:180); setup flow authenticates `gh`/claude at step 2 (source.md:90) before `af up` | +| Banner echoes a crafted repo string containing terminal escapes | Low | Security (D5) sanitizes/validates the repo string before it is stored or echoed | diff --git a/.designs/73/verification-report.md b/.designs/73/verification-report.md new file mode 100644 index 0000000..4b0d8f4 --- /dev/null +++ b/.designs/73/verification-report.md @@ -0,0 +1,59 @@ +# Fidelity Verification Report + +**Note on provenance:** A Phase-2.5 Fidelity Verifier sub-agent was spawned and +completed its claim inventory (it reported the load-bearing claims VERIFIED and one +minor test-file-location nuance), but was stopped before it flushed this file under +session-termination pressure from a mis-firing step-fidelity gate. The main context +then **independently re-verified every load-bearing codebase claim firsthand** +(reading `internal/config/dispatch.go`, `internal/cmd/dispatch.go`, +`internal/cmd/install.go`, `internal/config/config.go`, `internal/config/paths.go`, +and `internal/cmd/config_set.go` this session) and compiled the report below. Every +"VERIFIED" row states the verification action taken. + +## Summary +- Total load-bearing claims checked: 24 +- Verified: 23 +- Inaccurate: 1 (minor, non-load-bearing — corrected below) +- Unverifiable (proposed new code, excluded): the I3.1/I3.2/K1–K7 proposals are + about NEW code and are out of scope for fidelity verification. + +## Claim Details + +| # | Claim | Source File | Classification | Correction (if inaccurate) | +|---|-------|-------------|----------------|----------------------------| +| 1 | `DispatchConfig` struct fields (`repos`,`trigger_label`,`notify_on_complete`,`mappings`,`interval_seconds`,`retry_after_seconds`,`remove_trigger_after_dispatch`,`workflows`) at `internal/config/dispatch.go:17-27` | data.md, snapshot §6, api.md | VERIFIED | Read dispatch.go:17-27 firsthand — every field + json tag matches the proposed JSON exactly. | +| 2 | `DispatchMapping` (`label` deprecated, `labels`, `source`, `agent`) at dispatch.go:30-35; `Workflow` (`label`,`phases`) at :42-45 | data.md, snapshot §6 | VERIFIED | Read firsthand. | +| 3 | `ValidateDispatchConfig` HARD-FAILS (returns err) on the FIRST mapping whose agent is absent from agents.json — `"dispatch mapping references unknown agent %q"` | elevation, six_sigma Gap1, conflicts D2×D6 | VERIFIED | Read dispatch.go:93-138 firsthand: `for _, m := range disp.Mappings { if _, ok := agents.Agents[m.Agent]; !ok { return fmt.Errorf(...) } }`. THE load-bearing C-6 claim — CONFIRMED. | +| 4 | `ValidateDispatchConfig` is called at the dispatcher path AND the write path | integration, conflicts, six_sigma | VERIFIED | Grepped firsthand: `internal/cmd/dispatch.go:146` (dispatcher) and `internal/cmd/config_set.go:89` (write path). Both callers confirmed. | +| 5 | `LoadDispatchConfig` runs struct-only validation, NOT the cross-file agents.json check (dispatch.go:48-65) | data.md, snapshot §6 | VERIFIED | Read firsthand — calls `validateDispatchConfig` only; cross-file check is separate. | +| 6 | Default `agents.json` from `af install --init` contains ONLY `manager`+`supervisor` (install.go:143) | elevation, six_sigma, data.md | VERIFIED | Read install.go:143 firsthand — `{"agents":{"manager":{"type":"interactive",...},"supervisor":{"type":"autonomous",...}}}`. | +| 7 | Default `dispatch.json` from install is empty (`repos:[]`, `mappings:[]`, no workflows) at install.go:145 | snapshot §6, all dims | VERIFIED | Read firsthand. | +| 8 | dispatch.json write is idempotent write-if-absent (`os.Stat … os.IsNotExist`) at install.go:150-157 | data.md, six_sigma Gap6 | VERIFIED | Read firsthand — confirms C-2 "first created". | +| 9 | `factory.json` uses `config.DefaultFactoryConfigJSON()` (install.go:142); dispatch.json is the lone inline literal | elevation, snapshot DecHistory | VERIFIED | Read firsthand at install.go:142 vs :145. | +| 10 | `DefaultFactoryConfigJSON()` is at `internal/config/config.go:111` | data.md, integration, dependencies | VERIFIED | Grepped firsthand: `config.go:111`. | +| 11 | `DispatchConfigPath(root)` = `/.agentfactory/dispatch.json` (paths.go:19); `dotDir=".agentfactory"` (:10) | snapshot §6 | VERIFIED | Read paths.go firsthand. | +| 12 | All 4 referenced formulas (`rapid-soldesign-plan`,`rapid-implement`,`ultra-review`,`rapid-increment`) exist in BOTH `install_formulas/` and `store/formulas/`, version 1 | snapshot §3, data.md | VERIFIED | Sub-agent C listed all 15 formula files in both dirs; cross-checked. | +| 13 | The 4 specialists are formula-bearing specialists in THIS factory's agents.json | snapshot §3/§7 | VERIFIED | `af agents list --json` firsthand shows all 4 with `formula` set. (⚡ this factory; a FRESH agents.json lacks them — claim 6.) | +| 14 | `startup.json` default sets `start_dispatch:true` (dispatcher auto-starts on `af up`) at install.go:147 | audit, integration, ux | VERIFIED | Read install.go:147 firsthand. | +| 15 | `runInstallInit` takes no repo arg and does NO git-remote lookup; NO code under `internal/` reads a git remote for repo identity | elevation, six_sigma Gap3, snapshot §6 | VERIFIED | Sub-agent B + elevation grep (0 hits for git-remote read in internal/); consistent with install.go I read (runInstallInit builds starterConfigs without any repo input). | +| 16 | `quickstart.sh` `cd`s into the discovered repo (~:428) BEFORE `af install --init` (~:442) and provisions only manager+supervisor (~:448-470); never runs `af install --agents` | integration, six_sigma Gap1, snapshot §6 | VERIFIED | Sub-agent B read quickstart.sh; consistent with the documented flow. (Line numbers approximate per sub-agent read.) | +| 17 | `quickdocker.sh` already does `git remote get-url origin` for the agentfactory repo itself (~:41) — the technique is proven in-tree | six_sigma Gap3, elevation | VERIFIED | Sub-agent B read quickdocker.sh. | +| 18 | `startDispatch` swallows `ErrNotFound`/`ErrMissingField` as a benign "skipping dispatch (dispatch.json not configured)" message (dispatch.go:1328-1334) | six_sigma Gap3/Gap8 | VERIFIED | Grepped+read firsthand: dispatch.go:1328 `errors.Is(err, ErrNotFound)||errors.Is(err, ErrMissingField)` → :1330 prints "skipping dispatch (dispatch.json not configured)". | +| 19 | `queryGitHubIssues`/`queryGitHubPRs` filter GitHub by `--label triggerLabel` at query time (a hard pre-filter) | six_sigma Gap2 | VERIFIED | Read firsthand: dispatch.go:178/194 call with `dispatchCfg.TriggerLabel`; :298-301 `queryGitHubIssues(... "--label", triggerLabel ...)`. Confirms an item lacking the trigger label is never fetched. | +| 20 | `validateWorkflows` (dispatch.go:192) + `phaseResolvesAlone` (dispatch.go:256) enforce: each phase resolves to a single-label mapping; phases share one source; workflow label ≠ trigger/mapping label | data.md, snapshot, six_sigma Gap4 | VERIFIED | Grepped firsthand: both functions exist at the cited lines; `phaseResolvesAlone` used inside `ValidateDispatchConfig` (read firsthand). The proposed `feature-workflow` satisfies these (phases `rapid-plan`/`rapid-engineer` are single-label mappings, both source "issue"). | +| 21 | Import-cycle discipline: `internal/formula` imports `internal/config`, so `ValidateDispatchConfig` deliberately does NOT import `internal/formula` (dispatch.go:130-136 comment) | dependencies.md | VERIFIED | Read the comment firsthand inside `ValidateDispatchConfig`. Any new validation in `internal/config` must preserve this. | +| 22 | ADR-014 (no interactive prompting in cmd/internal; bootstrap scripts exempt), ADR-017 (no customer-data deletion; inside-af writes OK), ADR-008 (embed drift test), ADR-015 (formula three-location lifecycle) | snapshot DecHistory, all dims | VERIFIED | Read ADR-014, ADR-017, ADR-008 files firsthand; ADR-015 title/decision confirmed via snapshot + grep. Quotes in snapshot match the ADR text. | +| 23 | `dispatcher` runs in a tmux loop via `launchDispatchSession` (dispatch.go:1279-1282), so validation failures print to that pane/log, not the user's foreground | six_sigma Gap8 | VERIFIED | Grepped firsthand: `launchDispatchSession` at dispatch.go:1279/1282. | +| 24 | Existing dispatch tests `dispatch_workflow_test.go`, `dispatch_test.go` exist (model for the new golden test) | six_sigma Gap4, integration | INACCURATE (minor) | `dispatch_workflow_test.go` lives in **`internal/config/`**, NOT `internal/cmd/`. The file EXISTS (so "no golden test of the SHIPPED default" stands), but any design note implying it is under `internal/cmd/` should say `internal/config/dispatch_workflow_test.go`. Non-load-bearing. | + +## Fidelity verdict for synthesis + +- **The single load-bearing claim (C-6: `ValidateDispatchConfig` hard-fails the + whole dispatch cycle on an unknown mapped agent, invoked at dispatch.go:146) is + VERIFIED firsthand.** The design's central decision (provision the 4 specialists + + scope dispatcher tolerance) rests on solid ground. +- No INACCURATE claim is load-bearing. The only correction is a test-file path + (`internal/config/dispatch_workflow_test.go`), which does not change any design + decision. +- Synthesis may use all sub-agent codebase claims as-is, applying the single + correction in row 24 when referencing the existing workflow test location. diff --git a/.designs/73/verification.md b/.designs/73/verification.md new file mode 100644 index 0000000..be8b8b4 --- /dev/null +++ b/.designs/73/verification.md @@ -0,0 +1,35 @@ +# verification.md — GATE A: Forced-Enumeration Constraint + AC Verification + +Every row is COPIED verbatim from `source.md` and assigned a dimension owner. +Dimensions: **API**, **Data**, **UX**, **Scale**, **Security**, **Integration**. + +## Table A — Constraint Verification + +| C ref | Verbatim text (copy from source.md) | My interpretation | Prohibits | Requires | Dimension owner(s) | Relaxation impact | +|-------|-------------------------------------|-------------------|-----------|----------|--------------------|-------------------| +| C-1 | "update dispatch.json to include a baked-in default for dispatch with agentfactory" | The default config ships *with* the `af` tool (in-binary), not hand-authored by the new user | Shipping an empty/placeholder-only default that the user must fully author by hand | A non-empty, useful default baked into install (`DefaultDispatchConfigJSON()`-style) | Integration, API, Data | Relax → back to today's empty `{repos:[],mappings:[]}`; user must hand-edit; AC-2 fails | +| C-2 | "The dispatch.json should be boostrapped when the dispatch.json is first created during initial setup of the repository" | Bootstrap occurs at the moment dispatch.json is FIRST created during init — not lazily, not later | Clobbering an existing (customer-edited) dispatch.json; deferring the default to a separate command | The default to be written by the same idempotent write-if-absent path in `runInstallInit` (install.go:152) | Integration, UX | Relax → default applied on a later command; first-run autonomous path (AC-2/AC-4) not available out-of-box | +| C-3 | "so we know the repository-name and can include that appropriately in the dispatch.json" (and: "org/repository <-update this with the ACTUAL org/repository during the install") | The `repos` entry must be populated with the ACTUAL org/repository discovered during install, not a literal placeholder | Leaving `repos` as `["org/repository"]` placeholder (dispatcher would query a non-existent repo) | A non-interactive repo-identity discovery at init time (e.g. `git remote`) feeding `repos` | Integration, API, Security | Relax → `repos` left empty/placeholder; dispatcher polls nothing or errors; AC-2 fails for a fresh repo | +| C-4 | "Assume *.md documents might have outdated information and you should review the codebase as the only real source-of-truth and only use them to help in your search." | The Go codebase is the authoritative truth; markdown (incl. USING_AGENTFACTORY.md) may be stale and is a search aid only | Designing against a doc claim contradicted by code (e.g. doc says dispatch.json "created by af install --init" — verified TRUE but with EMPTY default) | Every design claim anchored to verified code (file:line), per codebase-snapshot.md | Integration, Data | Relax → risk designing to a stale doc; fidelity gate (Phase 2.5) would catch it | +| C-5 | "without doctor fixes or human interaction (unless absolutely necessary, or in the case of doctor - a `doctor --fix` is acceptable only as a bandaid/fix for legacy broken behaviors, not as an ongoing operational dependency)" | The autonomous path must work without `af doctor --fix` and without human intervention as an ongoing dependency; doctor --fix tolerated only as a temporary legacy bandaid | Designs that require a human to fix config, or that depend on `doctor --fix` to repair the default on every run; interactive prompts in Go init code (ADR-014) | A default that is valid-by-construction and a non-interactive repo discovery; fail-loud-with-flag instead of prompt | UX, Integration, Security | Relax → operator must run doctor/edit configs each setup; violates the "without human interaction" outcome | +| C-6 [inferred] | "mappings": [...agent: "rapid-soldesign-plan" / "rapid-implement" / "ultra-review" / "rapid-increment"...], "workflows": [{label: "feature-workflow", phases: ["rapid-plan","rapid-engineer"]}] | Every agent named in mappings and every label/phase in workflows must correspond to an agent/formula that exists and is dispatchable, or label-triggered dispatch fails at runtime | A default whose mappings reference agents absent from the fresh `agents.json` (manager+supervisor only) → `ValidateDispatchConfig` cross-file failure | The 4 specialist agents to be present in the default `agents.json` (or default mappings to reference only provisioned agents, or dispatcher to skip unknown-agent mappings) | Data, Integration, Security | Relax → dispatch cross-file validation fails on fresh install; whole dispatch cycle errors; AC-2/AC-4 fail | + +## Table B — AC Verification + +| AC ref | Verbatim text (copy from source.md) | My interpretation | What evidence proves it works? | Dimension owner(s) | +|--------|-------------------------------------|-------------------|--------------------------------|--------------------| +| AC-1 | "We need to update dispatch.json to include a baked-in default for dispatch with agentfactory" | Ship a non-empty default dispatch config baked into install | Unit test asserting `af install --init` writes a dispatch.json whose mappings/workflows match the baked-in default (golden compare) | Data, Integration | +| AC-2 | "they could opt to just start tagging their github issues with tags instead to kick off work without ever needing to visit the manager." | After a bootstrapped setup, applying the trigger/mapping labels to a GitHub issue dispatches work with zero manager interaction | E2E: on a fresh setup, label an issue with the dispatch labels; the dispatcher slings the mapped agent and work begins, no manager prompt | UX, Integration | +| AC-3 | "The dispatch.json should be boostrapped when the dispatch.json is first created during initial setup of the repository so we know the repository-name and can include that appropriately in the dispatch.json" | At first creation during init, dispatch.json is written with the actual repo name in `repos` | Unit/integration test: in a temp repo with a known `git remote origin`, `af install --init` produces dispatch.json with `repos:["/"]` | Integration, Data | +| AC-4 | "when we get to the step where we ask the manager,`run af sling --agent \"task description\"`, we should have the work executed autonomously using the step-by-step formula that represents the IDENTITY of that agent and respects the formulas rigid step-by-step process up to the point where human interaction is necessary for next steps" | `af sling --agent "task"` instantiates that agent's formula and the agent executes its steps autonomously until a genuine human-gate | Behavioral: sling a specialist on a fresh factory; observe formula steps advancing via af prime/af done up to a gate, no human input | Integration, UX | +| AC-5 | "all the code branches created should have been pushed as PR's against the main branch without doctor fixes or human interaction (unless absolutely necessary, or in the case of doctor - a `doctor --fix` is acceptable only as a bandaid/fix for legacy broken behaviors, not as an ongoing operational dependency)." | Slung agents push their branches as PRs against main, with no `doctor --fix` or human step as an ongoing dependency | Behavioral: a slung agent completes and a PR appears against main, with no doctor invocation in the run log | Integration, Security | +| AC-6 | "The agents should follow the known working formula process that IS their IDENTITY to perform their work so that we have consistent successful outcomes out of each agent. Your mission when addressing any problem scenario is to seek to understand how to achieve this desired outcome with systemic improvements while addressing the scenario." | The fix must be a systemic improvement that makes formula-driven autonomous outcomes consistent, not a one-off patch to the scenario | Design review: the solution keeps the dispatch.json default, agents.json, repo discovery, and startup consistent so dispatch reliably reproduces; no per-run manual fixups | Integration, UX, Scale | + +## Gate 1.4 — Forced-Enumeration check + +- source.md AC headings (`^### AC-`): **6** ↔ verification.md AC rows (`^| AC-`): **6** → MATCH +- source.md C headings (`^### C-`): **6** ↔ verification.md C rows (`^| C-`): **6** → MATCH +- Every "verbatim text" column is a direct quote (quotation marks / copied JSON fragments). +- Every row has at least one dimension owner. + +GATE A: PASS (pending the bash count-equality assertion). From 193a9802ec61401562013e2508f3df1be59072e8 Mon Sep 17 00:00:00 2001 From: agentfactory Date: Sun, 28 Jun 2026 17:44:20 +0000 Subject: [PATCH 3/9] rapid-soldesign-plan: cross-reviewed design artifacts for #73 (New dispatch workflow default to be included with agentfactory) Co-authored-by: agentfactory-cli <293373236+agentfactory-cli@users.noreply.github.com> --- .../73/cross-review/analyst-review-design.md | 58 +++++++++ .../73/cross-review/designer-update-log.md | 50 ++++++++ .designs/73/design-doc.md | 118 ++++++++++++------ .designs/73/design-refinement-progress.md | 8 +- 4 files changed, 194 insertions(+), 40 deletions(-) create mode 100644 .designs/73/cross-review/analyst-review-design.md create mode 100644 .designs/73/cross-review/designer-update-log.md diff --git a/.designs/73/cross-review/analyst-review-design.md b/.designs/73/cross-review/analyst-review-design.md new file mode 100644 index 0000000..d6af713 --- /dev/null +++ b/.designs/73/cross-review/analyst-review-design.md @@ -0,0 +1,58 @@ +# Analyst Cross-Review of `design-doc.md` — Round 1 (Issue #73) + +**Reviewer:** rootcause-all (Analyst) +**Date:** 2026-06-28 +**Basis:** Grounded in `.analysis/73/rootcause_analysis.md` (9 concerns; 8 VALIDATED, 1 INVALIDATED) and the per-concern investigations `rootcause_concern_{1..9}.md`. All citations re-checked against the codebase. + +## Summary Verdict + +The design is **strong and substantially correct** — it independently converges with my root-cause findings on every primary point. Two issues rise to **CRITICAL** because they are entry-point/sequencing gaps that defeat the design's own "valid-by-construction" and "without visiting the manager" guarantees on documented paths; both are directly evidenced by my VALIDATED concerns #1/#4 and #7. Fixing them is small and keeps the design's shape intact. + +## Convergence (no action needed — recorded for confidence) + +| Design element | My analysis | Agreement | +|---|---|---| +| K1 `DefaultDispatchConfigJSON(repo)` single-source builder (config.go:111 idiom) | Solution §"DefaultDispatchConfigJSON" | ✅ identical | +| K2 repo discovery via `gh repo view --json nameWithOwner` + `git remote` fallback, warn-don't-abort | Concern 2 recommended mechanism (`detectRepoSlug` on `runGitDetect` seam) | ✅ identical | +| Empty-`repos` fallback preserves friendly-skip | Concern 1/7 (`dispatch.go:142-150`, `:1327-1339`) | ✅ | +| K7 golden + cross-file drift test | Solution §"drift-interlock test (Poka-yoke)" | ✅ identical intent | +| Two-label requirement (`agentic` + mapping label) is a UX trap to document | Concern 8 | ✅ | +| AC-5 "no doctor": `doctor` is not a real command; PR-push is a formula-layer property | Concern 9 (`e2e_sling_test.go:67-69`) | ✅ identical | +| Architecture-elevation: Frame correct + repo self-derivation lift | (consistent with my synthesis) | ✅ | + +## CRITICAL + +### C1 — "Valid-by-construction" holds only on the quickstart path; bare `af install --init` is left internally inconsistent +**Evidence:** Concern 1 + Concern 4. The default `dispatch.json` (with the four mappings) is written by **`runInstallInit`** (K4, `install.go:145`), but the four specialists are provisioned by **K5 in `quickstart.sh configure_factory` (~:414-471)** — a *different* entry point. `af install --init` registers only `manager`+`supervisor` (`install.go:143`), and `ValidateDispatchConfig` hard-fails the dispatch cycle on the first unknown mapped agent (`internal/config/dispatch.go:101-102`, invoked at `internal/cmd/dispatch.go:146`). Therefore any path that runs `af install --init` **without** quickstart — including the documented "hard way" in `USING_AGENTFACTORY.md:40-52` (`af install --init` → `af install manager` → `af install supervisor`) — produces a populated default whose mappings reference unregistered agents. The cross-file invariant the design's own K7 test asserts ("default cross-validates against default+provisioned agents.json") is only true on the quickstart path; on bare init it is **false by construction**. + +**Why K6 doesn't save it:** with all four agents unknown (bare-init), the K6 skip-and-warn tolerance makes the dispatcher *run but match nothing* — functionally identical to the empty default, so AC-2 still fails for these users. + +**Recommended fix (small, keeps the design's shape):** make the default valid-by-construction *within `runInstallInit` itself* — seed the four specialist entries (`{"type":"autonomous","formula":""}`) into the default `agents.json` in the same write, rather than relying on a separate `quickstart.sh` step. I verified the four role templates are **already embedded** (`internal/templates/roles/{rapid-soldesign-plan,rapid-implement,ultra-review,rapid-increment}.md.tmpl` exist), so a seeded `agents.json` entry is sufficient for `af prime`/sling to resolve each specialist — no `agent-gen` run is required at install. This collapses K5's cross-entry-point dependency into the single `--init` write and makes K7's cross-file invariant hold on **every** init path. (Mirror the `DefaultAgentsConfigJSON()` single-source idiom alongside `DefaultDispatchConfigJSON()`.) + +### C2 — The design relies on dispatcher auto-start but does not address that the documented `af up manager` flow does NOT start it +**Evidence:** Concern 7. Auto-start of the dispatcher fires **only on the blanket `af up`** (no positional args): the `startDispatch` call sits inside `if blanket {` (`internal/cmd/up.go:92, 306, 330`). The issue's documented setup flow is `af up manager` (positional — `USING_AGENTFACTORY.md:67`, and the issue body steps 4–5), which is **gated out** and will not launch the polling loop even with a valid populated config and provisioned agents. The design cites "EX dispatcher auto-start (`start_dispatch:true`, install.go:147)" as satisfying AC-2 (rows AC-2, D3×D6) but nowhere reconciles the blanket-vs-positional gating. So the "just tag an issue without visiting the manager" promise breaks for any user who follows the documented `af up manager` step. + +**Recommended fix:** include an explicit component to either (a) hoist the `if startupCfg.StartDispatch { startDispatch(...) }` block out of the `blanket`-only gate so it runs on any `af up` (the launch is idempotent — already-running is a benign no-op, `dispatch.go:1322-1325`), or (b) at minimum, change the documented flow/docs to use bare `af up` and state that positional `af up ` does not start dispatch. Option (a) is the robust interlock and is the one my analysis recommends. This belongs in Phase 2 alongside K5/K6/K8. + +## HIGH + +### H1 — Pick the minimal K5 provisioning mechanism; `agent-gen-all.sh` is heavyweight and disruptive +**Evidence:** Concern 4 + my template-embedding verification. K5 currently says "`agent-gen-all.sh` direct (or targeted `af formula agent-gen` for the 4)" — it does not commit to one. `agent-gen-all.sh` runs `af down --all` and regenerates **every** template plus a full rebuild (`USING_AGENTFACTORY.md:636`), which is costly and could disrupt the bootstrap sequence mid-`configure_factory`. Since the four templates are already embedded, the **minimal** mechanism is direct `agents.json` seeding (my C1 fix) — no rebuild, no `af down --all`, no recursion. If the design prefers an `agent-gen` call, scope it to the **four targeted** agents, never the all-variant. Commit to one mechanism (single-solution discipline). + +### H2 — K6 skip-and-warn can mask "configured-but-broken" as "running," degrading observability vs. the clean friendly-skip +**Evidence:** Concern 1/7. Today an unconfigured default produces a clean, single "not configured" friendly-skip (`dispatch.go:1327-1339`). With a populated-but-unprovisioned default (the bare-init case), K6 makes the loop *start and then skip every mapping with warnings in a tmux pane* — which looks active but dispatches nothing, an arguably worse signal. K6 is sound as defense-in-depth, but it must be paired with K8 so `af up`/`af dispatch status` reports a **distinct** "references unprovisioned agents" state (the design lists this under K8 — good; the review note is to make K8 **non-optional** wherever K6 is enabled, not a "feasible/additive" nicety, since K6 without K8 hides the failure my C1 describes). + +## LOW + +- **L1 — `notify_on_complete` omission:** Agree. Defaulting to `"manager"` via `validateDispatchConfig` with a smaller validation surface (Gap-7) is sound; runtime behavior is unchanged. No action. +- **L2 — AC-5 formula-layer scoping:** Agree, and it matches Concern 9 exactly (PR-push lives in the formulas; `origin`+`gh` auth are container-layer via `quickdocker.sh:557/:567`, not `af install --init`). Good, honest scoping. +- **L3 — K3 `owner/name` validator:** Good security addition my analysis did not cover (injection via crafted remote). Concur; note the dispatcher's existing `strings.Cut(repo, "/")` (`dispatch.go:537-539`) is a useful second line of defense (the design already nods to this). +- **L4 — Installed-base scope (net-new only, ADR-017):** Agree it must be named; the write-if-absent idempotency (`install.go:150-157`) is the right boundary. No action. + +## Disposition + +- **CRITICAL (must address before implementation):** C1 (seed specialists in `runInstallInit` so the default is valid-by-construction on every init path), C2 (handle dispatcher auto-start on the documented `af up manager` path). +- **HIGH (should address):** H1 (commit to the minimal provisioning mechanism), H2 (make K8 mandatory wherever K6 is enabled). +- **LOW:** L1–L4 are agreements/nits. + +Both CRITICAL items are small, localized changes that *strengthen* the design's own valid-by-construction thesis rather than redirect it. With C1 and C2 incorporated, the design fully satisfies AC-2 and AC-3 on every documented setup path, not just the quickstart happy path. diff --git a/.designs/73/cross-review/designer-update-log.md b/.designs/73/cross-review/designer-update-log.md new file mode 100644 index 0000000..732b35c --- /dev/null +++ b/.designs/73/cross-review/designer-update-log.md @@ -0,0 +1,50 @@ +# Designer Update Log — Cross-Review Round 1 (Issue #73) + +**Designer:** design-v7 +**Date:** 2026-06-28 +**Reviewer source:** `.designs/73/cross-review/analyst-review-design.md` (rootcause-all), grounded in `.analysis/73/rootcause_analysis.md`. +**Disposition:** ALL CRITICAL (C1, C2) and HIGH (H1, H2) findings incorporated into `.designs/73/design-doc.md`. LOW (L1–L4) were already-agreements; no change required. Each analyst claim was re-verified firsthand against the codebase before incorporation. + +## Firsthand verification of the analyst's claims (before editing) + +| Claim | Verification command/result | Verdict | +|-------|------------------------------|---------| +| C1: 4 role templates already embedded | `ls internal/templates/roles/{rapid-soldesign-plan,rapid-implement,ultra-review,rapid-increment}.md.tmpl` → all 4 exist | CONFIRMED | +| C1: no `DefaultAgentsConfigJSON` exists yet | `grep -rn "func DefaultAgentsConfigJSON" internal/config/` → none | CONFIRMED (would be NEW) | +| C2: `startDispatch` gated by `blanket` | `up.go:306 if blanket {` wraps `up.go:330 if startupCfg.StartDispatch { startDispatch(...) }` | CONFIRMED — `af up manager` (positional) is gated out | +| H1: `startDispatch` idempotent | `dispatch.go:1322-1325` → `if running { "Dispatcher already running"; return nil }` | CONFIRMED — hoisting is a safe no-op if running | + +## Changes applied (per finding) + +### C1 (CRITICAL) — bare `af install --init` left the populated default invalid-by-construction +- **Change:** Redefined **K5** from "provision specialists via quickstart/`agent-gen-all.sh`" to **"`DefaultAgentsConfigJSON()` seeds the 4 specialist registry entries into the default `agents.json` within `runInstallInit` (the SAME write as K4)"**. Because the 4 role templates are already embedded, a seeded `agents.json` entry (with `formula`) is sufficient for `af prime`/sling — no `agent-gen` run, no rebuild, no quickstart-only dependency. +- **Sections edited:** Key Components (K5 row); Executive Summary (now "four moves", valid on every init path); Data Model (new "Default `agents.json` (K5)" subsection with the seeded JSON); Cross-Dimension Trade-offs (D2×D6 resolution); Cross-Perspective Conflicts (new C1 row + superseded the `agent-gen-all.sh` row); Decisions Made (C-6 mechanism row → seed agents.json, reversibility Easy); Risk Registry (new bare-init row); Implementation Plan (K5 moved into Phase 1, same `runInstallInit` write); Dependency Graph (K4→K5 same write, cross-entry-point sequencing eliminated); AC-2 traceability row. +- **Effect:** the default is valid-by-construction on EVERY init path, including the documented "hard way" (`USING_AGENTFACTORY.md:40-52`). K7's cross-file invariant now holds on bare init, not just quickstart. + +### C2 (CRITICAL) — documented `af up manager` does not auto-start the dispatcher +- **Change:** Added new component **K9 — hoist the `if startupCfg.StartDispatch { startDispatch(...) }` block out of the `blanket`-only gate** (`up.go:306,330`) so positional `af up ` (the documented `af up manager`) also auto-starts the dispatcher. Idempotent no-op if already running (`dispatch.go:1322-1325`). +- **Sections edited:** Key Components (new K9 row); Executive Summary ("(4) hoisting dispatcher auto-start"); Cross-Perspective Conflicts (new C2 row); Decisions Made (new "Dispatcher auto-start on positional `af up`" row); Risk Registry (new positional-`af up` row); Implementation Plan (Phase 2 deliverable 1 = K9, with an `af up manager` acceptance criterion); AC-2 traceability row ("K9 hoists dispatcher auto-start"). +- **Effect:** "tag an issue without visiting the manager" holds on the documented `af up manager` path, not only blanket `af up`. + +### H1 (HIGH) — commit to the minimal K5 provisioning mechanism +- **Change:** K5 is now committed to a single mechanism — **default `agents.json` seeding** (the C1 fix) — explicitly dropping `agent-gen-all.sh` (heavyweight: `af down --all` + full rebuild) and the prior recursion-prone `af install --agents`. Recorded in Decisions Made (options list shows all four considered; chosen = seed) and the Risk Registry (the `af install --agents` recursion risk row is removed/superseded as it no longer applies). +- **Sections edited:** Decisions Made; Cross-Perspective Conflicts (H1/H2 row); Implementation Plan (Phase 2 no longer mentions `agent-gen-all.sh`). + +### H2 (HIGH) — make K8 observability mandatory wherever K6 is enabled +- **Change:** **K8** elevated from "optional/additive" to **MANDATORY whenever K6 ships** — added as an explicit component row, noted in the K6 row ("K8 is MANDATORY wherever K6 is enabled"), in Decisions Made (dispatcher-tolerance row), and in Implementation Plan Phase 2 deliverable 3 ("MANDATORY with K6"). Rationale captured: K6 without K8 turns a clean friendly-skip into a silently-warning loop that looks active but dispatches nothing. +- **Sections edited:** Key Components (K6 + new K8 rows); Decisions Made; Implementation Plan (Phase 2). + +### LOW (L1–L4) — agreements, no change +- L1 (omit `notify_on_complete`): already adopted (Gap-7). L2 (AC-5 formula-layer scoping): already the design's position; analyst confirms it matches Concern 9. L3 (K3 `owner/name` validator): already present; analyst notes dispatcher's `strings.Cut` (dispatch.go:537-539) as a useful 2nd line — already nodded to in the design. L4 (installed-base net-new scope, ADR-017): already named in Six-Sigma Caveats. No edits required. + +## Post-edit integrity checks +- AC Traceability rows = 6 (= source ACs); Constraints Respected = 6 (≥ source Cs). +- No INACCURATE claim from the fidelity report reintroduced (`internal/cmd/dispatch_workflow_test.go` absent; the correct `internal/config/` path retained). +- design-doc.md grew 319 → 365 lines; all new components (K5 `DefaultAgentsConfigJSON`, K8 mandatory, K9) present. + +## Convergence note +The analyst's review independently converged with the design on every primary point +(K1 single-source builder, K2 discovery, K7 drift test, two-label UX trap, AC-5 +formula-layer scoping, Frame-correct + repo-self-derivation lift). The two CRITICAL +items were genuine entry-point/sequencing gaps the design missed; both are small, +localized, and reinforce — not redirect — the valid-by-construction thesis. diff --git a/.designs/73/design-doc.md b/.designs/73/design-doc.md index 69a0539..2247415 100644 --- a/.designs/73/design-doc.md +++ b/.designs/73/design-doc.md @@ -22,11 +22,18 @@ only `manager`+`supervisor`, verified), and `ValidateDispatchConfig` (`internal/config/dispatch.go:93+`) **hard-fails the entire dispatch cycle** on the first unknown mapped agent — invoked unconditionally at `internal/cmd/dispatch.go:146` (verified firsthand). So the default alone would break dispatch on every fresh -factory. The design therefore couples three moves into one systemic change: (1) the -baked-in default builder, (2) non-interactive repo discovery feeding `repos`, and -(3) provisioning the referenced specialists during bootstrap so the default is -**valid-by-construction**, backed by a defense-in-depth dispatcher tolerance and a -golden test that mechanically gates drift. +factory. The design therefore couples four moves into one systemic change: (1) the +baked-in default builder, (2) non-interactive repo discovery feeding `repos`, +(3) **seeding the four specialists into the default `agents.json` within +`runInstallInit` itself** (a `DefaultAgentsConfigJSON()` companion) so the default is +**valid-by-construction on EVERY init path** — including the documented bare +`af install --init` "hard way", not just quickstart — and (4) hoisting dispatcher +auto-start so the documented `af up manager` path also starts the polling loop. +Backed by a defense-in-depth dispatcher tolerance (with mandatory observability) and +a golden cross-file test that mechanically gates drift. The two cross-review CRITICAL +findings (bare-init inconsistency; positional-`af up` auto-start gating) are +incorporated below — both small, localized, and reinforcing the valid-by-construction +thesis rather than redirecting it. The Architecture Elevation verdict is **Frame correct** (the dispatch fields must exist; deleting `dispatch.json` only relocates them) with **one Frame-lift OFFERED** @@ -47,7 +54,7 @@ All proposals respect the constraints captured verbatim in `source.md`: | AC ref | Verbatim quote from source.md | Clause breakdown | Addressed by | Verified by | |--------|-------------------------------|------------------|--------------|-------------| | AC-1 | "We need to update dispatch.json to include a baked-in default for dispatch with agentfactory" | (i) update dispatch.json (ii) baked-in default (iii) for dispatch | K1 `DefaultDispatchConfigJSON` + K4 wiring at install.go:145 | `TestDefaultDispatchConfigJSON_*` golden test that the shipped default parses + equals expected mappings/workflow (model: `internal/config/dispatch_workflow_test.go`) | -| AC-2 | "they could opt to just start tagging their github issues with tags instead to kick off work without ever needing to visit the manager." | (i) tag issues with labels (ii) kick off work (iii) without visiting the manager | K1 mappings + `trigger_label`; EX dispatcher auto-start (`start_dispatch:true`, install.go:147) + K5 provisioned specialists | Integration test: fresh-init temp factory + provisioned specialists → `ValidateDispatchConfig` passes; `af dispatch --dry-run` matches a `agentic`+`rapid-plan` issue to `rapid-soldesign-plan` | +| AC-2 | "they could opt to just start tagging their github issues with tags instead to kick off work without ever needing to visit the manager." | (i) tag issues with labels (ii) kick off work (iii) without visiting the manager | K1 mappings + `trigger_label`; **K5 seeds the 4 specialists into the default agents.json** (valid on every init path); **K9 hoists dispatcher auto-start** so the documented `af up manager` (positional) starts the polling loop, not only blanket `af up` | Integration test: bare `af install --init` temp factory → `ValidateDispatchConfig(default,default-agents)` passes; `af dispatch --dry-run` matches an `agentic`+`rapid-plan` issue to `rapid-soldesign-plan`; `af up manager` launches the dispatch session | | AC-3 | "The dispatch.json should be boostrapped when the dispatch.json is first created during initial setup of the repository so we know the repository-name and can include that appropriately in the dispatch.json" | (i) first created (ii) during initial setup (iii) know repo-name (iv) include appropriately | K2 discovery + K3 validation + K4 write-if-absent (install.go:152) | Unit test: temp repo with known `git remote origin` → `af install --init` writes `repos:["/"]`; re-run does not clobber | | AC-4 | "when we get to the step where we ask the manager,`run af sling --agent \"task description\"`, we should have the work executed autonomously using the step-by-step formula that represents the IDENTITY of that agent and respects the formulas rigid step-by-step process up to the point where human interaction is necessary for next steps" | (i) `af sling --agent` (ii) autonomous via formula (iii) formula = identity (iv) rigid steps (v) up to human gate | EX `af sling --agent` specialist dispatch (sling.go, unchanged) + K5 (routed agents are formula-bearing) | Existing sling/formula behavior (unchanged); K5 ensures the 4 agents resolve as specialists with formulas | | AC-5 | "all the code branches created should have been pushed as PR's against the main branch without doctor fixes or human interaction (unless absolutely necessary, or in the case of doctor - a `doctor --fix` is acceptable only as a bandaid/fix for legacy broken behaviors, not as an ongoing operational dependency)." | (i) branches → PRs against main (ii) no doctor as ongoing dep (iii) no human (unless necessary) | (i) property of the routed FORMULAS (K5 routes; scoped to formula layer — see Six-Sigma Caveats) (ii) K1+K5 valid-by-construction → no doctor (iii) EX zero-touch happy path | Clause (i) owned by the formula layer (out of this change's scope, traceably noted); (ii)/(iii) by the valid-by-construction default + no-doctor-on-happy-path | @@ -112,26 +119,32 @@ skip-and-warn tolerance and a golden cross-file test as backstops. | K2 | Repo-discovery helper: `gh repo view --json nameWithOwner` (primary) with `git remote get-url origin` normalization (no-auth fallback); warn-don't-abort | `internal/cmd/install.go` (`runInstallInit`) | NEW | | K3 | Strict `owner/name` validator (allowlist regex) applied at the write boundary — guards `gh --repo` flag-injection and terminal-escape in the install banner | `internal/cmd` or `internal/config` | NEW | | K4 | Wire `runInstallInit` starter-config map (install.go:139-148) to call K2→K3→K1; reuse write-if-absent (install.go:150-157) | `internal/cmd/install.go:145` | MODIFIED | -| K5 | Provision the referenced specialists during bootstrap so `agents.json` contains them before the dispatcher runs — invoke `agent-gen-all.sh` directly (or targeted `af formula agent-gen` for the 4), NOT `af install --agents` | `quickstart.sh` `configure_factory` (~:414-471) | MODIFIED | -| K6 | Dispatch-loop unknown-agent tolerance: skip-and-warn instead of hard-fail, scoped to the dispatch-loop caller ONLY (`af config dispatch set` stays strict) | `internal/cmd/dispatch.go` (caller of `ValidateDispatchConfig`, :146) | MODIFIED (defense-in-depth) | -| K7 | Golden + cross-file tests: the shipped default parses (`validateDispatchConfig`) AND cross-validates (`ValidateDispatchConfig`) against the default+provisioned `agents.json` | `internal/config/*_test.go`, `internal/cmd/*_test.go` | NEW | +| K5 | **`DefaultAgentsConfigJSON()`** seeds the 4 specialist entries (`{"type":"autonomous","formula":""}`) into the default `agents.json` WITHIN `runInstallInit` (same write as K4). The 4 role templates are already embedded (`internal/templates/roles/{rapid-soldesign-plan,rapid-implement,ultra-review,rapid-increment}.md.tmpl`, verified), so a seeded agents.json entry is sufficient for `af prime`/sling to resolve each specialist — **no `agent-gen` run, no `quickstart` step, no rebuild**. Makes the default valid-by-construction on EVERY init path (bare `af install --init` AND quickstart). Mirrors `DefaultFactoryConfigJSON` single-source idiom | `internal/config` (new fn) + `internal/cmd/install.go:143` | NEW + MODIFIED | +| K6 | Dispatch-loop unknown-agent tolerance: skip-and-warn instead of hard-fail, scoped to the dispatch-loop caller ONLY (`af config dispatch set` stays strict). Defense-in-depth for partial/edited factories; K8 is MANDATORY wherever K6 is enabled (else it hides the failure) | `internal/cmd/dispatch.go` (caller of `ValidateDispatchConfig`, :146) | MODIFIED (defense-in-depth) | +| K7 | Golden + cross-file tests: the shipped default parses (`validateDispatchConfig`) AND cross-validates (`ValidateDispatchConfig`) against the default-seeded `agents.json` (K5) — asserting validity on the **bare-init** path, not just quickstart | `internal/config/*_test.go`, `internal/cmd/*_test.go` | NEW | +| K8 | Pre-flight `ValidateDispatchConfig` surfaced at `af up` / `af dispatch status` — distinguishes "empty by design" vs "discovery failed" vs "references unprovisioned agents"; warn, never abort `af up`. **MANDATORY wherever K6 is enabled** (K6 without K8 turns a clean friendly-skip into a silently-warning loop) | `internal/cmd/up.go` / `af dispatch status` | NEW (mandatory) | +| K9 | Hoist the `if startupCfg.StartDispatch { startDispatch(...) }` block out of the `blanket`-only gate so positional `af up ` (the documented `af up manager`) ALSO auto-starts the dispatcher. `startDispatch` is idempotent — already-running is a benign no-op (dispatch.go:1322-1325, verified) | `internal/cmd/up.go:306,330` | MODIFIED | ### Component Dependency Graph (from `dependencies.md`; DAG verified acyclic, topo order K3 → K1 → K2 → K4 → K5 → K6 → K7) ``` -K4 (install wiring) → K1 (default builder) → EX1 validateDispatchConfig (output must pass struct validation) +K4 (install wiring) → K1 (dispatch default builder) → EX1 validateDispatchConfig (output must pass struct validation) K4 → K2 (repo discovery) → K3 (repo validator) +K4 → K5 (DefaultAgentsConfigJSON — seed 4 specialists into default agents.json, SAME write) K4 → EX4 write-if-absent guard (reused, unchanged) -K5 (provisioning) → EX3 agent-gen-all.sh / af formula agent-gen +K9 (hoist auto-start) → up.go blanket-gate refactor (independent of install) K6 (dispatch tolerance) → EX2 ValidateDispatchConfig (relaxes ONLY the dispatch-loop caller) -K7 (tests) → K1, K4, K5, K6, EX1, EX2 +K8 (observability) pairs with K6 (mandatory) → EX2 (read-only pre-flight at af up / dispatch status) +K7 (tests) → K1, K4, K5, K6, K9, EX1, EX2 -Runtime sequencing: K2→K3→K1→K4 at install; K5 must complete before the dispatcher -runs EX2; EX2 consumes K4's written default + K5's provisioned agents.json. +Runtime sequencing: K2→K3→K1 and K5 all feed the SINGLE runInstallInit write (K4) → +EX2 (cross-file) then consumes K4's dispatch default AND K5's seeded agents.json — both +present after one `af install --init`, so there is NO cross-entry-point sequencing (the +cross-review C1 fix; the prior quickstart→dispatch ordering risk is eliminated). ``` -No cycles. **Constraint:** any new validation in `internal/config` MUST NOT import +No cycles. **Constraint:** any new code in `internal/config` (K1, K5) MUST NOT import `internal/formula` (it imports `internal/config` → cycle; dispatch.go:130-136). ### Interface (from api.md) @@ -177,13 +190,41 @@ single-label mapping (`phaseResolvesAlone`, dispatch.go:256), both phases share source `"issue"`, and `feature-workflow` collides with neither `trigger_label` nor a mapping label. +**Default `agents.json` (K5 — the cross-review C1 fix).** The same `runInstallInit` +write seeds the four referenced specialists into the default `agents.json` so the +dispatch default is valid-by-construction on EVERY init path. The current inline +literal (install.go:143) ships only `manager`+`supervisor`; it is replaced by a +`DefaultAgentsConfigJSON()` (mirroring `DefaultFactoryConfigJSON`) that adds: + +```json +{ + "agents": { + "manager": { "type": "interactive", "description": "...", "directive": "..." }, + "supervisor": { "type": "autonomous", "description": "...", "directive": "..." }, + "rapid-soldesign-plan": { "type": "autonomous", "formula": "rapid-soldesign-plan" }, + "rapid-implement": { "type": "autonomous", "formula": "rapid-implement" }, + "ultra-review": { "type": "autonomous", "formula": "ultra-review" }, + "rapid-increment": { "type": "autonomous", "formula": "rapid-increment" } + } +} +``` + +The four role templates are already embedded +(`internal/templates/roles/{rapid-soldesign-plan,rapid-implement,ultra-review,rapid-increment}.md.tmpl`, +verified), so a seeded registry entry (with `formula`) is sufficient for `af prime` +and `af sling --agent` to resolve each specialist — no `agent-gen` run, no rebuild, +no `quickstart`-only dependency. This is a registry/default-content change only: no +schema change, and write-if-absent (install.go:152) still preserves a customer-edited +agents.json (ADR-017). `ValidateDispatchConfig` (dispatch.go:101) then passes because +every `mappings[].agent` resolves in the seeded registry. + ## Cross-Dimension Trade-offs (from `conflicts.md`; every conflict has a resolution — none unresolved) | Conflict | Resolution | Rationale | |----------|-----------|-----------| -| D2×D6 (X) — default references 4 unprovisioned specialists → cross-file validation hard-fails (THE crux) | K5 provision specialists in bootstrap (primary) + K6 dispatcher skip-and-warn (defense-in-depth); Data ships the faithful default unchanged | Provisioning makes the default valid-by-construction (C-5/C-6); tolerance backstops a partial provision | +| D2×D6 (X) — default references 4 unprovisioned specialists → cross-file validation hard-fails (THE crux) | K5 seed the 4 specialists into the default `agents.json` within `runInstallInit` (valid-by-construction on every init path) + K6 dispatcher skip-and-warn (defense-in-depth); Data ships the faithful dispatch default unchanged | Seeding (one `--init` write, templates embedded) makes the default valid on bare init and quickstart alike (C-5/C-6); tolerance backstops a partial/edited factory | | D5×D6 (X) — how to resolve C-6 vs write-path strictness | Provision + scope the K6 tolerance to the dispatch-LOOP caller ONLY; `af config dispatch set` keeps strict `ValidateDispatchConfig` | The default is valid-by-construction; the dispatch loop degrades gracefully; human edits still catch typos | | D1×D5 / D3×D5 (T) — untrusted repo string from a crafted remote | K3 validate `owner/name` at the WRITE boundary before storing/echoing | A bad value never reaches disk, `gh --repo`, or the banner; dispatcher's `strings.Cut` becomes a 2nd line of defense | | D1×D2 (T) — function output must satisfy `validateDispatchConfig` | Build K1 from the `DispatchConfig` struct (compile-time field safety) + K7 golden test pins content | Struct guarantees well-formedness; golden test guarantees the specific mappings | @@ -197,7 +238,10 @@ mapping label. | Finding Source | Finding | Conflicts With | Nature | Resolution | |----------------|---------|----------------|--------|------------| | Elevation | Repo self-derivation is a Frame-lift OFFERED (not required) | Dimensions/Integration treat repo discovery (K2) as a core recommended component | tension (status vs adoption) | Adopt K2 as a design component AND document its "offered" elevation status; keep `Repos` editable (multi-repo edge) — both views reconciled | -| Six-Sigma Gap-1 + Integration I3.1 | Provision specialists via `af install --agents` in quickstart | Integration's own quickstart call site | **DIRECT (synthesis-discovered)** — `af install --agents` re-invokes `quickstart.sh` (USING_AGENTFACTORY.md:634), so calling it FROM quickstart RECURSES | **Correction:** K5 invokes `agent-gen-all.sh` directly (or targeted `af formula agent-gen` for the 4), NOT `af install --agents`. Same provisioning effect, no recursion | +| Six-Sigma Gap-1 + Integration I3.1 | Provision specialists via `af install --agents` in quickstart | Integration's own quickstart call site | **DIRECT (synthesis-discovered)** — `af install --agents` re-invokes `quickstart.sh` (USING_AGENTFACTORY.md:634), so calling it FROM quickstart RECURSES | **Superseded by C1 (below):** seed the 4 specialists into the default `agents.json` within `runInstallInit` — no quickstart step, no `agent-gen-all.sh`, no recursion, and valid on bare init too | +| Analyst cross-review **C1** (CRITICAL) | "Valid-by-construction" held only on the quickstart path; bare `af install --init` (USING_AGENTFACTORY.md:40-52) writes the populated default but registers only manager+supervisor → `ValidateDispatchConfig` hard-fails | original K5 (quickstart-only provisioning) | **DIRECT** — defeats the design's own valid-by-construction thesis on the documented "hard way" | **Incorporated:** K5 now seeds the 4 specialists into the default `agents.json` inside `runInstallInit` (templates already embedded — verified); K7 asserts validity on the bare-init path. Adopted verbatim | +| Analyst cross-review **C2** (CRITICAL) | Dispatcher auto-start fires only on blanket `af up` (inside `if blanket{` up.go:306,330); the documented `af up manager` is gated out → polling loop never starts | AC-2 "without visiting the manager" + the D3×D6 "EX auto-start" claim | **DIRECT** — breaks the zero-touch promise on the documented setup path | **Incorporated:** new K9 hoists the `StartDispatch` block out of the blanket gate so positional `af up ` auto-starts too (idempotent, dispatch.go:1322-1325 — verified). Adopted verbatim | +| Analyst cross-review **H1/H2** (HIGH) | Commit to the minimal K5 mechanism; make K8 mandatory wherever K6 is enabled | K5 ambiguity; K8 "optional" | tension | **Incorporated:** K5 committed to agents.json seeding (no `agent-gen-all.sh`); K8 is now MANDATORY whenever K6 ships | | Six-Sigma Gap-5 | AC-5 (PRs without doctor/human) is a property of the 4 FORMULAS, not dispatch.json | AC-5's apparent ownership by this change | scoping | Scope AC-5 clause (i) to the formula layer (necessary-but-not-sufficient); this change routes to those formulas and removes the doctor dependency, but does not (and cannot) verify formula-internal PR-push | | Six-Sigma Gap-2 | `trigger_label` is a hard query pre-filter (dispatch.go:301) — tagging ONLY a mapping label dispatches nothing | AC-2's natural reading ("just tag rapid-plan") | escape path | Document the two-label requirement (`agentic` + mapping label) in the default and setup docs; widening the query is out of scope (expands blast radius) | | Decision History (ADR-017) | Write-if-absent never upgrades existing empty-default installs | C-2 "first created" for the installed base | scope boundary | Scope to NET-NEW installs (ADR-017-honest); existing factories opt in via `af config dispatch set` — named, not silently accepted | @@ -215,16 +259,18 @@ decision-history-vs-dimensions (ADR-014/017 shape K2/K4/K5, no contradiction). | Default content | 4 mappings + `feature-workflow` (D2.2-A) / mappings-only (D2.2-C) / manager-supervisor-only (D2.2-B, rejected: fails AC-2) | 4 mappings + `feature-workflow`, intent-corrected from the struct | Maximally faithful to source intent; D2.2-C is the conservative fallback if the workflow surface is deemed risky | Easy | | Repo name | hand-authored placeholder (status quo) / `--init --repo` flag (I2.2) / self-derive in Go (I2.1) | Self-derive via `gh`/`git remote` in `runInstallInit`, validated | Adopts the elevation Frame-lift; ADR-014 prefers the Go path self-sufficient over a script-fed flag | Easy | | `notify_on_complete` | explicit `"manager"` (source) / omit | Omit (defaults to manager) | Strictly smaller validation surface; identical runtime behavior (Gap-7) | Easy | -| C-6 provisioning mechanism | `af install --agents` in quickstart (I3.1 as written) / `agent-gen-all.sh` direct / targeted `af formula agent-gen` | `agent-gen-all.sh` direct (or targeted agent-gen) | `af install --agents` re-invokes quickstart → RECURSION (USING:634); direct invocation provisions specialists without recursion | Moderate | -| Dispatcher tolerance | none (hard-fail) / skip-and-warn everywhere / skip-and-warn dispatch-loop only | Skip-and-warn scoped to the dispatch loop; write path strict | Graceful degradation on partial provision without weakening the human write-path typo check | Moderate | +| C-6 provisioning mechanism | `af install --agents` in quickstart (recurses) / `agent-gen-all.sh` direct (heavy: `af down --all`+rebuild) / targeted `af formula agent-gen` / **seed default agents.json in `runInstallInit`** | **Seed the 4 specialists into the default `agents.json` (`DefaultAgentsConfigJSON`) in `runInstallInit`** | Cross-review C1/H1: templates already embedded (verified), so a registry seed needs no `agent-gen`/rebuild; valid-by-construction on EVERY init path (incl. bare `af install --init`), not just quickstart; no recursion, no `af down --all` mid-bootstrap | Easy | +| Dispatcher tolerance | none (hard-fail) / skip-and-warn everywhere / skip-and-warn dispatch-loop only | Skip-and-warn scoped to the dispatch loop; write path strict; **K8 observability mandatory alongside** | Graceful degradation on partial/edited factory without weakening the human write-path typo check; K8 prevents K6 from hiding a "configured-but-broken" loop (cross-review H2) | Moderate | +| Dispatcher auto-start on positional `af up` | leave blanket-only (status quo) / hoist `StartDispatch` out of the blanket gate (K9) / change docs to bare `af up` | Hoist `StartDispatch` to run on any `af up` (K9) | Cross-review C2: the documented `af up manager` is positional and gated out of auto-start (up.go:306,330); hoisting is the robust interlock and is idempotent (dispatch.go:1322-1325, verified) — docs-only would leave the trap | Moderate | | Installed base | blind overwrite (rejected, ADR-017) / scope to net-new | Net-new installs; existing factories opt in | ADR-017 forbids clobbering customer-edited config | n/a (policy) | ## Risk Registry | Risk | Severity | Likelihood | Mitigation | Owner | Source | |------|----------|-----------|------------|-------|--------| -| Default references 4 specialists absent from fresh agents.json → `ValidateDispatchConfig` hard-fails dispatch cycle | HIGH | Certain (without K5) | K5 provision specialists in bootstrap + K6 dispatch-loop skip-and-warn | Integration | dimensions / elevation / gap | -| `af install --agents` from quickstart recurses | HIGH | Certain (if K5 used `--agents`) | K5 invokes `agent-gen-all.sh` directly / targeted `agent-gen`, never `af install --agents` | Integration | synthesis cross-perspective | +| Default references 4 specialists absent from fresh agents.json → `ValidateDispatchConfig` hard-fails dispatch cycle | HIGH | Certain (without K5) | K5 seeds the 4 specialists into the default agents.json in `runInstallInit` (valid on every init path) + K6 dispatch-loop skip-and-warn | Integration | dimensions / elevation / gap | +| Bare `af install --init` (documented "hard way") writes the populated default but registers only manager+supervisor → default invalid-by-construction off the quickstart path | HIGH | Certain (without C1 fix) | K5 `DefaultAgentsConfigJSON` seeds specialists in the SAME `runInstallInit` write, so validity holds on bare init; K7 asserts it | Integration | cross-review C1 | +| Documented `af up manager` (positional) is gated out of dispatcher auto-start (up.go:306,330) → polling loop never starts; "without visiting the manager" breaks | HIGH | Certain (without K9) | K9 hoists `StartDispatch` out of the blanket gate; idempotent no-op if already running (dispatch.go:1322-1325) | Integration/UX | cross-review C2 | | Repo discovery fails (no/non-GitHub/non-`origin` remote) → empty `repos`, dispatcher silently skips (looks like benign "not configured", dispatch.go:1330) | HIGH | Possible | K2 warn-don't-abort writes loadable default; surface a distinct fail-loud message (ADR-014); `af up` pre-flight warns (K8 observability, below) | Security/UX | gap-3 / gap-8 | | Crafted remote URL injects bad `repos` (gh flag-injection / terminal escape in banner) | MED | Low | K3 strict `owner/name` allowlist at the write boundary | Security | security | | K1 output fails `validateDispatchConfig` (drift) | MED | Low | Build from struct + K7 golden test | API/Data | conflicts | @@ -264,32 +310,32 @@ explicit repo; (b) AC-5 is a downstream-formula guarantee verified separately; 2. K2 repo-discovery helper in `runInstallInit` (`gh repo view --json nameWithOwner` → fallback `git remote get-url origin`), warn-don't-abort. 3. K3 strict `owner/name` validator applied before write/echo. 4. K4 replace install.go:145 literal with `config.DefaultDispatchConfigJSON(discoveredRepo)`; reuse write-if-absent. +5. K5 `DefaultAgentsConfigJSON()` in `internal/config` seeds the 4 specialists into the default `agents.json` (replace install.go:143 literal); SAME `runInstallInit` write, so the default is valid-by-construction on bare `af install --init` (cross-review C1). No `agent-gen`/rebuild (templates embedded). -**Source ACs satisfied**: AC-1, AC-3 (and the substrate of AC-2). -**Dependencies**: none new (uses existing struct, write path). -**Risks addressed**: drift (single-source), injection (K3), discovery failure (warn-don't-abort). -**Six-Sigma gaps closed**: Gap-7 (omit notify), partial Gap-3 (discovery exists). +**Source ACs satisfied**: AC-1, AC-3, AC-2 substrate (mappings + their agents now both present at init). +**Dependencies**: none new (uses existing struct, write path, embedded templates). +**Risks addressed**: drift (single-source), injection (K3), discovery failure (warn-don't-abort), bare-init invalidity (K5 seed — cross-review C1). +**Six-Sigma gaps closed**: Gap-1 (via seeding), Gap-7 (omit notify), partial Gap-3 (discovery exists). **Phase acceptance criteria:** - [ ] `DefaultDispatchConfigJSON("org/repo")` output passes `validateDispatchConfig` (unit test). -- [ ] In a temp repo with a known `git remote origin`, `af install --init` writes `dispatch.json` with `repos:["org/repo"]` and the 4 mappings + `feature-workflow`; a re-run does not clobber it. +- [ ] After bare `af install --init` in a temp repo with a known `git remote origin`, `dispatch.json` has `repos:["org/repo"]` + the 4 mappings + `feature-workflow`, `agents.json` contains the 4 specialists, and `ValidateDispatchConfig(default, default-agents)` returns nil; a re-run clobbers neither file. - [ ] A crafted/garbage remote URL is rejected by K3 and yields `repos:[]` + a loud warning, not a malformed write. -### Phase 2: C-6 resolution — provision specialists + dispatcher tolerance (Effort: M) +### Phase 2: Dispatcher auto-start + graceful degradation + observability (Effort: M) **Deliverables:** -1. K5 bootstrap provisioning in `quickstart.sh` `configure_factory` via `agent-gen-all.sh` (or targeted `af formula agent-gen` for the 4 referenced agents) — explicitly NOT `af install --agents` (recursion). +1. K9 hoist the `if startupCfg.StartDispatch { startDispatch(...) }` block out of the `blanket`-only gate (`internal/cmd/up.go:306,330`) so positional `af up ` (the documented `af up manager`) also auto-starts the dispatcher (cross-review C2). 2. K6 dispatch-loop skip-and-warn on unknown-agent mappings, scoped to the dispatcher caller of `ValidateDispatchConfig` (`internal/cmd/dispatch.go:146`); `af config dispatch set` unchanged (strict). -3. K8 observability: pre-flight validation surfaced at `af up` / `af dispatch status` (warn, never abort). +3. K8 (MANDATORY with K6) observability: pre-flight `ValidateDispatchConfig` surfaced at `af up` / `af dispatch status`, distinguishing "empty by design" vs "discovery failed" vs "references unprovisioned agents"; warn, never abort `af up`. -**Source ACs satisfied**: AC-2, AC-4, AC-6 (and the no-doctor clause of AC-5). -**Dependencies**: Phase 1's default (the mappings that name the specialists). -**Risks addressed**: hard-fail-on-fresh-factory (HIGH), recursion (HIGH), low-visibility failure (MED). -**Six-Sigma gaps closed**: Gap-1, Gap-8; Gap-2 documented. +**Source ACs satisfied**: AC-2 (auto-start on the documented path), AC-4, AC-6. +**Dependencies**: Phase 1's seeded default (so the common path needs no degradation). +**Risks addressed**: positional-`af up` auto-start gating (HIGH, cross-review C2), partial/edited-factory hard-fail (MED), low-visibility failure (MED, cross-review H2). +**Six-Sigma gaps closed**: Gap-8 (mandatory K8); Gap-2 documented. **Phase acceptance criteria:** -- [ ] After a fresh bootstrap (init + K5 provisioning), `agents.json` contains the 4 specialists and `ValidateDispatchConfig(default, agents)` returns nil. -- [ ] With one mapped agent absent, the dispatch LOOP skips that mapping with a warning and still dispatches the others; `af config dispatch set` with the same config still exits non-zero (strict). -- [ ] `af up`/`af dispatch status` reports a distinct, actionable state for "references unprovisioned agents" vs "empty by design" vs "discovery failed". +- [ ] `af up manager` (positional) launches the dispatch tmux session when `start_dispatch:true` and the default is valid; a second `af up manager` is a benign no-op ("Dispatcher already running"). +- [ ] With one mapped agent absent, the dispatch LOOP skips that mapping with a warning and still dispatches the others; `af config dispatch set` with the same config still exits non-zero (strict); `af dispatch status` reports the distinct "references unprovisioned agents" state. ### Phase 3: Drift/golden test gate + docs (Effort: S) **Deliverables:** diff --git a/.designs/73/design-refinement-progress.md b/.designs/73/design-refinement-progress.md index 068d53d..d1a2d7b 100644 --- a/.designs/73/design-refinement-progress.md +++ b/.designs/73/design-refinement-progress.md @@ -7,8 +7,8 @@ ## Agents | Role | Agent | Status | Started | Completed | |------|---------|--------|---------|-----------| -| Analyst | rootcause-all | Dispatched | 2026-06-28T16:44Z | - | -| Designer | design-v7 | Dispatched | 2026-06-28T16:44Z | - | +| Analyst | rootcause-all | Analysis Complete (holding for cross-review) | 2026-06-28T16:44Z | 2026-06-28T17:05Z | +| Designer | design-v7 | Design Complete (design-doc.md @ c21be0f3) | 2026-06-28T16:44Z | 2026-06-28T17:05Z | | Impl | design-plan-impl | Not dispatched | - | - | **Beads:** analyst=af-8fe67d60, designer=af-cf3cee4b, gate=af-bec549ca (gate blocks analyst bead close → premature-af-done detection) @@ -16,8 +16,8 @@ ## Cross-Review Progress | Round | Exchange | Status | Timestamp | |-------|----------|--------|-----------| -| 1 | Analyst reviews design-doc | Pending | - | -| 1 | Designer incorporates HIGH/CRIT | Pending | - | +| 1 | Analyst reviews design-doc | Complete (analyst-review-design.md, 58 lines) | 2026-06-28T17:30Z | +| 1 | Designer incorporates HIGH/CRIT | Complete (design-doc.md 319→365 lines; designer-update-log.md) | 2026-06-28T17:45Z | ## Implementation Plan | Stage | Status | Timestamp | From 5ff690e4e8e6db5081d415d98341cf2eebffdadf Mon Sep 17 00:00:00 2001 From: agentfactory Date: Sun, 28 Jun 2026 17:45:31 +0000 Subject: [PATCH 4/9] rapid-soldesign-plan: add PR body for #73 design Co-authored-by: agentfactory-cli <293373236+agentfactory-cli@users.noreply.github.com> --- .designs/73/cross-review/pr-body.md | 82 +++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .designs/73/cross-review/pr-body.md diff --git a/.designs/73/cross-review/pr-body.md b/.designs/73/cross-review/pr-body.md new file mode 100644 index 0000000..4c13217 --- /dev/null +++ b/.designs/73/cross-review/pr-body.md @@ -0,0 +1,82 @@ +## Issue + +Design for #73 — New dispatch workflow default to be included with agentfactory +https://github.com/stempeck/agentfactory/issues/73 + +A freshly bootstrapped factory cannot drive autonomous work from GitHub labels: +`af install --init` writes an **empty** `dispatch.json` (`repos:[]`, `mappings:[]`, +no `workflows` — `internal/cmd/install.go:145`), so a new user must hand-edit the +file before label-triggered dispatch works. The issue asks for a useful baked-in +default, populated with the real `owner/repo` discovered at install time. + +> This is a **design + cross-review** PR (no production code yet). It produces the +> validated design and a 3-phase implementation plan; implementation is a follow-up. + +## Root cause + +- `runInstallInit` ships an empty `dispatch.json` and performs **no repo-name + discovery**, even though quickstart has already `cd`'d into the repo (the fact is + discarded). — `install.go:145`, verified. +- The naive fix (ship the proposed default as-is) is **necessary but not + sufficient**: the default's four mappings reference specialist agents + (`rapid-soldesign-plan`, `rapid-implement`, `ultra-review`, `rapid-increment`) + that a fresh `agents.json` does **not** contain (install ships only + `manager`+`supervisor`), and `ValidateDispatchConfig` **hard-fails the entire + dispatch cycle** on the first unknown mapped agent (`internal/config/dispatch.go`, + invoked unconditionally at `internal/cmd/dispatch.go:146`). So the default alone + would break dispatch on every fresh factory. + +## Proposed solution + +A single **systemic** change (valid-by-construction), not just a literal default: + +- **K1** `DefaultDispatchConfigJSON(repo)` — single-source builder beside + `DefaultFactoryConfigJSON()` (`config.go:111`), emitting the 4 label→agent + mappings + `feature-workflow` + `trigger_label:"agentic"`. +- **K2** Non-interactive repo discovery (`gh repo view --json nameWithOwner`, with + `git remote get-url origin` fallback) feeding `repos`; warn-don't-abort. +- **K3** Strict `owner/name` validator at the write boundary (guards flag/escape + injection from a crafted remote). +- **K5** `DefaultAgentsConfigJSON()` — **seeds the 4 specialists into the default + `agents.json` within `runInstallInit` itself** (the role templates are already + embedded), making the default valid on **every** init path, including the bare + `af install --init` "hard way" — not just quickstart. +- **K9** Hoist dispatcher auto-start out of the `blanket`-only gate so the + documented `af up manager` (positional) also starts the polling loop (idempotent). +- **K6/K7/K8** Defense-in-depth: dispatch-loop skip-and-warn tolerance (K6) with + **mandatory** observability (K8), plus a golden cross-file drift test (K7). + +Architecture-elevation verdict: **Frame correct** (the dispatch fields must exist; +deleting `dispatch.json` only relocates them) with **one offered lift adopted** — +repo self-derivation (K2). + +## Cross-review (round 1) + +Analyst (`rootcause-all`) reviewed the design grounded in its root-cause analysis +(9 concerns; 8 validated). It **converged with the design on every primary point** +and raised, the designer (`design-v7`) incorporated, all findings: + +- **C1 (CRITICAL):** "valid-by-construction" held only on the quickstart path; bare + `af install --init` left the populated default referencing unregistered agents. + → Fixed by **K5** seeding specialists in `runInstallInit` itself. +- **C2 (CRITICAL):** the documented `af up manager` (positional) does **not** + auto-start the dispatcher (gated to blanket `af up`). → Fixed by **K9** hoisting + the auto-start. +- **H1 (HIGH):** commit to the minimal provisioning mechanism (drop heavyweight + `agent-gen-all.sh`). → K5 now commits to direct `agents.json` seeding. +- **H2 (HIGH):** make observability **K8 mandatory** wherever the K6 tolerance is + enabled (else a "running-but-dispatching-nothing" loop hides the failure). +- **L1–L4 (LOW):** agreements/nits; no change. + +Both CRITICAL items were genuine entry-point/sequencing gaps; both are small, +localized, and **strengthen** the valid-by-construction thesis rather than redirect +it. With C1+C2 incorporated, the design satisfies AC-2 and AC-3 on every documented +setup path. (design-doc.md grew 319 → 365 lines.) + +## Artifacts + +- `.designs/73/design-doc.md` — full design (dimension analyses, AC traceability + 6/6, elevation verdict, risk registry, 3-phase implementation plan) +- `.designs/73/cross-review/analyst-review-design.md` — analyst round-1 review +- `.designs/73/cross-review/designer-update-log.md` — incorporation log +- `.designs/73/problem-summary-73.md`, `.designs/73/design-refinement-progress.md` From c669226c141a1dc24801d24286dedc63a77ac770 Mon Sep 17 00:00:00 2001 From: agentfactory Date: Sun, 28 Jun 2026 17:49:37 +0000 Subject: [PATCH 5/9] rapid-soldesign-plan: record PR #74 + agent release in progress tracker (#73) Co-authored-by: agentfactory-cli <293373236+agentfactory-cli@users.noreply.github.com> --- .designs/73/design-refinement-progress.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.designs/73/design-refinement-progress.md b/.designs/73/design-refinement-progress.md index d1a2d7b..59d0bee 100644 --- a/.designs/73/design-refinement-progress.md +++ b/.designs/73/design-refinement-progress.md @@ -2,13 +2,14 @@ **Status**: In Progress **Issue**: https://github.com/stempeck/agentfactory/issues/73 +**PR**: https://github.com/stempeck/agentfactory/pull/74 **Started**: 2026-06-28T16:35:12Z ## Agents | Role | Agent | Status | Started | Completed | |------|---------|--------|---------|-----------| -| Analyst | rootcause-all | Analysis Complete (holding for cross-review) | 2026-06-28T16:44Z | 2026-06-28T17:05Z | -| Designer | design-v7 | Design Complete (design-doc.md @ c21be0f3) | 2026-06-28T16:44Z | 2026-06-28T17:05Z | +| Analyst | rootcause-all | Released (review complete, af down) | 2026-06-28T16:44Z | 2026-06-28T17:50Z | +| Designer | design-v7 | Released (design committed, af down) | 2026-06-28T16:44Z | 2026-06-28T17:50Z | | Impl | design-plan-impl | Not dispatched | - | - | **Beads:** analyst=af-8fe67d60, designer=af-cf3cee4b, gate=af-bec549ca (gate blocks analyst bead close → premature-af-done detection) @@ -22,6 +23,6 @@ ## Implementation Plan | Stage | Status | Timestamp | |-------|--------|-----------| -| PR opened | Pending | - | +| PR opened | Complete (https://github.com/stempeck/agentfactory/pull/74) | 2026-06-28T17:48Z | | implementation-plan agent dispatched | Pending | - | | implementation_plan_outline.md created | Pending | - | From 939d359681730cc439d4f85ce9a78d160d98345a Mon Sep 17 00:00:00 2001 From: agentfactory Date: Sun, 28 Jun 2026 18:08:01 +0000 Subject: [PATCH 6/9] plan: implementation outline for af-7fc78161 Co-Authored-By: Claude Opus 4.8 Co-authored-by: agentfactory-cli <293373236+agentfactory-cli@users.noreply.github.com> --- .../codebase-investigation.md | 110 +++++ .../implementation-plan/design-extraction.md | 155 +++++++ .../implementation_plan_outline.md | 377 ++++++++++++++++++ .designs/73/implementation-plan/synthesis.md | 180 +++++++++ 4 files changed, 822 insertions(+) create mode 100644 .designs/73/implementation-plan/codebase-investigation.md create mode 100644 .designs/73/implementation-plan/design-extraction.md create mode 100644 .designs/73/implementation-plan/implementation_plan_outline.md create mode 100644 .designs/73/implementation-plan/synthesis.md diff --git a/.designs/73/implementation-plan/codebase-investigation.md b/.designs/73/implementation-plan/codebase-investigation.md new file mode 100644 index 0000000..c6cb5f3 --- /dev/null +++ b/.designs/73/implementation-plan/codebase-investigation.md @@ -0,0 +1,110 @@ +# Codebase Investigation — Issue #73 (Phase 2 ground-truth) + +Firsthand current-state verification from 4 parallel Explore agents (3 phases + +deployment audit). The design describes TARGET state; this records CURRENT state. +**All paths relative to repo root** `/home/dev/af/agentfactory/.agentfactory/worktrees/wt-7605ca`. + +**Implementation status: K1–K9 are ALL unimplemented.** Every pattern they must +follow already exists. Below: verified anchors, design discrepancies, reuse assets, +and hidden issues. + +--- + +## 1. Verified anchors (CURRENT line numbers) + +### `internal/config/config.go` (K1/K5/K2 models) +- `DefaultFactoryConfigJSON()` — **108–124** (struct → `json.Marshal` → string; fallback literal on err). **The exact K1/K5 model.** (design said :111 ✓ within range) +- `DefaultGitIdentity()` — **88–91** (constant-sourced in-code default). Model for K2's "in-code default" idiom. (design :89 ✓) +- Constants `DefaultGitUserName`/`DefaultGitUserEmail` — 35–37. + +### `internal/config/dispatch.go` (schema + validators) +- `DispatchConfig` struct — **17–27**: `Repos[]string`(19), `TriggerLabel`(20), `NotifyOnComplete`(21), `Mappings`(22), `IntervalSecs`(23), `RetryAfterSecs`(24), `RemoveTriggerAfterDispatch`(25), `Workflows []Workflow`(26, omitempty). +- `DispatchMapping` struct — **29–35**: `label`(deprecated), `labels`, `source`(default "issue"), `agent`. +- `Workflow` struct — **42–45**: `label`, `phases`. +- `defaultNotifyAgent = "manager"` const — **15** (shared by both validators so they can't drift; comment 11–14). +- `validateDispatchConfig` (struct-level, private) — **140–185**: rejects empty `Repos`(142–143), empty `TriggerLabel`(145–146), empty `Mappings`(148–149); fills `Source`→"issue", `IntervalSecs`→300, `RetryAfterSecs`→1800 (175–179), **`NotifyOnComplete`→"manager" (181–183)**. +- `ValidateDispatchConfig` (cross-file, public) — **84–138**, signature at **93**: `func ValidateDispatchConfig(disp *DispatchConfig, agents *AgentConfig) error`. Mapping-agent existence check **100–103** (`"dispatch mapping references unknown agent %q"`); explicit-`NotifyOnComplete` check 105–108; workflow-phase agents must exist + have `formula` 115–129; **import-cycle comment 131–136** (`internal/formula` imports `internal/config`, so K1/K5/K6 in `internal/config` MUST NOT import `internal/formula`). + +### `internal/cmd/install.go` (K4/K5 wire points) +- `runInstallInit` — starts **97**; `--agents` refuses to run from a worktree (checks `.factory-root`) **103–104**. +- Starter-config map (`map[string]string`, 5 keys) — **139–148**. +- **agents.json inline literal — line 143** (manager+supervisor ONLY; design :143 ✓). K5 replaces with `DefaultAgentsConfigJSON()`. +- **dispatch.json inline literal — line 145 ONLY** (`{"repos":[],"trigger_label":"agentic","notify_on_complete":"manager","mappings":[],"interval_seconds":300,"retry_after_seconds":1800}`; design :145 ✓). K4 replaces with `config.DefaultDispatchConfigJSON(repo)`. +- Write-if-absent guard (`os.Stat`/`os.IsNotExist`) — **150–156** (reused by K4/K5, unchanged; ADR-017). +- `af install` command flags (`--init`, `[role]`, `--agents`, `--no-build`) — 38–94. + +### `internal/cmd/detect_default_branch.go` ⭐ (K2/K3 reuse template — NEW FINDING) +- `runGitDetect` package-level seam (ADR-009 injectable for tests) — **34–43**. +- `detectBranchTimeout = 5 * time.Second` — **14**; uses `context.WithTimeout` + `exec.CommandContext` (35–37). +- `gh repo view --json defaultBranchRef -q ...` — **67** (err → "" no-abort convention). +- `branchNameAllowlist = regexp.MustCompile(`^[A-Za-z0-9._/-]+$`)` — **21**. K3 needs the `owner/name` form `^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$`. +- Same exec+timeout idiom also at `prime.go:355–358` (mail check). + +### `internal/cmd/up.go` (K9) +- `blanket := len(args) == 0` — **92**; `startupCfg` read at **82**. +- `if blanket {` — **306**; quality/other gates 312–324; **`if startupCfg.StartDispatch { startDispatch(...) }` — 330–335** (inside the blanket gate; warning-on-fail, sets `allOK=false`). K9 hoists this block out of the `if blanket` so positional `af up ` also reaches it. Design :306,330 ✓. + +### `internal/cmd/dispatch.go` (K6/K8 + idempotency) +- Dispatch-loop `ValidateDispatchConfig` call — **146–148** (`if err := ... { return err }`; comment 143–145 notes it's the same validator as the write path). K6 relaxes ONLY this caller. Design :146 ✓. +- `startDispatch` — **1321–1340**: already-running benign no-op **1322–1325**; unconfigured skip (`ErrNotFound`/`ErrMissingField`) **1328–1331**; `launchDispatchSession` 1339. Idempotent ✓ (enables K9). Design :1322–1325 ✓. +- `runDispatchStatus` — **1356–1405**; `dispatchStatusJSON{ DispatcherRunning bool; Entries []dispatchStatusEntry }` — **1458–1461**. K8 adds config-validation-state fields here (additive to `--json` contract). +- Trigger-label hard pre-filter in issue/PR queries — ~**301/320** (Gap-2 substrate). + +### `internal/cmd/config_set.go` (write path — must STAY strict) +- `af config dispatch set` calls strict `ValidateDispatchConfig` — **89–90** (`{ return err }`), then `SaveDispatchConfig` 94. K6 MUST NOT touch this. Design :85–91 ✓. + +### `internal/config/startup.go` +- `StartDispatch bool` `json:"start_dispatch"` — **18**. install.go:147 ships `start_dispatch:true`. + +### `internal/templates/roles/` (K5 — all present, embedded) +- `rapid-soldesign-plan.md.tmpl`, `rapid-implement.md.tmpl`, `ultra-review.md.tmpl`, `rapid-increment.md.tmpl` — all exist. K5 needs no `agent-gen`/rebuild. + +### Tests (Phase 3 models + a hidden break) +- `internal/config/dispatch_workflow_test.go` — cross-file validation table pattern at **212–257**. **K7 model.** +- `formula_drift_test.go` — ADR-008 embedded-vs-installed drift test. K7-drift may mirror. +- ⚠️ **`internal/cmd/install_integration_test.go:66–72`** — existing test asserts `af install --init` writes **`repos:[]` + `mappings:[]`**. **K1/K5 change these defaults → this test WILL FAIL and MUST be updated** (not called out in the design). Also currently does NOT assert agents.json specialists. + +--- + +## 2. Design ↔ codebase discrepancies (must be reflected in the outline) + +| # | Design claim | Ground truth | Action | +|---|--------------|--------------|--------| +| D-a | dispatch.json empty literal at `install.go:176` (alt citation) | **`:176` is `mcpstore.New(...)`**, NOT a dispatch literal. Literal is at **:145 ONLY** | Use :145; drop :176 | +| D-b | "Today NO layer in `internal/` reads a git remote (0 grep hits)" | `git remote get-url` indeed absent, BUT **`gh repo view --json` already exists** in `detect_default_branch.go` | K2 has a real precedent to mirror; soften the "0 hits" framing | +| D-c | K5 = provision via `af install --agents` in quickstart (dependencies.md/integration.md/security.md) | **Superseded** by design-doc K5 = `DefaultAgentsConfigJSON()` seed in `runInstallInit`. `af install --agents` would RECURSE (calls quickstart) + refuses from worktree | Follow design-doc; companion docs stale | +| D-d | (implicit) new tests only | **Existing `install_integration_test.go:66–72` breaks** on the new defaults | Add "update existing test" to Phase 1 | +| D-e | `notify_on_complete` handling | Current literal explicitly sets `"manager"`; `validateDispatchConfig` fills it at runtime (181–183) anyway | K1 omits it (smaller validation surface, Gap-7); K7 must accept omitted-or-manager | + +--- + +## 3. Deployment / environment parity (audit) + +- **Bootstrap path A (bare `af install --init`)** and **path B (quickstart.sh → quickdocker.sh)** BOTH currently produce an invalid-by-construction default: empty `dispatch.json` + agents.json with only manager+supervisor. **C1 parity gap confirmed firsthand.** +- `quickstart.sh`: `cd` into target repo **428**; `af install --init` **441–445**; `af install manager`+`af install supervisor` **449–470**; **does NOT run `af install --agents`**. (717 lines) +- `quickdocker.sh`: clones target repo, delegates to quickstart. (694 lines) +- `af install --agents` → `runInstallAgents` → `agent-gen-all.sh` + `quickstart.sh`; refuses from worktree; requires initialized factory → confirms K5-seed-in-`runInstallInit` is the correct mechanism (no recursion). +- **CI (`.github/workflows/test.yml`, 157 lines):** unit `make test` (42–68), integration `make test-integration` +Python3.12/tmux (96–124), regen `make check-regen` (126–140), web-unit (70–94). **No job exercises `af install --init` dispatch validity** → K7 golden/cross-file test lands in the `make test` unit tier (gates AC-1/C-6 against drift). +- Out of scope (design doesn't touch, correctly): web/ module bootstrap, MaxWorktrees, MCP issue-store server (mcpstore.New at install.go:176). + +--- + +## 4. Reuse assets (don't reinvent) + +1. `DefaultFactoryConfigJSON()` (config.go:108–124) → K1, K5 (struct→marshal→string). +2. `detect_default_branch.go` (runGitDetect seam + 5s timeout + `gh repo view` + allowlist regex) → K2 discovery & K3 validator. +3. Write-if-absent guard (install.go:150–156) → K4, K5 (unchanged). +4. `dispatch_workflow_test.go` (212–257) → K7 cross-file test; `formula_drift_test.go` → K7 drift. +5. `startDispatch` idempotency (dispatch.go:1322–1325) → K9 (safe to hoist; no double-start). +6. `ErrNotFound`/`ErrMissingField` skip idiom (dispatch.go:1328–1331) → K8 "empty by design" vs "discovery failed" vs "unprovisioned" distinction. + +--- + +## 5. Hidden issues / latent risks + +1. **Existing test breakage** (D-d): `install_integration_test.go:66–72` must be updated in the SAME phase that changes the defaults, or Phase 1 reddens CI. +2. **Import cycle** (dispatch.go:131–136): K1/K5/K6 code in `internal/config` MUST NOT import `internal/formula`. +3. **K6 without K8** masks "configured-but-broken" as "running" (cross-review H2) → K8 mandatory with K6. +4. **K9 hoist** is safe (idempotent), but verify no second `startDispatch` caller would now double-fire — audit found only the up.go blanket-gated caller; hoisting changes only its gate. +5. **K3 regex** must reject leading `-` (gh flag-injection into `dispatch.go` `gh --repo`) and shell/terminal-escape chars before the value reaches disk/banner/`gh`. +6. **Two-label requirement** (Gap-2): trigger-label query pre-filter (dispatch.go ~301/320) means tagging only a mapping label dispatches nothing — docs-only fix in Phase 3. diff --git a/.designs/73/implementation-plan/design-extraction.md b/.designs/73/implementation-plan/design-extraction.md new file mode 100644 index 0000000..e0ece0e --- /dev/null +++ b/.designs/73/implementation-plan/design-extraction.md @@ -0,0 +1,155 @@ +# Design Extraction — Issue #73 (Phase 1 artifact) + +Structured extraction of `.designs/73/design-doc.md` and companion docs, assembled +during design-plan-impl Phase 1 (Discover Design Artifacts). This is the foundation +the implementation plan outline is built from. The **design-doc.md is authoritative**; +where a companion doc disagrees, the design-doc (latest synthesis) wins — drifts are +flagged below. + +Design dir: `.designs/73/` · Input: PR #74 (`af/rapid-soldesign-plan-7605ca`) · MODE=pr + +--- + +## 1. Phases (from design-doc.md §Implementation Plan, lines 305-352) + +| Phase | Title | Effort | Deliverables (components) | Source ACs | Dependencies | Phase acceptance criteria (verbatim refs) | +|-------|-------|--------|---------------------------|------------|--------------|--------------------------------------------| +| 1 | Single-source default builder + repo discovery | M | K1 `DefaultDispatchConfigJSON`, K2 repo discovery, K3 `owner/name` validator, K4 install wiring, K5 `DefaultAgentsConfigJSON` seed | AC-1, AC-3, AC-2 substrate | none new (existing struct, write path, embedded templates) | (a) `DefaultDispatchConfigJSON("org/repo")` passes `validateDispatchConfig`; (b) bare `af install --init` in temp repo w/ known remote → `dispatch.json` has `repos:["org/repo"]`+4 mappings+`feature-workflow`, `agents.json` has 4 specialists, `ValidateDispatchConfig` returns nil, re-run clobbers neither; (c) crafted remote URL rejected by K3 → `repos:[]` + loud warning | +| 2 | Dispatcher auto-start + graceful degradation + observability | M | K9 hoist `StartDispatch` out of blanket gate, K6 dispatch-loop skip-and-warn, K8 (MANDATORY) pre-flight observability | AC-2, AC-4, AC-6 | Phase 1's seeded default | (a) `af up manager` (positional) launches dispatch tmux session when `start_dispatch:true` + default valid; 2nd `af up manager` benign no-op; (b) with one mapped agent absent, dispatch LOOP skips that mapping w/ warning + dispatches others; `af config dispatch set` still exits non-zero (strict); `af dispatch status` reports distinct "references unprovisioned agents" state | +| 3 | Drift/golden test gate + docs | S | K7 golden + cross-file test, docs (two-label requirement + net-new scope) | AC-1 (drift-gated), AC-6 | Phases 1 & 2 | (a) test fails CI if a future edit makes the shipped default fail struct OR cross-file validation against default+provisioned agents.json; (b) setup docs state two-label requirement + net-new-install scope | + +**Dependency-ordered numbering note:** the design's phase order already follows +dependency order (Phase 1 default+seed → Phase 2 runtime auto-start/tolerance → +Phase 3 drift-gate/docs). The topo order of components is **K3 → K1 → K2 → K4 → K5 → +K6 → K7** (design-doc:130; K8/K9 added by cross-review). No renumbering needed. + +--- + +## 2. Components (design-doc §Key Components, lines 114-148) + +| Id | Component | Location (verified anchor) | New/Mod | Notes | +|----|-----------|----------------------------|---------|-------| +| K1 | `DefaultDispatchConfigJSON(repo string) string` — build from `DispatchConfig` struct; 4 mappings + `feature-workflow` + `trigger_label:"agentic"`; omit `notify_on_complete`; `remove_trigger_after_dispatch:true` | `internal/config` beside `DefaultFactoryConfigJSON` (config.go:111) | NEW | MUST NOT import `internal/formula` (cycle; dispatch.go:130-136) | +| K2 | Repo-discovery helper: `gh repo view --json nameWithOwner` (primary) + `git remote get-url origin` normalization (no-auth fallback); warn-don't-abort; **timeout-bounded** (scale S1.1) | `internal/cmd/install.go` `runInstallInit` (~:97+) | NEW | model: `DefaultGitIdentity()` config.go:89; precedent `quickdocker.sh:41` | +| K3 | Strict `owner/name` validator (allowlist regex `^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$`, one slash, no whitespace/shell-meta/`..`) at the WRITE boundary | `internal/cmd` or `internal/config` | NEW | guards `gh --repo` flag-injection (dispatch.go:300) + banner terminal-escape; `strings.Cut` (dispatch.go:537-539) is 2nd line of defense | +| K4 | Wire `runInstallInit` starter-config map to call K2→K3→K1; reuse write-if-absent | `internal/cmd/install.go:145` (literal replaced); guard install.go:150-157 | MOD | discover→validate→build→write BEFORE starterConfigs map built (one write) | +| K5 | `DefaultAgentsConfigJSON()` seeds 4 specialists `{"type":"autonomous","formula":""}` into default `agents.json` in SAME `runInstallInit` write | `internal/config` (new fn) + `internal/cmd/install.go:143` (literal replaced) | NEW+MOD | templates already embedded (`internal/templates/roles/{rapid-soldesign-plan,rapid-implement,ultra-review,rapid-increment}.md.tmpl`, verified) → no `agent-gen`/rebuild/quickstart step | +| K6 | Dispatch-loop unknown-agent tolerance: skip-and-warn (NOT hard-fail), scoped to dispatch-loop caller ONLY; `af config dispatch set` stays strict | `internal/cmd/dispatch.go` caller of `ValidateDispatchConfig` (:146) | MOD (defense-in-depth) | K8 MANDATORY wherever K6 enabled | +| K7 | Golden + cross-file tests: shipped default parses (`validateDispatchConfig`) AND cross-validates (`ValidateDispatchConfig`) against default-seeded `agents.json` — asserts on bare-init path | `internal/config/*_test.go`, `internal/cmd/*_test.go` | NEW | model: `internal/config/dispatch_workflow_test.go` | +| K8 | Pre-flight `ValidateDispatchConfig` surfaced at `af up` / `af dispatch status`; distinguishes "empty by design" vs "discovery failed" vs "references unprovisioned agents"; warn, never abort `af up` | `internal/cmd/up.go` / `af dispatch status` | NEW (mandatory) | additive to `af dispatch status --json` contract | +| K9 | Hoist `if startupCfg.StartDispatch { startDispatch(...) }` out of the `blanket`-only gate so positional `af up ` also auto-starts dispatcher | `internal/cmd/up.go:306,330` (gate up.go:92) | MOD | `startDispatch` idempotent — already-running benign no-op (dispatch.go:1322-1325, verified) | + +**Existing (consumed, anchors):** EX1 `validateDispatchConfig` (dispatch.go:141-185, struct); +EX2 `ValidateDispatchConfig` (dispatch.go:93-138, cross-file; called dispatch.go:146 AND +config_set.go:89); EX3 `af install --agents`/`agent-gen-all.sh:134-153` (NOT used by revised K5); +EX4 write-if-absent guard (install.go:150-157). + +--- + +## 3. Source ACs (source.md) & Constraints + +**ACs (verbatim, source.md:24-40):** +- AC-1: baked-in default for dispatch in dispatch.json → K1+K4 +- AC-2: tag issues to kick off work without visiting the manager → K1 mappings + K5 (specialists present) + K9 (auto-start on documented path) +- AC-3: bootstrap at first-create, know & include repo-name → K2 discovery + K3 validation + K4 write-if-absent +- AC-4: `af sling --agent` runs autonomously via identity formula up to human gate → EX sling (unchanged) + K5 (routed agents are formula-bearing) +- AC-5: branches pushed as PRs without doctor/human → **formula-layer property (scoped OUT, Gap-5)**; this change routes correctly + removes doctor dependency +- AC-6: formula process IS identity; systemic improvements → K5 routes to existing formulas unchanged; K1 single-source + K5 + K7 = repeatable validity + +**Constraints (source.md:42-74):** C-1 baked-in Go func (not script); C-2 bootstrap at +first-create (write-if-absent); C-3 actual `owner/name` discovered at install; C-4 +codebase is source of truth (docs are search aids); C-5 no doctor/human as ongoing +dependency (valid-by-construction); C-6 referenced agents MUST exist (K5 primary + +K6 defense). + +--- + +## 4. Dimension docs discovered & read (all of `.designs/73/*.md`) + +Read directly: design-doc.md, source.md, dependencies.md, conflicts.md. +Digested via parallel sub-agents (with file:line anchors preserved): +api.md, data.md, integration.md · security.md, scale.md, ux.md, codebase-snapshot.md · +six_sigma_gaps.md, elevation_assessment.md, audit.md, verification.md, +verification-report.md, cross-review/analyst-review-design.md. +Remaining (context): problem-summary-73.md, synthesis-checklist.md, +design-refinement-progress.md, cross-review/{designer-update-log.md, pr-body.md}. + +**Appendix links (design-doc:354-365):** source, verification, codebase-snapshot, +dimension analyses (api/data/ux/scale/security/integration), audit, conflicts, +dependencies, elevation_assessment, six_sigma_gaps, verification-report, +synthesis-checklist. + +--- + +## 5. Six-Sigma Gaps (six_sigma_gaps.md) — status + +| Gap | Impact | Status | Owner phase | +|-----|--------|--------|-------------| +| Gap-1: default references 4 absent specialists → hard-fail | CRITICAL | **closed** by K5 seed | P1 | +| Gap-2: `trigger_label` hard pre-filter — tagging only mapping label dispatches nothing (dispatch.go:301,320) | HIGH (UX) | named-and-scoped → docs (two-label requirement) | P3 | +| Gap-3: no repo discovery in Go init; empty `repos` silently skips | HIGH | closed by K2 (+K8 distinguishes) | P1/P2 | +| Gap-4: no golden/cross-file test → silent drift | HIGH | closed by K7 | P3 | +| Gap-5: AC-5 PR-push is a FORMULA-layer property, not dispatch.json | MED | scoped OUT (verified separately) | — | +| Gap-6: write-if-absent never upgrades existing installs | MED | named-and-scoped → net-new only (ADR-017); opt-in via `af config dispatch set` | P3 docs | +| Gap-7: explicit `notify_on_complete` adds brittle cross-file check | LOW | closed by omitting it (defaults to manager, const dispatch.go:15) | P1 | +| Gap-8: fresh-install dispatch failure low-visibility in tmux | MED | closed by K8 (mandatory) | P2 | + +--- + +## 6. Cross-review CRITICAL/HIGH (analyst-review-design.md) — all incorporated + +- **C1 (CRITICAL):** valid-by-construction held only on quickstart path; bare `af install + --init` registered only manager+supervisor → hard-fail. **Fix:** K5 seeds 4 specialists + in `runInstallInit` (templates embedded). Adopted verbatim. +- **C2 (CRITICAL):** dispatcher auto-start fires only on blanket `af up` (gate up.go:92,306,330); + documented `af up manager` (positional) never starts polling. **Fix:** K9 hoists + `StartDispatch` out of the blanket gate (idempotent). Adopted verbatim. +- **H1 (HIGH):** commit to minimal K5 mechanism (agents.json seeding, NOT `agent-gen-all.sh`). + Incorporated. +- **H2 (HIGH):** K6 skip-and-warn masks "configured-but-broken" as "running" → K8 MANDATORY + with K6. Incorporated. + +--- + +## 7. Architecture Elevation (elevation_assessment.md) + +Verdict: **Frame correct** — dispatch.json must exist (Candidate 0 "delete dispatch.json" +fails the subtraction gate; relocates the same fields + breaks the L-1 cross-file +validation seam dispatch.go:84-92). **One Frame-lift OFFERED & adopted:** repo +self-derivation (K2) from `git remote get-url origin` at install (today 0 grep hits for +git-remote read in `internal/`). OFFERED (not required) because empty-`repos` stays a +valid fallback and multi-repo keeps `Repos []string` editable. + +--- + +## 8. Cross-doc DRIFT flags (design-doc is authoritative) + +1. **K5 mechanism drift:** `dependencies.md:17` and `integration.md` (I3.1) and + `security.md` (SEC2.1) describe K5 as the OLD `af install --agents`-in-`quickstart.sh` + provisioning. The **design-doc.md (synthesis, post cross-review C1) supersedes this**: + K5 is now `DefaultAgentsConfigJSON()` seeding into the default `agents.json` inside + `runInstallInit` — NO quickstart step, NO `agent-gen-all.sh`, NO rebuild. The outline + MUST follow the design-doc, and note the stale companion docs. +2. **K8/K9 are cross-review additions:** not present in dependencies.md's original DAG; + the design-doc Risk Registry + Decisions + Implementation Plan carry them. Use the + design-doc. +3. **`notify_on_complete`:** source's proposed JSON includes `"manager"`; design omits it + (Gap-7). Follow the design (omit). +4. **`install.go` line drift:** the empty dispatch.json starter literal is cited as both + `install.go:145` and `install.go:176` across docs (the map vs the literal value); + codebase investigation (Phase 2) must confirm the actual current line numbers. + +--- + +## 9. Key MUST / MUST-NOT (for acceptance criteria authoring) + +- MUST build K1/K5 from structs (compile-time field safety); MUST NOT hand-author JSON. +- MUST NOT import `internal/formula` from `internal/config` (import cycle). +- MUST validate repo (K3 regex) at the WRITE boundary before disk/echo/`gh --repo`. +- MUST bound K2 discovery with a context timeout (scale S1.1); warn-don't-abort. +- MUST preserve write-if-absent (ADR-017): never clobber customer-edited config. +- MUST NOT prompt interactively (ADR-014); fail loud naming the remedy flag. +- K6 tolerance MUST be scoped to the dispatch-loop caller ONLY; write path strict. +- K8 MANDATORY wherever K6 ships (else K6 hides the failure). +- Default routes ONLY to formula-bearing specialists (verified present in + `install_formulas/` + `store/formulas/`). diff --git a/.designs/73/implementation-plan/implementation_plan_outline.md b/.designs/73/implementation-plan/implementation_plan_outline.md new file mode 100644 index 0000000..52632bc --- /dev/null +++ b/.designs/73/implementation-plan/implementation_plan_outline.md @@ -0,0 +1,377 @@ +# Implementation Plan Outline: Baked-in default `dispatch.json` for zero-touch label-triggered autonomy + +**Date**: 2026-06-28 +**Source**: `.designs/73/design-doc.md` (issue #73 / PR #74) +**Purpose**: Self-contained phase extraction guide for creating focused + IMPLREADME_PHASE{X}.md files +**Usage**: Phases are extracted one at a time by the design-phase-impl agent + (`af sling --agent design-phase-impl ""`), + which always extracts the NEXT pending phase + +--- + +## How To Use This Document + +Each phase below is a **self-contained extraction unit**. Workflow: + +1. `af sling --agent design-plan-impl ""` — produces this outline (Mode A) ✓ (this document) +2. Human reviews this outline (optionally `/peer-review {this_file}` first) +3. `af sling --agent design-phase-impl ""` — extracts the NEXT pending phase into an IMPLREADME (Mode B) +4. Use the phase's **Recommended Skill** to implement +5. Repeat steps 3-4 until every phase has an IMPLREADME and an implementation + (Skill-level equivalent of step 3, used by automated loops like soldesign-engineer: `/design-plan-impl {this_file} {N}`) + +**Phase dependency chain:** + +``` + ┌─────────────────────────────────────────────────────────────┐ + │ PHASE 1 Single-source default builder + repo discovery + │ + │ agents seed (K1 K2 K3 K4 K5 + fix existing test) │ + │ foundational — depends on nothing new │ + └───────────────┬─────────────────────────────────────────────┘ + │ (semantic: seeded default makes the common path valid, + │ so Phase 2's tolerance only handles the degraded path) + ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ PHASE 2 Dispatcher auto-start + graceful degradation + │ + │ observability (K9 K6 K8) │ + └───────────────┬─────────────────────────────────────────────┘ + │ (needs the valid default + the runtime behaviors to pin) + ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ PHASE 3 Drift/golden test gate + docs (K7 + docs) │ + │ depends on Phase 1 AND Phase 2 │ + └─────────────────────────────────────────────────────────────┘ +``` + +**Parallelism:** Phases 1 and 2 share **no compile-level dependency** — K9 is a pure +`up.go` gate refactor and K6/K8 consume the existing validator, so a two-agent split +could build them concurrently. They are sequenced here only so Phase 2's acceptance can +rely on Phase 1's valid default. **Phase 3 must run last** — it pins Phase 1's emitted +default and exercises Phase 2's behaviors. Within Phase 1, the topo order of components is +**K3 → K1 → K2 → K4 → K5** (K3 validator and K1 builder are leaf functions; K4 wires K1/K2; +K5 seeds in the same write). + +## Deployment Coverage + +| Target | Scripts/Config | Covered By Phase | Gap? | +|--------|---------------|-----------------|------| +| Bare `af install --init` ("hard way") | `internal/cmd/install.go` `runInstallInit` (97+) | Phase 1 (K1–K5 in the one write) | No | +| Container path (quickstart/quickdocker) | `quickstart.sh` (428, 441–470), `quickdocker.sh` | Phase 1 (SAME `runInstallInit` write — no script edit) | No | +| Documented startup `af up manager` | `internal/cmd/up.go` (92, 306, 330–335) | Phase 2 (K9 hoist) | No | +| Fresh-install dispatch visibility | `af up` / `af dispatch status` | Phase 2 (K8) | No | +| CI validity gate | `.github/workflows/test.yml` unit job (42–68, `make test`) | Phase 3 (K7 golden/cross-file) | No — closed | +| web/ module, `MaxWorktrees`, MCP issue-store | `web/`, `factory.json`, `mcpstore.New` (install.go:176) | — | Out of scope (correctly untouched) | + +> **Scope boundary (do NOT build a phase for this):** AC-5 ("all branches pushed as PRs +> without doctor/human") is a **formula-layer property, scoped OUT of this change** (design-doc +> Six-Sigma Caveats L291; six_sigma_gaps Gap-5). This change routes work to the four existing +> formulas (via K5) and removes the doctor dependency (valid-by-construction default), but it +> cannot and does not verify a formula's internal PR-push — that is verified separately. There +> is intentionally no phase for AC-5. +> +> **Cross-doc note for implementers:** the `design-doc.md` synthesis is authoritative. +> Companion docs `dependencies.md`, `integration.md` (I3.1), and `security.md` (SEC2.1) +> describe an OLDER K5 mechanism (provision via `af install --agents` in `quickstart.sh`). +> That was **superseded by cross-review C1**: K5 now seeds the four specialists into the +> default `agents.json` inside `runInstallInit` (templates already embedded; `af install +> --agents` would recurse into quickstart and refuses to run from a worktree). Follow this +> outline + `design-doc.md`, not the stale companion docs. + +--- + +## Phase 1: Single-source default builder + repo discovery + agents seed + +### Objective +Make a freshly-initialized factory ship a **valid-by-construction** default `dispatch.json` +(real `owner/name`, 4 label→agent mappings, `feature-workflow`) AND a default `agents.json` +that already contains the four referenced specialists — both written in the single existing +`runInstallInit` write so the result is valid on **every** init path (bare init and quickstart). + +### Prerequisites +None (foundational). Internal component order: K3 → K1 → K2 → K4 → K5. + +### Recommended Skill or Agent +`*implement` (e.g. `rapid-implement`) — clear, fully-specified backend Go change with no +design exploration; all patterns to mirror already exist in-tree. + +### Design References +| Document | Section | Lines | What It Specifies | +|----------|---------|-------|-------------------| +| design-doc.md | Implementation Plan → Phase 1 | L307–323 | K1–K5 deliverables + phase ACs | +| design-doc.md | Data Model (from data.md) | L160–219 | intent-corrected default JSON + default agents.json seed | +| design-doc.md | Interface (from api.md) | L150–158 | `DefaultDispatchConfigJSON(repo string) string`; no new flags; banner | +| design-doc.md | Key Components K1–K5 | L118–122 | per-component scope + locations | +| data.md | Data Model / D2.1 schema rules + D2.2-A | L160–185 (proposed default), schema-consistency rules | mapping/workflow validity (single-label phases, same source, no label collisions) | +| api.md | A2.1/A2.2 discovery + A3.1 degrade | discovery helper | `gh repo view --json nameWithOwner` + `git remote` normalization, warn-don't-abort | +| security.md | SEC1.1 / SEC3.1 | validator + write guard | `^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$` at write boundary; preserve write-if-absent | +| scale.md | S1.1 | discovery cost | bound the discovery call with a context timeout | +| codebase-investigation.md | §1 anchors, §2 D-a/D-b/D-d/D-e, §4 reuse | full | verified current lines + discrepancies + reuse assets | + +### Current State (files to read for context) +| File | Lines | What's There | +|------|-------|-------------| +| `internal/config/config.go` | L108–124 | `DefaultFactoryConfigJSON()` — the struct→`json.Marshal`→string pattern K1 & K5 MIRROR | +| `internal/config/config.go` | L88–91, 35–37 | `DefaultGitIdentity()` + constants — in-code-default idiom | +| `internal/config/dispatch.go` | L17–27, 29–35, 42–45 | `DispatchConfig` / `DispatchMapping` / `Workflow` structs (build K1 from these) | +| `internal/config/dispatch.go` | L140–185 | `validateDispatchConfig` (struct rules; fills `NotifyOnComplete`→"manager" at 181–183) | +| `internal/config/dispatch.go` | L84–138 | `ValidateDispatchConfig` (cross-file; import-cycle comment 131–136) | +| `internal/config/dispatch.go` | L15 | `const defaultNotifyAgent = "manager"` | +| `internal/cmd/install.go` | L97, 139–148 | `runInstallInit` + starter-config map | +| `internal/cmd/install.go` | **L143** | agents.json inline literal (manager+supervisor ONLY) — K5 replaces | +| `internal/cmd/install.go` | **L145** | dispatch.json inline literal (`repos:[]`,`mappings:[]`) — K4 replaces (ONLY location; `:176` is `mcpstore.New`) | +| `internal/cmd/install.go` | L150–156 | write-if-absent guard (reused unchanged) | +| `internal/cmd/detect_default_branch.go` | L14, 21, 34–43, 67 | ⭐ K2/K3 template: 5s timeout, allowlist regex, `runGitDetect` seam, `gh repo view --json` | +| `internal/templates/roles/` | — | `rapid-soldesign-plan/rapid-implement/ultra-review/rapid-increment.md.tmpl` (all embedded) | +| `internal/cmd/install_integration_test.go` | **L66–72** | existing test asserts `repos:[]`/`mappings:[]` — WILL break; must be updated here | + +### Required Changes + +**File 1 (NEW): `internal/config/config.go`** — add `DefaultDispatchConfigJSON` (K1) and `DefaultAgentsConfigJSON` (K5), mirroring `DefaultFactoryConfigJSON` (108–124). +```go +// K1 — build from the struct so the field set is compiler-checked; OMIT NotifyOnComplete +// (validateDispatchConfig fills it → "manager" at runtime, dispatch.go:181-183). +func DefaultDispatchConfigJSON(repo string) string { + repos := []string{} + if repo != "" { repos = []string{repo} } + cfg := DispatchConfig{ + Repos: repos, TriggerLabel: "agentic", + IntervalSecs: 300, RetryAfterSecs: 1800, RemoveTriggerAfterDispatch: true, + Mappings: []DispatchMapping{ + {Labels: []string{"rapid-plan"}, Source: "issue", Agent: "rapid-soldesign-plan"}, + {Labels: []string{"rapid-engineer"}, Source: "issue", Agent: "rapid-implement"}, + {Labels: []string{"pr-review"}, Source: "pr", Agent: "ultra-review"}, + {Labels: []string{"pr-iterate"}, Source: "pr", Agent: "rapid-increment"}, + }, + Workflows: []Workflow{{Label: "feature-workflow", Phases: []string{"rapid-plan", "rapid-engineer"}}}, + } + b, err := json.Marshal(cfg) + if err != nil { return `{"repos":[],"trigger_label":"agentic","mappings":[]}` } // unreachable fallback + return string(b) +} +// K5 — seed the 4 specialists alongside manager+supervisor. +func DefaultAgentsConfigJSON() string { /* mirror DefaultFactoryConfigJSON: build AgentConfig, marshal */ } +``` +- MUST NOT import `internal/formula` (cycle — dispatch.go:131–136). + +**File 2 (MODIFY): `internal/cmd/install.go`** +- **Before L139** (in `runInstallInit`, after the `cd`-into-repo context is available): add K2 repo discovery + K3 validation, mirroring `detect_default_branch.go` (seam + 5s `context.WithTimeout` + `gh repo view --json nameWithOwner`, fallback `git remote get-url origin` normalized, then K3 validate; on any failure → `repo=""` + a loud stderr warning naming `af config dispatch set`). +- **L143**: replace the agents.json literal with `config.DefaultAgentsConfigJSON()` (K5). +- **L145**: replace the dispatch.json literal with `config.DefaultDispatchConfigJSON(discoveredRepo)` (K4). +- Optional UX banner: echo the validated repo (escape-safe via K3). + +**File 3 (NEW): K3 validator** — package-level `regexp.MustCompile(`^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$`)` (in `internal/config` beside K1, or `internal/cmd` beside discovery). Rejects empty, leading `-`, whitespace, `..`, shell/escape chars BEFORE the value reaches disk / the banner / `gh --repo`. + +**File 4 (MODIFY): `internal/cmd/install_integration_test.go` L66–72** — update the `repos:[]`/`mappings:[]` assertions to expect the new valid default (discovered repo, 4 mappings, `feature-workflow`) and assert the 4 seeded agents in agents.json on the bare-init path. + +### Acceptance Criteria +```bash +# 1. K1 output passes struct validation (unit) +go test ./internal/config/ -run 'TestDefaultDispatchConfigJSON' -count=1 +# Expected: ok (PASS) + +# 2. Bare `af install --init` in a temp repo with a known remote yields the valid default +R=$(mktemp -d); git -C "$R" init -q; git -C "$R" remote add origin git@github.com:acme/widget.git +( cd "$R" && af install --init >/dev/null ) +jq -e '.repos==["acme/widget"]' "$R/.agentfactory/dispatch.json" # Expected: true +jq -e '.mappings|length==4' "$R/.agentfactory/dispatch.json" # Expected: true +jq -e '.workflows[0].label=="feature-workflow"' "$R/.agentfactory/dispatch.json" # Expected: true +jq -e 'has("notify_on_complete")|not' "$R/.agentfactory/dispatch.json" # Expected: true (omitted) +jq -e '.agents|has("rapid-soldesign-plan") and has("rapid-implement") and has("ultra-review") and has("rapid-increment")' "$R/.agentfactory/agents.json" # Expected: true (all 4 specialists seeded) + +# 3. cross-file validity on the bare-init artifacts (no unknown agent) +go test ./internal/config/ -run 'TestValidateDispatchConfig' -count=1 +# Expected: ok (PASS) — default mappings resolve against the seeded agents.json + +# 4. Idempotent re-run does not clobber +cp "$R/.agentfactory/dispatch.json" /tmp/d.bak; ( cd "$R" && af install --init >/dev/null ) +diff -q /tmp/d.bak "$R/.agentfactory/dispatch.json" # Expected: files identical (no output) + +# 5. Crafted/garbage remote is rejected -> empty repos + loud warning (no malformed write) +R2=$(mktemp -d); git -C "$R2" init -q; git -C "$R2" remote add origin 'https://x/--evil/$(touch pwned)' +( cd "$R2" && af install --init 2>/tmp/warn.txt >/dev/null ) +jq -e '.repos==[]' "$R2/.agentfactory/dispatch.json" # Expected: true +grep -qi 'warn\|could not' /tmp/warn.txt # Expected: exit 0 (warning emitted) + +# 6. Updated existing integration test passes +go test ./internal/cmd/ -run 'TestInstall' -count=1 +# Expected: ok (PASS) + +# 7. Full root-module suite stays green +make test +# Expected: all packages ok +``` + +### Gotchas (from codebase investigation) +- ⚠️ **Existing test breaks:** `internal/cmd/install_integration_test.go:66–72` asserts the + OLD empty defaults; update it IN THIS PHASE or CI reddens. +- **Import cycle:** new code in `internal/config` (K1/K5) MUST NOT import `internal/formula` + (dispatch.go:131–136 documents why). +- **Omit `notify_on_complete`:** `validateDispatchConfig` fills it to "manager" at runtime + (181–183); shipping it explicitly only adds a brittle cross-file check (Gap-7). +- **Reuse `detect_default_branch.go`,** don't reinvent: it already does `gh repo view --json` + with a `runGitDetect` seam + 5s timeout (need field `nameWithOwner` instead of + `defaultBranchRef`); the design's "0 git-remote grep hits" is only true for `git remote`. +- **K3 ordering:** validate BEFORE the value touches disk, the banner, or `gh --repo` + (flag-injection via leading `-`; terminal-escape in the banner). +- **`gh` auth may be absent at install:** `git remote get-url origin` is the auth-free + fallback (handle `git@host:o/r.git`, `https://host/o/r.git`, `https://host/o/r`). +- **Line citation:** the empty dispatch.json literal is at `install.go:145` ONLY (the design's + `:176` is `mcpstore.New`). + +--- + +## Phase 2: Dispatcher auto-start + graceful degradation + observability + +### Objective +Make the documented `af up manager` (positional) path actually start the dispatcher, let the +dispatch LOOP degrade gracefully (skip-and-warn) on a partially-provisioned factory without +weakening the strict write path, and surface dispatch-config validity so a broken loop is +visible instead of silently spinning. + +### Prerequisites +None at compile level. **Validated against Phase 1** (the seeded valid default means the +common path needs no degradation; K6/K8 cover the edited/partial factory). Treat Phase 1 as +a logical predecessor for acceptance. + +### Recommended Skill or Agent +`*implement` — localized backend Go edits across `up.go` and `dispatch.go`; no design exploration. + +### Design References +| Document | Section | Lines | What It Specifies | +|----------|---------|-------|-------------------| +| design-doc.md | Implementation Plan → Phase 2 | L325–339 | K9/K6/K8 deliverables + phase ACs | +| design-doc.md | Key Components K6/K8/K9 | L123–126 | per-component scope + locations | +| design-doc.md | Cross-Perspective Conflicts (C2/H1/H2) | L243–244 | hoist auto-start; commit minimal K5; K8 mandatory with K6 | +| design-doc.md | Risk Registry + K8 note | L273, L282–285 | positional-`af up` risk; K8 observability hardening | +| integration.md | I3.2 (dispatcher tolerance) | tolerance scope | skip-and-warn dispatch-loop caller only; write path strict | +| security.md | SEC2.2 | defense-in-depth | tolerate unknown agents with loud warning | +| ux.md | U2.1 | error messages | name remedy `af install --agents` / `af config dispatch set` | +| codebase-investigation.md | §1 (up.go/dispatch.go/config_set.go), §5 (3–4) | full | verified anchors + hoist-safety | + +### Current State (files to read for context) +| File | Lines | What's There | +|------|-------|-------------| +| `internal/cmd/up.go` | L92 | `blanket := len(args) == 0`; `startupCfg` read at L82 | +| `internal/cmd/up.go` | L306, 330–335 | `if blanket { … if startupCfg.StartDispatch { startDispatch(...) } }` — K9 hoists the StartDispatch block out of the `blanket` gate | +| `internal/cmd/dispatch.go` | L146–148 | dispatch-loop `ValidateDispatchConfig` call (hard-fail) — K6 relaxes THIS caller only | +| `internal/cmd/dispatch.go` | L1321–1340 | `startDispatch` — already-running no-op (1322–1325), unconfigured skip (1328–1331); idempotent (enables K9) | +| `internal/cmd/dispatch.go` | L1356–1405, 1458–1461 | `runDispatchStatus` + `dispatchStatusJSON{ DispatcherRunning, Entries }` — K8 extends this | +| `internal/config/dispatch.go` | L93, 100–103 | `ValidateDispatchConfig` signature + unknown-agent error (the rule K6 wraps) | +| `internal/cmd/config_set.go` | L89–90 | strict `ValidateDispatchConfig` on the WRITE path — MUST stay hard-fail | +| `internal/config/startup.go` | L18 | `StartDispatch bool` (`start_dispatch`); install.go:147 ships `true` | + +### Required Changes + +**File 1 (MODIFY): `internal/cmd/up.go`** — K9: move the `if startupCfg.StartDispatch { startDispatch(...) }` block (currently 330–335) OUT of the `if blanket {` block (306) so it runs for positional `af up ` too. `startDispatch` is idempotent (no double-start). K8: add a read-only pre-flight `ValidateDispatchConfig` that WARNS (never aborts `af up`) and classifies the state. + +**File 2 (MODIFY): `internal/cmd/dispatch.go`** — K6: at the dispatch-loop call site (146–148), replace the hard `return err` with skip-and-warn for the unknown-agent case — drop the offending mapping(s), emit a loud per-mapping warning naming `af install --agents`, and continue dispatching the rest. Scope strictly to this caller. K8: extend `dispatchStatusJSON` (1458–1461) with a config-validity field (e.g. `config_state: "ok" | "empty_by_design" | "discovery_failed" | "references_unprovisioned_agents"`), derived by reusing the `ErrNotFound`/`ErrMissingField` distinction (1328–1331). + +**File 3 (UNCHANGED, guard): `internal/cmd/config_set.go` L89–90** — confirm the write path keeps the strict `ValidateDispatchConfig`. K6 MUST NOT touch it. + +### Acceptance Criteria +```bash +# 1. Positional auto-start + idempotency (needs a valid default from Phase 1) +af up manager +# Expected: dispatch tmux session launched (e.g. "Dispatcher started") +af up manager +# Expected: "Dispatcher already running (session: ...)" (benign no-op) +tmux has-session -t af-dispatch 2>/dev/null && echo RUNNING +# Expected: RUNNING + +# 2. Dispatch LOOP skips an unknown-agent mapping with a warning and dispatches the rest +go test ./internal/cmd/ -run 'TestDispatch.*(Unknown|Skip|Tolerat)' -count=1 +# Expected: ok (PASS) — offending mapping skipped+warned, others dispatched + +# 3. Write path stays STRICT (K6 did not leak) +printf '%s' '{"repos":["o/r"],"trigger_label":"agentic","mappings":[{"labels":["x"],"source":"issue","agent":"does-not-exist"}]}' | af config dispatch set; echo "exit=$?" +# Expected: exit=1 (non-zero), file unchanged + +# 4. Status observability distinguishes the failure modes +af dispatch status --json | jq -e 'has("config_state")' +# Expected: true (value in: ok | empty_by_design | discovery_failed | references_unprovisioned_agents) + +# 5. Suite green +make test +# Expected: all packages ok +``` + +### Gotchas (from codebase investigation) +- **Scope K6 to the dispatch-loop caller ONLY** (dispatch.go:146); `config_set.go:89–90` + MUST remain hard-fail (human-typo guard). +- **K8 is MANDATORY with K6** (cross-review H2): without it, K6 turns a clean "not configured" + skip into a silently-warning loop that looks active but dispatches nothing. +- **K9 hoist is safe:** investigation found only the one blanket-gated `startDispatch` caller; + idempotency (1322–1325) prevents double-start. +- **Don't break the existing `--json` contract:** add `config_state` ADDITIVELY; keep + `dispatcher_running` and `entries`. +- **Two-label pre-filter (context):** the trigger-label query (dispatch.go ~301/320) is a hard + pre-filter — K6/K8 do not change it; the user-facing two-label caveat is a Phase-3 docs item. + +--- + +## Phase 3: Drift/golden test gate + docs + +### Objective +Mechanically gate the shipped default against drift (so a future label/phase/formula rename +fails CI), and document the two operational caveats — the two-label requirement and the +net-new-install scope. + +### Prerequisites +Phase 1 (the emitted default + seeded agents to pin) AND Phase 2 (the runtime behaviors to assert). + +### Recommended Skill or Agent +`*implement` — Go test code plus markdown doc edits in the same repo; no design exploration. + +### Design References +| Document | Section | Lines | What It Specifies | +|----------|---------|-------|-------------------| +| design-doc.md | Implementation Plan → Phase 3 | L340–352 | K7 golden/cross-file test + docs deliverables + phase ACs | +| design-doc.md | Risk Registry (formula-rename) | L278 | drift test ties default to shipped agents | +| six_sigma_gaps.md | Gap-2 / Gap-4 / Gap-6 | — | two-label requirement; drift gate; net-new scope | +| codebase-investigation.md | §1 (test models), §3 (CI gap) | full | `dispatch_workflow_test.go` model; no current init-validity gate | + +### Current State (files to read for context) +| File | Lines | What's There | +|------|-------|-------------| +| `internal/config/dispatch_workflow_test.go` | L212–257 | cross-file validation table — the K7 model | +| `internal/config/formula_drift_test.go` | Full file | ADR-008 embedded-vs-installed drift pattern (model for a golden/drift assertion) | +| `internal/cmd/install_integration_test.go` | L66–72 | bare-init assertions (updated in Phase 1; K7 complements at the config layer) | +| `.github/workflows/test.yml` | L42–68 (unit), 96–124 (integration), 126–140 (regen) | CI tiers; `make test` runs the unit tier where K7 lands | +| `USING_AGENTFACTORY.md` | dispatch section (~L225 example) | documents label-triggering; two-label requirement NOT stated; net-new scope NOT stated | + +### Required Changes + +**File 1 (NEW): `internal/config/dispatch_default_test.go`** (or extend `dispatch_workflow_test.go`) — K7: assert `DefaultDispatchConfigJSON("acme/widget")` (a) parses + passes `validateDispatchConfig`, and (b) cross-validates via `ValidateDispatchConfig` against `DefaultAgentsConfigJSON()`'s parsed agents (so the 4 mappings resolve). Pin the exact mapping label→agent set + the `feature-workflow` phases, and accept `notify_on_complete` omitted-OR-"manager". Mirror `dispatch_workflow_test.go:212–257`. + +**File 2 (MODIFY): `USING_AGENTFACTORY.md`** — docs-1 (Gap-2): state the **two-label requirement** — an issue/PR must carry BOTH the `agentic` trigger label AND a mapping/workflow label to dispatch (the trigger label is a hard query pre-filter). docs-2 (Gap-6): state that the baked-in default ships for **net-new installs only**; existing factories opt in via `af config dispatch set` (write-if-absent never clobbers customer config — ADR-017). + +### Acceptance Criteria +```bash +# 1. Golden + cross-file drift gate passes +go test ./internal/config/ -run 'TestDefaultDispatch.*(Golden|CrossFile|Drift)' -count=1 +# Expected: ok (PASS) + +# 2. The gate actually catches drift: a renamed mapping label / agent breaks it +# (verify by temporarily editing DefaultDispatchConfigJSON -> the test must FAIL) +# Expected: test reddens on any mismatch vs the pinned default + seeded agents + +# 3. Docs state the two-label requirement and the net-new scope +grep -qiE 'agentic.*(and|plus|\+|both).*label|two[ -]label|both labels' USING_AGENTFACTORY.md +# Expected: exit 0 +grep -qiE 'net[ -]new|new install|af config dispatch set' USING_AGENTFACTORY.md +# Expected: exit 0 + +# 4. Suite green (K7 runs in the unit tier) +make test +# Expected: all packages ok +``` + +### Gotchas (from codebase investigation) +- **No CI job exercises `af install --init` dispatch validity today** — K7 must live where + `make test` runs it (unit tier, test.yml:42–68), so a drift reaches CI, not the customer. +- **Accept omitted-OR-"manager"** for `notify_on_complete` (runtime fills it). +- **Two-label requirement is docs-only** (Gap-2): widening the trigger-label query is out of + scope (expands the dispatch blast radius); the only safe fix is documentation. +- **Net-new scope is a policy, not a bug** (ADR-017): do not add an auto-migration of existing + empty `dispatch.json` installs; name the opt-in path instead. diff --git a/.designs/73/implementation-plan/synthesis.md b/.designs/73/implementation-plan/synthesis.md new file mode 100644 index 0000000..52114a4 --- /dev/null +++ b/.designs/73/implementation-plan/synthesis.md @@ -0,0 +1,180 @@ +# Synthesis — Issue #73 (Phase 3: Cross-Reference) + +Merges the design spec (`design-doc.md`) with firsthand ground truth +(`codebase-investigation.md`). Per-phase: adjusted scope, gotchas, refined ACs (bash), +design refs, workstream class, deployment coverage. This is the blueprint for the +implementation_plan_outline. + +## Global synthesis decisions + +- **Phase 0: NONE.** `make test` is green on main; no latent bug exists in the code the + design modifies. The only "surprise" (existing test `install_integration_test.go:66–72` + breaks on the new defaults) is repaired INSIDE Phase 1, not before it. +- **Dependency order = design narrative order P1 → P2 → P3.** No renumbering. + - P1 (default builders + discovery + seed) is foundational; depends on nothing new. + - P2 (K9/K6/K8) has NO compile dependency on P1 (K9 is a pure `up.go` gate refactor; + K6/K8 consume the existing `ValidateDispatchConfig`). Its semantic dependency is that + P1's seeded default makes the common path valid, so K6/K8 only handle the degraded + path — therefore P2's acceptance is validated AFTER P1. Code-level, P1 and P2 could be + built in parallel; the outline keeps them sequential for clean acceptance. + - P3 (K7 + docs) depends on BOTH P1 (default+seed exist to pin) and P2 (behaviors to test). +- **No separate parity phase.** The audit found bare-init AND quickstart both ship invalid + defaults today (C1). The design's K5 seed lives in `runInstallInit`, fixing BOTH paths in + ONE write — parity is intrinsic to P1. The parity ASSERTION is the updated bare-init + integration test (P1) + the K7 cross-file golden test (P3). +- **Workstream classification:** all three phases are **Backend (Go)** → **`*implement`**. + Phase 3 also edits docs (markdown, no design exploration) → still **`*implement`**. + NO Infrastructure/Terraform (none in repo), NO Frontend UI, NO Manual/Human steps. + (See routing table at end.) + +--- + +## PHASE 1 — Single-source default builder + repo discovery + agents seed + +**Adjusted scope (CREATE vs MODIFY, with reuse):** +| Comp | Action | Where (current) | Reuse / model | +|------|--------|-----------------|---------------| +| K1 | CREATE `DefaultDispatchConfigJSON(repo string) string` | `internal/config/config.go` (beside 108–124) | mirror `DefaultFactoryConfigJSON` (struct→marshal→string); build from `DispatchConfig` struct (dispatch.go:17–27) | +| K2 | CREATE repo-discovery helper + call in `runInstallInit` | `internal/cmd/install.go` (runInstallInit @97; call before map @139) | **mirror `detect_default_branch.go`**: `runGitDetect` seam (34–43), `context.WithTimeout` 5s, `gh repo view --json nameWithOwner` (cf :67 uses `defaultBranchRef`), `git remote get-url origin` normalization fallback; warn-don't-abort | +| K3 | CREATE strict `owner/name` validator | `internal/config` or `internal/cmd` | extend allowlist idiom (`detect_default_branch.go:21`) → `^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$` | +| K4 | MODIFY: replace inline dispatch.json literal | `internal/cmd/install.go:145` | reuse write-if-absent (150–156) | +| K5 | CREATE `DefaultAgentsConfigJSON()` + MODIFY inline agents.json literal | `internal/config/config.go` + `install.go:143` | mirror `DefaultFactoryConfigJSON`; seed 4 `{"type":"autonomous","formula":""}` (templates embedded) | +| **K-test** | **MODIFY existing test (NEW — not in design)** | `internal/cmd/install_integration_test.go:66–72` | assertions `repos:[]`/`mappings:[]` now wrong; update to assert the valid default + 4 seeded agents on the bare-init path | +| (banner) | ADD escape-safe install line echoing discovered repo (optional, UX U1.2) | `runInstallInit` stdout | derive from the validated (K3) repo | + +**Gotchas (investigation-found):** +- ⚠️ Existing `install_integration_test.go:66–72` WILL fail unless updated in this phase (CI reddens otherwise). +- Import cycle: K1/K5 in `internal/config` MUST NOT import `internal/formula` (dispatch.go:131–136). +- `validateDispatchConfig` already fills `NotifyOnComplete`→"manager" at runtime (dispatch.go:181–183) → K1 OMITS the field (Gap-7). +- K3 must reject leading `-` (gh flag-injection into `gh --repo`) + shell/terminal-escape BEFORE the value hits disk/banner/gh. +- `gh repo view` needs `gh auth`; `git remote get-url origin` is the auth-free fallback (3 URL shapes: `git@…:o/r.git`, `https://…/o/r.git`, `https://…/o/r`). +- Discovery timing OK on both paths: quickstart `cd`s into the repo (quickstart.sh:428) before `af install --init` (:441); bare-init runs in the user's git repo. +- Design line-error: dispatch.json literal is at install.go:**145** ONLY (`:176` is `mcpstore.New`). + +**Refined acceptance criteria (bash, executable):** +```bash +# AC1.a struct validity (unit) +go test ./internal/config/ -run 'TestDefaultDispatchConfigJSON' -count=1 # PASS +# AC1.b bare init in a temp repo with a known remote +R=$(mktemp -d); git -C "$R" init -q; git -C "$R" remote add origin git@github.com:acme/widget.git +( cd "$R" && af install --init >/dev/null ) +jq -e '.repos==["acme/widget"]' "$R/.agentfactory/dispatch.json" # true +jq -e '.mappings|length==4' "$R/.agentfactory/dispatch.json" # true +jq -e '.workflows[0].label=="feature-workflow"' "$R/.agentfactory/dispatch.json" # true +jq -e 'has("notify_on_complete")|not' "$R/.agentfactory/dispatch.json" # true (omitted) +jq -e '.agents|keys|index("rapid-soldesign-plan")!=null and index("rapid-implement")!=null and index("ultra-review")!=null and index("rapid-increment")!=null' "$R/.agentfactory/agents.json" # true +# AC1.c idempotent re-run does not clobber +cp "$R/.agentfactory/dispatch.json" /tmp/d.bak; ( cd "$R" && af install --init >/dev/null ) +diff -q /tmp/d.bak "$R/.agentfactory/dispatch.json" # identical +# AC1.d crafted remote rejected → empty repos + loud warning +R2=$(mktemp -d); git -C "$R2" init -q; git -C "$R2" remote add origin 'https://x/--evil/$(touch pwned)' +( cd "$R2" && af install --init 2>/tmp/warn.txt >/dev/null ); jq -e '.repos==[]' "$R2/.agentfactory/dispatch.json" # true +grep -qi 'warn' /tmp/warn.txt # warning emitted +# AC1.e updated existing integration test passes +go test ./internal/cmd/ -run 'TestInstall' -count=1 # PASS +``` + +**Design references:** design-doc.md:307–323 (Phase 1), :160–219 (default JSON + agents seed), +:150–158 (K1 interface); data.md:160–185 (intent-corrected default), D2.1 schema rules; +api.md A1.1/A2.1/A2.2; integration.md I1.1/I2.1; security.md SEC1.1 (regex)/SEC3.1; +scale.md S1.1 (timeout); codebase-investigation.md §1 (anchors), §2 (D-a,D-b,D-d,D-e). + +**Workstream:** Backend (Go) → **`*implement`**. +**Deployment coverage:** one `runInstallInit` write fixes bare-init AND quickstart (parity +intrinsic); AC1.b/AC1.e assert the bare path. + +--- + +## PHASE 2 — Dispatcher auto-start + graceful degradation + observability + +**Adjusted scope:** +| Comp | Action | Where (current) | Notes | +|------|--------|-----------------|-------| +| K9 | MODIFY: hoist `StartDispatch` block out of the blanket gate | `internal/cmd/up.go:330–335` (gate @306; `blanket:=len(args)==0` @92) | positional `af up ` also auto-starts; `startDispatch` idempotent (dispatch.go:1322–1325) | +| K6 | MODIFY: dispatch-loop skip-and-warn on unknown agent | `internal/cmd/dispatch.go:146–148` (caller only) | MUST NOT relax `config_set.go:89–90` (write path stays strict) | +| K8 | CREATE pre-flight validation + extend status JSON | `internal/cmd/up.go` (pre-flight, warn-never-abort) + `dispatch.go:1356–1405` / `dispatchStatusJSON` (1458–1461) | distinguish "empty by design" vs "discovery failed" vs "references unprovisioned agents" (reuse `ErrNotFound`/`ErrMissingField` idiom @1328–1331) | + +**Gotchas:** +- K6 scoped to the dispatch-loop caller ONLY (dispatch.go:146); `config_set.go:89–90` MUST remain hard-fail. +- K8 is MANDATORY wherever K6 ships (cross-review H2 — else K6 hides "configured-but-broken"). +- K9 hoist is safe: audit found only the one blanket-gated `startDispatch` caller; idempotency prevents double-start. +- K8 status field is additive to the existing `af dispatch status --json` contract (don't break `DispatcherRunning`/`Entries`). + +**Refined acceptance criteria (bash):** +```bash +# AC2.a positional auto-start + idempotent +af up manager # -> launches dispatch tmux session when start_dispatch:true & default valid +af up manager # -> "Dispatcher already running" (benign no-op) +tmux has-session -t af-dispatch 2>/dev/null && echo RUNNING # RUNNING +# AC2.b degrade: one mapped agent absent -> loop skips+warns, dispatches the rest +go test ./internal/cmd/ -run 'TestDispatch.*Unknown(Agent|Mapping)' -count=1 # PASS (skip-and-warn) +# AC2.c write path stays strict +echo '' | af config dispatch set; echo $? # non-zero +# AC2.d status observability +af dispatch status --json | jq -e '.config_state' # e.g. "references_unprovisioned_agents" | "empty_by_design" | "ok" +``` + +**Design references:** design-doc.md:325–339 (Phase 2), :123–126 (K6/K8/K9), :243–244 +(cross-review C2/H1/H2), :273 (risk), :282–285 (K8); integration.md I3.2; security.md +SEC2.2; ux.md U2.1; codebase-investigation.md §1 (up.go/dispatch.go/config_set.go anchors), +§5 (hidden issues 3–4). + +**Workstream:** Backend (Go) → **`*implement`**. +**Deployment coverage:** K9 fixes the documented `af up manager` path (C2); K8 makes +fresh-install dispatch state visible (Gap-8). + +--- + +## PHASE 3 — Drift/golden test gate + docs + +**Adjusted scope:** +| Comp | Action | Where (current) | Model | +|------|--------|-----------------|-------| +| K7 | CREATE golden + cross-file test | `internal/config/*_test.go` (+ `internal/cmd/*_test.go`) | mirror `dispatch_workflow_test.go:212–257` (cross-file table); drift like `formula_drift_test.go` (ADR-008) | +| docs-1 | MODIFY: state two-label requirement (`agentic` + mapping/workflow label) | `USING_AGENTFACTORY.md` (dispatch section; example at ~:225) | Gap-2 | +| docs-2 | MODIFY: state net-new-install scope (existing factories opt in via `af config dispatch set`) | `USING_AGENTFACTORY.md` | Gap-6 / ADR-017 | + +**Gotchas:** +- K7 must accept `notify_on_complete` omitted-OR-"manager" (runtime fills it). +- No CI job exercises `af install --init` dispatch validity today → K7 lands in the `make test` + unit tier (test.yml unit job 42–68) to gate AC-1/C-6 against drift. +- Two-label requirement (Gap-2): trigger-label query is a hard pre-filter (dispatch.go ~301/320); + tagging only a mapping label dispatches nothing — docs-only fix (query-widening out of scope). + +**Refined acceptance criteria (bash):** +```bash +# AC3.a drift gate: shipped default parses AND cross-validates vs default-seeded agents.json +go test ./internal/config/ -run 'TestDefaultDispatch.*(Golden|CrossFile|Drift)' -count=1 # PASS +# AC3.b a deliberate mutation of the default OR a renamed mapping label fails the test +# (manually verify the test reddens when DefaultDispatchConfigJSON is edited) +# AC3.c docs +grep -qiE 'agentic.*(and|plus|\+).*label|two label|both labels' USING_AGENTFACTORY.md # two-label requirement stated +grep -qiE 'net-new|new install|af config dispatch set' USING_AGENTFACTORY.md # net-new scope + opt-in stated +``` + +**Design references:** design-doc.md:340–352 (Phase 3), :278 (formula-rename risk), +:298–299 (gaps closed); six_sigma_gaps.md Gap-2/Gap-4/Gap-6; codebase-investigation.md +§1 (test models), §3 (CI gap). + +**Workstream:** Backend (Go test) + docs → **`*implement`**. +**Deployment coverage:** closes the CI gap (no current `af install --init` validity gate). + +--- + +## Workstream routing table + +| Phase | Domain | Recommended skill/agent | Rationale | +|-------|--------|-------------------------|-----------| +| 1 | Backend (Go: config builders, install wiring, discovery, validator) + test update | `*implement` (e.g. `rapid-implement`) | clear spec, no design exploration; pure Go | +| 2 | Backend (Go: up.go gate, dispatch loop, status JSON) | `*implement` | clear spec; localized Go edits | +| 3 | Backend (Go test) + docs (markdown) | `*implement` | test code + doc edits, same repo/skill | + +No `*terraform` (no infra), no `*design` (no UI/design exploration), no `manual` +(fully autonomous Go + docs) workstreams exist for this change. + +## Deployment-gap verdict +- Environments in repo: bare-init path, quickstart/quickdocker path, CI (test.yml), web/ module. +- Parity gap (bare-init vs quickstart invalid default) is CLOSED by P1's single-write seed — no + parity phase needed; asserted by P1's updated integration test + P3's K7 cross-file test. +- CI coverage gap (no init-validity gate) CLOSED by P3 K7 in the unit tier. +- web/ module, MaxWorktrees, MCP store: correctly untouched by this change. From fca455b0f0e4b1f51b3735564abdf7ec20f9d752 Mon Sep 17 00:00:00 2001 From: agentfactory Date: Sun, 28 Jun 2026 18:25:02 +0000 Subject: [PATCH 7/9] plan: peer review of implementation outline for af-7fc78161 Independent rootcause-review (Mode A) appended a '# Peer Review' section: all code anchors re-verified; outline correctly corrects design-doc's install.go:176 mis-citation. Found 3 actionable defects (E-1 Phase-1 AC references an integration-tagged test invisible to make test; E-2 wrong path for formula_drift_test.go; G-1 K8 discovery_failed state not derivable from disk). Verdict: PARTIALLY VALIDATED, proceed to human review with corrections. Co-Authored-By: Claude Opus 4.8 Co-authored-by: agentfactory-cli <293373236+agentfactory-cli@users.noreply.github.com> --- .../implementation_plan_outline.md | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/.designs/73/implementation-plan/implementation_plan_outline.md b/.designs/73/implementation-plan/implementation_plan_outline.md index 52632bc..fa552ce 100644 --- a/.designs/73/implementation-plan/implementation_plan_outline.md +++ b/.designs/73/implementation-plan/implementation_plan_outline.md @@ -375,3 +375,239 @@ make test scope (expands the dispatch blast radius); the only safe fix is documentation. - **Net-new scope is a policy, not a bug** (ADR-017): do not add an auto-migration of existing empty `dispatch.json` installs; name the opt-in path instead. + +--- + +# Peer Review + +**Review Date**: 2026-06-28 +**Reviewer**: Claude Code (rootcause-review skill, adapted for an implementation plan outline) +**Original Outline Date**: 2026-06-28 +**Method**: Independent verification in a fresh session (reached via `af done && af handoff`, so +the reviewer did not author the outline). Every code reference was re-checked against the actual +worktree by four parallel read-only sub-agents (one per file cluster: `internal/config`, +`internal/cmd/install.go`+`detect_default_branch.go`+templates, `up.go`+cmd `dispatch.go`+`config_set.go`, +tests+CI+docs) plus direct reviewer greps. Files read at HEAD of `af/rapid-soldesign-plan-7605ca`. + +## Review Summary + +**Overall Verdict**: **PARTIALLY VALIDATED** (sound plan; three concrete, actionable defects) +**Confidence Level**: **High** (all anchors independently re-read against current code) + +The outline is high-quality and reference-dense. Its component→phase mapping is complete (K1–K9 +all placed; AC-5 correctly scoped out), its dependency ordering is correct and honestly labelled +(compile-independent P1/P2 sequenced only for acceptance), its acceptance criteria are mechanical +bash (not prose), and it **correctly corrects the design-doc** on the `install.go:176` mis-citation. +However, three defects must be addressed before phase extraction — one of them (the `//go:build +integration` tag on the existing test) makes a Phase-1 acceptance check a **false green** and +weakens the "or CI reddens" safety claim. + +## Architecture Elevation Pre-Check + +The mandatory elevation gate is **satisfied upstream**. `design-doc.md` L63–86 carries an +"Architecture Elevation Verdict" = **Frame correct — one Frame-lift OFFERED** (repo +self-derivation), and that lift was **adopted** as component K2 (design-doc Decisions table +L260; outline Phase 1 K2). Per the skill's elevation table, "Frame-lift offered → confirm the +analysis captured the lift": confirmed (K2 is a first-class phase-1 component, with `Repos` +kept editable for the multi-repo edge). No frame defect; proceeding to claim validation. + +## Claim-by-Claim Validation (code references) + +### Cluster A — `internal/config` (config.go, dispatch.go, startup.go) +**Status**: **VALIDATED** (1 trivial off-by-one) +- `DefaultFactoryConfigJSON()` struct→`json.Marshal`→string pattern — config.go:111–123 ✓ (the K1/K5 model). +- `DispatchConfig`/`DispatchMapping`/`Workflow` structs + JSON tags — dispatch.go:17–27 / 29–35 / 42–45 ✓. +- `validateDispatchConfig` fills `NotifyOnComplete`→"manager" — dispatch.go:181–183 ✓; `const defaultNotifyAgent="manager"` at :15 ✓. +- `ValidateDispatchConfig` (cross-file) signature :93, unknown-agent error :100–103, import-cycle comment :130–136 ✓. +- `phaseResolvesAlone` helper — dispatch.go:256 ✓. +- `StartDispatch bool json:"start_dispatch"` — startup.go:18 ✓. +- **Off-by-one (cosmetic):** `DefaultGitIdentity()` is config.go:**88–90**, not 88–91. + +### Cluster B — `internal/cmd/install.go`, `detect_default_branch.go`, templates +**Status**: **VALIDATED** (outline corrects the design-doc here) +- `runInstallInit` :97; starter-config map :139–148; write-if-absent guard :150–157 ✓. +- agents.json literal (manager+supervisor ONLY) — install.go:**143** ✓. +- dispatch.json empty literal (`repos:[]`,`mappings:[]`) — install.go:**145**, and it is the **only** such literal ✓. +- **Disputed `:176` resolved in the outline's favor:** install.go:176 is `store, err := mcpstore.New(cwd, "")`, **not** a dispatch literal. The outline's D-a correction is right; the design-doc was wrong. +- `detect_default_branch.go` is a valid K2/K3 template: `detectBranchTimeout=5*time.Second` (:14), `branchNameAllowlist` regex (:21), `runGitDetect` seam (:34–44), `gh repo view --json` (:67) with git fallback ✓. (Outline's "L14, 21" ↔ "5s timeout, allowlist regex" mapping is accurate.) +- Four role templates exist and are embedded via `//go:embed roles/*.md.tmpl` (templates.go:10) ✓. + +### Cluster C — `internal/cmd/up.go`, cmd `dispatch.go`, `config_set.go` +**Status**: **VALIDATED** (K9 hoist safety confirmed) +- `startupCfg` read up.go:82; `blanket:=len(args)==0` :92 ✓. +- `if startupCfg.StartDispatch { startDispatch(...) }` nested **inside** `if blanket {` (gate :306, call :330–335) — positional `af up ` does NOT reach it today ✓. **Single** `startDispatch` call site in up.go → hoist is safe ✓. +- Dispatch-loop hard-fail `if err := ValidateDispatchConfig(...); err != nil { return err }` — cmd dispatch.go:146–148 ✓. +- `startDispatch` idempotent: already-running no-op :1322–1325; unconfigured skip on `ErrNotFound`/`ErrMissingField` :1328–1331 ✓. +- `dispatchStatusJSON{ DispatcherRunning; Entries }` :1458–1461; `runDispatchStatus` :1356 ✓ (K8 extends additively). +- Write path strict `ValidateDispatchConfig` — config_set.go:89–90 ✓ (K6 must not touch). + +### Cluster D — tests, CI, docs +**Status**: **PARTIALLY VALIDATED** (two defects — see Errors Found) +- `dispatch_workflow_test.go:212–257` cross-file validation tests exist (good K7 model; pair of explicit cases, not a slice table, but cross-file) ✓. +- `.github/workflows/test.yml`: unit job runs `make test` (:68), integration runs `make test-integration` (:124), regen runs `make check-regen` (:140); **no job exercises `af install --init` dispatch validity** ✓ (confirms the CI-gap rationale for landing K7 in the unit tier). +- `USING_AGENTFACTORY.md`: documents label-triggering by example; **two-label requirement NOT explicitly stated**, **net-new scope NOT stated** ✓ (both docs-1/docs-2 gaps are real); `af install --agents` recursion into quickstart confirmed at ~:634 ✓. +- ✗ **`formula_drift_test.go` path is wrong** (Error 2). +- ✗ **`install_integration_test.go` is `//go:build integration`** (Error 1). + +### Two-label requirement (Phase 3 substrate) — **VALIDATED by reviewer grep** +`triggerLabel` is passed as `--label` to `gh issue list` (cmd dispatch.go:301) and `gh pr list` +(:320) — a hard query pre-filter. Confirmed by the in-code comment at dispatch.go:1080 +("the item drops out of the trigger-label query because agentic is gone") and the W-8 note at +:387 ("callers pass ONLY the configured trigger_label and workflows[].phases labels"). The +Gap-2 two-label documentation requirement is well-founded. + +## Code Reference Verification (summary table) + +| Reference (outline) | Claimed | Actual | Status | +|---------------------|---------|--------|--------| +| `install.go:145` | only dispatch.json literal | only dispatch.json literal | ✅ VALIDATED | +| `install.go:176` | `mcpstore.New` (not a literal) | `mcpstore.New(cwd, "")` | ✅ VALIDATED (corrects design-doc) | +| `install.go:143` | agents.json mgr+supervisor only | confirmed | ✅ VALIDATED | +| `up.go:306,330` | StartDispatch gated in `blanket` | confirmed; single call site | ✅ VALIDATED | +| cmd `dispatch.go:146–148` | loop hard-fail | confirmed | ✅ VALIDATED | +| cmd `dispatch.go:1322–1325` | idempotent no-op | confirmed | ✅ VALIDATED | +| `config_set.go:89–90` | strict write path | confirmed | ✅ VALIDATED | +| `config.go:88–91` | `DefaultGitIdentity()` | actually :88–90 | ⚠️ off-by-one (cosmetic) | +| `internal/config/formula_drift_test.go` | drift model file | lives in `internal/cmd/` | ❌ INVALIDATED (wrong path) | +| `install_integration_test.go:66–72` | "or CI reddens" via the ACs | `//go:build integration`; not run by `make test` | ❌ PARTIALLY INVALIDATED (Error 1) | + +## Dependency / Logic Chain Review + +| Link | Assessment | Status | +|------|------------|--------| +| P1 foundational (depends on nothing new) | K1–K5 touch only config.go + install.go | ✅ sound | +| P1 ⟂ P2 (no compile dependency) | K9/K6/K8 touch up.go + dispatch.go/dispatchStatusJSON; no symbol overlap with P1 | ✅ verified | +| P2 acceptance depends on P1 (semantic) | `af up manager` AC needs a valid default → sequencing justified, not over-claimed | ✅ honest | +| P3 depends on P1 ∧ P2 | K7 pins K1/K5 output + cross-validates; exercises K6/K9 behaviors | ✅ sound | +| Intra-P1 topo K3→K1→K2→K4→K5 | leaf validators first, K4 wires, K5 same write | ✅ acyclic, correct | + +The phase numbering follows dependency order (not merely the design narrative), satisfying the +formula's own anti-pattern guard. Each phase is self-contained (objective, prereqs, refs, +current-state, changes, bash ACs, gotchas). + +## Environment Verification + +**Environment**: local worktree `wt-7605ca` (no live GitHub/gh required for the static checks). +**Tests performed**: +1. `grep` for the trigger-label query → confirmed `--label triggerLabel` at cmd dispatch.go:301/320. +2. `head -1 internal/cmd/install_integration_test.go` → `//go:build integration` (confirmed Error 1). +3. `Makefile` targets → `test:` = `go test ./...` (:59, no tag); `test-integration:` = `go test -tags=integration` (:74). Confirms the integration-tagged test is invisible to `make test`. +4. `find . -name formula_drift_test.go` → `./internal/cmd/formula_drift_test.go` (confirmed Error 2). +5. `grep 'func TestInstall'` → multiple un-tagged `TestInstall*` in `install_test.go` (these mask Error 1's false-green; see below). + +## Falsification Attempts + +- **"A design-doc component was dropped."** Tried to find an unmapped K#. Result: refuted — K1–K9 are all placed (P1: K1–K5; P2: K6/K8/K9; P3: K7), AC-5 explicitly and correctly scoped out (formula layer). +- **"The compile-independence claim is false."** Tried to find a shared symbol forcing P1-before-P2 at compile time. Result: refuted — the file/symbol sets are disjoint. +- **"The K9 hoist double-starts the dispatcher."** Tried to find a second `startDispatch` caller. Result: refuted — single call site + idempotent already-running no-op. +- **"AC #6 actually catches the broken existing test."** Tried to confirm `go test ./internal/cmd/ -run 'TestInstall'` exercises the updated assertions. Result: **succeeded in falsifying** — the assertion-bearing test is `//go:build integration`, so without `-tags=integration` it is excluded; the command still prints `ok` because other un-tagged `TestInstall*` match. This is Error 1. + +## Gaps Identified + +1. **G-1 (MED) — K8's four-state `config_state` is not fully derivable at status time.** The outline + (carrying design-doc K8) promises `ok | empty_by_design | discovery_failed | references_unprovisioned_agents`. + `references_unprovisioned_agents` is detectable (ValidateDispatchConfig). But on disk, + `discovery_failed` and `empty_by_design` are **identical** (`repos:[]`) — `af up`/`af dispatch + status` read only the persisted dispatch.json and cannot recover *why* `repos` is empty. The + implementer must either (a) have install persist a breadcrumb (e.g. a one-line marker when K2 + discovery fails) so the status command can distinguish the two, or (b) collapse to a 3-state + taxonomy (`ok | empty | references_unprovisioned_agents`). Phase 2 AC #4 only asserts + `has("config_state")`, so it would pass even if `discovery_failed` is never reachable — the AC + does not protect the promised distinction. Recommend the outline note this and pick (a) or (b). +2. **G-2 (LOW) — K6's mechanism is under-specified vs the call it "relaxes."** The dispatch-loop + call (dispatch.go:146) is a single `ValidateDispatchConfig(...) → error`. "Skip-and-warn the + offending mapping(s) and dispatch the rest" cannot be done by relaxing that boolean; it needs a + per-mapping filter (drop mappings whose `agent` is absent from `agentsCfg`, warn per drop, then + validate/dispatch the remainder). The outline's prose does say "drop the offending mapping(s)", + so the intent is right, but the "Required Changes" wording "replace the hard `return err` with + skip-and-warn" understates that a new helper (filter-to-known-agents) is the actual unit of work. + A one-line clarification would prevent a literal-minded implementer from merely swallowing the error. +3. **G-3 (LOW) — Phase 1 AC #2 depends on `gh`/`git` remote-parsing not yet written.** AC #2 asserts + `repos==["acme/widget"]` from a `git@github.com:acme/widget.git` remote in a throwaway temp repo. + In that environment `gh repo view` will fail (no auth / not a real GH repo) and discovery must + fall through to `git remote get-url origin` normalization. The AC is correct *only if* K2's + fallback handles the `git@host:o/r.git` shape (the gotcha lists it, so this is a consistency note, + not a defect): keep AC #2 and the K2 fallback spec in lockstep. + +## Errors Found + +1. **E-1 (MED→HIGH for fidelity) — `install_integration_test.go` build tag breaks two Phase-1 claims.** + The file is `//go:build integration` (line 1); its `repos:[]`/`mappings:[]` assertions live in + `TestInstallInit_CreatesDispatchJson` (L22, asserts at ~L66–72). Consequences: + - **Phase 1 AC #6** (`go test ./internal/cmd/ -run 'TestInstall' -count=1 # Expected: ok`) is a + **false green**: without `-tags=integration` the updated test does not compile in, yet the + command prints `ok` because other un-tagged `TestInstall*` tests (in `install_test.go`) match + the `-run` filter. The AC must be `go test -tags=integration ./internal/cmd/ -run + 'TestInstallInit_CreatesDispatchJson' -count=1` (or `make test-integration`). + - **Phase 1 AC #7 / the "or CI reddens" gotcha**: `make test` (`go test ./...`, no tag) will + **not** run the integration test, so leaving it un-updated stays green under the unit tier. The + break only reddens the **integration** CI job (test.yml:96–124, `make test-integration`). The + outline should state the tag explicitly and route the "verify the updated test" AC through the + integration tag/tier. (K7 itself is correctly placed in the un-tagged unit tier and is unaffected.) +2. **E-2 (LOW) — wrong package path for the drift-test model.** Phase 3 "Current State" cites + `internal/config/formula_drift_test.go`; the file is at **`internal/cmd/formula_drift_test.go`**. + An implementer would not find the model where pointed. Fix the path (and note it is the ADR-008 + source-vs-installed drift pattern, `TestFormulaDriftSourceVsInstalled`). + +## Solution / Plan Assessment + +**Plan adequacy**: **Adequate** (sound, systemic) — pending the E-1/E-2 corrections and the G-1 note. +**Systemic**: **Yes.** Single-source `DefaultDispatchConfigJSON`/`DefaultAgentsConfigJSON` (K1/K5) +removes the last inline-literal drift surface; the default is valid-by-construction on **every** init +path via one `runInstallInit` write; K7 is a golden+cross-file **mechanical** drift gate in the unit +tier. No hardcoded one-off; new label/agent renames are caught by K7, not by manual vigilance. +**Completeness**: addresses bare-init AND quickstart parity in one write; the only residuals are +correctly scoped out (AC-5 formula layer; installed-base via opt-in; exotic remotes fail-loud). +**Risk**: low. The largest residual is E-1 — a fidelity gap in *how the plan verifies itself*, not in +the fix's architecture. + +### Enforcement Analysis +1. **Can the original failure (empty/invalid default, broken zero-touch) still occur after this plan?** + NO on the happy path — the default is valid-by-construction and K7 fails CI on drift; the documented + `af up manager` auto-starts via K9. Residual: an install where K2 discovery fails ships `repos:[]` + (fail-loud, by design), and G-1's status nuance. +2. **Enforcement type:** + - [x] Mechanical interlock (K1/K5 struct marshaling = compile-checked field shape; K7 golden+cross-file CI test) + - [x] Runtime guard (K3 regex at the write boundary; K6 skip-and-warn; K8 pre-flight) + - [ ] Instruction/configuration + - [ ] Advisory only +3. **Enforcement Score: 8/10.** Strong, code-level. Docked 2: (a) E-1 means the bare-init parity + assertion partly rides on an integration-tagged test that `make test` doesn't run (K7 in the unit + tier mitigates but does not fully replace the install-level integration assertion); (b) G-1's + `discovery_failed` state is not mechanically derivable as promised. +4. **What a tighter interlock looks like:** route the updated bare-init assertion explicitly through + `-tags=integration` in Phase 1's AC (so the parity check is actually executed), and make K7's + unit-tier cross-file test the authoritative drift gate (already specified) so the safety net does + not depend on the integration tier alone. + +Score ≥ 7 → the enforcement gate does not force "Needs revision"; the required changes are the three +documentation/AC corrections below, not an architectural redesign. + +## Final Verdict + +**Plan soundness**: **VALIDATED** (component coverage, dependency order, systemic fix all confirmed) +**Code-reference accuracy**: **PARTIALLY VALIDATED** (1 wrong path, 1 build-tag-blind AC, 1 cosmetic off-by-one; all other anchors exact) +**Recommendation**: **Proceed to human review — with three corrections applied first.** This is the +end of Mode A; the human reviewer should require, before any `design-phase-impl` (Mode B) extraction: +- **(must) E-1**: rewrite Phase 1 AC #6 to `go test -tags=integration ./internal/cmd/ -run + 'TestInstallInit_CreatesDispatchJson'` and add an explicit note that the existing test is + `//go:build integration` (breaks only under `make test-integration`, not `make test`). +- **(must) E-2**: correct the drift-model path to `internal/cmd/formula_drift_test.go`. +- **(should) G-1**: note in Phase 2 that `discovery_failed` vs `empty_by_design` needs an install-time + breadcrumb or a 3-state taxonomy; tighten AC #4 beyond `has("config_state")`. +- **(nice) G-2/G-3 + cosmetic off-by-one**: clarify K6 needs a filter helper; keep AC #2 and the K2 + fallback in lockstep; fix `DefaultGitIdentity()` to :88–90. + +None of these change the architecture or the phase structure; they harden the plan's self-verification. + +## Reviewer Notes + +- The outline's single best move is correcting the design-doc's `install.go:176` mis-citation and + the unflagged existing-test breakage — exactly the "design describes target, not current state" + discipline the formula demands. Both are firsthand-confirmed correct. +- The companion-doc staleness warning (outline L73–79: `dependencies.md`/`integration.md`/`security.md` + describe the superseded `af install --agents` K5 mechanism) is accurate and valuable — `af install + --agents` does recurse into quickstart (USING_AGENTFACTORY.md ~:634) and refuses from a worktree, so + the in-`runInstallInit` seed is the right mechanism. +- E-1 is the one finding I would not let through silently: an acceptance criterion that prints `ok` + without running the test it names is worse than no criterion, because it manufactures false confidence. From 7bfad3195e6b14694ea3be99cb16516cb66b76fe Mon Sep 17 00:00:00 2001 From: agentfactory Date: Sun, 28 Jun 2026 18:28:25 +0000 Subject: [PATCH 8/9] =?UTF-8?q?rapid-soldesign-plan:=20finalize=20#73=20?= =?UTF-8?q?=E2=80=94=20implementation=20plan=20complete,=20formula=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: agentfactory-cli <293373236+agentfactory-cli@users.noreply.github.com> --- .designs/73/design-refinement-progress.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.designs/73/design-refinement-progress.md b/.designs/73/design-refinement-progress.md index 59d0bee..e2e5d5d 100644 --- a/.designs/73/design-refinement-progress.md +++ b/.designs/73/design-refinement-progress.md @@ -1,16 +1,17 @@ # Rapid Design Refinement Progress: New dispatch workflow default to be included with agentfactory -**Status**: In Progress +**Status**: COMPLETE **Issue**: https://github.com/stempeck/agentfactory/issues/73 **PR**: https://github.com/stempeck/agentfactory/pull/74 **Started**: 2026-06-28T16:35:12Z +**Completed**: 2026-06-28T18:35:00Z ## Agents | Role | Agent | Status | Started | Completed | |------|---------|--------|---------|-----------| | Analyst | rootcause-all | Released (review complete, af down) | 2026-06-28T16:44Z | 2026-06-28T17:50Z | | Designer | design-v7 | Released (design committed, af down) | 2026-06-28T16:44Z | 2026-06-28T17:50Z | -| Impl | design-plan-impl | Not dispatched | - | - | +| Impl | design-plan-impl | Complete (WORK_DONE — 16 steps) | 2026-06-28T17:55Z | 2026-06-28T18:30Z | **Beads:** analyst=af-8fe67d60, designer=af-cf3cee4b, gate=af-bec549ca (gate blocks analyst bead close → premature-af-done detection) @@ -24,5 +25,5 @@ | Stage | Status | Timestamp | |-------|--------|-----------| | PR opened | Complete (https://github.com/stempeck/agentfactory/pull/74) | 2026-06-28T17:48Z | -| implementation-plan agent dispatched | Pending | - | -| implementation_plan_outline.md created | Pending | - | +| implementation-plan agent dispatched | Complete (design-plan-impl, WORK_DONE) | 2026-06-28T18:30Z | +| implementation_plan_outline.md created | Complete (.designs/73/implementation-plan/, 613 lines, 3 phases) | 2026-06-28T18:30Z | From b2b3305628894d6cacbee16f13480b61a2812907 Mon Sep 17 00:00:00 2001 From: agentfactory Date: Sun, 28 Jun 2026 19:20:38 +0000 Subject: [PATCH 9/9] feat(install): baked-in default dispatch.json + seeded specialists (#73) A fresh `af install --init` now produces a valid-by-construction, ready-to-use dispatch configuration so a bootstrapped factory can drive label-triggered work with no hand-editing (issue #73). - K1 config.DefaultDispatchConfigJSON(repo): single-source dispatch.json default built from the DispatchConfig struct (4 label->agent mappings + feature-workflow + trigger_label "agentic"); empty repo degrades to the loadable empty shape. notify_on_complete is omitted (defaults to "manager" at runtime; +omitempty tag). - K2/K3 discoverRepo: non-interactive owner/name discovery (gh repo view -> git remote origin fallback) via the runGitDetect seam, validated by a strict owner/name allowlist before it reaches disk/banner/gh (flag-injection guard). - K4 wire runInstallInit to the builders; additive escape-safe banner line. - K5 config.DefaultAgentsConfigJSON(): seeds the four specialists (rapid-soldesign-plan, rapid-implement, ultra-review, rapid-increment) into the default agents.json so every dispatch mapping resolves (cross-review C1). Each carries a description (validateAgentConfig requires it) + its formula. - K6 dispatch loop tolerates a partial factory: skip-and-warn on unprovisioned mapping agents, dispatch the rest; the write path (af config dispatch set) stays strict. - K8 `af dispatch status --json` and the `af up` pre-flight surface a config_state (ok / not_configured / references_unprovisioned_agents / invalid). - K9 hoist dispatcher auto-start out of the blanket-only gate so the documented positional `af up manager` also starts the polling loop (idempotent; cross-review C2). - K7 golden + cross-file tests pin the default's validity against the seeded agents; updated the scaffold + status-snapshot + install-integration tests; docs note the two-label requirement and net-new-install scope. Co-Authored-By: Claude Opus 4.8 Co-authored-by: agentfactory-cli <293373236+agentfactory-cli@users.noreply.github.com> --- USING_AGENTFACTORY.md | 15 +- internal/cmd/discover_repo.go | 69 ++++++++ internal/cmd/discover_repo_test.go | 112 +++++++++++++ internal/cmd/dispatch.go | 100 +++++++++++- internal/cmd/dispatch_test.go | 4 +- internal/cmd/dispatch_tolerate_test.go | 120 ++++++++++++++ internal/cmd/install.go | 21 ++- internal/cmd/install_integration_test.go | 111 ++++++++++++- internal/cmd/install_scaffold_test.go | 8 +- internal/cmd/up.go | 32 ++-- internal/cmd/up_dispatch_positional_test.go | 72 +++++++++ internal/config/config.go | 26 ++++ internal/config/default_dispatch_test.go | 164 ++++++++++++++++++++ internal/config/dispatch.go | 44 +++++- 14 files changed, 868 insertions(+), 30 deletions(-) create mode 100644 internal/cmd/discover_repo.go create mode 100644 internal/cmd/discover_repo_test.go create mode 100644 internal/cmd/dispatch_tolerate_test.go create mode 100644 internal/cmd/up_dispatch_positional_test.go create mode 100644 internal/config/default_dispatch_test.go diff --git a/USING_AGENTFACTORY.md b/USING_AGENTFACTORY.md index 0f4dc07..d6a78b9 100644 --- a/USING_AGENTFACTORY.md +++ b/USING_AGENTFACTORY.md @@ -142,7 +142,20 @@ af dispatch status # Show dispatch state and agent availabilit af dispatch --dry-run # Show what would be dispatched without acting ``` -Configuration lives in `.agentfactory/dispatch.json` (created by `af install --init`). Edit it to add repos, trigger label, and label-to-agent mappings before starting the dispatcher. +Configuration lives in `.agentfactory/dispatch.json`. On a **net-new** `af install --init`, +agentfactory bakes in a ready-to-use default: it auto-discovers the repo's `owner/name` +(via `gh` / `git remote origin`) into `repos`, ships the four label→agent mappings and the +`feature-workflow` shown below, and seeds the referenced specialist agents into `agents.json` +— so a freshly bootstrapped factory can drive label-triggered work with no hand-editing. +(If the repo can't be discovered, `repos` ships empty and the dispatcher friendly-skips until +you set it.) **Existing factories are not auto-migrated** — a customer-edited `dispatch.json` +is never clobbered; opt in by replacing it via `af config dispatch set` (pipe a JSON document +on stdin). Edit `dispatch.json` any time to change repos, labels, or mappings. + +> **Two-label requirement.** `trigger_label` (default `agentic`) is a hard query pre-filter: +> the dispatcher only fetches items that carry it. Tagging an issue with *only* a mapping or +> workflow label dispatches nothing — an item must have **both** `agentic` **and** the +> mapping/workflow label (e.g. `agentic` + `rapid-plan`) to be picked up. ```json { diff --git a/internal/cmd/discover_repo.go b/internal/cmd/discover_repo.go new file mode 100644 index 0000000..f02bc27 --- /dev/null +++ b/internal/cmd/discover_repo.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "regexp" + "strings" +) + +// repoSlugAllowlist constrains an accepted owner/name slug to a conservative set: +// letters, digits, dot, underscore, hyphen in each of exactly two slash-separated +// segments. The discovered repo is written into dispatch.json, echoed in the install +// banner, and later passed to `gh --repo `, so every candidate MUST pass this +// allowlist — it blocks shell-meta / terminal-escape and (with the leading-'-' guard +// in isValidRepoSlug) gh flag-injection. Mirrors branchNameAllowlist +// (detect_default_branch.go, security.md SEC-1). +var repoSlugAllowlist = regexp.MustCompile(`^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$`) + +// isValidRepoSlug reports whether s is a safe owner/name to store and shell out with: +// non-empty, not flag-like (no leading '-'), exactly owner/name, allowlist charset. +func isValidRepoSlug(s string) bool { + return s != "" && !strings.HasPrefix(s, "-") && repoSlugAllowlist.MatchString(s) +} + +// repoSlugFromRemote normalizes a `git remote get-url origin` value — in any of the +// common shapes (scp-like git@host:owner/repo.git, https://host/owner/repo[.git], +// ssh://host/owner/repo.git) — to the canonical owner/name slug. The result is NOT +// validated here; the caller (discoverRepo) runs isValidRepoSlug. Returns "" when it +// cannot extract two trailing path segments. +func repoSlugFromRemote(raw string) string { + s := strings.TrimSpace(raw) + if s == "" { + return "" + } + s = strings.TrimSuffix(s, ".git") + if i := strings.Index(s, "://"); i >= 0 { + // scheme://host/owner/repo → strip "scheme://host". + rest := s[i+3:] + j := strings.IndexByte(rest, '/') + if j < 0 { + return "" + } + s = rest[j+1:] + } else if i := strings.LastIndex(s, ":"); i >= 0 { + // scp-like user@host:owner/repo → keep the part after ':'. + s = s[i+1:] + } + s = strings.Trim(s, "/") + parts := strings.Split(s, "/") + if len(parts) < 2 { + return "" + } + return parts[len(parts)-2] + "/" + parts[len(parts)-1] +} + +// discoverRepo resolves the home repository's owner/name non-interactively at install +// time (issue #73 K2 — the architecture-elevation Frame-lift). It prefers `gh repo view` +// (canonical nameWithOwner, no URL parsing) and falls back to `git remote get-url origin` +// normalization (works without gh auth). Read-only and timeout-bounded via the +// runGitDetect seam (ADR-014 non-interactive, ADR-017 read-only). Returns the VALIDATED +// slug, or "" when discovery fails or yields an unsafe value — the caller degrades to +// empty repos and warns; install never aborts (A3.1 warn-don't-abort). +func discoverRepo(workDir string) string { + if slug := runGitDetect(workDir, "gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"); isValidRepoSlug(slug) { + return slug + } + if slug := repoSlugFromRemote(runGitDetect(workDir, "git", "remote", "get-url", "origin")); isValidRepoSlug(slug) { + return slug + } + return "" +} diff --git a/internal/cmd/discover_repo_test.go b/internal/cmd/discover_repo_test.go new file mode 100644 index 0000000..787adfc --- /dev/null +++ b/internal/cmd/discover_repo_test.go @@ -0,0 +1,112 @@ +package cmd + +import "testing" + +// K3: the owner/name validator accepts canonical slugs and rejects the empty, +// slashless, flag-like, multi-segment, and shell-meta forms — guarding gh +// flag-injection and terminal-escape before the value reaches disk/banner/gh. +func TestIsValidRepoSlug(t *testing.T) { + valid := []string{"acme/widget", "org-name/repo.name_1", "a/b", "A1/B2"} + for _, s := range valid { + if !isValidRepoSlug(s) { + t.Errorf("isValidRepoSlug(%q) = false, want true", s) + } + } + invalid := []string{ + "", // empty + "noslash", // no owner/name separator + "-evil/x", // leading dash → gh flag-injection + "a/b/c", // too many segments + "acme/", // empty repo + "/widget", // empty owner + "acme widget/x", // space + "acme/wid get", // space in repo + "https://x/y", // scheme chars (':') + "$(touch pwned)/x", // shell meta + "acme/$(touch pwned)", // shell meta in repo + "a/b\n", // newline + } + for _, s := range invalid { + if isValidRepoSlug(s) { + t.Errorf("isValidRepoSlug(%q) = true, want false", s) + } + } +} + +// K2: git remote URLs in all three shapes (scp-like, https, https-no-.git) plus +// ssh:// normalize to the canonical owner/name slug. +func TestRepoSlugFromRemote(t *testing.T) { + cases := map[string]string{ + "git@github.com:acme/widget.git": "acme/widget", + "https://github.com/acme/widget.git": "acme/widget", + "https://github.com/acme/widget": "acme/widget", + "ssh://git@github.com/acme/widget.git": "acme/widget", + "git@github.com:acme/widget.git\n": "acme/widget", + } + for raw, want := range cases { + if got := repoSlugFromRemote(raw); got != want { + t.Errorf("repoSlugFromRemote(%q) = %q, want %q", raw, got, want) + } + } +} + +// K2: discovery prefers `gh repo view` and returns the validated slug. +func TestDiscoverRepo_GHPrimary(t *testing.T) { + orig := runGitDetect + runGitDetect = func(workDir, name string, args ...string) string { + if name == "gh" { + return "acme/widget" + } + return "" + } + t.Cleanup(func() { runGitDetect = orig }) + + if got := discoverRepo("/x"); got != "acme/widget" { + t.Errorf("discoverRepo = %q, want acme/widget", got) + } +} + +// K2: when gh fails (no auth), discovery falls back to `git remote get-url origin` +// and normalizes it. +func TestDiscoverRepo_GitFallback(t *testing.T) { + orig := runGitDetect + runGitDetect = func(workDir, name string, args ...string) string { + if name == "git" { + return "git@github.com:acme/widget.git" + } + return "" // gh returns nothing + } + t.Cleanup(func() { runGitDetect = orig }) + + if got := discoverRepo("/x"); got != "acme/widget" { + t.Errorf("discoverRepo = %q, want acme/widget (git fallback)", got) + } +} + +// K2+K3: a crafted/garbage remote must be rejected by the validator → empty result +// (warn-don't-abort happens in the caller), never a malformed write. +func TestDiscoverRepo_CraftedRejected(t *testing.T) { + orig := runGitDetect + runGitDetect = func(workDir, name string, args ...string) string { + if name == "git" { + return "https://x/--evil/$(touch pwned)" + } + return "" + } + t.Cleanup(func() { runGitDetect = orig }) + + if got := discoverRepo("/x"); got != "" { + t.Errorf("discoverRepo on a crafted remote = %q, want \"\" (rejected)", got) + } +} + +// K2: no remote at all (both methods fail) → empty result, no panic. +func TestDiscoverRepo_NoRemote(t *testing.T) { + orig := runGitDetect + runGitDetect = func(workDir, name string, args ...string) string { return "" } + t.Cleanup(func() { runGitDetect = orig }) + + if got := discoverRepo("/x"); got != "" { + t.Errorf("discoverRepo with no remote = %q, want \"\"", got) + } +} diff --git a/internal/cmd/dispatch.go b/internal/cmd/dispatch.go index e032e7f..d525526 100644 --- a/internal/cmd/dispatch.go +++ b/internal/cmd/dispatch.go @@ -120,6 +120,72 @@ type dispatchEntry struct { Attempts int `json:"attempts,omitempty"` } +// partitionDispatchMappings splits a dispatch config's mappings into those whose agent +// is provisioned in agents.json (known, kept in order) and the distinct agent names +// referenced by mappings that are NOT (unknownAgents, in first-seen order). It backs the +// K6 dispatch-loop tolerance (skip-and-warn) WITHOUT relaxing the shared write-path +// validator (config.ValidateDispatchConfig), which the CLI/web config-set paths still use. +func partitionDispatchMappings(disp *config.DispatchConfig, agents *config.AgentConfig) (known []config.DispatchMapping, unknownAgents []string) { + seen := make(map[string]bool) + for _, m := range disp.Mappings { + if _, ok := agents.Agents[m.Agent]; ok { + known = append(known, m) + continue + } + if !seen[m.Agent] { + seen[m.Agent] = true + unknownAgents = append(unknownAgents, m.Agent) + } + } + return known, unknownAgents +} + +// Dispatch config-validity states surfaced by `af dispatch status --json` and the `af up` +// pre-flight (issue #73 K8). They make a fresh/partial factory's dispatch readiness +// observable so a degraded loop (K6) is never silent (cross-review H2). +const ( + dispatchStateOK = "ok" + dispatchStateNotConfigured = "not_configured" + dispatchStateUnprovisioned = "references_unprovisioned_agents" + dispatchStateInvalid = "invalid" +) + +// dispatchConfigState classifies a dispatch config's readiness from its load result and a +// cross-file check against agents.json. "not_configured" covers the empty install default +// and an absent file (the dispatcher friendly-skips both); "references_unprovisioned_agents" +// is the K6 degraded path the loop tolerates; "invalid" is any other load/validation error. +func dispatchConfigState(disp *config.DispatchConfig, agents *config.AgentConfig, loadErr error) string { + if loadErr != nil { + if errors.Is(loadErr, config.ErrNotFound) || errors.Is(loadErr, config.ErrMissingField) { + return dispatchStateNotConfigured + } + return dispatchStateInvalid + } + if _, unknown := partitionDispatchMappings(disp, agents); len(unknown) > 0 { + return dispatchStateUnprovisioned + } + if err := config.ValidateDispatchConfig(disp, agents); err != nil { + return dispatchStateInvalid + } + return dispatchStateOK +} + +// loadDispatchConfigState loads the dispatch + agents configs at root and classifies the +// dispatch config-validity state for observability (K8). A missing/invalid agents.json is +// "invalid" — the dispatcher cannot cross-validate without it. Read-only and advisory; it +// never mutates state and never aborts. +func loadDispatchConfigState(root string) string { + disp, loadErr := config.LoadDispatchConfig(root) + if loadErr != nil { + return dispatchConfigState(nil, nil, loadErr) + } + agents, err := config.LoadAgentConfig(config.AgentsConfigPath(root)) + if err != nil { + return dispatchStateInvalid + } + return dispatchConfigState(disp, agents, nil) +} + func runDispatch(cmd *cobra.Command, args []string) error { wd, err := getWd() if err != nil { @@ -140,11 +206,27 @@ func runDispatch(cmd *cobra.Command, args []string) error { return fmt.Errorf("loading agent config: %w", err) } - // Cross-validate via the shared single source of truth: every mapping agent and an - // explicitly set notify_on_complete must exist in agents.json. config.ValidateDispatchConfig - // is the same validator the CLI/web config-write paths use, so the rule cannot drift. + // K6 (issue #73): the dispatch LOOP tolerates a partial/edited factory — drop + // mappings whose agent is not provisioned (skip-and-warn) and dispatch the rest, + // instead of hard-failing the whole cycle. The write path (af config dispatch set) + // stays strict. K8 surfaces the degraded state at `af up` / `af dispatch status` so a + // "running-but-dispatching-nothing" loop is never silent (cross-review H2). + knownMappings, unknownAgents := partitionDispatchMappings(dispatchCfg, agentsCfg) + for _, agent := range unknownAgents { + fmt.Fprintf(cmd.ErrOrStderr(), "warning: skipping dispatch mappings for unprovisioned agent %q (not in agents.json)\n", agent) + } + dispatchCfg.Mappings = knownMappings + if len(dispatchCfg.Mappings) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "skipping dispatch: no mapping references a provisioned agent") + return nil + } + // The remaining cross-file rules (an explicitly set notify_on_complete, workflow phase + // formulas) stay authoritative; config.ValidateDispatchConfig is still the shared single + // source of truth. Only this loop caller relaxes hard-fail to skip-and-warn, so a + // residual error degrades the cycle rather than aborting the polling loop. if err := config.ValidateDispatchConfig(dispatchCfg, agentsCfg); err != nil { - return err + fmt.Fprintf(cmd.ErrOrStderr(), "warning: skipping dispatch: %v\n", err) + return nil } // Check gh auth @@ -1396,7 +1478,11 @@ func runDispatchStatus(cmd *cobra.Command, args []string) error { phaseComplete := computePhaseCompletion(cmd.Context(), root, state.Dispatched) if jsonOut { - return emitDispatchStatusJSON(cmd, running, state.Dispatched, agentState, phaseComplete) + // K8: surface the dispatch config-validity state so a fresh/partial factory's + // readiness ("not configured" vs "references unprovisioned agents" vs "ok") is + // observable to the web reader and operators (read-only, never aborts). + configState := loadDispatchConfigState(root) + return emitDispatchStatusJSON(cmd, running, configState, state.Dispatched, agentState, phaseComplete) } out := formatDispatchStatus(running, state.Dispatched, agentState, phaseComplete) @@ -1457,6 +1543,7 @@ type dispatchStatusEntry struct { // `af dispatch status --json`. type dispatchStatusJSON struct { DispatcherRunning bool `json:"dispatcher_running"` + ConfigState string `json:"config_state"` Entries []dispatchStatusEntry `json:"entries"` } @@ -1476,7 +1563,7 @@ func emitDispatchStatusError(cmd *cobra.Command, e error) error { // emitDispatchStatusJSON marshals the dispatcher state as JSON to stdout. Entries // are sorted by issue key for deterministic, snapshot-stable output. -func emitDispatchStatusJSON(cmd *cobra.Command, running bool, entries map[string]dispatchEntry, agentState map[string]bool, phaseComplete map[string]bool) error { +func emitDispatchStatusJSON(cmd *cobra.Command, running bool, configState string, entries map[string]dispatchEntry, agentState map[string]bool, phaseComplete map[string]bool) error { keys := make([]string, 0, len(entries)) for k := range entries { keys = append(keys, k) @@ -1485,6 +1572,7 @@ func emitDispatchStatusJSON(cmd *cobra.Command, running bool, entries map[string out := dispatchStatusJSON{ DispatcherRunning: running, + ConfigState: configState, Entries: make([]dispatchStatusEntry, 0, len(entries)), } for _, k := range keys { diff --git a/internal/cmd/dispatch_test.go b/internal/cmd/dispatch_test.go index e89b5b2..8d25746 100644 --- a/internal/cmd/dispatch_test.go +++ b/internal/cmd/dispatch_test.go @@ -632,7 +632,9 @@ func TestDispatchStatus_JSON_SchemaSnapshot(t *testing.T) { if err := json.Unmarshal([]byte(out), &top); err != nil { t.Fatalf("unmarshal %q: %v", out, err) } - wantTop := map[string]bool{"dispatcher_running": true, "entries": true} + // config_state is the additive K8 observability field (issue #73): the dispatch + // config-validity state ("not_configured" here — only factory.json was written). + wantTop := map[string]bool{"dispatcher_running": true, "config_state": true, "entries": true} if len(top) != len(wantTop) { t.Errorf("top-level key count = %d, want %d (%q)", len(top), len(wantTop), out) } diff --git a/internal/cmd/dispatch_tolerate_test.go b/internal/cmd/dispatch_tolerate_test.go new file mode 100644 index 0000000..258dcfd --- /dev/null +++ b/internal/cmd/dispatch_tolerate_test.go @@ -0,0 +1,120 @@ +package cmd + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/stempeck/agentfactory/internal/config" +) + +// K6: the dispatch loop tolerates a partial factory — mappings whose agent is not in +// agents.json are partitioned out (to be skipped-and-warned), while mappings with a +// provisioned agent are kept and dispatched. Order is preserved. +func TestPartitionDispatchMappings(t *testing.T) { + agents := &config.AgentConfig{Agents: map[string]config.AgentEntry{ + "known": {Type: "autonomous", Description: "k"}, + }} + disp := &config.DispatchConfig{Mappings: []config.DispatchMapping{ + {Labels: []string{"a"}, Agent: "known"}, + {Labels: []string{"b"}, Agent: "ghost"}, + {Labels: []string{"c"}, Agent: "phantom"}, + }} + + known, unknown := partitionDispatchMappings(disp, agents) + if len(known) != 1 || known[0].Agent != "known" { + t.Errorf("known mappings = %v, want one mapping for agent 'known'", known) + } + if len(unknown) != 2 || unknown[0] != "ghost" || unknown[1] != "phantom" { + t.Errorf("unknown agents = %v, want [ghost phantom] in mapping order", unknown) + } +} + +// K6: an all-provisioned config partitions to all-known, no unknowns (the common path +// after K5 seeds the specialists — identical to today's behavior). +func TestPartitionDispatchMappings_AllKnown(t *testing.T) { + agents := &config.AgentConfig{Agents: map[string]config.AgentEntry{ + "a1": {Type: "autonomous", Description: "x"}, + "a2": {Type: "autonomous", Description: "y"}, + }} + disp := &config.DispatchConfig{Mappings: []config.DispatchMapping{ + {Labels: []string{"l1"}, Agent: "a1"}, + {Labels: []string{"l2"}, Agent: "a2"}, + }} + known, unknown := partitionDispatchMappings(disp, agents) + if len(known) != 2 || len(unknown) != 0 { + t.Errorf("all-known config = (%d known, %d unknown), want (2, 0)", len(known), len(unknown)) + } +} + +// K8: the config-state classifier distinguishes the observable states the dispatcher +// can be in, so a "running-but-dispatching-nothing" loop is never silent. +func TestDispatchConfigState(t *testing.T) { + agents := &config.AgentConfig{Agents: map[string]config.AgentEntry{ + "mgr": {Type: "autonomous", Description: "m"}, + }} + okDisp := &config.DispatchConfig{ + Repos: []string{"o/r"}, TriggerLabel: "agentic", + Mappings: []config.DispatchMapping{{Labels: []string{"x"}, Agent: "mgr"}}, + } + unprovisioned := &config.DispatchConfig{ + Repos: []string{"o/r"}, TriggerLabel: "agentic", + Mappings: []config.DispatchMapping{{Labels: []string{"x"}, Agent: "ghost"}}, + } + + cases := []struct { + name string + disp *config.DispatchConfig + loadErr error + want string + }{ + {"not configured (empty default)", nil, config.ErrMissingField, "not_configured"}, + {"not configured (absent)", nil, config.ErrNotFound, "not_configured"}, + {"ok", okDisp, nil, "ok"}, + {"references unprovisioned agents", unprovisioned, nil, "references_unprovisioned_agents"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := dispatchConfigState(tc.disp, agents, tc.loadErr); got != tc.want { + t.Errorf("dispatchConfigState = %q, want %q", got, tc.want) + } + }) + } +} + +// K8: `af dispatch status --json` surfaces the config_state so the fresh-install +// "references unprovisioned agents" condition is observable (cross-review H2). +func TestDispatchStatus_JSON_ConfigState_Unprovisioned(t *testing.T) { + root := t.TempDir() + writeAFFile(t, root, "factory.json", `{"type":"factory","version":1}`) + writeAFFile(t, root, "agents.json", `{"agents":{"manager":{"type":"interactive","description":"m"}}}`) + writeAFFile(t, root, "dispatch.json", + `{"repos":["o/r"],"trigger_label":"agentic","mappings":[{"labels":["x"],"agent":"ghost"}],"interval_seconds":300}`) + + setupHermeticSessions(t) + t.Chdir(root) + + cmd := &cobra.Command{} + cmd.Flags().Bool("json", false, "") + _ = cmd.Flags().Set("json", "true") + var buf strings.Builder + cmd.SetOut(&buf) + if err := runDispatchStatus(cmd, nil); err != nil { + t.Fatalf("runDispatchStatus: %v", err) + } + + var top map[string]json.RawMessage + if err := json.Unmarshal([]byte(strings.TrimSpace(buf.String())), &top); err != nil { + t.Fatalf("unmarshal %q: %v", buf.String(), err) + } + raw, ok := top["config_state"] + if !ok { + t.Fatalf("status --json must include config_state; got %q", buf.String()) + } + var state string + _ = json.Unmarshal(raw, &state) + if state != "references_unprovisioned_agents" { + t.Errorf("config_state = %q, want references_unprovisioned_agents", state) + } +} diff --git a/internal/cmd/install.go b/internal/cmd/install.go index 2b6d86c..14ec880 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -135,14 +135,29 @@ func runInstallInit(cmd *cobra.Command) error { return fmt.Errorf("creating agents directory: %w", err) } + // 2c. Discover the home repo (issue #73 K2) non-interactively so the baked-in dispatch + // default ships with the ACTUAL owner/name in `repos` (C-3). Best-effort and validated + // (K3): an undiscoverable/unsafe remote degrades to an empty `repos` default (the + // dispatcher then friendly-skips) — install never aborts (A3.1 warn-don't-abort). + repo := discoverRepo(cwd) + if repo == "" { + fmt.Fprintln(cmd.ErrOrStderr(), "warning: could not discover an owner/name repo from `gh` / `git remote origin`; dispatch.json ships with empty repos — set it with `af config dispatch set` to enable label-triggered dispatch") + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Discovered repository: %s\n", repo) + } + // 3. Write starter configs (only if they don't exist — idempotent) starterConfigs := map[string]string{ // Built from the in-code defaults (incl. the C-3 git_identity) so the on-disk // literal cannot drift from internal/config's constants (issue #371 Gap-6). - "factory.json": config.DefaultFactoryConfigJSON(), - "agents.json": `{"agents":{"manager":{"type":"interactive","description":"Interactive agent for human-supervised work","directive":"Read your memory and docs, and prove it."},"supervisor":{"type":"autonomous","description":"Autonomous agent for independent task execution","directive":"Read your memory and docs, and prove it."}}}`, + "factory.json": config.DefaultFactoryConfigJSON(), + // agents.json + dispatch.json are built from the in-code defaults (single-source, + // issue #73 K5/K1) so the fresh factory is valid-by-construction: the seeded + // specialists satisfy the dispatch default's mappings (cross-review C1), and the + // discovered repo lands in dispatch.json's `repos` (C-3). + "agents.json": config.DefaultAgentsConfigJSON(), "messaging.json": `{"groups":{"all":["manager","supervisor"]}}`, - "dispatch.json": `{"repos":[],"trigger_label":"agentic","notify_on_complete":"manager","mappings":[],"interval_seconds":300,"retry_after_seconds":1800}`, + "dispatch.json": config.DefaultDispatchConfigJSON(repo), // Opinionated defaults for fresh installs (see TestLoadStartupConfig_ScaffoldLoads). "startup.json": `{"agents":["manager"],"quality":"default","fidelity":"default","start_dispatch":true,"watchdog_agents":["manager","supervisor"]}`, } diff --git a/internal/cmd/install_integration_test.go b/internal/cmd/install_integration_test.go index b968a4f..b6798b0 100644 --- a/internal/cmd/install_integration_test.go +++ b/internal/cmd/install_integration_test.go @@ -39,6 +39,13 @@ func TestInstallInit_CreatesDispatchJson(t *testing.T) { } } + // Deterministic discovery (issue #73 K2): this temp repo has no remote, so the + // dispatch default degrades to the empty shape. Stub the seam so the assertion never + // depends on the CI host's gh auth / ambient remote. + origDetect := runGitDetect + runGitDetect = func(workDir, name string, args ...string) string { return "" } + t.Cleanup(func() { runGitDetect = origDetect }) + output, err := runInstallInDir(t, dir, "--init") // Reset flag to avoid affecting subsequent tests (cobra doesn't reset bool flags) installInitFlag = false @@ -64,11 +71,11 @@ func TestInstallInit_CreatesDispatchJson(t *testing.T) { } repos, ok := cfg["repos"].([]interface{}) if !ok || len(repos) != 0 { - t.Errorf("repos should be empty array, got: %v", cfg["repos"]) + t.Errorf("repos should be empty array (no remote discovered), got: %v", cfg["repos"]) } mappings, ok := cfg["mappings"].([]interface{}) if !ok || len(mappings) != 0 { - t.Errorf("mappings should be empty array, got: %v", cfg["mappings"]) + t.Errorf("mappings should be empty array (degraded default), got: %v", cfg["mappings"]) } if interval, ok := cfg["interval_seconds"].(float64); !ok || int(interval) != 300 { t.Errorf("interval_seconds should be 300, got: %v", cfg["interval_seconds"]) @@ -76,8 +83,104 @@ func TestInstallInit_CreatesDispatchJson(t *testing.T) { if retry, ok := cfg["retry_after_seconds"].(float64); !ok || int(retry) != 1800 { t.Errorf("retry_after_seconds should be 1800, got: %v", cfg["retry_after_seconds"]) } - if notify, ok := cfg["notify_on_complete"].(string); !ok || notify != "manager" { - t.Errorf("notify_on_complete should be 'manager', got: %v", cfg["notify_on_complete"]) + // notify_on_complete is now OMITTED from the default (issue #73 Gap-7): it defaults to + // "manager" at runtime, so an explicit value would add a brittle cross-file check. + if _, present := cfg["notify_on_complete"]; present { + t.Errorf("notify_on_complete should be omitted from the default, got: %v", cfg["notify_on_complete"]) + } +} + +// TestInstallInit_PopulatedDispatchAndAgents_WithDiscoveredRepo exercises issue #73's +// happy path end-to-end: with a discoverable repo, `af install --init` bakes the +// owner/name into dispatch.json's repos AND ships the four label->agent mappings + +// feature-workflow, seeds the four specialists into agents.json, and the result is +// valid-by-construction (the default cross-validates against the seeded agents — C1/C-6). +// A re-run does not clobber. Discovery is stubbed (in-process install shares the package +// seam) so the assertion is deterministic regardless of the CI host's auth/remote. +func TestInstallInit_PopulatedDispatchAndAgents_WithDiscoveredRepo(t *testing.T) { + requirePython3WithServerDeps(t) + + dir := t.TempDir() + ensurePySymlink(t, dir) + t.Cleanup(func() { terminateMCPServer(dir) }) + + for _, args := range [][]string{ + {"init", "-q"}, + {"config", "user.email", "test@test.test"}, + {"config", "user.name", "Test"}, + } { + cmd := exec.Command("git", args...) + cmd.Dir = dir + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git %s: %s\n%s", strings.Join(args, " "), err, out) + } + } + + origDetect := runGitDetect + runGitDetect = func(workDir, name string, args ...string) string { + if name == "gh" { + return "acme/widget" + } + return "" + } + t.Cleanup(func() { runGitDetect = origDetect }) + + output, err := runInstallInDir(t, dir, "--init") + installInitFlag = false + if err != nil { + t.Fatalf("install --init failed: %v\nOutput: %s", err, output) + } + + dispatchPath := filepath.Join(dir, ".agentfactory", "dispatch.json") + dispData, err := os.ReadFile(dispatchPath) + if err != nil { + t.Fatalf("read dispatch.json: %v", err) + } + var disp config.DispatchConfig + if err := json.Unmarshal(dispData, &disp); err != nil { + t.Fatalf("dispatch.json invalid: %v", err) + } + if len(disp.Repos) != 1 || disp.Repos[0] != "acme/widget" { + t.Errorf("repos = %v, want [acme/widget]", disp.Repos) + } + if len(disp.Mappings) != 4 { + t.Errorf("mappings = %d, want 4", len(disp.Mappings)) + } + if len(disp.Workflows) != 1 || disp.Workflows[0].Label != "feature-workflow" { + t.Errorf("workflows = %v, want one feature-workflow", disp.Workflows) + } + if strings.Contains(string(dispData), "notify_on_complete") { + t.Errorf("notify_on_complete should be omitted; got: %s", dispData) + } + + agentsData, err := os.ReadFile(filepath.Join(dir, ".agentfactory", "agents.json")) + if err != nil { + t.Fatalf("read agents.json: %v", err) + } + var agents config.AgentConfig + if err := json.Unmarshal(agentsData, &agents); err != nil { + t.Fatalf("agents.json invalid: %v", err) + } + for _, name := range []string{"manager", "supervisor", "rapid-soldesign-plan", "rapid-implement", "ultra-review", "rapid-increment"} { + if _, ok := agents.Agents[name]; !ok { + t.Errorf("agents.json missing seeded agent %q", name) + } + } + // C1/C-6: the shipped default is valid-by-construction against the seeded agents. + if err := config.ValidateDispatchConfig(&disp, &agents); err != nil { + t.Errorf("default dispatch must cross-validate against seeded agents.json: %v", err) + } + + // Idempotent (ADR-017 write-if-absent): a re-run must not clobber the populated files. + before, _ := os.ReadFile(dispatchPath) + if _, err := runInstallInDir(t, dir, "--init"); err != nil { + installInitFlag = false + t.Fatalf("second install --init failed: %v", err) + } + installInitFlag = false + after, _ := os.ReadFile(dispatchPath) + if string(before) != string(after) { + t.Errorf("re-run clobbered dispatch.json:\nbefore: %s\nafter: %s", before, after) } } diff --git a/internal/cmd/install_scaffold_test.go b/internal/cmd/install_scaffold_test.go index 949c462..4ceb7f7 100644 --- a/internal/cmd/install_scaffold_test.go +++ b/internal/cmd/install_scaffold_test.go @@ -54,12 +54,14 @@ func TestInstallScaffold_WatchdogAgentsAreSeededAgents(t *testing.T) { } src := string(data) - agentsLiteral := extractScaffoldLiteral(t, src, `"agents.json":`) + // agents.json is now built by config.DefaultAgentsConfigJSON() (single-source, issue + // #73 K5) rather than an inline literal, so read the REAL default — still drift-proof, + // now also covering the seeded specialists. startup.json stays an inline literal. startupLiteral := extractScaffoldLiteral(t, src, `"startup.json":`) var agents config.AgentConfig - if err := json.Unmarshal([]byte(agentsLiteral), &agents); err != nil { - t.Fatalf("unmarshal scaffold agents.json literal: %v\nliteral: %s", err, agentsLiteral) + if err := json.Unmarshal([]byte(config.DefaultAgentsConfigJSON()), &agents); err != nil { + t.Fatalf("unmarshal DefaultAgentsConfigJSON: %v", err) } var startup config.StartupConfig if err := json.Unmarshal([]byte(startupLiteral), &startup); err != nil { diff --git a/internal/cmd/up.go b/internal/cmd/up.go index 6f5aab2..c1e83f8 100644 --- a/internal/cmd/up.go +++ b/internal/cmd/up.go @@ -301,8 +301,9 @@ func runUp(cmd *cobra.Command, args []string) error { } // Defined action order (Gap-9): agents → gates → dispatch → watchdog, each - // best-effort (warn+continue, never abort). Gates/dispatch are startup.json-driven - // and blanket-only so `af up ` stays byte-identical (C-4). + // best-effort (warn+continue, never abort). Gates stay blanket-only so `af up ` + // stays byte-identical (C-4); dispatch (K9) and the watchdog (N5) are startup.json-driven + // and run on BOTH paths so the documented `af up manager` also auto-starts the dispatcher. if blanket { // AC-3/AC-4: apply gates with the af-up-resolved root (R-7) — for the gate // files AND as the fidelity active-formula formulaDir, so the guard checks @@ -322,16 +323,25 @@ func runUp(cmd *cobra.Command, args []string) error { } else if startupCfg.Fidelity != "" && startupCfg.Fidelity != "default" { fmt.Fprintf(cmd.OutOrStdout(), "fidelity gate: %s\n", startupCfg.Fidelity) } + } - // AC-5: start the dispatcher when configured (friendly-skips internally when - // dispatch.json is absent/unconfigured, warns on real config errors; an - // already-running dispatcher is a benign no-op). A real launch failure is - // best-effort like every other af-up sub-action: warn + allOK=false. - if startupCfg.StartDispatch { - if dErr := startDispatch(cmd, root, t); dErr != nil { - fmt.Fprintf(cmd.ErrOrStderr(), "warning: dispatcher launch failed: %v\n", dErr) - allOK = false - } + // AC-5 + K9 (issue #73 / cross-review C2): auto-start the dispatcher on BOTH the blanket + // and positional `af up` paths, following the watchdog precedent (N5). The documented + // `af up manager` is positional, so gating the dispatcher to blanket `af up` only broke + // the "tag an issue, never visit the manager" promise on the documented setup path. + // startDispatch friendly-skips an absent/unconfigured dispatch.json, warns on real config + // errors, and is idempotent (already-running is a benign no-op), so running it on every + // path is safe. A real launch failure is best-effort: warn + allOK=false. + if startupCfg.StartDispatch { + // K8 (mandatory with K6): pre-flight the dispatch config against agents.json and warn + // (never abort) when it references unprovisioned agents — else a fresh/partial factory + // launches a dispatcher the K6 loop silently skip-and-warns on every cycle. + if loadDispatchConfigState(root) == dispatchStateUnprovisioned { + fmt.Fprintf(cmd.ErrOrStderr(), "warning: dispatch.json references agents not in agents.json; the dispatcher will skip those mappings — run `af install --agents` or fix the mappings\n") + } + if dErr := startDispatch(cmd, root, t); dErr != nil { + fmt.Fprintf(cmd.ErrOrStderr(), "warning: dispatcher launch failed: %v\n", dErr) + allOK = false } } diff --git a/internal/cmd/up_dispatch_positional_test.go b/internal/cmd/up_dispatch_positional_test.go new file mode 100644 index 0000000..11e8eda --- /dev/null +++ b/internal/cmd/up_dispatch_positional_test.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "bytes" + "testing" + + "github.com/spf13/cobra" +) + +// K9 (cross-review C2): the documented `af up manager` is POSITIONAL, but the +// dispatcher auto-start was gated to the blanket `af up` path only — so the +// "just tag an issue, never visit the manager" promise broke on the documented +// setup path. Hoisting StartDispatch out of the blanket gate makes the positional +// path also auto-start the dispatcher (idempotent). This pins it. +func TestRunUp_PositionalArg_AutoStartsDispatcher(t *testing.T) { + root := t.TempDir() + initTestGitRepo(t, root) + writeAFFile(t, root, "factory.json", `{"type":"factory","version":1,"name":"test"}`) + writeAFFile(t, root, "agents.json", + `{"agents":{"manager":{"type":"interactive","description":"m"}}}`) + writeAFFile(t, root, "startup.json", `{"start_dispatch":true}`) + writeAFFile(t, root, "dispatch.json", + `{"repos":["t/r"],"trigger_label":"agentic","mappings":[{"label":"x","agent":"manager"}],"interval_seconds":300}`) + + t.Setenv("AF_WORKTREE", "") + t.Setenv("AF_WORKTREE_ID", "") + t.Chdir(root) + + fake, _ := setupHermeticSessions(t) + + cmd := &cobra.Command{} + var buf bytes.Buffer + cmd.SetOut(&buf) + cmd.SetErr(&buf) + + _ = runUp(cmd, []string{"manager"}) + + if !opRecorded(fake.ops, "NewSession "+dispatchSessionName) { + t.Errorf("positional `af up manager` must auto-start the dispatcher (K9); ops=%v out=%q", fake.ops, buf.String()) + } +} + +// K9: the positional auto-start is idempotent — a second `af up manager` with the +// dispatcher already running is a benign no-op (no new session), not an error. +func TestRunUp_PositionalArg_DispatcherAlreadyRunning_NoOp(t *testing.T) { + root := t.TempDir() + initTestGitRepo(t, root) + writeAFFile(t, root, "factory.json", `{"type":"factory","version":1,"name":"test"}`) + writeAFFile(t, root, "agents.json", + `{"agents":{"manager":{"type":"interactive","description":"m"}}}`) + writeAFFile(t, root, "startup.json", `{"start_dispatch":true}`) + writeAFFile(t, root, "dispatch.json", + `{"repos":["t/r"],"trigger_label":"agentic","mappings":[{"label":"x","agent":"manager"}],"interval_seconds":300}`) + + t.Setenv("AF_WORKTREE", "") + t.Setenv("AF_WORKTREE_ID", "") + t.Chdir(root) + + fake, _ := setupHermeticSessions(t) + fake.present[dispatchSessionName] = true // already running + + cmd := &cobra.Command{} + var buf bytes.Buffer + cmd.SetOut(&buf) + cmd.SetErr(&buf) + + _ = runUp(cmd, []string{"manager"}) + + if opRecorded(fake.ops, "NewSession "+dispatchSessionName) { + t.Errorf("an already-running dispatcher must NOT be re-created on the positional path; ops=%v", fake.ops) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index cd7da18..3c438ab 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -123,6 +123,32 @@ func DefaultFactoryConfigJSON() string { return string(b) } +// DefaultAgentsConfigJSON returns the fresh-install agents.json content built from the +// AgentConfig struct (single-source, compiler-checked — mirroring DefaultFactoryConfigJSON). +// Beyond manager+supervisor it SEEDS the four dispatch specialists referenced by the +// baked-in default dispatch.json (issue #73 K5), so a fresh `af install --init` factory is +// valid-by-construction on EVERY init path (cross-review C1): ValidateDispatchConfig then +// finds every mapping agent present. The role templates are already embedded +// (internal/templates/roles/*.tmpl), so a seeded registry entry is sufficient for af prime +// and `af sling --agent` to resolve each specialist — no agent-gen run or rebuild. Each +// specialist carries a non-empty description (validateAgentConfig requires it) and the +// matching formula (so the feature-workflow phases resolve to formula-bearing agents). +func DefaultAgentsConfigJSON() string { + cfg := AgentConfig{Agents: map[string]AgentEntry{ + "manager": {Type: "interactive", Description: "Interactive agent for human-supervised work", Directive: "Read your memory and docs, and prove it."}, + "supervisor": {Type: "autonomous", Description: "Autonomous agent for independent task execution", Directive: "Read your memory and docs, and prove it."}, + "rapid-soldesign-plan": {Type: "autonomous", Description: "Autonomous design + implementation-planning specialist", Formula: "rapid-soldesign-plan"}, + "rapid-implement": {Type: "autonomous", Description: "Autonomous test-first implementation specialist", Formula: "rapid-implement"}, + "ultra-review": {Type: "autonomous", Description: "Autonomous multi-agent PR review specialist", Formula: "ultra-review"}, + "rapid-increment": {Type: "autonomous", Description: "Autonomous PR-iteration specialist", Formula: "rapid-increment"}, + }} + b, err := json.Marshal(cfg) + if err != nil { // unreachable for these field types + return `{"agents":{"manager":{"type":"interactive","description":"Interactive agent for human-supervised work","directive":"Read your memory and docs, and prove it."},"supervisor":{"type":"autonomous","description":"Autonomous agent for independent task execution","directive":"Read your memory and docs, and prove it."}}}` + } + return string(b) +} + // BuildHostConfig holds the contents of build-host.json type BuildHostConfig struct { Mode string `json:"mode"` diff --git a/internal/config/default_dispatch_test.go b/internal/config/default_dispatch_test.go new file mode 100644 index 0000000..92653ff --- /dev/null +++ b/internal/config/default_dispatch_test.go @@ -0,0 +1,164 @@ +package config + +import ( + "encoding/json" + "strings" + "testing" +) + +// K1: the baked-in default with a discovered repo must be a fully valid dispatch +// config — it parses AND passes the struct-level validator (LoadDispatchConfig) and +// carries exactly the four mappings + the feature-workflow the design specifies. +func TestDefaultDispatchConfigJSON_WithRepo_Valid(t *testing.T) { + js := DefaultDispatchConfigJSON("acme/widget") + dir := writeDispatchJSON(t, js) + cfg, err := LoadDispatchConfig(dir) + if err != nil { + t.Fatalf("default dispatch config must load+validate, got: %v\njson: %s", err, js) + } + if len(cfg.Repos) != 1 || cfg.Repos[0] != "acme/widget" { + t.Errorf("repos = %v, want [acme/widget]", cfg.Repos) + } + if cfg.TriggerLabel != "agentic" { + t.Errorf("trigger_label = %q, want agentic", cfg.TriggerLabel) + } + if cfg.IntervalSecs != 300 { + t.Errorf("interval_seconds = %d, want 300", cfg.IntervalSecs) + } + if cfg.RetryAfterSecs != 1800 { + t.Errorf("retry_after_seconds = %d, want 1800", cfg.RetryAfterSecs) + } + if !cfg.RemoveTriggerAfterDispatch { + t.Errorf("remove_trigger_after_dispatch = false, want true") + } + if len(cfg.Mappings) != 4 { + t.Fatalf("mappings = %d, want 4", len(cfg.Mappings)) + } + wantAgents := map[string]string{ + "rapid-plan": "rapid-soldesign-plan", + "rapid-engineer": "rapid-implement", + "pr-review": "ultra-review", + "pr-iterate": "rapid-increment", + } + gotAgents := map[string]string{} + for _, m := range cfg.Mappings { + if len(m.Labels) != 1 { + t.Errorf("mapping %v should have exactly one label", m) + continue + } + gotAgents[m.Labels[0]] = m.Agent + } + for label, agent := range wantAgents { + if gotAgents[label] != agent { + t.Errorf("mapping label %q -> %q, want %q", label, gotAgents[label], agent) + } + } + if len(cfg.Workflows) != 1 || cfg.Workflows[0].Label != "feature-workflow" { + t.Fatalf("workflows = %v, want one feature-workflow", cfg.Workflows) + } + if got := cfg.Workflows[0].Phases; len(got) != 2 || got[0] != "rapid-plan" || got[1] != "rapid-engineer" { + t.Errorf("feature-workflow phases = %v, want [rapid-plan rapid-engineer]", got) + } +} + +// K1: notify_on_complete is omitted from the default (Gap-7) — it defaults to +// "manager" at runtime, so an explicit value would add a brittle cross-file check. +func TestDefaultDispatchConfigJSON_OmitsNotifyOnComplete(t *testing.T) { + js := DefaultDispatchConfigJSON("acme/widget") + if strings.Contains(js, "notify_on_complete") { + t.Errorf("default must OMIT notify_on_complete (Gap-7); json: %s", js) + } +} + +// K1: pr-source mappings must carry source "pr"; issue-source mappings "issue". +func TestDefaultDispatchConfigJSON_MappingSources(t *testing.T) { + var disp DispatchConfig + if err := json.Unmarshal([]byte(DefaultDispatchConfigJSON("a/b")), &disp); err != nil { + t.Fatalf("unmarshal: %v", err) + } + wantSource := map[string]string{ + "rapid-plan": "issue", "rapid-engineer": "issue", + "pr-review": "pr", "pr-iterate": "pr", + } + for _, m := range disp.Mappings { + if len(m.Labels) == 1 && m.Source != wantSource[m.Labels[0]] { + t.Errorf("mapping %q source = %q, want %q", m.Labels[0], m.Source, wantSource[m.Labels[0]]) + } + } +} + +// K1 fallback: an empty repo degrades to the loadable empty shape (status quo), not +// a repos:[] + 4-mapping config (which would fail validateDispatchConfig's repo>0 rule +// and break the dispatcher). The dispatcher treats this as "not configured". +func TestDefaultDispatchConfigJSON_EmptyRepo_DegradesToEmpty(t *testing.T) { + var disp DispatchConfig + if err := json.Unmarshal([]byte(DefaultDispatchConfigJSON("")), &disp); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if len(disp.Repos) != 0 { + t.Errorf("empty-repo default must have repos:[], got %v", disp.Repos) + } + if len(disp.Mappings) != 0 { + t.Errorf("empty-repo default must have mappings:[] (else repos:[]+mappings fails to load); got %v", disp.Mappings) + } +} + +// K5: the default agents.json seeds manager + supervisor + the four specialists, each +// VALID (validateAgentConfig requires a non-empty description), with the specialists +// formula-bearing so the dispatch default cross-validates. +func TestDefaultAgentsConfigJSON_SeedsSpecialists(t *testing.T) { + var ac AgentConfig + if err := json.Unmarshal([]byte(DefaultAgentsConfigJSON()), &ac); err != nil { + t.Fatalf("unmarshal default agents.json: %v", err) + } + // Must pass the real validator (valid-by-construction). + if err := validateAgentConfig(&ac); err != nil { + t.Fatalf("default agents.json must be valid-by-construction, got: %v", err) + } + if e := ac.Agents["manager"]; e.Type != "interactive" || e.Description == "" { + t.Errorf("manager = %+v, want interactive with a description", e) + } + if e := ac.Agents["supervisor"]; e.Type != "autonomous" || e.Description == "" { + t.Errorf("supervisor = %+v, want autonomous with a description", e) + } + specialists := map[string]string{ + "rapid-soldesign-plan": "rapid-soldesign-plan", + "rapid-implement": "rapid-implement", + "ultra-review": "ultra-review", + "rapid-increment": "rapid-increment", + } + for name, formula := range specialists { + e, ok := ac.Agents[name] + if !ok { + t.Errorf("default agents.json must seed specialist %q", name) + continue + } + if e.Type != "autonomous" { + t.Errorf("specialist %q type = %q, want autonomous", name, e.Type) + } + if e.Formula != formula { + t.Errorf("specialist %q formula = %q, want %q", name, e.Formula, formula) + } + if e.Description == "" { + t.Errorf("specialist %q must have a non-empty description (validateAgentConfig requires it)", name) + } + } +} + +// K7 golden/cross-file: the shipped default parses (struct-level) AND cross-validates +// (ValidateDispatchConfig) against the default-seeded agents.json — proving the bare +// `af install --init` factory is valid-by-construction (the cross-review C1 guarantee). +func TestDefaultDispatchConfigJSON_CrossValidatesWithDefaultAgents(t *testing.T) { + dir := writeDispatchJSON(t, DefaultDispatchConfigJSON("acme/widget")) + disp, err := LoadDispatchConfig(dir) + if err != nil { + t.Fatalf("struct-level validation failed: %v", err) + } + var agents AgentConfig + if err := json.Unmarshal([]byte(DefaultAgentsConfigJSON()), &agents); err != nil { + t.Fatalf("unmarshal default agents.json: %v", err) + } + if err := ValidateDispatchConfig(disp, &agents); err != nil { + t.Fatalf("default dispatch must cross-validate against default agents.json (C1/C-6): %v", err) + } +} diff --git a/internal/config/dispatch.go b/internal/config/dispatch.go index 2d3a653..0210758 100644 --- a/internal/config/dispatch.go +++ b/internal/config/dispatch.go @@ -18,7 +18,7 @@ const defaultNotifyAgent = "manager" type DispatchConfig struct { Repos []string `json:"repos"` TriggerLabel string `json:"trigger_label"` - NotifyOnComplete string `json:"notify_on_complete"` + NotifyOnComplete string `json:"notify_on_complete,omitempty"` Mappings []DispatchMapping `json:"mappings"` IntervalSecs int `json:"interval_seconds"` RetryAfterSecs int `json:"retry_after_seconds"` @@ -44,6 +44,48 @@ type Workflow struct { Phases []string `json:"phases"` // ordered existing mapping labels } +// DefaultDispatchConfigJSON returns the fresh-install dispatch.json content built +// from the DispatchConfig struct — so the field set is compiler-checked and cannot +// drift from the schema, mirroring DefaultFactoryConfigJSON (issue #371 Gap-6). repo +// is the validated owner/name discovered at install time (the cmd-layer caller owns +// discovery/validation; this package has no git dependency). +// +// A non-empty repo yields the full label->agent default (the four specialists + the +// feature-workflow) the bootstrapped factory needs to drive autonomous work from +// GitHub labels (issue #73). An EMPTY repo degrades to the loadable empty default +// (repos:[], mappings:[]) — the status-quo "not configured" shape — because a +// repos:[]-with-mappings config fails validateDispatchConfig's repo>0 rule and would +// break the dispatcher. notify_on_complete is OMITTED (it defaults to "manager" at +// runtime via validateDispatchConfig; an explicit value would add a brittle cross-file +// existence check — Gap-7). +func DefaultDispatchConfigJSON(repo string) string { + cfg := DispatchConfig{ + Repos: []string{}, + TriggerLabel: "agentic", + Mappings: []DispatchMapping{}, + IntervalSecs: 300, + RetryAfterSecs: 1800, + } + if repo != "" { + cfg.Repos = []string{repo} + cfg.RemoveTriggerAfterDispatch = true + cfg.Mappings = []DispatchMapping{ + {Labels: []string{"rapid-plan"}, Source: "issue", Agent: "rapid-soldesign-plan"}, + {Labels: []string{"rapid-engineer"}, Source: "issue", Agent: "rapid-implement"}, + {Labels: []string{"pr-review"}, Source: "pr", Agent: "ultra-review"}, + {Labels: []string{"pr-iterate"}, Source: "pr", Agent: "rapid-increment"}, + } + cfg.Workflows = []Workflow{ + {Label: "feature-workflow", Phases: []string{"rapid-plan", "rapid-engineer"}}, + } + } + b, err := json.Marshal(cfg) + if err != nil { // unreachable for these scalar/slice field types + return `{"repos":[],"trigger_label":"agentic","mappings":[],"interval_seconds":300,"retry_after_seconds":1800}` + } + return string(b) +} + // LoadDispatchConfig loads and validates .agentfactory/dispatch.json. func LoadDispatchConfig(root string) (*DispatchConfig, error) { path := DispatchConfigPath(root)