@thecolonylab/hivemind 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/coverage.js +5 -3
- package/dist/commands/coverageLocal.js +1 -1
- package/dist/commands/create.js +1 -1
- package/dist/commands/explore.js +5 -2
- package/dist/commands/init.js +70 -3
- package/dist/commands/remoteExecutor.js +43 -0
- package/dist/commands/report.js +10 -3
- package/dist/commands/run.js +11 -8
- package/dist/commands/runLocal.js +6 -1
- package/dist/report/artifacts.js +6 -3
- package/dist/serverClient.js +6 -6
- package/package.json +2 -2
- package/skill/AGENTS.md.template +3 -2
- package/skill/hivemind-cli/SKILL.md +12 -1
|
@@ -6,6 +6,7 @@ import { checkCoverageLocally } from './coverageLocal.js';
|
|
|
6
6
|
import { saveTestFile, slugify } from '../testFile/write.js';
|
|
7
7
|
import { loadAllTestsFull } from '../testFile/loadAllTests.js';
|
|
8
8
|
import { renderCoveragePrComment } from '../report/prComment.js';
|
|
9
|
+
import { loadConfig } from '../config.js';
|
|
9
10
|
/**
|
|
10
11
|
* The actual diff-aware check: reads the real code diff (not just filenames), sends it — plus
|
|
11
12
|
* every local test with its full steps — to the server. The server figures out which known
|
|
@@ -23,6 +24,7 @@ export async function coverageCommand(opts) {
|
|
|
23
24
|
}
|
|
24
25
|
const diff = getFullDiff(cwd, base);
|
|
25
26
|
const localTests = await loadAllTestsFull(cwd);
|
|
27
|
+
const config = await loadConfig(cwd);
|
|
26
28
|
const serverUrl = process.env.HIVEMIND_SERVER_URL || 'https://app.thecolonylab.com';
|
|
27
29
|
console.log(`🐝 Scout reading the diff (${changedFiles.length} file${changedFiles.length === 1 ? '' : 's'} changed vs ${base})...`);
|
|
28
30
|
if (opts.local) {
|
|
@@ -31,15 +33,15 @@ export async function coverageCommand(opts) {
|
|
|
31
33
|
let result;
|
|
32
34
|
try {
|
|
33
35
|
result = opts.local
|
|
34
|
-
? await checkCoverageLocally({ serverUrl, diff, localTests, headed: opts.headed })
|
|
35
|
-
: await coverageOnServer(diff, localTests);
|
|
36
|
+
? await checkCoverageLocally({ serverUrl, diff, localTests, headed: opts.headed, projectId: config.projectId, projectName: config.projectName })
|
|
37
|
+
: await coverageOnServer(diff, localTests, config.projectId, config.projectName);
|
|
36
38
|
}
|
|
37
39
|
catch (err) {
|
|
38
40
|
console.error(err.message);
|
|
39
41
|
process.exitCode = 2;
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
|
-
// The server only knows its own path (e.g. /
|
|
44
|
+
// The server only knows its own path (e.g. /runs/...) — it has no idea what URL you're
|
|
43
45
|
// reaching it at (localhost in dev, a real domain in prod), so the full link gets built here.
|
|
44
46
|
for (const page of result.affectedPages) {
|
|
45
47
|
for (const t of page.testsRun) {
|
|
@@ -21,7 +21,7 @@ export async function checkCoverageLocally(opts) {
|
|
|
21
21
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
22
22
|
const socket = new WebSocket(wsUrl, { headers });
|
|
23
23
|
socket.on('open', () => {
|
|
24
|
-
socket.send(JSON.stringify({ type: 'start', diff: opts.diff, localTests: opts.localTests }));
|
|
24
|
+
socket.send(JSON.stringify({ type: 'start', diff: opts.diff, localTests: opts.localTests, projectId: opts.projectId, projectName: opts.projectName }));
|
|
25
25
|
});
|
|
26
26
|
socket.on('message', async (raw) => {
|
|
27
27
|
let msg;
|
package/dist/commands/create.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function createCommand(request, opts) {
|
|
|
10
10
|
const accountLabels = Object.keys(config.accounts || {});
|
|
11
11
|
let proposed;
|
|
12
12
|
try {
|
|
13
|
-
proposed = await createOnServer(request, opts.url, existingTests, accountLabels);
|
|
13
|
+
proposed = await createOnServer(request, opts.url, existingTests, accountLabels, config.projectId);
|
|
14
14
|
}
|
|
15
15
|
catch (err) {
|
|
16
16
|
console.error(err.message);
|
package/dist/commands/explore.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { exploreOnServer } from '../serverClient.js';
|
|
3
3
|
import { saveTestFile, slugify } from '../testFile/write.js';
|
|
4
4
|
import { listExistingTests } from '../testFile/listExisting.js';
|
|
5
|
+
import { loadConfig } from '../config.js';
|
|
5
6
|
/**
|
|
6
7
|
* A real multi-page crawl, not a one-shot snapshot — the server maps pages, follows doors, and
|
|
7
8
|
* proposes starter tests along the way. Existing tests are sent along (same pattern `create`
|
|
@@ -9,10 +10,12 @@ import { listExistingTests } from '../testFile/listExisting.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export async function exploreCommand(url, opts) {
|
|
11
12
|
console.log(`🐝 Scout dispatched to ${url}...`);
|
|
12
|
-
const
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const existingTests = await listExistingTests(cwd);
|
|
15
|
+
const config = await loadConfig(cwd);
|
|
13
16
|
let proposed;
|
|
14
17
|
try {
|
|
15
|
-
proposed = await exploreOnServer(url, existingTests);
|
|
18
|
+
proposed = await exploreOnServer(url, existingTests, config.projectId, config.projectName);
|
|
16
19
|
}
|
|
17
20
|
catch (err) {
|
|
18
21
|
console.error(err.message);
|
package/dist/commands/init.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { dirname, join, basename } from 'node:path';
|
|
3
3
|
import { mkdir, readFile, writeFile, copyFile, access } from 'node:fs/promises';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
4
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
9
|
// dist/commands/init.js -> package root (skill/ ships alongside dist/, see package.json "files")
|
|
6
10
|
const packageRoot = join(__dirname, '..', '..');
|
|
@@ -9,6 +13,53 @@ async function exists(path) {
|
|
|
9
13
|
.then(() => true)
|
|
10
14
|
.catch(() => false);
|
|
11
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Cloud mode (the default) never touches Playwright on this machine at all — the browser lives on
|
|
18
|
+
* the server. But `--local` mode does, and the starter hivemind.config.json defaults to
|
|
19
|
+
* localhost:3000, so most fresh projects are headed toward --local sooner or later. Checking here
|
|
20
|
+
* means the first `--local` run doesn't stall on an unexpected multi-minute download mid-test —
|
|
21
|
+
* `chromium.executablePath()` is a cheap path lookup, not a launch, so this stays fast when the
|
|
22
|
+
* browser's already installed (every run after the first).
|
|
23
|
+
*/
|
|
24
|
+
async function ensureLocalBrowser() {
|
|
25
|
+
const { chromium } = await import('playwright');
|
|
26
|
+
const installed = await exists(chromium.executablePath());
|
|
27
|
+
if (installed) {
|
|
28
|
+
console.log('✓ local browser (for --local mode) already installed');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log('⧗ downloading local browser for --local mode (one-time, ~150MB)...');
|
|
32
|
+
try {
|
|
33
|
+
await execFileAsync('npx', ['playwright', 'install', 'chromium']);
|
|
34
|
+
console.log('✓ local browser installed');
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.log(`- couldn't auto-install the local browser (${err?.message || err}) — run \`npx playwright install chromium\` yourself before using --local`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const GITIGNORE_BLOCK = `# HIVEMIND:START
|
|
41
|
+
# hivemind local state — real evidence (screenshots, run history, folded-in memory), but not
|
|
42
|
+
# meant to be reviewed in a PR the way hive/tests/ is. Regenerated by every run, safe to delete.
|
|
43
|
+
hive/history/
|
|
44
|
+
hive/artifacts/
|
|
45
|
+
hive/memory.md
|
|
46
|
+
hive/last-run.json
|
|
47
|
+
# HIVEMIND:END
|
|
48
|
+
`;
|
|
49
|
+
/** One hive/ folder for everything — tests/ is the only part meant to be committed and reviewed;
|
|
50
|
+
* history/, artifacts/, memory.md, and last-run.json are local-only, excluded here rather than
|
|
51
|
+
* living in a separate dot-prefixed folder. */
|
|
52
|
+
async function ensureGitignore(cwd) {
|
|
53
|
+
const path = join(cwd, '.gitignore');
|
|
54
|
+
const existing = (await exists(path)) ? await readFile(path, 'utf-8') : '';
|
|
55
|
+
if (existing.includes('# HIVEMIND:START')) {
|
|
56
|
+
console.log('- .gitignore already has the hivemind block, left untouched');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const updated = existing ? `${existing.trimEnd()}\n\n${GITIGNORE_BLOCK}` : GITIGNORE_BLOCK;
|
|
60
|
+
await writeFile(path, updated, 'utf-8');
|
|
61
|
+
console.log(existing ? '✓ .gitignore updated so hive/history, artifacts, memory stay local-only' : '✓ .gitignore created so hive/history, artifacts, memory stay local-only');
|
|
62
|
+
}
|
|
12
63
|
/**
|
|
13
64
|
* One-time setup for a project adopting hivemind — matches TesterArmy's `ta agent init` pattern.
|
|
14
65
|
* Idempotent: safe to run again later, never overwrites something already there.
|
|
@@ -17,18 +68,33 @@ export async function initCommand() {
|
|
|
17
68
|
const cwd = process.cwd();
|
|
18
69
|
await mkdir(join(cwd, 'hive', 'tests'), { recursive: true });
|
|
19
70
|
console.log('✓ hive/tests/ ready');
|
|
71
|
+
await ensureGitignore(cwd);
|
|
20
72
|
const configPath = join(cwd, 'hivemind.config.json');
|
|
21
73
|
if (await exists(configPath)) {
|
|
22
|
-
|
|
74
|
+
// Patch in a projectId even for a config that predates this field — without it, every
|
|
75
|
+
// request to the server would look like it belongs to no project at all (or worse, get
|
|
76
|
+
// lumped in with other projects on the same account that also lack one).
|
|
77
|
+
const existing = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
78
|
+
if (existing.projectId) {
|
|
79
|
+
console.log('- hivemind.config.json already exists, left untouched');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
existing.projectId = randomBytes(8).toString('hex');
|
|
83
|
+
existing.projectName ??= basename(cwd);
|
|
84
|
+
await writeFile(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf-8');
|
|
85
|
+
console.log(`✓ hivemind.config.json — added a projectId (project name: "${existing.projectName}")`);
|
|
86
|
+
}
|
|
23
87
|
}
|
|
24
88
|
else {
|
|
25
89
|
const starter = {
|
|
90
|
+
projectId: randomBytes(8).toString('hex'),
|
|
91
|
+
projectName: basename(cwd),
|
|
26
92
|
environments: { default: 'http://localhost:3000' },
|
|
27
93
|
accounts: {},
|
|
28
94
|
areas: {},
|
|
29
95
|
};
|
|
30
96
|
await writeFile(configPath, JSON.stringify(starter, null, 2) + '\n', 'utf-8');
|
|
31
|
-
console.log(
|
|
97
|
+
console.log(`✓ hivemind.config.json created (project name: "${starter.projectName}") — edit environments.default to match your app`);
|
|
32
98
|
}
|
|
33
99
|
const agentsPath = join(cwd, 'AGENTS.md');
|
|
34
100
|
const template = await readFile(join(packageRoot, 'skill', 'AGENTS.md.template'), 'utf-8');
|
|
@@ -45,6 +111,7 @@ export async function initCommand() {
|
|
|
45
111
|
await mkdir(skillDir, { recursive: true });
|
|
46
112
|
await copyFile(join(packageRoot, 'skill', 'hivemind-cli', 'SKILL.md'), join(skillDir, 'SKILL.md'));
|
|
47
113
|
console.log('✓ .agents/skills/hivemind-cli/SKILL.md installed');
|
|
114
|
+
await ensureLocalBrowser();
|
|
48
115
|
console.log('\nDone. A coding agent working in this repo now knows how to use hivemind.');
|
|
49
116
|
console.log('Next: hivemind create "<what to test>" --url <your app url>');
|
|
50
117
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const MAX_BROWSER_LOGS = 200;
|
|
1
2
|
/**
|
|
2
3
|
* The Forager — runs on your machine, executes exactly what the Hivemind brain tells it to, one
|
|
3
4
|
* named tool call at a time. Same `execute(name, input)` shape used in-process on the server for
|
|
@@ -11,14 +12,55 @@ export class Forager {
|
|
|
11
12
|
runStart;
|
|
12
13
|
pageStack = [];
|
|
13
14
|
screenshots = [];
|
|
15
|
+
browserLogs = [];
|
|
14
16
|
constructor(page, runStart) {
|
|
15
17
|
this.page = page;
|
|
16
18
|
this.runStart = runStart;
|
|
19
|
+
this.attachLogging(page);
|
|
20
|
+
}
|
|
21
|
+
/** Same reasoning as the server's ToolExecutor: a click can succeed while the page underneath
|
|
22
|
+
* throws a JS error or a fetch 500s, and the report today has no way to show that. This is the
|
|
23
|
+
* ONLY side that ever sees these events for a --local run — the server never opened this
|
|
24
|
+
* browser — so every call's result carries the full accumulated log back over the socket
|
|
25
|
+
* (see execute() below); RemoteToolRunner just keeps the latest one it receives. */
|
|
26
|
+
attachLogging(page) {
|
|
27
|
+
const push = (entry) => {
|
|
28
|
+
if (this.browserLogs.length < MAX_BROWSER_LOGS)
|
|
29
|
+
this.browserLogs.push(entry);
|
|
30
|
+
};
|
|
31
|
+
page.on('console', (msg) => {
|
|
32
|
+
if (msg.type() === 'log' || msg.type() === 'debug')
|
|
33
|
+
return;
|
|
34
|
+
push({ type: 'console', level: msg.type(), text: msg.text().slice(0, 500), elapsedMs: Date.now() - this.runStart });
|
|
35
|
+
});
|
|
36
|
+
page.on('pageerror', (err) => {
|
|
37
|
+
push({ type: 'console', level: 'error', text: err.message.slice(0, 500), elapsedMs: Date.now() - this.runStart });
|
|
38
|
+
});
|
|
39
|
+
page.on('requestfailed', (req) => {
|
|
40
|
+
push({
|
|
41
|
+
type: 'network',
|
|
42
|
+
level: 'failed',
|
|
43
|
+
text: `${req.method()} ${req.url()} — ${req.failure()?.errorText || 'failed'}`.slice(0, 500),
|
|
44
|
+
elapsedMs: Date.now() - this.runStart,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
page.on('response', (res) => {
|
|
48
|
+
if (res.status() < 400)
|
|
49
|
+
return;
|
|
50
|
+
push({ type: 'network', level: String(res.status()), text: `${res.request().method()} ${res.url()} → ${res.status()}`.slice(0, 500), elapsedMs: Date.now() - this.runStart });
|
|
51
|
+
});
|
|
17
52
|
}
|
|
18
53
|
getScreenshots() {
|
|
19
54
|
return this.screenshots;
|
|
20
55
|
}
|
|
56
|
+
getBrowserLogs() {
|
|
57
|
+
return this.browserLogs;
|
|
58
|
+
}
|
|
21
59
|
async execute(name, input) {
|
|
60
|
+
const result = await this.executeInner(name, input);
|
|
61
|
+
return { ...result, browserLogs: this.browserLogs };
|
|
62
|
+
}
|
|
63
|
+
async executeInner(name, input) {
|
|
22
64
|
try {
|
|
23
65
|
switch (name) {
|
|
24
66
|
case 'browser_navigate': {
|
|
@@ -170,6 +212,7 @@ export class Forager {
|
|
|
170
212
|
await newPage.waitForLoadState('domcontentloaded', { timeout: 10_000 }).catch(() => { });
|
|
171
213
|
this.pageStack.push(prevPage);
|
|
172
214
|
this.page = newPage;
|
|
215
|
+
this.attachLogging(newPage);
|
|
173
216
|
newPage.on('close', () => {
|
|
174
217
|
if (this.page === newPage) {
|
|
175
218
|
const fallback = this.pageStack.pop();
|
package/dist/commands/report.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { exec } from 'node:child_process';
|
|
4
|
+
import { readApiKey } from '../credentials.js';
|
|
4
5
|
function serverUrl() {
|
|
5
6
|
return process.env.HIVEMIND_SERVER_URL || 'https://app.thecolonylab.com';
|
|
6
7
|
}
|
|
@@ -9,7 +10,7 @@ function open(url) {
|
|
|
9
10
|
exec(`${opener} "${url}"`);
|
|
10
11
|
}
|
|
11
12
|
export async function reportCommand() {
|
|
12
|
-
const pointerPath = join(process.cwd(), '
|
|
13
|
+
const pointerPath = join(process.cwd(), 'hive', 'last-run.json');
|
|
13
14
|
try {
|
|
14
15
|
const pointer = JSON.parse(await readFile(pointerPath, 'utf-8'));
|
|
15
16
|
console.log(`Opening ${pointer.reportUrl}`);
|
|
@@ -21,9 +22,15 @@ export async function reportCommand() {
|
|
|
21
22
|
}
|
|
22
23
|
export async function historyCommand(opts) {
|
|
23
24
|
const url = `${serverUrl()}/api/runs`;
|
|
24
|
-
const
|
|
25
|
+
const apiKey = await readApiKey();
|
|
26
|
+
const res = await fetch(url, apiKey ? { headers: { Authorization: `Bearer ${apiKey}` } } : undefined).catch(() => null);
|
|
25
27
|
if (!res || !res.ok) {
|
|
26
|
-
|
|
28
|
+
if (res?.status === 401) {
|
|
29
|
+
console.error(`Not signed in — run "hivemind login <key>" (get a key from the dashboard's Settings page).`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.error(`Could not reach hivemind server at ${serverUrl()}. Is it running? (hivemind-server)`);
|
|
33
|
+
}
|
|
27
34
|
process.exitCode = 2;
|
|
28
35
|
return;
|
|
29
36
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -24,15 +24,15 @@ async function runPool(items, limit, worker) {
|
|
|
24
24
|
}
|
|
25
25
|
async function readLocalMemory(cwd) {
|
|
26
26
|
try {
|
|
27
|
-
return await readFile(join(cwd, '
|
|
27
|
+
return await readFile(join(cwd, 'hive', 'memory.md'), 'utf-8');
|
|
28
28
|
}
|
|
29
29
|
catch {
|
|
30
30
|
return '';
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
async function writeLocalMemory(cwd, content) {
|
|
34
|
-
const path = join(cwd, '
|
|
35
|
-
await mkdir(join(cwd, '
|
|
34
|
+
const path = join(cwd, 'hive', 'memory.md');
|
|
35
|
+
await mkdir(join(cwd, 'hive'), { recursive: true });
|
|
36
36
|
await writeFile(path, content, 'utf-8');
|
|
37
37
|
}
|
|
38
38
|
export async function runCommand(target, opts) {
|
|
@@ -81,7 +81,7 @@ export async function runCommand(target, opts) {
|
|
|
81
81
|
console.error(`✗ ${test.name}: ${err.message}`);
|
|
82
82
|
results[index] = {
|
|
83
83
|
name: test.name,
|
|
84
|
-
result: { status: 'error', verdict: err.message, steps: [], toolLog: [], screenshots: [], durationMs: 0 },
|
|
84
|
+
result: { status: 'error', verdict: err.message, steps: [], toolLog: [], screenshots: [], browserLogs: [], durationMs: 0 },
|
|
85
85
|
};
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
@@ -95,7 +95,7 @@ export async function runCommand(target, opts) {
|
|
|
95
95
|
console.error(`✗ ${test.name}: ${message}`);
|
|
96
96
|
results[index] = {
|
|
97
97
|
name: test.name,
|
|
98
|
-
result: { status: 'error', verdict: message, steps: [], toolLog: [], screenshots: [], durationMs: 0 },
|
|
98
|
+
result: { status: 'error', verdict: message, steps: [], toolLog: [], screenshots: [], browserLogs: [], durationMs: 0 },
|
|
99
99
|
};
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
@@ -115,8 +115,9 @@ export async function runCommand(target, opts) {
|
|
|
115
115
|
memoryContext,
|
|
116
116
|
credential,
|
|
117
117
|
headed: opts.headed,
|
|
118
|
+
projectId: config.projectId,
|
|
118
119
|
});
|
|
119
|
-
await writeFile(join(cwd, '
|
|
120
|
+
await writeFile(join(cwd, 'hive', 'last-run.json'), JSON.stringify({ reportUrl: reportPath, name: test.name }, null, 2)).catch(() => { });
|
|
120
121
|
results[index] = { name: test.name, result, reportUrl: reportPath };
|
|
121
122
|
if (!opts.json) {
|
|
122
123
|
printResult(test.name, result);
|
|
@@ -130,10 +131,12 @@ export async function runCommand(target, opts) {
|
|
|
130
131
|
steps: resolvedSteps,
|
|
131
132
|
memoryContext,
|
|
132
133
|
credential,
|
|
134
|
+
projectId: config.projectId,
|
|
135
|
+
projectName: config.projectName,
|
|
133
136
|
});
|
|
134
137
|
const reportUrl = `${serverUrl}${reportPath}`;
|
|
135
138
|
await writeLocalMemory(cwd, updatedMemory).catch(() => { });
|
|
136
|
-
await writeFile(join(cwd, '
|
|
139
|
+
await writeFile(join(cwd, 'hive', 'last-run.json'), JSON.stringify({ reportUrl, name: test.name }, null, 2)).catch(() => { });
|
|
137
140
|
results[index] = { name: test.name, result, reportUrl };
|
|
138
141
|
if (!opts.json) {
|
|
139
142
|
printResult(test.name, result);
|
|
@@ -145,7 +148,7 @@ export async function runCommand(target, opts) {
|
|
|
145
148
|
console.error(`✗ ${test.name}: ${err.message}`);
|
|
146
149
|
results[index] = {
|
|
147
150
|
name: test.name,
|
|
148
|
-
result: { status: 'error', verdict: err.message, steps: [], toolLog: [], screenshots: [], durationMs: 0 },
|
|
151
|
+
result: { status: 'error', verdict: err.message, steps: [], toolLog: [], screenshots: [], browserLogs: [], durationMs: 0 },
|
|
149
152
|
};
|
|
150
153
|
}
|
|
151
154
|
});
|
|
@@ -40,6 +40,7 @@ export async function runTestLocally(opts) {
|
|
|
40
40
|
try {
|
|
41
41
|
const wsUrl = opts.serverUrl.replace(/^http/, 'ws') + '/ws/local-run';
|
|
42
42
|
const apiKey = await readApiKey();
|
|
43
|
+
let hostedReportUrl;
|
|
43
44
|
const result = await new Promise((resolve, reject) => {
|
|
44
45
|
const headers = { 'User-Agent': 'Hivemind-Forager/0.1.0' };
|
|
45
46
|
if (apiKey)
|
|
@@ -52,6 +53,7 @@ export async function runTestLocally(opts) {
|
|
|
52
53
|
url: opts.url,
|
|
53
54
|
steps: opts.steps,
|
|
54
55
|
memoryContext: opts.memoryContext,
|
|
56
|
+
projectId: opts.projectId,
|
|
55
57
|
credential: opts.credential,
|
|
56
58
|
}));
|
|
57
59
|
});
|
|
@@ -69,6 +71,7 @@ export async function runTestLocally(opts) {
|
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
71
73
|
if (msg.type === 'finish') {
|
|
74
|
+
hostedReportUrl = msg.reportUrl;
|
|
72
75
|
resolve(msg.result);
|
|
73
76
|
socket.close();
|
|
74
77
|
return;
|
|
@@ -89,7 +92,9 @@ export async function runTestLocally(opts) {
|
|
|
89
92
|
await session.close();
|
|
90
93
|
closed = true;
|
|
91
94
|
await writeRunArtifacts(opts.cwd, runId, opts.name, result);
|
|
92
|
-
|
|
95
|
+
// Prefer the real, hosted, shareable report (shows up in the dashboard's Runs list) — the
|
|
96
|
+
// local file written above is just a backup for offline viewing, same data either way.
|
|
97
|
+
const reportPath = hostedReportUrl ? `${opts.serverUrl}${hostedReportUrl}` : `${artifactDirFor(opts.cwd, runId)}/report.html`;
|
|
93
98
|
return { result, reportPath };
|
|
94
99
|
}
|
|
95
100
|
finally {
|
package/dist/report/artifacts.js
CHANGED
|
@@ -5,8 +5,11 @@ import { slugify } from '../testFile/write.js';
|
|
|
5
5
|
function timestampDir() {
|
|
6
6
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
7
7
|
}
|
|
8
|
+
/** Everything lives under one hive/ folder — tests/ is the committed part, history/ and
|
|
9
|
+
* artifacts/ are local-only and gitignored (see `hivemind init`'s .gitignore rules). One folder
|
|
10
|
+
* to look in instead of splitting state across hive/ and a separate .hive/. */
|
|
8
11
|
export function artifactDirFor(cwd, runId) {
|
|
9
|
-
return join(cwd, 'artifacts', runId);
|
|
12
|
+
return join(cwd, 'hive', 'artifacts', runId);
|
|
10
13
|
}
|
|
11
14
|
/** Creates the artifact directory for a run BEFORE it starts, so ToolExecutor can write screenshots into it. */
|
|
12
15
|
export async function createArtifactDir(cwd, testName) {
|
|
@@ -20,7 +23,7 @@ export async function writeRunArtifacts(cwd, runId, testName, result) {
|
|
|
20
23
|
const artifactDir = artifactDirFor(cwd, runId);
|
|
21
24
|
const html = renderHtmlReport(testName, result);
|
|
22
25
|
await writeFile(join(artifactDir, 'report.html'), html, 'utf-8');
|
|
23
|
-
const historyDir = join(cwd, '
|
|
26
|
+
const historyDir = join(cwd, 'hive', 'history');
|
|
24
27
|
await mkdir(historyDir, { recursive: true });
|
|
25
28
|
const entry = {
|
|
26
29
|
name: testName,
|
|
@@ -34,7 +37,7 @@ export async function writeRunArtifacts(cwd, runId, testName, result) {
|
|
|
34
37
|
return artifactDir;
|
|
35
38
|
}
|
|
36
39
|
export async function readHistory(cwd) {
|
|
37
|
-
const historyDir = join(cwd, '
|
|
40
|
+
const historyDir = join(cwd, 'hive', 'history');
|
|
38
41
|
try {
|
|
39
42
|
const files = await readdir(historyDir);
|
|
40
43
|
const entries = await Promise.all(files
|
package/dist/serverClient.js
CHANGED
|
@@ -29,14 +29,14 @@ async function post(path, body) {
|
|
|
29
29
|
export async function runOnServer(input) {
|
|
30
30
|
return post('/api/runs', input);
|
|
31
31
|
}
|
|
32
|
-
export async function createOnServer(request, url, existingTests, accountLabels) {
|
|
33
|
-
return post('/api/create', { request, url, existingTests, accountLabels });
|
|
32
|
+
export async function createOnServer(request, url, existingTests, accountLabels, projectId) {
|
|
33
|
+
return post('/api/create', { request, url, existingTests, accountLabels, projectId });
|
|
34
34
|
}
|
|
35
|
-
export async function exploreOnServer(url, existingTests) {
|
|
36
|
-
return post('/api/explore', { url, existingTests });
|
|
35
|
+
export async function exploreOnServer(url, existingTests, projectId, projectName) {
|
|
36
|
+
return post('/api/explore', { url, existingTests, projectId, projectName });
|
|
37
37
|
}
|
|
38
|
-
export async function coverageOnServer(diff, localTests) {
|
|
39
|
-
return post('/api/coverage', { diff, localTests });
|
|
38
|
+
export async function coverageOnServer(diff, localTests, projectId, projectName) {
|
|
39
|
+
return post('/api/coverage', { diff, localTests, projectId, projectName });
|
|
40
40
|
}
|
|
41
41
|
export async function getDraftTest(code) {
|
|
42
42
|
const apiKey = await readApiKey();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thecolonylab/hivemind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Forager — the CLI that runs on your machine and executes for the Hivemind brain. Describe a flow in plain English, an AI agent drives a real browser and reports pass/fail. No LLM key needed on this machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"commander": "^12.1.0",
|
|
22
22
|
"dotenv": "^17.4.2",
|
|
23
|
-
"playwright": "
|
|
23
|
+
"playwright": "1.48.0",
|
|
24
24
|
"ws": "^8.21.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
package/skill/AGENTS.md.template
CHANGED
|
@@ -6,8 +6,9 @@ code or selectors required.
|
|
|
6
6
|
|
|
7
7
|
- **Skill:** see `.agents/skills/hivemind-cli/SKILL.md` for the full command reference, test file
|
|
8
8
|
format, login/precondition conventions, and how to read results.
|
|
9
|
-
- **Setup check:** `hivemind --help`;
|
|
10
|
-
`HIVEMIND_SERVER_URL`
|
|
9
|
+
- **Setup check:** `hivemind --help`; the CLI talks to the hosted server at
|
|
10
|
+
`https://app.thecolonylab.com` by default — only set `HIVEMIND_SERVER_URL` if pointing at a
|
|
11
|
+
self-hosted server instead.
|
|
11
12
|
- **Default workflow:** right after building or changing a UI flow, write or generate a test in
|
|
12
13
|
`hive/tests/`, run it with `hivemind run <file> --url <url> --json`, and fix your own code if it
|
|
13
14
|
fails — before telling the user the feature is done.
|
|
@@ -25,7 +25,7 @@ machine — the hivemind server (wherever `HIVEMIND_SERVER_URL` points) holds it
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
hivemind --help
|
|
28
|
-
echo $HIVEMIND_SERVER_URL # if unset, defaults to
|
|
28
|
+
echo $HIVEMIND_SERVER_URL # if unset, defaults to the hosted server at https://app.thecolonylab.com
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
If the command isn't found, it isn't installed — tell the user rather than trying to work around it.
|
|
@@ -50,6 +50,17 @@ hivemind explore <url>
|
|
|
50
50
|
# Open the most recent run's report, or list past runs.
|
|
51
51
|
hivemind report
|
|
52
52
|
hivemind history [--json]
|
|
53
|
+
|
|
54
|
+
# Diff-aware regression check — for CI, or before opening a PR. Reads the real git diff, figures
|
|
55
|
+
# out which known pages are affected, RE-RUNS whatever local tests already touch them (this is
|
|
56
|
+
# the actual regression check), and drafts a new test for any genuine gap left uncovered. Add
|
|
57
|
+
# --local when testing a build that only exists on this machine/runner (same reasoning as `run`).
|
|
58
|
+
# --pr-comment <path> writes a ready-to-post markdown report (numbered pass/fail + report links).
|
|
59
|
+
hivemind coverage [--changed <base>] [--local] [--pr-comment <path>]
|
|
60
|
+
|
|
61
|
+
# Pull down a test `coverage` drafted for a gap — identified by a short code (e.g.
|
|
62
|
+
# "amber-fern-cove") shown in its output/PR comment, not by pasting any code.
|
|
63
|
+
hivemind apply <code>
|
|
53
64
|
```
|
|
54
65
|
|
|
55
66
|
Always use `--json` on `run` when you (the agent) are consuming the result programmatically — it
|