@voybio/ace-swarm 2.4.3 → 2.4.6

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 (144) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +72 -532
  3. package/assets/agent-state/INTERFACE_REGISTRY.md +2 -2
  4. package/assets/agent-state/MODULES/gates/gate-autonomy.json +4 -2
  5. package/assets/agent-state/MODULES/gates/gate-completeness.json +4 -2
  6. package/assets/agent-state/MODULES/gates/gate-operability.json +4 -2
  7. package/assets/agent-state/MODULES/gates/gate-security.json +3 -1
  8. package/assets/agent-state/MODULES/registry.json +1 -4
  9. package/assets/agent-state/MODULES/roles/capability-build.json +1 -2
  10. package/assets/agent-state/MODULES/roles/capability-eval.json +1 -1
  11. package/assets/agent-state/MODULES/roles/capability-qa.json +1 -2
  12. package/assets/agent-state/QUALITY_GATES.md +25 -9
  13. package/assets/agent-state/TEAL_CONFIG.md +2 -11
  14. package/assets/eval-battery/battery.json +85 -0
  15. package/assets/eval-battery/fixtures/edit-target.js +3 -0
  16. package/assets/scripts/render-mcp-configs.sh +12 -0
  17. package/dist/ace-context.d.ts +18 -0
  18. package/dist/ace-context.js +161 -16
  19. package/dist/ace-server-instructions.js +36 -9
  20. package/dist/astgrep-index.d.ts +7 -1
  21. package/dist/astgrep-index.js +66 -39
  22. package/dist/bridge-trace.d.ts +17 -0
  23. package/dist/bridge-trace.js +23 -0
  24. package/dist/cli.d.ts +9 -1
  25. package/dist/cli.js +619 -10
  26. package/dist/discovery-runtime-wrappers.d.ts +16 -1
  27. package/dist/discovery-runtime-wrappers.js +65 -9
  28. package/dist/eval-harness.d.ts +110 -0
  29. package/dist/eval-harness.js +254 -0
  30. package/dist/experience-reflector.d.ts +13 -0
  31. package/dist/experience-reflector.js +62 -0
  32. package/dist/gate-contract.d.ts +63 -0
  33. package/dist/gate-contract.js +178 -0
  34. package/dist/goal-engine.d.ts +155 -0
  35. package/dist/goal-engine.js +795 -0
  36. package/dist/helpers/store-resolution.js +40 -39
  37. package/dist/hermes/session-manager.d.ts +10 -0
  38. package/dist/hermes/session-manager.js +130 -27
  39. package/dist/job-scheduler.d.ts +58 -0
  40. package/dist/job-scheduler.js +202 -2
  41. package/dist/job-worker.d.ts +87 -0
  42. package/dist/job-worker.js +182 -0
  43. package/dist/local-model-policy.d.ts +8 -2
  44. package/dist/local-model-policy.js +31 -17
  45. package/dist/local-model-runtime.d.ts +6 -0
  46. package/dist/local-model-runtime.js +130 -2
  47. package/dist/measured-classing.d.ts +66 -0
  48. package/dist/measured-classing.js +165 -0
  49. package/dist/model-bridge.d.ts +45 -0
  50. package/dist/model-bridge.js +502 -44
  51. package/dist/orchestrator-supervisor.js +1 -1
  52. package/dist/plan-proposal.d.ts +7 -0
  53. package/dist/plan-proposal.js +74 -4
  54. package/dist/playbook-adaptation.d.ts +37 -0
  55. package/dist/playbook-adaptation.js +159 -0
  56. package/dist/playbook-curator.d.ts +16 -0
  57. package/dist/playbook-curator.js +106 -0
  58. package/dist/playbook-llm-reflector.d.ts +26 -0
  59. package/dist/playbook-llm-reflector.js +157 -0
  60. package/dist/public-surface.d.ts +2 -0
  61. package/dist/public-surface.js +26 -14
  62. package/dist/runtime-executor.js +45 -2
  63. package/dist/runtime-tool-specs.d.ts +8 -1
  64. package/dist/runtime-tool-specs.js +19 -1
  65. package/dist/safe-edit.d.ts +20 -0
  66. package/dist/safe-edit.js +355 -78
  67. package/dist/schemas.js +16 -15
  68. package/dist/server.d.ts +11 -2
  69. package/dist/server.js +55 -5
  70. package/dist/shared.d.ts +12 -0
  71. package/dist/shared.js +13 -1
  72. package/dist/source-mutation-coordinator.d.ts +54 -0
  73. package/dist/source-mutation-coordinator.js +259 -0
  74. package/dist/status-events.d.ts +4 -1
  75. package/dist/status-events.js +41 -6
  76. package/dist/store/ace-packed-store.d.ts +28 -9
  77. package/dist/store/ace-packed-store.js +324 -57
  78. package/dist/store/bgzf.d.ts +4 -0
  79. package/dist/store/bgzf.js +94 -0
  80. package/dist/store/bootstrap-store.d.ts +4 -1
  81. package/dist/store/bootstrap-store.js +87 -9
  82. package/dist/store/cache-workspace.d.ts +8 -0
  83. package/dist/store/cache-workspace.js +67 -1
  84. package/dist/store/crc32.d.ts +2 -0
  85. package/dist/store/crc32.js +16 -0
  86. package/dist/store/fsck.d.ts +16 -0
  87. package/dist/store/fsck.js +240 -0
  88. package/dist/store/gate-contract-migration.d.ts +10 -0
  89. package/dist/store/gate-contract-migration.js +413 -0
  90. package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
  91. package/dist/store/materializers/context-snapshot-materializer.js +4 -1
  92. package/dist/store/materializers/projection-manager.d.ts +7 -0
  93. package/dist/store/materializers/projection-manager.js +37 -0
  94. package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
  95. package/dist/store/repositories/goal-repository.d.ts +112 -0
  96. package/dist/store/repositories/goal-repository.js +157 -0
  97. package/dist/store/repositories/playbook-repository.d.ts +107 -0
  98. package/dist/store/repositories/playbook-repository.js +314 -0
  99. package/dist/store/store-artifacts.js +44 -2
  100. package/dist/store/store-authority-audit.js +1 -0
  101. package/dist/store/store-cli.d.ts +30 -0
  102. package/dist/store/store-cli.js +242 -0
  103. package/dist/store/store-snapshot.d.ts +12 -0
  104. package/dist/store/store-snapshot.js +74 -11
  105. package/dist/store/types.d.ts +1 -0
  106. package/dist/store/write-coordinator.d.ts +7 -0
  107. package/dist/store/write-coordinator.js +11 -0
  108. package/dist/todo-state.d.ts +21 -0
  109. package/dist/todo-state.js +150 -53
  110. package/dist/tools-agent.js +86 -22
  111. package/dist/tools-discovery.js +5 -3
  112. package/dist/tools-files.js +107 -23
  113. package/dist/tools-framework.js +146 -40
  114. package/dist/tools-goal.d.ts +11 -0
  115. package/dist/tools-goal.js +108 -0
  116. package/dist/tools-handoff.js +7 -11
  117. package/dist/tools-lifecycle.js +2 -0
  118. package/dist/tools-scheduler.js +44 -1
  119. package/dist/tools-skills.js +3 -2
  120. package/dist/tools-todo.js +64 -3
  121. package/dist/tools.js +2 -0
  122. package/dist/tui/chat.js +4 -0
  123. package/dist/tui/index.js +4 -1
  124. package/dist/tui/ollama.d.ts +2 -0
  125. package/dist/tui/openai-compatible.d.ts +14 -0
  126. package/dist/tui/openai-compatible.js +15 -2
  127. package/dist/tui/provider-discovery.d.ts +1 -0
  128. package/dist/tui/provider-discovery.js +2 -2
  129. package/docs/cli-reference.md +125 -0
  130. package/docs/development.md +46 -0
  131. package/docs/evaluation-safety.md +27 -0
  132. package/docs/experience-playbook.md +35 -0
  133. package/docs/hermes.md +129 -0
  134. package/docs/library-api.md +31 -0
  135. package/docs/mcp-tool-reference.md +290 -0
  136. package/docs/provider-security.md +85 -0
  137. package/docs/state-model.md +113 -0
  138. package/docs/troubleshooting.md +95 -0
  139. package/docs/tui-operator-guide.md +75 -0
  140. package/package.json +5 -1
  141. package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
  142. package/assets/agent-state/MODULES/gates/gate-correctness.json +0 -7
  143. package/assets/agent-state/MODULES/gates/gate-evaluation.json +0 -7
  144. package/assets/agent-state/MODULES/gates/gate-typescript-public-surface.json +0 -7
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { ACE_TASKS_ROOT_REL, resolveWorkspaceRoot, safeRead, safeWrite, wsPath } from "./helpers.js";
2
+ import { ACE_TASKS_ROOT_REL, resolveWorkspaceRoot, resolveWritableTaskPath, safeRead, safeWrite, safeWriteAsync, withFileLock, wsPath, } from "./helpers.js";
3
3
  import { openStore } from "./store/ace-packed-store.js";
