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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"scripts": {
"build": "rm -rf dist && tsc && npm run copy-markdowns",
"copy-markdowns": "node ./scripts/copy-markdowns.js",
"prepack": "npm run build"
"prepack": "npm run build",
"test": "bun test"
},
"keywords": [
"opencode",
Expand Down
6 changes: 6 additions & 0 deletions scripts/copy-markdowns.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ console.log("Copied agents to dist/");
fs.cpSync("src/commands", "dist/commands", { recursive: true });

console.log("Copied commands to dist/");

// Copy the 'templates' folder recursively into 'dist/templates'
fs.cpSync("src/templates", "dist/templates", { recursive: true });

console.log("Copied templates to dist/");

166 changes: 166 additions & 0 deletions src/__tests__/verify_template.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { expect, test, describe, beforeAll, afterAll } from "bun:test";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { verify_template } from "../tools/verify_template";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

describe("verify_template tool", () => {
const templatesDir = path.join(__dirname, "../templates");
const testDir = path.join(__dirname, "test_tmp_dir");
const testTemplateName = "test-template.yml";
const testTemplatePath = path.join(templatesDir, testTemplateName);

beforeAll(() => {
// Ensure templates dir exists
if (!fs.existsSync(templatesDir)) {
fs.mkdirSync(templatesDir, { recursive: true });
}
// Create a dummy template
fs.writeFileSync(
testTemplatePath,
`
key1: value1
key2:
subKey1: subValue1
`,
);

// Ensure test dir exists
if (!fs.existsSync(testDir)) {
fs.mkdirSync(testDir, { recursive: true });
}
});

afterAll(() => {
// Cleanup
if (fs.existsSync(testTemplatePath)) {
fs.rmSync(testTemplatePath);
}
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});

test("should return error if file not found", async () => {
const result = await verify_template.execute(
{ filePath: "nonexistent.yml", templateName: testTemplateName },
{ directory: testDir } as any,
);
expect((result as any).output).toContain(
"File not found at nonexistent.yml",
);
});

test("should return error if template not found", async () => {
const filePath = "target.yml";
fs.writeFileSync(path.join(testDir, filePath), "key1: value");

const result = await verify_template.execute(
{ filePath, templateName: "nonexistent-template.yml" },
{ directory: testDir } as any,
);
expect((result as any).output).toContain(
"Template 'nonexistent-template.yml' not found",
);
});

test("should return error if target file has invalid YAML syntax", async () => {
const filePath = "invalid.yml";
fs.writeFileSync(path.join(testDir, filePath), "key1: : value\n invalid");

const result = await verify_template.execute(
{ filePath, templateName: testTemplateName },
{ directory: testDir } as any,
);
expect((result as any).output).toContain(
"YAML Syntax Error in invalid.yml",
);
});

test("should return error if target file is missing keys from template", async () => {
const filePath = "missing-keys.yml";
// Missing key2
fs.writeFileSync(path.join(testDir, filePath), "key1: some-value");

const result = await verify_template.execute(
{ filePath, templateName: testTemplateName },
{ directory: testDir } as any,
);
expect((result as any).output).toContain(
"YAML Structure Validation Failed",
);
expect((result as any).output).toContain("- Missing key: key2");
});

test("should pass if target file matches template structure", async () => {
const filePath = "valid.yml";
fs.writeFileSync(
path.join(testDir, filePath),
`
key1: different_value
key2:
subKey1: different_sub_value
extraKey: this is fine
`,
);

const result = await verify_template.execute(
{ filePath, templateName: testTemplateName },
{ directory: testDir } as any,
);
expect((result as any).output).toBe(
"File exists and follows the correct YAML structure.",
);
});

test("should pass if template has null or non-object values and target has them too", async () => {
const nullTemplateName = "null-template.yml";
const nullTemplatePath = path.join(templatesDir, nullTemplateName);
fs.writeFileSync(nullTemplatePath, "key: null\nother: 123");

const filePath = "valid-null.yml";
fs.writeFileSync(
path.join(testDir, filePath),
"key: something\nother: 456",
);

const result = await verify_template.execute(
{ filePath, templateName: nullTemplateName },
{ directory: testDir } as any,
);

expect((result as any).output).toBe(
"File exists and follows the correct YAML structure.",
);

fs.rmSync(nullTemplatePath);
});

describe("real templates validation", () => {
let templateFiles: string[] = [];
try {
const files = fs.readdirSync(templatesDir);
templateFiles = files.filter(f => f.endsWith('.yml') || f.endsWith('.yaml') || f.endsWith('.md'));
// Filter out our dummy test template
templateFiles = templateFiles.filter(f => f !== "test-template.yml" && f !== "null-template.yml");
} catch (e) {
// templates dir might not exist or be readable in some contexts, though beforeAll ensures it exists
}

for (const templateName of templateFiles) {
test(`should successfully validate a valid instance of ${templateName}`, async () => {
// We use the templates directory as the working directory
// and the template name as the filePath, effectively comparing the template to itself to ensure it's structurally valid.
const result = await verify_template.execute(
{ filePath: templateName, templateName },
{ directory: templatesDir } as any,
);
expect((result as any).output).toBe(
"File exists and follows the correct YAML structure.",
);
});
}
});
});
16 changes: 2 additions & 14 deletions src/commands/quick-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,8 @@ Implement a task directly without entering planning mode, while still applying p
- Prepend today's date: `YYYY-MM-DD-kebab-name`
- Examples: "Add a logout button to the navbar" → `2026-05-28-add-navbar-logout-button`, "Update the API endpoint to accept JSON" → `2026-05-28-update-api-accept-json`
2. Create directory: `.owflow/tasks/quick-dev/YYYY-MM-DD-task-name/`
3. Write `task.yml` with initial state:

```yaml
command: quick-dev
title: "Short title from task description"
description: "Full task description as provided by user"
status: in_progress
created: "YYYY-MM-DDTHH:MM:SSZ"
updated: "YYYY-MM-DDTHH:MM:SSZ"
task_path: .owflow/tasks/quick-dev/YYYY-MM-DD-task-name
escalated_to: null
escalation_reason: null
standards_applied: []
```
3. Write `task.yml` with initial state using the template [src/templates/quick-dev-task.yml](../templates/quick-dev-task.yml).
4. **CRITICAL**: Use the `verify_template` tool immediately after creation to check YAML validity against `quick-dev-task.yml`.

### Step 2: Discover Standards

Expand Down
17 changes: 2 additions & 15 deletions src/commands/quick-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,8 @@ Enter OpenCode's planning mode for a task, with automatic discovery of project s
- Examples: "Add retry logic to API client" → `2026-05-28-add-api-retry-logic`, "Refactor the payment processing module" → `2026-05-28-refactor-payment-module`
2. Create directory: `.owflow/tasks/quick-plan/YYYY-MM-DD-task-name/`
3. Create `analysis/` subdirectory inside it
4. Write `task.yml` with initial state:

```yaml
command: quick-plan
title: "Short title from task description"
description: "Full task description as provided by user"
status: in_progress
created: "YYYY-MM-DDTHH:MM:SSZ"
updated: "YYYY-MM-DDTHH:MM:SSZ"
task_path: .owflow/tasks/quick-plan/YYYY-MM-DD-task-name
escalated_to: null
escalation_reason: null
standards_applied: []
plan_path: null
```
4. Write `task.yml` with initial state using the template [src/templates/quick-plan-task.yml](../templates/quick-plan-task.yml).
5. **CRITICAL**: Use the `verify_template` tool immediately after creation to check YAML validity against `quick-plan-task.yml`.

### Step 2: Discover and Read Standards (BEFORE Plan Mode)

Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import fs from "node:fs";
import { fileURLToPath } from "url";
import type { OpenCodeConfig } from "./types/opencode-types";
import matter from "gray-matter";
import { verify_template } from "./tools/verify_template";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = __dirname; // Points to dist/ where skills/commands/agents are copied
Expand Down Expand Up @@ -92,6 +93,9 @@ const prepareAgent = (
const OwflowPlugin: Plugin = async ({ $, directory }) => {
const agentBySession = new Map();
return {
tool: {
verify_template,
},
/**
* Register owflow's skills, commands, and agents so OpenCode discovers
* them without requiring manual config file edits.
Expand Down
45 changes: 2 additions & 43 deletions src/skills/development/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Unified workflow for all development tasks — bug fixes, enhancements, and new
1. **Create Task Items**: Use `TaskCreate` for all phases (see Phase Configuration), then set dependencies with `TaskUpdate addBlockedBy`
2. **Create Task Directory**: `.owflow/tasks/development/YYYY-MM-DD-task-name/`
3. **Initialize State**: Create `orchestrator-state.yml` with task info and research reference
- **CRITICAL**: Use the `verify_template` tool immediately after creation to check YAML validity against `orchestrator-state-development.yml`.
4. **Discover project documentation**: Read `.owflow/docs/INDEX.md` (if exists), extract ALL file paths from the "Project Documentation" section. This includes predefined docs (vision, roadmap, tech-stack, architecture) AND any user-added project docs (e.g., deployment.md, api-strategy.md). Store complete list as `project_context.project_doc_paths` in state.

**Output**:
Expand Down Expand Up @@ -548,49 +549,7 @@ question - "Documentation complete. Continue to Phase 14?"

Development-specific fields in `orchestrator-state.yml`:

```yaml
orchestrator:
options:
spec_audit_enabled: true
skip_test_suite: true
e2e_enabled: null
user_docs_enabled: null
code_review_enabled: true
pragmatic_review_enabled: true
reality_check_enabled: true
production_check_enabled: true
task_context:
risk_level: null
clarifications_resolved: null
scope_expanded: null
architecture_decision: null
task_characteristics:
has_reproducible_defect: false
modifies_existing_code: false
creates_new_entities: false
involves_data_operations: false
ui_heavy: false
research_reference:
path: null
research_question: null
research_type: null
confidence_level: null
quick_reference:
path: null
command: null # quick-bugfix | quick-plan | quick-dev
escalation_reason: null
phase_summaries:
research: { summary: null, key_findings: [], recommended_approach: null }
quick_analysis: { summary: null, affected_files: [], root_cause: null }
codebase_analysis:
{ key_files: [], primary_language: null, summary: null }
clarifications: []
gap_analysis: { integration_points: [], summary: null }
scope_clarifications: { scope_expanded: null, summary: null }
ui_mockups: { components_designed: [], summary: null }
specification: { summary: null }
architecture_decision: { decision: null, summary: null }
```
Refer to the template [src/templates/orchestrator-state-development.yml](../../templates/orchestrator-state-development.yml).

---

Expand Down
35 changes: 2 additions & 33 deletions src/skills/migration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Systematic migration workflow from current state analysis to verified migration
1. **Create Task Items**: Use `TaskCreate` for all phases (see Phase Configuration), then set dependencies with `TaskUpdate addBlockedBy`
2. **Create Task Directory**: `.owflow/tasks/migrations/YYYY-MM-DD-task-name/`
3. **Initialize State**: Create `orchestrator-state.yml` with migration context
- **CRITICAL**: Use the `verify_template` tool immediately after creation to check YAML validity against `orchestrator-state-migration.yml`.
4. **Discover project documentation**: Read `.owflow/docs/INDEX.md` (if exists), extract ALL file paths from the "Project Documentation" section — includes predefined docs AND any user-added project docs. Store as `project_context.project_doc_paths` in state.

**Output**:
Expand Down Expand Up @@ -308,39 +309,7 @@ question - Display executive summary: total issues found, issues fixed, issues r

Migration-specific fields in `orchestrator-state.yml`:

```yaml
migration_context:
migration_type: "code" | "data" | "architecture" | "general"
current_system:
description: null
technologies: []
target_system:
description: null
technologies: []
migration_strategy:
approach: "incremental" | "big-bang" | "dual-run" | "phased"
phases: []
risk_level: null
breaking_changes: []
rollback_plan_created: false
dual_run_configured: false

external_research:
performed: false
category: null
breaking_changes: []
migration_guide_url: null

verification_context:
last_status: null
issues_found: null
fixes_applied: []
decisions_made: []
reverify_count: 0

options:
docs_enabled: false
```
Refer to the template [src/templates/orchestrator-state-migration.yml](../../templates/orchestrator-state-migration.yml).

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Before considering an orchestrator complete, verify ALL items:

- [ ] **Step 0: Load Framework** — Initialization reads `orchestrator-patterns.md`
- [ ] **State file creation** — Explicit step to CREATE `orchestrator-state.yml`
- [ ] **State verification** — Explicit step to use `verify_template` to check YAML validity after creation
- [ ] **Phase structure** — Each phase has: Purpose, Execute, Output, State, Transition (`→ Pause` / `→ AUTO-CONTINUE` / `→ Conditional`)
- [ ] **Delegation enforcement** — Each delegated phase has: ANTI-PATTERN block, INVOKE NOW block, SELF-CHECK
- [ ] **POST-CONTINUATION blocks** — After Skill tool phases, explicit instructions to read state, update completed_phases, and continue
Expand Down
Loading