@adhdev/mesh-shared 1.0.82-rc.4 → 1.0.82-rc.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/assistant-protocol.d.ts +161 -6
  2. package/dist/decision-push-relay.d.ts +20 -0
  3. package/dist/index.d.ts +11 -0
  4. package/dist/index.js +1197 -118
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1107 -118
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/launch-defaults.d.ts +196 -0
  9. package/dist/mesh-tool-names.d.ts +2 -2
  10. package/dist/mesh-vocabulary.d.ts +13 -4
  11. package/dist/mission-git-state.d.ts +83 -0
  12. package/dist/mission-items.d.ts +202 -0
  13. package/dist/node-facts.d.ts +75 -1
  14. package/dist/outbound-message.d.ts +9 -2
  15. package/dist/path-ownership.d.ts +1 -1
  16. package/dist/protocol/daemon-server.d.ts +42 -2
  17. package/dist/protocol/dashboard-daemon-p2p.d.ts +1 -1
  18. package/dist/protocol/dashboard-server.d.ts +20 -2
  19. package/dist/provider-approval.d.ts +36 -0
  20. package/dist/provider-question.d.ts +19 -0
  21. package/dist/quota-freshness.d.ts +65 -0
  22. package/dist/relay-reprobe.d.ts +52 -0
  23. package/dist/release.d.ts +90 -0
  24. package/dist/role-launch.d.ts +11 -0
  25. package/dist/session-launch.d.ts +10 -22
  26. package/dist/slot-proposal.d.ts +14 -0
  27. package/dist/task-source-phase.d.ts +75 -0
  28. package/dist/turn-evidence.d.ts +29 -13
  29. package/dist/turn-ipc-mission.d.ts +68 -1
  30. package/dist/turn-ipc-queue.d.ts +19 -0
  31. package/dist/turn-ipc.d.ts +2 -2
  32. package/dist/types.d.ts +1 -0
  33. package/dist/worker-protocol.d.ts +1 -1
  34. package/package.json +1 -1
  35. package/src/assistant-protocol.ts +302 -6
  36. package/src/daemon-normalize.ts +7 -5
  37. package/src/decision-push-relay.ts +23 -0
  38. package/src/index.ts +12 -0
  39. package/src/keyed-doc-delta.ts +9 -1
  40. package/src/launch-defaults.ts +170 -0
  41. package/src/mesh-tool-names.ts +16 -0
  42. package/src/mesh-vocabulary.ts +12 -2
  43. package/src/mission-git-state.ts +112 -0
  44. package/src/mission-items.ts +421 -0
  45. package/src/node-facts.ts +70 -1
  46. package/src/outbound-message.ts +19 -0
  47. package/src/path-ownership.ts +4 -6
  48. package/src/protocol/daemon-server.ts +37 -2
  49. package/src/protocol/dashboard-server.ts +13 -2
  50. package/src/provider-approval.ts +59 -0
  51. package/src/provider-question.ts +31 -0
  52. package/src/quota-freshness.ts +139 -0
  53. package/src/relay-reprobe.ts +117 -0
  54. package/src/release.ts +121 -0
  55. package/src/role-launch.ts +51 -0
  56. package/src/session-launch.ts +17 -1
  57. package/src/slot-proposal.ts +61 -2
  58. package/src/task-source-phase.ts +61 -0
  59. package/src/turn-evidence.ts +27 -20
  60. package/src/turn-ipc-mission.ts +121 -11
  61. package/src/turn-ipc-queue.ts +29 -1
  62. package/src/turn-ipc-turn.ts +1 -1
  63. package/src/turn-ipc.ts +1 -0
  64. package/src/types.ts +1 -0
  65. package/src/worker-protocol.ts +15 -0
