crewly 1.20.450 → 1.20.452
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/skills/agent/checkup-report/SKILL.md +22 -5
- package/config/skills/agent/checkup-report/app/index.html +10 -0
- package/config/skills/agent/checkup-report/execute.sh +6 -2
- package/config/skills/agent/checkup-report/execute.test.sh +37 -0
- package/config/skills/agent/checkup-report/report.mjs +92 -5
- package/config/skills/agent/spawn-subagent/SKILL.md +105 -0
- package/config/skills/agent/spawn-subagent/execute.sh +114 -0
- package/config/skills/agent/spawn-subagent/execute.test.sh +113 -0
- package/config/templates/small-business-checkup.json +22 -0
- package/dist/backend/backend/src/controllers/subagents/subagents.controller.d.ts +27 -0
- package/dist/backend/backend/src/controllers/subagents/subagents.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/subagents/subagents.controller.js +61 -0
- package/dist/backend/backend/src/controllers/subagents/subagents.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/subagents/subagents.routes.d.ts +15 -0
- package/dist/backend/backend/src/controllers/subagents/subagents.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/subagents/subagents.routes.js +21 -0
- package/dist/backend/backend/src/controllers/subagents/subagents.routes.js.map +1 -0
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +3 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +8 -2
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/subagent-run.service.d.ts +179 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/subagent-run.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/subagent-run.service.js +345 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/subagent-run.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/types.d.ts +33 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/types.js +15 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +8 -2
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/crewly-agent/types.d.ts +33 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/types.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/crewly-agent/types.js +15 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Check-up Report
|
|
3
|
-
description: The intake and report steps of the small-business check-up as one Crewly App. Copies the bundled App page (five intake questions, the report with the top 3 breaks and what an agent could do, a 7-day daily summary) into the project for publish-app, turns checkup-analyse's breaks.json into the App's report document (with the number the owner asked to see first),
|
|
4
|
-
version: 1.
|
|
3
|
+
description: The intake and report steps of the small-business check-up as one Crewly App. Copies the bundled App page (five intake questions, the report with the top 3 breaks and what an agent could do, a 7-day daily summary) into the project for publish-app, turns checkup-analyse's breaks.json into the App's report document (with the number the owner asked to see first), writes daily-summary documents for app-data, and keeps a per-step time log (--log-step) the report shows.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
category: productivity
|
|
6
6
|
skillType: claude-skill
|
|
7
7
|
assignableRoles:
|
|
@@ -12,6 +12,7 @@ triggers:
|
|
|
12
12
|
- publish the check-up
|
|
13
13
|
- intake questions
|
|
14
14
|
- daily check-up summary
|
|
15
|
+
- log a check-up step
|
|
15
16
|
tags:
|
|
16
17
|
- check-up
|
|
17
18
|
- small-business
|
|
@@ -84,10 +85,26 @@ bash app-data --app <appId> --set daily 2026-10-27 --data-file ./checkup-data/da
|
|
|
84
85
|
Say what changed and what was done, in the owner's language, under 300
|
|
85
86
|
characters. Seven entries, then stop unless the owner asks for more.
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
The free **Small Business Check-up** team template (`small-business-checkup`)
|
|
89
|
+
runs these steps with a Checkup Lead, a Data Analyst and an optional Reviewer
|
|
90
|
+
who checks `report-doc.json` before it is published; remove the Reviewer from
|
|
91
|
+
the team to skip that step.
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
## Time log — how long the check-up really took
|
|
94
|
+
|
|
95
|
+
Mark the start and end of every step; the report shows the minutes per step
|
|
96
|
+
and the total, and the owner sees them under "How long it took".
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
bash execute.sh --log-step intake --start --out ./checkup-data # steps: intake, connect, analyse, report, daily
|
|
100
|
+
bash execute.sh --log-step intake --end --out ./checkup-data
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`--out` is the same directory as the report; the log is `timelog.json` there
|
|
104
|
+
and `--breaks` folds it in as `timing` (closed steps with minutes, the total,
|
|
105
|
+
and any step still open). A second `--start` on an open step restarts it;
|
|
106
|
+
`--end` with no open entry fails (exit 2) so a forgotten start is noticed.
|
|
107
|
+
`--at <iso>` back-fills a time you know. One `daily` entry per day is fine.
|
|
91
108
|
|
|
92
109
|
## Report back
|
|
93
110
|
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
<div id="notes-wrap" class="hidden"><h2>Could not check</h2><ul class="plain" id="notes" data-crewly-id="notes"></ul></div>
|
|
69
69
|
<h2>Files</h2>
|
|
70
70
|
<ul class="plain" id="files" data-crewly-id="files"></ul>
|
|
71
|
+
<div id="timing-wrap" class="hidden"><h2>How long it took</h2><ul class="plain" id="timing" data-crewly-id="timing"></ul></div>
|
|
71
72
|
<h2>Assumptions</h2>
|
|
72
73
|
<ul class="plain" id="assumptions" data-crewly-id="assumptions"></ul>
|
|
73
74
|
<button class="secondary" id="ask" data-crewly-id="ask">Ask about this report</button>
|
|
@@ -193,6 +194,15 @@
|
|
|
193
194
|
(r.files || []).forEach(function (f) { files.appendChild(el('li', null, f.file + ' — ' + f.source + ' ' + f.kind + ', ' + f.rows + ' rows')); });
|
|
194
195
|
var as = $('assumptions'); as.replaceChildren();
|
|
195
196
|
Object.keys(r.assumptions || {}).forEach(function (k) { as.appendChild(el('li', null, k + ': ' + r.assumptions[k])); });
|
|
197
|
+
var timing = $('timing'); timing.replaceChildren();
|
|
198
|
+
var t = r.timing;
|
|
199
|
+
if (t && t.steps && t.steps.length) {
|
|
200
|
+
t.steps.forEach(function (s) {
|
|
201
|
+
timing.appendChild(el('li', null, s.step + ': ' + (s.endedAt ? s.minutes + ' min' : 'still running')));
|
|
202
|
+
});
|
|
203
|
+
timing.appendChild(el('li', 'note', 'Total ' + t.totalMinutes + ' min of agent time' + (t.open && t.open.length ? ' so far' : '')));
|
|
204
|
+
}
|
|
205
|
+
$('timing-wrap').classList.toggle('hidden', !(t && t.steps && t.steps.length));
|
|
196
206
|
}
|
|
197
207
|
$('ask').onclick = function () {
|
|
198
208
|
crewly.notify('The owner opened the check-up report and tapped "Ask about this report". Reply in their DM with a one-line offer to walk through the top 3.').then(function () {
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
# bash execute.sh --app-dir ./checkup-app # copy the App page
|
|
10
10
|
# bash execute.sh --breaks …/analysis/breaks.json --business "Oak Pottery" --intake ./intake.json --out ./checkup-data
|
|
11
11
|
# bash execute.sh --daily "Replied to 3 open inquiries; 1 booked." --out ./checkup-data
|
|
12
|
+
# bash execute.sh --log-step intake --start --out ./checkup-data # per-step time log (timelog.json)
|
|
13
|
+
# bash execute.sh '{"logStep":"intake","mark":"end","out":"./checkup-data"}'
|
|
12
14
|
# bash execute.sh '{"breaks":"…/breaks.json","business":"Oak Pottery","out":"./checkup-data"}'
|
|
13
15
|
#
|
|
14
16
|
# Output: one JSON line with the files written and the exact next command
|
|
@@ -23,13 +25,15 @@ command -v node >/dev/null 2>&1 || { printf '{"success":false,"error":"node is r
|
|
|
23
25
|
ARGS=()
|
|
24
26
|
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
25
27
|
command -v jq >/dev/null 2>&1 || { printf '{"success":false,"error":"jq is required to read JSON input"}\n'; exit 1; }
|
|
26
|
-
for key in appDir breaks business intake examples out daily date; do
|
|
28
|
+
for key in appDir breaks business intake examples out daily date logStep at; do
|
|
27
29
|
v=$(printf '%s' "$1" | jq -r --arg k "$key" '.[$k] // empty')
|
|
28
30
|
if [ -n "$v" ]; then
|
|
29
|
-
case "$key" in appDir) flag=--app-dir ;; *) flag="--$key" ;; esac
|
|
31
|
+
case "$key" in appDir) flag=--app-dir ;; logStep) flag=--log-step ;; *) flag="--$key" ;; esac
|
|
30
32
|
ARGS+=("$flag" "$v")
|
|
31
33
|
fi
|
|
32
34
|
done
|
|
35
|
+
mark=$(printf '%s' "$1" | jq -r '.mark // empty')
|
|
36
|
+
case "$mark" in start|end) ARGS+=("--$mark") ;; esac
|
|
33
37
|
shift
|
|
34
38
|
fi
|
|
35
39
|
ARGS+=("$@")
|
|
@@ -133,5 +133,42 @@ bash "$EXEC" --daily " " --out "$T/x" >/dev/null 2>&1; check "empty daily exit
|
|
|
133
133
|
bash "$EXEC" --breaks "$T/an/breaks.json" --examples lots --out "$T/x" >/dev/null 2>&1; check "bad examples exits 1" "$?" "1"
|
|
134
134
|
bash "$EXEC" --bogus >/dev/null 2>&1; check "unknown flag exits 1" "$?" "1"
|
|
135
135
|
|
|
136
|
+
# 9. --log-step: per-step time log
|
|
137
|
+
check "page has a timing block" "$(grep -c 'data-crewly-id="timing"' "$PAGE")" "1"
|
|
138
|
+
bash "$EXEC" --log-step intake --start --at 2026-10-26T09:00:00Z --out "$T/tl" >/dev/null; check "log start exits 0" "$?" "0"
|
|
139
|
+
check "timelog written" "$([ -f "$T/tl/timelog.json" ] && echo yes || echo no)" "yes"
|
|
140
|
+
check "open entry" "$(jq -c '.steps[0] | {step, endedAt, minutes}' "$T/tl/timelog.json")" '{"step":"intake","endedAt":null,"minutes":null}'
|
|
141
|
+
LOUT="$(bash "$EXEC" --log-step intake --end --at 2026-10-26T09:12:30Z --out "$T/tl")"
|
|
142
|
+
check "log end exits 0" "$?" "0"
|
|
143
|
+
check "minutes = 12.5" "$(jq -r '.steps[0].minutes' "$T/tl/timelog.json")" "12.5"
|
|
144
|
+
check "end output carries the entry" "$(printf '%s' "$LOUT" | jq -r '.entry.endedAt')" "2026-10-26T09:12:30.000Z"
|
|
145
|
+
check "end output totals" "$(printf '%s' "$LOUT" | jq -c '.timing | {totalMinutes, open}')" '{"totalMinutes":12.5,"open":[]}'
|
|
146
|
+
bash "$EXEC" --log-step connect --start --at 2026-10-26T09:13:00Z --out "$T/tl" >/dev/null
|
|
147
|
+
bash "$EXEC" --log-step connect --start --at 2026-10-26T09:14:00Z --out "$T/tl" >/dev/null
|
|
148
|
+
check "restart replaces the open start" "$(jq -r '[.steps[] | select(.step=="connect")] | length, .[0].startedAt' "$T/tl/timelog.json" | tr '\n' ' ')" "1 2026-10-26T09:14:00.000Z "
|
|
149
|
+
bash "$EXEC" --log-step connect --end --at 2026-10-26T10:14:00Z --out "$T/tl" >/dev/null
|
|
150
|
+
bash "$EXEC" --log-step analyse --start --at 2026-10-26T10:15:00Z --out "$T/tl" >/dev/null
|
|
151
|
+
bash "$EXEC" --log-step analyse --end --at 2026-10-26T10:21:00Z --out "$T/tl" >/dev/null
|
|
152
|
+
bash "$EXEC" --log-step report --start --at 2026-10-26T10:22:00Z --out "$T/tl" >/dev/null
|
|
153
|
+
JLOG="$(bash "$EXEC" "{\"logStep\":\"report\",\"mark\":\"end\",\"at\":\"2026-10-26T10:30:00Z\",\"out\":\"$T/tl\"}")"
|
|
154
|
+
check "json input logs a step" "$(printf '%s' "$JLOG" | jq -r '.entry.minutes')" "8"
|
|
155
|
+
check "four closed steps" "$(jq -r '[.steps[] | select(.endedAt)] | length' "$T/tl/timelog.json")" "4"
|
|
156
|
+
check "same step twice keeps both" "$(bash "$EXEC" --log-step daily --start --at 2026-10-27T09:00:00Z --out "$T/tl" >/dev/null; bash "$EXEC" --log-step daily --end --at 2026-10-27T09:03:00Z --out "$T/tl" >/dev/null; bash "$EXEC" --log-step daily --start --at 2026-10-28T09:00:00Z --out "$T/tl" >/dev/null; jq -r '[.steps[] | select(.step=="daily")] | length' "$T/tl/timelog.json")" "2"
|
|
157
|
+
bash "$EXEC" --breaks "$T/an/breaks.json" --business "Oak Pottery" --out "$T/tl" >/dev/null
|
|
158
|
+
check "report folds the time log in" "$(jq -c '.timing | {totalMinutes, open, n: (.steps|length)}' "$T/tl/report-doc.json")" '{"totalMinutes":89.5,"open":["daily"],"n":6}'
|
|
159
|
+
check "timing steps carry minutes" "$(jq -r '.timing.steps[1] | "\(.step) \(.minutes)"' "$T/tl/report-doc.json")" "connect 60"
|
|
160
|
+
check "no time log → no timing" "$(jq -r 'has("timing")' "$D")" "false"
|
|
161
|
+
bash "$EXEC" --log-step cook --start --out "$T/tl" >/dev/null 2>&1; check "unknown step exits 1" "$?" "1"
|
|
162
|
+
bash "$EXEC" --log-step analyse --end --out "$T/tl" >/dev/null 2>&1; check "end without start exits 2" "$?" "2"
|
|
163
|
+
bash "$EXEC" --log-step intake --out "$T/tl" >/dev/null 2>&1; check "log-step without --start/--end exits 1" "$?" "1"
|
|
164
|
+
bash "$EXEC" --start --out "$T/tl" >/dev/null 2>&1; check "--start without --log-step exits 1" "$?" "1"
|
|
165
|
+
bash "$EXEC" --log-step intake --start >/dev/null 2>&1; check "log-step without --out exits 1" "$?" "1"
|
|
166
|
+
bash "$EXEC" --log-step intake --start --at yesterday --out "$T/tl" >/dev/null 2>&1; check "bad --at exits 1" "$?" "1"
|
|
167
|
+
bash "$EXEC" --log-step intake --start --at 2026-10-26T09:00:00Z --out "$T/tl2" >/dev/null
|
|
168
|
+
bash "$EXEC" --log-step intake --end --at 2026-10-26T08:00:00Z --out "$T/tl2" >/dev/null 2>&1; check "end before start exits 1" "$?" "1"
|
|
169
|
+
printf '{"nope":1}' > "$T/tl3.json"; mkdir -p "$T/tl3"; cp "$T/tl3.json" "$T/tl3/timelog.json"
|
|
170
|
+
bash "$EXEC" --log-step intake --start --out "$T/tl3" >/dev/null 2>&1; check "broken timelog exits 2" "$?" "2"
|
|
171
|
+
bash "$EXEC" --breaks "$T/an/breaks.json" --out "$T/tl3" >/dev/null 2>&1; check "report with broken timelog exits 2" "$?" "2"
|
|
172
|
+
|
|
136
173
|
echo "checkup-report: $PASS passed, $FAIL failed"
|
|
137
174
|
[ "$FAIL" -eq 0 ]
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// node report.mjs --app-dir ./checkup-app # copy the App page to publish
|
|
11
11
|
// node report.mjs --breaks …/breaks.json --business "Oak Pottery" [--intake answers.json] --out ./checkup-data
|
|
12
12
|
// node report.mjs --daily "Replied to 3 open inquiries; 1 booked." [--date 2026-10-27] --out ./checkup-data
|
|
13
|
+
// node report.mjs --log-step intake --start --out ./checkup-data # per-step time log → timelog.json
|
|
14
|
+
// node report.mjs --log-step intake --end --out ./checkup-data
|
|
13
15
|
// =============================================================================
|
|
14
16
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, readdirSync, statSync } from 'node:fs';
|
|
15
17
|
import { join, resolve, dirname } from 'node:path';
|
|
@@ -20,6 +22,9 @@ const APP_SRC = join(HERE, 'app');
|
|
|
20
22
|
const DEFAULT_EXAMPLES = 3;
|
|
21
23
|
/** Crewly App documents are capped at 256 KB; stay well under it. */
|
|
22
24
|
const MAX_DOC_BYTES = 200 * 1024;
|
|
25
|
+
/** The check-up's steps, in order; the time log accepts no other name. */
|
|
26
|
+
export const STEPS = ['intake', 'connect', 'analyse', 'report', 'daily'];
|
|
27
|
+
const TIMELOG_FILE = 'timelog.json';
|
|
23
28
|
|
|
24
29
|
/** The "which number first" choices offered by the intake form, with how each is read off the breaks. */
|
|
25
30
|
const FIRST_NUMBER_RULES = [
|
|
@@ -60,7 +65,8 @@ function examplesFor(b, limit) {
|
|
|
60
65
|
/**
|
|
61
66
|
* Build the report document the App renders from checkup-analyse output.
|
|
62
67
|
* Items are summarised into a few examples per break so the document stays small
|
|
63
|
-
* and no full list of people lands in the App's data.
|
|
68
|
+
* and no full list of people lands in the App's data. `opts.timelog` (from
|
|
69
|
+
* `readTimelog`) adds a `timing` block with minutes per step.
|
|
64
70
|
*/
|
|
65
71
|
export function buildReportDoc(breaks, opts = {}) {
|
|
66
72
|
const by = Object.fromEntries(breaks.breaks.map((b) => [b.type, b]));
|
|
@@ -91,9 +97,58 @@ export function buildReportDoc(breaks, opts = {}) {
|
|
|
91
97
|
if (rule) doc.firstNumber = { asked: want, key: rule.key, label: rule.label, value: rule.value(by) };
|
|
92
98
|
else doc.notes = [...doc.notes, `The owner asked to see "${want}" first; no break maps to it, so the top 3 lead.`];
|
|
93
99
|
}
|
|
100
|
+
// How long the check-up took, when the agent logged its steps (`--log-step`).
|
|
101
|
+
if (opts.timelog && opts.timelog.steps.length) doc.timing = summariseTimelog(opts.timelog);
|
|
94
102
|
return doc;
|
|
95
103
|
}
|
|
96
104
|
|
|
105
|
+
/** Read `<dir>/timelog.json`; an absent file is an empty log, a broken one throws. */
|
|
106
|
+
export function readTimelog(dir) {
|
|
107
|
+
const file = join(dir, TIMELOG_FILE);
|
|
108
|
+
if (!existsSync(file)) return { steps: [] };
|
|
109
|
+
const raw = JSON.parse(readFileSync(file, 'utf8'));
|
|
110
|
+
if (!raw || !Array.isArray(raw.steps)) throw new Error(`${file}: not a time log ({ steps: [] })`);
|
|
111
|
+
return raw;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Record the start or end of a check-up step in the time log.
|
|
116
|
+
*
|
|
117
|
+
* `--start` opens an entry (a second start on a still-open step restarts it);
|
|
118
|
+
* `--end` closes the latest open entry of that step and fills in `minutes`.
|
|
119
|
+
* `at` overrides the clock (ISO timestamp) for back-filling and tests.
|
|
120
|
+
*
|
|
121
|
+
* @returns the updated log and the entry touched
|
|
122
|
+
* @throws when the step is unknown, `--end` finds no open entry, or `at` is not a date
|
|
123
|
+
*/
|
|
124
|
+
export function logStep(log, step, mark, at) {
|
|
125
|
+
if (!STEPS.includes(step)) throw new Error(`unknown step "${step}"; use one of ${STEPS.join(', ')}`);
|
|
126
|
+
const when = at ? new Date(at) : new Date();
|
|
127
|
+
if (Number.isNaN(when.getTime())) throw new Error(`--at must be an ISO timestamp, got "${at}"`);
|
|
128
|
+
const iso = when.toISOString();
|
|
129
|
+
const open = [...log.steps].reverse().find((s) => s.step === step && !s.endedAt);
|
|
130
|
+
let entry;
|
|
131
|
+
if (mark === 'start') {
|
|
132
|
+
if (open) { open.startedAt = iso; entry = open; }
|
|
133
|
+
else { entry = { step, startedAt: iso, endedAt: null, minutes: null }; log.steps.push(entry); }
|
|
134
|
+
} else {
|
|
135
|
+
if (!open) throw new Error(`no open "${step}" entry to end; run --log-step ${step} --start first`);
|
|
136
|
+
if (when.getTime() < new Date(open.startedAt).getTime()) throw new Error(`end (${iso}) is before start (${open.startedAt})`);
|
|
137
|
+
open.endedAt = iso;
|
|
138
|
+
open.minutes = Math.round((when.getTime() - new Date(open.startedAt).getTime()) / 6000) / 10;
|
|
139
|
+
entry = open;
|
|
140
|
+
}
|
|
141
|
+
return { log, entry };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Fold a time log into the shape the report document carries: closed steps with minutes, plus the total. */
|
|
145
|
+
export function summariseTimelog(log) {
|
|
146
|
+
const steps = log.steps.map((s) => ({ step: s.step, startedAt: s.startedAt, endedAt: s.endedAt, minutes: s.minutes }));
|
|
147
|
+
const closed = steps.filter((s) => s.endedAt);
|
|
148
|
+
const totalMinutes = Math.round(closed.reduce((a, s) => a + (s.minutes || 0), 0) * 10) / 10;
|
|
149
|
+
return { steps, totalMinutes, open: steps.filter((s) => !s.endedAt).map((s) => s.step) };
|
|
150
|
+
}
|
|
151
|
+
|
|
97
152
|
/** Read intake answers from a plain document or from app-data's `{ success, data }` output. */
|
|
98
153
|
function readIntake(path) {
|
|
99
154
|
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
@@ -119,7 +174,7 @@ function out(obj, code) {
|
|
|
119
174
|
}
|
|
120
175
|
|
|
121
176
|
function usage() {
|
|
122
|
-
out({ success: false, error: 'usage: report.mjs --app-dir <dir> | --breaks <breaks.json> [--business <name>] [--intake <answers.json>] [--examples N] --out <dir> | --daily "<summary>" [--date YYYY-MM-DD] --out <dir>' }, 1);
|
|
177
|
+
out({ success: false, error: 'usage: report.mjs --app-dir <dir> | --breaks <breaks.json> [--business <name>] [--intake <answers.json>] [--examples N] --out <dir> | --daily "<summary>" [--date YYYY-MM-DD] --out <dir> | --log-step <intake|connect|analyse|report|daily> --start|--end [--at <iso>] --out <dir>' }, 1);
|
|
123
178
|
}
|
|
124
179
|
|
|
125
180
|
function main(argv) {
|
|
@@ -135,14 +190,39 @@ function main(argv) {
|
|
|
135
190
|
else if (a === '--out') o.out = next();
|
|
136
191
|
else if (a === '--daily') o.daily = next();
|
|
137
192
|
else if (a === '--date') o.date = next();
|
|
193
|
+
else if (a === '--log-step') o.logStep = next();
|
|
194
|
+
else if (a === '--start') o.mark = 'start';
|
|
195
|
+
else if (a === '--end') o.mark = 'end';
|
|
196
|
+
else if (a === '--at') o.at = next();
|
|
138
197
|
else usage();
|
|
139
198
|
}
|
|
140
|
-
if (!o.appDir && !o.breaks && !o.daily) usage();
|
|
141
|
-
if ((o.breaks || o.daily) && !o.out) usage();
|
|
199
|
+
if (!o.appDir && !o.breaks && !o.daily && !o.logStep) usage();
|
|
200
|
+
if ((o.breaks || o.daily || o.logStep) && !o.out) usage();
|
|
201
|
+
if (o.logStep && !o.mark) usage();
|
|
202
|
+
if (o.mark && !o.logStep) usage();
|
|
142
203
|
if (o.date && !/^\d{4}-\d{2}-\d{2}$/.test(o.date)) out({ success: false, error: `--date must be YYYY-MM-DD, got "${o.date}"` }, 1);
|
|
143
204
|
if (!Number.isFinite(o.examples) || o.examples < 0) out({ success: false, error: '--examples needs a number >= 0' }, 1);
|
|
144
205
|
|
|
145
206
|
const result = { success: true };
|
|
207
|
+
if (o.logStep) {
|
|
208
|
+
let log;
|
|
209
|
+
try {
|
|
210
|
+
log = readTimelog(o.out);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
out({ success: false, error: e.message }, 2);
|
|
213
|
+
}
|
|
214
|
+
let entry;
|
|
215
|
+
try {
|
|
216
|
+
({ entry } = logStep(log, o.logStep, o.mark, o.at));
|
|
217
|
+
} catch (e) {
|
|
218
|
+
out({ success: false, error: e.message }, o.mark === 'end' && /no open/.test(e.message) ? 2 : 1);
|
|
219
|
+
}
|
|
220
|
+
mkdirSync(o.out, { recursive: true });
|
|
221
|
+
writeFileSync(join(o.out, TIMELOG_FILE), JSON.stringify(log, null, 2));
|
|
222
|
+
result.timelog = resolve(join(o.out, TIMELOG_FILE));
|
|
223
|
+
result.entry = entry;
|
|
224
|
+
result.timing = summariseTimelog(log);
|
|
225
|
+
}
|
|
146
226
|
if (o.appDir) {
|
|
147
227
|
result.appDir = resolve(o.appDir);
|
|
148
228
|
result.copied = copyApp(o.appDir);
|
|
@@ -166,7 +246,13 @@ function main(argv) {
|
|
|
166
246
|
out({ success: false, error: `${o.intake}: ${e.message}` }, 2);
|
|
167
247
|
}
|
|
168
248
|
}
|
|
169
|
-
|
|
249
|
+
let timelog = null;
|
|
250
|
+
try {
|
|
251
|
+
timelog = readTimelog(o.out);
|
|
252
|
+
} catch (e) {
|
|
253
|
+
out({ success: false, error: e.message }, 2);
|
|
254
|
+
}
|
|
255
|
+
const doc = buildReportDoc(breaks, { business: o.business, intake, examples: o.examples, timelog });
|
|
170
256
|
const json = JSON.stringify(doc, null, 2);
|
|
171
257
|
if (Buffer.byteLength(json) > MAX_DOC_BYTES) out({ success: false, error: `report document is ${Buffer.byteLength(json)} bytes; lower --examples` }, 2);
|
|
172
258
|
mkdirSync(o.out, { recursive: true });
|
|
@@ -175,6 +261,7 @@ function main(argv) {
|
|
|
175
261
|
result.top3 = doc.top3;
|
|
176
262
|
result.totals = doc.totals;
|
|
177
263
|
result.firstNumber = doc.firstNumber || null;
|
|
264
|
+
result.timing = doc.timing || null;
|
|
178
265
|
result.next = `app-data --app <appId> --set report current --data-file ${join(o.out, 'report-doc.json')}`;
|
|
179
266
|
}
|
|
180
267
|
if (o.daily) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Spawn Sub-agent
|
|
3
|
+
description: Run one short-lived helper for a bounded task (a large read, a parallel search, a scoped fix) and get its report back. Works on every runtime - Claude Code, Codex, Gemini, crewly-agent - because the helper runs in Crewly's own in-process runtime, not yours. The helper cannot message, delegate or schedule; it reads and edits under one directory and reports once. Treat the result as evidence, not as done work.
|
|
4
|
+
natural_language_description: Hand a bounded task to a short-lived helper and read its report.
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
category: task-management
|
|
7
|
+
skillType: claude-skill
|
|
8
|
+
assignableRoles:
|
|
9
|
+
- developer
|
|
10
|
+
- qa
|
|
11
|
+
- tpm
|
|
12
|
+
- designer
|
|
13
|
+
- frontend-developer
|
|
14
|
+
- backend-developer
|
|
15
|
+
- fullstack-dev
|
|
16
|
+
- qa-engineer
|
|
17
|
+
- product-manager
|
|
18
|
+
- architect
|
|
19
|
+
- generalist
|
|
20
|
+
- researcher
|
|
21
|
+
- team-leader
|
|
22
|
+
triggers:
|
|
23
|
+
- spawn sub-agent
|
|
24
|
+
- spawn subagent
|
|
25
|
+
- run a helper
|
|
26
|
+
- delegate to helper
|
|
27
|
+
- parallel research
|
|
28
|
+
tags:
|
|
29
|
+
- subagent
|
|
30
|
+
- delegation
|
|
31
|
+
- in-process
|
|
32
|
+
execution:
|
|
33
|
+
type: script
|
|
34
|
+
script:
|
|
35
|
+
file: execute.sh
|
|
36
|
+
interpreter: bash
|
|
37
|
+
timeoutMs: 660000
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# Spawn Sub-agent
|
|
41
|
+
|
|
42
|
+
Runs one short-lived helper for a single bounded task and returns its report.
|
|
43
|
+
The helper is a Crewly in-process worker (crewly#1317): it gets read tools
|
|
44
|
+
(`read_file`, `glob`, `grep`, `git_status`, `git_diff`, `web_search`),
|
|
45
|
+
write tools under its directory unless `--read-only`, and `report_to_parent`.
|
|
46
|
+
It cannot message anyone, delegate, schedule, touch tickets, or spawn helpers
|
|
47
|
+
of its own. The call blocks until the helper reports or its budget runs out.
|
|
48
|
+
|
|
49
|
+
Use it for work that is bounded and self-contained and that nobody needs to
|
|
50
|
+
own, review or hear about: reading a large tree for one question, three
|
|
51
|
+
searches in parallel, a scoped fix with a clear acceptance line. Do not use
|
|
52
|
+
it for work a teammate should own (that is a ticket) or for anything that
|
|
53
|
+
needs to talk to the owner.
|
|
54
|
+
|
|
55
|
+
## Parameters
|
|
56
|
+
|
|
57
|
+
| Flag | JSON Field | Required | Description |
|
|
58
|
+
|------|-----------|----------|-------------|
|
|
59
|
+
| `--task` / `-t` | `task` | Yes | The task, in full, with the acceptance line. Max 12,000 chars: summarise context, do not paste it |
|
|
60
|
+
| `--cwd` / `-C` | `cwd` | No | Absolute directory the helper works in and may write under. Default `$CREWLY_PROJECT_PATH`, else `$PWD` |
|
|
61
|
+
| `--read-only` | `readOnly` | No | No shell and no file writes |
|
|
62
|
+
| `--max-steps` | `maxSteps` | No | Model steps granted (default 30, cap 60) |
|
|
63
|
+
| `--model` / `-m` | `model` | No | `provider/modelId`; default is the model Crewly's fallback settings name for in-process agents |
|
|
64
|
+
| `--task-file` | - | No | Read the task from a file instead of the flag |
|
|
65
|
+
|
|
66
|
+
## Examples
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# A read-only question about the codebase
|
|
70
|
+
bash config/skills/agent/spawn-subagent/execute.sh --read-only \
|
|
71
|
+
--task "Where is the retry delay for Slack posts set? Report file and line, and the default value."
|
|
72
|
+
|
|
73
|
+
# A scoped fix in a worktree
|
|
74
|
+
bash config/skills/agent/spawn-subagent/execute.sh --cwd /abs/path/to/worktree \
|
|
75
|
+
--task "Rename helper foo() to bar() in src/x.ts and its test; run 'npx tsc --noEmit -p backend'. Report what changed and the tsc result."
|
|
76
|
+
|
|
77
|
+
# Three helpers in parallel from a shell
|
|
78
|
+
for q in "auth" "billing" "slack"; do
|
|
79
|
+
bash config/skills/agent/spawn-subagent/execute.sh --read-only --task "List every file under src that handles $q, one line each" > "/tmp/sub-$q.json" &
|
|
80
|
+
done; wait
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Output
|
|
84
|
+
|
|
85
|
+
JSON. `success` is true only when the helper did work and stopped on its
|
|
86
|
+
own. `stoppedReason` is one of `done`, `did_nothing`, `steps_exhausted`,
|
|
87
|
+
`incomplete`, `timeout`, `error`. `summary` is its report (capped at 6,000
|
|
88
|
+
chars), `toolCalls` / `toolNames` what it did, `filesChanged` what it
|
|
89
|
+
wrote, `usage` its tokens (booked under `sessionName`, `<you>/sub-<n>`).
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{"success":true,"stoppedReason":"done","summary":"...","toolCalls":4,"toolNames":["grep","read_file"],"filesChanged":[],"usage":{"input":1200,"output":300,"cachedInput":0},"steps":5,"sessionName":"crewly-dev-1/sub-1","durationMs":18000,"model":"deepseek/deepseek-chat"}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A refusal is an error with a `code`: `nested` (a helper cannot spawn),
|
|
96
|
+
`too_many` (3 already running for you), `task_too_long`, `cwd_invalid`,
|
|
97
|
+
`model_invalid`, `parent_required` (no agent session in this shell).
|
|
98
|
+
|
|
99
|
+
## Reading the result
|
|
100
|
+
|
|
101
|
+
`toolCalls: 0` means the helper did nothing, whatever `summary` says. Check
|
|
102
|
+
`filesChanged` against what you asked for. The report is a claim from a
|
|
103
|
+
model with less context than you: verify anything you will act on, and
|
|
104
|
+
never relay it to the owner as your own finding without reading the files it
|
|
105
|
+
names.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Run one short-lived in-process helper for a bounded task (crewly#1317).
|
|
3
|
+
# Thin wrapper over POST /api/subagents; the backend starts the worker,
|
|
4
|
+
# waits for its one report and shuts it down. Blocks until then.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
source "${SCRIPT_DIR}/../_common/lib.sh"
|
|
8
|
+
|
|
9
|
+
# Wall-clock budget the backend gives a helper (600 s) plus a margin: the
|
|
10
|
+
# reply arrives only when the helper is done.
|
|
11
|
+
SUBAGENT_CURL_MAX_TIME="${CREWLY_SUBAGENT_MAX_TIME:-660}"
|
|
12
|
+
|
|
13
|
+
print_usage() {
|
|
14
|
+
cat <<'EOF_USAGE'
|
|
15
|
+
Usage:
|
|
16
|
+
bash execute.sh --task "..." [--cwd /abs/dir] [--read-only] [--max-steps N] [--model provider/id]
|
|
17
|
+
bash execute.sh --task-file /path/to/task.md [...]
|
|
18
|
+
bash execute.sh '{"task":"...","cwd":"/abs/dir","readOnly":true}'
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--task | -t The task, in full, with its acceptance line (max 12,000 chars)
|
|
22
|
+
--task-file Read the task from a file
|
|
23
|
+
--cwd | -C Absolute directory the helper works in (default $CREWLY_PROJECT_PATH, else $PWD)
|
|
24
|
+
--read-only No shell and no file writes
|
|
25
|
+
--max-steps Model steps granted (default 30, cap 60)
|
|
26
|
+
--model | -m provider/modelId (default: the fallback settings' in-process model)
|
|
27
|
+
--json | -j Raw JSON payload (same as the legacy positional form)
|
|
28
|
+
--help | -h Show this help
|
|
29
|
+
EOF_USAGE
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
INPUT_JSON=""
|
|
33
|
+
TASK=""
|
|
34
|
+
TASK_FILE=""
|
|
35
|
+
CWD_ARG=""
|
|
36
|
+
READ_ONLY="false"
|
|
37
|
+
MAX_STEPS=""
|
|
38
|
+
MODEL=""
|
|
39
|
+
|
|
40
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
41
|
+
INPUT_JSON="$1"
|
|
42
|
+
shift || true
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
while [[ $# -gt 0 ]]; do
|
|
46
|
+
case "$1" in
|
|
47
|
+
--task|-t) TASK="${2:-}"; shift 2 ;;
|
|
48
|
+
--task-file) TASK_FILE="${2:-}"; shift 2 ;;
|
|
49
|
+
--cwd|-C) CWD_ARG="${2:-}"; shift 2 ;;
|
|
50
|
+
--read-only) READ_ONLY="true"; shift ;;
|
|
51
|
+
--max-steps) MAX_STEPS="${2:-}"; shift 2 ;;
|
|
52
|
+
--model|-m) MODEL="${2:-}"; shift 2 ;;
|
|
53
|
+
--json|-j) INPUT_JSON="${2:-}"; shift 2 ;;
|
|
54
|
+
--help|-h) print_usage; exit 0 ;;
|
|
55
|
+
--) shift; break ;;
|
|
56
|
+
*)
|
|
57
|
+
if [[ -z "$INPUT_JSON" && ${1:0:1} == '{' ]]; then
|
|
58
|
+
INPUT_JSON="$1"; shift
|
|
59
|
+
else
|
|
60
|
+
unknown_arg_exit "$1"
|
|
61
|
+
fi
|
|
62
|
+
;;
|
|
63
|
+
esac
|
|
64
|
+
done
|
|
65
|
+
|
|
66
|
+
if [ -z "$INPUT_JSON" ] && [ -z "$TASK" ] && [ -z "$TASK_FILE" ] && [ ! -t 0 ]; then
|
|
67
|
+
STDIN_DATA="$(cat)"
|
|
68
|
+
if [[ ${STDIN_DATA:0:1} == '{' ]]; then
|
|
69
|
+
INPUT_JSON="$STDIN_DATA"
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
if [ -n "$INPUT_JSON" ]; then
|
|
74
|
+
INPUT=$(read_json_input "$INPUT_JSON")
|
|
75
|
+
[ -z "$TASK" ] && TASK=$(printf '%s' "$INPUT" | jq -r '.task // empty')
|
|
76
|
+
[ -z "$CWD_ARG" ] && CWD_ARG=$(printf '%s' "$INPUT" | jq -r '.cwd // empty')
|
|
77
|
+
[ "$READ_ONLY" = "false" ] && READ_ONLY=$(printf '%s' "$INPUT" | jq -r 'if .readOnly == true then "true" else "false" end')
|
|
78
|
+
[ -z "$MAX_STEPS" ] && MAX_STEPS=$(printf '%s' "$INPUT" | jq -r '.maxSteps // empty')
|
|
79
|
+
[ -z "$MODEL" ] && MODEL=$(printf '%s' "$INPUT" | jq -r '.model // empty')
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [ -n "$TASK_FILE" ]; then
|
|
83
|
+
[ -f "$TASK_FILE" ] || error_exit "task file not found: $TASK_FILE"
|
|
84
|
+
TASK="$(cat "$TASK_FILE")"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
require_param "task (--task)" "$TASK"
|
|
88
|
+
|
|
89
|
+
if [ -z "$CWD_ARG" ]; then
|
|
90
|
+
CWD_ARG="${CREWLY_PROJECT_PATH:-$PWD}"
|
|
91
|
+
fi
|
|
92
|
+
case "$CWD_ARG" in
|
|
93
|
+
/*) ;;
|
|
94
|
+
*) error_exit "cwd must be an absolute path, got: $CWD_ARG" ;;
|
|
95
|
+
esac
|
|
96
|
+
[ -d "$CWD_ARG" ] || error_exit "cwd is not a directory: $CWD_ARG"
|
|
97
|
+
|
|
98
|
+
if [ -n "$MAX_STEPS" ] && ! [[ "$MAX_STEPS" =~ ^[0-9]+$ ]]; then
|
|
99
|
+
error_exit "--max-steps must be a whole number, got: $MAX_STEPS"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
BODY=$(jq -nc \
|
|
103
|
+
--arg task "$TASK" \
|
|
104
|
+
--arg cwd "$CWD_ARG" \
|
|
105
|
+
--argjson readOnly "$READ_ONLY" \
|
|
106
|
+
--arg maxSteps "$MAX_STEPS" \
|
|
107
|
+
--arg model "$MODEL" \
|
|
108
|
+
'{task: $task, cwd: $cwd, readOnly: $readOnly}
|
|
109
|
+
+ (if $maxSteps != "" then {maxSteps: ($maxSteps | tonumber)} else {} end)
|
|
110
|
+
+ (if $model != "" then {model: $model} else {} end)')
|
|
111
|
+
|
|
112
|
+
# The backend wraps the outcome in {success, data}; the agent wants the outcome.
|
|
113
|
+
OUT=$(CREWLY_API_MAX_TIME="$SUBAGENT_CURL_MAX_TIME" api_call POST "/subagents" "$BODY")
|
|
114
|
+
printf '%s' "$OUT" | jq -c '.data // .'
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for spawn-subagent — run with: bash execute.test.sh
|
|
3
|
+
# Python HTTP stub as the backend; asserts the request the skill sends and
|
|
4
|
+
# the JSON it prints. Separate HOME / CREWLY_HOME. Exit 0 on pass.
|
|
5
|
+
set -u
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
EXEC="$HERE/execute.sh"
|
|
8
|
+
PASS=0; FAIL=0
|
|
9
|
+
check() {
|
|
10
|
+
local name="$1"; local got="$2"; local want="$3"
|
|
11
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
PORT=18851
|
|
15
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
import json, os
|
|
19
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
20
|
+
OUTCOME = {'success': True, 'stoppedReason': 'done', 'summary': 'retry delay at src/slack.ts:42 = 2000',
|
|
21
|
+
'toolCalls': 2, 'toolNames': ['grep', 'read_file'], 'filesChanged': [],
|
|
22
|
+
'usage': {'input': 100, 'output': 20, 'cachedInput': 0}, 'steps': 3,
|
|
23
|
+
'sessionName': 'dev-ella/sub-1', 'durationMs': 1500, 'model': 'deepseek/deepseek-chat'}
|
|
24
|
+
class H(BaseHTTPRequestHandler):
|
|
25
|
+
def _send(self, code, obj):
|
|
26
|
+
self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
27
|
+
self.wfile.write(json.dumps(obj).encode())
|
|
28
|
+
def do_GET(self): self._send(200, {'success': True})
|
|
29
|
+
def do_POST(self):
|
|
30
|
+
n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
|
|
31
|
+
data = json.loads(body) if body else None
|
|
32
|
+
with open(LOG, 'a') as f:
|
|
33
|
+
f.write(json.dumps({'method': 'POST', 'path': self.path, 'body': data,
|
|
34
|
+
'session': self.headers.get('X-Agent-Session')}) + '\n')
|
|
35
|
+
if self.path != '/api/subagents':
|
|
36
|
+
return self._send(200, {'success': True})
|
|
37
|
+
task = (data or {}).get('task', '')
|
|
38
|
+
if task.startswith('NESTED'):
|
|
39
|
+
return self._send(400, {'success': False, 'code': 'nested', 'error': 'A sub-agent cannot spawn sub-agents.'})
|
|
40
|
+
if task.startswith('TIMEOUT'):
|
|
41
|
+
out = dict(OUTCOME); out.update({'success': False, 'stoppedReason': 'timeout', 'toolCalls': 0, 'error': 'sub-agent exceeded 600000 ms'})
|
|
42
|
+
return self._send(200, {'success': True, 'data': out})
|
|
43
|
+
self._send(200, {'success': True, 'data': OUTCOME})
|
|
44
|
+
def log_message(self, *a, **k): pass
|
|
45
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
46
|
+
PY
|
|
47
|
+
STUB_PID=$!
|
|
48
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
49
|
+
WORK="$(mktemp -d)"; mkdir -p "$WORK/home/.crewly" "$WORK/repo"
|
|
50
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -rf "$STUB_LOG" "$WORK"' EXIT
|
|
51
|
+
for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
|
|
52
|
+
|
|
53
|
+
ENVS=(CREWLY_API_URL="http://127.0.0.1:${PORT}" CREWLY_SESSION_NAME=dev-ella HOME="$WORK/home" CREWLY_HOME="$WORK/home/.crewly" CREWLY_PROJECT_PATH="$WORK/repo")
|
|
54
|
+
# stdin closed: the skill reads JSON from a pipe when no task is given, and a
|
|
55
|
+
# test runner's stdin is a pipe that never ends.
|
|
56
|
+
run() { : > "$STUB_LOG"; env "${ENVS[@]}" bash "$EXEC" "$@" 2>/dev/null </dev/null; }
|
|
57
|
+
run_err() { : > "$STUB_LOG"; env "${ENVS[@]}" bash "$EXEC" "$@" 2>&1 >/dev/null </dev/null; }
|
|
58
|
+
# The skill library's start-of-skill heartbeat is not part of what we test.
|
|
59
|
+
reqs() { jq -c 'select(.path != "/api/heartbeat") | {path, session, body}' "$STUB_LOG" | paste -sd'|' -; }
|
|
60
|
+
|
|
61
|
+
# 1. Minimal call: task only; cwd defaults to CREWLY_PROJECT_PATH; readOnly false; no optionals.
|
|
62
|
+
OUT=$(run --task "Where is the retry delay set?")
|
|
63
|
+
check "minimal: request" "$(reqs)" "{\"path\":\"/api/subagents\",\"session\":\"dev-ella\",\"body\":{\"task\":\"Where is the retry delay set?\",\"cwd\":\"$WORK/repo\",\"readOnly\":false}}"
|
|
64
|
+
check "minimal: prints the outcome unwrapped" "$(printf '%s' "$OUT" | jq -c '{success, stoppedReason, toolCalls, sessionName}')" '{"success":true,"stoppedReason":"done","toolCalls":2,"sessionName":"dev-ella/sub-1"}'
|
|
65
|
+
|
|
66
|
+
# 2. Every flag.
|
|
67
|
+
OUT=$(run --task "Fix it" --cwd "$WORK/repo" --read-only --max-steps 12 --model deepseek/deepseek-chat)
|
|
68
|
+
check "all flags" "$(jq -c 'select(.path == "/api/subagents") | .body' "$STUB_LOG")" "{\"task\":\"Fix it\",\"cwd\":\"$WORK/repo\",\"readOnly\":true,\"maxSteps\":12,\"model\":\"deepseek/deepseek-chat\"}"
|
|
69
|
+
|
|
70
|
+
# 3. Legacy JSON form.
|
|
71
|
+
OUT=$(run "{\"task\":\"From JSON\",\"cwd\":\"$WORK/repo\",\"readOnly\":true,\"maxSteps\":5}")
|
|
72
|
+
check "json form" "$(jq -c 'select(.path == "/api/subagents") | .body' "$STUB_LOG")" "{\"task\":\"From JSON\",\"cwd\":\"$WORK/repo\",\"readOnly\":true,\"maxSteps\":5}"
|
|
73
|
+
|
|
74
|
+
# 4. Task from a file keeps quotes and newlines.
|
|
75
|
+
printf 'Line one with "quotes"\nLine two\n' > "$WORK/task.md"
|
|
76
|
+
run --task-file "$WORK/task.md" >/dev/null
|
|
77
|
+
check "task file" "$(jq -r 'select(.path == "/api/subagents") | .body.task' "$STUB_LOG")" "$(printf 'Line one with "quotes"\nLine two\n')"
|
|
78
|
+
|
|
79
|
+
# 5. cwd falls back to PWD when CREWLY_PROJECT_PATH is unset.
|
|
80
|
+
: > "$STUB_LOG"
|
|
81
|
+
(cd "$WORK/repo" && env CREWLY_API_URL="http://127.0.0.1:${PORT}" CREWLY_SESSION_NAME=dev-ella HOME="$WORK/home" CREWLY_HOME="$WORK/home/.crewly" bash "$EXEC" --task "pwd default" >/dev/null 2>&1 </dev/null)
|
|
82
|
+
check "cwd defaults to PWD" "$(jq -r 'select(.path == "/api/subagents") | .body.cwd' "$STUB_LOG")" "$WORK/repo"
|
|
83
|
+
|
|
84
|
+
# 6. Refusals before any request.
|
|
85
|
+
OUT=$(run_err --cwd "$WORK/repo" || true)
|
|
86
|
+
check "task required" "$(printf '%s' "$OUT" | grep -c 'Missing required parameter: task')" "1"
|
|
87
|
+
check "task required: nothing sent" "$(reqs)" ""
|
|
88
|
+
OUT=$(run_err --task x --cwd relative/dir || true)
|
|
89
|
+
check "cwd must be absolute" "$(printf '%s' "$OUT" | grep -c 'must be an absolute path')" "1"
|
|
90
|
+
OUT=$(run_err --task x --cwd "$WORK/missing" || true)
|
|
91
|
+
check "cwd must exist" "$(printf '%s' "$OUT" | grep -c 'not a directory')" "1"
|
|
92
|
+
OUT=$(run_err --task x --max-steps many || true)
|
|
93
|
+
check "max-steps numeric" "$(printf '%s' "$OUT" | grep -c 'whole number')" "1"
|
|
94
|
+
OUT=$(run_err --task x --bogus || true)
|
|
95
|
+
check "unknown flag" "$(printf '%s' "$OUT" | grep -c 'Unknown argument: --bogus')" "1"
|
|
96
|
+
|
|
97
|
+
# 7. A backend refusal is an error with its code; exit non-zero.
|
|
98
|
+
OUT=$(run_err --task "NESTED please" || true)
|
|
99
|
+
check "backend refusal surfaces the code" "$(printf '%s' "$OUT" | jq -r 'select(.error == true) | .details.code' 2>/dev/null | head -1)" "nested"
|
|
100
|
+
set +e; run --task "NESTED please" >/dev/null 2>&1; RC=$?; set -e
|
|
101
|
+
check "backend refusal exits 1" "$RC" "1"
|
|
102
|
+
|
|
103
|
+
# 8. A child that timed out is still a 200 outcome with success:false.
|
|
104
|
+
OUT=$(run --task "TIMEOUT me")
|
|
105
|
+
check "timeout outcome passes through" "$(printf '%s' "$OUT" | jq -c '{success, stoppedReason, error}')" '{"success":false,"stoppedReason":"timeout","error":"sub-agent exceeded 600000 ms"}'
|
|
106
|
+
|
|
107
|
+
# 9. --help prints usage and sends nothing.
|
|
108
|
+
OUT=$(run --help)
|
|
109
|
+
check "help" "$(printf '%s' "$OUT" | grep -c -- '--read-only')" "2"
|
|
110
|
+
check "help sends nothing" "$(reqs)" ""
|
|
111
|
+
|
|
112
|
+
echo "spawn-subagent: $PASS passed, $FAIL failed"
|
|
113
|
+
[ "$FAIL" -eq 0 ]
|