@voybio/ace-swarm 2.4.4 → 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 (120) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +72 -532
  3. package/assets/eval-battery/battery.json +85 -0
  4. package/assets/eval-battery/fixtures/edit-target.js +3 -0
  5. package/assets/scripts/render-mcp-configs.sh +12 -0
  6. package/dist/ace-context.d.ts +18 -0
  7. package/dist/ace-context.js +161 -16
  8. package/dist/ace-server-instructions.js +36 -9
  9. package/dist/bridge-trace.d.ts +17 -0
  10. package/dist/bridge-trace.js +23 -0
  11. package/dist/cli.d.ts +9 -1
  12. package/dist/cli.js +619 -10
  13. package/dist/discovery-runtime-wrappers.d.ts +16 -1
  14. package/dist/discovery-runtime-wrappers.js +65 -9
  15. package/dist/eval-harness.d.ts +110 -0
  16. package/dist/eval-harness.js +254 -0
  17. package/dist/experience-reflector.d.ts +13 -0
  18. package/dist/experience-reflector.js +62 -0
  19. package/dist/goal-engine.d.ts +155 -0
  20. package/dist/goal-engine.js +795 -0
  21. package/dist/helpers/store-resolution.js +40 -39
  22. package/dist/hermes/session-manager.d.ts +10 -0
  23. package/dist/hermes/session-manager.js +130 -27
  24. package/dist/job-scheduler.d.ts +58 -0
  25. package/dist/job-scheduler.js +202 -2
  26. package/dist/job-worker.d.ts +87 -0
  27. package/dist/job-worker.js +182 -0
  28. package/dist/local-model-policy.d.ts +8 -2
  29. package/dist/local-model-policy.js +31 -17
  30. package/dist/local-model-runtime.d.ts +6 -0
  31. package/dist/local-model-runtime.js +130 -2
  32. package/dist/measured-classing.d.ts +66 -0
  33. package/dist/measured-classing.js +165 -0
  34. package/dist/model-bridge.d.ts +45 -0
  35. package/dist/model-bridge.js +502 -44
  36. package/dist/orchestrator-supervisor.js +1 -1
  37. package/dist/plan-proposal.d.ts +7 -0
  38. package/dist/plan-proposal.js +74 -4
  39. package/dist/playbook-adaptation.d.ts +37 -0
  40. package/dist/playbook-adaptation.js +159 -0
  41. package/dist/playbook-curator.d.ts +16 -0
  42. package/dist/playbook-curator.js +106 -0
  43. package/dist/playbook-llm-reflector.d.ts +26 -0
  44. package/dist/playbook-llm-reflector.js +157 -0
  45. package/dist/public-surface.d.ts +2 -0
  46. package/dist/public-surface.js +26 -14
  47. package/dist/safe-edit.d.ts +20 -0
  48. package/dist/safe-edit.js +355 -78
  49. package/dist/server.d.ts +11 -2
  50. package/dist/server.js +55 -5
  51. package/dist/shared.d.ts +12 -0
  52. package/dist/shared.js +13 -1
  53. package/dist/source-mutation-coordinator.d.ts +54 -0
  54. package/dist/source-mutation-coordinator.js +259 -0
  55. package/dist/status-events.d.ts +4 -1
  56. package/dist/status-events.js +41 -6
  57. package/dist/store/ace-packed-store.d.ts +28 -9
  58. package/dist/store/ace-packed-store.js +324 -57
  59. package/dist/store/bgzf.d.ts +4 -0
  60. package/dist/store/bgzf.js +94 -0
  61. package/dist/store/bootstrap-store.d.ts +4 -1
  62. package/dist/store/bootstrap-store.js +57 -3
  63. package/dist/store/cache-workspace.d.ts +8 -0
  64. package/dist/store/cache-workspace.js +67 -1
  65. package/dist/store/crc32.d.ts +2 -0
  66. package/dist/store/crc32.js +16 -0
  67. package/dist/store/fsck.d.ts +16 -0
  68. package/dist/store/fsck.js +240 -0
  69. package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
  70. package/dist/store/materializers/context-snapshot-materializer.js +4 -1
  71. package/dist/store/materializers/projection-manager.d.ts +7 -0
  72. package/dist/store/materializers/projection-manager.js +37 -0
  73. package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
  74. package/dist/store/repositories/goal-repository.d.ts +112 -0
  75. package/dist/store/repositories/goal-repository.js +157 -0
  76. package/dist/store/repositories/playbook-repository.d.ts +107 -0
  77. package/dist/store/repositories/playbook-repository.js +314 -0
  78. package/dist/store/store-artifacts.js +44 -2
  79. package/dist/store/store-authority-audit.js +1 -0
  80. package/dist/store/store-cli.d.ts +30 -0
  81. package/dist/store/store-cli.js +242 -0
  82. package/dist/store/store-snapshot.d.ts +12 -0
  83. package/dist/store/store-snapshot.js +74 -11
  84. package/dist/store/types.d.ts +1 -0
  85. package/dist/store/write-coordinator.d.ts +7 -0
  86. package/dist/store/write-coordinator.js +11 -0
  87. package/dist/todo-state.d.ts +21 -0
  88. package/dist/todo-state.js +150 -53
  89. package/dist/tools-agent.js +86 -22
  90. package/dist/tools-discovery.js +5 -3
  91. package/dist/tools-files.js +39 -18
  92. package/dist/tools-framework.js +31 -3
  93. package/dist/tools-goal.d.ts +11 -0
  94. package/dist/tools-goal.js +108 -0
  95. package/dist/tools-handoff.js +7 -11
  96. package/dist/tools-lifecycle.js +2 -0
  97. package/dist/tools-scheduler.js +44 -1
  98. package/dist/tools-skills.js +3 -2
  99. package/dist/tools-todo.js +64 -3
  100. package/dist/tools.js +2 -0
  101. package/dist/tui/chat.js +4 -0
  102. package/dist/tui/index.js +4 -1
  103. package/dist/tui/ollama.d.ts +2 -0
  104. package/dist/tui/openai-compatible.d.ts +14 -0
  105. package/dist/tui/openai-compatible.js +15 -2
  106. package/dist/tui/provider-discovery.d.ts +1 -0
  107. package/dist/tui/provider-discovery.js +2 -2
  108. package/docs/cli-reference.md +125 -0
  109. package/docs/development.md +46 -0
  110. package/docs/evaluation-safety.md +27 -0
  111. package/docs/experience-playbook.md +35 -0
  112. package/docs/hermes.md +129 -0
  113. package/docs/library-api.md +31 -0
  114. package/docs/mcp-tool-reference.md +290 -0
  115. package/docs/provider-security.md +85 -0
  116. package/docs/state-model.md +113 -0
  117. package/docs/troubleshooting.md +95 -0
  118. package/docs/tui-operator-guide.md +75 -0
  119. package/package.json +3 -1
  120. package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
