thinkpool-pair 0.7.364 → 0.7.366
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/LICENSE +18 -0
- package/README.md +20 -0
- package/abort-turn-barrier.mjs +1 -23
- package/account.mjs +1 -1437
- package/acp-client.mjs +1 -140
- package/agent-detect.mjs +1 -28
- package/agent-notify.mjs +1 -142
- package/agent-visibility.mjs +1 -67
- package/auth-store.mjs +1 -120
- package/bridge.mjs +1 -6266
- package/byok-detect.mjs +1 -126
- package/claude-command-catalog.mjs +1 -91
- package/claude-session.mjs +1 -1519
- package/code-event-contract.mjs +1 -118
- package/codex-app-server.mjs +1 -340
- package/codex-commands.mjs +1 -83
- package/codex-event-mapper.mjs +1 -232
- package/codex-images.mjs +1 -69
- package/codex-mcp-http.mjs +1 -131
- package/codex-session.mjs +1 -1295
- package/command-catalog.mjs +1 -118
- package/command-guidance.mjs +1 -8
- package/context-contract.mjs +1 -95
- package/context-windows.mjs +1 -107
- package/cross-terminal.mjs +1 -789
- package/cumulative-event-relay.mjs +1 -53
- package/design-edit.mjs +1 -424
- package/design-source-contract.mjs +1 -4
- package/direct-pair-room.mjs +1 -57
- package/dispatch-lease.mjs +1 -37
- package/dispatch-permission-cleanup.mjs +1 -86
- package/edit-diff.mjs +1 -136
- package/error-recovery.mjs +1 -50
- package/event-bounds.mjs +1 -121
- package/event-delivery-queue.mjs +1 -60
- package/event-id.mjs +1 -549
- package/evidence-citations.mjs +1 -50
- package/evidence-compact.mjs +1 -11
- package/flow-assembly.mjs +1 -196
- package/flow-budget.mjs +1 -84
- package/flow-conductor.mjs +1 -259
- package/flow-context-store.mjs +1 -387
- package/flow-host-revert.mjs +1 -42
- package/flow-models.mjs +1 -139
- package/flow-preview.mjs +1 -148
- package/flow-receipt.mjs +1 -122
- package/flow-redispatch.mjs +1 -72
- package/flow-review-gate.mjs +1 -402
- package/flow-review-reflect.mjs +1 -115
- package/flow-review.mjs +1 -152
- package/flow-scope-evidence.mjs +1 -117
- package/flow-skill-registry.mjs +1 -140
- package/flow-task-graph.mjs +1 -562
- package/flow-worktree.mjs +1 -71
- package/git-diff-report.mjs +1 -121
- package/hermes-delegation-guard.mjs +1 -14
- package/hermes-event-mapper.mjs +1 -194
- package/hermes-isolation.mjs +1 -61
- package/hermes-model-cache.mjs +1 -54
- package/hermes-policy.mjs +1 -92
- package/hermes-probe.mjs +1 -57
- package/hermes-session.mjs +1 -683
- package/hermes-setup.mjs +1 -167
- package/host-memory.mjs +1 -116
- package/interrupted-resume.mjs +1 -95
- package/keep-awake.mjs +1 -148
- package/key-shape.mjs +1 -49
- package/lane-continuation.mjs +1 -83
- package/lane-lifecycle.mjs +1 -189
- package/lane-worktree.mjs +1 -77
- package/launcher.mjs +1 -354
- package/mcp-flight-recorder.mjs +1 -79
- package/mockup-delivery.mjs +1 -57
- package/model-prices.mjs +1 -113
- package/package.json +13 -4
- package/pair-bus.mjs +1 -98
- package/pair-control-authority.mjs +1 -89
- package/past-work-search.mjs +1 -105
- package/plan-meters.mjs +1 -144
- package/presence.mjs +1 -191
- package/privacy-report.mjs +1 -108
- package/provider-resilience.mjs +1 -356
- package/provider.mjs +1 -133
- package/providers.mjs +1 -491
- package/publish-guard.mjs +2 -0
- package/publish-manifest.json +129 -0
- package/question-response.mjs +1 -58
- package/reap-terminal.mjs +1 -68
- package/recap.mjs +1 -297
- package/replay-transport.mjs +1 -64
- package/repo-search.mjs +1 -190
- package/review-check.mjs +1 -182
- package/runtime-contract.mjs +1 -93
- package/runtime-registry.mjs +1 -64
- package/runtime-session.mjs +1 -20
- package/scheduled-run-admission.mjs +1 -364
- package/scheduled-runs.mjs +1 -268
- package/sdk-admission.mjs +1 -9
- package/sdk-smoke.mjs +1 -61
- package/serve-consent.mjs +1 -118
- package/serve-dir.mjs +1 -40
- package/service.mjs +1 -877
- package/session-store.mjs +1 -484
- package/side-lane.mjs +1 -63
- package/supabase-key.mjs +1 -176
- package/supervisor-ready.mjs +1 -57
- package/switch-provider.mjs +1 -84
- package/terminal-name.mjs +1 -359
- package/terminal-row-reconcile.mjs +1 -70
- package/thinkpool-prompt-contracts.mjs +1 -85
- package/thinkpool-room-prompt.mjs +1 -187
- package/transcript-sanitize.mjs +1 -332
- package/turn-stall.mjs +1 -61
- package/update-gate.mjs +1 -53
- package/viewport.mjs +1 -810
- package/worker-completion.mjs +1 -57
package/lane-continuation.mjs
CHANGED
|
@@ -1,83 +1 @@
|
|
|
1
|
-
|
|
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)
|
|
1
|
+
import{isSecretFreeRoomPayload as e,runtimeSupportsCapability as t}from"./runtime-contract.mjs";const n=new Set(["waiting_for_human","resumable","canceled","terminal"]),r=new Set(["human_response","approval","redispatch","reconnect"]),o=new Set(["Working","Done","Failed","Canceled"]),i=/^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/,a=new WeakSet,c=e=>Array.isArray(e)?e.map(c):e&&"object"==typeof e?Object.fromEntries(Object.keys(e).sort().map(t=>[t,c(e[t])])):e,u=(e,t)=>JSON.stringify(c(e))===JSON.stringify(c(t)),s=e=>"string"==typeof e&&i.test(e),f=e=>Object.freeze({ok:!1,code:e}),d=e=>{const t=e=>{if(e&&"object"==typeof e){for(const n of Object.values(e))t(n);Object.freeze(e)}return e};return t(c(e))};export function isSafeContinuationProjection(i){return!!(i&&"object"==typeof i&&!Array.isArray(i)&&1===i.version&&s(i.id)&&s(i.laneRef)&&s(i.roomCode)&&s(i.authorityRef))&&!(!t(i.runtime,i.capabilityId)||!n.has(i.state)||"unverified"!==i.verification)&&!(null!=i.resumeKind&&!r.has(i.resumeKind))&&!!(null==i.resumeRef||i.resumeRef&&"object"==typeof i.resumeRef&&s(i.resumeRef.type)&&s(i.resumeRef.id))&&!!(["canceled","terminal"].includes(i.state)||i.resumeKind&&i.resumeRef)&&!!(i.executionBoundary&&o.has(i.executionBoundary.state)&&i.executionBoundary.eventRef&&e(i.executionBoundary.eventRef,2048))&&!(null!=i.expiresAt&&(!Number.isSafeInteger(i.expiresAt)||i.expiresAt<=0))&&e(i,8192)}export function createContinuationDispatcher({roomCode:e,authorityRef:t,now:n=()=>Date.now()}={}){if(!s(e)||!s(t)||"function"!=typeof n)throw new TypeError("Invalid continuation dispatcher authority");const r=new Map,o={create(n){const o={...n,version:1,roomCode:e,authorityRef:t,verification:"unverified"};if(!isSafeContinuationProjection(o))return f("invalid_continuation");const i=r.get(o.id);if(i)return u(i,o)?Object.freeze({ok:!0,code:"idempotent",record:i}):f("continuation_conflict");const a=d(o);return r.set(a.id,a),Object.freeze({ok:!0,code:"created",record:a})},consume({id:o,roomCode:i,authorityRef:a,control:c}={}){const s=r.get(o);return s?i!==e||a!==t?f("continuation_authority_mismatch"):"canceled"===s.state?f("continuation_canceled"):"terminal"===s.state?f("continuation_terminal"):null!=s.expiresAt&&n()>s.expiresAt?f("continuation_stale"):c&&!0===c.authorized&&c.roomCode===e&&c.authorityRef===t&&u(c.resumeRef,s.resumeRef)?Object.freeze({ok:!0,code:"resume_intent",record:s,resumeIntent:Object.freeze({runtime:s.runtime,laneRef:s.laneRef,resumeKind:s.resumeKind,resumeRef:s.resumeRef})}):f("continuation_control_unverified"):f("unknown_continuation")},cancel({id:n,roomCode:o,authorityRef:i,reason:a="interrupt"}={}){const c=r.get(n);if(!c)return f("unknown_continuation");if(o!==e||i!==t)return f("continuation_authority_mismatch");if("canceled"===c.state)return Object.freeze({ok:!0,code:"idempotent_cancel",record:c});const u=d({...c,state:"canceled",resumeKind:null,resumeRef:null,executionBoundary:{...c.executionBoundary,state:"Canceled"},verification:"unverified"});return r.set(n,u),Object.freeze({ok:!0,code:"canceled",record:u})}};return a.add(o),Object.freeze(o)}export const isBridgeContinuationDispatcher=e=>a.has(e);
|
package/lane-lifecycle.mjs
CHANGED
|
@@ -1,189 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { isSafeContinuationProjection } from './lane-continuation.mjs'
|
|
3
|
-
|
|
4
|
-
// This file ships in the standalone thinkpool-pair tarball. Keep the canonical
|
|
5
|
-
// vocabulary here rather than importing from ../src (which is not packaged).
|
|
6
|
-
// The browser-side module re-exports these constants from this file.
|
|
7
|
-
export const LIFECYCLE_STATE = Object.freeze({
|
|
8
|
-
WORKING: 'Working', BLOCKED: 'Blocked', FAILED: 'Failed', DONE: 'Done', CANCELED: 'Canceled',
|
|
9
|
-
})
|
|
10
|
-
export const BLOCKED_REASON = Object.freeze({
|
|
11
|
-
NEEDS_DECISION: 'needs_decision', NEEDS_INPUT: 'needs_input', DEPENDENCY: 'dependency',
|
|
12
|
-
OFFLINE: 'offline', STALLED: 'stalled', UNKNOWN_GAP: 'unknown_gap',
|
|
13
|
-
VERIFICATION_REQUIRED: 'verification_required',
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
const TERMINAL = new Set([LIFECYCLE_STATE.FAILED, LIFECYCLE_STATE.DONE, LIFECYCLE_STATE.CANCELED])
|
|
17
|
-
const RESULT_CANCELED = new Set(['aborted', 'cancelled', 'canceled', 'interrupted'])
|
|
18
|
-
|
|
19
|
-
export function canTransitionLifecycle (from, to, { beginsNewWork = false } = {}) {
|
|
20
|
-
if (!from || from === to) return true
|
|
21
|
-
if (TERMINAL.has(from)) return beginsNewWork && to === LIFECYCLE_STATE.WORKING
|
|
22
|
-
return from === LIFECYCLE_STATE.WORKING || from === LIFECYCLE_STATE.BLOCKED
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function transitionLifecycle (current, next, opts = {}) {
|
|
26
|
-
if (!canTransitionLifecycle(current?.state, next?.state, opts)) {
|
|
27
|
-
if (opts.strict !== false) throw new TypeError(`Illegal lifecycle transition: ${current?.state} -> ${next?.state}`)
|
|
28
|
-
return current
|
|
29
|
-
}
|
|
30
|
-
return { ...next, rawStatus: next.rawStatus ?? current?.rawStatus ?? null }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// JSON.stringify's array replacer applies at every nesting level. The previous
|
|
34
|
-
// implementation therefore discarded nested assistant blocks/payload fields and
|
|
35
|
-
// could treat two conflicting events as identical. Sort every object recursively
|
|
36
|
-
// so replay identity compares the complete event, not a top-level silhouette.
|
|
37
|
-
const stableEventShape = (event) => JSON.stringify(event, (_key, value) => {
|
|
38
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return value
|
|
39
|
-
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, value[key]]))
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
// Replay identity is two-dimensional: cid identifies the optimistic/live event and
|
|
43
|
-
// seq identifies its durable position. A conflict in either dimension is evidence
|
|
44
|
-
// of a split/restarted stream, never a reason to choose whichever array order arrived.
|
|
45
|
-
const normalizeReplayEvents = (events) => {
|
|
46
|
-
const input = Array.isArray(events) ? events : []
|
|
47
|
-
const allSequenced = input.every((event) => Number.isFinite(event?.seq))
|
|
48
|
-
const ordered = input
|
|
49
|
-
.map((event, sourceIndex) => ({ event, shape: stableEventShape(event), sourceIndex }))
|
|
50
|
-
.sort((a, b) => {
|
|
51
|
-
// Old/current optimistic transcript events can legitimately have no seq.
|
|
52
|
-
// Their array position is the only boundary order; lexical kind sorting
|
|
53
|
-
// can move a `you` event after its aborted `result` and resurrect a turn.
|
|
54
|
-
if (!allSequenced) return a.sourceIndex - b.sourceIndex
|
|
55
|
-
if (a.event.seq !== b.event.seq) return a.event.seq - b.event.seq
|
|
56
|
-
const at = Number(a.event?.ts) || 0, bt = Number(b.event?.ts) || 0
|
|
57
|
-
if (at !== bt) return at - bt
|
|
58
|
-
return String(a.event?.cid || '').localeCompare(String(b.event?.cid || '')) || a.shape.localeCompare(b.shape)
|
|
59
|
-
})
|
|
60
|
-
const cids = new Map()
|
|
61
|
-
const seqs = new Map()
|
|
62
|
-
const out = []
|
|
63
|
-
const conflicts = []
|
|
64
|
-
for (const item of ordered) {
|
|
65
|
-
const { event, shape } = item
|
|
66
|
-
const cid = typeof event?.cid === 'string' && event.cid ? event.cid : null
|
|
67
|
-
const seq = Number.isFinite(event?.seq) ? event.seq : null
|
|
68
|
-
const cidPrior = cid ? cids.get(cid) : null
|
|
69
|
-
const seqPrior = seq != null ? seqs.get(seq) : null
|
|
70
|
-
if (cidPrior || seqPrior) {
|
|
71
|
-
const prior = cidPrior || seqPrior
|
|
72
|
-
if (prior.shape !== shape) conflicts.push({ cid, seq, priorCid: prior.cid, priorSeq: prior.seq })
|
|
73
|
-
continue
|
|
74
|
-
}
|
|
75
|
-
const record = { cid, seq, shape }
|
|
76
|
-
if (cid) cids.set(cid, record)
|
|
77
|
-
if (seq != null) seqs.set(seq, record)
|
|
78
|
-
out.push(event)
|
|
79
|
-
}
|
|
80
|
-
return { events: out, conflicts }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const base = (state, reason = null, extra = {}) => ({ state, reason, phase: null, freshness: 'current', transport: 'online', rawStatus: null, verified: false, ...extra })
|
|
84
|
-
|
|
85
|
-
export function projectLaneLifecycle ({
|
|
86
|
-
events = [], busy = false, pendingCount = 0, pendingReason = null,
|
|
87
|
-
stalled = false, offline = false, stale = false, failed = false, canceled = false,
|
|
88
|
-
rawStatus = null, continuation = null,
|
|
89
|
-
} = {}) {
|
|
90
|
-
let out = base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.NEEDS_INPUT, { rawStatus })
|
|
91
|
-
const replay = normalizeReplayEvents(events)
|
|
92
|
-
let unknownEvent = false
|
|
93
|
-
for (const event of replay.events) {
|
|
94
|
-
const contract = classifyCodeEvent(event, { strict: false })
|
|
95
|
-
if (!contract.known) { unknownEvent = true; continue }
|
|
96
|
-
if (contract.eventClass === 'volatile-presence') continue
|
|
97
|
-
if (event.kind === 'pool' && (event.transient || event.pending)) {
|
|
98
|
-
continue
|
|
99
|
-
}
|
|
100
|
-
if (event.kind === 'you' || event.kind === 'assistant' || event.kind === 'tool_result' || event.kind === 'pool') {
|
|
101
|
-
out = base(LIFECYCLE_STATE.WORKING, null, { rawStatus, phase: event.kind })
|
|
102
|
-
} else if (event.kind === 'propose' || event.kind === 'answer') {
|
|
103
|
-
out = base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.NEEDS_DECISION, { rawStatus })
|
|
104
|
-
} else if (event.kind === 'needs-input') {
|
|
105
|
-
out = base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.NEEDS_INPUT, { rawStatus })
|
|
106
|
-
} else if (event.kind === 'error') {
|
|
107
|
-
out = base(LIFECYCLE_STATE.FAILED, null, { rawStatus })
|
|
108
|
-
} else if (event.kind === 'result') {
|
|
109
|
-
if (RESULT_CANCELED.has(event.subtype)) out = base(LIFECYCLE_STATE.CANCELED, null, { rawStatus })
|
|
110
|
-
else if (event.subtype && event.subtype !== 'success') out = base(LIFECYCLE_STATE.FAILED, null, { rawStatus })
|
|
111
|
-
// A successful result is authoritative for the execution boundary: the
|
|
112
|
-
// runtime turn finished and the lane is idle. It is not proof that the
|
|
113
|
-
// worker's findings or changed product behavior were independently
|
|
114
|
-
// verified. Keep that evidence dimension in `verified`; pair-control
|
|
115
|
-
// receipts, not transcript claims, are the only authority for upgrading it.
|
|
116
|
-
else out = base(LIFECYCLE_STATE.DONE, null, { rawStatus })
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (replay.conflicts.length || unknownEvent) out = base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.UNKNOWN_GAP, { rawStatus })
|
|
121
|
-
else if (failed) out = base(LIFECYCLE_STATE.FAILED, null, { rawStatus })
|
|
122
|
-
else if (canceled) out = base(LIFECYCLE_STATE.CANCELED, null, { rawStatus })
|
|
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
|
-
}
|
|
129
|
-
else if (stalled) out = base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.STALLED, { rawStatus, phase: 'stalled' })
|
|
130
|
-
else if (busy && !TERMINAL.has(out.state)) out = base(LIFECYCLE_STATE.WORKING, null, { rawStatus, phase: out.phase || 'active' })
|
|
131
|
-
else if (offline && !TERMINAL.has(out.state)) out = base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.OFFLINE, { rawStatus })
|
|
132
|
-
|
|
133
|
-
if (offline) out.transport = 'offline'
|
|
134
|
-
if (stale) out.freshness = 'stale'
|
|
135
|
-
return Object.freeze(out)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function projectFlowLifecycle (rawStatus) {
|
|
139
|
-
switch (rawStatus) {
|
|
140
|
-
case 'building': case 'dispatched': case 'reviewing': case 'assembling': case 'planning':
|
|
141
|
-
return base(LIFECYCLE_STATE.WORKING, null, { rawStatus, phase: rawStatus })
|
|
142
|
-
case 'awaiting_approval': case 'approved':
|
|
143
|
-
return base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.NEEDS_DECISION, { rawStatus })
|
|
144
|
-
case 'pending': case 'ready': case 'reverted':
|
|
145
|
-
return base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.DEPENDENCY, { rawStatus })
|
|
146
|
-
case 'failed': return base(LIFECYCLE_STATE.FAILED, null, { rawStatus })
|
|
147
|
-
case 'cancelled': case 'canceled': return base(LIFECYCLE_STATE.CANCELED, null, { rawStatus })
|
|
148
|
-
// `done` is the Flow execution boundary. Commits and artifacts remain
|
|
149
|
-
// unverified evidence until an authority-bound receipt observes them.
|
|
150
|
-
case 'done': return base(LIFECYCLE_STATE.DONE, null, { rawStatus })
|
|
151
|
-
default: return base(LIFECYCLE_STATE.BLOCKED, BLOCKED_REASON.NEEDS_INPUT, { rawStatus })
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function projectRecoveryWatermark ({ events = [], epoch = 0, floor = null, gap = null, offline = false, stale = false } = {}) {
|
|
156
|
-
const replay = normalizeReplayEvents(events)
|
|
157
|
-
const foreignEpoch = replay.events.some((e) => Number.isFinite(e?.epoch) && e.epoch !== epoch)
|
|
158
|
-
const seqEvents = replay.events.filter((e) => Number.isFinite(e?.seq) && (!Number.isFinite(e?.epoch) || e.epoch === epoch))
|
|
159
|
-
const last = seqEvents.reduce((best, e) => !best || e.seq > best.seq ? e : best, null)
|
|
160
|
-
let unknownGap = gap && Number.isFinite(gap.from) && Number.isFinite(gap.to)
|
|
161
|
-
? { from: gap.from, to: gap.to, reason: gap.reason || 'unknown', recoverable: gap.recoverable !== false }
|
|
162
|
-
: null
|
|
163
|
-
if (!unknownGap && (replay.conflicts.length || foreignEpoch)) {
|
|
164
|
-
const seqs = replay.conflicts.map((c) => c.seq).filter(Number.isFinite)
|
|
165
|
-
unknownGap = {
|
|
166
|
-
from: seqs.length ? Math.min(...seqs) : null,
|
|
167
|
-
to: seqs.length ? Math.max(...seqs) : null,
|
|
168
|
-
reason: replay.conflicts.length ? 'conflicting_event_identity' : 'foreign_epoch',
|
|
169
|
-
recoverable: true,
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (!unknownGap && seqEvents.length > 1) {
|
|
173
|
-
for (let i = 1; i < seqEvents.length; i++) {
|
|
174
|
-
if (seqEvents[i].seq > seqEvents[i - 1].seq + 1) {
|
|
175
|
-
unknownGap = { from: seqEvents[i - 1].seq + 1, to: seqEvents[i].seq - 1, reason: 'out_of_order_or_trimmed', recoverable: true }
|
|
176
|
-
break
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return Object.freeze({
|
|
181
|
-
epoch,
|
|
182
|
-
floor: Number.isFinite(floor) ? floor : null,
|
|
183
|
-
lastProven: last ? { cid: last.cid || null, seq: last.seq, epoch } : null,
|
|
184
|
-
unknownGap,
|
|
185
|
-
freshness: stale ? 'stale' : 'current',
|
|
186
|
-
transport: offline ? 'offline' : 'online',
|
|
187
|
-
globalRollbackAvailable: false,
|
|
188
|
-
})
|
|
189
|
-
}
|
|
1
|
+
import{classifyCodeEvent as e}from"./code-event-contract.mjs";import{isSafeContinuationProjection as t}from"./lane-continuation.mjs";export const LIFECYCLE_STATE=Object.freeze({WORKING:"Working",BLOCKED:"Blocked",FAILED:"Failed",DONE:"Done",CANCELED:"Canceled"});export const BLOCKED_REASON=Object.freeze({NEEDS_DECISION:"needs_decision",NEEDS_INPUT:"needs_input",DEPENDENCY:"dependency",OFFLINE:"offline",STALLED:"stalled",UNKNOWN_GAP:"unknown_gap",VERIFICATION_REQUIRED:"verification_required"});const n=new Set([LIFECYCLE_STATE.FAILED,LIFECYCLE_STATE.DONE,LIFECYCLE_STATE.CANCELED]),s=new Set(["aborted","cancelled","canceled","interrupted"]);export function canTransitionLifecycle(e,t,{beginsNewWork:s=!1}={}){return!e||e===t||(n.has(e)?s&&t===LIFECYCLE_STATE.WORKING:e===LIFECYCLE_STATE.WORKING||e===LIFECYCLE_STATE.BLOCKED)}export function transitionLifecycle(e,t,n={}){if(!canTransitionLifecycle(e?.state,t?.state,n)){if(!1!==n.strict)throw new TypeError(`Illegal lifecycle transition: ${e?.state} -> ${t?.state}`);return e}return{...t,rawStatus:t.rawStatus??e?.rawStatus??null}}const r=e=>JSON.stringify(e,(e,t)=>!t||"object"!=typeof t||Array.isArray(t)?t:Object.fromEntries(Object.keys(t).sort().map(e=>[e,t[e]]))),a=e=>{const t=Array.isArray(e)?e:[],n=t.every(e=>Number.isFinite(e?.seq)),s=t.map((e,t)=>({event:e,shape:r(e),sourceIndex:t})).sort((e,t)=>{if(!n)return e.sourceIndex-t.sourceIndex;if(e.event.seq!==t.event.seq)return e.event.seq-t.event.seq;const s=Number(e.event?.ts)||0,r=Number(t.event?.ts)||0;return s!==r?s-r:String(e.event?.cid||"").localeCompare(String(t.event?.cid||""))||e.shape.localeCompare(t.shape)}),a=new Map,E=new Map,l=[],i=[];for(const e of s){const{event:t,shape:n}=e,s="string"==typeof t?.cid&&t.cid?t.cid:null,r=Number.isFinite(t?.seq)?t.seq:null,o=s?a.get(s):null,c=null!=r?E.get(r):null;if(o||c){const e=o||c;e.shape!==n&&i.push({cid:s,seq:r,priorCid:e.cid,priorSeq:e.seq});continue}const u={cid:s,seq:r,shape:n};s&&a.set(s,u),null!=r&&E.set(r,u),l.push(t)}return{events:l,conflicts:i}},E=(e,t=null,n={})=>({state:e,reason:t,phase:null,freshness:"current",transport:"online",rawStatus:null,verified:!1,...n});export function projectLaneLifecycle({events:r=[],busy:l=!1,pendingCount:i=0,pendingReason:o=null,stalled:c=!1,offline:u=!1,stale:C=!1,failed:L=!1,canceled:S=!1,rawStatus:p=null,continuation:N=null}={}){let T=E(LIFECYCLE_STATE.BLOCKED,BLOCKED_REASON.NEEDS_INPUT,{rawStatus:p});const _=a(r);let f=!1;for(const t of _.events){const n=e(t,{strict:!1});n.known?"volatile-presence"!==n.eventClass&&("pool"===t.kind&&(t.transient||t.pending)||("you"===t.kind||"assistant"===t.kind||"tool_result"===t.kind||"pool"===t.kind?T=E(LIFECYCLE_STATE.WORKING,null,{rawStatus:p,phase:t.kind}):"propose"===t.kind||"answer"===t.kind?T=E(LIFECYCLE_STATE.BLOCKED,BLOCKED_REASON.NEEDS_DECISION,{rawStatus:p}):"needs-input"===t.kind?T=E(LIFECYCLE_STATE.BLOCKED,BLOCKED_REASON.NEEDS_INPUT,{rawStatus:p}):"error"===t.kind?T=E(LIFECYCLE_STATE.FAILED,null,{rawStatus:p}):"result"===t.kind&&(T=s.has(t.subtype)?E(LIFECYCLE_STATE.CANCELED,null,{rawStatus:p}):t.subtype&&"success"!==t.subtype?E(LIFECYCLE_STATE.FAILED,null,{rawStatus:p}):E(LIFECYCLE_STATE.DONE,null,{rawStatus:p})))):f=!0}return _.conflicts.length||f?T=E(LIFECYCLE_STATE.BLOCKED,BLOCKED_REASON.UNKNOWN_GAP,{rawStatus:p}):L?T=E(LIFECYCLE_STATE.FAILED,null,{rawStatus:p}):S?T=E(LIFECYCLE_STATE.CANCELED,null,{rawStatus:p}):i>0?T=E(LIFECYCLE_STATE.BLOCKED,o||BLOCKED_REASON.NEEDS_DECISION,{rawStatus:p}):t(N)&&!n.has(T.state)&&"terminal"!==N.state?T=E(LIFECYCLE_STATE.BLOCKED,"waiting_for_human"===N.state?BLOCKED_REASON.NEEDS_INPUT:BLOCKED_REASON.NEEDS_DECISION,{rawStatus:p,phase:"continuation"}):c?T=E(LIFECYCLE_STATE.BLOCKED,BLOCKED_REASON.STALLED,{rawStatus:p,phase:"stalled"}):l&&!n.has(T.state)?T=E(LIFECYCLE_STATE.WORKING,null,{rawStatus:p,phase:T.phase||"active"}):u&&!n.has(T.state)&&(T=E(LIFECYCLE_STATE.BLOCKED,BLOCKED_REASON.OFFLINE,{rawStatus:p})),u&&(T.transport="offline"),C&&(T.freshness="stale"),Object.freeze(T)}export function projectFlowLifecycle(e){switch(e){case"building":case"dispatched":case"reviewing":case"assembling":case"planning":return E(LIFECYCLE_STATE.WORKING,null,{rawStatus:e,phase:e});case"awaiting_approval":case"approved":return E(LIFECYCLE_STATE.BLOCKED,BLOCKED_REASON.NEEDS_DECISION,{rawStatus:e});case"pending":case"ready":case"reverted":return E(LIFECYCLE_STATE.BLOCKED,BLOCKED_REASON.DEPENDENCY,{rawStatus:e});case"failed":return E(LIFECYCLE_STATE.FAILED,null,{rawStatus:e});case"cancelled":case"canceled":return E(LIFECYCLE_STATE.CANCELED,null,{rawStatus:e});case"done":return E(LIFECYCLE_STATE.DONE,null,{rawStatus:e});default:return E(LIFECYCLE_STATE.BLOCKED,BLOCKED_REASON.NEEDS_INPUT,{rawStatus:e})}}export function projectRecoveryWatermark({events:e=[],epoch:t=0,floor:n=null,gap:s=null,offline:r=!1,stale:E=!1}={}){const l=a(e),i=l.events.some(e=>Number.isFinite(e?.epoch)&&e.epoch!==t),o=l.events.filter(e=>Number.isFinite(e?.seq)&&(!Number.isFinite(e?.epoch)||e.epoch===t)),c=o.reduce((e,t)=>!e||t.seq>e.seq?t:e,null);let u=s&&Number.isFinite(s.from)&&Number.isFinite(s.to)?{from:s.from,to:s.to,reason:s.reason||"unknown",recoverable:!1!==s.recoverable}:null;if(!u&&(l.conflicts.length||i)){const e=l.conflicts.map(e=>e.seq).filter(Number.isFinite);u={from:e.length?Math.min(...e):null,to:e.length?Math.max(...e):null,reason:l.conflicts.length?"conflicting_event_identity":"foreign_epoch",recoverable:!0}}if(!u&&o.length>1)for(let e=1;e<o.length;e++)if(o[e].seq>o[e-1].seq+1){u={from:o[e-1].seq+1,to:o[e].seq-1,reason:"out_of_order_or_trimmed",recoverable:!0};break}return Object.freeze({epoch:t,floor:Number.isFinite(n)?n:null,lastProven:c?{cid:c.cid||null,seq:c.seq,epoch:t}:null,unknownGap:u,freshness:E?"stale":"current",transport:r?"offline":"online",globalRollbackAvailable:!1})}
|
package/lane-worktree.mjs
CHANGED
|
@@ -1,77 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// is the persisted managedWorktree record, never a lane's arbitrary cwd.
|
|
3
|
-
import { execFileSync } from 'node:child_process'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import fs from 'node:fs'
|
|
6
|
-
|
|
7
|
-
const LOCAL_ENV_FILES = ['.env.local', '.env.development.local']
|
|
8
|
-
|
|
9
|
-
// Git deliberately omits local env files from worktrees. That is correct for
|
|
10
|
-
// version control but wrong for an isolated lane that must build/preview the
|
|
11
|
-
// same app as the main checkout: Vite otherwise compiles `undefined` public
|
|
12
|
-
// client config and the preview dies before React mounts. Link, never copy, the
|
|
13
|
-
// main checkout's existing ignored env files. Missing files are a clean no-op.
|
|
14
|
-
export function linkLocalEnvIntoWorktree({ root, dir, fsImpl = fs } = {}) {
|
|
15
|
-
const linked = []
|
|
16
|
-
for (const name of LOCAL_ENV_FILES) {
|
|
17
|
-
const source = path.join(root, name)
|
|
18
|
-
const target = path.join(dir, name)
|
|
19
|
-
try {
|
|
20
|
-
if (!fsImpl.existsSync?.(source) || fsImpl.existsSync?.(target)) continue
|
|
21
|
-
fsImpl.symlinkSync?.(source, target)
|
|
22
|
-
linked.push(name)
|
|
23
|
-
} catch { /* local env is optional; worktree creation must still succeed */ }
|
|
24
|
-
}
|
|
25
|
-
return linked
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function createManagedLaneWorktree({ terminalId, cwd = process.cwd(), git = runGit, fsImpl = fs } = {}) {
|
|
29
|
-
const id = String(terminalId || '')
|
|
30
|
-
const short = id.replace(/[^a-zA-Z0-9]/g, '').slice(0, 8)
|
|
31
|
-
if (!short) throw new Error('terminal id required for managed worktree')
|
|
32
|
-
const root = String(git(['rev-parse', '--show-toplevel'], cwd) || '').trim()
|
|
33
|
-
if (!root) throw new Error('not a git repository')
|
|
34
|
-
const dir = path.join(root, '.thinkpool', 'worktrees', `terminal-${short}`)
|
|
35
|
-
const branch = `thinkpool/terminal/${short}`
|
|
36
|
-
const base = resolveLaneBase({ root, git })
|
|
37
|
-
fsImpl.mkdirSync(path.dirname(dir), { recursive: true })
|
|
38
|
-
git(['worktree', 'add', '-b', branch, dir, base], root)
|
|
39
|
-
linkLocalEnvIntoWorktree({ root, dir, fsImpl })
|
|
40
|
-
return { terminalId: id, root, dir, branch }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Ordinary lanes should start from the repository's upstream default branch, not
|
|
44
|
-
// whichever parked/stale branch happens to be checked out in the bridge process.
|
|
45
|
-
// Repositories without a remote remain useful: fall back through conventional local
|
|
46
|
-
// defaults and finally HEAD.
|
|
47
|
-
function resolveLaneBase({ root, git }) {
|
|
48
|
-
for (const candidate of ['origin/HEAD', 'origin/main', 'origin/master', 'main', 'master', 'HEAD']) {
|
|
49
|
-
try {
|
|
50
|
-
const resolved = String(git(['rev-parse', '--verify', '--quiet', candidate], root) || '').trim()
|
|
51
|
-
if (resolved) return candidate
|
|
52
|
-
} catch { /* try the next base */ }
|
|
53
|
-
}
|
|
54
|
-
return 'HEAD'
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function removeManagedLaneWorktree({ terminalId, managedWorktree, git = runGit } = {}) {
|
|
58
|
-
const id = String(terminalId || '')
|
|
59
|
-
const rec = managedWorktree && typeof managedWorktree === 'object' ? managedWorktree : null
|
|
60
|
-
if (!id || !rec || rec.terminalId !== id) return { removed: false, reason: 'not managed by this terminal' }
|
|
61
|
-
|
|
62
|
-
const root = path.resolve(String(rec.root || ''))
|
|
63
|
-
const dir = path.resolve(String(rec.dir || ''))
|
|
64
|
-
const branch = String(rec.branch || '')
|
|
65
|
-
const ownedRoot = path.join(root, '.thinkpool', 'worktrees')
|
|
66
|
-
const short = id.replace(/[^a-zA-Z0-9]/g, '').slice(0, 8)
|
|
67
|
-
if (!root || dir === ownedRoot || !dir.startsWith(ownedRoot + path.sep)) return { removed: false, reason: 'path outside managed worktrees' }
|
|
68
|
-
if (!short || !path.basename(dir).includes(short) || !branch.startsWith(`thinkpool/terminal/${short}`)) return { removed: false, reason: 'ownership marker mismatch' }
|
|
69
|
-
|
|
70
|
-
try { git(['worktree', 'remove', dir, '--force'], root) } catch { /* already absent */ }
|
|
71
|
-
try { git(['branch', '-D', branch], root) } catch { /* already absent */ }
|
|
72
|
-
return { removed: true, dir, branch }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function runGit(args, cwd) {
|
|
76
|
-
return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' })
|
|
77
|
-
}
|
|
1
|
+
import{execFileSync as r}from"node:child_process";import e from"node:path";import t from"node:fs";const o=[".env.local",".env.development.local"];export function linkLocalEnvIntoWorktree({root:r,dir:n,fsImpl:i=t}={}){const a=[];for(const t of o){const o=e.join(r,t),c=e.join(n,t);try{if(!i.existsSync?.(o)||i.existsSync?.(c))continue;i.symlinkSync?.(o,c),a.push(t)}catch{}}return a}export function createManagedLaneWorktree({terminalId:r,cwd:o=process.cwd(),git:i=n,fsImpl:a=t}={}){const c=String(r||""),s=c.replace(/[^a-zA-Z0-9]/g,"").slice(0,8);if(!s)throw new Error("terminal id required for managed worktree");const m=String(i(["rev-parse","--show-toplevel"],o)||"").trim();if(!m)throw new Error("not a git repository");const l=e.join(m,".thinkpool","worktrees",`terminal-${s}`),d=`thinkpool/terminal/${s}`,p=function({root:r,git:e}){for(const t of["origin/HEAD","origin/main","origin/master","main","master","HEAD"])try{if(String(e(["rev-parse","--verify","--quiet",t],r)||"").trim())return t}catch{}return"HEAD"}({root:m,git:i});return a.mkdirSync(e.dirname(l),{recursive:!0}),i(["worktree","add","-b",d,l,p],m),linkLocalEnvIntoWorktree({root:m,dir:l,fsImpl:a}),{terminalId:c,root:m,dir:l,branch:d}}export function removeManagedLaneWorktree({terminalId:r,managedWorktree:t,git:o=n}={}){const i=String(r||""),a=t&&"object"==typeof t?t:null;if(!i||!a||a.terminalId!==i)return{removed:!1,reason:"not managed by this terminal"};const c=e.resolve(String(a.root||"")),s=e.resolve(String(a.dir||"")),m=String(a.branch||""),l=e.join(c,".thinkpool","worktrees"),d=i.replace(/[^a-zA-Z0-9]/g,"").slice(0,8);if(!c||s===l||!s.startsWith(l+e.sep))return{removed:!1,reason:"path outside managed worktrees"};if(!d||!e.basename(s).includes(d)||!m.startsWith(`thinkpool/terminal/${d}`))return{removed:!1,reason:"ownership marker mismatch"};try{o(["worktree","remove",s,"--force"],c)}catch{}try{o(["branch","-D",m],c)}catch{}return{removed:!0,dir:s,branch:m}}function n(e,t){return r("git",e,{cwd:t,stdio:["ignore","pipe","pipe"],encoding:"utf8"})}
|