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
@@ -1483,6 +1483,19 @@ export const PROJECT_TICKET_CONSTANTS = {
1483
1483
  SYNC_SWEEP_INTERVAL_MS: 60 * 1000,
1484
1484
  /** Successor hops followed from a ticket's WorkItem before giving up */
1485
1485
  MAX_SUCCESSOR_HOPS: 8,
1486
+ /**
1487
+ * Delegation through tickets (spec §11): the task-pool body field naming the
1488
+ * project ticket a delegated WorkItem works (`delegate-task --ticket`).
1489
+ */
1490
+ DELEGATION_TICKET_BODY_KEY: 'projectTicketId',
1491
+ /** WorkItem metadata key a delegating skill uses to name the delegator when no X-Agent-Session header is sent */
1492
+ DELEGATION_CALLER_METADATA_KEY: 'delegatedBy',
1493
+ /** WorkItem types that are delegations of work to someone (the only ones routed through tickets) */
1494
+ DELEGATION_WORK_ITEM_TYPES: ['delegate'] as const,
1495
+ /** Max characters of a ticket title derived from a delegation's title */
1496
+ DELEGATION_TITLE_MAX_CHARS: 120,
1497
+ /** Error `code` of a task-pool add refused by the ticket rules (skills stop instead of delivering) */
1498
+ DELEGATION_REFUSED_CODE: 'project_ticket_refused',
1486
1499
  } as const;
1487
1500
 
