pluidr 0.6.1 → 0.7.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
@@ -10,13 +10,13 @@
10
10
 
11
11
  ## What is Pluidr?
12
12
 
13
- Pluidr installs a **17-agent pipeline** into OpenCode — structured around **3 primary agents**, each with their own exclusive subagents, scoped permissions, and enforced workflow rules. No shared subagents. No ad-hoc delegation.
13
+ Pluidr installs **3 primary agents + 14 subagents** into OpenCode — each primary agent owns its exclusive subagents, scoped permissions, and enforced workflow rules. No shared subagents. No ad-hoc delegation.
14
14
 
15
- | Primary Agent | Purpose | Subagents |
16
- |---|---|---|
17
- | **Composer** | Feature work — Explore → Plan → Build | Researcher, Plan-Writer, Plan-Checker, Coder, Tester, Reviewer, Compose-Reporter |
18
- | **Debugger** | Bug investigation — Investigate → Fix → Report | Inspector, Fixer, Debug-Reporter |
19
- | **Prober** | Security audit — Trace → Patch → Audit | Tracer, Patcher, Auditor, Probe-Reporter |
15
+ | Primary Agent | Model Tier | Purpose | Subagents |
16
+ |---|---|---|---|
17
+ | **Composer** | 🧠 Reasoning | Feature work — Explore → Plan → Build | Researcher, Plan-Writer, Plan-Checker, Coder, Tester, Reviewer, Compose-Reporter |
18
+ | **Debugger** | 🧠 Reasoning | Bug investigation — Investigate → Fix → Report | Inspector, Fixer, Debug-Reporter |
19
+ | **Prober** | 🧠 Reasoning | Security audit — Trace → Patch → Audit | Tracer, Patcher, Auditor, Probe-Reporter |
20
20
 
21
21
  ---
22
22
 
@@ -231,22 +231,22 @@ You trigger a security audit
231
231
 
232
232
  Each subagent belongs to exactly one primary agent and cannot be invoked by anyone else:
233
233
 
234
- | Primary | Phase | Subagent | Role |
235
- |---------|-------|----------|------|
236
- | Composer | EXPLORE | Researcher | Fact-finding: confirmed_facts / inferred_facts / unknowns / risks |
237
- | Composer | PLAN | Plan-Writer | **Formatter** — writes PRD to `docs/plans/`, missing input = TBD |
238
- | Composer | PLAN | Plan-Checker | **Gate** — PASS/FAIL + gap list only, no suggestions |
239
- | Composer | BUILD | Coder | Implements code from PRD |
240
- | Composer | BUILD | Tester | PASS/FAIL/BLOCKED + coverage gaps only |
241
- | Composer | BUILD | Reviewer | **Gate** — PASS/FAIL + gap list only, no suggestions |
242
- | Composer | BUILD | Compose-Reporter | **Formatter** — completion report to `docs/reports/` |
243
- | Debugger | DEBUG | Inspector | Brooks-Lint RCA (Iron Law + 6 decay risks + 4 review modes) |
244
- | Debugger | DEBUG | Fixer | Minimal, root-cause-targeted fix |
245
- | Debugger | DEBUG | Debug-Reporter | **Formatter** — Iron Law diagnosis report to `docs/reports/` |
246
- | Prober | TRACE | Tracer | WSTG recon + vuln path tracing, no remedies |
247
- | Prober | PATCH | Patcher | Minimal, security-targeted fix (Ponytail, max 10 lines) |
248
- | Prober | AUDIT | Auditor | **Gate** — PASS/FAIL + Gap List + BLOAT List only |
249
- | Prober | AUDIT | Probe-Reporter | **Formatter** — security audit report to `docs/reports/` |
234
+ | Primary | Phase | Subagent | Model Tier | Role |
235
+ |---------|-------|----------|------------|------|
236
+ | Composer | EXPLORE | Researcher | 🧠 Reasoning | Fact-finding: confirmed_facts / inferred_facts / unknowns / risks |
237
+ | Composer | PLAN | Plan-Writer | ⚡ Fast | **Formatter** — writes PRD to `docs/plans/`, missing input = TBD |
238
+ | Composer | PLAN | Plan-Checker | 🧠 Reasoning | **Gate** — PASS/FAIL + gap list only, no suggestions |
239
+ | Composer | BUILD | Coder | ⚡ Fast | Implements code from PRD |
240
+ | Composer | BUILD | Tester | 🎯 Precision | PASS/FAIL/BLOCKED + coverage gaps only |
241
+ | Composer | BUILD | Reviewer | 🎯 Precision | **Gate** — PASS/FAIL + gap list only, no suggestions |
242
+ | Composer | BUILD | Compose-Reporter | ⚡ Fast | **Formatter** — completion report to `docs/reports/` |
243
+ | Debugger | INVESTIGATE | Inspector | 🧠 Reasoning | Brooks-Lint RCA (Iron Law + 6 decay risks + 4 review modes) |
244
+ | Debugger | FIX | Fixer | ⚡ Fast | Minimal, root-cause-targeted fix |
245
+ | Debugger | REPORT | Debug-Reporter | ⚡ Fast | **Formatter** — Iron Law diagnosis report to `docs/reports/` |
246
+ | Prober | TRACE | Tracer | 🎯 Precision | WSTG recon + vuln path tracing, no remedies |
247
+ | Prober | PATCH | Patcher | ⚡ Fast | Minimal, security-targeted fix (Ponytail, max 10 lines) |
248
+ | Prober | AUDIT | Auditor | 🎯 Precision | **Gate** — PASS/FAIL + Gap List + BLOAT List only |
249
+ | Prober | AUDIT | Probe-Reporter | ⚡ Fast | **Formatter** — security audit report to `docs/reports/` |
250
250
 
