agy-crew 0.0.0-stage → 0.9.2
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/LICENSE +22 -0
- package/README.md +315 -2
- package/README.zh-CN.md +314 -0
- package/companion/accounts.mjs +300 -0
- package/companion/agy-companion.mjs +2136 -0
- package/companion/identity.mjs +25 -0
- package/companion/install.mjs +443 -0
- package/companion/observation.mjs +147 -0
- package/companion/pool/env.mjs +14 -0
- package/companion/pool/lease.mjs +89 -0
- package/companion/pool/loans.mjs +46 -0
- package/companion/pool/quota.mjs +142 -0
- package/companion/pool/results.mjs +41 -0
- package/companion/pool/select.mjs +115 -0
- package/companion/pool/state.mjs +104 -0
- package/companion/pool/token.mjs +25 -0
- package/companion/state-lock.mjs +103 -0
- package/companion/stream-worker.mjs +361 -0
- package/docs/INSTALL_FOR_AGENTS.md +213 -0
- package/docs/REFERENCE.md +509 -0
- package/docs/REFERENCE.zh-CN.md +619 -0
- package/opencode-skills/agy-crew-accounts/SKILL.md +85 -0
- package/opencode-skills/agy-crew-ask/SKILL.md +46 -0
- package/opencode-skills/agy-crew-implementer/SKILL.md +56 -0
- package/opencode-skills/agy-crew-jobs/SKILL.md +99 -0
- package/opencode-skills/agy-crew-jobs/references/setup.md +41 -0
- package/opencode-skills/agy-crew-jobs/references/troubleshooting.md +35 -0
- package/opencode-skills/agy-crew-lead/SKILL.md +39 -0
- package/opencode-skills/agy-crew-researcher/SKILL.md +52 -0
- package/opencode-skills/agy-crew-reviewer/SKILL.md +62 -0
- package/opencode-skills/agy-crew-reviewer/references/code-review.md +46 -0
- package/opencode-skills/agy-crew-reviewer/references/general-review.md +22 -0
- package/opencode-skills/agy-crew-staffer/SKILL.md +54 -0
- package/opencode.mjs +14 -0
- package/package.json +53 -4
- package/pi-skills/agy-crew-accounts/SKILL.md +85 -0
- package/pi-skills/agy-crew-ask/SKILL.md +46 -0
- package/pi-skills/agy-crew-implementer/SKILL.md +56 -0
- package/pi-skills/agy-crew-jobs/SKILL.md +99 -0
- package/pi-skills/agy-crew-jobs/references/setup.md +41 -0
- package/pi-skills/agy-crew-jobs/references/troubleshooting.md +35 -0
- package/pi-skills/agy-crew-lead/SKILL.md +39 -0
- package/pi-skills/agy-crew-researcher/SKILL.md +52 -0
- package/pi-skills/agy-crew-reviewer/SKILL.md +62 -0
- package/pi-skills/agy-crew-reviewer/references/code-review.md +46 -0
- package/pi-skills/agy-crew-reviewer/references/general-review.md +22 -0
- package/pi-skills/agy-crew-staffer/SKILL.md +54 -0
- package/scripts/generate-skills.mjs +138 -0
- package/skills/accounts/SKILL.md +78 -0
- package/skills/ask/SKILL.md +40 -0
- package/skills/implementer/SKILL.md +50 -0
- package/skills/jobs/SKILL.md +93 -0
- package/skills/jobs/references/setup.md +39 -0
- package/skills/jobs/references/troubleshooting.md +33 -0
- package/skills/lead/SKILL.md +32 -0
- package/skills/researcher/SKILL.md +46 -0
- package/skills/reviewer/SKILL.md +56 -0
- package/skills/reviewer/references/code-review.md +44 -0
- package/skills/reviewer/references/general-review.md +20 -0
- package/skills/staffer/SKILL.md +48 -0
- package/templates/ask.md +5 -0
- package/templates/harness-compatibility.md +5 -0
- package/templates/implement.md +39 -0
- package/templates/research.md +37 -0
- package/templates/review.md +27 -0
- package/templates/staffer.md +19 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { PRODUCT } from './identity.mjs';
|
|
4
|
+
import { createParser, createProjection, excerpt, boundSnapshot } from './observation.mjs';
|
|
5
|
+
import { replaceFile } from './state-lock.mjs';
|
|
6
|
+
|
|
7
|
+
export function atomicJSON(file, value) {
|
|
8
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
9
|
+
fs.writeFileSync(tmp, JSON.stringify(value) + '\n');
|
|
10
|
+
replaceFile(tmp, file);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function signalGroup(pid, signal, runner = spawnSync, platform = process.platform) {
|
|
14
|
+
if (!Number.isInteger(pid) || pid <= 1) return;
|
|
15
|
+
if (platform === 'win32') {
|
|
16
|
+
try {
|
|
17
|
+
// Never /T here: taskkill's own tree walk follows stale ParentProcessId
|
|
18
|
+
// links (a dead parent's PID reused by this root) into unrelated orphans.
|
|
19
|
+
// Descendants are terminated one by one by stopExecution after each has
|
|
20
|
+
// passed the identity and birth-order checks in tree().
|
|
21
|
+
const res = runner('taskkill', ['/PID', String(pid), '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
22
|
+
if (res?.error || (res?.status != null && res.status !== 0)) {
|
|
23
|
+
try { process.kill(pid); } catch { /* already exited */ }
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
try { process.kill(pid); } catch { /* already exited */ }
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
try { process.kill(-pid, signal); } catch { /* already exited */ }
|
|
31
|
+
}
|
|
32
|
+
export const terminateProcessGroup = signalGroup;
|
|
33
|
+
|
|
34
|
+
export function parseWindowsProcessTable(stdout) {
|
|
35
|
+
if (!stdout || typeof stdout !== 'string') return [];
|
|
36
|
+
const lines = stdout.trim().split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
37
|
+
if (lines.length === 0) return [];
|
|
38
|
+
|
|
39
|
+
let format = 'powershell';
|
|
40
|
+
const firstLine = lines[0];
|
|
41
|
+
if (/^CreationDate/i.test(firstLine)) {
|
|
42
|
+
format = 'wmic';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
if (/^(ProcessId|ParentProcessId|CreationDate|--+)/i.test(line)) continue;
|
|
48
|
+
if (format === 'wmic') {
|
|
49
|
+
const match = /^(\S+)\s+(\d+)\s+(\d+)$/.exec(line);
|
|
50
|
+
if (match) {
|
|
51
|
+
const born = match[1];
|
|
52
|
+
const parent = Number(match[2]);
|
|
53
|
+
const pid = Number(match[3]);
|
|
54
|
+
rows.push({ pid, parent, group: pid, born });
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
const match = /^(\d+)\s+(\d+)\s*(.*)$/.exec(line);
|
|
58
|
+
if (match) {
|
|
59
|
+
const pid = Number(match[1]);
|
|
60
|
+
const parent = Number(match[2]);
|
|
61
|
+
const born = match[3].trim() || 'unknown';
|
|
62
|
+
rows.push({ pid, parent, group: pid, born });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return rows;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function windowsProcessTable(runner = spawnSync) {
|
|
70
|
+
let res;
|
|
71
|
+
try {
|
|
72
|
+
res = runner('powershell', [
|
|
73
|
+
'-NoLogo',
|
|
74
|
+
'-NoProfile',
|
|
75
|
+
'-NonInteractive',
|
|
76
|
+
'-Command',
|
|
77
|
+
// -Property limits the WMI fetch to the three columns we parse. The
|
|
78
|
+
// round-trip ("o") format keeps the 100 ns precision of CreationDate;
|
|
79
|
+
// the default locale rendering is second-granular, too coarse to order a
|
|
80
|
+
// process against one that reused its parent's PID in the same second.
|
|
81
|
+
"Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate | Select-Object ProcessId,ParentProcessId,@{Name='CreationDate';Expression={if ($_.CreationDate) { $_.CreationDate.ToString('o') } else { '' }}}",
|
|
82
|
+
// A cold PowerShell start plus the CIM query can take several seconds on
|
|
83
|
+
// a busy host; a timeout here would make cleanup skip the tree entirely.
|
|
84
|
+
], { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
res = { error: err };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (res?.error || res?.status !== 0) {
|
|
90
|
+
try {
|
|
91
|
+
res = runner('wmic', [
|
|
92
|
+
'process',
|
|
93
|
+
'get',
|
|
94
|
+
'ProcessId,ParentProcessId,CreationDate',
|
|
95
|
+
], { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
res = { error: err };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (res?.error || res?.status !== 0 || !res?.stdout) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return parseWindowsProcessTable(res.stdout);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Track process birth stamps so a previously observed PID cannot cause cleanup
|
|
108
|
+
// to kill an unrelated process after PID reuse. Tool shells may create groups.
|
|
109
|
+
let inspectionUnavailable = false;
|
|
110
|
+
// No caching, even though the Windows query is slow: callers rely on a
|
|
111
|
+
// process spawned a moment ago being visible (identity capture, hard stop).
|
|
112
|
+
export function processTable() {
|
|
113
|
+
if (process.platform === 'win32') {
|
|
114
|
+
const table = windowsProcessTable();
|
|
115
|
+
if (!table) {
|
|
116
|
+
if (!inspectionUnavailable) process.stderr.write(PRODUCT + ' warning: process-tree inspection unavailable; run unsandboxed to verify descendant cleanup.\n');
|
|
117
|
+
inspectionUnavailable = true;
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return table;
|
|
121
|
+
}
|
|
122
|
+
const ps = spawnSync('ps', ['-axo', 'pid=,ppid=,pgid=,lstart='], {
|
|
123
|
+
encoding: 'utf8',
|
|
124
|
+
timeout: 1000,
|
|
125
|
+
windowsHide: true,
|
|
126
|
+
env: { ...process.env, LC_ALL: 'C', TZ: 'UTC' },
|
|
127
|
+
});
|
|
128
|
+
if (ps.error || ps.status !== 0) {
|
|
129
|
+
if (!inspectionUnavailable) process.stderr.write(PRODUCT + ' warning: process-tree inspection unavailable; run unsandboxed to verify descendant cleanup.\n');
|
|
130
|
+
inspectionUnavailable = true;
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return (ps.stdout || '').trim().split('\n').flatMap((line) => {
|
|
134
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.+)$/.exec(line);
|
|
135
|
+
return match ? [{ pid: Number(match[1]), parent: Number(match[2]), group: Number(match[3]), born: match[4].trim() }] : [];
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Birth stamp as 100 ns ticks since the epoch (BigInt, so PowerShell's
|
|
139
|
+
* seven fractional digits survive), or null when the format is unknown.
|
|
140
|
+
* Accepts ISO-8601 (PowerShell "o"), WMIC (yyyyMMddHHmmss.ffffff+ZZZ) and
|
|
141
|
+
* any legacy rendering Date.parse understands. */
|
|
142
|
+
export function parseBorn(born) {
|
|
143
|
+
if (typeof born !== 'string' || !born || born === 'unknown') return null;
|
|
144
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,7}))?(Z|[+-]\d{2}:\d{2})?$/.exec(born);
|
|
145
|
+
if (iso) {
|
|
146
|
+
const seconds = BigInt(Date.UTC(+iso[1], +iso[2] - 1, +iso[3], +iso[4], +iso[5], +iso[6])) / 1000n;
|
|
147
|
+
const ticks = BigInt((iso[7] || '').padEnd(7, '0'));
|
|
148
|
+
const zoneMinutes = iso[8] && iso[8] !== 'Z' ? (iso[8].startsWith('-') ? -1n : 1n) * (BigInt(iso[8].slice(1, 3)) * 60n + BigInt(iso[8].slice(4, 6))) : 0n;
|
|
149
|
+
return (seconds - zoneMinutes * 60n) * 10_000_000n + ticks;
|
|
150
|
+
}
|
|
151
|
+
const wmic = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{6})([+-]\d{3})$/.exec(born);
|
|
152
|
+
if (wmic) {
|
|
153
|
+
const seconds = BigInt(Date.UTC(+wmic[1], +wmic[2] - 1, +wmic[3], +wmic[4], +wmic[5], +wmic[6])) / 1000n;
|
|
154
|
+
return (seconds - BigInt(wmic[8]) * 60n) * 10_000_000n + BigInt(wmic[7]) * 10n;
|
|
155
|
+
}
|
|
156
|
+
const legacy = Date.parse(born);
|
|
157
|
+
return Number.isNaN(legacy) ? null : BigInt(legacy) * 10_000n;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A process cannot be older than its parent. A row whose recorded parent was
|
|
161
|
+
* born after it is an orphan whose dead parent's PID has been reused: Windows
|
|
162
|
+
* keeps the stale ParentProcessId, and the reuser is not its ancestor. When
|
|
163
|
+
* either stamp is unreadable the edge is kept; POSIX reparents orphans to
|
|
164
|
+
* init, so the stale-link case does not arise there. */
|
|
165
|
+
export function bornAfterParent(child, parent) {
|
|
166
|
+
const c = parseBorn(child?.born), p = parseBorn(parent?.born);
|
|
167
|
+
if (c === null || p === null) return true;
|
|
168
|
+
return c >= p;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function tree(pid, rows = processTable()) {
|
|
172
|
+
if (!rows || !pid) return [];
|
|
173
|
+
const root = rows.find((row) => row.pid === pid);
|
|
174
|
+
if (!root) return [];
|
|
175
|
+
const found = new Map([[pid, root]]);
|
|
176
|
+
for (let changed = true; changed;) {
|
|
177
|
+
changed = false;
|
|
178
|
+
for (const row of rows) {
|
|
179
|
+
if (found.has(row.pid) || !found.has(row.parent)) continue;
|
|
180
|
+
if (!bornAfterParent(row, found.get(row.parent))) continue;
|
|
181
|
+
found.set(row.pid, row); changed = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return rows.filter((row) => row.pid !== pid && found.has(row.pid));
|
|
185
|
+
}
|
|
186
|
+
export function descendants(pid) { return tree(pid).map((row) => row.pid); }
|
|
187
|
+
|
|
188
|
+
export function processIdentity(pid) {
|
|
189
|
+
if (!Number.isInteger(pid) || pid <= 1) return null;
|
|
190
|
+
return processTable()?.find((row) => row.pid === pid) || null;
|
|
191
|
+
}
|
|
192
|
+
const matches = (rows, identity) => !!identity && !!rows?.some((row) => row.pid === identity.pid && row.born === identity.born);
|
|
193
|
+
|
|
194
|
+
export async function stopExecution(root, known = [], table = processTable) {
|
|
195
|
+
if (!root) return;
|
|
196
|
+
const current = table();
|
|
197
|
+
if (!current) return;
|
|
198
|
+
const children = new Map(known.filter((old) => matches(current, old)).map((row) => [row.pid, row]));
|
|
199
|
+
if (matches(current, root)) {
|
|
200
|
+
children.set(root.pid, root);
|
|
201
|
+
for (const row of tree(root.pid, current)) children.set(row.pid, row);
|
|
202
|
+
}
|
|
203
|
+
// This process is never a member of the execution group it is stopping.
|
|
204
|
+
children.delete(process.pid);
|
|
205
|
+
// Nothing of ours is left: skip the grace wait and the second table query.
|
|
206
|
+
if (children.size === 0) return;
|
|
207
|
+
// A member spawned after the last snapshot (a tool started during the grace
|
|
208
|
+
// period) is adopted from a still-live, identity-matched member under the
|
|
209
|
+
// same birth-order rule as tree(); a dead member's PID proves nothing.
|
|
210
|
+
const adopt = (rows) => {
|
|
211
|
+
if (!rows) return;
|
|
212
|
+
for (let changed = true; changed;) {
|
|
213
|
+
changed = false;
|
|
214
|
+
for (const row of rows) {
|
|
215
|
+
if (children.has(row.pid) || row.pid === process.pid) continue;
|
|
216
|
+
const parent = children.get(row.parent);
|
|
217
|
+
if (!parent || !matches(rows, parent) || !bornAfterParent(row, parent)) continue;
|
|
218
|
+
children.set(row.pid, row); changed = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const signal = (rows, kind) => {
|
|
223
|
+
adopt(rows);
|
|
224
|
+
// A surviving, identified member proves this is still our execution group.
|
|
225
|
+
const reusedLeader = rows?.some((row) => row.pid === root.pid && row.born !== root.born);
|
|
226
|
+
const ownedMember = rows?.some((row) => row.group === root.pid && children.get(row.pid)?.born === row.born);
|
|
227
|
+
if (!reusedLeader && ownedMember) signalGroup(root.pid, kind);
|
|
228
|
+
for (const child of [...children.values()].reverse()) {
|
|
229
|
+
if (matches(rows, child)) { try { process.kill(child.pid, kind); } catch {} }
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
signal(current, 'SIGTERM');
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
234
|
+
signal(table(), 'SIGKILL');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function runStreaming({ binary, args, job, budget, signal, update, conversation, env }) {
|
|
238
|
+
const hardDeadline = Date.now() + Math.max(0, budget);
|
|
239
|
+
const rawFd = fs.openSync(job.events_file, 'a');
|
|
240
|
+
const projection = createProjection(conversation);
|
|
241
|
+
let payload = null, stderr = '', stdoutTail = '', lastPublish = 0, child, root, deadline, publishTimer, trackingTimer;
|
|
242
|
+
let stopping = null, reason = null, spawnError = null, streamError = null;
|
|
243
|
+
const tracked = new Map();
|
|
244
|
+
const track = () => {
|
|
245
|
+
const rows = processTable();
|
|
246
|
+
if (!rows) return; // An unavailable inspection is not evidence of exit.
|
|
247
|
+
for (const [pid, old] of tracked) if (!rows.some((row) => row.pid === pid && row.born === old.born)) tracked.delete(pid);
|
|
248
|
+
if (!root) root = rows.find((row) => row.pid === child?.pid);
|
|
249
|
+
if (matches(rows, root)) for (const row of tree(root.pid, rows)) tracked.set(row.pid, row);
|
|
250
|
+
// A detached child owns its group until exit; collect orphaned members at
|
|
251
|
+
// the exit/result boundary too, unless the leader PID has been reused.
|
|
252
|
+
if (root && !rows.some((row) => row.pid === root.pid && row.born !== root.born)) {
|
|
253
|
+
for (const row of rows) if (row.pid !== root.pid && row.group === root.pid) tracked.set(row.pid, row);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const publish = () => {
|
|
257
|
+
lastPublish = Date.now();
|
|
258
|
+
atomicJSON(job.progress_file, boundSnapshot({ job_id: job.id, ...projection.snapshot() }));
|
|
259
|
+
};
|
|
260
|
+
const stop = (why) => {
|
|
261
|
+
if (why === 'canceled') reason = why;
|
|
262
|
+
if (stopping) return;
|
|
263
|
+
reason = why;
|
|
264
|
+
// Latch the stop reason before cleanup. The terminal record is published
|
|
265
|
+
// only after the report is durable; canceled records stay canceled.
|
|
266
|
+
clearInterval(trackingTimer);
|
|
267
|
+
track();
|
|
268
|
+
stopping = cleanup();
|
|
269
|
+
};
|
|
270
|
+
const cleanup = async () => {
|
|
271
|
+
// ChildProcess.kill only targets our still-running direct child. Stored
|
|
272
|
+
// numeric PIDs are never sufficient authority for a signal.
|
|
273
|
+
if (!root && child?.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
|
|
274
|
+
await stopExecution(root, [...tracked.values()]);
|
|
275
|
+
if (child?.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
276
|
+
};
|
|
277
|
+
const abort = () => stop('canceled');
|
|
278
|
+
const safely = (action) => {
|
|
279
|
+
try { action(); } catch (error) { streamError ||= error; stop('stream_error'); }
|
|
280
|
+
};
|
|
281
|
+
try {
|
|
282
|
+
publish();
|
|
283
|
+
// On Windows, detached: true creates a new console window; piped stdio keeps the
|
|
284
|
+
// process stream connected. On POSIX, detached: true creates a new process group.
|
|
285
|
+
child = spawn(binary, args, {
|
|
286
|
+
detached: process.platform !== 'win32',
|
|
287
|
+
windowsHide: true,
|
|
288
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
289
|
+
// undefined inherits the process environment; pool accounts pass a private HOME
|
|
290
|
+
env,
|
|
291
|
+
});
|
|
292
|
+
const exited = new Promise((resolve) => {
|
|
293
|
+
child.once('error', (error) => { spawnError = error; resolve({ exit: null, killedSignal: null }); });
|
|
294
|
+
child.once('exit', (exit, killedSignal) => resolve({ exit, killedSignal }));
|
|
295
|
+
});
|
|
296
|
+
const closed = new Promise((resolve) => child.once('close', resolve));
|
|
297
|
+
track();
|
|
298
|
+
// ps is cheap; the PowerShell CIM query on Windows takes 1-3 s and runs
|
|
299
|
+
// synchronously, so sample less often there to keep the event loop free.
|
|
300
|
+
trackingTimer = setInterval(track, process.platform === 'win32' ? 5000 : 1000);
|
|
301
|
+
update({ agy_pid: child.pid, execution_started_at: new Date().toISOString(), hard_deadline_at: new Date(hardDeadline).toISOString() });
|
|
302
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
303
|
+
if (signal.aborted) abort();
|
|
304
|
+
deadline = setTimeout(() => stop('hard_timeout'), Math.max(0, hardDeadline - Date.now()));
|
|
305
|
+
const parser = createParser((event) => {
|
|
306
|
+
projection.accept(event);
|
|
307
|
+
if (event.event === 'result' && event.result && typeof event.result === 'object') { payload = event.result; track(); }
|
|
308
|
+
if (Date.now() - lastPublish >= 100) publish();
|
|
309
|
+
else if (!publishTimer) publishTimer = setTimeout(() => { publishTimer = null; safely(publish); }, 100);
|
|
310
|
+
}, projection.warn, 64 * 1024 * 1024);
|
|
311
|
+
child.stdout.on('data', (chunk) => safely(() => {
|
|
312
|
+
fs.writeSync(rawFd, chunk);
|
|
313
|
+
stdoutTail = excerpt(stdoutTail + chunk.toString('utf8'), 8192, true).text;
|
|
314
|
+
parser.write(chunk);
|
|
315
|
+
}));
|
|
316
|
+
child.stderr.on('data', (chunk) => safely(() => {
|
|
317
|
+
process.stderr.write(chunk);
|
|
318
|
+
stderr = excerpt(stderr + chunk.toString('utf8'), 8192, true).text;
|
|
319
|
+
}));
|
|
320
|
+
const outcome = await exited;
|
|
321
|
+
clearTimeout(deadline); clearInterval(trackingTimer);
|
|
322
|
+
if (stopping) await stopping;
|
|
323
|
+
// Kill remaining members even if the CLI itself already exited.
|
|
324
|
+
else { track(); stopping = cleanup(); await stopping; }
|
|
325
|
+
// Drain buffered result bytes, but do not wait indefinitely for a tool
|
|
326
|
+
// process that inherited stdout/stderr and escaped cleanup.
|
|
327
|
+
let drainTimer;
|
|
328
|
+
const drained = await Promise.race([closed.then(() => true), new Promise((resolve) => { drainTimer = setTimeout(() => resolve(false), 500); })]);
|
|
329
|
+
clearTimeout(drainTimer);
|
|
330
|
+
if (!drained) {
|
|
331
|
+
projection.warn('AGY exited but inherited output pipes did not close; output drain was bounded.');
|
|
332
|
+
child.stdout.destroy(); child.stderr.destroy();
|
|
333
|
+
}
|
|
334
|
+
safely(() => parser.end());
|
|
335
|
+
clearTimeout(publishTimer);
|
|
336
|
+
if (inspectionUnavailable) projection.warn('Process-tree inspection unavailable; verify descendant cleanup from the original unsandboxed context.');
|
|
337
|
+
publish();
|
|
338
|
+
if (streamError) throw Object.assign(streamError, { reason: 'stream_error' });
|
|
339
|
+
if (reason === 'hard_timeout' && typeof payload?.response === 'string' && payload.response.trim()) {
|
|
340
|
+
const warning = 'Worker hard execution limit reached after a response was received; delivering the response with a cleanup warning.';
|
|
341
|
+
projection.warn(warning);
|
|
342
|
+
stderr = `${stderr}\n${warning}`.trim();
|
|
343
|
+
process.stderr.write(`${PRODUCT} warning: ${warning}\n`);
|
|
344
|
+
} else if (reason) throw Object.assign(new Error(reason === 'hard_timeout' ? 'Worker hard execution limit reached.' : 'Execution canceled.'), { reason });
|
|
345
|
+
if (spawnError) throw Object.assign(new Error(`failed to launch agy (${binary}): ${spawnError.message}`), { reason: 'launch_error' });
|
|
346
|
+
if (!payload) {
|
|
347
|
+
let message = `agy did not return parseable JSON result (exit ${outcome.exit}, signal ${outcome.killedSignal || 'none'}). stdout: see ${job.events_file}\nstderr: ${stderr || '(empty)'}`;
|
|
348
|
+
if (/operation not permitted/i.test(stderr)) message += '\nThis looks like a harness command sandbox blocking agy. Run this companion command unsandboxed (escalated permissions).';
|
|
349
|
+
throw Object.assign(new Error(message), { reason: 'missing_result', diagnosticText: `${stdoutTail}\n${stderr}` });
|
|
350
|
+
}
|
|
351
|
+
return { payload, stderr, exit: outcome.exit ?? 1, observationWarnings: projection.snapshot().warnings };
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (!stopping && child?.pid) await cleanup();
|
|
354
|
+
throw error;
|
|
355
|
+
} finally {
|
|
356
|
+
clearInterval(trackingTimer);
|
|
357
|
+
clearTimeout(deadline); clearTimeout(publishTimer);
|
|
358
|
+
signal.removeEventListener('abort', abort);
|
|
359
|
+
fs.closeSync(rawFd);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Installing agy-crew — a guide for coding agents
|
|
2
|
+
|
|
3
|
+
You are a coding agent that has been asked to install and verify the **agy-crew** plugin. Follow this file top to bottom. When you finish (or get blocked), report the outcome to the user **in the user's own language** — the language they have been using with you, not necessarily English.
|
|
4
|
+
|
|
5
|
+
> [!IMPORTANT]
|
|
6
|
+
> If you reached this file through a URL, read its **raw** text — `curl -fsSL https://raw.githubusercontent.com/pStrikeZ/agy-crew/master/docs/INSTALL_FOR_AGENTS.md` — or clone the repo. Fetching the rendered page through a web tool that summarises before handing you the content can return a paraphrase, and a paraphrased install command is a broken install command. If all you have is a summary, stop and fetch the raw file.
|
|
7
|
+
|
|
8
|
+
## 0. Prerequisites
|
|
9
|
+
|
|
10
|
+
1. **`agy` binary** — run `agy --version`. Use a version supporting `stream-json` (lifecycle smoke tests use v1.1.27). If it is missing, do **not** install it yourself: give the user the official install page <https://antigravity.google/docs/cli/install> (macOS/Linux: `curl -fsSL https://antigravity.google/cli/install.sh | bash`) and wait for them to install and authenticate (`agy` run interactively once handles login).
|
|
11
|
+
2. **Node.js** — run `node --version` (20 or newer). The companion script uses only the Node standard library.
|
|
12
|
+
3. **No agy-staff on this machine.** agy-crew is a fork of the upstream agy-staff, and the two must not be installed together: the skills would appear twice and both companions would write the same per-repository state. `agy-crew install` refuses to run when it finds agy-staff. If you find agy-staff (the `agy@agy-staff` Claude Code plugin, an agy-staff plugin in Codex, an `agy-staff@…` entry in `opencode.json`, an agy-staff Pi package, or upstream flat skills such as `agy-ask` and `agy-jobs` in a harness skills directory), **stop and ask the user before removing anything**. Never remove it silently. With the user's agreement, the removal is: Claude Code `claude plugin uninstall agy@agy-staff`; Codex remove the agy-staff plugin and restart; OpenCode remove the `agy-staff@…` entry from `opencode.json`; Pi uninstall the agy-staff package; and delete any upstream flat skills.
|
|
13
|
+
|
|
14
|
+
## 1. Detect which harness you are running in
|
|
15
|
+
|
|
16
|
+
You normally know which product you are. If unsure, check:
|
|
17
|
+
|
|
18
|
+
- **Claude Code** — you have `/plugin` slash commands, project instructions arrive via `CLAUDE.md`, and your Bash tool typically has `CLAUDECODE=1` in the environment. → Follow section 2a.
|
|
19
|
+
- **Codex** — you invoke skills with `$name` syntax, follow `AGENTS.md`, and plugins are managed through the `codex` CLI. → Follow section 2b.
|
|
20
|
+
- **Pi** — you invoke skills with `/skill:name` and manage packages with `pi install`. → Follow section 2c.
|
|
21
|
+
- **OpenCode V1** — you use `opencode`, its `plugin` config or `opencode plugin` installer, and native `/agy-crew-*` commands. Verified with OpenCode 1.18.34; V2 is not covered. → Follow section 2d.
|
|
22
|
+
- **Oh My Pi / DeepSeek Harness** — supported through route A only (`--hosts omp`, `--hosts dsh`).
|
|
23
|
+
- **Anything else** — this package documents Claude Code, Codex, Pi, OpenCode V1, Oh My Pi and DeepSeek Harness. Report the unsupported harness and stop.
|
|
24
|
+
|
|
25
|
+
### Route A (preferred): the `agy-crew` terminal command
|
|
26
|
+
|
|
27
|
+
If `node` and `npm` are available, install the command and let it put the skills into the harness you are running in. It supports Claude Code (`claude`), Codex (`codex`), Oh My Pi (`omp`), OpenCode (`opencode`) and DeepSeek Harness (`dsh`); it does not cover Pi, which uses section 2c.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g agy-crew
|
|
31
|
+
agy-crew install --dry-run --hosts <this harness> # optional: print the plan, write nothing
|
|
32
|
+
agy-crew install --hosts <this harness> # e.g. --hosts claude
|
|
33
|
+
agy-crew install --status --json # verify per-host link state and bundle
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For a local checkout the user gave you, run `npm link` inside it instead of `npm install -g`; the skills then run from the checkout, and `agy-crew install` must be re-run after a `git pull`. The installer renders the skills once into `~/.agy-crew/bundles/<version>-<digest>/` and links `agy-crew-<persona>` directories into the harness skills directory. It never overwrites anything it did not create; on any conflict it aborts before writing and prints the paths and a remedy, which you should relay verbatim and not work around. If route A succeeds, skip to section 3.
|
|
37
|
+
|
|
38
|
+
### Route A′ (fallback): no npm, git clone + node
|
|
39
|
+
|
|
40
|
+
If `node` is available but `npm` is not (or the user does not want a global npm package), and `git` is available, clone the repo and run the companion directly. Pick a stable location (for example `~/agy-crew`); the bundle stores the absolute path of the companion inside the clone, so the clone must not move or be deleted afterwards.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/pStrikeZ/agy-crew <clone>
|
|
44
|
+
node <clone>/companion/agy-companion.mjs install --dry-run --hosts <this harness> # optional
|
|
45
|
+
node <clone>/companion/agy-companion.mjs install --hosts <this harness>
|
|
46
|
+
node <clone>/companion/agy-companion.mjs install --status --json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The skills behave exactly as in route A, but there is **no `agy-crew` command on PATH**: wherever this guide says `agy-crew <command>`, run `node <clone>/companion/agy-companion.mjs <command>` instead (a shell alias is optional and the user's call). To update, `git -C <clone> pull`, then re-run the install command. To uninstall, run `node <clone>/companion/agy-companion.mjs uninstall --all`, then delete the clone; `~/.agy-crew` (pool accounts) is removed only manually and only if the user asks. If route A′ succeeds, skip to section 3.
|
|
50
|
+
|
|
51
|
+
### Route B: the harness plugin manager
|
|
52
|
+
|
|
53
|
+
Use this when route A and A′ are not possible (no node-based install, Pi, or the user prefers the plugin manager). Note the trade-off for the user: there is no terminal command, and the account pool commands are awkward because the companion lives in the plugin cache. Follow exactly one of the four sections below.
|
|
54
|
+
|
|
55
|
+
## 2a. Claude Code — plugin route (B) install / upgrade
|
|
56
|
+
|
|
57
|
+
Use the `claude` CLI. The `/plugin …` forms you may have seen are TUI slash commands typed by a human — you cannot execute them from your Bash tool, and there is no shell equivalent of "typing a slash command".
|
|
58
|
+
|
|
59
|
+
Install (use the local checkout path instead of the slug if the user gave you one):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
claude plugin marketplace add pStrikeZ/agy-crew # human types: /plugin marketplace add pStrikeZ/agy-crew
|
|
63
|
+
claude plugin install agy-crew@agy-crew # human types: /plugin install agy-crew@agy-crew
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Upgrade an existing install — note this is `update`, not `install`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
claude plugin marketplace update agy-crew
|
|
70
|
+
claude plugin update agy-crew@agy-crew
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> [!IMPORTANT]
|
|
74
|
+
> `install` never upgrades: on an already-installed plugin it answers "already installed" and does nothing, whatever the version. And `update` is keyed on the **version string**, not the commit — if the published version is unchanged it answers "already at the latest version", so the user keeps running the old commit while the marketplace clone has moved on. Check by comparing `gitCommitSha` in `~/.claude/plugins/installed_plugins.json` against `git -C ~/.claude/plugins/marketplaces/agy-crew log -1`. When they differ but the version does not, force the current commit in: `claude plugin uninstall agy-crew@agy-crew && claude plugin install agy-crew@agy-crew`.
|
|
75
|
+
|
|
76
|
+
Then verify what actually landed: `claude plugin list` should show `agy-crew@agy-crew` enabled, at the version you expected. If the user is a contributor, watch for an install whose marketplace source is a **local directory** rather than the GitHub slug — that install tracks their working tree, not a release, which is fine for development but is not what "install the plugin" usually means. Say so, and offer the clean path: `claude plugin uninstall agy-crew@agy-crew`, `claude plugin marketplace remove agy-crew`, then add the slug again.
|
|
77
|
+
|
|
78
|
+
**A restart is required before the plugin is usable.** A freshly installed plugin is not in the current session's skill registry, so `/agy-crew:…` either does not resolve or — if an older copy was loaded when the session started — silently resolves to that stale copy. Tell the user to restart Claude Code, or verify without a restart using the shell fallback in section 3.
|
|
79
|
+
|
|
80
|
+
## 2b. Codex — plugin route (B) install / upgrade
|
|
81
|
+
|
|
82
|
+
Install (use the local checkout path instead of the URL if the user gave you one):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
codex plugin marketplace add https://github.com/pStrikeZ/agy-crew
|
|
86
|
+
codex plugin add agy-crew@agy-crew
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Then the user must restart the app — Codex caches plugins per version. Upgrades reach the app only after the plugin version is bumped **and** `codex plugin marketplace upgrade` is run, followed by a restart.
|
|
90
|
+
|
|
91
|
+
> [!IMPORTANT]
|
|
92
|
+
> Codex's command sandbox cannot run agy. agy binds a localhost port for its internal language server and reads its OAuth token file; the workspace-write sandbox blocks the bind and hides the token (secret protection — no `writable_roots`/`network_access` config opens it). Every companion command (both job dispatch and management commands such as `wait`, `status`, `result`) must run **unsandboxed**: the workspace needs full access, or each companion command needs escalated approval. The failure signature is `operation not permitted` on `~/.gemini/antigravity-cli/...` followed by empty output or a bogus "authentication failed".
|
|
93
|
+
|
|
94
|
+
## 2c. Pi — package route (B) install / upgrade / local development
|
|
95
|
+
|
|
96
|
+
Install (use the local checkout path if provided):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pi install git:github.com/pStrikeZ/agy-crew
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
For local development checkouts, use `pi install /absolute/path/to/checkout` (or `pi -e /absolute/path/to/checkout` for a temporary session). Run `npm run generate:pi` in the checkout if canonical skills were modified.
|
|
103
|
+
|
|
104
|
+
To upgrade an existing Git install:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pi update --extension git:github.com/pStrikeZ/agy-crew
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Restart Pi or run `/reload` afterwards. Use `pi list` to verify the package is registered, then check Pi's skill picker for `agy-crew-lead`, `agy-crew-ask`, `agy-crewer`, `agy-crew-researcher`, `agy-crew-reviewer`, `agy-crew-implementer`, and `agy-crew-jobs`.
|
|
111
|
+
|
|
112
|
+
## 2d. OpenCode V1 — plugin route (B) install / upgrade / local development
|
|
113
|
+
|
|
114
|
+
Use the native plugin installer (verified with OpenCode 1.18.34):
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
opencode plugin 'agy-crew@git+https://github.com/pStrikeZ/agy-crew.git' --global
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This installs the full package and adds it to the `plugin` array in OpenCode's config, preserving other settings. Do not replace the user's config file. The plugin registers its bundled skills automatically; do not manually clone the repository or add `skills.paths` as an installation workaround. For an explicitly provided local checkout, use `opencode plugin /absolute/path/to/checkout --global`. Regenerate modified canonical skills with `npm run generate:skills` in that checkout.
|
|
121
|
+
|
|
122
|
+
Restart OpenCode, then verify with `opencode debug skill`: expect `agy-crew-ask`, `agy-crewer`, `agy-crew-researcher`, `agy-crew-reviewer`, `agy-crew-implementer`, `agy-crew-jobs`, and `agy-crew-lead` alongside any other installed skills. OpenCode exposes these directly as `/agy-crew-*` commands. Existing skill permissions remain in effect. Install the entire package, since its companion, templates and references are relative resources.
|
|
123
|
+
|
|
124
|
+
OpenCode 1.18.34 caches the full package spec. Restarting or reinstalling the same Git spec does not guarantee an update, even with `--force`. Select a newer tag or commit containing the adapter, replace the placeholder below, then restart:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
opencode plugin 'agy-crew@git+https://github.com/pStrikeZ/agy-crew.git#<tag-or-commit>' --global --force
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Changing the ref creates a fresh cache key; `--force` replaces the configured plugin entry. Do not clear unrelated caches. This adapter covers V1, not V2.
|
|
131
|
+
|
|
132
|
+
## 3. Smoke test
|
|
133
|
+
|
|
134
|
+
Run the zero-setup ask mode — it needs no allowlist and answers in ~3 seconds:
|
|
135
|
+
|
|
136
|
+
- Installed with route A or A′ (any harness): `/agy-crew-ask reply with OK` (Codex shows it as `$agy-crew-ask`) — after the restart
|
|
137
|
+
- Claude Code plugin route: `/agy-crew:ask "reply with OK"` — **after the restart**, otherwise you are testing the old copy or nothing at all
|
|
138
|
+
- Codex: `$agy-crew:ask reply with OK`
|
|
139
|
+
- Pi: `/skill:agy-crew-ask reply with OK` — after restart or `/reload`
|
|
140
|
+
- OpenCode: `/agy-crew-ask reply with OK` — after restart
|
|
141
|
+
|
|
142
|
+
If you cannot restart the session, call the companion of the freshly installed copy directly from the shell. After route A the `agy-crew` command is the companion: `agy-crew ask --prompt "reply with OK"`; after route A′ run `node <clone>/companion/agy-companion.mjs ask --prompt "reply with OK"`. It is the same code path the skill takes, so a pass here means the install is sound:
|
|
143
|
+
|
|
144
|
+
For Claude Code, resolve its installed copy with:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
AGY_ROOT=$(node -p 'require(process.env.HOME+"/.claude/plugins/installed_plugins.json").plugins["agy-crew@agy-crew"][0].installPath')
|
|
148
|
+
node "$AGY_ROOT/companion/agy-companion.mjs" ask --prompt "reply with OK"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Resolve the root that way rather than globbing `cache/agy-crew/agy-crew/*/`: superseded version directories are left behind after an upgrade, so the glob expands to several paths and the command fails with `unknown subcommand`. `installPath` is always the copy in use. (Codex's equivalent root is printed by `codex plugin list`.) For OpenCode, `opencode debug skill` reports each skill location; resolve `../../companion/agy-companion.mjs` from the installed `agy-crew-ask` skill directory and invoke it with `node` from the user’s project directory. For Pi, use the registered package root from `pi list`. A fallback pass still leaves the restart outstanding — report it as "installed and verified, restart the current harness to use it".
|
|
152
|
+
|
|
153
|
+
Expect a short answer on stdout with no telemetry mixed in, plus an `[agy-crew]` telemetry line on stderr (mode, profile, model, duration, tokens, conversation id — for you, not for the user). If it errors, relay the error verbatim; the usual causes are expired agy auth (user runs `agy` interactively once to re-login) or an invalid model id (`agy models` lists valid ids). Do not improvise flags to work around errors.
|
|
154
|
+
|
|
155
|
+
> [!IMPORTANT]
|
|
156
|
+
> A passing `ask` smoke means the install is done. `staffer`, `researcher`, `reviewer` and `implementer` all default to the **unrestricted** profile, so they gather evidence and edit files without any allowlist — nothing else is required to use them. Section 4 (`setup`) is **optional hardening**: it only matters if the user wants to run with `--restricted`, where headless agy fail-closes on every tool call that is not on the allowlist. Do not run setup unprompted; offer it, and apply it only if the user asks for the hardened path.
|
|
157
|
+
|
|
158
|
+
## 4. Optional hardening — setup, dry run first
|
|
159
|
+
|
|
160
|
+
Skip this section unless the user wants it. Every tool-using mode already works unrestricted; setup exists so that the opt-in `--restricted` profile is usable, because a restricted run needs an **evidence-gathering command allowlist** in agy's settings or it comes back empty. Mention it to security-sensitive users (shared machines, reviewing untrusted PRs) and let them decide. Setup can also record a per-repo policy (`setup --restrict review,research` makes those modes default to restricted in the current repository; `--restrict none` clears it) — offer that only in the same opt-in conversation.
|
|
161
|
+
|
|
162
|
+
If they opt in, global install is the normal path. Run the setup **dry run** first — never apply directly:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
agy-crew setup # dry run; only add --apply after user confirmation (without the command on PATH: node <plugin-root>/companion/agy-companion.mjs setup)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The full guided flow lives in the jobs skill (`skills/jobs/references/setup.md`).
|
|
169
|
+
|
|
170
|
+
Show the user the full dry-run output and state these four things plainly before asking for confirmation:
|
|
171
|
+
|
|
172
|
+
1. Which command rules would be added, and that they exist so `--restricted` runs can gather evidence unattended.
|
|
173
|
+
2. The target file is the **global** `~/.gemini/antigravity-cli/settings.json`, so the rules apply to every `agy` run on this machine — not only to agy-crew jobs.
|
|
174
|
+
3. Setup keeps broad git/gh grants and adds five native deny prefixes: `git push`, `git reset --hard`, `git clean`, `gh pr merge`, and `gh release delete`. AGY evaluates deny > ask > allow. Existing rules are preserved, and the dry run shows any additions. This prevents common mistakes, not every irreversible action: alternate command forms, scripts and APIs are not comprehensively covered. Task authorization does not override deny.
|
|
175
|
+
4. The existing file is backed up before writing.
|
|
176
|
+
|
|
177
|
+
Apply only after the user explicitly agrees. If they decline, nothing is lost from the default experience — all four modes keep working; they simply cannot harden a run with `--restricted` until the allowlist exists.
|
|
178
|
+
|
|
179
|
+
If the user is security-sensitive and the machine-wide scope is unacceptable, tell them agy also supports project-scoped permission rules (highest priority) tied to its `--project` system — but that **the project-settings file path is undocumented and unverified against the current agy release**. Do not guess a path and do not write one; point them at [REFERENCE.md → Advanced: project-scoped permissions](REFERENCE.md#advanced-project-scoped-permissions) and let them verify it interactively with `agy`.
|
|
180
|
+
|
|
181
|
+
## 5. Optional — the account pool
|
|
182
|
+
|
|
183
|
+
Skip this section unless the user wants concurrent jobs on several agy accounts. Without it everything runs on the user's own agy login (**live**), exactly as before; live is never part of the pool.
|
|
184
|
+
|
|
185
|
+
Never run `accounts login` yourself. It opens a browser sign-in and waits for the user to exit agy, so only the user can complete it. Offer it, and if they agree give them the command to run in their own terminal:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
agy-crew accounts login work1 # choose any name: lowercase letters, digits, . _ -
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Without the command on PATH (route A′ or B), use `node <clone-or-plugin-root>/companion/agy-companion.mjs accounts login work1`. Repeat with another name for each account (each must be a different Google account). If the user already has agy homes from agy-cli-manager, `agy-crew accounts import <name> --home <dir>` copies the token without a browser; ask before importing. Afterwards you can verify without side effects:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
agy-crew accounts list # each account should show "ready"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Runs then choose an account automatically (`--account auto`). New accounts copy their permission settings from the live settings file on login, and a later `setup --apply` updates every pool account. Details: [REFERENCE.md → Accounts](REFERENCE.md#accounts).
|
|
198
|
+
|
|
199
|
+
## 6. The execution model
|
|
200
|
+
|
|
201
|
+
`ask` returns its answer synchronously. `staffer`, `research`, `review` and `implement` return a job id, and the job-start output prints the exact collect command (`wait <id> --timeout 10m`). Run that as a background command — one background wait per job — and deliver the result when it exits; exit code 2 means still running, so run the same `wait` again (`cancel <id>` stops the job). Do not leave a started job unreported.
|
|
202
|
+
|
|
203
|
+
Exit 5 signals a timeout with a resumable conversation. Inspect the retained workspace changes and ask whether to continue with the suggested timeout or stop; recover only after explicit user confirmation. The continuation command preserves the original persona/model/profile. This also applies to synchronous `ask`; no run automatically retries or continues.
|
|
204
|
+
|
|
205
|
+
Per-repo state lives in `<repo>/.agy-crew/`; the companion git-ignores it automatically on first use (via `.git/info/exclude` — the tracked `.gitignore` is never touched).
|
|
206
|
+
|
|
207
|
+
## 7. Report back
|
|
208
|
+
|
|
209
|
+
Tell the user, in **their** language: whether install succeeded (name the version and whether it came from the GitHub slug or a local checkout, and whether you used route A, A′ or B; for A′ also the clone path, which must stay put), the smoke-test result, whether a restart is still needed before the skills load, and whether the optional setup rules were applied, declined, or never offered (the default unrestricted profile does not need it), and whether any pool accounts were added (and that the user, not you, ran the login).
|
|
210
|
+
|
|
211
|
+
Default: prepare the prompt, dispatch, wait for the final result, then validate as needed. On wait exit 2, wait again for the same job without extra progress checks. While running, do not proactively observe, read logs or inspect intermediate artifacts, including for routine updates. Observe only when the user explicitly asks for progress; diagnose after a failure or a result requiring intervention. Host collection of a pending wait command is necessary result collection, not active observation. Prefer background completion delivery or the longest practical blocking wait the host supports; avoid short empty polls and sleep/observe loops. The worker's hard limit and explicit recovery protocol remain separate from wait soft expiry.
|
|
212
|
+
|
|
213
|
+
`observe <id>` always returns bounded JSON, including after completion. Its exit 0 means the job finished; collect the existing wait session or use `result <id>` to obtain the full report. Observation does not consume the waiting command’s output.
|