@oisincoveney/pipeline 1.27.19 → 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/dist/config.d.ts CHANGED
@@ -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<{
@@ -511,7 +511,6 @@ declare const configSchema: z.ZodObject<{
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>;
package/dist/hooks.d.ts CHANGED
@@ -13,8 +13,8 @@ declare const hookResultSchema: z.ZodObject<{
13
13
  taskContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
14
  }, z.core.$strict>>;
15
15
  status: z.ZodEnum<{
16
- fail: "fail";
17
16
  pass: "pass";
17
+ fail: "fail";
18
18
  skip: "skip";
19
19
  }>;
20
20
  summary: z.ZodOptional<z.ZodString>;
@@ -163,6 +163,7 @@ function dispatchBlock(host, config, workflowId = config.default_workflow) {
163
163
  "Only package-configured gates are blocking. Do not invent RED, GREEN, full-suite, typecheck, or unrelated-drift gates.",
164
164
  "If a node returns targeted evidence and has no configured blocking gate, advance to the next node.",
165
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.",
166
167
  hostSpecificDispatchGuard(host, nativeRoutes, cliRoutes)
167
168
  ].filter((line) => Boolean(line)).join("\n");
168
169
  }
@@ -365,7 +366,7 @@ function opencodeDefinitions(config, cwd) {
365
366
  name: nativeAgentIdForHost("opencode", id),
366
367
  description: profile.description ?? id,
367
368
  hidden: false,
368
- mode: "subagent",
369
+ mode: "all",
369
370
  ...opencodeModelProjection(config, profile),
370
371
  permission: opencodePermission(profile)
371
372
  }, [
@@ -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 };
@@ -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
  ]
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.19",
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",