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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions app/config/schemas/baseworkflow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# baseworkflow.yml — the declarative shelf-key contract (typed I/O).
#
# Each top-level key is a "shelf.key"; its value is the schema for what that key
# holds. A key is declared ONCE here and shared by every action that reads or
# writes it (input.job is read by 11 actions) — the DRY, single-source contract.
# The debug viewer reflects these into typed, validated probe forms; the engine
# validates written values against them when DISPATCH_ENFORCE_SCHEMA is set
# (opt-in, off by default).
#
# Field spec keys: type (string|integer|number|boolean|array|object|any),
# required, description, enum, items (array element type), format (e.g.
# multiline), default, example, minimum, maximum, fields (nested object).
# Pulled in by app/workflows/baseworkflow.yml via `schemas: [../config/schemas/*.yml]`.

# -- run-time inputs (seeded onto the input shelf) --------------------------
input.job:
type: object
description: The unit of work from intake — the GitHub issue (or synthesized job) the pipeline implements.
fields:
issue: {type: integer, required: true, description: "Issue number", example: 9001}
title: {type: string, required: true, description: "One-line summary", example: "Add retry with backoff to fetch"}
body: {type: string, format: multiline, description: "Full body / acceptance criteria", example: "Implement retry.\n\n## Acceptance criteria\n- retries 3x\n- exponential backoff"}
labels: {type: array, items: string, description: "GitHub labels (drive purpose classification)", example: [enhancement]}
discovered: {type: array, items: string, description: "Files surfaced during triage", example: ["foundation/proc.py", "foundation/runtime.py"]}

input.triage:
type: object
description: The architect's routing decision for this job.
fields:
action: {type: string, required: true, enum: [implement, fix, docs], description: "Kind of work", example: implement}
scope: {type: string, enum: [s, m, l], description: "Rough size", example: m}
route: {type: string, description: "Generation route / model lane", example: gen-default}
confidence: {type: number, minimum: 0, maximum: 1, description: "Triage confidence (0–1)", example: 0.82}

input.config:
type: object
description: Per-run pipeline configuration (defaulted when absent).
fields:
engineering_budget: {type: integer, minimum: 0, description: "Token budget for the engineering phase", example: 450000}
verify_cmd: {type: string, description: "Command that validates the produced work", example: "bash scripts/smoke.sh"}

input.request:
type: object
description: The seed controller's raw intake envelope (ADR-003) — how the work entered.
fields:
source: {type: string, required: true, enum: [job, github], description: "Intake source", example: job}
job: {type: object, description: "Synthesized job (source=job): title / goal / acceptance"}
repo: {type: string, description: "owner/repo (source=github)"}
issue: {type: integer, description: "Issue number (source=github)"}
fields: {type: object, description: "Structured intake fields"}

# -- deliverables read downstream (an action's inputs) ----------------------
deliverables.purpose:
type: object
description: The classified purpose of the work — selects the engineering harness.
fields:
purpose: {type: string, description: "Classification", example: new-feature}
source: {type: string, enum: [label, keyword, default], description: "How it was classified"}
harness: {type: string, description: "Selected engineering harness/skill"}
harness_ref: {type: string, description: "Harness source ref"}
harness_setup: {type: string, format: multiline, description: "Setup commands"}
harness_rationale: {type: string, format: multiline, description: "Why this harness"}

deliverables.plan:
type: object
description: The decomposed work plan — units plus agent staffing.
fields:
units: {type: array, items: object, description: "Work units (id, deliverable, files, phase, acceptance)"}
staffing: {type: object, description: "Agent staffing / wave plan"}
criteria: {type: array, description: "Acceptance criteria"}

deliverables.strategy:
type: object
description: Parallelization strategy across the work units.
fields:
strategy: {type: string, description: "Chosen strategy"}
summary: {type: string, format: multiline}
requires_parallelization_rationale: {type: boolean}
parallelization_rationale: {type: string, format: multiline}
parallel_units: {type: array, items: string}
n_parallel: {type: integer, minimum: 0}
abc: {type: string}

deliverables.bucket:
type: object
description: The budget bucket the work falls into.
fields:
bucket: {type: string, description: "Bucket label"}
k_units: {type: integer, minimum: 0, description: "Unit count"}
rationale: {type: string, format: multiline}

