@netnodeag/kraftwerk 0.4.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.
Files changed (81) hide show
  1. package/README.md +192 -6
  2. package/dist/cli/doctor.js +77 -2
  3. package/dist/cli/init.js +65 -15
  4. package/dist/cli/knowledge.d.ts +2 -0
  5. package/dist/cli/knowledge.js +213 -0
  6. package/dist/cli/kraftwerk.js +12 -10
  7. package/dist/cli/runs.js +13 -6
  8. package/dist/cli/ui.js +3 -4
  9. package/dist/config.d.ts +9 -0
  10. package/dist/config.js +5 -2
  11. package/dist/gates.d.ts +7 -0
  12. package/dist/gates.js +27 -0
  13. package/dist/harnesses/claude.js +0 -1
  14. package/dist/inspector/chat/acp.d.ts +2 -2
  15. package/dist/inspector/chat/acp.js +44 -3
  16. package/dist/inspector/chat/backend.d.ts +14 -0
  17. package/dist/inspector/chat/pi.d.ts +2 -0
  18. package/dist/inspector/chat/pi.js +106 -0
  19. package/dist/inspector/chat/sessions.d.ts +33 -0
  20. package/dist/inspector/chat/sessions.js +407 -0
  21. package/dist/inspector/chat/store.d.ts +8 -0
  22. package/dist/inspector/chat/store.js +69 -0
  23. package/dist/inspector/chat/types.d.ts +7 -0
  24. package/dist/inspector/context.d.ts +7 -2
  25. package/dist/inspector/context.js +11 -3
  26. package/dist/inspector/knowledge.d.ts +24 -0
  27. package/dist/inspector/knowledge.js +41 -0
  28. package/dist/inspector/routines.d.ts +56 -0
  29. package/dist/inspector/routines.js +266 -0
  30. package/dist/inspector/runner.d.ts +10 -0
  31. package/dist/inspector/runner.js +6 -8
  32. package/dist/inspector/runs.d.ts +2 -0
  33. package/dist/inspector/runs.js +17 -12
  34. package/dist/inspector/server.d.ts +2 -0
  35. package/dist/inspector/server.js +311 -1
  36. package/dist/inspector/skills.d.ts +26 -0
  37. package/dist/inspector/skills.js +65 -0
  38. package/dist/inspector/team.d.ts +57 -0
  39. package/dist/inspector/team.js +121 -0
  40. package/dist/okf.d.ts +118 -0
  41. package/dist/okf.js +398 -0
  42. package/dist/runner/docker.d.ts +3 -1
  43. package/dist/runner/docker.js +5 -5
  44. package/dist/workflow.d.ts +7 -0
  45. package/dist/workflow.js +19 -0
  46. package/dist/yaml.js +43 -10
  47. package/inspector/dist/assets/index-C8AKqc_i.css +1 -0
  48. package/inspector/dist/assets/index-G3eFm1X4.js +13 -0
  49. package/inspector/dist/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
  50. package/inspector/dist/assets/roboto-latin-400-normal-D27F5sTu.woff +0 -0
  51. package/inspector/dist/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
  52. package/inspector/dist/assets/roboto-latin-500-normal-fsTFPL3E.woff +0 -0
  53. package/inspector/dist/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
  54. package/inspector/dist/assets/roboto-latin-700-normal-YuyVweIx.woff +0 -0
  55. package/inspector/dist/assets/roboto-mono-latin-400-normal-DBZPkcnn.woff +0 -0
  56. package/inspector/dist/assets/roboto-mono-latin-400-normal-GekRknry.woff2 +0 -0
  57. package/inspector/dist/assets/roboto-mono-latin-500-normal-D6j6AMzd.woff +0 -0
  58. package/inspector/dist/assets/roboto-mono-latin-500-normal-pxIOXZmA.woff2 +0 -0
  59. package/inspector/dist/assets/roboto-mono-latin-700-normal-CHYrj1DZ.woff +0 -0
  60. package/inspector/dist/assets/roboto-mono-latin-700-normal-foxDnrFi.woff2 +0 -0
  61. package/inspector/dist/index.html +2 -2
  62. package/package.json +1 -1
  63. package/schema/workflow.schema.json +166 -31
  64. package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  65. package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  66. package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  67. package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  68. package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  69. package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  70. package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2 +0 -0
  71. package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff +0 -0
  72. package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  73. package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  74. package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  75. package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  76. package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  77. package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  78. package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
  79. package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
  80. package/inspector/dist/assets/index-BCSWkslF.css +0 -1
  81. package/inspector/dist/assets/index-D6WuziFQ.js +0 -9
