@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
@@ -8,6 +8,31 @@ export declare const DEFAULT_RUNTIME_TOOL_REGISTRY_REL_PATH = "agent-state/runti
8
8
  export declare const DEFAULT_EXECUTOR_MAX_TURNS = 6;
9
9
  export declare const DEFAULT_EXECUTOR_TURN_TIMEOUT_MS = 300000;
10
10
  export declare const DEFAULT_VERICIFY_BRIDGE_REL_PATH = "agent-state/vericify/ace-bridge.json";
11
+ export type RuntimeModelClass = "frontier" | "mid" | "small_local";
12
+ export declare const DEFAULT_STALL_WINDOW_MS = 300000;
13
+ export declare const DEFAULT_TURN_BUDGET_MS = 1800000;
14
+ export declare const DEFAULT_MAX_STALL_RESTARTS = 3;
15
+ export declare const DEFAULT_INITIAL_BACKOFF_MS = 5000;
16
+ export declare const DEFAULT_MAX_RETRY_BACKOFF_MS = 300000;
17
+ export interface ModelClassLivenessDefaults {
18
+ turn_budget_ms: number;
19
+ stall_window_ms: number;
20
+ max_stall_restarts: number;
21
+ initial_backoff_ms: number;
22
+ max_retry_backoff_ms: number;
23
+ }
24
+ export interface SurgicalReadBudgets {
25
+ small_local: number;
26
+ mid: number;
27
+ frontier: number | null;
28
+ }
29
+ export interface ResolvedSurgicalReadBudget {
30
+ model_class: RuntimeModelClass;
31
+ read_file_lines_max_lines: number | null;
32
+ }
33
+ export declare const DEFAULT_SURGICAL_READ_BUDGETS: SurgicalReadBudgets;
34
+ export declare function getLivenessDefaults(modelClass?: RuntimeModelClass): ModelClassLivenessDefaults;
35
+ export declare function resolveEffectiveSurgicalReadBudget(profile: RuntimeProfile | undefined, modelClass?: RuntimeModelClass): ResolvedSurgicalReadBudget;
11
36
  export declare const DEFAULT_VERICIFY_PROCESS_POST_LOG_REL_PATH = "agent-state/vericify/process-posts.json";