1488
1501
  /**
@@ -878,6 +878,7 @@ Every project keeps its own backlog in `<project>/.crewly/tickets/` — one mark
878
878
  - **Owner says "put this in the backlog" / "add a ticket for …"** → `project-tickets create --project <path> --title "…" --acceptance "…" --source request:<TKT id>` and tell the owner the new ticket id. It starts in `backlog`; do not start the work unless the owner also asked for that.
879
879
  - **Owner says a ticket should be done now** → `project-tickets update --project <path> --id <ID> --status ready`. Idle members of the project's teams pick up `ready` tickets themselves (highest priority first).
880
880
  - **A specific person must do it** → `project-tickets assign --project <path> --id <ID> --to <session>`.
881
+ - **Delegating project work** (`delegate-task` to an agent whose team works on a project) always runs through a ticket: pass `--ticket <ID>` when one exists, otherwise one is created and returned as `projectTicket`. Work already running without a ticket → `project-tickets link --project <path> --id <ID> --work-item <WorkItem id>`.
881
882
  - A ticket moves to `done` by itself when its WorkItem is verified (or to `review` when the owner wants to check it personally — then the owner closes it).
882
883
 
883
884
  ### Task Routing
@@ -52,6 +52,7 @@ Use when: Orchestrator sends a new objective. Creates one WorkItem per sub-task
52
52
  bash {{TL_SKILLS_PATH}}/delegate-task/execute.sh '{"to":"worker-session","task":"implement feature","priority":"high","teamId":"{{TEAM_ID}}","tlMemberId":"{{MEMBER_ID}}","projectPath":"{{PROJECT_PATH}}"}'
53
53
  ```
54
54
  Use when: After decompose-goal, or when handle-failure says reassign. Validates hierarchy before delegation.
55
+ Project work for a worker always runs through a project ticket: add `"ticket":"APP-12"` (or `--ticket APP-12`) when a ticket exists; otherwise one is created for you and printed. Refused (nothing delivered) when that ticket is already being worked or closed.
55
56
 
56
57
  ### 3. verify-output — Check completed work quality
57
58
  ```bash
@@ -103,6 +104,8 @@ Use when: A specific worker should take a specific ticket of the project backlog
103
104
 
104
105
  Each project keeps its own backlog in `<project>/.crewly/tickets/` (one markdown file per ticket, tracked in git). Use the `project-tickets` skill (`{{AGENT_SKILLS_PATH}}/core/project-tickets/execute.sh`) to list, read, create and update tickets. Workers pick up `ready` tickets by themselves when idle; you decide what is `ready` (`update --status ready`), you may assign (`assign-ticket`), and you review the work through the normal verification item. Worker-created tickets land in `backlog` until you or the owner make them ready.
105
106
 
107
+ **Every piece of project work for a worker has a ticket.** Delegate with `--ticket <ID>` when the ticket exists (or `assign-ticket`); without it `delegate-task` creates one for you. Work already running without a ticket: `project-tickets link --project {{PROJECT_PATH}} --id <ID> --work-item <WorkItem id>`. Backlog tickets that should be picked up next: `update --status ready`. Reminders you set for yourself get no ticket.
108
+
106
109
  ---
107
110
 
108
111
  ## Brief Reception Protocol
@@ -33,6 +33,7 @@ Use when: Orchestrator sends a new objective. Creates one WorkItem per sub-task
33
33
  bash {{TL_SKILLS_PATH}}/delegate-task/execute.sh '{"to":"worker-session","task":"implement feature","priority":"high","teamId":"{{TEAM_ID}}","tlMemberId":"{{MEMBER_ID}}","projectPath":"{{PROJECT_PATH}}"}'
34
34
  ```
35
35
  Use when: After decompose-goal, or when handle-failure says reassign. Validates hierarchy before delegation.
36
+ Project work for a worker always runs through a project ticket: add `"ticket":"APP-12"` (or `--ticket APP-12`) when a ticket exists; otherwise one is created for you and printed. Refused (nothing delivered) when that ticket is already being worked or closed.
36
37
 
37
38
  #### 3. verify-output — Check completed work quality
38
39
  ```bash
@@ -89,6 +90,8 @@ Use when: A specific worker should take a specific ticket of the project backlog
89
90
  #### Project backlog (project tickets)
90
91
  Each project keeps its own backlog in `<project>/.crewly/tickets/` (one markdown file per ticket, tracked in git). Use the `project-tickets` skill to list, read, create and update tickets. Workers pick up `ready` tickets by themselves when idle; you decide what is `ready`, you may assign (`assign-ticket`), and you review the work through the normal verification item. Worker-created tickets land in `backlog` until you or the owner make them ready.
91
92
 
93
+ **Every piece of project work for a worker has a ticket.** Delegate with `--ticket <ID>` when the ticket exists (or `assign-ticket`); without it `delegate-task` creates one for you. Work already running without a ticket: `project-tickets link --project {{PROJECT_PATH}} --id <ID> --work-item <WorkItem id>`. Backlog tickets that should be picked up next: `update --status ready`. Reminders you set for yourself get no ticket.
94
+
92
95
  ---
93
96
 
94
97
  ### Standard Operating Procedure (5-Step SOP)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: Project Tickets
3
- description: A project's own backlog — list, read, create, update, claim, release and log project tickets (markdown files in <project>/.crewly/tickets/, tracked in git).
3
+ description: A project's own backlog — list, read, create, update, claim, release, assign, link and log project tickets (markdown files in <project>/.crewly/tickets/, tracked in git).
4
4
  version: 1.0.0
5
5
  category: task-management
6
6
  skillType: claude-skill
@@ -125,3 +125,19 @@ bash execute.sh assign --project P --id APP-12 --to worker-session --no-start
125
125
  Only the owner, the orchestrator or a lead of a team on the project may
126
126
  assign, and only to members of those teams (a person's name is recorded
127
127
  without a WorkItem). Team leads also have the `assign-ticket` skill.
128
+
129
+ ## Link work already in flight (orchestrator / team lead)
130
+
131
+ ```bash
132
+ bash execute.sh link --project P --id APP-12 --work-item <WorkItem id>
133
+ ```
134
+
135
+ For a WorkItem that is already queued or running without a ticket (e.g. it was
136
+ delegated before tickets were used). The ticket becomes `in_progress` (a ticket
137
+ in `review` stays there), its assignee is the WorkItem's target, and it closes
138
+ by itself when that WorkItem is verified. Refused when the ticket is done or
139
+ cancelled, the WorkItem is finished or already works another ticket, or the
140
+ ticket already has another live WorkItem.
141
+
142
+ New delegations need no `link`: `delegate-task` puts work for a teammate on a
143
+ project through a ticket by itself (pass `--ticket APP-12` to use an existing one).
@@ -16,6 +16,7 @@
16
16
  # bash execute.sh release --project P --id APP-12 [--note "why"]
17
17
  # bash execute.sh assign --project P --id APP-12 --to <session> [--no-start] (owner / orc / lead)
18
18
  # bash execute.sh log --project P --id APP-12 --note "progress"
19
+ # bash execute.sh link --project P --id APP-12 --work-item <WorkItem id> (owner / orc / lead)
19
20
  # bash execute.sh '{"action":"create","project":"P","title":"…"}'
20
21
  #
21
22
  # P = project id, name, or absolute path.
@@ -39,6 +40,9 @@ Usage:
39
40
  bash execute.sh assign --project P --id APP-12 --to <session> [--no-start]
40
41
  Orchestrator / team lead: put someone on it
41
42
  bash execute.sh log --project P --id APP-12 --note "…" Add a progress note to the ticket's Log
43
+ bash execute.sh link --project P --id APP-12 --work-item <id>
44
+ Orchestrator / team lead: tie work already in
45
+ flight (a live WorkItem) to this ticket
42
46
 
43
47
  P = project id, name or absolute path. Workers' new tickets start in backlog;
44
48
  the owner, the orchestrator or a team lead makes them ready.
@@ -46,7 +50,7 @@ EOF_USAGE
46
50
  }
47
51
 
48
52
  ACTION=""; PROJECT=""; ID=""; TITLE=""; DESCRIPTION=""; PRIORITY=""; LABELS=""; TEAM=""
49
- STATUS=""; SOURCE=""; REQUEST_ID=""; NOTE=""; OWNER_REVIEW=""; ASSIGNEE=""; START="true"
53
+ STATUS=""; SOURCE=""; REQUEST_ID=""; NOTE=""; OWNER_REVIEW=""; ASSIGNEE=""; START="true"; WORK_ITEM=""
50
54
  ACCEPTANCE_JSON="null"
51
55
  HAS_DESCRIPTION=0
52
56
 
@@ -70,6 +74,7 @@ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
70
74
  ACCEPTANCE_JSON=$(printf '%s' "$J" | jq -c 'if (.acceptance|type) == "array" then .acceptance else null end')
71
75
  ASSIGNEE=$(printf '%s' "$J" | jq -r '.to // .assignee // empty')
72
76
  START=$(printf '%s' "$J" | jq -r 'if .start == false then "false" else "true" end')
77
+ WORK_ITEM=$(printf '%s' "$J" | jq -r '.workItemId // .workItem // empty')
73
78
  fi
74
79
  if [[ -z "$ACTION" && $# -gt 0 && ${1:0:1} != '-' ]]; then ACTION="$1"; shift; fi
75
80
 
@@ -91,13 +96,15 @@ while [[ $# -gt 0 ]]; do
91
96
  --owner-review) OWNER_REVIEW="true"; shift ;;
92
97
  --to|--assignee) [ $# -ge 2 ] || error_exit "--to requires a value"; ASSIGNEE="$2"; shift 2 ;;
93
98
  --no-start) START="false"; shift ;;
99
+ --work-item|--work-item-id|--workItemId)
100
+ [ $# -ge 2 ] || error_exit "--work-item requires a value"; WORK_ITEM="$2"; shift 2 ;;
94
101
  --full) shift ;;
95
102
  --help|-h) print_usage; exit 0 ;;
96
103
  *) error_exit "Unknown option: $1" ;;
97
104
  esac
98
105
  done
99
106
 
100
- [ -n "$ACTION" ] || { print_usage >&2; error_exit "Missing action: list | show | create | update | claim | release | assign | log"; }
107
+ [ -n "$ACTION" ] || { print_usage >&2; error_exit "Missing action: list | show | create | update | claim | release | assign | log | link"; }
101
108
 
102
109
  # URL-encode a path segment (project paths contain slashes).
103
110
  enc() { jq -rn --arg v "$1" '$v|@uri'; }
@@ -176,7 +183,13 @@ case "$ACTION" in
176
183
  BODY=$(jq -n --arg note "$NOTE" '{note: $note}')
177
184
  api_call POST "/project-tickets/$(enc "$PROJECT")/$(enc "$ID")/log" "$BODY" | jq '{success, lastLog: (.data.log | last)}'
178
185
  ;;
186
+ link)
187
+ require_param "project" "$PROJECT"; require_param "id" "$ID"; require_param "work-item" "$WORK_ITEM"
188
+ BODY=$(jq -n --arg wi "$WORK_ITEM" '{workItemId: $wi}')
189
+ api_call POST "/project-tickets/$(enc "$PROJECT")/$(enc "$ID")/link" "$BODY" \
190
+ | jq "{success, workItemId: .data.workItem.id, ticket: (.data.ticket | ${TICKET_ROW})}"
191
+ ;;
179
192
  *)
180
- error_exit "Unknown action: $ACTION (use list | show | create | update | claim | release | assign | log)"
193
+ error_exit "Unknown action: $ACTION (use list | show | create | update | claim | release | assign | log | link)"
181
194
  ;;
182
195
  esac
@@ -37,6 +37,8 @@ class H(BaseHTTPRequestHandler):
37
37
  def do_POST(self):
38
38
  n = int(self.headers.get('content-length', '0')); body = json.loads(self.rfile.read(n).decode() or '{}')
39
39
  self.record(body)
40
+ if self.path.endswith('/link'):
41
+ return self.reply(200, {'success': True, 'data': {'workItem': {'id': body.get('workItemId')}, 'ticket': dict(T, status='in_progress', assignee='dev-bo', workItemId=body.get('workItemId'))}})
40
42
  if self.path.endswith('/claim'):
41
43
  return self.reply(200, {'success': True, 'data': {'claimed': True, 'workItem': {'id': 'wi-9'}, 'ticket': dict(T, status='in_progress', assignee='dev-ann', workItemId='wi-9')}})
42
44
  if 'forbidden' in self.path:
@@ -106,6 +108,15 @@ OUT=$(run log --project p1 --id APP-1 --note "halfway")
106
108
  check "log: body" "$(last '.body | tostring')" '{"note":"halfway"}'
107
109
  check "log: last line" "$(printf '%s' "$OUT" | jq -r .lastLog)" "b · dev · note"
108
110
 
111
+ # --- link (orchestrator / lead): tie a live WorkItem to a ticket ---
112
+ OUT=$(run link --project p1 --id APP-1 --work-item wi-42)
113
+ check "link: path" "$(last .path)" "/api/project-tickets/p1/APP-1/link"
114
+ check "link: body" "$(last '.body | tostring')" '{"workItemId":"wi-42"}'
115
+ check "link: output" "$(printf '%s' "$OUT" | jq -c '[.workItemId, .ticket.status, .ticket.workItemId]')" '["wi-42","in_progress","wi-42"]'
116
+ run '{"action":"link","project":"p1","id":"APP-1","workItemId":"wi-43"}' >/dev/null
117
+ check "link json" "$(last '.body | tostring')" '{"workItemId":"wi-43"}'
118
+ check "link: missing work item" "$(run_err link --project p1 --id APP-1 | grep -c 'work-item')" "1"
119
+
109
120
  # --- errors ---
110
121
  check "missing action" "$(run_err | grep -c 'Missing action')" "1"
111
122
  check "unknown action" "$(run_err explode | grep -c 'Unknown action')" "1"
@@ -47,8 +47,26 @@ The script auto-resolves `config/skills/...` references to absolute paths so del
47
47
  | `--task-type` | `taskType` | No | Task type: `general`, `technical` (default: `general`) |
48
48
  | `--force-cross-team` | `forceCrossTeam` | No | Allow cross-team delegation |
49
49
  | `--request-id` / `-R` | `requestId` | No | Ticket (Request) this work is for — the id or `TKT-123` from the `[TICKET:TKT-123 <id>]` line of the message you are acting on. Links the WorkItem into `Request.workItemIds[]`. Omit it and the WorkItem is still linked when your current turn has exactly one ticket |
50
+ | `--ticket` | `ticket` | No | Project ticket this work is for (e.g. `APP-12`). Without it a ticket is created for you when the target works on a project — see Project tickets |
50
51
  | `--fallback-minutes` | — | No | Minutes until the ONE §3.0 fallback check fires for this delegation (default `120` ≈ 2× a TL milestone ETA). `0` disables it. Not cancelled on completion — keep it at 2× ETA, never a poll interval |
51
52
 
53
+ ## Project tickets
54
+
55
+ Work for a teammate on a project always runs through a project ticket
56
+ (`<project>/.crewly/tickets/`, spec `2026-09-28-project-tickets.md` §11):
57
+
58
+ - `--ticket APP-12` — that ticket (must be `backlog` or `ready`, with no other
59
+ live WorkItem) is assigned to `--to` and this WorkItem becomes its work. A
60
+ ticket that is already being worked, done, cancelled or unknown is refused:
61
+ nothing is delivered and the reason is printed.
62
+ - no `--ticket` — a ticket is created from this delegation (title, brief,
63
+ the target's team, priority) and set `in_progress` for the target. The
64
+ output names it (`projectTicket`).
65
+
66
+ No ticket is made for work you target at yourself, review/verify items,
67
+ system or scheduled items, or targets whose team has no project. Work already
68
+ in flight without a ticket: `project-tickets link --project P --id APP-12 --work-item <id>`.
69
+
52
70
  ## Usage — CLI Flags (preferred)
53
71
 
54
72
  ```bash
@@ -17,6 +17,7 @@ TASK_TYPE="general"
17
17
  TEAM_ID=""
18
18
  FORCE_CROSS_TEAM="false"
19
19
  REQUEST_ID=""
20
+ PROJECT_TICKET=""
20
21
  # Self-heal fix #3 (2026-05-20): default fallback timer. Per the
21
22
  # orchestrator prompt §3.0 the orc is supposed to set 2× ETA manually
22
23
  # for every dispatch, but the convention was unenforced — observable in
@@ -54,11 +55,13 @@ while [[ $# -gt 0 ]]; do
54
55
  --task-type) TASK_TYPE="$2"; shift 2 ;;
55
56
  --team|-g) TEAM_ID="$2"; shift 2 ;;
56
57
  --request-id|-R) REQUEST_ID="$2"; shift 2 ;;
58
+ --ticket) PROJECT_TICKET="$2"; shift 2 ;;
57
59
  --force-cross-team) FORCE_CROSS_TEAM="true"; shift ;;
58
60
  --fallback-minutes) FALLBACK_MINUTES="$2"; shift 2 ;;
59
61
  --json|-j) INPUT_JSON="$2"; shift 2 ;;
60
62
  --help|-h)
61
- echo "Usage: execute.sh --to agent-session --task 'implement feature' --priority high --project /path [--team teamId] [--context 'extra info']"
63
+ echo "Usage: execute.sh --to agent-session --task 'implement feature' --priority high --project /path [--team teamId] [--context 'extra info'] [--ticket <project ticket id, e.g. APP-12>]"
64
+ echo "Work for an agent on a project always runs through a project ticket: --ticket uses that backlog/ready ticket, otherwise one is created for you."
62
65
  exit 0
63
66
  ;;
64
67
  --) shift; break ;;
@@ -94,6 +97,7 @@ if [ -n "$INPUT_JSON" ]; then
94
97
  [ -z "$TEAM_ID" ] && TEAM_ID=$(printf '%s' "$INPUT" | jq -r '.teamId // empty')
95
98
  [ "$FORCE_CROSS_TEAM" = "false" ] && FORCE_CROSS_TEAM=$(printf '%s' "$INPUT" | jq -r '.forceCrossTeam // "false"')
96
99
  [ -z "$REQUEST_ID" ] && REQUEST_ID=$(printf '%s' "$INPUT" | jq -r '.requestId // empty')
100
+ [ -z "$PROJECT_TICKET" ] && PROJECT_TICKET=$(printf '%s' "$INPUT" | jq -r '.ticket // .projectTicketId // empty')
97
101
  fi
98
102
 
99
103
  require_param "to (--to)" "$TO"
@@ -292,17 +296,29 @@ POOL_BODY=$(jq -n \
292
296
  --arg priority "$WI_PRIORITY" \
293
297
  --arg projectPath "${PROJECT_PATH:-}" \
294
298
  --arg requestId "${REQUEST_ID:-}" \
295
- '{type: $type, owner: $owner, target: $target, title: $title, description: $description, briefMarkdown: $briefMarkdown, metadata: ({priority: $priority} + (if $projectPath != "" then {projectPath: $projectPath} else {} end))} + (if $requestId != "" then {requestId: $requestId} else {} end)')
299
+ --arg projectTicketId "${PROJECT_TICKET:-}" \
300
+ --arg delegatedBy "${CREWLY_SESSION_NAME:-}" \
301
+ '{type: $type, owner: $owner, target: $target, title: $title, description: $description, briefMarkdown: $briefMarkdown, metadata: ({priority: $priority} + (if $projectPath != "" then {projectPath: $projectPath} else {} end) + (if $delegatedBy != "" then {delegatedBy: $delegatedBy} else {} end))} + (if $requestId != "" then {requestId: $requestId} else {} end) + (if $projectTicketId != "" then {projectTicketId: $projectTicketId} else {} end)')
302
+ # projectTicketId / metadata.delegatedBy: work for an agent on a project runs
303
+ # through a project ticket (specs/2026-09-28-project-tickets.md §11). The
304
+ # backend uses --ticket, or creates a ticket and links this WorkItem to it.
296
305
 
297
306
  # Pipeline-#4 fix (spec 2026-05-05-request-decompose-pipeline-gap.md, Patch B):
298
307
  # Route is /api/task-pool/add (not /api/pool/add — that endpoint does not exist
299
308
  # and previously returned 404, silently swallowed by the || fallback below).
300
- POOL_RESULT=$(api_call POST "/task-pool/add" "$POOL_BODY" 2>/dev/null || echo '{"success":false}')
309
+ POOL_ERR_FILE="$(mktemp)"
310
+ POOL_RESULT=$(api_call POST "/task-pool/add" "$POOL_BODY" 2>"$POOL_ERR_FILE" || echo '{"success":false}')
311
+ POOL_ERR="$(cat "$POOL_ERR_FILE" 2>/dev/null || true)"
312
+ rm -f "$POOL_ERR_FILE"
301
313
  POOL_OK=$(echo "$POOL_RESULT" | jq -r '.success // "false"' 2>/dev/null)
302
314
  WI_ID=$(echo "$POOL_RESULT" | jq -r '.data.id // .workItemId // empty' 2>/dev/null || true)
315
+ PROJECT_TICKET_INFO=$(echo "$POOL_RESULT" | jq -c '.data.projectTicket // null' 2>/dev/null || echo 'null')
316
+ [ -n "$PROJECT_TICKET_INFO" ] || PROJECT_TICKET_INFO='null'
303
317
 
304
318
  if [ "$POOL_OK" != "true" ]; then
305
- echo "{\"error\":\"Failed to create WorkItem in TaskPool\",\"details\":$(echo "$POOL_RESULT" | jq -c . 2>/dev/null || echo '{}')}"
319
+ # Surface the backend's reason (e.g. a --ticket refused by the project-ticket rules).
320
+ ERR_DETAILS=$(printf '%s' "$POOL_ERR" | grep '"error":true' | tail -1 | jq -c '.details // .' 2>/dev/null || true)
321
+ echo "{\"error\":\"Failed to create WorkItem in TaskPool\",\"details\":${ERR_DETAILS:-$(echo "$POOL_RESULT" | jq -c . 2>/dev/null || echo '{}')}}"
306
322
  exit 1
307
323
  fi
308
324
 
@@ -400,5 +416,7 @@ jq -n \
400
416
  --arg message "$RESULT_MESSAGE" \
401
417
  --arg fallbackTriggerId "${FALLBACK_TRIGGER_ID:-}" \
402
418
  --arg fallbackMinutes "${FALLBACK_MINUTES:-0}" \
419
+ --argjson projectTicket "$PROJECT_TICKET_INFO" \
403
420
  '{success: true, workItemId: $workItemId, target: $target, priority: $priority, title: $title, state: $state, message: $message}
404
- + (if $fallbackTriggerId != "" then {fallbackTriggerId: $fallbackTriggerId, fallbackMinutes: ($fallbackMinutes|tonumber)} else {} end)'
421
+ + (if $fallbackTriggerId != "" then {fallbackTriggerId: $fallbackTriggerId, fallbackMinutes: ($fallbackMinutes|tonumber)} else {} end)
422
+ + (if $projectTicket != null then {projectTicket: $projectTicket} else {} end)'
@@ -47,7 +47,15 @@ api_call() {
47
47
  echo "\${method} \${path} \${body}" >> "${CALL_LOG}"
48
48
  case "\${path}" in
49
49
  /task-pool/add)
50
- echo '{"success":true,"data":{"id":"wi-stub-id"}}'
50
+ if [ "\${TEST_ADD_REFUSE:-0}" = "1" ]; then
51
+ # Mirrors the real api_call on a 4xx: error envelope on stderr, exit 1.
52
+ echo '{"error":true,"status":409,"details":{"success":false,"error":"APP-3 is already being worked in WorkItem wi-9","code":"project_ticket_refused"}}' >&2
53
+ return 1
54
+ elif [ "\${TEST_ADD_TICKET:-0}" = "1" ]; then
55
+ echo '{"success":true,"data":{"id":"wi-stub-id","projectTicket":{"id":"APP-4","status":"in_progress","projectPath":"/p","project":"App","created":true}}}'
56
+ else
57
+ echo '{"success":true,"data":{"id":"wi-stub-id"}}'
58
+ fi
51
59
  ;;
52
60
  /task-pool/claim)
53
61
  if [ "\${TEST_CLAIM_FAIL:-0}" = "1" ]; then
@@ -386,6 +394,41 @@ else
386
394
  fi
387
395
  assert_log_contains "add still fires without a quoted Request" "POST /task-pool/add"
388
396
 
397
+ # ---------------------------------------------------------------------------
398
+ # Test 9: delegation through project tickets (spec 2026-09-28 §11)
399
+ # ---------------------------------------------------------------------------
400
+ echo ""
401
+ echo "=== ORC delegate-task — project tickets ==="
402
+ > "$CALL_LOG"
403
+ OUT=$(CALL_LOG="$CALL_LOG" CREWLY_ROOT=/tmp/crewly-test CREWLY_SESSION_NAME=crewly-orc \
404
+ bash "${SKILL_PARENT}/delegate-task/execute.sh" --to crewly-test-bob --task "Ship it" --ticket APP-3 --fallback-minutes 0 2>&1 || true)
405
+ assert_log_contains "--ticket travels as projectTicketId" '"projectTicketId": "APP-3"'
406
+ assert_log_contains "the delegator is stamped as metadata.delegatedBy" '"delegatedBy": "crewly-orc"'
407
+
408
+ > "$CALL_LOG"
409
+ OUT=$(CALL_LOG="$CALL_LOG" CREWLY_ROOT=/tmp/crewly-test TEST_ADD_TICKET=1 \
410
+ bash "${SKILL_PARENT}/delegate-task/execute.sh" --to crewly-test-bob --task "Ship it" --fallback-minutes 0 2>&1 || true)
411
+ assert_output_contains "result names the ticket the work is tracked under" '"id": "APP-4"' "$OUT"
412
+ if grep -q 'projectTicketId' "$CALL_LOG"; then
413
+ echo " FAIL: projectTicketId sent without --ticket"; FAIL=$((FAIL + 1))
414
+ else
415
+ echo " PASS: no projectTicketId without --ticket"; PASS=$((PASS + 1))
416
+ fi
417
+
418
+ > "$CALL_LOG"
419
+ set +e
420
+ OUT=$(CALL_LOG="$CALL_LOG" CREWLY_ROOT=/tmp/crewly-test TEST_ADD_REFUSE=1 \
421
+ bash "${SKILL_PARENT}/delegate-task/execute.sh" --to crewly-test-bob --task "Ship it" --ticket APP-3 --fallback-minutes 0 2>/dev/null)
422
+ RC=$?
423
+ set -e
424
+ if [ "$RC" -ne 0 ]; then echo " PASS: a refused --ticket fails the skill"; PASS=$((PASS + 1)); else echo " FAIL: refused --ticket exited 0"; FAIL=$((FAIL + 1)); fi
425
+ assert_output_contains "the refusal reason is shown" 'already being worked' "$OUT"
426
+ if grep -q "POST /task-pool/claim" "$CALL_LOG"; then
427
+ echo " FAIL: claim attempted after a refusal"; FAIL=$((FAIL + 1))
428
+ else
429
+ echo " PASS: nothing claimed after a refusal"; PASS=$((PASS + 1))
430
+ fi
431
+
389
432
  # --- Summary ---
390
433
  echo ""
391
434
  echo "=== Results: $PASS passed, $FAIL failed ==="
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "lastUpdated": "2026-09-25T15:19:59.383Z",
3
+ "lastUpdated": "2026-09-29T01:47:07.718Z",
4
4
  "cdnBaseUrl": "https://raw.githubusercontent.com/stevehuang0115/crewly/main",
5
5
  "source": "github",
6
6
  "items": [
@@ -903,12 +903,12 @@
903
903
  "downloads": 0,
904
904
  "rating": 0,
905
905
  "createdAt": "2026-02-22T23:57:06.027Z",
906
- "updatedAt": "2026-09-25T15:19:59.383Z",
906
+ "updatedAt": "2026-09-29T01:43:29.904Z",
907
907
  "source": "config/skills/agent/marketplace/nano-banana-image",
908
908
  "assets": {
909
909
  "archive": "config/skills/agent/marketplace/nano-banana-image",
910
910
  "checksum": "",
911
- "sizeBytes": 4048
911
+ "sizeBytes": 3944
912
912
  },
913
913
  "metadata": {
914
914
  "skillType": "claude-skill",
@@ -923,7 +923,6 @@
923
923
  "ai image"
924
924
  ],
925
925
  "files": [
926
- ".env",
927
926
  ".env.example",
928
927
  "generate.sh",
929
928
  "SKILL.md"
@@ -1407,6 +1406,58 @@
1407
1406
  ]
1408
1407
  }
1409
1408
  },
1409
+ {
1410
+ "id": "seo-ops",
1411
+ "type": "skill",
1412
+ "name": "SEO Ops",
1413
+ "description": "Search Console-driven SEO operations for any site: query patterns (low-CTR top-3, near-miss 4-20, rising queries, keyword cannibalization), per-URL report cards, pre-publish SEO/AEO checks, a gated programmatic-page queue, and a live-diff gate that stops agents from removing things from live pages. Use when auditing organic search, deciding what to rewrite, or before changing a live page. For writing posts use seo-blog-writer instead.",
1414
+ "author": "Crewly",
1415
+ "version": "1.0.0",
1416
+ "category": "development",
1417
+ "tags": [
1418
+ "seo",
1419
+ "aeo",
1420
+ "search-console",
1421
+ "marketing",
1422
+ "content"
1423
+ ],
1424
+ "license": "MIT",
1425
+ "downloads": 0,
1426
+ "rating": 0,
1427
+ "createdAt": "2026-09-29T01:43:29.904Z",
1428
+ "updatedAt": "2026-09-29T01:47:07.718Z",
1429
+ "source": "config/skills/agent/marketplace/seo-ops",
1430
+ "assets": {
1431
+ "archive": "config/skills/agent/marketplace/seo-ops",
1432
+ "checksum": "",
1433
+ "sizeBytes": 95495
1434
+ },
1435
+ "metadata": {
1436
+ "skillType": "claude-skill",
1437
+ "assignableRoles": [
1438
+ "marketing",
1439
+ "content-strategist",
1440
+ "generalist",
1441
+ "developer",
1442
+ "fullstack-dev"
1443
+ ],
1444
+ "triggers": [
1445
+ "seo report",
1446
+ "search console",
1447
+ "keyword cannibalization",
1448
+ "pre-publish seo check",
1449
+ "programmatic seo pages",
1450
+ "live page diff"
1451
+ ],
1452
+ "files": [
1453
+ "execute.sh",
1454
+ "pattern-template.example.md",
1455
+ "seo_ops.py",
1456
+ "seo-ops.config.example.json",
1457
+ "SKILL.md"
1458
+ ]
1459
+ }
1460
+ },
1410
1461
  {
1411
1462
  "id": "social-media-post",
1412
1463
  "type": "skill",
@@ -48,6 +48,24 @@ Assigns a task to a worker within the Team Leader's subordinate scope. Validates
48
48
  | `--tl-member` | `tlMemberId` | No | TL's member ID for hierarchy validation |
49
49
  | `--from` | `fromSession` | No | Delegating TL's session name (for monitoring) |
50
50
  | `--request-id` / `-R` | `requestId` | No | Ticket this work is for: the id (or `TKT-123`) from the `[TICKET:TKT-123 <id>]` line of the message you are acting on. Omit it and the task is still linked when your current turn has exactly one ticket |
51
+ | `--ticket` | `ticket` | No | Project ticket this work is for (e.g. `APP-12`). Without it a ticket is created for you when the target works on a project — see Project tickets |
52
+
53
+ ## Project tickets
54
+
55
+ Work for a teammate on a project always runs through a project ticket
56
+ (`<project>/.crewly/tickets/`, spec `2026-09-28-project-tickets.md` §11):
57
+
58
+ - `--ticket APP-12` — that ticket (must be `backlog` or `ready`, with no other
59
+ live WorkItem) is assigned to `--to` and this WorkItem becomes its work. A
60
+ ticket that is already being worked, done, cancelled or unknown is refused:
61
+ nothing is delivered and the reason is printed.
62
+ - no `--ticket` — a ticket is created from this delegation (title, brief,
63
+ the target's team, priority) and set `in_progress` for the target. The
64
+ output names it (`projectTicket`).
65
+
66
+ No ticket is made for work you target at yourself, review/verify items,
67
+ system or scheduled items, or targets whose team has no project. Work already
68
+ in flight without a ticket: `project-tickets link --project P --id APP-12 --work-item <id>`.
51
69
 
52
70
  ## Request Contract check
53
71
 
@@ -17,6 +17,7 @@ TEAM_ID=""
17
17
  TL_MEMBER_ID=""
18
18
  FROM_SESSION=""
19
19
  REQUEST_ID=""
20
+ PROJECT_TICKET=""
20
21
 
21
22
  # Detect legacy JSON argument
22
23
  if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
@@ -36,9 +37,11 @@ while [[ $# -gt 0 ]]; do
36
37
  --tl-member) TL_MEMBER_ID="$2"; shift 2 ;;
37
38
  --from) FROM_SESSION="$2"; shift 2 ;;
38
39
  --request-id|-R) REQUEST_ID="$2"; shift 2 ;;
40
+ --ticket) PROJECT_TICKET="$2"; shift 2 ;;
39
41
  --json|-j) INPUT_JSON="$2"; shift 2 ;;
40
42
  --help|-h)
41
- echo "Usage: execute.sh --to worker-session --task 'implement feature' --priority high --project /path [--team teamId] [--tl-member memberId] [--request-id <ticket id from the [TICKET:TKT-123 <id>] line>]"
43
+ echo "Usage: execute.sh --to worker-session --task 'implement feature' --priority high --project /path [--team teamId] [--tl-member memberId] [--request-id <ticket id from the [TICKET:TKT-123 <id>] line>] [--ticket <project ticket id, e.g. APP-12>]"
44
+ echo "Work for a teammate on a project always runs through a project ticket: --ticket uses that backlog/ready ticket, otherwise one is created for you."
42
45
  exit 0
43
46
  ;;
44
47
  --) shift; break ;;
@@ -75,6 +78,7 @@ if [ -n "$INPUT_JSON" ]; then
75
78
  [ -z "$TL_MEMBER_ID" ] && TL_MEMBER_ID=$(printf '%s' "$INPUT" | jq -r '.tlMemberId // empty')
76
79
  [ -z "$FROM_SESSION" ] && FROM_SESSION=$(printf '%s' "$INPUT" | jq -r '.fromSession // empty')
77
80
  [ -z "$REQUEST_ID" ] && REQUEST_ID=$(printf '%s' "$INPUT" | jq -r '.requestId // empty')
81
+ [ -z "$PROJECT_TICKET" ] && PROJECT_TICKET=$(printf '%s' "$INPUT" | jq -r '.ticket // .projectTicketId // empty')
78
82
  fi
79
83
 
80
84
  require_param "to (--to)" "$TO"
@@ -212,15 +216,38 @@ POOL_BODY=$(jq -n \
212
216
  --arg priority "$WI_PRIORITY" \
213
217
  --arg projectPath "${PROJECT_PATH:-}" \
214
218
  --arg requestId "${REQUEST_ID:-}" \
215
- '{type: $type, owner: $owner, target: $target, title: $title, description: $description, briefMarkdown: $briefMarkdown, metadata: ({priority: $priority, directDelivery: true} + (if $projectPath != "" then {projectPath: $projectPath} else {} end))} + (if $requestId != "" then {requestId: $requestId} else {} end)')
219
+ --arg projectTicketId "${PROJECT_TICKET:-}" \
220
+ --arg delegatedBy "${CREWLY_SESSION_NAME:-${FROM_SESSION:-}}" \
221
+ '{type: $type, owner: $owner, target: $target, title: $title, description: $description, briefMarkdown: $briefMarkdown, metadata: ({priority: $priority, directDelivery: true} + (if $projectPath != "" then {projectPath: $projectPath} else {} end) + (if $delegatedBy != "" then {delegatedBy: $delegatedBy} else {} end))} + (if $requestId != "" then {requestId: $requestId} else {} end) + (if $projectTicketId != "" then {projectTicketId: $projectTicketId} else {} end)')
216
222
  # metadata.directDelivery: this script delivers the brief itself (below), so
217
223
  # the backend's workitem:queued push holds off and only fires if that
218
224
  # delivery never lands — the task reaches the worker once.
219
-
220
- POOL_RESULT=$(api_call POST "/task-pool/add" "$POOL_BODY" 2>/dev/null || echo '{"success":false}')
225
+ # projectTicketId / metadata.delegatedBy: work for a teammate on a project runs
226
+ # through a project ticket (specs/2026-09-28-project-tickets.md §11). The
227
+ # backend uses --ticket, or creates a ticket and links this WorkItem to it.
228
+
229
+ POOL_ERR_FILE="$(mktemp)"
230
+ POOL_RESULT=$(api_call POST "/task-pool/add" "$POOL_BODY" 2>"$POOL_ERR_FILE" || echo '{"success":false}')
231
+ POOL_ERR="$(cat "$POOL_ERR_FILE" 2>/dev/null || true)"
232
+ rm -f "$POOL_ERR_FILE"
221
233
  POOL_OK=$(echo "$POOL_RESULT" | jq -r '.success // "false"' 2>/dev/null)
222
234
  TASK_ID=$(echo "$POOL_RESULT" | jq -r '.data.id // .workItemId // empty' 2>/dev/null || true)
223
235
 
236
+ # A refusal by the project-ticket rules (e.g. --ticket already being worked,
237
+ # done, or unknown) means the delegation must NOT go out: stop here instead
238
+ # of the deliver-anyway path below.
239
+ if [ "$POOL_OK" != "true" ] && printf '%s' "$POOL_ERR" | grep -q 'project_ticket_refused'; then
240
+ REFUSAL=$(printf '%s' "$POOL_ERR" | grep 'project_ticket_refused' | tail -1 | jq -r '.details.error // empty' 2>/dev/null || true)
241
+ jq -n --arg error "${REFUSAL:-Delegation refused by the project-ticket rules}" --arg to "$TO" --arg ticket "${PROJECT_TICKET:-}" \
242
+ '{success: false, error: $error, to: $to, hint: "Nothing was delivered. Fix the ticket (project-tickets show / update --status ready) or delegate without --ticket."} + (if $ticket != "" then {ticket: $ticket} else {} end)'
243
+ exit 1
244
+ fi
245
+
246
+ TICKET_INFO=$(echo "$POOL_RESULT" | jq -c '.data.projectTicket // empty' 2>/dev/null || true)
247
+ if [ -n "$TICKET_INFO" ]; then
248
+ echo "$TICKET_INFO" | jq -c '{projectTicket: ., info: ("Tracked as project ticket " + .id + " (" + .status + (if .created then ", created for this delegation" else "" end) + ")")}'
249
+ fi
250
+
224
251
  # The delivered text names the WorkItem id: the worker needs it for
225
252
  # report-status, and after a fresh-conversation clear the backend finds the
226
253
  # new conversation by it. The deliver body carries it too (workItemId), so
@@ -662,6 +662,65 @@ else
662
662
  PASS=$((PASS + 1))
663
663
  fi
664
664
 
665
+ # -----------------------------------------------------------------------
666
+ # Scenario 24: delegation through project tickets (spec 2026-09-28 §11).
667
+ #
668
+ # Runs a COPY of the real execute.sh next to a lib.sh whose api_call is
669
+ # stubbed (the same layout trick as the ORC delegate-task test), so the
670
+ # production --ticket handling is exercised verbatim.
671
+ # -----------------------------------------------------------------------
672
+ echo "Scenario 24: --ticket, the ticket report, and a refused ticket"
673
+ TK_PARENT=$(mktemp -d)
674
+ TK_LOG=$(mktemp)
675
+ mkdir -p "$TK_PARENT/_common" "$TK_PARENT/delegate-task"
676
+ cat > "$TK_PARENT/_common/lib.sh" <<LIBEOF
677
+ source "$(cd "$SCRIPT_DIR/../.." && pwd)/_common/lib.sh"
678
+ api_call() {
679
+ local method="\$1" path="\$2" body="\${3:-}"
680
+ printf '%s %s %s\n' "\$method" "\$path" "\$(printf '%s' "\$body" | tr -d '\n')" >> "$TK_LOG"
681
+ case "\$path" in
682
+ /task-pool/add)
683
+ if [ "\${TEST_ADD_REFUSE:-0}" = "1" ]; then
684
+ echo '{"error":true,"status":409,"details":{"success":false,"error":"CE-2 is already being worked in WorkItem wi-9","code":"project_ticket_refused"}}' >&2
685
+ return 1
686
+ fi
687
+ echo '{"success":true,"data":{"id":"wi-tl","projectTicket":{"id":"CE-14","status":"in_progress","projectPath":"/p","project":"ce-core","created":true}}}'
688
+ ;;
689
+ *) echo '{"success":true}' ;;
690
+ esac
691
+ }
692
+ LIBEOF
693
+ cp "$SKILL_DIR/execute.sh" "$TK_PARENT/delegate-task/execute.sh"
694
+
695
+ OUT=$(CREWLY_SESSION_NAME=ce-owen bash "$TK_PARENT/delegate-task/execute.sh" --to ce-vera --task "Goal: a. Expected Outcome: b. Eval Criteria: c." --ticket CE-9 2>/dev/null || true)
696
+ if grep -q '"projectTicketId": *"CE-9"' "$TK_LOG" && grep -q '"delegatedBy": *"ce-owen"' "$TK_LOG"; then
697
+ echo " PASS: --ticket and the delegator travel to /task-pool/add"; PASS=$((PASS + 1))
698
+ else
699
+ echo " FAIL: projectTicketId/delegatedBy missing: $(cat "$TK_LOG")"; FAIL=$((FAIL + 1))
700
+ fi
701
+ if printf '%s' "$OUT" | grep -q 'Tracked as project ticket CE-14'; then
702
+ echo " PASS: output names the project ticket"; PASS=$((PASS + 1))
703
+ else
704
+ echo " FAIL: no ticket line in output: $OUT"; FAIL=$((FAIL + 1))
705
+ fi
706
+
707
+ : > "$TK_LOG"
708
+ set +e
709
+ OUT=$(TEST_ADD_REFUSE=1 CREWLY_SESSION_NAME=ce-owen bash "$TK_PARENT/delegate-task/execute.sh" --to ce-vera --task "Goal: a. Expected Outcome: b. Eval Criteria: c." --ticket CE-2 2>/dev/null)
710
+ RC=$?
711
+ set -e
712
+ if [ "$RC" -ne 0 ] && printf '%s' "$OUT" | grep -q 'already being worked'; then
713
+ echo " PASS: a refused --ticket fails with the reason"; PASS=$((PASS + 1))
714
+ else
715
+ echo " FAIL: refused --ticket: rc=$RC out=$OUT"; FAIL=$((FAIL + 1))
716
+ fi
717
+ if grep -q '/deliver' "$TK_LOG"; then
718
+ echo " FAIL: the brief was delivered despite the refusal"; FAIL=$((FAIL + 1))
719
+ else
720
+ echo " PASS: nothing delivered after a refusal"; PASS=$((PASS + 1))
721
+ fi
722
+ rm -rf "$TK_PARENT" "$TK_LOG"
723
+
665
724
  # Cleanup
666
725
  rm -f "$REAL_FN_FILE"
667
726
  rm -rf "$MOCK_DIR"