@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.
- package/dist/board-host/index.cjs +217 -129
- package/dist/board-host/index.cjs.map +1 -1
- package/dist/board-host/index.d.cts +268 -3
- package/dist/board-host/index.d.ts +268 -3
- package/dist/board-host/index.js +218 -131
- package/dist/board-host/index.js.map +1 -1
- package/dist/board-host/server/index.d.cts +218 -2
- package/dist/board-host/server/index.d.ts +218 -2
- package/dist/chat/index.cjs +528 -47
- package/dist/chat/index.cjs.map +1 -1
- package/dist/chat/index.d.cts +29 -0
- package/dist/chat/index.d.ts +29 -0
- package/dist/chat/index.js +528 -47
- package/dist/chat/index.js.map +1 -1
- package/dist/index.cjs +453 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -1
- package/dist/index.d.ts +81 -1
- package/dist/index.js +453 -38
- package/dist/index.js.map +1 -1
- package/dist/kanban/engine/index.cjs +1535 -434
- package/dist/kanban/engine/index.cjs.map +1 -1
- package/dist/kanban/engine/index.d.cts +628 -6
- package/dist/kanban/engine/index.d.ts +628 -6
- package/dist/kanban/engine/index.js +1520 -435
- package/dist/kanban/engine/index.js.map +1 -1
- package/dist/kanban/index.cjs +751 -60
- package/dist/kanban/index.cjs.map +1 -1
- package/dist/kanban/index.d.cts +555 -9
- package/dist/kanban/index.d.ts +555 -9
- package/dist/kanban/index.js +747 -61
- package/dist/kanban/index.js.map +1 -1
- package/dist/teams/index.cjs +453 -38
- package/dist/teams/index.cjs.map +1 -1
- package/dist/teams/index.d.cts +27 -0
- package/dist/teams/index.d.ts +27 -0
- package/dist/teams/index.js +453 -38
- package/dist/teams/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
*
|
|
@@ -539,6 +549,11 @@ interface KanbanCard {
|
|
|
539
549
|
configVersion?: number;
|
|
540
550
|
/** Arbitrary shared context carried across transitions. */
|
|
541
551
|
sharedContext?: Record<string, unknown>;
|
|
552
|
+
/**
|
|
553
|
+
* p46 (P46-002): the card's id in an external host (a Jira issue key).
|
|
554
|
+
* Absent on native / crm / yobo cards.
|
|
555
|
+
*/
|
|
556
|
+
externalRef?: string;
|
|
542
557
|
/**
|
|
543
558
|
* Execution UUID of the card's current/last run, if any.
|
|
544
559
|
*
|
|
@@ -601,6 +616,18 @@ interface CreateCardInput {
|
|
|
601
616
|
* in the lifecycle's initial lane.
|
|
602
617
|
*/
|
|
603
618
|
lane?: string;
|
|
619
|
+
/**
|
|
620
|
+
* p46 (P46-002): the card's id in an EXTERNAL host (a Jira issue key, e.g.
|
|
621
|
+
* `CAD-231`). Optional. When set, a card is created at most ONCE per
|
|
622
|
+
* `(boardConfigUuid, externalRef)`: a repeat create returns the existing card
|
|
623
|
+
* and dispatches nothing. Hosts that don't store it simply ignore it.
|
|
624
|
+
*/
|
|
625
|
+
externalRef?: string;
|
|
626
|
+
/**
|
|
627
|
+
* p46 (P46-002): opaque host facts stored with the card (Jira `statusId`,
|
|
628
|
+
* `mover`, `detached`, …). Passed through to `adapter.createCard` untouched.
|
|
629
|
+
*/
|
|
630
|
+
externalMeta?: Record<string, unknown>;
|
|
604
631
|
}
|
|
605
632
|
/**
|
|
606
633
|
* Who performed a transition / stage-run (server-engine side).
|
|
@@ -718,7 +745,125 @@ interface ApplyTransitionInput {
|
|
|
718
745
|
contextPatch?: Record<string, unknown>;
|
|
719
746
|
/** Execution UUID to stamp on the card (handler advances). */
|
|
720
747
|
executionUuid?: string;
|
|
748
|
+
/**
|
|
749
|
+
* p46 (P46-002, F8): set ONLY by `engine.hostMove` (a move the external host
|
|
750
|
+
* made — e.g. a Jira drag mirrored by the sync). A host-aware adapter, inside
|
|
751
|
+
* the same card lock as the `fromLane` CAS, (1) refuses the move with a
|
|
752
|
+
* `HostFenceError` while the card has any `host.*` op `pending` (D19 — never
|
|
753
|
+
* mistake our own half-done move for a human one), (2) writes `externalMeta`,
|
|
754
|
+
* then (3) moves. Adapters that don't know it ignore it (optional, additive).
|
|
755
|
+
*/
|
|
756
|
+
hostObservation?: {
|
|
757
|
+
externalMeta?: Record<string, unknown>;
|
|
758
|
+
/**
|
|
759
|
+
* ISO time the host state was READ. The fence also refuses the move when a
|
|
760
|
+
* `host.*` op for the card settled (done/failed) after this instant — the
|
|
761
|
+
* read may predate our own write landing in the host.
|
|
762
|
+
*/
|
|
763
|
+
observedAt?: string;
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Handoff v1 — the one AI↔human contract every board lane speaks (p46 §0.3).
|
|
769
|
+
*
|
|
770
|
+
* A Handoff is the record written whenever work crosses between an agent and a
|
|
771
|
+
* person on a card: an agent's verdict ("decide A/B/C", "approve", "test this"),
|
|
772
|
+
* an engine-written FAILED / BLOCKED, or the human's answer carried forward to
|
|
773
|
+
* the next agent (`human_to_ai`). Every worker type produces the same object and
|
|
774
|
+
* every host reads it.
|
|
775
|
+
*
|
|
776
|
+
* This module is DEPENDENCY-FREE and client-safe (types, the verdict JSON
|
|
777
|
+
* schema, the prompt renderer, the link guard). The ajv-backed verdict PARSER
|
|
778
|
+
* lives in `./handoff-parse.ts` and is exported only from the server engine
|
|
779
|
+
* entry (`@cadraos/sdk/kanban/engine`), so the browser bundle never carries ajv.
|
|
780
|
+
*
|
|
781
|
+
* @module @cadraos/sdk/kanban
|
|
782
|
+
*/
|
|
783
|
+
type HandoffKind = "DECIDE" | "APPROVE" | "TEST" | "REVIEW" | "FAILED" | "BLOCKED" | "FYI";
|
|
784
|
+
interface HandoffOption {
|
|
785
|
+
/** Single capital letter, `^[A-Z]$`. */
|
|
786
|
+
id: string;
|
|
787
|
+
/** ≤80 chars. */
|
|
788
|
+
label: string;
|
|
789
|
+
/** ≤300 chars. */
|
|
790
|
+
consequence: string;
|
|
791
|
+
}
|
|
792
|
+
interface HandoffDecision {
|
|
793
|
+
/** ≤500 chars. */
|
|
794
|
+
question: string;
|
|
795
|
+
/** 2–4 options. */
|
|
796
|
+
options: HandoffOption[];
|
|
797
|
+
/** ∈ options[].id */
|
|
798
|
+
recommended: string;
|
|
799
|
+
/** ∈ options[].id */
|
|
800
|
+
defaultIfSilent?: string;
|
|
801
|
+
}
|
|
802
|
+
interface HandoffState {
|
|
803
|
+
repos: string[];
|
|
804
|
+
tests: string | null;
|
|
805
|
+
story: string | null;
|
|
806
|
+
costUsd: number | null;
|
|
807
|
+
}
|
|
808
|
+
interface HandoffAnswer {
|
|
809
|
+
option: string | null;
|
|
810
|
+
/** ≤4000 chars. */
|
|
811
|
+
text: string | null;
|
|
812
|
+
by: string;
|
|
813
|
+
at: string;
|
|
814
|
+
via: "board" | "channel";
|
|
815
|
+
source: string;
|
|
816
|
+
}
|
|
817
|
+
interface HandoffV1 {
|
|
818
|
+
v: 1;
|
|
819
|
+
/** = agentic_handoff.uuid */
|
|
820
|
+
id: string;
|
|
821
|
+
/** Card uuid. */
|
|
822
|
+
cardId: string;
|
|
823
|
+
direction: "ai_to_human" | "human_to_ai";
|
|
824
|
+
/** Lane KEYS. */
|
|
825
|
+
fromLane: string;
|
|
826
|
+
toLane: string;
|
|
827
|
+
/** ISO-8601. */
|
|
828
|
+
at: string;
|
|
829
|
+
by: {
|
|
830
|
+
type: "session" | "user" | "watchdog" | "timeout" | "health";
|
|
831
|
+
id: string;
|
|
832
|
+
};
|
|
833
|
+
kind: HandoffKind;
|
|
834
|
+
/** ≤200 chars. */
|
|
835
|
+
headline: string;
|
|
836
|
+
/** ≤20 × ≤300 chars. */
|
|
837
|
+
done: string[];
|
|
838
|
+
state: HandoffState;
|
|
839
|
+
decision?: HandoffDecision;
|
|
840
|
+
/** ≤300 chars. */
|
|
841
|
+
howToAnswer: string;
|
|
842
|
+
/** ≤10; url must be http(s): or a same-origin path starting "/". */
|
|
843
|
+
links: {
|
|
844
|
+
label: string;
|
|
845
|
+
url: string;
|
|
846
|
+
}[];
|
|
847
|
+
session: {
|
|
848
|
+
id: string | null;
|
|
849
|
+
runnerId: string | null;
|
|
850
|
+
resumableUntil: string | null;
|
|
851
|
+
};
|
|
852
|
+
/** BLOCKED only. */
|
|
853
|
+
blocked?: {
|
|
854
|
+
check: string;
|
|
855
|
+
reason: string;
|
|
856
|
+
fixUrl: string;
|
|
857
|
+
};
|
|
858
|
+
answer?: HandoffAnswer;
|
|
721
859
|
}
|
|
860
|
+
/** Ledger lifecycle of a stored handoff. */
|
|
861
|
+
type HandoffStatus = "open" | "answered" | "resolved" | "consumed";
|
|
862
|
+
/** A stored handoff: the v1 payload plus its ledger status. */
|
|
863
|
+
type HandoffRecord = HandoffV1 & {
|
|
864
|
+
status: HandoffStatus;
|
|
865
|
+
consumedAt: string | null;
|
|
866
|
+
};
|
|
722
867
|
|
|
723
868
|
/**
|
|
724
869
|
* KanbanStorageAdapter — the SERVER-ENGINE persistence contract.
|
|
@@ -801,6 +946,16 @@ interface KanbanStorageAdapter {
|
|
|
801
946
|
* config and passes it as `initialLane`.
|
|
802
947
|
*/
|
|
803
948
|
createCard(input: CreateCardServerInput): Promise<KanbanCard>;
|
|
949
|
+
/**
|
|
950
|
+
* p46 (P46-002) OPTIONAL: the card with this `externalRef` on this board, or
|
|
951
|
+
* null. When present, `engine.createCard({externalRef})` looks it up first and
|
|
952
|
+
* returns an existing card WITHOUT dispatching (exactly-once create per issue
|
|
953
|
+
* key). A host that stores `externalRef` SHOULD also make `createCard` itself
|
|
954
|
+
* idempotent on `(boardConfigUuid, externalRef)` (unique index + `ON CONFLICT
|
|
955
|
+
* DO NOTHING` → return the existing row) for the lookup/insert race. Hosts
|
|
956
|
+
* without external refs (crm/yobo) omit it.
|
|
957
|
+
*/
|
|
958
|
+
getCardByExternalRef?(boardConfigUuid: string, externalRef: string): Promise<KanbanCard | null>;
|
|
804
959
|
/**
|
|
805
960
|
* Patch a card's mutable fields.
|
|
806
961
|
*/
|
|
@@ -917,7 +1072,7 @@ interface KanbanStorageAdapter {
|
|
|
917
1072
|
toLane?: string;
|
|
918
1073
|
contextPatch?: Record<string, unknown>;
|
|
919
1074
|
actor: Actor;
|
|
920
|
-
}): Promise<{
|
|
1075
|
+
} & StageRunLedgerWrite): Promise<{
|
|
921
1076
|
applied: boolean;
|
|
922
1077
|
}>;
|
|
923
1078
|
/**
|
|
@@ -930,7 +1085,7 @@ interface KanbanStorageAdapter {
|
|
|
930
1085
|
*/
|
|
931
1086
|
failStageRun(idemKey: string, f: {
|
|
932
1087
|
error: string;
|
|
933
|
-
}): Promise<void>;
|
|
1088
|
+
} & StageRunLedgerWrite): Promise<void>;
|
|
934
1089
|
/**
|
|
935
1090
|
* OPTIONAL: record that a stage run was dispatched — set status `running`
|
|
936
1091
|
* and stamp the `executionUuid` (so the UI can stream the live run before
|
|
@@ -944,6 +1099,65 @@ interface KanbanStorageAdapter {
|
|
|
944
1099
|
* Read a stage run by its idempotency key, or null if none.
|
|
945
1100
|
*/
|
|
946
1101
|
getStageRun(idemKey: string): Promise<StageRun | null>;
|
|
1102
|
+
/** OPTIONAL (ledger): insert a handoff (adapter assigns `id`), or read the existing one on conflict. */
|
|
1103
|
+
appendHandoff?(h: {
|
|
1104
|
+
card: KanbanCard;
|
|
1105
|
+
stageId: string;
|
|
1106
|
+
configVersion: number;
|
|
1107
|
+
idemKey?: string;
|
|
1108
|
+
attempt?: number;
|
|
1109
|
+
dedupeKey?: string;
|
|
1110
|
+
payload: Omit<HandoffV1, "id">;
|
|
1111
|
+
}): Promise<{
|
|
1112
|
+
created: boolean;
|
|
1113
|
+
handoff: HandoffV1;
|
|
1114
|
+
}>;
|
|
1115
|
+
/** OPTIONAL (ledger): a card's handoffs, NEWEST FIRST. */
|
|
1116
|
+
listHandoffs?(cardId: string, opts?: {
|
|
1117
|
+
limit?: number;
|
|
1118
|
+
}): Promise<HandoffRecord[]>;
|
|
1119
|
+
/** OPTIONAL (ledger): CAS `open → answered` (ai_to_human, kind ∈ DECIDE|APPROVE|TEST|REVIEW). */
|
|
1120
|
+
answerHandoff?(cardId: string, handoffId: string, answer: NonNullable<HandoffV1["answer"]>, userId?: number): Promise<{
|
|
1121
|
+
applied: boolean;
|
|
1122
|
+
}>;
|
|
1123
|
+
/** OPTIONAL (ledger): CAS `open → resolved` (BLOCKED / FAILED). */
|
|
1124
|
+
resolveHandoff?(handoffId: string): Promise<{
|
|
1125
|
+
applied: boolean;
|
|
1126
|
+
}>;
|
|
1127
|
+
/** OPTIONAL (ledger): `human_to_ai` rows `open → consumed` by the dispatch `idemKey`. */
|
|
1128
|
+
consumeHandoffs?(ids: string[], idemKey: string): Promise<void>;
|
|
1129
|
+
/** OPTIONAL (outbox): insert-or-read an op; `run: false` iff it is already `done`. */
|
|
1130
|
+
claimOp?(op: {
|
|
1131
|
+
opId: string;
|
|
1132
|
+
kind: string;
|
|
1133
|
+
cardId?: string;
|
|
1134
|
+
handoffId?: string;
|
|
1135
|
+
payload: unknown;
|
|
1136
|
+
}): Promise<{
|
|
1137
|
+
run: boolean;
|
|
1138
|
+
}>;
|
|
1139
|
+
/** OPTIONAL (outbox): mark an op `done`. */
|
|
1140
|
+
completeOp?(opId: string): Promise<void>;
|
|
1141
|
+
/** OPTIONAL (outbox): mark an op `failed` with its error (replayable). */
|
|
1142
|
+
failOp?(opId: string, error: string): Promise<void>;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* The optional ledger write carried on `completeStageRun` / `failStageRun`
|
|
1146
|
+
* (p46 F3). Ignored by adapters without the ledger group.
|
|
1147
|
+
*
|
|
1148
|
+
* When present, the adapter writes — INSIDE THE SAME TRANSACTION as the run /
|
|
1149
|
+
* card update — the `agentic_handoff` row (using `payload.id` as its uuid;
|
|
1150
|
+
* `ON CONFLICT (dedupe_key) DO NOTHING`) and, only when that row was newly
|
|
1151
|
+
* inserted, a `pending` op row with id `notifyOpId`. If the run update is a
|
|
1152
|
+
* no-op (`completeStageRun` on an already-completed run; `failStageRun` on a
|
|
1153
|
+
* completed run) nothing is written. A throw anywhere rolls ALL of it back.
|
|
1154
|
+
*/
|
|
1155
|
+
interface StageRunLedgerWrite {
|
|
1156
|
+
handoff?: {
|
|
1157
|
+
payload: HandoffV1;
|
|
1158
|
+
dedupeKey: string;
|
|
1159
|
+
};
|
|
1160
|
+
notifyOpId?: string;
|
|
947
1161
|
}
|
|
948
1162
|
|
|
949
1163
|
/**
|
|
@@ -1418,6 +1632,8 @@ interface WireStage {
|
|
|
1418
1632
|
*/
|
|
1419
1633
|
dispatch?: KanbanLaneDispatch;
|
|
1420
1634
|
chunk_size?: number | null;
|
|
1635
|
+
/** p46 Handoff mode (D1). Absent ⇒ `off`. Mapped to `KanbanLane.handoff`. */
|
|
1636
|
+
handoff?: "off" | "verdict" | null;
|
|
1421
1637
|
}
|
|
1422
1638
|
/**
|
|
1423
1639
|
* A resolved board config: stable uuid + version + the mapped `KanbanBoard`
|
|
@@ -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
|
*
|
|
@@ -539,6 +549,11 @@ interface KanbanCard {
|
|
|
539
549
|
configVersion?: number;
|
|
540
550
|
/** Arbitrary shared context carried across transitions. */
|
|
541
551
|
sharedContext?: Record<string, unknown>;
|
|
552
|
+
/**
|
|
553
|
+
* p46 (P46-002): the card's id in an external host (a Jira issue key).
|
|
554
|
+
* Absent on native / crm / yobo cards.
|
|
555
|
+
*/
|
|
556
|
+
externalRef?: string;
|
|
542
557
|
/**
|
|
543
558
|
* Execution UUID of the card's current/last run, if any.
|
|
544
559
|
*
|
|
@@ -601,6 +616,18 @@ interface CreateCardInput {
|
|
|
601
616
|
* in the lifecycle's initial lane.
|
|
602
617
|
*/
|
|
603
618
|
lane?: string;
|
|
619
|
+
/**
|
|
620
|
+
* p46 (P46-002): the card's id in an EXTERNAL host (a Jira issue key, e.g.
|
|
621
|
+
* `CAD-231`). Optional. When set, a card is created at most ONCE per
|
|
622
|
+
* `(boardConfigUuid, externalRef)`: a repeat create returns the existing card
|
|
623
|
+
* and dispatches nothing. Hosts that don't store it simply ignore it.
|
|
624
|
+
*/
|
|
625
|
+
externalRef?: string;
|
|
626
|
+
/**
|
|
627
|
+
* p46 (P46-002): opaque host facts stored with the card (Jira `statusId`,
|
|
628
|
+
* `mover`, `detached`, …). Passed through to `adapter.createCard` untouched.
|
|
629
|
+
*/
|
|
630
|
+
externalMeta?: Record<string, unknown>;
|
|
604
631
|
}
|
|
605
632
|
/**
|
|
606
633
|
* Who performed a transition / stage-run (server-engine side).
|
|
@@ -718,7 +745,125 @@ interface ApplyTransitionInput {
|
|
|
718
745
|
contextPatch?: Record<string, unknown>;
|
|
719
746
|
/** Execution UUID to stamp on the card (handler advances). */
|
|
720
747
|
executionUuid?: string;
|
|
748
|
+
/**
|
|
749
|
+
* p46 (P46-002, F8): set ONLY by `engine.hostMove` (a move the external host
|
|
750
|
+
* made — e.g. a Jira drag mirrored by the sync). A host-aware adapter, inside
|
|
751
|
+
* the same card lock as the `fromLane` CAS, (1) refuses the move with a
|
|
752
|
+
* `HostFenceError` while the card has any `host.*` op `pending` (D19 — never
|
|
753
|
+
* mistake our own half-done move for a human one), (2) writes `externalMeta`,
|
|
754
|
+
* then (3) moves. Adapters that don't know it ignore it (optional, additive).
|
|
755
|
+
*/
|
|
756
|
+
hostObservation?: {
|
|
757
|
+
externalMeta?: Record<string, unknown>;
|
|
758
|
+
/**
|
|
759
|
+
* ISO time the host state was READ. The fence also refuses the move when a
|
|
760
|
+
* `host.*` op for the card settled (done/failed) after this instant — the
|
|
761
|
+
* read may predate our own write landing in the host.
|
|
762
|
+
*/
|
|
763
|
+
observedAt?: string;
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Handoff v1 — the one AI↔human contract every board lane speaks (p46 §0.3).
|
|
769
|
+
*
|
|
770
|
+
* A Handoff is the record written whenever work crosses between an agent and a
|
|
771
|
+
* person on a card: an agent's verdict ("decide A/B/C", "approve", "test this"),
|
|
772
|
+
* an engine-written FAILED / BLOCKED, or the human's answer carried forward to
|
|
773
|
+
* the next agent (`human_to_ai`). Every worker type produces the same object and
|
|
774
|
+
* every host reads it.
|
|
775
|
+
*
|
|
776
|
+
* This module is DEPENDENCY-FREE and client-safe (types, the verdict JSON
|
|
777
|
+
* schema, the prompt renderer, the link guard). The ajv-backed verdict PARSER
|
|
778
|
+
* lives in `./handoff-parse.ts` and is exported only from the server engine
|
|
779
|
+
* entry (`@cadraos/sdk/kanban/engine`), so the browser bundle never carries ajv.
|
|
780
|
+
*
|
|
781
|
+
* @module @cadraos/sdk/kanban
|
|
782
|
+
*/
|
|
783
|
+
type HandoffKind = "DECIDE" | "APPROVE" | "TEST" | "REVIEW" | "FAILED" | "BLOCKED" | "FYI";
|
|
784
|
+
interface HandoffOption {
|
|
785
|
+
/** Single capital letter, `^[A-Z]$`. */
|
|
786
|
+
id: string;
|
|
787
|
+
/** ≤80 chars. */
|
|
788
|
+
label: string;
|
|
789
|
+
/** ≤300 chars. */
|
|
790
|
+
consequence: string;
|
|
791
|
+
}
|
|
792
|
+
interface HandoffDecision {
|
|
793
|
+
/** ≤500 chars. */
|
|
794
|
+
question: string;
|
|
795
|
+
/** 2–4 options. */
|
|
796
|
+
options: HandoffOption[];
|
|
797
|
+
/** ∈ options[].id */
|
|
798
|
+
recommended: string;
|
|
799
|
+
/** ∈ options[].id */
|
|
800
|
+
defaultIfSilent?: string;
|
|
801
|
+
}
|
|
802
|
+
interface HandoffState {
|
|
803
|
+
repos: string[];
|
|
804
|
+
tests: string | null;
|
|
805
|
+
story: string | null;
|
|
806
|
+
costUsd: number | null;
|
|
807
|
+
}
|
|
808
|
+
interface HandoffAnswer {
|
|
809
|
+
option: string | null;
|
|
810
|
+
/** ≤4000 chars. */
|
|
811
|
+
text: string | null;
|
|
812
|
+
by: string;
|
|
813
|
+
at: string;
|
|
814
|
+
via: "board" | "channel";
|
|
815
|
+
source: string;
|
|
816
|
+
}
|
|
817
|
+
interface HandoffV1 {
|
|
818
|
+
v: 1;
|
|
819
|
+
/** = agentic_handoff.uuid */
|
|
820
|
+
id: string;
|
|
821
|
+
/** Card uuid. */
|
|
822
|
+
cardId: string;
|
|
823
|
+
direction: "ai_to_human" | "human_to_ai";
|
|
824
|
+
/** Lane KEYS. */
|
|
825
|
+
fromLane: string;
|
|
826
|
+
toLane: string;
|
|
827
|
+
/** ISO-8601. */
|
|
828
|
+
at: string;
|
|
829
|
+
by: {
|
|
830
|
+
type: "session" | "user" | "watchdog" | "timeout" | "health";
|
|
831
|
+
id: string;
|
|
832
|
+
};
|
|
833
|
+
kind: HandoffKind;
|
|
834
|
+
/** ≤200 chars. */
|
|
835
|
+
headline: string;
|
|
836
|
+
/** ≤20 × ≤300 chars. */
|
|
837
|
+
done: string[];
|
|
838
|
+
state: HandoffState;
|
|
839
|
+
decision?: HandoffDecision;
|
|
840
|
+
/** ≤300 chars. */
|
|
841
|
+
howToAnswer: string;
|
|
842
|
+
/** ≤10; url must be http(s): or a same-origin path starting "/". */
|
|
843
|
+
links: {
|
|
844
|
+
label: string;
|
|
845
|
+
url: string;
|
|
846
|
+
}[];
|
|
847
|
+
session: {
|
|
848
|
+
id: string | null;
|
|
849
|
+
runnerId: string | null;
|
|
850
|
+
resumableUntil: string | null;
|
|
851
|
+
};
|
|
852
|
+
/** BLOCKED only. */
|
|
853
|
+
blocked?: {
|
|
854
|
+
check: string;
|
|
855
|
+
reason: string;
|
|
856
|
+
fixUrl: string;
|
|
857
|
+
};
|
|
858
|
+
answer?: HandoffAnswer;
|
|
721
859
|
}
|
|
860
|
+
/** Ledger lifecycle of a stored handoff. */
|
|
861
|
+
type HandoffStatus = "open" | "answered" | "resolved" | "consumed";
|
|
862
|
+
/** A stored handoff: the v1 payload plus its ledger status. */
|
|
863
|
+
type HandoffRecord = HandoffV1 & {
|
|
864
|
+
status: HandoffStatus;
|
|
865
|
+
consumedAt: string | null;
|
|
866
|
+
};
|
|
722
867
|
|
|
723
868
|
/**
|
|
724
869
|
* KanbanStorageAdapter — the SERVER-ENGINE persistence contract.
|
|
@@ -801,6 +946,16 @@ interface KanbanStorageAdapter {
|
|
|
801
946
|
* config and passes it as `initialLane`.
|
|
802
947
|
*/
|
|
803
948
|
createCard(input: CreateCardServerInput): Promise<KanbanCard>;
|
|
949
|
+
/**
|
|
950
|
+
* p46 (P46-002) OPTIONAL: the card with this `externalRef` on this board, or
|
|
951
|
+
* null. When present, `engine.createCard({externalRef})` looks it up first and
|
|
952
|
+
* returns an existing card WITHOUT dispatching (exactly-once create per issue
|
|
953
|
+
* key). A host that stores `externalRef` SHOULD also make `createCard` itself
|
|
954
|
+
* idempotent on `(boardConfigUuid, externalRef)` (unique index + `ON CONFLICT
|
|
955
|
+
* DO NOTHING` → return the existing row) for the lookup/insert race. Hosts
|
|
956
|
+
* without external refs (crm/yobo) omit it.
|
|
957
|
+
*/
|
|
958
|
+
getCardByExternalRef?(boardConfigUuid: string, externalRef: string): Promise<KanbanCard | null>;
|
|
804
959
|
/**
|
|
805
960
|
* Patch a card's mutable fields.
|
|
806
961
|
*/
|
|
@@ -917,7 +1072,7 @@ interface KanbanStorageAdapter {
|
|
|
917
1072
|
toLane?: string;
|
|
918
1073
|
contextPatch?: Record<string, unknown>;
|
|
919
1074
|
actor: Actor;
|
|
920
|
-
}): Promise<{
|
|
1075
|
+
} & StageRunLedgerWrite): Promise<{
|
|
921
1076
|
applied: boolean;
|
|
922
1077
|
}>;
|
|
923
1078
|
/**
|
|
@@ -930,7 +1085,7 @@ interface KanbanStorageAdapter {
|
|
|
930
1085
|
*/
|
|
931
1086
|
failStageRun(idemKey: string, f: {
|
|
932
1087
|
error: string;
|
|
933
|
-
}): Promise<void>;
|
|
1088
|
+
} & StageRunLedgerWrite): Promise<void>;
|
|
934
1089
|
/**
|
|
935
1090
|
* OPTIONAL: record that a stage run was dispatched — set status `running`
|
|
936
1091
|
* and stamp the `executionUuid` (so the UI can stream the live run before
|
|
@@ -944,6 +1099,65 @@ interface KanbanStorageAdapter {
|
|
|
944
1099
|
* Read a stage run by its idempotency key, or null if none.
|
|
945
1100
|
*/
|
|
946
1101
|
getStageRun(idemKey: string): Promise<StageRun | null>;
|
|
1102
|
+
/** OPTIONAL (ledger): insert a handoff (adapter assigns `id`), or read the existing one on conflict. */
|
|
1103
|
+
appendHandoff?(h: {
|
|
1104
|
+
card: KanbanCard;
|
|
1105
|
+
stageId: string;
|
|
1106
|
+
configVersion: number;
|
|
1107
|
+
idemKey?: string;
|
|
1108
|
+
attempt?: number;
|
|
1109
|
+
dedupeKey?: string;
|
|
1110
|
+
payload: Omit<HandoffV1, "id">;
|
|
1111
|
+
}): Promise<{
|
|
1112
|
+
created: boolean;
|
|
1113
|
+
handoff: HandoffV1;
|
|
1114
|
+
}>;
|
|
1115
|
+
/** OPTIONAL (ledger): a card's handoffs, NEWEST FIRST. */
|
|
1116
|
+
listHandoffs?(cardId: string, opts?: {
|
|
1117
|
+
limit?: number;
|
|
1118
|
+
}): Promise<HandoffRecord[]>;
|
|
1119
|
+
/** OPTIONAL (ledger): CAS `open → answered` (ai_to_human, kind ∈ DECIDE|APPROVE|TEST|REVIEW). */
|
|
1120
|
+
answerHandoff?(cardId: string, handoffId: string, answer: NonNullable<HandoffV1["answer"]>, userId?: number): Promise<{
|
|
1121
|
+
applied: boolean;
|
|
1122
|
+
}>;
|
|
1123
|
+
/** OPTIONAL (ledger): CAS `open → resolved` (BLOCKED / FAILED). */
|
|
1124
|
+
resolveHandoff?(handoffId: string): Promise<{
|
|
1125
|
+
applied: boolean;
|
|
1126
|
+
}>;
|
|
1127
|
+
/** OPTIONAL (ledger): `human_to_ai` rows `open → consumed` by the dispatch `idemKey`. */
|
|
1128
|
+
consumeHandoffs?(ids: string[], idemKey: string): Promise<void>;
|
|
1129
|
+
/** OPTIONAL (outbox): insert-or-read an op; `run: false` iff it is already `done`. */
|
|
1130
|
+
claimOp?(op: {
|
|
1131
|
+
opId: string;
|
|
1132
|
+
kind: string;
|
|
1133
|
+
cardId?: string;
|
|
1134
|
+
handoffId?: string;
|
|
1135
|
+
payload: unknown;
|
|
1136
|
+
}): Promise<{
|
|
1137
|
+
run: boolean;
|
|
1138
|
+
}>;
|
|
1139
|
+
/** OPTIONAL (outbox): mark an op `done`. */
|
|
1140
|
+
completeOp?(opId: string): Promise<void>;
|
|
1141
|
+
/** OPTIONAL (outbox): mark an op `failed` with its error (replayable). */
|
|
1142
|
+
failOp?(opId: string, error: string): Promise<void>;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* The optional ledger write carried on `completeStageRun` / `failStageRun`
|
|
1146
|
+
* (p46 F3). Ignored by adapters without the ledger group.
|
|
1147
|
+
*
|
|
1148
|
+
* When present, the adapter writes — INSIDE THE SAME TRANSACTION as the run /
|
|
1149
|
+
* card update — the `agentic_handoff` row (using `payload.id` as its uuid;
|
|
1150
|
+
* `ON CONFLICT (dedupe_key) DO NOTHING`) and, only when that row was newly
|
|
1151
|
+
* inserted, a `pending` op row with id `notifyOpId`. If the run update is a
|
|
1152
|
+
* no-op (`completeStageRun` on an already-completed run; `failStageRun` on a
|
|
1153
|
+
* completed run) nothing is written. A throw anywhere rolls ALL of it back.
|
|
1154
|
+
*/
|
|
1155
|
+
interface StageRunLedgerWrite {
|
|
1156
|
+
handoff?: {
|
|
1157
|
+
payload: HandoffV1;
|
|
1158
|
+
dedupeKey: string;
|
|
1159
|
+
};
|
|
1160
|
+
notifyOpId?: string;
|
|
947
1161
|
}
|
|
948
1162
|
|
|
949
1163
|
/**
|
|
@@ -1418,6 +1632,8 @@ interface WireStage {
|
|
|
1418
1632
|
*/
|
|
1419
1633
|
dispatch?: KanbanLaneDispatch;
|
|
1420
1634
|
chunk_size?: number | null;
|
|
1635
|
+
/** p46 Handoff mode (D1). Absent ⇒ `off`. Mapped to `KanbanLane.handoff`. */
|
|
1636
|
+
handoff?: "off" | "verdict" | null;
|
|
1421
1637
|
}
|
|
1422
1638
|
/**
|
|
1423
1639
|
* A resolved board config: stable uuid + version + the mapped `KanbanBoard`
|