crewly 1.20.365 → 1.20.367
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/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 +3 -0
- package/config/skills/agent/core/reply-channel/SKILL.md +12 -5
- 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/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 +32 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +33 -1
- 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 +15 -0
- 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 +33 -3
- 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 +111 -11
- 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 +52 -4
- 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/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 +8 -0
- package/dist/backend/backend/src/services/messaging/agent-message-envelope.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/agent-message-envelope.js +10 -1
- package/dist/backend/backend/src/services/messaging/agent-message-envelope.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-hook-message.d.ts +62 -20
- 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 +178 -39
- package/dist/backend/backend/src/services/messaging/owner-hook-message.js.map +1 -1
- package/dist/backend/backend/src/services/orc/agent-mention-post-guard.d.ts +8 -5
- package/dist/backend/backend/src/services/orc/agent-mention-post-guard.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/agent-mention-post-guard.js +13 -10
- package/dist/backend/backend/src/services/orc/agent-mention-post-guard.js.map +1 -1
- 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/reply-destination-resolver.d.ts +49 -6
- 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 +284 -69
- 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 +64 -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 +215 -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 +10 -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 +25 -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/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.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 +74 -9
- 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 +32 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +33 -1
- 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 +8 -0
- package/dist/cli/backend/src/services/messaging/agent-message-envelope.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/agent-message-envelope.js +10 -1
- package/dist/cli/backend/src/services/messaging/agent-message-envelope.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/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/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 +74 -9
- package/dist/cli/backend/src/services/trace/turn-trigger.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 ]
|
|
@@ -44,6 +44,9 @@ bash config/skills/agent/core/reply/execute.sh "EFT 表改好了,附件是新
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
**Answer where you were asked; a new topic goes in a new thread.**
|
|
47
|
+
A new post the owner made in a channel is already its own thread: answer
|
|
48
|
+
under it (a plain `reply`), never with `--new-thread`; if you do pass
|
|
49
|
+
`--new-thread` then, the harness still puts the answer under the owner's post.
|
|
47
50
|
|
|
48
51
|
The harness knows what you are working on and sends your answer where that
|
|
49
52
|
work came from:
|
|
@@ -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:
|
|
@@ -159,11 +164,13 @@ with a non-zero code.
|
|
|
159
164
|
|
|
160
165
|
A message from another agent arrives as `[MESSAGE from <name> (<team>) — a
|
|
161
166
|
colleague, not the owner. Answer with send-message --to <session>; …]`.
|
|
162
|
-
Answer it with `send-message --to <session>`.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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.
|
|
167
174
|
|
|
168
175
|
## What not to put in a channel
|
|
169
176
|
|
|
@@ -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"
|
|
@@ -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
|
|
|
@@ -48,7 +48,7 @@ The script auto-resolves `config/skills/...` references to absolute paths so del
|
|
|
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
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 |
|
|
51
|
-
| `--thread` | `thread` | No | The owner's Slack thread this work was asked / approved in — the key from the `[SLACK-THREAD:<key>]` tag of the owner's message. Gives the work the owner origin (see Owner work below) |
|
|
51
|
+
| `--thread` | `thread` | No | The owner's Slack thread this work was asked / approved in — the key from the `[SLACK-THREAD:<key>]` tag of the owner's message. A `<conversation id>:<chat thread id>` pair from the chat works too. Gives the work the owner origin (see Owner work below) |
|
|
52
52
|
| `--conversation` | `conversation` | No | The owner's Crewly chat conversation this work was asked / approved in — the id from `[CHAT:<id>]`. Same as `--thread` for chat |
|
|
53
53
|
| `--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 |
|
|
54
54
|
|
|
@@ -185,9 +185,11 @@ if [ -n "$IMAGE_PATH" ]; then
|
|
|
185
185
|
--arg filePath "$IMAGE_PATH" \
|
|
186
186
|
--arg initialComment "${TEXT:-}" \
|
|
187
187
|
--arg threadTs "${THREAD_TS:-}" \
|
|
188
|
+
--argjson allowNewThread "$ALLOW_NEW_THREAD" \
|
|
188
189
|
'{channelId: $channelId, filePath: $filePath} +
|
|
189
190
|
(if $initialComment != "" then {initialComment: $initialComment} else {} end) +
|
|
190
|
-
(if $threadTs != "" then {threadTs: $threadTs} else {} end)
|
|
191
|
+
(if $threadTs != "" then {threadTs: $threadTs} else {} end) +
|
|
192
|
+
(if $allowNewThread then {allowNewThread: true} else {} end)')
|
|
191
193
|
|
|
192
194
|
api_call POST "/slack/upload-image" "$BODY"
|
|
193
195
|
elif [ -n "$FILE_PATH" ]; then
|
|
@@ -197,9 +199,11 @@ elif [ -n "$FILE_PATH" ]; then
|
|
|
197
199
|
--arg filePath "$FILE_PATH" \
|
|
198
200
|
--arg initialComment "${TEXT:-}" \
|
|
199
201
|
--arg threadTs "${THREAD_TS:-}" \
|
|
202
|
+
--argjson allowNewThread "$ALLOW_NEW_THREAD" \
|
|
200
203
|
'{channelId: $channelId, filePath: $filePath} +
|
|
201
204
|
(if $initialComment != "" then {initialComment: $initialComment} else {} end) +
|
|
202
|
-
(if $threadTs != "" then {threadTs: $threadTs} else {} end)
|
|
205
|
+
(if $threadTs != "" then {threadTs: $threadTs} else {} end) +
|
|
206
|
+
(if $allowNewThread then {allowNewThread: true} else {} end)')
|
|
203
207
|
|
|
204
208
|
api_call POST "/slack/upload-file" "$BODY"
|
|
205
209
|
else
|