@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.
Files changed (60) hide show
  1. package/defaults/pipeline.yaml +25 -0
  2. package/dist/argo-graph.js +7 -7
  3. package/dist/argo-submit.d.ts +2 -4
  4. package/dist/argo-submit.js +80 -80
  5. package/dist/bench/eval-report.js +27 -0
  6. package/dist/cli/program.js +19 -3
  7. package/dist/cluster-doctor.js +89 -101
  8. package/dist/commands/bench-command.js +18 -0
  9. package/dist/config/defaults.js +9 -19
  10. package/dist/config/load.js +47 -37
  11. package/dist/config/schemas.d.ts +24 -7
  12. package/dist/config/schemas.js +20 -7
  13. package/dist/context/repo-map.js +203 -0
  14. package/dist/install-commands/opencode.js +10 -1
  15. package/dist/mcp/gateway-error.js +15 -0
  16. package/dist/mcp/gateway.js +119 -220
  17. package/dist/moka-global-config.js +20 -20
  18. package/dist/moka-submit.d.ts +6 -6
  19. package/dist/pipeline-init.js +18 -12
  20. package/dist/pipeline-runtime.js +592 -372
  21. package/dist/planning/compile.d.ts +8 -3
  22. package/dist/planning/compile.js +7 -7
  23. package/dist/planning/generate.d.ts +6 -1
  24. package/dist/planning/generate.js +29 -7
  25. package/dist/run-state/git-refs.js +124 -94
  26. package/dist/runner-command-contract.d.ts +6 -1
  27. package/dist/runner-command-contract.js +6 -5
  28. package/dist/runner-event-schema.d.ts +6 -6
  29. package/dist/runner-event-sink.js +37 -68
  30. package/dist/runner.d.ts +6 -1
  31. package/dist/runner.js +3 -3
  32. package/dist/runtime/agent-node/agent-node.js +218 -159
  33. package/dist/runtime/changed-files/changed-files.js +15 -27
  34. package/dist/runtime/changed-files/index.js +2 -0
  35. package/dist/runtime/drain-merge/drain-merge.js +124 -82
  36. package/dist/runtime/gates/gates.js +45 -27
  37. package/dist/runtime/hooks/hooks.js +74 -29
  38. package/dist/runtime/local-scheduler.js +45 -0
  39. package/dist/runtime/opencode-server.js +32 -23
  40. package/dist/runtime/opencode-session-executor.js +101 -44
  41. package/dist/runtime/parallel-node/parallel-node.js +93 -75
  42. package/dist/runtime/parallel-worktrees/parallel-worktrees.js +49 -4
  43. package/dist/runtime/run-journal.js +21 -0
  44. package/dist/runtime/scheduler.js +122 -93
  45. package/dist/runtime/select-candidate/select-candidate.js +52 -24
  46. package/dist/runtime/services/agent-node-runtime-service.js +15 -0
  47. package/dist/runtime/services/command-executor-service.js +8 -0
  48. package/dist/runtime/services/config-io-service.js +42 -0
  49. package/dist/runtime/services/drain-merge-git-service.js +10 -0
  50. package/dist/runtime/services/git-porcelain-service.js +38 -0
  51. package/dist/runtime/services/kubernetes-argo-service.d.ts +13 -0
  52. package/dist/runtime/services/kubernetes-argo-service.js +81 -0
  53. package/dist/runtime/services/mcp-gateway-service.js +184 -0
  54. package/dist/runtime/services/opencode-sdk-service.js +27 -0
  55. package/dist/runtime/services/runner-event-sink-http-service.js +80 -0
  56. package/dist/runtime/services/select-candidate-service.js +13 -0
  57. package/dist/runtime/services/worktree-service.js +18 -0
  58. package/dist/schedule/passes/candidates.js +17 -8
  59. package/docs/config-architecture.md +105 -0
  60. package/package.json +7 -2