4
4
  import { ProjectionManager } from "./store/materializers/projection-manager.js";
5
5
  import { TodoRepository } from "./store/repositories/todo-repository.js";
@@ -385,6 +385,49 @@ function setTodoCheckbox(todoRaw, line, checked) {
385
385
  lines[line - 1] = target.replace(/^- \[( |x|X)\]/, `- [${checked ? "x" : " "}]`);
386
386
  return lines.join("\n");
387
387
  }
388
+ function insertTodoItem(todoRaw, section, title, id, dependsOn) {
389
+ const metaParts = [`id=${id}`];
390
+ if (dependsOn.length > 0)
391
+ metaParts.push(`depends_on=${dependsOn.join(",")}`);
392
+ const newLine = `- [ ] ${title} <!-- ace:${metaParts.join("; ")} -->`;
393
+ const lines = todoRaw.length > 0 ? todoRaw.split(/\r?\n/) : [];
394
+ let sectionStart = -1;
395
+ let sectionEnd = lines.length;
396
+ for (let i = 0; i < lines.length; i++) {
397
+ const header = lines[i]?.match(/^##\s+(.+)$/);
398
+ if (!header?.[1])
399
+ continue;
400
+ if (header[1].trim().toLowerCase() === section.toLowerCase()) {
401
+ sectionStart = i;
402
+ let j = i + 1;
403
+ while (j < lines.length && !/^##\s+/.test(lines[j] ?? ""))
404
+ j++;
405
+ sectionEnd = j;
406
+ break;
407
+ }
408
+ }
409
+ if (sectionStart === -1) {
410
+ const trimmed = todoRaw.replace(/\s+$/, "");
411
+ const prefix = trimmed.length > 0 ? `${trimmed}\n\n` : "";
412
+ return `${prefix}## ${section}\n${newLine}\n`;
413
+ }
414
+ let insertAt = sectionEnd;
415
+ while (insertAt > sectionStart + 1 && (lines[insertAt - 1] ?? "").trim() === "")
416
+ insertAt--;
417
+ lines.splice(insertAt, 0, newLine);
418
+ return lines.join("\n");
419
+ }
420
+ function uniqueTodoId(base, existingIds) {
421
+ if (!existingIds.has(base))
422
+ return base;
423
+ let n = 2;
424
+ let candidate = `${base}-${n}`;
425
+ while (existingIds.has(candidate)) {
426
+ n += 1;
427
+ candidate = `${base}-${n}`;
428
+ }
429
+ return candidate;
430
+ }
388
431
  function getTodoPath() {
389
432
  return wsPath(TODO_MARKDOWN_REL);
390
433
  }
@@ -401,7 +444,10 @@ export function syncTodoState(todoContent) {
401
444
  const path = writeState(state);
402
445
  return { state, path };
403
446
  }
404
- export async function syncTodoStateSafe(todoContent) {
447
+ // Lock-free core: callers that already hold the TODO_MARKDOWN_REL lock (e.g.
448
+ // appendTodoItemSafe) must call this directly instead of syncTodoStateSafe,
449
+ // since withFileLock is not reentrant and re-acquiring the same key deadlocks.
450
+ async function syncTodoStateLocked(todoContent) {
405
451
  const root = workspaceRoot();
406
452
  if (storeExistsSync(root)) {
407
453
  const todoRaw = typeof todoContent === "string" ? todoContent : safeRead(TODO_MARKDOWN_REL);
@@ -418,6 +464,9 @@ export async function syncTodoStateSafe(todoContent) {
418
464
  await waitForTodoStoreMirror();
419
465
  return result;
420
466
  }
467
+ export async function syncTodoStateSafe(todoContent) {
468
+ return withFileLock(TODO_MARKDOWN_REL, () => syncTodoStateLocked(todoContent));
469
+ }
421
470
  export function readTodoState() {
422
471
  if (storeExistsSync(workspaceRoot())) {
423
472
  return readPersistedState();
@@ -495,73 +544,121 @@ export function transitionTodoNode(nodeId, targetStatus) {
495
544
  };
496
545
  }
497
546
  export async function transitionTodoNodeSafe(nodeId, targetStatus) {
498
- const root = workspaceRoot();
499
- if (storeExistsSync(root)) {
500
- const todoRaw = safeRead(TODO_MARKDOWN_REL);
501
- if (isReadError(todoRaw)) {
502
- return {
503
- ok: false,
504
- path: getTodoStatePath(),
505
- todo_path: getTodoPath(),
506
- error: `${TODO_MARKDOWN_REL} not found; cannot apply transition.`,
507
- };
508
- }
509
- const synced = syncStateWithTodo(todoRaw, readPersistedState());
510
- const node = synced.nodes[nodeId];
511
- if (!node) {
512
- return {
513
- ok: false,
514
- path: getTodoStatePath(),
515
- todo_path: getTodoPath(),
516
- error: `Unknown node_id: ${nodeId}`,
517
- };
518
- }
519
- const from = node.status;
520
- if (from !== targetStatus) {
521
- const allowed = TRANSITIONS[from];
522
- if (!allowed.includes(targetStatus)) {
547
+ return withFileLock(TODO_MARKDOWN_REL, async () => {
548
+ const root = workspaceRoot();
549
+ if (storeExistsSync(root)) {
550
+ const todoRaw = safeRead(TODO_MARKDOWN_REL);
551
+ if (isReadError(todoRaw)) {
523
552
  return {
524
553
  ok: false,
525
554
  path: getTodoStatePath(),
526
555
  todo_path: getTodoPath(),
527
- node_id: nodeId,
528
- from,
529
- to: targetStatus,
530
- allowed,
531
- error: `Invalid transition ${from} -> ${targetStatus}`,
556
+ error: `${TODO_MARKDOWN_REL} not found; cannot apply transition.`,
532
557
  };
533
558
  }
534
- }
535
- if (targetStatus === "in_progress" ||
536
- targetStatus === "gated" ||
537
- targetStatus === "done") {
538
- const unresolved = node.depends_on.filter((depId) => synced.nodes[depId]?.status !== "done");
539
- if (unresolved.length > 0) {
559
+ const synced = syncStateWithTodo(todoRaw, readPersistedState());
560
+ const node = synced.nodes[nodeId];
561
+ if (!node) {
540
562
  return {
541
563
  ok: false,
542
564
  path: getTodoStatePath(),
543
565
  todo_path: getTodoPath(),
544
- node_id: nodeId,
545
- from,
546
- to: targetStatus,
547
- unresolved_dependencies: unresolved,
548
- error: `Dependencies not satisfied for ${nodeId}`,
566
+ error: `Unknown node_id: ${nodeId}`,
549
567
  };
550
568
  }
569
+ const from = node.status;
570
+ if (from !== targetStatus) {
571
+ const allowed = TRANSITIONS[from];
572
+ if (!allowed.includes(targetStatus)) {
573
+ return {
574
+ ok: false,
575
+ path: getTodoStatePath(),
576
+ todo_path: getTodoPath(),
577
+ node_id: nodeId,
578
+ from,
579
+ to: targetStatus,
580
+ allowed,
581
+ error: `Invalid transition ${from} -> ${targetStatus}`,
582
+ };
583
+ }
584
+ }
585
+ if (targetStatus === "in_progress" ||
586
+ targetStatus === "gated" ||
587
+ targetStatus === "done") {
588
+ const unresolved = node.depends_on.filter((depId) => synced.nodes[depId]?.status !== "done");
589
+ if (unresolved.length > 0) {
590
+ return {
591
+ ok: false,
592
+ path: getTodoStatePath(),
593
+ todo_path: getTodoPath(),
594
+ node_id: nodeId,
595
+ from,
596
+ to: targetStatus,
597
+ unresolved_dependencies: unresolved,
598
+ error: `Dependencies not satisfied for ${nodeId}`,
599
+ };
600
+ }
601
+ }
602
+ node.status = targetStatus;
603
+ const statePath = await writeStateStoreBacked(root, synced);
604
+ return {
605
+ ok: true,
606
+ path: statePath,
607
+ todo_path: getTodoPath(),
608
+ node_id: nodeId,
609
+ from,
610
+ to: targetStatus,
611
+ };
551
612
  }
552
- node.status = targetStatus;
553
- const statePath = await writeStateStoreBacked(root, synced);
613
+ const result = transitionTodoNode(nodeId, targetStatus);
614
+ await waitForTodoStoreMirror();
615
+ return result;
616
+ });
617
+ }
618
+ /**
619
+ * Atomically append a new TODO item under a section, without requiring the
620
+ * caller to read the whole board and resubmit it via update_todo(new_content).
621
+ * That read-modify-write dance is what let concurrent agents silently clobber
622
+ * each other's appended items: the read and write are separate MCP calls, so
623
+ * a lock around the write alone cannot close the race. Here the read, insert,
624
+ * and write all happen inside one TODO_MARKDOWN_REL lock acquisition.
625
+ */
626
+ export async function appendTodoItemSafe(title, options) {
627
+ const trimmedTitle = title.trim();
628
+ if (!trimmedTitle) {
554
629
  return {
555
- ok: true,
556
- path: statePath,
630
+ ok: false,
631
+ path: getTodoStatePath(),
557
632
  todo_path: getTodoPath(),
558
- node_id: nodeId,
559
- from,
560
- to: targetStatus,
633
+ error: "Todo item title must not be empty.",
561
634
  };
562
635
  }
563
- const result = transitionTodoNode(nodeId, targetStatus);
564
- await waitForTodoStoreMirror();
565
- return result;
636
+ return withFileLock(TODO_MARKDOWN_REL, async () => {
637
+ const existingRaw = safeRead(TODO_MARKDOWN_REL);
638
+ const baseRaw = isReadError(existingRaw) ? "# Tasks\n" : existingRaw;
639
+ const section = options?.section?.trim() || "Active Work";
640
+ const dependsOn = (options?.depends_on ?? []).map((dep) => dep.trim()).filter(Boolean);
641
+ const existingIds = new Set(parseTodoMarkdown(baseRaw).map((entry) => entry.id));
642
+ const requestedId = options?.id?.trim();
643
+ if (requestedId && existingIds.has(requestedId)) {
644
+ return {
645
+ ok: false,
646
+ path: getTodoStatePath(),
647
+ todo_path: getTodoPath(),
648
+ error: `Todo id already exists: ${requestedId}`,
649
+ };
650
+ }
651
+ const id = requestedId || uniqueTodoId(`todo-${slugify(trimmedTitle)}`, existingIds);
652
+ const updatedRaw = insertTodoItem(baseRaw, section, trimmedTitle, id, dependsOn);
653
+ const synced = await syncTodoStateLocked(updatedRaw);
654
+ const mdPath = await safeWriteAsync(resolveWritableTaskPath("todo"), updatedRaw);
655
+ return {
656
+ ok: true,
657
+ path: synced.path,
658
+ todo_path: mdPath,
659
+ node_id: id,
660
+ section,
661
+ };
662
+ });
566
663
  }
567
664
  //# sourceMappingURL=todo-state.js.map
@@ -2,9 +2,10 @@
2
2
  * Agent, skill, kernel, and task-pack tool registrations.
3
3
  */
4
4
  import { z } from "zod";
5
- import { ALL_AGENTS, COMPOSABLE_AGENTS, SWARM_AGENTS, SWARM_SUBAGENT_MAP, classifyPathSource, getAgentInstructionPath, getAgentManifestPath, getKernelArtifactPath, isSwarmRole, listAvailableSkills, readAgentInstructions, readAgentManifest, readKernelArtifact, readSkillInstructions, readTaskArtifact, resolveWorkspaceRoot, resolveWritableTaskPath, safeWriteAsync, } from "./helpers.js";
5
+ import { ALL_AGENTS, COMPOSABLE_AGENTS, SWARM_AGENTS, SWARM_SUBAGENT_MAP, classifyPathSource, getAgentInstructionPath, getAgentManifestPath, getKernelArtifactPath, isSwarmRole, listAvailableSkills, readAgentInstructions, readAgentManifest, readKernelArtifact, readSkillInstructions, readTaskArtifact, resolveWorkspaceRoot, } from "./helpers.js";
6
6
  import { loadRuntimeProfile, readRuntimePromptTemplate, readRuntimeProfileState, validateRuntimeProfileContent, } from "./runtime-profile.js";
7
7
  import { getUnattendedSession, listUnattendedSessions, startUnattendedSession, stopUnattendedSession, validateRuntimeExecutorSessionRegistryContent, waitForUnattendedSession, } from "./runtime-executor.js";
8
+ import { materializePlaybookMarkdown, PlaybookRepository, renderPlaybookMarkdown, } from "./store/repositories/playbook-repository.js";
8
9
  import { executeRuntimeTool, listRuntimeToolSpecs, loadRuntimeToolRegistry, validateRuntimeToolRegistryContent, } from "./runtime-tool-specs.js";
9
10
  import { createWorkspaceSessionAsync, listWorkspaceSessions, removeWorkspaceSession, resolveRuntimeWorkspaceRoot, validateManagedWorkspacePath, } from "./workspace-manager.js";
10
11
  import { getTrackerAdapter, listTrackerAdapterKinds, loadTrackerSnapshot, validateTrackerSnapshotContent, } from "./tracker-adapters.js";
@@ -350,8 +351,9 @@ function createToolOnlyBridgeResult(step, reason) {
350
351
  return {
351
352
  bridge_id: `tool-only-${step.step_id}-${Date.now()}`,
352
353
  role: step.role,
353
- status: "completed",
354
- summary: `Tool-only orchestrator completion for ${step.step_id} (${step.role}): ${step.task}. ${reason}`,
354
+ status: "blocked",
355
+ reason_code: "no_model_runtime",
356
+ summary: `Step blocked — no model runtime available for ${step.step_id} (${step.role}): ${step.task}. ${reason} Run \`ace connect\` or \`ace doctor --scan\`, or pass provider/model to resolve.`,
355
357
  turns: 0,
356
358
  tool_calls: [],
357
359
  child_results: [],
@@ -1369,7 +1371,11 @@ export function registerAgentTools(server) {
1369
1371
  .string()
1370
1372
  .optional()
1371
1373
  .describe("Optional workspace root override; defaults to the active workspace"),
1372
- }, async ({ task, steps, execution_mode, max_turns_per_step, provider, model, base_url, ollama_url, workspace_root }, extra) => {
1374
+ mode: z
1375
+ .enum(["execute", "plan_only"])
1376
+ .optional()
1377
+ .describe("'plan_only' compiles and validates the plan without executing any steps; 'execute' is the default"),
1378
+ }, async ({ task, steps, execution_mode, max_turns_per_step, provider, model, base_url, ollama_url, workspace_root, mode }, extra) => {
1373
1379
  let runtime;
1374
1380
  const runtimeWarnings = [];
1375
1381
  try {
@@ -1406,6 +1412,32 @@ export function registerAgentTools(server) {
1406
1412
  const bridge = runtime
1407
1413
  ? new ModelBridge(createDefaultModelBridgeClients(runtime))
1408
1414
  : undefined;
1415
+ // plan_only mode: return the compiled plan without executing anything
1416
+ if (mode === "plan_only" || (!runtime && runtimeWarnings.length > 0)) {
1417
+ const planOnly = mode === "plan_only";
1418
+ const reasonCode = planOnly ? "plan_only_requested" : "no_model_runtime";
1419
+ return {
1420
+ content: [
1421
+ {
1422
+ type: "text",
1423
+ text: JSON.stringify({
1424
+ ok: planOnly,
1425
+ reason_code: reasonCode,
1426
+ execution_backend: "none",
1427
+ mode: planOnly ? "plan_only" : "execute",
1428
+ plan,
1429
+ runtime: runtime ?? null,
1430
+ runtime_warnings: runtimeWarnings,
1431
+ remediation: !runtime
1432
+ ? "Run `ace connect` or `ace doctor --scan`, or pass provider/model. Use mode:\"plan_only\" explicitly to compile a plan without a runtime."
1433
+ : undefined,
1434
+ acceptance_trace_map: traceArtifactPath,
1435
+ plan_source: planSource,
1436
+ }, null, 2),
1437
+ },
1438
+ ],
1439
+ };
1440
+ }
1409
1441
  const fallbackHandoffPrefix = `LOCAL-${plan.plan_id}-`;
1410
1442
  const vericifyWarnings = [];
1411
1443
  const appendSessionPlanTransition = async (stepId, from, to, reason, reasonCode) => {
@@ -1426,6 +1458,22 @@ export function registerAgentTools(server) {
1426
1458
  evidence_refs: step.tool_scope ?? [],
1427
1459
  });
1428
1460
  }).catch(() => undefined);
1461
+ // WS-C item 5: emit a PLAN_UPDATE payload on the existing status-event pipe so
1462
+ // consumers can follow "exactly one item in_progress" without a new transport.
1463
+ const inProgressStep = plan.steps.find((candidate) => candidate.status === "running");
1464
+ await executeAceInternalTool("emit_status_event", {
1465
+ source_module: "capability-ops",
1466
+ event_type: "PLAN_UPDATE",
1467
+ status: to === "done" ? "done" : to === "blocked" || to === "failed" ? "blocked" : "in_progress",
1468
+ summary: `Plan ${plan.plan_id} step ${stepId}: ${from} -> ${to}`,
1469
+ payload_json: JSON.stringify({
1470
+ plan_id: plan.plan_id,
1471
+ step_id: stepId,
1472
+ from,
1473
+ to,
1474
+ in_progress_step_id: inProgressStep?.step_id ?? null,
1475
+ }),
1476
+ }, sessionId).catch(() => undefined);
1429
1477
  };
1430
1478
  const supervised = await superviseTaskPlan(plan, {
1431
1479
  async spawnStep(step) {
@@ -1439,6 +1487,8 @@ export function registerAgentTools(server) {
1439
1487
  provider: runtime.provider,
1440
1488
  model: runtime.model,
1441
1489
  toolScope: step.tool_scope,
1490
+ expectedArtifacts: step.expected_artifacts,
1491
+ requiredEvidenceRefs: step.required_evidence_refs,
1442
1492
  });
1443
1493
  }
1444
1494
  const reason = runtimeWarnings[0] ??
@@ -2287,31 +2337,45 @@ export function registerAgentTools(server) {
2287
2337
  };
2288
2338
  });
2289
2339
  // ── Lessons ───────────────────────────────────────────────────────
2290
- server.tool("get_lessons", "Read all accumulated lessons and guardrails", {}, async () => ({
2291
- content: [{ type: "text", text: readTaskArtifact("lessons") }],
2292
- }));
2340
+ server.tool("get_lessons", "Read all accumulated playbook lessons and guardrails", {}, async () => {
2341
+ const root = resolveWorkspaceRoot();
2342
+ const repo = new PlaybookRepository(root);
2343
+ const path = materializePlaybookMarkdown(root);
2344
+ return {
2345
+ content: [{ type: "text", text: `${renderPlaybookMarkdown(repo)}\nProjection: ${path}` }],
2346
+ };
2347
+ });
2293
2348
  server.tool("add_lesson", "Record a new lesson/guardrail after a correction or failure", {
2294
2349
  context: z.string().describe("What happened"),
2295
2350
  failure_pattern: z.string().describe("What went wrong"),
2296
2351
  guardrail: z.string().describe("Rule to prevent recurrence"),
2297
2352
  verification_check: z.string().describe("How to verify compliance"),
2298
2353
  }, async ({ context, failure_pattern, guardrail, verification_check }) => {
2299
- const date = new Date().toISOString().slice(0, 10);
2300
- const entry = [
2301
- "",
2302
- `## Lesson — ${date}`,
2303
- "",
2304
- `- **Date:** ${date}`,
2305
- `- **Context:** ${context}`,
2306
- `- **Failure Pattern:** ${failure_pattern}`,
2307
- `- **Guardrail:** ${guardrail}`,
2308
- `- **Verification Check:** ${verification_check}`,
2309
- "",
2310
- ].join("\n");
2311
- const existing = readTaskArtifact("lessons");
2312
- const path = await safeWriteAsync(resolveWritableTaskPath("lessons"), `${existing}${entry}`);
2354
+ const knownReasonCodes = [
2355
+ "false_completion_no_evidence",
2356
+ "forward_pacing_incomplete",
2357
+ "bridge_tool_doom_loop",
2358
+ "bridge_text_doom_loop",
2359
+ "context_overflow",
2360
+ "tool_timeout",
2361
+ "role_contract_violation",
2362
+ ];
2363
+ const scope = {};
2364
+ const lowered = failure_pattern.toLowerCase();
2365
+ const reason = knownReasonCodes.find((code) => lowered.includes(code));
2366
+ if (reason)
2367
+ scope.reason_code = reason;
2368
+ const root = resolveWorkspaceRoot();
2369
+ const bullet = new PlaybookRepository(root).createBullet({
2370
+ section: "failure_modes",
2371
+ scope,
2372
+ content: `${guardrail} Verify: ${verification_check}`,
2373
+ evidence_refs: [],
2374
+ provenance: { source: "operator" },
2375
+ });
2376
+ const path = materializePlaybookMarkdown(root);
2313
2377
  return {
2314
- content: [{ type: "text", text: `✅ Lesson recorded in ${path}` }],
2378
+ content: [{ type: "text", text: `✅ Lesson recorded as ${bullet.bullet_id} (${bullet.status}) in ${path}. Context: ${context}` }],
2315
2379
  };
2316
2380
  });
2317
2381
  // ── CLI Work Split ────────────────────────────────────────────────
@@ -217,7 +217,7 @@ export function registerDiscoveryTools(server) {
217
217
  },
218
218
  ],
219
219
  }));
220
- server.tool("mcp_ingestion_probe", "Validate an external MCP server configuration, persist request/response artifacts, and expose bounded declared tool metadata without leaking shell env", {
220
+ server.tool("mcp_ingestion_probe", "Connect to an external MCP server with a bounded initialize handshake, list tools/resources/templates/prompts, and persist the probe artifacts", {
221
221
  name: z.string().min(1).describe("Logical MCP server name"),
222
222
  transport: z.enum(["stdio", "http"]).describe("MCP transport class"),
223
223
  command: z.string().optional().describe("Command for stdio MCP servers"),
@@ -231,8 +231,9 @@ export function registerDiscoveryTools(server) {
231
231
  input_schema: z.unknown().optional(),
232
232
  }))
233
233
  .optional()
234
- .describe("Optional declared tool metadata discovered or provided by the server class"),
235
- }, async ({ name, transport, command, args, url, env, declared_tools }) => ({
234
+ .describe("Deprecated compatibility input; live server catalogs are authoritative"),
235
+ timeout_ms: z.number().int().min(250).max(60000).optional().describe("Bound for connect and each catalog request (default 10000ms)"),
236
+ }, async ({ name, transport, command, args, url, env, declared_tools, timeout_ms }) => ({
236
237
  content: [
237
238
  {
238
239
  type: "text",
@@ -244,6 +245,7 @@ export function registerDiscoveryTools(server) {
244
245
  url,
245
246
  env,
246
247
  declared_tools,
248
+ timeout_ms,
247
249
  }), null, 2),
248
250
  },
249
251
  ],