package/src/release.ts ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * release — types, limits and pure helpers for release objects
3
+ * (docs/design/2026-11-01-release-objects.md).
4
+ *
5
+ * A release is a mesh-scoped entity that groups MISSIONS (explicit membership) and records
6
+ * deploy facts. It is distinct from the vendor release stamp (a build channel stamp) and
7
+ * from `.adhdev/preview-deploy.json` (the per-checkout deploy record it snapshots).
8
+ * Membership is only ever created by an explicit call; git evaluation and deploy heads
9
+ * produce suggestions, never members. Dependency-free leaf.
10
+ */
11
+
12
+ export const MESH_RELEASE_CHANNELS = ['preview', 'stable'] as const
13
+ export type MeshReleaseChannel = typeof MESH_RELEASE_CHANNELS[number]
14
+ export function isMeshReleaseChannel(value: unknown): value is MeshReleaseChannel {
15
+ return typeof value === 'string' && (MESH_RELEASE_CHANNELS as readonly string[]).includes(value)
16
+ }
17
+
18
+ export const MESH_RELEASE_STATUSES = ['planned', 'frozen', 'deployed', 'verified', 'superseded', 'cancelled'] as const
19
+ export type ReleaseStatus = typeof MESH_RELEASE_STATUSES[number]
20
+ export function isReleaseStatus(value: unknown): value is ReleaseStatus {
21
+ return typeof value === 'string' && (MESH_RELEASE_STATUSES as readonly string[]).includes(value)
22
+ }
23
+
24
+ export const RELEASE_MEMBER_STATES = ['member', 'moved_out', 'removed'] as const
25
+ export type ReleaseMemberState = typeof RELEASE_MEMBER_STATES[number]
26
+
27
+ /** Who may write release membership. The daemon never adds a member on its own. */
28
+ export const RELEASE_ACTORS = ['coordinator', 'owner', 'assistant', 'script'] as const
29
+ export type ReleaseActor = typeof RELEASE_ACTORS[number]
30
+
31
+ export const RELEASE_EVENT_TYPES = [
32
+ 'created', 'status_changed', 'scope_added', 'scope_moved', 'scope_removed',
33
+ 'deploy_recorded', 'changelog_drafted', 'changelog_applied',
34
+ ] as const
35
+ export type ReleaseEventType = typeof RELEASE_EVENT_TYPES[number]
36
+
37
+ /** Mission-side events recorded when membership changes (so the mission history shows it). */
38
+ export const MISSION_RELEASE_EVENT_TYPES = ['release_attached', 'release_moved', 'release_detached'] as const
39
+
40
+ export const RELEASE_LIMITS = {
41
+ name: 80,
42
+ note: 500,
43
+ /** `mesh_missions.release_note`: one user/operator-visible sentence. */
44
+ releaseNote: 400,
45
+ missionsPerCall: 100,
46
+ changelogEntries: 300,
47
+ } as const
48
+
49
+ /** Deploy targets snapshot (a copy of `.adhdev/preview-deploy.json` `targets`). */
50
+ export interface ReleaseDeployTarget {
51
+ commit?: string
52
+ deployedAt?: string
53
+ previewVersion?: string
54
+ [key: string]: unknown
55
+ }
56
+ export type ReleaseTargets = Partial<Record<'npm' | 'server' | 'web', ReleaseDeployTarget>> & Record<string, ReleaseDeployTarget | undefined>
57
+
58
+ export const CHANGELOG_SECTIONS = ['Added', 'Fixed', 'Changed'] as const
59
+ export type ChangelogSection = typeof CHANGELOG_SECTIONS[number]
60
+
61
+ export interface ChangelogDraftEntry {
62
+ missionId: string
63
+ /** `<!-- m:<id8> -->` — the idempotency marker `--apply` writes after the line. */
64
+ marker: string
65
+ section: ChangelogSection
66
+ /** The sentence that would be written to CHANGELOG.md. */
67
+ text: string
68
+ source: 'release_note' | 'summary'
69
+ /** True when `text` is not user-facing wording yet (carries the needs-wording marker). */
70
+ needsWording?: boolean
71
+ /** Evidence kept in the draft JSON only; never written to CHANGELOG.md. */
72
+ evidence?: { gitState?: string; outcome?: string; shas?: string[] }
73
+ /** Non-blocking lint warnings (internal ids / wave numbers / forbidden phrases). */
74
+ lint?: string[]
75
+ }
76
+
77
+ export interface ChangelogDraftExcluded {
78
+ missionId: string
79
+ reason: string
80
+ }
81
+
82
+ export interface ChangelogDraft {
83
+ generatedAt: string
84
+ entries: ChangelogDraftEntry[]
85
+ /** Member missions without a `release_note` (target 0 at deploy time). */
86
+ needsNote: string[]
87
+ excluded: ChangelogDraftExcluded[]
88
+ /** Member missions whose refs are not confirmed on the default branch (`not_landed` / `unknown`). */
89
+ warnings: Array<{ missionId: string; gitState: string; message: string }>
90
+ appliedAt?: string
91
+ }
92
+
93
+ export const NEEDS_WORDING_MARKER = '[needs user-facing wording]'
94
+
95
+ /** Marker that makes `--apply` idempotent: one per mission entry inside CHANGELOG.md. */
96
+ export function changelogMissionMarker(missionId: string): string {
97
+ return `<!-- m:${missionId.slice(0, 8)} -->`
98
+ }
99
+
100
+ /** Default section for an entry; `[Fixed]`/`[Added]`/`[Changed]` prefixes on the note pin it. */
101
+ export function classifyChangelogSection(text: string, hint?: string): { section: ChangelogSection; text: string } {
102
+ const m = /^\s*\[(added|fixed|changed)\]\s*/i.exec(text)
103
+ if (m) {
104
+ const key = m[1].toLowerCase()
105
+ return { section: key === 'added' ? 'Added' : key === 'fixed' ? 'Fixed' : 'Changed', text: text.slice(m[0].length).trim() }
106
+ }
107
+ const probe = `${hint ?? ''} ${text}`.toLowerCase()
108
+ if (/\b(fix|fixes|fixed|bug|regression|crash|error)\b|오류|결함|버그|수정/.test(probe)) return { section: 'Fixed', text }
109
+ if (/\b(add|adds|added|new|introduc\w*|support\w*)\b|신규|추가/.test(probe)) return { section: 'Added', text }
110
+ return { section: 'Changed', text }
111
+ }
112
+
113
+ /** Phrases that must not reach a public CHANGELOG (internal waves, rc numbering, ids). Warning-only. */
114
+ export function lintChangelogText(text: string): string[] {
115
+ const out: string[] = []
116
+ if (/\bwave[\s-]*\d+/i.test(text) || /웨이브\s*\d+/.test(text)) out.push('mentions an internal wave number')
117
+ if (/\brc[.\s-]?\d+\b/i.test(text)) out.push('mentions an internal rc number')
118
+ if (/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i.test(text) || /\bmi_[0-9a-f]{12}\b/.test(text) || /\brel_[0-9a-f]{12}\b/.test(text)) out.push('contains an internal id')
119
+ if (/\bmission\b/i.test(text)) out.push('mentions "mission" (internal vocabulary)')
120
+ return out
121
+ }
@@ -0,0 +1,51 @@
1
+ /** Persisted role-launch intent. Restart replays this selection rather than
2
+ * consulting newer machine defaults; restore preserves its original provenance. */
3
+ import { isLaunchDefaultRole, isLaunchDefaultSource, type LaunchDefaultRole,
4
+ type LaunchDefaultSelection, type LaunchDefaultAxis, type LaunchDefaultRef } from './launch-defaults';
5
+ export interface RoleLaunchSelection {
6
+ role: LaunchDefaultRole;
7
+ selection: LaunchDefaultSelection;
8
+ applied: Record<keyof LaunchDefaultSelection, LaunchDefaultAxis>;
9
+ defaultRef?: LaunchDefaultRef;
10
+ documentRevision: number;
11
+ }
12
+ function record(value: unknown): Record<string, unknown> | undefined {
13
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
14
+ ? value as Record<string, unknown> : undefined;
15
+ }
16
+ function ref(value: unknown): LaunchDefaultRef | undefined {
17
+ const r = record(value);
18
+ if (!r || !isLaunchDefaultRole(r.role) || (r.scope !== 'global' && r.scope !== 'project')
19
+ || !Number.isSafeInteger(r.revision) || (r.revision as number) < 1
20
+ || (r.scope === 'project' && (typeof r.projectId !== 'string' || !r.projectId))) return undefined;
21
+ return { role: r.role, scope: r.scope, revision: r.revision as number,
22
+ ...(r.scope === 'project' ? { projectId: r.projectId as string } : {}) };
23
+ }
24
+ function axis(value: unknown): LaunchDefaultAxis | undefined {
25
+ const a = record(value);
26
+ if (!a || !isLaunchDefaultSource(a.source) || (a.value !== null && typeof a.value !== 'string')) return undefined;
27
+ const defaultRef = ref(a.defaultRef);
28
+ if (a.source === 'stored_default' && defaultRef?.scope !== 'global'
29
+ || a.source === 'project_default' && defaultRef?.scope !== 'project'
30
+ || (a.source === 'user' || a.source === 'provider_default') && a.defaultRef !== undefined) return undefined;
31
+ return { value: a.value as string | null, source: a.source, ...(defaultRef ? { defaultRef } : {}) };
32
+ }
33
+ export function parseRoleLaunchSelection(value: unknown): RoleLaunchSelection | undefined {
34
+ const r = record(value), s = record(r?.selection), a = record(r?.applied);
35
+ if (!r || !isLaunchDefaultRole(r.role) || !s || typeof s.provider !== 'string' || !s.provider
36
+ || !Number.isSafeInteger(r.documentRevision) || (r.documentRevision as number) < 1
37
+ || (s.model !== undefined && typeof s.model !== 'string')
38
+ || (s.thinkingLevel !== undefined && typeof s.thinkingLevel !== 'string')) return undefined;
39
+ const provider = axis(a?.provider), model = axis(a?.model), thinkingLevel = axis(a?.thinkingLevel);
40
+ if (!provider || !model || !thinkingLevel
41
+ || [provider, model, thinkingLevel].some(axis => axis.defaultRef && (axis.defaultRef.role !== r.role
42
+ || axis.defaultRef.scope === 'global' && axis.defaultRef.revision > (r.documentRevision as number)))
43
+ || provider.value !== s.provider
44
+ || model.value !== (s.model ?? null) || thinkingLevel.value !== (s.thinkingLevel ?? null)) return undefined;
45
+ const defaultRef = ref(r.defaultRef);
46
+ return { role: r.role, selection: { provider: s.provider,
47
+ ...(typeof s.model === 'string' ? { model: s.model } : {}),
48
+ ...(typeof s.thinkingLevel === 'string' ? { thinkingLevel: s.thinkingLevel } : {}) },
49
+ applied: { provider, model, thinkingLevel }, ...(defaultRef ? { defaultRef } : {}),
50
+ documentRevision: r.documentRevision as number };
51
+ }
@@ -1,3 +1,4 @@
1
+ import { parseRoleLaunchSelection, type RoleLaunchSelection } from './role-launch'
1
2
  /**
2
3
  * session-launch — how a session was launched: provider, model and thinking
3
4
  * level, and WHERE each value came from.
@@ -35,11 +36,14 @@ export const MODEL_AXIS_SOURCES = [
35
36
  'user',
36
37
  /** The dashboard dialog restored it from the last launch (browser-local memory). */
