@rpamis/comet 0.4.0-beta.8 → 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 (128) hide show
  1. package/README.md +1 -1
  2. package/assets/manifest.json +1 -1
  3. package/assets/skills/comet/reference/comet-yaml-fields.md +2 -2
  4. package/assets/skills/comet/scripts/comet-entry-runtime.mjs +95 -4
  5. package/assets/skills/comet/scripts/comet-hook-router.mjs +224 -35
  6. package/assets/skills/comet/scripts/comet-runtime.mjs +218 -102
  7. package/assets/skills/comet-archive/SKILL.md +29 -20
  8. package/assets/skills/comet-native/SKILL.md +23 -15
  9. package/assets/skills/comet-native/reference/artifacts.md +19 -4
  10. package/assets/skills/comet-native/reference/commands.md +8 -2
  11. package/assets/skills/comet-native/reference/recovery.md +1 -1
  12. package/assets/skills/comet-native/scripts/comet-native-runtime.mjs +1052 -458
  13. package/assets/skills-zh/comet/reference/comet-yaml-fields.md +2 -2
  14. package/assets/skills-zh/comet-archive/SKILL.md +29 -20
  15. package/assets/skills-zh/comet-native/SKILL.md +23 -15
  16. package/assets/skills-zh/comet-native/reference/artifacts.md +20 -5
  17. package/assets/skills-zh/comet-native/reference/commands.md +8 -2
  18. package/assets/skills-zh/comet-native/reference/recovery.md +1 -1
  19. package/dist/app/commands/init.d.ts.map +1 -1
  20. package/dist/app/commands/init.js +3 -6
  21. package/dist/app/commands/init.js.map +1 -1
  22. package/dist/domains/comet-entry/current-selection.d.ts.map +1 -1
  23. package/dist/domains/comet-entry/current-selection.js +6 -8
  24. package/dist/domains/comet-entry/current-selection.js.map +1 -1
  25. package/dist/domains/comet-entry/project-status.d.ts.map +1 -1
  26. package/dist/domains/comet-entry/project-status.js +5 -1
  27. package/dist/domains/comet-entry/project-status.js.map +1 -1
  28. package/dist/domains/comet-native/native-build-evidence.js +2 -2
  29. package/dist/domains/comet-native/native-build-evidence.js.map +1 -1
  30. package/dist/domains/comet-native/native-change.d.ts +4 -2
  31. package/dist/domains/comet-native/native-change.d.ts.map +1 -1
  32. package/dist/domains/comet-native/native-change.js +18 -4
  33. package/dist/domains/comet-native/native-change.js.map +1 -1
  34. package/dist/domains/comet-native/native-check-receipt.d.ts.map +1 -1
  35. package/dist/domains/comet-native/native-check-receipt.js +5 -3
  36. package/dist/domains/comet-native/native-check-receipt.js.map +1 -1
  37. package/dist/domains/comet-native/native-cli.d.ts.map +1 -1
  38. package/dist/domains/comet-native/native-cli.js +120 -16
  39. package/dist/domains/comet-native/native-cli.js.map +1 -1
  40. package/dist/domains/comet-native/native-config.d.ts +5 -1
  41. package/dist/domains/comet-native/native-config.d.ts.map +1 -1
  42. package/dist/domains/comet-native/native-config.js +78 -0
  43. package/dist/domains/comet-native/native-config.js.map +1 -1
  44. package/dist/domains/comet-native/native-continuation.d.ts +2 -1
  45. package/dist/domains/comet-native/native-continuation.d.ts.map +1 -1
  46. package/dist/domains/comet-native/native-continuation.js +13 -5
  47. package/dist/domains/comet-native/native-continuation.js.map +1 -1
  48. package/dist/domains/comet-native/native-diagnostics.d.ts +7 -3
  49. package/dist/domains/comet-native/native-diagnostics.d.ts.map +1 -1
  50. package/dist/domains/comet-native/native-diagnostics.js +19 -9
  51. package/dist/domains/comet-native/native-diagnostics.js.map +1 -1
  52. package/dist/domains/comet-native/native-file-identity.d.ts +2 -8
  53. package/dist/domains/comet-native/native-file-identity.d.ts.map +1 -1
  54. package/dist/domains/comet-native/native-file-identity.js +1 -20
  55. package/dist/domains/comet-native/native-file-identity.js.map +1 -1
  56. package/dist/domains/comet-native/native-findings.d.ts.map +1 -1
  57. package/dist/domains/comet-native/native-findings.js +19 -1
  58. package/dist/domains/comet-native/native-findings.js.map +1 -1
  59. package/dist/domains/comet-native/native-guards.d.ts +2 -1
  60. package/dist/domains/comet-native/native-guards.d.ts.map +1 -1
  61. package/dist/domains/comet-native/native-guards.js +14 -0
  62. package/dist/domains/comet-native/native-guards.js.map +1 -1
  63. package/dist/domains/comet-native/native-lock.d.ts.map +1 -1
  64. package/dist/domains/comet-native/native-lock.js +24 -32
  65. package/dist/domains/comet-native/native-lock.js.map +1 -1
  66. package/dist/domains/comet-native/native-root-move.d.ts.map +1 -1
  67. package/dist/domains/comet-native/native-root-move.js +2 -0
  68. package/dist/domains/comet-native/native-root-move.js.map +1 -1
  69. package/dist/domains/comet-native/native-snapshot.d.ts +5 -1
  70. package/dist/domains/comet-native/native-snapshot.d.ts.map +1 -1
  71. package/dist/domains/comet-native/native-snapshot.js +218 -2
  72. package/dist/domains/comet-native/native-snapshot.js.map +1 -1
  73. package/dist/domains/comet-native/native-transitions.d.ts +2 -1
  74. package/dist/domains/comet-native/native-transitions.d.ts.map +1 -1
  75. package/dist/domains/comet-native/native-transitions.js +34 -11
  76. package/dist/domains/comet-native/native-transitions.js.map +1 -1
  77. package/dist/domains/comet-native/native-types.d.ts +16 -0
  78. package/dist/domains/comet-native/native-types.d.ts.map +1 -1
  79. package/dist/domains/comet-native/native-verification-runtime.d.ts.map +1 -1
  80. package/dist/domains/comet-native/native-verification-runtime.js +5 -3
  81. package/dist/domains/comet-native/native-verification-runtime.js.map +1 -1
  82. package/dist/domains/comet-native/native-verification-scope.d.ts +2 -1
  83. package/dist/domains/comet-native/native-verification-scope.d.ts.map +1 -1
  84. package/dist/domains/comet-native/native-verification-scope.js +6 -1
  85. package/dist/domains/comet-native/native-verification-scope.js.map +1 -1
  86. package/dist/domains/dashboard/native-collector.d.ts.map +1 -1
  87. package/dist/domains/dashboard/native-collector.js +4 -1
  88. package/dist/domains/dashboard/native-collector.js.map +1 -1
  89. package/dist/domains/skill/platform-install.d.ts.map +1 -1
  90. package/dist/domains/skill/platform-install.js +9 -0
  91. package/dist/domains/skill/platform-install.js.map +1 -1
  92. package/dist/domains/skill/project-instructions.d.ts.map +1 -1
  93. package/dist/domains/skill/project-instructions.js +4 -2
  94. package/dist/domains/skill/project-instructions.js.map +1 -1
  95. package/dist/domains/workflow-contract/project-config.d.ts +1 -1
  96. package/dist/domains/workflow-contract/project-config.d.ts.map +1 -1
  97. package/dist/domains/workflow-contract/project-config.js +24 -2
  98. package/dist/domains/workflow-contract/project-config.js.map +1 -1
  99. package/dist/platform/fs/file-identity.d.ts +9 -0
  100. package/dist/platform/fs/file-identity.d.ts.map +1 -0
  101. package/dist/platform/fs/file-identity.js +21 -0
  102. package/dist/platform/fs/file-identity.js.map +1 -0
  103. package/dist/platform/fs/file-system.d.ts.map +1 -1
  104. package/dist/platform/fs/file-system.js +13 -9
  105. package/dist/platform/fs/file-system.js.map +1 -1
  106. package/dist/platform/fs/race-safe-read.d.ts +47 -0
  107. package/dist/platform/fs/race-safe-read.d.ts.map +1 -0
  108. package/dist/platform/fs/race-safe-read.js +132 -0
  109. package/dist/platform/fs/race-safe-read.js.map +1 -0
  110. package/eval/README.md +1 -1
  111. package/eval/local/tasks/comet-native-clarification-depth/environment/Dockerfile +27 -0
  112. package/eval/local/tasks/comet-native-clarification-depth/environment/test_wordcount.py +25 -0
  113. package/eval/local/tasks/comet-native-clarification-depth/environment/wordcount.py +36 -0
  114. package/eval/local/tasks/comet-native-clarification-depth/instruction.md +17 -0
  115. package/eval/local/tasks/comet-native-clarification-depth/task.toml +47 -0
  116. package/eval/local/tasks/comet-native-clarification-depth/validation/test_native_clarification_depth.py +618 -0
  117. package/eval/local/tasks/comet-native-clarification-modes/validation/test_native_clarification_modes.py +1 -1
  118. package/eval/local/tasks/index.yaml +6 -0
  119. package/eval/local/tests/conftest.py +146 -2
  120. package/eval/local/tests/scaffold/test_conftest_helpers.py +188 -1
  121. package/eval/local/tests/scaffold/test_tasks.py +440 -8
  122. package/eval/local/tests/scaffold/test_utils.py +114 -1
  123. package/eval/local/tests/tasks/test_tasks.py +2 -0
  124. package/eval/scaffold/python/aligned_comparison.py +1 -0
  125. package/eval/scaffold/python/tasks.py +2 -0
  126. package/eval/scaffold/shell/docker.sh +1 -1
  127. package/eval/scaffold/shell/run-claude-loop.sh +25 -2
  128. package/package.json +1 -1
