tascan-mcp 3.16.2 → 3.17.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 +3 -0
- package/package.json +2 -1
- package/scopes.cjs +11 -0
- package/tools-fanout.cjs +175 -0
- package/tools.cjs +3 -1
package/README.md
CHANGED
|
@@ -288,6 +288,9 @@ API keys are scoped to your organization and support rate limiting (60 requests/
|
|
|
288
288
|
|
|
289
289
|
## Changelog
|
|
290
290
|
|
|
291
|
+
### v3.17.0 — 2026-09-28
|
|
292
|
+
- **Fan-out (phase 1b), 101 tools.** Three new tools over the phase 1a REST surface (`/coord/fanouts`, migration 224): `tascan_create_fanout` (1-30 RESEARCH seats plus one synthesizer under ONE cycle root T_F — zero Decision cards, zero Parked cards, zero pages per seat; requires `agent:dispatch:code`; fails closed `fanout_ceiling_unset` until the org owner sets a spending ceiling), `tascan_get_fanout` (`view=status|report|rollup|verify`, read tier), `tascan_control_fanout` (`pause|resume|cancel|close_barrier` — no `amend`, no `answer` in phase 1; requires `agent:dispatch:code`). `dry_run` on create is forwarded to the route as-is; the route has no server-side preflight for this path yet, so it does not yet prevent a real create — see the tool description.
|
|
293
|
+
|
|
291
294
|
### v3.16.2 — 2026-09-28
|
|
292
295
|
- 98 tools. Rolls up 3.16.0 to 3.16.2: `tascan_get_usage` (GET /usage, per-key daily actions and rate-limit headers, migration 220); the device control plane read/revoke tools (`tascan_list_devices`, `tascan_get_device`, `tascan_revoke_device`; register and rotate stay admin-app only by design); `tascan_delegate_to_agent` (child keys with a TTL of 60 s to 24 h clamped to the parent, non-delegable flags refused, the plaintext returned once) and the per-key action budgets that fail closed (migration 221). Hosted MCP at https://app.tascan.io/mcp already serves this version; this release brings the stdio package up to it.
|
|
293
296
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tascan-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.17.0",
|
|
4
4
|
"mcpName": "io.github.snowbikemike/tascan-mcp",
|
|
5
5
|
"description": "TaScan MCP Server — Closed-loop autonomous operations protocol. 70 tools for projects, tasks, workers, QR codes, NFC tags, worker marketplace with consented SMS invites, geofenced zones with rules (work site / hazard + AI-verified PPE checkpoints / containment / restricted, breach alerts, zone compliance audit), AI condition assessment, worker passports, verification-gated gig payments, client invoicing from verified work (overtime / day rate / per diem / expenses), shareable report links (completion, client service report, project, evidence pack), signed Action Receipts (Ed25519 JWS), cross-entity search, duplicate-worker detection, worker identity merge, AI issue analysis, and autonomous remediation dispatch. 15 patents filed (470+ claims). Task. Scan. Done.",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"index.js",
|
|
16
16
|
"tools.cjs",
|
|
17
17
|
"tools-devices.cjs",
|
|
18
|
+
"tools-fanout.cjs",
|
|
18
19
|
"scopes.cjs",
|
|
19
20
|
"README.md",
|
|
20
21
|
"LICENSE"
|
package/scopes.cjs
CHANGED
|
@@ -219,6 +219,11 @@ const TOOL_SCOPES = {
|
|
|
219
219
|
tascan_post_message: W,
|
|
220
220
|
// creating a cycle queues a CODE:/SHELL: prompt on the AI Inbox → agent:dispatch:code, always
|
|
221
221
|
tascan_create_cycle: { tier: 'write', dispatch: 'code' },
|
|
222
|
+
// Fan-out phase 1b (2026-09-28): three tools over coord-fanout-lib.js's REST routes — same shapes as
|
|
223
|
+
// ROUTE_SCOPES' coord_fanouts / coord_fanout / coord_fanout_control below (:363-365).
|
|
224
|
+
tascan_create_fanout: { tier: 'write', dispatch: 'code' },
|
|
225
|
+
tascan_get_fanout: R,
|
|
226
|
+
tascan_control_fanout: { tier: 'write', dispatch: 'code' },
|
|
222
227
|
// M1 (2026-09-23): ten tools wrapping routes that already existed in api-v1.js / coord-dispatch-lib.js /
|
|
223
228
|
// receipt.js but had no MCP tool. tascan_verify_receipt: receipt.js's POST /receipts/verify is a PUBLIC
|
|
224
229
|
// route (no scope check at all inside receipt.js) — read is the floor because mcp-endpoint.js still
|
|
@@ -357,6 +362,12 @@ const ROUTE_SCOPES = {
|
|
|
357
362
|
// coordination layer (api-v1-helpers.js parsePath handlers task_messages / coord_cycles / coord_cycle_report / build / build_file)
|
|
358
363
|
task_messages: { GET: R, POST: CYCLE_WRITE }, // trail messages; POST answer on a dispatcher question → coord_answer_question
|
|
359
364
|
coord_cycles: { GET: R, POST: { tier: 'write', dispatch: 'code' } }, // list roots / coord_create_cycle (T1 = CODE:/SHELL: on the AI Inbox)
|
|
365
|
+
// Fan-out phase 1a-ii (master item 4): matched in coord-fanout-lib.js's own matchFanoutRoute, not
|
|
366
|
+
// api-v1-helpers.js's parsePath ROUTES table — same D6/D8 idiom as coord_dispatcher_action/coord_build_diff below.
|
|
367
|
+
// coordFanoutLib.handleFanoutRoute self-gates with these exact shapes too (belt-and-suspenders).
|
|
368
|
+
coord_fanouts: { GET: R, POST: { tier: 'write', dispatch: 'code' } }, // GET /coord/fanouts (list) / POST (create, T_F = CODE:/SHELL:-equivalent dispatch)
|
|
369
|
+
coord_fanout: { GET: R }, // GET /coord/fanouts/:id[?view=status|report|rollup|verify]
|
|
370
|
+
coord_fanout_control: { POST: { tier: 'write', dispatch: 'code' } }, // POST /coord/fanouts/:id/control (pause|resume|cancel|close_barrier)
|
|
360
371
|
coord_integrate: { POST: WD }, // D4 (181): POST /coord/cycles/:root/integrate — the dispatcher records the deploy id (agent:dispatch)
|
|
361
372
|
coord_dispatcher_action: { POST: WD }, // D6 (194): POST /coord/cycles/:root/dispatcher-actions — a chief-of-staff action becomes a receipt (agent:dispatch); the handler self-gates too
|
|
362
373
|
coord_build_diff: { POST: WD }, // D8 item 6 (196): POST /coord/builds/:ref/diff — the dispatcher stores an already-computed diff (agent:dispatch); the handler self-gates too
|
package/tools-fanout.cjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Fan-out phase 1b (2026-09-28, briefs/2026-09-28-fanout-phase1b-mcp-tools.md; master
|
|
2
|
+
// briefs/2026-09-27-fanout-phase1.md, r3): three MCP tools over the 1a-ii REST surface
|
|
3
|
+
// (coord-fanout-lib.js's matchFanoutRoute / handleFanoutRoute, wired into api-v1.js) -- never Supabase
|
|
4
|
+
// directly, same split-file pattern as tools-devices.cjs (spread into TOOLS by one line there).
|
|
5
|
+
//
|
|
6
|
+
// tascan_create_fanout: POST /coord/fanouts. Every preflight problem the route can see (fanout_ceiling_unset,
|
|
7
|
+
// over_ceiling, cap_below_plan, idempotency_body_mismatch, fanout_open_limit, seats_invalid, reserve_floor_unset,
|
|
8
|
+
// no_authority, key_required) reaches the caller VERBATIM: api() (tascan-mcp/index.js / mcp-endpoint.js) throws
|
|
9
|
+
// Error(data.error) on a non-2xx response and the transport prints "Error: <message>" unchanged -- this file adds
|
|
10
|
+
// no try/catch around that path on purpose. dry_run is forwarded to the route as-is (literal passthrough, the
|
|
11
|
+
// brief's own word); the route built in 1a-ii has NO server-side preflight mode for this path (unlike
|
|
12
|
+
// tascan_create_cycle's dry_run, which /coord/cycles DOES implement) -- a create call with dry_run true still
|
|
13
|
+
// creates a REAL fan-out today. Documented here, in the tool description and at runtime, rather than silently
|
|
14
|
+
// guessed: fixing that gap means touching coord-fanout-lib.js, which is out of this brief's six deliverables.
|
|
15
|
+
//
|
|
16
|
+
// tascan_get_fanout: GET /coord/fanouts/:id?view=. tascan_control_fanout: POST /coord/fanouts/:id/control, action
|
|
17
|
+
// enum WITHOUT amend and WITHOUT answer (master: no such actions in phase 1) -- exactly coord-fanout-lib.js's own
|
|
18
|
+
// CONTROL_ACTIONS.
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
|
+
const VIEWS = ['status', 'report', 'rollup', 'verify'];
|
|
23
|
+
const CONTROL_ACTIONS = ['pause', 'resume', 'cancel', 'close_barrier'];
|
|
24
|
+
|
|
25
|
+
module.exports = [
|
|
26
|
+
{
|
|
27
|
+
name: 'tascan_create_fanout',
|
|
28
|
+
description: 'Create a fan-out: 1-30 RESEARCH seats plus one synthesizer under ONE cycle root T_F (POST /coord/fanouts). Zero Decision cards, zero Parked cards, zero pages per seat -- only a control action or the closing roll-up reaches a human. Requires an API key with agent:dispatch:code -- an admin session has no dispatch budget (key_required). Consumes seats.length + 1 dispatch units BEFORE the authoritative create; a refusal only the RPC can see (cap_below_plan, fanout_open_limit, bad agent_id, reserve_floor_unset) still consumes them, by design. Fails closed fanout_ceiling_unset until the owner sets a ceiling; over_ceiling when cap exceeds it. idempotency_key replays duplicate within 24h on the same plan, else idempotency_body_mismatch. dry_run is forwarded as-is; the route has no preflight mode yet, so dry_run true still creates for real -- never a way to avoid creating. Leaves fanout_created; every seat, the synth and the roll-up leave their own signed completion. Track with tascan_get_fanout.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
project_id: { type: 'string', description: 'Working project (UUID). The fan-out root T_F is minted on its Questions list (coord_ensure_lists).' },
|
|
33
|
+
title: { type: 'string', description: 'Short human title (1-200 chars). T_F becomes "FANOUT: <title>"; seats become "RESEARCH: [seat-NN] <title>"; the synth becomes "RESEARCH: [synth] <title>".' },
|
|
34
|
+
seats: {
|
|
35
|
+
type: 'array',
|
|
36
|
+
description: '1-30 seats. Each names the agent that runs it; brief/role fall back to seat_defaults when omitted.',
|
|
37
|
+
items: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
agent_id: { type: 'string', description: 'A registered agent_registry id in this org, matching ^[a-z][a-z0-9-]{0,39}$. Never a system-* worker.' },
|
|
41
|
+
brief: { type: 'string', description: 'This seat\'s own prompt (1-40000 chars). Falls back to seat_defaults.brief when omitted -- one of the two is required.' },
|
|
42
|
+
role: { type: 'string', description: 'Optional label for this seat (1-100 chars), e.g. "topic A".' }
|
|
43
|
+
},
|
|
44
|
+
required: ['agent_id']
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
seat_defaults: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
description: 'Shared defaults applied when a seat names no brief of its own.',
|
|
50
|
+
properties: {
|
|
51
|
+
brief: { type: 'string', description: 'Default seat prompt (1-40000 chars), used by any seat that names no brief of its own.' },
|
|
52
|
+
est_micro_usd: { type: 'integer', description: 'Positive integer micro-USD estimate per seat, used by the RPC\'s cap_below_plan check.' },
|
|
53
|
+
review_est_micro_usd: { type: 'integer', description: 'Non-negative integer micro-USD estimate per seat review. Must be > 0 when seat_review is per_seat.' }
|
|
54
|
+
},
|
|
55
|
+
required: ['est_micro_usd']
|
|
56
|
+
},
|
|
57
|
+
synth: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
description: 'The synthesizer that runs once the barrier closes, reading every done seat\'s stored output as DATA.',
|
|
60
|
+
properties: {
|
|
61
|
+
brief: { type: 'string', description: 'The synthesizer\'s prompt (1-40000 chars).' },
|
|
62
|
+
max_cost_micro_usd: { type: 'integer', description: 'Positive integer micro-USD reserved for the synth run; reserved out of cap up front, released back into the seat budget only at barrier close.' }
|
|
63
|
+
},
|
|
64
|
+
required: ['brief', 'max_cost_micro_usd']
|
|
65
|
+
},
|
|
66
|
+
canary: { type: 'integer', description: 'How many seats release immediately; the rest stay held on T_F until the first canary settles done. Default min(2, seats.length).' },
|
|
67
|
+
min_ok: { type: 'integer', description: 'Minimum seats that must finish done before the synth barrier can close. Default ceil(seats.length / 2).' },
|
|
68
|
+
breaker_k: { type: 'integer', description: 'Paid (non-infrastructure) seat failures before the breaker pauses the fan-out (2-5, default 2). An expired run never counts.' },
|
|
69
|
+
deadline_min: { type: 'integer', description: 'Minutes before an idle seat is dropped and the barrier re-evaluated, and before a stuck synthesizing fan-out closes deadline (10-1440, default 240).' },
|
|
70
|
+
seat_review: { type: 'string', enum: ['per_seat', 'none'], description: 'per_seat mints one review per seat (allowed for at most 10 seats); none skips seat review. Default per_seat for <=10 seats, none above that.' },
|
|
71
|
+
cap: { type: 'integer', description: 'This fan-out\'s total micro-USD budget, synth reserve included. Refused over_ceiling if it exceeds the org\'s fan-out spending ceiling.' },
|
|
72
|
+
idempotency_key: { type: 'string', description: 'Optional replay key (<= 200 chars). The same key within 24h on the same plan returns duplicate:true with the existing fanout_id; on a different plan it is refused idempotency_body_mismatch.' },
|
|
73
|
+
dry_run: { type: 'boolean', description: 'Forwarded to POST /coord/fanouts as given. The route has no server-side preflight for this path today, so true still performs a REAL create -- do not rely on it to avoid creating a fan-out.' }
|
|
74
|
+
},
|
|
75
|
+
required: ['project_id', 'title', 'seats', 'synth', 'cap']
|
|
76
|
+
},
|
|
77
|
+
annotations: { title: 'Create Fan-out', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
78
|
+
handler: async (args, api) => {
|
|
79
|
+
const projectId = String(args.project_id || '').trim();
|
|
80
|
+
if (!UUID_RE.test(projectId)) throw new Error('project_id must be a UUID');
|
|
81
|
+
if (!args.title || typeof args.title !== 'string') throw new Error('title is required');
|
|
82
|
+
if (!Array.isArray(args.seats) || !args.seats.length) throw new Error('seats is required (1-30 entries)');
|
|
83
|
+
if (!Number.isInteger(args.cap) || args.cap <= 0) throw new Error('cap is required (a positive integer, this fan-out\'s total micro-USD budget)');
|
|
84
|
+
if (!args.synth || typeof args.synth !== 'object') throw new Error('synth is required ({brief, max_cost_micro_usd})');
|
|
85
|
+
|
|
86
|
+
const body = {
|
|
87
|
+
project_id: projectId,
|
|
88
|
+
title: args.title,
|
|
89
|
+
seats: args.seats.map(s => ({
|
|
90
|
+
agent_id: s && s.agent_id,
|
|
91
|
+
...(s && s.brief != null ? { brief: s.brief } : {}),
|
|
92
|
+
...(s && s.role != null ? { role: s.role } : {})
|
|
93
|
+
})),
|
|
94
|
+
synth: { brief: args.synth.brief, max_cost_micro_usd: args.synth.max_cost_micro_usd },
|
|
95
|
+
cap: args.cap
|
|
96
|
+
};
|
|
97
|
+
if (args.seat_defaults) body.seat_defaults = args.seat_defaults;
|
|
98
|
+
for (const k of ['canary', 'min_ok', 'breaker_k', 'deadline_min', 'seat_review', 'idempotency_key']) {
|
|
99
|
+
if (args[k] != null) body[k] = args[k];
|
|
100
|
+
}
|
|
101
|
+
const dryRun = args.dry_run === true || args.dry_run === 'true';
|
|
102
|
+
if (args.dry_run != null) body.dry_run = dryRun;
|
|
103
|
+
|
|
104
|
+
const result = await api('POST', '/coord/fanouts', body, args.idempotency_key);
|
|
105
|
+
const d = result.data || {};
|
|
106
|
+
if (d.duplicate) {
|
|
107
|
+
return `DUPLICATE — nothing was created. A fan-out with the same idempotency_key and plan exists from the last 24h.\nfanout_id: ${d.fanout_id}\n\nRead it: tascan_get_fanout fanout_id=${d.fanout_id}`;
|
|
108
|
+
}
|
|
109
|
+
const seatLines = Array.isArray(d.seats)
|
|
110
|
+
? d.seats.map(s => ` seat-${String(s.seat_no).padStart(2, '0')}: ${s.seat_id} (${s.released ? 'released' : 'held on T_F'}, ${s.status})`).join('\n')
|
|
111
|
+
: ' (none returned)';
|
|
112
|
+
let text = `Fan-out created.\nfanout_id (T_F): ${d.fanout_id}\nplan_sha256: ${d.plan_sha256}\nstatus: ${d.status}\nsynth_task_id: ${d.synth_task_id}\npoll_after_s: ${d.poll_after_s}\nseats:\n${seatLines}\n\nFollow with tascan_get_fanout fanout_id=${d.fanout_id} (view=status).`;
|
|
113
|
+
if (dryRun) {
|
|
114
|
+
text = 'NOTE: dry_run has no server-side preflight on this route yet — the fan-out below was created for REAL, not simulated.\n\n' + text;
|
|
115
|
+
}
|
|
116
|
+
return text;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'tascan_get_fanout',
|
|
121
|
+
description: 'Read a fan-out (GET /coord/fanouts/:id?view=). view=status (default): state, seat counts, spend, cap, synth_deadline_at, poll_after_s. view=report: the full cycle report (get_cycle_report) — every seat, review and control step. view=rollup: the exact roll-up response_value plus its leaves (path, sha256, completion_id) once closed. view=verify: the server recomputes rollup_ref over the frozen leaves and returns ok / mismatches — the same check scripts/verify-fanout.js performs independently from another machine, which is the trusted proof; this view is a convenience, not a substitute. A foreign org\'s fanout_id answers 404, never 403, so another org\'s fan-out never even appears to exist. Read tier, no dispatch permission required.',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
fanout_id: { type: 'string', description: 'Fan-out id (UUID) — the T_F root, from tascan_create_fanout or tascan_list_cycles.' },
|
|
126
|
+
view: { type: 'string', enum: VIEWS, description: 'status (default), report, rollup or verify.' }
|
|
127
|
+
},
|
|
128
|
+
required: ['fanout_id']
|
|
129
|
+
},
|
|
130
|
+
annotations: { title: 'Get Fan-out', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
131
|
+
handler: async (args, api) => {
|
|
132
|
+
const id = String(args.fanout_id || '').trim();
|
|
133
|
+
if (!UUID_RE.test(id)) throw new Error('fanout_id must be a UUID');
|
|
134
|
+
const view = args.view != null && args.view !== '' ? String(args.view) : 'status';
|
|
135
|
+
if (!VIEWS.includes(view)) throw new Error(`view must be one of: ${VIEWS.join(', ')}`);
|
|
136
|
+
const result = await api('GET', `/coord/fanouts/${id}?view=${view}`);
|
|
137
|
+
const out = `Fan-out ${id} (view=${view}):\n\n${JSON.stringify(result.data, null, 2)}`;
|
|
138
|
+
return out.length > 12000 ? out.slice(0, 12000) + '\n…[truncated at 12000 chars]' : out;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'tascan_control_fanout',
|
|
143
|
+
description: 'Control a running fan-out (POST /coord/fanouts/:id/control). pause holds every unclaimed seat and stops the orphan pass from re-firing them; resume releases them again; cancel supersedes every unclaimed task and closes once quiescent; close_barrier drops unsettled or named exclude_seats and forces the synth barrier now instead of waiting for more seats. No amend and no answer in phase 1. Every action mints a signed control-child completion FIRST, as the org\'s own system worker (coord_fanout_control_receipt) — never coord_dispatcher_action, so a control action can never sign into another org\'s Chief of Staff identity. idempotency_key replays the prior outcome (replayed:true) and applies nothing twice; a replayed resume after a later pause leaves the fan-out paused. Refused fanout_closed once the roll-up has closed. Requires agent:dispatch:code.',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
fanout_id: { type: 'string', description: 'Fan-out id (UUID) — the T_F root.' },
|
|
148
|
+
action: { type: 'string', enum: CONTROL_ACTIONS, description: 'pause, resume, cancel or close_barrier. No amend, no answer (phase 1).' },
|
|
149
|
+
reason: { type: 'string', description: 'Optional free-text reason (<= 4000 chars), recorded on the control child\'s completion.' },
|
|
150
|
+
exclude_seats: { type: 'array', items: { type: 'integer' }, description: 'close_barrier only: 1-based seat numbers to drop from the barrier instead of waiting for them to settle.' },
|
|
151
|
+
idempotency_key: { type: 'string', description: 'Optional replay key (<= 200 chars). The same key returns the prior outcome (replayed:true) and applies nothing a second time.' }
|
|
152
|
+
},
|
|
153
|
+
required: ['fanout_id', 'action']
|
|
154
|
+
},
|
|
155
|
+
annotations: { title: 'Control Fan-out', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
156
|
+
handler: async (args, api) => {
|
|
157
|
+
const id = String(args.fanout_id || '').trim();
|
|
158
|
+
if (!UUID_RE.test(id)) throw new Error('fanout_id must be a UUID');
|
|
159
|
+
const action = String(args.action || '');
|
|
160
|
+
if (!CONTROL_ACTIONS.includes(action)) throw new Error(`action must be one of: ${CONTROL_ACTIONS.join(', ')}`);
|
|
161
|
+
const body = { action };
|
|
162
|
+
if (args.reason != null) body.reason = args.reason;
|
|
163
|
+
if (args.exclude_seats != null) {
|
|
164
|
+
if (!Array.isArray(args.exclude_seats) || !args.exclude_seats.every(n => Number.isInteger(n) && n > 0)) {
|
|
165
|
+
throw new Error('exclude_seats must be an array of positive integers');
|
|
166
|
+
}
|
|
167
|
+
body.exclude_seats = args.exclude_seats;
|
|
168
|
+
}
|
|
169
|
+
if (args.idempotency_key != null) body.idempotency_key = args.idempotency_key;
|
|
170
|
+
const result = await api('POST', `/coord/fanouts/${id}/control`, body, args.idempotency_key);
|
|
171
|
+
const d = result.data || {};
|
|
172
|
+
return `${d.replayed ? 'REPLAYED (idempotent — nothing applied twice)' : 'Applied'}: ${action} on fan-out ${id}.\nfanout_status: ${d.fanout_status}\nreceipt_completion_id: ${d.receipt_completion_id}\n\n${JSON.stringify(d, null, 2)}`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
];
|
package/tools.cjs
CHANGED
|
@@ -2824,7 +2824,9 @@ const TOOLS = [
|
|
|
2824
2824
|
}
|
|
2825
2825
|
},
|
|
2826
2826
|
// P0 (migration 216): device control plane — split out (194 KB already close to the ~200 KB bundle cap).
|
|
2827
|
-
...require('./tools-devices.cjs')
|
|
2827
|
+
...require('./tools-devices.cjs'),
|
|
2828
|
+
// Fan-out phase 1b (2026-09-28): three tools over the 1a-ii REST surface — split out, same reason.
|
|
2829
|
+
...require('./tools-fanout.cjs')
|
|
2828
2830
|
];
|
|
2829
2831
|
|
|
2830
2832
|
module.exports = { TOOLS, AGENT_REGISTRY, dynamicAgents };
|