@gredescerber/agent-crew 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +24 -0
- package/LICENSE +201 -0
- package/README.md +411 -2
- package/README.ru.md +414 -0
- package/adapters/claude/adapter.js +107 -0
- package/adapters/codex/adapter.js +101 -0
- package/bin/crew.js +60 -0
- package/docs/AGENT-SETUP.md +138 -0
- package/ecc/profile.json +16 -0
- package/evals/README.md +123 -0
- package/evals/fixtures/mini-app/README.md +10 -0
- package/evals/fixtures/mini-app/contracts/health.json +11 -0
- package/evals/fixtures/mini-app/package.json +10 -0
- package/evals/fixtures/mini-app/src/cart.js +14 -0
- package/evals/fixtures/mini-app/src/handlers.js +16 -0
- package/evals/fixtures/mini-app/src/pager.js +7 -0
- package/evals/fixtures/mini-app/test/cart.test.js +10 -0
- package/evals/fixtures/mini-app/test/handlers.test.js +13 -0
- package/evals/fixtures/mini-app/test/pager.test.js +8 -0
- package/evals/tasks/add-input-validation.json +15 -0
- package/evals/tasks/fix-off-by-one.json +14 -0
- package/evals/tasks/handler-per-contract.json +15 -0
- package/evals/tasks/judges/add-input-validation.judge.mjs +28 -0
- package/evals/tasks/judges/fix-off-by-one.judge.mjs +22 -0
- package/evals/tasks/judges/handler-per-contract.judge.mjs +18 -0
- package/package.json +46 -4
- package/roles/analyst.md +11 -0
- package/roles/builder.md +16 -0
- package/roles/implementer.md +17 -0
- package/roles/integrator.md +12 -0
- package/roles/planner.md +19 -0
- package/roles/reviewer.md +16 -0
- package/roles/scout.md +15 -0
- package/skills/crew-brief-analysis/SKILL.md +32 -0
- package/skills/crew-kickoff/SKILL.md +77 -0
- package/skills/crew-kickoff/task-format.md +215 -0
- package/skills/crew-review/SKILL.md +22 -0
- package/skills/crew-workflow/SKILL.md +43 -0
- package/src/commands/adapters.js +85 -0
- package/src/commands/check.js +130 -0
- package/src/commands/commit.js +105 -0
- package/src/commands/compliance.js +109 -0
- package/src/commands/doctor.js +48 -0
- package/src/commands/eval.js +507 -0
- package/src/commands/github.js +169 -0
- package/src/commands/guard.js +101 -0
- package/src/commands/init.js +76 -0
- package/src/commands/kickoff.js +275 -0
- package/src/commands/labels.js +28 -0
- package/src/commands/metrics.js +48 -0
- package/src/commands/pr.js +211 -0
- package/src/commands/shots.js +51 -0
- package/src/commands/status.js +48 -0
- package/src/commands/task.js +569 -0
- package/src/commands/up.js +355 -0
- package/src/commands/worker.js +322 -0
- package/src/core/config.js +77 -0
- package/src/core/exec.js +50 -0
- package/src/core/guard.js +248 -0
- package/src/core/issues.js +154 -0
- package/src/core/lease.js +48 -0
- package/src/core/metrics.js +232 -0
- package/src/core/output.js +56 -0
- package/src/core/prd.js +593 -0
- package/src/core/proc.js +52 -0
- package/src/core/queue.js +92 -0
- package/src/core/registry.js +138 -0
- package/src/core/shots.js +68 -0
- package/src/core/supervisor.js +56 -0
- package/src/core/version.js +44 -0
- package/src/mcp/server.js +71 -0
- package/templates/labels.json +18 -0
- package/templates/mcp.optional.json +23 -0
- package/templates/product/.github/ISSUE_TEMPLATE/epic.yml +22 -0
- package/templates/product/.github/ISSUE_TEMPLATE/feedback.yml +36 -0
- package/templates/product/.github/ISSUE_TEMPLATE/task.yml +62 -0
- package/templates/product/.github/pull_request_template.md +16 -0
- package/templates/product/.github/workflows/crew-guard.yml +45 -0
- package/templates/product/.github/workflows/crew.yml +108 -0
- package/templates/product/AGENTS.md +109 -0
- package/templates/product/contracts/README.md +8 -0
- package/templates/product/docs/architecture.md +41 -0
- package/templates/product/docs/brief-analysis.md +62 -0
- package/templates/product/docs/brief.md +25 -0
- package/templates/product/docs/playbooks/issues.md +91 -0
- package/templates/product/docs/playbooks/preflight.md +28 -0
- package/templates/product/docs/playbooks/timeline.md +52 -0
- package/templates/product/docs/prd.md +58 -0
- package/templates/product/events/event.example.json +10 -0
- package/templates/product/tests/acceptance/README.md +9 -0
- package/templates/stacks/web-fastapi.json +74 -0
- package/templates/stacks/web-node.json +70 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// crew up — set everything up with one command (run by an agent following docs/AGENT-SETUP.md).
|
|
2
|
+
// Captain: crew up <dir> --repo owner/name [--stack web-node] [--public] [--invite a,b] [--start ISO --end ISO]
|
|
3
|
+
// project → event.json → first commit → GitHub repo → invitations → main protection, labels, CREW_HARNESS.
|
|
4
|
+
// Member: crew up --join owner/name [<dir>]
|
|
5
|
+
// accept the invitation → clone → agent configs → check the machine.
|
|
6
|
+
// Every step first checks whether it is already done, so a re-run continues from the point of failure.
|
|
7
|
+
// What needs a human (account sign-in, CI access to the package) the command does not do — it returns it in human_steps.
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
import { ok, warn, fail } from '../core/output.js';
|
|
11
|
+
import { run } from '../core/exec.js';
|
|
12
|
+
import { loadConfig } from '../core/config.js';
|
|
13
|
+
import init from './init.js';
|
|
14
|
+
import adapters from './adapters.js';
|
|
15
|
+
import github, { DEFAULT_HARNESS, REPO_RE } from './github.js';
|
|
16
|
+
import { mainProtected } from './pr.js';
|
|
17
|
+
|
|
18
|
+
const LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
19
|
+
export const EVENT_PATH = 'events/event.json';
|
|
20
|
+
|
|
21
|
+
/** «a, b,@c» → { logins: ['a','b','c'], bad: [...] }. */
|
|
22
|
+
export function parseInvite(s = '') {
|
|
23
|
+
const all = String(s ?? '').split(/[,\s]+/).map((x) => x.replace(/^@/, '').trim()).filter(Boolean);
|
|
24
|
+
return { logins: [...new Set(all.filter((x) => LOGIN_RE.test(x)))], bad: all.filter((x) => !LOGIN_RE.test(x)) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Contents of events/event.json (pure function). */
|
|
28
|
+
export function eventFile({ name, start = '', end = '', team = [] }) {
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
rules_url: '',
|
|
32
|
+
start: start ?? '',
|
|
33
|
+
end: end ?? '',
|
|
34
|
+
ai_allowed: true,
|
|
35
|
+
ai_disclosure_required: true,
|
|
36
|
+
prebuilt_code: 'tooling_only',
|
|
37
|
+
team_members: [...new Set(team)],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** How to start the agent (Codex does not take CREW_AGENT from the MCP config into its shell — without it CLI calls would be human@…). */
|
|
42
|
+
export function launchHints(hosts = [], root = '.', platform = process.platform) {
|
|
43
|
+
const out = [];
|
|
44
|
+
if (hosts.includes('claude')) out.push(`Claude Code: open the folder ${root} in the Claude app (Code tab) or cd ${root} && claude — CLAUDE.md and the crew MCP are picked up automatically`);
|
|
45
|
+
if (hosts.includes('codex')) {
|
|
46
|
+
out.push(platform === 'win32'
|
|
47
|
+
? `Codex: cd ${root}; $env:CREW_AGENT='codex'; codex — and confirm trust for the project`
|
|
48
|
+
: `Codex: cd ${root} && CREW_AGENT=codex codex — and confirm trust for the project`);
|
|
49
|
+
}
|
|
50
|
+
out.push('first message to the agent: "Work through the crew-workflow cycle: take tasks from the queue and do them until they run out."');
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Step log: done — done now, skipped — already existed, manual — a human is needed, failed — error. */
|
|
55
|
+
function journal() {
|
|
56
|
+
const steps = [];
|
|
57
|
+
const human = [];
|
|
58
|
+
return {
|
|
59
|
+
steps,
|
|
60
|
+
human,
|
|
61
|
+
add: (id, state, note = '') => steps.push({ id, state, note }),
|
|
62
|
+
manual: (id, note) => { steps.push({ id, state: 'manual', note }); human.push(note); },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** CLI tool; on Windows the npm .cmd wrappers (claude, codex, crew) go through cmd /c. */
|
|
67
|
+
function tool(sh, name, args, opts = {}) {
|
|
68
|
+
const r = sh(name, args, { ...opts, allowFail: true });
|
|
69
|
+
if (r.code === 0 || process.platform !== 'win32') return r;
|
|
70
|
+
return sh('cmd', ['/c', name, ...args], { ...opts, allowFail: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Does the remote URL (https or ssh) point exactly at owner/name? */
|
|
74
|
+
export function sameRepo(url = '', repo = '') {
|
|
75
|
+
const m = /github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?\/?$/i.exec(String(url).trim());
|
|
76
|
+
return !!m && m[1].toLowerCase() === String(repo).toLowerCase();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const firstLine = (r) => (r.stderr || r.stdout || '').split('\n')[0];
|
|
80
|
+
|
|
81
|
+
function stop(j, code, summary, recovery = {}) {
|
|
82
|
+
return fail(code, summary, { ...recovery, artifacts: { steps: j.steps, human_steps: j.human } });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Step 1 of both modes: node, git, gh and the gh login. → { login, id } or a fail result. */
|
|
86
|
+
function checkMachine(j, sh) {
|
|
87
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
88
|
+
if (major < 22) return stop(j, 'env_missing', `Node.js 22+ is required, found ${process.version}`, { retry: 'install Node.js 22 LTS and re-run crew up', stop_when: 'no permission to install — call a human' });
|
|
89
|
+
for (const t of ['git', 'gh']) {
|
|
90
|
+
if (tool(sh, t, ['--version']).code !== 0) {
|
|
91
|
+
return stop(j, 'env_missing', `${t} is not installed`, { retry: `install ${t} and re-run the same crew up command`, stop_when: 'no permission to install — call a human' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (sh('gh', ['auth', 'status'], { allowFail: true }).code !== 0) {
|
|
95
|
+
j.manual('gh-login', 'sign in to GitHub: gh auth login (via the browser; a human signs in)');
|
|
96
|
+
return stop(j, 'gh_auth', 'gh is not authenticated', {
|
|
97
|
+
cause_hint: 'the agent does not enter passwords or tokens — a human signs in',
|
|
98
|
+
retry: 'a human runs gh auth login, then the same crew up command',
|
|
99
|
+
stop_when: 'immediately: ask the human to sign in',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const u = sh('gh', ['api', 'user', '--jq', '.login + " " + (.id|tostring)'], { allowFail: true });
|
|
103
|
+
const [login, id] = u.stdout.trim().split(/\s+/);
|
|
104
|
+
if (u.code !== 0 || !login) return stop(j, 'gh_auth', 'Could not determine your GitHub login', { cause_hint: firstLine(u), retry: 'gh auth refresh, then the same crew up command' });
|
|
105
|
+
j.add('machine', 'done', `node ${process.version}, git, gh (${login})`);
|
|
106
|
+
return { login, id };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** git user.name/email: if not set — locally in the repo, from the GitHub account (noreply email). */
|
|
110
|
+
function ensureIdentity(j, sh, root, me) {
|
|
111
|
+
const name = sh('git', ['config', 'user.name'], { cwd: root, allowFail: true }).stdout;
|
|
112
|
+
const email = sh('git', ['config', 'user.email'], { cwd: root, allowFail: true }).stdout;
|
|
113
|
+
if (name && email) return j.add('git-identity', 'skipped', `${name} <${email}>`);
|
|
114
|
+
if (!name) sh('git', ['config', 'user.name', me.login], { cwd: root });
|
|
115
|
+
if (!email) sh('git', ['config', 'user.email', `${me.id}+${me.login}@users.noreply.github.com`], { cwd: root });
|
|
116
|
+
j.add('git-identity', 'done', `local to the repo: ${me.login}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Windows: long paths (task worktrees + node_modules), in this repo only. */
|
|
120
|
+
function ensureLongPaths(j, sh, root) {
|
|
121
|
+
if (process.platform !== 'win32') return;
|
|
122
|
+
sh('git', ['config', 'core.longpaths', 'true'], { cwd: root, allowFail: true });
|
|
123
|
+
j.add('longpaths', 'done', 'git core.longpaths=true (local)');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** crew must be on PATH for the agent MCP configs (command: crew). */
|
|
127
|
+
function checkHkOnPath(j, sh) {
|
|
128
|
+
if (tool(sh, 'crew', ['--version']).code === 0) return j.add('crew-on-path', 'skipped', 'crew is available on PATH');
|
|
129
|
+
j.manual('crew-on-path', 'crew is not on PATH — the agent MCP will not start: run npm link in the harness folder');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function checkHosts(j, sh, hosts) {
|
|
133
|
+
const missing = hosts.filter((h) => tool(sh, h, ['--version']).code !== 0);
|
|
134
|
+
if (missing.length === hosts.length) j.manual('agents', `no agent found (${hosts.join(', ')}) — install at least one`);
|
|
135
|
+
else if (missing.length) j.add('agents', 'skipped', `found: ${hosts.filter((h) => !missing.includes(h)).join(', ')}; missing: ${missing.join(', ')}`);
|
|
136
|
+
else j.add('agents', 'skipped', `found: ${hosts.join(', ')}`);
|
|
137
|
+
return hosts.filter((h) => !missing.includes(h));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function lead(o, sh) {
|
|
141
|
+
const j = journal();
|
|
142
|
+
const { repo, stack = 'web-fastapi', harness = DEFAULT_HARNESS } = o;
|
|
143
|
+
if (!REPO_RE.test(String(repo))) return fail('bad_input', '--repo: expected owner/name', { retry: 'crew up <dir> --repo <owner>/<name>' });
|
|
144
|
+
const { logins: invite, bad } = parseInvite(o.invite);
|
|
145
|
+
if (bad.length) return fail('bad_input', `These do not look like GitHub logins: ${bad.join(', ')}`, { retry: 'crew up ... --invite login1,login2' });
|
|
146
|
+
for (const k of ['start', 'end']) {
|
|
147
|
+
if (o[k] && !Number.isFinite(Date.parse(o[k]))) return fail('bad_input', `--${k}: an ISO date is required, e.g. 2026-11-10T10:00:00+03:00`, { retry: 'crew up ... --start <ISO> --end <ISO>' });
|
|
148
|
+
}
|
|
149
|
+
if (!!o.start !== !!o.end) return fail('bad_input', '--start and --end must be given together', { retry: 'crew up ... --start <ISO> --end <ISO>' });
|
|
150
|
+
const root = resolve(o.dir ?? repo.split('/')[1]);
|
|
151
|
+
|
|
152
|
+
const me = checkMachine(j, sh);
|
|
153
|
+
if (me.status === 'error') return me;
|
|
154
|
+
|
|
155
|
+
// 2. Project
|
|
156
|
+
if (existsSync(join(root, 'crew.config.json'))) j.add('init', 'skipped', `${root} already exists`);
|
|
157
|
+
else {
|
|
158
|
+
const r = await init.run({ dir: root, stack, hosts: o.hosts });
|
|
159
|
+
if (r.status === 'error') return stop(j, r.error.code, `crew init: ${r.summary}`, r.error);
|
|
160
|
+
j.add('init', 'done', r.summary);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 3. Event rules — only before the first commit: afterwards main is protected and changes go through PRs.
|
|
164
|
+
// Before the commit the file is rewritten, so a re-run with corrected dates/members applies them.
|
|
165
|
+
const eventAbs = join(root, EVENT_PATH);
|
|
166
|
+
const committed = sh('git', ['rev-parse', '--verify', '-q', 'HEAD'], { cwd: root, allowFail: true }).code === 0;
|
|
167
|
+
if (committed) {
|
|
168
|
+
j.add('event', 'skipped', existsSync(eventAbs) ? `${EVENT_PATH} is already in the repo` : `commits already exist — not creating ${EVENT_PATH}`);
|
|
169
|
+
if (o.start || !existsSync(eventAbs)) j.human.push(`event rules are changed through a PR: ${EVENT_PATH} (start/end, team_members) and "event": "${EVENT_PATH}" in crew.config.json`);
|
|
170
|
+
} else {
|
|
171
|
+
const team = [me.login, ...invite];
|
|
172
|
+
writeFileSync(eventAbs, JSON.stringify(eventFile({ name: o.event_name ?? repo.split('/')[1], start: o.start, end: o.end, team }), null, 2) + '\n');
|
|
173
|
+
// The captain is whoever sets the project up: only they run kickoff (protection against a second plan).
|
|
174
|
+
const cfgPath = join(root, 'crew.config.json');
|
|
175
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
|
|
176
|
+
writeFileSync(cfgPath, JSON.stringify({ ...cfg, captain: me.login, ...(o.start ? { event: EVENT_PATH } : {}) }, null, 2) + '\n');
|
|
177
|
+
if (o.start) {
|
|
178
|
+
j.add('event', 'done', `${EVENT_PATH}: ${o.start} — ${o.end}, team ${team.join(', ')}; compliance check enabled`);
|
|
179
|
+
} else {
|
|
180
|
+
j.add('event', 'done', `${EVENT_PATH} without times — compliance check disabled`);
|
|
181
|
+
j.human.push(`once the event time is known: a PR with start/end in ${EVENT_PATH} and "event": "${EVENT_PATH}" in crew.config.json`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 4. First commit
|
|
186
|
+
ensureIdentity(j, sh, root, me);
|
|
187
|
+
ensureLongPaths(j, sh, root);
|
|
188
|
+
if (committed) j.add('commit', 'skipped', 'commits already exist');
|
|
189
|
+
else {
|
|
190
|
+
sh('git', ['add', '-A'], { cwd: root });
|
|
191
|
+
sh('git', ['commit', '-q', '-m', `chore: crew init (stack ${stack})`], { cwd: root });
|
|
192
|
+
j.add('commit', 'done', `chore: crew init (stack ${stack})`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 5. GitHub repo + push
|
|
196
|
+
const exists = sh('gh', ['repo', 'view', repo, '--json', 'nameWithOwner'], { allowFail: true }).code === 0;
|
|
197
|
+
if (!exists) {
|
|
198
|
+
const r = sh('gh', ['repo', 'create', repo, o.public ? '--public' : '--private', '--source', root, '--remote', 'origin', '--push'], { cwd: root, allowFail: true });
|
|
199
|
+
if (r.code !== 0) {
|
|
200
|
+
return stop(j, 'repo_create_failed', `Could not create ${repo}`, {
|
|
201
|
+
cause_hint: firstLine(r),
|
|
202
|
+
retry: 'the same crew up command (completed steps are skipped)',
|
|
203
|
+
stop_when: 'no permission to create repos for this owner — call a human',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
j.add('repo', 'done', `created ${repo} (${o.public ? 'public' : 'private'}), main pushed`);
|
|
207
|
+
} else {
|
|
208
|
+
const origin = sh('git', ['remote', 'get-url', 'origin'], { cwd: root, allowFail: true });
|
|
209
|
+
if (origin.code !== 0) sh('git', ['remote', 'add', 'origin', `https://github.com/${repo}.git`], { cwd: root });
|
|
210
|
+
else if (!sameRepo(origin.stdout, repo)) {
|
|
211
|
+
return stop(j, 'remote_mismatch', `origin points at ${origin.stdout}, not ${repo}`, { retry: 'check --repo and the folder', stop_when: 'immediately: we do not overwrite a foreign remote' });
|
|
212
|
+
}
|
|
213
|
+
const heads = sh('git', ['ls-remote', '--heads', 'origin', 'main'], { cwd: root, allowFail: true });
|
|
214
|
+
if (heads.code === 0 && heads.stdout) j.add('repo', 'skipped', `${repo} already exists, main pushed`);
|
|
215
|
+
else {
|
|
216
|
+
const p = sh('git', ['push', '-u', 'origin', 'main'], { cwd: root, allowFail: true });
|
|
217
|
+
if (p.code !== 0) return stop(j, 'push_failed', `Could not push main to ${repo}`, { cause_hint: firstLine(p), retry: 'the same crew up command', stop_when: 'the repo belongs to someone else or already has a different history — call a human' });
|
|
218
|
+
j.add('repo', 'done', `${repo} already existed, main pushed`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 6. Invitations (PUT is idempotent: 201 — invited, 204 — already a member)
|
|
223
|
+
const others = invite.filter((l) => l.toLowerCase() !== me.login.toLowerCase());
|
|
224
|
+
for (const login of others) {
|
|
225
|
+
const r = sh('gh', ['api', '-X', 'PUT', `repos/${repo}/collaborators/${login}`, '-f', 'permission=push'], { allowFail: true });
|
|
226
|
+
if (r.code === 0) j.add(`invite:${login}`, 'done', 'invited with write access (or already a member)');
|
|
227
|
+
else j.add(`invite:${login}`, 'failed', firstLine(r));
|
|
228
|
+
}
|
|
229
|
+
if (others.length) j.human.push(`for the members (${others.join(', ')}): send their agent the member message from docs/AGENT-SETUP.md — crew up --join ${repo} accepts the invitation by itself`);
|
|
230
|
+
|
|
231
|
+
// 7. Main protection, labels, CREW_HARNESS
|
|
232
|
+
const g = await github.run({ repo, harness, apply: true, cwd: root }, { run: sh });
|
|
233
|
+
if (g.status === 'error') return stop(j, g.error.code, `crew github setup: ${g.summary}`, g.error);
|
|
234
|
+
j.add('github-setup', 'done', g.summary);
|
|
235
|
+
// A private repo on the free plan: there is no main protection, so the checks affect nothing.
|
|
236
|
+
if (!mainProtected(root, (args) => sh('gh', args, { cwd: root, allowFail: true }))) {
|
|
237
|
+
j.manual('main-protection', `main protection is unavailable (private repo on the free plan): PRs can merge without review and checks. Make the repo public (Settings → General → Change visibility) or get GitHub Pro, then run crew github setup --repo ${repo} --apply`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
checkHkOnPath(j, sh);
|
|
241
|
+
const hosts = loadConfig(root).config.hosts ?? [];
|
|
242
|
+
checkHosts(j, sh, hosts);
|
|
243
|
+
|
|
244
|
+
const failed = j.steps.filter((s) => s.state === 'failed');
|
|
245
|
+
return (failed.length ? warn : ok)(`Project ${repo} set up in ${root}${failed.length ? `, but with errors: ${failed.map((s) => s.id).join(', ')}` : ''}`, {
|
|
246
|
+
artifacts: { root, repo, steps: j.steps, human_steps: j.human },
|
|
247
|
+
next_actions: [
|
|
248
|
+
...j.human.map((h) => `for the human: ${h}`),
|
|
249
|
+
`after access is granted, check CI: gh run list -R ${repo} -L 3 (a run that failed before access — gh run rerun <id> -R ${repo})`,
|
|
250
|
+
...launchHints(hosts, root),
|
|
251
|
+
'paste the organizers brief into docs/brief.md as is → tell the agent: "analyze the brief with the crew-brief-analysis skill", then crew-kickoff',
|
|
252
|
+
],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function member(o, sh) {
|
|
257
|
+
const j = journal();
|
|
258
|
+
const repo = o.join;
|
|
259
|
+
if (!REPO_RE.test(String(repo))) return fail('bad_input', '--join: expected owner/name', { retry: 'crew up --join <owner>/<name>' });
|
|
260
|
+
const root = resolve(o.dir ?? repo.split('/')[1]);
|
|
261
|
+
|
|
262
|
+
const me = checkMachine(j, sh);
|
|
263
|
+
if (me.status === 'error') return me;
|
|
264
|
+
|
|
265
|
+
// 2. Invitation — to this repo only
|
|
266
|
+
const inv = sh('gh', ['api', 'user/repository_invitations'], { allowFail: true });
|
|
267
|
+
let list = [];
|
|
268
|
+
try { list = inv.code === 0 ? JSON.parse(inv.stdout || '[]') ?? [] : []; } catch { list = []; }
|
|
269
|
+
const pending = list.find((i) => i.repository?.full_name?.toLowerCase() === repo.toLowerCase());
|
|
270
|
+
if (pending) {
|
|
271
|
+
const a = sh('gh', ['api', '-X', 'PATCH', `user/repository_invitations/${pending.id}`], { allowFail: true });
|
|
272
|
+
if (a.code !== 0) return stop(j, 'invite_failed', `Could not accept the invitation to ${repo}`, { cause_hint: firstLine(a), retry: `a human accepts it at https://github.com/${repo}/invitations, then crew up --join ${repo}` });
|
|
273
|
+
j.add('invite', 'done', `invitation to ${repo} accepted`);
|
|
274
|
+
}
|
|
275
|
+
const perm = sh('gh', ['repo', 'view', repo, '--json', 'viewerPermission', '--jq', '.viewerPermission'], { allowFail: true });
|
|
276
|
+
if (perm.code !== 0) {
|
|
277
|
+
return stop(j, 'no_access', `No access to ${repo}`, {
|
|
278
|
+
cause_hint: 'there is no invitation: the captain has not invited you yet, or the repo name has a typo',
|
|
279
|
+
retry: `captain: crew up ... --invite ${me.login}; then run crew up --join ${repo} again`,
|
|
280
|
+
stop_when: 'immediately: ask the captain to invite you',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (!['ADMIN', 'MAINTAIN', 'WRITE'].includes(perm.stdout.trim())) {
|
|
284
|
+
return stop(j, 'no_write', `Permission on ${repo} is ${perm.stdout.trim() || 'READ'} — the agent will not be able to push`, { retry: `captain: crew up ... --invite ${me.login}`, stop_when: 'immediately: ask the captain to grant write access' });
|
|
285
|
+
}
|
|
286
|
+
if (!pending) j.add('invite', 'skipped', `access already granted (${perm.stdout.trim()})`);
|
|
287
|
+
|
|
288
|
+
// 3. Clone
|
|
289
|
+
if (existsSync(join(root, '.git'))) {
|
|
290
|
+
const origin = sh('git', ['remote', 'get-url', 'origin'], { cwd: root, allowFail: true });
|
|
291
|
+
if (!sameRepo(origin.stdout, repo)) {
|
|
292
|
+
return stop(j, 'remote_mismatch', `${root} holds a different repo (origin: ${origin.stdout || 'none'}), not ${repo}`, { retry: `crew up --join ${repo} <other dir>`, stop_when: 'immediately: we do not touch a foreign folder' });
|
|
293
|
+
}
|
|
294
|
+
const pull = sh('git', ['pull', '--ff-only', '-q'], { cwd: root, allowFail: true });
|
|
295
|
+
if (pull.code === 0) j.add('clone', 'skipped', `${root} already exists, updated (git pull --ff-only)`);
|
|
296
|
+
else j.add('clone', 'failed', `${root} already exists, but git pull --ff-only failed: ${firstLine(pull)} — local changes or divergence from origin`);
|
|
297
|
+
} else if (existsSync(root) && readdirSync(root).length) {
|
|
298
|
+
return stop(j, 'dir_not_empty', `${root} is not empty and not a git repo`, { retry: `crew up --join ${repo} <other dir>` });
|
|
299
|
+
} else {
|
|
300
|
+
const c = sh('gh', ['repo', 'clone', repo, root], { allowFail: true });
|
|
301
|
+
if (c.code !== 0) return stop(j, 'clone_failed', `Could not clone ${repo}`, { cause_hint: firstLine(c), retry: `crew up --join ${repo}` });
|
|
302
|
+
j.add('clone', 'done', `${repo} → ${root}`);
|
|
303
|
+
}
|
|
304
|
+
if (!existsSync(join(root, 'crew.config.json'))) {
|
|
305
|
+
return stop(j, 'no_config', `${repo} has no crew.config.json — the captain has not set the project up yet`, { retry: `wait for the captain and re-run crew up --join ${repo}` });
|
|
306
|
+
}
|
|
307
|
+
ensureIdentity(j, sh, root, me);
|
|
308
|
+
ensureLongPaths(j, sh, root);
|
|
309
|
+
|
|
310
|
+
// 4. Local agent configs (machine-dependent, not committed)
|
|
311
|
+
const a = await adapters.run({ cwd: root });
|
|
312
|
+
if (a.status === 'error') return stop(j, a.error.code, `crew adapters build: ${a.summary}`, a.error);
|
|
313
|
+
j.add('adapters', 'done', a.summary);
|
|
314
|
+
|
|
315
|
+
checkHkOnPath(j, sh);
|
|
316
|
+
const hosts = loadConfig(root).config.hosts ?? [];
|
|
317
|
+
checkHosts(j, sh, hosts);
|
|
318
|
+
if (hosts.includes('codex')) j.human.push('Codex: on the first run in the project folder confirm trust (otherwise it will not read .codex/config.toml)');
|
|
319
|
+
|
|
320
|
+
const failed = j.steps.filter((s) => s.state === 'failed');
|
|
321
|
+
return (failed.length ? warn : ok)(`Done: ${repo} in ${root}, you are ${me.login}${failed.length ? ` (with errors: ${failed.map((s) => s.id).join(', ')})` : ''}`, {
|
|
322
|
+
artifacts: { root, repo, steps: j.steps, human_steps: j.human },
|
|
323
|
+
next_actions: [...j.human.map((h) => `for the human: ${h}`), ...launchHints(hosts, root)],
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export default {
|
|
328
|
+
name: 'up',
|
|
329
|
+
summary: 'Set everything up with one command: captain — project and GitHub repo (--repo), member — join (--join)',
|
|
330
|
+
agent: false,
|
|
331
|
+
args: [
|
|
332
|
+
{ name: 'dir', type: 'string', positional: true, description: 'project folder (default — the repo name)' },
|
|
333
|
+
{ name: 'repo', type: 'string', description: 'captain: owner/name of the new product repo' },
|
|
334
|
+
{ name: 'join', type: 'string', description: 'member: owner/name of the existing repo' },
|
|
335
|
+
{ name: 'stack', type: 'string', description: 'captain: stack preset (web-fastapi, web-node)' },
|
|
336
|
+
{ name: 'public', type: 'boolean', description: 'captain: public repo (private by default)' },
|
|
337
|
+
{ name: 'invite', type: 'string', description: 'captain: GitHub logins, comma-separated — invite with write access' },
|
|
338
|
+
{ name: 'start', type: 'string', description: 'captain: event start, ISO (together with --end enables the compliance check)' },
|
|
339
|
+
{ name: 'end', type: 'string', description: 'captain: event end, ISO' },
|
|
340
|
+
{ name: 'event_name', type: 'string', description: 'captain: event name for events/event.json' },
|
|
341
|
+
{ name: 'hosts', type: 'string', description: 'captain: agent hosts, comma-separated (default claude,codex)' },
|
|
342
|
+
{ name: 'harness', type: 'string', description: `captain: CREW_HARNESS value (default ${DEFAULT_HARNESS})` },
|
|
343
|
+
],
|
|
344
|
+
// deps.run — stub for running external commands (tests without a network).
|
|
345
|
+
async run(o, deps = {}) {
|
|
346
|
+
const sh = deps.run ?? run;
|
|
347
|
+
if (o.repo && o.join) return fail('bad_input', '--repo (captain) and --join (member) cannot be used together', { retry: 'crew up <dir> --repo <owner>/<name> or crew up --join <owner>/<name>' });
|
|
348
|
+
if (o.join) return member(o, sh);
|
|
349
|
+
if (o.repo) return lead(o, sh);
|
|
350
|
+
return fail('bad_input', 'Pass --repo (create a project) or --join (connect)', {
|
|
351
|
+
retry: 'crew up <dir> --repo <owner>/<name> --invite a,b or crew up --join <owner>/<name>',
|
|
352
|
+
stop_when: 'unclear whether you are the captain or a member — ask the human',
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
};
|