@cadraos/sdk 0.3.12 → 0.3.14-p102.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 (39) hide show
  1. package/dist/board-host/index.cjs +217 -129
  2. package/dist/board-host/index.cjs.map +1 -1
  3. package/dist/board-host/index.d.cts +268 -3
  4. package/dist/board-host/index.d.ts +268 -3
  5. package/dist/board-host/index.js +218 -131
  6. package/dist/board-host/index.js.map +1 -1
  7. package/dist/board-host/server/index.d.cts +218 -2
  8. package/dist/board-host/server/index.d.ts +218 -2
  9. package/dist/chat/index.cjs +528 -47
  10. package/dist/chat/index.cjs.map +1 -1
  11. package/dist/chat/index.d.cts +29 -0
  12. package/dist/chat/index.d.ts +29 -0
  13. package/dist/chat/index.js +528 -47
  14. package/dist/chat/index.js.map +1 -1
  15. package/dist/index.cjs +453 -38
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +81 -1
  18. package/dist/index.d.ts +81 -1
  19. package/dist/index.js +453 -38
  20. package/dist/index.js.map +1 -1
  21. package/dist/kanban/engine/index.cjs +1535 -434
  22. package/dist/kanban/engine/index.cjs.map +1 -1
  23. package/dist/kanban/engine/index.d.cts +628 -6
  24. package/dist/kanban/engine/index.d.ts +628 -6
  25. package/dist/kanban/engine/index.js +1520 -435
  26. package/dist/kanban/engine/index.js.map +1 -1
  27. package/dist/kanban/index.cjs +751 -60
  28. package/dist/kanban/index.cjs.map +1 -1
  29. package/dist/kanban/index.d.cts +555 -9
  30. package/dist/kanban/index.d.ts +555 -9
  31. package/dist/kanban/index.js +747 -61
  32. package/dist/kanban/index.js.map +1 -1
  33. package/dist/teams/index.cjs +453 -38
  34. package/dist/teams/index.cjs.map +1 -1
  35. package/dist/teams/index.d.cts +27 -0
  36. package/dist/teams/index.d.ts +27 -0
  37. package/dist/teams/index.js +453 -38
  38. package/dist/teams/index.js.map +1 -1
  39. package/package.json +1 -1
@@ -213,7 +213,17 @@ interface KanbanLane {
213
213
  * a tick fans out ⌈cap / chunkSize⌉ runs. Ignored for `per-card`/`event` lanes.
214
214
  */
215
215
  chunkSize?: number;
216
+ /**
217
+ * p46 Handoff contract, OPT-IN per lane (D1). Absent ⇒ `off` (existing boards
218
+ * are byte-identical). `verdict` ⇒ the engine sends
219
+ * `HANDOFF_VERDICT_JSON_SCHEMA` as the run's `context.outputSchema` and a
220
+ * completion MUST carry a valid verdict (else the run fails with a FAILED
221
+ * handoff). Active only on hosts whose adapter implements the ledger group.
222
+ */
223
+ handoff?: KanbanLaneHandoffMode;
216
224
  }
225
+ /** Per-lane Handoff mode (p46 D1). */
226
+ type KanbanLaneHandoffMode = "off" | "verdict";
217
227
  /**
218
228
  * A declared edge between two lanes in the lifecycle.
219
229
  *
@@ -309,6 +319,11 @@ interface KanbanCard {
309
319
  configVersion?: number;
310
320
  /** Arbitrary shared context carried across transitions. */
311
321
  sharedContext?: Record<string, unknown>;
322
+ /**
323
+ * p46 (P46-002): the card's id in an external host (a Jira issue key).
324
+ * Absent on native / crm / yobo cards.
325
+ */
326
+ externalRef?: string;
312
327
  /**
313
328
  * Execution UUID of the card's current/last run, if any.
314
329
  *
@@ -371,6 +386,18 @@ interface CreateCardInput {
371
386
  * in the lifecycle's initial lane.
372
387
  */
373
388
  lane?: string;
389
+ /**
390
+ * p46 (P46-002): the card's id in an EXTERNAL host (a Jira issue key, e.g.
391
+ * `CAD-231`). Optional. When set, a card is created at most ONCE per
392
+ * `(boardConfigUuid, externalRef)`: a repeat create returns the existing card
393
+ * and dispatches nothing. Hosts that don't store it simply ignore it.
394
+ */
395
+ externalRef?: string;
396
+ /**
397
+ * p46 (P46-002): opaque host facts stored with the card (Jira `statusId`,
398
+ * `mover`, `detached`, …). Passed through to `adapter.createCard` untouched.
399
+ */
400
+ externalMeta?: Record<string, unknown>;
374
401
  }
