@oisincoveney/pipeline 1.27.18 → 1.28.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 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.
@@ -11,6 +11,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
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
+ }];
14
18
  const kubernetesNameSchema = z.string().min(1);
15
19
  const labelValueSchema = z.string().min(1);
16
20
  const stringMapSchema = z.record(z.string().min(1), z.string().min(1));
@@ -346,6 +350,7 @@ function runnerCommandTemplate(task, options, volumeMounts) {
346
350
  RUNNER_WORKFLOW_SCHEDULE_PATH
347
351
  ],
348
352
  command: ["moka"],
353
+ env: [...RUNNER_OPENCODE_ENV],
349
354
  image: options.image,
350
355
  imagePullPolicy: options.imagePullPolicy,
351
356
  name: "runner",
@@ -368,6 +373,7 @@ function runnerFinalizerTemplate(options, volumeMounts) {
368
373
  "{{workflow.status}}"
369
374
  ],
370
375
  command: ["moka"],
376
+ env: [...RUNNER_OPENCODE_ENV],
371
377
  image: options.image,
372
378
  imagePullPolicy: options.imagePullPolicy,
373
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.d.ts CHANGED
@@ -362,8 +362,8 @@ declare const configSchema: z.ZodObject<{
362
362
  policy: z.ZodOptional<z.ZodObject<{
363
363
  commands: z.ZodOptional<z.ZodEnum<{
364
364
  allow: "allow";
365
- deny: "deny";
366
365
  "trusted-only": "trusted-only";
366
+ deny: "deny";
367
367
  }>>;
368
368
  modules: z.ZodOptional<z.ZodEnum<{
369
369
  allow: "allow";
@@ -387,8 +387,8 @@ declare const configSchema: z.ZodObject<{
387
387
  }, z.core.$strict>>>;
388
388
  default_profile: z.ZodOptional<z.ZodString>;
389
389
  mode: z.ZodEnum<{
390
- local: "local";
391
390
  hosted: "hosted";
391
+ local: "local";
392
392
  }>;
393
393
  provider: z.ZodLiteral<"toolhive">;
394
394
  authorization_env: z.ZodDefault<z.ZodString>;
@@ -431,10 +431,10 @@ declare const configSchema: z.ZodObject<{
431
431
  }, z.core.$strict>>;
432
432
  output: z.ZodOptional<z.ZodObject<{
433
433
  format: z.ZodEnum<{
434
- json_schema: "json_schema";
435
434
  text: "text";
436
435
  json: "json";
437
436
  jsonl: "jsonl";
437
+ json_schema: "json_schema";
438
438
  }>;
439
439
  repair: z.ZodOptional<z.ZodObject<{
440
440
  enabled: z.ZodOptional<z.ZodBoolean>;
@@ -452,7 +452,6 @@ declare const configSchema: z.ZodObject<{
452
452
  skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
453
453
  timeout_ms: z.ZodOptional<z.ZodNumber>;
454
454
  tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
455
- task: "task";
456
455
  read: "read";
457
456
  list: "list";
458
457
  grep: "grep";
@@ -460,6 +459,7 @@ declare const configSchema: z.ZodObject<{
460
459
  bash: "bash";
461
460
  edit: "edit";
462
461
  write: "write";
462
+ task: "task";
463
463
  }>>>;
464
464
  }, z.core.$strict>>>;
465
465
  runner_command: z.ZodDefault<z.ZodObject<{
@@ -485,8 +485,8 @@ declare const configSchema: z.ZodObject<{
485
485
  rules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
486
486
  path: z.ZodString;
487
487
  source_root: z.ZodDefault<z.ZodEnum<{
488
- project: "project";
489
488
  package: "package";
489
+ project: "project";
490
490
  }>>;
491
491
  }, z.core.$strict>>>;
492
492
  runners: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -503,15 +503,14 @@ declare const configSchema: z.ZodObject<{
503
503
  disabled: "disabled";
504
504
  }>>>;
505
505
  output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
506
- json_schema: "json_schema";
507
506
  text: "text";
508
507
  json: "json";
509
508
  jsonl: "jsonl";
509
+ json_schema: "json_schema";
510
510
  }>>>;
511
511
  rules: z.ZodOptional<z.ZodBoolean>;
512
512
  skills: z.ZodOptional<z.ZodBoolean>;
513
513
  tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
514
- task: "task";
515
514
  read: "read";
516
515
  list: "list";
517
516
  grep: "grep";
@@ -519,6 +518,7 @@ declare const configSchema: z.ZodObject<{
519
518
  bash: "bash";
520
519
  edit: "edit";
521
520
  write: "write";
521
+ task: "task";
522
522
  }>>>;
523
523
  }, z.core.$strict>;
524
524
  command: z.ZodOptional<z.ZodString>;
@@ -613,8 +613,8 @@ declare const configSchema: z.ZodObject<{
613
613
  schedules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
614
614
  description: z.ZodOptional<z.ZodString>;
615
615
  baseline: z.ZodEnum<{
616
- quick: "quick";
617
616
  execute: "execute";
617
+ quick: "quick";
618
618
  }>;
619
619
  max_parallel_nodes: z.ZodOptional<z.ZodNumber>;
620
620
  node_catalog: z.ZodOptional<z.ZodString>;
@@ -626,8 +626,8 @@ declare const configSchema: z.ZodObject<{
626
626
  skills: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
627
627
  path: z.ZodString;
628
628
  source_root: z.ZodDefault<z.ZodEnum<{
629
- project: "project";
630
629
  package: "package";
630
+ project: "project";
631
631
  }>>;
632
632
  }, z.core.$strict>>>;
633
633
  task_context: z.ZodOptional<z.ZodObject<{
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,6 +4,7 @@ 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";
@@ -289,6 +290,15 @@ function createCliProgram() {
289
290
  });
290
291
  console.log(formatInstallCommandsResult(result));
291
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
+ });
292
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) => {
293
303
  const result = await runMokaSubmitFromCli(input, flags);
294
304
  console.log(`Workflow submitted: ${result.workflowName} in ${result.namespace}`);
@@ -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-";
@@ -162,6 +163,7 @@ function dispatchBlock(host, config, workflowId = config.default_workflow) {
162
163
  "Only package-configured gates are blocking. Do not invent RED, GREEN, full-suite, typecheck, or unrelated-drift gates.",
163
164
  "If a node returns targeted evidence and has no configured blocking gate, advance to the next node.",
164
165
  "Do not bypass configured runner subprocesses or package-configured gates when executing nodes.",
166
+ "Use the listed Task tool routes for native nodes, and run nodes with satisfied dependencies in parallel whenever the host supports concurrent subagent work.",
165
167
  hostSpecificDispatchGuard(host, nativeRoutes, cliRoutes)
166
168
  ].filter((line) => Boolean(line)).join("\n");
167
169
  }
@@ -334,7 +336,7 @@ function opencodeDefinitions(config, cwd) {
334
336
  ]).join("\n")),
