agent-relay-server 0.115.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 (94) hide show
  1. package/docs/openapi.json +1 -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-C17_n4Ff.js → PlanGraphPanel-CoK024n9.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-C17_n4Ff.js.map → PlanGraphPanel-CoK024n9.js.map} +1 -1
  7. package/public/assets/{activity-G-s2N4eU.js → activity-CBjg7NSP.js} +2 -2
  8. package/public/assets/{activity-G-s2N4eU.js.map → activity-CBjg7NSP.js.map} +1 -1
  9. package/public/assets/{agent-profiles-v4CcpJWT.js → agent-profiles-DzkjQaCS.js} +2 -2
  10. package/public/assets/{agent-profiles-v4CcpJWT.js.map → agent-profiles-DzkjQaCS.js.map} +1 -1
  11. package/public/assets/{agents-CuEoaCLs.js → agents-Bu1i1jH-.js} +2 -2
  12. package/public/assets/{agents-CuEoaCLs.js.map → agents-Bu1i1jH-.js.map} +1 -1
  13. package/public/assets/{analytics-Dvg0J33b.js → analytics-Cc2GoEq_.js} +2 -2
  14. package/public/assets/{analytics-Dvg0J33b.js.map → analytics-Cc2GoEq_.js.map} +1 -1
  15. package/public/assets/{automation-wXSbBEei.js → automation-CcMveyvi.js} +2 -2
  16. package/public/assets/{automation-wXSbBEei.js.map → automation-CcMveyvi.js.map} +1 -1
  17. package/public/assets/{branch-state-badge-YzkNCHyZ.js → branch-state-badge-CYWBVZDI.js} +2 -2
  18. package/public/assets/{branch-state-badge-YzkNCHyZ.js.map → branch-state-badge-CYWBVZDI.js.map} +1 -1
  19. package/public/assets/{channels-Vl9YHoGK.js → channels-IdC7jyLt.js} +2 -2
  20. package/public/assets/{channels-Vl9YHoGK.js.map → channels-IdC7jyLt.js.map} +1 -1
  21. package/public/assets/{chat-yiXwg4kh.js → chat-CP4x6Gcr.js} +2 -2
  22. package/public/assets/{chat-yiXwg4kh.js.map → chat-CP4x6Gcr.js.map} +1 -1
  23. package/public/assets/{connectors-C33KsouZ.js → connectors-Dngl_FJz.js} +2 -2
  24. package/public/assets/{connectors-C33KsouZ.js.map → connectors-Dngl_FJz.js.map} +1 -1
  25. package/public/assets/formatted-body-impl-CBHoD82a.js +3 -0
  26. package/public/assets/formatted-body-impl-CBHoD82a.js.map +1 -0
  27. package/public/assets/{index-DYwuv_Na.css → index-DcmG5gLR.css} +1 -1
  28. package/public/assets/{index-4XRZ-n_A.js → index-DocqZ4Q0.js} +6 -6
  29. package/public/assets/{index-4XRZ-n_A.js.map → index-DocqZ4Q0.js.map} +1 -1
  30. package/public/assets/{insights-C5Ft13cw.js → insights-Du9X-nwA.js} +2 -2
  31. package/public/assets/{insights-C5Ft13cw.js.map → insights-Du9X-nwA.js.map} +1 -1
  32. package/public/assets/{integrations-DR3pboEG.js → integrations-BlSadMFC.js} +2 -2
  33. package/public/assets/{integrations-DR3pboEG.js.map → integrations-BlSadMFC.js.map} +1 -1
  34. package/public/assets/{maintenance-Ddl6-McO.js → maintenance-KeCPGZG4.js} +2 -2
  35. package/public/assets/{maintenance-Ddl6-McO.js.map → maintenance-KeCPGZG4.js.map} +1 -1
  36. package/public/assets/{managed-agents-pLThsagU.js → managed-agents-YKqARLQn.js} +2 -2
  37. package/public/assets/{managed-agents-pLThsagU.js.map → managed-agents-YKqARLQn.js.map} +1 -1
  38. package/public/assets/{markdown-preview-impl-CiENso5E.js → markdown-preview-impl-Bo8ql-lO.js} +2 -2
  39. package/public/assets/{markdown-preview-impl-CiENso5E.js.map → markdown-preview-impl-Bo8ql-lO.js.map} +1 -1
  40. package/public/assets/{memory-CSzdP2o2.js → memory-BlftQACU.js} +2 -2
  41. package/public/assets/{memory-CSzdP2o2.js.map → memory-BlftQACU.js.map} +1 -1
  42. package/public/assets/{messages-1ton_JT5.js → messages-DQu0Np7r.js} +2 -2
  43. package/public/assets/{messages-1ton_JT5.js.map → messages-DQu0Np7r.js.map} +1 -1
  44. package/public/assets/{orchestrators-C2kC07Qm.js → orchestrators-CW00iA0W.js} +2 -2
  45. package/public/assets/{orchestrators-C2kC07Qm.js.map → orchestrators-CW00iA0W.js.map} +1 -1
  46. package/public/assets/{overview-CXc2vQRQ.js → overview-DyiH9eVP.js} +2 -2
  47. package/public/assets/{overview-CXc2vQRQ.js.map → overview-DyiH9eVP.js.map} +1 -1
  48. package/public/assets/{pairs-DLfFKNEf.js → pairs-Ee8Npq67.js} +2 -2
  49. package/public/assets/{pairs-DLfFKNEf.js.map → pairs-Ee8Npq67.js.map} +1 -1
  50. package/public/assets/{projects-TM20JuNI.js → projects-CqeJsfB6.js} +2 -2
  51. package/public/assets/{projects-TM20JuNI.js.map → projects-CqeJsfB6.js.map} +1 -1
  52. package/public/assets/{prompt-settings-DWYbD9TA.js → prompt-settings-0fHJtGD5.js} +2 -2
  53. package/public/assets/{prompt-settings-DWYbD9TA.js.map → prompt-settings-0fHJtGD5.js.map} +1 -1
  54. package/public/assets/{registry-BWRVKBhU.js → registry-QTLN8Jy-.js} +2 -2
  55. package/public/assets/{registry-BWRVKBhU.js.map → registry-QTLN8Jy-.js.map} +1 -1
  56. package/public/assets/{security-DcNNqoA4.js → security-B0FTbGXU.js} +2 -2
  57. package/public/assets/{security-DcNNqoA4.js.map → security-B0FTbGXU.js.map} +1 -1
  58. package/public/assets/{select-DkpcyiTY.js → select-DhY0OvwP.js} +2 -2
  59. package/public/assets/{select-DkpcyiTY.js.map → select-DhY0OvwP.js.map} +1 -1
  60. package/public/assets/{settings-C9-w25pF.js → settings-B2UEpdBi.js} +2 -2
  61. package/public/assets/{settings-C9-w25pF.js.map → settings-B2UEpdBi.js.map} +1 -1
  62. package/public/assets/{store-BZ2N51To.js → store-DwgQJjEv.js} +2 -2
  63. package/public/assets/store-DwgQJjEv.js.map +1 -0
  64. package/public/assets/tasks-BvDfBcxZ.js +2 -0
  65. package/public/assets/tasks-BvDfBcxZ.js.map +1 -0
  66. package/public/assets/{teams-CQZ6a8A5.js → teams-CIqsg55S.js} +2 -2
  67. package/public/assets/{teams-CQZ6a8A5.js.map → teams-CIqsg55S.js.map} +1 -1
  68. package/public/assets/{terminal-viewer-impl-B1A49Vs2.js → terminal-viewer-impl-Cig30gnt.js} +2 -2
  69. package/public/assets/{terminal-viewer-impl-B1A49Vs2.js.map → terminal-viewer-impl-Cig30gnt.js.map} +1 -1
  70. package/public/assets/types-DSwhOFcJ.js.map +1 -1
  71. package/public/assets/{user-avatar-C3eL_il0.js → user-avatar-Ct6HEqZm.js} +2 -2
  72. package/public/assets/{user-avatar-C3eL_il0.js.map → user-avatar-Ct6HEqZm.js.map} +1 -1
  73. package/public/assets/{work-queue-IfxOcGe5.js → work-queue-XWcDwe8G.js} +2 -2
  74. package/public/assets/{work-queue-IfxOcGe5.js.map → work-queue-XWcDwe8G.js.map} +1 -1
  75. package/public/assets/{workspaces-gPEVu-L5.js → workspaces-EI4N2N7L.js} +2 -2
  76. package/public/assets/{workspaces-gPEVu-L5.js.map → workspaces-EI4N2N7L.js.map} +1 -1
  77. package/public/index.html +3 -3
  78. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  79. package/src/branch-landed.ts +14 -1
  80. package/src/db/mappers.ts +3 -0
  81. package/src/db/migrations.ts +39 -0
  82. package/src/db/schema.ts +2 -0
  83. package/src/db/task-entity.ts +78 -3
  84. package/src/mcp-task-tools.ts +16 -7
  85. package/src/routes/tasks.ts +13 -5
  86. package/src/services/spawn-agent.ts +25 -22
  87. package/src/services/task-entity.ts +6 -1
  88. package/public/assets/MessageBubble-Dy37JuVw.js +0 -4
  89. package/public/assets/MessageBubble-Dy37JuVw.js.map +0 -1
  90. package/public/assets/formatted-body-impl-CfnhWZAX.js +0 -3
  91. package/public/assets/formatted-body-impl-CfnhWZAX.js.map +0 -1
  92. package/public/assets/store-BZ2N51To.js.map +0 -1
  93. package/public/assets/tasks-DuH_JQJM.js +0 -2
  94. package/public/assets/tasks-DuH_JQJM.js.map +0 -1
