agent-relay-server 0.125.0 → 0.127.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/docs/openapi.json +1 -1
- package/package.json +9 -5
- package/public/assets/{MessageBubble-BYeYFteG.js → MessageBubble-D2cnxyTy.js} +2 -2
- package/public/assets/{MessageBubble-BYeYFteG.js.map → MessageBubble-D2cnxyTy.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-iAXvkZKW.js → PlanGraphPanel-Cnxt-x7R.js} +2 -2
- package/public/assets/{PlanGraphPanel-iAXvkZKW.js.map → PlanGraphPanel-Cnxt-x7R.js.map} +1 -1
- package/public/assets/{activity-Du9SRJt5.js → activity-BEm1BQB1.js} +2 -2
- package/public/assets/{activity-Du9SRJt5.js.map → activity-BEm1BQB1.js.map} +1 -1
- package/public/assets/{agent-profiles-DXDQnrlH.js → agent-profiles-C8V_oCiM.js} +2 -2
- package/public/assets/{agent-profiles-DXDQnrlH.js.map → agent-profiles-C8V_oCiM.js.map} +1 -1
- package/public/assets/{agent-type-icon-B3i0rndK.js → agent-type-icon-BmBR-9CH.js} +2 -2
- package/public/assets/{agent-type-icon-B3i0rndK.js.map → agent-type-icon-BmBR-9CH.js.map} +1 -1
- package/public/assets/{agents-CWOdKrxP.js → agents-D8mc9X1j.js} +2 -2
- package/public/assets/{agents-CWOdKrxP.js.map → agents-D8mc9X1j.js.map} +1 -1
- package/public/assets/{analytics-YcY59Hv9.js → analytics-BZtRycyL.js} +2 -2
- package/public/assets/{analytics-YcY59Hv9.js.map → analytics-BZtRycyL.js.map} +1 -1
- package/public/assets/{automation-CItTse7F.js → automation-BEhJZkNQ.js} +2 -2
- package/public/assets/{automation-CItTse7F.js.map → automation-BEhJZkNQ.js.map} +1 -1
- package/public/assets/{badge-XEru6wto.js → badge-BCvp7LK3.js} +2 -2
- package/public/assets/{badge-XEru6wto.js.map → badge-BCvp7LK3.js.map} +1 -1
- package/public/assets/{branch-state-badge-QjTktTYj.js → branch-state-badge-Cscjqf-E.js} +2 -2
- package/public/assets/{branch-state-badge-QjTktTYj.js.map → branch-state-badge-Cscjqf-E.js.map} +1 -1
- package/public/assets/{button-C3CQSr4p.js → button-DyHjsHY3.js} +2 -2
- package/public/assets/{button-C3CQSr4p.js.map → button-DyHjsHY3.js.map} +1 -1
- package/public/assets/{card-C5XTIPvq.js → card-BoqYxpVW.js} +2 -2
- package/public/assets/{card-C5XTIPvq.js.map → card-BoqYxpVW.js.map} +1 -1
- package/public/assets/{channels-BSFTsbhT.js → channels-DAtOnA6o.js} +2 -2
- package/public/assets/{channels-BSFTsbhT.js.map → channels-DAtOnA6o.js.map} +1 -1
- package/public/assets/chat-BAH9bEPx.js +2 -0
- package/public/assets/chat-BAH9bEPx.js.map +1 -0
- package/public/assets/{connectors-C7d5dlYz.js → connectors-B2grC-hl.js} +2 -2
- package/public/assets/{connectors-C7d5dlYz.js.map → connectors-B2grC-hl.js.map} +1 -1
- package/public/assets/{copy-button-Bil2CjzT.js → copy-button-C67FUG3E.js} +2 -2
- package/public/assets/{copy-button-Bil2CjzT.js.map → copy-button-C67FUG3E.js.map} +1 -1
- package/public/assets/{dist-V3wu3BHw.js → dist-BD4Xc13v.js} +2 -2
- package/public/assets/{dist-V3wu3BHw.js.map → dist-BD4Xc13v.js.map} +1 -1
- package/public/assets/{dist-C5z05Vkz.js → dist-BFDfbfNC.js} +2 -2
- package/public/assets/{dist-C5z05Vkz.js.map → dist-BFDfbfNC.js.map} +1 -1
- package/public/assets/{dist-CjyfQThq.js → dist-BJKxqIQd.js} +2 -2
- package/public/assets/{dist-CjyfQThq.js.map → dist-BJKxqIQd.js.map} +1 -1
- package/public/assets/{dist-B8tD9ZcR.js → dist-BNB3fTO-.js} +2 -2
- package/public/assets/{dist-B8tD9ZcR.js.map → dist-BNB3fTO-.js.map} +1 -1
- package/public/assets/{dist-CFtFIhu3.js → dist-CHKra2JV.js} +2 -2
- package/public/assets/{dist-CFtFIhu3.js.map → dist-CHKra2JV.js.map} +1 -1
- package/public/assets/{dist-r8wyUTkK.js → dist-CQMQ5iku.js} +2 -2
- package/public/assets/{dist-r8wyUTkK.js.map → dist-CQMQ5iku.js.map} +1 -1
- package/public/assets/{dist-DKmTXfpP.js → dist-C_VOBSaK.js} +2 -2
- package/public/assets/{dist-DKmTXfpP.js.map → dist-C_VOBSaK.js.map} +1 -1
- package/public/assets/{dist-CGaHpxnN.js → dist-D5w04z0l.js} +2 -2
- package/public/assets/{dist-CGaHpxnN.js.map → dist-D5w04z0l.js.map} +1 -1
- package/public/assets/{dist-4VwhnLtj.js → dist-DG75bvPp.js} +2 -2
- package/public/assets/{dist-4VwhnLtj.js.map → dist-DG75bvPp.js.map} +1 -1
- package/public/assets/{dist-CrUl2aQp.js → dist-G8YBD-wp.js} +2 -2
- package/public/assets/{dist-CrUl2aQp.js.map → dist-G8YBD-wp.js.map} +1 -1
- package/public/assets/{dist-BJaV2BD4.js → dist-f6MW74eX.js} +2 -2
- package/public/assets/{dist-BJaV2BD4.js.map → dist-f6MW74eX.js.map} +1 -1
- package/public/assets/{es2015-5DTJe35n.js → es2015-a6fysEVz.js} +2 -2
- package/public/assets/{es2015-5DTJe35n.js.map → es2015-a6fysEVz.js.map} +1 -1
- package/public/assets/formatted-body-Rv9RDPsQ.js +3 -0
- package/public/assets/{formatted-body-BuLfVMeD.js.map → formatted-body-Rv9RDPsQ.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DO-8giS6.js → formatted-body-impl-XFU4tPeT.js} +2 -2
- package/public/assets/{formatted-body-impl-DO-8giS6.js.map → formatted-body-impl-XFU4tPeT.js.map} +1 -1
- package/public/assets/{index-De1tR7J4.js → index-Bhy_jnX4.js} +6 -6
- package/public/assets/{index-De1tR7J4.js.map → index-Bhy_jnX4.js.map} +1 -1
- package/public/assets/index-DHzXsQot.css +2 -0
- package/public/assets/{input-CXMbJ848.js → input-Rc4oWv4q.js} +2 -2
- package/public/assets/{input-CXMbJ848.js.map → input-Rc4oWv4q.js.map} +1 -1
- package/public/assets/{insights-Ci3VuA5Y.js → insights-DpXPAEwH.js} +2 -2
- package/public/assets/{insights-Ci3VuA5Y.js.map → insights-DpXPAEwH.js.map} +1 -1
- package/public/assets/{integrations-BboxWQQD.js → integrations-9c6IdRU4.js} +2 -2
- package/public/assets/{integrations-BboxWQQD.js.map → integrations-9c6IdRU4.js.map} +1 -1
- package/public/assets/{lib-BcUW1l-W.js → lib-BZ6WPg2t.js} +2 -2
- package/public/assets/{lib-BcUW1l-W.js.map → lib-BZ6WPg2t.js.map} +1 -1
- package/public/assets/lucide-react-NXHPIMsX.js +10 -0
- package/public/assets/lucide-react-NXHPIMsX.js.map +1 -0
- package/public/assets/{maintenance-CO1miJIJ.js → maintenance-BrkqfGwx.js} +2 -2
- package/public/assets/{maintenance-CO1miJIJ.js.map → maintenance-BrkqfGwx.js.map} +1 -1
- package/public/assets/{managed-agents-r4oYK3Fm.js → managed-agents-CLANxOAN.js} +2 -2
- package/public/assets/{managed-agents-r4oYK3Fm.js.map → managed-agents-CLANxOAN.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-5CCM9iyL.js → markdown-preview-impl-CcZFkjn-.js} +2 -2
- package/public/assets/{markdown-preview-impl-5CCM9iyL.js.map → markdown-preview-impl-CcZFkjn-.js.map} +1 -1
- package/public/assets/{memory-BJJS07W-.js → memory-BoQw8q-i.js} +2 -2
- package/public/assets/{memory-BJJS07W-.js.map → memory-BoQw8q-i.js.map} +1 -1
- package/public/assets/{messages-RT_dfEsL.js → messages-DfKNuph2.js} +2 -2
- package/public/assets/{messages-RT_dfEsL.js.map → messages-DfKNuph2.js.map} +1 -1
- package/public/assets/{orchestrators-8hyl8hW1.js → orchestrators-sQh8e7-V.js} +2 -2
- package/public/assets/{orchestrators-8hyl8hW1.js.map → orchestrators-sQh8e7-V.js.map} +1 -1
- package/public/assets/{overview-CUJ9QmW_.js → overview-B0z8ZNtp.js} +2 -2
- package/public/assets/{overview-CUJ9QmW_.js.map → overview-B0z8ZNtp.js.map} +1 -1
- package/public/assets/{pairs-CzkzMGZq.js → pairs-B3FhGmn-.js} +2 -2
- package/public/assets/{pairs-CzkzMGZq.js.map → pairs-B3FhGmn-.js.map} +1 -1
- package/public/assets/{projects-CAuLWBU5.js → projects-iDkxAGBW.js} +2 -2
- package/public/assets/{projects-CAuLWBU5.js.map → projects-iDkxAGBW.js.map} +1 -1
- package/public/assets/{prompt-settings-BnB90R7F.js → prompt-settings-CCj_s008.js} +2 -2
- package/public/assets/{prompt-settings-BnB90R7F.js.map → prompt-settings-CCj_s008.js.map} +1 -1
- package/public/assets/{react-dom-BQICbUuF.js → react-dom-8yaD9iJG.js} +2 -2
- package/public/assets/{react-dom-BQICbUuF.js.map → react-dom-8yaD9iJG.js.map} +1 -1
- package/public/assets/{registry-B8TInqru.js → registry-mD_y06Ru.js} +2 -2
- package/public/assets/{registry-B8TInqru.js.map → registry-mD_y06Ru.js.map} +1 -1
- package/public/assets/{security-DG_VVZUW.js → security-CTSA9niL.js} +2 -2
- package/public/assets/{security-DG_VVZUW.js.map → security-CTSA9niL.js.map} +1 -1
- package/public/assets/{select-BJ8G38_V.js → select-gEQEHCIf.js} +2 -2
- package/public/assets/{select-BJ8G38_V.js.map → select-gEQEHCIf.js.map} +1 -1
- package/public/assets/{settings-CK_T9z5w.js → settings-ByAR-TYj.js} +2 -2
- package/public/assets/{settings-CK_T9z5w.js.map → settings-ByAR-TYj.js.map} +1 -1
- package/public/assets/store-D-Frk_EC.js +9 -0
- package/public/assets/store-D-Frk_EC.js.map +1 -0
- package/public/assets/{task-chip-BHM-e3I8.js → task-chip-CeujtXYT.js} +2 -2
- package/public/assets/{task-chip-BHM-e3I8.js.map → task-chip-CeujtXYT.js.map} +1 -1
- package/public/assets/{tasks-Briui9Mp.js → tasks-CZyJX881.js} +2 -2
- package/public/assets/{tasks-Briui9Mp.js.map → tasks-CZyJX881.js.map} +1 -1
- package/public/assets/{teams-Ck4EVV3R.js → teams-BrL4sEL7.js} +2 -2
- package/public/assets/{teams-Ck4EVV3R.js.map → teams-BrL4sEL7.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-66ZjVN1v.js → terminal-viewer-impl-LjIph8ql.js} +2 -2
- package/public/assets/{terminal-viewer-impl-66ZjVN1v.js.map → terminal-viewer-impl-LjIph8ql.js.map} +1 -1
- package/public/assets/types-BqeSkWrv.js.map +1 -1
- package/public/assets/{use-keyboard-viewport-CAa7IQTm.js → use-keyboard-viewport-cGEmrGE7.js} +2 -2
- package/public/assets/{use-keyboard-viewport-CAa7IQTm.js.map → use-keyboard-viewport-cGEmrGE7.js.map} +1 -1
- package/public/assets/{user-avatar-BOy9o2xK.js → user-avatar-qbo-B3dQ.js} +2 -2
- package/public/assets/{user-avatar-BOy9o2xK.js.map → user-avatar-qbo-B3dQ.js.map} +1 -1
- package/public/assets/{work-queue-CnghFRG2.js → work-queue-CSu5kRsw.js} +2 -2
- package/public/assets/{work-queue-CnghFRG2.js.map → work-queue-CSu5kRsw.js.map} +1 -1
- package/public/assets/{workspaces-BzmQnzSm.js → workspaces-CoDz-iZd.js} +2 -2
- package/public/assets/{workspaces-BzmQnzSm.js.map → workspaces-CoDz-iZd.js.map} +1 -1
- package/public/index.html +24 -24
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +2 -2
- package/runner/src/adapters/codex-agent-message-capture.ts +38 -8
- package/runner/src/relay-mcp-proxy.ts +1 -3
- package/runner/src/runner-core.ts +33 -13
- package/runner/src/runner-helpers.ts +31 -0
- package/src/db/index.ts +1 -0
- package/src/db/messages.ts +38 -2
- package/src/db/task-deliverables.ts +206 -0
- package/src/db/task-entity.ts +4 -8
- package/src/mcp/index.ts +4 -4
- package/src/mcp/tools-workspace.ts +3 -36
- package/src/mcp-task-deliver-tools.ts +143 -0
- package/src/notification-rules.ts +1 -0
- package/src/notification-types.ts +2 -0
- package/src/prompt-resolver.ts +6 -6
- package/src/routes/agent-sessions.ts +10 -7
- package/src/routes/orchestrator.ts +5 -2
- package/src/routes/settings.ts +31 -2
- package/src/services/restart-instructions.ts +36 -0
- package/src/services/task-delivery.ts +275 -0
- package/src/settings/registry.ts +14 -3
- package/src/workspace-phase.ts +5 -5
- package/public/assets/chat-CRzFSFWe.js +0 -2
- package/public/assets/chat-CRzFSFWe.js.map +0 -1
- package/public/assets/formatted-body-BuLfVMeD.js +0 -3
- package/public/assets/index-C9X_0QBt.css +0 -2
- package/public/assets/lucide-react-CoU_Xts2.js +0 -10
- package/public/assets/lucide-react-CoU_Xts2.js.map +0 -1
- package/public/assets/store-Dl9Z8YUS.js +0 -9
- package/public/assets/store-Dl9Z8YUS.js.map +0 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TASK_DELIVERY_DISPOSITIONS,
|
|
3
|
+
TASK_GATE_STATUSES,
|
|
4
|
+
type TaskCommitRef,
|
|
5
|
+
type TaskBlocker,
|
|
6
|
+
type TaskDeliverableEventMetadata,
|
|
7
|
+
type TaskDeliveryDisposition,
|
|
8
|
+
type TaskDeliveryGateReports,
|
|
9
|
+
type TaskDeliveryResult,
|
|
10
|
+
type TaskDeliverySelfAcceptance,
|
|
11
|
+
type TaskEvent,
|
|
12
|
+
type TaskGateStatus,
|
|
13
|
+
} from "../types";
|
|
14
|
+
import { parseJson } from "../utils";
|
|
15
|
+
import { getDb, ValidationError } from "./connection.ts";
|
|
16
|
+
import { rowToTaskEvent } from "./mappers.ts";
|
|
17
|
+
import { DELIVERABLE_TASK_KIND, insertTaskEvent } from "./task-core.ts";
|
|
18
|
+
|
|
19
|
+
function requireText(value: unknown, field: string, max: number): string {
|
|
20
|
+
if (typeof value !== "string" || !value.trim()) throw new ValidationError(`${field} required`);
|
|
21
|
+
const cleaned = value.trim();
|
|
22
|
+
if (cleaned.length > max) throw new ValidationError(`${field} must be ${max} characters or fewer`);
|
|
23
|
+
return cleaned;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function optionalText(value: unknown, field: string, max: number): string | undefined {
|
|
27
|
+
if (value === undefined || value === null) return undefined;
|
|
28
|
+
if (typeof value !== "string") throw new ValidationError(`${field} must be a string`);
|
|
29
|
+
const cleaned = value.trim();
|
|
30
|
+
if (!cleaned) return undefined;
|
|
31
|
+
if (cleaned.length > max) throw new ValidationError(`${field} must be ${max} characters or fewer`);
|
|
32
|
+
return cleaned;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function taskCommitRows(id: number): TaskCommitRef[] {
|
|
40
|
+
const row = getDb().query("SELECT kind, commits FROM tasks WHERE id = ?").get(id) as { kind: string; commits: string | null } | undefined;
|
|
41
|
+
if (!row) throw new ValidationError(`task ${id} not found`);
|
|
42
|
+
if (row.kind !== DELIVERABLE_TASK_KIND) throw new ValidationError(`task ${id} is not a deliverable task`);
|
|
43
|
+
return parseJson<TaskCommitRef[]>(row.commits, []);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function appendCommitRow(id: number, commit: TaskCommitRef): void {
|
|
47
|
+
const sha = requireText(commit.sha, "commit.sha", 80);
|
|
48
|
+
const subject = optionalText(commit.subject, "commit.subject", 1000);
|
|
49
|
+
const commits = taskCommitRows(id);
|
|
50
|
+
if (commits.some((c) => c.sha === sha)) return;
|
|
51
|
+
commits.push({ sha, ...(subject ? { subject } : {}) });
|
|
52
|
+
getDb().query("UPDATE tasks SET commits = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(commits), Date.now(), id);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cleanCommitRefs(value: unknown): TaskCommitRef[] | undefined {
|
|
56
|
+
if (value === undefined || value === null) return undefined;
|
|
57
|
+
if (!Array.isArray(value)) throw new ValidationError("commits must be an array");
|
|
58
|
+
const commits = value.map((entry, index) => {
|
|
59
|
+
if (!isObject(entry)) throw new ValidationError(`commits[${index}] must be an object`);
|
|
60
|
+
return {
|
|
61
|
+
sha: requireText(entry.sha, `commits[${index}].sha`, 80),
|
|
62
|
+
...(optionalText(entry.subject, `commits[${index}].subject`, 1000) ? { subject: optionalText(entry.subject, `commits[${index}].subject`, 1000)! } : {}),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
return commits.length > 0 ? commits : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cleanGateReport(value: unknown, field: string): TaskDeliveryGateReports["typecheck"] {
|
|
69
|
+
if (value === undefined || value === null) return undefined;
|
|
70
|
+
if (!isObject(value)) throw new ValidationError(`${field} must be an object`);
|
|
71
|
+
if (!TASK_GATE_STATUSES.includes(value.status as TaskGateStatus)) throw new ValidationError(`${field}.status must be one of: ${TASK_GATE_STATUSES.join(", ")}`);
|
|
72
|
+
const count = value.count;
|
|
73
|
+
if (count !== undefined && (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0)) {
|
|
74
|
+
throw new ValidationError(`${field}.count must be a non-negative integer`);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
status: value.status as TaskGateStatus,
|
|
78
|
+
...(optionalText(value.detail, `${field}.detail`, 4000) ? { detail: optionalText(value.detail, `${field}.detail`, 4000)! } : {}),
|
|
79
|
+
...(typeof count === "number" ? { count } : {}),
|
|
80
|
+
...(optionalText(value.runId, `${field}.runId`, 240) ? { runId: optionalText(value.runId, `${field}.runId`, 240)! } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cleanGateReports(value: unknown): TaskDeliveryGateReports | undefined {
|
|
85
|
+
if (value === undefined || value === null) return undefined;
|
|
86
|
+
if (!isObject(value)) throw new ValidationError("gates must be an object");
|
|
87
|
+
const out: TaskDeliveryGateReports = {};
|
|
88
|
+
const typecheck = cleanGateReport(value.typecheck, "gates.typecheck");
|
|
89
|
+
const test = cleanGateReport(value.test, "gates.test");
|
|
90
|
+
if (typecheck) out.typecheck = typecheck;
|
|
91
|
+
if (test) out.test = test;
|
|
92
|
+
if (value.other !== undefined) {
|
|
93
|
+
if (!Array.isArray(value.other)) throw new ValidationError("gates.other must be an array");
|
|
94
|
+
const other = value.other.map((entry, index) => {
|
|
95
|
+
if (!isObject(entry)) throw new ValidationError(`gates.other[${index}] must be an object`);
|
|
96
|
+
const gate = cleanGateReport(entry, `gates.other[${index}]`)!;
|
|
97
|
+
return { name: requireText(entry.name, `gates.other[${index}].name`, 120), ...gate };
|
|
98
|
+
});
|
|
99
|
+
if (other.length > 0) out.other = other;
|
|
100
|
+
}
|
|
101
|
+
return out.typecheck || out.test || out.other?.length ? out : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cleanRisks(value: unknown): string[] | undefined {
|
|
105
|
+
if (value === undefined || value === null) return undefined;
|
|
106
|
+
if (!Array.isArray(value)) throw new ValidationError("risks must be an array");
|
|
107
|
+
const risks = value.map((entry, index) => requireText(entry, `risks[${index}]`, 4000));
|
|
108
|
+
return risks.length > 0 ? risks : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function cleanBlockedBy(value: unknown): TaskBlocker[] | undefined {
|
|
112
|
+
if (value === undefined || value === null) return undefined;
|
|
113
|
+
if (!Array.isArray(value)) throw new ValidationError("blockedBy must be an array");
|
|
114
|
+
const entries = value.map((entry, index) => {
|
|
115
|
+
if (!isObject(entry)) throw new ValidationError(`blockedBy[${index}] must be an object`);
|
|
116
|
+
const type = entry.type as TaskBlocker["type"];
|
|
117
|
+
if (type !== "dep" && type !== "gate" && type !== "decision" && type !== "external") {
|
|
118
|
+
throw new ValidationError(`blockedBy[${index}].type must be one of: dep, gate, decision, external`);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
type,
|
|
122
|
+
...(optionalText(entry.ref, `blockedBy[${index}].ref`, 240) ? { ref: optionalText(entry.ref, `blockedBy[${index}].ref`, 240)! } : {}),
|
|
123
|
+
...(optionalText(entry.note, `blockedBy[${index}].note`, 2000) ? { note: optionalText(entry.note, `blockedBy[${index}].note`, 2000)! } : {}),
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
return entries.length > 0 ? entries : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function cleanSelfAcceptance(value: unknown): TaskDeliverySelfAcceptance[] | undefined {
|
|
130
|
+
if (value === undefined || value === null) return undefined;
|
|
131
|
+
if (!Array.isArray(value)) throw new ValidationError("selfAcceptance must be an array");
|
|
132
|
+
const entries = value.map((entry, index) => {
|
|
133
|
+
if (!isObject(entry)) throw new ValidationError(`selfAcceptance[${index}] must be an object`);
|
|
134
|
+
if (entry.status !== "met" && entry.status !== "unmet") throw new ValidationError(`selfAcceptance[${index}].status must be met or unmet`);
|
|
135
|
+
return {
|
|
136
|
+
criterion: requireText(entry.criterion, `selfAcceptance[${index}].criterion`, 1000),
|
|
137
|
+
status: entry.status as TaskDeliverySelfAcceptance["status"],
|
|
138
|
+
...(optionalText(entry.note, `selfAcceptance[${index}].note`, 4000) ? { note: optionalText(entry.note, `selfAcceptance[${index}].note`, 4000)! } : {}),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
return entries.length > 0 ? entries : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface RecordTaskDeliverableInput extends TaskDeliveryResult {
|
|
145
|
+
actor: string;
|
|
146
|
+
workspaceId?: string;
|
|
147
|
+
idempotencyKey?: string;
|
|
148
|
+
land?: TaskDeliverableEventMetadata["land"];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function recordTaskDeliverable(id: number, input: RecordTaskDeliverableInput): { event: TaskEvent; created: boolean } {
|
|
152
|
+
taskCommitRows(id);
|
|
153
|
+
const actor = requireText(input.actor, "actor", 200);
|
|
154
|
+
const idempotencyKey = optionalText(input.idempotencyKey, "idempotencyKey", 240);
|
|
155
|
+
if (idempotencyKey) {
|
|
156
|
+
const existing = getDb()
|
|
157
|
+
.query("SELECT * FROM task_events WHERE task_id = ? AND type = 'deliverable' AND json_extract(metadata, '$.idempotencyKey') = ? LIMIT 1")
|
|
158
|
+
.get(id, idempotencyKey) as any;
|
|
159
|
+
if (existing) return { event: rowToTaskEvent(existing), created: false };
|
|
160
|
+
}
|
|
161
|
+
if (!TASK_DELIVERY_DISPOSITIONS.includes(input.disposition as TaskDeliveryDisposition)) {
|
|
162
|
+
throw new ValidationError(`disposition must be one of: ${TASK_DELIVERY_DISPOSITIONS.join(", ")}`);
|
|
163
|
+
}
|
|
164
|
+
const commits = cleanCommitRefs(input.commits);
|
|
165
|
+
const gates = cleanGateReports(input.gates);
|
|
166
|
+
const reason = optionalText(input.reason, "reason", 64_000);
|
|
167
|
+
const blockedBy = cleanBlockedBy(input.blockedBy);
|
|
168
|
+
const filesChanged = input.filesChanged;
|
|
169
|
+
if (filesChanged !== undefined && (typeof filesChanged !== "number" || !Number.isSafeInteger(filesChanged) || filesChanged < 0)) {
|
|
170
|
+
throw new ValidationError("filesChanged must be a non-negative integer");
|
|
171
|
+
}
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const metadata: TaskDeliverableEventMetadata = {
|
|
174
|
+
v: 1,
|
|
175
|
+
disposition: input.disposition,
|
|
176
|
+
summary: requireText(input.summary, "summary", 1_000_000),
|
|
177
|
+
actor,
|
|
178
|
+
...(reason ? { reason } : {}),
|
|
179
|
+
...(blockedBy ? { blockedBy } : {}),
|
|
180
|
+
...(commits ? { commits } : {}),
|
|
181
|
+
...(gates ? { gates } : {}),
|
|
182
|
+
...(typeof filesChanged === "number" ? { filesChanged } : {}),
|
|
183
|
+
...(optionalText(input.rca, "rca", 64_000) ? { rca: optionalText(input.rca, "rca", 64_000)! } : {}),
|
|
184
|
+
...(optionalText(input.testCoverage, "testCoverage", 64_000) ? { testCoverage: optionalText(input.testCoverage, "testCoverage", 64_000)! } : {}),
|
|
185
|
+
...(cleanRisks(input.risks) ? { risks: cleanRisks(input.risks)! } : {}),
|
|
186
|
+
...(cleanSelfAcceptance(input.selfAcceptance) ? { selfAcceptance: cleanSelfAcceptance(input.selfAcceptance)! } : {}),
|
|
187
|
+
...(optionalText(input.workspaceId, "workspaceId", 240) ? { workspaceId: optionalText(input.workspaceId, "workspaceId", 240)! } : {}),
|
|
188
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
189
|
+
...(input.land ? { land: input.land } : {}),
|
|
190
|
+
};
|
|
191
|
+
const event = getDb().transaction(() => {
|
|
192
|
+
if (commits) {
|
|
193
|
+
for (const commit of commits) appendCommitRow(id, commit);
|
|
194
|
+
}
|
|
195
|
+
getDb().query("UPDATE tasks SET updated_at = ?, last_seen_at = ? WHERE id = ?").run(now, now, id);
|
|
196
|
+
return insertTaskEvent(id, {
|
|
197
|
+
source: actor,
|
|
198
|
+
type: "deliverable",
|
|
199
|
+
severity: input.disposition === "failed" || input.disposition === "blocked" ? "warning" : "info",
|
|
200
|
+
title: `Delivery result: ${input.disposition}`,
|
|
201
|
+
body: metadata.summary,
|
|
202
|
+
metadata: metadata as unknown as Record<string, unknown>,
|
|
203
|
+
}, now);
|
|
204
|
+
})();
|
|
205
|
+
return { event, created: true };
|
|
206
|
+
}
|
package/src/db/task-entity.ts
CHANGED
|
@@ -322,14 +322,7 @@ export function setTaskStage(id: number, input: SetTaskStageInput): TaskDetail {
|
|
|
322
322
|
return getTaskDetail(id)!;
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
export interface RecordGateInput {
|
|
326
|
-
gate: string;
|
|
327
|
-
status: TaskGateStatus;
|
|
328
|
-
detail?: string;
|
|
329
|
-
commitSha?: string;
|
|
330
|
-
runId?: string;
|
|
331
|
-
actor?: string;
|
|
332
|
-
}
|
|
325
|
+
export interface RecordGateInput { gate: string; status: TaskGateStatus; detail?: string; count?: number; commitSha?: string; runId?: string; actor?: string; }
|
|
333
326
|
|
|
334
327
|
// Quality-gate ledger write (#834). The append-only `task_events` IS the ledger (no second
|
|
335
328
|
// system of record). Idempotent by runId: the SAME run reported twice does not double-append,
|
|
@@ -341,6 +334,7 @@ export function recordGate(id: number, input: RecordGateInput): TaskGateState {
|
|
|
341
334
|
if (!TASK_GATE_STATUSES.includes(input.status)) {
|
|
342
335
|
throw new ValidationError(`gate status must be one of: ${TASK_GATE_STATUSES.join(", ")}`);
|
|
343
336
|
}
|
|
337
|
+
if (input.count !== undefined && (!Number.isSafeInteger(input.count) || input.count < 0)) throw new ValidationError("count must be a non-negative integer");
|
|
344
338
|
const type = `${GATE_EVENT_PREFIX}${gate}`;
|
|
345
339
|
const runId = optionalText(input.runId, "runId", 240);
|
|
346
340
|
if (runId) {
|
|
@@ -360,6 +354,7 @@ export function recordGate(id: number, input: RecordGateInput): TaskGateState {
|
|
|
360
354
|
gate,
|
|
361
355
|
status: input.status,
|
|
362
356
|
...(input.detail ? { detail: input.detail } : {}),
|
|
357
|
+
...(typeof input.count === "number" ? { count: input.count } : {}),
|
|
363
358
|
...(input.commitSha ? { commitSha: input.commitSha } : {}),
|
|
364
359
|
...(runId ? { runId } : {}),
|
|
365
360
|
},
|
|
@@ -373,6 +368,7 @@ function gateStateFromEvent(row: { type: string; created_at: number; metadata: s
|
|
|
373
368
|
gate: typeof meta.gate === "string" ? meta.gate : row.type.slice(GATE_EVENT_PREFIX.length),
|
|
374
369
|
status: (meta.status as TaskGateStatus) ?? "fail",
|
|
375
370
|
...(typeof meta.detail === "string" ? { detail: meta.detail } : {}),
|
|
371
|
+
...(typeof meta.count === "number" ? { count: meta.count } : {}),
|
|
376
372
|
...(typeof meta.commitSha === "string" ? { commitSha: meta.commitSha } : {}),
|
|
377
373
|
...(typeof meta.runId === "string" ? { runId: meta.runId } : {}),
|
|
378
374
|
ranAt: row.created_at,
|
package/src/mcp/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { McpAuthError, McpNotFoundError } from "../mcp-errors";
|
|
|
6
6
|
import { createActivityEvent, ValidationError } from "../db";
|
|
7
7
|
import { PLAN_TOOLS, relayPlanTool } from "../mcp-plan-tools";
|
|
8
8
|
import { TASK_TOOLS, relayTaskTool } from "../mcp-task-tools";
|
|
9
|
+
import { TASK_DELIVER_TOOLS, relayTaskDeliverTool } from "../mcp-task-deliver-tools";
|
|
9
10
|
import { TASK_MEMORY_TOOLS, relayTaskMemoryTool } from "../mcp-task-memory-tools";
|
|
10
11
|
import { DISPATCH_TOOLS, relayDispatchTool } from "../mcp-dispatch-tools";
|
|
11
12
|
import { TASK_WEAVE_TOOLS, relayTaskWeaveTool } from "../mcp-task-weave-tools";
|
|
@@ -37,7 +38,7 @@ const TOOLS: ToolDefinition[] = [
|
|
|
37
38
|
AGENT_ACTION_TOOL,
|
|
38
39
|
...WORKSPACE_TOOLS,
|
|
39
40
|
...MERGE_TOOLS, ...TEAM_TOOLS,
|
|
40
|
-
...PLAN_TOOLS, ...TASK_TOOLS, ...TASK_MEMORY_TOOLS, ...DISPATCH_TOOLS, ...TASK_WEAVE_TOOLS, ...RELEASE_TOOLS, ...ISSUE_REF_TOOLS, ...ISSUE_ASSOCIATION_TOOLS, ...ISSUE_ACTIVITY_TOOLS, ...ISSUE_LIFECYCLE_TOOLS,
|
|
41
|
+
...PLAN_TOOLS, ...TASK_TOOLS, ...TASK_DELIVER_TOOLS, ...TASK_MEMORY_TOOLS, ...DISPATCH_TOOLS, ...TASK_WEAVE_TOOLS, ...RELEASE_TOOLS, ...ISSUE_REF_TOOLS, ...ISSUE_ASSOCIATION_TOOLS, ...ISSUE_ACTIVITY_TOOLS, ...ISSUE_LIFECYCLE_TOOLS,
|
|
41
42
|
...BLACKBOARD_TOOLS,
|
|
42
43
|
...PROJECT_TOOLS,
|
|
43
44
|
...MEMORY_TOOLS,
|
|
@@ -52,7 +53,7 @@ export const RELAY_TOOL_GROUPS: Record<string, readonly string[]> = {
|
|
|
52
53
|
core: ["relay_whoami", "relay_send_message", "relay_reply", "relay_get_message", "relay_get_thread", "relay_find_agents"],
|
|
53
54
|
memory: ["relay_recall", "relay_compact_and_resume", "relay_memory_write", "relay_memory_promote", "relay_memory_retract", "relay_memory_read", "relay_memory_search"],
|
|
54
55
|
artifacts: ["relay_upload_artifact", "relay_attach_artifact"],
|
|
55
|
-
workspace: ["relay_workspace_status", "
|
|
56
|
+
workspace: ["relay_workspace_status", "relay_workspace_deps", "relay_workspace_list", "relay_workspace_claim", "relay_workspace_release", "relay_task_deliver", "relay_merge_pause", "relay_merge_pauses", "relay_merge_renew", "relay_merge_resume"],
|
|
56
57
|
"spawn-lifecycle": ["relay_spawn_agent", "relay_spawn_targets", "relay_my_agents", "relay_shutdown_agent", "relay_agent_action", "relay_agent_status", "relay_agent_crash_report"],
|
|
57
58
|
schedule: ["relay_schedule_timer", "relay_list_timers", "relay_cancel_timer"],
|
|
58
59
|
plans: ["relay_plan_create", "relay_plan_add_node", "relay_plan_update_node", "relay_plan_add_edge", "relay_plan_remove_node", "relay_plan_remove_edge", "relay_plan_archive", "relay_plan_delete", "relay_plan_get"],
|
|
@@ -167,13 +168,12 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
|
|
|
167
168
|
else if (name === "relay_agent_action") result = relayAgentAction(auth, args);
|
|
168
169
|
else if (name === "relay_workspace_status") result = await relayWorkspaceStatus(auth, args);
|
|
169
170
|
else if (name === "relay_workspace_list") result = relayWorkspaceList(auth, args);
|
|
170
|
-
else if (name === "relay_workspace_ready") result = await relayWorkspaceMutation(auth, "ready", args);
|
|
171
171
|
else if (name === "relay_workspace_deps") result = await relayWorkspaceMutation(auth, "deps-refresh", args);
|
|
172
172
|
else if (name === "relay_workspace_claim") result = await relayWorkspaceMutation(auth, "claim", args);
|
|
173
173
|
else if (name === "relay_workspace_release") result = await relayWorkspaceMutation(auth, "release-claim", args);
|
|
174
|
-
else if (name === "relay_workspace_land") result = await relayWorkspaceMutation(auth, "merge", args);
|
|
175
174
|
else if (name.startsWith("relay_merge_")) result = relayMergeTool(auth, name, args); else if (name.startsWith("relay_team_")) result = await relayTeamTool(auth, name, args);
|
|
176
175
|
else if (name.startsWith("relay_plan_")) result = relayPlanTool(auth, name, args);
|
|
176
|
+
else if (name === "relay_task_deliver") result = await relayTaskDeliverTool(auth, name, args);
|
|
177
177
|
else if (name.startsWith("relay_task_memory_")) result = relayTaskMemoryTool(auth, name, args);
|
|
178
178
|
else if (name === "relay_task_dispatch") result = relayDispatchTool(auth, name, args);
|
|
179
179
|
else if (name === "relay_task_comment" || name === "relay_task_close_bridge") result = await relayTaskWeaveTool(auth, name, args);
|
|
@@ -15,33 +15,19 @@ import { optionalBoolean, optionalPositiveInt, optionalString } from "./validati
|
|
|
15
15
|
export const WORKSPACE_TOOLS: ToolDefinition[] = [
|
|
16
16
|
{
|
|
17
17
|
name: "relay_workspace_status",
|
|
18
|
-
description: "Get your isolated workspace's lifecycle status (active/ready/conflict/review_requested/merged/...). With wait:true this BLOCKS until the status changes; do not use wait:true after
|
|
18
|
+
description: "Get your isolated workspace's lifecycle status (active/ready/conflict/review_requested/merged/...). With wait:true this BLOCKS until the status changes; do not use wait:true after relay_task_deliver unless explicitly debugging. Normal branch workers deliver the Task, stop their turn, and are woken later by landed-success or landed-failure notifications. Defaults to your own workspace; pass workspaceId to target another.",
|
|
19
19
|
requiredScopes: ["agents:read", "agent:read", "agent:write"],
|
|
20
20
|
alwaysLoad: true,
|
|
21
21
|
inputSchema: {
|
|
22
22
|
type: "object",
|
|
23
23
|
properties: {
|
|
24
24
|
workspaceId: { type: "string", description: "Defaults to your own isolated workspace (resolved from your identity)." },
|
|
25
|
-
wait: { type: "boolean", description: "Block until the workspace status changes (the actionable signal), or until the timeout. Do not use this after
|
|
25
|
+
wait: { type: "boolean", description: "Block until the workspace status changes (the actionable signal), or until the timeout. Do not use this after relay_task_deliver in the normal worker flow; stop and wait for landed-success/landed-failure instead." },
|
|
26
26
|
timeoutSeconds: { type: "integer", minimum: 1, maximum: 600, description: "Max seconds to wait when wait:true (default 300, max 600). Returns early on any transition." },
|
|
27
27
|
},
|
|
28
28
|
additionalProperties: false,
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
|
-
{
|
|
32
|
-
name: "relay_workspace_ready",
|
|
33
|
-
description: "Signal that your branch work is finished and ready to merge back to main. This kicks off the auto-merge-back: a clean merge lands immediately; on conflict a steward agent reconciles and lands it (escalating if it can't). After this tool returns, stop your turn and go idle; Relay wakes you later with landed-success when the merge lands and your branch refreshes, or landed-failure if landing fatally fails. Defaults to your own workspace.",
|
|
34
|
-
requiredScopes: ["agent:write"],
|
|
35
|
-
alwaysLoad: true,
|
|
36
|
-
inputSchema: {
|
|
37
|
-
type: "object",
|
|
38
|
-
properties: {
|
|
39
|
-
workspaceId: { type: "string", description: "Defaults to your own isolated workspace." },
|
|
40
|
-
detail: { type: "string", description: "Optional note recorded on the activity feed." },
|
|
41
|
-
},
|
|
42
|
-
additionalProperties: false,
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
31
|
{
|
|
46
32
|
name: "relay_workspace_deps",
|
|
47
33
|
description: "Re-provision your worktree's dependencies when the shared symlinked node_modules has gone stale (e.g. typecheck reports a missing module). checkOnly:true just reports staleness without changing anything. Self-service — acts only on your own worktree.",
|
|
@@ -98,25 +84,6 @@ export const WORKSPACE_TOOLS: ToolDefinition[] = [
|
|
|
98
84
|
additionalProperties: false,
|
|
99
85
|
},
|
|
100
86
|
},
|
|
101
|
-
{
|
|
102
|
-
name: "relay_workspace_land",
|
|
103
|
-
description: "Dispatch a base merge for your workspace directly (lease-serialized per repo, same path the auto-merge job uses). Most branch agents should use relay_workspace_ready instead and let the relay land it; this is the explicit/steward path and requires the command:write scope.",
|
|
104
|
-
requiredScopes: ["command:write"],
|
|
105
|
-
inputSchema: {
|
|
106
|
-
type: "object",
|
|
107
|
-
properties: {
|
|
108
|
-
workspaceId: { type: "string", description: "Defaults to your own isolated workspace." },
|
|
109
|
-
strategy: { type: "string", enum: ["pr", "rebase-ff", "auto"], description: "Merge strategy (default auto). Falls back to the instance landing.strategy config when unset." },
|
|
110
|
-
deleteBranch: { type: "boolean", description: "Delete the branch after a successful merge (default true)." },
|
|
111
|
-
prTitle: { type: "string" },
|
|
112
|
-
prBody: { type: "string" },
|
|
113
|
-
autoMerge: { type: "string", enum: AUTO_MERGE_POLICIES, description: "Auto-merge policy for PR-strategy lands. Defaults to 'on-green' when strategy resolves to 'pr'. Ignored for rebase-ff/auto strategies." },
|
|
114
|
-
reviewer: { type: "string", description: "GitHub login or team slug to request as a reviewer on the opened PR (pr strategy only)." },
|
|
115
|
-
detail: { type: "string" },
|
|
116
|
-
},
|
|
117
|
-
additionalProperties: false,
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
87
|
];
|
|
121
88
|
|
|
122
89
|
function isWorkspaceAdmin(auth: McpAuthContext): boolean {
|
|
@@ -219,7 +186,7 @@ export async function relayWorkspaceMutation(auth: McpAuthContext, action: Works
|
|
|
219
186
|
if (action === "ready") {
|
|
220
187
|
payload.contract = readyContract(result.workspace);
|
|
221
188
|
payload.guidance = describeWorkspacePhase(result.workspace);
|
|
222
|
-
//
|
|
189
|
+
// Legacy workspace-ready route/CLI path is an explicit, platform-owned completion signal: the caller
|
|
223
190
|
// is declaring its work window closed. Close it, forwarding only the held final group.
|
|
224
191
|
markAgentWorkFinal(result.workspace.ownerAgentId ?? callerAgentId(auth), "workspace-ready");
|
|
225
192
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { McpAuthError } from "./mcp-errors";
|
|
2
|
+
import { ValidationError } from "./db";
|
|
3
|
+
import { authContextFromMcp } from "./services/auth-context";
|
|
4
|
+
import { deliverTask } from "./services/task-delivery";
|
|
5
|
+
import { ServiceAuthError } from "./services/errors";
|
|
6
|
+
import { cleanString, optionalEnum } from "./validation";
|
|
7
|
+
import {
|
|
8
|
+
TASK_DELIVERY_DISPOSITIONS,
|
|
9
|
+
TASK_BLOCKER_TYPES,
|
|
10
|
+
TASK_GATE_STATUSES,
|
|
11
|
+
isRecord,
|
|
12
|
+
type TaskDeliveryGateReports,
|
|
13
|
+
type TaskDeliveryResult,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
const TASK_ID_PROP = { oneOf: [{ type: "integer", minimum: 1 }, { type: "string" }], description: "Task id. Optional when the caller is bound to exactly one Task." } as const;
|
|
17
|
+
const COMMIT_SCHEMA = {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: { sha: { type: "string" }, subject: { type: "string" } },
|
|
20
|
+
required: ["sha"],
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
} as const;
|
|
23
|
+
const GATE_RESULT_SCHEMA = {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
status: { type: "string", enum: TASK_GATE_STATUSES },
|
|
27
|
+
detail: { type: "string" },
|
|
28
|
+
count: { type: "integer", minimum: 0 },
|
|
29
|
+
runId: { type: "string" },
|
|
30
|
+
},
|
|
31
|
+
required: ["status"],
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
export const TASK_DELIVER_TOOLS = [
|
|
36
|
+
{
|
|
37
|
+
name: "relay_task_deliver",
|
|
38
|
+
description: "Submit a structured Delivery Result for a durable Task. Writes a typed deliverable event to the Task thread before any side effect. disposition:'complete' requires passing typecheck+test gates and auto-triggers the caller's task-bound isolated workspace handoff to land.",
|
|
39
|
+
requiredScopes: ["task:write"],
|
|
40
|
+
alwaysLoad: true,
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
taskId: TASK_ID_PROP,
|
|
45
|
+
disposition: { type: "string", enum: TASK_DELIVERY_DISPOSITIONS },
|
|
46
|
+
summary: { type: "string" },
|
|
47
|
+
reason: { type: "string", description: "Required for blocked/failed deliveries." },
|
|
48
|
+
blockedBy: {
|
|
49
|
+
type: "array",
|
|
50
|
+
description: "Required for blocked deliveries; records the blocker set on the Task.",
|
|
51
|
+
items: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: { type: { type: "string", enum: TASK_BLOCKER_TYPES }, ref: { type: "string" }, note: { type: "string" } },
|
|
54
|
+
required: ["type"],
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
commits: { type: "array", items: COMMIT_SCHEMA },
|
|
59
|
+
gates: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
typecheck: GATE_RESULT_SCHEMA,
|
|
63
|
+
test: GATE_RESULT_SCHEMA,
|
|
64
|
+
other: {
|
|
65
|
+
type: "array",
|
|
66
|
+
items: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: { name: { type: "string" }, status: { type: "string", enum: TASK_GATE_STATUSES }, detail: { type: "string" }, count: { type: "integer", minimum: 0 }, runId: { type: "string" } },
|
|
69
|
+
required: ["name", "status"],
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
filesChanged: { type: "integer", minimum: 0 },
|
|
77
|
+
rca: { type: "string" },
|
|
78
|
+
testCoverage: { type: "string" },
|
|
79
|
+
risks: { type: "array", items: { type: "string" } },
|
|
80
|
+
selfAcceptance: {
|
|
81
|
+
type: "array",
|
|
82
|
+
items: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: { criterion: { type: "string" }, status: { type: "string", enum: ["met", "unmet"] }, note: { type: "string" } },
|
|
85
|
+
required: ["criterion", "status"],
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
workspaceId: { type: "string", description: "Defaults to the caller's isolated workspace." },
|
|
90
|
+
idempotencyKey: { type: "string", description: "Dedupes retry delivery and prevents double-land on replay." },
|
|
91
|
+
},
|
|
92
|
+
required: ["disposition", "summary"],
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
] satisfies Array<{ name: string; description: string; requiredScopes: string[]; inputSchema: Record<string, unknown>; alwaysLoad?: boolean }>;
|
|
97
|
+
|
|
98
|
+
type McpAuthLike = Parameters<typeof authContextFromMcp>[0];
|
|
99
|
+
|
|
100
|
+
function optionalPositiveInt(value: unknown, field: string): number | undefined {
|
|
101
|
+
if (value === undefined || value === null) return undefined;
|
|
102
|
+
const n = typeof value === "string" ? Number(value) : value;
|
|
103
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) throw new ValidationError(`${field} must be a positive integer`);
|
|
104
|
+
return n;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cleanDeliveryGates(value: unknown): TaskDeliveryGateReports | undefined {
|
|
108
|
+
if (value === undefined || value === null) return undefined;
|
|
109
|
+
if (!isRecord(value)) throw new ValidationError("gates must be an object");
|
|
110
|
+
return value as TaskDeliveryGateReports;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cleanDeliveryResult(args: Record<string, unknown>): TaskDeliveryResult {
|
|
114
|
+
return {
|
|
115
|
+
disposition: optionalEnum(args.disposition, "disposition", TASK_DELIVERY_DISPOSITIONS)!,
|
|
116
|
+
summary: cleanString(args.summary, "summary", { required: true, max: 1_000_000 })!,
|
|
117
|
+
...(args.reason !== undefined ? { reason: cleanString(args.reason, "reason", { max: 64_000 }) } : {}),
|
|
118
|
+
...(Array.isArray(args.blockedBy) ? { blockedBy: args.blockedBy as TaskDeliveryResult["blockedBy"] } : {}),
|
|
119
|
+
...(Array.isArray(args.commits) ? { commits: args.commits as TaskDeliveryResult["commits"] } : {}),
|
|
120
|
+
...(args.gates !== undefined ? { gates: cleanDeliveryGates(args.gates) } : {}),
|
|
121
|
+
...(typeof args.filesChanged === "number" ? { filesChanged: args.filesChanged } : {}),
|
|
122
|
+
...(args.rca !== undefined ? { rca: cleanString(args.rca, "rca", { max: 64_000 }) } : {}),
|
|
123
|
+
...(args.testCoverage !== undefined ? { testCoverage: cleanString(args.testCoverage, "testCoverage", { max: 64_000 }) } : {}),
|
|
124
|
+
...(Array.isArray(args.risks) ? { risks: args.risks.filter((risk): risk is string => typeof risk === "string") } : {}),
|
|
125
|
+
...(Array.isArray(args.selfAcceptance) ? { selfAcceptance: args.selfAcceptance as TaskDeliveryResult["selfAcceptance"] } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function relayTaskDeliverTool(auth: McpAuthLike, name: string, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
130
|
+
if (name !== "relay_task_deliver") throw new ValidationError(`unknown tool: ${name}`);
|
|
131
|
+
if (!isRecord(args)) throw new ValidationError("arguments must be an object");
|
|
132
|
+
try {
|
|
133
|
+
return await deliverTask({
|
|
134
|
+
...cleanDeliveryResult(args),
|
|
135
|
+
taskId: optionalPositiveInt(args.taskId, "taskId"),
|
|
136
|
+
workspaceId: cleanString(args.workspaceId, "workspaceId", { max: 240 }),
|
|
137
|
+
idempotencyKey: cleanString(args.idempotencyKey, "idempotencyKey", { max: 240 }),
|
|
138
|
+
}, authContextFromMcp(auth));
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error instanceof ServiceAuthError) throw new McpAuthError(error.message);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -2,6 +2,7 @@ export type NotificationType =
|
|
|
2
2
|
| "message.new"
|
|
3
3
|
| "landed-failure"
|
|
4
4
|
| "branch.landed"
|
|
5
|
+
| "task.deliverable"
|
|
5
6
|
| "task.deliverable_landed"
|
|
6
7
|
| "task.created"
|
|
7
8
|
| "task.started"
|
|
@@ -99,6 +100,7 @@ export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
|
|
|
99
100
|
"message.new": null,
|
|
100
101
|
"landed-failure": null,
|
|
101
102
|
"branch.landed": null,
|
|
103
|
+
"task.deliverable": null,
|
|
102
104
|
// The #831 auto-driven "task deliverable landed" receipt to the coordinator. Durable (null):
|
|
103
105
|
// it carries the auto-actions the coordinator confirms/exception-handles, so it must survive
|
|
104
106
|
// the coordinator being briefly offline rather than expiring as an ephemeral bystander ping.
|
package/src/prompt-resolver.ts
CHANGED
|
@@ -289,7 +289,7 @@ const DEFAULT_PROMPT_TEMPLATES = [
|
|
|
289
289
|
template: lines(
|
|
290
290
|
"You are in an isolated git worktree on branch {{branch}}, based on {{baseRef}} — NOT the main checkout. {{baseRef}} moves under you as other agents land in parallel; that's expected.",
|
|
291
291
|
"Your shell's working directory is already this worktree — run commands directly without prefixing `cd`.",
|
|
292
|
-
"Changes reach the base via: commit your work, then call `
|
|
292
|
+
"Changes reach the base via: commit your work, then call `relay_task_deliver` with disposition `complete`, a summary, commits, and passing `typecheck` + `test` gates. Relay records the typed delivery on the Task, then rebases onto the latest base, lands, and pushes for you.",
|
|
293
293
|
"Do NOT push, rebase, merge, resolve conflicts, or `cd` into the main checkout — Relay (and a steward, spawned only if a clean auto-merge isn't possible) own all of that.",
|
|
294
294
|
"After `ready` the status is `ready` (a normal, healthy hand-off state, not a stall). Stop your turn and go idle; Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails.",
|
|
295
295
|
"Call `relay_workspace_status` anytime to see where you are and the exact next step.",
|
|
@@ -297,7 +297,7 @@ const DEFAULT_PROMPT_TEMPLATES = [
|
|
|
297
297
|
defaultTemplate: lines(
|
|
298
298
|
"You are in an isolated git worktree on branch {{branch}}, based on {{baseRef}} — NOT the main checkout. {{baseRef}} moves under you as other agents land in parallel; that's expected.",
|
|
299
299
|
"Your shell's working directory is already this worktree — run commands directly without prefixing `cd`.",
|
|
300
|
-
"Changes reach the base via: commit your work, then call `
|
|
300
|
+
"Changes reach the base via: commit your work, then call `relay_task_deliver` with disposition `complete`, a summary, commits, and passing `typecheck` + `test` gates. Relay records the typed delivery on the Task, then rebases onto the latest base, lands, and pushes for you.",
|
|
301
301
|
"Do NOT push, rebase, merge, resolve conflicts, or `cd` into the main checkout — Relay (and a steward, spawned only if a clean auto-merge isn't possible) own all of that.",
|
|
302
302
|
"After `ready` the status is `ready` (a normal, healthy hand-off state, not a stall). Stop your turn and go idle; Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails.",
|
|
303
303
|
"Call `relay_workspace_status` anytime to see where you are and the exact next step.",
|
|
@@ -423,8 +423,8 @@ const DEFAULT_PROMPT_TEMPLATES = [
|
|
|
423
423
|
category: "workspace-phase",
|
|
424
424
|
title: "Workspace Active Hint",
|
|
425
425
|
description: "Workspace status hint for active isolated work.",
|
|
426
|
-
template: "Commit your work in this worktree, then
|
|
427
|
-
defaultTemplate: "Commit your work in this worktree, then
|
|
426
|
+
template: "Commit your work in this worktree, then deliver the Task with `relay_task_deliver` using disposition `complete` and passing `typecheck` + `test` gates. Relay records the typed delivery, lands the branch, and pushes for you — do NOT push, rebase, or merge yourself, and never touch the main checkout.",
|
|
427
|
+
defaultTemplate: "Commit your work in this worktree, then deliver the Task with `relay_task_deliver` using disposition `complete` and passing `typecheck` + `test` gates. Relay records the typed delivery, lands the branch, and pushes for you — do NOT push, rebase, or merge yourself, and never touch the main checkout.",
|
|
428
428
|
variables: [],
|
|
429
429
|
},
|
|
430
430
|
{
|
|
@@ -432,8 +432,8 @@ const DEFAULT_PROMPT_TEMPLATES = [
|
|
|
432
432
|
category: "workspace-phase",
|
|
433
433
|
title: "Workspace Ready Headline",
|
|
434
434
|
description: "Workspace status headline for healthy handoff states.",
|
|
435
|
-
template: "
|
|
436
|
-
defaultTemplate: "
|
|
435
|
+
template: "Delivered — stop this turn and go idle while Relay lands your branch. This is the normal, healthy post-delivery state (not an escalation).",
|
|
436
|
+
defaultTemplate: "Delivered — stop this turn and go idle while Relay lands your branch. This is the normal, healthy post-delivery state (not an escalation).",
|
|
437
437
|
variables: [],
|
|
438
438
|
},
|
|
439
439
|
{
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { RELAY_TOKEN_HEADER, isRecord, isSpawnProvider } from "agent-relay-sdk";
|
|
3
3
|
import { VALID_AGENT_ACTIONS, agentControlActionIcon, auditEvent, authAuditMetadata, authorizeAgentRoute, authorizeRoute, canViewAgentRoute, cleanAttachmentRefs, dashboardAttribution, emitCommand, error, json, metaString, parseBody, requestUserAddress, requestUserId, spawnRequestId, type AgentControlAction, type Handler } from "./_shared";
|
|
4
4
|
import { arbitrateDrive, currentDriver, drivingMessage, releaseDrive, type DriveArbitration } from "../agent-drive-arbiter";
|
|
5
|
-
import { ValidationError, getAgent, getAgentTimeline, getOrchestrator, markReady, mergeAgentMeta,
|
|
5
|
+
import { ValidationError, getAgent, getAgentTimeline, getOrchestrator, markReady, mergeAgentMeta, sendMessage, withResolvedProvisioning } from "../db";
|
|
6
6
|
import { announcePresence, viewersForAgent } from "../dashboard-presence";
|
|
7
7
|
import { buildManagedSpawnParams } from "../managed-policy";
|
|
8
8
|
import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams } from "../spawn-command";
|
|
@@ -19,8 +19,8 @@ import { ShutdownAuthError, ShutdownTargetError, shutdownAgent } from "../servic
|
|
|
19
19
|
import { type AgentCard, type SpawnApprovalMode } from "../types";
|
|
20
20
|
import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
21
21
|
import { getManifest } from "agent-relay-providers";
|
|
22
|
-
import { composeProfileSpawnInstructions } from "../project-instructions";
|
|
23
22
|
import { relayToken } from "../config";
|
|
23
|
+
import { restartInstructionParamsForAgent } from "../services/restart-instructions";
|
|
24
24
|
|
|
25
25
|
// 409 when a second human tries to drive an agent another user is already driving (#394). The
|
|
26
26
|
// blocked driver learns who holds the wheel so the UI can show "X is driving" + offer "request".
|
|
@@ -133,6 +133,7 @@ export function restartSpawnParamsForAgent(
|
|
|
133
133
|
const agentProfile = resolvedProfile ? applyDefaultSharedMcp(resolvedProfile) : undefined;
|
|
134
134
|
const workspaceMode = metaString(agent.meta, "workspaceMode") ?? "inherit";
|
|
135
135
|
const resolvedModel = resolveSpawnModelParams(provider, model, effort, { onError: "passthrough", skipDefaultWhenEmpty: true });
|
|
136
|
+
const projectedInstructions = restartInstructionParamsForAgent(agent, { spawnRequestId, cwd }) ?? {};
|
|
136
137
|
// #1052-v2 — a dashboard-spawned agent has NO spawn policy, so its restart skips the
|
|
137
138
|
// `buildManagedSpawnParams` policy branch above and lands here. Re-project the profile role
|
|
138
139
|
// charter / project knowledge / scoped memory (the SAME projection a fresh spawn gets) so the
|
|
@@ -152,8 +153,6 @@ export function restartSpawnParamsForAgent(
|
|
|
152
153
|
// params — the SAME field a fresh agent.spawn stashes it in, see spawn-agent.ts). The reader then
|
|
153
154
|
// recovers it from either the original agent.spawn row OR a later agent.restart row, so each
|
|
154
155
|
// generation has its own recoverable copy and the brief follows the agent across every rotation.
|
|
155
|
-
const callerAppend = readSpawnCallerSystemPromptAppend(spawnRequestId);
|
|
156
|
-
const composed = composeProfileSpawnInstructions({ cwd, operatorId: resolveAgentOwnership(agent.id).ownerUserId, ...(profileName ? { profileName } : {}), ...(agentProfile ? { profile: agentProfile } : {}), ...(callerAppend ? { callerSystemPromptAppend: callerAppend } : {}) });
|
|
157
156
|
const params = buildSpawnCommand({
|
|
158
157
|
provider,
|
|
159
158
|
modelParams: resolvedModel,
|
|
@@ -168,12 +167,12 @@ export function restartSpawnParamsForAgent(
|
|
|
168
167
|
approvalMode: approvalMode ?? "guarded",
|
|
169
168
|
permissionMode: approvalMode ?? "guarded",
|
|
170
169
|
providerArgs: providerArgs.length ? providerArgs : undefined,
|
|
171
|
-
...(
|
|
170
|
+
...(typeof projectedInstructions.systemPromptAppend === "string" ? { systemPromptAppend: projectedInstructions.systemPromptAppend } : {}),
|
|
172
171
|
// #1067 — re-persist the RAW brief (pre-composition) under this restart's fresh spawnRequestId so
|
|
173
172
|
// the NEXT restart, whose predecessor id is this fresh id, can recover it. Without this the brief
|
|
174
173
|
// survives only one restart (the original agent.spawn row is keyed on the very first id).
|
|
175
|
-
...(
|
|
176
|
-
...(
|
|
174
|
+
...(typeof projectedInstructions.callerSystemPromptAppend === "string" ? { callerSystemPromptAppend: projectedInstructions.callerSystemPromptAppend } : {}),
|
|
175
|
+
...(Array.isArray(projectedInstructions.relayInjectionEvents) ? { relayInjectionEvents: projectedInstructions.relayInjectionEvents } : {}),
|
|
177
176
|
policyName: policyName || undefined,
|
|
178
177
|
headless: true,
|
|
179
178
|
spawnRequestId: requestId,
|
|
@@ -298,6 +297,9 @@ export const postAgentAction: Handler = async (req, params) => {
|
|
|
298
297
|
const metaPolicyName = typeof agent.meta?.policyName === "string" ? agent.meta.policyName : undefined;
|
|
299
298
|
const metaSpawnRequestId = typeof agent.meta?.spawnRequestId === "string" ? agent.meta.spawnRequestId : undefined;
|
|
300
299
|
const resumeId = action === "resume" ? latestResumeIdForAgent(agent) : undefined;
|
|
300
|
+
const restartInstructions = action === "restart" || action === "resume"
|
|
301
|
+
? restartInstructionParamsForAgent(agent, { spawnRequestId: metaSpawnRequestId, cwd: metaString(agent.meta, "cwd") ?? orchestrator?.baseDir })
|
|
302
|
+
: undefined;
|
|
301
303
|
if (action === "resume" && !orchestrator) return error("no online orchestrator available to resume agent", 409);
|
|
302
304
|
if (action === "resume" && !resumeId) return error("no resume id recorded for agent", 422);
|
|
303
305
|
const denied = authorizeRoute(req, {
|
|
@@ -319,6 +321,7 @@ export const postAgentAction: Handler = async (req, params) => {
|
|
|
319
321
|
...(metaPolicyName ? { policyName: metaPolicyName } : {}),
|
|
320
322
|
...(metaSpawnRequestId ? { spawnRequestId: metaSpawnRequestId } : {}),
|
|
321
323
|
...(action === "restart" || action === "resume" ? { restartSpawn: restartSpawnParamsForAgent(agent, orchestrator, metaPolicyName, metaSpawnRequestId, { resumeId }) } : {}),
|
|
324
|
+
...(restartInstructions ?? {}),
|
|
322
325
|
...(action === "compact" ? compactMemoryParams(agent) : {}),
|
|
323
326
|
...(resumeId ? { claudeResumeId: resumeId } : {}),
|
|
324
327
|
requestedBy: "dashboard",
|