# Goal: `house` sub-workflow node

Status: **proposal / not yet built.** Sub-workflows are deferred in
SPEC §14 ("Sub-workflow (`house`)"); the parser rejects `subgraph`
blocks and the shape map has no `house` entry. This plan adds a
first-class sub-workflow node so a workflow can run another workflow
as one step and route on its outcome.

Per `AGENTS.md`, this touches a deferred §14 item, so the SPEC edit +
new requirement land **first**, with sign-off, before code.

---

## 1. What a `house` node does

A `house` node references a child `.dot` workflow and runs it to a
terminal state. Isolation follows `read_only` (§2): a mutating house
runs the child in its own seeded worktree and recombines on success; a
read-only house runs the child against the shared parent tree with
enforcement. Either way the child's terminal `RunStatus` maps to the
node's `OutcomeStatus`, which routes the parent's outgoing edges
exactly like a tool/agent node.

```dot
build_sub [
    shape    = house,
    label    = "Run preflight",
    workflow = "workflows/preflight.dot",   // required, resolved vs parent .dot dir
    inputs   = "goal.md=goal.md"            // optional: parent-worktree file -> child input
]
build_sub -> next     [label="SUCCESS"]
build_sub -> recover  [label="FAILURE"]
```

### Outcome mapping (child `RunStatus` → node `OutcomeStatus`)
- `COMPLETED` → `SUCCESS`
- `INCOMPLETE` → `FAILURE` (routes `label="FAILURE"`; ends parent
  INCOMPLETE if no such edge, per §6.1)
- `ABORTED` → `FAILURE` with a stable message
- **Paused at a human gate** → v1 returns `FAILURE` with
  `"sub-workflow paused at gate <id>; nested gates unsupported in v1"`.
  Nested-gate propagation (pausing the parent, nested resume) is an
  explicit deferral (see §8 open questions).

`captured_output` = a compact child summary (status + child run id +
last node). Token totals from the child run sum into the node's
`tok-in`/`tok-out`.

---

## 2. Worktree & checkpoint model

> **Unified with `read_only` (see `goal-cwd-not-worktree.md`).** A
> `house` node obeys the same conditional-isolation rule as any node:
>
> - **`read_only=true` house** — the child runs against the **shared
>   parent tree**; nothing to recombine. The engine launches the child
>   run with **`default_read_only=true`** so the child's own agents are
>   enforced read-only top-to-bottom and cannot mutate the parent tree.
>   Zero worktrees. (Use for search / triage / review sub-workflows.)
> - **`read_only=false` house** — the isolated, seeded, recombine-on-
>   success path described below.
>
> Inside a parallel fan-out a sub-workflow branch classifies the same
> way: all-nodes-read_only → shares; otherwise isolates + recombines.

### Deriving read-only-ness from the child (not trusting a flag)

The caller does not guess whether the child mutates — it **derives** it.
The engine already parses + validates the child graph, so:

> A `house` node is read-only **iff every node in its child graph
> resolves `read_only=true`** (under the child's own
> `default_read_only`), recursively through nested `house` nodes. If
> *any* child node is mutating, the house node is mutating → isolate +
> recombine.

Consequences:
- A child that can write **always** gets isolation — safety is
  automatic, not contingent on the author flagging the house node.
- An explicit `read_only=false` on the house node can *force*
  isolation (belt-and-suspenders).
