tascan-mcp 3.17.0 → 3.18.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 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.18.0 — 2026-09-28
292
+ - **Fan-out (phase 2, item 1: subagent seats), 104 tools.** A fan-out seat may now name `runner: 'local'` (`tascan_create_fanout` `seats[i].runner`, default `research`) — a seat claimable ONLY by its own registered local/subagent instance, never webhook-fired from the cloud. Three new tools over the seat's own run lifecycle (`/runs/claim`, `/runs/:id/heartbeat`, `/runs/:id/finish`, migration 229): `tascan_claim_run` (claims as `"<agent_id>:<anything>"`; a foreign instance is refused `seat_not_yours`, before routing is even evaluated), `tascan_heartbeat_run` (extends the lease; `{ok:false, reason:'not_live'}` means stop), `tascan_finish_run` (stores `document`[/`verification`] as the seat's own build artifacts, hashes `build_ref`, and finishes the run — a real signed completion, exactly like a research seat's or a CODE build's; zero changes to `coord_fanout_settle` / the barrier / the roll-up). All three require `agent:dispatch:code`; a run or task outside your org is 404, never 403.
293
+
291
294
  ### v3.17.0 — 2026-09-28
292
295
  - **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
296
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tascan-mcp",
3
- "version": "3.17.0",
3
+ "version": "3.18.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",
@@ -16,6 +16,7 @@
16
16
  "tools.cjs",
17
17
  "tools-devices.cjs",
18
18
  "tools-fanout.cjs",
19
+ "tools-runs.cjs",
19
20
  "scopes.cjs",
20
21
  "README.md",
21
22
  "LICENSE"
