@rpamis/comet 0.4.0-beta.7 → 0.4.0-beta.9

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 (161) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3 -3
  3. package/assets/manifest.json +1 -1
  4. package/assets/skills/comet/reference/comet-yaml-fields.md +2 -2
  5. package/assets/skills/comet/scripts/comet-entry-runtime.mjs +118 -6
  6. package/assets/skills/comet/scripts/comet-hook-router.mjs +244 -26
  7. package/assets/skills/comet/scripts/comet-runtime.mjs +221 -104
  8. package/assets/skills/comet-archive/SKILL.md +29 -20
  9. package/assets/skills/comet-native/SKILL.md +23 -15
  10. package/assets/skills/comet-native/reference/artifacts.md +19 -4
  11. package/assets/skills/comet-native/reference/commands.md +8 -2
  12. package/assets/skills/comet-native/reference/recovery.md +1 -1
  13. package/assets/skills/comet-native/scripts/comet-native-runtime.mjs +1136 -471
  14. package/assets/skills-zh/comet/reference/comet-yaml-fields.md +2 -2
  15. package/assets/skills-zh/comet-archive/SKILL.md +29 -20
  16. package/assets/skills-zh/comet-native/SKILL.md +23 -15
  17. package/assets/skills-zh/comet-native/reference/artifacts.md +20 -5
  18. package/assets/skills-zh/comet-native/reference/commands.md +8 -2
  19. package/assets/skills-zh/comet-native/reference/recovery.md +1 -1
  20. package/dist/app/commands/init.d.ts.map +1 -1
  21. package/dist/app/commands/init.js +3 -6
  22. package/dist/app/commands/init.js.map +1 -1
  23. package/dist/domains/bundle/bundle-platform.d.ts.map +1 -1
  24. package/dist/domains/bundle/bundle-platform.js +1 -1
  25. package/dist/domains/bundle/bundle-platform.js.map +1 -1
  26. package/dist/domains/comet-classic/classic-guard.d.ts.map +1 -1
  27. package/dist/domains/comet-classic/classic-guard.js +3 -2
  28. package/dist/domains/comet-classic/classic-guard.js.map +1 -1
  29. package/dist/domains/comet-entry/current-selection.d.ts.map +1 -1
  30. package/dist/domains/comet-entry/current-selection.js +6 -8
  31. package/dist/domains/comet-entry/current-selection.js.map +1 -1
  32. package/dist/domains/comet-entry/project-status.d.ts.map +1 -1
  33. package/dist/domains/comet-entry/project-status.js +5 -1
  34. package/dist/domains/comet-entry/project-status.js.map +1 -1
  35. package/dist/domains/comet-native/native-archive-transaction.d.ts.map +1 -1
  36. package/dist/domains/comet-native/native-archive-transaction.js +5 -6
  37. package/dist/domains/comet-native/native-archive-transaction.js.map +1 -1
  38. package/dist/domains/comet-native/native-atomic-file.d.ts.map +1 -1
  39. package/dist/domains/comet-native/native-atomic-file.js +11 -7
  40. package/dist/domains/comet-native/native-atomic-file.js.map +1 -1
  41. package/dist/domains/comet-native/native-bounded-file.d.ts.map +1 -1
  42. package/dist/domains/comet-native/native-bounded-file.js +11 -7
  43. package/dist/domains/comet-native/native-bounded-file.js.map +1 -1
  44. package/dist/domains/comet-native/native-build-evidence.js +2 -2
  45. package/dist/domains/comet-native/native-build-evidence.js.map +1 -1
  46. package/dist/domains/comet-native/native-change.d.ts +4 -2
  47. package/dist/domains/comet-native/native-change.d.ts.map +1 -1
  48. package/dist/domains/comet-native/native-change.js +18 -4
  49. package/dist/domains/comet-native/native-change.js.map +1 -1
  50. package/dist/domains/comet-native/native-check-receipt-storage.d.ts.map +1 -1
  51. package/dist/domains/comet-native/native-check-receipt-storage.js +11 -7
  52. package/dist/domains/comet-native/native-check-receipt-storage.js.map +1 -1
  53. package/dist/domains/comet-native/native-check-receipt.d.ts.map +1 -1
  54. package/dist/domains/comet-native/native-check-receipt.js +16 -11
  55. package/dist/domains/comet-native/native-check-receipt.js.map +1 -1
  56. package/dist/domains/comet-native/native-cli.d.ts.map +1 -1
  57. package/dist/domains/comet-native/native-cli.js +120 -16
  58. package/dist/domains/comet-native/native-cli.js.map +1 -1
  59. package/dist/domains/comet-native/native-config.d.ts +5 -1
  60. package/dist/domains/comet-native/native-config.d.ts.map +1 -1
  61. package/dist/domains/comet-native/native-config.js +78 -0
  62. package/dist/domains/comet-native/native-config.js.map +1 -1
  63. package/dist/domains/comet-native/native-continuation.d.ts +2 -1
  64. package/dist/domains/comet-native/native-continuation.d.ts.map +1 -1
  65. package/dist/domains/comet-native/native-continuation.js +13 -5
  66. package/dist/domains/comet-native/native-continuation.js.map +1 -1
  67. package/dist/domains/comet-native/native-diagnostics.d.ts +7 -3
  68. package/dist/domains/comet-native/native-diagnostics.d.ts.map +1 -1
  69. package/dist/domains/comet-native/native-diagnostics.js +19 -9
  70. package/dist/domains/comet-native/native-diagnostics.js.map +1 -1
  71. package/dist/domains/comet-native/native-evidence-retention.d.ts.map +1 -1
  72. package/dist/domains/comet-native/native-evidence-retention.js +5 -3
  73. package/dist/domains/comet-native/native-evidence-retention.js.map +1 -1
  74. package/dist/domains/comet-native/native-evidence-storage.d.ts.map +1 -1
  75. package/dist/domains/comet-native/native-evidence-storage.js +11 -7
  76. package/dist/domains/comet-native/native-evidence-storage.js.map +1 -1
  77. package/dist/domains/comet-native/native-file-identity.d.ts +3 -0
  78. package/dist/domains/comet-native/native-file-identity.d.ts.map +1 -0
  79. package/dist/domains/comet-native/native-file-identity.js +2 -0
  80. package/dist/domains/comet-native/native-file-identity.js.map +1 -0
  81. package/dist/domains/comet-native/native-findings.d.ts.map +1 -1
  82. package/dist/domains/comet-native/native-findings.js +19 -1
  83. package/dist/domains/comet-native/native-findings.js.map +1 -1
  84. package/dist/domains/comet-native/native-guards.d.ts +2 -1
  85. package/dist/domains/comet-native/native-guards.d.ts.map +1 -1
  86. package/dist/domains/comet-native/native-guards.js +14 -0
  87. package/dist/domains/comet-native/native-guards.js.map +1 -1
  88. package/dist/domains/comet-native/native-lock.d.ts.map +1 -1
  89. package/dist/domains/comet-native/native-lock.js +30 -35
  90. package/dist/domains/comet-native/native-lock.js.map +1 -1
  91. package/dist/domains/comet-native/native-protected-file.d.ts.map +1 -1
  92. package/dist/domains/comet-native/native-protected-file.js +9 -8
  93. package/dist/domains/comet-native/native-protected-file.js.map +1 -1
  94. package/dist/domains/comet-native/native-root-move.d.ts.map +1 -1
  95. package/dist/domains/comet-native/native-root-move.js +2 -0
  96. package/dist/domains/comet-native/native-root-move.js.map +1 -1
  97. package/dist/domains/comet-native/native-run-store.d.ts.map +1 -1
  98. package/dist/domains/comet-native/native-run-store.js +9 -8
  99. package/dist/domains/comet-native/native-run-store.js.map +1 -1
  100. package/dist/domains/comet-native/native-snapshot.d.ts +5 -1
  101. package/dist/domains/comet-native/native-snapshot.d.ts.map +1 -1
  102. package/dist/domains/comet-native/native-snapshot.js +225 -5
  103. package/dist/domains/comet-native/native-snapshot.js.map +1 -1
  104. package/dist/domains/comet-native/native-transitions.d.ts +2 -1
  105. package/dist/domains/comet-native/native-transitions.d.ts.map +1 -1
  106. package/dist/domains/comet-native/native-transitions.js +34 -11
  107. package/dist/domains/comet-native/native-transitions.js.map +1 -1
  108. package/dist/domains/comet-native/native-types.d.ts +16 -0
  109. package/dist/domains/comet-native/native-types.d.ts.map +1 -1
  110. package/dist/domains/comet-native/native-verification-runtime.d.ts.map +1 -1
  111. package/dist/domains/comet-native/native-verification-runtime.js +5 -3
  112. package/dist/domains/comet-native/native-verification-runtime.js.map +1 -1
  113. package/dist/domains/comet-native/native-verification-scope.d.ts +2 -1
  114. package/dist/domains/comet-native/native-verification-scope.d.ts.map +1 -1
  115. package/dist/domains/comet-native/native-verification-scope.js +6 -1
  116. package/dist/domains/comet-native/native-verification-scope.js.map +1 -1
  117. package/dist/domains/dashboard/native-collector.d.ts.map +1 -1
  118. package/dist/domains/dashboard/native-collector.js +4 -1
  119. package/dist/domains/dashboard/native-collector.js.map +1 -1
  120. package/dist/domains/dashboard/web/index.html +14 -14
  121. package/dist/domains/skill/platform-install.d.ts.map +1 -1
  122. package/dist/domains/skill/platform-install.js +9 -0
  123. package/dist/domains/skill/platform-install.js.map +1 -1
  124. package/dist/domains/skill/project-instructions.d.ts.map +1 -1
  125. package/dist/domains/skill/project-instructions.js +4 -2
  126. package/dist/domains/skill/project-instructions.js.map +1 -1
  127. package/dist/domains/workflow-contract/project-config.d.ts +1 -1
  128. package/dist/domains/workflow-contract/project-config.d.ts.map +1 -1
  129. package/dist/domains/workflow-contract/project-config.js +24 -2
  130. package/dist/domains/workflow-contract/project-config.js.map +1 -1
  131. package/dist/platform/fs/file-identity.d.ts +9 -0
  132. package/dist/platform/fs/file-identity.d.ts.map +1 -0
  133. package/dist/platform/fs/file-identity.js +21 -0
  134. package/dist/platform/fs/file-identity.js.map +1 -0
  135. package/dist/platform/fs/file-system.d.ts.map +1 -1
  136. package/dist/platform/fs/file-system.js +13 -9
  137. package/dist/platform/fs/file-system.js.map +1 -1
  138. package/dist/platform/fs/race-safe-read.d.ts +47 -0
  139. package/dist/platform/fs/race-safe-read.d.ts.map +1 -0
  140. package/dist/platform/fs/race-safe-read.js +132 -0
  141. package/dist/platform/fs/race-safe-read.js.map +1 -0
  142. package/eval/README.md +1 -1
  143. package/eval/local/tasks/comet-native-clarification-depth/environment/Dockerfile +27 -0
  144. package/eval/local/tasks/comet-native-clarification-depth/environment/test_wordcount.py +25 -0
  145. package/eval/local/tasks/comet-native-clarification-depth/environment/wordcount.py +36 -0
  146. package/eval/local/tasks/comet-native-clarification-depth/instruction.md +17 -0
  147. package/eval/local/tasks/comet-native-clarification-depth/task.toml +47 -0
  148. package/eval/local/tasks/comet-native-clarification-depth/validation/test_native_clarification_depth.py +618 -0
  149. package/eval/local/tasks/comet-native-clarification-modes/validation/test_native_clarification_modes.py +1 -1
  150. package/eval/local/tasks/index.yaml +6 -0
  151. package/eval/local/tests/conftest.py +146 -2
  152. package/eval/local/tests/scaffold/test_conftest_helpers.py +188 -1
  153. package/eval/local/tests/scaffold/test_tasks.py +440 -8
  154. package/eval/local/tests/scaffold/test_utils.py +114 -1
  155. package/eval/local/tests/tasks/test_tasks.py +2 -0
  156. package/eval/pyproject.toml +4 -4
  157. package/eval/scaffold/python/aligned_comparison.py +1 -0
  158. package/eval/scaffold/python/tasks.py +2 -0
  159. package/eval/scaffold/shell/docker.sh +1 -1
  160. package/eval/scaffold/shell/run-claude-loop.sh +25 -2
  161. package/package.json +8 -1
