thinkpool-pair 0.7.354 → 0.7.357
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/bridge.mjs +42 -1
- package/claude-session.mjs +240 -15
- package/code-event-contract.mjs +9 -0
- package/codex-event-mapper.mjs +4 -1
- package/context-contract.mjs +95 -0
- package/design-edit.mjs +116 -8
- package/design-source-contract.mjs +4 -0
- package/error-recovery.mjs +50 -0
- package/event-bounds.mjs +4 -0
- package/evidence-citations.mjs +50 -0
- package/evidence-compact.mjs +11 -0
- package/flow-preview.mjs +4 -0
- package/hermes-event-mapper.mjs +8 -1
- package/lane-continuation.mjs +83 -0
- package/lane-lifecycle.mjs +7 -1
- package/package.json +9 -1
- package/provider-resilience.mjs +356 -0
- package/providers.mjs +61 -2
- package/recap.mjs +13 -5
- package/repo-search.mjs +2 -0
- package/runtime-contract.mjs +93 -0
- package/runtime-registry.mjs +6 -0
- package/runtime-session.mjs +5 -0
- package/thinkpool-capabilities.json +5 -5
- package/thinkpool-room-prompt.mjs +17 -1
- package/viewport.mjs +18 -0
package/design-edit.mjs
CHANGED
|
@@ -2,13 +2,24 @@ import crypto from 'node:crypto'
|
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
+
import {
|
|
6
|
+
DESIGN_SOURCE_MAP_RELATIVE,
|
|
7
|
+
DESIGN_SOURCE_MAP_VERSION,
|
|
8
|
+
MAX_DESIGN_SOURCE_ENTRIES,
|
|
9
|
+
MAX_DESIGN_SOURCE_MAP_BYTES,
|
|
10
|
+
} from './design-source-contract.mjs'
|
|
5
11
|
|
|
6
12
|
const MAX_SOURCE_BYTES = 2 * 1024 * 1024
|
|
7
13
|
const MAX_INTENT = 2_000
|
|
8
14
|
const MAX_TEXT = 4_000
|
|
9
15
|
const MAX_BATCH_EDITS = 8
|
|
16
|
+
const GENERATED_SOURCE_PARTS = new Set(['dist', 'build', '.next', '.git', 'coverage', 'node_modules'])
|
|
10
17
|
|
|
11
18
|
const clean = (value, max) => String(value ?? '').replace(/\0/g, '').trim().slice(0, max)
|
|
19
|
+
const isInside = (parent, child) => {
|
|
20
|
+
const rel = path.relative(parent, child)
|
|
21
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))
|
|
22
|
+
}
|
|
12
23
|
|
|
13
24
|
// supabase-js does not copy the Auth session into an already-created Realtime
|
|
14
25
|
// client. Private tpdesign:* joins therefore need the bridge owner's JWT pinned
|
|
@@ -28,6 +39,52 @@ export function sourceRevision(source) {
|
|
|
28
39
|
return crypto.createHash('sha256').update(String(source)).digest('hex').slice(0, 24)
|
|
29
40
|
}
|
|
30
41
|
|
|
42
|
+
export function resolveDesignSourceMap(file, previewRoot, workspaceRoot) {
|
|
43
|
+
if (!file || !previewRoot || !workspaceRoot) return null
|
|
44
|
+
let sourceMapPath, expectedSourceMapPath, builtRoot, root, raw, parsed
|
|
45
|
+
try {
|
|
46
|
+
sourceMapPath = fs.realpathSync(file)
|
|
47
|
+
builtRoot = fs.realpathSync(previewRoot)
|
|
48
|
+
expectedSourceMapPath = fs.realpathSync(path.join(builtRoot, DESIGN_SOURCE_MAP_RELATIVE))
|
|
49
|
+
root = fs.realpathSync(workspaceRoot)
|
|
50
|
+
const stat = fs.statSync(sourceMapPath)
|
|
51
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_DESIGN_SOURCE_MAP_BYTES) return null
|
|
52
|
+
if (sourceMapPath !== expectedSourceMapPath || !isInside(root, builtRoot) || !isInside(builtRoot, sourceMapPath)) return null
|
|
53
|
+
raw = fs.readFileSync(sourceMapPath, 'utf8')
|
|
54
|
+
parsed = JSON.parse(raw)
|
|
55
|
+
} catch { return null }
|
|
56
|
+
if (parsed?.version !== DESIGN_SOURCE_MAP_VERSION || !parsed.entries || typeof parsed.entries !== 'object' || Array.isArray(parsed.entries)) return null
|
|
57
|
+
const rows = Object.entries(parsed.entries)
|
|
58
|
+
if (rows.length > MAX_DESIGN_SOURCE_ENTRIES) return null
|
|
59
|
+
|
|
60
|
+
const files = new Map()
|
|
61
|
+
const entries = new Map()
|
|
62
|
+
for (const [sourceId, value] of rows) {
|
|
63
|
+
if (!/^[A-Za-z0-9_-]{8,80}$/.test(sourceId) || !value || typeof value !== 'object') return null
|
|
64
|
+
const relative = clean(value.file, 700)
|
|
65
|
+
if (!relative || path.isAbsolute(relative) || relative.split(/[\\/]/).some((part) => part === '..' || GENERATED_SOURCE_PARTS.has(part))) return null
|
|
66
|
+
let sourcePath = files.get(relative)
|
|
67
|
+
if (!sourcePath) {
|
|
68
|
+
try {
|
|
69
|
+
sourcePath = fs.realpathSync(path.resolve(root, relative))
|
|
70
|
+
const stat = fs.statSync(sourcePath)
|
|
71
|
+
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES || !isInside(root, sourcePath)) return null
|
|
72
|
+
} catch { return null }
|
|
73
|
+
files.set(relative, sourcePath)
|
|
74
|
+
}
|
|
75
|
+
const start = Number(value.start)
|
|
76
|
+
const end = Number(value.end)
|
|
77
|
+
const line = Number(value.line)
|
|
78
|
+
const column = Number(value.column)
|
|
79
|
+
const tag = clean(value.tag, 40).toLowerCase()
|
|
80
|
+
const fileHash = clean(value.fileHash, 64).toLowerCase()
|
|
81
|
+
if (![start, end, line, column].every(Number.isSafeInteger) || start < 0 || end <= start || line < 1 || column < 0) return null
|
|
82
|
+
if (!/^[a-z][a-z0-9:-]*$/.test(tag) || !/^[a-f0-9]{24}$/.test(fileHash)) return null
|
|
83
|
+
entries.set(sourceId, { sourceId, file: relative.split(path.sep).join('/'), sourcePath, start, end, line, column, tag, fileHash })
|
|
84
|
+
}
|
|
85
|
+
return { path: sourceMapPath, revision: sourceRevision(raw), buildId: clean(parsed.buildId, 80), entries }
|
|
86
|
+
}
|
|
87
|
+
|
|
31
88
|
export function resolveDesignSource(file, workspaceRoot) {
|
|
32
89
|
if (!file || !workspaceRoot) return null
|
|
33
90
|
let sourcePath, root
|
|
@@ -67,11 +124,21 @@ export function resolvePreviewDesignSource(manifest, workspaceRoot, box) {
|
|
|
67
124
|
const captureKey = clean(manifest.captureKey, 1000)
|
|
68
125
|
if (!route.startsWith('/') || !captureKey || captureKey !== JSON.stringify([previewRoot, route])) return null
|
|
69
126
|
const source = fs.readFileSync(sourcePath, 'utf8')
|
|
70
|
-
|
|
127
|
+
let builtRoot = null
|
|
128
|
+
try {
|
|
129
|
+
builtRoot = fs.realpathSync(path.resolve(root, previewRoot))
|
|
130
|
+
if (!isInside(root, builtRoot)) return null
|
|
131
|
+
} catch { return null }
|
|
132
|
+
const sourceMap = manifest.sourceMap ? resolveDesignSourceMap(manifest.sourceMap, builtRoot, root) : null
|
|
133
|
+
const revision = sourceRevision(sourceMap ? `${source}\0${sourceMap.revision}` : source)
|
|
71
134
|
const previewId = crypto.createHash('sha256')
|
|
72
135
|
.update(`${root}\0${captureKey}\0${revision}`)
|
|
73
136
|
.digest('base64url').slice(0, 32)
|
|
74
|
-
return {
|
|
137
|
+
return {
|
|
138
|
+
sourceKind: 'preview', previewId, revision, sourcePath, workspaceRoot: root, source, outbox,
|
|
139
|
+
route, previewRoot, captureKey, sourceMap, sourceMapPath: sourceMap?.path || null,
|
|
140
|
+
sourceMapDeclared: !!manifest.sourceMap,
|
|
141
|
+
}
|
|
75
142
|
}
|
|
76
143
|
|
|
77
144
|
// Older lane worktrees predate the explicit `source` manifest field and emit the
|
|
@@ -108,7 +175,7 @@ export function refreshDesignSource(record) {
|
|
|
108
175
|
if (record.sourceKind !== 'preview') return resolveDesignSource(record.sourcePath, record.workspaceRoot)
|
|
109
176
|
return resolvePreviewDesignSource({
|
|
110
177
|
sourceKind: 'preview', snapshot: record.sourcePath, route: record.route,
|
|
111
|
-
previewRoot: record.previewRoot, captureKey: record.captureKey,
|
|
178
|
+
previewRoot: record.previewRoot, captureKey: record.captureKey, sourceMap: record.sourceMapPath,
|
|
112
179
|
}, record.workspaceRoot, record.outbox)
|
|
113
180
|
}
|
|
114
181
|
|
|
@@ -168,10 +235,35 @@ function safeTarget(value) {
|
|
|
168
235
|
canReplaceImage: value.canReplaceImage === true,
|
|
169
236
|
nodeKey: clean(value.nodeKey, 80),
|
|
170
237
|
parentNodeKey: clean(value.parentNodeKey, 80),
|
|
238
|
+
sourceId: clean(value.sourceId, 80),
|
|
239
|
+
parentSourceId: clean(value.parentSourceId, 80),
|
|
171
240
|
rect,
|
|
172
241
|
}
|
|
173
242
|
}
|
|
174
243
|
|
|
244
|
+
export function resolveRequestSourceIdentity(record, target) {
|
|
245
|
+
if (!target?.sourceId) return { ok: true, identity: null }
|
|
246
|
+
const entry = record?.sourceMap?.entries?.get(target.sourceId)
|
|
247
|
+
if (!entry) return { ok: false, error: record?.sourceMapDeclared ? 'stale-source-identity' : 'source-identity-unavailable' }
|
|
248
|
+
if (target.tag && entry.tag !== target.tag) return { ok: false, error: 'source-identity-mismatch' }
|
|
249
|
+
let source
|
|
250
|
+
try { source = fs.readFileSync(entry.sourcePath, 'utf8') } catch { return { ok: false, error: 'stale-source-identity' } }
|
|
251
|
+
if (sourceRevision(source) !== entry.fileHash) return { ok: false, error: 'stale-source-identity' }
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
identity: {
|
|
255
|
+
sourceId: entry.sourceId,
|
|
256
|
+
file: entry.file,
|
|
257
|
+
start: entry.start,
|
|
258
|
+
end: entry.end,
|
|
259
|
+
line: entry.line,
|
|
260
|
+
column: entry.column,
|
|
261
|
+
tag: entry.tag,
|
|
262
|
+
fileHash: entry.fileHash,
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
175
267
|
function safeMovement(value) {
|
|
176
268
|
if (!value || typeof value !== 'object') return null
|
|
177
269
|
const fromIndex = Number(value.fromIndex)
|
|
@@ -211,7 +303,16 @@ export function validateDesignRequest(payload, registry, room = '') {
|
|
|
211
303
|
if (mode === 'move' && (!movement || movement.fromIndex === movement.toIndex)) return { ok: false, error: 'invalid-move' }
|
|
212
304
|
const asset = mode === 'image' ? safeAsset(payload.asset, room) : null
|
|
213
305
|
if (mode === 'image' && (!target.canReplaceImage || !asset)) return { ok: false, error: 'invalid-image' }
|
|
214
|
-
|
|
306
|
+
const source = resolveRequestSourceIdentity(record, target)
|
|
307
|
+
if (!source.ok) return source
|
|
308
|
+
return {
|
|
309
|
+
ok: true,
|
|
310
|
+
record,
|
|
311
|
+
request: {
|
|
312
|
+
previewId, revision: record.revision, mode, intent, target, movement, asset,
|
|
313
|
+
sourceIdentity: source.identity, cid: clean(payload.cid, 80),
|
|
314
|
+
},
|
|
315
|
+
}
|
|
215
316
|
}
|
|
216
317
|
|
|
217
318
|
export function validateDesignBatchRequest(payload, registry, room = '') {
|
|
@@ -226,7 +327,7 @@ export function validateDesignBatchRequest(payload, registry, room = '') {
|
|
|
226
327
|
for (let index = 0; index < payload.edits.length; index++) {
|
|
227
328
|
const verdict = validateDesignRequest({ ...payload.edits[index], previewId, revision, cid }, registry, room)
|
|
228
329
|
if (!verdict.ok) return { ...verdict, editIndex: index }
|
|
229
|
-
const targetKey = verdict.request.target.nodeKey || verdict.request.target.selector || verdict.request.target.xpath
|
|
330
|
+
const targetKey = verdict.request.target.sourceId || verdict.request.target.nodeKey || verdict.request.target.selector || verdict.request.target.xpath
|
|
230
331
|
if (targets.has(targetKey)) return { ok: false, error: 'duplicate-target', editIndex: index }
|
|
231
332
|
targets.add(targetKey)
|
|
232
333
|
record = verdict.record
|
|
@@ -276,6 +377,13 @@ function designAction(request) {
|
|
|
276
377
|
return `Apply this visual change to only the selected element unless the instruction explicitly requires its immediate context:\n${request.intent}`
|
|
277
378
|
}
|
|
278
379
|
|
|
380
|
+
function selectedElementBlock(request) {
|
|
381
|
+
const identity = request.sourceIdentity
|
|
382
|
+
? `\n\nBridge-verified application source:\n${JSON.stringify(request.sourceIdentity, null, 2)}`
|
|
383
|
+
: '\n\nApplication source identity: unavailable — selector/XPath fallback only.'
|
|
384
|
+
return `Selected element:\n${JSON.stringify(request.target, null, 2)}${identity}`
|
|
385
|
+
}
|
|
386
|
+
|
|
279
387
|
const designModeLabel = (mode) => ({ text: 'Text', prompt: 'Prompt', move: 'Move', image: 'Image' })[mode] || 'Edit'
|
|
280
388
|
|
|
281
389
|
function visibleDesignAction(request) {
|
|
@@ -307,10 +415,10 @@ export function designPrompt({ record, request, by, restore = false, priorRecord
|
|
|
307
415
|
const work = restore
|
|
308
416
|
? `Restore only the immediately previous Design Mode change by comparing the current artifact with this bridge-created verified backup: ${priorRecord?.backupPath || '(backup unavailable)'}. Do not overwrite unrelated changes.`
|
|
309
417
|
: batch
|
|
310
|
-
? batch.map((edit, index) => `Edit ${index + 1} of ${batch.length} · ${edit.mode}\
|
|
311
|
-
:
|
|
418
|
+
? batch.map((edit, index) => `Edit ${index + 1} of ${batch.length} · ${edit.mode}\n${selectedElementBlock(edit)}\n\n${designAction(edit)}`).join('\n\n---\n\n')
|
|
419
|
+
: `${selectedElementBlock(request)}\n\n${designAction(request)}`
|
|
312
420
|
if (record.sourceKind === 'preview') {
|
|
313
|
-
return `A room member used ThinkPool Design Mode on a rendered application preview. This is an authorized source edit in your current workspace.\n\nRendered snapshot (read-only evidence): ${record.sourcePath}\nApplication workspace: ${record.workspaceRoot}\nPreview root: ${record.previewRoot}\nPreview route: ${record.route}\nExpected snapshot revision: ${record.revision}\nRequested by: ${clean(by || 'A room member', 120)}\nOperation: ${batch ? `${batch.length} queued edits in one batch` : request.mode}\n\n${work}\n\nRules:\n- Re-read the rendered snapshot and confirm the selected element still matches before editing.\n- Do not edit the snapshot.
|
|
421
|
+
return `A room member used ThinkPool Design Mode on a rendered application preview. This is an authorized source edit in your current workspace.\n\nRendered snapshot (read-only evidence): ${record.sourcePath}\nApplication workspace: ${record.workspaceRoot}\nPreview root: ${record.previewRoot}\nPreview route: ${record.route}\nExpected snapshot revision: ${record.revision}\nRequested by: ${clean(by || 'A room member', 120)}\nOperation: ${batch ? `${batch.length} queued edits in one batch` : request.mode}\n\n${work}\n\nRules:\n- Re-read the rendered snapshot and confirm the selected element still matches before editing.\n- Do not edit the snapshot. For an edit with bridge-verified application source, start at that exact relative file and range. For a selector/XPath fallback, locate the represented element in application source and do not call the match exact.\n- Before editing a bridge-verified target, confirm the current source still hashes to the supplied fileHash. If it does not, stop and request a fresh capture/reselection.\n- If fallback selector, text, or surrounding structure does not identify one application source location unambiguously, stop without editing and explain that the person must reselect it.\n${batch ? '- Apply the queued edits in order in one source pass, then build and capture once after the full batch.' : '- Apply only the requested change, then build and capture once.'}\n- Preserve the product page faithfully: keep its real copy, structure, fonts, assets, spacing, responsive behavior, and unrelated source unchanged.\n- Run the project build, then use preview_start with root ${record.previewRoot} and preview_capture with path ${record.route}, both viewports, and title ${JSON.stringify(record.title || record.slug || 'Design preview')}. The bridge correlates that capture to this request.\n- Do not claim the edit is verified until the room receives the new desktop and mobile capture.\n- If the build or capture fails, report the failure instead of substituting a screenshot-only result.`
|
|
314
422
|
}
|
|
315
423
|
return `A room member used ThinkPool Design Mode on the artifact below. This is an authorized source edit in your current workspace.\n\nArtifact source: ${record.sourcePath}\nWorkspace: ${record.workspaceRoot}\nExpected source revision: ${record.revision}\nRequested by: ${clean(by || 'A room member', 120)}\nOperation: ${restore ? 'restore the immediately previous verified Design Mode revision' : batch ? `${batch.length} queued edits in one batch` : request.mode}\n\n${work}\n\nRules:\n- Re-read the exact artifact source and confirm its revision/content still matches every selected element before editing.\n${batch ? '- Apply the queued edits in order in one source pass, then render once after the full batch.' : '- Apply only the requested change, then render the result once.'}\n- Edit the canonical source above; do not search for or edit a different plausible file.\n- Preserve the represented product page faithfully: keep its real copy, structure, fonts, assets, spacing, and responsive behavior except for the selected edits. Never replace it with generic mockup content.\n- Preserve unrelated content and styling.\n- Use the existing mockup render workflow to produce both desktop and mobile captures after the edit. Run it with TP_DESIGN_REQUEST_ID=${request.cid} and TP_DESIGN_PARENT_REVISION=${record.revision} in the command environment so the proof correlates to this request.\n- Do not claim this is live until both captures succeed and the room receives the manifest.\n- If any target is stale or ambiguous, stop without editing and explain that the person must reselect it.`
|
|
316
424
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// A safe recovery envelope. It records a bounded decision; it never performs a
|
|
2
|
+
// retry and therefore cannot alter native adapter transport behavior.
|
|
3
|
+
|
|
4
|
+
import { isSecretFreeRoomPayload, runtimeSupportsCapability } from './runtime-contract.mjs'
|
|
5
|
+
|
|
6
|
+
export const ERROR_RECOVERY_VERSION = 1
|
|
7
|
+
export const MAX_RECOVERY_ATTEMPTS = 3
|
|
8
|
+
const CODE = /^[a-z][a-z0-9_-]{0,79}$/
|
|
9
|
+
const SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/
|
|
10
|
+
|
|
11
|
+
const policyFor = ({ runtime, capabilityId }) => runtimeSupportsCapability(runtime, capabilityId)
|
|
12
|
+
? Object.freeze({ maxAttempts: MAX_RECOVERY_ATTEMPTS })
|
|
13
|
+
: Object.freeze({ maxAttempts: 1 })
|
|
14
|
+
|
|
15
|
+
export function normalizeErrorRecovery({ runtime, capabilityId, code = 'runtime_error', retryable = false, attempt = 1, evidenceRefs = [] } = {}) {
|
|
16
|
+
const policy = policyFor({ runtime, capabilityId })
|
|
17
|
+
const safeAttempt = Math.max(1, Math.min(policy.maxAttempts, Number.isSafeInteger(attempt) ? attempt : 1))
|
|
18
|
+
const safeRefs = (Array.isArray(evidenceRefs) ? evidenceRefs : []).slice(0, 16)
|
|
19
|
+
.filter((ref) => ref && SAFE_REF.test(String(ref.type || '')) && SAFE_REF.test(String(ref.id || '')))
|
|
20
|
+
.map((ref) => ({ type: String(ref.type), id: String(ref.id) }))
|
|
21
|
+
const admitted = runtimeSupportsCapability(runtime, capabilityId) && CODE.test(String(code))
|
|
22
|
+
const canRetry = admitted && retryable === true && safeAttempt < policy.maxAttempts
|
|
23
|
+
const envelope = {
|
|
24
|
+
version: ERROR_RECOVERY_VERSION,
|
|
25
|
+
capabilityId: admitted ? capabilityId : 'unknown',
|
|
26
|
+
code: admitted ? String(code) : 'invalid_recovery_input',
|
|
27
|
+
retryable: canRetry,
|
|
28
|
+
attempt: safeAttempt,
|
|
29
|
+
maxAttempts: policy.maxAttempts,
|
|
30
|
+
next: canRetry ? 'retry' : (admitted && retryable === true ? 'ask_human' : 'stop'),
|
|
31
|
+
evidenceRefs: safeRefs,
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze(isSecretFreeRoomPayload(envelope, 4096) ? envelope : {
|
|
34
|
+
version: ERROR_RECOVERY_VERSION, capabilityId: 'unknown', code: 'invalid_recovery_input', retryable: false,
|
|
35
|
+
attempt: 1, maxAttempts: 1, next: 'stop', evidenceRefs: [],
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function recoveryCodeEvent(input) {
|
|
40
|
+
const recovery = normalizeErrorRecovery(input)
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
kind: recovery.next === 'ask_human' ? 'needs-input' : 'error',
|
|
43
|
+
subtype: 'bounded_recovery',
|
|
44
|
+
message: recovery.next === 'ask_human'
|
|
45
|
+
? `Recovery attempt ${recovery.attempt}/${recovery.maxAttempts} is exhausted; human input is required.`
|
|
46
|
+
: `Recovery stopped safely (${recovery.code}).`,
|
|
47
|
+
recovery,
|
|
48
|
+
verified: false,
|
|
49
|
+
})
|
|
50
|
+
}
|
package/event-bounds.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { compactEvidenceEnvelope } from './evidence-compact.mjs'
|
|
2
|
+
|
|
1
3
|
/* Browser-safe structured-event bounding shared by the bridge broadcast path and
|
|
2
4
|
the web client's persisted reload snapshot. Large Codex/Claude tool payloads
|
|
3
5
|
must remain recognisable (tool name + a useful preview), but must never turn a
|
|
@@ -66,6 +68,7 @@ export function boundStructuredEvent(event, cap = STRUCTURED_EVENT_CAP) {
|
|
|
66
68
|
if (!event || typeof event !== 'object' || sizeOf(event) <= cap) return event
|
|
67
69
|
|
|
68
70
|
const out = { ...event, _bounded: true }
|
|
71
|
+
if (out.evidence) out.evidence = compactEvidenceEnvelope(out.evidence, 0)
|
|
69
72
|
const stringBudget = Math.min(12000, Math.max(1000, Math.floor(cap / 4)))
|
|
70
73
|
for (const key of ['text', 'output', 'stdout', 'stderr', 'input']) {
|
|
71
74
|
if (typeof out[key] === 'string') out[key] = clipString(out[key], stringBudget)
|
|
@@ -89,6 +92,7 @@ export function boundStructuredEvent(event, cap = STRUCTURED_EVENT_CAP) {
|
|
|
89
92
|
for (const key of ['kind', 'seq', 'cid', 'ts', 'term', 'toolUseId', 'isError', 'by', 'crosspost', 'relaySourceName']) {
|
|
90
93
|
if (out[key] != null) identity[key] = out[key]
|
|
91
94
|
}
|
|
95
|
+
if (out.evidence) identity.evidence = compactEvidenceEnvelope(out.evidence, 0)
|
|
92
96
|
if (Array.isArray(out.blocks)) identity.blocks = out.blocks.slice(0, 4).map(block => compactBlock(block, 6000))
|
|
93
97
|
else if (Array.isArray(out.content)) identity.content = out.content.slice(0, 4).map(block => compactBlock(block, 6000))
|
|
94
98
|
else if (typeof out.content === 'string') identity.content = clipString(out.content, 12000)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Strict, runtime-neutral EvidenceEnvelopeV1 parsing. Only an actual
|
|
2
|
+
// project-context evidence tool result can create room evidence in S1.
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { EVIDENCE_CAP, compactEvidenceEnvelope } from './evidence-compact.mjs'
|
|
5
|
+
|
|
6
|
+
export { EVIDENCE_CAP, compactEvidenceEnvelope }
|
|
7
|
+
|
|
8
|
+
export const EVIDENCE_START = '<<<TP_EVIDENCE_V1>>>'
|
|
9
|
+
export const EVIDENCE_END = '<<<END_TP_EVIDENCE_V1>>>'
|
|
10
|
+
const LEVELS = new Set(['exact', 'section', 'source', 'unavailable'])
|
|
11
|
+
const WARNINGS = new Set(['NO_MATCH', 'LOW_LEXICAL_MATCH', 'UNCALIBRATED_RELEVANCE', 'SECTION_ONLY', 'SOURCE_ONLY', 'SOURCE_UNAVAILABLE', 'SPAN_ANCHOR_MISMATCH', 'TRANSCRIPT_BOUNDED'])
|
|
12
|
+
const safeText = (value, max) => typeof value === 'string' && value.length <= max && !/(?:https?:\/\/|file:|bearer\s|token=|signature=|x-amz-|\.\.[\\/]|^\/|[A-Za-z]:[\\/])/i.test(value)
|
|
13
|
+
const isProjectEvidenceTool = (name) => ['mcp__project_context__search_context_evidence', 'mcp__project-context__search_context_evidence'].includes(String(name || ''))
|
|
14
|
+
const textContent = (content) => typeof content === 'string' ? content : Array.isArray(content) ? content.map((part) => typeof part === 'string' ? part : part?.text || '').join('\n') : ''
|
|
15
|
+
const excerptDigest = (value) => createHash('sha256').update(value, 'utf8').digest('hex')
|
|
16
|
+
|
|
17
|
+
function validCitation(raw) {
|
|
18
|
+
if (!raw || raw.v !== 1 || !/^ev1_[a-f0-9]{24,64}$/.test(raw.id || '')) return null
|
|
19
|
+
const source = raw.source || {}, locator = raw.locator || {}, revision = source.revision || {}, assessment = raw.assessment || {}, provenance = raw.provenance || {}
|
|
20
|
+
if (source.kind !== 'project_prose' || source.collection !== 'repo' || !safeText(source.label, 180) || !safeText(source.path, 500) || !/^[a-f0-9]{40,64}$/i.test(revision.value || '') || !['git', 'content_sha256'].includes(revision.kind)) return null
|
|
21
|
+
if (!LEVELS.has(locator.level) || !safeText(locator.heading || '', 180)) return null
|
|
22
|
+
if (!['high', 'medium', 'low', 'unscored'].includes(assessment.relevance) || !['direct', 'partial', 'source_only', 'unavailable'].includes(assessment.confidence) || !Array.isArray(assessment.warnings) || assessment.warnings.some((warning) => !WARNINGS.has(warning))) return null
|
|
23
|
+
if (provenance.capturedBy !== 'project-context' || provenance.method !== 'fts5_heading') return null
|
|
24
|
+
const citation = { v: 1, id: raw.id, source: { kind: source.kind, collection: source.collection, label: source.label, path: source.path, revision: { kind: revision.kind, value: revision.value, ...(safeText(revision.indexedAt || '', 80) ? { indexedAt: revision.indexedAt } : {}) } }, locator: { level: locator.level, ...(safeText(locator.heading || '', 180) ? { heading: locator.heading } : {}) }, provenance: { capturedBy: provenance.capturedBy, method: provenance.method, ...(safeText(provenance.retrievedAt || '', 80) ? { retrievedAt: provenance.retrievedAt } : {}), ...(safeText(provenance.query || '', 160) ? { query: provenance.query } : {}), ...(Number.isSafeInteger(provenance.rank) && provenance.rank > 0 ? { rank: provenance.rank } : {}) }, assessment: { relevance: assessment.relevance, confidence: assessment.confidence, warnings: [...new Set(assessment.warnings)].slice(0, 8) } }
|
|
25
|
+
if (locator.level === 'exact') {
|
|
26
|
+
if (![locator.charStart, locator.charEnd, locator.lineStart, locator.lineEnd].every(Number.isSafeInteger) || locator.charStart < 0 || locator.charEnd <= locator.charStart || locator.lineStart < 1 || locator.lineEnd < locator.lineStart || !safeText(locator.anchorBefore || '', 96) || !safeText(locator.anchorAfter || '', 96) || !safeText(raw.excerpt || '', 600) || !/^[a-f0-9]{64}$/i.test(raw.excerptSha256 || '') || excerptDigest(raw.excerpt) !== raw.excerptSha256) return null
|
|
27
|
+
citation.locator = { ...citation.locator, charStart: locator.charStart, charEnd: locator.charEnd, lineStart: locator.lineStart, lineEnd: locator.lineEnd, anchorBefore: locator.anchorBefore, anchorAfter: locator.anchorAfter }
|
|
28
|
+
citation.excerpt = raw.excerpt
|
|
29
|
+
citation.excerptSha256 = raw.excerptSha256
|
|
30
|
+
}
|
|
31
|
+
return citation
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseEvidenceEnvelope(toolName, content) {
|
|
35
|
+
if (!isProjectEvidenceTool(toolName)) return null
|
|
36
|
+
const text = textContent(content)
|
|
37
|
+
const start = text.indexOf(EVIDENCE_START), end = text.indexOf(EVIDENCE_END)
|
|
38
|
+
if (start < 0 || end < start || text.indexOf(EVIDENCE_START, start + EVIDENCE_START.length) >= 0 || text.indexOf(EVIDENCE_END, end + EVIDENCE_END.length) >= 0) return null
|
|
39
|
+
let raw
|
|
40
|
+
try { raw = JSON.parse(text.slice(start + EVIDENCE_START.length, end)) } catch { return null }
|
|
41
|
+
if (!raw || raw.v !== 1 || !Array.isArray(raw.citations) || raw.citations.length > EVIDENCE_CAP) return null
|
|
42
|
+
const citations = raw.citations.map(validCitation)
|
|
43
|
+
if (citations.some((citation) => !citation)) return null
|
|
44
|
+
const codes = Array.isArray(raw.summary?.warningCodes) ? raw.summary.warningCodes.filter((code) => WARNINGS.has(code)).slice(0, 8) : []
|
|
45
|
+
const status = ['evidence_found', 'weak_evidence', 'no_evidence'].includes(raw.summary?.status) ? raw.summary.status : null
|
|
46
|
+
if (!status || !Array.isArray(raw.summary?.searchedCollections) || raw.summary.searchedCollections.some((collection) => collection !== 'repo')) return null
|
|
47
|
+
return { v: 1, citations, summary: { searchedCollections: ['repo'], directCount: citations.filter((citation) => ['exact', 'section'].includes(citation.locator.level)).length, warningCodes: [...new Set(codes)], status } }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function evidenceForToolResult(toolName, content) { return parseEvidenceEnvelope(toolName, content) || undefined }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Browser-safe evidence bounding shared by the bridge and replay client.
|
|
2
|
+
// Parsing and digest verification stay in evidence-citations.mjs (Node-only).
|
|
3
|
+
export const EVIDENCE_CAP = 8
|
|
4
|
+
|
|
5
|
+
export function compactEvidenceEnvelope(envelope, maxExcerpt = 600) {
|
|
6
|
+
if (!envelope?.citations?.length) return envelope
|
|
7
|
+
return { ...envelope, citations: envelope.citations.slice(0, EVIDENCE_CAP).map((citation) => {
|
|
8
|
+
if (typeof citation.excerpt !== 'string' || citation.excerpt.length <= maxExcerpt) return citation
|
|
9
|
+
return { ...citation, excerpt: undefined, excerptSha256: undefined, locator: { ...citation.locator, level: 'unavailable' }, assessment: { ...citation.assessment, confidence: 'unavailable', warnings: [...new Set([...citation.assessment.warnings, 'TRANSCRIPT_BOUNDED', 'SOURCE_UNAVAILABLE'])].slice(0, 8) } }
|
|
10
|
+
}) }
|
|
11
|
+
}
|
package/flow-preview.mjs
CHANGED
|
@@ -82,6 +82,10 @@ function makeHandler (dir) {
|
|
|
82
82
|
return async (req, res) => {
|
|
83
83
|
const resolved = resolveUnderRoot(dir, req.url || '/')
|
|
84
84
|
if (resolved === null) return send(res, 403, 'text/plain; charset=utf-8', 'Forbidden')
|
|
85
|
+
const relative = path.relative(dir, resolved)
|
|
86
|
+
if (relative.split(path.sep)[0] === '.thinkpool-design') {
|
|
87
|
+
return send(res, 404, 'text/plain; charset=utf-8', 'Not found')
|
|
88
|
+
}
|
|
85
89
|
|
|
86
90
|
// A concrete file hit (and still a real file, not a dir) → serve it.
|
|
87
91
|
if (await isFile(resolved)) {
|
package/hermes-event-mapper.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { evidenceForToolResult } from './evidence-citations.mjs'
|
|
2
|
+
|
|
1
3
|
const textOf = (content) => {
|
|
2
4
|
if (typeof content === 'string') return content
|
|
3
5
|
if (content?.type === 'text') return String(content.text || '')
|
|
@@ -31,6 +33,9 @@ export function hermesToolFor(update = {}) {
|
|
|
31
33
|
const kind = String(update.kind || '').toLowerCase()
|
|
32
34
|
const title = String(update.title || '')
|
|
33
35
|
const raw = update.rawInput && typeof update.rawInput === 'object' ? update.rawInput : {}
|
|
36
|
+
const server = String(update.server || raw.server || '')
|
|
37
|
+
const tool = String(update.tool || raw.tool || raw.name || '')
|
|
38
|
+
if (server === 'project-context' && tool === 'search_context_evidence') return { name: 'mcp__project-context__search_context_evidence', input: raw }
|
|
34
39
|
const terminal = (update.content || []).find((part) => part?.type === 'terminal')
|
|
35
40
|
const shellText = (update.content || []).map(textOf).find((text) => text.trim().startsWith('$ '))
|
|
36
41
|
const location = update.locations?.[0]?.path
|
|
@@ -94,9 +99,11 @@ export class HermesEventMapper {
|
|
|
94
99
|
this.tools.set(update.toolCallId, merged)
|
|
95
100
|
if (!['completed', 'failed'].includes(update.status)) return
|
|
96
101
|
const completedTool = hermesToolFor(merged)
|
|
102
|
+
const content = [{ type: 'text', text: outputText(merged) }]
|
|
103
|
+
const evidence = evidenceForToolResult(completedTool.name, content)
|
|
97
104
|
this._emit({
|
|
98
105
|
kind: 'tool_result', toolUseId: update.toolCallId,
|
|
99
|
-
content
|
|
106
|
+
content, ...(evidence ? { evidence } : {}),
|
|
100
107
|
toolInput: { ...(prior.input || {}), ...(completedTool.input || {}) },
|
|
101
108
|
isError: update.status === 'failed',
|
|
102
109
|
durationMs: prior.startedAt ? Date.now() - prior.startedAt : undefined,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// A continuation is a durable-safe control projection, never a provider
|
|
2
|
+
// transcript or a command to start a new native turn.
|
|
3
|
+
|
|
4
|
+
import { isSecretFreeRoomPayload, runtimeSupportsCapability } from './runtime-contract.mjs'
|
|
5
|
+
|
|
6
|
+
const STATES = new Set(['waiting_for_human', 'resumable', 'canceled', 'terminal'])
|
|
7
|
+
const RESUME_KINDS = new Set(['human_response', 'approval', 'redispatch', 'reconnect'])
|
|
8
|
+
const EXECUTION = new Set(['Working', 'Done', 'Failed', 'Canceled'])
|
|
9
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/
|
|
10
|
+
const dispatchers = new WeakSet()
|
|
11
|
+
|
|
12
|
+
const canonical = (value) => {
|
|
13
|
+
if (Array.isArray(value)) return value.map(canonical)
|
|
14
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]))
|
|
15
|
+
return value
|
|
16
|
+
}
|
|
17
|
+
const same = (a, b) => JSON.stringify(canonical(a)) === JSON.stringify(canonical(b))
|
|
18
|
+
const safeId = (value) => typeof value === 'string' && SAFE_ID.test(value)
|
|
19
|
+
const denied = (code) => Object.freeze({ ok: false, code })
|
|
20
|
+
const clone = (value) => {
|
|
21
|
+
const freeze = (node) => {
|
|
22
|
+
if (node && typeof node === 'object') {
|
|
23
|
+
for (const child of Object.values(node)) freeze(child)
|
|
24
|
+
Object.freeze(node)
|
|
25
|
+
}
|
|
26
|
+
return node
|
|
27
|
+
}
|
|
28
|
+
return freeze(canonical(value))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isSafeContinuationProjection (record) {
|
|
32
|
+
if (!record || typeof record !== 'object' || Array.isArray(record) || record.version !== 1 || !safeId(record.id) || !safeId(record.laneRef) || !safeId(record.roomCode) || !safeId(record.authorityRef)) return false
|
|
33
|
+
if (!runtimeSupportsCapability(record.runtime, record.capabilityId) || !STATES.has(record.state) || record.verification !== 'unverified') return false
|
|
34
|
+
if (record.resumeKind != null && !RESUME_KINDS.has(record.resumeKind)) return false
|
|
35
|
+
if (record.resumeRef != null && (!record.resumeRef || typeof record.resumeRef !== 'object' || !safeId(record.resumeRef.type) || !safeId(record.resumeRef.id))) return false
|
|
36
|
+
if (!['canceled', 'terminal'].includes(record.state) && (!record.resumeKind || !record.resumeRef)) return false
|
|
37
|
+
if (!record.executionBoundary || !EXECUTION.has(record.executionBoundary.state) || !record.executionBoundary.eventRef || !isSecretFreeRoomPayload(record.executionBoundary.eventRef, 2048)) return false
|
|
38
|
+
if (record.expiresAt != null && (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= 0)) return false
|
|
39
|
+
return isSecretFreeRoomPayload(record, 8192)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createContinuationDispatcher ({ roomCode, authorityRef, now = () => Date.now() } = {}) {
|
|
43
|
+
if (!safeId(roomCode) || !safeId(authorityRef) || typeof now !== 'function') throw new TypeError('Invalid continuation dispatcher authority')
|
|
44
|
+
const records = new Map()
|
|
45
|
+
const dispatcher = {
|
|
46
|
+
create (record) {
|
|
47
|
+
const next = { ...record, version: 1, roomCode, authorityRef, verification: 'unverified' }
|
|
48
|
+
if (!isSafeContinuationProjection(next)) return denied('invalid_continuation')
|
|
49
|
+
const prior = records.get(next.id)
|
|
50
|
+
if (prior) return same(prior, next) ? Object.freeze({ ok: true, code: 'idempotent', record: prior }) : denied('continuation_conflict')
|
|
51
|
+
const frozen = clone(next)
|
|
52
|
+
records.set(frozen.id, frozen)
|
|
53
|
+
return Object.freeze({ ok: true, code: 'created', record: frozen })
|
|
54
|
+
},
|
|
55
|
+
consume ({ id, roomCode: consumeRoom, authorityRef: consumeAuthority, control } = {}) {
|
|
56
|
+
const record = records.get(id)
|
|
57
|
+
if (!record) return denied('unknown_continuation')
|
|
58
|
+
if (consumeRoom !== roomCode || consumeAuthority !== authorityRef) return denied('continuation_authority_mismatch')
|
|
59
|
+
if (record.state === 'canceled') return denied('continuation_canceled')
|
|
60
|
+
if (record.state === 'terminal') return denied('continuation_terminal')
|
|
61
|
+
if (record.expiresAt != null && now() > record.expiresAt) return denied('continuation_stale')
|
|
62
|
+
if (!control || control.authorized !== true || control.roomCode !== roomCode || control.authorityRef !== authorityRef || !same(control.resumeRef, record.resumeRef)) return denied('continuation_control_unverified')
|
|
63
|
+
// The adapter must explicitly act on this intent. Returning it is not an
|
|
64
|
+
// auto-resume and cannot fabricate an execution or verification result.
|
|
65
|
+
return Object.freeze({ ok: true, code: 'resume_intent', record, resumeIntent: Object.freeze({ runtime: record.runtime, laneRef: record.laneRef, resumeKind: record.resumeKind, resumeRef: record.resumeRef }) })
|
|
66
|
+
},
|
|
67
|
+
cancel ({ id, roomCode: cancelRoom, authorityRef: cancelAuthority, reason = 'interrupt' } = {}) {
|
|
68
|
+
const record = records.get(id)
|
|
69
|
+
if (!record) return denied('unknown_continuation')
|
|
70
|
+
if (cancelRoom !== roomCode || cancelAuthority !== authorityRef) return denied('continuation_authority_mismatch')
|
|
71
|
+
if (record.state === 'canceled') return Object.freeze({ ok: true, code: 'idempotent_cancel', record })
|
|
72
|
+
// Do not preserve an arbitrary reason string: it might be provider prose.
|
|
73
|
+
// The canceled execution boundary is the safe observable fact.
|
|
74
|
+
const canceled = clone({ ...record, state: 'canceled', resumeKind: null, resumeRef: null, executionBoundary: { ...record.executionBoundary, state: 'Canceled' }, verification: 'unverified' })
|
|
75
|
+
records.set(id, canceled)
|
|
76
|
+
return Object.freeze({ ok: true, code: 'canceled', record: canceled })
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
dispatchers.add(dispatcher)
|
|
80
|
+
return Object.freeze(dispatcher)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const isBridgeContinuationDispatcher = (value) => dispatchers.has(value)
|
package/lane-lifecycle.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { classifyCodeEvent } from './code-event-contract.mjs'
|
|
2
|
+
import { isSafeContinuationProjection } from './lane-continuation.mjs'
|
|
2
3
|
|
|
3
4
|
// This file ships in the standalone thinkpool-pair tarball. Keep the canonical
|
|
4
5
|
// vocabulary here rather than importing from ../src (which is not packaged).
|
|
@@ -84,7 +85,7 @@ const base = (state, reason = null, extra = {}) => ({ state, reason, phase: null
|
|
|
84
85
|
export function projectLaneLifecycle ({
|
|
85
86
|
events = [], busy = false, pendingCount = 0, pendingReason = null,
|
|
86
87
|
stalled = false, offline = false, stale = false, failed = false, canceled = false,
|
|
87
|
-
rawStatus = null,
|
|
88
|
+
rawStatus = null, continuation = null,
|
|
88
89
|
} = {}) {
|
|
89
90
|
let out = base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.NEEDS_INPUT, { rawStatus })
|
|
90
91
|
const replay = normalizeReplayEvents(events)
|
|
@@ -120,6 +121,11 @@ export function projectLaneLifecycle ({
|
|
|
120
121
|
else if (failed) out = base(LIFECYCLE_STATE.FAILED, null, { rawStatus })
|
|
121
122
|
else if (canceled) out = base(LIFECYCLE_STATE.CANCELED, null, { rawStatus })
|
|
122
123
|
else if (pendingCount > 0) out = base(LIFECYCLE_STATE.BLOCKED, pendingReason || BLOCKED_REASON.NEEDS_DECISION, { rawStatus })
|
|
124
|
+
// This is explanation only. A continuation cannot certify verification or
|
|
125
|
+
// resurrect an interrupted/canceled execution boundary.
|
|
126
|
+
else if (isSafeContinuationProjection(continuation) && !TERMINAL.has(out.state) && continuation.state !== 'terminal') {
|
|
127
|
+
out = base(LIFECYCLE_STATE.BLOCKED, continuation.state === 'waiting_for_human' ? BLOCKED_REASON.NEEDS_INPUT : BLOCKED_REASON.NEEDS_DECISION, { rawStatus, phase: 'continuation' })
|
|
128
|
+
}
|
|
123
129
|
else if (stalled) out = base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.STALLED, { rawStatus, phase: 'stalled' })
|
|
124
130
|
else if (busy && !TERMINAL.has(out.state)) out = base(LIFECYCLE_STATE.WORKING, null, { rawStatus, phase: out.phase || 'active' })
|
|
125
131
|
else if (offline && !TERMINAL.has(out.state)) out = base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.OFFLINE, { rawStatus })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.357",
|
|
4
4
|
"description": "Connect Claude Code, Codex, or Hermes on your computer to a Thinkpool Code room.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"privacy-report.mjs",
|
|
21
21
|
"agent-visibility.mjs",
|
|
22
22
|
"byok-detect.mjs",
|
|
23
|
+
"context-contract.mjs",
|
|
23
24
|
"context-windows.mjs",
|
|
24
25
|
"claude-session.mjs",
|
|
25
26
|
"terminal-name.mjs",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"hermes-isolation.mjs",
|
|
49
50
|
"hermes-delegation-guard.mjs",
|
|
50
51
|
"runtime-registry.mjs",
|
|
52
|
+
"runtime-contract.mjs",
|
|
51
53
|
"command-catalog.mjs",
|
|
52
54
|
"repo-search.mjs",
|
|
53
55
|
"git-diff-report.mjs",
|
|
@@ -57,6 +59,9 @@
|
|
|
57
59
|
"agent-notify.mjs",
|
|
58
60
|
"agent-detect.mjs",
|
|
59
61
|
"code-event-contract.mjs",
|
|
62
|
+
"evidence-compact.mjs",
|
|
63
|
+
"evidence-citations.mjs",
|
|
64
|
+
"error-recovery.mjs",
|
|
60
65
|
"pair-control-authority.mjs",
|
|
61
66
|
"event-id.mjs",
|
|
62
67
|
"event-bounds.mjs",
|
|
@@ -71,6 +76,7 @@
|
|
|
71
76
|
"pair-bus.mjs",
|
|
72
77
|
"direct-pair-room.mjs",
|
|
73
78
|
"lane-lifecycle.mjs",
|
|
79
|
+
"lane-continuation.mjs",
|
|
74
80
|
"interrupted-resume.mjs",
|
|
75
81
|
"dispatch-lease.mjs",
|
|
76
82
|
"dispatch-permission-cleanup.mjs",
|
|
@@ -83,6 +89,7 @@
|
|
|
83
89
|
"mockup-delivery.mjs",
|
|
84
90
|
"viewport.mjs",
|
|
85
91
|
"design-edit.mjs",
|
|
92
|
+
"design-source-contract.mjs",
|
|
86
93
|
"flow-review.mjs",
|
|
87
94
|
"review-check.mjs",
|
|
88
95
|
"flow-review-gate.mjs",
|
|
@@ -106,6 +113,7 @@
|
|
|
106
113
|
"supabase-key.mjs",
|
|
107
114
|
"provider.mjs",
|
|
108
115
|
"providers.mjs",
|
|
116
|
+
"provider-resilience.mjs",
|
|
109
117
|
"model-prices.mjs",
|
|
110
118
|
"README.md"
|
|
111
119
|
],
|