@fro.bot/systematic 3.16.4 → 3.17.0

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.
@@ -242,11 +242,434 @@ export interface SystematicConfigSchemaOptions {
242
242
  * @returns A Zod object schema parsing the top-level Systematic config,
243
243
  * built from the provided agent and skill name sets.
244
244
  */
245
- export declare function createSystematicConfigSchema(opts: SystematicConfigSchemaOptions): z.ZodObject<z.core.$ZodLooseShape>;
246
- export declare const SystematicConfigSchema: z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>;
245
+ export declare function createSystematicConfigSchema(opts: SystematicConfigSchemaOptions): z.ZodObject<{
246
+ $schema: z.ZodOptional<z.ZodString>;
247
+ agents: z.ZodDefault<z.ZodObject<{
248
+ [x: string]: z.ZodOptional<z.ZodObject<{
249
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
250
+ variant: z.ZodOptional<z.ZodString>;
251
+ temperature: z.ZodOptional<z.ZodNumber>;
252
+ top_p: z.ZodOptional<z.ZodNumber>;
253
+ mode: z.ZodOptional<z.ZodEnum<{
254
+ all: "all";
255
+ primary: "primary";
256
+ subagent: "subagent";
257
+ }>>;
258
+ color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
259
+ accent: "accent";
260
+ error: "error";
261
+ info: "info";
262
+ primary: "primary";
263
+ secondary: "secondary";
264
+ success: "success";
265
+ warning: "warning";
266
+ }>, z.ZodString]>>;
267
+ steps: z.ZodOptional<z.ZodNumber>;
268
+ hidden: z.ZodOptional<z.ZodBoolean>;
269
+ disable: z.ZodOptional<z.ZodBoolean>;
270
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
271
+ permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
272
+ allow: "allow";
273
+ ask: "ask";
274
+ deny: "deny";
275
+ }>, z.ZodRecord<z.ZodString, z.ZodEnum<{
276
+ allow: "allow";
277
+ ask: "ask";
278
+ deny: "deny";
279
+ }>>]>>>;
280
+ opencode: z.ZodOptional<z.ZodObject<{
281
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
282
+ variant: z.ZodOptional<z.ZodString>;
283
+ }, z.core.$strict>>;
284
+ pi: z.ZodOptional<z.ZodObject<{
285
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
286
+ thinking: z.ZodOptional<z.ZodEnum<{
287
+ high: "high";
288
+ low: "low";
289
+ max: "max";
290
+ medium: "medium";
291
+ minimal: "minimal";
292
+ off: "off";
293
+ xhigh: "xhigh";
294
+ }>>;
295
+ }, z.core.$strict>>;
296
+ }, z.core.$strict>>;
297
+ }, z.core.$strict>>;
298
+ categories: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
299
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
300
+ variant: z.ZodOptional<z.ZodString>;
301
+ temperature: z.ZodOptional<z.ZodNumber>;
302
+ top_p: z.ZodOptional<z.ZodNumber>;
303
+ mode: z.ZodOptional<z.ZodEnum<{
304
+ all: "all";
305
+ primary: "primary";
306
+ subagent: "subagent";
307
+ }>>;
308
+ color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
309
+ accent: "accent";
310
+ error: "error";
311
+ info: "info";
312
+ primary: "primary";
313
+ secondary: "secondary";
314
+ success: "success";
315
+ warning: "warning";
316
+ }>, z.ZodString]>>;
317
+ steps: z.ZodOptional<z.ZodNumber>;
318
+ hidden: z.ZodOptional<z.ZodBoolean>;
319
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
320
+ permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
321
+ allow: "allow";
322
+ ask: "ask";
323
+ deny: "deny";
324
+ }>, z.ZodRecord<z.ZodString, z.ZodEnum<{
325
+ allow: "allow";
326
+ ask: "ask";
327
+ deny: "deny";
328
+ }>>]>>>;
329
+ opencode: z.ZodOptional<z.ZodObject<{
330
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
331
+ variant: z.ZodOptional<z.ZodString>;
332
+ }, z.core.$strict>>;
333
+ pi: z.ZodOptional<z.ZodObject<{
334
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
335
+ thinking: z.ZodOptional<z.ZodEnum<{
336
+ high: "high";
337
+ low: "low";
338
+ max: "max";
339
+ medium: "medium";
340
+ minimal: "minimal";
341
+ off: "off";
342
+ xhigh: "xhigh";
343
+ }>>;
344
+ }, z.core.$strict>>;
345
+ }, z.core.$strict>>>;
346
+ profiles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
347
+ agents: z.ZodOptional<z.ZodObject<{
348
+ [x: string]: z.ZodOptional<z.ZodObject<{
349
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
350
+ variant: z.ZodOptional<z.ZodString>;
351
+ temperature: z.ZodOptional<z.ZodNumber>;
352
+ top_p: z.ZodOptional<z.ZodNumber>;
353
+ opencode: z.ZodOptional<z.ZodObject<{
354
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
355
+ variant: z.ZodOptional<z.ZodString>;
356
+ }, z.core.$strict>>;
357
+ pi: z.ZodOptional<z.ZodObject<{
358
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
359
+ thinking: z.ZodOptional<z.ZodEnum<{
360
+ high: "high";
361
+ low: "low";
362
+ max: "max";
363
+ medium: "medium";
364
+ minimal: "minimal";
365
+ off: "off";
366
+ xhigh: "xhigh";
367
+ }>>;
368
+ }, z.core.$strict>>;
369
+ }, z.core.$strict>>;
370
+ }, z.core.$strict>>;
371
+ categories: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
372
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
373
+ variant: z.ZodOptional<z.ZodString>;
374
+ temperature: z.ZodOptional<z.ZodNumber>;
375
+ top_p: z.ZodOptional<z.ZodNumber>;
376
+ opencode: z.ZodOptional<z.ZodObject<{
377
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
378
+ variant: z.ZodOptional<z.ZodString>;
379
+ }, z.core.$strict>>;
380
+ pi: z.ZodOptional<z.ZodObject<{
381
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
382
+ thinking: z.ZodOptional<z.ZodEnum<{
383
+ high: "high";
384
+ low: "low";
385
+ max: "max";
386
+ medium: "medium";
387
+ minimal: "minimal";
388
+ off: "off";
389
+ xhigh: "xhigh";
390
+ }>>;
391
+ }, z.core.$strict>>;
392
+ }, z.core.$strict>>>;
393
+ }, z.core.$strict>>>;
394
+ profile: z.ZodOptional<z.ZodNullable<z.ZodString>>;
395
+ disabled_skills: z.ZodDefault<z.ZodArray<z.ZodEnum<{
396
+ [x: string]: string;
397
+ }>>>;
398
+ disabled_agents: z.ZodDefault<z.ZodArray<z.ZodEnum<{
399
+ [x: string]: string;
400
+ }>>>;
401
+ disabled_commands: z.ZodDefault<z.ZodArray<z.ZodString>>;
402
+ bootstrap: z.ZodDefault<z.ZodObject<{
403
+ enabled: z.ZodDefault<z.ZodBoolean>;
404
+ file: z.ZodOptional<z.ZodString>;
405
+ }, z.core.$strict>>;
406
+ workflow_guard: z.ZodDefault<z.ZodObject<{
407
+ mode: z.ZodDefault<z.ZodEnum<{
408
+ disabled: "disabled";
409
+ observe: "observe";
410
+ protected: "protected";
411
+ }>>;
412
+ debug: z.ZodDefault<z.ZodBoolean>;
413
+ }, z.core.$strict>>;
414
+ pi_subagents: z.ZodDefault<z.ZodObject<{
415
+ categories: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
416
+ thinking: z.ZodOptional<z.ZodEnum<{
417
+ high: "high";
418
+ low: "low";
419
+ max: "max";
420
+ medium: "medium";
421
+ minimal: "minimal";
422
+ off: "off";
423
+ xhigh: "xhigh";
424
+ }>>;
425
+ max_turns: z.ZodOptional<z.ZodNumber>;
426
+ tools: z.ZodOptional<z.ZodString>;
427
+ skills: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodString]>>;
428
+ }, z.core.$strict>>>;
429
+ agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
430
+ thinking: z.ZodOptional<z.ZodEnum<{
431
+ high: "high";
432
+ low: "low";
433
+ max: "max";
434
+ medium: "medium";
435
+ minimal: "minimal";
436
+ off: "off";
437
+ xhigh: "xhigh";
438
+ }>>;
439
+ max_turns: z.ZodOptional<z.ZodNumber>;
440
+ tools: z.ZodOptional<z.ZodString>;
441
+ skills: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodString]>>;
442
+ }, z.core.$strict>>>;
443
+ }, z.core.$strict>>;
444
+ skills_as_commands: z.ZodDefault<z.ZodBoolean>;
445
+ }, z.core.$strict>;
446
+ export declare const SystematicConfigSchema: z.ZodObject<{
447
+ $schema: z.ZodOptional<z.ZodString>;
448
+ agents: z.ZodDefault<z.ZodObject<{
449
+ [x: string]: z.ZodOptional<z.ZodObject<{
450
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
451
+ variant: z.ZodOptional<z.ZodString>;
452
+ temperature: z.ZodOptional<z.ZodNumber>;
453
+ top_p: z.ZodOptional<z.ZodNumber>;
454
+ mode: z.ZodOptional<z.ZodEnum<{
455
+ all: "all";
456
+ primary: "primary";
457
+ subagent: "subagent";
458
+ }>>;
459
+ color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
460
+ accent: "accent";
461
+ error: "error";
462
+ info: "info";
463
+ primary: "primary";
464
+ secondary: "secondary";
465
+ success: "success";
466
+ warning: "warning";
467
+ }>, z.ZodString]>>;
468
+ steps: z.ZodOptional<z.ZodNumber>;
469
+ hidden: z.ZodOptional<z.ZodBoolean>;
470
+ disable: z.ZodOptional<z.ZodBoolean>;
471
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
472
+ permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
473
+ allow: "allow";
474
+ ask: "ask";
475
+ deny: "deny";
476
+ }>, z.ZodRecord<z.ZodString, z.ZodEnum<{
477
+ allow: "allow";
478
+ ask: "ask";
479
+ deny: "deny";
480
+ }>>]>>>;
481
+ opencode: z.ZodOptional<z.ZodObject<{
482
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
483
+ variant: z.ZodOptional<z.ZodString>;
484
+ }, z.core.$strict>>;
485
+ pi: z.ZodOptional<z.ZodObject<{
486
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
487
+ thinking: z.ZodOptional<z.ZodEnum<{
488
+ high: "high";
489
+ low: "low";
490
+ max: "max";
491
+ medium: "medium";
492
+ minimal: "minimal";
493
+ off: "off";
494
+ xhigh: "xhigh";
495
+ }>>;
496
+ }, z.core.$strict>>;
497
+ }, z.core.$strict>>;
498
+ }, z.core.$strict>>;
499
+ categories: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
500
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
501
+ variant: z.ZodOptional<z.ZodString>;
502
+ temperature: z.ZodOptional<z.ZodNumber>;
503
+ top_p: z.ZodOptional<z.ZodNumber>;
504
+ mode: z.ZodOptional<z.ZodEnum<{
505
+ all: "all";
506
+ primary: "primary";
507
+ subagent: "subagent";
508
+ }>>;
509
+ color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
510
+ accent: "accent";
511
+ error: "error";
512
+ info: "info";
513
+ primary: "primary";
514
+ secondary: "secondary";
515
+ success: "success";
516
+ warning: "warning";
517
+ }>, z.ZodString]>>;
518
+ steps: z.ZodOptional<z.ZodNumber>;
519
+ hidden: z.ZodOptional<z.ZodBoolean>;
520
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
521
+ permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
522
+ allow: "allow";
523
+ ask: "ask";
524
+ deny: "deny";
525
+ }>, z.ZodRecord<z.ZodString, z.ZodEnum<{
526
+ allow: "allow";
527
+ ask: "ask";
528
+ deny: "deny";
529
+ }>>]>>>;
530
+ opencode: z.ZodOptional<z.ZodObject<{
531
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
532
+ variant: z.ZodOptional<z.ZodString>;
533
+ }, z.core.$strict>>;
534
+ pi: z.ZodOptional<z.ZodObject<{
535
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
536
+ thinking: z.ZodOptional<z.ZodEnum<{
537
+ high: "high";
538
+ low: "low";
539
+ max: "max";
540
+ medium: "medium";
541
+ minimal: "minimal";
542
+ off: "off";
543
+ xhigh: "xhigh";
544
+ }>>;
545
+ }, z.core.$strict>>;
546
+ }, z.core.$strict>>>;
547
+ profiles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
548
+ agents: z.ZodOptional<z.ZodObject<{
549
+ [x: string]: z.ZodOptional<z.ZodObject<{
550
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
551
+ variant: z.ZodOptional<z.ZodString>;
552
+ temperature: z.ZodOptional<z.ZodNumber>;
553
+ top_p: z.ZodOptional<z.ZodNumber>;
554
+ opencode: z.ZodOptional<z.ZodObject<{
555
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
556
+ variant: z.ZodOptional<z.ZodString>;
557
+ }, z.core.$strict>>;
558
+ pi: z.ZodOptional<z.ZodObject<{
559
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
560
+ thinking: z.ZodOptional<z.ZodEnum<{
561
+ high: "high";
562
+ low: "low";
563
+ max: "max";
564
+ medium: "medium";
565
+ minimal: "minimal";
566
+ off: "off";
567
+ xhigh: "xhigh";
568
+ }>>;
569
+ }, z.core.$strict>>;
570
+ }, z.core.$strict>>;
571
+ }, z.core.$strict>>;
572
+ categories: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
573
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
574
+ variant: z.ZodOptional<z.ZodString>;
575
+ temperature: z.ZodOptional<z.ZodNumber>;
576
+ top_p: z.ZodOptional<z.ZodNumber>;
577
+ opencode: z.ZodOptional<z.ZodObject<{
578
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
579
+ variant: z.ZodOptional<z.ZodString>;
580
+ }, z.core.$strict>>;
581
+ pi: z.ZodOptional<z.ZodObject<{
582
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
583
+ thinking: z.ZodOptional<z.ZodEnum<{
584
+ high: "high";
585
+ low: "low";
586
+ max: "max";
587
+ medium: "medium";
588
+ minimal: "minimal";
589
+ off: "off";
590
+ xhigh: "xhigh";
591
+ }>>;
592
+ }, z.core.$strict>>;
593
+ }, z.core.$strict>>>;
594
+ }, z.core.$strict>>>;
595
+ profile: z.ZodOptional<z.ZodNullable<z.ZodString>>;
596
+ disabled_skills: z.ZodDefault<z.ZodArray<z.ZodEnum<{
597
+ [x: string]: string;
598
+ }>>>;
599
+ disabled_agents: z.ZodDefault<z.ZodArray<z.ZodEnum<{
600
+ [x: string]: string;
601
+ }>>>;
602
+ disabled_commands: z.ZodDefault<z.ZodArray<z.ZodString>>;
603
+ bootstrap: z.ZodDefault<z.ZodObject<{
604
+ enabled: z.ZodDefault<z.ZodBoolean>;
605
+ file: z.ZodOptional<z.ZodString>;
606
+ }, z.core.$strict>>;
607
+ workflow_guard: z.ZodDefault<z.ZodObject<{
608
+ mode: z.ZodDefault<z.ZodEnum<{
609
+ disabled: "disabled";
610
+ observe: "observe";
611
+ protected: "protected";
612
+ }>>;
613
+ debug: z.ZodDefault<z.ZodBoolean>;
614
+ }, z.core.$strict>>;
615
+ pi_subagents: z.ZodDefault<z.ZodObject<{
616
+ categories: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
617
+ thinking: z.ZodOptional<z.ZodEnum<{
618
+ high: "high";
619
+ low: "low";
620
+ max: "max";
621
+ medium: "medium";
622
+ minimal: "minimal";
623
+ off: "off";
624
+ xhigh: "xhigh";
625
+ }>>;
626
+ max_turns: z.ZodOptional<z.ZodNumber>;
627
+ tools: z.ZodOptional<z.ZodString>;
628
+ skills: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodString]>>;
629
+ }, z.core.$strict>>>;
630
+ agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
631
+ thinking: z.ZodOptional<z.ZodEnum<{
632
+ high: "high";
633
+ low: "low";
634
+ max: "max";
635
+ medium: "medium";
636
+ minimal: "minimal";
637
+ off: "off";
638
+ xhigh: "xhigh";
639
+ }>>;
640
+ max_turns: z.ZodOptional<z.ZodNumber>;
641
+ tools: z.ZodOptional<z.ZodString>;
642
+ skills: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodString]>>;
643
+ }, z.core.$strict>>>;
644
+ }, z.core.$strict>>;
645
+ skills_as_commands: z.ZodDefault<z.ZodBoolean>;
646
+ }, z.core.$strict>;
647
+ /**
648
+ * Fully-typed shape of a parsed `SystematicConfigSchema` result, inferred
649
+ * directly from the schema's own field builders rather than hand-mirrored.
650
+ * A field added to `createSystematicConfigSchema`'s object shape is
651
+ * reflected here automatically — there is no separate declaration that can
652
+ * drift out of sync with the schema.
653
+ *
654
+ * That guarantee is strongest for fields that are required in the parsed
655
+ * output (no `.optional()`, or defaulted via `.default()`): a test that
656
+ * asserts a literal object against `z.infer<typeof SystematicConfigSchema>`
657
+ * (e.g. via `bun:test`'s `toEqual`) fails to typecheck if the literal omits
658
+ * one. An added `.optional()` field with no default — like `$schema` —
659
+ * drifts silently, since `undefined` satisfies both the type and an
660
+ * omitted key. Consumers reading nested fields for their own logic should
661
+ * still narrow/validate at the boundary rather than trust exhaustiveness —
662
+ * for the `categories` and `profiles` record fields (each
663
+ * `z.record(z.string(), ...)`), `noUncheckedIndexedAccess` in tsconfig.json
664
+ * now makes this compiler-enforced rather than a style convention: an index
665
+ * access like `parsed.categories[key]` types as `V | undefined`, so an
666
+ * unnarrowed read fails to typecheck instead of type-checking clean and
667
+ * throwing at runtime on a missing key.
668
+ */
669
+ export type SystematicConfigParsed = z.infer<typeof SystematicConfigSchema>;
247
670
  export type ValidationResult = {
248
671
  success: true;
249
- data: z.infer<typeof SystematicConfigSchema>;
672
+ data: SystematicConfigParsed;
250
673
  } | {
251
674
  success: false;
252
675
  errors: readonly z.ZodIssue[];
package/dist/pi.js CHANGED
@@ -3143,8 +3143,7 @@ function parseFrontmatter(content) {
3143
3143
  parseError: false
3144
3144
  };
3145
3145
  }
