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/flow-task-graph.mjs
CHANGED
|
@@ -1,562 +1 @@
|
|
|
1
|
-
// Thinkpool Flow — task-graph data model.
|
|
2
|
-
//
|
|
3
|
-
// A Flow run = one FlowSession (the "build me X" prompt + mode + status) decomposed
|
|
4
|
-
// by the conductor into FlowTasks (RUNNABLE slices + dependencies). The plan lives
|
|
5
|
-
// HERE, in the store, not in the conductor's chat context — research move #1
|
|
6
|
-
// (docs/specs/2026-06-29-thinkpool-flow.md): "move the plan out of the context
|
|
7
|
-
// window into a task-graph." Pure module — shared by the bridge (conductor) and the
|
|
8
|
-
// client (room UI), and mirrored by CHECK constraints in the
|
|
9
|
-
// 20260629120000_flow_sessions_tasks migration (one source of truth).
|
|
10
|
-
|
|
11
|
-
export const FLOW_STATUS = {
|
|
12
|
-
planning: 'planning', // conductor is decomposing
|
|
13
|
-
awaiting_approval: 'awaiting_approval', // plan submitted, waiting on the human
|
|
14
|
-
approved: 'approved', // human approved; lanes may dispatch (Step 2+)
|
|
15
|
-
building: 'building', // lanes are running
|
|
16
|
-
reviewing: 'reviewing', // adversarial-review phase
|
|
17
|
-
assembling: 'assembling', // merging worktrees
|
|
18
|
-
done: 'done',
|
|
19
|
-
failed: 'failed',
|
|
20
|
-
cancelled: 'cancelled',
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const TASK_STATUS = {
|
|
24
|
-
pending: 'pending',
|
|
25
|
-
ready: 'ready', // deps satisfied, awaiting dispatch
|
|
26
|
-
dispatched: 'dispatched', // a lane is running it
|
|
27
|
-
reviewing: 'reviewing', // adversarial-review lane is checking it
|
|
28
|
-
done: 'done',
|
|
29
|
-
failed: 'failed',
|
|
30
|
-
reverted: 'reverted', // rolled back by the atomic-revert net
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export const FLOW_MODE = {
|
|
34
|
-
steer: 'steer', // human watches every lane, live
|
|
35
|
-
guide: 'guide', // phase-gated (DEFAULT): approve plan + phase reviews
|
|
36
|
-
autopilot: 'autopilot', // full-auto, adaptive escalation + budget cap
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const SLICE_TYPE = {
|
|
40
|
-
feature: 'feature',
|
|
41
|
-
scaffold: 'scaffold', // project bootstrap / shared foundation
|
|
42
|
-
review: 'review', // adversarial-review lane
|
|
43
|
-
fix: 'fix',
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Gate-first Flow contracts are stored in flow_tasks.contract (JSONB). Keep every
|
|
47
|
-
// free-text field bounded and single-line: this payload crosses the terminal MCP
|
|
48
|
-
// boundary and is later injected into builder/reviewer prompts.
|
|
49
|
-
export const FLOW_CONTRACT_LIMITS = Object.freeze({
|
|
50
|
-
maxNonGoals: 6,
|
|
51
|
-
maxNonGoalChars: 240,
|
|
52
|
-
maxBaselineGateChars: 600,
|
|
53
|
-
maxBaselineEvidenceChars: 1400,
|
|
54
|
-
maxScopePaths: 32,
|
|
55
|
-
maxScopePathChars: 180,
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const BASELINE_NEGATIVE_SIGNAL = /\b(fail(?:s|ed|ing)?|absent|missing|not\s+(?:present|implemented|available|found)|does\s+not|404|empty)\b/i
|
|
59
|
-
const CONTRACT_DIGEST = /^[a-f0-9]{64}$/
|
|
60
|
-
const SHA256_K = [
|
|
61
|
-
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
62
|
-
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
63
|
-
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
64
|
-
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
65
|
-
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
66
|
-
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
67
|
-
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
68
|
-
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
69
|
-
]
|
|
70
|
-
const rotateRight = (value, bits) => (value >>> bits) | (value << (32 - bits))
|
|
71
|
-
const hasDisallowedControl = (value) => {
|
|
72
|
-
for (const char of value) {
|
|
73
|
-
const code = char.charCodeAt(0)
|
|
74
|
-
if (code === 0x7f || (code < 0x20 && code !== 0x09)) return true
|
|
75
|
-
}
|
|
76
|
-
return false
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function boundedLine (value, label, max, { required = false } = {}) {
|
|
80
|
-
if (value == null || value === '') {
|
|
81
|
-
if (required) throw new Error(`${label} is required`)
|
|
82
|
-
return ''
|
|
83
|
-
}
|
|
84
|
-
if (typeof value !== 'string') throw new Error(`${label} must be a string`)
|
|
85
|
-
const text = value.trim()
|
|
86
|
-
if (!text) {
|
|
87
|
-
if (required) throw new Error(`${label} is required`)
|
|
88
|
-
return ''
|
|
89
|
-
}
|
|
90
|
-
if (text.length > max) throw new Error(`${label} exceeds ${max} characters`)
|
|
91
|
-
if (hasDisallowedControl(text)) throw new Error(`${label} must be safe single-line text`)
|
|
92
|
-
return text
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function normalizeScopePath (value) {
|
|
96
|
-
if (typeof value !== 'string') return null
|
|
97
|
-
const text = value.trim().replaceAll('\\', '/')
|
|
98
|
-
if (!text || text.length > FLOW_CONTRACT_LIMITS.maxScopePathChars || text.startsWith('/') || /^[A-Za-z]:\//.test(text)) return null
|
|
99
|
-
const directoryPattern = text.endsWith('/**')
|
|
100
|
-
if (text.includes('*') && !directoryPattern) return null
|
|
101
|
-
const bare = directoryPattern ? text.slice(0, -3) : text
|
|
102
|
-
if (!bare || bare.endsWith('/') || bare.split('/').some((part) => !part || part === '.' || part === '..')) return null
|
|
103
|
-
if (/(?:^|\/)(?:\.env(?:\.|$)|\.git(?:\/|$)|\.thinkpool(?:\/|$)|node_modules(?:\/|$))/.test(bare)) return null
|
|
104
|
-
const last = bare.split('/').at(-1)
|
|
105
|
-
if (!directoryPattern && !last.includes('.') && !/^(?:Dockerfile|Makefile|LICENSE|README)$/i.test(last)) return null
|
|
106
|
-
return directoryPattern ? `${bare}/**` : bare
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function normalizeTaskScopePaths (value, { fallbackScope = '', required = true } = {}) {
|
|
110
|
-
let values = value
|
|
111
|
-
if (values == null && fallbackScope) {
|
|
112
|
-
const inferred = normalizeScopePath(fallbackScope)
|
|
113
|
-
if (inferred) values = [inferred]
|
|
114
|
-
}
|
|
115
|
-
if (values == null) values = []
|
|
116
|
-
if (!Array.isArray(values)) throw new Error('scopePaths must be an array')
|
|
117
|
-
if (values.length > FLOW_CONTRACT_LIMITS.maxScopePaths) throw new Error(`scopePaths exceeds ${FLOW_CONTRACT_LIMITS.maxScopePaths} entries`)
|
|
118
|
-
const out = []
|
|
119
|
-
for (const item of values) {
|
|
120
|
-
const safe = normalizeScopePath(item)
|
|
121
|
-
if (!safe) throw new Error(`invalid scope path: ${JSON.stringify(item)}`)
|
|
122
|
-
if (!out.includes(safe)) out.push(safe)
|
|
123
|
-
}
|
|
124
|
-
if (required && out.length === 0) throw new Error('machine-readable scopePaths are required for builder/fix/scaffold tasks')
|
|
125
|
-
return out.sort()
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function normalizeBaselineEvidence (value) {
|
|
129
|
-
return boundedLine(value, 'baseline evidence receipt', FLOW_CONTRACT_LIMITS.maxBaselineEvidenceChars, { required: true })
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// This is deliberately a small synchronous SHA-256 implementation instead of a
|
|
133
|
-
// Node crypto import or WebCrypto: the task graph must stay byte-identical and run
|
|
134
|
-
// in both the bridge's Node runtime and the browser bundle.
|
|
135
|
-
function sha256Hex (message) {
|
|
136
|
-
const source = new TextEncoder().encode(message)
|
|
137
|
-
const totalLength = Math.ceil((source.length + 9) / 64) * 64
|
|
138
|
-
const bytes = new Uint8Array(totalLength)
|
|
139
|
-
bytes.set(source)
|
|
140
|
-
bytes[source.length] = 0x80
|
|
141
|
-
let bitLength = BigInt(source.length) * 8n
|
|
142
|
-
for (let index = totalLength - 1; index >= totalLength - 8; index--) {
|
|
143
|
-
bytes[index] = Number(bitLength & 0xffn)
|
|
144
|
-
bitLength >>= 8n
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const hash = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
|
|
148
|
-
const words = new Uint32Array(64)
|
|
149
|
-
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
150
|
-
for (let index = 0; index < 16; index++) {
|
|
151
|
-
const start = offset + (index * 4)
|
|
152
|
-
words[index] = ((bytes[start] << 24) | (bytes[start + 1] << 16) | (bytes[start + 2] << 8) | bytes[start + 3]) >>> 0
|
|
153
|
-
}
|
|
154
|
-
for (let index = 16; index < 64; index++) {
|
|
155
|
-
const low = rotateRight(words[index - 15], 7) ^ rotateRight(words[index - 15], 18) ^ (words[index - 15] >>> 3)
|
|
156
|
-
const high = rotateRight(words[index - 2], 17) ^ rotateRight(words[index - 2], 19) ^ (words[index - 2] >>> 10)
|
|
157
|
-
words[index] = (words[index - 16] + low + words[index - 7] + high) >>> 0
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let [a, b, c, d, e, f, g, h] = hash
|
|
161
|
-
for (let index = 0; index < 64; index++) {
|
|
162
|
-
const sigma1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25)
|
|
163
|
-
const choose = (e & f) ^ (~e & g)
|
|
164
|
-
const temp1 = (h + sigma1 + choose + SHA256_K[index] + words[index]) >>> 0
|
|
165
|
-
const sigma0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22)
|
|
166
|
-
const majority = (a & b) ^ (a & c) ^ (b & c)
|
|
167
|
-
const temp2 = (sigma0 + majority) >>> 0
|
|
168
|
-
h = g
|
|
169
|
-
g = f
|
|
170
|
-
f = e
|
|
171
|
-
e = (d + temp1) >>> 0
|
|
172
|
-
d = c
|
|
173
|
-
c = b
|
|
174
|
-
b = a
|
|
175
|
-
a = (temp1 + temp2) >>> 0
|
|
176
|
-
}
|
|
177
|
-
hash[0] = (hash[0] + a) >>> 0
|
|
178
|
-
hash[1] = (hash[1] + b) >>> 0
|
|
179
|
-
hash[2] = (hash[2] + c) >>> 0
|
|
180
|
-
hash[3] = (hash[3] + d) >>> 0
|
|
181
|
-
hash[4] = (hash[4] + e) >>> 0
|
|
182
|
-
hash[5] = (hash[5] + f) >>> 0
|
|
183
|
-
hash[6] = (hash[6] + g) >>> 0
|
|
184
|
-
hash[7] = (hash[7] + h) >>> 0
|
|
185
|
-
}
|
|
186
|
-
return hash.map((word) => word.toString(16).padStart(8, '0')).join('')
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// The acceptance pack belongs to the builder target. A review task copies that
|
|
190
|
-
// target's digest, so its inherited target is bound without re-hashing reviewer
|
|
191
|
-
// prose or later baseline evidence receipts.
|
|
192
|
-
function acceptancePack ({ key, title, scope, scopePaths, acceptance, sliceType, nonGoals, baselineGate, inheritedReviewerTarget = key, version = 2 }) {
|
|
193
|
-
const pack = {
|
|
194
|
-
version,
|
|
195
|
-
task: { key, title, scope, acceptance, sliceType },
|
|
196
|
-
nonGoals: [...nonGoals].sort(),
|
|
197
|
-
baselineGate,
|
|
198
|
-
inheritedReviewerTarget,
|
|
199
|
-
}
|
|
200
|
-
if (version >= 2) pack.task.scopePaths = normalizeTaskScopePaths(scopePaths, { fallbackScope: scope })
|
|
201
|
-
return pack
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function acceptancePackDigest (input) {
|
|
205
|
-
return sha256Hex(JSON.stringify(acceptancePack(input)))
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function contractSource (task) {
|
|
209
|
-
const source = task?.contract && typeof task.contract === 'object' && !Array.isArray(task.contract)
|
|
210
|
-
? { ...task.contract }
|
|
211
|
-
: {}
|
|
212
|
-
if (source.nonGoals === undefined) source.nonGoals = task?.nonGoals ?? task?.non_goals
|
|
213
|
-
if (source.baseline === undefined) source.baseline = task?.baseline ?? task?.baselineGate ?? task?.baseline_gate
|
|
214
|
-
if (source.scopePaths === undefined) source.scopePaths = task?.scopePaths ?? task?.scope_paths
|
|
215
|
-
return source
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function normalizeProvidedContractDigest (source) {
|
|
219
|
-
if (source.digest === undefined) return null
|
|
220
|
-
if (typeof source.digest !== 'string' || !CONTRACT_DIGEST.test(source.digest)) {
|
|
221
|
-
throw new Error('contract digest must be a lowercase SHA-256 hex string')
|
|
222
|
-
}
|
|
223
|
-
return source.digest
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function normalizeProvidedAcceptancePack (source) {
|
|
227
|
-
if (source.acceptancePack === undefined) return null
|
|
228
|
-
if (!source.acceptancePack || typeof source.acceptancePack !== 'object' || Array.isArray(source.acceptancePack)) {
|
|
229
|
-
throw new Error('contract acceptance pack must be an object')
|
|
230
|
-
}
|
|
231
|
-
return source.acceptancePack
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Strict for new terminal submissions. `legacy:true` is restore-only: old persisted
|
|
235
|
-
// plans predate this contract and must remain runnable, but they can never be used to
|
|
236
|
-
// submit a new incomplete plan.
|
|
237
|
-
export function normalizeTaskContract (task, { sliceType, legacy = false } = {}) {
|
|
238
|
-
const source = contractSource(task)
|
|
239
|
-
const digest = normalizeProvidedContractDigest(source)
|
|
240
|
-
const sealedPack = normalizeProvidedAcceptancePack(source)
|
|
241
|
-
if (sliceType === SLICE_TYPE.review) return legacy ? null : { digest, acceptancePack: sealedPack }
|
|
242
|
-
const required = !legacy
|
|
243
|
-
const rawNonGoals = source.nonGoals
|
|
244
|
-
const values = rawNonGoals == null ? [] : (Array.isArray(rawNonGoals) ? rawNonGoals : [rawNonGoals])
|
|
245
|
-
if (values.length > FLOW_CONTRACT_LIMITS.maxNonGoals) throw new Error(`non-goals exceeds ${FLOW_CONTRACT_LIMITS.maxNonGoals} items`)
|
|
246
|
-
const nonGoals = values.map((value) => boundedLine(value, 'non-goal', FLOW_CONTRACT_LIMITS.maxNonGoalChars, { required: true }))
|
|
247
|
-
if (required && !nonGoals.length) throw new Error('explicit non-goals are required for builder/fix/scaffold tasks')
|
|
248
|
-
|
|
249
|
-
const rawBaseline = source.baseline
|
|
250
|
-
const baselineObject = typeof rawBaseline === 'string' ? { gate: rawBaseline } : rawBaseline
|
|
251
|
-
if (baselineObject != null && (typeof baselineObject !== 'object' || Array.isArray(baselineObject))) throw new Error('baseline gate must be a string or object')
|
|
252
|
-
const gate = boundedLine(baselineObject?.gate ?? baselineObject?.expectation, 'baseline gate', FLOW_CONTRACT_LIMITS.maxBaselineGateChars, { required })
|
|
253
|
-
if (gate && !BASELINE_NEGATIVE_SIGNAL.test(gate)) throw new Error('baseline gate must state what fails or is absent before implementation')
|
|
254
|
-
const evidence = baselineObject?.evidence == null || baselineObject?.evidence === ''
|
|
255
|
-
? ''
|
|
256
|
-
: normalizeBaselineEvidence(baselineObject.evidence)
|
|
257
|
-
const scopePaths = normalizeTaskScopePaths(source.scopePaths, { fallbackScope: task?.scope ?? task?.description ?? '', required })
|
|
258
|
-
return { nonGoals, baseline: gate ? { gate, evidence } : null, scopePaths, digest, acceptancePack: sealedPack }
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function bindContractDigest (task, { legacy = false } = {}) {
|
|
262
|
-
const sealedPack = acceptancePack({
|
|
263
|
-
key: task.key,
|
|
264
|
-
title: task.title,
|
|
265
|
-
scope: task.scope,
|
|
266
|
-
scopePaths: task.contract.scopePaths,
|
|
267
|
-
acceptance: task.acceptance,
|
|
268
|
-
sliceType: task.sliceType,
|
|
269
|
-
nonGoals: task.contract.nonGoals,
|
|
270
|
-
baselineGate: task.contract.baseline?.gate || '',
|
|
271
|
-
inheritedReviewerTarget: task.key,
|
|
272
|
-
version: legacy ? 1 : 2,
|
|
273
|
-
})
|
|
274
|
-
const digest = sha256Hex(JSON.stringify(sealedPack))
|
|
275
|
-
if (task.contract.digest && task.contract.digest !== digest) throw new Error(`task "${task.key}" contract digest does not match its acceptance pack`)
|
|
276
|
-
if (task.contract.acceptancePack && JSON.stringify(task.contract.acceptancePack) !== JSON.stringify(sealedPack)) {
|
|
277
|
-
throw new Error(`task "${task.key}" sealed acceptance pack does not match its task fields`)
|
|
278
|
-
}
|
|
279
|
-
task.contract.digest = digest
|
|
280
|
-
task.contract.acceptancePack = sealedPack
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// The browser persists normalized task rows before approval, then later sends a
|
|
284
|
-
// ready row back to the bridge for dispatch. Recompute the seal at that final
|
|
285
|
-
// boundary so changed acceptance/scope/non-goals can never ride beside an old hash.
|
|
286
|
-
export function validateTaskContractSeal (task, { legacy = false } = {}) {
|
|
287
|
-
const contract = task?.contract
|
|
288
|
-
if (!contract?.digest && legacy) return true
|
|
289
|
-
if (!contract || typeof contract !== 'object' || !contract.digest || !contract.acceptancePack) {
|
|
290
|
-
throw new Error('task is missing its sealed acceptance contract')
|
|
291
|
-
}
|
|
292
|
-
const pack = contract.acceptancePack
|
|
293
|
-
const packTask = pack?.task
|
|
294
|
-
if (!packTask || typeof packTask !== 'object' || !Array.isArray(pack.nonGoals)) {
|
|
295
|
-
throw new Error('task sealed acceptance pack is malformed')
|
|
296
|
-
}
|
|
297
|
-
const canonicalPack = acceptancePack({
|
|
298
|
-
key: packTask.key,
|
|
299
|
-
title: packTask.title,
|
|
300
|
-
scope: packTask.scope,
|
|
301
|
-
scopePaths: packTask.scopePaths,
|
|
302
|
-
acceptance: packTask.acceptance,
|
|
303
|
-
sliceType: packTask.sliceType,
|
|
304
|
-
nonGoals: pack.nonGoals,
|
|
305
|
-
baselineGate: pack.baselineGate,
|
|
306
|
-
inheritedReviewerTarget: pack.inheritedReviewerTarget,
|
|
307
|
-
version: pack.version === 1 ? 1 : 2,
|
|
308
|
-
})
|
|
309
|
-
if (JSON.stringify(pack) !== JSON.stringify(canonicalPack)) throw new Error('task sealed acceptance pack is not canonical')
|
|
310
|
-
if (acceptancePackDigest({
|
|
311
|
-
key: packTask.key,
|
|
312
|
-
title: packTask.title,
|
|
313
|
-
scope: packTask.scope,
|
|
314
|
-
scopePaths: packTask.scopePaths,
|
|
315
|
-
acceptance: packTask.acceptance,
|
|
316
|
-
sliceType: packTask.sliceType,
|
|
317
|
-
nonGoals: pack.nonGoals,
|
|
318
|
-
baselineGate: pack.baselineGate,
|
|
319
|
-
inheritedReviewerTarget: pack.inheritedReviewerTarget,
|
|
320
|
-
version: pack.version === 1 ? 1 : 2,
|
|
321
|
-
}) !== contract.digest) throw new Error('task acceptance digest does not match its sealed pack')
|
|
322
|
-
|
|
323
|
-
const sliceType = task.sliceType ?? task.slice_type
|
|
324
|
-
const key = task.key ?? task.task_key
|
|
325
|
-
const deps = Array.isArray(task.deps) ? task.deps : []
|
|
326
|
-
if (sliceType === SLICE_TYPE.review) {
|
|
327
|
-
if (contract.inheritedFrom !== packTask.key || deps.length !== 1 || deps[0] !== packTask.key) {
|
|
328
|
-
throw new Error('review task no longer targets its sealed builder')
|
|
329
|
-
}
|
|
330
|
-
if (pack.inheritedReviewerTarget !== packTask.key || task.acceptance !== packTask.acceptance) {
|
|
331
|
-
throw new Error(`review task "${key}" changed after its builder acceptance pack was sealed`)
|
|
332
|
-
}
|
|
333
|
-
return true
|
|
334
|
-
}
|
|
335
|
-
const livePack = acceptancePack({
|
|
336
|
-
key,
|
|
337
|
-
title: task.title,
|
|
338
|
-
scope: task.scope ?? '',
|
|
339
|
-
scopePaths: contract.scopePaths,
|
|
340
|
-
acceptance: task.acceptance ?? '',
|
|
341
|
-
sliceType,
|
|
342
|
-
nonGoals: contract.nonGoals || [],
|
|
343
|
-
baselineGate: contract.baseline?.gate || '',
|
|
344
|
-
inheritedReviewerTarget: key,
|
|
345
|
-
version: pack.version === 1 ? 1 : 2,
|
|
346
|
-
})
|
|
347
|
-
if (JSON.stringify(livePack) !== JSON.stringify(canonicalPack)) {
|
|
348
|
-
throw new Error(`task "${key}" changed after its acceptance pack was sealed`)
|
|
349
|
-
}
|
|
350
|
-
return true
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// A FlowTask slice. Each is RUNNABLE — a lane can build + run + self-correct it in
|
|
354
|
-
// its own WebContainer (research: the runtime is the moat). Partition by file
|
|
355
|
-
// ownership (scope) so two lanes never edit the same file (a branch is NOT isolation).
|
|
356
|
-
export function makeTask ({ key, title, scope = '', acceptance = '', deps = [], sliceType = SLICE_TYPE.feature, contract = null }) {
|
|
357
|
-
if (!key || typeof key !== 'string') throw new Error('task key required (string)')
|
|
358
|
-
if (!title) throw new Error('task title required')
|
|
359
|
-
if (!Array.isArray(deps)) throw new Error('deps must be an array of task keys')
|
|
360
|
-
if (!Object.values(SLICE_TYPE).includes(sliceType)) throw new Error(`unknown sliceType: ${sliceType}`)
|
|
361
|
-
return {
|
|
362
|
-
key,
|
|
363
|
-
title,
|
|
364
|
-
scope, // files/components this lane OWNS (disjoint from other slices)
|
|
365
|
-
acceptance, // runnable proof the lane + reviewer use to call it done
|
|
366
|
-
contract, // bounded gate-first contract; null only for legacy/restored rows
|
|
367
|
-
deps, // task keys this depends on (must form a DAG)
|
|
368
|
-
sliceType,
|
|
369
|
-
status: TASK_STATUS.pending,
|
|
370
|
-
laneId: null, // the spawned lane's terminal id, once dispatched
|
|
371
|
-
commitSha: null, // per-lane atomic commit (revert target)
|
|
372
|
-
failCount: 0, // adaptive-escalation counter (Autopilot pulls human in at N)
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Validate that tasks form a real DAG: unique keys, every dep references an existing
|
|
377
|
-
// key, no self-deps, no cycles. Throws on invalid. Returns the tasks (chainable).
|
|
378
|
-
export function validateDag (tasks) {
|
|
379
|
-
if (!Array.isArray(tasks) || tasks.length === 0) throw new Error('at least one task required')
|
|
380
|
-
const keys = new Set(tasks.map(t => t.key))
|
|
381
|
-
if (keys.size !== tasks.length) throw new Error('duplicate task keys')
|
|
382
|
-
const byKey = new Map(tasks.map(t => [t.key, t]))
|
|
383
|
-
for (const t of tasks) {
|
|
384
|
-
if (t.deps.includes(t.key)) throw new Error(`task "${t.key}" depends on itself`)
|
|
385
|
-
for (const d of t.deps) {
|
|
386
|
-
if (!keys.has(d)) throw new Error(`task "${t.key}" depends on unknown task "${d}"`)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
// Cycle detection (DFS, three-color). byKey guaranteed complete by the checks above.
|
|
390
|
-
const WHITE = 0, GRAY = 1, BLACK = 2
|
|
391
|
-
const color = new Map(tasks.map(t => [t.key, WHITE]))
|
|
392
|
-
function visit (k) {
|
|
393
|
-
color.set(k, GRAY)
|
|
394
|
-
for (const d of byKey.get(k).deps) {
|
|
395
|
-
const c = color.get(d)
|
|
396
|
-
if (c === GRAY) throw new Error(`cycle detected at "${k}" → "${d}"`)
|
|
397
|
-
if (c === WHITE) visit(d)
|
|
398
|
-
}
|
|
399
|
-
color.set(k, BLACK)
|
|
400
|
-
}
|
|
401
|
-
for (const t of tasks) if (color.get(t.key) === WHITE) visit(t.key)
|
|
402
|
-
return tasks
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Tasks whose deps are all DONE → ready to dispatch. Pure.
|
|
406
|
-
export function readyTasks (tasks) {
|
|
407
|
-
const done = new Set(tasks.filter(t => t.status === TASK_STATUS.done).map(t => t.key))
|
|
408
|
-
return tasks.filter(t =>
|
|
409
|
-
t.status === TASK_STATUS.pending &&
|
|
410
|
-
t.deps.every(d => done.has(d))
|
|
411
|
-
)
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Topological order (Kahn's algorithm). Returns keys. Throws if not a DAG.
|
|
415
|
-
export function topoOrder (tasks) {
|
|
416
|
-
validateDag(tasks)
|
|
417
|
-
const indeg = new Map(tasks.map(t => [t.key, t.deps.length]))
|
|
418
|
-
const dependents = new Map(tasks.map(t => [t.key, []])) // dep-key → keys that depend on it
|
|
419
|
-
for (const t of tasks) for (const d of t.deps) dependents.get(d).push(t.key)
|
|
420
|
-
const queue = tasks.filter(t => indeg.get(t.key) === 0).map(t => t.key)
|
|
421
|
-
const order = []
|
|
422
|
-
while (queue.length) {
|
|
423
|
-
const k = queue.shift()
|
|
424
|
-
order.push(k)
|
|
425
|
-
for (const m of dependents.get(k)) {
|
|
426
|
-
indeg.set(m, indeg.get(m) - 1)
|
|
427
|
-
if (indeg.get(m) === 0) queue.push(m)
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
if (order.length !== tasks.length) throw new Error('cycle detected (topo incomplete)')
|
|
431
|
-
return order
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// A FlowSession (the run). `tasks` is the decomposed plan (FlowTask[]).
|
|
435
|
-
export function makeFlowSession ({ prompt, mode = FLOW_MODE.guide, sessionRef = null, budgetCapAutopilot = null }) {
|
|
436
|
-
if (!prompt) throw new Error('prompt required')
|
|
437
|
-
if (!Object.values(FLOW_MODE).includes(mode)) throw new Error(`unknown mode: ${mode}`)
|
|
438
|
-
return {
|
|
439
|
-
prompt,
|
|
440
|
-
mode,
|
|
441
|
-
sessionRef, // the /code room session id this Flow runs in
|
|
442
|
-
status: FLOW_STATUS.planning,
|
|
443
|
-
budgetCapAutopilot, // token/step cap (Autopilot only); null in guide/steer
|
|
444
|
-
createdAt: null, // stamped by the DB
|
|
445
|
-
tasks: [],
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Normalize the conductor's raw plan output into a validated { summary, tasks }.
|
|
450
|
-
// Accepts a parsed object OR a JSON string (optionally ```json-fenced). Throws if the
|
|
451
|
-
// tasks don't form a real DAG. Lives in this shared module so neither the bridge nor
|
|
452
|
-
// the client has to import across the bridge↔src boundary.
|
|
453
|
-
export function normalizePlanOutput (raw, { legacy = false } = {}) {
|
|
454
|
-
let obj = raw
|
|
455
|
-
if (typeof raw === 'string') {
|
|
456
|
-
let s = raw.trim()
|
|
457
|
-
const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/i)
|
|
458
|
-
if (fence) s = fence[1].trim()
|
|
459
|
-
obj = JSON.parse(s)
|
|
460
|
-
}
|
|
461
|
-
if (!obj || typeof obj !== 'object') throw new Error('plan output is not an object')
|
|
462
|
-
const summary = typeof obj.summary === 'string' && obj.summary.trim() ? obj.summary.trim() : ''
|
|
463
|
-
if (!Array.isArray(obj.tasks) || obj.tasks.length === 0) throw new Error('plan has no tasks')
|
|
464
|
-
const tasks = obj.tasks.map((t) => {
|
|
465
|
-
// Accept `id` as an alias for `key` and `description` for `scope` — conductors emit
|
|
466
|
-
// either naming. A task with NO key is rejected (was silently String(undefined) →
|
|
467
|
-
// task_key "undefined", which broke worktree/dispatch); throwing makes it re-emit.
|
|
468
|
-
const key = t.key ?? t.id
|
|
469
|
-
if (key == null || !String(key).trim()) throw new Error('a task is missing its "key"')
|
|
470
|
-
const sliceType = t.sliceType ?? t.slice_type
|
|
471
|
-
return makeTask({
|
|
472
|
-
key: String(key),
|
|
473
|
-
title: String(t.title ?? key),
|
|
474
|
-
scope: String(t.scope ?? t.description ?? ''),
|
|
475
|
-
acceptance: String(t.acceptance ?? ''),
|
|
476
|
-
deps: Array.isArray(t.deps) ? t.deps.map(String) : [],
|
|
477
|
-
sliceType: Object.values(SLICE_TYPE).includes(sliceType) ? sliceType : SLICE_TYPE.feature,
|
|
478
|
-
contract: normalizeTaskContract(t, { sliceType: Object.values(SLICE_TYPE).includes(sliceType) ? sliceType : SLICE_TYPE.feature, legacy }),
|
|
479
|
-
})
|
|
480
|
-
})
|
|
481
|
-
validateDag(tasks)
|
|
482
|
-
for (const task of tasks) {
|
|
483
|
-
if (task.sliceType !== SLICE_TYPE.review) bindContractDigest(task, { legacy })
|
|
484
|
-
}
|
|
485
|
-
for (const task of tasks) {
|
|
486
|
-
if (task.sliceType !== SLICE_TYPE.review) continue
|
|
487
|
-
// New review tasks verify exactly one builder contract. Legacy persisted reviews
|
|
488
|
-
// keep their prior multi-target shape for restore compatibility only.
|
|
489
|
-
if (legacy) continue
|
|
490
|
-
if (task.deps.length !== 1) throw new Error(`review task "${task.key}" must depend on exactly one builder task to inherit its contract`)
|
|
491
|
-
const target = tasks.find((candidate) => candidate.key === task.deps[0])
|
|
492
|
-
if (!target || target.sliceType === SLICE_TYPE.review) throw new Error(`review task "${task.key}" must target a builder/fix/scaffold task`)
|
|
493
|
-
if (task.contract.digest && task.contract.digest !== target.contract.digest) {
|
|
494
|
-
throw new Error(`review task "${task.key}" contract digest does not match target "${target.key}"`)
|
|
495
|
-
}
|
|
496
|
-
task.contract = {
|
|
497
|
-
nonGoals: [...(target.contract?.nonGoals || [])],
|
|
498
|
-
baseline: target.contract?.baseline ? { ...target.contract.baseline } : null,
|
|
499
|
-
scopePaths: [...(target.contract?.scopePaths || [])],
|
|
500
|
-
inheritedFrom: target.key,
|
|
501
|
-
digest: target.contract.digest,
|
|
502
|
-
acceptancePack: target.contract.acceptancePack,
|
|
503
|
-
}
|
|
504
|
-
// Reviewer-authored prose is not an authority boundary. Normalize the live
|
|
505
|
-
// review row to the builder's sealed acceptance so dispatch and the prompt
|
|
506
|
-
// cannot silently substitute a weaker criterion.
|
|
507
|
-
task.acceptance = target.contract.acceptancePack.task.acceptance
|
|
508
|
-
}
|
|
509
|
-
return { summary, tasks }
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Codex review protocol deliberately maps one reviewer lane to one builder target.
|
|
513
|
-
// Claude's legacy Flow plans may review several deps and remain unchanged.
|
|
514
|
-
export function validatePlanForRuntime (plan, runtime = 'claude') {
|
|
515
|
-
if (runtime !== 'codex' && runtime !== 'hermes') return plan
|
|
516
|
-
for (const task of plan?.tasks || []) {
|
|
517
|
-
if (task.sliceType === SLICE_TYPE.review && task.deps.length !== 1) {
|
|
518
|
-
throw new Error(`${runtime === 'hermes' ? 'Hermes' : 'Codex'} review task "${task.key}" must depend on exactly one builder task`)
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
return plan
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
export function validReviewTargetShape (task, runtime = 'claude') {
|
|
525
|
-
if (!task || task.slice_type !== SLICE_TYPE.review) return true
|
|
526
|
-
return (runtime !== 'codex' && runtime !== 'hermes') || (Array.isArray(task.deps) && task.deps.length === 1)
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
export function legacyBuilderCompletionAllowed ({ runtime, flowRole, eventKind, eventSubtype, interrupted = false } = {}) {
|
|
530
|
-
return runtime === 'claude' && flowRole === 'builder' && !interrupted && eventKind === 'result' && eventSubtype === 'success'
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// ── Flow lane model tiers ───────────────────────────────────────────────────
|
|
534
|
-
// Spec: docs/specs/2026-07-03-flow-lane-model-tiers.md
|
|
535
|
-
// The conductor thinks; the worker lanes type. Running every lane on the same
|
|
536
|
-
// expensive brain is wasteful — the tier map picks a model per slice_type so
|
|
537
|
-
// intelligence matches the job:
|
|
538
|
-
// scaffold → sonnet (boilerplate / project bootstrap — mechanical, cheap)
|
|
539
|
-
// feature → opus (real building — needs the smart brain)
|
|
540
|
-
// fix → opus (root-cause debugging — needs the smart brain)
|
|
541
|
-
// review → opus (adversarial verification is intelligence-SENSITIVE; never cheapen it)
|
|
542
|
-
// Unknown/missing slice types fall to opus (safe default — never silently cheapen).
|
|
543
|
-
// Dev-only, no product gating. The conductor's own model is untouched by this map
|
|
544
|
-
// (bridge reads TP_FLOW_CONDUCTOR_MODEL at the conductor spawn site instead).
|
|
545
|
-
//
|
|
546
|
-
// Env overrides (checked FIRST, in order):
|
|
547
|
-
// TP_FLOW_LANE_MODEL='inherit' → return undefined (no model key passed → today's exact behavior)
|
|
548
|
-
// TP_FLOW_LANE_MODEL=<model> → blanket-override every lane to that model
|
|
549
|
-
// (empty-string / unset) → fall through to the tier map
|
|
550
|
-
const LANE_MODEL_TIERS = {
|
|
551
|
-
scaffold: 'sonnet',
|
|
552
|
-
feature: 'opus',
|
|
553
|
-
fix: 'opus',
|
|
554
|
-
review: 'opus',
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
export function laneModelFor (sliceType, env = process.env) {
|
|
558
|
-
const override = env.TP_FLOW_LANE_MODEL
|
|
559
|
-
if (override === 'inherit') return undefined // sentinel — restore today's behavior exactly
|
|
560
|
-
if (override) return override // blanket override for all lanes (non-empty string)
|
|
561
|
-
return LANE_MODEL_TIERS[sliceType] || 'opus' // tier map; unknown/missing → opus (never cheapen by accident)
|
|
562
|
-
}
|
|
1
|
+
export const FLOW_STATUS={planning:"planning",awaiting_approval:"awaiting_approval",approved:"approved",building:"building",reviewing:"reviewing",assembling:"assembling",done:"done",failed:"failed",cancelled:"cancelled"};export const TASK_STATUS={pending:"pending",ready:"ready",dispatched:"dispatched",reviewing:"reviewing",done:"done",failed:"failed",reverted:"reverted"};export const FLOW_MODE={steer:"steer",guide:"guide",autopilot:"autopilot"};export const SLICE_TYPE={feature:"feature",scaffold:"scaffold",review:"review",fix:"fix"};export const FLOW_CONTRACT_LIMITS=Object.freeze({maxNonGoals:6,maxNonGoalChars:240,maxBaselineGateChars:600,maxBaselineEvidenceChars:1400,maxScopePaths:32,maxScopePathChars:180});const e=/\b(fail(?:s|ed|ing)?|absent|missing|not\s+(?:present|implemented|available|found)|does\s+not|404|empty)\b/i,t=/^[a-f0-9]{64}$/,n=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],r=(e,t)=>e>>>t|e<<32-t;function a(e,t,n,{required:r=!1}={}){if(null==e||""===e){if(r)throw new Error(`${t} is required`);return""}if("string"!=typeof e)throw new Error(`${t} must be a string`);const a=e.trim();if(!a){if(r)throw new Error(`${t} is required`);return""}if(a.length>n)throw new Error(`${t} exceeds ${n} characters`);if((e=>{for(const t of e){const e=t.charCodeAt(0);if(127===e||e<32&&9!==e)return!0}return!1})(a))throw new Error(`${t} must be safe single-line text`);return a}function s(e){if("string"!=typeof e)return null;const t=e.trim().replaceAll("\\","/");if(!t||t.length>FLOW_CONTRACT_LIMITS.maxScopePathChars||t.startsWith("/")||/^[A-Za-z]:\//.test(t))return null;const n=t.endsWith("/**");if(t.includes("*")&&!n)return null;const r=n?t.slice(0,-3):t;if(!r||r.endsWith("/")||r.split("/").some(e=>!e||"."===e||".."===e))return null;if(/(?:^|\/)(?:\.env(?:\.|$)|\.git(?:\/|$)|\.thinkpool(?:\/|$)|node_modules(?:\/|$))/.test(r))return null;const a=r.split("/").at(-1);return n||a.includes(".")||/^(?:Dockerfile|Makefile|LICENSE|README)$/i.test(a)?n?`${r}/**`:r:null}export function normalizeTaskScopePaths(e,{fallbackScope:t="",required:n=!0}={}){let r=e;if(null==r&&t){const e=s(t);e&&(r=[e])}if(null==r&&(r=[]),!Array.isArray(r))throw new Error("scopePaths must be an array");if(r.length>FLOW_CONTRACT_LIMITS.maxScopePaths)throw new Error(`scopePaths exceeds ${FLOW_CONTRACT_LIMITS.maxScopePaths} entries`);const a=[];for(const e of r){const t=s(e);if(!t)throw new Error(`invalid scope path: ${JSON.stringify(e)}`);a.includes(t)||a.push(t)}if(n&&0===a.length)throw new Error("machine-readable scopePaths are required for builder/fix/scaffold tasks");return a.sort()}export function normalizeBaselineEvidence(e){return a(e,"baseline evidence receipt",FLOW_CONTRACT_LIMITS.maxBaselineEvidenceChars,{required:!0})}function o(e){const t=(new TextEncoder).encode(e),a=64*Math.ceil((t.length+9)/64),s=new Uint8Array(a);s.set(t),s[t.length]=128;let o=8n*BigInt(t.length);for(let e=a-1;e>=a-8;e--)s[e]=Number(0xffn&o),o>>=8n;const c=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],i=new Uint32Array(64);for(let e=0;e<s.length;e+=64){for(let t=0;t<16;t++){const n=e+4*t;i[t]=(s[n]<<24|s[n+1]<<16|s[n+2]<<8|s[n+3])>>>0}for(let e=16;e<64;e++){const t=r(i[e-15],7)^r(i[e-15],18)^i[e-15]>>>3,n=r(i[e-2],17)^r(i[e-2],19)^i[e-2]>>>10;i[e]=i[e-16]+t+i[e-7]+n>>>0}let[t,a,o,l,p,d,f,u]=c;for(let e=0;e<64;e++){const s=u+(r(p,6)^r(p,11)^r(p,25))+(p&d^~p&f)+n[e]+i[e]>>>0,c=t&a^t&o^a&o;u=f,f=d,d=p,p=l+s>>>0,l=o,o=a,a=t,t=s+((r(t,2)^r(t,13)^r(t,22))+c>>>0)>>>0}c[0]=c[0]+t>>>0,c[1]=c[1]+a>>>0,c[2]=c[2]+o>>>0,c[3]=c[3]+l>>>0,c[4]=c[4]+p>>>0,c[5]=c[5]+d>>>0,c[6]=c[6]+f>>>0,c[7]=c[7]+u>>>0}return c.map(e=>e.toString(16).padStart(8,"0")).join("")}function c({key:e,title:t,scope:n,scopePaths:r,acceptance:a,sliceType:s,nonGoals:o,baselineGate:c,inheritedReviewerTarget:i=e,version:l=2}){const p={version:l,task:{key:e,title:t,scope:n,acceptance:a,sliceType:s},nonGoals:[...o].sort(),baselineGate:c,inheritedReviewerTarget:i};return l>=2&&(p.task.scopePaths=normalizeTaskScopePaths(r,{fallbackScope:n})),p}export function acceptancePackDigest(e){return o(JSON.stringify(c(e)))}export function normalizeTaskContract(n,{sliceType:r,legacy:s=!1}={}){const o=function(e){const t=e?.contract&&"object"==typeof e.contract&&!Array.isArray(e.contract)?{...e.contract}:{};return void 0===t.nonGoals&&(t.nonGoals=e?.nonGoals??e?.non_goals),void 0===t.baseline&&(t.baseline=e?.baseline??e?.baselineGate??e?.baseline_gate),void 0===t.scopePaths&&(t.scopePaths=e?.scopePaths??e?.scope_paths),t}(n),c=function(e){if(void 0===e.digest)return null;if("string"!=typeof e.digest||!t.test(e.digest))throw new Error("contract digest must be a lowercase SHA-256 hex string");return e.digest}(o),i=function(e){if(void 0===e.acceptancePack)return null;if(!e.acceptancePack||"object"!=typeof e.acceptancePack||Array.isArray(e.acceptancePack))throw new Error("contract acceptance pack must be an object");return e.acceptancePack}(o);if(r===SLICE_TYPE.review)return s?null:{digest:c,acceptancePack:i};const l=!s,p=o.nonGoals,d=null==p?[]:Array.isArray(p)?p:[p];if(d.length>FLOW_CONTRACT_LIMITS.maxNonGoals)throw new Error(`non-goals exceeds ${FLOW_CONTRACT_LIMITS.maxNonGoals} items`);const f=d.map(e=>a(e,"non-goal",FLOW_CONTRACT_LIMITS.maxNonGoalChars,{required:!0}));if(l&&!f.length)throw new Error("explicit non-goals are required for builder/fix/scaffold tasks");const u=o.baseline,g="string"==typeof u?{gate:u}:u;if(null!=g&&("object"!=typeof g||Array.isArray(g)))throw new Error("baseline gate must be a string or object");const h=a(g?.gate??g?.expectation,"baseline gate",FLOW_CONTRACT_LIMITS.maxBaselineGateChars,{required:l});if(h&&!e.test(h))throw new Error("baseline gate must state what fails or is absent before implementation");const y=null==g?.evidence||""===g?.evidence?"":normalizeBaselineEvidence(g.evidence);return{nonGoals:f,baseline:h?{gate:h,evidence:y}:null,scopePaths:normalizeTaskScopePaths(o.scopePaths,{fallbackScope:n?.scope??n?.description??"",required:l}),digest:c,acceptancePack:i}}function i(e,{legacy:t=!1}={}){const n=c({key:e.key,title:e.title,scope:e.scope,scopePaths:e.contract.scopePaths,acceptance:e.acceptance,sliceType:e.sliceType,nonGoals:e.contract.nonGoals,baselineGate:e.contract.baseline?.gate||"",inheritedReviewerTarget:e.key,version:t?1:2}),r=o(JSON.stringify(n));if(e.contract.digest&&e.contract.digest!==r)throw new Error(`task "${e.key}" contract digest does not match its acceptance pack`);if(e.contract.acceptancePack&&JSON.stringify(e.contract.acceptancePack)!==JSON.stringify(n))throw new Error(`task "${e.key}" sealed acceptance pack does not match its task fields`);e.contract.digest=r,e.contract.acceptancePack=n}export function validateTaskContractSeal(e,{legacy:t=!1}={}){const n=e?.contract;if(!n?.digest&&t)return!0;if(!n||"object"!=typeof n||!n.digest||!n.acceptancePack)throw new Error("task is missing its sealed acceptance contract");const r=n.acceptancePack,a=r?.task;if(!a||"object"!=typeof a||!Array.isArray(r.nonGoals))throw new Error("task sealed acceptance pack is malformed");const s=c({key:a.key,title:a.title,scope:a.scope,scopePaths:a.scopePaths,acceptance:a.acceptance,sliceType:a.sliceType,nonGoals:r.nonGoals,baselineGate:r.baselineGate,inheritedReviewerTarget:r.inheritedReviewerTarget,version:1===r.version?1:2});if(JSON.stringify(r)!==JSON.stringify(s))throw new Error("task sealed acceptance pack is not canonical");if(acceptancePackDigest({key:a.key,title:a.title,scope:a.scope,scopePaths:a.scopePaths,acceptance:a.acceptance,sliceType:a.sliceType,nonGoals:r.nonGoals,baselineGate:r.baselineGate,inheritedReviewerTarget:r.inheritedReviewerTarget,version:1===r.version?1:2})!==n.digest)throw new Error("task acceptance digest does not match its sealed pack");const o=e.sliceType??e.slice_type,i=e.key??e.task_key,l=Array.isArray(e.deps)?e.deps:[];if(o===SLICE_TYPE.review){if(n.inheritedFrom!==a.key||1!==l.length||l[0]!==a.key)throw new Error("review task no longer targets its sealed builder");if(r.inheritedReviewerTarget!==a.key||e.acceptance!==a.acceptance)throw new Error(`review task "${i}" changed after its builder acceptance pack was sealed`);return!0}const p=c({key:i,title:e.title,scope:e.scope??"",scopePaths:n.scopePaths,acceptance:e.acceptance??"",sliceType:o,nonGoals:n.nonGoals||[],baselineGate:n.baseline?.gate||"",inheritedReviewerTarget:i,version:1===r.version?1:2});if(JSON.stringify(p)!==JSON.stringify(s))throw new Error(`task "${i}" changed after its acceptance pack was sealed`);return!0}export function makeTask({key:e,title:t,scope:n="",acceptance:r="",deps:a=[],sliceType:s=SLICE_TYPE.feature,contract:o=null}){if(!e||"string"!=typeof e)throw new Error("task key required (string)");if(!t)throw new Error("task title required");if(!Array.isArray(a))throw new Error("deps must be an array of task keys");if(!Object.values(SLICE_TYPE).includes(s))throw new Error(`unknown sliceType: ${s}`);return{key:e,title:t,scope:n,acceptance:r,contract:o,deps:a,sliceType:s,status:TASK_STATUS.pending,laneId:null,commitSha:null,failCount:0}}export function validateDag(e){if(!Array.isArray(e)||0===e.length)throw new Error("at least one task required");const t=new Set(e.map(e=>e.key));if(t.size!==e.length)throw new Error("duplicate task keys");const n=new Map(e.map(e=>[e.key,e]));for(const n of e){if(n.deps.includes(n.key))throw new Error(`task "${n.key}" depends on itself`);for(const e of n.deps)if(!t.has(e))throw new Error(`task "${n.key}" depends on unknown task "${e}"`)}const r=new Map(e.map(e=>[e.key,0]));function a(e){r.set(e,1);for(const t of n.get(e).deps){const n=r.get(t);if(1===n)throw new Error(`cycle detected at "${e}" → "${t}"`);0===n&&a(t)}r.set(e,2)}for(const t of e)0===r.get(t.key)&&a(t.key);return e}export function readyTasks(e){const t=new Set(e.filter(e=>e.status===TASK_STATUS.done).map(e=>e.key));return e.filter(e=>e.status===TASK_STATUS.pending&&e.deps.every(e=>t.has(e)))}export function topoOrder(e){validateDag(e);const t=new Map(e.map(e=>[e.key,e.deps.length])),n=new Map(e.map(e=>[e.key,[]]));for(const t of e)for(const e of t.deps)n.get(e).push(t.key);const r=e.filter(e=>0===t.get(e.key)).map(e=>e.key),a=[];for(;r.length;){const e=r.shift();a.push(e);for(const a of n.get(e))t.set(a,t.get(a)-1),0===t.get(a)&&r.push(a)}if(a.length!==e.length)throw new Error("cycle detected (topo incomplete)");return a}export function makeFlowSession({prompt:e,mode:t=FLOW_MODE.guide,sessionRef:n=null,budgetCapAutopilot:r=null}){if(!e)throw new Error("prompt required");if(!Object.values(FLOW_MODE).includes(t))throw new Error(`unknown mode: ${t}`);return{prompt:e,mode:t,sessionRef:n,status:FLOW_STATUS.planning,budgetCapAutopilot:r,createdAt:null,tasks:[]}}export function normalizePlanOutput(e,{legacy:t=!1}={}){let n=e;if("string"==typeof e){let t=e.trim();const r=t.match(/```(?:json)?\s*([\s\S]*?)```/i);r&&(t=r[1].trim()),n=JSON.parse(t)}if(!n||"object"!=typeof n)throw new Error("plan output is not an object");const r="string"==typeof n.summary&&n.summary.trim()?n.summary.trim():"";if(!Array.isArray(n.tasks)||0===n.tasks.length)throw new Error("plan has no tasks");const a=n.tasks.map(e=>{const n=e.key??e.id;if(null==n||!String(n).trim())throw new Error('a task is missing its "key"');const r=e.sliceType??e.slice_type;return makeTask({key:String(n),title:String(e.title??n),scope:String(e.scope??e.description??""),acceptance:String(e.acceptance??""),deps:Array.isArray(e.deps)?e.deps.map(String):[],sliceType:Object.values(SLICE_TYPE).includes(r)?r:SLICE_TYPE.feature,contract:normalizeTaskContract(e,{sliceType:Object.values(SLICE_TYPE).includes(r)?r:SLICE_TYPE.feature,legacy:t})})});validateDag(a);for(const e of a)e.sliceType!==SLICE_TYPE.review&&i(e,{legacy:t});for(const e of a){if(e.sliceType!==SLICE_TYPE.review)continue;if(t)continue;if(1!==e.deps.length)throw new Error(`review task "${e.key}" must depend on exactly one builder task to inherit its contract`);const n=a.find(t=>t.key===e.deps[0]);if(!n||n.sliceType===SLICE_TYPE.review)throw new Error(`review task "${e.key}" must target a builder/fix/scaffold task`);if(e.contract.digest&&e.contract.digest!==n.contract.digest)throw new Error(`review task "${e.key}" contract digest does not match target "${n.key}"`);e.contract={nonGoals:[...n.contract?.nonGoals||[]],baseline:n.contract?.baseline?{...n.contract.baseline}:null,scopePaths:[...n.contract?.scopePaths||[]],inheritedFrom:n.key,digest:n.contract.digest,acceptancePack:n.contract.acceptancePack},e.acceptance=n.contract.acceptancePack.task.acceptance}return{summary:r,tasks:a}}export function validatePlanForRuntime(e,t="claude"){if("codex"!==t&&"hermes"!==t)return e;for(const n of e?.tasks||[])if(n.sliceType===SLICE_TYPE.review&&1!==n.deps.length)throw new Error(`${"hermes"===t?"Hermes":"Codex"} review task "${n.key}" must depend on exactly one builder task`);return e}export function validReviewTargetShape(e,t="claude"){return!e||e.slice_type!==SLICE_TYPE.review||"codex"!==t&&"hermes"!==t||Array.isArray(e.deps)&&1===e.deps.length}export function legacyBuilderCompletionAllowed({runtime:e,flowRole:t,eventKind:n,eventSubtype:r,interrupted:a=!1}={}){return"claude"===e&&"builder"===t&&!a&&"result"===n&&"success"===r}const l={scaffold:"sonnet",feature:"opus",fix:"opus",review:"opus"};export function laneModelFor(e,t=process.env){const n=t.TP_FLOW_LANE_MODEL;if("inherit"!==n)return n||l[e]||"opus"}
|