agent-relay-server 0.114.0 → 0.116.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 (159) hide show
  1. package/docs/openapi.json +39 -1
  2. package/package.json +3 -3
  3. package/public/assets/MessageBubble-BJllE_2c.js +4 -0
  4. package/public/assets/MessageBubble-BJllE_2c.js.map +1 -0
  5. package/public/assets/PlanGraphPanel-CoK024n9.js +2 -0
  6. package/public/assets/PlanGraphPanel-CoK024n9.js.map +1 -0
  7. package/public/assets/{activity-Dw15HMdi.js → activity-CBjg7NSP.js} +2 -2
  8. package/public/assets/{activity-Dw15HMdi.js.map → activity-CBjg7NSP.js.map} +1 -1
  9. package/public/assets/{agent-profiles-DNtXbS77.js → agent-profiles-DzkjQaCS.js} +2 -2
  10. package/public/assets/{agent-profiles-DNtXbS77.js.map → agent-profiles-DzkjQaCS.js.map} +1 -1
  11. package/public/assets/agents-Bu1i1jH-.js +2 -0
  12. package/public/assets/{agents-DOwnLE42.js.map → agents-Bu1i1jH-.js.map} +1 -1
  13. package/public/assets/{analytics-CRwxJWuV.js → analytics-Cc2GoEq_.js} +3 -3
  14. package/public/assets/{analytics-CRwxJWuV.js.map → analytics-Cc2GoEq_.js.map} +1 -1
  15. package/public/assets/{automation-BQk8XQEj.js → automation-CcMveyvi.js} +2 -2
  16. package/public/assets/{automation-BQk8XQEj.js.map → automation-CcMveyvi.js.map} +1 -1
  17. package/public/assets/{badge-oaCc6hVH.js → badge-BhUZ1cAb.js} +2 -2
  18. package/public/assets/{badge-oaCc6hVH.js.map → badge-BhUZ1cAb.js.map} +1 -1
  19. package/public/assets/branch-state-badge-CYWBVZDI.js +2 -0
  20. package/public/assets/{branch-state-badge-JDaHO2fG.js.map → branch-state-badge-CYWBVZDI.js.map} +1 -1
  21. package/public/assets/{button-Cx0NiToX.js → button-C-V4oij1.js} +2 -2
  22. package/public/assets/{button-Cx0NiToX.js.map → button-C-V4oij1.js.map} +1 -1
  23. package/public/assets/{card-DkHdS68K.js → card-DAkf5Kzw.js} +2 -2
  24. package/public/assets/{card-DkHdS68K.js.map → card-DAkf5Kzw.js.map} +1 -1
  25. package/public/assets/{channels-wQha9YA6.js → channels-IdC7jyLt.js} +2 -2
  26. package/public/assets/{channels-wQha9YA6.js.map → channels-IdC7jyLt.js.map} +1 -1
  27. package/public/assets/chat-CP4x6Gcr.js +2 -0
  28. package/public/assets/{chat-BTmgzO_g.js.map → chat-CP4x6Gcr.js.map} +1 -1
  29. package/public/assets/{connectors-BF-XDm0s.js → connectors-Dngl_FJz.js} +2 -2
  30. package/public/assets/{connectors-BF-XDm0s.js.map → connectors-Dngl_FJz.js.map} +1 -1
  31. package/public/assets/{copy-button-CsCBPQzj.js → copy-button-BStzXgtd.js} +2 -2
  32. package/public/assets/{copy-button-CsCBPQzj.js.map → copy-button-BStzXgtd.js.map} +1 -1
  33. package/public/assets/{dist-BJnh7VkB.js → dist-CBMWGhWc.js} +2 -2
  34. package/public/assets/{dist-BJnh7VkB.js.map → dist-CBMWGhWc.js.map} +1 -1
  35. package/public/assets/{dist-Pak9TL_I.js → dist-CDZZxxJq.js} +2 -2
  36. package/public/assets/{dist-Pak9TL_I.js.map → dist-CDZZxxJq.js.map} +1 -1
  37. package/public/assets/{dist-AbnkNPcD.js → dist-CQ0Z-TDi.js} +2 -2
  38. package/public/assets/{dist-AbnkNPcD.js.map → dist-CQ0Z-TDi.js.map} +1 -1
  39. package/public/assets/{dist-REUlCFee.js → dist-ChDX_t12.js} +2 -2
  40. package/public/assets/{dist-REUlCFee.js.map → dist-ChDX_t12.js.map} +1 -1
  41. package/public/assets/{dist-DLOrqIs-.js → dist-CmFeEXMe.js} +2 -2
  42. package/public/assets/{dist-DLOrqIs-.js.map → dist-CmFeEXMe.js.map} +1 -1
  43. package/public/assets/{dist-DBti5--U.js → dist-Cs8X0P1d.js} +2 -2
  44. package/public/assets/{dist-DBti5--U.js.map → dist-Cs8X0P1d.js.map} +1 -1
  45. package/public/assets/dist-D2jvo5oA.js +2 -0
  46. package/public/assets/{dist-QY_EIkcd.js.map → dist-D2jvo5oA.js.map} +1 -1
  47. package/public/assets/{dist-1g1lCjVb.js → dist-DV83j9tm.js} +2 -2
  48. package/public/assets/{dist-1g1lCjVb.js.map → dist-DV83j9tm.js.map} +1 -1
  49. package/public/assets/{dist-DJpDTmGg.js → dist-DyNJNwpi.js} +2 -2
  50. package/public/assets/{dist-DJpDTmGg.js.map → dist-DyNJNwpi.js.map} +1 -1
  51. package/public/assets/dist-mm7c7nBt.js +2 -0
  52. package/public/assets/{dist-DjJMNrsj.js.map → dist-mm7c7nBt.js.map} +1 -1
  53. package/public/assets/dist-y03IwOrd.js +2 -0
  54. package/public/assets/{dist-C7VlX6hz.js.map → dist-y03IwOrd.js.map} +1 -1
  55. package/public/assets/{es2015-GhJvD7eR.js → es2015-VnZ2kVIz.js} +2 -2
  56. package/public/assets/{es2015-GhJvD7eR.js.map → es2015-VnZ2kVIz.js.map} +1 -1
  57. package/public/assets/formatted-body-impl-CBHoD82a.js +3 -0
  58. package/public/assets/formatted-body-impl-CBHoD82a.js.map +1 -0
  59. package/public/assets/{index-DYwuv_Na.css → index-DcmG5gLR.css} +1 -1
  60. package/public/assets/index-DocqZ4Q0.js +23 -0
  61. package/public/assets/index-DocqZ4Q0.js.map +1 -0
  62. package/public/assets/{input-CPfrBmej.js → input-CtLT7WXi.js} +2 -2
  63. package/public/assets/{input-CPfrBmej.js.map → input-CtLT7WXi.js.map} +1 -1
  64. package/public/assets/{insights-DwS0svZy.js → insights-Du9X-nwA.js} +2 -2
  65. package/public/assets/{insights-DwS0svZy.js.map → insights-Du9X-nwA.js.map} +1 -1
  66. package/public/assets/{integrations-De1_UHQE.js → integrations-BlSadMFC.js} +2 -2
  67. package/public/assets/{integrations-De1_UHQE.js.map → integrations-BlSadMFC.js.map} +1 -1
  68. package/public/assets/{lib-Bi-xeh6t.js → lib-BLoevhQO.js} +3 -3
  69. package/public/assets/{lib-Bi-xeh6t.js.map → lib-BLoevhQO.js.map} +1 -1
  70. package/public/assets/lucide-react-Dc3ZwCBi.js +9 -0
  71. package/public/assets/{lucide-react-De5edQpl.js.map → lucide-react-Dc3ZwCBi.js.map} +1 -1
  72. package/public/assets/{maintenance-B5qrcsDt.js → maintenance-KeCPGZG4.js} +2 -2
  73. package/public/assets/{maintenance-B5qrcsDt.js.map → maintenance-KeCPGZG4.js.map} +1 -1
  74. package/public/assets/{managed-agents-BGMWAUfG.js → managed-agents-YKqARLQn.js} +4 -4
  75. package/public/assets/{managed-agents-BGMWAUfG.js.map → managed-agents-YKqARLQn.js.map} +1 -1
  76. package/public/assets/{markdown-preview-impl-DsZ4Ek80.js → markdown-preview-impl-Bo8ql-lO.js} +2 -2
  77. package/public/assets/{markdown-preview-impl-DsZ4Ek80.js.map → markdown-preview-impl-Bo8ql-lO.js.map} +1 -1
  78. package/public/assets/memory-BlftQACU.js +2 -0
  79. package/public/assets/{memory-CouN7Y4r.js.map → memory-BlftQACU.js.map} +1 -1
  80. package/public/assets/{messages-D9mYFxhU.js → messages-DQu0Np7r.js} +2 -2
  81. package/public/assets/{messages-D9mYFxhU.js.map → messages-DQu0Np7r.js.map} +1 -1
  82. package/public/assets/{orchestrators-B_SD_mhI.js → orchestrators-CW00iA0W.js} +2 -2
  83. package/public/assets/{orchestrators-B_SD_mhI.js.map → orchestrators-CW00iA0W.js.map} +1 -1
  84. package/public/assets/{overview-C8hVsCqz.js → overview-DyiH9eVP.js} +2 -2
  85. package/public/assets/{overview-C8hVsCqz.js.map → overview-DyiH9eVP.js.map} +1 -1
  86. package/public/assets/{pairs-B6vWKXnX.js → pairs-Ee8Npq67.js} +2 -2
  87. package/public/assets/{pairs-B6vWKXnX.js.map → pairs-Ee8Npq67.js.map} +1 -1
  88. package/public/assets/{projects-C2gHOdEY.js → projects-CqeJsfB6.js} +2 -2
  89. package/public/assets/{projects-C2gHOdEY.js.map → projects-CqeJsfB6.js.map} +1 -1
  90. package/public/assets/{prompt-settings-D4SXk0af.js → prompt-settings-0fHJtGD5.js} +2 -2
  91. package/public/assets/{prompt-settings-D4SXk0af.js.map → prompt-settings-0fHJtGD5.js.map} +1 -1
  92. package/public/assets/{react-dom-FNIdlE_z.js → react-dom-cLkVFBsS.js} +2 -2
  93. package/public/assets/{react-dom-FNIdlE_z.js.map → react-dom-cLkVFBsS.js.map} +1 -1
  94. package/public/assets/{registry-CMPhWcx3.js → registry-QTLN8Jy-.js} +2 -2
  95. package/public/assets/{registry-CMPhWcx3.js.map → registry-QTLN8Jy-.js.map} +1 -1
  96. package/public/assets/{security-C1fE4DVg.js → security-B0FTbGXU.js} +2 -2
  97. package/public/assets/{security-C1fE4DVg.js.map → security-B0FTbGXU.js.map} +1 -1
  98. package/public/assets/{select-D9j1MBVi.js → select-DhY0OvwP.js} +2 -2
  99. package/public/assets/{select-D9j1MBVi.js.map → select-DhY0OvwP.js.map} +1 -1
  100. package/public/assets/settings-B2UEpdBi.js +6 -0
  101. package/public/assets/{settings-CduSwUr9.js.map → settings-B2UEpdBi.js.map} +1 -1
  102. package/public/assets/store-DwgQJjEv.js +9 -0
  103. package/public/assets/store-DwgQJjEv.js.map +1 -0
  104. package/public/assets/tasks-BvDfBcxZ.js +2 -0
  105. package/public/assets/tasks-BvDfBcxZ.js.map +1 -0
  106. package/public/assets/{teams-ZEXvcAz_.js → teams-CIqsg55S.js} +2 -2
  107. package/public/assets/{teams-ZEXvcAz_.js.map → teams-CIqsg55S.js.map} +1 -1
  108. package/public/assets/{terminal-viewer-impl-tktfofs3.js → terminal-viewer-impl-Cig30gnt.js} +3 -3
  109. package/public/assets/{terminal-viewer-impl-tktfofs3.js.map → terminal-viewer-impl-Cig30gnt.js.map} +1 -1
  110. package/public/assets/types-DSwhOFcJ.js.map +1 -1
  111. package/public/assets/{use-keyboard-viewport-wzkGlTqU.js → use-keyboard-viewport-DucYmqmP.js} +2 -2
  112. package/public/assets/{use-keyboard-viewport-wzkGlTqU.js.map → use-keyboard-viewport-DucYmqmP.js.map} +1 -1
  113. package/public/assets/{user-avatar-_Dq16BWV.js → user-avatar-Ct6HEqZm.js} +2 -2
  114. package/public/assets/{user-avatar-_Dq16BWV.js.map → user-avatar-Ct6HEqZm.js.map} +1 -1
  115. package/public/assets/{work-queue-Bk8g7t9K.js → work-queue-XWcDwe8G.js} +2 -2
  116. package/public/assets/{work-queue-Bk8g7t9K.js.map → work-queue-XWcDwe8G.js.map} +1 -1
  117. package/public/assets/workspaces-EI4N2N7L.js +3 -0
  118. package/public/assets/{workspaces-D2bYW3zl.js.map → workspaces-EI4N2N7L.js.map} +1 -1
  119. package/public/index.html +21 -21
  120. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  121. package/scripts/orchestrator-spawn-smoke.ts +30 -13
  122. package/src/branch-landed.ts +14 -1
  123. package/src/config-store.ts +15 -3
  124. package/src/db/mappers.ts +3 -0
  125. package/src/db/migrations.ts +41 -2
  126. package/src/db/plans.ts +11 -0
  127. package/src/db/schema.ts +3 -1
  128. package/src/db/task-entity.ts +78 -3
  129. package/src/mcp/index.ts +1 -1
  130. package/src/mcp-plan-tools.ts +3 -0
  131. package/src/mcp-task-tools.ts +16 -7
  132. package/src/routes/index.ts +2 -1
  133. package/src/routes/plans.ts +12 -2
  134. package/src/routes/tasks.ts +13 -5
  135. package/src/services/plan-graph.ts +8 -1
  136. package/src/services/spawn-agent.ts +25 -22
  137. package/src/services/task-entity.ts +6 -1
  138. package/public/assets/MessageBubble-CxqPFLGP.js +0 -4
  139. package/public/assets/MessageBubble-CxqPFLGP.js.map +0 -1
  140. package/public/assets/PlanGraphPanel-gQ_lb6zP.js +0 -2
  141. package/public/assets/PlanGraphPanel-gQ_lb6zP.js.map +0 -1
  142. package/public/assets/agents-DOwnLE42.js +0 -2
  143. package/public/assets/branch-state-badge-JDaHO2fG.js +0 -2
  144. package/public/assets/chat-BTmgzO_g.js +0 -2
  145. package/public/assets/dist-C7VlX6hz.js +0 -2
  146. package/public/assets/dist-DjJMNrsj.js +0 -2
  147. package/public/assets/dist-QY_EIkcd.js +0 -2
  148. package/public/assets/formatted-body-impl-2ZWd80_S.js +0 -3
  149. package/public/assets/formatted-body-impl-2ZWd80_S.js.map +0 -1
  150. package/public/assets/index-D4mb_LKw.js +0 -23
  151. package/public/assets/index-D4mb_LKw.js.map +0 -1
  152. package/public/assets/lucide-react-De5edQpl.js +0 -9
  153. package/public/assets/memory-CouN7Y4r.js +0 -2
  154. package/public/assets/settings-CduSwUr9.js +0 -6
  155. package/public/assets/store-Bk6Lku_S.js +0 -9
  156. package/public/assets/store-Bk6Lku_S.js.map +0 -1
  157. package/public/assets/tasks-Bi456f7f.js +0 -2
  158. package/public/assets/tasks-Bi456f7f.js.map +0 -1
  159. package/public/assets/workspaces-D2bYW3zl.js +0 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.114.0",
