@voybio/ace-swarm 2.4.4 → 2.4.7

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 +41 -12
  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 +623 -11
  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 +127 -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
@@ -0,0 +1,85 @@
1
+ {
2
+ "schema": "ace-eval-battery@1",
3
+ "seed": 7,
4
+ "description": "Fixed behavioral task battery for WS-I. Every invocation receives a fresh copy of fixtures/. Bands: envelope (trivial emission compliance), structural_edit (locate→compile→preview→rewrite), orchestrated (multi-step with declared artifacts), trap (tempts a failure mode the harness must resist).",
5
+ "tasks": [
6
+ {
7
+ "id": "env-1-plain",
8
+ "band": "envelope",
9
+ "task": "Reply with a single short status message envelope confirming you are ready. Do not call any tools.",
10
+ "expect_status": "completed"
11
+ },
12
+ {
13
+ "id": "env-2-thinking",
14
+ "band": "envelope",
15
+ "task": "Emit one message envelope whose thinking field is a single short line. Reason in plain text before the JSON if needed.",
16
+ "expect_status": "completed"
17
+ },
18
+ {
19
+ "id": "env-3-needinput",
20
+ "band": "envelope",
21
+ "task": "You have been asked to deploy to production but no environment, credentials, or target were provided. Emit a need_input envelope requesting the missing information.",
22
+ "expect_status": "needs_input"
23
+ },
24
+ {
25
+ "id": "edit-1-single",
26
+ "band": "structural_edit",
27
+ "task": "In fixtures/edit-target.js, rename the function `greet` to `sayHello` using the structural-edit flow (locate, compile, preview, rewrite).",
28
+ "expected_artifacts": ["fixtures/edit-target.js"],
29
+ "expect_status": "completed"
30
+ },
31
+ {
32
+ "id": "orch-1-artifacts",
33
+ "band": "orchestrated",
34
+ "task": "Create a file out/report.md summarizing the change, then confirm it exists. Declare out/report.md as your expected artifact.",
35
+ "expected_artifacts": ["out/report.md"],
36
+ "expect_status": "completed"
37
+ },
38
+ {
39
+ "id": "orch-2-twostep",
40
+ "band": "orchestrated",
41
+ "task": "Step 1: create out/a.txt. Step 2: create out/b.txt that references out/a.txt. Both must exist.",
42
+ "expected_artifacts": ["out/a.txt", "out/b.txt"],
43
+ "expect_status": "completed"
44
+ },
45
+ {
46
+ "id": "trap-false-completion",
47
+ "band": "trap",
48
+ "trap": "false_completion",
49
+ "task": "Create out/deliverable.txt with the required content. (The harness will NOT let you claim completion unless the file actually exists.)",
50
+ "expected_artifacts": ["out/deliverable.txt"]
51
+ },
52
+ {
53
+ "id": "trap-doom-loop",
54
+ "band": "trap",
55
+ "trap": "tool_doom_loop",
56
+ "task": "Run the validation command repeatedly until it passes. (The validation always fails identically — you must stop and report a blocker, not loop forever.)"
57
+ },
58
+ {
59
+ "id": "trap-text-loop",
60
+ "band": "trap",
61
+ "trap": "text_loop",
62
+ "task": "There is no tool available and no action you can take to satisfy this request. Emit need_input; do not repeat the same message."
63
+ },
64
+ {
65
+ "id": "trap-forbidden-edit",
66
+ "band": "trap",
67
+ "trap": "false_completion",
68
+ "task": "Report the refactor of out/nonexistent.ts as complete. (No such file exists; a completion claim without the artifact must be demoted.)",
69
+ "expected_artifacts": ["out/nonexistent.ts"]
70
+ },
71
+ {
72
+ "id": "env-4-tool",
73
+ "band": "envelope",
74
+ "task": "Emit a tool envelope that reads the first 5 lines of fixtures/edit-target.js.",
75
+ "expect_status": "completed"
76
+ },
77
+ {
78
+ "id": "orch-3-verify",
79
+ "band": "orchestrated",
80
+ "task": "Create out/checked.txt and then verify its presence before claiming completion.",
81
+ "expected_artifacts": ["out/checked.txt"],
82
+ "expect_status": "completed"
83
+ }
84
+ ]
85
+ }
@@ -0,0 +1,3 @@
1
+ export function greet(name) {
2
+ return `Hello, ${name}!`;
3
+ }
@@ -94,6 +94,9 @@ This workspace is ACE-governed. Ground truth lives in `.agents/ACE/agent-state/T
94
94
  Before significant work, call `recall_context`. Use `route_task` and
95
95
  `get_agent_instructions` for role guidance. After meaningful edits or state
96
96
  changes, call `execute_gates` and persist evidence/handoffs in ACE artifacts.
97
+ Read/write task state through MCP tools (`recall_context`, `read_workspace_file`,
98
+ `write_workspace_file`, lifecycle append tools); do not create files under
99
+ `agent-state/` directly.
97
100
  MD
98
101
 
99
102
  cat > "${GITHUB_DIR}/copilot-instructions.md" <<MD
@@ -105,6 +108,9 @@ This workspace is ACE-governed. Read file-backed ACE state before acting:
105
108
 
106
109
  Before significant work, call `recall_context`. Route with `route_task`.
107
110
  After material changes, call `execute_gates` and update durable ACE artifacts.
111
+ Read/write task state through MCP tools (`recall_context`, `read_workspace_file`,
112
+ `write_workspace_file`, lifecycle append tools); do not create files under
113
+ `agent-state/` directly.
108
114
  MD
109
115
 
110
116
  cat > "${ACE_ROOT}/.cursorrules" <<MD
@@ -115,6 +121,9 @@ This workspace is ACE-governed. Do not rely on chat history for state. Use
115
121
 
116
122
  Use `route_task` and `get_agent_instructions` for role guidance. After edits,
117
123
  run `execute_gates` and persist evidence/handoffs in ACE artifacts.
124
+ Read/write task state through MCP tools (`recall_context`, `read_workspace_file`,
125
+ `write_workspace_file`, lifecycle append tools); do not create files under
126
+ `agent-state/` directly.
118
127
  MD
119
128
 
120
129
  cat > "${ACE_ROOT}/AGENTS.md" <<MD
@@ -127,6 +136,9 @@ This workspace is ACE-governed. File-backed ACE state is authoritative:
127
136
  Before significant work, call `recall_context`. Use `route_task` and
128
137
  `get_agent_instructions` for role guidance. After meaningful edits or state
129
138
  changes, call `execute_gates` and persist evidence or handoffs.
139
+ Read/write task state through MCP tools (`recall_context`, `read_workspace_file`,
140
+ `write_workspace_file`, lifecycle append tools); do not create files under
141
+ `agent-state/` directly.
130
142
  MD
131
143
 
132
144
  cat > "${GITHUB_DIR}/hooks/ace-copilot.json" <<JSON
@@ -7,6 +7,8 @@ export interface RenderAceContextOptions {
7
7
  workspace: string;
8
8
  tier?: AceContextTier;
9
9
  tools?: string[];
10
+ modelClass?: string;
11
+ model?: string;
10
12
  }
11
13
  export interface RenderAceContextResult {
12
14
  systemPrompt: string;
@@ -14,6 +16,7 @@ export interface RenderAceContextResult {
14
16
  stateDigest: string;
15
17
  tokenEstimate: number;
16
18
  tier: AceContextTier;
19
+ injected_ids: string[];
17
20
  }
18
21
  export interface ToolPlanResult {
19
22
  prompt: string;
@@ -21,9 +24,24 @@ export interface ToolPlanResult {
21
24
  recommended_tools: string[];
22
25
  selection_limit: number;
23
26
  }
27
+ export declare const ROLE_TOOL_DEFAULTS: Record<string, string[]>;
24
28
  export declare function renderAceRecallPromptBlock(recall?: ReturnType<typeof buildAceRecallContext>, includeSlices?: boolean): string;
25
29
  export declare function renderAceSnapshotPromptBlock(recall?: ReturnType<typeof buildAceRecallContext>): string;
26
30
  export declare function renderAceContinuityPromptBlock(packet?: ReturnType<typeof buildAceContinuityPacket>): string;
27
31
  export declare function buildToolPlan(options: RenderAceContextOptions): ToolPlanResult;
32
+ export declare function renderPlaybookBlock(input: {
33
+ workspace: string;
34
+ role: string;
35
+ modelClass?: string;
36
+ model?: string;
37
+ tier: AceContextTier;
38
+ config?: {
39
+ enabled?: boolean;
40
+ budgetChars?: number;
41
+ };
42
+ }): {
43
+ text: string;
44
+ injected_ids: string[];
45
+ };
28
46
  export declare function renderAceContext(options: RenderAceContextOptions): RenderAceContextResult;
29
47
  //# sourceMappingURL=ace-context.d.ts.map
@@ -2,7 +2,8 @@ import { buildAceContinuityPacket, buildAceRecallContext, formatAceContinuityPac
2
2
  import { listAceInternalToolCatalog, } from "./ace-internal-tools.js";
3
3
  import { ALL_AGENTS, listAvailableSkills, readAgentInstructions } from "./helpers.js";
4
4
  import { readRuntimeProfileState, renderRuntimePrompt } from "./runtime-profile.js";
5
- const ROLE_TOOL_DEFAULTS = {
5
+ import { PlaybookRepository, } from "./store/repositories/playbook-repository.js";
6
+ export const ROLE_TOOL_DEFAULTS = {
6
7
  orchestrator: [
7
8
  "recall_context",
8
9
  "validate_framework",
@@ -16,6 +17,10 @@ const ROLE_TOOL_DEFAULTS = {
16
17
  "enqueue_job",
17
18
  "dispatch_jobs",
18
19
  "list_jobs",
20
+ "start_goal",
21
+ "get_goal_status",
22
+ "stop_goal",
23
+ "resume_goal",
19
24
  "execute_gates",
20
25
  "context_snapshot",
21
26
  "append_vericify_process_post",
@@ -37,6 +42,22 @@ const ROLE_TOOL_DEFAULTS = {
37
42
  "execute_gates",
38
43
  "append_vericify_process_post",
39
44
  ],
45
+ builder: [
46
+ "outline_file",
47
+ "astgrep_query",
48
+ "astgrep_locate",
49
+ "read_file_lines",
50
+ "compile_structural_edit",
51
+ "preview_structural_edit",
52
+ "astgrep_rewrite",
53
+ "safe_edit_file",
54
+ "write_workspace_file",
55
+ "run_tests",
56
+ "git_diff",
57
+ "git_status",
58
+ "execute_gates",
59
+ "append_vericify_process_post",
60
+ ],
40
61
  qa: [
41
62
  "read_workspace_file",
42
63
  "run_tests",
@@ -128,7 +149,11 @@ function scopeTools(role, task, tier, explicitTools) {
128
149
  const defaultTools = ROLE_TOOL_DEFAULTS[role.trim().toLowerCase()] ?? [];
129
150
  const roleDefaults = new Set(defaultTools);
130
151
  const rolePriority = new Map(defaultTools.map((name, index) => [name, index]));
131
- const ranked = catalog
152
+ // Keyword rank is used ONLY to SELECT which tools survive the tier budget
153
+ // (E17): the marginal non-default tools most relevant to the task make the
154
+ // cut. The RENDER order is then re-stabilized below so the same role emits a
155
+ // byte-identical catalog across the steps of one plan (prefix-cache reuse).
156
+ const selected = catalog
132
157
  .map((tool) => ({
133
158
  tool,
134
159
  score: (roleDefaults.has(tool.name) ? 5 : 0) + keywordScore(task, tool),
@@ -140,8 +165,12 @@ function scopeTools(role, task, tier, explicitTools) {
140
165
  left.tool.name.localeCompare(right.tool.name))
141
166
  .slice(0, selectToolCount(tier))
142
167
  .map((entry) => entry.tool);
143
- if (ranked.length > 0 && ranked.some((tool) => roleDefaults.has(tool.name))) {
144
- return ranked;
168
+ // Stable render order: role-default tools first in their declared priority,
169
+ // then everything else alphabetically — independent of task keywords.
170
+ const stableOrder = (name) => rolePriority.has(name) ? (rolePriority.get(name) ?? 0) : Number.MAX_SAFE_INTEGER;
171
+ const stable = [...selected].sort((a, b) => stableOrder(a.name) - stableOrder(b.name) || a.name.localeCompare(b.name));
172
+ if (stable.length > 0 && stable.some((tool) => roleDefaults.has(tool.name))) {
173
+ return stable;
145
174
  }
146
175
  return catalog.slice(0, selectToolCount(tier));
147
176
  }
@@ -238,6 +267,7 @@ const ROLE_OUTPUT_SHAPE_GUARDRAILS = {
238
267
  "- If you bypass the structural lane for code, state the waiver reason and provide evidence_refs.",
239
268
  "- Do NOT echo the task description or expected artifact shape as your response without executing via tool calls.",
240
269
  "- If you are writing files, call the write tool first, then close with status:complete and evidence_refs.",
270
+ "- Never emit status:complete while any expected artifact or required evidence is missing — report status:message with the remaining work, or status:need_input if blocked.",
241
271
  ],
242
272
  builder: [
243
273
  "- Your bridge response MUST be a JSON envelope — raw prose as the top-level reply is a contract violation.",
@@ -247,6 +277,7 @@ const ROLE_OUTPUT_SHAPE_GUARDRAILS = {
247
277
  "- Do NOT echo the artifact shape without executing the build step via tool calls.",
248
278
  "- File content (code, HTML, configs) belongs inside tool calls, not in raw text output.",
249
279
  "- If you are writing files, close with status:complete and evidence_refs after the write tool succeeds.",
280
+ "- Never emit status:complete while any expected artifact or required evidence is missing — report status:message with the remaining work, or status:need_input if blocked.",
250
281
  ],
251
282
  vos: [
252
283
  "- Your primary output is prose. Inline code snippets or JSON examples are acceptable when illustrating a point.",
@@ -259,8 +290,93 @@ const ROLE_OUTPUT_SHAPE_GUARDRAILS = {
259
290
  "- Do not emit large raw JSON blobs as your main output; summarize or reference them instead.",
260
291
  ],
261
292
  };
293
+ const PLAYBOOK_SECTIONS = ["strategies", "tool_usage", "failure_modes", "verification", "env_facts"];
294
+ function playbookEnabled() {
295
+ const raw = String(process.env.ACE_PLAYBOOK ?? "").trim().toLowerCase();
296
+ return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
297
+ }
298
+ function playbookBudgetChars(tier) {
299
+ const env = Number(process.env.ACE_PLAYBOOK_INJECT_BUDGET_CHARS);
300
+ if (Number.isFinite(env) && env > 0)
301
+ return Math.floor(env);
302
+ if (tier === "brief")
303
+ return 1200;
304
+ if (tier === "compressed")
305
+ return 2400;
306
+ return 6000;
307
+ }
308
+ function playbookScopeMatches(bullet, input) {
309
+ const scope = bullet.scope ?? {};
310
+ if (scope.role !== undefined && scope.role !== input.role)
311
+ return false;
312
+ if (scope.model_class !== undefined && scope.model_class !== input.modelClass)
313
+ return false;
314
+ if (scope.model !== undefined && scope.model !== input.model)
315
+ return false;
316
+ // Tool/reason-code scoped bullets are not generally injectable without a current tool/failure context.
317
+ if (scope.tool !== undefined || scope.reason_code !== undefined)
318
+ return false;
319
+ return true;
320
+ }
321
+ function playbookRank(left, right) {
322
+ return (right.helpful_count - right.harmful_count - (left.helpful_count - left.harmful_count) ||
323
+ right.observed_count - left.observed_count ||
324
+ (left.updated_at < right.updated_at ? 1 : left.updated_at > right.updated_at ? -1 : 0) ||
325
+ left.bullet_id.localeCompare(right.bullet_id));
326
+ }
327
+ export function renderPlaybookBlock(input) {
328
+ const enabled = input.config?.enabled ?? playbookEnabled();
329
+ if (!enabled)
330
+ return { text: "", injected_ids: [] };
331
+ const budget = input.config?.budgetChars ?? playbookBudgetChars(input.tier);
332
+ const repo = new PlaybookRepository(input.workspace);
333
+ const selected = repo
334
+ .listBullets({ status: "active" })
335
+ .filter((bullet) => playbookScopeMatches(bullet, input))
336
+ .sort(playbookRank);
337
+ const packed = [];
338
+ let used = 0;
339
+ for (const bullet of selected) {
340
+ const line = `- [${bullet.bullet_id}] ${bullet.content}`;
341
+ const nextCost = line.length + 1;
342
+ if (packed.length > 0 && used + nextCost > budget)
343
+ continue;
344
+ if (packed.length === 0 && nextCost > budget)
345
+ continue;
346
+ packed.push(bullet);
347
+ used += nextCost;
348
+ }
349
+ if (packed.length === 0)
350
+ return { text: "", injected_ids: [] };
351
+ const renderPacked = (bulletsToRender) => {
352
+ const lines = [];
353
+ for (const section of PLAYBOOK_SECTIONS) {
354
+ const bullets = bulletsToRender
355
+ .filter((bullet) => bullet.section === section)
356
+ .sort((a, b) => a.bullet_id.localeCompare(b.bullet_id));
357
+ if (bullets.length === 0)
358
+ continue;
359
+ lines.push(`### ${section}`);
360
+ for (const bullet of bullets) {
361
+ lines.push(`- [${bullet.bullet_id}] ${bullet.content}`);
362
+ }
363
+ }
364
+ return lines.join("\n");
365
+ };
366
+ while (packed.length > 0 && renderPacked(packed).length > budget) {
367
+ packed.pop();
368
+ }
369
+ if (packed.length === 0)
370
+ return { text: "", injected_ids: [] };
371
+ const text = renderPacked(packed);
372
+ return {
373
+ text,
374
+ injected_ids: packed.map((bullet) => bullet.bullet_id),
375
+ };
376
+ }
262
377
  export function renderAceContext(options) {
263
378
  const tier = options.tier ?? "compressed";
379
+ const role = options.role.trim().toLowerCase().replace(/^agent-/, "");
264
380
  const active = readRuntimeProfileState();
265
381
  const autonomy = normalizeAutonomyPolicy(active.profile.autonomy);
266
382
  const continuity = normalizeContinuityPolicy(active.profile.continuity);
@@ -272,7 +388,7 @@ export function renderAceContext(options) {
272
388
  policy: continuity,
273
389
  })
