@anchrd/intel-contract 0.10.0 → 0.12.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.
@@ -471,6 +471,7 @@ export declare const BoardStatus: z.ZodObject<{
471
471
  id: z.ZodString;
472
472
  label: z.ZodString;
473
473
  order: z.ZodNumber;
474
+ terminal: z.ZodBoolean;
474
475
  }, z.core.$strict>;
475
476
  export type BoardStatus = z.infer<typeof BoardStatus>;
476
477
  export declare const BoardDefaultStatuses: readonly BoardStatus[];
@@ -496,12 +497,66 @@ export type BoardAssignee = z.infer<typeof BoardAssignee>;
496
497
  * mints a key between theirs, so moving one task writes one task and renumbers nothing — the whole
497
498
  * reason a board is not addressed by position the way a table's rows are (`TableRowPosition`).
498
499
  * A hand-written key could collide, and two tasks sharing a key have no defined order at all.
500
+ *
501
+ * ⚠️ This CHARACTER CLASS is the truth about a stored key, not `fractional-indexing`, and the two
502
+ * are deliberately not the same set (anchrd/intel#359). A board that arrived through a bundle may
503
+ * spell `"0"`, `"a00"`, `"zzz"` or `"A"` — all of them legal here, none of them readable by that
504
+ * library. Narrowing the regex to what it reads would be a rule on the STORED document, so a board
505
+ * carrying one would stop parsing in all four places a board body is read (the board read, the
506
+ * indexer, the link reader, the bundle import) — unreadable, unsearchable and unmovable at once,
507
+ * over a value its owner never wrote. That is the trap #311, #318 and #321 each walked into from a
508
+ * different side.
509
+ *
510
+ * ⚠️ What every consumer may therefore rely on is exactly what stands here: keys are non-empty
511
+ * strings over `[0-9A-Za-z]` and are ORDERED BY STRING COMPARISON. Nothing may assume more — and
512
+ * `packages/api`'s `orderBetween` is the one place that asks `fractional-indexing` for a key and
513
+ * carries on without it when it refuses a bound.
499
514
  */
515
+ export declare const MaxBoardTaskOrderLength = 64;
500
516
  export declare const BoardTaskOrder: z.ZodString;
501
517
  export declare const BoardTaskId: z.ZodString;
502
518
  export declare const BoardTaskLabel: z.ZodString;
503
519
  export declare const BoardTaskDate: z.ZodISODate;
504
520
  export declare const BoardTaskDescription: z.ZodString;
521
+ /**
522
+ * The tasks one task waits for (#285), each of them at most once (anchrd/intel#318).
523
+ *
524
+ * ⚠️ Board-internal only, enforced on the write path: a dependency on a task in another board would
525
+ * hang this node on a file that can change without anyone here noticing. Across boards the link is
526
+ * `references`, which points at the board NODE and lands in the link graph.
527
+ *
528
+ * ⚠️ Refused rather than folded together, the same shape as the status ids in `ConfigureBoardInput`.
529
+ * A repeat carries no information — but that is a fact about the value, not about the answer: a
530
+ * caller handed back a shorter list than it sent is told nothing, and composes the same one again.
531
+ * The refusal names the mistake once, and the caller is holding the list it has to fix. (The one
532
+ * place a repeat is folded instead is `upgradeStoredBoard`, where there is no caller to tell.)
533
+ *
534
+ * ⚠️ It sits on the STORED task as well as on the two inputs, so a consumer may rely on it rather
535
+ * than defend against it — `createBoardGraph` mints one edge key per pair, and a second one threw
536
+ * the whole graph view off the screen for everybody looking at that board.
537
+ */
538
+ export declare const BoardTaskDependsOn: z.ZodArray<z.ZodString>;
539
+ /**
540
+ * A task's labels, each of them at most once (anchrd/intel#318).
541
+ *
542
+ * ⚠️ The same rule the detail panel has always applied to what a person types — it refuses to add a
543
+ * label the task already carries — stated where every surface meets it, because the MCP write path
544
+ * did not. A repeated label draws the same chip twice on the card, with two remove buttons of which
545
+ * either takes both away, and weights that word higher in the search text (`indexing.ts`).
546
+ */
547
+ export declare const BoardTaskLabels: z.ZodArray<z.ZodString>;
548
+ /**
549
+ * The Intel nodes a task points at, each of them at most once (anchrd/intel#318).
550
+ *
551
+ * They land in the link graph as `text` links, the same way a document's inline links do, so what a
552
+ * board points at is visible from the other side too.
553
+ *
554
+ * ⚠️ Distinct for the same reason as `labels`: the picker in the detail panel already refuses one
555
+ * the task holds, and the link graph counts a repeat once anyway (`ON CONFLICT DO NOTHING`), so a
556
+ * duplicate is a second row in the panel and nothing else — which is exactly the kind of value that
557
+ * has no reading and should not be storable.
558
+ */
559
+ export declare const BoardTaskReferences: z.ZodArray<z.ZodString>;
505
560
  export declare const BoardTask: z.ZodObject<{
506
561
  id: z.ZodString;
507
562
  title: z.ZodString;
@@ -528,6 +583,7 @@ export declare const BoardDocument: z.ZodObject<{
528
583
  id: z.ZodString;
529
584
  label: z.ZodString;
530
585
  order: z.ZodNumber;
586
+ terminal: z.ZodBoolean;
531
587
  }, z.core.$strict>>;
532
588
  tasks: z.ZodArray<z.ZodObject<{
533
589
  id: z.ZodString;
@@ -581,6 +637,7 @@ export declare const NodeBoard: z.ZodObject<{
581
637
  id: z.ZodString;
582
638
  label: z.ZodString;
583
639
  order: z.ZodNumber;
640
+ terminal: z.ZodBoolean;
584
641
  }, z.core.$strict>>;
585
642
  tasks: z.ZodArray<z.ZodObject<{
586
643
  id: z.ZodString;
@@ -609,6 +666,7 @@ export type NodeBoard = z.infer<typeof NodeBoard>;
609
666
  export declare const BoardStatusInput: z.ZodObject<{
610
667
  id: z.ZodString;
611
668
  label: z.ZodString;
669
+ terminal: z.ZodOptional<z.ZodBoolean>;
612
670
  }, z.core.$strict>;
613
671
  export type BoardStatusInput = z.infer<typeof BoardStatusInput>;
614
672
  export declare const ConfigureBoardInput: z.ZodObject<{
@@ -616,6 +674,7 @@ export declare const ConfigureBoardInput: z.ZodObject<{
616
674
  statuses: z.ZodArray<z.ZodObject<{
617
675
  id: z.ZodString;
618
676
  label: z.ZodString;
677
+ terminal: z.ZodOptional<z.ZodBoolean>;
619
678
  }, z.core.$strict>>;
620
679
  idempotencyKey: z.ZodString;
621
680
  }, z.core.$strict>;
@@ -695,6 +754,24 @@ export declare const DeleteBoardTaskInput: z.ZodObject<{
695
754
  idempotencyKey: z.ZodString;
696
755
  }, z.core.$strict>;
697
756
  export type DeleteBoardTaskInput = z.infer<typeof DeleteBoardTaskInput>;
757
+ /**
758
+ * The way out of a board whose `tasks[]` names one id twice (anchrd/intel#341).
759
+ *
760
+ * ⚠️ It names no task, and that is not an oversight. Such a board holds a pair the caller cannot
761
+ * tell apart — every other operation here addresses a task BY id (#285), so the one thing nobody
762
+ * can say is "the second of the two". The board is what is named, and the server does the one thing
763
+ * that removes the ambiguity: the first entry under an id keeps it, every later one gets a freshly
764
+ * minted id and keeps everything else it carries.
765
+ *
766
+ * ⚠️ It exists because the pair is deliberately NOT folded away on read (`upgradeStoredBoard`,
767
+ * `repeatedBoardId`): two tasks under one id are two whole tasks, and a fold on the read is written
768
+ * back by the next save of any kind. So somebody has to ask for the repair, and this is the asking.
769
+ */
770
+ export declare const RepairBoardTaskIdsInput: z.ZodObject<{
771
+ nodeId: z.ZodString;
772
+ idempotencyKey: z.ZodString;
773
+ }, z.core.$strict>;
774
+ export type RepairBoardTaskIdsInput = z.infer<typeof RepairBoardTaskIdsInput>;
698
775
  export declare const BoardTaskResult: z.ZodObject<{
699
776
  node: z.ZodObject<{
700
777
  id: z.ZodString;
@@ -829,9 +906,82 @@ export declare const ConfigureBoardResult: z.ZodObject<{
829
906
  id: z.ZodString;
830
907
  label: z.ZodString;
831
908
  order: z.ZodNumber;
909
+ terminal: z.ZodBoolean;
832
910
  }, z.core.$strict>>;
833
911
  }, z.core.$strict>;
834
912
  export type ConfigureBoardResult = z.infer<typeof ConfigureBoardResult>;
913
+ /**
914
+ * What the repair did, task by task (anchrd/intel#341).
915
+ *
916
+ * ⚠️ `previousId` is the id the entry shared, and it still names a task on this board — the FIRST
917
+ * entry under it, the one that kept it. That is what makes the repair readable: nothing that
918
+ * pointed at that id moved, so a caller can see which of the two the board's `parentId` and
919
+ * `dependsOn` edges have been meaning all along, and move them with `board_task_move` and
920
+ * `board_task_update` if they meant the other one.
921
+ *
922
+ * ⚠️ At least one entry, because a board with nothing to repair is refused rather than answered
923
+ * with an empty list and a new version that changed nothing.
924
+ */
925
+ export declare const RepairBoardTaskIdsResult: z.ZodObject<{
926
+ node: z.ZodObject<{
927
+ id: z.ZodString;
928
+ parentId: z.ZodNullable<z.ZodString>;
929
+ kind: z.ZodEnum<{
930
+ folder: "folder";
931
+ document: "document";
932
+ attachment: "attachment";
933
+ table: "table";
934
+ agent: "agent";
935
+ board: "board";
936
+ }>;
937
+ title: z.ZodString;
938
+ description: z.ZodNullable<z.ZodString>;
939
+ ownerId: z.ZodString;
940
+ currentVersionId: z.ZodNullable<z.ZodString>;
941
+ createdAt: z.ZodISODateTime;
942
+ updatedAt: z.ZodISODateTime;
943
+ archivedAt: z.ZodNullable<z.ZodISODateTime>;
944
+ }, z.core.$strict>;
945
+ version: z.ZodObject<{
946
+ id: z.ZodString;
947
+ nodeId: z.ZodString;
948
+ sequence: z.ZodNumber;
949
+ contentKey: z.ZodString;
950
+ mediaType: z.ZodString;
951
+ contentHash: z.ZodString;
952
+ size: z.ZodNumber;
953
+ segment: z.ZodNullable<z.ZodEnum<{
954
+ append: "append";
955
+ snapshot: "snapshot";
956
+ }>>;
957
+ createdBy: z.ZodString;
958
+ createdAt: z.ZodISODateTime;
959
+ }, z.core.$strict>;
960
+ renumbered: z.ZodArray<z.ZodObject<{
961
+ previousId: z.ZodString;
962
+ task: z.ZodObject<{
963
+ id: z.ZodString;
964
+ title: z.ZodString;
965
+ status: z.ZodString;
966
+ assignee: z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
967
+ type: z.ZodLiteral<"user">;
968
+ id: z.ZodString;
969
+ }, z.core.$strict>, z.ZodObject<{
970
+ type: z.ZodLiteral<"agent">;
971
+ nodeId: z.ZodString;
972
+ }, z.core.$strict>], "type">>;
973
+ labels: z.ZodArray<z.ZodString>;
974
+ startDate: z.ZodNullable<z.ZodISODate>;
975
+ dueDate: z.ZodNullable<z.ZodISODate>;
976
+ parentId: z.ZodNullable<z.ZodString>;
977
+ dependsOn: z.ZodArray<z.ZodString>;
978
+ order: z.ZodString;
979
+ description: z.ZodString;
980
+ references: z.ZodArray<z.ZodString>;
981
+ }, z.core.$strict>;
982
+ }, z.core.$strict>>;
983
+ }, z.core.$strict>;
984
+ export type RepairBoardTaskIdsResult = z.infer<typeof RepairBoardTaskIdsResult>;
835
985
  export declare const AgentMediaType = "application/vnd.anchrd.agent+json";
836
986
  export declare const AgentReferenceRole: z.ZodEnum<{
837
987
  "system-message": "system-message";
@@ -1761,6 +1911,7 @@ export declare const ToolCatalog: z.ZodObject<{
1761
1911
  }, z.core.$strict>;
1762
1912
  fingerprint: z.ZodString;
1763
1913
  }, z.core.$strict>>;
1914
+ reached: z.ZodOptional<z.ZodArray<z.ZodString>>;
1764
1915
  }, z.core.$strict>;
1765
1916
  export type ToolCatalog = z.infer<typeof ToolCatalog>;
1766
1917
  /**
@@ -291,14 +291,32 @@ export const BoardStatus = z.strictObject({
291
291
  // both claiming position 3 is a board no surface could draw, and it is a state nobody has to be
292
292
  // able to reason about if it cannot be expressed.
293
293
  order: z.number().int().nonnegative(),
294
+ /**
295
+ * Whether standing in this column means the work is finished (anchrd/intel#311).
296
+ *
297
+ * It answers the one question #285 left open — when a `dependsOn` is satisfied — and it is the
298
+ * ONLY answer to it. There is deliberately no second reading anywhere: a surface that decided
299
+ * "done" for itself would decide it differently the first time somebody reconfigured a board.
300
+ *
301
+ * ⚠️ It is a property of the status, not a position in the list, and that distinction is the
302
+ * whole ticket. #286 first read "the last column before `archived`" as done, reasoning by symmetry
303
+ * with the server's rule that a new task lands in the first column that is not `archived`. But a
304
+ * status list is configurable on purpose, so `… done → blocked → archived` makes "blocked" mean
305
+ * finished — silently, with a wrong blocked marker as the only symptom.
306
+ *
307
+ * ⚠️ Several columns may carry it. "Done" and a cancelled-like column are both ends of the work,
308
+ * and nothing waiting on a cancelled task is still blocked by it.
309
+ */
310
+ terminal: z.boolean(),
294
311
  });
295
312
  // What a board starts out with. Five columns, of which the last one is the fixed `archived` shelf.
313
+ // `done` and the shelf are where work ends; the three before them are not (anchrd/intel#311).
296
314
  export const BoardDefaultStatuses = [
297
- { id: "backlog", label: "Backlog", order: 0 },
298
- { id: "in_progress", label: "In progress", order: 1 },
299
- { id: "review", label: "Review", order: 2 },
300
- { id: "done", label: "Done", order: 3 },
301
- { id: ArchivedBoardStatusId, label: "Archived", order: 4 },
315
+ { id: "backlog", label: "Backlog", order: 0, terminal: false },
316
+ { id: "in_progress", label: "In progress", order: 1, terminal: false },
317
+ { id: "review", label: "Review", order: 2, terminal: false },
318
+ { id: "done", label: "Done", order: 3, terminal: true },
319
+ { id: ArchivedBoardStatusId, label: "Archived", order: 4, terminal: true },
302
320
  ];
303
321
  /**
304
322
  * Who a task is on: a person Gate knows, or an agent node in this installation (#285).
@@ -318,8 +336,28 @@ export const BoardAssignee = z.discriminatedUnion("type", [
318
336
  * mints a key between theirs, so moving one task writes one task and renumbers nothing — the whole
319
337
  * reason a board is not addressed by position the way a table's rows are (`TableRowPosition`).
320
338
  * A hand-written key could collide, and two tasks sharing a key have no defined order at all.
339
+ *
340
+ * ⚠️ This CHARACTER CLASS is the truth about a stored key, not `fractional-indexing`, and the two
341
+ * are deliberately not the same set (anchrd/intel#359). A board that arrived through a bundle may
342
+ * spell `"0"`, `"a00"`, `"zzz"` or `"A"` — all of them legal here, none of them readable by that
343
+ * library. Narrowing the regex to what it reads would be a rule on the STORED document, so a board
344
+ * carrying one would stop parsing in all four places a board body is read (the board read, the
345
+ * indexer, the link reader, the bundle import) — unreadable, unsearchable and unmovable at once,
346
+ * over a value its owner never wrote. That is the trap #311, #318 and #321 each walked into from a
347
+ * different side.
348
+ *
349
+ * ⚠️ What every consumer may therefore rely on is exactly what stands here: keys are non-empty
350
+ * strings over `[0-9A-Za-z]` and are ORDERED BY STRING COMPARISON. Nothing may assume more — and
351
+ * `packages/api`'s `orderBetween` is the one place that asks `fractional-indexing` for a key and
352
+ * carries on without it when it refuses a bound.
321
353
  */
322
- export const BoardTaskOrder = z.string().regex(/^[0-9A-Za-z]{1,64}$/);
354
+ // ⚠️ Named because a consumer has to be able to stay inside it. The repair of a repeated task id
355
+ // (anchrd/intel#341) mints a key beside an existing one, and a key one character too long would be
356
+ // written and then refused by the very next read — the whole board lost over a repair.
357
+ export const MaxBoardTaskOrderLength = 64;
358
+ export const BoardTaskOrder = z
359
+ .string()
360
+ .regex(new RegExp(`^[0-9A-Za-z]{1,${MaxBoardTaskOrderLength}}$`));
323
361
  export const BoardTaskId = IntelId;
324
362
  export const BoardTaskLabel = z.string().trim().min(1).max(60);
325
363
  // A day, not an instant. A task is due on a date; giving it a time zone would make the same task
@@ -328,27 +366,76 @@ export const BoardTaskDate = z.iso.date();
328
366
  // Markdown, and capped: a task's description is a card, and what needs more than this is a document
329
367
  // the task can point at through `references`.
330
368
  export const BoardTaskDescription = z.string().max(20_000);
369
+ /**
370
+ * The tasks one task waits for (#285), each of them at most once (anchrd/intel#318).
371
+ *
372
+ * ⚠️ Board-internal only, enforced on the write path: a dependency on a task in another board would
373
+ * hang this node on a file that can change without anyone here noticing. Across boards the link is
374
+ * `references`, which points at the board NODE and lands in the link graph.
375
+ *
376
+ * ⚠️ Refused rather than folded together, the same shape as the status ids in `ConfigureBoardInput`.
377
+ * A repeat carries no information — but that is a fact about the value, not about the answer: a
378
+ * caller handed back a shorter list than it sent is told nothing, and composes the same one again.
379
+ * The refusal names the mistake once, and the caller is holding the list it has to fix. (The one
380
+ * place a repeat is folded instead is `upgradeStoredBoard`, where there is no caller to tell.)
381
+ *
382
+ * ⚠️ It sits on the STORED task as well as on the two inputs, so a consumer may rely on it rather
383
+ * than defend against it — `createBoardGraph` mints one edge key per pair, and a second one threw
384
+ * the whole graph view off the screen for everybody looking at that board.
385
+ */
386
+ export const BoardTaskDependsOn = z
387
+ .array(BoardTaskId)
388
+ .max(64)
389
+ .refine((ids) => new Set(ids).size === ids.length, {
390
+ error: "A task can be named only once in dependsOn",
391
+ });
392
+ /**
393
+ * A task's labels, each of them at most once (anchrd/intel#318).
394
+ *
395
+ * ⚠️ The same rule the detail panel has always applied to what a person types — it refuses to add a
396
+ * label the task already carries — stated where every surface meets it, because the MCP write path
397
+ * did not. A repeated label draws the same chip twice on the card, with two remove buttons of which
398
+ * either takes both away, and weights that word higher in the search text (`indexing.ts`).
399
+ */
400
+ export const BoardTaskLabels = z
401
+ .array(BoardTaskLabel)
402
+ .max(32)
403
+ .refine((labels) => new Set(labels).size === labels.length, {
404
+ error: "A label can be named only once",
405
+ });
406
+ /**
407
+ * The Intel nodes a task points at, each of them at most once (anchrd/intel#318).
408
+ *
409
+ * They land in the link graph as `text` links, the same way a document's inline links do, so what a
410
+ * board points at is visible from the other side too.
411
+ *
412
+ * ⚠️ Distinct for the same reason as `labels`: the picker in the detail panel already refuses one
413
+ * the task holds, and the link graph counts a repeat once anyway (`ON CONFLICT DO NOTHING`), so a
414
+ * duplicate is a second row in the panel and nothing else — which is exactly the kind of value that
415
+ * has no reading and should not be storable.
416
+ */
417
+ export const BoardTaskReferences = z
418
+ .array(IntelId)
419
+ .max(64)
420
+ .refine((ids) => new Set(ids).size === ids.length, {
421
+ error: "A node can be referenced only once",
422
+ });
331
423
  export const BoardTask = z.strictObject({
332
424
  id: BoardTaskId,
333
425
  title: z.string().trim().min(1).max(240),
334
426
  status: BoardStatusId,
335
427
  assignee: BoardAssignee.nullable(),
336
- labels: z.array(BoardTaskLabel).max(32),
428
+ labels: BoardTaskLabels,
337
429
  startDate: BoardTaskDate.nullable(),
338
430
  dueDate: BoardTaskDate.nullable(),
339
431
  // ⚠️ The whole hierarchy in one field, deliberately: epic, task and subtask are a DEPTH and not a
340
432
  // type (#285). A `kind` beside it would allow a subtask under nothing and an epic under an epic,
341
433
  // and every surface would then need its own opinion about which combinations mean anything.
342
434
  parentId: BoardTaskId.nullable(),
343
- // ⚠️ Board-internal only, enforced on the write path: a dependency on a task in another board
344
- // would hang this node on a file that can change without anyone here noticing. Across boards the
345
- // link is `references`, which points at the board NODE and lands in the link graph.
346
- dependsOn: z.array(BoardTaskId).max(64),
435
+ dependsOn: BoardTaskDependsOn,
347
436
  order: BoardTaskOrder,
348
437
  description: BoardTaskDescription,
349
- // Node ids. They land in the link graph as `text` links, the same way a document's inline links
350
- // do, so what a board points at is visible from the other side too.
351
- references: z.array(IntelId).max(64),
438
+ references: BoardTaskReferences,
352
439
  });
353
440
  // The whole board, as it is stored and as it is read. There is no second representation to keep in
354
441
  // step with it — this document is the file.
@@ -370,6 +457,16 @@ export const NodeBoard = z.strictObject({
370
457
  export const BoardStatusInput = z.strictObject({
371
458
  id: BoardStatusId,
372
459
  label: z.string().trim().min(1).max(60),
460
+ /**
461
+ * Whether this column means finished (anchrd/intel#311).
462
+ *
463
+ * ⚠️ Optional, and the absence is not the same as `false`. A caller who says nothing gets the
464
+ * server's answer — `false` for an ordinary column, `true` for the shelf, which cannot be
465
+ * anything else. Making it a required boolean would force every caller that only wanted to rename
466
+ * a column to restate the whole board's notion of done, and getting one entry wrong there is a
467
+ * silent change to what counts as blocked.
468
+ */
469
+ terminal: z.boolean().optional(),
373
470
  });
374
471
  // The status list, written whole and in the order it should be drawn — never a patch. Adding,
375
472
  // renaming and reordering are all this one call, and `archived` has to be in what it is given.
@@ -384,7 +481,12 @@ export const ConfigureBoardInput = z.strictObject({
384
481
  })
385
482
  .refine((statuses) => statuses.some((status) => status.id === ArchivedBoardStatusId), {
386
483
  error: `The "${ArchivedBoardStatusId}" status cannot be removed`,
387
- }),
484
+ })
485
+ // ⚠️ Refused rather than corrected, the same way removing the shelf is refused. A task swept
486
+ // onto `archived` is finished with, and a board that could declare the shelf non-terminal would
487
+ // hold every archived task open as a blocker forever. Only an EXPLICIT `false` is refused —
488
+ // saying nothing is fine and means the server's `true` (anchrd/intel#311).
489
+ .refine((statuses) => statuses.find((status) => status.id === ArchivedBoardStatusId)?.terminal !== false, { error: `The "${ArchivedBoardStatusId}" status is always terminal` }),
388
490
  idempotencyKey: z.string().min(8).max(200),
389
491
  });