@@ -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
  ],
@@ -6,7 +6,7 @@ import { spawnSync } from "node:child_process";
6
6
  import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
7
7
  import { dirname, isAbsolute, relative, resolve } from "node:path";
8
8
  import { z } from "zod";
9
- import { ACE_TASKS_ROOT_REL, normalizePathForValidation, safeRead, safeWriteAsync, safeWrite, resolveWorkspaceWritePath, resolveStoreFallbackKeysForPath, wsPath, WORKSPACE_ROOT, } from "./helpers.js";
9
+ import { ACE_TASKS_ROOT_REL, normalizePathForValidation, isReadError, safeRead, safeWriteAsync, safeWrite, resolveWorkspaceWritePath, resolveStoreFallbackKeysForPath, wsPath, WORKSPACE_ROOT, } from "./helpers.js";
10
10
  import { isInside, looksLikeSwarmHandoffPath, normalizeRelPath } from "./shared.js";
11
11
  import { validateAgentStateHandoffPayload, validateArtifactManifestPayload, validateProvenanceLogContent, validateStatusEventsNdjsonContent, validateSwarmHandoffPayload, validateTealConfigContent, validateRuntimeExecutorSessionRegistryPayload, validateRuntimeToolSpecRegistryPayload, validateTrackerSnapshotPayload, validateVericifyBridgeSnapshotPayload, validateVericifyProcessPostLogPayload, validateWorkspaceSessionRegistryPayload, lintHandoffPayload, } from "./schemas.js";