375
402
  /**
376
403
  * Who performed a transition / stage-run (server-engine side).
@@ -488,8 +515,126 @@ interface ApplyTransitionInput {
488
515
  contextPatch?: Record<string, unknown>;
489
516
  /** Execution UUID to stamp on the card (handler advances). */
490
517
  executionUuid?: string;
518
+ /**
519
+ * p46 (P46-002, F8): set ONLY by `engine.hostMove` (a move the external host
520
+ * made — e.g. a Jira drag mirrored by the sync). A host-aware adapter, inside
521
+ * the same card lock as the `fromLane` CAS, (1) refuses the move with a
522
+ * `HostFenceError` while the card has any `host.*` op `pending` (D19 — never
523
+ * mistake our own half-done move for a human one), (2) writes `externalMeta`,
524
+ * then (3) moves. Adapters that don't know it ignore it (optional, additive).
525
+ */
526
+ hostObservation?: {
527
+ externalMeta?: Record<string, unknown>;
528
+ /**
529
+ * ISO time the host state was READ. The fence also refuses the move when a
530
+ * `host.*` op for the card settled (done/failed) after this instant — the
531
+ * read may predate our own write landing in the host.
532
+ */
533
+ observedAt?: string;
534
+ };
491
535
  }
492
536
 
537
+ /**
538
+ * Handoff v1 — the one AI↔human contract every board lane speaks (p46 §0.3).
539
+ *
540
+ * A Handoff is the record written whenever work crosses between an agent and a
541
+ * person on a card: an agent's verdict ("decide A/B/C", "approve", "test this"),
542
+ * an engine-written FAILED / BLOCKED, or the human's answer carried forward to
543
+ * the next agent (`human_to_ai`). Every worker type produces the same object and
544
+ * every host reads it.
545
+ *
546
+ * This module is DEPENDENCY-FREE and client-safe (types, the verdict JSON
547
+ * schema, the prompt renderer, the link guard). The ajv-backed verdict PARSER
548
+ * lives in `./handoff-parse.ts` and is exported only from the server engine
549
+ * entry (`@cadraos/sdk/kanban/engine`), so the browser bundle never carries ajv.
550
+ *
551
+ * @module @cadraos/sdk/kanban
552
+ */
553
+ type HandoffKind = "DECIDE" | "APPROVE" | "TEST" | "REVIEW" | "FAILED" | "BLOCKED" | "FYI";
554
+ interface HandoffOption {
555
+ /** Single capital letter, `^[A-Z]$`. */
556
+ id: string;
557
+ /** ≤80 chars. */
558
+ label: string;
559
+ /** ≤300 chars. */
560
+ consequence: string;
561
+ }
562
+ interface HandoffDecision {
563
+ /** ≤500 chars. */
564
+ question: string;
565
+ /** 2–4 options. */
566
+ options: HandoffOption[];
567
+ /** ∈ options[].id */
568
+ recommended: string;
569
+ /** ∈ options[].id */
570
+ defaultIfSilent?: string;
571
+ }
572
+ interface HandoffState {
573
+ repos: string[];
574
+ tests: string | null;
575
+ story: string | null;
576
+ costUsd: number | null;
577
+ }
578
+ interface HandoffAnswer {
579
+ option: string | null;
580
+ /** ≤4000 chars. */
581
+ text: string | null;
582
+ by: string;
583
+ at: string;
584
+ via: "board" | "channel";
585
+ source: string;
586
+ }
587
+ interface HandoffV1 {
588
+ v: 1;
589
+ /** = agentic_handoff.uuid */
590
+ id: string;
591
+ /** Card uuid. */
592
+ cardId: string;
593
+ direction: "ai_to_human" | "human_to_ai";
594
+ /** Lane KEYS. */
595
+ fromLane: string;
596
+ toLane: string;
597
+ /** ISO-8601. */
598
+ at: string;
599
+ by: {
600
+ type: "session" | "user" | "watchdog" | "timeout" | "health";
601
+ id: string;
602
+ };
603
+ kind: HandoffKind;
604
+ /** ≤200 chars. */
605
+ headline: string;
606
+ /** ≤20 × ≤300 chars. */
607
+ done: string[];
608
+ state: HandoffState;
609
+ decision?: HandoffDecision;
610
+ /** ≤300 chars. */
611
+ howToAnswer: string;
612
+ /** ≤10; url must be http(s): or a same-origin path starting "/". */
613
+ links: {
614
+ label: string;
615
+ url: string;
616
+ }[];
617
+ session: {
618
+ id: string | null;
619
+ runnerId: string | null;
620
+ resumableUntil: string | null;
621
+ };
622
+ /** BLOCKED only. */
623
+ blocked?: {
624
+ check: string;
625
+ reason: string;
626
+ fixUrl: string;
627
+ };
628
+ answer?: HandoffAnswer;
629
+ }
630
+ /** Ledger lifecycle of a stored handoff. */
631
+ type HandoffStatus = "open" | "answered" | "resolved" | "consumed";
632
+ /** A stored handoff: the v1 payload plus its ledger status. */
633
+ type HandoffRecord = HandoffV1 & {
634
+ status: HandoffStatus;
635
+ consumedAt: string | null;
636
+ };
637
+
493
638
  /**
494
639
  * KanbanStorageAdapter — the SERVER-ENGINE persistence contract.
495
640
  *
@@ -571,6 +716,16 @@ interface KanbanStorageAdapter {
571
716
  * config and passes it as `initialLane`.
572
717
  */