@@ -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
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, {}) } : {}),
@@ -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/schema.ts CHANGED
@@ -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
@@ -12,7 +12,7 @@ import {
12
12
  assignTaskEntityAgent,
13
13
  createTaskEntity,
14
14
  getTaskEntity,
15
- listTaskEntities,
15
+ listTaskEntitySummaries,
16
16
  recordTaskEntityGate,
17
17
  removeTaskEntityDependency,
18
18
  removeTaskEntityRef,
@@ -80,23 +80,26 @@ export const TASK_TOOLS = [
80
80
  type: "object",
81
81
  properties: {
82
82
  title: { type: "string" },
83
+ label: { type: "string", description: "alias for title; concise task label" },
83
84
  intent: { type: "string", description: "original prompt / why this task exists" },
84
85
  complexity: { type: "string", enum: TASK_DISPATCH_COMPLEXITIES, description: "coarse task complexity; defaults to moderate" },
85
86
  context: { type: "string", description: "optional context needed to understand or execute the task" },
86
87
  doneWhen: { type: "string", description: "exit predicate / acceptance for execution" },
87
88
  teamId: { type: "string" },
89
+ projectId: { type: "string" },
90
+ repo: { type: "string", description: "owner/repo for the task's project/repository" },
88
91
  dispatch: DISPATCH_SCHEMA,
89
92
  issueRefs: { type: "array", items: { type: "string" }, description: "#n, owner/repo#n, GitHub URL, or issue_<id>" },
90
93
  defaultRepo: { type: "string", description: "owner/repo for bare #n issue refs" },
91
94
  dependsOn: { type: "array", items: { type: "integer", minimum: 1 } },
92
95
  },
93
- required: ["title", "intent"],
96
+ required: ["intent"],
94
97
  additionalProperties: false,
95
98
  },
96
99
  },
