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.
Files changed (169) hide show
  1. package/docs/openapi.json +141 -1
  2. package/package.json +2 -2
  3. package/public/assets/MessageBubble-CHor4Sxp.js +4 -0
  4. package/public/assets/{MessageBubble-BpiTbbnp.js.map → MessageBubble-CHor4Sxp.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-k8Aa98g5.js → PlanGraphPanel-jHfkQBX6.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-k8Aa98g5.js.map → PlanGraphPanel-jHfkQBX6.js.map} +1 -1
  7. package/public/assets/{activity-CxKi5Erb.js → activity-DDXS6LsX.js} +2 -2
  8. package/public/assets/{activity-CxKi5Erb.js.map → activity-DDXS6LsX.js.map} +1 -1
  9. package/public/assets/{agent-profiles-DMZXdN6K.js → agent-profiles-Cn8lCTkg.js} +2 -2
  10. package/public/assets/{agent-profiles-DMZXdN6K.js.map → agent-profiles-Cn8lCTkg.js.map} +1 -1
  11. package/public/assets/agents-DBTdcFFz.js +2 -0
  12. package/public/assets/{agents-BrvpH3gn.js.map → agents-DBTdcFFz.js.map} +1 -1
  13. package/public/assets/{analytics-1fvh-9ok.js → analytics-VximqXQ9.js} +3 -3
  14. package/public/assets/{analytics-1fvh-9ok.js.map → analytics-VximqXQ9.js.map} +1 -1
  15. package/public/assets/{automation-BCgX5WOH.js → automation-L9wq4y5F.js} +2 -2
  16. package/public/assets/{automation-BCgX5WOH.js.map → automation-L9wq4y5F.js.map} +1 -1
  17. package/public/assets/{badge-BLar6FjP.js → badge-D8fABzMs.js} +2 -2
  18. package/public/assets/{badge-BLar6FjP.js.map → badge-D8fABzMs.js.map} +1 -1
  19. package/public/assets/{branch-state-badge-DYA7bU0i.js → branch-state-badge-CcLZ_u86.js} +2 -2
  20. package/public/assets/{branch-state-badge-DYA7bU0i.js.map → branch-state-badge-CcLZ_u86.js.map} +1 -1
  21. package/public/assets/{button-BrIB69qd.js → button-KASZcscA.js} +2 -2
  22. package/public/assets/{button-BrIB69qd.js.map → button-KASZcscA.js.map} +1 -1
  23. package/public/assets/card-Cds0MWME.js +2 -0
  24. package/public/assets/{card-lhnFnBoz.js.map → card-Cds0MWME.js.map} +1 -1
  25. package/public/assets/{channels-CK80oJYG.js → channels-CcSnLzCG.js} +2 -2
  26. package/public/assets/{channels-CK80oJYG.js.map → channels-CcSnLzCG.js.map} +1 -1
  27. package/public/assets/{chat-D_2PEz06.js → chat-BzMj_PO6.js} +2 -2
  28. package/public/assets/{chat-D_2PEz06.js.map → chat-BzMj_PO6.js.map} +1 -1
  29. package/public/assets/{connectors-ChGyht5m.js → connectors-DbPOlQhL.js} +2 -2
  30. package/public/assets/{connectors-ChGyht5m.js.map → connectors-DbPOlQhL.js.map} +1 -1
  31. package/public/assets/copy-button-d9DFNX9v.js +2 -0
  32. package/public/assets/{copy-button-Cd-LbSbg.js.map → copy-button-d9DFNX9v.js.map} +1 -1
  33. package/public/assets/{display-D1xB8NJZ.js → display-BWi8DQCp.js} +2 -2
  34. package/public/assets/display-BWi8DQCp.js.map +1 -0
  35. package/public/assets/{dist-BnbGIUmi.js → dist-BbUJ1Vht.js} +2 -2
  36. package/public/assets/{dist-BnbGIUmi.js.map → dist-BbUJ1Vht.js.map} +1 -1
  37. package/public/assets/{dist-Bp10H3oR.js → dist-BlZvUZZB.js} +2 -2
  38. package/public/assets/{dist-Bp10H3oR.js.map → dist-BlZvUZZB.js.map} +1 -1
  39. package/public/assets/{dist-DDUGmmoi.js → dist-BwwlmyBq.js} +2 -2
  40. package/public/assets/{dist-DDUGmmoi.js.map → dist-BwwlmyBq.js.map} +1 -1
  41. package/public/assets/dist-CBVZk9U8.js +2 -0
  42. package/public/assets/{dist-BEzpMqLy.js.map → dist-CBVZk9U8.js.map} +1 -1
  43. package/public/assets/{dist-B5VvlkhI.js → dist-CgqfdHSt.js} +2 -2
  44. package/public/assets/{dist-B5VvlkhI.js.map → dist-CgqfdHSt.js.map} +1 -1
  45. package/public/assets/dist-DC-TqTz2.js +2 -0
  46. package/public/assets/{dist-30weaXto.js.map → dist-DC-TqTz2.js.map} +1 -1
  47. package/public/assets/{dist-DZ97OosY.js → dist-DOAjFOS4.js} +2 -2
  48. package/public/assets/{dist-DZ97OosY.js.map → dist-DOAjFOS4.js.map} +1 -1
  49. package/public/assets/{dist-BjOBkHHY.js → dist-DTXKbk2K.js} +2 -2
  50. package/public/assets/{dist-BjOBkHHY.js.map → dist-DTXKbk2K.js.map} +1 -1
  51. package/public/assets/dist-DreO5AcZ.js +2 -0
  52. package/public/assets/{dist-Cgy0FmP-.js.map → dist-DreO5AcZ.js.map} +1 -1
  53. package/public/assets/{dist-CQF3Jiqg.js → dist-DybScJmm.js} +2 -2
  54. package/public/assets/{dist-CQF3Jiqg.js.map → dist-DybScJmm.js.map} +1 -1
  55. package/public/assets/{dist-DAl28ejX.js → dist-UhEAFdvZ.js} +2 -2
  56. package/public/assets/{dist-DAl28ejX.js.map → dist-UhEAFdvZ.js.map} +1 -1
  57. package/public/assets/{es2015-B8Z9p8oQ.js → es2015-DUjFyjDp.js} +2 -2
  58. package/public/assets/{es2015-B8Z9p8oQ.js.map → es2015-DUjFyjDp.js.map} +1 -1
  59. package/public/assets/{formatted-body-impl-BJ4WCF5J.js → formatted-body-impl-CFWHLkCY.js} +2 -2
  60. package/public/assets/{formatted-body-impl-BJ4WCF5J.js.map → formatted-body-impl-CFWHLkCY.js.map} +1 -1
  61. package/public/assets/index-BjjzI2LT.css +2 -0
  62. package/public/assets/{index-CN7QZlMb.js → index-aCfZQ7LY.js} +13 -13
  63. package/public/assets/index-aCfZQ7LY.js.map +1 -0
  64. package/public/assets/{input-B_fUHtLD.js → input-CtHfLn1i.js} +2 -2
  65. package/public/assets/{input-B_fUHtLD.js.map → input-CtHfLn1i.js.map} +1 -1
  66. package/public/assets/{insights-BCZRV0hX.js → insights-BuPFE0tf.js} +2 -2
  67. package/public/assets/{insights-BCZRV0hX.js.map → insights-BuPFE0tf.js.map} +1 -1
  68. package/public/assets/{integrations-C7UGs00U.js → integrations-hkSW8CzM.js} +2 -2
  69. package/public/assets/{integrations-C7UGs00U.js.map → integrations-hkSW8CzM.js.map} +1 -1
  70. package/public/assets/{lib-ng6WZYjE.js → lib-l-JkMVR9.js} +2 -2
  71. package/public/assets/{lib-ng6WZYjE.js.map → lib-l-JkMVR9.js.map} +1 -1
  72. package/public/assets/{lucide-react-BYZuAhQs.js → lucide-react-C4eXYCwx.js} +2 -2
  73. package/public/assets/{lucide-react-BYZuAhQs.js.map → lucide-react-C4eXYCwx.js.map} +1 -1
  74. package/public/assets/{maintenance-ChYno3Fm.js → maintenance-wYwJ7w6h.js} +2 -2
  75. package/public/assets/{maintenance-ChYno3Fm.js.map → maintenance-wYwJ7w6h.js.map} +1 -1
  76. package/public/assets/{managed-agents-DjxZQNIR.js → managed-agents-CKaYD93I.js} +4 -4
  77. package/public/assets/{managed-agents-DjxZQNIR.js.map → managed-agents-CKaYD93I.js.map} +1 -1
  78. package/public/assets/{markdown-preview-impl-DFa5HF1H.js → markdown-preview-impl-bf4eDQUM.js} +2 -2
  79. package/public/assets/{markdown-preview-impl-DFa5HF1H.js.map → markdown-preview-impl-bf4eDQUM.js.map} +1 -1
  80. package/public/assets/memory-DIneEza9.js +2 -0
  81. package/public/assets/{memory-CTw6kadT.js.map → memory-DIneEza9.js.map} +1 -1
  82. package/public/assets/{messages-Cjr816D_.js → messages-DP9Vv_wh.js} +2 -2
  83. package/public/assets/{messages-Cjr816D_.js.map → messages-DP9Vv_wh.js.map} +1 -1
  84. package/public/assets/{orchestrators-CdmFKA7K.js → orchestrators-CAnKjxdO.js} +2 -2
  85. package/public/assets/{orchestrators-CdmFKA7K.js.map → orchestrators-CAnKjxdO.js.map} +1 -1
  86. package/public/assets/{overview-DBOnjfpP.js → overview-B98U3C-y.js} +2 -2
  87. package/public/assets/{overview-DBOnjfpP.js.map → overview-B98U3C-y.js.map} +1 -1
  88. package/public/assets/{pairs-BkcdKdF7.js → pairs-DVeeuxhY.js} +2 -2
  89. package/public/assets/{pairs-BkcdKdF7.js.map → pairs-DVeeuxhY.js.map} +1 -1
  90. package/public/assets/{projects-ClKbDQB1.js → projects-vt63bwHN.js} +2 -2
  91. package/public/assets/{projects-ClKbDQB1.js.map → projects-vt63bwHN.js.map} +1 -1
  92. package/public/assets/prompt-settings-Df-0ZMJ5.js +2 -0
  93. package/public/assets/prompt-settings-Df-0ZMJ5.js.map +1 -0
  94. package/public/assets/{react-dom-Dbhs84ug.js → react-dom-BMF4hNzG.js} +2 -2
  95. package/public/assets/{react-dom-Dbhs84ug.js.map → react-dom-BMF4hNzG.js.map} +1 -1
  96. package/public/assets/{registry-Bsf3RtOO.js → registry-DlLZyW7_.js} +2 -2
  97. package/public/assets/{registry-Bsf3RtOO.js.map → registry-DlLZyW7_.js.map} +1 -1
  98. package/public/assets/{security-Iyu5bhJw.js → security-Bo4Ty3lz.js} +2 -2
  99. package/public/assets/{security-Iyu5bhJw.js.map → security-Bo4Ty3lz.js.map} +1 -1
  100. package/public/assets/{select-Bi6tNJDx.js → select-BguZ80M8.js} +2 -2
  101. package/public/assets/{select-Bi6tNJDx.js.map → select-BguZ80M8.js.map} +1 -1
  102. package/public/assets/settings-CpX9Xw2j.js +6 -0
  103. package/public/assets/{settings-CbHp2F9r.js.map → settings-CpX9Xw2j.js.map} +1 -1
  104. package/public/assets/{store-zeyDiSKE.js → store-DUfrGvOE.js} +3 -3
  105. package/public/assets/{store-zeyDiSKE.js.map → store-DUfrGvOE.js.map} +1 -1
  106. package/public/assets/{tasks-CPlf-5Tr.js → tasks-Cg1HXY9l.js} +2 -2
  107. package/public/assets/{tasks-CPlf-5Tr.js.map → tasks-Cg1HXY9l.js.map} +1 -1
  108. package/public/assets/{teams-D2uUNhlY.js → teams-BFcYunxB.js} +2 -2
  109. package/public/assets/{teams-D2uUNhlY.js.map → teams-BFcYunxB.js.map} +1 -1
  110. package/public/assets/{terminal-viewer-impl-uRzbrlXy.js → terminal-viewer-impl-CF2EBa2K.js} +3 -3
  111. package/public/assets/{terminal-viewer-impl-uRzbrlXy.js.map → terminal-viewer-impl-CF2EBa2K.js.map} +1 -1
  112. package/public/assets/types-DorDDvEb.js.map +1 -1
  113. package/public/assets/{use-keyboard-viewport-D8_NYDkh.js → use-keyboard-viewport-RYMALvu1.js} +2 -2
  114. package/public/assets/{use-keyboard-viewport-D8_NYDkh.js.map → use-keyboard-viewport-RYMALvu1.js.map} +1 -1
  115. package/public/assets/user-avatar-D1ffSClz.js +2 -0
  116. package/public/assets/{user-avatar-GFWMuAGK.js.map → user-avatar-D1ffSClz.js.map} +1 -1
  117. package/public/assets/{work-queue-B30SNmuq.js → work-queue-DV36cxjp.js} +2 -2
  118. package/public/assets/{work-queue-B30SNmuq.js.map → work-queue-DV36cxjp.js.map} +1 -1
  119. package/public/assets/workspaces-CIsQqQby.js +3 -0
  120. package/public/assets/workspaces-CIsQqQby.js.map +1 -0
  121. package/public/index.html +22 -22
  122. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  123. package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +114 -23
  124. package/runner/plugins/claude/monitors/relay-monitor.ts +2 -2
  125. package/runner/src/adapter.ts +4 -1
  126. package/src/agent-lifecycle-events.ts +21 -5
  127. package/src/automations/reconcile.ts +4 -1
  128. package/src/branch-landed.ts +22 -4
  129. package/src/config-store.ts +17 -21
  130. package/src/context-advisory.ts +10 -3
  131. package/src/db/index.ts +1 -0
  132. package/src/db/projects.ts +29 -0
  133. package/src/db/prompt-templates-schema.ts +20 -0
  134. package/src/db/prompt-templates.ts +91 -0
  135. package/src/db/schema.ts +5 -4
  136. package/src/db/tasks.ts +2 -1
  137. package/src/db/workspaces.ts +41 -1
  138. package/src/index.ts +3 -0
  139. package/src/project-instructions.ts +7 -14
  140. package/src/prompt-resolver.ts +530 -0
  141. package/src/prompt-template-defaults-runner.ts +136 -0
  142. package/src/routes/index.ts +5 -0
  143. package/src/routes/prompt-templates.ts +60 -0
  144. package/src/routes/workspaces.ts +4 -3
  145. package/src/routing-policy-push.ts +4 -1
  146. package/src/security.ts +3 -0
  147. package/src/self-resume.ts +8 -15
  148. package/src/services/prompt-templates.ts +19 -0
  149. package/src/services/register-agent.ts +2 -0
  150. package/src/services/spawn-issue-activity.ts +107 -0
  151. package/src/spawn-targets.ts +3 -11
  152. package/src/steward.ts +5 -23
  153. package/src/workspace-auto-merge.ts +19 -2
  154. package/src/workspace-phase.ts +15 -28
  155. package/public/assets/MessageBubble-BpiTbbnp.js +0 -4
  156. package/public/assets/agents-BrvpH3gn.js +0 -2
  157. package/public/assets/card-lhnFnBoz.js +0 -2
  158. package/public/assets/copy-button-Cd-LbSbg.js +0 -2
  159. package/public/assets/display-D1xB8NJZ.js.map +0 -1
  160. package/public/assets/dist-30weaXto.js +0 -2
  161. package/public/assets/dist-BEzpMqLy.js +0 -2
  162. package/public/assets/dist-Cgy0FmP-.js +0 -2
  163. package/public/assets/index-BMO_Sblt.css +0 -2
  164. package/public/assets/index-CN7QZlMb.js.map +0 -1
  165. package/public/assets/memory-CTw6kadT.js +0 -2
  166. package/public/assets/settings-CbHp2F9r.js +0 -6
  167. package/public/assets/user-avatar-GFWMuAGK.js +0 -2
  168. package/public/assets/workspaces-CSpmgbPT.js +0 -3
  169. package/public/assets/workspaces-CSpmgbPT.js.map +0 -1