@@ -67,6 +67,142 @@ def _extract_loop_interaction(stderr: str | None) -> dict[str, int | None]:
67
67
  }
68
68
 
69
69
 
70
+ def _bounded_command_evidence(command: str, matches: list[re.Match[str]]) -> str:
71
+ chunks: list[str] = []
72
+ remaining = 4000
73
+ last_end = -1
74
+ for match in matches:
75
+ start = max(0, match.start() - 300)
76
+ end = min(len(command), match.end() + 300)
77
+ if start <= last_end and chunks:
78
+ overlap = max(0, last_end - start)
79
+ addition = command[start + overlap : end]
80
+ addition = addition[:remaining]
81
+ chunks[-1] += addition
82
+ remaining -= len(addition)
83
+ last_end = max(last_end, end)
84
+ else:
85
+ chunk = command[start:end][:remaining]
86
+ if chunks:
87
+ separator = "\n...[truncated]...\n"
88
+ if len(separator) > remaining:
89
+ break
90
+ chunks.append(separator)
91
+ remaining -= len(separator)
92
+ chunks.append(chunk)
93
+ remaining -= len(chunk)
94
+ last_end = end
95
+ if remaining <= 0:
96
+ break
97
+ excerpt = "".join(chunks)
98
+ return re.sub(
99
+ r"(?i)\b(api[_-]?key|auth[_-]?token|password|secret)\s*=\s*"
100
+ r"(?:\"[^\"]*\"|'[^']*'|[^\s;]+)",
101
+ r"\1=[REDACTED]",
102
+ excerpt,
103
+ )
104
+
105
+
106
+ def _tool_result_succeeded(item: dict[str, Any]) -> bool:
107
+ is_error = item.get("is_error")
108
+ if is_error is True or str(is_error).strip().lower() == "true" or item.get("error"):
109
+ return False
110
+ status = str(item.get("status") or "").strip().lower()
111
+ if status in {"error", "failed", "failure", "cancelled", "canceled"}:
112
+ return False
113
+ content = item.get("content", "")
114
+ if isinstance(content, list):
115
+ content = " ".join(
116
+ str(block.get("text") or "") if isinstance(block, dict) else str(block)
117
+ for block in content
118
+ )
119
+ return not bool(
120
+ re.search(
121
+ r"(?i)^\s*(?:error|failed|failure|tool[_ -]?error)\b|"
122
+ r"\b(?:exit|exited with)(?:\s+code)?\s*[1-9]\d*\b",
123
+ str(content),
124
+ )
125
+ )
126
+
127
+
128
+ def _extract_subject_turn_evidence(stdout: str | None) -> list[dict[str, Any]]:
129
+ """Group safe assistant result text and bounded tool evidence by subject turn."""
130
+ turns: list[dict[str, Any]] = []
131
+ tool_calls: list[dict[str, Any]] = []
132
+ tool_calls_by_id: dict[str, dict[str, Any]] = {}
133
+ last_assistant_text = ""
134
+ for line in (stdout or "").splitlines():
135
+ try:
136
+ event = json.loads(line)
137
+ except (json.JSONDecodeError, TypeError):
138
+ continue
139
+ if not isinstance(event, dict):
140
+ continue
141
+ if event.get("type") == "assistant":
142
+ content = (event.get("message") or {}).get("content") or []
143
+ assistant_text = " ".join(
144
+ str(block.get("text") or "")
145
+ for block in content
146
+ if isinstance(block, dict) and block.get("type") == "text"
147
+ ).strip()
148
+ if assistant_text:
149
+ last_assistant_text = assistant_text
150
+ for block in content:
151
+ if isinstance(block, dict) and block.get("type") == "tool_use":
152
+ name = block.get("name")
153
+ if isinstance(name, str) and name:
154
+ evidence: dict[str, Any] = {"name": name, "success": False}
155
+ tool_input = block.get("input")
156
+ if isinstance(tool_input, dict):
157
+ for key in ("file_path", "path", "notebook_path"):
158
+ value = tool_input.get(key)
159
+ if isinstance(value, str) and value:
160
+ evidence["path"] = value[:500]
161
+ break
162
+ command = tool_input.get("command")
163
+ if isinstance(command, str) and command:
164
+ target_matches = list(
165
+ re.finditer(
166
+ r"(?i)(?:[a-z]:)?[./\\\w-]*\.py\b|"
167
+ r"\bbrief\.md\b|"
168
+ r"\bspec\.md\b",
169
+ command,
170
+ )
171
+ )
172
+ if target_matches:
173
+ evidence["command"] = _bounded_command_evidence(
174
+ command, target_matches
175
+ )
176
+ tool_calls.append(evidence)
177
+ tool_id = block.get("id")
178
+ if isinstance(tool_id, str) and tool_id:
179
+ tool_calls_by_id[tool_id] = evidence
180
+ continue
181
+ if event.get("type") == "user":
182
+ content = (event.get("message") or {}).get("content") or []
183
+ for block in content:
184
+ if not isinstance(block, dict) or block.get("type") != "tool_result":
185
+ continue
186
+ tool_use_id = block.get("tool_use_id")
187
+ if isinstance(tool_use_id, str) and tool_use_id in tool_calls_by_id:
188
+ tool_calls_by_id[tool_use_id]["success"] = _tool_result_succeeded(block)
189
+ continue
190
+ if event.get("type") != "result":
191
+ continue
192
+ result = event.get("result")
193
+ turns.append(
194
+ {
195
+ "turn": len(turns) + 1,
196
+ "result": result if isinstance(result, str) and result else last_assistant_text,
197
+ "tool_calls": tool_calls,
198
+ }
199
+ )
200
+ tool_calls = []
201
+ tool_calls_by_id = {}
202
+ last_assistant_text = ""
203
+ return turns
204
+
205
+
70
206
  # =============================================================================
