tascan-mcp 3.13.1 → 3.14.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/index.js +1 -1
- package/package.json +55 -55
- package/scopes.cjs +28 -3
- package/tools.cjs +235 -6
package/README.md
CHANGED
|
@@ -268,6 +268,12 @@ API keys are scoped to your organization and support rate limiting (60 requests/
|
|
|
268
268
|
|
|
269
269
|
## Changelog
|
|
270
270
|
|
|
271
|
+
### v3.14.0 — 2026-09-18
|
|
272
|
+
- **Coordination layer live** (migration 155): `tascan_create_cycle`, `tascan_get_cycle_report`, `tascan_get_build`, `tascan_get_build_file`, `tascan_post_message` now run against the released schema — build → independent review → one human decision → integrate, with signed receipts at every step.
|
|
273
|
+
- **Scheduled SMS** (migration 157): `tascan_schedule_sms` (full tier), `tascan_list_scheduled_sms` (read), `tascan_cancel_scheduled_sms` (full) — TaScan's own 5-minute scheduler sends through the same guarded lane as `tascan_send_sms`; nothing outside TaScan has to stay awake.
|
|
274
|
+
- `tascan_get_receipt`: `profile` argument (`full` | `public`); public-profile receipts are now published for the protocol page's build trail.
|
|
275
|
+
- 78 tools. Hosted MCP (https://app.tascan.io/mcp) already serves this version; this release brings the stdio package to parity.
|
|
276
|
+
|
|
271
277
|
### v3.13.1 — 2026-09-14
|
|
272
278
|
- Docs only. `tascan_get_task` and `tascan_get_receipt` now say it plainly: a completion with status `completed` means the executor returned and a result was recorded — it does **not** mean the result was accepted. Acceptance is the receipt's `verification.result` under a named policy, and in v0.1 no policy runs for agent tasks (`no_policy_run`), so read the recorded response before treating an agent completion as success (protocol §8.3 C11). The receipt description also lists the eleven-step verifier (65,536-byte size gate, real-calendar datetimes, GPS bounds, token-free locators, `verifier.identity` vocabulary). No tool additions — still 70 tools.
|
|
273
279
|
|
package/index.js
CHANGED
|
@@ -46,7 +46,7 @@ async function api(method, path, body) {
|
|
|
46
46
|
api.key = API_KEY; // raw-fetch tools read the key from here (S99: those functions now require auth)
|
|
47
47
|
|
|
48
48
|
const server = new Server(
|
|
49
|
-
{ name: 'tascan', version: '3.
|
|
49
|
+
{ name: 'tascan', version: '3.14.0' },
|
|
50
50
|
{ capabilities: { tools: {} } }
|
|
51
51
|
);
|
|
52
52
|
|
package/package.json
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "tascan-mcp",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"mcpName": "io.github.snowbikemike/tascan-mcp",
|
|
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
|
-
"type": "module",
|
|
7
|
-
"main": "index.js",
|
|
8
|
-
"bin": {
|
|
9
|
-
"tascan-mcp": "index.js"
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"start": "node index.js"
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"index.js",
|
|
16
|
-
"tools.cjs",
|
|
17
|
-
"scopes.cjs",
|
|
18
|
-
"README.md",
|
|
19
|
-
"LICENSE"
|
|
20
|
-
],
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
23
|
-
"zod": "^4.3.6"
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"tascan",
|
|
27
|
-
"mcp",
|
|
28
|
-
"model-context-protocol",
|
|
29
|
-
"ai",
|
|
30
|
-
"task-management",
|
|
31
|
-
"qr-code",
|
|
32
|
-
"field-operations",
|
|
33
|
-
"workforce",
|
|
34
|
-
"pwa",
|
|
35
|
-
"claude"
|
|
36
|
-
],
|
|
37
|
-
"author": {
|
|
38
|
-
"name": "Michael Edward Love II",
|
|
39
|
-
"email": "Michael@TaScan.io",
|
|
40
|
-
"url": "https://tascan.io"
|
|
41
|
-
},
|
|
42
|
-
"repository": {
|
|
43
|
-
"type": "git",
|
|
44
|
-
"url": "git+https://github.com/snowbikemike/tascan-mcp.git"
|
|
45
|
-
},
|
|
46
|
-
"homepage": "https://tascan.io",
|
|
47
|
-
"bugs": {
|
|
48
|
-
"url": "https://github.com/snowbikemike/tascan-mcp/issues",
|
|
49
|
-
"email": "Michael@TaScan.io"
|
|
50
|
-
},
|
|
51
|
-
"license": "MIT",
|
|
52
|
-
"engines": {
|
|
53
|
-
"node": ">=18.0.0"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "tascan-mcp",
|
|
3
|
+
"version": "3.14.0",
|
|
4
|
+
"mcpName": "io.github.snowbikemike/tascan-mcp",
|
|
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
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"tascan-mcp": "index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.js",
|
|
16
|
+
"tools.cjs",
|
|
17
|
+
"scopes.cjs",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
23
|
+
"zod": "^4.3.6"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"tascan",
|
|
27
|
+
"mcp",
|
|
28
|
+
"model-context-protocol",
|
|
29
|
+
"ai",
|
|
30
|
+
"task-management",
|
|
31
|
+
"qr-code",
|
|
32
|
+
"field-operations",
|
|
33
|
+
"workforce",
|
|
34
|
+
"pwa",
|
|
35
|
+
"claude"
|
|
36
|
+
],
|
|
37
|
+
"author": {
|
|
38
|
+
"name": "Michael Edward Love II",
|
|
39
|
+
"email": "Michael@TaScan.io",
|
|
40
|
+
"url": "https://tascan.io"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/snowbikemike/tascan-mcp.git"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://tascan.io",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/snowbikemike/tascan-mcp/issues",
|
|
49
|
+
"email": "Michael@TaScan.io"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/scopes.cjs
CHANGED
|
@@ -42,7 +42,8 @@ const AGENT_INBOX_IDS = ['f745d0fa-421a-42e5-85be-0a3f9acd28e8'];
|
|
|
42
42
|
// as an urgency marker). No keyword heuristics anywhere.
|
|
43
43
|
const TASK_TYPES = ['CODE', 'SHELL', 'MCP', 'PLAN', 'WRITE', 'RESEARCH', 'REVIEW'];
|
|
44
44
|
const LOCAL_TASK_TYPES = ['CODE', 'SHELL', 'PLAN', 'DEFAULT', 'MCP'];
|
|
45
|
-
const CLOUD_TASK_TYPES = ['RESEARCH', 'WRITE', '
|
|
45
|
+
const CLOUD_TASK_TYPES = ['RESEARCH', 'WRITE', 'DEFAULT'];
|
|
46
|
+
const REVIEWER_TASK_TYPES = ['REVIEW']; // agent-review-background.js (155): OpenAI reviewer runner, read-tier key
|
|
46
47
|
const LOCAL_ONLY_TASK_TYPES = ['CODE', 'SHELL', 'PLAN', 'MCP']; // the cloud runner refuses these; DEFAULT goes either way
|
|
47
48
|
const CODE_TASK_TYPES = ['CODE', 'SHELL'];
|
|
48
49
|
const TASK_PREFIX_RE = new RegExp('^(' + TASK_TYPES.join('|') + '):');
|
|
@@ -176,6 +177,12 @@ const TOOL_SCOPES = {
|
|
|
176
177
|
tascan_list_payments: R, tascan_get_worker_passport: R, tascan_server_info: R, tascan_find: R,
|
|
177
178
|
tascan_get_worker: R, tascan_find_duplicate_workers: R,
|
|
178
179
|
tascan_get_receipt: R, // W3 receipt.js (GET /api/v1/receipts/:id — read tier there too)
|
|
180
|
+
// coordination layer (protocol v0.2 design item 1, migration 155) — read tier: the reviewer's ['read'] key may call these
|
|
181
|
+
tascan_get_build: R, tascan_get_build_file: R, tascan_get_cycle_report: R,
|
|
182
|
+
// write here; REST upgrades POST /tasks/:id/messages to agent:dispatch (:code for CODE/SHELL askers) when the task is a cycle task
|
|
183
|
+
tascan_post_message: W,
|
|
184
|
+
// creating a cycle queues a CODE:/SHELL: prompt on the AI Inbox → agent:dispatch:code, always
|
|
185
|
+
tascan_create_cycle: { tier: 'write', dispatch: 'code' },
|
|
179
186
|
// write (27)
|
|
180
187
|
tascan_create_project: W, tascan_update_project: W, tascan_delete_project: W, tascan_create_event: W,
|
|
181
188
|
tascan_update_event: W, tascan_delete_event: W, tascan_update_task: W, tascan_reply_with_list: W,
|
|
@@ -193,6 +200,7 @@ const TOOL_SCOPES = {
|
|
|
193
200
|
tascan_dispatch_to_agent: a => ({ tier: 'write', dispatch: dispatchKind(a.task) }),
|
|
194
201
|
// full (7): sends to a human, spends AI credits, or pledges money
|
|
195
202
|
tascan_dispatch_instruction: F, tascan_auto_resolve: F, tascan_send_sms: F, tascan_send_task_email: F,
|
|
203
|
+
tascan_schedule_sms: F, tascan_list_scheduled_sms: R, tascan_cancel_scheduled_sms: F,
|
|
196
204
|
tascan_invite_worker: F, tascan_assess_condition: F, tascan_request_payment: F,
|
|
197
205
|
// argument-dependent (2)
|
|
198
206
|
tascan_analyze_issue: a => (a.server_side_ai ? { ...F } : { ...R }),
|
|
@@ -211,6 +219,10 @@ function requiredForTool(name, args) {
|
|
|
211
219
|
// ─── Route map: EVERY handler name in api-v1-helpers.js parsePath × accepted methods. ─────
|
|
212
220
|
// (handler, method) not listed → null → 403 unclassified. 'admin' = admin-JWT routes (gate skipped).
|
|
213
221
|
const INBOX_WRITE = () => ({ tier: 'write', dispatch: 'if_inbox' });
|
|
222
|
+
// Posting on a CYCLE task (tasks.coord IS NOT NULL) can become executor prompt text or reviewer input, so it needs
|
|
223
|
+
// agent:dispatch (agent:dispatch:code when the task — or the asker a question task stands for — is CODE:/SHELL:).
|
|
224
|
+
// Resolved server-side by api-v1.js exactly like if_inbox; an ordinary task stays write tier. Design §4.4, invariant 18.
|
|
225
|
+
const CYCLE_WRITE = () => ({ tier: 'write', dispatch: 'if_cycle' });
|
|
214
226
|
const ROUTE_SCOPES = {
|
|
215
227
|
projects: { GET: R, POST: W },
|
|
216
228
|
project: { GET: R, PUT: W, DELETE: W },
|
|
@@ -246,6 +258,9 @@ const ROUTE_SCOPES = {
|
|
|
246
258
|
find: { GET: R },
|
|
247
259
|
scans: { GET: R },
|
|
248
260
|
sms: { POST: F },
|
|
261
|
+
sms_schedule: { POST: F },
|
|
262
|
+
sms_scheduled: { GET: R },
|
|
263
|
+
sms_scheduled_item: { GET: R, DELETE: F },
|
|
249
264
|
sms_status: { GET: R },
|
|
250
265
|
marketplace: { GET: R },
|
|
251
266
|
marketplace_invite: { POST: F },
|
|
@@ -263,7 +278,17 @@ const ROUTE_SCOPES = {
|
|
|
263
278
|
tags: { GET: R, POST: W },
|
|
264
279
|
tag: { GET: R, PUT: W, DELETE: W },
|
|
265
280
|
tag_scans: { GET: R },
|
|
266
|
-
|
|
281
|
+
// coordination layer (api-v1-helpers.js parsePath handlers task_messages / coord_cycles / coord_cycle_report / build / build_file)
|
|
282
|
+
task_messages: { GET: R, POST: CYCLE_WRITE }, // trail messages; POST answer on a dispatcher question → coord_answer_question
|
|
283
|
+
coord_cycles: { GET: R, POST: { tier: 'write', dispatch: 'code' } }, // list roots / coord_create_cycle (T1 = CODE:/SHELL: on the AI Inbox)
|
|
284
|
+
coord_cycle_report: { GET: R }, // get_cycle_report (+ summary); an admin Supabase JWT also reaches it (Reports Hub) — api-v1 skips the key lane for that case
|
|
285
|
+
build: { GET: R }, // GET /builds/:build_ref — bundle manifest (build_artifacts)
|
|
286
|
+
build_file: { GET: R }, // GET /builds/:build_ref/file?path=&offset=&limit= ≤ 12,000 chars per call
|
|
287
|
+
keys: 'admin', key: 'admin',
|
|
288
|
+
// receipt.js lanes (a separate function that calls scopes.check itself; classified HERE so there is one map).
|
|
289
|
+
// The anonymous GET /api/v1/receipts/:id/public is credential-free and therefore not classified here.
|
|
290
|
+
receipt: { GET: R }, // GET /api/v1/receipts/:id[?profile=public] (was hard-coded read in receipt.js)
|
|
291
|
+
receipt_publish: { POST: W, DELETE: W } // open / close the anonymous public-profile path
|
|
267
292
|
};
|
|
268
293
|
|
|
269
294
|
function requiredForRoute(handler, method, body) {
|
|
@@ -307,7 +332,7 @@ function restDenialHeaders(d) {
|
|
|
307
332
|
|
|
308
333
|
module.exports = {
|
|
309
334
|
SCOPE_VALUES, ALL_SCOPES, TIERS, RANK, DISPATCH, DISPATCH_CODE, AGENT_INBOX_IDS,
|
|
310
|
-
TASK_TYPES, LOCAL_TASK_TYPES, CLOUD_TASK_TYPES, LOCAL_ONLY_TASK_TYPES, CODE_TASK_TYPES, taskType, dispatchScopeFor, dispatchKind,
|
|
335
|
+
TASK_TYPES, LOCAL_TASK_TYPES, CLOUD_TASK_TYPES, REVIEWER_TASK_TYPES, LOCAL_ONLY_TASK_TYPES, CODE_TASK_TYPES, taskType, dispatchScopeFor, dispatchKind,
|
|
311
336
|
normalize, toArray, toScopeString, parseRequested, echoScope, SCOPE_PARAM_MAX, clamp, validateStored, check,
|
|
312
337
|
TOOL_SCOPES, PENDING_TOOLS, ROUTE_SCOPES, requiredForTool, requiredForRoute,
|
|
313
338
|
mcpDenialText, restDenialBody, restDenialHeaders
|
package/tools.cjs
CHANGED
|
@@ -442,17 +442,34 @@ const TOOLS = [
|
|
|
442
442
|
},
|
|
443
443
|
{
|
|
444
444
|
name: 'tascan_get_receipt',
|
|
445
|
-
description: 'Fetch the signed Action Receipt (Ed25519 JWS) for one completed task by completion_id (tascan_get_task -> completions[].id). Returns a readable summary (what, who, verification, evidence hashes, outcome, ledger chain) plus receipt_id/serial/kid, the compact JWS and the public verify URL. Verify offline against the JWKS or online by POSTing a JSON body whose jws field holds the compact receipt. Read outcome and verification separately: outcome completed = the executor returned and a result was recorded; verification.result = the verdict of a named policy
|
|
445
|
+
description: 'Fetch the signed Action Receipt (Ed25519 JWS) for one completed task by completion_id (tascan_get_task -> completions[].id). Returns a readable summary (what, who, verification, evidence hashes, outcome, ledger chain) plus receipt_id/serial/kid, the compact JWS and the public verify URL. Verify offline against the JWKS or online by POSTing a JSON body whose jws field holds the compact receipt. Read outcome and verification separately: outcome completed = the executor returned and a result was recorded; verification.result = the verdict of a named policy; all-null verification with reason no_policy_run = no policy ran. Never treat outcome=completed as success without a policy verdict you trust (protocol 8.3 C11). Verifier: 6.8. profile=public returns the separately signed public export profile (protocol 6.10): it withholds the raw org, list, project, worker, run and trace ids (each a 16-hex id_hash) and storage locators, and binds to the full receipt - the form for anyone outside the org. Read tier.',
|
|
446
446
|
inputSchema: {
|
|
447
447
|
type: 'object',
|
|
448
|
-
properties: {
|
|
448
|
+
properties: {
|
|
449
|
+
completion_id: { type: 'string', description: 'task_completions.id (UUID) - from tascan_get_task -> completions[].id' },
|
|
450
|
+
profile: { type: 'string', enum: ['full', 'public'], description: 'full (default) = the org view with the unsigned private block; public = the separately signed public export profile: no raw org, list, project, worker, run or trace id (each is a 16-hex id_hash), no storage locators, plus a binding to the full receipt it was derived from - the form to hand to anyone outside the org' }
|
|
451
|
+
},
|
|
449
452
|
required: ['completion_id']
|
|
450
453
|
},
|
|
451
454
|
annotations: { title: 'Get Action Receipt', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
452
455
|
handler: async (args, api) => {
|
|
453
456
|
const id = String(args.completion_id || '').trim().toLowerCase();
|
|
454
457
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) throw new Error('completion_id must be a UUID');
|
|
455
|
-
|
|
458
|
+
// profile: absent → full; an EXPLICIT value must be one of the two lanes — anything else is an error, never a
|
|
459
|
+
// silent fall-back to the full (org-private) envelope.
|
|
460
|
+
const profile = args.profile === undefined || args.profile === null ? 'full' : args.profile;
|
|
461
|
+
if (profile !== 'full' && profile !== 'public') throw new Error("profile must be 'full' or 'public'");
|
|
462
|
+
const env = await api('GET', `/receipts/${id}${profile === 'public' ? '?profile=public' : ''}`); // receipt.js returns the envelope itself (no data wrapper)
|
|
463
|
+
if (profile === 'public') {
|
|
464
|
+
// Fail CLOSED on the boundary: the deployed receipt endpoint, the npm package and the connector schema roll out
|
|
465
|
+
// separately, so an endpoint that predates the public profile ignores ?profile=public and answers its FULL
|
|
466
|
+
// envelope (raw ids + the unsigned private block). A public request is served ONLY by a public-profile
|
|
467
|
+
// envelope: view/profile public, no private block, and the JWS header typ tascan-receipt-public+jwt.
|
|
468
|
+
let typ = null;
|
|
469
|
+
try { typ = JSON.parse(Buffer.from(String(env && env.jws || '').split('.')[0], 'base64url').toString('utf8')).typ; } catch (e) { typ = null; }
|
|
470
|
+
const isPublic = env && typeof env === 'object' && env.profile === 'public' && env.view === 'public' && !('private' in env) && typ === 'tascan-receipt-public+jwt';
|
|
471
|
+
if (!isPublic) throw new Error('the receipt endpoint did not return the public export profile for profile=public (got ' + (env && typeof env === 'object' ? 'view ' + (env.view || 'unknown') + ', typ ' + (typ || 'none') + ('private' in env ? ', a private block' : '') : 'no envelope') + '); the deployed API may predate protocol 6.10 - nothing from this response is shown, ask for the full profile explicitly or retry after the deploy');
|
|
472
|
+
}
|
|
456
473
|
const r = (env.claims && env.claims.receipt) || {};
|
|
457
474
|
const v = x => (x && typeof x === 'object' && 'value' in x) ? x.value : x;
|
|
458
475
|
const a = r.action || {}, ex = r.executor || {}, ts = r.timestamps || {}, ver = r.verification || null, ch = r.chain || {};
|
|
@@ -460,10 +477,10 @@ const TOOLS = [
|
|
|
460
477
|
const verify = env.verify || {};
|
|
461
478
|
const lines = [
|
|
462
479
|
'=== TASCAN ACTION RECEIPT ===',
|
|
463
|
-
`Receipt: ${env.receipt_id} (serial ${env.serial}, kid ${env.kid}, status ${env.status}, view ${env.view})`,
|
|
480
|
+
`Receipt: ${env.receipt_id} (serial ${env.serial}, kid ${env.kid}, status ${env.status}, view ${env.view}${env.profile === 'public' ? `, public profile${env.published ? ', PUBLISHED at ' + env.url : ', not published'}` : ''})`,
|
|
464
481
|
`Subject: ${env.subject} (the Evidence row; the Action is task ${v(a.task_id)})`,
|
|
465
482
|
`Action: ${(env.private && env.private.title) || '(title signed as sha256 ' + String(v(a.title_sha256) || '').slice(0, 16) + '…)'} [task ${v(a.task_id)} in list ${(env.private && env.private.list_name) || v(a.list_id)}]`,
|
|
466
|
-
`Executor: ${(env.private && env.private.executor_display) || v(ex.id) || 'unknown'} (${v(ex.kind)}, verification_level ${v(ex.verification_level)}${v(ex.runner) ? `, runner ${v(ex.runner)}, trace ${v(ex.trace_id)}, attempt ${v(ex.attempt)}` : ''})`,
|
|
483
|
+
`Executor: ${(env.private && env.private.executor_display) || v(ex.id) || (v(ex.id_hash) ? 'id_hash ' + v(ex.id_hash) + ' (public profile)' : null) || 'unknown'} (${v(ex.kind)}, verification_level ${v(ex.verification_level)}${v(ex.runner) ? `, runner ${v(ex.runner)}, trace ${v(ex.trace_id) || (v(ex.trace_id_hash) ? 'id_hash ' + v(ex.trace_id_hash) : null)}, attempt ${v(ex.attempt)}` : ''})`,
|
|
467
484
|
`Outcome: ${v(r.outcome)} | completed ${v(ts.completed)} | recorded ${v(ts.recorded)}`,
|
|
468
485
|
`Evidence: ${(r.evidence || []).map(e => `${v(e.type)} sha256:${String(v(e.sha256) || '').slice(0, 16)}...`).join(', ') || 'none'}`,
|
|
469
486
|
`Verification: ${vr ? `${vr.verified ? 'VERIFIED' : 'NOT verified'} (confidence ${vr.confidence}) by ${vf && vf.kind}${vf && vf.identity ? ' ' + vf.identity : ''}, policy ${v(ver.policy_id)}@${v(ver.policy_version)}${env.private && env.private.verification_reason ? ' - ' + env.private.verification_reason + ' (unsigned; sha256 signed)' : ''}` : 'none recorded'}`,
|
|
@@ -673,7 +690,7 @@ const TOOLS = [
|
|
|
673
690
|
},
|
|
674
691
|
{
|
|
675
692
|
name: 'tascan_complete_task',
|
|
676
|
-
description: 'Complete
|
|
693
|
+
description: 'Complete an ORDINARY task on behalf of a worker. Inserts a completion record and timer event. Use this to simulate or record task completions via the API. Coordination-cycle tasks (a CODE:/REVIEW: build or review, a Decision / Question / Integrate / Parked card on a project Decisions list — tasks that carry `coord`) are refused with 403 for every key tier: builds and reviews are completed by their runner, decisions only by the human on the worker page.',
|
|
677
694
|
inputSchema: {
|
|
678
695
|
type: 'object',
|
|
679
696
|
properties: {
|
|
@@ -1119,6 +1136,52 @@ const TOOLS = [
|
|
|
1119
1136
|
return text;
|
|
1120
1137
|
}
|
|
1121
1138
|
},
|
|
1139
|
+
{
|
|
1140
|
+
name: 'tascan_schedule_sms',
|
|
1141
|
+
description: 'Schedule a transactional TaScan SMS for a future time (up to 90 days out): the text is sent by TaScan\'s own scheduler (every 5 minutes) through the same guarded lane as tascan_send_sms — recipient must be a worker of your org or a phone the org already knows, STOP opt-outs honoured, burst limits and the SMS quota apply, the "TaScan:" prefix is added, and a list_id appends a tap-to-open checklist link. Use this for reminders (e.g. "log your out time" each show night) — nothing outside TaScan needs to stay awake. Returns the scheduled row id; cancel with tascan_cancel_scheduled_sms while it is still pending. The same idempotency_key within an org returns the existing row instead of a duplicate.',
|
|
1142
|
+
inputSchema: {
|
|
1143
|
+
type: 'object',
|
|
1144
|
+
properties: {
|
|
1145
|
+
send_at: { type: 'string', description: 'When to send — ISO 8601 with a timezone offset, e.g. "2026-09-25T18:30:00-07:00" (Las Vegas is -07:00 in September). Delivery happens on the next 5-minute tick at or after this time.' },
|
|
1146
|
+
message: { type: 'string', description: 'Message text (1-400 chars, links stripped). Transactional and work-related only.' },
|
|
1147
|
+
worker_id: { type: 'string', description: 'Worker ID — texts their phone on file (preferred over raw phone)' },
|
|
1148
|
+
phone: { type: 'string', description: 'Raw phone (E.164) — must already belong to a worker or roster entry of your org' },
|
|
1149
|
+
list_id: { type: 'string', description: 'Optional task list ID — appends the tap-to-open checklist link' },
|
|
1150
|
+
include_link: { type: 'boolean', description: 'Append the list link (default true when list_id is given)' },
|
|
1151
|
+
idempotency_key: { type: 'string', description: 'Optional caller key (≤ 200 chars) — replays return the existing scheduled row' }
|
|
1152
|
+
},
|
|
1153
|
+
required: ['send_at', 'message']
|
|
1154
|
+
},
|
|
1155
|
+
annotations: { title: 'Schedule SMS', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1156
|
+
handler: async (args, api) => {
|
|
1157
|
+
const result = await api('POST', '/sms/schedule', { send_at: args.send_at, message: args.message, worker_id: args.worker_id, phone: args.phone, list_id: args.list_id, include_link: args.include_link, idempotency_key: args.idempotency_key });
|
|
1158
|
+
const d = result.data || {};
|
|
1159
|
+
return `${d.created === false ? 'Already scheduled (same idempotency_key)' : 'Scheduled'}: id ${d.id}\n send_at ${d.send_at} status ${d.status}\n to ${d.worker_id ? 'worker ' + d.worker_id : d.phone}${d.list_id ? ' list ' + d.list_id + (d.include_link === false ? ' (no link)' : ' (+ link)') : ''}\n message: ${d.message}\nCancel while pending: tascan_cancel_scheduled_sms id=${d.id}`;
|
|
1160
|
+
}
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
name: 'tascan_list_scheduled_sms',
|
|
1164
|
+
description: 'List your org\'s scheduled texts (default: pending + sending, soonest first; status=sent|failed|cancelled to see history). Each row shows send_at, status, attempts, the Twilio sid once sent, and the last error for a failed row.',
|
|
1165
|
+
inputSchema: { type: 'object', properties: { status: { type: 'string', enum: ['pending', 'sending', 'sent', 'failed', 'cancelled'], description: 'Filter by status (default pending + sending)' } } },
|
|
1166
|
+
annotations: { title: 'List Scheduled SMS', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1167
|
+
handler: async (args, api) => {
|
|
1168
|
+
const result = await api('GET', '/sms/scheduled' + (args.status ? '?status=' + encodeURIComponent(args.status) : ''));
|
|
1169
|
+
const rows = result.data || [];
|
|
1170
|
+
if (!rows.length) return 'No scheduled texts' + (args.status ? ` with status ${args.status}` : ' pending') + '.';
|
|
1171
|
+
return rows.map(r => `${r.id} ${r.send_at} ${r.status}${r.attempts ? ' (attempt ' + r.attempts + ')' : ''} to ${r.worker_id ? 'worker ' + r.worker_id : r.phone}${r.list_id ? ' list ' + r.list_id : ''}\n ${r.message}${r.provider_ref ? '\n sid ' + r.provider_ref : ''}${r.error ? '\n error: ' + r.error : ''}`).join('\n');
|
|
1172
|
+
}
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
name: 'tascan_cancel_scheduled_sms',
|
|
1176
|
+
description: 'Cancel a scheduled text that has not been sent yet (status pending). A row already sending, sent, failed or cancelled is refused (409) — a sent text cannot be recalled.',
|
|
1177
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'The scheduled_sms row id from tascan_schedule_sms / tascan_list_scheduled_sms' } }, required: ['id'] },
|
|
1178
|
+
annotations: { title: 'Cancel Scheduled SMS', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
1179
|
+
handler: async (args, api) => {
|
|
1180
|
+
const result = await api('DELETE', '/sms/scheduled/' + encodeURIComponent(String(args.id || '')));
|
|
1181
|
+
const d = result.data || {};
|
|
1182
|
+
return `Cancelled scheduled text ${d.id} (was due ${d.send_at}).`;
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1122
1185
|
{
|
|
1123
1186
|
name: 'tascan_get_sms_status',
|
|
1124
1187
|
description: 'Check delivery status of a previously sent TaScan SMS by its Twilio SID (returned by tascan_send_sms). Shows queued/sent/delivered/undelivered/failed plus carrier error codes.',
|
|
@@ -1921,6 +1984,172 @@ const TOOLS = [
|
|
|
1921
1984
|
const result = await api('DELETE', `/workers/${args.worker_id}`);
|
|
1922
1985
|
return `Worker ${args.worker_id} tombstoned (soft delete — record retained for audit).`;
|
|
1923
1986
|
}
|
|
1987
|
+
},
|
|
1988
|
+
// ─── Coordination layer (protocol v0.2 design item 1, migration 155) ─────────────────────────────
|
|
1989
|
+
// A cycle = T1 CODE/SHELL build (local executor, stores the build bundle → build_ref) → T2 REVIEW (independent
|
|
1990
|
+
// reviewer runner, same build_ref) → T3 Decision (the human authority on the project's Decisions list, one SMS)
|
|
1991
|
+
// → T4 Integrate (deploy id). Nothing here can approve, deploy, page a human or move the DAG: those are
|
|
1992
|
+
// SECURITY DEFINER RPCs behind human completions. The cycle report is the audit surface.
|
|
1993
|
+
{
|
|
1994
|
+
name: 'tascan_create_cycle',
|
|
1995
|
+
description: 'Start an unattended build-review-decide cycle (protocol v0.2). Queues T1 CODE: (or SHELL:) with your build_brief on the AI Inbox and T2 REVIEW: with your review_brief, born blocked on T1. The local executor builds, stores the exact bytes of artifact_paths as a bundle (build_ref = sha256 over the manifest), the independent reviewer reviews THAT bundle, an approve verdict mints a Decision task for the human authority (one SMS), Approve mints an Integrate task for the deploy id. Revise verdicts spawn revisions (cap max_revisions, default 3); reject, human Reject, scope violations or exhausted revisions PARK the cycle (a Parked task with Resume with notes / Close). REQUIRES agent:dispatch:code. Duplicate protection: the same idempotency_key, or the same briefs + paths, within 24 h returns the existing root instead of queueing again (created=false). Track with tascan_get_cycle_report (root_id). Nothing spawns a cycle on its own.',
|
|
1996
|
+
inputSchema: {
|
|
1997
|
+
type: 'object',
|
|
1998
|
+
properties: {
|
|
1999
|
+
project_id: { type: 'string', description: 'Working project (UUID). Its Decisions and Agent Questions lists are created on the first cycle (coord_ensure_lists). Requires the org human authority to be configured (coord_set_authority) — otherwise 403.' },
|
|
2000
|
+
title: { type: 'string', description: 'Short human title (1-200 chars). T1 becomes "CODE: <title>", T2 "REVIEW: <title>".' },
|
|
2001
|
+
build_brief: { type: 'string', description: 'The executor prompt (1-40000 chars). Executed verbatim by the local Claude Code runner as a CODE:/SHELL: task — write it as a complete instruction, name the files, forbid nothing the runner already forbids (deploy/commit/push are refused by its context).' },
|
|
2002
|
+
review_brief: { type: 'string', description: 'The reviewer prompt (1-40000 chars). The reviewer reads the stored bundle (tascan_get_build / tascan_get_build_file), the task text and the filtered trail; it returns approve, revise, reject or needs_input as strict JSON bound to the build_ref.' },
|
|
2003
|
+
artifact_paths: { type: 'array', items: { type: 'string' }, description: 'Repo-relative paths the build binds (1-64). Exactly these files are stored as the bundle and hashed into build_ref; files the executor touches OUTSIDE them fail the scope check and park the cycle. No .., not absolute, no drive letter, no backslash.' },
|
|
2004
|
+
task_type: { type: 'string', enum: ['CODE', 'SHELL'], description: 'T1 prefix (default CODE).' },
|
|
2005
|
+
max_revisions: { type: 'integer', description: 'Revision cap (default 3): at most max_revisions + 1 builds and reviews.' },
|
|
2006
|
+
max_questions: { type: 'integer', description: 'Questions a runner may ask per task before the attempt fails (default 3).' },
|
|
2007
|
+
max_cost_micro_usd: { type: 'integer', description: 'Per-cycle spend cap summed over every attempt, in micro-USD (default 5000000 = USD 5). A claim that could overrun it is refused (budget_exhausted).' },
|
|
2008
|
+
preview_url: { type: 'string', description: 'Optional https preview link shown on the Decision task.' },
|
|
2009
|
+
checkpoint_title: { type: 'string', description: 'Optional title template for the Decision task (default "Decision: <title>").' },
|
|
2010
|
+
checkpoint_description: { type: 'string', description: 'Optional description template for the Decision task; the build_ref, preview and findings summary are appended.' },
|
|
2011
|
+
integrate_title: { type: 'string', description: 'Optional title template for the Integrate task (default "Integrate: <title>").' },
|
|
2012
|
+
integrate_description: { type: 'string', description: 'Optional description template for the Integrate task.' },
|
|
2013
|
+
idempotency_key: { type: 'string', description: 'Optional caller key (≤ 200 chars). The same key within 24 h returns the existing cycle (created=false) instead of a duplicate dispatch.' }
|
|
2014
|
+
},
|
|
2015
|
+
required: ['project_id', 'title', 'build_brief', 'review_brief', 'artifact_paths']
|
|
2016
|
+
},
|
|
2017
|
+
annotations: { title: 'Create Cycle', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2018
|
+
handler: async (args, api) => {
|
|
2019
|
+
const paths = Array.isArray(args.artifact_paths) ? args.artifact_paths : (typeof args.artifact_paths === 'string' ? [args.artifact_paths] : []);
|
|
2020
|
+
if (!paths.length) throw new Error('artifact_paths is required (1-64 repo-relative paths)');
|
|
2021
|
+
const options = {};
|
|
2022
|
+
for (const k of ['task_type', 'max_revisions', 'max_questions', 'max_cost_micro_usd', 'preview_url']) if (args[k] != null && args[k] !== '') options[k] = args[k];
|
|
2023
|
+
const templates = {};
|
|
2024
|
+
if (args.checkpoint_title || args.checkpoint_description) templates.checkpoint = { ...(args.checkpoint_title ? { title: args.checkpoint_title } : {}), ...(args.checkpoint_description ? { description: args.checkpoint_description } : {}) };
|
|
2025
|
+
if (args.integrate_title || args.integrate_description) templates.integrate = { ...(args.integrate_title ? { title: args.integrate_title } : {}), ...(args.integrate_description ? { description: args.integrate_description } : {}) };
|
|
2026
|
+
if (Object.keys(templates).length) options.templates = templates;
|
|
2027
|
+
const body = { project_id: args.project_id, title: args.title, build_brief: args.build_brief, review_brief: args.review_brief, artifact_paths: paths, options };
|
|
2028
|
+
if (args.idempotency_key) body.idempotency_key = args.idempotency_key;
|
|
2029
|
+
const result = await api('POST', '/coord/cycles', body);
|
|
2030
|
+
const d = result.data || {};
|
|
2031
|
+
if (result.duplicate || d.created === false) {
|
|
2032
|
+
return `DUPLICATE — nothing was queued. A cycle with the same ${args.idempotency_key ? 'idempotency_key' : 'briefs and paths'} exists from the last 24 h (status ${d.status || 'active'}).\nroot_id: ${d.root_id}\nreview_id: ${d.review_id || 'n/a'}\n\nRead it: tascan_get_cycle_report root_id=${d.root_id}`;
|
|
2033
|
+
}
|
|
2034
|
+
return `Cycle created.\nroot_id (T1 ${options.task_type || 'CODE'}: build): ${d.root_id}\nreview_id (T2 REVIEW, blocked on T1): ${d.review_id}\nbrief_sha256: ${d.brief_sha256}\ndecisions_list_id: ${d.decisions_list_id}\nquestions_list_id: ${d.questions_list_id}\nartifact_paths: ${paths.join(', ')}\n\nNext: the local executor claims T1 on its next poll (≤ 30 s), stores the bundle and finishes with build_ref; T2 is released from that finish. Follow with tascan_get_cycle_report root_id=${d.root_id} or tascan_get_task task_id=${d.root_id} (agent + trail blocks). Nobody is texted until the reviewer approves.`;
|
|
2035
|
+
}
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
name: 'tascan_post_message',
|
|
2039
|
+
description: 'Post a message on a task trail (protocol v0.2 trail_messages): kind question, answer, handoff or discussion. A message never completes a task, never satisfies a gate and never pages anyone. The actor is stamped from your credential (key:<id>, actor_type "key" — a credential, never a human), never from the body; the executor and the reviewer consume a key\'s answers only when the key holds agent:dispatch. On an ordinary task this is write tier. On a CYCLE task (one with coord) it needs agent:dispatch (agent:dispatch:code when the task, or the asker a question task stands for, is CODE:/SHELL:) because the text can become executor prompt or reviewer input. kind=answer on a dispatcher-addressed question task answers it through coord_answer_question and releases the blocked asker; a human-addressed question is answered only on the worker page (403 here). finding (reviewer runner) and decision (human completion) cannot be posted. Body ≤ 8000 chars; idempotency_key makes a replay return the same message.',
|
|
2040
|
+
inputSchema: {
|
|
2041
|
+
type: 'object',
|
|
2042
|
+
properties: {
|
|
2043
|
+
task_id: { type: 'string', description: 'Task ID (UUID). For an answer: the QUESTION task id (tascan_get_task on the asker shows coord.blocked_by).' },
|
|
2044
|
+
kind: { type: 'string', enum: ['question', 'answer', 'handoff', 'discussion'], description: 'question = a question for the record (does not block anything); answer = answers a dispatcher question task and releases the asker; handoff = hand work or context to the next agent; discussion = a note.' },
|
|
2045
|
+
body: { type: 'string', description: 'The message text (1-8000 chars). Treated as DATA by every reader; on a cycle task it may be prepended to the executor prompt as TRAIL INPUT.' },
|
|
2046
|
+
reply_to: { type: 'string', description: 'Optional message id this replies to.' },
|
|
2047
|
+
build_ref: { type: 'string', description: 'Optional sha256:<64 hex> the message is about (defaults to the cycle task\'s bound build).' },
|
|
2048
|
+
addressee: { type: 'string', enum: ['human', 'dispatcher'], description: 'Optional, kind=question only: who the question is for (recorded; nobody is paged).' },
|
|
2049
|
+
idempotency_key: { type: 'string', description: 'Optional replay key (≤ 200 chars): the same key on the same task returns the same message id.' }
|
|
2050
|
+
},
|
|
2051
|
+
required: ['task_id', 'kind', 'body']
|
|
2052
|
+
},
|
|
2053
|
+
annotations: { title: 'Post Trail Message', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2054
|
+
handler: async (args, api) => {
|
|
2055
|
+
const id = String(args.task_id || '').trim().toLowerCase();
|
|
2056
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) throw new Error('task_id must be a UUID');
|
|
2057
|
+
const body = { kind: args.kind, body: args.body };
|
|
2058
|
+
for (const k of ['reply_to', 'build_ref', 'addressee', 'idempotency_key']) if (args[k]) body[k] = args[k];
|
|
2059
|
+
const result = await api('POST', `/tasks/${id}/messages`, body);
|
|
2060
|
+
const m = result.data || {};
|
|
2061
|
+
let text = `Posted ${m.kind || args.kind} message ${m.id} on task ${m.task_id || id} as ${m.actor_id || 'you'}` + (m.build_ref ? ` (build ${m.build_ref})` : '') + '.';
|
|
2062
|
+
if (result.question_task_id) text += `\nAnswered question task ${result.question_task_id}; asker ${result.asker_id} ${result.released ? 'RELEASED — its runner picks it up on the next poll' : 'was not released (already released or not blocked on this question)'}.`;
|
|
2063
|
+
if (result.note) text += '\n' + result.note;
|
|
2064
|
+
return text + '\n\n' + JSON.stringify(m, null, 2);
|
|
2065
|
+
}
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
name: 'tascan_get_build',
|
|
2069
|
+
description: 'Manifest of a stored build bundle by build_ref (protocol v0.2 build_artifacts): the exact files the executor produced for the cycle\'s artifact_paths, each with sha256, byte length and whether text content is stored (binary or over-cap files keep the sha only). build_ref = sha256 over the manifest, computed in the database once; the reviewer reviews THESE bytes, the human approves THIS ref, the integrate task records THIS ref. Read tier. Use tascan_get_build_file to read a file. Truncated at 12000 chars. Reading the manifest is discovery, not a read of any file.',
|
|
2070
|
+
inputSchema: {
|
|
2071
|
+
type: 'object',
|
|
2072
|
+
properties: { build_ref: { type: 'string', description: 'sha256:<64 hex> (or the bare 64 hex) — from tascan_get_task (coord.build_ref / agent.runs[].build_ref) or tascan_get_cycle_report.' } },
|
|
2073
|
+
required: ['build_ref']
|
|
2074
|
+
},
|
|
2075
|
+
annotations: { title: 'Get Build Manifest', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2076
|
+
handler: async (args, api) => {
|
|
2077
|
+
const ref = String(args.build_ref || '').trim().toLowerCase();
|
|
2078
|
+
if (!/^(sha256:)?[0-9a-f]{64}$/.test(ref)) throw new Error('build_ref must be sha256:<64 hex>');
|
|
2079
|
+
const result = await api('GET', `/builds/${encodeURIComponent(ref.startsWith('sha256:') ? ref : 'sha256:' + ref)}`);
|
|
2080
|
+
const d = result.data || {};
|
|
2081
|
+
const lines = [`BUILD ${d.build_ref}`, `run ${d.run_id} · task ${d.task_id}${d.task_title ? ' · ' + d.task_title : ''} · ${d.file_count} file(s)`, ''];
|
|
2082
|
+
(d.files || []).forEach(f => lines.push(`${f.path} sha256:${f.sha256} ${f.byte_length} B${f.content_type ? ' ' + f.content_type : ''}${f.has_content ? '' : ' (no stored text: binary or over cap)'}`));
|
|
2083
|
+
lines.push('', `Read a file: tascan_get_build_file build_ref=${d.build_ref} path=<path>`, 'This manifest proves nothing about the file contents: every file under review must be read with tascan_get_build_file until its chunks cover the whole file (a result that says TRUNCATED is continued from next_offset).');
|
|
2084
|
+
const out = lines.join('\n');
|
|
2085
|
+
return out.length > 12000 ? out.slice(0, 12000) + '\n…[truncated at 12000 chars]' : out;
|
|
2086
|
+
}
|
|
2087
|
+
},
|
|
2088
|
+
{
|
|
2089
|
+
name: 'tascan_get_build_file',
|
|
2090
|
+
description: 'Read one file from a stored build bundle by build_ref and path (protocol v0.2 build_artifacts) — the exact bytes the executor produced, not a working-tree read. Returns up to 12000 chars per call with offset/limit paging (next_offset when truncated), plus the file\'s sha256 and byte length. Binary or over-cap files return no content (the sha256 still binds them). Read tier; this is what the independent reviewer reads. The header lines (path, build, sha256, chars a-b of total) are the record a reviewer\'s read is bound to; read every chunk until the range covers the whole file.',
|
|
2091
|
+
inputSchema: {
|
|
2092
|
+
type: 'object',
|
|
2093
|
+
properties: {
|
|
2094
|
+
build_ref: { type: 'string', description: 'sha256:<64 hex> (or the bare 64 hex).' },
|
|
2095
|
+
path: { type: 'string', description: 'Repo-relative path exactly as listed by tascan_get_build.' },
|
|
2096
|
+
offset: { type: 'integer', description: 'Character offset to start from (default 0).' },
|
|
2097
|
+
limit: { type: 'integer', description: 'Characters to return (1-12000, default 12000).' }
|
|
2098
|
+
},
|
|
2099
|
+
required: ['build_ref', 'path']
|
|
2100
|
+
},
|
|
2101
|
+
annotations: { title: 'Get Build File', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2102
|
+
handler: async (args, api) => {
|
|
2103
|
+
const ref = String(args.build_ref || '').trim().toLowerCase();
|
|
2104
|
+
if (!/^(sha256:)?[0-9a-f]{64}$/.test(ref)) throw new Error('build_ref must be sha256:<64 hex>');
|
|
2105
|
+
const path = String(args.path || '').trim();
|
|
2106
|
+
if (!path) throw new Error('path is required');
|
|
2107
|
+
const qs = new URLSearchParams({ path });
|
|
2108
|
+
if (args.offset != null) qs.set('offset', String(args.offset));
|
|
2109
|
+
if (args.limit != null) qs.set('limit', String(args.limit));
|
|
2110
|
+
const result = await api('GET', `/builds/${encodeURIComponent(ref.startsWith('sha256:') ? ref : 'sha256:' + ref)}/file?${qs.toString()}`);
|
|
2111
|
+
const d = result.data || {};
|
|
2112
|
+
const head = `FILE ${d.path} (build ${d.build_ref})\nsha256:${d.sha256} ${d.byte_length} B${d.content_type ? ' ' + d.content_type : ''}\nchars ${d.offset}-${(d.offset || 0) + (d.chars_returned || 0)} of ${d.total_chars}${d.truncated ? ` — TRUNCATED, continue with offset=${d.next_offset}` : ''}`;
|
|
2113
|
+
if (d.content == null) return head + '\n\n' + (d.note || 'No stored text for this file.');
|
|
2114
|
+
return head + '\n\n----- content -----\n' + d.content;
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
name: 'tascan_get_cycle_report',
|
|
2119
|
+
description: 'The audit report of one coordination cycle by its root task id (protocol v0.2, get_cycle_report): every step task (build, review, checkpoint, integrate, question, parked) with its revision and state, every execution attempt with runner, outcome, build_ref and usage/cost, every completion (receipt id = completion id, receipt hash), every reviewer verdict, every human decision and answer, the full trail (messages), the ledger events and the hash-chain verdict per task, plus spend against the cap. A computed summary (stage, attempts, verdicts, decisions, receipts, spend, chains_ok) comes first; pass full=true for the complete JSON (large). Read tier. This is the ONLY per-cycle notification surface: cycle steps do not e-mail or text anyone except the one checkpoint / human-question SMS.',
|
|
2120
|
+
inputSchema: {
|
|
2121
|
+
type: 'object',
|
|
2122
|
+
properties: {
|
|
2123
|
+
root_id: { type: 'string', description: 'The cycle root = the T1 build task id (returned by tascan_create_cycle; a non-root cycle task returns its root_id in the error).' },
|
|
2124
|
+
full: { type: 'boolean', description: 'true = the complete report JSON (tasks, runs, completions, messages, events, bundles, chains) after the summary, capped at 12000 chars. Default: summary only.' }
|
|
2125
|
+
},
|
|
2126
|
+
required: ['root_id']
|
|
2127
|
+
},
|
|
2128
|
+
annotations: { title: 'Get Cycle Report', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2129
|
+
handler: async (args, api) => {
|
|
2130
|
+
const id = String(args.root_id || '').trim().toLowerCase();
|
|
2131
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) throw new Error('root_id must be a UUID');
|
|
2132
|
+
const result = await api('GET', `/coord/cycles/${id}/report`);
|
|
2133
|
+
const d = result.data || {}, r = d.root || {}, s = d.summary || {};
|
|
2134
|
+
const short = ref => ref ? String(ref).replace(/^sha256:/, '').slice(0, 12) + '…' : 'none';
|
|
2135
|
+
const lines = [
|
|
2136
|
+
`=== CYCLE REPORT: ${r.title || id} ===`,
|
|
2137
|
+
`root ${r.id} · status ${r.status} · stage ${s.stage} · project ${r.project}`,
|
|
2138
|
+
`build_ref ${s.latest_build_ref || r.build_ref || 'none yet'}${s.deploy_id ? ' · deploy ' + s.deploy_id : ''} · brief_sha256 ${r.brief_sha256 || 'n/a'}`,
|
|
2139
|
+
`attempts ${(s.attempts && s.attempts.total) || 0} (${Object.entries((s.attempts && s.attempts.by_runner) || {}).map(([k, v]) => k + ' ' + v).join(', ') || 'none'}; ${(s.attempts && s.attempts.failed_or_expired) || 0} failed/expired) · spend USD ${s.spend_usd != null ? s.spend_usd.toFixed(2) : '0.00'} · ledger chains ${s.chains_ok === true ? 'OK' : s.chains_ok === false ? 'BROKEN' : 'n/a'} (${s.chains_checked || 0} tasks)`,
|
|
2140
|
+
''
|
|
2141
|
+
];
|
|
2142
|
+
lines.push('STEPS');
|
|
2143
|
+
(s.steps || []).forEach(st => lines.push(` ${st.kind} r${st.revision}${st.seq ? '.' + st.seq : ''} ${st.title} [${st.blocked_by ? 'blocked' : st.completed ? 'completed' : st.released_at ? 'released' : 'open'}; ${st.attempts || 0} attempt(s)] ${st.id}`));
|
|
2144
|
+
if (s.verdicts && s.verdicts.length) { lines.push('', 'VERDICTS'); s.verdicts.forEach(v => lines.push(` r${v.revision} ${v.verdict || 'missing'} on ${short(v.build_ref)} ${v.from_reviewer ? '' : '(NOT the reviewer identity) '}completion ${v.completion_id}`)); }
|
|
2145
|
+
if (s.decisions && s.decisions.length) { lines.push('', 'DECISIONS'); s.decisions.forEach(v => lines.push(` ${v.decision} by ${v.actor_id} on ${short(v.build_ref)} completion ${v.completion_id || 'n/a'}`)); }
|
|
2146
|
+
if (s.questions && s.questions.length) { lines.push('', 'QUESTIONS'); s.questions.forEach(q => lines.push(` to ${q.addressee}: ${q.title} — ${q.answered ? 'answered' : 'AWAITING ANSWER'} (${q.id})`)); }
|
|
2147
|
+
if (s.receipts && s.receipts.length) { lines.push('', 'RECEIPTS (tascan_get_receipt completion_id=…)'); s.receipts.forEach(c => lines.push(` ${c.completion_id} ${c.kind || ''} ${c.source || ''} ${c.receipt_hash ? 'sha256:' + String(c.receipt_hash).slice(0, 12) + '…' : 'unsigned'}`)); }
|
|
2148
|
+
lines.push('', `TRAIL: ${s.messages || 0} message(s) · LEDGER: ${s.events || 0} event(s) ${Object.entries(s.event_types || {}).map(([k, v]) => k + '×' + v).join(' ')}`);
|
|
2149
|
+
let out = lines.join('\n');
|
|
2150
|
+
if (args.full) out += '\n\n' + JSON.stringify(d, null, 2);
|
|
2151
|
+
return out.length > 12000 ? out.slice(0, 12000) + '\n…[truncated at 12000 chars — read sections with tascan_get_task / tascan_get_receipt]' : out;
|
|
2152
|
+
}
|
|
1924
2153
|
}
|
|
1925
2154
|
];
|
|
1926
2155
|
|