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.
Files changed (86) hide show
  1. package/README.md +13 -0
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-coordination.js +65 -1
  4. package/dist/commands/attempt-authority.js +80 -0
  5. package/dist/commands/harvest.js +140 -61
  6. package/dist/commands/loop.js +34 -0
  7. package/dist/commands/loops-handlers.js +87 -14
  8. package/dist/commands/mcp-catalog.js +42 -18
  9. package/dist/commands/mcp-schemas.generated.js +44 -0
  10. package/dist/commands/mcp-write-claims.js +128 -1
  11. package/dist/commands/mcp-write-coordination.js +146 -76
  12. package/dist/core/agent-capability.js +1 -1
  13. package/dist/core/agentrun-reconciler.js +148 -22
  14. package/dist/core/agentruns.js +254 -29
  15. package/dist/core/assignment-request-schema.js +7 -0
  16. package/dist/core/assignment-sweeper.js +5 -3
  17. package/dist/core/assignments.js +131 -33
  18. package/dist/core/claim-request-schema.js +7 -0
  19. package/dist/core/claims.js +53 -2
  20. package/dist/core/dispatch-status.js +16 -6
  21. package/dist/core/dispatcher.js +51 -51
  22. package/dist/core/entity-operations.js +20 -0
  23. package/dist/core/events.js +4 -0
  24. package/dist/core/execution-adapters.js +160 -14
  25. package/dist/core/execution-contract.js +345 -0
  26. package/dist/core/execution.js +130 -16
  27. package/dist/core/harness-adapters/base.js +150 -0
  28. package/dist/core/harness-adapters/claude.js +39 -0
  29. package/dist/core/harness-adapters/codex.js +57 -0
  30. package/dist/core/harness-adapters/harvest.js +109 -0
  31. package/dist/core/harness-adapters/index.js +8 -0
  32. package/dist/core/harness-adapters/prompt-only.js +13 -0
  33. package/dist/core/harness-adapters/registry.js +48 -0
  34. package/dist/core/harness-adapters/result.js +33 -0
  35. package/dist/core/harness-adapters/types.js +2 -0
  36. package/dist/core/ideation-loop-close.js +25 -2
  37. package/dist/core/instruction-templates.js +3 -2
  38. package/dist/core/loop-turn-dispatch.js +207 -0
  39. package/dist/core/loops/artifact-contract.js +11 -0
  40. package/dist/core/loops/attempt-authority.js +476 -0
  41. package/dist/core/loops/attempt-generations.js +509 -0
  42. package/dist/core/loops/attempt-reservation.js +197 -35
  43. package/dist/core/loops/attempt-rollout.js +404 -0
  44. package/dist/core/loops/attempt-takeover.js +155 -0
  45. package/dist/core/loops/bootstrap-acquire.js +7 -3
  46. package/dist/core/loops/evidence.js +187 -0
  47. package/dist/core/loops/facade-schema.js +41 -10
  48. package/dist/core/loops/gate-policy.js +485 -0
  49. package/dist/core/loops/impl-bind.js +37 -79
  50. package/dist/core/loops/index.js +9 -0
  51. package/dist/core/loops/iteration-engine.js +31 -19
  52. package/dist/core/loops/kind-policies.js +90 -0
  53. package/dist/core/loops/lock.js +71 -13
  54. package/dist/core/loops/reconcile-turn.js +235 -18
  55. package/dist/core/loops/result-reducers.js +99 -10
  56. package/dist/core/loops/store.js +30 -3
  57. package/dist/core/loops/turn-execution.js +480 -0
  58. package/dist/core/loops/types.js +113 -2
  59. package/dist/core/loops/verbs.js +332 -99
  60. package/dist/core/loops/verify-command.js +31 -8
  61. package/dist/core/loops/workspace-digest.js +54 -0
  62. package/dist/core/review-loop-close.js +25 -3
  63. package/dist/core/review-loop-turn-dispatch.js +210 -161
  64. package/dist/core/runtime-signals.js +62 -25
  65. package/dist/core/schema.js +35 -0
  66. package/dist/core/spawn-check.js +3 -2
  67. package/dist/core/upgrades/backup.js +27 -4
  68. package/dist/facts.js +7 -6
  69. package/dist/facts.json +6 -5
  70. package/docs/cli.md +49 -1
  71. package/docs/concepts/attempt-authority.md +407 -0
  72. package/docs/concepts/evidence-attestations.md +135 -0
  73. package/docs/concepts/execution-contract.md +166 -0
  74. package/docs/concepts/harness-adapters.md +166 -0
  75. package/docs/concepts/ideation-loop.md +5 -4
  76. package/docs/concepts/loop-engine.md +302 -113
  77. package/docs/index.md +4 -1
  78. package/docs/integrations/codex.md +3 -3
  79. package/docs/integrations/mcp.md +59 -5
  80. package/docs/loops/debug.md +144 -0
  81. package/docs/loops/ideation.md +158 -0
  82. package/docs/loops/implementation.md +154 -0
  83. package/docs/loops/research.md +136 -0
  84. package/docs/loops/review.md +200 -0
  85. package/docs/mcp-schema-changelog.md +14 -5
  86. package/package.json +1 -1