4
+ "version": "0.116.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+ import { spawnSync } from "node:child_process";
2
3
  import { hostname } from "node:os";
3
4
  import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
4
5
 
@@ -16,7 +17,7 @@ type Agent = {
16
17
  status: string;
17
18
  ready: boolean;
18
19
  tags?: string[];
19
- meta?: { cwd?: string; provider?: string };
20
+ meta?: { cwd?: string; provider?: string; workspace?: { repoRoot?: string; sourceCwd?: string } };
20
21
  createdAt?: number;
21
22
  };
22
23
 
@@ -25,10 +26,9 @@ type ApprovalMode = "open" | "guarded" | "read-only";
25
26
  const args = process.argv.slice(2);
26
27
  let relayUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
27
28
  let orchestratorId = process.env.AGENT_RELAY_SMOKE_ORCHESTRATOR || "";
28
- // Default cwd is resolved from the selected orchestrator's baseDir below, not
29
- // the local process.cwd() the chosen orchestrator may be on a different host
30
- // (e.g. macOS macmini with /Users/admin/projects) where a local Linux path
31
- // fails cwd containment.
29
+ // Default cwd is resolved to the local git repo root below. For remote
30
+ // orchestrators, pass --cwd or AGENT_RELAY_SMOKE_CWD with a repo path on that
31
+ // host so isolated workspace creation has a valid source repository.
32
32
  let cwd = process.env.AGENT_RELAY_SMOKE_CWD || "";
