@voybio/ace-swarm 0.2.4 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +20 -13
  3. package/assets/.agents/skills/eval-harness/SKILL.md +14 -0
  4. package/assets/.agents/skills/handoff-lint/SKILL.md +14 -0
  5. package/assets/.agents/skills/incident-commander/SKILL.md +14 -0
  6. package/assets/.agents/skills/memory-curator/SKILL.md +14 -0
  7. package/assets/.agents/skills/release-sentry/SKILL.md +14 -0
  8. package/assets/.agents/skills/risk-quant/SKILL.md +14 -0
  9. package/assets/.agents/skills/schema-forge/SKILL.md +14 -0
  10. package/assets/.agents/skills/state-auditor/SKILL.md +14 -0
  11. package/assets/agent-state/EVIDENCE_LOG.md +1 -1
  12. package/assets/agent-state/MODULES/gates/gate-correctness.json +1 -1
  13. package/assets/agent-state/MODULES/roles/capability-framework.json +41 -0
  14. package/assets/agent-state/MODULES/roles/capability-git.json +33 -0
  15. package/assets/agent-state/MODULES/roles/capability-safety.json +37 -0
  16. package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +21 -0
  17. package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +43 -0
  18. package/assets/agent-state/MODULES/schemas/WORKSPACE_SESSION_REGISTRY.schema.json +11 -0
  19. package/assets/agent-state/STATUS.md +2 -2
  20. package/assets/scripts/ace-hook-dispatch.mjs +70 -6
  21. package/assets/scripts/render-mcp-configs.sh +19 -5
  22. package/dist/ace-context.js +22 -1
  23. package/dist/ace-server-instructions.js +3 -3
  24. package/dist/ace-state-resolver.js +5 -3
  25. package/dist/astgrep-index.d.ts +9 -1
  26. package/dist/astgrep-index.js +14 -3
  27. package/dist/cli.js +52 -20
  28. package/dist/handoff-registry.js +5 -5
  29. package/dist/helpers/artifacts.d.ts +19 -0
  30. package/dist/helpers/artifacts.js +152 -0
  31. package/dist/helpers/bootstrap.d.ts +24 -0
  32. package/dist/helpers/bootstrap.js +894 -0
  33. package/dist/helpers/constants.d.ts +53 -0
  34. package/dist/helpers/constants.js +288 -0
  35. package/dist/helpers/drift.d.ts +13 -0
  36. package/dist/helpers/drift.js +45 -0
  37. package/dist/helpers/path-utils.d.ts +17 -0
  38. package/dist/helpers/path-utils.js +104 -0
  39. package/dist/helpers/store-resolution.d.ts +19 -0
  40. package/dist/helpers/store-resolution.js +301 -0
  41. package/dist/helpers/workspace-root.d.ts +3 -0
  42. package/dist/helpers/workspace-root.js +80 -0
  43. package/dist/helpers.d.ts +8 -123
  44. package/dist/helpers.js +8 -1747
  45. package/dist/job-scheduler.js +3 -3
  46. package/dist/local-model-runtime.js +12 -1
  47. package/dist/model-bridge.d.ts +7 -0
  48. package/dist/model-bridge.js +75 -5
  49. package/dist/orchestrator-supervisor.d.ts +14 -0
  50. package/dist/orchestrator-supervisor.js +72 -1
  51. package/dist/run-ledger.js +3 -3
  52. package/dist/runtime-command.d.ts +8 -0
  53. package/dist/runtime-command.js +38 -6
  54. package/dist/runtime-executor.d.ts +14 -0
  55. package/dist/runtime-executor.js +669 -171
  56. package/dist/runtime-profile.d.ts +32 -0
  57. package/dist/runtime-profile.js +89 -13
  58. package/dist/runtime-tool-specs.d.ts +21 -0
  59. package/dist/runtime-tool-specs.js +78 -3
  60. package/dist/safe-edit.d.ts +7 -0
  61. package/dist/safe-edit.js +163 -37
  62. package/dist/schemas.js +19 -0
  63. package/dist/shared.d.ts +2 -2
  64. package/dist/status-events.js +9 -6
  65. package/dist/store/ace-packed-store.d.ts +3 -2
  66. package/dist/store/ace-packed-store.js +188 -110
  67. package/dist/store/bootstrap-store.d.ts +1 -1
  68. package/dist/store/bootstrap-store.js +94 -81
  69. package/dist/store/cache-workspace.d.ts +22 -0
  70. package/dist/store/cache-workspace.js +149 -0
  71. package/dist/store/materializers/context-snapshot-materializer.js +6 -7
  72. package/dist/store/materializers/hook-context-materializer.d.ts +6 -9
  73. package/dist/store/materializers/hook-context-materializer.js +11 -21
  74. package/dist/store/materializers/host-file-materializer.js +6 -0
  75. package/dist/store/materializers/projection-manager.d.ts +0 -1
  76. package/dist/store/materializers/projection-manager.js +5 -13
  77. package/dist/store/materializers/scheduler-projection-materializer.js +1 -1
  78. package/dist/store/materializers/vericify-projector.d.ts +7 -7
  79. package/dist/store/materializers/vericify-projector.js +11 -11
  80. package/dist/store/repositories/local-model-runtime-repository.d.ts +120 -3
  81. package/dist/store/repositories/local-model-runtime-repository.js +242 -6
  82. package/dist/store/skills-install.d.ts +4 -0
  83. package/dist/store/skills-install.js +21 -12
  84. package/dist/store/state-reader.d.ts +2 -0
  85. package/dist/store/state-reader.js +20 -0
  86. package/dist/store/store-artifacts.d.ts +7 -0
  87. package/dist/store/store-artifacts.js +27 -1
  88. package/dist/store/store-authority-audit.d.ts +18 -1
  89. package/dist/store/store-authority-audit.js +115 -5
  90. package/dist/store/store-snapshot.d.ts +3 -0
  91. package/dist/store/store-snapshot.js +22 -2
  92. package/dist/store/workspace-store-paths.d.ts +39 -0
  93. package/dist/store/workspace-store-paths.js +94 -0
  94. package/dist/store/write-coordinator.d.ts +65 -0
  95. package/dist/store/write-coordinator.js +386 -0
  96. package/dist/todo-state.js +5 -5
  97. package/dist/tools-agent.js +319 -34
  98. package/dist/tools-discovery.js +1 -1
  99. package/dist/tools-files.d.ts +7 -0
  100. package/dist/tools-files.js +299 -10
  101. package/dist/tools-framework.js +107 -27
  102. package/dist/tools-handoff.js +2 -2
  103. package/dist/tools-lifecycle.js +4 -4
  104. package/dist/tools-memory.js +6 -6
  105. package/dist/tools-todo.js +2 -2
  106. package/dist/tracker-adapters.d.ts +1 -1
  107. package/dist/tracker-adapters.js +13 -18
  108. package/dist/tracker-sync.js +5 -3
  109. package/dist/tui/agent-runner.js +3 -1
  110. package/dist/tui/chat.js +103 -7
  111. package/dist/tui/dashboard.d.ts +1 -0
  112. package/dist/tui/dashboard.js +43 -0
  113. package/dist/tui/layout.d.ts +20 -0
  114. package/dist/tui/layout.js +31 -1
  115. package/dist/tui/local-model-contract.d.ts +6 -2
  116. package/dist/tui/local-model-contract.js +16 -3
  117. package/dist/vericify-bridge.d.ts +5 -0
  118. package/dist/vericify-bridge.js +27 -3
  119. package/dist/workspace-manager.d.ts +30 -3
  120. package/dist/workspace-manager.js +257 -27
  121. package/package.json +1 -2
  122. package/dist/internal-tool-runtime.d.ts +0 -21
  123. package/dist/internal-tool-runtime.js +0 -136
  124. package/dist/store/workspace-snapshot.d.ts +0 -26
  125. package/dist/store/workspace-snapshot.js +0 -107