@@ -0,0 +1,187 @@
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
+ iteration: artifact.iteration ?? 0,
28
+ });
29
+ }
30
+ /**
31
+ * Classify both new and historical artifacts without a migration rewrite.
32
+ * Old sealed envelopes predate the explicit field but are still attested;
33
+ * old unsealed records remain legacy. The marker is intentionally excluded
34
+ * from artifactEvidenceDigest so adding it cannot invalidate a v1 seal.
35
+ */
36
+ export function artifactEvidenceProvenance(artifact) {
37
+ return artifact.provenance ?? (artifact.evidence ? 'attested' : 'legacy');
38
+ }
39
+ function isAcceptedVerdict(artifact) {
40
+ return artifact.type === 'verdict' && /^accepted(?:\b|[:\s])/.test((artifact.body ?? '').trim().toLowerCase());
41
+ }
42
+ function attestation(kind, issuer, issued_at, subject_digest, rights) {
43
+ return { kind, issuer, issued_at, subject_digest, rights };
44
+ }
45
+ /**
46
+ * Bind evidence at the server-controlled artifact commit boundary. Callers
47
+ * provide identity context, never an envelope or rights.
48
+ */
49
+ export function sealArtifactEvidence(thread, artifact, context) {
50
+ const committedArtifact = {
51
+ ...artifact,
52
+ produced_by: context.producer_id,
53
+ iteration: artifact.iteration ?? thread.iteration_count,
54
+ };
55
+ const iteration = committedArtifact.iteration ?? thread.iteration_count;
56
+ const subject = {
57
+ loop_id: thread.id,
58
+ artifact_id: artifact.artifact_id,
59
+ phase: artifact.phase,
60
+ iteration,
61
+ slot_id: context.slot_id,
62
+ turn_id: context.turn_id,
63
+ assignment_id: context.assignment_id,
64
+ claim_id: context.claim_id,
65
+ run_id: context.run_id,
66
+ nonce_digest: context.nonce ? evidenceDigest({ launch_nonce: context.nonce }) : undefined,
67
+ attempt_epoch: context.attempt_epoch,
68
+ execution_contract_hash: context.execution_contract_hash,
69
+ command_digest: context.command_digest,
70
+ workspace_digest: context.workspace_digest,
71
+ };
72
+ const subjectDigest = evidenceDigest(subject);
73
+ const attestations = [];
74
+ if (context.claim_id) {
75
+ attestations.push(attestation('claim', 'brainclaw:claim-binding', committedArtifact.produced_at, subjectDigest, ['subject:claim']));
76
+ }
77
+ if (context.channel === 'verify_command' && context.producer_kind === 'engine') {
78
+ attestations.push(attestation('verification', 'brainclaw:verify-command', committedArtifact.produced_at, subjectDigest, ['artifact:write', 'gate:artifact', 'gate:command_green']));
79
+ }
80
+ else {
81
+ const rights = context.channel === 'add_artifact'
82
+ ? ['artifact:write']
83
+ : ['artifact:write', 'gate:artifact'];
84
+ attestations.push(attestation('observation', 'brainclaw:artifact-commit', committedArtifact.produced_at, subjectDigest, rights));
85
+ }
86
+ if (isAcceptedVerdict(committedArtifact) &&
87
+ (context.channel === 'complete_turn' || context.channel === 'reconcile_turn') &&
88
+ /review/i.test(context.slot_role ?? '')) {
89
+ attestations.push(attestation('approval', 'brainclaw:review-slot', committedArtifact.produced_at, subjectDigest, ['gate:reviewer_green']));
90
+ }
91
+ const unsigned = {
92
+ version: 1,
93
+ evidence_id: `evd_${crypto.randomBytes(8).toString('hex')}`,
94
+ evidence_type: 'artifact_commit',
95
+ policy_version: GATE_POLICY_VERSION,
96
+ subject,
97
+ producer: {
98
+ kind: context.producer_kind,
99
+ id: context.producer_id,
100
+ agent_id: context.agent_id,
101
+ channel: context.channel,
102
+ },
103
+ artifact_digest: artifactEvidenceDigest(committedArtifact),
104
+ issued_at: committedArtifact.produced_at,
105
+ observed_at: committedArtifact.produced_at,
106
+ validity: { not_before: committedArtifact.produced_at },
107
+ attestations,
108
+ };
109
+ const envelope = {
110
+ ...unsigned,
111
+ seal: { algorithm: 'sha256', digest: evidenceDigest(unsigned) },
112
+ };
113
+ return { ...committedArtifact, provenance: 'attested', evidence: envelope };
114
+ }
115
+ /** Validate every binding before an envelope may influence a gate. */
116
+ export function validateArtifactEvidence(thread, artifact, now = new Date()) {
117
+ const envelope = artifact.evidence;
118
+ if (!envelope)
119
+ return { valid: false, reasons: ['missing_evidence'] };
120
+ const reasons = [];
121
+ const { seal, ...unsigned } = envelope;
122
+ if (seal.algorithm !== 'sha256' || evidenceDigest(unsigned) !== seal.digest)
123
+ reasons.push('invalid_seal');
124
+ const { evidence: _ignored, ...unsignedArtifact } = artifact;
125
+ void _ignored;
126
+ if (artifactEvidenceDigest(unsignedArtifact) !== envelope.artifact_digest)
127
+ reasons.push('artifact_digest_mismatch');
128
+ if (envelope.subject.loop_id !== thread.id)
129
+ reasons.push('wrong_loop_subject');
130
+ if (envelope.subject.artifact_id !== artifact.artifact_id)
131
+ reasons.push('wrong_artifact_subject');
132
+ if (envelope.subject.phase !== artifact.phase)
133
+ reasons.push('wrong_phase_subject');
134
+ if (envelope.subject.iteration !== (artifact.iteration ?? 0))
135
+ reasons.push('wrong_iteration_subject');
136
+ if (envelope.issued_at !== artifact.produced_at)
137
+ reasons.push('issued_at_mismatch');
138
+ if (envelope.observed_at !== artifact.produced_at)
139
+ reasons.push('observed_at_mismatch');
140
+ const issued = Date.parse(envelope.issued_at);
141
+ const loopCreated = Date.parse(thread.created_at);
142
+ if (!Number.isFinite(issued))
143
+ reasons.push('invalid_issued_at');
144
+ if (Number.isFinite(issued) && Number.isFinite(loopCreated) && issued < loopCreated - 5 * 60_000)
145
+ reasons.push('stale_before_loop');
146
+ if (Number.isFinite(issued) && issued > now.getTime() + 5 * 60_000)
147
+ reasons.push('issued_in_future');
148
+ const notBefore = Date.parse(envelope.validity.not_before);
149
+ const notAfter = envelope.validity.not_after ? Date.parse(envelope.validity.not_after) : undefined;
150
+ if (!Number.isFinite(notBefore) || issued < notBefore)
151
+ reasons.push('outside_validity_window');
152
+ if (notAfter !== undefined && (!Number.isFinite(notAfter) || now.getTime() > notAfter))
153
+ reasons.push('outside_validity_window');
154
+ if (notAfter !== undefined && Number.isFinite(notBefore) && Number.isFinite(notAfter) && notAfter < notBefore)
155
+ reasons.push('invalid_validity_window');
156
+ if (artifact.produced_by !== envelope.producer.id)
157
+ reasons.push('producer_binding_mismatch');
158
+ const subjectDigest = evidenceDigest(envelope.subject);
159
+ for (const item of envelope.attestations) {
160
+ if (item.subject_digest !== subjectDigest)
161
+ reasons.push(`attestation_subject_mismatch:${item.kind}`);
162
+ if (item.issued_at !== envelope.issued_at)
163
+ reasons.push(`attestation_time_mismatch:${item.kind}`);
164
+ }
165
+ return { valid: reasons.length === 0, reasons, evidence_id: envelope.evidence_id };
166
+ }
167
+ export function validateThreadEvidence(thread) {
168
+ return thread.artifacts.flatMap((artifact) => {
169
+ if (!artifact.evidence)
170
+ return [];
171
+ const result = validateArtifactEvidence(thread, artifact);
172
+ return result.valid ? [] : [{ artifact_id: artifact.artifact_id, reasons: result.reasons }];
173
+ });
174
+ }
175
+ /** Feature rollout: new loops are strict unless the writer is explicitly disabled or shadowed. */
176
+ export function evidencePolicyForNewLoop(env = process.env) {
177
+ const configured = env.BRAINCLAW_EVIDENCE_ENVELOPES?.trim().toLowerCase();
178
+ if (configured === 'off')
179
+ return undefined;
180
+ if (configured === 'shadow')
181
+ return { version: GATE_POLICY_VERSION, mode: 'shadow' };
182
+ return { version: GATE_POLICY_VERSION, mode: 'strict' };
183
+ }
184
+ export function evidenceWriterEnabled(env = process.env) {
185
+ return env.BRAINCLAW_EVIDENCE_ENVELOPES?.trim().toLowerCase() !== 'off';
186
+ }
187
+ //# 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
@@ -91,6 +107,19 @@ export const BclawLoopCompleteTurnSchema = z.object({
91
107
  expected_version: z.number().int().nonnegative().optional(),
92
108
  ...CallerEnvelopeFields,
93
109
  });
