Skip to content
Open
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
8 changes: 5 additions & 3 deletions fixtures/txt/plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ epics:
- id: scaffolding
summary: "CLI scaffolding"
depends_on: []
verification:
- kind: integration-test
target: "tests/cli-scaffolding.integration.test.ts"
# Slice unit tests cover version-flag and help-flag; epic verify deferred until
# text-ops slices exist (cli-scaffolding integration spans the full CLI).
verification: []

- id: text-ops
summary: "Text operations"
depends_on: [scaffolding]
verification:
- kind: integration-test
target: "tests/cli-scaffolding.integration.test.ts"
- kind: integration-test
target: "tests/text-ops-pipe.integration.test.ts"

Expand Down
3 changes: 2 additions & 1 deletion memory/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ The May 2026 intent-spec, multi-chat, changeset-ledger, prompt/context, and agen

### Recently Completed

- `petri-epic-verification-merge` — `verify-epic` now runs against a freshly-merged `<parentSandboxDir>/__epic__/<epicId>/` built from completed slice worktrees (declaration-order wins on path collisions; conflicts surfaced via `epic-sandbox-merged` event). Unblocks multi-slice `cook` runs. Follows FE-743.
- `petri-parallel-execution` (FE-743) — parallel firing policy, shared resource pool tokens, worktree-per-slice isolation. Decision gate passed: parallel measurably beats serial on wall clock for multi-slice plans. Follows `petri-semantic-lanes` (FE-738).
- `petri-semantic-lanes` (FE-738) — two-lane subnet, compiler topology/wiring split, engine factory, semantic rework budget, §7 events. PR #148. Criterion (5) stale-graph deferred → `petri-graph-compilation`.

