@os-eco/overstory-cli 0.8.7 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,17 +84,18 @@ Every command supports `--json` where noted. Global flags: `-q`/`--quiet`, `--ti
84
84
  | Command | Description |
85
85
  |---------|-------------|
86
86
  | `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`, `--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`) |
88
88
  | `ov stop <agent-name>` | Terminate a running agent (`--clean-worktree`, `--json`) |
89
89
  | `ov prime` | Load context for orchestrator/agent (`--agent`, `--compact`) |
90
90
  | `ov spec write <task-id>` | Write a task specification (`--body`) |
91
+ | `ov discover` | Discover a brownfield codebase via coordinator-driven scout swarm (`--skip`, `--name`, `--attach`, `--watchdog`, `--json`) |
91
92
  | `ov update` | Refresh `.overstory/` managed files from installed package (`--agents`, `--manifest`, `--hooks`, `--dry-run`, `--json`) |
92
93
 
93
94
  ### Coordination
94
95
 
95
96
  | Command | Description |
96
97
  |---------|-------------|
97
- | `ov coordinator start` | Start persistent coordinator agent (`--attach`/`--no-attach`, `--watchdog`, `--monitor`) |
98
+ | `ov coordinator start` | Start persistent coordinator agent (`--attach`/`--no-attach`, `--watchdog`, `--monitor`, `--profile`) |
98
99
  | `ov coordinator stop` | Stop coordinator |
99
100
  | `ov coordinator status` | Show coordinator state |
100
101
  | `ov coordinator send` | Fire-and-forget message to coordinator (`--subject`) |
@@ -236,7 +237,7 @@ overstory/
236
237
  config.ts Config loader + validation
237
238
  errors.ts Custom error types
238
239
  json.ts Standardized JSON envelope helpers
239
- commands/ One file per CLI subcommand (35 commands)
240
+ commands/ One file per CLI subcommand (36 commands)
240
241
  agents.ts Agent discovery and querying
241
242
  coordinator.ts Persistent orchestrator lifecycle
242
243
  supervisor.ts Team lead management [DEPRECATED]
@@ -270,7 +271,10 @@ overstory/
270
271
  ecosystem.ts os-eco tool dashboard
271
272
  update.ts Refresh managed files
272
273
  upgrade.ts npm version upgrades
274
+ discover.ts Brownfield codebase discovery via coordinator-driven scout swarm
273
275
  completions.ts Shell completion generation (bash/zsh/fish)
276
+ canopy/
277
+ client.ts Canopy client (prompt rendering, listing, emission)
274
278
  agents/ Agent lifecycle management
275
279
  manifest.ts Agent registry (load + query)
276
280
  overlay.ts Dynamic CLAUDE.md overlay generator
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: ov-co-creation
3
+ description: Co-creation workflow profile — human-in-the-loop at explicit decision gates
4
+ ---
5
+
6
+ ## propulsion-principle
7
+
8
+ Read your assignment. For implementation work within an approved plan, execute immediately — no confirmation needed for routine decisions (naming, file organization, test strategy, implementation details within spec).
9
+
10
+ PAUSE at decision gates. When you encounter an architectural choice, design fork, scope boundary, or tool selection, stop and do not proceed. Instead:
11
+
12
+ 1. Write a structured decision document (context, options, tradeoffs, recommendation).
13
+ 2. Send it as a decision_gate mail to the coordinator.
14
+ 3. Wait for a response before proceeding past the gate.
15
+
16
+ Hesitation is the default at gates; action is the default within approved plans.
17
+
18
+ ## escalation-policy
19
+
20
+ At decision points, present options rather than choosing. When you encounter a meaningful decision:
21
+
22
+ 1. Write a structured decision document: context, 2+ options with tradeoffs, and your recommendation.
23
+ 2. Send it as a decision_gate mail to the coordinator and wait.
24
+ 3. Do not proceed until you receive a reply selecting an option.
25
+
26
+ Routine implementation decisions within an already-approved plan remain autonomous. Do not send decision gates for: variable names, file organization within spec, test strategy, or minor implementation choices that do not affect overall direction.
27
+
28
+ Escalate immediately (not as a decision gate) when you discover: risks that could cause data loss, security issues, or breaking changes beyond scope; blocked dependencies outside your control.
29
+
30
+ ## artifact-expectations
31
+
32
+ Decision artifacts come before code. Deliverables in order:
33
+
34
+ 1. **Option memos**: For any decision with multiple viable approaches, write a structured memo with options, tradeoffs, and a recommendation. Send as a decision_gate mail and await approval.
35
+ 2. **ADRs (Architecture Decision Records)**: For architectural choices, create a lightweight ADR capturing context, decision, and consequences.
36
+ 3. **Tradeoff matrices**: When comparing approaches across multiple dimensions, present a structured comparison.
37
+ 4. **Code and tests**: Implementation proceeds after decision artifacts are approved. Code must be clean, follow project conventions, and include automated tests.
38
+ 5. **Quality gates**: All lints, type checks, and tests must pass before reporting completion.
39
+
40
+ Do not write implementation code before decisions are resolved. The human reviews and approves decision documents; implementation follows approval.
41
+
42
+ ## completion-criteria
43
+
44
+ Work is complete when all of the following are true:
45
+
46
+ - All quality gates pass: tests green, linting clean, type checking passes.
47
+ - Changes are committed to the appropriate branch.
48
+ - Any issues tracked in the task system are updated or closed.
49
+ - A completion signal has been sent to the appropriate recipient (parent agent, coordinator, or human).
50
+
51
+ Do not declare completion prematurely. Run the quality gates yourself — do not assume they pass. If a gate fails, fix the issue before reporting done.
52
+
53
+ ## human-role
54
+
55
+ The human is an active co-creator at explicit decision gates — not a hands-off supervisor.
56
+
57
+ - **Active at gates.** The human reviews decision documents and selects options via mail reply. The agent waits for this input before proceeding.
58
+ - **Autonomous between gates.** Once a direction is approved, the agent executes without further check-ins. Implementation details within an approved plan are delegated.
59
+ - **Milestone reviews.** The human reviews work at defined checkpoints (planning, prototype, final). These are collaborative reviews with explicit proceed signals.
60
+ - **Minimal interruption between gates.** Do not ask questions that could be answered by reading the codebase or attempting something. Reserve interruptions for genuinely ambiguous requirements.
61
+
62
+ ## decision-gates
63
+
64
+ When you reach a decision point (architectural choice, scope boundary, design fork, tool selection), follow this protocol:
65
+
66
+ 1. **Write a structured decision document** containing:
67
+ - **Context**: What problem are you solving? What constraints apply?
68
+ - **Options**: At least 2 viable approaches, each with: description, tradeoffs (pros/cons), and implementation implications.
69
+ - **Recommendation**: Which option you recommend and why.
70
+
71
+ 2. **Send a decision_gate mail** to the coordinator with the decision document in the body. Include a payload with the options array and brief context. Use --type decision_gate.
72
+
73
+ 3. **BLOCK and wait** for a reply. Do not continue past the gate without a response. Poll your inbox periodically while waiting.
74
+
75
+ Decision gates are NOT for: variable names, file organization within spec, test strategy, or minor implementation choices within an approved design. They are for choices that meaningfully affect the direction of work.
76
+
77
+ ## milestone-reviews
78
+
79
+ Send checkpoint reviews at three milestones:
80
+
81
+ **After planning** (before any implementation begins):
82
+ Send a status mail with: scope summary (what will be built), approach (high-level design with all decisions resolved via gates), file list (which files will be affected), and any open questions requiring confirmation before starting.
83
+
84
+ **After prototyping** (when a working prototype exists):
85
+ Send a status mail with: what works and what is rough, remaining decisions (if any), revised scope if it changed during prototyping, and an explicit request to proceed before final implementation.
86
+
87
+ **Before final implementation** (after all gates resolved and prototype reviewed):
88
+ Send a status mail summarizing: complete plan with all decisions incorporated, any deviations from original scope, and a confirmation request before beginning the final commit sequence.
89
+
90
+ Each milestone review uses mail type status and clearly labels the milestone in the subject line.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@os-eco/overstory-cli",
3
- "version": "0.8.7",
3
+ "version": "0.9.1",
4
4
  "description": "Multi-agent orchestration for AI coding agents — spawn workers in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution. Pluggable runtime adapters for Claude Code, Pi, and more.",
5
5
  "author": "Jaymin West",
6
6
  "license": "MIT",
@@ -35,6 +35,18 @@ function formatMulchDomains(domains: readonly string[]): string {
35
35
  return `\`\`\`bash\nml prime ${domains.join(" ")}\n\`\`\``;