251
251
  ---
252
252
 
@@ -301,9 +301,17 @@ npx pluidr init
301
301
 
302
302
  ## Usage
303
303
 
304
+ ### `pluidr`
305
+
306
+ The everyday command. Runs three steps in sequence:
307
+
308
+ 1. **Update check** — compares local version against npm registry; prompts to `npm install -g pluidr` if a new version is available
309
+ 2. **Doctor check** — verifies installation health; prompts to repair with `pluidr init` if any checks fail
310
+ 3. **Launch** — spawns `opencode`
311
+
304
312
  ### `pluidr init`
305
313
 
306
- Prompts you to select models for two agent tiers (reasoning-heavy and fast), then:
314
+ Prompts you to select models for three agent tiers (reasoning, precision, and fast), then:
307
315
 
308
316
  - Asks whether to install the pluidr-squeeze plugin (can decline)
309
317
  - Builds a complete `opencode.jsonc` config with the chosen models injected into the right agents
@@ -328,7 +336,7 @@ Exits with code `0` if all pass, `1` if any fail.
328
336
 
329
337
  ### `pluidr update`
330
338
 
331
- Re-runs the setup wizard. Warns if existing config is found and asks for confirmation before overwriting.
339
+ Checks the npm registry for a newer version of pluidr and prompts to install it.
332
340
 
333
341
  ### `pluidr uninstall`
334
342
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pluidr",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Opinionated Engineering Workflow for OpenCode — Explore, Plan, Build, Debug, and Security-Audit.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -1,15 +1,12 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs"
2
- import { join, dirname } from "node:path"
3
- import { fileURLToPath } from "node:url"
4
- import { execFileSync } from "node:child_process"
2
+ import { join } from "node:path"
5
3
  import { getConfigDir, getConfigPath, getPromptsDir } from "../../core/paths.js"
6
4
  import { findSqueezePath } from "../../core/squeezeInstaller.js"
7
5
 
8
- const __dirname = dirname(fileURLToPath(import.meta.url))
9
6
  const EXPECTED_PROMPT_COUNT = 18
