@oisincoveney/pipeline 2.11.2 → 3.0.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.
@@ -330,26 +330,26 @@ function createCliProgram(options = {}) {
330
330
  const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
331
331
  console.log(await localGatewayStatus(cwd));
332
332
  });
333
- program.command("init").description("Initialize package-owned pipeline support without repo-local config").addOption(new Option$1("--skill-scope <scope>", "where to install default skills: project (repo-local copy) or personal (one inherited user/global install)").choices(["project", "personal"]).default("project")).action(async (flags) => {
333
+ program.command("init").description("Initialize package-owned pipeline support without repo-local config").addOption(new Option$1("--scope <scope>", "where to install the harness: global (one per-machine install in ~/.claude, ~/.config/opencode, ~/.codex; inherited by every repo) or project (repo-local copy)").choices(["global", "project"]).default("global")).action(async (flags) => {
334
334
  const result = await initPipelineProject({
335
335
  cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd(),
336
- scope: flags.skillScope
336
+ scope: flags.scope
337
337
  });
338
338
  console.log(formatPipelineInitResult(result));
339
339
  });
340
- program.command("refresh-harnesses").description("Force-refresh generated agent harnesses and commit owned resource changes").addOption(new Option$1("--skill-scope <scope>", "where to install default skills: project (repo-local copy) or personal (one inherited user/global install)").choices(["project", "personal"]).default("project")).option("--message <message>", "git commit message for refreshed harness changes", "chore: update agent harnesses").action(async (flags) => {
340
+ program.command("refresh-harnesses").description("Force-refresh generated agent harnesses (global by default; commits owned resource changes only for project scope)").addOption(new Option$1("--scope <scope>", "where to refresh the harness: global (per-machine install) or project (repo-local copy, committed)").choices(["global", "project"]).default("global")).option("--message <message>", "git commit message for refreshed harness changes (project scope only)", "chore: update agent harnesses").action(async (flags) => {
341
341
  const result = await refreshAgentHarnesses({
342
342
  commitMessage: flags.message,
343
343
  cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd(),
344
- scope: flags.skillScope
344
+ scope: flags.scope
345
345
  });
346
346
  console.log(formatRefreshAgentHarnessesResult(result));
347
347
  });
348
- program.command("install-commands").description("Install generated slash-command adapters into this repository").addOption(new Option$1("--host <host>", "host command set to install").choices([
348
+ program.command("install-commands").description("Install generated slash-command adapters (global per-machine by default)").addOption(new Option$1("--host <host>", "host command set to install").choices([
349
349
  "all",
350
350
  "opencode",
351
351
  "claude-code"
352
- ]).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) => {
352
+ ]).default("all").argParser(parseCommandHost)).addOption(new Option$1("--scope <scope>", "where to install: global (~/.claude, ~/.config/opencode, ~/.codex) or project (repo-local)").choices(["global", "project"]).default("global")).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) => {
353
353
  const result = await installCommands({
354
354
  ...flags,
355
355
  cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd()
@@ -1,3 +1,5 @@
1
+ import { join } from "node:path";
2
+ import { homedir } from "node:os";
1
3
  //#region src/install-commands/shared.ts
2
4
  const GENERATED_MARKER = "<!-- Generated by @oisincoveney/pipeline. -->";
3
5
  const GENERATED_TS_MARKER = "// Generated by @oisincoveney/pipeline.";
@@ -16,6 +18,45 @@ const ENTRYPOINT_PATH_PATTERNS = {
16
18
  "claude-code": [/^\.claude\/commands\/(?:moka-)?([^/]+)\.md$/]
17
19
  };
18
20
  const COMMAND_HOSTS = ["opencode", "claude-code"];
21
+ const DEFAULT_HARNESS_SCOPE = "global";
22
+ /**
23
+ * The per-machine host config dirs. Each honors the same env var the host tool
24
+ * itself uses to relocate its config (so containers/CI can redirect, and so
25
+ * does our test suite): Claude Code reads `CLAUDE_CONFIG_DIR`, Codex reads
26
+ * `CODEX_HOME`, OpenCode reads `OPENCODE_CONFIG_DIR`/`XDG_CONFIG_HOME`.
27
+ */
28
+ function claudeGlobalConfigDir() {
29
+ return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
30
+ }
31
+ function codexGlobalConfigDir() {
32
+ return process.env.CODEX_HOME ?? join(homedir(), ".codex");
33
+ }
34
+ function opencodeGlobalConfigDir() {
35
+ return process.env.OPENCODE_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "opencode");
36
+ }
37
+ function stripPrefix(value, prefix) {
38
+ if (value === prefix) return "";
39
+ return value.startsWith(`${prefix}/`) ? value.slice(prefix.length + 1) : value;
40
+ }
41
+ /**
42
+ * Resolve a repo-relative harness path (e.g. ".claude/agents/x.md",
43
+ * ".opencode/opencode.json", "AGENTS.md") to its absolute on-disk target for
44
+ * the given scope.
45
+ *
46
+ * - project: rooted at `cwd` (the repo) — the historical behaviour.
47
+ * - global: rooted at the host-native per-machine config dirs. `.claude/*` →
48
+ * the Claude config dir, `.codex/*` → the Codex config dir, and `.opencode/*`
49
+ * plus bare root files (e.g. AGENTS.md, which OpenCode reads globally) → the
50
+ * OpenCode config dir. The global `.opencode` and `~/.config/opencode`
51
+ * layouts use identical subdir names, so this is a pure prefix rebase.
52
+ */
53
+ function resolveHarnessTarget(scope, cwd, relPath) {
54
+ if (scope === "project") return join(cwd, relPath);
55
+ const normalized = relPath.replaceAll("\\", "/");
56
+ if (normalized === ".claude" || normalized.startsWith(".claude/")) return join(claudeGlobalConfigDir(), stripPrefix(normalized, ".claude"));
57
+ if (normalized === ".codex" || normalized.startsWith(".codex/")) return join(codexGlobalConfigDir(), stripPrefix(normalized, ".codex"));
58
+ return join(opencodeGlobalConfigDir(), stripPrefix(normalized, ".opencode"));
59
+ }
19
60
  function profileEntries(config) {
20
61
  return Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b));
21
62
  }
@@ -47,4 +88,4 @@ function commandIdForHost(host, entrypointId) {
47
88
  return entrypointId;
48
89
  }
49
90
  //#endregion
50
- export { AGENTS_MD_END, AGENTS_MD_START, CLAUDE_PROJECT_CONFIG_PATH, COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, GENERATED_MARKER, GENERATED_TS_MARKER, GENERATED_YAML_MARKER, OPENCODE_PROJECT_CONFIG_PATH, OWNER_MARKER_PREFIX, OWNER_TS_MARKER_PREFIX, OWNER_YAML_MARKER_PREFIX, SINGLE_OPENCODE_PLUGIN_ARRAY_RE, commandIdForHost, compactLines, entrypointDescription, entrypointEntries, instructionsPointer, invocationForHost, profileEntries };
91
+ export { AGENTS_MD_END, AGENTS_MD_START, CLAUDE_PROJECT_CONFIG_PATH, COMMAND_HOSTS, DEFAULT_HARNESS_SCOPE, ENTRYPOINT_PATH_PATTERNS, GENERATED_MARKER, GENERATED_TS_MARKER, GENERATED_YAML_MARKER, OPENCODE_PROJECT_CONFIG_PATH, OWNER_MARKER_PREFIX, OWNER_TS_MARKER_PREFIX, OWNER_YAML_MARKER_PREFIX, SINGLE_OPENCODE_PLUGIN_ARRAY_RE, commandIdForHost, compactLines, entrypointDescription, entrypointEntries, instructionsPointer, invocationForHost, profileEntries, resolveHarnessTarget };
@@ -1,6 +1,6 @@
1
1
  import { loadPipelineConfig } from "./config/load.js";
2
2
  import "./config.js";
3
- import { COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, invocationForHost } from "./install-commands/shared.js";
3
+ import { COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, invocationForHost, resolveHarnessTarget } from "./install-commands/shared.js";
4
4
  import { opencodeAdapter } from "./install-commands/opencode.js";
5
5
  import { claudeCodeAdapter } from "./install-commands/claude-code.js";
6
6
  import { existsSync, readFileSync, statSync } from "node:fs";
@@ -40,13 +40,24 @@ async function listFiles(root) {
40
40
  function generatedHostFor(content) {
41
41
  return COMMAND_HOSTS.find((host) => content.includes(`<!-- @oisincoveney/pipeline:host=${host} -->`) || content.includes(`// @oisincoveney/pipeline:host=${host}`) || content.includes(`# @oisincoveney/pipeline:host=${host}`));
42
42
  }
43
- async function obsoleteGeneratedItems(cwd, host, wantedPaths) {
43
+ async function obsoleteGeneratedItems(cwd, host, wantedPaths, scope) {
44
44
  const hosts = new Set(selectedHosts(host));
45
45
  const roots = selectedHosts(host).flatMap((selectedHost) => resourceRootsFor(selectedHost));
46
- return (await Promise.all(roots.map((root) => listFiles(join(cwd, root))))).flat().flatMap((absolutePath) => {
47
- const generatedHost = generatedHostFor(readFileSync(absolutePath, "utf8"));
46
+ return (await Promise.all(roots.map(async (root) => {
47
+ const absRoot = resolveHarnessTarget(scope, cwd, root);
48
+ return (await listFiles(absRoot)).map((absolutePath) => ({
49
+ absolutePath,
50
+ path: join(root, relative(absRoot, absolutePath)).replaceAll("\\", "/")
51
+ }));
52
+ }))).flat().flatMap(({ absolutePath, path }) => {
53
+ let content;
54
+ try {
55
+ content = readFileSync(absolutePath, "utf8");
56
+ } catch {
57
+ return [];
58
+ }
59
+ const generatedHost = generatedHostFor(content);
48
60
  if (!(generatedHost && hosts.has(generatedHost))) return [];
49
- const path = relative(cwd, absolutePath).replaceAll("\\", "/");
50
61
  if (wantedPaths.has(path)) return [];
51
62
  return [{
52
63
  action: "delete",
@@ -127,8 +138,8 @@ async function writeDefinition(definition, target, content) {
127
138
  function shouldSkipInstallWrite(options, action) {
128
139
  return Boolean(options.check || options.dryRun || action === "unchanged" || action === "conflict");
129
140
  }
130
- async function installDefinition(cwd, definition, options) {
131
- const target = join(cwd, definition.path);
141
+ async function installDefinition(cwd, definition, options, scope) {
142
+ const target = resolveHarnessTarget(scope, cwd, definition.path);
132
143
  const resolved = resolveDefinitionContent(definition, target);
133
144
  const action = installActionForDefinition(definition, target, resolved, Boolean(options.force));
134
145
  const item = commandInstallPlanItem(definition, action);
@@ -146,25 +157,27 @@ function commandInstallPlanItem(definition, action) {
146
157
  function installCommandsContext(options) {
147
158
  const cwd = options.cwd ?? process.cwd();
148
159
  const host = options.host ?? "all";
160
+ const scope = options.scope ?? "global";
149
161
  const definitions = definitionsFor(host, loadPipelineConfig(cwd, { allowMissingLintFileReferences: true }), cwd);
150
162
  return {
151
163
  cwd,
152
164
  definitions,
153
165
  host,
166
+ scope,
154
167
  wantedPaths: new Set(definitions.map((definition) => definition.path))
155
168
  };
156
169
  }
157
- async function installDefinitions(cwd, definitions, options) {
170
+ async function installDefinitions(cwd, definitions, options, scope) {
158
171
  const items = [];
159
- for (const definition of definitions) items.push(await installDefinition(cwd, definition, options));
172
+ for (const definition of definitions) items.push(await installDefinition(cwd, definition, options, scope));
160
173
  return items;
161
174
  }
162
175
  function shouldRemoveObsoleteItems(options) {
163
176
  return !(options.check || options.dryRun);
164
177
  }
165
- async function removeObsoleteItems(cwd, items, options) {
178
+ async function removeObsoleteItems(cwd, items, options, scope) {
166
179
  if (!shouldRemoveObsoleteItems(options)) return;
167
- for (const item of items) await rm(join(cwd, item.path), { force: true });
180
+ for (const item of items) await rm(resolveHarnessTarget(scope, cwd, item.path), { force: true });
168
181
  }
169
182
  function actionIsConflict(item) {
170
183
  return item.action === "conflict";
@@ -190,10 +203,10 @@ function assertInstallCheckCurrent(options, items) {
190
203
  }
191
204
  async function installCommands(options = {}) {
192
205
  const context = installCommandsContext(options);
193
- const items = await installDefinitions(context.cwd, context.definitions, options);
194
- const obsoleteItems = await obsoleteGeneratedItems(context.cwd, context.host, context.wantedPaths);
206
+ const items = await installDefinitions(context.cwd, context.definitions, options, context.scope);
207
+ const obsoleteItems = await obsoleteGeneratedItems(context.cwd, context.host, context.wantedPaths, context.scope);
195
208
  items.push(...obsoleteItems);
196
- await removeObsoleteItems(context.cwd, obsoleteItems, options);
209
+ await removeObsoleteItems(context.cwd, obsoleteItems, options, context.scope);
197
210
  assertNoInstallConflicts(options, items);
198
211
  assertInstallCheckCurrent(options, items);
199
212
  return { items };
@@ -5,13 +5,13 @@ import { z } from "zod";
5
5
  //#region src/moka-submit.d.ts
6
6
  declare const mokaSubmitDirectHooksSchema: z.ZodRecord<z.ZodEnum<{
7
7
  "workflow.start": "workflow.start";
8
+ "node.finish": "node.finish";
9
+ "node.start": "node.start";
8
10
  "workflow.success": "workflow.success";
9
11
  "workflow.failure": "workflow.failure";
10
12
  "workflow.complete": "workflow.complete";
11
- "node.start": "node.start";
12
13
  "node.success": "node.success";
13
14
  "node.error": "node.error";
14
- "node.finish": "node.finish";
15
15
  "gate.failure": "gate.failure";
16
16
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
17
17
  failure: z.ZodDefault<z.ZodEnum<{
@@ -94,13 +94,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
94
94
  }, z.core.$strict>>;
95
95
  hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
96
96
  "workflow.start": "workflow.start";
97
+ "node.finish": "node.finish";
98
+ "node.start": "node.start";
97
99
  "workflow.success": "workflow.success";
98
100
  "workflow.failure": "workflow.failure";
99
101
  "workflow.complete": "workflow.complete";
100
- "node.start": "node.start";
101
102
  "node.success": "node.success";
102
103
  "node.error": "node.error";
103
- "node.finish": "node.finish";
104
104
  "gate.failure": "gate.failure";
105
105
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
106
106
  failure: z.ZodDefault<z.ZodEnum<{
@@ -206,13 +206,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
206
206
  }, z.core.$strict>>;
207
207
  hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
208
208
  "workflow.start": "workflow.start";
209
+ "node.finish": "node.finish";
210
+ "node.start": "node.start";
209
211
  "workflow.success": "workflow.success";
210
212
  "workflow.failure": "workflow.failure";
211
213
  "workflow.complete": "workflow.complete";
212
- "node.start": "node.start";
213
214
  "node.success": "node.success";
214
215
  "node.error": "node.error";
215
- "node.finish": "node.finish";
216
216
  "gate.failure": "gate.failure";
217
217
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
218
218
  failure: z.ZodDefault<z.ZodEnum<{
@@ -1,8 +1,18 @@
1
+ import "./install-commands/shared.js";
1
2
  import { installCommands } from "./install-commands.js";
2
3
  import { existsSync } from "node:fs";
3
4
  import { execa } from "execa";
4
5
  import { join } from "node:path";
5
6
  //#region src/pipeline-init.ts
7
+ /**
8
+ * The harness scope drives every generated resource at once: skills, host
9
+ * commands/agents, and config. "global" (default) installs the single
10
+ * per-machine harness (user/global skills + ~/.claude, ~/.config/opencode,
11
+ * ~/.codex host config); "project" vendors everything repo-local.
12
+ */
13
+ function skillScopeFor(scope) {
14
+ return scope === "global" ? "personal" : "project";
15
+ }
6
16
  const DEFAULT_SKILL_INSTALL_SOURCE = "oisin-ee/skills";
7
17
  const SKILL_INSTALL_AGENT_ARGS = [
8
18
  "--agent",
@@ -49,13 +59,14 @@ async function installDefaultSkills(cwd, scope) {
49
59
  }
50
60
  async function initPipelineProject(options = {}) {
51
61
  const cwd = options.cwd ?? process.cwd();
52
- const scope = options.scope ?? "project";
53
- await (options.skillInstaller ?? ((target) => installDefaultSkills(target, scope)))(cwd);
62
+ const scope = options.scope ?? "global";
63
+ await (options.skillInstaller ?? ((target) => installDefaultSkills(target, skillScopeFor(scope))))(cwd);
54
64
  return {
55
65
  files: (await installCommands({
56
66
  cwd,
57
67
  force: true,
58
- host: "all"
68
+ host: "all",
69
+ scope
59
70
  })).items.map((item) => item.path),
60
71
  scope
61
72
  };
@@ -67,7 +78,7 @@ async function refreshAgentHarnesses(options = {}) {
67
78
  scope: options.scope,
68
79
  skillInstaller: options.skillInstaller
69
80
  });
70
- const committed = await refreshAgentHarnessesCommitResult(context);
81
+ const committed = init.scope === "project" ? await refreshAgentHarnessesCommitResult(context) : false;
71
82
  return {
72
83
  ...init,
73
84
  commitMessage: context.commitMessage,
@@ -88,13 +99,18 @@ async function refreshAgentHarnessesCommitResult(context) {
88
99
  function formatPipelineInitResult(result) {
89
100
  return [
90
101
  "Initialized package-owned pipeline support:",
91
- result.scope === "personal" ? "installed default skills at user/global scope (inherited by every repo, no per-repo copy)" : "installed default skills (repo-local copy)",
102
+ result.scope === "global" ? "installed the per-machine harness globally (user/global skills + ~/.claude, ~/.config/opencode, ~/.codex); inherited by every repo with no per-repo copy" : "installed default skills and host config repo-local",
92
103
  ...result.files.map((path) => `generated ${path}`),
93
104
  "no repo-local pipeline config files were created"
94
105
  ].join("\n");
95
106
  }
107
+ function refreshCommitSummary(result) {
108
+ if (result.scope === "global") return "global harness refreshed; no repo commit (per-machine install)";
109
+ if (result.committed) return `committed refreshed harnesses: ${result.commitMessage}`;
110
+ return "refreshed harnesses already current; no commit created";
111
+ }
96
112
  function formatRefreshAgentHarnessesResult(result) {
97
- return [formatPipelineInitResult(result), result.committed ? `committed refreshed harnesses: ${result.commitMessage}` : "refreshed harnesses already current; no commit created"].join("\n");
113
+ return [formatPipelineInitResult(result), refreshCommitSummary(result)].join("\n");
98
114
  }
99
115
  async function runCommand(command, args, options) {
100
116
  const result = await execa(command, args, {
@@ -10,8 +10,8 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
10
10
  at: z.ZodOptional<z.ZodString>;
11
11
  sequence: z.ZodNumber;
12
12
  type: z.ZodEnum<{
13
- "workflow.start": "workflow.start";
14
13
  "workflow.planned": "workflow.planned";
14
+ "workflow.start": "workflow.start";
15
15
  }>;
16
16
  workflowPlan: z.ZodObject<{
17
17
  edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -55,10 +55,10 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
55
55
  }>;
56
56
  }, z.core.$strip>;
57
57
  type: z.ZodEnum<{
58
- "node.start": "node.start";
59
- "node.finish": "node.finish";
60
58
  "agent.finish": "agent.finish";
61
59
  "agent.start": "agent.start";
60
+ "node.finish": "node.finish";
61
+ "node.start": "node.start";
62
62
  }>;
63
63
  }, z.core.$strip>, z.ZodObject<{
64
64
  at: z.ZodOptional<z.ZodString>;
@@ -180,8 +180,8 @@ declare const runnerEventBatchSchema: z.ZodObject<{
180
180
  at: z.ZodOptional<z.ZodString>;
181
181
  sequence: z.ZodNumber;
182
182
  type: z.ZodEnum<{
183
- "workflow.start": "workflow.start";
184
183
  "workflow.planned": "workflow.planned";
184
+ "workflow.start": "workflow.start";
185
185
  }>;
186
186
  workflowPlan: z.ZodObject<{
187
187
  edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -225,10 +225,10 @@ declare const runnerEventBatchSchema: z.ZodObject<{
225
225
  }>;
226
226
  }, z.core.$strip>;
227
227
  type: z.ZodEnum<{
228
- "node.start": "node.start";
229
- "node.finish": "node.finish";
230
228
  "agent.finish": "agent.finish";
231
229
  "agent.start": "agent.start";
230
+ "node.finish": "node.finish";
231
+ "node.start": "node.start";
232
232
  }>;
233
233
  }, z.core.$strip>, z.ZodObject<{
234
234
  at: z.ZodOptional<z.ZodString>;
package/package.json CHANGED
@@ -127,7 +127,7 @@
127
127
  "prepack": "bun run build:cli"
128
128
  },
129
129
  "type": "module",
130
- "version": "2.11.2",
130
+ "version": "3.0.0",
131
131
  "description": "Config-driven multi-agent pipeline runner for repository work",
132
132
  "main": "./dist/index.js",
133
133
  "types": "./dist/index.d.ts",