package/scopes.cjs CHANGED
@@ -224,6 +224,11 @@ const TOOL_SCOPES = {
224
224
  tascan_create_fanout: { tier: 'write', dispatch: 'code' },
225
225
  tascan_get_fanout: R,
226
226
  tascan_control_fanout: { tier: 'write', dispatch: 'code' },
227
+ // Fan-out phase 2 item 1 (2026-09-28): three tools over coord-run-lib.js's REST routes -- same shapes as
228
+ // ROUTE_SCOPES' runs_claim / run_heartbeat / run_finish above.
229
+ tascan_claim_run: { tier: 'write', dispatch: 'code' },
230
+ tascan_heartbeat_run: { tier: 'write', dispatch: 'code' },
231
+ tascan_finish_run: { tier: 'write', dispatch: 'code' },
227
232
  // M1 (2026-09-23): ten tools wrapping routes that already existed in api-v1.js / coord-dispatch-lib.js /
228
233
  // receipt.js but had no MCP tool. tascan_verify_receipt: receipt.js's POST /receipts/verify is a PUBLIC
229
234
  // route (no scope check at all inside receipt.js) — read is the floor because mcp-endpoint.js still
@@ -368,6 +373,12 @@ const ROUTE_SCOPES = {
368
373
  coord_fanouts: { GET: R, POST: { tier: 'write', dispatch: 'code' } }, // GET /coord/fanouts (list) / POST (create, T_F = CODE:/SHELL:-equivalent dispatch)
369
374
  coord_fanout: { GET: R }, // GET /coord/fanouts/:id[?view=status|report|rollup|verify]
370
375
  coord_fanout_control: { POST: { tier: 'write', dispatch: 'code' } }, // POST /coord/fanouts/:id/control (pause|resume|cancel|close_barrier)
376
+ // Fan-out phase 2 item 1 (2026-09-28): matched in coord-run-lib.js's own matchRunRoute, same D6/D8 idiom as the
377
+ // fan-out routes above -- a LOCAL fan-out seat's own run lifecycle (migration 229). coordRunLib's three handlers
378
+ // self-gate with these exact shapes too (belt-and-suspenders, same reason coord-fanout-lib.js documents).
379
+ runs_claim: { POST: { tier: 'write', dispatch: 'code' } }, // POST /runs/claim -- claim_agent_run(p_runner='local') for a subagent seat
380
+ run_heartbeat: { POST: { tier: 'write', dispatch: 'code' } }, // POST /runs/:id/heartbeat
381
+ run_finish: { POST: { tier: 'write', dispatch: 'code' } }, // POST /runs/:id/finish -- stores document[/verification], finish_agent_run
371
382
  coord_integrate: { POST: WD }, // D4 (181): POST /coord/cycles/:root/integrate — the dispatcher records the deploy id (agent:dispatch)
372
383
  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
373
384
  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-runs.cjs ADDED
@@ -0,0 +1,110 @@
1
+ // Fan-out phase 2, item 1 (2026-09-28, briefs/2026-09-28-fanout-phase2-subagent-seats.md): three MCP tools over
2
+ // coord-run-lib.js's REST surface (POST /runs/claim, POST /runs/:id/heartbeat, POST /runs/:id/finish) -- never
3
+ // Supabase directly, same split-file pattern as tools-devices.cjs / tools-fanout.cjs (spread into TOOLS by one
4
+ // line there).
5
+ //
6
+ // The Workflow: a fan-out seat named with runner:'local' (tascan_create_fanout, seats[i].runner) is claimable ONLY
7
+ // by its own registered agent -- p_instance ("<agent_id>:<anything>") must start with the seat's own coord.agent_id
8
+ // (migration 229's claim_agent_run gate). A subagent that is new registers itself first (tascan_register_agent,
9
+ // type 'local' or 'subagent'), then: tascan_claim_run -> works -> tascan_finish_run with its document. The seat's
10
+ // completion is a normal, signed TaScan receipt (tascan_get_receipt), exactly like a research seat's or a CODE
11
+ // build's -- coord_fanout_settle / the barrier / the roll-up need no awareness that the seat was local at all
12
+ // (migration 229's own header note: "prove it, do not re-implement it").
13
+ 'use strict';
14
+
15
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16
+
17
+ module.exports = [
18
+ {
19
+ name: 'tascan_claim_run',
20
+ description: 'Claim a fan-out seat task as a LOCAL subagent run (POST /runs/claim -> claim_agent_run(p_runner=\'local\')). Only admitted when instance starts with "<your registered agent_id>:" -- the seat\'s own coord.agent_id, set when the fan-out was created (tascan_create_fanout seats[i].runner=\'local\'). Any other instance is refused, claimed false with reason seat_not_yours -- this tool never claims a seat that is not yours, and never claims a cloud (research) seat at all (refused not_routable). Never mints a completion or a receipt by itself -- claiming only opens the run; work happens after, and tascan_finish_run is what leaves the signed receipt. Requires agent:dispatch:code. A foreign or unknown task_id is 404, never 403 (S2: another org\'s task never even appears to exist).',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ task_id: { type: 'string', description: 'The seat task ID (UUID) -- from tascan_create_fanout\'s seats[] or tascan_get_fanout (view=status/report).' },
25
+ instance: { type: 'string', description: 'Your claim identity, "<agent_id>:<anything>" -- e.g. "researcher-07:host-1234". Must start with the seat\'s own registered agent_id or the claim is refused seat_not_yours.' },
26
+ ttl_seconds: { type: 'integer', description: 'Optional lease length in seconds (60-86400, default 1800). Extend it later with tascan_heartbeat_run instead of claiming a longer one up front.' }
27
+ },
28
+ required: ['task_id', 'instance']
29
+ },
30
+ annotations: { title: 'Claim Run', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
31
+ handler: async (args, api) => {
32
+ const taskId = String(args.task_id || '').trim();
33
+ if (!UUID_RE.test(taskId)) throw new Error('task_id must be a UUID');
34
+ const instance = typeof args.instance === 'string' ? args.instance.trim() : '';
35
+ if (!instance) throw new Error('instance is required, "<agent_id>:<anything>"');
36
+ const body = { task_id: taskId, instance };
37
+ if (args.ttl_seconds != null) body.ttl_seconds = args.ttl_seconds;
38
+ const result = await api('POST', '/runs/claim', body);
39
+ const d = result.data || {};
40
+ if (d.claimed !== true) {
41
+ return `NOT claimed -- reason: ${d.reason}\n\n${JSON.stringify(d, null, 2)}`;
42
+ }
43
+ return `Claimed. run_id: ${d.run_id}\nroute: ${d.route}\nattempt: ${d.attempt} / max_attempts: ${d.max_attempts}\nexpires_at: ${d.expires_at}\ncycle_root: ${d.cycle_root}\nbuild_ref (the cycle's own binding, if any): ${d.build_ref}\n\nNext: work, optionally tascan_heartbeat_run before the lease expires, then tascan_finish_run with your document.`;
44
+ }
45
+ },
46
+ {
47
+ name: 'tascan_heartbeat_run',
48
+ description: 'Extend a claimed local run\'s lease and mark it running (POST /runs/:run_id/heartbeat -> heartbeat_agent_run). ok false with reason not_live means the run is no longer yours -- the sweep expired it, or it was never yours to begin with -- STOP, do not finish it. Never mints a completion or a receipt. Requires agent:dispatch:code. A run outside your org is 404, never 403.',
49
+ inputSchema: {
50
+ type: 'object',
51
+ properties: {
52
+ run_id: { type: 'string', description: 'The run ID from tascan_claim_run\'s response.' },
53
+ instance: { type: 'string', description: 'The SAME instance string you claimed with -- a mismatch answers not_live.' },
54
+ extend_seconds: { type: 'integer', description: 'Optional: push expires_at out to now + this many seconds (60-86400) if that is later than the current lease -- never shortens it.' }
55
+ },
56
+ required: ['run_id', 'instance']
57
+ },
58
+ annotations: { title: 'Heartbeat Run', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
59
+ handler: async (args, api) => {
60
+ const runId = String(args.run_id || '').trim();
61
+ if (!UUID_RE.test(runId)) throw new Error('run_id must be a UUID');
62
+ const instance = typeof args.instance === 'string' ? args.instance.trim() : '';
63
+ if (!instance) throw new Error('instance is required (the same one you claimed with)');
64
+ const body = { instance };
65
+ if (args.extend_seconds != null) body.extend_seconds = args.extend_seconds;
66
+ const result = await api('POST', `/runs/${runId}/heartbeat`, body);
67
+ const d = result.data || {};
68
+ return d.ok ? `Alive. state: ${d.state}, expires_at: ${d.expires_at}, attempt: ${d.attempt}` : `NOT live -- reason: ${d.reason}, state: ${d.state}. Stop; do not finish this run.`;
69
+ }
70
+ },
71
+ {
72
+ name: 'tascan_finish_run',
73
+ description: 'Finish a claimed local run (POST /runs/:run_id/finish). outcome=\'completed\' stores `document` (and optional `verification`) as the seat\'s own build artifacts under EXACTLY the artifact_paths the fan-out named for this seat, hashes them into build_ref, and finishes the run -- this mints a real task_completions row and a SIGNED TASCAN RECEIPT for your work (tascan_get_receipt), the same as any other build; `document` is your deliverable and IS what gets hashed, so send the real content, not a summary. Zero changes to coord_fanout_settle / the barrier / the roll-up -- your completion releases the next held seat and, once every seat settles, feeds the fan-out\'s own synthesizer and rollup exactly like a cloud research seat\'s would. outcome=\'failed\' finishes the run failed with your `error` text and stores nothing. Requires agent:dispatch:code. A run outside your org, or already finished, is refused (404 / the RPC\'s own not_live).',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ run_id: { type: 'string', description: 'The run ID from tascan_claim_run\'s response.' },
78
+ instance: { type: 'string', description: 'The SAME instance string you claimed with.' },
79
+ outcome: { type: 'string', enum: ['completed', 'failed'], description: '\'completed\' stores your document and mints a signed completion; \'failed\' stores nothing.' },
80
+ document: { type: 'string', description: 'Required when outcome is completed. The seat\'s deliverable -- hashed as build_ref, stored verbatim under the seat\'s own artifact_paths.' },
81
+ verification: { type: 'string', description: 'Optional second document (a short verification/self-check note) -- stored as the seat\'s second artifact_path when the fan-out named two.' },
82
+ error: { type: 'string', description: 'Required when outcome is failed -- why (up to 4000 chars).' },
83
+ cost_micro_usd: { type: 'integer', description: 'Optional micro-USD cost to record against the cycle\'s own budget (default 0 -- a Claude Code subscription seat has no per-call API cost to report).' }
84
+ },
85
+ required: ['run_id', 'instance', 'outcome']
86
+ },
87
+ annotations: { title: 'Finish Run', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
88
+ handler: async (args, api) => {
89
+ const runId = String(args.run_id || '').trim();
90
+ if (!UUID_RE.test(runId)) throw new Error('run_id must be a UUID');
91
+ const instance = typeof args.instance === 'string' ? args.instance.trim() : '';
92
+ if (!instance) throw new Error('instance is required (the same one you claimed with)');
93
+ const outcome = String(args.outcome || '');
94
+ if (!['completed', 'failed'].includes(outcome)) throw new Error('outcome must be "completed" or "failed"');
95
+ if (outcome === 'completed' && (typeof args.document !== 'string' || !args.document.trim())) {
96
+ throw new Error('document is required when outcome is "completed"');
97
+ }
98
+ const body = { instance, outcome };
99
+ if (args.document != null) body.document = args.document;
100
+ if (args.verification != null) body.verification = args.verification;
101
+ if (args.error != null) body.error = args.error;
102
+ if (args.cost_micro_usd != null) body.cost_micro_usd = args.cost_micro_usd;
103
+ const result = await api('POST', `/runs/${runId}/finish`, body);
104
+ const d = result.data || {};
105
+ if (outcome === 'failed') return `Finished failed. ok: ${d.ok}, state: ${d.state}.`;
106
+ if (d.ok === false) return `NOT accepted -- reason: ${d.reason}, state: ${d.state}. The seat is still unfinished; nothing was released.`;
107
+ return `Finished completed. completion_id: ${result.completion_id}\nbuild_ref: ${result.build_ref}\nrun state: ${d.state}${d.advance ? '\nadvance: ' + JSON.stringify(d.advance) : ''}\n\nThis is a signed receipt -- tascan_get_receipt completion_id=${result.completion_id} to fetch it.`;
108
+ }
109
+ }
110
+ ];
package/tools.cjs CHANGED
@@ -2826,7 +2826,9 @@ const TOOLS = [
2826
2826
  // P0 (migration 216): device control plane — split out (194 KB already close to the ~200 KB bundle cap).
2827
2827
  ...require('./tools-devices.cjs'),
2828
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')
2829
+ ...require('./tools-fanout.cjs'),
2830
+ // Fan-out phase 2 item 1 (2026-09-28): three tools over a LOCAL seat's own run lifecycle — split out, same reason.
2831
+ ...require('./tools-runs.cjs')
2830
2832
  ];
2831
2833
 
2832
2834
  module.exports = { TOOLS, AGENT_REGISTRY, dynamicAgents };