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.
Files changed (80) hide show
  1. package/config/constants.ts +13 -0
  2. package/config/roles/orchestrator/prompt.md +1 -0
  3. package/config/roles/team-leader/prompt.md +3 -0
  4. package/config/roles/team-leader/tl-addon.md +3 -0
  5. package/config/skills/agent/core/project-tickets/SKILL.md +17 -1
  6. package/config/skills/agent/core/project-tickets/execute.sh +16 -3
  7. package/config/skills/agent/core/project-tickets/execute.test.sh +11 -0
  8. package/config/skills/orchestrator/delegate-task/SKILL.md +18 -0
  9. package/config/skills/orchestrator/delegate-task/execute.sh +23 -5
  10. package/config/skills/orchestrator/delegate-task/execute.test.sh +44 -1
  11. package/config/skills/registry.json +55 -4
  12. package/config/skills/team-leader/delegate-task/SKILL.md +18 -0
  13. package/config/skills/team-leader/delegate-task/execute.sh +31 -4
  14. package/config/skills/team-leader/delegate-task/execute.test.sh +59 -0
  15. package/dist/backend/backend/src/constants.d.ts +59 -3
  16. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  17. package/dist/backend/backend/src/constants.js +54 -3
  18. package/dist/backend/backend/src/constants.js.map +1 -1
  19. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts +8 -0
  20. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts.map +1 -1
  21. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js +13 -0
  22. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js.map +1 -1
  23. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.d.ts +1 -0
  24. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.d.ts.map +1 -1
  25. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.js +3 -1
  26. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.js.map +1 -1
  27. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts.map +1 -1
  28. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +73 -3
  29. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
  30. package/dist/backend/backend/src/controllers/worktree-janitor/worktree-janitor.routes.d.ts +2 -1
  31. package/dist/backend/backend/src/controllers/worktree-janitor/worktree-janitor.routes.d.ts.map +1 -1
  32. package/dist/backend/backend/src/controllers/worktree-janitor/worktree-janitor.routes.js +4 -2
  33. package/dist/backend/backend/src/controllers/worktree-janitor/worktree-janitor.routes.js.map +1 -1
  34. package/dist/backend/backend/src/index.d.ts.map +1 -1
  35. package/dist/backend/backend/src/index.js +17 -2
  36. package/dist/backend/backend/src/index.js.map +1 -1
  37. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  38. package/dist/backend/backend/src/services/agent/agent-registration.service.js +9 -1
  39. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  40. package/dist/backend/backend/src/services/project-tickets/delegation-ticket-route.d.ts +63 -0
  41. package/dist/backend/backend/src/services/project-tickets/delegation-ticket-route.d.ts.map +1 -0
  42. package/dist/backend/backend/src/services/project-tickets/delegation-ticket-route.js +80 -0
  43. package/dist/backend/backend/src/services/project-tickets/delegation-ticket-route.js.map +1 -0
  44. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts +92 -1
  45. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts.map +1 -1
  46. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js +255 -4
  47. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js.map +1 -1
  48. package/dist/backend/backend/src/services/worktree/low-disk-guard.d.ts +109 -0
  49. package/dist/backend/backend/src/services/worktree/low-disk-guard.d.ts.map +1 -0
  50. package/dist/backend/backend/src/services/worktree/low-disk-guard.js +148 -0
  51. package/dist/backend/backend/src/services/worktree/low-disk-guard.js.map +1 -0
  52. package/dist/backend/backend/src/services/worktree/scratch-janitor.d.ts +140 -0
  53. package/dist/backend/backend/src/services/worktree/scratch-janitor.d.ts.map +1 -0
  54. package/dist/backend/backend/src/services/worktree/scratch-janitor.js +249 -0
  55. package/dist/backend/backend/src/services/worktree/scratch-janitor.js.map +1 -0
  56. package/dist/backend/backend/src/services/worktree/worktree-janitor.git.d.ts +92 -1
  57. package/dist/backend/backend/src/services/worktree/worktree-janitor.git.d.ts.map +1 -1
  58. package/dist/backend/backend/src/services/worktree/worktree-janitor.git.js +207 -1
  59. package/dist/backend/backend/src/services/worktree/worktree-janitor.git.js.map +1 -1
  60. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts +135 -28
  61. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts.map +1 -1
  62. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js +248 -39
  63. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js.map +1 -1
  64. package/dist/backend/build-info.json +2 -2
  65. package/dist/backend/config/constants.d.ts +13 -0
  66. package/dist/backend/config/constants.d.ts.map +1 -1
  67. package/dist/backend/config/constants.js +13 -0
  68. package/dist/backend/config/constants.js.map +1 -1
  69. package/dist/cli/backend/src/constants.d.ts +59 -3
  70. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  71. package/dist/cli/backend/src/constants.js +54 -3
  72. package/dist/cli/backend/src/constants.js.map +1 -1
  73. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  74. package/dist/cli/backend/src/services/agent/agent-registration.service.js +9 -1
  75. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  76. package/dist/cli/config/constants.d.ts +13 -0
  77. package/dist/cli/config/constants.d.ts.map +1 -1
  78. package/dist/cli/config/constants.js +13 -0
  79. package/dist/cli/config/constants.js.map +1 -1
  80. 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"}
@@ -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): Promise<void>;
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
  *
@@ -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":"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"}
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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"}
@@ -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: [options.self ? `claimed by ${assignee} — WorkItem ${created.id}` : `assigned to ${assignee} — WorkItem ${created.id}`],
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
  *