@tea-agent/loop-agent 0.16.1-beta.2 → 0.16.2

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 (113) hide show
  1. package/AGENTS.md +4 -8
  2. package/CHANGELOG.md +76 -18
  3. package/README.md +76 -299
  4. package/dist/application/evaluation/alias.js +184 -0
  5. package/dist/application/evaluation/budget.js +192 -0
  6. package/dist/application/evaluation/campaign-hash.js +47 -0
  7. package/dist/application/evaluation/campaign-matrix.js +372 -0
  8. package/dist/application/evaluation/campaign-scorecard.js +135 -0
  9. package/dist/application/evaluation/campaign.js +370 -0
  10. package/dist/application/evaluation/candidate.js +23 -6
  11. package/dist/application/evaluation/corpus-hash.js +38 -0
  12. package/dist/application/evaluation/corpus.js +56 -0
  13. package/dist/application/evaluation/experiment.js +294 -0
  14. package/dist/application/evaluation/ignition.js +198 -0
  15. package/dist/application/evaluation/integrity-audit.js +162 -0
  16. package/dist/application/evaluation/outer-loop.js +132 -0
  17. package/dist/application/evaluation/pi-cell-executor.js +39 -0
  18. package/dist/application/evaluation/private-verifier.js +46 -0
  19. package/dist/application/evaluation/promotion-policy.js +151 -0
  20. package/dist/application/evaluation/proposer.js +98 -0
  21. package/dist/application/evaluation/types.js +522 -0
  22. package/dist/cli/command-definitions.js +19 -3
  23. package/dist/commands/dag-reconcile-run.js +3 -116
  24. package/dist/commands/eval.js +1176 -13
  25. package/dist/commands/init.js +7 -1
  26. package/dist/executors/dag-pi-executor.js +8 -46
  27. package/dist/executors/pi-sdk-executor.js +66 -3
  28. package/dist/executors/shell-executor.js +213 -30
  29. package/dist/executors/shell-presets.js +12 -2
  30. package/dist/executors/shell-write-guard.js +20 -1
  31. package/dist/infrastructure/evaluation/alias-store.js +199 -0
  32. package/dist/infrastructure/evaluation/campaign-store.js +154 -0
  33. package/dist/infrastructure/evaluation/corpus-store.js +181 -0
  34. package/dist/infrastructure/evaluation/experiment-store.js +124 -0
  35. package/dist/infrastructure/evaluation/ignition-store.js +82 -0
  36. package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
  37. package/dist/infrastructure/evaluation/proposer-store.js +78 -0
  38. package/dist/records/promotion.js +3 -1
  39. package/dist/shared/git-progress.js +9 -2
  40. package/dist/worker/cli.js +83 -0
  41. package/dist/worker/delivery/git-transaction.js +75 -0
  42. package/dist/worker/delivery/verification-bundle.js +13 -2
  43. package/dist/worker/feature/review.js +3 -2
  44. package/dist/worker/observability/read-model.js +56 -0
  45. package/dist/worker/observe/server.js +6 -3
  46. package/dist/worker/observe/static/dag-helpers.js +0 -62
  47. package/dist/worker/observe/static/styles.css +18 -55
  48. package/dist/worker/observe/static/views/dag.js +13 -5
  49. package/dist/worker/outcomes/adapters.js +4 -1
  50. package/dist/worker/outcomes/declared-artifacts.js +103 -0
  51. package/dist/worker/outcomes/evidence-tokens.js +29 -0
  52. package/dist/worker/outcomes/gate.js +10 -11
  53. package/dist/worker/outcomes/projector.js +30 -4
  54. package/dist/worker/outcomes/types.js +3 -0
  55. package/dist/worker/pool/reconcile.js +285 -0
  56. package/dist/worker/run-task/run-task.js +81 -4
  57. package/dist/worker/runner/run-ready.js +25 -2
  58. package/dist/worker/task-graph/ready-planner.js +14 -8
  59. package/dist/worker/task-graph/task-graph-schema.js +5 -3
  60. package/dist/workflows/dag/backend-test-analysis-contract.js +87 -30
  61. package/dist/workflows/dag/backend-test-case-manifest.js +71 -8
  62. package/dist/workflows/dag/backend-test-execution-contract.js +63 -11
  63. package/dist/workflows/dag/backend-test-repair-contract.js +94 -0
  64. package/dist/workflows/dag/backend-test-result-contract.js +6 -4
  65. package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
  66. package/dist/workflows/dag/budget-enforcement.js +67 -0
  67. package/dist/workflows/dag/context-policy.js +137 -0
  68. package/dist/workflows/dag/dynamic-runtime/condition.js +1 -1
  69. package/dist/workflows/dag/dynamic-runtime/shared.js +42 -0
  70. package/dist/workflows/dag/failure-routing.js +8 -1
  71. package/dist/workflows/dag/frontend-implementation-contract.js +32 -93
  72. package/dist/workflows/dag/init-hybrid.js +624 -172
  73. package/dist/workflows/dag/knowledge-curator.js +3 -0
  74. package/dist/workflows/dag/lifecycle.js +33 -2
  75. package/dist/workflows/dag/node-execution.js +11 -4
  76. package/dist/workflows/dag/prompt.js +1 -1
  77. package/dist/workflows/dag/reconcile-run.js +121 -0
  78. package/dist/workflows/dag/report.js +12 -0
  79. package/dist/workflows/dag/runner.js +43 -16
  80. package/dist/workflows/dag/scheduler.js +87 -17
  81. package/dist/workflows/dag/skill-snapshot.js +11 -7
  82. package/dist/workflows/dag/types.js +49 -1
  83. package/dist/workflows/dag/validate.js +35 -15
  84. package/docs/README.md +3 -1
  85. package/docs/architecture/runtime-boundaries.md +3 -2
  86. package/docs/init-surface.manifest.json +4 -0
  87. package/docs/local-development-environment.md +52 -0
  88. package/docs/templates/agent-dag.schema.json +25 -7
  89. package/docs/templates/agent-dag.supervised-implementation.json +23 -4
  90. package/docs/templates/backend-test-analysis.schema.json +9 -16
  91. package/docs/templates/backend-test-dag.json +493 -197
  92. package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -4
  93. package/docs/templates/backend-test-execution.schema.json +6 -1
  94. package/docs/templates/branch-merge-report.md +14 -0
  95. package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
  96. package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
  97. package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
  98. package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
  99. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
  100. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
  101. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
  102. package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
  103. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
  104. package/docs/templates/product-line/AGENTS.md +1 -0
  105. package/docs/templates/product-line/README.md +17 -0
  106. package/docs/templates/product-line/acceptance.yaml +9 -0
  107. package/docs/templates/product-line/feature.yaml +11 -0
  108. package/docs/templates/product-line/task-graph.yaml +8 -0
  109. package/docs/templates/product-line/task.yaml +4 -0
  110. package/package.json +2 -1
  111. package/skills/frontend-implementation/references/node-contracts.md +3 -3
  112. package/skills/loop-agent/references/command-reference.md +5 -0
  113. package/skills/loop-agent/references/hybrid-dag.md +7 -4
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { campaignBudgetSchema } from "./budget.js";
2
3
  export const evalSplitSchema = z.enum(["public", "private", "held_out"]);
