@oisincoveney/pipeline 1.27.19 → 1.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +2 -2
- package/dist/install-commands.js +2 -1
- package/dist/run-state/git-refs.js +74 -11
- package/dist/runner-command/run.js +175 -7
- package/dist/schedule-planner.js +22 -2
- package/package.json +2 -1
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/install-commands.js
CHANGED
|
@@ -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: "
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,30 @@ 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
|
+
logFailedTaskRun(logger, descriptor.nodeId, result);
|
|
166
|
+
logger.info({
|
|
167
|
+
nodeId: descriptor.nodeId,
|
|
168
|
+
phase: "git.node-ref.push",
|
|
169
|
+
status: "start"
|
|
170
|
+
}, "git.node-ref.push start");
|
|
76
171
|
await commitAndPushNodeRef({
|
|
77
172
|
committer: compiled.config.runner_command.git.committer,
|
|
78
173
|
nodeId: descriptor.nodeId,
|
|
79
174
|
payload,
|
|
80
175
|
worktreePath
|
|
81
176
|
});
|
|
177
|
+
logger.info({
|
|
178
|
+
nodeId: descriptor.nodeId,
|
|
179
|
+
phase: "git.node-ref.push",
|
|
180
|
+
status: "finish"
|
|
181
|
+
}, "git.node-ref.push finish");
|
|
82
182
|
sink.recordRunnerCommandPhase("task.finish", `Finished ${descriptor.nodeId}`, {
|
|
83
183
|
evidence: result.evidence,
|
|
84
184
|
exitCode: result.exitCode,
|
|
@@ -86,11 +186,14 @@ async function runRunnerCommand(rawOptions = {}) {
|
|
|
86
186
|
taskId: descriptor.nodeId,
|
|
87
187
|
workflowId: payload.workflow.id
|
|
88
188
|
});
|
|
89
|
-
await flushAndReport(sink,
|
|
189
|
+
await flushAndReport(sink, logger);
|
|
90
190
|
return result.status === "passed" ? EXIT_PASS : EXIT_FAIL;
|
|
91
191
|
} catch (error) {
|
|
92
192
|
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
-
|
|
193
|
+
logger.error({
|
|
194
|
+
error: message,
|
|
195
|
+
phase: "runner-command"
|
|
196
|
+
}, message);
|
|
94
197
|
return error instanceof RunnerCommandPayloadValidationError || error instanceof z.ZodError ? EXIT_VALIDATION : EXIT_STARTUP;
|
|
95
198
|
}
|
|
96
199
|
}
|
|
@@ -102,15 +205,41 @@ function findPlannedNode(nodes, nodeId) {
|
|
|
102
205
|
}
|
|
103
206
|
}
|
|
104
207
|
async function runSetupCommands(commands, options) {
|
|
105
|
-
for (const command of commands) {
|
|
208
|
+
for (const [index, command] of commands.entries()) {
|
|
209
|
+
options.logger.info({
|
|
210
|
+
command: command.command,
|
|
211
|
+
index: index + 1,
|
|
212
|
+
phase: "setup.command",
|
|
213
|
+
status: "start"
|
|
214
|
+
}, "setup.command start");
|
|
106
215
|
const result = await execa(command.command, command.args, {
|
|
107
216
|
cwd: options.worktreePath,
|
|
108
217
|
env: options.env,
|
|
109
218
|
reject: false
|
|
110
219
|
});
|
|
220
|
+
options.logger.info({
|
|
221
|
+
command: command.command,
|
|
222
|
+
exitCode: result.exitCode,
|
|
223
|
+
index: index + 1,
|
|
224
|
+
phase: "setup.command",
|
|
225
|
+
required: command.required,
|
|
226
|
+
status: "finish"
|
|
227
|
+
}, "setup.command finish");
|
|
111
228
|
if (result.exitCode !== 0 && command.required) throw new Error(`runner setup command '${command.command}' failed with exit ${result.exitCode}`);
|
|
112
229
|
}
|
|
113
230
|
}
|
|
231
|
+
function logFailedTaskRun(logger, nodeId, result) {
|
|
232
|
+
if (result.status === "passed" && result.exitCode === 0) return;
|
|
233
|
+
logger.error({
|
|
234
|
+
evidence: result.evidence,
|
|
235
|
+
exitCode: result.exitCode,
|
|
236
|
+
nodeId,
|
|
237
|
+
output: result.output,
|
|
238
|
+
phase: "task.run",
|
|
239
|
+
resultStatus: result.status,
|
|
240
|
+
status: "failed"
|
|
241
|
+
}, "task.run failed");
|
|
242
|
+
}
|
|
114
243
|
function runnerTaskText(task, worktreePath) {
|
|
115
244
|
if (task.kind === "prompt") return task.prompt;
|
|
116
245
|
if (task.path) return readFileSync(resolve(worktreePath, task.path), "utf8");
|
|
@@ -119,13 +248,52 @@ function runnerTaskText(task, worktreePath) {
|
|
|
119
248
|
function isOutputStream(value) {
|
|
120
249
|
return typeof value === "object" && value !== null && "write" in value && typeof value.write === "function";
|
|
121
250
|
}
|
|
122
|
-
async function flushAndReport(sink,
|
|
251
|
+
async function flushAndReport(sink, logger) {
|
|
252
|
+
logger.info({
|
|
253
|
+
phase: "event.flush",
|
|
254
|
+
status: "start"
|
|
255
|
+
}, "event.flush start");
|
|
123
256
|
try {
|
|
124
257
|
await sink.flush();
|
|
258
|
+
logger.info({
|
|
259
|
+
phase: "event.flush",
|
|
260
|
+
status: "finish"
|
|
261
|
+
}, "event.flush finish");
|
|
125
262
|
} catch (error) {
|
|
126
263
|
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
-
|
|
264
|
+
logger.error({
|
|
265
|
+
error: message,
|
|
266
|
+
phase: "event.flush"
|
|
267
|
+
}, `runner event flush failed: ${message}`);
|
|
128
268
|
}
|
|
129
269
|
}
|
|
270
|
+
function createRunnerLogger(options) {
|
|
271
|
+
const streams = [{
|
|
272
|
+
level: "info",
|
|
273
|
+
stream: options.stdout
|
|
274
|
+
}, {
|
|
275
|
+
level: "error",
|
|
276
|
+
stream: options.stderr
|
|
277
|
+
}];
|
|
278
|
+
return pino({
|
|
279
|
+
base: void 0,
|
|
280
|
+
level: "info",
|
|
281
|
+
name: "moka-runner",
|
|
282
|
+
redact: {
|
|
283
|
+
censor: "[redacted]",
|
|
284
|
+
paths: [
|
|
285
|
+
"authToken",
|
|
286
|
+
"*.authToken",
|
|
287
|
+
"token",
|
|
288
|
+
"*.token",
|
|
289
|
+
"password",
|
|
290
|
+
"*.password",
|
|
291
|
+
"identity",
|
|
292
|
+
"*.identity"
|
|
293
|
+
]
|
|
294
|
+
},
|
|
295
|
+
timestamp: pino.stdTimeFunctions.isoTime
|
|
296
|
+
}, pino.multistream(streams, { dedupe: true }));
|
|
297
|
+
}
|
|
130
298
|
//#endregion
|
|
131
299
|
export { runRunnerCommand };
|
package/dist/schedule-planner.js
CHANGED
|
@@ -472,13 +472,33 @@ function executeBaselineWorkflow() {
|
|
|
472
472
|
],
|
|
473
473
|
id: "verification",
|
|
474
474
|
kind: "agent",
|
|
475
|
-
needs: [
|
|
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: [
|
|
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.
|
|
126
|
+
"version": "1.28.1",
|
|
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",
|