@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.
Files changed (101) hide show
  1. package/AGENTS.md +25 -0
  2. package/LICENSE +201 -0
  3. package/README.md +418 -2
  4. package/README.ru.md +421 -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 +46 -0
  39. package/src/commands/adapters.js +85 -0
  40. package/src/commands/ask.js +123 -0
  41. package/src/commands/board.js +88 -0
  42. package/src/commands/check.js +148 -0
  43. package/src/commands/commit.js +105 -0
  44. package/src/commands/compliance.js +109 -0
  45. package/src/commands/digest.js +64 -0
  46. package/src/commands/doctor.js +48 -0
  47. package/src/commands/eval.js +507 -0
  48. package/src/commands/github.js +169 -0
  49. package/src/commands/guard.js +101 -0
  50. package/src/commands/init.js +76 -0
  51. package/src/commands/kickoff.js +275 -0
  52. package/src/commands/labels.js +28 -0
  53. package/src/commands/merge-ready.js +33 -0
  54. package/src/commands/metrics.js +48 -0
  55. package/src/commands/pr.js +273 -0
  56. package/src/commands/shots.js +51 -0
  57. package/src/commands/status.js +48 -0
  58. package/src/commands/task.js +590 -0
  59. package/src/commands/up.js +355 -0
  60. package/src/commands/upgrade.js +178 -0
  61. package/src/commands/worker.js +322 -0
  62. package/src/core/board.js +127 -0
  63. package/src/core/config.js +83 -0
  64. package/src/core/digest.js +67 -0
  65. package/src/core/exec.js +50 -0
  66. package/src/core/guard.js +273 -0
  67. package/src/core/issues.js +154 -0
  68. package/src/core/lease.js +48 -0
  69. package/src/core/metrics.js +232 -0
  70. package/src/core/output.js +56 -0
  71. package/src/core/prd.js +612 -0
  72. package/src/core/proc.js +52 -0
  73. package/src/core/questions.js +51 -0
  74. package/src/core/queue.js +94 -0
  75. package/src/core/registry.js +143 -0
  76. package/src/core/session.js +61 -0
  77. package/src/core/shots.js +68 -0
  78. package/src/core/supervisor.js +56 -0
  79. package/src/core/version.js +44 -0
  80. package/src/mcp/server.js +81 -0
  81. package/templates/labels.json +23 -0
  82. package/templates/mcp.optional.json +23 -0
  83. package/templates/product/.github/ISSUE_TEMPLATE/epic.yml +22 -0
  84. package/templates/product/.github/ISSUE_TEMPLATE/feedback.yml +36 -0
  85. package/templates/product/.github/ISSUE_TEMPLATE/task.yml +62 -0
  86. package/templates/product/.github/pull_request_template.md +16 -0
  87. package/templates/product/.github/workflows/crew-guard.yml +45 -0
  88. package/templates/product/.github/workflows/crew.yml +118 -0
  89. package/templates/product/AGENTS.md +109 -0
  90. package/templates/product/contracts/README.md +8 -0
  91. package/templates/product/docs/architecture.md +41 -0
  92. package/templates/product/docs/brief-analysis.md +62 -0
  93. package/templates/product/docs/brief.md +25 -0
  94. package/templates/product/docs/playbooks/issues.md +91 -0
  95. package/templates/product/docs/playbooks/preflight.md +28 -0
  96. package/templates/product/docs/playbooks/timeline.md +52 -0
  97. package/templates/product/docs/prd.md +58 -0
  98. package/templates/product/events/event.example.json +10 -0
  99. package/templates/product/tests/acceptance/README.md +9 -0
  100. package/templates/stacks/web-fastapi.json +74 -0
  101. package/templates/stacks/web-node.json +70 -0