@@ -847,13 +847,109 @@ function messageBodyMaxCharsFromEnv() {
847
847
  return process.env.AGENT_RELAY_MESSAGE_BODY_MAX_CHARS;
848
848
  }
849
849
 
850
- // runner/src/relay-instructions.ts
851
- var CLAUDE_RELAY_CONTEXT = `[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.`;
852
- var CLAUDE_READ_ONLY_RELAY_CONTEXT = `${CLAUDE_RELAY_CONTEXT}
850
+ // src/prompt-template-defaults-runner.ts
851
+ function lines(...parts) {
852
+ return parts.join(`
853
+ `);
854
+ }
855
+ var RUNNER_PROMPT_TEMPLATE_DEFAULTS = [
856
+ {
857
+ slug: "relay.manual.claude",
858
+ category: "runner",
859
+ title: "Claude Relay Manual",
860
+ description: "Bundled CLAUDE.md relay manual written into provisioned Claude homes.",
861
+ template: lines("# Agent Relay", "", "- Agent Relay messages may come from humans, channels, or other agents.", "- Read the delivered message body and do the requested work.", "- Reply through Relay only when the sender still needs an answer.", "- Prefer the inline MCP tools `relay_reply` or `relay_send_message` for the actual answer to an incoming Relay message.", "- Do not send `/message` with the useful answer and then a separate `/reply` that only says it was sent.", "- 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.", '- 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.', "- No reply is needed for pure info messages, passive acknowledgements, or reactions that do not ask for action.", "- NEVER reply to a notification-class message. The server marks these and renders a single `\u21AA Notification \u2014 no reply needed.` line instead of the reply reminder \u2014 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.", '- Use `agent-relay /react <messageId> <emoji>` instead of a text reply for lightweight acknowledgement, approval, thanks, or "good job" after a completed work update.', "- Good reaction uses: acknowledge praise with \uD83D\uDC4D or \u2764\uFE0F, mark a completed handoff as seen, approve a proposed next step, or acknowledge a passive FYI.", "- Do not use reactions when the user asked a question, gave a new task, reported a bug, or needs a textual result.", "- A thumbs-up reaction to your question means approval to proceed. A thumbs-up reaction to your completed status means no further action.", "- Use `agent-relay get-message <id>` when a delivered preview is truncated.", "- Use `agent-relay /status --json` to inspect your current Relay identity and `agent-relay /guide` for command details."),
862
+ defaultTemplate: lines("# Agent Relay", "", "- Agent Relay messages may come from humans, channels, or other agents.", "- Read the delivered message body and do the requested work.", "- Reply through Relay only when the sender still needs an answer.", "- Prefer the inline MCP tools `relay_reply` or `relay_send_message` for the actual answer to an incoming Relay message.", "- Do not send `/message` with the useful answer and then a separate `/reply` that only says it was sent.", "- 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.", '- 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.', "- No reply is needed for pure info messages, passive acknowledgements, or reactions that do not ask for action.", "- NEVER reply to a notification-class message. The server marks these and renders a single `\u21AA Notification \u2014 no reply needed.` line instead of the reply reminder \u2014 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.", '- Use `agent-relay /react <messageId> <emoji>` instead of a text reply for lightweight acknowledgement, approval, thanks, or "good job" after a completed work update.', "- Good reaction uses: acknowledge praise with \uD83D\uDC4D or \u2764\uFE0F, mark a completed handoff as seen, approve a proposed next step, or acknowledge a passive FYI.", "- Do not use reactions when the user asked a question, gave a new task, reported a bug, or needs a textual result.", "- A thumbs-up reaction to your question means approval to proceed. A thumbs-up reaction to your completed status means no further action.", "- Use `agent-relay get-message <id>` when a delivered preview is truncated.", "- Use `agent-relay /status --json` to inspect your current Relay identity and `agent-relay /guide` for command details."),
863
+ variables: []
864
+ },
865
+ {
866
+ slug: "relay.context.claude",
867
+ category: "runner",
868
+ title: "Claude Relay Context",
869
+ description: "One-line relay context note for Claude sessions.",
870
+ 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.",
871
+ 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.",
872
+ variables: []
873
+ },
874
+ {
875
+ slug: "relay.context.claude.readonly",
876
+ category: "runner",
877
+ title: "Claude Relay Context (Read Only)",
878
+ description: "Read-only relay context note for Claude sessions.",
879
+ template: lines("[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.", "", "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:", "", 'agent-relay /reply <messageId> "<your reply>"'),
880
+ defaultTemplate: lines("[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.", "", "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:", "", 'agent-relay /reply <messageId> "<your reply>"'),
881
+ variables: []
882
+ },
883
+ {
884
+ slug: "relay.context.codex",
885
+ category: "runner",
886
+ title: "Codex Relay Context",
887
+ description: "Relay context note injected into Codex sessions.",
888
+ 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 \u2014 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 \u2014 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",
889
+ 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 \u2014 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 \u2014 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",
890
+ variables: []
891
+ },
892
+ {
893
+ slug: "workspace.lifecycle",
894
+ category: "runner",
895
+ title: "Runner Workspace Lifecycle Note",
896
+ description: "Lifecycle note injected by the runner for isolated workspaces.",
897
+ template: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 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.", "Do NOT push this branch yourself \u2014 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` \u2014 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`.", "After `ready`, the status becomes `review_requested` \u2014 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 \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
898
+ defaultTemplate: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 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.", "Do NOT push this branch yourself \u2014 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` \u2014 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`.", "After `ready`, the status becomes `review_requested` \u2014 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 \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
899
+ variables: ["branch", "baseRef"]
900
+ },
901
+ {
902
+ slug: "workspace.deps.symlink",
903
+ category: "runner",
904
+ title: "Runner Workspace Deps Symlink Note",
905
+ description: "Symlinked node_modules caveat injected by the runner.",
906
+ template: "[agent-relay] Isolated workspace: this is a git worktree, and its node_modules are SYMLINKED from the main checkout \u2014 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` \u2014 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.",
907
+ defaultTemplate: "[agent-relay] Isolated workspace: this is a git worktree, and its node_modules are SYMLINKED from the main checkout \u2014 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` \u2014 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.",
908
+ variables: []
909
+ },
910
+ {
911
+ slug: "workspace.deps.none",
912
+ category: "runner",
913
+ title: "Runner Workspace Deps None Note",
914
+ description: "Missing dependency provisioning caveat injected by the runner.",
915
+ 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.",
916
+ 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.",
917
+ variables: []
918
+ },
919
+ {
920
+ slug: "workspace.symlinks",
921
+ category: "runner",
922
+ title: "Runner Workspace Symlink Note",
923
+ description: "Write-through symlink caveat injected by the runner.",
924
+ 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 \u2014 treat them as read-only unless you intend to change main.",
925
+ 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 \u2014 treat them as read-only unless you intend to change main.",
926
+ variables: ["linked"]
927
+ }
928
+ ];
853
929
 
