@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.
Files changed (92) hide show
  1. package/AGENTS.md +24 -0
  2. package/LICENSE +201 -0
  3. package/README.md +411 -2
  4. package/README.ru.md +414 -0
  5. package/adapters/claude/adapter.js +107 -0
  6. package/adapters/codex/adapter.js +101 -0
  7. package/bin/crew.js +60 -0
  8. package/docs/AGENT-SETUP.md +138 -0
  9. package/ecc/profile.json +16 -0
  10. package/evals/README.md +123 -0
  11. package/evals/fixtures/mini-app/README.md +10 -0
  12. package/evals/fixtures/mini-app/contracts/health.json +11 -0
  13. package/evals/fixtures/mini-app/package.json +10 -0
  14. package/evals/fixtures/mini-app/src/cart.js +14 -0
  15. package/evals/fixtures/mini-app/src/handlers.js +16 -0
  16. package/evals/fixtures/mini-app/src/pager.js +7 -0
  17. package/evals/fixtures/mini-app/test/cart.test.js +10 -0
  18. package/evals/fixtures/mini-app/test/handlers.test.js +13 -0
  19. package/evals/fixtures/mini-app/test/pager.test.js +8 -0
  20. package/evals/tasks/add-input-validation.json +15 -0
  21. package/evals/tasks/fix-off-by-one.json +14 -0
  22. package/evals/tasks/handler-per-contract.json +15 -0
  23. package/evals/tasks/judges/add-input-validation.judge.mjs +28 -0
  24. package/evals/tasks/judges/fix-off-by-one.judge.mjs +22 -0
  25. package/evals/tasks/judges/handler-per-contract.judge.mjs +18 -0
  26. package/package.json +46 -4
  27. package/roles/analyst.md +11 -0
  28. package/roles/builder.md +16 -0
  29. package/roles/implementer.md +17 -0
  30. package/roles/integrator.md +12 -0
  31. package/roles/planner.md +19 -0
  32. package/roles/reviewer.md +16 -0
  33. package/roles/scout.md +15 -0
  34. package/skills/crew-brief-analysis/SKILL.md +32 -0
  35. package/skills/crew-kickoff/SKILL.md +77 -0
  36. package/skills/crew-kickoff/task-format.md +215 -0
  37. package/skills/crew-review/SKILL.md +22 -0
  38. package/skills/crew-workflow/SKILL.md +43 -0
  39. package/src/commands/adapters.js +85 -0
  40. package/src/commands/check.js +130 -0
  41. package/src/commands/commit.js +105 -0
  42. package/src/commands/compliance.js +109 -0
  43. package/src/commands/doctor.js +48 -0
  44. package/src/commands/eval.js +507 -0
  45. package/src/commands/github.js +169 -0
  46. package/src/commands/guard.js +101 -0
  47. package/src/commands/init.js +76 -0
  48. package/src/commands/kickoff.js +275 -0
  49. package/src/commands/labels.js +28 -0
  50. package/src/commands/metrics.js +48 -0
  51. package/src/commands/pr.js +211 -0
  52. package/src/commands/shots.js +51 -0
  53. package/src/commands/status.js +48 -0
  54. package/src/commands/task.js +569 -0
  55. package/src/commands/up.js +355 -0
  56. package/src/commands/worker.js +322 -0
  57. package/src/core/config.js +77 -0
  58. package/src/core/exec.js +50 -0
  59. package/src/core/guard.js +248 -0
  60. package/src/core/issues.js +154 -0
  61. package/src/core/lease.js +48 -0
  62. package/src/core/metrics.js +232 -0
  63. package/src/core/output.js +56 -0
  64. package/src/core/prd.js +593 -0
  65. package/src/core/proc.js +52 -0
  66. package/src/core/queue.js +92 -0
  67. package/src/core/registry.js +138 -0
  68. package/src/core/shots.js +68 -0
  69. package/src/core/supervisor.js +56 -0
  70. package/src/core/version.js +44 -0
  71. package/src/mcp/server.js +71 -0
  72. package/templates/labels.json +18 -0
  73. package/templates/mcp.optional.json +23 -0
  74. package/templates/product/.github/ISSUE_TEMPLATE/epic.yml +22 -0
  75. package/templates/product/.github/ISSUE_TEMPLATE/feedback.yml +36 -0
  76. package/templates/product/.github/ISSUE_TEMPLATE/task.yml +62 -0
  77. package/templates/product/.github/pull_request_template.md +16 -0
  78. package/templates/product/.github/workflows/crew-guard.yml +45 -0
  79. package/templates/product/.github/workflows/crew.yml +108 -0
  80. package/templates/product/AGENTS.md +109 -0
  81. package/templates/product/contracts/README.md +8 -0
  82. package/templates/product/docs/architecture.md +41 -0
  83. package/templates/product/docs/brief-analysis.md +62 -0
  84. package/templates/product/docs/brief.md +25 -0
  85. package/templates/product/docs/playbooks/issues.md +91 -0
  86. package/templates/product/docs/playbooks/preflight.md +28 -0
  87. package/templates/product/docs/playbooks/timeline.md +52 -0
  88. package/templates/product/docs/prd.md +58 -0
  89. package/templates/product/events/event.example.json +10 -0
  90. package/templates/product/tests/acceptance/README.md +9 -0
  91. package/templates/stacks/web-fastapi.json +74 -0
  92. package/templates/stacks/web-node.json +70 -0