10
7
  const PLUGIN_FILES = ["pluidr-flow.js", "pluidr-squeeze.js"]
11
8
 
12
- export async function runDoctor() {
9
+ export function collectChecks() {
13
10
  const configDir = getConfigDir()
14
11
  const configPath = getConfigPath()
15
12
  const promptsDir = getPromptsDir()
@@ -79,8 +76,13 @@ export async function runDoctor() {
79
76
  }
80
77
  checks.push({ component: "config JSON validity", pass: configValid })
81
78
 
82
- // Print summary table
79
+ return checks
80
+ }
81
+
82
+ export async function runDoctor() {
83
+ const checks = collectChecks()
83
84
  const allPass = checks.every((c) => c.pass)
85
+
84
86
  console.log("\nPluidr Doctor — Installation Health Check\n")
85
87
  for (const c of checks) {
86
88
  const icon = c.pass ? "\u2713" : "\u2717"
@@ -25,7 +25,7 @@ export async function runInit() {
25
25
  initialValue: true,
26
26
  })
27
27
 
28
- const installSqueezePlugin = isCancel(squeezeAnswer) ? true : squeezeAnswer
28
+ const installSqueezePlugin = isCancel(squeezeAnswer) ? false : squeezeAnswer
29
29
 
30
30
  backupExistingConfig()
31
31
 
@@ -0,0 +1,44 @@
1
+ import { spawn } from "node:child_process"
2
+ import { confirm, isCancel } from "@clack/prompts"
3
+ import { checkAndPromptUpdate } from "../../core/versionCheck.js"
4
+ import { version } from "../../core/version.js"
5
+ import { collectChecks } from "./doctor.js"
6
+ import { runInit } from "./init.js"
7
+
8
+ export async function runLaunch() {
9
+ // 1. Update check
10
+ await checkAndPromptUpdate(version)
11
+
12
+ // 2. Doctor check
13
+ process.stdout.write("Running doctor... ")
14
+ const checks = collectChecks()
15
+ const allPass = checks.every((c) => c.pass)
16
+
17
+ if (allPass) {
18
+ console.log("✓ All checks passed")
19
+ } else {
20
+ const failed = checks.filter((c) => !c.pass)
21
+ console.log(`✗ ${failed.length} check${failed.length > 1 ? "s" : ""} failed`)
22
+ for (const c of failed) {
23
+ const detail = c.detail ? ` (${c.detail})` : ""
24
+ console.log(` ✗ ${c.component}${detail}`)
25
+ }
26
+
27
+ const answer = await confirm({ message: "Repair with pluidr init?", initialValue: true })
28
+ if (!isCancel(answer) && answer) {
29
+ await runInit()
30
+ }
31
+ }
32
+
33
+ // 3. Launch opencode
34
+ console.log("Launching opencode...")
35
+ const child = spawn("opencode", [], { stdio: "inherit" })
36
+ child.on("error", (err) => {
37
+ if (err.code === "ENOENT") {
38
+ console.error("Error: opencode not found on PATH. Install it from https://opencode.ai")
39
+ } else {
40
+ console.error(`Error launching opencode: ${err.message}`)
41
+ }
42
+ process.exit(1)
43
+ })
44
+ }
@@ -1,5 +1,5 @@
1
1
  import { existsSync, copyFileSync, rmSync, readdirSync } from "node:fs"
2
- import { join, dirname } from "node:path"
2
+ import { join } from "node:path"
3
3
  import { getConfigDir, getConfigPath } from "../../core/paths.js"
4
4
 
5
5
  function findLatestBackup() {
@@ -27,11 +27,7 @@ export async function runUninstall() {
27
27
 
28
28
  // Restore latest backup
29
29
  const latestBackup = findLatestBackup()
30
- if (latestBackup && existsSync(configPath)) {
31
- copyFileSync(latestBackup, configPath)
32
- summary.restored = latestBackup
33
- } else if (latestBackup) {
34
- // Config doesn't exist but backup does — restore anyway
30
+ if (latestBackup) {
35
31
  copyFileSync(latestBackup, configPath)
36
32
  summary.restored = latestBackup
37
33
  }
@@ -1,22 +1,6 @@
1
- import { existsSync } from "node:fs"
2
- import { confirm, isCancel } from "@clack/prompts"
3
- import { getConfigPath } from "../../core/paths.js"
4
- import { runInit } from "./init.js"
1
+ import { checkAndPromptUpdate } from "../../core/versionCheck.js"
2
+ import { version } from "../../core/version.js"
5
3
 
6
4
  export async function runUpdate() {
7
- const configPath = getConfigPath()
8
-
9
- if (existsSync(configPath)) {
10
- const answer = await confirm({
11
- message: "Existing Pluidr config found. Overwrite? (Y/n)",
12
- initialValue: true,
13
- })
14
-
15
- if (isCancel(answer) || !answer) {
16
- console.log("Update cancelled.")
17
- return
18
- }
19
- }
20
-
21
- await runInit()
5
+ await checkAndPromptUpdate(version)
22
6
  }
package/src/cli/index.js CHANGED
@@ -5,30 +5,33 @@ import { runInit } from "./commands/init.js"
5
5
  import { runUninstall } from "./commands/uninstall.js"
6
6
  import { runUpdate } from "./commands/update.js"
7
7
  import { runDoctor } from "./commands/doctor.js"
8
+ import { runLaunch } from "./commands/launch.js"
9
+ import { version } from "../core/version.js"
8
10
 
9
- const __dirname = dirname(fileURLToPath(import.meta.url))
10
- const pkg = JSON.parse(readFileSync(resolve(__dirname, "../../package.json"), "utf-8"))
11
+ const HELP = `Usage: pluidr [command]
11
12
 
12
- const HELP = `Usage: pluidr <command>
13
-
14
- Commands:
15
- init Set up OpenCode with Pluidr's 12-agent pipeline
16
- uninstall Remove Pluidr artifacts and restore previous config
17
- update Re-run setup (overwrites current config)
18
- doctor Verify installation health
19
- --version, -v Print version number
20
- --help, -h Show this help message`
13
+ pluidr Check for updates, run doctor, launch opencode
14
+ pluidr init Set up OpenCode with Pluidr's 17-agent pipeline
15
+ pluidr update Check for pluidr updates
16
+ pluidr doctor Verify installation health
17
+ pluidr uninstall Remove Pluidr artifacts and restore previous config
18
+ --version, -v Print version number
19
+ --help, -h Show this help message`
21
20
 
22
21
  export function run(argv) {
23
22
  const cmd = argv[2]
24
23
 
24
+ if (!cmd) {
25
+ return runLaunch()
26
+ }
27
+
25
28
  if (cmd === "--help" || cmd === "-h") {
26
29
  console.log(HELP)
27
30
  return
28
31
  }
29
32
 
30
33
  if (cmd === "--version" || cmd === "-v") {
31
- console.log(pkg.version)
34
+ console.log(version)
32
35
  return
33
36
  }
34
37
 
@@ -48,6 +51,7 @@ export function run(argv) {
48
51
  return runDoctor()
49
52
  }
50
53
 
54
+ console.error(`Unknown command: ${cmd}\n`)
51
55
  console.log(HELP)
52
56
  process.exit(1)
53
57
  }
@@ -1,8 +1,9 @@
1
1
  import { select, text, isCancel } from "@clack/prompts"
2
2
 
3
3
  const TIER_LABELS = {
4
- reasoningHeavy: "Model for reasoning task",
5
- fast: "Model for fast/cheap task",
4
+ reasoning: "Model for reasoning task (orchestrators + deep analysis)",
5
+ precision: "Model for precision task (quality gates + verification)",
6
+ fast: "Model for fast/cheap task (builders + formatters)",
6
7
  }
7
8
 
8
9
  export async function selectModelTier(modelDefaults) {
@@ -20,7 +20,9 @@ function rotateBackups(configPath) {
20
20
  .filter((f) => f.startsWith(baseName + "."))
21
21
  .map((f) => join(dir, f))
22
22
  .sort()
23
- } catch { /* dir doesn't exist yet */ }
23
+ } catch (err) {
24
+ if (err.code !== "ENOENT") throw err
25
+ }
24
26
 
25
27
  while (backups.length >= MAX_BACKUPS) {
26
28
  const oldest = backups.shift()
@@ -12,15 +12,21 @@ describe("configBuilder", () => {
12
12
  const configJson = {
13
13
  agent: {
14
14
  composer: {},
15
+ reviewer: {},
15
16
  coder: {},
16
17
  },
17
18
  }
18
19
  const defaultsJson = {
19
- reasoningHeavy: {
20
+ reasoning: {
20
21
  provider: "test",
21
22
  model: "big-model",
22
23
  agents: ["composer"],
23
24
  },
25
+ precision: {
26
+ provider: "test",
27
+ model: "mid-model",
28
+ agents: ["reviewer"],
29
+ },
24
30
  fast: {
25
31
  provider: "test",
26
32
  model: "small-model",
@@ -33,15 +39,55 @@ describe("configBuilder", () => {
33
39
 
34
40
  const result = buildConfig(
35
41
  {
36
- reasoningHeavy: "test/big-model",
42
+ reasoning: "test/big-model",
43
+ precision: "test/mid-model",
37
44
  fast: "test/small-model",
38
45
  },
39
46
  tmpDir,
40
47
  )
41
48
 
42
49
  assert.strictEqual(result.agent.composer.model, "test/big-model")
50
+ assert.strictEqual(result.agent.reviewer.model, "test/mid-model")
43
51
  assert.strictEqual(result.agent.coder.model, "test/small-model")
44
52
 
45
53
  rmSync(tmpDir, { recursive: true })
46
54
  })
55
+
56
+ it("throws an error if a defaulted agent is missing from config template", () => {
57
+ const tmpDir = mkdtempSync(join(tmpdir(), "configbuilder-test-err-"))
58
+
59
+ const configJson = {
60
+ agent: {
61
+ // missing 'composer'
62
+ coder: {},
63
+ },
64
+ }
65
+ const defaultsJson = {
66
+ reasoning: {
67
+ provider: "test",
68
+ model: "big-model",
69
+ agents: ["composer"],
70
+ },
71
+ fast: {
72
+ provider: "test",
73
+ model: "small-model",
74
+ agents: ["coder"],
75
+ },
76
+ }
77
+
78
+ writeFileSync(join(tmpDir, "opencode.config.json"), JSON.stringify(configJson), "utf-8")
79
+ writeFileSync(join(tmpDir, "model-defaults.json"), JSON.stringify(defaultsJson), "utf-8")
80
+
81
+ assert.throws(() => {
82
+ buildConfig(
83
+ {
84
+ reasoning: "test/big-model",
85
+ fast: "test/small-model",
86
+ },
87
+ tmpDir,
88
+ )
89
+ }, /Template validation failed: agent\(s\) not found/)
90
+
91
+ rmSync(tmpDir, { recursive: true })
92
+ })
47
93
  })
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { resolve, dirname } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url))
6
+ export const version = JSON.parse(
7
+ readFileSync(resolve(__dirname, "../../package.json"), "utf-8")
8
+ ).version
@@ -0,0 +1,35 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import { confirm, isCancel } from "@clack/prompts"
3
+
4
+ export function fetchLatestVersion() {
5
+ try {
6
+ return execFileSync("npm", ["view", "pluidr", "version"], { stdio: "pipe" })
7
+ .toString()
8
+ .trim()
9
+ } catch {
10
+ return null
11
+ }
12
+ }
13
+
14
+ export async function checkAndPromptUpdate(currentVersion) {
15
+ process.stdout.write("Checking for updates... ")
16
+
17
+ const latest = fetchLatestVersion()
18
+ if (!latest) {
19
+ console.log("⚠ Could not reach npm registry — skipping")
20
+ return false
21
+ }
22
+
23
+ if (latest === currentVersion) {
24
+ console.log(`✓ Up to date (${currentVersion})`)
25
+ return false
26
+ }
27
+
28
+ console.log(`⚠ pluidr ${latest} available (you have ${currentVersion})`)
29
+
30
+ const answer = await confirm({ message: "Update now?", initialValue: true })
31
+ if (isCancel(answer) || !answer) return false
32
+
33
+ execFileSync("npm", ["install", "-g", "pluidr"], { stdio: "inherit" })
34
+ return true
35
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, it } from "node:test"
2
+ import assert from "node:assert"
3
+ import { execFileSync } from "node:child_process"
4
+ import { fetchLatestVersion } from "./versionCheck.js"
5
+
6
+ describe("fetchLatestVersion", () => {
7
+ it("returns a string or null", () => {
8
+ // Either reaches npm registry (string) or fails gracefully (null)
9
+ const result = fetchLatestVersion()
10
+ assert.ok(result === null || typeof result === "string")
11
+ })
12
+
13
+ it("returns null when npm command throws", () => {
14
+ // Verify the try/catch in fetchLatestVersion swallows errors
15
+ let caught = null
16
+ try {
17
+ execFileSync("npm", ["view", "__nonexistent_pkg_xyz_pluidr__", "version"], { stdio: "pipe" })
18
+ } catch (err) {
19
+ caught = err
20
+ }
21
+ // npm throws on unknown package — fetchLatestVersion handles this and returns null
22
+ assert.ok(caught instanceof Error, "npm should throw for unknown package")
23
+ })
24
+ })
@@ -1,14 +1,14 @@
1
1
  {
2
- "reasoningHeavy": {
2
+ "reasoning": {
3
3
  "provider": "opencode",
4
4
  "model": "big-pickle",
5
5
  "agents": [
6
6
  "composer",
7
7
  "debugger",
8
+ "prober",
8
9
  "researcher",
9
10
  "plan-checker",
10
- "inspector",
11
- "prober"
11
+ "inspector"
12
12
  ],
13
13
 
14
14
  "recommended": [
@@ -23,20 +23,38 @@
23
23
  { "value": "openai/gpt-5.5", "label": "openai/gpt-5.5" }
24
24
  ]
25
25
  },
26
+ "precision": {
27
+ "provider": "opencode",
28
+ "model": "big-pickle",
29
+ "agents": [
30
+ "tester",
31
+ "reviewer",
32
+ "auditor",
33
+ "tracer"
34
+ ],
35
+
36
+ "recommended": [
37
+ {
38
+ "value": "opencode/big-pickle",
39
+ "label": "default (opencode/big-pickle)"
40
+ },
41
+ {
42
+ "value": "anthropic/claude-sonnet-4-5",
43
+ "label": "anthropic/claude-sonnet-4-5"
44
+ },
45
+ { "value": "openai/gpt-5.1", "label": "openai/gpt-5.1" }
46
+ ]
47
+ },
26
48
  "fast": {
27
49
  "provider": "opencode",
28
50
  "model": "deepseek-v4-flash-free",
29
51
  "agents": [
30
52
  "coder",
31
- "tester",
32
- "reviewer",
33
- "compose-reporter",
34
53
  "fixer",
54
+ "patcher",
35
55
  "plan-writer",
56
+ "compose-reporter",
36
57
  "debug-reporter",
37
- "tracer",
38
- "patcher",
39
- "auditor",
40
58
  "probe-reporter"
41
59
  ],
42
60