71
207
  # CONSTANTS
72
208
  # =============================================================================
@@ -695,6 +831,7 @@ def _resolve_interaction_config(task, profile_name: str, config):
695
831
  task_interaction.decision_patterns or profile_default.decision_patterns
696
832
  )
697
833
  decision_reply = task_interaction.decision_reply or profile_default.decision_reply
834
+ decision_replies = list(task_interaction.decision_replies or profile_default.decision_replies)
698
835
  continue_prompt = task_interaction.continue_prompt or profile_default.continue_prompt
699
836
  fresh_resume_marker = task_interaction.fresh_resume_marker
700
837
 
@@ -712,7 +849,7 @@ def _resolve_interaction_config(task, profile_name: str, config):
712
849
  prompt_path = Path(prompt_file) if prompt_file else (EVAL_ROOT / "simulator-instruction.md")
713
850
  if not prompt_path.is_absolute():
714
851
  prompt_path = EVAL_ROOT / prompt_path
715
- if prompt_path.exists():
852
+ if prompt_path.exists() and (prompt_file or not task_interaction.simulator_prompt):
716
853
  simulator_prompt = prompt_path.read_text(encoding="utf-8")
717
854
 
718
855
  if simulator_prompt_override:
@@ -724,6 +861,7 @@ def _resolve_interaction_config(task, profile_name: str, config):
724
861
  simulator_prompt=simulator_prompt,
