crewly 1.20.179 → 1.20.180

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/config/roles/team-leader/prompt.md +2 -2
  2. package/config/roles/team-leader/tl-addon.md +2 -2
  3. package/config/skills/agent/core/ask-owner/SKILL.md +79 -0
  4. package/config/skills/agent/core/ask-owner/execute.sh +66 -0
  5. package/config/skills/agent/core/ask-owner/execute.test.sh +62 -0
  6. package/config/skills/agent/core/project-tickets/SKILL.md +21 -9
  7. package/config/skills/agent/core/project-tickets/execute.sh +28 -9
  8. package/config/skills/agent/core/project-tickets/execute.test.sh +12 -4
  9. package/config/skills/agent/core/reply/SKILL.md +22 -5
  10. package/config/skills/agent/core/reply/execute.sh +12 -5
  11. package/config/skills/agent/core/reply/execute.test.sh +8 -0
  12. package/config/skills/agent/core/schedule-followup/SKILL.md +8 -0
  13. package/config/skills/agent/core/schedule-followup/execute.sh +7 -0
  14. package/config/slack-app-manifest.json +5 -1
  15. package/config/slack-master-app-manifest.json +7 -2
  16. package/dist/backend/backend/src/constants.d.ts +76 -5
  17. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  18. package/dist/backend/backend/src/constants.js +83 -5
  19. package/dist/backend/backend/src/constants.js.map +1 -1
  20. package/dist/backend/backend/src/controllers/chat/agent-reply.controller.d.ts +3 -0
  21. package/dist/backend/backend/src/controllers/chat/agent-reply.controller.d.ts.map +1 -1
  22. package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js +54 -1
  23. package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js.map +1 -1
  24. package/dist/backend/backend/src/controllers/decisions/decisions.controller.d.ts +67 -0
  25. package/dist/backend/backend/src/controllers/decisions/decisions.controller.d.ts.map +1 -0
  26. package/dist/backend/backend/src/controllers/decisions/decisions.controller.js +215 -0
  27. package/dist/backend/backend/src/controllers/decisions/decisions.controller.js.map +1 -0
  28. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts +8 -5
  29. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts.map +1 -1
  30. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js +51 -12
  31. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js.map +1 -1
  32. package/dist/backend/backend/src/index.d.ts +4 -0
  33. package/dist/backend/backend/src/index.d.ts.map +1 -1
  34. package/dist/backend/backend/src/index.js +88 -2
  35. package/dist/backend/backend/src/index.js.map +1 -1
  36. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  37. package/dist/backend/backend/src/routes/api.routes.js +6 -0
  38. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  39. package/dist/backend/backend/src/services/boot/boot-announce.service.d.ts +1 -1
  40. package/dist/backend/backend/src/services/boot/boot-announce.service.js +13 -13
  41. package/dist/backend/backend/src/services/boot/boot-announce.service.js.map +1 -1
  42. package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.service.js +6 -6
  43. package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.service.js.map +1 -1
  44. package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.d.ts +3 -3
  45. package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.d.ts.map +1 -1
  46. package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.js +14 -13
  47. package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.js.map +1 -1
  48. package/dist/backend/backend/src/services/decisions/decision-card.d.ts +147 -0
  49. package/dist/backend/backend/src/services/decisions/decision-card.d.ts.map +1 -0
  50. package/dist/backend/backend/src/services/decisions/decision-card.js +311 -0
  51. package/dist/backend/backend/src/services/decisions/decision-card.js.map +1 -0
  52. package/dist/backend/backend/src/services/decisions/decision-contract.d.ts +83 -0
  53. package/dist/backend/backend/src/services/decisions/decision-contract.d.ts.map +1 -0
  54. package/dist/backend/backend/src/services/decisions/decision-contract.js +226 -0
  55. package/dist/backend/backend/src/services/decisions/decision-contract.js.map +1 -0
  56. package/dist/backend/backend/src/services/decisions/decision-routing.d.ts +34 -0
  57. package/dist/backend/backend/src/services/decisions/decision-routing.d.ts.map +1 -0
  58. package/dist/backend/backend/src/services/decisions/decision-routing.js +64 -0
  59. package/dist/backend/backend/src/services/decisions/decision-routing.js.map +1 -0
  60. package/dist/backend/backend/src/services/decisions/decision-store.d.ts +80 -0
  61. package/dist/backend/backend/src/services/decisions/decision-store.d.ts.map +1 -0
  62. package/dist/backend/backend/src/services/decisions/decision-store.js +151 -0
  63. package/dist/backend/backend/src/services/decisions/decision-store.js.map +1 -0
  64. package/dist/backend/backend/src/services/decisions/decision.service.d.ts +281 -0
  65. package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -0
  66. package/dist/backend/backend/src/services/decisions/decision.service.js +639 -0
  67. package/dist/backend/backend/src/services/decisions/decision.service.js.map +1 -0
  68. package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts +70 -0
  69. package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts.map +1 -0
  70. package/dist/backend/backend/src/services/decisions/decision.wiring.js +172 -0
  71. package/dist/backend/backend/src/services/decisions/decision.wiring.js.map +1 -0
  72. package/dist/backend/backend/src/services/decisions/ticket-thread-store.d.ts +97 -0
  73. package/dist/backend/backend/src/services/decisions/ticket-thread-store.d.ts.map +1 -0
  74. package/dist/backend/backend/src/services/decisions/ticket-thread-store.js +137 -0
  75. package/dist/backend/backend/src/services/decisions/ticket-thread-store.js.map +1 -0
  76. package/dist/backend/backend/src/services/orc/work-item-destination.d.ts +165 -0
  77. package/dist/backend/backend/src/services/orc/work-item-destination.d.ts.map +1 -0
  78. package/dist/backend/backend/src/services/orc/work-item-destination.js +239 -0
  79. package/dist/backend/backend/src/services/orc/work-item-destination.js.map +1 -0
  80. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.d.ts +134 -0
  81. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.d.ts.map +1 -0
  82. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.js +217 -0
  83. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.js.map +1 -0
  84. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts +1 -30
  85. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts.map +1 -1
  86. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js +1 -22
  87. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js.map +1 -1
  88. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.d.ts +2 -15
  89. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.d.ts.map +1 -1
  90. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.js +8 -22
  91. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.js.map +1 -1
  92. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts +11 -12
  93. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts.map +1 -1
  94. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js +27 -52
  95. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js.map +1 -1
  96. package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts +6 -0
  97. package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts.map +1 -1
  98. package/dist/backend/backend/src/services/slack/slack-agent-post.service.js +3 -1
  99. package/dist/backend/backend/src/services/slack/slack-agent-post.service.js.map +1 -1
  100. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +12 -12
  101. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  102. package/dist/backend/backend/src/services/slack/slack.service.d.ts +16 -1
  103. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  104. package/dist/backend/backend/src/services/slack/slack.service.js +56 -2
  105. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  106. package/dist/backend/backend/src/services/system/auto-update.service.js +2 -2
  107. package/dist/backend/backend/src/services/system/auto-update.service.js.map +1 -1
  108. package/dist/backend/backend/src/services/system/auto-update.utils.js +2 -2
  109. package/dist/backend/backend/src/services/system/auto-update.utils.js.map +1 -1
  110. package/dist/backend/backend/src/types/decision.types.d.ts +139 -0
  111. package/dist/backend/backend/src/types/decision.types.d.ts.map +1 -0
  112. package/dist/backend/backend/src/types/decision.types.js +13 -0
  113. package/dist/backend/backend/src/types/decision.types.js.map +1 -0
  114. package/dist/backend/backend/src/types/slack.types.d.ts +30 -0
  115. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  116. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  117. package/dist/backend/backend/src/types/v2/trigger.types.d.ts +17 -0
  118. package/dist/backend/backend/src/types/v2/trigger.types.d.ts.map +1 -1
  119. package/dist/backend/backend/src/types/v2/trigger.types.js +17 -0
  120. package/dist/backend/backend/src/types/v2/trigger.types.js.map +1 -1
  121. package/dist/backend/build-info.json +2 -2
  122. package/dist/cli/backend/src/constants.d.ts +76 -5
  123. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  124. package/dist/cli/backend/src/constants.js +83 -5
  125. package/dist/cli/backend/src/constants.js.map +1 -1
  126. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +12 -12
  127. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  128. package/dist/cli/backend/src/services/slack/slack.service.d.ts +16 -1
  129. package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
  130. package/dist/cli/backend/src/services/slack/slack.service.js +56 -2
  131. package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
  132. package/dist/cli/backend/src/services/system/auto-update.utils.js +2 -2
  133. package/dist/cli/backend/src/services/system/auto-update.utils.js.map +1 -1
  134. package/dist/cli/backend/src/types/slack.types.d.ts +30 -0
  135. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  136. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  137. package/dist/cli/backend/src/types/v2/trigger.types.d.ts +17 -0
  138. package/dist/cli/backend/src/types/v2/trigger.types.d.ts.map +1 -1
  139. package/dist/cli/backend/src/types/v2/trigger.types.js +17 -0
  140. package/dist/cli/backend/src/types/v2/trigger.types.js.map +1 -1
  141. package/frontend/dist/assets/index-04d192f8.css +42 -0
  142. package/frontend/dist/assets/{index-69af46c3.js → index-4271e95e.js} +259 -259
  143. package/frontend/dist/index.html +2 -2
  144. package/package.json +1 -1
  145. package/frontend/dist/assets/index-cdc52122.css +0 -42