12
37
  declare const runtimeProfileSchema: z.ZodObject<{
13
38
  ace_runtime_version: z.ZodLiteral<"1.0.0">;
@@ -41,6 +66,11 @@ declare const runtimeProfileSchema: z.ZodObject<{
41
66
  }, z.core.$strict>;
42
67
  tools: z.ZodDefault<z.ZodOptional<z.ZodObject<{
43
68
  registry_path: z.ZodDefault<z.ZodOptional<z.ZodString>>;
69
+ surgical_read_budgets: z.ZodDefault<z.ZodOptional<z.ZodObject<{
70
+ small_local: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
71
+ mid: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
72
+ frontier: z.ZodDefault<z.ZodOptional<z.ZodNullable<z.ZodNumber>>>;
73
+ }, z.core.$strict>>>;
44
74
  }, z.core.$strict>>>;
45
75
  autonomy: z.ZodDefault<z.ZodOptional<z.ZodObject<{
46
76
  orchestrator_preflight: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
@@ -110,7 +140,9 @@ export declare function readRuntimeProfile(): RuntimeProfile;
110
140
  export declare function readRuntimePromptTemplate(): string;
111
141
  export declare function readRuntimeProfileState(): RuntimeProfileSnapshot;
112
142
  export declare function renderRuntimePrompt(templateContext: Record<string, unknown>): string;
143
+ export declare function renderRuntimePromptFromSnapshot(snapshot: RuntimeProfileSnapshot, templateContext: Record<string, unknown>): string;
113
144
  export declare function getRuntimeProfilePath(): string;
114
145
  export declare function getRuntimeProfileSchemaPath(): string;
146
+ export declare function getSurgicalReadBudget(modelClass: "frontier" | "mid" | "small_local" | undefined, profile?: RuntimeProfile): number | null;
115
147
  export {};
116
148
  //# sourceMappingURL=runtime-profile.d.ts.map
@@ -4,7 +4,7 @@ import { resolve } from "node:path";
4
4
  import { z } from "zod";
5
5
  import { DEFAULTS_ROOT, resolveWorkspaceArtifactPath, resolveWorkspaceRoot, } from "./helpers.js";
6
6
  import { getStoreStatSync, getWorkspaceStorePath, parseVirtualStorePath, readStoreBlobSync, readStoreBlobByPathSync, toVirtualStorePath, } from "./store/store-snapshot.js";
7
- import { appendStatusEvent } from "./status-events.js";
7
+ import { appendStatusEventSafe } from "./status-events.js";
8
8
  import { DEFAULT_AUTONOMY_POLICY, DEFAULT_CONTINUITY_POLICY, } from "./ace-autonomy.js";
9
9
  export const RUNTIME_PROFILE_REL_PATH = "agent-state/ACE_WORKFLOW.md";
10
10
  export const RUNTIME_PROFILE_SCHEMA_REL_PATH = "agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json";
@@ -13,6 +13,57 @@ export const DEFAULT_RUNTIME_TOOL_REGISTRY_REL_PATH = "agent-state/runtime-tool-
13
13
  export const DEFAULT_EXECUTOR_MAX_TURNS = 6;
14
14
  export const DEFAULT_EXECUTOR_TURN_TIMEOUT_MS = 300_000;
15
15
  export const DEFAULT_VERICIFY_BRIDGE_REL_PATH = "agent-state/vericify/ace-bridge.json";
16
+ // Liveness defaults per model class
17
+ export const DEFAULT_STALL_WINDOW_MS = 300_000; // 5 min — inter-progress cap
18
+ export const DEFAULT_TURN_BUDGET_MS = 1_800_000; // 30 min — per-turn soft cap
19
+ export const DEFAULT_MAX_STALL_RESTARTS = 3;
20
+ export const DEFAULT_INITIAL_BACKOFF_MS = 5_000;
21
+ export const DEFAULT_MAX_RETRY_BACKOFF_MS = 300_000;
22
+ export const DEFAULT_SURGICAL_READ_BUDGETS = {
23
+ small_local: 200,
24
+ mid: 800,
25
+ frontier: null,
26
+ };
27
+ export function getLivenessDefaults(modelClass) {
28
+ switch (modelClass) {
29
+ case "small_local":
30
+ return {
31
+ turn_budget_ms: 3_600_000, // 60 min — slow models need generous cap
32
+ stall_window_ms: 180_000, // 3 min stall window
33
+ max_stall_restarts: 3,
34
+ initial_backoff_ms: 10_000,
35
+ max_retry_backoff_ms: 300_000,
36
+ };
37
+ case "mid":
38
+ return {
39
+ turn_budget_ms: 1_800_000, // 30 min
40
+ stall_window_ms: 300_000, // 5 min
41
+ max_stall_restarts: 3,
42
+ initial_backoff_ms: 5_000,
43
+ max_retry_backoff_ms: 180_000,
44
+ };
45
+ case "frontier":
46
+ default:
47
+ return {
48
+ turn_budget_ms: DEFAULT_TURN_BUDGET_MS,
49
+ stall_window_ms: DEFAULT_STALL_WINDOW_MS,
50
+ max_stall_restarts: DEFAULT_MAX_STALL_RESTARTS,
51
+ initial_backoff_ms: DEFAULT_INITIAL_BACKOFF_MS,
52
+ max_retry_backoff_ms: DEFAULT_MAX_RETRY_BACKOFF_MS,
53
+ };
54
+ }
55
+ }
56
+ function resolveSurgicalReadBudgetValue(budgets, modelClass) {
57
+ const resolved = budgets ?? DEFAULT_SURGICAL_READ_BUDGETS;
58
+ return resolved[modelClass] ?? null;
59
+ }
60
+ export function resolveEffectiveSurgicalReadBudget(profile, modelClass) {
61
+ const effectiveModelClass = modelClass ?? "frontier";
62
+ return {
63
+ model_class: effectiveModelClass,
64
+ read_file_lines_max_lines: resolveSurgicalReadBudgetValue(profile?.tools?.surgical_read_budgets, effectiveModelClass),
65
+ };
66
+ }
16
67
  export const DEFAULT_VERICIFY_PROCESS_POST_LOG_REL_PATH = "agent-state/vericify/process-posts.json";
17
68
  const DEFAULT_RUNTIME_PROFILE_PATH = resolve(DEFAULTS_ROOT, RUNTIME_PROFILE_REL_PATH);
18
69
  const DEFAULT_RUNTIME_PROFILE_SCHEMA_PATH = resolve(DEFAULTS_ROOT, RUNTIME_PROFILE_SCHEMA_REL_PATH);
@@ -63,10 +114,22 @@ const runtimeProfileSchema = z
63
114
  .min(1)
64
115
  .optional()
65
116
  .default(DEFAULT_RUNTIME_TOOL_REGISTRY_REL_PATH),
117
+ surgical_read_budgets: z
118
+ .object({
119
+ small_local: z.number().int().positive().optional().default(DEFAULT_SURGICAL_READ_BUDGETS.small_local),
120
+ mid: z.number().int().positive().optional().default(DEFAULT_SURGICAL_READ_BUDGETS.mid),
121
+ frontier: z.number().int().positive().nullable().optional().default(DEFAULT_SURGICAL_READ_BUDGETS.frontier),
122
+ })
123
+ .strict()
124
+ .optional()
125
+ .default(DEFAULT_SURGICAL_READ_BUDGETS),
66
126
  })
