crewly 1.5.21 → 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.
- package/config/roles/orchestrator/prompt.md +182 -25
- package/config/skills/agent/core/cancel-followup/SKILL.md +38 -0
- package/config/skills/agent/core/cancel-followup/execute.sh +111 -0
- package/config/skills/agent/core/cancel-followup/execute.test.sh +42 -0
- package/config/skills/agent/core/list-my-followups/SKILL.md +36 -0
- package/config/skills/agent/core/list-my-followups/execute.sh +93 -0
- package/config/skills/agent/core/list-my-followups/execute.test.sh +41 -0
- package/config/skills/agent/core/schedule-followup/SKILL.md +53 -0
- package/config/skills/agent/core/schedule-followup/execute.sh +195 -0
- package/config/skills/agent/core/schedule-followup/execute.test.sh +48 -0
- package/config/skills/agent/core/watch-for-event/SKILL.md +60 -0
- package/config/skills/agent/core/watch-for-event/execute.sh +177 -0
- package/config/skills/agent/core/watch-for-event/execute.test.sh +43 -0
- package/config/skills/orchestrator/credential-manager/SKILL.md +218 -0
- package/config/skills/orchestrator/credential-manager/execute.sh +166 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.controller.d.ts +80 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.controller.js +365 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.routes.d.ts +26 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.routes.js +40 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.routes.js.map +1 -0
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +23 -14
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
- package/dist/backend/backend/src/scripts/backfill-mission-priority.d.ts +3 -1
- package/dist/backend/backend/src/scripts/backfill-mission-priority.d.ts.map +1 -1
- package/dist/backend/backend/src/scripts/backfill-mission-priority.js +16 -4
- package/dist/backend/backend/src/scripts/backfill-mission-priority.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js +22 -2
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
- package/dist/backend/backend/src/services/credential/credential-store.service.d.ts +161 -0
- package/dist/backend/backend/src/services/credential/credential-store.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/credential/credential-store.service.js +298 -0
- package/dist/backend/backend/src/services/credential/credential-store.service.js.map +1 -0
- package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts +117 -0
- package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -0
- package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +293 -0
- package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -0
- package/dist/backend/backend/src/services/project/task.service.d.ts +18 -2
- package/dist/backend/backend/src/services/project/task.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project/task.service.js +69 -53
- package/dist/backend/backend/src/services/project/task.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/contract-matcher.d.ts +20 -0
- package/dist/backend/backend/src/services/v3/contract-matcher.d.ts.map +1 -0
- package/dist/backend/backend/src/services/v3/contract-matcher.js +33 -0
- package/dist/backend/backend/src/services/v3/contract-matcher.js.map +1 -0
- package/dist/backend/backend/src/services/v3/escalation.service.d.ts +20 -1
- package/dist/backend/backend/src/services/v3/escalation.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation.service.js +97 -28
- package/dist/backend/backend/src/services/v3/escalation.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/service-contract-gate.service.d.ts +6 -4
- package/dist/backend/backend/src/services/v3/service-contract-gate.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/service-contract-gate.service.js +18 -28
- package/dist/backend/backend/src/services/v3/service-contract-gate.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js +14 -9
- package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts +34 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.js +115 -5
- package/dist/backend/backend/src/services/v3/trigger-engine.service.js.map +1 -1
- package/dist/backend/backend/src/types/credential.types.d.ts +185 -0
- package/dist/backend/backend/src/types/credential.types.d.ts.map +1 -0
- package/dist/backend/backend/src/types/credential.types.js +76 -0
- package/dist/backend/backend/src/types/credential.types.js.map +1 -0
- package/dist/backend/backend/src/utils/encryption.utils.d.ts +57 -0
- package/dist/backend/backend/src/utils/encryption.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/encryption.utils.js +162 -0
- package/dist/backend/backend/src/utils/encryption.utils.js.map +1 -0
- package/dist/cli/backend/src/services/credential/credential-store.service.d.ts +161 -0
- package/dist/cli/backend/src/services/credential/credential-store.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/credential/credential-store.service.js +298 -0
- package/dist/cli/backend/src/services/credential/credential-store.service.js.map +1 -0
- package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts +117 -0
- package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -0
- package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +293 -0
- package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -0
- package/dist/cli/backend/src/services/settings/settings.service.d.ts +168 -0
- package/dist/cli/backend/src/services/settings/settings.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/settings/settings.service.js +312 -0
- package/dist/cli/backend/src/services/settings/settings.service.js.map +1 -0
- package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts +159 -0
- package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill/skill-executor.service.js +626 -0
- package/dist/cli/backend/src/services/skill/skill-executor.service.js.map +1 -0
- package/dist/cli/backend/src/services/skill/skill.service.d.ts +273 -0
- package/dist/cli/backend/src/services/skill/skill.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill/skill.service.js +655 -0
- package/dist/cli/backend/src/services/skill/skill.service.js.map +1 -0
- package/dist/cli/backend/src/types/credential.types.d.ts +185 -0
- package/dist/cli/backend/src/types/credential.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/credential.types.js +76 -0
- package/dist/cli/backend/src/types/credential.types.js.map +1 -0
- package/dist/cli/backend/src/utils/encryption.utils.d.ts +57 -0
- package/dist/cli/backend/src/utils/encryption.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/encryption.utils.js +162 -0
- package/dist/cli/backend/src/utils/encryption.utils.js.map +1 -0
- package/dist/cli/backend/src/utils/skill-md-parser.d.ts +38 -0
- package/dist/cli/backend/src/utils/skill-md-parser.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/skill-md-parser.js +47 -0
- package/dist/cli/backend/src/utils/skill-md-parser.js.map +1 -0
- package/frontend/dist/assets/{index-dc92ab64.css → index-6aaa0630.css} +1 -1
- package/frontend/dist/assets/{index-76d76633.js → index-9e6d97d1.js} +334 -328
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/config/experts/empathetic-resolver/expert.json +0 -11
- package/config/experts/empathetic-resolver.md +0 -32
- package/config/experts/pragmatic-architect/expert.json +0 -11
- package/config/experts/pragmatic-architect.md +0 -32
- package/config/experts/viral-alchemist/expert.json +0 -11
- 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 ]
|