@adhdev/mesh-shared 1.0.82-rc.33 → 1.0.82-rc.35

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.
@@ -18,10 +18,10 @@
18
18
  * Order mirrors mcp-server `ALL_MESH_TOOLS` for easy visual diffing, but the consistency
19
19
  * checks are set-based (order-insensitive).
20
20
  */
21
- export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_queue_revise", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_create", "mesh_add_node", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots", "driver_list", "driver_describe", "artifact_list"];
21
+ export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_queue_revise", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_create", "mesh_add_node", "mesh_reattach_source_parent", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots", "driver_list", "driver_describe", "artifact_list", "artifact_read", "artifact_register"];
22
22
  export type CanonicalMeshToolName = typeof CANONICAL_MESH_TOOL_NAMES[number];
23
23
  /** The count the `NN tools` barrel doc comments and consistency test assert against. */
24
- export declare const CANONICAL_MESH_TOOL_COUNT: 45;
24
+ export declare const CANONICAL_MESH_TOOL_COUNT: 48;
25
25
  /**
26
26
  * Tool names retired by the 2026-09-26 tool-surface consolidation (60 → 48).
27
27
  * Every capability behind them is still reachable: each retired name maps to the
@@ -0,0 +1,202 @@
1
+ /**
2
+ * mission-items — structured mission records: the item kinds a mission carries
3
+ * (remaining work, live checks, owner decisions, evidence), their normalization
4
+ * and limits, dedupe-key derivation and the close outcome enum
5
+ * (docs/design/2026-10-28-structured-missions-and-owner-decisions.md §4, §5).
6
+ *
7
+ * Philosophy (owner 2026-10-28, design §0): NEVER reject for size or shape —
8
+ * over-long text is truncated, extra refs are dropped, unusable items are
9
+ * reported per-item. The only hard rule lives elsewhere (the daemon): a
10
+ * coordinator or worker can never ANSWER a decision, so `answer`/`delivery`
11
+ * are stripped from every caller-supplied `decision` here.
12
+ *
13
+ * Dependency-free leaf: pure functions over plain data, no storage.
14
+ */
15
+ export declare const MISSION_ITEM_KINDS: readonly ["remaining", "live_check", "decision", "evidence"];
16
+ export type MissionItemKind = typeof MISSION_ITEM_KINDS[number];
17
+ export declare const MISSION_ITEM_STATES: readonly ["open", "done", "dropped", "moved", "answered", "expired", "withdrawn", "superseded"];
18
+ export type MissionItemState = typeof MISSION_ITEM_STATES[number];
19
+ /** States a coordinator/worker may SET through items_update, per kind. `answered`/`expired` are daemon-owned. */
20
+ export declare const COORDINATOR_SETTABLE_ITEM_STATES: Readonly<Record<MissionItemKind, readonly MissionItemState[]>>;
21
+ export declare const MISSION_OUTCOMES: readonly ["merged", "verified_live", "superseded", "not_needed", "owner_abandoned", "unspecified"];
22
+ export type MissionOutcome = typeof MISSION_OUTCOMES[number];
23
+ export declare function isMissionOutcome(value: unknown): value is MissionOutcome;
24
+ export declare const MISSION_REF_KINDS: readonly ["commit", "branch", "pr", "task", "artifact", "mission", "url"];
25
+ export type MissionRefKind = typeof MISSION_REF_KINDS[number];
26
+ export declare const MISSION_LIMITS: {
27
+ readonly summary: 300;
28
+ readonly outcomeReason: 500;
29
+ readonly itemText: 300;
30
+ readonly machine: 80;
31
+ readonly refs: 8;
32
+ readonly refValue: 300;
33
+ readonly refRepo: 120;
34
+ readonly refNote: 200;
35
+ readonly dedupeKey: 400;
36
+ readonly question: 300;
37
+ readonly context: 600;
38
+ readonly options: 5;
39
+ readonly optionLabel: 60;
40
+ readonly optionDetail: 200;
41
+ /** Open items per mission before the oldest `remaining` fold into a "N more" summary item. */
42
+ readonly openItemsPerMission: 200;
43
+ /** One event payload (serialized). */
44
+ readonly eventPayloadBytes: number;
45
+ /** items_add / items_update entries honoured per call (extra entries are reported, not applied). */
46
+ readonly itemsPerCall: 50;
47
+ };
48
+ export declare const MISSION_DECISION_OPTION_IDS: readonly ["a", "b", "c", "d", "e"];
49
+ export declare const MISSION_DECISION_URGENCIES: readonly ["normal", "blocking"];
50
+ export type MissionDecisionUrgency = typeof MISSION_DECISION_URGENCIES[number];
51
+ export declare const MISSION_DECISION_ON_DUE: readonly ["remind", "apply_recommended"];
52
+ export type MissionDecisionOnDue = typeof MISSION_DECISION_ON_DUE[number];
53
+ export interface MissionItemRef {
54
+ kind: MissionRefKind;
55
+ repo?: string;
56
+ value: string;
57
+ note?: string;
58
+ }
59
+ export interface MissionDecisionOption {
60
+ id: string;
61
+ label: string;
62
+ detail?: string;
63
+ }
64
+ /** Who/where an answer came from. Written only by the daemon's (owner-side) answer path — never from tool input. */
65
+ export interface MissionDecisionAnswer {
66
+ optionId?: string;
67
+ text?: string;
68
+ by: 'owner' | 'auto_recommended';
69
+ via: 'dashboard' | 'assistant' | 'push' | 'cli' | 'auto';
70
+ interpretedFrom?: string;
71
+ at: string;
72
+ ingress?: string;
73
+ }
74
+ export interface MissionDecisionDelivery {
75
+ messageId: string;
76
+ /**
77
+ * `queued`: the coordinator's input queue accepted it but has not typed it yet — the daemon
78
+ * confirms it against the send receipt (a daemon restart drops that queue). Daemon-written.
79
+ */
80
+ state: 'pending' | 'queued' | 'sent' | 'acked';
81
+ lastAttemptAt?: string;
82
+ /** Coordinator session + funnel messageId of a `queued` delivery (receipt lookup). Daemon-written. */
83
+ sessionId?: string;
84
+ wireMessageId?: string;
85
+ /** Failed send attempts of this revision (retry backoff). Daemon-written. */
86
+ attempts?: number;
87
+ /** What the coordinator is told: a first answer, a changed answer, or an undone one. Daemon-written. */
88
+ kind?: 'answered' | 'changed' | 'undone';
89
+ }
90
+ export interface MissionDecision {
91
+ question: string;
92
+ context?: string;
93
+ options: MissionDecisionOption[];
94
+ recommended?: string;
95
+ allowFreeText: boolean;
96
+ dueAt?: string;
97
+ onDue: MissionDecisionOnDue;
98
+ urgency: MissionDecisionUrgency;
99
+ answer?: MissionDecisionAnswer;
100
+ delivery?: MissionDecisionDelivery;
101
+ /**
102
+ * Coordinator-prompt watermark (bundle B): ISO time the coordinator last "saw" this item's
103
+ * answer. An answered decision is shown in the coordinator prompt while this is absent or
104
+ * older than `answer.at` (a changed answer re-surfaces). Written only by the daemon.
105
+ */
106
+ promptAckedAt?: string;
107
+ /** Counts answer writes (answer / change / undo); delivery messageIds are `decision:<itemId>:r<n>`. Daemon-written. */
108
+ answerRevision?: number;
109
+ /** ISO time an `onDue: remind` crossing was recorded (once per dueAt). Daemon-written. */
110
+ remindedAt?: string;
111
+ }
112
+ export interface NormalizedMissionItem {
113
+ kind: MissionItemKind;
114
+ text: string;
115
+ machine?: string;
116
+ refs: MissionItemRef[];
117
+ decision?: MissionDecision;
118
+ dedupeKey: string;
119
+ /** Fields that were truncated/dropped — informational, never an error. */
120
+ truncated: string[];
121
+ }
122
+ export declare function truncateMissionText(value: string, max: number): string;
123
+ export declare function normalizeMissionSummary(value: unknown): {
124
+ value: string;
125
+ truncated: boolean;
126
+ } | null;
127
+ export declare function normalizeOutcomeReason(value: unknown): {
128
+ value: string;
129
+ truncated: boolean;
130
+ } | null;
131
+ /** Refs: ≤8, `{kind, repo?, value, note?}`. Unusable entries are dropped; the 9th and later are dropped. */
132
+ export declare function normalizeMissionRefs(value: unknown): {
133
+ refs: MissionItemRef[];
134
+ truncated: boolean;
135
+ };
136
+ /**
137
+ * Caller-supplied decision → stored shape. `answer`/`delivery` (and any other
138
+ * unknown key) are DROPPED: a coordinator/worker cannot pre-answer its own
139
+ * question. `recommended` must name one of the options, else it is dropped.
140
+ */
141
+ export declare function normalizeDecisionInput(raw: Record<string, unknown>, truncated: string[]): MissionDecision | null;
142
+ /** Normalized text for dedupe: lower-case, whitespace-collapsed, trailing punctuation trimmed. */
143
+ export declare function normalizeForDedupe(text: string): string;
144
+ export declare function deriveItemDedupeKey(kind: MissionItemKind, parts: {
145
+ text?: string;
146
+ machine?: string;
147
+ refs?: readonly MissionItemRef[];
148
+ question?: string;
149
+ explicit?: string;
150
+ }): string;
151
+ /**
152
+ * Normalize one caller-supplied `items_add[]` entry. Returns `null` + `error`
153
+ * only when nothing usable remains (no text/question) — everything else is
154
+ * clamped, never rejected.
155
+ */
156
+ export declare function normalizeMissionItemInput(raw: unknown): {
157
+ item: NormalizedMissionItem;
158
+ } | {
159
+ error: string;
160
+ };
161
+ export interface MissionItemUpdateInput {
162
+ id: string;
163
+ state?: MissionItemState;
164
+ text?: string;
165
+ machine?: string | null;
166
+ refs?: MissionItemRef[];
167
+ }
168
+ /** Normalize one `items_update[]` entry (id required). The kind-specific state rule is applied by the daemon, which knows the stored kind. */
169
+ export declare function normalizeMissionItemUpdate(raw: unknown): {
170
+ update: MissionItemUpdateInput;
171
+ truncated: string[];
172
+ } | {
173
+ error: string;
174
+ };
175
+ /** Cap a serialized event payload at {@link MISSION_LIMITS.eventPayloadBytes}; over-size payloads become a stub that says so. */
176
+ export declare function boundEventPayload(payload: Record<string, unknown>): string;
177
+ /** The carry-over mission's fixed title (design §5.1). */
178
+ export declare const CARRY_OVER_MISSION_TITLE = "Carry-over \u2014 open items from closed missions";
179
+ /** The title earlier builds stored; existing DBs still hold it, so it keeps matching (no rename). */
180
+ export declare const LEGACY_CARRY_OVER_MISSION_TITLE = "\uC774\uC6D4 \u2014 \uB2EB\uD78C \uBBF8\uC158\uC5D0\uC11C \uB118\uC5B4\uC628 \uD56D\uBAA9";
181
+ /** Canonical goal text of a newly created carry-over mission. */
182
+ export declare const CARRY_OVER_MISSION_GOAL = "Open items carried over from closed missions, and decisions requested without a mission. Managed automatically \u2014 do not close.";
183
+ /**
184
+ * THE one predicate for "is this the auto carry-over mission". The daemon's `is_carry_over`
185
+ * column (`isCarryOver`) is the source of truth; wire views that cannot carry the flag (older
186
+ * strict decoders reject unknown keys) fall back to the fixed title. Use this everywhere.
187
+ */
188
+ export declare function isCarryOverMissionRecord(m: {
189
+ isCarryOver?: boolean;
190
+ title?: unknown;
191
+ } | null | undefined): boolean;
192
+ /** The carry-over mission's title is fixed (the daemon ignores renames), so a title match identifies it on wire views that carry no flag. */
193
+ export declare function isCarryOverMissionTitle(title: unknown): boolean;
194
+ /** Prefix of the owner-confirmation decision a coordinator's `owner_abandoned` close creates. */
195
+ export declare const OWNER_ABANDON_CONFIRM_KEY_PREFIX = "owner-abandon-confirm:";
196
+ export declare const OWNER_ABANDON_CONFIRM_QUESTION = "The coordinator closed this as 'owner abandoned' \u2014 correct?";
197
+ export declare const OWNER_ABANDON_CONFIRM_OPTIONS: readonly ["Correct", "Reopen"];
198
+ /** worker `report_completion.decisions_needed` cap. */
199
+ export declare const WORKER_DECISIONS_NEEDED_MAX = 3;
200
+ /** worker `report_completion.commits` cap and shape. */
201
+ export declare const WORKER_COMMITS_MAX = 20;
202
+ export declare function normalizeReportCommits(value: unknown): string[] | null;
@@ -32,15 +32,38 @@ import { type CommandPayload, type Envelope, type MessageId } from './envelope';
32
32
  * time (UserSession.ts), so adding a member here without adding it there — or