37
38
  'remembered',
39
+ /** The authoritative daemon role default or host project override. */
40
+ 'stored_default',
41
+ 'project_default',
38
42
  /** A mesh capability slot supplied it (or a difficulty preset the slot outranked). */
39
43
  'mesh_slot',
40
44
  /** An explicit per-task model / thinking level on a mesh queue task. */
41
45
  'task_override',
42
- /** Nothing was requested; the provider's default is known (discovery / manifest). */
46
+ /** Nothing requested: the provider chooses; resolvedDefault records it when known. */
43
47
  'provider_default',
44
48
  /** A value was requested without saying where it came from, or nothing is known. */
45
49
  'unspecified',
@@ -97,11 +101,19 @@ export interface ModelSelection {
97
101
  /** Last value the provider itself reported (statusline, native history). */
98
102
  observed?: string
99
103
  observedAt?: number
104
+ /**
105
+ * The provider SILENTLY substituted another model for the requested one
106
+ * (antigravity: `model X is not recognized … Using "Y" instead`). `observed`
107
+ * then holds the model actually running. P2P/launch-record only — the
108
+ * server path never carries it (see the content boundary above).
109
+ */
110
+ fallback?: boolean
100
111
  /** Every value change in order. The last entry is the value in force. */
101
112
  history: ModelSelectionHistoryEntry[]
102
113
  }