- An explicit `read_only=true` that **contradicts** a mutating child is
  a validation error (can't promise read-only over a writing child).
- Strength: the derived read-only-ness is exactly as strong as the
  per-node enforcement underneath — enforced for agent nodes (tool
  allowlist), a contract for tool/shell nodes.
- The recursion/cycle guard (§3) bounds the recursive derivation.

The mutating (`read_only=false`) path reuses the parallel-branch
machinery (§6.10), which already solved "run work in a sub-worktree
seeded from the parent and recombine":

1. Snapshot the parent worktree commit (the node's entry state).
2. Run the child via a child `Engine` on the **same repo**, with
   `base_ref = <parent worktree commit>`. The child gets its own
   `refs/heads/attractor/run/<child-id>/{state,worktree}` — fully
   local-first (§1), nothing pushed.
3. On child `COMPLETED`, recombine the child worktree tree into the
   parent worktree (same primitive `worktree_commit` / branch merge
   used for `tripleoctagon` fan-in, §6.10.3). On non-success, leave the
   parent worktree untouched.
4. The parent node's dual-commit checkpoint records the post-recombine
   parent worktree as usual (§6.4).

This keeps child and parent journals separate and inspectable
(`attractor run show <child-id>` works standalone) while the parent
node remains a single checkpoint boundary.

---

## 3. Recursion / cycle safety

Static validation can't see across files, so guard at runtime:
- Thread a `subworkflow_stack: tuple[Path, ...]` (resolved child
  paths) down through dispatch.
- Before launching a child, if its resolved path is already in the
  stack → `FAILURE` `"sub-workflow recursion detected: <cycle>"`.
- Cap absolute depth with `default_max_subworkflow_depth` (graph attr,
  default 5); exceeding it → `FAILURE`.

---

## 4. Code changes (by file)

### Workflow layer
- `workflow/graph.py` — add `NodeKind.SUBWORKFLOW = "subworkflow"`;
  add `"house": NodeKind.SUBWORKFLOW` to `_SHAPE_TO_KIND`.
- `workflow/parser.py` — no change to the `subgraph` rejection (still
  unsupported); `house` flows through the normal shape path.
- `workflow/validate.py` — new rules:
  - a `house` node **must** declare `workflow=`; missing → actionable
    error (line/col).
  - resolve `workflow=` relative to the parent `.dot`'s directory;
    error if the file does not exist or fails to parse/validate
    (recursively validate the child, bounded by the cycle guard).
  - forbid `house` inside a parallel region (mirror the existing
    "nested component" rule, validate.py Rule 3).
  - `workflow=` / `inputs=` are subworkflow-only attributes (§5.5
    table); reject on other node kinds.
- `workflow/arguments.py` — parse `inputs="name=src,name2=src2"` into a
  typed mapping (reuse the existing attribute-splitting helpers).

### Engine layer
- `engine/engine.py` — new branch in `_dispatch_handler`:
  `if kind == NodeKind.SUBWORKFLOW:` → resolve+parse child, build the
  child `Engine`, run it with the seeded `base_ref`, recombine on
  success, map status → the 6-tuple `(status, captured_output,
  tok_in, tok_out, preferred_label, suggested_next_ids)`. Catch
  `EngineError`/`ParseError`/`ValidationFailed` as `FAILURE` (same
  pattern as `AgentError`/`ExecError`).
- `engine/journal.py` — add optional `child_run_id: str | None = None`
  to `NodeCompleted` (additive, like `suggested_next_ids`); lets
  `run show` link parent→child.
- `engine/events.py` — optional: `SubworkflowStarted` /
  `SubworkflowCompleted` events carrying `child_run_id`, or forward
  child events through the parent callback with the child id attached.
  v1 can ship with just the house node's normal `NodeStarted` /
  `NodeOutcome` and add nested events later.

### Resume
- Mid-node interruption inside a `house` node follows §6.3: on resume
  the parent re-dispatches the node. v1 re-runs the child fresh (new
  child id), consistent with agent/tool mid-node semantics. If
  `child_run_id` is journaled and the child is resumable, a later
  revision can resume the child instead of restarting — noted as
  follow-up, not v1.

---

## 5. SPEC changes
- §5.3 shape→handler: add `house → sub-workflow`; remove it from the
  "Deferred shapes" line.
- §5.5 attribute set: add `workflow` (required, subworkflow-only) and
  `inputs` (optional, subworkflow-only); note token roll-up.
- New §6.x "Sub-workflows": semantics, outcome mapping, worktree
  recombination, recursion guard, the v1 no-nested-gate rule.
- §14: remove "Sub-workflow (`house`)" from Deferred; keep "explicit
  Task-tool dispatch" deferred (pi sub-agents are a separate path).
- §15: add open questions — nested human-gate propagation, child-run
  resume linkage.

## 6. traceable-reqs
Add (with the user's approval, before tags):
- `REQ-EXEC-SUBWORKFLOW` — "a `house` node runs a referenced child
  workflow to a terminal state in a seeded sub-worktree, recombines on
  success, and maps child RunStatus → node OutcomeStatus; recursion is
  bounded." `required_stages = ["doc", "impl", "unit", "int"]`.
- `REQ-WORKFLOW-SUBWORKFLOW-VALIDATION` — "validation requires
  `workflow=` on house nodes, resolves+validates the child, rejects
  house inside parallel regions, and confines `workflow`/`inputs` to
  house nodes." `["doc", "impl", "unit"]`.

## 7. Tests
- **unit** (`tests/test_workflow/`): `house`→SUBWORKFLOW mapping;
  missing `workflow=` error (line/col); child-not-found error; house
  inside parallel region rejected; `inputs` parsing.
- **unit** (`tests/test_engine/`): status→outcome mapping table;
  recursion/depth guard returns FAILURE; paused-child → FAILURE
  message.
- **int** (`tests/test_engine/test_subworkflow.py`): parent workflow
  with a `house` node running a real **tool-only** child
  (`echo > file`) → parent COMPLETED and the child's file is present
  in the parent worktree (proves recombination); a failing child
  routes the parent `FAILURE` edge; a self-referential parent is
  caught by the cycle guard. No LLM needed → runs in CI.

## 8. Open questions to settle before building
1. **Nested human gates.** v1 fails the node on a child pause. Worth
   propagating up (pause parent, nested resume) now, or defer?
2. **Recombination conflict policy.** If the child's tree conflicts
   with concurrent parent changes — but in linear flow the parent
   worktree is quiescent while the node runs, so a fast-forward/replace
   should suffice. Confirm we want replace-from-child vs a real merge.
3. **Inputs direction.** v1 copies parent-worktree files into the
   child as inputs (§ like `Engine.start(inputs=...)`). Do we also need
   to surface named child *outputs* back, or is "the whole recombined
   tree" enough? (Proposed: tree is enough for v1.)
4. **Child events.** Ship v1 with just the house node's
   NodeStarted/NodeOutcome, or add SubworkflowStarted/Completed +
   child-id-tagged forwarding now?

## 9. Phasing
1. SPEC §5/§6/§14/§15 edit + the two new REQ ids (doc tags). **← sign-off gate**
2. `NodeKind.SUBWORKFLOW` + shape map + validation rules + unit tests.
3. Engine dispatch + recombination + recursion guard + journal field.
4. Integration test (tool-only child, failure routing, cycle guard).
5. A real example `workflows/with-subworkflow.dot` + README/skill note.
