crewly 1.20.158 → 1.20.160
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/config/constants.ts +13 -0
- package/config/roles/orchestrator/prompt.md +1 -0
- package/config/roles/team-leader/prompt.md +3 -0
- package/config/roles/team-leader/tl-addon.md +3 -0
- package/config/skills/agent/core/project-tickets/SKILL.md +17 -1
- package/config/skills/agent/core/project-tickets/execute.sh +16 -3
- package/config/skills/agent/core/project-tickets/execute.test.sh +11 -0
- package/config/skills/orchestrator/delegate-task/SKILL.md +18 -0
- package/config/skills/orchestrator/delegate-task/execute.sh +23 -5
- package/config/skills/orchestrator/delegate-task/execute.test.sh +44 -1
- package/config/skills/registry.json +55 -4
- package/config/skills/team-leader/delegate-task/SKILL.md +18 -0
- package/config/skills/team-leader/delegate-task/execute.sh +31 -4
- package/config/skills/team-leader/delegate-task/execute.test.sh +59 -0
- package/dist/backend/backend/src/constants.d.ts +59 -3
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +54 -3
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts +8 -0
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js +13 -0
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.d.ts +1 -0
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.js +3 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +73 -3
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/worktree-janitor/worktree-janitor.routes.d.ts +2 -1
- package/dist/backend/backend/src/controllers/worktree-janitor/worktree-janitor.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/worktree-janitor/worktree-janitor.routes.js +4 -2
- package/dist/backend/backend/src/controllers/worktree-janitor/worktree-janitor.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +17 -2
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +9 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/delegation-ticket-route.d.ts +63 -0
- package/dist/backend/backend/src/services/project-tickets/delegation-ticket-route.d.ts.map +1 -0
- package/dist/backend/backend/src/services/project-tickets/delegation-ticket-route.js +80 -0
- package/dist/backend/backend/src/services/project-tickets/delegation-ticket-route.js.map +1 -0
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts +92 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js +255 -4
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js.map +1 -1
- package/dist/backend/backend/src/services/worktree/low-disk-guard.d.ts +109 -0
- package/dist/backend/backend/src/services/worktree/low-disk-guard.d.ts.map +1 -0
- package/dist/backend/backend/src/services/worktree/low-disk-guard.js +148 -0
- package/dist/backend/backend/src/services/worktree/low-disk-guard.js.map +1 -0
- package/dist/backend/backend/src/services/worktree/scratch-janitor.d.ts +140 -0
- package/dist/backend/backend/src/services/worktree/scratch-janitor.d.ts.map +1 -0
- package/dist/backend/backend/src/services/worktree/scratch-janitor.js +249 -0
- package/dist/backend/backend/src/services/worktree/scratch-janitor.js.map +1 -0
- package/dist/backend/backend/src/services/worktree/worktree-janitor.git.d.ts +92 -1
- package/dist/backend/backend/src/services/worktree/worktree-janitor.git.d.ts.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-janitor.git.js +207 -1
- package/dist/backend/backend/src/services/worktree/worktree-janitor.git.js.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts +135 -28
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js +248 -39
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/backend/config/constants.d.ts +13 -0
- package/dist/backend/config/constants.d.ts.map +1 -1
- package/dist/backend/config/constants.js +13 -0
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +59 -3
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +54 -3
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +9 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/config/constants.d.ts +13 -0
- package/dist/cli/config/constants.d.ts.map +1 -1
- package/dist/cli/config/constants.js +13 -0
- package/dist/cli/config/constants.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation through project tickets — the rule deciding whether a WorkItem
|
|
3
|
+
* created through `POST /api/task-pool/add` is routed through a project
|
|
4
|
+
* ticket (specs/2026-09-28-project-tickets.md §11).
|
|
5
|
+
*
|
|
6
|
+
* Pure: no I/O. The workflow service gathers the inputs (who is calling, the
|
|
7
|
+
* projects of the target's teams) and acts on the decision.
|
|
8
|
+
*
|
|
9
|
+
* @module services/project-tickets/delegation-ticket-route
|
|
10
|
+
*/
|
|
11
|
+
import type { WorkItem } from '../../types/v2/work-item.types.js';
|
|
12
|
+
/** The WorkItem fields the rule looks at. */
|
|
13
|
+
export type DelegationRouteItem = Pick<WorkItem, 'type' | 'owner' | 'target' | 'triggerId' | 'scheduledAt' | 'metadata'>;
|
|
14
|
+
/** Inputs of {@link decideDelegationTicketRoute}. */
|
|
15
|
+
export interface DelegationRouteInput {
|
|
16
|
+
/** The WorkItem about to be added */
|
|
17
|
+
workItem: DelegationRouteItem;
|
|
18
|
+
/** The delegating agent (X-Agent-Session, or `metadata.delegatedBy`); absent = unknown */
|
|
19
|
+
callerSession?: string;
|
|
20
|
+
/** `--ticket <ID>` given by the delegator */
|
|
21
|
+
explicitTicketId?: string;
|
|
22
|
+
/** Ids of the projects the target's teams work on */
|
|
23
|
+
targetProjectIds: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What to do with the WorkItem.
|
|
27
|
+
*
|
|
28
|
+
* - `route`: go through a ticket (the given one, or a new one).
|
|
29
|
+
* - `skip`: add the WorkItem as before, no ticket.
|
|
30
|
+
* - `refuse`: the delegator named a ticket, but this item cannot carry one.
|
|
31
|
+
*/
|
|
32
|
+
export type DelegationRouteDecision = {
|
|
33
|
+
action: 'route';
|
|
34
|
+
} | {
|
|
35
|
+
action: 'skip';
|
|
36
|
+
reason: string;
|
|
37
|
+
} | {
|
|
38
|
+
action: 'refuse';
|
|
39
|
+
reason: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Decide whether a WorkItem goes through a project ticket.
|
|
43
|
+
*
|
|
44
|
+
* Routed: a `delegate` item from a known agent (or the owner, with an
|
|
45
|
+
* explicit ticket) to a DIFFERENT agent whose team works on a project. Never
|
|
46
|
+
* routed: review/verify items, system / trigger / scheduled items, items an
|
|
47
|
+
* agent targets at itself, targets with no project, items already linked.
|
|
48
|
+
* Naming a ticket on an item that cannot carry one is refused rather than
|
|
49
|
+
* silently ignored.
|
|
50
|
+
*
|
|
51
|
+
* @param input - The WorkItem, the caller, the explicit ticket, the target's projects
|
|
52
|
+
* @returns The decision
|
|
53
|
+
*/
|
|
54
|
+
export declare function decideDelegationTicketRoute(input: DelegationRouteInput): DelegationRouteDecision;
|
|
55
|
+
/**
|
|
56
|
+
* Ticket title for a delegation title: the first non-empty line, markdown
|
|
57
|
+
* heading marks dropped, capped.
|
|
58
|
+
*
|
|
59
|
+
* @param title - WorkItem title (may be the first 200 chars of a brief)
|
|
60
|
+
* @returns Title (never empty)
|
|
61
|
+
*/
|
|
62
|
+
export declare function delegationTicketTitle(title: string): string;
|
|
63
|
+
//# sourceMappingURL=delegation-ticket-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delegation-ticket-route.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/project-tickets/delegation-ticket-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAGlE,6CAA6C;AAC7C,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,aAAa,GAAG,UAAU,CAAC,CAAC;AAEzH,qDAAqD;AACrD,MAAM,WAAW,oBAAoB;IACnC,qCAAqC;IACrC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,0FAA0F;IAC1F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6CAA6C;IAC7C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,uBAAuB,GAC/B;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GACnB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAyBzC;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,oBAAoB,GAAG,uBAAuB,CAIhG;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ3D"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation through project tickets — the rule deciding whether a WorkItem
|
|
3
|
+
* created through `POST /api/task-pool/add` is routed through a project
|
|
4
|
+
* ticket (specs/2026-09-28-project-tickets.md §11).
|
|
5
|
+
*
|
|
6
|
+
* Pure: no I/O. The workflow service gathers the inputs (who is calling, the
|
|
7
|
+
* projects of the target's teams) and acts on the decision.
|
|
8
|
+
*
|
|
9
|
+
* @module services/project-tickets/delegation-ticket-route
|
|
10
|
+
*/
|
|
11
|
+
import { PROJECT_TICKET_CONSTANTS } from '../../constants.js';
|
|
12
|
+
import { readProjectTicketLink } from '../../types/project-ticket.types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Why an item is never routed through a ticket, or null when it may be.
|
|
15
|
+
*
|
|
16
|
+
* @param input - Rule inputs
|
|
17
|
+
* @returns Reason, or null
|
|
18
|
+
*/
|
|
19
|
+
function exclusionReason(input) {
|
|
20
|
+
const { workItem: wi, callerSession } = input;
|
|
21
|
+
const types = PROJECT_TICKET_CONSTANTS.DELEGATION_WORK_ITEM_TYPES;
|
|
22
|
+
if (!types.includes(wi.type))
|
|
23
|
+
return `a ${wi.type} item is not a delegation`;
|
|
24
|
+
if (readProjectTicketLink(wi.metadata))
|
|
25
|
+
return 'the item already carries a project ticket';
|
|
26
|
+
if (typeof wi.metadata?.verifyOf === 'string')
|
|
27
|
+
return 'review / verify items do not get tickets';
|
|
28
|
+
if (wi.owner === 'system')
|
|
29
|
+
return 'system items do not get tickets';
|
|
30
|
+
if (wi.triggerId)
|
|
31
|
+
return 'trigger / cron items do not get tickets';
|
|
32
|
+
if (wi.scheduledAt)
|
|
33
|
+
return 'scheduled reminders do not get tickets';
|
|
34
|
+
const target = typeof wi.target === 'string' ? wi.target.trim() : '';
|
|
35
|
+
if (!target)
|
|
36
|
+
return 'an unassigned item has nobody to put on a ticket';
|
|
37
|
+
if (!callerSession && !input.explicitTicketId)
|
|
38
|
+
return 'the delegator is unknown, so a self-reminder cannot be told apart';
|
|
39
|
+
if (callerSession && callerSession === target)
|
|
40
|
+
return 'an item an agent targets at itself is a self-reminder';
|
|
41
|
+
if (input.targetProjectIds.length === 0)
|
|
42
|
+
return `${target} is not on a team with a project`;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Decide whether a WorkItem goes through a project ticket.
|
|
47
|
+
*
|
|
48
|
+
* Routed: a `delegate` item from a known agent (or the owner, with an
|
|
49
|
+
* explicit ticket) to a DIFFERENT agent whose team works on a project. Never
|
|
50
|
+
* routed: review/verify items, system / trigger / scheduled items, items an
|
|
51
|
+
* agent targets at itself, targets with no project, items already linked.
|
|
52
|
+
* Naming a ticket on an item that cannot carry one is refused rather than
|
|
53
|
+
* silently ignored.
|
|
54
|
+
*
|
|
55
|
+
* @param input - The WorkItem, the caller, the explicit ticket, the target's projects
|
|
56
|
+
* @returns The decision
|
|
57
|
+
*/
|
|
58
|
+
export function decideDelegationTicketRoute(input) {
|
|
59
|
+
const reason = exclusionReason(input);
|
|
60
|
+
if (!reason)
|
|
61
|
+
return { action: 'route' };
|
|
62
|
+
return input.explicitTicketId ? { action: 'refuse', reason: `--ticket ${input.explicitTicketId} refused: ${reason}` } : { action: 'skip', reason };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Ticket title for a delegation title: the first non-empty line, markdown
|
|
66
|
+
* heading marks dropped, capped.
|
|
67
|
+
*
|
|
68
|
+
* @param title - WorkItem title (may be the first 200 chars of a brief)
|
|
69
|
+
* @returns Title (never empty)
|
|
70
|
+
*/
|
|
71
|
+
export function delegationTicketTitle(title) {
|
|
72
|
+
const line = String(title ?? '')
|
|
73
|
+
.split('\n')
|
|
74
|
+
.map((l) => l.replace(/^#+\s*/, '').trim())
|
|
75
|
+
.find((l) => l.length > 0) ?? '';
|
|
76
|
+
const max = PROJECT_TICKET_CONSTANTS.DELEGATION_TITLE_MAX_CHARS;
|
|
77
|
+
const text = line || 'Delegated task';
|
|
78
|
+
return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=delegation-ticket-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delegation-ticket-route.js","sourceRoot":"","sources":["../../../../../../backend/src/services/project-tickets/delegation-ticket-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AA6B5E;;;;;GAKG;AACH,SAAS,eAAe,CAAC,KAA2B;IAClD,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,aAAa,EAAE,GAAG,KAAK,CAAC;IAC9C,MAAM,KAAK,GAAsB,wBAAwB,CAAC,0BAA0B,CAAC;IACrF,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,EAAE,CAAC,IAAI,2BAA2B,CAAC;IAC7E,IAAI,qBAAqB,CAAC,EAAE,CAAC,QAAQ,CAAC;QAAE,OAAO,2CAA2C,CAAC;IAC3F,IAAI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,QAAQ;QAAE,OAAO,0CAA0C,CAAC;IACjG,IAAI,EAAE,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,iCAAiC,CAAC;IACpE,IAAI,EAAE,CAAC,SAAS;QAAE,OAAO,yCAAyC,CAAC;IACnE,IAAI,EAAE,CAAC,WAAW;QAAE,OAAO,wCAAwC,CAAC;IACpE,MAAM,MAAM,GAAG,OAAO,EAAE,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACrE,IAAI,CAAC,MAAM;QAAE,OAAO,kDAAkD,CAAC;IACvE,IAAI,CAAC,aAAa,IAAI,CAAC,KAAK,CAAC,gBAAgB;QAAE,OAAO,mEAAmE,CAAC;IAC1H,IAAI,aAAa,IAAI,aAAa,KAAK,MAAM;QAAE,OAAO,uDAAuD,CAAC;IAC9G,IAAI,KAAK,CAAC,gBAAgB,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,MAAM,kCAAkC,CAAC;IAC5F,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,2BAA2B,CAAC,KAA2B;IACrE,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IACxC,OAAO,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,KAAK,CAAC,gBAAgB,aAAa,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACrJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAa;IACjD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;SAC7B,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;SAC1C,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACnC,MAAM,GAAG,GAAG,wBAAwB,CAAC,0BAA0B,CAAC;IAChE,MAAM,IAAI,GAAG,IAAI,IAAI,gBAAgB,CAAC;IACtC,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC3E,CAAC"}
|
package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts
CHANGED
|
@@ -27,7 +27,9 @@ export interface ProjectTicketCaller {
|
|
|
27
27
|
export type ProjectTicketAccess = 'owner' | 'orchestrator' | 'lead' | 'member' | 'outsider';
|
|
28
28
|
/** The subset of the task pool this service uses. */
|
|
29
29
|
export interface ProjectTicketPool {
|
|
30
|
-
addToPool(workItem: WorkItem
|
|
30
|
+
addToPool(workItem: WorkItem, options?: {
|
|
31
|
+
creatorSession?: string;
|
|
32
|
+
}): Promise<void>;
|
|
31
33
|
claimSpecificItem(agentId: string, workItemId: string): Promise<{
|
|
32
34
|
workItem: WorkItem;
|
|
33
35
|
} | null>;
|
|
@@ -36,6 +38,7 @@ export interface ProjectTicketPool {
|
|
|
36
38
|
cancelQueued(workItemId: string, reason: string): Promise<void>;
|
|
37
39
|
transitionStatus(workItemId: string, status: WorkItemStatus, actor: 'system', mutator?: (wi: WorkItem) => void, reason?: string): Promise<WorkItem | null>;
|
|
38
40
|
releaseClaim(workItemId: string, endReason: string): Promise<void>;
|
|
41
|
+
mergeItemMetadata(workItemId: string, patch: Record<string, unknown>): Promise<WorkItem | null>;
|
|
39
42
|
}
|
|
40
43
|
/** The subset of storage this service uses. */
|
|
41
44
|
export interface ProjectTicketDirectory {
|
|
@@ -57,6 +60,28 @@ export interface StartedTicketWork {
|
|
|
57
60
|
/** True when the WorkItem was claimed for the assignee right away */
|
|
58
61
|
claimed: boolean;
|
|
59
62
|
}
|
|
63
|
+
/** A delegated WorkItem routed through a ticket (spec §11). */
|
|
64
|
+
export interface RoutedDelegation {
|
|
65
|
+
ticket: ProjectTicket;
|
|
66
|
+
/** The delegated WorkItem as added to the pool, now carrying `metadata.projectTicket` */
|
|
67
|
+
workItem: WorkItem;
|
|
68
|
+
project: Project;
|
|
69
|
+
/** True when the ticket was created for this delegation */
|
|
70
|
+
createdTicket: boolean;
|
|
71
|
+
}
|
|
72
|
+
/** Inputs of {@link ProjectTicketWorkflowService.routeDelegation}. */
|
|
73
|
+
export interface RouteDelegationInput {
|
|
74
|
+
/** The WorkItem the delegate skill is adding (built, not yet in the pool) */
|
|
75
|
+
workItem: WorkItem;
|
|
76
|
+
/** The delegator (X-Agent-Session or `metadata.delegatedBy`); absent = owner / unknown */
|
|
77
|
+
callerSession?: string;
|
|
78
|
+
/** `--ticket <ID>` */
|
|
79
|
+
ticketId?: string;
|
|
80
|
+
/** Options passed through to the pool's add */
|
|
81
|
+
addOptions?: {
|
|
82
|
+
creatorSession?: string;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
60
85
|
/** What {@link ProjectTicketWorkflowService.syncTicket} did. */
|
|
61
86
|
export type TicketSyncOutcome = 'unchanged' | 'done' | 'review' | 'relinked' | 'returned';
|
|
62
87
|
/** Tickets of one project, for the per-agent listing. */
|
|
@@ -283,6 +308,43 @@ export declare class ProjectTicketWorkflowService {
|
|
|
283
308
|
* @returns The started work, or null
|
|
284
309
|
*/
|
|
285
310
|
claimNextForAgent(session: string): Promise<StartedTicketWork | null>;
|
|
311
|
+
/**
|
|
312
|
+
* Route a delegated WorkItem through a project ticket, or leave it alone.
|
|
313
|
+
*
|
|
314
|
+
* When {@link decideDelegationTicketRoute} says `route`, the WorkItem is
|
|
315
|
+
* added to the pool here (under the ticket folder lock, through the same
|
|
316
|
+
* path as {@link assign}) instead of by the caller:
|
|
317
|
+
* - with `ticketId`: that ticket (must be `backlog`/`ready`, no live
|
|
318
|
+
* WorkItem, caller owner/orc/lead) is assigned to the target;
|
|
319
|
+
* - without: a ticket is created from the delegation (title, brief,
|
|
320
|
+
* target's team, priority) and assigned to the target at once.
|
|
321
|
+
* The WorkItem keeps all its fields; `metadata.projectTicket` (plus
|
|
322
|
+
* `projectId` / `projectPath` / `teamId` when missing) is added.
|
|
323
|
+
*
|
|
324
|
+
* @param input - WorkItem, delegator, optional ticket, add options
|
|
325
|
+
* @returns The routed delegation, or null when the caller should add the WorkItem itself
|
|
326
|
+
* @throws ProjectTicketError when a named ticket cannot take this delegation
|
|
327
|
+
*/
|
|
328
|
+
routeDelegation(input: RouteDelegationInput): Promise<RoutedDelegation | null>;
|
|
329
|
+
/**
|
|
330
|
+
* Link a live WorkItem that is already in flight to a ticket (spec §11).
|
|
331
|
+
*
|
|
332
|
+
* The ticket must not be done/cancelled and must have no other live
|
|
333
|
+
* WorkItem; the WorkItem must be live and carry no other ticket. The item
|
|
334
|
+
* gets `metadata.projectTicket`; the ticket becomes `in_progress` (a ticket
|
|
335
|
+
* in `review` stays there), assignee = the item's target, workItemId set.
|
|
336
|
+
*
|
|
337
|
+
* @param ref - Project reference
|
|
338
|
+
* @param id - Ticket id
|
|
339
|
+
* @param workItemId - The live WorkItem
|
|
340
|
+
* @param caller - Owner, orchestrator or a lead of a project team
|
|
341
|
+
* @returns Updated ticket and WorkItem
|
|
342
|
+
* @throws ProjectTicketError(400/403/404/409)
|
|
343
|
+
*/
|
|
344
|
+
link(ref: string, id: string, workItemId: string, caller: ProjectTicketCaller): Promise<{
|
|
345
|
+
ticket: ProjectTicket;
|
|
346
|
+
workItem: WorkItem;
|
|
347
|
+
}>;
|
|
286
348
|
/**
|
|
287
349
|
* React to a pool lifecycle event: find the ticket the WorkItem (or one of
|
|
288
350
|
* its ancestors — a reviewer's retry or review item) belongs to and sync it.
|
|
@@ -353,6 +415,35 @@ export declare class ProjectTicketWorkflowService {
|
|
|
353
415
|
* @param reason - Cancel reason
|
|
354
416
|
*/
|
|
355
417
|
private cancelLiveWorkItem;
|
|
418
|
+
/**
|
|
419
|
+
* Projects an agent's (non-archived) teams work on, with the team.
|
|
420
|
+
*
|
|
421
|
+
* @param session - Agent session
|
|
422
|
+
* @returns One entry per project (first team wins)
|
|
423
|
+
*/
|
|
424
|
+
private projectsOfAgent;
|
|
425
|
+
/**
|
|
426
|
+
* Which of the target's projects a delegation belongs to: the one matching
|
|
427
|
+
* the item's `metadata.projectPath`, else the only one, else (with a
|
|
428
|
+
* ticket id) the one holding that ticket.
|
|
429
|
+
*
|
|
430
|
+
* @param candidates - Target's projects
|
|
431
|
+
* @param wi - Delegated WorkItem
|
|
432
|
+
* @param ticketId - Explicit ticket, if any
|
|
433
|
+
* @returns The choice, or null when it cannot be told
|
|
434
|
+
*/
|
|
435
|
+
private pickDelegationProject;
|
|
436
|
+
/**
|
|
437
|
+
* The delegated WorkItem with the ticket link added; every other field is
|
|
438
|
+
* kept as the delegator built it.
|
|
439
|
+
*
|
|
440
|
+
* @param wi - Delegated WorkItem
|
|
441
|
+
* @param project - Project
|
|
442
|
+
* @param t - Ticket
|
|
443
|
+
* @param teamId - Target's team on the project
|
|
444
|
+
* @returns WorkItem to add
|
|
445
|
+
*/
|
|
446
|
+
private linkDelegatedItem;
|
|
356
447
|
/**
|
|
357
448
|
* The team an agent would work a ticket for, or null when it may not.
|
|
358
449
|
*
|
package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-ticket-workflow.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/project-tickets/project-ticket-workflow.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAChF,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,cAAc,EACpB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,
|
|
1
|
+
{"version":3,"file":"project-ticket-workflow.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/project-tickets/project-ticket-workflow.service.ts"],"names":[],"mappings":"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"}
|
package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js
CHANGED
|
@@ -16,9 +16,10 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
16
16
|
import { ORCHESTRATOR_SESSION_NAME, PROJECT_TICKET_CONSTANTS } from '../../constants.js';
|
|
17
17
|
import { LoggerService } from '../core/logger.service.js';
|
|
18
18
|
import { MAX_BRIEF_MARKDOWN_BYTES, getWorkItemDisposition, } from '../../types/v2/work-item.types.js';
|
|
19
|
-
import { projectTicketPriorityRank, readProjectTicketLink, } from '../../types/project-ticket.types.js';
|
|
19
|
+
import { normalizeProjectTicketPriority, projectTicketPriorityRank, readProjectTicketLink, } from '../../types/project-ticket.types.js';
|
|
20
20
|
import { ProjectTicketError, } from './project-ticket.service.js';
|
|
21
21
|
import { SUPERSEDED_BY_METADATA_KEY } from '../v3/request-completion.js';
|
|
22
|
+
import { decideDelegationTicketRoute, delegationTicketTitle } from './delegation-ticket-route.js';
|
|
22
23
|
/** WorkItem statuses that still carry the ticket's work. */
|
|
23
24
|
const LIVE_STATUSES = new Set([
|
|
24
25
|
'queued',
|
|
@@ -491,6 +492,184 @@ export class ProjectTicketWorkflowService {
|
|
|
491
492
|
return null;
|
|
492
493
|
}
|
|
493
494
|
// ---------------------------------------------------------------------------
|
|
495
|
+
// Delegation through tickets (spec §11)
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
/**
|
|
498
|
+
* Route a delegated WorkItem through a project ticket, or leave it alone.
|
|
499
|
+
*
|
|
500
|
+
* When {@link decideDelegationTicketRoute} says `route`, the WorkItem is
|
|
501
|
+
* added to the pool here (under the ticket folder lock, through the same
|
|
502
|
+
* path as {@link assign}) instead of by the caller:
|
|
503
|
+
* - with `ticketId`: that ticket (must be `backlog`/`ready`, no live
|
|
504
|
+
* WorkItem, caller owner/orc/lead) is assigned to the target;
|
|
505
|
+
* - without: a ticket is created from the delegation (title, brief,
|
|
506
|
+
* target's team, priority) and assigned to the target at once.
|
|
507
|
+
* The WorkItem keeps all its fields; `metadata.projectTicket` (plus
|
|
508
|
+
* `projectId` / `projectPath` / `teamId` when missing) is added.
|
|
509
|
+
*
|
|
510
|
+
* @param input - WorkItem, delegator, optional ticket, add options
|
|
511
|
+
* @returns The routed delegation, or null when the caller should add the WorkItem itself
|
|
512
|
+
* @throws ProjectTicketError when a named ticket cannot take this delegation
|
|
513
|
+
*/
|
|
514
|
+
async routeDelegation(input) {
|
|
515
|
+
const { workItem, callerSession } = input;
|
|
516
|
+
const ticketId = input.ticketId?.trim() || undefined;
|
|
517
|
+
const target = typeof workItem.target === 'string' ? workItem.target.trim() : '';
|
|
518
|
+
const candidates = target ? await this.projectsOfAgent(target) : [];
|
|
519
|
+
const decision = decideDelegationTicketRoute({
|
|
520
|
+
workItem,
|
|
521
|
+
callerSession,
|
|
522
|
+
explicitTicketId: ticketId,
|
|
523
|
+
targetProjectIds: candidates.map((c) => c.project.id),
|
|
524
|
+
});
|
|
525
|
+
if (decision.action === 'refuse')
|
|
526
|
+
throw new ProjectTicketError(400, decision.reason);
|
|
527
|
+
if (decision.action === 'skip') {
|
|
528
|
+
this.logger.debug('Delegation not routed through a project ticket', { workItemId: workItem.id, target, reason: decision.reason });
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
const choice = await this.pickDelegationProject(candidates, workItem, ticketId);
|
|
532
|
+
if (!choice) {
|
|
533
|
+
if (ticketId)
|
|
534
|
+
throw new ProjectTicketError(404, `Ticket ${ticketId} not found in any project of ${target}'s teams`);
|
|
535
|
+
this.logger.info('Delegation not routed: target works on several projects and none was named', { workItemId: workItem.id, target });
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const { project } = choice;
|
|
539
|
+
const caller = callerSession ? { session: callerSession } : {};
|
|
540
|
+
const actor = this.actorName(caller);
|
|
541
|
+
if (ticketId) {
|
|
542
|
+
const { access } = await this.accessOf(caller, project);
|
|
543
|
+
this.requireAccess(access, ['owner', 'orchestrator', 'lead'], `delegate ticket ${ticketId}`);
|
|
544
|
+
const ticket = await this.tickets.get(project.path, ticketId);
|
|
545
|
+
if (!ticket)
|
|
546
|
+
throw new ProjectTicketError(404, `Ticket not found: ${ticketId}`);
|
|
547
|
+
const teamId = await this.eligibleTeamOf(project, ticket, target);
|
|
548
|
+
if (!teamId)
|
|
549
|
+
throw new ProjectTicketError(403, `${target} is not on a team that works on ${ticket.id}`);
|
|
550
|
+
const started = await this.startWork(project, ticket.id, target, actor, teamId, {
|
|
551
|
+
self: false,
|
|
552
|
+
allowed: ['backlog', 'ready'],
|
|
553
|
+
prepared: (t) => this.linkDelegatedItem(workItem, project, t, teamId),
|
|
554
|
+
addOptions: input.addOptions,
|
|
555
|
+
logFirst: [`delegated by ${actor}`],
|
|
556
|
+
});
|
|
557
|
+
return { ticket: started.ticket, workItem: started.workItem, project, createdTicket: false };
|
|
558
|
+
}
|
|
559
|
+
const teamId = choice.teamId;
|
|
560
|
+
const brief = workItem.briefMarkdown || workItem.description || '';
|
|
561
|
+
// A ticket store that cannot be written must not stop delegation itself:
|
|
562
|
+
// the WorkItem is then added as before, without a ticket.
|
|
563
|
+
let created;
|
|
564
|
+
try {
|
|
565
|
+
created = await this.tickets.create(project.path, project.name, {
|
|
566
|
+
title: delegationTicketTitle(workItem.title),
|
|
567
|
+
description: brief,
|
|
568
|
+
team: teamId,
|
|
569
|
+
priority: normalizeProjectTicketPriority(workItem.metadata?.priority) ?? undefined,
|
|
570
|
+
status: 'backlog',
|
|
571
|
+
source: callerSession ? `agent:${callerSession}` : 'owner',
|
|
572
|
+
requestId: workItem.requestId ?? null,
|
|
573
|
+
}, actor);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
this.logger.warn('Could not create a ticket for a delegation; adding the WorkItem without one', {
|
|
577
|
+
projectPath: project.path,
|
|
578
|
+
workItemId: workItem.id,
|
|
579
|
+
error: err instanceof Error ? err.message : String(err),
|
|
580
|
+
});
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const started = await this.startWork(project, created.id, target, actor, teamId, {
|
|
585
|
+
self: false,
|
|
586
|
+
allowed: ['backlog'],
|
|
587
|
+
prepared: (t) => this.linkDelegatedItem(workItem, project, t, teamId),
|
|
588
|
+
addOptions: input.addOptions,
|
|
589
|
+
logFirst: [`created from delegation by ${actor}`],
|
|
590
|
+
});
|
|
591
|
+
this.logger.info('Delegation routed through a new project ticket', { projectPath: project.path, id: created.id, workItemId: started.workItem.id });
|
|
592
|
+
return { ticket: started.ticket, workItem: started.workItem, project, createdTicket: true };
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
// The delegation did not start: do not leave an orphan backlog ticket behind.
|
|
596
|
+
await this.tickets
|
|
597
|
+
.transition(project.path, created.id, 'cancelled', 'crewly', `delegation could not start: ${err instanceof Error ? err.message : String(err)}`)
|
|
598
|
+
.catch(() => undefined);
|
|
599
|
+
throw err;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Link a live WorkItem that is already in flight to a ticket (spec §11).
|
|
604
|
+
*
|
|
605
|
+
* The ticket must not be done/cancelled and must have no other live
|
|
606
|
+
* WorkItem; the WorkItem must be live and carry no other ticket. The item
|
|
607
|
+
* gets `metadata.projectTicket`; the ticket becomes `in_progress` (a ticket
|
|
608
|
+
* in `review` stays there), assignee = the item's target, workItemId set.
|
|
609
|
+
*
|
|
610
|
+
* @param ref - Project reference
|
|
611
|
+
* @param id - Ticket id
|
|
612
|
+
* @param workItemId - The live WorkItem
|
|
613
|
+
* @param caller - Owner, orchestrator or a lead of a project team
|
|
614
|
+
* @returns Updated ticket and WorkItem
|
|
615
|
+
* @throws ProjectTicketError(400/403/404/409)
|
|
616
|
+
*/
|
|
617
|
+
async link(ref, id, workItemId, caller) {
|
|
618
|
+
const project = await this.resolveProject(ref);
|
|
619
|
+
const { access } = await this.accessOf(caller, project);
|
|
620
|
+
this.requireAccess(access, ['owner', 'orchestrator', 'lead'], 'link WorkItems to tickets');
|
|
621
|
+
const wiId = String(workItemId ?? '').trim();
|
|
622
|
+
if (!wiId)
|
|
623
|
+
throw new ProjectTicketError(400, 'workItemId is required');
|
|
624
|
+
const actor = this.actorName(caller);
|
|
625
|
+
let linked = null;
|
|
626
|
+
let wroteMetadata = false;
|
|
627
|
+
try {
|
|
628
|
+
const ticket = await this.tickets.mutate(project.path, id, actor, async (t) => {
|
|
629
|
+
if (t.status === 'done' || t.status === 'cancelled') {
|
|
630
|
+
throw new ProjectTicketError(409, `${t.id} is ${t.status}; reopen it before linking work to it`);
|
|
631
|
+
}
|
|
632
|
+
const wi = await this.pool.findWorkItem(wiId);
|
|
633
|
+
if (!wi)
|
|
634
|
+
throw new ProjectTicketError(404, `WorkItem not found: ${wiId}`);
|
|
635
|
+
if (!LIVE_STATUSES.has(wi.status))
|
|
636
|
+
throw new ProjectTicketError(409, `WorkItem ${wi.id} is ${wi.status}; only live work can be linked`);
|
|
637
|
+
const existing = readProjectTicketLink(wi.metadata);
|
|
638
|
+
const sameTicket = !!existing && existing.id === t.id && path.resolve(existing.projectPath) === path.resolve(project.path);
|
|
639
|
+
if (existing && !sameTicket)
|
|
640
|
+
throw new ProjectTicketError(409, `WorkItem ${wi.id} already works ticket ${existing.id}`);
|
|
641
|
+
const live = await this.findLiveLinkedWorkItem(project.path, t);
|
|
642
|
+
if (live && live.id !== wi.id)
|
|
643
|
+
throw new ProjectTicketError(409, `${t.id} is already being worked in WorkItem ${live.id}`);
|
|
644
|
+
linked = wi;
|
|
645
|
+
if (sameTicket && t.workItemId === wi.id)
|
|
646
|
+
return null;
|
|
647
|
+
if (!sameTicket) {
|
|
648
|
+
const updated = await this.pool.mergeItemMetadata(wi.id, {
|
|
649
|
+
[PROJECT_TICKET_CONSTANTS.WORK_ITEM_METADATA_KEY]: { projectPath: project.path, id: t.id },
|
|
650
|
+
});
|
|
651
|
+
if (!updated)
|
|
652
|
+
throw new ProjectTicketError(404, `WorkItem not found: ${wi.id}`);
|
|
653
|
+
wroteMetadata = true;
|
|
654
|
+
linked = updated;
|
|
655
|
+
}
|
|
656
|
+
const status = t.status === 'review' ? 'review' : 'in_progress';
|
|
657
|
+
return {
|
|
658
|
+
fields: { status, workItemId: wi.id, ...(wi.target ? { assignee: wi.target } : {}) },
|
|
659
|
+
log: [`linked to WorkItem ${wi.id} (${wi.status}${wi.target ? `, ${wi.target}` : ''}) by ${actor}`],
|
|
660
|
+
};
|
|
661
|
+
});
|
|
662
|
+
this.logger.info('Project ticket linked to a WorkItem', { projectPath: project.path, id, workItemId: wiId });
|
|
663
|
+
return { ticket, workItem: linked };
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
if (wroteMetadata) {
|
|
667
|
+
await this.pool.mergeItemMetadata(wiId, { [PROJECT_TICKET_CONSTANTS.WORK_ITEM_METADATA_KEY]: undefined }).catch(() => undefined);
|
|
668
|
+
}
|
|
669
|
+
throw err;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
494
673
|
// Sync (WorkItem → ticket)
|
|
495
674
|
// ---------------------------------------------------------------------------
|
|
496
675
|
/**
|
|
@@ -631,11 +810,14 @@ export class ProjectTicketWorkflowService {
|
|
|
631
810
|
const live = await this.findLiveLinkedWorkItem(project.path, t);
|
|
632
811
|
if (live)
|
|
633
812
|
throw new ProjectTicketError(409, `${t.id} is already being worked in WorkItem ${live.id}`);
|
|
634
|
-
created = this.buildWorkItem(project, t, assignee, teamId);
|
|
635
|
-
await this.pool.addToPool(created);
|
|
813
|
+
created = options.prepared ? options.prepared(t) : this.buildWorkItem(project, t, assignee, teamId);
|
|
814
|
+
await this.pool.addToPool(created, options.addOptions);
|
|
636
815
|
return {
|
|
637
816
|
fields: { status: 'in_progress', assignee, workItemId: created.id },
|
|
638
|
-
log: [
|
|
817
|
+
log: [
|
|
818
|
+
...(options.logFirst ?? []),
|
|
819
|
+
options.self ? `claimed by ${assignee} — WorkItem ${created.id}` : `assigned to ${assignee} — WorkItem ${created.id}`,
|
|
820
|
+
],
|
|
639
821
|
};
|
|
640
822
|
});
|
|
641
823
|
}
|
|
@@ -797,6 +979,75 @@ export class ProjectTicketWorkflowService {
|
|
|
797
979
|
this.logger.warn('Could not cancel the linked WorkItem', { workItemId, error: err instanceof Error ? err.message : String(err) });
|
|
798
980
|
}
|
|
799
981
|
}
|
|
982
|
+
/**
|
|
983
|
+
* Projects an agent's (non-archived) teams work on, with the team.
|
|
984
|
+
*
|
|
985
|
+
* @param session - Agent session
|
|
986
|
+
* @returns One entry per project (first team wins)
|
|
987
|
+
*/
|
|
988
|
+
async projectsOfAgent(session) {
|
|
989
|
+
const teams = (await this.directory.getTeams()).filter((t) => !t.archived && (t.members ?? []).some((m) => isSession(m, session)));
|
|
990
|
+
if (teams.length === 0)
|
|
991
|
+
return [];
|
|
992
|
+
const projects = await this.directory.getProjects();
|
|
993
|
+
const out = [];
|
|
994
|
+
for (const team of teams) {
|
|
995
|
+
for (const pid of team.projectIds ?? []) {
|
|
996
|
+
const project = projects.find((p) => p.id === pid);
|
|
997
|
+
if (project && !out.some((o) => o.project.id === pid))
|
|
998
|
+
out.push({ project, teamId: team.id });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return out;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Which of the target's projects a delegation belongs to: the one matching
|
|
1005
|
+
* the item's `metadata.projectPath`, else the only one, else (with a
|
|
1006
|
+
* ticket id) the one holding that ticket.
|
|
1007
|
+
*
|
|
1008
|
+
* @param candidates - Target's projects
|
|
1009
|
+
* @param wi - Delegated WorkItem
|
|
1010
|
+
* @param ticketId - Explicit ticket, if any
|
|
1011
|
+
* @returns The choice, or null when it cannot be told
|
|
1012
|
+
*/
|
|
1013
|
+
async pickDelegationProject(candidates, wi, ticketId) {
|
|
1014
|
+
const wanted = typeof wi.metadata?.projectPath === 'string' && wi.metadata.projectPath ? path.resolve(wi.metadata.projectPath) : null;
|
|
1015
|
+
const byPath = wanted ? candidates.find((c) => path.resolve(c.project.path) === wanted) : undefined;
|
|
1016
|
+
if (ticketId) {
|
|
1017
|
+
const ordered = byPath ? [byPath, ...candidates.filter((c) => c !== byPath)] : candidates;
|
|
1018
|
+
for (const c of ordered) {
|
|
1019
|
+
if (await this.tickets.get(c.project.path, ticketId).catch(() => null))
|
|
1020
|
+
return c;
|
|
1021
|
+
}
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
if (byPath)
|
|
1025
|
+
return byPath;
|
|
1026
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* The delegated WorkItem with the ticket link added; every other field is
|
|
1030
|
+
* kept as the delegator built it.
|
|
1031
|
+
*
|
|
1032
|
+
* @param wi - Delegated WorkItem
|
|
1033
|
+
* @param project - Project
|
|
1034
|
+
* @param t - Ticket
|
|
1035
|
+
* @param teamId - Target's team on the project
|
|
1036
|
+
* @returns WorkItem to add
|
|
1037
|
+
*/
|
|
1038
|
+
linkDelegatedItem(wi, project, t, teamId) {
|
|
1039
|
+
const meta = wi.metadata ?? {};
|
|
1040
|
+
return {
|
|
1041
|
+
...wi,
|
|
1042
|
+
metadata: {
|
|
1043
|
+
...meta,
|
|
1044
|
+
[PROJECT_TICKET_CONSTANTS.WORK_ITEM_METADATA_KEY]: { projectPath: project.path, id: t.id },
|
|
1045
|
+
...(meta.projectId ? {} : { projectId: project.id }),
|
|
1046
|
+
...(meta.projectPath ? {} : { projectPath: project.path }),
|
|
1047
|
+
...(meta.teamId ? {} : { teamId }),
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
800
1051
|
/**
|
|
801
1052
|
* The team an agent would work a ticket for, or null when it may not.
|
|
802
1053
|
*
|