97
100
  { name: "relay_task_get", description: "Fetch a deliverable Task with its issue links, dependency edges, and current gate state.", requiredScopes: ["task:read"], inputSchema: { type: "object", properties: { taskId: TASK_ID_PROP }, required: ["taskId"], additionalProperties: false } },
98
- { name: "relay_task_list", description: "List deliverable Tasks, optionally filtered by execution status, deliverable stage, or team.", requiredScopes: ["task:read"], inputSchema: { type: "object", properties: { status: { type: "string", enum: DELIVERABLE_STATUS_ENUM }, deliverableStage: { type: "string", enum: TASK_DELIVERABLE_STAGES }, teamId: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 500 } }, additionalProperties: false } },
99
- { name: "relay_task_update", description: "Update a deliverable Task's editable fields (title, intent, complexity, context, latest steer, doneWhen, dispatch, team).", requiredScopes: ["task:write"], inputSchema: { type: "object", properties: { taskId: TASK_ID_PROP, title: { type: "string" }, intent: { type: "string" }, complexity: { type: "string", enum: TASK_DISPATCH_COMPLEXITIES }, context: { type: "string" }, latestSteer: { type: "string" }, doneWhen: { type: "string" }, dispatch: DISPATCH_SCHEMA, teamId: { type: "string" } }, required: ["taskId"], additionalProperties: false } },
101
+ { name: "relay_task_list", description: "List lean deliverable Task summaries, optionally filtered by execution status, deliverable stage, team, project, or repo. Does not return full intent.", requiredScopes: ["task:read"], inputSchema: { type: "object", properties: { status: { type: "string", enum: DELIVERABLE_STATUS_ENUM }, deliverableStage: { type: "string", enum: TASK_DELIVERABLE_STAGES }, teamId: { type: "string" }, projectId: { type: "string" }, repo: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 500 } }, additionalProperties: false } },
102
+ { name: "relay_task_update", description: "Update a deliverable Task's editable fields (title/label, intent, complexity, context, latest steer, doneWhen, dispatch, team, project/repo).", requiredScopes: ["task:write"], inputSchema: { type: "object", properties: { taskId: TASK_ID_PROP, title: { type: "string" }, label: { type: "string" }, intent: { type: "string" }, complexity: { type: "string", enum: TASK_DISPATCH_COMPLEXITIES }, context: { type: "string" }, latestSteer: { type: "string" }, doneWhen: { type: "string" }, dispatch: DISPATCH_SCHEMA, teamId: { type: "string" }, projectId: { type: "string" }, repo: { type: "string" } }, required: ["taskId"], additionalProperties: false } },
100
103
  { name: "relay_task_set_status", description: "Transition execution status (open|claimed|in_progress|blocked|done|failed|canceled). blockedBy is required when status is 'blocked'. Appends to the audit ledger; never touches the claim lease.", requiredScopes: ["task:write"], inputSchema: { type: "object", properties: { taskId: TASK_ID_PROP, status: { type: "string", enum: DELIVERABLE_STATUS_ENUM }, blockedBy: { type: "array", items: BLOCKER_SCHEMA }, note: { type: "string" } }, required: ["taskId", "status"], additionalProperties: false } },