deliverables.budget:
type: object
description: The engineering budget authorization envelope.
fields:
authorization: {type: object, description: "Session authorization (id, branch, route, budgets…)"}
k_units: {type: integer, minimum: 0}
engineering_budget: {type: integer, minimum: 0}

deliverables.orchestration_script:
type: object
description: The compiled multi-agent orchestration script (foundation.actions.script).
fields:
phases: {type: array, items: object, description: "PhaseSpec list"}
permission: {type: string, description: "Permission posture", example: deny-by-default}
allow_tools: {type: array, items: string}
budget: {type: integer, minimum: 0}

deliverables.work_plan:
type: object
description: The consolidated architect work plan handed to the engineer.
fields:
issue: {type: integer}
purpose: {type: object}
strategy: {type: object}
plan: {type: object}
budget: {type: object}

deliverables.engineering_result:
type: object
description: The engineer's output for the work unit.
fields:
ok: {type: boolean, required: true, description: "Did the engineer succeed"}
value: {type: string, format: multiline, description: "Produced artifact / summary"}
meta: {type: object, description: "usage, model, source, budget"}

deliverables.docs:
type: object
description: The docs update produced for the work.
fields:
issue: {type: integer}
summary: {type: string, format: multiline}
sections: {type: array, items: string}

deliverables.consolidation:
type: object
description: The PR consolidation record (branch, PR, mutations).
fields:
issue: {type: string}
status: {type: string}
pr_number: {type: integer, description: "PR number (null until opened)"}
pr_url: {type: string}
branch: {type: string}
base: {type: string}
skipped: {type: boolean}
dry_run: {type: boolean}
mutations: {type: array, description: "gh mutations performed (or dry-run)"}

# -- shared shelf (seed variant ↔ lifecycle hand-off, ADR-003) --------------
shared.envelope:
type: object
description: The intake envelope passed between seed variants.
fields:
source: {type: string, enum: [job, github, interactive]}
repo: {type: string}
issue: {type: integer, description: "Issue number (null for job source)"}
fields: {type: object, description: "title / goal / acceptance"}

shared.intake:
type: object
description: The seed controller's intake decision.
fields:
source: {type: string}
variant: {type: string, description: "Which seed variant handled it"}
accepted: {type: boolean, required: true}
unmet: {type: array, description: "Unmet needs"}
missing: {type: array, description: "Missing fields"}
reason: {type: string, format: multiline}

shared.payload:
type: object
description: The normalized work payload the seed hands to spec.
fields:
title: {type: string, required: true}
goal: {type: string, format: multiline}
acceptance: {type: string, format: multiline}
labels: {type: array, items: string}
open_needs: {type: array, items: string}
provenance: {type: object, description: "kind + origin"}
6 changes: 6 additions & 0 deletions app/workflows/baseworkflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ uses:
# this file's directory).
- ../config/actions/**/*.yml # #base-style include of every action interface file

