@oisincoveney/pipeline 3.19.4 → 3.19.5

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.
@@ -26,6 +26,7 @@ declare const submitRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
26
26
  Never: "Never";
27
27
  }>>;
28
28
  imagePullSecretName: z.ZodOptional<z.ZodString>;
29
+ kubeContext: z.ZodOptional<z.ZodString>;
29
30
  kubeconfigPath: z.ZodOptional<z.ZodString>;
30
31
  mcpGatewayAuth: z.ZodOptional<z.ZodObject<{
31
32
  secretKey: z.ZodDefault<z.ZodString>;
@@ -59,6 +60,7 @@ declare const submitDynamicRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
59
60
  Never: "Never";
60
61
  }>>;
61
62
  imagePullSecretName: z.ZodOptional<z.ZodString>;
63
+ kubeContext: z.ZodOptional<z.ZodString>;
62
64
  kubeconfigPath: z.ZodOptional<z.ZodString>;
63
65
  mcpGatewayAuth: z.ZodOptional<z.ZodObject<{
64
66
  secretKey: z.ZodDefault<z.ZodString>;
@@ -43,6 +43,7 @@ const submitRunnerArgoWorkflowBaseOptionShape = {
43
43
  "Never"
44
44
  ]).optional(),
45
45
  imagePullSecretName: z.string().min(1).optional(),
46
+ kubeContext: z.string().min(1).optional(),
46
47
  kubeconfigPath: z.string().min(1).optional(),
47
48
  mcpGatewayAuth: mcpGatewayAuthOptionSchema.optional(),
48
49
  name: z.string().min(1).optional(),
@@ -166,16 +167,16 @@ function submitRunnerArgoWorkflowEffect(rawOptions, dependencies) {
166
167
  namespace: options.namespace,
167
168
  options
168
169
  });
169
- return workflowSubmitResult(yield* service.createWorkflow({
170
- body: runnerArgoWorkflowManifestSchema.parse(workflow),
170
+ return yield* submitWorkflowManifest({
171
171
  dependencies,
172
172
  namespace: options.namespace,
173
- options
174
- }), workflow, {
175
- namespace: options.namespace,
176
- payloadConfigMapName,
177
- scheduleConfigMapName: scheduleArtifactConfigMapName,
178
- taskDescriptorConfigMapName
173
+ options,
174
+ resultExtras: {
175
+ payloadConfigMapName,
176
+ scheduleConfigMapName: scheduleArtifactConfigMapName,
177
+ taskDescriptorConfigMapName
178
+ },
179
+ workflow
179
180
  });
180
181
  });
181
182
  }
@@ -219,8 +220,7 @@ function submitDynamicRunnerArgoWorkflowEffect(rawOptions, dependencies) {
219
220
  workflowId: options.workflowId
220
221
  });
