@gredescerber/agent-crew 0.0.0-stage → 0.5.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/AGENTS.md +24 -0
- package/LICENSE +201 -0
- package/README.md +411 -2
- package/README.ru.md +414 -0
- package/adapters/claude/adapter.js +107 -0
- package/adapters/codex/adapter.js +101 -0
- package/bin/crew.js +60 -0
- package/docs/AGENT-SETUP.md +138 -0
- package/ecc/profile.json +16 -0
- package/evals/README.md +123 -0
- package/evals/fixtures/mini-app/README.md +10 -0
- package/evals/fixtures/mini-app/contracts/health.json +11 -0
- package/evals/fixtures/mini-app/package.json +10 -0
- package/evals/fixtures/mini-app/src/cart.js +14 -0
- package/evals/fixtures/mini-app/src/handlers.js +16 -0
- package/evals/fixtures/mini-app/src/pager.js +7 -0
- package/evals/fixtures/mini-app/test/cart.test.js +10 -0
- package/evals/fixtures/mini-app/test/handlers.test.js +13 -0
- package/evals/fixtures/mini-app/test/pager.test.js +8 -0
- package/evals/tasks/add-input-validation.json +15 -0
- package/evals/tasks/fix-off-by-one.json +14 -0
- package/evals/tasks/handler-per-contract.json +15 -0
- package/evals/tasks/judges/add-input-validation.judge.mjs +28 -0
- package/evals/tasks/judges/fix-off-by-one.judge.mjs +22 -0
- package/evals/tasks/judges/handler-per-contract.judge.mjs +18 -0
- package/package.json +46 -4
- package/roles/analyst.md +11 -0
- package/roles/builder.md +16 -0
- package/roles/implementer.md +17 -0
- package/roles/integrator.md +12 -0
- package/roles/planner.md +19 -0
- package/roles/reviewer.md +16 -0
- package/roles/scout.md +15 -0
- package/skills/crew-brief-analysis/SKILL.md +32 -0
- package/skills/crew-kickoff/SKILL.md +77 -0
- package/skills/crew-kickoff/task-format.md +215 -0
- package/skills/crew-review/SKILL.md +22 -0
- package/skills/crew-workflow/SKILL.md +43 -0
- package/src/commands/adapters.js +85 -0
- package/src/commands/check.js +130 -0
- package/src/commands/commit.js +105 -0
- package/src/commands/compliance.js +109 -0
- package/src/commands/doctor.js +48 -0
- package/src/commands/eval.js +507 -0
- package/src/commands/github.js +169 -0
- package/src/commands/guard.js +101 -0
- package/src/commands/init.js +76 -0
- package/src/commands/kickoff.js +275 -0
- package/src/commands/labels.js +28 -0
- package/src/commands/metrics.js +48 -0
- package/src/commands/pr.js +211 -0
- package/src/commands/shots.js +51 -0
- package/src/commands/status.js +48 -0
- package/src/commands/task.js +569 -0
- package/src/commands/up.js +355 -0
- package/src/commands/worker.js +322 -0
- package/src/core/config.js +77 -0
- package/src/core/exec.js +50 -0
- package/src/core/guard.js +248 -0
- package/src/core/issues.js +154 -0
- package/src/core/lease.js +48 -0
- package/src/core/metrics.js +232 -0
- package/src/core/output.js +56 -0
- package/src/core/prd.js +593 -0
- package/src/core/proc.js +52 -0
- package/src/core/queue.js +92 -0
- package/src/core/registry.js +138 -0
- package/src/core/shots.js +68 -0
- package/src/core/supervisor.js +56 -0
- package/src/core/version.js +44 -0
- package/src/mcp/server.js +71 -0
- package/templates/labels.json +18 -0
- package/templates/mcp.optional.json +23 -0
- package/templates/product/.github/ISSUE_TEMPLATE/epic.yml +22 -0
- package/templates/product/.github/ISSUE_TEMPLATE/feedback.yml +36 -0
- package/templates/product/.github/ISSUE_TEMPLATE/task.yml +62 -0
- package/templates/product/.github/pull_request_template.md +16 -0
- package/templates/product/.github/workflows/crew-guard.yml +45 -0
- package/templates/product/.github/workflows/crew.yml +108 -0
- package/templates/product/AGENTS.md +109 -0
- package/templates/product/contracts/README.md +8 -0
- package/templates/product/docs/architecture.md +41 -0
- package/templates/product/docs/brief-analysis.md +62 -0
- package/templates/product/docs/brief.md +25 -0
- package/templates/product/docs/playbooks/issues.md +91 -0
- package/templates/product/docs/playbooks/preflight.md +28 -0
- package/templates/product/docs/playbooks/timeline.md +52 -0
- package/templates/product/docs/prd.md +58 -0
- package/templates/product/events/event.example.json +10 -0
- package/templates/product/tests/acceptance/README.md +9 -0
- package/templates/stacks/web-fastapi.json +74 -0
- package/templates/stacks/web-node.json +70 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Metrics log: every crew command appends an event to <project root>/.crew/metrics.jsonl.
|
|
2
|
+
// Event: { command, issue, agent, status, error_code, duration_ms, at }.
|
|
3
|
+
// Writing never throws: metrics must not break the agent's work.
|
|
4
|
+
// The root is shared by all worktrees of one repo (otherwise claim and pr open would land in different files).
|
|
5
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { loadConfig, agentId } from './config.js';
|
|
8
|
+
|
|
9
|
+
export const METRICS_FILE = join('.crew', 'metrics.jsonl');
|
|
10
|
+
|
|
11
|
+
const readText = (p) => {
|
|
12
|
+
try {
|
|
13
|
+
return readFileSync(p, 'utf8').trim();
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Project git dir: .git is a folder (regular repo) or a "gitdir: ..." file (worktree). */
|
|
20
|
+
function gitDir(root) {
|
|
21
|
+
const dotgit = join(root, '.git');
|
|
22
|
+
try {
|
|
23
|
+
if (statSync(dotgit).isDirectory()) return dotgit;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const m = /^gitdir:\s*(.+)$/m.exec(readText(dotgit) ?? '');
|
|
28
|
+
return m ? resolve(root, m[1].trim()) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Where to write metrics: for a worktree, the main repo root (via commondir),
|
|
33
|
+
* for a regular repo and a folder without git, root itself. No git invocation, only file reads.
|
|
34
|
+
*/
|
|
35
|
+
export function metricsRoot(root) {
|
|
36
|
+
try {
|
|
37
|
+
const dotgit = join(root, '.git');
|
|
38
|
+
if (!existsSync(dotgit) || statSync(dotgit).isDirectory()) return root;
|
|
39
|
+
const gd = gitDir(root);
|
|
40
|
+
const common = gd && readText(join(gd, 'commondir'));
|
|
41
|
+
return common ? dirname(resolve(gd, common)) : root;
|
|
42
|
+
} catch {
|
|
43
|
+
return root;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Task number from the current branch crew/<id>-... (reads HEAD directly, without git). */
|
|
48
|
+
export function branchIssue(root) {
|
|
49
|
+
try {
|
|
50
|
+
const gd = gitDir(root);
|
|
51
|
+
const head = gd && readText(join(gd, 'HEAD'));
|
|
52
|
+
return Number(/^ref:\s*refs\/heads\/crew\/(\d+)-/.exec(head ?? '')?.[1]) || null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Append an event. Returns true if written; false on any error. */
|
|
59
|
+
export function recordEvent(root, event = {}) {
|
|
60
|
+
try {
|
|
61
|
+
if (!root || ['0', 'off', 'false'].includes(String(process.env.CREW_METRICS ?? '').toLowerCase())) return false;
|
|
62
|
+
const e = {
|
|
63
|
+
command: event.command ?? null,
|
|
64
|
+
issue: event.issue ?? null,
|
|
65
|
+
agent: event.agent ?? null,
|
|
66
|
+
status: event.status ?? null,
|
|
67
|
+
error_code: event.error_code ?? null,
|
|
68
|
+
duration_ms: Number.isFinite(event.duration_ms) ? Math.round(event.duration_ms) : null,
|
|
69
|
+
at: event.at ?? new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
const p = join(metricsRoot(root), METRICS_FILE);
|
|
72
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
73
|
+
appendFileSync(p, JSON.stringify(e) + '\n');
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Event from a crew/v1 command result (for bin/crew.js and the MCP server). */
|
|
81
|
+
export function eventFromResult({ command, opts = {}, result, root, agent, started }) {
|
|
82
|
+
const fromArtifacts = Number(result?.artifacts?.issue) || null;
|
|
83
|
+
const fromOpts = typeof opts.id === 'number' && Number.isFinite(opts.id) ? opts.id : null;
|
|
84
|
+
return {
|
|
85
|
+
command,
|
|
86
|
+
issue: fromOpts ?? fromArtifacts ?? (root ? branchIssue(root) : null),
|
|
87
|
+
agent,
|
|
88
|
+
status: result?.status ?? 'error',
|
|
89
|
+
error_code: result?.error?.code ?? null,
|
|
90
|
+
duration_ms: started === undefined ? null : Date.now() - started,
|
|
91
|
+
at: new Date().toISOString(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Record a command's outcome. Shared entry point for the CLI (bin/crew.js) and MCP (callTool).
|
|
97
|
+
* Only writes into a real project (crew.config.json found), so as not to litter arbitrary folders.
|
|
98
|
+
* opts.cwd (if the command supports it) is the folder where the command actually ran.
|
|
99
|
+
*/
|
|
100
|
+
export function recordCommand({ command, opts = {}, result, started }) {
|
|
101
|
+
try {
|
|
102
|
+
const { root, path } = loadConfig(typeof opts.cwd === 'string' && opts.cwd ? opts.cwd : process.cwd());
|
|
103
|
+
if (!path) return false;
|
|
104
|
+
return recordEvent(root, eventFromResult({ command, opts, result, root, agent: agentId(), started }));
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Read the log. Corrupt lines are skipped (the file may have been cut off mid-write). */
|
|
111
|
+
export function readEvents(root) {
|
|
112
|
+
const text = readText(join(metricsRoot(root), METRICS_FILE));
|
|
113
|
+
if (!text) return [];
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const line of text.split(/\r?\n/)) {
|
|
116
|
+
try {
|
|
117
|
+
const e = JSON.parse(line);
|
|
118
|
+
if (e && typeof e === 'object') out.push(e);
|
|
119
|
+
} catch {
|
|
120
|
+
/* skip the corrupt line */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** "2026-11-01", ISO, or relative "30m" / "6h" / "2d" -> epoch milliseconds. null if not recognized. */
|
|
127
|
+
export function parseSince(since, now = Date.now()) {
|
|
128
|
+
if (since === undefined || since === null || since === '') return null;
|
|
129
|
+
const rel = /^(\d+)\s*([mhd])$/.exec(String(since).trim());
|
|
130
|
+
if (rel) return now - Number(rel[1]) * { m: 60_000, h: 3_600_000, d: 86_400_000 }[rel[2]];
|
|
131
|
+
const t = Date.parse(since);
|
|
132
|
+
return Number.isFinite(t) ? t : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Agent match: exact or by vendor ("codex" matches "codex/gpt-5"). */
|
|
136
|
+
export const agentMatches = (agent, filter) =>
|
|
137
|
+
!filter || agent === filter || String(agent ?? '').startsWith(filter + '/') || String(agent ?? '').startsWith(filter + '@');
|
|
138
|
+
|
|
139
|
+
export function filterEvents(events, { agent, sinceMs } = {}) {
|
|
140
|
+
return events.filter((e) => agentMatches(e.agent, agent) && (sinceMs == null || Date.parse(e.at) >= sinceMs));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const median = (xs) => {
|
|
144
|
+
if (!xs.length) return null;
|
|
145
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
146
|
+
const mid = s.length >> 1;
|
|
147
|
+
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const round = (x, d = 2) => (x == null ? null : Math.round(x * 10 ** d) / 10 ** d);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Pure aggregation of events per agent:
|
|
154
|
+
* completion_rate successful `pr open` / successful `task claim`
|
|
155
|
+
* retries_per_task average number of `check` runs per task (over tasks where check ran)
|
|
156
|
+
* median_claim_to_pr_ms median time from a successful claim to the first successful pr open on the same task
|
|
157
|
+
* plus the top 5 error codes across all events.
|
|
158
|
+
*/
|
|
159
|
+
export function aggregate(events) {
|
|
160
|
+
const sorted = [...events].sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
161
|
+
const agents = new Map();
|
|
162
|
+
const errors = new Map();
|
|
163
|
+
const get = (id) => {
|
|
164
|
+
if (!agents.has(id)) agents.set(id, { claims: 0, prs: 0, checks: new Map(), check_failures: 0, claimedAt: new Map(), durations: [], errors: 0, events: 0 });
|
|
165
|
+
return agents.get(id);
|
|
166
|
+
};
|
|
167
|
+
for (const e of sorted) {
|
|
168
|
+
const a = get(e.agent ?? 'unknown');
|
|
169
|
+
a.events++;
|
|
170
|
+
const success = e.status === 'success' || e.status === 'warning';
|
|
171
|
+
if (e.status === 'error') {
|
|
172
|
+
a.errors++;
|
|
173
|
+
const code = e.error_code ?? 'unknown';
|
|
174
|
+
errors.set(code, (errors.get(code) ?? 0) + 1);
|
|
175
|
+
}
|
|
176
|
+
if (e.command === 'task claim' && success) {
|
|
177
|
+
a.claims++;
|
|
178
|
+
if (e.issue != null && !a.claimedAt.has(e.issue)) a.claimedAt.set(e.issue, Date.parse(e.at));
|
|
179
|
+
} else if (e.command === 'pr open' && success) {
|
|
180
|
+
a.prs++;
|
|
181
|
+
const t0 = e.issue != null ? a.claimedAt.get(e.issue) : undefined;
|
|
182
|
+
if (t0 !== undefined) {
|
|
183
|
+
a.durations.push(Date.parse(e.at) - t0);
|
|
184
|
+
a.claimedAt.delete(e.issue); // a repeated pr open on the same task is not counted
|
|
185
|
+
}
|
|
186
|
+
} else if (e.command === 'check') {
|
|
187
|
+
const key = e.issue ?? 'no task';
|
|
188
|
+
a.checks.set(key, (a.checks.get(key) ?? 0) + 1);
|
|
189
|
+
if (e.status === 'error') a.check_failures++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const rows = [...agents.entries()]
|
|
193
|
+
.map(([agent, a]) => {
|
|
194
|
+
const perTask = [...a.checks.entries()].filter(([k]) => k !== 'no task').map(([, n]) => n);
|
|
195
|
+
return {
|
|
196
|
+
agent,
|
|
197
|
+
events: a.events,
|
|
198
|
+
errors: a.errors,
|
|
199
|
+
claims: a.claims,
|
|
200
|
+
prs: a.prs,
|
|
201
|
+
completion_rate: a.claims ? round(a.prs / a.claims) : null,
|
|
202
|
+
checks: [...a.checks.values()].reduce((s, n) => s + n, 0),
|
|
203
|
+
check_failures: a.check_failures,
|
|
204
|
+
retries_per_task: perTask.length ? round(perTask.reduce((s, n) => s + n, 0) / perTask.length) : null,
|
|
205
|
+
median_claim_to_pr_ms: median(a.durations),
|
|
206
|
+
};
|
|
207
|
+
})
|
|
208
|
+
.sort((x, y) => x.agent.localeCompare(y.agent));
|
|
209
|
+
const top_errors = [...errors.entries()]
|
|
210
|
+
.map(([code, count]) => ({ code, count }))
|
|
211
|
+
.sort((x, y) => y.count - x.count || x.code.localeCompare(y.code))
|
|
212
|
+
.slice(0, 5);
|
|
213
|
+
return { total: events.length, agents: rows, top_errors };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Human-readable duration: 95000 -> "1m 35s". */
|
|
217
|
+
export function humanMs(ms) {
|
|
218
|
+
if (ms == null) return '-';
|
|
219
|
+
const s = Math.round(ms / 1000);
|
|
220
|
+
if (s < 60) return `${s}s`;
|
|
221
|
+
const m = Math.floor(s / 60);
|
|
222
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
223
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Simple plain-text table (monospaced). */
|
|
227
|
+
export function table(headers, rows) {
|
|
228
|
+
const cells = [headers, ...rows].map((r) => r.map((c) => (c == null ? '-' : String(c))));
|
|
229
|
+
const w = headers.map((_, i) => Math.max(...cells.map((r) => r[i].length)));
|
|
230
|
+
const line = (r) => r.map((c, i) => c.padEnd(w[i])).join(' ').trimEnd();
|
|
231
|
+
return [line(cells[0]), w.map((n) => '-'.repeat(n)).join(' '), ...cells.slice(1).map(line)].join('\n');
|
|
232
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Single response shape for all crew commands (schema crew/v1).
|
|
2
|
+
// Any agent (Claude, Codex, ...) gets the same thing: status, summary, next_actions, artifacts, error.
|
|
3
|
+
|
|
4
|
+
export const SCHEMA = 'crew/v1';
|
|
5
|
+
|
|
6
|
+
export function ok(summary, { next_actions = [], artifacts = {}, status = 'success' } = {}) {
|
|
7
|
+
return { schema: SCHEMA, status, summary, next_actions, artifacts, error: null };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function warn(summary, opts = {}) {
|
|
11
|
+
return ok(summary, { ...opts, status: 'warning' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Error with a recovery contract: every error carries a cause hint,
|
|
16
|
+
* a safe retry and a stop condition. There are no errors without next_actions.
|
|
17
|
+
*/
|
|
18
|
+
export function fail(code, summary, { cause_hint = '', retry = null, stop_when = '', next_actions = [], artifacts = {} } = {}) {
|
|
19
|
+
return {
|
|
20
|
+
schema: SCHEMA,
|
|
21
|
+
status: 'error',
|
|
22
|
+
summary,
|
|
23
|
+
next_actions: next_actions.length ? next_actions : retry ? [retry] : ['stop and report in the issue'],
|
|
24
|
+
artifacts,
|
|
25
|
+
error: { code, cause_hint, retry, stop_when },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class CrewError extends Error {
|
|
30
|
+
constructor(code, summary, recovery = {}) {
|
|
31
|
+
super(summary);
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.recovery = recovery;
|
|
34
|
+
}
|
|
35
|
+
toResult() {
|
|
36
|
+
return fail(this.code, this.message, this.recovery);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function render(result, { json }) {
|
|
41
|
+
if (json) return JSON.stringify(result, null, 2);
|
|
42
|
+
const icon = { success: '✔', warning: '!', error: '✖' }[result.status] ?? '?';
|
|
43
|
+
const lines = [`${icon} ${result.summary}`];
|
|
44
|
+
if (result.error) {
|
|
45
|
+
if (result.error.cause_hint) lines.push(` cause: ${result.error.cause_hint}`);
|
|
46
|
+
if (result.error.stop_when) lines.push(` stop if: ${result.error.stop_when}`);
|
|
47
|
+
}
|
|
48
|
+
for (const [k, v] of Object.entries(result.artifacts ?? {})) {
|
|
49
|
+
lines.push(` ${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`);
|
|
50
|
+
}
|
|
51
|
+
if (result.next_actions?.length) {
|
|
52
|
+
lines.push(' next:');
|
|
53
|
+
for (const a of result.next_actions) lines.push(` → ${a}`);
|
|
54
|
+
}
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|