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.
- package/docs/openapi.json +1 -1
- package/package.json +3 -3
- package/public/assets/MessageBubble-BJllE_2c.js +4 -0
- package/public/assets/MessageBubble-BJllE_2c.js.map +1 -0
- package/public/assets/{PlanGraphPanel-C17_n4Ff.js → PlanGraphPanel-CoK024n9.js} +2 -2
- package/public/assets/{PlanGraphPanel-C17_n4Ff.js.map → PlanGraphPanel-CoK024n9.js.map} +1 -1
- package/public/assets/{activity-G-s2N4eU.js → activity-CBjg7NSP.js} +2 -2
- package/public/assets/{activity-G-s2N4eU.js.map → activity-CBjg7NSP.js.map} +1 -1
- package/public/assets/{agent-profiles-v4CcpJWT.js → agent-profiles-DzkjQaCS.js} +2 -2
- package/public/assets/{agent-profiles-v4CcpJWT.js.map → agent-profiles-DzkjQaCS.js.map} +1 -1
- package/public/assets/{agents-CuEoaCLs.js → agents-Bu1i1jH-.js} +2 -2
- package/public/assets/{agents-CuEoaCLs.js.map → agents-Bu1i1jH-.js.map} +1 -1
- package/public/assets/{analytics-Dvg0J33b.js → analytics-Cc2GoEq_.js} +2 -2
- package/public/assets/{analytics-Dvg0J33b.js.map → analytics-Cc2GoEq_.js.map} +1 -1
- package/public/assets/{automation-wXSbBEei.js → automation-CcMveyvi.js} +2 -2
- package/public/assets/{automation-wXSbBEei.js.map → automation-CcMveyvi.js.map} +1 -1
- package/public/assets/{branch-state-badge-YzkNCHyZ.js → branch-state-badge-CYWBVZDI.js} +2 -2
- package/public/assets/{branch-state-badge-YzkNCHyZ.js.map → branch-state-badge-CYWBVZDI.js.map} +1 -1
- package/public/assets/{channels-Vl9YHoGK.js → channels-IdC7jyLt.js} +2 -2
- package/public/assets/{channels-Vl9YHoGK.js.map → channels-IdC7jyLt.js.map} +1 -1
- package/public/assets/{chat-yiXwg4kh.js → chat-CP4x6Gcr.js} +2 -2
- package/public/assets/{chat-yiXwg4kh.js.map → chat-CP4x6Gcr.js.map} +1 -1
- package/public/assets/{connectors-C33KsouZ.js → connectors-Dngl_FJz.js} +2 -2
- package/public/assets/{connectors-C33KsouZ.js.map → connectors-Dngl_FJz.js.map} +1 -1
- package/public/assets/formatted-body-impl-CBHoD82a.js +3 -0
- package/public/assets/formatted-body-impl-CBHoD82a.js.map +1 -0
- package/public/assets/{index-DYwuv_Na.css → index-DcmG5gLR.css} +1 -1
- package/public/assets/{index-4XRZ-n_A.js → index-DocqZ4Q0.js} +6 -6
- package/public/assets/{index-4XRZ-n_A.js.map → index-DocqZ4Q0.js.map} +1 -1
- package/public/assets/{insights-C5Ft13cw.js → insights-Du9X-nwA.js} +2 -2
- package/public/assets/{insights-C5Ft13cw.js.map → insights-Du9X-nwA.js.map} +1 -1
- package/public/assets/{integrations-DR3pboEG.js → integrations-BlSadMFC.js} +2 -2
- package/public/assets/{integrations-DR3pboEG.js.map → integrations-BlSadMFC.js.map} +1 -1
- package/public/assets/{maintenance-Ddl6-McO.js → maintenance-KeCPGZG4.js} +2 -2
- package/public/assets/{maintenance-Ddl6-McO.js.map → maintenance-KeCPGZG4.js.map} +1 -1
- package/public/assets/{managed-agents-pLThsagU.js → managed-agents-YKqARLQn.js} +2 -2
- package/public/assets/{managed-agents-pLThsagU.js.map → managed-agents-YKqARLQn.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CiENso5E.js → markdown-preview-impl-Bo8ql-lO.js} +2 -2
- package/public/assets/{markdown-preview-impl-CiENso5E.js.map → markdown-preview-impl-Bo8ql-lO.js.map} +1 -1
- package/public/assets/{memory-CSzdP2o2.js → memory-BlftQACU.js} +2 -2
- package/public/assets/{memory-CSzdP2o2.js.map → memory-BlftQACU.js.map} +1 -1
- package/public/assets/{messages-1ton_JT5.js → messages-DQu0Np7r.js} +2 -2
- package/public/assets/{messages-1ton_JT5.js.map → messages-DQu0Np7r.js.map} +1 -1
- package/public/assets/{orchestrators-C2kC07Qm.js → orchestrators-CW00iA0W.js} +2 -2
- package/public/assets/{orchestrators-C2kC07Qm.js.map → orchestrators-CW00iA0W.js.map} +1 -1
- package/public/assets/{overview-CXc2vQRQ.js → overview-DyiH9eVP.js} +2 -2
- package/public/assets/{overview-CXc2vQRQ.js.map → overview-DyiH9eVP.js.map} +1 -1
- package/public/assets/{pairs-DLfFKNEf.js → pairs-Ee8Npq67.js} +2 -2
- package/public/assets/{pairs-DLfFKNEf.js.map → pairs-Ee8Npq67.js.map} +1 -1
- package/public/assets/{projects-TM20JuNI.js → projects-CqeJsfB6.js} +2 -2
- package/public/assets/{projects-TM20JuNI.js.map → projects-CqeJsfB6.js.map} +1 -1
- package/public/assets/{prompt-settings-DWYbD9TA.js → prompt-settings-0fHJtGD5.js} +2 -2
- package/public/assets/{prompt-settings-DWYbD9TA.js.map → prompt-settings-0fHJtGD5.js.map} +1 -1
- package/public/assets/{registry-BWRVKBhU.js → registry-QTLN8Jy-.js} +2 -2
- package/public/assets/{registry-BWRVKBhU.js.map → registry-QTLN8Jy-.js.map} +1 -1
- package/public/assets/{security-DcNNqoA4.js → security-B0FTbGXU.js} +2 -2
- package/public/assets/{security-DcNNqoA4.js.map → security-B0FTbGXU.js.map} +1 -1
- package/public/assets/{select-DkpcyiTY.js → select-DhY0OvwP.js} +2 -2
- package/public/assets/{select-DkpcyiTY.js.map → select-DhY0OvwP.js.map} +1 -1
- package/public/assets/{settings-C9-w25pF.js → settings-B2UEpdBi.js} +2 -2
- package/public/assets/{settings-C9-w25pF.js.map → settings-B2UEpdBi.js.map} +1 -1
- package/public/assets/{store-BZ2N51To.js → store-DwgQJjEv.js} +2 -2
- package/public/assets/store-DwgQJjEv.js.map +1 -0
- package/public/assets/tasks-BvDfBcxZ.js +2 -0
- package/public/assets/tasks-BvDfBcxZ.js.map +1 -0
- package/public/assets/{teams-CQZ6a8A5.js → teams-CIqsg55S.js} +2 -2
- package/public/assets/{teams-CQZ6a8A5.js.map → teams-CIqsg55S.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-B1A49Vs2.js → terminal-viewer-impl-Cig30gnt.js} +2 -2
- package/public/assets/{terminal-viewer-impl-B1A49Vs2.js.map → terminal-viewer-impl-Cig30gnt.js.map} +1 -1
- package/public/assets/types-DSwhOFcJ.js.map +1 -1
- package/public/assets/{user-avatar-C3eL_il0.js → user-avatar-Ct6HEqZm.js} +2 -2
- package/public/assets/{user-avatar-C3eL_il0.js.map → user-avatar-Ct6HEqZm.js.map} +1 -1
- package/public/assets/{work-queue-IfxOcGe5.js → work-queue-XWcDwe8G.js} +2 -2
- package/public/assets/{work-queue-IfxOcGe5.js.map → work-queue-XWcDwe8G.js.map} +1 -1
- package/public/assets/{workspaces-gPEVu-L5.js → workspaces-EI4N2N7L.js} +2 -2
- package/public/assets/{workspaces-gPEVu-L5.js.map → workspaces-EI4N2N7L.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/branch-landed.ts +14 -1
- package/src/db/mappers.ts +3 -0
- package/src/db/migrations.ts +39 -0
- package/src/db/schema.ts +2 -0
- package/src/db/task-entity.ts +78 -3
- package/src/mcp-task-tools.ts +16 -7
- package/src/routes/tasks.ts +13 -5
- package/src/services/spawn-agent.ts +25 -22
- package/src/services/task-entity.ts +6 -1
- package/public/assets/MessageBubble-Dy37JuVw.js +0 -4
- package/public/assets/MessageBubble-Dy37JuVw.js.map +0 -1
- package/public/assets/formatted-body-impl-CfnhWZAX.js +0 -3
- package/public/assets/formatted-body-impl-CfnhWZAX.js.map +0 -1
- package/public/assets/store-BZ2N51To.js.map +0 -1
- package/public/assets/tasks-DuH_JQJM.js +0 -2
- package/public/assets/tasks-DuH_JQJM.js.map +0 -1
package/src/branch-landed.ts
CHANGED
|
@@ -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, {}) } : {}),
|
package/src/db/migrations.ts
CHANGED
|
@@ -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
package/src/db/task-entity.ts
CHANGED
|
@@ -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-task-tools.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
assignTaskEntityAgent,
|
|
13
13
|
createTaskEntity,
|
|
14
14
|
getTaskEntity,
|
|
15
|
-
|
|
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: ["
|
|
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
|
|
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:
|
|
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
|
}
|
package/src/routes/tasks.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
378
|
+
createdBy: requestedBy,
|
|
360
379
|
source: "spawn",
|
|
361
|
-
target:
|
|
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");
|