@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 +3 -3
- package/dist/cli/program.js +8 -18
- package/dist/hooks.d.ts +1 -1
- package/dist/install-commands/shared.js +8 -13
- package/dist/install-commands.js +13 -19
- package/dist/install-hooks.js +24 -26
- package/dist/install-rules.js +1 -6
- package/dist/pipeline-init.js +20 -156
- package/dist/runner-event-schema.d.ts +2 -2
- package/dist/schedule/baseline.js +5 -1
- package/package.json +1 -1
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 --
|
|
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
|
|
109
|
-
|
|
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"
|
package/dist/cli/program.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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)).
|
|
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
|
|
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"
|
|
47
|
-
* the
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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(
|
|
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,
|
|
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 };
|
package/dist/install-commands.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
142
|
-
const target = resolveHarnessTarget(
|
|
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
|
|
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
|
|
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(
|
|
169
|
+
async function installDefinitions(definitions, options) {
|
|
176
170
|
const items = [];
|
|
177
|
-
for (const definition of definitions) items.push(await installDefinition(
|
|
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(
|
|
177
|
+
async function removeObsoleteItems(items, options) {
|
|
184
178
|
if (!shouldRemoveObsoleteItems(options)) return;
|
|
185
|
-
for (const item of items) await rm(resolveHarnessTarget(
|
|
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.
|
|
212
|
-
const obsoleteItems = await obsoleteGeneratedItems(context.
|
|
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(
|
|
208
|
+
await removeObsoleteItems(obsoleteItems, options);
|
|
215
209
|
assertNoInstallConflicts(options, items);
|
|
216
210
|
assertInstallCheckCurrent(options, items);
|
|
217
211
|
return { items };
|
package/dist/install-hooks.js
CHANGED
|
@@ -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(
|
|
72
|
-
return resolveHarnessTarget(
|
|
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(
|
|
82
|
-
const path = manifestPath(
|
|
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(
|
|
109
|
-
return resolveHarnessTarget(
|
|
108
|
+
function targetPath(path) {
|
|
109
|
+
return resolveHarnessTarget(path);
|
|
110
110
|
}
|
|
111
|
-
function actionForFile(file,
|
|
112
|
-
const target = targetPath(
|
|
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,
|
|
119
|
+
function planFiles(files, force, manifests) {
|
|
120
120
|
return files.map((file) => ({
|
|
121
121
|
...file,
|
|
122
|
-
action: actionForFile(file,
|
|
122
|
+
action: actionForFile(file, force, manifests)
|
|
123
123
|
}));
|
|
124
124
|
}
|
|
125
|
-
function planObsoleteFiles(desiredPaths,
|
|
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(
|
|
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
|
|
140
|
+
async function writePlannedFile(file) {
|
|
141
141
|
if (file.action === "conflict" || file.action === "unchanged") return;
|
|
142
|
-
const target = targetPath(
|
|
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
|
|
160
|
+
async function removeObsoleteFile(file) {
|
|
161
161
|
if (file.action !== "delete") return;
|
|
162
|
-
await rm(targetPath(
|
|
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
|
|
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(
|
|
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(
|
|
203
|
-
const planned = planFiles(files,
|
|
204
|
-
const obsolete = planObsoleteFiles(new Set(files.map((file) => file.path)),
|
|
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
|
|
210
|
-
for (const file of obsolete) await removeObsoleteFile(file
|
|
211
|
-
await writeManifests(planned
|
|
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,
|
package/dist/install-rules.js
CHANGED
|
@@ -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
|
|
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);
|
package/dist/pipeline-init.js
CHANGED
|
@@ -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
|
|
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
|
-
...
|
|
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(
|
|
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
|
|
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 ?? ((
|
|
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
|
|
92
|
-
const rulesResult = await rulesInstaller(cwd
|
|
93
|
-
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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),
|
|
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.
|
|
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",
|