@@ -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
@@ -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 Error {
22
- kind;
23
- nodeId;
22
+ var ArgoGraphCompilerError = class extends Data.TaggedError("ArgoGraphCompilerError") {
24
23
  constructor(kind, nodeId) {
25
- super(`Argo graph compiler: node kind '${kind}' on node '${nodeId}' cannot be lowered to an Argo DAG task`);
26
- this.name = "ArgoGraphCompilerError";
27
- this.kind = kind;
28
- this.nodeId = nodeId;
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) {
@@ -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?: KubeConfig;
41
+ kubeConfig?: KubernetesArgoIoDependencies["kubeConfig"];
44
42
  workflowApi?: WorkflowApi;
45
43
  }
46
44
  declare function submitRunnerArgoWorkflow(rawOptions: SubmitRunnerArgoWorkflowOptions, dependencies?: SubmitRunnerArgoWorkflowDependencies): Promise<SubmitRunnerArgoWorkflowResult>;
@@ -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
- async function submitRunnerArgoWorkflow(rawOptions, dependencies = {}) {
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 graph = compileSubmitArgoGraph(compiled);
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
- const { coreApi, workflowApi } = apiClients(options, dependencies);
95
- await coreApi.createNamespacedConfigMap({
96
- body: configMapSchema.parse({
97
- apiVersion: "v1",
98
- data: { "payload.json": payloadJson },
99
- kind: "ConfigMap",
100
- metadata: {
101
- labels,
102
- name: payloadConfigMapName,
103
- namespace: options.namespace
104
- }
105
- }),
106
- namespace: options.namespace
107
- });
108
- await coreApi.createNamespacedConfigMap({
109
- body: configMapSchema.parse({
110
- apiVersion: "v1",
111
- data: Object.fromEntries(graph.tasks.map((task) => [`${task.taskName}.json`, `${JSON.stringify(buildRunnerTaskDescriptor(task.nodeId))}\n`])),
112
- kind: "ConfigMap",
113
- metadata: {
114
- labels,
115
- name: taskDescriptorConfigMapName,
116
- namespace: options.namespace
117
- }
118
- }),
119
- namespace: options.namespace
120
- });
121
- await coreApi.createNamespacedConfigMap({
122
- body: configMapSchema.parse({
123
- apiVersion: "v1",
124
- data: { "schedule.yaml": options.scheduleYaml },
125
- kind: "ConfigMap",
126
- metadata: {
127
- labels,
128
- name: scheduleArtifactConfigMapName,
129
- namespace: options.namespace
130
- }
131
- }),
132
- namespace: options.namespace
133
- });
134
- const response = await workflowApi.createNamespacedCustomObject({
135
- body: runnerArgoWorkflowManifestSchema.parse(workflow),
136
- group: "argoproj.io",
137
- namespace: options.namespace,
138
- plural: "workflows",
139
- version: "v1alpha1"
140
- });
141
- const created = z.object({ metadata: z.object({
142
- name: z.string().min(1).optional(),
143
- uid: z.string().min(1).optional()
144
- }).passthrough() }).passthrough().parse(response);
145
- return workflowSubmitResultSchema.parse({
146
- namespace: options.namespace,
147
- payloadConfigMapName,
148
- scheduleConfigMapName: scheduleArtifactConfigMapName,
149
- taskDescriptorConfigMapName,
150
- workflowName: created.metadata.name ?? workflow.metadata.name,
151
- workflowUid: created.metadata.uid
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
- return compileArgoExecutionGraph(compiled.plan);
187
- } catch (err) {
188
- if (err instanceof ArgoGraphCompilerError) throw new Error(`Schedule '${compiled.workflowId}' cannot be submitted: ${err.message}`);
189
- throw err;
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 };
@@ -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
- async function runConfiguredPipeline(inputs) {
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({ cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd() });
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
  });
@@ -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 { execa } from "execa";
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
- async function runClusterDoctor(options = {}) {
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
- const checks = await Promise.all([
24
- checkKubectlNamespace(namespace, kubectlOptions),
25
- ...secretChecks(namespace, kubectlOptions, resources),
26
- checkExternalSecret(namespace, resources.eventAuthExternalSecretName, resources.externalSecretRemoteRef, kubectlOptions),
27
- checkClusterSecretStore("openbao", kubectlOptions),
28
- checkServiceAccount(namespace, resources.serviceAccountName, kubectlOptions),
29
- checkWorkflowSubmitPermission(namespace, {
30
- resource: "workflows.argoproj.io",
31
- verb: "create",
32
- ...kubectlOptions
33
- }),
34
- checkClusterResource("argo-workflow-crd", [
35
- "get",
36
- "crd",
37
- "workflows.argoproj.io"
38
- ], kubectlOptions),
39
- checkClusterResource("argo-workflow-controller", [
40
- "get",
41
- "pods",
42
- "-A",
43
- "-l",
44
- "app=workflow-controller"
45
- ], kubectlOptions)
46
- ]);
47
- return {
48
- checks,
49
- passed: checks.every((check) => check.passed)
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
- async function checkNamespacedResource(name, args, missingDetail, kubectlOptions) {
93
- const result = await kubectl(args, kubectlOptions);
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
- async function checkExternalSecret(namespace, name, remoteRef, kubectlOptions) {
105
- const result = await kubectl([
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
- if (!result.ok) {
115
- const missingDetail = `ExternalSecret ${name} missing in ${namespace}; expected it to sync ${remoteRef}.`;
116
- return {
117
- detail: inaccessibleOrMissingDetail(`externalsecret/${name}`, missingDetail, result),
118
- name: `externalsecret/${name}`,
119
- passed: false
120
- };
121
- }
122
- return readyConditionCheck(`externalsecret/${name}`, result.stdout);
123
- }
124
- async function checkClusterSecretStore(name, kubectlOptions) {
125
- const result = await kubectl([
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
- if (!result.ok) {
133
- const missingDetail = `ClusterSecretStore/${name} missing or inaccessible; OpenBao/ESO readiness is an external prerequisite.`;
134
- return {
135
- detail: inaccessibleOrMissingDetail(`clustersecretstore/${name}`, missingDetail, result),
136
- name: `clustersecretstore/${name}`,
137
- passed: false
138
- };
139
- }
140
- return readyConditionCheck(`clustersecretstore/${name}`, result.stdout);
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
- async function checkWorkflowSubmitPermission(namespace, options) {
152
- return (await kubectl([
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
- async function checkClusterResource(name, args, kubectlOptions) {
170
- const result = await kubectl(args, kubectlOptions);
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 = parseJson(source).status?.conditions?.find((condition) => condition.type === "Ready");
183
- return ready?.status === "True" ? {
184
- detail: ready.message || "Ready=True",
185
- name,
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: false
200
+ passed
191
201
  };
192
202
  }
193
- async function kubectl(args, options) {
194
- try {
195
- const result = await execa("kubectl", kubectlArgs(args, options.kubeContext), {
196
- env: options.kubeconfigPath ? { KUBECONFIG: options.kubeconfigPath } : void 0,
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 };