@evomap/evolver 1.72.0 → 1.74.1

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.
@@ -1,4 +1 @@
1
- {"type":"CapabilityCandidate","id":"cand_b9a66a5c","title":"Harden session log detection and fallback behavior","source":"signals","created_at":"2026-04-27T10:30:27.039Z","signals":["memory_missing","user_missing","session_logs_missing"],"tags":["memory_missing","user_missing","session_logs_missing","area:memory"],"shape":{"title":"Harden session log detection and fallback behavior","input":"Recent session transcript + memory snippets + user instructions","output":"A safe, auditable evolution patch guided by GEP assets","invariants":"Protocol order, small reversible patches, validation, append-only events","params":"Signals: memory_missing, user_missing, session_logs_missing","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Signal present: session_logs_missing"}}
2
- {"type":"CapabilityCandidate","id":"cand_b9a66a5c","title":"Harden session log detection and fallback behavior","source":"signals","created_at":"2026-04-27T10:30:56.391Z","signals":["memory_missing","user_missing","session_logs_missing"],"tags":["memory_missing","user_missing","session_logs_missing","area:memory"],"shape":{"title":"Harden session log detection and fallback behavior","input":"Recent session transcript + memory snippets + user instructions","output":"A safe, auditable evolution patch guided by GEP assets","invariants":"Protocol order, small reversible patches, validation, append-only events","params":"Signals: memory_missing, user_missing, session_logs_missing","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Signal present: session_logs_missing"}}
3
- {"type":"CapabilityCandidate","id":"cand_b9a66a5c","title":"Harden session log detection and fallback behavior","source":"signals","created_at":"2026-04-27T10:31:11.248Z","signals":["memory_missing","user_missing","session_logs_missing"],"tags":["memory_missing","user_missing","session_logs_missing","area:memory"],"shape":{"title":"Harden session log detection and fallback behavior","input":"Recent session transcript + memory snippets + user instructions","output":"A safe, auditable evolution patch guided by GEP assets","invariants":"Protocol order, small reversible patches, validation, append-only events","params":"Signals: memory_missing, user_missing, session_logs_missing","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Signal present: session_logs_missing"}}
4
- {"type":"CapabilityCandidate","id":"cand_b9a66a5c","title":"Harden session log detection and fallback behavior","source":"signals","created_at":"2026-04-27T10:31:26.375Z","signals":["memory_missing","user_missing","session_logs_missing"],"tags":["memory_missing","user_missing","session_logs_missing","area:memory"],"shape":{"title":"Harden session log detection and fallback behavior","input":"Recent session transcript + memory snippets + user instructions","output":"A safe, auditable evolution patch guided by GEP assets","invariants":"Protocol order, small reversible patches, validation, append-only events","params":"Signals: memory_missing, user_missing, session_logs_missing","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Signal present: session_logs_missing"}}
1
+ {"type":"CapabilityCandidate","id":"cand_b9a66a5c","title":"Harden session log detection and fallback behavior","source":"signals","created_at":"2026-04-28T10:59:23.012Z","signals":["memory_missing","user_missing","session_logs_missing"],"tags":["memory_missing","user_missing","session_logs_missing","area:memory"],"shape":{"title":"Harden session log detection and fallback behavior","input":"Recent session transcript + memory snippets + user instructions","output":"A safe, auditable evolution patch guided by GEP assets","invariants":"Protocol order, small reversible patches, validation, append-only events","params":"Signals: memory_missing, user_missing, session_logs_missing","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Signal present: session_logs_missing"}}
package/index.js CHANGED
@@ -1072,6 +1072,45 @@ async function main() {
1072
1072
  process.exit(1);
1073
1073
  }
1074
1074
 
