@oisincoveney/pipeline 3.22.0 → 3.23.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 (46) hide show
  1. package/README.md +17 -17
  2. package/dist/argo-submit.d.ts +56 -0
  3. package/dist/argo-submit.js +205 -66
  4. package/dist/argo-workflow.d.ts +22 -0
  5. package/dist/commands/runner-command-command.js +1 -1
  6. package/dist/factory/create-experiment.js +2 -1
  7. package/dist/factory/exec.js +1 -0
  8. package/dist/factory/git-credentials.js +37 -0
  9. package/dist/factory/template-update.js +5 -1
  10. package/dist/loop/controller-deps.js +8 -3
  11. package/dist/loop/controller.js +1 -0
  12. package/dist/moka-submit.d.ts +140 -0
  13. package/dist/moka-submit.js +5 -2
  14. package/dist/planning/generate.d.ts +1 -0
  15. package/dist/planning/generate.js +5 -2
  16. package/dist/remote/argo/model.d.ts +30 -1
  17. package/dist/remote/argo/model.js +36 -14
  18. package/dist/remote/argo/templates.js +6 -2
  19. package/dist/remote/submit/argo-submission.d.ts +4 -0
  20. package/dist/remote/submit/argo-submission.js +4 -1
  21. package/dist/remote/submit/compilation.js +14 -0
  22. package/dist/run-control/postgres/schema.js +7 -7
  23. package/dist/run-control/runtime-event-projection.js +1 -0
  24. package/dist/runner-command/finalize.js +65 -4
  25. package/dist/runner-command-contract.d.ts +9 -1
  26. package/dist/runner-command-contract.js +11 -1
  27. package/dist/runner-event-schema.d.ts +26 -0
  28. package/dist/runner-event-schema.js +10 -0
  29. package/dist/runner-event-sink.js +7 -5
  30. package/dist/runtime/contracts/contracts.d.ts +6 -0
  31. package/dist/runtime/durable-store/postgres/migrate-substrate.js +112 -5
  32. package/dist/runtime/durable-store/postgres/postgres-store.js +1 -1
  33. package/dist/runtime/durable-store/postgres/schema.js +8 -5
  34. package/dist/runtime/open-pull-request/open-pull-request.js +30 -4
  35. package/dist/runtime/services/kubernetes-argo-service.d.ts +5 -1
  36. package/dist/runtime/services/kubernetes-argo-service.js +22 -1
  37. package/dist/runtime/services/runner-command-io-service.js +11 -2
  38. package/dist/runtime/services/runner-event-sink-http-service.js +7 -1
  39. package/dist/schedule/passes/open-pull-request.js +4 -1
  40. package/dist/tickets/backlog-task-store.d.ts +41 -1
  41. package/dist/tickets/ticket-graph-dto.d.ts +60 -0
  42. package/dist/tickets/ticket-graph.d.ts +20 -1
  43. package/docs/adr-moka-postgres-schema-isolation.md +58 -0
  44. package/docs/operator-guide.md +26 -28
  45. package/docs/pipeline-console-runner-contract.md +19 -10
  46. package/package.json +5 -1
package/README.md CHANGED
@@ -44,12 +44,12 @@ Initialize package-owned pipeline support:
44
44
  moka init
45
45
  ```
46
46
 
47
- `moka init` installs or refreshes the whole per-machine harness in one step:
48
- the package's default skills, generated host command surfaces, the singleton
49
- `pipeline-gateway` MCP entry, copied hook files from private
50
- `oisin-ee/agent/hooks`, and global instruction files from `oisin-ee/agent/rules`.
51
- OpenCode is the package default runtime. The command does not create repo-local
52
- `.pipeline` config files.
47
+ `moka init` installs or refreshes Moka-owned host adapters: `/moka-quick`,
48
+ `/moka-execute`, and `/moka-inspect` command surfaces, native-agent projections,
49
+ and the singleton `pipeline-gateway` MCP host config. It does not install the
50
+ shared agent harness; skills, hooks, rules, and Claude settings come from
51
+ `oisin-ee/agent` via chezmoi. OpenCode is the package default runtime. The
52
+ command does not create repo-local `.pipeline` config files.
53
53
 
54
54
  The default MCP gateway can run locally or point at the hosted Momokaya gateway.
55
55
  Set `PIPELINE_MCP_GATEWAY_AUTHORIZATION` to the full HTTP `Authorization` header
@@ -59,14 +59,14 @@ value before starting OpenCode when using a protected gateway:
59
59
  export PIPELINE_MCP_GATEWAY_AUTHORIZATION="Basic $(printf '%s' 'user:password' | base64)"
60
60
  ```
61
61
 
62
- Verify the generated harness (commands, hooks, rules) is current after package
63
- upgrades or edits to `oisin-ee/agent`, without writing anything:
62
+ Verify generated Moka host adapters are current after package upgrades, without
63
+ writing anything:
64
64
 
65
65
  ```shell
66
66
  moka init --check
67
67
  ```
68
68
 
69
- Refresh it, overwriting any locally edited harness files:
69
+ Refresh generated Moka host adapters, overwriting locally edited adapter files:
70
70
 