@@ -111,14 +111,14 @@ Each project keeps its own backlog in `<project>/.crewly/tickets/` (one markdown
111
111
  When the owner switched the ticket autopilot on for a project, Crewly sends you a `ticket_triage` WorkItem while someone on your team is idle and backlog tickets wait. Decide every ticket it lists, then complete the WorkItem (one line per ticket):
112
112
  - **ready + assign** (`assign-ticket`, or `project-tickets update --status ready` for the next idle member) — at most the stated number of in-progress tickets per member;
113
113
  - **split** into smaller ready tickets and cancel the original with a note;
114
- - **needs the owner** — `project-tickets ask-owner --project <P> --id <ID> --question "<one line>"`; Crewly batches these to the owner's phone, you do not message the owner about them;
114
+ - **needs the owner** — `project-tickets ask-owner --project <P> --id <ID> --question "<one line>" --option "<choice>" --option "<choice>" --default "<choice or wait>"` (2–3 options; `--sensitive email|publish|deploy|spend` for the boundaries below). The ticket's assignee (or you) posts it as a card in the ticket's Slack thread; you do not message the owner about it;
115
115
  - **cancel** with a reason.
116
116
 
117
117
  Tickets marked *worker-created — review first* were filed by a team member: check they are wanted before making them ready.
118
118
 
119
119
  **Who does what.** Delegate by role: give each ticket to the member whose role fits the work. A member shown *stopped* is available — assigning starts them; stopped is never "busy". Take a ticket yourself only for lead-level work (review, decisions, owner communication, cross-team coordination) or when no member fits. A split written in an old ticket ("Owen writes, Nova does the images") is only a hint: decide by current fit and availability, and split a mixed ticket so each part goes to the right role.
120
120
 
121
- **The autopilot does not lift the approval boundary.** Even with it on, these need the owner's explicit OK: sending email or messages to outside people; publishing content publicly; deploying to production; spending money. Work such a ticket up to a draft or a PR, then `ask-owner` for the final step. When the owner's answer reaches you, act on it and clear the mark (`ask-owner --clear`).
121
+ **The autopilot does not lift the approval boundary.** Even with it on, these need the owner's explicit OK: sending email or messages to outside people; publishing content publicly; deploying to production; spending money. Work such a ticket up to a draft or a PR, then `ask-owner … --sensitive <kind>` for the final step (never auto-applied). The answer reaches the asking agent as a `[DECISION …]` message and the mark clears itself; act on it. Withdraw a question that is no longer needed with `ask-owner --clear`.
122
122
 
123
123
  ---
124
124
 
@@ -97,14 +97,14 @@ Each project keeps its own backlog in `<project>/.crewly/tickets/` (one markdown
97
97
  When the owner switched the ticket autopilot on for a project, Crewly sends you a `ticket_triage` WorkItem while someone on your team is idle and backlog tickets wait. Decide every ticket it lists, then complete the WorkItem (one line per ticket):
98
98
  - **ready + assign** (`assign-ticket`, or `project-tickets update --status ready` for the next idle member) — at most the stated number of in-progress tickets per member;
99
99
  - **split** into smaller ready tickets and cancel the original with a note;
100
- - **needs the owner** — `project-tickets ask-owner --project <P> --id <ID> --question "<one line>"`; Crewly batches these to the owner's phone, you do not message the owner about them;
100
+ - **needs the owner** — `project-tickets ask-owner --project <P> --id <ID> --question "<one line>" --option "<choice>" --option "<choice>" --default "<choice or wait>"` (2–3 options; `--sensitive email|publish|deploy|spend` for the boundaries below). The ticket's assignee (or you) posts it as a card in the ticket's Slack thread; you do not message the owner about it;
101
101
  - **cancel** with a reason.
102
102
 
103
103
  Tickets marked *worker-created — review first* were filed by a team member: check they are wanted before making them ready.
104
104
 
105
105
  **Who does what.** Delegate by role: give each ticket to the member whose role fits the work. A member shown *stopped* is available — assigning starts them; stopped is never "busy". Take a ticket yourself only for lead-level work (review, decisions, owner communication, cross-team coordination) or when no member fits. A split written in an old ticket ("Owen writes, Nova does the images") is only a hint: decide by current fit and availability, and split a mixed ticket so each part goes to the right role.
106
106
 
107
- **The autopilot does not lift the approval boundary.** Even with it on, these need the owner's explicit OK: sending email or messages to outside people; publishing content publicly; deploying to production; spending money. Work such a ticket up to a draft or a PR, then `ask-owner` for the final step. When the owner's answer reaches you, act on it and clear the mark (`ask-owner --clear`).
107
+ **The autopilot does not lift the approval boundary.** Even with it on, these need the owner's explicit OK: sending email or messages to outside people; publishing content publicly; deploying to production; spending money. Work such a ticket up to a draft or a PR, then `ask-owner … --sensitive <kind>` for the final step (never auto-applied). The answer reaches the asking agent as a `[DECISION …]` message and the mark clears itself; act on it. Withdraw a question that is no longer needed with `ask-owner --clear`.
108
108
 
109
109
  ---
110
110
 
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: Ask Owner
3
+ description: Ask the owner ONE decision with 2–3 options, a default and a deadline. Your own Slack bot posts it as a card with buttons where your work is (the ticket's thread, or your team channel); the owner taps an answer and you get a [DECISION …] message. Never ask the owner open-ended questions in chat.
4
+ version: 1.0.0
5
+ category: communication
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - developer
9
+ - qa
10
+ - tpm
11
+ - designer
12
+ - frontend-developer
13
+ - backend-developer
14
+ - fullstack-dev
15
+ - qa-engineer
16
+ - product-manager
17
+ - architect
18
+ - generalist
19
+ - sales
20
+ - support
21
+ - team-leader
22
+ - orchestrator
23
+ triggers:
24
+ - ask owner
25
+ - owner decision
26
+ - need approval
27
+ - needs your OK
28
+ tags:
29
+ - communication
30
+ - decision
31
+ - slack
32
+ execution:
33
+ type: script
34
+ script:
35
+ file: execute.sh
36
+ interpreter: bash
37
+ timeoutMs: 20000
38
+ ---
39
+
40
+ # Ask Owner
41
+
42
+ Use this when you need the owner to decide something. The owner reads it on a phone, so ask one
43
+ concrete question and offer the real choices.
44
+
45
+ ```bash
46
+ bash execute.sh --question "Send the partner email on Monday?" \
47
+ --option "Send Monday — after the review call" \
48
+ --option "Hold — wait for legal" \
49
+ --default "Hold"
50
+ ```
51
+
52
+ | Flag | Rule |
53
+ |---|---|
54
+ | `--question` | One line (8–280 characters). Name the decision, not "thoughts?" |
55
+ | `--option` | 2–3 times. `"Label"` or `"Label — detail"`. The label is the button (≤ 40 characters). |
56
+ | `--default` | The option you will take if there is no answer by the deadline, or `wait` |
57
+ | `--deadline` | Optional, ISO (`2026-10-02T12:00`). Default: tomorrow 12:00 |
58
+ | `--ticket APP-12 --project P` | Optional. Ask about a ticket; the card goes in the ticket's thread, asked by its assignee |
59
+ | `--sensitive email\|publish\|deploy\|spend` | Required for messages to outside people, public publishing, prod deploys, spending money. These are never auto-applied: the owner is re-asked once, then the question is parked |
60
+
61
+ What happens:
62
+
63
+ - The card is posted by your own Slack bot:
64
+ - a ticket ask goes in the ticket's thread;
65
+ - any other ask goes where your current work is, else as a new thread in your team channel.
66
+ - The owner can answer by tapping a button, reacting (✅ default, ❌ "no", ⏰ tomorrow), replying in
67
+ the thread, or using the dashboard.
68
+ - You receive `[DECISION D-7] The owner chose "…"`. Act on it. Do not ask again.
69
+ - If nobody answers by the deadline, the default is applied and you are told. A `wait` default or a
70
+ sensitive ask never goes ahead without an answer.
71
+ - Do not also message the owner about it. The card is the question.
72
+
73
+ Withdraw a question you no longer need:
74
+
75
+ ```bash
76
+ bash execute.sh --cancel D-7
77
+ ```
78
+
79
+ A rejected ask (vague, missing options, etc.) comes back with an error that says what to fix.
@@ -0,0 +1,66 @@
1
+ #!/bin/bash
2
+ # ask-owner — one structured owner decision (question + 2–3 options + default
3
+ # + deadline), posted as a Slack card by the asking agent's own bot.
4
+ # See specs/2026-10-01-decision-cards.md.
5
+ set -euo pipefail
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
8
+
9
+ print_usage() {
10
+ cat <<'EOF_USAGE'
11
+ Usage:
12
+ bash execute.sh --question "Send the partner email on Monday?" \
13
+ --option "Send Monday — after the review call" --option "Hold — wait for legal" \
14
+ --default "Hold" [--deadline 2026-10-02T12:00] [--ticket APP-12 --project P] \
15
+ [--sensitive email|publish|deploy|spend]
16
+ bash execute.sh --cancel D-7 Withdraw a question you no longer need
17
+ bash execute.sh '{"question":"…","options":["A","B"],"default":"A"}'
18
+ EOF_USAGE
19
+ }
20
+
21
+ QUESTION=""; OPTIONS_JSON="[]"; DEFAULT_OPT=""; DEADLINE=""; SENSITIVE=""; TICKET=""; PROJECT=""; CANCEL=""
22
+
23
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
24
+ J="$1"; shift
25
+ QUESTION=$(printf '%s' "$J" | jq -r '.question // empty')
26
+ OPTIONS_JSON=$(printf '%s' "$J" | jq -c 'if (.options|type) == "array" then .options else [] end')
27
+ DEFAULT_OPT=$(printf '%s' "$J" | jq -r '.default // empty')
28
+ DEADLINE=$(printf '%s' "$J" | jq -r '.deadline // empty')
29
+ SENSITIVE=$(printf '%s' "$J" | jq -r '.sensitive // empty')
30
+ TICKET=$(printf '%s' "$J" | jq -r '.ticket // empty')
31
+ PROJECT=$(printf '%s' "$J" | jq -r '.project // empty')
32
+ CANCEL=$(printf '%s' "$J" | jq -r '.cancel // empty')
33
+ fi
34
+
35
+ while [[ $# -gt 0 ]]; do
36
+ case "$1" in
37
+ --question|-q) [ $# -ge 2 ] || error_exit "--question requires a value"; QUESTION="$2"; shift 2 ;;
38
+ --option|-o) [ $# -ge 2 ] || error_exit "--option requires a value"
39
+ OPTIONS_JSON=$(jq -c --arg o "$2" '. + [$o]' <<<"$OPTIONS_JSON"); shift 2 ;;
40
+ --default|-d) [ $# -ge 2 ] || error_exit "--default requires a value"; DEFAULT_OPT="$2"; shift 2 ;;
41
+ --deadline) [ $# -ge 2 ] || error_exit "--deadline requires a value"; DEADLINE="$2"; shift 2 ;;
42
+ --sensitive) [ $# -ge 2 ] || error_exit "--sensitive requires a value"; SENSITIVE="$2"; shift 2 ;;
43
+ --ticket|--id) [ $# -ge 2 ] || error_exit "--ticket requires a value"; TICKET="$2"; shift 2 ;;
44
+ --project|-p) [ $# -ge 2 ] || error_exit "--project requires a value"; PROJECT="$2"; shift 2 ;;
45
+ --cancel) [ $# -ge 2 ] || error_exit "--cancel requires a decision id"; CANCEL="$2"; shift 2 ;;
46
+ --help|-h) print_usage; exit 0 ;;
47
+ *) error_exit "Unknown option: $1 (see --help)" ;;
48
+ esac
49
+ done
50
+
51
+ if [ -n "$CANCEL" ]; then
52
+ api_call POST "/decisions/$(printf '%s' "$CANCEL" | jq -sRr @uri)/cancel" '{}' | jq '{success, decision: (.data | {id, status})}'
53
+ exit 0
54
+ fi
55
+
56
+ require_param "question" "$QUESTION"
57
+ BODY=$(jq -n --arg q "$QUESTION" --argjson o "$OPTIONS_JSON" --arg d "$DEFAULT_OPT" --arg dl "$DEADLINE" \
58
+ --arg s "$SENSITIVE" --arg t "$TICKET" --arg p "$PROJECT" \
59
+ '{question: $q, options: $o}
60
+ + (if $d != "" then {default: $d} else {} end)
61
+ + (if $dl != "" then {deadline: $dl} else {} end)
62
+ + (if $s != "" then {sensitive: $s} else {} end)
63
+ + (if $t != "" then {ticket: $t} else {} end)
64
+ + (if $p != "" then {project: $p} else {} end)')
65
+ api_call POST "/decisions" "$BODY" \
66
+ | jq '{success, decision: (.data | {id, asker, status, deadline, posted: (.card != null), postError})}'
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # Tests for ask-owner — run with: bash execute.test.sh
3
+ # A python HTTP stub plays the backend; asserts the calls and the output.
4
+ set -u
5
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ EXEC="$HERE/execute.sh"
7
+ PASS=0; FAIL=0
8
+ check() {
9
+ local name="$1"; local got="$2"; local want="$3"
10
+ if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
11
+ }
12
+
13
+ PORT=18853
14
+ STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
15
+ python3 -u <<'PY' >/dev/null 2>&1 &
16
+ from http.server import BaseHTTPRequestHandler, HTTPServer
17
+ import json, os
18
+ LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
19
+ class H(BaseHTTPRequestHandler):
20
+ def reply(self, code, obj):
21
+ self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers()
22
+ self.wfile.write(json.dumps(obj).encode())
23
+ def do_GET(self):
24
+ return self.reply(200, {'success': True})
25
+ def do_POST(self):
26
+ n = int(self.headers.get('content-length', '0')); body = json.loads(self.rfile.read(n).decode() or '{}')
27
+ open(LOG, 'w').write(json.dumps({'method': self.command, 'path': self.path, 'body': body, 'agent': self.headers.get('X-Agent-Session')}))
28
+ if self.path.endswith('/cancel'):
29
+ return self.reply(200, {'success': True, 'data': {'id': 'D-7', 'status': 'cancelled'}})
30
+ if len(body.get('options', [])) < 2:
31
+ return self.reply(400, {'success': False, 'error': 'options are required: give the owner 2–3 concrete choices'})
32
+ return self.reply(201, {'success': True, 'data': {'id': 'D-7', 'asker': 'dev-ann', 'status': 'open', 'deadline': 'x', 'card': {'slackChannelId': 'C1'}}})
33
+ def log_message(self, *a, **k): pass
34
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
35
+ PY
36
+ STUB_PID=$!
37
+ disown "$STUB_PID" 2>/dev/null || true
38
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
39
+ 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
40
+
41
+ run() { CREWLY_SESSION_NAME=dev-ann CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
42
+ run_err() { CREWLY_SESSION_NAME=dev-ann CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
43
+ last() { jq -r "$1" "$STUB_LOG"; }
44
+
45
+ OUT=$(run --question "Send the partner email on Monday?" --option "Send Monday — after review" --option "Hold" --default Hold --ticket APP-12 --project p1 --sensitive email)
46
+ check "ask: path" "$(last .path)" "/api/decisions"
47
+ check "ask: agent header" "$(last .agent)" "dev-ann"
48
+ check "ask: body" "$(last '.body | tostring')" '{"question":"Send the partner email on Monday?","options":["Send Monday — after review","Hold"],"default":"Hold","sensitive":"email","ticket":"APP-12","project":"p1"}'
49
+ check "ask: output" "$(printf '%s' "$OUT" | jq -c '.decision')" '{"id":"D-7","asker":"dev-ann","status":"open","deadline":"x","posted":true,"postError":null}'
50
+
51
+ run '{"question":"Blue or green for the logo?","options":["Blue","Green"],"default":"wait"}' >/dev/null
52
+ check "ask json: body" "$(last '.body | tostring')" '{"question":"Blue or green for the logo?","options":["Blue","Green"],"default":"wait"}'
53
+
54
+ check "ask: server error reaches the agent" "$(run_err --question "Thoughts on this one?" --option "A" | grep -c 'options are required')" "1"
55
+ check "ask: missing question" "$(run_err --option A --option B | grep -c 'question')" "1"
56
+
57
+ OUT=$(run --cancel D-7)
58
+ check "cancel: path" "$(last .path)" "/api/decisions/D-7/cancel"
59
+ check "cancel: output" "$(printf '%s' "$OUT" | jq -c '.decision')" '{"id":"D-7","status":"cancelled"}'
60
+
61
+ echo "ask-owner: $PASS passed, $FAIL failed"
62
+ [ "$FAIL" -eq 0 ]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: Project Tickets
3
- description: A project's own backlog — list, read, create, update, claim, release, assign, link, log and ask-owner on project tickets (markdown files in <project>/.crewly/tickets/, tracked in git), plus the per-project ticket autopilot switch.
3
+ description: A project's own backlog — list, read, create, update, claim, release, assign, link, log and ask-owner (structured decision cards) on project tickets (markdown files in <project>/.crewly/tickets/, tracked in git), plus the per-project ticket autopilot switch.
4
4
  version: 1.0.0
5
5
  category: task-management
6
6
  skillType: claude-skill
@@ -147,18 +147,30 @@ ticket already has another live WorkItem.
147
147
  New delegations need no `link`: `delegate-task` puts work for a teammate on a
148
148
  project through a ticket by itself (pass `--ticket APP-12` to use an existing one).
149
149
 
150
- ## Ask the owner (orchestrator / team lead)
150
+ ## Ask the owner (assignee / team lead / orchestrator)
151
151
 
152
152
  ```bash
153
- bash execute.sh ask-owner --project P --id APP-12 --question "Send the draft to the 3 partners?"
154
- # the owner answered:
155
- bash execute.sh ask-owner --project P --id APP-12 --clear --note "owner: yes, Monday"
153
+ bash execute.sh ask-owner --project P --id APP-12 \
154
+ --question "Send the draft to the 3 partners?" \
155
+ --option "Send Monday — after the review call" --option "Hold — wait for legal" \
156
+ --default "Hold" [--deadline 2026-10-02T12:00] [--sensitive email]
157
+ # no longer needed (withdraws the open card and removes the mark):
158
+ bash execute.sh ask-owner --project P --id APP-12 --clear --note "resolved in the review call"
156
159
  ```
157
160
 
158
- The ticket gets the `needs-owner` label and the question goes into its Log.
159
- With the ticket autopilot on, Crewly batches the open questions into one short
160
- message on the owner's phone — do not message the owner about them yourself.
161
- Keep the question to one line the owner can answer with a tap or a word.
161
+ ONE question with 2–3 options and a default (an option, or `wait`). The deadline defaults to
162
+ tomorrow at 12:00. The ticket's assignee, else the lead, posts the question as a card in the
163
+ ticket's Slack thread, using its own bot. The ticket gets `needs-owner` until the question is
164
+ answered.
165
+
166
+ - **The answer** comes back to that agent as a `[DECISION D-7] The owner chose "…"` message,
167
+ and the ticket log records it.
168
+ - **No answer by the deadline:** the default is applied.
169
+ - **Sensitive asks** (`email` = messages to outside people, `publish`, `deploy` = production,
170
+ `spend`) are never auto-applied. The owner is re-asked once, then the question is parked.
171
+ - Vague asks ("thoughts?") and asks without options are rejected, with an error that says what
172
+ to fix.
173
+ - Do not message the owner about it yourself.
162
174
 
163
175
  ## Ticket autopilot (owner / orchestrator)
164
176
 
@@ -17,7 +17,9 @@
17
17
  # bash execute.sh assign --project P --id APP-12 --to <session> [--no-start] (owner / orc / lead)
18
18
  # bash execute.sh log --project P --id APP-12 --note "progress"
19
19
  # bash execute.sh link --project P --id APP-12 --work-item <WorkItem id> (owner / orc / lead)
20
- # bash execute.sh ask-owner --project P --id APP-12 --question "…" (owner / orc / lead)
20
+ # bash execute.sh ask-owner --project P --id APP-12 --question "…" --option "A" --option "B — detail"
21
+ # --default "B"|wait [--deadline ISO] [--sensitive email|publish|deploy|spend]
22
+ # (owner / orc / lead / assignee)
21
23
  # bash execute.sh ask-owner --project P --id APP-12 --clear [--note "answer"]
22
24
  # bash execute.sh autopilot --project P [--on|--off] [--driver <session>|--driver default]
23
25
  # [--daily-budget <usd>] [--max-in-flight <n>] (owner / orchestrator)
@@ -47,10 +49,12 @@ Usage:
47
49
  bash execute.sh link --project P --id APP-12 --work-item <id>
48
50
  Orchestrator / team lead: tie work already in
49
51
  flight (a live WorkItem) to this ticket
50
- bash execute.sh ask-owner --project P --id APP-12 --question "…"
51
- Orchestrator / team lead: the ticket waits on the
52
- owner (label needs-owner); Crewly batches the
53
- question to the owner's phone
52
+ bash execute.sh ask-owner --project P --id APP-12 --question "…" --option "A" --option "B — detail"
53
+ --default "B"|wait [--deadline ISO] [--sensitive email|publish|deploy|spend]
54
+ Ask the owner ONE question with 2–3 options. The
55
+ ticket's assignee (else its lead) posts it as a card
56
+ in the ticket's Slack thread; the answer comes back
57
+ to that agent as a [DECISION …] message
54
58
  bash execute.sh ask-owner --project P --id APP-12 --clear [--note "answer"]
55
59
  The owner answered: remove the needs-owner mark
56
60
  bash execute.sh autopilot --project P [--on|--off] [--driver <session>|default]
@@ -67,7 +71,7 @@ ACTION=""; PROJECT=""; ID=""; TITLE=""; DESCRIPTION=""; PRIORITY=""; LABELS="";
67
71
  STATUS=""; SOURCE=""; REQUEST_ID=""; NOTE=""; OWNER_REVIEW=""; ASSIGNEE=""; START="true"; WORK_ITEM=""
68
72
  ACCEPTANCE_JSON="null"
69
73
  HAS_DESCRIPTION=0
70
- QUESTION=""; CLEAR=""; AP_ENABLED=""; AP_DRIVER=""; AP_BUDGET=""; AP_MAX=""
74
+ QUESTION=""; CLEAR=""; OPTIONS_JSON="[]"; DEFAULT_OPT=""; DEADLINE=""; SENSITIVE=""; AP_ENABLED=""; AP_DRIVER=""; AP_BUDGET=""; AP_MAX=""
71
75
 
72
76
  if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
73
77
  J="$1"; shift
@@ -92,6 +96,10 @@ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
92
96
  WORK_ITEM=$(printf '%s' "$J" | jq -r '.workItemId // .workItem // empty')
93
97
  QUESTION=$(printf '%s' "$J" | jq -r '.question // empty')
94
98
  CLEAR=$(printf '%s' "$J" | jq -r 'if .clear == true then "true" else empty end')
99
+ OPTIONS_JSON=$(printf '%s' "$J" | jq -c 'if (.options|type) == "array" then .options else [] end')
100
+ DEFAULT_OPT=$(printf '%s' "$J" | jq -r '.default // empty')
101
+ DEADLINE=$(printf '%s' "$J" | jq -r '.deadline // empty')
102
+ SENSITIVE=$(printf '%s' "$J" | jq -r '.sensitive // empty')
95
103
  AP_ENABLED=$(printf '%s' "$J" | jq -r 'if (.enabled|type) == "boolean" then (.enabled|tostring) else empty end')
96
104
  AP_DRIVER=$(printf '%s' "$J" | jq -r '.driver // empty')
97
105
  AP_BUDGET=$(printf '%s' "$J" | jq -r '.dailyBudgetUsd // empty')
@@ -121,6 +129,11 @@ while [[ $# -gt 0 ]]; do
121
129
  [ $# -ge 2 ] || error_exit "--work-item requires a value"; WORK_ITEM="$2"; shift 2 ;;
122
130
  --question|-q) [ $# -ge 2 ] || error_exit "--question requires a value"; QUESTION="$2"; shift 2 ;;
123
131
  --clear) CLEAR="true"; shift ;;
132
+ --option|-o) [ $# -ge 2 ] || error_exit "--option requires a value"
133
+ OPTIONS_JSON=$(jq -c --arg o "$2" '. + [$o]' <<<"$OPTIONS_JSON"); shift 2 ;;
134
+ --default) [ $# -ge 2 ] || error_exit "--default requires a value"; DEFAULT_OPT="$2"; shift 2 ;;
135
+ --deadline) [ $# -ge 2 ] || error_exit "--deadline requires a value"; DEADLINE="$2"; shift 2 ;;
136
+ --sensitive) [ $# -ge 2 ] || error_exit "--sensitive requires a value"; SENSITIVE="$2"; shift 2 ;;
124
137
  --on) AP_ENABLED="true"; shift ;;
125
138
  --off) AP_ENABLED="false"; shift ;;
126
139
  --driver) [ $# -ge 2 ] || error_exit "--driver requires a value"; AP_DRIVER="$2"; shift 2 ;;
@@ -222,12 +235,18 @@ case "$ACTION" in
222
235
  require_param "project" "$PROJECT"; require_param "id" "$ID"
223
236
  if [ "$CLEAR" = "true" ]; then
224
237
  BODY=$(jq -n --arg note "$NOTE" '{clear: true} + (if $note != "" then {note: $note} else {} end)')
238
+ api_call POST "/project-tickets/$(enc "$PROJECT")/$(enc "$ID")/ask-owner" "$BODY" \
239
+ | jq "{success, withdrawn: .data.withdrawn, ticket: (.data.ticket | ${TICKET_ROW})}"
225
240
  else
226
241
  require_param "question" "$QUESTION"
227
- BODY=$(jq -n --arg q "$QUESTION" '{question: $q}')
242
+ BODY=$(jq -n --arg q "$QUESTION" --argjson o "$OPTIONS_JSON" --arg d "$DEFAULT_OPT" --arg dl "$DEADLINE" --arg s "$SENSITIVE" \
243
+ '{question: $q, options: $o}
244
+ + (if $d != "" then {default: $d} else {} end)
245
+ + (if $dl != "" then {deadline: $dl} else {} end)
246
+ + (if $s != "" then {sensitive: $s} else {} end)')
247
+ api_call POST "/project-tickets/$(enc "$PROJECT")/$(enc "$ID")/ask-owner" "$BODY" \
248
+ | jq "{success, decision: (.data.decision | {id, asker, status, deadline, posted: (.card != null), postError}), ticket: (.data.ticket | if . == null then null else ${TICKET_ROW} end)}"
228
249
  fi
229
- api_call POST "/project-tickets/$(enc "$PROJECT")/$(enc "$ID")/ask-owner" "$BODY" \
230
- | jq "{success, ticket: (.data | ${TICKET_ROW})}"
231
250
  ;;
232
251
  autopilot)
233
252
  require_param "project" "$PROJECT"
@@ -43,6 +43,10 @@ class H(BaseHTTPRequestHandler):
43
43
  return self.reply(200, {'success': True, 'data': {'workItem': {'id': body.get('workItemId')}, 'ticket': dict(T, status='in_progress', assignee='dev-bo', workItemId=body.get('workItemId'))}})
44
44
  if self.path.endswith('/claim'):
45
45
  return self.reply(200, {'success': True, 'data': {'claimed': True, 'workItem': {'id': 'wi-9'}, 'ticket': dict(T, status='in_progress', assignee='dev-ann', workItemId='wi-9')}})
46
+ if self.path.endswith('/ask-owner'):
47
+ if body.get('clear'):
48
+ return self.reply(200, {'success': True, 'data': {'ticket': T, 'withdrawn': 1}})
49
+ return self.reply(200, {'success': True, 'data': {'decision': {'id': 'D-1', 'asker': 'dev-ann', 'status': 'open', 'deadline': 'x', 'card': {'slackChannelId': 'C1'}}, 'ticket': dict(T, labels=['ui', 'needs-owner'])}})
46
50
  if 'forbidden' in self.path:
47
51
  return self.reply(403, {'success': False, 'error': 'Not allowed'})
48
52
  return self.reply(200, {'success': True, 'data': T})
@@ -119,12 +123,16 @@ run '{"action":"link","project":"p1","id":"APP-1","workItemId":"wi-43"}' >/dev/n
119
123
  check "link json" "$(last '.body | tostring')" '{"workItemId":"wi-43"}'
120
124
  check "link: missing work item" "$(run_err link --project p1 --id APP-1 | grep -c 'work-item')" "1"
121
125
 
122
- # --- ask-owner (orchestrator / lead): needs-owner mark with a one-line question ---
123
- run ask-owner --project p1 --id APP-1 --question "Send the draft to the partners?" >/dev/null
126
+ # --- ask-owner: a structured decision (question + 2–3 options + default) ---
127
+ OUT=$(run ask-owner --project p1 --id APP-1 --question "Send the draft to the partners?" --option "Send Monday — after review" --option "Hold" --default Hold --sensitive email)
124
128
  check "ask-owner: path" "$(last .path)" "/api/project-tickets/p1/APP-1/ask-owner"
125
- check "ask-owner: body" "$(last '.body | tostring')" '{"question":"Send the draft to the partners?"}'
126
- run ask-owner --project p1 --id APP-1 --clear --note "owner said yes" >/dev/null
129
+ check "ask-owner: body" "$(last '.body | tostring')" '{"question":"Send the draft to the partners?","options":["Send Monday — after review","Hold"],"default":"Hold","sensitive":"email"}'
130
+ check "ask-owner: output" "$(printf '%s' "$OUT" | jq -c '.decision')" '{"id":"D-1","asker":"dev-ann","status":"open","deadline":"x","posted":true,"postError":null}'
131
+ run '{"action":"ask-owner","project":"p1","id":"APP-1","question":"Q is long enough?","options":["A","B"],"default":"wait","deadline":"2026-10-02T12:00"}' >/dev/null
132
+ check "ask-owner json: body" "$(last '.body | tostring')" '{"question":"Q is long enough?","options":["A","B"],"default":"wait","deadline":"2026-10-02T12:00"}'
133
+ OUT=$(run ask-owner --project p1 --id APP-1 --clear --note "owner said yes")
127
134
  check "ask-owner: clear" "$(last '.body | tostring')" '{"clear":true,"note":"owner said yes"}'
135
+ check "ask-owner: clear output" "$(printf '%s' "$OUT" | jq -c '.withdrawn')" '1'
128
136
  check "ask-owner: missing question" "$(run_err ask-owner --project p1 --id APP-1 | grep -c 'question')" "1"
129
137
 
130
138
  # --- autopilot (owner / orchestrator): show or change the switch ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: Reply
3
- description: Answer the message you are working on. It goes back where that message came from — Slack DM, Slack channel thread, or the Crewly chat/portal — without channel ids.
3
+ description: Answer the work you are doing. It goes where that work came from — the owner's Slack DM or thread, the ticket's thread, or a new top-level post for scheduled work — without channel ids. --new-thread starts a new topic.
4
4
  version: 1.0.0
5
5
  category: communication
6
6
  skillType: claude-skill
@@ -43,10 +43,26 @@ Answer the message you are working on:
43
43
  bash config/skills/agent/core/reply/execute.sh "EFT 表改好了,附件是新版。"
44
44
  ```
45
45
 
46
- The harness remembers which message you are answering (the latest one
47
- delivered to you) and sends your answer back there, over the right path:
48
- the owner's Slack DM with you, the Slack channel thread, or the Crewly
49
- chat / portal conversation. You do not pass channel ids or thread keys.
46
+ **Answer where you were asked; a new topic goes in a new thread.**
47
+
48
+ The harness knows what you are working on and sends your answer where that
49
+ work came from:
50
+
51
+ | Your current work | Where the answer goes |
52
+ |---|---|
53
+ | a message from the owner | that conversation / Slack thread (DM, channel thread, Crewly chat / portal) |
54
+ | a project ticket | the ticket's thread in your team channel (started on the first post) |
55
+ | a scheduled / triggered task | the trigger's destination, else a NEW top-level post in your team channel |
56
+ | nothing in particular | a new top-level post in your team channel |
57
+
58
+ You do not pass channel ids or thread keys. A scheduled task's output never
59
+ lands in an old, unrelated thread.
60
+
61
+ To start a new topic yourself:
62
+
63
+ ```bash
64
+ bash config/skills/agent/core/reply/execute.sh --new-thread "Wiki link audit" "Found 3 broken links: …"
65
+ ```
50
66
 
51
67
  Keep the `CREWLY_SESSION_NAME=<you>` prefix when your prompt shows one — it
52
68
  tells the system that you are the one replying.
@@ -58,6 +74,7 @@ tells the system that you are the one replying.
58
74
  | `"<text>"` / `--text` / stdin / `--text-file` | the reply |
59
75
  | `--interim` | a short note before the real answer (what you understood, how long); "working on it" stays up |
60
76
  | `--none` | nothing to answer (you answered elsewhere, or the message was not for you) |
77
+ | `--new-thread "<title>"` | a new topic: a new top-level post in your team channel, opened with the title |
61
78
  | `--conversation <id>` / `--thread <key>` | only when your prompt tells you to answer somewhere specific |
62
79
 
63
80
  ## Rules
@@ -1,8 +1,9 @@
1
1
  #!/bin/bash
2
- # reply — answer the message you are working on. The harness sends it back
3
- # where that message came from (Slack DM, Slack room thread, portal / Talk
4
- # chat) — no channel ids, no choosing between reply-channel / reply-chat /
5
- # reply-slack. Status lines ([DONE], [BLOCKED], …) still go to the
2
+ # reply — answer the work you are doing. The harness sends it back where
3
+ # that work came from (the owner's Slack DM / room thread / portal chat, the
4
+ # ticket's thread, or a new top-level post for scheduled work) — no channel
5
+ # ids, no choosing between reply-channel / reply-chat / reply-slack.
6
+ # `--new-thread "<title>"` starts a new topic in your team channel. Status lines ([DONE], [BLOCKED], …) still go to the
6
7
  # orchestrator. See specs/2026-09-30-owner-message-guarantee.md §B.
7
8
  set -euo pipefail
8
9
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -16,12 +17,14 @@ Usage:
16
17
  bash execute.sh --text-file /path/reply.md
17
18
  bash execute.sh --interim "Got it — comparing the three quotes (~10 min)."
18
19
  bash execute.sh --none # no answer needed for this message
20
+ bash execute.sh --new-thread "Wiki link audit" "Found 3 broken links: …" # a new topic
19
21
 
20
22
  Options:
21
23
  --text | -t Reply text (or the first positional argument, or stdin)
22
24
  --text-file Read the reply from a file
23
25
  --interim A short note before the real answer; "working on it" stays up
24
26
  --none Nothing to answer (already answered elsewhere / not for you)
27
+ --new-thread Start a new thread in your team channel with this title (a new topic)
25
28
  --conversation|-C Only if your prompt tells you to answer somewhere specific
26
29
  --thread | -T Only if your prompt tells you to answer in a specific thread
27
30
  --help | -h Show this help
@@ -33,6 +36,7 @@ INTERIM=""
33
36
  NONE=""
34
37
  CONVERSATION_ID=""
35
38
  THREAD=""
39
+ NEW_THREAD=""
36
40
 
37
41
  while [[ $# -gt 0 ]]; do
38
42
  case "$1" in
@@ -43,6 +47,7 @@ while [[ $# -gt 0 ]]; do
43
47
  --none) NONE="1"; shift ;;
44
48
  --conversation|-C|--channel|-c) CONVERSATION_ID="$2"; shift 2 ;;
45
49
  --thread|-T) THREAD="$2"; shift 2 ;;
50
+ --new-thread) NEW_THREAD="$2"; shift 2 ;;
46
51
  --help|-h) print_usage; exit 0 ;;
47
52
  --) shift; if [[ $# -gt 0 && -z "$TEXT" ]]; then TEXT="$*"; fi; break ;;
48
53
  -*) echo "{\"success\":false,\"error\":\"Unknown option: $1\"}" >&2; print_usage >&2; exit 2 ;;
@@ -62,7 +67,7 @@ fi
62
67
  # Literal \n (from JSON-escaped text) → real newlines
63
68
  if [ -n "$TEXT" ]; then _NL=$'\n'; TEXT="${TEXT//\\n/$_NL}"; fi
64
69
 
65
- BODY=$(TEXT="$TEXT" INTERIM="$INTERIM" NONE="$NONE" CONVERSATION_ID="$CONVERSATION_ID" THREAD="$THREAD" python3 -c '
70
+ BODY=$(TEXT="$TEXT" INTERIM="$INTERIM" NONE="$NONE" CONVERSATION_ID="$CONVERSATION_ID" THREAD="$THREAD" NEW_THREAD="$NEW_THREAD" python3 -c '
66
71
  import os, json
67
72
  p = {}
68
73
  if os.environ.get("NONE"):
@@ -75,6 +80,8 @@ if os.environ.get("CONVERSATION_ID"):
75
80
  p["conversationId"] = os.environ["CONVERSATION_ID"]
76
81
  if os.environ.get("THREAD"):
77
82
  p["thread"] = os.environ["THREAD"]
83
+ if os.environ.get("NEW_THREAD"):
84
+ p["newThread"] = os.environ["NEW_THREAD"]
78
85
  print(json.dumps(p))
79
86
  ')
80
87
 
@@ -78,6 +78,14 @@ LOG=$(cat "$STUB_LOG")
78
78
  check "conversationId" "$LOG" 'chan-9'
79
79
  check "thread" "$LOG" 'D0ABC:1790000000.000100'
80
80
 
81
+ echo "test 4b: --new-thread"
82
+ : > "$STUB_LOG"
83
+ run_skill --new-thread "Wiki link audit" "Found 3 broken links" </dev/null >/dev/null 2>&1 || true
84
+ LOG=$(cat "$STUB_LOG")
85
+ check "newThread title" "$LOG" 'newThread'
86
+ check "title text" "$LOG" 'Wiki link audit'
87
+ check "body text" "$LOG" 'Found 3 broken links'
88
+
81
89
  echo "test 5: no text → error, nothing sent"
82
90
  : > "$STUB_LOG"
83
91
  if OUT=$(run_skill </dev/null 2>&1); then FAIL=$((FAIL + 1)); echo " ✗ expected non-zero exit"; else PASS=$((PASS + 1)); echo " ✓ non-zero exit"; fi
@@ -56,6 +56,10 @@ re-creating (#621).
56
56
  fires.
57
57
  - If you forget to cancel a done followup, it will still terminate itself.
58
58
  That's by design — we refuse to let followups live forever.
59
+ - **Where the output goes:** a fired followup's answer is posted as a NEW
60
+ top-level message in the target's team channel (never an old thread).
61
+ Pass `--destination "#channel"`, `C…` or `C…:<ts>` to post it somewhere
62
+ specific.
59
63
 
60
64
  ## Examples
61
65
 
@@ -71,6 +75,10 @@ bash execute.sh --fire-at 2026-04-24T09:00:00Z \
71
75
  # Recurring with cap — will auto-stop after 3 tries even if you forget to cancel
72
76
  bash execute.sh --cron "0 * * * *" --max-fires 3 \
73
77
  --title "Poll Rex hourly (give up after 3)"
78
+
79
+ # Quarterly review — post the result in a specific thread
80
+ bash execute.sh --cron "0 9 1 */3 *" --title "Quarterly wiki review" \
81
+ --destination "C0123ABCD:1790000000.000100"
74
82
  ```
75
83
 
76
84
  ## Output
@@ -43,6 +43,8 @@ Options:
43
43
  --name Stable name (used for cancel / dedup). Auto-generated if absent.
44
44
  --max-fires Stop after N fires (default: 1 for --in-minutes/--fire-at, unlimited for --cron)
45
45
  --max-idle-fires Auto-cancel after N consecutive fires that produced no work (default: 3)
46
+ --destination Where the output is posted: "#channel-name", a channel id (C…) or a thread (C…:<ts>).
47
+ Default: a new top-level post in the target's team channel.
46
48
  --json | -j Raw JSON payload (same as legacy)
47
49
  --help | -h Show this help
48
50
  EOF_USAGE
@@ -59,6 +61,7 @@ DESCRIPTION=""
59
61
  NAME=""
60
62
  MAX_FIRES=""
61
63
  MAX_IDLE_FIRES="3"
64
+ DESTINATION=""
62
65
 
63
66
  if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
64
67
  INPUT_JSON="$1"
@@ -77,6 +80,7 @@ while [[ $# -gt 0 ]]; do
77
80
  --name) NAME="$2"; shift 2 ;;
78
81
  --max-fires) MAX_FIRES="$2"; shift 2 ;;
79
82
  --max-idle-fires) MAX_IDLE_FIRES="$2"; shift 2 ;;
83
+ --destination) DESTINATION="$2"; shift 2 ;;
80
84
  --json|-j) INPUT_JSON="$2"; shift 2 ;;
81
85
  --help|-h) print_usage; exit 0 ;;
82
86
  --) shift; break ;;
@@ -103,6 +107,7 @@ if [ -n "$INPUT_JSON" ]; then
103
107
  NAME=$(printf '%s' "$INPUT_JSON" | jq -r '.name // empty')
104
108
  MAX_FIRES=$(printf '%s' "$INPUT_JSON" | jq -r '.maxFires // empty')
105
109
  MAX_IDLE_FIRES=$(printf '%s' "$INPUT_JSON" | jq -r '.maxIdleFires // "3"')
110
+ DESTINATION=$(printf '%s' "$INPUT_JSON" | jq -r '.destination // empty')
106
111
  fi
107
112
 
108
113
  # Validation
@@ -167,7 +172,9 @@ BODY=$(jq -n \
167
172
  --arg teamId "$TEAM_ID" \
168
173
  --arg maxFires "$MAX_FIRES" \
169
174
  --arg maxIdle "$MAX_IDLE_FIRES" \
175
+ --arg destination "$DESTINATION" \
170
176
  '{type:$type, config:$config, action:$action, createdBy:$createdBy, name:$name}
177
+ + (if $destination != "" then {destination:$destination} else {} end)
171
178
  + (if $teamId != "" then {teamId:$teamId} else {} end)
172
179
  + (if $maxFires != "" then {maxFires:($maxFires|tonumber)} else {} end)
173
180
  + (if $maxIdle != "" then {maxIdleFires:($maxIdle|tonumber)} else {} end)')
@@ -44,8 +44,12 @@
44
44
  "bot_events": [
45
45
  "message.im",
46
46
  "message.channels",
47
- "app_mention"
47
+ "app_mention",
48
+ "reaction_added"
48
49
  ]
50
+ },
51
+ "interactivity": {
52
+ "is_enabled": true
49
53
  }
50
54
  }
51
55
  }