573
718
  createCard(input: CreateCardServerInput): Promise<KanbanCard>;
719
+ /**
720
+ * p46 (P46-002) OPTIONAL: the card with this `externalRef` on this board, or
721
+ * null. When present, `engine.createCard({externalRef})` looks it up first and
722
+ * returns an existing card WITHOUT dispatching (exactly-once create per issue
723
+ * key). A host that stores `externalRef` SHOULD also make `createCard` itself
724
+ * idempotent on `(boardConfigUuid, externalRef)` (unique index + `ON CONFLICT
725
+ * DO NOTHING` → return the existing row) for the lookup/insert race. Hosts
726
+ * without external refs (crm/yobo) omit it.
727
+ */
728
+ getCardByExternalRef?(boardConfigUuid: string, externalRef: string): Promise<KanbanCard | null>;
574
729
  /**
575
730
  * Patch a card's mutable fields.
576
731
  */
@@ -687,7 +842,7 @@ interface KanbanStorageAdapter {
687
842
  toLane?: string;
688
843
  contextPatch?: Record<string, unknown>;
689
844
  actor: Actor;
690
- }): Promise<{
845
+ } & StageRunLedgerWrite): Promise<{
691
846
  applied: boolean;
692
847
  }>;
693
848
  /**
@@ -700,7 +855,7 @@ interface KanbanStorageAdapter {
700
855
  */
701
856
  failStageRun(idemKey: string, f: {
702
857
  error: string;
703
- }): Promise<void>;
858
+ } & StageRunLedgerWrite): Promise<void>;
704
859
  /**
705
860
  * OPTIONAL: record that a stage run was dispatched — set status `running`
706
861
  * and stamp the `executionUuid` (so the UI can stream the live run before
@@ -714,6 +869,65 @@ interface KanbanStorageAdapter {
714
869
  * Read a stage run by its idempotency key, or null if none.
715
870
  */
716
871
  getStageRun(idemKey: string): Promise<StageRun | null>;
872
+ /** OPTIONAL (ledger): insert a handoff (adapter assigns `id`), or read the existing one on conflict. */
873
+ appendHandoff?(h: {
874
+ card: KanbanCard;
875
+ stageId: string;
876
+ configVersion: number;
877
+ idemKey?: string;
878
+ attempt?: number;
879
+ dedupeKey?: string;
880
+ payload: Omit<HandoffV1, "id">;
881
+ }): Promise<{
882
+ created: boolean;
883
+ handoff: HandoffV1;
884
+ }>;
885
+ /** OPTIONAL (ledger): a card's handoffs, NEWEST FIRST. */
886
+ listHandoffs?(cardId: string, opts?: {
887
+ limit?: number;
888
+ }): Promise<HandoffRecord[]>;
889
+ /** OPTIONAL (ledger): CAS `open → answered` (ai_to_human, kind ∈ DECIDE|APPROVE|TEST|REVIEW). */
890
+ answerHandoff?(cardId: string, handoffId: string, answer: NonNullable<HandoffV1["answer"]>, userId?: number): Promise<{
891
+ applied: boolean;
892
+ }>;
893
+ /** OPTIONAL (ledger): CAS `open → resolved` (BLOCKED / FAILED). */
894
+ resolveHandoff?(handoffId: string): Promise<{
895
+ applied: boolean;
896
+ }>;
897
+ /** OPTIONAL (ledger): `human_to_ai` rows `open → consumed` by the dispatch `idemKey`. */
898
+ consumeHandoffs?(ids: string[], idemKey: string): Promise<void>;
899
+ /** OPTIONAL (outbox): insert-or-read an op; `run: false` iff it is already `done`. */
900
+ claimOp?(op: {
901
+ opId: string;
902
+ kind: string;
903
+ cardId?: string;
904
+ handoffId?: string;
905
+ payload: unknown;
906
+ }): Promise<{
907
+ run: boolean;
908
+ }>;
909
+ /** OPTIONAL (outbox): mark an op `done`. */
910
+ completeOp?(opId: string): Promise<void>;
911
+ /** OPTIONAL (outbox): mark an op `failed` with its error (replayable). */
912
+ failOp?(opId: string, error: string): Promise<void>;
913
+ }
914
+ /**
915
+ * The optional ledger write carried on `completeStageRun` / `failStageRun`
916
+ * (p46 F3). Ignored by adapters without the ledger group.
917
+ *
918
+ * When present, the adapter writes — INSIDE THE SAME TRANSACTION as the run /
919
+ * card update — the `agentic_handoff` row (using `payload.id` as its uuid;
920
+ * `ON CONFLICT (dedupe_key) DO NOTHING`) and, only when that row was newly
921
+ * inserted, a `pending` op row with id `notifyOpId`. If the run update is a
922
+ * no-op (`completeStageRun` on an already-completed run; `failStageRun` on a
923
+ * completed run) nothing is written. A throw anywhere rolls ALL of it back.
924
+ */
925
+ interface StageRunLedgerWrite {
926
+ handoff?: {
927
+ payload: HandoffV1;
928
+ dedupeKey: string;
929
+ };
930
+ notifyOpId?: string;
717
931
  }
