openfox 1.6.103 → 2.0.0-beta.1

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 (39) hide show
  1. package/dist/agent-defaults/builder.agent.md +1 -2
  2. package/dist/agent-defaults/code-reviewer.agent.md +16 -9
  3. package/dist/agent-defaults/planner.agent.md +1 -1
  4. package/dist/agent-defaults/verifier.agent.md +2 -3
  5. package/dist/{auto-compaction-7LMAY5JV.js → auto-compaction-UIWTO3H5.js} +6 -6
  6. package/dist/{chat-handler-ZJVIM7U4.js → chat-handler-IVIWOR3D.js} +9 -10
  7. package/dist/{chunk-PQOK4XCX.js → chunk-4U4CSRBE.js} +7 -5
  8. package/dist/{chunk-RYEZ7LDX.js → chunk-ALH4KM2Y.js} +108 -231
  9. package/dist/{chunk-RCQOHFAN.js → chunk-CEZNMLOO.js} +10 -2
  10. package/dist/{chunk-MEYDERUX.js → chunk-FQLK5GL5.js} +1 -1
  11. package/dist/{chunk-6WZI6F6G.js → chunk-NONI4CRP.js} +19 -1
  12. package/dist/{chunk-IZC2EZ4D.js → chunk-QFSSKIQ3.js} +4 -4
  13. package/dist/{chunk-PDPXHSNZ.js → chunk-UFY4I643.js} +4 -4
  14. package/dist/{chunk-J335KZ6U.js → chunk-XKR3HCAL.js} +14 -3
  15. package/dist/{chunk-LGN6IXGS.js → chunk-ZLUBZ4K4.js} +501 -263
  16. package/dist/cli/dev.js +1 -1
  17. package/dist/cli/index.js +1 -1
  18. package/dist/cli/update.sh +8 -3
  19. package/dist/{events-OKTZRT3Y.js → events-OHJWCI3S.js} +5 -3
  20. package/dist/{folding-Z3OMWNOE.js → folding-U6UHQAS5.js} +4 -2
  21. package/dist/{orchestrator-5EYWRAZ2.js → orchestrator-7ZXM4J22.js} +7 -7
  22. package/dist/package.json +1 -1
  23. package/dist/{processor-XJYCHNV6.js → processor-TKBJLQ2N.js} +7 -7
  24. package/dist/{protocol-CdVNdXi8.d.ts → protocol-BQ12Aw5J.d.ts} +14 -3
  25. package/dist/{protocol-TNC6MCJV.js → protocol-K62EPEGS.js} +5 -3
  26. package/dist/{serve-NWPJO6J7.js → serve-5KA6SSSP.js} +8 -8
  27. package/dist/server/index.d.ts +7 -1
  28. package/dist/server/index.js +7 -7
  29. package/dist/{service-EUOBAATS.js → service-GB7AIOG5.js} +10 -4
  30. package/dist/shared/index.d.ts +2 -2
  31. package/dist/shared/index.js +1 -1
  32. package/dist/{tools-WFIYKFBE.js → tools-Z7X4HXZN.js} +6 -6
  33. package/dist/web/assets/{index-aAebFDVk.css → index-BY8EbT5Z.css} +1 -1
  34. package/dist/web/assets/index-CqOJgw7l.js +299 -0
  35. package/dist/web/index.html +2 -2
  36. package/dist/web/sw.js +1 -1
  37. package/dist/workflow-defaults/default.workflow.json +58 -10
  38. package/package.json +1 -1
  39. package/dist/web/assets/index-DcGVOFte.js +0 -299
@@ -2,7 +2,7 @@ import {
2
2
  createVerifierNudgeConfig,
3
3
  runBuilderTurn,
4
4
  runChatTurn
5
- } from "./chunk-PDPXHSNZ.js";
5
+ } from "./chunk-UFY4I643.js";
6
6
  import {
7
7
  TurnMetrics,
8
8
  agentExists,
@@ -51,7 +51,7 @@ import {
51
51
  saveSkillToProject,
52
52
  skillExists,
53
53
  spawnShellProcess
54
- } from "./chunk-RYEZ7LDX.js";
54
+ } from "./chunk-ALH4KM2Y.js";
55
55
  import {
56
56
  getProject
57
57
  } from "./chunk-XAMAYRDA.js";
@@ -84,6 +84,7 @@ import {
84
84
  emitContextState,
85
85
  emitCriteriaSet,
86
86
  emitCriterionUpdated,
87
+ emitMetadataSet,
87
88
  emitModeChanged,
88
89
  emitPhaseChanged,
89
90
  emitRunningChanged,
@@ -100,14 +101,14 @@ import {
100
101
  updateSessionMetadata,
101
102
  updateSessionProvider,
102
103
  updateSessionRunning
103
- } from "./chunk-J335KZ6U.js";
104
+ } from "./chunk-XKR3HCAL.js";
104
105
  import {
105
106
  initDatabase
106
107
  } from "./chunk-FBGWG4N6.js";
107
108
  import {
108
109
  buildMessagesFromStoredEvents,
109
110
  foldPendingConfirmations
110
- } from "./chunk-6WZI6F6G.js";
111
+ } from "./chunk-NONI4CRP.js";
111
112
  import {
112
113
  createChatErrorMessage,
113
114
  createChatMessageMessage,
@@ -122,7 +123,7 @@ import {
122
123
  parseClientMessage,
123
124
  serializeServerMessage,
124
125
  storedEventToServerMessage
125
- } from "./chunk-RCQOHFAN.js";
126
+ } from "./chunk-CEZNMLOO.js";
126
127
  import {
127
128
  EventEmitter,
128
129
  provideAnswer
@@ -132,7 +133,7 @@ import {
132
133
  } from "./chunk-VUQCQXXJ.js";
133
134
  import {
134
135
  createServerMessage
135
- } from "./chunk-MEYDERUX.js";
136
+ } from "./chunk-FQLK5GL5.js";
136
137
  import {
137
138
  getGlobalConfigDir
138
139
  } from "./chunk-CQGTEGKL.js";
@@ -158,161 +159,181 @@ import { createServer as createViteServer } from "vite";
158
159
  import { setTimeout as sleep } from "timers/promises";
