crewly 1.5.22 → 1.6.1

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 (180) hide show
  1. package/config/roles/orchestrator/fragments/role-boundary.md +4 -1
  2. package/config/roles/orchestrator/prompt.md +219 -25
  3. package/config/roles/orchestrator/soul.md +47 -10
  4. package/config/skills/_common/lib.sh +28 -0
  5. package/config/skills/agent/core/cancel-followup/SKILL.md +38 -0
  6. package/config/skills/agent/core/cancel-followup/execute.sh +92 -0
  7. package/config/skills/agent/core/cancel-followup/execute.test.sh +42 -0
  8. package/config/skills/agent/core/list-my-followups/SKILL.md +36 -0
  9. package/config/skills/agent/core/list-my-followups/execute.sh +74 -0
  10. package/config/skills/agent/core/list-my-followups/execute.test.sh +41 -0
  11. package/config/skills/agent/core/schedule-followup/SKILL.md +53 -0
  12. package/config/skills/agent/core/schedule-followup/execute.sh +176 -0
  13. package/config/skills/agent/core/schedule-followup/execute.test.sh +48 -0
  14. package/config/skills/agent/core/watch-for-event/SKILL.md +60 -0
  15. package/config/skills/agent/core/watch-for-event/execute.sh +158 -0
  16. package/config/skills/agent/core/watch-for-event/execute.test.sh +43 -0
  17. package/config/skills/orchestrator/credential-manager/SKILL.md +218 -0
  18. package/config/skills/orchestrator/credential-manager/execute.sh +166 -0
  19. package/config/skills/orchestrator/credential-manager/execute.test.sh +88 -0
  20. package/dist/backend/backend/src/config/oauth.config.d.ts +33 -0
  21. package/dist/backend/backend/src/config/oauth.config.d.ts.map +1 -0
  22. package/dist/backend/backend/src/config/oauth.config.js +45 -0
  23. package/dist/backend/backend/src/config/oauth.config.js.map +1 -0
  24. package/dist/backend/backend/src/controllers/credentials/credentials.controller.d.ts +54 -0
  25. package/dist/backend/backend/src/controllers/credentials/credentials.controller.d.ts.map +1 -0
  26. package/dist/backend/backend/src/controllers/credentials/credentials.controller.js +228 -0
  27. package/dist/backend/backend/src/controllers/credentials/credentials.controller.js.map +1 -0
  28. package/dist/backend/backend/src/controllers/credentials/credentials.routes.d.ts +26 -0
  29. package/dist/backend/backend/src/controllers/credentials/credentials.routes.d.ts.map +1 -0
  30. package/dist/backend/backend/src/controllers/credentials/credentials.routes.js +41 -0
  31. package/dist/backend/backend/src/controllers/credentials/credentials.routes.js.map +1 -0
  32. package/dist/backend/backend/src/controllers/credentials/google-oauth.controller.d.ts +40 -0
  33. package/dist/backend/backend/src/controllers/credentials/google-oauth.controller.d.ts.map +1 -0
  34. package/dist/backend/backend/src/controllers/credentials/google-oauth.controller.js +162 -0
  35. package/dist/backend/backend/src/controllers/credentials/google-oauth.controller.js.map +1 -0
  36. package/dist/backend/backend/src/controllers/skill/skill.controller.d.ts.map +1 -1
  37. package/dist/backend/backend/src/controllers/skill/skill.controller.js +1 -0
  38. package/dist/backend/backend/src/controllers/skill/skill.controller.js.map +1 -1
  39. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +23 -14
  40. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
  41. package/dist/backend/backend/src/index.d.ts.map +1 -1
  42. package/dist/backend/backend/src/index.js +23 -4
  43. package/dist/backend/backend/src/index.js.map +1 -1
  44. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  45. package/dist/backend/backend/src/routes/api.routes.js +3 -0
  46. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  47. package/dist/backend/backend/src/scripts/backfill-mission-priority.d.ts +3 -1
  48. package/dist/backend/backend/src/scripts/backfill-mission-priority.d.ts.map +1 -1
  49. package/dist/backend/backend/src/scripts/backfill-mission-priority.js +16 -4
  50. package/dist/backend/backend/src/scripts/backfill-mission-priority.js.map +1 -1
  51. package/dist/backend/backend/src/services/ai/prompt-modules/role-boundary.module.d.ts.map +1 -1
  52. package/dist/backend/backend/src/services/ai/prompt-modules/role-boundary.module.js +4 -1
  53. package/dist/backend/backend/src/services/ai/prompt-modules/role-boundary.module.js.map +1 -1
  54. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  55. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +17 -0
  56. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  57. package/dist/backend/backend/src/services/browser/browser-proxy.service.js +1 -1
  58. package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
  59. package/dist/backend/backend/src/services/credential/credential-store.service.d.ts +161 -0
  60. package/dist/backend/backend/src/services/credential/credential-store.service.d.ts.map +1 -0
  61. package/dist/backend/backend/src/services/credential/credential-store.service.js +298 -0
  62. package/dist/backend/backend/src/services/credential/credential-store.service.js.map +1 -0
  63. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts +105 -0
  64. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -0
  65. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +272 -0
  66. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -0
  67. package/dist/backend/backend/src/services/mcp-server.d.ts +46 -2
  68. package/dist/backend/backend/src/services/mcp-server.d.ts.map +1 -1
  69. package/dist/backend/backend/src/services/mcp-server.js +216 -211
  70. package/dist/backend/backend/src/services/mcp-server.js.map +1 -1
  71. package/dist/backend/backend/src/services/mcp-tool-definitions.d.ts +254 -0
  72. package/dist/backend/backend/src/services/mcp-tool-definitions.d.ts.map +1 -0
  73. package/dist/backend/backend/src/services/mcp-tool-definitions.js +285 -0
  74. package/dist/backend/backend/src/services/mcp-tool-definitions.js.map +1 -0
  75. package/dist/backend/backend/src/services/project/task.service.d.ts +18 -2
  76. package/dist/backend/backend/src/services/project/task.service.d.ts.map +1 -1
  77. package/dist/backend/backend/src/services/project/task.service.js +74 -53
  78. package/dist/backend/backend/src/services/project/task.service.js.map +1 -1
  79. package/dist/backend/backend/src/services/skill/skill-executor.service.d.ts +41 -0
  80. package/dist/backend/backend/src/services/skill/skill-executor.service.d.ts.map +1 -1
  81. package/dist/backend/backend/src/services/skill/skill-executor.service.js +136 -7
  82. package/dist/backend/backend/src/services/skill/skill-executor.service.js.map +1 -1
  83. package/dist/backend/backend/src/services/skill/skill.service.d.ts.map +1 -1
  84. package/dist/backend/backend/src/services/skill/skill.service.js +1 -0
  85. package/dist/backend/backend/src/services/skill/skill.service.js.map +1 -1
  86. package/dist/backend/backend/src/services/v3/contract-matcher.d.ts +20 -0
  87. package/dist/backend/backend/src/services/v3/contract-matcher.d.ts.map +1 -0
  88. package/dist/backend/backend/src/services/v3/contract-matcher.js +33 -0
  89. package/dist/backend/backend/src/services/v3/contract-matcher.js.map +1 -0
  90. package/dist/backend/backend/src/services/v3/escalation.service.d.ts +20 -1
  91. package/dist/backend/backend/src/services/v3/escalation.service.d.ts.map +1 -1
  92. package/dist/backend/backend/src/services/v3/escalation.service.js +97 -28
  93. package/dist/backend/backend/src/services/v3/escalation.service.js.map +1 -1
  94. package/dist/backend/backend/src/services/v3/service-contract-gate.service.d.ts +6 -4
  95. package/dist/backend/backend/src/services/v3/service-contract-gate.service.d.ts.map +1 -1
  96. package/dist/backend/backend/src/services/v3/service-contract-gate.service.js +18 -28
  97. package/dist/backend/backend/src/services/v3/service-contract-gate.service.js.map +1 -1
  98. package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.d.ts.map +1 -1
  99. package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js +14 -9
  100. package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js.map +1 -1
  101. package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts +34 -1
  102. package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts.map +1 -1
  103. package/dist/backend/backend/src/services/v3/trigger-engine.service.js +115 -5
  104. package/dist/backend/backend/src/services/v3/trigger-engine.service.js.map +1 -1
  105. package/dist/backend/backend/src/types/credential.types.d.ts +185 -0
  106. package/dist/backend/backend/src/types/credential.types.d.ts.map +1 -0
  107. package/dist/backend/backend/src/types/credential.types.js +76 -0
  108. package/dist/backend/backend/src/types/credential.types.js.map +1 -0
  109. package/dist/backend/backend/src/types/skill.types.d.ts +9 -0
  110. package/dist/backend/backend/src/types/skill.types.d.ts.map +1 -1
  111. package/dist/backend/backend/src/types/skill.types.js.map +1 -1
  112. package/dist/backend/backend/src/utils/encryption.utils.d.ts +57 -0
  113. package/dist/backend/backend/src/utils/encryption.utils.d.ts.map +1 -0
  114. package/dist/backend/backend/src/utils/encryption.utils.js +162 -0
  115. package/dist/backend/backend/src/utils/encryption.utils.js.map +1 -0
  116. package/dist/backend/backend/src/utils/google-userinfo.utils.d.ts +41 -0
  117. package/dist/backend/backend/src/utils/google-userinfo.utils.d.ts.map +1 -0
  118. package/dist/backend/backend/src/utils/google-userinfo.utils.js +44 -0
  119. package/dist/backend/backend/src/utils/google-userinfo.utils.js.map +1 -0
  120. package/dist/cli/backend/src/config/oauth.config.d.ts +33 -0
  121. package/dist/cli/backend/src/config/oauth.config.d.ts.map +1 -0
  122. package/dist/cli/backend/src/config/oauth.config.js +45 -0
  123. package/dist/cli/backend/src/config/oauth.config.js.map +1 -0
  124. package/dist/cli/backend/src/services/credential/credential-store.service.d.ts +161 -0
  125. package/dist/cli/backend/src/services/credential/credential-store.service.d.ts.map +1 -0
  126. package/dist/cli/backend/src/services/credential/credential-store.service.js +298 -0
  127. package/dist/cli/backend/src/services/credential/credential-store.service.js.map +1 -0
  128. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts +105 -0
  129. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -0
  130. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +272 -0
  131. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -0
  132. package/dist/cli/backend/src/services/mcp-server.d.ts +46 -2
  133. package/dist/cli/backend/src/services/mcp-server.d.ts.map +1 -1
  134. package/dist/cli/backend/src/services/mcp-server.js +216 -211
  135. package/dist/cli/backend/src/services/mcp-server.js.map +1 -1
  136. package/dist/cli/backend/src/services/mcp-tool-definitions.d.ts +254 -0
  137. package/dist/cli/backend/src/services/mcp-tool-definitions.d.ts.map +1 -0
  138. package/dist/cli/backend/src/services/mcp-tool-definitions.js +285 -0
  139. package/dist/cli/backend/src/services/mcp-tool-definitions.js.map +1 -0
  140. package/dist/cli/backend/src/services/settings/settings.service.d.ts +168 -0
  141. package/dist/cli/backend/src/services/settings/settings.service.d.ts.map +1 -0
  142. package/dist/cli/backend/src/services/settings/settings.service.js +312 -0
  143. package/dist/cli/backend/src/services/settings/settings.service.js.map +1 -0
  144. package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts +177 -0
  145. package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts.map +1 -0
  146. package/dist/cli/backend/src/services/skill/skill-executor.service.js +624 -0
  147. package/dist/cli/backend/src/services/skill/skill-executor.service.js.map +1 -0
  148. package/dist/cli/backend/src/services/skill/skill.service.d.ts +273 -0
  149. package/dist/cli/backend/src/services/skill/skill.service.d.ts.map +1 -0
  150. package/dist/cli/backend/src/services/skill/skill.service.js +655 -0
  151. package/dist/cli/backend/src/services/skill/skill.service.js.map +1 -0
  152. package/dist/cli/backend/src/types/credential.types.d.ts +185 -0
  153. package/dist/cli/backend/src/types/credential.types.d.ts.map +1 -0
  154. package/dist/cli/backend/src/types/credential.types.js +76 -0
  155. package/dist/cli/backend/src/types/credential.types.js.map +1 -0
  156. package/dist/cli/backend/src/types/skill.types.d.ts +9 -0
  157. package/dist/cli/backend/src/types/skill.types.d.ts.map +1 -1
  158. package/dist/cli/backend/src/types/skill.types.js.map +1 -1
  159. package/dist/cli/backend/src/utils/encryption.utils.d.ts +57 -0
  160. package/dist/cli/backend/src/utils/encryption.utils.d.ts.map +1 -0
  161. package/dist/cli/backend/src/utils/encryption.utils.js +162 -0
  162. package/dist/cli/backend/src/utils/encryption.utils.js.map +1 -0
  163. package/dist/cli/backend/src/utils/google-userinfo.utils.d.ts +41 -0
  164. package/dist/cli/backend/src/utils/google-userinfo.utils.d.ts.map +1 -0
  165. package/dist/cli/backend/src/utils/google-userinfo.utils.js +44 -0
  166. package/dist/cli/backend/src/utils/google-userinfo.utils.js.map +1 -0
  167. package/dist/cli/backend/src/utils/skill-md-parser.d.ts +38 -0
  168. package/dist/cli/backend/src/utils/skill-md-parser.d.ts.map +1 -0
  169. package/dist/cli/backend/src/utils/skill-md-parser.js +47 -0
  170. package/dist/cli/backend/src/utils/skill-md-parser.js.map +1 -0
  171. package/frontend/dist/assets/{index-dc92ab64.css → index-6aaa0630.css} +1 -1
  172. package/frontend/dist/assets/{index-76d76633.js → index-70356616.js} +334 -328
  173. package/frontend/dist/index.html +2 -2
  174. package/package.json +1 -1
  175. package/config/experts/empathetic-resolver/expert.json +0 -11
  176. package/config/experts/empathetic-resolver.md +0 -32
  177. package/config/experts/pragmatic-architect/expert.json +0 -11
  178. package/config/experts/pragmatic-architect.md +0 -32
  179. package/config/experts/viral-alchemist/expert.json +0 -11
  180. package/config/experts/viral-alchemist.md +0 -32