@@ -1,18 +1,66 @@
1
1
  /**
2
2
  * File operation tool registrations + new safe-edit and diff tools.
3
3
  */
4
- import { readdirSync } from "node:fs";
5
- import { isAbsolute, relative } from "node:path";
4
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
6
6
  import { z } from "zod";
7
- import { ACE_TASKS_ROOT_REL, normalizePathForValidation, safeRead, safeWrite, wsPath, WORKSPACE_ROOT, } from "./helpers.js";
8
- import { looksLikeSwarmHandoffPath } from "./shared.js";
7
+ import { ACE_TASKS_ROOT_REL, normalizePathForValidation, safeRead, safeWriteAsync, safeWrite, resolveStoreFallbackKeysForPath, wsPath, WORKSPACE_ROOT, } from "./helpers.js";
8
+ import { isInside, looksLikeSwarmHandoffPath, normalizeRelPath } from "./shared.js";
9
9
  import { validateAgentStateHandoffPayload, validateArtifactManifestPayload, validateProvenanceLogContent, validateStatusEventsNdjsonContent, validateSwarmHandoffPayload, validateTealConfigContent, validateRuntimeExecutorSessionRegistryPayload, validateRuntimeToolSpecRegistryPayload, validateTrackerSnapshotPayload, validateVericifyBridgeSnapshotPayload, validateVericifyProcessPostLogPayload, validateWorkspaceSessionRegistryPayload, lintHandoffPayload, } from "./schemas.js";
10
10
  import { syncTodoStateSafe } from "./todo-state.js";
11
- import { validateRuntimeProfileContent } from "./runtime-profile.js";
12
11
  import { shouldAutoRefreshKanbanForPath, refreshKanbanArtifacts, } from "./kanban.js";
13
12
  import { appendRunLedgerEntrySafe } from "./run-ledger.js";
14
13
  import { appendStatusEventSafe } from "./status-events.js";
15
- import { safeEditFile, diffContents } from "./safe-edit.js";
14
+ import { safeEditFile, diffContents, applyPatch } from "./safe-edit.js";
15
+ import { readRuntimeProfile, resolveEffectiveSurgicalReadBudget, validateRuntimeProfileContent, } from "./runtime-profile.js";
16
+ import { withLocalModelRuntimeRepository } from "./store/repositories/local-model-runtime-repository.js";
17
+ export function planAstgrepRewriteTargets(files) {
18
+ const affected = [...new Set(files.filter(Boolean))].sort((a, b) => a.localeCompare(b));
19
+ if (affected.length > 1) {
20
+ return {
21
+ ok: false,
22
+ affected_files: affected,
23
+ error: `astgrep_rewrite refuses multi-file rewrites (${affected.length} files): ${affected.slice(0, 5).join(", ")}`,
24
+ };
25
+ }
26
+ return {
27
+ ok: true,
28
+ affected_files: affected,
29
+ target_file: affected[0],
30
+ };
31
+ }
32
+ async function appendAstgrepRewriteTransition(sessionId, to, reason, evidenceRefs) {
33
+ if (!sessionId)
34
+ return;
35
+ await withLocalModelRuntimeRepository(WORKSPACE_ROOT, async (repo) => {
36
+ await repo.appendTransitionRecord({
37
+ subject_kind: "plan_step",
38
+ subject_id: `${sessionId}/astgrep_rewrite`,
39
+ from: "staged",
40
+ to,
41
+ reason,
42
+ reason_code: to === "promoted" ? "surgical_rewrite" : "surgical_rewrite_failed",
43
+ evidence_refs: evidenceRefs,
44
+ });
45
+ }).catch(() => undefined);
46
+ }
47
+ function astgrepFileToWorkspaceRel(file) {
48
+ const abs = isAbsolute(file) ? file : resolve(WORKSPACE_ROOT, file);
49
+ if (!isInside(WORKSPACE_ROOT, abs))
50
+ return undefined;
51
+ return normalizeRelPath(relative(WORKSPACE_ROOT, abs));
52
+ }
53
+ async function resolveReadFileLinesBudget() {
54
+ let modelClass;
55
+ const statuses = await withLocalModelRuntimeRepository(WORKSPACE_ROOT, async (repo) => repo.listRuntimeStatuses()).catch(() => undefined);
56
+ modelClass = statuses?.[0]?.model_class;
57
+ const profile = readRuntimeProfile();
58
+ const resolved = resolveEffectiveSurgicalReadBudget(profile, modelClass);
59
+ return {
60
+ ...resolved,
61
+ source: modelClass ? "runtime-status" : "runtime-profile",
62
+ };
63
+ }
16
64
  export function registerFileTools(server) {
17
65
  // ── Append-only artifact protection ───────────────────────────────
18
66
  // These files must only grow; overwrites from the generic write tool
@@ -26,12 +74,12 @@ export function registerFileTools(server) {
26
74
  "agent-state/PROVENANCE_LOG.md",
27
75
  ]);
