crewly 1.20.364 → 1.20.366
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/roles/team-leader/prompt.md +2 -0
- package/config/skills/agent/core/attach-file/SKILL.md +7 -0
- package/config/skills/agent/core/attach-file/execute.sh +8 -3
- package/config/skills/agent/core/complete-task/SKILL.md +4 -2
- package/config/skills/agent/core/complete-task/execute.sh +9 -1
- package/config/skills/agent/core/complete-task/explicit-id.test.sh +13 -2
- package/config/skills/agent/core/create-task/SKILL.md +2 -0
- package/config/skills/agent/core/create-task/execute.sh +27 -1
- package/config/skills/agent/core/create-task/execute.test.sh +67 -0
- package/config/skills/agent/core/handoff-task/execute.sh +12 -5
- package/config/skills/agent/core/handoff-task/explicit-id.test.sh +70 -0
- package/config/skills/agent/core/reply/SKILL.md +10 -2
- package/config/skills/agent/core/reply-channel/SKILL.md +17 -0
- package/config/skills/agent/core/reply-channel/execute.sh +17 -2
- package/config/skills/agent/core/reply-channel/execute.test.sh +15 -2
- package/config/skills/agent/core/send-message/SKILL.md +14 -0
- package/config/skills/agent/core/slack-post/SKILL.md +3 -3
- package/config/skills/orchestrator/delegate-task/SKILL.md +1 -1
- package/config/skills/orchestrator/reply-slack/execute.sh +6 -2
- package/config/skills/orchestrator/send-pdf-to-slack/SKILL.md +2 -1
- package/config/skills/orchestrator/send-pdf-to-slack/execute.sh +10 -2
- package/config/skills/registry.json +6 -6
- package/config/skills/team-leader/delegate-task/SKILL.md +1 -1
- package/config/skills/team-leader/delegate-task/execute.sh +6 -0
- package/dist/backend/backend/src/constants.d.ts +26 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +26 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/agent-hooks/agent-hooks.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/agent-hooks/agent-hooks.controller.js +7 -3
- package/dist/backend/backend/src/controllers/agent-hooks/agent-hooks.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js +2 -0
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts +12 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.js +56 -7
- package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts +9 -0
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js +159 -0
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +89 -2
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.js +58 -4
- package/dist/backend/backend/src/controllers/slack/slack.controller.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 +36 -2
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +9 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +16 -10
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +19 -11
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/agent-message-envelope.d.ts +106 -0
- package/dist/backend/backend/src/services/messaging/agent-message-envelope.d.ts.map +1 -0
- package/dist/backend/backend/src/services/messaging/agent-message-envelope.js +124 -0
- package/dist/backend/backend/src/services/messaging/agent-message-envelope.js.map +1 -0
- package/dist/backend/backend/src/services/messaging/owner-hook-message.d.ts +62 -13
- package/dist/backend/backend/src/services/messaging/owner-hook-message.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-hook-message.js +196 -22
- package/dist/backend/backend/src/services/messaging/owner-hook-message.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.d.ts +15 -5
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.js +22 -0
- package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.js.map +1 -1
- package/dist/backend/backend/src/services/orc/agent-mention-post-guard.d.ts +69 -0
- package/dist/backend/backend/src/services/orc/agent-mention-post-guard.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orc/agent-mention-post-guard.js +81 -0
- package/dist/backend/backend/src/services/orc/agent-mention-post-guard.js.map +1 -0
- package/dist/backend/backend/src/services/orc/message-route.d.ts +129 -0
- package/dist/backend/backend/src/services/orc/message-route.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orc/message-route.js +197 -0
- package/dist/backend/backend/src/services/orc/message-route.js.map +1 -0
- package/dist/backend/backend/src/services/orc/owner-completion-report.service.d.ts +34 -3
- package/dist/backend/backend/src/services/orc/owner-completion-report.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/owner-completion-report.service.js +65 -8
- package/dist/backend/backend/src/services/orc/owner-completion-report.service.js.map +1 -1
- package/dist/backend/backend/src/services/orc/owner-completion-report.text.d.ts +15 -2
- package/dist/backend/backend/src/services/orc/owner-completion-report.text.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/owner-completion-report.text.js +21 -6
- package/dist/backend/backend/src/services/orc/owner-completion-report.text.js.map +1 -1
- package/dist/backend/backend/src/services/orc/owner-completion-report.wiring.d.ts +16 -1
- package/dist/backend/backend/src/services/orc/owner-completion-report.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/owner-completion-report.wiring.js +19 -3
- package/dist/backend/backend/src/services/orc/owner-completion-report.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.d.ts +76 -4
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.js +343 -45
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.js.map +1 -1
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts +70 -8
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.js +256 -100
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/orc/triggered-by.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/triggered-by.js +9 -0
- package/dist/backend/backend/src/services/orc/triggered-by.js.map +1 -1
- package/dist/backend/backend/src/services/orc/work-item-destination.d.ts +2 -1
- package/dist/backend/backend/src/services/orc/work-item-destination.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/work-item-destination.js +1 -1
- package/dist/backend/backend/src/services/orc/work-item-destination.js.map +1 -1
- package/dist/backend/backend/src/services/orc/work-item-destination.wiring.d.ts +16 -1
- package/dist/backend/backend/src/services/orc/work-item-destination.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/work-item-destination.wiring.js +33 -4
- package/dist/backend/backend/src/services/orc/work-item-destination.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts +1 -0
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/room-responder.d.ts +7 -3
- package/dist/backend/backend/src/services/slack/room-responder.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/room-responder.js +26 -20
- package/dist/backend/backend/src/services/slack/room-responder.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.js +20 -13
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +17 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +35 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts +20 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.js +58 -3
- package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +31 -0
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js +62 -5
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/backend/backend/src/services/trace/turn-trigger.d.ts +34 -1
- package/dist/backend/backend/src/services/trace/turn-trigger.d.ts.map +1 -1
- package/dist/backend/backend/src/services/trace/turn-trigger.js +76 -6
- package/dist/backend/backend/src/services/trace/turn-trigger.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +26 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +26 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +16 -10
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +19 -11
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/cli/backend/src/services/messaging/agent-message-envelope.d.ts +106 -0
- package/dist/cli/backend/src/services/messaging/agent-message-envelope.d.ts.map +1 -0
- package/dist/cli/backend/src/services/messaging/agent-message-envelope.js +124 -0
- package/dist/cli/backend/src/services/messaging/agent-message-envelope.js.map +1 -0
- package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.d.ts +15 -5
- package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.js +22 -0
- package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.js.map +1 -1
- package/dist/cli/backend/src/services/orc/message-route.d.ts +129 -0
- package/dist/cli/backend/src/services/orc/message-route.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orc/message-route.js +197 -0
- package/dist/cli/backend/src/services/orc/message-route.js.map +1 -0
- package/dist/cli/backend/src/services/orc/owner-completion-report.text.d.ts +15 -2
- package/dist/cli/backend/src/services/orc/owner-completion-report.text.d.ts.map +1 -1
- package/dist/cli/backend/src/services/orc/owner-completion-report.text.js +21 -6
- package/dist/cli/backend/src/services/orc/owner-completion-report.text.js.map +1 -1
- package/dist/cli/backend/src/services/orc/work-item-destination.d.ts +2 -1
- package/dist/cli/backend/src/services/orc/work-item-destination.d.ts.map +1 -1
- package/dist/cli/backend/src/services/orc/work-item-destination.js +1 -1
- package/dist/cli/backend/src/services/orc/work-item-destination.js.map +1 -1
- package/dist/cli/backend/src/services/slack/room-responder.d.ts +7 -3
- package/dist/cli/backend/src/services/slack/room-responder.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/room-responder.js +26 -20
- package/dist/cli/backend/src/services/slack/room-responder.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +17 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +35 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts +20 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.js +58 -3
- package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +31 -0
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js +62 -5
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/cli/backend/src/services/trace/turn-trigger.d.ts +34 -1
- package/dist/cli/backend/src/services/trace/turn-trigger.d.ts.map +1 -1
- package/dist/cli/backend/src/services/trace/turn-trigger.js +76 -6
- package/dist/cli/backend/src/services/trace/turn-trigger.js.map +1 -1
- package/package.json +1 -1
|
@@ -120,6 +120,8 @@ Tickets marked *worker-created — review first* were filed by a team member: ch
|
|
|
120
120
|
|
|
121
121
|
**Owner requests from Slack.** When the owner asks you something in Slack that a member can do, delegate it with `--thread <key>` (the key from `[SLACK-THREAD:<key>]`; in the Crewly chat, `--conversation <id>`). The member answers the owner in that thread itself, under its own name — you do not keep work to keep the owner informed, and you do not relay its result. A short "Sage is on it" from you is enough. Work you delegate on your own initiative gets no `--thread`: its result comes back to you, nothing reaches the owner, and you decide what the owner hears.
|
|
122
122
|
|
|
123
|
+
**A new deliverable gets a new thread.** One thread holds one topic. When a finished deliverable is not what its thread is about (the owner asked for a PDF in a thread that held earlier videos), post it with `reply --new-thread "<title>"` and leave one line with the link in the old thread, so the owner never reads one deliverable as the answer to another.
|
|
124
|
+
|
|
123
125
|
**Team work the orchestrator delegated for the owner: you report it.** When work the orchestrator handed your team for the owner is finished, you tell the owner — in your team channel, as yourself, not in the orchestrator's thread (the orchestrator does not relay it). You get an `[OWNER REPORT DUE]` message saying so. Post one or two plain lines in the owner's language: what the owner approved, the outcome with its link, and the link to the thread the approval came from — `reply --work-item <id> "<message>"`. No ticket ids, no status lines. When you hand such work to a member, the member does not post it to the owner; you do.
|
|
124
126
|
|
|
125
127
|
**Execution nudge.** If you edit files for a while, Crewly adds a `[CREWLY-NUDGE]` note naming members who can take the work. Delegate, or record why you keep it (`--no-member-fits`).
|
|
@@ -47,6 +47,8 @@ backend resolves the Slack channel, the thread and your bot identity from it.
|
|
|
47
47
|
| `--title` | no | Title shown above the file in Slack |
|
|
48
48
|
| `--comment` | no | A line of text posted with the file |
|
|
49
49
|
| `--thread` | no | The thread the file belongs to: the `[SLACK-THREAD:<key>]` key from your prompt, or the id `reply-channel` takes. **Pass it** when you have more than one thread going — without it the file follows the answer you just posted, else the oldest thread still waiting on you. |
|
|
50
|
+
| `--work-item` | no | The work item the file delivers (the id in your `[CREWLY-DISPATCH]`). The file goes into the thread that work came from — use it when the work was delegated to you from a conversation you are not in. No `--channel` needed. |
|
|
51
|
+
| `--ticket` / `--to` | no | The ticket, or the message id from your prompt, the file answers. No `--channel` needed. |
|
|
50
52
|
|
|
51
53
|
## When to use it
|
|
52
54
|
|
|
@@ -76,6 +78,11 @@ it to the owner if they ask why the name differs.
|
|
|
76
78
|
|
|
77
79
|
## Failures
|
|
78
80
|
|
|
81
|
+
`{"success":false,"code":"thread_unresolved","error":"File NOT sent: …"}` —
|
|
82
|
+
Crewly could not tell which thread the file answers. The file is **never**
|
|
83
|
+
posted at the channel's top level instead: run it again with `--work-item <id>`
|
|
84
|
+
(from your `[CREWLY-DISPATCH]`), `--to <message id>` or `--ticket <id>`.
|
|
85
|
+
|
|
79
86
|
`{"success":false,"error":"not_a_slack_channel"}` — that chat channel is not
|
|
80
87
|
mirrored to Slack. Nothing to attach to; reply with text.
|
|
81
88
|
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
#
|
|
8
8
|
# Usage: execute.sh --channel <chatChannelId> --path <file> [--name <n>]
|
|
9
9
|
# [--title <t>] [--comment <c>] [--thread <messageId>]
|
|
10
|
+
# [--work-item <id>] [--ticket <id>] [--to <messageId>]
|
|
10
11
|
set -euo pipefail
|
|
11
12
|
|
|
12
13
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
14
|
# shellcheck source=/dev/null
|
|
14
15
|
source "$SCRIPT_DIR/../../../_common/lib.sh"
|
|
15
16
|
|
|
16
|
-
CHANNEL=""; FILE_PATH=""; NAME=""; TITLE=""; COMMENT=""; THREAD=""; TICKET=""; TO=""
|
|
17
|
+
CHANNEL=""; FILE_PATH=""; NAME=""; TITLE=""; COMMENT=""; THREAD=""; TICKET=""; TO=""; WORK_ITEM=""
|
|
17
18
|
|
|
18
19
|
usage() {
|
|
19
20
|
cat <<'USAGE'
|
|
@@ -27,6 +28,7 @@ attach-file — send a file into your Slack channel
|
|
|
27
28
|
--thread <id> The thread the file belongs to ([SLACK-THREAD:<key>] key, or a chat thread id)
|
|
28
29
|
--ticket <id> The ticket the file is for (TKT-187 / CE-7) — Crewly finds its thread (no --channel needed)
|
|
29
30
|
--to <id> The message the file answers (from your prompt) — no --channel needed
|
|
31
|
+
--work-item <id> The work item the file delivers ([CREWLY-DISPATCH] id) — Crewly posts it in that work's thread (no --channel needed)
|
|
30
32
|
USAGE
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -40,13 +42,14 @@ while [ $# -gt 0 ]; do
|
|
|
40
42
|
--thread|-t) THREAD="${2:-}"; shift 2 ;;
|
|
41
43
|
--ticket) TICKET="${2:-}"; shift 2 ;;
|
|
42
44
|
--to) TO="${2:-}"; shift 2 ;;
|
|
45
|
+
--work-item|--workitem) WORK_ITEM="${2:-}"; shift 2 ;;
|
|
43
46
|
-h|--help) usage; exit 0 ;;
|
|
44
47
|
*) unknown_arg_exit "$1" ;;
|
|
45
48
|
esac
|
|
46
49
|
done
|
|
47
50
|
|
|
48
|
-
if { [ -z "$CHANNEL" ] && [ -z "$TICKET" ] && [ -z "$TO" ]; } || [ -z "$FILE_PATH" ]; then
|
|
49
|
-
echo '{"error":"Missing required parameter: --path, and --channel (or --ticket / --to)"}' >&2
|
|
51
|
+
if { [ -z "$CHANNEL" ] && [ -z "$TICKET" ] && [ -z "$TO" ] && [ -z "$WORK_ITEM" ]; } || [ -z "$FILE_PATH" ]; then
|
|
52
|
+
echo '{"error":"Missing required parameter: --path, and --channel (or --work-item / --ticket / --to)"}' >&2
|
|
50
53
|
usage >&2
|
|
51
54
|
exit 1
|
|
52
55
|
fi
|
|
@@ -65,10 +68,12 @@ BODY=$(jq -cn \
|
|
|
65
68
|
--arg threadId "$THREAD" \
|
|
66
69
|
--arg ticket "$TICKET" \
|
|
67
70
|
--arg to "$TO" \
|
|
71
|
+
--arg workItemId "$WORK_ITEM" \
|
|
68
72
|
'{filePath: $filePath}
|
|
69
73
|
+ (if $channelId != "" then {channelId: $channelId} else {} end)
|
|
70
74
|
+ (if $ticket != "" then {ticket: $ticket} else {} end)
|
|
71
75
|
+ (if $to != "" then {to: $to} else {} end)
|
|
76
|
+
+ (if $workItemId != "" then {workItemId: $workItemId} else {} end)
|
|
72
77
|
+ (if $filename != "" then {filename: $filename} else {} end)
|
|
73
78
|
+ (if $title != "" then {title: $title} else {} end)
|
|
74
79
|
+ (if $comment != "" then {comment: $comment} else {} end)
|
|
@@ -62,14 +62,16 @@ Resolution order:
|
|
|
62
62
|
1. `workItemId`, if you pass it — always wins.
|
|
63
63
|
2. Otherwise, ONLY when you named no task (no `workItemId`, no `taskId`): a lookup of the WorkItem currently `running` against your
|
|
64
64
|
`sessionName` (`GET /api/task-pool/items?status=running&target=<session>`).
|
|
65
|
+
It is used only when exactly ONE item is running. With several, the skill
|
|
66
|
+
refuses (non-zero exit) and lists their ids: pass `workItemId` with one.
|
|
65
67
|
3. If you passed `taskId` without `workItemId`, the skill
|
|
66
68
|
refuses (non-zero exit): it never substitutes your running item for an id you named.
|
|
67
69
|
4. If nothing resolves, the skill exits non-zero with an error naming the
|
|
68
70
|
session it searched. It never reports success without completing something.
|
|
69
71
|
|
|
70
72
|
Pass `workItemId` whenever you know it — the lookup in step 2 depends on the
|
|
71
|
-
pool state at that instant and
|
|
72
|
-
|
|
73
|
+
pool state at that instant, and with two running items it refuses rather
|
|
74
|
+
than guess.
|
|
73
75
|
|
|
74
76
|
## Evidence contract (#873)
|
|
75
77
|
|
|
@@ -138,9 +138,17 @@ fi
|
|
|
138
138
|
if [ -z "$WORK_ITEM_ID" ] && [ -n "$TASK_ID" ]; then
|
|
139
139
|
error_exit "Nothing was completed: you passed taskId '${TASK_ID}' but no 'workItemId', and this skill does not guess which WorkItem you meant. Pass 'workItemId' (find it with GET /api/task-pool/items?target=${SESSION_NAME}) and call again."
|
|
140
140
|
fi
|
|
141
|
+
# The pool lookup only stands in for an explicit id when exactly ONE WorkItem
|
|
142
|
+
# is running: with several, picking the first closed the wrong one and sent
|
|
143
|
+
# its result to the wrong place (message-routing audit 2026-10-18).
|
|
141
144
|
if [ -z "$WORK_ITEM_ID" ]; then
|
|
142
145
|
POOL_RESP=$(api_call_full GET "/task-pool/items?status=running&target=${SESSION_NAME}" 2>/dev/null || echo '{}')
|
|
143
|
-
|
|
146
|
+
RUNNING_IDS=$(echo "$POOL_RESP" | jq -r '(.workItems // .data // [])[]?.id // empty' 2>/dev/null || true)
|
|
147
|
+
RUNNING_COUNT=$(printf '%s\n' "$RUNNING_IDS" | grep -c . || true)
|
|
148
|
+
if [ "${RUNNING_COUNT:-0}" -gt 1 ]; then
|
|
149
|
+
error_exit "Nothing was completed: ${RUNNING_COUNT} WorkItems are running for '${SESSION_NAME}' ($(printf '%s' "$RUNNING_IDS" | tr '\n' ' ' | sed 's/ *$//')) and this skill does not guess which one you finished. Pass 'workItemId' with one of them and call again."
|
|
150
|
+
fi
|
|
151
|
+
WORK_ITEM_ID=$(printf '%s\n' "$RUNNING_IDS" | head -n1)
|
|
144
152
|
fi
|
|
145
153
|
|
|
146
154
|
# Neither an explicit `workItemId` nor the pool lookup produced a target.
|
|
@@ -10,6 +10,8 @@ check() {
|
|
|
10
10
|
|
|
11
11
|
PORT=18854
|
|
12
12
|
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
13
|
+
STUB_RUNNING="$(mktemp)"; export STUB_RUNNING
|
|
14
|
+
echo 'wi-running' > "$STUB_RUNNING"
|
|
13
15
|
: > "$STUB_LOG"
|
|
14
16
|
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
15
17
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
@@ -23,7 +25,8 @@ class H(BaseHTTPRequestHandler):
|
|
|
23
25
|
open(LOG, 'a').write(self.command + ' ' + self.path + '\n')
|
|
24
26
|
def do_GET(self):
|
|
25
27
|
self.note()
|
|
26
|
-
|
|
28
|
+
ids = [l.strip() for l in open(os.environ['STUB_RUNNING']) if l.strip()]
|
|
29
|
+
return self.reply(200, {'workItems': [{'id': i} for i in ids]})
|
|
27
30
|
def do_POST(self):
|
|
28
31
|
n = int(self.headers.get('content-length', '0')); self.rfile.read(n)
|
|
29
32
|
self.note()
|
|
@@ -33,7 +36,7 @@ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
|
33
36
|
PY
|
|
34
37
|
STUB_PID=$!
|
|
35
38
|
disown "$STUB_PID" 2>/dev/null || true
|
|
36
|
-
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
|
|
39
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG" "$STUB_RUNNING"' EXIT
|
|
37
40
|
for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
|
|
38
41
|
|
|
39
42
|
run() { CREWLY_SESSION_NAME=dev-1 CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$1" 2>&1; echo "exit=$?"; }
|
|
@@ -52,5 +55,13 @@ check "explicit workItemId: completes exactly that one" "$(grep -v heartbeat "$S
|
|
|
52
55
|
run '{"sessionName":"dev-1","summary":"done"}' >/dev/null
|
|
53
56
|
check "no id at all: running-item lookup still works" "$(tail -1 "$STUB_LOG")" "POST /api/task-pool/complete/wi-running"
|
|
54
57
|
|
|
58
|
+
# Several running items: no `[0]` pick (message-routing audit 2026-10-18).
|
|
59
|
+
printf 'wi-a\nwi-b\n' > "$STUB_RUNNING"
|
|
60
|
+
: > "$STUB_LOG"
|
|
61
|
+
OUT=$(run '{"sessionName":"dev-1","summary":"done"}')
|
|
62
|
+
check "two running, no id: refused" "$(printf '%s' "$OUT" | grep -c 'exit=1')" "1"
|
|
63
|
+
check "two running, no id: lists both ids" "$(printf '%s' "$OUT" | grep -c 'wi-a wi-b')" "1"
|
|
64
|
+
check "two running, no id: nothing completed" "$(grep -c 'POST /api/task-pool/complete' "$STUB_LOG")" "0"
|
|
65
|
+
|
|
55
66
|
echo "complete-task: $PASS passed, $FAIL failed"
|
|
56
67
|
[ "$FAIL" -eq 0 ]
|
|
@@ -52,6 +52,8 @@ Create a new task via the `/api/task-management/create` endpoint. This allows te
|
|
|
52
52
|
| `--owner` | `owner` | No | Responsible role: `orchestrator`, `team_lead`, `agent` (default), `system`. A role, not a session name — use `--session` for the session |
|
|
53
53
|
| `--description` / `-d` | `description` | No | Short summary of the task |
|
|
54
54
|
| `--brief` | `briefMarkdown` | No | Long-form brief in markdown. Inline text, or `@/path/to/file.md`. Max 16384 bytes |
|
|
55
|
+
| `--thread` | `thread` | No | Owner thread the task answers: the Slack thread key from your prompt. A task you assign to yourself inherits no owner thread from your last turn — name it here when the owner should get its result |
|
|
56
|
+
| `--conversation` | `conversation` | No | Owner conversation the task answers (chat-v2 id) |
|
|
55
57
|
| `--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 |
|
|
56
58
|
|
|
57
59
|
### Always attach a brief
|
|
@@ -32,6 +32,10 @@ Options:
|
|
|
32
32
|
Should carry Goal + Expected Outcome + Eval Criteria. Max 16384 bytes.
|
|
33
33
|
--request-id | -R Ticket this task is for: the id (or TKT-123) from the
|
|
34
34
|
[TICKET:TKT-123 <id>] line of the message you are working on
|
|
35
|
+
--thread Owner thread the task answers (Slack thread key from your
|
|
36
|
+
prompt). A task you assign to yourself inherits no owner
|
|
37
|
+
thread: name it here, or its result goes nowhere public
|
|
38
|
+
--conversation Owner conversation the task answers (chat-v2 id)
|
|
35
39
|
--json | -j Raw JSON payload (same as legacy)
|
|
36
40
|
--help | -h Show this help
|
|
37
41
|
EOF_USAGE
|
|
@@ -48,6 +52,8 @@ OWNER=""
|
|
|
48
52
|
DESCRIPTION=""
|
|
49
53
|
BRIEF=""
|
|
50
54
|
REQUEST_ID=""
|
|
55
|
+
OWNER_THREAD=""
|
|
56
|
+
OWNER_CONVERSATION=""
|
|
51
57
|
|
|
52
58
|
# Detect legacy JSON argument
|
|
53
59
|
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
@@ -97,6 +103,14 @@ while [[ $# -gt 0 ]]; do
|
|
|
97
103
|
REQUEST_ID="$2"
|
|
98
104
|
shift 2
|
|
99
105
|
;;
|
|
106
|
+
--thread)
|
|
107
|
+
OWNER_THREAD="$2"
|
|
108
|
+
shift 2
|
|
109
|
+
;;
|
|
110
|
+
--conversation)
|
|
111
|
+
OWNER_CONVERSATION="$2"
|
|
112
|
+
shift 2
|
|
113
|
+
;;
|
|
100
114
|
--json|-j)
|
|
101
115
|
INPUT_JSON="$2"
|
|
102
116
|
shift 2
|
|
@@ -141,6 +155,8 @@ if [ -n "$INPUT_JSON" ]; then
|
|
|
141
155
|
[ -z "$DESCRIPTION" ] && DESCRIPTION=$(printf '%s' "$INPUT" | jq -r '.description // empty')
|
|
142
156
|
[ -z "$BRIEF" ] && BRIEF=$(printf '%s' "$INPUT" | jq -r '.briefMarkdown // .brief // empty')
|
|
143
157
|
[ -z "$REQUEST_ID" ] && REQUEST_ID=$(printf '%s' "$INPUT" | jq -r '.requestId // empty')
|
|
158
|
+
[ -z "$OWNER_THREAD" ] && OWNER_THREAD=$(printf '%s' "$INPUT" | jq -r '.thread // .ownerThread // empty')
|
|
159
|
+
[ -z "$OWNER_CONVERSATION" ] && OWNER_CONVERSATION=$(printf '%s' "$INPUT" | jq -r '.conversation // .conversationId // .ownerConversation // empty')
|
|
144
160
|
fi
|
|
145
161
|
|
|
146
162
|
# Apply defaults
|
|
@@ -212,12 +228,16 @@ WORK_ITEM=$(jq -n \
|
|
|
212
228
|
--arg description "$DESCRIPTION" \
|
|
213
229
|
--arg briefMarkdown "$BRIEF" \
|
|
214
230
|
--arg requestId "$REQUEST_ID" \
|
|
231
|
+
--arg ownerThread "$OWNER_THREAD" \
|
|
232
|
+
--arg ownerConversation "$OWNER_CONVERSATION" \
|
|
215
233
|
'{
|
|
216
234
|
title: $title,
|
|
217
235
|
type: "delegate",
|
|
218
236
|
owner: $owner,
|
|
219
237
|
target: (if $target == "" then null else $target end),
|
|
220
|
-
metadata: { projectPath: $projectPath, milestone: $milestone, priority: $priority }
|
|
238
|
+
metadata: ({ projectPath: $projectPath, milestone: $milestone, priority: $priority }
|
|
239
|
+
+ (if $ownerThread != "" then {ownerThread: $ownerThread} else {} end)
|
|
240
|
+
+ (if $ownerConversation != "" then {ownerConversation: $ownerConversation} else {} end))
|
|
221
241
|
}
|
|
222
242
|
+ (if $description != "" then {description: $description} else {} end)
|
|
223
243
|
+ (if $briefMarkdown != "" then {briefMarkdown: $briefMarkdown} else {} end)
|
|
@@ -228,4 +248,10 @@ if [ -n "$OUTPUT_SCHEMA" ] && [ "$OUTPUT_SCHEMA" != "" ]; then
|
|
|
228
248
|
WORK_ITEM=$(printf '%s' "$WORK_ITEM" | jq --argjson schema "$OUTPUT_SCHEMA" '.metadata += {outputSchema: $schema}')
|
|
229
249
|
fi
|
|
230
250
|
|
|
251
|
+
# A task with no owner place answers no owner thread: say so (message-routing
|
|
252
|
+
# audit 2026-10-18: a self-assigned task no longer borrows your last one).
|
|
253
|
+
if [ -z "$OWNER_THREAD" ] && [ -z "$OWNER_CONVERSATION" ] && [ -z "$REQUEST_ID" ]; then
|
|
254
|
+
echo "Note: no --thread / --conversation given — this task answers no owner thread unless it is delegated from your current work. Pass --thread <key> to tie it to the owner's request." >&2
|
|
255
|
+
fi
|
|
256
|
+
|
|
231
257
|
api_call POST "/task-pool/add" "$WORK_ITEM"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for create-task carrying the owner place it is given (--thread / --conversation)
|
|
3
|
+
# and saying so when it is given none (message-routing audit 2026-10-18) — run with: bash execute.test.sh
|
|
4
|
+
set -u
|
|
5
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
EXEC="$HERE/execute.sh"
|
|
7
|
+
PASS=0; FAIL=0
|
|
8
|
+
check() {
|
|
9
|
+
if [ "$2" = "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1"; echo " want: $3"; echo " got: $2"; fi
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
PORT=18856
|
|
13
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
14
|
+
STUB_BODIES="$(mktemp)"; export STUB_BODIES
|
|
15
|
+
STUB_RUNNING="$(mktemp)"; export STUB_RUNNING
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
import json, os
|
|
19
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
20
|
+
class H(BaseHTTPRequestHandler):
|
|
21
|
+
def reply(self, code, obj):
|
|
22
|
+
self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
23
|
+
self.wfile.write(json.dumps(obj).encode())
|
|
24
|
+
def note(self, body=''):
|
|
25
|
+
open(LOG, 'a').write(self.command + ' ' + self.path + '\n')
|
|
26
|
+
if body:
|
|
27
|
+
try: body = json.dumps(json.loads(body), separators=(',', ':'))
|
|
28
|
+
except Exception: body = body.replace('\n', ' ')
|
|
29
|
+
open(os.environ['STUB_BODIES'], 'a').write(self.path + ' ' + body + '\n')
|
|
30
|
+
def do_GET(self):
|
|
31
|
+
self.note()
|
|
32
|
+
ids = [l.strip() for l in open(os.environ['STUB_RUNNING']) if l.strip()]
|
|
33
|
+
return self.reply(200, {'workItems': [{'id': i} for i in ids]})
|
|
34
|
+
def do_POST(self):
|
|
35
|
+
n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode()
|
|
36
|
+
self.note(body)
|
|
37
|
+
return self.reply(200, {'success': True})
|
|
38
|
+
def log_message(self, *a, **k): pass
|
|
39
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
40
|
+
PY
|
|
41
|
+
STUB_PID=$!
|
|
42
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
43
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG" "$STUB_BODIES" "$STUB_RUNNING"' EXIT
|
|
44
|
+
for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
|
|
45
|
+
|
|
46
|
+
run() { CREWLY_SESSION_NAME=dev-1 CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$1" 2>&1; echo "exit=$?"; }
|
|
47
|
+
reset() { : > "$STUB_LOG"; : > "$STUB_BODIES"; }
|
|
48
|
+
|
|
49
|
+
: > "$STUB_RUNNING"
|
|
50
|
+
|
|
51
|
+
reset
|
|
52
|
+
OUT=$(run '{"projectPath":"/tmp/p","task":"Write the report","sessionName":"dev-1","thread":"C0A:1.2"}')
|
|
53
|
+
check "--thread: stored as metadata.ownerThread" "$(grep '/task-pool/add' "$STUB_BODIES" | grep -c '"ownerThread":"C0A:1.2"')" "1"
|
|
54
|
+
check "--thread: no note" "$(printf '%s' "$OUT" | grep -c 'answers no owner thread')" "0"
|
|
55
|
+
|
|
56
|
+
reset
|
|
57
|
+
CREWLY_SESSION_NAME=dev-1 CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" --project-path /tmp/p --task "Write it" --session dev-1 --conversation room-1 >/dev/null 2>&1
|
|
58
|
+
check "--conversation flag: stored as metadata.ownerConversation" "$(grep '/task-pool/add' "$STUB_BODIES" | grep -c '"ownerConversation":"room-1"')" "1"
|
|
59
|
+
|
|
60
|
+
reset
|
|
61
|
+
OUT=$(run '{"projectPath":"/tmp/p","task":"Write the report","sessionName":"dev-1"}')
|
|
62
|
+
check "no owner place: still created" "$(grep -c 'POST /api/task-pool/add' "$STUB_LOG")" "1"
|
|
63
|
+
check "no owner place: says it answers no owner thread" "$(printf '%s' "$OUT" | grep -c 'answers no owner thread')" "1"
|
|
64
|
+
check "no owner place: no owner fields sent" "$(grep '/task-pool/add' "$STUB_BODIES" | grep -c 'ownerThread\|ownerConversation')" "0"
|
|
65
|
+
|
|
66
|
+
echo "create-task: $PASS passed, $FAIL failed"
|
|
67
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -54,10 +54,17 @@ CREWLY_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"
|
|
|
54
54
|
TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
55
55
|
|
|
56
56
|
# --- Step 0: Resolve target WorkItem if not supplied ---
|
|
57
|
-
# Resolve order: explicit workItemId > the agent's
|
|
57
|
+
# Resolve order: explicit workItemId > the agent's ONE running claim. With
|
|
58
|
+
# several running, the first one was handed over (and its owner thread with
|
|
59
|
+
# it) — this skill no longer guesses (message-routing audit 2026-10-18).
|
|
58
60
|
if [ -z "$WORK_ITEM_ID" ]; then
|
|
59
61
|
POOL_RESP=$(api_call_full GET "/task-pool/items?status=running&target=${SESSION_NAME}" 2>/dev/null || echo '{}')
|
|
60
|
-
|
|
62
|
+
RUNNING_IDS=$(echo "$POOL_RESP" | jq -r '(.workItems // .data // [])[]?.id // empty' 2>/dev/null || true)
|
|
63
|
+
RUNNING_COUNT=$(printf '%s\n' "$RUNNING_IDS" | grep -c . || true)
|
|
64
|
+
if [ "${RUNNING_COUNT:-0}" -gt 1 ]; then
|
|
65
|
+
error_exit "Nothing was handed off: ${RUNNING_COUNT} WorkItems are running for '${SESSION_NAME}' ($(printf '%s' "$RUNNING_IDS" | tr '\n' ' ' | sed 's/ *$//')) and this skill does not guess which one you mean. Pass 'workItemId' with one of them and call again."
|
|
66
|
+
fi
|
|
67
|
+
WORK_ITEM_ID=$(printf '%s\n' "$RUNNING_IDS" | head -n1)
|
|
61
68
|
fi
|
|
62
69
|
|
|
63
70
|
# --- Step 1: Fission guard check (rate limit before handoff) ---
|
|
@@ -101,7 +108,7 @@ HANDOFF_MSG_FILE=$(mktemp)
|
|
|
101
108
|
trap 'rm -f "$HANDOFF_MSG_FILE"' EXIT
|
|
102
109
|
|
|
103
110
|
cat > "$HANDOFF_MSG_FILE" << MSG_EOF
|
|
104
|
-
[TASK HANDOFF] from ${SESSION_NAME}
|
|
111
|
+
[TASK HANDOFF] from ${SESSION_NAME}${WORK_ITEM_ID:+ — WorkItem: ${WORK_ITEM_ID} {route: work-item ${WORK_ITEM_ID}}}
|
|
105
112
|
|
|
106
113
|
## Reason for Handoff
|
|
107
114
|
${REASON}
|
|
@@ -157,8 +164,8 @@ deliver_result=$(api_call POST "/terminal/${TO}/write" "$DELIVER_BODY" 2>/dev/nu
|
|
|
157
164
|
|
|
158
165
|
# --- Step 4: Notify orchestrator/TL about the handoff ---
|
|
159
166
|
NOTIFY_MSG="[HANDOFF] ${SESSION_NAME} → ${TO}: ${REASON}"
|
|
160
|
-
NOTIFY_BODY=$(jq -n --arg content "$NOTIFY_MSG" --arg senderName "$SESSION_NAME" \
|
|
161
|
-
'{content: $content, senderName: $senderName, senderType: "agent"}')
|
|
167
|
+
NOTIFY_BODY=$(jq -n --arg content "$NOTIFY_MSG" --arg senderName "$SESSION_NAME" --arg workItemId "${WORK_ITEM_ID:-}" \
|
|
168
|
+
'{content: $content, senderName: $senderName, senderType: "agent"} + (if $workItemId == "" then {} else {workItemId: $workItemId} end)')
|
|
162
169
|
api_call POST "/chat/agent-response" "$NOTIFY_BODY" >/dev/null 2>&1 || true
|
|
163
170
|
|
|
164
171
|
# --- Step 5: Output result as safe JSON ---
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for handoff-task naming its WorkItem and never guessing among several
|
|
3
|
+
# running ones (message-routing audit 2026-10-18) — run with: bash explicit-id.test.sh
|
|
4
|
+
set -u
|
|
5
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
EXEC="$HERE/execute.sh"
|
|
7
|
+
PASS=0; FAIL=0
|
|
8
|
+
check() {
|
|
9
|
+
if [ "$2" = "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1"; echo " want: $3"; echo " got: $2"; fi
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
PORT=18855
|
|
13
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
14
|
+
STUB_BODIES="$(mktemp)"; export STUB_BODIES
|
|
15
|
+
STUB_RUNNING="$(mktemp)"; export STUB_RUNNING
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
import json, os
|
|
19
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
20
|
+
class H(BaseHTTPRequestHandler):
|
|
21
|
+
def reply(self, code, obj):
|
|
22
|
+
self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
23
|
+
self.wfile.write(json.dumps(obj).encode())
|
|
24
|
+
def note(self, body=''):
|
|
25
|
+
open(LOG, 'a').write(self.command + ' ' + self.path + '\n')
|
|
26
|
+
if body:
|
|
27
|
+
try: body = json.dumps(json.loads(body), separators=(',', ':'))
|
|
28
|
+
except Exception: body = body.replace('\n', ' ')
|
|
29
|
+
open(os.environ['STUB_BODIES'], 'a').write(self.path + ' ' + body + '\n')
|
|
30
|
+
def do_GET(self):
|
|
31
|
+
self.note()
|
|
32
|
+
ids = [l.strip() for l in open(os.environ['STUB_RUNNING']) if l.strip()]
|
|
33
|
+
return self.reply(200, {'workItems': [{'id': i} for i in ids]})
|
|
34
|
+
def do_POST(self):
|
|
35
|
+
n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode()
|
|
36
|
+
self.note(body)
|
|
37
|
+
return self.reply(200, {'success': True})
|
|
38
|
+
def log_message(self, *a, **k): pass
|
|
39
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
40
|
+
PY
|
|
41
|
+
STUB_PID=$!
|
|
42
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
43
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG" "$STUB_BODIES" "$STUB_RUNNING"' EXIT
|
|
44
|
+
for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
|
|
45
|
+
|
|
46
|
+
run() { CREWLY_SESSION_NAME=dev-1 CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$1" 2>&1; echo "exit=$?"; }
|
|
47
|
+
reset() { : > "$STUB_LOG"; : > "$STUB_BODIES"; }
|
|
48
|
+
|
|
49
|
+
# Several running items and no id: refused, nothing handed over.
|
|
50
|
+
printf 'wi-a\nwi-b\n' > "$STUB_RUNNING"; reset
|
|
51
|
+
OUT=$(run '{"sessionName":"dev-1","to":"dev-2","reason":"swap"}')
|
|
52
|
+
check "two running, no id: refused" "$(printf '%s' "$OUT" | grep -c 'exit=1')" "1"
|
|
53
|
+
check "two running, no id: lists both ids" "$(printf '%s' "$OUT" | grep -c 'wi-a wi-b')" "1"
|
|
54
|
+
check "two running, no id: nothing delivered" "$(grep -c '/terminal/' "$STUB_LOG")" "0"
|
|
55
|
+
|
|
56
|
+
# An explicit id: the hand-over text carries it and its route; the notice names it.
|
|
57
|
+
reset
|
|
58
|
+
run '{"sessionName":"dev-1","to":"dev-2","reason":"swap","workItemId":"wi-b"}' >/dev/null
|
|
59
|
+
check "explicit id: hand-over names the WorkItem and its route" \
|
|
60
|
+
"$(grep '/terminal/dev-2' "$STUB_BODIES" | grep -c 'WorkItem: wi-b {route: work-item wi-b}')" "1"
|
|
61
|
+
check "explicit id: [HANDOFF] notice carries the workItemId" \
|
|
62
|
+
"$(grep '/chat/agent-response' "$STUB_BODIES" | grep -c '"workItemId":"wi-b"')" "1"
|
|
63
|
+
|
|
64
|
+
# One running item, no id: that one is used.
|
|
65
|
+
printf 'wi-only\n' > "$STUB_RUNNING"; reset
|
|
66
|
+
run '{"sessionName":"dev-1","to":"dev-2","reason":"swap"}' >/dev/null
|
|
67
|
+
check "one running, no id: hands over that one" "$(grep -c 'POST /api/task-pool/items/wi-only/handoff' "$STUB_LOG")" "1"
|
|
68
|
+
|
|
69
|
+
echo "handoff-task: $PASS passed, $FAIL failed"
|
|
70
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -51,6 +51,7 @@ work came from:
|
|
|
51
51
|
| Your current work | Where the answer goes |
|
|
52
52
|
|---|---|
|
|
53
53
|
| a message from the owner | that conversation / Slack thread (DM, channel thread, Crewly chat / portal) |
|
|
54
|
+
| a message from another agent (`[MESSAGE from <name> … send-message --to <session>]`) | back to that agent, agent-to-agent — never the owner's thread |
|
|
54
55
|
| a project ticket | the ticket's thread in your team channel (started on the first post) |
|
|
55
56
|
| a scheduled / triggered task | the trigger's destination, else a NEW top-level post in your team channel |
|
|
56
57
|
| nothing in particular | a new top-level post in your team channel |
|
|
@@ -79,8 +80,15 @@ bash config/skills/agent/core/reply/execute.sh --decision D-12 "Going with optio
|
|
|
79
80
|
Order the harness uses: the message you name → the ticket's thread (request
|
|
80
81
|
ticket `TKT-…` or project ticket like `CE-7`) → the work item's origin → ids
|
|
81
82
|
you passed (only if they name a conversation you are in, and a thread key's
|
|
82
|
-
channel matches) →
|
|
83
|
-
|
|
83
|
+
channel matches) → the agent whose message is the latest thing you got →
|
|
84
|
+
what the harness last prompted you about → where your current turn came
|
|
85
|
+
from → your DM with the owner.
|
|
86
|
+
|
|
87
|
+
When a bare reply could mean two places — another agent's message came in
|
|
88
|
+
but you also owe the owner an answer, or a dispatch arrived after the
|
|
89
|
+
owner's message — nothing is sent: you get `success: false` and both
|
|
90
|
+
commands (`send-message --to <agent> …` or `reply --conversation <id> …`).
|
|
91
|
+
Run the one you mean.
|
|
84
92
|
|
|
85
93
|
To start a new topic yourself:
|
|
86
94
|
|
|
@@ -56,6 +56,11 @@ a `[SLACK-THREAD:<key>]` tag in your prompt — the reply lands in exactly that
|
|
|
56
56
|
thread. Answer each thread in its own thread; never bundle answers for
|
|
57
57
|
different threads into one message.
|
|
58
58
|
|
|
59
|
+
In a Slack team channel a post with no `--thread` is refused
|
|
60
|
+
(`409 thread_required`): it is never put at the channel's top level by
|
|
61
|
+
itself. Pass the thread from your prompt; use `send-message` to reach a
|
|
62
|
+
colleague; pass `--new-thread` only for a deliberate new top-level post.
|
|
63
|
+
|
|
59
64
|
To post in a **Crewly channel** you are a member of (a room shared with
|
|
60
65
|
agents of other teams — `list-channels` shows yours), pass its name with a
|
|
61
66
|
`#`, or its id:
|
|
@@ -155,6 +160,18 @@ On failure prints `{"success":false, "error":"..."}` to stdout and exits
|
|
|
155
160
|
with a non-zero code.
|
|
156
161
|
|
|
157
162
|
|
|
163
|
+
## Messages from other agents do not go in the owner's thread
|
|
164
|
+
|
|
165
|
+
A message from another agent arrives as `[MESSAGE from <name> (<team>) — a
|
|
166
|
+
colleague, not the owner. Answer with send-message --to <session>; …]`.
|
|
167
|
+
Answer it with `send-message --to <session>`. Agents never talk to each
|
|
168
|
+
other in the owner's thread: a post into it that only @-mentions agents is
|
|
169
|
+
not posted. It is delivered privately to the agents it @-mentions (the
|
|
170
|
+
skill prints `"posted":false,"deliveredPrivately":true,"to":[…]`), or, when
|
|
171
|
+
that is not possible, refused (`409 agent_mention_in_owner_thread`) with the
|
|
172
|
+
send-message command to run instead. Post in the owner's thread only your
|
|
173
|
+
final answer to the owner, without the @-mention.
|
|
174
|
+
|
|
158
175
|
## What not to put in a channel
|
|
159
176
|
|
|
160
177
|
A team channel is public — everyone in the workspace can read it. When your
|
|
@@ -40,6 +40,9 @@ Options:
|
|
|
40
40
|
"<you> is working on it…" in the Slack thread, which your
|
|
41
41
|
real reply then replaces. Use it when a message was only
|
|
42
42
|
passed to you to judge, and you have decided to answer.
|
|
43
|
+
--new-thread A deliberate new top-level post in a Slack team room. Without
|
|
44
|
+
--thread or --new-thread such a post is refused there
|
|
45
|
+
(409 thread_required), never put at the top level silently.
|
|
43
46
|
--adds-new Post even though the agent chosen to answer the owner
|
|
44
47
|
already replied in this thread -- when you have something
|
|
45
48
|
new, or when you were asked (without it such a post is
|
|
@@ -66,6 +69,7 @@ WORKING=""
|
|
|
66
69
|
HANDOFF=""
|
|
67
70
|
MESSAGE_REF=""
|
|
68
71
|
ADDS_NEW=""
|
|
72
|
+
NEW_THREAD=""
|
|
69
73
|
FILES_JSON="[]"
|
|
70
74
|
|
|
71
75
|
# Detect legacy JSON argument as $1
|
|
@@ -92,6 +96,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
92
96
|
ADDS_NEW="1"
|
|
93
97
|
shift
|
|
94
98
|
;;
|
|
99
|
+
--new-thread)
|
|
100
|
+
NEW_THREAD="1"
|
|
101
|
+
shift
|
|
102
|
+
;;
|
|
95
103
|
--handoff)
|
|
96
104
|
HANDOFF="$2"
|
|
97
105
|
shift 2
|
|
@@ -252,7 +260,7 @@ fi
|
|
|
252
260
|
# Build JSON body. Export CONTENT/CMID so python can read them safely without
|
|
253
261
|
# quoting the values through the shell (avoids escaping hell for multi-line
|
|
254
262
|
# content with quotes, backslashes, backticks, etc.).
|
|
255
|
-
BODY=$(CONTENT="$CONTENT" CMID="$CMID" THREAD_ID="$THREAD_ID" INTERIM="${INTERIM:-}" ADDS_NEW="$ADDS_NEW" FILES_JSON="$FILES_JSON" PROJECT_PATH="${CREWLY_PROJECT_PATH:-$PWD}" python3 -c '
|
|
263
|
+
BODY=$(CONTENT="$CONTENT" CMID="$CMID" THREAD_ID="$THREAD_ID" INTERIM="${INTERIM:-}" ADDS_NEW="$ADDS_NEW" NEW_THREAD="$NEW_THREAD" FILES_JSON="$FILES_JSON" PROJECT_PATH="${CREWLY_PROJECT_PATH:-$PWD}" python3 -c '
|
|
256
264
|
import os, json
|
|
257
265
|
p = {"content": os.environ["CONTENT"], "contentType": "markdown"}
|
|
258
266
|
files = json.loads(os.environ.get("FILES_JSON", "[]"))
|
|
@@ -264,6 +272,8 @@ if os.environ.get("INTERIM"):
|
|
|
264
272
|
p["interim"] = True
|
|
265
273
|
if os.environ.get("ADDS_NEW"):
|
|
266
274
|
p["addsNew"] = True
|
|
275
|
+
if os.environ.get("NEW_THREAD"):
|
|
276
|
+
p["newThread"] = True
|
|
267
277
|
cmid = os.environ.get("CMID", "")
|
|
268
278
|
if cmid:
|
|
269
279
|
p["clientMessageId"] = cmid
|
|
@@ -285,7 +295,12 @@ try:
|
|
|
285
295
|
except Exception:
|
|
286
296
|
print(json.dumps({"success": True, "channelId": os.environ["CHANNEL_ID"], "raw": raw}, separators=(",", ":")))
|
|
287
297
|
raise SystemExit(0)
|
|
288
|
-
|
|
298
|
+
data = d.get("data") or {}
|
|
299
|
+
if data.get("deliveredPrivately"):
|
|
300
|
+
# R6: an agents-only post never lands in the owner thread; it went to the agents.
|
|
301
|
+
print(json.dumps({"success": True, "posted": False, "deliveredPrivately": True, "to": data.get("to", []), "note": data.get("note", "")}, separators=(",", ":"), ensure_ascii=False))
|
|
302
|
+
raise SystemExit(0)
|
|
303
|
+
mid = data.get("id", "")
|
|
289
304
|
out = {"success": True}
|
|
290
305
|
if mid:
|
|
291
306
|
out["messageId"] = mid
|
|
@@ -42,10 +42,16 @@ class H(BaseHTTPRequestHandler):
|
|
|
42
42
|
}
|
|
43
43
|
with open(LOG_PATH, 'w') as f:
|
|
44
44
|
f.write(json.dumps(log))
|
|
45
|
-
|
|
45
|
+
if '@Atlas' in body:
|
|
46
|
+
# R6: an agents-only post in an owner thread goes to the agent privately.
|
|
47
|
+
self.send_response(200)
|
|
48
|
+
reply = {'success': True, 'data': {'deliveredPrivately': True, 'to': ['tt-atlas-1'], 'note': 'Not posted in the owner thread'}}
|
|
49
|
+
else:
|
|
50
|
+
self.send_response(201)
|
|
51
|
+
reply = {'success': True, 'data': {'id': 'm-stub-123', 'channelId': 'chan-xyz'}}
|
|
46
52
|
self.send_header('Content-Type', 'application/json')
|
|
47
53
|
self.end_headers()
|
|
48
|
-
self.wfile.write(json.dumps(
|
|
54
|
+
self.wfile.write(json.dumps(reply).encode('utf-8'))
|
|
49
55
|
def do_GET(self):
|
|
50
56
|
# Channel lookup by name (reply-channel --channel '#name').
|
|
51
57
|
if self.path.startswith('/api/channels/%23tech-brief'):
|
|
@@ -198,6 +204,13 @@ else
|
|
|
198
204
|
PASS=$((PASS + 1)); echo " ✓ a plain reply carries no files list"
|
|
199
205
|
fi
|
|
200
206
|
|
|
207
|
+
echo "test: an agents-only post delivered privately says it was not posted (R6)"
|
|
208
|
+
: > "$STUB_LOG"
|
|
209
|
+
OUT=$(run_skill --channel chan-xyz --content "@Atlas the numbers" 2>&1 || true)
|
|
210
|
+
assert_contains "reports posted:false" "$OUT" '"posted":false'
|
|
211
|
+
assert_contains "reports deliveredPrivately" "$OUT" '"deliveredPrivately":true'
|
|
212
|
+
assert_contains "names the agent it went to" "$OUT" 'tt-atlas-1'
|
|
213
|
+
|
|
201
214
|
echo
|
|
202
215
|
echo "========================================"
|
|
203
216
|
echo "reply-channel skill: PASS=$PASS FAIL=$FAIL"
|
|
@@ -87,6 +87,20 @@ recipient has hit its daily token cap: the message waits and is delivered
|
|
|
87
87
|
automatically when the cap resets at midnight or the owner boosts it. Do not
|
|
88
88
|
resend a queued message.
|
|
89
89
|
|
|
90
|
+
## What the recipient sees
|
|
91
|
+
|
|
92
|
+
Your message arrives with one header line naming you and how to answer:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
[MESSAGE from Atlas (Think Tank) — a colleague, not the owner. Answer with send-message --to think-tank-atlas-b4e166f6; do not post it in an owner thread.]
|
|
96
|
+
Thanks. Two things. …
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
When you receive such a message, answer it with `send-message --to <that
|
|
100
|
+
session>` (a bare `reply` also goes back to that agent). Never answer it in
|
|
101
|
+
the owner's thread with `reply-channel "@Name …"`: the owner would read a
|
|
102
|
+
conversation that is not for them, and the harness refuses that post.
|
|
103
|
+
|
|
90
104
|
## Paused teams
|
|
91
105
|
|
|
92
106
|
The owner can pause a team temporarily (specs/2026-10-04-team-pause.md). A
|
|
@@ -59,7 +59,7 @@ reply in the same Slack thread.
|
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
bash config/skills/agent/core/slack-post/execute.sh \
|
|
62
|
-
--target "#general" \
|
|
62
|
+
--target "#general" --new-topic \
|
|
63
63
|
--text "Deploy finished, staging is on 1.4.2."
|
|
64
64
|
```
|
|
65
65
|
|
|
@@ -71,7 +71,7 @@ bash config/skills/agent/core/slack-post/execute.sh --target "@steve" --text "..
|
|
|
71
71
|
bash config/skills/agent/core/slack-post/execute.sh --target C0123ABCD --thread 1712345678.000100 --text "..."
|
|
72
72
|
|
|
73
73
|
# Long text from a file, or piped
|
|
74
|
-
bash .../execute.sh --target "#general" --text-file /tmp/report.md
|
|
74
|
+
bash .../execute.sh --target "#general" --new-topic --text-file /tmp/report.md
|
|
75
75
|
echo "..." | bash .../execute.sh --target "@steve"
|
|
76
76
|
```
|
|
77
77
|
|
|
@@ -111,7 +111,7 @@ two are told apart. Write ids exactly as Slack shows them.
|
|
|
111
111
|
| `--text` | `-m` | Message text (required unless piped or `--text-file`) |
|
|
112
112
|
| `--text-file` | | Read the text from a file |
|
|
113
113
|
| `--thread` | `-t` | Slack thread timestamp to reply inside, or the `[SLACK-THREAD:<key>]` key from your prompt |
|
|
114
|
-
| `--new-topic` | | A new top-level message
|
|
114
|
+
| `--new-topic` | | A new top-level message (scheduled output, a briefing, a new subject). **A channel post needs `--thread` or `--new-topic`**: with neither it is refused (409 `thread_required`), never placed by guess. A post with no thread where you still owe an answer is refused too, and the error names that thread — pass it as `--thread` |
|
|
115
115
|
| `--file` | `-f` | Upload a file into the channel/thread right after the text (repeatable, up to 5; relative paths resolve against your project). Sent by your own bot, or the workspace bot with your name when you have none. Output has `filesNotSent` with Slack's error code if one did not go |
|
|
116
116
|
| `--json` | `-j` | Raw JSON payload |
|
|
117
117
|
|