718
932
 
719
933
  /**
@@ -1198,6 +1412,57 @@ interface BoardHostMappingProps {
1198
1412
  }
1199
1413
  declare function BoardHostMapping({ boardConfigUuid, adapter: adapterProp, onSaved, onError, }: BoardHostMappingProps): react_jsx_runtime.JSX.Element;
1200
1414
 
1415
+ /**
1416
+ * `<LaneMapStep/>` — the lane-map rows (board stage → host state), extracted
1417
+ * from `<BoardHostMapping/>` ② (P46-002-1) so a hosted host (the Jira link
1418
+ * page) reuses the same step.
1419
+ *
1420
+ * - WITHOUT `hostStates`: today's free-text host-state input per lane — the
1421
+ * crm/yobo `<BoardHostMapping/>` path, DOM unchanged.
1422
+ * - WITH `hostStates` (e.g. a Jira board's column statuses): a `<select>` per
1423
+ * lane plus an "Also accepts" multi-select (D17: inbound, any of
1424
+ * `[hostState, ...alsoFrom]` maps to the lane; outbound writes `hostState`).
1425
+ *
1426
+ * Controlled: `value` in, the whole next map out via `onChange`. Zero runtime
1427
+ * deps; plain inline styles.
1428
+ *
1429
+ * @module @cadraos/sdk/board-host
1430
+ */
1431
+
1432
+ /** One lane's host-state binding (`LaneBinding` minus the stage identity). */
1433
+ interface LaneMapEntry {
1434
+ /** The host state a move INTO this lane writes (and reads). */
1435
+ hostState: string;
1436
+ /** Extra host states that also map INTO this lane (inbound only, D17). */
1437
+ alsoFrom?: string[];
1438
+ }
1439
+ /** Lane map keyed by stage `key`. */
1440
+ type LaneMapValue = Record<string, LaneMapEntry>;
1441
+ /** A host state the host offers (e.g. a Jira status: id + display name). */
1442
+ interface HostStateOption {
1443
+ value: string;
1444
+ label: string;
1445
+ }
1446
+ interface LaneMapStepProps {
1447
+ /** Board lanes, in order. */
1448
+ lanes: KanbanLane[];
1449
+ /** Current map (keyed by stage key). */
1450
+ value: LaneMapValue;
1451
+ /** Receives the WHOLE next map. */
1452
+ onChange: (next: LaneMapValue) => void;
1453
+ /** Offered host states → `<select>` mode; absent → free-text mode. */
1454
+ hostStates?: HostStateOption[];
1455
+ /**
1456
+ * Optional per-lane "create if missing" checkbox (the p34 crm/yobo mapping).
1457
+ * Absent → no checkbox.
1458
+ */
1459
+ createIfMissing?: {
1460
+ value: Record<string, boolean>;
1461
+ onChange: (stageKey: string, checked: boolean) => void;
1462
+ };
1463
+ }
1464
+ declare function LaneMapStep({ lanes, value, onChange, hostStates, createIfMissing, }: LaneMapStepProps): React.ReactElement;
1465
+
1201
1466
  interface BoardHostMappingProviderProps {
1202
1467
  /**
1203
1468
  * The session-authed adapter the host injects (wraps the §6b endpoints with
@@ -1280,4 +1545,4 @@ interface DueRef {
1280
1545
  */
1281
1546
  declare function collectDueRefs(lanes: KanbanLane[]): DueRef[];
1282
1547
 
1283
- export { type BoardHostDelegate, BoardHostMapping, type BoardHostMappingAdapter, type BoardHostMapping$1 as BoardHostMappingData, type BoardHostMappingProps, BoardHostMappingProvider, type BoardHostMappingProviderProps, type BoardQueryRunner, type DueRef, type FieldBinding, type LaneBinding, type MappingValidationStatus, type ScopeBinding, WIDGET_IDENTIFIER_PATTERN, collectDueRefs, identifierError, isValidIdentifier, isValidJsonPath, useBoardHostMappingAdapter, useBoardHostMappingAdapterOptional };
1548
+ export { type BoardHostDelegate, BoardHostMapping, type BoardHostMappingAdapter, type BoardHostMapping$1 as BoardHostMappingData, type BoardHostMappingProps, BoardHostMappingProvider, type BoardHostMappingProviderProps, type BoardQueryRunner, type DueRef, type FieldBinding, type HostStateOption, type LaneBinding, type LaneMapEntry, LaneMapStep, type LaneMapStepProps, type LaneMapValue, type MappingValidationStatus, type ScopeBinding, WIDGET_IDENTIFIER_PATTERN, collectDueRefs, identifierError, isValidIdentifier, isValidJsonPath, useBoardHostMappingAdapter, useBoardHostMappingAdapterOptional };
@@ -213,7 +213,17 @@ interface KanbanLane {
213
213
  * a tick fans out ⌈cap / chunkSize⌉ runs. Ignored for `per-card`/`event` lanes.
214
214
  */
215
215
  chunkSize?: number;
216
+ /**
217
+ * p46 Handoff contract, OPT-IN per lane (D1). Absent ⇒ `off` (existing boards
218
+ * are byte-identical). `verdict` ⇒ the engine sends
219
+ * `HANDOFF_VERDICT_JSON_SCHEMA` as the run's `context.outputSchema` and a
220
+ * completion MUST carry a valid verdict (else the run fails with a FAILED
221
+ * handoff). Active only on hosts whose adapter implements the ledger group.
222
+ */
223
+ handoff?: KanbanLaneHandoffMode;
216
224
  }
225
+ /** Per-lane Handoff mode (p46 D1). */
226
+ type KanbanLaneHandoffMode = "off" | "verdict";
217
227
  /**
218
228
  * A declared edge between two lanes in the lifecycle.
219
229
  *
@@ -309,6 +319,11 @@ interface KanbanCard {
309
319
  configVersion?: number;
310
320
  /** Arbitrary shared context carried across transitions. */
311
321
  sharedContext?: Record<string, unknown>;
322
+ /**
323
+ * p46 (P46-002): the card's id in an external host (a Jira issue key).
324
+ * Absent on native / crm / yobo cards.
325
+ */
326
+ externalRef?: string;
312
327
  /**
313
328
  * Execution UUID of the card's current/last run, if any.
314
329
  *
@@ -371,6 +386,18 @@ interface CreateCardInput {
371
386
  * in the lifecycle's initial lane.
372
387
  */
373
388
  lane?: string;
389
+ /**
390
+ * p46 (P46-002): the card's id in an EXTERNAL host (a Jira issue key, e.g.
391
+ * `CAD-231`). Optional. When set, a card is created at most ONCE per
392
+ * `(boardConfigUuid, externalRef)`: a repeat create returns the existing card
393
+ * and dispatches nothing. Hosts that don't store it simply ignore it.
394
+ */
395
+ externalRef?: string;
396
+ /**
397
+ * p46 (P46-002): opaque host facts stored with the card (Jira `statusId`,
398
+ * `mover`, `detached`, …). Passed through to `adapter.createCard` untouched.
399
+ */
400
+ externalMeta?: Record<string, unknown>;
374
401
  }
