@oisincoveney/pipeline 2.7.0 → 2.8.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/defaults/pipeline.yaml +25 -0
- package/dist/argo-graph.js +7 -7
- package/dist/argo-submit.d.ts +2 -4
- package/dist/argo-submit.js +80 -80
- package/dist/bench/eval-report.js +27 -0
- package/dist/cli/program.js +19 -3
- package/dist/cluster-doctor.js +89 -101
- package/dist/commands/bench-command.js +18 -0
- package/dist/config/defaults.js +9 -19
- package/dist/config/load.js +47 -37
- package/dist/config/schemas.d.ts +24 -7
- package/dist/config/schemas.js +20 -7
- package/dist/context/repo-map.js +203 -0
- package/dist/install-commands/opencode.js +10 -1
- package/dist/mcp/gateway-error.js +15 -0
- package/dist/mcp/gateway.js +119 -220
- package/dist/moka-global-config.js +20 -20
- package/dist/moka-submit.d.ts +6 -6
- package/dist/pipeline-init.js +18 -12
- package/dist/pipeline-runtime.js +592 -372
- package/dist/planning/compile.d.ts +8 -3
- package/dist/planning/compile.js +7 -7
- package/dist/planning/generate.d.ts +6 -1
- package/dist/planning/generate.js +29 -7
- package/dist/run-state/git-refs.js +124 -94
- package/dist/runner-command-contract.d.ts +6 -1
- package/dist/runner-command-contract.js +6 -5
- package/dist/runner-event-schema.d.ts +6 -6
- package/dist/runner-event-sink.js +37 -68
- package/dist/runner.d.ts +6 -1
- package/dist/runner.js +3 -3
- package/dist/runtime/agent-node/agent-node.js +218 -159
- package/dist/runtime/changed-files/changed-files.js +15 -27
- package/dist/runtime/changed-files/index.js +2 -0
- package/dist/runtime/drain-merge/drain-merge.js +124 -82
- package/dist/runtime/gates/gates.js +45 -27
- package/dist/runtime/hooks/hooks.js +74 -29
- package/dist/runtime/local-scheduler.js +45 -0
- package/dist/runtime/opencode-server.js +32 -23
- package/dist/runtime/opencode-session-executor.js +101 -44
- package/dist/runtime/parallel-node/parallel-node.js +93 -75
- package/dist/runtime/parallel-worktrees/parallel-worktrees.js +49 -4
- package/dist/runtime/run-journal.js +21 -0
- package/dist/runtime/scheduler.js +122 -93
- package/dist/runtime/select-candidate/select-candidate.js +52 -24
- package/dist/runtime/services/agent-node-runtime-service.js +15 -0
- package/dist/runtime/services/command-executor-service.js +8 -0
- package/dist/runtime/services/config-io-service.js +42 -0
- package/dist/runtime/services/drain-merge-git-service.js +10 -0
- package/dist/runtime/services/git-porcelain-service.js +38 -0
- package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
- package/dist/runtime/services/kubernetes-argo-service.js +81 -0
- package/dist/runtime/services/mcp-gateway-service.js +184 -0
- package/dist/runtime/services/opencode-sdk-service.js +27 -0
- package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
- package/dist/runtime/services/select-candidate-service.js +13 -0
- package/dist/runtime/services/worktree-service.js +18 -0
- package/dist/schedule/passes/candidates.js +17 -8
- package/docs/config-architecture.md +105 -0
- package/package.json +7 -2
package/defaults/pipeline.yaml
CHANGED
|
@@ -2,6 +2,31 @@ version: 1
|
|
|
2
2
|
default_workflow: inspect
|
|
3
3
|
orchestrator:
|
|
4
4
|
profile: moka-orchestrator
|
|
5
|
+
# PIPE-83 architecture hardening — ON by default so moka actually uses it.
|
|
6
|
+
# context_handoff: nodes pass curated NodeHandoffs downstream instead of raw
|
|
7
|
+
# transitive transcripts (kills the re-hydration latency/quality leak).
|
|
8
|
+
context_handoff:
|
|
9
|
+
enabled: true
|
|
10
|
+
# repo_map: prepend a tree-sitter + PageRank ranked code map to agent prompts,
|
|
11
|
+
# seeded by the node's task + handoff artifacts, within token_budget.
|
|
12
|
+
repo_map:
|
|
13
|
+
enabled: true
|
|
14
|
+
# durability: journal each terminal node result so a killed run resumes from the
|
|
15
|
+
# last passed node without re-running (or re-spending tokens on) finished work.
|
|
16
|
+
durability:
|
|
17
|
+
enabled: true
|
|
18
|
+
# best_of_n / parallel_worktrees: the verifier-pattern dial. Schedule generation
|
|
19
|
+
# + selection are validated/tested, BUT a live end-to-end run (2026-06-16) proved
|
|
20
|
+
# the EXECUTION is not yet production-ready, so it stays OFF by default (on-by-
|
|
21
|
+
# default made real runs hang). Two runtime gaps remain — see PIPE-83.14:
|
|
22
|
+
# 1. The leased opencode server is rooted at the main worktree and throws
|
|
23
|
+
# "Unexpected server error" when a candidate session runs with directory set
|
|
24
|
+
# to its isolated worktree → candidates exit 70, retries exhaust, green loops.
|
|
25
|
+
# Fix: lease a per-worktree opencode server for each candidate.
|
|
26
|
+
# 2. The winning candidate's file changes live in its worktree and are never
|
|
27
|
+
# merged back to the main tree, so downstream nodes wouldn't see them.
|
|
28
|
+
# Enable explicitly (best_of_n.enabled + n:2 + categories:[green] + parallel_
|
|
29
|
+
# worktrees.enabled) once those land; n=2 also ~doubles green-node spend.
|
|
5
30
|
token_budget:
|
|
6
31
|
default_context_window: 200000
|
|
7
32
|
max_context_pct: 50
|
package/dist/argo-graph.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { uniqueStrings } from "./strings.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { Data } from "effect";
|
|
3
4
|
//#region src/argo-graph.ts
|
|
4
5
|
const argoExecutableTaskSchema = z.object({
|
|
5
6
|
dependencies: z.array(z.string().min(1)),
|
|
@@ -18,14 +19,13 @@ const argoExecutionGraphSchema = z.object({
|
|
|
18
19
|
* lowered to an Argo DAG task. Callers should surface this as a validation
|
|
19
20
|
* failure before attempting a cluster submission.
|
|
20
21
|
*/
|
|
21
|
-
var ArgoGraphCompilerError = class extends
|
|
22
|
-
kind;
|
|
23
|
-
nodeId;
|
|
22
|
+
var ArgoGraphCompilerError = class extends Data.TaggedError("ArgoGraphCompilerError") {
|
|
24
23
|
constructor(kind, nodeId) {
|
|
25
|
-
super(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
super({
|
|
25
|
+
kind,
|
|
26
|
+
nodeId,
|
|
27
|
+
message: `Argo graph compiler: node kind '${kind}' on node '${nodeId}' cannot be lowered to an Argo DAG task`
|
|
28
|
+
});
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
31
|
function compileArgoExecutionGraph(plan) {
|
package/dist/argo-submit.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PipelineConfig } from "./config/schemas.js";
|
|
2
|
+
import { CoreApi, KubernetesArgoIoDependencies, WorkflowApi } from "./runtime/services/kubernetes-argo-service.js";
|
|
2
3
|
import { workflowSubmitResultSchema } from "./workflow-submit-contract.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { CoreV1Api, CustomObjectsApi, KubeConfig } from "@kubernetes/client-node";
|
|
5
5
|
|
|
6
6
|
//#region src/argo-submit.d.ts
|
|
7
7
|
declare const submitRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
|
|
@@ -36,11 +36,9 @@ type SubmitRunnerArgoWorkflowOptions = z.input<typeof submitRunnerArgoWorkflowOp
|
|
|
36
36
|
};
|
|
37
37
|
type SubmitRunnerArgoWorkflowResult = z.infer<typeof workflowSubmitResultSchema>;
|
|
38
38
|
type CommandScheduleOptions = z.input<typeof commandScheduleOptionsSchema>;
|
|
39
|
-
type CoreApi = Pick<CoreV1Api, "createNamespacedConfigMap">;
|
|
40
|
-
type WorkflowApi = Pick<CustomObjectsApi, "createNamespacedCustomObject">;
|
|
41
39
|
interface SubmitRunnerArgoWorkflowDependencies {
|
|
42
40
|
coreApi?: CoreApi;
|
|
43
|
-
kubeConfig?:
|
|
41
|
+
kubeConfig?: KubernetesArgoIoDependencies["kubeConfig"];
|
|
44
42
|
workflowApi?: WorkflowApi;
|
|
45
43
|
}
|
|
46
44
|
declare function submitRunnerArgoWorkflow(rawOptions: SubmitRunnerArgoWorkflowOptions, dependencies?: SubmitRunnerArgoWorkflowDependencies): Promise<SubmitRunnerArgoWorkflowResult>;
|
package/dist/argo-submit.js
CHANGED
|
@@ -4,11 +4,12 @@ import { buildRunnerArgoWorkflowManifest, runnerArgoWorkflowManifestSchema } fro
|
|
|
4
4
|
import { normalizeRunnerRepositoryForSubmit } from "./git-remote-url.js";
|
|
5
5
|
import { compileScheduleArtifact, parseScheduleArtifact } from "./planning/generate.js";
|
|
6
6
|
import { parseRunnerCommandPayload, runnerCommandPayloadSchema } from "./runner-command-contract.js";
|
|
7
|
+
import { KubernetesArgoService, KubernetesArgoServiceLive } from "./runtime/services/kubernetes-argo-service.js";
|
|
7
8
|
import { workflowSubmitResultSchema } from "./workflow-submit-contract.js";
|
|
8
9
|
import { stringify } from "yaml";
|
|
9
10
|
import { z } from "zod";
|
|
11
|
+
import { Effect } from "effect";
|
|
10
12
|
import { randomBytes } from "node:crypto";
|
|
11
|
-
import { CoreV1Api, CustomObjectsApi, KubeConfig } from "@kubernetes/client-node";
|
|
12
13
|
//#region src/argo-submit.ts
|
|
13
14
|
const scheduleIdSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
|
|
14
15
|
const configMapSchema = z.object({
|
|
@@ -48,7 +49,10 @@ const commandScheduleOptionsSchema = z.object({
|
|
|
48
49
|
scheduleId: scheduleIdSchema.optional(),
|
|
49
50
|
task: z.string().min(1)
|
|
50
51
|
}).strict();
|
|
51
|
-
|
|
52
|
+
function submitRunnerArgoWorkflow(rawOptions, dependencies = {}) {
|
|
53
|
+
return Effect.runPromise(Effect.provide(Effect.suspend(() => submitRunnerArgoWorkflowEffect(rawOptions, dependencies)), KubernetesArgoServiceLive));
|
|
54
|
+
}
|
|
55
|
+
function submitRunnerArgoWorkflowEffect(rawOptions, dependencies) {
|
|
52
56
|
const { config, ...schemaOptions } = rawOptions;
|
|
53
57
|
const options = submitRunnerArgoWorkflowOptionsSchema.parse(schemaOptions);
|
|
54
58
|
const { payload, payloadJson } = normalizeRunnerPayloadForSubmit({
|
|
@@ -60,7 +64,7 @@ async function submitRunnerArgoWorkflow(rawOptions, dependencies = {}) {
|
|
|
60
64
|
const scheduleArtifactConfigMapName = `pipeline-schedule-${randomBytes(6).toString("hex")}`;
|
|
61
65
|
const taskDescriptorConfigMapName = `pipeline-task-descriptors-${randomBytes(6).toString("hex")}`;
|
|
62
66
|
if (payload.workflow.id !== compiled.workflowId) throw new Error(`Runner payload workflow '${payload.workflow.id}' does not match schedule workflow '${compiled.workflowId}'`);
|
|
63
|
-
const
|
|
67
|
+
const graphEffect = compileSubmitArgoGraph(compiled).pipe(Effect.mapError((error) => /* @__PURE__ */ new Error(`Schedule '${compiled.workflowId}' cannot be submitted: ${error.message}`)));
|
|
64
68
|
const labels = {
|
|
65
69
|
"pipeline.oisin.dev/project": payload.run.project,
|
|
66
70
|
"pipeline.oisin.dev/run-id": payload.run.id,
|
|
@@ -91,64 +95,72 @@ async function submitRunnerArgoWorkflow(rawOptions, dependencies = {}) {
|
|
|
91
95
|
serviceAccountName: options.serviceAccountName,
|
|
92
96
|
taskDescriptorConfigMapName
|
|
93
97
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
98
|
+
return Effect.gen(function* () {
|
|
99
|
+
const service = yield* KubernetesArgoService;
|
|
100
|
+
const graph = yield* graphEffect;
|
|
101
|
+
yield* service.createConfigMap({
|
|
102
|
+
body: configMapSchema.parse({
|
|
103
|
+
apiVersion: "v1",
|
|
104
|
+
data: { "payload.json": payloadJson },
|
|
105
|
+
kind: "ConfigMap",
|
|
106
|
+
metadata: {
|
|
107
|
+
labels,
|
|
108
|
+
name: payloadConfigMapName,
|
|
109
|
+
namespace: options.namespace
|
|
110
|
+
}
|
|
111
|
+
}),
|
|
112
|
+
dependencies,
|
|
113
|
+
namespace: options.namespace,
|
|
114
|
+
options
|
|
115
|
+
});
|
|
116
|
+
yield* service.createConfigMap({
|
|
117
|
+
body: configMapSchema.parse({
|
|
118
|
+
apiVersion: "v1",
|
|
119
|
+
data: Object.fromEntries(graph.tasks.map((task) => [`${task.taskName}.json`, `${JSON.stringify(buildRunnerTaskDescriptor(task.nodeId))}\n`])),
|
|
120
|
+
kind: "ConfigMap",
|
|
121
|
+
metadata: {
|
|
122
|
+
labels,
|
|
123
|
+
name: taskDescriptorConfigMapName,
|
|
124
|
+
namespace: options.namespace
|
|
125
|
+
}
|
|
126
|
+
}),
|
|
127
|
+
dependencies,
|
|
128
|
+
namespace: options.namespace,
|
|
129
|
+
options
|
|
130
|
+
});
|
|
131
|
+
yield* service.createConfigMap({
|
|
132
|
+
body: configMapSchema.parse({
|
|
133
|
+
apiVersion: "v1",
|
|
134
|
+
data: { "schedule.yaml": options.scheduleYaml },
|
|
135
|
+
kind: "ConfigMap",
|
|
136
|
+
metadata: {
|
|
137
|
+
labels,
|
|
138
|
+
name: scheduleArtifactConfigMapName,
|
|
139
|
+
namespace: options.namespace
|
|
140
|
+
}
|
|
141
|
+
}),
|
|
142
|
+
dependencies,
|
|
143
|
+
namespace: options.namespace,
|
|
144
|
+
options
|
|
145
|
+
});
|
|
146
|
+
const response = yield* service.createWorkflow({
|
|
147
|
+
body: runnerArgoWorkflowManifestSchema.parse(workflow),
|
|
148
|
+
dependencies,
|
|
149
|
+
namespace: options.namespace,
|
|
150
|
+
options
|
|
151
|
+
});
|
|
152
|
+
const created = z.object({ metadata: z.object({
|
|
153
|
+
name: z.string().min(1).optional(),
|
|
154
|
+
uid: z.string().min(1).optional()
|
|
155
|
+
}).passthrough() }).passthrough().parse(response);
|
|
156
|
+
return workflowSubmitResultSchema.parse({
|
|
157
|
+
namespace: options.namespace,
|
|
158
|
+
payloadConfigMapName,
|
|
159
|
+
scheduleConfigMapName: scheduleArtifactConfigMapName,
|
|
160
|
+
taskDescriptorConfigMapName,
|
|
161
|
+
workflowName: created.metadata.name ?? workflow.metadata.name,
|
|
162
|
+
workflowUid: created.metadata.uid
|
|
163
|
+
});
|
|
152
164
|
});
|
|
153
165
|
}
|
|
154
166
|
function buildCommandScheduleYaml(rawOptions) {
|
|
@@ -182,25 +194,13 @@ function normalizeRunnerPayloadForSubmit(input) {
|
|
|
182
194
|
};
|
|
183
195
|
}
|
|
184
196
|
function compileSubmitArgoGraph(compiled) {
|
|
185
|
-
try
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
function apiClients(options, dependencies) {
|
|
193
|
-
if (dependencies.coreApi && dependencies.workflowApi) return {
|
|
194
|
-
coreApi: dependencies.coreApi,
|
|
195
|
-
workflowApi: dependencies.workflowApi
|
|
196
|
-
};
|
|
197
|
-
const kubeConfig = dependencies.kubeConfig ?? new KubeConfig();
|
|
198
|
-
if (!dependencies.kubeConfig) if (options.kubeconfigPath) kubeConfig.loadFromFile(options.kubeconfigPath);
|
|
199
|
-
else kubeConfig.loadFromDefault();
|
|
200
|
-
return {
|
|
201
|
-
coreApi: dependencies.coreApi ?? kubeConfig.makeApiClient(CoreV1Api),
|
|
202
|
-
workflowApi: dependencies.workflowApi ?? kubeConfig.makeApiClient(CustomObjectsApi)
|
|
203
|
-
};
|
|
197
|
+
return Effect.try({
|
|
198
|
+
try: () => compileArgoExecutionGraph(compiled.plan),
|
|
199
|
+
catch: (error) => {
|
|
200
|
+
if (error instanceof ArgoGraphCompilerError) return error;
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
204
|
}
|
|
205
205
|
//#endregion
|
|
206
206
|
export { buildCommandScheduleYaml, submitRunnerArgoWorkflow };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/bench/eval-report.ts
|
|
2
|
+
function buildEvalReport(results) {
|
|
3
|
+
const variants = [...new Set(results.map((r) => r.variant))].sort();
|
|
4
|
+
return {
|
|
5
|
+
tasks: new Set(results.map((r) => r.task)).size,
|
|
6
|
+
variants: variants.map((variant) => summarizeVariant(variant, results.filter((r) => r.variant === variant)))
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function summarizeVariant(variant, runs) {
|
|
10
|
+
const resolved = runs.filter((r) => r.resolved).length;
|
|
11
|
+
const totalWall = runs.reduce((sum, r) => sum + r.wallMs, 0);
|
|
12
|
+
return {
|
|
13
|
+
avgWallMs: runs.length ? Math.round(totalWall / runs.length) : 0,
|
|
14
|
+
count: runs.length,
|
|
15
|
+
resolutionRate: runs.length ? resolved / runs.length : 0,
|
|
16
|
+
resolved,
|
|
17
|
+
totalCostTokens: runs.reduce((sum, r) => sum + r.costTokens, 0),
|
|
18
|
+
variant
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function renderEvalReport(report) {
|
|
22
|
+
const lines = [`Eval over ${report.tasks} task(s):`, "variant | resolved | rate | tokens | avg ms"];
|
|
23
|
+
for (const v of report.variants) lines.push(`${v.variant} | ${v.resolved}/${v.count} | ${(v.resolutionRate * 100).toFixed(0)}% | ${v.totalCostTokens} | ${v.avgWallMs}`);
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { buildEvalReport, renderEvalReport };
|
package/dist/cli/program.js
CHANGED
|
@@ -7,8 +7,11 @@ import { compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifac
|
|
|
7
7
|
import { loadMokaGlobalConfig } from "../moka-global-config.js";
|
|
8
8
|
import { defaultClusterDoctorNamespace, runClusterDoctor } from "../cluster-doctor.js";
|
|
9
9
|
import { formatCodexAuthSyncResult, syncLocalCodexAuth } from "../codex-auth-sync.js";
|
|
10
|
+
import { registerBenchCommand } from "../commands/bench-command.js";
|
|
10
11
|
import { registerConfiguredEntrypointCommands } from "../commands/pipeline-command.js";
|
|
11
12
|
import { configureGatewayHosts, localGatewayStatus, reconcileGateway, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "../mcp/gateway.js";
|
|
13
|
+
import { generateRuntimeRunId } from "../runtime/context/context.js";
|
|
14
|
+
import "../runtime/context/index.js";
|
|
12
15
|
import { runPipelineFromConfig } from "../pipeline-runtime.js";
|
|
13
16
|
import { registerRunnerCommandCommand } from "../commands/runner-command-command.js";
|
|
14
17
|
import { formatConfigLintWarning, lintPipelineConfig } from "../config/lint.js";
|
|
@@ -47,7 +50,14 @@ function quick(description, options = {}) {
|
|
|
47
50
|
entrypoint: "quick"
|
|
48
51
|
});
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
function withRunId(inputs) {
|
|
54
|
+
return {
|
|
55
|
+
...inputs,
|
|
56
|
+
runId: inputs.runId ?? generateRuntimeRunId()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function runConfiguredPipeline(rawInputs) {
|
|
60
|
+
const inputs = withRunId(rawInputs);
|
|
51
61
|
const config = loadPipelineConfig(inputs.worktreePath, { allowMissingLintFileReferences: true });
|
|
52
62
|
if (inputs.schedule) {
|
|
53
63
|
const compiled = compileScheduleArtifact(config, parseScheduleArtifact(readFileSync(inputs.schedule, "utf8"), inputs.schedule), inputs.worktreePath);
|
|
@@ -70,6 +80,7 @@ async function runConfiguredPipeline(inputs) {
|
|
|
70
80
|
const result = await generateScheduleArtifact({
|
|
71
81
|
config,
|
|
72
82
|
entrypointId: scheduledEntrypoint,
|
|
83
|
+
runId: inputs.runId,
|
|
73
84
|
task: inputs.task,
|
|
74
85
|
worktreePath: inputs.worktreePath
|
|
75
86
|
});
|
|
@@ -94,6 +105,7 @@ async function runAndPrintPipeline(inputs) {
|
|
|
94
105
|
config: inputs.config,
|
|
95
106
|
reporter,
|
|
96
107
|
entrypoint: inputs.entrypoint,
|
|
108
|
+
runId: inputs.runId,
|
|
97
109
|
task: inputs.task,
|
|
98
110
|
workflowId: inputs.workflow,
|
|
99
111
|
worktreePath: inputs.worktreePath
|
|
@@ -177,8 +189,11 @@ function createCliProgram() {
|
|
|
177
189
|
const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
|
|
178
190
|
console.log(await localGatewayStatus(cwd));
|
|
179
191
|
});
|
|
180
|
-
program.command("init").description("Initialize package-owned pipeline support without repo-local config").action(async () => {
|
|
181
|
-
const result = await initPipelineProject({
|
|
192
|
+
program.command("init").description("Initialize package-owned pipeline support without repo-local config").addOption(new Option("--skill-scope <scope>", "where to install default skills: project (repo-local copy) or personal (one inherited user/global install)").choices(["project", "personal"]).default("project")).action(async (flags) => {
|
|
193
|
+
const result = await initPipelineProject({
|
|
194
|
+
cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd(),
|
|
195
|
+
scope: flags.skillScope
|
|
196
|
+
});
|
|
182
197
|
console.log(formatPipelineInitResult(result));
|
|
183
198
|
});
|
|
184
199
|
program.command("install-commands").description("Install generated slash-command adapters into this repository").addOption(new Option("--host <host>", "host command set to install").choices([
|
|
@@ -207,6 +222,7 @@ function createCliProgram() {
|
|
|
207
222
|
if (result.workflowUid) console.log(`Workflow UID: ${result.workflowUid}`);
|
|
208
223
|
});
|
|
209
224
|
registerRunnerCommandCommand(program);
|
|
225
|
+
registerBenchCommand(program);
|
|
210
226
|
const configuredEntrypointCommands = registerConfiguredEntrypointCommands(program, omitConfiguredEntrypoints(configuredPipeline, ["execute", "quick"]), async (entrypoint, task, _opts) => {
|
|
211
227
|
await execute(task, { entrypoint });
|
|
212
228
|
});
|
package/dist/cluster-doctor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { KubernetesArgoService, KubernetesArgoServiceLive } from "./runtime/services/kubernetes-argo-service.js";
|
|
1
2
|
import { loadMokaGlobalConfig } from "./moka-global-config.js";
|
|
2
|
-
import {
|
|
3
|
+
import { Effect } from "effect";
|
|
3
4
|
//#region src/cluster-doctor.ts
|
|
4
5
|
const DEFAULT_NAMESPACE = "momokaya-pipeline";
|
|
5
6
|
const DEFAULT_RESOURCES = {
|
|
@@ -13,41 +14,46 @@ const DEFAULT_RESOURCES = {
|
|
|
13
14
|
serviceAccountName: "pipeline-runner"
|
|
14
15
|
};
|
|
15
16
|
const FORBIDDEN_RE = /forbidden/i;
|
|
16
|
-
|
|
17
|
+
function runClusterDoctor(options = {}) {
|
|
18
|
+
return Effect.runPromise(Effect.provide(Effect.suspend(() => runClusterDoctorEffect(options)), KubernetesArgoServiceLive));
|
|
19
|
+
}
|
|
20
|
+
function runClusterDoctorEffect(options = {}) {
|
|
17
21
|
const resources = clusterResources();
|
|
18
22
|
const namespace = options.namespace ?? DEFAULT_NAMESPACE;
|
|
19
23
|
const kubectlOptions = {
|
|
20
24
|
kubeContext: options.kubeContext,
|
|
21
25
|
kubeconfigPath: options.kubeconfigPath
|
|
22
26
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
27
|
+
return Effect.gen(function* () {
|
|
28
|
+
const checks = yield* Effect.all([
|
|
29
|
+
checkKubectlNamespace(namespace, kubectlOptions),
|
|
30
|
+
...secretChecks(namespace, kubectlOptions, resources),
|
|
31
|
+
checkExternalSecret(namespace, resources.eventAuthExternalSecretName, resources.externalSecretRemoteRef, kubectlOptions),
|
|
32
|
+
checkClusterSecretStore("openbao", kubectlOptions),
|
|
33
|
+
checkServiceAccount(namespace, resources.serviceAccountName, kubectlOptions),
|
|
34
|
+
checkWorkflowSubmitPermission(namespace, {
|
|
35
|
+
resource: "workflows.argoproj.io",
|
|
36
|
+
verb: "create",
|
|
37
|
+
...kubectlOptions
|
|
38
|
+
}),
|
|
39
|
+
checkClusterResource("argo-workflow-crd", [
|
|
40
|
+
"get",
|
|
41
|
+
"crd",
|
|
42
|
+
"workflows.argoproj.io"
|
|
43
|
+
], kubectlOptions),
|
|
44
|
+
checkClusterResource("argo-workflow-controller", [
|
|
45
|
+
"get",
|
|
46
|
+
"pods",
|
|
47
|
+
"-A",
|
|
48
|
+
"-l",
|
|
49
|
+
"app=workflow-controller"
|
|
50
|
+
], kubectlOptions)
|
|
51
|
+
], { concurrency: "unbounded" });
|
|
52
|
+
return {
|
|
53
|
+
checks,
|
|
54
|
+
passed: checks.every((check) => check.passed)
|
|
55
|
+
};
|
|
56
|
+
});
|
|
51
57
|
}
|
|
52
58
|
function defaultClusterDoctorNamespace() {
|
|
53
59
|
return DEFAULT_NAMESPACE;
|
|
@@ -89,9 +95,8 @@ function checkKubectlNamespace(namespace, kubectlOptions) {
|
|
|
89
95
|
namespace
|
|
90
96
|
], `Namespace ${namespace} missing or inaccessible.`, kubectlOptions);
|
|
91
97
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return result.ok ? {
|
|
98
|
+
function checkNamespacedResource(name, args, missingDetail, kubectlOptions) {
|
|
99
|
+
return kubectl(args, kubectlOptions).pipe(Effect.map((result) => result.ok ? {
|
|
95
100
|
detail: "present",
|
|
96
101
|
name,
|
|
97
102
|
passed: true
|
|
@@ -99,10 +104,10 @@ async function checkNamespacedResource(name, args, missingDetail, kubectlOptions
|
|
|
99
104
|
detail: inaccessibleOrMissingDetail(name, missingDetail, result),
|
|
100
105
|
name,
|
|
101
106
|
passed: false
|
|
102
|
-
};
|
|
107
|
+
}));
|
|
103
108
|
}
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
function checkExternalSecret(namespace, name, remoteRef, kubectlOptions) {
|
|
110
|
+
return kubectl([
|
|
106
111
|
"get",
|
|
107
112
|
"externalsecret",
|
|
108
113
|
name,
|
|
@@ -110,34 +115,36 @@ async function checkExternalSecret(namespace, name, remoteRef, kubectlOptions) {
|
|
|
110
115
|
namespace,
|
|
111
116
|
"-o",
|
|
112
117
|
"json"
|
|
113
|
-
], kubectlOptions)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
118
|
+
], kubectlOptions).pipe(Effect.map((result) => {
|
|
119
|
+
if (!result.ok) {
|
|
120
|
+
const missingDetail = `ExternalSecret ${name} missing in ${namespace}; expected it to sync ${remoteRef}.`;
|
|
121
|
+
return {
|
|
122
|
+
detail: inaccessibleOrMissingDetail(`externalsecret/${name}`, missingDetail, result),
|
|
123
|
+
name: `externalsecret/${name}`,
|
|
124
|
+
passed: false
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return readyConditionCheck(`externalsecret/${name}`, result.stdout);
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
function checkClusterSecretStore(name, kubectlOptions) {
|
|
131
|
+
return kubectl([
|
|
126
132
|
"get",
|
|
127
133
|
"clustersecretstore",
|
|
128
134
|
name,
|
|
129
135
|
"-o",
|
|
130
136
|
"json"
|
|
131
|
-
], kubectlOptions)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
], kubectlOptions).pipe(Effect.map((result) => {
|
|
138
|
+
if (!result.ok) {
|
|
139
|
+
const missingDetail = `ClusterSecretStore/${name} missing or inaccessible; OpenBao/ESO readiness is an external prerequisite.`;
|
|
140
|
+
return {
|
|
141
|
+
detail: inaccessibleOrMissingDetail(`clustersecretstore/${name}`, missingDetail, result),
|
|
142
|
+
name: `clustersecretstore/${name}`,
|
|
143
|
+
passed: false
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return readyConditionCheck(`clustersecretstore/${name}`, result.stdout);
|
|
147
|
+
}));
|
|
141
148
|
}
|
|
142
149
|
function checkServiceAccount(namespace, name, kubectlOptions) {
|
|
143
150
|
return checkNamespacedResource(`serviceaccount/${name}`, [
|
|
@@ -148,15 +155,15 @@ function checkServiceAccount(namespace, name, kubectlOptions) {
|
|
|
148
155
|
namespace
|
|
149
156
|
], `ServiceAccount ${name} missing in ${namespace}; runner pods must use this account for workflow execution.`, kubectlOptions);
|
|
150
157
|
}
|
|
151
|
-
|
|
152
|
-
return
|
|
158
|
+
function checkWorkflowSubmitPermission(namespace, options) {
|
|
159
|
+
return kubectl([
|
|
153
160
|
"auth",
|
|
154
161
|
"can-i",
|
|
155
162
|
options.verb,
|
|
156
163
|
options.resource,
|
|
157
164
|
"-n",
|
|
158
165
|
namespace
|
|
159
|
-
], options)).stdout.trim() === "yes" ? {
|
|
166
|
+
], options).pipe(Effect.map((result) => result.stdout.trim() === "yes" ? {
|
|
160
167
|
detail: `current kube identity can ${options.verb} ${options.resource}`,
|
|
161
168
|
name: "rbac/workflow-create",
|
|
162
169
|
passed: true
|
|
@@ -164,11 +171,10 @@ async function checkWorkflowSubmitPermission(namespace, options) {
|
|
|
164
171
|
detail: `current kube identity cannot ${options.verb} ${options.resource}; check submitter RBAC for Workflow creation.`,
|
|
165
172
|
name: "rbac/workflow-create",
|
|
166
173
|
passed: false
|
|
167
|
-
};
|
|
174
|
+
}));
|
|
168
175
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return result.ok ? {
|
|
176
|
+
function checkClusterResource(name, args, kubectlOptions) {
|
|
177
|
+
return kubectl(args, kubectlOptions).pipe(Effect.map((result) => result.ok ? {
|
|
172
178
|
detail: "present",
|
|
173
179
|
name,
|
|
174
180
|
passed: true
|
|
@@ -176,46 +182,28 @@ async function checkClusterResource(name, args, kubectlOptions) {
|
|
|
176
182
|
detail: isForbidden(result) ? inaccessibleDetail(name, result) : result.stderr || "missing or inaccessible",
|
|
177
183
|
name,
|
|
178
184
|
passed: false
|
|
179
|
-
};
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
function findReadyCondition(source) {
|
|
188
|
+
return parseJson(source).status?.conditions?.find((condition) => condition.type === "Ready");
|
|
189
|
+
}
|
|
190
|
+
function readyDetail(ready, passed) {
|
|
191
|
+
const fallback = passed ? "Ready=True" : "Ready condition is missing or not True";
|
|
192
|
+
return ready?.message || fallback;
|
|
180
193
|
}
|
|
181
194
|
function readyConditionCheck(name, source) {
|
|
182
|
-
const ready =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
passed: true
|
|
187
|
-
} : {
|
|
188
|
-
detail: ready?.message || "Ready condition is missing or not True",
|
|
195
|
+
const ready = findReadyCondition(source);
|
|
196
|
+
const passed = ready?.status === "True";
|
|
197
|
+
return {
|
|
198
|
+
detail: readyDetail(ready, passed),
|
|
189
199
|
name,
|
|
190
|
-
passed
|
|
200
|
+
passed
|
|
191
201
|
};
|
|
192
202
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
stdin: "ignore"
|
|
198
|
-
});
|
|
199
|
-
return {
|
|
200
|
-
ok: true,
|
|
201
|
-
stderr: result.stderr,
|
|
202
|
-
stdout: result.stdout
|
|
203
|
-
};
|
|
204
|
-
} catch (err) {
|
|
205
|
-
const error = err;
|
|
206
|
-
return {
|
|
207
|
-
ok: false,
|
|
208
|
-
stderr: (error.stderr || error.shortMessage || "kubectl failed").trim(),
|
|
209
|
-
stdout: (error.stdout || "").trim()
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
function kubectlArgs(args, kubeContext) {
|
|
214
|
-
return kubeContext ? [
|
|
215
|
-
"--context",
|
|
216
|
-
kubeContext,
|
|
217
|
-
...args
|
|
218
|
-
] : args;
|
|
203
|
+
function kubectl(args, options) {
|
|
204
|
+
return Effect.gen(function* () {
|
|
205
|
+
return yield* (yield* KubernetesArgoService).kubectl(args, options);
|
|
206
|
+
});
|
|
219
207
|
}
|
|
220
208
|
function inaccessibleOrMissingDetail(name, missingDetail, result) {
|
|
221
209
|
return isForbidden(result) ? inaccessibleDetail(name, result) : missingDetail;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { buildEvalReport, renderEvalReport } from "../bench/eval-report.js";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
//#region src/commands/bench-command.ts
|
|
4
|
+
/**
|
|
5
|
+
* PIPE-83.6: `moka bench` — score a flat single-agent baseline vs the pipeline
|
|
6
|
+
* (and ablations) over a recorded run set. Runs are produced by executing the
|
|
7
|
+
* bench task set (bench/tasks) through `moka run` for each variant and recording
|
|
8
|
+
* one EvalRunResult per task+variant; this command turns those records into the
|
|
9
|
+
* comparison report.
|
|
10
|
+
*/
|
|
11
|
+
function registerBenchCommand(program) {
|
|
12
|
+
program.command("bench").description("Score a flat single-agent baseline vs the pipeline over recorded bench runs").requiredOption("--results <path>", "JSON file: array of { task, variant, resolved, costTokens, wallMs }").action((options) => {
|
|
13
|
+
const records = JSON.parse(readFileSync(options.results, "utf8"));
|
|
14
|
+
process.stdout.write(`${renderEvalReport(buildEvalReport(records))}\n`);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { registerBenchCommand };
|