@@ -0,0 +1,273 @@
1
+ // crew pr open — hand in the work. Proceeds only with a green crew check on the current commit.
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { ok, fail } from '../core/output.js';
5
+ import { git, gh, ghJson, currentBranch } from '../core/exec.js';
6
+ import { loadConfig, agentId } from '../core/config.js';
7
+ import { viewIssue, setStatus, labelValue } from '../core/issues.js';
8
+ import { WAIVABLE, waiveMarker, authorAgent, reviewMarker, reviewPolicy, vendorOf } from '../core/guard.js';
9
+ import { checkLease, leaseLostError } from '../core/lease.js';
10
+ import { isFullRun } from './check.js';
11
+ import { safeText } from '../core/issues.js';
12
+ import { REQUIRED_CHECKS } from './github.js';
13
+ import { uiFiles, listShots, shotsSection } from '../core/shots.js';
14
+ import { LOCAL_VERSION, compareVersions } from '../core/version.js';
15
+
16
+ /**
17
+ * Whether main has protection that waits for BOTH check AND crew-guard (without them GitHub auto-merge merges the PR immediately, skipping review).
18
+ * Names come from REQUIRED_CHECKS. Any API error, a foreign set of checks or a malformed structure — no.
19
+ */
20
+ export function mainProtected(root, sh = (args) => gh(args, { cwd: root, allowFail: true })) {
21
+ const r = sh(['api', 'repos/{owner}/{repo}/branches/main/protection']);
22
+ if (r.code !== 0) return false;
23
+ try {
24
+ const rsc = JSON.parse(r.stdout || '{}').required_status_checks;
25
+ if (!rsc || typeof rsc !== 'object') return false;
26
+ const names = new Set();
27
+ if (Array.isArray(rsc.contexts)) for (const c of rsc.contexts) if (typeof c === 'string') names.add(c);
28
+ if (Array.isArray(rsc.checks)) for (const c of rsc.checks) if (typeof c?.context === 'string') names.add(c.context);
29
+ return REQUIRED_CHECKS.every((n) => names.has(n));
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Files that would conflict when merging origin/main into HEAD (git merge-tree, the working tree is untouched).
37
+ * Empty when there are no conflicts or git cannot tell (old git, no origin/main): then CI and crew-guard decide.
38
+ */
39
+ export function mainConflicts(root, sh = (args) => git(args, { cwd: root, allowFail: true })) {
40
+ sh(['fetch', '--quiet', 'origin', 'main']);
41
+ const r = sh(['merge-tree', '--write-tree', '--name-only', '--no-messages', 'origin/main', 'HEAD']);
42
+ if (r.code !== 1) return [];
43
+ return r.stdout.split('\n').slice(1).map((s) => s.trim()).filter(Boolean);
44
+ }
45
+
46
+ export function issueFromBranch(branch) {
47
+ return Number(/^(?:crew|hk)\/(\d+)-/.exec(branch)?.[1]) || null;
48
+ }
49
+
50
+ /**
51
+ * Merge policy by task risk (continuous merge instead of integration windows).
52
+ * low/med → GitHub auto-merge: the PR merges itself once the required checks and review pass.
53
+ * high → no auto-merge: the integrator merges manually and FIRST (this is the foundation: auth, data, demo path).
54
+ * A regular merge (--merge), not squash: otherwise the Agent/Issue trailers are lost, and compliance depends on them.
55
+ */
56
+ export function mergePolicy(risk = 'med', { draft = false } = {}) {
57
+ const r = ['low', 'med', 'high'].includes(risk) ? risk : 'med';
58
+ if (draft) return { risk: r, auto: false, note: 'draft — auto-merge is not enabled' };
59
+ if (r === 'high') return { risk: r, auto: false, note: 'risk:high — the integrator merges manually, first' };
60
+ return { risk: r, auto: true, args: ['--auto', '--merge', '--delete-branch'], note: 'auto-merge after green checks and review' };
61
+ }
62
+
63
+ /**
64
+ * crew pr review <n> --verdict approve|changes — the reviewer agent verdict.
65
+ * Writes a PR comment with a crew:review marker for the current commit; the crew-guard workflow
66
+ * recomputes the status itself. A new push to the PR resets approvals (the new commit needs a review).
67
+ */
68
+ export const prReview = {
69
+ name: 'pr review',
70
+ summary: 'PR review verdict (approve / changes) — counted by the crew-guard check',
71
+ args: [
72
+ { name: 'pr', type: 'number', positional: true, required: true, description: 'PR number' },
73
+ { name: 'verdict', type: 'string', required: true, enum: ['approve', 'changes'], description: 'approve — OK to merge; changes — fixes needed' },
74
+ { name: 'note', type: 'string', description: 'summary of the crew-review checklist; for changes — the specific fixes (required)' },
75
+ ],
76
+ run({ pr, verdict, note }) {
77
+ const { config, root } = loadConfig();
78
+ const me = agentId();
79
+ if (verdict === 'changes' && !note) {
80
+ return fail('bad_input', 'changes requires --note with the specific fixes', { retry: `crew pr review ${pr} --verdict changes --note "1) ... 2) ..."` });
81
+ }
82
+ const view = ghJson(['pr', 'view', String(pr), '--json', 'number,body,headRefOid,state,url'], { cwd: root });
83
+ if (!view || view.state !== 'OPEN') {
84
+ return fail('pr_not_open', `PR #${pr} is not open`, { retry: 'gh pr list', stop_when: 'PR is already merged or closed' });
85
+ }
86
+ const author = authorAgent(view.body);
87
+ if (author && author === me) {
88
+ return fail('self_review', `You wrote PR #${pr} yourself (${me}) — a review of your own work does not count`, {
89
+ retry: 'crew task next or review someone else PR',
90
+ stop_when: 'there are no other PRs to review',
91
+ });
92
+ }
93
+ const policy = reviewPolicy(config);
94
+ const sameVendor = !!author && vendorOf(author) === vendorOf(me);
95
+ const sha = view.headRefOid;
96
+ const head = verdict === 'approve' ? `✅ **Review by ${me}: OK to merge**` : `❌ **Review by ${me}: changes needed**`;
97
+ const body = [reviewMarker({ verdict, agent: me, sha }), `${head} (commit ${sha.slice(0, 7)})`, '', safeText(note ?? '')].join('\n').trim() + '\n';
98
+ gh(['pr', 'comment', String(pr), '--body', body], { cwd: root });
99
+ const notCounted = policy.require === 'other-vendor' && sameVendor;
100
+ return ok(`Review of PR #${pr} recorded: ${verdict}${notCounted ? ` (same vendor as the author — counts only if no reviewer from another vendor appears within ${policy.fallback_minutes} min)` : ''}`, {
101
+ status: notCounted ? 'warning' : 'success',
102
+ artifacts: { pr, verdict, agent: me, sha, author, counted_now: !notCounted },
103
+ next_actions: verdict === 'approve' ? [`crew guard --pr ${pr} (status)`, 'crew task next'] : ['the author applies the fixes and pushes; then a new review is needed', 'crew task next'],
104
+ });
105
+ },
106
+ };
107
+
108
+ export const prOpen = {
109
+ name: 'pr open',
110
+ summary: 'Open a PR for the current task (only after a green crew check)',
111
+ usesCwd: true,
112
+ args: [{ name: 'draft', type: 'boolean', description: 'draft PR' }],
113
+ run({ draft, cwd }) {
114
+ const { root } = loadConfig(cwd);
115
+ const branch = currentBranch(root);
116
+ const id = issueFromBranch(branch);
117
+ if (!id) {
118
+ return fail('not_task_branch', `Branch ${branch} does not look like crew/<number>-...`, {
119
+ cause_hint: 'work must start from a claimed task',
120
+ retry: 'crew task next → crew task claim <id>',
121
+ });
122
+ }
123
+ if (git(['status', '--porcelain'], { cwd: root }).stdout) {
124
+ return fail('dirty_tree', 'There are uncommitted changes', { retry: 'crew commit -m "..." and run crew check again' });
125
+ }
126
+ const lcPath = join(root, '.crew', 'last-check.json');
127
+ const sha = git(['rev-parse', 'HEAD'], { cwd: root }).stdout;
128
+ const lc = existsSync(lcPath) ? JSON.parse(readFileSync(lcPath, 'utf8')) : null;
129
+ const { config: cfg } = loadConfig(cwd);
130
+ // Compared against the FULL list of config steps: a partial run (--only) or an old config does not count.
131
+ const full = lc && isFullRun(lc.results ?? [], cfg.check ?? []);
132
+ if (!lc || !lc.green || lc.partial || !full || lc.sha !== sha || lc.dirty) {
133
+ return fail('check_required', 'No green full crew check for the current commit', {
134
+ cause_hint: !lc ? 'crew check has not been run yet'
135
+ : lc.partial ? 'the last run was partial (--only)'
136
+ : !lc.green || !full ? 'the last check was red or did not run all crew.config.json steps'
137
+ : 'there were changes after the check',
138
+ retry: 'crew check',
139
+ });
140
+ }
141
+ const issue = viewIssue(id);
142
+ const agent = agentId();
143
+ const vendor = vendorOf(agent);
144
+ // Is the task still mine? Otherwise (the lease was taken away, someone else claimed it) — two PRs for one task.
145
+ const lease = checkLease({ id, me: agent, view: () => issue });
146
+ if (lease.state === 'lost') return leaseLostError(fail, id, lease, agent);
147
+ // An outdated local crew opened PRs that bypassed review at the hackathon: refuse when the repo pins a newer one.
148
+ const required = /@(\d+\.\d+\.\d+)\s*$|#v(\d+\.\d+\.\d+)\s*$/.exec(gh(['variable', 'get', 'CREW_HARNESS'], { cwd: root, allowFail: true }).stdout ?? '');
149
+ const requiredVersion = required?.[1] ?? required?.[2];
150
+ if (requiredVersion && compareVersions(LOCAL_VERSION, requiredVersion) === -1) {
151
+ return fail('outdated_crew', `This repo uses agent-crew ${requiredVersion}, your crew is ${LOCAL_VERSION}`, {
152
+ cause_hint: 'fixes and rules of the newer version would not apply to your PR',
153
+ retry: `tell the human: npm i -g @gredescerber/agent-crew@${requiredVersion}, restart the agent session, then crew pr open`,
154
+ stop_when: 'you cannot update — ask the human',
155
+ });
156
+ }
157
+ // A PR that conflicts with main sits silently (CI does not even start). Shared files (translations, contracts)
158
+ // conflicted all the time at the hackathon: catch it here, before pushing, with the exact files.
159
+ const conflicts = mainConflicts(root);
160
+ if (conflicts.length) {
161
+ return fail('conflicts_with_main', `The branch conflicts with main in ${conflicts.length} file(s): ${conflicts.slice(0, 5).join(', ')}`, {
162
+ cause_hint: 'someone merged changes to the same lines; shared append-only files (translations, lists) conflict most often',
163
+ retry: 'git merge origin/main → resolve the conflicts (keep both sides where they just add) → crew commit -m "merge main" → crew check → crew pr open',
164
+ stop_when: 'you cannot tell which side is right — crew ask <area of the file> --text "..."',
165
+ artifacts: { conflicts },
166
+ });
167
+ }
168
+
169
+ // A PR for this branch is already open (rework after review) — we only push and do not create a second PR.
170
+ const existing = ghJson(['pr', 'view', branch, '--json', 'url,state,number'], { cwd: root, allowFail: true });
171
+ git(['push', '-u', 'origin', branch], { cwd: root });
172
+ if (existing?.state === 'OPEN') {
173
+ setStatus(id, 'review', { from: ['claimed', 'blocked'] });
174
+ return ok(`PR updated: ${existing.url} (commit ${sha.slice(0, 7)}) — a new review of this commit is needed`, {
175
+ artifacts: { pr: existing.url, issue: id, branch, updated: true, sha },
176
+ next_actions: [`reviewer: crew pr review ${existing.number} --verdict approve|changes (old approvals do not apply to the new commit)`, 'crew task next'],
177
+ });
178
+ }
179
+ const body = [
180
+ `Closes #${id}`,
181
+ '',
182
+ `**Agent:** \`${agent}\``,
183
+ '',
184
+ '## What was done',
185
+ '_(the agent fills this in: 2–5 bullets)_',
186
+ '',
187
+ '## Checks',
188
+ `- [x] crew check green on ${sha.slice(0, 7)} (${lc.results.map((r) => r.name).join(', ')})`,
189
+ '',
190
+ '## Review checklist (filled in by a reviewer from another vendor)',
191
+ '- [ ] No behavioural regressions / demo path not broken',
192
+ '- [ ] Security assumptions (secrets, auth, input, prompt injection)',
193
+ '- [ ] Data integrity and conformance to contracts/',
194
+ '- [ ] Error handling (no swallowed exceptions, fallbacks exist)',
195
+ '- [ ] Rollout safety (rollback possible, does not break main)',
196
+ ].join('\n');
197
+ // Visual change: show the human and the reviewer what it looked like before and after (crew shots).
198
+ const changed = git(['diff', '--name-only', 'origin/main...HEAD'], { cwd: root, allowFail: true }).stdout.split('\n').filter(Boolean);
199
+ const ui = uiFiles(changed);
200
+ const shots = listShots(root, id);
201
+ const repoSlug = /github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?\/?$/i.exec(git(['remote', 'get-url', 'origin'], { cwd: root, allowFail: true }).stdout)?.[1];
202
+ const visual = repoSlug ? shotsSection({ repo: repoSlug, sha, shots }) : '';
203
+ const fullBody = visual ? `${body}\n\n${visual}` : body;
204
+ const noShots = ui.length > 0 && !Object.keys(shots).length;
205
+ const args = ['pr', 'create', '--head', branch, '--title', `${issue.title} (#${id})`, '--body', fullBody];
206
+ if (draft) args.push('--draft');
207
+ const url = gh(args, { cwd: root }).stdout.split('\n').pop();
208
+ const policy = mergePolicy(labelValue(issue, 'risk') ?? 'med', { draft });
209
+ gh(['pr', 'edit', url, '--add-label', `agent:${vendor}`, '--add-label', `risk:${policy.risk}`], { cwd: root, allowFail: true });
210
+ setStatus(id, 'review', { from: ['claimed'] });
211
+ let merge = policy.note;
212
+ let status = 'success';
213
+ // A private repo on the free plan: there is no main protection, and GitHub "auto-merge" merges the PR immediately, without review or crew-guard
214
+ //. So we do not enable auto-merge: crew task next will hand out the merge after a green crew-guard.
215
+ const guarded = policy.auto ? mainProtected(root) : true;
216
+ if (policy.auto && !guarded) {
217
+ status = 'warning';
218
+ merge = 'main is not protected (private repo on the free plan) — auto-merge not enabled; the agent that crew task next hands the merge to will merge after review';
219
+ } else if (policy.auto) {
220
+ const r = gh(['pr', 'merge', url, ...policy.args], { cwd: root, allowFail: true });
221
+ if (r.code !== 0) {
222
+ // Without main protection GitHub auto-merge is unavailable. We do NOT merge directly — review is mandatory.
223
+ status = 'warning';
224
+ merge = `auto-merge did not enable: ${(r.stderr || r.stdout).split('\n')[0]}`;
225
+ }
226
+ }
227
+ const { config } = loadConfig(cwd);
228
+ const rp = reviewPolicy(config);
229
+ const need = policy.risk === 'high' ? rp.approvals_high : rp.approvals;
230
+ const who = rp.require === 'other-vendor' ? `an agent from another vendor (not ${vendor})` : `another agent (not ${agent})`;
231
+ return ok(`PR opened: ${url} (${merge})`, {
232
+ status,
233
+ artifacts: { pr: url, issue: id, branch, risk: policy.risk, auto_merge: policy.auto && status === 'success', reviews_needed: need },
234
+ next_actions: [
235
+ `review: ${who}, approvals needed — ${need}${policy.risk === 'high' ? ' (one of them is a security review)' : ''}: skill crew-review → crew pr review <n> --verdict approve|changes`,
236
+ ...(noShots ? [`UI files changed (${ui.slice(0, 3).join(', ')}${ui.length > 3 ? ', …' : ''}) but there are no before/after screenshots: crew shots after --url <page>, crew commit, crew pr open (and next time crew shots before at the start of the task)`] : []),
237
+ ...(status === 'warning' ? ['do not merge yourself: crew task next hands out the merge (kind: merge) when review and crew-guard are green; for the human — main protection: a public repo or GitHub Pro, then crew github setup --apply'] : []),
238
+ 'crew task next',
239
+ ],
240
+ });
241
+ },
242
+ };
243
+
244
+
245
+ /**
246
+ * crew pr waive <n> --rule <r> --reason "..." — a human records why a paperwork rule does not apply to this commit
247
+ * (a PR made outside crew: cloud session, plain git). crew-guard then ignores that rule for this head commit only.
248
+ * Protected files, acceptance tests and the review can never be waived.
249
+ */
250
+ export const prWaive = {
251
+ name: 'pr waive',
252
+ summary: 'Human: waive a paperwork rule of crew-guard (closes, agent_trailer, not_crew_pr) for the current commit, with a reason',
253
+ agent: false,
254
+ args: [
255
+ { name: 'pr', type: 'number', positional: true, required: true, description: 'PR number' },
256
+ { name: 'rule', type: 'string', required: true, enum: WAIVABLE, description: 'the crew-guard rule to waive' },
257
+ { name: 'reason', type: 'string', required: true, description: 'why (kept in the PR for the record)' },
258
+ ],
259
+ run({ pr, rule, reason }) {
260
+ const { root } = loadConfig();
261
+ const view = ghJson(['pr', 'view', String(pr), '--json', 'headRefOid,state'], { cwd: root, allowFail: true });
262
+ if (!view || view.state !== 'OPEN') return fail('pr_not_open', `PR #${pr} is not open`, { retry: 'gh pr list' });
263
+ const sha = view.headRefOid.slice(0, 12);
264
+ gh(['pr', 'comment', String(pr), '--body', `${waiveMarker({ rule, sha })}
265
+ **Waived \`${rule}\`** for commit ${sha.slice(0, 7)}: ${safeText(reason)}`], { cwd: root });
266
+ return ok(`Rule ${rule} waived for PR #${pr} at ${sha.slice(0, 7)}`, {
267
+ artifacts: { pr, rule, sha },
268
+ next_actions: ['crew-guard recomputes on this comment; the PR still needs a review', `a new push needs a new waiver: crew pr waive ${pr} --rule ${rule} --reason "..."`],
269
+ });
270
+ },
271
+ };
272
+
273
+ export default [prOpen, prReview, prWaive];
@@ -0,0 +1,51 @@
1
+ // crew shots before|after --url <page> [--name main] — screenshot for a task that changes how the product looks.
2
+ // "before" at the start of the task (base code running), "after" when done. crew pr open embeds both into the PR.
3
+ import { existsSync, mkdirSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { ok, fail } from '../core/output.js';
6
+ import { runShell, currentBranch } from '../core/exec.js';
7
+ import { loadConfig } from '../core/config.js';
8
+ import { issueFromBranch } from './pr.js';
9
+ import { PHASES, NAME_RE, safeUrl, shotPath, captureCommand } from '../core/shots.js';
10
+
11
+ export default {
12
+ name: 'shots',
13
+ summary: 'Take a before/after screenshot of a page for a task that changes the UI (Playwright); crew pr open shows them in the PR',
14
+ usesCwd: true,
15
+ args: [
16
+ { name: 'phase', type: 'string', positional: true, required: true, enum: PHASES, description: 'before (at the start of the task) or after (when done)' },
17
+ { name: 'url', type: 'string', required: true, description: 'page of the running app, e.g. http://localhost:5173/items' },
18
+ { name: 'name', type: 'string', description: 'screen name: a-z, 0-9, dash (default main); one before/after pair per screen' },
19
+ { name: 'width', type: 'number', description: 'viewport width (default 1280; use 390 for a phone)' },
20
+ { name: 'height', type: 'number', description: 'viewport height (default 800)' },
21
+ ],
22
+ run({ phase, url, name = 'main', width = 1280, height = 800, cwd }, deps = {}) {
23
+ const { root } = loadConfig(cwd);
24
+ const id = issueFromBranch(currentBranch(root));
25
+ if (!id) {
26
+ return fail('not_task_branch', 'Screenshots belong to a task: run this in the task worktree (branch crew/<id>-...)', {
27
+ retry: 'crew task claim <id>, then crew shots in the worktree',
28
+ });
29
+ }
30
+ const safe = safeUrl(url);
31
+ if (!safe) return fail('bad_input', `Not a plain http(s) URL: ${url}`, { retry: 'crew shots after --url http://localhost:5173/' });
32
+ if (!NAME_RE.test(name)) return fail('bad_input', `Bad screen name "${name}": a-z, 0-9, dash`, { retry: 'crew shots after --name main --url ...' });
33
+ const rel = shotPath(id, name, phase);
34
+ const abs = join(root, rel);
35
+ mkdirSync(dirname(abs), { recursive: true });
36
+ const r = (deps.runShell ?? runShell)(captureCommand({ url: safe, file: rel, width, height }), { cwd: root, timeout_s: 180 });
37
+ if (r.code !== 0 || !(deps.exists ?? existsSync)(abs)) {
38
+ return fail('shot_failed', `Screenshot ${phase} of ${safe} failed`, {
39
+ cause_hint: r.timedOut ? 'timed out after 180 s' : (r.stderr || r.stdout || '').split('\n').slice(-3).join(' | '),
40
+ retry: 'start the app first (the page must open in a browser); first run downloads Playwright: npx playwright install chromium',
41
+ stop_when: 'the app cannot run in this environment — say so in the PR instead of screenshots',
42
+ });
43
+ }
44
+ return ok(`Screenshot saved: ${rel}`, {
45
+ artifacts: { path: rel, phase, name, url: safe, issue: id },
46
+ next_actions: phase === 'before'
47
+ ? ['make the change, then: crew shots after --url ... --name ' + name]
48
+ : ['crew commit -m "..." → crew check → crew pr open (the PR shows Before / After)'],
49
+ });
50
+ },
51
+ };
@@ -0,0 +1,48 @@
1
+ // crew status — overview for the captain: queue by status, who is doing what, what is stuck (blocked, PRs without review, PRs ready to merge).
2
+ import { ok } from '../core/output.js';
3
+ import { listIssues, labelValue } from '../core/issues.js';
4
+ import { loadConfig, agentId } from '../core/config.js';
5
+ import { ghJson } from '../core/exec.js';
6
+ import { prQueue } from '../core/queue.js';
7
+
8
+ /** PRs waiting on humans/agents: ready to merge and without review (pure function for tests). */
9
+ export function prLines(prs, { config, now = Date.now() } = {}) {
10
+ // me/login are empty: we look through the captain view — whoever reviews, the PR is waiting.
11
+ return prQueue(prs, { me: agentId(), login: null, reviewers: [], config, now }).map((x) =>
12
+ x.kind === 'fix'
13
+ ? `PR #${x.number} ${x.title} — waiting on the author: ${x.reason}${x.age > 30 ? ' ⚠' : ''}`
14
+ : x.kind === 'merge'
15
+ ? `PR #${x.number} ${x.title} — ready (${x.risk}): merge it (gh pr merge ${x.number} --merge --delete-branch)`
16
+ : `PR #${x.number} ${x.title} — waiting for review ${x.age} min (${x.have}/${x.need})${x.age > 30 ? ' ⚠' : ''}`);
17
+ }
18
+
19
+ export default {
20
+ name: 'status',
21
+ summary: 'Overview: queue by status, who is doing what, what is stuck (blocked, PRs without review, PRs ready to merge)',
22
+ args: [],
23
+ run() {
24
+ const { config, root } = loadConfig();
25
+ const issues = listIssues();
26
+ const by = {};
27
+ for (const i of issues) (by[labelValue(i, 'status') ?? 'no-status'] ??= []).push(i);
28
+ const counts = Object.fromEntries(Object.entries(by).map(([k, v]) => [k, v.length]));
29
+ const now = Date.now();
30
+ const who = (i) => i.assignees.map((a) => a.login).join(',') || '?';
31
+ const working = (by.claimed ?? []).map((i) => {
32
+ const age = Math.round((now - Date.parse(i.updatedAt)) / 60000);
33
+ return `#${i.number} ${i.title} — ${who(i)}, ${age} min ago${age > config.lease_minutes ? ' ⚠ going stale' : ''}`;
34
+ });
35
+ const blocked = [...(by.blocked ?? []), ...(by['needs-spec'] ?? [])].map((i) => `#${i.number} ${i.title} (${labelValue(i, 'status')}, area ${labelValue(i, 'area') ?? '?'}, ${who(i)})`);
36
+ const prs = ghJson(['pr', 'list', '--state', 'open', '--limit', '50', '--json', 'number,title,body,headRefOid,createdAt,isDraft,author,labels,comments,statusCheckRollup,mergeable,isCrossRepository,autoMergeRequest'], { cwd: root, allowFail: true }) ?? [];
37
+ const waiting_prs = prLines(prs, { config, now });
38
+ const next_actions = [
39
+ ...(blocked.length ? ['captain: triage the stuck ones — gh issue view <id>; answer and return to the queue: crew task release <id> --force --note "answer: ..."; or add a task to the PRD for the right area and run crew kickoff --apply'] : []),
40
+ ...(waiting_prs.length ? ['PRs waiting: reviewer — crew task next (hands out review/merge first)'] : []),
41
+ 'crew task next',
42
+ ];
43
+ return ok(`Open tasks: ${issues.length} · ${Object.entries(counts).map(([k, v]) => `${k}: ${v}`).join(' · ')}${waiting_prs.length ? ` · PRs waiting: ${waiting_prs.length}` : ''}`, {
44
+ artifacts: { counts, working, blocked, waiting_prs },
45
+ next_actions,
46
+ });
47
+ },
48
+ };