@oisincoveney/pipeline 1.5.1 → 1.5.3

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
@@ -152,22 +152,36 @@ Generate native host files from the YAML config:
152
152
  pipe install-commands --host all
153
153
  ```
154
154
 
155
- Generated resources are projections of the three config files; they are not
156
- separate sources of truth. The pipeline `orchestrator.profile` configures the
157
- host orchestrator surface, while workflow node `profile` values configure
158
- delegated agent boundaries.
159
-
160
- | Host | Generated files | Invocation |
161
- | --- | --- | --- |
162
- | Claude Code | `.claude/commands/pipe.md`, `.claude/agents/*.md` | `/pipe <task>` |
163
- | Codex | `.agents/skills/pipe/SKILL.md`, `.codex/agents/*.toml` | `$pipe <task>` |
164
- | OpenCode | `.opencode/commands/pipe.md`, `.opencode/agents/*.md` | `/pipe <task>` |
165
- | Kimi | `.kimi/commands/pipe.md`, `.kimi/agents/*.md` | `/pipe <task>` |
166
- | Pi | `.pi/extensions/pipe.ts`, `.pi/prompts/pipe.md` | `/pipe <task>` |
155
+ Generated resources are derived from the three config files; they are not
156
+ separate sources of truth. Host resources use exact native agents when the node
157
+ runner matches the host. Cross-runner nodes use host-native execution only when
158
+ the host can run the requested model explicitly, such as OpenCode subagents with
159
+ a resolved `model:`; otherwise generated instructions dispatch to that runner's
160
+ CLI instead of doing instruction-only translation.
161
+
162
+ | Host | Generated files | Invocation |
163
+ | ----------- | ------------------------------------------------------ | -------------------- |
164
+ | Claude Code | `.claude/commands/pipe.md`, `.claude/agents/*.md` | `/pipe <task>` |
165
+ | Codex | `.agents/skills/pipe/SKILL.md`, `.codex/agents/*.toml` | `$pipe <task>` |
166
+ | OpenCode | `.opencode/commands/pipe.md`, `.opencode/agents/*.md` | `/pipe <task>` |
167
+ | Kimi | `.kimi/skills/pipe/SKILL.md`, `.kimi/agents/*.yaml` | `/skill:pipe <task>` |
168
+ | Pi | `.pi/prompts/pipe.md`, `.pi/extensions/pipe.ts` | `/pipe <task>` |
167
169
 
168
170
  The installer is idempotent, supports `--check` and `--dry-run`, and refuses to
169
171
  overwrite manually edited files unless `--force` is supplied.
170
172
 
173
+ Runner `model` is the canonical model id. Optional
174
+ `host_models.<host>` entries are only needed when a host uses a different model
175
+ identifier:
176
+
177
+ ```yaml
178
+ runners:
179
+ kimi:
180
+ type: kimi
181
+ command: kimi
182
+ model: moonshot/kimi-k2.6
183
+ ```
184
+
171
185
  ## Runtime Guarantees
172
186
 
173
187
  - `pipe run` fails without `.pipeline/pipeline.yaml`,
@@ -189,9 +203,16 @@ External apps can import the stable config, planner, and runtime surfaces
189
203
  without deep-importing private source paths:
190
204
 
191
205
  ```ts
192
- import { loadPipelineConfig, parsePipelineConfigParts } from "@oisincoveney/pipeline/config";
206
+ import {
207
+ loadPipelineConfig,
208
+ parsePipelineConfigParts,
209
+ } from "@oisincoveney/pipeline/config";
193
210
  import { compileWorkflowPlan } from "@oisincoveney/pipeline/planner";
194
- import { runPipelineFromConfig, type PipelineRuntimeResult, type PipelineTaskContext } from "@oisincoveney/pipeline/runtime";
211
+ import {
212
+ runPipelineFromConfig,
213
+ type PipelineRuntimeResult,
214
+ type PipelineTaskContext,
215
+ } from "@oisincoveney/pipeline/runtime";
195
216
  ```
196
217
 
197
218
  ## Verification
package/dist/config.d.ts CHANGED
@@ -74,6 +74,7 @@ declare const configSchema: z.ZodObject<{
74
74
  "workspace-write": "workspace-write";
75
75
  }>;
76
76
  }, z.core.$strict>>;
77
+ host_models: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
77
78
  instructions: z.ZodObject<{
78
79
  inline: z.ZodOptional<z.ZodString>;
79
80
  path: z.ZodOptional<z.ZodString>;
@@ -150,6 +151,7 @@ declare const configSchema: z.ZodObject<{
150
151
  }>>>;
151
152
  }, z.core.$strict>;
152
153
  command: z.ZodOptional<z.ZodString>;
154
+ host_models: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
153
155
  model: z.ZodOptional<z.ZodString>;
154
156
  type: z.ZodEnum<{
155
157
  claude: "claude";
package/dist/config.js CHANGED
@@ -21357,6 +21357,7 @@ var runnerSchema = exports_external.object({
21357
21357
  args: exports_external.array(exports_external.string()).optional(),
21358
21358
  capabilities: runnerCapabilitiesSchema,
21359
21359
  command: exports_external.string().optional(),
21360
+ host_models: exports_external.record(exports_external.string(), exports_external.string().min(1)).optional(),
21360
21361
  model: exports_external.string().optional(),
21361
21362
  type: exports_external.enum(RUNNER_TYPES)
21362
21363
  }).strict();
@@ -21422,6 +21423,7 @@ var retriesSchema = exports_external.object({
21422
21423
  var profileSchema = exports_external.object({
21423
21424
  description: exports_external.string().optional(),
21424
21425
  filesystem: filesystemSchema.optional(),
21426
+ host_models: exports_external.record(exports_external.string(), exports_external.string().min(1)).optional(),
21425
21427
  instructions: instructionsSchema,
21426
21428
  mcp_servers: exports_external.array(exports_external.string()).optional(),
21427
21429
  model: exports_external.string().optional(),
package/dist/index.js CHANGED
@@ -27430,6 +27430,7 @@ var runnerSchema = exports_external.object({
27430
27430
  args: exports_external.array(exports_external.string()).optional(),
27431
27431
  capabilities: runnerCapabilitiesSchema,
27432
27432
  command: exports_external.string().optional(),
27433
+ host_models: exports_external.record(exports_external.string(), exports_external.string().min(1)).optional(),
27433
27434
  model: exports_external.string().optional(),
27434
27435
  type: exports_external.enum(RUNNER_TYPES)
27435
27436
  }).strict();
@@ -27495,6 +27496,7 @@ var retriesSchema = exports_external.object({
27495
27496
  var profileSchema = exports_external.object({
27496
27497
  description: exports_external.string().optional(),
27497
27498
  filesystem: filesystemSchema.optional(),
27499
+ host_models: exports_external.record(exports_external.string(), exports_external.string().min(1)).optional(),
27498
27500
  instructions: instructionsSchema,
27499
27501
  mcp_servers: exports_external.array(exports_external.string()).optional(),
27500
27502
  model: exports_external.string().optional(),
@@ -27938,8 +27940,8 @@ function validationError(issues) {
27938
27940
  // src/install-commands.ts
27939
27941
  var import_gray_matter = __toESM(require_gray_matter(), 1);
27940
27942
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
27941
- import { mkdir, writeFile } from "node:fs/promises";
27942
- import { dirname, join as join2 } from "node:path";
27943
+ import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
27944
+ import { dirname, join as join2, relative } from "node:path";
27943
27945
 
27944
27946
  // node_modules/smol-toml/dist/error.js
27945
27947
  /*!
@@ -28663,8 +28665,10 @@ function codeForIssue(message) {
28663
28665
  // src/install-commands.ts
28664
28666
  var GENERATED_MARKER = "<!-- Generated by @oisincoveney/pipeline. -->";
28665
28667
  var GENERATED_TS_MARKER = "// Generated by @oisincoveney/pipeline.";
28668
+ var GENERATED_YAML_MARKER = "# Generated by @oisincoveney/pipeline.";
28666
28669
  var OWNER_MARKER_PREFIX = "<!-- @oisincoveney/pipeline:";
28667
28670
  var OWNER_TS_MARKER_PREFIX = "// @oisincoveney/pipeline:";
28671
+ var OWNER_YAML_MARKER_PREFIX = "# @oisincoveney/pipeline:";
28668
28672
  var COMMAND_HOSTS = [
28669
28673
  "claude",
28670
28674
  "opencode",
@@ -28676,14 +28680,17 @@ function header(host) {
28676
28680
  return [GENERATED_MARKER, `${OWNER_MARKER_PREFIX}host=${host} -->`, ""].join(`
28677
28681
  `);
28678
28682
  }
28679
- function tsHeader(host) {
28683
+ function yamlHeader(host) {
28680
28684
  return [
28681
- GENERATED_TS_MARKER,
28682
- `${OWNER_TS_MARKER_PREFIX}host=${host}`,
28685
+ GENERATED_YAML_MARKER,
28686
+ `${OWNER_YAML_MARKER_PREFIX}host=${host}`,
28683
28687
  ""
28684
28688
  ].join(`
28685
28689
  `);
28686
28690
  }
28691
+ function hashHeader(host) {
28692
+ return yamlHeader(host);
28693
+ }
28687
28694
  function markdown(data, body) {
28688
28695
  return `${import_gray_matter.default.stringify(body.trimEnd(), data).trimEnd()}
28689
28696
  `;
@@ -28699,11 +28706,8 @@ function profileEntries(config2) {
28699
28706
  }
28700
28707
  return [...profileIds].sort((a, b) => a.localeCompare(b)).map((id) => [id, config2.profiles[id]]);
28701
28708
  }
28702
- function hasAgentWorkflowNodes(config2) {
28703
- return compileWorkflowPlan(config2).topologicalOrder.some((node) => node.kind === "agent");
28704
- }
28705
- function profileNames(config2) {
28706
- return profileEntries(config2).map(([name]) => `\`${name}\``).join(", ");
28709
+ function nativeProfileEntries(host, config2) {
28710
+ return profileEntries(config2).filter(([_, profile]) => canRunNatively(host, config2, profile));
28707
28711
  }
28708
28712
  function orchestratorProfile(config2) {
28709
28713
  const profile = config2.profiles[config2.orchestrator.profile];
@@ -28715,34 +28719,69 @@ function orchestratorProfile(config2) {
28715
28719
  hooks: config2.orchestrator.hooks
28716
28720
  };
28717
28721
  }
28718
- function workflowSummary(config2) {
28722
+ function resolvedHostModel(config2, host, profile) {
28723
+ const runner = config2.runners[profile.runner];
28724
+ return profile.host_models?.[host] ?? runner?.host_models?.[host] ?? profile.model ?? runner?.model;
28725
+ }
28726
+ function canRunNatively(host, config2, profile) {
28727
+ if (host === "pi") {
28728
+ return false;
28729
+ }
28730
+ if (profile.runner === host) {
28731
+ return true;
28732
+ }
28733
+ return host === "opencode" && isModelRunner(profile.runner) && resolvedHostModel(config2, host, profile) !== undefined;
28734
+ }
28735
+ function isModelRunner(runnerId) {
28736
+ return COMMAND_HOSTS.some((host) => host === runnerId);
28737
+ }
28738
+ function agentDispatchRoutes(host, config2) {
28719
28739
  const plan = compileWorkflowPlan(config2);
28720
- return [
28721
- `Workflow: ${plan.workflowId}`,
28722
- entrypointSummary(config2),
28723
- "",
28724
- ...plan.topologicalOrder.map((node) => {
28725
- const parts = [
28726
- `- ${node.id}`,
28727
- `kind=${node.kind}`,
28728
- node.profile ? `profile=${node.profile}` : "",
28729
- node.needs.length ? `needs=${node.needs.join(",")}` : "needs=none"
28730
- ].filter(Boolean);
28731
- return parts.join(" ");
28732
- })
28733
- ].join(`
28734
- `);
28740
+ return plan.topologicalOrder.flatMap((node) => {
28741
+ if (!(node.kind === "agent" && node.profile)) {
28742
+ return [];
28743
+ }
28744
+ const profile = config2.profiles[node.profile];
28745
+ if (!profile) {
28746
+ return [];
28747
+ }
28748
+ return [
28749
+ dispatchRouteForAgent(host, config2, {
28750
+ needs: node.needs,
28751
+ nodeId: node.id,
28752
+ profile,
28753
+ profileId: node.profile
28754
+ })
28755
+ ];
28756
+ });
28735
28757
  }
28736
- function entrypointSummary(config2) {
28737
- const entries = Object.entries(config2.entrypoints);
28738
- if (entries.length === 0) {
28739
- return "Entrypoints: none";
28758
+ function dispatchRouteForAgent(host, config2, route) {
28759
+ const runnerId = route.profile.runner;
28760
+ if (host !== "pi" && runnerId === host) {
28761
+ return {
28762
+ ...route,
28763
+ kind: "native-named-agent",
28764
+ nativeAgentId: route.profileId,
28765
+ runnerId
28766
+ };
28740
28767
  }
28741
- return [
28742
- "Entrypoints:",
28743
- ...entries.map(([id, entrypoint]) => `- ${id} -> ${entrypoint.workflow}${entrypoint.description ? ` (${entrypoint.description})` : ""}`)
28744
- ].join(`
28745
- `);
28768
+ if (host === "opencode" && isModelRunner(runnerId)) {
28769
+ const model = resolvedHostModel(config2, host, route.profile);
28770
+ if (model) {
28771
+ return {
28772
+ ...route,
28773
+ kind: "native-model-agent",
28774
+ model,
28775
+ nativeAgentId: route.profileId,
28776
+ runnerId
28777
+ };
28778
+ }
28779
+ }
28780
+ return {
28781
+ ...route,
28782
+ kind: "cli",
28783
+ runnerId
28784
+ };
28746
28785
  }
28747
28786
  function grants(actor) {
28748
28787
  return [
@@ -28767,14 +28806,126 @@ function orchestratorBlock(config2) {
28767
28806
  ].join(`
28768
28807
  `);
28769
28808
  }
28770
- function nativeDelegationInstruction(host, config2) {
28771
- if (!hasAgentWorkflowNodes(config2)) {
28809
+ function dispatchBlock(host, config2) {
28810
+ const routes = agentDispatchRoutes(host, config2);
28811
+ if (routes.length === 0) {
28772
28812
  return;
28773
28813
  }
28814
+ const plan = compileWorkflowPlan(config2);
28815
+ const nativeRoutes = routes.filter((route) => route.kind !== "cli");
28816
+ const cliRoutes = routes.filter((route) => route.kind === "cli");
28817
+ return [
28818
+ `Run workflow \`${plan.workflowId}\` for the user task.`,
28819
+ "",
28820
+ nativeDispatchBlock(host, nativeRoutes),
28821
+ cliDispatchBlock(host, cliRoutes),
28822
+ nodePromptContract(plan.workflowId, routes),
28823
+ "Do not use `pipe`, `oisin-pipeline`, or package scripts to execute workflow nodes.",
28824
+ hostSpecificDispatchGuard(host, nativeRoutes, cliRoutes)
28825
+ ].filter((line) => Boolean(line)).join(`
28826
+ `);
28827
+ }
28828
+ function nativeDispatchBlock(host, routes) {
28829
+ if (routes.length === 0) {
28830
+ return;
28831
+ }
28832
+ return [
28833
+ `${hostDisplayName(host)} native routes:`,
28834
+ ...routes.map((route) => nativeDispatchLine(host, route)),
28835
+ ""
28836
+ ].join(`
28837
+ `);
28838
+ }
28839
+ function nativeDispatchLine(host, route) {
28840
+ const needs = needsSummary(route.needs);
28841
+ if (host === "codex") {
28842
+ return `- ${route.nodeId}: spawn_agent agent_type=${route.nativeAgentId} runner=${route.runnerId} needs=${needs}`;
28843
+ }
28844
+ if (host === "claude") {
28845
+ return `- ${route.nodeId}: Agent tool subagent_type=${route.nativeAgentId} runner=${route.runnerId} needs=${needs}`;
28846
+ }
28847
+ if (host === "kimi") {
28848
+ return `- ${route.nodeId}: Agent tool subagent_type=${route.nativeAgentId} runner=${route.runnerId} needs=${needs}`;
28849
+ }
28774
28850
  if (host === "opencode") {
28775
- return "Use OpenCode's task tool to delegate each agent workflow node to its configured profile. Do not invoke package scripts or the pipeline CLI to run this workflow.";
28851
+ const model = route.model ? ` model=${route.model}` : "";
28852
+ return `- ${route.nodeId}: Task tool subagent_type=${route.nativeAgentId}${model} runner=${route.runnerId} needs=${needs}`;
28853
+ }
28854
+ return `- ${route.nodeId}: native agent ${route.nativeAgentId} runner=${route.runnerId} needs=${needs}`;
28855
+ }
28856
+ function cliDispatchBlock(host, routes) {
28857
+ if (routes.length === 0) {
28858
+ return;
28859
+ }
28860
+ const nativeNotice = host === "pi" ? "Pi native dispatch is not enabled for this generated workflow." : `These nodes are not ${hostDisplayName(host)} native routes.`;
28861
+ return [nativeNotice, "CLI routes:", ...routes.map(cliDispatchLine), ""].join(`
28862
+ `);
28863
+ }
28864
+ function cliDispatchLine(route) {
28865
+ return `- ${route.nodeId}: ${route.runnerId} CLI profile=${route.profileId} command=\`${runnerCliCommand(route)}\` needs=${needsSummary(route.needs)}`;
28866
+ }
28867
+ function runnerCliCommand(route) {
28868
+ if (route.runnerId === "codex") {
28869
+ return `codex exec --json -C <repo-root> --sandbox ${codexSandbox(route.profile)} --skip-git-repo-check <node prompt>`;
28870
+ }
28871
+ if (route.runnerId === "kimi") {
28872
+ return `kimi --print --agent-file .kimi/agents/${route.profileId}.yaml --work-dir <repo-root> --final-message-only --prompt <node prompt>`;
28873
+ }
28874
+ if (route.runnerId === "opencode") {
28875
+ return `opencode run --agent ${route.profileId} --format json --dir <repo-root> <node prompt>`;
28876
+ }
28877
+ if (route.runnerId === "claude") {
28878
+ return `claude --agent ${route.profileId} --print -p <node prompt>`;
28879
+ }
28880
+ if (route.runnerId === "pi") {
28881
+ return "pi --print --no-session <node prompt>";
28776
28882
  }
28777
- return "Use this host's native subagent mechanism for agent workflow nodes. Do not invoke package scripts or the pipeline CLI to run this workflow.";
28883
+ return `${route.runnerId} <node prompt>`;
28884
+ }
28885
+ function codexSandbox(profile) {
28886
+ return profile.filesystem?.mode === "workspace-write" ? "workspace-write" : "read-only";
28887
+ }
28888
+ function nodePromptContract(workflowId, routes) {
28889
+ const hasCliRoutes = routes.some((route) => route.kind === "cli");
28890
+ const lead = hasCliRoutes ? "For each CLI node prompt include:" : "For each native node prompt include:";
28891
+ return [
28892
+ lead,
28893
+ "- user task",
28894
+ `- workflow id: ${workflowId}`,
28895
+ "- node id",
28896
+ "- profile id",
28897
+ "- runner id",
28898
+ "- profile instructions reference",
28899
+ "- profile grants",
28900
+ "- dependency outputs",
28901
+ ""
28902
+ ].join(`
28903
+ `);
28904
+ }
28905
+ function hostSpecificDispatchGuard(host, nativeRoutes, cliRoutes) {
28906
+ if (host === "codex" && nativeRoutes.length > 0) {
28907
+ return "Do not substitute the generic Codex worker for configured profiles.";
28908
+ }
28909
+ if (cliRoutes.length > 0 && nativeRoutes.length > 0) {
28910
+ return `Do not claim CLI routes are ${hostDisplayName(host)} native routes.`;
28911
+ }
28912
+ if (cliRoutes.length > 0 && nativeRoutes.length === 0 && host !== "pi") {
28913
+ return `Do not claim these nodes are ${hostDisplayName(host)} subagents.`;
28914
+ }
28915
+ return;
28916
+ }
28917
+ function hostDisplayName(host) {
28918
+ const names = {
28919
+ claude: "Claude",
28920
+ codex: "Codex",
28921
+ kimi: "Kimi",
28922
+ opencode: "OpenCode",
28923
+ pi: "Pi"
28924
+ };
28925
+ return names[host];
28926
+ }
28927
+ function needsSummary(needs) {
28928
+ return needs.length > 0 ? needs.join(",") : "none";
28778
28929
  }
28779
28930
  function compactLines(lines) {
28780
28931
  return lines.filter((line) => line !== undefined);
@@ -28803,20 +28954,16 @@ function claudeDefinitions(config2) {
28803
28954
  }, compactLines([
28804
28955
  header("claude").trimEnd(),
28805
28956
  "",
28806
- workflowSummary(config2),
28807
- "",
28808
28957
  orchestratorBlock(config2),
28809
28958
  "",
28810
- nativeDelegationInstruction("claude", config2),
28811
- "",
28812
- `Delegate work only to configured profiles: ${profileNames(config2)}.`
28959
+ dispatchBlock("claude", config2)
28813
28960
  ]).join(`
28814
28961
  `)),
28815
28962
  host: "claude",
28816
28963
  invocation: "/pipe <task description>",
28817
28964
  path: ".claude/commands/pipe.md"
28818
28965
  },
28819
- ...profileEntries(config2).map(([id, profile]) => ({
28966
+ ...nativeProfileEntries("claude", config2).map(([id, profile]) => ({
28820
28967
  content: markdown({
28821
28968
  description: profile.description ?? id,
28822
28969
  name: id,
@@ -28856,18 +29003,13 @@ function opencodeDefinitions(config2) {
28856
29003
  {
28857
29004
  content: markdown({
28858
29005
  agent: "pipeline-orchestrator",
28859
- description: "Run the configured pipeline workflow",
28860
- subtask: true
29006
+ description: "Run the configured pipeline workflow"
28861
29007
  }, compactLines([
28862
29008
  header("opencode").trimEnd(),
28863
29009
  "",
28864
- workflowSummary(config2),
28865
- "",
28866
29010
  orchestratorBlock(config2),
28867
29011
  "",
28868
- nativeDelegationInstruction("opencode", config2),
28869
- "",
28870
- `Delegate work only to configured profiles: ${profileNames(config2)}.`
29012
+ dispatchBlock("opencode", config2)
28871
29013
  ]).join(`
28872
29014
  `)),
28873
29015
  host: "opencode",
@@ -28879,24 +29021,25 @@ function opencodeDefinitions(config2) {
28879
29021
  description: "Orchestrate the configured pipeline and enforce gates.",
28880
29022
  mode: "primary",
28881
29023
  permission: opencodePermission(orchestratorProfile(config2), {
28882
- forceTask: hasAgentWorkflowNodes(config2)
29024
+ forceTask: agentDispatchRoutes("opencode", config2).some((route) => route.kind !== "cli")
28883
29025
  })
28884
29026
  }, compactLines([
28885
29027
  header("opencode").trimEnd(),
28886
29028
  "",
28887
29029
  orchestratorBlock(config2),
28888
29030
  "",
28889
- nativeDelegationInstruction("opencode", config2)
29031
+ dispatchBlock("opencode", config2)
28890
29032
  ]).join(`
28891
29033
  `)),
28892
29034
  host: "opencode",
28893
29035
  invocation: "/pipe <task description>",
28894
29036
  path: ".opencode/agents/pipeline-orchestrator.md"
28895
29037
  },
28896
- ...profileEntries(config2).map(([id, profile]) => ({
29038
+ ...nativeProfileEntries("opencode", config2).map(([id, profile]) => ({
28897
29039
  content: markdown({
28898
29040
  description: profile.description ?? id,
28899
29041
  mode: "subagent",
29042
+ ...opencodeModelProjection(config2, profile),
28900
29043
  permission: opencodePermission(profile)
28901
29044
  }, [
28902
29045
  header("opencode").trimEnd(),
@@ -28926,21 +29069,17 @@ function codexDefinitions(config2) {
28926
29069
  "",
28927
29070
  "Invoke this skill with `$pipe <task description>`.",
28928
29071
  "",
28929
- workflowSummary(config2),
28930
- "",
28931
29072
  orchestratorBlock(config2),
28932
29073
  "",
28933
- nativeDelegationInstruction("codex", config2),
28934
- "",
28935
- `Use separate configured profiles: ${profileNames(config2)}.`
29074
+ dispatchBlock("codex", config2)
28936
29075
  ]).join(`
28937
29076
  `)),
28938
29077
  host: "codex",
28939
29078
  invocation: "$pipe <task description>",
28940
29079
  path: ".agents/skills/pipe/SKILL.md"
28941
29080
  },
28942
- ...profileEntries(config2).map(([id, profile]) => ({
28943
- content: `${stringify({
29081
+ ...nativeProfileEntries("codex", config2).map(([id, profile]) => ({
29082
+ content: `${hashHeader("codex")}${stringify({
28944
29083
  description: profile.description ?? id,
28945
29084
  developer_instructions: [
28946
29085
  profile.description ?? id,
@@ -28963,39 +29102,153 @@ function kimiDefinitions(config2) {
28963
29102
  return [
28964
29103
  {
28965
29104
  content: markdown({
29105
+ name: "pipe",
28966
29106
  description: "Run the configured pipeline workflow with Kimi agents"
28967
29107
  }, compactLines([
28968
29108
  header("kimi").trimEnd(),
28969
29109
  "",
28970
- workflowSummary(config2),
28971
- "",
28972
29110
  orchestratorBlock(config2),
28973
29111
  "",
28974
- nativeDelegationInstruction("kimi", config2),
28975
- "",
28976
- `Use separate configured profiles: ${profileNames(config2)}.`
29112
+ dispatchBlock("kimi", config2)
28977
29113
  ]).join(`
28978
29114
  `)),
28979
29115
  host: "kimi",
28980
- invocation: "/pipe <task description>",
28981
- path: ".kimi/commands/pipe.md"
29116
+ invocation: "/skill:pipe <task description>",
29117
+ path: ".kimi/skills/pipe/SKILL.md"
28982
29118
  },
28983
- ...profileEntries(config2).map(([id, profile]) => ({
28984
- content: markdown({ description: profile.description ?? id, name: id }, [
29119
+ ...kimiOrchestratorAgentDefinitions(config2),
29120
+ ...nativeProfileEntries("kimi", config2).flatMap(([id, profile]) => kimiAgentDefinitions(id, profile))
29121
+ ];
29122
+ }
29123
+ function opencodeModelProjection(config2, profile) {
29124
+ const model = resolvedHostModel(config2, "opencode", profile);
29125
+ return model ? { model } : {};
29126
+ }
29127
+ function kimiAgentDefinitions(id, profile) {
29128
+ const agentPath = `.kimi/agents/${id}.yaml`;
29129
+ const promptPath = `.kimi/agents/${id}.prompt.md`;
29130
+ const promptDefinitions = profile.instructions.inline ? [
29131
+ {
29132
+ content: [
28985
29133
  header("kimi").trimEnd(),
28986
29134
  "",
28987
- profile.description ?? id,
29135
+ profile.instructions.inline,
29136
+ ""
29137
+ ].join(`
29138
+ `),
29139
+ host: "kimi",
29140
+ invocation: "/skill:pipe <task description>",
29141
+ path: promptPath
29142
+ }
29143
+ ] : [];
29144
+ const systemPromptPath = profile.instructions.path ? relative(dirname(agentPath), profile.instructions.path).replaceAll("\\", "/") : `./${id}.prompt.md`;
29145
+ return [
29146
+ {
29147
+ content: [
29148
+ yamlHeader("kimi").trimEnd(),
28988
29149
  "",
28989
- "Configured grants:",
28990
- grants(profile),
29150
+ $stringify({
29151
+ version: 1,
29152
+ agent: {
29153
+ allowed_tools: kimiAllowedTools(profile),
29154
+ extend: "default",
29155
+ name: id,
29156
+ system_prompt_path: systemPromptPath
29157
+ }
29158
+ }).trimEnd(),
29159
+ ""
29160
+ ].join(`
29161
+ `),
29162
+ host: "kimi",
29163
+ invocation: "/skill:pipe <task description>",
29164
+ path: agentPath
29165
+ },
29166
+ ...promptDefinitions
29167
+ ];
29168
+ }
29169
+ function kimiOrchestratorAgentDefinitions(config2) {
29170
+ if (agentDispatchRoutes("kimi", config2).every((route) => route.kind !== "native-named-agent")) {
29171
+ return [];
29172
+ }
29173
+ const agentPath = ".kimi/agents/pipeline-orchestrator.yaml";
29174
+ const promptPath = ".kimi/agents/pipeline-orchestrator.prompt.md";
29175
+ const nativeKimiProfiles = nativeProfileEntries("kimi", config2);
29176
+ const subagents = Object.fromEntries(nativeKimiProfiles.map(([id, profile]) => [
29177
+ id,
29178
+ {
29179
+ description: profile.description ?? id,
29180
+ path: `./${id}.yaml`
29181
+ }
29182
+ ]));
29183
+ const plan = compileWorkflowPlan(config2);
29184
+ const hasKimiNodes = plan.topologicalOrder.some((node) => {
29185
+ if (!(node.kind === "agent" && node.profile)) {
29186
+ return false;
29187
+ }
29188
+ return config2.profiles[node.profile]?.runner === "kimi";
29189
+ });
29190
+ const hasNonKimiNodes = plan.topologicalOrder.some((node) => {
29191
+ if (!(node.kind === "agent" && node.profile)) {
29192
+ return false;
29193
+ }
29194
+ return config2.profiles[node.profile]?.runner !== "kimi";
29195
+ });
29196
+ const allowedTools = [
29197
+ ...hasKimiNodes ? ["kimi_cli.tools.agent:Agent"] : [],
29198
+ ...hasNonKimiNodes ? ["kimi_cli.tools.shell:Shell"] : []
29199
+ ];
29200
+ return [
29201
+ {
29202
+ content: [
29203
+ yamlHeader("kimi").trimEnd(),
28991
29204
  "",
28992
- instructionsPointer(profile)
29205
+ $stringify({
29206
+ version: 1,
29207
+ agent: {
29208
+ allowed_tools: allowedTools,
29209
+ extend: "default",
29210
+ name: "pipeline-orchestrator",
29211
+ ...Object.keys(subagents).length > 0 ? { subagents } : {},
29212
+ system_prompt_path: "./pipeline-orchestrator.prompt.md"
29213
+ }
29214
+ }).trimEnd(),
29215
+ ""
28993
29216
  ].join(`
28994
- `)),
29217
+ `),
28995
29218
  host: "kimi",
28996
- invocation: "/pipe <task description>",
28997
- path: `.kimi/agents/${id}.md`
28998
- }))
29219
+ invocation: 'kimi --agent-file .kimi/agents/pipeline-orchestrator.yaml --work-dir <repo-root> --prompt "/skill:pipe <task description>"',
29220
+ path: agentPath
29221
+ },
29222
+ {
29223
+ content: compactLines([
29224
+ header("kimi").trimEnd(),
29225
+ "",
29226
+ orchestratorBlock(config2),
29227
+ "",
29228
+ dispatchBlock("kimi", config2),
29229
+ "",
29230
+ "This agent file is the Kimi-native orchestrator surface. Launch Kimi with `--agent-file .kimi/agents/pipeline-orchestrator.yaml` before using `/skill:pipe` when you want Kimi runner nodes to run through Kimi's native Agent tool."
29231
+ ]).join(`
29232
+ `),
29233
+ host: "kimi",
29234
+ invocation: 'kimi --agent-file .kimi/agents/pipeline-orchestrator.yaml --work-dir <repo-root> --prompt "/skill:pipe <task description>"',
29235
+ path: promptPath
29236
+ }
29237
+ ];
29238
+ }
29239
+ function kimiAllowedTools(profile) {
29240
+ const mapped = new Map([
29241
+ ["bash", "kimi_cli.tools.shell:Shell"],
29242
+ ["edit", "kimi_cli.tools.file:StrReplaceFile"],
29243
+ ["glob", "kimi_cli.tools.file:Glob"],
29244
+ ["grep", "kimi_cli.tools.file:Grep"],
29245
+ ["list", "kimi_cli.tools.file:Glob"],
29246
+ ["read", "kimi_cli.tools.file:ReadFile"],
29247
+ ["task", "kimi_cli.tools.agent:Agent"],
29248
+ ["write", "kimi_cli.tools.file:WriteFile"]
29249
+ ]);
29250
+ return [
29251
+ ...new Set((profile.tools ?? []).flatMap((tool) => mapped.get(tool) ?? []))
28999
29252
  ];
29000
29253
  }
29001
29254
  function piDefinitions(config2) {
@@ -29003,111 +29256,21 @@ function piDefinitions(config2) {
29003
29256
  {
29004
29257
  content: markdown({
29005
29258
  "argument-hint": "<task description>",
29006
- description: "Run the configured pipeline workflow with Pi subagents"
29259
+ description: "Run the configured pipeline workflow"
29007
29260
  }, compactLines([
29008
29261
  header("pi").trimEnd(),
29009
29262
  "",
29010
- workflowSummary(config2),
29011
- "",
29012
29263
  orchestratorBlock(config2),
29013
29264
  "",
29014
- nativeDelegationInstruction("pi", config2)
29265
+ dispatchBlock("pi", config2)
29015
29266
  ]).join(`
29016
29267
  `)),
29017
29268
  host: "pi",
29018
29269
  invocation: "/pipe <task description>",
29019
29270
  path: ".pi/prompts/pipe.md"
29020
- },
29021
- {
29022
- content: [
29023
- tsHeader("pi").trimEnd(),
29024
- "",
29025
- "interface PiCommand {",
29026
- " name: string;",
29027
- "}",
29028
- "interface PiCommandContext {",
29029
- " sendUserMessage(message: string): Promise<void> | void;",
29030
- " ui: { notify(message: string, type?: string): void };",
29031
- "}",
29032
- "interface PiExtensionApi {",
29033
- " getCommands?(): PiCommand[];",
29034
- " registerCommand(",
29035
- " name: string,",
29036
- " command: {",
29037
- " description: string;",
29038
- " handler(args: string, ctx: PiCommandContext): Promise<void> | void;",
29039
- " }",
29040
- " ): void;",
29041
- "}",
29042
- "",
29043
- ...piOrchestratorComment(config2),
29044
- "",
29045
- ...piWorkflowNodesLiteral(config2),
29046
- "",
29047
- "function renderSubagentCommand(task: string): string {",
29048
- ' const chain = WORKFLOW_NODES.filter((node) => node.kind === "agent")',
29049
- " .map((node) => `" + "$" + "{String(node.profile)}: " + "$" + "{node.id} " + "$" + "{task}" + "`)",
29050
- ' .join(" -> ");',
29051
- [" return `/chain ", "$", "{JSON.stringify(chain)}`;"].join(""),
29052
- "}",
29053
- "",
29054
- "export default function pipelineWorkNext(pi: PiExtensionApi): void {",
29055
- ' pi.registerCommand("pipe", {',
29056
- ' description: "Run the configured pipeline with Pi subagents",',
29057
- " handler: async (args: string, ctx) => {",
29058
- ' const task = String(args ?? "").trim();',
29059
- " if (!task) {",
29060
- ' ctx.ui.notify("Usage: /pipe <task description>", "error");',
29061
- " return;",
29062
- " }",
29063
- " const commands =",
29064
- ' typeof pi.getCommands === "function" ? pi.getCommands() : [];',
29065
- " const hasPiSubagents = commands.some((command) =>",
29066
- ' ["run", "chain", "parallel", "run-chain", "subagents-doctor"].some(',
29067
- " (name) => command.name === name || command.name.startsWith(`" + "$" + "{name}:" + "`)",
29068
- " )",
29069
- " );",
29070
- " if (!hasPiSubagents) {",
29071
- ' ctx.ui.notify("Install pi-subagents before running /pipe.", "error");',
29072
- " return;",
29073
- " }",
29074
- " await ctx.sendUserMessage(renderSubagentCommand(task));",
29075
- " },",
29076
- " });",
29077
- "}",
29078
- ""
29079
- ].join(`
29080
- `),
29081
- host: "pi",
29082
- invocation: "/pipe <task description>",
29083
- path: ".pi/extensions/pipe.ts"
29084
29271
  }
29085
29272
  ];
29086
29273
  }
29087
- function piWorkflowNodesLiteral(config2) {
29088
- const plan = compileWorkflowPlan(config2);
29089
- return [
29090
- "const WORKFLOW_NODES = [",
29091
- ...plan.topologicalOrder.flatMap((node) => [
29092
- " {",
29093
- ` id: ${JSON.stringify(node.id)},`,
29094
- ` kind: ${JSON.stringify(node.kind)},`,
29095
- ` needs: ${JSON.stringify(node.needs)},`,
29096
- ` profile: ${JSON.stringify(node.profile ?? null)},`,
29097
- " },"
29098
- ]),
29099
- "] as const;"
29100
- ];
29101
- }
29102
- function piOrchestratorComment(config2) {
29103
- return compactLines([
29104
- "/*",
29105
- orchestratorBlock(config2),
29106
- "",
29107
- nativeDelegationInstruction("pi", config2),
29108
- "*/"
29109
- ]);
29110
- }
29111
29274
  function instructionsPointer(actor) {
29112
29275
  if (actor.instructions.path) {
29113
29276
  return `Instructions: ${actor.instructions.path}`;
@@ -29126,6 +29289,67 @@ function definitionsFor(host, config2) {
29126
29289
  const hosts = host === "all" ? COMMAND_HOSTS : [host];
29127
29290
  return hosts.flatMap((name) => definitions[name]());
29128
29291
  }
29292
+ function selectedHosts(host) {
29293
+ return host === "all" ? [...COMMAND_HOSTS] : [host];
29294
+ }
29295
+ var GENERATED_RESOURCE_ROOTS = {
29296
+ claude: [".claude/commands", ".claude/agents"],
29297
+ codex: [".agents/skills", ".codex/agents"],
29298
+ kimi: [".kimi/skills", ".kimi/agents", ".kimi/commands"],
29299
+ opencode: [".opencode/commands", ".opencode/agents"],
29300
+ pi: [".pi/prompts", ".pi/extensions"]
29301
+ };
29302
+ async function listFiles(root) {
29303
+ if (!existsSync2(root)) {
29304
+ return [];
29305
+ }
29306
+ const entries = await readdir(root, { withFileTypes: true });
29307
+ const files = await Promise.all(entries.map((entry) => {
29308
+ const path = join2(root, entry.name);
29309
+ if (entry.isDirectory()) {
29310
+ return listFiles(path);
29311
+ }
29312
+ return [path];
29313
+ }));
29314
+ return files.flat();
29315
+ }
29316
+ function generatedHostFor(content) {
29317
+ return COMMAND_HOSTS.find((host) => content.includes(`${OWNER_MARKER_PREFIX}host=${host} -->`) || content.includes(`${OWNER_TS_MARKER_PREFIX}host=${host}`) || content.includes(`${OWNER_YAML_MARKER_PREFIX}host=${host}`));
29318
+ }
29319
+ async function obsoleteGeneratedItems(cwd, host, wantedPaths) {
29320
+ const hosts = new Set(selectedHosts(host));
29321
+ const roots = selectedHosts(host).flatMap((selectedHost) => GENERATED_RESOURCE_ROOTS[selectedHost]);
29322
+ const files = await Promise.all(roots.map((root) => listFiles(join2(cwd, root))));
29323
+ return files.flat().flatMap((absolutePath) => {
29324
+ const content = readFileSync2(absolutePath, "utf8");
29325
+ const generatedHost = generatedHostFor(content);
29326
+ if (!(generatedHost && hosts.has(generatedHost))) {
29327
+ return [];
29328
+ }
29329
+ const path = relative(cwd, absolutePath).replaceAll("\\", "/");
29330
+ if (wantedPaths.has(path)) {
29331
+ return [];
29332
+ }
29333
+ return [
29334
+ {
29335
+ action: "delete",
29336
+ host: generatedHost,
29337
+ invocation: invocationForHost(generatedHost),
29338
+ path
29339
+ }
29340
+ ];
29341
+ }).sort((a, b) => a.path.localeCompare(b.path));
29342
+ }
29343
+ function invocationForHost(host) {
29344
+ const invocations = {
29345
+ claude: "/pipe <task description>",
29346
+ codex: "$pipe <task description>",
29347
+ kimi: "/skill:pipe <task description>",
29348
+ opencode: "/pipe <task description>",
29349
+ pi: "/pipe <task description>"
29350
+ };
29351
+ return invocations[host];
29352
+ }
29129
29353
  function actionFor(path, content, force) {
29130
29354
  if (!existsSync2(path)) {
29131
29355
  return "create";
@@ -29134,7 +29358,7 @@ function actionFor(path, content, force) {
29134
29358
  if (current === content) {
29135
29359
  return "unchanged";
29136
29360
  }
29137
- if (!(current.includes(GENERATED_MARKER) || current.includes(GENERATED_TS_MARKER) || force)) {
29361
+ if (!(current.includes(GENERATED_MARKER) || current.includes(GENERATED_TS_MARKER) || current.includes(GENERATED_YAML_MARKER) || force)) {
29138
29362
  return "conflict";
29139
29363
  }
29140
29364
  return "update";
@@ -29144,7 +29368,9 @@ async function installCommands(options2 = {}) {
29144
29368
  const host = options2.host ?? "all";
29145
29369
  const config2 = loadPipelineConfig(cwd);
29146
29370
  const items = [];
29147
- for (const definition of definitionsFor(host, config2)) {
29371
+ const definitions = definitionsFor(host, config2);
29372
+ const wantedPaths = new Set(definitions.map((definition) => definition.path));
29373
+ for (const definition of definitions) {
29148
29374
  const target = join2(cwd, definition.path);
29149
29375
  const action = actionFor(target, definition.content, Boolean(options2.force));
29150
29376
  items.push({
@@ -29162,6 +29388,13 @@ async function installCommands(options2 = {}) {
29162
29388
  await mkdir(dirname(target), { recursive: true });
29163
29389
  await writeFile(target, definition.content);
29164
29390
  }
29391
+ const obsoleteItems = await obsoleteGeneratedItems(cwd, host, wantedPaths);
29392
+ items.push(...obsoleteItems);
29393
+ if (!(options2.check || options2.dryRun)) {
29394
+ for (const item of obsoleteItems) {
29395
+ await rm(join2(cwd, item.path), { force: true });
29396
+ }
29397
+ }
29165
29398
  if (!options2.dryRun && items.some((item) => item.action === "conflict")) {
29166
29399
  throw new Error([
29167
29400
  "Refusing to overwrite manually edited command files.",
@@ -29619,7 +29852,7 @@ function hostResourceInput(host) {
29619
29852
  return [
29620
29853
  `# ${host} Resource Input`,
29621
29854
  "",
29622
- "This file is scaffolded input for host-specific resource projection.",
29855
+ "This file is scaffolded input for host-specific generated resources.",
29623
29856
  "The source of truth is `.pipeline/pipeline.yaml` plus `.pipeline/profiles.yaml` and `.pipeline/runners.yaml`; generated host resources must preserve the profiles, prompts, rules, tools, filesystem policy, network policy, and output contracts declared there.",
29624
29857
  ""
29625
29858
  ].join(`
@@ -36912,8 +37145,6 @@ function createActorLaunchPlan(config2, input, actor, runnerId) {
36912
37145
  function piArgv(prompt, config2, actor, worktreePath = process.cwd(), runner) {
36913
37146
  return [
36914
37147
  "--print",
36915
- "--mode",
36916
- "json",
36917
37148
  ...optionalModelArgs("pi", runner, actor),
36918
37149
  ...piToolArgs(actor?.tools ?? []),
36919
37150
  ...skillArgsFor("pi", config2, actor, worktreePath),
@@ -38642,7 +38873,10 @@ function createCliProgram() {
38642
38873
  console.log(formatPipelineInitResult(result));
38643
38874
  });
38644
38875
  program2.command("install-commands").description("Install generated slash-command adapters into this repository").addOption(new Option("--host <host>", "host command set to install").choices(["all", "claude", "opencode", "codex", "kimi", "pi"]).default("all").argParser(parseCommandHost)).option("--dry-run", "show planned changes without writing files").option("--check", "fail if generated command files are missing or stale").option("--force", "overwrite manually edited command files").action(async (flags) => {
38645
- const result = await installCommands(flags);
38876
+ const result = await installCommands({
38877
+ ...flags,
38878
+ cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd()
38879
+ });
38646
38880
  console.log(formatInstallCommandsResult(result));
38647
38881
  });
38648
38882
  return program2;
@@ -1,7 +1,7 @@
1
1
  export declare const COMMAND_HOSTS: readonly ["claude", "opencode", "codex", "kimi", "pi"];
2
2
  export type CommandHost = (typeof COMMAND_HOSTS)[number];
3
3
  export type CommandHostSelection = CommandHost | "all";
4
- export type InstallAction = "conflict" | "create" | "unchanged" | "update";
4
+ export type InstallAction = "conflict" | "create" | "delete" | "unchanged" | "update";
5
5
  export interface CommandInstallPlanItem {
6
6
  action: InstallAction;
7
7
  host: CommandHost;
@@ -28560,6 +28560,7 @@ var runnerSchema = exports_external.object({
28560
28560
  args: exports_external.array(exports_external.string()).optional(),
28561
28561
  capabilities: runnerCapabilitiesSchema,
28562
28562
  command: exports_external.string().optional(),
28563
+ host_models: exports_external.record(exports_external.string(), exports_external.string().min(1)).optional(),
28563
28564
  model: exports_external.string().optional(),
28564
28565
  type: exports_external.enum(RUNNER_TYPES)
28565
28566
  }).strict();
@@ -28625,6 +28626,7 @@ var retriesSchema = exports_external.object({
28625
28626
  var profileSchema = exports_external.object({
28626
28627
  description: exports_external.string().optional(),
28627
28628
  filesystem: filesystemSchema.optional(),
28629
+ host_models: exports_external.record(exports_external.string(), exports_external.string().min(1)).optional(),
28628
28630
  instructions: instructionsSchema,
28629
28631
  mcp_servers: exports_external.array(exports_external.string()).optional(),
28630
28632
  model: exports_external.string().optional(),
@@ -29627,8 +29629,6 @@ function createActorLaunchPlan(config2, input, actor, runnerId) {
29627
29629
  function piArgv(prompt, config2, actor, worktreePath = process.cwd(), runner) {
29628
29630
  return [
29629
29631
  "--print",
29630
- "--mode",
29631
- "json",
29632
29632
  ...optionalModelArgs("pi", runner, actor),
29633
29633
  ...piToolArgs(actor?.tools ?? []),
29634
29634
  ...skillArgsFor("pi", config2, actor, worktreePath),
package/dist/runner.js CHANGED
@@ -7381,8 +7381,6 @@ async function execaHarnessPi(prompt, contextFile, worktreePath) {
7381
7381
  ${prompt}` : prompt;
7382
7382
  const argv = [
7383
7383
  "--print",
7384
- "--mode",
7385
- "json",
7386
7384
  ...optionalModelArgs("pi"),
7387
7385
  "--no-session",
7388
7386
  effectivePrompt
@@ -7477,8 +7475,6 @@ function createActorLaunchPlan(config, input, actor, runnerId) {
7477
7475
  function piArgv(prompt, config, actor, worktreePath = process.cwd(), runner) {
7478
7476
  return [
7479
7477
  "--print",
7480
- "--mode",
7481
- "json",
7482
7478
  ...optionalModelArgs("pi", runner, actor),
7483
7479
  ...piToolArgs(actor?.tools ?? []),
7484
7480
  ...skillArgsFor("pi", config, actor, worktreePath),
@@ -189,14 +189,14 @@ timeouts, output limits, sanitized env, and explicit trust flags.
189
189
 
190
190
  ## Host Support Matrix
191
191
 
192
- | Runner | Native subagents | Rules | Skills | MCP | Outputs | Generated resources |
193
- | --- | --- | --- | --- | --- | --- | --- |
194
- | Claude | yes | yes | projected as text when declared | yes | text, JSON, schema | command plus `.claude/agents` |
195
- | Codex | yes | yes | yes | yes | text, JSON, JSONL, schema | skill plus `.codex/agents` |
196
- | OpenCode | yes | yes | projected as text when declared | yes | text, JSON, JSONL, schema | command plus `.opencode/agents` |
197
- | Kimi | yes | yes | projected as text when declared | no | text, JSON | command plus `.kimi/agents` |
198
- | Pi | yes, with pi-subagents | yes | projected as text when declared | no | text, JSON | extension plus prompt |
199
- | command | no | no | no | no | declared by runner | subprocess argv |
192
+ | Runner | Native subagents | Rules | Skills | MCP | Outputs | Generated resources |
193
+ | -------- | ---------------------- | ----- | --------------------------------------------- | --- | ------------------------- | -------------------------------- |
194
+ | Claude | yes | yes | included in generated profile text | yes | text, JSON, schema | command plus `.claude/agents` |
195
+ | Codex | yes | yes | yes | yes | text, JSON, JSONL, schema | skill plus `.codex/agents` |
196
+ | OpenCode | yes | yes | included in generated profile text | yes | text, JSON, JSONL, schema | command plus `.opencode/agents` |
197
+ | Kimi | yes | yes | surfaced through project skills when declared | no | text, JSON | skill plus `.kimi/agents/*.yaml` |
198
+ | Pi | yes, with pi-subagents | yes | included in generated prompt text | no | text, JSON | prompt plus no-op extension shim |
199
+ | command | no | no | no | no | declared by runner | subprocess argv |
200
200
 
201
201
  The runtime prefers native subagents when the runner advertises
202
202
  `native_subagents: true` and the configured permissions, runner, output, and
@@ -204,6 +204,15 @@ resource grants can be represented safely. Otherwise it uses a subprocess for
204
204
  the agent node. In both cases each agent node records a separate invocation
205
205
  boundary; multi-agent workflows are never collapsed into one prompt.
206
206
 
207
+ Generated host resources follow a native-first, runner-correct rule. Same-host
208
+ agent nodes use exact native subagents. Cross-runner nodes use host-native
209
+ execution only when the host can explicitly run the requested model; OpenCode
210
+ does this through per-agent `model:` values resolved from profile or runner
211
+ `model`, with optional `host_models.opencode` overrides when model ids differ.
212
+ If native execution cannot represent the requested runner/model, generated
213
+ instructions dispatch to that runner's CLI instead of doing instruction-only
214
+ translation.
215
+
207
216
  ## Troubleshooting
208
217
 
209
218
  - Missing config: run `pipe init`; `pipe run` requires
@@ -212,8 +221,9 @@ boundary; multi-agent workflows are never collapsed into one prompt.
212
221
  - Capability error: reduce the profile grants or choose a runner whose declared
213
222
  capabilities include the requested tools, filesystem, network, output, rules,
214
223
  skills, or MCP access.
215
- - Pi native execution error: install and enable `pi-subagents`; generated Pi
216
- resources check for its commands before sending a chain.
224
+ - Pi native execution unavailable: install and enable `pi-subagents` if you want
225
+ Pi-native chains. Otherwise `/pipe` uses the CLI dispatch instructions in the
226
+ generated Pi prompt.
217
227
  - Gate failure: inspect `pipe run` output for node, gate, reason, and evidence.
218
228
  Dependent nodes are not executed after a required gate fails.
219
229
  - Schema failure: ensure the agent emits valid JSON and that `schema_path`
@@ -1,8 +1,9 @@
1
1
  # Host Resource Adapter Contract
2
2
 
3
- Generated host resources are projections of `.pipeline/runners.yaml`,
4
- `.pipeline/profiles.yaml`, and `.pipeline/pipeline.yaml`. They do not shell out
5
- to a legacy command and do not maintain independent profile definitions.
3
+ Generated host resources are derived from `.pipeline/runners.yaml`,
4
+ `.pipeline/profiles.yaml`, and `.pipeline/pipeline.yaml`. They do not maintain
5
+ independent profile definitions or silently translate one runner into another
6
+ host's default agent.
6
7
 
7
8
  Install or check generated resources with:
8
9
 
@@ -13,23 +14,30 @@ pipe install-commands --host all --check
13
14
 
14
15
  ## Host Mappings
15
16
 
16
- | Host | Generated resources | Invocation | Mechanical path |
17
- | --- | --- | --- | --- |
18
- | Claude Code | `.claude/commands/pipe.md`, `.claude/agents/*.md` | `/pipe <task>` | Project command delegates to configured Claude agents. |
19
- | Codex | `.agents/skills/pipe/SKILL.md`, `.codex/agents/*.toml` | `$pipe <task>` | Skill instructs Codex to use generated Codex agents. |
20
- | OpenCode | `.opencode/commands/pipe.md`, `.opencode/agents/*.md` | `/pipe <task>` | Project command runs a primary orchestrator and configured subagents. |
21
- | Kimi | `.kimi/commands/pipe.md`, `.kimi/agents/*.md` | `/pipe <task>` | Project command and agent specs mirror YAML grants. |
22
- | Pi | `.pi/extensions/pipe.ts`, `.pi/prompts/pipe.md` | `/pipe <task>` | Extension requires `pi-subagents` before sending a chain. |
17
+ | Host | Generated resources | Invocation | Mechanical path |
18
+ | ----------- | ------------------------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------ |
19
+ | Claude Code | `.claude/commands/pipe.md`, `.claude/agents/*.md` | `/pipe <task>` | Project command delegates to configured Claude agents. |
20
+ | Codex | `.agents/skills/pipe/SKILL.md`, `.codex/agents/*.toml` | `$pipe <task>` | Skill instructs Codex to use generated Codex agents for Codex runner nodes. |
21
+ | OpenCode | `.opencode/commands/pipe.md`, `.opencode/agents/*.md` | `/pipe <task>` | Project command runs a primary orchestrator and native subagents when the requested model is resolved. |
22
+ | Kimi | `.kimi/skills/pipe/SKILL.md`, `.kimi/agents/*.yaml` | `/skill:pipe <task>` | Kimi discovers project skills as `/skill:<name>` commands; Kimi agents are generated as YAML specs. |
23
+ | Pi | `.pi/prompts/pipe.md`, `.pi/extensions/pipe.ts` | `/pipe <task>` | Pi discovers project prompt templates as slash commands; the generated extension is a no-op shim. |
23
24
 
24
25
  ## Projection Rules
25
26
 
26
27
  - Profile names, descriptions, instructions, tools, rules, skills, MCP servers,
27
28
  filesystem mode, network mode, and output contracts are read from YAML.
29
+ - Exact native dispatch is used when a node runner matches the host.
30
+ - OpenCode can run mixed native subagents when the node runner has a resolved
31
+ model from `profile.host_models.opencode`, `runner.host_models.opencode`,
32
+ `profile.model`, or `runner.model`.
33
+ - Cross-runner nodes that cannot be represented natively are dispatched through
34
+ that runner's CLI. Instruction-only translation is not runner-correct and is
35
+ not used as an implicit fallback.
28
36
  - Host-specific formats can omit unsupported capabilities, but they must not
29
37
  grant broader access than requested.
30
38
  - Regeneration is idempotent for generated files. Manual edits are protected
31
39
  unless `--force` is supplied.
32
40
 
33
- The CLI runtime and host projections share the same workflow plan. Multi-agent
41
+ The CLI runtime and generated host resources share the same workflow plan. Multi-agent
34
42
  workflows require separate agent boundaries; host resources must not collapse
35
43
  the workflow into a single prompt.
package/package.json CHANGED
@@ -73,7 +73,7 @@
73
73
  "prepack": "bun run build:cli"
74
74
  },
75
75
  "type": "module",
76
- "version": "1.5.1",
76
+ "version": "1.5.3",
77
77
  "description": "",
78
78
  "main": "index.js",
79
79
  "keywords": [],