101
104
  { name: "relay_task_set_stage", description: "Set the deliverable-position stage (none→uncommitted→committed-on-branch→landed-on-main→deploying→deployed). Normally driven by the land/deploy pipeline; exposed for manual/test use.", requiredScopes: ["task:write"], inputSchema: { type: "object", properties: { taskId: TASK_ID_PROP, deliverableStage: { type: "string", enum: TASK_DELIVERABLE_STAGES }, branch: { type: "string" }, commit: COMMIT_SCHEMA }, required: ["taskId", "deliverableStage"], additionalProperties: false } },
102
105
  { name: "relay_task_record_gate", description: "Record a quality-gate run (typecheck, test, file-size, api-spec, smoke, …) in the append-only ledger. Idempotent by runId; latest run per gate wins.", requiredScopes: ["task:write"], inputSchema: { type: "object", properties: { taskId: TASK_ID_PROP, gate: { type: "string" }, status: { type: "string", enum: TASK_GATE_STATUSES }, detail: { type: "string" }, commitSha: { type: "string" }, runId: { type: "string" } }, required: ["taskId", "gate", "status"], additionalProperties: false } },
@@ -129,12 +132,14 @@ export function relayTaskTool(auth: McpAuthLike, name: string, args: Record<stri
129
132
 
130
133
  if (name === "relay_task_create") {
131
134
  const created = createTaskEntity({
132
- title: cleanString(args.title, "title", { required: true, max: 500 })!,
135
+ title: cleanString(args.title ?? args.label, "title", { required: true, max: 500 })!,
133
136
  intent: cleanString(args.intent, "intent", { required: true, max: 1_000_000 })!,
134
137
  complexity: optionalEnum(args.complexity, "complexity", TASK_DISPATCH_COMPLEXITIES),
135
138
  context: cleanString(args.context, "context", { max: 1_000_000 }),
136
139
  doneWhen: cleanString(args.doneWhen, "doneWhen", { max: 1_000_000 }),
137
140
  teamId: cleanString(args.teamId, "teamId", { max: 200 }),
141
+ projectId: cleanString(args.projectId, "projectId", { max: 120 }),
142
+ repo: cleanString(args.repo, "repo", { max: 500 }),
138
143
  dispatch: args.dispatch as TaskDispatch | undefined,
139
144
  createdBy: actor,
140
145
  });
@@ -155,17 +160,19 @@ export function relayTaskTool(auth: McpAuthLike, name: string, args: Record<stri
155
160
  }
156
161
  if (name === "relay_task_list") {
157
162
  return {
158
- tasks: listTaskEntities({
163
+ tasks: listTaskEntitySummaries({
159
164
  status: optionalEnum(args.status, "status", DELIVERABLE_STATUS_ENUM),
160
165
  deliverableStage: optionalEnum(args.deliverableStage, "deliverableStage", TASK_DELIVERABLE_STAGES),
161
166
  teamId: cleanString(args.teamId, "teamId", { max: 200 }),
167
+ projectId: cleanString(args.projectId, "projectId", { max: 120 }),
168
+ repo: cleanString(args.repo, "repo", { max: 500 }),
162
169
  limit: typeof args.limit === "number" ? args.limit : undefined,
163
170
  }),
164
171
  };
165
172
  }
166
173
  if (name === "relay_task_update") {
167
174
  return updateTaskEntity(taskIdArg(args.taskId), {
168
- ...(args.title !== undefined ? { title: cleanString(args.title, "title", { max: 500 }) } : {}),
175
+ ...(args.title !== undefined || args.label !== undefined ? { title: cleanString(args.title ?? args.label, "title", { max: 500 }) } : {}),
169
176
  ...(args.intent !== undefined ? { intent: cleanString(args.intent, "intent", { max: 1_000_000 }) ?? "" } : {}),
170
177
  ...(args.complexity !== undefined ? { complexity: optionalEnum(args.complexity, "complexity", TASK_DISPATCH_COMPLEXITIES) } : {}),
171
178
  ...(args.context !== undefined ? { context: cleanString(args.context, "context", { max: 1_000_000 }) ?? "" } : {}),
@@ -173,6 +180,8 @@ export function relayTaskTool(auth: McpAuthLike, name: string, args: Record<stri
173
180
  ...(args.doneWhen !== undefined ? { doneWhen: cleanString(args.doneWhen, "doneWhen", { max: 1_000_000 }) ?? "" } : {}),
174
181
  ...(args.dispatch !== undefined ? { dispatch: args.dispatch as TaskDispatch } : {}),
175
182
  ...(args.teamId !== undefined ? { teamId: cleanString(args.teamId, "teamId", { max: 200 }) ?? "" } : {}),
183
+ ...(args.projectId !== undefined ? { projectId: cleanString(args.projectId, "projectId", { max: 120 }) ?? "" } : {}),
184
+ ...(args.repo !== undefined ? { repo: cleanString(args.repo, "repo", { max: 500 }) ?? "" } : {}),
176
185
  actor,
177
186
  }) as unknown as Record<string, unknown>;
178
187
  }
@@ -9,7 +9,7 @@ import { ValidationError, claimTask, getMessage, getTask, listTaskEvents, listTa
9
9
  import { readTaskMemoryEnvelope } from "../db/task-memory";
10
10
  import {
11
11
  addTaskEntityDependency, addTaskEntityRef, assignTaskEntityAgent, createTaskEntity, getTaskEntity,
12
- listTaskEntities, recordTaskEntityGate, removeTaskEntityDependency, removeTaskEntityRef, setTaskEntityStage, setTaskEntityStatus, taskEntityHistory, updateTaskEntity,
12
+ listTaskEntitySummaries, recordTaskEntityGate, removeTaskEntityDependency, removeTaskEntityRef, setTaskEntityStage, setTaskEntityStatus, taskEntityHistory, updateTaskEntity,
13
13
  } from "../services/task-entity";
14
14
  import { CommentThroughError, commentOnTaskThrough, taskCloseBridge } from "../services/task-issue-weave";
15
15
  import { captureTaskResultMemory, injectMemoryForTaskClaim } from "../memory-service";
@@ -52,11 +52,13 @@ export const getTasks: Handler = (req) => {
52
52
  // durable Tasks (optionally filtered by deliverable stage / team).
53
53
  const kind = url.searchParams.get("kind") ?? undefined;
54
54
  if (kind === "deliverable") {
55
- return json(listTaskEntities({
55
+ return json(listTaskEntitySummaries({
56
56
  status: url.searchParams.get("status") ?? undefined,
57
57
  deliverableStage: url.searchParams.get("deliverableStage") ?? undefined,
58
58
  teamId: url.searchParams.get("teamId") ?? undefined,
59
59
  ownerAgentId: url.searchParams.get("ownerAgentId") ?? undefined,
60
+ projectId: url.searchParams.get("projectId") ?? undefined,
61
+ repo: url.searchParams.get("repo") ?? undefined,
60
62
  limit: limitRaw ?? 100,
61
63
  }));
62
64
  }
@@ -73,11 +75,13 @@ export const getTaskEntities: Handler = (req) => {
73
75
  const url = new URL(req.url);
74
76
  const limitRaw = parseQueryInt(url.searchParams.get("limit"), { min: 1, max: 500 });
75
77
  if (Number.isNaN(limitRaw)) return error("limit must be an integer between 1 and 500");
76
- return json(listTaskEntities({
78
+ return json(listTaskEntitySummaries({
77
79
  status: url.searchParams.get("status") ?? undefined,
78
80
  deliverableStage: url.searchParams.get("deliverableStage") ?? undefined,
79
81
  teamId: url.searchParams.get("teamId") ?? undefined,
80
82
  createdBy: url.searchParams.get("createdBy") ?? undefined,
83
+ projectId: url.searchParams.get("projectId") ?? undefined,
84
+ repo: url.searchParams.get("repo") ?? undefined,
81
85
  limit: limitRaw ?? 100,
82
86
  }));
83
87
  };
@@ -91,12 +95,14 @@ export const postTaskCreate: Handler = async (req) => {
91
95
  if (!isRecord(body)) return error("JSON object body required");
92
96
  try {
93
97
  const created = createTaskEntity({
94
- title: body.title as string,
98
+ title: (body.title ?? body.label) as string,
95
99
  intent: cleanString(body.intent, "intent", { required: true, max: MAX_BODY_BYTES })!,
96
100
  complexity: optionalEnum(body.complexity, "complexity", TASK_DISPATCH_COMPLEXITIES),
97
101
  context: cleanString(body.context, "context", { max: MAX_BODY_BYTES }),
98
102
  doneWhen: cleanString(body.doneWhen, "doneWhen", { max: MAX_BODY_BYTES }),
99
103
  teamId: cleanString(body.teamId, "teamId", { max: 200 }),
104
+ projectId: cleanString(body.projectId, "projectId", { max: 120 }),
105
+ repo: cleanString(body.repo, "repo", { max: 500 }),
100
106
  dispatch: body.dispatch as TaskDispatch | undefined,
101
107
  createdBy: actorOf(req, body.createdBy),
102
108
  });
@@ -155,7 +161,7 @@ export const patchTaskEntity: Handler = async (req, params) => {
155
161
  if (!isRecord(body)) return error("JSON object body required");
156
162
  try {
157
163
  const detail = updateTaskEntity(id, {
158
- ...(body.title !== undefined ? { title: cleanString(body.title, "title", { max: 500 }) } : {}),
164
+ ...(body.title !== undefined || body.label !== undefined ? { title: cleanString(body.title ?? body.label, "title", { max: 500 }) } : {}),
159
165
  ...(body.intent !== undefined ? { intent: cleanString(body.intent, "intent", { max: MAX_BODY_BYTES }) ?? "" } : {}),
160
166
  ...(body.complexity !== undefined ? { complexity: optionalEnum(body.complexity, "complexity", TASK_DISPATCH_COMPLEXITIES) } : {}),
161
167
  ...(body.context !== undefined ? { context: cleanString(body.context, "context", { max: MAX_BODY_BYTES }) ?? "" } : {}),
@@ -163,6 +169,8 @@ export const patchTaskEntity: Handler = async (req, params) => {
163
169
  ...(body.doneWhen !== undefined ? { doneWhen: cleanString(body.doneWhen, "doneWhen", { max: MAX_BODY_BYTES }) ?? "" } : {}),
164
170
  ...(body.dispatch !== undefined ? { dispatch: body.dispatch as TaskDispatch } : {}),
165
171
  ...(body.teamId !== undefined ? { teamId: cleanString(body.teamId, "teamId", { max: 200 }) ?? "" } : {}),
172
+ ...(body.projectId !== undefined ? { projectId: cleanString(body.projectId, "projectId", { max: 120 }) ?? "" } : {}),
173
+ ...(body.repo !== undefined ? { repo: cleanString(body.repo, "repo", { max: 500 }) ?? "" } : {}),
166
174
  actor: actorOf(req),
167
175
  });
168
176
  return json(detail);
@@ -267,7 +267,9 @@ function spawnBrief(input: SpawnAgentInput): string | undefined {
267
267
  return undefined;
268
268
  }
269
269
 
270
- function taskTitleFromBrief(brief: string): string {
270
+ function taskTitleFromBrief(brief: string, label?: string): string {
271
+ const cleanLabel = label?.trim();
272
+ if (cleanLabel) return cleanLabel.length <= 500 ? cleanLabel : `${cleanLabel.slice(0, 497)}...`;
271
273
  const firstLine = brief.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "Spawned work";
272
274
  return firstLine.length <= 500 ? firstLine : `${firstLine.slice(0, 497)}...`;
273
275
  }
@@ -349,16 +351,34 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
349
351
  // outside its cwd prefix, and MCP now honors the same bound.
350
352
  assertResourceAllowed(ctx, { scope: "agent:write", resource: { orchestratorId: orchestrator.id, cwd: resolvedCwd } });
351
353
 
354
+ // Provenance actor: the resolved caller agent when known, else the transport's label, else
355
+ // the raw token actor. Recorded in the command payload, the token mint, and the audit row.
356
+ const requestedBy = callerId ?? taskCreatorAgentId ?? input.requestedBy ?? ctx.actor.id;
357
+
358
+ // #405 — durable project binding: the project whose registered root contains the spawn cwd.
359
+ // Stamped on both the child token and an auto-scaffolded Task row so issueless spawned work
360
+ // stays filterable by project.
361
+ let callerProject = resolveProjectForCwd(resolvedCwd);
362
+ try {
363
+ const seeded = autoSeedProjectForCwd(resolvedCwd, requestedBy);
364
+ callerProject = seeded?.project ?? callerProject;
365
+ if (seeded?.ingestError) {
366
+ console.warn(`project auto-seed failed to ingest CLAUDE.md for ${resolvedCwd}: ${seeded.ingestError.message}`);
367
+ }
368
+ } catch (err) {
369
+ if (!callerProject) console.warn(`project auto-seed skipped for ${resolvedCwd}: ${errMessage(err)}`);
370
+ }
371
+
352
372
  if (shouldAutoScaffoldTask(input, profileName)) {
353
373
  const brief = spawnBrief(input)!;
354
- const taskRequestedBy = callerId ?? input.requestedBy ?? ctx.actor.id;
355
374
  linkedTask = createTaskEntity({
356
- title: taskTitleFromBrief(brief),
375
+ title: taskTitleFromBrief(brief, input.label),
357
376
  intent: brief,
358
377
  latestSteer: input.steer,
359
- createdBy: taskRequestedBy,
378
+ createdBy: requestedBy,
360
379
  source: "spawn",
361
- target: taskRequestedBy,
380
+ target: requestedBy,
381
+ ...(callerProject ? { projectId: callerProject.id, repo: callerProject.issueRepo } : {}),
362
382
  });
363
383
  input = { ...input, taskId: linkedTask.id };
364
384
  autoScaffoldedTask = { id: linkedTask.id, autoScaffolded: true, nudge: AUTO_SCAFFOLD_TASK_NUDGE };
@@ -474,26 +494,9 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
474
494
  };
475
495
  }
476
496
 
477
- // Provenance actor: the resolved caller agent when known, else the transport's label, else
478
- // the raw token actor. Recorded in the command payload, the token mint, and the audit row.
479
- const requestedBy = callerId ?? taskCreatorAgentId ?? input.requestedBy ?? ctx.actor.id;
480
497
  const callerTeam = lineageAgentId ? (lineageAgent?.teamId ? { id: lineageAgent.teamId } : ensureTeam(lineageAgentId)) : undefined;
481
498
  if (lineageAgentId && callerTeam && !lineageAgent?.teamId) setAgentTeam(lineageAgentId, callerTeam.id);
482
499
 
483
- // #405 — durable project binding: the project whose registered root contains the spawn cwd.
484
- // Stamped on the child token's constraints (identity-only, like teamId) so callerProject(auth)
485
- // resolves it. Absent (graceful) when the cwd is inside no registered root.
486
- let callerProject = resolveProjectForCwd(resolvedCwd);
487
- try {
488
- const seeded = autoSeedProjectForCwd(resolvedCwd, requestedBy);
489
- callerProject = seeded?.project ?? callerProject;
490
- if (seeded?.ingestError) {
491
- console.warn(`project auto-seed failed to ingest CLAUDE.md for ${resolvedCwd}: ${seeded.ingestError.message}`);
492
- }
493
- } catch (err) {
494
- if (!callerProject) console.warn(`project auto-seed skipped for ${resolvedCwd}: ${errMessage(err)}`);
495
- }
496
-
497
500
  // #409 + #463 + #557 — the instruction-sources half of the launch-assembly contract.
498
501
  // composeSpawnInstructions is the single home that composes the prompt material handed to
499
502
  // providers (project preamble → profile system → profile append → caller/fork context) and
@@ -8,6 +8,7 @@ import {
8
8
  assignTaskAgent,
9
9
  createDeliverableTask,
10
10
  getTaskDetail,
11
+ listDeliverableTaskSummaries,
11
12
  listDeliverableTasks,
12
13
  listTaskEvents,
13
14
  recordGate,
@@ -28,7 +29,7 @@ import {
28
29
  type UpdateDeliverableTaskInput,
29
30
  } from "../db";
30
31
  import { emitTaskChanged } from "../sse";
31
- import type { Task, TaskDetail, TaskEvent, TaskGateState } from "../types";
32
+ import type { Task, TaskDetail, TaskEvent, TaskGateState, TaskListItem } from "../types";
32
33
  import { removeSubscriptionsForTask } from "../db/notification-subscriptions";
33
34
  import { stageSatisfiesDoneWhen } from "./task-done-when";
34
35
  import { autoSubscribeTaskCoordinator, emitTaskSemanticNotification, taskTransitionForStage, taskTransitionForStatus } from "./task-semantic-events";
@@ -54,6 +55,10 @@ export function listTaskEntities(filter: ListDeliverableTasksFilter = {}): Task[
54
55
  return listDeliverableTasks(filter);
55
56
  }
56
57
 
58
+ export function listTaskEntitySummaries(filter: ListDeliverableTasksFilter = {}): TaskListItem[] {
59
+ return listDeliverableTaskSummaries(filter);
60
+ }
61
+
57
62
  export function updateTaskEntity(id: number, input: UpdateDeliverableTaskInput): TaskDetail {
58
63
  const detail = updateDeliverableTask(id, input);
59
64
  emitTaskChanged(detail, "task.updated");