agent-relay-server 0.104.5 → 0.105.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 +141 -1
- package/package.json +2 -2
- package/public/assets/MessageBubble-CHor4Sxp.js +4 -0
- package/public/assets/{MessageBubble-BpiTbbnp.js.map → MessageBubble-CHor4Sxp.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-k8Aa98g5.js → PlanGraphPanel-jHfkQBX6.js} +2 -2
- package/public/assets/{PlanGraphPanel-k8Aa98g5.js.map → PlanGraphPanel-jHfkQBX6.js.map} +1 -1
- package/public/assets/{activity-CxKi5Erb.js → activity-DDXS6LsX.js} +2 -2
- package/public/assets/{activity-CxKi5Erb.js.map → activity-DDXS6LsX.js.map} +1 -1
- package/public/assets/{agent-profiles-DMZXdN6K.js → agent-profiles-Cn8lCTkg.js} +2 -2
- package/public/assets/{agent-profiles-DMZXdN6K.js.map → agent-profiles-Cn8lCTkg.js.map} +1 -1
- package/public/assets/agents-DBTdcFFz.js +2 -0
- package/public/assets/{agents-BrvpH3gn.js.map → agents-DBTdcFFz.js.map} +1 -1
- package/public/assets/{analytics-1fvh-9ok.js → analytics-VximqXQ9.js} +3 -3
- package/public/assets/{analytics-1fvh-9ok.js.map → analytics-VximqXQ9.js.map} +1 -1
- package/public/assets/{automation-BCgX5WOH.js → automation-L9wq4y5F.js} +2 -2
- package/public/assets/{automation-BCgX5WOH.js.map → automation-L9wq4y5F.js.map} +1 -1
- package/public/assets/{badge-BLar6FjP.js → badge-D8fABzMs.js} +2 -2
- package/public/assets/{badge-BLar6FjP.js.map → badge-D8fABzMs.js.map} +1 -1
- package/public/assets/{branch-state-badge-DYA7bU0i.js → branch-state-badge-CcLZ_u86.js} +2 -2
- package/public/assets/{branch-state-badge-DYA7bU0i.js.map → branch-state-badge-CcLZ_u86.js.map} +1 -1
- package/public/assets/{button-BrIB69qd.js → button-KASZcscA.js} +2 -2
- package/public/assets/{button-BrIB69qd.js.map → button-KASZcscA.js.map} +1 -1
- package/public/assets/card-Cds0MWME.js +2 -0
- package/public/assets/{card-lhnFnBoz.js.map → card-Cds0MWME.js.map} +1 -1
- package/public/assets/{channels-CK80oJYG.js → channels-CcSnLzCG.js} +2 -2
- package/public/assets/{channels-CK80oJYG.js.map → channels-CcSnLzCG.js.map} +1 -1
- package/public/assets/{chat-D_2PEz06.js → chat-BzMj_PO6.js} +2 -2
- package/public/assets/{chat-D_2PEz06.js.map → chat-BzMj_PO6.js.map} +1 -1
- package/public/assets/{connectors-ChGyht5m.js → connectors-DbPOlQhL.js} +2 -2
- package/public/assets/{connectors-ChGyht5m.js.map → connectors-DbPOlQhL.js.map} +1 -1
- package/public/assets/copy-button-d9DFNX9v.js +2 -0
- package/public/assets/{copy-button-Cd-LbSbg.js.map → copy-button-d9DFNX9v.js.map} +1 -1
- package/public/assets/{display-D1xB8NJZ.js → display-BWi8DQCp.js} +2 -2
- package/public/assets/display-BWi8DQCp.js.map +1 -0
- package/public/assets/{dist-BnbGIUmi.js → dist-BbUJ1Vht.js} +2 -2
- package/public/assets/{dist-BnbGIUmi.js.map → dist-BbUJ1Vht.js.map} +1 -1
- package/public/assets/{dist-Bp10H3oR.js → dist-BlZvUZZB.js} +2 -2
- package/public/assets/{dist-Bp10H3oR.js.map → dist-BlZvUZZB.js.map} +1 -1
- package/public/assets/{dist-DDUGmmoi.js → dist-BwwlmyBq.js} +2 -2
- package/public/assets/{dist-DDUGmmoi.js.map → dist-BwwlmyBq.js.map} +1 -1
- package/public/assets/dist-CBVZk9U8.js +2 -0
- package/public/assets/{dist-BEzpMqLy.js.map → dist-CBVZk9U8.js.map} +1 -1
- package/public/assets/{dist-B5VvlkhI.js → dist-CgqfdHSt.js} +2 -2
- package/public/assets/{dist-B5VvlkhI.js.map → dist-CgqfdHSt.js.map} +1 -1
- package/public/assets/dist-DC-TqTz2.js +2 -0
- package/public/assets/{dist-30weaXto.js.map → dist-DC-TqTz2.js.map} +1 -1
- package/public/assets/{dist-DZ97OosY.js → dist-DOAjFOS4.js} +2 -2
- package/public/assets/{dist-DZ97OosY.js.map → dist-DOAjFOS4.js.map} +1 -1
- package/public/assets/{dist-BjOBkHHY.js → dist-DTXKbk2K.js} +2 -2
- package/public/assets/{dist-BjOBkHHY.js.map → dist-DTXKbk2K.js.map} +1 -1
- package/public/assets/dist-DreO5AcZ.js +2 -0
- package/public/assets/{dist-Cgy0FmP-.js.map → dist-DreO5AcZ.js.map} +1 -1
- package/public/assets/{dist-CQF3Jiqg.js → dist-DybScJmm.js} +2 -2
- package/public/assets/{dist-CQF3Jiqg.js.map → dist-DybScJmm.js.map} +1 -1
- package/public/assets/{dist-DAl28ejX.js → dist-UhEAFdvZ.js} +2 -2
- package/public/assets/{dist-DAl28ejX.js.map → dist-UhEAFdvZ.js.map} +1 -1
- package/public/assets/{es2015-B8Z9p8oQ.js → es2015-DUjFyjDp.js} +2 -2
- package/public/assets/{es2015-B8Z9p8oQ.js.map → es2015-DUjFyjDp.js.map} +1 -1
- package/public/assets/{formatted-body-impl-BJ4WCF5J.js → formatted-body-impl-CFWHLkCY.js} +2 -2
- package/public/assets/{formatted-body-impl-BJ4WCF5J.js.map → formatted-body-impl-CFWHLkCY.js.map} +1 -1
- package/public/assets/index-BjjzI2LT.css +2 -0
- package/public/assets/{index-CN7QZlMb.js → index-aCfZQ7LY.js} +13 -13
- package/public/assets/index-aCfZQ7LY.js.map +1 -0
- package/public/assets/{input-B_fUHtLD.js → input-CtHfLn1i.js} +2 -2
- package/public/assets/{input-B_fUHtLD.js.map → input-CtHfLn1i.js.map} +1 -1
- package/public/assets/{insights-BCZRV0hX.js → insights-BuPFE0tf.js} +2 -2
- package/public/assets/{insights-BCZRV0hX.js.map → insights-BuPFE0tf.js.map} +1 -1
- package/public/assets/{integrations-C7UGs00U.js → integrations-hkSW8CzM.js} +2 -2
- package/public/assets/{integrations-C7UGs00U.js.map → integrations-hkSW8CzM.js.map} +1 -1
- package/public/assets/{lib-ng6WZYjE.js → lib-l-JkMVR9.js} +2 -2
- package/public/assets/{lib-ng6WZYjE.js.map → lib-l-JkMVR9.js.map} +1 -1
- package/public/assets/{lucide-react-BYZuAhQs.js → lucide-react-C4eXYCwx.js} +2 -2
- package/public/assets/{lucide-react-BYZuAhQs.js.map → lucide-react-C4eXYCwx.js.map} +1 -1
- package/public/assets/{maintenance-ChYno3Fm.js → maintenance-wYwJ7w6h.js} +2 -2
- package/public/assets/{maintenance-ChYno3Fm.js.map → maintenance-wYwJ7w6h.js.map} +1 -1
- package/public/assets/{managed-agents-DjxZQNIR.js → managed-agents-CKaYD93I.js} +4 -4
- package/public/assets/{managed-agents-DjxZQNIR.js.map → managed-agents-CKaYD93I.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DFa5HF1H.js → markdown-preview-impl-bf4eDQUM.js} +2 -2
- package/public/assets/{markdown-preview-impl-DFa5HF1H.js.map → markdown-preview-impl-bf4eDQUM.js.map} +1 -1
- package/public/assets/memory-DIneEza9.js +2 -0
- package/public/assets/{memory-CTw6kadT.js.map → memory-DIneEza9.js.map} +1 -1
- package/public/assets/{messages-Cjr816D_.js → messages-DP9Vv_wh.js} +2 -2
- package/public/assets/{messages-Cjr816D_.js.map → messages-DP9Vv_wh.js.map} +1 -1
- package/public/assets/{orchestrators-CdmFKA7K.js → orchestrators-CAnKjxdO.js} +2 -2
- package/public/assets/{orchestrators-CdmFKA7K.js.map → orchestrators-CAnKjxdO.js.map} +1 -1
- package/public/assets/{overview-DBOnjfpP.js → overview-B98U3C-y.js} +2 -2
- package/public/assets/{overview-DBOnjfpP.js.map → overview-B98U3C-y.js.map} +1 -1
- package/public/assets/{pairs-BkcdKdF7.js → pairs-DVeeuxhY.js} +2 -2
- package/public/assets/{pairs-BkcdKdF7.js.map → pairs-DVeeuxhY.js.map} +1 -1
- package/public/assets/{projects-ClKbDQB1.js → projects-vt63bwHN.js} +2 -2
- package/public/assets/{projects-ClKbDQB1.js.map → projects-vt63bwHN.js.map} +1 -1
- package/public/assets/prompt-settings-Df-0ZMJ5.js +2 -0
- package/public/assets/prompt-settings-Df-0ZMJ5.js.map +1 -0
- package/public/assets/{react-dom-Dbhs84ug.js → react-dom-BMF4hNzG.js} +2 -2
- package/public/assets/{react-dom-Dbhs84ug.js.map → react-dom-BMF4hNzG.js.map} +1 -1
- package/public/assets/{registry-Bsf3RtOO.js → registry-DlLZyW7_.js} +2 -2
- package/public/assets/{registry-Bsf3RtOO.js.map → registry-DlLZyW7_.js.map} +1 -1
- package/public/assets/{security-Iyu5bhJw.js → security-Bo4Ty3lz.js} +2 -2
- package/public/assets/{security-Iyu5bhJw.js.map → security-Bo4Ty3lz.js.map} +1 -1
- package/public/assets/{select-Bi6tNJDx.js → select-BguZ80M8.js} +2 -2
- package/public/assets/{select-Bi6tNJDx.js.map → select-BguZ80M8.js.map} +1 -1
- package/public/assets/settings-CpX9Xw2j.js +6 -0
- package/public/assets/{settings-CbHp2F9r.js.map → settings-CpX9Xw2j.js.map} +1 -1
- package/public/assets/{store-zeyDiSKE.js → store-DUfrGvOE.js} +3 -3
- package/public/assets/{store-zeyDiSKE.js.map → store-DUfrGvOE.js.map} +1 -1
- package/public/assets/{tasks-CPlf-5Tr.js → tasks-Cg1HXY9l.js} +2 -2
- package/public/assets/{tasks-CPlf-5Tr.js.map → tasks-Cg1HXY9l.js.map} +1 -1
- package/public/assets/{teams-D2uUNhlY.js → teams-BFcYunxB.js} +2 -2
- package/public/assets/{teams-D2uUNhlY.js.map → teams-BFcYunxB.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-uRzbrlXy.js → terminal-viewer-impl-CF2EBa2K.js} +3 -3
- package/public/assets/{terminal-viewer-impl-uRzbrlXy.js.map → terminal-viewer-impl-CF2EBa2K.js.map} +1 -1
- package/public/assets/types-DorDDvEb.js.map +1 -1
- package/public/assets/{use-keyboard-viewport-D8_NYDkh.js → use-keyboard-viewport-RYMALvu1.js} +2 -2
- package/public/assets/{use-keyboard-viewport-D8_NYDkh.js.map → use-keyboard-viewport-RYMALvu1.js.map} +1 -1
- package/public/assets/user-avatar-D1ffSClz.js +2 -0
- package/public/assets/{user-avatar-GFWMuAGK.js.map → user-avatar-D1ffSClz.js.map} +1 -1
- package/public/assets/{work-queue-B30SNmuq.js → work-queue-DV36cxjp.js} +2 -2
- package/public/assets/{work-queue-B30SNmuq.js.map → work-queue-DV36cxjp.js.map} +1 -1
- package/public/assets/workspaces-CIsQqQby.js +3 -0
- package/public/assets/workspaces-CIsQqQby.js.map +1 -0
- package/public/index.html +22 -22
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +114 -23
- package/runner/plugins/claude/monitors/relay-monitor.ts +2 -2
- package/runner/src/adapter.ts +4 -1
- package/src/agent-lifecycle-events.ts +21 -5
- package/src/automations/reconcile.ts +4 -1
- package/src/branch-landed.ts +22 -4
- package/src/config-store.ts +17 -21
- package/src/context-advisory.ts +10 -3
- package/src/db/index.ts +1 -0
- package/src/db/projects.ts +29 -0
- package/src/db/prompt-templates-schema.ts +20 -0
- package/src/db/prompt-templates.ts +91 -0
- package/src/db/schema.ts +5 -4
- package/src/db/tasks.ts +2 -1
- package/src/db/workspaces.ts +41 -1
- package/src/index.ts +3 -0
- package/src/project-instructions.ts +7 -14
- package/src/prompt-resolver.ts +530 -0
- package/src/prompt-template-defaults-runner.ts +136 -0
- package/src/routes/index.ts +5 -0
- package/src/routes/prompt-templates.ts +60 -0
- package/src/routes/workspaces.ts +4 -3
- package/src/routing-policy-push.ts +4 -1
- package/src/security.ts +3 -0
- package/src/self-resume.ts +8 -15
- package/src/services/prompt-templates.ts +19 -0
- package/src/services/register-agent.ts +2 -0
- package/src/services/spawn-issue-activity.ts +107 -0
- package/src/spawn-targets.ts +3 -11
- package/src/steward.ts +5 -23
- package/src/workspace-auto-merge.ts +19 -2
- package/src/workspace-phase.ts +15 -28
- package/public/assets/MessageBubble-BpiTbbnp.js +0 -4
- package/public/assets/agents-BrvpH3gn.js +0 -2
- package/public/assets/card-lhnFnBoz.js +0 -2
- package/public/assets/copy-button-Cd-LbSbg.js +0 -2
- package/public/assets/display-D1xB8NJZ.js.map +0 -1
- package/public/assets/dist-30weaXto.js +0 -2
- package/public/assets/dist-BEzpMqLy.js +0 -2
- package/public/assets/dist-Cgy0FmP-.js +0 -2
- package/public/assets/index-BMO_Sblt.css +0 -2
- package/public/assets/index-CN7QZlMb.js.map +0 -1
- package/public/assets/memory-CTw6kadT.js +0 -2
- package/public/assets/settings-CbHp2F9r.js +0 -6
- package/public/assets/user-avatar-GFWMuAGK.js +0 -2
- package/public/assets/workspaces-CSpmgbPT.js +0 -3
- package/public/assets/workspaces-CSpmgbPT.js.map +0 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { SeedTemplate } from "./db/prompt-templates";
|
|
2
|
+
|
|
3
|
+
function lines(...parts: string[]): string {
|
|
4
|
+
return parts.join("\n");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const RUNNER_PROMPT_TEMPLATE_DEFAULTS = [
|
|
8
|
+
{
|
|
9
|
+
slug: "relay.manual.claude",
|
|
10
|
+
category: "runner",
|
|
11
|
+
title: "Claude Relay Manual",
|
|
12
|
+
description: "Bundled CLAUDE.md relay manual written into provisioned Claude homes.",
|
|
13
|
+
template: lines(
|
|
14
|
+
"# Agent Relay",
|
|
15
|
+
"",
|
|
16
|
+
"- Agent Relay messages may come from humans, channels, or other agents.",
|
|
17
|
+
"- Read the delivered message body and do the requested work.",
|
|
18
|
+
"- Reply through Relay only when the sender still needs an answer.",
|
|
19
|
+
"- Prefer the inline MCP tools `relay_reply` or `relay_send_message` for the actual answer to an incoming Relay message.",
|
|
20
|
+
"- Do not send `/message` with the useful answer and then a separate `/reply` that only says it was sent.",
|
|
21
|
+
"- If multiple Relay messages arrive together, answer once to the latest relevant message and cover the current request. Do not separately acknowledge stale greetings or context.",
|
|
22
|
+
"- If the useful response was already delivered through Relay, do not send an extra \"sent\", \"done\", or \"drafts sent\" confirmation unless the user explicitly asked for one.",
|
|
23
|
+
"- No reply is needed for pure info messages, passive acknowledgements, or reactions that do not ask for action.",
|
|
24
|
+
"- NEVER reply to a notification-class message. The server marks these and renders a single `↪ Notification — no reply needed.` line instead of the reply reminder — it is a fire-and-forget signal (a merge notice, lifecycle event, or FYI). Act on the information if relevant, but do not send any reply, status confirmation, or reaction back.",
|
|
25
|
+
"- Use `agent-relay /react <messageId> <emoji>` instead of a text reply for lightweight acknowledgement, approval, thanks, or \"good job\" after a completed work update.",
|
|
26
|
+
"- Good reaction uses: acknowledge praise with 👍 or ❤️, mark a completed handoff as seen, approve a proposed next step, or acknowledge a passive FYI.",
|
|
27
|
+
"- Do not use reactions when the user asked a question, gave a new task, reported a bug, or needs a textual result.",
|
|
28
|
+
"- A thumbs-up reaction to your question means approval to proceed. A thumbs-up reaction to your completed status means no further action.",
|
|
29
|
+
"- Use `agent-relay get-message <id>` when a delivered preview is truncated.",
|
|
30
|
+
"- Use `agent-relay /status --json` to inspect your current Relay identity and `agent-relay /guide` for command details.",
|
|
31
|
+
),
|
|
32
|
+
defaultTemplate: lines(
|
|
33
|
+
"# Agent Relay",
|
|
34
|
+
"",
|
|
35
|
+
"- Agent Relay messages may come from humans, channels, or other agents.",
|
|
36
|
+
"- Read the delivered message body and do the requested work.",
|
|
37
|
+
"- Reply through Relay only when the sender still needs an answer.",
|
|
38
|
+
"- Prefer the inline MCP tools `relay_reply` or `relay_send_message` for the actual answer to an incoming Relay message.",
|
|
39
|
+
"- Do not send `/message` with the useful answer and then a separate `/reply` that only says it was sent.",
|
|
40
|
+
"- If multiple Relay messages arrive together, answer once to the latest relevant message and cover the current request. Do not separately acknowledge stale greetings or context.",
|
|
41
|
+
"- If the useful response was already delivered through Relay, do not send an extra \"sent\", \"done\", or \"drafts sent\" confirmation unless the user explicitly asked for one.",
|
|
42
|
+
"- No reply is needed for pure info messages, passive acknowledgements, or reactions that do not ask for action.",
|
|
43
|
+
"- NEVER reply to a notification-class message. The server marks these and renders a single `↪ Notification — no reply needed.` line instead of the reply reminder — it is a fire-and-forget signal (a merge notice, lifecycle event, or FYI). Act on the information if relevant, but do not send any reply, status confirmation, or reaction back.",
|
|
44
|
+
"- Use `agent-relay /react <messageId> <emoji>` instead of a text reply for lightweight acknowledgement, approval, thanks, or \"good job\" after a completed work update.",
|
|
45
|
+
"- Good reaction uses: acknowledge praise with 👍 or ❤️, mark a completed handoff as seen, approve a proposed next step, or acknowledge a passive FYI.",
|
|
46
|
+
"- Do not use reactions when the user asked a question, gave a new task, reported a bug, or needs a textual result.",
|
|
47
|
+
"- A thumbs-up reaction to your question means approval to proceed. A thumbs-up reaction to your completed status means no further action.",
|
|
48
|
+
"- Use `agent-relay get-message <id>` when a delivered preview is truncated.",
|
|
49
|
+
"- Use `agent-relay /status --json` to inspect your current Relay identity and `agent-relay /guide` for command details.",
|
|
50
|
+
),
|
|
51
|
+
variables: [],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
slug: "relay.context.claude",
|
|
55
|
+
category: "runner",
|
|
56
|
+
title: "Claude Relay Context",
|
|
57
|
+
description: "One-line relay context note for Claude sessions.",
|
|
58
|
+
template: "[agent-relay] Relay is available. Follow the Agent Relay rules in CLAUDE.md when present, or run agent-relay /guide. Reply through Relay only when a response is needed; do not send status-only follow-ups after the useful Relay response was already delivered.",
|
|
59
|
+
defaultTemplate: "[agent-relay] Relay is available. Follow the Agent Relay rules in CLAUDE.md when present, or run agent-relay /guide. Reply through Relay only when a response is needed; do not send status-only follow-ups after the useful Relay response was already delivered.",
|
|
60
|
+
variables: [],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
slug: "relay.context.claude.readonly",
|
|
64
|
+
category: "runner",
|
|
65
|
+
title: "Claude Relay Context (Read Only)",
|
|
66
|
+
description: "Read-only relay context note for Claude sessions.",
|
|
67
|
+
template: lines(
|
|
68
|
+
"[agent-relay] Relay is available. Follow the Agent Relay rules in CLAUDE.md when present, or run agent-relay /guide. Reply through Relay only when a response is needed; do not send status-only follow-ups after the useful Relay response was already delivered.",
|
|
69
|
+
"",
|
|
70
|
+
"This Claude session is running with restricted read-only Relay permissions. Do not invoke Agent Relay skills. If you need to reply, use this Bash command shape:",
|
|
71
|
+
"",
|
|
72
|
+
"agent-relay /reply <messageId> \"<your reply>\"",
|
|
73
|
+
),
|
|
74
|
+
defaultTemplate: lines(
|
|
75
|
+
"[agent-relay] Relay is available. Follow the Agent Relay rules in CLAUDE.md when present, or run agent-relay /guide. Reply through Relay only when a response is needed; do not send status-only follow-ups after the useful Relay response was already delivered.",
|
|
76
|
+
"",
|
|
77
|
+
"This Claude session is running with restricted read-only Relay permissions. Do not invoke Agent Relay skills. If you need to reply, use this Bash command shape:",
|
|
78
|
+
"",
|
|
79
|
+
"agent-relay /reply <messageId> \"<your reply>\"",
|
|
80
|
+
),
|
|
81
|
+
variables: [],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
slug: "relay.context.codex",
|
|
85
|
+
category: "runner",
|
|
86
|
+
title: "Codex Relay Context",
|
|
87
|
+
description: "Relay context note injected into Codex sessions.",
|
|
88
|
+
template: "[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_compact_and_resume, relay_recall, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id — relay fills it from your token; use relay_whoami only if you need to reason about yourself. relay_compact_and_resume is for clean-seam self-resume after a context advisory: pass workingState and optional ruledOut; Relay owns the objective envelope. relay_recall searches your own archived pre-compaction segments by keyword when a discarded detail is needed. relay_spawn_targets / relay_spawn_agent / relay_shutdown_agent only appear if your profile grants spawning (a live-children quota); when present, call relay_spawn_targets FIRST for the live host/provider/model matrix + your quota, then stand up long-living child agents and shut down your own — find them later with relay_find_agents spawnedBy:me. For replies, prefer relay_reply or relay_send_message; CLI fallback: agent-relay /reply <messageId> \"<your reply>\". If a delivered message says it was truncated, fetch the full body with: agent-relay get-message <messageId>. For command details, run: agent-relay /guide",
|
|
89
|
+
defaultTemplate: "[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_compact_and_resume, relay_recall, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id — relay fills it from your token; use relay_whoami only if you need to reason about yourself. relay_compact_and_resume is for clean-seam self-resume after a context advisory: pass workingState and optional ruledOut; Relay owns the objective envelope. relay_recall searches your own archived pre-compaction segments by keyword when a discarded detail is needed. relay_spawn_targets / relay_spawn_agent / relay_shutdown_agent only appear if your profile grants spawning (a live-children quota); when present, call relay_spawn_targets FIRST for the live host/provider/model matrix + your quota, then stand up long-living child agents and shut down your own — find them later with relay_find_agents spawnedBy:me. For replies, prefer relay_reply or relay_send_message; CLI fallback: agent-relay /reply <messageId> \"<your reply>\". If a delivered message says it was truncated, fetch the full body with: agent-relay get-message <messageId>. For command details, run: agent-relay /guide",
|
|
90
|
+
variables: [],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
slug: "workspace.lifecycle",
|
|
94
|
+
category: "runner",
|
|
95
|
+
title: "Runner Workspace Lifecycle Note",
|
|
96
|
+
description: "Lifecycle note injected by the runner for isolated workspaces.",
|
|
97
|
+
template: lines(
|
|
98
|
+
"[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} — NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.",
|
|
99
|
+
"Do NOT push this branch yourself — not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, run `agent-relay workspace ready` — Relay rebases onto the latest {{baseRef}}, lands your work, and pushes for you. If the installed `agent-relay` binary is stale and says the workspace command is unknown, run the repo-local fallback: `bun src/index.ts workspace ready`.",
|
|
100
|
+
"After `ready`, the status becomes `review_requested` — this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after `ready` and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix — expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself — Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step.",
|
|
101
|
+
),
|
|
102
|
+
defaultTemplate: lines(
|
|
103
|
+
"[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} — NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.",
|
|
104
|
+
"Do NOT push this branch yourself — not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, run `agent-relay workspace ready` — Relay rebases onto the latest {{baseRef}}, lands your work, and pushes for you. If the installed `agent-relay` binary is stale and says the workspace command is unknown, run the repo-local fallback: `bun src/index.ts workspace ready`.",
|
|
105
|
+
"After `ready`, the status becomes `review_requested` — this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after `ready` and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix — expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself — Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step.",
|
|
106
|
+
),
|
|
107
|
+
variables: ["branch", "baseRef"],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
slug: "workspace.deps.symlink",
|
|
111
|
+
category: "runner",
|
|
112
|
+
title: "Runner Workspace Deps Symlink Note",
|
|
113
|
+
description: "Symlinked node_modules caveat injected by the runner.",
|
|
114
|
+
template: "[agent-relay] Isolated workspace: this is a git worktree, and its node_modules are SYMLINKED from the main checkout — dependencies are already installed and ready to use. Do NOT run a clean dependency install (`bun install` / `npm install` / `pnpm install`): it writes through the symlink and mutates the main checkout's shared node_modules. Build caches written under node_modules are shared too. If typecheck/build fails on a missing module (a dependency added to the base AFTER this worktree was created), run `agent-relay workspace deps` — it re-provisions only the stale dirs with a real isolated install, safely, without touching the shared node_modules. If you genuinely need to change dependencies in isolation, ask the host to spawn with AGENT_RELAY_WORKSPACE_DEPS=install.",
|
|
115
|
+
defaultTemplate: "[agent-relay] Isolated workspace: this is a git worktree, and its node_modules are SYMLINKED from the main checkout — dependencies are already installed and ready to use. Do NOT run a clean dependency install (`bun install` / `npm install` / `pnpm install`): it writes through the symlink and mutates the main checkout's shared node_modules. Build caches written under node_modules are shared too. If typecheck/build fails on a missing module (a dependency added to the base AFTER this worktree was created), run `agent-relay workspace deps` — it re-provisions only the stale dirs with a real isolated install, safely, without touching the shared node_modules. If you genuinely need to change dependencies in isolation, ask the host to spawn with AGENT_RELAY_WORKSPACE_DEPS=install.",
|
|
116
|
+
variables: [],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
slug: "workspace.deps.none",
|
|
120
|
+
category: "runner",
|
|
121
|
+
title: "Runner Workspace Deps None Note",
|
|
122
|
+
description: "Missing dependency provisioning caveat injected by the runner.",
|
|
123
|
+
template: "[agent-relay] Isolated workspace: dependencies were not provisioned (AGENT_RELAY_WORKSPACE_DEPS=none). You may need to install node_modules before typecheck/test/build work.",
|
|
124
|
+
defaultTemplate: "[agent-relay] Isolated workspace: dependencies were not provisioned (AGENT_RELAY_WORKSPACE_DEPS=none). You may need to install node_modules before typecheck/test/build work.",
|
|
125
|
+
variables: [],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
slug: "workspace.symlinks",
|
|
129
|
+
category: "runner",
|
|
130
|
+
title: "Runner Workspace Symlink Note",
|
|
131
|
+
description: "Write-through symlink caveat injected by the runner.",
|
|
132
|
+
template: "[agent-relay] Isolated workspace: these untracked paths are SYMLINKED from the main checkout: {{linked}}. They resolve to the real files in main, so editing or deleting them writes THROUGH to main — treat them as read-only unless you intend to change main.",
|
|
133
|
+
defaultTemplate: "[agent-relay] Isolated workspace: these untracked paths are SYMLINKED from the main checkout: {{linked}}. They resolve to the real files in main, so editing or deleting them writes THROUGH to main — treat them as read-only unless you intend to change main.",
|
|
134
|
+
variables: ["linked"],
|
|
135
|
+
},
|
|
136
|
+
] as const satisfies readonly SeedTemplate[];
|
package/src/routes/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { getAgentIssueActivityRoute, getIssueActivityAgentsRoute, postIssueActiv
|
|
|
40
40
|
import { postIssueLifecycleReopenRoute } from "./issue-lifecycle";
|
|
41
41
|
import { deleteIssueRefRoute, getIssueRefRoute, listIssueRefsRoute, patchIssueRefRoute, postIssueRefRoute, resolveIssueRefRoute } from "./issue-refs";
|
|
42
42
|
import { getProjectEntriesRoute, getProjectEntryRoute, getProjectResolveRoute, getProjectRoute, getProjectsRoute } from "./projects";
|
|
43
|
+
import { getPromptTemplateRoute, getPromptTemplatesRoute, postPromptTemplateResetRoute, putPromptTemplateRoute } from "./prompt-templates";
|
|
43
44
|
import { getRecipeByName, getRecipeInstanceArtifacts, getRecipeInstanceById, getRecipeInstances, getRecipes, postRecipeInstanceArtifacts, postRecipeStart, postRecipeStop } from "./recipes";
|
|
44
45
|
import { getSettingRoute, getSettingsNamespaceRoute, getSettingsRoute, putSettingRoute, putSettingsDefaultRoute } from "./settings";
|
|
45
46
|
import { postNotificationPreviewRoute } from "./notifications";
|
|
@@ -256,6 +257,10 @@ const routes: Route[] = [
|
|
|
256
257
|
route("GET", "/api/insights/observations", getInsightsObservationsRoute),
|
|
257
258
|
route("POST", "/api/insights/observations", postInsightsObservationRoute),
|
|
258
259
|
route("POST", "/api/continuation-archives", postContinuationArchiveRoute),
|
|
260
|
+
route("GET", "/api/settings/prompts", getPromptTemplatesRoute),
|
|
261
|
+
route("GET", "/api/settings/prompts/:slug", getPromptTemplateRoute),
|
|
262
|
+
route("PUT", "/api/settings/prompts/:slug", putPromptTemplateRoute),
|
|
263
|
+
route("POST", "/api/settings/prompts/:slug/reset", postPromptTemplateResetRoute),
|
|
259
264
|
route("GET", "/api/settings", getSettingsRoute),
|
|
260
265
|
route("GET", "/api/settings/:namespace", getSettingsNamespaceRoute),
|
|
261
266
|
route("GET", "/api/settings/:namespace/:key", getSettingRoute),
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ValidationError } from "../db";
|
|
2
|
+
import { getComponentAuth, hasComponentScope } from "../security";
|
|
3
|
+
import { getPromptTemplate, listPromptTemplates, resetPromptTemplate, updatePromptTemplate } from "../services/prompt-templates";
|
|
4
|
+
import { cleanString } from "../validation";
|
|
5
|
+
import { authorizeRoute, error, json, parseBody, type Handler } from "./_shared";
|
|
6
|
+
|
|
7
|
+
function slugParam(params: Record<string, string | undefined>): string {
|
|
8
|
+
return cleanString(params.slug, "slug", { required: true, max: 200 })!;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function routeError(err: unknown): Response {
|
|
12
|
+
if (err instanceof ValidationError) return error(err.message, 400);
|
|
13
|
+
if (err instanceof Error && /not found/.test(err.message)) return error(err.message, 404);
|
|
14
|
+
return error(err instanceof Error ? err.message : "prompt template route failed");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function authorizePromptRead(req: Request): Response | null {
|
|
18
|
+
const auth = getComponentAuth(req);
|
|
19
|
+
if (auth && (hasComponentScope(auth, "system:admin") || hasComponentScope(auth, "agent:read"))) return null;
|
|
20
|
+
return authorizeRoute(req, { scope: "system:admin" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getPromptTemplatesRoute: Handler = (req) => {
|
|
24
|
+
const denied = authorizePromptRead(req);
|
|
25
|
+
if (denied) return denied;
|
|
26
|
+
const category = cleanString(new URL(req.url).searchParams.get("category"), "category", { max: 120 }) ?? undefined;
|
|
27
|
+
return json({ templates: listPromptTemplates(category) });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getPromptTemplateRoute: Handler = (req, params) => {
|
|
31
|
+
const denied = authorizePromptRead(req);
|
|
32
|
+
if (denied) return denied;
|
|
33
|
+
const template = getPromptTemplate(slugParam(params));
|
|
34
|
+
return template ? json(template) : error("prompt template not found", 404);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const putPromptTemplateRoute: Handler = async (req, params) => {
|
|
38
|
+
const denied = authorizeRoute(req, { scope: "system:admin" });
|
|
39
|
+
if (denied) return denied;
|
|
40
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
41
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
42
|
+
try {
|
|
43
|
+
const template = cleanString(parsed.body?.template, "template", { required: true, max: 200_000 })!;
|
|
44
|
+
const slug = slugParam(params);
|
|
45
|
+
return json(updatePromptTemplate(slug, template));
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return routeError(err);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const postPromptTemplateResetRoute: Handler = (req, params) => {
|
|
52
|
+
const denied = authorizeRoute(req, { scope: "system:admin" });
|
|
53
|
+
if (denied) return denied;
|
|
54
|
+
try {
|
|
55
|
+
const slug = slugParam(params);
|
|
56
|
+
return json(resetPromptTemplate(slug));
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return routeError(err);
|
|
59
|
+
}
|
|
60
|
+
};
|
package/src/routes/workspaces.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { RELAY_TOKEN_HEADER, isRecord } from "agent-relay-sdk";
|
|
3
3
|
import { TERMINAL_WORKSPACE_STATUSES, describeWorkspacePhase, landReceipt } from "../workspace-phase";
|
|
4
4
|
import { VALID_WORKSPACE_STATUSES, cleanMeta, cleanString, optionalEnum } from "../validation";
|
|
5
|
-
import { ValidationError, deleteWorkspace, getAgent, getWorkspace, listMergeLeases, listOrchestrators, listRepoStewards, listWorkspaces, updateWorkspaceStatus } from "../db";
|
|
5
|
+
import { ValidationError, deleteWorkspace, getAgent, getWorkspace, listMergeLeases, listOrchestrators, listRepoStewards, listWorkspaces, updateWorkspaceStatus, withResolvedWorkspaceProjects } from "../db";
|
|
6
6
|
import { WORKSPACE_ACTIONS, applyWorkspaceAction, buildWorkspaceCleanupCommand, waitForWorkspaceStatus } from "../workspace-actions";
|
|
7
7
|
import { auditEvent, authAuditMetadata, authorizeRoute, emitCommand, error, json, parseBody, type Handler } from "./_shared";
|
|
8
8
|
import { collectWorkspaceOrphans } from "../workspace-orphans";
|
|
@@ -21,8 +21,9 @@ export const getWorkspaces: Handler = (req) => {
|
|
|
21
21
|
const url = new URL(req.url);
|
|
22
22
|
const repoRoot = cleanString(url.searchParams.get("repoRoot") ?? undefined, "repoRoot", { max: 1000 });
|
|
23
23
|
const ownerAgentId = cleanString(url.searchParams.get("agentId") ?? undefined, "agentId", { max: 240 });
|
|
24
|
+
const projectId = cleanString(url.searchParams.get("projectId") ?? undefined, "projectId", { max: 120 });
|
|
24
25
|
const status = optionalEnum(url.searchParams.get("status") ?? undefined, "status", VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined;
|
|
25
|
-
return json(listWorkspaces({ repoRoot, ownerAgentId, status }).map(withOwnerOnline));
|
|
26
|
+
return json(withResolvedWorkspaceProjects(listWorkspaces({ repoRoot, ownerAgentId, status }), { projectId }).map(withOwnerOnline));
|
|
26
27
|
} catch (e) {
|
|
27
28
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
28
29
|
throw e;
|
|
@@ -32,7 +33,7 @@ export const getWorkspaces: Handler = (req) => {
|
|
|
32
33
|
export const getWorkspaceById: Handler = (_req, params) => {
|
|
33
34
|
const workspace = getWorkspace(params.id!);
|
|
34
35
|
if (!workspace) return error("workspace not found", 404);
|
|
35
|
-
return json(withOwnerOnline(workspace));
|
|
36
|
+
return json(withOwnerOnline(withResolvedWorkspaceProjects([workspace])[0]!));
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
export const getWorkspaceStewards: Handler = () => json({ stewards: listRepoStewards(), mergeLeases: listMergeLeases() });
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { getDb } from "./db";
|
|
15
15
|
import { emitRelayEvent } from "./events";
|
|
16
16
|
import { routeNotification } from "./notification-router";
|
|
17
|
+
import { renderTemplate } from "./prompt-resolver";
|
|
17
18
|
import { parseJson } from "./utils";
|
|
18
19
|
import { isRecord } from "agent-relay-sdk";
|
|
19
20
|
import type { RoutingPolicy } from "agent-relay-sdk";
|
|
@@ -43,7 +44,9 @@ export function notifyRoutingPolicyChanged(policy: RoutingPolicy, opts: { update
|
|
|
43
44
|
ttlMs: PUSH_TTL_MS,
|
|
44
45
|
message: {
|
|
45
46
|
subject: "Routing policy updated",
|
|
46
|
-
body:
|
|
47
|
+
body: renderTemplate("notify.routing.policy-changed", {
|
|
48
|
+
updatedBy: opts.updatedBy ? ` by ${opts.updatedBy}` : "",
|
|
49
|
+
}),
|
|
47
50
|
payload: { kind: "routing.policy.changed", policy: policy as unknown as Record<string, unknown>, updatedBy: opts.updatedBy ?? null },
|
|
48
51
|
replyExpected: false,
|
|
49
52
|
},
|
package/src/security.ts
CHANGED
|
@@ -202,6 +202,7 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
202
202
|
if (pathname === "/api/provider-quotas") return method === "GET" ? "agent:read" : "quota:write";
|
|
203
203
|
if (pathname === "/api/routing-policy") return method === "GET" ? "agent:read" : "settings:write:routing";
|
|
204
204
|
if (pathname === "/api/teams/config") return method === "GET" ? "teams:read" : "settings:write:teams";
|
|
205
|
+
if (pathname === "/api/settings/prompts" || pathname.startsWith("/api/settings/prompts/")) return method === "GET" ? "agent:read" : "system:admin";
|
|
205
206
|
if (pathname.startsWith("/api/teams")) return method === "GET" ? "teams:read" : "teams:write";
|
|
206
207
|
if (pathname.startsWith("/api/blackboard")) return method === "GET" ? "blackboard:read" : "blackboard:write";
|
|
207
208
|
if (pathname.startsWith("/api/issue-associations")) return method === "GET" ? "issue:read" : "issue:write";
|
|
@@ -297,6 +298,7 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
297
298
|
return method === "GET" ? "agent:read" : "settings:write:routing";
|
|
298
299
|
}
|
|
299
300
|
if (pathname === "/api/teams/config") return method === "GET" ? "teams:read" : "settings:write:teams";
|
|
301
|
+
if (pathname === "/api/settings/prompts" || pathname.startsWith("/api/settings/prompts/")) return method === "GET" ? "agent:read" : "system:admin";
|
|
300
302
|
if (pathname.startsWith("/api/messages") || pathname.startsWith("/api/inbox")) return method === "GET" ? "message:read" : "message:send";
|
|
301
303
|
if (pathname.startsWith("/api/agent-profiles")) return method === "GET" ? "agent:read" : "agent:write";
|
|
302
304
|
if (pathname.startsWith("/api/provisioning")) return method === "GET" ? "provisioning:read" : "provisioning:write";
|
|
@@ -326,6 +328,7 @@ const CONFIG_SETTINGS_NAMESPACES = new Set(["steward", "insights", "notification
|
|
|
326
328
|
|
|
327
329
|
function requiredSettingsScopeFor(method: string, pathname: string): string | null | undefined {
|
|
328
330
|
if (pathname === "/api/settings") return method === "GET" ? "settings:read" : null;
|
|
331
|
+
if (pathname === "/api/settings/prompts" || pathname.startsWith("/api/settings/prompts/")) return undefined;
|
|
329
332
|
const settingsMatch = pathname.match(/^\/api\/settings\/([^/]+)(?:\/[^/]+)?$/);
|
|
330
333
|
if (settingsMatch) return settingsScope(method, decodeURIComponent(settingsMatch[1]!));
|
|
331
334
|
if (pathname === "/api/steward-config") return settingsScope(method, "steward");
|
package/src/self-resume.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./db";
|
|
14
14
|
import { emitRelayEvent } from "./events";
|
|
15
15
|
import { routeNotification } from "./notification-router";
|
|
16
|
+
import { renderTemplate } from "./prompt-resolver";
|
|
16
17
|
import type { AgentCard, Command, ContinuationEnvelope, ContinuationLedgerEntry, SelfResumeMode } from "./types";
|
|
17
18
|
|
|
18
19
|
interface CompactAndResumeInput {
|
|
@@ -149,21 +150,13 @@ function renderContinuation(envelope: ContinuationEnvelope): string {
|
|
|
149
150
|
const ledger = envelope.ledger.length
|
|
150
151
|
? envelope.ledger.map((entry) => `- gen ${entry.gen} @ ${new Date(entry.at).toISOString()}: ${entry.ruledOut}${entry.why ? `; why: ${entry.why}` : ""}`).join("\n")
|
|
151
152
|
: "- none";
|
|
152
|
-
return
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
envelope.
|
|
157
|
-
"",
|
|
158
|
-
|
|
159
|
-
ledger,
|
|
160
|
-
"",
|
|
161
|
-
"Working state:",
|
|
162
|
-
envelope.workingState,
|
|
163
|
-
"",
|
|
164
|
-
"Archive refs:",
|
|
165
|
-
envelope.archiveRefs.length ? envelope.archiveRefs.join("\n") : "- none",
|
|
166
|
-
].join("\n");
|
|
153
|
+
return renderTemplate("continuation.envelope", {
|
|
154
|
+
generation: String(envelope.generation),
|
|
155
|
+
objective: envelope.objective,
|
|
156
|
+
ruledOut: ledger,
|
|
157
|
+
workingState: envelope.workingState,
|
|
158
|
+
archiveRefs: envelope.archiveRefs.length ? envelope.archiveRefs.join("\n") : "- none",
|
|
159
|
+
});
|
|
167
160
|
}
|
|
168
161
|
|
|
169
162
|
function assertResumeSupported(agent: AgentCard, mode: SelfResumeMode): void {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getTemplate, listTemplates, resetTemplate, upsertTemplate, type PromptTemplateRecord } from "../db";
|
|
2
|
+
|
|
3
|
+
export function listPromptTemplates(category?: string): PromptTemplateRecord[] {
|
|
4
|
+
return listTemplates(category);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getPromptTemplate(slug: string): PromptTemplateRecord | null {
|
|
8
|
+
return getTemplate(slug);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function updatePromptTemplate(slug: string, template: string): PromptTemplateRecord | null {
|
|
12
|
+
upsertTemplate(slug, template);
|
|
13
|
+
return getTemplate(slug);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resetPromptTemplate(slug: string): PromptTemplateRecord | null {
|
|
17
|
+
resetTemplate(slug);
|
|
18
|
+
return getTemplate(slug);
|
|
19
|
+
}
|
|
@@ -18,6 +18,7 @@ import { notifyAgentReady } from "../agent-lifecycle-events";
|
|
|
18
18
|
import { resolveSpawnLineage, resolveTeamLineage } from "../security";
|
|
19
19
|
import { flushQueuedDirectMessages, markManagedAgentRunning } from "./managed-running";
|
|
20
20
|
import { isAgentUnavailable } from "../db/agent-predicates";
|
|
21
|
+
import { recordSpawnPromptIssueActivity } from "./spawn-issue-activity";
|
|
21
22
|
import type { AuthContext } from "./auth-context";
|
|
22
23
|
import type { AgentCard, RegisterAgentInput } from "../types";
|
|
23
24
|
|
|
@@ -86,6 +87,7 @@ export function registerAgent(input: RegisterAgentInput, ctx: AuthContext): Agen
|
|
|
86
87
|
const promptMirror = seedSpawnPromptMirror(agent.id, spawnRequestId);
|
|
87
88
|
if (promptMirror?.created || promptMirror?.updated) emitNewMessage(promptMirror.message);
|
|
88
89
|
seedSpawnReplyObligation(agent.id, agent.spawnedBy, spawnRequestId);
|
|
90
|
+
recordSpawnPromptIssueActivity(agent, spawnRequestId);
|
|
89
91
|
createActivityEvent({
|
|
90
92
|
clientId: `server-agent-${agent.id}-registered`,
|
|
91
93
|
kind: "state",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { isRecord } from "agent-relay-sdk";
|
|
2
|
+
import { getDb, resolveProjectForCwd } from "../db";
|
|
3
|
+
import { resolveIssueRefFromString } from "../db/issue-refs";
|
|
4
|
+
import { defaultIssueRepo } from "../config";
|
|
5
|
+
import { parseJson } from "../utils";
|
|
6
|
+
import { recordAndEmitIssueActivity } from "./issue-activity";
|
|
7
|
+
import type { AgentCard, WorkspaceRecord } from "../types";
|
|
8
|
+
|
|
9
|
+
const BARE_ISSUE_REF_RE = /#([0-9]+)/g;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse spawn prompt for bare #NNN refs and record "edit" activity for the
|
|
13
|
+
* newly registered agent. Called once on first registration. Silently ignores
|
|
14
|
+
* prompts with no refs or refs that can't be resolved (no issueRepo configured).
|
|
15
|
+
* Uses a stable clientId so repeated registration never double-records.
|
|
16
|
+
*/
|
|
17
|
+
export function recordSpawnPromptIssueActivity(agent: AgentCard, spawnRequestId: string | undefined): void {
|
|
18
|
+
if (!spawnRequestId) return;
|
|
19
|
+
try {
|
|
20
|
+
const row = getDb().query(`
|
|
21
|
+
SELECT params FROM commands
|
|
22
|
+
WHERE type = 'agent.spawn' AND correlation_id = ?
|
|
23
|
+
ORDER BY created_at DESC
|
|
24
|
+
LIMIT 1
|
|
25
|
+
`).get(spawnRequestId) as { params: string } | undefined;
|
|
26
|
+
const params = row ? parseJson<unknown>(row.params, {}) : {};
|
|
27
|
+
if (!isRecord(params) || typeof params.prompt !== "string") return;
|
|
28
|
+
const prompt = params.prompt;
|
|
29
|
+
if (!prompt.trim()) return;
|
|
30
|
+
|
|
31
|
+
const cwd = typeof agent.meta?.cwd === "string" ? agent.meta.cwd : undefined;
|
|
32
|
+
const issueRepo = (cwd ? resolveProjectForCwd(cwd)?.issueRepo : undefined) ?? defaultIssueRepo();
|
|
33
|
+
|
|
34
|
+
const seen = new Set<string>();
|
|
35
|
+
for (const match of prompt.matchAll(BARE_ISSUE_REF_RE)) {
|
|
36
|
+
const num = match[1];
|
|
37
|
+
if (!num || seen.has(num)) continue;
|
|
38
|
+
seen.add(num);
|
|
39
|
+
try {
|
|
40
|
+
const issueRef = resolveIssueRefFromString({ ref: `#${num}`, defaultRepo: issueRepo });
|
|
41
|
+
recordAndEmitIssueActivity({
|
|
42
|
+
agentId: agent.id,
|
|
43
|
+
action: "edit",
|
|
44
|
+
issueRefId: issueRef.id,
|
|
45
|
+
clientId: `spawn-issue:${agent.id}:${issueRef.id}`,
|
|
46
|
+
title: "Issue referenced in spawn prompt",
|
|
47
|
+
metadata: { source: "spawn-prompt", spawnRequestId },
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
// Unresolvable ref (no issueRepo configured) — skip silently.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Must never break registration.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Inspect a newly created isolated workspace for issue refs from:
|
|
60
|
+
* 1. metadata.issueNumber (set by some spawn flows)
|
|
61
|
+
* 2. branch name (e.g. "agent/build-389-p1-..." → 389)
|
|
62
|
+
* Records "edit" activity for the owning agent. Uses the same stable clientId
|
|
63
|
+
* as recordSpawnPromptIssueActivity so prompt + branch refs for the same issue
|
|
64
|
+
* are deduplicated at the DB layer.
|
|
65
|
+
*/
|
|
66
|
+
export function recordWorkspaceIssueActivity(workspace: WorkspaceRecord): void {
|
|
67
|
+
const agentId = workspace.ownerAgentId;
|
|
68
|
+
if (!agentId || workspace.mode !== "isolated") return;
|
|
69
|
+
|
|
70
|
+
const seen = new Set<string>();
|
|
71
|
+
|
|
72
|
+
const issueNumber = workspace.metadata?.issueNumber;
|
|
73
|
+
if (typeof issueNumber === "number") {
|
|
74
|
+
seen.add(String(Math.trunc(issueNumber)));
|
|
75
|
+
} else if (typeof issueNumber === "string") {
|
|
76
|
+
const t = issueNumber.trim();
|
|
77
|
+
if (/^[0-9]+$/.test(t)) seen.add(t);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (workspace.branch) {
|
|
81
|
+
// Match the first 3+-digit number surrounded by separators in the branch name
|
|
82
|
+
// (e.g. agent/build-389-p1-foo → "389"). Skip 1–2-digit numbers to avoid
|
|
83
|
+
// partition or sequence suffixes (e.g. issue-activity-auto-01 → skip "01").
|
|
84
|
+
const match = workspace.branch.match(/(?:^|[-/])([0-9]{3,})(?:[-/]|$)/);
|
|
85
|
+
if (match?.[1]) seen.add(match[1]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (seen.size === 0) return;
|
|
89
|
+
|
|
90
|
+
const issueRepo = resolveProjectForCwd(workspace.repoRoot)?.issueRepo ?? defaultIssueRepo();
|
|
91
|
+
|
|
92
|
+
for (const num of seen) {
|
|
93
|
+
try {
|
|
94
|
+
const issueRef = resolveIssueRefFromString({ ref: `#${num}`, defaultRepo: issueRepo });
|
|
95
|
+
recordAndEmitIssueActivity({
|
|
96
|
+
agentId,
|
|
97
|
+
action: "edit",
|
|
98
|
+
issueRefId: issueRef.id,
|
|
99
|
+
clientId: `spawn-issue:${agentId}:${issueRef.id}`,
|
|
100
|
+
title: "Issue referenced in workspace",
|
|
101
|
+
metadata: { source: "workspace", workspaceId: workspace.id, branch: workspace.branch },
|
|
102
|
+
});
|
|
103
|
+
} catch {
|
|
104
|
+
// Unresolvable ref — skip silently.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/spawn-targets.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { countLiveSpawnedAgents, getAgent, getOrchestrator, listOrchestrators, listProviderQuotas, ValidationError } from "./db";
|
|
2
2
|
import { DEFAULT_SPAWN_PROFILE_NAME, listAgentProfiles } from "./config-store";
|
|
3
3
|
import { effectiveProviderCatalogList } from "./provider-catalog-store";
|
|
4
|
+
import { renderTemplate } from "./prompt-resolver";
|
|
4
5
|
import { getRoutingPolicy } from "./routing-policy-store";
|
|
5
6
|
import { isPathWithinBase } from "./utils";
|
|
6
7
|
import { McpNotFoundError } from "./mcp-errors";
|
|
@@ -436,17 +437,8 @@ function spawnAlternatives(provider?: SpawnProvider): string {
|
|
|
436
437
|
// stays lazy — fetched only when the agent actually decides to spawn. Returns "" for non-spawn agents.
|
|
437
438
|
export function spawnCapablePrimer(input: { canSpawn: boolean; quota?: number }): string {
|
|
438
439
|
if (!input.canSpawn) return "";
|
|
439
|
-
const
|
|
440
|
-
return
|
|
441
|
-
`You can spawn long-living child agents${quotaLabel}. Shut your own down with relay_shutdown_agent.`,
|
|
442
|
-
"- BEFORE spawning, call relay_spawn_targets: it returns the LIVE matrix of hosts × providers × models × per-model effort, advisory quota burn/penalty metadata, your remaining quota, and named profiles. Don't guess — that tool is the discovery path.",
|
|
443
|
-
"- Default to your OWN host. Only set orchestratorId to spawn onto another host when it runs a provider/model yours doesn't, or the work must run there.",
|
|
444
|
-
`- Prefer a named profile (profile: "<name>") over hand-assembling provider+model+effort+approvalMode. Omitting profile applies ${DEFAULT_SPAWN_PROFILE_NAME}; omitting model/effort uses the provider default. Lean to the cheapest provider/model/effort that fits; escalate only for complex work.`,
|
|
445
|
-
"- Set spawnRequestId for idempotency so a retried spawn doesn't double-create.",
|
|
446
|
-
"- After spawning, use relay_my_agents for your self-scoped child roster + quota and copy a returned spawnRequestId into relay_shutdown_agent when you need to reap.",
|
|
447
|
-
"- Spawn is fire-and-forget: pass waitForRegistrationMs:0 and keep working — you'll be PUSHED agent.ready when the child is genuinely ready (past onboarding), or agent.spawn_failed / agent.exited otherwise. Don't block-poll.",
|
|
448
|
-
"- Spawned children cannot themselves spawn (no grandchildren).",
|
|
449
|
-
].join("\n");
|
|
440
|
+
const quota = typeof input.quota === "number" && input.quota > 0 ? ` (live-children quota: ${input.quota})` : "";
|
|
441
|
+
return renderTemplate("spawn.primer", { quota, defaultProfile: DEFAULT_SPAWN_PROFILE_NAME });
|
|
450
442
|
}
|
|
451
443
|
|
|
452
444
|
// === Orchestrator selection ===
|
package/src/steward.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { stewardFallbackTarget } from "./config";
|
|
|
4
4
|
import { getSpawnPolicy, getStewardConfig, setConfig } from "./config-store";
|
|
5
5
|
import { listOrchestrators } from "./db";
|
|
6
6
|
import { getLifecycleManager } from "./lifecycle-manager";
|
|
7
|
+
import { renderTemplate } from "./prompt-resolver";
|
|
7
8
|
import type { Orchestrator, SpawnPolicy, StewardConfig } from "./types";
|
|
8
9
|
import { isPathWithinBase } from "./utils";
|
|
9
10
|
|
|
@@ -25,29 +26,10 @@ export function buildStewardPrompt(repoRoot: string): string {
|
|
|
25
26
|
// wake/task message via #640), not a static `system` dead-end. The fallback target below is
|
|
26
27
|
// used ONLY when a wake message names no coordinator.
|
|
27
28
|
const fallback = stewardFallbackTarget();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"Multiple agents work this repo in isolated git worktrees. The relay auto-merges any branch that rebases cleanly — including ones whose base moved on; it hands YOU only what it can't land deterministically: real merge conflicts, or an unknown/ambiguous merge state. Your job is to land that work with no human in the loop whenever you safely can.",
|
|
33
|
-
"",
|
|
34
|
-
"Each workspace belongs to a BRANCH AGENT (the owner who did the work) who in turn has a COORDINATOR (the agent that spawned it and is responsible for the merge landing). Your wake/task message names both — the `ownerAgentId` and the `coordinatorAgentId` — and `agent-relay steward inspect <id>` shows them. When you can't land work yourself, the coordinator is who you escalate to.",
|
|
35
|
-
"",
|
|
36
|
-
"Use the `agent-relay` CLI for EVERY relay interaction — it wraps the relay API and is the supported toolkit, and it already attaches your session token with the right scopes. Do NOT hand-curl the HTTP API (`/api/workspaces`, …): the CLI exists so you never have to. If you ever truly must call the API directly, authenticate with the `X-Agent-Relay-Token: <your session token>` header — NOT `Authorization: Bearer $AGENT_RELAY_TOKEN`; the admin env token is not present in your environment, so Bearer will 401.",
|
|
37
|
-
"Handle ONE workspace at a time — the relay's per-repo merge lease serializes you with any other merger.",
|
|
38
|
-
"",
|
|
39
|
-
"1. See the queue: `agent-relay steward queue` (workspaces in conflict / review_requested / merge_planned for this repo). The wake message(s) also name a `workspaceId`.",
|
|
40
|
-
"2. CLAIM it FIRST: `agent-relay workspace claim --id <id> --purpose steward`. This stops the deterministic auto-merge from racing you while you validate. The claim auto-expires, so a crash never blocks the repo.",
|
|
41
|
-
"3. Understand it: `agent-relay steward inspect <id>` — a diagnostics briefing (the branch agent owner + its coordinator, owner liveness, live git state, ahead/behind, recorded-vs-live branch mismatch, and a recommended action). `agent-relay steward checks <id>` suggests check commands from the changed files.",
|
|
42
|
-
"4. `cd` into the workspace's `worktreePath` and rebase the branch onto its `baseRef` (`git rebase <baseRef>`). Resolve conflicts faithfully — preserve the intent of EVERY side; never silently drop a change. If a conflict needs product judgment you can't make confidently, stop and escalate to the coordinator.",
|
|
43
|
-
"5. Run the repo's checks/tests (the suggested ones, plus the project's documented commands). Fix trivial breakage you caused while rebasing; do not paper over real failures.",
|
|
44
|
-
"6. If green, land it: `agent-relay workspace land --id <id> --strategy rebase-ff`. The relay rebases onto the latest base, fast-forwards, and pushes to origin under the lease. The land consumes your claim.",
|
|
45
|
-
"7. If you must stop without landing, RELEASE the claim so automation can resume: `agent-relay workspace release --id <id>`, then send a clear, specific summary to the workspace's COORDINATOR (the `coordinatorAgentId` from the wake message) — they own the work and can act on it. Reply to the wake message or message that agent id directly.",
|
|
46
|
-
"",
|
|
47
|
-
`Escalate (do NOT merge) when: checks fail and you can't fix them, the conflict resolution is genuinely ambiguous, or anything looks risky. ALWAYS escalate to the branch agent's coordinator (the \`coordinatorAgentId\` in the wake message). Only if the wake message names NO coordinator, escalate to ${fallbackTarget}. Never force-merge a red branch, never \`git push --force\` to a shared base, never discard committed work.`,
|
|
48
|
-
"",
|
|
49
|
-
"Be terminal-efficient and decisive. When the queue is empty, you're done — you'll be woken again when there's more.",
|
|
50
|
-
].join("\n");
|
|
29
|
+
return renderTemplate("steward.prompt", {
|
|
30
|
+
repoRoot,
|
|
31
|
+
escalationTarget: fallback ? `\`${fallback}\`` : "the human operator who configured this relay",
|
|
32
|
+
});
|
|
51
33
|
}
|
|
52
34
|
|
|
53
35
|
function desiredStewardPolicy(repoRoot: string, owner: Orchestrator, config: StewardConfig): SpawnPolicy {
|
|
@@ -31,6 +31,7 @@ import { emitCommandEvent } from "./command-events";
|
|
|
31
31
|
import { getLandingConfig, getStewardConfig } from "./config-store";
|
|
32
32
|
import { getLifecycleManager } from "./lifecycle-manager";
|
|
33
33
|
import { routeNotification } from "./notification-router";
|
|
34
|
+
import { renderTemplate } from "./prompt-resolver";
|
|
34
35
|
import { isAwaitingReviewerApproval } from "./reviewer-pipeline";
|
|
35
36
|
import { ensureRepoSteward } from "./steward";
|
|
36
37
|
import { requestWorkspaceMerge } from "./workspace-merge";
|
|
@@ -115,7 +116,17 @@ export function wakeRepoSteward(ws: WorkspaceRecord, reason: string): string | n
|
|
|
115
116
|
recipients: [`policy:${policyName}`],
|
|
116
117
|
message: {
|
|
117
118
|
subject: `Steward: ${ws.status} workspace needs attention`,
|
|
118
|
-
body:
|
|
119
|
+
body: renderTemplate("steward.wake", {
|
|
120
|
+
branch: ws.branch ?? ws.id,
|
|
121
|
+
wsId: ws.id,
|
|
122
|
+
repoRoot: ws.repoRoot,
|
|
123
|
+
status: ws.status,
|
|
124
|
+
reason,
|
|
125
|
+
ownerLine,
|
|
126
|
+
worktreePath: ws.worktreePath ?? ws.sourceCwd,
|
|
127
|
+
baseRef: ws.baseRef ?? "base",
|
|
128
|
+
coordinatorLine,
|
|
129
|
+
}),
|
|
119
130
|
payload: { kind: "workspace.steward-task", workspaceId: ws.id, repoRoot: ws.repoRoot, worktreePath: ws.worktreePath, branch: ws.branch, baseRef: ws.baseRef, status: ws.status, reason, ownerAgentId, coordinatorAgentId },
|
|
120
131
|
},
|
|
121
132
|
});
|
|
@@ -202,7 +213,13 @@ function escalateNoProgressMerge(ws: WorkspaceRecord): void {
|
|
|
202
213
|
recipients,
|
|
203
214
|
message: {
|
|
204
215
|
subject: "Workspace can't auto-land — needs attention",
|
|
205
|
-
body:
|
|
216
|
+
body: renderTemplate("steward.merge-stall", {
|
|
217
|
+
branch: `\`${ws.branch ?? ws.id}\``,
|
|
218
|
+
repoRoot: ws.repoRoot,
|
|
219
|
+
attempts: String(MAX_NO_PROGRESS_MERGE_ATTEMPTS),
|
|
220
|
+
mergeError,
|
|
221
|
+
baseRef: ws.baseRef ?? "base",
|
|
222
|
+
}),
|
|
206
223
|
payload: { kind: "workspace.auto-merge-stuck", workspaceId: ws.id, repoRoot: ws.repoRoot, branch: ws.branch, baseRef: ws.baseRef, attempts: MAX_NO_PROGRESS_MERGE_ATTEMPTS, reason: mergeError },
|
|
207
224
|
},
|
|
208
225
|
});
|