@oisincoveney/pipeline 1.27.17 → 1.27.19

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
@@ -241,7 +241,7 @@ runner.
241
241
 
242
242
  | Host | Generated files | Invocation |
243
243
  | --- | --- | --- |
244
- | OpenCode | `.opencode/commands/<entrypoint>.md`, `.opencode/agents/*.md`, `.opencode/plugins/*.ts`, `.opencode/opencode.json` | `/quick <task>`, `/execute <task>`, `/inspect <task>` |
244
+ | OpenCode | `.opencode/commands/moka-<entrypoint>.md`, `.opencode/agents/*.md`, `.opencode/plugins/*.ts`, `.opencode/opencode.json` | `/moka-quick <task>`, `/moka-execute <task>`, `/moka-inspect <task>` |
245
245
 
246
246
  The installer is idempotent, supports `--check` and `--dry-run`, and refuses to
247
247
  overwrite manually edited files unless `--force` is supplied.
@@ -8,6 +8,7 @@ declare const submitRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
8
8
  eventAuthSecretKey: z.ZodOptional<z.ZodString>;
9
9
  eventAuthSecretName: z.ZodOptional<z.ZodString>;
10
10
  generateName: z.ZodOptional<z.ZodString>;
11
+ gitCredentialsSecretName: z.ZodOptional<z.ZodString>;
11
12
  githubAuthSecretName: z.ZodOptional<z.ZodString>;
12
13
  image: z.ZodOptional<z.ZodString>;