221
222
  return Effect.gen(function* () {
222
- const service = yield* KubernetesArgoService;
223
- yield* service.createConfigMap({
223
+ yield* (yield* KubernetesArgoService).createConfigMap({
224
224
  body: configMapSchema.parse({
225
225
  apiVersion: "v1",
226
226
  data: { "payload.json": payloadJson },
@@ -235,14 +235,25 @@ function submitDynamicRunnerArgoWorkflowEffect(rawOptions, dependencies) {
235
235
  namespace: options.namespace,
236
236
  options
237
237
  });
238
- return workflowSubmitResult(yield* service.createWorkflow({
239
- body: runnerArgoWorkflowManifestSchema.parse(workflow),
238
+ return yield* submitWorkflowManifest({
240
239
  dependencies,
241
240
  namespace: options.namespace,
242
- options
243
- }), workflow, {
244
- namespace: options.namespace,
245
- payloadConfigMapName
241
+ options,
242
+ resultExtras: { payloadConfigMapName },
243
+ workflow
244
+ });
245
+ });
246
+ }
247
+ function submitWorkflowManifest(input) {
248
+ return Effect.gen(function* () {
249
+ return workflowSubmitResult(yield* (yield* KubernetesArgoService).createWorkflow({
250
+ body: runnerArgoWorkflowManifestSchema.parse(input.workflow),
251
+ dependencies: input.dependencies,
252
+ namespace: input.namespace,
253
+ options: input.options
254
+ }), input.workflow, {
255
+ namespace: input.namespace,
256
+ ...input.resultExtras
246
257
  });
247
258
  });
248
259
  }
@@ -4,8 +4,16 @@ import { loadMokaGlobalConfig } from "../moka-global-config.js";
4
4
  import { submitMoka } from "../moka-submit.js";
5
5
  import { Option } from "commander";
6
6
  //#region src/cli/submit-options.ts
7
+ function resolveOptionalSecretRef(flags, fromGlobalConfig) {
8
+ if (flags.skip) return;
9
+ if (flags.secretName) return {
10
+ secretName: flags.secretName,
11
+ ...flags.secretKey ? { secretKey: flags.secretKey } : {}
12
+ };
13
+ return fromGlobalConfig;
14
+ }
7
15
  function addMokaSubmitOptions(command) {
8
- return addRunnerArgoOptions(command.option("--quick", "submit the compact graph").option("--command", "treat input after -- as explicit argv").option("--schedule <path>", "approved schedule YAML to submit").option("--event-url <url>", "runner event sink URL").option("--open-pr", "append an open-pull-request delivery node (preview-labelled PR)").option("--task <text>", "task description for command-mode metadata"), { kubeconfig: true });
16
+ return addRunnerArgoOptions(command.option("--quick", "submit the compact graph").option("--command", "treat input after -- as explicit argv").option("--schedule <path>", "approved schedule YAML to submit").option("--event-url <url>", "runner event sink URL").option("--open-pr", "append an open-pull-request delivery node (preview-labelled PR)").option("--task <text>", "task description for command-mode metadata").option("--db-auth-secret-name <name>", "override momokaya.submit.dbAuth secret name").option("--db-auth-secret-key <key>", "override momokaya.submit.dbAuth secret key").option("--skip-db-auth", "omit MOKA_DB_URL injection regardless of global config").option("--mcp-gateway-auth-secret-name <name>", "override momokaya.submit.mcpGatewayAuth secret name").option("--mcp-gateway-auth-secret-key <key>", "override momokaya.submit.mcpGatewayAuth secret key").option("--skip-mcp-gateway-auth", "omit PIPELINE_MCP_GATEWAY_AUTHORIZATION injection regardless of global config"), { kubeconfig: true });
9
17
  }
10
18
  function runMokaSubmitFromCli(input, flags) {
11
19
  const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
@@ -36,8 +44,16 @@ function mokaCommonSubmitOptions(input) {
36
44
  return {
37
45
  brokerAuth: resolveMokaBrokerAuth(input.globalConfig),
38
46
  config: input.config,
39
- dbAuth: momokaya?.submit.dbAuth,
40
- mcpGatewayAuth: momokaya?.submit.mcpGatewayAuth,
47
+ dbAuth: resolveOptionalSecretRef({
48
+ secretKey: input.flags.dbAuthSecretKey,
49
+ secretName: input.flags.dbAuthSecretName,
50
+ skip: input.flags.skipDbAuth
51
+ }, momokaya?.submit.dbAuth),
52
+ mcpGatewayAuth: resolveOptionalSecretRef({
53
+ secretKey: input.flags.mcpGatewayAuthSecretKey,
54
+ secretName: input.flags.mcpGatewayAuthSecretName,
55
+ skip: input.flags.skipMcpGatewayAuth
56
+ }, momokaya?.submit.mcpGatewayAuth),
41
57
  delivery: { pullRequest: input.flags.openPr === true },
42
58
  eventUrl: input.eventUrl,
43
59
  eventAuthSecretKey: momokaya?.submit.eventAuthSecretKey,
@@ -48,6 +64,7 @@ function mokaCommonSubmitOptions(input) {
48
64
  image: input.flags.image,
49
65
  imagePullPolicy: parseImagePullPolicy(input.flags.imagePullPolicy),
50
66
  imagePullSecretName: input.flags.imagePullSecret ?? momokaya?.submit.imagePullSecretName,
67
+ kubeContext: input.flags.kubeContext ?? momokaya?.kubernetes.context,
51
68
  kubeconfigPath: input.flags.kubeconfig ?? momokaya?.kubernetes.kubeconfig,
52
69
  name: input.flags.name,
53
70
  namespace: input.flags.namespace ?? momokaya?.kubernetes.namespace,
@@ -83,7 +100,7 @@ function submitMokaGraphInput(input, flags, commonOptions) {
83
100
  }
84
101
  function addRunnerArgoOptions(command, options = {}) {
85
102
  command.option("--name <name>", "Workflow metadata.name").option("--generate-name <prefix>", "Workflow metadata.generateName").option("--namespace <namespace>", "Workflow namespace");
86
- if (options.kubeconfig) command.option("--kubeconfig <path>", "kubeconfig path");
103
+ if (options.kubeconfig) command.option("--kubeconfig <path>", "kubeconfig path").option("--kube-context <name>", "kubeconfig context to target");
87
104
  return command.option("--service-account <name>", "Workflow service account").option("--image <image>", "runner image").addOption(new Option("--image-pull-policy <policy>", "runner image pull policy").choices([
88
105
  "Always",
89
106
  "IfNotPresent",
@@ -9,6 +9,7 @@ declare const mokaGlobalConfigSchema: z.ZodObject<{
9
9
  url: z.ZodString;
10
10
  }, z.core.$strict>>;
11
11
  kubernetes: z.ZodObject<{
12
+ context: z.ZodOptional<z.ZodString>;
12
13
  kubeconfig: z.ZodOptional<z.ZodString>;
13
14
  namespace: z.ZodString;
14
15
  }, z.core.$strict>;
@@ -29,6 +29,7 @@ const mokaSubmitGlobalConfigSchema = z.object({
29
29
  serviceAccountName: z.string().min(1)
30
30
  }).strict();
31
31
  const mokaKubernetesGlobalConfigSchema = z.object({
32
+ context: z.string().min(1).optional(),
32
33
  kubeconfig: z.string().min(1).optional(),
33
34
  namespace: z.string().min(1)
34
35
  }).strict();
@@ -197,6 +197,7 @@ declare const mokaSubmitBaseOptionsSchema: z.ZodObject<{
197
197
  Never: "Never";
198
198
  }>>;
199
199
  imagePullSecretName: z.ZodOptional<z.ZodString>;
200
+ kubeContext: z.ZodOptional<z.ZodString>;
200
201
  kubeconfigPath: z.ZodOptional<z.ZodString>;
201
202
  name: z.ZodOptional<z.ZodString>;
202
203
  namespace: z.ZodOptional<z.ZodString>;
@@ -310,6 +311,7 @@ declare const mokaGraphSubmitOptionsSchema: z.ZodObject<{
310
311
  Never: "Never";
311
312
  }>>;
312
313
  imagePullSecretName: z.ZodOptional<z.ZodString>;
314
+ kubeContext: z.ZodOptional<z.ZodString>;
313
315
  kubeconfigPath: z.ZodOptional<z.ZodString>;
314
316
  name: z.ZodOptional<z.ZodString>;
315
317
  namespace: z.ZodOptional<z.ZodString>;
@@ -440,6 +442,7 @@ declare const mokaCommandSubmitOptionsSchema: z.ZodObject<{
440
442
  Never: "Never";
441
443
  }>>;
442
444
  imagePullSecretName: z.ZodOptional<z.ZodString>;
445
+ kubeContext: z.ZodOptional<z.ZodString>;
443
446
  kubeconfigPath: z.ZodOptional<z.ZodString>;
444
447
  name: z.ZodOptional<z.ZodString>;
445
448
  namespace: z.ZodOptional<z.ZodString>;
@@ -565,6 +568,7 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
565
568
  Never: "Never";
566
569
  }>>;
567
570
  imagePullSecretName: z.ZodOptional<z.ZodString>;
571
+ kubeContext: z.ZodOptional<z.ZodString>;
568
572
  kubeconfigPath: z.ZodOptional<z.ZodString>;
569
573
  name: z.ZodOptional<z.ZodString>;
570
574
  namespace: z.ZodOptional<z.ZodString>;
@@ -694,6 +698,7 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
694
698
  Never: "Never";
695
699
  }>>;
696
700
  imagePullSecretName: z.ZodOptional<z.ZodString>;
701
+ kubeContext: z.ZodOptional<z.ZodString>;
697
702
  kubeconfigPath: z.ZodOptional<z.ZodString>;
698
703
  name: z.ZodOptional<z.ZodString>;
699
704
  namespace: z.ZodOptional<z.ZodString>;
@@ -66,6 +66,7 @@ const mokaSubmitBaseOptionsSchema = z.object({
66
66
  image: z.string().min(1).optional(),
67
67
  imagePullPolicy: imagePullPolicySchema,
68
68
  imagePullSecretName: z.string().min(1).optional(),
69
+ kubeContext: z.string().min(1).optional(),
69
70
  kubeconfigPath: z.string().min(1).optional(),
70
71
  name: z.string().min(1).optional(),
71
72
  namespace: z.string().min(1).optional(),
@@ -18,6 +18,7 @@ interface MokaWorkflowSubmitOptions {
18
18
  image?: string;
19
19
  imagePullPolicy?: "Always" | "IfNotPresent" | "Never";
20
20
  imagePullSecretName?: string;
21
+ kubeContext?: string;
21
22
  kubeconfigPath?: string;
22
23
  mcpGatewayAuth?: {
23
24
  secretKey?: string;
@@ -50,6 +50,7 @@ function workflowSubmitOptions(options) {
50
50
  image: options.image,
51
51
  imagePullPolicy: options.imagePullPolicy,
52
52
  imagePullSecretName: options.imagePullSecretName,
53
+ kubeContext: options.kubeContext,
53
54
  kubeconfigPath: options.kubeconfigPath,
54
55
  name: options.name,
55
56
  namespace: requireSubmitOption(options.namespace, "namespace"),
@@ -1,13 +1,12 @@
1
+ import "../../runtime/durable-store/postgres/migrate-substrate.js";
1
2
  import { DEFAULT_RUN_CONTROL_STALE_DETECTION, parseMokaNodeStatus, parseMokaRunController, parseMokaRunEvent, parseMokaRunManifest, parseMokaRunStatus, parseRunControlStaleDetection, parseRunEffort, parseRunMode, parseRunTarget } from "../contracts.js";
2
3
  import { publishScheduleManifest } from "../store-manifest.js";
3
4
  import { runControlEvent, runControlNodeArtifact, runControlNodeSession, runControlRun } from "./schema.js";
4
5
  import { Effect } from "effect";
5
- import { fileURLToPath } from "node:url";
6
- import { asc, eq, inArray } from "drizzle-orm";
7
6
  import { drizzle } from "drizzle-orm/postgres-js";
8
- import "drizzle-orm/postgres-js/migrator";
9
7
  import postgres from "postgres";
10
- fileURLToPath(new URL("../../runtime/durable-store/postgres/migrations", import.meta.url));
8
+ import { asc, eq, inArray } from "drizzle-orm";
9
+ //#region src/run-control/postgres/postgres-run-control-store.ts
11
10
  function openClient(dbUrl) {
12
11
  return postgres(dbUrl, { max: 1 });
13
12
  }
@@ -39,7 +38,7 @@ function createRun(db, input) {
39
38
  manifest,
40
39
  runId: manifest.runId
41
40
  }).onConflictDoNothing());
42
- const existing = yield* readRun(db, manifest.runId);
41
+ const existing = yield* loadBaseManifest(db, manifest.runId);
43
42
  if (existing === void 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`Run ${manifest.runId} not found after createRun upsert.`));
44
43
  return existing;
45
44
  });
@@ -1,4 +1,5 @@
1
1
  import { loadMokaDbUrl, requireMokaDbUrl } from "../moka-global-config.js";
2
+ import { migratePostgresSubstrate } from "../runtime/durable-store/postgres/migrate-substrate.js";
2
3
  import { postgresRunControlStore } from "./postgres/postgres-run-control-store.js";
3
4
  import "./store-paths.js";
4
5
  import "./store.js";
@@ -19,7 +20,10 @@ import { Effect } from "effect";
19
20
  * filesystem store holds no resources, so it is returned directly.
20
21
  */
21
22
  function resolveRunControlStore(dbUrl, _workspaceRoot) {
22
- return requireMokaDbUrl(dbUrl).pipe(Effect.flatMap((requiredDbUrl) => Effect.acquireRelease(Effect.sync(() => postgresRunControlStore(requiredDbUrl)), (store) => Effect.promise(() => store.close()))));
23
+ return requireMokaDbUrl(dbUrl).pipe(Effect.flatMap((requiredDbUrl) => Effect.tryPromise({
24
+ catch: (error) => error,
25
+ try: () => migratePostgresSubstrate(requiredDbUrl)
26
+ }).pipe(Effect.flatMap(() => Effect.acquireRelease(Effect.sync(() => postgresRunControlStore(requiredDbUrl)), (store) => Effect.promise(() => store.close()))))));
23
27
  }
24
28
  /**
25
29
  * PIPE-91.14: the single store-lifecycle wrapper shared by every run-control
@@ -1,9 +1,10 @@
1
1
  import { requireMokaDbUrl } from "../../moka-global-config.js";
2
+ import { migratePostgresSubstrate } from "./postgres/migrate-substrate.js";
2
3
  import { postgresDurableRunStore } from "./postgres/postgres-store.js";
3
4
  import { Effect } from "effect";
4
5
  //#region src/runtime/durable-store/acquisition.ts
5
6
  function resolveDurableStore(dbUrl, runId) {
6
- return requireMokaDbUrl(dbUrl).pipe(Effect.flatMap((requiredDbUrl) => Effect.acquireRelease(Effect.tryPromise(() => postgresDurableRunStore(requiredDbUrl, runId)), (store) => Effect.promise(() => store.close()))));
7
+ return requireMokaDbUrl(dbUrl).pipe(Effect.flatMap((requiredDbUrl) => Effect.tryPromise(() => migratePostgresSubstrate(requiredDbUrl)).pipe(Effect.flatMap(() => Effect.acquireRelease(Effect.tryPromise(() => postgresDurableRunStore(requiredDbUrl, runId)), (store) => Effect.promise(() => store.close()))))));
7
8
  }
8
9
  //#endregion
9
10
  export { resolveDurableStore };
@@ -0,0 +1,29 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { drizzle } from "drizzle-orm/postgres-js";
3
+ import { migrate } from "drizzle-orm/postgres-js/migrator";
4
+ import postgres from "postgres";
5
+ //#region src/runtime/durable-store/postgres/migrate-substrate.ts
6
+ const migrationsFolder = fileURLToPath(new URL("./migrations", import.meta.url));
7
+ const MIGRATION_LOCK_KEY = 8942017;
8
+ /**
9
+ * Apply the Drizzle migrations shared by the run-control and durable stores to
10
+ * `dbUrl`. Idempotent (Drizzle tracks applied migrations in
11
+ * `__drizzle_migrations` by content hash) and safe under concurrent callers
12
+ * (Postgres advisory lock serializes them). Opens and closes its own
13
+ * single-connection client.
14
+ */
15
+ async function migratePostgresSubstrate(dbUrl) {
16
+ const client = postgres(dbUrl, { max: 1 });
17
+ try {
18
+ await client`select pg_advisory_lock(${MIGRATION_LOCK_KEY}::bigint)`;
19
+ try {
20
+ await migrate(drizzle(client), { migrationsFolder });
21
+ } finally {
22
+ await client`select pg_advisory_unlock(${MIGRATION_LOCK_KEY}::bigint)`;
23
+ }
24
+ } finally {
25
+ await client.end();
26
+ }
27
+ }
28
+ //#endregion
29
+ export { migratePostgresSubstrate };
@@ -1,15 +1,13 @@
1
+ import "./migrate-substrate.js";
1
2
  import { createSerializedWriteQueue } from "../../../serialized-write-queue.js";
2
3
  import { buildRunJournal } from "../../run-journal.js";
3
4
  import { durableNodeRecord, durableRun } from "./schema.js";
4
- import { fileURLToPath } from "node:url";
5
- import { eq } from "drizzle-orm";
6
5
  import { drizzle } from "drizzle-orm/postgres-js";
7
- import "drizzle-orm/postgres-js/migrator";
8
6
  import postgres from "postgres";
7
+ import { eq } from "drizzle-orm";
9
8
  import pino from "pino";
10
9
  //#region src/runtime/durable-store/postgres/postgres-store.ts
11
10
  const logger = pino({ name: "postgres-durable-store" });
12
- fileURLToPath(new URL("./migrations", import.meta.url));
13
11
  function openClient(dbUrl) {
14
12
  return postgres(dbUrl, { max: 1 });
15
13
  }
@@ -62,6 +62,10 @@ function resolveKubeConfig(options, dependencies) {
62
62
  const kubeConfig = new KubeConfig();
63
63
  if (options.kubeconfigPath) kubeConfig.loadFromFile(options.kubeconfigPath);
64
64
  else kubeConfig.loadFromDefault();
65
+ if (options.kubeContext) {
66
+ if (!kubeConfig.getContextObject(options.kubeContext)) throw new Error(`Kube context '${options.kubeContext}' was not found in the resolved kubeconfig`);
67
+ kubeConfig.setCurrentContext(options.kubeContext);
68
+ }
65
69
  return kubeConfig;
66
70
  }
67
71
  function apiClients(options, dependencies) {
@@ -0,0 +1,112 @@
1
+ # ADR: Two-Stage `moka submit` — Schedule-Then-Dispatch
2
+
3
+ Status: Accepted
4
+
5
+ Date: 2026-07-01
6
+
7
+ ## Decision
8
+
9
+ `moka submit` is one flow with two stages, not two separate commands and not two
10
+ separate code paths:
11
+
12
+ 1. **Schedule** — research, planning, and graph generation, producing a
13
+ `ScheduleArtifact` and persisting it to the durable Postgres run-control
14
+ store before any Argo Workflow exists.
15
+ 2. **Dispatch** — compile the persisted (or explicitly supplied) schedule into
16
+ an Argo Workflow manifest and submit it, targeting whatever Kubernetes
17
+ cluster/context the operator configured.
18
+
19
+ Both stages run inside the single call chain:
20
+
21
+ ```
22
+ moka submit [args]
23
+ -> registerSubmitCommand() src/cli/program.ts:71
24
+ -> runMokaSubmitFromCli() src/cli/submit-options.ts:55
25
+ -> submitMoka() src/moka-submit.ts:238
26
+ -> submitParsedMoka() src/remote/submit/service.ts:47
27
+ - compileMokaSubmitPlan() src/remote/submit/compilation.ts:32
28
+ - upsertRunRecord() [Schedule -> DB]
29
+ - submitCompiledMokaWorkflow() src/remote/submit/argo-submission.ts:46 [Dispatch]
30
+ ```
31
+
32
+ There is exactly one WorkflowSpec builder chain (`buildRunnerArgoWorkflowManifest`
33
+ / `buildDynamicRunnerArgoWorkflowManifest` in `src/argo-workflow.ts`) and exactly
34
+ one Kubernetes/Argo client (`KubernetesArgoService` in
35
+ `src/runtime/services/kubernetes-argo-service.ts`). Static and dynamic
36
+ scheduling are a discriminated union (`dynamicScheduling: boolean` on
37
+ `CompiledMokaSubmitPlan`), not parallel implementations.
38
+
39
+ ## Stage 1 — Schedule (research -> planning -> graph, persisted)
40
+
41
+ When no `scheduleYaml`/`schedulePath` is supplied at submit time
42
+ (`dynamicScheduling: true`), the schedule is produced inside the runner pod by
43
+ three sequential phases, run via `moka runner-pre-schedule --phase <phase>`
44
+ (`src/runner-command/pre-schedule.ts`):
45
+
46
+ 1. `pre-research` — produces research context (findings, risks, target).
47
+ 2. `pre-planning` — consumes research, produces a `TicketPlan` (tickets +
48
+ dependencies), via `ticketPlanPlanningContext()`
49
+ (`src/planning/generate.ts:343`).
50
+ 3. `generate-schedule` — calls the configured planner profile to produce the
51
+ final `ScheduleArtifact`. If the planner fails or emits an invalid
52
+ artifact, `ticketPlanScheduleArtifact()` (`src/planning/generate.ts:497`)
53
+ deterministically derives a schedule directly from the `TicketPlan` graph
54
+ instead — this is the accepted fallback shape, not a patch to remove.
55
+
56
+ The result is written once via `publishSchedule()`
57
+ (`src/run-control/run-control-store.ts:60`) into the run's manifest
58
+ (`moka_run_control_run.manifest` JSONB). `publishSchedule` is idempotent for an
59
+ identical schedule and rejects a conflicting one for the same run
60
+ (`tests/run-control-store-contract.test.ts:423`).
61
+
62
+ ## Stage 2 — Dispatch (compile -> submit)
63
+
64
+ `submitParsedMoka()` (`src/remote/submit/service.ts:47`) upserts the run record
65
+ to Postgres **before** calling Argo, but a DB write failure never blocks
66
+ dispatch — the guard at `service.ts:56-61` logs and proceeds, because the
67
+ in-pod runner lifecycle (`src/runner-command/lifecycle.ts`) re-creates the same
68
+ run record idempotently once the pod starts. Postgres is a durability/inspection
69
+ substrate for `moka status`/`moka logs`/`moka resume`, not a precondition for
70
+ submission.
71
+
72
+ `submitCompiledMokaWorkflow()` then calls `submitRunnerArgoWorkflow()` (static,
73
+ schedule already known) or `submitDynamicRunnerArgoWorkflow()` (dynamic,
74
+ schedule generated in-pod) — both in `src/argo-submit.ts`, both routed through
75
+ the same `KubernetesArgoService`.
76
+
77
+ ## Cluster targeting is one seam, not per-caller
78
+
79
+ Kubernetes context/kubeconfig resolution is a single function,
80
+ `resolveKubeConfig()` (`src/runtime/services/kubernetes-argo-service.ts`), used
81
+ by every Argo/K8s call (`createConfigMap`, `createWorkflow`,
82
+ `getWorkflowPhase`). Precedence, highest wins:
83
+
84
+ 1. `--kube-context <name>` / `--kubeconfig <path>` CLI flags
85
+ (`src/cli/submit-options.ts`)
86
+ 2. `momokaya.kubernetes.context` / `momokaya.kubernetes.kubeconfig` in
87
+ `~/.config/moka/config.yaml` (`src/moka-global-config.ts`)
88
+ 3. The kubeconfig's own `current-context` (`KubeConfig.loadFromDefault()`)
89
+
90
+ An unresolvable `kubeContext` fails loud at resolution time (`kubeConfig.
91
+ getContextObject()` check) rather than silently falling through to whatever
92
+ context happened to be current — this is what makes "point submit at momokaya
93
+ or at a local orbstack context" a config change, never a code change. Prior to
94
+ this ADR, `kubeContext` existed for the `kubectl`-shellout polling path
95
+ (`cluster-doctor.ts`) but not for the actual submission API calls; that gap is
96
+ closed as part of this decision.
97
+
98
+ ## Consequences
99
+
100
+ - Do not add a second "generate the graph" path or a second Argo client.
101
+ Static vs. dynamic scheduling is a data flag on `CompiledMokaSubmitPlan`, not
102
+ a reason to fork the builder or the service.
103
+ - Do not gate submission on the DB write succeeding — the guard in
104
+ `service.ts` is intentional, not a missing try/catch to "fix".
105
+ - Any new cluster-targeting option (a third context source, a per-namespace
106
+ override) is added to `resolveKubeConfig`'s precedence list, not threaded
107
+ ad hoc through individual call sites.
108
+
109
+ ## References
110
+
111
+ - Run-control persistence model: `./run-control.md`
112
+ - PIPE-94 durable substrate: `oisin-ee` backlog PIPE-94.3/94.4/94.5/94.8
package/package.json CHANGED
@@ -132,7 +132,7 @@
132
132
  "prepack": "nub run build:cli"
133
133
  },
134
134
  "type": "module",
135
- "version": "3.19.4",
135
+ "version": "3.19.5",
136
136
  "description": "Config-driven multi-agent pipeline runner for repository work",
137
137
  "main": "./dist/index.js",
138
138
  "types": "./dist/index.d.ts",