crewly 1.20.179 → 1.20.181
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/roles/team-leader/prompt.md +2 -2
- package/config/roles/team-leader/tl-addon.md +2 -2
- package/config/skills/agent/core/ask-owner/SKILL.md +79 -0
- package/config/skills/agent/core/ask-owner/execute.sh +66 -0
- package/config/skills/agent/core/ask-owner/execute.test.sh +62 -0
- package/config/skills/agent/core/project-tickets/SKILL.md +21 -9
- package/config/skills/agent/core/project-tickets/execute.sh +28 -9
- package/config/skills/agent/core/project-tickets/execute.test.sh +12 -4
- package/config/skills/agent/core/reply/SKILL.md +22 -5
- package/config/skills/agent/core/reply/execute.sh +12 -5
- package/config/skills/agent/core/reply/execute.test.sh +8 -0
- package/config/skills/agent/core/schedule-followup/SKILL.md +8 -0
- package/config/skills/agent/core/schedule-followup/execute.sh +7 -0
- package/config/skills/agent/remote-browser/SKILL.md +28 -8
- package/config/slack-app-manifest.json +5 -1
- package/config/slack-master-app-manifest.json +7 -2
- package/dist/backend/backend/src/constants.d.ts +99 -5
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +106 -5
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts +2 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.js +15 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.d.ts +3 -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 +54 -1
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/decisions/decisions.controller.d.ts +67 -0
- package/dist/backend/backend/src/controllers/decisions/decisions.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/decisions/decisions.controller.js +215 -0
- package/dist/backend/backend/src/controllers/decisions/decisions.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts +8 -5
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js +51 -12
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts +4 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +135 -2
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +6 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/boot/boot-announce.service.d.ts +1 -1
- package/dist/backend/backend/src/services/boot/boot-announce.service.js +13 -13
- package/dist/backend/backend/src/services/boot/boot-announce.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-approval.service.d.ts +202 -0
- package/dist/backend/backend/src/services/browser/browser-approval.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/browser/browser-approval.service.js +533 -0
- package/dist/backend/backend/src/services/browser/browser-approval.service.js.map +1 -0
- package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts +24 -0
- package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-bridge.service.js +55 -0
- package/dist/backend/backend/src/services/browser/browser-bridge.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-session.service.d.ts +51 -0
- package/dist/backend/backend/src/services/browser/browser-session.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-session.service.js +82 -3
- package/dist/backend/backend/src/services/browser/browser-session.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/held-action-store.d.ts +125 -0
- package/dist/backend/backend/src/services/browser/held-action-store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/browser/held-action-store.js +141 -0
- package/dist/backend/backend/src/services/browser/held-action-store.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.service.js +6 -6
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.d.ts +3 -3
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.js +14 -13
- package/dist/backend/backend/src/services/cloud/cloud-disconnect-notice.utils.js.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision-card.d.ts +155 -0
- package/dist/backend/backend/src/services/decisions/decision-card.d.ts.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision-card.js +337 -0
- package/dist/backend/backend/src/services/decisions/decision-card.js.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision-contract.d.ts +83 -0
- package/dist/backend/backend/src/services/decisions/decision-contract.d.ts.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision-contract.js +226 -0
- package/dist/backend/backend/src/services/decisions/decision-contract.js.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision-routing.d.ts +34 -0
- package/dist/backend/backend/src/services/decisions/decision-routing.d.ts.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision-routing.js +64 -0
- package/dist/backend/backend/src/services/decisions/decision-routing.js.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision-store.d.ts +80 -0
- package/dist/backend/backend/src/services/decisions/decision-store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision-store.js +151 -0
- package/dist/backend/backend/src/services/decisions/decision-store.js.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision.service.d.ts +338 -0
- package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision.service.js +729 -0
- package/dist/backend/backend/src/services/decisions/decision.service.js.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts +70 -0
- package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts.map +1 -0
- package/dist/backend/backend/src/services/decisions/decision.wiring.js +172 -0
- package/dist/backend/backend/src/services/decisions/decision.wiring.js.map +1 -0
- package/dist/backend/backend/src/services/decisions/ticket-thread-store.d.ts +97 -0
- package/dist/backend/backend/src/services/decisions/ticket-thread-store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/decisions/ticket-thread-store.js +137 -0
- package/dist/backend/backend/src/services/decisions/ticket-thread-store.js.map +1 -0
- package/dist/backend/backend/src/services/orc/work-item-destination.d.ts +165 -0
- package/dist/backend/backend/src/services/orc/work-item-destination.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orc/work-item-destination.js +239 -0
- package/dist/backend/backend/src/services/orc/work-item-destination.js.map +1 -0
- package/dist/backend/backend/src/services/orc/work-item-destination.wiring.d.ts +134 -0
- package/dist/backend/backend/src/services/orc/work-item-destination.wiring.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orc/work-item-destination.wiring.js +217 -0
- package/dist/backend/backend/src/services/orc/work-item-destination.wiring.js.map +1 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts +1 -30
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js +1 -22
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.d.ts +2 -15
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.js +8 -22
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-messages.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts +11 -12
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js +27 -52
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts +6 -0
- 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 +3 -1
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +12 -12
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts +16 -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 +56 -2
- package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/backend/backend/src/services/system/auto-update.service.js +2 -2
- package/dist/backend/backend/src/services/system/auto-update.service.js.map +1 -1
- package/dist/backend/backend/src/services/system/auto-update.utils.js +2 -2
- package/dist/backend/backend/src/services/system/auto-update.utils.js.map +1 -1
- package/dist/backend/backend/src/types/decision.types.d.ts +168 -0
- package/dist/backend/backend/src/types/decision.types.d.ts.map +1 -0
- package/dist/backend/backend/src/types/decision.types.js +13 -0
- package/dist/backend/backend/src/types/decision.types.js.map +1 -0
- package/dist/backend/backend/src/types/slack.types.d.ts +30 -0
- package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/slack.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/trigger.types.d.ts +17 -0
- package/dist/backend/backend/src/types/v2/trigger.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/trigger.types.js +17 -0
- package/dist/backend/backend/src/types/v2/trigger.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +99 -5
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +106 -5
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +12 -12
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts +16 -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 +56 -2
- package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/cli/backend/src/services/system/auto-update.utils.js +2 -2
- package/dist/cli/backend/src/services/system/auto-update.utils.js.map +1 -1
- package/dist/cli/backend/src/types/slack.types.d.ts +30 -0
- package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/slack.types.js.map +1 -1
- package/dist/cli/backend/src/types/v2/trigger.types.d.ts +17 -0
- package/dist/cli/backend/src/types/v2/trigger.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/trigger.types.js +17 -0
- package/dist/cli/backend/src/types/v2/trigger.types.js.map +1 -1
- package/frontend/dist/assets/index-04d192f8.css +42 -0
- package/frontend/dist/assets/{index-69af46c3.js → index-4271e95e.js} +259 -259
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- 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>"
|
|
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
|
|
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>"
|
|
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
|
|
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 (
|
|
150
|
+
## Ask the owner (assignee / team lead / orchestrator)
|
|
151
151
|
|
|
152
152
|
```bash
|
|
153
|
-
bash execute.sh ask-owner --project P --id APP-12
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 "…"
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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"
|
|
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
|
|
123
|
-
run ask-owner --project p1 --id APP-1 --question "Send the draft to the partners?"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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)')
|