390
492
  /**
@@ -402,13 +504,13 @@ export const AddBoardTaskInput = z.strictObject({
402
504
  // the shelf.
403
505
  status: BoardStatusId.optional(),
404
506
  assignee: BoardAssignee.nullable().default(null),
405
- labels: z.array(BoardTaskLabel).max(32).default([]),
507
+ labels: BoardTaskLabels.default([]),
406
508
  startDate: BoardTaskDate.nullable().default(null),
407
509
  dueDate: BoardTaskDate.nullable().default(null),
408
510
  parentId: BoardTaskId.nullable().default(null),
409
- dependsOn: z.array(BoardTaskId).max(64).default([]),
511
+ dependsOn: BoardTaskDependsOn.default([]),
410
512
  description: BoardTaskDescription.default(""),
411
- references: z.array(IntelId).max(64).default([]),
513
+ references: BoardTaskReferences.default([]),
412
514
  // Where among its neighbours it goes. Both absent puts it last in its column.
413
515
  afterTaskId: BoardTaskId.nullable().default(null),
414
516
  beforeTaskId: BoardTaskId.nullable().default(null),
@@ -428,12 +530,12 @@ export const UpdateBoardTaskInput = z
428
530
  taskId: BoardTaskId,
429
531
  title: z.string().trim().min(1).max(240).optional(),
430
532
  assignee: BoardAssignee.nullable().optional(),
431
- labels: z.array(BoardTaskLabel).max(32).optional(),
533
+ labels: BoardTaskLabels.optional(),
432
534
  startDate: BoardTaskDate.nullable().optional(),
433
535
  dueDate: BoardTaskDate.nullable().optional(),
434
- dependsOn: z.array(BoardTaskId).max(64).optional(),
536
+ dependsOn: BoardTaskDependsOn.optional(),
435
537
  description: BoardTaskDescription.optional(),
436
- references: z.array(IntelId).max(64).optional(),
538
+ references: BoardTaskReferences.optional(),
437
539
  idempotencyKey: z.string().min(8).max(200),
438
540
  })
439
541
  .refine((input) => input.title !== undefined ||
@@ -467,6 +569,23 @@ export const DeleteBoardTaskInput = z.strictObject({
467
569
  taskId: BoardTaskId,
468
570
  idempotencyKey: z.string().min(8).max(200),
469
571
  });
572
+ /**
573
+ * The way out of a board whose `tasks[]` names one id twice (anchrd/intel#341).
574
+ *
575
+ * ⚠️ It names no task, and that is not an oversight. Such a board holds a pair the caller cannot
576
+ * tell apart — every other operation here addresses a task BY id (#285), so the one thing nobody
577
+ * can say is "the second of the two". The board is what is named, and the server does the one thing
578
+ * that removes the ambiguity: the first entry under an id keeps it, every later one gets a freshly
579
+ * minted id and keeps everything else it carries.
580
+ *
581
+ * ⚠️ It exists because the pair is deliberately NOT folded away on read (`upgradeStoredBoard`,
582
+ * `repeatedBoardId`): two tasks under one id are two whole tasks, and a fold on the read is written
583
+ * back by the next save of any kind. So somebody has to ask for the repair, and this is the asking.
584
+ */
585
+ export const RepairBoardTaskIdsInput = z.strictObject({
586
+ nodeId: IntelId,
587
+ idempotencyKey: z.string().min(8).max(200),
588
+ });
470
589
  // The one task that was written, not the whole board: a board can hold thousands of tasks, and