Expand Down Expand Up @@ -104,7 +105,6 @@ The May 2026 intent-spec, multi-chat, changeset-ledger, prompt/context, and agen
- **Open design constraints (from PR #143 / FE-743 review):**
- **Declarative output arcs:** Current topology declares only input places; output routing lives in fire closures (conditional on report payloads). FE-738's `HandlerDescriptor` declares candidate outputs (`onTrue`/`onFalse`/`onPass`/`onFail`) but selection is runtime. This limits formal analyzability (reachability, deadlock detection, simulation) to input-side structure. Phase 3 should move conditional routing into the topology — explicit guard predicates + declared output arcs per branch — so the compiled net is formally analyzable end-to-end.
- **Token state enrichment:** Open question whether more metadata should move from reports into tokens (richer typed token payloads per spec §3). FE-738 added `reworkCount`, FE-743 added pool tokens with `agentPoolSize`, but the boundary between control state (tokens) and substantive handoff state (reports) is a design choice this frontier needs to resolve as the token taxonomy gets richer.
- **Epic verification sandbox scope:** Per-slice sandbox isolation means `verify-epic` can't see all slices' artifacts. Currently `verify-epic` falls back to the parent sandbox dir. The production fix is to merge per-slice sandboxes into an epic-scoped dir before epic verification runs.
- **Acceptance:** TBD — depends on FE-700 relation-policy shape.
- **Verification:** Compiled-net topology tests against plan-graph fixtures; reachability assertions for relation-policy-derived gates; comparison of compiled vs hand-authored net shapes.
- **Traceability:** Requirements 46–50; spec §5 (relation-policy compilation), §6 (transition contracts).
Expand Down Expand Up @@ -505,6 +505,7 @@ TRACK F — Petri-net execution substrate (umbrella H-6476)
orchestrator-poc (Phase 0: compiler extraction — done)
└──→ petri-semantic-lanes (Phase 1: two-lane subnet + §7 events — done)
└──→ petri-parallel-execution (Phase 2: concurrent firing + resource pools — done)
├──→ petri-epic-verification-merge (hardening: merge slice worktrees for verify-epic — done)
└──→ petri-graph-compilation (Phase 3: compile from plan-graph + relation policy)
├──→ depends on intent-graph-semantics (FE-700) for relation-policy gates
└──→ petri-simulation-oracle (Phase 4: reachability, deadlock, resume)
Expand Down
1 change: 1 addition & 0 deletions memory/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ Each invariant is a formalization candidate: the property is stated in human lan
| I121-K | Both orchestrator engines (`proc` and `petri`) pass the same contract test suite with identical observable behavior. | contract tests with fake agents/runner | Requirements 46, 47; D155-K |
| I122-K | Orchestrator event content lives in `reports.jsonl`; petri engine tokens carry only `{ reportId, sliceId, epicId }` pointers. Proc engine may pass data through normal function calls — the shared seam is inputs and outputs. | contract tests | Requirement 48; D156-K |
| I123-K | Worktree isolation holds — fixture directory and source repo are never mutated by an orchestrator run; worktree is cwd-scoped at `<cwd>/.cook/runs/<runId>/worktree/`. | integration tests, worktree.test.ts | Requirement 49; D159-K |
| I124-K | Epic verification runs against a freshly-rebuilt `<parentSandboxDir>/__epic__/<epicId>/` dir holding the deterministic merge of its completed slices' worktrees (later slices in plan declaration order overwrite earlier ones on path collisions; collisions are reported via the `epic-sandbox-merged` event). Per-slice worktrees are not mutated by the merge. | epic-sandbox-merge.test.ts, engine-contract.test.ts | Requirement 49; D159-K |

## Future Direction Register

Expand Down
122 changes: 99 additions & 23 deletions src/orchestrator/src/engine-contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { describe, expect, it } from 'vitest';

import { createOrchestrator } from './engine.js';
Expand Down Expand Up @@ -1080,7 +1084,7 @@ describe('Engine contract test #13 — resource pool bounds concurrency', () =>
// ---------------------------------------------------------------------------

describe('Adapter: sandbox-per-slice isolation', () => {
it('each action handler receives a per-slice sandboxDir', async () => {
it('each action handler receives a per-slice sandboxDir (parallel-safe)', async () => {
const sandboxDirs = new Map<string, string>();

const fakes = createFakes({ evalSequence: [true], semanticResults: [true] });
Expand All @@ -1103,57 +1107,129 @@ describe('Adapter: sandbox-per-slice isolation', () => {
});

expect(result.status).toBe('completed');
// Every action should receive sandboxDir = /tmp/run/<sliceId>
for (const [key, dir] of sandboxDirs) {
const sliceId = key.split(':')[0]!;
expect(dir).toBe(`/tmp/run/${sliceId}`);
expect(simplePlan.slices.find((s) => s.id === sliceId)?.epic_id).toBe('epic-1');
}
// At least evaluate-done and assess-semantic were called
expect(sandboxDirs.size).toBeGreaterThanOrEqual(2);
});

it('verify-epic receives the parent sandboxDir (not per-slice)', async () => {
const verifyPlan: Plan = {
epics: [
it('parallel slices in the same epic receive distinct sandboxDirs', async () => {
const parallelPlan: Plan = {
epics: [{ id: 'e1', summary: 'Three independent slices', depends_on: [], verification: [] }],
slices: [
{
id: 'ev',
summary: 'Verified',
id: 'p1',
epic_id: 'e1',
definition: 'S1',
depends_on: [],
verification: [{ kind: 'integration-test', target: 't' }],
verification: [{ kind: 'unit-test', target: 't1' }],
},
],
slices: [
{
id: 'sv',
epic_id: 'ev',
definition: 'S',
id: 'p2',
epic_id: 'e1',
definition: 'S2',
depends_on: [],
verification: [{ kind: 'unit-test', target: 't' }],
verification: [{ kind: 'unit-test', target: 't2' }],
},
{
id: 'p3',
epic_id: 'e1',
definition: 'S3',
depends_on: [],
verification: [{ kind: 'unit-test', target: 't3' }],
},
],
};

let verifyEpicSandboxDir = '';
const fakes = createFakes({ evalSequence: [true], semanticResults: [true], verifyEpicResult: true });
const sandboxDirs = new Set<string>();
const fakes = createFakes({ evalSequence: [true], semanticResults: [true] });
const trackingActions: ActionHandlers = {};
for (const [key, handler] of Object.entries(fakes.actions)) {
trackingActions[key] = async (ctx: ActionContext) => {
if (key === 'verify-epic') verifyEpicSandboxDir = ctx.sandboxDir;
sandboxDirs.add(ctx.sandboxDir);
return handler!(ctx);
};
}

const result = await createOrchestrator('serial').run({
plan: verifyPlan,
sandboxDir: '/tmp/run',
const result = await createOrchestrator('parallel').run({
plan: parallelPlan,
sandboxDir: '/tmp/parallel-run',
actions: trackingActions,
reports: fakes.reports,
testRunner: fakes.testRunner,
policy: { maxRetries: 3 },
});

expect(result.status).toBe('completed');
// verify-epic gets the parent sandbox, not /tmp/run/sv
expect(verifyEpicSandboxDir).toBe('/tmp/run');
expect(sandboxDirs.size).toBeGreaterThan(1);
for (const dir of sandboxDirs) {
expect(dir.startsWith('/tmp/parallel-run/')).toBe(true);
}
});

it('verify-epic receives a merged epic sandbox under <parent>/__epic__/<epicId>/ (not slice worktree, not parent)', async () => {
const verifyPlan: Plan = {
epics: [
{
id: 'ev',
summary: 'Verified',
depends_on: [],
verification: [{ kind: 'integration-test', target: 't' }],
},
],
slices: [
{
id: 'sv',
epic_id: 'ev',
definition: 'S',
depends_on: [],
verification: [{ kind: 'unit-test', target: 't' }],
},
],
};

const parent = mkdtempSync(join(tmpdir(), 'cook-ec-'));
try {
// Seed the slice worktree with a file so the merge has something to copy.
mkdirSync(join(parent, 'sv'), { recursive: true });
writeFileSync(join(parent, 'sv', 'slice-marker.txt'), 'from-slice-sv');

let verifyEpicSandboxDir = '';
const fakes = createFakes({ evalSequence: [true], semanticResults: [true], verifyEpicResult: true });
const trackingActions: ActionHandlers = {};
for (const [key, handler] of Object.entries(fakes.actions)) {
trackingActions[key] = async (ctx: ActionContext) => {
if (key === 'verify-epic') verifyEpicSandboxDir = ctx.sandboxDir;
return handler!(ctx);
};
}

const result = await createOrchestrator('serial').run({
plan: verifyPlan,
sandboxDir: parent,
actions: trackingActions,
reports: fakes.reports,
testRunner: fakes.testRunner,
policy: { maxRetries: 3 },
});

expect(result.status).toBe('completed');
expect(verifyEpicSandboxDir).toBe(join(parent, '__epic__', 'ev'));
// Merge produced a real dir holding the slice worktree seed file.
expect(existsSync(join(verifyEpicSandboxDir, 'slice-marker.txt'))).toBe(true);

// An epic-sandbox-merged event was appended before verify-epic.
const merged = fakes.reports.getAll().find((r) => r.event === 'epic-sandbox-merged');
expect(merged).toBeDefined();
expect(merged?.payload).toMatchObject({
epicSandboxDir: join(parent, '__epic__', 'ev'),
sliceIds: ['sv'],
conflicts: [],
});
} finally {
rmSync(parent, { recursive: true, force: true });
}
});
});
Loading
Loading