854
- 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:
930
+ // runner/src/template-resolver.ts
931
+ var RUNNER_DEFAULTS = new Map(RUNNER_PROMPT_TEMPLATE_DEFAULTS.map((template) => [template.slug, template.defaultTemplate]));
932
+ var templateCache = new Map;
933
+ function renderValue(value) {
934
+ return value ?? "";
935
+ }
936
+ function applyVariables(template, vars) {
937
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => renderValue(vars[key]));
938
+ }
939
+ function renderRunnerTemplate(slug, vars = {}) {
940
+ const template = templateCache.get(slug) ?? RUNNER_DEFAULTS.get(slug);
941
+ if (template === undefined)
942
+ throw new Error(`unknown runner prompt template slug: ${slug}`);
943
+ return applyVariables(template, vars);
944
+ }
855
945
 
856
- agent-relay /reply <messageId> "<your reply>"`;
946
+ // runner/src/relay-instructions.ts
947
+ function claudeRelayContext() {
948
+ return renderRunnerTemplate("relay.context.claude");
949
+ }
950
+ function claudeReadOnlyRelayContext() {
951
+ return renderRunnerTemplate("relay.context.claude.readonly");
952
+ }
857
953
  function relayReplyActionText(messageId) {
858
954
  return `relay_reply (messageId: ${messageId}, body: "<your reply>"). CLI fallback: agent-relay /reply ${messageId} "<your reply>"`;
859
955
  }
@@ -865,9 +961,9 @@ function workspaceDepsNote(input) {
865
961
  return "";
866
962
  switch (input.depsMode) {
867
963
  case "symlink":
868
- return "[agent-relay] Isolated workspace: this is a git worktree, and its node_modules are SYMLINKED from the main checkout \u2014 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` \u2014 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.";
964
+ return renderRunnerTemplate("workspace.deps.symlink");
869
965
  case "none":