71
71
  ```shell
72
72
  moka init --force
@@ -107,10 +107,10 @@ Canonical commands:
107
107
  - `moka stop <run-id> [node-id]`: abort a run or one active node.
108
108
  - `moka export <run-id> --sanitize`: print a portable evidence bundle.
109
109
  - `moka doctor`: check local prerequisites and config health.
110
- - `moka init`: install or refresh the whole per-machine harness (skills,
111
- command surfaces, hooks, rules). `--check` verifies without writing,
112
- `--dry-run` previews, `--force` overwrites locally edited files. The harness
113
- is always installed globally; there is no `--scope`.
110
+ - `moka init`: install or refresh Moka-owned host adapters (command surfaces,
111
+ native-agent projections, and gateway config). `--check` verifies without
112
+ writing, `--dry-run` previews, `--force` overwrites locally edited adapter
113
+ files. The shared agent harness is installed separately by chezmoi.
114
114
 
115
115
  ```shell
116
116
  moka run "Implement PIPE-123 user-facing behavior"
@@ -197,10 +197,10 @@ For a compact command reference, see
197
197
  ## Momokaya Runner Image
198
198
 
199
199
  The package is also the runner code used by the Momokaya runner image. The
200
- control plane owns Argo Workflow submission, run listing, cancellation, event
201
- storage, Kueue discovery, and UI rendering. This package owns the in-container
202
- `moka runner-command` and `moka runner-finalize` commands used by Argo Workflow
203
- DAG tasks.
200
+ control plane owns Argo Workflow submission, status observation, run listing,
201
+ cancellation, event storage, and UI rendering. This package owns the
202
+ in-container `moka runner-command` and `moka runner-finalize` commands used by
203
+ Argo Workflow DAG tasks.
204
204
 
205
205
  Argo starts the image with payload, schedule, and per-task descriptor files
206
206
  mounted from ConfigMaps, plus the event auth token mounted from a Secret. The
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
 
6
6
  //#region src/argo-submit.d.ts
7
7
  declare const submitRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
8
+ activeDeadlineSeconds: z.ZodOptional<z.ZodNumber>;
8
9
  brokerAuth: z.ZodObject<{
9
10
  secretKey: z.ZodDefault<z.ZodString>;
10
11
  secretName: z.ZodString;
@@ -36,10 +37,38 @@ declare const submitRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
36
37
  namespace: z.ZodString;
37
38
  npmRegistryAuthSecretName: z.ZodOptional<z.ZodString>;
38
39
  payloadJson: z.ZodString;
40
+ podGC: z.ZodOptional<z.ZodObject<{
41
+ deleteDelayDuration: z.ZodOptional<z.ZodString>;
42
+ labelSelector: z.ZodOptional<z.ZodObject<{
43
+ matchExpressions: z.ZodOptional<z.ZodArray<z.ZodObject<{
44
+ key: z.ZodString;
45
+ operator: z.ZodEnum<{
46
+ DoesNotExist: "DoesNotExist";
47
+ Exists: "Exists";
48
+ In: "In";
49
+ NotIn: "NotIn";
50
+ }>;
51
+ values: z.ZodOptional<z.ZodArray<z.ZodString>>;
52
+ }, z.core.$strict>>>;
53
+ matchLabels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
54
+ }, z.core.$strict>>;
55
+ strategy: z.ZodOptional<z.ZodEnum<{
56
+ OnPodCompletion: "OnPodCompletion";
57
+ OnPodSuccess: "OnPodSuccess";
58
+ OnWorkflowCompletion: "OnWorkflowCompletion";
59
+ OnWorkflowSuccess: "OnWorkflowSuccess";
60
+ }>>;
61
+ }, z.core.$strict>>;
39
62
  serviceAccountName: z.ZodOptional<z.ZodString>;
63
+ ttlStrategy: z.ZodOptional<z.ZodObject<{
64
+ secondsAfterCompletion: z.ZodOptional<z.ZodNumber>;
65
+ secondsAfterFailure: z.ZodOptional<z.ZodNumber>;
66
+ secondsAfterSuccess: z.ZodOptional<z.ZodNumber>;
67
+ }, z.core.$strict>>;
40
68
  scheduleYaml: z.ZodString;
41
69
  }, z.core.$strict>;
42
70
  declare const submitDynamicRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
