@aiwg/cli 2026.7.24 → 2026.8.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/dist/src/artifacts/cli.js +53 -10
- package/dist/src/artifacts/fortemi-shard-export.js +107 -18
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/job.js +97 -0
- package/dist/src/cli/handlers/runtime-info.js +2 -2
- package/dist/src/cli/handlers/serve.js +2 -2
- package/dist/src/cli/handlers/sessions.js +188 -0
- package/dist/src/cli/handlers/steward.js +16 -3
- package/dist/src/extensions/commands/definitions.js +30 -5
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/features/catalog.js +3 -3
- package/dist/src/jobs/executor.js +83 -0
- package/dist/src/jobs/flow.js +106 -0
- package/dist/src/jobs/gitea.js +91 -0
- package/dist/src/jobs/render.js +53 -0
- package/dist/src/jobs/runner.js +315 -0
- package/dist/src/jobs/types.js +3 -0
- package/dist/src/providers/capability-matrix.js +11 -4
- package/dist/src/providers/capability-matrix.yaml +39 -42
- package/dist/src/sessions/analytics.js +303 -0
- package/dist/src/sessions/importer.js +7 -1
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/policy.js +1 -1
- package/dist/src/sessions/repository.js +213 -0
- package/package.json +10 -10
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { approvalLabel, approvalRequired, resolveWorkspaceFile } from './flow.js';
|
|
5
|
+
const CLAIM_PREFIX = '<!-- aiwg-job:claim ';
|
|
6
|
+
const COMPLETE_PREFIX = '<!-- aiwg-job:complete ';
|
|
7
|
+
const FAILURE_PREFIX = '<!-- aiwg-job:failed ';
|
|
8
|
+
function marker(body, prefix) {
|
|
9
|
+
const line = body.split(/\r?\n/u).find(candidate => candidate.startsWith(prefix) && candidate.endsWith(' -->'));
|
|
10
|
+
if (!line)
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(line.slice(prefix.length, -4));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function jobKey(flow, issue) {
|
|
20
|
+
return createHash('sha256')
|
|
21
|
+
.update(`${flow.metadata.name}\0${flow.metadata.revision}\0${issue}`)
|
|
22
|
+
.digest('hex');
|
|
23
|
+
}
|
|
24
|
+
function claimBody(flow, issue, runnerId, expiresAt) {
|
|
25
|
+
const value = {
|
|
26
|
+
job: flow.metadata.name,
|
|
27
|
+
revision: flow.metadata.revision,
|
|
28
|
+
idempotencyKey: jobKey(flow, issue.number),
|
|
29
|
+
runnerId,
|
|
30
|
+
expiresAt,
|
|
31
|
+
};
|
|
32
|
+
return `${CLAIM_PREFIX}${JSON.stringify(value)} -->\n\nAIWG external job claim. Execution remains subject to the reviewed flow contract.`;
|
|
33
|
+
}
|
|
34
|
+
function completionBody(flow, result) {
|
|
35
|
+
const value = {
|
|
36
|
+
job: flow.metadata.name,
|
|
37
|
+
revision: flow.metadata.revision,
|
|
38
|
+
idempotencyKey: result.idempotencyKey,
|
|
39
|
+
externalResultUrl: result.externalResultUrl,
|
|
40
|
+
};
|
|
41
|
+
return [
|
|
42
|
+
`${COMPLETE_PREFIX}${JSON.stringify(value)} -->`,
|
|
43
|
+
'',
|
|
44
|
+
`AIWG external job **${flow.metadata.name}** completed.`,
|
|
45
|
+
`- Idempotency key: \`${result.idempotencyKey}\``,
|
|
46
|
+
`- External result: ${result.externalResultUrl}`,
|
|
47
|
+
`- Account: \`${result.account}\``,
|
|
48
|
+
`- Verification: \`${result.verification.replace(/[\r\n`]+/gu, ' ').slice(0, 1000)}\``,
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
51
|
+
function failureBody(flow, key, message) {
|
|
52
|
+
const safeMessage = message.replace(/[\r\n]+/gu, ' ').slice(0, 1000);
|
|
53
|
+
const value = {
|
|
54
|
+
job: flow.metadata.name,
|
|
55
|
+
revision: flow.metadata.revision,
|
|
56
|
+
idempotencyKey: key,
|
|
57
|
+
reason: safeMessage,
|
|
58
|
+
};
|
|
59
|
+
return [
|
|
60
|
+
`${FAILURE_PREFIX}${JSON.stringify(value)} -->`,
|
|
61
|
+
'',
|
|
62
|
+
`AIWG external job **${flow.metadata.name}** did not pass completion verification.`,
|
|
63
|
+
`- Idempotency key: \`${key}\``,
|
|
64
|
+
`- Result: no completion marker was written`,
|
|
65
|
+
`- Reason: ${safeMessage}`,
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
68
|
+
function failedBy(comments, actor, key) {
|
|
69
|
+
for (const comment of comments) {
|
|
70
|
+
if (comment.author !== actor)
|
|
71
|
+
continue;
|
|
72
|
+
const parsed = marker(comment.body, FAILURE_PREFIX);
|
|
73
|
+
if (parsed?.idempotencyKey === key)
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function validateExternalUrl(flow, value) {
|
|
79
|
+
const resultUrl = new URL(value);
|
|
80
|
+
if (!flow.spec.security.allowedOrigins.includes(resultUrl.origin))
|
|
81
|
+
throw new Error('external result origin is not allow-listed');
|
|
82
|
+
if (resultUrl.username || resultUrl.password)
|
|
83
|
+
throw new Error('external result URL must not contain user information');
|
|
84
|
+
for (const parameter of resultUrl.searchParams.keys()) {
|
|
85
|
+
if (/(auth|cookie|credential|key|password|secret|session|signature|token)/iu.test(parameter)) {
|
|
86
|
+
throw new Error('external result URL contains a sensitive query parameter');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return resultUrl;
|
|
90
|
+
}
|
|
91
|
+
function completedBy(flow, comments, actor, key) {
|
|
92
|
+
for (const comment of comments) {
|
|
93
|
+
if (comment.author !== actor)
|
|
94
|
+
continue;
|
|
95
|
+
const parsed = marker(comment.body, COMPLETE_PREFIX);
|
|
96
|
+
if (parsed?.idempotencyKey === key && typeof parsed.externalResultUrl === 'string') {
|
|
97
|
+
validateExternalUrl(flow, parsed.externalResultUrl);
|
|
98
|
+
return parsed;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function activeClaims(comments, actor, key, now) {
|
|
104
|
+
return comments.flatMap(comment => {
|
|
105
|
+
if (comment.author !== actor)
|
|
106
|
+
return [];
|
|
107
|
+
const parsed = marker(comment.body, CLAIM_PREFIX);
|
|
108
|
+
const expiry = parsed ? Date.parse(parsed.expiresAt) : Number.NaN;
|
|
109
|
+
if (!parsed || parsed.idempotencyKey !== key || !Number.isFinite(expiry) || expiry <= now)
|
|
110
|
+
return [];
|
|
111
|
+
return [{ comment, marker: parsed }];
|
|
112
|
+
}).sort((left, right) => left.comment.id - right.comment.id);
|
|
113
|
+
}
|
|
114
|
+
async function isInside(file, roots) {
|
|
115
|
+
let resolved;
|
|
116
|
+
try {
|
|
117
|
+
resolved = await fs.realpath(file);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const canonicalRoots = await Promise.all(roots.map(root => fs.realpath(root)));
|
|
123
|
+
return canonicalRoots.some(root => {
|
|
124
|
+
const relation = path.relative(root, resolved);
|
|
125
|
+
return relation === '' || (!relation.startsWith('..') && !path.isAbsolute(relation));
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async function validateResult(flow, key, finalMessage) {
|
|
129
|
+
let value;
|
|
130
|
+
try {
|
|
131
|
+
value = JSON.parse(finalMessage);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new Error('executor final response is not JSON');
|
|
135
|
+
}
|
|
136
|
+
if (value.idempotencyKey !== key)
|
|
137
|
+
throw new Error('executor returned a different idempotency key');
|
|
138
|
+
if (typeof value.externalResultUrl !== 'string')
|
|
139
|
+
throw new Error('externalResultUrl is required');
|
|
140
|
+
validateExternalUrl(flow, value.externalResultUrl);
|
|
141
|
+
if (typeof value.account !== 'string' || !flow.spec.security.allowedAccounts.includes(value.account)) {
|
|
142
|
+
throw new Error('result account is not allow-listed');
|
|
143
|
+
}
|
|
144
|
+
if (typeof value.verification !== 'string' || !value.verification.trim())
|
|
145
|
+
throw new Error('verification evidence is required');
|
|
146
|
+
const attachments = value.attachmentPaths ?? [];
|
|
147
|
+
if (!Array.isArray(attachments) || !attachments.every(item => typeof item === 'string')) {
|
|
148
|
+
throw new Error('attachmentPaths must be an array of paths');
|
|
149
|
+
}
|
|
150
|
+
const approvedAttachments = await Promise.all(attachments.map(item => isInside(item, flow.spec.security.approvedAttachmentRoots)));
|
|
151
|
+
if (!approvedAttachments.every(Boolean)) {
|
|
152
|
+
throw new Error('result references an attachment outside approved roots');
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
status: 'completed', idempotencyKey: key, externalResultUrl: value.externalResultUrl,
|
|
156
|
+
account: value.account, verification: value.verification, attachmentPaths: attachments,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async function delay(ms, signal) {
|
|
160
|
+
if (ms <= 0)
|
|
161
|
+
return;
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
const timer = setTimeout(resolve, ms);
|
|
164
|
+
signal?.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason); }, { once: true });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async function acquireLock(stateRoot, flow) {
|
|
168
|
+
const directory = path.join(stateRoot, 'locks');
|
|
169
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
170
|
+
const file = path.join(directory, `${flow.metadata.name}.lock`);
|
|
171
|
+
const create = async () => {
|
|
172
|
+
const handle = await fs.open(file, 'wx', 0o600);
|
|
173
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
|
|
174
|
+
await handle.close();
|
|
175
|
+
};
|
|
176
|
+
try {
|
|
177
|
+
await create();
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error.code !== 'EEXIST')
|
|
181
|
+
throw error;
|
|
182
|
+
let stale = false;
|
|
183
|
+
try {
|
|
184
|
+
const lock = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
185
|
+
const tooOld = !lock.startedAt || Date.now() - Date.parse(lock.startedAt) > (flow.spec.workItem.claimTtlSeconds ?? 900) * 1000;
|
|
186
|
+
let alive = false;
|
|
187
|
+
if (typeof lock.pid === 'number') {
|
|
188
|
+
try {
|
|
189
|
+
process.kill(lock.pid, 0);
|
|
190
|
+
alive = true;
|
|
191
|
+
}
|
|
192
|
+
catch { /* process is gone or inaccessible */ }
|
|
193
|
+
}
|
|
194
|
+
stale = tooOld && !alive;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
stale = true;
|
|
198
|
+
}
|
|
199
|
+
if (!stale)
|
|
200
|
+
throw new Error(`job ${flow.metadata.name} is already running on this host`);
|
|
201
|
+
await fs.unlink(file).catch(() => undefined);
|
|
202
|
+
await create();
|
|
203
|
+
}
|
|
204
|
+
return async () => { await fs.unlink(file).catch(() => undefined); };
|
|
205
|
+
}
|
|
206
|
+
async function readRecovery(stateRoot, key) {
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(await fs.readFile(path.join(stateRoot, 'results', `${key}.json`), 'utf8'));
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error.code === 'ENOENT')
|
|
212
|
+
return null;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function writeRecovery(stateRoot, result) {
|
|
217
|
+
const directory = path.join(stateRoot, 'results');
|
|
218
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
219
|
+
const target = path.join(directory, `${result.idempotencyKey}.json`);
|
|
220
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
221
|
+
await fs.writeFile(temporary, `${JSON.stringify(result, null, 2)}\n`, { mode: 0o600 });
|
|
222
|
+
await fs.rename(temporary, target);
|
|
223
|
+
}
|
|
224
|
+
async function writeRunRecord(runDirectory, record) {
|
|
225
|
+
const target = path.join(runDirectory, 'run.json');
|
|
226
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
227
|
+
await fs.writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
|
|
228
|
+
await fs.rename(temporary, target);
|
|
229
|
+
}
|
|
230
|
+
export async function runExternalJob(options) {
|
|
231
|
+
const { flow, client, executor, signal } = options;
|
|
232
|
+
const now = options.now ?? Date.now;
|
|
233
|
+
const stateRoot = options.stateRoot ?? path.join(flow.spec.executor.workspace, '.aiwg', 'jobs');
|
|
234
|
+
if (path.resolve(stateRoot) === path.parse(path.resolve(stateRoot)).root)
|
|
235
|
+
throw new Error('job state root must not be a filesystem root');
|
|
236
|
+
const release = await acquireLock(stateRoot, flow);
|
|
237
|
+
try {
|
|
238
|
+
const actor = await client.currentUser(signal);
|
|
239
|
+
const requiredLabels = [...flow.spec.workItem.eligibleLabels];
|
|
240
|
+
if (approvalRequired(flow) && !requiredLabels.includes(approvalLabel(flow)))
|
|
241
|
+
requiredLabels.push(approvalLabel(flow));
|
|
242
|
+
const issues = await client.listOpenIssues(requiredLabels, signal);
|
|
243
|
+
let lostClaim = false;
|
|
244
|
+
for (const issue of issues) {
|
|
245
|
+
const key = jobKey(flow, issue.number);
|
|
246
|
+
let comments = await client.listComments(issue.number, signal);
|
|
247
|
+
const completed = completedBy(flow, comments, actor, key);
|
|
248
|
+
if (completed)
|
|
249
|
+
return { status: 'already-completed', issue: issue.number, idempotencyKey: key, externalResultUrl: completed.externalResultUrl };
|
|
250
|
+
const priorFailure = failedBy(comments, actor, key);
|
|
251
|
+
if (priorFailure)
|
|
252
|
+
return { status: 'failed-verification', issue: issue.number, idempotencyKey: key, message: priorFailure.reason };
|
|
253
|
+
const recovered = await readRecovery(stateRoot, key);
|
|
254
|
+
if (recovered?.status === 'completed') {
|
|
255
|
+
const verifiedRecovery = { ...await validateResult(flow, key, JSON.stringify(recovered)), issue: issue.number };
|
|
256
|
+
await client.addComment(issue.number, completionBody(flow, verifiedRecovery), signal);
|
|
257
|
+
return { ...verifiedRecovery, status: 'already-completed' };
|
|
258
|
+
}
|
|
259
|
+
if (recovered?.status === 'failed-verification')
|
|
260
|
+
return { ...recovered, issue: issue.number };
|
|
261
|
+
if (activeClaims(comments, actor, key, now()).length > 0)
|
|
262
|
+
continue;
|
|
263
|
+
const runnerId = options.runnerId ?? randomUUID();
|
|
264
|
+
const ttl = flow.spec.workItem.claimTtlSeconds ?? 900;
|
|
265
|
+
const expiresAt = new Date(now() + ttl * 1000).toISOString();
|
|
266
|
+
const own = await client.addComment(issue.number, claimBody(flow, issue, runnerId, expiresAt), signal);
|
|
267
|
+
await delay(flow.spec.workItem.claimSettleMs ?? 1000, signal);
|
|
268
|
+
comments = await client.listComments(issue.number, signal);
|
|
269
|
+
const duringClaim = completedBy(flow, comments, actor, key);
|
|
270
|
+
if (duringClaim)
|
|
271
|
+
return { status: 'already-completed', issue: issue.number, idempotencyKey: key, externalResultUrl: duringClaim.externalResultUrl };
|
|
272
|
+
const winner = activeClaims(comments, actor, key, now())[0];
|
|
273
|
+
if (!winner || winner.comment.id !== own.id) {
|
|
274
|
+
lostClaim = true;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const stillEligible = (await client.listOpenIssues(requiredLabels, signal)).some(candidate => candidate.number === issue.number);
|
|
278
|
+
if (!stillEligible) {
|
|
279
|
+
lostClaim = true;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const runDirectory = path.join(stateRoot, 'runs', `${key}-${Date.now()}`);
|
|
283
|
+
await fs.mkdir(runDirectory, { recursive: true, mode: 0o700 });
|
|
284
|
+
const prompt = await fs.readFile(resolveWorkspaceFile(flow, flow.spec.executor.prompt), 'utf8');
|
|
285
|
+
const startedAt = new Date().toISOString();
|
|
286
|
+
const execution = await executor.execute({ flow, prompt, issue, idempotencyKey: key, runDirectory, signal });
|
|
287
|
+
if (execution.exitCode !== 0) {
|
|
288
|
+
const message = `executor exited with status ${execution.exitCode}`;
|
|
289
|
+
await writeRunRecord(runDirectory, { job: flow.metadata.name, revision: flow.metadata.revision, issue: issue.number, idempotencyKey: key, startedAt, finishedAt: new Date().toISOString(), exitStatus: execution.exitCode, status: 'failed-verification', message });
|
|
290
|
+
await writeRecovery(stateRoot, { status: 'failed-verification', issue: issue.number, idempotencyKey: key, message });
|
|
291
|
+
await client.addComment(issue.number, failureBody(flow, key, message), signal);
|
|
292
|
+
return { status: 'failed-verification', issue: issue.number, idempotencyKey: key, message };
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const result = { ...await validateResult(flow, key, execution.finalMessage), issue: issue.number };
|
|
296
|
+
await writeRunRecord(runDirectory, { job: flow.metadata.name, revision: flow.metadata.revision, issue: issue.number, idempotencyKey: key, startedAt, finishedAt: new Date().toISOString(), exitStatus: execution.exitCode, status: result.status, externalResultUrl: result.externalResultUrl });
|
|
297
|
+
await writeRecovery(stateRoot, result);
|
|
298
|
+
await client.addComment(issue.number, completionBody(flow, result), signal);
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
303
|
+
await writeRunRecord(runDirectory, { job: flow.metadata.name, revision: flow.metadata.revision, issue: issue.number, idempotencyKey: key, startedAt, finishedAt: new Date().toISOString(), exitStatus: execution.exitCode, status: 'failed-verification', message });
|
|
304
|
+
await writeRecovery(stateRoot, { status: 'failed-verification', issue: issue.number, idempotencyKey: key, message });
|
|
305
|
+
await client.addComment(issue.number, failureBody(flow, key, message), signal);
|
|
306
|
+
return { status: 'failed-verification', issue: issue.number, idempotencyKey: key, message };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { status: lostClaim ? 'claim-lost' : 'no-eligible-work' };
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
await release();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -13,6 +13,9 @@ import { resolve, dirname } from 'path';
|
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import { load as loadYaml } from 'js-yaml';
|
|
15
15
|
import { findPackageRoot } from '../cli/find-package-root.js';
|
|
16
|
+
export function isExternalStrategy(strategy) {
|
|
17
|
+
return strategy === 'external-trigger';
|
|
18
|
+
}
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
17
20
|
// Loader
|
|
18
21
|
// ---------------------------------------------------------------------------
|
|
@@ -257,6 +260,8 @@ export function formatCapabilityTable() {
|
|
|
257
260
|
if (caps.native_features[f])
|
|
258
261
|
return padRight('NATIVE', 15);
|
|
259
262
|
const emu = caps.emulation[f];
|
|
263
|
+
if (isExternalStrategy(emu))
|
|
264
|
+
return padRight('EXTERNAL', 15);
|
|
260
265
|
if (emu)
|
|
261
266
|
return padRight(emu, 15);
|
|
262
267
|
return padRight('--', 15);
|
|
@@ -264,7 +269,7 @@ export function formatCapabilityTable() {
|
|
|
264
269
|
lines.push([padRight(caps.display_name, 18), ...cells].join('| '));
|
|
265
270
|
}
|
|
266
271
|
lines.push('');
|
|
267
|
-
lines.push('Legend: NATIVE = built-in support, aiwg-* = AIWG emulation, -- = unsupported');
|
|
272
|
+
lines.push('Legend: NATIVE = built-in support, aiwg-* = AIWG emulation, EXTERNAL = host/CI owns the trigger, -- = unsupported');
|
|
268
273
|
return lines.join('\n');
|
|
269
274
|
}
|
|
270
275
|
/**
|
|
@@ -287,9 +292,11 @@ export function formatFeatureSupport(feature) {
|
|
|
287
292
|
const strategy = caps.emulation[feature];
|
|
288
293
|
const status = native
|
|
289
294
|
? 'NATIVE'
|
|
290
|
-
: strategy
|
|
291
|
-
?
|
|
292
|
-
:
|
|
295
|
+
: isExternalStrategy(strategy)
|
|
296
|
+
? 'external trigger'
|
|
297
|
+
: strategy
|
|
298
|
+
? `emulated (${strategy})`
|
|
299
|
+
: 'unsupported';
|
|
293
300
|
lines.push(` ${padRight(caps.display_name, 18)} ${status}`);
|
|
294
301
|
}
|
|
295
302
|
return lines.join('\n');
|
|
@@ -16,8 +16,8 @@ providers:
|
|
|
16
16
|
claude-code:
|
|
17
17
|
display_name: Claude Code
|
|
18
18
|
status: stable
|
|
19
|
-
daemon_tier:
|
|
20
|
-
daemon_pty_adapter:
|
|
19
|
+
daemon_tier: unsupported
|
|
20
|
+
daemon_pty_adapter: false
|
|
21
21
|
artifact_paths:
|
|
22
22
|
agents: .claude/agents/
|
|
23
23
|
commands: .claude/commands/
|
|
@@ -31,7 +31,7 @@ providers:
|
|
|
31
31
|
mcp: true
|
|
32
32
|
behaviors: false
|
|
33
33
|
mission_control: false
|
|
34
|
-
daemon:
|
|
34
|
+
daemon: false
|
|
35
35
|
emulation:
|
|
36
36
|
cron: native
|
|
37
37
|
agent_teams: native
|
|
@@ -39,7 +39,7 @@ providers:
|
|
|
39
39
|
mcp: native
|
|
40
40
|
behaviors: hooks
|
|
41
41
|
mission_control: aiwg-mc
|
|
42
|
-
daemon:
|
|
42
|
+
daemon: null
|
|
43
43
|
hook_wiring:
|
|
44
44
|
at_link_support: true
|
|
45
45
|
hook_file: AIWG.md
|
|
@@ -80,8 +80,8 @@ providers:
|
|
|
80
80
|
aliases:
|
|
81
81
|
- openai
|
|
82
82
|
status: stable
|
|
83
|
-
daemon_tier:
|
|
84
|
-
daemon_pty_adapter:
|
|
83
|
+
daemon_tier: unsupported
|
|
84
|
+
daemon_pty_adapter: false
|
|
85
85
|
artifact_paths:
|
|
86
86
|
agents: .codex/agents/
|
|
87
87
|
commands: "~/.codex/prompts/"
|
|
@@ -100,15 +100,15 @@ providers:
|
|
|
100
100
|
mcp: false
|
|
101
101
|
behaviors: false
|
|
102
102
|
mission_control: false
|
|
103
|
-
daemon:
|
|
103
|
+
daemon: false
|
|
104
104
|
emulation:
|
|
105
|
-
cron:
|
|
105
|
+
cron: external-trigger
|
|
106
106
|
agent_teams: aiwg-mc
|
|
107
107
|
tasks: aiwg-mc
|
|
108
108
|
mcp: null
|
|
109
109
|
behaviors: aiwg-mc
|
|
110
110
|
mission_control: aiwg-mc
|
|
111
|
-
daemon:
|
|
111
|
+
daemon: null
|
|
112
112
|
hook_wiring:
|
|
113
113
|
at_link_support: false
|
|
114
114
|
hook_file: AIWG-codex.md
|
|
@@ -164,7 +164,7 @@ providers:
|
|
|
164
164
|
mission_control: false
|
|
165
165
|
daemon: false
|
|
166
166
|
emulation:
|
|
167
|
-
cron:
|
|
167
|
+
cron: null
|
|
168
168
|
agent_teams: aiwg-mc
|
|
169
169
|
tasks: aiwg-mc
|
|
170
170
|
mcp: null
|
|
@@ -183,8 +183,8 @@ providers:
|
|
|
183
183
|
display_name: Factory AI
|
|
184
184
|
status: stable
|
|
185
185
|
# droid CLI runs headless via `droid exec`; AIWG daemon provides persistence layer
|
|
186
|
-
daemon_tier:
|
|
187
|
-
daemon_pty_adapter:
|
|
186
|
+
daemon_tier: unsupported
|
|
187
|
+
daemon_pty_adapter: false
|
|
188
188
|
artifact_paths:
|
|
189
189
|
agents: .factory/droids/
|
|
190
190
|
commands: .factory/commands/
|
|
@@ -198,15 +198,15 @@ providers:
|
|
|
198
198
|
mcp: true # Native MCP support (stdio + HTTP transports)
|
|
199
199
|
behaviors: false
|
|
200
200
|
mission_control: true # Factory Missions maps to MC concept
|
|
201
|
-
daemon: false
|
|
201
|
+
daemon: false
|
|
202
202
|
emulation:
|
|
203
|
-
cron:
|
|
203
|
+
cron: null
|
|
204
204
|
agent_teams: null # native Missions
|
|
205
205
|
tasks: null # native Task tool
|
|
206
206
|
mcp: null # native MCP
|
|
207
207
|
behaviors: aiwg-mc
|
|
208
208
|
mission_control: null # native Missions
|
|
209
|
-
daemon:
|
|
209
|
+
daemon: null
|
|
210
210
|
hook_wiring:
|
|
211
211
|
at_link_support: false
|
|
212
212
|
hook_file: AIWG-factory.md
|
|
@@ -236,7 +236,7 @@ providers:
|
|
|
236
236
|
mission_control: false
|
|
237
237
|
daemon: false
|
|
238
238
|
emulation:
|
|
239
|
-
cron:
|
|
239
|
+
cron: null
|
|
240
240
|
agent_teams: aiwg-mc
|
|
241
241
|
tasks: aiwg-mc
|
|
242
242
|
mcp: null
|
|
@@ -254,7 +254,7 @@ providers:
|
|
|
254
254
|
opencode:
|
|
255
255
|
display_name: OpenCode
|
|
256
256
|
status: stable
|
|
257
|
-
daemon_tier:
|
|
257
|
+
daemon_tier: unsupported
|
|
258
258
|
daemon_pty_adapter: false
|
|
259
259
|
artifact_paths:
|
|
260
260
|
agents: .opencode/agent/
|
|
@@ -269,15 +269,15 @@ providers:
|
|
|
269
269
|
mcp: false
|
|
270
270
|
behaviors: false
|
|
271
271
|
mission_control: false
|
|
272
|
-
daemon:
|
|
272
|
+
daemon: false
|
|
273
273
|
emulation:
|
|
274
|
-
cron:
|
|
274
|
+
cron: null
|
|
275
275
|
agent_teams: aiwg-mc
|
|
276
276
|
tasks: aiwg-mc
|
|
277
277
|
mcp: null
|
|
278
278
|
behaviors: aiwg-mc
|
|
279
279
|
mission_control: aiwg-mc
|
|
280
|
-
daemon:
|
|
280
|
+
daemon: null
|
|
281
281
|
hook_wiring:
|
|
282
282
|
at_link_support: true
|
|
283
283
|
hook_file: AIWG-opencode.md
|
|
@@ -289,7 +289,7 @@ providers:
|
|
|
289
289
|
warp:
|
|
290
290
|
display_name: Warp Terminal
|
|
291
291
|
status: stable
|
|
292
|
-
daemon_tier:
|
|
292
|
+
daemon_tier: unsupported
|
|
293
293
|
daemon_pty_adapter: false
|
|
294
294
|
artifact_paths:
|
|
295
295
|
agents: .warp/agents/
|
|
@@ -305,15 +305,15 @@ providers:
|
|
|
305
305
|
mcp: false
|
|
306
306
|
behaviors: false
|
|
307
307
|
mission_control: false
|
|
308
|
-
daemon:
|
|
308
|
+
daemon: false
|
|
309
309
|
emulation:
|
|
310
|
-
cron:
|
|
310
|
+
cron: null
|
|
311
311
|
agent_teams: aiwg-mc
|
|
312
312
|
tasks: aiwg-mc
|
|
313
313
|
mcp: null
|
|
314
314
|
behaviors: aiwg-mc
|
|
315
315
|
mission_control: aiwg-mc
|
|
316
|
-
daemon:
|
|
316
|
+
daemon: null
|
|
317
317
|
hook_wiring:
|
|
318
318
|
at_link_support: true
|
|
319
319
|
hook_file: AIWG-warp.md
|
|
@@ -344,7 +344,7 @@ providers:
|
|
|
344
344
|
mission_control: false
|
|
345
345
|
daemon: false
|
|
346
346
|
emulation:
|
|
347
|
-
cron:
|
|
347
|
+
cron: null
|
|
348
348
|
agent_teams: aiwg-mc
|
|
349
349
|
tasks: aiwg-mc
|
|
350
350
|
mcp: null
|
|
@@ -362,7 +362,7 @@ providers:
|
|
|
362
362
|
hermes:
|
|
363
363
|
display_name: Hermes
|
|
364
364
|
status: experimental
|
|
365
|
-
daemon_tier:
|
|
365
|
+
daemon_tier: unsupported
|
|
366
366
|
daemon_pty_adapter: false
|
|
367
367
|
artifact_paths:
|
|
368
368
|
# MCP is OPTIONAL for Hermes (validated v2026.5.13, #1527): it integrates
|
|
@@ -382,15 +382,15 @@ providers:
|
|
|
382
382
|
mcp: true
|
|
383
383
|
behaviors: false
|
|
384
384
|
mission_control: false
|
|
385
|
-
daemon:
|
|
385
|
+
daemon: false
|
|
386
386
|
emulation:
|
|
387
|
-
cron:
|
|
387
|
+
cron: null
|
|
388
388
|
agent_teams: aiwg-mc
|
|
389
389
|
tasks: aiwg-mc
|
|
390
390
|
mcp: native
|
|
391
391
|
behaviors: aiwg-mc
|
|
392
392
|
mission_control: aiwg-mc
|
|
393
|
-
daemon:
|
|
393
|
+
daemon: null
|
|
394
394
|
hook_wiring:
|
|
395
395
|
at_link_support: false
|
|
396
396
|
hook_file: .hermes.md
|
|
@@ -402,7 +402,7 @@ providers:
|
|
|
402
402
|
openclaw:
|
|
403
403
|
display_name: OpenClaw
|
|
404
404
|
status: stable
|
|
405
|
-
daemon_tier:
|
|
405
|
+
daemon_tier: unsupported
|
|
406
406
|
daemon_pty_adapter: false
|
|
407
407
|
artifact_paths:
|
|
408
408
|
agents: "~/.openclaw/agents/"
|
|
@@ -417,15 +417,15 @@ providers:
|
|
|
417
417
|
mcp: true
|
|
418
418
|
behaviors: true
|
|
419
419
|
mission_control: false
|
|
420
|
-
daemon:
|
|
420
|
+
daemon: false
|
|
421
421
|
emulation:
|
|
422
|
-
cron:
|
|
422
|
+
cron: null
|
|
423
423
|
agent_teams: aiwg-mc
|
|
424
424
|
tasks: aiwg-mc
|
|
425
425
|
mcp: native
|
|
426
426
|
behaviors: native
|
|
427
427
|
mission_control: aiwg-mc
|
|
428
|
-
daemon:
|
|
428
|
+
daemon: null
|
|
429
429
|
hook_wiring:
|
|
430
430
|
at_link_support: false
|
|
431
431
|
context_file: "~/.openclaw/config.yaml"
|
|
@@ -457,7 +457,7 @@ providers:
|
|
|
457
457
|
mission_control: false
|
|
458
458
|
daemon: false
|
|
459
459
|
emulation:
|
|
460
|
-
cron:
|
|
460
|
+
cron: null
|
|
461
461
|
agent_teams: aiwg-mc
|
|
462
462
|
tasks: aiwg-mc
|
|
463
463
|
mcp: null
|
|
@@ -477,7 +477,7 @@ features:
|
|
|
477
477
|
description: Scheduled task execution (recurring triggers)
|
|
478
478
|
native_example: Claude Code CronCreate/CronList/CronDelete tools
|
|
479
479
|
emulation_strategies:
|
|
480
|
-
|
|
480
|
+
external-trigger: System cron, systemd timers, or CI own time and launch a reviewed provider command
|
|
481
481
|
agent_teams:
|
|
482
482
|
description: Multi-agent orchestration with team coordination
|
|
483
483
|
native_example: Claude Code native agent teams
|
|
@@ -505,10 +505,7 @@ features:
|
|
|
505
505
|
aiwg-mc: AIWG mc start/dispatch/status/watch CLI
|
|
506
506
|
daemon:
|
|
507
507
|
description: >
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
native_example: aiwg daemon start (Claude Code, OpenCode, Warp, OpenClaw, Codex)
|
|
513
|
-
emulation_strategies:
|
|
514
|
-
pty-adapter: node-pty bridge for TUI platforms (Claude Code, Codex secondary mode)
|
|
508
|
+
Resident AIWG daemon lifecycle. The current production CLI does not expose
|
|
509
|
+
a daemon command; bundled daemon sources are non-routable development artifacts.
|
|
510
|
+
native_example: null
|
|
511
|
+
emulation_strategies: {}
|