@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,130 @@
1
+ // crew check — runs the check steps from crew.config.json and reports errors concisely;
2
+ // the full log goes to .crew/logs/, the result to .crew/last-check.json (required by crew pr open).
3
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { ok, fail } from '../core/output.js';
6
+ import { runShell, git, currentBranch } from '../core/exec.js';
7
+ import { loadConfig, agentId } from '../core/config.js';
8
+ import { checkLease } from '../core/lease.js';
9
+
10
+ const tail = (s, n = 20) => s.trim().split('\n').slice(-n).join('\n');
11
+
12
+ /** Default step timeout, in seconds (step field timeout_s). */
13
+ export const DEFAULT_TIMEOUT_S = 600;
14
+
15
+ /** Default test file names: pytest, jest/vitest/playwright, node --test. */
16
+ export const DEFAULT_TESTS_PATTERN = '(^test_.*\\.py$)|(_test\\.py$)|(\\.(test|spec)\\.[cm]?[jt]sx?$)';
17
+
18
+ /** How many test files are in a folder (recursive, excluding node_modules). */
19
+ export function countTests(dir, pattern = DEFAULT_TESTS_PATTERN) {
20
+ if (!existsSync(dir)) return 0;
21
+ const re = new RegExp(pattern);
22
+ let n = 0;
23
+ for (const e of readdirSync(dir, { withFileTypes: true, recursive: true })) {
24
+ const parent = (e.parentPath ?? e.path ?? '').replace(/\\/g, '/');
25
+ if (e.isFile() && re.test(e.name) && !parent.includes('/node_modules')) n++;
26
+ }
27
+ return n;
28
+ }
29
+
30
+ /**
31
+ * Whether the run counts for crew pr open: all config steps passed (or were skipped by the rules).
32
+ * A partial run (--only) does not count as green.
33
+ */
34
+ export function isFullRun(results = [], steps = []) {
35
+ const names = new Set(results.map((r) => r.name));
36
+ return steps.length > 0 && steps.every((s) => names.has(s.name)) && results.every((r) => r.ok);
37
+ }
38
+
39
+ export default {
40
+ name: 'check',
41
+ summary: 'Run the checks (linter, types, tests) from crew.config.json',
42
+ usesCwd: true,
43
+ args: [{ name: 'only', type: 'string', description: 'run only the step with this name' }],
44
+ run({ only, cwd }, deps = {}) {
45
+ const { config, root } = loadConfig(cwd);
46
+ const all = config.check ?? [];
47
+ const steps = all.filter((s) => !only || s.name === only);
48
+ if (!steps.length) {
49
+ return fail('no_checks', 'No check list is defined in crew.config.json', {
50
+ cause_hint: 'the project is not configured yet',
51
+ retry: 'add steps to "check": [{ "name": "test", "run": "npm test" }]',
52
+ stop_when: 'you do not own the config — tell a human',
53
+ });
54
+ }
55
+ const dir = join(root, '.crew', 'logs');
56
+ mkdirSync(dir, { recursive: true });
57
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
58
+ const logPath = join(dir, `check-${stamp}.txt`);
59
+ // #13 (H5): we extend the lease BEFORE the steps too — a long check (up to timeout_s per step) could otherwise outlive the lease,
60
+ // and sweep would return the task to the queue midway. After the steps — once more (at most once per 5 minutes).
61
+ const id = Number(/^crew\/(\d+)-/.exec(currentBranch(root))?.[1]) || null;
62
+ const leaseCheck = deps.checkLease ?? checkLease;
63
+ if (id) leaseCheck({ id, me: agentId() });
64
+ const results = [];
65
+ let log = '';
66
+ for (const s of steps) {
67
+ // when_exists: the step only makes sense if that part of the project already exists (web/package.json etc.)
68
+ if (s.when_exists && !existsSync(join(root, s.when_exists))) {
69
+ results.push({ name: s.name, ok: true, skipped: true, exit: 0, excerpt: '' });
70
+ continue;
71
+ }
72
+ // tests_dir: a mandatory set (acceptance tests). While there are no tests (the very start) — skipped;
73
+ // once there is at least one, the step must pass with exit code 0 ("no tests found" does not count).
74
+ // An agent cannot delete existing acceptance tests — crew-guard catches that.
75
+ const tests = s.tests_dir ? countTests(join(root, s.tests_dir), s.tests_pattern) : null;
76
+ if (s.tests_dir && tests === 0) {
77
+ results.push({ name: s.name, ok: true, skipped: true, no_tests: true, exit: 0, excerpt: '' });
78
+ continue;
79
+ }
80
+ const timeout_s = Number(s.timeout_s) > 0 ? Number(s.timeout_s) : DEFAULT_TIMEOUT_S;
81
+ const r = runShell(s.run, { cwd: join(root, s.cwd ?? '.'), timeout_s });
82
+ const okCodes = s.tests_dir ? [0] : (s.ok_exit_codes ?? [0]);
83
+ const passed = !r.timedOut && okCodes.includes(r.code);
84
+ log += `\n===== ${s.name}: ${s.run} (${r.timedOut ? `timeout ${timeout_s} s` : `exit ${r.code}`})\n${r.stdout}\n${r.stderr}\n`;
85
+ const output = tail(r.stdout + '\n' + r.stderr, r.timedOut ? 10 : 20);
86
+ const excerpt = passed
87
+ ? ''
88
+ : r.timedOut
89
+ ? `timeout: step "${s.name}" did not finish within ${timeout_s} s (timeout_s) and was stopped — is a test hung, waiting for input, or running a dev server?${output ? '\n' + output : ''}`
90
+ : output;
91
+ results.push({ name: s.name, ok: passed, exit: r.code, ...(tests !== null ? { tests } : {}), ...(r.timedOut ? { timed_out: true, timeout_s } : {}), excerpt });
92
+ if (!passed && s.stop_on_fail !== false) break;
93
+ }
94
+ writeFileSync(logPath, log);
95
+ const sha = git(['rev-parse', 'HEAD'], { cwd: root, allowFail: true }).stdout;
96
+ const dirty = git(['status', '--porcelain'], { cwd: root, allowFail: true }).stdout !== '';
97
+ const partial = !!only;
98
+ const passedSteps = results.every((r) => r.ok) && results.length === steps.length;
99
+ // green — only a full run of all config steps: `--only` does not unlock crew pr open.
100
+ const green = !partial && isFullRun(results, all);
101
+ writeFileSync(join(root, '.crew', 'last-check.json'), JSON.stringify({ sha, dirty, green, partial, at: new Date().toISOString(), results }, null, 2));
102
+
103
+ // On a task branch, check also counts as activity: we extend the lease (at most once per 5 minutes).
104
+ const lease = id ? leaseCheck({ id, me: agentId() }) : null;
105
+ const leaseNote = lease?.state === 'lost' ? [`⚠ task #${id} is no longer yours (${lease.owner ?? 'returned to the queue'}) — crew commit and crew pr open will refuse; crew task claim ${id} if it is ready again`] : [];
106
+
107
+ if (passedSteps) {
108
+ const ran = results.filter((r) => !r.skipped).map((r) => r.name);
109
+ const skipped = results.filter((r) => r.skipped).map((r) => (r.no_tests ? `${r.name} (no tests yet)` : r.name));
110
+ const noTests = results.filter((r) => r.no_tests).map((r) => r.name);
111
+ return ok(`${partial ? `Step ${only} is green (partial run — crew pr open needs a full crew check)` : 'All checks green'} (${ran.join(', ') || 'nothing to check'}${skipped.length ? `; skipped: ${skipped.join(', ')}` : ''})`, {
112
+ status: partial || leaseNote.length ? 'warning' : 'success',
113
+ artifacts: { log: logPath, sha, partial, green, ...(lease ? { lease: lease.state } : {}) },
114
+ next_actions: [
115
+ ...leaseNote,
116
+ ...(noTests.length ? [`no acceptance tests yet (${noTests.join(', ')}) — per AGENTS.md they are written in the task first commit`] : []),
117
+ ...(partial ? ['crew check (full run)'] : dirty ? ['crew commit -m "..."', 'crew check (again, after the commit)', 'crew pr open'] : ['crew pr open']),
118
+ ],
119
+ });
120
+ }
121
+ const failed = results.filter((r) => !r.ok);
122
+ const timedOut = failed.filter((f) => f.timed_out).map((f) => f.name);
123
+ return fail('ci_red', `Failed: ${failed.map((f) => (f.timed_out ? `${f.name} (timeout)` : f.name)).join(', ')}`, {
124
+ cause_hint: failed[0].excerpt,
125
+ retry: 'fix and run crew check again',
126
+ stop_when: 'the same error 3 times in a row → crew task status <id> blocked --note "..."',
127
+ artifacts: { log: logPath, failed: failed.map((f) => f.name), ...(timedOut.length ? { timed_out: timedOut } : {}) },
128
+ });
129
+ },
130
+ };
@@ -0,0 +1,105 @@
1
+ // crew commit — git commit with mandatory trailers (Agent, Issue) so the origin of the code is visible.
2
+ // Only on a task branch crew/<id>-*, only while the task is yours (lease), and without secret-looking files.
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { ok, fail } from '../core/output.js';
6
+ import { git, currentBranch } from '../core/exec.js';
7
+ import { loadConfig, agentId } from '../core/config.js';
8
+ import { checkLease, leaseLostError } from '../core/lease.js';
9
+ import { issueFromBranch } from './pr.js';
10
+
11
+ /** Files that are almost always secrets or dumps (by name). */
12
+ const SECRET_FILES = [
13
+ [/(^|\/)\.env(\.[^/]*)?$/i, '.env file'],
14
+ [/\.(pem|key|p12|pfx|jks|keystore)$/i, 'key/certificate'],
15
+ [/(^|\/)id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/i, 'SSH key'],
16
+ [/(^|\/)(credentials|service[-_]?account|secrets?)[^/]*\.(json|ya?ml|toml)$/i, 'credentials file'],
17
+ [/\.(sqlite3?|db|dump)$/i, 'database / dump'],
18
+ ];
19
+ const SECRET_FILE_OK = [/(^|\/)\.env\.(example|sample|template)$/i];
20
+
21
+ /** Lines that look like tokens and keys (in added diff lines). */
22
+ const SECRET_TEXT = [
23
+ [/-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/, 'private key'],
24
+ [/\bAKIA[0-9A-Z]{16}\b/, 'AWS access key'],
25
+ [/\bgh[pousr]_[A-Za-z0-9]{36,}\b|\bgithub_pat_[A-Za-z0-9_]{40,}\b/, 'GitHub token'],
26
+ [/\bsk-ant-[A-Za-z0-9_-]{20,}/, 'Anthropic API key'],
27
+ [/\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}/, 'OpenAI API key'],
28
+ [/\bxox[abposr]-[A-Za-z0-9-]{10,}/, 'Slack token'],
29
+ [/\bAIza[0-9A-Za-z_-]{35}\b/, 'Google API key'],
30
+ ];
31
+
32
+ /**
33
+ * Simple index check before committing (pure function).
34
+ * names — paths from `git diff --cached --name-only`; diff — `git diff --cached -U0`.
35
+ * → [{ file, reason }]
36
+ */
37
+ export function scanStaged(names = [], diff = '') {
38
+ const hits = [];
39
+ for (const f of names) {
40
+ const p = f.replace(/\\/g, '/');
41
+ if (SECRET_FILE_OK.some((re) => re.test(p))) continue;
42
+ const m = SECRET_FILES.find(([re]) => re.test(p));
43
+ if (m) hits.push({ file: p, reason: m[1] });
44
+ }
45
+ let file = null;
46
+ for (const line of diff.split(/\r?\n/)) {
47
+ if (line.startsWith('+++ ')) file = line.replace(/^\+\+\+ (b\/)?/, '');
48
+ // Template names are allowed, but their contents must still be scanned for credentials.
49
+ else if (line.startsWith('+') && file) {
50
+ const m = SECRET_TEXT.find(([re]) => re.test(line));
51
+ if (m && !hits.some((h) => h.file === file && h.reason === m[1])) hits.push({ file, reason: m[1] });
52
+ }
53
+ }
54
+ return hits;
55
+ }
56
+
57
+ export default {
58
+ name: 'commit',
59
+ summary: 'Commit everything with Agent/Issue trailers (task branch only, no secrets)',
60
+ usesCwd: true,
61
+ args: [{ name: 'm', type: 'string', required: true, description: 'commit message (Conventional Commits: feat: ..., fix: ...)' }],
62
+ run({ m, cwd }, deps = {}) {
63
+ const { root } = loadConfig(cwd);
64
+ const branch = currentBranch(root);
65
+ const id = issueFromBranch(branch);
66
+ if (!id) {
67
+ return fail('not_task_branch', `Branch ${branch} is not a task branch crew/<number>-...`, {
68
+ cause_hint: 'commits to main and to arbitrary branches are forbidden (AGENTS.md): work happens only from a claimed task',
69
+ retry: 'crew task next → crew task claim <id> → work in the issued worktree',
70
+ stop_when: 'this is project setup by a human — then use a plain git commit',
71
+ });
72
+ }
73
+ const me = agentId();
74
+ const lease = (deps.checkLease ?? checkLease)({ id, me });
75
+ if (lease.state === 'lost') return leaseLostError(fail, id, lease, me);
76
+
77
+ git(['add', '-A'], { cwd: root });
78
+ // .crew/ is gitignored (local state), but before/after screenshots belong to the PR (crew shots).
79
+ if (existsSync(join(root, '.crew', 'shots'))) git(['add', '-f', '--', '.crew/shots'], { cwd: root });
80
+ const names = git(['diff', '--cached', '--name-only'], { cwd: root }).stdout.split('\n').filter(Boolean);
81
+ if (!names.length) return fail('nothing_to_commit', 'Nothing to commit', { retry: 'crew check' });
82
+ const hits = scanStaged(names, git(['diff', '--cached', '-U0', '--no-color'], { cwd: root }).stdout);
83
+ if (hits.length) {
84
+ git(['reset', '-q'], { cwd: root }); // unstage; leave the files untouched
85
+ return fail('secret_suspected', `Looks like secrets, commit not made: ${hits.map((h) => `${h.file} (${h.reason})`).join(', ')}`, {
86
+ cause_hint: 'secrets belong only in .env (gitignored); the repo holds .env.example with empty values',
87
+ retry: 'remove the file/line (or add the file to .gitignore) and run crew commit -m "..." again',
88
+ stop_when: 'it is definitely not a secret (test fixture) — rename the value so it does not look like a key; do not bypass the check',
89
+ artifacts: { hits },
90
+ });
91
+ }
92
+ const trailers = [`Agent: ${me}`, `Issue: #${id}`];
93
+ git(['commit', '-m', m, '-m', trailers.join('\n')], { cwd: root });
94
+ const sha = git(['rev-parse', '--short', 'HEAD'], { cwd: root }).stdout;
95
+ return ok(`Commit ${sha}: ${m}`, {
96
+ status: lease.state === 'unknown' ? 'warning' : 'success',
97
+ artifacts: { sha, issue: id, lease: lease.state, heartbeat: !!lease.heartbeat },
98
+ next_actions: [
99
+ ...(lease.state === 'unknown' ? [`could not verify the lease (${lease.error}) — extend it manually: crew task heartbeat ${id}`] : []),
100
+ 'crew check',
101
+ 'crew pr open',
102
+ ],
103
+ });
104
+ },
105
+ };
@@ -0,0 +1,109 @@
1
+ // crew compliance check | disclosure — compliance with the event rules (event mode).
2
+ // Event rules live in events/<name>.json (start, end, ai_allowed, team_members, ...).
3
+ import { writeFileSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { ok, warn, fail } from '../core/output.js';
6
+ import { git, run } from '../core/exec.js';
7
+ import { loadConfig, loadEvent, HARNESS_ROOT } from '../core/config.js';
8
+
9
+ /** Commits with changes, excluding service merge commits: author date, author, Agent trailer. */
10
+ export function commits(root) {
11
+ const out = git(['log', '--no-merges', '--format=%H%x1f%aI%x1f%ae%x1f%(trailers:key=Agent,valueonly,separator=%x2c)%x1e'], { cwd: root, allowFail: true });
12
+ if (out.code !== 0) return [];
13
+ return out.stdout.split('\x1e').map((r) => r.trim()).filter(Boolean).map((r) => {
14
+ const [sha, date, email, agent] = r.split('\x1f');
15
+ return { sha, date, email, agent: (agent ?? '').trim() || null };
16
+ });
17
+ }
18
+
19
+ /** Pure check function — the tests run it too. */
20
+ export function evaluate(event, list, prAuthors = []) {
21
+ const problems = [];
22
+ const warnings = [];
23
+ const start = Date.parse(event.start);
24
+ const end = Date.parse(event.end);
25
+ const early = list.filter((c) => Date.parse(c.date) < start);
26
+ const late = list.filter((c) => Date.parse(c.date) > end);
27
+ if (early.length) problems.push(`commits before the start: ${early.length} (first ${early.at(-1).sha.slice(0, 7)})`);
28
+ if (late.length) problems.push(`commits after the end: ${late.length}`);
29
+ const agentCommits = list.filter((c) => c.agent && c.agent !== 'human');
30
+ if (event.ai_allowed === false && agentCommits.length) problems.push(`AI is forbidden, but agent commits: ${agentCommits.length}`);
31
+ const team = new Set((event.team_members ?? []).map((s) => s.toLowerCase()));
32
+ if (team.size) {
33
+ const outsiders = [...new Set(prAuthors.filter((a) => !team.has(a.toLowerCase())))];
34
+ if (outsiders.length) problems.push(`PRs from humans outside the team: ${outsiders.join(', ')}`);
35
+ } else {
36
+ warnings.push('team_members is empty — team membership is not checked');
37
+ }
38
+ const noTrailer = list.filter((c) => !c.agent && Date.parse(c.date) >= start);
39
+ if (noTrailer.length) problems.push(`commits without the Agent trailer: ${noTrailer.length} (use crew commit)`);
40
+ return { problems, warnings, stats: { commits: list.length, agent_commits: agentCommits.length } };
41
+ }
42
+
43
+ const check = {
44
+ name: 'compliance check',
45
+ summary: 'Event mode: check the repo against the event rules (time window, team membership, AI use)',
46
+ usesCwd: true,
47
+ args: [],
48
+ run({ cwd } = {}) {
49
+ const { config, root } = loadConfig(cwd);
50
+ const event = loadEvent(config, root);
51
+ if (!event) {
52
+ return warn('No event configured — nothing to check', {
53
+ next_actions: ['fill in events/event.json and set "event": "events/event.json" in crew.config.json'],
54
+ });
55
+ }
56
+ const pr = run('gh', ['pr', 'list', '--state', 'all', '--limit', '500', '--json', 'author'], { cwd: root, allowFail: true });
57
+ const authors = pr.code === 0 ? JSON.parse(pr.stdout || '[]').map((p) => p.author?.login).filter(Boolean) : [];
58
+ const res = evaluate(event, commits(root), authors);
59
+ if (res.problems.length) {
60
+ return fail('compliance_violation', `Violations: ${res.problems.length}`, {
61
+ cause_hint: res.problems.join('; '),
62
+ stop_when: 'always — a human decides this; agents do not rewrite history',
63
+ next_actions: ['tell the team captain', 'do not rewrite git history'],
64
+ artifacts: res,
65
+ });
66
+ }
67
+ return (res.warnings.length ? warn : ok)(`Compliance with the rules of "${event.name}": OK`, { artifacts: res });
68
+ },
69
+ };
70
+
71
+ const disclosure = {
72
+ name: 'compliance disclosure',
73
+ summary: 'Generate DISCLOSURE.md: which tools and agents were used',
74
+ agent: false,
75
+ args: [],
76
+ run() {
77
+ const { config, root } = loadConfig();
78
+ const event = loadEvent(config, root) ?? {};
79
+ const list = commits(root);
80
+ const byAgent = {};
81
+ for (const c of list) byAgent[c.agent ?? 'unknown'] = (byAgent[c.agent ?? 'unknown'] ?? 0) + 1;
82
+ const version = JSON.parse(readFileSync(join(HARNESS_ROOT, 'package.json'), 'utf8')).version;
83
+ const md = [
84
+ '# Disclosure',
85
+ '',
86
+ `Event: **${event.name ?? '—'}** (${event.start ?? '?'} — ${event.end ?? '?'})`,
87
+ '',
88
+ '## Tools',
89
+ `- [agent-crew](${config.harness_url ?? 'https://github.com/<org>/agent-crew'}) v${version} — an open-source tool for coordinating agents (rules, templates, CLI). Contains no product code.`,
90
+ `- AI agents: ${config.hosts.join(', ')}${config.ecc?.enabled !== false ? '; ECC plugin for Claude Code' : ''}.`,
91
+ `- Stack generators: ${(config.scaffold ?? []).map((s) => '`' + s + '`').join(', ') || '—'}`,
92
+ '',
93
+ '## What was done before the event',
94
+ '- Only tooling and public dependencies. All product code was written during the event.',
95
+ '',
96
+ '## Commit provenance',
97
+ '| Agent / author | Commits |',
98
+ '|---|---|',
99
+ ...Object.entries(byAgent).map(([a, n]) => `| ${a} | ${n} |`),
100
+ '',
101
+ 'Every agent commit carries an `Agent:` trailer and a link to the task (`Issue: #N`).',
102
+ '',
103
+ ].join('\n');
104
+ writeFileSync(join(root, 'DISCLOSURE.md'), md);
105
+ return ok('DISCLOSURE.md created', { artifacts: { path: join(root, 'DISCLOSURE.md'), by_agent: byAgent } });
106
+ },
107
+ };
108
+
109
+ export default [check, disclosure];
@@ -0,0 +1,48 @@
1
+ // crew doctor — check that the machine is ready to work.
2
+ import { ok, fail } from '../core/output.js';
3
+ import { run } from '../core/exec.js';
4
+ import { loadConfig, agentId } from '../core/config.js';
5
+ import { LOCAL_VERSION, updateHint } from '../core/version.js';
6
+
7
+ const probe = (cmd, args) => {
8
+ const r = run(cmd, args, { allowFail: true });
9
+ return r.code === 0 ? (r.stdout || r.stderr).split('\n')[0] : null;
10
+ };
11
+
12
+ export default {
13
+ name: 'doctor',
14
+ summary: 'Check the environment: node, git, gh (auth), claude, codex, project config',
15
+ args: [],
16
+ run() {
17
+ const { config, path } = loadConfig();
18
+ const winCmd = process.platform === 'win32';
19
+ const tool = (name, args) => probe(name, args) ?? (winCmd ? probe('cmd', ['/c', name, ...args]) : null);
20
+ const items = {
21
+ node: process.version,
22
+ git: tool('git', ['--version']),
23
+ gh: tool('gh', ['--version']),
24
+ gh_auth: run('gh', ['auth', 'status'], { allowFail: true }).code === 0 ? 'ok' : null,
25
+ config: path ?? null,
26
+ agent_id: agentId(),
27
+ };
28
+ for (const h of config.hosts ?? []) items[h] = tool(h, ['--version']);
29
+ const required = ['git', 'gh', 'gh_auth'];
30
+ const missing = required.filter((k) => !items[k]);
31
+ const hostsMissing = (config.hosts ?? []).filter((h) => !items[h]);
32
+ if (missing.length) {
33
+ return fail('env_missing', `Missing: ${missing.join(', ')}`, {
34
+ cause_hint: missing.includes('gh_auth') ? 'gh is not authenticated' : 'tool is not installed',
35
+ retry: missing.includes('gh_auth') ? 'gh auth login' : 'install it and re-run crew doctor',
36
+ artifacts: items,
37
+ });
38
+ }
39
+ items.crew = LOCAL_VERSION;
40
+ const hint = updateHint();
41
+ const notes = [hostsMissing.length ? `agents missing: ${hostsMissing.join(', ')}` : null, hint ? `crew is outdated (${LOCAL_VERSION})` : null].filter(Boolean);
42
+ return ok(notes.length ? `Environment OK, but ${notes.join('; ')}` : 'Environment ready', {
43
+ status: notes.length ? 'warning' : 'success',
44
+ artifacts: items,
45
+ next_actions: [...(hint ? [hint] : []), ...(path ? ['crew status'] : ['crew init <dir> — create a project'])],
46
+ });
47
+ },
48
+ };