103
114
 
104
115
  export interface SessionLaunchRecord {
116
+ roleLaunch?: RoleLaunchSelection
105
117
  sessionId: string
106
118
  providerType: string
107
119
  providerVersion?: string
@@ -201,6 +213,7 @@ function parseModelSelection(raw: unknown): ModelSelection | undefined {
201
213
  const current = readString(record.current)
202
214
  const observed = readString(record.observed)
203
215
  const observedAt = readFiniteNumber(record.observedAt)
216
+ const fallback = record.fallback === true
204
217
  return {
205
218
  source: isModelAxisSource(record.source) ? record.source : 'unspecified',
206
219
  ...(requested ? { requested } : {}),
@@ -209,6 +222,7 @@ function parseModelSelection(raw: unknown): ModelSelection | undefined {
209
222
  ...(current ? { current } : {}),
210
223
  ...(observed ? { observed } : {}),
211
224
  ...(observedAt !== undefined ? { observedAt } : {}),
225
+ ...(fallback ? { fallback } : {}),
212
226
  history: history.slice(-MODEL_SELECTION_HISTORY_LIMIT),
213
227
  }
214
228
  }
@@ -230,7 +244,9 @@ export function parseSessionLaunchRecord(raw: unknown): SessionLaunchRecord | un
230
244
  const providerVersion = readString(record.providerVersion)
231
245
  const workspace = readString(record.workspace)
232
246
  const autoApproveModeId = readString(record.autoApproveModeId)
247
+ const roleLaunch = parseRoleLaunchSelection(record.roleLaunch)
233
248
  return {
249
+ ...(roleLaunch ? { roleLaunch } : {}),
234
250
  sessionId,
235
251
  providerType,
236
252
  ...(providerVersion ? { providerVersion } : {}),
@@ -40,6 +40,13 @@ import {
40
40
  export interface CliSlotRecipe {
41
41
  /** Optional model to pin on the slot. Best-effort at launch. */
42
42
  model?: string
43
+ /**
44
+ * Optional ordered candidate models to prefer when the provider's live
45
+ * `modelOptions` (from model discovery) is present on `DetectedCliProvider`.
46
+ * The first candidate present in `modelOptions` wins; falls back to `model`
47
+ * when `modelOptions` is absent or contains none of the candidates.
48
+ */
49
+ modelCandidates?: readonly string[]
43
50
  /** Optional thinking level, in the provider's own vocabulary. */
44
51
  thinkingLevel?: string
45
52
  /** Difficulty range this slot handles. Empty = general-purpose. */
@@ -101,11 +108,26 @@ export const CLI_SLOT_RECIPES: Readonly<Record<string, readonly CliSlotRecipe[]>
101
108
  ],
102
109
  'antigravity-cli': [
103
110
  {
104
- model: 'Gemini 3.1 Pro (High)',
111
+ model: 'Gemini 3.8 Flash (High)',
112
+ modelCandidates: ['Gemini 3.8 Flash (High)', 'Gemini 4 Argon (Low)', 'Gemini 3.7 Flash (High)', 'Gemini 3.1 Pro (High)'],
105
113
  difficulty: ['easy'],
106
114
  maxParallel: 2,
107
115
  rationale: 'Cheap capacity for easy tasks.',
108
116
  },
117
+ {
118
+ model: 'Gemini 4 Argon (Medium)',
119
+ modelCandidates: ['Gemini 4 Argon (Medium)', 'Gemini 3.8 Flash (High)', 'Gemini 3.7 Flash (High)', 'Gemini 3.1 Pro (High)'],
120
+ difficulty: ['medium'],
121
+ maxParallel: 2,
122
+ rationale: 'Balanced Argon tier for medium tasks.',
123
+ },
124
+ {
125
+ model: 'Gemini 4 Argon (High)',
126
+ modelCandidates: ['Gemini 4 Argon (High)', 'Gemini 3.8 Flash (High)', 'Gemini 3.1 Pro (High)'],
127
+ difficulty: ['difficult'],
128
+ maxParallel: 1,
129
+ rationale: 'Reserved for hard tasks; capped at 1 to bound cost.',
130
+ },
109
131
  ],
110
132
  'cursor-cli': [
111
133
  {
@@ -137,6 +159,13 @@ export interface DetectedCliProvider {
137
159
  displayName?: string
138
160
  /** Detected version, when known. Display only — never affects the mapping. */
139
161
  version?: string
162
+ /**
163
+ * Live discovered / resolved `modelOptions` for this provider on the node,
164
+ * when available on the status catalog. When present and non-empty, recipes
165
+ * prefer the first matching model from `[recipe.model, ...recipe.modelCandidates]`
166
+ * that exists in `modelOptions`.
167
+ */
168
+ modelOptions?: readonly string[]
140
169
  }
141
170
 
142
171
  /** One proposed slot plus why it was proposed. */
@@ -197,6 +226,36 @@ function dedupeDetected(detected: readonly DetectedCliProvider[]): DetectedCliPr
197
226
  return out
198
227
  }
199
228
 
229
+ /**
230
+ * Resolve a recipe's pinned model against the provider's live `modelOptions`
231
+ * (populated from the daemon's model-discovery overlay when available). If
232
+ * `modelOptions` is absent or none of the recipe's candidates appear in it,
233
+ * falls back to `recipe.model`.
234
+ */
235
+ function resolveRecipeModel(
236
+ recipe: CliSlotRecipe,
237
+ modelOptions: readonly string[] | undefined,
238
+ ): string | undefined {
239
+ if (!Array.isArray(modelOptions) || modelOptions.length === 0) {
240
+ return recipe.model
241
+ }
242
+ const available = new Set(
243
+ modelOptions
244
+ .filter((m): m is string => typeof m === 'string' && m.trim().length > 0)
245
+ .map(m => m.trim()),
246
+ )
247
+ if (available.size === 0) return recipe.model
248
+ const candidates = [
249
+ ...(recipe.model ? [recipe.model] : []),
250
+ ...(recipe.modelCandidates ?? []),
251
+ ]
252
+ for (const candidate of candidates) {
253
+ const trimmed = typeof candidate === 'string' ? candidate.trim() : ''
254
+ if (trimmed && available.has(trimmed)) return trimmed
255
+ }
256
+ return recipe.model
257
+ }
258
+
200
259
  /**
201
260
  * Build a capability-slot proposal from a node's detected CLI providers.
202
261
  *
@@ -229,7 +288,7 @@ export function buildSlotProposal(
229
288
  // a proposal can never preview a shape the write would reshape.
230
289
  const slot = normalizeNodeCapabilitySlot({
231
290
  provider: provider.type,
232
- model: recipe.model,
291
+ model: resolveRecipeModel(recipe, provider.modelOptions),
233
292
  thinkingLevel: recipe.thinkingLevel,
234
293
  difficulty: recipe.difficulty,
235
294
  maxParallel: recipe.maxParallel,
@@ -0,0 +1,61 @@
1
+ import type { OwnedPathEntry } from './path-ownership'
2
+
3
+ export type MeshTaskPhase = 'source' | 'integration'
4
+
5
+ /** Host-produced evidence. Never accepted from MCP/peer-supplied paths or branch labels. */
6
+ export interface PhysicalOwnershipScope {
7
+ version: 1
8
+ repoIdentity: string
9
+ machineId: string
10
+ nodeId: string
11
+ worktree: string
12
+ worktreeIdentity: string
13
+ gitDir: string
14
+ commonGitDir: string
15
+ phase: MeshTaskPhase
16
+ gitResources: { index: string; refs: string; hooks: string; config: string }
17
+ resources: Array<{ path: string; identity?: string; subtree: boolean }>
18
+ declaration: readonly OwnedPathEntry[]
19
+ }
20
+
21
+ /** A source packet is an input to integration, never release/feature acceptance. */
22
+ export interface MeshSourceCandidate {
23
+ version: 1
24
+ readiness: 'uncommitted' | 'verified_source_candidate'
25
+ taskId: string
26
+ missionId: string
27
+ revision: number
28
+ repoIdentity: string
29
+ machineId: string
30
+ canonicalWorktree: string
31
+ rootBaseSha: string
32
+ ossBaseSha: string
33
+ rootCandidateSha?: string
34
+ ossCandidateSha?: string
35
+ patchSha256: string
36
+ manifestSha256: string
37
+ touchedPaths: string[]
38
+ tests: Array<{ command: string; exit: number; logSha256: string }>
39
+ settledCommands: boolean
40
+ integrationTaskId: string
41
+ logicalOverlapTaskIds: string[]
42
+ }
43
+
44
+ /** Product B consumes one frozen batch; normal hooks/parity/gates remain required. */
45
+ export interface MeshIntegrationBatchContract {
46
+ version: 1
47
+ batchId: string
48
+ candidates: MeshSourceCandidate[]
49
+ integrationOrder: string[]
50
+ approvedRootSha: string
51
+ approvedOssSha: string
52
+ bundleInvocationCount: 1
53
+ requiredGates: string[]
54
+ normalHooksRequired: true
55
+ independentReviewRequired: true
56
+ ownerLandingApprovalRequired: true
57
+ }
58
+
59
+ /** These only rank legal targets; targetNodeId/SessionId and requiredTags remain hard. */
60
+ export interface MeshRoutingPreferences { nodeIds?: string[]; providers?: string[] }
61
+ export interface MeshReviewConstraint { sourceTaskId: string; differentProvider: true }
@@ -56,7 +56,11 @@ export type DeliveryOutcome = typeof DELIVERY_OUTCOMES[number]
56
56
  export const DELIVERY_VIAS = ['local', 'p2p'] as const
57
57
  export type DeliveryVia = typeof DELIVERY_VIAS[number]
58
58
 
59
- export const DISPATCH_FAILURE_REASONS = ['worker_absent', 'transport_error', 'spawn_failed', 'rejected_by_worker', 'timeout'] as const
59
+ /** `submit_unconfirmed` (SUBMIT-UNCONFIRMED-RECLAIM): the worker's CLI never
60
+ * took the written prompt — the submit budget and one re-paste were spent at
61
+ * an idle composer. An owner daemon older than this value drops the evidence
62
+ * as invalid and falls back to its consume/turn holds (the pre-fix path). */
63
+ export const DISPATCH_FAILURE_REASONS = ['worker_absent', 'transport_error', 'spawn_failed', 'rejected_by_worker', 'timeout', 'submit_unconfirmed'] as const
60
64
  export type DispatchFailureReason = typeof DISPATCH_FAILURE_REASONS[number]
61
65
 
62
66
  /**
@@ -83,9 +87,9 @@ export type SessionReboundReason = typeof SESSION_REBOUND_REASONS[number]
83
87
 
84
88
  export const SUSPENSION_MODALS = ['approval', 'choice'] as const
85
89
  export type SuspensionModal = typeof SUSPENSION_MODALS[number]
86
- export const SUSPENSION_RESOLUTIONS = ['approved', 'rejected', 'answered'] as const
90
+ export const SUSPENSION_RESOLUTIONS = ['approved', 'rejected', 'answered', 'dismissed'] as const
87
91
  export type SuspensionResolution = typeof SUSPENSION_RESOLUTIONS[number]
88
- export const SUSPENSION_RESOLUTION_VIAS = ['modal_button', 'auto_approve', 'prompt_answer'] as const
92
+ export const SUSPENSION_RESOLUTION_VIAS = ['modal_button', 'auto_approve', 'prompt_answer', 'modal_disappeared'] as const
89
93
  export type SuspensionResolutionVia = typeof SUSPENSION_RESOLUTION_VIAS[number]
90
94
 
91
95
  export const TURN_END_STRENGTHS = ['genuine', 'weak'] as const
@@ -97,13 +101,13 @@ export const TURN_END_BLOCK_REASONS = [
97
101
  ] as const
98
102
  export type TurnEndBlockReason = typeof TURN_END_BLOCK_REASONS[number]
99
103
 
100
- export const NATIVE_TURN_OUTCOMES = ['completed', 'aborted'] as const
104
+ export const NATIVE_TURN_OUTCOMES = ['completed', 'aborted', 'failed'] as const
101
105
  export type NativeTurnOutcome = typeof NATIVE_TURN_OUTCOMES[number]
102
106
 
103
107
  export const LIVENESS_RESULTS = ['alive', 'unknown', 'read_failed', 'dead'] as const
104
108
  export type LivenessResult = typeof LIVENESS_RESULTS[number]
105
109
 
106
- export const PROVIDER_FAILURES = ['auth_failed', 'billing_failed'] as const
110
+ export const PROVIDER_FAILURES = ['auth_failed', 'billing_failed', 'usage_limit_exceeded', 'server_overloaded', 'provider_error'] as const
107
111
  export type ProviderFailure = typeof PROVIDER_FAILURES[number]
108
112
 
109
113
  /**
@@ -150,11 +154,11 @@ export const HOLD_REASONS = [
150
154
  // — a turn_end commits the report (R9t), expiry commits it anyway (R13t) so
151
155
  // a session whose FSM never shows the idle edge again does not wait for
152
156
  // liveness / hard_ceiling.
153
- 'await_end',
157
+ 'await_end', 'report_correction',
154
158
  ] as const
155
159
  export type HoldReason = typeof HOLD_REASONS[number]
156
160
 
157
- export const TURN_OUTCOMES = ['completed', 'failed', 'cancelled'] as const
161
+ export const TURN_OUTCOMES = ['completed', 'blocked', 'failed', 'cancelled'] as const
158
162
  export type TurnOutcome = typeof TURN_OUTCOMES[number]
159
163
 
160
164
  export const COMMIT_STRENGTHS = ['genuine', 'weak', 'tool_report', 'operator'] as const
@@ -164,7 +168,7 @@ export type CommitStrength = typeof COMMIT_STRENGTHS[number]
164
168
  export const TURN_REASONS = [
165
169
  // commits
166
170
  'turn_end', 'weak_end_confirmed', 'transcript_final', 'worker_reported',
167
- 'finalization_timeout_no_response', 'session_error', 'provider_auth_failed', 'provider_billing_failed',
171
+ 'finalization_timeout_no_response', 'weak_end_liveness_unconfirmed', 'weak_end_probe_unobserved', 'report_correction_expired', 'session_error', 'provider_auth_failed', 'provider_billing_failed', 'provider_usage_limit_exceeded', 'provider_server_overloaded', 'provider_error',
168
172
  'operator_cancel', 'intentional_cleanup', 'task_removed', 'mission_abandoned', 'superseded',
169
173
  'operator_update', 'refine_terminal', 'validation_terminal',
170
174
  'hard_ceiling', 'reclaim_budget_exhausted', 'hollow_max_retries', 'migration_orphan',
@@ -186,7 +190,7 @@ export type TurnReason = typeof TURN_REASONS[number]
186
190
  * owner's local `turn_events.payload_json` and is rendered at deliver time.
187
191
  */
188
192
  export const NOTIFY_KINDS = [
189
- 'completed', 'failed', 'cancelled', 'stopped', 'approval', 'choice', 'approval_resolved',
193
+ 'completed', 'blocked', 'failed', 'cancelled', 'stopped', 'approval', 'choice', 'approval_resolved',
190
194
  'candidate', 'no_progress', 'progress', 'late_completion', 'mesh_event',
191
195
  ] as const
192
196
  export type NotifyKind = typeof NOTIFY_KINDS[number]
@@ -207,7 +211,7 @@ export interface LiveTurnPending { modal: boolean; adapterPending: boolean; trai
207
211
  /** Structural copy of daemon-core's `CoordinatorIdentity` (this package cannot import daemon-core). */
208
212
  export interface EvidenceCoordinatorIdentity { daemonId: string; coordinatorRunId: string; sessionId?: string }
209
213
 
210
- export interface NativeTurnMarkerRef { outcome: NativeTurnOutcome; turnId?: string }
214
+ export interface NativeTurnMarkerRef { outcome: NativeTurnOutcome; turnId?: string; receivedAt?: number; startedAt?: number; errorAt?: number }
211
215
 
212
216
  export interface EvidenceEnvelope {
213
217
  /** Stable id of this observation; dedupe key across republish/replication. */
@@ -250,10 +254,10 @@ export type TurnEvidenceBody =
250
254
  | { kind: 'session_rebound'; toSessionId: string; reason: SessionReboundReason }
251
255
  // ── worker turn lifecycle ──
252
256
  | { kind: 'turn_started'; retro: boolean }
253
- | { kind: 'suspension'; modal: SuspensionModal; modalKey?: string }
254
- | { kind: 'suspension_resolved'; resolution: SuspensionResolution; via: SuspensionResolutionVia }
257
+ | { kind: 'suspension'; modal: SuspensionModal; modalKey?: string; providerSetup?: boolean; clockUnknown?: boolean }
258
+ | { kind: 'suspension_resolved'; resolution: SuspensionResolution; via: SuspensionResolutionVia; modalKey?: string }
255
259
  | { kind: 'turn_end'; strength: TurnEndStrength; afterFinalizationTimeout?: boolean; hollow?: boolean; summary?: SummaryRef
256
- blockReason?: TurnEndBlockReason; releasedByHardCap?: boolean; nativeOutcome?: NativeTurnOutcome; live?: LiveTurnPending
260
+ blockReason?: TurnEndBlockReason; releasedByHardCap?: boolean; nativeOutcome?: NativeTurnOutcome; nativeMarker?: NativeTurnMarkerRef; live?: LiveTurnPending
257
261
  /**
258
262
  * Stamped (true only) by the WORKER's daemon when the session holds a live
259
263
  * worker-MCP session bind, i.e. the worker can call report_completion. The
@@ -273,7 +277,7 @@ export type TurnEvidenceBody =
273
277
  live: LiveTurnPending; summary?: SummaryRef; messageAt?: number }
274
278
  | { kind: 'transcript_activity'; newestActivityAt: number }
275
279
  | { kind: 'no_progress'; stalledMs: number; observedStatus: NoProgressObservedStatus; finalAssistantPresent: boolean }
276
- | { kind: 'liveness'; result: LivenessResult }
280
+ | { kind: 'liveness'; result: LivenessResult; probeStartedAtMs?: number }
277
281
  | { kind: 'process_exit'; exitCode: number | null; providerFailure?: ProviderFailure }
278
282
  | { kind: 'session_error'; reason: SessionErrorReason }
279
283
  // ── worker MCP (F2) ──
@@ -284,6 +288,7 @@ export type TurnEvidenceBody =
284
288
  */
285
289
  | { kind: 'worker_report'; outcome: WorkerReportOutcome; summary?: SummaryRef; hasHandoffNotes: boolean
286
290
  branchState?: WorkerBranchState; touchedFileCount?: number }
291
+ | { kind: 'worker_report_correction_requested'; deliveryId?: string }
287
292
  | { kind: 'worker_progress'; note?: SummaryRef }
288
293
  // ── coordinator / operator ──
289
294
  | { kind: 'git_side_effect'; dirty: boolean; commitsSinceDispatch: number; attributable: boolean }
@@ -352,13 +357,13 @@ export const TURN_EVIDENCE_FIELD_SPECS: TurnEvidenceFieldSpecs = {
352
357
  duplicate_dispatch_refusal: { holderSessionId: id, holderAttemptId: idOpt },
353
358
  session_rebound: { toSessionId: id, reason: en(SESSION_REBOUND_REASONS) },
354
359
  turn_started: { retro: bool },
355
- suspension: { modal: en(SUSPENSION_MODALS), modalKey: idOpt },
356
- suspension_resolved: { resolution: en(SUSPENSION_RESOLUTIONS), via: en(SUSPENSION_RESOLUTION_VIAS) },
360
+ suspension: { modal: en(SUSPENSION_MODALS), modalKey: idOpt, providerSetup: boolOpt, clockUnknown: boolOpt },
361
+ suspension_resolved: { resolution: en(SUSPENSION_RESOLUTIONS), via: en(SUSPENSION_RESOLUTION_VIAS), modalKey: idOpt },
357
362
  turn_end: {
358
363
  strength: en(TURN_END_STRENGTHS), afterFinalizationTimeout: boolOpt, hollow: boolOpt,
359
364
  summary: { t: 'summary_ref', optional: true }, blockReason: enOpt(TURN_END_BLOCK_REASONS),
360
365
  releasedByHardCap: boolOpt, nativeOutcome: enOpt(NATIVE_TURN_OUTCOMES), live: { t: 'live', optional: true },
361
- reportExpected: boolOpt, providerFailure: enOpt(PROVIDER_FAILURES),
366
+ nativeMarker: { t: 'native_marker', optional: true }, reportExpected: boolOpt, providerFailure: enOpt(PROVIDER_FAILURES),
362
367
  },
363
368
  transcript_final: {
364
369
  selfAttributing: bool, nativeRead: bool, nativeMarker: { t: 'native_marker', optional: true },
@@ -366,13 +371,14 @@ export const TURN_EVIDENCE_FIELD_SPECS: TurnEvidenceFieldSpecs = {
366
371
  },
367
372
  transcript_activity: { newestActivityAt: ms },
368
373
  no_progress: { stalledMs: ms, observedStatus: en(NO_PROGRESS_OBSERVED_STATUSES), finalAssistantPresent: bool },
369
- liveness: { result: en(LIVENESS_RESULTS) },
374
+ liveness: { result: en(LIVENESS_RESULTS), probeStartedAtMs: msOpt },
370
375
  process_exit: { exitCode: { t: 'int', nullable: true }, providerFailure: enOpt(PROVIDER_FAILURES) },
371
376
  session_error: { reason: en(SESSION_ERROR_REASONS) },
372
377
  worker_report: {
373
378
  outcome: en(WORKER_REPORT_OUTCOMES), summary: { t: 'summary_ref', optional: true }, hasHandoffNotes: bool,
374
379
  branchState: enOpt(WORKER_BRANCH_STATES), touchedFileCount: intOpt,
375
380
  },
381
+ worker_report_correction_requested: { deliveryId: { t: 'id', optional: true } },
376
382
  worker_progress: { note: { t: 'summary_ref', optional: true } },
377
383
  git_side_effect: { dirty: bool, commitsSinceDispatch: { t: 'int' }, attributable: bool },
378
384
  cancel: { reason: en(CANCEL_REASONS) },
@@ -440,11 +446,12 @@ function isLiveTurnPending(value: unknown): value is LiveTurnPending {
440
446
  && (value.newestActivityAt === undefined || isFiniteNumber(value.newestActivityAt))
441
447
  }
442
448
 
443
- function isNativeMarkerRef(value: unknown): value is NativeTurnMarkerRef {
449
+ export function isNativeMarkerRef(value: unknown): value is NativeTurnMarkerRef {
444
450
  return isRecord(value)
445
- && hasOnlyKeys(value, ['outcome', 'turnId'])
451
+ && hasOnlyKeys(value, ['outcome', 'turnId', 'receivedAt', 'startedAt', 'errorAt'])
446
452
  && (NATIVE_TURN_OUTCOMES as readonly string[]).includes(value.outcome as string)
447
453
  && (value.turnId === undefined || isEvidenceIdentifier(value.turnId))
454
+ && ['receivedAt', 'startedAt', 'errorAt'].every((key) => value[key] === undefined || isFiniteNumber(value[key]))
448
455
  }
449
456
 
450
457
  function isCoordinatorIdentity(value: unknown): value is EvidenceCoordinatorIdentity {