12
12
  import { syncTodoStateSafe } from "./todo-state.js";
@@ -17,6 +17,7 @@ import { safeEditFile, diffContents, applyPatch } from "./safe-edit.js";
17
17
  import { detectAstgrepCommand, flattenMetaVariables, locateAstgrepMatches, runAstgrepQuery, } from "./astgrep-index.js";
18
18
  import { readRuntimeProfile, resolveEffectiveSurgicalReadBudget, validateRuntimeProfileContent, } from "./runtime-profile.js";
19
19
  import { withLocalModelRuntimeRepository } from "./store/repositories/local-model-runtime-repository.js";
20
+ import { listStoreKeysSync } from "./store/store-snapshot.js";
20
21
  function reasonCodeFromError(error) {
21
22
  return typeof error === "object" && error !== null && "reason_code" in error
22
23
  ? String(error.reason_code)
@@ -129,13 +130,22 @@ async function storeLocateArtifact(input, result) {
129
130
  });
130
131
  }
131
132
  function listStructuralEditArtifacts(prefix) {
133
+ const relPaths = new Set();
134
+ const storePrefix = `state/artifacts/${STRUCTURAL_EDIT_ARTIFACTS_REL}/${prefix}-`;
135
+ for (const key of listStoreKeysSync(WORKSPACE_ROOT, storePrefix)) {
136
+ if (key.endsWith(".json")) {
137
+ relPaths.add(key.slice("state/artifacts/".length));
138
+ }
139
+ }
132
140
  const dir = wsPath(STRUCTURAL_EDIT_ARTIFACTS_REL);
133
- if (!existsSync(dir))
134
- return [];
135
- return readdirSync(dir)
136
- .filter((name) => name.startsWith(`${prefix}-`) && name.endsWith(".json"))
137
- .sort((a, b) => b.localeCompare(a))
138
- .map((name) => normalizeRelPath(relative(WORKSPACE_ROOT, resolve(dir, name))));
141
+ if (existsSync(dir)) {
142
+ for (const name of readdirSync(dir)) {
143
+ if (name.startsWith(`${prefix}-`) && name.endsWith(".json")) {
144
+ relPaths.add(normalizeRelPath(relative(WORKSPACE_ROOT, resolve(dir, name))));
145
+ }
146
+ }
147
+ }
148
+ return [...relPaths].sort((a, b) => b.localeCompare(a));
139
149
  }
