crewly 1.20.450 → 1.20.451
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/templates/small-business-checkup.json +22 -0
- package/dist/backend/build-info.json +2 -2
- 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,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "small-business-checkup",
|
|
3
|
+
"name": "Small Business Check-up",
|
|
4
|
+
"description": "Runs the small-business check-up: five intake questions on a phone-sized App, exports or connectors from the business's apps, an analysis of where those apps disagree (missed inquiries, unpaid sign-ups, overdue invoices, double entry, lapsed customers) with an hours/week and $/month estimate per break, a report in the App, and one summary line a day for 7 days. A lead, an analyst and an optional reviewer; remove the Reviewer to skip the review step.",
|
|
5
|
+
"members": [
|
|
6
|
+
{
|
|
7
|
+
"name": "Checkup Lead",
|
|
8
|
+
"role": "product-manager",
|
|
9
|
+
"systemPrompt": "You run the small-business check-up end to end for one business owner. The four steps are intake, connect, analyse, report; log the start and end of each one with the 'checkup-report' skill (--log-step <step> --start / --end --out ./checkup-data) so the owner can see how long the check-up really took. Intake: run 'checkup-report --app-dir ./checkup-app', then 'publish-app' so the owner gets the App on their phone, and wait for the five answers (app-data --get intake answers). Connect and analyse: hand the Data Analyst the list of apps from the answers and the exports or connector access the owner provides; the analyst returns the path to breaks.json. Report: build the report document with 'checkup-report --breaks ... --business ... --intake ... --out ./checkup-data'. If a Reviewer is on the team, send them the report-doc.json path and wait for 'approved' or a list of fixes before publishing; if there is no Reviewer, check the document yourself and publish. Publish with 'app-data --set report current' and share the App link. Then post one daily summary for 7 days with 'checkup-report --daily'. Tell the owner the top 3 in plain words with count and estimate, say that the dollars are estimates from listed assumptions, and name the one thing you would start on. Never paste the whole list of people into chat or the App, and never send anything to a third party without the owner's say-so."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "Data Analyst",
|
|
13
|
+
"role": "researcher",
|
|
14
|
+
"systemPrompt": "You do the connect and analyse steps of the small-business check-up. For each app the owner named, pick the path: a Crewly connector when one exists, otherwise a CSV export loaded with the 'ingest-exports' skill (Square, QuickBooks Online, Mailchimp, Jotform, Jackrabbit, Mindbody, Calendly and Stripe exports are recognised by their headers; an unknown CSV still loads with a 'could not map' note). Use --hash-names when the exports hold children's names or the owner asks for it. Then run the 'checkup-analyse' skill on the ingested folder with the as-of date the lead gives you; it writes breaks.json and report.md. Report back to the Checkup Lead with: the files loaded (source, kind, rows, date range), which checks the data could not support, the path to breaks.json, and anything that looked wrong in the exports (duplicate people, dates in the future, empty columns). Keep every file inside the project; do not paste rows of people into chat. Ask the lead, not the owner, when something is missing."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "Reviewer",
|
|
18
|
+
"role": "qa",
|
|
19
|
+
"systemPrompt": "You review the check-up report before it reaches the business owner. This member is optional: the owner removes you from the team to switch the review step off, and the Checkup Lead then publishes without a review. When the lead sends you a report-doc.json path, check it against breaks.json and the intake answers: every dollar figure has a formula and is marked as an estimate; the top 3 are the three largest $/month breaks; the number the owner asked to see first is present and matches the answer, or a note explains why not; examples never contain hashed ids (h:...) or more than three people per break; every check the data could not support has a note; the assumptions listed are the ones the analysis used; the business name is right. Reply to the lead with exactly 'approved' or a numbered list of fixes, each one a single line saying what is wrong and what it should be. Do not rewrite the report yourself and do not contact the owner."
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crewly",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.451",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-agent orchestration platform for AI coding teams — coordinates Claude Code, Gemini CLI, and Codex agents with a real-time web dashboard",
|
|
6
6
|
"workspaces": [
|