870
- return "[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.";
966
+ return renderRunnerTemplate("workspace.deps.none");
871
967
  default:
872
968
  return "";
873
969
  }
@@ -877,17 +973,12 @@ function workspaceLifecycleNote(input) {
877
973
  return "";
878
974
  const branch = input.branch ? `\`${input.branch}\`` : "an isolated agent branch";
879
975
  const base = input.baseRef ? `\`${input.baseRef}\`` : "the base branch";
880
- return [
881
- `[agent-relay] Isolated workspace: you are in a git worktree on branch ${branch}, based on ${base} \u2014 NOT the main checkout. Other agents may work in parallel and land to ${base}, so ${base} will move under you. That is expected; don't fight it.`,
882
- `Do NOT push this branch yourself \u2014 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\` \u2014 Relay rebases onto the latest ${base}, 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\`.`,
883
- `After \`ready\`, the status becomes \`review_requested\` \u2014 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 \u2014 expected, keep working there. Never \`cd\` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. \`agent-relay workspace status\` anytime shows your current state and the exact next step.`
884
- ].join(`
885
- `);
976
+ return renderRunnerTemplate("workspace.lifecycle", { branch, baseRef: base });
886
977
  }
887
978
  function workspaceSymlinksNote(linked) {
888
979
  if (!linked.length)
889
980
  return "";
890
- return `[agent-relay] Isolated workspace: these untracked paths are SYMLINKED from the main checkout: ${linked.join(", ")}. They resolve to the real files in main, so editing or deleting them writes THROUGH to main \u2014 treat them as read-only unless you intend to change main.`;
981
+ return renderRunnerTemplate("workspace.symlinks", { linked: linked.join(", ") });
891
982
  }
