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.
Files changed (116) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +20 -0
  3. package/abort-turn-barrier.mjs +1 -23
  4. package/account.mjs +1 -1437
  5. package/acp-client.mjs +1 -140
  6. package/agent-detect.mjs +1 -28
  7. package/agent-notify.mjs +1 -142
  8. package/agent-visibility.mjs +1 -67
  9. package/auth-store.mjs +1 -120
  10. package/bridge.mjs +1 -6266
  11. package/byok-detect.mjs +1 -126
  12. package/claude-command-catalog.mjs +1 -91
  13. package/claude-session.mjs +1 -1519
  14. package/code-event-contract.mjs +1 -118
  15. package/codex-app-server.mjs +1 -340
  16. package/codex-commands.mjs +1 -83
  17. package/codex-event-mapper.mjs +1 -232
  18. package/codex-images.mjs +1 -69
  19. package/codex-mcp-http.mjs +1 -131
  20. package/codex-session.mjs +1 -1295
  21. package/command-catalog.mjs +1 -118
  22. package/command-guidance.mjs +1 -8
  23. package/context-contract.mjs +1 -95
  24. package/context-windows.mjs +1 -107
  25. package/cross-terminal.mjs +1 -789
  26. package/cumulative-event-relay.mjs +1 -53
  27. package/design-edit.mjs +1 -424
  28. package/design-source-contract.mjs +1 -4
  29. package/direct-pair-room.mjs +1 -57
  30. package/dispatch-lease.mjs +1 -37
  31. package/dispatch-permission-cleanup.mjs +1 -86
  32. package/edit-diff.mjs +1 -136
  33. package/error-recovery.mjs +1 -50
  34. package/event-bounds.mjs +1 -121
  35. package/event-delivery-queue.mjs +1 -60
  36. package/event-id.mjs +1 -549
  37. package/evidence-citations.mjs +1 -50
  38. package/evidence-compact.mjs +1 -11
  39. package/flow-assembly.mjs +1 -196
  40. package/flow-budget.mjs +1 -84
  41. package/flow-conductor.mjs +1 -259
  42. package/flow-context-store.mjs +1 -387
  43. package/flow-host-revert.mjs +1 -42
  44. package/flow-models.mjs +1 -139
  45. package/flow-preview.mjs +1 -148
  46. package/flow-receipt.mjs +1 -122
  47. package/flow-redispatch.mjs +1 -72
  48. package/flow-review-gate.mjs +1 -402
  49. package/flow-review-reflect.mjs +1 -115
  50. package/flow-review.mjs +1 -152
  51. package/flow-scope-evidence.mjs +1 -117
  52. package/flow-skill-registry.mjs +1 -140
  53. package/flow-task-graph.mjs +1 -562
  54. package/flow-worktree.mjs +1 -71
  55. package/git-diff-report.mjs +1 -121
  56. package/hermes-delegation-guard.mjs +1 -14
  57. package/hermes-event-mapper.mjs +1 -194
  58. package/hermes-isolation.mjs +1 -61
  59. package/hermes-model-cache.mjs +1 -54
  60. package/hermes-policy.mjs +1 -92
  61. package/hermes-probe.mjs +1 -57
  62. package/hermes-session.mjs +1 -683
  63. package/hermes-setup.mjs +1 -167
  64. package/host-memory.mjs +1 -116
  65. package/interrupted-resume.mjs +1 -95
  66. package/keep-awake.mjs +1 -148
  67. package/key-shape.mjs +1 -49
  68. package/lane-continuation.mjs +1 -83
  69. package/lane-lifecycle.mjs +1 -189
  70. package/lane-worktree.mjs +1 -77
  71. package/launcher.mjs +1 -354
  72. package/mcp-flight-recorder.mjs +1 -79
  73. package/mockup-delivery.mjs +1 -57
  74. package/model-prices.mjs +1 -113
  75. package/package.json +13 -4
  76. package/pair-bus.mjs +1 -98
  77. package/pair-control-authority.mjs +1 -89
  78. package/past-work-search.mjs +1 -105
  79. package/plan-meters.mjs +1 -144
  80. package/presence.mjs +1 -191
  81. package/privacy-report.mjs +1 -108
  82. package/provider-resilience.mjs +1 -356
  83. package/provider.mjs +1 -133
  84. package/providers.mjs +1 -491
  85. package/publish-guard.mjs +2 -0
  86. package/publish-manifest.json +129 -0
  87. package/question-response.mjs +1 -58
  88. package/reap-terminal.mjs +1 -68
  89. package/recap.mjs +1 -297
  90. package/replay-transport.mjs +1 -64
  91. package/repo-search.mjs +1 -190
  92. package/review-check.mjs +1 -182
  93. package/runtime-contract.mjs +1 -93
  94. package/runtime-registry.mjs +1 -64
  95. package/runtime-session.mjs +1 -20
  96. package/scheduled-run-admission.mjs +1 -364
  97. package/scheduled-runs.mjs +1 -268
  98. package/sdk-admission.mjs +1 -9
  99. package/sdk-smoke.mjs +1 -61
  100. package/serve-consent.mjs +1 -118
  101. package/serve-dir.mjs +1 -40
  102. package/service.mjs +1 -877
  103. package/session-store.mjs +1 -484
  104. package/side-lane.mjs +1 -63
  105. package/supabase-key.mjs +1 -176
  106. package/supervisor-ready.mjs +1 -57
  107. package/switch-provider.mjs +1 -84
  108. package/terminal-name.mjs +1 -359
  109. package/terminal-row-reconcile.mjs +1 -70
  110. package/thinkpool-prompt-contracts.mjs +1 -85
  111. package/thinkpool-room-prompt.mjs +1 -187
  112. package/transcript-sanitize.mjs +1 -332
  113. package/turn-stall.mjs +1 -61
  114. package/update-gate.mjs +1 -53
  115. package/viewport.mjs +1 -810
  116. package/worker-completion.mjs +1 -57
@@ -1,83 +1 @@
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)
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);
@@ -1,189 +1 @@
1
- import { classifyCodeEvent } from './code-event-contract.mjs'
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
- // Lifecycle for ordinary bridge-created terminal worktrees. Deletion authority
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"})}