159
160
  var RULES = [
160
161
  // -------------------------------------------------------------------------
161
- // Criterion Tools
162
+ // Session Metadata Tools (criteria, todos, review_findings)
162
163
  // -------------------------------------------------------------------------
163
164
  // Multi-criteria: ID "crit-a": "First" and ID "crit-b": "Second"
164
165
  {
165
166
  match: /ID\s*["']([a-z0-9-]+)["']:\s*["']([^"']+)["'][\s\S]*ID\s*["']([a-z0-9-]+)["']:\s*["']([^"']+)["']/i,
166
167
  tools: [
167
- { name: "criterion", arguments: { action: "add", id: "$1", description: "$2" } },
168
- { name: "criterion", arguments: { action: "add", id: "$3", description: "$4" } }
168
+ { name: "session_metadata", arguments: { action: "add", key: "criteria", id: "$1", description: "$2" } },
169
+ { name: "session_metadata", arguments: { action: "add", key: "criteria", id: "$3", description: "$4" } }
169
170
  ],
170
171
  response: "Added both criteria."
171
172
  },
172
173
  // Single criterion: "Add criterion ID "test-1" with description "The tests pass""
173
174
  {
174
175
  match: /Add criterion ID\s*["']([a-z0-9-]+)["']\s*with description\s*["']([^"']+)["'][\s\S]*/i,
175
- tools: [{ name: "criterion", arguments: { action: "add", id: "$1", description: "$2" } }],
176
+ tools: [{ name: "session_metadata", arguments: { action: "add", key: "criteria", id: "$1", description: "$2" } }],
176
177
  response: "Added the criterion."
177
178
  },
178
179
  // Single criterion: ID "test-1" with/: description "The tests pass"
179
180
  {
180
181
  match: /ID\s*["']([a-z0-9-]+)["'].*description\s*["']([^"']+)["']/i,
181
- tools: [{ name: "criterion", arguments: { action: "add", id: "$1", description: "$2" } }],
182
+ tools: [{ name: "session_metadata", arguments: { action: "add", key: "criteria", id: "$1", description: "$2" } }],
182
183
  response: "Added the criterion."
183
184
  },
184
185
  {
185
186
  match: /Add these two acceptance criteria:\s*1\.\s*([^\n]+)\s*2\.\s*([^\n]+)\s*Use criterion for each\./i,
186
187
  tools: [
187
- { name: "criterion", arguments: { action: "add", id: "criterion-1", description: "$1" } },
188
- { name: "criterion", arguments: { action: "add", id: "criterion-2", description: "$2" } }
188
+ { name: "session_metadata", arguments: { action: "add", key: "criteria", id: "criterion-1", description: "$1" } },
189
+ { name: "session_metadata", arguments: { action: "add", key: "criteria", id: "criterion-2", description: "$2" } }
189
190
  ],
190
191
  response: "Added both criteria."
191
192
  },
192
193
  {
193
194
  match: /Add these two acceptance criteria:[\s\S]*?1\.\s*([^\n]+)[\s\S]*?2\.\s*([^\n]+)[\s\S]*?Use (?:add_)?criterion for each one\./i,
194
195
  tools: [
195
- { name: "criterion", arguments: { action: "add", id: "criterion-1", description: "$1" } },
196
- { name: "criterion", arguments: { action: "add", id: "criterion-2", description: "$2" } }
196
+ { name: "session_metadata", arguments: { action: "add", key: "criteria", id: "criterion-1", description: "$1" } },
197
+ { name: "session_metadata", arguments: { action: "add", key: "criteria", id: "criterion-2", description: "$2" } }
197
198
  ],
198
199
  response: "Added both criteria."
199
200
  },
200
201
  {
201
202
  match: /Add these two acceptance criteria:[\s\S]*?1\.\s*([^\n]+)[\s\S]*?2\.\s*([^\n]+)/i,
202
203
  tools: [
203
- { name: "criterion", arguments: { action: "add", id: "criterion-1", description: "$1" } },
204
- { name: "criterion", arguments: { action: "add", id: "criterion-2", description: "$2" } }
204
+ { name: "session_metadata", arguments: { action: "add", key: "criteria", id: "criterion-1", description: "$1" } },
205
+ { name: "session_metadata", arguments: { action: "add", key: "criteria", id: "criterion-2", description: "$2" } }
205
206
  ],
206
207
  response: "Added both criteria."
207
208
  },
208
209
  {
209
210
  match: /Add criterion:\s*([\s\S]+?)\s*Use criterion\.?/i,
210
- tools: [{ name: "criterion", arguments: { action: "add", id: "$auto", description: "$1" } }],
211
+ tools: [
212
+ { name: "session_metadata", arguments: { action: "add", key: "criteria", id: "$auto", description: "$1" } }
213
+ ],
211
214
  response: "Added the criterion."
212
215
  },
216
+ // Propose acceptance criteria (for planning prompts)
217
+ {
218
+ match: /propose acceptance criteria/i,
219
+ tools: [
220
+ {
221
+ name: "session_metadata",
222
+ arguments: {
223
+ action: "add",
224
+ key: "criteria",
225
+ id: "criteria-1",
226
+ description: "A multiply function exists in math.ts that takes two numbers"
227
+ }
228
+ },
229
+ {
230
+ name: "session_metadata",
231
+ arguments: {
232
+ action: "add",
233
+ key: "criteria",
234
+ id: "criteria-2",
235
+ description: "The multiply function returns the correct product"
236
+ }
237
+ }
238
+ ],
239
+ response: "Added criteria for the multiply function."
240
+ },
213
241
  // Single criterion: ID "test-1": "The tests pass" (colon format)
214
242
  {
215
243
  match: /ID\s*["']([a-z0-9-]+)["']\s*:\s*["']([^"']+)["']/i,
216
- tools: [{ name: "criterion", arguments: { action: "add", id: "$1", description: "$2" } }],
244
+ tools: [{ name: "session_metadata", arguments: { action: "add", key: "criteria", id: "$1", description: "$2" } }],
217
245
  response: "Added the criterion."
218
246
  },
219
- // complete_criterion to mark "id" as done
220
- {
221
- match: /complete_criterion.*mark\s*["']([a-z0-9-]+)["']/i,
222
- tools: [{ name: "criterion", arguments: { action: "complete", id: "$1", reason: "Completed successfully" } }],
223
- response: "Marked criterion as complete."
224
- },
225
- // complete_criterion with ID
247
+ // session_metadata to mark criteria as completed (natural language)
226
248
  {
227
- match: /complete_criterion.*["']([a-z0-9-]+)["']/i,
228
- tools: [{ name: "criterion", arguments: { action: "complete", id: "$1", reason: "Completed successfully" } }],
249
+ match: /session_metadata.*(?:mark|status.*completed).*["']([a-z0-9-]+)["']/i,
250
+ tools: [
251
+ { name: "session_metadata", arguments: { action: "update", key: "criteria", id: "$1", status: "completed" } }
252
+ ],
229
253
  response: "Marked criterion as complete."
230
254
  },
231
- // criterion with action "complete" for "id"
255
+ // session_metadata to mark criteria as completed (explicit args)
232
256
  {
233
- match: /criterion.*action\s*["']complete["'].*["']([a-z0-9-]+)["']/i,
234
- tools: [{ name: "criterion", arguments: { action: "complete", id: "$1", reason: "Completed successfully" } }],
257
+ match: /session_metadata.*action\s*["']update["'].*key\s*["']criteria["'].*id\s*["']([a-z0-9-]+)["'].*status\s*["']completed["']/i,
258
+ tools: [
259
+ { name: "session_metadata", arguments: { action: "update", key: "criteria", id: "$1", status: "completed" } }
260
+ ],
235
261
  response: "Marked criterion as complete."
236
262
  },
237
- // remove_criterion to remove "id"
238
- {
239
- match: /remove_criterion.*remove\s*["']([a-z0-9-]+)["']/i,
240
- tools: [{ name: "criterion", arguments: { action: "remove", id: "$1" } }],
241
- response: "Removed the criterion."
242
- },
243
- // remove_criterion with ID
244
- {
245
- match: /remove_criterion.*["']([a-z0-9-]+)["']/i,
246
- tools: [{ name: "criterion", arguments: { action: "remove", id: "$1" } }],
247
- response: "Removed the criterion."
248
- },
249
- // update_criterion to change "id" description to "new"
263
+ // session_metadata to mark criteria as passed
250
264
  {
251
- match: /update_criterion.*change\s*["']([a-z0-9-]+)["'].*to\s*["']([^"']+)["']/i,
252
- tools: [{ name: "criterion", arguments: { action: "update", id: "$1", description: "$2" } }],
253
- response: "Updated the criterion."
265
+ match: /session_metadata.*action\s*["']update["'].*key\s*["']criteria["'].*id\s*["']([a-z0-9-]+)["'].*status\s*["']passed["']/i,
266
+ tools: [{ name: "session_metadata", arguments: { action: "update", key: "criteria", id: "$1", status: "passed" } }],
267
+ response: "Criterion passed."
254
268
  },
255
- // update_criterion with ID and description
269
+ // session_metadata to mark criteria as failed
256
270
  {
257
- match: /update_criterion.*["']([a-z0-9-]+)["'].*["']([^"']+)["']/i,
258
- tools: [{ name: "criterion", arguments: { action: "update", id: "$1", description: "$2" } }],
259
- response: "Updated the criterion."
271
+ match: /session_metadata.*action\s*["']update["'].*key\s*["']criteria["'].*id\s*["']([a-z0-9-]+)["'].*status\s*["']failed["']/i,
272
+ tools: [
273
+ {
274
+ name: "session_metadata",
275
+ arguments: { action: "update", key: "criteria", id: "$1", status: "failed", reason: "Verification failed" }
276
+ }
277
+ ],
278
+ response: "Criterion failed."
260
279
  },
261
- // get_criteria
280
+ // get criteria via session_metadata
262
281
  {
263
282
  match: /get_criteria|show.*criteria|list.*criteria/i,
264
- tools: [{ name: "criterion", arguments: { action: "get" } }],
283
+ tools: [{ name: "session_metadata", arguments: { action: "get", key: "criteria" } }],
265
284
  response: "Here are the current criteria."
266
285
  },
267
- // pass_criterion
286
+ // Update criterion via session_metadata
268
287
  {
269
- match: /pass_criterion.*["']([a-z0-9-]+)["']/i,
270
- tools: [{ name: "criterion", arguments: { action: "pass", id: "$1", reason: "Verified successfully" } }],
271
- response: "Criterion passed."
288
+ match: /session_metadata.*action\s*["']update["'].*description/i,
289
+ tools: [
290
+ {
291
+ name: "session_metadata",
292
+ arguments: { action: "update", key: "criteria", id: "0", description: "Updated description" }
293
+ }
294
+ ],
295
+ response: "Updated the criterion."
272
296
  },
273
- // fail_criterion
297
+ // Remove criterion via session_metadata
274
298
  {
275
- match: /fail_criterion.*["']([a-z0-9-]+)["']/i,
276
- tools: [{ name: "criterion", arguments: { action: "fail", id: "$1", reason: "Verification failed" } }],
277
- response: "Criterion failed."
299
+ match: /session_metadata.*action\s*["']remove["']/i,
300
+ tools: [{ name: "session_metadata", arguments: { action: "remove", key: "criteria", id: "0" } }],
301
+ response: "Removed the criterion."
278
302
  },
279
- // Generic add criterion (fallback)
303
+ // Generic add criterion via session_metadata
280
304
  {
281
305
  match: /add.*criterion/i,
282
- tools: [{ name: "criterion", arguments: { action: "add", description: "Test criterion" } }],
306
+ tools: [{ name: "session_metadata", arguments: { action: "add", key: "criteria", description: "Test criterion" } }],
283
307
  response: "Added the criterion."
284
308
  },
285
- // Verifier nudge with fail criterion (check for "fail" in ID) - MUST come before general pass rule
286
- {
287
- match: /Use criterion with action ["']pass["'] or ["']fail["'].*criterion.*:\s*([a-z0-9-]*fail[a-z0-9-]*)/i,
288
- tools: [{ name: "criterion", arguments: { action: "fail", id: "$1", reason: "Verification failed" } }],
289
- response: "Criterion failed."
290
- },
291
- // Verifier nudge: Use criterion with action "pass" or "fail" for criterion-id-1, criterion-id-2
309
+ // -------------------------------------------------------------------------
310
+ // Session Metadata Tools
311
+ // -------------------------------------------------------------------------
292
312
  {
293
- match: /Use criterion with action ["']pass["'] or ["']fail["'].*criterion:\s*([a-z0-9-]+)/i,
294
- tools: [{ name: "criterion", arguments: { action: "pass", id: "$1", reason: "Verified successfully" } }],
295
- response: "Criterion passed."
313
+ match: /session_metadata.*action\s*["']add["'].*key\s*["']criteria["'].*description\s*["']([^"']+)["']/i,
314
+ tools: [{ name: "session_metadata", arguments: { action: "add", key: "criteria", description: "$1" } }],
315
+ response: "Added criterion."
296
316
  },
297
- // Verifier nudge with multiple criteria
298
317
  {
299
- match: /Use criterion with action ["']pass["'] or ["']fail["'].*criterion:\s*([a-z0-9-]+),\s*([a-z0-9-]+)/i,
318
+ match: /session_metadata.*action\s*["']update["'].*key\s*["']criteria["'].*id\s*["']([a-z0-9-]+)["'].*status\s*["']completed["']/i,
300
319
  tools: [
301
- { name: "criterion", arguments: { action: "pass", id: "$1", reason: "Verified successfully" } },
302
- { name: "criterion", arguments: { action: "pass", id: "$2", reason: "Verified successfully" } }
320
+ { name: "session_metadata", arguments: { action: "update", key: "criteria", id: "$1", status: "completed" } }
303
321
  ],
304
- response: "Criteria passed."
322
+ response: "Criterion completed."
305
323
  },
306
- // Verifier should fail a criterion
307
324
  {
308
- match: /Verifier should fail.*criterion.*ID\s*["']([a-z0-9-]+)["']/i,
309
- tools: [{ name: "criterion", arguments: { action: "fail", id: "$1", reason: "Verification failed" } }],
310
- response: "Criterion failed."
325
+ match: /session_metadata.*action\s*["']update["'].*key\s*["']criteria["'].*id\s*["']([a-z0-9-]+)["'].*status\s*["']passed["']/i,
326
+ tools: [{ name: "session_metadata", arguments: { action: "update", key: "criteria", id: "$1", status: "passed" } }],
327
+ response: "Criterion passed."
311
328
  },
312
- // Verifier fail_criterion command
313
329
  {
314
- match: /fail_criterion.*ID\s*["']([a-z0-9-]+)["']/i,
315
- tools: [{ name: "criterion", arguments: { action: "fail", id: "$1", reason: "Verification failed" } }],
330
+ match: /session_metadata.*action\s*["']update["'].*key\s*["']criteria["'].*id\s*["']([a-z0-9-]+)["'].*status\s*["']failed["']/i,
331
+ tools: [
332
+ {
333
+ name: "session_metadata",
334
+ arguments: { action: "update", key: "criteria", id: "$1", status: "failed", reason: "Verification failed" }
335
+ }
336
+ ],
316
337
  response: "Criterion failed."
317
338
  },
318
339
  // -------------------------------------------------------------------------
@@ -617,13 +638,19 @@ var RULES = [
617
638
  match: /todo_write.*Read files.*Make changes/i,
618
639
  tools: [
619
640
  {
620
- name: "todo",
641
+ name: "session_metadata",
621
642
  arguments: {
622
- action: "write",
623
- todos: [
624
- { content: "Read files", status: "in_progress" },
625
- { content: "Make changes", status: "pending" }
626
- ]
643
+ action: "add",
644
+ key: "todos",
645
+ description: "Read files"
646
+ }
647
+ },
648
+ {
649
+ name: "session_metadata",
650
+ arguments: {
651
+ action: "add",
652
+ key: "todos",
653
+ description: "Make changes"
627
654
  }
628
655
  }
629
656
  ],
@@ -633,10 +660,11 @@ var RULES = [
633
660
  match: /todo_write|todo.*list/i,
634
661
  tools: [
635
662
  {
636
- name: "todo",
663
+ name: "session_metadata",
637
664
  arguments: {
638
- action: "write",
639
- todos: [{ content: "Test task", status: "pending" }]
665
+ action: "add",
666
+ key: "todos",
667
+ description: "Test task"
640
668
  }
641
669
  }
642
670
  ],
@@ -836,31 +864,69 @@ function getInstructionAwareResponse(request) {
836
864
  function getPromptAwareToolResponse(prompt) {
837
865
  const exactCommandMatch = prompt.match(/Run the exact command:\s*(.+)$/i) ?? prompt.match(/^Run exactly:\s*(.+)$/i);
838
866
  const quotedValues = [...prompt.matchAll(/"([^"]+)"/g)].map((match) => match[1]);
867
+ if (/Use the session_metadata tool with key.*todos.*to create a todo list/i.test(prompt)) {
868
+ return {
869
+ tools: [
870
+ {
871
+ name: "session_metadata",
872
+ arguments: {
873
+ action: "add",
874
+ key: "todos",
875
+ description: "Read files"
876
+ }
877
+ },
878
+ {
879
+ name: "session_metadata",
880
+ arguments: {
881
+ action: "add",
882
+ key: "todos",
883
+ description: "Make changes"
884
+ }
885
+ }
886
+ ],
887
+ response: "Created todo list."
888
+ };
889
+ }
839
890
  if (/Use the todo_write tool to create a todo list with 2 items/i.test(prompt)) {
840
891
  return {
841
892
  tools: [
842
893
  {
843
- name: "todo",
894
+ name: "session_metadata",
895
+ arguments: {
896
+ action: "add",
897
+ key: "todos",
898
+ description: "Read files"
899
+ }
900
+ },
901
+ {
902
+ name: "session_metadata",
844
903
  arguments: {
845
- action: "write",
846
- todos: [
847
- { content: "Read files", status: "in_progress" },
848
- { content: "Make changes", status: "pending" }
849
- ]
904
+ action: "add",
905
+ key: "todos",
906
+ description: "Make changes"
850
907
  }
851
908
  }
852
909
  ],
853
910
  response: "Created todo list."
854
911
  };
855
912
  }
856
- if (/First call get_criteria to see what needs to be done, then create src\/test\.ts and call complete_criterion for ["']test-file["']\./i.test(
913
+ if (/First call get_criteria to see what needs to be done, then create src\/test\.ts and call session_metadata to mark criteria as completed for ["']test-file["']\./i.test(
857
914
  prompt
858
915
  )) {
859
916
  return {
860
917
  tools: [
861
- { name: "criterion", arguments: { action: "get" } },
918
+ { name: "session_metadata", arguments: { action: "get", key: "criteria" } },
862
919
  { name: "write_file", arguments: { path: "src/test.ts", content: "export const created = true" } },
863
- { name: "criterion", arguments: { action: "complete", id: "test-file", reason: "Created the requested file" } },
920
+ {
921
+ name: "session_metadata",
922
+ arguments: {
923
+ action: "update",
924
+ key: "criteria",
925
+ status: "completed",
926
+ id: "test-file",
927
+ reason: "Created the requested file"
928
+ }
929
+ },
864
930
  { name: "step_done", arguments: {} }
865
931
  ],
866
932
  response: "Reviewed the criteria, created the file, and completed the criterion."
@@ -871,8 +937,14 @@ function getPromptAwareToolResponse(prompt) {
871
937
  tools: [
872
938
  { name: "write_file", arguments: { path: "src/utils.ts", content: "export const created = true" } },
873
939
  {
874
- name: "criterion",
875
- arguments: { action: "complete", id: "file-created", reason: "Created the requested file" }
940
+ name: "session_metadata",
941
+ arguments: {
942
+ action: "update",
943
+ key: "criteria",
944
+ status: "completed",
945
+ id: "file-created",
946
+ reason: "Created the requested file"
947
+ }
876
948
  },
877
949
  { name: "step_done", arguments: {} }
878
950
  ],
@@ -973,9 +1045,11 @@ function getConversationAwareToolResponse(request) {
973
1045
  tools.push(
974
1046
  { name: "read_file", arguments: { path: "src" } },
975
1047
  {
976
- name: "criterion",
1048
+ name: "session_metadata",
977
1049
  arguments: {
978
- action: "complete",
1050
+ action: "update",
1051
+ key: "criteria",
1052
+ status: "completed",
979
1053
  id: "inspect-src",
980
1054
  reason: "Inspected the src directory and reported what exists"
981
1055
  }
@@ -985,15 +1059,27 @@ function getConversationAwareToolResponse(request) {
985
1059
  }
986
1060
  if (conversationText.includes("trivial-pass")) {
987
1061
  tools.push({
988
- name: "criterion",
989
- arguments: { action: "complete", id: "trivial-pass", reason: "Trivial criterion passes immediately" }
1062
+ name: "session_metadata",
1063
+ arguments: {
1064
+ action: "update",
1065
+ key: "criteria",
1066
+ status: "completed",
1067
+ id: "trivial-pass",
1068
+ reason: "Trivial criterion passes immediately"
1069
+ }
990
1070
  });
991
1071
  completedCriteria.push("trivial-pass");
992
1072
  }
993
1073
  if (conversationText.includes("verify-fail")) {
994
1074
  tools.push({
995
- name: "criterion",
996
- arguments: { action: "complete", id: "verify-fail", reason: "Prepared criterion for verification" }
1075
+ name: "session_metadata",
1076
+ arguments: {
1077
+ action: "update",
1078
+ key: "criteria",
1079
+ status: "completed",
1080
+ id: "verify-fail",
1081
+ reason: "Prepared criterion for verification"
1082
+ }
997
1083
  });
998
1084
  completedCriteria.push("verify-fail");
999
1085
  }
@@ -1001,16 +1087,28 @@ function getConversationAwareToolResponse(request) {
1001
1087
  tools.push(
1002
1088
  { name: "write_file", arguments: { path: "src/utils.ts", content: "export const created = true" } },
1003
1089
  {
1004
- name: "criterion",
1005
- arguments: { action: "complete", id: "file-created", reason: "Created the requested file" }
1090
+ name: "session_metadata",
1091
+ arguments: {
1092
+ action: "update",
1093
+ key: "criteria",
1094
+ status: "completed",
1095
+ id: "file-created",
1096
+ reason: "Created the requested file"
1097
+ }
1006
1098
  }
1007
1099
  );
1008
1100
  completedCriteria.push("file-created");
1009
1101
  }
1010
1102
  if (tools.length === 0) {
1011
1103
  tools.push({
1012
- name: "criterion",
1013
- arguments: { action: "complete", id: "mock-crit", reason: "Completed for testing" }
1104
+ name: "session_metadata",
1105
+ arguments: {
1106
+ action: "update",
1107
+ key: "criteria",
1108
+ status: "completed",
1109
+ id: "mock-crit",
1110
+ reason: "Completed for testing"
1111
+ }
1014
1112
  });
1015
1113
  completedCriteria.push("mock-crit");
1016
1114
  }
@@ -1023,7 +1121,16 @@ function getConversationAwareToolResponse(request) {
1023
1121
  if (/fulfil the \d+ criteria/i.test(prompt)) {
1024
1122
  return {
1025
1123
  tools: [
1026
- { name: "criterion", arguments: { action: "complete", id: "mock-crit", reason: "Completed for testing" } },
1124
+ {
1125
+ name: "session_metadata",
1126
+ arguments: {
1127
+ action: "update",
1128
+ key: "criteria",
1129
+ status: "completed",
1130
+ id: "mock-crit",
1131
+ reason: "Completed for testing"
1132
+ }
1133
+ },
1027
1134
  { name: "step_done", arguments: {} }
1028
1135
  ],
1029
1136
  response: "Completed criterion and finished step."
@@ -1035,30 +1142,60 @@ function getConversationAwareToolResponse(request) {
1035
1142
  tools.push(
1036
1143
  { name: "write_file", arguments: { path: "src/utils.ts", content: "export const created = true" } },
1037
1144
  {
1038
- name: "criterion",
1039
- arguments: { action: "complete", id: "file-created", reason: "Created the requested file" }
1145
+ name: "session_metadata",
1146
+ arguments: {
1147
+ action: "update",
1148
+ key: "criteria",
1149
+ status: "completed",
1150
+ id: "file-created",
1151
+ reason: "Created the requested file"
1152
+ }
1040
1153
  }
1041
1154
  );
1042
1155
  } else if (conversationText.includes("trivial-pass")) {
1043
1156
  tools.push({
1044
- name: "criterion",
1045
- arguments: { action: "complete", id: "trivial-pass", reason: "Trivial criterion passes immediately" }
1157
+ name: "session_metadata",
1158
+ arguments: {
1159
+ action: "update",
1160
+ key: "criteria",
1161
+ status: "completed",
1162
+ id: "trivial-pass",
1163
+ reason: "Trivial criterion passes immediately"
1164
+ }
1046
1165
  });
1047
1166
  } else if (conversationText.includes("verify-fail")) {
1048
1167
  tools.push({
1049
- name: "criterion",
1050
- arguments: { action: "complete", id: "verify-fail", reason: "Prepared criterion for verification" }
1168
+ name: "session_metadata",
1169
+ arguments: {
1170
+ action: "update",
1171
+ key: "criteria",
1172
+ status: "completed",
1173
+ id: "verify-fail",
1174
+ reason: "Prepared criterion for verification"
1175
+ }
1051
1176
  });
1052
1177
  } else {
1053
1178
  if (/1 criteria remaining/i.test(prompt)) {
1054
1179
  tools.push({
1055
- name: "criterion",
1056
- arguments: { action: "complete", id: "trivial-pass", reason: "Trivial criterion passes immediately" }
1180
+ name: "session_metadata",
1181
+ arguments: {
1182
+ action: "update",
1183
+ key: "criteria",
1184
+ status: "completed",
1185
+ id: "trivial-pass",
1186
+ reason: "Trivial criterion passes immediately"
1187
+ }
1057
1188
  });
1058
1189
  } else {
1059
1190
  tools.push({
1060
- name: "criterion",
1061
- arguments: { action: "complete", id: "mock-crit", reason: "Completed for testing" }
1191
+ name: "session_metadata",
1192
+ arguments: {
1193
+ action: "update",
1194
+ key: "criteria",
1195
+ status: "completed",
1196
+ id: "mock-crit",
1197
+ reason: "Completed for testing"
1198
+ }
1062
1199
  });
1063
1200
  }
1064
1201
  }
@@ -1071,7 +1208,7 @@ function getConversationAwareToolResponse(request) {
1071
1208
  if (/Verify each criterion marked \[NEEDS VERIFICATION\]\./i.test(prompt)) {
1072
1209
  const alreadyVerified = request.messages.some(
1073
1210
  (m) => m.role === "assistant" && m.toolCalls?.some(
1074
- (tc) => tc.name === "criterion" && (tc.arguments["action"] === "pass" || tc.arguments["action"] === "fail")
1211
+ (tc) => tc.name === "session_metadata" && (tc.arguments["action"] === "pass" || tc.arguments["action"] === "fail")
1075
1212
  )
1076
1213
  );
1077
1214
  if (alreadyVerified) {
@@ -1081,16 +1218,24 @@ function getConversationAwareToolResponse(request) {
1081
1218
  const terminalizedCriteria = [];
1082
1219
  if (conversationText.includes("trivial-pass")) {
1083
1220
  tools.push({
1084
- name: "criterion",
1085
- arguments: { action: "pass", id: "trivial-pass", reason: "Verified successfully" }
1221
+ name: "session_metadata",
1222
+ arguments: {
1223
+ action: "update",
1224
+ key: "criteria",
1225
+ status: "passed",
1226
+ id: "trivial-pass",
1227
+ reason: "Verified successfully"
1228
+ }
1086
1229
  });
1087
1230
  terminalizedCriteria.push("trivial-pass");
1088
1231
  }
1089
1232
  if (conversationText.includes("inspect-src")) {
1090
1233
  tools.push({
1091
- name: "criterion",
1234
+ name: "session_metadata",
1092
1235
  arguments: {
1093
- action: "pass",
1236
+ action: "update",
1237
+ key: "criteria",
1238
+ status: "passed",
1094
1239
  id: "inspect-src",
1095
1240
  reason: "Verified the src directory was inspected successfully"
1096
1241
  }
@@ -1099,9 +1244,11 @@ function getConversationAwareToolResponse(request) {
1099
1244
  }
1100
1245
  if (conversationText.includes("verify-fail")) {
1101
1246
  tools.push({
1102
- name: "criterion",
1247
+ name: "session_metadata",
1103
1248
  arguments: {
1104
- action: "complete",
1249
+ action: "update",
1250
+ key: "criteria",
1251
+ status: "completed",
1105
1252
  id: "verify-fail",
1106
1253
  reason: "Verification fails intentionally for this criterion"
1107
1254
  }
@@ -1110,15 +1257,27 @@ function getConversationAwareToolResponse(request) {
1110
1257
  }
1111
1258
  if (conversationText.includes("file-created")) {
1112
1259
  tools.push({
1113
- name: "criterion",
1114
- arguments: { action: "pass", id: "file-created", reason: "Verified the file was created successfully" }
1260
+ name: "session_metadata",
1261
+ arguments: {
1262
+ action: "update",
1263
+ key: "criteria",
1264
+ status: "passed",
1265
+ id: "file-created",
1266
+ reason: "Verified the file was created successfully"
1267
+ }
1115
1268
  });
1116
1269
  terminalizedCriteria.push("file-created");
1117
1270
  }
1118
1271
  if (conversationText.includes("impossible/path")) {
1119
1272
  tools.push({
1120
- name: "criterion",
1121
- arguments: { action: "complete", id: "auto-impossible", reason: "Impossible path does not exist" }
1273
+ name: "session_metadata",
1274
+ arguments: {
1275
+ action: "update",
1276
+ key: "criteria",
1277
+ status: "completed",
1278
+ id: "auto-impossible",
1279
+ reason: "Impossible path does not exist"
1280
+ }
1122
1281
  });
1123
1282
  terminalizedCriteria.push("auto-impossible");
1124
1283
  }
@@ -1290,7 +1449,7 @@ function createMockLLMClient() {
1290
1449
 
1291
1450
  // src/server/ws/server.ts
1292
1451
  import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
1293
- import { spawn as spawn2 } from "child_process";
1452
+ import { spawn as spawn3 } from "child_process";
1294
1453
  import { createHash as createHash2 } from "crypto";
1295
1454
 
1296
1455
  // src/server/ws/terminal.ts
@@ -1663,16 +1822,79 @@ async function deleteWorkflow(configDir, workflowId) {
1663
1822
  }
1664
1823
  }
1665
1824
 
1666
- // src/server/runner/types.ts
1667
- var RUNNER_CONFIG = {
1668
- maxVerifyRetries: 10
1669
- // Max times to retry a failing criterion
1670
- };
1671
-
1672
1825
  // src/server/workflows/types.ts
1673
1826
  var TERMINAL_DONE = "$done";
1674
1827
  var TERMINAL_BLOCKED = "$blocked";
1675
1828
 
1829
+ // src/server/git/diff.ts
1830
+ import { spawn as spawn2 } from "child_process";
1831
+ function getGitDiffFiles(cwd) {
1832
+ return new Promise((resolve3) => {
1833
+ const diffProc = spawn2("git", ["diff", "--name-status", "HEAD"], { cwd, stdio: ["ignore", "pipe", "pipe"] });
1834
+ const statusProc = spawn2("git", ["status", "--porcelain"], { cwd, stdio: ["ignore", "pipe", "pipe"] });
1835
+ let diffStdout = "";
1836
+ let statusStdout = "";
1837
+ let diffExited = false;
1838
+ let statusExited = false;
1839
+ let diffCode = null;
1840
+ let statusCode = null;
1841
+ const collect = () => {
1842
+ if (!diffExited || !statusExited) return;
1843
+ const files = [];
1844
+ if (diffCode === 0) {
1845
+ for (const line of diffStdout.split("\n")) {
1846
+ if (!line.trim()) continue;
1847
+ const [statusChar, ...pathParts] = line.split(" ");
1848
+ const path = pathParts.join(" ") || statusChar || "";
1849
+ if (!path) continue;
1850
+ const status = statusChar === "A" ? "added" : statusChar === "D" ? "deleted" : "modified";
1851
+ files.push({ path, status, additions: 0, deletions: 0 });
1852
+ }
1853
+ }
1854
+ if (statusCode === 0) {
1855
+ for (const line of statusStdout.split("\n")) {
1856
+ if (!line.startsWith("?? ")) continue;
1857
+ const path = line.slice(3).trim();
1858
+ if (!path) continue;
1859
+ files.push({ path, status: "added", additions: 0, deletions: 0 });
1860
+ }
1861
+ }
1862
+ resolve3(files);
1863
+ };
1864
+ diffProc.stdout.on("data", (data) => {
1865
+ diffStdout += data.toString();
1866
+ });
1867
+ statusProc.stdout.on("data", (data) => {
1868
+ statusStdout += data.toString();
1869
+ });
1870
+ diffProc.on("close", (code) => {
1871
+ diffExited = true;
1872
+ diffCode = code;
1873
+ collect();
1874
+ });
1875
+ statusProc.on("close", (code) => {
1876
+ statusExited = true;
1877
+ statusCode = code;
1878
+ collect();
1879
+ });
1880
+ diffProc.on("error", () => {
1881
+ diffExited = true;
1882
+ diffCode = 1;
1883
+ collect();
1884
+ });
1885
+ statusProc.on("error", () => {
1886
+ statusExited = true;
1887
+ statusCode = 1;
1888
+ collect();
1889
+ });
1890
+ });
1891
+ }
1892
+ async function formatGitDiffFiles(cwd) {
1893
+ const files = await getGitDiffFiles(cwd);
1894
+ if (files.length === 0) return "(none)";
1895
+ return files.map((f) => `- ${f.path} (${f.status})`).join("\n");
1896
+ }
1897
+
1676
1898
  // src/server/workflows/shell.ts
1677
1899
  function executeShellCommand(command, cwd, timeout, signal) {
1678
1900
  return new Promise((resolve3, reject) => {
@@ -1741,16 +1963,15 @@ var TEMPLATE_VARIABLES = [
1741
1963
  { name: "criteriaList", description: "Formatted list of all criteria with status" },
1742
1964
  { name: "modifiedFiles", description: "List of modified files" }
1743
1965
  ];
1744
- function formatCriteriaList(criteria) {
1745
- if (criteria.length === 0) return "(none)";
1746
- return criteria.map((c) => {
1747
- const status = c.status.type === "passed" ? "[PASSED]" : c.status.type === "completed" ? "[NEEDS VERIFICATION]" : c.status.type === "failed" ? "[FAILED]" : "[NOT COMPLETED]";
1748
- return `- **${c.id}** ${status}: ${c.description}`;
1966
+ function formatCriteriaList(entries) {
1967
+ if (entries.length === 0) return "(none)";
1968
+ return entries.map((e) => {
1969
+ const status = e.status === "passed" ? "[PASSED]" : e.status === "completed" ? "[NEEDS VERIFICATION]" : e.status === "failed" ? "[FAILED]" : "[NOT COMPLETED]";
1970
+ return `- **${e.id}** ${status}: ${e.description}`;
1749
1971
  }).join("\n");
1750
1972
  }
1751
- function formatModifiedFiles(session) {
1752
- const files = session.executionState?.modifiedFiles ?? [];
1753
- return files.length > 0 ? files.map((f) => `- ${f}`).join("\n") : "(none)";
1973
+ async function formatModifiedFiles(session) {
1974
+ return formatGitDiffFiles(session.workdir);
1754
1975
  }
1755
1976
  function resolveTemplate(template, ctx) {
1756
1977
  let result = template;
@@ -1764,28 +1985,30 @@ function resolveTemplate(template, ctx) {
1764
1985
  result = result.replace(/\{\{previousStepOutput\}\}/g, ctx.stepOutput["stdout"] ?? "");
1765
1986
  return result;
1766
1987
  }
1767
- function evaluateCondition(condition, criteria, stepOutcome) {
1988
+ function evaluateCondition(condition, stepOutcome, metadataEntries) {
1768
1989
  switch (condition.type) {
1769
- case "all_criteria_passed":
1770
- return criteria.length === 0 || criteria.every((c) => c.status.type === "passed");
1771
- case "all_criteria_completed_or_passed":
1772
- return criteria.every((c) => c.status.type === "completed" || c.status.type === "passed");
1773
- case "any_criteria_blocked":
1774
- return criteria.some(
1775
- (c) => c.status.type === "failed" && c.attempts.filter((a) => a.status === "failed").length >= RUNNER_CONFIG.maxVerifyRetries
1776
- );
1777
- case "has_pending_criteria":
1778
- return criteria.some((c) => c.status.type !== "passed");
1779
1990
  case "step_result":
1780
1991
  if (!stepOutcome) return false;
1781
1992
  return stepOutcome.result === condition.result;
1993
+ case "metadata_all_match": {
1994
+ if (!metadataEntries) return false;
1995
+ const entries = metadataEntries[condition.key];
1996
+ if (!entries || entries.length === 0) return true;
1997
+ return entries.every((e) => e[condition.field] === condition.value);
1998
+ }
1999
+ case "metadata_all_in": {
2000
+ if (!metadataEntries) return false;
2001
+ const entries = metadataEntries[condition.key];
2002
+ if (!entries || entries.length === 0) return true;
2003
+ return entries.every((e) => condition.values.includes(e[condition.field]));
2004
+ }
1782
2005
  case "always":
1783
2006
  return true;
1784
2007
  }
1785
2008
  }
1786
- function evaluateTransitions(transitions, criteria, stepOutcome) {
2009
+ function evaluateTransitions(transitions, stepOutcome, metadataEntries) {
1787
2010
  for (const transition of transitions) {
1788
- if (evaluateCondition(transition.when, criteria, stepOutcome)) {
2011
+ if (evaluateCondition(transition.when, stepOutcome, metadataEntries)) {
1789
2012
  return transition.goto;
1790
2013
  }
1791
2014
  }
@@ -1822,8 +2045,9 @@ function getCurrentWindowMessageOptions(sessionId) {
1822
2045
  const contextWindowId = getCurrentContextWindowId(sessionId);
1823
2046
  return contextWindowId ? { contextWindowId } : void 0;
1824
2047
  }
1825
- function buildReason(criteria) {
1826
- const remaining = criteria.filter((c) => c.status.type !== "passed");
2048
+ function buildReason(metadataEntries) {
2049
+ const entries = metadataEntries?.["criteria"] ?? [];
2050
+ const remaining = entries.filter((e) => e.status !== "passed");
1827
2051
  return `${remaining.length} criteria remaining`;
1828
2052
  }
1829
2053
  async function executeWorkflow(workflow, options) {
@@ -1833,7 +2057,7 @@ async function executeWorkflow(workflow, options) {
1833
2057
  let iterations = 0;
1834
2058
  let currentStepId = workflow.entryStep;
1835
2059
  let lastStepOutput = {};
1836
- let isFirstBuilderEntry = true;
2060
+ const firstEntryForStep = /* @__PURE__ */ new Set();
1837
2061
  const messagesBeforeWorkflow = sessionManager.requireSession(sessionId).messages.length;
1838
2062
  const stepsById = /* @__PURE__ */ new Map();
1839
2063
  for (const step of workflow.steps) {
@@ -1842,8 +2066,11 @@ async function executeWorkflow(workflow, options) {
1842
2066
  logger.debug("Workflow executor starting", { sessionId, workflow: workflow.metadata.id });
1843
2067
  if (workflow.startCondition && workflow.startCondition.type !== "always") {
1844
2068
  const session = sessionManager.requireSession(sessionId);
1845
- const criteria = session.criteria;
1846
- const conditionMet = evaluateCondition(workflow.startCondition, criteria, null);
2069
+ const conditionMet = evaluateCondition(
2070
+ workflow.startCondition,
2071
+ null,
2072
+ session.metadataEntries
2073
+ );
1847
2074
  if (!conditionMet) {
1848
2075
  logger.debug("Workflow start condition not met", { sessionId, condition: workflow.startCondition.type });
1849
2076
  return {
@@ -1901,16 +2128,16 @@ async function executeWorkflow(workflow, options) {
1901
2128
  }
1902
2129
  const session = sessionManager.requireSession(sessionId);
1903
2130
  const currentWindowMessageOptions = getCurrentWindowMessageOptions(sessionId);
1904
- const criteria = session.criteria;
2131
+ const criteriaEntries = session.metadataEntries["criteria"] ?? [];
1905
2132
  const templateCtx = {
1906
2133
  workdir: session.workdir,
1907
- reason: buildReason(criteria),
2134
+ reason: buildReason(session.metadataEntries),
1908
2135
  verifierFindings: lastStepOutput["content"] ?? "",
1909
2136
  previousStepOutput: lastStepOutput["stdout"] ?? "",
1910
- criteriaCount: criteria.length,
1911
- pendingCount: criteria.filter((c) => c.status.type !== "passed").length,
1912
- criteriaList: formatCriteriaList(criteria),
1913
- modifiedFiles: formatModifiedFiles(session),
2137
+ criteriaCount: criteriaEntries.length,
2138
+ pendingCount: criteriaEntries.filter((e) => e.status !== "passed").length,
2139
+ criteriaList: formatCriteriaList(criteriaEntries),
2140
+ modifiedFiles: await formatModifiedFiles(session),
1914
2141
  stepOutput: lastStepOutput
1915
2142
  };
1916
2143
  sessionManager.setPhase(sessionId, step.phase);
@@ -1923,7 +2150,7 @@ async function executeWorkflow(workflow, options) {
1923
2150
  const STEP_DONE_NUDGE = "You haven't called step_done(). If you haven't finished the task, continue and when you're finished call step_done()";
1924
2151
  let promptContent;
1925
2152
  let nudgeContent;
1926
- if (isFirstBuilderEntry && agentStep.prompt) {
2153
+ if (!firstEntryForStep.has(step.id) && agentStep.prompt) {
1927
2154
  const resolvedPrompt = resolveTemplate(agentStep.prompt, templateCtx);
1928
2155
  promptContent = resolvedPrompt + STEP_DONE_PROMPT;
1929
2156
  const promptMsgId = crypto.randomUUID();
@@ -1951,7 +2178,7 @@ async function executeWorkflow(workflow, options) {
1951
2178
  })
1952
2179
  );
1953
2180
  }
1954
- } else if (!isFirstBuilderEntry) {
2181
+ } else if (firstEntryForStep.has(step.id)) {
1955
2182
  const parts = [];
1956
2183
  if (agentStep.nudgePrompt) {
1957
2184
  const resolvedNudge = resolveTemplate(agentStep.nudgePrompt, templateCtx);
@@ -1969,13 +2196,13 @@ async function executeWorkflow(workflow, options) {
1969
2196
  llmClient,
1970
2197
  injectStepDone: true,
1971
2198
  ...options.statsIdentity ? { statsIdentity: options.statsIdentity } : {},
1972
- ...isFirstBuilderEntry && !agentStep.prompt && options.injectBuilderKickoff === true ? { injectBuilderKickoff: true } : {},
2199
+ ...!firstEntryForStep.has(step.id) && !agentStep.prompt && options.injectBuilderKickoff === true ? { injectBuilderKickoff: true } : {},
1973
2200
  ...signal ? { signal } : {},
1974
2201
  ...onMessage ? { onMessage } : {}
1975
2202
  },
1976
2203
  turnMetrics
1977
2204
  );
1978
- isFirstBuilderEntry = false;
2205
+ firstEntryForStep.add(step.id);
1979
2206
  const agentReturnValue = agentResult.returnValueResult ?? "completed";
1980
2207
  const stepDoneCalled = agentResult.stepDoneCalled ?? false;
1981
2208
  lastStepOutput = {
@@ -2098,7 +2325,7 @@ ${output.slice(0, 1e4)}
2098
2325
  }
2099
2326
  }
2100
2327
  const refreshedSession = sessionManager.requireSession(sessionId);
2101
- const nextStepId = evaluateTransitions(step.transitions, refreshedSession.criteria, stepOutcome);
2328
+ const nextStepId = evaluateTransitions(step.transitions, stepOutcome, refreshedSession.metadataEntries);
2102
2329
  if (nextStepId === TERMINAL_DONE) {
2103
2330
  sessionManager.setPhase(sessionId, "done");
2104
2331
  const totalTimeSeconds = Math.round((performance.now() - startTime) / 100) / 10;
@@ -2141,13 +2368,10 @@ ${output.slice(0, 1e4)}
2141
2368
  }
2142
2369
  if (nextStepId === TERMINAL_BLOCKED) {
2143
2370
  sessionManager.setPhase(sessionId, "blocked");
2144
- const blockedCriteria = refreshedSession.criteria.filter(
2145
- (c) => c.status.type === "failed" && c.attempts.filter((a) => a.status === "failed").length >= RUNNER_CONFIG.maxVerifyRetries
2146
- ).map((c) => c.id);
2147
- const reason = blockedCriteria.length > 0 ? `Retry limit reached for: ${blockedCriteria.join(", ")}` : "No matching transition";
2371
+ const reason = "No matching transition";
2148
2372
  emitWorkflowMessage(eventStore, sessionId, `Runner blocked: ${reason}`, currentWindowMessageOptions, onMessage);
2149
2373
  logger.warn("Workflow executor blocked", { sessionId, iterations, reason });
2150
- const blockedAction = { type: "BLOCKED", reason, blockedCriteria };
2374
+ const blockedAction = { type: "BLOCKED", reason, blockedCriteria: [] };
2151
2375
  return {
2152
2376
  finalAction: blockedAction,
2153
2377
  iterations,
@@ -2308,7 +2532,7 @@ async function isValidToken(token) {
2308
2532
  // src/server/ws/server.ts
2309
2533
  function moduleGitBranch(cwd) {
2310
2534
  return new Promise((resolve3) => {
2311
- const proc = spawn2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
2535
+ const proc = spawn3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
2312
2536
  cwd,
2313
2537
  stdio: ["ignore", "pipe", "ignore"]
2314
2538
  });
@@ -2331,75 +2555,70 @@ function hashContent(content) {
2331
2555
  }
2332
2556
  function moduleGitDiff(cwd) {
2333
2557
  return new Promise((resolve3) => {
2334
- const proc = spawn2("git", ["diff", "--stat", "--numstat", "--format=", "HEAD"], {
2558
+ const diffProc = spawn3("git", ["diff", "--name-status", "HEAD"], {
2335
2559
  cwd,
2336
2560
  stdio: ["ignore", "pipe", "pipe"]
2337
2561
  });
2338
- const statusProc = spawn2("git", ["status", "--porcelain", "-uall"], {
2562
+ const statusProc = spawn3("git", ["status", "--porcelain"], {
2339
2563
  cwd,
2340
2564
  stdio: ["ignore", "pipe", "pipe"]
2341
2565
  });
2342
2566
  let diffStdout = "";
2343
2567
  let statusStdout = "";
2344
- proc.stdout.on("data", (data) => {
2345
- diffStdout += data.toString();
2346
- });
2347
- statusProc.stdout.on("data", (data) => {
2348
- statusStdout += data.toString();
2349
- });
2350
- let procExited = false;
2568
+ let diffExited = false;
2351
2569
  let statusExited = false;
2352
2570
  let diffCode = null;
2353
2571
  let statusCode = null;
2354
2572
  const processResults = () => {
2355
- if (diffCode !== 0 || statusCode !== 0) {
2356
- resolve3({ hash: "", files: [] });
2357
- return;
2358
- }
2359
- const hash = hashContent(diffStdout + statusStdout);
2573
+ if (!diffExited || !statusExited) return;
2574
+ const raw = diffStdout + statusStdout;
2575
+ const hash = raw ? hashContent(raw) : "";
2360
2576
  const files = [];
2361
- for (const line of diffStdout.split("\n")) {
2362
- if (!line) continue;
2363
- const parts = line.split(" ");
2364
- if (parts.length < 3) continue;
2365
- const statusChar = parts[1]?.[0];
2366
- const status = statusChar === "A" ? "added" : statusChar === "d" ? "deleted" : "modified";
2367
- files.push({
2368
- path: parts[2] ?? parts[1] ?? parts[0] ?? "",
2369
- status,
2370
- additions: parseInt(parts[0] ?? "") || 0,
2371
- deletions: parseInt(parts[1] ?? "") || 0
2372
- });
2577
+ if (diffCode === 0) {
2578
+ for (const line of diffStdout.split("\n")) {
2579
+ if (!line.trim()) continue;
2580
+ const [statusChar, ...pathParts] = line.split(" ");
2581
+ const path = pathParts.join(" ") || statusChar || "";
2582
+ if (!path) continue;
2583
+ const status = statusChar === "A" ? "added" : statusChar === "D" ? "deleted" : "modified";
2584
+ files.push({ path, status, additions: 0, deletions: 0 });
2585
+ }
2373
2586
  }
2374
- for (const line of statusStdout.split("\n")) {
2375
- if (!line) continue;
2376
- if (!line.startsWith("?? ")) continue;
2377
- const path = line.slice(3);
2378
- if (!path) continue;
2379
- files.push({
2380
- path,
2381
- status: "added",
2382
- additions: 0,
2383
- deletions: 0
2384
- });
2587
+ if (statusCode === 0) {
2588
+ for (const line of statusStdout.split("\n")) {
2589
+ if (!line.startsWith("?? ")) continue;
2590
+ const path = line.slice(3).trim();
2591
+ if (!path) continue;
2592
+ files.push({ path, status: "added", additions: 0, deletions: 0 });
2593
+ }
2385
2594
  }
2386
2595
  resolve3({ hash, files });
2387
2596
  };
2388
- proc.on("close", (code) => {
2389
- procExited = true;
2597
+ diffProc.stdout.on("data", (data) => {
2598
+ diffStdout += data.toString();
2599
+ });
2600
+ statusProc.stdout.on("data", (data) => {
2601
+ statusStdout += data.toString();
2602
+ });
2603
+ diffProc.on("close", (code) => {
2604
+ diffExited = true;
2390
2605
  diffCode = code;
2391
- if (statusExited) processResults();
2606
+ processResults();
2392
2607
  });
2393
2608
  statusProc.on("close", (code) => {
2394
2609
  statusExited = true;
2395
2610
  statusCode = code;
2396
- if (procExited) processResults();
2611
+ processResults();
2612
+ });
2613
+ diffProc.on("error", () => {
2614
+ diffExited = true;
2615
+ diffCode = 1;
2616
+ processResults();
2397
2617
  });
2398
- proc.on("error", () => resolve3({ hash: "", files: [] }));
2399
2618
  statusProc.on("error", () => {
2400
- if (!procExited) {
2401
- resolve3({ hash: "", files: [] });
2402
- }
2619
+ statusExited = true;
2620
+ statusCode = 1;
2621
+ processResults();
2403
2622
  });
2404
2623
  });
2405
2624
  }
@@ -3089,7 +3308,6 @@ ${content}` : workflowInfo;
3089
3308
  if (session.phase === "blocked") {
3090
3309
  logger.info("User launched runner - resetting blocked state", { sessionId });
3091
3310
  sessionManager.setPhase(sessionId, "build");
3092
- sessionManager.resetAllCriteriaAttempts(sessionId);
3093
3311
  }
3094
3312
  const launchPayload = message.payload;
3095
3313
  const launchAttachments = launchPayload?.attachments;
@@ -3180,7 +3398,7 @@ ${content}` : workflowInfo;
3180
3398
  }
3181
3399
 
3182
3400
  // src/server/lsp/server.ts
3183
- import { spawn as spawn3 } from "child_process";
3401
+ import { spawn as spawn4 } from "child_process";
3184
3402
  import { extname } from "path";
3185
3403
  import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node.js";
3186
3404
  var LSP = {
@@ -3242,7 +3460,7 @@ var LspServer = class {
3242
3460
  }
3243
3461
  this.state = "starting";
3244
3462
  try {
3245
- this.process = spawn3(this.commandPath, this.config.serverArgs, {
3463
+ this.process = spawn4(this.commandPath, this.config.serverArgs, {
3246
3464
  cwd: this.workdir,
3247
3465
  stdio: ["pipe", "pipe", "pipe"],
3248
3466
  env: {
@@ -4673,6 +4891,14 @@ ${summary}`, {
4673
4891
  return updatedCriteria;
4674
4892
  }
4675
4893
  // ============================================================================
4894
+ // Metadata Operations
4895
+ // ============================================================================
4896
+ setMetadataEntries(sessionId, key, entries) {
4897
+ this.requireSession(sessionId);
4898
+ emitMetadataSet(sessionId, key, entries);
4899
+ this.emit({ type: "metadata_updated", sessionId, key, entries });
4900
+ }
4901
+ // ============================================================================
4676
4902
  // Message Queue (runtime state, transient while agent is running)
4677
4903
  // ============================================================================
4678
4904
  messageQueues = /* @__PURE__ */ new Map();
@@ -4961,6 +5187,7 @@ ${summary}`,
4961
5187
  isRunning,
4962
5188
  messages,
4963
5189
  criteria: eventState.criteria,
5190
+ metadataEntries: eventState.metadataEntries,
4964
5191
  contextWindows: [],
4965
5192
  // Derived from events, not stored separately
4966
5193
  executionState: eventState.lastModeWithReminder || eventState.cachedSystemPrompt || hasCachedPrompt ? {
@@ -5505,25 +5732,29 @@ function createDirectoryRoutes() {
5505
5732
 
5506
5733
  // src/server/routes/auto-update.ts
5507
5734
  import { Router as Router6 } from "express";
5508
- import { spawn as spawn4 } from "child_process";
5735
+ import { spawn as spawn5 } from "child_process";
5509
5736
 
5510
5737
  // src/constants.ts
5511
- var VERSION = "1.6.103";
5738
+ var VERSION = "2.0.0-beta.1";
5512
5739
 
5513
5740
  // src/server/routes/auto-update.ts
5514
5741
  var updateInProgress = false;
5742
+ function isRunningAsService() {
5743
+ return process.env["OPENFOX_SERVICE"] === "true";
5744
+ }
5515
5745
  function createAutoUpdateRoutes(options = {}) {
5516
5746
  const router = Router6();
5517
5747
  router.get("/check", async (req, res) => {
5518
5748
  const isTest = req.query["test"] === "1";
5749
+ const isService = isRunningAsService();
5519
5750
  const current = VERSION;
5520
5751
  if (isTest) {
5521
- res.json({ current: "1.0.0", latest: "1.1.0", isUpdateAvailable: true });
5752
+ res.json({ current: "1.0.0", latest: "1.1.0", isUpdateAvailable: true, isService });
5522
5753
  return;
5523
5754
  }
5524
5755
  try {
5525
5756
  const latest = await new Promise((resolve3, reject) => {
5526
- const child = spawn4("npm", ["view", "openfox", "version"], {
5757
+ const child = spawn5("npm", ["view", "openfox", "version"], {
5527
5758
  stdio: ["ignore", "pipe", "pipe"]
5528
5759
  });
5529
5760
  let stdout = "";
@@ -5544,9 +5775,9 @@ function createAutoUpdateRoutes(options = {}) {
5544
5775
  }, 1e4);
5545
5776
  });
5546
5777
  const isUpdateAvailable = current !== latest;
5547
- res.json({ current, latest, isUpdateAvailable });
5778
+ res.json({ current, latest, isUpdateAvailable, isService });
5548
5779
  } catch {
5549
- res.json({ current, latest: current, isUpdateAvailable: false });
5780
+ res.json({ current, latest: current, isUpdateAvailable: false, isService });
5550
5781
  }
5551
5782
  });
5552
5783
  router.post("/", async (req, res) => {
@@ -5563,7 +5794,9 @@ function createAutoUpdateRoutes(options = {}) {
5563
5794
  }
5564
5795
  try {
5565
5796
  let stderr = "";
5566
- const child = spawn4("bash", ["-c", "openfox update"], {
5797
+ const isService = isRunningAsService();
5798
+ const updateCmd = isService ? "openfox update --service" : "openfox update";
5799
+ const child = spawn5("bash", ["-c", updateCmd], {
5567
5800
  detached: true,
5568
5801
  stdio: ["ignore", "ignore", "pipe"]
5569
5802
  });
@@ -5583,7 +5816,7 @@ function createAutoUpdateRoutes(options = {}) {
5583
5816
  }
5584
5817
  updateInProgress = false;
5585
5818
  }, 3e4);
5586
- res.json({ success: true });
5819
+ res.json({ success: true, isService });
5587
5820
  } catch (err) {
5588
5821
  updateInProgress = false;
5589
5822
  res.status(500).json({ error: err instanceof Error ? err.message : "Update failed to start" });
@@ -5805,7 +6038,7 @@ async function createServerHandle(config4) {
5805
6038
  res.json({ project });
5806
6039
  });
5807
6040
  app.get("/api/sessions", async (req, res) => {
5808
- const { getRecentUserPromptsForSession } = await import("./events-OKTZRT3Y.js");
6041
+ const { getRecentUserPromptsForSession } = await import("./events-OHJWCI3S.js");
5809
6042
  const projectId = req.query["projectId"];
5810
6043
  const limit = Math.min(parseInt(req.query["limit"]) || 20, 100);
5811
6044
  const offset = parseInt(req.query["offset"]) || 0;
@@ -5838,8 +6071,8 @@ async function createServerHandle(config4) {
5838
6071
  res.status(201).json({ session });
5839
6072
  });
5840
6073
  app.get("/api/sessions/:id", async (req, res) => {
5841
- const { getEventStore: getEventStore2 } = await import("./events-OKTZRT3Y.js");
5842
- const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2 } = await import("./folding-Z3OMWNOE.js");
6074
+ const { getEventStore: getEventStore2 } = await import("./events-OHJWCI3S.js");
6075
+ const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2 } = await import("./folding-U6UHQAS5.js");
5843
6076
  const session = sessionManager.getSession(req.params.id);
5844
6077
  if (!session) {
5845
6078
  return res.status(404).json({ error: "Session not found" });
@@ -5869,8 +6102,8 @@ async function createServerHandle(config4) {
5869
6102
  res.json({ success: true });
5870
6103
  });
5871
6104
  app.post("/api/sessions/:id/provider", async (req, res) => {
5872
- const { getEventStore: getEventStore2 } = await import("./events-OKTZRT3Y.js");
5873
- const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2 } = await import("./folding-Z3OMWNOE.js");
6105
+ const { getEventStore: getEventStore2 } = await import("./events-OHJWCI3S.js");
6106
+ const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2 } = await import("./folding-U6UHQAS5.js");
5874
6107
  const sessionId = req.params.id;
5875
6108
  const session = sessionManager.getSession(sessionId);
5876
6109
  if (!session) {
@@ -5903,12 +6136,17 @@ async function createServerHandle(config4) {
5903
6136
  if (!Array.isArray(criteria)) {
5904
6137
  return res.status(400).json({ error: "criteria is required and must be an array" });
5905
6138
  }
5906
- sessionManager.setCriteria(sessionId, criteria);
6139
+ const entries = criteria.map((c, i) => ({
6140
+ id: c.id ?? String(i),
6141
+ description: c.description,
6142
+ status: c.status ?? "pending"
6143
+ }));
6144
+ sessionManager.setMetadataEntries(sessionId, "criteria", entries);
5907
6145
  res.json({ success: true });
5908
6146
  });
5909
6147
  app.put("/api/sessions/:id/mode", async (req, res) => {
5910
- const { getEventStore: getEventStore2 } = await import("./events-OKTZRT3Y.js");
5911
- const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2 } = await import("./folding-Z3OMWNOE.js");
6148
+ const { getEventStore: getEventStore2 } = await import("./events-OHJWCI3S.js");
6149
+ const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2 } = await import("./folding-U6UHQAS5.js");
5912
6150
  const sessionId = req.params.id;
5913
6151
  const session = sessionManager.getSession(sessionId);
5914
6152
  if (!session) {
@@ -5964,14 +6202,14 @@ async function createServerHandle(config4) {
5964
6202
  if (!callId || approved === void 0) {
5965
6203
  return res.status(400).json({ error: "callId and approved are required" });
5966
6204
  }
5967
- const { providePathConfirmation } = await import("./tools-WFIYKFBE.js");
6205
+ const { providePathConfirmation } = await import("./tools-Z7X4HXZN.js");
5968
6206
  const result = providePathConfirmation(callId, approved, alwaysAllow);
5969
6207
  if (!result.found) {
5970
6208
  return res.status(404).json({ error: "No pending path confirmation with that ID" });
5971
6209
  }
5972
- const { getEventStore: getEventStore2 } = await import("./events-OKTZRT3Y.js");
5973
- const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2, foldPendingConfirmations: foldPendingConfirmations2 } = await import("./folding-Z3OMWNOE.js");
5974
- const { createSessionStateMessage: createSessionStateMessage2 } = await import("./protocol-TNC6MCJV.js");
6210
+ const { getEventStore: getEventStore2 } = await import("./events-OHJWCI3S.js");
6211
+ const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2, foldPendingConfirmations: foldPendingConfirmations2 } = await import("./folding-U6UHQAS5.js");
6212
+ const { createSessionStateMessage: createSessionStateMessage2 } = await import("./protocol-K62EPEGS.js");
5975
6213
  const eventStore = getEventStore2();
5976
6214
  const events = eventStore.getEvents(sessionId);
5977
6215
  const messages = buildMessagesFromStoredEvents2(events);
@@ -5988,7 +6226,7 @@ async function createServerHandle(config4) {
5988
6226
  if (!callId || !answer) {
5989
6227
  return res.status(400).json({ error: "callId and answer are required" });
5990
6228
  }
5991
- const { provideAnswer: provideAnswer2 } = await import("./tools-WFIYKFBE.js");
6229
+ const { provideAnswer: provideAnswer2 } = await import("./tools-Z7X4HXZN.js");
5992
6230
  const found = provideAnswer2(callId, answer);
5993
6231
  if (!found) {
5994
6232
  return res.status(404).json({ error: "No pending question with that ID" });
@@ -6024,14 +6262,14 @@ async function createServerHandle(config4) {
6024
6262
  if (!session) {
6025
6263
  return res.status(404).json({ error: "Session not found" });
6026
6264
  }
6027
- const { stopSessionExecution } = await import("./chat-handler-ZJVIM7U4.js");
6028
- const { cancelQuestionsForSession, cancelPathConfirmationsForSession } = await import("./tools-WFIYKFBE.js");
6265
+ const { stopSessionExecution } = await import("./chat-handler-IVIWOR3D.js");
6266
+ const { cancelQuestionsForSession, cancelPathConfirmationsForSession } = await import("./tools-Z7X4HXZN.js");
6029
6267
  stopSessionExecution(sessionId, sessionManager);
6030
6268
  abortSession(sessionId);
6031
6269
  cancelQuestionsForSession(sessionId, "Session stopped by user");
6032
6270
  cancelPathConfirmationsForSession(sessionId, "Session stopped by user");
6033
6271
  sessionManager.clearMessageQueue(sessionId);
6034
- const eventStore = (await import("./events-OKTZRT3Y.js")).getEventStore();
6272
+ const eventStore = (await import("./events-OHJWCI3S.js")).getEventStore();
6035
6273
  eventStore.append(sessionId, { type: "running.changed", data: { isRunning: false } });
6036
6274
  res.json({ success: true });
6037
6275
  });
@@ -6045,7 +6283,7 @@ async function createServerHandle(config4) {
6045
6283
  if (typeof messageIndex !== "number" || messageIndex < 0) {
6046
6284
  return res.status(400).json({ error: "messageIndex must be a non-negative number" });
6047
6285
  }
6048
- const { truncateSessionMessages } = await import("./events-OKTZRT3Y.js");
6286
+ const { truncateSessionMessages } = await import("./events-OHJWCI3S.js");
6049
6287
  truncateSessionMessages(sessionId, messageIndex);
6050
6288
  res.json({ success: true });
6051
6289
  });
@@ -6063,7 +6301,7 @@ async function createServerHandle(config4) {
6063
6301
  if (!msg) {
6064
6302
  return res.status(400).json({ error: "Message not found at this index" });
6065
6303
  }
6066
- const { truncateSessionMessages } = await import("./events-OKTZRT3Y.js");
6304
+ const { truncateSessionMessages } = await import("./events-OHJWCI3S.js");
6067
6305
  truncateSessionMessages(sessionId, messageIndex - 1);
6068
6306
  sessionManager.queueMessage(sessionId, "asap", msg.content, msg.attachments, msg.messageKind);
6069
6307
  res.json({ success: true });
@@ -6540,7 +6778,7 @@ async function createServerHandle(config4) {
6540
6778
  providerManager
6541
6779
  );
6542
6780
  const wss = wssExports.wss;
6543
- const { QueueProcessor } = await import("./processor-XJYCHNV6.js");
6781
+ const { QueueProcessor } = await import("./processor-TKBJLQ2N.js");
6544
6782
  const queueProcessor = new QueueProcessor({
6545
6783
  sessionManager,
6546
6784
  providerManager,
@@ -6612,4 +6850,4 @@ export {
6612
6850
  createServerHandle,
6613
6851
  createServer
6614
6852
  };
6615
- //# sourceMappingURL=chunk-LGN6IXGS.js.map
6853
+ //# sourceMappingURL=chunk-ZLUBZ4K4.js.map