crewly 1.5.22 → 1.6.0

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 (112) hide show
  1. package/config/roles/orchestrator/prompt.md +182 -25
  2. package/config/skills/agent/core/cancel-followup/SKILL.md +38 -0
  3. package/config/skills/agent/core/cancel-followup/execute.sh +111 -0
  4. package/config/skills/agent/core/cancel-followup/execute.test.sh +42 -0
  5. package/config/skills/agent/core/list-my-followups/SKILL.md +36 -0
  6. package/config/skills/agent/core/list-my-followups/execute.sh +93 -0
  7. package/config/skills/agent/core/list-my-followups/execute.test.sh +41 -0
  8. package/config/skills/agent/core/schedule-followup/SKILL.md +53 -0
  9. package/config/skills/agent/core/schedule-followup/execute.sh +195 -0
  10. package/config/skills/agent/core/schedule-followup/execute.test.sh +48 -0
  11. package/config/skills/agent/core/watch-for-event/SKILL.md +60 -0
  12. package/config/skills/agent/core/watch-for-event/execute.sh +177 -0
  13. package/config/skills/agent/core/watch-for-event/execute.test.sh +43 -0
  14. package/config/skills/orchestrator/credential-manager/SKILL.md +218 -0
  15. package/config/skills/orchestrator/credential-manager/execute.sh +166 -0
  16. package/dist/backend/backend/src/controllers/credentials/credentials.controller.d.ts +80 -0
  17. package/dist/backend/backend/src/controllers/credentials/credentials.controller.d.ts.map +1 -0
  18. package/dist/backend/backend/src/controllers/credentials/credentials.controller.js +365 -0
  19. package/dist/backend/backend/src/controllers/credentials/credentials.controller.js.map +1 -0
  20. package/dist/backend/backend/src/controllers/credentials/credentials.routes.d.ts +26 -0
  21. package/dist/backend/backend/src/controllers/credentials/credentials.routes.d.ts.map +1 -0
  22. package/dist/backend/backend/src/controllers/credentials/credentials.routes.js +40 -0
  23. package/dist/backend/backend/src/controllers/credentials/credentials.routes.js.map +1 -0
  24. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +23 -14
  25. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
  26. package/dist/backend/backend/src/scripts/backfill-mission-priority.d.ts +3 -1
  27. package/dist/backend/backend/src/scripts/backfill-mission-priority.d.ts.map +1 -1
  28. package/dist/backend/backend/src/scripts/backfill-mission-priority.js +16 -4
  29. package/dist/backend/backend/src/scripts/backfill-mission-priority.js.map +1 -1
  30. package/dist/backend/backend/src/services/browser/browser-proxy.service.js +1 -1
  31. package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
  32. package/dist/backend/backend/src/services/credential/credential-store.service.d.ts +161 -0
  33. package/dist/backend/backend/src/services/credential/credential-store.service.d.ts.map +1 -0
  34. package/dist/backend/backend/src/services/credential/credential-store.service.js +298 -0
  35. package/dist/backend/backend/src/services/credential/credential-store.service.js.map +1 -0
  36. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts +117 -0
  37. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -0
  38. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +293 -0
  39. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -0
  40. package/dist/backend/backend/src/services/project/task.service.d.ts +18 -2
  41. package/dist/backend/backend/src/services/project/task.service.d.ts.map +1 -1
  42. package/dist/backend/backend/src/services/project/task.service.js +69 -53
  43. package/dist/backend/backend/src/services/project/task.service.js.map +1 -1
  44. package/dist/backend/backend/src/services/v3/contract-matcher.d.ts +20 -0
  45. package/dist/backend/backend/src/services/v3/contract-matcher.d.ts.map +1 -0
  46. package/dist/backend/backend/src/services/v3/contract-matcher.js +33 -0
  47. package/dist/backend/backend/src/services/v3/contract-matcher.js.map +1 -0
  48. package/dist/backend/backend/src/services/v3/escalation.service.d.ts +20 -1
  49. package/dist/backend/backend/src/services/v3/escalation.service.d.ts.map +1 -1
  50. package/dist/backend/backend/src/services/v3/escalation.service.js +97 -28
  51. package/dist/backend/backend/src/services/v3/escalation.service.js.map +1 -1
  52. package/dist/backend/backend/src/services/v3/service-contract-gate.service.d.ts +6 -4
  53. package/dist/backend/backend/src/services/v3/service-contract-gate.service.d.ts.map +1 -1
  54. package/dist/backend/backend/src/services/v3/service-contract-gate.service.js +18 -28
  55. package/dist/backend/backend/src/services/v3/service-contract-gate.service.js.map +1 -1
  56. package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.d.ts.map +1 -1
  57. package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js +14 -9
  58. package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js.map +1 -1
  59. package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts +34 -1
  60. package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts.map +1 -1
  61. package/dist/backend/backend/src/services/v3/trigger-engine.service.js +115 -5
  62. package/dist/backend/backend/src/services/v3/trigger-engine.service.js.map +1 -1
  63. package/dist/backend/backend/src/types/credential.types.d.ts +185 -0
  64. package/dist/backend/backend/src/types/credential.types.d.ts.map +1 -0
  65. package/dist/backend/backend/src/types/credential.types.js +76 -0
  66. package/dist/backend/backend/src/types/credential.types.js.map +1 -0
  67. package/dist/backend/backend/src/utils/encryption.utils.d.ts +57 -0
  68. package/dist/backend/backend/src/utils/encryption.utils.d.ts.map +1 -0
  69. package/dist/backend/backend/src/utils/encryption.utils.js +162 -0
  70. package/dist/backend/backend/src/utils/encryption.utils.js.map +1 -0
  71. package/dist/cli/backend/src/services/credential/credential-store.service.d.ts +161 -0
  72. package/dist/cli/backend/src/services/credential/credential-store.service.d.ts.map +1 -0
  73. package/dist/cli/backend/src/services/credential/credential-store.service.js +298 -0
  74. package/dist/cli/backend/src/services/credential/credential-store.service.js.map +1 -0
  75. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts +117 -0
  76. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -0
  77. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +293 -0
  78. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -0
  79. package/dist/cli/backend/src/services/settings/settings.service.d.ts +168 -0
  80. package/dist/cli/backend/src/services/settings/settings.service.d.ts.map +1 -0
  81. package/dist/cli/backend/src/services/settings/settings.service.js +312 -0
  82. package/dist/cli/backend/src/services/settings/settings.service.js.map +1 -0
  83. package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts +159 -0
  84. package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts.map +1 -0
  85. package/dist/cli/backend/src/services/skill/skill-executor.service.js +626 -0
  86. package/dist/cli/backend/src/services/skill/skill-executor.service.js.map +1 -0
  87. package/dist/cli/backend/src/services/skill/skill.service.d.ts +273 -0
  88. package/dist/cli/backend/src/services/skill/skill.service.d.ts.map +1 -0
  89. package/dist/cli/backend/src/services/skill/skill.service.js +655 -0
  90. package/dist/cli/backend/src/services/skill/skill.service.js.map +1 -0
  91. package/dist/cli/backend/src/types/credential.types.d.ts +185 -0
  92. package/dist/cli/backend/src/types/credential.types.d.ts.map +1 -0
  93. package/dist/cli/backend/src/types/credential.types.js +76 -0
  94. package/dist/cli/backend/src/types/credential.types.js.map +1 -0
  95. package/dist/cli/backend/src/utils/encryption.utils.d.ts +57 -0
  96. package/dist/cli/backend/src/utils/encryption.utils.d.ts.map +1 -0
  97. package/dist/cli/backend/src/utils/encryption.utils.js +162 -0
  98. package/dist/cli/backend/src/utils/encryption.utils.js.map +1 -0
  99. package/dist/cli/backend/src/utils/skill-md-parser.d.ts +38 -0
  100. package/dist/cli/backend/src/utils/skill-md-parser.d.ts.map +1 -0
  101. package/dist/cli/backend/src/utils/skill-md-parser.js +47 -0
  102. package/dist/cli/backend/src/utils/skill-md-parser.js.map +1 -0
  103. package/frontend/dist/assets/{index-dc92ab64.css → index-6aaa0630.css} +1 -1
  104. package/frontend/dist/assets/{index-76d76633.js → index-9e6d97d1.js} +334 -328
  105. package/frontend/dist/index.html +2 -2
  106. package/package.json +1 -1
  107. package/config/experts/empathetic-resolver/expert.json +0 -11
  108. package/config/experts/empathetic-resolver.md +0 -32
  109. package/config/experts/pragmatic-architect/expert.json +0 -11
  110. package/config/experts/pragmatic-architect.md +0 -32
  111. package/config/experts/viral-alchemist/expert.json +0 -11
  112. package/config/experts/viral-alchemist.md +0 -32
