@oneie/sdk 0.14.13 → 0.14.14
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/dist/generated/fn-map.d.ts +1 -1
- package/dist/generated/fn-map.js +2 -2
- package/dist/generated/schemas.d.ts +10 -0
- package/dist/generated/schemas.d.ts.map +1 -1
- package/dist/generated/schemas.js +9 -0
- package/dist/generated/schemas.js.map +1 -1
- package/dist/receivers.d.ts +153 -0
- package/dist/receivers.d.ts.map +1 -1
- package/dist/receivers.js +320 -3
- package/dist/receivers.js.map +1 -1
- package/dist/work-contract-grammar.json +29 -0
- package/package.json +3 -3
- package/dist/.build-fingerprint +0 -1
package/dist/receivers.js
CHANGED
|
@@ -440,6 +440,73 @@ export const RECEIVERS = {
|
|
|
440
440
|
effect: "ask", auth: "manage_integrations", reversible: false,
|
|
441
441
|
examples: [{ slug: "acme", toolkit: "stripe", credentials: { api_key: "sk_test_..." } }],
|
|
442
442
|
}),
|
|
443
|
+
// git:pr — open or update ONE pull request on the repository a workspace owns.
|
|
444
|
+
//
|
|
445
|
+
// THE REPO IS NOT A PARAMETER, and that is the security property, not an
|
|
446
|
+
// ergonomic one. `bind-receiver.ts` splits the work: the binder asks "does this
|
|
447
|
+
// caller hold this CLASS of authority?", the handler asks "…over this
|
|
448
|
+
// PARTICULAR object?". Every other tenant receiver can answer the second
|
|
449
|
+
// because its object is a node in the six dimensions. A GitHub repository is
|
|
450
|
+
// not one — grep every schema/*.tql and the only hit is a `front-door`
|
|
451
|
+
// attribute whose VALUE may be the string "github". So `git:pr({ repo, … })`
|
|
452
|
+
// would take its target from the body and its credential from env: workspace
|
|
453
|
+
// authority anywhere becoming a write to any repo the token can reach, signed
|
|
454
|
+
// by ONE. That is the confused deputy, and it is the same defect class as
|
|
455
|
+
// trusting an actorId from a request body.
|
|
456
|
+
//
|
|
457
|
+
// So the caller never names the repository. The object of the decision is the
|
|
458
|
+
// WORKSPACE — dimension 1, a real node — resolved from the attested
|
|
459
|
+
// `ctx.ownerSlug` exactly as `tools:composio` resolves its entity ("never a
|
|
460
|
+
// body field", in that resolver's own words), and the repo is looked up FROM
|
|
461
|
+
// the workspace. No binding is a refusal, never a fallback to a default repo.
|
|
462
|
+
//
|
|
463
|
+
// `auth` is `manage_integrations` because it must be: AUTH_POLICY maps that to
|
|
464
|
+
// the `tenant` floor, and an unrecognised label falls to `authenticated`
|
|
465
|
+
// (bind-receiver.ts:120) — a novel name would have shipped this WEAKER, and
|
|
466
|
+
// nothing in the suite would have said so.
|
|
467
|
+
//
|
|
468
|
+
// It cannot derive its own title or body: there is no git and no shell in a
|
|
469
|
+
// Worker. Those are the caller's, which is why this receiver is the DOOR and
|
|
470
|
+
// `.claude/scripts/pr-body.sh` is the document.
|
|
471
|
+
"git:pr": receiver({
|
|
472
|
+
receiver: "git:pr",
|
|
473
|
+
summary: "Open or update one pull request on the repository this workspace is bound to — the repo is resolved from the workspace, never from the payload",
|
|
474
|
+
request: z.object({
|
|
475
|
+
workspace: z.string().optional().describe("Workspace slug; defaults to the attested caller's. The bound repo is resolved FROM this."),
|
|
476
|
+
head: z.string().describe("Branch to merge from — must already be pushed"),
|
|
477
|
+
base: z.string().optional().describe("Branch to merge into; defaults to main"),
|
|
478
|
+
title: z.string().describe("PR title"),
|
|
479
|
+
body: z.string().optional().describe("PR body — derive it with .claude/scripts/pr-body.sh; a Worker cannot"),
|
|
480
|
+
draft: z.boolean().optional().describe("Open as a draft"),
|
|
481
|
+
}),
|
|
482
|
+
response: z.object({
|
|
483
|
+
ok: z.boolean(),
|
|
484
|
+
url: z.string().optional(),
|
|
485
|
+
number: z.number().optional(),
|
|
486
|
+
updated: z.boolean().optional().describe("true = an open PR was updated rather than a new one created"),
|
|
487
|
+
repo: z.string().optional(),
|
|
488
|
+
error: z.string().optional(),
|
|
489
|
+
detail: z.string().optional(),
|
|
490
|
+
}),
|
|
491
|
+
effect: "ask",
|
|
492
|
+
auth: "manage_integrations",
|
|
493
|
+
// A PR is closable, and a re-run updates the open one rather than opening a
|
|
494
|
+
// second — so a retried workflow step is safe as-is.
|
|
495
|
+
reversible: true,
|
|
496
|
+
idempotent: true,
|
|
497
|
+
cost: "free",
|
|
498
|
+
settles: "none",
|
|
499
|
+
version: "1.0.0",
|
|
500
|
+
examples: [{ head: "feat/x", base: "main", title: "feat: x" }],
|
|
501
|
+
}),
|
|
502
|
+
"tools:composio": receiver({
|
|
503
|
+
receiver: "tools:composio",
|
|
504
|
+
summary: "Execute one connected Composio tool from a workflow `tool` step (config.composio)",
|
|
505
|
+
request: z.object({ workspace: z.string().optional().describe("Workspace slug; defaults to the attested caller's"), tool: z.string().describe("Composio tool slug to execute"), args: z.record(z.string(), z.unknown()).optional().describe("Arguments passed to the tool") }),
|
|
506
|
+
response: z.object({ ok: z.boolean(), tool: z.string().optional(), result: z.unknown().optional(), error: z.string().optional() }),
|
|
507
|
+
effect: "ask", auth: "manage_integrations", reversible: false, idempotent: false,
|
|
508
|
+
examples: [{ workspace: "acme", tool: "GMAIL_SEND_EMAIL", args: { to: "x@y.com" } }],
|
|
509
|
+
}),
|
|
443
510
|
"brand:set": receiver({
|
|
444
511
|
receiver: "brand:set",
|
|
445
512
|
summary: "Write up to 6 brand color tokens (primary/secondary/accent/bg/text/border) to a workspace theme",
|
|
@@ -947,6 +1014,37 @@ export const RECEIVERS = {
|
|
|
947
1014
|
}),
|
|
948
1015
|
effect: "ask", idempotent: true,
|
|
949
1016
|
}),
|
|
1017
|
+
"factory:size": receiver({
|
|
1018
|
+
receiver: "factory:size",
|
|
1019
|
+
summary: "How big is this change — the tier `.claude/scripts/do-tier.sh` returns for the same paths, answered where a Worker can ask (a Worker cannot run a shell script, which is the only reason the Size card at factory flow.ts:306 is a gap). Zero real paths is REFUSED as `unsized`, mirroring the script's exit 3: absence of recon must not read as simplicity. The pin ladder in the response is a pure function of the tier; the surfaces are additive obligation, never a second gate selector",
|
|
1020
|
+
request: z.object({
|
|
1021
|
+
// The changed paths, repo-relative. do-tier sizes a DIFF, never a sentence,
|
|
1022
|
+
// so this is the only field that can decide the answer.
|
|
1023
|
+
paths: z.array(z.string()).max(2000),
|
|
1024
|
+
// Free text. It can only RAISE the answer to FEATURE, and is ignored entirely
|
|
1025
|
+
// when `paths` is empty — "add a null check" and "add a settings page" share a verb.
|
|
1026
|
+
intent: z.string().max(2000).optional(),
|
|
1027
|
+
}),
|
|
1028
|
+
response: z.object({
|
|
1029
|
+
ok: z.boolean(),
|
|
1030
|
+
tier: z.enum(["PATCH", "FIX", "FEATURE", "SCHEMA"]).optional(),
|
|
1031
|
+
spine: z.string().optional(),
|
|
1032
|
+
classifier: z.string().optional(),
|
|
1033
|
+
ceilingTokens: z.number().optional(),
|
|
1034
|
+
// The pinned suites this tier buys — derived from `tier` alone, mirroring
|
|
1035
|
+
// .claude/scripts/verify-fast.sh:302-306. There is no second selector.
|
|
1036
|
+
pins: z.array(z.string()).optional(),
|
|
1037
|
+
// What the paths touch, and the ceremony rung after every surface floor is
|
|
1038
|
+
// applied. Obligations, not gate selection.
|
|
1039
|
+
surfaces: z.array(z.string()).optional(),
|
|
1040
|
+
risk: z.string().optional(),
|
|
1041
|
+
ratcheted: z.boolean().optional(),
|
|
1042
|
+
produces: z.array(z.object({ what: z.string(), source: z.string() })).optional(),
|
|
1043
|
+
gates: z.array(z.object({ what: z.string(), source: z.string() })).optional(),
|
|
1044
|
+
error: z.string().optional(),
|
|
1045
|
+
}),
|
|
1046
|
+
effect: "ask", idempotent: true, reversible: true,
|
|
1047
|
+
}),
|
|
950
1048
|
"factory:attempt": receiver({
|
|
951
1049
|
receiver: "factory:attempt",
|
|
952
1050
|
summary: "Open an attempt contained by its task, or close it with the production edge to what it produced plus the rubric. A do-cycle IS an attempt (factory-plan.md §3.1) — do:cycle-close arms as a caller of the close phase, never as a second writer",
|
|
@@ -986,6 +1084,82 @@ export const RECEIVERS = {
|
|
|
986
1084
|
}),
|
|
987
1085
|
effect: "ask", idempotent: true,
|
|
988
1086
|
}),
|
|
1087
|
+
"factory:event": receiver({
|
|
1088
|
+
receiver: "factory:event",
|
|
1089
|
+
summary: "One factory-executor stage lands on the same run/event stream the canvas already reads — the six-stage pipeline (ready·claim·build·review·prove·close) that until now touched the substrate only at claim and close, so every surface could show a job before and after but never during. An unknown stage or status is REFUSED, never recorded as a default frame",
|
|
1090
|
+
request: z.object({
|
|
1091
|
+
// The task id being built. It IS the run key (`factory:<job>`), which is why
|
|
1092
|
+
// a retried stage appends to its own run instead of minting a second one.
|
|
1093
|
+
job: z.string(),
|
|
1094
|
+
// Must equal FACTORY_SPINE_STEPS (one.ie/web/src/lib/factory/event.ts).
|
|
1095
|
+
// Retyped because packages/sdk cannot import from one.ie/web; pinned by
|
|
1096
|
+
// tests/unit/factory/event-receiver.test.ts so the two cannot drift.
|
|
1097
|
+
stage: z.enum(["ready", "claim", "build", "review", "prove", "close"]),
|
|
1098
|
+
// There is deliberately no "skip": a stage that did not run must be ABSENT.
|
|
1099
|
+
status: z.enum(["start", "ok", "fail"]),
|
|
1100
|
+
// Board workspace. TAGS ONLY *inside the resolver* — effectiveWorkspace
|
|
1101
|
+
// (resolvers/tasks.ts:35) reads `workspace` and never `slug`, so the resolver
|
|
1102
|
+
// itself cannot be steered by this field.
|
|
1103
|
+
//
|
|
1104
|
+
// BUT NOT BEFORE IT. For a VERIFIED SERVICE CALLER the ask route nominates
|
|
1105
|
+
// the identity from the body and `slug` WINS:
|
|
1106
|
+
// pages/api/ask/[...receiver].ts:237
|
|
1107
|
+
// nominated = typeof p.slug === 'string' ? p.slug
|
|
1108
|
+
// : typeof p.workspace === 'string' ? p.workspace : undefined
|
|
1109
|
+
// :324 ownerSlug = locals.slug ?? callerUid ?? serviceOwnerSlug
|
|
1110
|
+
// A sessionless curl (no `locals.slug`, and `callerUid` is only resolved in the
|
|
1111
|
+
// `bearerToken && !isServiceCaller` branch) therefore lands ctx.ownerSlug =
|
|
1112
|
+
// data.slug, and effectiveWorkspace returns that owner unchanged. So on the
|
|
1113
|
+
// shipped emitter — .claude/scripts/factory-emit.sh, which posts
|
|
1114
|
+
// `Authorization: Bearer $GATEWAY_API_KEY` (isVerifiedServiceCaller case 2,
|
|
1115
|
+
// gateway-guard.ts:118) and sends `--slug` but never `workspace` — the `slug`
|
|
1116
|
+
// COLUMN is data.slug, chosen by the caller. That is by design for a caller
|
|
1117
|
+
// holding a shared service secret, and it is NOT what "never authz" says.
|
|
1118
|
+
// The tags/authz split is a property of the RESOLVER, not of this field.
|
|
1119
|
+
slug: z.string().optional(),
|
|
1120
|
+
model: z.string().optional(),
|
|
1121
|
+
detail: z.record(z.string(), z.unknown()).optional(),
|
|
1122
|
+
reason: z.string().optional(),
|
|
1123
|
+
// "dev" flags the projected run is_test so a worktree never pollutes the list.
|
|
1124
|
+
env: z.string().optional(),
|
|
1125
|
+
// Which authorized workspace the `slug` COLUMN resolves to (effectiveWorkspace,
|
|
1126
|
+
// resolvers/tasks.ts). Never identity: an unauthorized value falls back to the
|
|
1127
|
+
// attested ownerSlug rather than escalating.
|
|
1128
|
+
workspace: z.string().optional(),
|
|
1129
|
+
}),
|
|
1130
|
+
response: z.object({
|
|
1131
|
+
ok: z.boolean(),
|
|
1132
|
+
runId: z.string().optional(),
|
|
1133
|
+
stage: z.string().optional(),
|
|
1134
|
+
status: z.string().optional(),
|
|
1135
|
+
events: z.number().optional(),
|
|
1136
|
+
slug: z.string().optional(),
|
|
1137
|
+
error: z.string().optional(),
|
|
1138
|
+
}),
|
|
1139
|
+
// VERIFIED, not assumed: authClassFor(undefined) returns 'open'
|
|
1140
|
+
// (bind-receiver.ts:117-120), and the 'authenticated' floor tests exactly
|
|
1141
|
+
// `ctx.staff === true || Boolean(ctx.ownerSlug)` (bind-receiver.ts:159) — the
|
|
1142
|
+
// same predicate the handler's own guard uses, one layer earlier, never
|
|
1143
|
+
// stricter. The four siblings omit the label; declaring it here makes the
|
|
1144
|
+
// fail-closed intent survive a refactor of the handler.
|
|
1145
|
+
//
|
|
1146
|
+
// HOW THE FLOOR ACTUALLY CLEARS — the previous note here said "the executor's
|
|
1147
|
+
// world-key call carries `data.workspace`, which the ask route turns into
|
|
1148
|
+
// ctx.ownerSlug". Both halves were wrong. The shipped emitter
|
|
1149
|
+
// (.claude/scripts/factory-emit.sh) is NOT a world-key call: it posts
|
|
1150
|
+
// `Authorization: Bearer $GATEWAY_API_KEY`, which is isVerifiedServiceCaller
|
|
1151
|
+
// case 2 (gateway-guard.ts:118) — the SERVICE-secret door, not the per-actor
|
|
1152
|
+
// one. A world-key bearer takes the other branch entirely
|
|
1153
|
+
// (`bearerToken && !isServiceCaller`, [...receiver].ts:238) and resolves
|
|
1154
|
+
// `callerUid`, never `serviceOwnerSlug`. And it does not send `workspace` at
|
|
1155
|
+
// all; it sends `slug`, which is the FIRST nomination at :237 and therefore
|
|
1156
|
+
// wins for a service caller. So the floor clears on a caller-supplied `slug`
|
|
1157
|
+
// reaching ctx.ownerSlug — see the note on the `slug` field above.
|
|
1158
|
+
auth: "required",
|
|
1159
|
+
// Two `build/ok` events for one job are two real frames with rising seq, not
|
|
1160
|
+
// a dedupe. The RUN row dedupes (INSERT OR IGNORE); the events do not.
|
|
1161
|
+
effect: "ask", idempotent: false,
|
|
1162
|
+
}),
|
|
989
1163
|
"do:halt": receiver({
|
|
990
1164
|
receiver: "do:halt",
|
|
991
1165
|
summary: "Stop a factory run, or release the stop. Sets a halt latch on the plan thing that `factory:attempt` phase \"open\" refuses against, so no new attempt can be claimed; with `attempt`, also closes that in-flight attempt as dissolved. It does NOT kill an OS process — a cycle already executing stops at its next substrate write",
|
|
@@ -1024,13 +1198,18 @@ export const RECEIVERS = {
|
|
|
1024
1198
|
// Prose goal / what "done" looks like — the context a human attaches at create so
|
|
1025
1199
|
// the task is pickable by an agent or a Claude Code session without a round trip.
|
|
1026
1200
|
notes: z.string().optional(),
|
|
1201
|
+
// Optional parent tid — writes the `containment` edge in the SAME pipeline as the
|
|
1202
|
+
// row, so the factory's downward walk (fn ready-tasks -> derived-from) can reach the
|
|
1203
|
+
// task. Without it the task is an orphan: on the board, unreachable from any plan.
|
|
1204
|
+
// Refused unless the caller has operate access to the parent.
|
|
1205
|
+
parent: z.string().optional(),
|
|
1027
1206
|
// The viewed /u/<slug> workspace to file the task under. Honored only when the
|
|
1028
1207
|
// caller is authorized for it (attested staff or owner-tree control); otherwise
|
|
1029
1208
|
// the resolver falls back to the caller's own slug. Reconciles the create tag
|
|
1030
1209
|
// with the /api/things read filter so a created task survives reload.
|
|
1031
1210
|
workspace: z.string().optional(),
|
|
1032
1211
|
}),
|
|
1033
|
-
response: z.object({ ok: z.boolean(), tid: z.string().optional(), tags: z.array(z.string()).optional() }),
|
|
1212
|
+
response: z.object({ ok: z.boolean(), tid: z.string().optional(), tags: z.array(z.string()).optional(), parent: z.string().optional() }),
|
|
1034
1213
|
effect: "ask", idempotent: false,
|
|
1035
1214
|
}),
|
|
1036
1215
|
"tasks:claim": receiver({
|
|
@@ -1140,12 +1319,70 @@ export const RECEIVERS = {
|
|
|
1140
1319
|
}),
|
|
1141
1320
|
effect: "ask", idempotent: true, auth: "member",
|
|
1142
1321
|
}),
|
|
1322
|
+
// tasks:generate — read what a page says, propose the work it implies, and file it.
|
|
1323
|
+
//
|
|
1324
|
+
// The one task verb that does not take a title: the caller hands over the PAGE
|
|
1325
|
+
// (its title, its own summary, its visible text) and gets back real rows on the
|
|
1326
|
+
// board. Every row is written through `tasks:create`, so the workspace gate, the
|
|
1327
|
+
// announce and the write-failure semantics are the ones that verb already holds —
|
|
1328
|
+
// this receiver only decides WHAT to file, never who may file it.
|
|
1329
|
+
//
|
|
1330
|
+
// No URL is fetched server-side. The caller sends the text it can already see
|
|
1331
|
+
// (the browser reads its own DOM, or same-origin-fetches a page under the
|
|
1332
|
+
// viewer's own session); a receiver that fetched an arbitrary `url` would be an
|
|
1333
|
+
// SSRF door on a surface that spends LLM tokens. `url` is a LABEL here — it
|
|
1334
|
+
// names the source and seeds the dedupe key, and is never dereferenced.
|
|
1335
|
+
//
|
|
1336
|
+
// Idempotent by (source, title): each row carries a derived `slug:` tag, which
|
|
1337
|
+
// tasks:create dedupes on. Clicking twice on the same page cannot double the
|
|
1338
|
+
// board, while a genuinely new suggestion still lands.
|
|
1339
|
+
"tasks:generate": receiver({
|
|
1340
|
+
receiver: "tasks:generate",
|
|
1341
|
+
surfaces: { mcp: true },
|
|
1342
|
+
summary: "Turn a page into tasks: read the page text the caller supplies, propose 3-7 concrete next actions, and write each one through tasks:create (deduped by source+title). Never fetches the URL — `url` labels the source and seeds the dedupe key",
|
|
1343
|
+
request: z.object({
|
|
1344
|
+
/** Source label + dedupe seed. Never fetched. */
|
|
1345
|
+
url: z.string().optional(),
|
|
1346
|
+
title: z.string().optional(),
|
|
1347
|
+
/** The page's own description / heading trail — what `readPageSummary()` returns. */
|
|
1348
|
+
summary: z.string().optional(),
|
|
1349
|
+
/** Visible page text, if the caller has it. Capped server-side. */
|
|
1350
|
+
text: z.string().optional(),
|
|
1351
|
+
/** How many tasks to aim for. Clamped to 1-8. */
|
|
1352
|
+
count: z.number().optional(),
|
|
1353
|
+
/** Extra tags every generated task carries, on top of `from-page`. */
|
|
1354
|
+
tags: z.array(z.string()).optional(),
|
|
1355
|
+
/** The viewed /u/<slug> workspace — same authorization contract as tasks:create. */
|
|
1356
|
+
workspace: z.string().optional(),
|
|
1357
|
+
}),
|
|
1358
|
+
response: z.object({
|
|
1359
|
+
ok: z.boolean(),
|
|
1360
|
+
created: z.number().optional(),
|
|
1361
|
+
deduped: z.number().optional(),
|
|
1362
|
+
/** The workspace the rows actually landed in — the resolver's own answer,
|
|
1363
|
+
* not the caller's request. A caller that guessed this instead would link
|
|
1364
|
+
* the operator at a board their tasks are not on whenever the workspace
|
|
1365
|
+
* override was refused. */
|
|
1366
|
+
workspace: z.string().optional(),
|
|
1367
|
+
tasks: z.array(z.object({ tid: z.string(), title: z.string(), deduped: z.boolean().optional() })).optional(),
|
|
1368
|
+
error: z.string().optional(),
|
|
1369
|
+
}),
|
|
1370
|
+
// Spends LLM tokens and writes rows. An undeclared label is an anonymous door
|
|
1371
|
+
// (see receiver-envelope.ts § requiresAttestedCaller) — this one refuses.
|
|
1372
|
+
effect: "ask", auth: "member", reversible: false, idempotent: true,
|
|
1373
|
+
}),
|
|
1143
1374
|
"tasks:list": receiver({
|
|
1144
1375
|
receiver: "tasks:list",
|
|
1145
1376
|
surfaces: { mcp: true },
|
|
1146
1377
|
summary: "Every task carrying a tag, with its task-status — the read that lets a plan's checkboxes be a PROJECTION of task state instead of a second copy. Scoped to the caller's workspace on the server",
|
|
1147
1378
|
request: z.object({
|
|
1148
1379
|
tag: z.string(),
|
|
1380
|
+
// Optional task-status filter (open|blocked|picked|done|verified|failed|dissolved).
|
|
1381
|
+
// Declared here because the HTTP edge dispatches zod's PARSED output — an
|
|
1382
|
+
// undeclared field is silently stripped before the resolver reads it, and the
|
|
1383
|
+
// factory turn (one.ie/ai/workflows/factory-turn.tql step:rows) needs the
|
|
1384
|
+
// plan's OPEN rows to fan tasks:launch over.
|
|
1385
|
+
status: z.string().optional(),
|
|
1149
1386
|
workspace: z.string().optional(),
|
|
1150
1387
|
}),
|
|
1151
1388
|
response: z.object({
|
|
@@ -1282,12 +1519,20 @@ export const RECEIVERS = {
|
|
|
1282
1519
|
"tasks:subtask": receiver({
|
|
1283
1520
|
receiver: "tasks:subtask",
|
|
1284
1521
|
surfaces: { mcp: true },
|
|
1285
|
-
summary: "Create a child task
|
|
1522
|
+
summary: "Create a child task COMPLETE — row, notes, tags, its `containment` edge to the parent and every `blockedBy` prerequisite — in ONE pipeline, so a child never appears claimable with an empty body or missing ordering",
|
|
1286
1523
|
request: z.object({
|
|
1287
1524
|
parent: z.string(),
|
|
1288
1525
|
title: z.string(),
|
|
1289
1526
|
tags: z.array(z.string()).optional(),
|
|
1290
1527
|
assignee: z.string().optional(),
|
|
1528
|
+
// The prose goal, written at creation. A child that arrives with an empty body is
|
|
1529
|
+
// claimable-but-unbuildable; notes here close that window.
|
|
1530
|
+
notes: z.string().optional(),
|
|
1531
|
+
// One tid or a list (max 16) of prerequisites. Each writes a `blocks` edge in the
|
|
1532
|
+
// same pipeline as the row, so ordering is created at intake rather than remembered
|
|
1533
|
+
// later. Validated exactly as tasks:depend does: operate-role on each prerequisite
|
|
1534
|
+
// and the transitive cycle walk.
|
|
1535
|
+
blockedBy: z.union([z.string(), z.array(z.string())]).optional(),
|
|
1291
1536
|
workspace: z.string().optional(),
|
|
1292
1537
|
}),
|
|
1293
1538
|
response: z.object({
|
|
@@ -1295,6 +1540,7 @@ export const RECEIVERS = {
|
|
|
1295
1540
|
tid: z.string().optional(),
|
|
1296
1541
|
parent: z.string().optional(),
|
|
1297
1542
|
tags: z.array(z.string()).optional(),
|
|
1543
|
+
blockedBy: z.array(z.string()).optional(),
|
|
1298
1544
|
error: z.string().optional(),
|
|
1299
1545
|
}),
|
|
1300
1546
|
effect: "ask", idempotent: false, auth: "member",
|
|
@@ -1390,7 +1636,14 @@ export const RECEIVERS = {
|
|
|
1390
1636
|
tid: z.string(),
|
|
1391
1637
|
ok: z.boolean(),
|
|
1392
1638
|
actorId: z.string().optional(),
|
|
1393
|
-
|
|
1639
|
+
// `unspecced` = the task has no Proof: row (factory/spec-gate.ts).
|
|
1640
|
+
// `spec_unreadable` = we could not READ the task to decide — an outage,
|
|
1641
|
+
// kept distinct from a verdict per factory-spec.md demand 6.
|
|
1642
|
+
reason: z.enum(["unassigned", "human-assignee", "blocked", "forbidden", "fire_failed", "unspecced", "spec_unreadable"]).optional(),
|
|
1643
|
+
/** Why the gate refused, in one human-readable line. */
|
|
1644
|
+
detail: z.string().optional(),
|
|
1645
|
+
/** Set when the task launched only on the spec gate's grandfather clause. */
|
|
1646
|
+
warning: z.string().optional(),
|
|
1394
1647
|
})),
|
|
1395
1648
|
}),
|
|
1396
1649
|
effect: "ask", idempotent: false,
|
|
@@ -2283,6 +2536,55 @@ export const RECEIVERS = {
|
|
|
2283
2536
|
}),
|
|
2284
2537
|
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
|
|
2285
2538
|
}),
|
|
2539
|
+
// ── EVENTS (dim 5) — the analytics ingress ───────────────────────────────────
|
|
2540
|
+
// event:track — the write behind POST /api/events, the browser pixel's door.
|
|
2541
|
+
// PUBLIC by construction: the pixel runs on third-party pages with no session,
|
|
2542
|
+
// no key and no cookie the substrate controls, so an auth label here would
|
|
2543
|
+
// throw in bindReceiver's applyAuth and silently kill 46k events/30d.
|
|
2544
|
+
// Identity is never taken from this payload; the route attests the caller and
|
|
2545
|
+
// hands the resolver an already-enriched row.
|
|
2546
|
+
"event:track": receiver({
|
|
2547
|
+
receiver: "event:track",
|
|
2548
|
+
summary: "Record one analytics event (agent_events_warm) and broadcast it to the workspace AnalyticsRelay",
|
|
2549
|
+
request: z.object({
|
|
2550
|
+
id: z.string(),
|
|
2551
|
+
ts: z.number(),
|
|
2552
|
+
slug: z.string(),
|
|
2553
|
+
event: z.string(),
|
|
2554
|
+
source: z.string(),
|
|
2555
|
+
visitor_hash: z.string().optional().nullable(),
|
|
2556
|
+
agent_id: z.string().optional().nullable(),
|
|
2557
|
+
variant: z.string().optional().nullable(),
|
|
2558
|
+
thread_id: z.string().optional().nullable(),
|
|
2559
|
+
actor_id: z.string().optional().nullable(),
|
|
2560
|
+
actor_type: z.string().optional().nullable(),
|
|
2561
|
+
channel: z.string().optional().nullable(),
|
|
2562
|
+
campaign: z.string().optional().nullable(),
|
|
2563
|
+
link_id: z.string().optional().nullable(),
|
|
2564
|
+
referrer: z.string().optional().nullable(),
|
|
2565
|
+
user_agent_class: z.string().optional().nullable(),
|
|
2566
|
+
locale: z.string().optional().nullable(),
|
|
2567
|
+
payload: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
2568
|
+
consent_state: z.string().optional().nullable(),
|
|
2569
|
+
region: z.string().optional().nullable(),
|
|
2570
|
+
tags: z.array(z.string()).optional().nullable(),
|
|
2571
|
+
}),
|
|
2572
|
+
response: z.object({ ok: z.boolean(), id: z.string().optional(), error: z.string().optional() }),
|
|
2573
|
+
examples: [
|
|
2574
|
+
{
|
|
2575
|
+
id: "01J8ZQ3K2P0000000000000000",
|
|
2576
|
+
ts: 1767225600000,
|
|
2577
|
+
slug: "one",
|
|
2578
|
+
event: "pageview",
|
|
2579
|
+
source: "web",
|
|
2580
|
+
visitor_hash: "d41d8cd98f00b204",
|
|
2581
|
+
consent_state: "granted",
|
|
2582
|
+
region: "IE",
|
|
2583
|
+
payload: { page: "/pricing" },
|
|
2584
|
+
},
|
|
2585
|
+
],
|
|
2586
|
+
effect: "signal", cost: "free", reversible: false, idempotent: true, auth: "public",
|
|
2587
|
+
}),
|
|
2286
2588
|
// ── FUNNELS (text/funnels-plan.md) — author + run funnel definitions ──────────
|
|
2287
2589
|
// funnel:create — create a new funnel definition (draft).
|
|
2288
2590
|
"funnel:create": receiver({
|
|
@@ -4667,6 +4969,21 @@ export const RECEIVERS = {
|
|
|
4667
4969
|
}),
|
|
4668
4970
|
effect: "ask", auth: "public", cost: "free", reversible: false, idempotent: true,
|
|
4669
4971
|
}),
|
|
4972
|
+
// ── health — the doctor. Handler in resolvers/health.ts ───────────────────
|
|
4973
|
+
"health:diagnose": receiver({
|
|
4974
|
+
receiver: "health:diagnose",
|
|
4975
|
+
summary: "Probe every deployed surface and the receiver failure rates D1 already records, then write the verdict back as weighted paths (mark on up, warn on down or on a sick receiver) so the next sweep starts from what the last one learned. Diagnoses only — it never restarts, redeploys or kills anything, because the remote half has no provably safe remedy. Fired by the health-tick cron trigger.",
|
|
4976
|
+
request: z.object({}),
|
|
4977
|
+
response: z.object({
|
|
4978
|
+
ok: z.boolean(),
|
|
4979
|
+
verdict: z.enum(["healthy", "degraded", "unhealthy"]).optional(),
|
|
4980
|
+
symptoms: z.array(z.string()).optional(),
|
|
4981
|
+
surfaces: z.array(z.unknown()).optional(),
|
|
4982
|
+
sick: z.array(z.unknown()).optional(),
|
|
4983
|
+
checkedAt: z.string().optional(),
|
|
4984
|
+
}),
|
|
4985
|
+
effect: "ask", auth: "public", cost: "free", reversible: false, idempotent: true,
|
|
4986
|
+
}),
|
|
4670
4987
|
// ── seo — handlers in resolvers/seo.ts (C1 live; C2 async) ─────────────────
|
|
4671
4988
|
"seo:backlinks-summary": receiver({
|
|
4672
4989
|
receiver: "seo:backlinks-summary",
|