@adhdev/mesh-shared 1.0.82-rc.4 → 1.0.82-rc.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assistant-protocol.d.ts +161 -6
- package/dist/decision-push-relay.d.ts +20 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1197 -118
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1107 -118
- package/dist/index.mjs.map +1 -1
- package/dist/launch-defaults.d.ts +196 -0
- package/dist/mesh-tool-names.d.ts +2 -2
- package/dist/mesh-vocabulary.d.ts +13 -4
- package/dist/mission-git-state.d.ts +83 -0
- package/dist/mission-items.d.ts +202 -0
- package/dist/node-facts.d.ts +75 -1
- package/dist/outbound-message.d.ts +9 -2
- package/dist/path-ownership.d.ts +1 -1
- package/dist/protocol/daemon-server.d.ts +42 -2
- package/dist/protocol/dashboard-daemon-p2p.d.ts +1 -1
- package/dist/protocol/dashboard-server.d.ts +20 -2
- package/dist/provider-approval.d.ts +36 -0
- package/dist/provider-question.d.ts +19 -0
- package/dist/quota-freshness.d.ts +65 -0
- package/dist/relay-reprobe.d.ts +52 -0
- package/dist/release.d.ts +90 -0
- package/dist/role-launch.d.ts +11 -0
- package/dist/session-launch.d.ts +10 -22
- package/dist/slot-proposal.d.ts +14 -0
- package/dist/task-source-phase.d.ts +75 -0
- package/dist/turn-evidence.d.ts +29 -13
- package/dist/turn-ipc-mission.d.ts +68 -1
- package/dist/turn-ipc-queue.d.ts +19 -0
- package/dist/turn-ipc.d.ts +2 -2
- package/dist/types.d.ts +1 -0
- package/dist/worker-protocol.d.ts +1 -1
- package/package.json +1 -1
- package/src/assistant-protocol.ts +302 -6
- package/src/daemon-normalize.ts +7 -5
- package/src/decision-push-relay.ts +23 -0
- package/src/index.ts +12 -0
- package/src/keyed-doc-delta.ts +9 -1
- package/src/launch-defaults.ts +170 -0
- package/src/mesh-tool-names.ts +16 -0
- package/src/mesh-vocabulary.ts +12 -2
- package/src/mission-git-state.ts +112 -0
- package/src/mission-items.ts +421 -0
- package/src/node-facts.ts +70 -1
- package/src/outbound-message.ts +19 -0
- package/src/path-ownership.ts +4 -6
- package/src/protocol/daemon-server.ts +37 -2
- package/src/protocol/dashboard-server.ts +13 -2
- package/src/provider-approval.ts +59 -0
- package/src/provider-question.ts +31 -0
- package/src/quota-freshness.ts +139 -0
- package/src/relay-reprobe.ts +117 -0
- package/src/release.ts +121 -0
- package/src/role-launch.ts +51 -0
- package/src/session-launch.ts +17 -1
- package/src/slot-proposal.ts +61 -2
- package/src/task-source-phase.ts +61 -0
- package/src/turn-evidence.ts +27 -20
- package/src/turn-ipc-mission.ts +121 -11
- package/src/turn-ipc-queue.ts +29 -1
- package/src/turn-ipc-turn.ts +1 -1
- package/src/turn-ipc.ts +1 -0
- package/src/types.ts +1 -0
- 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
|
+
}
|
package/src/session-launch.ts
CHANGED
|
@@ -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
|
|
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 } : {}),
|
package/src/slot-proposal.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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 }
|
package/src/turn-evidence.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|