110
+ export const BclawLoopTakeoverSchema = z.object({
111
+ intent: z.literal('takeover'),
112
+ loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
113
+ slot_id: z.string().min(1),
114
+ turn_id: z.string().min(1),
115
+ expected_epoch: z.number().int().nonnegative(),
116
+ cause: z.string().min(1),
117
+ liveness_evidence: z.string().min(1),
118
+ external_effect_policy: z.enum(['none', 'idempotent', 'externally_fenced']),
119
+ next_workspace_path: z.string().min(1),
120
+ takeover_mode: z.enum(['takeover', 'retry']).optional(),
121
+ ...CallerEnvelopeFields,
122
+ });
94
123
  export const BclawLoopAdvanceSchema = z.object({
95
124
  intent: z.literal('advance'),
96
125
  loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
@@ -107,7 +136,6 @@ export const BclawLoopAddArtifactSchema = z.object({
107
136
  phase: z.string().min(1),
108
137
  type: z.string().min(1),
109
138
  body: z.string().optional(),
110
- produced_by: z.string().optional(),
111
139
  ref: LoopRefSchema.optional(),
112
140
  /** pln#492 synthesis audit trail. Required when type === 'plan_draft'. */
113
141
  addresses_critique: z.array(z.string().min(1)).optional(),
@@ -151,23 +179,24 @@ export const BclawLoopVerifySchema = z.object({
151
179
  });
152
180
  /**
153
181
  * pln#632 — `bclaw_loop(intent='bind')`: the ENGINE action for an implementation loop's
154
- * `bind` phase. Dispatches the loop's linked sequence (by id, no active-sequence hijack)
155
- * and advances `bind → execute`. Idempotent (a loop past `bind` → noop). `dry_run`
156
- * previews what would dispatch without spawning or advancing. Implementation loops only —
157
- * review/ideation loops dispatch via bclaw_coordinate.
182
+ * `bind` phase. Validates the loop's linked sequence and advances
183
+ * `bind → execute`; it never dispatches a worker. Idempotent (a loop past
184
+ * `bind` noop). `dry_run` validates without advancing. Historical launch
185
+ * options remain accepted but are ignored during migration; worker launch is
186
+ * exclusively `turn(dispatch=true)` through AttemptAuthority.
158
187
  */
159
188
  export const BclawLoopBindSchema = z.object({
160
189
  intent: z.literal('bind'),
161
190
  loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
162
- /** Analyze + report what would dispatch; no spawn, no advance. */
191
+ /** Validate the linked sequence; no advance. Bind never spawns. */
163
192
  dry_run: z.boolean().optional(),
164
- /** Restrict the bind dispatch to specific lanes. */
193
+ /** @deprecated Retained for compatibility and ignored. */
165
194
  lanes: z.array(z.string().min(1)).optional(),
166
- /** Deliver briefs without spawning (→ manual launch commands). */
195
+ /** @deprecated Retained for compatibility and ignored. */
167
196
  auto_execute: z.boolean().optional(),
168
- /** Model override for the dispatched agents. */
197
+ /** @deprecated Retained for compatibility and ignored. */
169
198
  model: z.string().min(1).optional(),
170
- /** Cap assignments made in this bind. */
199
+ /** @deprecated Retained for compatibility and ignored. */
171
200
  max_assignments: z.number().int().positive().optional(),
172
201
  // No expected_version: bind is idempotent by loop phase (past `bind` → noop), not CAS.
173
202
  ...CallerEnvelopeFields,
@@ -236,6 +265,7 @@ export const BclawLoopRequestSchema = z.discriminatedUnion('intent', [
236
265
  BclawLoopListSchema,
237
266
  BclawLoopTurnSchema,
238
267
  BclawLoopCompleteTurnSchema,
268
+ BclawLoopTakeoverSchema,
239
269
  BclawLoopAdvanceSchema,
240
270
  BclawLoopAddArtifactSchema,
241
271
  BclawLoopPauseSchema,
@@ -252,6 +282,7 @@ export const BCLAW_LOOP_INTENTS = [
252
282
  'list',
253
283
  'turn',
254
284
  'complete_turn',
285
+ 'takeover',
255
286
  'advance',
256
287
  'add_artifact',
257
288
  'pause',