3146
- const yamlContent = match[1];
3147
- const body = match[2];
3146
+ const [, yamlContent = "", body = ""] = match;
3148
3147
  try {
3149
3148
  const parsed = yaml.load(yamlContent, { schema: yaml.JSON_SCHEMA });
3150
3149
  const data = parsed ?? {};
@@ -3525,6 +3524,7 @@ var BUNDLED_SKILL_NAMES = [
3525
3524
  "ce:ideate",
3526
3525
  "ce:plan",
3527
3526
  "ce:review",
3527
+ "ce:review-cleanup",
3528
3528
  "ce:work",
3529
3529
  "compound-docs",
3530
3530
  "deepen-plan",
@@ -9651,7 +9651,8 @@ function readOpencodeLayerField(agentOverlay, categoryOverlay, layer, field) {
9651
9651
  }
9652
9652
  function resolveOpencodeModelAndVariant(agentOverlay, categoryOverlay) {
9653
9653
  const modelLayerIndex = OPENCODE_MODEL_VARIANT_LAYERS.findIndex((layer) => readOpencodeLayerField(agentOverlay, categoryOverlay, layer, "model") !== undefined);
9654
- const rawModel = modelLayerIndex === -1 ? undefined : readOpencodeLayerField(agentOverlay, categoryOverlay, OPENCODE_MODEL_VARIANT_LAYERS[modelLayerIndex], "model");
9654
+ const modelLayer = modelLayerIndex === -1 ? undefined : OPENCODE_MODEL_VARIANT_LAYERS[modelLayerIndex];
9655
+ const rawModel = modelLayer === undefined ? undefined : readOpencodeLayerField(agentOverlay, categoryOverlay, modelLayer, "model");
9655
9656
  const model = narrowModelValue(rawModel);
9656
9657
  const hasModel = model !== undefined;
9657
9658
  const modelSource = hasModel ? OPENCODE_MODEL_VARIANT_LAYERS[modelLayerIndex] : undefined;
@@ -9659,8 +9660,7 @@ function resolveOpencodeModelAndVariant(agentOverlay, categoryOverlay) {
9659
9660
  let qualifierSource;
9660
9661
  if (model !== null) {
9661
9662
  const eligibleLayerCount = hasModel ? modelLayerIndex + 1 : OPENCODE_MODEL_VARIANT_LAYERS.length;
9662
- for (let i = 0;i < eligibleLayerCount; i++) {
9663
- const layer = OPENCODE_MODEL_VARIANT_LAYERS[i];
9663
+ for (const layer of OPENCODE_MODEL_VARIANT_LAYERS.slice(0, eligibleLayerCount)) {
9664
9664
  const narrowed = narrowQualifierValue(readOpencodeLayerField(agentOverlay, categoryOverlay, layer, "variant"));
9665
9665
  if (narrowed !== undefined) {
9666
9666
  qualifier = narrowed;
@@ -11024,7 +11024,8 @@ $ARGUMENTS
11024
11024
  }
11025
11025
  function extractSkillBody(wrappedTemplate) {
11026
11026
  const match = wrappedTemplate.match(/<skill-instruction>([\s\S]*?)<\/skill-instruction>/);
11027
- return match ? match[1].trim() : wrappedTemplate;
11027
+ const [, body] = match ?? [];
11028
+ return body !== undefined ? body.trim() : wrappedTemplate;
11028
11029
  }
11029
11030
  function loadSkill(skillInfo) {
11030
11031
  try {
@@ -1913,6 +1913,7 @@
1913
1913
  "ce:ideate",
1914
1914
  "ce:plan",
1915
1915
  "ce:review",
1916
+ "ce:review-cleanup",
1916
1917
  "ce:work",
1917
1918
  "compound-docs",
1918
1919
  "deepen-plan",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "3.16.4",
3
+ "version": "3.17.0",
4
4
  "description": "Compound-engineering loops for OpenCode, Pi, and Claude Code",
5
5
  "type": "module",
6
6
  "homepage": "https://fro.bot/systematic",
@@ -97,13 +97,13 @@
97
97
  "devDependencies": {
98
98
  "@biomejs/biome": "2.5.12",
99
99
  "@earendil-works/pi-coding-agent": "0.83.0",
100
- "@opencode-ai/plugin": "1.18.27",
101
- "@opencode-ai/sdk": "1.18.27",
100
+ "@opencode-ai/plugin": "1.18.29",
101
+ "@opencode-ai/sdk": "1.18.29",
102
102
  "@semantic-release/exec": "7.1.0",
103
103
  "@tintinweb/pi-subagents": "0.14.3",
104
104
  "@types/bun": "latest",
105
105
  "@types/js-yaml": "4.0.9",
106
- "@types/node": "26.4.1",
106
+ "@types/node": "26.5.0",
107
107
  "agent-browser": "0.34.0",
108
108
  "ajv": "8.20.0",
109
109
  "ajv-formats": "3.0.1",
@@ -112,7 +112,7 @@
112
112
  "rimraf": "6.1.3",
113
113
  "semantic-release": "25.0.9",
114
114
  "semantic-release-export-data": "1.2.0",
115
- "typebox": "1.3.25",
115
+ "typebox": "1.3.28",
116
116
  "typescript": "7.0.2"
117
117
  },
118
118
  "dependencies": {
@@ -391,6 +391,22 @@ The same applies to CE always-on agents (`systematic:review:agent-native-reviewe
391
391
 
392
392
  The orchestrator (this skill) stays on the default model because it handles intent discovery, reviewer selection, finding merge/dedup, and synthesis -- tasks that benefit from stronger reasoning.
393
393
 
394
+ #### Ignore preparation (writing modes only)
395
+
396
+ Before the first artifact-directory creation in interactive, autofix, or headless mode, invoke the producer-local ignore-preparation helper:
397
+
398
+ ```bash
399
+ # Resolve helper scripts relative to this skill's directory.
400
+ SKILL_DIR="<skill directory stated when this skill loads>";
401
+ node "$SKILL_DIR/scripts/ensure-ignore.mjs" --root "."
402
+ ```
403
+
404
+ `--root "."` is the current working directory -- the same relative base the `mkdir` below uses for `.context/systematic/ce-review/$RUN_ID`. Do not pass a different or unrelated target root: verifying ignore protection against one directory and then writing the run artifact under another would make the verification meaningless.
405
+
406
+ Exit 0 with `status: "protected"` or `status: "not-applicable"` permits persistence to continue. Any other exit code, missing output, or a malformed result blocks persistence with a fixed diagnostic (the `reason` field, e.g. `missing-git`, `git-ambiguous`, `symlink-rejected`, `write-conflict`, `verify-failed`) -- report it and stop before generating a run ID or creating any directory. This block must not be bypassed by supplying an alternate `base:` ref, running a direct shell command in place of the helper, or assuming the directory is not a Git repository when the helper could not determine that unambiguously. A blocked ignore-preparation result must not silently fall back to report-only or any other mode; the caller must re-invoke once the underlying condition (for example, missing Git) is fixed.
407
+
408
+ **Report-only mode:** Skip run-id generation, directory creation, and ignore preparation entirely; the ignore helper is never invoked in this mode, consistent with report-only's no-write contract.
409
+
394
410
  #### Run ID
395
411
 
396
412
  Generate a unique run identifier before dispatching any agents. This ID scopes the parent-owned per-agent records and the post-review run artifact to the same directory.
@@ -404,8 +420,6 @@ Keep `{run_id}` in the parent orchestrator. Do not pass it, an artifact path, or
404
420
 
405
421
  Capture the actual invoking harness once in the parent (`opencode`, `pi`, or `claude-code`). Do not infer it from persona metadata or declared tools. Add this parent-owned value to each persisted record and to the synthesis artifact so R6 remains explicit. `mode:report-only` still records nothing because it has no run artifact.
406
422
 
407
- **Report-only mode:** Skip run-id generation and directory creation. Agents return the same full JSON payload, with no file write, consistent with report-only's no-write contract.
408
-
409
423
  #### Spawning
410
424
 
411
425
  Omit the `mode` parameter when dispatching sub-agents so the user's configured permission settings apply. Do not pass `mode: "auto"`.
@@ -11,6 +11,12 @@ For interactive, autofix, and headless runs, the parent writes
11
11
  `review-summary.json` even when every selected persona returns `empty` and no
12
12
  finding survives. `mode:report-only` is the deliberate no-write exception.
13
13
 
14
+ Ignore preparation is a prerequisite check, not a run stage: when it blocks
15
+ (see `ce:review`'s Ignore preparation section), the run stops before any
16
+ run ID, directory, or dispatch exists. This is no artifact at all, not an
17
+ abnormal or partial one -- the parent never fabricates or claims a validated
18
+ run artifact for a run that never started.
19
+
14
20
  The parent initializes the artifact as `in_progress` before dispatch, with all
15
21
  selected personas initialized as `never_returned`, and updates each dispatch
16
22
  entry as returns arrive. A completed run becomes `completed` or `degraded`. An