725
862
  decision_patterns=decision_patterns,
726
863
  decision_reply=decision_reply,
864
+ decision_replies=decision_replies,
727
865
  continue_prompt=continue_prompt,
728
866
  fresh_resume_marker=fresh_resume_marker,
729
867
  )
@@ -1137,12 +1275,18 @@ def run_claude(test_dir, experiment_logger, request):
1137
1275
  loop_args += ["--decision-pattern", pattern]
1138
1276
  if interaction.decision_reply:
1139
1277
  loop_args += ["--decision-reply", interaction.decision_reply]
1278
+ for decision_reply_step in interaction.decision_replies:
1279
+ loop_args += ["--decision-reply-step", decision_reply_step]
1140
1280
  if interaction.fresh_resume_marker:
1141
1281
  loop_args += ["--fresh-resume-marker", interaction.fresh_resume_marker]
1142
1282
 
1143
1283
  prompt_file = None
1144
1284
  try:
1145
- if interaction.simulator_prompt and not interaction.decision_reply:
1285
+ if (
1286
+ interaction.simulator_prompt
1287
+ and not interaction.decision_reply
1288
+ and not interaction.decision_replies
1289
+ ):
1146
1290
  prompt_file = test_dir / ".eval-simulator-prompt.txt"
1147
1291
  prompt_file.write_text(interaction.simulator_prompt, encoding="utf-8")
