@oisincoveney/pipeline 3.7.2 → 3.9.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.
- package/README.md +11 -4
- package/dist/argo-submit.d.ts +1 -0
- package/dist/argo-submit.js +2 -0
- package/dist/argo-workflow.js +20 -0
- package/dist/cli/program.js +8 -0
- package/dist/cli/submit-options.js +1 -0
- package/dist/config/schemas.d.ts +4 -4
- package/dist/install-commands/shared.js +1 -3
- package/dist/install-commands.js +2 -6
- package/dist/install-hooks.js +223 -0
- package/dist/install-rules.js +127 -0
- package/dist/moka-global-config.d.ts +1 -0
- package/dist/moka-global-config.js +1 -0
- package/dist/moka-submit.d.ts +9 -6
- package/dist/moka-submit.js +2 -0
- package/dist/pipeline-init.js +38 -8
- package/dist/runner-event-schema.d.ts +6 -6
- package/dist/runtime/opencode-session-executor.js +1 -12
- package/docs/config-architecture.md +18 -0
- package/docs/operator-guide.md +27 -5
- package/package.json +2 -1
- package/defaults/instructions/global.md +0 -68
- package/dist/install-commands/instructions.js +0 -62
package/README.md
CHANGED
|
@@ -43,10 +43,10 @@ Initialize package-owned pipeline support:
|
|
|
43
43
|
moka init
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
`moka init`
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`.pipeline` config files.
|
|
46
|
+
`moka init` installs the package's default skills, generated host command
|
|
47
|
+
surfaces, the singleton `pipeline-gateway` MCP entry, and copied hook files from
|
|
48
|
+
the private `oisin-ee/agent-hooks` repository. OpenCode is the package default
|
|
49
|
+
runtime. The command does not create repo-local `.pipeline` config files.
|
|
50
50
|
|
|
51
51
|
The default MCP gateway can run locally or point at the hosted Momokaya gateway.
|
|
52
52
|
Set `PIPELINE_MCP_GATEWAY_AUTHORIZATION` to the full HTTP `Authorization` header
|
|
@@ -62,6 +62,12 @@ Check or refresh generated host files after package upgrades:
|
|
|
62
62
|
moka install-commands --host all --check
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
Check or refresh copied agent hooks after editing `oisin-ee/agent-hooks`:
|
|
66
|
+
|
|
67
|
+
```shell
|
|
68
|
+
moka install-hooks --scope global --check
|
|
69
|
+
```
|
|
70
|
+
|
|
65
71
|
Check local prerequisites and config health:
|
|
66
72
|
|
|
67
73
|
```shell
|
|
@@ -98,6 +104,7 @@ Canonical commands:
|
|
|
98
104
|
- `moka export <run-id> --sanitize`: print a portable evidence bundle.
|
|
99
105
|
- `moka doctor`: check local prerequisites and config health.
|
|
100
106
|
- `moka init`: install package-owned host resources for a repository.
|
|
107
|
+
- `moka install-hooks`: copy manually authored hooks from `oisin-ee/agent-hooks`.
|
|
101
108
|
- `moka refresh-harnesses`: force-refresh generated agent harnesses and commit
|
|
102
109
|
owned resource changes.
|
|
103
110
|
|
package/dist/argo-submit.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ declare const submitRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
|
|
|
21
21
|
name: z.ZodOptional<z.ZodString>;
|
|
22
22
|
namespace: z.ZodString;
|
|
23
23
|
opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
|
|
24
|
+
opencodeOpenaiAccountsSecretName: z.ZodOptional<z.ZodString>;
|
|
24
25
|
payloadJson: z.ZodString;
|
|
25
26
|
scheduleYaml: z.ZodString;
|
|
26
27
|
serviceAccountName: z.ZodOptional<z.ZodString>;
|
package/dist/argo-submit.js
CHANGED
|
@@ -39,6 +39,7 @@ const submitRunnerArgoWorkflowOptionsSchema = z.object({
|
|
|
39
39
|
name: z.string().min(1).optional(),
|
|
40
40
|
namespace: z.string().min(1),
|
|
41
41
|
opencodeAuthSecretName: z.string().min(1).optional(),
|
|
42
|
+
opencodeOpenaiAccountsSecretName: z.string().min(1).optional(),
|
|
42
43
|
payloadJson: z.string().min(1),
|
|
43
44
|
scheduleYaml: z.string().min(1),
|
|
44
45
|
serviceAccountName: z.string().min(1).optional()
|
|
@@ -89,6 +90,7 @@ function submitRunnerArgoWorkflowEffect(rawOptions, dependencies) {
|
|
|
89
90
|
name: options.name,
|
|
90
91
|
namespace: options.namespace,
|
|
91
92
|
opencodeAuthSecretName: options.opencodeAuthSecretName,
|
|
93
|
+
opencodeOpenaiAccountsSecret: options.opencodeOpenaiAccountsSecretName ? { name: options.opencodeOpenaiAccountsSecretName } : void 0,
|
|
92
94
|
payloadConfigMapName,
|
|
93
95
|
plan: compiled.plan,
|
|
94
96
|
scheduleConfigMapName: scheduleArtifactConfigMapName,
|
package/dist/argo-workflow.js
CHANGED
|
@@ -296,6 +296,26 @@ function runnerWorkflowStorage(options, tasks) {
|
|
|
296
296
|
subPath: "auth.json"
|
|
297
297
|
});
|
|
298
298
|
}
|
|
299
|
+
if (options.opencodeOpenaiAccountsSecret) {
|
|
300
|
+
const accountsKey = options.opencodeOpenaiAccountsSecret.key ?? "accounts.json";
|
|
301
|
+
volumes.push({
|
|
302
|
+
name: "opencode-openai-accounts",
|
|
303
|
+
secret: {
|
|
304
|
+
defaultMode: 256,
|
|
305
|
+
items: [{
|
|
306
|
+
key: accountsKey,
|
|
307
|
+
path: "accounts.json"
|
|
308
|
+
}],
|
|
309
|
+
secretName: options.opencodeOpenaiAccountsSecret.name
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
volumeMounts.push({
|
|
313
|
+
mountPath: "/root/.opencode/oc-codex-multi-auth-accounts.json",
|
|
314
|
+
name: "opencode-openai-accounts",
|
|
315
|
+
readOnly: true,
|
|
316
|
+
subPath: "accounts.json"
|
|
317
|
+
});
|
|
318
|
+
}
|
|
299
319
|
if (options.gitCredentialsSecretName) {
|
|
300
320
|
volumes.push({
|
|
301
321
|
name: "runner-git-credentials",
|
package/dist/cli/program.js
CHANGED
|
@@ -16,6 +16,7 @@ import { MOKA_RUN_EFFORTS, MOKA_RUN_TARGETS, resolveMokaRun } from "./run-resolv
|
|
|
16
16
|
import { registerTicketCommand } from "../commands/ticket-command.js";
|
|
17
17
|
import { formatConfigLintWarning, lintPipelineConfig } from "../config/lint.js";
|
|
18
18
|
import { formatInstallCommandsResult, installCommands, parseCommandHost } from "../install-commands.js";
|
|
19
|
+
import { formatInstallHooksResult, installHooks } from "../install-hooks.js";
|
|
19
20
|
import { formatPipelineInitResult, formatRefreshAgentHarnessesResult, initPipelineProject, refreshAgentHarnesses } from "../pipeline-init.js";
|
|
20
21
|
import { createRun, runControlStatusPaths, updateRunController } from "../run-control/store.js";
|
|
21
22
|
import { registerRunControlCommands } from "../run-control/commands.js";
|
|
@@ -357,6 +358,13 @@ function createCliProgram(options = {}) {
|
|
|
357
358
|
});
|
|
358
359
|
console.log(formatInstallCommandsResult(result));
|
|
359
360
|
});
|
|
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
|
+
});
|
|
366
|
+
console.log(formatInstallHooksResult(result));
|
|
367
|
+
});
|
|
360
368
|
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) => {
|
|
361
369
|
const result = syncLocalCodexAuth({
|
|
362
370
|
check: flags.check,
|
|
@@ -49,6 +49,7 @@ function mokaCommonSubmitOptions(input) {
|
|
|
49
49
|
name: input.flags.name,
|
|
50
50
|
namespace: input.flags.namespace ?? momokaya?.kubernetes.namespace,
|
|
51
51
|
opencodeAuthSecretName: momokaya?.submit.opencodeAuthSecretName,
|
|
52
|
+
opencodeOpenaiAccountsSecretName: momokaya?.submit.opencodeOpenaiAccountsSecretName,
|
|
52
53
|
serviceAccountName: input.flags.serviceAccount ?? momokaya?.submit.serviceAccountName,
|
|
53
54
|
worktreePath: input.cwd
|
|
54
55
|
};
|
package/dist/config/schemas.d.ts
CHANGED
|
@@ -226,8 +226,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
226
226
|
policy: z.ZodOptional<z.ZodObject<{
|
|
227
227
|
commands: z.ZodOptional<z.ZodEnum<{
|
|
228
228
|
allow: "allow";
|
|
229
|
-
"trusted-only": "trusted-only";
|
|
230
229
|
deny: "deny";
|
|
230
|
+
"trusted-only": "trusted-only";
|
|
231
231
|
}>>;
|
|
232
232
|
modules: z.ZodOptional<z.ZodEnum<{
|
|
233
233
|
allow: "allow";
|
|
@@ -255,8 +255,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
255
255
|
global: "global";
|
|
256
256
|
}>>;
|
|
257
257
|
mode: z.ZodEnum<{
|
|
258
|
-
hosted: "hosted";
|
|
259
258
|
local: "local";
|
|
259
|
+
hosted: "hosted";
|
|
260
260
|
}>;
|
|
261
261
|
provider: z.ZodLiteral<"toolhive">;
|
|
262
262
|
authorization_env: z.ZodDefault<z.ZodString>;
|
|
@@ -299,10 +299,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
299
299
|
}, z.core.$strict>>;
|
|
300
300
|
output: z.ZodOptional<z.ZodObject<{
|
|
301
301
|
format: z.ZodEnum<{
|
|
302
|
+
json_schema: "json_schema";
|
|
302
303
|
text: "text";
|
|
303
304
|
json: "json";
|
|
304
305
|
jsonl: "jsonl";
|
|
305
|
-
json_schema: "json_schema";
|
|
306
306
|
}>;
|
|
307
307
|
repair: z.ZodOptional<z.ZodObject<{
|
|
308
308
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -371,10 +371,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
371
371
|
disabled: "disabled";
|
|
372
372
|
}>>>;
|
|
373
373
|
output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
374
|
+
json_schema: "json_schema";
|
|
374
375
|
text: "text";
|
|
375
376
|
json: "json";
|
|
376
377
|
jsonl: "jsonl";
|
|
377
|
-
json_schema: "json_schema";
|
|
378
378
|
}>>>;
|
|
379
379
|
rules: z.ZodOptional<z.ZodBoolean>;
|
|
380
380
|
skills: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -9,8 +9,6 @@ const OWNER_TS_MARKER_PREFIX = "// @oisincoveney/pipeline:";
|
|
|
9
9
|
const OWNER_YAML_MARKER_PREFIX = "# @oisincoveney/pipeline:";
|
|
10
10
|
const AGENTS_MD_START = "<!-- @oisincoveney/pipeline:agents:start -->";
|
|
11
11
|
const AGENTS_MD_END = "<!-- @oisincoveney/pipeline:agents:end -->";
|
|
12
|
-
const INSTRUCTIONS_START = "<!-- @oisincoveney/pipeline:instructions:start -->";
|
|
13
|
-
const INSTRUCTIONS_END = "<!-- @oisincoveney/pipeline:instructions:end -->";
|
|
14
12
|
const SINGLE_OPENCODE_PLUGIN_ARRAY_RE = /\n {2}"plugin": \[\n {4}("[^"]+")\n {2}\]/;
|
|
15
13
|
const OPENCODE_PROJECT_CONFIG_PATH = ".opencode/opencode.json";
|
|
16
14
|
const CLAUDE_PROJECT_CONFIG_PATH = ".claude/settings.json";
|
|
@@ -94,4 +92,4 @@ function commandIdForHost(host, entrypointId) {
|
|
|
94
92
|
return entrypointId;
|
|
95
93
|
}
|
|
96
94
|
//#endregion
|
|
97
|
-
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,
|
|
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 };
|
package/dist/install-commands.js
CHANGED
|
@@ -3,7 +3,6 @@ import "./config.js";
|
|
|
3
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
|
-
import { INSTRUCTION_PATHS, globalInstructionDefinitions } from "./install-commands/instructions.js";
|
|
7
6
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
8
7
|
import { dirname, join, relative } from "node:path";
|
|
9
8
|
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -156,18 +155,15 @@ function commandInstallPlanItem(definition, action) {
|
|
|
156
155
|
};
|
|
157
156
|
}
|
|
158
157
|
const PROJECT_ONLY_PATHS = new Set(["AGENTS.md"]);
|
|
159
|
-
const GLOBAL_ONLY_PATHS = new Set(INSTRUCTION_PATHS);
|
|
160
158
|
function scopedDefinitions(definitions, scope) {
|
|
161
159
|
if (scope === "global") return definitions.filter((definition) => !PROJECT_ONLY_PATHS.has(definition.path));
|
|
162
|
-
return definitions
|
|
160
|
+
return definitions;
|
|
163
161
|
}
|
|
164
162
|
function installCommandsContext(options) {
|
|
165
163
|
const cwd = options.cwd ?? process.cwd();
|
|
166
164
|
const host = options.host ?? "all";
|
|
167
165
|
const scope = options.scope ?? "global";
|
|
168
|
-
const
|
|
169
|
-
const instructionDefinitions = globalInstructionDefinitions().filter((definition) => host === "all" || definition.host === host);
|
|
170
|
-
const definitions = scopedDefinitions([...definitionsFor(host, config, cwd), ...instructionDefinitions], scope);
|
|
166
|
+
const definitions = scopedDefinitions(definitionsFor(host, loadPipelineConfig(cwd, { allowMissingLintFileReferences: true }), cwd), scope);
|
|
171
167
|
return {
|
|
172
168
|
cwd,
|
|
173
169
|
definitions,
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { resolveHarnessTarget } from "./install-commands/shared.js";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
import { dirname, join, relative } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
//#region src/install-hooks.ts
|
|
9
|
+
const DEFAULT_HOOK_INSTALL_SOURCE = "oisin-ee/agent-hooks";
|
|
10
|
+
const HOOK_HOSTS = [
|
|
11
|
+
"claude-code",
|
|
12
|
+
"codex",
|
|
13
|
+
"opencode"
|
|
14
|
+
];
|
|
15
|
+
const MANIFEST_FILE = ".moka-agent-hooks.json";
|
|
16
|
+
const HOST_TARGET_ROOT = {
|
|
17
|
+
"claude-code": ".claude",
|
|
18
|
+
codex: ".codex",
|
|
19
|
+
opencode: ".opencode"
|
|
20
|
+
};
|
|
21
|
+
function hashContent(content) {
|
|
22
|
+
return createHash("sha256").update(content).digest("hex");
|
|
23
|
+
}
|
|
24
|
+
async function cloneHookRepository(targetDir) {
|
|
25
|
+
await execa("gh", [
|
|
26
|
+
"repo",
|
|
27
|
+
"clone",
|
|
28
|
+
DEFAULT_HOOK_INSTALL_SOURCE,
|
|
29
|
+
targetDir,
|
|
30
|
+
"--",
|
|
31
|
+
"--depth=1"
|
|
32
|
+
], { stdio: "inherit" });
|
|
33
|
+
}
|
|
34
|
+
async function withHookSource(useSource) {
|
|
35
|
+
const parent = await mkdtemp(join(tmpdir(), "moka-agent-hooks-"));
|
|
36
|
+
const source = join(parent, "agent-hooks");
|
|
37
|
+
try {
|
|
38
|
+
await cloneHookRepository(source);
|
|
39
|
+
return await useSource(source);
|
|
40
|
+
} finally {
|
|
41
|
+
await rm(parent, {
|
|
42
|
+
force: true,
|
|
43
|
+
recursive: true
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function listFiles(root) {
|
|
48
|
+
if (!existsSync(root)) return [];
|
|
49
|
+
if (statSync(root).isFile()) return [root];
|
|
50
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
51
|
+
return (await Promise.all(entries.map((entry) => {
|
|
52
|
+
const path = join(root, entry.name);
|
|
53
|
+
return entry.isDirectory() ? listFiles(path) : [path];
|
|
54
|
+
}))).flat();
|
|
55
|
+
}
|
|
56
|
+
async function sourceHookFiles(source) {
|
|
57
|
+
return (await Promise.all(HOOK_HOSTS.map(async (host) => {
|
|
58
|
+
const hostRoot = join(source, host);
|
|
59
|
+
return (await listFiles(hostRoot)).map((file) => {
|
|
60
|
+
const relativePath = relative(hostRoot, file).replaceAll("\\", "/");
|
|
61
|
+
const content = readFileSync(file);
|
|
62
|
+
return {
|
|
63
|
+
content,
|
|
64
|
+
hash: hashContent(content),
|
|
65
|
+
host,
|
|
66
|
+
path: `${HOST_TARGET_ROOT[host]}/${relativePath}`
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}))).flat().sort((a, b) => a.path.localeCompare(b.path));
|
|
70
|
+
}
|
|
71
|
+
function manifestPath(scope, cwd, host) {
|
|
72
|
+
return resolveHarnessTarget(scope, cwd, `${HOST_TARGET_ROOT[host]}/${MANIFEST_FILE}`);
|
|
73
|
+
}
|
|
74
|
+
function emptyManifest() {
|
|
75
|
+
return {
|
|
76
|
+
files: {},
|
|
77
|
+
repository: DEFAULT_HOOK_INSTALL_SOURCE,
|
|
78
|
+
version: 1
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function readManifest(scope, cwd, host) {
|
|
82
|
+
const path = manifestPath(scope, cwd, host);
|
|
83
|
+
if (!existsSync(path)) return emptyManifest();
|
|
84
|
+
try {
|
|
85
|
+
return normalizeManifest(JSON.parse(readFileSync(path, "utf8")));
|
|
86
|
+
} catch {
|
|
87
|
+
return emptyManifest();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function isRecord(value) {
|
|
91
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
92
|
+
}
|
|
93
|
+
function normalizeManifest(value) {
|
|
94
|
+
const files = {};
|
|
95
|
+
const manifestFiles = isRecord(value) ? value.files : void 0;
|
|
96
|
+
if (!isRecord(manifestFiles)) return {
|
|
97
|
+
files,
|
|
98
|
+
repository: DEFAULT_HOOK_INSTALL_SOURCE,
|
|
99
|
+
version: 1
|
|
100
|
+
};
|
|
101
|
+
for (const [path, entry] of Object.entries(manifestFiles)) if (isRecord(entry) && typeof entry.hash === "string") files[path] = { hash: entry.hash };
|
|
102
|
+
return {
|
|
103
|
+
files,
|
|
104
|
+
repository: DEFAULT_HOOK_INSTALL_SOURCE,
|
|
105
|
+
version: 1
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function targetPath(scope, cwd, path) {
|
|
109
|
+
return resolveHarnessTarget(scope, cwd, path);
|
|
110
|
+
}
|
|
111
|
+
function actionForFile(file, scope, cwd, force, manifests) {
|
|
112
|
+
const target = targetPath(scope, cwd, file.path);
|
|
113
|
+
if (!existsSync(target)) return "create";
|
|
114
|
+
const currentHash = hashContent(readFileSync(target));
|
|
115
|
+
if (currentHash === file.hash) return "unchanged";
|
|
116
|
+
if (force) return "update";
|
|
117
|
+
return (manifests.get(file.host)?.files[file.path])?.hash === currentHash ? "update" : "conflict";
|
|
118
|
+
}
|
|
119
|
+
function planFiles(files, scope, cwd, force, manifests) {
|
|
120
|
+
return files.map((file) => ({
|
|
121
|
+
...file,
|
|
122
|
+
action: actionForFile(file, scope, cwd, force, manifests)
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
function planObsoleteFiles(desiredPaths, scope, cwd, force, manifests) {
|
|
126
|
+
const obsolete = [];
|
|
127
|
+
for (const [host, manifest] of manifests) for (const [path, entry] of Object.entries(manifest.files)) {
|
|
128
|
+
if (desiredPaths.has(path)) continue;
|
|
129
|
+
const target = targetPath(scope, cwd, path);
|
|
130
|
+
if (!existsSync(target)) continue;
|
|
131
|
+
const currentHash = hashContent(readFileSync(target));
|
|
132
|
+
obsolete.push({
|
|
133
|
+
action: force || currentHash === entry.hash ? "delete" : "conflict",
|
|
134
|
+
host,
|
|
135
|
+
path
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return obsolete.sort((a, b) => a.path.localeCompare(b.path));
|
|
139
|
+
}
|
|
140
|
+
async function writePlannedFile(file, scope, cwd) {
|
|
141
|
+
if (file.action === "conflict" || file.action === "unchanged") return;
|
|
142
|
+
const target = targetPath(scope, cwd, file.path);
|
|
143
|
+
await mkdir(dirname(target), { recursive: true });
|
|
144
|
+
await writeFile(target, file.content);
|
|
145
|
+
}
|
|
146
|
+
function itemFor(file) {
|
|
147
|
+
return {
|
|
148
|
+
action: file.action,
|
|
149
|
+
host: file.host,
|
|
150
|
+
path: file.path
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function itemForObsolete(file) {
|
|
154
|
+
return {
|
|
155
|
+
action: file.action,
|
|
156
|
+
host: file.host,
|
|
157
|
+
path: file.path
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function removeObsoleteFile(file, scope, cwd) {
|
|
161
|
+
if (file.action !== "delete") return;
|
|
162
|
+
await rm(targetPath(scope, cwd, file.path), { force: true });
|
|
163
|
+
}
|
|
164
|
+
function assertNoConflicts(items, dryRun) {
|
|
165
|
+
if (dryRun) return;
|
|
166
|
+
const conflicts = items.filter((item) => item.action === "conflict");
|
|
167
|
+
if (conflicts.length === 0) return;
|
|
168
|
+
throw new Error([
|
|
169
|
+
"Refusing to overwrite manually edited hook files.",
|
|
170
|
+
...conflicts.map((item) => `- ${item.path}`),
|
|
171
|
+
"Re-run with --force to overwrite them."
|
|
172
|
+
].join("\n"));
|
|
173
|
+
}
|
|
174
|
+
function assertCheckCurrent(items, check) {
|
|
175
|
+
if (!check) return;
|
|
176
|
+
const changed = items.filter((item) => item.action !== "unchanged");
|
|
177
|
+
if (changed.length === 0) return;
|
|
178
|
+
throw new Error(["Installed hook files are not up to date.", ...changed.map((item) => `- ${item.path}: ${item.action}`)].join("\n"));
|
|
179
|
+
}
|
|
180
|
+
async function writeManifests(files, scope, cwd) {
|
|
181
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const host of HOOK_HOSTS) byHost.set(host, emptyManifest());
|
|
183
|
+
for (const file of files) {
|
|
184
|
+
const manifest = byHost.get(file.host);
|
|
185
|
+
if (manifest) manifest.files[file.path] = { hash: file.hash };
|
|
186
|
+
}
|
|
187
|
+
await Promise.all([...byHost.entries()].map(async ([host, manifest]) => {
|
|
188
|
+
const path = manifestPath(scope, cwd, host);
|
|
189
|
+
if (Object.keys(manifest.files).length === 0) {
|
|
190
|
+
await rm(path, { force: true });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
await mkdir(dirname(path), { recursive: true });
|
|
194
|
+
await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
function installHooks(options = {}) {
|
|
198
|
+
const cwd = options.cwd ?? process.cwd();
|
|
199
|
+
const scope = options.scope ?? "global";
|
|
200
|
+
return withHookSource(async (source) => {
|
|
201
|
+
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);
|
|
205
|
+
const items = [...planned.map(itemFor), ...obsolete.map(itemForObsolete)];
|
|
206
|
+
assertCheckCurrent(items, Boolean(options.check));
|
|
207
|
+
assertNoConflicts(items, Boolean(options.dryRun));
|
|
208
|
+
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);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
items,
|
|
215
|
+
source: DEFAULT_HOOK_INSTALL_SOURCE
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function formatInstallHooksResult(result) {
|
|
220
|
+
return result.items.map((item) => `${item.action} ${item.host}: ${item.path}`).join("\n");
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
export { formatInstallHooksResult, installHooks };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import "./install-commands/shared.js";
|
|
2
|
+
import { execa } from "execa";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
//#region src/install-rules.ts
|
|
8
|
+
const DEFAULT_RULES_INSTALL_SOURCE = "oisin-ee/rules";
|
|
9
|
+
const RULESYNC_TARGETS = [
|
|
10
|
+
"claudecode",
|
|
11
|
+
"codexcli",
|
|
12
|
+
"geminicli",
|
|
13
|
+
"opencode"
|
|
14
|
+
];
|
|
15
|
+
function packageRoot() {
|
|
16
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
}
|
|
18
|
+
const PACKAGE_ROOT = packageRoot();
|
|
19
|
+
async function cloneRulesRepository(targetDir) {
|
|
20
|
+
await execa("gh", [
|
|
21
|
+
"repo",
|
|
22
|
+
"clone",
|
|
23
|
+
DEFAULT_RULES_INSTALL_SOURCE,
|
|
24
|
+
targetDir,
|
|
25
|
+
"--",
|
|
26
|
+
"--depth=1"
|
|
27
|
+
], { stdio: "inherit" });
|
|
28
|
+
}
|
|
29
|
+
async function withRulesSource(sourceOverride, useSource) {
|
|
30
|
+
if (sourceOverride !== void 0) return useSource(sourceOverride);
|
|
31
|
+
const parent = await mkdtemp(join(tmpdir(), "moka-rules-"));
|
|
32
|
+
const source = join(parent, "rules");
|
|
33
|
+
try {
|
|
34
|
+
await cloneRulesRepository(source);
|
|
35
|
+
return await useSource(source);
|
|
36
|
+
} finally {
|
|
37
|
+
await rm(parent, {
|
|
38
|
+
force: true,
|
|
39
|
+
recursive: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function defaultRulesyncRunner(args, opts) {
|
|
44
|
+
try {
|
|
45
|
+
await execa("rulesync", args, {
|
|
46
|
+
cwd: opts.cwd,
|
|
47
|
+
env: opts.env,
|
|
48
|
+
localDir: PACKAGE_ROOT,
|
|
49
|
+
preferLocal: true,
|
|
50
|
+
stdio: "inherit"
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
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\`.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function buildRootRule(source) {
|
|
58
|
+
const rulesDir = join(source, "rules");
|
|
59
|
+
let entries = [];
|
|
60
|
+
try {
|
|
61
|
+
entries = (await readdir(rulesDir, { withFileTypes: true })).filter((d) => d.isFile() && d.name.endsWith(".md")).map((d) => d.name).sort((a, b) => a.localeCompare(b));
|
|
62
|
+
} catch {}
|
|
63
|
+
const rootContent = `---
|
|
64
|
+
root: true
|
|
65
|
+
targets:
|
|
66
|
+
- "*"
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
${(await Promise.all(entries.map(async (name) => {
|
|
70
|
+
return (await readFile(join(rulesDir, name), "utf8")).trimEnd();
|
|
71
|
+
}))).join("\n\n")}\n`;
|
|
72
|
+
const rulesyncRulesDir = join(source, ".rulesync", "rules");
|
|
73
|
+
await mkdir(rulesyncRulesDir, { recursive: true });
|
|
74
|
+
await writeFile(join(rulesyncRulesDir, "_root.md"), rootContent);
|
|
75
|
+
}
|
|
76
|
+
function installRules(options = {}) {
|
|
77
|
+
if ((options.scope ?? "global") !== "global") return Promise.resolve({
|
|
78
|
+
items: [],
|
|
79
|
+
source: DEFAULT_RULES_INSTALL_SOURCE
|
|
80
|
+
});
|
|
81
|
+
const runner = options.rulesyncRunner ?? defaultRulesyncRunner;
|
|
82
|
+
return withRulesSource(options.sourceOverride, async (source) => {
|
|
83
|
+
await buildRootRule(source);
|
|
84
|
+
const home = process.env.HOME_DIR ?? homedir();
|
|
85
|
+
const args = [
|
|
86
|
+
"generate",
|
|
87
|
+
"-t",
|
|
88
|
+
RULESYNC_TARGETS.join(","),
|
|
89
|
+
"-f",
|
|
90
|
+
"rules",
|
|
91
|
+
"--delete"
|
|
92
|
+
];
|
|
93
|
+
if (options.dryRun) args.push("--dry-run");
|
|
94
|
+
if (options.check) args.push("--check");
|
|
95
|
+
await runner(args, {
|
|
96
|
+
cwd: source,
|
|
97
|
+
env: {
|
|
98
|
+
...process.env,
|
|
99
|
+
HOME_DIR: home
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
const action = options.dryRun ?? options.check ? "skip" : "generate";
|
|
103
|
+
return {
|
|
104
|
+
items: [
|
|
105
|
+
{
|
|
106
|
+
action,
|
|
107
|
+
path: join(home, ".claude/CLAUDE.md")
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
action,
|
|
111
|
+
path: join(home, ".codex/AGENTS.md")
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
action,
|
|
115
|
+
path: join(home, ".gemini/GEMINI.md")
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
action,
|
|
119
|
+
path: join(home, ".config/opencode/AGENTS.md")
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
source: DEFAULT_RULES_INSTALL_SOURCE
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
export { installRules };
|
|
@@ -16,6 +16,7 @@ declare const mokaGlobalConfigSchema: z.ZodObject<{
|
|
|
16
16
|
githubAuthSecretName: z.ZodString;
|
|
17
17
|
imagePullSecretName: z.ZodString;
|
|
18
18
|
opencodeAuthSecretName: z.ZodString;
|
|
19
|
+
opencodeOpenaiAccountsSecretName: z.ZodOptional<z.ZodString>;
|
|
19
20
|
serviceAccountName: z.ZodString;
|
|
20
21
|
}, z.core.$strict>;
|
|
21
22
|
}, z.core.$strict>;
|
|
@@ -15,6 +15,7 @@ const mokaSubmitGlobalConfigSchema = z.object({
|
|
|
15
15
|
githubAuthSecretName: z.string().min(1),
|
|
16
16
|
imagePullSecretName: z.string().min(1),
|
|
17
17
|
opencodeAuthSecretName: z.string().min(1),
|
|
18
|
+
opencodeOpenaiAccountsSecretName: z.string().min(1).optional(),
|
|
18
19
|
serviceAccountName: z.string().min(1)
|
|
19
20
|
}).strict();
|
|
20
21
|
const mokaKubernetesGlobalConfigSchema = z.object({
|
package/dist/moka-submit.d.ts
CHANGED
|
@@ -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";
|
|
10
8
|
"workflow.success": "workflow.success";
|
|
11
9
|
"workflow.failure": "workflow.failure";
|
|
12
10
|
"workflow.complete": "workflow.complete";
|
|
11
|
+
"node.start": "node.start";
|
|
13
12
|
"node.success": "node.success";
|
|
14
13
|
"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";
|
|
99
97
|
"workflow.success": "workflow.success";
|
|
100
98
|
"workflow.failure": "workflow.failure";
|
|
101
99
|
"workflow.complete": "workflow.complete";
|
|
100
|
+
"node.start": "node.start";
|
|
102
101
|
"node.success": "node.success";
|
|
103
102
|
"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<{
|
|
@@ -148,6 +148,7 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
148
148
|
name: z.ZodOptional<z.ZodString>;
|
|
149
149
|
namespace: z.ZodOptional<z.ZodString>;
|
|
150
150
|
opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
|
|
151
|
+
opencodeOpenaiAccountsSecretName: z.ZodOptional<z.ZodString>;
|
|
151
152
|
repository: z.ZodOptional<z.ZodObject<{
|
|
152
153
|
baseBranch: z.ZodString;
|
|
153
154
|
sha: z.ZodOptional<z.ZodString>;
|
|
@@ -206,13 +207,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
206
207
|
}, z.core.$strict>>;
|
|
207
208
|
hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
208
209
|
"workflow.start": "workflow.start";
|
|
209
|
-
"node.finish": "node.finish";
|
|
210
|
-
"node.start": "node.start";
|
|
211
210
|
"workflow.success": "workflow.success";
|
|
212
211
|
"workflow.failure": "workflow.failure";
|
|
213
212
|
"workflow.complete": "workflow.complete";
|
|
213
|
+
"node.start": "node.start";
|
|
214
214
|
"node.success": "node.success";
|
|
215
215
|
"node.error": "node.error";
|
|
216
|
+
"node.finish": "node.finish";
|
|
216
217
|
"gate.failure": "gate.failure";
|
|
217
218
|
}> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
218
219
|
failure: z.ZodDefault<z.ZodEnum<{
|
|
@@ -260,6 +261,7 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
260
261
|
name: z.ZodOptional<z.ZodString>;
|
|
261
262
|
namespace: z.ZodOptional<z.ZodString>;
|
|
262
263
|
opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
|
|
264
|
+
opencodeOpenaiAccountsSecretName: z.ZodOptional<z.ZodString>;
|
|
263
265
|
repository: z.ZodOptional<z.ZodObject<{
|
|
264
266
|
baseBranch: z.ZodString;
|
|
265
267
|
sha: z.ZodOptional<z.ZodString>;
|
|
@@ -324,6 +326,7 @@ interface MokaWorkflowSubmitOptions {
|
|
|
324
326
|
name?: string;
|
|
325
327
|
namespace: string;
|
|
326
328
|
opencodeAuthSecretName?: string;
|
|
329
|
+
opencodeOpenaiAccountsSecretName?: string;
|
|
327
330
|
payloadJson: string;
|
|
328
331
|
scheduleYaml: string;
|
|
329
332
|
serviceAccountName?: string;
|
package/dist/moka-submit.js
CHANGED
|
@@ -78,6 +78,7 @@ const mokaSubmitBaseOptionsSchema = z.object({
|
|
|
78
78
|
name: z.string().min(1).optional(),
|
|
79
79
|
namespace: z.string().min(1).optional(),
|
|
80
80
|
opencodeAuthSecretName: z.string().min(1).optional(),
|
|
81
|
+
opencodeOpenaiAccountsSecretName: z.string().min(1).optional(),
|
|
81
82
|
repository: runnerRepositoryContextSchema.optional(),
|
|
82
83
|
run: runnerRunIdentitySchema.optional(),
|
|
83
84
|
serviceAccountName: z.string().min(1).optional()
|
|
@@ -300,6 +301,7 @@ function workflowSubmitOptions(options) {
|
|
|
300
301
|
name: options.name,
|
|
301
302
|
namespace: requireSubmitOption(options.namespace, "namespace"),
|
|
302
303
|
opencodeAuthSecretName: options.opencodeAuthSecretName,
|
|
304
|
+
opencodeOpenaiAccountsSecretName: options.opencodeOpenaiAccountsSecretName,
|
|
303
305
|
serviceAccountName: options.serviceAccountName
|
|
304
306
|
};
|
|
305
307
|
}
|
package/dist/pipeline-init.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import "./install-commands/shared.js";
|
|
2
2
|
import { installCommands } from "./install-commands.js";
|
|
3
|
+
import { installHooks } from "./install-hooks.js";
|
|
4
|
+
import { installRules } from "./install-rules.js";
|
|
3
5
|
import { existsSync } from "node:fs";
|
|
4
6
|
import { execa } from "execa";
|
|
5
7
|
import { join } from "node:path";
|
|
@@ -33,8 +35,12 @@ const OWNED_HARNESS_PATHS = [
|
|
|
33
35
|
".agents/skills",
|
|
34
36
|
".claude/agents",
|
|
35
37
|
".claude/commands",
|
|
38
|
+
".claude/hooks",
|
|
39
|
+
".claude/.moka-agent-hooks.json",
|
|
36
40
|
".claude/settings.json",
|
|
37
41
|
".claude/skills",
|
|
42
|
+
".codex/hooks",
|
|
43
|
+
".codex/.moka-agent-hooks.json",
|
|
38
44
|
".codex/skills",
|
|
39
45
|
".opencode",
|
|
40
46
|
"AGENTS.md",
|
|
@@ -57,17 +63,39 @@ async function installDefaultSkills(cwd, scope) {
|
|
|
57
63
|
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\`.`);
|
|
58
64
|
}
|
|
59
65
|
}
|
|
66
|
+
function installDefaultHooks(cwd, scope) {
|
|
67
|
+
return installHooks({
|
|
68
|
+
cwd,
|
|
69
|
+
scope
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function hookInstallerFiles(result) {
|
|
73
|
+
return "items" in result ? result.items.map((item) => item.path) : result.files;
|
|
74
|
+
}
|
|
60
75
|
async function initPipelineProject(options = {}) {
|
|
61
76
|
const cwd = options.cwd ?? process.cwd();
|
|
62
77
|
const scope = options.scope ?? "global";
|
|
63
|
-
|
|
78
|
+
const skillInstaller = options.skillInstaller ?? ((target) => installDefaultSkills(target, skillScopeFor(scope)));
|
|
79
|
+
const hookInstaller = options.hookInstaller ?? installDefaultHooks;
|
|
80
|
+
const rulesInstaller = options.rulesInstaller ?? ((target, s) => installRules({
|
|
81
|
+
cwd: target,
|
|
82
|
+
scope: s
|
|
83
|
+
}));
|
|
84
|
+
await skillInstaller(cwd);
|
|
85
|
+
const result = await installCommands({
|
|
86
|
+
cwd,
|
|
87
|
+
force: true,
|
|
88
|
+
host: "all",
|
|
89
|
+
scope
|
|
90
|
+
});
|
|
91
|
+
const hooks = await hookInstaller(cwd, scope);
|
|
92
|
+
const rulesResult = await rulesInstaller(cwd, scope);
|
|
64
93
|
return {
|
|
65
|
-
files:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
})).items.map((item) => item.path),
|
|
94
|
+
files: [
|
|
95
|
+
...result.items.map((item) => item.path),
|
|
96
|
+
...hookInstallerFiles(hooks),
|
|
97
|
+
...rulesResult.items.map((item) => item.path)
|
|
98
|
+
],
|
|
71
99
|
scope
|
|
72
100
|
};
|
|
73
101
|
}
|
|
@@ -75,6 +103,8 @@ async function refreshAgentHarnesses(options = {}) {
|
|
|
75
103
|
const context = refreshAgentHarnessesContext(options);
|
|
76
104
|
const init = await initPipelineProject({
|
|
77
105
|
cwd: context.cwd,
|
|
106
|
+
hookInstaller: options.hookInstaller,
|
|
107
|
+
rulesInstaller: options.rulesInstaller,
|
|
78
108
|
scope: options.scope,
|
|
79
109
|
skillInstaller: options.skillInstaller
|
|
80
110
|
});
|
|
@@ -99,7 +129,7 @@ async function refreshAgentHarnessesCommitResult(context) {
|
|
|
99
129
|
function formatPipelineInitResult(result) {
|
|
100
130
|
return [
|
|
101
131
|
"Initialized package-owned pipeline support:",
|
|
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",
|
|
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",
|
|
103
133
|
...result.files.map((path) => `generated ${path}`),
|
|
104
134
|
"no repo-local pipeline config files were created"
|
|
105
135
|
].join("\n");
|
|
@@ -11,8 +11,8 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
11
11
|
runId: z.ZodString;
|
|
12
12
|
sequence: z.ZodNumber;
|
|
13
13
|
type: z.ZodEnum<{
|
|
14
|
-
"workflow.planned": "workflow.planned";
|
|
15
14
|
"workflow.start": "workflow.start";
|
|
15
|
+
"workflow.planned": "workflow.planned";
|
|
16
16
|
}>;
|
|
17
17
|
workflowPlan: z.ZodObject<{
|
|
18
18
|
edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -58,10 +58,10 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
58
58
|
}>;
|
|
59
59
|
}, z.core.$strip>;
|
|
60
60
|
type: z.ZodEnum<{
|
|
61
|
+
"node.start": "node.start";
|
|
62
|
+
"node.finish": "node.finish";
|
|
61
63
|
"agent.finish": "agent.finish";
|
|
62
64
|
"agent.start": "agent.start";
|
|
63
|
-
"node.finish": "node.finish";
|
|
64
|
-
"node.start": "node.start";
|
|
65
65
|
}>;
|
|
66
66
|
}, z.core.$strip>, z.ZodObject<{
|
|
67
67
|
at: z.ZodOptional<z.ZodString>;
|
|
@@ -189,8 +189,8 @@ declare const runnerEventBatchSchema: z.ZodObject<{
|
|
|
189
189
|
runId: z.ZodString;
|
|
190
190
|
sequence: z.ZodNumber;
|
|
191
191
|
type: z.ZodEnum<{
|
|
192
|
-
"workflow.planned": "workflow.planned";
|
|
193
192
|
"workflow.start": "workflow.start";
|
|
193
|
+
"workflow.planned": "workflow.planned";
|
|
194
194
|
}>;
|
|
195
195
|
workflowPlan: z.ZodObject<{
|
|
196
196
|
edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -236,10 +236,10 @@ declare const runnerEventBatchSchema: z.ZodObject<{
|
|
|
236
236
|
}>;
|
|
237
237
|
}, z.core.$strip>;
|
|
238
238
|
type: z.ZodEnum<{
|
|
239
|
+
"node.start": "node.start";
|
|
240
|
+
"node.finish": "node.finish";
|
|
239
241
|
"agent.finish": "agent.finish";
|
|
240
242
|
"agent.start": "agent.start";
|
|
241
|
-
"node.finish": "node.finish";
|
|
242
|
-
"node.start": "node.start";
|
|
243
243
|
}>;
|
|
244
244
|
}, z.core.$strip>, z.ZodObject<{
|
|
245
245
|
at: z.ZodOptional<z.ZodString>;
|
|
@@ -324,18 +324,7 @@ function unwrap(response) {
|
|
|
324
324
|
return response.data;
|
|
325
325
|
}
|
|
326
326
|
function errorMessage(error) {
|
|
327
|
-
|
|
328
|
-
const messages = [];
|
|
329
|
-
const seen = /* @__PURE__ */ new Set();
|
|
330
|
-
let current = error;
|
|
331
|
-
while (current instanceof Error && !seen.has(current)) {
|
|
332
|
-
seen.add(current);
|
|
333
|
-
const code = current.code;
|
|
334
|
-
const codeSuffix = typeof code === "string" ? ` (${code})` : "";
|
|
335
|
-
messages.push(`${current.message}${codeSuffix}`);
|
|
336
|
-
current = current.cause;
|
|
337
|
-
}
|
|
338
|
-
return messages.join(": ");
|
|
327
|
+
return error instanceof Error ? error.message : String(error);
|
|
339
328
|
}
|
|
340
329
|
//#endregion
|
|
341
330
|
export { createOpencodeExecutor, createOpencodeSessionRegistry };
|
|
@@ -164,6 +164,22 @@ Project-authored skill and rule paths resolve from the project root and must
|
|
|
164
164
|
exist for runtime use. If default skill files are missing, run `moka init` to
|
|
165
165
|
install them before executing workflows.
|
|
166
166
|
|
|
167
|
+
Default agent hooks are copied by `moka init` and `moka install-hooks` from the
|
|
168
|
+
private `oisin-ee/agent-hooks` repository. That source repository has one
|
|
169
|
+
canonical host-level layout:
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
claude-code/
|
|
173
|
+
codex/
|
|
174
|
+
opencode/
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Moka overlays those folders onto `.claude`, `.codex`, and `.opencode` for
|
|
178
|
+
project scope, or the corresponding per-machine host config directories for
|
|
179
|
+
global scope. Moka tracks installed hashes in host-local manifests so it can
|
|
180
|
+
update unchanged owned hook files, delete owned files removed from the hook
|
|
181
|
+
repository, and reject manual drift unless `--force` is used.
|
|
182
|
+
|
|
167
183
|
`moka init --skill-scope` (PIPE-83.12) chooses how the default set is installed:
|
|
168
184
|
`project` (default) vendors a repo-local copy (`skills add … --copy`,
|
|
169
185
|
`skills-lock.json`); `personal` installs once at user/global scope
|
|
@@ -198,6 +214,8 @@ OpenCode host resources are generated from the same profile registry:
|
|
|
198
214
|
resolved model, explicit permissions, and task access to generated agents only.
|
|
199
215
|
- `.opencode/skills/*/SKILL.md` is installed by `skills add`; Moka only
|
|
200
216
|
generates agents, commands, plugins, and project config.
|
|
217
|
+
- Additional manually authored OpenCode hook plugins can be copied from
|
|
218
|
+
`oisin-ee/agent-hooks/opencode/` by `moka install-hooks`.
|
|
201
219
|
- `.opencode/plugins/pipeline-goal-context.ts` projects package-owned
|
|
202
220
|
continuation context into OpenCode compaction.
|
|
203
221
|
- `.opencode/opencode.json` contains the gateway MCP config, enables LSP, and
|
package/docs/operator-guide.md
CHANGED
|
@@ -175,8 +175,8 @@ OpenBao, publish Secret values, or mutate ESO resources from this package.
|
|
|
175
175
|
|
|
176
176
|
`moka init`
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
Installs the package's default skills, generated host resources, and copied
|
|
179
|
+
agent hooks from the private `oisin-ee/agent-hooks` repository. OpenCode is the
|
|
180
180
|
package default runtime. `moka init` does not create repo-local `.pipeline`
|
|
181
181
|
config files.
|
|
182
182
|
|
|
@@ -196,11 +196,28 @@ moka install-commands --host all --force
|
|
|
196
196
|
|
|
197
197
|
Host choices are `all` and `opencode`.
|
|
198
198
|
|
|
199
|
+
`moka install-hooks`
|
|
200
|
+
|
|
201
|
+
Copies manually authored hook files from the private `oisin-ee/agent-hooks`
|
|
202
|
+
repository into the selected harness scope. The hook source repository has only
|
|
203
|
+
host folders: `claude-code/`, `codex/`, and `opencode/`. Install scope controls
|
|
204
|
+
the destination; it is not represented in the source repository.
|
|
205
|
+
|
|
206
|
+
```shell
|
|
207
|
+
moka install-hooks --scope global --check
|
|
208
|
+
moka install-hooks --scope project --force
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
There is no source override flag and no symlink mode. Moka clones
|
|
212
|
+
`oisin-ee/agent-hooks`, copies files, and tracks installed hashes so later runs
|
|
213
|
+
can update unchanged owned files, delete removed owned files, and refuse to
|
|
214
|
+
overwrite manually edited hook files unless `--force` is supplied.
|
|
215
|
+
|
|
199
216
|
`moka refresh-harnesses`
|
|
200
217
|
|
|
201
|
-
Force-refreshes package-owned skills
|
|
202
|
-
owned harness/resource paths, and commits them with
|
|
203
|
-
commit message is `chore: update agent harnesses`.
|
|
218
|
+
Force-refreshes package-owned skills, generated agent harnesses, and copied hook
|
|
219
|
+
files, stages only owned harness/resource paths, and commits them with
|
|
220
|
+
`--no-verify`. The default commit message is `chore: update agent harnesses`.
|
|
204
221
|
|
|
205
222
|
```shell
|
|
206
223
|
moka refresh-harnesses
|
|
@@ -392,6 +409,11 @@ Claude Code: /moka-quick, /moka-execute, /moka-inspect
|
|
|
392
409
|
- `.opencode/opencode.json` with LSP, the singleton `pipeline-gateway` MCP
|
|
393
410
|
server, and pinned package-selected plugins
|
|
394
411
|
|
|
412
|
+
`moka init` and `moka install-hooks` also copy hook files from
|
|
413
|
+
`oisin-ee/agent-hooks` by overlaying `opencode/`, `claude-code/`, and `codex/`
|
|
414
|
+
onto the selected host config roots. Hook files are authored in the hook repo,
|
|
415
|
+
not generated by Moka.
|
|
416
|
+
|
|
395
417
|
`moka install-commands --host claude-code` generates `.claude/commands/moka-<entrypoint>.md`
|
|
396
418
|
slash commands for Claude Code.
|
|
397
419
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
3
|
"@dagrejs/graphlib": "^4.0.1",
|
|
4
|
+
"rulesync": "8.30.1",
|
|
4
5
|
"@kubernetes/client-node": "^1.4.0",
|
|
5
6
|
"@opencode-ai/sdk": "1.17.4",
|
|
6
7
|
"ajv": "^8.20.0",
|
|
@@ -126,7 +127,7 @@
|
|
|
126
127
|
"prepack": "bun run build:cli"
|
|
127
128
|
},
|
|
128
129
|
"type": "module",
|
|
129
|
-
"version": "3.
|
|
130
|
+
"version": "3.9.0",
|
|
130
131
|
"description": "Config-driven multi-agent pipeline runner for repository work",
|
|
131
132
|
"main": "./dist/index.js",
|
|
132
133
|
"types": "./dist/index.d.ts",
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# Caveman Mode (default ON)
|
|
2
|
-
|
|
3
|
-
Operate in caveman mode by default on every response: terse, smart-caveman phrasing — drop filler, keep ALL technical substance, code, commands, paths, and accuracy. This is active every response and does not drift off over a long session. Defer to the `caveman` skill for the exact compression rules and intensity levels. Turn off only when the user says "stop caveman" or "normal mode".
|
|
4
|
-
|
|
5
|
-
# Global Behavior
|
|
6
|
-
|
|
7
|
-
- Answer fully and stop. Never end with "Want me to do X?" or "Should I implement this?" — if the user wants more, they'll ask.
|
|
8
|
-
- Research before acting — know how components and libraries work before making changes.
|
|
9
|
-
- When uncertain, ask — don't guess.
|
|
10
|
-
- The answer to "Why is X an improvement?" should never be "I'm not sure."
|
|
11
|
-
|
|
12
|
-
## Before writing code
|
|
13
|
-
|
|
14
|
-
- Search for existing implementations before creating new code.
|
|
15
|
-
- Check for existing utilities before adding helpers.
|
|
16
|
-
- Don't add dependencies without checking if functionality already exists in current deps.
|
|
17
|
-
- Reuse patterns from similar files in the codebase.
|
|
18
|
-
|
|
19
|
-
## Problem-solving
|
|
20
|
-
|
|
21
|
-
- Is this a real problem? Reject over-engineering.
|
|
22
|
-
- Is there a simpler way? Always seek the simplest solution.
|
|
23
|
-
- Will it break anything? Backward compatibility matters.
|
|
24
|
-
|
|
25
|
-
# Anti-Patterns
|
|
26
|
-
|
|
27
|
-
## Code quality
|
|
28
|
-
|
|
29
|
-
- NEVER manually edit auto-generated files — regenerate them instead.
|
|
30
|
-
- NEVER suppress type errors (`as any`, `@ts-ignore`, `@ts-expect-error`).
|
|
31
|
-
- NEVER use bandaids or hacks — proper fixes only.
|
|
32
|
-
- NEVER create new Zod schemas when generated ones exist.
|
|
33
|
-
- NEVER use `var` declarations.
|
|
34
|
-
- NEVER add `.unwrap()` calls in Rust code.
|
|
35
|
-
|
|
36
|
-
## Error handling
|
|
37
|
-
|
|
38
|
-
- Silent error handling is NEVER permitted.
|
|
39
|
-
- Every fallback and default value MUST have specific business-logic reasoning.
|
|
40
|
-
- Unexpected errors MUST be logged, not swallowed.
|
|
41
|
-
- Errors affecting user flow MUST surface to the user — never hide failures.
|
|
42
|
-
|
|
43
|
-
## Testing
|
|
44
|
-
|
|
45
|
-
- NEVER commit code without tests for new functionality.
|
|
46
|
-
- NEVER skip tests or mark them skipped to make CI pass.
|
|
47
|
-
- NEVER disable or delete existing tests — fix the code, not the tests.
|
|
48
|
-
- Test both success and error cases.
|
|
49
|
-
|
|
50
|
-
# Verification
|
|
51
|
-
|
|
52
|
-
- Run tests, lint, and typecheck after every change.
|
|
53
|
-
- Self-assessment is unreliable — use external signals (build output, test results) as ground truth.
|
|
54
|
-
- Don't claim something works without running it.
|
|
55
|
-
- If a test suite exists, run it. Don't skip it because "the change is small."
|
|
56
|
-
|
|
57
|
-
# Coding Style
|
|
58
|
-
|
|
59
|
-
- 120 char line width.
|
|
60
|
-
- Trailing commas everywhere.
|
|
61
|
-
|
|
62
|
-
## Git
|
|
63
|
-
|
|
64
|
-
- NEVER commit/push directly to main.
|
|
65
|
-
- NEVER amend commits or rewrite history after pushing.
|
|
66
|
-
- NEVER use `--force` without explicit approval.
|
|
67
|
-
- Always create new commits — never amend, squash, or rebase unless explicitly asked.
|
|
68
|
-
- Conventional commit format: `feat|fix|chore|docs|test|refactor(scope): description`.
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { resolvePackageAssetPath } from "../package-assets.js";
|
|
2
|
-
import { GENERATED_MARKER, INSTRUCTIONS_END, INSTRUCTIONS_START, OWNER_MARKER_PREFIX } from "./shared.js";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
//#region src/install-commands/instructions.ts
|
|
5
|
-
/**
|
|
6
|
-
* The canonical global agent instruction body, package-owned so every machine,
|
|
7
|
-
* k8s job, and host renders the same behavior. Shipped in `defaults/` (see
|
|
8
|
-
* package.json `files`).
|
|
9
|
-
*/
|
|
10
|
-
const INSTRUCTION_BODY = readFileSync(resolvePackageAssetPath("defaults/instructions/global.md"), "utf8").trimEnd();
|
|
11
|
-
/**
|
|
12
|
-
* The global instruction memory file each host reads, and the repo-relative
|
|
13
|
-
* path that `resolveHarnessTarget` rebases onto its per-machine config dir.
|
|
14
|
-
* Distinct from the bare `AGENTS.md` project guidance file, so these coexist
|
|
15
|
-
* with (and never trip) the PROJECT_ONLY AGENTS.md handling.
|
|
16
|
-
*/
|
|
17
|
-
const INSTRUCTION_TARGETS = [
|
|
18
|
-
{
|
|
19
|
-
host: "claude-code",
|
|
20
|
-
path: ".claude/CLAUDE.md"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
host: "codex",
|
|
24
|
-
path: ".codex/AGENTS.md"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
host: "gemini",
|
|
28
|
-
path: ".gemini/GEMINI.md"
|
|
29
|
-
}
|
|
30
|
-
];
|
|
31
|
-
/** Repo-relative paths of every generated instruction file (global scope). */
|
|
32
|
-
const INSTRUCTION_PATHS = INSTRUCTION_TARGETS.map((target) => target.path);
|
|
33
|
-
function instructionContent(host) {
|
|
34
|
-
return `${[
|
|
35
|
-
INSTRUCTIONS_START,
|
|
36
|
-
GENERATED_MARKER,
|
|
37
|
-
`${OWNER_MARKER_PREFIX}host=${host} -->`,
|
|
38
|
-
"",
|
|
39
|
-
INSTRUCTION_BODY,
|
|
40
|
-
"",
|
|
41
|
-
INSTRUCTIONS_END
|
|
42
|
-
].join("\n")}\n`;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Per-host global instruction definitions. Each is upserted as a marker block
|
|
46
|
-
* so any user-authored content outside the markers in the target file is
|
|
47
|
-
* preserved. Emitted in global scope only (see GLOBAL_ONLY_PATHS).
|
|
48
|
-
*/
|
|
49
|
-
function globalInstructionDefinitions() {
|
|
50
|
-
return INSTRUCTION_TARGETS.map(({ host, path }) => ({
|
|
51
|
-
block: {
|
|
52
|
-
end: INSTRUCTIONS_END,
|
|
53
|
-
start: INSTRUCTIONS_START
|
|
54
|
-
},
|
|
55
|
-
content: instructionContent(host),
|
|
56
|
-
host,
|
|
57
|
-
invocation: "(global instructions)",
|
|
58
|
-
path
|
|
59
|
-
}));
|
|
60
|
-
}
|
|
61
|
-
//#endregion
|
|
62
|
-
export { INSTRUCTION_PATHS, globalInstructionDefinitions };
|