375
402
  /**
376
403
  * Who performed a transition / stage-run (server-engine side).
@@ -488,8 +515,126 @@ interface ApplyTransitionInput {
488
515
  contextPatch?: Record<string, unknown>;
489
516
  /** Execution UUID to stamp on the card (handler advances). */
490
517
  executionUuid?: string;
518
+ /**
519
+ * p46 (P46-002, F8): set ONLY by `engine.hostMove` (a move the external host
520
+ * made — e.g. a Jira drag mirrored by the sync). A host-aware adapter, inside
521
+ * the same card lock as the `fromLane` CAS, (1) refuses the move with a
522
+ * `HostFenceError` while the card has any `host.*` op `pending` (D19 — never
523
+ * mistake our own half-done move for a human one), (2) writes `externalMeta`,
524
+ * then (3) moves. Adapters that don't know it ignore it (optional, additive).
525
+ */
526
+ hostObservation?: {
527
+ externalMeta?: Record<string, unknown>;
528
+ /**
529
+ * ISO time the host state was READ. The fence also refuses the move when a
530
+ * `host.*` op for the card settled (done/failed) after this instant — the
531
+ * read may predate our own write landing in the host.
532
+ */
533
+ observedAt?: string;
534
+ };
491
535
  }
492
536
 
537
+ /**
538
+ * Handoff v1 — the one AI↔human contract every board lane speaks (p46 §0.3).
539
+ *
540
+ * A Handoff is the record written whenever work crosses between an agent and a
541
+ * person on a card: an agent's verdict ("decide A/B/C", "approve", "test this"),
542
+ * an engine-written FAILED / BLOCKED, or the human's answer carried forward to
543
+ * the next agent (`human_to_ai`). Every worker type produces the same object and
544
+ * every host reads it.
545
+ *
546
+ * This module is DEPENDENCY-FREE and client-safe (types, the verdict JSON
547
+ * schema, the prompt renderer, the link guard). The ajv-backed verdict PARSER
548
+ * lives in `./handoff-parse.ts` and is exported only from the server engine
549
+ * entry (`@cadraos/sdk/kanban/engine`), so the browser bundle never carries ajv.
550
+ *
551
+ * @module @cadraos/sdk/kanban
552
+ */
553
+ type HandoffKind = "DECIDE" | "APPROVE" | "TEST" | "REVIEW" | "FAILED" | "BLOCKED" | "FYI";
554
+ interface HandoffOption {
555
+ /** Single capital letter, `^[A-Z]$`. */
556
+ id: string;
557
+ /** ≤80 chars. */
558
+ label: string;
559
+ /** ≤300 chars. */
560
+ consequence: string;
561
+ }
562
+ interface HandoffDecision {
563
+ /** ≤500 chars. */
564
+ question: string;
565
+ /** 2–4 options. */
566
+ options: HandoffOption[];
567
+ /** ∈ options[].id */
568
+ recommended: string;
569
+ /** ∈ options[].id */
570
+ defaultIfSilent?: string;
571
+ }
572
+ interface HandoffState {
573
+ repos: string[];
574
+ tests: string | null;
575
+ story: string | null;
576
+ costUsd: number | null;
577
+ }
578
+ interface HandoffAnswer {
579
+ option: string | null;
580
+ /** ≤4000 chars. */
581
+ text: string | null;
582
+ by: string;
583
+ at: string;
584
+ via: "board" | "channel";
585
+ source: string;
586
+ }
587
+ interface HandoffV1 {
588
+ v: 1;
589
+ /** = agentic_handoff.uuid */
590
+ id: string;
591
+ /** Card uuid. */
592
+ cardId: string;
593
+ direction: "ai_to_human" | "human_to_ai";
594
+ /** Lane KEYS. */
595
+ fromLane: string;
596
+ toLane: string;
597
+ /** ISO-8601. */
598
+ at: string;
599
+ by: {
600
+ type: "session" | "user" | "watchdog" | "timeout" | "health";
601
+ id: string;
602
+ };
603
+ kind: HandoffKind;
604
+ /** ≤200 chars. */
605
+ headline: string;
606
+ /** ≤20 × ≤300 chars. */
607
+ done: string[];
608
+ state: HandoffState;
609
+ decision?: HandoffDecision;
610
+ /** ≤300 chars. */
611
+ howToAnswer: string;
612
+ /** ≤10; url must be http(s): or a same-origin path starting "/". */
613
+ links: {
614
+ label: string;
615
+ url: string;
616
+ }[];
617
+ session: {
618
+ id: string | null;
619
+ runnerId: string | null;
620
+ resumableUntil: string | null;
621
+ };
622
+ /** BLOCKED only. */
623
+ blocked?: {
624
+ check: string;
625
+ reason: string;
626
+ fixUrl: string;
627
+ };
628
+ answer?: HandoffAnswer;
629
+ }
630
+ /** Ledger lifecycle of a stored handoff. */
631
+ type HandoffStatus = "open" | "answered" | "resolved" | "consumed";
632
+ /** A stored handoff: the v1 payload plus its ledger status. */
633
+ type HandoffRecord = HandoffV1 & {
634
+ status: HandoffStatus;
635
+ consumedAt: string | null;
636
+ };
637
+
493
638
  /**
494
639
  * KanbanStorageAdapter — the SERVER-ENGINE persistence contract.
495
640
  *
@@ -571,6 +716,16 @@ interface KanbanStorageAdapter {
571
716
  * config and passes it as `initialLane`.
572
717
  */