1148
1292
  loop_args += [
@@ -101,6 +101,154 @@ def test_extract_loop_turns_reads_driver_completion_line():
101
101
  assert conftest._extract_loop_turns("ordinary stderr") is None
102
102
 
103
103
 
104
+ def test_extract_subject_turn_evidence_groups_results_and_tool_calls():
105
+ stdout = "\n".join(
106
+ [
107
+ json.dumps(
108
+ {
109
+ "type": "assistant",
110
+ "message": {
111
+ "content": [
112
+ {"type": "text", "text": "Investigating."},
113
+ {
114
+ "type": "tool_use",
115
+ "id": "read-1",
116
+ "name": "Read",
117
+ "input": {"file_path": "x"},
118
+ },
119
+ {
120
+ "type": "tool_use",
121
+ "id": "bash-1",
122
+ "name": "Bash",
123
+ "input": {"command": "AUTH_TOKEN=topsecret cat > wordcount.py"},
124
+ },
125
+ {
126
+ "type": "tool_use",
127
+ "id": "edit-failed",
128
+ "name": "Edit",
129
+ "input": {"file_path": "sentence.py"},
130
+ },
131
+ {
132
+ "type": "tool_use",
133
+ "id": "bash-failed",
134
+ "name": "Bash",
135
+ "input": {"command": "cat > fallback.py"},
136
+ },
137
+ ]
138
+ },
139
+ }
140
+ ),
141
+ json.dumps(
142
+ {
143
+ "type": "user",
144
+ "message": {
145
+ "content": [
146
+ {
147
+ "type": "tool_result",
148
+ "tool_use_id": "read-1",
149
+ "content": "source",
150
+ },
151
+ {
152
+ "type": "tool_result",
153
+ "tool_use_id": "bash-1",
154
+ "content": "updated",
155
+ },
156
+ {
157
+ "type": "tool_result",
158
+ "tool_use_id": "edit-failed",
159
+ "content": "permission denied",
160
+ "is_error": True,
161
+ },
162
+ {
163
+ "type": "tool_result",
164
+ "tool_use_id": "bash-failed",
165
+ "content": "Process exited with code 1",
166
+ },
167
+ ]
168
+ },
169
+ }
170
+ ),
171
+ json.dumps(
172
+ {
173
+ "type": "result",
174
+ "subtype": "success",
175
+ "result": "[blocking] QUESTION\nShould empty input return zero?",
176
+ }
177
+ ),
178
+ json.dumps(
179
+ {
180
+ "type": "assistant",
181
+ "message": {
182
+ "content": [
183
+ {
184
+ "type": "tool_use",
185
+ "id": "write-1",
186
+ "name": "Write",
187
+ "input": {"file_path": "y"},
188
+ },
189
+ {
190
+ "type": "text",
191
+ "text": "Implementation completed through archive.",
192
+ },
193
+ ]
194
+ },
195
+ }
196
+ ),
197
+ json.dumps(
198
+ {
199
+ "type": "user",
200
+ "message": {
201
+ "content": [
202
+ {
203
+ "type": "tool_result",
204
+ "tool_use_id": "write-1",
205
+ "content": "created",
206
+ }
207
+ ]
208
+ },
209
+ }
210
+ ),
211
+ json.dumps(
212
+ {
213
+ "type": "result",
214
+ "subtype": "success",
215
+ "result": "",
216
+ }
217
+ ),
218
+ ]
219
+ )
220
+
221
+ assert conftest._extract_subject_turn_evidence(stdout) == [
222
+ {
223
+ "turn": 1,
224
+ "result": "[blocking] QUESTION\nShould empty input return zero?",
225
+ "tool_calls": [
226
+ {"name": "Read", "success": True, "path": "x"},
227
+ {
228
+ "name": "Bash",
229
+ "success": True,
230
+ "command": "AUTH_TOKEN=[REDACTED] cat > wordcount.py",
231
+ },
232
+ {
233
+ "name": "Edit",
234
+ "success": False,
235
+ "path": "sentence.py",
236
+ },
237
+ {
238
+ "name": "Bash",
239
+ "success": False,
240
+ "command": "cat > fallback.py",
241
+ },
242
+ ],
243
+ },
244
+ {
245
+ "turn": 2,
246
+ "result": "Implementation completed through archive.",
247
+ "tool_calls": [{"name": "Write", "success": True, "path": "y"}],
248
+ },
249
+ ]
250
+
251
+
104
252
  def test_capture_execution_identity_separates_runtime_image_from_safe_report(
105
253
  tmp_path: Path, monkeypatch
106
254
  ):
@@ -201,7 +349,8 @@ def test_auto_user_prompt_paths_bypass_msys_path_conversion():
201
349
 
202
350
  assert '"@//workspace/.eval-task-prompt.txt"' in source
203
351
  assert '"//workspace/.eval-simulator-prompt.txt"' in source
204
- assert "interaction.simulator_prompt and not interaction.decision_reply" in source
352
+ assert "and not interaction.decision_reply" in source
353
+ assert "and not interaction.decision_replies" in source
205
354
 
206
355
 
207
356
  def test_dynamic_treatment_config_from_skill_path(tmp_path: Path):
@@ -272,6 +421,44 @@ def test_resolve_interaction_config_uses_profile_default_prompt():
272
421
  assert interaction.simulator_prompt is not None
273
422
 
274
423
 
424
+ def test_resolve_interaction_config_preserves_task_simulator_prompt(monkeypatch):
425
+ task = load_task("comet-native-clarification-modes")
426
+ monkeypatch.delenv("BENCH_SIMULATOR_PROMPT_FILE", raising=False)
427
+
428
+ class Config:
429
+ def getoption(self, name):
430
+ return {
431
+ "--interaction-mode": None,
432
+ "--max-turns": None,
433
+ "--simulator-prompt": None,
434
+ }.get(name)
435
+
436
+ interaction = conftest._resolve_interaction_config(task, "generic", Config())
437
+
438
+ assert interaction.simulator_prompt == task.config.interaction.simulator_prompt
439
+
440
+
441
+ def test_resolve_interaction_config_allows_explicit_prompt_file_override(
442
+ tmp_path: Path, monkeypatch
443
+ ):
444
+ task = load_task("comet-native-clarification-modes")
445
+ prompt_file = tmp_path / "simulator.md"
446
+ prompt_file.write_text("Use the explicit simulator.", encoding="utf-8")
447
+ monkeypatch.setenv("BENCH_SIMULATOR_PROMPT_FILE", str(prompt_file))
448
+
449
+ class Config:
450
+ def getoption(self, name):
451
+ return {
452
+ "--interaction-mode": None,
453
+ "--max-turns": None,
454
+ "--simulator-prompt": None,
455
+ }.get(name)
456
+
457
+ interaction = conftest._resolve_interaction_config(task, "generic", Config())
458
+
459
+ assert interaction.simulator_prompt == "Use the explicit simulator."
460
+
461
+
275
462
  def test_build_eval_claude_md_injects_comet_workflow_contract():
276
463
  claude_md = conftest._build_eval_claude_md("comet-workflow")
277
464