28
76
  // ── Core file operations ──────────────────────────────────────────
29
- server.tool("read_workspace_file", "Read any workspace file by relative path", {
77
+ server.tool("read_workspace_file", "Read any workspace file by relative path. Prefer outline_file for discovery, read_file_lines for known regions, astgrep_query for symbol lookup. For small_local runs, this is the fallback after surgical reads are insufficient. Cost: heavy for large files.", {
30
78
  path: z.string().describe("Relative path from workspace root"),
31
79
  }, async ({ path }) => ({
32
80
  content: [{ type: "text", text: safeRead(path) }],
33
81
  }));
34
- server.tool("write_workspace_file", "Write or update a workspace file", {
82
+ server.tool("write_workspace_file", "Write or update a workspace file. Prefer apply_patch for targeted edits, astgrep_rewrite for structural rewrites. Cost: heavy.", {
35
83
  path: z.string().describe("Relative path from workspace root"),
36
84
  content: z.string().describe("File content"),
37
85
  }, async ({ path, content }) => {
@@ -475,8 +523,10 @@ export function registerFileTools(server) {
475
523
  const synced = await syncTodoStateSafe(content);
476
524
  todoStateSuffix = `\nTODO state synced: ${synced.path}`;
477
525
  }
526
+ const storeFallbackKeys = resolveStoreFallbackKeysForPath(normalizedPath);
527
+ const useAsyncStoreAdmission = storeFallbackKeys.length > 0;
478
528
  // Keep the file as a materialized projection after the canonical store update.
479
- const abs = safeWrite(path, content);
529
+ const abs = useAsyncStoreAdmission ? await safeWriteAsync(path, content) : safeWrite(path, content);
480
530
  let kanbanSuffix = "";
481
531
  const shouldRefreshKanban = shouldAutoRefreshKanbanForPath(normalizedPath);
482
532
  if (shouldRefreshKanban) {
@@ -544,7 +594,7 @@ export function registerFileTools(server) {
544
594
  }
545
595
  });
546
596
  // ── Safe-edit (new P0 tool) ───────────────────────────────────────
547
- server.tool("safe_edit_file", "Edit a file using copy→validate→swap pattern. Automatically rolls back if validation or tests fail.", {
597
+ server.tool("safe_edit_file", "Edit a file using copy→validate→swap pattern. Automatically rolls back if validation or tests fail. Prefer apply_patch for small targeted edits. Cost: heavy for large files.", {
548
598
  path: z.string().describe("Workspace-relative file path to edit"),
549
599
  content: z.string().describe("New file content"),
550
600
  validation_command: z
@@ -676,5 +726,244 @@ export function registerFileTools(server) {
676
726
  ],
677
727
  };
678
728
  });
729
+ // ── Surgical read / query / patch tools ──────────────────────────
730
+ server.tool("read_file_lines", "Read a specific line range from a workspace file. Prefer this over read_workspace_file for large files. Cost: moderate.", {
731
+ path: z.string().describe("Workspace-relative file path"),
732
+ start_line: z.number().int().min(1).describe("First line to read (1-indexed)"),
733
+ end_line: z.number().int().min(1).describe("Last line to read (inclusive)"),
734
+ symbol_anchor: z.string().optional().describe("Optional symbol name hint for context (informational only)"),
735
+ }, async ({ path: relPath, start_line, end_line, symbol_anchor }) => {
736
+ const budget = await resolveReadFileLinesBudget();
737
+ const requestedLines = end_line - start_line + 1;
738
+ const budgetText = budget.read_file_lines_max_lines === null
739
+ ? "unbounded"
740
+ : String(budget.read_file_lines_max_lines);
741
+ const headerLines = [
742
+ `# Budget: model_class=${budget.model_class}; read_file_lines_max_lines=${budgetText}`,
743
+ `# Budget source: ${budget.source}`,
744
+ ];
745
+ if (budget.read_file_lines_max_lines !== null &&
746
+ requestedLines > budget.read_file_lines_max_lines) {
747
+ return {
748
+ content: [
749
+ {
750
+ type: "text",
751
+ text: [
752
+ ...headerLines,
753
+ `Error: requested range (${requestedLines} lines) exceeds ${budget.model_class} budget of ${budgetText} lines.`,
754
+ "Use outline_file for discovery or astgrep_query for symbol lookup.",
755
+ ].join("\n"),
756
+ },
757
+ ],
758
+ isError: true,
759
+ };
760
+ }
761
+ const content = safeRead(relPath);
762
+ const lines = content.split("\n");
763
+ const slice = lines.slice(start_line - 1, end_line);
764
+ const result = slice.map((l, i) => `${start_line + i}: ${l}`).join("\n");
765
+ return {
766
+ content: [
767
+ {
768
+ type: "text",
769
+ text: [
770
+ ...headerLines,
771
+ `# ${relPath} (lines ${start_line}–${end_line})`,
772
+ symbol_anchor ? `# Symbol context: ${symbol_anchor}` : "",
773
+ "",
774
+ result,
775
+ ]
776
+ .filter(Boolean)
777
+ .join("\n"),
778
+ },
779
+ ],
780
+ };
781
+ });
782
+ server.tool("outline_file", "Return symbol signatures and top-level structure of a file without bodies. Use for discovery before reading specific regions. Cost: cheap.", {
783
+ path: z.string().describe("Workspace-relative file path"),
784
+ }, async ({ path: relPath }) => {
785
+ const content = safeRead(relPath);
786
+ const lines = content.split("\n");
787
+ const outlinePatterns = [
788
+ /^export\s+(async\s+)?function\s+\w+/,
789
+ /^export\s+(class|interface|type|enum|const|abstract)\s+\w+/,
790
+ /^(export\s+)?default\s+/,
791
+ /^\s+(async\s+)?(\w+\s+)?\w+\s*\([^)]*\)\s*[:{]/,
792
+ /^(function|class|interface|type|enum|const|let|var)\s+\w+/,
793
+ ];
794
+ const outline = [];
795
+ lines.forEach((line, idx) => {
796
+ if (outlinePatterns.some(p => p.test(line))) {
797
+ outline.push(`${idx + 1}: ${line.trimEnd()}`);
798
+ }
799
+ });
800
+ return {
801
+ content: [{
802
+ type: "text",
803
+ text: [
804
+ `# Outline: ${relPath} (${lines.length} lines total)`,
805
+ "",
806
+ outline.length > 0 ? outline.join("\n") : "(no symbols found)",
807
+ ].join("\n"),
808
+ }],
809
+ };
810
+ });
811
+ server.tool("astgrep_query", "Search for a structural pattern in workspace files using ast-grep. Returns matching ranges without reading whole files. Cost: cheap.", {
812
+ pattern: z.string().describe("ast-grep pattern, e.g. 'export function $NAME($$$)'"),
813
+ lang: z.string().describe("Language: ts, py, rust, go, js"),
814
+ scope: z.string().optional().describe("Workspace-relative directory to search (default: src)"),
815
+ max_results: z.number().int().min(1).max(200).optional().describe("Max results to return (default: 50)"),
816
+ }, async ({ pattern, lang, scope, max_results }) => {
817
+ const { runAstgrepQuery } = await import("./astgrep-index.js");
818
+ const root = wsPath(scope ?? "src");
819
+ const matches = runAstgrepQuery(pattern, lang, [root]);
820
+ const limited = matches.slice(0, max_results ?? 50);
821
+ const lines = limited.map((m) => `${m.file}:${m.line}: ${m.text}`);
822
+ return {
823
+ content: [{
824
+ type: "text",
825
+ text: [
826
+ `# astgrep_query: ${pattern} (${lang})`,
827
+ `# Found ${matches.length} matches, showing ${limited.length}`,
828
+ "",
829
+ ...lines,
830
+ ].join("\n"),
831
+ }],
832
+ };
833
+ });
834
+ server.tool("astgrep_rewrite", "Apply a structural rewrite to one workspace file using ast-grep pattern + replacement. Multi-file rewrites are refused; staged through safe_edit_file. Emits transition-compatible result details. Cost: heavy.", {
835
+ pattern: z.string().describe("ast-grep pattern to match"),
836
+ rewrite: z.string().describe("Replacement template (use $NAME etc. from pattern)"),
837
+ lang: z.string().describe("Language: ts, py, rust, go, js"),
838
+ scope: z.string().optional().describe("Workspace-relative directory (default: src)"),
839
+ confirm_multi_file: z.boolean().optional().describe("Deprecated; multi-file rewrites are always refused"),
840
+ validation_command: z.string().optional().describe("Shell command to validate the staged rewrite before promotion"),
841
+ test_command: z.string().optional().describe("Shell command to test the staged rewrite before promotion"),
842
+ session_id: z.string().optional().describe("Optional runtime session id for transition-record emission"),
843
+ }, async ({ pattern, rewrite, lang, scope, validation_command, test_command, session_id }) => {
844
+ const { runAstgrepQuery } = await import("./astgrep-index.js");
845
+ const root = wsPath(scope ?? "src");
846
+ if (!isInside(WORKSPACE_ROOT, root)) {
847
+ return {
848
+ content: [{ type: "text", text: `astgrep_rewrite failed: scope escapes workspace root (${scope})` }],
849
+ isError: true,
850
+ };
851
+ }
852
+ const matches = runAstgrepQuery(pattern, lang, [root]);
853
+ const affectedFiles = [...new Set(matches.map(m => m.file))];
854
+ const plan = planAstgrepRewriteTargets(affectedFiles);
855
+ if (!plan.ok) {
856
+ await appendAstgrepRewriteTransition(session_id, "refused", plan.error ?? "astgrep_rewrite refused the requested rewrite", plan.affected_files);
857
+ return {
858
+ content: [{
859
+ type: "text",
860
+ text: plan.error ?? "astgrep_rewrite refused the requested rewrite",
861
+ }],
862
+ isError: true,
863
+ };
864
+ }
865
+ if (matches.length === 0) {
866
+ return {
867
+ content: [{ type: "text", text: "No matches found for pattern. No files modified." }],
868
+ };
869
+ }
870
+ const targetRel = astgrepFileToWorkspaceRel(plan.target_file ?? "");
871
+ if (!targetRel) {
872
+ return {
873
+ content: [{ type: "text", text: `astgrep_rewrite failed: matched path escapes workspace root (${plan.target_file ?? ""})` }],
874
+ isError: true,
875
+ };
876
+ }
877
+ const originalContent = safeRead(targetRel);
878
+ if (originalContent.startsWith("[FILE NOT FOUND]") || originalContent.startsWith("[ACCESS DENIED]")) {
879
+ return {
880
+ content: [{ type: "text", text: `astgrep_rewrite failed: cannot read ${targetRel}\n${originalContent}` }],
881
+ isError: true,
882
+ };
883
+ }
884
+ const stagingDir = wsPath(".ace-staging", `astgrep-rewrite-${Date.now()}`);
885
+ const stagedFile = resolve(stagingDir, targetRel);
886
+ mkdirSync(dirname(stagedFile), { recursive: true });
887
+ writeFileSync(stagedFile, originalContent, "utf-8");
888
+ const { spawnSync } = await import("node:child_process");
889
+ const result = spawnSync("ast-grep", ["--pattern", pattern, "--rewrite", rewrite, "--lang", lang, stagedFile, "--update-all"], { encoding: "utf8", cwd: WORKSPACE_ROOT });
890
+ if (result.status !== 0) {
891
+ await appendAstgrepRewriteTransition(session_id, "failed", `astgrep_rewrite failed in staging: ${result.stderr || result.stdout || "unknown error"}`, [targetRel]);
892
+ return {
893
+ content: [{ type: "text", text: `astgrep rewrite failed:\n${result.stderr}` }],
894
+ isError: true,
895
+ };
896
+ }
897
+ const rewrittenContent = readFileSync(stagedFile, "utf-8");
898
+ const safeResult = safeEditFile({
899
+ path: targetRel,
900
+ content: rewrittenContent,
901
+ validation_command,
902
+ test_command,
903
+ });
904
+ if (!safeResult.ok) {
905
+ await appendAstgrepRewriteTransition(session_id, "failed", `astgrep_rewrite failed before promotion for ${targetRel}: ${safeResult.error ?? "unknown error"}`, [targetRel]);
906
+ return {
907
+ content: [{
908
+ type: "text",
909
+ text: [
910
+ `astgrep_rewrite failed before promotion`,
911
+ `Path: ${targetRel}`,
912
+ `Error: ${safeResult.error ?? "unknown error"}`,
913
+ safeResult.validation_passed !== undefined ? `Validation: ${safeResult.validation_passed ? "passed" : "FAILED"}` : "Validation: not requested",
914
+ safeResult.validation_output ? `Validation output:\n${safeResult.validation_output}` : "",
915
+ safeResult.test_passed !== undefined ? `Tests: ${safeResult.test_passed ? "passed" : "FAILED"}` : "Tests: not requested",
916
+ safeResult.test_output ? `Test output:\n${safeResult.test_output}` : "",
917
+ `Staging: ${safeResult.staging_path || stagingDir}`,
918
+ ].filter(Boolean).join("\n"),
919
+ }],
920
+ isError: true,
921
+ };
922
+ }
923
+ await appendAstgrepRewriteTransition(session_id, "promoted", `astgrep_rewrite promoted staged rewrite for ${targetRel}`, [targetRel]);
924
+ return {
925
+ content: [{
926
+ type: "text",
927
+ text: [
928
+ `# astgrep_rewrite completed`,
929
+ `Pattern: ${pattern}`,
930
+ `Rewrite: ${rewrite}`,
931
+ `Files affected: ${affectedFiles.length}`,
932
+ `Path: ${targetRel}`,
933
+ `Hash: ${safeResult.original_hash} → ${safeResult.new_hash}`,
934
+ safeResult.validation_passed !== undefined ? `Validation: ${safeResult.validation_passed ? "passed" : "failed"}` : "Validation: not requested",
935
+ safeResult.test_passed !== undefined ? `Tests: ${safeResult.test_passed ? "passed" : "failed"}` : "Tests: not requested",
936
+ `Staging: ${safeResult.staging_path}`,
937
+ result.stdout ? `\nOutput:\n${result.stdout}` : "",
938
+ ].filter(Boolean).join("\n"),
939
+ }],
940
+ };
941
+ });
942
+ 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.", {
943
+ path: z.string().describe("Workspace-relative file path to patch"),
944
+ patch: z.string().describe("Unified diff patch (output of diff -u or similar)"),
945
+ validation_command: z.string().optional().describe("Shell command to validate after applying (e.g. 'npx tsc --noEmit')"),
946
+ test_command: z.string().optional().describe("Shell command to run tests after applying"),
947
+ }, async ({ path: relPath, patch, validation_command, test_command }) => {
948
+ const result = applyPatch({ path: relPath, patch, validation_command, test_command });
949
+ if (!result.ok) {
950
+ return {
951
+ content: [{ type: "text", text: `apply_patch failed: ${result.error ?? "unknown error"}\n${result.validation_output ?? ""}` }],
952
+ isError: true,
953
+ };
954
+ }
955
+ return {
956
+ content: [{
957
+ type: "text",
958
+ text: [
959
+ `# apply_patch: ${relPath}`,
960
+ `Status: success`,
961
+ result.validation_passed !== undefined ? `Validation: ${result.validation_passed ? "passed" : "failed"}` : "",
962
+ result.test_passed !== undefined ? `Tests: ${result.test_passed ? "passed" : "failed"}` : "",
963
+ `Staging: ${result.staging_path}`,
964
+ ].filter(Boolean).join("\n"),
965
+ }],
966
+ };
967
+ });
679
968
  }
680
969
  //# sourceMappingURL=tools-files.js.map
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { z } from "zod";
6
6
  import { bootstrapStoreWorkspace } from "./store/bootstrap-store.js";
7
- import { ACE_ROOT_REL, ACE_TASKS_ROOT_REL, ALL_MCP_CLIENTS, ALL_LLM_PROVIDERS, ALL_AGENTS, COMPOSABLE_AGENTS, SWARM_AGENTS, SWARM_SUBAGENT_MAP, WORKSPACE_ROOT, classifyPathSource, detectAssetDrift, getAllMcpServerConfigSnippets, getAgentInstructionPath, getAgentManifestPath, getKernelArtifactPath, getMcpClientInstallHint, getMcpServerConfigSnippet, getTaskArtifactPath, isSwarmRole, listAvailableSkills, normalizePathForValidation, safeRead, safeWrite, withFileLock, wsPath, } from "./helpers.js";
7
+ import { ACE_ROOT_REL, ACE_TASKS_ROOT_REL, ALL_MCP_CLIENTS, ALL_LLM_PROVIDERS, ALL_AGENTS, COMPOSABLE_AGENTS, SWARM_AGENTS, SWARM_SUBAGENT_MAP, WORKSPACE_ROOT, classifyPathSource, detectAssetDrift, getAllMcpServerConfigSnippets, getAgentInstructionPath, getAgentManifestPath, getKernelArtifactPath, getMcpClientInstallHint, getMcpServerConfigSnippet, getTaskArtifactPath, isSwarmRole, listAvailableSkills, normalizePathForValidation, resolveWorkspaceRoot, safeRead, safeWriteAsync, withFileLock, wsPath, } from "./helpers.js";
8
8
  import { getRoleTitle, MCP_CLIENT_ENUM, scoreDomains, } from "./shared.js";
9
9
  import { defaultModelForProvider, } from "./tui/provider-discovery.js";
10
10
  import { refreshAstgrepIndex } from "./astgrep-index.js";
@@ -20,6 +20,21 @@ import { auditPublicSurface } from "./public-surface.js";
20
20
  import { auditStoreAuthority, writeStoreAuthorityAuditReport, } from "./store/store-authority-audit.js";
21
21
  import { PROVENANCE_CRITICAL_EVENT_TYPES, validateArtifactManifestPayload, validateProvenanceLogContent, validateTealConfigContent, } from "./schemas.js";
22
22
  import { readAceTaskContractAssessment } from "./ace-autonomy.js";
23
+ import { listStoreKeysSync, readStoreBlobSync } from "./store/store-snapshot.js";
24
+ function executionRoleForDomain(domain) {
25
+ switch (domain) {
26
+ case "venture":
27
+ return "vos";
28
+ case "ux":
29
+ return "ui";
30
+ case "engineering":
31
+ return "coders";
32
+ case "research":
33
+ return "research";
34
+ default:
35
+ return "orchestrator";
36
+ }
37
+ }
23
38
  function getArtifactManifestEntries(payload) {
24
39
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
25
40
  return [];
@@ -38,15 +53,37 @@ function getArtifactManifestEntries(payload) {
38
53
  }
39
54
  return [];
40
55
  }
56
+ function parseGateManifest(raw, sourceRef) {
57
+ try {
58
+ const gate = JSON.parse(raw);
59
+ const id = typeof gate.id === "string" ? gate.id.trim() : "";
60
+ if (!id)
61
+ return undefined;
62
+ const type = gate.type === "executable" || gate.type === "artifact_scan" || gate.type === "manual_review"
63
+ ? gate.type
64
+ : "manual_review";
65
+ return {
66
+ id,
67
+ type,
68
+ invariant: typeof gate.invariant === "string" ? gate.invariant : "",
69
+ command: typeof gate.command === "string" ? gate.command : "",
70
+ evidence_requirement: typeof gate.evidence_requirement === "string" ? gate.evidence_requirement : "",
71
+ source_ref: sourceRef,
72
+ };
73
+ }
74
+ catch {
75
+ return undefined;
76
+ }
77
+ }
41
78
  function readGateManifests(gatesDir) {
42
79
  const files = readdirSync(gatesDir).filter((f) => f.endsWith(".json"));
43
80
  const allGates = [];
44
81
  for (const file of files) {
45
82
  try {
46
83
  const raw = readFileSync(resolve(gatesDir, file), "utf-8");
47
- const gate = JSON.parse(raw);
48
- if (gate.id)
49
- allGates.push(gate);
84
+ const parsed = parseGateManifest(raw, `agent-state/MODULES/gates/${file}`);
85
+ if (parsed)
86
+ allGates.push(parsed);
50
87
  }
51
88
  catch {
52
89
  /* skip corrupt manifests */
@@ -54,6 +91,44 @@ function readGateManifests(gatesDir) {
54
91
  }
55
92
  return allGates;
56
93
  }
94
+ function readStoreGateManifests(workspaceRoot) {
95
+ const gateKeys = listStoreKeysSync(workspaceRoot, "knowledge/gates/").filter((key) => key.endsWith(".json"));
96
+ const allGates = [];
97
+ for (const key of gateKeys) {
98
+ const raw = readStoreBlobSync(workspaceRoot, key);
99
+ if (typeof raw !== "string")
100
+ continue;
101
+ const parsed = parseGateManifest(raw, key);
102
+ if (parsed)
103
+ allGates.push(parsed);
104
+ }
105
+ return allGates;
106
+ }
107
+ function resolveGateManifests(gatesDir) {
108
+ if (existsSync(gatesDir)) {
109
+ const workspaceGates = readGateManifests(gatesDir);
110
+ if (workspaceGates.length > 0) {
111
+ return { gates: workspaceGates, source: "workspace" };
112
+ }
113
+ }
114
+ const storeGates = readStoreGateManifests(resolveWorkspaceRoot());
115
+ if (storeGates.length > 0) {
116
+ return { gates: storeGates, source: "store" };
117
+ }
118
+ return { gates: [], source: "none" };
119
+ }
120
+ function hasArtifactEvidence(reference) {
121
+ const normalized = reference.trim();
122
+ if (!normalized)
123
+ return false;
124
+ const candidates = [wsPath("agent-state", normalized), wsPath(normalized)];
125
+ if (candidates.some((candidate) => existsSync(candidate)))
126
+ return true;
127
+ return candidates.some((candidate) => {
128
+ const content = safeRead(candidate);
129
+ return !content.startsWith("[FILE NOT FOUND]") && !content.startsWith("[ACCESS DENIED]");
130
+ });
131
+ }
57
132
  function evaluateGateTargets(targets) {
58
133
  const results = [];
59
134
  for (const gate of targets) {
@@ -97,7 +172,7 @@ function evaluateGateTargets(targets) {
97
172
  }
98
173
  else {
99
174
  for (const artifact of relevantArtifacts) {
100
- if (!existsSync(wsPath("agent-state", artifact))) {
175
+ if (!hasArtifactEvidence(artifact)) {
101
176
  missing.push(artifact);
102
177
  }
103
178
  }
@@ -109,11 +184,9 @@ function evaluateGateTargets(targets) {
109
184
  .map((s) => s.trim())
110
185
  .filter(Boolean);
111
186
  for (const ref of evidenceFiles) {
112
- const candidates = [wsPath("agent-state", ref), wsPath(ref)];
113
- const found = candidates.some((candidate) => existsSync(candidate));
114
- if (!found) {
187
+ if (!hasArtifactEvidence(ref)) {
115
188
  const asFile = ref.replace(/\s+/g, "_").replace(/[^a-zA-Z0-9_./-]/g, "");
116
- const fileFound = existsSync(wsPath("agent-state", asFile));
189
+ const fileFound = hasArtifactEvidence(asFile);
117
190
  if (!fileFound)
118
191
  missing.push(ref);
119
192
  }
@@ -228,7 +301,7 @@ async function appendEvidenceLogEntrySafe(review) {
228
301
  const timestamp = new Date().toISOString();
229
302
  const anchor = `#ts:${timestamp}`;
230
303
  const evidenceRef = `agent-state/EVIDENCE_LOG.md${anchor}`;
231
- return withFileLock("agent-state/EVIDENCE_LOG.md", () => {
304
+ return withFileLock("agent-state/EVIDENCE_LOG.md", async () => {
232
305
  const existing = safeRead("agent-state/EVIDENCE_LOG.md");
233
306
  const seed = existing.startsWith("[FILE NOT FOUND]") || existing.startsWith("[ACCESS DENIED]")
234
307
  ? "# EVIDENCE LOG\n\nAppend-only validation evidence.\n\n## Entries\n"
@@ -260,7 +333,7 @@ async function appendEvidenceLogEntrySafe(review) {
260
333
  })
261
334
  : [" - none"]),
262
335
  ];
263
- const path = safeWrite("agent-state/EVIDENCE_LOG.md", `${seed}${lines.join("\n")}\n`);
336
+ const path = await safeWriteAsync("agent-state/EVIDENCE_LOG.md", `${seed}${lines.join("\n")}\n`);
264
337
  return { path, evidenceRef };
265
338
  });
266
339
  }
@@ -475,6 +548,12 @@ export function registerFrameworkTools(server) {
475
548
  recommendedSubagents.add("memory");
476
549
  }
477
550
  return {
551
+ structuredContent: {
552
+ detected_domain: detected,
553
+ suggested_execution_role: executionRoleForDomain(detected),
554
+ default_entry_agent: "orchestrator",
555
+ task_contract_ok: taskContract.ok,
556
+ },
478
557
  content: [
479
558
  {
480
559
  type: "text",
@@ -576,7 +655,7 @@ export function registerFrameworkTools(server) {
576
655
  model: resolvedLlmModel ?? undefined,
577
656
  baseUrl: resolvedLlmBaseUrl ?? undefined,
578
657
  });
579
- const astIndex = refreshAstgrepIndex({
658
+ const astIndex = await refreshAstgrepIndex({
580
659
  scope: ".",
581
660
  append_evidence: true,
582
661
  emit_event: true,
@@ -603,7 +682,7 @@ export function registerFrameworkTools(server) {
603
682
  category: "major_update",
604
683
  message: "Bootstrapped ACE workspace state",
605
684
  artifacts: [
606
- `${ACE_ROOT_REL}/ace-state.ace`,
685
+ storeResult.storePath,
607
686
  `${ACE_TASKS_ROOT_REL}/todo.md`,
608
687
  normalizePathForValidation(delta.index_path),
609
688
  "agent-state/AST_GREP_INDEX.json",
@@ -1349,31 +1428,27 @@ export function registerFrameworkTools(server) {
1349
1428
  .describe("Optional short focus string persisted with skeptic adversarial review evidence."),
1350
1429
  }, async ({ gate_ids, review_mode, review_focus }) => {
1351
1430
  const gatesDir = wsPath("agent-state", "MODULES", "gates");
1352
- if (!existsSync(gatesDir)) {
1353
- return {
1354
- content: [
1355
- {
1356
- type: "text",
1357
- text: "❌ Gates directory not found: agent-state/MODULES/gates/",
1358
- },
1359
- ],
1360
- };
1361
- }
1362
- const allGates = readGateManifests(gatesDir);
1431
+ const resolved = resolveGateManifests(gatesDir);
1432
+ const allGates = resolved.gates;
1433
+ const gateEvidenceRef = resolved.source === "store" ? "knowledge/gates/*" : "agent-state/MODULES/gates/";
1363
1434
  // Filter to requested gates (or run all)
1364
1435
  const targets = gate_ids
1365
1436
  ? allGates.filter((g) => gate_ids.includes(g.id))
1366
1437
  : allGates;
1367
1438
  if (targets.length === 0) {
1439
+ const noGateMessage = allGates.length === 0
1440
+ ? "❌ No gate manifests found in agent-state/MODULES/gates/ or ace-state.ace knowledge/gates/*."
1441
+ : `❌ No matching gates found. Available: ${allGates.map((g) => g.id).join(", ")}`;
1368
1442
  return {
1369
1443
  content: [
1370
1444
  {
1371
1445
  type: "text",
1372
- text: `❌ No matching gates found. Available: ${allGates.map((g) => g.id).join(", ")}`,
1446
+ text: noGateMessage,
1373
1447
  },
1374
1448
  ],
1375
1449
  };
1376
1450
  }
1451
+ const gateArtifactRefs = targets.map((gate) => gate.source_ref ?? gateEvidenceRef);
1377
1452
  const results = evaluateGateTargets(targets);
1378
1453
  const passed = results.filter((r) => r.ok).length;
1379
1454
  const failed = results.filter((r) => !r.ok).length;
@@ -1401,7 +1476,8 @@ export function registerFrameworkTools(server) {
1401
1476
  })),
1402
1477
  passed,
1403
1478
  failed,
1404
- evidence_ref: evidence?.evidenceRef ?? "agent-state/MODULES/gates/",
1479
+ evidence_ref: evidence?.evidenceRef ?? gateEvidenceRef,
1480
+ gate_manifest_source: resolved.source,
1405
1481
  ...(review
1406
1482
  ? {
1407
1483
  review: {
@@ -1428,13 +1504,14 @@ export function registerFrameworkTools(server) {
1428
1504
  ? `Gate execution with skeptic review: ${review.summary.confirmed_count} confirmed findings`
1429
1505
  : `Gate execution: ${passed}/${results.length} passed`,
1430
1506
  artifacts: [
1431
- ...results.map((r) => `agent-state/MODULES/gates/${r.id}.json`),
1507
+ ...gateArtifactRefs,
1432
1508
  ...(evidence ? ["agent-state/EVIDENCE_LOG.md"] : []),
1433
1509
  ],
1434
1510
  metadata: {
1435
1511
  passed,
1436
1512
  failed,
1437
1513
  gate_ids: results.map((r) => r.id),
1514
+ gate_manifest_source: resolved.source,
1438
1515
  ...(review
1439
1516
  ? {
1440
1517
  review_mode: review.mode,
@@ -1460,6 +1537,9 @@ export function registerFrameworkTools(server) {
1460
1537
  : allOk
1461
1538
  ? `✅ All gates passed: ${passed}/${results.length}`
1462
1539
  : `❌ Gate failures: ${failed}/${results.length} failed`,
1540
+ `Gate manifest source: ${resolved.source === "store"
1541
+ ? "ace-state.ace (knowledge/gates/*)"
1542
+ : "agent-state/MODULES/gates/"}`,
1463
1543
  `Run ledger: ${ledger.path} (${ledger.entry.id})`,
1464
1544
  ...(evidence ? [`Evidence: ${evidence.path} (${evidence.evidenceRef})`] : []),
1465
1545
  "",
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { z } from "zod";
6
- import { ACE_TASKS_ROOT_REL, normalizePathForValidation, readTaskArtifact, safeWrite, wsPath, WORKSPACE_ROOT, } from "./helpers.js";
6
+ import { ACE_TASKS_ROOT_REL, normalizePathForValidation, readTaskArtifact, safeWriteAsync, wsPath, WORKSPACE_ROOT, } from "./helpers.js";
7
7
  import { ROLE_ENUM, HANDOFF_VALIDATION_MODE, HANDOFF_LIFECYCLE_STATUS_ENUM, } from "./shared.js";
8
8
  import { validateHandoffPayload, validateSwarmHandoffPayload, lintHandoffPayload, } from "./schemas.js";
9
9
  import { acknowledgeHandoffSafe, getHandoffRegistryPath, readHandoffRegistry, registerHandoffSafe, } from "./handoff-registry.js";
@@ -247,7 +247,7 @@ export function registerHandoffTools(server) {
247
247
  .replace("T", "-")
248
248
  .slice(0, 15);
249
249
  const filePath = wsPath("tasks", `SWARM_HANDOFF.${from}_to_${to}_${timeStr}_${shortId}.json`);
250
- safeWrite(filePath, JSON.stringify(handoff, null, 2));
250
+ await safeWriteAsync(filePath, JSON.stringify(handoff, null, 2));
251
251
  const registry = await registerHandoffSafe({
252
252
  handoff_id: handoff.handoff_id,
253
253
  from: handoff.router.from,
@@ -2,8 +2,8 @@
2
2
  * Lifecycle tool registrations: status events, circuit breaker, and kanban.
3
3
  */
4
4
  import { z } from "zod";
5
- import { safeRead, safeWrite } from "./helpers.js";
6
- import { appendStatusEventSafe, getStatusEventsPath, readStatusEvents, } from "./status-events.js";
5
+ import { safeRead, safeWriteAsync } from "./helpers.js";
6
+ import { appendStatusEventSafe, getStatusEventsPath, readStatusEvents } from "./status-events.js";
7
7
  import { refreshKanbanArtifacts } from "./kanban.js";
8
8
  export function registerLifecycleTools(server) {
9
9
  server.tool("emit_status_event", "Append a schema-valid lifecycle event into agent-state/STATUS_EVENTS.ndjson", {
@@ -119,7 +119,7 @@ export function registerLifecycleTools(server) {
119
119
  ? "# STATUS\n\n"
120
120
  : current.trimEnd();
121
121
  const next = `${seed}\n- [${timestamp}] CIRCUIT_BREAKER_OPENED: ${reason}${owner ? ` (owner: ${owner})` : ""}\n`;
122
- const statusPath = safeWrite("agent-state/STATUS.md", next);
122
+ const statusPath = await safeWriteAsync("agent-state/STATUS.md", next);
123
123
  const event = await appendStatusEventSafe({
124
124
  source_module: "capability-ops",
125
125
  event_type: "CIRCUIT_BREAKER_OPENED",
@@ -158,7 +158,7 @@ export function registerLifecycleTools(server) {
158
158
  ? "# STATUS\n\n"
159
159
  : current.trimEnd();
160
160
  const next = `${seed}\n- [${timestamp}] CIRCUIT_BREAKER_CLOSED: ${reason}\n`;
161
- const statusPath = safeWrite("agent-state/STATUS.md", next);
161
+ const statusPath = await safeWriteAsync("agent-state/STATUS.md", next);
162
162
  const event = await appendStatusEventSafe({
163
163
  source_module: "capability-ops",
164
164
  event_type: "CIRCUIT_BREAKER_CLOSED",