Skip to content

Commit cfcf84a

Browse files
feat: add first-class workflow profiles
1 parent 8e471fb commit cfcf84a

16 files changed

Lines changed: 495 additions & 23 deletions

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,19 @@ Every command supports `--json` where noted. Global flags: `-q`/`--quiet`, `--ti
8484
| Command | Description |
8585
|---------|-------------|
8686
| `ov init` | Initialize `.overstory/` and bootstrap os-eco tools (`--yes`, `--name`, `--tools`, `--skip-mulch`, `--skip-seeds`, `--skip-canopy`, `--skip-onboard`, `--json`) |
87-
| `ov sling <task-id>` | Spawn a worker agent (`--capability`, `--name`, `--spec`, `--files`, `--parent`, `--depth`, `--skip-scout`, `--skip-review`, `--max-agents`, `--dispatch-max-agents`, `--skip-task-check`, `--no-scout-check`, `--runtime`, `--base-branch`, `--profile`, `--json`) |
87+
| `ov sling <task-id>` | Spawn a worker agent (`--capability`, `--name`, `--spec`, `--files`, `--parent`, `--depth`, `--skip-scout`, `--skip-review`, `--max-agents`, `--dispatch-max-agents`, `--skip-task-check`, `--no-scout-check`, `--runtime`, `--base-branch`, `--profile`, `--workflow`, `--json`) |
8888
| `ov stop <agent-name>` | Terminate a running agent (`--clean-worktree`, `--json`) |
8989
| `ov prime` | Load context for orchestrator/agent (`--agent`, `--compact`) |
90-
| `ov spec write <task-id>` | Write a task specification (`--body`) |
90+
| `ov spec write <task-id>` | Write a task specification (`--body`, `--workflow`, `--openspec`) |
9191
| `ov discover` | Discover a brownfield codebase via coordinator-driven scout swarm (`--skip`, `--name`, `--attach`, `--watchdog`, `--json`) |
92+
| `ov workflow start <workflow>` | Start the coordinator in `delivery` or `co-creation` mode |
9293
| `ov update` | Refresh `.overstory/` managed files from installed package (`--agents`, `--manifest`, `--hooks`, `--dry-run`, `--json`) |
9394

9495
### Coordination
9596

9697
| Command | Description |
9798
|---------|-------------|
98-
| `ov coordinator start` | Start persistent coordinator agent (`--attach`/`--no-attach`, `--watchdog`, `--monitor`, `--profile`) |
99+
| `ov coordinator start` | Start persistent coordinator agent (`--attach`/`--no-attach`, `--watchdog`, `--monitor`, `--profile`, `--workflow`) |
99100
| `ov coordinator stop` | Stop coordinator |
100101
| `ov coordinator status` | Show coordinator state |
101102
| `ov coordinator send` | Fire-and-forget message to coordinator (`--subject`) |

agents/ov-co-creation.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Decision artifacts come before code. Deliverables in order:
3737
4. **Code and tests**: Implementation proceeds after decision artifacts are approved. Code must be clean, follow project conventions, and include automated tests.
3838
5. **Quality gates**: All lints, type checks, and tests must pass before reporting completion.
3939

40+
When the project uses OpenSpec, place durable co-creation artifacts under `openspec/changes/<task-id>/`. Overstory's co-creation workflow writes task specs to `openspec/changes/<task-id>/tasks.md` by default; keep option memos, ADRs, and milestone notes alongside that change set.
41+
4042
Do not write implementation code before decisions are resolved. The human reviews and approves decision documents; implementation follows approval.
4143

4244
## completion-criteria

agents/ov-delivery.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
name: ov-delivery
3+
description: Default delivery workflow profile — autonomous, hands-off execution
4+
---
5+
6+
## propulsion-principle
7+
8+
Read your assignment. Execute immediately. Do not ask for confirmation, do not propose a plan and wait for approval, do not summarize back what you were told. Start working within your first tool calls. The human gave you work because they want it done, not discussed. Assess the task, choose the right approach, and begin. If you need to explore first, explore. If you can implement directly, implement. Action is the default — hesitation is the exception.
9+
10+
## escalation-policy
11+
12+
Handle routine decisions autonomously. You have the context, the tools, and the expertise to make implementation choices without checking in. Escalate only when:
13+
14+
- The task is genuinely ambiguous and multiple valid interpretations would lead to significantly different outcomes.
15+
- You discover a risk that could cause data loss, security issues, or breaking changes beyond your scope.
16+
- You are blocked by a dependency outside your control.
17+
- The scope of work has grown significantly beyond what was originally described.
18+
19+
Do not escalate for: naming choices, implementation approach within spec, test strategy, file organization, or any decision you can make and verify yourself. When you do escalate, be specific: state what you found, what the options are, and what you recommend.
20+
21+
## artifact-expectations
22+
23+
Your primary deliverable is working software. Every task completion should include:
24+
25+
- **Code**: Clean, tested implementation that follows project conventions.
26+
- **Tests**: Automated tests that verify the new behavior.
27+
- **Quality gates**: All lints, type checks, and tests must pass before you report completion.
28+
29+
Documentation updates are expected only when the change affects public APIs, configuration, or user-facing behavior.
30+
31+
## completion-criteria
32+
33+
Work is complete when all of the following are true:
34+
35+
- All quality gates pass.
36+
- Changes are committed to the appropriate branch.
37+
- Any issues tracked in the task system are updated or closed.
38+
- A completion signal has been sent to the appropriate recipient.
39+
40+
Do not declare completion prematurely. Run the quality gates yourself — do not assume they pass.
41+
42+
## human-role
43+
44+
The human operates in a hands-off mode. They provide objectives and review results — they do not micromanage execution.
45+
46+
- **No real-time supervision.** Make decisions and proceed.
47+
- **Post-completion review.** The human reviews diffs, test results, and summaries after you report done.
48+
- **Minimal interaction.** Questions to the human should be rare and high-signal.
49+
- **Trust in autonomy.** The human chose automated delivery because they trust the system to execute.

src/commands/completions.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import {
1212
} from "./completions.ts";
1313

1414
describe("COMMANDS array", () => {
15-
it("should have exactly 33 commands", () => {
16-
expect(COMMANDS).toHaveLength(33);
15+
it("should have exactly 34 commands", () => {
16+
expect(COMMANDS).toHaveLength(34);
1717
});
1818

1919
it("should include all expected command names", () => {
@@ -38,6 +38,7 @@ describe("COMMANDS array", () => {
3838
expect(names).toContain("metrics");
3939
expect(names).toContain("spec");
4040
expect(names).toContain("coordinator");
41+
expect(names).toContain("workflow");
4142
expect(names).toContain("supervisor");
4243
expect(names).toContain("hooks");
4344
expect(names).toContain("monitor");
@@ -62,7 +63,7 @@ describe("generateBash", () => {
6263
expect(script).toContain("_init_completion");
6364
});
6465

65-
it("should include all 33 command names", () => {
66+
it("should include all 34 command names", () => {
6667
const script = generateBash();
6768
for (const cmd of COMMANDS) {
6869
expect(script).toContain(cmd.name);
@@ -96,7 +97,7 @@ describe("generateZsh", () => {
9697
expect(script).toContain("_arguments");
9798
});
9899

99-
it("should include all 33 command names", () => {
100+
it("should include all 34 command names", () => {
100101
const script = generateZsh();
101102
for (const cmd of COMMANDS) {
102103
expect(script).toContain(cmd.name);
@@ -126,7 +127,7 @@ describe("generateFish", () => {
126127
expect(script).toContain("__fish_use_subcommand");
127128
});
128129

129-
it("should include all 33 command names", () => {
130+
it("should include all 34 command names", () => {
130131
const script = generateFish();
131132
for (const cmd of COMMANDS) {
132133
expect(script).toContain(cmd.name);

src/commands/completions.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ export const COMMANDS: readonly CommandDef[] = [
9292
{ name: "--max-agents", desc: "Max children per lead", takesValue: true },
9393
{ name: "--dispatch-max-agents", desc: "Per-lead max agents ceiling", takesValue: true },
9494
{ name: "--runtime", desc: "Runtime adapter", takesValue: true },
95+
{
96+
name: "--workflow",
97+
desc: "Workflow profile alias",
98+
takesValue: true,
99+
values: ["delivery", "co-creation"],
100+
},
95101
{ name: "--json", desc: "JSON output" },
96102
{ name: "--help", desc: "Show help" },
97103
],
@@ -338,6 +344,13 @@ export const COMMANDS: readonly CommandDef[] = [
338344
flags: [
339345
{ name: "--body", desc: "Spec content", takesValue: true },
340346
{ name: "--agent", desc: "Agent attribution", takesValue: true },
347+
{
348+
name: "--workflow",
349+
desc: "Workflow profile alias",
350+
takesValue: true,
351+
values: ["delivery", "co-creation"],
352+
},
353+
{ name: "--openspec", desc: "Write to openspec tasks.md" },
341354
{ name: "--help", desc: "Show help" },
342355
],
343356
},
@@ -358,6 +371,13 @@ export const COMMANDS: readonly CommandDef[] = [
358371
{ name: "--attach", desc: "Attach to tmux session" },
359372
{ name: "--no-attach", desc: "Do not attach to tmux session" },
360373
{ name: "--watchdog", desc: "Auto-start watchdog daemon" },
374+
{ name: "--monitor", desc: "Auto-start Tier 2 monitor agent" },
375+
{
376+
name: "--workflow",
377+
desc: "Workflow profile alias",
378+
takesValue: true,
379+
values: ["delivery", "co-creation"],
380+
},
361381
{ name: "--json", desc: "JSON output" },
362382
],
363383
},
@@ -373,6 +393,24 @@ export const COMMANDS: readonly CommandDef[] = [
373393
},
374394
],
375395
},
396+
{
397+
name: "workflow",
398+
desc: "Start a coordinator in a workflow mode",
399+
flags: [{ name: "--help", desc: "Show help" }],
400+
subcommands: [
401+
{
402+
name: "start",
403+
desc: "Start workflow-mode coordination",
404+
flags: [
405+
{ name: "--attach", desc: "Attach to tmux session" },
406+
{ name: "--no-attach", desc: "Do not attach to tmux session" },
407+
{ name: "--watchdog", desc: "Auto-start watchdog daemon" },
408+
{ name: "--monitor", desc: "Auto-start Tier 2 monitor agent" },
409+
{ name: "--json", desc: "JSON output" },
410+
],
411+
},
412+
],
413+
},
376414
{
377415
name: "supervisor",
378416
desc: "[DEPRECATED] Per-project supervisor agent",

src/commands/coordinator.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,22 @@ describe("startCoordinator", () => {
534534
expect(session?.runId).toBe(fileRunId);
535535
});
536536

537+
test("maps --workflow co-creation to OVERSTORY_PROFILE env", async () => {
538+
const { deps, calls } = makeDeps();
539+
const originalSleep = Bun.sleep;
540+
Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
541+
542+
try {
543+
await captureStdout(() =>
544+
coordinatorCommand(["start", "--workflow", "co-creation", "--no-attach"], deps),
545+
);
546+
} finally {
547+
Bun.sleep = originalSleep;
548+
}
549+
550+
expect(calls.createSession[0]?.env?.OVERSTORY_PROFILE).toBe("ov-co-creation");
551+
});
552+
537553
test("deploys hooks to project root .claude/settings.local.json", async () => {
538554
const { deps } = makeDeps();
539555
const originalSleep = Bun.sleep;
@@ -1527,6 +1543,18 @@ describe("watchdog integration", () => {
15271543
expect(output).toContain("--watchdog");
15281544
expect(output).toContain("watchdog");
15291545
});
1546+
1547+
test("start help text includes --workflow flag", async () => {
1548+
const cmd = createCoordinatorCommand({});
1549+
for (const sub of cmd.commands) {
1550+
sub.exitOverride();
1551+
}
1552+
const output = await captureStdout(async () => {
1553+
await cmd.parseAsync(["start", "--help"], { from: "user" }).catch(() => {});
1554+
});
1555+
expect(output).toContain("--workflow");
1556+
expect(output).toContain("co-creation");
1557+
});
15301558
});
15311559
});
15321560

src/commands/coordinator.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { createRunStore, createSessionStore } from "../sessions/store.ts";
2929
import { resolveBackend, trackerCliName } from "../tracker/factory.ts";
3030
import type { AgentSession } from "../types.ts";
3131
import { isProcessRunning } from "../watchdog/health.ts";
32+
import { resolveProfileName } from "../workflow.ts";
3233
import type { SessionState } from "../worktree/tmux.ts";
3334
import {
3435
capturePaneContent,
@@ -300,6 +301,7 @@ export interface CoordinatorSessionOptions {
300301
watchdog: boolean;
301302
monitor: boolean;
302303
profile?: string;
304+
workflow?: string;
303305
/** Override coordinator name (default: "coordinator"). */
304306
coordinatorName?: string;
305307
/** Custom beacon builder. Receives tracker CLI name, returns beacon string. */
@@ -331,12 +333,14 @@ export async function startCoordinatorSession(
331333
watchdog: watchdogFlag,
332334
monitor: monitorFlag,
333335
profile: profileFlag,
336+
workflow: workflowFlag,
334337
coordinatorName: coordinatorNameOpt,
335338
beaconBuilder: beaconBuilderOpt,
336339
} = opts;
337340

338341
const coordinatorName = coordinatorNameOpt ?? COORDINATOR_NAME;
339342
const beaconBuilder = beaconBuilderOpt ?? buildCoordinatorBeacon;
343+
const effectiveProfile = resolveProfileName(profileFlag ?? workflowFlag);
340344

341345
if (isRunningAsRoot()) {
342346
throw new AgentError(
@@ -449,13 +453,13 @@ export async function startCoordinatorSession(
449453
env: {
450454
...runtime.buildEnv(resolvedModel),
451455
OVERSTORY_AGENT_NAME: coordinatorName,
452-
...(profileFlag ? { OVERSTORY_PROFILE: profileFlag } : {}),
456+
...(effectiveProfile ? { OVERSTORY_PROFILE: effectiveProfile } : {}),
453457
},
454458
});
455459
const pid = await tmux.createSession(tmuxSession, projectRoot, spawnCmd, {
456460
...runtime.buildEnv(resolvedModel),
457461
OVERSTORY_AGENT_NAME: coordinatorName,
458-
...(profileFlag ? { OVERSTORY_PROFILE: profileFlag } : {}),
462+
...(effectiveProfile ? { OVERSTORY_PROFILE: effectiveProfile } : {}),
459463
});
460464

461465
// Create a run for this coordinator session BEFORE recording the session,
@@ -606,8 +610,15 @@ export async function startCoordinatorSession(
606610
}
607611
}
608612

609-
async function startCoordinator(
610-
opts: { json: boolean; attach: boolean; watchdog: boolean; monitor: boolean; profile?: string },
613+
export async function startCoordinator(
614+
opts: {
615+
json: boolean;
616+
attach: boolean;
617+
watchdog: boolean;
618+
monitor: boolean;
619+
profile?: string;
620+
workflow?: string;
621+
},
611622
deps: CoordinatorDeps = {},
612623
): Promise<void> {
613624
await startCoordinatorSession(
@@ -1304,6 +1315,7 @@ export function createCoordinatorCommand(deps: CoordinatorDeps = {}): Command {
13041315
.option("--watchdog", "Auto-start watchdog daemon with coordinator")
13051316
.option("--monitor", "Auto-start Tier 2 monitor agent with coordinator")
13061317
.option("--profile <name>", "Canopy profile to apply to spawned agents")
1318+
.option("--workflow <name>", "Workflow profile alias: delivery or co-creation")
13071319
.option("--json", "Output as JSON")
13081320
.action(
13091321
async (opts: {
@@ -1312,6 +1324,7 @@ export function createCoordinatorCommand(deps: CoordinatorDeps = {}): Command {
13121324
monitor?: boolean;
13131325
json?: boolean;
13141326
profile?: string;
1327+
workflow?: string;
13151328
}) => {
13161329
// opts.attach = true if --attach, false if --no-attach, undefined if neither
13171330
const shouldAttach = opts.attach !== undefined ? opts.attach : !!process.stdout.isTTY;
@@ -1322,6 +1335,7 @@ export function createCoordinatorCommand(deps: CoordinatorDeps = {}): Command {
13221335
watchdog: opts.watchdog ?? false,
13231336
monitor: opts.monitor ?? false,
13241337
profile: opts.profile,
1338+
workflow: opts.workflow,
13251339
},
13261340
deps,
13271341
);

src/commands/discover.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export async function discoverCommand(
165165
watchdog: opts.watchdog ?? false,
166166
monitor: false,
167167
profile: "ov-discovery",
168+
workflow: undefined,
168169
coordinatorName,
169170
beaconBuilder: (_trackerCli) => buildDiscoveryBeacon(categories, coordinatorName),
170171
},

src/commands/sling.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ import { createRunStore } from "../sessions/store.ts";
3939
import type { TrackerIssue } from "../tracker/factory.ts";
4040
import { createTrackerClient, resolveBackend, trackerCliName } from "../tracker/factory.ts";
4141
import type { AgentSession, OverlayConfig } from "../types.ts";
42+
import {
43+
normalizeWorkflowName,
44+
repoRootFromCommandDir,
45+
resolveProfileName,
46+
workflowPromptPath,
47+
} from "../workflow.ts";
4248
import { createWorktree, rollbackWorktree } from "../worktree/manager.ts";
4349
import { spawnHeadlessAgent } from "../worktree/process.ts";
4450
import {
@@ -155,6 +161,7 @@ export interface SlingOptions {
155161
noScoutCheck?: boolean;
156162
baseBranch?: string;
157163
profile?: string;
164+
workflow?: string;
158165
}
159166

160167
export interface AutoDispatchOptions {
@@ -471,6 +478,7 @@ export async function getCurrentBranch(repoRoot: string): Promise<string | null>
471478
* @param opts - Command options
472479
*/
473480
export async function slingCommand(taskId: string, opts: SlingOptions): Promise<void> {
481+
const overstoryRepoRoot = repoRootFromCommandDir(import.meta.dir);
474482
if (!taskId) {
475483
throw new ValidationError("Task ID is required: ov sling <task-id>", {
476484
field: "taskId",
@@ -787,8 +795,12 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
787795
}
788796

789797
// 8b. Resolve canopy profile if specified
790-
const profileName =
791-
opts.profile ?? process.env.OVERSTORY_PROFILE ?? config.project.defaultProfile;
798+
const profileName = resolveProfileName(
799+
opts.profile ??
800+
opts.workflow ??
801+
process.env.OVERSTORY_PROFILE ??
802+
config.project.defaultProfile,
803+
);
792804
let profileContent: string | undefined;
793805
if (profileName) {
794806
try {
@@ -798,8 +810,16 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
798810
profileContent = rendered.sections.map((s) => s.body).join("\n\n");
799811
}
800812
} catch {
801-
// Non-fatal: canopy may not be installed or profile may not exist
802-
profileContent = undefined;
813+
// Fallback to emitted prompt files so first-class workflows still work
814+
// when cn is unavailable locally.
815+
const workflow = normalizeWorkflowName(profileName);
816+
if (workflow) {
817+
const fallbackPath = workflowPromptPath(overstoryRepoRoot, workflow);
818+
const fallbackFile = Bun.file(fallbackPath);
819+
if (await fallbackFile.exists()) {
820+
profileContent = await fallbackFile.text();
821+
}
822+
}
803823
}
804824
}
805825

0 commit comments

Comments
 (0)