crewly 1.20.453 → 1.20.454
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-end/SKILL.md +86 -0
- package/config/skills/agent/checkup-end/end.mjs +339 -0
- package/config/skills/agent/checkup-end/execute.sh +39 -0
- package/config/skills/agent/checkup-end/execute.test.sh +134 -0
- package/config/skills/agent/checkup-report/SKILL.md +8 -0
- package/config/skills/agent/ingest-exports/SKILL.md +2 -2
- package/config/templates/small-business-checkup.json +1 -1
- package/dist/backend/build-info.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Check-up End
|
|
3
|
+
description: End a small-business check-up. Deletes every file the check-up produced on this machine (ingested exports, the analysis with its list of people, the report and daily documents, the intake answers, the time log, the App page copy, and optionally the raw CSV exports) and writes a receipt the owner can keep that says what was deleted, what was kept and what is still to do (unpublish the App, revoke any access). --dry-run lists without deleting.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: productivity
|
|
6
|
+
skillType: claude-skill
|
|
7
|
+
assignableRoles:
|
|
8
|
+
- "*"
|
|
9
|
+
triggers:
|
|
10
|
+
- end the check-up
|
|
11
|
+
- check-up is over
|
|
12
|
+
- delete check-up data
|
|
13
|
+
- wipe the check-up
|
|
14
|
+
- check-up receipt
|
|
15
|
+
tags:
|
|
16
|
+
- check-up
|
|
17
|
+
- small-business
|
|
18
|
+
- privacy
|
|
19
|
+
- cleanup
|
|
20
|
+
execution:
|
|
21
|
+
type: script
|
|
22
|
+
script:
|
|
23
|
+
file: execute.sh
|
|
24
|
+
interpreter: bash
|
|
25
|
+
timeoutMs: 60000
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# Check-up End
|
|
29
|
+
|
|
30
|
+
The last step of the small-business check-up (crewly#1055). The check-up
|
|
31
|
+
leaves rows of people on this machine: the ingested exports, `breaks.json`
|
|
32
|
+
with every person behind every break, the report document, the intake
|
|
33
|
+
answers. When the check-up is over (the 7 daily lines are posted, or the
|
|
34
|
+
owner says stop), delete all of it and give the owner a receipt.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bash execute.sh --dir . --business "Oak Pottery" --app <appId> # from the project the team worked in
|
|
38
|
+
bash execute.sh --dir . --exports ~/Downloads/oak-exports # also the raw CSVs the owner handed over
|
|
39
|
+
bash execute.sh --dir . --dry-run # list what would go, delete nothing
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## What it deletes
|
|
43
|
+
|
|
44
|
+
Only the files the check-up skills are known to write, in the folders the
|
|
45
|
+
team template uses under `--dir` (override any with a flag):
|
|
46
|
+
|
|
47
|
+
| Folder / file | Flag | Files |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `ingest/` | `--ingest` | `*.json` from `ingest-exports` (rows of people), `summary.json`, `unknown-report.json` |
|
|
50
|
+
| `analysis/` | `--analysis` | `breaks.json`, `report.md` from `checkup-analyse` |
|
|
51
|
+
| `checkup-data/` (or `data/`) | `--data` | `report-doc.json`, `daily-YYYY-MM-DD.json`, `timelog.json` from `checkup-report` |
|
|
52
|
+
| `checkup-app/` | `--app-dir` | `index.html` (the App page copy) |
|
|
53
|
+
| `intake.json` | `--intake` | the answers read with `app-data --get intake answers` |
|
|
54
|
+
| raw exports | `--exports <dir>` | `*.csv` in that folder, not recursive; only when you pass it |
|
|
55
|
+
|
|
56
|
+
Anything else in those folders (a stray file, a sub-folder, a symbolic link)
|
|
57
|
+
is **left alone and listed in the receipt as a leftover**. A folder that ends
|
|
58
|
+
up empty is removed. If none of `ingest/summary.json`, `analysis/breaks.json`,
|
|
59
|
+
`report-doc.json`, `timelog.json` or `intake.json` exists the skill refuses
|
|
60
|
+
(exit 2) and deletes nothing, so it cannot be pointed at the wrong folder.
|
|
61
|
+
|
|
62
|
+
Run `--dry-run` first when you are not sure what is there; it writes the same
|
|
63
|
+
receipt marked as a dry run.
|
|
64
|
+
|
|
65
|
+
## The receipt
|
|
66
|
+
|
|
67
|
+
`checkup-end-receipt.json` and `checkup-end-receipt.md` in `--dir` (or
|
|
68
|
+
`--receipt <dir>`): the business, when, every file deleted with its kind,
|
|
69
|
+
size and whether it held people data, whether names were hashed at ingest,
|
|
70
|
+
the leftovers, and **Still to do**. Send the owner the `.md` or say the same
|
|
71
|
+
in plain words: what was deleted, that nothing about their customers stays on
|
|
72
|
+
this machine, and what you are doing next.
|
|
73
|
+
|
|
74
|
+
## Still to do — the two things this skill cannot do itself
|
|
75
|
+
|
|
76
|
+
1. **Unpublish the App.** The report, the intake answers and the daily lines
|
|
77
|
+
live in the App until it is deleted. Run `publish-app --app <appId> --delete`
|
|
78
|
+
(pass `--app` here so the receipt carries the exact command). A public App
|
|
79
|
+
needs `--private` first.
|
|
80
|
+
2. **Revoke access.** Crewly stored no key or login for the apps named in the
|
|
81
|
+
intake (this check-up runs on exports); if the owner handed you a key, a
|
|
82
|
+
login or a read-only share, tell them to revoke it in that app. The receipt
|
|
83
|
+
lists the apps from the intake answers.
|
|
84
|
+
|
|
85
|
+
Never delete before the owner has the report: the 7 daily lines come first,
|
|
86
|
+
or the owner's word that the check-up is over.
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// checkup-end — the last step of the small-business check-up (crewly#1055):
|
|
4
|
+
// delete every file the check-up produced on this machine (ingested exports,
|
|
5
|
+
// the analysis with its list of people, the report and daily documents, the
|
|
6
|
+
// intake answers, the time log, the copied App page, and optionally the raw
|
|
7
|
+
// CSV exports the owner handed over) and write a receipt that says what was
|
|
8
|
+
// deleted, what was kept, and what the lead still has to do (unpublish the
|
|
9
|
+
// App, revoke any access the owner granted).
|
|
10
|
+
//
|
|
11
|
+
// node end.mjs --dir <project> [--business "Oak Pottery"] [--app <appId>] [--exports <csv dir>] [--dry-run]
|
|
12
|
+
// node end.mjs --dir . --ingest ./ingest --analysis ./analysis --data ./checkup-data --app-dir ./checkup-app --intake ./intake.json
|
|
13
|
+
//
|
|
14
|
+
// Only the files the check-up skills are known to write are deleted; anything
|
|
15
|
+
// else found in those folders is listed in the receipt as a leftover and left
|
|
16
|
+
// alone. Symbolic links are never followed. Exit 0 = done (or dry run),
|
|
17
|
+
// 2 = the folder does not look like a check-up, 1 = usage.
|
|
18
|
+
// =============================================================================
|
|
19
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, lstatSync, unlinkSync, rmdirSync } from 'node:fs';
|
|
20
|
+
import { join, resolve, relative, basename } from 'node:path';
|
|
21
|
+
|
|
22
|
+
/** Where the check-up team writes each kind of file, relative to the project (`--dir`). */
|
|
23
|
+
export const DEFAULT_LAYOUT = {
|
|
24
|
+
ingest: ['ingest'],
|
|
25
|
+
analysis: ['analysis'],
|
|
26
|
+
data: ['checkup-data', 'data'],
|
|
27
|
+
appDir: ['checkup-app'],
|
|
28
|
+
intake: ['intake.json'],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const RECEIPT_JSON = 'checkup-end-receipt.json';
|
|
32
|
+
const RECEIPT_MD = 'checkup-end-receipt.md';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What each check-up skill writes, by folder: file-name test and what the
|
|
36
|
+
* receipt calls it. Only these are deleted.
|
|
37
|
+
*/
|
|
38
|
+
const KNOWN = {
|
|
39
|
+
ingest: [
|
|
40
|
+
{ test: (f) => f === 'summary.json', kind: 'ingest summary', people: false },
|
|
41
|
+
{ test: (f) => f === 'unknown-report.json', kind: 'ingest note (unmapped file)', people: true },
|
|
42
|
+
{ test: (f) => f.endsWith('.json'), kind: 'ingested export (rows of people)', people: true },
|
|
43
|
+
],
|
|
44
|
+
analysis: [
|
|
45
|
+
{ test: (f) => f === 'breaks.json', kind: 'analysis (every break with its people)', people: true },
|
|
46
|
+
{ test: (f) => f === 'report.md', kind: 'analysis report', people: true },
|
|
47
|
+
],
|
|
48
|
+
data: [
|
|
49
|
+
{ test: (f) => f === 'report-doc.json', kind: 'App report document', people: true },
|
|
50
|
+
{ test: (f) => /^daily-\d{4}-\d{2}-\d{2}\.json$/.test(f), kind: 'daily summary', people: false },
|
|
51
|
+
{ test: (f) => f === 'timelog.json', kind: 'time log', people: false },
|
|
52
|
+
],
|
|
53
|
+
appDir: [
|
|
54
|
+
{ test: (f) => f === 'index.html', kind: 'App page copy', people: false },
|
|
55
|
+
],
|
|
56
|
+
exports: [
|
|
57
|
+
{ test: (f) => /\.csv$/i.test(f), kind: 'raw export (owner-provided CSV)', people: true },
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Files that mark a folder as a check-up; at least one must exist or nothing is touched. */
|
|
62
|
+
const MARKERS = [
|
|
63
|
+
['ingest', 'summary.json'],
|
|
64
|
+
['analysis', 'breaks.json'],
|
|
65
|
+
['data', 'report-doc.json'],
|
|
66
|
+
['data', 'timelog.json'],
|
|
67
|
+
['intake', null],
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/** Resolve a folder/file for one slot: the explicit flag, else the first default that exists under `--dir`. */
|
|
71
|
+
function resolveSlot(o, slot) {
|
|
72
|
+
if (o[slot]) return resolve(o[slot]);
|
|
73
|
+
for (const candidate of DEFAULT_LAYOUT[slot]) {
|
|
74
|
+
const p = resolve(o.dir, candidate);
|
|
75
|
+
if (existsSync(p)) return p;
|
|
76
|
+
}
|
|
77
|
+
return resolve(o.dir, DEFAULT_LAYOUT[slot][0]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** True when the path exists and is not a symbolic link (links are never followed). */
|
|
81
|
+
function isRealFile(p) {
|
|
82
|
+
try {
|
|
83
|
+
const st = lstatSync(p);
|
|
84
|
+
return st.isFile();
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isRealDir(p) {
|
|
91
|
+
try {
|
|
92
|
+
return lstatSync(p).isDirectory();
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Walk one folder (not recursive) and split its entries into the files the
|
|
100
|
+
* check-up wrote (to delete) and everything else (to leave and report).
|
|
101
|
+
*/
|
|
102
|
+
function inventoryDir(dir, rules, root) {
|
|
103
|
+
const toDelete = [];
|
|
104
|
+
const leftovers = [];
|
|
105
|
+
if (!isRealDir(dir)) return { toDelete, leftovers, present: false };
|
|
106
|
+
for (const name of readdirSync(dir).sort()) {
|
|
107
|
+
const p = join(dir, name);
|
|
108
|
+
const st = lstatSync(p);
|
|
109
|
+
const rel = relative(root, p) || name;
|
|
110
|
+
if (st.isSymbolicLink()) { leftovers.push({ path: rel, note: 'symbolic link, not followed' }); continue; }
|
|
111
|
+
if (st.isDirectory()) { leftovers.push({ path: rel, note: 'folder, not entered' }); continue; }
|
|
112
|
+
const rule = rules.find((r) => r.test(name));
|
|
113
|
+
if (rule) toDelete.push({ path: rel, abs: p, bytes: st.size, kind: rule.kind, people: rule.people });
|
|
114
|
+
else leftovers.push({ path: rel, note: 'not written by a check-up skill' });
|
|
115
|
+
}
|
|
116
|
+
return { toDelete, leftovers, present: true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Read the intake answers (plain document or app-data's `{ success, data }`) for the apps to revoke; missing → null. */
|
|
120
|
+
function readIntakeApps(path) {
|
|
121
|
+
if (!isRealFile(path)) return null;
|
|
122
|
+
try {
|
|
123
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
124
|
+
const doc = raw && raw.data && typeof raw.data === 'object' && !Array.isArray(raw.data) ? raw.data : raw;
|
|
125
|
+
return Array.isArray(doc.apps) ? doc.apps.filter((a) => typeof a === 'string' && a.trim()).map((a) => a.trim()) : [];
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Whether ingest-exports hashed the names (then the stored rows held no name, email or phone). */
|
|
132
|
+
function readNamesHashed(ingestDir) {
|
|
133
|
+
const p = join(ingestDir, 'summary.json');
|
|
134
|
+
if (!isRealFile(p)) return null;
|
|
135
|
+
try {
|
|
136
|
+
return Boolean(JSON.parse(readFileSync(p, 'utf8')).namesHashed);
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build the end-of-check-up plan: every file to delete, the leftovers, the
|
|
144
|
+
* App and access the lead still has to deal with. Deletes nothing.
|
|
145
|
+
*/
|
|
146
|
+
export function planEnd(o) {
|
|
147
|
+
const root = resolve(o.dir);
|
|
148
|
+
const slots = {
|
|
149
|
+
ingest: resolveSlot(o, 'ingest'),
|
|
150
|
+
analysis: resolveSlot(o, 'analysis'),
|
|
151
|
+
data: resolveSlot(o, 'data'),
|
|
152
|
+
appDir: resolveSlot(o, 'appDir'),
|
|
153
|
+
intake: resolveSlot(o, 'intake'),
|
|
154
|
+
};
|
|
155
|
+
const looksLikeCheckup = MARKERS.some(([slot, file]) => (file ? isRealFile(join(slots[slot], file)) : isRealFile(slots[slot])));
|
|
156
|
+
const toDelete = [];
|
|
157
|
+
const leftovers = [];
|
|
158
|
+
const folders = [];
|
|
159
|
+
for (const slot of ['ingest', 'analysis', 'data', 'appDir']) {
|
|
160
|
+
const inv = inventoryDir(slots[slot], KNOWN[slot], root);
|
|
161
|
+
toDelete.push(...inv.toDelete);
|
|
162
|
+
leftovers.push(...inv.leftovers);
|
|
163
|
+
if (inv.present) folders.push(slots[slot]);
|
|
164
|
+
}
|
|
165
|
+
if (o.exports) {
|
|
166
|
+
const dir = resolve(o.exports);
|
|
167
|
+
const inv = inventoryDir(dir, KNOWN.exports, root);
|
|
168
|
+
toDelete.push(...inv.toDelete);
|
|
169
|
+
leftovers.push(...inv.leftovers);
|
|
170
|
+
if (inv.present) folders.push(dir);
|
|
171
|
+
}
|
|
172
|
+
if (isRealFile(slots.intake)) {
|
|
173
|
+
toDelete.push({ path: relative(root, slots.intake) || basename(slots.intake), abs: slots.intake, bytes: lstatSync(slots.intake).size, kind: 'intake answers', people: false });
|
|
174
|
+
}
|
|
175
|
+
const apps = readIntakeApps(slots.intake);
|
|
176
|
+
const namesHashed = readNamesHashed(slots.ingest);
|
|
177
|
+
return { root, slots, looksLikeCheckup, toDelete, leftovers, folders, apps, namesHashed };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The steps this skill cannot do itself, in the order the lead should take them. */
|
|
181
|
+
function todoFor(o, plan) {
|
|
182
|
+
const todo = [];
|
|
183
|
+
if (o.app) {
|
|
184
|
+
todo.push({
|
|
185
|
+
step: 'unpublish the App',
|
|
186
|
+
why: 'the report, the intake answers and the daily lines live in the App until it is deleted',
|
|
187
|
+
command: `publish-app --app ${o.app} --delete`,
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
todo.push({
|
|
191
|
+
step: 'unpublish the App',
|
|
192
|
+
why: 'the report, the intake answers and the daily lines live in the App until it is deleted (pass --app <appId> to put the exact command here)',
|
|
193
|
+
command: 'publish-app --app <appId> --delete',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const apps = plan.apps && plan.apps.length ? plan.apps : [];
|
|
197
|
+
todo.push({
|
|
198
|
+
step: 'revoke access',
|
|
199
|
+
why: apps.length
|
|
200
|
+
? `the owner named ${apps.join(', ')}; Crewly stored no key or login for them in this check-up (exports only), so any key, login or read-only share the owner handed over is revoked in that app`
|
|
201
|
+
: 'any key, login or read-only share the owner handed over for the check-up is revoked in that app; Crewly stored none',
|
|
202
|
+
apps,
|
|
203
|
+
});
|
|
204
|
+
return todo;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The receipt as Markdown the lead can paste to the owner. */
|
|
208
|
+
function receiptMarkdown(r) {
|
|
209
|
+
const lines = [];
|
|
210
|
+
lines.push(`# Check-up ended${r.business ? ` — ${r.business}` : ''}`);
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push(`Ended at ${r.endedAt}${r.dryRun ? ' (dry run: nothing was deleted)' : ''}.`);
|
|
213
|
+
lines.push('');
|
|
214
|
+
lines.push(`## Deleted (${r.deleted.length} file${r.deleted.length === 1 ? '' : 's'}, ${r.bytes.toLocaleString('en-US')} bytes)`);
|
|
215
|
+
lines.push('');
|
|
216
|
+
if (!r.deleted.length) lines.push('- nothing: no check-up files were found');
|
|
217
|
+
for (const f of r.deleted) lines.push(`- \`${f.path}\` — ${f.kind}${f.people ? ' (held people data)' : ''}`);
|
|
218
|
+
lines.push('');
|
|
219
|
+
if (r.namesHashed === true) lines.push('Names, emails and phones were hashed when the exports were ingested; the stored rows held none.');
|
|
220
|
+
else if (r.namesHashed === false) lines.push('The ingested rows held names as exported; they are gone with the files above.');
|
|
221
|
+
lines.push('');
|
|
222
|
+
lines.push('## Kept');
|
|
223
|
+
lines.push('');
|
|
224
|
+
lines.push(`- this receipt (\`${r.receipt.json}\`, \`${r.receipt.md}\`)`);
|
|
225
|
+
for (const l of r.leftovers) lines.push(`- \`${l.path}\` — ${l.note}`);
|
|
226
|
+
lines.push('');
|
|
227
|
+
lines.push('## Still to do');
|
|
228
|
+
lines.push('');
|
|
229
|
+
for (const t of r.todo) {
|
|
230
|
+
lines.push(`- **${t.step}**: ${t.why}${t.command ? ` — \`${t.command}\`` : ''}`);
|
|
231
|
+
}
|
|
232
|
+
lines.push('');
|
|
233
|
+
return lines.join('\n');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Delete the planned files, then remove any check-up folder left empty. Returns what was actually removed. */
|
|
237
|
+
function applyPlan(plan) {
|
|
238
|
+
const deleted = [];
|
|
239
|
+
const failed = [];
|
|
240
|
+
for (const f of plan.toDelete) {
|
|
241
|
+
try {
|
|
242
|
+
unlinkSync(f.abs);
|
|
243
|
+
deleted.push(f);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
failed.push({ path: f.path, error: e.message });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const removedFolders = [];
|
|
249
|
+
for (const dir of plan.folders) {
|
|
250
|
+
try {
|
|
251
|
+
if (isRealDir(dir) && readdirSync(dir).length === 0) {
|
|
252
|
+
rmdirSync(dir);
|
|
253
|
+
removedFolders.push(relative(plan.root, dir) || dir);
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
/* a folder that will not go is simply kept */
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { deleted, failed, removedFolders };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function out(obj, code) {
|
|
263
|
+
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
264
|
+
process.exit(code);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function usage() {
|
|
268
|
+
out({ success: false, error: 'usage: end.mjs --dir <project> [--business <name>] [--app <appId>] [--exports <csv dir>] [--ingest <dir>] [--analysis <dir>] [--data <dir>] [--app-dir <dir>] [--intake <file>] [--receipt <dir>] [--dry-run]' }, 1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function main(argv) {
|
|
272
|
+
const o = {};
|
|
273
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
274
|
+
const a = argv[i];
|
|
275
|
+
const next = () => { i += 1; if (i >= argv.length) usage(); return argv[i]; };
|
|
276
|
+
if (a === '--dir') o.dir = next();
|
|
277
|
+
else if (a === '--business') o.business = next();
|
|
278
|
+
else if (a === '--app') o.app = next();
|
|
279
|
+
else if (a === '--exports') o.exports = next();
|
|
280
|
+
else if (a === '--ingest') o.ingest = next();
|
|
281
|
+
else if (a === '--analysis') o.analysis = next();
|
|
282
|
+
else if (a === '--data') o.data = next();
|
|
283
|
+
else if (a === '--app-dir') o.appDir = next();
|
|
284
|
+
else if (a === '--intake') o.intake = next();
|
|
285
|
+
else if (a === '--receipt') o.receipt = next();
|
|
286
|
+
else if (a === '--dry-run') o.dryRun = true;
|
|
287
|
+
else usage();
|
|
288
|
+
}
|
|
289
|
+
if (!o.dir) usage();
|
|
290
|
+
if (!isRealDir(o.dir)) out({ success: false, error: `${o.dir}: not a folder` }, 2);
|
|
291
|
+
if (o.exports && !isRealDir(o.exports)) out({ success: false, error: `${o.exports}: not a folder` }, 2);
|
|
292
|
+
|
|
293
|
+
const plan = planEnd(o);
|
|
294
|
+
if (!plan.looksLikeCheckup) {
|
|
295
|
+
out({
|
|
296
|
+
success: false,
|
|
297
|
+
error: `${plan.root} does not look like a check-up folder (no ingest/summary.json, analysis/breaks.json, report-doc.json, timelog.json or intake.json); nothing deleted`,
|
|
298
|
+
}, 2);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const endedAt = new Date().toISOString();
|
|
302
|
+
const receiptDir = resolve(o.receipt || plan.root);
|
|
303
|
+
const receipt = { json: join(receiptDir, RECEIPT_JSON), md: join(receiptDir, RECEIPT_MD) };
|
|
304
|
+
const result = o.dryRun ? { deleted: plan.toDelete, failed: [], removedFolders: [] } : applyPlan(plan);
|
|
305
|
+
const bytes = result.deleted.reduce((n, f) => n + f.bytes, 0);
|
|
306
|
+
const r = {
|
|
307
|
+
schemaVersion: '1.0',
|
|
308
|
+
business: o.business || null,
|
|
309
|
+
endedAt,
|
|
310
|
+
dryRun: Boolean(o.dryRun),
|
|
311
|
+
root: plan.root,
|
|
312
|
+
deleted: result.deleted.map(({ path, bytes: b, kind, people }) => ({ path, bytes: b, kind, people })),
|
|
313
|
+
bytes,
|
|
314
|
+
failed: result.failed,
|
|
315
|
+
removedFolders: result.removedFolders,
|
|
316
|
+
leftovers: plan.leftovers,
|
|
317
|
+
namesHashed: plan.namesHashed,
|
|
318
|
+
todo: todoFor(o, plan),
|
|
319
|
+
receipt,
|
|
320
|
+
};
|
|
321
|
+
mkdirSync(receiptDir, { recursive: true });
|
|
322
|
+
writeFileSync(receipt.json, JSON.stringify(r, null, 2));
|
|
323
|
+
writeFileSync(receipt.md, receiptMarkdown(r));
|
|
324
|
+
|
|
325
|
+
out({
|
|
326
|
+
success: result.failed.length === 0,
|
|
327
|
+
dryRun: r.dryRun,
|
|
328
|
+
deleted: r.deleted.length,
|
|
329
|
+
bytes,
|
|
330
|
+
peopleFiles: r.deleted.filter((f) => f.people).length,
|
|
331
|
+
leftovers: r.leftovers.length,
|
|
332
|
+
failed: result.failed,
|
|
333
|
+
receipt,
|
|
334
|
+
next: r.todo.map((t) => t.command || `${t.step}: ${t.why}`),
|
|
335
|
+
}, result.failed.length ? 2 : 0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
|
|
339
|
+
if (isMain) main(process.argv.slice(2));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# checkup-end — end a small-business check-up (crewly#1055): delete every
|
|
4
|
+
# file the check-up produced on this machine and write a receipt that says
|
|
5
|
+
# what was deleted, what was kept and what the lead still has to do
|
|
6
|
+
# (unpublish the App, revoke any access the owner granted).
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# bash execute.sh --dir . --business "Oak Pottery" --app <appId> # the project the team worked in
|
|
10
|
+
# bash execute.sh --dir . --exports ~/Downloads/oak-exports # also the raw CSVs the owner handed over
|
|
11
|
+
# bash execute.sh --dir . --dry-run # list what would go, delete nothing
|
|
12
|
+
# bash execute.sh '{"dir":".","business":"Oak Pottery","app":"28au74d9cj","dryRun":true}'
|
|
13
|
+
#
|
|
14
|
+
# Output: one JSON line (files deleted, bytes, leftovers, receipt paths, the
|
|
15
|
+
# commands still to run). Exit 0 = done, 2 = not a check-up folder or a file
|
|
16
|
+
# would not delete, 1 = usage.
|
|
17
|
+
# =============================================================================
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
END="${SCRIPT_DIR}/end.mjs"
|
|
21
|
+
|
|
22
|
+
command -v node >/dev/null 2>&1 || { printf '{"success":false,"error":"node is required but not on PATH"}\n'; exit 1; }
|
|
23
|
+
|
|
24
|
+
ARGS=()
|
|
25
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
26
|
+
command -v jq >/dev/null 2>&1 || { printf '{"success":false,"error":"jq is required to read JSON input"}\n'; exit 1; }
|
|
27
|
+
for key in dir business app exports ingest analysis data appDir intake receipt; do
|
|
28
|
+
v=$(printf '%s' "$1" | jq -r --arg k "$key" '.[$k] // empty')
|
|
29
|
+
if [ -n "$v" ]; then
|
|
30
|
+
case "$key" in appDir) flag=--app-dir ;; *) flag="--$key" ;; esac
|
|
31
|
+
ARGS+=("$flag" "$v")
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
[ "$(printf '%s' "$1" | jq -r '.dryRun // false')" = "true" ] && ARGS+=(--dry-run)
|
|
35
|
+
shift
|
|
36
|
+
fi
|
|
37
|
+
ARGS+=("$@")
|
|
38
|
+
|
|
39
|
+
exec node "$END" ${ARGS[@]+"${ARGS[@]}"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for checkup-end — run with: bash execute.test.sh
|
|
3
|
+
# Offline: builds a check-up folder with ingest-exports + checkup-analyse + checkup-report first. Needs node and jq.
|
|
4
|
+
set -u
|
|
5
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
EXEC="$HERE/execute.sh"
|
|
7
|
+
INGEST="$HERE/../ingest-exports/execute.sh"
|
|
8
|
+
ANALYSE="$HERE/../checkup-analyse/execute.sh"
|
|
9
|
+
REPORT="$HERE/../checkup-report/execute.sh"
|
|
10
|
+
FX="$HERE/../ingest-exports/fixtures"
|
|
11
|
+
PASS=0; FAIL=0
|
|
12
|
+
check() {
|
|
13
|
+
local name="$1"; local got="$2"; local want="$3"
|
|
14
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
|
|
15
|
+
}
|
|
16
|
+
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
|
17
|
+
|
|
18
|
+
# A check-up laid out the way the team template works: project P with ingest/, analysis/, checkup-data/, checkup-app/, intake.json
|
|
19
|
+
build_checkup() {
|
|
20
|
+
local P="$1"; local hash="${2:-}"
|
|
21
|
+
mkdir -p "$P"
|
|
22
|
+
if [ -n "$hash" ]; then bash "$INGEST" --in "$FX" --out "$P/ingest" --hash-names >/dev/null; else bash "$INGEST" --in "$FX" --out "$P/ingest" >/dev/null; fi
|
|
23
|
+
bash "$ANALYSE" --in "$P/ingest" --as-of 2026-10-26 --out "$P/analysis" >/dev/null
|
|
24
|
+
cat > "$P/intake.json" <<'EOF'
|
|
25
|
+
{"apps":["Square","Jotform","Jackrabbit"],"slowestTask":"copying sign-ups","fears":[],"appsPerCustomer":"3","firstNumber":"Money owed to me","savedAt":"2026-10-26T10:00:00.000Z"}
|
|
26
|
+
EOF
|
|
27
|
+
bash "$REPORT" --log-step intake --start --out "$P/checkup-data" >/dev/null
|
|
28
|
+
bash "$REPORT" --log-step intake --end --out "$P/checkup-data" >/dev/null
|
|
29
|
+
bash "$REPORT" --breaks "$P/analysis/breaks.json" --business "Oak Pottery" --intake "$P/intake.json" --out "$P/checkup-data" >/dev/null
|
|
30
|
+
bash "$REPORT" --daily "Replied to 3 inquiries." --date 2026-10-27 --out "$P/checkup-data" >/dev/null
|
|
31
|
+
bash "$REPORT" --app-dir "$P/checkup-app" >/dev/null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# 1. Dry run: lists everything, deletes nothing, receipt marked dryRun
|
|
35
|
+
build_checkup "$T/p1"
|
|
36
|
+
BEFORE=$(find "$T/p1" -type f | wc -l | tr -d ' ')
|
|
37
|
+
OUT="$(bash "$EXEC" --dir "$T/p1" --business "Oak Pottery" --app 28au74d9cj --dry-run)"; RC=$?
|
|
38
|
+
check "dry run exits 0" "$RC" "0"
|
|
39
|
+
check "dry run success" "$(printf '%s' "$OUT" | jq -r '.success')" "true"
|
|
40
|
+
check "dry run flagged" "$(printf '%s' "$OUT" | jq -r '.dryRun')" "true"
|
|
41
|
+
check "dry run counts the files" "$(printf '%s' "$OUT" | jq -r '.deleted >= 12')" "true"
|
|
42
|
+
AFTER=$(find "$T/p1" -type f -not -name 'checkup-end-receipt.*' | wc -l | tr -d ' ')
|
|
43
|
+
check "dry run deleted nothing" "$AFTER" "$BEFORE"
|
|
44
|
+
check "dry run still has breaks.json" "$([ -f "$T/p1/analysis/breaks.json" ] && echo yes || echo no)" "yes"
|
|
45
|
+
R="$T/p1/checkup-end-receipt.json"
|
|
46
|
+
check "receipt json written" "$([ -f "$R" ] && echo yes || echo no)" "yes"
|
|
47
|
+
check "receipt md written" "$([ -f "$T/p1/checkup-end-receipt.md" ] && echo yes || echo no)" "yes"
|
|
48
|
+
check "receipt dryRun" "$(jq -r '.dryRun' "$R")" "true"
|
|
49
|
+
check "receipt business" "$(jq -r '.business' "$R")" "Oak Pottery"
|
|
50
|
+
check "receipt next = unpublish with the app id" "$(printf '%s' "$OUT" | jq -r '.next[0]')" "publish-app --app 28au74d9cj --delete"
|
|
51
|
+
check "receipt lists the apps to revoke" "$(jq -c '.todo[1].apps' "$R")" '["Square","Jotform","Jackrabbit"]'
|
|
52
|
+
check "receipt says names were not hashed" "$(jq -r '.namesHashed' "$R")" "false"
|
|
53
|
+
check "md mentions dry run" "$(grep -c 'dry run' "$T/p1/checkup-end-receipt.md")" "1"
|
|
54
|
+
|
|
55
|
+
# 2. Real run: known files gone, empty folders removed, leftovers kept and listed
|
|
56
|
+
build_checkup "$T/p2" hash
|
|
57
|
+
echo "owner note" > "$T/p2/analysis/notes.txt" # not written by a check-up skill → leftover
|
|
58
|
+
mkdir -p "$T/p2/ingest/raw" # a sub-folder → leftover
|
|
59
|
+
ln -s "$T/p2/intake.json" "$T/p2/checkup-data/link.json" # a symlink → never followed
|
|
60
|
+
OUT="$(bash "$EXEC" --dir "$T/p2" --business "Oak Pottery")"; RC=$?
|
|
61
|
+
check "end exits 0" "$RC" "0"
|
|
62
|
+
check "end success" "$(printf '%s' "$OUT" | jq -r '.success')" "true"
|
|
63
|
+
check "end not a dry run" "$(printf '%s' "$OUT" | jq -r '.dryRun')" "false"
|
|
64
|
+
check "breaks.json gone" "$([ -e "$T/p2/analysis/breaks.json" ] && echo still || echo gone)" "gone"
|
|
65
|
+
check "ingested rows gone" "$(ls "$T/p2/ingest" 2>/dev/null | grep -c '\.json$')" "0"
|
|
66
|
+
check "report-doc gone" "$([ -e "$T/p2/checkup-data/report-doc.json" ] && echo still || echo gone)" "gone"
|
|
67
|
+
check "daily gone" "$([ -e "$T/p2/checkup-data/daily-2026-10-27.json" ] && echo still || echo gone)" "gone"
|
|
68
|
+
check "timelog gone" "$([ -e "$T/p2/checkup-data/timelog.json" ] && echo still || echo gone)" "gone"
|
|
69
|
+
check "intake gone" "$([ -e "$T/p2/intake.json" ] && echo still || echo gone)" "gone"
|
|
70
|
+
check "app page copy gone" "$([ -e "$T/p2/checkup-app/index.html" ] && echo still || echo gone)" "gone"
|
|
71
|
+
check "empty checkup-app folder removed" "$([ -d "$T/p2/checkup-app" ] && echo still || echo gone)" "gone"
|
|
72
|
+
check "leftover note kept" "$([ -f "$T/p2/analysis/notes.txt" ] && echo yes || echo no)" "yes"
|
|
73
|
+
check "leftover folder kept" "$([ -d "$T/p2/ingest/raw" ] && echo yes || echo no)" "yes"
|
|
74
|
+
check "symlink kept (not followed)" "$([ -L "$T/p2/checkup-data/link.json" ] && echo yes || echo no)" "yes"
|
|
75
|
+
check "non-empty folders kept" "$([ -d "$T/p2/analysis" ] && [ -d "$T/p2/ingest" ] && echo yes || echo no)" "yes"
|
|
76
|
+
R="$T/p2/checkup-end-receipt.json"
|
|
77
|
+
check "receipt leftovers = 3" "$(jq -r '.leftovers | length' "$R")" "3"
|
|
78
|
+
check "receipt leftover note reason" "$(jq -r '.leftovers[] | select(.path=="analysis/notes.txt") | .note' "$R")" "not written by a check-up skill"
|
|
79
|
+
check "receipt symlink reason" "$(jq -r '.leftovers[] | select(.path=="checkup-data/link.json") | .note' "$R")" "symbolic link, not followed"
|
|
80
|
+
check "receipt names hashed" "$(jq -r '.namesHashed' "$R")" "true"
|
|
81
|
+
check "receipt people files counted" "$(printf '%s' "$OUT" | jq -r '.peopleFiles >= 8')" "true"
|
|
82
|
+
check "receipt bytes > 0" "$(jq -r '.bytes > 0' "$R")" "true"
|
|
83
|
+
check "receipt removed folders lists checkup-app" "$(jq -r '.removedFolders | index("checkup-app") != null' "$R")" "true"
|
|
84
|
+
check "receipt next without --app shows placeholder" "$(printf '%s' "$OUT" | jq -r '.next[0]')" "publish-app --app <appId> --delete"
|
|
85
|
+
check "md lists deleted kinds" "$(grep -c 'held people data' "$T/p2/checkup-end-receipt.md" | awk '{print ($1>=5)?"yes":"no"}')" "yes"
|
|
86
|
+
check "md hashed note" "$(grep -c 'were hashed' "$T/p2/checkup-end-receipt.md")" "1"
|
|
87
|
+
|
|
88
|
+
# 3. Raw exports folder: only *.csv in it, not recursive
|
|
89
|
+
build_checkup "$T/p3"
|
|
90
|
+
mkdir -p "$T/exports/sub"
|
|
91
|
+
cp "$FX"/*.csv "$T/exports/"
|
|
92
|
+
echo "x" > "$T/exports/readme.txt"
|
|
93
|
+
cp "$FX/square-transactions.csv" "$T/exports/sub/"
|
|
94
|
+
N_CSV=$(ls "$T/exports"/*.csv | wc -l | tr -d ' ')
|
|
95
|
+
OUT="$(bash "$EXEC" --dir "$T/p3" --exports "$T/exports")"; RC=$?
|
|
96
|
+
check "exports exits 0" "$RC" "0"
|
|
97
|
+
check "csv exports gone" "$(ls "$T/exports" | grep -c '\.csv$')" "0"
|
|
98
|
+
check "readme kept" "$([ -f "$T/exports/readme.txt" ] && echo yes || echo no)" "yes"
|
|
99
|
+
check "sub-folder csv kept" "$([ -f "$T/exports/sub/square-transactions.csv" ] && echo yes || echo no)" "yes"
|
|
100
|
+
check "receipt counts the csvs" "$(jq -r --argjson n "$N_CSV" '[.deleted[] | select(.kind | startswith("raw export"))] | length == $n' "$T/p3/checkup-end-receipt.json")" "true"
|
|
101
|
+
|
|
102
|
+
# 4. Refuses a folder that is not a check-up, deletes nothing
|
|
103
|
+
mkdir -p "$T/notme/analysis"; echo "keep" > "$T/notme/analysis/report.md"; echo "keep" > "$T/notme/other.txt"
|
|
104
|
+
OUT="$(bash "$EXEC" --dir "$T/notme")"; RC=$?
|
|
105
|
+
check "not a check-up exits 2" "$RC" "2"
|
|
106
|
+
check "not a check-up error" "$(printf '%s' "$OUT" | jq -r '.error' | grep -c 'does not look like a check-up')" "1"
|
|
107
|
+
check "not a check-up kept report.md" "$([ -f "$T/notme/analysis/report.md" ] && echo yes || echo no)" "yes"
|
|
108
|
+
check "not a check-up wrote no receipt" "$([ -e "$T/notme/checkup-end-receipt.json" ] && echo yes || echo no)" "no"
|
|
109
|
+
|
|
110
|
+
# 5. Missing folder, usage, JSON input, explicit slot flags and --receipt
|
|
111
|
+
OUT="$(bash "$EXEC" --dir "$T/nope")"; RC=$?
|
|
112
|
+
check "missing dir exits 2" "$RC" "2"
|
|
113
|
+
OUT="$(bash "$EXEC")"; RC=$?
|
|
114
|
+
check "no args exits 1" "$RC" "1"
|
|
115
|
+
OUT="$(bash "$EXEC" --dir "$T/p3" --bogus)"; RC=$?
|
|
116
|
+
check "unknown flag exits 1" "$RC" "1"
|
|
117
|
+
build_checkup "$T/p5"
|
|
118
|
+
mv "$T/p5/checkup-data" "$T/p5/elsewhere"
|
|
119
|
+
OUT="$(bash "$EXEC" "{\"dir\":\"$T/p5\",\"data\":\"$T/p5/elsewhere\",\"business\":\"Oak Pottery\",\"app\":\"abc123\",\"receipt\":\"$T/p5-receipt\",\"dryRun\":true}")"; RC=$?
|
|
120
|
+
check "json exits 0" "$RC" "0"
|
|
121
|
+
check "json dry run" "$(printf '%s' "$OUT" | jq -r '.dryRun')" "true"
|
|
122
|
+
check "json app in next" "$(printf '%s' "$OUT" | jq -r '.next[0]')" "publish-app --app abc123 --delete"
|
|
123
|
+
check "json --data slot found the moved folder" "$(jq -r '[.deleted[] | select(.path=="elsewhere/report-doc.json")] | length' "$T/p5-receipt/checkup-end-receipt.json")" "1"
|
|
124
|
+
check "json receipt in --receipt dir" "$([ -f "$T/p5-receipt/checkup-end-receipt.md" ] && echo yes || echo no)" "yes"
|
|
125
|
+
check "json receipt not in project" "$([ -e "$T/p5/checkup-end-receipt.json" ] && echo yes || echo no)" "no"
|
|
126
|
+
|
|
127
|
+
# 6. Ending twice: the second run finds nothing and says so
|
|
128
|
+
build_checkup "$T/p6"
|
|
129
|
+
bash "$EXEC" --dir "$T/p6" >/dev/null
|
|
130
|
+
OUT="$(bash "$EXEC" --dir "$T/p6")"; RC=$?
|
|
131
|
+
check "second end refuses (nothing left to mark a check-up)" "$RC" "2"
|
|
132
|
+
|
|
133
|
+
echo "checkup-end: $PASS passed, $FAIL failed"
|
|
134
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -106,6 +106,14 @@ and any step still open). A second `--start` on an open step restarts it;
|
|
|
106
106
|
`--end` with no open entry fails (exit 2) so a forgotten start is noticed.
|
|
107
107
|
`--at <iso>` back-fills a time you know. One `daily` entry per day is fine.
|
|
108
108
|
|
|
109
|
+
## End — when the 7 days are over
|
|
110
|
+
|
|
111
|
+
Run `checkup-end --dir . --business "<Business>" --app <appId>`: it deletes
|
|
112
|
+
every file the check-up left on this machine (ingested rows, `breaks.json`,
|
|
113
|
+
the report and daily documents, the intake answers, the time log, the App
|
|
114
|
+
page copy) and writes a receipt. Then `publish-app --app <appId> --delete`
|
|
115
|
+
and send the owner the receipt. See that skill.
|
|
116
|
+
|
|
109
117
|
## Report back
|
|
110
118
|
|
|
111
119
|
Tell the owner in plain words: the App is ready (the card has the link), the
|
|
@@ -88,8 +88,8 @@ Children's names, parents' emails and phone numbers are in most of these
|
|
|
88
88
|
exports. Run with `--hash-names` unless the analysis truly needs the names:
|
|
89
89
|
`name`, `email`, `phone` and every original column that looks personal become
|
|
90
90
|
a short stable hash (`h:3f1a…`), so the same person still counts once across
|
|
91
|
-
files but nobody's name is stored. Report counts, not names, and
|
|
92
|
-
|
|
91
|
+
files but nobody's name is stored. Report counts, not names, and run
|
|
92
|
+
`checkup-end` when the check-up is over so the `--out` folder goes with a receipt.
|
|
93
93
|
|
|
94
94
|
## Report back
|
|
95
95
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"name": "Checkup Lead",
|
|
8
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."
|
|
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'. End: after the seventh daily line, or when the owner says the check-up is over, run 'checkup-end --dir . --business <name> --app <appId>' (--dry-run first if unsure; add --exports <dir> when the owner handed over CSV files) so every ingested row, the analysis and the report leave this machine, then 'publish-app --app <appId> --delete', and send the owner the receipt it wrote in plain words. 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
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"name": "Data Analyst",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crewly",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.454",
|
|
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": [
|