67
127
  .strict()
68
128
  .optional()
69
- .default({ registry_path: DEFAULT_RUNTIME_TOOL_REGISTRY_REL_PATH }),
129
+ .default({
130
+ registry_path: DEFAULT_RUNTIME_TOOL_REGISTRY_REL_PATH,
131
+ surgical_read_budgets: DEFAULT_SURGICAL_READ_BUDGETS,
132
+ }),
70
133
  autonomy: z
71
134
  .object({
72
135
  orchestrator_preflight: z.boolean().optional().default(true),
@@ -336,7 +399,7 @@ function parseRuntimeProfileContent(raw) {
336
399
  }
337
400
  function emitRuntimeProfileEvent(eventType, input) {
338
401
  try {
339
- appendStatusEvent({
402
+ void appendStatusEventSafe({
340
403
  source_module: "capability-framework",
341
404
  event_type: eventType,
342
405
  status: eventType === "RUNTIME_PROFILE_INVALID" ? "fail" : "pass",
@@ -349,6 +412,8 @@ function emitRuntimeProfileEvent(eventType, input) {
349
412
  signature: input.signature,
350
413
  errors: input.errors,
351
414
  },
415
+ }).catch(() => {
416
+ // Runtime profile reads must not fail due to observability side effects.
352
417
  });
353
418
  }
354
419
  catch {
@@ -357,13 +422,16 @@ function emitRuntimeProfileEvent(eventType, input) {
357
422
  }
358
423
  function ensureLastGoodSnapshot() {
359
424
  const current = loadRuntimeProfile();
360
- if (current.ok && lastGoodSnapshot)
361
- return lastGoodSnapshot;
362
- if (lastGoodSnapshot)
425
+ if (current.ok)
426
+ return current;
427
+ if (lastGoodSnapshot &&
428
+ lastGoodSnapshot.path === current.path &&
429
+ lastGoodSnapshot.source === current.source) {
363
430
  return lastGoodSnapshot;
431
+ }
364
432
  const packaged = loadRuntimeProfile(DEFAULT_RUNTIME_PROFILE_PATH);
365
- if (packaged.ok && lastGoodSnapshot)
366
- return lastGoodSnapshot;
433
+ if (packaged.ok)
434
+ return packaged;
367
435
  throw new Error(`Packaged ACE runtime profile is invalid: ${(packaged.ok ? [] : packaged.errors).join("; ")}`);
368
436
  }
369
437
  function resolveTemplatePath(context, path) {
@@ -511,14 +579,17 @@ export function readRuntimeProfileState() {
511
579
  }
512
580
  export function renderRuntimePrompt(templateContext) {
513
581
  const active = ensureLastGoodSnapshot();
582
+ return renderRuntimePromptFromSnapshot(active, templateContext);
583
+ }
584
+ export function renderRuntimePromptFromSnapshot(snapshot, templateContext) {
514
585
  const renderContext = {
515
- ...active.profile,
516
- runtime_profile_path: active.path,
517
- runtime_profile_source: active.source,
518
- prompt_template: active.prompt_template,
586
+ ...snapshot.profile,
587
+ runtime_profile_path: snapshot.path,
588
+ runtime_profile_source: snapshot.source,
589
+ prompt_template: snapshot.prompt_template,
519
590
  ...templateContext,
520
591
  };
521
- return active.prompt_template.replace(/{{\s*([A-Za-z0-9_.-]+)\s*}}/g, (match, token) => {
592
+ return snapshot.prompt_template.replace(/{{\s*([A-Za-z0-9_.-]+)\s*}}/g, (match, token) => {
522
593
  const value = resolveTemplatePath(renderContext, token);
523
594
  return value === undefined ? match : stringifyTemplateValue(value);
524
595
  });
@@ -529,4 +600,9 @@ export function getRuntimeProfilePath() {
529
600
  export function getRuntimeProfileSchemaPath() {
530
601
  return DEFAULT_RUNTIME_PROFILE_SCHEMA_PATH;
531
602
  }
603
+ export function getSurgicalReadBudget(modelClass, profile) {
604
+ const effectiveProfile = profile ?? readRuntimeProfile();
605
+ const budgets = effectiveProfile.tools?.surgical_read_budgets ?? DEFAULT_SURGICAL_READ_BUDGETS;
606
+ return budgets[modelClass ?? "frontier"] ?? null;
607
+ }
532
608
  //# sourceMappingURL=runtime-profile.js.map
@@ -1,4 +1,6 @@
1
+ import type { ToolCostClass } from "./store/repositories/local-model-runtime-repository.js";
1
2
  import { type ArtifactSource } from "./helpers.js";
3
+ import { type RuntimeModelClass } from "./runtime-profile.js";
2
4
  import { type ValidationResult } from "./schemas.js";
3
5
  export declare const RUNTIME_TOOL_SPEC_REGISTRY_REL_PATH = "agent-state/runtime-tool-specs.json";
4
6
  export declare const RUNTIME_TOOL_SPEC_SCHEMA_REL_PATH = "agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json";
@@ -64,5 +66,24 @@ export declare function readRuntimeToolRegistry(): RuntimeToolSpecRegistry;
64
66
  export declare function listRuntimeToolSpecs(): RuntimeToolSpec[];
65
67
  export declare function getRuntimeToolRegistryPath(): string;
66
68
  export declare function executeRuntimeTool(name: string, input: unknown, context?: RuntimeToolExecutionContext): Promise<RuntimeToolExecutionResult>;
69
+ export type ModelClass = RuntimeModelClass;
70
+ export declare const SMALL_LOCAL_PRIORITY_TOOL_NAMES: readonly ["outline_file", "astgrep_query", "list_workspace", "read_file_lines", "recall_context", "validate_framework", "run_orchestrator"];
71
+ export interface FilteredToolCatalogEntry {
72
+ name: string;
73
+ description: string;
74
+ input_schema: RuntimeToolSchema;
75
+ cost_class: ToolCostClass;
76
+ }
77
+ export interface FilteredToolCatalog {
78
+ entries: FilteredToolCatalogEntry[];
79
+ model_class: ModelClass;
80
+ unavailable_tools: {
81
+ name: string;
82
+ reason: string;
83
+ reason_code: string;
84
+ }[];
85
+ tool_cost_class: Record<string, ToolCostClass>;
86
+ }
87
+ export declare function buildFilteredToolCatalog(modelClass: ModelClass | undefined, allowedTools?: string[]): FilteredToolCatalog;
67
88
  export {};
68
89
  //# sourceMappingURL=runtime-tool-specs.d.ts.map
@@ -12,7 +12,7 @@ import { appendStatusEventSafe } from "./status-events.js";
12
12
  import { openStore } from "./store/ace-packed-store.js";
13
13
  import { getWorkspaceStorePath, readStoreBlobSync, storeExistsSync, toVirtualStorePath, } from "./store/store-snapshot.js";
14
14
  import { operationalArtifactKey, } from "./store/store-artifacts.js";
15
- import { withStoreWriteQueue } from "./store/write-queue.js";
15
+ import { withStoreWriteCoordinator } from "./store/write-coordinator.js";
16
16
  export const RUNTIME_TOOL_SPEC_REGISTRY_REL_PATH = "agent-state/runtime-tool-specs.json";
17
17
  export const RUNTIME_TOOL_SPEC_SCHEMA_REL_PATH = "agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json";
18
18
  export const RUNTIME_TOOL_SPEC_SCHEMA_NAME = "runtime-tool-spec-registry@1.0.0";
@@ -113,7 +113,7 @@ async function persistToolInvocationArtifacts(callId, requestContent, responseCo
113
113
  return { requestPath, responsePath };
114
114
  }
115
115
  const storePath = getWorkspaceStorePath(workspaceRoot);
116
- await withStoreWriteQueue(storePath, async () => {
116
+ await withStoreWriteCoordinator(storePath, async () => {
117
117
  const store = await openStore(storePath);
118
118
  try {
119
119
  await store.setBlob(runtimeToolArtifactKey(callId, "request"), requestContent);
@@ -123,7 +123,7 @@ async function persistToolInvocationArtifacts(callId, requestContent, responseCo
123
123
  finally {
124
124
  await store.close();
125
125
  }
126
- });
126
+ }, { operation_label: "persistToolInvocationArtifacts" });
127
127
  return {
128
128
  requestPath: runtimeToolArtifactVirtualPath(callId, "request"),
129
129
  responsePath: runtimeToolArtifactVirtualPath(callId, "response"),
@@ -524,4 +524,79 @@ export async function executeRuntimeTool(name, input, context = {}) {
524
524
  });
525
525
  return result;
526
526
  }
527
+ export const SMALL_LOCAL_PRIORITY_TOOL_NAMES = [
528
+ "outline_file",
529
+ "astgrep_query",
530
+ "list_workspace",
531
+ "read_file_lines",
532
+ "recall_context",
533
+ "validate_framework",
534
+ "run_orchestrator",
535
+ ];
536
+ const HEAVY_TOOL_NAMES = new Set([
537
+ "read_workspace_file",
538
+ "write_workspace_file",
539
+ "safe_edit_file",
540
+ "astgrep_rewrite",
541
+ ]);
542
+ function extractCostClass(tool) {
543
+ const desc = (tool.description ?? "").toLowerCase();
544
+ if (desc.includes("cost: cheap"))
545
+ return "cheap";
546
+ if (desc.includes("cost: moderate"))
547
+ return "moderate";
548
+ if (desc.includes("cost: heavy"))
549
+ return "heavy";
550
+ if (HEAVY_TOOL_NAMES.has(tool.name))
551
+ return "heavy";
552
+ if (["read_file_lines", "apply_patch", "diff_files"].includes(tool.name))
553
+ return "moderate";
554
+ return "cheap";
555
+ }
556
+ export function buildFilteredToolCatalog(modelClass, allowedTools) {
557
+ const allTools = listRuntimeToolSpecs();
558
+ const effectiveModelClass = modelClass ?? "frontier";
559
+ const allowed = allowedTools ? new Set(allowedTools) : null;
560
+ const unavailable = [];
561
+ let entries = [];
562
+ for (const tool of allTools) {
563
+ if (allowed !== null && !allowed.has(tool.name)) {
564
+ unavailable.push({
565
+ name: tool.name,
566
+ reason: "Not in allowed_tools for this session",
567
+ reason_code: "workspace_disabled",
568
+ });
569
+ continue;
570
+ }
571
+ const costClass = extractCostClass(tool);
572
+ let description = tool.description;
573
+ if (effectiveModelClass === "small_local" && costClass === "heavy") {
574
+ description = `[cost: heavy — prefer surgical alternatives] ${description}`;
575
+ }
576
+ entries.push({
577
+ name: tool.name,
578
+ description,
579
+ input_schema: tool.input_schema,
580
+ cost_class: costClass,
581
+ });
582
+ }
583
+ if (effectiveModelClass === "small_local" || effectiveModelClass === "mid") {
584
+ const priority = new Map(SMALL_LOCAL_PRIORITY_TOOL_NAMES.map((name, idx) => [name, idx]));
585
+ entries.sort((a, b) => {
586
+ const pa = priority.get(a.name) ?? 999;
587
+ const pb = priority.get(b.name) ?? 999;
588
+ return pa - pb;
589
+ });
590
+ }
591
+ const toolCostClass = {};
592
+ for (const e of entries) {
593
+ toolCostClass[e.name] = e.cost_class;
594
+ }
595
+ return {
596
+ entries,
597
+ model_class: effectiveModelClass,
598
+ unavailable_tools: unavailable,
599
+ tool_cost_class: toolCostClass,
600
+ };
601
+ }
527
602
  //# sourceMappingURL=runtime-tool-specs.js.map
@@ -49,4 +49,11 @@ export declare function diffContents(original: string, updated: string): DiffRes
49
49
  export declare function diffFiles(pathA: string, pathB: string): DiffResult & {
50
50
  error?: string;
51
51
  };
52
+ export interface ApplyPatchInput {
53
+ path: string;
54
+ patch: string;
55
+ validation_command?: string;
56
+ test_command?: string;
57
+ }
58
+ export declare function applyPatch(input: ApplyPatchInput): SafeEditResult;
52
59
  //# sourceMappingURL=safe-edit.d.ts.map
package/dist/safe-edit.js CHANGED
@@ -2,10 +2,11 @@
2
2
  * Safe-edit module: copy → modify → validate → swap pattern.
3
3
  * Prevents bricking by never leaving core files in a broken state.
4
4
  */
5
- import { copyFileSync, existsSync, mkdirSync, unlinkSync, writeFileSync, } from "node:fs";
5
+ import { copyFileSync, cpSync, existsSync, mkdtempSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
6
6
  import { createHash } from "node:crypto";
7
- import { dirname, resolve } from "node:path";
7
+ import { basename, dirname, relative, resolve } from "node:path";
8
8
  import { spawnSync } from "node:child_process";
9
+ import { tmpdir } from "node:os";
9
10
  import { WORKSPACE_ROOT, safeRead, wsPath } from "./helpers.js";
10
11
  import { isInside, isReadError, normalizeRelPath } from "./shared.js";
11
12
  const STAGING_DIR = ".ace-staging";
@@ -19,14 +20,56 @@ function ensureDir(dirPath) {
19
20
  if (!existsSync(dirPath))
20
21
  mkdirSync(dirPath, { recursive: true });
21
22
  }
22
- function runValidation(validationCmd, testCmd) {
23
+ function resolveWorkspaceTarget(inputPath) {
24
+ const absPath = resolve(WORKSPACE_ROOT, inputPath);
25
+ if (!isInside(WORKSPACE_ROOT, absPath)) {
26
+ return { error: `Path escapes workspace root: ${inputPath}`, absPath };
27
+ }
28
+ return {
29
+ absPath,
30
+ relPath: normalizeRelPath(relative(WORKSPACE_ROOT, absPath)),
31
+ };
32
+ }
33
+ function shouldCopyIntoStaging(src) {
34
+ const rel = normalizeRelPath(relative(WORKSPACE_ROOT, src));
35
+ if (!rel || rel === ".")
36
+ return true;
37
+ const first = rel.split("/")[0];
38
+ return !new Set([".ace-staging", ".git", "node_modules", ".npm-cache"]).has(first);
39
+ }
40
+ function prepareStagedWorkspace(stagingDir) {
41
+ const stagedWorkspace = mkdtempSync(resolve(tmpdir(), "ace-staged-workspace-"));
42
+ rmSync(stagedWorkspace, { recursive: true, force: true });
43
+ mkdirSync(stagedWorkspace, { recursive: true });
44
+ cpSync(WORKSPACE_ROOT, stagedWorkspace, {
45
+ recursive: true,
46
+ dereference: false,
47
+ filter: (src) => shouldCopyIntoStaging(src),
48
+ });
49
+ const rootNodeModules = resolve(WORKSPACE_ROOT, "node_modules");
50
+ if (existsSync(rootNodeModules)) {
51
+ symlinkSync(rootNodeModules, resolve(stagedWorkspace, "node_modules"), "dir");
52
+ }
53
+ writeFileSync(resolve(stagingDir, "workspace-path.txt"), stagedWorkspace, "utf-8");
54
+ return stagedWorkspace;
55
+ }
56
+ function workspacePathIn(root, relPath) {
57
+ return resolve(root, relPath);
58
+ }
59
+ function swapTargetContent(absPath, content) {
60
+ ensureDir(dirname(absPath));
61
+ const swapPath = `${absPath}.ace-swap.${process.pid}.${Date.now()}`;
62
+ writeFileSync(swapPath, content, "utf-8");
63
+ renameSync(swapPath, absPath);
64
+ }
65
+ function runValidation(validationCmd, testCmd, cwd = WORKSPACE_ROOT) {
23
66
  let validation_passed;
24
67
  let validation_output;
25
68
  let test_passed;
26
69
  let test_output;
27
70
  if (validationCmd) {
28
71
  const res = spawnSync("sh", ["-c", validationCmd], {
29
- cwd: WORKSPACE_ROOT,
72
+ cwd,
30
73
  encoding: "utf-8",
31
74
  timeout: 60_000,
32
75
  maxBuffer: 4 * 1024 * 1024,
@@ -36,7 +79,7 @@ function runValidation(validationCmd, testCmd) {
36
79
  }
37
80
  if (testCmd && (validation_passed === undefined || validation_passed)) {
38
81
  const res = spawnSync("sh", ["-c", testCmd], {
39
- cwd: WORKSPACE_ROOT,
82
+ cwd,
40
83
  encoding: "utf-8",
41
84
  timeout: 120_000,
42
85
  maxBuffer: 4 * 1024 * 1024,
@@ -51,84 +94,80 @@ function runValidation(validationCmd, testCmd) {
51
94
  // Safe edit (copy → modify → validate → swap)
52
95
  // ────────────────────────────────────────────────────────────────────
53
96
  export function safeEditFile(input) {
54
- const absPath = resolve(WORKSPACE_ROOT, input.path);
55
- // ── Workspace boundary guard ──────────────────────────────────────
56
- if (!isInside(WORKSPACE_ROOT, absPath)) {
97
+ const target = resolveWorkspaceTarget(input.path);
98
+ if ("error" in target) {
57
99
  return {
58
100
  ok: false,
59
- original_path: absPath,
101
+ original_path: target.absPath,
60
102
  staging_path: "",
61
103
  original_hash: "none",
62
104
  new_hash: "none",
63
- error: `Path escapes workspace root: ${input.path}`,
105
+ error: target.error,
64
106
  restored: false,
65
107
  };
66
108
  }
67
- const relPath = normalizeRelPath(input.path);
109
+ const { absPath, relPath } = target;
68
110
  const timestamp = Date.now();
69
111
  const originalContent = safeRead(input.path);
70
112
  const isNew = isReadError(originalContent);
71
113
  const originalHash = isNew ? "none" : fileHash(originalContent);
72
- const newHash = fileHash(input.content);
114
+ const stagedNewHash = fileHash(input.content);
73
115
  // Step 1: Create staging directory
74
116
  const stagingDir = wsPath(STAGING_DIR, `${timestamp}-${isNew ? "new" : originalHash.slice(0, 8)}`);
75
117
  ensureDir(stagingDir);
76
118
  const flatName = relPath.replace(/\//g, "__");
77
119
  const stagingNew = resolve(stagingDir, `new__${flatName}`);
78
120
  writeFileSync(stagingNew, input.content, "utf-8");
121
+ const stagedWorkspace = prepareStagedWorkspace(stagingDir);
122
+ const stagedTarget = workspacePathIn(stagedWorkspace, relPath);
123
+ ensureDir(dirname(stagedTarget));
124
+ writeFileSync(stagedTarget, input.content, "utf-8");
79
125
  // Step 2: If file exists, copy original to staging + backup
80
126
  let backupPath;
81
127
  if (!isNew) {
82
128
  const stagingOriginal = resolve(stagingDir, `original__${flatName}`);
83
129
  writeFileSync(stagingOriginal, originalContent, "utf-8");
84
- if (input.backup !== false) {
85
- backupPath = `${absPath}.ace-backup.${originalHash.slice(0, 8)}`;
86
- copyFileSync(absPath, backupPath);
87
- }
88
130
  }
89
- // Step 3: Write new content
90
- ensureDir(dirname(absPath));
91
- writeFileSync(absPath, input.content, "utf-8");
92
- // Step 4: Run validation/tests
131
+ // Step 3: Run validation/tests against the staged workspace.
132
+ let check;
93
133
  if (input.validation_command || input.test_command) {
94
- const check = runValidation(input.validation_command, input.test_command);
134
+ check = runValidation(input.validation_command, input.test_command, stagedWorkspace);
95
135
  if (!check.passed) {
96
- // ROLLBACK: restore original content
97
- if (!isNew) {
98
- writeFileSync(absPath, originalContent, "utf-8");
99
- }
100
- else {
101
- // Remove newly created file
102
- try {
103
- unlinkSync(absPath);
104
- }
105
- catch {
106
- /* best effort */
107
- }
108
- }
109
136
  return {
110
137
  ok: false,
111
138
  original_path: absPath,
112
139
  staging_path: stagingDir,
113
140
  backup_path: backupPath,
114
141
  original_hash: originalHash,
115
- new_hash: newHash,
142
+ new_hash: stagedNewHash,
116
143
  validation_passed: check.validation_passed,
117
144
  validation_output: check.validation_output,
118
145
  test_passed: check.test_passed,
119
146
  test_output: check.test_output,
120
- error: "Validation/test failed — original restored",
147
+ error: "Validation/test failed — staged change not promoted",
121
148
  restored: true,
122
149
  };
123
150
  }
124
151
  }
152
+ // Step 4: Promote staged content to the real target only after checks pass.
153
+ if (!isNew && input.backup !== false) {
154
+ backupPath = `${absPath}.ace-backup.${originalHash.slice(0, 8)}`;
155
+ copyFileSync(absPath, backupPath);
156
+ }
157
+ const promotedContent = readFileSync(stagedTarget, "utf-8");
158
+ const promotedHash = fileHash(promotedContent);
159
+ swapTargetContent(absPath, promotedContent);
125
160
  return {
126
161
  ok: true,
127
162
  original_path: absPath,
128
163
  staging_path: stagingDir,
129
164
  backup_path: backupPath,
130
165
  original_hash: originalHash,
131
- new_hash: newHash,
166
+ new_hash: promotedHash,
167
+ validation_passed: check?.validation_passed,
168
+ validation_output: check?.validation_output,
169
+ test_passed: check?.test_passed,
170
+ test_output: check?.test_output,
132
171
  };
133
172
  }
134
173
  // ────────────────────────────────────────────────────────────────────
@@ -252,4 +291,91 @@ export function diffFiles(pathA, pathB) {
252
291
  }
253
292
  return diffContents(contentA, contentB);
254
293
  }
294
+ export function applyPatch(input) {
295
+ const target = resolveWorkspaceTarget(input.path);
296
+ if ("error" in target) {
297
+ return {
298
+ ok: false,
299
+ original_path: target.absPath,
300
+ staging_path: "",
301
+ original_hash: "none",
302
+ new_hash: "none",
303
+ error: target.error,
304
+ restored: false,
305
+ };
306
+ }
307
+ const { absPath, relPath } = target;
308
+ const originalContent = safeRead(input.path);
309
+ const isNew = isReadError(originalContent);
310
+ const originalHash = isNew ? "none" : fileHash(originalContent);
311
+ const timestamp = Date.now();
312
+ const stagingDir = wsPath(STAGING_DIR, `${timestamp}-patch-${isNew ? "new" : originalHash.slice(0, 8)}`);
313
+ ensureDir(stagingDir);
314
+ const stagedWorkspace = prepareStagedWorkspace(stagingDir);
315
+ const stagedTarget = workspacePathIn(stagedWorkspace, relPath);
316
+ ensureDir(dirname(stagedTarget));
317
+ if (isNew && !existsSync(stagedTarget))
318
+ writeFileSync(stagedTarget, "", "utf-8");
319
+ const tmpPatch = resolve(stagingDir, `${basename(relPath)}.${timestamp}.patch`);
320
+ try {
321
+ writeFileSync(tmpPatch, input.patch, "utf8");
322
+ const patchResult = spawnSync("patch", [stagedTarget, tmpPatch], {
323
+ encoding: "utf8",
324
+ cwd: stagedWorkspace,
325
+ });
326
+ if (patchResult.status !== 0) {
327
+ return {
328
+ ok: false,
329
+ original_path: absPath,
330
+ staging_path: stagingDir,
331
+ original_hash: originalHash,
332
+ new_hash: "none",
333
+ error: `patch command failed: ${patchResult.stderr || patchResult.stdout}`,
334
+ restored: true,
335
+ };
336
+ }
337
+ const patchedContent = readFileSync(stagedTarget, "utf-8");
338
+ const newHash = fileHash(patchedContent);
339
+ const check = runValidation(input.validation_command, input.test_command, stagedWorkspace);
340
+ if (!check.passed) {
341
+ return {
342
+ ok: false,
343
+ original_path: absPath,
344
+ staging_path: stagingDir,
345
+ original_hash: originalHash,
346
+ new_hash: newHash,
347
+ validation_passed: check.validation_passed,
348
+ validation_output: check.validation_output,
349
+ test_passed: check.test_passed,
350
+ test_output: check.test_output,
351
+ error: "Validation/test failed — staged patch not promoted",
352
+ restored: true,
353
+ };
354
+ }
355
+ let backupPath;
356
+ if (!isNew) {
357
+ backupPath = `${absPath}.ace-backup.${originalHash.slice(0, 8)}`;
358
+ copyFileSync(absPath, backupPath);
359
+ }
360
+ swapTargetContent(absPath, patchedContent);
361
+ return {
362
+ ok: true,
363
+ original_path: absPath,
364
+ staging_path: stagingDir,
365
+ backup_path: backupPath,
366
+ original_hash: originalHash,
367
+ new_hash: newHash,
368
+ validation_passed: check.validation_passed,
369
+ validation_output: check.validation_output,
370
+ test_passed: check.test_passed,
371
+ test_output: check.test_output,
372
+ };
373
+ }
374
+ finally {
375
+ try {
376
+ unlinkSync(tmpPatch);
377
+ }
378
+ catch { /* ignore */ }
379
+ }
380
+ }
255
381
  //# sourceMappingURL=safe-edit.js.map