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.
Files changed (88) 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 +143 -15
  8. package/dist/commands/mcp-catalog.js +52 -18
  9. package/dist/commands/mcp-schemas.generated.js +64 -0
  10. package/dist/commands/mcp-write-claims.js +128 -1
  11. package/dist/commands/mcp-write-coordination.js +149 -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 +189 -14
  25. package/dist/core/execution-contract.js +345 -0
  26. package/dist/core/execution.js +130 -16
  27. package/dist/core/facade-schema.js +3 -0
  28. package/dist/core/harness-adapters/base.js +150 -0
  29. package/dist/core/harness-adapters/claude.js +39 -0
  30. package/dist/core/harness-adapters/codex.js +57 -0
  31. package/dist/core/harness-adapters/harvest.js +109 -0
  32. package/dist/core/harness-adapters/index.js +8 -0
  33. package/dist/core/harness-adapters/prompt-only.js +13 -0
  34. package/dist/core/harness-adapters/registry.js +48 -0
  35. package/dist/core/harness-adapters/result.js +33 -0
  36. package/dist/core/harness-adapters/types.js +2 -0
  37. package/dist/core/ideation-loop-close.js +25 -2
  38. package/dist/core/instruction-templates.js +3 -2
  39. package/dist/core/loop-turn-dispatch.js +235 -0
  40. package/dist/core/loops/artifact-contract.js +11 -0
  41. package/dist/core/loops/attempt-authority.js +496 -0
  42. package/dist/core/loops/attempt-generations.js +509 -0
  43. package/dist/core/loops/attempt-reservation.js +197 -35
  44. package/dist/core/loops/attempt-rollout.js +404 -0
  45. package/dist/core/loops/attempt-takeover.js +155 -0
  46. package/dist/core/loops/bootstrap-acquire.js +7 -3
  47. package/dist/core/loops/brief-assembly.js +21 -4
  48. package/dist/core/loops/evidence.js +188 -0
  49. package/dist/core/loops/facade-schema.js +75 -11
  50. package/dist/core/loops/gate-policy.js +533 -0
  51. package/dist/core/loops/impl-bind.js +91 -81
  52. package/dist/core/loops/index.js +9 -0
  53. package/dist/core/loops/iteration-engine.js +31 -19
  54. package/dist/core/loops/kind-policies.js +90 -0
  55. package/dist/core/loops/lock.js +71 -13
  56. package/dist/core/loops/reconcile-turn.js +237 -18
  57. package/dist/core/loops/result-reducers.js +113 -10
  58. package/dist/core/loops/store.js +34 -3
  59. package/dist/core/loops/turn-execution.js +480 -0
  60. package/dist/core/loops/types.js +127 -3
  61. package/dist/core/loops/verbs.js +335 -99
  62. package/dist/core/loops/verify-command.js +105 -20
  63. package/dist/core/loops/workspace-digest.js +54 -0
  64. package/dist/core/review-loop-close.js +25 -3
  65. package/dist/core/review-loop-turn-dispatch.js +210 -161
  66. package/dist/core/runtime-signals.js +62 -25
  67. package/dist/core/schema.js +40 -0
  68. package/dist/core/spawn-check.js +3 -2
  69. package/dist/core/upgrades/backup.js +27 -4
  70. package/dist/facts.js +9 -8
  71. package/dist/facts.json +8 -7
  72. package/docs/cli.md +49 -1
  73. package/docs/concepts/attempt-authority.md +407 -0
  74. package/docs/concepts/evidence-attestations.md +135 -0
  75. package/docs/concepts/execution-contract.md +166 -0
  76. package/docs/concepts/harness-adapters.md +166 -0
  77. package/docs/concepts/ideation-loop.md +5 -4
  78. package/docs/concepts/loop-engine.md +302 -113
  79. package/docs/index.md +4 -1
  80. package/docs/integrations/codex.md +3 -3
  81. package/docs/integrations/mcp.md +59 -5
  82. package/docs/loops/debug.md +144 -0
  83. package/docs/loops/ideation.md +158 -0
  84. package/docs/loops/implementation.md +174 -0
  85. package/docs/loops/research.md +136 -0
  86. package/docs/loops/review.md +200 -0
  87. package/docs/mcp-schema-changelog.md +18 -5
  88. 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.object({
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. 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.
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
- /** Analyze + report what would dispatch; no spawn, no advance. */
224
+ /** Validate the linked sequence; no advance. Bind never spawns. */
163
225
  dry_run: z.boolean().optional(),
164
- /** Restrict the bind dispatch to specific lanes. */
226
+ /** @deprecated Retained for compatibility and ignored. */
165
227
  lanes: z.array(z.string().min(1)).optional(),
166
- /** Deliver briefs without spawning (→ manual launch commands). */
228
+ /** @deprecated Retained for compatibility and ignored. */
167
229
  auto_execute: z.boolean().optional(),
168
- /** Model override for the dispatched agents. */
230
+ /** @deprecated Retained for compatibility and ignored. */
169
231
  model: z.string().min(1).optional(),
170
- /** Cap assignments made in this bind. */
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',