mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 11:51:37 -10:00
fix(workflow-spec): unique roles per step in openrig-velocity + projection-shape test
Forward-fix #3 ondcf1994. Guard re-review found thedcf1994spec was not coherent under the projector contract. projectSpecGraph deduplicates by actor_role (keeps the FIRST step per role); the prior YAML had TWO steps with actor_role: orch (dispatch + merge). Result: vm-verify's `next_hop.suggested_roles: [orch]` resolved to dispatch (the kept step) → loop-back instead of forward to merge. refine had no incoming edge (orphan) and merge had no incoming edge (orphan). 6 steps, 4 edges, 2 orphans, 1 loop-back. Resolution: split the orch role into two uniquely-named roles — `orch-dispatch` (entry) and `orch-merge` (terminal) — both with preferred_targets pointing at orch-lead@openrig-velocity since the same human seat performs both in practice. vm-verify now routes to orch-merge. Resulting graph (6 nodes, 5 edges, no orphans): dispatch → refine → implement → guard-review → vm-verify → merge Discriminator test added in starter-spec-loader.test.ts: loads the shipped spec, calls projectSpecGraph(spec, null), asserts the EXACT edge chain + entry placement (dispatch) + terminal placement (merge) + zero orphans (every non-entry node has incoming + every non-terminal node has outgoing). The prior "has guard role" assertion was a weak discriminator; this shape-test catches the role-collision class banked feedback_poc_regression_must_discriminate calls out. Verified: pre-fix YAML (dcf1994) fails the new discriminator test (temporarily reverted only the YAML, kept the new test; FAIL on that case; restored YAML to PASS). Full daemon suite: 3379 passed / 1 skipped / 0 failed on clean run. git diff --check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
dcf1994567
commit
ac7d199450
2 files changed
+74
-22
No files matched your search
@@ -6,24 +6,30 @@
|
||||
# `workflow_spec: [email protected]` and projected onto the mission
|
||||
# Topology tab by the missions route.
|
||||
#
|
||||
# Flow (one slice end-to-end):
|
||||
# orch dispatches → advisor refines (optional) → driver implements
|
||||
# → guard narrowly reviews → qa exercises in disposable VM → orch
|
||||
# merges + closes. Single frontier (v1 projector contract).
|
||||
# Flow (one slice end-to-end, linear single-frontier chain):
|
||||
# orch-dispatch → advisor (refine) → driver (implement) →
|
||||
# guard (review) → qa (vm-verify) → orch-merge.
|
||||
#
|
||||
# Roles are uniquely named per step so the projector's role→step
|
||||
# resolution doesn't collapse two orch steps onto the same node (the
|
||||
# projector keeps the FIRST step per actor_role; identical roles on
|
||||
# two steps would orphan the second). orch-dispatch + orch-merge both
|
||||
# preferred_targets to orch-lead@openrig-velocity since the same human
|
||||
# seat performs both roles in practice.
|
||||
workflow:
|
||||
id: openrig-velocity
|
||||
version: "1.0"
|
||||
objective: >
|
||||
Drive one slice from dispatch through merge using the
|
||||
openrig-velocity rig's role chain (orch / advisor / driver /
|
||||
guard / qa).
|
||||
openrig-velocity rig's role chain (orch-dispatch / advisor / driver /
|
||||
guard / qa / orch-merge).
|
||||
target:
|
||||
rig: openrig-velocity
|
||||
entry:
|
||||
role: orch
|
||||
role: orch-dispatch
|
||||
coordination_terminal_turn_rule: hot_potato
|
||||
roles:
|
||||
orch:
|
||||
orch-dispatch:
|
||||
skill_refs: [openrig-user, orchestration-team]
|
||||
preferred_targets: [orch-lead@openrig-velocity]
|
||||
advisor:
|
||||
@@ -38,14 +44,17 @@ workflow:
|
||||
qa:
|
||||
skill_refs: [openrig-user, dogfood, verification-before-completion]
|
||||
preferred_targets: [velocity-qa@openrig-velocity]
|
||||
orch-merge:
|
||||
skill_refs: [openrig-user, orchestration-team]
|
||||
preferred_targets: [orch-lead@openrig-velocity]
|
||||
steps:
|
||||
- id: dispatch
|
||||
actor_role: orch
|
||||
objective: Author the dispatch packet + queue handoff to driver.
|
||||
actor_role: orch-dispatch
|
||||
objective: Author the dispatch packet + queue handoff to advisor or driver.
|
||||
allowed_exits: [handoff, waiting, failed]
|
||||
next_hop:
|
||||
mode: require
|
||||
suggested_roles: [driver]
|
||||
suggested_roles: [advisor]
|
||||
- id: refine
|
||||
actor_role: advisor
|
||||
objective: Author or refine the IMPL-PRD before driver picks up.
|
||||
@@ -73,9 +82,9 @@ workflow:
|
||||
allowed_exits: [handoff, waiting, failed]
|
||||
next_hop:
|
||||
mode: require
|
||||
suggested_roles: [orch]
|
||||
suggested_roles: [orch-merge]
|
||||
- id: merge
|
||||
actor_role: orch
|
||||
actor_role: orch-merge
|
||||
objective: Merge the slice to main + close the dispatch loop.
|
||||
allowed_exits: [done, failed]
|
||||
next_hop:
|
||||
|
||||
@@ -25,6 +25,7 @@ import { coreSchema } from "../src/db/migrations/001_core_schema.js";
|
||||
import { workflowSpecsSchema } from "../src/db/migrations/033_workflow_specs.js";
|
||||
import { WorkflowSpecCache } from "../src/domain/workflow-spec-cache.js";
|
||||
import { loadStarterWorkflowSpecs, defaultBuiltinSpecsDir } from "../src/domain/workflow/starter-spec-loader.js";
|
||||
import { projectSpecGraph } from "../src/domain/workflow/slice-workflow-projection.js";
|
||||
|
||||
const ALPHA_SPEC = `workflow:
|
||||
id: alpha-spec
|
||||
@@ -156,19 +157,61 @@ describe("built-in workflow spec loader", () => {
|
||||
});
|
||||
|
||||
// V0.3.1 slice 13 walk-item 7 — load the actual shipped builtins
|
||||
// directory and assert [email protected] is in it. This is what
|
||||
// release-0.3.1/README.md's frontmatter declares; without the spec
|
||||
// present, the mission Topology tab falls back to the session-name
|
||||
// union (velocity-qa VM caught exactly this on tip ef3e94d).
|
||||
it("shipped builtins include [email protected] (mission Topology acceptance for slice 13)", () => {
|
||||
// directory and assert [email protected] projects to a coherent
|
||||
// linear graph. velocity-qa VM caught the missing-spec case on
|
||||
// ef3e94d; guard re-review on 10ed741 caught the role-collision
|
||||
// case (two steps sharing actor_role 'orch' → projector kept only
|
||||
// the first → orphan nodes). This test is the projection-shape
|
||||
// discriminator per banked feedback_poc_regression_must_discriminate:
|
||||
// exact edge chain + zero orphans + entry/terminal placement.
|
||||
it("shipped [email protected] projects to a 6-node linear chain with no orphans (mission Topology acceptance)", () => {
|
||||
const shippedDir = defaultBuiltinSpecsDir();
|
||||
const result = loadStarterWorkflowSpecs({ cache, builtinDir: shippedDir });
|
||||
expect(result.errors).toEqual([]);
|
||||
const row = cache.getByNameVersion("openrig-velocity", "1.0");
|
||||
expect(row).not.toBeNull();
|
||||
expect(row!.spec.steps.length).toBeGreaterThan(0);
|
||||
// Discriminate against the basic-loop spec (which is also in the
|
||||
// shipped dir): openrig-velocity must have a 'guard' role.
|
||||
expect(Object.keys(row!.spec.roles)).toContain("guard");
|
||||
|
||||
const graph = projectSpecGraph(row!.spec, null);
|
||||
|
||||
// Six declared steps; six projected nodes.
|
||||
expect(graph.nodes.map((n) => n.stepId)).toEqual([
|
||||
"dispatch",
|
||||
"refine",
|
||||
"implement",
|
||||
"guard-review",
|
||||
"vm-verify",
|
||||
"merge",
|
||||
]);
|
||||
|
||||
// Linear chain — exact edges (newer→older permitted only at the
|
||||
// terminal merge step which has no outgoing edge).
|
||||
expect(
|
||||
graph.edges.map((e) => `${e.fromStepId}→${e.toStepId}`),
|
||||
).toEqual([
|
||||
"dispatch→refine",
|
||||
"refine→implement",
|
||||
"implement→guard-review",
|
||||
"guard-review→vm-verify",
|
||||
"vm-verify→merge",
|
||||
]);
|
||||
|
||||
// Entry + terminal placement.
|
||||
const entryNodes = graph.nodes.filter((n) => n.isEntry).map((n) => n.stepId);
|
||||
expect(entryNodes).toEqual(["dispatch"]);
|
||||
const terminalNodes = graph.nodes.filter((n) => n.isTerminal).map((n) => n.stepId);
|
||||
expect(terminalNodes).toEqual(["merge"]);
|
||||
|
||||
// No orphans: every non-entry node has at least one incoming edge
|
||||
// and every non-terminal node has at least one outgoing edge.
|
||||
const incoming = new Set(graph.edges.map((e) => e.toStepId));
|
||||
const outgoing = new Set(graph.edges.map((e) => e.fromStepId));
|
||||
for (const node of graph.nodes) {
|
||||
if (!node.isEntry) {
|
||||
expect(incoming.has(node.stepId), `${node.stepId} has no incoming edge`).toBe(true);
|
||||
}
|
||||
if (!node.isTerminal) {
|
||||
expect(outgoing.has(node.stepId), `${node.stepId} has no outgoing edge`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user