@cadraos/sdk 0.3.13 → 0.3.14-p46.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.
@@ -443,7 +443,17 @@ interface KanbanLane {
443
443
  * a tick fans out ⌈cap / chunkSize⌉ runs. Ignored for `per-card`/`event` lanes.
444
444
  */
445
445
  chunkSize?: number;
446
+ /**
447
+ * p46 Handoff contract, OPT-IN per lane (D1). Absent ⇒ `off` (existing boards
448
+ * are byte-identical). `verdict` ⇒ the engine sends
449
+ * `HANDOFF_VERDICT_JSON_SCHEMA` as the run's `context.outputSchema` and a
450
+ * completion MUST carry a valid verdict (else the run fails with a FAILED
451
+ * handoff). Active only on hosts whose adapter implements the ledger group.
452
+ */
453
+ handoff?: KanbanLaneHandoffMode;
446
454
  }
455
+ /** Per-lane Handoff mode (p46 D1). */
456
+ type KanbanLaneHandoffMode = "off" | "verdict";
447
457
  /**
448
458
  * A declared edge between two lanes in the lifecycle.
449
459
  *
@@ -720,6 +730,107 @@ interface ApplyTransitionInput {
720
730
  executionUuid?: string;
721
731
  }
722
732
 
733
+ /**
734
+ * Handoff v1 — the one AI↔human contract every board lane speaks (p46 §0.3).
735
+ *
736
+ * A Handoff is the record written whenever work crosses between an agent and a
737
+ * person on a card: an agent's verdict ("decide A/B/C", "approve", "test this"),
738
+ * an engine-written FAILED / BLOCKED, or the human's answer carried forward to
739
+ * the next agent (`human_to_ai`). Every worker type produces the same object and
740
+ * every host reads it.
741
+ *
742
+ * This module is DEPENDENCY-FREE and client-safe (types, the verdict JSON
743
+ * schema, the prompt renderer, the link guard). The ajv-backed verdict PARSER
744
+ * lives in `./handoff-parse.ts` and is exported only from the server engine
745
+ * entry (`@cadraos/sdk/kanban/engine`), so the browser bundle never carries ajv.
746
+ *
747
+ * @module @cadraos/sdk/kanban
748
+ */
749
+ type HandoffKind = "DECIDE" | "APPROVE" | "TEST" | "REVIEW" | "FAILED" | "BLOCKED" | "FYI";
750
+ interface HandoffOption {
751
+ /** Single capital letter, `^[A-Z]$`. */
752
+ id: string;
753
+ /** ≤80 chars. */
754
+ label: string;
755
+ /** ≤300 chars. */
756
+ consequence: string;
757
+ }
758
+ interface HandoffDecision {
759
+ /** ≤500 chars. */
760
+ question: string;
761
+ /** 2–4 options. */
762
+ options: HandoffOption[];
763
+ /** ∈ options[].id */
764
+ recommended: string;
765
+ /** ∈ options[].id */
766
+ defaultIfSilent?: string;
767
+ }
768
+ interface HandoffState {
769
+ repos: string[];
770
+ tests: string | null;
771
+ story: string | null;
772
+ costUsd: number | null;
773
+ }
774
+ interface HandoffAnswer {
775
+ option: string | null;
776
+ /** ≤4000 chars. */
777
+ text: string | null;
778
+ by: string;
779
+ at: string;
780
+ via: "board" | "channel";
781
+ source: string;
782
+ }
783
+ interface HandoffV1 {
784
+ v: 1;
785
+ /** = agentic_handoff.uuid */
786
+ id: string;
787
+ /** Card uuid. */
788
+ cardId: string;
789
+ direction: "ai_to_human" | "human_to_ai";
790
+ /** Lane KEYS. */
791
+ fromLane: string;
792
+ toLane: string;
793
+ /** ISO-8601. */
794
+ at: string;
795
+ by: {
796
+ type: "session" | "user" | "watchdog" | "timeout" | "health";
797
+ id: string;
798
+ };
799
+ kind: HandoffKind;
800
+ /** ≤200 chars. */
801
+ headline: string;
802
+ /** ≤20 × ≤300 chars. */
803
+ done: string[];
804
+ state: HandoffState;
805
+ decision?: HandoffDecision;
806
+ /** ≤300 chars. */
807
+ howToAnswer: string;
808
+ /** ≤10; url must be http(s): or a same-origin path starting "/". */
809
+ links: {
810
+ label: string;
811
+ url: string;
812
+ }[];
813
+ session: {
814
+ id: string | null;
815
+ runnerId: string | null;
816
+ resumableUntil: string | null;
817
+ };
818
+ /** BLOCKED only. */
819
+ blocked?: {
820
+ check: string;
821
+ reason: string;
822
+ fixUrl: string;
823
+ };
824
+ answer?: HandoffAnswer;
825
+ }
826
+ /** Ledger lifecycle of a stored handoff. */
827
+ type HandoffStatus = "open" | "answered" | "resolved" | "consumed";
828
+ /** A stored handoff: the v1 payload plus its ledger status. */
829
+ type HandoffRecord = HandoffV1 & {
830
+ status: HandoffStatus;
831
+ consumedAt: string | null;
832
+ };
833
+
723
834
  /**
724
835
  * KanbanStorageAdapter — the SERVER-ENGINE persistence contract.
725
836
  *
@@ -917,7 +1028,7 @@ interface KanbanStorageAdapter {
917
1028
  toLane?: string;
918
1029
  contextPatch?: Record<string, unknown>;
919
1030
  actor: Actor;
920
- }): Promise<{
1031
+ } & StageRunLedgerWrite): Promise<{
921
1032
  applied: boolean;
922
1033
  }>;
923
1034
  /**
@@ -930,7 +1041,7 @@ interface KanbanStorageAdapter {
930
1041
  */
