opencode-conductor-plugin 1.9.0 → 1.10.0

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/dist/index.js CHANGED
@@ -1,17 +1,13 @@
1
- import { setupCommand } from "./commands/setup.js";
2
- import { newTrackCommand } from "./commands/newTrack.js";
3
- import { implementCommand } from "./commands/implement.js";
4
- import { statusCommand } from "./commands/status.js";
5
- import { revertCommand } from "./commands/revert.js";
6
1
  import { join, dirname } from "path";
7
2
  import { homedir } from "os";
8
3
  import { existsSync, readFileSync } from "fs";
9
4
  import { readFile } from "fs/promises";
10
5
  import { fileURLToPath } from "url";
6
+ import { parse } from "smol-toml";
11
7
  const __filename = fileURLToPath(import.meta.url);
12
8
  const __dirname = dirname(__filename);
13
9
  const ConductorPlugin = async (ctx) => {
14
- // Detect oh-my-opencode for synergy features
10
+ // 1. Detect oh-my-opencode for synergy features
15
11
  const configPath = join(homedir(), ".config", "opencode", "opencode.json");
16
12
  let isOMOActive = false;
17
13
  try {
@@ -21,32 +17,96 @@ const ConductorPlugin = async (ctx) => {
21
17
  }
22
18
  }
23
19
  catch (e) {
24
- // Fallback to filesystem check if config read fails
25
20
  const omoPath = join(homedir(), ".config", "opencode", "node_modules", "oh-my-opencode");
26
21
  isOMOActive = existsSync(omoPath);
27
22
  }
28
- console.log(`[Conductor] Plugin tools loaded. (OMO Synergy: ${isOMOActive ? "Enabled" : "Disabled"})`);
29
- const extendedCtx = { ...ctx, isOMOActive };
23
+ console.log(`[Conductor] Plugin loaded. (OMO Synergy: ${isOMOActive ? "Enabled" : "Disabled"})`);
24
+ // 2. Helper to load and process prompt templates
25
+ const loadPrompt = async (filename, replacements = {}) => {
26
+ const promptPath = join(__dirname, "prompts", filename);
27
+ try {
28
+ const content = await readFile(promptPath, "utf-8");
29
+ const parsed = parse(content);
30
+ let promptText = parsed.prompt;
31
+ // Default Replacements
32
+ const defaults = {
33
+ isOMOActive: isOMOActive ? "true" : "false",
34
+ templatesDir: join(dirname(__dirname), "templates")
35
+ };
36
+ const finalReplacements = { ...defaults, ...replacements };
37
+ for (const [key, value] of Object.entries(finalReplacements)) {
38
+ promptText = promptText.replaceAll(`{{${key}}}`, value || "");
39
+ }
40
+ return {
41
+ prompt: promptText,
42
+ description: parsed.description
43
+ };
44
+ }
45
+ catch (error) {
46
+ console.error(`[Conductor] Failed to load prompt ${filename}:`, error);
47
+ return { prompt: `SYSTEM ERROR: Failed to load prompt ${filename}`, description: "Error loading command" };
48
+ }
49
+ };
50
+ // 3. Load Strategies for Implement Command
51
+ let strategySection = "";
52
+ try {
53
+ const strategyFile = isOMOActive ? "delegate.md" : "manual.md";
54
+ strategySection = await readFile(join(__dirname, "prompts", "strategies", strategyFile), "utf-8");
55
+ }
56
+ catch (e) {
57
+ strategySection = "SYSTEM ERROR: Could not load execution strategy.";
58
+ }
59
+ // 4. Load all Command Prompts
60
+ // conductor:setup
61
+ const setup = await loadPrompt("setup.toml");
62
+ // conductor:newTrack
63
+ // Note: Arguments ($ARGUMENTS) are handled natively by OpenCode commands via variable injection in the template string?
64
+ // Actually, for OpenCode commands, we put the placeholder directly in the string passed to 'template'.
65
+ // Our TOML files use {{args}}, so we need to map that to "$ARGUMENTS" or "$1".
66
+ const newTrack = await loadPrompt("newTrack.toml", { args: "$ARGUMENTS" });
67
+ // conductor:implement
68
+ const implement = await loadPrompt("implement.toml", {
69
+ track_name: "$ARGUMENTS", // Map command arg to the TOML variable
70
+ strategy_section: strategySection
71
+ });
72
+ // conductor:status
73
+ const status = await loadPrompt("status.toml");
74
+ // conductor:revert
75
+ const revert = await loadPrompt("revert.toml", { target: "$ARGUMENTS" });
30
76
  return {
31
- tool: {
32
- conductor_setup: setupCommand(extendedCtx),
33
- conductor_new_track: newTrackCommand(extendedCtx),
34
- conductor_implement: implementCommand(extendedCtx),
35
- conductor_status: statusCommand(extendedCtx),
36
- conductor_revert: revertCommand(extendedCtx),
77
+ command: {
78
+ "conductor:setup": {
79
+ template: setup.prompt,
80
+ description: setup.description,
81
+ agent: "conductor",
82
+ },
83
+ "conductor:newTrack": {
84
+ template: newTrack.prompt,
85
+ description: newTrack.description,
86
+ agent: "conductor",
87
+ },
88
+ "conductor:implement": {
89
+ template: implement.prompt,
90
+ description: implement.description,
91
+ agent: "sisyphus",
92
+ },
93
+ "conductor:status": {
94
+ template: status.prompt,
95
+ description: status.description,
96
+ agent: "conductor",
97
+ },
98
+ "conductor:revert": {
99
+ template: revert.prompt,
100
+ description: revert.description,
101
+ agent: "conductor",
102
+ }
37
103
  },
104
+ // Keep the Hook for Sisyphus Synergy
38
105
  "tool.execute.before": async (input, output) => {
39
- // INTERCEPT: Sisyphus Delegation Hook
40
- // Purpose: Automatically inject the full Conductor context (Plan, Spec, Workflow, Protocol)
41
- // whenever the Conductor delegates a task to Sisyphus. This ensures Sisyphus has "Engineering Authority"
42
- // without needing the LLM to manually copy-paste huge context blocks.
43
106
  if (input.tool === "delegate_to_agent") {
44
107
  const agentName = (output.args.agent_name || output.args.agent || "").toLowerCase();
45
108
  if (agentName.includes("sisyphus")) {
46
- console.log("[Conductor] Intercepting Sisyphus delegation. Injecting Context Packet...");
47
109
  const conductorDir = join(ctx.directory, "conductor");
48
- const promptsDir = join(__dirname, "prompts");
49
- // Helper to safely read file content
50
110
  const safeRead = async (path) => {
51
111
  try {
52
112
  if (existsSync(path))
@@ -55,36 +115,22 @@ const ConductorPlugin = async (ctx) => {
55
115
  catch (e) { /* ignore */ }
56
116
  return null;
57
117
  };
58
- // 1. Read Project Context Files
59
- // We need to find the active track to get the correct spec/plan.
60
- // Since we don't know the track ID easily here, we look for the 'plan.md' that might be in the args
61
- // OR we just rely on the Conductor having already done the setup.
62
- // WAIT: We can't easily guess the track ID here.
63
- // BETTER APPROACH: We rely on the generic 'conductor/workflow.md' and 'prompts/implement.toml'.
64
- // For 'spec.md' and 'plan.md', the Conductor usually puts the path in the message.
65
- // However, to be robust, we will read the GLOBAL workflow and the IMPLEMENT prompt.
66
- // We will explicitly inject the IMPLEMENT PROMPT as requested.
67
- const implementToml = await safeRead(join(promptsDir, "implement.toml"));
118
+ // We load the raw TOML just to get the protocol text if needed, or just hardcode a reference.
119
+ // Since we already loaded 'implement' above, we could potentially reuse it, but simplicity is better here.
120
+ // Let's just grab the workflow.md
68
121
  const workflowMd = await safeRead(join(conductorDir, "workflow.md"));
69
- // Construct the injection block
70
122
  let injection = "\n\n--- [SYSTEM INJECTION: CONDUCTOR CONTEXT PACKET] ---\n";
71
123
  injection += "You are receiving this task from the Conductor Architect.\n";
72
- if (implementToml) {
73
- injection += "\n### 1. ARCHITECTURAL PROTOCOL (Reference Only)\n";
74
- injection += "Use this protocol to understand the project's rigorous standards. DO NOT restart the project management lifecycle (e.g. track selection).\n";
75
- injection += "```toml\n" + implementToml + "\n```\n";
76
- }
77
124
  if (workflowMd) {
78
- injection += "\n### 2. DEVELOPMENT WORKFLOW\n";
125
+ injection += "\n### DEVELOPMENT WORKFLOW\n";
79
126
  injection += "Follow these TDD and Commit rules precisely.\n";
80
127
  injection += "```markdown\n" + workflowMd + "\n```\n";
81
128
  }
82
- injection += "\n### 3. DELEGATED AUTHORITY\n";
129
+ injection += "\n### DELEGATED AUTHORITY\n";
83
130
  injection += "- **EXECUTE:** Implement the requested task using the Workflow.\n";
84
131
  injection += "- **REFINE:** You have authority to update `plan.md` if it is flawed.\n";
85
132
  injection += "- **ESCALATE:** If you modify the Plan or Spec, report 'PLAN_UPDATED' immediately.\n";
86
133
  injection += "--- [END INJECTION] ---\n";
87
- // Append to the objective
88
134
  output.args.objective += injection;
89
135
  }
90
136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-conductor-plugin",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Conductor plugin for OpenCode",
5
5
  "type": "module",
6
6
  "repository": "derekbar90/opencode-conductor",
@@ -30,7 +30,7 @@
30
30
  "scripts": {
31
31
  "postinstall": "node scripts/postinstall.cjs",
32
32
  "build": "tsc && npm run copy-prompts && npm run copy-templates",
33
- "copy-prompts": "mkdir -p dist/prompts && cp src/prompts/*.toml src/prompts/*.md dist/prompts/ && mkdir -p dist/prompts/agent && cp src/prompts/agent/*.md dist/prompts/agent/ && mkdir -p dist/prompts/commands && cp src/prompts/commands/*.md dist/prompts/commands/ && mkdir -p dist/prompts/strategies && cp src/prompts/strategies/*.md dist/prompts/strategies/",
33
+ "copy-prompts": "mkdir -p dist/prompts && cp src/prompts/*.toml src/prompts/*.md dist/prompts/ && mkdir -p dist/prompts/agent && cp src/prompts/agent/*.md dist/prompts/agent/ && mkdir -p dist/prompts/strategies && cp src/prompts/strategies/*.md dist/prompts/strategies/",
34
34
  "copy-templates": "mkdir -p dist/templates && cp -r src/templates/* dist/templates/",
35
35
  "prepublishOnly": "npm run build"
36
36
  },
@@ -1 +0,0 @@
1
- export declare const implementCommand: (ctx: any) => import("@opencode-ai/plugin/tool").ToolDefinition;
@@ -1,31 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin/tool";
2
- import { createConductorCommand } from "../utils/commandFactory.js";
3
- import { join, dirname } from "path";
4
- import { fileURLToPath } from "url";
5
- import { readFile } from "fs/promises";
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = dirname(__filename);
8
- export const implementCommand = createConductorCommand({
9
- name: "implement.toml",
10
- description: "Implements tasks from a Conductor track.",
11
- args: {
12
- track_name: tool.schema.string().optional().describe("Specific track to implement. If omitted, selects the next incomplete track."),
13
- },
14
- additionalContext: async (ctx, args) => {
15
- // 1. Choose strategy based on OMO activity
16
- const strategyFile = ctx.isOMOActive ? "delegate.md" : "manual.md";
17
- const strategyPath = join(__dirname, "../prompts/strategies", strategyFile);
18
- let strategySection = "";
19
- try {
20
- strategySection = await readFile(strategyPath, "utf-8");
21
- }
22
- catch (e) {
23
- console.warn(`[Conductor] Failed to load strategy ${strategyFile}:`, e);
24
- strategySection = "Error: Could not load execution strategy.";
25
- }
26
- return {
27
- strategy_section: strategySection,
28
- track_name: args.track_name || ""
29
- };
30
- }
31
- });
@@ -1 +0,0 @@
1
- export declare const newTrackCommand: (ctx: any) => import("@opencode-ai/plugin/tool").ToolDefinition;
@@ -1,12 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin/tool";
2
- import { createConductorCommand } from "../utils/commandFactory.js";
3
- export const newTrackCommand = createConductorCommand({
4
- name: "newTrack.toml",
5
- description: "Creates a new track (feature/bug) in the Conductor system. IMPORTANT: Do NOT create any todos using 'todowrite' or 'task' tools before or during this command, as it manages its own interactive state and will conflict with continuation enforcers.",
6
- args: {
7
- description: tool.schema.string().optional().describe("Brief description of the track."),
8
- },
9
- additionalContext: async (_, args) => ({
10
- args: args.description || ""
11
- })
12
- });
@@ -1 +0,0 @@
1
- export declare const revertCommand: (ctx: any) => import("@opencode-ai/plugin/tool").ToolDefinition;
@@ -1,9 +0,0 @@
1
- import { createConductorCommand } from "../utils/commandFactory.js";
2
- import { tool } from "@opencode-ai/plugin/tool";
3
- export const revertCommand = createConductorCommand({
4
- name: "revert.toml",
5
- description: "Reverts a Conductor track, phase, or task.",
6
- args: {
7
- target: tool.schema.string().optional().describe("ID or description of what to revert."),
8
- }
9
- });
@@ -1 +0,0 @@
1
- export declare const setupCommand: (ctx: any) => import("@opencode-ai/plugin/tool").ToolDefinition;
@@ -1,10 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin/tool";
2
- import { createConductorCommand } from "../utils/commandFactory.js";
3
- export const setupCommand = createConductorCommand({
4
- name: "setup.toml",
5
- description: "Sets up the Conductor environment for the project. Call this to start or resume the setup process. IMPORTANT: Do NOT create any todos using 'todowrite' or 'task' tools before or during this command, as it manages its own interactive state and will conflict with continuation enforcers.",
6
- args: {
7
- user_input: tool.schema.string().optional().describe("The user's response to a previous question, if applicable."),
8
- },
9
- requiresSetup: false // Setup command is what creates the setup
10
- });
@@ -1 +0,0 @@
1
- export declare const statusCommand: (ctx: any) => import("@opencode-ai/plugin/tool").ToolDefinition;
@@ -1,6 +0,0 @@
1
- import { createConductorCommand } from "../utils/commandFactory.js";
2
- export const statusCommand = createConductorCommand({
3
- name: "status.toml",
4
- description: "Shows the status of Conductor tracks.",
5
- args: {}
6
- });
@@ -1,4 +0,0 @@
1
- ---
2
- description: Implement the next pending task
3
- ---
4
- Invoke the conductor_implement tool. If a track name is provided ("$ARGUMENTS"), use it; otherwise, implement the next available track.
@@ -1,4 +0,0 @@
1
- ---
2
- description: Create a new track (feature/bug)
3
- ---
4
- Invoke the conductor_new_track tool with description: "$ARGUMENTS". Do NOT create todos during this phase.
@@ -1,4 +0,0 @@
1
- ---
2
- description: Revert a track, phase, or task
3
- ---
4
- Invoke the conductor_revert tool for: "$ARGUMENTS"
@@ -1,4 +0,0 @@
1
- ---
2
- description: Setup or resume Conductor environment
3
- ---
4
- Invoke the conductor_setup tool to start or resume the project initialization. Do NOT create todos during this phase.
@@ -1,4 +0,0 @@
1
- ---
2
- description: Show Conductor project status
3
- ---
4
- Invoke the conductor_status tool to summarize the project progress.
@@ -1,10 +0,0 @@
1
- import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
- interface CommandOptions {
3
- name: string;
4
- description: string;
5
- args?: any;
6
- requiresSetup?: boolean;
7
- additionalContext?: (ctx: any, args: any) => Promise<Record<string, string>>;
8
- }
9
- export declare const createConductorCommand: (options: CommandOptions) => (ctx: any) => ToolDefinition;
10
- export {};
@@ -1,47 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin/tool";
2
- import { existsSync } from "fs";
3
- import { readFile } from "fs/promises";
4
- import { join, dirname } from "path";
5
- import { fileURLToPath } from "url";
6
- import { parse } from "smol-toml";
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = dirname(__filename);
9
- export const createConductorCommand = (options) => (ctx) => tool({
10
- description: options.description,
11
- args: options.args || {},
12
- async execute(args) {
13
- // 1. Setup Check
14
- if (options.requiresSetup !== false) {
15
- const conductorDir = join(ctx.directory, "conductor");
16
- if (!existsSync(join(conductorDir, "product.md"))) {
17
- return "Conductor is not set up. Please run `conductor_setup`.";
18
- }
19
- }
20
- // 2. Load Prompt from TOML
21
- const promptPath = join(__dirname, "../prompts", options.name);
22
- let promptText = "";
23
- try {
24
- const content = await readFile(promptPath, "utf-8");
25
- const parsed = parse(content);
26
- promptText = parsed.prompt;
27
- }
28
- catch (error) {
29
- throw new Error(`Failed to load prompt from ${promptPath}: ${error}`);
30
- }
31
- // 3. Prepare Replacements
32
- const replacements = {
33
- isOMOActive: ctx.isOMOActive ? "true" : "false",
34
- templatesDir: join(dirname(dirname(__dirname)), "dist/templates") // Fixing template path for setup
35
- };
36
- // 4. Inject Additional Context (e.g. from args)
37
- if (options.additionalContext) {
38
- const extra = await options.additionalContext(ctx, args);
39
- Object.assign(replacements, extra);
40
- }
41
- // 5. Apply Replacements
42
- for (const [key, value] of Object.entries(replacements)) {
43
- promptText = promptText.replaceAll(`{{${key}}}`, value || "");
44
- }
45
- return promptText;
46
- },
47
- });
@@ -1,10 +0,0 @@
1
- export interface SetupState {
2
- last_successful_step: string;
3
- }
4
- export declare class StateManager {
5
- private statePath;
6
- constructor(workDir: string);
7
- ensureConductorDir(): void;
8
- readState(): SetupState;
9
- writeState(step: string): void;
10
- }
@@ -1,30 +0,0 @@
1
- import { join } from "path";
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
3
- export class StateManager {
4
- statePath;
5
- constructor(workDir) {
6
- this.statePath = join(workDir, "conductor", "setup_state.json");
7
- }
8
- ensureConductorDir() {
9
- const dir = join(this.statePath, "..");
10
- if (!existsSync(dir)) {
11
- mkdirSync(dir, { recursive: true });
12
- }
13
- }
14
- readState() {
15
- if (!existsSync(this.statePath)) {
16
- return { last_successful_step: "" };
17
- }
18
- try {
19
- return JSON.parse(readFileSync(this.statePath, "utf-8"));
20
- }
21
- catch (e) {
22
- return { last_successful_step: "" };
23
- }
24
- }
25
- writeState(step) {
26
- this.ensureConductorDir();
27
- const state = { last_successful_step: step };
28
- writeFileSync(this.statePath, JSON.stringify(state, null, 2));
29
- }
30
- }