140
150
  function findStoredMatch(matchId) {
141
151
  for (const relPath of listStructuralEditArtifacts("locate")) {
@@ -493,6 +503,7 @@ async function buildStructuralPreview(plan) {
493
503
  : diffProcess.status === 0
494
504
  ? ""
495
505
  : diff.diff_summary;
506
+ const hasUnifiedDiff = diffProcess.status === 1;
496
507
  return {
497
508
  artifact: await finalize({
498
509
  ok: true,
@@ -505,7 +516,7 @@ async function buildStructuralPreview(plan) {
505
516
  changed_ranges: parseChangedRanges(diffText),
506
517
  diff_summary: diff.diff_summary,
507
518
  diff_preview: diffText ? previewDiffText(diffText) : diff.diff_summary,
508
- promotable: diff.has_diff,
519
+ promotable: hasUnifiedDiff,
509
520
  staging_path: stagingDir,
510
521
  }),
511
522
  rewritten_content: rewrittenContent,
@@ -1134,10 +1145,15 @@ export function registerFileTools(server) {
1134
1145
  .boolean()
1135
1146
  .optional()
1136
1147
  .describe("Keep .ace-backup copy (default: true)"),
1137
- }, async ({ path, content, validation_command, test_command, backup }) => {
1148
+ expected_base_hash: z
1149
+ .string()
1150
+ .optional()
1151
+ .describe("Optional source precondition checked immediately before promotion; prefer full sha256:<hex>"),
1152
+ }, async ({ path, content, validation_command, test_command, backup, expected_base_hash }) => {
1138
1153
  const result = safeEditFile({
1139
1154
  path,
1140
1155
  content,
1156
+ expected_base_hash,
1141
1157
  validation_command,
1142
1158
  test_command,
1143
1159
  backup,
@@ -1154,6 +1170,9 @@ export function registerFileTools(server) {
1154
1170
  original_hash: result.original_hash,
1155
1171
  new_hash: result.new_hash,
1156
1172
  restored: result.restored,
1173
+ reason_code: result.reason_code,
1174
+ expected_hash: result.expected_hash,
1175
+ observed_hash: result.observed_hash,
1157
1176
  validation_passed: result.validation_passed,
1158
1177
  test_passed: result.test_passed,
1159
1178
  },
@@ -1215,19 +1234,19 @@ export function registerFileTools(server) {
1215
1234
  };
1216
1235
  });
1217
1236
  // ── Diff (new P2 tool) ────────────────────────────────────────────
1218
- server.tool("diff_files", "Compute a line-based diff between two file contents or file paths", {
1237
+ server.tool("diff_files", "Produce a sequence-aware unified diff between two contents or two workspace paths", {
1219
1238
  original: z.string().describe("Original content or workspace-relative file path"),
1220
1239
  modified: z.string().describe("Modified content or workspace-relative file path"),
1221
- is_path: z
1222
- .boolean()
1223
- .optional()
1224
- .describe("If true, treat original/modified as file paths and read their contents"),
1225
- }, async ({ original, modified, is_path }) => {
1240
+ mode: z.enum(["content", "path"]).default("content").describe("Explicitly interpret both inputs as literal content or workspace-relative paths"),
1241
+ }, async ({ original, modified, mode }) => {
1226
1242
  let origContent = original;
1227
1243
  let modContent = modified;
1228
- if (is_path) {
1244
+ if (mode === "path") {
1229
1245
  origContent = safeRead(original);
1230
1246
  modContent = safeRead(modified);
1247
+ if (isReadError(origContent) || isReadError(modContent)) {
1248
+ throw new Error(`Unable to read diff path: ${isReadError(origContent) ? origContent : modContent}`);
1249
+ }
1231
1250
  }
1232
1251
  const diff = diffContents(origContent, modContent);
1233
1252
  return {
@@ -1660,6 +1679,7 @@ export function registerFileTools(server) {
1660
1679
  const safeResult = safeEditFile({
1661
1680
  path: compiledPlan.target.file,
1662
1681
  content: preview.rewritten_content,
1682
+ expected_base_hash: compiledPlan.target.file_hash,
1663
1683
  validation_command: validation_command ?? compiledPlan.validation_command,
1664
1684
  test_command: test_command ?? compiledPlan.test_command,
1665
1685
  });
@@ -1707,10 +1727,11 @@ export function registerFileTools(server) {
1707
1727
  server.tool("apply_patch", "Apply a unified diff patch to a workspace file. Applies the patch to staged content, runs optional validation/tests, and promotes to the target only on success. Cost: moderate.", {
1708
1728
  path: z.string().describe("Workspace-relative file path to patch"),
1709
1729
  patch: z.string().describe("Unified diff patch (output of diff -u or similar)"),
1730
+ expected_base_hash: z.string().optional().describe("Optional source precondition checked immediately before promotion; prefer full sha256:<hex>"),
1710
1731
  validation_command: z.string().optional().describe("Shell command to validate after applying (e.g. 'npx tsc --noEmit')"),
1711
1732
  test_command: z.string().optional().describe("Shell command to run tests after applying"),
1712
- }, async ({ path: relPath, patch, validation_command, test_command }) => {
1713
- const result = applyPatch({ path: relPath, patch, validation_command, test_command });
1733
+ }, async ({ path: relPath, patch, expected_base_hash, validation_command, test_command }) => {
1734
+ const result = applyPatch({ path: relPath, patch, expected_base_hash, validation_command, test_command });
1714
1735
  if (!result.ok) {
1715
1736
  return {
1716
1737
  content: [{ type: "text", text: `apply_patch failed: ${result.error ?? "unknown error"}\n${result.validation_output ?? ""}` }],
@@ -18,6 +18,8 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
18
18
  import { resolve } from "node:path";
19
19
  import { auditPublicSurface } from "./public-surface.js";
20
20
  import { auditStoreAuthority, writeStoreAuthorityAuditReport, } from "./store/store-authority-audit.js";
21
+ import { openStore } from "./store/ace-packed-store.js";
22
+ import { getWorkspaceStorePath } from "./store/store-snapshot.js";
21
23
  import { parseGateManifest, resolveGateSelection, } from "./gate-contract.js";
22
24
  import { PROVENANCE_CRITICAL_EVENT_TYPES, validateArtifactManifestPayload, validateProvenanceLogContent, validateTealConfigContent, } from "./schemas.js";
23
25
  import { readAceTaskContractAssessment } from "./ace-autonomy.js";
@@ -1199,6 +1201,27 @@ export function registerFrameworkTools(server) {
1199
1201
  ok: isGitRepo(),
1200
1202
  detail: isGitRepo() ? "initialized" : "NOT a git repo",
1201
1203
  });
1204
+ // Check: malformed store events were not silently dropped.
1205
+ const storePath = getWorkspaceStorePath(resolveWorkspaceRoot());
1206
+ if (existsSync(storePath)) {
1207
+ try {
1208
+ const store = await openStore(storePath, { readOnly: true });
1209
+ const skipped = store.skippedMalformedEventCount;
1210
+ await store.close();
1211
+ checks.push({
1212
+ name: "acepack:skipped-malformed-events",
1213
+ ok: skipped === 0,
1214
+ detail: skipped === 0 ? "0 skipped malformed events" : `${skipped} skipped malformed events`,
1215
+ });
1216
+ }
1217
+ catch (error) {
1218
+ checks.push({
1219
+ name: "acepack:skipped-malformed-events",
1220
+ ok: false,
1221
+ detail: `store validation failed: ${error instanceof Error ? error.message : String(error)}`,
1222
+ });
1223
+ }
1224
+ }
1202
1225
  // ── Deep checks (P2 hardening) ─────────────────────────────────
1203
1226
  // Check: module registry role coverage vs event emitters
1204
1227
  const registryRaw = safeRead("agent-state/MODULES/registry.json");
@@ -1368,7 +1391,9 @@ export function registerFrameworkTools(server) {
1368
1391
  result.ok
1369
1392
  ? `✅ Public surface audit passed (${result.summary.tools} tools, ${result.summary.resources} resources, ${result.summary.prompts} prompts)`
1370
1393
  : `❌ Public surface audit failed (${result.failures.length} failure${result.failures.length === 1 ? "" : "s"})`,
1371
- `Report: ${result.report_path}`,
1394
+ `Artifact: ${result.artifact_path}`,
1395
+ `Read: ace read ${result.artifact_path}`,
1396
+ ...(result.written_path ? [`Store path: ${result.written_path}`] : []),
1372
1397
  `Events: ${result.summary.emitted_events}`,
1373
1398
  ...(result.failures.slice(0, 12).map((entry) => `- ${entry}`)),
1374
1399
  ...(result.warnings.slice(0, 12).map((entry) => `- warning: ${entry}`)),
@@ -1389,8 +1414,9 @@ export function registerFrameworkTools(server) {
1389
1414
  }, async ({ write_artifact, artifact_path }) => {
1390
1415
  const result = auditStoreAuthority();
1391
1416
  const reportPath = artifact_path ?? result.report_path;
1417
+ let writtenPath;
1392
1418
  if (write_artifact !== false) {
1393
- writeStoreAuthorityAuditReport({
1419
+ writtenPath = writeStoreAuthorityAuditReport({
1394
1420
  ...result,
1395
1421
  report_path: reportPath,
1396
1422
  }, reportPath);
@@ -1401,7 +1427,9 @@ export function registerFrameworkTools(server) {
1401
1427
  type: "text",
1402
1428
  text: [
1403
1429
  `✅ Store authority audit completed (${result.total_safe_write_sites} safeWrite users)`,
1404
- `Report: ${reportPath}`,
1430
+ `Artifact: ${reportPath}`,
1431
+ `Read: ace read ${reportPath}`,
1432
+ ...(writtenPath ? [`Store path: ${writtenPath}`] : []),
1405
1433
  `Bypassing ACEPACK: ${result.bypassing_sites.length}`,
1406
1434
  `Runtime files without fallback keys: ${result.runtime_files_without_store_fallback_keys.length}`,
1407
1435
  `Non-regenerable projections: ${result.non_regenerable_projections.length}`,