Skip to content
Open
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b6b7179
FE-864: raise pi action timeout to 600s
kostandinang Jun 16, 2026
1928ea3
FE-878: TUI polish — global timer, lowercase wordmark, Static log str…
kostandinang Jun 16, 2026
4db4aa5
FE-878: light brigade taste (epic verdict) + serve (run complete)
kostandinang Jun 16, 2026
b0c06f0
FE-878: stream the agent's latest line as the wait heartbeat (Option A)
kostandinang Jun 16, 2026
eb93537
FE-878: per-tool/per-file heartbeat — instrument pi's coding tools
kostandinang Jun 16, 2026
320bff4
FE-879: lazy per-slice cook worktrees + shared node_modules
kostandinang Jun 16, 2026
5404e18
FE-879: update default Anthropic model
kostandinang Jun 16, 2026
2af79bd
FE-864: use Opus 4.6 for orchestration defaults
kostandinang Jun 16, 2026
ede7a9e
FE-878: structured epic→slice progress grid (ln-review improvement)
kostandinang Jun 16, 2026
d3693f5
FE-864: fail slice rows when writer pi aborts
kostandinang Jun 16, 2026
c2582d8
FE-843: Frontier setup — toolchain-profile-expansion plan entry + 3-c…
kostandinang Jun 10, 2026
9a4e333
FE-843: Data-driven profile registry + node-vitest/node-test/node-jes…
kostandinang Jun 10, 2026
2f43fd0
FE-843: Live + strict profile selection — flag ≫ spec ≫ bun, persiste…
kostandinang Jun 10, 2026
7067c7f
FE-843: Architect classifies the toolchain profile from spec prose
kostandinang Jun 10, 2026
7fa83a0
FE-864: Add agent-extension-host contract (dual-mode pi harness)
kostandinang Jun 15, 2026
b87e404
FE-864: Base the Arc-1 linear stack on agent-extension-host
kostandinang Jun 17, 2026
026fbd3
FE-867: agent-extension-host mode-neutral contract (slice 1)
kostandinang Jun 15, 2026
9d8a954
FE-867: tighten agent-extension-host neutrality & witness proofs
kostandinang Jun 15, 2026
e2c47e1
FE-871: co-locate generated tests in the repo's own test dir (slice 3)
kostandinang Jun 16, 2026
5e0f1cb
FE-871: monorepo-robust test-dir + workspace runner detection (slice 4)
kostandinang Jun 16, 2026
22355a3
FE-872: classify test-run failures as infra vs test (slice 1)
kostandinang Jun 15, 2026
ae9ff99
FE-872: unify test execution on one runner + verification seam (slice 4)
kostandinang Jun 16, 2026
692054a
FE-875: bound the app probe's HTTP calls so a hung app can't hang the…
kostandinang Jun 16, 2026
e6d9e6f
FE-876: integration oracle Half A — fold runProbe reachability into t…
kostandinang Jun 16, 2026
79a3aa8
FE-876: integration oracle Half B seam — reachability intent + inject…
kostandinang Jun 16, 2026
f4dc22d
FE-878: extract CLI presentation seam; migrate plan surface (slice 1a)
kostandinang Jun 16, 2026
1531793
FE-878: migrate cook surface to the presentation seam (slice 1b)
kostandinang Jun 16, 2026
007da0e
FE-878: Ink TUI presenter — egg logo + brigade tracker (slice 2a)
kostandinang Jun 16, 2026
33a73be
FE-878: brunch wordmark header in brand gradient; revert brigade to m…
kostandinang Jun 16, 2026
4e16ac0
FE-864: keep slice grid current during verification
kostandinang Jun 17, 2026
305cf2a
FE-864: clean up restack conflict artifacts
kostandinang Jun 17, 2026
040c24d
FE-864: serve --land merges promoted cook branch into active branch
kostandinang Jun 17, 2026
91a281d
FE-864: bump interviewer + cook agents to Opus 4.8
kostandinang Jun 17, 2026
b7b2e53
FE-864: use adaptive thinking + effort for the Opus 4.8 interviewer
kostandinang Jun 17, 2026
950894f
FE-878: surface failure reason on the grid + pinned halt summary
kostandinang Jun 17, 2026
ff0034c
FE-878: show per-slice attempt count on the grid
kostandinang Jun 17, 2026
33fd602
FE-878: format the slice attempt as n/max
kostandinang Jun 17, 2026
ae86861
FE-864: stop Opus 4.8 fragmenting ask_question into failed tool parts
kostandinang Jun 17, 2026
871ef08
FE-864: not-started evaluate gate + git-merge run-artifact composer
kostandinang Jun 17, 2026
8b044bc
FE-864: stop dev server reloading on .brunch cook worktrees
kostandinang Jun 17, 2026
f96b365
FE-864: drop duplicated monorepo detectProfile suite
kostandinang Jun 17, 2026
3ef81ca
FE-864: warn that serve --land is ignored for greenfield runs
kostandinang Jun 17, 2026
a68ed36
FE-864: paint a failed row when verify-epic's runPi throws
kostandinang Jun 17, 2026
f972bdd
FE-864: re-link shared node_modules into the merged epic tree
kostandinang Jun 17, 2026
b69e1ca
FE-864: drop duplicated withTestDir suite in project-profile.test.ts
kostandinang Jun 17, 2026
979c3e1
FE-864: align architect default model to claude-opus-4-8
kostandinang Jun 17, 2026
21e1653
FE-864: align recipe banner lexicon + exact out dir + verbose warning…
kostandinang Jun 17, 2026
a1bc039
FE-864: make pi timeout an idle deadline, not a wall-clock cap
kostandinang Jun 17, 2026
3f6ca8d
chore: apply oxfmt line-wrapping to orchestrator files
kostandinang Jun 17, 2026
7a7e1e5
FE-864: stop test-writer re-implementing the slice to validate its or…
kostandinang Jun 17, 2026
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Open http://localhost:5173.
| Variable | Required | Description |
|---|---|---|
| `ANTHROPIC_API_KEY` | Yes | Anthropic API key |
| `ANTHROPIC_MODEL` | No | Interviewer model (default: `claude-sonnet-4-20250514`) |
| `ANTHROPIC_MODEL` | No | Interviewer model (default: `claude-opus-4-6`) |
| `OBSERVER_MODEL` | No | Observer model (default: `claude-haiku-4-5-20251001`) |
| `BRUNCH_DB` | No | Override the default project-local SQLite path for dev workflows |
| `BRUNCH_PORT` | No | Backend port override |
Expand Down
2 changes: 1 addition & 1 deletion memory/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ The May 2026 intent-spec, multi-chat, changeset-ledger, prompt/context, and agen
4. `dogfood-spike` (ln-spike) — **(done — 2026-06-16)** ran a real brownfield cook (hand-authored 2-slice plan: feature + wiring, `node:http` app) against a throwaway git repo. **Verdict:** chain works end-to-end (CoW worktree, clean-tree gate, per-slice→`__epic__` merge composed the wiring, TDD red/green, working branch untouched); the agent wired the feature reachable and **self-authored a genuine boot-and-probe** integration test (imports the real entry, `listen(0)`, `http.get('/health')`, asserts not-404). Orphan did **not** reproduce — but reachability was **agent-discretion, not enforced** → confirms the *value* of `integration-oracle`/`app-runtime-probe` (independent, unshortcuttable reachability). Two refinements surfaced: the probe should own the boot mechanism (the agent had to invent a `.js→.ts` resolve hook), and dep-install was unexercised (zero-dep app). Bonus: the `Cannot find module` TDD red was handled as a test-red (not infra) — validates FE-872 slice 1 live.
5. `app-runtime-probe` — **(slices 1–2 landed — FE-875, `runProbe` + `buildProbeSpec`)** build + boot + exercise the host app; the concrete reachability mechanism `integration-oracle` depends on (without it, "reachable" collapses back to "a test that imports the module"). Slice 1: boot + HTTP probe + reachable/not-reachable/infra classification + teardown. Slice 2: harness-owned `ProbeSpec` resolution — `buildProbeSpec(ProbeTarget)` allocates a free ephemeral port and assembles ready/feature URLs from boot-argv + *paths*, so a hardcoded port can't collide under parallel cook (the boot test's hand-rolled port dance is now the production primitive it dogfoods). Stays off the dispatch seam: argv + paths are inputs cook-time grounding will supply; the harness owns only the port pick + URL/env assembly (loopback-only; best-effort ephemeral port with an acknowledged TOCTOU window, no retry framework). Every probe HTTP call (readiness poll + feature request) carries a per-call `AbortSignal.timeout` so a server that accepts a connection but never responds can't hang the probe (and the cook) past the deadline; timeouts are overridable for tests. Remaining: mode-awareness, integration-oracle gating (where the `ProbeTarget` argv/paths come from = `integration-oracle` #6).
6. `integration-oracle` — **(Half A + Half B seam landed — FE-876)** oracle asserts product reachability via `app-runtime-probe`. Half A (off-seam): `Epic.probe?: ProbeTarget` folds a `runProbe` result into the `verify-epic` verdict — after slices merge into `__epic__/<epicId>/`, the epic is `done` only when tests pass **and** the feature is reachable; `not-reachable` is the FE-800 orphan, `infra` is a harness fault. Probe gated behind tests passing (never boot a known-broken build); absent → unchanged unit verdict; reachability rides the existing `report.passed` routing. Half B seam: host-blind `Epic.reachability?: ReachabilityIntent` (architect-emittable, D160-K) + an injectable `ProbeGrounder` (`createPiActions({ groundProbe })`) that cook-time-resolves intent → concrete `ProbeTarget` by reading the worktree; `verify-epic` resolves via `probe ?? ground(reachability)`, a grounder that throws is an `infra` fault (visible, not a silent pass), intent without a grounder is an inert no-op. **Remaining (dispatch seam, lands atomically with the pi-harness contract):** the production `ProbeGrounder` (an `execute`-mode agent that reads the worktree) + architect emission of `reachability` intent — deferred together so intent is enforced the moment it's emitted (avoids perturbing the 3 reference fixtures). Runs in the FE-738 semantic lane. Promotes FE-800's integration-blind follow-on to a frontier. *(grounder impl depends on `agent-extension-host`)*
7. `brownfield-promotion` — **(landed — FE-877, `promoteBrownfieldRun`)** commit a completed brownfield cook result onto the repo's own `cook/<runId>` branch as one reviewable commit; extends FE-827's greenfield promotion to brownfield and closes the cook-codebase-mode follow-on (the result no longer sits uncommitted in the worktree). Git plumbing only (`commit-tree` + CAS `update-ref`, parent = the existing `cook/<runId>` base, throwaway index + external work-tree), so the user's active branch, working tree, and index are never touched; gitignored deps don't land. Reuses `promotionSourceDir` to compose the tree across slice layouts. Auto-runs on a completed brownfield cook (no `--out` needed); merging into the working branch stays the **user's** call. Unblocks FE-872's brownfield dep-delta capture.
7. `brownfield-promotion` — **(landed — FE-877, `promoteBrownfieldRun`)** commit a completed brownfield cook result onto the repo's own `cook/<runId>` branch as one reviewable commit; extends FE-827's greenfield promotion to brownfield and closes the cook-codebase-mode follow-on (the result no longer sits uncommitted in the worktree). Git plumbing only (`commit-tree` + CAS `update-ref`, parent = the existing `cook/<runId>` base, throwaway index + external work-tree), so the user's active branch, working tree, and index are never touched; gitignored deps don't land. Reuses `promotionSourceDir` to compose the tree across slice layouts. Auto-runs on a completed brownfield cook (no `--out` needed); merging into the working branch stays the **user's** call. Unblocks FE-872's brownfield dep-delta capture. **Follow-on (FE-864, `landCookBranch`):** `brunch serve --land` opt-in softens that default — after promotion it merges `cook/<runId>` into the repo's active branch as serve's final step, but refuses on a dirty tree / detached HEAD and aborts on conflict (cook branch always left intact). Plain `cook` and default `serve` are unchanged, so the "never freelance into the working branch" invariant holds unless the user explicitly asks.
8. `brunch-ship` — **(landed — FE-878, `brunch serve`)** one-shot `brunch serve <specId>` = `plan <specId>` then `cook --spec=<specId>` (cook reads the plan just emitted), no manual steps. Pure glue, no new orchestration: serve's `--out` is the *promote* target → cook (brownfield auto-promotes via FE-877 regardless), `--profile` stamps the plan, petrinaut/policy/retry flags forward to cook, `--verbose` to both; a failed plan short-circuits (nothing cooked). Testable units `parseServeArgs` + `runServe` (stages injected); db/snapshot wiring stays in `cli.ts`. Cook's `dir` is threaded from the resolved launch cwd (the dir the plan was written to) — `runCook` reads `opts.dir` raw, so serve must supply it rather than rely on the `parseCookArgs`-only default (R46). **Closes Arc 1.**

