brainclaw 1.26.2 → 1.28.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 +143 -15
- package/dist/commands/mcp-catalog.js +52 -18
- package/dist/commands/mcp-schemas.generated.js +64 -0
- package/dist/commands/mcp-write-claims.js +128 -1
- package/dist/commands/mcp-write-coordination.js +149 -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 +189 -14
- package/dist/core/execution-contract.js +345 -0
- package/dist/core/execution.js +130 -16
- package/dist/core/facade-schema.js +3 -0
- 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 +235 -0
- package/dist/core/loops/artifact-contract.js +11 -0
- package/dist/core/loops/attempt-authority.js +496 -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/brief-assembly.js +21 -4
- package/dist/core/loops/evidence.js +188 -0
- package/dist/core/loops/facade-schema.js +75 -11
- package/dist/core/loops/gate-policy.js +533 -0
- package/dist/core/loops/impl-bind.js +91 -81
- 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 +237 -18
- package/dist/core/loops/result-reducers.js +113 -10
- package/dist/core/loops/store.js +34 -3
- package/dist/core/loops/turn-execution.js +480 -0
- package/dist/core/loops/types.js +127 -3
- package/dist/core/loops/verbs.js +335 -99
- package/dist/core/loops/verify-command.js +105 -20
- 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 +40 -0
- package/dist/core/spawn-check.js +3 -2
- package/dist/core/upgrades/backup.js +27 -4
- package/dist/facts.js +9 -8
- package/dist/facts.json +8 -7
- 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 +174 -0
- package/docs/loops/research.md +136 -0
- package/docs/loops/review.md +200 -0
- package/docs/mcp-schema-changelog.md +18 -5
- package/package.json +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
export const GATE_POLICY_VERSION = 'gate-policy-v1';
|
|
3
|
+
function canonicalize(value) {
|
|
4
|
+
if (Array.isArray(value))
|
|
5
|
+
return value.map(canonicalize);
|
|
6
|
+
if (value && typeof value === 'object') {
|
|
7
|
+
return Object.fromEntries(Object.entries(value)
|
|
8
|
+
.filter(([, child]) => child !== undefined)
|
|
9
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
10
|
+
.map(([key, child]) => [key, canonicalize(child)]));
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
export function evidenceDigest(value) {
|
|
15
|
+
return crypto.createHash('sha256').update(JSON.stringify(canonicalize(value))).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
export function artifactEvidenceDigest(artifact) {
|
|
18
|
+
return evidenceDigest({
|
|
19
|
+
artifact_id: artifact.artifact_id,
|
|
20
|
+
phase: artifact.phase,
|
|
21
|
+
type: artifact.type,
|
|
22
|
+
ref: artifact.ref,
|
|
23
|
+
body: artifact.body,
|
|
24
|
+
produced_by: artifact.produced_by,
|
|
25
|
+
produced_at: artifact.produced_at,
|
|
26
|
+
addresses_critique: artifact.addresses_critique,
|
|
27
|
+
implementation_verify: artifact.implementation_verify,
|
|
28
|
+
iteration: artifact.iteration ?? 0,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Classify both new and historical artifacts without a migration rewrite.
|
|
33
|
+
* Old sealed envelopes predate the explicit field but are still attested;
|
|
34
|
+
* old unsealed records remain legacy. The marker is intentionally excluded
|
|
35
|
+
* from artifactEvidenceDigest so adding it cannot invalidate a v1 seal.
|
|
36
|
+
*/
|
|
37
|
+
export function artifactEvidenceProvenance(artifact) {
|
|
38
|
+
return artifact.provenance ?? (artifact.evidence ? 'attested' : 'legacy');
|
|
39
|
+
}
|
|
40
|
+
function isAcceptedVerdict(artifact) {
|
|
41
|
+
return artifact.type === 'verdict' && /^accepted(?:\b|[:\s])/.test((artifact.body ?? '').trim().toLowerCase());
|
|
42
|
+
}
|
|
43
|
+
function attestation(kind, issuer, issued_at, subject_digest, rights) {
|
|
44
|
+
return { kind, issuer, issued_at, subject_digest, rights };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Bind evidence at the server-controlled artifact commit boundary. Callers
|
|
48
|
+
* provide identity context, never an envelope or rights.
|
|
49
|
+
*/
|
|
50
|
+
export function sealArtifactEvidence(thread, artifact, context) {
|
|
51
|
+
const committedArtifact = {
|
|
52
|
+
...artifact,
|
|
53
|
+
produced_by: context.producer_id,
|
|
54
|
+
iteration: artifact.iteration ?? thread.iteration_count,
|
|
55
|
+
};
|
|
56
|
+
const iteration = committedArtifact.iteration ?? thread.iteration_count;
|
|
57
|
+
const subject = {
|
|
58
|
+
loop_id: thread.id,
|
|
59
|
+
artifact_id: artifact.artifact_id,
|
|
60
|
+
phase: artifact.phase,
|
|
61
|
+
iteration,
|
|
62
|
+
slot_id: context.slot_id,
|
|
63
|
+
turn_id: context.turn_id,
|
|
64
|
+
assignment_id: context.assignment_id,
|
|
65
|
+
claim_id: context.claim_id,
|
|
66
|
+
run_id: context.run_id,
|
|
67
|
+
nonce_digest: context.nonce ? evidenceDigest({ launch_nonce: context.nonce }) : undefined,
|
|
68
|
+
attempt_epoch: context.attempt_epoch,
|
|
69
|
+
execution_contract_hash: context.execution_contract_hash,
|
|
70
|
+
command_digest: context.command_digest,
|
|
71
|
+
workspace_digest: context.workspace_digest,
|
|
72
|
+
};
|
|
73
|
+
const subjectDigest = evidenceDigest(subject);
|
|
74
|
+
const attestations = [];
|
|
75
|
+
if (context.claim_id) {
|
|
76
|
+
attestations.push(attestation('claim', 'brainclaw:claim-binding', committedArtifact.produced_at, subjectDigest, ['subject:claim']));
|
|
77
|
+
}
|
|
78
|
+
if (context.channel === 'verify_command' && context.producer_kind === 'engine') {
|
|
79
|
+
attestations.push(attestation('verification', 'brainclaw:verify-command', committedArtifact.produced_at, subjectDigest, ['artifact:write', 'gate:artifact', 'gate:command_green']));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const rights = context.channel === 'add_artifact'
|
|
83
|
+
? ['artifact:write']
|
|
84
|
+
: ['artifact:write', 'gate:artifact'];
|
|
85
|
+
attestations.push(attestation('observation', 'brainclaw:artifact-commit', committedArtifact.produced_at, subjectDigest, rights));
|
|
86
|
+
}
|
|
87
|
+
if (isAcceptedVerdict(committedArtifact) &&
|
|
88
|
+
(context.channel === 'complete_turn' || context.channel === 'reconcile_turn') &&
|
|
89
|
+
/review/i.test(context.slot_role ?? '')) {
|
|
90
|
+
attestations.push(attestation('approval', 'brainclaw:review-slot', committedArtifact.produced_at, subjectDigest, ['gate:reviewer_green']));
|
|
91
|
+
}
|
|
92
|
+
const unsigned = {
|
|
93
|
+
version: 1,
|
|
94
|
+
evidence_id: `evd_${crypto.randomBytes(8).toString('hex')}`,
|
|
95
|
+
evidence_type: 'artifact_commit',
|
|
96
|
+
policy_version: GATE_POLICY_VERSION,
|
|
97
|
+
subject,
|
|
98
|
+
producer: {
|
|
99
|
+
kind: context.producer_kind,
|
|
100
|
+
id: context.producer_id,
|
|
101
|
+
agent_id: context.agent_id,
|
|
102
|
+
channel: context.channel,
|
|
103
|
+
},
|
|
104
|
+
artifact_digest: artifactEvidenceDigest(committedArtifact),
|
|
105
|
+
issued_at: committedArtifact.produced_at,
|
|
106
|
+
observed_at: committedArtifact.produced_at,
|
|
107
|
+
validity: { not_before: committedArtifact.produced_at },
|
|
108
|
+
attestations,
|
|
109
|
+
};
|
|
110
|
+
const envelope = {
|
|
111
|
+
...unsigned,
|
|
112
|
+
seal: { algorithm: 'sha256', digest: evidenceDigest(unsigned) },
|
|
113
|
+
};
|
|
114
|
+
return { ...committedArtifact, provenance: 'attested', evidence: envelope };
|
|
115
|
+
}
|
|
116
|
+
/** Validate every binding before an envelope may influence a gate. */
|
|
117
|
+
export function validateArtifactEvidence(thread, artifact, now = new Date()) {
|
|
118
|
+
const envelope = artifact.evidence;
|
|
119
|
+
if (!envelope)
|
|
120
|
+
return { valid: false, reasons: ['missing_evidence'] };
|
|
121
|
+
const reasons = [];
|
|
122
|
+
const { seal, ...unsigned } = envelope;
|
|
123
|
+
if (seal.algorithm !== 'sha256' || evidenceDigest(unsigned) !== seal.digest)
|
|
124
|
+
reasons.push('invalid_seal');
|
|
125
|
+
const { evidence: _ignored, ...unsignedArtifact } = artifact;
|
|
126
|
+
void _ignored;
|
|
127
|
+
if (artifactEvidenceDigest(unsignedArtifact) !== envelope.artifact_digest)
|
|
128
|
+
reasons.push('artifact_digest_mismatch');
|
|
129
|
+
if (envelope.subject.loop_id !== thread.id)
|
|
130
|
+
reasons.push('wrong_loop_subject');
|
|
131
|
+
if (envelope.subject.artifact_id !== artifact.artifact_id)
|
|
132
|
+
reasons.push('wrong_artifact_subject');
|
|
133
|
+
if (envelope.subject.phase !== artifact.phase)
|
|
134
|
+
reasons.push('wrong_phase_subject');
|
|
135
|
+
if (envelope.subject.iteration !== (artifact.iteration ?? 0))
|
|
136
|
+
reasons.push('wrong_iteration_subject');
|
|
137
|
+
if (envelope.issued_at !== artifact.produced_at)
|
|
138
|
+
reasons.push('issued_at_mismatch');
|
|
139
|
+
if (envelope.observed_at !== artifact.produced_at)
|
|
140
|
+
reasons.push('observed_at_mismatch');
|
|
141
|
+
const issued = Date.parse(envelope.issued_at);
|
|
142
|
+
const loopCreated = Date.parse(thread.created_at);
|
|
143
|
+
if (!Number.isFinite(issued))
|
|
144
|
+
reasons.push('invalid_issued_at');
|
|
145
|
+
if (Number.isFinite(issued) && Number.isFinite(loopCreated) && issued < loopCreated - 5 * 60_000)
|
|
146
|
+
reasons.push('stale_before_loop');
|
|
147
|
+
if (Number.isFinite(issued) && issued > now.getTime() + 5 * 60_000)
|
|
148
|
+
reasons.push('issued_in_future');
|
|
149
|
+
const notBefore = Date.parse(envelope.validity.not_before);
|
|
150
|
+
const notAfter = envelope.validity.not_after ? Date.parse(envelope.validity.not_after) : undefined;
|
|
151
|
+
if (!Number.isFinite(notBefore) || issued < notBefore)
|
|
152
|
+
reasons.push('outside_validity_window');
|
|
153
|
+
if (notAfter !== undefined && (!Number.isFinite(notAfter) || now.getTime() > notAfter))
|
|
154
|
+
reasons.push('outside_validity_window');
|
|
155
|
+
if (notAfter !== undefined && Number.isFinite(notBefore) && Number.isFinite(notAfter) && notAfter < notBefore)
|
|
156
|
+
reasons.push('invalid_validity_window');
|
|
157
|
+
if (artifact.produced_by !== envelope.producer.id)
|
|
158
|
+
reasons.push('producer_binding_mismatch');
|
|
159
|
+
const subjectDigest = evidenceDigest(envelope.subject);
|
|
160
|
+
for (const item of envelope.attestations) {
|
|
161
|
+
if (item.subject_digest !== subjectDigest)
|
|
162
|
+
reasons.push(`attestation_subject_mismatch:${item.kind}`);
|
|
163
|
+
if (item.issued_at !== envelope.issued_at)
|
|
164
|
+
reasons.push(`attestation_time_mismatch:${item.kind}`);
|
|
165
|
+
}
|
|
166
|
+
return { valid: reasons.length === 0, reasons, evidence_id: envelope.evidence_id };
|
|
167
|
+
}
|
|
168
|
+
export function validateThreadEvidence(thread) {
|
|
169
|
+
return thread.artifacts.flatMap((artifact) => {
|
|
170
|
+
if (!artifact.evidence)
|
|
171
|
+
return [];
|
|
172
|
+
const result = validateArtifactEvidence(thread, artifact);
|
|
173
|
+
return result.valid ? [] : [{ artifact_id: artifact.artifact_id, reasons: result.reasons }];
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** Feature rollout: new loops are strict unless the writer is explicitly disabled or shadowed. */
|
|
177
|
+
export function evidencePolicyForNewLoop(env = process.env) {
|
|
178
|
+
const configured = env.BRAINCLAW_EVIDENCE_ENVELOPES?.trim().toLowerCase();
|
|
179
|
+
if (configured === 'off')
|
|
180
|
+
return undefined;
|
|
181
|
+
if (configured === 'shadow')
|
|
182
|
+
return { version: GATE_POLICY_VERSION, mode: 'shadow' };
|
|
183
|
+
return { version: GATE_POLICY_VERSION, mode: 'strict' };
|
|
184
|
+
}
|
|
185
|
+
export function evidenceWriterEnabled(env = process.env) {
|
|
186
|
+
return env.BRAINCLAW_EVIDENCE_ENVELOPES?.trim().toLowerCase() !== 'off';
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=evidence.js.map
|
|
@@ -68,7 +68,11 @@ export const BclawLoopTurnSchema = z.object({
|
|
|
68
68
|
assignment_id: z.string().optional(),
|
|
69
69
|
/** pln#562 step 4 — claim binding the turn's slot to a dispatched instance. */
|
|
70
70
|
claim_id: z.string().optional(),
|
|
71
|
+
/** Trusted production driver: claim + AttemptAuthority + message + worker spawn. */
|
|
71
72
|
dispatch: z.boolean().optional(),
|
|
73
|
+
auto_execute: z.boolean().optional(),
|
|
74
|
+
model: z.string().min(1).optional(),
|
|
75
|
+
target_agents: z.array(z.string().min(1)).min(1).optional(),
|
|
72
76
|
expected_version: z.number().int().nonnegative().optional(),
|
|
73
77
|
...CallerEnvelopeFields,
|
|
74
78
|
});
|
|
@@ -76,6 +80,18 @@ export const BclawLoopCompleteTurnSchema = z.object({
|
|
|
76
80
|
intent: z.literal('complete_turn'),
|
|
77
81
|
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
78
82
|
slot_id: z.string().min(1),
|
|
83
|
+
/**
|
|
84
|
+
* AttemptAuthority fence. These fields are optional at the transport layer
|
|
85
|
+
* so pre-v2/legacy turns remain completable, but the verb requires the whole
|
|
86
|
+
* tuple whenever the slot is backed by AttemptAuthority v2.
|
|
87
|
+
*/
|
|
88
|
+
assignment_id: z.string().min(1).optional(),
|
|
89
|
+
turn_id: z.string().min(1).optional(),
|
|
90
|
+
run_id: z.string().min(1).optional(),
|
|
91
|
+
nonce: z.string().min(1).optional(),
|
|
92
|
+
attempt_epoch: z.number().int().nonnegative().optional(),
|
|
93
|
+
execution_contract_hash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
94
|
+
workspace_digest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
79
95
|
outcome: z.enum(['done', 'failed', 'cancelled']).optional(),
|
|
80
96
|
failure_reason: z.string().optional(),
|
|
81
97
|
artifact: z
|
|
@@ -86,11 +102,39 @@ export const BclawLoopCompleteTurnSchema = z.object({
|
|
|
86
102
|
ref: LoopRefSchema.optional(),
|
|
87
103
|
/** pln#492 synthesis audit trail. Required when type === 'plan_draft'. */
|
|
88
104
|
addresses_critique: z.array(z.string().min(1)).optional(),
|
|
105
|
+
implementation_verify: z
|
|
106
|
+
.object({
|
|
107
|
+
command: z.array(z.string().min(1)).min(1),
|
|
108
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
109
|
+
})
|
|
110
|
+
.optional(),
|
|
111
|
+
})
|
|
112
|
+
.superRefine((artifact, ctx) => {
|
|
113
|
+
if (artifact.type === 'plan_draft' && !artifact.implementation_verify) {
|
|
114
|
+
ctx.addIssue({
|
|
115
|
+
code: z.ZodIssueCode.custom,
|
|
116
|
+
message: "plan_draft requires implementation_verify for deterministic downstream verification",
|
|
117
|
+
path: ['implementation_verify'],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
89
120
|
})
|
|
90
121
|
.optional(),
|
|
91
122
|
expected_version: z.number().int().nonnegative().optional(),
|
|
92
123
|
...CallerEnvelopeFields,
|
|
93
124
|
});
|
|
125
|
+
export const BclawLoopTakeoverSchema = z.object({
|
|
126
|
+
intent: z.literal('takeover'),
|
|
127
|
+
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
128
|
+
slot_id: z.string().min(1),
|
|
129
|
+
turn_id: z.string().min(1),
|
|
130
|
+
expected_epoch: z.number().int().nonnegative(),
|
|
131
|
+
cause: z.string().min(1),
|
|
132
|
+
liveness_evidence: z.string().min(1),
|
|
133
|
+
external_effect_policy: z.enum(['none', 'idempotent', 'externally_fenced']),
|
|
134
|
+
next_workspace_path: z.string().min(1),
|
|
135
|
+
takeover_mode: z.enum(['takeover', 'retry']).optional(),
|
|
136
|
+
...CallerEnvelopeFields,
|
|
137
|
+
});
|
|
94
138
|
export const BclawLoopAdvanceSchema = z.object({
|
|
95
139
|
intent: z.literal('advance'),
|
|
96
140
|
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
@@ -103,14 +147,29 @@ export const BclawLoopAdvanceSchema = z.object({
|
|
|
103
147
|
export const BclawLoopAddArtifactSchema = z.object({
|
|
104
148
|
intent: z.literal('add_artifact'),
|
|
105
149
|
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
106
|
-
artifact: z
|
|
150
|
+
artifact: z
|
|
151
|
+
.object({
|
|
107
152
|
phase: z.string().min(1),
|
|
108
153
|
type: z.string().min(1),
|
|
109
154
|
body: z.string().optional(),
|
|
110
|
-
produced_by: z.string().optional(),
|
|
111
155
|
ref: LoopRefSchema.optional(),
|
|
112
156
|
/** pln#492 synthesis audit trail. Required when type === 'plan_draft'. */
|
|
113
157
|
addresses_critique: z.array(z.string().min(1)).optional(),
|
|
158
|
+
implementation_verify: z
|
|
159
|
+
.object({
|
|
160
|
+
command: z.array(z.string().min(1)).min(1),
|
|
161
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
162
|
+
})
|
|
163
|
+
.optional(),
|
|
164
|
+
})
|
|
165
|
+
.superRefine((artifact, ctx) => {
|
|
166
|
+
if (artifact.type === 'plan_draft' && !artifact.implementation_verify) {
|
|
167
|
+
ctx.addIssue({
|
|
168
|
+
code: z.ZodIssueCode.custom,
|
|
169
|
+
message: "plan_draft requires implementation_verify for deterministic downstream verification",
|
|
170
|
+
path: ['implementation_verify'],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
114
173
|
}),
|
|
115
174
|
expected_version: z.number().int().nonnegative().optional(),
|
|
116
175
|
...CallerEnvelopeFields,
|
|
@@ -145,29 +204,32 @@ export const BclawLoopCloseSchema = z.object({
|
|
|
145
204
|
export const BclawLoopVerifySchema = z.object({
|
|
146
205
|
intent: z.literal('verify'),
|
|
147
206
|
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
207
|
+
/** Required when an implementation loop has more than one bound lane. */
|
|
208
|
+
slot_id: z.string().regex(/^lsl_[0-9a-z]+$/).optional(),
|
|
148
209
|
// No expected_version: runVerify is idempotent by (loop, iteration) via its own
|
|
149
210
|
// two-lock re-check, not optimistic-concurrency CAS (review F3).
|
|
150
211
|
...CallerEnvelopeFields,
|
|
151
212
|
});
|
|
152
213
|
/**
|
|
153
214
|
* pln#632 — `bclaw_loop(intent='bind')`: the ENGINE action for an implementation loop's
|
|
154
|
-
* `bind` phase.
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
215
|
+
* `bind` phase. Validates the loop's linked sequence and advances
|
|
216
|
+
* `bind → execute`; it never dispatches a worker. Idempotent (a loop past
|
|
217
|
+
* `bind` → noop). `dry_run` validates without advancing. Historical launch
|
|
218
|
+
* options remain accepted but are ignored during migration; worker launch is
|
|
219
|
+
* exclusively `turn(dispatch=true)` through AttemptAuthority.
|
|
158
220
|
*/
|
|
159
221
|
export const BclawLoopBindSchema = z.object({
|
|
160
222
|
intent: z.literal('bind'),
|
|
161
223
|
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
162
|
-
/**
|
|
224
|
+
/** Validate the linked sequence; no advance. Bind never spawns. */
|
|
163
225
|
dry_run: z.boolean().optional(),
|
|
164
|
-
/**
|
|
226
|
+
/** @deprecated Retained for compatibility and ignored. */
|
|
165
227
|
lanes: z.array(z.string().min(1)).optional(),
|
|
166
|
-
/**
|
|
228
|
+
/** @deprecated Retained for compatibility and ignored. */
|
|
167
229
|
auto_execute: z.boolean().optional(),
|
|
168
|
-
/**
|
|
230
|
+
/** @deprecated Retained for compatibility and ignored. */
|
|
169
231
|
model: z.string().min(1).optional(),
|
|
170
|
-
/**
|
|
232
|
+
/** @deprecated Retained for compatibility and ignored. */
|
|
171
233
|
max_assignments: z.number().int().positive().optional(),
|
|
172
234
|
// No expected_version: bind is idempotent by loop phase (past `bind` → noop), not CAS.
|
|
173
235
|
...CallerEnvelopeFields,
|
|
@@ -236,6 +298,7 @@ export const BclawLoopRequestSchema = z.discriminatedUnion('intent', [
|
|
|
236
298
|
BclawLoopListSchema,
|
|
237
299
|
BclawLoopTurnSchema,
|
|
238
300
|
BclawLoopCompleteTurnSchema,
|
|
301
|
+
BclawLoopTakeoverSchema,
|
|
239
302
|
BclawLoopAdvanceSchema,
|
|
240
303
|
BclawLoopAddArtifactSchema,
|
|
241
304
|
BclawLoopPauseSchema,
|
|
@@ -252,6 +315,7 @@ export const BCLAW_LOOP_INTENTS = [
|
|
|
252
315
|
'list',
|
|
253
316
|
'turn',
|
|
254
317
|
'complete_turn',
|
|
318
|
+
'takeover',
|
|
255
319
|
'advance',
|
|
256
320
|
'add_artifact',
|
|
257
321
|
'pause',
|