taskforce-loop-engineering 0.9.1 → 0.12.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/CHANGELOG.md +23 -0
- package/MIGRATING.md +47 -2
- package/README.md +81 -7
- package/bin/loop-engineering.mjs +181 -0
- package/docs/architecture.md +444 -0
- package/docs/multi-agent-control-plane.md +31 -0
- package/docs/operator-dashboard.md +27 -0
- package/docs/release-0.12-acceptance.md +35 -0
- package/lib/action-reservations.mjs +196 -0
- package/lib/core.mjs +230 -14
- package/lib/operator-dashboard.mjs +198 -0
- package/lib/todo-control-plane.mjs +287 -0
- package/package.json +3 -2
- package/scripts/action-reservation-self-test.mjs +65 -0
- package/scripts/hermes-install-self-test.mjs +2 -0
- package/scripts/hermes-install.mjs +40 -25
- package/scripts/human-gate-lifecycle-v2-self-test.mjs +81 -0
- package/scripts/openclaw-install-self-test.mjs +19 -5
- package/scripts/openclaw-install.mjs +101 -37
- package/scripts/openclaw-manage.mjs +1 -1
- package/scripts/operator-dashboard-self-test.mjs +74 -0
- package/scripts/route-notify-self-test.mjs +7 -0
- package/scripts/todo-control-plane-self-test.mjs +74 -0
- package/templates/operator-projection.schema.json +1 -0
- package/templates/todo.schema.json +28 -0
|
@@ -73,7 +73,7 @@ async function main() {
|
|
|
73
73
|
if (!plan.ready) throw new Error(`Refusing upgrade because managed content changed: ${[...modified, ...(!instructionsClean ? ['AGENTS.md managed block'] : [])].join(', ')}`);
|
|
74
74
|
const installer = new URL('./openclaw-install.mjs', import.meta.url).pathname;
|
|
75
75
|
const result = await new Promise((resolve) => {
|
|
76
|
-
const child = spawn(process.execPath, [installer, '--root', args.root, '--queue', manifest.queue, '--worker-agent', manifest.workerAgent, '--openclaw-bin', manifest.openclawBin || 'openclaw', '--systemctl-bin', manifest.systemctlBin || 'systemctl', '--confirm-install', '--force', '--json'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
76
|
+
const child = spawn(process.execPath, [installer, '--root', args.root, '--queue', manifest.queue, '--worker-agent', manifest.workerAgent, '--language', manifest.language || 'en', '--openclaw-bin', manifest.openclawBin || 'openclaw', '--systemctl-bin', manifest.systemctlBin || 'systemctl', '--confirm-install', '--force', '--json'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
77
77
|
let stdout = ''; let stderr = ''; child.stdout.on('data', (c) => { stdout += c; }); child.stderr.on('data', (c) => { stderr += c; });
|
|
78
78
|
child.on('close', (code) => resolve({ code, stdout, stderr }));
|
|
79
79
|
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { buildOperatorProjection, createDashboardServer, dashboardHealth, exportDashboard, filterProjection } from '../lib/operator-dashboard.mjs';
|
|
6
|
+
|
|
7
|
+
const root = await mkdtemp(path.join(tmpdir(), 'loop-dashboard-'));
|
|
8
|
+
const loops = path.join(root, 'runtime', 'loops');
|
|
9
|
+
await mkdir(path.join(loops, 'control-plane'), { recursive: true });
|
|
10
|
+
await mkdir(path.join(loops, 'action-reservations'), { recursive: true });
|
|
11
|
+
await mkdir(path.join(loops, 'legacy', 'waiting'), { recursive: true });
|
|
12
|
+
await mkdir(path.join(loops, 'legacy', 'active'), { recursive: true });
|
|
13
|
+
await mkdir(path.join(loops, 'projects', 'p3', 'intake'), { recursive: true });
|
|
14
|
+
const now = '2026-08-13T16:00:00.000Z';
|
|
15
|
+
const control = {
|
|
16
|
+
version: 2, updated_at: now, quotas: { credits: 9 }, agents: { a: { id: 'a', capabilities: ['code'], authority_grants: ['local'], provider_token: 'never-show' } },
|
|
17
|
+
handoffs: { h: { id: 'h', todo_id: 'leased', from_agent_id: 'a', to_agent_id: 'b', state: 'pending', created_at: now } },
|
|
18
|
+
todos: {
|
|
19
|
+
human: { version: 2, id: 'human', title: '<img src=x onerror=alert(1)>', state: 'runnable', priority: 2, risk: 'medium', authority_class: 'local', required_capabilities: [], acceptance_contract: { checks: ['ok'] }, evidence_requirements: ['test'], cost_envelope: { quota: 'credits', amount: 2 }, parked: { state: 'waiting_for_human', gate_id: 'g1', secret_input: 'hide' }, blocked_reasons: [], claim: null, lineage: { root_todo_id: 'human' }, evidence: [], idempotency_keys: [], created_at: now, updated_at: now },
|
|
20
|
+
leased: { version: 2, id: 'leased', title: 'Lease expired', state: 'claimed', priority: 1, authority_class: 'local', required_capabilities: [], acceptance_contract: {}, evidence_requirements: ['lease'], cost_envelope: { quota: 'credits', amount: 3 }, claim: { owner: 'a', fencing_token: 7, claimed_at: '2026-08-13T15:00:00.000Z', lease_expires_at: '2026-08-13T15:01:00.000Z' }, blocked_reasons: [], lineage: {}, evidence: [], idempotency_keys: ['paid:x'], created_at: now, updated_at: now }
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
await writeFile(path.join(loops, 'control-plane', 'state.json'), JSON.stringify(control));
|
|
24
|
+
await writeFile(path.join(loops, 'action-reservations', 'x.json'), JSON.stringify({ version: 1, idempotency_key: 'paid:x', kind: 'paid_api', state: 'unknown', request: { api_key: 'hide', prompt: '<script>x</script>' }, request_fingerprint: 'abc', authorization: { scope: 'paid', credential: 'hide' }, claim: null, reconciliation: { required: true, reason: 'stale_lease' }, created_at: now, updated_at: now }));
|
|
25
|
+
await writeFile(path.join(loops, 'legacy', 'waiting', 'vps.json'), JSON.stringify({ id: 'vps', title: 'VPS down', parked: { kind: 'external_condition', next_check_at: '2026-08-13T17:00:00.000Z' }, provider: 'hidden' }));
|
|
26
|
+
await writeFile(path.join(loops, 'legacy', 'active', 'bad.json'), '{broken');
|
|
27
|
+
await writeFile(path.join(loops, 'projects', 'p3', 'intake', 'latest.json'), JSON.stringify({ version: 1, goal: 'Operator dashboard', token: 'hidden' }));
|
|
28
|
+
|
|
29
|
+
const before = await stat(path.join(loops, 'control-plane', 'state.json'));
|
|
30
|
+
const first = await buildOperatorProjection(root, { now });
|
|
31
|
+
const second = await buildOperatorProjection(root, { now });
|
|
32
|
+
const after = await stat(path.join(loops, 'control-plane', 'state.json'));
|
|
33
|
+
assert.deepEqual(first, second, 'fixed-time projections are deterministic');
|
|
34
|
+
assert.equal(before.mtimeMs, after.mtimeMs, 'projection does not mutate source state');
|
|
35
|
+
assert.equal(first.todos.find((item) => item.id === 'human').state, 'waiting_for_human');
|
|
36
|
+
assert.equal(first.todos.find((item) => item.id === 'leased').state, 'reconciliation_required');
|
|
37
|
+
assert.equal(first.actions[0].state, 'reconciliation_required');
|
|
38
|
+
assert.equal(first.queues[0].tasks[0].state, 'waiting_for_external_condition');
|
|
39
|
+
assert.equal(first.agents[0].provider_token, '[REDACTED]');
|
|
40
|
+
assert.equal(first.todos[0].gate.secret_input, '[REDACTED]');
|
|
41
|
+
assert.equal(JSON.stringify(first).includes('never-show'), false);
|
|
42
|
+
assert.equal(JSON.stringify(first).includes('hidden'), false);
|
|
43
|
+
assert.equal(first.health.status, 'degraded');
|
|
44
|
+
assert.equal(filterProjection(first, { state: 'waiting_for_human', query: 'img' }).todos.length, 1);
|
|
45
|
+
assert.equal(dashboardHealth(first, { maxAgeSeconds: 1 }).stale, false);
|
|
46
|
+
|
|
47
|
+
const output = path.join(root, 'export');
|
|
48
|
+
await exportDashboard(root, output, { now });
|
|
49
|
+
assert.match(await readFile(path.join(output, 'index.html'), 'utf8'), /projection\.json/);
|
|
50
|
+
assert.equal(JSON.parse(await readFile(path.join(output, 'projection.json'), 'utf8')).schema_version, '1.0.0');
|
|
51
|
+
await assert.rejects(() => createDashboardServer(root, { host: '0.0.0.0' }), /requires --allow-non-loopback/);
|
|
52
|
+
|
|
53
|
+
const server = await createDashboardServer(root, { host: '127.0.0.1', port: 0 });
|
|
54
|
+
const address = server.address(); const base = `http://127.0.0.1:${address.port}`;
|
|
55
|
+
try {
|
|
56
|
+
const overview = await fetch(`${base}/api/v1/overview?state=waiting_for_human`).then((response) => response.json());
|
|
57
|
+
assert.equal(overview.todos.length, 1);
|
|
58
|
+
const detail = await fetch(`${base}/api/v1/todos/human`).then((response) => response.json());
|
|
59
|
+
assert.match(detail.title, /onerror/);
|
|
60
|
+
const page = await fetch(base).then((response) => response.text());
|
|
61
|
+
assert.doesNotMatch(page, /<img src=x/);
|
|
62
|
+
assert.equal((await fetch(`${base}/api/v1/todos/%2e%2e%2fsecret`)).status, 400);
|
|
63
|
+
assert.equal((await fetch(`${base}/api/v1/unknown`)).status, 404);
|
|
64
|
+
assert.equal((await fetch(`${base}/api/v1/overview`, { method: 'POST' })).status, 405);
|
|
65
|
+
} finally { await new Promise((resolve) => server.close(resolve)); }
|
|
66
|
+
|
|
67
|
+
// Large queue stays dependency-light and completes within a generous local budget.
|
|
68
|
+
await mkdir(path.join(loops, 'large', 'inbox'), { recursive: true });
|
|
69
|
+
await Promise.all(Array.from({ length: 500 }, (_, i) => writeFile(path.join(loops, 'large', 'inbox', `${i}.json`), JSON.stringify({ id: `bulk-${i}`, title: `Task ${i}`, state: 'runnable' }))));
|
|
70
|
+
const started = performance.now(); const large = await buildOperatorProjection(root, { now });
|
|
71
|
+
assert.equal(large.queues.find((queue) => queue.id === 'large').tasks.length, 500);
|
|
72
|
+
assert.ok(performance.now() - started < 5000, '500 task projection should finish under 5 seconds');
|
|
73
|
+
|
|
74
|
+
console.log(JSON.stringify({ status: 'ok', assertions: 'empty-compatible, legacy, malformed, deterministic, read-only, P0 gates, P1 reconciliation, P2 lease/handoff, redaction, XSS, traversal, bind safety, export, restart-safe server, large queue performance' }));
|
|
@@ -54,6 +54,10 @@ assert.deepEqual(classifyLoopMessage('用 loop engineering 把现有的 growth o
|
|
|
54
54
|
enqueue: true,
|
|
55
55
|
readOnly: false
|
|
56
56
|
});
|
|
57
|
+
assert.equal(classifyLoopMessage('Use Loop Engineering to fix this issue.').intent, 'execute');
|
|
58
|
+
assert.equal(classifyLoopMessage('Run this through Loop Engineering.').intent, 'execute');
|
|
59
|
+
assert.equal(classifyLoopMessage('Continue the current loop with this amendment: add English examples.').intent, 'execute');
|
|
60
|
+
assert.equal(classifyLoopMessage('我们继续开发我们的loop engineering').intent, 'execute');
|
|
57
61
|
|
|
58
62
|
const routed = await routeLoopMessage(root, {
|
|
59
63
|
route: true,
|
|
@@ -380,10 +384,12 @@ await writeJson(checkpointFile, {
|
|
|
380
384
|
const failedFile = path.join(queueSubdirFor(root, queue, 'failed'), path.basename(terminalFile));
|
|
381
385
|
await writeJson(failedFile, terminalTask);
|
|
382
386
|
if (failedFile !== terminalFile) await rm(terminalFile, { force: true });
|
|
387
|
+
await writeJson(path.join(root, 'configs', 'loops', 'queues', `${queue}.json`), { queue, language: 'zh' });
|
|
383
388
|
|
|
384
389
|
const gateDryRun = await notifyHumanInputRequests(root, { queue, dryRun: true });
|
|
385
390
|
assert.equal(gateDryRun.results[0].outcome, 'dry_run');
|
|
386
391
|
assert.match(gateDryRun.results[0].message, /Provide the SMS code/);
|
|
392
|
+
assert.match(gateDryRun.results[0].message, /正在等待你的输入/);
|
|
387
393
|
const gateSent = await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
|
|
388
394
|
assert.equal(gateSent.sent, 1);
|
|
389
395
|
const gateId = gateSent.results[0].gateId;
|
|
@@ -470,6 +476,7 @@ await writeJson(failedFile, { ...requeuedTask, status: 'needs_human_input' });
|
|
|
470
476
|
await rm(inboxRequeued, { force: true });
|
|
471
477
|
|
|
472
478
|
const dryRun = await notifyTerminalTasks(root, { queue, dryRun: true });
|
|
479
|
+
assert.match(dryRun.results[0].message, /Loop 任务/);
|
|
473
480
|
assert.equal(dryRun.results[0].outcome, 'dry_run');
|
|
474
481
|
const sent = await notifyTerminalTasks(root, { queue, notifyCommand: '/bin/true' });
|
|
475
482
|
assert.equal(sent.sent, 1);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, readFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { claimAction, markActionUnknown, reserveAction } from '../lib/action-reservations.mjs';
|
|
8
|
+
import { claimTodo, createTodo, decideHandoff, handoffTodo, inspectTodo, recoverTodos, registerAgent, releaseTodo, renewTodo } from '../lib/todo-control-plane.mjs';
|
|
9
|
+
|
|
10
|
+
const root = await mkdtemp(path.join(tmpdir(), 'loop-p2-'));
|
|
11
|
+
const agent = (id, capabilities = ['code'], authority_grants = ['local'], quota = 10) => registerAgent(root, { id, capabilities, authority_grants, quota_grants: { default: quota } });
|
|
12
|
+
const todo = (id, extra = {}) => createTodo(root, { id, title: `Todo ${id}`, required_capabilities: ['code'], authority_class: 'local', acceptance_contract: { checks: ['test'] }, evidence_requirements: ['test-output'], cost: 1, ...extra });
|
|
13
|
+
|
|
14
|
+
await Promise.all([agent('alpha'), agent('beta'), agent('observer', []), agent('poor', ['code'], ['local'], 0)]);
|
|
15
|
+
await todo('race');
|
|
16
|
+
const race = await Promise.all([claimTodo(root, { todoId: 'race', agentId: 'alpha', leaseMs: 1000 }), claimTodo(root, { todoId: 'race', agentId: 'beta', leaseMs: 1000 })]);
|
|
17
|
+
assert.equal(race.filter((item) => item.claimed).length, 1, 'exactly one agent wins an atomic claim');
|
|
18
|
+
const winner = race.find((item) => item.claimed);
|
|
19
|
+
const loser = race.find((item) => !item.claimed);
|
|
20
|
+
assert.match(loser.reason, /state:claimed/);
|
|
21
|
+
await assert.rejects(() => renewTodo(root, { todoId: 'race', agentId: loser === race[0] ? 'alpha' : 'beta', fencingToken: winner.fencing_token }), /Stale or invalid/);
|
|
22
|
+
|
|
23
|
+
await todo('capability');
|
|
24
|
+
assert.equal((await claimTodo(root, { todoId: 'capability', agentId: 'observer' })).reason, 'capability_mismatch');
|
|
25
|
+
await todo('quota');
|
|
26
|
+
assert.equal((await claimTodo(root, { todoId: 'quota', agentId: 'poor' })).reason, 'quota_exhausted');
|
|
27
|
+
|
|
28
|
+
await todo('dependency');
|
|
29
|
+
await todo('dependent', { dependencies: ['dependency'], priority: 100 });
|
|
30
|
+
const blocked = await claimTodo(root, { todoId: 'dependent', agentId: 'alpha' });
|
|
31
|
+
assert.match(blocked.reason, /dependencies/);
|
|
32
|
+
const dependencyClaim = await claimTodo(root, { todoId: 'dependency', agentId: 'alpha' });
|
|
33
|
+
await releaseTodo(root, { todoId: 'dependency', agentId: 'alpha', fencingToken: dependencyClaim.fencing_token, completed: true, evidence: 'passed' });
|
|
34
|
+
assert.equal((await claimTodo(root, { todoId: 'dependent', agentId: 'alpha' })).claimed, true, 'completion unlocks dependencies');
|
|
35
|
+
|
|
36
|
+
await todo('stale');
|
|
37
|
+
const stale = await claimTodo(root, { todoId: 'stale', agentId: 'alpha', leaseMs: 1 });
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
39
|
+
await assert.rejects(() => renewTodo(root, { todoId: 'stale', agentId: 'alpha', fencingToken: stale.fencing_token }), /expired/);
|
|
40
|
+
assert.equal((await recoverTodos(root)).results.find((item) => item.todo_id === 'stale').outcome, 'runnable');
|
|
41
|
+
const reclaimed = await claimTodo(root, { todoId: 'stale', agentId: 'beta' });
|
|
42
|
+
assert.ok(reclaimed.fencing_token > stale.fencing_token, 'recovered work gets a higher fencing token');
|
|
43
|
+
|
|
44
|
+
await todo('handoff');
|
|
45
|
+
const owned = await claimTodo(root, { todoId: 'handoff', agentId: 'alpha' });
|
|
46
|
+
const packet = await handoffTodo(root, { todoId: 'handoff', agentId: 'alpha', targetAgentId: 'beta', fencingToken: owned.fencing_token });
|
|
47
|
+
assert.deepEqual(packet.idempotency_keys, []);
|
|
48
|
+
const cli = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../bin/loop-engineering.mjs');
|
|
49
|
+
const restarted = spawnSync(process.execPath, [cli, 'todo-inspect', '--todo-id', 'handoff', '--root', root], { encoding: 'utf8' });
|
|
50
|
+
assert.equal(restarted.status, 0);
|
|
51
|
+
assert.equal(JSON.parse(restarted.stdout).state, 'handoff_pending', 'handoff survives a fresh CLI process');
|
|
52
|
+
const accepted = await decideHandoff(root, { handoffId: packet.id, agentId: 'beta', accept: true });
|
|
53
|
+
assert.equal(accepted.todo.claim.owner, 'beta');
|
|
54
|
+
assert.ok(accepted.todo.claim.fencing_token > owned.fencing_token);
|
|
55
|
+
await todo('handoff-reject');
|
|
56
|
+
const rejectOwned = await claimTodo(root, { todoId: 'handoff-reject', agentId: 'alpha' });
|
|
57
|
+
const rejectPacket = await handoffTodo(root, { todoId: 'handoff-reject', agentId: 'alpha', targetAgentId: 'beta', fencingToken: rejectOwned.fencing_token });
|
|
58
|
+
const rejected = await decideHandoff(root, { handoffId: rejectPacket.id, agentId: 'beta', accept: false, reason: 'busy' });
|
|
59
|
+
assert.equal(rejected.todo.claim.owner, 'alpha');
|
|
60
|
+
|
|
61
|
+
await todo('parked', { parked: { state: 'waiting_for_human' } });
|
|
62
|
+
assert.equal((await claimTodo(root, { todoId: 'parked', agentId: 'alpha' })).reason, 'parked_human_gate');
|
|
63
|
+
|
|
64
|
+
await reserveAction(root, { idempotencyKey: 'side-effect', kind: 'external_message', authorizationScope: 'todo:external', request: { body: 'once' } });
|
|
65
|
+
const action = await claimAction(root, { idempotencyKey: 'side-effect', owner: 'alpha', leaseMs: 1000 });
|
|
66
|
+
await markActionUnknown(root, { idempotencyKey: 'side-effect', fencingToken: action.fencingToken, reason: 'worker_crash' });
|
|
67
|
+
await todo('unknown-action', { idempotency_keys: ['side-effect'], authorization: { scope: 'todo:external', grant: 'preserved' } });
|
|
68
|
+
assert.match((await claimTodo(root, { todoId: 'unknown-action', agentId: 'beta' })).reason, /action_reconciliation/);
|
|
69
|
+
assert.equal((await inspectTodo(root, 'unknown-action')).authorization.grant, 'preserved');
|
|
70
|
+
|
|
71
|
+
const audit = await readFile(path.join(root, 'runtime', 'loops', 'control-plane', 'audit.jsonl'), 'utf8');
|
|
72
|
+
assert.match(audit, /todo_claimed/);
|
|
73
|
+
assert.match(audit, /handoff_accepted/);
|
|
74
|
+
console.log(JSON.stringify({ ok: true, root, assertions: ['atomic claim', 'capability', 'dependency', 'quota', 'lease fencing', 'orphan recovery', 'handoff accept/reject', 'parked gate', 'unknown action reconciliation', 'audit'] }));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://loop-engineering.local/schema/operator-projection-v1.json","title":"Loop Engineering read-only operator projection v1","type":"object","required":["schema_version","generated_at","source","health","overview","projects","queues","todos","agents","handoffs","gates","actions","cost"],"properties":{"schema_version":{"const":"1.0.0"},"generated_at":{"type":"string","format":"date-time"},"source":{"type":"object"},"health":{"type":"object"},"overview":{"type":"object"},"projects":{"type":"array"},"queues":{"type":"array"},"todos":{"type":"array"},"agents":{"type":"array"},"handoffs":{"type":"array"},"gates":{"type":"array"},"actions":{"type":"array"},"cost":{"type":"object"}},"additionalProperties":false}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://loop-engineering.local/schema/todo-v2.json",
|
|
4
|
+
"title": "Loop Engineering typed todo v2",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["id", "title", "priority", "risk", "authority_class", "required_capabilities", "acceptance_contract", "evidence_requirements", "cost_envelope", "state"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"id": { "type": "string", "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,199}$" },
|
|
9
|
+
"title": { "type": "string", "minLength": 1 },
|
|
10
|
+
"project_id": { "type": ["string", "null"] },
|
|
11
|
+
"dependencies": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
|
|
12
|
+
"priority": { "type": "number" },
|
|
13
|
+
"risk": { "enum": ["low", "medium", "high", "critical"] },
|
|
14
|
+
"authority_class": { "type": "string", "minLength": 1 },
|
|
15
|
+
"required_capabilities": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
|
|
16
|
+
"acceptance_contract": { "type": "object" },
|
|
17
|
+
"evidence_requirements": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string" } },
|
|
18
|
+
"cost_envelope": { "type": "object", "required": ["quota", "amount"], "properties": { "quota": { "type": "string" }, "amount": { "type": "number", "minimum": 0 } } },
|
|
19
|
+
"state": { "enum": ["runnable", "blocked", "claimed", "handoff_pending", "completed"] },
|
|
20
|
+
"authorization": {},
|
|
21
|
+
"idempotency_keys": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
|
|
22
|
+
"lineage": { "type": "object" },
|
|
23
|
+
"context": { "type": "object" },
|
|
24
|
+
"evidence": { "type": "array" },
|
|
25
|
+
"claim": { "type": ["object", "null"] }
|
|
26
|
+
},
|
|
27
|
+
"additionalProperties": true
|
|
28
|
+
}
|