931
1042
  failStageRun(idemKey: string, f: {
932
1043
  error: string;
933
- }): Promise<void>;
1044
+ } & StageRunLedgerWrite): Promise<void>;
934
1045
  /**
935
1046
  * OPTIONAL: record that a stage run was dispatched — set status `running`
936
1047
  * and stamp the `executionUuid` (so the UI can stream the live run before
@@ -944,6 +1055,65 @@ interface KanbanStorageAdapter {
944
1055
  * Read a stage run by its idempotency key, or null if none.
945
1056
  */
946
1057
  getStageRun(idemKey: string): Promise<StageRun | null>;
1058
+ /** OPTIONAL (ledger): insert a handoff (adapter assigns `id`), or read the existing one on conflict. */
1059
+ appendHandoff?(h: {
1060
+ card: KanbanCard;
1061
+ stageId: string;
1062
+ configVersion: number;
1063
+ idemKey?: string;
1064
+ attempt?: number;
1065
+ dedupeKey?: string;
1066
+ payload: Omit<HandoffV1, "id">;
1067
+ }): Promise<{
1068
+ created: boolean;
1069
+ handoff: HandoffV1;
1070
+ }>;
1071
+ /** OPTIONAL (ledger): a card's handoffs, NEWEST FIRST. */
1072
+ listHandoffs?(cardId: string, opts?: {
1073
+ limit?: number;
1074
+ }): Promise<HandoffRecord[]>;
1075
+ /** OPTIONAL (ledger): CAS `open → answered` (ai_to_human, kind ∈ DECIDE|APPROVE|TEST|REVIEW). */
1076
+ answerHandoff?(cardId: string, handoffId: string, answer: NonNullable<HandoffV1["answer"]>, userId?: number): Promise<{
1077
+ applied: boolean;
1078
+ }>;
1079
+ /** OPTIONAL (ledger): CAS `open → resolved` (BLOCKED / FAILED). */
1080
+ resolveHandoff?(handoffId: string): Promise<{
1081
+ applied: boolean;
1082
+ }>;
1083
+ /** OPTIONAL (ledger): `human_to_ai` rows `open → consumed` by the dispatch `idemKey`. */
1084
+ consumeHandoffs?(ids: string[], idemKey: string): Promise<void>;
1085
+ /** OPTIONAL (outbox): insert-or-read an op; `run: false` iff it is already `done`. */
1086
+ claimOp?(op: {
1087
+ opId: string;
1088
+ kind: string;
1089
+ cardId?: string;
1090
+ handoffId?: string;
1091
+ payload: unknown;
1092
+ }): Promise<{
1093
+ run: boolean;
1094
+ }>;
1095
+ /** OPTIONAL (outbox): mark an op `done`. */
1096
+ completeOp?(opId: string): Promise<void>;
1097
+ /** OPTIONAL (outbox): mark an op `failed` with its error (replayable). */
1098
+ failOp?(opId: string, error: string): Promise<void>;
1099
+ }
1100
+ /**
1101
+ * The optional ledger write carried on `completeStageRun` / `failStageRun`
1102
+ * (p46 F3). Ignored by adapters without the ledger group.
1103
+ *
1104
+ * When present, the adapter writes — INSIDE THE SAME TRANSACTION as the run /
1105
+ * card update — the `agentic_handoff` row (using `payload.id` as its uuid;
1106
+ * `ON CONFLICT (dedupe_key) DO NOTHING`) and, only when that row was newly
1107
+ * inserted, a `pending` op row with id `notifyOpId`. If the run update is a
1108
+ * no-op (`completeStageRun` on an already-completed run; `failStageRun` on a
1109
+ * completed run) nothing is written. A throw anywhere rolls ALL of it back.
1110
+ */
1111
+ interface StageRunLedgerWrite {
1112
+ handoff?: {
1113
+ payload: HandoffV1;
1114
+ dedupeKey: string;
1115
+ };
1116
+ notifyOpId?: string;
947
1117
  }
948
1118
 
949
1119
  /**
@@ -1418,6 +1588,8 @@ interface WireStage {
1418
1588
  */