1075
+ } else if (command === 'atp-complete') {
1076
+ // Invoked by a spawned Cursor sub-session after it has written the ATP
1077
+ // task answer to a file. Drives publish -> task/complete -> atp/deliver.
1078
+ try {
1079
+ const subArgs = args.slice(1);
1080
+ function flag(name) {
1081
+ const pref = '--' + name + '=';
1082
+ const hit = subArgs.find(function (a) { return typeof a === 'string' && a.startsWith(pref); });
1083
+ return hit ? hit.slice(pref.length) : null;
1084
+ }
1085
+ function list(name) {
1086
+ const raw = flag(name);
1087
+ if (!raw) return null;
1088
+ return raw.split(',').map(function (s) { return String(s).trim(); }).filter(Boolean);
1089
+ }
1090
+ const taskId = flag('task-id');
1091
+ const orderId = flag('order-id');
1092
+ const answerFile = flag('answer-file');
1093
+ const summary = flag('summary');
1094
+ const capabilities = list('capabilities');
1095
+ const signals = list('signals');
1096
+ if (!taskId || !orderId || !answerFile) {
1097
+ console.error('[ATP-Complete] Missing required flags: --task-id, --order-id, --answer-file');
1098
+ console.error('Usage: node index.js atp-complete --task-id=<tid> --order-id=<oid> --answer-file=<path> [--summary="..."] [--capabilities=cap1,cap2] [--signals=sig1,sig2]');
1099
+ process.exit(2);
1100
+ }
1101
+ const { completeAtpTask } = require('./src/atp/atpExecute');
1102
+ const res = await completeAtpTask({ taskId, orderId, answerFile, summary, capabilities, signals });
1103
+ if (res && res.ok) {
1104
+ console.log('[ATP-Complete] OK asset_id=' + res.assetId + (res.deliveryId ? ' delivery_id=' + res.deliveryId : ''));
1105
+ process.exit(0);
1106
+ }
1107
+ console.error('[ATP-Complete] FAILED stage=' + (res && res.stage) + ' error=' + (res && res.error));
1108
+ process.exit(1);
1109
+ } catch (atpCompleteErr) {
1110
+ console.error('[ATP-Complete] Error:', atpCompleteErr && atpCompleteErr.message || atpCompleteErr);
1111
+ process.exit(1);
1112
+ }
1113
+
1075
1114
  } else if (command === 'buy' || command === 'orders' || command === 'verify') {
1076
1115
  try {
1077
1116
  const atpCli = require('./src/atp/cli');
@@ -1101,7 +1140,7 @@ async function main() {
1101
1140
  }
1102
1141
 
1103
1142
  } else {
1104
- console.log(`Usage: node index.js [run|/evolve|solidify|review|distill|fetch|asset-log|setup-hooks|buy|orders|verify] [--loop]
1143
+ console.log(`Usage: node index.js [run|/evolve|solidify|review|distill|fetch|asset-log|setup-hooks|buy|orders|verify|atp-complete] [--loop]
1105
1144
  - fetch flags:
1106
1145
  - --skill=<id> | -s <id> (skill ID to download)
1107
1146
  - --out=<dir> (output directory, default: ./skills/<skill_id>)
@@ -1141,6 +1180,13 @@ async function main() {
1141
1180
  - --json (raw JSON)
1142
1181
  - verify <orderId> (confirm delivery or trigger AI judge)
1143
1182
  - --action=confirm|ai_judge (default confirm)
1183
+ - atp-complete (internal: spawned Cursor sub-session uses this to settle an ATP task)
1184
+ - --task-id=<tid> (Hub task id, required)
1185
+ - --order-id=<oid> (ATP DeliveryProof id, required)
1186
+ - --answer-file=<path> (file containing the merchant answer, required)
1187
+ - --summary="..." (capsule summary, optional)
1188
+ - --capabilities=a,b (listing capabilities, optional)
1189
+ - --signals=s1,s2 (task signals, optional)
1144
1190
 
1145
1191
  Validator role (decentralized validation, default ON since v1.69.0):
1146
1192
  - EVOLVER_VALIDATOR_ENABLED=0 opt out (env beats persisted flag and default)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver",
3
- "version": "1.72.0",
3
+ "version": "1.74.1",
4
4
  "description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,285 @@
1
+ // ATP end-to-end task completer.
2
+ //
3
+ // Invoked from the `atp-complete` subcommand (index.js). A spawned Cursor
4
+ // sub-session answers an ATP task, writes the answer to disk, then runs:
5
+ //
6
+ // node index.js atp-complete \
7
+ // --task-id=<tid> --order-id=<oid> --answer-file=<path> [--summary="..."]
8
+ //
9
+ // This module takes that answer and drives the full settlement path:
10
+ // 1. Synthesize a minimal Gene + Capsule bundle wrapping the answer.
11
+ // 2. POST /a2a/publish to register the Capsule asset on the Hub (signed).
12
+ // 3. POST /a2a/task/complete to bind the resultAssetId to the claimed task.
13
+ // 4. POST /a2a/atp/deliver to submit the proof_payload (asset_id + result).
14
+ //
15
+ // Autoverify (verifyMode=auto) on the Hub treats `payload.asset_id` and
16
+ // `payload.result` as has_result=true with pass_rate=1.0, so a valid answer
17
+ // immediately progresses the DeliveryProof from pending -> verified -> settled.
18
+ //
19
+ // Failures are returned as { ok:false, stage:..., error:... } so the caller can
20
+ // retry per-stage without duplicating upstream effects (Gene/Capsule asset_ids
21
+ // are deterministic content-hashes, so republish of the same bundle is
22
+ // idempotent server-side).
23
+
24
+ const fs = require('fs');
25
+ const http = require('http');
26
+ const https = require('https');
27
+ const crypto = require('crypto');
28
+
29
+ const { computeAssetId } = require('../gep/contentHash');
30
+ const {
31
+ getNodeId,
32
+ getHubUrl,
33
+ getHubNodeSecret,
34
+ buildHubHeaders,
35
+ sendHelloToHub,
36
+ } = require('../gep/a2aProtocol');
37
+ const { submitDelivery } = require('./hubClient');
38
+
39
+ const MAX_ANSWER_CHARS = 32000; // cap capsule.content to protect Hub payload limits
40
+ const PUBLISH_TIMEOUT_MS = 15000;
41
+
42
+ function _readAnswer(answerFile) {
43
+ const raw = fs.readFileSync(answerFile, 'utf8');
44
+ const trimmed = String(raw || '').trim();
45
+ if (!trimmed) throw new Error('answer file is empty');
46
+ if (trimmed.length > MAX_ANSWER_CHARS) {
47
+ return trimmed.slice(0, MAX_ANSWER_CHARS - 40) + '\n...[TRUNCATED]...';
48
+ }
49
+ return trimmed;
50
+ }
51
+
52
+ function _buildGene(capabilities, signals) {
53
+ const caps = Array.isArray(capabilities) && capabilities.length > 0
54
+ ? capabilities.slice(0, 8)
55
+ : ['general'];
56
+ const sig = Array.isArray(signals) && signals.length > 0
57
+ ? signals.slice(0, 8)
58
+ : ['atp_task'];
59
+ const gene = {
60
+ type: 'Gene',
61
+ schema_version: '1.0',
62
+ id: 'gene_atp_answer_' + caps.sort().join('_').slice(0, 40),
63
+ summary: 'Deliver an ATP task answer for capabilities: ' + caps.join(', '),
64
+ signals_match: sig,
65
+ category: 'innovate',
66
+ strategy: [
67
+ 'Read the buyer question carefully and identify the requested capability.',
68
+ 'Produce a concrete, actionable answer addressing the question directly.',
69
+ 'Return the answer as Capsule content for verifiable delivery.',
70
+ ],
71
+ validation: [
72
+ 'Answer is non-empty and directly addresses the buyer question.',
73
+ 'Answer references the requested capabilities where relevant.',
74
+ ],
75
+ };
76
+ gene.asset_id = computeAssetId(gene);
77
+ return gene;
78
+ }
79
+
80
+ function _buildCapsule({ gene, answer, summary, orderId, taskId, capabilities, signals }) {
81
+ const caps = Array.isArray(capabilities) ? capabilities.slice(0, 8) : [];
82
+ const sig = Array.isArray(signals) && signals.length > 0 ? signals.slice(0, 8) : ['atp_task'];
83
+ const confidence = 0.9; // merchant self-attested; buyer verify may override
84
+ const capsuleSummary = String(summary || '').trim()
85
+ || 'ATP merchant delivery for order ' + String(orderId || '').slice(0, 24);
86
+ const capsule = {
87
+ type: 'Capsule',
88
+ schema_version: '1.0',
89
+ id: 'capsule_atp_' + String(orderId || taskId || Date.now()).replace(/[^a-zA-Z0-9_\-]/g, '_').slice(0, 40),
90
+ trigger: sig,
91
+ gene: gene.id,
92
+ summary: capsuleSummary.slice(0, 200),
93
+ confidence,
94
+ blast_radius: { files: 0, lines: Math.min(1000, answer.split('\n').length) },
95
+ outcome: { status: 'success', score: confidence },
96
+ env_fingerprint: { platform: process.platform, arch: process.arch, runtime: 'evolver-atp' },
97
+ content: answer,
98
+ source_type: 'atp_task_executor',
99
+ atp: {
100
+ order_id: orderId || null,
101
+ task_id: taskId || null,
102
+ capabilities: caps,
103
+ },
104
+ };
105
+ capsule.asset_id = computeAssetId(capsule);
106
+ return capsule;
107
+ }
108
+
109
+ function _publishUrl() {
110
+ const base = String(getHubUrl() || '').replace(/\/+$/, '');
111
+ if (!base) throw new Error('hub url not configured');
112
+ return base + '/a2a/publish';
113
+ }
114
+
115
+ function _postJson(urlStr, body, timeoutMs) {
116
+ return new Promise(function (resolve) {
117
+ let parsed;
118
+ try {
119
+ parsed = new URL(urlStr);
120
+ } catch (e) {
121
+ resolve({ ok: false, error: 'invalid_url: ' + (e && e.message) });
122
+ return;
123
+ }
124
+ const isHttps = parsed.protocol === 'https:';
125
+ const lib = isHttps ? https : http;
126
+ const payload = JSON.stringify(body || {});
127
+ const headers = Object.assign(
128
+ { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
129
+ buildHubHeaders() || {},
130
+ );
131
+ const req = lib.request(
132
+ {
133
+ hostname: parsed.hostname,
134
+ port: parsed.port || (isHttps ? 443 : 80),
135
+ path: parsed.pathname + (parsed.search || ''),
136
+ method: 'POST',
137
+ headers: headers,
138
+ timeout: timeoutMs || PUBLISH_TIMEOUT_MS,
139
+ },
140
+ function (res) {
141
+ const chunks = [];
142
+ res.on('data', function (c) { chunks.push(c); });
143
+ res.on('end', function () {
144
+ const text = Buffer.concat(chunks).toString('utf8');
145
+ let data = null;
146
+ try { data = text ? JSON.parse(text) : null; } catch (e) { data = { raw: text }; }
147
+ if (res.statusCode >= 200 && res.statusCode < 300) {
148
+ resolve({ ok: true, status: res.statusCode, data });
149
+ } else {
150
+ resolve({ ok: false, status: res.statusCode, data, error: 'http_' + res.statusCode });
151
+ }
152
+ });
153
+ },
154
+ );
155
+ req.on('timeout', function () { req.destroy(new Error('timeout')); });
156
+ req.on('error', function (err) { resolve({ ok: false, error: err.message }); });
157
+ req.write(payload);
158
+ req.end();
159
+ });
160
+ }
161
+
162
+ async function _ensureNodeSecret() {
163
+ if (getHubNodeSecret()) return true;
164
+ try {
165
+ const hello = await sendHelloToHub();
166
+ return !!(hello && hello.ok);
167
+ } catch (e) {
168
+ return false;
169
+ }
170
+ }
171
+
172
+ async function _publishBundle(gene, capsule) {
173
+ const nodeSecret = getHubNodeSecret();
174
+ if (!nodeSecret) return { ok: false, error: 'missing_node_secret_after_hello' };
175
+ const signatureInput = [gene.asset_id, capsule.asset_id].sort().join('|');
176
+ const signature = crypto.createHmac('sha256', nodeSecret).update(signatureInput).digest('hex');
177
+ const msg = {
178
+ protocol: 'gep-a2a',
179
+ protocol_version: '1.0.0',
180
+ message_type: 'publish',
181
+ message_id: 'msg_atp_' + crypto.randomBytes(8).toString('hex'),
182
+ timestamp: new Date().toISOString(),
183
+ sender_id: getNodeId(),
184
+ payload: { assets: [gene, capsule], signature: signature },
185
+ };
186
+ return _postJson(_publishUrl(), msg, PUBLISH_TIMEOUT_MS);
187
+ }
188
+
189
+ async function _completeTaskOnHub(taskId, assetId) {
190
+ const base = String(getHubUrl() || '').replace(/\/+$/, '');
191
+ if (!base) return { ok: false, error: 'hub_url_missing' };
192
+ return _postJson(base + '/a2a/task/complete', {
193
+ task_id: taskId,
194
+ asset_id: assetId,
195
+ node_id: getNodeId(),
196
+ }, PUBLISH_TIMEOUT_MS);
197
+ }
198
+
199
+ /**
200
+ * End-to-end ATP task completion driver.
201
+ *
202
+ * @param {object} opts
203
+ * @param {string} opts.taskId - Hub task row id (required)
204
+ * @param {string} opts.orderId - ATP DeliveryProof id (required)
205
+ * @param {string} opts.answerFile - Path to file holding the merchant answer (required)
206
+ * @param {string} [opts.summary] - Short summary for capsule.summary
207
+ * @param {string[]} [opts.capabilities] - Listing capabilities (metadata only)
208
+ * @param {string[]} [opts.signals] - Task signals (metadata only)
209
+ * @returns {Promise<{ok:boolean, stage?:string, error?:string, assetId?:string}>}
210
+ */
211
+ async function completeAtpTask(opts) {
212
+ const taskId = opts && opts.taskId;
213
+ const orderId = opts && opts.orderId;
214
+ const answerFile = opts && opts.answerFile;
215
+ if (!taskId || !orderId || !answerFile) {
216
+ return { ok: false, stage: 'input', error: 'taskId, orderId, answerFile are required' };
217
+ }
218
+
219
+ let answer;
220
+ try {
221
+ answer = _readAnswer(answerFile);
222
+ } catch (e) {
223
+ return { ok: false, stage: 'read_answer', error: e && e.message };
224
+ }
225
+
226
+ const handshakeOk = await _ensureNodeSecret();
227
+ if (!handshakeOk) {
228
+ return { ok: false, stage: 'hello', error: 'failed to register with hub; node_secret missing' };
229
+ }
230
+
231
+ const gene = _buildGene(opts.capabilities, opts.signals);
232
+ const capsule = _buildCapsule({
233
+ gene,
234
+ answer,
235
+ summary: opts.summary,
236
+ orderId,
237
+ taskId,
238
+ capabilities: opts.capabilities,
239
+ signals: opts.signals,
240
+ });
241
+
242
+ const pub = await _publishBundle(gene, capsule);
243
+ if (!pub.ok) {
244
+ return { ok: false, stage: 'publish', error: pub.error || 'publish_failed', details: pub };
245
+ }
246
+ const decision = pub.data && pub.data.payload && pub.data.payload.decision;
247
+ if (decision && decision !== 'accept') {
248
+ const reason = pub.data.payload.reason || 'unknown';
249
+ return { ok: false, stage: 'publish', error: 'publish_rejected: ' + reason, details: pub.data };
250
+ }
251
+
252
+ const complete = await _completeTaskOnHub(taskId, capsule.asset_id);
253
+ if (!complete.ok) {
254
+ return { ok: false, stage: 'complete', error: complete.error || 'complete_failed', details: complete };
255
+ }
256
+
257
+ const proofPayload = {
258
+ asset_id: capsule.asset_id,
259
+ result: capsule.summary,
260
+ content_hash: capsule.asset_id,
261
+ pass_rate: 1.0,
262
+ delivered_by: getNodeId(),
263
+ task_id: taskId,
264
+ };
265
+
266
+ const delivery = await submitDelivery(orderId, proofPayload);
267
+ if (!delivery || !delivery.ok) {
268
+ return {
269
+ ok: false,
270
+ stage: 'deliver',
271
+ error: (delivery && delivery.error) || 'deliver_failed',
272
+ assetId: capsule.asset_id,
273
+ details: delivery,
274
+ };
275
+ }
276
+
277
+ return { ok: true, assetId: capsule.asset_id, deliveryId: delivery.data && delivery.data.proof_id };
278
+ }
279
+
280
+ module.exports = {
281
+ completeAtpTask,
282
+ // exported for tests
283
+ _buildGene,
284
+ _buildCapsule,
285
+ };
@@ -0,0 +1,233 @@
1
+ // ATP Task Pickup (merchant-side)
2
+ //
3
+ // When a buyer places an ATP order, the Hub creates a Task row in status
4
+ // "claimed" already bound to a target merchant node (see orderRouterService).
5
+ // That Task never appears in /a2a/fetch (which only returns status="open"
6
+ // tasks), so without this module the merchant's Evolver runtime never knows
7
+ // it has work to do, no resultAssetId is ever written, autoDeliver never
8
+ // runs, and the DeliveryProof expires after 7 days.
9
+ //
10
+ // This module bridges the gap by:
11
+ // 1. Polling /a2a/task/my for tasks with atp_order_id set and no
12
+ // result_asset_id yet (the "merchant owes work" shape).
13
+ // 2. Producing a renderable sessions_spawn(...) prompt that the main loop
14
+ // can emit to stdout. The Evolver wrapper (Cursor/Claude Code hook)
15
+ // picks that up and launches a sub-session that answers the question
16
+ // and runs `node index.js atp-complete` to settle the order.
17
+ // 3. Deduping via a local ledger so the same task is never spawned twice,
18
+ // even across restarts.
19
+ //
20
+ // The module never *itself* prints sessions_spawn. It only PROVIDES the
21
+ // spawn string to whoever orchestrates stdout (evolve.js main loop), so the
22
+ // existing "one sessions_spawn per cycle" contract with the wrapper is
23
+ // preserved and evolve's normal bridge is not interfered with.
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ const { getMemoryDir } = require('../gep/paths');
29
+ const { renderSessionsSpawnCall } = require('../gep/bridge');
30
+ const hubClient = require('./hubClient');
31
+
32
+ const LEDGER_FILENAME = 'atp-pickup-ledger.json';
33
+ const LEDGER_MAX_ENTRIES = 500;
34
+ const SPAWN_COOLDOWN_MS = 5 * 60 * 1000; // do not respawn the same task within 5 min
35
+ const MAX_ANSWER_PROMPT_CHARS = 12000;
36
+
37
+ function _isEnabled() {
38
+ const raw = (process.env.EVOLVER_ATP_PICKUP || 'on').toLowerCase().trim();
39
+ return raw !== 'off' && raw !== '0' && raw !== 'false';
40
+ }
41
+
42
+ function _ledgerPath() {
43
+ return path.join(getMemoryDir(), LEDGER_FILENAME);
44
+ }
45
+
46
+ function _emptyLedger() {
47
+ return { version: 1, spawned: {} };
48
+ }
49
+
50
+ function _readLedger() {
51
+ try {
52
+ const p = _ledgerPath();
53
+ if (!fs.existsSync(p)) return _emptyLedger();
54
+ const raw = fs.readFileSync(p, 'utf8');
55
+ const parsed = JSON.parse(raw);
56
+ if (!parsed || typeof parsed !== 'object' || !parsed.spawned) return _emptyLedger();
57
+ return parsed;
58
+ } catch (_) {
59
+ return _emptyLedger();
60
+ }
61
+ }
62
+
63
+ function _writeLedger(ledger) {
64
+ try {
65
+ const dir = getMemoryDir();
66
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
67
+ const entries = Object.entries(ledger.spawned || {});
68
+ if (entries.length > LEDGER_MAX_ENTRIES) {
69
+ ledger.spawned = Object.fromEntries(entries.slice(-LEDGER_MAX_ENTRIES));
70
+ }
71
+ const tmp = _ledgerPath() + '.tmp';
72
+ fs.writeFileSync(tmp, JSON.stringify(ledger, null, 2));
73
+ fs.renameSync(tmp, _ledgerPath());
74
+ } catch (_) {
75
+ // Non-fatal: next tick will re-read Hub state. Stale ledger at worst
76
+ // causes a duplicate spawn, which the Hub will 409 on already-completed
77
+ // tasks without side effects.
78
+ }
79
+ }
80
+
81
+ function _isEligible(task) {
82
+ if (!task || typeof task !== 'object') return false;
83
+ if (!task.atp_order_id) return false;
84
+ if (task.result_asset_id) return false;
85
+ if (task.status && task.status !== 'claimed' && task.status !== 'open') return false;
86
+ if (!task.id) return false;
87
+ return true;
88
+ }
89
+
90
+ function _recentlySpawned(ledger, taskId) {
91
+ const entry = ledger.spawned && ledger.spawned[taskId];
92
+ if (!entry || typeof entry !== 'object') return false;
93
+ const ts = Number(entry.at) || 0;
94
+ return Date.now() - ts < SPAWN_COOLDOWN_MS;
95
+ }
96
+
97
+ function _clipQuestion(q) {
98
+ const s = String(q || '').trim();
99
+ if (!s) return '(buyer did not provide a question body)';
100
+ if (s.length <= MAX_ANSWER_PROMPT_CHARS) return s;
101
+ return s.slice(0, MAX_ANSWER_PROMPT_CHARS - 40) + '\n...[TRUNCATED]...';
102
+ }
103
+
104
+ function _answerFilePath(taskId) {
105
+ const safe = String(taskId || 'task').replace(/[^a-zA-Z0-9_\-]/g, '_').slice(0, 64);
106
+ return path.join(getMemoryDir(), 'atp_answer_' + safe + '.md');
107
+ }
108
+
109
+ function _buildSpawnTask(task, opts) {
110
+ const capabilities = Array.isArray(task.capabilities) ? task.capabilities.slice(0, 8) : [];
111
+ const signalsCsv = task.signals ? String(task.signals) : '';
112
+ const answerPath = _answerFilePath(task.id);
113
+ const question = _clipQuestion(task.user_question_body || task.description || task.title);
114
+
115
+ const evolverExec = opts && opts.evolverExec ? opts.evolverExec : 'node index.js';
116
+
117
+ const lines = [
118
+ 'You are an ATP merchant sub-agent. A buyer has paid credits for your node to answer their request.',
119
+ '',
120
+ '# Task',
121
+ '- Task ID: ' + task.id,
122
+ '- ATP Order ID: ' + task.atp_order_id,
123
+ '- Title: ' + String(task.title || '(no title)').slice(0, 200),
124
+ '- Capabilities requested: ' + (capabilities.length ? capabilities.join(', ') : '(none)'),
125
+ '- Signals: ' + (signalsCsv || '(none)'),
126
+ '',
127
+ '# Buyer question',
128
+ question,
129
+ '',
130
+ '# Your job',
131
+ '1. Produce a concrete, useful answer to the buyer question above.',
132
+ ' - Use your existing tools (web search, code read, reasoning) as appropriate.',
133
+ ' - Keep the answer focused and actionable; do not invent facts.',
134
+ ' - If the question is ambiguous, answer the most reasonable interpretation and state your assumption.',
135
+ '2. Write the full answer to this file (plain text or markdown):',
136
+ ' ' + answerPath,
137
+ '3. Settle the order by running EXACTLY this command from the Evolver install dir:',
138
+ ' ' + evolverExec + ' atp-complete \\',
139
+ ' --task-id=' + task.id + ' \\',
140
+ ' --order-id=' + task.atp_order_id + ' \\',
141
+ ' --answer-file=' + answerPath +
142
+ (capabilities.length ? ' \\\n --capabilities=' + capabilities.join(',') : '') +
143
+ (signalsCsv ? ' \\\n --signals=' + signalsCsv : ''),
144
+ '4. If atp-complete prints "[ATP-Complete] OK asset_id=...", you are done.',
145
+ ' If it prints "FAILED", read the stage= field. Safe to retry the same command.',
146
+ '',
147
+ '# Hard rules',
148
+ '- Do NOT commit or push any repo changes -- this is a per-order side task, not a code evolution.',
149
+ '- Do NOT run `node index.js solidify` or `node index.js run`.',
150
+ '- Do NOT fabricate the answer; if you cannot answer, still run atp-complete with a short',
151
+ ' honest explanation so the buyer is not left waiting for 7 days.',
152
+ '- Keep the answer under 12k characters.',
153
+ ];
154
+ return lines.join('\n');
155
+ }
156
+
157
+ /**
158
+ * Fetch a pickup action if one is due. Idempotent -- safe to call from the
159
+ * main loop every cycle.
160
+ *
161
+ * @param {object} [opts]
162
+ * @param {number} [opts.limit=5] -- how many Hub tasks to consider per call
163
+ * @param {string} [opts.evolverExec] -- how the wrapper should invoke Evolver
164
+ * @returns {Promise<null | { spawnCall: string, task: object }>}
165
+ * null when there is nothing to do; otherwise a sessions_spawn() string
166
+ * the caller SHOULD print to stdout on its next cycle output and the task
167
+ * we picked so the caller can log it.
168
+ */
169
+ async function pickOne(opts) {
170
+ if (!_isEnabled()) return null;
171
+ const limit = Math.max(1, Math.min(20, Number(opts && opts.limit) || 5));
172
+
173
+ let listResult;
174
+ try {
175
+ listResult = await hubClient.listMyTasks(limit);
176
+ } catch (_) {
177
+ return null;
178
+ }
179
+ if (!listResult || !listResult.ok) return null;
180
+
181
+ const tasks = (listResult.data && Array.isArray(listResult.data.tasks))
182
+ ? listResult.data.tasks
183
+ : (Array.isArray(listResult.data) ? listResult.data : []);
184
+ if (!tasks.length) return null;
185
+
186
+ const ledger = _readLedger();
187
+ let picked = null;
188
+ for (const t of tasks) {
189
+ if (!_isEligible(t)) continue;
190
+ if (_recentlySpawned(ledger, t.id)) continue;
191
+ picked = t;
192
+ break;
193
+ }
194
+ if (!picked) return null;
195
+
196
+ const spawnTask = _buildSpawnTask(picked, opts);
197
+ const spawnCall = renderSessionsSpawnCall({
198
+ task: spawnTask,
199
+ agentId: 'atp_pickup',
200
+ cleanup: 'delete',
201
+ label: 'atp_pickup_' + String(picked.id).slice(0, 32),
202
+ });
203
+
204
+ ledger.spawned = ledger.spawned || {};
205
+ ledger.spawned[picked.id] = { at: Date.now(), order_id: picked.atp_order_id };
206
+ _writeLedger(ledger);
207
+
208
+ return { spawnCall, task: picked };
209
+ }
210
+
211
+ /**
212
+ * Forget a previously-spawned task so the main loop will retry it next cycle.
213
+ * Called by callers that detected the spawn channel was unavailable (e.g.
214
+ * wrapper not attached) so we do not burn the cooldown on a no-op spawn.
215
+ */
216
+ function forget(taskId) {
217
+ if (!taskId) return;
218
+ const ledger = _readLedger();
219
+ if (ledger.spawned && ledger.spawned[taskId]) {
220
+ delete ledger.spawned[taskId];
221
+ _writeLedger(ledger);
222
+ }
223
+ }
224
+
225
+ module.exports = {
226
+ pickOne,
227
+ forget,
228
+ _isEnabled,
229
+ _isEligible,
230
+ _buildSpawnTask,
231
+ _recentlySpawned,
232
+ _answerFilePath,
233
+ };
package/src/atp/index.js CHANGED
@@ -9,6 +9,8 @@
9
9
  // defaultHandler - default order handler + config helpers for auto-ATP
10
10
  // autoBuyer - opt-out capability-gap auto order helper with budget caps
11
11
  // autoDeliver - opt-out merchant-side submitDelivery daemon
12
+ // atpTaskPickup - merchant-side bridge from pre-claimed ATP tasks to sessions_spawn
13
+ // atpExecute - end-to-end completer (publish Gene+Capsule, complete, deliver)
12
14
  // cli - parsers and runners for the `buy`/`orders`/`verify` subcommands
13
15
 
14
16
  const hubClient = require('./hubClient');
@@ -18,6 +20,8 @@ const serviceHelper = require('./serviceHelper');
18
20
  const defaultHandler = require('./defaultHandler');
19
21
  const autoBuyer = require('./autoBuyer');
20
22
  const autoDeliver = require('./autoDeliver');
23
+ const atpTaskPickup = require('./atpTaskPickup');
24
+ const atpExecute = require('./atpExecute');
21
25
  const cli = require('./cli');
22
26
 
23
27
  module.exports = {
@@ -28,5 +32,7 @@ module.exports = {
28
32
  defaultHandler,
29
33
  autoBuyer,
30
34
  autoDeliver,
35
+ atpTaskPickup,
36
+ atpExecute,
31
37
  cli,
32
38
  };