892
983
  function workspaceDepsNoteFromEnv(env = process.env) {
893
984
  const json = env.AGENT_RELAY_WORKSPACE_JSON;
@@ -948,7 +1039,7 @@ function providerAttachmentText(message) {
948
1039
  const refs = attachmentRefs(message);
949
1040
  if (!refs.length)
950
1041
  return;
951
- const lines = refs.flatMap((ref) => {
1042
+ const lines2 = refs.flatMap((ref) => {
952
1043
  const artifactId = typeof ref.artifactId === "string" ? ref.artifactId : "unknown-artifact";
953
1044
  const source = isRecord(ref.ref) ? [
954
1045
  typeof ref.ref.provider === "string" ? ref.ref.provider : undefined,
@@ -974,7 +1065,7 @@ function providerAttachmentText(message) {
974
1065
  ];
975
1066
  return [
976
1067
  "Attachments:",
977
- ...lines,
1068
+ ...lines2,
978
1069
  ...guidance
979
1070
  ].join(`
980
1071
  `);
@@ -984,18 +1075,18 @@ function providerAttachmentText(message) {
984
1075
  var REMINDER_EVERY_DELIVERIES = 5;
985
1076
  var TASK_CLAIM_INSTRUCTION = "Claim this task before working it, then update task status when finished.";
986
1077
  function stripRelayScaffolding(body) {
987
- const lines = body.split(`
1078
+ const lines2 = body.split(`
988
1079
  `);
989
1080
  const popTrailingBlanks = () => {
990
- while (lines.length && (lines.at(-1) ?? "").trim() === "")
991
- lines.pop();
1081
+ while (lines2.length && (lines2.at(-1) ?? "").trim() === "")
1082
+ lines2.pop();
992
1083
  };
993
1084
  popTrailingBlanks();
994
- if (lines.at(-1) === TASK_CLAIM_INSTRUCTION) {
995
- lines.pop();
1085
+ if (lines2.at(-1) === TASK_CLAIM_INSTRUCTION) {
1086
+ lines2.pop();
996
1087
  popTrailingBlanks();
997
1088
  }
998
- return lines.join(`
1089
+ return lines2.join(`
999
1090
  `);
1000
1091
  }
1001
1092
  function isMemoryInjection(message) {
@@ -1100,7 +1191,7 @@ ws.onmessage = (event) => {
1100
1191
  const messages2 = payload.messages;
1101
1192
  deliveryCount += 1;
1102
1193
  if (firstDelivery) {
1103
- console.log(process.env.AGENT_RELAY_APPROVAL === "read-only" ? CLAUDE_READ_ONLY_RELAY_CONTEXT : CLAUDE_RELAY_CONTEXT);
1194
+ console.log(process.env.AGENT_RELAY_APPROVAL === "read-only" ? claudeReadOnlyRelayContext() : claudeRelayContext());
1104
1195
  const wsNote = workspaceDepsNoteFromEnv();
1105
1196
  if (wsNote) {
1106
1197
  console.log("");
@@ -1,6 +1,6 @@
1
1
  import type { Message } from "agent-relay-sdk";
2
2
  import { claudeProviderMessageText } from "../../../src/adapters/claude-delivery";
3
- import { CLAUDE_READ_ONLY_RELAY_CONTEXT, CLAUDE_RELAY_CONTEXT, workspaceDepsNoteFromEnv } from "../../../src/relay-instructions";
3
+ import { claudeReadOnlyRelayContext, claudeRelayContext, workspaceDepsNoteFromEnv } from "../../../src/relay-instructions";
4
4
 
5
5
  const port = process.env.AGENT_RELAY_RUNNER_PORT;
6
6
  if (!port) process.exit(0);
@@ -15,7 +15,7 @@ ws.onmessage = (event) => {
15
15
  const messages = payload.messages as Message[];
16
16
  deliveryCount += 1;
17
17
  if (firstDelivery) {
18
- console.log(process.env.AGENT_RELAY_APPROVAL === "read-only" ? CLAUDE_READ_ONLY_RELAY_CONTEXT : CLAUDE_RELAY_CONTEXT);
18
+ console.log(process.env.AGENT_RELAY_APPROVAL === "read-only" ? claudeReadOnlyRelayContext() : claudeRelayContext());
19
19
  const wsNote = workspaceDepsNoteFromEnv();
20
20
  if (wsNote) {
21
21
  console.log("");
@@ -4,6 +4,7 @@ import type { LivenessInputs } from "./liveness";
4
4
  import type { SessionEvent } from "./session-insights";
5
5
  import { messageBodyMaxCharsFromEnv } from "./config";
6
6
  import { relayReplyReminderText } from "./relay-instructions";
7
+ import { renderRunnerTemplate } from "./template-resolver";
7
8
 
8
9
  export type SemanticStatus = "idle" | "busy" | "offline" | "error";
9
10
  type ProviderWorkKind = "provider-turn" | "subagent" | "background-script";
@@ -226,7 +227,9 @@ export function profileIsVanillaBase(config: Pick<RunnerSpawnConfig, "agentProfi
226
227
  return config.agentProfile?.base === "vanilla";
227
228
  }
228
229
 
229
- export const RELAY_CONTEXT = `[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`;
230
+ export function relayContext(): string {
231
+ return renderRunnerTemplate("relay.context.codex");
232
+ }
230
233
 
231
234
  // #306 — deliver the FULL message body by default. Only a pathological body beyond this
232
235
  // high cap truncates (with a get-message hint) so it can't nuke an agent's context; the 99%
@@ -2,6 +2,7 @@ import { emitRelayEvent } from "./events";
2
2
  import { routeNotification } from "./notification-router";
3
3
  import { getAgent, markAgentReadyNotified, recordCrashReport, recordTerminalEvent } from "./db";
4
4
  import { crashReportFor } from "./agent-death-diagnosis";
5
+ import { renderTemplate } from "./prompt-resolver";
5
6
  import type { AgentCard, AgentDeathDiagnosis } from "./types";
6
7
 
7
8
  /** One-line cause tag appended to a death notification body, e.g. " · why: Clean exit (exit code 0)". */
@@ -122,7 +123,11 @@ export function notifyAgentReady(childId: string): void {
122
123
  recipients: [parent],
123
124
  message: {
124
125
  subject: "Spawn recovered",
125
- body: `♻️ Correction: your earlier spawn failure${spawnRequestId ? ` for \`${spawnRequestId}\`` : ""} was a false alarm — ${describe(child)} actually registered and is ready and idle. Disregard the failure and do NOT retry; send it work with relay_send_message to \`${childId}\`.`,
126
+ body: renderTemplate("notify.spawn.recovered", {
127
+ spawnRequestId: spawnRequestId ? ` for \`${spawnRequestId}\`` : "",
128
+ label: describe(child),
129
+ childId,
130
+ }),
126
131
  payload: { kind: "agent.spawn_recovered", agentId: childId, parent, provider: providerOf(child), spawnRequestId, supersedes: "agent.spawn_failed" },
127
132
  replyExpected: false,
128
133
  },
@@ -135,7 +140,7 @@ export function notifyAgentReady(childId: string): void {
135
140
  recipients: [parent],
136
141
  message: {
137
142
  subject: "Spawned agent ready",
138
- body: `✅ Your spawned agent ${describe(child)} is ready and idle — send it work with relay_send_message to \`${childId}\`.`,
143
+ body: renderTemplate("notify.agent.ready", { label: describe(child), childId }),
139
144
  payload: { kind: "agent.ready", agentId: childId, parent, provider: providerOf(child), spawnRequestId },
140
145
  replyExpected: false,
141
146
  },
@@ -178,7 +183,11 @@ export function notifyAgentOffline(childId: string, reason: string, diagnosis?:
178
183
  recipients: [parent],
179
184
  message: {
180
185
  subject: "Spawned agent exited",
181
- body: `⏹ Your spawned agent ${describe(child)} exited — ${reason}${diagnosisSuffix(diagnosis)}.`,
186
+ body: renderTemplate("notify.agent.exited", {
187
+ label: describe(child),
188
+ reason,
189
+ diagnosis: diagnosisSuffix(diagnosis),
190
+ }),
182
191
  payload: { kind: "agent.exited", agentId: childId, parent, reason, ...(diagnosis ? { diagnosis } : {}) },
183
192
  replyExpected: false,
184
193
  },
@@ -201,7 +210,11 @@ export function notifyAgentOffline(childId: string, reason: string, diagnosis?:
201
210
  recipients: [parent],
202
211
  message: {
203
212
  subject: "Spawn failed",
204
- body: `❌ Your spawned agent ${describe(child)} went offline before ever becoming ready — ${reason}${diagnosisSuffix(diagnosis)}. Likely a crash-loop, onboarding gate, or bad launch; call relay_spawn_targets to re-check provider availability before retrying.`,
213
+ body: renderTemplate("notify.agent.never-ready", {
214
+ label: describe(child),
215
+ reason,
216
+ diagnosis: diagnosisSuffix(diagnosis),
217
+ }),
205
218
  payload: { kind: "agent.spawn_failed", agentId: childId, parent, reason, ...(diagnosis ? { diagnosis } : {}) },
206
219
  replyExpected: false,
207
220
  },
@@ -231,7 +244,10 @@ export function notifyAgentSpawnFailed(input: { parent: string; spawnRequestId?:
231
244
  recipients: [input.parent],
232
245
  message: {
233
246
  subject: "Spawn failed",
234
- body: `❌ Your spawn request${input.spawnRequestId ? ` \`${input.spawnRequestId}\`` : ""} failed — ${input.reason}. Call relay_spawn_targets to check live provider/host availability before retrying.`,
247
+ body: renderTemplate("notify.spawn.failed", {
248
+ spawnRequestId: input.spawnRequestId ? ` \`${input.spawnRequestId}\`` : "",
249
+ reason: input.reason,
250
+ }),
235
251
  payload: { kind: "agent.spawn_failed", parent: input.parent, spawnRequestId: input.spawnRequestId, provider: input.provider, reason: input.reason },
236
252
  replyExpected: false,
237
253
  },
@@ -1,5 +1,6 @@
1
1
  import { createCommand } from "../commands-db";
2
2
  import { getAgent, getTask, listOrchestrators, sendMessage, updateTaskStatus } from "../db";
3
+ import { renderTemplate } from "../prompt-resolver";
3
4
  import { CLOSED_TASK_STATUS } from "./constants";
4
5
  import {
5
6
  ensureAutomationTables,
@@ -132,7 +133,9 @@ function maybeWarnRuntimeBudget(run: AutomationRun, task: Task, now: number): Me
132
133
  to: target,
133
134
  kind: "control",
134
135
  subject: "Automation runtime budget warning",
135
- body: budget.warningMessage ?? `Runtime budget warning: this automation run will be shut down in about ${remainingMinutes} minute${remainingMinutes === 1 ? "" : "s"}. Finish current work, commit and push anything worth keeping, then stop cleanly before the deadline.`,
136
+ body: budget.warningMessage ?? renderTemplate("notify.automation.budget-warning", {
137
+ remainingMinutes: `${remainingMinutes} minute${remainingMinutes === 1 ? "" : "s"}`,
138
+ }),
136
139
  payload: {
137
140
  automationId: run.automationId,
138
141
  automationRunId: run.id,
@@ -3,6 +3,7 @@ import { emitRelayEvent } from "./events";
3
3
  import { routeNotification, INFORMATIONAL_TTL_MS } from "./notification-router";
4
4
  import { registerDefaultAudienceRules, audiencePredicates } from "./notification-rules";
5
5
  import { createActivityEvent, getAgent, updateWorkspaceStatus } from "./db";
6
+ import { renderTemplate } from "./prompt-resolver";
6
7
  import { handleLandedIssueLifecycle } from "./services/issue-lifecycle";
7
8
  import type { WorkspaceMergePreview, WorkspaceRecord } from "./types";
8
9
 
@@ -170,7 +171,13 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
170
171
  ttlMs: null,
171
172
  message: {
172
173
  subject: "Your branch landed",
173
- body: `✅ ${branchLabel} landed on \`${base}\`${shaLabel}${subjectLabel}.${publishLabel}${continueLabel}`,
174
+ body: renderTemplate("notify.land.success", {
175
+ branch: branchLabel,
176
+ base,
177
+ sha: `${shaLabel}${subjectLabel}`,
178
+ publishLabel,
179
+ continueLabel,
180
+ }),
174
181
  payload: { ...payload, notification: "landed-success" },
175
182
  replyExpected: false,
176
183
  },
@@ -241,7 +248,7 @@ export function notifyBranchLandFailed(input: {
241
248
  ttlMs: null,
242
249
  message: {
243
250
  subject: "Workspace land failed",
244
- body: `❌ ${branchLabel} did not land on \`${base}\`: ${reason}. You were woken by \`landed-failure\`; inspect workspace status before doing more work. Do NOT push, rebase, merge, or touch the main checkout yourself.`,
251
+ body: renderTemplate("notify.land.failure", { branch: branchLabel, base, reason }),
245
252
  payload,
246
253
  replyExpected: false,
247
254
  },
@@ -300,7 +307,12 @@ export function notifyStewardHandoff(input: {
300
307
  ttlMs: null,
301
308
  message: {
302
309
  subject: "Your merge is now with a steward",
303
- body: `🛟 ${branchLabel} couldn't be auto-merged to \`${base}\` and is now being handled by steward agent \`${input.stewardId}\`${reasonLabel}. The steward will rebase/resolve and land it. Do NOT push, rebase, merge, or touch the main checkout yourself; stay idle until you're woken with the result.`,
310
+ body: renderTemplate("notify.land.steward-taken", {
311
+ branch: branchLabel,
312
+ base,
313
+ stewardId: input.stewardId,
314
+ reasonLabel,
315
+ }),
304
316
  payload: { ...payload, notification: "steward-handoff" },
305
317
  replyExpected: false,
306
318
  },
@@ -318,7 +330,13 @@ export function notifyStewardHandoff(input: {
318
330
  ttlMs: null,
319
331
  message: {
320
332
  subject: "A branch agent's merge is now with a steward",
321
- body: `🛟 Your branch agent \`${owner ?? "?"}\`'s merge (${branchLabel} → \`${base}\`) couldn't be auto-merged and is now with steward agent \`${input.stewardId}\`${reasonLabel}. The steward will land it or escalate to you if it can't.`,
333
+ body: renderTemplate("notify.land.steward-taken.coordinator", {
334
+ owner: owner ?? "?",
335
+ branch: branchLabel,
336
+ base,
337
+ stewardId: input.stewardId,
338
+ reasonLabel,
339
+ }),
322
340
  payload: { ...payload, notification: "steward-handoff", audience: "coordinator" },
323
341
  replyExpected: false,
324
342
  },
@@ -11,6 +11,7 @@ import {
11
11
  import { DEFAULT_SELF_RESUME_CONFIG, validateSelfResumeConfig } from "./self-resume-config";
12
12
  import { TEAM_TEMPLATE_NAMESPACE, validateTeamTemplate } from "./team-template-config";
13
13
  import { PROJECT_INSTRUCTION_POLICY_NAMES } from "./project-instructions";
14
+ import { renderTemplate } from "./prompt-resolver";
14
15
  import { PROJECT_MCP_POLICY_NAMES } from "./project-mcp";
15
16
  import { PROJECT_SETTINGS_POLICY_NAMES } from "./project-settings";
16
17
  import { PROJECT_SKILLS_POLICY_NAMES } from "./project-skills";
@@ -60,28 +61,23 @@ const VALID_PERMISSION_MODES = ["open", "guarded", "read-only"] as const;
60
61
  const VALID_POLICY_MODES = ["always-on", "on-demand"] as const;
61
62
  const VALID_MANAGED_STATUSES = ["stopped", "starting", "running", "stopping", "backoff", "failed"] as const;
62
63
  export const DEFAULT_SPAWN_PROFILE_NAME = "default-relay";
63
- const COORDINATION_CHARTER = "Coordination charter: stay at helicopter altitude; delegate implementation to spawned workers; write briefs, maintain the blackboard and plan DAG; coordinate, review, and release; do not drift into mechanical edits when a worker can do them; protect your context for coordination.";
64
- const OPS_ASSISTANT_BRIEF = [
65
- "Ops-assistant standing brief: execute mechanical operations on your coordinator's behalf: test suites, release commands such as `bun run release`, smoke and verification probes, gates, and host/version/status checks.",
66
- "When running a self-verifying long script, especially `bun run release` (gates -> CI-watch -> deploy -> host-verify, then one final result), redirect its output to a file rather than letting it stream into your context: `bun run release > /tmp/ar-release.log 2>&1`. Wait for it to exit, then read only the final result with `tail /tmp/ar-release.log`. Never run it with inline streaming output. Do not poll, monitor, tail logs, run parallel status/version checks, or otherwise babysit it while it runs. Trust the script; act only on its final result. This generalizes to any long-running command whose intermediate output you do not need.",
67
- "Hold and apply session-local ops conventions the coordinator gives you. Report crisp outcomes only: command/gate, pass/fail, commit/version/host evidence, and the next blocker when one exists. Do not dump logs unless asked.",
68
- "Do not make strategy, design, scope, or release-timing calls. Escalate those decisions to your spawn-parent. Report operational results to your spawn-parent by default and keep their context clean.",
69
- ].join("\n");
70
64
  const BUILT_IN_AGENT_PROFILE_NAMES = new Set([DEFAULT_SPAWN_PROFILE_NAME, "minimal", "isolated-research", "vanilla", "lean-worker", "coordinator", "ops-assistant", "worker", "reviewer"]);
71
65
  // Tool group set for worker-style profiles (#551): excludes spawn/coordinator-only surface.
72
66
  const WORKER_TOOL_GROUPS = ["core", "workspace", "memory", "artifacts", "blackboard", "subscriptions"];
73
67
 
74
- const BUILT_IN_AGENT_PROFILES: AgentProfile[] = [
75
- agentProfileDefaults({ name: DEFAULT_SPAWN_PROFILE_NAME, icon: "🤖", description: "Relay-aware spawned agent with bundled Relay context, skills, plugins, and status integration.", base: "host", mcp: { mode: "none" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
76
- agentProfileDefaults({ name: "minimal", icon: "▫️", description: "Repo-focused spawned agent with Relay affordances disabled and host/global extras avoided where supported.", base: "minimal" }),
77
- agentProfileDefaults({ name: "isolated-research", icon: "🔬", description: "Strict profile for autonomous research runs: repo context only, no Relay prompt surface, no global extras where supported.", base: "isolated" }),
78
- agentProfileDefaults({ name: "vanilla", icon: "🫥", description: "Zero host bleed (#552): isolated provider home with host plugins, skills, hooks, MCP, and the CLAUDE.md/AGENTS.md cascade (repo + global) all suppressed at launch. Relay provisions any surface explicitly per profile.", base: "vanilla" }),
79
- agentProfileDefaults({ name: "lean-worker", icon: "🪶", description: "Lean worker profile (#570): vanilla host-dependency drop with only Relay context/MCP restored and Project knowledge injected from the spawn cwd.", base: "vanilla", relay: { context: true, skills: false, plugins: false, statusLine: false, mcp: true, tools: { groups: WORKER_TOOL_GROUPS } }, projectInstructions: { policy: "all" } }),
80
- agentProfileDefaults({ name: "coordinator", icon: "🧭", description: "Relay-aware coordination lead for spawning worker teams, maintaining shared state, and driving review and release.", base: "host", instructions: { append: [COORDINATION_CHARTER], repoInstructions: "allow", globalInstructions: "allow" }, maxSpawnedAgents: 8 }),
81
- agentProfileDefaults({ name: "ops-assistant", icon: "🛠️", description: "Relay-aware ops lieutenant for a coordinator: runs tests, releases, smoke probes, gates, and host checks, then reports crisp outcomes back up.", provider: "codex", base: "host", instructions: { append: [OPS_ASSISTANT_BRIEF], repoInstructions: "allow", globalInstructions: "allow" }, permissions: { mode: "open", filesystem: "host" } }),
82
- agentProfileDefaults({ name: "worker", icon: "🔨", description: "Relay-aware focused implementer with open approval posture and no delegated spawn capability.", base: "host", mcp: { mode: "none" }, permissions: { mode: "open", filesystem: "host" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
83
- agentProfileDefaults({ name: "reviewer", icon: "🔍", description: "Relay-aware read-only reviewer for audit and review passes without file writes.", base: "host", mcp: { mode: "none" }, permissions: { mode: "read-only", filesystem: "host" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
84
- ].map((profile) => ({ ...profile, builtIn: true }));
68
+ function builtInAgentProfiles(): AgentProfile[] {
69
+ return [
70
+ agentProfileDefaults({ name: DEFAULT_SPAWN_PROFILE_NAME, icon: "🤖", description: "Relay-aware spawned agent with bundled Relay context, skills, plugins, and status integration.", base: "host", mcp: { mode: "none" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
71
+ agentProfileDefaults({ name: "minimal", icon: "▫️", description: "Repo-focused spawned agent with Relay affordances disabled and host/global extras avoided where supported.", base: "minimal" }),
72
+ agentProfileDefaults({ name: "isolated-research", icon: "🔬", description: "Strict profile for autonomous research runs: repo context only, no Relay prompt surface, no global extras where supported.", base: "isolated" }),
73
+ agentProfileDefaults({ name: "vanilla", icon: "🫥", description: "Zero host bleed (#552): isolated provider home with host plugins, skills, hooks, MCP, and the CLAUDE.md/AGENTS.md cascade (repo + global) all suppressed at launch. Relay provisions any surface explicitly per profile.", base: "vanilla" }),
74
+ agentProfileDefaults({ name: "lean-worker", icon: "🪶", description: "Lean worker profile (#570): vanilla host-dependency drop with only Relay context/MCP restored and Project knowledge injected from the spawn cwd.", base: "vanilla", relay: { context: true, skills: false, plugins: false, statusLine: false, mcp: true, tools: { groups: WORKER_TOOL_GROUPS } }, projectInstructions: { policy: "all" } }),
75
+ agentProfileDefaults({ name: "coordinator", icon: "🧭", description: "Relay-aware coordination lead for spawning worker teams, maintaining shared state, and driving review and release.", base: "host", instructions: { append: [renderTemplate("profile.coordinator.charter")], repoInstructions: "allow", globalInstructions: "allow" }, maxSpawnedAgents: 8 }),
76
+ agentProfileDefaults({ name: "ops-assistant", icon: "🛠️", description: "Relay-aware ops lieutenant for a coordinator: runs tests, releases, smoke probes, gates, and host checks, then reports crisp outcomes back up.", provider: "codex", base: "host", instructions: { append: [renderTemplate("profile.ops-assistant.brief")], repoInstructions: "allow", globalInstructions: "allow" }, permissions: { mode: "open", filesystem: "host" } }),
77
+ agentProfileDefaults({ name: "worker", icon: "🔨", description: "Relay-aware focused implementer with open approval posture and no delegated spawn capability.", base: "host", instructions: { system: renderTemplate("profile.worker.system"), append: [], repoInstructions: "allow", globalInstructions: "allow" }, mcp: { mode: "none" }, permissions: { mode: "open", filesystem: "host" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
78
+ agentProfileDefaults({ name: "reviewer", icon: "🔍", description: "Relay-aware read-only reviewer for audit and review passes without file writes.", base: "host", mcp: { mode: "none" }, permissions: { mode: "read-only", filesystem: "host" }, relay: { tools: { groups: WORKER_TOOL_GROUPS } } }),
79
+ ].map((profile) => ({ ...profile, builtIn: true }));
80
+ }
85
81
 
86
82
  interface ConfigRow {
87
83
  namespace: string;
@@ -455,7 +451,7 @@ function agentProfileOverrideDiff(base: unknown, merged: unknown): unknown {
455
451
  }
456
452
 
457
453
  function validateAgentProfileOverride(key: string, value: unknown): Partial<AgentProfile> {
458
- const builtIn = BUILT_IN_AGENT_PROFILES.find((profile) => profile.name === key);
454
+ const builtIn = builtInAgentProfiles().find((profile) => profile.name === key);
459
455
  if (!builtIn) throw new ValidationError("agent profile override target must be a built-in profile");
460
456
  if (!isRecord(value)) throw new ValidationError("agent-profile override value must be an object");
461
457
  const merged = mergeProfileOverride(builtIn, { ...value, name: key, builtIn: true });
@@ -891,7 +887,7 @@ function builtInProfileEntry(profile: AgentProfile): ConfigEntry<AgentProfile> {
891
887
  }
892
888
 
893
889
  export function getAgentProfile(name: string): ConfigEntry<AgentProfile> | null {
894
- const builtIn = BUILT_IN_AGENT_PROFILES.find((profile) => profile.name === name);
890
+ const builtIn = builtInAgentProfiles().find((profile) => profile.name === name);
895
891
  if (builtIn) return builtInProfileEntry(builtIn);
896
892
  return getConfig<AgentProfile>(AGENT_PROFILE_NAMESPACE, name);
897
893
  }
@@ -922,7 +918,7 @@ export function toolGroupsForProfile(profileName: string | undefined): string[]
922
918
  export function listAgentProfiles(): ConfigEntry<AgentProfile>[] {
923
919
  const custom = listConfig<AgentProfile>(AGENT_PROFILE_NAMESPACE);
924
920
  return [
925
- ...BUILT_IN_AGENT_PROFILES.map(builtInProfileEntry),
921
+ ...builtInAgentProfiles().map(builtInProfileEntry),
926
922
  ...custom,
927
923
  ].sort((a, b) => Number(Boolean(b.value.builtIn)) - Number(Boolean(a.value.builtIn)) || a.key.localeCompare(b.key));
928
924
  }
@@ -7,6 +7,7 @@ import { NOTIFICATIONS_DEFAULT_KEY, NOTIFICATIONS_NAMESPACE, notificationAgentKe
7
7
  import { getDb } from "./db/connection.ts";
8
8
  import { emitRelayEvent } from "./events";
9
9
  import { routeNotification } from "./notification-router";
10
+ import { renderTemplate } from "./prompt-resolver";
10
11
  import { effectiveProviderCatalogList } from "./provider-catalog-store";
11
12
  import type { AgentCard, Command, ContextState } from "./types";
12
13
 
@@ -71,12 +72,18 @@ function notifyContextAdvisory(input: ContextAdvisoryInput): void {
71
72
  }
72
73
 
73
74
  function contextAdvisoryBody(input: ContextAdvisoryInput): string {
74
- const usage = `${formatTokens(input.tokens)} / ${formatTokens(input.windowTokens)} tokens used (${input.percent}% of window, ${input.band}% band)`;
75
+ const tokens = `${formatTokens(input.tokens)} / ${formatTokens(input.windowTokens)} tokens used`;
76
+ const percent = `${input.percent}% of window, ${input.band}% band`;
75
77
  const threshold = `resolved self-compact threshold ${input.selfCompactPercent}% (${input.thresholdSource})`;
76
78
  if (input.stage === "warn") {
77
- return `Context warning: ${usage}. You are approaching the ${threshold}; if the current task is at a clean seam, prepare to call relay_compact_and_resume before continuing.`;
79
+ return renderTemplate("context.advisory.warn", { tokens, percent, threshold });
78
80
  }
79
- return `Context advisory: ${usage}. You are over the ${threshold}; call relay_compact_and_resume at the next clean seam between tasks or waves, not mid-edit. The ${input.hardBackstopPercent}% hard backstop may request compaction if usage keeps climbing.`;
81
+ return renderTemplate("context.advisory.compact", {
82
+ tokens,
83
+ percent,
84
+ threshold,
85
+ hardBackstop: `${input.hardBackstopPercent}%`,
86
+ });
80
87
  }
81
88
 
82
89
  function contextAdvisoryBand(utilization: number, threshold: EffectiveContextThreshold): { band: number; stage: ContextAdvisoryStage } | null {
package/src/db/index.ts CHANGED
@@ -38,3 +38,4 @@ export * from "./provisioning-registry.ts";
38
38
  export * from "./provider-quotas.ts";
39
39
  export * from "./provider-quota-leases.ts";
40
40
  export * from "./scheduled-timers.ts";
41
+ export * from "./prompt-templates.ts";
@@ -247,6 +247,35 @@ export function resolveProjectForCwd(cwd: string): Project | null {
247
247
  return best ? getProject(best.projectId) : null;
248
248
  }
249
249
 
250
+ /** Batch cwd→project resolution using the same longest-root-match rule as the scalar helper. */
251
+ export function resolveProjectsForCwds(cwds: string[]): Map<string, Project> {
252
+ const requested = [...new Set(cwds.filter(Boolean))];
253
+ const resolved = new Map<string, { projectId: string; len: number }>();
254
+ if (requested.length === 0) return new Map();
255
+ const rows = getDb()
256
+ .query("SELECT project_id, root_path, created_at FROM project_roots")
257
+ .all() as ProjectRootRow[];
258
+ for (const cwd of requested) {
259
+ for (const row of rows) {
260
+ const current = resolved.get(cwd);
261
+ if (isPathWithinBase(cwd, row.root_path) && (!current || row.root_path.length > current.len)) {
262
+ resolved.set(cwd, { projectId: row.project_id, len: row.root_path.length });
263
+ }
264
+ }
265
+ }
266
+ const mapped = new Map<string, Project>();
267
+ const projectsById = new Map<string, Project>();
268
+ for (const [cwd, match] of resolved.entries()) {
269
+ let project = projectsById.get(match.projectId);
270
+ if (!project) {
271
+ project = getProject(match.projectId) ?? undefined;
272
+ if (project) projectsById.set(match.projectId, project);
273
+ }
274
+ if (project) mapped.set(cwd, project);
275
+ }
276
+ return mapped;
277
+ }
278
+
250
279
  // ── Composition graph (#406) ────────────────────────────────────────────────
251
280
  // project_edges models two relations, asymmetric by design:
252
281
  // • contains — umbrella → sub-project hierarchy. INHERITS: resolving a
@@ -0,0 +1,20 @@
1
+ import { defaultPromptTemplates } from "../prompt-resolver";
2
+ import { seedTemplates } from "./prompt-templates";
3
+
4
+ export const PROMPT_TEMPLATES_SCHEMA_SQL = `
5
+ CREATE TABLE IF NOT EXISTS prompt_templates (
6
+ slug TEXT PRIMARY KEY,
7
+ category TEXT NOT NULL,
8
+ title TEXT NOT NULL,
9
+ description TEXT,
10
+ template TEXT NOT NULL,
11
+ default_template TEXT NOT NULL,
12
+ variables TEXT NOT NULL DEFAULT '[]',
13
+ updated_at INTEGER NOT NULL,
14
+ created_at INTEGER NOT NULL
15
+ );
16
+ `;
17
+
18
+ export function seedPromptTemplatesIfNeeded(): void {
19
+ seedTemplates(defaultPromptTemplates());
20
+ }