# The declarative shelf-key contract: typed I/O for every "shelf.key" the actions
# read/write, authored once per key (schema.py). Drives typed debug-viewer probe
# forms and opt-in engine enforcement (DISPATCH_ENFORCE_SCHEMA).
schemas:
- ../config/schemas/*.yml

inputs: [request, job, triage] # provided at run time (seeded onto the input shelf)

# The parties to the contract (ADR-003): controllers the workflow references —
Expand Down
11 changes: 10 additions & 1 deletion foundation/workflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@

from .controller import CompiledWorkflow, YamlController
from .graph import GraphVisitor, workflow_to_graph
from .loader import SchemaError, load_workflow, parse_document
from .loader import SchemaError, load_schemas, load_workflow, parse_document
from .schema import (
FieldSchema,
SchemaContractError,
ShelfSchema,
parse_schema,
parse_schemas,
)
from .manifest import (
ActionManifest,
IORef,
Expand Down Expand Up @@ -74,6 +81,8 @@
"TokenRegistry", "ActionBinding", "ControllerSpec", "TokenError", "UnknownToken",
# manifest
"ActionManifest", "IORef", "ManifestError", "load_manifests", "manifest_from_dict",
# schema (the declarative shelf-key contract)
"ShelfSchema", "FieldSchema", "SchemaContractError", "parse_schema", "parse_schemas", "load_schemas",
# nodes
"Node", "WorkflowNode", "PhaseNode", "SequenceNode", "ParallelNode", "LoopNode",
"ActionRefNode", "ControllerRefNode",
Expand Down
3 changes: 3 additions & 0 deletions foundation/workflow/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ def visit_workflow(self, node: Any) -> Dict[str, Any]:
"budgets": dict(node.budgets or {}),
"inputs": list(node.inputs or ()),
"controllers": list(getattr(node, "controllers", ()) or ()),
# The declarative shelf-key contract, as JSON, keyed by "shelf.key" — the
# viewer reflects these into typed, validated probe forms.
"schemas": {ref: s.to_dict() for ref, s in (getattr(node, "schemas", {}) or {}).items()},
"nodes": self.nodes,
"edges": self.edges,
}
Expand Down
37 changes: 33 additions & 4 deletions foundation/workflow/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from foundation import filesys

from .manifest import ActionManifest, load_manifests
from .schema import parse_schemas
from .nodes import (
ActionRefNode,
ControllerRefNode,
Expand Down Expand Up @@ -175,6 +176,32 @@ def _apply_header(merged: WorkflowNode, doc: Dict[str, Any], base: WorkflowNode)
)


def load_schemas(base_dir: str, spec: Any) -> Dict[str, Any]:
"""Resolve a workflow's ``schemas:`` declaration into ``{"shelf.key": ShelfSchema}``.

``spec`` may be an **inline map** (``ref -> schema``) or a **list of file globs**
(each file a ``ref -> schema`` map), resolved relative to ``base_dir`` — the
``#base`` convention, exactly like ``uses:``. Later files/keys win on collision.
The declarative-file form is the DRY authoring model: a key like ``input.job`` is
declared once and shared by every action that reads it."""
if not spec:
return {}
if isinstance(spec, dict):
return parse_schemas(spec, where="schemas")
import glob as _glob

import yaml

merged: Dict[str, Any] = {}
for g in spec:
pattern = g if os.path.isabs(g) else os.path.join(base_dir, g)
for path in sorted(_glob.glob(pattern, recursive=True)):
with open(path, encoding="utf-8") as fh:
d = yaml.safe_load(fh) or {}
merged.update(parse_schemas(d, where=os.path.relpath(path, base_dir)))
return merged


def load_workflow(relpath: str) -> WorkflowNode:
"""Read + parse a workflow YAML into a ``WorkflowNode``. ``relpath`` resolves
via :func:`_resolve_path`; ``uses:`` manifest globs resolve relative to the
Expand All @@ -189,17 +216,19 @@ def load_workflow(relpath: str) -> WorkflowNode:
doc = yaml.safe_load(fh) or {}
wf_dir = os.path.dirname(os.path.abspath(path))
manifests = load_manifests(wf_dir, doc.get("uses") or [])
schemas = load_schemas(wf_dir, doc.get("schemas"))

from dataclasses import replace

extends = doc.get("extends")
if extends:
from dataclasses import replace

from .overlay import apply_overlays

base = load_workflow(os.path.join(wf_dir, f"{extends}.yml"))
merged = apply_overlays(base, doc.get("overlays") or [], manifests, source=relpath)
# The merged tree's interface library is the base's plus the overlay's own.
merged = replace(merged, manifests={**base.manifests, **manifests})
return _apply_header(merged, doc, base)
# Contract likewise extends the base's (overlay keys override same refs).
return replace(_apply_header(merged, doc, base), schemas={**base.schemas, **schemas})

return parse_document(doc, manifests, source=relpath)
return replace(parse_document(doc, manifests, source=relpath), schemas=schemas)
5 changes: 5 additions & 0 deletions foundation/workflow/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ class WorkflowNode(Node):
# declared-only controllers (dynamic supersede targets, never in a phase)
# can still be expanded, needs-checked and materialized.
manifests: Dict[str, Any] = field(default_factory=dict)
# The declared shelf-key contract (``"shelf.key" -> ShelfSchema``): typed I/O
# for the workflow, authored once per key (``schemas:`` include) and shared by
# every action that touches it. Drives typed debug-viewer forms and the
# opt-in engine enforcement (``DISPATCH_ENFORCE_SCHEMA``).
schemas: Dict[str, Any] = field(default_factory=dict)
admin_spec_split: float = 0.5
# Optional declarative early-completion: a registered predicate name. When a
# step completes *successfully* and this predicate admits its Result, the whole
Expand Down
Loading
Loading