3
4
  export const replayEvidenceRefSchema = z
4
5
  .object({
@@ -128,3 +129,524 @@ export const lifecycleEventSchema = z
128
129
  eventHash: z.string().regex(/^[a-f0-9]{64}$/),
129
130
  })
130
131
  .strict();
132
+ // --- Incumbent Alias + Promotion Decision (M2 W2.4) ---
133
+ export const aliasNameSchema = z
134
+ .string()
135
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "alias must be one safe path segment");
136
+ export const incumbentAliasSchema = z
137
+ .object({
138
+ schemaVersion: z.literal(1),
139
+ alias: aliasNameSchema,
140
+ candidateId: candidateIdSchema,
141
+ bundleHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
142
+ updatedAt: z.string().min(1),
143
+ updatedByDecisionId: z.string().min(1),
144
+ previousCandidateId: candidateIdSchema.nullable(),
145
+ })
146
+ .strict();
147
+ export const aliasHistoryEventSchema = z
148
+ .object({
149
+ schemaVersion: z.literal(1),
150
+ seq: z.number().int().positive(),
151
+ decisionId: z.string().min(1),
152
+ action: z.enum(["promote", "rollback"]),
153
+ fromCandidateId: candidateIdSchema.nullable(),
154
+ toCandidateId: candidateIdSchema,
155
+ toBundleHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
156
+ at: z.string().min(1),
157
+ })
158
+ .strict();
159
+ export const promotionDecisionSchema = z
160
+ .object({
161
+ schemaVersion: z.literal(1),
162
+ decisionId: z.string().min(1),
163
+ alias: aliasNameSchema,
164
+ action: z.enum(["promote", "rollback"]),
165
+ fromCandidateId: candidateIdSchema.nullable(),
166
+ toCandidateId: candidateIdSchema,
167
+ toBundleHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
168
+ reason: z.string().min(1),
169
+ actor: z.literal("human"),
170
+ humanRequired: z.literal(true),
171
+ applied: z.boolean(),
172
+ createdAt: z.string().min(1),
173
+ campaignId: z.string().min(1).nullable().optional(),
174
+ })
175
+ .strict();
176
+ // --- Corpus Manifest (W1.5) ---
177
+ export const corpusIdSchema = z
178
+ .string()
179
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "corpusId must be one safe path segment");
180
+ export const corpusTaskClassSchema = z.enum([
181
+ "docs",
182
+ "bugfix",
183
+ "implement",
184
+ "frontend",
185
+ "backend-test",
186
+ "fullstack",
187
+ "bootstrap",
188
+ "refactor",
189
+ ]);
190
+ export const corpusTaskSchema = z
191
+ .object({
192
+ taskRef: z
193
+ .string()
194
+ .min(1)
195
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/, "taskRef must be a safe relative id (letters, numbers, . _ - /)")
196
+ .refine((value) => !value.includes(".."), "taskRef must not contain .."),
197
+ class: corpusTaskClassSchema,
198
+ split: evalSplitSchema,
199
+ seeds: z
200
+ .array(z.number().int().nonnegative())
201
+ .min(2)
202
+ .max(3)
203
+ .superRefine((seeds, ctx) => {
204
+ const seen = new Set();
205
+ for (let i = 0; i < seeds.length; i += 1) {
206
+ const seed = seeds[i];
207
+ if (seen.has(seed)) {
208
+ ctx.addIssue({
209
+ code: z.ZodIssueCode.custom,
210
+ message: `duplicate seed ${seed}`,
211
+ path: [i],
212
+ });
213
+ }
214
+ seen.add(seed);
215
+ }
216
+ }),
217
+ description: z.string().optional(),
218
+ featureId: z.string().min(1).optional(),
219
+ })
220
+ .strict();
221
+ function refineCorpusTasks(manifest, ctx) {
222
+ const refs = new Set();
223
+ const classes = new Set();
224
+ const splits = new Set();
225
+ for (let i = 0; i < manifest.tasks.length; i += 1) {
226
+ const task = manifest.tasks[i];
227
+ if (refs.has(task.taskRef)) {
228
+ ctx.addIssue({
229
+ code: z.ZodIssueCode.custom,
230
+ message: `duplicate taskRef: ${task.taskRef}`,
231
+ path: ["tasks", i, "taskRef"],
232
+ });
233
+ }
234
+ refs.add(task.taskRef);
235
+ classes.add(task.class);
236
+ splits.add(task.split);
237
+ }
238
+ if (classes.size < 3) {
239
+ ctx.addIssue({
240
+ code: z.ZodIssueCode.custom,
241
+ message: "corpus requires at least 3 distinct task classes",
242
+ path: ["tasks"],
243
+ });
244
+ }
245
+ for (const required of ["public", "private", "held_out"]) {
246
+ if (!splits.has(required)) {
247
+ ctx.addIssue({
248
+ code: z.ZodIssueCode.custom,
249
+ message: `corpus requires at least one ${required} task`,
250
+ path: ["tasks"],
251
+ });
252
+ }
253
+ }
254
+ }
255
+ const corpusManifestObjectSchema = z
256
+ .object({
257
+ schemaVersion: z.literal(1),
258
+ corpusId: corpusIdSchema,
259
+ createdAt: z.string().min(1),
260
+ description: z.string().optional(),
261
+ tasks: z.array(corpusTaskSchema).min(12).max(20),
262
+ corpusHash: z
263
+ .string()
264
+ .regex(/^(sha256:)?[a-f0-9]{64}$/i)
265
+ .optional(),
266
+ })
267
+ .strict();
268
+ export const corpusManifestInputSchema = corpusManifestObjectSchema.superRefine(refineCorpusTasks);
269
+ export const corpusManifestSchema = z
270
+ .object({
271
+ schemaVersion: z.literal(1),
272
+ corpusId: corpusIdSchema,
273
+ createdAt: z.string().min(1),
274
+ description: z.string().optional(),
275
+ tasks: z.array(corpusTaskSchema).min(12).max(20),
276
+ corpusHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
277
+ })
278
+ .strict()
279
+ .superRefine(refineCorpusTasks);
280
+ // --- Private Verifier + Campaign (M4) ---
281
+ export const privateVerifierIdSchema = z
282
+ .string()
283
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "privateVerifierId must be one safe path segment");
284
+ export const privateVerifierCheckSchema = z
285
+ .object({
286
+ checkId: z
287
+ .string()
288
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "checkId must be a safe id"),
289
+ kind: z.literal("shell"),
290
+ command: z.array(z.string().min(1)).min(1),
291
+ cwd: z.string().min(1).default("."),
292
+ timeoutMs: z.number().int().positive().max(3_600_000).default(60_000),
293
+ expectedExitCode: z.number().int().default(0),
294
+ })
295
+ .strict();
296
+ export const privateVerifierManifestInputSchema = z
297
+ .object({
298
+ schemaVersion: z.literal(1),
299
+ privateVerifierId: privateVerifierIdSchema,
300
+ createdAt: z.string().min(1),
301
+ description: z.string().optional(),
302
+ checks: z.array(privateVerifierCheckSchema).min(1),
303
+ manifestHash: z
304
+ .string()
305
+ .regex(/^(sha256:)?[a-f0-9]{64}$/i)
306
+ .optional(),
307
+ })
308
+ .strict();
309
+ export const privateVerifierManifestSchema = privateVerifierManifestInputSchema
310
+ .extend({
311
+ manifestHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
312
+ })
313
+ .strict();
314
+ export const campaignIdSchema = z
315
+ .string()
316
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "campaignId must be one safe path segment");
317
+ export const campaignStatusSchema = z.enum([
318
+ "prepared",
319
+ "running",
320
+ "completed",
321
+ "invalid",
322
+ "aborted",
323
+ ]);
324
+ export const campaignEvaluatorIntegritySchema = z
325
+ .object({
326
+ corpusHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
327
+ privateVerifierManifestHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
328
+ budgetHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
329
+ scoringRulesHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
330
+ })
331
+ .strict();
332
+ function refineCampaignCandidates(manifest, ctx) {
333
+ if (manifest.budget.mode !== "hard") {
334
+ ctx.addIssue({
335
+ code: z.ZodIssueCode.custom,
336
+ message: "campaign budget.mode must be hard (record-only campaigns cannot enter promotion path)",
337
+ path: ["budget", "mode"],
338
+ });
339
+ }
340
+ if (manifest.challengerCandidateIds.includes(manifest.incumbentCandidateId)) {
341
+ ctx.addIssue({
342
+ code: z.ZodIssueCode.custom,
343
+ message: "challengerCandidateIds must not include incumbentCandidateId",
344
+ path: ["challengerCandidateIds"],
345
+ });
346
+ }
347
+ const seen = new Set();
348
+ for (let i = 0; i < manifest.challengerCandidateIds.length; i += 1) {
349
+ const id = manifest.challengerCandidateIds[i];
350
+ if (seen.has(id)) {
351
+ ctx.addIssue({
352
+ code: z.ZodIssueCode.custom,
353
+ message: `duplicate challengerCandidateId: ${id}`,
354
+ path: ["challengerCandidateIds", i],
355
+ });
356
+ }
357
+ seen.add(id);
358
+ }
359
+ }
360
+ const campaignManifestObjectSchema = z
361
+ .object({
362
+ schemaVersion: z.literal(1),
363
+ campaignId: campaignIdSchema,
364
+ createdAt: z.string().min(1),
365
+ status: campaignStatusSchema.default("prepared"),
366
+ controllerFingerprint: z.string().min(1).optional(),
367
+ policyVersion: z.string().min(1).default("promotion-policy-v1"),
368
+ incumbentCandidateId: candidateIdSchema,
369
+ challengerCandidateIds: z.array(candidateIdSchema).min(1),
370
+ corpusId: corpusIdSchema,
371
+ corpusHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
372
+ privateVerifierId: privateVerifierIdSchema,
373
+ privateVerifierManifestHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
374
+ budget: campaignBudgetSchema,
375
+ seeds: z.array(z.number().int().nonnegative()).min(1).optional(),
376
+ notes: z.string().optional(),
377
+ evaluatorIntegrity: campaignEvaluatorIntegritySchema.optional(),
378
+ })
379
+ .strict();
380
+ export const campaignManifestInputSchema = campaignManifestObjectSchema.superRefine(refineCampaignCandidates);
381
+ export const campaignManifestSchema = z
382
+ .object({
383
+ schemaVersion: z.literal(1),
384
+ campaignId: campaignIdSchema,
385
+ createdAt: z.string().min(1),
386
+ status: campaignStatusSchema,
387
+ controllerFingerprint: z.string().min(1).optional(),
388
+ policyVersion: z.string().min(1),
389
+ incumbentCandidateId: candidateIdSchema,
390
+ challengerCandidateIds: z.array(candidateIdSchema).min(1),
391
+ corpusId: corpusIdSchema,
392
+ corpusHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
393
+ privateVerifierId: privateVerifierIdSchema,
394
+ privateVerifierManifestHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
395
+ budget: campaignBudgetSchema,
396
+ seeds: z.array(z.number().int().nonnegative()).min(1).optional(),
397
+ notes: z.string().optional(),
398
+ evaluatorIntegrity: campaignEvaluatorIntegritySchema,
399
+ })
400
+ .strict()
401
+ .superRefine(refineCampaignCandidates);
402
+ export const campaignCellExecutionRoleSchema = z.enum([
403
+ "candidate_run",
404
+ "private_verify",
405
+ "promotion_gate_only",
406
+ ]);
407
+ export const campaignPlanCellSchema = z
408
+ .object({
409
+ cellId: z.string().min(1),
410
+ candidateId: candidateIdSchema,
411
+ bundleHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
412
+ taskRef: z.string().min(1),
413
+ seed: z.number().int().nonnegative(),
414
+ split: evalSplitSchema,
415
+ executionRole: campaignCellExecutionRoleSchema,
416
+ /** Controller-only flag; never serialized into candidate DAG prompts. */
417
+ privateVerifierAttached: z.boolean(),
418
+ })
419
+ .strict();
420
+ export const campaignPlanSchema = z
421
+ .object({
422
+ schemaVersion: z.literal(1),
423
+ campaignId: campaignIdSchema,
424
+ planHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
425
+ createdAt: z.string().min(1),
426
+ cells: z.array(campaignPlanCellSchema).min(1),
427
+ })
428
+ .strict();
429
+ // --- Campaign Evidence + Promotion Policy (M5) ---
430
+ export const campaignEvidenceCellSchema = z
431
+ .object({
432
+ cellId: z.string().min(1).optional(),
433
+ candidateId: candidateIdSchema,
434
+ taskRef: z.string().min(1),
435
+ seed: z.number().int().nonnegative(),
436
+ split: evalSplitSchema,
437
+ verifyPassed: z.boolean(),
438
+ runId: z.string().min(1).optional(),
439
+ score: z.number().optional(),
440
+ })
441
+ .strict();
442
+ export const campaignEvidenceDiffSchema = z
443
+ .object({
444
+ path: z.string().min(1),
445
+ unifiedDiff: z.string(),
446
+ })
447
+ .strict();
448
+ export const campaignEvidenceSchema = z
449
+ .object({
450
+ schemaVersion: z.literal(1),
451
+ campaignId: campaignIdSchema,
452
+ challengerCandidateId: candidateIdSchema,
453
+ cells: z.array(campaignEvidenceCellSchema).min(1),
454
+ diffs: z.array(campaignEvidenceDiffSchema).default([]),
455
+ verifyCommands: z
456
+ .object({
457
+ incumbent: z.array(z.string().min(1)).default([]),
458
+ challenger: z.array(z.string().min(1)).default([]),
459
+ })
460
+ .strict()
461
+ .default({ incumbent: [], challenger: [] }),
462
+ budgetStatus: z
463
+ .object({
464
+ incumbent: z.enum(["ok", "breached", "missing"]),
465
+ challenger: z.enum(["ok", "breached", "missing"]),
466
+ })
467
+ .strict()
468
+ .default({ incumbent: "missing", challenger: "missing" }),
469
+ })
470
+ .strict();
471
+ // --- Learned Guidance Experiment (M6) ---
472
+ export const experimentIdSchema = z
473
+ .string()
474
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "experimentId must be one safe path segment");
475
+ export const experimentPatternEvidenceSchema = z
476
+ .object({
477
+ key: z.string().min(1),
478
+ failureClass: z.string().min(1),
479
+ fixScopeShape: z.string().min(1),
480
+ invariant: z.string().min(1),
481
+ count: z.number().int().positive(),
482
+ sourceRunIds: z.array(z.string().min(1)).default([]),
483
+ whenToApply: z.string().min(1),
484
+ whenNotToApply: z.string().min(1),
485
+ })
486
+ .strict();
487
+ export const experimentDispositionSchema = z
488
+ .object({
489
+ status: z.enum(["accepted", "rejected"]),
490
+ at: z.string().min(1),
491
+ reason: z.string().min(1),
492
+ campaignId: campaignIdSchema.optional(),
493
+ scorecardPlanHash: z.string().min(1).optional(),
494
+ heldOutNonRegression: z
495
+ .enum(["pass", "fail", "insufficient", "review"])
496
+ .optional(),
497
+ })
498
+ .strict();
499
+ export const experimentRecordSchema = z
500
+ .object({
501
+ schemaVersion: z.literal(1),
502
+ experimentId: experimentIdSchema,
503
+ candidateId: candidateIdSchema,
504
+ source: z.literal("knowledge-curate"),
505
+ createdAt: z.string().min(1),
506
+ proposalPath: z.string().min(1),
507
+ proposalSha256: z.string().regex(/^sha256:[a-f0-9]{64}$/),
508
+ patterns: z.array(experimentPatternEvidenceSchema).default([]),
509
+ status: z.enum(["experimenting", "accepted", "rejected"]),
510
+ disposition: experimentDispositionSchema.optional(),
511
+ })
512
+ .strict();
513
+ // --- Outer-loop Proposer + Candidate Matrix (M7) ---
514
+ export const proposerRunIdSchema = z
515
+ .string()
516
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "proposerRunId must be one safe path segment");
517
+ export const proposerProposalSchema = z
518
+ .object({
519
+ candidateId: candidateIdSchema,
520
+ experimentId: experimentIdSchema,
521
+ proposalPath: z.string().min(1),
522
+ bundleHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
523
+ status: z.literal("proposed_experimenting"),
524
+ })
525
+ .strict();
526
+ export const proposerRunSchema = z
527
+ .object({
528
+ schemaVersion: z.literal(1),
529
+ proposerRunId: proposerRunIdSchema,
530
+ source: z.literal("knowledge-curate"),
531
+ createdAt: z.string().min(1),
532
+ maxCandidates: z.number().int().positive(),
533
+ proposals: z.array(proposerProposalSchema),
534
+ skippedReasons: z.array(z.string().min(1)).default([]),
535
+ /** Always false: proposer must never promote aliases. */
536
+ autoPromote: z.literal(false),
537
+ })
538
+ .strict();
539
+ export const matrixModeSchema = z.enum([
540
+ "dry-run",
541
+ "stub",
542
+ "pi-plan",
543
+ "pi",
544
+ ]);
545
+ export const matrixCellResultSchema = z
546
+ .object({
547
+ cellId: z.string().min(1),
548
+ candidateId: candidateIdSchema,
549
+ taskRef: z.string().min(1),
550
+ seed: z.number().int().nonnegative(),
551
+ split: evalSplitSchema,
552
+ executionRole: z.enum([
553
+ "candidate_run",
554
+ "private_verify",
555
+ "promotion_gate_only",
556
+ ]),
557
+ mode: matrixModeSchema,
558
+ verifyPassed: z.boolean().optional(),
559
+ runId: z.string().min(1).optional(),
560
+ jobPath: z.string().min(1).optional(),
561
+ detail: z.string().optional(),
562
+ skipped: z.boolean().default(false),
563
+ })
564
+ .strict();
565
+ export const matrixRunSchema = z
566
+ .object({
567
+ schemaVersion: z.literal(1),
568
+ campaignId: campaignIdSchema,
569
+ planHash: z.string().min(1),
570
+ mode: matrixModeSchema,
571
+ createdAt: z.string().min(1),
572
+ cells: z.array(matrixCellResultSchema),
573
+ privateCheckCount: z.number().int().nonnegative(),
574
+ privateCheckFailed: z.number().int().nonnegative(),
575
+ evidencePath: z.string().min(1).optional(),
576
+ /** Always false: matrix must never promote aliases. */
577
+ autoPromote: z.literal(false),
578
+ })
579
+ .strict();
580
+ export const matrixFixtureSchema = z
581
+ .object({
582
+ schemaVersion: z.literal(1),
583
+ defaultVerifyPassed: z.boolean().default(false),
584
+ results: z
585
+ .array(z
586
+ .object({
587
+ cellId: z.string().min(1).optional(),
588
+ candidateId: candidateIdSchema.optional(),
589
+ taskRef: z.string().min(1).optional(),
590
+ seed: z.number().int().nonnegative().optional(),
591
+ split: evalSplitSchema.optional(),
592
+ verifyPassed: z.boolean(),
593
+ runId: z.string().min(1).optional(),
594
+ })
595
+ .strict())
596
+ .default([]),
597
+ })
598
+ .strict();
599
+ // --- Ignition research gate (M8) ---
600
+ export const ignitionIdSchema = z
601
+ .string()
602
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "ignitionId must be one safe path segment");
603
+ export const ignitionGenerationSchema = z
604
+ .object({
605
+ generation: z.number().int().nonnegative(),
606
+ parentCandidateId: candidateIdSchema.nullable(),
607
+ proposerRunId: proposerRunIdSchema,
608
+ proposedCandidateIds: z.array(candidateIdSchema).default([]),
609
+ proposalCount: z.number().int().nonnegative(),
610
+ campaignId: campaignIdSchema.optional(),
611
+ promotionEligible: z.boolean().optional(),
612
+ heldOutNonRegression: z
613
+ .enum(["pass", "fail", "insufficient", "review"])
614
+ .optional(),
615
+ privateImprovement: z
616
+ .enum(["pass", "fail", "insufficient", "review"])
617
+ .optional(),
618
+ recordedAt: z.string().min(1),
619
+ })
620
+ .strict();
621
+ export const ignitionVerdictSchema = z.enum([
622
+ "insufficient_evidence",
623
+ "trend_positive",
624
+ "trend_flat",
625
+ "trend_negative",
626
+ "inconclusive",
627
+ ]);
628
+ export const ignitionReportSchema = z
629
+ .object({
630
+ schemaVersion: z.literal(1),
631
+ ignitionId: ignitionIdSchema,
632
+ createdAt: z.string().min(1),
633
+ updatedAt: z.string().min(1),
634
+ generations: z.array(ignitionGenerationSchema).default([]),
635
+ metrics: z
636
+ .object({
637
+ generationCount: z.number().int().nonnegative(),
638
+ meanProposalsPerGeneration: z.number().nonnegative(),
639
+ proposalYieldDelta: z.number().nullable(),
640
+ scorecardObservedRate: z.number().nullable(),
641
+ minGenerationsForSignal: z.number().int().positive(),
642
+ sampleSufficient: z.boolean(),
643
+ })
644
+ .strict(),
645
+ verdict: ignitionVerdictSchema,
646
+ reasons: z.array(z.string().min(1)).default([]),
647
+ /** Research gate only — never authorizes product RSI Level 1 claims. */
648
+ rsiLevel1ClaimAllowed: z.literal(false),
649
+ /** Always false — ignition never promotes. */
650
+ autoPromote: z.literal(false),
651
+ })
652
+ .strict();
@@ -77,7 +77,23 @@ const INIT_SUBCOMMANDS = [
77
77
  "update",
78
78
  ];
