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,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.
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
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
|
-
/**
|
|
191
|
+
/** Validate the linked sequence; no advance. Bind never spawns. */
|
|
163
192
|
dry_run: z.boolean().optional(),
|
|
164
|
-
/**
|
|
193
|
+
/** @deprecated Retained for compatibility and ignored. */
|
|
165
194
|
lanes: z.array(z.string().min(1)).optional(),
|
|
166
|
-
/**
|
|
195
|
+
/** @deprecated Retained for compatibility and ignored. */
|
|
167
196
|
auto_execute: z.boolean().optional(),
|
|
168
|
-
/**
|
|
197
|
+
/** @deprecated Retained for compatibility and ignored. */
|
|
169
198
|
model: z.string().min(1).optional(),
|
|
170
|
-
/**
|
|
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',
|