@@ -88,6 +88,7 @@ max_turns = 7
88
88
  simulator_prompt = "Answer as a concise developer user."
89
89
  decision_patterns = ["confirm", "choose"]
90
90
  decision_reply = "Use the recommended option."
91
+ decision_replies = ["First reply.", "Second reply."]
91
92
  continue_prompt = "Please continue."
92
93
  fresh_resume_marker = "COLD_RESUME_READY"
93
94
  """
@@ -105,6 +106,7 @@ fresh_resume_marker = "COLD_RESUME_READY"
105
106
  assert task.config.interaction.simulator_prompt == "Answer as a concise developer user."
106
107
  assert task.config.interaction.decision_patterns == ["confirm", "choose"]
107
108
  assert task.config.interaction.decision_reply == "Use the recommended option."
109
+ assert task.config.interaction.decision_replies == ["First reply.", "Second reply."]
108
110
  assert task.config.interaction.continue_prompt == "Please continue."
109
111
  assert task.config.interaction.fresh_resume_marker == "COLD_RESUME_READY"
110
112
 
@@ -153,7 +155,7 @@ def test_comet_task_index_lists_real_tasks():
153
155
  index = yaml.safe_load(index_path.read_text(encoding="utf-8"))
154
156
  names = [task["name"] for task in index["tasks"]]
155
157
  assert sorted(names) == sorted(list_tasks())
156
- assert len(names) == 30
158
+ assert len(names) == 31
157
159
  assert set(names) == {
158
160
  "authoring-skill-smoke",
159
161
  "comet-agent-memory-routing",
@@ -173,6 +175,7 @@ def test_comet_task_index_lists_real_tasks():
173
175
  "comet-noise-distractor",
174
176
  "comet-native-workflow",
175
177
  "comet-native-clarification",
178
+ "comet-native-clarification-depth",
176
179
  "comet-native-clarification-modes",
177
180
  "comet-native-repository-fact",
178
181
  "comet-native-interrupted-transition",
@@ -237,10 +240,429 @@ def test_native_clarification_modes_task_compares_batch_and_sequential():
237
240
  assert "Do not choose the clarification mode" in prompt
238
241
 
239
242
 
243
+ def test_native_clarification_depth_task_keeps_answers_private_and_sequential():
244
+ task = load_task("comet-native-clarification-depth")
245
+
246
+ assert task.default_treatments == ["COMET_NATIVE_SEQUENTIAL"]
247
+ assert task.config.evaluation.profile == "generic"
248
+ assert task.config.evaluation.required_skills == ["comet-native"]
249
+ assert task.config.interaction.mode == "auto_user"
250
+ assert task.config.interaction.max_turns == 8
251
+ assert task.config.timeout_sec == 2400
252
+ assert task.config.interaction.decision_reply is None
253
+ assert task.config.interaction.simulator_prompt is None
254
+ assert task.config.interaction.decision_replies == [
255
+ "It prints Sentences: 0.",
256
+ "They count as one sentence boundary.",
257
+ (
258
+ "Recognized abbreviations such as e.g. and Dr. always suppress their periods, "
259
+ "including at end-of-input; every other occurrence of ., !, or ? remains a "
260
+ "sentence boundary, and the non-empty fallback still makes an "
261
+ "abbreviation-only input one sentence."
262
+ ),
263
+ "Use a small explicit list. Ask me for the exact entries separately.",
264
+ (
265
+ "Use exactly Dr., Mr., Mrs., Ms., Prof., Sr., Jr., e.g., i.e., etc., "
266
+ "vs., Inc., Ltd., Corp., St., and Ave., matched case-sensitively exactly "
267
+ "as written."
268
+ ),
269
+ "confirmed",
270
+ ]
271
+
272
+ prompt = task.render_prompt().lower()
273
+ assert "sentences: 0" not in prompt
274
+ assert "small explicit list" not in prompt
275
+ assert "e.g." not in prompt
276
+ assert "follow the already configured native sequential clarification protocol" in prompt
277
+ assert "resolve every undefined user-visible behavior" in prompt
278
+ for leaked_protocol_step in (
279
+ "one decision per turn",
280
+ "recommendation",
281
+ "rescan",
282
+ "shared-understanding",
283
+ "explicit confirmation",
284
+ ):
285
+ assert leaked_protocol_step not in prompt
286
+
287
+ task_root = get_tasks_dir() / "comet-native-clarification-depth"
288
+ forbidden = "gr" + "ill"
289
+ for path in task_root.rglob("*"):
290
+ assert forbidden not in path.name.lower()
291
+ if path.is_file() and path.suffix in {".md", ".toml", ".py", ".txt", ".yaml", ".yml"}:
292
+ assert forbidden not in path.read_text(encoding="utf-8").lower()
293
+
294
+
295
+ def test_native_clarification_depth_validator_requires_five_questions_then_confirmation(
296
+ tmp_path: Path,
297
+ ):
298
+ validator_path = (
299
+ get_tasks_dir()
300
+ / "comet-native-clarification-depth"
301
+ / "validation"
302
+ / "test_native_clarification_depth.py"
303
+ )
304
+ spec = importlib.util.spec_from_file_location(
305
+ "native_clarification_depth_validator", validator_path
306
+ )
307
+ assert spec and spec.loader
308
+ validator = importlib.util.module_from_spec(spec)
309
+ spec.loader.exec_module(validator)
310
+ validator.WORKSPACE = tmp_path
311
+ brief_path = "/workspace/docs/comet/changes/add-sentence-counting/brief.md"
312
+ spec_path = (
313
+ "/workspace/docs/comet/changes/add-sentence-counting/specs/sentence-counting/spec.md"
314
+ )
315
+
316
+ config = tmp_path / ".comet" / "config.yaml"
317
+ config.parent.mkdir(parents=True)
318
+ config.write_text(
319
+ "schema: comet.project.v1\nnative:\n clarification_mode: sequential\n",
320
+ encoding="utf-8",
321
+ )
322
+ (tmp_path / "_test_context.json").write_text(
323
+ json.dumps(
324
+ {
325
+ "treatment_name": "COMET_NATIVE_SEQUENTIAL",
326
+ "interaction": {
327
+ "mode": "auto_user",
328
+ "actual_turns": 7,
329
+ "decision_points": 6,
330
+ "deterministic_replies": 6,
331
+ "subject_turns": [
332
+ {
333
+ "turn": 1,
334
+ "result": (
335
+ "Question: What should empty input print?\n"
336
+ "Recommendation: print zero. Impact: keeps output numeric."
337
+ ),
338
+ "tool_calls": [
339
+ {
340
+ "name": "Read",
341
+ "success": True,
342
+ "path": "/workspace/wordcount.py",
343
+ }
344
+ ],
345
+ },
346
+ {
347
+ "turn": 2,
348
+ "result": (
349
+ "Question: Should consecutive terminators such as ?! "
350
+ "form one boundary?\nRecommendation: count one boundary. "
351
+ "Impact: avoids double counting."
352
+ ),
353
+ "tool_calls": [
354
+ {
355
+ "name": "Edit",
356
+ "success": True,
357
+ "path": brief_path,
358
+ },
359
+ {
360
+ "name": "Edit",
361
+ "success": True,
362
+ "path": spec_path,
363
+ },
364
+ ],
365
+ },
366
+ {
367
+ "turn": 3,
368
+ "result": (
369
+ "**Question:** Should abbreviations such as Dr. end a "
370
+ "sentence?\n**Recommendation:** do not treat recognized "
371
+ "abbreviations as boundaries. **Impact:** avoids false boundaries."
372
+ ),
373
+ "tool_calls": [
374
+ {
375
+ "name": "Edit",
376
+ "success": True,
377
+ "path": brief_path,
378
+ },
379
+ {
380
+ "name": "Edit",
381
+ "success": True,
382
+ "path": spec_path,
383
+ },
384
+ ],
385
+ },
386
+ {
387
+ "turn": 4,
388
+ "result": (
389
+ "Question: How should the recognized abbreviation collection "
390
+ "be maintained?\nRecommendation: use a small explicit list. "
391
+ "Impact: keeps exceptions reviewable."
392
+ ),
393
+ "tool_calls": [
394
+ {
395
+ "name": "Edit",
396
+ "success": True,
397
+ "path": brief_path,
398
+ },
399
+ {
400
+ "name": "Edit",
401
+ "success": True,
402
+ "path": spec_path,
403
+ },
404
+ ],
405
+ },
406
+ {
407
+ "turn": 5,
408
+ "result": (
409
+ "Question: Which exact abbreviation entries should the list "
410
+ "contain?\nRecommendation: include common titles and prose "
411
+ "short forms. Impact: fixes the compatibility boundary."
412
+ ),
413
+ "tool_calls": [
414
+ {
415
+ "name": "Edit",
416
+ "success": True,
417
+ "path": brief_path,
418
+ },
419
+ {
420
+ "name": "Edit",
421
+ "success": True,
422
+ "path": spec_path,
423
+ },
424
+ ],
425
+ },
426
+ {
427
+ "turn": 6,
428
+ "result": (
429
+ "My understanding:\nGoal / outcome: add reliable sentence counting."
430
+ "\nScope: the --sentences CLI flag and focused tests."
431
+ "\nExplicit non-goals: changing existing word or line counts."
432
+ "\nAcceptance criteria: common CLI inputs return the agreed counts."
433
+ "\nDecisions: abbreviations do not end sentences; use a small "
434
+ "explicit abbreviation list containing Dr., Mr., Mrs., Ms., Prof., "
435
+ "Sr., Jr., e.g., i.e., etc., vs., Inc., Ltd., Corp., St., and Ave.; "
436
+ "empty input "
437
+ "prints Sentences: 0; consecutive terminators form one boundary. "
438
+ "Please confirm."
439
+ ),
440
+ "tool_calls": [
441
+ {
442
+ "name": "Edit",
443
+ "success": True,
444
+ "path": brief_path,
445
+ },
446
+ {
447
+ "name": "Edit",
448
+ "success": True,
449
+ "path": spec_path,
450
+ },
451
+ ],
452
+ },
453
+ {
454
+ "turn": 7,
455
+ "result": "Completed through all phases and archived.",
456
+ "tool_calls": [
457
+ {
458
+ "name": "Edit",
459
+ "success": True,
460
+ "path": "/workspace/wordcount.py",
461
+ },
462
+ {
463
+ "name": "Bash",
464
+ "success": True,
465
+ "command": "python wordcount.py --sentences",
466
+ },
467
+ ],
468
+ },
469
+ ],
470
+ },
471
+ }
472
+ ),
473
+ encoding="utf-8",
474
+ )
475
+
476
+ protocol = validator.check_clarification_protocol()
477
+ assert protocol["status"] == "passed", protocol
478
+ assert validator.contradictory_decisions(
479
+ "Abbreviations do not end sentences. Abbreviations are ordinary prose "
480
+ "tokens with no special-casing."
481
+ ) == ["abbreviation behavior"]
482
+ assert (
483
+ validator.contradictory_decisions(
484
+ "Abbreviations do not end sentences and use a fixed explicit list."
485
+ )
486
+ == []
487
+ )
488
+
489
+ context = json.loads((tmp_path / "_test_context.json").read_text(encoding="utf-8"))
490
+ context["interaction"]["subject_turns"][0]["tool_calls"].append(
491
+ {
492
+ "name": "Edit",
493
+ "success": False,
494
+ "path": "/workspace/sentence.py",
495
+ }
496
+ )
497
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
498
+
499
+ protocol = validator.check_clarification_protocol()
500
+ assert protocol["status"] == "passed", protocol
501
+
502
+ context["interaction"]["subject_turns"][0]["tool_calls"].pop()
503
+ context["interaction"]["subject_turns"][0]["tool_calls"].append(
504
+ {
505
+ "name": "Write",
506
+ "success": True,
507
+ "path": "/tmp/evidence.py",
508
+ }
509
+ )
510
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
511
+
512
+ protocol = validator.check_clarification_protocol()
513
+ assert protocol["status"] == "passed", protocol
514
+
515
+ context["interaction"]["subject_turns"][0]["tool_calls"].pop()
516
+ context["interaction"]["subject_turns"][0]["tool_calls"].append(
517
+ {
518
+ "name": "Write",
519
+ "success": True,
520
+ "path": ("/workspace/docs/comet/changes/add-sentence-counting/runtime/evidence.py"),
521
+ }
522
+ )
523
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
524
+
525
+ result = validator.check_clarification_protocol()
526
+ assert result["status"] == "failed"
527
+ assert "before explicit confirmation" in result["reason"].lower()
528
+
529
+ context["interaction"]["subject_turns"][0]["tool_calls"].pop()
530
+ context["interaction"]["subject_turns"][0]["tool_calls"].append(
531
+ {
532
+ "name": "Edit",
533
+ "success": True,
534
+ "path": "/workspace/sentence.py",
535
+ }
536
+ )
537
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
538
+
539
+ result = validator.check_clarification_protocol()
540
+ assert result["status"] == "failed"
541
+ assert "before explicit confirmation" in result["reason"].lower()
542
+
543
+ context["interaction"]["subject_turns"][0]["tool_calls"].pop()
544
+ context["interaction"]["subject_turns"][0]["tool_calls"].append(
545
+ {
546
+ "name": "Bash",
547
+ "success": True,
548
+ "command": "cat > test_wordcount.py",
549
+ }
550
+ )
551
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
552
+
553
+ result = validator.check_clarification_protocol()
554
+ assert result["status"] == "failed"
555
+ assert "before explicit confirmation" in result["reason"].lower()
556
+
557
+ context["interaction"]["subject_turns"][0]["tool_calls"].pop()
558
+ investigation = context["interaction"]["subject_turns"][0]["tool_calls"]
559
+ context["interaction"]["subject_turns"][0]["tool_calls"] = [
560
+ {
561
+ **investigation[0],
562
+ "success": False,
563
+ }
564
+ ]
565
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
566
+
567
+ result = validator.check_clarification_protocol()
568
+ assert result["status"] == "failed"
569
+ assert "not investigated" in result["reason"].lower()
570
+
571
+ context["interaction"]["subject_turns"][0]["tool_calls"] = investigation
572
+ persistence = context["interaction"]["subject_turns"][1]["tool_calls"]
573
+ context["interaction"]["subject_turns"][1]["tool_calls"] = [
574
+ {
575
+ **persistence[0],
576
+ "success": False,
577
+ },
578
+ persistence[1],
579
+ ]
580
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
581
+
582
+ result = validator.check_clarification_protocol()
583
+ assert result["status"] == "failed"
584
+ assert "persistence evidence" in result["reason"].lower()
585
+ assert "brief" in result["reason"].lower()
586
+
587
+ context["interaction"]["subject_turns"][1]["tool_calls"] = [
588
+ {
589
+ **persistence[0],
590
+ "path": "/workspace/brief.md",
591
+ },
592
+ persistence[1],
593
+ ]
594
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
595
+
596
+ result = validator.check_clarification_protocol()
597
+ assert result["status"] == "failed"
598
+ assert "brief" in result["reason"].lower()
599
+
600
+ context["interaction"]["subject_turns"][1]["tool_calls"] = persistence
601
+ implementation = context["interaction"]["subject_turns"][6]["tool_calls"]
602
+ context["interaction"]["subject_turns"][6]["tool_calls"] = [
603
+ {
604
+ "name": "Bash",
605
+ "success": True,
606
+ "command": "python wordcount.py --sentences",
607
+ }
608
+ ]
609
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
610
+
611
+ result = validator.check_clarification_protocol()
612
+ assert result["status"] == "failed"
613
+ assert "after explicit confirmation" in result["reason"].lower()
614
+
615
+ context["interaction"]["subject_turns"][6]["tool_calls"] = implementation
616
+ confirmation = context["interaction"]["subject_turns"][5]["result"]
617
+ context["interaction"]["subject_turns"][5]["result"] = (
618
+ "Our understanding summary:\nGoal: add sentence counting.\nScope: the CLI."
619
+ "\nNon-goals: existing counters.\nAcceptance criteria: agreed counts are printed."
620
+ "\nDecisions: abbreviation behavior, empty input, and terminator runs. Please confirm."
621
+ )
622
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
623
+
624
+ result = validator.check_clarification_protocol()
625
+ assert result["status"] == "failed"
626
+ assert "confirmation summary is incomplete" in result["reason"].lower()
627
+
628
+ context["interaction"]["subject_turns"][5]["result"] = (
629
+ "Our understanding: abbreviations do not end sentences; use a small explicit "
630
+ "abbreviation list including e.g. and Dr.; empty input prints Sentences: 0; "
631
+ "consecutive terminators form one boundary. Please confirm."
632
+ )
633
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
634
+
635
+ result = validator.check_clarification_protocol()
636
+ assert result["status"] == "failed"
637
+ assert "goal/outcome" in result["reason"].lower()
638
+
639
+ context["interaction"]["subject_turns"][5]["result"] = confirmation
640
+ upstream = context["interaction"]["subject_turns"][3]["result"]
641
+ downstream = context["interaction"]["subject_turns"][4]["result"]
642
+ context["interaction"]["subject_turns"][3]["result"] = downstream
643
+ context["interaction"]["subject_turns"][4]["result"] = upstream
644
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
645
+
646
+ result = validator.check_clarification_protocol()
647
+ assert result["status"] == "failed"
648
+ assert "hidden and dependent decisions in order" in result["reason"].lower()
649
+
650
+ context["interaction"]["subject_turns"][3]["result"] = upstream
651
+ context["interaction"]["subject_turns"][4]["result"] = downstream
652
+ context["interaction"]["subject_turns"][0]["result"] += (
653
+ "\nQuestion: What should empty input print?"
654
+ )
655
+ (tmp_path / "_test_context.json").write_text(json.dumps(context), encoding="utf-8")
656
+
657
+ result = validator.check_clarification_protocol()
658
+ assert result["status"] == "failed"
659
+ assert "exactly one" in result["reason"].lower()
660
+
661
+
240
662
  @pytest.mark.parametrize(
241
663
  ("treatment", "mode", "decision_points"),
242
664
  [
243
- ("COMET_NATIVE_SEQUENTIAL", "sequential", 3),
665
+ ("COMET_NATIVE_SEQUENTIAL", "sequential", 4),
244
666
  ("COMET_NATIVE_BATCH", "batch", 2),
245
667
  ],
246
668
  )
@@ -320,7 +742,10 @@ A contiguous run of terminal punctuation (`.`, `!`, `?`) counts as exactly one b
320
742
 
321
743
  def test_native_clarification_validator_rejects_multiple_archives(tmp_path: Path):
322
744
  validator_path = (
323
- get_tasks_dir() / "comet-native-clarification" / "validation" / "test_native_clarification.py"
745
+ get_tasks_dir()
746
+ / "comet-native-clarification"
747
+ / "validation"
748
+ / "test_native_clarification.py"
324
749
  )
325
750
  spec = importlib.util.spec_from_file_location("native_clarification_validator", validator_path)
326
751
  assert spec and spec.loader
@@ -346,7 +771,10 @@ def test_native_clarification_validator_rejects_multiple_archives(tmp_path: Path
346
771
 
347
772
  def test_native_clarification_validator_accepts_one_semantic_canonical_spec(tmp_path: Path):
348
773
  validator_path = (
349
- get_tasks_dir() / "comet-native-clarification" / "validation" / "test_native_clarification.py"
774
+ get_tasks_dir()
775
+ / "comet-native-clarification"
776
+ / "validation"
777
+ / "test_native_clarification.py"
350
778
  )
351
779
  spec = importlib.util.spec_from_file_location("native_clarification_validator", validator_path)
352
780
  assert spec and spec.loader
@@ -431,7 +859,10 @@ archived: true
431
859
 
432
860
  def test_native_clarification_validator_rejects_leftover_active_change(tmp_path: Path):
433
861
  validator_path = (
434
- get_tasks_dir() / "comet-native-clarification" / "validation" / "test_native_clarification.py"
862
+ get_tasks_dir()
863
+ / "comet-native-clarification"
864
+ / "validation"
865
+ / "test_native_clarification.py"
435
866
  )
436
867
  spec = importlib.util.spec_from_file_location("native_clarification_validator", validator_path)
437
868
  assert spec and spec.loader
@@ -474,7 +905,9 @@ def test_native_interrupted_transition_fixture_is_recovered_by_current_runtime(t
474
905
  task = load_task("comet-native-interrupted-transition")
475
906
  workspace = tmp_path / "workspace"
476
907
  shutil.copytree(task.environment_dir, workspace)
477
- runtime = get_tasks_dir().parents[2] / "assets/skills/comet-native/scripts/comet-native-runtime.mjs"
908
+ runtime = (
909
+ get_tasks_dir().parents[2] / "assets/skills/comet-native/scripts/comet-native-runtime.mjs"
910
+ )
478
911
  change = workspace / "docs/comet/changes/add-character-counting"
479
912
 
480
913
  def run_native(*args: str, expected_exit: int = 0) -> dict:
@@ -538,7 +971,6 @@ def test_native_interrupted_transition_fixture_is_recovered_by_current_runtime(t
538
971
  event
539
972
  for event in events
540
973
  if event.get("type") == "state_transitioned"
541
- and event.get("data", {}).get("transitionId")
542
- == "11111111-2222-4333-8444-555555555555"
974
+ and event.get("data", {}).get("transitionId") == "11111111-2222-4333-8444-555555555555"
543
975
  ]
544
976
  assert len(recovered) == 1
@@ -232,6 +232,10 @@ def test_claude_loop_applies_plugin_args_to_subject_turns_only():
232
232
  assert 'claude -p "$USER_REPLY" "${PLUGIN_ARGS[@]}"' in loop_sh
233
233
  assert "fresh resume boundary detected" in loop_sh
234
234
  assert 'claude -p "$sim_prompt" "${PLUGIN_ARGS[@]}"' not in loop_sh
235
+ assert 'if ! rm -f -- "$SIMULATOR_PROMPT_FILE"; then' in loop_sh
236
+ assert loop_sh.index('rm -f -- "$SIMULATOR_PROMPT_FILE"') < loop_sh.index(
237
+ 'RAW=$(claude -p "$SUBJECT_PROMPT"'
238
+ )
235
239
 
236
240
 
237
241
  def test_claude_loop_surfaces_subject_resume_failure(tmp_path: Path):
@@ -284,6 +288,113 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"session-1","re
284
288
  assert "subject turn 2 failed" in result.stderr
285
289
 
286
290
 
291
+ def test_claude_loop_consumes_deterministic_reply_steps_in_order(tmp_path: Path):
292
+ fake_bin = tmp_path / "bin"
293
+ fake_bin.mkdir()
294
+ fake_claude = fake_bin / "claude"
295
+ fake_claude.write_text(
296
+ """#!/usr/bin/env bash
297
+ prompt=""
298
+ while [[ $# -gt 0 ]]; do
299
+ if [[ "$1" == "-p" ]]; then
300
+ prompt="$2"
301
+ break
302
+ fi
303
+ shift
304
+ done
305
+ printf '%s\n' '{"type":"system","session_id":"session-1"}'
306
+ case "$prompt" in
307
+ "First reply.")
308
+ printf '%s\n' '{"type":"result","subtype":"success","session_id":"session-1","result":"Question: Which second choice should be used? Recommendation: B. Impact: changes output."}'
309
+ ;;
310
+ "Second reply.")
311
+ printf '%s\n' '{"type":"result","subtype":"success","session_id":"session-1","result":"Workflow completed through all phases and archived."}'
312
+ ;;
313
+ *)
314
+ printf '%s\n' '{"type":"result","subtype":"success","session_id":"session-1","result":"Question: Which first choice should be used? Recommendation: A. Impact: changes output."}'
315
+ ;;
316
+ esac
317
+ """,
318
+ encoding="utf-8",
319
+ newline="\n",
320
+ )
321
+ fake_claude.chmod(0o755)
322
+
323
+ env = os.environ.copy()
324
+ env["PATH"] = f"{utils._to_bash_path(fake_bin)}:{env.get('PATH', '')}"
325
+ result = subprocess.run(
326
+ [
327
+ utils.BASH_EXEC,
328
+ utils._to_bash_path(utils.SHELL_DIR / "run-claude-loop.sh"),
329
+ "Implement the requested change.",
330
+ "--max-turns",
331
+ "3",
332
+ "--decision-reply-step",
333
+ "First reply.",
334
+ "--decision-reply-step",
335
+ "Second reply.",
336
+ ],
337
+ capture_output=True,
338
+ text=True,
339
+ encoding="utf-8",
340
+ errors="replace",
341
+ timeout=10,
342
+ check=False,
343
+ env=env,
344
+ )
345
+
346
+ assert result.returncode == 0, result.stderr
347
+ assert result.stderr.count("deterministic decision reply applied") == 2
348
+ assert "workflow completion detected" in result.stderr
349
+
350
+
351
+ def test_claude_loop_removes_private_simulator_prompt_before_subject_run(tmp_path: Path):
352
+ fake_bin = tmp_path / "bin"
353
+ fake_bin.mkdir()
354
+ fake_claude = fake_bin / "claude"
355
+ fake_claude.write_text(
356
+ """#!/usr/bin/env bash
357
+ if [[ -e "$SIMULATOR_LEAK_PATH" ]]; then
358
+ echo "private simulator prompt leaked to subject" >&2
359
+ exit 43
360
+ fi
361
+ printf '%s\n' '{"type":"system","session_id":"session-1"}'
362
+ printf '%s\n' '{"type":"result","subtype":"success","session_id":"session-1","result":"Workflow completed through all phases and archived."}'
363
+ """,
364
+ encoding="utf-8",
365
+ newline="\n",
366
+ )
367
+ fake_claude.chmod(0o755)
368
+ simulator_prompt = tmp_path / ".eval-simulator-prompt.txt"
369
+ simulator_prompt.write_text("private fixed decisions", encoding="utf-8")
370
+
371
+ env = os.environ.copy()
372
+ env["PATH"] = f"{utils._to_bash_path(fake_bin)}:{env.get('PATH', '')}"
373
+ env["SIMULATOR_LEAK_PATH"] = utils._to_bash_path(simulator_prompt)
374
+ result = subprocess.run(
375
+ [
376
+ utils.BASH_EXEC,
377
+ utils._to_bash_path(utils.SHELL_DIR / "run-claude-loop.sh"),
378
+ "Implement the requested change.",
379
+ "--max-turns",
380
+ "1",
381
+ "--simulator-prompt-file",
382
+ utils._to_bash_path(simulator_prompt),
383
+ ],
384
+ capture_output=True,
385
+ text=True,
386
+ encoding="utf-8",
387
+ errors="replace",
388
+ timeout=10,
389
+ check=False,
390
+ env=env,
391
+ )
392
+
393
+ assert result.returncode == 0, result.stderr
394
+ assert "private simulator prompt leaked" not in result.stderr
395
+ assert not simulator_prompt.exists()
396
+
397
+
287
398
  def test_decision_point_detector_rejects_completion_statements():
288
399
  statement = "Implementation is complete and the artifacts provide the requested evidence."
289
400
  punctuation_summary = "Done. The counter recognizes ., !, and ? terminators."
@@ -373,7 +484,9 @@ def test_completion_point_detector_requires_explicit_non_negated_workflow_comple
373
484
  "completion-point.sh", "Change add-counting completed through Archive.", check=False
374
485
  )
375
486
  archived_to = utils.run_shell(
376
- "completion-point.sh", "- **Archived to**: docs/comet/archive/2026-07-19-add-counting/", check=False
487
+ "completion-point.sh",
488
+ "- **Archived to**: docs/comet/archive/2026-07-19-add-counting/",
489
+ check=False,
377
490
  )
378
491
  completed_all_phases = utils.run_shell(
379
492
  "completion-point.sh",
@@ -388,6 +388,7 @@ def test_task_treatment(task_name, treatment_name):
388
388
 
389
389
  events = extract_events(parse_output(result.stdout))
390
390
  loop_interaction = conftest._extract_loop_interaction(result.stderr)
391
+ subject_turns = conftest._extract_subject_turn_evidence(result.stdout)
391
392
  outputs = {
392
393
  "run_id": run_id,
393
394
  "treatment_name": treatment_name,
@@ -418,6 +419,7 @@ def test_task_treatment(task_name, treatment_name):
418
419
  "mode": interaction.mode,
419
420
  "max_turns": interaction.max_turns,
420
421
  **loop_interaction,
422
+ "subject_turns": subject_turns,
421
423
  },
422
424
  "case_manifest": case_manifest,
423
425
  }
@@ -269,6 +269,7 @@ def build_execution_identity(
269
269
  "simulator_prompt": getattr(interaction, "simulator_prompt", None),
270
270
  "decision_patterns": list(getattr(interaction, "decision_patterns", ()) or ()),
271
271
  "decision_reply": getattr(interaction, "decision_reply", None),
272
+ "decision_replies": list(getattr(interaction, "decision_replies", []) or []),
272
273
  "continue_prompt": getattr(interaction, "continue_prompt", None),
273
274
  "fresh_resume_marker": getattr(interaction, "fresh_resume_marker", None),
274
275
  }