tascan-mcp 3.17.0 → 3.19.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 +6 -0
- package/package.json +2 -1
- package/scopes.cjs +14 -0
- package/tools-fanout.cjs +20 -18
- package/tools-runs.cjs +110 -0
- package/tools.cjs +35 -1
package/README.md
CHANGED
|
@@ -288,6 +288,12 @@ API keys are scoped to your organization and support rate limiting (60 requests/
|
|
|
288
288
|
|
|
289
289
|
## Changelog
|
|
290
290
|
|
|
291
|
+
### v3.19.0 — 2026-09-28
|
|
292
|
+
- **Quickstart (105 tools).** `tascan_quickstart` (`POST /api/v1/quickstart`, write tier) takes a new user from nothing to a receipt whose `verification.result.value` is true in one action: it reuses-or-creates the project "TaScan Quickstart", its list "Quickstart" and the worker "Quickstart executor", adds a new task "Reply with the exact text: <expected>" (`expected` defaults to `VERIFIED`), completes it with your key, has the new `exact_output` policy `quickstart_exact_output` v1 (migration 237) record `verified` or `refuted` (pass a different `response` to see the refutation), and publishes the receipt's public profile. It returns the receipt URL and the public verify URL. Completed means someone said it was done; verified means a named policy checked it and signed the result.
|
|
293
|
+
|
|
294
|
+
### v3.18.0 — 2026-09-28
|
|
295
|
+
- **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.
|
|
296
|
+
|
|
291
297
|
### v3.17.0 — 2026-09-28
|
|
292
298
|
- **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
299
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tascan-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.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
|
|
@@ -270,6 +275,8 @@ const TOOL_SCOPES = {
|
|
|
270
275
|
tascan_revoke_device: { tier: 'full', device_admin: true },
|
|
271
276
|
// B1 (migration 220): GET /usage wrapped as a tool — read tier, same as every other read-only usage/analytics tool.
|
|
272
277
|
tascan_get_usage: R,
|
|
278
|
+
// Quickstart (migration 237): creates rows and a completion under the caller's org -- write tier, like tascan_complete_task.
|
|
279
|
+
tascan_quickstart: W,
|
|
273
280
|
// B2 (migration 221): per-key budgets + delegated child keys. tascan_get_budget is the caller's own row (read
|
|
274
281
|
// tier, same idiom as tascan_get_usage). tascan_delegate_to_agent mints (NOT destructiveHint — see tools.cjs);
|
|
275
282
|
// tascan_revoke_delegation is destructive+idempotent. Both need write + delegate (owner-only to grant, I-6).
|
|
@@ -312,6 +319,7 @@ const ROUTE_SCOPES = {
|
|
|
312
319
|
list_report: { GET: R },
|
|
313
320
|
task: { GET: R, PUT: W, DELETE: W }, // ANY PUT on an inbox task re-checked in handleTask (dispatch / code by existing+new title); DELETE stays write (kill switch)
|
|
314
321
|
task_complete: { POST: W },
|
|
322
|
+
quickstart: { POST: W }, // POST /quickstart (migration 237): project+list+worker+task+completion+verdict+publish in one call
|
|
315
323
|
task_subtasks: { GET: R, POST: W },
|
|
316
324
|
subtask: { GET: R, PUT: W, DELETE: W },
|
|
317
325
|
subtask_complete: { POST: W },
|
|
@@ -368,6 +376,12 @@ const ROUTE_SCOPES = {
|
|
|
368
376
|
coord_fanouts: { GET: R, POST: { tier: 'write', dispatch: 'code' } }, // GET /coord/fanouts (list) / POST (create, T_F = CODE:/SHELL:-equivalent dispatch)
|
|
369
377
|
coord_fanout: { GET: R }, // GET /coord/fanouts/:id[?view=status|report|rollup|verify]
|
|
370
378
|
coord_fanout_control: { POST: { tier: 'write', dispatch: 'code' } }, // POST /coord/fanouts/:id/control (pause|resume|cancel|close_barrier)
|
|
379
|
+
// Fan-out phase 2 item 1 (2026-09-28): matched in coord-run-lib.js's own matchRunRoute, same D6/D8 idiom as the
|
|
380
|
+
// fan-out routes above -- a LOCAL fan-out seat's own run lifecycle (migration 229). coordRunLib's three handlers
|
|
381
|
+
// self-gate with these exact shapes too (belt-and-suspenders, same reason coord-fanout-lib.js documents).
|
|
382
|
+
runs_claim: { POST: { tier: 'write', dispatch: 'code' } }, // POST /runs/claim -- claim_agent_run(p_runner='local') for a subagent seat
|
|
383
|
+
run_heartbeat: { POST: { tier: 'write', dispatch: 'code' } }, // POST /runs/:id/heartbeat
|
|
384
|
+
run_finish: { POST: { tier: 'write', dispatch: 'code' } }, // POST /runs/:id/finish -- stores document[/verification], finish_agent_run
|
|
371
385
|
coord_integrate: { POST: WD }, // D4 (181): POST /coord/cycles/:root/integrate — the dispatcher records the deploy id (agent:dispatch)
|
|
372
386
|
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
387
|
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
CHANGED
|
@@ -7,25 +7,26 @@
|
|
|
7
7
|
// over_ceiling, cap_below_plan, idempotency_body_mismatch, fanout_open_limit, seats_invalid, reserve_floor_unset,
|
|
8
8
|
// no_authority, key_required) reaches the caller VERBATIM: api() (tascan-mcp/index.js / mcp-endpoint.js) throws
|
|
9
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
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
10
|
+
// no try/catch around that path on purpose. dry_run is forwarded to the route as-is; coord-fanout-lib.js's
|
|
11
|
+
// handleCreateFanout (229) honors it as a REAL preflight -- validation plus the ceiling/cap pre-check, no replay
|
|
12
|
+
// lookup, no budget consumption, no coord_create_fanout call -- and returns {dry_run:true, plan_sha256, seats,
|
|
13
|
+
// problems:[]} instead of a fanout_id. Every seats[] entry also forwards runner ('research' default, 'local'
|
|
14
|
+
// names a registered local/subagent seat) so a local-seat plan is never silently turned into an all-research plan.
|
|
15
15
|
//
|
|
16
16
|
// tascan_get_fanout: GET /coord/fanouts/:id?view=. tascan_control_fanout: POST /coord/fanouts/:id/control, action
|
|
17
17
|
// enum WITHOUT amend and WITHOUT answer (master: no such actions in phase 1) -- exactly coord-fanout-lib.js's own
|
|
18
|
-
// CONTROL_ACTIONS.
|
|
18
|
+
// CONTROL_ACTIONS. 'retry_synth' (migration 232) added alongside pause/resume/cancel/close_barrier: mints a synth
|
|
19
|
+
// revision when the current synth's every failed run is input_ref_mismatch.
|
|
19
20
|
'use strict';
|
|
20
21
|
|
|
21
22
|
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
23
|
const VIEWS = ['status', 'report', 'rollup', 'verify'];
|
|
23
|
-
const CONTROL_ACTIONS = ['pause', 'resume', 'cancel', 'close_barrier'];
|
|
24
|
+
const CONTROL_ACTIONS = ['pause', 'resume', 'cancel', 'close_barrier', 'retry_synth'];
|
|
24
25
|
|
|
25
26
|
module.exports = [
|
|
26
27
|
{
|
|
27
28
|
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).
|
|
29
|
+
description: 'Create a fan-out: 1-30 seats (RESEARCH by default, or LOCAL subagent 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). dry_run true runs the same validation plus the ceiling/cap pre-check and returns {dry_run:true, plan_sha256, seats, problems:[]} WITHOUT consuming budget or creating anything -- a real preflight, not a real create. Otherwise 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. Leaves fanout_created; every seat, the synth and the roll-up leave their own signed completion. Track with tascan_get_fanout.',
|
|
29
30
|
inputSchema: {
|
|
30
31
|
type: 'object',
|
|
31
32
|
properties: {
|
|
@@ -39,7 +40,8 @@ module.exports = [
|
|
|
39
40
|
properties: {
|
|
40
41
|
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
42
|
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
|
+
role: { type: 'string', description: 'Optional label for this seat (1-100 chars), e.g. "topic A".' },
|
|
44
|
+
runner: { type: 'string', enum: ['research', 'local'], description: 'Default "research" (the cloud runner claims it). "local" names a registered local/subagent agent_id -- only that agent, claiming as itself (tascan_claim_run instance starting with "<agent_id>:"), can claim the seat; it is never webhook-fired.' }
|
|
43
45
|
},
|
|
44
46
|
required: ['agent_id']
|
|
45
47
|
}
|
|
@@ -70,7 +72,7 @@ module.exports = [
|
|
|
70
72
|
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
73
|
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
74
|
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: '
|
|
75
|
+
dry_run: { type: 'boolean', description: 'true runs the same validation plus the ceiling/cap pre-check and returns {dry_run:true, plan_sha256, seats, problems:[]} WITHOUT consuming budget or creating a fan-out -- a real preflight, not a real create.' }
|
|
74
76
|
},
|
|
75
77
|
required: ['project_id', 'title', 'seats', 'synth', 'cap']
|
|
76
78
|
},
|
|
@@ -89,7 +91,8 @@ module.exports = [
|
|
|
89
91
|
seats: args.seats.map(s => ({
|
|
90
92
|
agent_id: s && s.agent_id,
|
|
91
93
|
...(s && s.brief != null ? { brief: s.brief } : {}),
|
|
92
|
-
...(s && s.role != null ? { role: s.role } : {})
|
|
94
|
+
...(s && s.role != null ? { role: s.role } : {}),
|
|
95
|
+
...(s && s.runner != null ? { runner: s.runner } : {})
|
|
93
96
|
})),
|
|
94
97
|
synth: { brief: args.synth.brief, max_cost_micro_usd: args.synth.max_cost_micro_usd },
|
|
95
98
|
cap: args.cap
|
|
@@ -103,17 +106,16 @@ module.exports = [
|
|
|
103
106
|
|
|
104
107
|
const result = await api('POST', '/coord/fanouts', body, args.idempotency_key);
|
|
105
108
|
const d = result.data || {};
|
|
109
|
+
if (d.dry_run) {
|
|
110
|
+
return `DRY RUN — nothing was created, no budget consumed.\nplan_sha256: ${d.plan_sha256}\nseats: ${d.seats}\nproblems: ${JSON.stringify(d.problems || [])}`;
|
|
111
|
+
}
|
|
106
112
|
if (d.duplicate) {
|
|
107
113
|
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
114
|
}
|
|
109
115
|
const seatLines = Array.isArray(d.seats)
|
|
110
116
|
? 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
117
|
: ' (none returned)';
|
|
112
|
-
|
|
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;
|
|
118
|
+
return `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).`;
|
|
117
119
|
}
|
|
118
120
|
},
|
|
119
121
|
{
|
|
@@ -140,12 +142,12 @@ module.exports = [
|
|
|
140
142
|
},
|
|
141
143
|
{
|
|
142
144
|
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.',
|
|
145
|
+
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; retry_synth (migration 232) mints a new synth revision from the current synth\'s own stored inputs, correctly ordered, when every one of its failed runs is input_ref_mismatch -- refused otherwise (not_synthesizing, no_synth_task, no_failed_runs, not_input_ref_mismatch). 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
146
|
inputSchema: {
|
|
145
147
|
type: 'object',
|
|
146
148
|
properties: {
|
|
147
149
|
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
|
|
150
|
+
action: { type: 'string', enum: CONTROL_ACTIONS, description: 'pause, resume, cancel, close_barrier or retry_synth. No amend, no answer (phase 1).' },
|
|
149
151
|
reason: { type: 'string', description: 'Optional free-text reason (<= 4000 chars), recorded on the control child\'s completion.' },
|
|
150
152
|
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
153
|
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.' }
|
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
|
@@ -2823,10 +2823,44 @@ const TOOLS = [
|
|
|
2823
2823
|
return d.already_revoked ? `Key ${args.key_id} was already revoked.` : `Key ${args.key_id} revoked.`;
|
|
2824
2824
|
}
|
|
2825
2825
|
},
|
|
2826
|
+
// Quickstart (migration 237): one action to a verified receipt.
|
|
2827
|
+
{
|
|
2828
|
+
name: 'tascan_quickstart',
|
|
2829
|
+
description: 'One action to a genuinely verified receipt (POST /quickstart). Under your org it reuses-or-creates the project "TaScan Quickstart", its list "Quickstart" and the worker "Quickstart executor", adds a NEW task "Reply with the exact text: <expected>", completes it with your key, has the named policy quickstart_exact_output v1 compare the response to the expected text (verified or refuted, signed into the receipt), and publishes the receipt\'s public profile. expected defaults to "VERIFIED" (max 200 chars); response defaults to expected — pass a different response to see a refuted receipt. Returns the receipt URL and the public verify URL. Write tier.',
|
|
2830
|
+
inputSchema: {
|
|
2831
|
+
type: 'object',
|
|
2832
|
+
properties: {
|
|
2833
|
+
expected: { type: 'string', description: 'The exact text the task asks for (default "VERIFIED", max 200 chars).' },
|
|
2834
|
+
response: { type: 'string', description: 'The response to submit (default: the expected text). A different value is refuted.' }
|
|
2835
|
+
}
|
|
2836
|
+
},
|
|
2837
|
+
annotations: { title: 'Quickstart: First Verified Receipt', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
2838
|
+
handler: async (args, api) => {
|
|
2839
|
+
const body = {};
|
|
2840
|
+
if (args.expected != null) body.expected = args.expected;
|
|
2841
|
+
if (args.response != null) body.response = args.response;
|
|
2842
|
+
const result = await api('POST', '/quickstart', body);
|
|
2843
|
+
const d = result.data || {};
|
|
2844
|
+
const pol = d.policy || {};
|
|
2845
|
+
const lines = [
|
|
2846
|
+
`Created (or reused by name): project "TaScan Quickstart" ${d.project_id}, list "Quickstart" ${d.list_id}, worker "Quickstart executor" ${d.worker_id}, and a new task ${d.task_id}.`,
|
|
2847
|
+
'Completed: yes (by your key)',
|
|
2848
|
+
d.verified
|
|
2849
|
+
? `Verified: yes — policy ${pol.id} v${pol.version} compared the response to the expected text`
|
|
2850
|
+
: 'Verified: NO — the response did not match; the receipt records the refutation',
|
|
2851
|
+
`Receipt: ${d.receipt_url}`,
|
|
2852
|
+
`Public verify page: ${d.verify_url}` + (d.published ? '' : ` (not public yet — publish failed: ${d.publish_error || 'unknown'})`),
|
|
2853
|
+
'Completed means someone said it was done. Verified means a named policy checked it and signed the result.'
|
|
2854
|
+
];
|
|
2855
|
+
return lines.join('\n');
|
|
2856
|
+
}
|
|
2857
|
+
},
|
|
2826
2858
|
// P0 (migration 216): device control plane — split out (194 KB already close to the ~200 KB bundle cap).
|
|
2827
2859
|
...require('./tools-devices.cjs'),
|
|
2828
2860
|
// Fan-out phase 1b (2026-09-28): three tools over the 1a-ii REST surface — split out, same reason.
|
|
2829
|
-
...require('./tools-fanout.cjs')
|
|
2861
|
+
...require('./tools-fanout.cjs'),
|
|
2862
|
+
// Fan-out phase 2 item 1 (2026-09-28): three tools over a LOCAL seat's own run lifecycle — split out, same reason.
|
|
2863
|
+
...require('./tools-runs.cjs')
|
|
2830
2864
|
];
|
|
2831
2865
|
|
|
2832
2866
|
module.exports = { TOOLS, AGENT_REGISTRY, dynamicAgents };
|