1419
1589
  dispatch?: KanbanLaneDispatch;
1420
1590
  chunk_size?: number | null;
1591
+ /** p46 Handoff mode (D1). Absent ⇒ `off`. Mapped to `KanbanLane.handoff`. */
1592
+ handoff?: "off" | "verdict" | null;
1421
1593
  }
1422
1594
  /**
1423
1595
  * A resolved board config: stable uuid + version + the mapped `KanbanBoard`
@@ -3724,7 +3724,17 @@ interface KanbanLane {
3724
3724
  * a tick fans out ⌈cap / chunkSize⌉ runs. Ignored for `per-card`/`event` lanes.
3725
3725
  */
3726
3726
  chunkSize?: number;
3727
+ /**
3728
+ * p46 Handoff contract, OPT-IN per lane (D1). Absent ⇒ `off` (existing boards
3729
+ * are byte-identical). `verdict` ⇒ the engine sends
3730
+ * `HANDOFF_VERDICT_JSON_SCHEMA` as the run's `context.outputSchema` and a
3731
+ * completion MUST carry a valid verdict (else the run fails with a FAILED
3732
+ * handoff). Active only on hosts whose adapter implements the ledger group.
3733
+ */
3734
+ handoff?: KanbanLaneHandoffMode;
3727
3735
  }
3736
+ /** Per-lane Handoff mode (p46 D1). */
3737
+ type KanbanLaneHandoffMode = "off" | "verdict";
3728
3738
  /**
3729
3739
  * A declared edge between two lanes in the lifecycle.
3730
3740
  *
@@ -3724,7 +3724,17 @@ interface KanbanLane {
3724
3724
  * a tick fans out ⌈cap / chunkSize⌉ runs. Ignored for `per-card`/`event` lanes.
3725
3725
  */
3726
3726
  chunkSize?: number;
3727
+ /**
3728
+ * p46 Handoff contract, OPT-IN per lane (D1). Absent ⇒ `off` (existing boards
3729
+ * are byte-identical). `verdict` ⇒ the engine sends
3730
+ * `HANDOFF_VERDICT_JSON_SCHEMA` as the run's `context.outputSchema` and a
3731
+ * completion MUST carry a valid verdict (else the run fails with a FAILED
3732
+ * handoff). Active only on hosts whose adapter implements the ledger group.
3733
+ */
3734
+ handoff?: KanbanLaneHandoffMode;
3727
3735
  }
3736
+ /** Per-lane Handoff mode (p46 D1). */
3737
+ type KanbanLaneHandoffMode = "off" | "verdict";
3728
3738
  /**
3729
3739
  * A declared edge between two lanes in the lifecycle.
3730
3740
  *
package/dist/index.d.cts CHANGED
@@ -2795,7 +2795,17 @@ interface KanbanLane {
2795
2795
  * a tick fans out ⌈cap / chunkSize⌉ runs. Ignored for `per-card`/`event` lanes.
2796
2796
  */
2797
2797
  chunkSize?: number;
2798
+ /**
2799
+ * p46 Handoff contract, OPT-IN per lane (D1). Absent ⇒ `off` (existing boards
2800
+ * are byte-identical). `verdict` ⇒ the engine sends
2801
+ * `HANDOFF_VERDICT_JSON_SCHEMA` as the run's `context.outputSchema` and a
2802
+ * completion MUST carry a valid verdict (else the run fails with a FAILED
2803
+ * handoff). Active only on hosts whose adapter implements the ledger group.
2804
+ */
2805
+ handoff?: KanbanLaneHandoffMode;
2798
2806
  }
2807
+ /** Per-lane Handoff mode (p46 D1). */
2808
+ type KanbanLaneHandoffMode = "off" | "verdict";
2799
2809
  /**
2800
2810
  * A declared edge between two lanes in the lifecycle.
2801
2811
  *
package/dist/index.d.ts CHANGED
@@ -2795,7 +2795,17 @@ interface KanbanLane {
2795
2795
  * a tick fans out ⌈cap / chunkSize⌉ runs. Ignored for `per-card`/`event` lanes.
2796
2796
  */
2797
2797
  chunkSize?: number;
2798
+ /**
2799
+ * p46 Handoff contract, OPT-IN per lane (D1). Absent ⇒ `off` (existing boards
2800
+ * are byte-identical). `verdict` ⇒ the engine sends
2801
+ * `HANDOFF_VERDICT_JSON_SCHEMA` as the run's `context.outputSchema` and a
2802
+ * completion MUST carry a valid verdict (else the run fails with a FAILED
2803
+ * handoff). Active only on hosts whose adapter implements the ledger group.
2804
+ */
2805
+ handoff?: KanbanLaneHandoffMode;
2798
2806
  }
2807
+ /** Per-lane Handoff mode (p46 D1). */
2808
+ type KanbanLaneHandoffMode = "off" | "verdict";
2799
2809
  /**
2800
2810
  * A declared edge between two lanes in the lifecycle.
2801
2811
  *