@@ -11,15 +11,17 @@ import { loadWorkflow, missingEnv } from "../yaml.js";
11
11
  import { renderCreateBrief } from "./create-brief.js";
12
12
  import { runDoctor } from "./doctor.js";
13
13
  import { runInit } from "./init.js";
14
+ import { registerKnowledgeCommands } from "./knowledge.js";
14
15
  import { listRuns, showRun } from "./runs.js";
15
16
  import { runUi } from "./ui.js";
16
17
  /**
17
18
  * kraftwerk — the kraftwerk CLI.
18
19
  *
19
- * kraftwerk init scaffold kraftwerk.yml + workflows/ + example
20
+ * kraftwerk init scaffold kraftwerk.yml + kraftwerk-data/ (workflow, agent, knowledge)
20
21
  * kraftwerk list discover + list workflows (--json, --from)
21
22
  * kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
22
23
  * kraftwerk runs [show <id>] inspect past runs from their traces
24
+ * kraftwerk knowledge ... Context & Knowledge: OKF bundles (list/get/put/verify/...)
23
25
  * kraftwerk ui start the inspector web UI (localhost:1981)
24
26
  * kraftwerk doctor preflight: harness CLIs, docker, workflows, env
25
27
  * kraftwerk validate [paths...] validate without executing
@@ -127,7 +129,7 @@ program
127
129
  .option("--from <source>", "Workflows from a git remote: github:org/repo[@ref] or git URL")
128
130
  .option("--sandbox", "Run inside the Docker sandbox container (image: kraftwerk-runner)")
129
131
  .option("--ssh", "Forward the SSH agent + known_hosts into the sandbox (only with --sandbox)")
130
- .option("--run-id <id>", "Pin the run folder name (output/<id>), e.g. for external triggers")
132
+ .option("--run-id <id>", "Pin the run folder name (output/runs/<id>), e.g. for external triggers")
131
133
  .action(async (name, requestParts, opts) => {
132
134
  const machine = !!opts.json;
133
135
  const fail = (code, message) => {
@@ -170,8 +172,10 @@ program
170
172
  }
171
173
  if (opts.sandbox) {
172
174
  const { runSandboxed } = await import("../runner/docker.js");
175
+ const { resolveProject } = await import("../config.js");
173
176
  const handle = await runSandboxed({
174
177
  projectRoot: baseDir,
178
+ outputDir: opts.from ? undefined : (await resolveProject(baseDir)).outputDir,
175
179
  workflowPath: entry.path,
176
180
  workflowName: workflow.name,
177
181
  request,
@@ -182,17 +186,14 @@ program
182
186
  console.log(chalk.dim(`sandbox ${handle.containerName} → ${handle.runDir}`));
183
187
  process.exit(await handle.finished);
184
188
  }
185
- if (opts.runId) {
186
- process.env.KRAFTWERK_RUN_DIR = path.resolve("output", opts.runId);
187
- }
188
- else if (!process.env.KRAFTWERK_RUN_DIR) {
189
+ if (opts.runId || !process.env.KRAFTWERK_RUN_DIR) {
189
190
  // Root the run dir explicitly: at the project's output dir (honors
190
191
  // kraftwerk.yml `output:` and works from subdirs); for remote runs at
191
192
  // the caller's cwd — never inside the clone cache.
192
- const { runStamp } = await import("../workflow.js");
193
+ const { newRunId, runDirFor } = await import("../workflow.js");
193
194
  const { resolveProject } = await import("../config.js");
194
195
  const outBase = opts.from ? path.resolve("output") : (await resolveProject(baseDir)).outputDir;
195
- process.env.KRAFTWERK_RUN_DIR = path.join(outBase, `run-${runStamp()}`);
196
+ process.env.KRAFTWERK_RUN_DIR = runDirFor(outBase, opts.runId ?? newRunId(workflow.name));
196
197
  }
197
198
  // Machine/quiet mode: workflow narration must not pollute stdout — the
198
199
  // engine logs via console.log, so reroute it for the duration of the run.
@@ -223,7 +224,7 @@ program
223
224
  });
224
225
  program
225
226
  .command("init")
226
- .description("Scaffold the project: kraftwerk.yml, workflows/ with an example, .gitignore")
227
+ .description("Scaffold the project: kraftwerk.yml, kraftwerk-data/ (example workflow, agent max, demo knowledge), .gitignore")
227
228
  .action(async () => {
228
229
  await runInit(process.cwd());
229
230
  });
@@ -246,11 +247,12 @@ runs
246
247
  runs
247
248
  .command("show")
248
249
  .description("Show one run in detail: phases, gates, cost")
249
- .argument("<runId>", "Folder name under output/, see `kraftwerk runs`")
250
+ .argument("<runId>", "Folder name under output/runs/, see `kraftwerk runs`")
250
251
  .option("--json", "Machine-readable output (all trace events)")
251
252
  .action(async (runId, opts) => {
252
253
  await showRun(process.cwd(), runId, opts);
253
254
  });
255
+ registerKnowledgeCommands(program);
254
256
  program
255
257
  .command("ui")
256
258
  .description("Start the inspector web UI for this project's runs and workflows")
package/dist/cli/runs.js CHANGED
@@ -4,6 +4,7 @@ import chalk from "chalk";
4
4
  import Table from "cli-table3";
5
5
  import { resolveProject } from "../config.js";
6
6
  import { fmtDuration, fmtTokens } from "../stats.js";
7
+ import { runDirFor } from "../workflow.js";
7
8
  async function readRun(dir) {
8
9
  const tracePath = path.join(dir, "trace.jsonl");
9
10
  const raw = await readFile(tracePath, "utf8").catch(() => null);
@@ -37,12 +38,18 @@ async function readRun(dir) {
37
38
  }
38
39
  async function collectRuns(cwd) {
39
40
  const { outputDir } = await resolveProject(cwd);
40
- const entries = await readdir(outputDir, { withFileTypes: true }).catch(() => []);
41
+ // New runs live under <output>/runs/; legacy run-* folders sit at the root.
42
+ const dirs = [
43
+ ...(await readdir(path.join(outputDir, "runs"), { withFileTypes: true }).catch(() => []))
44
+ .filter((e) => e.isDirectory())
45
+ .map((e) => path.join(outputDir, "runs", e.name)),
46
+ ...(await readdir(outputDir, { withFileTypes: true }).catch(() => []))
47
+ .filter((e) => e.isDirectory() && e.name.startsWith("run-"))
48
+ .map((e) => path.join(outputDir, e.name)),
49
+ ];
41
50
  const runs = [];
42
- for (const entry of entries) {
43
- if (!entry.isDirectory())
44
- continue;
45
- const info = await readRun(path.join(outputDir, entry.name));
51
+ for (const dir of dirs) {
52
+ const info = await readRun(dir);
46
53
  if (info)
47
54
  runs.push(info);
48
55
  }
@@ -83,7 +90,7 @@ export async function listRuns(cwd, opts = {}) {
83
90
  }
84
91
  export async function showRun(cwd, id, opts = {}) {
85
92
  const { outputDir } = await resolveProject(cwd);
86
- const dir = path.join(outputDir, id);
93
+ const dir = runDirFor(outputDir, id);
87
94
  if (!(await stat(dir).catch(() => null))) {
88
95
  console.error(chalk.red(`Run "${id}" not found under ${outputDir}.`));
89
96
  process.exit(2);
package/dist/cli/ui.js CHANGED
@@ -42,11 +42,10 @@ function ensureBuilt() {
42
42
  }
43
43
  export async function runUi(cwd, opts) {
44
44
  const staticDir = ensureBuilt();
45
- const outputDir = opts.output
46
- ? path.resolve(cwd, opts.output)
47
- : (await resolveProject(cwd)).outputDir;
45
+ const project = await resolveProject(cwd);
46
+ const outputDir = opts.output ? path.resolve(cwd, opts.output) : project.outputDir;
48
47
  const port = Number(opts.port ?? "1981");
49
- await startInspector({ outputDir, staticDir, port });
48
+ await startInspector({ outputDir, staticDir, port, projectRoot: project.root });
50
49
  console.log(`${chalk.green("✔")} Inspector: ${chalk.cyan(`http://localhost:${port}`)} ` +
51
50
  chalk.dim(`(output: ${outputDir})`));
52
51
  }
package/dist/config.d.ts CHANGED
@@ -9,17 +9,26 @@
9
9
  *
10
10
  * All fields are optional — a valid kraftwerk.yml may be empty:
11
11
  *
12
+ * name: my-project # display name (inspector header, "environment")
12
13
  * workflows: src/workflows # workflows root, relative to the file
13
14
  * output: output # run-artifact directory, relative to the file
15
+ * knowledge: knowledge # OKF knowledge-bundle root, relative to the file
16
+ * agents: agents # team agent-definition root, relative to the file
14
17
  */