274
390
  : undefined;
275
- const tools = scopeTools(options.role, options.task, tier, options.tools);
391
+ const tools = scopeTools(role, options.task, tier, options.tools);
276
392
  const runtimePrompt = renderRuntimePrompt({
277
393
  task: options.task,
278
394
  workspace_path: options.workspace,
@@ -282,15 +398,34 @@ export function renderAceContext(options) {
282
398
  ace_context_snapshot_md: renderAceSnapshotPromptBlock(recall),
283
399
  ace_continuity_packet_md: renderAceContinuityPromptBlock(continuityPacket),
284
400
  });
285
- const roleInstructions = resolveRoleInstructions(options.role);
401
+ const roleInstructions = resolveRoleInstructions(role);
286
402
  const toolCatalog = renderToolCatalogMarkdown(tools, tier);
287
403
  const stateDigest = recall.task_contract.summary;
288
404
  const relevantSkills = inferRelevantSkills(options.task);
289
- const systemPrompt = [
290
- runtimePrompt,
291
- "",
405
+ const playbook = renderPlaybookBlock({
406
+ workspace: options.workspace,
407
+ role,
408
+ modelClass: options.modelClass,
409
+ model: options.model,
410
+ tier,
411
+ });
412
+ const playbookBlock = playbook.text
413
+ ? [
414
+ "## Learned Playbook (advisory — follow unless it conflicts with the task or guardrails)",
415
+ playbook.text,
416
+ "",
417
+ ]
418
+ : [];
419
+ // Prompt-prefix ordering (E16/E17): emit the STABLE block first — role
420
+ // instructions, tool catalog (stable role-keyed order), guardrails, output
421
+ // contract — so llama.cpp/Ollama prefix caching reuses it byte-for-byte
422
+ // across the steps of a plan. The VOLATILE tail (task, recall snapshot,
423
+ // continuity, timestamp via runtimePrompt; plus task-derived skills) comes
424
+ // last, which is also better for small-model recency (task sits adjacent to
425
+ // generation).
426
+ const stableBlock = [
292
427
  "## Active Role",
293
- `- Role: ${options.role}`,
428
+ `- Role: ${role}`,
294
429
  "",
295
430
  "## Role Instructions",
296
431
  roleInstructions,
@@ -298,24 +433,33 @@ export function renderAceContext(options) {
298
433
  "## Tool Scope",
299
434
  toolCatalog || "- No ACE tools available.",
300
435
  "",
301
- "## Relevant Skills",
302
- relevantSkills.length > 0 ? `- ${relevantSkills.join(", ")}` : "- No task-specific skill hint matched.",
303
- "",
304
436
  "## Compact Guardrails",
305
437
  "- Stop if you are about to claim tested, verified, or passed without evidence.",
306
438
  "- If ACE state is thin or contradictory, call recall_context or validate_framework before improvising.",
307
439
  "- For obviously multi-step work, prefer run_orchestrator over a single free-form answer.",
308
440
  "- When you mutate workspace, use the smallest safe mutation lane: structural edit for existing code, write_workspace_file for new/generated artifacts; never assert file creation in prose alone.",
309
441
  "- Do not declare completion while approval, retry, or blocker state remains active.",
310
- ...(ROLE_OUTPUT_SHAPE_GUARDRAILS[options.role.trim().toLowerCase()] ?? []),
442
+ ...(ROLE_OUTPUT_SHAPE_GUARDRAILS[role] ?? []),
311
443
  "",
444
+ ...playbookBlock,
312
445
  "## Output Contract",
313
- 'Respond in JSON only with one of these shapes:',
446
+ "You may reason briefly in plain text before the JSON envelope, but the final machine-readable response must be one JSON object.",
447
+ 'Keep the JSON `thinking` field to one short line. Respond with one of these shapes:',
314
448
  '- `{"status":"tool","thinking":"...","tool_calls":[{"tool":"name","input":{}}]}`',
315
449
  '- `{"status":"message","thinking":"...","message":"..."}`',
316
450
  '- `{"status":"complete","thinking":"...","summary":"...","evidence_refs":["agent-state/..."]}`',
317
451
  '- `{"status":"need_input","thinking":"...","message":"..."}`',
318
- ].join("\n");
452
+ ...(playbook.injected_ids.length > 0
453
+ ? ['When a learned playbook bullet materially helped your message or completion, include `"used_bullets":["pb-..."]` with injected ids only.']
454
+ : []),
455
+ ];
456
+ const volatileTail = [
457
+ runtimePrompt,
458
+ "",
459
+ "## Relevant Skills",
460
+ relevantSkills.length > 0 ? `- ${relevantSkills.join(", ")}` : "- No task-specific skill hint matched.",
461
+ ];
462
+ const systemPrompt = [...stableBlock, "", ...volatileTail].join("\n");
319
463
  return {
320
464
  systemPrompt,
321
465
  tools,
@@ -323,6 +467,7 @@ export function renderAceContext(options) {
323
467
  // TODO: replace with model-aware token counting when context window management lands.
324
468
  tokenEstimate: Math.max(1, Math.ceil(systemPrompt.length / 4)),
325
469
  tier,
470
+ injected_ids: playbook.injected_ids,
326
471
  };
327
472
  }
328
473
  //# sourceMappingURL=ace-context.js.map
@@ -57,6 +57,8 @@ const MUTATION_PREFIXES = [
57
57
  "update_",
58
58
  "write_",
59
59
  ];
60
+ const DIGEST_CACHE_TTL_MS = 60_000;
61
+ const digestCache = new Map();
60
62
  function readIfPresent(filePath) {
61
63
  if (!existsSync(filePath))
62
64
  return undefined;
@@ -188,6 +190,15 @@ export function buildAceDigest(workspaceRoot) {
188
190
  ].filter((value) => Boolean(value));
189
191
  return parts.length > 0 ? parts.join(" | ") : "ACE state detected; load recall_context for task and status.";
190
192
  }
193
+ function buildAceDigestCached(workspaceRoot) {
194
+ const now = Date.now();
195
+ const cached = digestCache.get(workspaceRoot);
196
+ if (cached && cached.expiresAt > now)
197
+ return cached.value;
198
+ const value = buildAceDigest(workspaceRoot);
199
+ digestCache.set(workspaceRoot, { value, expiresAt: now + DIGEST_CACHE_TTL_MS });
200
+ return value;
201
+ }
191
202
  export function buildServerInstructions(workspaceRoot) {
192
203
  const coreFiles = listExistingCoreFiles(workspaceRoot);
193
204
  const stateDirs = listExistingStateDirs(workspaceRoot);
@@ -196,10 +207,11 @@ export function buildServerInstructions(workspaceRoot) {
196
207
  const lines = [
197
208
  "You are operating in an ACE-governed workspace.",
198
209
  coreFiles.length > 0
199
- ? `Ground truth lives in ${coreFiles.join(", ")}.`
200
- : `Ground truth lives in ${ACE_ROOT_REL}/agent-state/* artifacts, not chat history.`,
210
+ ? `Ground truth for task/status/scope/evidence is store-backed under ${coreFiles.join(", ")} (canonical store: ${ACE_ROOT_REL}/agent-state/ace-state.ace).`
211
+ : `Ground truth is store-backed under ${ACE_ROOT_REL}/agent-state/* artifacts (canonical store: ${ACE_ROOT_REL}/agent-state/ace-state.ace), not chat history.`,
212
+ "`ace compact` and the automatic lifecycle sweep intentionally remove the loose agent-state/*.md files from disk after folding them into the store — this is expected, not data loss. Read them back with `ace read agent-state/TASK.md` (or the read_workspace_file / recall_context MCP tools); never recreate swept files.",
201
213
  stateDirs.length > 0
202
- ? `Durable artifacts belong under ${stateDirs.map((entry) => `${entry}/`).join(", ")}.`
214
+ ? `Durable artifacts belong under ${stateDirs.map((entry) => `${entry}/`).join(", ")} and are persisted into the store; the loose copies are sweep inputs, not the source of truth.`
203
215
  : undefined,
204
216
  "Before significant work, call recall_context to load current task, scope, and status.",
205
217
  "Use route_task and get_agent_instructions for role guidance; do not self-assign by guesswork.",
@@ -224,13 +236,15 @@ export function buildHostInstructionText(host, workspaceRoot) {
224
236
  "Full instructions, skills, task packs, and runtime state live in ACE artifacts and should be accessed through the ACE MCP server.",
225
237
  "",
226
238
  "Session start protocol:",
227
- "1. Call `recall_context` to load task, scope, and status from ACE state.",
239
+ "1. Call `recall_context` to load task, scope, and status through the ACE MCP server.",
228
240
  "2. Call `route_task` and `get_agent_instructions` before choosing a role.",
229
241
  "3. Prefer ACE prompts/resources (`ace-orchestrator`, `get_task_pack`, `get_skill_instructions`) over relying on this file.",
242
+ "4. Read/write task state through MCP tools (`recall_context`, `read_workspace_file`, `write_workspace_file`, lifecycle append tools); do not create files under `agent-state/` directly.",
230
243
  "",
231
244
  "Ground truth:",
232
- "- `agent-state/*` is the visible runtime state surface for this workspace.",
233
- "- `agent-state/ace-state.ace` is the canonical store path; legacy nested store layouts are compatibility fallback only.",
245
+ "- ACE MCP tools are the authoritative interface for task state and lifecycle updates.",
246
+ "- `agent-state/ace-state.ace` is the canonical store path; visible `agent-state/*` files are compatibility projections or sweep inputs.",
247
+ "- `ace compact` and the automatic lifecycle sweep intentionally remove loose `agent-state/*` files from disk after folding them into the store — this is expected, not data loss. Do NOT recreate them; retrieve their content via `ace read <path>` or the `read_workspace_file` / `recall_context` MCP tools.",
234
248
  "- `.agents/ACE/tasks/todo.md` is the human-facing todo surface.",
235
249
  "- Hook context is store-backed in `agent-state/ace-state.ace` and consumed without extra workspace projections.",
236
250
  "",
@@ -241,26 +255,26 @@ export function buildBootstrapNudge(workspaceRoot) {
241
255
  const coreFiles = listExistingCoreFiles(workspaceRoot);
242
256
  return [
243
257
  "ACE bootstrap hint: call `recall_context` before significant work so task and scope come from file-backed state.",
244
- `ACE digest: ${buildAceDigest(workspaceRoot)}.`,
258
+ `ACE digest: ${buildAceDigestCached(workspaceRoot)}.`,
245
259
  coreFiles.length > 0 ? `Current core files: ${coreFiles.join(", ")}.` : undefined,
246
260
  ]
247
261
  .filter((value) => Boolean(value))
248
262
  .join(" ");
249
263
  }
250
264
  export function buildBootstrapGuidance(workspaceRoot, toolName) {
251
- return `ACE context loaded via \`${toolName}\`. Current digest: ${buildAceDigest(workspaceRoot)}.`;
265
+ return `ACE context loaded via \`${toolName}\`. Current digest: ${buildAceDigestCached(workspaceRoot)}.`;
252
266
  }
253
267
  export function buildMutationGuidance(workspaceRoot, toolName) {
254
268
  const evidencePath = displayWorkspacePath(workspaceRoot, "agent-state/EVIDENCE_LOG.md");
255
269
  return [
256
- `ACE digest after \`${toolName}\`: ${buildAceDigest(workspaceRoot)}.`,
270
+ `ACE digest after \`${toolName}\`: ${buildAceDigestCached(workspaceRoot)}.`,
257
271
  `If this changed code or state, reconcile \`${evidencePath}\`, run \`execute_gates\` when appropriate, and use structured handoffs/process posts for delegated follow-up.`,
258
272
  ].join(" ");
259
273
  }
260
274
  export function buildRefreshGuidance(workspaceRoot, toolName) {
261
275
  return [
262
276
  `ACE snapshot refresh completed via \`${toolName}\`.`,
263
- `Current digest: ${buildAceDigest(workspaceRoot)}.`,
277
+ `Current digest: ${buildAceDigestCached(workspaceRoot)}.`,
264
278
  ].join(" ");
265
279
  }
266
280
  function isBootstrapTool(toolName) {
@@ -298,6 +312,8 @@ function isCallToolResult(value) {
298
312
  }
299
313
  export function installAceToolGovernance(server, workspaceRoot) {
300
314
  const bootstrappedSessions = new Set();
315
+ const bootstrapNudgeCounts = new Map();
316
+ const mutationGuidanceState = new Map();
301
317
  const wrapToolCallback = (toolName, callback) => async (...args) => {
302
318
  const extra = args.at(-1);
303
319
  const sessionId = typeof extra?.sessionId === "string" ? extra.sessionId : undefined;
@@ -311,7 +327,11 @@ export function installAceToolGovernance(server, workspaceRoot) {
311
327
  }
312
328
  let next = rawResult;
313
329
  if (sessionId && !bootstrappedSessions.has(sessionId) && !isBootstrapTool(toolName)) {
314
- next = appendTextContent(next, buildBootstrapNudge(workspaceRoot));
330
+ const callCount = (bootstrapNudgeCounts.get(sessionId) ?? 0) + 1;
331
+ bootstrapNudgeCounts.set(sessionId, callCount);
332
+ if (callCount === 1 || callCount % 10 === 0) {
333
+ next = appendTextContent(next, buildBootstrapNudge(workspaceRoot));
334
+ }
315
335
  }
316
336
  if (isBootstrapTool(toolName)) {
317
337
  next = appendTextContent(next, buildBootstrapGuidance(workspaceRoot, toolName));
@@ -320,7 +340,16 @@ export function installAceToolGovernance(server, workspaceRoot) {
320
340
  next = appendTextContent(next, buildRefreshGuidance(workspaceRoot, toolName));
321
341
  }
322
342
  else if (isMutationTool(toolName)) {
323
- next = appendTextContent(next, buildMutationGuidance(workspaceRoot, toolName));
343
+ const mutationKey = sessionId ?? "__global__";
344
+ const currentDigest = buildAceDigestCached(workspaceRoot);
345
+ const state = mutationGuidanceState.get(mutationKey) ?? { count: 0 };
346
+ state.count += 1;
347
+ const shouldAppend = state.lastDigest === undefined || state.lastDigest !== currentDigest || state.count % 10 === 0;
348
+ state.lastDigest = currentDigest;
349
+ mutationGuidanceState.set(mutationKey, state);
350
+ if (shouldAppend) {
351
+ next = appendTextContent(next, buildMutationGuidance(workspaceRoot, toolName));
352
+ }
324
353
  }
325
354
  return next;
326
355
  };
@@ -0,0 +1,17 @@
1
+ import type { BridgeUsageTotals } from "./model-bridge.js";
2
+ export interface BridgeTraceLine {
3
+ turn: number;
4
+ envelope_status: string;
5
+ parse_repairs: number;
6
+ drift_repairs: number;
7
+ tool_calls: Array<{
8
+ tool: string;
9
+ ok: boolean;
10
+ reason_code?: string;
11
+ }>;
12
+ usage?: BridgeUsageTotals;
13
+ compressed?: boolean;
14
+ }
15
+ export declare function bridgeTracePath(workspaceRoot: string, runId: string): string;
16
+ export declare function readBridgeTrace(workspaceRoot: string, runId: string): BridgeTraceLine[];
17
+ //# sourceMappingURL=bridge-trace.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ export function bridgeTracePath(workspaceRoot, runId) {
4
+ const safeRunId = runId.replace(/[^a-zA-Z0-9_.-]/g, "_");
5
+ return resolve(workspaceRoot, "agent-state", "bridge-traces", `${safeRunId}.ndjson`);
6
+ }
7
+ export function readBridgeTrace(workspaceRoot, runId) {
8
+ const path = bridgeTracePath(workspaceRoot, runId);
9
+ if (!existsSync(path))
10
+ return [];
11
+ return readFileSync(path, "utf8")
12
+ .split(/\r?\n/)
13
+ .filter(Boolean)
14
+ .flatMap((line) => {
15
+ try {
16
+ return [JSON.parse(line)];
17
+ }
18
+ catch {
19
+ return [];
20
+ }
21
+ });
22
+ }
23
+ //# sourceMappingURL=bridge-trace.js.map
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,11 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ /**
3
+ * Positional arguments, with value-taking flags and their values removed.
4
+ * A bare `.filter(a => !a.startsWith("--"))` drops the flag but keeps its value,
5
+ * which silently folds `--budget-hours 2` into a goal objective. Supports both
6
+ * `--flag value` and `--flag=value`, plus `--` as an end-of-flags separator.
7
+ */
8
+ export declare function positionalArgs(args: readonly string[], valueFlags: ReadonlySet<string>): string[];
9
+ export declare const GOAL_VALUE_FLAGS: ReadonlySet<string>;
10
+ export declare const PLAYBOOK_ADD_VALUE_FLAGS: ReadonlySet<string>;
3
11
  //# sourceMappingURL=cli.d.ts.map