brainclaw 1.26.2 → 1.27.0
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/README.md +13 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-coordination.js +65 -1
- package/dist/commands/attempt-authority.js +80 -0
- package/dist/commands/harvest.js +140 -61
- package/dist/commands/loop.js +34 -0
- package/dist/commands/loops-handlers.js +87 -14
- package/dist/commands/mcp-catalog.js +42 -18
- package/dist/commands/mcp-schemas.generated.js +44 -0
- package/dist/commands/mcp-write-claims.js +128 -1
- package/dist/commands/mcp-write-coordination.js +146 -76
- package/dist/core/agent-capability.js +1 -1
- package/dist/core/agentrun-reconciler.js +148 -22
- package/dist/core/agentruns.js +254 -29
- package/dist/core/assignment-request-schema.js +7 -0
- package/dist/core/assignment-sweeper.js +5 -3
- package/dist/core/assignments.js +131 -33
- package/dist/core/claim-request-schema.js +7 -0
- package/dist/core/claims.js +53 -2
- package/dist/core/dispatch-status.js +16 -6
- package/dist/core/dispatcher.js +51 -51
- package/dist/core/entity-operations.js +20 -0
- package/dist/core/events.js +4 -0
- package/dist/core/execution-adapters.js +160 -14
- package/dist/core/execution-contract.js +345 -0
- package/dist/core/execution.js +130 -16
- package/dist/core/harness-adapters/base.js +150 -0
- package/dist/core/harness-adapters/claude.js +39 -0
- package/dist/core/harness-adapters/codex.js +57 -0
- package/dist/core/harness-adapters/harvest.js +109 -0
- package/dist/core/harness-adapters/index.js +8 -0
- package/dist/core/harness-adapters/prompt-only.js +13 -0
- package/dist/core/harness-adapters/registry.js +48 -0
- package/dist/core/harness-adapters/result.js +33 -0
- package/dist/core/harness-adapters/types.js +2 -0
- package/dist/core/ideation-loop-close.js +25 -2
- package/dist/core/instruction-templates.js +3 -2
- package/dist/core/loop-turn-dispatch.js +207 -0
- package/dist/core/loops/artifact-contract.js +11 -0
- package/dist/core/loops/attempt-authority.js +476 -0
- package/dist/core/loops/attempt-generations.js +509 -0
- package/dist/core/loops/attempt-reservation.js +197 -35
- package/dist/core/loops/attempt-rollout.js +404 -0
- package/dist/core/loops/attempt-takeover.js +155 -0
- package/dist/core/loops/bootstrap-acquire.js +7 -3
- package/dist/core/loops/evidence.js +187 -0
- package/dist/core/loops/facade-schema.js +41 -10
- package/dist/core/loops/gate-policy.js +485 -0
- package/dist/core/loops/impl-bind.js +37 -79
- package/dist/core/loops/index.js +9 -0
- package/dist/core/loops/iteration-engine.js +31 -19
- package/dist/core/loops/kind-policies.js +90 -0
- package/dist/core/loops/lock.js +71 -13
- package/dist/core/loops/reconcile-turn.js +235 -18
- package/dist/core/loops/result-reducers.js +99 -10
- package/dist/core/loops/store.js +30 -3
- package/dist/core/loops/turn-execution.js +480 -0
- package/dist/core/loops/types.js +113 -2
- package/dist/core/loops/verbs.js +332 -99
- package/dist/core/loops/verify-command.js +31 -8
- package/dist/core/loops/workspace-digest.js +54 -0
- package/dist/core/review-loop-close.js +25 -3
- package/dist/core/review-loop-turn-dispatch.js +210 -161
- package/dist/core/runtime-signals.js +62 -25
- package/dist/core/schema.js +35 -0
- package/dist/core/spawn-check.js +3 -2
- package/dist/core/upgrades/backup.js +27 -4
- package/dist/facts.js +7 -6
- package/dist/facts.json +6 -5
- package/docs/cli.md +49 -1
- package/docs/concepts/attempt-authority.md +407 -0
- package/docs/concepts/evidence-attestations.md +135 -0
- package/docs/concepts/execution-contract.md +166 -0
- package/docs/concepts/harness-adapters.md +166 -0
- package/docs/concepts/ideation-loop.md +5 -4
- package/docs/concepts/loop-engine.md +302 -113
- package/docs/index.md +4 -1
- package/docs/integrations/codex.md +3 -3
- package/docs/integrations/mcp.md +59 -5
- package/docs/loops/debug.md +144 -0
- package/docs/loops/ideation.md +158 -0
- package/docs/loops/implementation.md +154 -0
- package/docs/loops/research.md +136 -0
- package/docs/loops/review.md +200 -0
- package/docs/mcp-schema-changelog.md +14 -5
- package/package.json +1 -1
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { evidenceDigest, validateArtifactEvidence } from './evidence.js';
|
|
2
|
+
import { captureWorkspaceDigest } from './workspace-digest.js';
|
|
3
|
+
import { currentNonce, findReservationByAssignmentId, getReservation, } from './attempt-reservation.js';
|
|
4
|
+
import { readLaunchDecision, resolveTurnGenerationChain, } from './attempt-generations.js';
|
|
5
|
+
const OBSERVATION = {
|
|
6
|
+
right: 'gate:artifact',
|
|
7
|
+
authorities: [
|
|
8
|
+
{ channel: 'complete_turn', producer_kind: 'slot', attestation: 'observation', issuer: 'brainclaw:artifact-commit', required_subject_fields: ['slot_id'] },
|
|
9
|
+
{ channel: 'reconcile_turn', producer_kind: 'slot', attestation: 'observation', issuer: 'brainclaw:artifact-commit', required_subject_fields: ['slot_id', 'turn_id', 'run_id', 'nonce_digest', 'attempt_epoch', 'execution_contract_hash', 'workspace_digest'] },
|
|
10
|
+
{ channel: 'operator_input', producer_kind: 'slot', attestation: 'observation', issuer: 'brainclaw:artifact-commit', required_subject_fields: ['slot_id'] },
|
|
11
|
+
{ channel: 'operator_input', producer_kind: 'operator', attestation: 'observation', issuer: 'brainclaw:artifact-commit' },
|
|
12
|
+
{ channel: 'operator_input', producer_kind: 'engine', attestation: 'observation', issuer: 'brainclaw:artifact-commit' },
|
|
13
|
+
{ channel: 'system_hook', producer_kind: 'engine', attestation: 'observation', issuer: 'brainclaw:artifact-commit' },
|
|
14
|
+
{ channel: 'verify_command', producer_kind: 'engine', attestation: 'verification', issuer: 'brainclaw:verify-command', required_subject_fields: ['command_digest', 'workspace_digest'] },
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
const APPROVAL = {
|
|
18
|
+
right: 'gate:reviewer_green',
|
|
19
|
+
authorities: [
|
|
20
|
+
{ channel: 'complete_turn', producer_kind: 'slot', attestation: 'approval', issuer: 'brainclaw:review-slot', required_subject_fields: ['slot_id'] },
|
|
21
|
+
{ channel: 'reconcile_turn', producer_kind: 'slot', attestation: 'approval', issuer: 'brainclaw:review-slot', required_subject_fields: ['slot_id', 'turn_id', 'run_id', 'nonce_digest', 'attempt_epoch', 'execution_contract_hash', 'workspace_digest'] },
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
const VERIFICATION = {
|
|
25
|
+
right: 'gate:command_green',
|
|
26
|
+
authorities: [
|
|
27
|
+
{ channel: 'verify_command', producer_kind: 'engine', attestation: 'verification', issuer: 'brainclaw:verify-command', required_subject_fields: ['command_digest', 'workspace_digest'] },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
const CRITIC_OBSERVATION = {
|
|
31
|
+
right: 'gate:artifact',
|
|
32
|
+
authorities: OBSERVATION.authorities.filter((authority) => authority.channel === 'complete_turn' || authority.channel === 'reconcile_turn'),
|
|
33
|
+
};
|
|
34
|
+
const NO_REVIEWER_GREEN = { right: 'gate:reviewer_green', authorities: [] };
|
|
35
|
+
const NO_COMMAND_GREEN = { right: 'gate:command_green', authorities: [] };
|
|
36
|
+
const NO_CRITIC_SIGNAL = { right: 'gate:artifact', authorities: [] };
|
|
37
|
+
/** Policies refine the five protocol graphs; they do not duplicate phase order. */
|
|
38
|
+
export const GATE_POLICIES = {
|
|
39
|
+
review: {
|
|
40
|
+
version: 'gate-policy-v1',
|
|
41
|
+
kind: 'review',
|
|
42
|
+
requirements: {
|
|
43
|
+
artifact: OBSERVATION,
|
|
44
|
+
reviewer_green: APPROVAL,
|
|
45
|
+
command_green: NO_COMMAND_GREEN,
|
|
46
|
+
critic_signal: NO_CRITIC_SIGNAL,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
ideation: {
|
|
50
|
+
version: 'gate-policy-v1',
|
|
51
|
+
kind: 'ideation',
|
|
52
|
+
requirements: {
|
|
53
|
+
artifact: OBSERVATION,
|
|
54
|
+
reviewer_green: NO_REVIEWER_GREEN,
|
|
55
|
+
command_green: NO_COMMAND_GREEN,
|
|
56
|
+
critic_signal: CRITIC_OBSERVATION,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
implementation: {
|
|
60
|
+
version: 'gate-policy-v1',
|
|
61
|
+
kind: 'implementation',
|
|
62
|
+
requirements: {
|
|
63
|
+
artifact: OBSERVATION,
|
|
64
|
+
reviewer_green: NO_REVIEWER_GREEN,
|
|
65
|
+
command_green: VERIFICATION,
|
|
66
|
+
critic_signal: NO_CRITIC_SIGNAL,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
research: {
|
|
70
|
+
version: 'gate-policy-v1',
|
|
71
|
+
kind: 'research',
|
|
72
|
+
requirements: {
|
|
73
|
+
artifact: OBSERVATION,
|
|
74
|
+
reviewer_green: NO_REVIEWER_GREEN,
|
|
75
|
+
command_green: NO_COMMAND_GREEN,
|
|
76
|
+
// The shipped research FSM exits investigate↔synthesize on this signal.
|
|
77
|
+
critic_signal: CRITIC_OBSERVATION,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
debug: {
|
|
81
|
+
version: 'gate-policy-v1',
|
|
82
|
+
kind: 'debug',
|
|
83
|
+
requirements: {
|
|
84
|
+
artifact: OBSERVATION,
|
|
85
|
+
reviewer_green: NO_REVIEWER_GREEN,
|
|
86
|
+
// The shipped debug FSM exits hypothesize↔isolate↔fix on command_green.
|
|
87
|
+
command_green: VERIFICATION,
|
|
88
|
+
critic_signal: NO_CRITIC_SIGNAL,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
function hasUsableContent(artifact) {
|
|
93
|
+
return (artifact.body ?? '').trim().length > 0 || artifact.ref !== undefined;
|
|
94
|
+
}
|
|
95
|
+
function legacyEligibleCount(thread, artifacts, purpose, cwd) {
|
|
96
|
+
// Kind-specialized purposes stay fail-closed even for persisted legacy
|
|
97
|
+
// loops. Legacy relaxes envelope presence; it does not invent an authority
|
|
98
|
+
// that the kind's policy explicitly forbids.
|
|
99
|
+
if (GATE_POLICIES[thread.kind].requirements[purpose].authorities.length === 0)
|
|
100
|
+
return 0;
|
|
101
|
+
return artifacts.filter((artifact) => {
|
|
102
|
+
if (purpose !== 'critic_signal' && !hasUsableContent(artifact))
|
|
103
|
+
return false;
|
|
104
|
+
if (!artifact.evidence)
|
|
105
|
+
return true;
|
|
106
|
+
return validateArtifactEvidence(thread, artifact).valid
|
|
107
|
+
&& reconciledV2AuthorityRejection(thread, artifact, cwd) === undefined;
|
|
108
|
+
}).length;
|
|
109
|
+
}
|
|
110
|
+
function payloadFingerprint(artifact) {
|
|
111
|
+
return evidenceDigest({
|
|
112
|
+
phase: artifact.phase,
|
|
113
|
+
type: artifact.type,
|
|
114
|
+
body: artifact.body,
|
|
115
|
+
ref: artifact.ref,
|
|
116
|
+
iteration: artifact.iteration ?? 0,
|
|
117
|
+
producer: artifact.evidence?.subject.slot_id ?? artifact.evidence?.producer.id ?? artifact.produced_by,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A reconciled v2 envelope is only a claim about an attempt until the gate
|
|
122
|
+
* compares it with immutable AttemptAuthority. The persisted nonce is a digest
|
|
123
|
+
* (never the bearer token), so compare it with a server-derived digest of the
|
|
124
|
+
* current generation nonce.
|
|
125
|
+
*
|
|
126
|
+
* No v2 chain enters an explicit restrictive legacy verifier backed by the
|
|
127
|
+
* committed reservation. Missing/unverifiable authority and every tuple
|
|
128
|
+
* mismatch are fail-closed in both modes.
|
|
129
|
+
*/
|
|
130
|
+
function reconciledV2AuthorityRejection(thread, artifact, cwd) {
|
|
131
|
+
const envelope = artifact.evidence;
|
|
132
|
+
if (!envelope || envelope.producer.channel !== 'reconcile_turn')
|
|
133
|
+
return undefined;
|
|
134
|
+
const subject = envelope.subject;
|
|
135
|
+
const reservation = subject.assignment_id
|
|
136
|
+
? findReservationByAssignmentId(subject.assignment_id, cwd)
|
|
137
|
+
: undefined;
|
|
138
|
+
const resolvedReservation = reservation ?? (subject.turn_id ? getReservation(subject.turn_id, cwd) : undefined);
|
|
139
|
+
if (!resolvedReservation)
|
|
140
|
+
return 'reconciled_authority_missing';
|
|
141
|
+
let chain;
|
|
142
|
+
try {
|
|
143
|
+
chain = resolveTurnGenerationChain(resolvedReservation.store_root, resolvedReservation.turn_id);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return 'reconciled_authority_unreadable';
|
|
147
|
+
}
|
|
148
|
+
if (!chain) {
|
|
149
|
+
// Legacy is deliberately narrow, not permissive: every binding is
|
|
150
|
+
// reconstructed from the committed, crossed reservation. Pre-P1 records
|
|
151
|
+
// have no stored ExecutionContract, so use the same deterministic
|
|
152
|
+
// compatibility identities the reconciler seals. This preserves dual-read
|
|
153
|
+
// without accepting a caller-chosen hash.
|
|
154
|
+
const nonce = currentNonce(resolvedReservation);
|
|
155
|
+
if (resolvedReservation.decision !== 'committed'
|
|
156
|
+
|| resolvedReservation.launch?.status !== 'crossed'
|
|
157
|
+
|| !nonce) {
|
|
158
|
+
return 'reconciled_legacy_authority_unverifiable';
|
|
159
|
+
}
|
|
160
|
+
const contractHash = resolvedReservation.execution_contract_ref?.hash ?? evidenceDigest({
|
|
161
|
+
version: 'legacy-uncontracted-reservation-v1',
|
|
162
|
+
turn_id: resolvedReservation.turn_id,
|
|
163
|
+
run_id: resolvedReservation.child_ids.run_id,
|
|
164
|
+
epoch: resolvedReservation.epoch,
|
|
165
|
+
phase: resolvedReservation.phase,
|
|
166
|
+
iteration: resolvedReservation.iteration,
|
|
167
|
+
cwd: resolvedReservation.cwd,
|
|
168
|
+
});
|
|
169
|
+
const workspaceDigest = evidenceDigest({
|
|
170
|
+
workspace_policy: resolvedReservation.execution_contract?.workspace_policy,
|
|
171
|
+
cwd: resolvedReservation.cwd,
|
|
172
|
+
store_root: resolvedReservation.store_root,
|
|
173
|
+
});
|
|
174
|
+
const expectedLegacy = {
|
|
175
|
+
loop_id: resolvedReservation.loop_id,
|
|
176
|
+
slot_id: resolvedReservation.slot_id,
|
|
177
|
+
turn_id: resolvedReservation.turn_id,
|
|
178
|
+
assignment_id: resolvedReservation.child_ids.assignment_id,
|
|
179
|
+
run_id: resolvedReservation.child_ids.run_id,
|
|
180
|
+
nonce_digest: evidenceDigest({ launch_nonce: nonce }),
|
|
181
|
+
attempt_epoch: resolvedReservation.epoch,
|
|
182
|
+
execution_contract_hash: contractHash,
|
|
183
|
+
workspace_digest: workspaceDigest,
|
|
184
|
+
};
|
|
185
|
+
const actualLegacy = {
|
|
186
|
+
loop_id: thread.id,
|
|
187
|
+
slot_id: subject.slot_id,
|
|
188
|
+
turn_id: subject.turn_id,
|
|
189
|
+
assignment_id: subject.assignment_id,
|
|
190
|
+
run_id: subject.run_id,
|
|
191
|
+
nonce_digest: subject.nonce_digest,
|
|
192
|
+
attempt_epoch: subject.attempt_epoch,
|
|
193
|
+
execution_contract_hash: subject.execution_contract_hash,
|
|
194
|
+
workspace_digest: subject.workspace_digest,
|
|
195
|
+
};
|
|
196
|
+
const mismatch = Object.keys(expectedLegacy)
|
|
197
|
+
.find((field) => actualLegacy[field] !== expectedLegacy[field]);
|
|
198
|
+
return mismatch ? `reconciled_legacy_authority_mismatch:${mismatch}` : undefined;
|
|
199
|
+
}
|
|
200
|
+
if (chain.status !== 'active' && chain.status !== 'settled') {
|
|
201
|
+
return `reconciled_authority_not_usable:${chain.status}`;
|
|
202
|
+
}
|
|
203
|
+
const generation = chain.latest_generation;
|
|
204
|
+
let launch;
|
|
205
|
+
try {
|
|
206
|
+
launch = readLaunchDecision(resolvedReservation.store_root, generation.turn_id, generation.attempt_epoch);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return 'reconciled_authority_unreadable';
|
|
210
|
+
}
|
|
211
|
+
if (launch?.decision !== 'crossed')
|
|
212
|
+
return 'reconciled_authority_launch_not_crossed';
|
|
213
|
+
const expected = {
|
|
214
|
+
loop_id: resolvedReservation.loop_id,
|
|
215
|
+
slot_id: resolvedReservation.slot_id,
|
|
216
|
+
turn_id: generation.turn_id,
|
|
217
|
+
assignment_id: generation.assignment_id,
|
|
218
|
+
run_id: generation.run_id,
|
|
219
|
+
nonce_digest: evidenceDigest({ launch_nonce: generation.launch_nonce }),
|
|
220
|
+
attempt_epoch: generation.attempt_epoch,
|
|
221
|
+
execution_contract_hash: generation.contract_hash,
|
|
222
|
+
workspace_digest: generation.workspace_digest,
|
|
223
|
+
};
|
|
224
|
+
const actual = {
|
|
225
|
+
loop_id: thread.id,
|
|
226
|
+
slot_id: subject.slot_id,
|
|
227
|
+
turn_id: subject.turn_id,
|
|
228
|
+
assignment_id: subject.assignment_id,
|
|
229
|
+
run_id: subject.run_id,
|
|
230
|
+
nonce_digest: subject.nonce_digest,
|
|
231
|
+
attempt_epoch: subject.attempt_epoch,
|
|
232
|
+
execution_contract_hash: subject.execution_contract_hash,
|
|
233
|
+
workspace_digest: subject.workspace_digest,
|
|
234
|
+
};
|
|
235
|
+
const mismatch = Object.keys(expected)
|
|
236
|
+
.find((field) => actual[field] !== expected[field]);
|
|
237
|
+
return mismatch ? `reconciled_authority_mismatch:${mismatch}` : undefined;
|
|
238
|
+
}
|
|
239
|
+
function selectEligible(thread, artifacts, purpose, cwd) {
|
|
240
|
+
const mode = thread.evidence_policy?.mode ?? 'legacy';
|
|
241
|
+
const requirement = GATE_POLICIES[thread.kind].requirements[purpose];
|
|
242
|
+
const eligible = [];
|
|
243
|
+
const accepted_evidence_ids = [];
|
|
244
|
+
const rejected = [];
|
|
245
|
+
const fingerprints = new Set();
|
|
246
|
+
for (const artifact of artifacts) {
|
|
247
|
+
// critic_signal is intentionally a typed marker; unlike deliverable
|
|
248
|
+
// artifacts it does not require an inline/ref payload.
|
|
249
|
+
if (purpose !== 'critic_signal' && !hasUsableContent(artifact)) {
|
|
250
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'no_usable_content' });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
// A present envelope is always validated, including on legacy threads.
|
|
254
|
+
if (artifact.evidence) {
|
|
255
|
+
const validity = validateArtifactEvidence(thread, artifact);
|
|
256
|
+
if (!validity.valid) {
|
|
257
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: validity.reasons.join(',') });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (mode === 'strict' || mode === 'shadow') {
|
|
262
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'missing_evidence' });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (mode !== 'legacy') {
|
|
266
|
+
const envelope = artifact.evidence;
|
|
267
|
+
if (purpose === 'reviewer_green' && (artifact.iteration ?? 0) !== thread.iteration_count) {
|
|
268
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'stale_subject_iteration' });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const channelAuthorities = requirement.authorities.filter((authority) => authority.channel === envelope.producer.channel);
|
|
272
|
+
if (channelAuthorities.length === 0) {
|
|
273
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: `channel_not_allowed:${envelope.producer.channel}` });
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const producerAuthorities = channelAuthorities.filter((authority) => authority.producer_kind === envelope.producer.kind);
|
|
277
|
+
if (producerAuthorities.length === 0) {
|
|
278
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: `producer_channel_mismatch:${envelope.producer.kind}:${envelope.producer.channel}` });
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const authority = producerAuthorities.find((candidate) => envelope.attestations.some((item) => item.kind === candidate.attestation
|
|
282
|
+
&& item.issuer === candidate.issuer
|
|
283
|
+
&& item.rights.includes(requirement.right)));
|
|
284
|
+
if (!authority) {
|
|
285
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: `missing_authorized_attestation:${requirement.right}` });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const missingSubject = authority.required_subject_fields?.find((field) => envelope.subject[field] === undefined || envelope.subject[field] === '');
|
|
289
|
+
if (missingSubject) {
|
|
290
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: `missing_subject_binding:${missingSubject}` });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (envelope.subject.slot_id) {
|
|
294
|
+
const slot = thread.slots.find((candidate) => candidate.slot_id === envelope.subject.slot_id);
|
|
295
|
+
if (!slot) {
|
|
296
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'unknown_subject_slot' });
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (slot.current_turn_id && envelope.subject.turn_id !== slot.current_turn_id) {
|
|
300
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'wrong_subject_turn' });
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (slot.assignment_id && envelope.subject.assignment_id !== slot.assignment_id) {
|
|
304
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'wrong_subject_assignment' });
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (slot.claim_id && envelope.subject.claim_id !== slot.claim_id) {
|
|
308
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'wrong_subject_claim' });
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (envelope.producer.channel === 'verify_command') {
|
|
313
|
+
let report;
|
|
314
|
+
try {
|
|
315
|
+
report = JSON.parse(artifact.body ?? '{}');
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
report = {};
|
|
319
|
+
}
|
|
320
|
+
if (report.workspace_stable !== true
|
|
321
|
+
|| !report.command_argv
|
|
322
|
+
|| evidenceDigest({ command: report.command_argv }) !== report.command_digest
|
|
323
|
+
|| (thread.protocol?.verify
|
|
324
|
+
&& evidenceDigest({ command: thread.protocol.verify.command }) !== report.command_digest)
|
|
325
|
+
|| report.command_digest !== envelope.subject.command_digest
|
|
326
|
+
|| report.workspace_digest !== envelope.subject.workspace_digest) {
|
|
327
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'verification_subject_mismatch' });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
if (!report.cwd || captureWorkspaceDigest(report.cwd) !== envelope.subject.workspace_digest) {
|
|
332
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'workspace_changed_after_verification' });
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'workspace_freshness_unavailable' });
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (envelope.producer.channel === 'reconcile_turn' && envelope.subject.execution_contract_hash === undefined) {
|
|
342
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'missing_subject_binding:execution_contract_hash' });
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const authorityRejection = reconciledV2AuthorityRejection(thread, artifact, cwd);
|
|
346
|
+
if (authorityRejection) {
|
|
347
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: authorityRejection });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (mode !== 'legacy') {
|
|
352
|
+
const fingerprint = payloadFingerprint(artifact);
|
|
353
|
+
if (fingerprints.has(fingerprint)) {
|
|
354
|
+
rejected.push({ artifact_id: artifact.artifact_id, reason: 'duplicate_payload' });
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
fingerprints.add(fingerprint);
|
|
358
|
+
}
|
|
359
|
+
eligible.push(artifact);
|
|
360
|
+
if (artifact.evidence)
|
|
361
|
+
accepted_evidence_ids.push(artifact.evidence.evidence_id);
|
|
362
|
+
}
|
|
363
|
+
return { eligible, accepted_evidence_ids, rejected };
|
|
364
|
+
}
|
|
365
|
+
function modeFor(thread) {
|
|
366
|
+
return thread.evidence_policy?.mode ?? 'legacy';
|
|
367
|
+
}
|
|
368
|
+
function decision(thread, condition, strictPassed, legacyPassed, set = { eligible: [], accepted_evidence_ids: [], rejected: [] }) {
|
|
369
|
+
const mode = modeFor(thread);
|
|
370
|
+
return {
|
|
371
|
+
passed: mode === 'legacy' || mode === 'shadow' ? legacyPassed : strictPassed,
|
|
372
|
+
strict_passed: strictPassed,
|
|
373
|
+
legacy_passed: legacyPassed,
|
|
374
|
+
policy_version: mode === 'legacy' ? 'legacy' : 'gate-policy-v1',
|
|
375
|
+
mode,
|
|
376
|
+
condition_digest: evidenceDigest(condition),
|
|
377
|
+
accepted_evidence_ids: [...new Set(set.accepted_evidence_ids)],
|
|
378
|
+
rejected: set.rejected,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function artifactCandidates(thread, condition) {
|
|
382
|
+
return thread.artifacts.filter((artifact) => {
|
|
383
|
+
if (artifact.type !== condition.type)
|
|
384
|
+
return false;
|
|
385
|
+
if (condition.kind === 'artifact_produced')
|
|
386
|
+
return artifact.phase === condition.phase;
|
|
387
|
+
if (condition.scope !== 'phase')
|
|
388
|
+
return true;
|
|
389
|
+
if (artifact.phase !== thread.current_phase)
|
|
390
|
+
return false;
|
|
391
|
+
const iterationAware = thread.iteration_count > 0 || thread.artifacts.some((item) => item.iteration !== undefined);
|
|
392
|
+
return !iterationAware || (artifact.iteration ?? 0) === thread.iteration_count;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/** One evaluator owns both terminal stop conditions and phase gates. */
|
|
396
|
+
export function evaluateGateCondition(thread, condition, cwd) {
|
|
397
|
+
if (!condition)
|
|
398
|
+
return decision(thread, { kind: 'absent' }, false, false);
|
|
399
|
+
switch (condition.kind) {
|
|
400
|
+
case 'phase_reached': {
|
|
401
|
+
const passed = thread.current_phase === condition.phase;
|
|
402
|
+
return decision(thread, condition, passed, passed);
|
|
403
|
+
}
|
|
404
|
+
case 'max_iterations':
|
|
405
|
+
case 'min_iterations': {
|
|
406
|
+
const passed = thread.iteration_count >= condition.n;
|
|
407
|
+
return decision(thread, condition, passed, passed);
|
|
408
|
+
}
|
|
409
|
+
case 'no_open_questions': {
|
|
410
|
+
const passed = thread.open_questions.length === 0;
|
|
411
|
+
return decision(thread, condition, passed, passed);
|
|
412
|
+
}
|
|
413
|
+
case 'manual':
|
|
414
|
+
return decision(thread, condition, false, false);
|
|
415
|
+
case 'reviewer_green': {
|
|
416
|
+
const candidates = thread.artifacts.filter((artifact) => artifact.type === 'verdict' && /^accepted(?:\b|[:\s])/.test((artifact.body ?? '').trim().toLowerCase()));
|
|
417
|
+
const set = selectEligible(thread, candidates, 'reviewer_green', cwd);
|
|
418
|
+
return decision(thread, condition, set.eligible.length > 0, legacyEligibleCount(thread, candidates, 'reviewer_green', cwd) > 0, set);
|
|
419
|
+
}
|
|
420
|
+
case 'artifact_produced': {
|
|
421
|
+
const candidates = artifactCandidates(thread, condition);
|
|
422
|
+
const set = selectEligible(thread, candidates, 'artifact', cwd);
|
|
423
|
+
return decision(thread, condition, set.eligible.length > 0, legacyEligibleCount(thread, candidates, 'artifact', cwd) > 0, set);
|
|
424
|
+
}
|
|
425
|
+
case 'min_artifacts_by_type': {
|
|
426
|
+
const candidates = artifactCandidates(thread, condition);
|
|
427
|
+
const set = selectEligible(thread, candidates, 'artifact', cwd);
|
|
428
|
+
return decision(thread, condition, set.eligible.length >= condition.n, legacyEligibleCount(thread, candidates, 'artifact', cwd) >= condition.n, set);
|
|
429
|
+
}
|
|
430
|
+
case 'any': {
|
|
431
|
+
const children = condition.conditions.map((child) => evaluateGateCondition(thread, child, cwd));
|
|
432
|
+
return decision(thread, condition, children.some((child) => child.strict_passed ?? child.passed), children.some((child) => child.legacy_passed ?? child.passed), {
|
|
433
|
+
eligible: [],
|
|
434
|
+
accepted_evidence_ids: children.flatMap((child) => child.accepted_evidence_ids),
|
|
435
|
+
rejected: children.flatMap((child) => child.rejected),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
case 'all': {
|
|
439
|
+
const children = condition.conditions.map((child) => evaluateGateCondition(thread, child, cwd));
|
|
440
|
+
return decision(thread, condition, children.every((child) => child.strict_passed ?? child.passed), children.every((child) => child.legacy_passed ?? child.passed), {
|
|
441
|
+
eligible: [],
|
|
442
|
+
accepted_evidence_ids: children.flatMap((child) => child.accepted_evidence_ids),
|
|
443
|
+
rejected: children.flatMap((child) => child.rejected),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
export function eligibleArtifactsForPurpose(thread, artifacts, purpose, cwd) {
|
|
449
|
+
return selectEligible(thread, artifacts, purpose, cwd);
|
|
450
|
+
}
|
|
451
|
+
export function evaluateCommandGreen(thread, iteration, cwd) {
|
|
452
|
+
const candidates = thread.artifacts.filter((artifact) => {
|
|
453
|
+
if (artifact.type !== 'verify_report' || (artifact.iteration ?? 0) !== iteration)
|
|
454
|
+
return false;
|
|
455
|
+
try {
|
|
456
|
+
return JSON.parse(artifact.body ?? '{}').passed === true;
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
const set = selectEligible(thread, candidates, 'command_green', cwd);
|
|
463
|
+
return decision(thread, { kind: 'command_green', iteration }, set.eligible.length > 0, legacyEligibleCount(thread, candidates, 'command_green', cwd) > 0, set);
|
|
464
|
+
}
|
|
465
|
+
export function evaluateCriticSignal(thread, iteration, cwd) {
|
|
466
|
+
const candidates = thread.artifacts.filter((artifact) => artifact.type === 'critic_signal' && (artifact.iteration ?? 0) === iteration);
|
|
467
|
+
const set = selectEligible(thread, candidates, 'critic_signal', cwd);
|
|
468
|
+
return decision(thread, { kind: 'critic_signal', iteration }, set.eligible.length > 0, legacyEligibleCount(thread, candidates, 'critic_signal', cwd) > 0, set);
|
|
469
|
+
}
|
|
470
|
+
export function evaluateNoNewCritique(thread, iteration, cwd) {
|
|
471
|
+
const candidates = thread.artifacts.filter((artifact) => artifact.type === 'critique' && (artifact.iteration ?? 0) === iteration);
|
|
472
|
+
const set = selectEligible(thread, candidates, 'artifact', cwd);
|
|
473
|
+
const closureCandidates = thread.artifacts.filter((artifact) => artifact.type === 'critique_window_closed' && (artifact.iteration ?? 0) === iteration);
|
|
474
|
+
const closure = selectEligible(thread, closureCandidates, 'artifact', cwd);
|
|
475
|
+
// Invalid/replayed critiques cannot be used to manufacture a negative convergence signal.
|
|
476
|
+
const strictPassed = set.eligible.length === 0
|
|
477
|
+
&& set.rejected.length === 0
|
|
478
|
+
&& closure.eligible.length > 0;
|
|
479
|
+
return decision(thread, { kind: 'no_new_critique_artifacts', iteration }, strictPassed, candidates.length === 0, {
|
|
480
|
+
eligible: closure.eligible,
|
|
481
|
+
accepted_evidence_ids: [...set.accepted_evidence_ids, ...closure.accepted_evidence_ids],
|
|
482
|
+
rejected: [...set.rejected, ...closure.rejected],
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
//# sourceMappingURL=gate-policy.js.map
|
|
@@ -1,32 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pln#632 impl-loop bind — the ENGINE action for an implementation loop's `bind` phase.
|
|
3
|
-
*
|
|
4
|
-
* The implementation protocol declares `bind` as "bind plan+sequence and dispatch" (see
|
|
5
|
-
* LOOP_PROTOCOLS.implementation in types.ts). This is that action: read the loop's linked
|
|
6
|
-
* sequence, dispatch its ready lanes via the EXISTING sequence spawner, then advance
|
|
7
|
-
* `bind → execute` so the loop enters its execute↔verify cycle.
|
|
8
|
-
*
|
|
9
|
-
* Reuses `dispatch()` ADDITIVELY via its `sequenceId` option (pln#632) so the loop drives
|
|
10
|
-
* its OWN linked sequence WITHOUT touching the project's global active-sequence pointer —
|
|
11
|
-
* no hijack of whatever sequence other bclaw_dispatch work is using. Mirrors the
|
|
12
|
-
* async-handler-spawns pattern already used by bclaw_coordinate(open_loop=true) for review
|
|
13
|
-
* loops; touches NO review/ideation dispatch. The live spawn happens only when this runs
|
|
14
|
-
* on a real loop (exactly like bclaw_dispatch) — `dryRun` previews with no spawn and no
|
|
15
|
-
* phase mutation, so the flow is unit-testable.
|
|
16
|
-
*/
|
|
17
|
-
import { dispatch } from '../dispatcher.js';
|
|
18
1
|
import { listSequences } from '../sequence.js';
|
|
2
|
+
import { withLoopLock } from './lock.js';
|
|
19
3
|
import { getLoop } from './store.js';
|
|
20
4
|
import { advance } from './verbs.js';
|
|
21
|
-
|
|
5
|
+
const ENGINE_ONLY_WARNING = 'implementation bind is engine-only and does not dispatch workers; use bclaw_loop(intent="turn", dispatch=true, slot_id=...) in execute';
|
|
6
|
+
function compatibilityWarnings(input) {
|
|
7
|
+
const usedLaunchOption = input.lanes !== undefined
|
|
8
|
+
|| input.autoExecute !== undefined
|
|
9
|
+
|| input.model !== undefined
|
|
10
|
+
|| input.maxAssignments !== undefined;
|
|
11
|
+
return usedLaunchOption
|
|
12
|
+
? [`${ENGINE_ONLY_WARNING}; bind launch options are retained but ignored`]
|
|
13
|
+
: [ENGINE_ONLY_WARNING];
|
|
14
|
+
}
|
|
22
15
|
/**
|
|
23
|
-
*
|
|
24
|
-
* underlying spawn is async (the only awaited work; `dryRun` resolves synchronously).
|
|
16
|
+
* Validate an implementation loop's linked sequence and advance to execute.
|
|
25
17
|
*
|
|
26
|
-
* Idempotent: a loop already past `bind` returns
|
|
27
|
-
*
|
|
28
|
-
* so a retry re-dispatches only the still-unassigned lanes (dispatch skips lanes with an
|
|
29
|
-
* active assignment) and re-attempts the advance — safe crash-recovery.
|
|
18
|
+
* Idempotent: a loop already past `bind` returns `noop`. The phase re-check and
|
|
19
|
+
* advance happen under the loop lock, so racing bind calls cannot advance twice.
|
|
30
20
|
*/
|
|
31
21
|
export async function runImplBind(input, cwd) {
|
|
32
22
|
const { loop_id, dispatcherAgent } = input;
|
|
@@ -36,16 +26,10 @@ export async function runImplBind(input, cwd) {
|
|
|
36
26
|
if (loop.kind !== 'implementation') {
|
|
37
27
|
throw new Error(`bind is only valid for implementation loops (loop ${loop_id} is kind='${loop.kind}'); review/ideation loops dispatch via bclaw_coordinate`);
|
|
38
28
|
}
|
|
39
|
-
// bind SPAWNS real workers via dispatch() — never do that on a loop that is not open.
|
|
40
|
-
// A paused loop keeps current_phase='bind' (pause only flips status), and open→close
|
|
41
|
-
// leaves the phase at 'bind' too, so the phase-idempotency check below is NOT sufficient
|
|
42
|
-
// to stop a spawn on a held/terminal loop (review Finding 1). Gate on status FIRST, so
|
|
43
|
-
// no dispatch fires and we don't spawn then throw at the advance.
|
|
44
29
|
if (loop.status !== 'open') {
|
|
45
30
|
throw new Error(`bind requires an open loop; loop ${loop_id} is ${loop.status} (resume a paused loop, or reopen a terminal one, before binding)`);
|
|
46
31
|
}
|
|
47
32
|
const sequenceId = loop.linked?.sequence_ids?.[0];
|
|
48
|
-
// Idempotency: `bind` is the loop's FIRST phase. A loop already past it was bound before.
|
|
49
33
|
if (loop.current_phase !== 'bind') {
|
|
50
34
|
return {
|
|
51
35
|
loop_id,
|
|
@@ -53,58 +37,27 @@ export async function runImplBind(input, cwd) {
|
|
|
53
37
|
action: 'noop',
|
|
54
38
|
dispatch: null,
|
|
55
39
|
messages_sent: 0,
|
|
56
|
-
|
|
40
|
+
warnings: compatibilityWarnings(input),
|
|
41
|
+
reason: `loop is in phase '${loop.current_phase}', not 'bind' - already bound (idempotent)`,
|
|
57
42
|
};
|
|
58
43
|
}
|
|
59
44
|
if (!sequenceId) {
|
|
60
|
-
throw new Error(`impl-bind requires a linked sequence: open the implementation loop with linked.sequence_ids=[
|
|
45
|
+
throw new Error(`impl-bind requires a linked sequence: open the implementation loop with linked.sequence_ids=[...] (the sequence whose lanes it executes). None found on ${loop_id}.`);
|
|
61
46
|
}
|
|
62
|
-
|
|
63
|
-
// message rather than a silent null dispatch.
|
|
64
|
-
const seq = listSequences(cwd).find((s) => s.id === sequenceId);
|
|
65
|
-
if (!seq)
|
|
47
|
+
if (!listSequences(cwd).some((sequence) => sequence.id === sequenceId)) {
|
|
66
48
|
throw new Error(`linked sequence ${sequenceId} not found for loop ${loop_id}`);
|
|
67
|
-
const dispatched = await dispatch({
|
|
68
|
-
sequenceId,
|
|
69
|
-
dispatcherAgent,
|
|
70
|
-
dispatcherAgentId: input.dispatcherAgentId,
|
|
71
|
-
sessionId: input.sessionId,
|
|
72
|
-
dryRun: input.dryRun,
|
|
73
|
-
lanes: input.lanes,
|
|
74
|
-
autoExecute: input.autoExecute,
|
|
75
|
-
model: input.model,
|
|
76
|
-
maxAssignments: input.maxAssignments,
|
|
77
|
-
}, cwd ?? process.cwd());
|
|
78
|
-
const result = dispatched?.result ?? null;
|
|
79
|
-
const messages_sent = result?.messages_sent.length ?? 0;
|
|
80
|
-
// Guard a race: the sequence disappeared between validation and dispatch → don't advance.
|
|
81
|
-
if (!dispatched) {
|
|
82
|
-
return {
|
|
83
|
-
loop_id,
|
|
84
|
-
sequence_id: sequenceId,
|
|
85
|
-
action: 'noop',
|
|
86
|
-
dispatch: null,
|
|
87
|
-
messages_sent: 0,
|
|
88
|
-
reason: `sequence ${sequenceId} yielded no dispatch analysis (unavailable); loop stays in 'bind'`,
|
|
89
|
-
};
|
|
90
49
|
}
|
|
91
50
|
if (input.dryRun) {
|
|
92
51
|
return {
|
|
93
52
|
loop_id,
|
|
94
53
|
sequence_id: sequenceId,
|
|
95
54
|
action: 'preview',
|
|
96
|
-
dispatch:
|
|
97
|
-
messages_sent,
|
|
98
|
-
|
|
55
|
+
dispatch: null,
|
|
56
|
+
messages_sent: 0,
|
|
57
|
+
warnings: compatibilityWarnings(input),
|
|
58
|
+
reason: `dry run: linked sequence ${sequenceId} is valid; loop stays in 'bind' and no worker is dispatched`,
|
|
99
59
|
};
|
|
100
60
|
}
|
|
101
|
-
// Real bind: advance bind → execute so the loop enters the execute↔verify cycle. The
|
|
102
|
-
// implementation protocol's `bind` phase carries no advance_gate, so the advance is
|
|
103
|
-
// unconditional FROM bind — but the phase-check and the advance must be ATOMIC or two
|
|
104
|
-
// racing binds each advance once and push the loop bind→execute→verify with no execute
|
|
105
|
-
// work done (review Finding 2). advance() does not self-lock (its callers — the facade's
|
|
106
|
-
// withLockedLoopMutation and the ideation closer — provide the lock), so we take the loop
|
|
107
|
-
// lock here and re-check the phase under it: only the bind that still sees 'bind' advances.
|
|
108
61
|
const advanced = withLoopLock({
|
|
109
62
|
cwd,
|
|
110
63
|
intent: 'impl-bind-advance',
|
|
@@ -112,12 +65,15 @@ export async function runImplBind(input, cwd) {
|
|
|
112
65
|
scope: { kind: 'loop', loopId: loop_id },
|
|
113
66
|
work: () => {
|
|
114
67
|
const fresh = getLoop(loop_id, cwd);
|
|
115
|
-
// Raced: a concurrent bind (or a pause/close) moved the loop out of an open 'bind'
|
|
116
|
-
// state → do NOT advance again. Idempotent by construction.
|
|
117
68
|
if (!fresh || fresh.status !== 'open' || fresh.current_phase !== 'bind')
|
|
118
69
|
return null;
|
|
119
|
-
const
|
|
120
|
-
|
|
70
|
+
const freshSequenceId = fresh.linked?.sequence_ids?.[0];
|
|
71
|
+
if (freshSequenceId !== sequenceId
|
|
72
|
+
|| !listSequences(cwd).some((sequence) => sequence.id === sequenceId)) {
|
|
73
|
+
throw new Error(`linked sequence ${sequenceId} changed or disappeared before bind could advance`);
|
|
74
|
+
}
|
|
75
|
+
const result = advance({ id: loop_id, actor: dispatcherAgent }, cwd);
|
|
76
|
+
return { phase: result.loop.current_phase, auto_closed: result.auto_closed };
|
|
121
77
|
},
|
|
122
78
|
});
|
|
123
79
|
if (!advanced) {
|
|
@@ -125,9 +81,10 @@ export async function runImplBind(input, cwd) {
|
|
|
125
81
|
loop_id,
|
|
126
82
|
sequence_id: sequenceId,
|
|
127
83
|
action: 'noop',
|
|
128
|
-
dispatch:
|
|
129
|
-
messages_sent,
|
|
130
|
-
|
|
84
|
+
dispatch: null,
|
|
85
|
+
messages_sent: 0,
|
|
86
|
+
warnings: compatibilityWarnings(input),
|
|
87
|
+
reason: `phase already advanced out of 'bind' under a concurrent bind (idempotent - not re-advanced)`,
|
|
131
88
|
};
|
|
132
89
|
}
|
|
133
90
|
return {
|
|
@@ -136,9 +93,10 @@ export async function runImplBind(input, cwd) {
|
|
|
136
93
|
action: 'bound',
|
|
137
94
|
advanced_to: advanced.phase,
|
|
138
95
|
auto_closed: advanced.auto_closed,
|
|
139
|
-
dispatch:
|
|
140
|
-
messages_sent,
|
|
141
|
-
|
|
96
|
+
dispatch: null,
|
|
97
|
+
messages_sent: 0,
|
|
98
|
+
warnings: compatibilityWarnings(input),
|
|
99
|
+
reason: `validated linked sequence ${sequenceId}; advanced bind -> ${advanced.phase}; dispatch worker slots with turn(dispatch=true)`,
|
|
142
100
|
};
|
|
143
101
|
}
|
|
144
102
|
//# sourceMappingURL=impl-bind.js.map
|