71
+ activeDeadlineSeconds: z.ZodOptional<z.ZodNumber>;
43
72
  brokerAuth: z.ZodObject<{
44
73
  secretKey: z.ZodDefault<z.ZodString>;
45
74
  secretName: z.ZodString;
@@ -71,7 +100,34 @@ declare const submitDynamicRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
71
100
  namespace: z.ZodString;
72
101
  npmRegistryAuthSecretName: z.ZodOptional<z.ZodString>;
73
102
  payloadJson: z.ZodString;
103
+ podGC: z.ZodOptional<z.ZodObject<{
104
+ deleteDelayDuration: z.ZodOptional<z.ZodString>;
105
+ labelSelector: z.ZodOptional<z.ZodObject<{
106
+ matchExpressions: z.ZodOptional<z.ZodArray<z.ZodObject<{
107
+ key: z.ZodString;
108
+ operator: z.ZodEnum<{
109
+ DoesNotExist: "DoesNotExist";
110
+ Exists: "Exists";
111
+ In: "In";
112
+ NotIn: "NotIn";
113
+ }>;
114
+ values: z.ZodOptional<z.ZodArray<z.ZodString>>;
115
+ }, z.core.$strict>>>;
116
+ matchLabels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
117
+ }, z.core.$strict>>;
118
+ strategy: z.ZodOptional<z.ZodEnum<{
119
+ OnPodCompletion: "OnPodCompletion";
120
+ OnPodSuccess: "OnPodSuccess";
121
+ OnWorkflowCompletion: "OnWorkflowCompletion";
122
+ OnWorkflowSuccess: "OnWorkflowSuccess";
123
+ }>>;
124
+ }, z.core.$strict>>;
74
125
  serviceAccountName: z.ZodOptional<z.ZodString>;
126
+ ttlStrategy: z.ZodOptional<z.ZodObject<{
127
+ secondsAfterCompletion: z.ZodOptional<z.ZodNumber>;
128
+ secondsAfterFailure: z.ZodOptional<z.ZodNumber>;
129
+ secondsAfterSuccess: z.ZodOptional<z.ZodNumber>;
130
+ }, z.core.$strict>>;
75
131
  workflowId: z.ZodString;
76
132
  }, z.core.$strict>;
77
133
  declare const commandScheduleOptionsSchema: z.ZodObject<{
@@ -1,5 +1,5 @@
1
1
  import { ArgoGraphCompilerError, compileArgoExecutionGraph } from "./argo-graph.js";
2
- import { dbAuthOptionSchema, mcpGatewayAuthOptionSchema } from "./remote/argo/model.js";
2
+ import { argoWorkflowActiveDeadlineSecondsSchema, argoWorkflowPodGcSchema, argoWorkflowTtlStrategySchema, dbAuthOptionSchema, mcpGatewayAuthOptionSchema } from "./remote/argo/model.js";
3
3
  import { brokerAuthOptionSchema } from "./credentials/broker.js";
4
4
  import { appendPullRequestDelivery } from "./schedule/passes/open-pull-request.js";
5
5
  import { compileScheduleArtifact, parseScheduleArtifact } from "./planning/generate.js";
@@ -25,11 +25,19 @@ const configMapSchema = z.object({
25
25
  namespace: z.string().min(1)
26
26
  }).strict()
27
27
  }).strict();
28
+ const workflowOwnerReferenceSchema = z.object({
29
+ apiVersion: z.literal("argoproj.io/v1alpha1"),
30
+ kind: z.literal("Workflow"),
31
+ name: z.string().min(1),
32
+ uid: z.string().min(1)
33
+ }).strict();
34
+ const configMapOwnerReferencesPatchSchema = z.object({ metadata: z.object({ ownerReferences: z.tuple([workflowOwnerReferenceSchema]) }).strict() }).strict();
28
35
  const createdWorkflowSchema = z.object({ metadata: z.object({
29
36
  name: z.string().min(1).optional(),
30
37
  uid: z.string().min(1).optional()
31
38
  }).passthrough() }).passthrough();