573
718
  createCard(input: CreateCardServerInput): Promise<KanbanCard>;
719
+ /**
720
+ * p46 (P46-002) OPTIONAL: the card with this `externalRef` on this board, or
721
+ * null. When present, `engine.createCard({externalRef})` looks it up first and
722
+ * returns an existing card WITHOUT dispatching (exactly-once create per issue
723
+ * key). A host that stores `externalRef` SHOULD also make `createCard` itself
724
+ * idempotent on `(boardConfigUuid, externalRef)` (unique index + `ON CONFLICT
725
+ * DO NOTHING` → return the existing row) for the lookup/insert race. Hosts
726
+ * without external refs (crm/yobo) omit it.
727
+ */
728
+ getCardByExternalRef?(boardConfigUuid: string, externalRef: string): Promise<KanbanCard | null>;
574
729
  /**
575
730
  * Patch a card's mutable fields.
576
731
  */
@@ -687,7 +842,7 @@ interface KanbanStorageAdapter {
687
842
  toLane?: string;
688
843
  contextPatch?: Record<string, unknown>;
689
844
  actor: Actor;
690
- }): Promise<{
845
+ } & StageRunLedgerWrite): Promise<{
691
846
  applied: boolean;
692
847
  }>;
693
848
  /**
@@ -700,7 +855,7 @@ interface KanbanStorageAdapter {
700
855
  */
