@oisincoveney/pipeline 3.9.1 → 3.10.1

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
@@ -65,7 +65,7 @@ moka install-commands --host all --check
65
65
  Check or refresh copied agent hooks after editing `oisin-ee/agent-hooks`:
66
66
 
67
67
  ```shell
68
- moka install-hooks --scope global --check
68
+ moka install-hooks --check
69
69
  ```
70
70
 
71
71
  Check local prerequisites and config health:
@@ -105,8 +105,8 @@ Canonical commands:
105
105
  - `moka doctor`: check local prerequisites and config health.
106
106
  - `moka init`: install package-owned host resources for a repository.
107
107
  - `moka install-hooks`: copy manually authored hooks from `oisin-ee/agent-hooks`.
108
- - `moka refresh-harnesses`: force-refresh generated agent harnesses and commit
109
- owned resource changes.
108
+ - `moka refresh-harnesses`: force-refresh the per-machine (global) agent
109
+ harnesses. The harness is always installed globally; there is no `--scope`.
110
110
 
111
111
  ```shell
112
112
  moka run "Implement PIPE-123 user-facing behavior"
@@ -332,37 +332,27 @@ function createCliProgram(options = {}) {
332
332
  const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
333
333
  console.log(await localGatewayStatus(cwd));
334
334
  });
335
- 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) => {
336
- const result = await initPipelineProject({
337
- cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd(),
338
- scope: flags.scope
339
- });
335
+ program.command("init").description("Initialize package-owned pipeline support without repo-local config (global per-machine install: ~/.claude, ~/.config/opencode, ~/.codex)").action(async () => {
336
+ const result = await initPipelineProject({ cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd() });
340
337
  console.log(formatPipelineInitResult(result));
341
338
  });
342
- 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) => {
343
- const result = await refreshAgentHarnesses({
344
- commitMessage: flags.message,
345
- cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd(),
346
- scope: flags.scope
347
- });
339
+ program.command("refresh-harnesses").description("Force-refresh generated agent harnesses (global per-machine install: ~/.claude, ~/.config/opencode, ~/.codex)").action(async () => {
340
+ const result = await refreshAgentHarnesses({ cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd() });
348
341
  console.log(formatRefreshAgentHarnessesResult(result));
349
342
  });
350
- 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([
343
+ program.command("install-commands").description("Install generated slash-command adapters into per-machine host dirs (~/.claude, ~/.config/opencode, ~/.codex)").addOption(new Option$1("--host <host>", "host command set to install").choices([
351
344
  "all",
352
345
  "opencode",
353
346
  "claude-code"
354
- ]).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) => {
347
+ ]).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) => {
355
348
  const result = await installCommands({
356
349
  ...flags,
357
350
  cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd()
358
351
  });
359
352
  console.log(formatInstallCommandsResult(result));
360
353
  });
361
- program.command("install-hooks").description("Install agent hooks from oisin-ee/agent-hooks (global per-machine by default)").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 installed hook files are missing or stale").option("--force", "overwrite manually edited hook files").action(async (flags) => {
362
- const result = await installHooks({
363
- ...flags,
364
- cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd()
365
- });
354
+ program.command("install-hooks").description("Install agent hooks from oisin-ee/agent-hooks into per-machine host dirs (~/.claude, ~/.config/opencode, ~/.codex)").option("--dry-run", "show planned changes without writing files").option("--check", "fail if installed hook files are missing or stale").option("--force", "overwrite manually edited hook files").action(async (flags) => {
355
+ const result = await installHooks({ ...flags });
366
356
  console.log(formatInstallHooksResult(result));
367
357
  });
368
358
  program.command("codex-auth").description("Manage local Codex multi-auth integration").command("sync-local").description("Use one local oc-codex account pool and declare the plugin in dev repos").option("--root <path>", "directory containing repositories to sync").option("--dry-run", "show planned changes without writing files").option("--check", "fail if local Codex auth config is not synced").action((flags) => {
package/dist/hooks.d.ts CHANGED
@@ -13,8 +13,8 @@ declare const hookResultSchema: z.ZodObject<{
13
13
  taskContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
14
  }, z.core.$strict>>;
15
15
  status: z.ZodEnum<{
16
- pass: "pass";
17
16
  fail: "fail";
17
+ pass: "pass";
18
18
  skip: "skip";
19
19
  }>;
20
20
  summary: z.ZodOptional<z.ZodString>;
@@ -18,7 +18,6 @@ const ENTRYPOINT_PATH_PATTERNS = {
18
18
  "claude-code": [/^\.claude\/commands\/(?:moka-)?([^/]+)\.md$/]
19
19
  };
20
20
  const COMMAND_HOSTS = ["opencode", "claude-code"];
21
- const DEFAULT_HARNESS_SCOPE = "global";
22
21
  /**
23
22
  * The per-machine host config dirs. Each honors the same env var the host tool
24
23
  * itself uses to relocate its config (so containers/CI can redirect, and so
@@ -43,18 +42,14 @@ function stripPrefix(value, prefix) {
43
42
  }
44
43
  /**
45
44
  * Resolve a repo-relative harness path (e.g. ".claude/agents/x.md",
46
- * ".opencode/opencode.json", "AGENTS.md") to its absolute on-disk target for
47
- * the given scope.
48
- *
49
- * - project: rooted at `cwd` (the repo) the historical behaviour.
50
- * - global: rooted at the host-native per-machine config dirs. `.claude/*` →
51
- * the Claude config dir, `.codex/*` the Codex config dir, and `.opencode/*`
52
- * plus bare root files (e.g. AGENTS.md, which OpenCode reads globally) → the
53
- * OpenCode config dir. The global `.opencode` and `~/.config/opencode`
54
- * layouts use identical subdir names, so this is a pure prefix rebase.
45
+ * ".opencode/opencode.json") to its absolute on-disk target in the
46
+ * per-machine host config dirs. `.claude/*` → the Claude config dir,
47
+ * `.codex/*` → the Codex config dir, `.gemini/*` → the Gemini config dir,
48
+ * and `.opencode/*` plus other root files the OpenCode config dir. The
49
+ * global `.opencode` and `~/.config/opencode` layouts use identical subdir
50
+ * names, so this is a pure prefix rebase.
55
51
  */