**Runtime umbrella + semantic substrate:**
Expand Down
36 changes: 36 additions & 0 deletions src/orchestrator/src/app-probe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,42 @@ describe('runProbe bounds its HTTP calls so a hung app cannot hang the probe', (
});
});

describe('runProbe bounds its HTTP calls so a hung app cannot hang the probe', () => {
Comment thread
kostandinang marked this conversation as resolved.
// A server that accepts connections (and the HTTP request) but never sends a
// response — the case the wall-clock deadline alone can't catch, because a
// bare `await fetch` would block forever between deadline checks.
const neverResponds = (readyRoutes: Record<string, number> = {}): string =>
`const http = require('node:http');\n` +
`const ready = ${JSON.stringify(readyRoutes)};\n` +
`http.createServer((req, res) => {\n` +
` if (ready[req.url] !== undefined) { res.writeHead(ready[req.url]); res.end('ok'); return; }\n` +
` /* otherwise: never respond */\n` +
`}).listen(Number(process.env.PORT), '127.0.0.1');\n`;

it('a ready path that accepts connections but never responds → infra within the deadline', async () => {
const spec = await buildProbeSpec({
boot: ['node', 'server.js'],
readyPath: '/health',
featurePath: '/feature',
});
const dir = sandbox(neverResponds());
const result = await runProbe(spec, dir, { readyTimeoutMs: 600, readyAttemptMs: 150 });
expect(result.kind).toBe('infra');
});

it('a booted app whose feature endpoint never responds → infra, not a hang', async () => {
const spec = await buildProbeSpec({
boot: ['node', 'server.js'],
readyPath: '/health',
featurePath: '/feature',
});
const dir = sandbox(neverResponds({ '/health': 200 }));
const result = await runProbe(spec, dir, { requestTimeoutMs: 300 });
expect(result.kind).toBe('infra');
expect(result.output).toMatch(/feature probe request failed/);
});
});