13
14
  imagePullPolicy: z.ZodOptional<z.ZodEnum<{
@@ -18,7 +19,7 @@ declare const submitRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
18
19
  imagePullSecretName: z.ZodOptional<z.ZodString>;
19
20
  kubeconfigPath: z.ZodOptional<z.ZodString>;
20
21
  name: z.ZodOptional<z.ZodString>;
21
- namespace: z.ZodDefault<z.ZodString>;
22
+ namespace: z.ZodString;
22
23
  opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
23
24
  payloadJson: z.ZodString;
24
25
  queueName: z.ZodOptional<z.ZodString>;
@@ -24,6 +24,7 @@ const submitRunnerArgoWorkflowOptionsSchema = z.object({
24
24
  eventAuthSecretKey: z.string().min(1).optional(),
25
25
  eventAuthSecretName: z.string().min(1).optional(),
26
26
  generateName: z.string().min(1).optional(),
27
+ gitCredentialsSecretName: z.string().min(1).optional(),
27
28
  githubAuthSecretName: z.string().min(1).optional(),
28
29
  image: z.string().min(1).optional(),
29
30
  imagePullPolicy: z.enum([
@@ -34,7 +35,7 @@ const submitRunnerArgoWorkflowOptionsSchema = z.object({
34
35
  imagePullSecretName: z.string().min(1).optional(),
35
36
  kubeconfigPath: z.string().min(1).optional(),
36
37
  name: z.string().min(1).optional(),
37
- namespace: z.string().min(1).default("momokaya-pipeline"),
38
+ namespace: z.string().min(1),
38
39
  opencodeAuthSecretName: z.string().min(1).optional(),
39
40
  payloadJson: z.string().min(1),
40
41
  queueName: z.string().min(1).optional(),
@@ -72,6 +73,7 @@ async function submitRunnerArgoWorkflow(rawOptions, dependencies = {}) {
72
73
  eventAuthSecretKey: options.eventAuthSecretKey,
73
74
  eventAuthSecretName: options.eventAuthSecretName,
74
75
  generateName: options.generateName,
76
+ gitCredentialsSecretName: options.gitCredentialsSecretName,
75
77
  githubAuthSecretName: options.githubAuthSecretName,
76
78
  image: options.image,
77
79
  imagePullPolicy: options.imagePullPolicy,
@@ -102,6 +102,7 @@ declare const runnerArgoWorkflowManifestSchema: z.ZodObject<{
102
102
  key: z.ZodString;
103
103
  path: z.ZodString;
104
104
  }, z.core.$strict>>>;
105
+ optional: z.ZodOptional<z.ZodBoolean>;
105
106
  secretName: z.ZodString;
106
107
  }, z.core.$strict>>;
107
108
  }, z.core.$strict>>;
@@ -112,6 +113,7 @@ declare const buildRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
112
113
  eventAuthSecretKey: z.ZodOptional<z.ZodString>;
113
114
  eventAuthSecretName: z.ZodOptional<z.ZodString>;
114
115
  generateName: z.ZodOptional<z.ZodString>;
116
+ gitCredentialsSecretName: z.ZodOptional<z.ZodString>;
115
117
  githubAuthSecretName: z.ZodOptional<z.ZodString>;
116
118
  annotations: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodString>>>;
117
119
  image: z.ZodDefault<z.ZodString>;
@@ -10,6 +10,11 @@ const RUNNER_WORKFLOW_SERVICE_ACCOUNT = "pipeline-runner";
10
10
  const RUNNER_WORKFLOW_ENTRYPOINT = "pipeline";
11
11
  const RUNNER_WORKFLOW_PAYLOAD_PATH = "/etc/pipeline/payload.json";
12
12
  const RUNNER_WORKFLOW_SCHEDULE_PATH = "/etc/pipeline/schedule.yaml";
13
+ const RUNNER_GIT_CREDENTIALS_PATH = "/etc/pipeline/git-credentials";
14
+ const RUNNER_OPENCODE_ENV = [{
15
+ name: "CODEX_AUTH_PER_PROJECT_ACCOUNTS",
16
+ value: "0"
17
+ }];
13
18
  const kubernetesNameSchema = z.string().min(1);
14
19
  const labelValueSchema = z.string().min(1);
15
20
  const stringMapSchema = z.record(z.string().min(1), z.string().min(1));
@@ -26,6 +31,7 @@ const secretVolumeSchema = z.object({
26
31
  key: z.string().min(1),
27
32
  path: z.string().min(1)
28
33
  }).strict()).optional(),
34
+ optional: z.boolean().optional(),
29
35
  secretName: kubernetesNameSchema
30
36
  }).strict();
31
37
  const argoWorkflowVolumeSchema = z.object({
@@ -110,6 +116,7 @@ const buildRunnerArgoWorkflowOptionsSchema = z.object({
110
116
  eventAuthSecretKey: z.string().min(1).optional(),
111
117
  eventAuthSecretName: kubernetesNameSchema.optional(),
112
118
  generateName: z.string().min(1).optional(),
119
+ gitCredentialsSecretName: kubernetesNameSchema.optional(),
113
120
  githubAuthSecretName: kubernetesNameSchema.optional(),
114
121
  annotations: z.record(z.string().min(1), z.string().min(1).optional()).default({}),
115
122
  image: z.string().min(1).default(RUNNER_WORKFLOW_IMAGE),
@@ -270,38 +277,51 @@ function runnerWorkflowStorage(options, tasks) {
270
277
  subPath: "auth.json"
271
278
  });
272
279
  }
273
- if (options.githubAuthSecretName) {
280
+ if (options.gitCredentialsSecretName) {
274
281
  volumes.push({
275
- name: "github-auth",
282
+ name: "runner-git-credentials",
276
283
  secret: {
284
+ defaultMode: 256,
277
285
  items: [
278
286
  {
279
- key: "gitconfig",
280
- path: "gitconfig"
287
+ key: "username",
288
+ path: "username"
281
289
  },
282
290
  {
283
- key: "git-credentials",
284
- path: "git-credentials"
291
+ key: "password",
292
+ path: "password"
285
293
  },
286
294
  {
287
- key: "hosts.yml",
288
- path: "hosts.yml"
295
+ key: "identity",
296
+ path: "identity"
297
+ },
298
+ {
299
+ key: "known_hosts",
300
+ path: "known_hosts"
289
301
  }
290
302
  ],
291
- secretName: options.githubAuthSecretName
303
+ optional: true,
304
+ secretName: options.gitCredentialsSecretName
292
305
  }
293
306
  });
294
307
  volumeMounts.push({
295
- mountPath: "/root/.gitconfig",
296
- name: "github-auth",
297
- readOnly: true,
298
- subPath: "gitconfig"
299
- }, {
300
- mountPath: "/root/.git-credentials",
308
+ mountPath: RUNNER_GIT_CREDENTIALS_PATH,
309
+ name: "runner-git-credentials",
310
+ readOnly: true
311
+ });
312
+ }
313
+ if (options.githubAuthSecretName) {
314
+ volumes.push({
301
315
  name: "github-auth",
302
- readOnly: true,
303
- subPath: "git-credentials"
304
- }, {
316
+ secret: {
317
+ items: [{
318
+ key: "hosts.yml",
319
+ path: "hosts.yml"
320
+ }],
321
+ secretName: options.githubAuthSecretName
322
+ }
323
+ });
324
+ volumeMounts.push({
305
325
  mountPath: "/root/.config/gh/hosts.yml",
306
326
  name: "github-auth",
307
327
  readOnly: true,
@@ -330,6 +350,7 @@ function runnerCommandTemplate(task, options, volumeMounts) {
330
350
  RUNNER_WORKFLOW_SCHEDULE_PATH
331
351
  ],
332
352
  command: ["moka"],
353
+ env: [...RUNNER_OPENCODE_ENV],
333
354
  image: options.image,
334
355
  imagePullPolicy: options.imagePullPolicy,
335
356
  name: "runner",
@@ -352,6 +373,7 @@ function runnerFinalizerTemplate(options, volumeMounts) {
352
373
  "{{workflow.status}}"
353
374
  ],
354
375
  command: ["moka"],
376
+ env: [...RUNNER_OPENCODE_ENV],
355
377
  image: options.image,
356
378
  imagePullPolicy: options.imagePullPolicy,
357
379
  name: "runner",
@@ -0,0 +1,98 @@
1
+ import { mergeOpenCodeProjectConfig } from "./opencode-project-config.js";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { parse } from "jsonc-parser";
6
+ //#region src/codex-auth-sync.ts
7
+ const CODEX_MULTI_AUTH_PLUGIN = "oc-codex-multi-auth";
8
+ const GLOBAL_CODEX_AUTH_CONFIG_PATH = join(homedir(), ".opencode/openai-codex-auth-config.json");
9
+ function syncLocalCodexAuth(options) {
10
+ const items = [syncGlobalPluginConfig(options.globalConfigPath ?? GLOBAL_CODEX_AUTH_CONFIG_PATH, options), ...discoverGitRepositories(options.root).map((repo) => syncProjectOpenCodeConfig(repo, options))];
11
+ const hasRequiredChanges = items.some((item) => item.action === "create" || item.action === "update");
12
+ const hasErrors = items.some((item) => item.action === "error");
13
+ const checkFailed = options.check === true && hasRequiredChanges;
14
+ return {
15
+ items,
16
+ ok: !(hasErrors || checkFailed)
17
+ };
18
+ }
19
+ function formatCodexAuthSyncResult(result) {
20
+ const lines = result.items.map((item) => {
21
+ const suffix = item.message ? `: ${item.message}` : "";
22
+ return `${item.action} ${item.path}${suffix}`;
23
+ });
24
+ if (!result.ok) lines.push("codex-auth sync-local check failed");
25
+ return lines.join("\n");
26
+ }
27
+ function syncGlobalPluginConfig(path, options) {
28
+ const currentText = existsSync(path) ? readFileSync(path, "utf8") : void 0;
29
+ const parsed = parseJsonObject(currentText ?? "{}");
30
+ if (!parsed.ok) return {
31
+ action: "error",
32
+ message: formatParseErrors(parsed.errors),
33
+ path
34
+ };
35
+ return writeIfChanged(path, currentText, `${JSON.stringify({
36
+ ...parsed.value,
37
+ perProjectAccounts: false
38
+ }, null, 2)}\n`, options);
39
+ }
40
+ function syncProjectOpenCodeConfig(repo, options) {
41
+ const path = join(repo, ".opencode/opencode.json");
42
+ const currentText = existsSync(path) ? readFileSync(path, "utf8") : void 0;
43
+ const merged = mergeOpenCodeProjectConfig(currentText, { plugin: [CODEX_MULTI_AUTH_PLUGIN] });
44
+ if (!merged.ok) return {
45
+ action: "error",
46
+ message: formatParseErrors(merged.errors),
47
+ path
48
+ };
49
+ return writeIfChanged(path, currentText, merged.content, options);
50
+ }
51
+ function writeIfChanged(path, currentText, nextText, options) {
52
+ if (currentText === nextText) return {
53
+ action: "unchanged",
54
+ path
55
+ };
56
+ const action = currentText === void 0 ? "create" : "update";
57
+ if (!(options.check || options.dryRun)) {
58
+ mkdirSync(dirname(path), { recursive: true });
59
+ writeFileSync(path, nextText);
60
+ }
61
+ return {
62
+ action,
63
+ path
64
+ };
65
+ }
66
+ function discoverGitRepositories(root) {
67
+ return readdirSync(root).map((name) => join(root, name)).filter((path) => isDirectory(path) && existsSync(join(path, ".git"))).sort((a, b) => a.localeCompare(b));
68
+ }
69
+ function isDirectory(path) {
70
+ try {
71
+ return statSync(path).isDirectory();
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+ function parseJsonObject(content) {
77
+ const errors = [];
78
+ const value = parse(content, errors, {
79
+ allowTrailingComma: true,
80
+ disallowComments: false
81
+ });
82
+ if (errors.length > 0 || !isRecord(value)) return {
83
+ errors,
84
+ ok: false
85
+ };
86
+ return {
87
+ ok: true,
88
+ value
89
+ };
90
+ }
91
+ function formatParseErrors(errors) {
92
+ return errors.length > 0 ? `invalid JSONC (${errors.length} parse error${errors.length === 1 ? "" : "s"})` : "expected a JSON object";
93
+ }
94
+ function isRecord(value) {
95
+ return typeof value === "object" && value !== null && !Array.isArray(value);
96
+ }
97
+ //#endregion
98
+ export { formatCodexAuthSyncResult, syncLocalCodexAuth };
package/dist/config.js CHANGED
@@ -315,7 +315,7 @@ hooks:
315
315
  runner_command:
316
316
  environment:
317
317
  setup:
318
- - command: pnpm
318
+ - command: bun
319
319
  args: [install, --frozen-lockfile]
320
320
  scheduler:
321
321
  commands:
package/dist/index.js CHANGED
@@ -4,12 +4,14 @@ import { PipelineConfigError, loadPipelineConfig } from "./config.js";
4
4
  import { createOrchestratorLaunchPlan, createRunnerLaunchPlan } from "./runner.js";
5
5
  import { compileWorkflowPlan } from "./workflow-planner.js";
6
6
  import { compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact } from "./schedule-planner.js";
7
+ import { formatCodexAuthSyncResult, syncLocalCodexAuth } from "./codex-auth-sync.js";
7
8
  import { BUILTIN_PIPE_COMMANDS, registerConfiguredEntrypointCommands } from "./commands/pipeline-command.js";
8
9
  import { configureGatewayHosts, localGatewayStatus, reconcileGateway, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "./mcp/gateway.js";
9
10
  import { resolvePackageAssetPath } from "./package-assets.js";
10
11
  import { formatConfigError, runPipelineFromConfig } from "./pipeline-runtime.js";
11
12
  import { registerRunnerCommandCommand } from "./commands/runner-command-command.js";
12
13
  import { formatInstallCommandsResult, installCommands, parseCommandHost } from "./install-commands.js";
14
+ import { loadMokaGlobalConfig } from "./moka-global-config.js";
13
15
  import { submitMoka } from "./moka-submit.js";
14
16
  import { formatPipelineInitResult, initPipelineProject } from "./pipeline-init.js";
15
17
  import { existsSync, readFileSync, realpathSync } from "node:fs";
@@ -288,6 +290,15 @@ function createCliProgram() {
288
290
  });
289
291
  console.log(formatInstallCommandsResult(result));
290
292
  });
293
+ 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) => {
294
+ const result = syncLocalCodexAuth({
295
+ check: flags.check,
296
+ dryRun: flags.dryRun,
297
+ root: resolve(flags.root ?? process.env.PIPELINE_TARGET_PATH ?? process.cwd())
298
+ });
299
+ console.log(formatCodexAuthSyncResult(result));
300
+ if (!result.ok) process.exitCode = 1;
301
+ });
291
302
  addMokaSubmitOptions(program.command("submit").description("Submit work to Momokaya as an Argo Workflow").argument("[input...]", "task description, or command argv with --command")).action(async (input, flags) => {
292
303
  const result = await runMokaSubmitFromCli(input, flags);
293
304
  console.log(`Workflow submitted: ${result.workflowName} in ${result.namespace}`);
@@ -313,31 +324,40 @@ function addMokaSubmitOptions(command) {
313
324
  }
314
325
  function runMokaSubmitFromCli(input, flags) {
315
326
  const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
327
+ const config = loadPipelineConfig(cwd, { allowMissingLintFileReferences: true });
328
+ const globalConfig = loadMokaGlobalConfig();
316
329
  const commonOptions = mokaCommonSubmitOptions({
317
- config: loadPipelineConfig(cwd, { allowMissingLintFileReferences: true }),
330
+ config,
318
331
  cwd,
319
- eventUrl: resolveMokaEventUrl(flags),
320
- flags
332
+ eventUrl: resolveMokaEventUrl(flags, globalConfig),
333
+ flags,
334
+ globalConfig
321
335
  });
322
336
  if (flags.command) return submitMokaCommandFromCli(input, flags, commonOptions);
323
337
  return submitMokaGraphFromCli(input, flags, commonOptions);
324
338
  }
325
- function resolveMokaEventUrl(flags) {
326
- return flags.eventUrl ?? process.env.PIPELINE_EVENT_URL ?? "https://pipeline-console.momokaya.ee/api/pipeline/runner-events";
339
+ function resolveMokaEventUrl(flags, globalConfig) {
340
+ return flags.eventUrl ?? globalConfig?.momokaya.submit.eventUrl;
327
341
  }
328
342
  function mokaCommonSubmitOptions(input) {
343
+ const momokaya = input.globalConfig?.momokaya;
329
344
  return {
330
345
  config: input.config,
331
346
  eventUrl: input.eventUrl,
347
+ eventAuthSecretKey: momokaya?.submit.eventAuthSecretKey,
348
+ eventAuthSecretName: momokaya?.submit.eventAuthSecretName,
332
349
  generateName: input.flags.generateName,
350
+ gitCredentialsSecretName: momokaya?.submit.gitCredentialsSecretName,
351
+ githubAuthSecretName: momokaya?.submit.githubAuthSecretName,
333
352
  image: input.flags.image,
334
353
  imagePullPolicy: parseImagePullPolicy(input.flags.imagePullPolicy),
335
- imagePullSecretName: input.flags.imagePullSecret,
336
- kubeconfigPath: input.flags.kubeconfig,
354
+ imagePullSecretName: input.flags.imagePullSecret ?? momokaya?.submit.imagePullSecretName,
355
+ kubeconfigPath: input.flags.kubeconfig ?? momokaya?.kubernetes.kubeconfig,
337
356
  name: input.flags.name,
338
- namespace: input.flags.namespace,
339
- queueName: input.flags.queueName,
340
- serviceAccountName: input.flags.serviceAccount,
357
+ namespace: input.flags.namespace ?? momokaya?.kubernetes.namespace,
358
+ opencodeAuthSecretName: momokaya?.submit.opencodeAuthSecretName,
359
+ queueName: input.flags.queueName ?? momokaya?.submit.queueName,
360
+ serviceAccountName: input.flags.serviceAccount ?? momokaya?.submit.serviceAccountName,
341
361
  worktreePath: input.cwd
342
362
  };
343
363
  }
@@ -377,7 +397,7 @@ function parseGatewayHostScope(value) {
377
397
  throw new Error("scope must be project or global");
378
398
  }
379
399
  function addRunnerArgoOptions(command, options = {}) {
380
- command.option("--name <name>", "Workflow metadata.name").option("--generate-name <prefix>", "Workflow metadata.generateName").option("--namespace <namespace>", "Workflow namespace", "momokaya-pipeline");
400
+ command.option("--name <name>", "Workflow metadata.name").option("--generate-name <prefix>", "Workflow metadata.generateName").option("--namespace <namespace>", "Workflow namespace");
381
401
  if (options.kubeconfig) command.option("--kubeconfig <path>", "kubeconfig path");
382
402
  return command.option("--queue-name <name>", "Kueue LocalQueue label for Workflow pods").option("--service-account <name>", "Workflow service account").option("--image <image>", "runner image").addOption(new Option("--image-pull-policy <policy>", "runner image pull policy").choices([
383
403
  "Always",
@@ -1,8 +1,8 @@
1
1
  import { DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST, loadPipelineConfig } from "./config.js";
2
2
  import { compileWorkflowPlan } from "./workflow-planner.js";
3
+ import { mergeOpenCodeProjectConfig } from "./opencode-project-config.js";
3
4
  import { renderOpenCodeGatewayConfig } from "./mcp/gateway.js";
4
5
  import { resolvePackageAssetPath } from "./package-assets.js";
5
- import { mergeOpenCodeProjectConfig } from "./opencode-project-config.js";
6
6
  import { existsSync, readFileSync, statSync } from "node:fs";
7
7
  import { basename, dirname, join, relative } from "node:path";
8
8
  import matter from "gray-matter";
@@ -18,7 +18,8 @@ const AGENTS_MD_START = "<!-- @oisincoveney/pipeline:agents:start -->";
18
18
  const AGENTS_MD_END = "<!-- @oisincoveney/pipeline:agents:end -->";
19
19
  const SINGLE_OPENCODE_PLUGIN_ARRAY_RE = /\n {2}"plugin": \[\n {4}("[^"]+")\n {2}\]/;
20
20
  const OPENCODE_PROJECT_CONFIG_PATH = ".opencode/opencode.json";
21
- const ENTRYPOINT_PATH_PATTERNS = { opencode: [/^\.opencode\/commands\/([^/]+)\.md$/] };
21
+ const OPENCODE_COMMAND_PREFIX = "moka-";
22
+ const ENTRYPOINT_PATH_PATTERNS = { opencode: [/^\.opencode\/commands\/(?:moka-)?([^/]+)\.md$/] };
22
23
  const COMMAND_HOSTS = ["opencode"];
23
24
  const OPENCODE_ORCHESTRATOR_AGENT_ID = "MoKa Orchestrator";
24
25
  const MOKA_PROFILE_PREFIX = "moka-";
@@ -172,7 +173,7 @@ function entrypointDispatchBlock(host, config, id, entrypoint) {
172
173
  `The schedule policy is \`${entrypoint.schedule}\`.`,
173
174
  id === "quick" ? "Run `moka submit --quick <task description>` to submit the graph as an Argo Workflow." : `Run \`moka submit <task description>\` to submit the \`${id}\` graph as an Argo Workflow.`,
174
175
  "The pipeline runtime executes as Argo DAG tasks using the package-owned runner image.",
175
- "Use `--kubeconfig <path>` and `--namespace <namespace>` to target a local or remote Kubernetes cluster.",
176
+ "Configure the target in `~/.config/moka/config.yaml`; use `--kubeconfig <path>` and `--namespace <namespace>` only for explicit command overrides.",
176
177
  "Use `moka submit --schedule <schedule.yaml> <task description>` only when rerunning an existing schedule artifact."
177
178
  ].join("\n");
178
179
  }
@@ -334,7 +335,7 @@ function opencodeDefinitions(config, cwd) {
334
335
  ]).join("\n")),
335
336
  host: "opencode",
336
337
  invocation: invocationForHost("opencode", id),
337
- path: `.opencode/commands/${id}.md`
338
+ path: `.opencode/commands/${commandIdForHost("opencode", id)}.md`
338
339
  })),
339
340
  {
340
341
  content: renderOpenCodeProjectConfig(config),
@@ -401,7 +402,7 @@ function projectAgentsMdDefinition(cwd, host) {
401
402
  "",
402
403
  "This repository uses package-owned `@oisincoveney/pipeline` config.",
403
404
  "",
404
- "- Use `/quick`, `/execute`, or `/inspect` for OpenCode slash-command entrypoints when available.",
405
+ "- Use `/moka-quick`, `/moka-execute`, or `/moka-inspect` for OpenCode slash-command entrypoints when available.",
405
406
  "- Load and follow the relevant skill from `.agents/skills` before doing specialized work.",
406
407
  "- Prefer the package-defined pipeline profiles and generated command surfaces over ad hoc subagent prompts.",
407
408
  "",
@@ -446,7 +447,8 @@ function selectedHosts(host) {
446
447
  const GENERATED_RESOURCE_ROOTS = { opencode: [
447
448
  ".opencode/commands",
448
449
  ".opencode/agents",
449
- ".opencode/plugins"
450
+ ".opencode/plugins",
451
+ ".opencode/skills"
450
452
  ] };
451
453
  async function listFiles(root) {
452
454
  if (!existsSync(root)) return [];
@@ -484,7 +486,11 @@ function entrypointIdFromGeneratedPath(host, path) {
484
486
  }
485
487
  }
486
488
  function invocationForHost(host, entrypointId = "execute") {
487
- return `${{ opencode: "/" }[host]}${entrypointId} <task description>`;
489
+ return `${{ opencode: "/" }[host]}${commandIdForHost(host, entrypointId)} <task description>`;
490
+ }
491
+ function commandIdForHost(host, entrypointId) {
492
+ if (host === "opencode") return `${OPENCODE_COMMAND_PREFIX}${entrypointId}`;
493
+ return entrypointId;
488
494
  }
489
495
  function resolveDefinitionContent(definition, target) {
490
496
  if (definition.path !== OPENCODE_PROJECT_CONFIG_PATH || !existsSync(target)) return {
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/moka-global-config.d.ts
4
+ declare const MOKA_GLOBAL_CONFIG_PATH = ".config/moka/config.yaml";
5
+ declare const mokaGlobalConfigSchema: z.ZodObject<{
6
+ momokaya: z.ZodObject<{
7
+ kubernetes: z.ZodObject<{
8
+ kubeconfig: z.ZodOptional<z.ZodString>;
9
+ namespace: z.ZodString;
10
+ }, z.core.$strict>;
11
+ submit: z.ZodObject<{
12
+ eventAuthSecretKey: z.ZodString;
13
+ eventAuthSecretName: z.ZodString;
14
+ eventUrl: z.ZodString;
15
+ gitCredentialsSecretName: z.ZodString;
16
+ githubAuthSecretName: z.ZodString;
17
+ imagePullSecretName: z.ZodString;
18
+ opencodeAuthSecretName: z.ZodString;
19
+ queueName: z.ZodString;
20
+ serviceAccountName: z.ZodString;
21
+ }, z.core.$strict>;
22
+ }, z.core.$strict>;
23
+ }, z.core.$strict>;
24
+ type MokaGlobalConfig = z.infer<typeof mokaGlobalConfigSchema>;
25
+ declare function mokaGlobalConfigPath(homeDir?: string): string;
26
+ declare function loadMokaGlobalConfig(): MokaGlobalConfig | null;
27
+ declare function parseMokaGlobalConfig(source: string, sourcePath: string): MokaGlobalConfig;
28
+ //#endregion
29
+ export { MOKA_GLOBAL_CONFIG_PATH, MokaGlobalConfig, loadMokaGlobalConfig, mokaGlobalConfigPath, mokaGlobalConfigSchema, parseMokaGlobalConfig };
@@ -0,0 +1,56 @@
1
+ import { PipelineConfigError } from "./config.js";
2
+ import { parseDocument } from "yaml";
3
+ import { z } from "zod";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { homedir } from "node:os";
7
+ //#region src/moka-global-config.ts
8
+ const MOKA_GLOBAL_CONFIG_PATH = ".config/moka/config.yaml";
9
+ const mokaSubmitGlobalConfigSchema = z.object({
10
+ eventAuthSecretKey: z.string().min(1),
11
+ eventAuthSecretName: z.string().min(1),
12
+ eventUrl: z.string().url(),
13
+ gitCredentialsSecretName: z.string().min(1),
14
+ githubAuthSecretName: z.string().min(1),
15
+ imagePullSecretName: z.string().min(1),
16
+ opencodeAuthSecretName: z.string().min(1),
17
+ queueName: z.string().min(1),
18
+ serviceAccountName: z.string().min(1)
19
+ }).strict();
20
+ const mokaKubernetesGlobalConfigSchema = z.object({
21
+ kubeconfig: z.string().min(1).optional(),
22
+ namespace: z.string().min(1)
23
+ }).strict();
24
+ const mokaGlobalConfigSchema = z.object({ momokaya: z.object({
25
+ kubernetes: mokaKubernetesGlobalConfigSchema,
26
+ submit: mokaSubmitGlobalConfigSchema
27
+ }).strict() }).strict();
28
+ function mokaGlobalConfigPath(homeDir = homedir()) {
29
+ return join(homeDir, MOKA_GLOBAL_CONFIG_PATH);
30
+ }
31
+ function loadMokaGlobalConfig() {
32
+ const configPath = mokaGlobalConfigPath();
33
+ if (!existsSync(configPath)) return null;
34
+ return parseMokaGlobalConfig(readFileSync(configPath, "utf8"), configPath);
35
+ }
36
+ function parseMokaGlobalConfig(source, sourcePath) {
37
+ const document = parseDocument(source, {
38
+ prettyErrors: false,
39
+ uniqueKeys: true
40
+ });
41
+ if (document.errors.length > 0) throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse ${sourcePath}`, document.errors.map((err) => ({
42
+ message: err.message,
43
+ path: sourcePath
44
+ })));
45
+ const parsed = mokaGlobalConfigSchema.safeParse(document.toJS());
46
+ if (!parsed.success) {
47
+ const issues = parsed.error.issues.map((issue) => ({
48
+ path: issue.path.join("."),
49
+ message: issue.message
50
+ }));
51
+ throw new PipelineConfigError("PIPELINE_CONFIG_VALIDATION_ERROR", [`Invalid ${sourcePath}:`, ...issues.map((issue) => issue.path ? `- ${issue.path}: ${issue.message}` : `- ${issue.message}`)].join("\n"), issues);
52
+ }
53
+ return parsed.data;
54
+ }
55
+ //#endregion
56
+ export { MOKA_GLOBAL_CONFIG_PATH, loadMokaGlobalConfig, mokaGlobalConfigPath, mokaGlobalConfigSchema, parseMokaGlobalConfig };
@@ -73,16 +73,17 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
73
73
  authTokenFile: z.ZodOptional<z.ZodString>;
74
74
  url: z.ZodString;
75
75
  }, z.core.$strict>>;
76
- eventAuthSecretKey: z.ZodDefault<z.ZodString>;
77
- eventAuthSecretName: z.ZodDefault<z.ZodString>;
78
- eventUrl: z.ZodDefault<z.ZodString>;
76
+ eventAuthSecretKey: z.ZodOptional<z.ZodString>;
77
+ eventAuthSecretName: z.ZodOptional<z.ZodString>;
78
+ eventUrl: z.ZodOptional<z.ZodString>;
79
79
  events: z.ZodOptional<z.ZodObject<{
80
80
  authHeader: z.ZodDefault<z.ZodString>;
81
81
  authTokenFile: z.ZodOptional<z.ZodString>;
82
82
  url: z.ZodString;
83
83
  }, z.core.$strict>>;
84
84
  generateName: z.ZodOptional<z.ZodString>;
85
- githubAuthSecretName: z.ZodDefault<z.ZodString>;
85
+ gitCredentialsSecretName: z.ZodOptional<z.ZodString>;
86
+ githubAuthSecretName: z.ZodOptional<z.ZodString>;
86
87
  hookPolicy: z.ZodOptional<z.ZodObject<{
87
88
  allowCommandHooks: z.ZodOptional<z.ZodBoolean>;
88
89
  allowUntrustedCommandHooks: z.ZodOptional<z.ZodBoolean>;
@@ -142,12 +143,12 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
142
143
  IfNotPresent: "IfNotPresent";
143
144
  Never: "Never";
144
145
  }>>;
145
- imagePullSecretName: z.ZodDefault<z.ZodString>;
146
+ imagePullSecretName: z.ZodOptional<z.ZodString>;
146
147
  kubeconfigPath: z.ZodOptional<z.ZodString>;
147
148
  name: z.ZodOptional<z.ZodString>;
148
- namespace: z.ZodDefault<z.ZodString>;
149
- opencodeAuthSecretName: z.ZodDefault<z.ZodString>;
150
- queueName: z.ZodDefault<z.ZodString>;
149
+ namespace: z.ZodOptional<z.ZodString>;
150
+ opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
151
+ queueName: z.ZodOptional<z.ZodString>;
151
152
  repository: z.ZodOptional<z.ZodObject<{
152
153
  baseBranch: z.ZodString;
153
154
  sha: z.ZodOptional<z.ZodString>;
@@ -158,7 +159,7 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
158
159
  project: z.ZodString;
159
160
  requestedBy: z.ZodOptional<z.ZodString>;
160
161
  }, z.core.$strict>>;
161
- serviceAccountName: z.ZodDefault<z.ZodString>;
162
+ serviceAccountName: z.ZodOptional<z.ZodString>;
162
163
  mode: z.ZodEnum<{
163
164
  quick: "quick";
164
165
  full: "full";
@@ -185,16 +186,17 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
185
186
  authTokenFile: z.ZodOptional<z.ZodString>;
186
187
  url: z.ZodString;
187
188
  }, z.core.$strict>>;
188
- eventAuthSecretKey: z.ZodDefault<z.ZodString>;
189
- eventAuthSecretName: z.ZodDefault<z.ZodString>;
190
- eventUrl: z.ZodDefault<z.ZodString>;
189
+ eventAuthSecretKey: z.ZodOptional<z.ZodString>;
190
+ eventAuthSecretName: z.ZodOptional<z.ZodString>;
191
+ eventUrl: z.ZodOptional<z.ZodString>;
191
192
  events: z.ZodOptional<z.ZodObject<{
192
193
  authHeader: z.ZodDefault<z.ZodString>;
193
194
  authTokenFile: z.ZodOptional<z.ZodString>;
194
195
  url: z.ZodString;
195
196
  }, z.core.$strict>>;
196
197
  generateName: z.ZodOptional<z.ZodString>;
197
- githubAuthSecretName: z.ZodDefault<z.ZodString>;
198
+ gitCredentialsSecretName: z.ZodOptional<z.ZodString>;
199
+ githubAuthSecretName: z.ZodOptional<z.ZodString>;
198
200
  hookPolicy: z.ZodOptional<z.ZodObject<{
199
201
  allowCommandHooks: z.ZodOptional<z.ZodBoolean>;
200
202
  allowUntrustedCommandHooks: z.ZodOptional<z.ZodBoolean>;
@@ -254,12 +256,12 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
254
256
  IfNotPresent: "IfNotPresent";
255
257
  Never: "Never";
256
258
  }>>;
257
- imagePullSecretName: z.ZodDefault<z.ZodString>;
259
+ imagePullSecretName: z.ZodOptional<z.ZodString>;
258
260
  kubeconfigPath: z.ZodOptional<z.ZodString>;
259
261
  name: z.ZodOptional<z.ZodString>;
260
- namespace: z.ZodDefault<z.ZodString>;
261
- opencodeAuthSecretName: z.ZodDefault<z.ZodString>;
262
- queueName: z.ZodDefault<z.ZodString>;
262
+ namespace: z.ZodOptional<z.ZodString>;
263
+ opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
264
+ queueName: z.ZodOptional<z.ZodString>;
263
265
  repository: z.ZodOptional<z.ZodObject<{
264
266
  baseBranch: z.ZodString;
265
267
  sha: z.ZodOptional<z.ZodString>;
@@ -270,7 +272,7 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
270
272
  project: z.ZodString;
271
273
  requestedBy: z.ZodOptional<z.ZodString>;
272
274
  }, z.core.$strict>>;
273
- serviceAccountName: z.ZodDefault<z.ZodString>;
275
+ serviceAccountName: z.ZodOptional<z.ZodString>;
274
276
  commandArgv: z.ZodArray<z.ZodString>;
275
277
  task: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
276
278
  kind: z.ZodLiteral<"prompt">;
@@ -315,13 +317,14 @@ interface MokaWorkflowSubmitOptions {
315
317
  eventAuthSecretKey?: string;
316
318
  eventAuthSecretName?: string;
317
319
  generateName?: string;
320
+ gitCredentialsSecretName?: string;
318
321
  githubAuthSecretName?: string;
319
322
  image?: string;
320
323
  imagePullPolicy?: "Always" | "IfNotPresent" | "Never";
321
324
  imagePullSecretName?: string;
322
325
  kubeconfigPath?: string;
323
326
  name?: string;
324
- namespace?: string;
327
+ namespace: string;
325
328
  opencodeAuthSecretName?: string;
326
329
  payloadJson: string;
327
330
  queueName?: string;
@@ -9,14 +9,6 @@ import parseGitUrl from "git-url-parse";
9
9
  import { resolve } from "node:path";
10
10
  import { simpleGit } from "simple-git";
11
11
  //#region src/moka-submit.ts
12
- const MOMOKAYA_EVENT_AUTH_SECRET_KEY = "OISIN_PIPELINE_EVENT_AUTH_TOKEN";
13
- const MOMOKAYA_EVENT_AUTH_SECRET_NAME = "pipeline-runner-event-auth";
14
- const MOMOKAYA_EVENT_URL = "https://pipeline-console.momokaya.ee/api/pipeline/runner-events";
15
- const MOMOKAYA_GITHUB_AUTH_SECRET_NAME = "oisin-bot-github-auth";
16
- const MOMOKAYA_IMAGE_PULL_SECRET_NAME = "ghcr-pull-secret";
17
- const MOMOKAYA_OPENCODE_AUTH_SECRET_NAME = "opencode-auth-1";
18
- const MOMOKAYA_QUEUE_NAME = "momokaya-pipeline";
19
- const MOMOKAYA_RUNNER_SERVICE_ACCOUNT_NAME = "pipeline-runner";
20
12
  const imagePullPolicySchema = z.enum([
21
13
  "Always",
22
14
  "IfNotPresent",
@@ -69,25 +61,26 @@ const mokaSubmitResultSchema = workflowSubmitResultSchema;
69
61
  const mokaSubmitBaseOptionsSchema = z.object({
70
62
  delivery: runnerDeliverySchema.default({ pullRequest: false }),
71
63
  eventSink: mokaSubmitEventsSchema.optional(),
72
- eventAuthSecretKey: z.string().min(1).default(MOMOKAYA_EVENT_AUTH_SECRET_KEY),
73
- eventAuthSecretName: z.string().min(1).default(MOMOKAYA_EVENT_AUTH_SECRET_NAME),
74
- eventUrl: z.string().url().default(MOMOKAYA_EVENT_URL),
64
+ eventAuthSecretKey: z.string().min(1).optional(),
65
+ eventAuthSecretName: z.string().min(1).optional(),
66
+ eventUrl: z.string().url().optional(),
75
67
  events: mokaSubmitEventsSchema.optional(),
76
68
  generateName: z.string().min(1).optional(),
77
- githubAuthSecretName: z.string().min(1).default(MOMOKAYA_GITHUB_AUTH_SECRET_NAME),
69
+ gitCredentialsSecretName: z.string().min(1).optional(),
70
+ githubAuthSecretName: z.string().min(1).optional(),
78
71
  hookPolicy: mokaSubmitHookPolicySchema.optional(),
79
72
  hooks: mokaSubmitDirectHooksSchema.optional(),
80
73
  image: z.string().min(1).optional(),
81
74
  imagePullPolicy: imagePullPolicySchema,
82
- imagePullSecretName: z.string().min(1).default(MOMOKAYA_IMAGE_PULL_SECRET_NAME),
75
+ imagePullSecretName: z.string().min(1).optional(),
83
76
  kubeconfigPath: z.string().min(1).optional(),
84
77
  name: z.string().min(1).optional(),
85
- namespace: z.string().min(1).default("momokaya-pipeline"),
86
- opencodeAuthSecretName: z.string().min(1).default(MOMOKAYA_OPENCODE_AUTH_SECRET_NAME),
87
- queueName: z.string().min(1).default(MOMOKAYA_QUEUE_NAME),
78
+ namespace: z.string().min(1).optional(),
79
+ opencodeAuthSecretName: z.string().min(1).optional(),
80
+ queueName: z.string().min(1).optional(),
88
81
  repository: runnerRepositoryContextSchema.optional(),
89
82
  run: runnerRunIdentitySchema.optional(),
90
- serviceAccountName: z.string().min(1).default(MOMOKAYA_RUNNER_SERVICE_ACCOUNT_NAME)
83
+ serviceAccountName: z.string().min(1).optional()
91
84
  }).strict();
92
85
  const mokaGraphSubmitOptionsSchema = mokaSubmitBaseOptionsSchema.extend({
93
86
  mode: z.enum(["full", "quick"]),
@@ -107,6 +100,11 @@ const mokaSubmitOptionsSchema = z.discriminatedUnion("type", [mokaGraphSubmitOpt
107
100
  message: "Choose either eventSink or events, not both",
108
101
  path: ["eventSink"]
109
102
  });
103
+ if (data.eventSink === void 0 && data.events === void 0 && data.eventUrl === void 0) ctx.addIssue({
104
+ code: "custom",
105
+ message: "eventUrl is required unless eventSink or events is provided",
106
+ path: ["eventUrl"]
107
+ });
110
108
  });
111
109
  const submitHookId = (event) => `moka-submit-${event.replaceAll(".", "-")}`;
112
110
  function objectWithoutUndefined(value) {
@@ -284,18 +282,23 @@ function workflowSubmitOptions(options) {
284
282
  return {
285
283
  eventAuthSecretKey: options.eventAuthSecretKey,
286
284
  eventAuthSecretName: options.eventAuthSecretName,
285
+ gitCredentialsSecretName: options.gitCredentialsSecretName,
287
286
  githubAuthSecretName: options.githubAuthSecretName,
288
287
  image: options.image,
289
288
  imagePullPolicy: options.imagePullPolicy,
290
289
  imagePullSecretName: options.imagePullSecretName,
291
290
  kubeconfigPath: options.kubeconfigPath,
292
291
  name: options.name,
293
- namespace: options.namespace,
292
+ namespace: requireSubmitOption(options.namespace, "namespace"),
294
293
  opencodeAuthSecretName: options.opencodeAuthSecretName,
295
294
  queueName: options.queueName,
296
295
  serviceAccountName: options.serviceAccountName
297
296
  };
298
297
  }
298
+ function requireSubmitOption(value, name) {
299
+ if (!value) throw new Error(`${name} is required for moka submit`);
300
+ return value;
301
+ }
299
302
  function runnerPayloadJson(input) {
300
303
  return JSON.stringify(buildRunnerCommandPayload({
301
304
  delivery: input.options.delivery,
@@ -323,6 +326,7 @@ function runnerEvents(options) {
323
326
  authTokenFile: eventSink.authTokenFile ?? eventAuthTokenFile(options),
324
327
  url: eventSink.url
325
328
  };
329
+ if (!options.eventUrl) throw new Error("eventUrl is required unless eventSink or events is provided");
326
330
  return {
327
331
  authHeader: "Authorization",
328
332
  authTokenFile: eventAuthTokenFile(options),
@@ -330,19 +334,23 @@ function runnerEvents(options) {
330
334
  };
331
335
  }
332
336
  function eventAuthTokenFile(options) {
337
+ if (!options.eventAuthSecretKey) throw new Error("eventAuthSecretKey is required unless eventSink.authTokenFile is provided");
333
338
  return `/etc/pipeline/event-auth/${options.eventAuthSecretKey}`;
334
339
  }
335
340
  async function resolveSubmissionContext(options, dependencies, runId) {
336
341
  const explicitContext = explicitSubmissionContext(options);
337
342
  if (explicitContext) return explicitContext;
338
343
  const git = await resolveRequiredGit(options, dependencies);
344
+ const repository = repositoryContext(options, git);
345
+ assertRepositoryCredentialConfiguration(options);
339
346
  return {
340
- repository: repositoryContext(options, git),
347
+ repository,
341
348
  run: runContext(options, git, runId)
342
349
  };
343
350
  }
344
351
  function explicitSubmissionContext(options) {
345
352
  if (!(options.repository && options.run)) return null;
353
+ assertRepositoryCredentialConfiguration(options);
346
354
  return {
347
355
  repository: options.repository,
348
356
  run: options.run
@@ -359,6 +367,9 @@ function repositoryContext(options, git) {
359
367
  url: git.url
360
368
  };
361
369
  }
370
+ function assertRepositoryCredentialConfiguration(options) {
371
+ if (!options.gitCredentialsSecretName) throw new Error("gitCredentialsSecretName is required for runner git clone, fetch, and push operations");
372
+ }
362
373
  function runContext(options, git, runId) {
363
374
  return options.run ?? {
364
375
  id: runId,
@@ -1,14 +1,15 @@
1
- import { chmodSync, copyFileSync, existsSync, mkdirSync } from "node:fs";
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { execFile } from "node:child_process";
5
5
  import { promisify } from "node:util";
6
6
  //#region src/run-state/git-refs.ts
7
7
  const DEFAULT_WORKSPACE_PATH = "/workspace";
8
- const DEFAULT_GIT_CREDENTIAL_STORE = "/root/.git-credentials";
8
+ const DEFAULT_GIT_CREDENTIALS_DIR = "/etc/pipeline/git-credentials";
9
9
  const WRITABLE_GIT_CREDENTIAL_STORE = resolve(tmpdir(), "pipeline-git-credentials");
10
+ const SCP_LIKE_SSH_REMOTE_RE = /^[^@\s]+@[^:\s]+:.+/u;
10
11
  const execGit = promisify(execFile);
11
- let preparedCredentialStore;
12
+ let preparedBasicAuthCredentialStore;
12
13
  function runnerGitRefs(payload, nodeId) {
13
14
  const prefix = `refs/heads/pipeline/runs/${payload.run.id}/${payload.workflow.id}`;
14
15
  return {
@@ -102,21 +103,19 @@ async function configureGitCommitter(worktreePath, committer) {
102
103
  ]);
103
104
  }
104
105
  function runnerGitCommandArgs(args) {
105
- return [...gitCredentialConfigArgs(), ...args];
106
+ return [...gitCredentialConfigArgs(remoteUrlFromGitArgs(args)), ...args];
106
107
  }
107
108
  async function runGit(cwd, args) {
109
+ const remoteUrl = remoteUrlFromGitArgs(args);
108
110
  const { stdout } = await execGit("git", runnerGitCommandArgs(args), {
109
111
  cwd,
110
112
  encoding: "utf8",
111
- env: {
112
- ...process.env,
113
- GIT_TERMINAL_PROMPT: "0"
114
- }
113
+ env: runnerGitEnv(remoteUrl)
115
114
  });
116
115
  return stdout;
117
116
  }
118
- function gitCredentialConfigArgs() {
119
- const writablePath = prepareWritableGitCredentialStore();
117
+ function gitCredentialConfigArgs(remoteUrl) {
118
+ const writablePath = prepareWritableGitCredentialStore(remoteUrl);
120
119
  if (!writablePath) return [];
121
120
  return [
122
121
  "-c",
@@ -125,26 +124,102 @@ function gitCredentialConfigArgs() {
125
124
  `credential.helper=store --file=${writablePath}`
126
125
  ];
127
126
  }
128
- function prepareWritableGitCredentialStore() {
129
- const sourcePath = availableGitCredentialStore();
130
- if (!sourcePath) return;
127
+ function prepareWritableGitCredentialStore(remoteUrl) {
131
128
  const writablePath = writableGitCredentialStore();
132
- copyGitCredentialStore(sourcePath, writablePath);
133
- return writablePath;
129
+ const basicAuth = availableBasicAuthCredentials();
130
+ if (basicAuth) return prepareBasicAuthCredentialStore(basicAuth, writablePath, remoteUrl);
131
+ }
132
+ function availableBasicAuthCredentials() {
133
+ const credentialsDir = gitCredentialsDir();
134
+ const usernamePath = resolve(credentialsDir, "username");
135
+ const passwordPath = resolve(credentialsDir, "password");
136
+ if (!(existsSync(usernamePath) && existsSync(passwordPath))) return;
137
+ return {
138
+ password: readCredentialFile(passwordPath),
139
+ username: readCredentialFile(usernamePath)
140
+ };
134
141
  }
135
- function availableGitCredentialStore() {
136
- const sourcePath = process.env.PIPELINE_GIT_CREDENTIAL_STORE ?? DEFAULT_GIT_CREDENTIAL_STORE;
137
- return existsSync(sourcePath) ? sourcePath : void 0;
142
+ function gitCredentialsDir() {
143
+ return process.env.PIPELINE_GIT_CREDENTIALS_DIR ?? DEFAULT_GIT_CREDENTIALS_DIR;
138
144
  }
139
145
  function writableGitCredentialStore() {
140
146
  return process.env.PIPELINE_WRITABLE_GIT_CREDENTIAL_STORE ?? WRITABLE_GIT_CREDENTIAL_STORE;
141
147
  }
142
- function copyGitCredentialStore(sourcePath, writablePath) {
143
- if (preparedCredentialStore === writablePath) return;
148
+ function writeGitCredentialStore(credentials, writablePath, host) {
144
149
  mkdirSync(dirname(writablePath), { recursive: true });
145
- copyFileSync(sourcePath, writablePath);
150
+ writeFileSync(writablePath, `https://${encodeURIComponent(credentials.username)}:${encodeURIComponent(credentials.password)}@${host}\n`, { mode: 384 });
146
151
  chmodSync(writablePath, 384);
147
- preparedCredentialStore = writablePath;
152
+ }
153
+ function prepareBasicAuthCredentialStore(credentials, writablePath, remoteUrl) {
154
+ const host = remoteUrl ? credentialHost(remoteUrl) : void 0;
155
+ if (!host) return existingPreparedBasicAuthCredentialStore(writablePath);
156
+ if (isPreparedBasicAuthCredentialStore(writablePath, host)) return writablePath;
157
+ writeGitCredentialStore(credentials, writablePath, host);
158
+ preparedBasicAuthCredentialStore = {
159
+ host,
160
+ path: writablePath
161
+ };
162
+ return writablePath;
163
+ }
164
+ function existingPreparedBasicAuthCredentialStore(writablePath) {
165
+ return preparedBasicAuthCredentialStore?.path === writablePath ? writablePath : void 0;
166
+ }
167
+ function isPreparedBasicAuthCredentialStore(writablePath, host) {
168
+ return preparedBasicAuthCredentialStore?.path === writablePath && preparedBasicAuthCredentialStore.host === host;
169
+ }
170
+ function runnerGitEnv(remoteUrl) {
171
+ const env = {
172
+ ...process.env,
173
+ GIT_TERMINAL_PROMPT: "0"
174
+ };
175
+ if (remoteUrl && isSshRemote(remoteUrl)) {
176
+ const sshCommand = gitSshCommand();
177
+ if (sshCommand) env.GIT_SSH_COMMAND = sshCommand;
178
+ }
179
+ return env;
180
+ }
181
+ function gitSshCommand() {
182
+ const credentialsDir = gitCredentialsDir();
183
+ const identityPath = resolve(credentialsDir, "identity");
184
+ const knownHostsPath = resolve(credentialsDir, "known_hosts");
185
+ if (!(existsSync(identityPath) && existsSync(knownHostsPath))) return;
186
+ chmodSync(identityPath, 256);
187
+ return [
188
+ "ssh",
189
+ "-i",
190
+ shellQuote(identityPath),
191
+ "-o",
192
+ "IdentitiesOnly=yes",
193
+ "-o",
194
+ `UserKnownHostsFile=${shellQuote(knownHostsPath)}`,
195
+ "-o",
196
+ "StrictHostKeyChecking=yes"
197
+ ].join(" ");
198
+ }
199
+ function readCredentialFile(path) {
200
+ return readFileSync(path, "utf8").trim();
201
+ }
202
+ function remoteUrlFromGitArgs(args) {
203
+ if (args[0] === "clone") return args.find((arg) => isRemoteUrl(arg));
204
+ }
205
+ function isRemoteUrl(value) {
206
+ return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("ssh://") || isScpLikeSshRemote(value);
207
+ }
208
+ function isSshRemote(value) {
209
+ return value.startsWith("ssh://") || isScpLikeSshRemote(value);
210
+ }
211
+ function isScpLikeSshRemote(value) {
212
+ return SCP_LIKE_SSH_REMOTE_RE.test(value);
213
+ }
214
+ function credentialHost(remoteUrl) {
215
+ try {
216
+ return new URL(remoteUrl).host || void 0;
217
+ } catch {
218
+ return;
219
+ }
220
+ }
221
+ function shellQuote(value) {
222
+ return `'${value.replaceAll("'", `'\\''`)}'`;
148
223
  }
149
224
  //#endregion
150
225
  export { commitAndPushNodeRef, mergeDependencyRefs, prepareRunnerGitWorkspace, promoteFinalRef };
@@ -129,11 +129,27 @@ description and current git context, creates payload/schedule ConfigMaps, and
129
129
  submits an Argo Workflow that runs the graph as DAG tasks using the package-owned
130
130
  runner image.
131
131
 
132
- `moka submit` uses the Momokaya default event sink unless overridden with
133
- `PIPELINE_EVENT_URL` or `--event-url`.
132
+ `moka submit` reads the private Momokaya target from
133
+ `~/.config/moka/config.yaml`.
134
+
135
+ ```yaml
136
+ momokaya:
137
+ kubernetes:
138
+ kubeconfig: /path/to/cluster.kubeconfig
139
+ namespace: <workflow-namespace>
140
+ submit:
141
+ eventAuthSecretKey: <event-auth-secret-key>
142
+ eventAuthSecretName: <event-auth-secret-name>
143
+ eventUrl: <runner-event-sink-url>
144
+ gitCredentialsSecretName: <git-credentials-secret-name>
145
+ githubAuthSecretName: <github-auth-secret-name>
146
+ imagePullSecretName: <image-pull-secret-name>
147
+ opencodeAuthSecretName: <opencode-auth-secret-name>
148
+ queueName: <local-queue-name>
149
+ serviceAccountName: <runner-service-account-name>
150
+ ```
134
151
 
135
152
  ```shell
136
- export PIPELINE_EVENT_URL="https://console.example.com/api/pipeline/runner-events"
137
153
  moka submit "fix the login bug" --quick
138
154
  moka submit "Implement PIPE-54"
139
155
  ```
@@ -142,7 +158,7 @@ For a local cluster, point the same commands at that cluster with
142
158
  `--kubeconfig <path>` and `--namespace <namespace>`:
143
159
 
144
160
  ```shell
145
- moka submit "fix the login bug" --quick --kubeconfig ~/.kube/config --namespace momokaya-pipeline
161
+ moka submit "fix the login bug" --quick --kubeconfig ~/.kube/config --namespace <workflow-namespace>
146
162
  ```
147
163
 
148
164
  There is no separate workstation-local `submit` path; local submission means
@@ -169,7 +185,7 @@ apiVersion: argoproj.io/v1alpha1
169
185
  kind: Workflow
170
186
  metadata:
171
187
  generateName: pipeline-run-alpha-
172
- namespace: momokaya-pipeline
188
+ namespace: <workflow-namespace>
173
189
  spec:
174
190
  entrypoint: pipeline
175
191
  onExit: pipeline-finalizer
@@ -211,17 +227,28 @@ Secret volume at the path configured in `events.authTokenFile`.
211
227
 
212
228
  Expected namespace resources:
213
229
 
214
- - ServiceAccount `pipeline-runner` with the required RBAC
215
- - Secret `opencode-auth-1` with key `auth.json`
216
- - Secret `pipeline-runner-event-auth` with key
217
- `OISIN_PIPELINE_EVENT_AUTH_TOKEN`
218
- - Secret `oisin-bot-github-auth` with keys `gitconfig`, `git-credentials`, and
219
- `hosts.yml`
230
+ - The ServiceAccount named by `submit.serviceAccountName` with the required RBAC
231
+ - The OpenCode auth Secret named by `submit.opencodeAuthSecretName` with key
232
+ `auth.json`
233
+ - The event auth Secret named by `submit.eventAuthSecretName` with the key named
234
+ by `submit.eventAuthSecretKey`
235
+ - The git credentials Secret named by `submit.gitCredentialsSecretName` using
236
+ the same key conventions as Flux `GitRepository` credentials: `username` and
237
+ `password` for HTTPS remotes, or `identity` and `known_hosts` for SSH remotes
238
+ - The GitHub CLI auth Secret named by `submit.githubAuthSecretName` with key
239
+ `hosts.yml`; this Secret is for `gh` and pull request delivery, not git
240
+ clone/fetch/push authentication
220
241
  - A pipeline-console event sink reachable from the pod
221
242
 
222
- Credential rotation is owned by the infra repository scripts. `moka submit`
223
- references the managed Momokaya Secret names; it does not accept per-run auth
224
- Secret overrides.
243
+ Credential issuance and rotation are owned by the cluster/infra layer, not by
244
+ runner payloads; existing infra repository scripts can continue to own the
245
+ operator-facing lifecycle. Recommended production setups use External Secrets
246
+ Operator or Secrets Store CSI Driver. For GitHub HTTPS, prefer a GitHub App
247
+ installation token materialized by External Secrets Operator's
248
+ `GithubAccessToken` generator with a refresh interval below the token lifetime,
249
+ then template it as `username`/`password`. For SSH, materialize `identity` and
250
+ `known_hosts` from the external secret manager. `moka submit` references
251
+ configured Secret names; it does not accept per-run secret values.
225
252
 
226
253
  ## Payload Contract
227
254
 
@@ -260,13 +287,13 @@ Troubleshooting:
260
287
  Generated invocations include:
261
288
 
262
289
  ```text
263
- OpenCode: /quick, /execute, /inspect
290
+ OpenCode: /moka-quick, /moka-execute, /moka-inspect
264
291
  Codex: $quick, $execute, $inspect
265
292
  ```
266
293
 
267
294
  `moka init` and `moka install-commands --host opencode` generate:
268
295
 
269
- - `.opencode/commands/<entrypoint>.md` for `/quick`, `/execute`, and `/inspect`
296
+ - `.opencode/commands/moka-<entrypoint>.md` for `/moka-quick`, `/moka-execute`, and `/moka-inspect`
270
297
  - `.opencode/agents/*.md` for primary and subagent profiles with explicit
271
298
  `permission` maps, `task` grants, and denied ungranted tools
272
299
  - `.opencode/skills/*/SKILL.md` from `npx skills add`, not Moka generation
@@ -43,7 +43,7 @@ validation, tests, and docs.
43
43
  "events": {
44
44
  "url": "https://console.example/api/pipeline/runner-events",
45
45
  "authHeader": "Authorization",
46
- "authTokenFile": "/etc/pipeline/event-auth/OISIN_PIPELINE_EVENT_AUTH_TOKEN"
46
+ "authTokenFile": "/etc/pipeline/event-auth/<event-auth-secret-key>"
47
47
  }
48
48
  }
49
49
  ```
@@ -160,11 +160,16 @@ environment and mounted secrets.
160
160
 
161
161
  Expected runner Job secrets and mounts include:
162
162
 
163
- - `opencode-auth-1` mounted at `/root/.local/share/opencode/auth.json`
164
- - `pipeline-runner-event-auth` mounted at
165
- `/etc/pipeline/event-auth/OISIN_PIPELINE_EVENT_AUTH_TOKEN`
163
+ - The configured OpenCode auth Secret mounted at
164
+ `/root/.local/share/opencode/auth.json`
165
+ - The configured event auth Secret mounted at
166
+ `/etc/pipeline/event-auth/<event-auth-secret-key>`
166
167
  - MCP gateway auth Secret mounted at a path referenced by the runner
167
- - `oisin-bot-github-auth` mounted for GitHub auth usable by both `git` and `gh`
168
+ - The configured git credentials Secret mounted for `git`; HTTPS remotes use
169
+ `username` and `password`, and SSH remotes use `identity` and `known_hosts`
170
+ following Flux `GitRepository` credential conventions
171
+ - The configured GitHub CLI auth Secret mounted for `gh` when pull request
172
+ delivery is enabled
168
173
 
169
174
  The runner image sets `HOME=/root`; Kubernetes must project each numbered auth
170
175
  Secret with the key `auth.json` into the target directory. The runner does not
@@ -172,6 +177,12 @@ read event auth tokens or payloads from environment variables. No
172
177
  `OISIN_PIPELINE_RUNNER_PAYLOAD_JSON`, `PIPELINE_EVENT_API_TOKEN`, or
173
178
  `OPENCODE_AUTH_JSON` env vars are used.
174
179
 
180
+ Git credential issuance and rotation are cluster concerns. For GitHub HTTPS,
181
+ operators should prefer GitHub App installation tokens generated by External
182
+ Secrets Operator or equivalent control-plane automation. For SSH, operators
183
+ must provide both the private identity and pinned `known_hosts` data. Runner
184
+ payloads never carry credential values or Secret names.
185
+
175
186
  ## Boundary
176
187
 
177
188
  The `moka` command is its own user-facing command and runtime. Argo
@@ -15,7 +15,7 @@ moka install-commands --host all --check
15
15
 
16
16
  | Host | Generated resources | Invocation | Mechanical path |
17
17
  | -------- | -------------------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
18
- | OpenCode | `.opencode/commands/<entrypoint>.md`, `.opencode/agents/*.md`, `.opencode/opencode.json` | `/quick <task>`, `/execute <task>`, `/inspect <task>` | Project commands run a primary orchestrator and OpenCode native subagents with package-owned skill, MCP, permission, and LSP projection. |
18
+ | OpenCode | `.opencode/commands/moka-<entrypoint>.md`, `.opencode/agents/*.md`, `.opencode/opencode.json` | `/moka-quick <task>`, `/moka-execute <task>`, `/moka-inspect <task>` | Project commands run a primary orchestrator and OpenCode native subagents with package-owned skill, MCP, permission, and LSP projection. |
19
19
 
20
20
  ## Projection Rules
21
21
 
package/package.json CHANGED
@@ -70,6 +70,10 @@
70
70
  "types": "./dist/moka-submit.d.ts",
71
71
  "import": "./dist/moka-submit.js"
72
72
  },
73
+ "./moka-global-config": {
74
+ "types": "./dist/moka-global-config.d.ts",
75
+ "import": "./dist/moka-global-config.js"
76
+ },
73
77
  "./planner": {
74
78
  "types": "./dist/workflow-planner.d.ts",
75
79
  "import": "./dist/workflow-planner.js"
@@ -118,7 +122,7 @@
118
122
  "prepack": "bun run build:cli"
119
123
  },
120
124
  "type": "module",
121
- "version": "1.27.17",
125
+ "version": "1.27.19",
122
126
  "description": "Config-driven multi-agent pipeline runner for repository work",
123
127
  "main": "./dist/index.js",
124
128
  "types": "./dist/index.d.ts",