32
39
  const submitRunnerArgoWorkflowBaseOptionShape = {
40
+ activeDeadlineSeconds: argoWorkflowActiveDeadlineSecondsSchema.optional(),
33
41
  brokerAuth: brokerAuthOptionSchema,
34
42
  dbAuth: dbAuthOptionSchema.optional(),
35
43
  eventAuthSecretKey: z.string().min(1).optional(),
@@ -51,7 +59,9 @@ const submitRunnerArgoWorkflowBaseOptionShape = {
51
59
  namespace: z.string().min(1),
52
60
  npmRegistryAuthSecretName: z.string().min(1).optional(),
53
61
  payloadJson: z.string().min(1),
54
- serviceAccountName: z.string().min(1).optional()
62
+ podGC: argoWorkflowPodGcSchema.optional(),
63
+ serviceAccountName: z.string().min(1).optional(),
64
+ ttlStrategy: argoWorkflowTtlStrategySchema.optional()
55
65
  };
56
66
  const submitRunnerArgoWorkflowOptionsSchema = z.object({
57
67
  ...submitRunnerArgoWorkflowBaseOptionShape,
@@ -102,6 +112,7 @@ function submitRunnerArgoWorkflowEffect(rawOptions, dependencies) {
102
112
  "pipeline.oisin.dev/ticket-project": payload.run.project,
103
113
  "pipeline.oisin.dev/ticket-title": payload.task.title
104
114
  } : {},
115
+ activeDeadlineSeconds: options.activeDeadlineSeconds,
105
116
  brokerAuth: options.brokerAuth,
106
117
  dbAuth: options.dbAuth,
107
118
  mcpGatewayAuth: options.mcpGatewayAuth,
@@ -121,51 +132,21 @@ function submitRunnerArgoWorkflowEffect(rawOptions, dependencies) {
121
132
  plan: compiled.plan,
122
133
  scheduleConfigMapName: scheduleArtifactConfigMapName,
123
134
  serviceAccountName: options.serviceAccountName,
124
- taskDescriptorConfigMapName
135
+ taskDescriptorConfigMapName,
136
+ ttlStrategy: options.ttlStrategy
125
137
  });
126
138
  return Effect.gen(function* () {
127
- const service = yield* KubernetesArgoService;
128
139
  const graph = yield* graphEffect;
129
- yield* service.createConfigMap({
130
- body: configMapSchema.parse({
131
- apiVersion: "v1",
132
- data: { "payload.json": payloadJson },
133
- kind: "ConfigMap",
134
- metadata: {
135
- labels,
136
- name: payloadConfigMapName,
137
- namespace: options.namespace
138
- }
139
- }),
140
- dependencies,
141
- namespace: options.namespace,
142
- options
143
- });
144
- yield* service.createConfigMap({
145
- body: configMapSchema.parse({
146
- apiVersion: "v1",
147
- data: Object.fromEntries(graph.tasks.map((task) => [`${task.taskName}.json`, `${JSON.stringify(buildRunnerTaskDescriptor(task.nodeId))}\n`])),
148
- kind: "ConfigMap",
149
- metadata: {
150
- labels,
151
- name: taskDescriptorConfigMapName,
152
- namespace: options.namespace
153
- }
154
- }),
155
- dependencies,
156
- namespace: options.namespace,
157
- options
158
- });
159
- yield* service.createConfigMap({
160
- body: configMapSchema.parse({
161
- apiVersion: "v1",
162
- data: { "schedule.yaml": options.scheduleYaml },
163
- kind: "ConfigMap",
164
- metadata: {
165
- labels,
166
- name: scheduleArtifactConfigMapName,
167
- namespace: options.namespace
168
- }
140
+ const createdConfigMapNames = yield* createRunConfigMaps({
141
+ configMaps: staticRunConfigMaps({
142
+ labels,
143
+ namespace: options.namespace,
144
+ payloadConfigMapName,
145
+ payloadJson,
146
+ scheduleConfigMapName: scheduleArtifactConfigMapName,
147
+ scheduleYaml: options.scheduleYaml,
148
+ taskDescriptorConfigMapName,
149
+ tasks: graph.tasks
169
150
  }),
170
151
  dependencies,
171
152
  namespace: options.namespace,
@@ -180,8 +161,21 @@ function submitRunnerArgoWorkflowEffect(rawOptions, dependencies) {
180
161
  scheduleConfigMapName: scheduleArtifactConfigMapName,
181
162
  taskDescriptorConfigMapName
182
163
  },
183
- workflow
184
- });
164
+ workflow,
165
+ workflowFieldOverrides: { podGC: options.podGC }
166
+ }).pipe(Effect.flatMap((result) => ownRunConfigMaps({
167
+ configMapNames: createdConfigMapNames,
168
+ dependencies,
169
+ namespace: options.namespace,
170
+ options,
171
+ result
172
+ })), Effect.catch((error) => cleanupRunConfigMapsOnFailure({
173
+ configMapNames: createdConfigMapNames,
174
+ dependencies,
175
+ error,
176
+ namespace: options.namespace,
177
+ options
178
+ })));
185
179
  });
186
180
  }
187
181
  function submitDynamicRunnerArgoWorkflowEffect(rawOptions, dependencies) {
@@ -199,12 +193,14 @@ function submitDynamicRunnerArgoWorkflowEffect(rawOptions, dependencies) {
199
193
  "pipeline.oisin.dev/source": "argo-workflow",
200
194
  "pipeline.oisin.dev/workflow": options.workflowId
201
195
  };
196
+ const annotations = payload.task.kind === "ticket" ? {
197
+ "pipeline.oisin.dev/ticket-id": payload.task.id,
198
+ "pipeline.oisin.dev/ticket-project": payload.run.project,
199
+ "pipeline.oisin.dev/ticket-title": payload.task.title
200
+ } : {};
202
201
  const workflow = buildDynamicRunnerArgoWorkflowManifest({
203
- annotations: payload.task.kind === "ticket" ? {
204
- "pipeline.oisin.dev/ticket-id": payload.task.id,
205
- "pipeline.oisin.dev/ticket-project": payload.run.project,
206
- "pipeline.oisin.dev/ticket-title": payload.task.title
207
- } : {},
202
+ activeDeadlineSeconds: options.activeDeadlineSeconds,
203
+ annotations,
208
204
  brokerAuth: options.brokerAuth,
209
205
  dbAuth: options.dbAuth,
210
206
  eventAuthSecretKey: options.eventAuthSecretKey,
@@ -222,19 +218,16 @@ function submitDynamicRunnerArgoWorkflowEffect(rawOptions, dependencies) {
222
218
  npmRegistryAuthSecretName: options.npmRegistryAuthSecretName,
223
219
  payloadConfigMapName,
224
220
  serviceAccountName: options.serviceAccountName,
221
+ ttlStrategy: options.ttlStrategy,
225
222
  workflowId: options.workflowId
226
223
  });
227
224
  return Effect.gen(function* () {
228
- yield* (yield* KubernetesArgoService).createConfigMap({
229
- body: configMapSchema.parse({
230
- apiVersion: "v1",
231
- data: { "payload.json": payloadJson },
232
- kind: "ConfigMap",
233
- metadata: {
234
- labels,
235
- name: payloadConfigMapName,
236
- namespace: options.namespace
237
- }
225
+ const createdConfigMapNames = yield* createRunConfigMaps({
226
+ configMaps: dynamicRunConfigMaps({
227
+ labels,
228
+ namespace: options.namespace,
229
+ payloadConfigMapName,
230
+ payloadJson
238
231
  }),
239
232
  dependencies,
240
233
  namespace: options.namespace,
@@ -245,23 +238,169 @@ function submitDynamicRunnerArgoWorkflowEffect(rawOptions, dependencies) {
245
238
  namespace: options.namespace,
246
239
  options,
247
240
  resultExtras: { payloadConfigMapName },
248
- workflow
249
- });
241
+ workflow,
242
+ workflowFieldOverrides: { podGC: options.podGC }
243
+ }).pipe(Effect.flatMap((result) => ownRunConfigMaps({
244
+ configMapNames: createdConfigMapNames,
245
+ dependencies,
246
+ namespace: options.namespace,
247
+ options,
248
+ result
249
+ })), Effect.catch((error) => cleanupRunConfigMapsOnFailure({
250
+ configMapNames: createdConfigMapNames,
251
+ dependencies,
252
+ error,
253
+ namespace: options.namespace,
254
+ options
255
+ })));
250
256
  });
251
257
  }
258
+ function staticRunConfigMaps(input) {
259
+ return [
260
+ runConfigMap({
261
+ data: { "payload.json": input.payloadJson },
262
+ labels: input.labels,
263
+ name: input.payloadConfigMapName,
264
+ namespace: input.namespace
265
+ }),
266
+ runConfigMap({
267
+ data: Object.fromEntries(input.tasks.map((task) => [`${task.taskName}.json`, `${JSON.stringify(buildRunnerTaskDescriptor(task.nodeId))}\n`])),
268
+ labels: input.labels,
269
+ name: input.taskDescriptorConfigMapName,
270
+ namespace: input.namespace
271
+ }),
272
+ runConfigMap({
273
+ data: { "schedule.yaml": input.scheduleYaml },
274
+ labels: input.labels,
275
+ name: input.scheduleConfigMapName,
276
+ namespace: input.namespace
277
+ })
278
+ ];
279
+ }
280
+ function dynamicRunConfigMaps(input) {
281
+ return [runConfigMap({
282
+ data: { "payload.json": input.payloadJson },
283
+ labels: input.labels,
284
+ name: input.payloadConfigMapName,
285
+ namespace: input.namespace
286
+ })];
287
+ }
288
+ function runConfigMap(input) {
289
+ return {
290
+ body: configMapSchema.parse({
291
+ apiVersion: "v1",
292
+ data: input.data,
293
+ kind: "ConfigMap",
294
+ metadata: {
295
+ labels: input.labels,
296
+ name: input.name,
297
+ namespace: input.namespace
298
+ }
299
+ }),
300
+ name: input.name
301
+ };
302
+ }
303
+ function createRunConfigMaps(input) {
304
+ const createdConfigMapNames = [];
305
+ return Effect.gen(function* () {
306
+ const service = yield* KubernetesArgoService;
307
+ yield* Effect.forEach(input.configMaps, (configMap) => service.createConfigMap({
308
+ body: configMap.body,
309
+ dependencies: input.dependencies,
310
+ namespace: input.namespace,
311
+ options: input.options
312
+ }).pipe(Effect.tap(() => Effect.sync(() => {
313
+ createdConfigMapNames.push(configMap.name);
314
+ }))));
315
+ return createdConfigMapNames;
316
+ }).pipe(Effect.catch((error) => cleanupRunConfigMapsOnFailure({
317
+ configMapNames: createdConfigMapNames,
318
+ dependencies: input.dependencies,
319
+ error,
320
+ namespace: input.namespace,
321
+ options: input.options
322
+ })));
323
+ }
324
+ function ownRunConfigMaps(input) {
325
+ return patchRunConfigMapOwnerReferences({
326
+ body: configMapOwnerReferencesPatch(workflowOwnerReference(input.result)),
327
+ configMapNames: input.configMapNames,
328
+ dependencies: input.dependencies,
329
+ namespace: input.namespace,
330
+ options: input.options
331
+ }).pipe(Effect.as(input.result));
332
+ }
333
+ function cleanupRunConfigMapsOnFailure(input) {
334
+ return deleteRunConfigMaps({
335
+ configMapNames: input.configMapNames,
336
+ dependencies: input.dependencies,
337
+ namespace: input.namespace,
338
+ options: input.options
339
+ }).pipe(Effect.catch((cleanupError) => Effect.fail(/* @__PURE__ */ new Error(`Failed to clean up ConfigMaps after submit failure: ${errorMessage(input.error)}; cleanup failed: ${errorMessage(cleanupError)}`))), Effect.flatMap(() => Effect.fail(input.error)));
340
+ }
341
+ function patchRunConfigMapOwnerReferences(input) {
342
+ return Effect.gen(function* () {
343
+ const service = yield* KubernetesArgoService;
344
+ return yield* Effect.all(input.configMapNames.map((name) => service.patchConfigMapOwnerReferences({
345
+ body: input.body,
346
+ dependencies: input.dependencies,
347
+ name,
348
+ namespace: input.namespace,
349
+ options: input.options
350
+ })), { concurrency: "unbounded" });
351
+ });
352
+ }
353
+ function deleteRunConfigMaps(input) {
354
+ return Effect.gen(function* () {
355
+ const service = yield* KubernetesArgoService;
356
+ return yield* Effect.all(input.configMapNames.map((name) => service.deleteConfigMap({
357
+ dependencies: input.dependencies,
358
+ name,
359
+ namespace: input.namespace,
360
+ options: input.options
361
+ })), { concurrency: "unbounded" });
362
+ });
363
+ }
364
+ function workflowOwnerReference(result) {
365
+ if (result.workflowUid === void 0) throw new Error(`Created Argo Workflow '${result.workflowName}' did not include metadata.uid; cannot own ConfigMaps`);
366
+ return workflowOwnerReferenceSchema.parse({
367
+ apiVersion: "argoproj.io/v1alpha1",
368
+ kind: "Workflow",
369
+ name: result.workflowName,
370
+ uid: result.workflowUid
371
+ });
372
+ }
373
+ function configMapOwnerReferencesPatch(ownerReference) {
374
+ return configMapOwnerReferencesPatchSchema.parse({ metadata: { ownerReferences: [ownerReference] } });
375
+ }
376
+ function errorMessage(error) {
377
+ return error instanceof Error ? error.message : String(error);
378
+ }
252
379
  function submitWorkflowManifest(input) {
253
380
  return Effect.gen(function* () {
254
- return workflowSubmitResult(yield* (yield* KubernetesArgoService).createWorkflow({
255
- body: runnerArgoWorkflowManifestSchema.parse(input.workflow),
381
+ const service = yield* KubernetesArgoService;
382
+ const workflow = applyWorkflowFieldOverrides(input.workflow, input.workflowFieldOverrides);
383
+ return workflowSubmitResult(yield* service.createWorkflow({
384
+ body: runnerArgoWorkflowManifestSchema.parse(workflow),
256
385
  dependencies: input.dependencies,
257
386
  namespace: input.namespace,
258
387
  options: input.options
259
- }), input.workflow, {
388
+ }), workflow, {
260
389
  namespace: input.namespace,
261
390
  ...input.resultExtras
262
391
  });
263
392
  });
264
393
  }
394
+ function applyWorkflowFieldOverrides(workflow, overrides) {
395
+ if (overrides?.podGC === void 0) return workflow;
396
+ return runnerArgoWorkflowManifestSchema.parse({
397
+ ...workflow,
398
+ spec: {
399
+ ...workflow.spec,
400
+ podGC: overrides.podGC
401
+ }
402
+ });
403
+ }
265
404
  function workflowSubmitResult(response, workflow, base) {
266
405
  const created = createdWorkflowSchema.parse(response);
267
406
  return workflowSubmitResultSchema.parse({
@@ -126,6 +126,28 @@ declare const runnerArgoWorkflowManifestSchema: z.ZodObject<{
126
126
  }>;
127
127
  }, z.core.$strict>>;
128
128
  }, z.core.$strict>>;
129
+ podGC: z.ZodOptional<z.ZodObject<{
130
+ deleteDelayDuration: z.ZodOptional<z.ZodString>;
131
+ labelSelector: z.ZodOptional<z.ZodObject<{
132
+ matchExpressions: z.ZodOptional<z.ZodArray<z.ZodObject<{
133
+ key: z.ZodString;
134
+ operator: z.ZodEnum<{
135
+ DoesNotExist: "DoesNotExist";
136
+ Exists: "Exists";
137
+ In: "In";
138
+ NotIn: "NotIn";
139
+ }>;
140
+ values: z.ZodOptional<z.ZodArray<z.ZodString>>;
141
+ }, z.core.$strict>>>;
142
+ matchLabels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
143
+ }, z.core.$strict>>;
144
+ strategy: z.ZodOptional<z.ZodEnum<{
145
+ OnPodCompletion: "OnPodCompletion";
146
+ OnPodSuccess: "OnPodSuccess";
147
+ OnWorkflowCompletion: "OnWorkflowCompletion";
148
+ OnWorkflowSuccess: "OnWorkflowSuccess";
149
+ }>>;
150
+ }, z.core.$strict>>;
129
151
  ttlStrategy: z.ZodOptional<z.ZodObject<{
130
152
  secondsAfterCompletion: z.ZodOptional<z.ZodNumber>;
131
153
  secondsAfterFailure: z.ZodOptional<z.ZodNumber>;
@@ -51,7 +51,7 @@ function registerRunnerCommandCommand(program) {
51
51
  program.command("runner-command").description("Run one scheduled Argo Workflow task").requiredOption("--payload-file <path>", "Path to the runner payload JSON").option("--node-id <id>", "Node id to execute without a task descriptor").option("--schedule-file <path>", "Path to the schedule artifact YAML").option("--schedule-source <source>", "Schedule source: file or db").action((options) => runRunnerProgram(runRunnerCommandEffect(options)));
52
52
  program.command("runner-lifecycle").description("Run one Argo Workflow lifecycle phase").requiredOption("--phase <phase>", "Lifecycle phase to run").requiredOption("--payload-file <path>", "Path to the runner payload JSON").requiredOption("--schedule-file <path>", "Path to the schedule artifact YAML").action((options) => runRunnerProgram(runRunnerLifecycleEffect(options)));
53
53
  program.command("runner-pre-schedule").description("Run one dynamic pre-schedule phase").requiredOption("--phase <phase>", "pre-research, pre-planning, or generate-schedule").requiredOption("--payload-file <path>", "Path to the runner payload JSON").action((options) => runRunnerProgram(runPreScheduleEffect(options)));
54
- program.command("runner-finalize").description("Finalize one Argo Workflow run").requiredOption("--payload-file <path>", "Path to the runner payload JSON").option("--schedule-file <path>", "Path to the schedule artifact YAML").option("--schedule-source <source>", "Schedule source: file or db").requiredOption("--argo-status <status>", "Argo Workflow status").action((options) => runRunnerProgram(runRunnerFinalizeEffect(options)));
54
+ program.command("runner-finalize").description("Finalize one Argo Workflow run").requiredOption("--payload-file <path>", "Path to the runner payload JSON").option("--schedule-file <path>", "Path to the schedule artifact YAML").option("--schedule-source <source>", "Schedule source: file or db").requiredOption("--argo-status <status>", "Argo Workflow status").option("--argo-failures <json>", "Argo Workflow failure details JSON").action((options) => runRunnerProgram(runRunnerFinalizeEffect(options)));
55
55
  program.command("runner-select-ready-wave").description("Select DB-ready nodes for the next dynamic Argo wave").requiredOption("--payload-file <path>", "Path to the runner payload JSON").requiredOption("--output-file <path>", "Path where the ready node id JSON array is written").action((options) => runRunnerProgram(runSelectReadyWaveEffect(options)));
56
56
  }
57
57
  //#endregion
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_RUNNER_COMMAND_GIT_COMMITTER } from "../config/schema/catalog.js";
2
2
  import { resolveFactorySeams } from "./exec.js";
3
+ import { githubGitCredentialEnv } from "./git-credentials.js";
3
4
  import { existsSync, mkdtempSync } from "node:fs";
4
5
  import { tmpdir } from "node:os";
5
6
  import { join, resolve } from "node:path";
@@ -80,7 +81,7 @@ async function runCreateExperiment(options) {
80
81
  previews,
81
82
  ...options.templateRef ? { templateRef: options.templateRef } : {},
82
83
  templateSource
83
- }));
84
+ }), { env: githubGitCredentialEnv() });
84
85
  const stampedRegistryEntry = join(stampDir, STAMPED_REGISTRY_DIR, `${name}.yaml`);
85
86
  if (!existsSync(stampedRegistryEntry)) throw new Error(`create-experiment: stamp is missing the registry entry ${STAMPED_REGISTRY_DIR}/${name}.yaml — template contract changed?`);
86
87
  log("create-experiment: committing the stamped tree");
@@ -3,6 +3,7 @@ import { execa } from "execa";
3
3
  //#region src/factory/exec.ts
4
4
  const defaultFactoryExec = (command, args, options) => execa(command, [...args], {
5
5
  ...options?.cwd ? { cwd: options.cwd } : {},
6
+ ...options?.env ? { env: options.env } : {},
6
7
  stdin: "ignore"
7
8
  });
8
9
  const defaultFactoryGit = (cwd, args) => runAuthenticatedGit(cwd, args);
@@ -0,0 +1,37 @@
1
+ import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ //#region src/factory/git-credentials.ts
5
+ const DEFAULT_GIT_CREDENTIALS_DIR = "/etc/pipeline/git-credentials";
6
+ /**
7
+ * copier fetches the (private) momokaya-template by spawning its OWN git
8
+ * subprocess (`git ls-remote` / `clone`, via plumbum). `runAuthenticatedGit`
9
+ * only configures credentials per-invocation with `-c` flags on the git calls
10
+ * the lane makes itself, so copier's git sees no github.com credential and
11
+ * dies `fatal: could not read Username for 'https://github.com'`.
12
+ *
13
+ * This materializes a process-wide github.com credential from the runner's
14
+ * mounted git-credentials (username + PAT under /etc/pipeline/git-credentials)
15
+ * as `GIT_CONFIG_*` env any child (and grandchild) git process inherits, so
16
+ * copier's template fetch authenticates. The token is written to a 0600 store
17
+ * file and referenced by a `credential.helper=store` entry — it never lands in
18
+ * a URL or argv. No mounted credentials (local dev) -> empty env, so git falls
19
+ * back to ambient auth.
20
+ */
21
+ function githubGitCredentialEnv(credentialsDir = process.env.PIPELINE_GIT_CREDENTIALS_DIR ?? DEFAULT_GIT_CREDENTIALS_DIR) {
22
+ const usernamePath = resolve(credentialsDir, "username");
23
+ const passwordPath = resolve(credentialsDir, "password");
24
+ if (!(existsSync(usernamePath) && existsSync(passwordPath))) return {};
25
+ const username = readFileSync(usernamePath, "utf8").trim();
26
+ const password = readFileSync(passwordPath, "utf8").trim();
27
+ const storePath = join(mkdtempSync(join(tmpdir(), "factory-git-cred-")), ".git-credentials");
28
+ writeFileSync(storePath, `https://${encodeURIComponent(username)}:${encodeURIComponent(password)}@github.com\n`, { mode: 384 });
29
+ return {
30
+ GIT_CONFIG_COUNT: "1",
31
+ GIT_CONFIG_KEY_0: "credential.helper",
32
+ GIT_CONFIG_VALUE_0: `store --file=${storePath}`,
33
+ GIT_TERMINAL_PROMPT: "0"
34
+ };
35
+ }
36
+ //#endregion
37
+ export { githubGitCredentialEnv };
@@ -1,4 +1,5 @@
1
1
  import { resolveFactorySeams } from "./exec.js";
2
+ import { githubGitCredentialEnv } from "./git-credentials.js";
2
3
  import { committerConfigArgs } from "./create-experiment.js";
3
4
  import { isStampOf, parseCopierAnswers } from "./stamp-answers.js";
4
5
  import { parse } from "yaml";
@@ -101,7 +102,10 @@ async function updateRepo(input) {
101
102
  "--trust",
102
103
  "--defaults",
103
104
  ...input.templateRef ? ["--vcs-ref", input.templateRef] : []
104
- ], { cwd: cloneDir });
105
+ ], {
106
+ cwd: cloneDir,
107
+ env: githubGitCredentialEnv()
108
+ });
105
109
  const status = await git(cloneDir, ["status", "--porcelain"]);
106
110
  if (status.trim().length === 0) return {
107
111
  repo,
@@ -1,5 +1,6 @@
1
1
  import { runAuthenticatedGit } from "../run-state/git-refs.js";
2
2
  import { RunnerEventSinkHttpService, RunnerEventSinkHttpServiceLive } from "../runtime/services/runner-event-sink-http-service.js";
3
+ import { RUNNER_EVENT_SINK_RETRY_POLICY } from "../runner-event-sink.js";
3
4
  import { pollWorkflowPhaseUntilTerminal } from "./argo-poll.js";
4
5
  import { ticketGraphDtoSchema } from "../tickets/ticket-graph-dto.js";
5
6
  import { submitMoka } from "../moka-submit.js";
@@ -39,6 +40,7 @@ function buildControllerDeps(context, seams = {}) {
39
40
  runId: input.runId,
40
41
  workflowName: input.workflowName
41
42
  }),
43
+ projectId: context.project,
42
44
  refreshBacklog: () => Effect.tryPromise({
43
45
  catch: refreshError,
44
46
  try: () => gitRefresh(context.worktreePath)
@@ -130,7 +132,10 @@ function submitRepository(context, request) {
130
132
  const RECORD_BUILDER = {
131
133
  "loop.start": (event, envelope) => event.type === "loop.start" ? {
132
134
  ...envelope,
133
- loopStart: { strategy: event.strategy },
135
+ loopStart: {
136
+ projectId: event.projectId,
137
+ strategy: event.strategy
138
+ },
134
139
  type: "loop.start"
135
140
  } : unreachable(event),
136
141
  "loop.graph.snapshot": (event, envelope) => event.type === "loop.graph.snapshot" ? {
@@ -183,8 +188,8 @@ function defaultPostEvent(context) {
183
188
  authToken: context.eventAuthToken,
184
189
  events: [record],
185
190
  fetch: fetchImpl,
186
- maxRetries: 0,
187
- retryDelayMs: 250,
191
+ maxRetries: RUNNER_EVENT_SINK_RETRY_POLICY.maxRetries,
192
+ retryDelayMs: RUNNER_EVENT_SINK_RETRY_POLICY.retryDelayMs,
188
193
  url: context.eventUrl
189
194
  })), RunnerEventSinkHttpServiceLive));
190
195
  }
@@ -17,6 +17,7 @@ function runLoopController(deps) {
17
17
  const graph = yield* buildTicketGraphEffect([...initialTasks]).pipe(Effect.mapError((error) => new Error(error.message)));
18
18
  yield* deps.emit({
19
19
  type: "loop.start",
20
+ projectId: deps.projectId,
20
21
  strategy: deps.strategy
21
22
  });
22
23
  const snapshot = yield* serializeTicketGraph(graph);