@adhdev/mesh-shared 1.0.82-rc.34 → 1.0.82-rc.36

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.
@@ -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 = {
@@ -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
@@ -41,6 +41,13 @@ export interface ModelSelection {
41
41
  /** Last value the provider itself reported (statusline, native history). */
42
42
  observed?: string;
43
43
  observedAt?: number;
44
+ /**
45
+ * The provider SILENTLY substituted another model for the requested one
46
+ * (antigravity: `model X is not recognized … Using "Y" instead`). `observed`
47
+ * then holds the model actually running. P2P/launch-record only — the
48
+ * server path never carries it (see the content boundary above).
49
+ */
50
+ fallback?: boolean;
44
51
  /** Every value change in order. The last entry is the value in force. */
45
52
  history: ModelSelectionHistoryEntry[];
46
53
  }
@@ -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", "artifact_list", "artifact_read", "contract_read", "contract_observe", "contract_current"];
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.34",
3
+ "version": "1.0.82-rc.36",
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",
@@ -36,6 +36,8 @@ export const ASSISTANT_TOOLS = [
36
36
  'driver_describe',
37
37
  'artifact_list',
38
38
  'artifact_read',
39
+ 'decision_answer',
40
+ 'decision_request',
39
41
  ] as const
40
42
 
41
43
  export type AssistantTool = typeof ASSISTANT_TOOLS[number]
@@ -71,6 +73,10 @@ export const ASSISTANT_VERB = {
71
73
  launchDefaultsSet: 'assistant_launch_defaults_set',
72
74
  artifactList: 'assistant_artifact_list',
73
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',
74
80
  // assistant-side daemon verbs without a tool of the same name
75
81
  launch: 'launch_assistant',
76
82
  pendingRelays: 'assistant_pending_relays',
@@ -107,6 +113,8 @@ export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>
107
113
  launch_defaults_set: ASSISTANT_VERB.launchDefaultsSet,
108
114
  artifact_list: ASSISTANT_VERB.artifactList,
109
115
  artifact_read: ASSISTANT_VERB.artifactRead,
116
+ decision_answer: ASSISTANT_VERB.decisionAnswer,
117
+ decision_request: ASSISTANT_VERB.decisionRequest,
110
118
  }
111
119
 
112
120
  /**
@@ -115,7 +123,7 @@ export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>
115
123
  * because they cause effects (a coordinator message or launch, a new
116
124
  * project), and MCP clients use readOnlyHint to skip confirmation.
117
125
  */
118
- 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']
119
127
 
120
128
  /** Owner-only verbs: never reachable from the assistant's MCP server (no `ipc`). */
121
129
  export const ASSISTANT_OWNER_VERBS: readonly AssistantVerb[] = [
@@ -170,7 +178,7 @@ export const ASSISTANT_REMOTE_FOLLOW_COMMAND = 'assistant_remote_follow' as cons
170
178
  * `assistant_remote_project` when the host's `assistantRemoteAccess` ACL is active.
171
179
  * `note`, `terminal`, and `inventory` remain strictly roster-member-only.
172
180
  */
173
- export const ASSISTANT_FOLLOWER_ALLOWED_OPS = ['status', 'read', 'poll', 'report', 'send', 'artifact_list', 'artifact_read'] 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
174
182
  export type AssistantFollowerAllowedOp = typeof ASSISTANT_FOLLOWER_ALLOWED_OPS[number]
175
183
 
176
184
  const ASSISTANT_FOLLOWER_ALLOWED_OP_SET: ReadonlySet<string> = new Set(ASSISTANT_FOLLOWER_ALLOWED_OPS)
@@ -179,6 +187,25 @@ export function isAssistantFollowerAllowedOp(value: unknown): value is Assistant
179
187
  return typeof value === 'string' && ASSISTANT_FOLLOWER_ALLOWED_OP_SET.has(value)
180
188
  }
181
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
+
182
209
  /**
183
210
  * Host-authoritative narrow project ACL persisted on the hosting daemon's
184
211
  * `LocalMeshEntry` in `meshes.json`. Revocation increments `grantRevision`
@@ -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
+ }
package/src/index.ts CHANGED
@@ -41,6 +41,8 @@ export * from './session-launch'
41
41
  export * from './turn-ipc'
42
42
  export * from './path-ownership'
43
43
  export * from './mission-brief'
44
+ export * from './mission-items'
45
+ export * from './decision-push-relay'
44
46
  export * from './keyed-doc-delta'
45
47
  export * from './provider-approval'
46
48
  export * from './provider-question'