@@ -0,0 +1,53 @@
1
+ # schedule-followup
2
+
3
+ Schedule a future check-in that creates a WorkItem for you or another agent.
4
+ Use this when you need to **come back to something at a specific time** — most
5
+ commonly to chase a response, verify an async result, or re-prompt an agent
6
+ that went quiet.
7
+
8
+ ## When to use this vs other scheduling skills
9
+
10
+ | Need | Use |
11
+ |------|-----|
12
+ | "Wake me up in 30 min to check X" (self) | `schedule-followup --in-minutes 30 --title "Check X"` |
13
+ | "Check back on another agent at 9am" | `schedule-followup --fire-at 2026-04-24T09:00:00Z --target agent-session --title "..."` |
14
+ | "Hourly for up to 3 tries, then give up" | `schedule-followup --cron "0 * * * *" --max-fires 3` |
15
+ | "Whenever Rex goes idle, check progress" | `watch-for-event` (event-based, not this skill) |
16
+ | Simple self-reminder with no team scope | `schedule-check` (older skill) |
17
+
18
+ ## Key invariants
19
+
20
+ - Every followup is **team-scoped**. It shows up under `list-my-followups` and
21
+ can be cancelled by `name` via `cancel-followup`.
22
+ - Every followup has a **finite lifetime**: either `maxFires` exhausts it, or
23
+ `maxIdleFires` (default 3) auto-cancels it after consecutive unproductive
24
+ fires.
25
+ - If you forget to cancel a done followup, it will still terminate itself.
26
+ That's by design — we refuse to let followups live forever.
27
+
28
+ ## Examples
29
+
30
+ ```bash
31
+ # One-shot self-reminder 30 minutes from now
32
+ bash execute.sh --in-minutes 30 --title "Verify Rex delivered the draft"
33
+
34
+ # One-shot absolute time, targeting Ella
35
+ bash execute.sh --fire-at 2026-04-24T09:00:00Z \
36
+ --target crewly-marketing-ella-member-1 \
37
+ --title "Re-prompt Rex if no response yet"
38
+
39
+ # Recurring with cap — will auto-stop after 3 tries even if you forget to cancel
40
+ bash execute.sh --cron "0 * * * *" --max-fires 3 \
41
+ --title "Poll Rex hourly (give up after 3)"
42
+ ```
43
+
44
+ ## Output
45
+
46
+ On success, returns the created `Trigger` object (JSON with `id`, `status`,
47
+ `nextFireAt`, etc). Keep the `id` or `name` if you may need to cancel later.
48
+
49
+ ## Cancelling
50
+
51
+ - By id: `cancel-followup --id <trigger-id>`
52
+ - By name: `cancel-followup --name "followup:abc123"`
53
+ - Or let `maxFires`/`maxIdleFires` do it automatically.
@@ -0,0 +1,195 @@
1
+ #!/bin/bash
2
+ # Schedule a one-shot (or bounded-recurring) follow-up trigger that fires in
3
+ # the future and drops a WorkItem into the pool for you or another agent.
4
+ #
5
+ # This is the "timer-based follow-up" pattern: you commit to checking back on
6
+ # something at time T, and the system wakes you (or the target) when T arrives.
7
+ # The trigger is scoped to your team so it shows up under list-my-followups,
8
+ # and it auto-cancels after maxFires / maxIdleFires — there is no silent
9
+ # forever-loop risk.
10
+ #
11
+ # Contrast with:
12
+ # - `schedule-check` — self-timer that just messages you; no team scope.
13
+ # - orchestrator `create-cron` — for recurring org-level schedules.
14
+ #
15
+ # Supports CLI flags (preferred), legacy JSON, stdin pipe, or @filepath.
16
+ set -euo pipefail
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
19
+
20
+ CREWLY_HOME="${HOME}/.crewly"
21
+
22
+ print_usage() {
23
+ cat <<'EOF_USAGE'
24
+ Usage:
25
+ # Fire once, 30 minutes from now, target self
26
+ bash execute.sh --in-minutes 30 --title "Check Rex response"
27
+
28
+ # Fire once at a specific absolute time (ISO8601), target another agent
29
+ bash execute.sh --fire-at 2026-04-24T09:00:00Z --target crewly-marketing-ella-member-1 --title "Re-prompt Rex"
30
+
31
+ # Recurring with a hard cap — 3 tries, 1h apart
32
+ bash execute.sh --cron "0 * * * *" --max-fires 3 --title "Hourly Rex check"
33
+
34
+ # Legacy JSON
35
+ bash execute.sh '{"inMinutes":30,"title":"Check Rex","target":"ella"}'
36
+
37
+ Options:
38
+ --in-minutes | -m Fire once N minutes from now (mutually exclusive with --fire-at/--cron)
39
+ --fire-at Fire once at ISO8601 datetime (e.g. 2026-04-24T09:00:00Z)
40
+ --cron Cron expression for recurring fires (UTC by default)
41
+ --timezone IANA timezone for cron (default: UTC)
42
+ --target Agent session to receive the follow-up WorkItem (default: yourself)
43
+ --title | -T Title for the generated WorkItem (required)
44
+ --description Longer description / context
45
+ --name Stable name (used for cancel / dedup). Auto-generated if absent.
46
+ --max-fires Stop after N fires (default: 1 for --in-minutes/--fire-at, unlimited for --cron)
47
+ --max-idle-fires Auto-cancel after N consecutive fires that produced no work (default: 3)
48
+ --json | -j Raw JSON payload (same as legacy)
49
+ --help | -h Show this help
50
+ EOF_USAGE
51
+ }
52
+
53
+ resolve_team_id() {
54
+ local session="${1:-${CREWLY_SESSION_NAME:-}}"
55
+ [ -z "$session" ] && return 1
56
+ local teams_dir="${CREWLY_HOME}/teams"
57
+ [ ! -d "$teams_dir" ] && return 1
58
+ for config in "$teams_dir"/*/config.json; do
59
+ [ -f "$config" ] || continue
60
+ local found
61
+ found=$(jq -r --arg s "$session" '.members[]? | select(.sessionName == $s) | "found"' "$config" 2>/dev/null | head -1)
62
+ if [ "$found" = "found" ]; then
63
+ basename "$(dirname "$config")"
64
+ return 0
65
+ fi
66
+ done
67
+ return 1
68
+ }
69
+
70
+ INPUT_JSON=""
71
+ IN_MINUTES=""
72
+ FIRE_AT=""
73
+ CRON_EXPR=""
74
+ TIMEZONE="UTC"
75
+ TARGET=""
76
+ TITLE=""
77
+ DESCRIPTION=""
78
+ NAME=""
79
+ MAX_FIRES=""
80
+ MAX_IDLE_FIRES="3"
81
+
82
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
83
+ INPUT_JSON="$1"
84
+ shift || true
85
+ fi
86
+
87
+ while [[ $# -gt 0 ]]; do
88
+ case "$1" in
89
+ --in-minutes|-m) IN_MINUTES="$2"; shift 2 ;;
90
+ --fire-at) FIRE_AT="$2"; shift 2 ;;
91
+ --cron) CRON_EXPR="$2"; shift 2 ;;
92
+ --timezone) TIMEZONE="$2"; shift 2 ;;
93
+ --target) TARGET="$2"; shift 2 ;;
94
+ --title|-T) TITLE="$2"; shift 2 ;;
95
+ --description) DESCRIPTION="$2"; shift 2 ;;
96
+ --name) NAME="$2"; shift 2 ;;
97
+ --max-fires) MAX_FIRES="$2"; shift 2 ;;
98
+ --max-idle-fires) MAX_IDLE_FIRES="$2"; shift 2 ;;
99
+ --json|-j) INPUT_JSON="$2"; shift 2 ;;
100
+ --help|-h) print_usage; exit 0 ;;
101
+ --) shift; break ;;
102
+ *)
103
+ if [[ -z "$INPUT_JSON" && ${1:0:1} == '{' ]]; then
104
+ INPUT_JSON="$1"; shift
105
+ else
106
+ echo '{"error":"Unknown argument: '"$1"'"}' >&2
107
+ exit 1
108
+ fi
109
+ ;;
110
+ esac
111
+ done
112
+
113
+ # Legacy JSON → populate vars
114
+ if [ -n "$INPUT_JSON" ]; then
115
+ IN_MINUTES=$(printf '%s' "$INPUT_JSON" | jq -r '.inMinutes // empty')
116
+ FIRE_AT=$(printf '%s' "$INPUT_JSON" | jq -r '.fireAt // empty')
117
+ CRON_EXPR=$(printf '%s' "$INPUT_JSON" | jq -r '.cron // empty')
118
+ TIMEZONE=$(printf '%s' "$INPUT_JSON" | jq -r '.timezone // "UTC"')
119
+ TARGET=$(printf '%s' "$INPUT_JSON" | jq -r '.target // empty')
120
+ TITLE=$(printf '%s' "$INPUT_JSON" | jq -r '.title // empty')
121
+ DESCRIPTION=$(printf '%s' "$INPUT_JSON" | jq -r '.description // empty')
122
+ NAME=$(printf '%s' "$INPUT_JSON" | jq -r '.name // empty')
123
+ MAX_FIRES=$(printf '%s' "$INPUT_JSON" | jq -r '.maxFires // empty')
124
+ MAX_IDLE_FIRES=$(printf '%s' "$INPUT_JSON" | jq -r '.maxIdleFires // "3"')
125
+ fi
126
+
127
+ # Validation
128
+ [ -z "$TITLE" ] && { echo '{"error":"--title is required"}' >&2; exit 1; }
129
+
130
+ TIMING_COUNT=0
131
+ [ -n "$IN_MINUTES" ] && TIMING_COUNT=$((TIMING_COUNT + 1))
132
+ [ -n "$FIRE_AT" ] && TIMING_COUNT=$((TIMING_COUNT + 1))
133
+ [ -n "$CRON_EXPR" ] && TIMING_COUNT=$((TIMING_COUNT + 1))
134
+ if [ "$TIMING_COUNT" -ne 1 ]; then
135
+ echo '{"error":"Exactly one of --in-minutes, --fire-at, or --cron must be provided"}' >&2
136
+ exit 1
137
+ fi
138
+
139
+ # Default target = self
140
+ [ -z "$TARGET" ] && TARGET="${CREWLY_SESSION_NAME:-}"
141
+ [ -z "$TARGET" ] && { echo '{"error":"No --target and CREWLY_SESSION_NAME is unset"}' >&2; exit 1; }
142
+
143
+ # Resolve owning team (for team-scoped listing / cancel)
144
+ TEAM_ID=$(resolve_team_id || true)
145
+
146
+ # Auto-generate name if caller didn't supply one (stable per target+title)
147
+ if [ -z "$NAME" ]; then
148
+ HASH=$(printf '%s|%s' "$TARGET" "$TITLE" | shasum -a 1 | awk '{print substr($1,1,8)}')
149
+ NAME="followup:${HASH}"
150
+ fi
151
+
152
+ # Build TimeTriggerConfig
153
+ CONFIG_JSON=""
154
+ if [ -n "$IN_MINUTES" ]; then
155
+ DELAY_MS=$((IN_MINUTES * 60 * 1000))
156
+ CONFIG_JSON=$(jq -n --argjson d "$DELAY_MS" '{type:"time", delayMs:$d}')
157
+ # Sensible default for one-shot
158
+ [ -z "$MAX_FIRES" ] && MAX_FIRES="1"
159
+ elif [ -n "$FIRE_AT" ]; then
160
+ CONFIG_JSON=$(jq -n --arg f "$FIRE_AT" '{type:"time", fireAt:$f}')
161
+ [ -z "$MAX_FIRES" ] && MAX_FIRES="1"
162
+ else
163
+ CONFIG_JSON=$(jq -n --arg c "$CRON_EXPR" --arg tz "$TIMEZONE" '{type:"time", cronExpression:$c, timezone:$tz}')
164
+ # Recurring: no implicit maxFires cap (caller should set one for followup hygiene)
165
+ fi
166
+
167
+ # Build TriggerAction.createWorkItem
168
+ WI_JSON=$(jq -n \
169
+ --arg type "delegate" \
170
+ --arg owner "team_lead" \
171
+ --arg target "$TARGET" \
172
+ --arg title "$TITLE" \
173
+ --arg description "$DESCRIPTION" \
174
+ '{type:$type, owner:$owner, target:$target, title:$title}
175
+ + (if $description != "" then {description:$description} else {} end)')
176
+
177
+ ACTION_JSON=$(jq -n --argjson wi "$WI_JSON" '{createWorkItem: $wi}')
178
+
179
+ # Assemble final CreateTriggerInput
180
+ BODY=$(jq -n \
181
+ --arg type "time" \
182
+ --argjson config "$CONFIG_JSON" \
183
+ --argjson action "$ACTION_JSON" \
184
+ --arg createdBy "system" \
185
+ --arg name "$NAME" \
186
+ --arg teamId "$TEAM_ID" \
187
+ --arg maxFires "$MAX_FIRES" \
188
+ --arg maxIdle "$MAX_IDLE_FIRES" \
189
+ '{type:$type, config:$config, action:$action, createdBy:$createdBy, name:$name}
190
+ + (if $teamId != "" then {teamId:$teamId} else {} end)
191
+ + (if $maxFires != "" then {maxFires:($maxFires|tonumber)} else {} end)
192
+ + (if $maxIdle != "" then {maxIdleFires:($maxIdle|tonumber)} else {} end)')
193
+
194
+ RESP=$(api_call POST "/triggers" "$BODY")
195
+ echo "$RESP"
@@ -0,0 +1,48 @@
1
+ #!/bin/bash
2
+ # Validation tests for schedule-followup skill.
3
+ # End-to-end behaviour (actually POSTing to /api/triggers) is verified by the
4
+ # backend integration tests; here we cover argument parsing + guard rails.
5
+ set -eo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ PASS=0
9
+ FAIL=0
10
+
11
+ assert_contains() {
12
+ local test_name="$1" needle="$2" haystack="$3"
13
+ if printf '%s' "$haystack" | grep -q -- "$needle"; then
14
+ PASS=$((PASS + 1))
15
+ echo " ✓ ${test_name}"
16
+ else
17
+ FAIL=$((FAIL + 1))
18
+ echo " ✗ ${test_name}"
19
+ echo " expected to contain: ${needle}"
20
+ echo " got: ${haystack}"
21
+ fi
22
+ }
23
+
24
+ echo "=== schedule-followup tests ==="
25
+
26
+ echo ""
27
+ echo "--- Required args ---"
28
+
29
+ OUTPUT=$(bash "$SCRIPT_DIR/execute.sh" --in-minutes 30 2>&1) || true
30
+ assert_contains "Missing --title rejected" "title is required" "$OUTPUT"
31
+
32
+ OUTPUT=$(bash "$SCRIPT_DIR/execute.sh" --title "Check Rex" 2>&1) || true
33
+ assert_contains "No timing spec rejected" "one of --in-minutes" "$OUTPUT"
34
+
35
+ OUTPUT=$(bash "$SCRIPT_DIR/execute.sh" --title "Check" --in-minutes 30 --fire-at "2026-01-01T00:00:00Z" 2>&1) || true
36
+ assert_contains "Conflicting timing specs rejected" "one of --in-minutes" "$OUTPUT"
37
+
38
+ echo ""
39
+ echo "--- Help ---"
40
+
41
+ OUTPUT=$(bash "$SCRIPT_DIR/execute.sh" --help 2>&1) || true
42
+ assert_contains "Help lists --in-minutes" -- "--in-minutes" "$OUTPUT"
43
+ assert_contains "Help lists --fire-at" -- "--fire-at" "$OUTPUT"
44
+ assert_contains "Help lists --cron" -- "--cron" "$OUTPUT"
45
+
46
+ echo ""
47
+ echo "=== Results: ${PASS} passed, ${FAIL} failed ==="
48
+ [ $FAIL -eq 0 ]
@@ -0,0 +1,60 @@
1
+ # watch-for-event
2
+
3
+ Subscribe to a system event (like `agent:idle` or `task:completed`) and create
4
+ a WorkItem for yourself or another agent every time it fires. Use this when
5
+ your follow-up should be **reactive to what Rex/the system does**, not tied to
6
+ a wall-clock time.
7
+
8
+ ## When to use this vs `schedule-followup`
9
+
10
+ | Need | Use |
11
+ |------|-----|
12
+ | "Whenever Rex goes idle, check progress" | `watch-for-event --event-type agent:idle --filter-session rex-session` |
13
+ | "Alert me when any task completes" | `watch-for-event --event-type task:completed` |
14
+ | "Poll at 9am tomorrow" | `schedule-followup --fire-at ...` (time, not event) |
15
+ | "Check every hour up to 3 times" | `schedule-followup --cron "0 * * * *" --max-fires 3` |
16
+
17
+ ## Common event types
18
+
19
+ | Event | When it fires |
20
+ |-------|---------------|
21
+ | `agent:idle` | An agent's `workingStatus` flipped to `idle` |
22
+ | `agent:active` | An agent started working |
23
+ | `agent:inactive` | An agent stopped heartbeating |
24
+ | `task:completed` | A task was marked done |
25
+ | `task:failed` | A task failed |
26
+
27
+ See `types/event-bus.types.ts` for the authoritative list.
28
+
29
+ ## Filtering
30
+
31
+ By default the watcher fires on **every** occurrence of the event type. Narrow
32
+ it with `--filter-session` (most common) to only fire for one specific agent,
33
+ or `--filter-json` for arbitrary payload matching.
34
+
35
+ ```bash
36
+ # Only when THIS agent goes idle
37
+ bash execute.sh --event-type agent:idle \
38
+ --filter-session crewly-marketing-rex-member-1 \
39
+ --title "Rex idle — verify Rex task"
40
+
41
+ # Match a richer filter shape
42
+ bash execute.sh --event-type task:completed \
43
+ --filter-json '{"missionId":"q2-growth"}' \
44
+ --title "Q2 mission task landed"
45
+ ```
46
+
47
+ ## Key invariants
48
+
49
+ - Every watcher is **team-scoped**, listed under `list-my-followups`,
50
+ cancellable by `name` via `cancel-followup`.
51
+ - **No implicit upper bound on fires**: if you set `--max-fires`, great.
52
+ Otherwise rely on `--max-idle-fires` (default 3) to auto-cancel once the
53
+ watcher stops producing useful work.
54
+ - A watcher that fires 20× in a minute because an agent is flapping is YOUR
55
+ responsibility to handle (either narrower filter, or cap with max-fires).
56
+
57
+ ## Cancelling
58
+
59
+ - Explicit: `cancel-followup --id <trigger-id>` or `--name <name>`
60
+ - Automatic: `maxFires` exhausts, or `maxIdleFires` trips.
@@ -0,0 +1,177 @@
1
+ #!/bin/bash
2
+ # Subscribe to an EventBus event (e.g. `agent:idle`, `task:completed`) and
3
+ # create a WorkItem for you or another agent when it fires.
4
+ #
5
+ # This is the "event-based follow-up" pattern: you commit to reacting to a
6
+ # specific signal, rather than polling on a timer. Most common use is "when
7
+ # Rex stops working, check whether the task is done and if not, re-prompt".
8
+ #
9
+ # Known event types (see types/event-bus.types.ts for the authoritative list):
10
+ # agent:idle — an agent entered idle state
11
+ # agent:active — an agent started working
12
+ # agent:inactive — an agent was marked inactive (stopped heartbeating)
13
+ # task:completed — a task was marked done
14
+ # task:failed — a task failed
15
+ #
16
+ # Supports CLI flags (preferred), legacy JSON, stdin pipe, or @filepath.
17
+ set -euo pipefail
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
20
+
21
+ CREWLY_HOME="${HOME}/.crewly"
22
+
23
+ print_usage() {
24
+ cat <<'EOF_USAGE'
25
+ Usage:
26
+ # Watch Rex for idle and route a follow-up WorkItem back to yourself
27
+ bash execute.sh --event-type agent:idle \
28
+ --filter-session crewly-marketing-rex-member-1 \
29
+ --title "Rex went idle — check task status"
30
+
31
+ # Watch for any task:completed event, fire-and-done after 1 hit
32
+ bash execute.sh --event-type task:completed --max-fires 1 --title "Task done — ack"
33
+
34
+ Options:
35
+ --event-type Event type to watch (required). Examples: agent:idle, task:completed, task:failed.
36
+ --filter-session Only fire when the event's sessionName matches this value
37
+ --filter-json Raw JSON filter object (advanced; merged with --filter-session)
38
+ --target Session that should receive the generated WorkItem (default: yourself)
39
+ --title | -T Title for the generated WorkItem (required)
40
+ --description Longer description / context
41
+ --name Stable name for cancel/dedup. Auto-generated if absent.
42
+ --max-fires Stop after N fires (default: unlimited — use --max-idle-fires as safety)
43
+ --max-idle-fires Auto-cancel after N consecutive unproductive fires (default: 3)
44
+ --json | -j Raw JSON payload (legacy)
45
+ --help | -h Show this help
46
+ EOF_USAGE
47
+ }
48
+
49
+ resolve_team_id() {
50
+ local session="${1:-${CREWLY_SESSION_NAME:-}}"
51
+ [ -z "$session" ] && return 1
52
+ local teams_dir="${CREWLY_HOME}/teams"
53
+ [ ! -d "$teams_dir" ] && return 1
54
+ for config in "$teams_dir"/*/config.json; do
55
+ [ -f "$config" ] || continue
56
+ local found
57
+ found=$(jq -r --arg s "$session" '.members[]? | select(.sessionName == $s) | "found"' "$config" 2>/dev/null | head -1)
58
+ if [ "$found" = "found" ]; then
59
+ basename "$(dirname "$config")"
60
+ return 0
61
+ fi
62
+ done
63
+ return 1
64
+ }
65
+
66
+ INPUT_JSON=""
67
+ EVENT_TYPE=""
68
+ FILTER_SESSION=""
69
+ FILTER_JSON=""
70
+ TARGET=""
71
+ TITLE=""
72
+ DESCRIPTION=""
73
+ NAME=""
74
+ MAX_FIRES=""
75
+ MAX_IDLE_FIRES="3"
76
+
77
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
78
+ INPUT_JSON="$1"
79
+ shift || true
80
+ fi
81
+
82
+ while [[ $# -gt 0 ]]; do
83
+ case "$1" in
84
+ --event-type) EVENT_TYPE="$2"; shift 2 ;;
85
+ --filter-session) FILTER_SESSION="$2"; shift 2 ;;
86
+ --filter-json) FILTER_JSON="$2"; shift 2 ;;
87
+ --target) TARGET="$2"; shift 2 ;;
88
+ --title|-T) TITLE="$2"; shift 2 ;;
89
+ --description) DESCRIPTION="$2"; shift 2 ;;
90
+ --name) NAME="$2"; shift 2 ;;
91
+ --max-fires) MAX_FIRES="$2"; shift 2 ;;
92
+ --max-idle-fires) MAX_IDLE_FIRES="$2"; shift 2 ;;
93
+ --json|-j) INPUT_JSON="$2"; shift 2 ;;
94
+ --help|-h) print_usage; exit 0 ;;
95
+ --) shift; break ;;
96
+ *)
97
+ if [[ -z "$INPUT_JSON" && ${1:0:1} == '{' ]]; then
98
+ INPUT_JSON="$1"; shift
99
+ else
100
+ echo '{"error":"Unknown argument: '"$1"'"}' >&2
101
+ exit 1
102
+ fi
103
+ ;;
104
+ esac
105
+ done
106
+
107
+ if [ -n "$INPUT_JSON" ]; then
108
+ EVENT_TYPE=$(printf '%s' "$INPUT_JSON" | jq -r '.eventType // empty')
109
+ FILTER_SESSION=$(printf '%s' "$INPUT_JSON" | jq -r '.filterSession // empty')
110
+ FILTER_JSON=$(printf '%s' "$INPUT_JSON" | jq -rc '.filter // empty')
111
+ TARGET=$(printf '%s' "$INPUT_JSON" | jq -r '.target // empty')
112
+ TITLE=$(printf '%s' "$INPUT_JSON" | jq -r '.title // empty')
113
+ DESCRIPTION=$(printf '%s' "$INPUT_JSON" | jq -r '.description // empty')
114
+ NAME=$(printf '%s' "$INPUT_JSON" | jq -r '.name // empty')
115
+ MAX_FIRES=$(printf '%s' "$INPUT_JSON" | jq -r '.maxFires // empty')
116
+ MAX_IDLE_FIRES=$(printf '%s' "$INPUT_JSON" | jq -r '.maxIdleFires // "3"')
117
+ fi
118
+
119
+ [ -z "$EVENT_TYPE" ] && { echo '{"error":"--event-type is required"}' >&2; exit 1; }
120
+ [ -z "$TITLE" ] && { echo '{"error":"--title is required"}' >&2; exit 1; }
121
+
122
+ [ -z "$TARGET" ] && TARGET="${CREWLY_SESSION_NAME:-}"
123
+ [ -z "$TARGET" ] && { echo '{"error":"No --target and CREWLY_SESSION_NAME is unset"}' >&2; exit 1; }
124
+
125
+ TEAM_ID=$(resolve_team_id || true)
126
+
127
+ if [ -z "$NAME" ]; then
128
+ HASH=$(printf '%s|%s|%s' "$EVENT_TYPE" "$TARGET" "$TITLE" | shasum -a 1 | awk '{print substr($1,1,8)}')
129
+ NAME="watch:${HASH}"
130
+ fi
131
+
132
+ # Build the filter object. --filter-session is sugar for {sessionName: "..."}.
133
+ FILTER_OBJ=""
134
+ if [ -n "$FILTER_JSON" ]; then
135
+ FILTER_OBJ="$FILTER_JSON"
136
+ fi
137
+ if [ -n "$FILTER_SESSION" ]; then
138
+ if [ -n "$FILTER_OBJ" ]; then
139
+ FILTER_OBJ=$(printf '%s' "$FILTER_OBJ" | jq --arg s "$FILTER_SESSION" '. + {sessionName:$s}')
140
+ else
141
+ FILTER_OBJ=$(jq -n --arg s "$FILTER_SESSION" '{sessionName:$s}')
142
+ fi
143
+ fi
144
+
145
+ if [ -n "$FILTER_OBJ" ]; then
146
+ CONFIG_JSON=$(jq -n --arg et "$EVENT_TYPE" --argjson f "$FILTER_OBJ" '{type:"signal", eventType:$et, filter:$f}')
147
+ else
148
+ CONFIG_JSON=$(jq -n --arg et "$EVENT_TYPE" '{type:"signal", eventType:$et}')
149
+ fi
150
+
151
+ WI_JSON=$(jq -n \
152
+ --arg type "delegate" \
153
+ --arg owner "team_lead" \
154
+ --arg target "$TARGET" \
155
+ --arg title "$TITLE" \
156
+ --arg description "$DESCRIPTION" \
157
+ '{type:$type, owner:$owner, target:$target, title:$title}
158
+ + (if $description != "" then {description:$description} else {} end)')
159
+
160
+ ACTION_JSON=$(jq -n --argjson wi "$WI_JSON" '{createWorkItem: $wi}')
161
+
162
+ BODY=$(jq -n \
163
+ --arg type "signal" \
164
+ --argjson config "$CONFIG_JSON" \
165
+ --argjson action "$ACTION_JSON" \
166
+ --arg createdBy "system" \
167
+ --arg name "$NAME" \
168
+ --arg teamId "$TEAM_ID" \
169
+ --arg maxFires "$MAX_FIRES" \
170
+ --arg maxIdle "$MAX_IDLE_FIRES" \
171
+ '{type:$type, config:$config, action:$action, createdBy:$createdBy, name:$name}
172
+ + (if $teamId != "" then {teamId:$teamId} else {} end)
173
+ + (if $maxFires != "" then {maxFires:($maxFires|tonumber)} else {} end)
174
+ + (if $maxIdle != "" then {maxIdleFires:($maxIdle|tonumber)} else {} end)')
175
+
176
+ RESP=$(api_call POST "/triggers" "$BODY")
177
+ echo "$RESP"
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # Validation tests for watch-for-event skill.
3
+ set -eo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ PASS=0
7
+ FAIL=0
8
+
9
+ assert_contains() {
10
+ local test_name="$1" needle="$2" haystack="$3"
11
+ if printf '%s' "$haystack" | grep -q -- "$needle"; then
12
+ PASS=$((PASS + 1))
13
+ echo " ✓ ${test_name}"
14
+ else
15
+ FAIL=$((FAIL + 1))
16
+ echo " ✗ ${test_name}"
17
+ echo " expected to contain: ${needle}"
18
+ echo " got: ${haystack}"
19
+ fi
20
+ }
21
+
22
+ echo "=== watch-for-event tests ==="
23
+
24
+ echo ""
25
+ echo "--- Required args ---"
26
+
27
+ OUTPUT=$(bash "$SCRIPT_DIR/execute.sh" --title "Check" 2>&1) || true
28
+ assert_contains "Missing --event-type rejected" "event-type is required" "$OUTPUT"
29
+
30
+ OUTPUT=$(bash "$SCRIPT_DIR/execute.sh" --event-type agent:idle 2>&1) || true
31
+ assert_contains "Missing --title rejected" "title is required" "$OUTPUT"
32
+
33
+ echo ""
34
+ echo "--- Help ---"
35
+
36
+ OUTPUT=$(bash "$SCRIPT_DIR/execute.sh" --help 2>&1) || true
37
+ assert_contains "Help lists --event-type" -- "--event-type" "$OUTPUT"
38
+ assert_contains "Help lists --filter-session" -- "--filter-session" "$OUTPUT"
39
+ assert_contains "Help mentions agent:idle example" "agent:idle" "$OUTPUT"
40
+
41
+ echo ""
42
+ echo "=== Results: ${PASS} passed, ${FAIL} failed ==="
43
+ [ $FAIL -eq 0 ]