@@ -0,0 +1,92 @@
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: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
+ return { issue: i, owners, mine, rank: mine ? 0 : owners.length ? 2 : 1 };
90
+ })
91
+ .sort((a, b) => a.rank - b.rank || a.issue.number - b.issue.number);
92
+ }
@@ -0,0 +1,138 @@
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 { existsSync, realpathSync } from 'node:fs';
32
+ import { isAbsolute, resolve } from 'node:path';
33
+ import { loadConfig } from './config.js';
34
+ import { metricsRoot } from './metrics.js';
35
+
36
+ /** Shared argument for commands with usesCwd: true. */
37
+ export const CWD_ARG = {
38
+ name: 'cwd',
39
+ type: 'string',
40
+ description: 'absolute path of the working copy (the task worktree from crew_task_claim -> artifacts.worktree); defaults to the current folder',
41
+ };
42
+
43
+ /** Command arguments including the shared ones (cwd). */
44
+ export const argsOf = (cmd) => [...(cmd.args ?? []), ...(cmd.usesCwd && !(cmd.args ?? []).some((a) => a.name === 'cwd') ? [CWD_ARG] : [])];
45
+
46
+ export const commands = [...task, ...kickoff, check, ...pr, guard, status, commit, init, up, adapters, labels, ...compliance, doctor, worker, github, metrics, evalCmd, shots];
47
+
48
+ /** For MCP: only commands safe for agents (no init/labels/adapters; a human does those). */
49
+ export const agentCommands = commands.filter((c) => c.agent !== false);
50
+
51
+ export function findCommand(words) {
52
+ // longest name match: "task claim 42" -> 'task claim'
53
+ let best = null;
54
+ for (const c of commands) {
55
+ const parts = c.name.split(' ');
56
+ if (parts.every((p, i) => words[i] === p) && (!best || parts.length > best.name.split(' ').length)) best = c;
57
+ }
58
+ return best;
59
+ }
60
+
61
+ export function toolName(cmd) {
62
+ return 'crew_' + cmd.name.replace(/[\s-]+/g, '_');
63
+ }
64
+
65
+ export function inputSchema(cmd) {
66
+ const properties = {};
67
+ const required = [];
68
+ for (const a of argsOf(cmd)) {
69
+ properties[a.name] = { type: a.type === 'number' ? 'number' : a.type === 'boolean' ? 'boolean' : 'string', description: a.description };
70
+ if (a.enum) properties[a.name].enum = a.enum;
71
+ if (a.required) required.push(a.name);
72
+ }
73
+ return { type: 'object', properties, required, additionalProperties: false };
74
+ }
75
+
76
+ /** argv words -> options object per the args spec. */
77
+ export function parseArgs(cmd, rest) {
78
+ const opts = {};
79
+ const specs = argsOf(cmd);
80
+ const positionals = specs.filter((a) => a.positional);
81
+ let pi = 0;
82
+ for (let i = 0; i < rest.length; i++) {
83
+ const w = rest[i];
84
+ const isShort = /^-[a-zA-Z]$/.test(w); // -m "msg", as in git
85
+ if (w.startsWith('--') || isShort) {
86
+ const [k, v] = w.slice(isShort ? 1 : 2).split('=');
87
+ const spec = specs.find((a) => a.name === k.replace(/-/g, '_'));
88
+ const key = spec?.name ?? k.replace(/-/g, '_');
89
+ if (spec?.type === 'boolean' || (v === undefined && (rest[i + 1] === undefined || rest[i + 1].startsWith('--')))) {
90
+ opts[key] = v === undefined ? true : v !== 'false';
91
+ } else {
92
+ opts[key] = v ?? rest[++i];
93
+ }
94
+ } else if (pi < positionals.length) {
95
+ opts[positionals[pi++].name] = w;
96
+ }
97
+ }
98
+ for (const a of specs) {
99
+ if (a.type === 'number' && opts[a.name] !== undefined) opts[a.name] = Number(opts[a.name]);
100
+ }
101
+ return opts;
102
+ }
103
+
104
+ /**
105
+ * cwd may only point inside the crew project and only the same repository crew runs in
106
+ * (the main copy or its worktree). Otherwise an agent could commit/push to another repo on the machine.
107
+ * home is the project root where crew/the MCP server runs (default process.cwd()).
108
+ */
109
+ export function cwdScopeError(cwd, home = process.cwd()) {
110
+ const target = loadConfig(cwd);
111
+ if (!target.path) return `cwd is not a crew project (no crew.config.json up the folder tree): ${cwd}`;
112
+ const here = loadConfig(home);
113
+ if (!here.path) return null; // crew runs outside a project: it is enough that cwd itself is a crew project
114
+ const real = (p) => { try { return realpathSync.native(p); } catch { return resolve(p); } };
115
+ const norm = (p) => {
116
+ const s = real(p).replace(/\\/g, '/').replace(/\/+$/, '');
117
+ return process.platform === 'win32' ? s.toLowerCase() : s;
118
+ };
119
+ if (norm(metricsRoot(target.root)) !== norm(metricsRoot(here.root))) {
120
+ return `cwd is outside the current project: ${cwd} (only ${here.root} and its worktrees are allowed)`;
121
+ }
122
+ return null;
123
+ }
124
+
125
+ export function validate(cmd, opts) {
126
+ if (cmd.usesCwd && opts.cwd !== undefined) {
127
+ if (typeof opts.cwd !== 'string' || !isAbsolute(opts.cwd)) return 'cwd must be an absolute path';
128
+ if (!existsSync(opts.cwd)) return `cwd does not exist: ${opts.cwd}`;
129
+ const scope = cwdScopeError(opts.cwd);
130
+ if (scope) return scope;
131
+ }
132
+ for (const a of argsOf(cmd)) {
133
+ if (a.required && (opts[a.name] === undefined || opts[a.name] === '')) return `missing argument: ${a.name}`;
134
+ if (a.type === 'number' && opts[a.name] !== undefined && !Number.isFinite(opts[a.name])) return `${a.name} must be a number`;
135
+ if (a.enum && opts[a.name] !== undefined && !a.enum.includes(opts[a.name])) return `${a.name}: one of ${a.enum.join(', ')}`;
136
+ }
137
+ return null;
138
+ }
@@ -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 ? `![](https://github.com/${repo}/blob/${sha}/${p}?raw=true)` : '_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,71 @@
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 { createInterface } from 'node:readline';
5
+ import { agentCommands, toolName, inputSchema, validate } from '../core/registry.js';
6
+ import { CrewError, fail } from '../core/output.js';
7
+ import { recordCommand } from '../core/metrics.js';
8
+
9
+ const SUPPORTED = ['2025-06-18', '2025-03-26', '2024-11-05'];
10
+
11
+ export async function callTool(name, args = {}) {
12
+ const cmd = agentCommands.find((c) => toolName(c) === name);
13
+ if (!cmd) return fail('unknown_tool', `No such tool: ${name}`, { retry: 'tools/list' });
14
+ const started = Date.now();
15
+ const err = validate(cmd, args);
16
+ let result;
17
+ if (err) result = fail('bad_input', err, { retry: `fix the arguments of ${name}` });
18
+ else {
19
+ try {
20
+ result = await cmd.run(args, {});
21
+ } catch (e) {
22
+ result = e instanceof CrewError ? e.toResult() : fail('internal', e.message, { stop_when: 'it repeats: report to a human' });
23
+ }
24
+ }
25
+ // Agents work mostly through MCP; without this crew metrics does not see them. Never throws.
26
+ recordCommand({ command: cmd.name, opts: args, result, started });
27
+ return result;
28
+ }
29
+
30
+ export async function handle(msg) {
31
+ const { id, method, params } = msg;
32
+ switch (method) {
33
+ case 'initialize': {
34
+ const v = SUPPORTED.includes(params?.protocolVersion) ? params.protocolVersion : SUPPORTED[0];
35
+ return { protocolVersion: v, capabilities: { tools: {} }, serverInfo: { name: 'crew', version: '0.1.0' } };
36
+ }
37
+ case 'ping':
38
+ return {};
39
+ case 'tools/list':
40
+ return {
41
+ tools: agentCommands.map((c) => ({ name: toolName(c), description: c.summary, inputSchema: inputSchema(c) })),
42
+ };
43
+ case 'tools/call': {
44
+ const result = await callTool(params?.name, params?.arguments ?? {});
45
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], isError: result.status === 'error' };
46
+ }
47
+ default:
48
+ if (id === undefined) return undefined; // notification (notifications/initialized etc.)
49
+ throw Object.assign(new Error(`Method not found: ${method}`), { rpcCode: -32601 });
50
+ }
51
+ }
52
+
53
+ export function startServer() {
54
+ const rl = createInterface({ input: process.stdin });
55
+ const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
56
+ rl.on('line', async (line) => {
57
+ if (!line.trim()) return;
58
+ let msg;
59
+ try {
60
+ msg = JSON.parse(line);
61
+ } catch {
62
+ return send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
63
+ }
64
+ try {
65
+ const result = await handle(msg);
66
+ if (msg.id !== undefined && result !== undefined) send({ jsonrpc: '2.0', id: msg.id, result });
67
+ } catch (e) {
68
+ if (msg.id !== undefined) send({ jsonrpc: '2.0', id: msg.id, error: { code: e.rpcCode ?? -32603, message: e.message } });
69
+ }
70
+ });
71
+ }
@@ -0,0 +1,18 @@
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": "two agent reviews (one security), merged first, E2E required" },
15
+ { "name": "feedback", "color": "d4c5f9", "description": "human feedback after testing the running app; the product lead triages it regularly" },
16
+ { "name": "epic", "color": "3e4b9e", "description": "a large scenario made of several tasks" },
17
+ { "name": "crew:pause", "color": "000000", "description": "EMERGENCY STOP: all agents stop" }
18
+ ]
@@ -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
+ }
@@ -0,0 +1,22 @@
1
+ name: Epic
2
+ description: A large user scenario that is sliced into tasks.
3
+ labels: ["epic"]
4
+ body:
5
+ - type: textarea
6
+ id: scenario
7
+ attributes:
8
+ label: User scenario
9
+ description: What the user can do once the epic is done.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: tasks
14
+ attributes:
15
+ label: Tasks
16
+ description: Links to tasks (#N). Vertical slices, not layers.
17
+ value: |
18
+ - [ ] #
19
+ - type: textarea
20
+ id: demo
21
+ attributes:
22
+ label: How to demo
@@ -0,0 +1,36 @@
1
+ name: Feedback
2
+ description: Tested the running app - what is wrong or what to improve. The product lead regularly turns feedback into tasks.
3
+ labels: ["feedback"]
4
+ body:
5
+ - type: dropdown
6
+ id: kind
7
+ attributes:
8
+ label: What is it
9
+ options: ["Bug", "Design / UX", "Functionality", "Idea"]
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: did
14
+ attributes:
15
+ label: What you did
16
+ description: Steps to reproduce. A link to the page of the running app.
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: expected
21
+ attributes:
22
+ label: What you expected
23
+ validations:
24
+ required: true
25
+ - type: textarea
26
+ id: actual
27
+ attributes:
28
+ label: What you saw
29
+ description: You can paste a screenshot (Ctrl+V).
30
+ - type: dropdown
31
+ id: severity
32
+ attributes:
33
+ label: How important
34
+ options: ["Breaks the demo", "Noticeable to stakeholders", "Minor"]
35
+ validations:
36
+ required: true
@@ -0,0 +1,62 @@
1
+ name: Task
2
+ description: One task for one agent. Without acceptance criteria an agent will not take it.
3
+ labels: ["status:needs-spec"]
4
+ body:
5
+ - type: textarea
6
+ id: goal
7
+ attributes:
8
+ label: Goal
9
+ description: One sentence - why this task exists.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: acceptance
14
+ attributes:
15
+ label: Acceptance criteria
16
+ description: Given / When / Then. Every item must be verifiable by a test.
17
+ value: |
18
+ - [ ] Given ... When ... Then ...
19
+ validations:
20
+ required: true
21
+ - type: input
22
+ id: contract
23
+ attributes:
24
+ label: Contract
25
+ description: A link to a part of the contract, e.g. contracts/api.yaml#/paths/~1auth
26
+ - type: input
27
+ id: paths
28
+ attributes:
29
+ label: Allowed paths
30
+ description: For example web/src/features/auth/**
31
+ - type: input
32
+ id: after
33
+ attributes:
34
+ label: Dependencies
35
+ description: "After #N, #M - until they are closed the task stays in status:waiting (crew task promote moves it to ready)"
36
+ - type: input
37
+ id: req
38
+ attributes:
39
+ label: Brief requirements
40
+ description: Requirement IDs from docs/brief-analysis.md, e.g. R1, R3
41
+ - type: textarea
42
+ id: changes_acceptance
43
+ attributes:
44
+ label: Changes acceptance
45
+ description: "Only if the task changes EXISTING acceptance tests (e.g. because of feedback). One path per line: tests/acceptance/test_12_login.py. The task's PR cannot change any other existing tests (crew-guard)."
46
+ - type: textarea
47
+ id: out_of_scope
48
+ attributes:
49
+ label: Out of scope
50
+ - type: dropdown
51
+ id: risk
52
+ attributes:
53
+ label: Risk
54
+ options: ["low", "med", "high"]
55
+ - type: textarea
56
+ id: tests
57
+ attributes:
58
+ label: Required tests
59
+ value: |
60
+ - [ ] unit
61
+ - [ ] contract
62
+ - [ ] e2e (if it touches the demo path)
@@ -0,0 +1,16 @@
1
+ Closes #
2
+
3
+ **Agent:** `` <!-- claude/opus, codex/..., human -->
4
+
5
+ ## What was done
6
+ -
7
+
8
+ ## Checks
9
+ - [ ] `crew check` is green
10
+
11
+ ## Review checklist (filled in by a reviewer from another vendor, with evidence: file:line or test)
12
+ - [ ] Behavior regressions / the demo path is not broken
13
+ - [ ] Security assumptions (secrets, auth, untrusted input, prompt injection)
14
+ - [ ] Data integrity and conformance to `contracts/`
15
+ - [ ] Error handling (no swallowed exceptions, fallbacks for LLM/API)
16
+ - [ ] Rollout safety (can be rolled back, main stays demo-ready)