33
33
  let timeoutMs = Number(process.env.AGENT_RELAY_SMOKE_TIMEOUT_MS || 90_000);
34
34
  let providers = (process.env.AGENT_RELAY_SMOKE_PROVIDERS || "codex,claude").split(",").filter(Boolean);
@@ -83,11 +83,24 @@ async function waitFor<T>(label: string, fn: () => Promise<T | null>): Promise<T
83
83
  throw new Error(`${label} timed out after ${timeoutMs}ms`);
84
84
  }
85
85
 
86
+ function localRepoRoot(): string | null {
87
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
88
+ cwd: process.cwd(),
89
+ encoding: "utf8",
90
+ });
91
+ if (result.status !== 0) return null;
92
+ return result.stdout.trim() || null;
93
+ }
94
+
86
95
  function findSpawnedAgent(agents: Agent[], provider: string, label: string, startedAt: number): Agent | null {
87
96
  return agents
88
97
  .filter((agent) => agent.ready && agent.status !== "offline")
89
98
  .filter((agent) => agent.label === label)
90
- .filter((agent) => agent.meta?.cwd === cwd)
99
+ .filter((agent) =>
100
+ agent.meta?.cwd === cwd ||
101
+ agent.meta?.workspace?.sourceCwd === cwd ||
102
+ agent.meta?.workspace?.repoRoot === cwd
103
+ )
91
104
  .filter((agent) => (agent.tags || []).includes(provider))
92
105
  .filter((agent) => (agent.tags || []).includes("dashboard-spawned"))
93
106
  .filter((agent) => !agent.createdAt || agent.createdAt >= startedAt - 5_000)
@@ -118,8 +131,8 @@ const orchestrators = await api<Orchestrator[]>("GET", "/orchestrators");
118
131
  // Default selection prefers the orchestrator on the same host as the relay (the
119
132
  // gate runs there): a local spawn is faster and avoids cross-host teardown
120
133
  // flakiness. Fall back to any online orchestrator. An explicit --orchestrator
121
- // always wins — pair it with --cwd (or rely on the baseDir default below) to
122
- // target a remote host such as a macOS macmini.
134
+ // always wins — pair it with --cwd to target a remote host such as a macOS
135
+ // macmini.
123
136
  const onlineOrchestrators = orchestrators.filter((orch) => orch.status === "online");
124
137
  const localOrchestrator = onlineOrchestrators.find((orch) => orch.id === hostname());
125
138
  const orchestrator = orchestratorId
@@ -129,13 +142,17 @@ if (!orchestrator) throw new Error(orchestratorId ? `orchestrator not found: ${o
129
142
  if (orchestrator.status !== "online") throw new Error(`orchestrator ${orchestrator.id} is ${orchestrator.status}`);
130
143
 
131
144
  // Resolve the spawn cwd against the chosen orchestrator's own base directory so
132
- // the smoke works regardless of which host runs it. An explicit --cwd / env
133
- // override still wins (and is subject to the orchestrator's containment check).
145
+ // the smoke exercises isolated workspace creation from a real git repository.
146
+ // An explicit --cwd / env override still wins (and is subject to the
147
+ // orchestrator's containment check).
134
148
  if (!cwd) {
135
- if (!orchestrator.baseDir) {
136
- throw new Error(`orchestrator ${orchestrator.id} did not report a baseDir; pass --cwd or set AGENT_RELAY_SMOKE_CWD`);
149
+ if (orchestrator.id !== hostname()) {
150
+ throw new Error(`orchestrator ${orchestrator.id} is remote; pass --cwd or set AGENT_RELAY_SMOKE_CWD to a git repo on that host`);
151
+ }
152
+ cwd = localRepoRoot() || "";
153
+ if (!cwd) {
154
+ throw new Error("could not resolve the local git repo root; pass --cwd or set AGENT_RELAY_SMOKE_CWD");
137
155
  }
138
- cwd = orchestrator.baseDir;
139
156
  }
140
157
  console.log(`using cwd ${cwd} on ${orchestrator.id} (base ${orchestrator.baseDir})`);
141
158
 
@@ -2,7 +2,7 @@ import { errMessage, type BaseWorktreeSyncResult } from "agent-relay-sdk";
2
2
  import { emitRelayEvent } from "./events";
3
3
  import { routeNotification, INFORMATIONAL_TTL_MS } from "./notification-router";
4
4
  import { registerDefaultAudienceRules, audiencePredicates } from "./notification-rules";
5
- import { createActivityEvent, getAgent, updateWorkspaceStatus } from "./db";
5
+ import { createActivityEvent, deliverableTaskIdsByBranch, deliverableTaskIdsByOwnerAgent, getAgent, getTaskDetail, updateWorkspaceStatus } from "./db";
6
6
  import { renderTemplate } from "./prompt-resolver";
7
7
  import { resolveCoordinatorAgentId } from "./steward-identity";
8
8
  import { stewardFallbackTarget } from "./config";
@@ -85,6 +85,15 @@ function landedCommitSubjects(input: BranchLandedInput, log: BranchLandedLogFn):
85
85
  }
86
86
  }
87
87
 
88
+ function resolveLandedTask(workspace: Pick<WorkspaceRecord, "branch" | "ownerAgentId">): { taskId: number; taskLabel: string } | undefined {
89
+ const branchTaskId = workspace.branch ? deliverableTaskIdsByBranch(workspace.branch)[0] : undefined;
90
+ const ownerTaskIds = !branchTaskId && workspace.ownerAgentId ? deliverableTaskIdsByOwnerAgent(workspace.ownerAgentId) : [];
91
+ const taskId = branchTaskId ?? (ownerTaskIds.length === 1 ? ownerTaskIds[0] : undefined);
92
+ if (!taskId) return undefined;
93
+ const task = getTaskDetail(taskId);
94
+ return task ? { taskId: task.id, taskLabel: task.title } : undefined;
95
+ }
96
+
88
97
  /**
89
98
  * #239 — turn an authoritative land completion into a relay-driven push so the author
90
99
  * stops polling to learn it merged. Always emits the durable `branch.landed` event (the
@@ -100,6 +109,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
100
109
  const shortSha = input.mergedSha ? input.mergedSha.slice(0, 12) : undefined;
101
110
  const lifecycleLog = (message: string) => console.warn(message);
102
111
  const commitSubjects = landedCommitSubjects(input, lifecycleLog);
112
+ const landedTask = resolveLandedTask(workspace);
103
113
 
104
114
  emitRelayEvent({
105
115
  type: "branch.landed",
@@ -115,6 +125,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
115
125
  author: workspace.ownerAgentId,
116
126
  newBranch: input.newBranch,
117
127
  pushed: input.pushed,
128
+ ...landedTask,
118
129
  },
119
130
  });
120
131
  emitRelayEvent({
@@ -132,6 +143,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
132
143
  newBranch: input.newBranch,
133
144
  pushed: input.pushed,
134
145
  notification: "landed-success",
146
+ ...landedTask,
135
147
  },
136
148
  });
137
149
  // #482/#833 issue close FIRST, then the #831 Task drive: the Task's close PROJECTION
@@ -164,6 +176,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
164
176
  author,
165
177
  newBranch: input.newBranch,
166
178
  pushed: input.pushed,
179
+ ...landedTask,
167
180
  };
168
181
 
169
182
  // A local-only land (push disabled, or base has no upstream) has NOT reached the
@@ -67,14 +67,14 @@ const WORKER_TOOL_GROUPS = ["core", "workspace", "memory", "artifacts", "blackbo
67
67
 
68
68
  function builtInAgentProfiles(): AgentProfile[] {
69
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 } } }),
70
+ agentProfileDefaults({ name: DEFAULT_SPAWN_PROFILE_NAME, icon: "🤖", description: "Relay-aware spawned agent with bundled Relay context, skills, plugins, and status integration.", base: "minimal", instructions: { append: [], repoInstructions: "allow", globalInstructions: "allow" }, sharedMcp: { policy: "all" }, hostAssets: { skills: true, plugins: true }, relay: { context: true, skills: true, plugins: true, statusLine: true, mcp: true, tools: { groups: WORKER_TOOL_GROUPS } }, permissions: { filesystem: "host" } }),
71
71
  agentProfileDefaults({ name: "minimal", icon: "▫️", description: "Repo-focused spawned agent with Relay affordances disabled and host/global extras avoided where supported.", base: "minimal" }),
72
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
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
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 }),
75
+ agentProfileDefaults({ name: "coordinator", icon: "🧭", description: "Relay-aware coordination lead for spawning worker teams, maintaining shared state, and driving review and release.", base: "minimal", instructions: { append: [renderTemplate("profile.coordinator.charter")], repoInstructions: "allow", globalInstructions: "allow" }, sharedMcp: { policy: "all" }, hostAssets: { skills: true, plugins: true }, relay: { context: true, skills: true, plugins: true, statusLine: true, mcp: true }, permissions: { filesystem: "host" }, maxSpawnedAgents: 8 }),
76
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" }, notifications: { responseMode: "results-only" }, 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 } } }),
77
+ agentProfileDefaults({ name: "worker", icon: "🔨", description: "Relay-aware focused implementer with open approval posture and no delegated spawn capability.", base: "minimal", instructions: { system: renderTemplate("profile.worker.system"), append: [], repoInstructions: "allow", globalInstructions: "allow" }, sharedMcp: { policy: "all" }, hostAssets: { skills: true, plugins: true }, relay: { context: true, skills: true, plugins: true, statusLine: true, mcp: true, tools: { groups: WORKER_TOOL_GROUPS } }, permissions: { mode: "open", filesystem: "host" } }),
78
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
79
  ].map((profile) => ({ ...profile, builtIn: true }));
80
80
  }
@@ -272,6 +272,16 @@ function cleanAgentProfileSharedMcp(value: unknown): AgentProfile["sharedMcp"] {
272
272
  return { policy: cleanEnum(policy, "sharedMcp.policy", SHARED_MCP_POLICY_NAMES) as NonNullable<AgentProfile["sharedMcp"]>["policy"] };
273
273
  }
274
274
 
275
+ function cleanAgentProfileHostAssets(value: unknown): AgentProfile["hostAssets"] {
276
+ if (value === undefined || value === null || value === false) return undefined;
277
+ if (!isRecord(value)) throw new ValidationError("hostAssets must be an object");
278
+ const hostAssets = {
279
+ ...(value.skills === undefined ? {} : { skills: cleanBoolean(value.skills, "hostAssets.skills") }),
280
+ ...(value.plugins === undefined ? {} : { plugins: cleanBoolean(value.plugins, "hostAssets.plugins") }),
281
+ };
282
+ return Object.keys(hostAssets).length ? hostAssets : undefined;
283
+ }
284
+
275
285
  function cleanAgentProfileNotifications(value: unknown): AgentProfile["notifications"] {
276
286
  if (value === undefined || value === null) return undefined;
277
287
  if (!isRecord(value)) throw new ValidationError("notifications must be an object");
@@ -306,6 +316,7 @@ function agentProfileDefaults(input: Pick<AgentProfile, "name" | "base"> & Omit<
306
316
  ...(input.projectSkills ? { projectSkills: input.projectSkills } : {}),
307
317
  ...(input.projectSettings ? { projectSettings: input.projectSettings } : {}),
308
318
  ...(input.sharedMcp ? { sharedMcp: input.sharedMcp } : {}),
319
+ ...(input.hostAssets ? { hostAssets: input.hostAssets } : {}),
309
320
  ...(input.notifications ? { notifications: input.notifications } : {}),
310
321
  relay: {
311
322
  context: input.relay?.context ?? !isolated,
@@ -389,6 +400,7 @@ function validateAgentProfile(key: string, value: unknown, opts: { allowBuiltIn?
389
400
  projectSkills: cleanAgentProfileProjectSkills(value.projectSkills),
390
401
  projectSettings: cleanAgentProfileProjectSettings(value.projectSettings),
391
402
  sharedMcp: cleanAgentProfileSharedMcp(value.sharedMcp),
403
+ hostAssets: cleanAgentProfileHostAssets(value.hostAssets),
392
404
  notifications: cleanAgentProfileNotifications(value.notifications),
393
405
  relay: {
394
406
  context: relay.context === undefined ? defaults.relay.context : cleanBoolean(relay.context, "relay.context"),
package/src/db/mappers.ts CHANGED
@@ -277,6 +277,7 @@ export function rowToTask(row: any): Task {
277
277
  return {
278
278
  id: row.id,
279
279
  source: row.source,
280
+ label: row.title,
280
281
  title: row.title,
281
282
  body: row.body,
282
283
  severity: row.severity as TaskSeverity,
@@ -310,6 +311,8 @@ export function rowToTask(row: any): Task {
310
311
  ...(row.commits != null ? { commits: parseJson(row.commits, []) } : {}),
311
312
  ...(row.done_when != null ? { doneWhen: row.done_when } : {}),
312
313
  ...(row.team_id != null ? { teamId: row.team_id } : {}),
314
+ ...(row.project_id != null ? { projectId: row.project_id } : {}),
315
+ ...(row.repo != null ? { repo: row.repo } : {}),
313
316
  ...(row.created_by != null ? { createdBy: row.created_by } : {}),
314
317
  ...(row.owner_agent_id != null ? { ownerAgentId: row.owner_agent_id } : {}),
315
318
  ...(row.dispatch != null ? { dispatch: parseJson(row.dispatch, {}) } : {}),
@@ -444,11 +444,11 @@ export function applyMigrations(): void {
444
444
  created_by TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
445
445
  )
446
446
  `);
447
- const planCols = getDb().query("PRAGMA table_info(plans)").all() as any[];
448
- const planColNames = planCols.map((c: any) => c.name);
447
+ const planColNames = (getDb().query("PRAGMA table_info(plans)").all() as any[]).map((c: any) => c.name);
449
448
  if (!planColNames.includes("team_id")) {
450
449
  getDb().run("ALTER TABLE plans ADD COLUMN team_id TEXT");
451
450
  }
451
+ if (!planColNames.includes("archived")) getDb().run("ALTER TABLE plans ADD COLUMN archived INTEGER NOT NULL DEFAULT 0");
452
452
  getDb().run("CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_thread_channel ON plans(thread_id, coalesce(channel, ''))");
453
453
  getDb().run("CREATE INDEX IF NOT EXISTS idx_plans_updated ON plans(updated_at)");
454
454
  getDb().run("CREATE INDEX IF NOT EXISTS idx_plans_team ON plans(team_id)");
@@ -1049,6 +1049,8 @@ export function applyMigrations(): void {
1049
1049
  addTaskCol("commits", "commits TEXT");
1050
1050
  addTaskCol("done_when", "done_when TEXT");
1051
1051
  addTaskCol("team_id", "team_id TEXT");
1052
+ addTaskCol("project_id", "project_id TEXT");
1053
+ addTaskCol("repo", "repo TEXT");
1052
1054
  addTaskCol("created_by", "created_by TEXT");
1053
1055
  addTaskCol("owner_agent_id", "owner_agent_id TEXT");
1054
1056
  addTaskCol("dispatch", "dispatch TEXT");
@@ -1056,6 +1058,43 @@ export function applyMigrations(): void {
1056
1058
  getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_kind ON tasks(kind, status, updated_at)");
1057
1059
  getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_branch ON tasks(branch) WHERE branch IS NOT NULL");
1058
1060
  getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks(team_id) WHERE team_id IS NOT NULL");
1061
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id, status, updated_at) WHERE project_id IS NOT NULL");
1062
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_repo ON tasks(repo, status, updated_at) WHERE repo IS NOT NULL");
1063
+ getDb().run(`
1064
+ UPDATE tasks
1065
+ SET repo = (
1066
+ SELECT ir.repo
1067
+ FROM issue_associations ia
1068
+ JOIN issue_refs ir ON ir.id = ia.issue_ref_id
1069
+ WHERE ia.entity_type = 'task' AND ia.entity_id = CAST(tasks.id AS TEXT)
1070
+ ORDER BY ia.created_at ASC, ir.id ASC
1071
+ LIMIT 1
1072
+ )
1073
+ WHERE kind = 'deliverable'
1074
+ AND (repo IS NULL OR trim(repo) = '')
1075
+ AND EXISTS (
1076
+ SELECT 1 FROM issue_associations ia
1077
+ JOIN issue_refs ir ON ir.id = ia.issue_ref_id
1078
+ WHERE ia.entity_type = 'task' AND ia.entity_id = CAST(tasks.id AS TEXT)
1079
+ )
1080
+ `);
1081
+ getDb().run(`
1082
+ UPDATE tasks
1083
+ SET project_id = (
1084
+ SELECT p.id
1085
+ FROM projects p
1086
+ WHERE p.scope = 'project' AND p.issue_repo = tasks.repo
1087
+ ORDER BY p.created_at ASC, p.id ASC
1088
+ LIMIT 1
1089
+ )
1090
+ WHERE kind = 'deliverable'
1091
+ AND (project_id IS NULL OR trim(project_id) = '')
1092
+ AND repo IS NOT NULL AND trim(repo) <> ''
1093
+ AND EXISTS (
1094
+ SELECT 1 FROM projects p
1095
+ WHERE p.scope = 'project' AND p.issue_repo = tasks.repo
1096
+ )
1097
+ `);
1059
1098
 
1060
1099
  getDb().run(`
1061
1100
  CREATE TABLE IF NOT EXISTS task_dependencies (
package/src/db/plans.ts CHANGED
@@ -8,6 +8,7 @@ interface PlanRow {
8
8
  thread_id: number;
9
9
  team_id: string | null;
10
10
  channel: string | null;
11
+ archived: number;
11
12
  version: number;
12
13
  title: string | null;
13
14
  objective: string | null;
@@ -52,6 +53,7 @@ function rowToPlan(row: PlanRow): Plan {
52
53
  threadId: String(row.thread_id),
53
54
  ...(row.team_id ? { teamId: row.team_id } : {}),
54
55
  ...(row.channel ? { channel: row.channel } : {}),
56
+ archived: Boolean(row.archived),
55
57
  version: row.version,
56
58
  ...(row.title ? { title: row.title } : {}),
57
59
  ...(row.objective ? { objective: row.objective } : {}),
@@ -189,6 +191,15 @@ export function replacePlan(input: Plan, changedBy?: string): Plan {
189
191
  return getPlan(input.id)!;
190
192
  }
191
193
 
194
+ export function setPlanArchived(id: string, archived: boolean): Plan {
195
+ const existing = getPlan(id);
196
+ if (!existing) throw new ValidationError(`plan ${id} not found`);
197
+ if (existing.archived === archived) return existing;
198
+ const now = Date.now();
199
+ getDb().query("UPDATE plans SET archived = ?, updated_at = ? WHERE id = ?").run(archived ? 1 : 0, now, id);
200
+ return getPlan(id)!;
201
+ }
202
+
192
203
  export function deletePlan(id: string, changedBy?: string): boolean {
193
204
  const existing = getPlan(id);
194
205
  if (!existing) return false;
package/src/db/schema.ts CHANGED
@@ -304,7 +304,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
304
304
  CREATE INDEX IF NOT EXISTS idx_project_edges_to ON project_edges(to_project, kind);
305
305
  CREATE TABLE IF NOT EXISTS plans (
306
306
  id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, team_id TEXT, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
307
- title TEXT, objective TEXT, nodes TEXT NOT NULL DEFAULT '[]', edges TEXT NOT NULL DEFAULT '[]',
307
+ archived INTEGER NOT NULL DEFAULT 0, title TEXT, objective TEXT, nodes TEXT NOT NULL DEFAULT '[]', edges TEXT NOT NULL DEFAULT '[]',
308
308
  created_by TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
309
309
  );
310
310
  CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_thread_channel ON plans(thread_id, coalesce(channel, ''));
@@ -657,6 +657,8 @@ export function initDb(path: string = "agent-relay.db"): Database {
657
657
  commits TEXT,
658
658
  done_when TEXT,
659
659
  team_id TEXT,
660
+ project_id TEXT,
661
+ repo TEXT,
660
662
  created_by TEXT,
661
663
  owner_agent_id TEXT,
662
664
  dispatch TEXT,
@@ -28,6 +28,7 @@ import {
28
28
  type TaskGateState,
29
29
  type TaskGateStatus,
30
30
  type TaskIssueLink,
31
+ type TaskListItem,
31
32
  type TaskStatus,
32
33
  } from "../types";
33
34
  import { parseJson } from "../utils";
@@ -38,6 +39,7 @@ import { latestTaskDispatchDecision, taskBoundWorkerModel, taskDispatchWorkShape
38
39
  import { TASK_SELECT, insertTaskEvent, DELIVERABLE_TASK_KIND } from "./task-core.ts";
39
40
  import { associateIssue, listIssuesForEntity, removeAssociation } from "./issue-associations.ts";
40
41
  import { resolveIssueRefFromString } from "./issue-refs.ts";
42
+ import { getProject } from "./projects.ts";
41
43
 
42
44
  // Execution-status axis (#834): the existing claim FSM vocabulary, reused so there is no
43
45
  // enum break. not-started=open, working=in_progress, plus claimed/blocked/done/failed/canceled.
@@ -100,6 +102,38 @@ function cleanDispatch(value: unknown): TaskDispatch | undefined {
100
102
  return out;
101
103
  }
102
104
 
105
+ function projectForRepo(repo: string | undefined): string | undefined {
106
+ const cleaned = repo?.trim();
107
+ if (!cleaned) return undefined;
108
+ const row = getDb()
109
+ .query("SELECT id FROM projects WHERE scope = 'project' AND issue_repo = ? ORDER BY created_at ASC, id ASC LIMIT 1")
110
+ .get(cleaned) as { id: string } | undefined;
111
+ return row?.id;
112
+ }
113
+
114
+ function cleanTaskProject(input: { projectId?: unknown; repo?: unknown }): { projectId?: string; repo?: string } {
115
+ const projectId = optionalText(input.projectId, "projectId", 120);
116
+ const project = projectId ? getProject(projectId) : null;
117
+ const repo = optionalText(input.repo, "repo", 500) ?? project?.issueRepo;
118
+ return {
119
+ ...(projectId ? { projectId } : repo ? { projectId: projectForRepo(repo) } : {}),
120
+ ...(repo ? { repo } : {}),
121
+ };
122
+ }
123
+
124
+ function fillTaskProjectFromRepo(id: number, repo: string, now = Date.now()): void {
125
+ const cleaned = optionalText(repo, "repo", 500);
126
+ if (!cleaned) return;
127
+ const projectId = projectForRepo(cleaned);
128
+ getDb().query(`
129
+ UPDATE tasks
130
+ SET repo = COALESCE(NULLIF(trim(repo), ''), ?),
131
+ project_id = COALESCE(NULLIF(trim(project_id), ''), ?),
132
+ updated_at = ?
133
+ WHERE id = ?
134
+ `).run(cleaned, projectId ?? null, now, id);
135
+ }
136
+
103
137
  function getRawTask(id: number): Task | null {
104
138
  const row = getDb().query(`${TASK_SELECT} WHERE id = ?`).get(id) as any;
105
139
  return row ? rowToTask(row) : null;
@@ -119,6 +153,8 @@ export interface CreateDeliverableTaskInput {
119
153
  context?: string;
120
154
  doneWhen?: string;
121
155
  teamId?: string;
156
+ projectId?: string;
157
+ repo?: string;
122
158
  createdBy: string;
123
159
  source?: string;
124
160
  target?: string;
@@ -134,13 +170,14 @@ export function createDeliverableTask(input: CreateDeliverableTaskInput): TaskDe
134
170
  const dispatch = cleanDispatch(input.dispatch);
135
171
  const complexity = cleanTaskComplexity(input.complexity ?? dispatch?.complexity, "complexity") ?? DEFAULT_TASK_COMPLEXITY;
136
172
  const context = optionalText(input.context, "context", 1_000_000);
173
+ const taskProject = cleanTaskProject(input);
137
174
  // Legacy NOT-NULL columns have no DB defaults (#834 finding 6) — set them explicitly.
138
175
  const result = getDb().query(`
139
176
  INSERT INTO tasks (
140
177
  source, title, body, severity, status, target, occurrence_count, metadata,
141
178
  created_at, updated_at, last_seen_at,
142
- kind, intent, complexity, context, latest_steer, deliverable_stage, done_when, team_id, created_by, dispatch
143
- ) VALUES (?, ?, ?, 'info', 'open', ?, 1, '{}', ?, ?, ?, '${DELIVERABLE_TASK_KIND}', ?, ?, ?, ?, 'none', ?, ?, ?, ?)
179
+ kind, intent, complexity, context, latest_steer, deliverable_stage, done_when, team_id, project_id, repo, created_by, dispatch
180
+ ) VALUES (?, ?, ?, 'info', 'open', ?, 1, '{}', ?, ?, ?, '${DELIVERABLE_TASK_KIND}', ?, ?, ?, ?, 'none', ?, ?, ?, ?, ?, ?)
144
181
  `).run(
145
182
  optionalText(input.source, "source", 200) ?? "coordinator",
146
183
  title,
@@ -153,6 +190,8 @@ export function createDeliverableTask(input: CreateDeliverableTaskInput): TaskDe
153
190
  optionalText(input.latestSteer, "latestSteer", 1_000_000) ?? null,
154
191
  optionalText(input.doneWhen, "doneWhen", 1_000_000) ?? null,
155
192
  optionalText(input.teamId, "teamId", 200) ?? null,
193
+ taskProject.projectId ?? null,
194
+ taskProject.repo ?? null,
156
195
  createdBy,
157
196
  dispatch ? JSON.stringify(dispatch) : null,
158
197
  );
@@ -163,7 +202,7 @@ export function createDeliverableTask(input: CreateDeliverableTaskInput): TaskDe
163
202
  severity: "info",
164
203
  title: `Task created: ${title}`,
165
204
  body: intent,
166
- metadata: { createdBy, complexity, ...(context ? { context } : {}), ...(dispatch ? { dispatch } : {}) },
205
+ metadata: { createdBy, complexity, ...(context ? { context } : {}), ...(taskProject.projectId ? { projectId: taskProject.projectId } : {}), ...(taskProject.repo ? { repo: taskProject.repo } : {}), ...(dispatch ? { dispatch } : {}) },
167
206
  }, now);
168
207
  return getTaskDetail(id)!;
169
208
  }
@@ -177,6 +216,8 @@ export interface UpdateDeliverableTaskInput {
177
216
  doneWhen?: string;
178
217
  dispatch?: TaskDispatch;
179
218
  teamId?: string;
219
+ projectId?: string;
220
+ repo?: string;
180
221
  actor?: string;
181
222
  }
182
223
 
@@ -192,6 +233,11 @@ export function updateDeliverableTask(id: number, input: UpdateDeliverableTaskIn
192
233
  if (input.latestSteer !== undefined) push("latest_steer", optionalText(input.latestSteer, "latestSteer", 1_000_000) ?? null);
193
234
  if (input.doneWhen !== undefined) push("done_when", optionalText(input.doneWhen, "doneWhen", 1_000_000) ?? null);
194
235
  if (input.teamId !== undefined) push("team_id", optionalText(input.teamId, "teamId", 200) ?? null);
236
+ if (input.projectId !== undefined || input.repo !== undefined) {
237
+ const taskProject = cleanTaskProject(input);
238
+ if (input.projectId !== undefined) push("project_id", taskProject.projectId ?? null);
239
+ if (input.repo !== undefined) push("repo", taskProject.repo ?? null);
240
+ }
195
241
  if (input.dispatch !== undefined) { const d = cleanDispatch(input.dispatch); push("dispatch", d ? JSON.stringify(d) : null); }
196
242
  const now = Date.now();
197
243
  if (sets.length > 0) {
@@ -424,6 +470,7 @@ export function addTaskRef(id: number, input: AddTaskRefInput): TaskDetail {
424
470
  if (input.issue) {
425
471
  const ref = resolveIssueRefFromString({ ref: input.issue, defaultRepo: optionalText(input.defaultRepo, "defaultRepo", 500) });
426
472
  associateIssue(ref.id, "task", taskEntityId(String(id)));
473
+ fillTaskProjectFromRepo(id, ref.repo, now);
427
474
  insertTaskEvent(id, {
428
475
  source: input.actor ?? "agent-relay",
429
476
  type: "ref.issue.added",
@@ -619,6 +666,8 @@ export interface ListDeliverableTasksFilter {
619
666
  teamId?: string;
620
667
  createdBy?: string;
621
668
  ownerAgentId?: string;
669
+ projectId?: string;
670
+ repo?: string;
622
671
  limit?: number;
623
672
  }
624
673
 
@@ -630,11 +679,37 @@ export function listDeliverableTasks(filter: ListDeliverableTasksFilter = {}): T
630
679
  if (filter.teamId) { conditions.push("team_id = ?"); params.push(filter.teamId); }
631
680
  if (filter.createdBy) { conditions.push("created_by = ?"); params.push(filter.createdBy); }
632
681
  if (filter.ownerAgentId) { conditions.push("owner_agent_id = ?"); params.push(filter.ownerAgentId); }
682
+ if (filter.projectId) { conditions.push("project_id = ?"); params.push(filter.projectId); }
683
+ if (filter.repo) { conditions.push("repo = ?"); params.push(filter.repo); }
633
684
  const limit = Math.min(Math.max(filter.limit ?? 100, 1), 500);
634
685
  params.push(limit);
635
686
  return (getDb().query(`${TASK_SELECT} WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC LIMIT ?`).all(...params as any[]) as any[]).map(rowToTask);
636
687
  }
637
688
 
689
+ function taskListItem(task: Task): TaskListItem {
690
+ return {
691
+ id: task.id,
692
+ label: task.title,
693
+ title: task.title,
694
+ status: task.status,
695
+ kind: task.kind,
696
+ ...(task.deliverableStage ? { deliverableStage: task.deliverableStage } : {}),
697
+ ...(task.teamId ? { teamId: task.teamId } : {}),
698
+ ...(task.projectId ? { projectId: task.projectId } : {}),
699
+ ...(task.repo ? { repo: task.repo } : {}),
700
+ ...(task.createdBy ? { createdBy: task.createdBy } : {}),
701
+ ...(task.ownerAgentId ? { ownerAgentId: task.ownerAgentId } : {}),
702
+ ...(task.branch ? { branch: task.branch } : {}),
703
+ createdAt: task.createdAt,
704
+ updatedAt: task.updatedAt,
705
+ ...(task.reapedAt ? { reapedAt: task.reapedAt } : {}),
706
+ };
707
+ }
708
+
709
+ export function listDeliverableTaskSummaries(filter: ListDeliverableTasksFilter = {}): TaskListItem[] {
710
+ return listDeliverableTasks(filter).map(taskListItem);
711
+ }
712
+
638
713
  // Task-aware issue resolution for the land→close bridge (#833/#834 finding 4). Deliverable
639
714
  // Tasks own `branch`, so a landed branch resolves to its Task(s). The land WIRING is #831;
640
715
  // this is the resolution path + entity-type the bridge needs to be ABLE to fire. The issue
package/src/mcp/index.ts CHANGED
@@ -54,7 +54,7 @@ export const RELAY_TOOL_GROUPS: Record<string, readonly string[]> = {
54
54
  artifacts: ["relay_upload_artifact", "relay_attach_artifact"],
55
55
  workspace: ["relay_workspace_status", "relay_workspace_ready", "relay_workspace_deps", "relay_workspace_list", "relay_workspace_claim", "relay_workspace_release", "relay_workspace_land", "relay_merge_pause", "relay_merge_pauses", "relay_merge_renew", "relay_merge_resume"],
56
56
  "spawn-lifecycle": ["relay_spawn_agent", "relay_spawn_targets", "relay_my_agents", "relay_shutdown_agent", "relay_agent_action", "relay_agent_status", "relay_agent_crash_report", "relay_schedule_timer", "relay_list_timers", "relay_cancel_timer"],
57
- plans: ["relay_plan_create", "relay_plan_add_node", "relay_plan_update_node", "relay_plan_add_edge", "relay_plan_remove_node", "relay_plan_remove_edge", "relay_plan_delete", "relay_plan_get"],
57
+ plans: ["relay_plan_create", "relay_plan_add_node", "relay_plan_update_node", "relay_plan_add_edge", "relay_plan_remove_node", "relay_plan_remove_edge", "relay_plan_archive", "relay_plan_delete", "relay_plan_get"],
58
58
  tasks: ["relay_task_create", "relay_task_get", "relay_task_list", "relay_task_update", "relay_task_set_status", "relay_task_set_stage", "relay_task_record_gate", "relay_task_add_dependency", "relay_task_remove_dependency", "relay_task_add_ref", "relay_task_remove_ref", "relay_task_assign_agent", "relay_task_history", "relay_task_memory_read", "relay_task_memory_replace_working_state", "relay_task_memory_append_ruled_out", "relay_task_memory_append_decision", "relay_task_dispatch", "relay_task_comment", "relay_task_close_bridge", "relay_release_readiness"],
59
59
  teams: ["relay_team_get", "relay_team_set_brief", "relay_team_claim", "relay_team_dissolve"],
60
60
  blackboard: ["relay_blackboard_write", "relay_blackboard_get", "relay_blackboard_read", "relay_blackboard_search", "relay_blackboard_retract"],
@@ -4,6 +4,7 @@ import { authContextFromMcp } from "./services/auth-context";
4
4
  import {
5
5
  addPlanEdge,
6
6
  addPlanNode,
7
+ archivePlanGraph,
7
8
  bindPlanNodeToTask,
8
9
  createPlan,
9
10
  deletePlanGraph,
@@ -98,6 +99,7 @@ export const PLAN_TOOLS = [
98
99
  { name: "relay_plan_add_edge", description: "Add a dependency edge to a plan DAG; rejects cycles.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" }, from: { type: "string" }, to: { type: "string" } }, required: ["planId", "from", "to"], additionalProperties: false } },
99
100
  { name: "relay_plan_remove_node", description: "Remove a node and its connected edges from a plan DAG.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" }, nodeId: { type: "string" } }, required: ["planId", "nodeId"], additionalProperties: false } },
100
101
  { name: "relay_plan_remove_edge", description: "Remove a dependency edge from a plan DAG.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" }, from: { type: "string" }, to: { type: "string" } }, required: ["planId", "from", "to"], additionalProperties: false } },
102
+ { name: "relay_plan_archive", description: "Soft-archive or unarchive a persisted plan DAG without deleting its history.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" }, archived: { type: "boolean", default: true } }, required: ["planId"], additionalProperties: false } },
101
103
  { name: "relay_plan_delete", description: "Delete a persisted plan DAG and archive its latest version.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" } }, required: ["planId"], additionalProperties: false } },
102
104
  {
103
105
  name: "relay_plan_bind_node",
@@ -135,6 +137,7 @@ export function relayPlanTool(auth: McpAuthLike, name: string, args: Record<stri
135
137
  if (name === "relay_plan_add_edge") return addPlanEdge(stringArg(args.planId, "planId"), { from: args.from, to: args.to }, ctx) as unknown as Record<string, unknown>;
136
138
  if (name === "relay_plan_remove_node") return removePlanNode(stringArg(args.planId, "planId"), stringArg(args.nodeId, "nodeId"), ctx) as unknown as Record<string, unknown>;
137
139
  if (name === "relay_plan_remove_edge") return removePlanEdge(stringArg(args.planId, "planId"), { from: args.from, to: args.to }, ctx) as unknown as Record<string, unknown>;
140
+ if (name === "relay_plan_archive") return archivePlanGraph(stringArg(args.planId, "planId"), args.archived !== false, ctx) as unknown as Record<string, unknown>;
138
141
  if (name === "relay_plan_delete") return deletePlanGraph(stringArg(args.planId, "planId"), ctx);
139
142
  if (name === "relay_plan_bind_node") {
140
143
  return bindPlanNodeToTask(