701
856
  failStageRun(idemKey: string, f: {
702
857
  error: string;
703
- }): Promise<void>;
858
+ } & StageRunLedgerWrite): Promise<void>;
704
859
  /**
705
860
  * OPTIONAL: record that a stage run was dispatched — set status `running`
706
861
  * and stamp the `executionUuid` (so the UI can stream the live run before
@@ -714,6 +869,65 @@ interface KanbanStorageAdapter {
714
869
  * Read a stage run by its idempotency key, or null if none.
715
870
  */
716
871
  getStageRun(idemKey: string): Promise<StageRun | null>;
872
+ /** OPTIONAL (ledger): insert a handoff (adapter assigns `id`), or read the existing one on conflict. */
873
+ appendHandoff?(h: {
874
+ card: KanbanCard;
875
+ stageId: string;
876
+ configVersion: number;
877
+ idemKey?: string;
878
+ attempt?: number;
879
+ dedupeKey?: string;
880
+ payload: Omit<HandoffV1, "id">;
881
+ }): Promise<{
882
+ created: boolean;
883
+ handoff: HandoffV1;
884
+ }>;
885
+ /** OPTIONAL (ledger): a card's handoffs, NEWEST FIRST. */
886
+ listHandoffs?(cardId: string, opts?: {
887
+ limit?: number;
888
+ }): Promise<HandoffRecord[]>;
889
+ /** OPTIONAL (ledger): CAS `open → answered` (ai_to_human, kind ∈ DECIDE|APPROVE|TEST|REVIEW). */
890
+ answerHandoff?(cardId: string, handoffId: string, answer: NonNullable<HandoffV1["answer"]>, userId?: number): Promise<{
891
+ applied: boolean;
892
+ }>;
893
+ /** OPTIONAL (ledger): CAS `open → resolved` (BLOCKED / FAILED). */
894
+ resolveHandoff?(handoffId: string): Promise<{
895
+ applied: boolean;
896
+ }>;
897
+ /** OPTIONAL (ledger): `human_to_ai` rows `open → consumed` by the dispatch `idemKey`. */
898
+ consumeHandoffs?(ids: string[], idemKey: string): Promise<void>;
899
+ /** OPTIONAL (outbox): insert-or-read an op; `run: false` iff it is already `done`. */
900
+ claimOp?(op: {
901
+ opId: string;
902
+ kind: string;
903
+ cardId?: string;
904
+ handoffId?: string;
905
+ payload: unknown;
906
+ }): Promise<{
907
+ run: boolean;
908
+ }>;
909
+ /** OPTIONAL (outbox): mark an op `done`. */
910
+ completeOp?(opId: string): Promise<void>;
911
+ /** OPTIONAL (outbox): mark an op `failed` with its error (replayable). */
912
+ failOp?(opId: string, error: string): Promise<void>;
913
+ }
914
+ /**
915
+ * The optional ledger write carried on `completeStageRun` / `failStageRun`
916
+ * (p46 F3). Ignored by adapters without the ledger group.
917
+ *
918
+ * When present, the adapter writes — INSIDE THE SAME TRANSACTION as the run /
919
+ * card update — the `agentic_handoff` row (using `payload.id` as its uuid;
920
+ * `ON CONFLICT (dedupe_key) DO NOTHING`) and, only when that row was newly
921
+ * inserted, a `pending` op row with id `notifyOpId`. If the run update is a
922
+ * no-op (`completeStageRun` on an already-completed run; `failStageRun` on a
923
+ * completed run) nothing is written. A throw anywhere rolls ALL of it back.
924
+ */
925
+ interface StageRunLedgerWrite {
926
+ handoff?: {
927
+ payload: HandoffV1;
928
+ dedupeKey: string;
929
+ };
930
+ notifyOpId?: string;
717
931
  }