describe('runProbe tears the boot process down', () => {
it('the booted app is no longer listening after the probe returns', async () => {
const { spec, dir } = await specFor({ '/health': 200, '/feature': 200 });
Expand Down
63 changes: 47 additions & 16 deletions src/orchestrator/src/cook-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { createPiActions } from './pi-actions.js';
import { loadPlan } from './plan-loader.js';
import type { CookBus } from './presenter.js';
import { resolveToolchain } from './project-profile.js';
import { promoteBrownfieldRun, promoteGreenfieldRun } from './promote-run.js';
import { landCookBranch, promoteBrownfieldRun, promoteGreenfieldRun } from './promote-run.js';
import { parseSpecId, resolveLatestSpecPlanPath, specPlanPath, specsRootDir } from './spec-plan-paths.js';
import { ToolchainTestRunner } from './test-runner.js';
import type { Plan, PlanMode } from './types.js';
Expand Down Expand Up @@ -49,6 +49,12 @@ export type CookOptions = {
outDir?: string;
/** Allow promoting into a non-empty target (otherwise refused). */
force: boolean;
/**
* Brownfield only: after promotion, merge `cook/<runId>` into the repo's active
* branch as the final step. Set by `serve --land`; plain `cook` never sets it,
* keeping promotion's hands-off default intact unless the user opts in.
*/
landBranch?: boolean;
/**
* Explicit specification id whose emitted plan (under
* `<dir>/.brunch/cook/specs/<id>/plan.yaml`) should be cooked.
Expand Down Expand Up @@ -120,6 +126,10 @@ export function parseCookArgs(args: string[]): CookOptions {
verbose = true;
} else if (!arg.startsWith('-')) {
dir = arg;
} else {
// Reject unknown flags instead of silently ignoring them (e.g. --spec-id
// is not a flag; the spec selector is --spec=<id>).
throw new Error(`Unknown flag "${arg}". Run "brunch --help" for cook usage.`);
}
}

Expand Down Expand Up @@ -426,28 +436,21 @@ export async function runCook(opts: CookOptions, bus: CookBus): Promise<void> {
cliFlag: opts.petrinautUrl,
env: { PETRINAUT_URL: process.env.PETRINAUT_URL },
});
if ('error' in resolvedUrl) {
line(resolvedUrl.error);
process.exit(1);
}
// Throw, never process.exit — the caller (withCookBus) must dispose the
// presenter (unmount Ink) before the error is printed, or the TUI hangs.
if ('error' in resolvedUrl) throw new Error(resolvedUrl.error);
petrinautUrl = resolvedUrl.url;
streamPort = resolvePetrinautStreamPort({ PORT: process.env.PORT });
}

const resolved = resolveCookPlan(opts.dir, opts.specId);
if (resolved.kind === 'error') {
line(resolved.message);
process.exit(1);
}
if (resolved.kind === 'error') throw new Error(resolved.message);

const plan = loadPlan(resolved.planPath);

// Worktree strategy follows the plan's spec-derived mode, not its location.
const sandbox = resolveSandboxPlan(plan.mode, resolved.sourceDir);
if (sandbox.kind === 'error') {
line(sandbox.message);
process.exit(1);
}
if (sandbox.kind === 'error') throw new Error(sandbox.message);

// Single shared tree only for serial greenfield (parallel would race on it);
// every other case isolates slices per-slice.
Expand Down Expand Up @@ -483,6 +486,13 @@ export async function runCook(opts: CookOptions, bus: CookBus): Promise<void> {
// Seed the presenter's elapsed clock; per-action progress carries no
// pre-formatted timing — the presenter owns it (I136-K).
bus.emit({ kind: 'cook-start', runStart });
// Seed the slice grid up front so queued work is visible before it starts.
bus.emit({
kind: 'run-shape',
epics: plan.epics.map((e) => ({ id: e.id })),
slices: plan.slices.map((s) => ({ id: s.id, epicId: s.epic_id })),
maxRetries: opts.maxRetries,
});
const actions = createPiActions({
verbose: opts.verbose,
emit: (event) => bus.emit(event),
Expand Down Expand Up @@ -511,6 +521,7 @@ export async function runCook(opts: CookOptions, bus: CookBus): Promise<void> {
reports,
testRunner,
policy: { maxRetries: opts.maxRetries },
emit: (event) => bus.emit(event),
sandboxMode: sandbox.kind === 'codebase' ? 'codebase' : 'fixture',
sliceLayout,
runId,
Expand Down Expand Up @@ -569,9 +580,26 @@ export async function runCook(opts: CookOptions, bus: CookBus): Promise<void> {
runId,
}),
);
line(
` ✓ promoted → ${promoted.branch} @ ${promoted.commit.slice(0, 8)} (merge it into your branch when ready)`,
);
if (opts.landBranch) {
const landed = promoting(`landing → ${promoted.branch} into the active branch`, () =>
landCookBranch({ sourceDir: sandbox.sourceDir, runId }),
);
if (landed.kind === 'landed') {
line(` ✓ promoted + landed ${promoted.branch} onto ${landed.branch} (${landed.mode})`);
} else if (landed.kind === 'refused') {
line(
` ✓ promoted → ${promoted.branch} @ ${promoted.commit.slice(0, 8)} (not landed: working tree ${landed.reason}; merge it when ready)`,
);
} else {
line(
` ✓ promoted → ${promoted.branch} @ ${promoted.commit.slice(0, 8)} (not landed: merge conflict on ${landed.branch}; resolve with \`git merge ${promoted.branch}\`)`,
);
}
} else {
line(
` ✓ promoted → ${promoted.branch} @ ${promoted.commit.slice(0, 8)} (merge it into your branch when ready)`,
);
}
Comment thread
kostandinang marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
line('');
} catch (err) {
line(` ✗ promotion failed: ${err instanceof Error ? err.message : String(err)}`);
Expand Down Expand Up @@ -615,6 +643,9 @@ export async function runCook(opts: CookOptions, bus: CookBus): Promise<void> {
}
}

// Run complete (after promotion) — lights the brigade's `serve` phase, or
// pins a halt summary with the reason when it did not complete.
bus.emit({ kind: 'cook-done', ok, ...(result.reason ? { reason: result.reason } : {}) });
recordCookExitStatus(ok);
return;
} finally {
Expand Down
19 changes: 16 additions & 3 deletions src/orchestrator/src/cow-copy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { spawnSync } from 'node:child_process';
import { cpSync, existsSync, readdirSync } from 'node:fs';
import { cpSync, existsSync, readdirSync, symlinkSync } from 'node:fs';
import { join, resolve } from 'node:path';

/**
Expand All @@ -23,23 +23,36 @@ export function cowCopy(src: string, dest: string): void {
/** Top-level names skipped when CoW-copying into cook sandboxes. */
export const COW_COPY_DEFAULT_EXCLUDE = new Set(['.git', '.brunch']);

const NO_SYMLINKS: ReadonlySet<string> = new Set();

/**
* CoW-copy top-level entries from `sourceDir` that are absent in `destDir`
* Provision top-level entries from `sourceDir` that are absent in `destDir`
* (untracked/gitignored dirs like `node_modules/`, `dist/`). Skips names in
* `exclude` and entries already present in the destination (typically tracked
* files materialized by `git worktree add`).
*
* Names in `symlink` are linked to the source entry instead of copied — used to
* share a single read-only `node_modules/` across slice sandboxes rather than
* paying a CoW copy per slice. Everything else is CoW-copied (lazy on APFS /
* reflink filesystems, deep copy otherwise).
*/
export function copyMissingTopLevelEntries(
sourceDir: string,
destDir: string,
exclude: ReadonlySet<string> = COW_COPY_DEFAULT_EXCLUDE,
symlink: ReadonlySet<string> = NO_SYMLINKS,
): void {
const source = resolve(sourceDir);
const dest = resolve(destDir);
for (const entry of readdirSync(source)) {
if (exclude.has(entry)) continue;
const destPath = join(dest, entry);
if (existsSync(destPath)) continue;
cowCopy(join(source, entry), destPath);
const sourcePath = join(source, entry);
if (symlink.has(entry)) {
symlinkSync(sourcePath, destPath);
} else {
Comment thread
kostandinang marked this conversation as resolved.
cowCopy(sourcePath, destPath);
}
}
}
19 changes: 19 additions & 0 deletions src/orchestrator/src/engine-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { createNetFolding } from './petrinaut-fold.js';
import type { SdcpnFile } from './petrinaut-sdcpn.js';
import { type BrunchExecutionExportFrame, createPetrinautStreamBus } from './petrinaut-stream-bus.js';
import { reduceBrunchExecutionExport } from './petrinaut-stream-export.js';
import type { CookEvent } from './presenter/events.js';
import { InMemoryReportSink } from './report-sink.js';
import type { ActionContext, ActionHandlers, OrchestratorInput, Plan, RunCtx, TestRunner } from './types.js';

Expand Down Expand Up @@ -262,6 +263,24 @@ describe('Engine contract test #1 — single epic, single slice, happy path', ()
]);
});

it('emits slice grid events around net-level test runs', async () => {
const fakes = createFakes();
const events: CookEvent[] = [];
await create().run({
plan: simplePlan,
sandboxDir: '/tmp/fake',
actions: fakes.actions,
reports: fakes.reports,
testRunner: fakes.testRunner,
policy: { maxRetries: 3 },
emit: (event) => events.push(event),
});
expect(events.filter((e) => e.kind === 'slice')).toEqual([
{ kind: 'slice', id: 'slice-1', epicId: 'epic-1', status: 'running', step: 'verify' },
{ kind: 'slice', id: 'slice-1', epicId: 'epic-1', status: 'passed' },
]);
});

it('report sink contains expected lines', async () => {
const fakes = createFakes();
await create().run({
Expand Down
Loading
Loading