33
33
  * vice versa — fails `tsc`.
34
34
  */
35
- export declare const DAEMON_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress"];
35
+ export declare const DAEMON_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress", "agent:decision_needed"];
36
36
  export type DaemonStatusEventName = typeof DAEMON_STATUS_EVENT_NAMES[number];
37
- export declare const isDaemonStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress";
37
+ export declare const isDaemonStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress" | "agent:decision_needed";
38
38
  /**
39
39
  * Server-bound status event. Content boundary: identifiers, enums, counters
40
40
  * and booleans only — plus the approved approval-modal exception
41
41
  * (`modalMessage`/`modalButtons`, CLAUDE.md "Push notification exception").
42
42
  * Mirror of daemon-core `DaemonStatusEventPayload`.
43
43
  */
44
+ /**
45
+ * Owner-decision push payload (design §9, owner decision D1 2026-10-28). The ONLY mission text that
46
+ * reaches the server: the question (<=80 chars) and up to 3 option labels (<=24 chars). Never
47
+ * `context`, free-text answers, mission goal/summary or chat content.
48
+ */
49
+ export type DecisionPushWire = {
50
+ meshId: string;
51
+ missionId: string;
52
+ itemId: string;
53
+ question: string;
54
+ options: Array<{
55
+ id: string;
56
+ label: string;
57
+ }>;
58
+ recommended?: string;
59
+ urgency: 'normal' | 'blocking';
60
+ dueAt?: string;
61
+ /** `new` = freshly asked; `remind` = a blocking decision crossed its due time. */
62
+ reason: 'new' | 'remind';
63
+ };
64
+ export declare const DECISION_PUSH_QUESTION_MAX = 80;
65
+ export declare const DECISION_PUSH_OPTION_LABEL_MAX = 24;
66
+ export declare const DECISION_PUSH_OPTIONS_MAX = 3;
44
67
  export type DaemonStatusEventWirePayload = {
45
68
  event: DaemonStatusEventName;
46
69
  timestamp: number;
@@ -57,6 +80,8 @@ export type DaemonStatusEventWirePayload = {
57
80
  waitKind?: 'external';
58
81
  surfaceHidden?: boolean;
59
82
  muted?: boolean;
83
+ /** Present only on `agent:decision_needed`. */
84
+ decision?: DecisionPushWire;
60
85
  };
61
86
  /** Routing-only session entry the daemon reports over the server WS. Mirror of daemon-core `RoutingSessionEntry`. */
62
87
  export type RoutingSessionWireEntry = {
@@ -218,6 +243,7 @@ export type P2POfferWirePayload = {
218
243
  peerId: string;
219
244
  attemptId?: number;
220
245
  attemptEpoch?: string;
246
+ probe?: boolean;
221
247
  };
222
248
  export type P2PIceWirePayload = {
223
249
  candidate: string;
@@ -225,6 +251,7 @@ export type P2PIceWirePayload = {
225
251
  peerId: string;
226
252
  attemptId?: number;
227
253
  attemptEpoch?: string;
254
+ probe?: boolean;
228
255
  };
229
256
  /**
230
257
  * A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
@@ -238,6 +265,10 @@ export type RelayedP2PSignalWirePayload = {
238
265
  daemonId?: string;
239
266
  attemptId?: number;
240
267
  attemptEpoch?: string;
268
+ /** p2p_ready only: the dashboard's stale-connect backoff index (see DashboardP2PReadyWirePayload). */
269
+ backoffStep?: number;
270
+ /** Make-before-break relay->direct re-probe of an existing peer (see DashboardP2PReadyWirePayload). */
271
+ probe?: boolean;
241
272
  sharePermission?: string;
242
273
  shareTargetSessionId?: string;
243
274
  sdp?: string;
@@ -21,9 +21,9 @@ import { type DaemonStatusEventName, type P2PIceWirePayload, type P2POfferWirePa
21
21
  /** Server-originated events the dashboard receives in addition to the daemon set. */
22
22
  export declare const SERVER_STATUS_EVENT_NAMES: readonly ["daemon:disconnect", "team:session_viewed", "team:view_request", "team:view_request_approved", "team:view_request_rejected"];
23
23
  /** Mirror of daemon-core `DashboardStatusEventName`; pinned equal by the server at compile time. */
24
- export declare const DASHBOARD_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress", "daemon:disconnect", "team:session_viewed", "team:view_request", "team:view_request_approved", "team:view_request_rejected"];
24
+ export declare const DASHBOARD_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress", "agent:decision_needed", "daemon:disconnect", "team:session_viewed", "team:view_request", "team:view_request_approved", "team:view_request_rejected"];
25
25
  export type DashboardStatusEventName = DaemonStatusEventName | typeof SERVER_STATUS_EVENT_NAMES[number];
26
- export declare const isDashboardStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress" | "daemon:disconnect" | "team:session_viewed" | "team:view_request" | "team:view_request_approved" | "team:view_request_rejected";
26
+ export declare const isDashboardStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress" | "agent:decision_needed" | "daemon:disconnect" | "team:session_viewed" | "team:view_request" | "team:view_request_approved" | "team:view_request_rejected";
27
27
  /**
28
28
  * Sanitized status event as the server relays it (UserSession
29
29
  * `buildDashboardStatusEvent`). Mirror of daemon-core
@@ -111,6 +111,7 @@ export type DashboardP2POfferWirePayload = {
111
111
  peerId?: string;
112
112
  attemptId?: number;
113
113
  attemptEpoch?: string;
114
+ probe?: boolean;
114
115
  sdp: string;
115
116
  type: string;
116
117
  };
@@ -119,6 +120,7 @@ export type DashboardP2PIceWirePayload = {
119
120
  peerId?: string;
120
121
  attemptId?: number;
121
122
  attemptEpoch?: string;
123
+ probe?: boolean;
122
124
  candidate: string;
123
125
  mid?: string;
124
126
  sdpMid?: string | null;
@@ -155,11 +157,15 @@ export declare const isDashboardToUserSessionType: (value: unknown) => value is
155
157
  export declare const DASHBOARD_P2P_SIGNAL_TYPES: readonly ["p2p_ready", "p2p_answer", "p2p_ice"];
156
158
  export type DashboardP2PSignalType = typeof DASHBOARD_P2P_SIGNAL_TYPES[number];
157
159
  export declare const isDashboardP2PSignalType: (value: unknown) => value is "p2p_ready" | "p2p_answer" | "p2p_ice";
160
+ /** `backoffStep`: the dashboard's stale-connect backoff index (0 = 15s, 1 = 30s, 2 = 60s), so the daemon holds the connecting slot for the same window. */
161
+ /** `probe`: identifier-only flag — this signal belongs to a make-before-break relay->direct re-probe of the SAME logical peer (never a new peer for the connection cap). */
158
162
  export type DashboardP2PReadyWirePayload = {
159
163
  daemonId: string;
160
164
  peerId: string;
161
165
  attemptId?: number;
162
166
  attemptEpoch?: string;
167
+ backoffStep?: number;
168
+ probe?: boolean;
163
169
  };
164
170
  export type DashboardP2PAnswerWirePayload = {
165
171
  daemonId: string;
@@ -168,12 +174,14 @@ export type DashboardP2PAnswerWirePayload = {
168
174
  type: string;
169
175
  attemptId?: number;
170
176
  attemptEpoch?: string;
177
+ probe?: boolean;
171
178
  };
172
179
  export type DashboardP2PLocalIceWirePayload = {
173
180
  daemonId: string;
174
181
  peerId: string;
175
182
  attemptId?: number;
176
183
  attemptEpoch?: string;
184
+ probe?: boolean;
177
185
  candidate: string;
178
186
  sdpMid: string | null;
179
187
  sdpMLineIndex: number | null;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Relay -> direct re-probe scheduler (pure, dependency-free).
3
+ *
4
+ * A P2P connection that settled on a TURN relay never looks for a direct path
5
+ * again. The owning transport (dashboard `P2PConnection`, daemon peer manager)
6
+ * uses this scheduler to periodically attempt a make-before-break probe: a
7
+ * SECOND peer connection (STUN-only) is built next to the working relay one and
8
+ * only swapped in once it is verified direct. The scheduler owns nothing but
9
+ * timing: backoff 60s -> 5min (doubling), jitter, and the stop conditions. The
10
+ * transport supplies `shouldProbe` (open + idle + visible) and `probe`.
11
+ */
12
+ export type RelayReprobeOutcome = 'direct' | 'relay' | 'failed' | 'skipped';
13
+ export interface RelayReprobeTiming {
14
+ /** First delay and the unit of growth (default 60s). */
15
+ baseDelayMs?: number;
16
+ /** Backoff ceiling before jitter (default 5min). */
17
+ maxDelayMs?: number;
18
+ /** +/- fraction applied to every delay (default 0.2). */
19
+ jitterRatio?: number;
20
+ }
21
+ export declare const RELAY_REPROBE_BASE_DELAY_MS = 60000;
22
+ export declare const RELAY_REPROBE_MAX_DELAY_MS: number;
23
+ export declare const RELAY_REPROBE_JITTER_RATIO = 0.2;
24
+ /** Delay before the next attempt after `failures` consecutive non-direct outcomes. */
25
+ export declare function computeRelayReprobeDelay(failures: number, random: () => number, timing?: RelayReprobeTiming): number;
26
+ export interface RelayReprobeSchedulerOptions extends RelayReprobeTiming {
27
+ random: () => number;
28
+ setTimer: (callback: () => void, ms: number) => unknown;
29
+ clearTimer: (handle: unknown) => void;
30
+ /** Connected over relay, idle, tab visible, auth epoch unchanged... Re-checked on every tick. */
31
+ shouldProbe: () => boolean;
32
+ /** One make-before-break attempt. Must never throw into the scheduler; a rejection counts as 'failed'. */
33
+ probe: () => Promise<RelayReprobeOutcome>;
34
+ }
35
+ export declare class RelayReprobeScheduler {
36
+ private readonly options;
37
+ private handle;
38
+ private running;
39
+ private inFlight;
40
+ private failureCount;
41
+ private generation;
42
+ constructor(options: RelayReprobeSchedulerOptions);
43
+ get isRunning(): boolean;
44
+ get failures(): number;
45
+ get probing(): boolean;
46
+ /** Begin probing (idempotent). Call when a connection is selected as relay. */
47
+ start(): void;
48
+ /** Stop for good (direct path found, connection closed, auth epoch changed). */
49
+ stop(): void;
50
+ private schedule;
51
+ private tick;
52
+ }
@@ -30,8 +30,40 @@ export interface MissionUpsertRequest {
30
30
  * this wire guard only checks SHAPE, not that decision.
31
31
  */
32
32
  brief?: MissionBriefWire | null;
33
+ /**
34
+ * Structured-missions bundle A (design 2026-10-28 §5.1). Every key below is optional
35
+ * and ABSENT on a legacy call. `structured: true` is the caller's capability flag: the
36
+ * daemon only puts the new keys on the response (mission summary/outcome/version,
37
+ * `result`) when it is set, so an older mcp-server that never sends it keeps decoding
38
+ * byte-identical responses (mixed-version fleets, design §13.1).
39
+ */
40
+ structured?: boolean;
41
+ summary?: string;
42
+ outcome?: string;
43
+ outcomeReason?: string;
44
+ /** Mission id open items move to when this call closes the mission (default: the carry-over mission). */
45
+ carryOverTo?: string;
46
+ /** Route to the mesh's auto-created carry-over mission instead of `id`/`title` (decision requests without a mission). `title` may then be empty. */
47
+ toCarryOver?: boolean;
48
+ itemsAdd?: readonly Record<string, unknown>[];
49
+ itemsUpdate?: readonly Record<string, unknown>[];
50
+ }
51
+ export interface MeshMissionOpenItemCountsWire {
52
+ remaining: number;
53
+ live_check: number;
54
+ decision: number;
55
+ evidence: number;
56
+ }
57
+ /** Structured-missions fields (only with `structured: true` on the request). */
58
+ export interface MeshMissionStructuredWire {
59
+ summary?: string;
60
+ outcome?: string;
61
+ outcomeReason?: string;
62
+ version?: number;
63
+ carryOver?: boolean;
64
+ openItems?: MeshMissionOpenItemCountsWire;
33
65
  }
34
- export interface MeshMissionRecordWire {
66
+ export interface MeshMissionRecordWire extends MeshMissionStructuredWire {
35
67
  id: string;
36
68
  meshId: string;
37
69
  title: string;
@@ -42,6 +74,8 @@ export interface MeshMissionRecordWire {
42
74
  }
43
75
  export interface MissionUpsertResponse {
44
76
  mission: MeshMissionRecordWire;
77
+ /** Structured calls only: per-item results, movedItems/movedTo, restoredItems, ownerConfirmationItemId, warnings. */
78
+ result?: Record<string, unknown>;
45
79
  }
46
80
  export declare function decodeMissionUpsertRequest(value: unknown): MissionUpsertRequest | null;
47
81
  export declare function isMissionUpsertResponse(value: unknown): value is MissionUpsertResponse;
@@ -77,6 +111,12 @@ export interface MissionListQueryRequest {
77
111
  * option except `historyIdLimit` is ignored. An older daemon rejects the key.
78
112
  */
79
113
  meshStatusView?: 'compact' | 'verbose';
114
+ /** Structured-missions capability flag (see MissionUpsertRequest.structured): rows then carry summary/outcome/openItems/carryOver/version. */
115
+ structured?: boolean;
116
+ /** Structured only: attach each mission's items. */
117
+ items?: boolean;
118
+ /** Structured only: attach each mission's change history (bounded tail). */
119
+ history?: boolean;
80
120
  }
81
121
  export interface MeshMissionTaskAggregateWire {
82
122
  total: number;
@@ -115,6 +155,15 @@ export interface MissionListSummaryVerboseWire {
115
155
  createdAt?: string;
116
156
  updatedAt?: string;
117
157
  closeCandidateEmittedAt?: string | null;
158
+ /** Structured lists only. */
159
+ summary?: string;
160
+ outcome?: string;
161
+ outcomeReason?: string;
162
+ version?: number;
163
+ carryOver?: boolean;
164
+ openItems?: MeshMissionOpenItemCountsWire;
165
+ items?: readonly Record<string, unknown>[];
166
+ history?: readonly Record<string, unknown>[];
118
167
  }
119
168
  /** Compact (default) mission row. `goalPreview`/`goalTruncated` present, `goal` absent. */
120
169
  export interface MissionListSummarySlimWire {
@@ -132,6 +181,15 @@ export interface MissionListSummarySlimWire {
132
181
  createdAt?: string;
133
182
  updatedAt?: string;
134
183
  closeCandidateEmittedAt?: string | null;
184
+ /** Structured lists only. */
185
+ summary?: string;
186
+ outcome?: string;
187
+ outcomeReason?: string;
188
+ version?: number;
189
+ carryOver?: boolean;
190
+ openItems?: MeshMissionOpenItemCountsWire;
191
+ items?: readonly Record<string, unknown>[];
192
+ history?: readonly Record<string, unknown>[];
135
193
  }
136
194
  export type MissionListSummaryWire = MissionListSummaryVerboseWire | MissionListSummarySlimWire;
137
195
  export interface MeshMissionHistoryFoldWire {
@@ -30,7 +30,7 @@
30
30
  * the protocol contract the marker demarcates.
31
31
  */
32
32
  import { type MissionBrief } from './mission-brief';
33
- export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff", "driver_list", "driver_describe", "driver_invoke", "artifact_register"];
33
+ export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff", "driver_list", "driver_describe", "driver_invoke", "driver_scaffold", "driver_validate", "driver_export", "driver_install", "artifact_register", "artifact_list", "artifact_read", "contract_read", "contract_observe", "contract_current"];
34
34
  export type WorkerTool = typeof WORKER_TOOLS[number];
35
35
  export declare function isWorkerTool(value: unknown): value is WorkerTool;
36
36
  /** Stable marker line so a body is never footered twice and tests can find it. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/mesh-shared",
3
- "version": "1.0.82-rc.33",
3
+ "version": "1.0.82-rc.35",
4
4
  "description": "ADHDev mesh-shared — pure mesh/git status normalizers shared by daemon-core and web-core",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -34,6 +34,10 @@ export const ASSISTANT_TOOLS = [
34
34
  'launch_defaults_set',
35
35
  'driver_list',
36
36
  'driver_describe',
37
+ 'artifact_list',
38
+ 'artifact_read',
39
+ 'decision_answer',
40
+ 'decision_request',
37
41
  ] as const
38
42
 
39
43
  export type AssistantTool = typeof ASSISTANT_TOOLS[number]
@@ -67,6 +71,12 @@ export const ASSISTANT_VERB = {
67
71
  projectNote: 'assistant_project_note',
68
72
  launchDefaults: 'assistant_launch_defaults',
69
73
  launchDefaultsSet: 'assistant_launch_defaults_set',
74
+ artifactList: 'assistant_artifact_list',
75
+ artifactRead: 'assistant_artifact_read',
76
+ /** Owner-decision inbox (structured-missions design 2026-10-28 §6.3): the owner's words recorded as answers. */
77
+ decisionAnswer: 'assistant_decision_answer',
78
+ /** Register a plain-text question a coordinator put to the owner as a decision item (§5.2). */
79
+ decisionRequest: 'assistant_decision_request',
70
80
  // assistant-side daemon verbs without a tool of the same name
71
81
  launch: 'launch_assistant',
72
82
  pendingRelays: 'assistant_pending_relays',
@@ -101,6 +111,10 @@ export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>
101
111
  project_note: ASSISTANT_VERB.projectNote,
102
112
  launch_defaults: ASSISTANT_VERB.launchDefaults,
103
113
  launch_defaults_set: ASSISTANT_VERB.launchDefaultsSet,
114
+ artifact_list: ASSISTANT_VERB.artifactList,
115
+ artifact_read: ASSISTANT_VERB.artifactRead,
116
+ decision_answer: ASSISTANT_VERB.decisionAnswer,
117
+ decision_request: ASSISTANT_VERB.decisionRequest,
104
118
  }
105
119
 
106
120
  /**
@@ -109,7 +123,7 @@ export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>
109
123
  * because they cause effects (a coordinator message or launch, a new
110
124
  * project), and MCP clients use readOnlyHint to skip confirmation.
111
125
  */
112
- export const ASSISTANT_WRITE_TOOLS: readonly AssistantTool[] = ['project_watch', 'assistant_action', 'memory', 'skill_manage', 'project_note', 'project_send', 'project_add', 'project_attach_remote', 'launch_defaults_set']
126
+ export const ASSISTANT_WRITE_TOOLS: readonly AssistantTool[] = ['project_watch', 'assistant_action', 'memory', 'skill_manage', 'project_note', 'project_send', 'project_add', 'project_attach_remote', 'launch_defaults_set', 'decision_answer', 'decision_request']
113
127
 
114
128
  /** Owner-only verbs: never reachable from the assistant's MCP server (no `ipc`). */
115
129
  export const ASSISTANT_OWNER_VERBS: readonly AssistantVerb[] = [
@@ -164,7 +178,7 @@ export const ASSISTANT_REMOTE_FOLLOW_COMMAND = 'assistant_remote_follow' as cons
164
178
  * `assistant_remote_project` when the host's `assistantRemoteAccess` ACL is active.
165
179
  * `note`, `terminal`, and `inventory` remain strictly roster-member-only.
166
180
  */
167
- export const ASSISTANT_FOLLOWER_ALLOWED_OPS = ['status', 'read', 'poll', 'report', 'send'] as const
181
+ export const ASSISTANT_FOLLOWER_ALLOWED_OPS = ['status', 'read', 'poll', 'report', 'send', 'artifact_list', 'artifact_read', 'decision_list', 'decision_answer', 'decision_request'] as const
168
182
  export type AssistantFollowerAllowedOp = typeof ASSISTANT_FOLLOWER_ALLOWED_OPS[number]
169
183
 
170
184
  const ASSISTANT_FOLLOWER_ALLOWED_OP_SET: ReadonlySet<string> = new Set(ASSISTANT_FOLLOWER_ALLOWED_OPS)
@@ -173,6 +187,25 @@ export function isAssistantFollowerAllowedOp(value: unknown): value is Assistant
173
187
  return typeof value === 'string' && ASSISTANT_FOLLOWER_ALLOWED_OP_SET.has(value)
174
188
  }
175
189
 
190
+ /**
191
+ * Owner-decision ops were added after grants were persisted with an explicit
192
+ * `allowedOps` list. A grant that already lets the follower read status (list)
193
+ * or send to the coordinator (answer / register) covers the matching decision op,
194
+ * so no existing grant needs re-issuing.
195
+ */
196
+ const DECISION_OP_IMPLIED_BY: Readonly<Record<string, AssistantFollowerAllowedOp>> = {
197
+ decision_list: 'status',
198
+ decision_answer: 'send',
199
+ decision_request: 'send',
200
+ }
201
+
202
+ export function followerOpPermitted(op: string, allowedOps: readonly AssistantFollowerAllowedOp[]): boolean {
203
+ if (!isAssistantFollowerAllowedOp(op)) return false
204
+ if ((allowedOps as readonly string[]).includes(op)) return true
205
+ const implied = DECISION_OP_IMPLIED_BY[op]
206
+ return implied !== undefined && allowedOps.includes(implied)
207
+ }
208
+
176
209
  /**
177
210
  * Host-authoritative narrow project ACL persisted on the hosting daemon's
178
211
  * `LocalMeshEntry` in `meshes.json`. Revocation increments `grantRevision`
@@ -188,6 +221,7 @@ export interface AssistantRemoteAccessBinding {
188
221
  status: 'active' | 'revoked'
189
222
  allowedOps: readonly AssistantFollowerAllowedOp[]
190
223
  allowCoordinatorLaunch?: boolean
224
+ allowArtifactRead?: boolean
191
225
  grantedAt: number
192
226
  revokedAt?: number
193
227
  }
@@ -234,6 +268,7 @@ export interface FollowedRemoteProjectEntry {
234
268
  bindingRevision: number
235
269
  grantRevision: number
236
270
  allowCoordinatorLaunch?: boolean
271
+ allowArtifactRead?: boolean
237
272
  state: FollowedRemoteProjectState
238
273
  followedAt: number | string
239
274
  updatedAt: number | string
@@ -264,6 +299,7 @@ export interface RemoteProjectCatalogItem {
264
299
  sanitizedRepoIdentity: string | null
265
300
  coordinatorState?: 'none' | 'idle' | 'working' | 'waiting' | string
266
301
  allowCoordinatorLaunch?: boolean
302
+ allowArtifactRead?: boolean
267
303
  capabilities: {
268
304
  status: true
269
305
  read: true
@@ -272,6 +308,7 @@ export interface RemoteProjectCatalogItem {
272
308
  send: true
273
309
  coordinatorPresent?: boolean
274
310
  allowCoordinatorLaunch?: boolean
311
+ allowArtifactRead?: boolean
275
312
  }
276
313
  observedAt: number | string
277
314
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Opt-in "answer a decision from the push notification" relay (mission 4ae3edf2, owner 2026-10-29).
3
+ *
4
+ * ★ A deliberate, narrow exception to "no dashboard command relay over server WS" (CLAUDE.md).
5
+ * With the user's opt-in setting ON and no dashboard window open, the notification's
6
+ * recommended-option action is POSTed by the service worker to the server, which forwards it to the
7
+ * owning daemon over the existing daemon WS as ONE fixed command. What crosses the server is exactly
8
+ * `{ meshId, itemId, optionId }` (+ the single-use answer token that authorizes it) — never the
9
+ * question, context or free text. Shared so the server's frame and the daemon's ingress cannot drift.
10
+ */
11
+
12
+ /** Daemon command the server sends; its only accepted source is {@link DECISION_PUSH_RELAY_SOURCE}. */
13
+ export const DECISION_PUSH_RELAY_COMMAND = 'mission_decision_push_relay_answer'
14
+
15
+ /** Command source the server stamps on the relayed frame (never settable by REST/API callers). */
16
+ export const DECISION_PUSH_RELAY_SOURCE = 'push_relay'
17
+
18
+ /** The complete relayed payload — ids only. */
19
+ export type DecisionPushRelayPayload = {
20
+ meshId: string
21
+ itemId: string
22
+ optionId: string
23
+ }