36
36
  }
37
37
 
38
+ /**
39
+ * Format profile content (Layer 2: deployment-specific WHAT KIND) for embedding in the overlay.
40
+ * Returns empty string if no profile was provided (omits the section entirely).
41
+ * When profile IS provided, renders it as-is — the caller (canopy) owns the formatting.
42
+ */
43
+ function formatProfile(profileContent: string | undefined): string {
44
+ if (!profileContent || profileContent.trim().length === 0) {
45
+ return "";
46
+ }
47
+ return profileContent;
48
+ }
49
+
38
50
  /**
39
51
  * Format pre-fetched mulch expertise for embedding in the overlay.
40
52
  * Returns empty string if no expertise was provided (omits the section entirely).
@@ -314,6 +326,7 @@ export async function generateOverlay(config: OverlayConfig): Promise<string> {
314
326
  "{{SKIP_SCOUT}}": config.skipScout ? SKIP_SCOUT_SECTION : "",
315
327
  "{{DISPATCH_OVERRIDES}}": formatDispatchOverrides(config),
316
328
  "{{BASE_DEFINITION}}": config.baseDefinition,
329
+ "{{PROFILE_INSTRUCTIONS}}": formatProfile(config.profileContent),
317
330
  "{{QUALITY_GATE_INLINE}}": formatQualityGatesInline(config.qualityGates),
318
331
  "{{QUALITY_GATE_STEPS}}": formatQualityGatesSteps(config.qualityGates),
319
332
  "{{QUALITY_GATE_BASH}}": formatQualityGatesBash(config.qualityGates),
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Tests for the Canopy CLI client.
3
+ *
4
+ * Uses real `cn` CLI calls against the actual .canopy/ directory.
5
+ * We do not mock the CLI — the project root has real prompts to test against.
6
+ * Tests are skipped if the `cn` CLI is not installed (e.g. in CI).
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import { AgentError } from "../errors.ts";
11
+ import { createCanopyClient } from "./client.ts";
12
+
13
+ // Check if canopy CLI is available
14
+ let hasCanopy = false;
15
+ try {
16
+ const proc = Bun.spawn(["which", "cn"], { stdout: "pipe", stderr: "pipe" });
17
+ const exitCode = await proc.exited;
18
+ hasCanopy = exitCode === 0;
19
+ } catch {
20
+ hasCanopy = false;
21
+ }
22
+
23
+ // The worktree root has its own .canopy/ symlinked/shared from the canonical root.
24
+ // Use process.cwd() which is set to the worktree root in bun test.
25
+ const cwd = process.cwd();
26
+ const client = createCanopyClient(cwd);
27
+
28
+ describe("CanopyClient.list()", () => {
29
+ test.skipIf(!hasCanopy)("returns prompts array with at least one entry", async () => {
30
+ const result = await client.list();
31
+ expect(result.success).toBe(true);
32
+ expect(Array.isArray(result.prompts)).toBe(true);
33
+ expect(result.prompts.length).toBeGreaterThan(0);
34
+ const first = result.prompts[0];
35
+ expect(first).toBeDefined();
36
+ expect(typeof first?.name).toBe("string");
37
+ expect(typeof first?.version).toBe("number");
38
+ expect(Array.isArray(first?.sections)).toBe(true);
39
+ });
40
+ });
41
+
42
+ describe("CanopyClient.render()", () => {
43
+ test.skipIf(!hasCanopy)(
44
+ "returns CanopyRenderResult with name, version, sections for 'builder' prompt",
45
+ async () => {
46
+ const result = await client.render("builder");
47
+ expect(result.success).toBe(true);
48
+ expect(result.name).toBe("builder");
49
+ expect(typeof result.version).toBe("number");
50
+ expect(result.version).toBeGreaterThan(0);
51
+ expect(Array.isArray(result.sections)).toBe(true);
52
+ expect(result.sections.length).toBeGreaterThan(0);
53
+ const section = result.sections[0];
54
+ expect(section).toBeDefined();
55
+ expect(typeof section?.name).toBe("string");
56
+ expect(typeof section?.body).toBe("string");
57
+ },
58
+ );
59
+
60
+ test.skipIf(!hasCanopy)("throws AgentError on non-existent prompt", async () => {
61
+ await expect(client.render("nonexistent-prompt-xyz-404")).rejects.toThrow(AgentError);
62
+ });
63
+ });
64
+
65
+ describe("CanopyClient.show()", () => {
66
+ test.skipIf(!hasCanopy)("returns prompt object for 'builder'", async () => {
67
+ const result = await client.show("builder");
68
+ expect(result.success).toBe(true);
69
+ expect(result.prompt).toBeDefined();
70
+ expect(result.prompt.name).toBe("builder");
71
+ expect(typeof result.prompt.version).toBe("number");
72
+ expect(typeof result.prompt.id).toBe("string");
73
+ expect(Array.isArray(result.prompt.sections)).toBe(true);
74
+ });
75
+
76
+ test.skipIf(!hasCanopy)("throws AgentError on non-existent prompt", async () => {
77
+ await expect(client.show("nonexistent-prompt-xyz-404")).rejects.toThrow(AgentError);
78
+ });
79
+ });
80
+
81
+ describe("CanopyClient.validate()", () => {
82
+ test.skipIf(!hasCanopy)("returns {success, errors} for a named prompt", async () => {
83
+ const result = await client.validate("scout");
84
+ expect(typeof result.success).toBe("boolean");
85
+ expect(Array.isArray(result.errors)).toBe(true);
86
+ if (result.success) {
87
+ expect(result.errors.length).toBe(0);
88
+ }
89
+ });
90
+
91
+ test.skipIf(!hasCanopy)("returns success=false with errors for an invalid prompt", async () => {
92
+ // 'builder' is known to fail schema validation (missing test gate)
93
+ const result = await client.validate("builder");
94
+ expect(typeof result.success).toBe("boolean");
95
+ expect(Array.isArray(result.errors)).toBe(true);
96
+ // Either valid or invalid — just verify structure is correct
97
+ if (!result.success) {
98
+ expect(result.errors.length).toBeGreaterThan(0);
99
+ }
100
+ });
101
+
102
+ test.skipIf(!hasCanopy)("validate --all returns result with success boolean", async () => {
103
+ const result = await client.validate(undefined, { all: true });
104
+ expect(typeof result.success).toBe("boolean");
105
+ expect(Array.isArray(result.errors)).toBe(true);
106
+ });
107
+ });
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Canopy CLI client.
3
+ *
4
+ * Wraps the `cn` command-line tool for prompt management operations.
5
+ * All methods use Bun.spawn to invoke the CLI directly.
6
+ */
7
+
8
+ import { AgentError } from "../errors.ts";
9
+ import type {
10
+ CanopyListResult,
11
+ CanopyRenderResult,
12
+ CanopyShowResult,
13
+ CanopyValidateResult,
14
+ } from "../types.ts";
15
+
16
+ export interface CanopyClient {
17
+ /** Render a prompt, resolving inheritance. */
18
+ render(name: string, options?: { format?: "md" | "json" }): Promise<CanopyRenderResult>;
19
+
20
+ /** Validate a prompt (or all prompts) against its schema. */
21
+ validate(name?: string, options?: { all?: boolean }): Promise<CanopyValidateResult>;
22
+
23
+ /** List all prompts. */
24
+ list(options?: {
25
+ tag?: string;
26
+ status?: string;
27
+ extends?: string;
28
+ mixin?: string;
29
+ }): Promise<CanopyListResult>;
30
+
31
+ /** Show a prompt record. */
32
+ show(name: string): Promise<CanopyShowResult>;
33
+ }
34
+
35
+ /**
36
+ * Run a shell command and capture its output.
37
+ */
38
+ async function runCommand(
39
+ cmd: string[],
40
+ cwd: string,
41
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
42
+ const proc = Bun.spawn(cmd, {
43
+ cwd,
44
+ stdout: "pipe",
45
+ stderr: "pipe",
46
+ });
47
+ const stdout = await new Response(proc.stdout).text();
48
+ const stderr = await new Response(proc.stderr).text();
49
+ const exitCode = await proc.exited;
50
+ return { stdout, stderr, exitCode };
51
+ }
52
+
53
+ /**
54
+ * Create a CanopyClient bound to the given working directory.
55
+ *
56
+ * @param cwd - Working directory where cn commands should run
57
+ * @returns A CanopyClient instance wrapping the cn CLI
58
+ */
59
+ export function createCanopyClient(cwd: string): CanopyClient {
60
+ async function runCanopy(
61
+ args: string[],
62
+ context: string,
63
+ ): Promise<{ stdout: string; stderr: string }> {
64
+ const { stdout, stderr, exitCode } = await runCommand(["cn", ...args], cwd);
65
+ if (exitCode !== 0) {
66
+ throw new AgentError(`canopy ${context} failed (exit ${exitCode}): ${stderr.trim()}`);
67
+ }
68
+ return { stdout, stderr };
69
+ }
70
+
71
+ return {
72
+ async render(name, _options) {
73
+ // Always use --json for structured output; format param reserved for future use
74
+ const { stdout } = await runCanopy(["render", name, "--json"], `render ${name}`);
75
+ const trimmed = stdout.trim();
76
+ try {
77
+ const raw = JSON.parse(trimmed) as {
78
+ success: boolean;
79
+ name: string;
80
+ version: number;
81
+ sections: Array<{ name: string; body: string }>;
82
+ };
83
+ return {
84
+ success: raw.success,
85
+ name: raw.name,
86
+ version: raw.version,
87
+ sections: raw.sections,
88
+ };
89
+ } catch {
90
+ throw new AgentError(
91
+ `Failed to parse JSON from cn render ${name}: ${trimmed.slice(0, 200)}`,
92
+ );
93
+ }
94
+ },
95
+
96
+ async validate(name, options) {
97
+ const args = ["validate"];
98
+ if (options?.all) {
99
+ args.push("--all");
100
+ } else if (name) {
101
+ args.push(name);
102
+ }
103
+ // cn validate does not support --json; parse exit code and stdout/stderr
104
+ const { stdout, stderr, exitCode } = await runCommand(["cn", ...args], cwd);
105
+ const output = (stdout + stderr).trim();
106
+ const errors: string[] = [];
107
+ if (exitCode !== 0) {
108
+ // Extract error lines from output (lines containing "error:")
109
+ for (const line of output.split("\n")) {
110
+ const trimmedLine = line.trim();
111
+ if (trimmedLine.includes("error:")) {
112
+ errors.push(trimmedLine);
113
+ }
114
+ }
115
+ if (errors.length === 0 && output) {
116
+ errors.push(output);
117
+ }
118
+ }
119
+ return { success: exitCode === 0, errors };
120
+ },
121
+
122
+ async list(options) {
123
+ const args = ["list", "--json"];
124
+ if (options?.tag) {
125
+ args.push("--tag", options.tag);
126
+ }
127
+ if (options?.status) {
128
+ args.push("--status", options.status);
129
+ }
130
+ if (options?.extends) {
131
+ args.push("--extends", options.extends);
132
+ }
133
+ if (options?.mixin) {
134
+ args.push("--mixin", options.mixin);
135
+ }
136
+ const { stdout } = await runCanopy(args, "list");
137
+ const trimmed = stdout.trim();
138
+ try {
139
+ const raw = JSON.parse(trimmed) as {
140
+ success: boolean;
141
+ prompts: Array<{
142
+ id: string;
143
+ name: string;
144
+ version: number;
145
+ sections: Array<{ name: string; body: string }>;
146
+ }>;
147
+ };
148
+ return {
149
+ success: raw.success,
150
+ prompts: raw.prompts,
151
+ };
152
+ } catch {
153
+ throw new AgentError(`Failed to parse JSON from cn list: ${trimmed.slice(0, 200)}`);
154
+ }
155
+ },
156
+
157
+ async show(name) {
158
+ const { stdout } = await runCanopy(["show", name, "--json"], `show ${name}`);
159
+ const trimmed = stdout.trim();
160
+ try {
161
+ const raw = JSON.parse(trimmed) as {
162
+ success: boolean;
163
+ prompt: {
164
+ id: string;
165
+ name: string;
166
+ version: number;
167
+ sections: Array<{ name: string; body: string }>;
168
+ };
169
+ };
170
+ return {
171
+ success: raw.success,
172
+ prompt: raw.prompt,
173
+ };
174
+ } catch {
175
+ throw new AgentError(`Failed to parse JSON from cn show ${name}: ${trimmed.slice(0, 200)}`);
176
+ }
177
+ },
178
+ };
179
+ }