335
337
  host: "opencode",
336
338
  invocation: invocationForHost("opencode", id),
337
- path: `.opencode/commands/${id}.md`
339
+ path: `.opencode/commands/${commandIdForHost("opencode", id)}.md`
338
340
  })),
339
341
  {
340
342
  content: renderOpenCodeProjectConfig(config),
@@ -364,7 +366,7 @@ function opencodeDefinitions(config, cwd) {
364
366
  name: nativeAgentIdForHost("opencode", id),
365
367
  description: profile.description ?? id,
366
368
  hidden: false,
367
- mode: "subagent",
369
+ mode: "all",
368
370
  ...opencodeModelProjection(config, profile),
369
371
  permission: opencodePermission(profile)
370
372
  }, [
@@ -401,7 +403,7 @@ function projectAgentsMdDefinition(cwd, host) {
401
403
  "",
402
404
  "This repository uses package-owned `@oisincoveney/pipeline` config.",
403
405
  "",
404
- "- Use `/quick`, `/execute`, or `/inspect` for OpenCode slash-command entrypoints when available.",
406
+ "- Use `/moka-quick`, `/moka-execute`, or `/moka-inspect` for OpenCode slash-command entrypoints when available.",
405
407
  "- Load and follow the relevant skill from `.agents/skills` before doing specialized work.",
406
408
  "- Prefer the package-defined pipeline profiles and generated command surfaces over ad hoc subagent prompts.",
407
409
  "",
@@ -485,7 +487,11 @@ function entrypointIdFromGeneratedPath(host, path) {
485
487
  }
486
488
  }
487
489
  function invocationForHost(host, entrypointId = "execute") {
488
- return `${{ opencode: "/" }[host]}${entrypointId} <task description>`;
490
+ return `${{ opencode: "/" }[host]}${commandIdForHost(host, entrypointId)} <task description>`;
491
+ }
492
+ function commandIdForHost(host, entrypointId) {
493
+ if (host === "opencode") return `${OPENCODE_COMMAND_PREFIX}${entrypointId}`;
494
+ return entrypointId;
489
495
  }
490
496
  function resolveDefinitionContent(definition, target) {
491
497
  if (definition.path !== OPENCODE_PROJECT_CONFIG_PATH || !existsSync(target)) return {
@@ -161,8 +161,8 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
161
161
  }, z.core.$strict>>;
162
162
  serviceAccountName: z.ZodOptional<z.ZodString>;
163
163
  mode: z.ZodEnum<{
164
- full: "full";
165
164
  quick: "quick";
165
+ full: "full";
166
166
  }>;
167
167
  schedulePath: z.ZodOptional<z.ZodString>;
168
168
  scheduleYaml: z.ZodOptional<z.ZodString>;
@@ -1,4 +1,4 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, 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";
@@ -102,18 +102,26 @@ async function configureGitCommitter(worktreePath, committer) {
102
102
  committer.email
103
103
  ]);
104
104
  }
105
- function runnerGitCommandArgs(args) {
106
- return [...gitCredentialConfigArgs(remoteUrlFromGitArgs(args)), ...args];
105
+ function runnerGitCommandArgs(args, remoteUrl) {
106
+ return [...gitCredentialConfigArgs(remoteUrl), ...args];
107
107
  }
108
108
  async function runGit(cwd, args) {
109
- const remoteUrl = remoteUrlFromGitArgs(args);
110
- const { stdout } = await execGit("git", runnerGitCommandArgs(args), {
109
+ const remoteUrl = await remoteUrlFromGitArgs(cwd, args);
110
+ assertSshCredentialsAvailable(remoteUrl);
111
+ const { stdout } = await execGit("git", runnerGitCommandArgs(args, remoteUrl), {
111
112
  cwd,
112
113
  encoding: "utf8",
113
114
  env: runnerGitEnv(remoteUrl)
114
115
  });
115
116
  return stdout;
116
117
  }
118
+ function assertSshCredentialsAvailable(remoteUrl) {
119
+ if (!(remoteUrl && isSshRemote(remoteUrl))) return;
120
+ const credentialsDir = gitCredentialsDir();
121
+ const missing = [["identity", resolve(credentialsDir, "identity")], ["known_hosts", resolve(credentialsDir, "known_hosts")]].filter(([, filePath]) => !existsSync(filePath)).map(([name]) => name);
122
+ if (missing.length === 0) return;
123
+ throw new Error(`SSH git remote ${remoteUrl} requires mounted git credential file(s): ${missing.join(", ")}`);
124
+ }
117
125
  function gitCredentialConfigArgs(remoteUrl) {
118
126
  const writablePath = prepareWritableGitCredentialStore(remoteUrl);
119
127
  if (!writablePath) return [];
@@ -125,6 +133,8 @@ function gitCredentialConfigArgs(remoteUrl) {
125
133
  ];
126
134
  }
127
135
  function prepareWritableGitCredentialStore(remoteUrl) {
136
+ if (!remoteUrl) return existingPreparedBasicAuthCredentialStore(writableGitCredentialStore());
137
+ if (!isHttpRemote(remoteUrl)) return;
128
138
  const writablePath = writableGitCredentialStore();
129
139
  const basicAuth = availableBasicAuthCredentials();
130
140
  if (basicAuth) return prepareBasicAuthCredentialStore(basicAuth, writablePath, remoteUrl);
@@ -172,10 +182,8 @@ function runnerGitEnv(remoteUrl) {
172
182
  ...process.env,
173
183
  GIT_TERMINAL_PROMPT: "0"
174
184
  };
175
- if (remoteUrl && isSshRemote(remoteUrl)) {
176
- const sshCommand = gitSshCommand();
177
- if (sshCommand) env.GIT_SSH_COMMAND = sshCommand;
178
- }
185
+ const sshCommand = gitSshCommand();
186
+ if (sshCommand && remoteUrl && isSshRemote(remoteUrl)) env.GIT_SSH_COMMAND = sshCommand;
179
187
  return env;
180
188
  }
181
189
  function gitSshCommand() {
@@ -183,7 +191,7 @@ function gitSshCommand() {
183
191
  const identityPath = resolve(credentialsDir, "identity");
184
192
  const knownHostsPath = resolve(credentialsDir, "known_hosts");
185
193
  if (!(existsSync(identityPath) && existsSync(knownHostsPath))) return;
186
- chmodSync(identityPath, 256);
194
+ ensureSshIdentityPermissions(identityPath);
187
195
  return [
188
196
  "ssh",
189
197
  "-i",
@@ -196,11 +204,63 @@ function gitSshCommand() {
196
204
  "StrictHostKeyChecking=yes"
197
205
  ].join(" ");
198
206
  }
207
+ function ensureSshIdentityPermissions(identityPath) {
208
+ try {
209
+ chmodSync(identityPath, 256);
210
+ return;
211
+ } catch (error) {
212
+ if (!(isReadOnlyFileSystemError(error) && isOwnerReadOnly(identityPath))) throw error;
213
+ }
214
+ }
215
+ function isReadOnlyFileSystemError(error) {
216
+ return error instanceof Error && "code" in error && error.code === "EROFS";
217
+ }
218
+ function isOwnerReadOnly(path) {
219
+ const permissions = statSync(path).mode.toString(8).slice(-3);
220
+ return [
221
+ "4",
222
+ "5",
223
+ "6",
224
+ "7"
225
+ ].includes(permissions[0] ?? "") && permissions.slice(1) === "00";
226
+ }
199
227
  function readCredentialFile(path) {
200
228
  return readFileSync(path, "utf8").trim();
201
229
  }
202
- function remoteUrlFromGitArgs(args) {
230
+ async function remoteUrlFromGitArgs(cwd, args) {
231
+ const literalRemoteUrl = literalRemoteUrlFromGitArgs(args);
232
+ if (literalRemoteUrl) return literalRemoteUrl;
233
+ const remoteName = remoteNameFromGitArgs(args);
234
+ if (!remoteName) return;
235
+ return await gitRemoteUrl(cwd, remoteName);
236
+ }
237
+ function literalRemoteUrlFromGitArgs(args) {
203
238
  if (args[0] === "clone") return args.find((arg) => isRemoteUrl(arg));
239
+ if (args[0] === "fetch" || args[0] === "push" || args[0] === "ls-remote") {
240
+ const remoteArg = args[1];
241
+ return remoteArg && isRemoteUrl(remoteArg) ? remoteArg : void 0;
242
+ }
243
+ }
244
+ function remoteNameFromGitArgs(args) {
245
+ if (args[0] === "fetch" || args[0] === "push" || args[0] === "ls-remote") {
246
+ const remoteArg = args[1];
247
+ if (remoteArg && !remoteArg.startsWith("-") && !isRemoteUrl(remoteArg)) return remoteArg;
248
+ }
249
+ }
250
+ async function gitRemoteUrl(cwd, remoteName) {
251
+ const { stdout } = await execGit("git", [
252
+ "remote",
253
+ "get-url",
254
+ remoteName
255
+ ], {
256
+ cwd,
257
+ encoding: "utf8",
258
+ env: {
259
+ ...process.env,
260
+ GIT_TERMINAL_PROMPT: "0"
261
+ }
262
+ });
263
+ return stdout.trim() || void 0;
204
264
  }
205
265
  function isRemoteUrl(value) {
206
266
  return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("ssh://") || isScpLikeSshRemote(value);
@@ -208,6 +268,9 @@ function isRemoteUrl(value) {
208
268
  function isSshRemote(value) {
209
269
  return value.startsWith("ssh://") || isScpLikeSshRemote(value);
210
270
  }
271
+ function isHttpRemote(value) {
272
+ return value.startsWith("http://") || value.startsWith("https://");
273
+ }
211
274
  function isScpLikeSshRemote(value) {
212
275
  return SCP_LIKE_SSH_REMOTE_RE.test(value);
213
276
  }
@@ -9,6 +9,7 @@ import { z } from "zod";
9
9
  import { readFileSync } from "node:fs";
10
10
  import { resolve } from "node:path";
11
11
  import { execa } from "execa";
12
+ import pino from "pino";
12
13
  //#region src/runner-command/run.ts
13
14
  const runnerCommandOptionsSchema = z.object({
14
15
  cwd: z.string().min(1).optional(),
@@ -17,6 +18,7 @@ const runnerCommandOptionsSchema = z.object({
17
18
  payloadFile: z.string().min(1),
18
19
  scheduleFile: z.string().min(1),
19
20
  stderr: z.custom((value) => isOutputStream(value)).optional(),
21
+ stdout: z.custom((value) => isOutputStream(value)).optional(),
20
22
  taskDescriptorFile: z.string().min(1).optional()
21
23
  }).strict();
22
24
  const EXIT_PASS = 0;
@@ -25,15 +27,36 @@ const EXIT_VALIDATION = 64;
25
27
  const EXIT_STARTUP = 70;
26
28
  async function runRunnerCommand(rawOptions = {}) {
27
29
  const parsedOptions = runnerCommandOptionsSchema.safeParse(rawOptions);
28
- const stderr = rawOptions.stderr ?? process.stderr;
30
+ const logger = createRunnerLogger({
31
+ stderr: isOutputStream(rawOptions.stderr) ? rawOptions.stderr : process.stderr,
32
+ stdout: isOutputStream(rawOptions.stdout) ? rawOptions.stdout : process.stdout
33
+ });
29
34
  if (!parsedOptions.success) {
30
- stderr.write(`${parsedOptions.error.message}\n`);
35
+ logger.error({
36
+ error: parsedOptions.error.message,
37
+ phase: "options.validate"
38
+ }, "runner options validation failed");
31
39
  return EXIT_VALIDATION;
32
40
  }
33
41
  const options = parsedOptions.data;
34
42
  try {
43
+ logger.info({
44
+ phase: "payload.load",
45
+ status: "start"
46
+ }, "payload.load start");
35
47
  const payload = parseRunnerCommandPayload(readFileSync(options.payloadFile, "utf8"));
36
48
  const descriptor = readRunnerTaskDescriptor(options.taskDescriptorFile ?? "/etc/pipeline/task.json");
49
+ logger.info({
50
+ nodeId: descriptor.nodeId,
51
+ phase: "payload.load",
52
+ runId: payload.run.id,
53
+ status: "finish",
54
+ workflowId: payload.workflow.id
55
+ }, "payload.load finish");
56
+ logger.info({
57
+ phase: "event.sink.configure",
58
+ status: "start"
59
+ }, "event.sink.configure start");
37
60
  const authToken = resolveRunnerEventSinkAuthToken({ authTokenFile: payload.events.authTokenFile });
38
61
  const sink = createRunnerEventSink({
39
62
  authHeader: payload.events.authHeader,
@@ -42,27 +65,86 @@ async function runRunnerCommand(rawOptions = {}) {
42
65
  runId: payload.run.id,
43
66
  url: payload.events.url
44
67
  });
68
+ logger.info({
69
+ phase: "event.sink.configure",
70
+ status: "finish"
71
+ }, "event.sink.configure finish");
72
+ logger.info({
73
+ hasProvidedCwd: Boolean(options.cwd),
74
+ phase: "git.workspace.prepare",
75
+ status: "start"
76
+ }, "git.workspace.prepare start");
45
77
  const worktreePath = await prepareRunnerGitWorkspace(payload, { cwd: options.cwd });
78
+ logger.info({
79
+ phase: "git.workspace.prepare",
80
+ status: "finish"
81
+ }, "git.workspace.prepare finish");
82
+ logger.info({
83
+ phase: "config.load",
84
+ status: "start"
85
+ }, "config.load start");
46
86
  const baseConfig = loadPipelineConfig(worktreePath, { allowMissingLintFileReferences: true });
87
+ logger.info({
88
+ phase: "config.load",
89
+ status: "finish"
90
+ }, "config.load finish");
91
+ logger.info({
92
+ phase: "schedule.compile",
93
+ status: "start"
94
+ }, "schedule.compile start");
47
95
  const compiled = compileScheduleArtifact(baseConfig, parseScheduleArtifact(readFileSync(options.scheduleFile, "utf8"), options.scheduleFile), worktreePath);
96
+ logger.info({
97
+ phase: "schedule.compile",
98
+ status: "finish",
99
+ workflowId: compiled.workflowId
100
+ }, "schedule.compile finish");
48
101
  if (payload.workflow.id !== compiled.workflowId) throw new Error(`Runner payload workflow '${payload.workflow.id}' does not match schedule workflow '${compiled.workflowId}'`);
49
102
  const node = findPlannedNode(compiled.plan.topologicalOrder, descriptor.nodeId);
50
103
  if (!node) throw new Error(`Argo task '${descriptor.nodeId}' is not declared in workflow '${compiled.workflowId}'`);
104
+ logger.info({
105
+ dependencyCount: node.needs.length,
106
+ nodeId: descriptor.nodeId,
107
+ phase: "dependency.merge",
108
+ status: "start"
109
+ }, "dependency.merge start");
51
110
  await mergeDependencyRefs({
52
111
  committer: compiled.config.runner_command.git.committer,
53
112
  dependencyNodeIds: node.needs,
54
113
  payload,
55
114
  worktreePath
56
115
  });
116
+ logger.info({
117
+ dependencyCount: node.needs.length,
118
+ nodeId: descriptor.nodeId,
119
+ phase: "dependency.merge",
120
+ status: "finish"
121
+ }, "dependency.merge finish");
122
+ logger.info({
123
+ commandCount: baseConfig.runner_command.environment.setup.length,
124
+ phase: "setup.commands",
125
+ status: "start"
126
+ }, "setup.commands start");
57
127
  await runSetupCommands(baseConfig.runner_command.environment.setup, {
58
128
  env: options.env ?? process.env,
129
+ logger,
59
130
  worktreePath
60
131
  });
132
+ logger.info({
133
+ commandCount: baseConfig.runner_command.environment.setup.length,
134
+ phase: "setup.commands",
135
+ status: "finish"
136
+ }, "setup.commands finish");
61
137
  sink.recordRunnerCommandPhase("task.start", `Starting ${descriptor.nodeId}`, {
62
138
  kind: node.kind,
63
139
  taskId: descriptor.nodeId,
64
140
  workflowId: payload.workflow.id
65
141
  });
142
+ logger.info({
143
+ kind: node.kind,
144
+ nodeId: descriptor.nodeId,
145
+ phase: "task.run",
146
+ status: "start"
147
+ }, "task.run start");
66
148
  const result = await runScheduledWorkflowTask({
67
149
  config: compiled.config,
68
150
  hookPolicy: payload.hookPolicy,
@@ -73,12 +155,29 @@ async function runRunnerCommand(rawOptions = {}) {
73
155
  workflowId: compiled.workflowId,
74
156
  worktreePath
75
157
  });
158
+ logger.info({
159
+ exitCode: result.exitCode,
160
+ nodeId: descriptor.nodeId,
161
+ phase: "task.run",
162
+ resultStatus: result.status,
163
+ status: "finish"
164
+ }, "task.run finish");
165
+ logger.info({
166
+ nodeId: descriptor.nodeId,
167
+ phase: "git.node-ref.push",
168
+ status: "start"
169
+ }, "git.node-ref.push start");
76
170
  await commitAndPushNodeRef({
77
171
  committer: compiled.config.runner_command.git.committer,
78
172
  nodeId: descriptor.nodeId,
79
173
  payload,
80
174
  worktreePath
81
175
  });
176
+ logger.info({
177
+ nodeId: descriptor.nodeId,
178
+ phase: "git.node-ref.push",
179
+ status: "finish"
180
+ }, "git.node-ref.push finish");
82
181
  sink.recordRunnerCommandPhase("task.finish", `Finished ${descriptor.nodeId}`, {
83
182
  evidence: result.evidence,
84
183
  exitCode: result.exitCode,
@@ -86,11 +185,14 @@ async function runRunnerCommand(rawOptions = {}) {
86
185
  taskId: descriptor.nodeId,
87
186
  workflowId: payload.workflow.id
88
187
  });
89
- await flushAndReport(sink, stderr);
188
+ await flushAndReport(sink, logger);
90
189
  return result.status === "passed" ? EXIT_PASS : EXIT_FAIL;
91
190
  } catch (error) {
92
191
  const message = error instanceof Error ? error.message : String(error);
93
- stderr.write(`${message}\n`);
192
+ logger.error({
193
+ error: message,
194
+ phase: "runner-command"
195
+ }, message);
94
196
  return error instanceof RunnerCommandPayloadValidationError || error instanceof z.ZodError ? EXIT_VALIDATION : EXIT_STARTUP;
95
197
  }
96
198
  }
@@ -102,12 +204,26 @@ function findPlannedNode(nodes, nodeId) {
102
204
  }
103
205
  }
104
206
  async function runSetupCommands(commands, options) {
105
- for (const command of commands) {
207
+ for (const [index, command] of commands.entries()) {
208
+ options.logger.info({
209
+ command: command.command,
210
+ index: index + 1,
211
+ phase: "setup.command",
212
+ status: "start"
213
+ }, "setup.command start");
106
214
  const result = await execa(command.command, command.args, {
107
215
  cwd: options.worktreePath,
108
216
  env: options.env,
109
217
  reject: false
110
218
  });
219
+ options.logger.info({
220
+ command: command.command,
221
+ exitCode: result.exitCode,
222
+ index: index + 1,
223
+ phase: "setup.command",
224
+ required: command.required,
225
+ status: "finish"
226
+ }, "setup.command finish");
111
227
  if (result.exitCode !== 0 && command.required) throw new Error(`runner setup command '${command.command}' failed with exit ${result.exitCode}`);
112
228
  }
113
229
  }
@@ -119,13 +235,52 @@ function runnerTaskText(task, worktreePath) {
119
235
  function isOutputStream(value) {
120
236
  return typeof value === "object" && value !== null && "write" in value && typeof value.write === "function";
121
237
  }
122
- async function flushAndReport(sink, stderr) {
238
+ async function flushAndReport(sink, logger) {
239
+ logger.info({
240
+ phase: "event.flush",
241
+ status: "start"
242
+ }, "event.flush start");
123
243
  try {
124
244
  await sink.flush();
245
+ logger.info({
246
+ phase: "event.flush",
247
+ status: "finish"
248
+ }, "event.flush finish");
125
249
  } catch (error) {
126
250
  const message = error instanceof Error ? error.message : String(error);
127
- stderr.write(`runner event flush failed: ${message}\n`);
251
+ logger.error({
252
+ error: message,
253
+ phase: "event.flush"
254
+ }, `runner event flush failed: ${message}`);
128
255
  }
129
256
  }
257
+ function createRunnerLogger(options) {
258
+ const streams = [{
259
+ level: "info",
260
+ stream: options.stdout
261
+ }, {
262
+ level: "error",
263
+ stream: options.stderr
264
+ }];
265
+ return pino({
266
+ base: void 0,
267
+ level: "info",
268
+ name: "moka-runner",
269
+ redact: {
270
+ censor: "[redacted]",
271
+ paths: [
272
+ "authToken",
273
+ "*.authToken",
274
+ "token",
275
+ "*.token",
276
+ "password",
277
+ "*.password",
278
+ "identity",
279
+ "*.identity"
280
+ ]
281
+ },
282
+ timestamp: pino.stdTimeFunctions.isoTime
283
+ }, pino.multistream(streams, { dedupe: true }));
284
+ }
130
285
  //#endregion
131
286
  export { runRunnerCommand };
@@ -43,8 +43,8 @@ declare const runnerDeliverySchema: z.ZodObject<{
43
43
  declare const mokaSubmissionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
44
44
  kind: z.ZodLiteral<"graph">;
45
45
  mode: z.ZodEnum<{
46
- full: "full";
47
46
  quick: "quick";
47
+ full: "full";
48
48
  }>;
49
49
  }, z.core.$strict>, z.ZodObject<{
50
50
  argv: z.ZodArray<z.ZodString>;
@@ -104,8 +104,8 @@ declare const runnerCommandPayloadSchema: z.ZodObject<{
104
104
  submission: z.ZodDefault<z.ZodDiscriminatedUnion<[z.ZodObject<{
105
105
  kind: z.ZodLiteral<"graph">;
106
106
  mode: z.ZodEnum<{
107
- full: "full";
108
107
  quick: "quick";
108
+ full: "full";
109
109
  }>;
110
110
  }, z.core.$strict>, z.ZodObject<{
111
111
  argv: z.ZodArray<z.ZodString>;
@@ -472,13 +472,33 @@ function executeBaselineWorkflow() {
472
472
  ],
473
473
  id: "verification",
474
474
  kind: "agent",
475
- needs: ["acceptance-review"],
475
+ needs: [
476
+ "mechanical-green-tests",
477
+ "mechanical-green-typecheck",
478
+ "mechanical-green-lint",
479
+ "mechanical-green-fallow"
480
+ ],
476
481
  profile: "moka-verifier"
477
482
  },
483
+ {
484
+ id: "code-quality-review",
485
+ kind: "agent",
486
+ needs: [
487
+ "mechanical-green-tests",
488
+ "mechanical-green-typecheck",
489
+ "mechanical-green-lint",
490
+ "mechanical-green-fallow"
491
+ ],
492
+ profile: "moka-thermo-nuclear-reviewer"
493
+ },
478
494
  {
479
495
  id: "learn",
480
496
  kind: "agent",
481
- needs: ["verification"],
497
+ needs: [
498
+ "acceptance-review",
499
+ "verification",
500
+ "code-quality-review"
501
+ ],
482
502
  profile: "moka-learner"
483
503
  }
484
504
  ]
@@ -287,13 +287,13 @@ Troubleshooting:
287
287
  Generated invocations include:
288
288
 
289
289
  ```text
290
- OpenCode: /quick, /execute, /inspect
290
+ OpenCode: /moka-quick, /moka-execute, /moka-inspect
291
291
  Codex: $quick, $execute, $inspect
292
292
  ```
293
293
 
294
294
  `moka init` and `moka install-commands --host opencode` generate:
295
295
 
296
- - `.opencode/commands/<entrypoint>.md` for `/quick`, `/execute`, and `/inspect`
296
+ - `.opencode/commands/moka-<entrypoint>.md` for `/moka-quick`, `/moka-execute`, and `/moka-inspect`
297
297
  - `.opencode/agents/*.md` for primary and subagent profiles with explicit
298
298
  `permission` maps, `task` grants, and denied ungranted tools
299
299
  - `.opencode/skills/*/SKILL.md` from `npx skills add`, not Moka generation
@@ -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
@@ -13,6 +13,7 @@
13
13
  "micromatch": "^4.0.8",
14
14
  "p-limit": "^7.3.0",
15
15
  "package-manager-detector": "^1.6.0",
16
+ "pino": "^10.3.1",
16
17
  "secure-json-parse": "^4.1.0",
17
18
  "simple-git": "^3.36.0",
18
19
  "xstate": "^5.31.1",
@@ -122,7 +123,7 @@
122
123
  "prepack": "bun run build:cli"
123
124
  },
124
125
  "type": "module",
125
- "version": "1.27.18",
126
+ "version": "1.28.0",
126
127
  "description": "Config-driven multi-agent pipeline runner for repository work",
127
128
  "main": "./dist/index.js",
128
129
  "types": "./dist/index.d.ts",