56
- function resolveHarnessTarget(scope, cwd, relPath) {
57
- if (scope === "project") return join(cwd, relPath);
52
+ function resolveHarnessTarget(relPath) {
58
53
  const normalized = relPath.replaceAll("\\", "/");
59
54
  if (normalized === ".claude" || normalized.startsWith(".claude/")) return join(claudeGlobalConfigDir(), stripPrefix(normalized, ".claude"));
60
55
  if (normalized === ".codex" || normalized.startsWith(".codex/")) return join(codexGlobalConfigDir(), stripPrefix(normalized, ".codex"));
@@ -92,4 +87,4 @@ function commandIdForHost(host, entrypointId) {
92
87
  return entrypointId;
93
88
  }
94
89
  //#endregion
95
- 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 };
90
+ 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, resolveHarnessTarget };
@@ -40,11 +40,11 @@ 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, scope) {
43
+ async function obsoleteGeneratedItems(host, wantedPaths) {
44
44
  const hosts = new Set(selectedHosts(host));
45
45
  const roots = selectedHosts(host).flatMap((selectedHost) => resourceRootsFor(selectedHost));
46
46
  return (await Promise.all(roots.map(async (root) => {
47
- const absRoot = resolveHarnessTarget(scope, cwd, root);
47
+ const absRoot = resolveHarnessTarget(root);
48
48
  return (await listFiles(absRoot)).map((absolutePath) => ({
49
49
  absolutePath,
50
50
  path: join(root, relative(absRoot, absolutePath)).replaceAll("\\", "/")
@@ -138,8 +138,8 @@ async function writeDefinition(definition, target, content) {
138
138
  function shouldSkipInstallWrite(options, action) {
139
139
  return Boolean(options.check || options.dryRun || action === "unchanged" || action === "conflict");
140
140
  }
141
- async function installDefinition(cwd, definition, options, scope) {
142
- const target = resolveHarnessTarget(scope, cwd, definition.path);
141
+ async function installDefinition(definition, options) {
142
+ const target = resolveHarnessTarget(definition.path);
143
143
  const resolved = resolveDefinitionContent(definition, target);
144
144
  const action = installActionForDefinition(definition, target, resolved, Boolean(options.force));
145
145
  const item = commandInstallPlanItem(definition, action);
@@ -154,35 +154,29 @@ function commandInstallPlanItem(definition, action) {
154
154
  path: definition.path
155
155
  };
156
156
  }
157
- const PROJECT_ONLY_PATHS = new Set(["AGENTS.md"]);
158
- function scopedDefinitions(definitions, scope) {
159
- if (scope === "global") return definitions.filter((definition) => !PROJECT_ONLY_PATHS.has(definition.path));
160
- return definitions;
161
- }
157
+ const GLOBAL_EXCLUDED_PATHS = new Set(["AGENTS.md"]);
162
158
  function installCommandsContext(options) {
163
159
  const cwd = options.cwd ?? process.cwd();
164
160
  const host = options.host ?? "all";
165
- const scope = options.scope ?? "global";
166
- const definitions = scopedDefinitions(definitionsFor(host, loadPipelineConfig(cwd, { allowMissingLintFileReferences: true }), cwd), scope);
161
+ const definitions = definitionsFor(host, loadPipelineConfig(cwd, { allowMissingLintFileReferences: true }), cwd).filter((definition) => !GLOBAL_EXCLUDED_PATHS.has(definition.path));
167
162
  return {
168
163
  cwd,
169
164
  definitions,
170
165
  host,
171
- scope,
172
166
  wantedPaths: new Set(definitions.map((definition) => definition.path))
173
167
  };
174
168
  }
175
- async function installDefinitions(cwd, definitions, options, scope) {
169
+ async function installDefinitions(definitions, options) {
176
170
  const items = [];
177
- for (const definition of definitions) items.push(await installDefinition(cwd, definition, options, scope));
171
+ for (const definition of definitions) items.push(await installDefinition(definition, options));
178
172
  return items;
179
173
  }
180
174
  function shouldRemoveObsoleteItems(options) {
181
175
  return !(options.check || options.dryRun);
182
176
  }
183
- async function removeObsoleteItems(cwd, items, options, scope) {
177
+ async function removeObsoleteItems(items, options) {
184
178
  if (!shouldRemoveObsoleteItems(options)) return;
185
- for (const item of items) await rm(resolveHarnessTarget(scope, cwd, item.path), { force: true });
179
+ for (const item of items) await rm(resolveHarnessTarget(item.path), { force: true });
186
180
  }
187
181
  function actionIsConflict(item) {
188
182
  return item.action === "conflict";
@@ -208,10 +202,10 @@ function assertInstallCheckCurrent(options, items) {
208
202
  }
209
203
  async function installCommands(options = {}) {
210
204
  const context = installCommandsContext(options);
211
- const items = await installDefinitions(context.cwd, context.definitions, options, context.scope);
212
- const obsoleteItems = await obsoleteGeneratedItems(context.cwd, context.host, context.wantedPaths, context.scope);
205
+ const items = await installDefinitions(context.definitions, options);
206
+ const obsoleteItems = await obsoleteGeneratedItems(context.host, context.wantedPaths);
213
207
  items.push(...obsoleteItems);
214
- await removeObsoleteItems(context.cwd, obsoleteItems, options, context.scope);
208
+ await removeObsoleteItems(obsoleteItems, options);
215
209
  assertNoInstallConflicts(options, items);
216
210
  assertInstallCheckCurrent(options, items);
217
211
  return { items };
@@ -68,8 +68,8 @@ async function sourceHookFiles(source) {
68
68
  });
69
69
  }))).flat().sort((a, b) => a.path.localeCompare(b.path));
70
70
  }
71
- function manifestPath(scope, cwd, host) {
72
- return resolveHarnessTarget(scope, cwd, `${HOST_TARGET_ROOT[host]}/${MANIFEST_FILE}`);
71
+ function manifestPath(host) {
72
+ return resolveHarnessTarget(`${HOST_TARGET_ROOT[host]}/${MANIFEST_FILE}`);
73
73
  }
74
74
  function emptyManifest() {
75
75
  return {
@@ -78,8 +78,8 @@ function emptyManifest() {
78
78
  version: 1
79
79
  };
80
80
  }
81
- function readManifest(scope, cwd, host) {
82
- const path = manifestPath(scope, cwd, host);
81
+ function readManifest(host) {
82
+ const path = manifestPath(host);
83
83
  if (!existsSync(path)) return emptyManifest();
84
84
  try {
85
85
  return normalizeManifest(JSON.parse(readFileSync(path, "utf8")));
@@ -105,28 +105,28 @@ function normalizeManifest(value) {
105
105
  version: 1
106
106
  };
107
107
  }
108
- function targetPath(scope, cwd, path) {
109
- return resolveHarnessTarget(scope, cwd, path);
108
+ function targetPath(path) {
109
+ return resolveHarnessTarget(path);
110
110
  }
111
- function actionForFile(file, scope, cwd, force, manifests) {
112
- const target = targetPath(scope, cwd, file.path);
111
+ function actionForFile(file, force, manifests) {
112
+ const target = targetPath(file.path);
113
113
  if (!existsSync(target)) return "create";
114
114
  const currentHash = hashContent(readFileSync(target));
115
115
  if (currentHash === file.hash) return "unchanged";
116
116
  if (force) return "update";
117
117
  return (manifests.get(file.host)?.files[file.path])?.hash === currentHash ? "update" : "conflict";
118
118
  }
119
- function planFiles(files, scope, cwd, force, manifests) {
119
+ function planFiles(files, force, manifests) {
120
120
  return files.map((file) => ({
121
121
  ...file,
122
- action: actionForFile(file, scope, cwd, force, manifests)
122
+ action: actionForFile(file, force, manifests)
123
123
  }));
124
124
  }
125
- function planObsoleteFiles(desiredPaths, scope, cwd, force, manifests) {
125
+ function planObsoleteFiles(desiredPaths, force, manifests) {
126
126
  const obsolete = [];
127
127
  for (const [host, manifest] of manifests) for (const [path, entry] of Object.entries(manifest.files)) {
128
128
  if (desiredPaths.has(path)) continue;
129
- const target = targetPath(scope, cwd, path);
129
+ const target = targetPath(path);
130
130
  if (!existsSync(target)) continue;
131
131
  const currentHash = hashContent(readFileSync(target));
132
132
  obsolete.push({
@@ -137,9 +137,9 @@ function planObsoleteFiles(desiredPaths, scope, cwd, force, manifests) {
137
137
  }
138
138
  return obsolete.sort((a, b) => a.path.localeCompare(b.path));
139
139
  }
140
- async function writePlannedFile(file, scope, cwd) {
140
+ async function writePlannedFile(file) {
141
141
  if (file.action === "conflict" || file.action === "unchanged") return;
142
- const target = targetPath(scope, cwd, file.path);
142
+ const target = targetPath(file.path);
143
143
  await mkdir(dirname(target), { recursive: true });
144
144
  await writeFile(target, file.content);
145
145
  }
@@ -157,9 +157,9 @@ function itemForObsolete(file) {
157
157
  path: file.path
158
158
  };
159
159
  }
160
- async function removeObsoleteFile(file, scope, cwd) {
160
+ async function removeObsoleteFile(file) {
161
161
  if (file.action !== "delete") return;
162
- await rm(targetPath(scope, cwd, file.path), { force: true });
162
+ await rm(targetPath(file.path), { force: true });
163
163
  }
164
164
  function assertNoConflicts(items, dryRun) {
165
165
  if (dryRun) return;
@@ -177,7 +177,7 @@ function assertCheckCurrent(items, check) {
177
177
  if (changed.length === 0) return;
178
178
  throw new Error(["Installed hook files are not up to date.", ...changed.map((item) => `- ${item.path}: ${item.action}`)].join("\n"));
179
179
  }
180
- async function writeManifests(files, scope, cwd) {
180
+ async function writeManifests(files) {
181
181
  const byHost = /* @__PURE__ */ new Map();
182
182
  for (const host of HOOK_HOSTS) byHost.set(host, emptyManifest());
183
183
  for (const file of files) {
@@ -185,7 +185,7 @@ async function writeManifests(files, scope, cwd) {
185
185
  if (manifest) manifest.files[file.path] = { hash: file.hash };
186
186
  }
187
187
  await Promise.all([...byHost.entries()].map(async ([host, manifest]) => {
188
- const path = manifestPath(scope, cwd, host);
188
+ const path = manifestPath(host);
189
189
  if (Object.keys(manifest.files).length === 0) {
190
190
  await rm(path, { force: true });
191
191
  return;
@@ -195,20 +195,18 @@ async function writeManifests(files, scope, cwd) {
195
195
  }));
196
196
  }
197
197
  function installHooks(options = {}) {
198
- const cwd = options.cwd ?? process.cwd();
199
- const scope = options.scope ?? "global";
200
198
  return withHookSource(async (source) => {
201
199
  const files = await sourceHookFiles(source);
202
- const manifests = new Map(HOOK_HOSTS.map((host) => [host, readManifest(scope, cwd, host)]));
203
- const planned = planFiles(files, scope, cwd, Boolean(options.force), manifests);
204
- const obsolete = planObsoleteFiles(new Set(files.map((file) => file.path)), scope, cwd, Boolean(options.force), manifests);
200
+ const manifests = new Map(HOOK_HOSTS.map((host) => [host, readManifest(host)]));
201
+ const planned = planFiles(files, Boolean(options.force), manifests);
202
+ const obsolete = planObsoleteFiles(new Set(files.map((file) => file.path)), Boolean(options.force), manifests);
205
203
  const items = [...planned.map(itemFor), ...obsolete.map(itemForObsolete)];
206
204
  assertCheckCurrent(items, Boolean(options.check));
207
205
  assertNoConflicts(items, Boolean(options.dryRun));
208
206
  if (!(options.check || options.dryRun)) {
209
- for (const file of planned) await writePlannedFile(file, scope, cwd);
210
- for (const file of obsolete) await removeObsoleteFile(file, scope, cwd);
211
- await writeManifests(planned, scope, cwd);
207
+ for (const file of planned) await writePlannedFile(file);
208
+ for (const file of obsolete) await removeObsoleteFile(file);
209
+ await writeManifests(planned);
212
210
  }
213
211
  return {
214
212
  items,
@@ -1,4 +1,3 @@
1
- import "./install-commands/shared.js";
2
1
  import { execa } from "execa";
3
2
  import { dirname, join } from "node:path";
4
3
  import { homedir, tmpdir } from "node:os";
@@ -51,7 +50,7 @@ async function defaultRulesyncRunner(args, opts) {
51
50
  });
52
51
  } catch (error) {
53
52
  const cause = error instanceof Error ? `: ${error.message}` : "";
54
- throw new Error(`Failed to generate global rules from ${DEFAULT_RULES_INSTALL_SOURCE}${cause}. If this is a private repository, authenticate GitHub access with \`gh auth login\` and rerun \`moka refresh-harnesses --scope global\`.`);
53
+ throw new Error(`Failed to generate global rules from ${DEFAULT_RULES_INSTALL_SOURCE}${cause}. If this is a private repository, authenticate GitHub access with \`gh auth login\` and rerun \`moka refresh-harnesses\`.`);
55
54
  }
56
55
  }
57
56
  async function buildRootRule(source) {
@@ -74,10 +73,6 @@ ${(await Promise.all(entries.map(async (name) => {
74
73
  await writeFile(join(rulesyncRulesDir, "_root.md"), rootContent);
75
74
  }
76
75
  function installRules(options = {}) {
77
- if ((options.scope ?? "global") !== "global") return Promise.resolve({
78
- items: [],
79
- source: DEFAULT_RULES_INSTALL_SOURCE
80
- });
81
76
  const runner = options.rulesyncRunner ?? defaultRulesyncRunner;
82
77
  return withRulesSource(options.sourceOverride, async (source) => {
83
78
  await buildRootRule(source);
@@ -1,20 +1,8 @@
1
- import "./install-commands/shared.js";
2
1
  import { installCommands } from "./install-commands.js";
3
2
  import { installHooks } from "./install-hooks.js";
4
3
  import { installRules } from "./install-rules.js";
5
- import { existsSync } from "node:fs";
6
4
  import { execa } from "execa";
7
- import { join } from "node:path";
8
5
  //#region src/pipeline-init.ts
9
- /**
10
- * The harness scope drives every generated resource at once: skills, host
11
- * commands/agents, and config. "global" (default) installs the single
12
- * per-machine harness (user/global skills + ~/.claude, ~/.config/opencode,
13
- * ~/.codex host config); "project" vendors everything repo-local.
14
- */
15
- function skillScopeFor(scope) {
16
- return scope === "global" ? "personal" : "project";
17
- }
18
6
  const DEFAULT_SKILL_INSTALL_SOURCE = "oisin-ee/skills";
19
7
  const SKILL_INSTALL_AGENT_ARGS = [
20
8
  "--agent",
@@ -25,35 +13,17 @@ const SKILL_INSTALL_AGENT_ARGS = [
25
13
  "claude-code",
26
14
  "--skill",
27
15
  "*",
28
- "--yes"
29
- ];
30
- function skillInstallArgs(scope) {
31
- return scope === "personal" ? [...SKILL_INSTALL_AGENT_ARGS, "--global"] : [...SKILL_INSTALL_AGENT_ARGS, "--copy"];
32
- }
33
- const DEFAULT_HARNESS_COMMIT_MESSAGE = "chore: update agent harnesses";
34
- const OWNED_HARNESS_PATHS = [
35
- ".agents/skills",
36
- ".claude/agents",
37
- ".claude/commands",
38
- ".claude/hooks",
39
- ".claude/.moka-agent-hooks.json",
40
- ".claude/settings.json",
41
- ".claude/skills",
42
- ".codex/hooks",
43
- ".codex/.moka-agent-hooks.json",
44
- ".codex/skills",
45
- ".opencode",
46
- "AGENTS.md",
47
- "skills-lock.json"
16
+ "--yes",
17
+ "--global"
48
18
  ];
49
- async function installDefaultSkills(cwd, scope) {
19
+ async function installDefaultSkills(cwd) {
50
20
  try {
51
21
  await execa("npx", [
52
22
  "--yes",
53
23
  "skills",
54
24
  "add",
55
25
  DEFAULT_SKILL_INSTALL_SOURCE,
56
- ...skillInstallArgs(scope)
26
+ ...SKILL_INSTALL_AGENT_ARGS
57
27
  ], {
58
28
  cwd,
59
29
  stdio: "inherit"
@@ -63,150 +33,44 @@ async function installDefaultSkills(cwd, scope) {
63
33
  throw new Error(`Failed to install default skills from ${DEFAULT_SKILL_INSTALL_SOURCE}${cause}. If this is a private repository, authenticate GitHub access for npx skills add and rerun \`moka init\`.`);
64
34
  }
65
35
  }
66
- function installDefaultHooks(cwd, scope) {
67
- return installHooks({
68
- cwd,
69
- scope
70
- });
36
+ function installDefaultHooks() {
37
+ return installHooks({});
71
38
  }
72
39
  function hookInstallerFiles(result) {
73
40
  return "items" in result ? result.items.map((item) => item.path) : result.files;
74
41
  }
75
42
  async function initPipelineProject(options = {}) {
76
43
  const cwd = options.cwd ?? process.cwd();
77
- const scope = options.scope ?? "global";
78
- const skillInstaller = options.skillInstaller ?? ((target) => installDefaultSkills(target, skillScopeFor(scope)));
44
+ const skillInstaller = options.skillInstaller ?? installDefaultSkills;
79
45
  const hookInstaller = options.hookInstaller ?? installDefaultHooks;
80
- const rulesInstaller = options.rulesInstaller ?? ((target, s) => installRules({
81
- cwd: target,
82
- scope: s
83
- }));
46
+ const rulesInstaller = options.rulesInstaller ?? ((_target) => installRules({}));
84
47
  await skillInstaller(cwd);
85
48
  const result = await installCommands({
86
49
  cwd,
87
50
  force: true,
88
- host: "all",
89
- scope
51
+ host: "all"
90
52
  });
91
- const hooks = await hookInstaller(cwd, scope);
92
- const rulesResult = await rulesInstaller(cwd, scope);
93
- return {
94
- files: [
95
- ...result.items.map((item) => item.path),
96
- ...hookInstallerFiles(hooks),
97
- ...rulesResult.items.map((item) => item.path)
98
- ],
99
- scope
100
- };
53
+ const hooks = await hookInstaller(cwd);
54
+ const rulesResult = await rulesInstaller(cwd);
55
+ return { files: [
56
+ ...result.items.map((item) => item.path),
57
+ ...hookInstallerFiles(hooks),
58
+ ...rulesResult.items.map((item) => item.path)
59
+ ] };
101
60
  }
102
- async function refreshAgentHarnesses(options = {}) {
103
- const context = refreshAgentHarnessesContext(options);
104
- const init = await initPipelineProject({
105
- cwd: context.cwd,
106
- hookInstaller: options.hookInstaller,
107
- rulesInstaller: options.rulesInstaller,
108
- scope: options.scope,
109
- skillInstaller: options.skillInstaller
110
- });
111
- const committed = init.scope === "project" ? await refreshAgentHarnessesCommitResult(context) : false;
112
- return {
113
- ...init,
114
- commitMessage: context.commitMessage,
115
- committed
116
- };
117
- }
118
- function refreshAgentHarnessesContext(options) {
119
- return {
120
- commandRunner: options.commandRunner ?? runCommand,
121
- commitMessage: options.commitMessage ?? DEFAULT_HARNESS_COMMIT_MESSAGE,
122
- cwd: options.cwd ?? process.cwd()
123
- };
124
- }
125
- async function refreshAgentHarnessesCommitResult(context) {
126
- await assertGitWorktree(context.cwd, context.commandRunner);
127
- return stageableOwnedPathsExist(context.cwd) ? await commitOwnedHarnessRefresh(context.cwd, context.commandRunner, context.commitMessage) : false;
61
+ function refreshAgentHarnesses(options = {}) {
62
+ return initPipelineProject(options);
128
63
  }
129
64
  function formatPipelineInitResult(result) {
130
65
  return [
131
66
  "Initialized package-owned pipeline support:",
132
- result.scope === "global" ? "installed the per-machine harness globally (user/global skills + ~/.claude, ~/.config/opencode, ~/.codex); global instruction files generated via rulesync from oisin-ee/rules; inherited by every repo with no per-repo copy" : "installed default skills and host config repo-local",
67
+ "installed the per-machine harness globally (user/global skills + ~/.claude, ~/.config/opencode, ~/.codex); global instruction files generated via rulesync from oisin-ee/rules; inherited by every repo with no per-repo copy",
133
68
  ...result.files.map((path) => `generated ${path}`),
134
69
  "no repo-local pipeline config files were created"
135
70
  ].join("\n");
136
71
  }
137
- function refreshCommitSummary(result) {
138
- if (result.scope === "global") return "global harness refreshed; no repo commit (per-machine install)";
139
- if (result.committed) return `committed refreshed harnesses: ${result.commitMessage}`;
140
- return "refreshed harnesses already current; no commit created";
141
- }
142
72
  function formatRefreshAgentHarnessesResult(result) {
143
- return [formatPipelineInitResult(result), refreshCommitSummary(result)].join("\n");
144
- }
145
- async function runCommand(command, args, options) {
146
- const result = await execa(command, args, {
147
- cwd: options.cwd,
148
- reject: options.reject ?? true
149
- });
150
- return {
151
- exitCode: result.exitCode ?? 0,
152
- stderr: result.stderr,
153
- stdout: result.stdout
154
- };
155
- }
156
- async function assertGitWorktree(cwd, commandRunner) {
157
- await commandRunner("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
158
- }
159
- async function commitOwnedHarnessRefresh(cwd, commandRunner, commitMessage) {
160
- await stageOwnedHarnessPaths(cwd, commandRunner);
161
- await assertOnlyOwnedHarnessFilesStaged(cwd, commandRunner);
162
- if (!await hasStagedChanges(cwd, commandRunner)) return false;
163
- await commandRunner("git", [
164
- "commit",
165
- "--no-verify",
166
- "-m",
167
- commitMessage
168
- ], { cwd });
169
- return true;
170
- }
171
- async function hasStagedChanges(cwd, commandRunner) {
172
- return (await commandRunner("git", [
173
- "diff",
174
- "--cached",
175
- "--quiet"
176
- ], {
177
- cwd,
178
- reject: false
179
- })).exitCode !== 0;
180
- }
181
- async function assertOnlyOwnedHarnessFilesStaged(cwd, commandRunner) {
182
- const unrelatedFiles = (await commandRunner("git", [
183
- "diff",
184
- "--cached",
185
- "--name-only"
186
- ], { cwd })).stdout.split("\n").map((path) => path.trim()).filter(Boolean).filter((path) => !isOwnedHarnessPath(path));
187
- if (unrelatedFiles.length === 0) return;
188
- throw new Error([
189
- "Refusing to commit because unrelated files are already staged.",
190
- ...unrelatedFiles.map((path) => `- ${path}`),
191
- "Unstage unrelated files before running `moka refresh-harnesses`."
192
- ].join("\n"));
193
- }
194
- function isOwnedHarnessPath(path) {
195
- return OWNED_HARNESS_PATHS.some((ownedPath) => path === ownedPath || path.startsWith(`${ownedPath}/`));
196
- }
197
- function existingOwnedPaths(cwd) {
198
- return OWNED_HARNESS_PATHS.filter((path) => existsSync(join(cwd, path)));
199
- }
200
- function stageableOwnedPathsExist(cwd) {
201
- return existingOwnedPaths(cwd).length > 0;
202
- }
203
- async function stageOwnedHarnessPaths(cwd, commandRunner) {
204
- await commandRunner("git", [
205
- "add",
206
- "-A",
207
- "--",
208
- ...existingOwnedPaths(cwd)
209
- ], { cwd });
73
+ return [formatPipelineInitResult(result), "global harness refreshed; no repo commit (per-machine install)"].join("\n");
210
74
  }
211
75
  //#endregion
212
76
  export { formatPipelineInitResult, formatRefreshAgentHarnessesResult, initPipelineProject, refreshAgentHarnesses };
@@ -108,8 +108,8 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
108
108
  nodeId: z.ZodOptional<z.ZodString>;
109
109
  outputs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
110
110
  status: z.ZodEnum<{
111
- pass: "pass";
112
111
  fail: "fail";
112
+ pass: "pass";
113
113
  skip: "skip";
114
114
  }>;
115
115
  summary: z.ZodOptional<z.ZodString>;
@@ -286,8 +286,8 @@ declare const runnerEventBatchSchema: z.ZodObject<{
286
286
  nodeId: z.ZodOptional<z.ZodString>;
287
287
  outputs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
288
288
  status: z.ZodEnum<{
289
- pass: "pass";
290
289
  fail: "fail";
290
+ pass: "pass";
291
291
  skip: "skip";
292
292
  }>;
293
293
  summary: z.ZodOptional<z.ZodString>;
@@ -118,6 +118,8 @@ function executeBaselineWorkflow() {
118
118
  "**/__tests__/**",
119
119
  "test/**",
120
120
  "tests/**",
121
+ "**/test/**",
122
+ "**/tests/**",
121
123
  "**/*.snap"
122
124
  ],
123
125
  require_any: [
@@ -126,7 +128,9 @@ function executeBaselineWorkflow() {
126
128
  "**/*_test.*",
127
129
  "**/__tests__/**",
128
130
  "test/**",
129
- "tests/**"
131
+ "tests/**",
132
+ "**/test/**",
133
+ "**/tests/**"
130
134
  ]
131
135
  },
132
136
  id: "red-test-file-policy",
package/package.json CHANGED
@@ -127,7 +127,7 @@
127
127
  "prepack": "bun run build:cli"
128
128
  },
129
129
  "type": "module",
130
- "version": "3.9.1",
130
+ "version": "3.10.1",
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",