@jstn-sdk/ma 0.14.0 → 0.14.1
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/COVERAGE.md +9 -9
- package/DEMO.md +4 -4
- package/README.md +80 -70
- package/bin/ma.js +6 -8
- package/docs/README.md +2 -2
- package/docs/assets/banner.png +0 -0
- package/docs/getting-started.md +9 -9
- package/docs/qa/{release-issue-gates-0.14.0.json → release-issue-gates-0.14.1.json} +77 -77
- package/docs/qa/{release-readiness-0.14.0.md → release-readiness-0.14.1.md} +12 -12
- package/docs/release-spec.md +12 -12
- package/docs/vendor-plugin-publishing.md +49 -0
- package/mcp/local/code-intel.js +9 -3
- package/package.json +12 -3
- package/plugins/meta-architect/.app.json +1 -1
- package/plugins/meta-architect/.claude-plugin/plugin.json +12 -0
- package/plugins/meta-architect/.codex-plugin/plugin.json +1 -1
- package/plugins/meta-architect/.mcp.json +1 -1
- package/plugins/meta-architect/README.md +13 -1
- package/plugins/meta-architect/obsidian/manifest.json +1 -1
- package/plugins/meta-architect/skills/maestro/SKILL.md +17 -1
- package/plugins/meta-architect/skills/maestro/references/core-release-rules.md +2 -2
- package/schemas/autonomous-task-queue.schema.json +2 -0
- package/scripts/install.sh +1 -1
- package/scripts/install.sh.sha256 +1 -1
- package/scripts/plugin-build.js +314 -0
- package/scripts/plugin-publish.js +77 -0
- package/scripts/release-sync.js +11 -0
- package/scripts/release-verify.js +35 -4
- package/skills/maestro/SKILL.md +17 -1
- package/skills/maestro/references/core-release-rules.md +2 -2
- package/sprint/07-release.md +2 -2
- package/src/agents.js +4 -2
- package/src/bootstrap.js +20 -2
- package/src/codex-app-server.js +4 -4
- package/src/launcher.js +2 -2
- package/src/mcp-live-client.js +2 -2
- package/src/prelaunch.js +34 -0
- package/src/process-utils.js +62 -0
- package/src/quality/ai-quality-orchestrator.js +2 -2
- package/src/release-operations.js +14 -4
- package/src/runtime/architect-review.js +2 -2
- package/src/runtime/autonomous-tasks.js +220 -43
- package/src/runtime/codeburn-core.js +9 -3
- package/src/runtime/core-source-ingest.js +2 -2
- package/src/runtime/detached-provider.js +17 -8
- package/src/runtime/live-agent-verification.js +2 -2
- package/src/runtime/project-context.js +2 -2
- package/src/runtime/skills-registry-export.js +7 -3
- package/src/skills.js +12 -12
- package/src/test-fixtures.js +4 -4
- package/support-bundle.json +1 -1
- package/scripts/postinstall.js +0 -30
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { Worker } from "node:worker_threads";
|
|
4
5
|
import { getAgentInvocation } from "../agents.js";
|
|
5
6
|
import { readJson, withRuntimeStateLock, writeFileIfMissing, writeJson } from "../fs-utils.js";
|
|
6
7
|
import { getRuntimeSubsystemPath } from "../paths.js";
|
|
@@ -16,11 +17,16 @@ import { createTaskContract, validateTaskContract } from "./task-contracts.js";
|
|
|
16
17
|
export const autonomousTaskSchemaVersion = "0.1.0";
|
|
17
18
|
const statuses = new Set(["queued", "running", "completed", "failed", "blocked", "cancelled"]);
|
|
18
19
|
const priorities = new Set(["low", "normal", "high", "critical"]);
|
|
20
|
+
const runnerLeaseMaxAgeMs = 30_000;
|
|
19
21
|
|
|
20
22
|
export function getAutonomousTaskQueuePath() {
|
|
21
23
|
return getRuntimeSubsystemPath("tasks", "autonomous-queue.json");
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
export function getRunnerLeasePath(runnerId) {
|
|
27
|
+
return getRuntimeSubsystemPath("tasks", "leases", `${encodeURIComponent(runnerId)}.json`);
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
export function createTaskQueue() {
|
|
25
31
|
return {
|
|
26
32
|
schemaVersion: autonomousTaskSchemaVersion,
|
|
@@ -87,6 +93,8 @@ function normalizeTask(input) {
|
|
|
87
93
|
completedAt: input?.completedAt ?? null,
|
|
88
94
|
error: input?.error ?? null,
|
|
89
95
|
blocker: input?.blocker ?? null,
|
|
96
|
+
runnerPid: Number.isInteger(input?.runnerPid) ? input.runnerPid : null,
|
|
97
|
+
runnerId: typeof input?.runnerId === "string" ? input.runnerId : null,
|
|
90
98
|
evidence: Array.isArray(input?.evidence) ? [...input.evidence] : [],
|
|
91
99
|
selectedCapabilities: Array.isArray(input?.selectedCapabilities)
|
|
92
100
|
? input.selectedCapabilities
|
|
@@ -136,15 +144,40 @@ function byId(tasks) {
|
|
|
136
144
|
return new Map(tasks.map((task) => [task.id, task]));
|
|
137
145
|
}
|
|
138
146
|
|
|
147
|
+
async function runnerIsAlive(task) {
|
|
148
|
+
if (!Number.isInteger(task.runnerPid) || task.runnerPid < 1 || !task.runnerId) return false;
|
|
149
|
+
try {
|
|
150
|
+
const lease = await readJson(getRunnerLeasePath(task.runnerId));
|
|
151
|
+
const heartbeatAt = Date.parse(lease.heartbeatAt);
|
|
152
|
+
if (
|
|
153
|
+
lease.runnerId !== task.runnerId ||
|
|
154
|
+
lease.runnerPid !== task.runnerPid ||
|
|
155
|
+
!Number.isFinite(heartbeatAt) ||
|
|
156
|
+
Date.now() - heartbeatAt > runnerLeaseMaxAgeMs
|
|
157
|
+
) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
process.kill(task.runnerPid, 0);
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
139
167
|
async function loadTaskQueueUnlocked() {
|
|
140
168
|
try {
|
|
141
169
|
const queue = validateTaskQueue(await readJson(getAutonomousTaskQueuePath()));
|
|
142
|
-
const interrupted =
|
|
170
|
+
const interrupted = [];
|
|
171
|
+
for (const task of queue.tasks) {
|
|
172
|
+
if (task.status === "running" && !(await runnerIsAlive(task))) interrupted.push(task);
|
|
173
|
+
}
|
|
143
174
|
if (interrupted.length === 0) return queue;
|
|
144
175
|
const recoveredAt = new Date().toISOString();
|
|
145
176
|
for (const task of interrupted) {
|
|
146
177
|
task.status = "queued";
|
|
147
178
|
task.error = "Recovered after the previous process exited while the task was running";
|
|
179
|
+
task.runnerPid = null;
|
|
180
|
+
task.runnerId = null;
|
|
148
181
|
task.updatedAt = recoveredAt;
|
|
149
182
|
}
|
|
150
183
|
await writeJsonAtomically(getAutonomousTaskQueuePath(), queue);
|
|
@@ -164,6 +197,80 @@ export async function saveTaskQueue(queue) {
|
|
|
164
197
|
return writeJson(getAutonomousTaskQueuePath(), validateTaskQueue(queue));
|
|
165
198
|
}
|
|
166
199
|
|
|
200
|
+
async function updateTaskQueueTask(taskId, update) {
|
|
201
|
+
return withRuntimeStateLock(
|
|
202
|
+
getAutonomousTaskQueuePath(),
|
|
203
|
+
async () => {
|
|
204
|
+
const queue = await loadTaskQueueUnlocked();
|
|
205
|
+
const task = queue.tasks.find((entry) => entry.id === taskId);
|
|
206
|
+
if (!task) return null;
|
|
207
|
+
const changed = (await update(task, queue)) !== false;
|
|
208
|
+
if (changed) {
|
|
209
|
+
task.updatedAt = new Date().toISOString();
|
|
210
|
+
await writeJsonAtomically(getAutonomousTaskQueuePath(), validateTaskQueue(queue));
|
|
211
|
+
}
|
|
212
|
+
return { task, changed };
|
|
213
|
+
},
|
|
214
|
+
{ wait: true },
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function createRunnerLease(runnerId) {
|
|
219
|
+
await writeJsonAtomically(getRunnerLeasePath(runnerId), {
|
|
220
|
+
schemaVersion: autonomousTaskSchemaVersion,
|
|
221
|
+
runnerId,
|
|
222
|
+
runnerPid: process.pid,
|
|
223
|
+
heartbeatAt: new Date().toISOString(),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function startRunnerLeaseHeartbeat(runnerId) {
|
|
228
|
+
const leasePath = getRunnerLeasePath(runnerId);
|
|
229
|
+
const worker = new Worker(
|
|
230
|
+
`const { parentPort, workerData } = require("node:worker_threads");
|
|
231
|
+
const fs = require("node:fs");
|
|
232
|
+
const path = require("node:path");
|
|
233
|
+
let active = true;
|
|
234
|
+
const beat = () => {
|
|
235
|
+
if (!active) return;
|
|
236
|
+
const temporary = workerData.leasePath + ".heartbeat-" + process.pid;
|
|
237
|
+
fs.writeFile(temporary, JSON.stringify({
|
|
238
|
+
schemaVersion: workerData.schemaVersion,
|
|
239
|
+
runnerId: workerData.runnerId,
|
|
240
|
+
runnerPid: workerData.runnerPid,
|
|
241
|
+
heartbeatAt: new Date().toISOString()
|
|
242
|
+
}) + "\\n", { mode: 0o600 }, (writeError) => {
|
|
243
|
+
if (writeError) return;
|
|
244
|
+
fs.rename(temporary, workerData.leasePath, () => {});
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
const timer = setInterval(beat, workerData.intervalMs);
|
|
248
|
+
timer.unref();
|
|
249
|
+
parentPort.on("message", (message) => {
|
|
250
|
+
if (message === "stop") {
|
|
251
|
+
active = false;
|
|
252
|
+
clearInterval(timer);
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
});`,
|
|
256
|
+
{
|
|
257
|
+
eval: true,
|
|
258
|
+
workerData: {
|
|
259
|
+
leasePath,
|
|
260
|
+
runnerId,
|
|
261
|
+
runnerPid: process.pid,
|
|
262
|
+
schemaVersion: autonomousTaskSchemaVersion,
|
|
263
|
+
intervalMs: runnerLeaseMaxAgeMs / 3,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
worker.on("error", () => {});
|
|
268
|
+
return async () => {
|
|
269
|
+
worker.postMessage("stop");
|
|
270
|
+
await worker.terminate();
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
167
274
|
export async function enqueueAutonomousTasks(inputs) {
|
|
168
275
|
return withRuntimeStateLock(
|
|
169
276
|
getAutonomousTaskQueuePath(),
|
|
@@ -227,7 +334,7 @@ export async function runAutonomousTasks({
|
|
|
227
334
|
if (!Number.isInteger(concurrency) || concurrency < 1)
|
|
228
335
|
throw new Error("concurrency must be a positive integer");
|
|
229
336
|
const queue = await loadTaskQueue();
|
|
230
|
-
const
|
|
337
|
+
const taskById = new Map(queue.tasks.map((task) => [task.id, task]));
|
|
231
338
|
const capabilities = createDefaultEnvironmentAwarenessCore({
|
|
232
339
|
capabilities: await discoverEnvironmentCapabilities({ cwd, includeGlobal }),
|
|
233
340
|
});
|
|
@@ -269,7 +376,7 @@ export async function runAutonomousTasks({
|
|
|
269
376
|
const now = Date.now();
|
|
270
377
|
const eligible = queue.tasks
|
|
271
378
|
.filter((task) => task.status === "queued" && !running.has(task.id))
|
|
272
|
-
.filter((task) => !isExpired(task, now) && dependenciesReady(task,
|
|
379
|
+
.filter((task) => !isExpired(task, now) && dependenciesReady(task, taskById))
|
|
273
380
|
.sort(
|
|
274
381
|
(a, b) =>
|
|
275
382
|
(priorities.has(b.priority)
|
|
@@ -289,40 +396,103 @@ export async function runAutonomousTasks({
|
|
|
289
396
|
).selected;
|
|
290
397
|
task.invocation = task.vendor ? getAgentInvocation(task.vendor, "maestro") : null;
|
|
291
398
|
if (hasUnsafeAction(task)) {
|
|
292
|
-
task.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
399
|
+
const transition = await updateTaskQueueTask(task.id, (current) => {
|
|
400
|
+
if (current.status !== "queued") return false;
|
|
401
|
+
current.status = "blocked";
|
|
402
|
+
current.blocker =
|
|
403
|
+
"Explicit approval required for destructive, production, credential, or external mutation task";
|
|
404
|
+
return true;
|
|
405
|
+
});
|
|
406
|
+
if (transition?.changed) {
|
|
407
|
+
Object.assign(task, transition.task);
|
|
408
|
+
await emit({ taskId: task.id, status: task.status, blocker: task.blocker });
|
|
409
|
+
}
|
|
297
410
|
processed++;
|
|
298
411
|
continue;
|
|
299
412
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
413
|
+
const runnerId = randomUUID();
|
|
414
|
+
try {
|
|
415
|
+
await createRunnerLease(runnerId);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
await emit({
|
|
418
|
+
taskId: task.id,
|
|
419
|
+
status: "queued",
|
|
420
|
+
error: `Unable to establish task runner lease: ${error.message}`,
|
|
421
|
+
});
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const claim = await updateTaskQueueTask(task.id, (current) => {
|
|
425
|
+
if (current.status !== "queued") return false;
|
|
426
|
+
current.status = "running";
|
|
427
|
+
current.attempts++;
|
|
428
|
+
current.startedAt ??= new Date().toISOString();
|
|
429
|
+
current.runnerPid = process.pid;
|
|
430
|
+
current.runnerId = runnerId;
|
|
431
|
+
current.selectedCapabilities = task.selectedCapabilities;
|
|
432
|
+
current.invocation = task.invocation;
|
|
433
|
+
return true;
|
|
434
|
+
});
|
|
435
|
+
if (!claim?.changed) {
|
|
436
|
+
await fs.rm(getRunnerLeasePath(runnerId), { force: true });
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
Object.assign(task, claim.task);
|
|
304
440
|
const promise = (async () => {
|
|
441
|
+
const stopHeartbeat = startRunnerLeaseHeartbeat(runnerId);
|
|
305
442
|
try {
|
|
306
443
|
const result = await runner(task);
|
|
444
|
+
const patch = { runnerPid: null, runnerId: null };
|
|
307
445
|
if (result?.status === "blocked") {
|
|
308
|
-
|
|
309
|
-
|
|
446
|
+
Object.assign(patch, {
|
|
447
|
+
status: "blocked",
|
|
448
|
+
blocker: result.reason ?? "Runner reported a blocker",
|
|
449
|
+
});
|
|
310
450
|
} else if (result?.status === "failed" && task.attempts < task.maxAttempts) {
|
|
311
|
-
|
|
312
|
-
|
|
451
|
+
Object.assign(patch, {
|
|
452
|
+
status: "queued",
|
|
453
|
+
error: result.reason ?? "Runner failed; task queued for retry",
|
|
454
|
+
});
|
|
313
455
|
} else if (result?.status === "failed") {
|
|
314
|
-
|
|
315
|
-
|
|
456
|
+
Object.assign(patch, {
|
|
457
|
+
status: "failed",
|
|
458
|
+
error: result.reason ?? "Runner failed after maximum attempts",
|
|
459
|
+
});
|
|
316
460
|
} else {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
461
|
+
Object.assign(patch, {
|
|
462
|
+
status: "completed",
|
|
463
|
+
completedAt: new Date().toISOString(),
|
|
464
|
+
evidence: [
|
|
465
|
+
...task.evidence,
|
|
466
|
+
...(Array.isArray(result?.evidence) ? result.evidence : []),
|
|
467
|
+
],
|
|
468
|
+
});
|
|
320
469
|
}
|
|
470
|
+
const transition = await updateTaskQueueTask(task.id, (current) => {
|
|
471
|
+
if (current.status === "cancelled") return false;
|
|
472
|
+
Object.assign(current, patch);
|
|
473
|
+
return true;
|
|
474
|
+
});
|
|
475
|
+
if (transition) Object.assign(task, transition.task);
|
|
321
476
|
} catch (error) {
|
|
322
477
|
task.error = error instanceof Error ? error.message : String(error);
|
|
323
478
|
task.status = task.attempts < task.maxAttempts ? "queued" : "failed";
|
|
479
|
+
task.runnerPid = null;
|
|
480
|
+
task.runnerId = null;
|
|
481
|
+
const transition = await updateTaskQueueTask(task.id, (current) => {
|
|
482
|
+
if (current.status === "cancelled") return false;
|
|
483
|
+
Object.assign(current, {
|
|
484
|
+
status: task.status,
|
|
485
|
+
error: task.error,
|
|
486
|
+
runnerPid: null,
|
|
487
|
+
runnerId: null,
|
|
488
|
+
});
|
|
489
|
+
return true;
|
|
490
|
+
});
|
|
491
|
+
if (transition) Object.assign(task, transition.task);
|
|
492
|
+
} finally {
|
|
493
|
+
await stopHeartbeat();
|
|
494
|
+
await fs.rm(getRunnerLeasePath(runnerId), { force: true });
|
|
324
495
|
}
|
|
325
|
-
task.updatedAt = new Date().toISOString();
|
|
326
496
|
await emit({
|
|
327
497
|
taskId: task.id,
|
|
328
498
|
status: task.status,
|
|
@@ -330,37 +500,44 @@ export async function runAutonomousTasks({
|
|
|
330
500
|
error: task.error,
|
|
331
501
|
blocker: task.blocker,
|
|
332
502
|
});
|
|
333
|
-
await saveTaskQueue(queue);
|
|
334
503
|
running.delete(promise);
|
|
335
504
|
processed++;
|
|
336
505
|
})();
|
|
337
506
|
running.add(promise);
|
|
338
|
-
await saveTaskQueue(queue);
|
|
339
507
|
}
|
|
340
508
|
}
|
|
341
509
|
await Promise.all(running);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
510
|
+
await withRuntimeStateLock(
|
|
511
|
+
getAutonomousTaskQueuePath(),
|
|
512
|
+
async () => {
|
|
513
|
+
const latestQueue = await loadTaskQueueUnlocked();
|
|
514
|
+
const latestById = byId(latestQueue.tasks);
|
|
515
|
+
for (const task of latestQueue.tasks) {
|
|
516
|
+
if (task.status !== "queued") continue;
|
|
517
|
+
const failedDependency = task.dependencies.find((id) =>
|
|
518
|
+
["failed", "blocked", "cancelled"].includes(latestById.get(id)?.status),
|
|
519
|
+
);
|
|
520
|
+
if (failedDependency) {
|
|
521
|
+
task.status = "blocked";
|
|
522
|
+
task.blocker = `Dependency ${failedDependency} is ${latestById.get(failedDependency).status}`;
|
|
523
|
+
} else if (isExpired(task, Date.now())) {
|
|
524
|
+
task.status = "blocked";
|
|
525
|
+
task.blocker = "Task deadline expired";
|
|
526
|
+
} else if (!dependenciesReady(task, latestById)) {
|
|
527
|
+
task.blocker = "Waiting for dependencies";
|
|
528
|
+
}
|
|
529
|
+
task.updatedAt = new Date().toISOString();
|
|
530
|
+
}
|
|
531
|
+
await writeJsonAtomically(getAutonomousTaskQueuePath(), validateTaskQueue(latestQueue));
|
|
532
|
+
},
|
|
533
|
+
{ wait: true },
|
|
534
|
+
);
|
|
535
|
+
const finalQueue = await loadTaskQueue();
|
|
359
536
|
return {
|
|
360
537
|
schemaVersion: autonomousTaskSchemaVersion,
|
|
361
|
-
tasks:
|
|
538
|
+
tasks: finalQueue.tasks,
|
|
362
539
|
events,
|
|
363
|
-
summary: summarizeTasks(
|
|
540
|
+
summary: summarizeTasks(finalQueue.tasks),
|
|
364
541
|
};
|
|
365
542
|
}
|
|
366
543
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { promisify } from "node:util";
|
|
3
2
|
import { ensureDir, readJson, writeFileIfMissing, writeJson } from "../fs-utils.js";
|
|
4
3
|
import { getRuntimeSubsystemPath } from "../paths.js";
|
|
4
|
+
import { assertSafeExecutable, safeExecFile } from "../process-utils.js";
|
|
5
5
|
|
|
6
|
-
const execFileAsync = promisify(
|
|
6
|
+
const execFileAsync = promisify(safeExecFile);
|
|
7
7
|
export const codeburnSchemaVersion = "0.1.0";
|
|
8
8
|
|
|
9
9
|
export function getCodeburnRoot() {
|
|
@@ -87,8 +87,14 @@ export async function syncCodeburnUsage({
|
|
|
87
87
|
const existing = await loadCodeburnUsage();
|
|
88
88
|
const executable = command ?? process.env.MA_CODEBURN_BIN ?? "npx";
|
|
89
89
|
const commandArgs = args ?? ["--yes", "codeburn", "usage", "--json"];
|
|
90
|
+
assertSafeExecutable(executable, "Codeburn executable");
|
|
90
91
|
try {
|
|
91
|
-
const result = await exec(executable, commandArgs, {
|
|
92
|
+
const result = await exec(executable, commandArgs, {
|
|
93
|
+
cwd,
|
|
94
|
+
encoding: "utf8",
|
|
95
|
+
timeout: 15_000,
|
|
96
|
+
shell: false,
|
|
97
|
+
});
|
|
92
98
|
const entries = parseCodeburnOutput(result.stdout ?? result);
|
|
93
99
|
const normalized = entries
|
|
94
100
|
.filter((entry) => entry && typeof entry === "object")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
2
|
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { ensureDir, readJson, writeFileIfMissing, writeJson } from "../fs-utils.js";
|
|
6
5
|
import { getRepoRoot, getRuntimeSubsystemPath, getRuntimeWritePath } from "../paths.js";
|
|
6
|
+
import { safeSpawn } from "../process-utils.js";
|
|
7
7
|
|
|
8
8
|
export const coreSourceIngestSchemaVersion = "0.1.0";
|
|
9
9
|
|
|
@@ -153,7 +153,7 @@ export async function loadCoreSourceIngest() {
|
|
|
153
153
|
export async function ingestCoreSources({
|
|
154
154
|
definitions = coreSourceDefinitions,
|
|
155
155
|
refresh = false,
|
|
156
|
-
spawnImpl =
|
|
156
|
+
spawnImpl = safeSpawn,
|
|
157
157
|
} = {}) {
|
|
158
158
|
await ensureDir(getCoreSourcesRootPath());
|
|
159
159
|
const sources = [];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { ensureDir } from "../fs-utils.js";
|
|
5
4
|
import { getRuntimeSubsystemPath } from "../paths.js";
|
|
5
|
+
import { assertSafeExecutable, safeSpawnSync } from "../process-utils.js";
|
|
6
6
|
|
|
7
7
|
export function getRuntimeLogsRoot() {
|
|
8
8
|
return getRuntimeSubsystemPath("logs");
|
|
@@ -11,7 +11,7 @@ export function getRuntimeLogsRoot() {
|
|
|
11
11
|
export function resolveDetachedProvider() {
|
|
12
12
|
const preferred = process.env.MA_DETACHED_PROVIDER ?? "";
|
|
13
13
|
if (preferred === "tmux" || process.env.MA_TMUX_PROVIDER === "1") {
|
|
14
|
-
const probe =
|
|
14
|
+
const probe = safeSpawnSync("tmux", ["ls"], {
|
|
15
15
|
encoding: "utf8",
|
|
16
16
|
env: buildDetachedEnv(),
|
|
17
17
|
});
|
|
@@ -24,6 +24,10 @@ export function resolveDetachedProvider() {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export async function launchDetachedTrack({ trackId, title, command, args = [] }) {
|
|
27
|
+
assertSafeExecutable(command, "Detached provider command");
|
|
28
|
+
if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string" || arg.includes("\0"))) {
|
|
29
|
+
throw new Error("Detached provider arguments must be strings without NUL bytes");
|
|
30
|
+
}
|
|
27
31
|
const provider = resolveDetachedProvider();
|
|
28
32
|
await ensureDir(getRuntimeLogsRoot());
|
|
29
33
|
const safeTrackId = sanitizeTrackId(trackId);
|
|
@@ -43,10 +47,15 @@ export async function launchDetachedTrack({ trackId, title, command, args = [] }
|
|
|
43
47
|
|
|
44
48
|
const sessionName = `ma_${safeTrackId}`;
|
|
45
49
|
await fs.writeFile(logPath, `provider=tmux\n${metadata}`, "utf8");
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
const tmuxCommand = [command, ...args].map(quoteForShell).join(" ");
|
|
51
|
+
const started = safeSpawnSync(
|
|
52
|
+
"tmux",
|
|
53
|
+
["new-session", "-d", "-s", sessionName, "--", tmuxCommand],
|
|
54
|
+
{
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
env: buildDetachedEnv(),
|
|
57
|
+
},
|
|
58
|
+
);
|
|
50
59
|
|
|
51
60
|
if (started.status !== 0) {
|
|
52
61
|
await fs.writeFile(
|
|
@@ -64,13 +73,13 @@ export async function launchDetachedTrack({ trackId, title, command, args = [] }
|
|
|
64
73
|
};
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
const piped =
|
|
76
|
+
const piped = safeSpawnSync(
|
|
68
77
|
"tmux",
|
|
69
78
|
["pipe-pane", "-t", sessionName, "-o", `cat >> ${quoteForShell(logPath)}`],
|
|
70
79
|
{ encoding: "utf8", env: buildDetachedEnv() },
|
|
71
80
|
);
|
|
72
81
|
if (piped.status !== 0) {
|
|
73
|
-
|
|
82
|
+
safeSpawnSync("tmux", ["kill-session", "-t", sessionName], {
|
|
74
83
|
encoding: "utf8",
|
|
75
84
|
env: buildDetachedEnv(),
|
|
76
85
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { agentRegistry as executableAgents, resolveAgentCommand } from "../agents.js";
|
|
5
|
+
import { safeSpawn } from "../process-utils.js";
|
|
6
6
|
import {
|
|
7
7
|
agentRegistry,
|
|
8
8
|
createSkillCompatibilityPayload,
|
|
@@ -13,7 +13,7 @@ const defaultTimeoutMs = 5000;
|
|
|
13
13
|
|
|
14
14
|
function runVersionProbe(command, timeoutMs, args = ["--version"]) {
|
|
15
15
|
return new Promise((resolve) => {
|
|
16
|
-
const child =
|
|
16
|
+
const child = safeSpawn(command, args, {
|
|
17
17
|
shell: false,
|
|
18
18
|
stdio: ["ignore", "pipe", "pipe"],
|
|
19
19
|
});
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
2
|
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { promisify } from "node:util";
|
|
6
5
|
import { readJson, writeJson } from "../fs-utils.js";
|
|
7
6
|
import { getRepoRoot } from "../paths.js";
|
|
7
|
+
import { safeExecFile } from "../process-utils.js";
|
|
8
8
|
import { createFreshness } from "./context-authority.js";
|
|
9
9
|
import { createManagedMarkdownBlock, replaceManagedMarkdownBlock } from "./managed-markdown.js";
|
|
10
10
|
|
|
11
|
-
const execFileAsync = promisify(
|
|
11
|
+
const execFileAsync = promisify(safeExecFile);
|
|
12
12
|
export const projectIndexSchemaVersion = "0.1.0";
|
|
13
13
|
const MAX_SOURCE_FILES = 2000;
|
|
14
14
|
const GENERATED_DIRS = [
|
|
@@ -440,7 +440,9 @@ export function renderSkillCompatibilitySkillMd(payload, { agentType = "codex" }
|
|
|
440
440
|
"",
|
|
441
441
|
"## Invocation",
|
|
442
442
|
"",
|
|
443
|
-
|
|
443
|
+
agentType === "generic"
|
|
444
|
+
? "- Start the umbrella lane using this host's native Meta-Architect invocation syntax."
|
|
445
|
+
: `- Start the umbrella lane with \`${getAgentInvocation(agentType, agentType === "codex" ? "maestro" : payload.name)}\` in this host.`,
|
|
444
446
|
"- Dispatch to the canonical MA runtime as `ma run '$maestro'`.",
|
|
445
447
|
"- Lane aliases: `arch`, `sage`, `flow`, `vet`, `vibe`, and `build`.",
|
|
446
448
|
"",
|
|
@@ -562,6 +564,7 @@ export async function writeSkillCompatibilityExport({
|
|
|
562
564
|
agentRootExists = null,
|
|
563
565
|
createSymlink = fs.symlink,
|
|
564
566
|
lockMetadata = {},
|
|
567
|
+
renderAgentType = agentType,
|
|
565
568
|
}) {
|
|
566
569
|
const effectiveAgentRootExists =
|
|
567
570
|
agentRootExists ?? (await detectProjectAgentRoot(agentType, cwd));
|
|
@@ -573,11 +576,11 @@ export async function writeSkillCompatibilityExport({
|
|
|
573
576
|
mode,
|
|
574
577
|
agentRootExists: effectiveAgentRootExists,
|
|
575
578
|
});
|
|
576
|
-
const skillMd = renderSkillCompatibilitySkillMd(payload, { agentType });
|
|
579
|
+
const skillMd = renderSkillCompatibilitySkillMd(payload, { agentType: renderAgentType });
|
|
577
580
|
const lockEntry = validateSkillLockEntry(
|
|
578
581
|
createSkillLockEntry({
|
|
579
582
|
payload,
|
|
580
|
-
agentType,
|
|
583
|
+
agentType: renderAgentType,
|
|
581
584
|
selectedAgentTargets: [agentType],
|
|
582
585
|
...lockMetadata,
|
|
583
586
|
}),
|
|
@@ -729,6 +732,7 @@ export async function verifyCrossAgentInstallMatrix({
|
|
|
729
732
|
const result = await writeSkillCompatibilityExport({
|
|
730
733
|
payload,
|
|
731
734
|
agentType,
|
|
735
|
+
renderAgentType: targets.length > 1 ? "generic" : agentType,
|
|
732
736
|
cwd,
|
|
733
737
|
agentRootExists: await detectProjectAgentRoot(agentType, cwd),
|
|
734
738
|
createSymlink: forceFallback
|
package/src/skills.js
CHANGED
|
@@ -268,31 +268,31 @@ function chooseMaestroRecommendation(releaseState, idea, buildReadiness = null)
|
|
|
268
268
|
|
|
269
269
|
if (releaseState.merge_status !== "MERGED_TO_DEVELOPMENT") {
|
|
270
270
|
return {
|
|
271
|
-
nextStep: "Finish the implementation slice and merge it into
|
|
271
|
+
nextStep: "Finish the implementation slice and merge it into dev.",
|
|
272
272
|
why: "The build gate is ready or done, but the branch promotion path has not completed yet.",
|
|
273
273
|
primaryLane: "implementation",
|
|
274
274
|
supportLane: "merge",
|
|
275
275
|
assignments: [
|
|
276
276
|
"Use the current build plan to finish the smallest viable implementation slice.",
|
|
277
|
-
"When ready, merge feature work into
|
|
277
|
+
"When ready, merge feature work into dev with `ma merge`.",
|
|
278
278
|
],
|
|
279
|
-
avoid: ["Do not promote directly to
|
|
280
|
-
nextTrigger: "`ma merge <feature/*>
|
|
279
|
+
avoid: ["Do not promote directly to main."],
|
|
280
|
+
nextTrigger: "`ma merge <feature/*> dev`",
|
|
281
281
|
};
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
if (releaseState.release_status !== "SHIPPED_TO_PROD") {
|
|
285
285
|
return {
|
|
286
|
-
nextStep: "Promote the approved release line to
|
|
286
|
+
nextStep: "Promote the approved release line to main.",
|
|
287
287
|
why: "Implementation and merge gates are complete, so the remaining step is the controlled release promotion.",
|
|
288
288
|
primaryLane: "release",
|
|
289
289
|
supportLane: "verification",
|
|
290
290
|
assignments: [
|
|
291
|
-
"Verify the origin branch is
|
|
292
|
-
"Run `ma release <origin>
|
|
291
|
+
"Verify the origin branch is dev or an approved release branch.",
|
|
292
|
+
"Run `ma release <origin> main` when the line is ready.",
|
|
293
293
|
],
|
|
294
294
|
avoid: ["Do not reopen earlier gates unless a new blocker appears."],
|
|
295
|
-
nextTrigger: "`ma release <
|
|
295
|
+
nextTrigger: "`ma release <dev|release/*> main`",
|
|
296
296
|
};
|
|
297
297
|
}
|
|
298
298
|
|
|
@@ -687,7 +687,7 @@ export async function runBuildLane({ actor = null, reviewMode = null, quorumVote
|
|
|
687
687
|
allowed: true,
|
|
688
688
|
gateState: "DONE",
|
|
689
689
|
blockers: [],
|
|
690
|
-
nextTriggers: ["ma merge <feature/*>
|
|
690
|
+
nextTriggers: ["ma merge <feature/*> dev"],
|
|
691
691
|
suggestedBranches,
|
|
692
692
|
buildSlice,
|
|
693
693
|
verificationPlan,
|
|
@@ -708,21 +708,21 @@ export async function runBuildLane({ actor = null, reviewMode = null, quorumVote
|
|
|
708
708
|
{ kind: "ralph-prd", value: ralphContract.prdPath },
|
|
709
709
|
],
|
|
710
710
|
blockers: [],
|
|
711
|
-
next_allowed_triggers: ["ma merge <feature/*>
|
|
711
|
+
next_allowed_triggers: ["ma merge <feature/*> dev"],
|
|
712
712
|
});
|
|
713
713
|
await syncStatusUpdates({ build_status: "DONE" }, { actor: authority.leaderActor });
|
|
714
714
|
await updateMaestroLedgerForBuild({
|
|
715
715
|
buildStatus: "DONE",
|
|
716
716
|
runtimeSummary,
|
|
717
717
|
blockers: [],
|
|
718
|
-
nextTriggers: ["ma merge <feature/*>
|
|
718
|
+
nextTriggers: ["ma merge <feature/*> dev"],
|
|
719
719
|
gateState: "COMPLETED",
|
|
720
720
|
trackStatus: "COMPLETED",
|
|
721
721
|
completionEvidence,
|
|
722
722
|
});
|
|
723
723
|
return {
|
|
724
724
|
status: "DONE",
|
|
725
|
-
nextTrigger: "ma merge <feature/*>
|
|
725
|
+
nextTrigger: "ma merge <feature/*> dev",
|
|
726
726
|
blockers: [],
|
|
727
727
|
suggestedBranches,
|
|
728
728
|
};
|
package/src/test-fixtures.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
2
|
import {
|
|
4
3
|
chmodSync,
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
import fs from "node:fs/promises";
|
|
14
13
|
import os from "node:os";
|
|
15
14
|
import path from "node:path";
|
|
15
|
+
import { safeSpawn } from "./process-utils.js";
|
|
16
16
|
|
|
17
17
|
const TEST_ROOT = path.join(os.tmpdir(), "ma-tests");
|
|
18
18
|
const RETENTION_ROOT = path.join(TEST_ROOT, "retained");
|
|
@@ -215,7 +215,7 @@ async function copyTree(sourceRoot, targetRoot, { strategy = "copy" } = {}) {
|
|
|
215
215
|
|
|
216
216
|
function runCommand(command, args) {
|
|
217
217
|
return new Promise((resolve, reject) => {
|
|
218
|
-
const child =
|
|
218
|
+
const child = safeSpawn(command, args, { stdio: "ignore" });
|
|
219
219
|
child.once("error", reject);
|
|
220
220
|
child.once("close", (code) =>
|
|
221
221
|
code === 0 ? resolve() : reject(new Error(`${command} exited with ${code}`)),
|
|
@@ -337,7 +337,7 @@ async function enforceFixtureBudget(root, strategy) {
|
|
|
337
337
|
|
|
338
338
|
async function runArchive(args) {
|
|
339
339
|
return new Promise((resolve, reject) => {
|
|
340
|
-
const child =
|
|
340
|
+
const child = safeSpawn("tar", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
341
341
|
let stderr = "";
|
|
342
342
|
child.stderr.on("data", (chunk) => {
|
|
343
343
|
stderr += chunk.toString();
|
|
@@ -442,7 +442,7 @@ export function runTestWithStreaming(command, args, options) {
|
|
|
442
442
|
assertNoSymlinkComponentsSync(TEST_ROOT, logFile);
|
|
443
443
|
assertNoSymlinkComponentsSync(TEST_ROOT, summaryFile);
|
|
444
444
|
return new Promise((resolve, reject) => {
|
|
445
|
-
const child =
|
|
445
|
+
const child = safeSpawn(command, args, { stdio: ["inherit", "pipe", "pipe"] });
|
|
446
446
|
const log = fs.open(logFile, "w");
|
|
447
447
|
const summary = fs.open(summaryFile, "w");
|
|
448
448
|
let summaryBytes = 0;
|