79
79
  const EXAMPLES_SUBCOMMANDS = ["list", "show", "copy"];
80
- const EVAL_SUBCOMMANDS = ["replay", "report", "candidate"];
80
+ const EVAL_SUBCOMMANDS = [
81
+ "replay",
82
+ "report",
83
+ "candidate",
84
+ "alias",
85
+ "promote",
86
+ "rollback",
87
+ "corpus",
88
+ "context-policy",
89
+ "budget",
90
+ "private-verifier",
91
+ "campaign",
92
+ "experiment",
93
+ "propose",
94
+ "outer-loop",
95
+ "ignition",
96
+ ];
81
97
  const CLOSEOUT_SUBCOMMANDS = ["task"];
82
98
  const PLAN_SUBCOMMANDS = ["list", "create", "complete", "check"];
83
99
  const SPINE_SUBCOMMANDS = ["audit"];
@@ -200,8 +216,8 @@ export const COMMAND_DEFINITIONS = [
200
216
  name: "eval",
201
217
  adapter: "required",
202
218
  tier: "operator",
203
- intent: "Replay completed DAG evidence and manage immutable Candidate Registry lifecycle without live model execution or promotion.",
204
- usage: "eval <replay|report|candidate> ...; candidate <register|show|list|transition> [--json|--markdown]",
219
+ intent: "Replay completed DAG evidence, manage Candidate Registry and corpus contracts, and human-gate incumbent alias promote/rollback without live campaigns.",
220
+ usage: "eval <…|propose|outer-loop|ignition> ...; propose from-curate; outer-loop run; campaign matrix; ignition <record|evaluate|show|list>; promote remains human-gated (no auto-promote; ignition never claims RSI Level 1)",
205
221
  subcommands: [...EVAL_SUBCOMMANDS],
206
222
  handler: async ({ repoRoot, subcommand, rest }) => {
207
223
  await runEval(repoRoot, [subcommand, ...rest].filter((arg) => Boolean(arg)));
@@ -1,118 +1,5 @@
1
- import path from "node:path";
2
- import { writeJsonAtomic } from "../infrastructure/harness/atomic-write.js";
3
- import { assessDagRunLiveness, assessDagRunRecoveryEligibility, assertDagRunTransferTargetAvailable, getDagRunDir, locateDagRun, readDagRunState, transferDagRunDir, writeDagRunState, } from "../workflows/dag/lifecycle.js";
4
- export function parseDagReconcileRunArgs(args) {
5
- let runId;
6
- let action;
7
- let reason;
8
- for (let index = 0; index < args.length; index += 1) {
9
- const arg = args[index];
10
- if (arg === "--run-id")
11
- runId = args[++index];
12
- else if (arg.startsWith("--run-id="))
13
- runId = arg.slice("--run-id=".length);
14
- else if (arg === "--action")
15
- action = parseAction(args[++index]);
16
- else if (arg.startsWith("--action="))
17
- action = parseAction(arg.slice("--action=".length));
18
- else if (arg === "--reason")
19
- reason = args[++index];
20
- else if (arg.startsWith("--reason="))
21
- reason = arg.slice("--reason=".length);
22
- else if (arg.startsWith("-"))
23
- throw new Error(`unknown dag reconcile-run flag: ${arg}`);
24
- else
25
- throw new Error(`unexpected positional argument: ${arg}`);
26
- }
27
- if (!runId)
28
- throw new Error("dag reconcile-run requires --run-id <id>");
29
- if (action && !reason?.trim()) {
30
- throw new Error("dag reconcile-run mutation requires --reason <text>");
31
- }
32
- return { runId, action, ...(reason?.trim() ? { reason: reason.trim() } : {}) };
33
- }
34
- function parseAction(value) {
35
- if (value === "supersede" || value === "abandon")
36
- return value;
37
- throw new Error("dag reconcile-run --action must be supersede or abandon");
38
- }
1
+ import { executeDagReconcileRun, parseDagReconcileRunArgs, } from "../workflows/dag/reconcile-run.js";
2
+ export { executeDagReconcileRun, parseDagReconcileRunArgs };
39
3
  export async function runDagReconcileRun(repoRoot, rawArgs) {
40
- const parsed = parseDagReconcileRunArgs(rawArgs);
41
- const located = await locateDagRun(repoRoot, parsed.runId);
42
- if (!located)
43
- throw new Error(`dag run not found: ${parsed.runId}`);
44
- const state = await readDagRunState(located.runDir);
45
- const liveness = assessDagRunLiveness({ state });
46
- const eligibility = assessDagRunRecoveryEligibility({
47
- lifecycle: located.lifecycle,
48
- state,
49
- liveness: liveness.status,
50
- });
51
- if (!parsed.action) {
52
- console.log(JSON.stringify({
53
- action: "inspect",
54
- runId: state.runId,
55
- lifecycle: located.lifecycle,
56
- status: state.status,
57
- liveness: liveness.status,
58
- eligibility,
59
- nextRecommendedAction: eligibility.canReconcile
60
- ? "Re-run with --action supersede|abandon and --reason <text>."
61
- : "Do not reconcile this run; inspect runner and lifecycle evidence first.",
62
- }, null, 2));
63
- return;
64
- }
65
- if (!eligibility.canReconcile || !eligibility.allowedActions.includes(parsed.action)) {
66
- throw new Error(`dag run ${state.runId} is not eligible for reconciliation: ${eligibility.reasons.join(", ") || "unknown reason"}`);
67
- }
68
- const reconciledAt = new Date().toISOString();
69
- const completedRunDir = getDagRunDir(repoRoot, "completed", state.runId);
70
- if (located.lifecycle !== "completed") {
71
- await assertDagRunTransferTargetAvailable(completedRunDir);
72
- }
73
- const artifactRelativePath = path.join(".harness", "dag-runs", "completed", state.runId, "reconciliation.json").replace(/\\/g, "/");
74
- await writeJsonAtomic(path.join(located.runDir, "reconciliation.json"), {
75
- schemaVersion: 1,
76
- runId: state.runId,
77
- action: parsed.action,
78
- reason: parsed.reason,
79
- reconciledAt,
80
- previousLifecycle: located.lifecycle,
81
- previousStatus: state.status,
82
- liveness: liveness.status,
83
- originalState: state,
84
- });
85
- const previousStatus = state.status;
86
- for (const node of Object.values(state.nodes)) {
87
- if (node.status !== "RUNNING")
88
- continue;
89
- node.status = "ERROR";
90
- node.finishedAt = reconciledAt;
91
- node.failureCategory = `operator-${parsed.action}`;
92
- }
93
- state.status = parsed.action === "supersede" ? "superseded" : "abandoned";
94
- state.finishedAt = reconciledAt;
95
- state.failureCategory = `operator-${parsed.action}`;
96
- state.reconciliation = {
97
- action: parsed.action,
98
- reason: parsed.reason,
99
- reconciledAt,
100
- previousStatus,
101
- previousLifecycle: located.lifecycle,
102
- liveness: liveness.status,
103
- artifactPath: artifactRelativePath,
104
- };
105
- await writeDagRunState(located.runDir, state);
106
- const runDir = located.lifecycle === "completed"
107
- ? located.runDir
108
- : await transferDagRunDir(located.runDir, completedRunDir);
109
- console.log(JSON.stringify({
110
- action: parsed.action,
111
- runId: state.runId,
112
- status: state.status,
113
- lifecycle: "completed",
114
- reason: parsed.reason,
115
- reconciliationArtifactPath: artifactRelativePath,
116
- runDir,
117
- }, null, 2));
4
+ console.log(JSON.stringify(await executeDagReconcileRun(repoRoot, rawArgs), null, 2));
118
5
  }