@@ -0,0 +1,74 @@
1
+ #!/bin/bash
2
+ # List all follow-up triggers owned by your team. Useful for stock-taking
3
+ # before adding a new watcher (so you don't stack duplicates), or for
4
+ # auditing why something keeps firing.
5
+ #
6
+ # Supports CLI flags or legacy JSON.
7
+ set -euo pipefail
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
10
+
11
+ print_usage() {
12
+ cat <<'EOF_USAGE'
13
+ Usage:
14
+ bash execute.sh # All triggers owned by your team
15
+ bash execute.sh --status active # Only active ones
16
+ bash execute.sh --name-prefix followup: # Filter by name prefix
17
+
18
+ Options:
19
+ --status One of: active | paused | exhausted | cancelled (optional)
20
+ --name-prefix Only return triggers whose name starts with this string
21
+ --json -j Raw JSON payload (legacy)
22
+ --help -h Show this help
23
+
24
+ Output: JSON object { success, count, data: [Trigger, ...] }
25
+ EOF_USAGE
26
+ }
27
+
28
+ INPUT_JSON=""
29
+ STATUS_FILTER=""
30
+ NAME_PREFIX=""
31
+
32
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
33
+ INPUT_JSON="$1"
34
+ shift || true
35
+ fi
36
+
37
+ while [[ $# -gt 0 ]]; do
38
+ case "$1" in
39
+ --status) STATUS_FILTER="$2"; shift 2 ;;
40
+ --name-prefix) NAME_PREFIX="$2"; shift 2 ;;
41
+ --json|-j) INPUT_JSON="$2"; shift 2 ;;
42
+ --help|-h) print_usage; exit 0 ;;
43
+ --) shift; break ;;
44
+ *)
45
+ if [[ -z "$INPUT_JSON" && ${1:0:1} == '{' ]]; then
46
+ INPUT_JSON="$1"; shift
47
+ else
48
+ echo '{"error":"Unknown argument: '"$1"'"}' >&2
49
+ exit 1
50
+ fi
51
+ ;;
52
+ esac
53
+ done
54
+
55
+ if [ -n "$INPUT_JSON" ]; then
56
+ STATUS_FILTER=$(printf '%s' "$INPUT_JSON" | jq -r '.status // empty')
57
+ NAME_PREFIX=$(printf '%s' "$INPUT_JSON" | jq -r '.namePrefix // empty')
58
+ fi
59
+
60
+ TEAM_ID=$(resolve_team_id || true)
61
+ [ -z "$TEAM_ID" ] && { echo '{"error":"Cannot resolve owning team from CREWLY_SESSION_NAME"}' >&2; exit 1; }
62
+
63
+ LIST_RESP=$(api_call GET "/triggers" "")
64
+ FILTERED=$(printf '%s' "$LIST_RESP" | jq \
65
+ --arg team "$TEAM_ID" \
66
+ --arg status "$STATUS_FILTER" \
67
+ --arg prefix "$NAME_PREFIX" \
68
+ '(.data // [])
69
+ | map(select(.teamId == $team))
70
+ | (if $status != "" then map(select(.status == $status)) else . end)
71
+ | (if $prefix != "" then map(select(.name != null and (.name | startswith($prefix)))) else . end)')
72
+
73
+ COUNT=$(printf '%s' "$FILTERED" | jq 'length')
74
+ printf '%s' "$FILTERED" | jq --arg c "$COUNT" '{success:true, count:($c|tonumber), data:.}'
@@ -0,0 +1,41 @@
1
+ #!/bin/bash
2
+ # Validation tests for list-my-followups 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 "=== list-my-followups tests ==="
23
+
24
+ echo ""
25
+ echo "--- Help ---"
26
+
27
+ OUTPUT=$(bash "$SCRIPT_DIR/execute.sh" --help 2>&1) || true
28
+ assert_contains "Help mentions --status" -- "--status" "$OUTPUT"
29
+ assert_contains "Help mentions --name-prefix" -- "--name-prefix" "$OUTPUT"
30
+
31
+ echo ""
32
+ echo "--- Team resolution ---"
33
+
34
+ # Without CREWLY_SESSION_NAME, the skill must refuse rather than returning an
35
+ # all-teams dump. Clear env and run.
36
+ OUTPUT=$(env -u CREWLY_SESSION_NAME bash "$SCRIPT_DIR/execute.sh" 2>&1) || true
37
+ assert_contains "No session → team-resolve error" "Cannot resolve owning team" "$OUTPUT"
38
+
39
+ echo ""
40
+ echo "=== Results: ${PASS} passed, ${FAIL} failed ==="
41
+ [ $FAIL -eq 0 ]
@@ -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,176 @@
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
+ print_usage() {
21
+ cat <<'EOF_USAGE'
22
+ Usage:
23
+ # Fire once, 30 minutes from now, target self
24
+ bash execute.sh --in-minutes 30 --title "Check Rex response"
25
+
26
+ # Fire once at a specific absolute time (ISO8601), target another agent
27
+ bash execute.sh --fire-at 2026-04-24T09:00:00Z --target crewly-marketing-ella-member-1 --title "Re-prompt Rex"
28
+
29
+ # Recurring with a hard cap — 3 tries, 1h apart
30
+ bash execute.sh --cron "0 * * * *" --max-fires 3 --title "Hourly Rex check"
31
+
32
+ # Legacy JSON
33
+ bash execute.sh '{"inMinutes":30,"title":"Check Rex","target":"ella"}'
34
+
35
+ Options:
36
+ --in-minutes | -m Fire once N minutes from now (mutually exclusive with --fire-at/--cron)
37
+ --fire-at Fire once at ISO8601 datetime (e.g. 2026-04-24T09:00:00Z)
38
+ --cron Cron expression for recurring fires (UTC by default)
39
+ --timezone IANA timezone for cron (default: UTC)
40
+ --target Agent session to receive the follow-up WorkItem (default: yourself)
41
+ --title | -T Title for the generated WorkItem (required)
42
+ --description Longer description / context
43
+ --name Stable name (used for cancel / dedup). Auto-generated if absent.
44
+ --max-fires Stop after N fires (default: 1 for --in-minutes/--fire-at, unlimited for --cron)
45
+ --max-idle-fires Auto-cancel after N consecutive fires that produced no work (default: 3)
46
+ --json | -j Raw JSON payload (same as legacy)
47
+ --help | -h Show this help
48
+ EOF_USAGE
49
+ }
50
+
51
+ INPUT_JSON=""
52
+ IN_MINUTES=""
53
+ FIRE_AT=""
54
+ CRON_EXPR=""
55
+ TIMEZONE="UTC"
56
+ TARGET=""
57
+ TITLE=""
58
+ DESCRIPTION=""
59
+ NAME=""
60
+ MAX_FIRES=""
61
+ MAX_IDLE_FIRES="3"
62
+
63
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
64
+ INPUT_JSON="$1"
65
+ shift || true
66
+ fi
67
+
68
+ while [[ $# -gt 0 ]]; do
69
+ case "$1" in
70
+ --in-minutes|-m) IN_MINUTES="$2"; shift 2 ;;
71
+ --fire-at) FIRE_AT="$2"; shift 2 ;;
72
+ --cron) CRON_EXPR="$2"; shift 2 ;;
73
+ --timezone) TIMEZONE="$2"; shift 2 ;;
74
+ --target) TARGET="$2"; shift 2 ;;
75
+ --title|-T) TITLE="$2"; shift 2 ;;
76
+ --description) DESCRIPTION="$2"; shift 2 ;;
77
+ --name) NAME="$2"; shift 2 ;;
78
+ --max-fires) MAX_FIRES="$2"; shift 2 ;;
79
+ --max-idle-fires) MAX_IDLE_FIRES="$2"; shift 2 ;;
80
+ --json|-j) INPUT_JSON="$2"; shift 2 ;;
81
+ --help|-h) print_usage; exit 0 ;;
82
+ --) shift; break ;;
83
+ *)
84
+ if [[ -z "$INPUT_JSON" && ${1:0:1} == '{' ]]; then
85
+ INPUT_JSON="$1"; shift
86
+ else
87
+ echo '{"error":"Unknown argument: '"$1"'"}' >&2
88
+ exit 1
89
+ fi
90
+ ;;
91
+ esac
92
+ done
93
+
94
+ # Legacy JSON → populate vars
95
+ if [ -n "$INPUT_JSON" ]; then
96
+ IN_MINUTES=$(printf '%s' "$INPUT_JSON" | jq -r '.inMinutes // empty')
97
+ FIRE_AT=$(printf '%s' "$INPUT_JSON" | jq -r '.fireAt // empty')
98
+ CRON_EXPR=$(printf '%s' "$INPUT_JSON" | jq -r '.cron // empty')
99
+ TIMEZONE=$(printf '%s' "$INPUT_JSON" | jq -r '.timezone // "UTC"')
100
+ TARGET=$(printf '%s' "$INPUT_JSON" | jq -r '.target // empty')
101
+ TITLE=$(printf '%s' "$INPUT_JSON" | jq -r '.title // empty')
102
+ DESCRIPTION=$(printf '%s' "$INPUT_JSON" | jq -r '.description // empty')
103
+ NAME=$(printf '%s' "$INPUT_JSON" | jq -r '.name // empty')
104
+ MAX_FIRES=$(printf '%s' "$INPUT_JSON" | jq -r '.maxFires // empty')
105
+ MAX_IDLE_FIRES=$(printf '%s' "$INPUT_JSON" | jq -r '.maxIdleFires // "3"')
106
+ fi
107
+
108
+ # Validation
109
+ [ -z "$TITLE" ] && { echo '{"error":"--title is required"}' >&2; exit 1; }
110
+
111
+ TIMING_COUNT=0
112
+ [ -n "$IN_MINUTES" ] && TIMING_COUNT=$((TIMING_COUNT + 1))
113
+ [ -n "$FIRE_AT" ] && TIMING_COUNT=$((TIMING_COUNT + 1))
114
+ [ -n "$CRON_EXPR" ] && TIMING_COUNT=$((TIMING_COUNT + 1))
115
+ if [ "$TIMING_COUNT" -ne 1 ]; then
116
+ echo '{"error":"Exactly one of --in-minutes, --fire-at, or --cron must be provided"}' >&2
117
+ exit 1
118
+ fi
119
+
120
+ # Default target = self
121
+ [ -z "$TARGET" ] && TARGET="${CREWLY_SESSION_NAME:-}"
122
+ [ -z "$TARGET" ] && { echo '{"error":"No --target and CREWLY_SESSION_NAME is unset"}' >&2; exit 1; }
123
+
124
+ # Resolve owning team (for team-scoped listing / cancel)
125
+ TEAM_ID=$(resolve_team_id || true)
126
+
127
+ # Auto-generate name if caller didn't supply one (stable per target+title)
128
+ if [ -z "$NAME" ]; then
129
+ HASH=$(printf '%s|%s' "$TARGET" "$TITLE" | shasum -a 1 | awk '{print substr($1,1,8)}')
130
+ NAME="followup:${HASH}"
131
+ fi
132
+
133
+ # Build TimeTriggerConfig
134
+ CONFIG_JSON=""
135
+ if [ -n "$IN_MINUTES" ]; then
136
+ DELAY_MS=$((IN_MINUTES * 60 * 1000))
137
+ CONFIG_JSON=$(jq -n --argjson d "$DELAY_MS" '{type:"time", delayMs:$d}')
138
+ # Sensible default for one-shot
139
+ [ -z "$MAX_FIRES" ] && MAX_FIRES="1"
140
+ elif [ -n "$FIRE_AT" ]; then
141
+ CONFIG_JSON=$(jq -n --arg f "$FIRE_AT" '{type:"time", fireAt:$f}')
142
+ [ -z "$MAX_FIRES" ] && MAX_FIRES="1"
143
+ else
144
+ CONFIG_JSON=$(jq -n --arg c "$CRON_EXPR" --arg tz "$TIMEZONE" '{type:"time", cronExpression:$c, timezone:$tz}')
145
+ # Recurring: no implicit maxFires cap (caller should set one for followup hygiene)
146
+ fi
147
+
148
+ # Build TriggerAction.createWorkItem
149
+ WI_JSON=$(jq -n \
150
+ --arg type "delegate" \
151
+ --arg owner "team_lead" \
152
+ --arg target "$TARGET" \
153
+ --arg title "$TITLE" \
154
+ --arg description "$DESCRIPTION" \
155
+ '{type:$type, owner:$owner, target:$target, title:$title}
156
+ + (if $description != "" then {description:$description} else {} end)')
157
+
158
+ ACTION_JSON=$(jq -n --argjson wi "$WI_JSON" '{createWorkItem: $wi}')
159
+
160
+ # Assemble final CreateTriggerInput
161
+ BODY=$(jq -n \
162
+ --arg type "time" \
163
+ --argjson config "$CONFIG_JSON" \
164
+ --argjson action "$ACTION_JSON" \
165
+ --arg createdBy "system" \
166
+ --arg name "$NAME" \
167
+ --arg teamId "$TEAM_ID" \
168
+ --arg maxFires "$MAX_FIRES" \
169
+ --arg maxIdle "$MAX_IDLE_FIRES" \
170
+ '{type:$type, config:$config, action:$action, createdBy:$createdBy, name:$name}
171
+ + (if $teamId != "" then {teamId:$teamId} else {} end)
172
+ + (if $maxFires != "" then {maxFires:($maxFires|tonumber)} else {} end)
173
+ + (if $maxIdle != "" then {maxIdleFires:($maxIdle|tonumber)} else {} end)')
174
+
175
+ RESP=$(api_call POST "/triggers" "$BODY")
176
+ 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,158 @@
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
+ print_usage() {
22
+ cat <<'EOF_USAGE'
23
+ Usage:
24
+ # Watch Rex for idle and route a follow-up WorkItem back to yourself
25
+ bash execute.sh --event-type agent:idle \
26
+ --filter-session crewly-marketing-rex-member-1 \
27
+ --title "Rex went idle — check task status"
28
+
29
+ # Watch for any task:completed event, fire-and-done after 1 hit
30
+ bash execute.sh --event-type task:completed --max-fires 1 --title "Task done — ack"
31
+
32
+ Options:
33
+ --event-type Event type to watch (required). Examples: agent:idle, task:completed, task:failed.
34
+ --filter-session Only fire when the event's sessionName matches this value
35
+ --filter-json Raw JSON filter object (advanced; merged with --filter-session)
36
+ --target Session that should receive the generated WorkItem (default: yourself)
37
+ --title | -T Title for the generated WorkItem (required)
38
+ --description Longer description / context
39
+ --name Stable name for cancel/dedup. Auto-generated if absent.
40
+ --max-fires Stop after N fires (default: unlimited — use --max-idle-fires as safety)
41
+ --max-idle-fires Auto-cancel after N consecutive unproductive fires (default: 3)
42
+ --json | -j Raw JSON payload (legacy)
43
+ --help | -h Show this help
44
+ EOF_USAGE
45
+ }
46
+
47
+ INPUT_JSON=""
48
+ EVENT_TYPE=""
49
+ FILTER_SESSION=""
50
+ FILTER_JSON=""
51
+ TARGET=""
52
+ TITLE=""
53
+ DESCRIPTION=""
54
+ NAME=""
55
+ MAX_FIRES=""
56
+ MAX_IDLE_FIRES="3"
57
+
58
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
59
+ INPUT_JSON="$1"
60
+ shift || true
61
+ fi
62
+
63
+ while [[ $# -gt 0 ]]; do
64
+ case "$1" in
65
+ --event-type) EVENT_TYPE="$2"; shift 2 ;;
66
+ --filter-session) FILTER_SESSION="$2"; shift 2 ;;
67
+ --filter-json) FILTER_JSON="$2"; shift 2 ;;
68
+ --target) TARGET="$2"; shift 2 ;;
69
+ --title|-T) TITLE="$2"; shift 2 ;;
70
+ --description) DESCRIPTION="$2"; shift 2 ;;
71
+ --name) NAME="$2"; shift 2 ;;
72
+ --max-fires) MAX_FIRES="$2"; shift 2 ;;
73
+ --max-idle-fires) MAX_IDLE_FIRES="$2"; shift 2 ;;
74
+ --json|-j) INPUT_JSON="$2"; shift 2 ;;
75
+ --help|-h) print_usage; exit 0 ;;
76
+ --) shift; break ;;
77
+ *)
78
+ if [[ -z "$INPUT_JSON" && ${1:0:1} == '{' ]]; then
79
+ INPUT_JSON="$1"; shift
80
+ else
81
+ echo '{"error":"Unknown argument: '"$1"'"}' >&2
82
+ exit 1
83
+ fi
84
+ ;;
85
+ esac
86
+ done
87
+
88
+ if [ -n "$INPUT_JSON" ]; then
89
+ EVENT_TYPE=$(printf '%s' "$INPUT_JSON" | jq -r '.eventType // empty')
90
+ FILTER_SESSION=$(printf '%s' "$INPUT_JSON" | jq -r '.filterSession // empty')
91
+ FILTER_JSON=$(printf '%s' "$INPUT_JSON" | jq -rc '.filter // empty')
92
+ TARGET=$(printf '%s' "$INPUT_JSON" | jq -r '.target // empty')
93
+ TITLE=$(printf '%s' "$INPUT_JSON" | jq -r '.title // empty')
94
+ DESCRIPTION=$(printf '%s' "$INPUT_JSON" | jq -r '.description // empty')
95
+ NAME=$(printf '%s' "$INPUT_JSON" | jq -r '.name // empty')
96
+ MAX_FIRES=$(printf '%s' "$INPUT_JSON" | jq -r '.maxFires // empty')
97
+ MAX_IDLE_FIRES=$(printf '%s' "$INPUT_JSON" | jq -r '.maxIdleFires // "3"')
98
+ fi
99
+
100
+ [ -z "$EVENT_TYPE" ] && { echo '{"error":"--event-type is required"}' >&2; exit 1; }
101
+ [ -z "$TITLE" ] && { echo '{"error":"--title is required"}' >&2; exit 1; }
102
+
103
+ [ -z "$TARGET" ] && TARGET="${CREWLY_SESSION_NAME:-}"
104
+ [ -z "$TARGET" ] && { echo '{"error":"No --target and CREWLY_SESSION_NAME is unset"}' >&2; exit 1; }
105
+
106
+ TEAM_ID=$(resolve_team_id || true)
107
+
108
+ if [ -z "$NAME" ]; then
109
+ HASH=$(printf '%s|%s|%s' "$EVENT_TYPE" "$TARGET" "$TITLE" | shasum -a 1 | awk '{print substr($1,1,8)}')
110
+ NAME="watch:${HASH}"
111
+ fi
112
+
113
+ # Build the filter object. --filter-session is sugar for {sessionName: "..."}.
114
+ FILTER_OBJ=""
115
+ if [ -n "$FILTER_JSON" ]; then
116
+ FILTER_OBJ="$FILTER_JSON"
117
+ fi
118
+ if [ -n "$FILTER_SESSION" ]; then
119
+ if [ -n "$FILTER_OBJ" ]; then
120
+ FILTER_OBJ=$(printf '%s' "$FILTER_OBJ" | jq --arg s "$FILTER_SESSION" '. + {sessionName:$s}')
121
+ else
122
+ FILTER_OBJ=$(jq -n --arg s "$FILTER_SESSION" '{sessionName:$s}')
123
+ fi
124
+ fi
125
+
126
+ if [ -n "$FILTER_OBJ" ]; then
127
+ CONFIG_JSON=$(jq -n --arg et "$EVENT_TYPE" --argjson f "$FILTER_OBJ" '{type:"signal", eventType:$et, filter:$f}')
128
+ else
129
+ CONFIG_JSON=$(jq -n --arg et "$EVENT_TYPE" '{type:"signal", eventType:$et}')
130
+ fi
131
+
132
+ WI_JSON=$(jq -n \
133
+ --arg type "delegate" \
134
+ --arg owner "team_lead" \
135
+ --arg target "$TARGET" \
136
+ --arg title "$TITLE" \
137
+ --arg description "$DESCRIPTION" \
138
+ '{type:$type, owner:$owner, target:$target, title:$title}
139
+ + (if $description != "" then {description:$description} else {} end)')
140
+
141
+ ACTION_JSON=$(jq -n --argjson wi "$WI_JSON" '{createWorkItem: $wi}')
142
+
143
+ BODY=$(jq -n \
144
+ --arg type "signal" \
145
+ --argjson config "$CONFIG_JSON" \
146
+ --argjson action "$ACTION_JSON" \
147
+ --arg createdBy "system" \
148
+ --arg name "$NAME" \
149
+ --arg teamId "$TEAM_ID" \
150
+ --arg maxFires "$MAX_FIRES" \
151
+ --arg maxIdle "$MAX_IDLE_FIRES" \
152
+ '{type:$type, config:$config, action:$action, createdBy:$createdBy, name:$name}
153
+ + (if $teamId != "" then {teamId:$teamId} else {} end)
154
+ + (if $maxFires != "" then {maxFires:($maxFires|tonumber)} else {} end)
155
+ + (if $maxIdle != "" then {maxIdleFires:($maxIdle|tonumber)} else {} end)')
156
+
157
+ RESP=$(api_call POST "/triggers" "$BODY")
158
+ 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 ]