15
18
  /** Stable, versionless URL of the workflow JSON schema (editor validation). */
16
19
  export declare const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
17
20
  export declare const CONFIG_FILENAMES: string[];
18
21
  export interface ProjectConfig {
22
+ /** Display name of the project ("environment"), shown in the inspector header. */
23
+ name?: string;
19
24
  /** Workflows root relative to the project root. */
20
25
  workflows?: string;
21
26
  /** Run-artifact directory relative to the project root. Default: output */
22
27
  output?: string;
28
+ /** OKF knowledge-bundle root relative to the project root. Default: knowledge */
29
+ knowledge?: string;
30
+ /** Team agent-definition root relative to the project root. Default: agents */
31
+ agents?: string;
23
32
  }
24
33
  export interface Project {
25
34
  /** Absolute project root the CLI operates on. */
package/dist/config.js CHANGED
@@ -12,8 +12,11 @@ import { parse } from "yaml";
12
12
  *
13
13
  * All fields are optional — a valid kraftwerk.yml may be empty:
14
14
  *
15
+ * name: my-project # display name (inspector header, "environment")
15
16
  * workflows: src/workflows # workflows root, relative to the file
16
17
  * output: output # run-artifact directory, relative to the file
18
+ * knowledge: knowledge # OKF knowledge-bundle root, relative to the file
19
+ * agents: agents # team agent-definition root, relative to the file
17
20
  */
18
21
  /** Stable, versionless URL of the workflow JSON schema (editor validation). */
19
22
  export const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
@@ -81,10 +84,10 @@ async function loadConfig(configPath) {
81
84
  if (raw === null || raw === undefined)
82
85
  return {};
83
86
  if (typeof raw !== "object" || Array.isArray(raw)) {
84
- throw new Error(`${path.basename(configPath)}: expected a mapping (workflows, output)`);
87
+ throw new Error(`${path.basename(configPath)}: expected a mapping (name, workflows, output, knowledge, agents)`);
85
88
  }
86
89
  const config = raw;
87
- const known = ["workflows", "output"];
90
+ const known = ["name", "workflows", "output", "knowledge", "agents"];
88
91
  for (const key of Object.keys(config)) {
89
92
  if (!known.includes(key)) {
90
93
  throw new Error(`${path.basename(configPath)}: unknown key "${key}" (allowed: ${known.join(", ")})`);
package/dist/gates.d.ts CHANGED
@@ -14,5 +14,12 @@ export interface Gate {
14
14
  export declare const fileNonEmpty: (file: string) => Gate;
15
15
  /** No {{...}} template slots may survive a fill step. */
16
16
  export declare const slotsFilled: (file: string) => Gate;
17
+ /**
18
+ * Workflow-specific validation as a bash script: exit 0 = passed, non-zero =
19
+ * failed and everything the script printed becomes the failure message — so
20
+ * precise per-violation output turns directly into a precise correction
21
+ * prompt. The script runs in the run directory (env: RUN_DIR).
22
+ */
23
+ export declare const checkScript: (script: string, label: string) => Gate;
17
24
  /** Fixed template parts chosen by code must survive byte-identically. */
18
25
  export declare const containsText: (file: string, needle: string, label: string) => Gate;
package/dist/gates.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { readFile, stat } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  export const fileNonEmpty = (file) => ({
@@ -22,6 +23,32 @@ export const slotsFilled = (file) => ({
22
23
  : null;
23
24
  },
24
25
  });
26
+ /**
27
+ * Workflow-specific validation as a bash script: exit 0 = passed, non-zero =
28
+ * failed and everything the script printed becomes the failure message — so
29
+ * precise per-violation output turns directly into a precise correction
30
+ * prompt. The script runs in the run directory (env: RUN_DIR).
31
+ */
32
+ export const checkScript = (script, label) => ({
33
+ name: `check(${label})`,
34
+ async check(runDir) {
35
+ const result = await new Promise((resolve, reject) => {
36
+ const child = spawn("bash", ["-c", script], {
37
+ cwd: runDir,
38
+ env: { ...process.env, RUN_DIR: runDir },
39
+ });
40
+ let output = "";
41
+ child.stdout.on("data", (chunk) => (output += String(chunk)));
42
+ child.stderr.on("data", (chunk) => (output += String(chunk)));
43
+ child.on("error", reject);
44
+ child.on("close", (code) => resolve({ code: code ?? 1, output }));
45
+ });
46
+ if (result.code === 0)
47
+ return null;
48
+ const tail = result.output.trim().split("\n").slice(-12).join("\n");
49
+ return tail || `check script failed with exit code ${result.code}`;
50
+ },
51
+ });
25
52
  /** Fixed template parts chosen by code must survive byte-identically. */
26
53
  export const containsText = (file, needle, label) => ({
27
54
  name: `contains(${file}, ${label})`,
@@ -8,7 +8,6 @@ function invokeClaude(inv) {
8
8
  ];
9
9
  const args = [
10
10
  "-p",
11
- "--chrome",
12
11
  "--output-format", "stream-json",
13
12
  "--verbose",
14
13
  "--model", inv.model,
@@ -1,2 +1,2 @@
1
- import type { BackendHooks, ChatBackend } from "./backend.js";
2
- export declare function startAcpBackend(agent: "claude" | "codex", cwd: string, hooks: BackendHooks): Promise<ChatBackend>;
1
+ import type { BackendHooks, BackendTuning, ChatBackend } from "./backend.js";
2
+ export declare function startAcpBackend(agent: "claude" | "codex", cwd: string, hooks: BackendHooks, tuning?: BackendTuning): Promise<ChatBackend>;
@@ -15,15 +15,42 @@ const ADAPTERS = {
15
15
  claude: "@agentclientprotocol/claude-agent-acp/dist/index.js",
16
16
  codex: "@agentclientprotocol/codex-acp/dist/index.js",
17
17
  };
18
+ /** Effort tier -> Claude thinking budget (the adapter reads MAX_THINKING_TOKENS). */
19
+ const CLAUDE_THINKING_BUDGET = {
20
+ low: 2048,
21
+ medium: 8192,
22
+ high: 16384,
23
+ xhigh: 32000,
24
+ max: 63999,
25
+ };
26
+ /**
27
+ * Model/effort overrides ride on adapter-specific channels: the claude
28
+ * adapter takes the model via session `_meta.claudeCode.options` and the
29
+ * thinking budget via env; codex-acp merges a CODEX_CONFIG env JSON into
30
+ * the session config it hands to `codex app-server`.
31
+ */
32
+ function tuningEnv(agent, tuning) {
33
+ const env = { ...process.env };
34
+ if (agent === "claude" && tuning.effort && CLAUDE_THINKING_BUDGET[tuning.effort]) {
35
+ env.MAX_THINKING_TOKENS = String(CLAUDE_THINKING_BUDGET[tuning.effort]);
36
+ }
37
+ if (agent === "codex" && (tuning.model || tuning.effort)) {
38
+ env.CODEX_CONFIG = JSON.stringify({
39
+ ...(tuning.model ? { model: tuning.model } : {}),
40
+ ...(tuning.effort ? { model_reasoning_effort: tuning.effort } : {}),
41
+ });
42
+ }
43
+ return env;
44
+ }
18
45
  function contentText(content) {
19
46
  return content.type === "text" ? content.text : "";
20
47
  }
21
- export async function startAcpBackend(agent, cwd, hooks) {
48
+ export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
22
49
  const entry = fileURLToPath(import.meta.resolve(ADAPTERS[agent]));
23
50
  const child = spawn(process.execPath, [entry], {
24
51
  cwd,
25
52
  stdio: ["pipe", "pipe", "pipe"],
26
- env: process.env,
53
+ env: tuningEnv(agent, tuning),
27
54
  });
28
55
  let stderr = "";
29
56
  child.stderr.on("data", (c) => {
@@ -91,7 +118,21 @@ export async function startAcpBackend(agent, cwd, hooks) {
91
118
  clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
92
119
  clientInfo: { name: "kraftwerk-inspector", version: "1.0.0" },
93
120
  });
94
- const session = await conn.newSession({ cwd, mcpServers: [] });
121
+ // Claude-only session options ride on _meta.claudeCode.options (the
122
+ // adapter spreads them into the Agent SDK options): model override,
123
+ // skill allowlist (undefined = all discovered skills), extra work dirs.
124
+ const claudeOptions = {
125
+ ...(tuning.model ? { model: tuning.model } : {}),
126
+ ...(tuning.skills ? { skills: tuning.skills } : {}),
127
+ ...(tuning.addDirs?.length ? { additionalDirectories: tuning.addDirs } : {}),
128
+ };
129
+ const session = await conn.newSession({
130
+ cwd,
131
+ mcpServers: [],
132
+ ...(agent === "claude" && Object.keys(claudeOptions).length
133
+ ? { _meta: { claudeCode: { options: claudeOptions } } }
134
+ : {}),
135
+ });
95
136
  const sessionId = session.sessionId;
96
137
  return {
97
138
  async prompt(text) {
@@ -18,6 +18,20 @@ export interface BackendHooks {
18
18
  kind?: string;
19
19
  }>): Promise<string | null>;
20
20
  }
21
+ /**
22
+ * Optional per-chat model/effort overrides (from a team member definition).
23
+ * Each backend maps them onto whatever its agent actually supports.
24
+ */
25
+ export interface BackendTuning {
26
+ model?: string;
27
+ effort?: string;
28
+ /** claude: restrict native skill discovery to these names (undefined = all). */
29
+ skills?: string[];
30
+ /** pi: skill folders to load explicitly via --skill. */
31
+ skillDirs?: string[];
32
+ /** claude: extra directories the session may work in (e.g. project root for run chats). */
33
+ addDirs?: string[];
34
+ }
21
35
  export interface ChatBackend {
22
36
  /** Send one user message; resolves with the stop reason at turn end. */
23
37
  prompt(text: string): Promise<string>;
@@ -0,0 +1,2 @@
1
+ import type { BackendHooks, BackendTuning, ChatBackend } from "./backend.js";
2
+ export declare function startPiBackend(cwd: string, hooks: BackendHooks, tuning?: BackendTuning): ChatBackend;
@@ -0,0 +1,106 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ const clip = (raw) => {
4
+ const oneLine = String(raw ?? "").replace(/\s+/g, " ").trim();
5
+ return oneLine.length > 120 ? `${oneLine.slice(0, 120)}…` : oneLine;
6
+ };
7
+ export function startPiBackend(cwd, hooks, tuning = {}) {
8
+ const sessionId = randomUUID();
9
+ let child = null;
10
+ let cancelled = false;
11
+ const tuningArgs = [
12
+ ...(tuning.model ? ["--model", tuning.model] : []),
13
+ ...(tuning.effort ? ["--thinking", tuning.effort] : []),
14
+ ...(tuning.skillDirs ?? []).flatMap((dir) => ["--skill", dir]),
15
+ ];
16
+ return {
17
+ prompt(text) {
18
+ cancelled = false;
19
+ return new Promise((resolve, reject) => {
20
+ const proc = spawn("pi", ["-p", "--mode", "json", "--session-id", sessionId, ...tuningArgs], {
21
+ cwd,
22
+ stdio: ["pipe", "pipe", "pipe"],
23
+ env: process.env,
24
+ });
25
+ child = proc;
26
+ proc.stdin.write(text);
27
+ proc.stdin.end();
28
+ let sawAssistant = false;
29
+ let failure;
30
+ let stderr = "";
31
+ let buffer = "";
32
+ let toolSeq = 0;
33
+ const handleLine = (line) => {
34
+ if (!line.trim())
35
+ return;
36
+ let event;
37
+ try {
38
+ event = JSON.parse(line);
39
+ }
40
+ catch {
41
+ return; // non-JSON noise on stdout
42
+ }
43
+ if (event.type === "tool_execution_start" && event.toolName) {
44
+ const a = event.args ?? {};
45
+ const target = clip(a.path ?? a.file_path ?? a.command ?? a.pattern ?? "");
46
+ hooks.emit({
47
+ type: "tool_call",
48
+ callId: `pi-${sessionId}-${toolSeq++}`,
49
+ title: target ? `${event.toolName} ${target}` : event.toolName,
50
+ status: "completed",
51
+ });
52
+ }
53
+ if (event.type === "message_end" && event.message?.role === "assistant") {
54
+ sawAssistant = true;
55
+ for (const block of event.message.content ?? []) {
56
+ if (block?.type === "text" && block.text?.trim()) {
57
+ hooks.emit({ type: "text", text: block.text });
58
+ }
59
+ if (block?.type === "thinking" && block.text?.trim()) {
60
+ hooks.emit({ type: "thought", text: block.text });
61
+ }
62
+ }
63
+ }
64
+ if (event.type === "error") {
65
+ failure = typeof event.error === "string" ? event.error : line;
66
+ }
67
+ // API failures don't exit non-zero: they ride on turn_end as
68
+ // stopReason "error" with an empty assistant message.
69
+ if (event.type === "turn_end" && event.message?.stopReason === "error") {
70
+ failure = clip(event.message.errorMessage ?? "unknown pi error");
71
+ }
72
+ };
73
+ proc.stdout.on("data", (chunk) => {
74
+ buffer += chunk.toString("utf8");
75
+ const lines = buffer.split("\n");
76
+ buffer = lines.pop() ?? "";
77
+ lines.forEach(handleLine);
78
+ });
79
+ proc.stderr.on("data", (chunk) => {
80
+ stderr += chunk.toString("utf8");
81
+ });
82
+ proc.on("error", (err) => reject(new Error(`could not spawn pi: ${err.message}`)));
83
+ proc.on("close", (code) => {
84
+ child = null;
85
+ handleLine(buffer);
86
+ if (cancelled)
87
+ return resolve("cancelled");
88
+ if (failure || code !== 0 || !sawAssistant) {
89
+ reject(new Error(`pi failed (exit ${code})` +
90
+ (failure ? `: ${failure}` : "") +
91
+ (stderr.trim() ? `\nstderr: ${stderr.trim().slice(-1000)}` : "")));
92
+ return;
93
+ }
94
+ resolve("end_turn");
95
+ });
96
+ });
97
+ },
98
+ cancel() {
99
+ cancelled = true;
100
+ child?.kill("SIGTERM");
101
+ },
102
+ dispose() {
103
+ child?.kill("SIGTERM");
104
+ },
105
+ };
106
+ }
@@ -0,0 +1,33 @@
1
+ import type { ChatAgentId, ChatMeta, ChatScope, StoredChatEvent } from "./types.js";
2
+ export declare function createChat(opts: {
3
+ agent: ChatAgentId;
4
+ scope: ChatScope;
5
+ /** Preset title (e.g. routine runs); otherwise the first message names the chat. */
6
+ title?: string;
7
+ }): Promise<ChatMeta>;
8
+ export declare function listChats(): Promise<Array<ChatMeta & {
9
+ busy: boolean;
10
+ }>>;
11
+ export declare function getChat(id: string): Promise<{
12
+ meta: ChatMeta;
13
+ events: StoredChatEvent[];
14
+ busy: boolean;
15
+ } | null>;
16
+ export declare function postMessage(id: string, text: string): Promise<{
17
+ error?: string;
18
+ }>;
19
+ export declare function resolvePermission(id: string, requestId: string, optionId: string | null): Promise<{
20
+ error?: string;
21
+ }>;
22
+ /** Delete a chat: kill its backend, drop the live state, remove its folder. */
23
+ export declare function deleteChat(id: string): Promise<{
24
+ error?: string;
25
+ }>;
26
+ export declare function cancelChat(id: string): Promise<{
27
+ error?: string;
28
+ }>;
29
+ /**
30
+ * SSE subscription: replays events after `afterSeq`, then streams new ones.
31
+ * Returns an unsubscribe function.
32
+ */
33
+ export declare function subscribeChat(id: string, afterSeq: number, fn: (ev: StoredChatEvent) => void): Promise<(() => void) | null>;