471
590
  // answering a one-card edit with all of them would make every write pay for the read.
472
591
  export const BoardTaskResult = z.strictObject({
@@ -486,6 +605,26 @@ export const ConfigureBoardResult = z.strictObject({
486
605
  version: NodeVersion,
487
606
  statuses: z.array(BoardStatus),
488
607
  });
608
+ /**
609
+ * What the repair did, task by task (anchrd/intel#341).
610
+ *
611
+ * ⚠️ `previousId` is the id the entry shared, and it still names a task on this board — the FIRST
612
+ * entry under it, the one that kept it. That is what makes the repair readable: nothing that
613
+ * pointed at that id moved, so a caller can see which of the two the board's `parentId` and
614
+ * `dependsOn` edges have been meaning all along, and move them with `board_task_move` and
615
+ * `board_task_update` if they meant the other one.
616
+ *
617
+ * ⚠️ At least one entry, because a board with nothing to repair is refused rather than answered
618
+ * with an empty list and a new version that changed nothing.
619
+ */
620
+ export const RepairBoardTaskIdsResult = z.strictObject({
621
+ node: Node,
622
+ version: NodeVersion,
623
+ renumbered: z
624
+ .array(z.strictObject({ previousId: BoardTaskId, task: BoardTask }))
625
+ .min(1)
626
+ .max(5_000),
627
+ });
489
628
  // ── The agent definition (#139, ADR-0005 §4) ─────────────────────────────────────────────────────
490
629
  //
491
630
  // An agent's body is a definition, stored as an immutable version in R2 exactly like a document's.
@@ -1030,6 +1169,24 @@ export const ToolCapability = z.strictObject({
1030
1169
  export const ToolCatalog = z.strictObject({
1031
1170
  portalConnected: z.boolean(),
1032
1171
  items: z.array(ToolCapability),
1172
+ /**
1173
+ * Which delegated servers actually contributed a tool to this catalog (#289).
1174
+ *
1175
+ * ⚠️ Present only where the attribution was actually made — a delegated caller whose catalog was
1176
+ * read. It is absent for an ordinary user, and absent as well when the answer comes from one of
1177
+ * the short paths that never reach the portal (nothing delegated, no portal sign-in, connection
1178
+ * dropped). Absent therefore means "not stated", never "nothing arrived"; `[]` is the second one.
1179
+ *
1180
+ * That it is missing rather than empty on those paths is deliberate rather than half-finished:
1181
+ * the attribution already happens for a delegation — `capabilities` has to make it to cut the
1182
+ * list — so naming it costs nothing there, while computing the same thing for an ordinary user
1183
+ * would mean a second portal request per call, for a question their screen does not ask.
1184
+ *
1185
+ * ⚠️ It is the answer to "what arrived", never to "what was granted". A server missing here has
1186
+ * been switched off, revoked, or is failing right now; the delegation in the definition is
1187
+ * unchanged. Reading it the other way round would turn an outage into a permission change.
1188
+ */
1189
+ reached: z.array(ToolServerHandle).optional(),
1033
1190
  });
1034
1191
  /**
1035
1192
  * One MCP server the asking user reaches right now, as the portal itself names it (D30).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anchrd/intel-contract",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {