pluidr 0.3.0 → 0.4.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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Pluidr
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/pluidr)](https://www.npmjs.com/package/pluidr)
4
+ [![npm downloads](https://img.shields.io/npm/dm/pluidr)](https://www.npmjs.com/package/pluidr)
5
+ [![License](https://img.shields.io/npm/l/pluidr)](https://github.com/funara/pluidr/blob/main/LICENSE)
6
+
3
7
  **Plan · Build · Review · Debug** — opinionated engineering workflow installer for [OpenCode](https://opencode.ai).
4
8
 
5
9
  ## How it works
@@ -10,7 +14,7 @@ Pluidr sets up a **14-agent** pipeline in OpenCode organized under **4 primary a
10
14
 
11
15
  **1. Explorer tab** (optional, research-only) — Brainstorms with you, scans the codebase and web for context, then produces actionable recommendations. No subagents, no file editing. Handoff to Planner when you're ready to formalize.
12
16
 
13
- **2. Planner tab** — Turns your request into a verified PRD. It researches technical facts via Researcher, writes the spec via Plan-Writer, and validates it for completeness via Plan-Checker before asking you to confirm. Planner never edits files directly.
17
+ **2. Planner tab** — Turns your request into a verified PRD. It researches technical facts via Researcher, writes the spec via Plan-Writer, and validates it for completeness via Plan-Checker. On FAIL, Planner surfaces gap remedies to you via the question tool (you pick the fix direction) — Planner never silently re-routes. After your input, Planner internally delegates to Researcher or Plan-Writer. Max 3 loops, then surfaces to you for direction. Planner never edits files directly.
14
18
 
15
19
  **3. Builder tab** — Executes a confirmed PRD. It delegates implementation to Coder, tests via Tester, checks traceability with Reviewer, and produces a completion report via Writer. Builder never edits files or runs bash directly.
16
20
 
@@ -65,7 +69,8 @@ Each subagent belongs to exactly one primary agent and cannot be invoked by anyo
65
69
  | **Researcher agents** | Researcher and Inspector output confirmed_facts/inferred_facts/unknowns/risks — no recommendations |
66
70
  | **Writer agents** | Plan-Writer, Writer, and Reporter are stateless formatters — missing input = TBD, never invent content |
67
71
  | **Build gate order** | Coder → Tester → Reviewer → Writer (Builder orchestrates in sequence) |
68
- | **Loop limit** | 3 consecutive FAILs from Tester or ReviewerBuilder surfaces to you |
72
+ | **Planner FAIL loop** | Plan-checker FAIL Planner surfaces gap remedies to you via question tool (MC options) you pick → Planner internally routes to Researcher or Plan-Writer. Max 3 loops, then surfaces to you for direction |
73
+ | **Builder FAIL loop** | 3 consecutive FAILs from Tester or Reviewer → Builder surfaces to you |
69
74
  | **No shared subagents** | Each subagent belongs to exactly one primary — no cross-primary delegation |
70
75
  | **Debugger independence** | Debugger is standalone — does not flow through Builder, triggered directly by you |
71
76
 
@@ -110,3 +115,22 @@ Prompts you to select models for two agent tiers, then:
110
115
  - Backs up any existing config at `~/.config/opencode/opencode.jsonc` to `opencode.jsonc.bak`
111
116
  - Writes the new config to `~/.config/opencode/opencode.jsonc`
112
117
  - Copies agent system-prompt files into `~/.config/opencode/prompts/`
118
+ - Copies the bundled `parent-session` plugin into `~/.config/opencode/plugins/`
119
+ - Writes a `package.json` into `~/.config/opencode/` declaring `@opencode-ai/plugin` as a dependency (OpenCode installs it automatically on first launch via its bundled Bun runtime)
120
+
121
+ On completion, prints:
122
+ ```
123
+ Pluidr setup complete!
124
+ You can update your agent model settings later in opencode.jsonc
125
+ ```
126
+ The filename `opencode.jsonc` (second line) is a clickable terminal hyperlink — Ctrl+click to open the config in your default editor.
127
+
128
+ ## Bundled plugins
129
+
130
+ Pluidr ships with one plugin: [`parent-session`](src/plugins/README.md), which gives subagents three tools for cross-session context access:
131
+
132
+ - `parent_session_messages` — read the parent session's transcript
133
+ - `session_messages(sessionId)` — read any session by ID
134
+ - `session_messages_batch(sessionIds)` — read multiple sessions in one call
135
+
136
+ `pluidr init` installs the plugin and its dependency declaration automatically — no extra user action. On OpenCode's first launch, the bundled Bun runtime installs `@opencode-ai/plugin` from the generated `package.json`, then the plugin's tools become available to all subagents.
package/package.json CHANGED
@@ -1,11 +1,32 @@
1
1
  {
2
2
  "name": "pluidr",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Opinionated Engineering Workflow for OpenCode — Plan, Build, Review.",
5
+ "keywords": [
6
+ "opencode",
7
+ "workflow",
8
+ "engineering",
9
+ "pipeline",
10
+ "ai-agents",
11
+ "code-review",
12
+ "debugging",
13
+ "planner",
14
+ "builder",
15
+ "cli"
16
+ ],
17
+ "license": "MIT",
5
18
  "type": "module",
6
19
  "bin": {
7
20
  "pluidr": "bin/pluidr.js"
8
21
  },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/funara/pluidr.git"
25
+ },
26
+ "homepage": "https://github.com/funara/pluidr#readme",
27
+ "bugs": {
28
+ "url": "https://github.com/funara/pluidr/issues"
29
+ },
9
30
  "engines": {
10
31
  "node": ">=18"
11
32
  },
@@ -6,11 +6,18 @@ import { backupExistingConfig } from "../../core/backup.js"
6
6
  import { buildConfig } from "../../core/configBuilder.js"
7
7
  import { writeConfig } from "../../core/configWriter.js"
8
8
  import { writeAgentPrompts } from "../../core/agentPromptWriter.js"
9
- import { getConfigPath, getPromptsDir } from "../../core/paths.js"
9
+ import { writePluginBundle, writePluginPackageJson } from "../../core/pluginWriter.js"
10
+ import { getConfigPath } from "../../core/paths.js"
10
11
 
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url))
12
13
  const TEMPLATES_DIR = resolve(__dirname, "../../templates")
13
14
 
15
+ function makeFileHyperlink(absolutePath, displayText) {
16
+ const normalized = absolutePath.replace(/\\/g, "/").replace(/^\//, "")
17
+ const fileUrl = `file:///${normalized}`
18
+ return `\x1b]8;;${fileUrl}\x1b\\${displayText}\x1b]8;;\x1b\\`
19
+ }
20
+
14
21
  export async function runInit() {
15
22
  const defaultsPath = resolve(TEMPLATES_DIR, "model-defaults.json")
16
23
  const modelDefaults = JSON.parse(readFileSync(defaultsPath, "utf-8"))
@@ -22,8 +29,8 @@ export async function runInit() {
22
29
  const configObject = buildConfig(tierChoices, TEMPLATES_DIR)
23
30
  writeConfig(configObject)
24
31
  writeAgentPrompts(TEMPLATES_DIR)
32
+ writePluginBundle()
33
+ writePluginPackageJson()
25
34
 
26
- console.log("\nPluidr init complete!")
27
- console.log(` Config : ${getConfigPath()}`)
28
- console.log(` Prompts : ${getPromptsDir()}`)
35
+ console.log(`Pluidr setup complete!\nYou can update your agent model settings later in ${makeFileHyperlink(getConfigPath(), "opencode.jsonc")}`)
29
36
  }
@@ -1,6 +1,12 @@
1
- import { mkdirSync, readdirSync, copyFileSync } from "node:fs"
2
- import { resolve } from "node:path"
1
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
2
+ import { basename, extname, resolve } from "node:path"
3
3
  import { getPromptsDir } from "./paths.js"
4
+ import IDENTITY_HEADER from "./identityHeader.js"
5
+
6
+ // hierarchy.txt is a global reference document (referenced from
7
+ // opencode.config.json's `instructions`), not an agent prompt. It must be
8
+ // copied verbatim — no agent-identity header.
9
+ const REFERENCE_FILES = new Set(["hierarchy"])
4
10
 
5
11
  export function writeAgentPrompts(templatesDir) {
6
12
  const sourceDir = resolve(templatesDir, "agent-prompts")
@@ -9,6 +15,18 @@ export function writeAgentPrompts(templatesDir) {
9
15
  mkdirSync(destDir, { recursive: true })
10
16
 
11
17
  for (const file of readdirSync(sourceDir)) {
12
- copyFileSync(resolve(sourceDir, file), resolve(destDir, file))
18
+ if (extname(file) !== ".txt") continue
19
+
20
+ const sourcePath = resolve(sourceDir, file)
21
+ const destPath = resolve(destDir, file)
22
+ const content = readFileSync(sourcePath, "utf-8")
23
+
24
+ const agentName = basename(file, ".txt")
25
+ if (REFERENCE_FILES.has(agentName)) {
26
+ writeFileSync(destPath, content, "utf-8")
27
+ continue
28
+ }
29
+
30
+ writeFileSync(destPath, IDENTITY_HEADER(agentName) + content, "utf-8")
13
31
  }
14
32
  }
@@ -0,0 +1,8 @@
1
+ // Single source of truth for the agent identity prefix prepended to every
2
+ // generated prompt file. Mirrors the system identity concept from the
3
+ // dropped hook-based AgentSelfIdentityPlugin, but as a static prepend
4
+ // (KISS: agent names are known at install time).
5
+
6
+ export default function IDENTITY_HEADER(agentName) {
7
+ return `You are the "${agentName}" agent.\n\n`
8
+ }
@@ -0,0 +1,25 @@
1
+ import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs"
2
+ import { dirname, join, resolve } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+ import { getConfigDir } from "./paths.js"
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url))
7
+ const PLUGIN_NAME = "parent-session.js"
8
+ const PACKAGE_JSON = {
9
+ dependencies: { "@opencode-ai/plugin": "^1.17.9" },
10
+ }
11
+
12
+ export function writePluginBundle() {
13
+ const pluginsDir = join(getConfigDir(), "plugins")
14
+ mkdirSync(pluginsDir, { recursive: true })
15
+
16
+ const sourcePath = resolve(__dirname, "../plugins", PLUGIN_NAME)
17
+ copyFileSync(sourcePath, join(pluginsDir, PLUGIN_NAME))
18
+ }
19
+
20
+ export function writePluginPackageJson() {
21
+ const packageJsonPath = join(getConfigDir(), "package.json")
22
+ if (existsSync(packageJsonPath)) return
23
+
24
+ writeFileSync(packageJsonPath, JSON.stringify(PACKAGE_JSON, null, 2), "utf-8")
25
+ }
@@ -0,0 +1,54 @@
1
+ # Bundled plugins
2
+
3
+ This folder holds plugin source files that `pluidr init` copies into the
4
+ user's OpenCode plugin directory (`~/.config/opencode/plugins/`), giving
5
+ subagents cross-session context tools out of the box.
6
+
7
+ ## What is `parent-session.js`?
8
+
9
+ It is the implementation of the `ParentSessionPlugin`, registered by
10
+ OpenCode at startup. It exposes three tools that subagents use to read
11
+ the conversation history of related sessions (parent, sibling, or batch):
12
+
13
+ - `parent_session_messages` — read the parent session's full transcript
14
+ - `session_messages(sessionId)` — read any session by ID
15
+ - `session_messages_batch(sessionIds)` — read multiple sessions in one call
16
+
17
+ The output is a structured text dump with numbered messages, agent
18
+ attribution, and one-line tool-invocation summaries, separated by
19
+ `\n\n---\n\n`.
20
+
21
+ ## Why `parent-session` was extracted
22
+
23
+ Subagents (Coder, Tester, Reviewer, Inspector, Fixer, etc.) run in fresh
24
+ child sessions with no access to the parent orchestrator's conversation
25
+ history. When a parent agent dispatches a subagent via the `Task` tool, the
26
+ subagent cannot see what was discussed in the parent — which means it has
27
+ to ask the user to re-paste context, or guess. `parent_session_messages`
28
+ bridges that gap: the subagent can fetch the parent transcript on demand
29
+ and pick up where the parent left off.
30
+
31
+ ## Why the identity plugin was dropped
32
+
33
+ The original `AgentSelfIdentityPlugin` injected agent identity via
34
+ `experimental.chat.*` hooks. Pluidr's 14 agent names are known at install
35
+ time, so we prepend a static identity header (`You are the "<name>" agent.`)
36
+ to every generated prompt file. This is KISS: no hook evaluation per
37
+ request, no dependency on undocumented experimental APIs, single source
38
+ of truth in `src/core/identityHeader.js`.
39
+
40
+ ## Why the attribution tool was dropped
41
+
42
+ The original `AgentAttributionToolPlugin` was designed for a Retrospective
43
+ agent that captures post-session observations. Pluidr has no Retrospective
44
+ agent in its pipeline, so the tool would have no caller — YAGNI.
45
+
46
+ ## How it is loaded
47
+
48
+ `pluidr init` copies `parent-session.js` into
49
+ `~/.config/opencode/plugins/` and writes a `package.json` next to it
50
+ declaring `@opencode-ai/plugin: ^1.17.9` as a dependency. On OpenCode's
51
+ first launch, the bundled Bun runtime detects the `package.json` and runs
52
+ `bun install` automatically — no user action required. Once installed,
53
+ the plugin's three tools are available to every agent that has the
54
+ appropriate permissions.
@@ -0,0 +1,142 @@
1
+ // Parent-session plugin: provides tools for cross-session context access in
2
+ // subagent workflows. Translated from the original TypeScript implementation
3
+ // in addition/opencode-session-context/src/parent-session.ts to plain ESM JS
4
+ // (no build step required). Behavior, formatting, and tool names are unchanged.
5
+
6
+ import { tool } from "@opencode-ai/plugin"
7
+
8
+ function formatInput(input) {
9
+ if (
10
+ input == null ||
11
+ (typeof input === "object" &&
12
+ !Array.isArray(input) &&
13
+ Object.keys(input).length === 0)
14
+ ) {
15
+ return ""
16
+ }
17
+ return `\n input: ${JSON.stringify(input)}`
18
+ }
19
+
20
+ function formatTool(part) {
21
+ const state = part.state
22
+ const status = state.status
23
+ const title = state.title ?? part.tool
24
+ const input = formatInput(state.input)
25
+ if (status === "error") {
26
+ return ` [tool] ${title} → error: ${state.error}${input}`
27
+ }
28
+ return ` [tool] ${title} → ${status}${input}`
29
+ }
30
+
31
+ function formatParts(parts) {
32
+ const lines = []
33
+ for (const part of parts) {
34
+ if (part.type === "text") {
35
+ lines.push(part.text)
36
+ } else if (part.type === "tool") {
37
+ lines.push(formatTool(part))
38
+ }
39
+ }
40
+ return lines.join("\n")
41
+ }
42
+
43
+ function formatModel(info) {
44
+ const base = `${info.providerID}/${info.modelID}`
45
+ return info.variant ? `${base} (${info.variant})` : base
46
+ }
47
+
48
+ function formatMessage(msg, index) {
49
+ const num = index + 1
50
+ if (msg.info.role === "assistant") {
51
+ const info = msg.info
52
+ const agent = info.agent ?? "unknown"
53
+ const header = `${num}. assistant (${agent}) [${formatModel(info)}]`
54
+ return `${header}\n${formatParts(msg.parts)}`
55
+ }
56
+ return `${num}. ${msg.info.role}\n${formatParts(msg.parts)}`
57
+ }
58
+
59
+ export const ParentSessionPlugin = async ({ client }) => {
60
+ async function formatSessionMessages(sessionID, emptyMessage) {
61
+ const response = await client.session.messages({
62
+ path: { id: sessionID },
63
+ })
64
+ const messages = response.data ?? []
65
+ if (messages.length === 0) {
66
+ return emptyMessage
67
+ }
68
+
69
+ return messages.map(formatMessage).join("\n\n---\n\n")
70
+ }
71
+
72
+ async function formatSessionMessagesBatch(sessionIDs) {
73
+ const sections = await Promise.all(
74
+ sessionIDs.map(async (sessionID) => {
75
+ const emptyMessage = "(No messages found or session not accessible)"
76
+
77
+ try {
78
+ const output = await formatSessionMessages(sessionID, emptyMessage)
79
+ return `=== Session: ${sessionID} ===\n${output}`
80
+ } catch {
81
+ return `=== Session: ${sessionID} ===\n${emptyMessage}`
82
+ }
83
+ }),
84
+ )
85
+
86
+ return sections.join("\n\n")
87
+ }
88
+
89
+ return {
90
+ tool: {
91
+ parent_session_messages: tool({
92
+ description:
93
+ "Fetch all messages from the parent session. " +
94
+ "Returns the full conversation with agent attribution " +
95
+ "and message content. Only works from subagent sessions " +
96
+ "(sessions with a parentID).",
97
+ args: {},
98
+ async execute(_args, context) {
99
+ const session = await client.session.get({
100
+ path: { id: context.sessionID },
101
+ })
102
+ const parent = session.data?.parentID
103
+ if (!parent) {
104
+ return `Error: Session ${context.sessionID} has no parent. This tool only works from subagent sessions.`
105
+ }
106
+
107
+ return formatSessionMessages(
108
+ parent,
109
+ `Session ${parent} has no messages.`,
110
+ )
111
+ },
112
+ }),
113
+ session_messages: tool({
114
+ description:
115
+ "Fetch all messages from a session by ID. " +
116
+ "Returns the full conversation with agent attribution " +
117
+ "and message content.",
118
+ args: {
119
+ sessionId: tool.schema.string().describe("The session ID to read"),
120
+ },
121
+ async execute(args) {
122
+ return formatSessionMessages(
123
+ args.sessionId,
124
+ `Session ${args.sessionId} has no messages.`,
125
+ )
126
+ },
127
+ }),
128
+ session_messages_batch: tool({
129
+ description:
130
+ "Fetch all messages from multiple sessions by ID. Returns the full conversations concatenated with session delimiters. Useful for reading multiple related sessions (e.g., TDD phase sessions from a dispatch log) in a single tool call.",
131
+ args: {
132
+ sessionIds: tool.schema
133
+ .array(tool.schema.string())
134
+ .describe("Array of session IDs to fetch messages from."),
135
+ },
136
+ async execute(args) {
137
+ return formatSessionMessagesBatch(args.sessionIds)
138
+ },
139
+ }),
140
+ },
141
+ }
142
+ }
@@ -25,19 +25,33 @@ edit code or files — this is a hard constraint, not a guideline.
25
25
  of the plan-writer's PRD output — Planner does not write or edit the
26
26
  file directly. Then present the PRD to the user, ask for confirmation
27
27
  to proceed to Builder.
28
- - FAIL → determine what the gap requires:
29
- * If the gap needs new research (missing information, unverified
30
- assumption) delegate to researcher with the gap list first, then
31
- delegate to plan-writer (PRD mode) with the updated input, then loop back
32
- to step 5 (plan-checker).
33
- * If the gap is a decision only the user can make surface it to the
34
- user, do not guess.
35
- * Never delegate directly to plan-writer on a FAIL plan-writer fills in the
36
- updated content, it does not decide what the content should be.
37
- * After 3 consecutive FAIL verdicts from plan-checker on the same PRD,
38
- surface the accumulated gap list to the user with a summary of what
39
- has been tried, and ask the user for direction rather than continuing
40
- to loop. A PASS from plan-checker resets the counter.
28
+ - FAIL → Surface gap list to user with remedy options:
29
+ * The Planner does NOT decide the gap remedies the user does.
30
+ * The Planner DOES decide the delegation route (researcher vs.
31
+ plan-writer) that is a process decision, not a domain decision.
32
+ * On plan-checker FAIL, the Planner MUST surface the gap list to the
33
+ user with gap-remedy options using the question tool. This is the
34
+ ONLY permitted mechanism for gathering user input on gaps. The
35
+ Planner MUST NOT invent remedies, assume the user's choice, or use
36
+ silent interpretation, auto-pick, or prose-only responses.
37
+ * Present each gap via the question tool with 2-4 multiple-choice
38
+ remedy options per gap. The question is GAP-RELATED (what remedy
39
+ should be applied), not ROUTE-RELATED (do not ask the user to pick
40
+ researcher vs. plan-writer).
41
+ * After the user submits their gap-remedy answers, the Planner
42
+ internally decides the delegation route:
43
+ - Knowledge/research gaps → delegate to researcher.
44
+ - Revision/content gaps → delegate to plan-writer (PRD mode).
45
+ * The Planner does NOT ask the user for the route choice.
46
+ * After delegation, loop back to step 5 (plan-checker).
47
+ * If plan-checker returns PASS → done, loop counter resets.
48
+ * If plan-checker returns FAIL again → increment loop counter by 1,
49
+ repeat from step 1 (surface gaps to user again).
50
+ * After 3 consecutive FAIL→user→delegate→plan-checker loops without
51
+ PASS, the Planner MUST surface the accumulated gap list with the
52
+ loop count to the user, and ask for direction (without auto-delegating
53
+ further). The user decides whether to continue, change approach, or
54
+ cancel.
41
55
 
42
56
  ## Delegation rules
43
57
 
@@ -2,11 +2,11 @@
2
2
  "reasoningHeavy": {
3
3
  "provider": "opencode",
4
4
  "model": "big-pickle",
5
- "agents": ["planner", "debugger", "explorer", "researcher", "plan-writer", "plan-checker", "inspector", "reporter"]
5
+ "agents": ["planner", "debugger", "explorer", "researcher", "plan-checker", "inspector", "builder"]
6
6
  },
7
7
  "fast": {
8
8
  "provider": "opencode",
9
9
  "model": "deepseek-v4-flash-free",
10
- "agents": ["builder", "coder", "tester", "reviewer", "writer", "fixer"]
10
+ "agents": ["coder", "tester", "reviewer", "writer", "fixer", "plan-writer", "reporter"]
11
11
  }
12
12
  }