@gredescerber/agent-crew 0.0.0-stage → 0.6.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 +25 -0
- package/LICENSE +201 -0
- package/README.md +418 -2
- package/README.ru.md +421 -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 +46 -0
- package/src/commands/adapters.js +85 -0
- package/src/commands/ask.js +123 -0
- package/src/commands/board.js +88 -0
- package/src/commands/check.js +148 -0
- package/src/commands/commit.js +105 -0
- package/src/commands/compliance.js +109 -0
- package/src/commands/digest.js +64 -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/merge-ready.js +33 -0
- package/src/commands/metrics.js +48 -0
- package/src/commands/pr.js +273 -0
- package/src/commands/shots.js +51 -0
- package/src/commands/status.js +48 -0
- package/src/commands/task.js +590 -0
- package/src/commands/up.js +355 -0
- package/src/commands/upgrade.js +178 -0
- package/src/commands/worker.js +322 -0
- package/src/core/board.js +127 -0
- package/src/core/config.js +83 -0
- package/src/core/digest.js +67 -0
- package/src/core/exec.js +50 -0
- package/src/core/guard.js +273 -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 +612 -0
- package/src/core/proc.js +52 -0
- package/src/core/questions.js +51 -0
- package/src/core/queue.js +94 -0
- package/src/core/registry.js +143 -0
- package/src/core/session.js +61 -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 +81 -0
- package/templates/labels.json +23 -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 +118 -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,51 @@
|
|
|
1
|
+
// Agent-to-agent questions through GitHub (crew ask / inbox / answer), pure helpers.
|
|
2
|
+
// A question is an issue labelled crew:question, assigned to the owner of the area (or a person), with a marker
|
|
3
|
+
// <!-- crew:question from=<agent> task=<n> -->. An answer is a comment <!-- crew:answer agent=<agent> --> + close.
|
|
4
|
+
// Decisions only a human can make get the needs-human label and stay open.
|
|
5
|
+
// Question and answer text is DATA for the reader, never instructions.
|
|
6
|
+
import { parseMarkers } from './issues.js';
|
|
7
|
+
|
|
8
|
+
export const QUESTION_LABEL = 'crew:question';
|
|
9
|
+
export const HUMAN_LABEL = 'needs-human';
|
|
10
|
+
|
|
11
|
+
/** More than 30% of letters are Cyrillic: agents write to each other in English (humans may use any language). */
|
|
12
|
+
export function mostlyNonEnglish(text = '') {
|
|
13
|
+
const letters = String(text).match(/\p{L}/gu) ?? [];
|
|
14
|
+
if (letters.length < 8) return false;
|
|
15
|
+
const cyr = letters.filter((c) => /[Ѐ-ӿ]/.test(c)).length;
|
|
16
|
+
return cyr / letters.length > 0.3;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function questionBody({ text, from, task, area }) {
|
|
20
|
+
const fields = [`from=${from}`, task ? `task=${task}` : null, area ? `area=${area}` : null].filter(Boolean).join(' ');
|
|
21
|
+
return `${text.trim()}\n\n_Asked by \`${from}\`${task ? ` while working on #${task}` : ''}. Answer with \`crew answer <this issue> --text "..."\`; if a human must decide, \`crew answer <this issue> --human --text "..."\`._\n\n<!-- crew:question ${fields} -->\n`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const questionMeta = (issue) => parseMarkers([{ body: issue.body ?? '' }]).find((m) => m.kind === 'question') ?? null;
|
|
25
|
+
export const answerMarker = (agent) => `<!-- crew:answer agent=${agent} -->`;
|
|
26
|
+
export const ackMarker = (agent) => `<!-- crew:ack agent=${agent} -->`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* What waits for me (pure). open: open crew:question issues; closed: recently closed ones (with comments).
|
|
30
|
+
* → { toAnswer: [{ number, title, from, task }], answered: [{ number, title, task }] }
|
|
31
|
+
*/
|
|
32
|
+
export function inboxFor({ open = [], closed = [], me, login, myAreas = [] }) {
|
|
33
|
+
const lc = (s) => String(s ?? '').toLowerCase();
|
|
34
|
+
const toAnswer = [];
|
|
35
|
+
for (const q of open) {
|
|
36
|
+
const meta = questionMeta(q) ?? {};
|
|
37
|
+
const labels = (q.labels ?? []).map((l) => l.name ?? l);
|
|
38
|
+
if (labels.includes(HUMAN_LABEL)) continue; // waits for a human, not for an agent
|
|
39
|
+
if (meta.from === me) continue;
|
|
40
|
+
const mine = (q.assignees ?? []).some((a) => lc(a.login) === lc(login)) || (meta.area && myAreas.includes(meta.area));
|
|
41
|
+
if (mine) toAnswer.push({ number: q.number, title: q.title, from: meta.from ?? null, task: meta.task ? Number(meta.task) : null });
|
|
42
|
+
}
|
|
43
|
+
const answered = [];
|
|
44
|
+
for (const q of closed) {
|
|
45
|
+
const meta = questionMeta(q);
|
|
46
|
+
if (!meta || meta.from !== me) continue;
|
|
47
|
+
const acked = parseMarkers(q.comments ?? []).some((m) => m.kind === 'ack' && m.agent === me);
|
|
48
|
+
if (!acked) answered.push({ number: q.number, title: q.title, task: meta.task ? Number(meta.task) : null });
|
|
49
|
+
}
|
|
50
|
+
return { toAnswer: toAnswer.sort((a, b) => a.number - b.number), answered: answered.sort((a, b) => a.number - b.number) };
|
|
51
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// What an agent should do next (pure functions, no network): merge a ready PR, review, take a task.
|
|
2
|
+
// Used by crew task next and crew status. Without it PRs hang without review
|
|
3
|
+
// and the whole queue waits behind the skeleton.
|
|
4
|
+
import { authorAgent, linkedIssue, reviewPolicy, reviewState, vendorOf } from './guard.js';
|
|
5
|
+
import { labelValue, tierAllows } from './issues.js';
|
|
6
|
+
|
|
7
|
+
const riskOfPr = (pr) => (pr.labels ?? []).map((l) => l.name ?? l).find((n) => n.startsWith('risk:'))?.slice(5) ?? 'med';
|
|
8
|
+
const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PR check summary from statusCheckRollup: { check, guard } -> 'success' | 'failure' | 'pending' | null.
|
|
12
|
+
* Reruns of one check produce several entries: any success counts as success (the merge is still guarded by main protection).
|
|
13
|
+
*/
|
|
14
|
+
export function prChecks(pr) {
|
|
15
|
+
const seen = { check: [], guard: [] };
|
|
16
|
+
for (const c of pr.statusCheckRollup ?? []) {
|
|
17
|
+
const name = c.name ?? c.context;
|
|
18
|
+
const key = name === 'check' ? 'check' : name === 'crew-guard' ? 'guard' : null;
|
|
19
|
+
if (key) seen[key].push(String(c.conclusion || c.state || '').toUpperCase());
|
|
20
|
+
}
|
|
21
|
+
const sum = (xs) => {
|
|
22
|
+
if (!xs.length) return null;
|
|
23
|
+
if (xs.includes('SUCCESS')) return 'success';
|
|
24
|
+
if (xs.some((s) => ['FAILURE', 'ERROR', 'TIMED_OUT', 'ACTION_REQUIRED'].includes(s))) return 'failure';
|
|
25
|
+
return 'pending';
|
|
26
|
+
};
|
|
27
|
+
return { check: sum(seen.check), guard: sum(seen.guard) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* PRs waiting for the team. -> [{ kind: 'merge'|'review', number, title, risk, age, reviewable, why_not }]
|
|
32
|
+
* merge: risk:high, reviews collected, checks green: there is no auto-merge, someone must merge.
|
|
33
|
+
* review: approval needed; reviewable is whether this particular agent can give it (and whether it will count).
|
|
34
|
+
* reviewers: logins with the review area in crew-team; they review first, the others after fallback_minutes.
|
|
35
|
+
*/
|
|
36
|
+
export function prQueue(prs = [], { me, login, reviewers = [], config = {}, now = Date.now() } = {}) {
|
|
37
|
+
const policy = reviewPolicy(config);
|
|
38
|
+
const lc = (s) => String(s ?? '').toLowerCase();
|
|
39
|
+
const isReviewer = !!login && reviewers.some((r) => lc(r) === lc(login));
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const pr of prs) {
|
|
42
|
+
if (pr.isDraft) continue;
|
|
43
|
+
// PR from a fork (public repo): its text and diff are foreign, so we do not hand it to agents for review (prompt injection).
|
|
44
|
+
if (pr.isCrossRepository) continue;
|
|
45
|
+
const author = authorAgent(pr.body);
|
|
46
|
+
const mine = !!author && author === me;
|
|
47
|
+
const risk = riskOfPr(pr);
|
|
48
|
+
const need = risk === 'high' ? policy.approvals_high : policy.approvals;
|
|
49
|
+
const rs = reviewState(pr.comments ?? [], { headSha: pr.headRefOid, author, policy, prCreatedAt: pr.createdAt, now });
|
|
50
|
+
const age = Math.max(0, Math.round((now - Date.parse(pr.createdAt ?? '')) / 60000)) || 0;
|
|
51
|
+
const checks = prChecks(pr);
|
|
52
|
+
const conflict = String(pr.mergeable ?? '').toUpperCase() === 'CONFLICTING';
|
|
53
|
+
// PR is waiting for its author: reviewer requests changes, red check or conflict with main. The author fixes it before new tasks.
|
|
54
|
+
const fixWhy = rs.changes.length ? `reviewer requests changes (${rs.changes.join(', ')})` : conflict ? 'conflict with main' : checks.check === 'failure' ? 'red check' : null;
|
|
55
|
+
if (fixWhy) {
|
|
56
|
+
out.push({ kind: 'fix', number: pr.number, title: pr.title, issue: linkedIssue(pr.body), risk, age, mine, reason: fixWhy, reviewable: false });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (rs.approvals.length >= need) {
|
|
60
|
+
// risk:high is merged manually; low/med if GitHub auto-merge did not turn on (no autoMergeRequest).
|
|
61
|
+
const manual = risk === 'high' || ('autoMergeRequest' in pr && !pr.autoMergeRequest);
|
|
62
|
+
if (manual && checks.check === 'success' && checks.guard === 'success') out.push({ kind: 'merge', number: pr.number, title: pr.title, risk, age, reviewable: false });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const sha7 = String(pr.headRefOid ?? '').slice(0, 7);
|
|
66
|
+
const reviewedHead = !!sha7 && (pr.comments ?? []).some((c) => new RegExp(`<!-- (?:crew|hk):review [^>]*agent=${esc(me)} [^>]*sha=${esc(sha7)}`).test(c.body ?? ''));
|
|
67
|
+
const counts = policy.require === 'other-agent' || rs.fallback || !author || vendorOf(author) !== vendorOf(me);
|
|
68
|
+
const authorIsReviewer = reviewers.some((r) => lc(r) === lc(pr.author?.login));
|
|
69
|
+
const turn = isReviewer || !reviewers.length || authorIsReviewer || age >= policy.fallback_minutes;
|
|
70
|
+
const why_not = mine ? 'own PR' : reviewedHead ? 'already reviewed this commit' : !counts ? 'needs a reviewer from another vendor' : !turn ? `the team reviewer goes first (${reviewers.join(', ')})` : null;
|
|
71
|
+
out.push({ kind: 'review', number: pr.number, title: pr.title, risk, age, need, have: rs.approvals.length, reviewable: !why_not, why_not });
|
|
72
|
+
}
|
|
73
|
+
const rank = (x) => (x.kind === 'fix' ? 0 : x.kind === 'merge' ? 10 : 20) + (x.risk === 'high' ? 0 : 1);
|
|
74
|
+
return out.sort((a, b) => rank(a) - rank(b) || a.number - b.number);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Ready tasks ordered "assigned to me -> nobody's -> in someone else's area" (assignment = area owner from crew-team).
|
|
79
|
+
* -> [{ issue, owners, mine }]
|
|
80
|
+
*/
|
|
81
|
+
export function taskOrder(ready = [], { login, area, tier = 'high' } = {}) {
|
|
82
|
+
const me = String(login ?? '').toLowerCase();
|
|
83
|
+
return ready
|
|
84
|
+
.filter((i) => !area || labelValue(i, 'area') === area)
|
|
85
|
+
.filter((i) => tierAllows(tier, labelValue(i, 'tier')))
|
|
86
|
+
.map((i) => {
|
|
87
|
+
const owners = (i.assignees ?? []).map((a) => a.login);
|
|
88
|
+
const mine = !!me && owners.some((o) => o.toLowerCase() === me);
|
|
89
|
+
// priority:high / priority:low (set by a human, e.g. on crew board) order tasks inside each group
|
|
90
|
+
const p = { high: 0, low: 2 }[labelValue(i, 'priority')] ?? 1;
|
|
91
|
+
return { issue: i, owners, mine, rank: (mine ? 0 : owners.length ? 2 : 1) * 10 + p };
|
|
92
|
+
})
|
|
93
|
+
.sort((a, b) => a.rank - b.rank || a.issue.number - b.issue.number);
|
|
94
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Command registry. One command = one module in src/commands/ with fields:
|
|
2
|
+
// name 'task next' - how to call it from the CLI
|
|
3
|
+
// summary one line - for help and the MCP description
|
|
4
|
+
// args [{ name, type, positional?, required?, description }]
|
|
5
|
+
// run(opts, ctx) -> result from core/output.js
|
|
6
|
+
// usesCwd true - the command works with a working copy; takes an optional
|
|
7
|
+
// cwd argument (absolute path, e.g. the task's worktree).
|
|
8
|
+
// The MCP server runs in the project root, so without cwd
|
|
9
|
+
// the tools would see main, not the task branch.
|
|
10
|
+
// The CLI (bin/crew.js) and the MCP server (src/mcp/server.js) are built from this list,
|
|
11
|
+
// so a new command is automatically available to both humans and agents.
|
|
12
|
+
|
|
13
|
+
import task from '../commands/task.js';
|
|
14
|
+
import check from '../commands/check.js';
|
|
15
|
+
import pr from '../commands/pr.js';
|
|
16
|
+
import status from '../commands/status.js';
|
|
17
|
+
import commit from '../commands/commit.js';
|
|
18
|
+
import init from '../commands/init.js';
|
|
19
|
+
import adapters from '../commands/adapters.js';
|
|
20
|
+
import labels from '../commands/labels.js';
|
|
21
|
+
import compliance from '../commands/compliance.js';
|
|
22
|
+
import doctor from '../commands/doctor.js';
|
|
23
|
+
import worker from '../commands/worker.js';
|
|
24
|
+
import github from '../commands/github.js';
|
|
25
|
+
import metrics from '../commands/metrics.js';
|
|
26
|
+
import evalCmd from '../commands/eval.js';
|
|
27
|
+
import kickoff from '../commands/kickoff.js';
|
|
28
|
+
import guard from '../commands/guard.js';
|
|
29
|
+
import up from '../commands/up.js';
|
|
30
|
+
import shots from '../commands/shots.js';
|
|
31
|
+
import board from '../commands/board.js';
|
|
32
|
+
import upgrade from '../commands/upgrade.js';
|
|
33
|
+
import askCmds from '../commands/ask.js';
|
|
34
|
+
import mergeReady from '../commands/merge-ready.js';
|
|
35
|
+
import digest from '../commands/digest.js';
|
|
36
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
37
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
38
|
+
import { loadConfig } from './config.js';
|
|
39
|
+
import { metricsRoot } from './metrics.js';
|
|
40
|
+
|
|
41
|
+
/** Shared argument for commands with usesCwd: true. */
|
|
42
|
+
export const CWD_ARG = {
|
|
43
|
+
name: 'cwd',
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'absolute path of the working copy (the task worktree from crew_task_claim -> artifacts.worktree); defaults to the current folder',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Command arguments including the shared ones (cwd). */
|
|
49
|
+
export const argsOf = (cmd) => [...(cmd.args ?? []), ...(cmd.usesCwd && !(cmd.args ?? []).some((a) => a.name === 'cwd') ? [CWD_ARG] : [])];
|
|
50
|
+
|
|
51
|
+
export const commands = [...task, ...kickoff, check, ...pr, guard, status, commit, init, up, adapters, labels, ...compliance, doctor, worker, github, metrics, evalCmd, shots, board, upgrade, ...askCmds, mergeReady, digest];
|
|
52
|
+
|
|
53
|
+
/** For MCP: only commands safe for agents (no init/labels/adapters; a human does those). */
|
|
54
|
+
export const agentCommands = commands.filter((c) => c.agent !== false);
|
|
55
|
+
|
|
56
|
+
export function findCommand(words) {
|
|
57
|
+
// longest name match: "task claim 42" -> 'task claim'
|
|
58
|
+
let best = null;
|
|
59
|
+
for (const c of commands) {
|
|
60
|
+
const parts = c.name.split(' ');
|
|
61
|
+
if (parts.every((p, i) => words[i] === p) && (!best || parts.length > best.name.split(' ').length)) best = c;
|
|
62
|
+
}
|
|
63
|
+
return best;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function toolName(cmd) {
|
|
67
|
+
return 'crew_' + cmd.name.replace(/[\s-]+/g, '_');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function inputSchema(cmd) {
|
|
71
|
+
const properties = {};
|
|
72
|
+
const required = [];
|
|
73
|
+
for (const a of argsOf(cmd)) {
|
|
74
|
+
properties[a.name] = { type: a.type === 'number' ? 'number' : a.type === 'boolean' ? 'boolean' : 'string', description: a.description };
|
|
75
|
+
if (a.enum) properties[a.name].enum = a.enum;
|
|
76
|
+
if (a.required) required.push(a.name);
|
|
77
|
+
}
|
|
78
|
+
return { type: 'object', properties, required, additionalProperties: false };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** argv words -> options object per the args spec. */
|
|
82
|
+
export function parseArgs(cmd, rest) {
|
|
83
|
+
const opts = {};
|
|
84
|
+
const specs = argsOf(cmd);
|
|
85
|
+
const positionals = specs.filter((a) => a.positional);
|
|
86
|
+
let pi = 0;
|
|
87
|
+
for (let i = 0; i < rest.length; i++) {
|
|
88
|
+
const w = rest[i];
|
|
89
|
+
const isShort = /^-[a-zA-Z]$/.test(w); // -m "msg", as in git
|
|
90
|
+
if (w.startsWith('--') || isShort) {
|
|
91
|
+
const [k, v] = w.slice(isShort ? 1 : 2).split('=');
|
|
92
|
+
const spec = specs.find((a) => a.name === k.replace(/-/g, '_'));
|
|
93
|
+
const key = spec?.name ?? k.replace(/-/g, '_');
|
|
94
|
+
if (spec?.type === 'boolean' || (v === undefined && (rest[i + 1] === undefined || rest[i + 1].startsWith('--')))) {
|
|
95
|
+
opts[key] = v === undefined ? true : v !== 'false';
|
|
96
|
+
} else {
|
|
97
|
+
opts[key] = v ?? rest[++i];
|
|
98
|
+
}
|
|
99
|
+
} else if (pi < positionals.length) {
|
|
100
|
+
opts[positionals[pi++].name] = w;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const a of specs) {
|
|
104
|
+
if (a.type === 'number' && opts[a.name] !== undefined) opts[a.name] = Number(opts[a.name]);
|
|
105
|
+
}
|
|
106
|
+
return opts;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* cwd may only point inside the crew project and only the same repository crew runs in
|
|
111
|
+
* (the main copy or its worktree). Otherwise an agent could commit/push to another repo on the machine.
|
|
112
|
+
* home is the project root where crew/the MCP server runs (default process.cwd()).
|
|
113
|
+
*/
|
|
114
|
+
export function cwdScopeError(cwd, home = process.cwd()) {
|
|
115
|
+
const target = loadConfig(cwd);
|
|
116
|
+
if (!target.path) return `cwd is not a crew project (no crew.config.json up the folder tree): ${cwd}`;
|
|
117
|
+
const here = loadConfig(home);
|
|
118
|
+
if (!here.path) return null; // crew runs outside a project: it is enough that cwd itself is a crew project
|
|
119
|
+
const real = (p) => { try { return realpathSync.native(p); } catch { return resolve(p); } };
|
|
120
|
+
const norm = (p) => {
|
|
121
|
+
const s = real(p).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
122
|
+
return process.platform === 'win32' ? s.toLowerCase() : s;
|
|
123
|
+
};
|
|
124
|
+
if (norm(metricsRoot(target.root)) !== norm(metricsRoot(here.root))) {
|
|
125
|
+
return `cwd is outside the current project: ${cwd} (only ${here.root} and its worktrees are allowed)`;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function validate(cmd, opts) {
|
|
131
|
+
if (cmd.usesCwd && opts.cwd !== undefined) {
|
|
132
|
+
if (typeof opts.cwd !== 'string' || !isAbsolute(opts.cwd)) return 'cwd must be an absolute path';
|
|
133
|
+
if (!existsSync(opts.cwd)) return `cwd does not exist: ${opts.cwd}`;
|
|
134
|
+
const scope = cwdScopeError(opts.cwd);
|
|
135
|
+
if (scope) return scope;
|
|
136
|
+
}
|
|
137
|
+
for (const a of argsOf(cmd)) {
|
|
138
|
+
if (a.required && (opts[a.name] === undefined || opts[a.name] === '')) return `missing argument: ${a.name}`;
|
|
139
|
+
if (a.type === 'number' && opts[a.name] !== undefined && !Number.isFinite(opts[a.name])) return `${a.name} must be a number`;
|
|
140
|
+
if (a.enum && opts[a.name] !== undefined && !a.enum.includes(opts[a.name])) return `${a.name}: one of ${a.enum.join(', ')}`;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// A unique agent name per session on one machine, without setting CREW_AGENT_ID by hand.
|
|
2
|
+
// Each Claude Code / Codex session starts its own crew MCP server; the server takes the first free slot
|
|
3
|
+
// (claude@pc, claude@pc-2, ...) with a lock file holding its PID, and frees it on exit.
|
|
4
|
+
// crew task claim pins the name into the task worktree (.crew/agent-id), so CLI calls there use it too.
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export const SLOTS_DIR = join(tmpdir(), 'crew-agents');
|
|
10
|
+
export const PIN_FILE = join('.crew', 'agent-id');
|
|
11
|
+
|
|
12
|
+
const alive = (pid) => {
|
|
13
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
|
|
14
|
+
};
|
|
15
|
+
const slotName = (base, n) => (n === 1 ? base : `${base}-${n}`);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Take the first free slot for base (e.g. claude@pc). A slot is free if it has no lock file or its PID is dead.
|
|
19
|
+
* → { id, release() } or null when every slot is busy. fs and isAlive are injectable for tests.
|
|
20
|
+
*/
|
|
21
|
+
export function claimSlot(base, { dir = SLOTS_DIR, pid = process.pid, isAlive = alive, max = 20 } = {}) {
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
for (let n = 1; n <= max; n++) {
|
|
24
|
+
const id = slotName(base, n);
|
|
25
|
+
const file = join(dir, `${id.replace(/[^A-Za-z0-9@._-]/g, '_')}.lock`);
|
|
26
|
+
if (existsSync(file)) {
|
|
27
|
+
const owner = Number(readFileSync(file, 'utf8').trim());
|
|
28
|
+
if (owner && owner !== pid && isAlive(owner)) continue;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
writeFileSync(file, String(pid), { flag: existsSync(file) ? 'w' : 'wx' });
|
|
32
|
+
} catch {
|
|
33
|
+
continue; // another session took it between the check and the write
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
id,
|
|
37
|
+
release: () => {
|
|
38
|
+
try { if (Number(readFileSync(file, 'utf8').trim()) === pid) unlinkSync(file); } catch { /* already gone */ }
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The name pinned in this worktree (nearest .crew/agent-id walking up from cwd), or null. */
|
|
46
|
+
export function pinnedId(cwd = process.cwd()) {
|
|
47
|
+
let dir = resolve(cwd);
|
|
48
|
+
for (let i = 0; i < 30; i++) {
|
|
49
|
+
const f = join(dir, PIN_FILE);
|
|
50
|
+
if (existsSync(f)) return readFileSync(f, 'utf8').trim() || null;
|
|
51
|
+
const up = dirname(dir);
|
|
52
|
+
if (up === dir) return null;
|
|
53
|
+
dir = up;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function pinId(worktree, id) {
|
|
59
|
+
mkdirSync(join(worktree, '.crew'), { recursive: true });
|
|
60
|
+
writeFileSync(join(worktree, PIN_FILE), `${id}\n`);
|
|
61
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Before/after screenshots for tasks that change how the product looks.
|
|
2
|
+
// The executor takes "before" at the start of the task (base code) and "after" when done;
|
|
3
|
+
// `crew pr open` embeds both into the PR so reviewers and humans see the visual change.
|
|
4
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const SHOTS_DIR = '.crew/shots';
|
|
8
|
+
export const PHASES = ['before', 'after'];
|
|
9
|
+
|
|
10
|
+
/** Files whose change usually changes what users see. */
|
|
11
|
+
const UI_RE = [
|
|
12
|
+
/^web\//, /^design\//, /^app\//, /^frontend\//, /^ui\//,
|
|
13
|
+
/\.(css|scss|sass|less|styl|html|htm|vue|svelte|tsx|jsx)$/i,
|
|
14
|
+
];
|
|
15
|
+
export const isUiFile = (p) => {
|
|
16
|
+
const f = String(p).replace(/\\/g, '/');
|
|
17
|
+
return !/(^|\/)(tests?|__tests__|e2e)\//.test(f) && !/\.(test|spec)\.[jt]sx?$/.test(f) && UI_RE.some((re) => re.test(f));
|
|
18
|
+
};
|
|
19
|
+
export const uiFiles = (files = []) => files.filter(isUiFile);
|
|
20
|
+
|
|
21
|
+
/** Screen name: letters, digits, dash. Used as part of a file name. */
|
|
22
|
+
export const NAME_RE = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Only plain http(s) URLs without characters a shell could interpret. Rejected on the RAW input
|
|
26
|
+
* (not after URL encoding): quotes, backtick, $, \, whitespace, < > | ; and % ^ ! (cmd.exe expands
|
|
27
|
+
* %VAR% even inside double quotes).
|
|
28
|
+
*/
|
|
29
|
+
export function safeUrl(url) {
|
|
30
|
+
if (/[\s"'`$\\<>|;%^!]/.test(String(url))) return null;
|
|
31
|
+
try {
|
|
32
|
+
const u = new URL(String(url));
|
|
33
|
+
if (!['http:', 'https:'].includes(u.protocol)) return null;
|
|
34
|
+
const s = u.toString();
|
|
35
|
+
return /^[A-Za-z0-9:/?#[\]@!$&()*+,;=%._~-]+$/.test(s) && !/[`$"'\\]/.test(s) ? s : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const shotPath = (id, name, phase) => `${SHOTS_DIR}/${id}/${name}-${phase}.png`;
|
|
42
|
+
|
|
43
|
+
/** Playwright CLI command line (run through the shell so npx resolves on Windows). */
|
|
44
|
+
export function captureCommand({ url, file, width = 1280, height = 800 }) {
|
|
45
|
+
return `npx --yes playwright screenshot --full-page --viewport-size=${Number(width)},${Number(height)} "${url}" "${file}"`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Shots present for a task: { name: { before: path|null, after: path|null } }. */
|
|
49
|
+
export function listShots(root, id) {
|
|
50
|
+
const dir = join(root, SHOTS_DIR, String(id));
|
|
51
|
+
const out = {};
|
|
52
|
+
if (!existsSync(dir)) return out;
|
|
53
|
+
for (const f of readdirSync(dir)) {
|
|
54
|
+
const m = /^([a-z0-9][a-z0-9-]*)-(before|after)\.png$/.exec(f);
|
|
55
|
+
if (!m) continue;
|
|
56
|
+
(out[m[1]] ??= { before: null, after: null })[m[2]] = `${SHOTS_DIR}/${id}/${f}`;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Markdown section for the PR body. Images are pinned to the head commit so links survive branch deletion. */
|
|
62
|
+
export function shotsSection({ repo, sha, shots }) {
|
|
63
|
+
const names = Object.keys(shots).sort();
|
|
64
|
+
if (!names.length) return '';
|
|
65
|
+
const img = (p) => (p ? `` : '_missing_');
|
|
66
|
+
const rows = names.map((n) => `| ${n} | ${img(shots[n].before)} | ${img(shots[n].after)} |`);
|
|
67
|
+
return ['## Before / After', '', '| Screen | Before | After |', '|---|---|---|', ...rows, ''].join('\n');
|
|
68
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Supervisor process for runSupervised (proc.js). Do not import: it is launched as a separate node process.
|
|
2
|
+
// stdin: JSON { cmd, args, shell, cwd, input, timeoutMs } -> stdout: JSON { code, stdout, stderr, timedOut, error }.
|
|
3
|
+
// On timeout it kills the command's whole process tree and exits without waiting for grandchildren to close the pipes.
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { killTree } from './proc.js';
|
|
6
|
+
|
|
7
|
+
const CAP = 64 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
let raw = '';
|
|
10
|
+
process.stdin.setEncoding('utf8');
|
|
11
|
+
process.stdin.on('data', (d) => (raw += d));
|
|
12
|
+
process.stdin.on('end', () => {
|
|
13
|
+
const job = JSON.parse(raw);
|
|
14
|
+
const out = { stdout: '', stderr: '' };
|
|
15
|
+
let timedOut = false;
|
|
16
|
+
let done = false;
|
|
17
|
+
|
|
18
|
+
const finish = (code, error = null) => {
|
|
19
|
+
if (done) return;
|
|
20
|
+
done = true;
|
|
21
|
+
process.stdout.write(JSON.stringify({ code, stdout: out.stdout, stderr: out.stderr, timedOut, error }));
|
|
22
|
+
process.exit(0);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let child;
|
|
26
|
+
try {
|
|
27
|
+
child = spawn(job.cmd, job.args ?? [], {
|
|
28
|
+
cwd: job.cwd ?? undefined,
|
|
29
|
+
shell: !!job.shell,
|
|
30
|
+
stdio: [job.input != null ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
detached: process.platform !== 'win32', // own process group, so kill(-pid) kills everyone
|
|
33
|
+
});
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return finish(-1, e.code ?? e.message);
|
|
36
|
+
}
|
|
37
|
+
for (const k of ['stdout', 'stderr']) {
|
|
38
|
+
child[k].setEncoding('utf8');
|
|
39
|
+
child[k].on('data', (d) => { if (out[k].length < CAP) out[k] += d; });
|
|
40
|
+
}
|
|
41
|
+
if (job.input != null) {
|
|
42
|
+
child.stdin.on('error', () => {}); // the process may have exited without reading stdin
|
|
43
|
+
child.stdin.end(job.input);
|
|
44
|
+
}
|
|
45
|
+
child.on('error', (e) => finish(-1, e.code ?? e.message));
|
|
46
|
+
child.on('close', (code) => finish(code ?? -1));
|
|
47
|
+
|
|
48
|
+
if (job.timeoutMs) {
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
timedOut = true;
|
|
51
|
+
killTree(child, { graceMs: 2_000 });
|
|
52
|
+
// Do not wait for close: surviving grandchildren could hold the pipes. Give SIGKILL 3 s and exit.
|
|
53
|
+
setTimeout(() => finish(-1), 3_000);
|
|
54
|
+
}, job.timeoutMs);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// An outdated crew on a member's machine means fixes
|
|
2
|
+
// do not take effect until the person updates their clone. crew compares itself with the latest harness tag.
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { HARNESS_ROOT } from './config.js';
|
|
7
|
+
import { run } from './exec.js';
|
|
8
|
+
|
|
9
|
+
export const LOCAL_VERSION = JSON.parse(readFileSync(join(HARNESS_ROOT, 'package.json'), 'utf8')).version;
|
|
10
|
+
const REPO = 'GredesCerber/agent-crew';
|
|
11
|
+
const CACHE = join(tmpdir(), 'crew-latest.json');
|
|
12
|
+
const TTL_MS = 60 * 60_000;
|
|
13
|
+
|
|
14
|
+
/** '0.2.10' > '0.2.9' (numbers, not strings). Not a version: null. */
|
|
15
|
+
export function compareVersions(a, b) {
|
|
16
|
+
const p = (v) => (/^v?(\d+)\.(\d+)\.(\d+)/.exec(String(v ?? '')) ?? []).slice(1).map(Number);
|
|
17
|
+
const x = p(a);
|
|
18
|
+
const y = p(b);
|
|
19
|
+
if (x.length !== 3 || y.length !== 3) return null;
|
|
20
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i] ? 1 : -1;
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Latest harness tag (via gh, cached for an hour in tmp). No network/access: null, does not interfere with work. */
|
|
25
|
+
export function latestVersion({ now = Date.now(), fetch = () => run('gh', ['api', `repos/${REPO}/tags`, '--jq', '.[].name'], { allowFail: true }), cache = CACHE } = {}) {
|
|
26
|
+
try {
|
|
27
|
+
const c = JSON.parse(readFileSync(cache, 'utf8'));
|
|
28
|
+
if (now - c.at < TTL_MS && c.latest) return c.latest;
|
|
29
|
+
} catch { /* no cache */ }
|
|
30
|
+
const r = fetch();
|
|
31
|
+
if (r.code !== 0) return null;
|
|
32
|
+
const tags = r.stdout.split('\n').map((s) => s.trim()).filter((t) => compareVersions(t, '0.0.0') !== null);
|
|
33
|
+
const latest = tags.sort((a, b) => compareVersions(b, a))[0]?.replace(/^v/, '') ?? null;
|
|
34
|
+
if (latest) {
|
|
35
|
+
try { writeFileSync(cache, JSON.stringify({ at: now, latest })); } catch { /* tmp unavailable: no cache */ }
|
|
36
|
+
}
|
|
37
|
+
return latest;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Advice to update if the local crew is older than the latest tag; otherwise null. */
|
|
41
|
+
export function updateHint({ local = LOCAL_VERSION, latest = latestVersion() } = {}) {
|
|
42
|
+
if (!latest || compareVersions(local, latest) !== -1) return null;
|
|
43
|
+
return `crew ${local} is outdated, ${latest} is available: git -C ~/agent-crew fetch --tags && git -C ~/agent-crew checkout v${latest} (fixes do not take effect until you update)`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Minimal MCP server (stdio, JSON-RPC 2.0, one message per line).
|
|
2
|
+
// Exposes crew commands as tools: crew_task_next, crew_task_claim, crew_check, ...
|
|
3
|
+
// Connects to any MCP client: Claude Code, Codex, Cursor, etc. No dependencies.
|
|
4
|
+
import { claimSlot } from '../core/session.js';
|
|
5
|
+
import { agentId } from '../core/config.js';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { agentCommands, toolName, inputSchema, validate } from '../core/registry.js';
|
|
8
|
+
import { CrewError, fail } from '../core/output.js';
|
|
9
|
+
import { recordCommand } from '../core/metrics.js';
|
|
10
|
+
|
|
11
|
+
const SUPPORTED = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
12
|
+
|
|
13
|
+
export async function callTool(name, args = {}) {
|
|
14
|
+
const cmd = agentCommands.find((c) => toolName(c) === name);
|
|
15
|
+
if (!cmd) return fail('unknown_tool', `No such tool: ${name}`, { retry: 'tools/list' });
|
|
16
|
+
const started = Date.now();
|
|
17
|
+
const err = validate(cmd, args);
|
|
18
|
+
let result;
|
|
19
|
+
if (err) result = fail('bad_input', err, { retry: `fix the arguments of ${name}` });
|
|
20
|
+
else {
|
|
21
|
+
try {
|
|
22
|
+
result = await cmd.run(args, {});
|
|
23
|
+
} catch (e) {
|
|
24
|
+
result = e instanceof CrewError ? e.toResult() : fail('internal', e.message, { stop_when: 'it repeats: report to a human' });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Agents work mostly through MCP; without this crew metrics does not see them. Never throws.
|
|
28
|
+
recordCommand({ command: cmd.name, opts: args, result, started });
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function handle(msg) {
|
|
33
|
+
const { id, method, params } = msg;
|
|
34
|
+
switch (method) {
|
|
35
|
+
case 'initialize': {
|
|
36
|
+
const v = SUPPORTED.includes(params?.protocolVersion) ? params.protocolVersion : SUPPORTED[0];
|
|
37
|
+
return { protocolVersion: v, capabilities: { tools: {} }, serverInfo: { name: 'crew', version: '0.1.0' } };
|
|
38
|
+
}
|
|
39
|
+
case 'ping':
|
|
40
|
+
return {};
|
|
41
|
+
case 'tools/list':
|
|
42
|
+
return {
|
|
43
|
+
tools: agentCommands.map((c) => ({ name: toolName(c), description: c.summary, inputSchema: inputSchema(c) })),
|
|
44
|
+
};
|
|
45
|
+
case 'tools/call': {
|
|
46
|
+
const result = await callTool(params?.name, params?.arguments ?? {});
|
|
47
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], isError: result.status === 'error' };
|
|
48
|
+
}
|
|
49
|
+
default:
|
|
50
|
+
if (id === undefined) return undefined; // notification (notifications/initialized etc.)
|
|
51
|
+
throw Object.assign(new Error(`Method not found: ${method}`), { rpcCode: -32601 });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function startServer() {
|
|
56
|
+
// One MCP server per agent session: take a free name slot (claude@pc, claude@pc-2, ...) unless CREW_AGENT_ID is set.
|
|
57
|
+
if (!process.env.CREW_AGENT_ID) {
|
|
58
|
+
const slot = claimSlot(agentId());
|
|
59
|
+
if (slot) {
|
|
60
|
+
process.env.CREW_AGENT_ID = slot.id;
|
|
61
|
+
for (const ev of ['exit', 'SIGINT', 'SIGTERM']) process.once(ev, () => { slot.release(); if (ev !== 'exit') process.exit(0); });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const rl = createInterface({ input: process.stdin });
|
|
65
|
+
const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
|
|
66
|
+
rl.on('line', async (line) => {
|
|
67
|
+
if (!line.trim()) return;
|
|
68
|
+
let msg;
|
|
69
|
+
try {
|
|
70
|
+
msg = JSON.parse(line);
|
|
71
|
+
} catch {
|
|
72
|
+
return send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const result = await handle(msg);
|
|
76
|
+
if (msg.id !== undefined && result !== undefined) send({ jsonrpc: '2.0', id: msg.id, result });
|
|
77
|
+
} catch (e) {
|
|
78
|
+
if (msg.id !== undefined) send({ jsonrpc: '2.0', id: msg.id, error: { code: e.rpcCode ?? -32603, message: e.message } });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"name": "status:needs-spec", "color": "fbca04", "description": "no verifiable acceptance criteria - agents do not take it"},
|
|
3
|
+
{"name": "status:ready", "color": "0e8a16", "description": "ready, can be taken (crew task next)"},
|
|
4
|
+
{"name": "status:claimed", "color": "1d76db", "description": "taken by an agent, work in progress (lease)"},
|
|
5
|
+
{"name": "status:review", "color": "5319e7", "description": "PR open, waiting for review"},
|
|
6
|
+
{"name": "status:blocked", "color": "b60205", "description": "blocked, a human is needed"},
|
|
7
|
+
{"name": "status:waiting", "color": "c5def5", "description": "waiting for dependencies (after); crew task promote moves it to ready"},
|
|
8
|
+
{"name": "status:done", "color": "c2e0c6", "description": "done"},
|
|
9
|
+
{"name": "tier:low", "color": "f9f9f9", "description": "simple task - a junior model will do"},
|
|
10
|
+
{"name": "tier:mid", "color": "e4e4e4", "description": "regular task"},
|
|
11
|
+
{"name": "tier:high", "color": "bfbfbf", "description": "complex - senior model only"},
|
|
12
|
+
{"name": "risk:low", "color": "c2e0c6", "description": "one agent review, auto-merge"},
|
|
13
|
+
{"name": "risk:med", "color": "fbca04", "description": "agent review + tests for the criteria, auto-merge"},
|
|
14
|
+
{"name": "risk:high", "color": "d93f0b", "description": "agent review (security first), merged first by hand, E2E required"},
|
|
15
|
+
{"name": "priority:high", "color": "b60205", "description": "do first: crew task next hands it out before other tasks of the same owner"},
|
|
16
|
+
{"name": "priority:low", "color": "d4d4d4", "description": "do last"},
|
|
17
|
+
{"name": "feedback", "color": "d4c5f9", "description": "human feedback after testing the running app; the product lead triages it regularly"},
|
|
18
|
+
{"name": "epic", "color": "3e4b9e", "description": "a large scenario made of several tasks"},
|
|
19
|
+
{"name": "crew:pause", "color": "000000", "description": "EMERGENCY STOP: all agents stop"},
|
|
20
|
+
{"name": "crew:question", "color": "0052cc", "description": "a question from an agent to the owner of an area (crew ask); answered with crew answer"},
|
|
21
|
+
{"name": "needs-human", "color": "e99695", "description": "only a human can decide this (scope, contract, product choice)"},
|
|
22
|
+
{"name": "crew:digest", "color": "bfdadc", "description": "the pinned team status issue, rewritten by crew digest"}
|
|
23
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"context7": {
|
|
3
|
+
"enabled": false,
|
|
4
|
+
"about": "Up-to-date library and API documentation right in the agent's context. Enable this first.",
|
|
5
|
+
"command": "npx",
|
|
6
|
+
"args": ["-y", "@upstash/context7-mcp"],
|
|
7
|
+
"env_from": []
|
|
8
|
+
},
|
|
9
|
+
"perplexity": {
|
|
10
|
+
"enabled": false,
|
|
11
|
+
"about": "Deep web search for kickoff: the idea, competitors, arguments for the pitch. Key: PERPLEXITY_API_KEY in the environment.",
|
|
12
|
+
"command": "npx",
|
|
13
|
+
"args": ["-y", "VERIFY-PACKAGE-NAME-BEFORE-ENABLING"],
|
|
14
|
+
"env_from": ["PERPLEXITY_API_KEY"]
|
|
15
|
+
},
|
|
16
|
+
"playwright": {
|
|
17
|
+
"enabled": false,
|
|
18
|
+
"about": "A browser for QA and E2E of the demo path.",
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["-y", "@playwright/mcp"],
|
|
21
|
+
"env_from": []
|
|
22
|
+
}
|
|
23
|
+
}
|