718
932
 
719
933
  /**
@@ -1198,6 +1412,57 @@ interface BoardHostMappingProps {
1198
1412
  }
1199
1413
  declare function BoardHostMapping({ boardConfigUuid, adapter: adapterProp, onSaved, onError, }: BoardHostMappingProps): react_jsx_runtime.JSX.Element;
1200
1414
 
1415
+ /**
1416
+ * `<LaneMapStep/>` — the lane-map rows (board stage → host state), extracted
1417
+ * from `<BoardHostMapping/>` ② (P46-002-1) so a hosted host (the Jira link
1418
+ * page) reuses the same step.
1419
+ *
1420
+ * - WITHOUT `hostStates`: today's free-text host-state input per lane — the
1421
+ * crm/yobo `<BoardHostMapping/>` path, DOM unchanged.
1422
+ * - WITH `hostStates` (e.g. a Jira board's column statuses): a `<select>` per
1423
+ * lane plus an "Also accepts" multi-select (D17: inbound, any of
1424
+ * `[hostState, ...alsoFrom]` maps to the lane; outbound writes `hostState`).
1425
+ *
1426
+ * Controlled: `value` in, the whole next map out via `onChange`. Zero runtime
1427
+ * deps; plain inline styles.
1428
+ *
1429
+ * @module @cadraos/sdk/board-host
1430
+ */
1431
+
1432
+ /** One lane's host-state binding (`LaneBinding` minus the stage identity). */
1433
+ interface LaneMapEntry {
1434
+ /** The host state a move INTO this lane writes (and reads). */
1435
+ hostState: string;
1436
+ /** Extra host states that also map INTO this lane (inbound only, D17). */
1437
+ alsoFrom?: string[];
1438
+ }
1439
+ /** Lane map keyed by stage `key`. */
1440
+ type LaneMapValue = Record<string, LaneMapEntry>;
1441
+ /** A host state the host offers (e.g. a Jira status: id + display name). */
1442
+ interface HostStateOption {
1443
+ value: string;
1444
+ label: string;
1445
+ }
1446
+ interface LaneMapStepProps {
1447
+ /** Board lanes, in order. */
1448
+ lanes: KanbanLane[];
1449
+ /** Current map (keyed by stage key). */
1450
+ value: LaneMapValue;
1451
+ /** Receives the WHOLE next map. */
1452
+ onChange: (next: LaneMapValue) => void;
1453
+ /** Offered host states → `<select>` mode; absent → free-text mode. */
1454
+ hostStates?: HostStateOption[];
1455
+ /**
1456
+ * Optional per-lane "create if missing" checkbox (the p34 crm/yobo mapping).
1457
+ * Absent → no checkbox.
1458
+ */
1459
+ createIfMissing?: {
1460
+ value: Record<string, boolean>;
1461
+ onChange: (stageKey: string, checked: boolean) => void;
1462
+ };
1463
+ }
1464
+ declare function LaneMapStep({ lanes, value, onChange, hostStates, createIfMissing, }: LaneMapStepProps): React.ReactElement;
1465
+
1201
1466
  interface BoardHostMappingProviderProps {
1202
1467
  /**
1203
1468
  * The session-authed adapter the host injects (wraps the §6b endpoints with
@@ -1280,4 +1545,4 @@ interface DueRef {
1280
1545
  */
1281
1546
  declare function collectDueRefs(lanes: KanbanLane[]): DueRef[];
1282
1547
 
1283
- export { type BoardHostDelegate, BoardHostMapping, type BoardHostMappingAdapter, type BoardHostMapping$1 as BoardHostMappingData, type BoardHostMappingProps, BoardHostMappingProvider, type BoardHostMappingProviderProps, type BoardQueryRunner, type DueRef, type FieldBinding, type LaneBinding, type MappingValidationStatus, type ScopeBinding, WIDGET_IDENTIFIER_PATTERN, collectDueRefs, identifierError, isValidIdentifier, isValidJsonPath, useBoardHostMappingAdapter, useBoardHostMappingAdapterOptional };
1548
+ export { type BoardHostDelegate, BoardHostMapping, type BoardHostMappingAdapter, type BoardHostMapping$1 as BoardHostMappingData, type BoardHostMappingProps, BoardHostMappingProvider, type BoardHostMappingProviderProps, type BoardQueryRunner, type DueRef, type FieldBinding, type HostStateOption, type LaneBinding, type LaneMapEntry, LaneMapStep, type LaneMapStepProps, type LaneMapValue, type MappingValidationStatus, type ScopeBinding, WIDGET_IDENTIFIER_PATTERN, collectDueRefs, identifierError, isValidIdentifier, isValidJsonPath, useBoardHostMappingAdapter, useBoardHostMappingAdapterOptional };