@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.
Files changed (52) hide show
  1. package/COVERAGE.md +9 -9
  2. package/DEMO.md +4 -4
  3. package/README.md +80 -70
  4. package/bin/ma.js +6 -8
  5. package/docs/README.md +2 -2
  6. package/docs/assets/banner.png +0 -0
  7. package/docs/getting-started.md +9 -9
  8. package/docs/qa/{release-issue-gates-0.14.0.json → release-issue-gates-0.14.1.json} +77 -77
  9. package/docs/qa/{release-readiness-0.14.0.md → release-readiness-0.14.1.md} +12 -12
  10. package/docs/release-spec.md +12 -12
  11. package/docs/vendor-plugin-publishing.md +49 -0
  12. package/mcp/local/code-intel.js +9 -3
  13. package/package.json +12 -3
  14. package/plugins/meta-architect/.app.json +1 -1
  15. package/plugins/meta-architect/.claude-plugin/plugin.json +12 -0
  16. package/plugins/meta-architect/.codex-plugin/plugin.json +1 -1
  17. package/plugins/meta-architect/.mcp.json +1 -1
  18. package/plugins/meta-architect/README.md +13 -1
  19. package/plugins/meta-architect/obsidian/manifest.json +1 -1
  20. package/plugins/meta-architect/skills/maestro/SKILL.md +17 -1
  21. package/plugins/meta-architect/skills/maestro/references/core-release-rules.md +2 -2
  22. package/schemas/autonomous-task-queue.schema.json +2 -0
  23. package/scripts/install.sh +1 -1
  24. package/scripts/install.sh.sha256 +1 -1
  25. package/scripts/plugin-build.js +314 -0
  26. package/scripts/plugin-publish.js +77 -0
  27. package/scripts/release-sync.js +11 -0
  28. package/scripts/release-verify.js +35 -4
  29. package/skills/maestro/SKILL.md +17 -1
  30. package/skills/maestro/references/core-release-rules.md +2 -2
  31. package/sprint/07-release.md +2 -2
  32. package/src/agents.js +4 -2
  33. package/src/bootstrap.js +20 -2
  34. package/src/codex-app-server.js +4 -4
  35. package/src/launcher.js +2 -2
  36. package/src/mcp-live-client.js +2 -2
  37. package/src/prelaunch.js +34 -0
  38. package/src/process-utils.js +62 -0
  39. package/src/quality/ai-quality-orchestrator.js +2 -2
  40. package/src/release-operations.js +14 -4
  41. package/src/runtime/architect-review.js +2 -2
  42. package/src/runtime/autonomous-tasks.js +220 -43
  43. package/src/runtime/codeburn-core.js +9 -3
  44. package/src/runtime/core-source-ingest.js +2 -2
  45. package/src/runtime/detached-provider.js +17 -8
  46. package/src/runtime/live-agent-verification.js +2 -2
  47. package/src/runtime/project-context.js +2 -2
  48. package/src/runtime/skills-registry-export.js +7 -3
  49. package/src/skills.js +12 -12
  50. package/src/test-fixtures.js +4 -4
  51. package/support-bundle.json +1 -1
  52. 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 = queue.tasks.filter((task) => task.status === "running");
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 byId = new Map(queue.tasks.map((task) => [task.id, task]));
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, byId))
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.status = "blocked";
293
- task.blocker =
294
- "Explicit approval required for destructive, production, credential, or external mutation task";
295
- task.updatedAt = new Date().toISOString();
296
- await emit({ taskId: task.id, status: task.status, blocker: task.blocker });
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
- task.status = "running";
301
- task.attempts++;
302
- task.startedAt ??= new Date().toISOString();
303
- task.updatedAt = new Date().toISOString();
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
- task.status = "blocked";
309
- task.blocker = result.reason ?? "Runner reported a blocker";
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
- task.status = "queued";
312
- task.error = result.reason ?? "Runner failed; task queued for retry";
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
- task.status = "failed";
315
- task.error = result.reason ?? "Runner failed after maximum attempts";
456
+ Object.assign(patch, {
457
+ status: "failed",
458
+ error: result.reason ?? "Runner failed after maximum attempts",
459
+ });
316
460
  } else {
317
- task.status = "completed";
318
- task.completedAt = new Date().toISOString();
319
- task.evidence.push(...(Array.isArray(result?.evidence) ? result.evidence : []));
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
- for (const task of queue.tasks) {
343
- if (task.status !== "queued") continue;
344
- const failedDependency = task.dependencies.find((id) =>
345
- ["failed", "blocked", "cancelled"].includes(byId.get(id)?.status),
346
- );
347
- if (failedDependency) {
348
- task.status = "blocked";
349
- task.blocker = `Dependency ${failedDependency} is ${byId.get(failedDependency).status}`;
350
- } else if (isExpired(task, Date.now())) {
351
- task.status = "blocked";
352
- task.blocker = "Task deadline expired";
353
- } else if (!dependenciesReady(task, byId)) {
354
- task.blocker = "Waiting for dependencies";
355
- }
356
- task.updatedAt = new Date().toISOString();
357
- }
358
- await saveTaskQueue(queue);
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: queue.tasks,
538
+ tasks: finalQueue.tasks,
362
539
  events,
363
- summary: summarizeTasks(queue.tasks),
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(execFile);
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, { cwd, encoding: "utf8", timeout: 15_000 });
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 = spawn,
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 = spawnSync("tmux", ["ls"], {
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 started = spawnSync("tmux", ["new-session", "-d", "-s", sessionName, command, ...args], {
47
- encoding: "utf8",
48
- env: buildDetachedEnv(),
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 = spawnSync(
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
- spawnSync("tmux", ["kill-session", "-t", sessionName], {
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 = spawn(command, args, {
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(execFile);
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
- `- Start the umbrella lane with \`${getAgentInvocation(agentType, agentType === "codex" ? "maestro" : payload.name)}\` in this host.`,
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 development.",
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 development with `ma merge`.",
277
+ "When ready, merge feature work into dev with `ma merge`.",
278
278
  ],
279
- avoid: ["Do not promote directly to prod."],
280
- nextTrigger: "`ma merge <feature/*> development`",
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 prod.",
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 development or an approved release branch.",
292
- "Run `ma release <origin> prod` when the line is ready.",
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 <development|release/*> prod`",
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/*> development"],
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/*> development"],
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/*> development"],
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/*> development",
725
+ nextTrigger: "ma merge <feature/*> dev",
726
726
  blockers: [],
727
727
  suggestedBranches,
728
728
  };
@@ -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 = spawn(command, args, { stdio: "ignore" });
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 = spawn("tar", args, { stdio: ["ignore", "ignore", "pipe"] });
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 = spawn(command, args, { stdio: ["inherit", "pipe", "pipe"] });
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;