@tencent-rtc/trtc-agent-skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/README.zh.md +173 -0
  4. package/bin/cli.js +434 -0
  5. package/knowledge-base/index.yaml +454 -0
  6. package/knowledge-base/platform-slice-template.md +233 -0
  7. package/knowledge-base/scenario-spec.md +350 -0
  8. package/knowledge-base/scenarios/conference/base/general-conference.md +365 -0
  9. package/knowledge-base/scenarios/conference/base/webinar-conference.md +130 -0
  10. package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +145 -0
  11. package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +113 -0
  12. package/knowledge-base/scenarios/live/entertainment-live-room.md +118 -0
  13. package/knowledge-base/slice-spec.md +546 -0
  14. package/knowledge-base/slices/conference/web/ai-tools.md +225 -0
  15. package/knowledge-base/slices/conference/web/beauty-effects.md +188 -0
  16. package/knowledge-base/slices/conference/web/device-control.md +338 -0
  17. package/knowledge-base/slices/conference/web/login-auth.md +261 -0
  18. package/knowledge-base/slices/conference/web/network-quality.md +190 -0
  19. package/knowledge-base/slices/conference/web/official-roomkit-api.md +298 -0
  20. package/knowledge-base/slices/conference/web/official-roomkit-login-ui.md +246 -0
  21. package/knowledge-base/slices/conference/web/participant-list.md +238 -0
  22. package/knowledge-base/slices/conference/web/participant-management.md +718 -0
  23. package/knowledge-base/slices/conference/web/prejoin-check.md +293 -0
  24. package/knowledge-base/slices/conference/web/room-call.md +213 -0
  25. package/knowledge-base/slices/conference/web/room-chat.md +426 -0
  26. package/knowledge-base/slices/conference/web/room-lifecycle.md +534 -0
  27. package/knowledge-base/slices/conference/web/room-schedule.md +281 -0
  28. package/knowledge-base/slices/conference/web/screen-share.md +211 -0
  29. package/knowledge-base/slices/conference/web/video-layout.md +675 -0
  30. package/knowledge-base/slices/conference/web/virtual-background.md +197 -0
  31. package/knowledge-base/slices/conference/web/webinar-interaction.md +206 -0
  32. package/knowledge-base/slices/live/anchor-lifecycle.md +122 -0
  33. package/knowledge-base/slices/live/anchor-preview.md +90 -0
  34. package/knowledge-base/slices/live/anchor-room-config.md +104 -0
  35. package/knowledge-base/slices/live/audience-list.md +86 -0
  36. package/knowledge-base/slices/live/audience-manage.md +92 -0
  37. package/knowledge-base/slices/live/audience-watch.md +85 -0
  38. package/knowledge-base/slices/live/audio.md +116 -0
  39. package/knowledge-base/slices/live/barrage.md +88 -0
  40. package/knowledge-base/slices/live/beauty.md +99 -0
  41. package/knowledge-base/slices/live/coguest-apply.md +105 -0
  42. package/knowledge-base/slices/live/device-control.md +91 -0
  43. package/knowledge-base/slices/live/error-codes.md +167 -0
  44. package/knowledge-base/slices/live/gift.md +84 -0
  45. package/knowledge-base/slices/live/ios/.gitkeep +0 -0
  46. package/knowledge-base/slices/live/ios/anchor-lifecycle.md +313 -0
  47. package/knowledge-base/slices/live/ios/anchor-preview.md +228 -0
  48. package/knowledge-base/slices/live/ios/anchor-room-config.md +257 -0
  49. package/knowledge-base/slices/live/ios/audience-list.md +353 -0
  50. package/knowledge-base/slices/live/ios/audience-manage.md +381 -0
  51. package/knowledge-base/slices/live/ios/audience-watch.md +286 -0
  52. package/knowledge-base/slices/live/ios/audio.md +373 -0
  53. package/knowledge-base/slices/live/ios/barrage.md +285 -0
  54. package/knowledge-base/slices/live/ios/beauty.md +323 -0
  55. package/knowledge-base/slices/live/ios/coguest-apply.md +506 -0
  56. package/knowledge-base/slices/live/ios/device-control.md +286 -0
  57. package/knowledge-base/slices/live/ios/error-codes.md +270 -0
  58. package/knowledge-base/slices/live/ios/gift.md +315 -0
  59. package/knowledge-base/slices/live/ios/live-list.md +269 -0
  60. package/knowledge-base/slices/live/ios/login-auth.md +247 -0
  61. package/knowledge-base/slices/live/live-list.md +82 -0
  62. package/knowledge-base/slices/live/login-auth.md +78 -0
  63. package/package.json +34 -0
  64. package/skills/trtc/SKILL.md +326 -0
  65. package/skills/trtc/room-builder/SKILL.md +138 -0
  66. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +108 -0
  67. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/backend-contract.zh-CN.md +162 -0
  68. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/integration.zh-CN.md +154 -0
  69. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/theme.zh-CN.md +78 -0
  70. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/index.html +12 -0
  71. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/package.json +28 -0
  72. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/postcss.config.js +5 -0
  73. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/App.vue +25 -0
  74. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/ConsultationManagePanel.vue +838 -0
  75. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LanguageSwitch.vue +102 -0
  76. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LoadingSpinner.vue +6 -0
  77. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalAlert.vue +34 -0
  78. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalBusinessPanel.vue +148 -0
  79. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalButton.vue +49 -0
  80. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalConfirmDialog.vue +68 -0
  81. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalDataPanel.vue +196 -0
  82. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalRecordPanel.vue +270 -0
  83. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/PrescriptionPanel.vue +363 -0
  84. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/basic-info-config.ts +29 -0
  85. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
  86. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
  87. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/runtime-config.ts +12 -0
  88. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/env.d.ts +32 -0
  89. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +123 -0
  90. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +230 -0
  91. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +135 -0
  92. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +113 -0
  93. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +132 -0
  94. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +50 -0
  95. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/types.ts +77 -0
  96. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationChat.ts +97 -0
  97. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationDevices.ts +48 -0
  98. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationParticipants.ts +121 -0
  99. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationPermissions.ts +25 -0
  100. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/utils.ts +70 -0
  101. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/en-US/index.ts +553 -0
  102. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/index.ts +25 -0
  103. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/medicalTranslate.ts +85 -0
  104. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/state.ts +49 -0
  105. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/zh-CN/index.ts +463 -0
  106. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/main.ts +12 -0
  107. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/appointments.ts +96 -0
  108. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/users.ts +79 -0
  109. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/router/index.ts +63 -0
  110. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/index.ts +25 -0
  111. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/appointmentService.ts +77 -0
  112. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/authService.ts +38 -0
  113. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/launchContext.ts +31 -0
  114. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/userService.ts +35 -0
  115. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/appointmentService.ts +43 -0
  116. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/authService.ts +33 -0
  117. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/userService.ts +43 -0
  118. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/types.ts +135 -0
  119. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/shared/icons.ts +53 -0
  120. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/index.css +106 -0
  121. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/tailwind.css +3 -0
  122. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/theme.css +209 -0
  123. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/auth.ts +50 -0
  124. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/format.ts +24 -0
  125. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/navigation.ts +12 -0
  126. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/session.ts +28 -0
  127. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorConsultationView.vue +777 -0
  128. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorDashboardView.vue +678 -0
  129. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/LoginView.vue +441 -0
  130. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationFinishedView.vue +185 -0
  131. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationView.vue +1003 -0
  132. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientSelectDoctorView.vue +317 -0
  133. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientWaitingView.vue +454 -0
  134. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.json +21 -0
  135. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.node.json +8 -0
  136. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/vite.config.ts +17 -0
  137. package/skills/trtc/room-builder/templates/scenarios/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md +6 -0
  138. package/skills/trtc/room-builder/tools/render_ai_instructions.py +226 -0
  139. package/skills/trtc-apply/SKILL.md +97 -0
  140. package/skills/trtc-apply/guardrails/apply_lib/__init__.py +0 -0
  141. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  142. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
  143. package/skills/trtc-apply/guardrails/apply_lib/rule_parser.py +268 -0
  144. package/skills/trtc-docs/SKILL.md +207 -0
  145. package/skills/trtc-onboarding/SKILL.md +839 -0
  146. package/skills/trtc-onboarding/reference/path-a1-demo.md +103 -0
  147. package/skills/trtc-onboarding/reference/path-a2-integrate.md +693 -0
  148. package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +115 -0
  149. package/skills/trtc-onboarding/reference/path-c-expand.md +43 -0
  150. package/skills/trtc-onboarding/reference/reporting-protocol.md +174 -0
  151. package/skills/trtc-onboarding/reference/supported-matrix.md +100 -0
  152. package/skills/trtc-onboarding/reference/usersig-handling.md +140 -0
  153. package/skills/trtc-search/SKILL.md +221 -0
  154. package/skills/trtc-topic/SKILL.md +638 -0
  155. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
  156. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
  157. package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
  158. package/skills/trtc-topic/guardrails/gate_slice_read.py +133 -0
  159. package/skills/trtc-topic/guardrails/gate_slice_write.py +169 -0
  160. package/skills/trtc-topic/guardrails/stop_require_apply_evidence.py +97 -0
  161. package/skills/trtc-topic/references/execution-units.yaml +58 -0
  162. package/skills/trtc-topic/runtime/README.md +50 -0
  163. package/skills/trtc-topic/runtime/RUNTIME.md +128 -0
  164. package/skills/trtc-topic/runtime/lib/__init__.py +0 -0
  165. package/skills/trtc-topic/runtime/lib/platforms.py +194 -0
  166. package/skills/trtc-topic/runtime/package-lock.json +1211 -0
  167. package/skills/trtc-topic/runtime/package.json +13 -0
  168. package/skills/trtc-topic/runtime/telemetry-bridge.mjs +339 -0
  169. package/skills/trtc-topic/runtime/telemetry_collector.py +293 -0
  170. package/skills/trtc-topic/scripts/STATE-MACHINE-GUIDE.md +186 -0
  171. package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
  172. package/skills/trtc-topic/scripts/apply.py +581 -0
  173. package/skills/trtc-topic/scripts/finalize_session.py +113 -0
  174. package/skills/trtc-topic/scripts/init_slice_queue.py +96 -0
  175. package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
  176. package/skills/trtc-topic/scripts/lib/state_machine.py +328 -0
  177. package/skills/trtc-topic/scripts/next_slice.py +137 -0
  178. package/skills/trtc-topic/tests/README.md +70 -0
  179. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
  180. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  181. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
  182. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  183. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
  184. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
  185. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
  186. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
  187. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
  188. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
  189. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
  190. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
  191. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
  192. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
  193. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
  194. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
  195. package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
  196. package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.3.pyc +0 -0
  197. package/skills/trtc-topic/tests/conftest.py +72 -0
  198. package/skills/trtc-topic/tests/test_apply_cli.py +480 -0
  199. package/skills/trtc-topic/tests/test_end_to_end.py +305 -0
  200. package/skills/trtc-topic/tests/test_finalize_session.py +51 -0
  201. package/skills/trtc-topic/tests/test_gates.py +316 -0
  202. package/skills/trtc-topic/tests/test_session_resolver.py +260 -0
  203. package/skills/trtc-topic/tests/test_state_machine.py +414 -0
  204. package/skills/trtc-topic/tests/test_stop_require_apply.py +99 -0
  205. package/skills/trtc-topic/tests/test_topic_skill_invariants.py +130 -0
@@ -0,0 +1,305 @@
1
+ """End-to-end test: run the full slice loop through every component.
2
+
3
+ Simulates one full slice's lifecycle by exercising the actual CLI scripts
4
+ and gate scripts, not the in-process state_machine module. This catches
5
+ regressions where the moving parts agree on the field shape in unit tests
6
+ but disagree in integration.
7
+
8
+ Flow:
9
+
10
+ init_slice_queue -> next_slice status (cursor at execution step 0)
11
+ → gate_slice_read (current slice OK, next slice BLOCKED)
12
+ → next_slice advance mark_slice_read
13
+ → gate_slice_write (project file ALLOWED)
14
+ → write code
15
+ → next_slice advance mark_code_written
16
+ → stop hook (BLOCKED — apply hasn't run)
17
+ → apply.py (PASS → state = apply_passed)
18
+ → stop hook (ALLOWED)
19
+ → gate_slice_write (BLOCKED — must wait for confirm)
20
+ → next_slice advance mark_user_confirmed
21
+ → next_slice status (cursor at slice 1)
22
+ → gate_slice_read (slice 1 OK, slice 0 BLOCKED)
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import subprocess
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ import pytest
32
+ import yaml
33
+
34
+ ROOT = Path(__file__).resolve().parents[1]
35
+ INIT = ROOT / "scripts" / "init_slice_queue.py"
36
+ NEXT = ROOT / "scripts" / "next_slice.py"
37
+ APPLY = ROOT / "scripts" / "apply.py"
38
+ GATE_READ = ROOT / "guardrails" / "gate_slice_read.py"
39
+ GATE_WRITE = ROOT / "guardrails" / "gate_slice_write.py"
40
+ STOP = ROOT / "guardrails" / "stop_require_apply_evidence.py"
41
+
42
+
43
+ def _run_cmd(*args, **kwargs) -> subprocess.CompletedProcess:
44
+ return subprocess.run(
45
+ [sys.executable, *map(str, args)],
46
+ text=True,
47
+ capture_output=True,
48
+ **kwargs,
49
+ )
50
+
51
+
52
+ def _gate(script: Path, payload: dict, session_path: Path) -> subprocess.CompletedProcess:
53
+ return subprocess.run(
54
+ [sys.executable, str(script)],
55
+ input=json.dumps(payload),
56
+ text=True,
57
+ capture_output=True,
58
+ env={"TRTC_SESSION_PATH": str(session_path), "PATH": "/usr/bin:/bin"},
59
+ )
60
+
61
+
62
+ def _stop(session_path: Path) -> subprocess.CompletedProcess:
63
+ return subprocess.run(
64
+ [sys.executable, str(STOP)],
65
+ text=True,
66
+ capture_output=True,
67
+ env={"TRTC_SESSION_PATH": str(session_path), "PATH": "/usr/bin:/bin"},
68
+ )
69
+
70
+
71
+ # Same VUE we used in test_apply_cli.py — wires up the login-auth entry
72
+ # (useLoginState), so the entry-symbol gate passes.
73
+ _PASSING_LOGIN_VUE = '''<template><div /></template>
74
+ <script setup lang="ts">
75
+ import { useLoginState, LoginEvent } from "@trtc/tuikit-atomicx-vue3";
76
+ const { login, setSelfInfo, subscribeEvent } = useLoginState();
77
+ await login({ sdkAppId: 0, userId: "u", userSig: "x", scene: 5001 });
78
+ setSelfInfo({ nickName: "Alice" });
79
+ subscribeEvent(LoginEvent.onLoginExpired, () => {});
80
+ subscribeEvent(LoginEvent.onKickedOffline, () => {});
81
+ </script>
82
+ '''
83
+
84
+
85
+ def test_full_slice_loop_through_real_scripts(session_factory, project_factory):
86
+ session = session_factory(
87
+ confirmed_plan=["conference/login-auth", "conference/room-lifecycle"]
88
+ )
89
+ proj = project_factory()
90
+ target = proj / "src" / "views" / "MeetingRoom.vue"
91
+
92
+ # 1. init queue.
93
+ r = _run_cmd(INIT, "--session", session)
94
+ assert r.returncode == 0, r.stderr
95
+ data = yaml.safe_load(session.read_text())
96
+ assert data["current_execution_index"] == 0
97
+ assert data["current_execution_state"] == "not_started"
98
+
99
+ # 2. status.
100
+ r = _run_cmd(NEXT, "--session", session, "--json", "status")
101
+ assert r.returncode == 0
102
+ st = json.loads(r.stdout)
103
+ assert st["current_slice_id"] == "conference/login-auth"
104
+ assert st["state"] == "not_started"
105
+
106
+ # 3. read gate: current slice OK.
107
+ r = _gate(GATE_READ, {
108
+ "tool_name": "Read",
109
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/login-auth.md"},
110
+ }, session)
111
+ assert r.returncode == 0, r.stderr
112
+
113
+ # 3b. read gate: next slice BLOCKED.
114
+ r = _gate(GATE_READ, {
115
+ "tool_name": "Read",
116
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
117
+ }, session)
118
+ assert r.returncode == 2
119
+ assert "current slice" in r.stderr.lower() or "login-auth" in r.stderr
120
+
121
+ # 4. write gate: not_started → BLOCKED for project files.
122
+ r = _gate(GATE_WRITE, {
123
+ "tool_name": "Write",
124
+ "tool_input": {"file_path": str(target)},
125
+ }, session)
126
+ assert r.returncode == 2
127
+
128
+ # 5. advance mark_slice_read.
129
+ r = _run_cmd(NEXT, "--session", session, "advance", "mark_slice_read")
130
+ assert r.returncode == 0, r.stderr
131
+
132
+ # 6. write gate: now ALLOWED.
133
+ r = _gate(GATE_WRITE, {
134
+ "tool_name": "Write",
135
+ "tool_input": {"file_path": str(target)},
136
+ }, session)
137
+ assert r.returncode == 0, r.stderr
138
+
139
+ # Actually write the file.
140
+ target.parent.mkdir(parents=True, exist_ok=True)
141
+ target.write_text(_PASSING_LOGIN_VUE)
142
+
143
+ # 7. advance mark_code_written.
144
+ r = _run_cmd(NEXT, "--session", session, "advance", "mark_code_written")
145
+ assert r.returncode == 0, r.stderr
146
+
147
+ # 8. stop hook: BLOCKED, apply hasn't run.
148
+ r = _stop(session)
149
+ assert r.returncode == 2
150
+ assert "apply" in r.stderr.lower()
151
+
152
+ # 9. apply.py: pass.
153
+ r = _run_cmd(APPLY, "--slice", "conference/login-auth", "--session", session, "--project", proj)
154
+ assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}"
155
+
156
+ # State must be apply_passed.
157
+ data = yaml.safe_load(session.read_text())
158
+ assert data["current_execution_state"] == "apply_passed"
159
+
160
+ # 10. stop hook: ALLOWED now.
161
+ r = _stop(session)
162
+ assert r.returncode == 0
163
+
164
+ # 11. write gate: apply_passed → BLOCKED (must confirm first).
165
+ r = _gate(GATE_WRITE, {
166
+ "tool_name": "Write",
167
+ "tool_input": {"file_path": str(target)},
168
+ }, session)
169
+ assert r.returncode == 2
170
+ assert "apply_passed" in r.stderr or "confirm" in r.stderr.lower()
171
+
172
+ # 11b. evidence file is present BEFORE confirmation (apply.py wrote it).
173
+ ev_dir = session.parent / ".trtc-apply-evidence"
174
+ ev_files = list(ev_dir.glob("*.json"))
175
+ assert len(ev_files) == 1
176
+ ev = json.loads(ev_files[0].read_text())
177
+ assert ev["status"] == "pass"
178
+ assert ev["slice_id"] == "conference/login-auth"
179
+
180
+ # 12. user confirms.
181
+ r = _run_cmd(NEXT, "--session", session, "advance", "mark_user_confirmed")
182
+ assert r.returncode == 0, r.stderr
183
+
184
+ # 13. status: cursor advanced to slice 1.
185
+ r = _run_cmd(NEXT, "--session", session, "--json", "status")
186
+ st = json.loads(r.stdout)
187
+ assert st["current_slice_id"] == "conference/room-lifecycle"
188
+ assert st["state"] == "not_started"
189
+
190
+ # 14. read gate: slice 1 now OK, slice 0 now BLOCKED.
191
+ r = _gate(GATE_READ, {
192
+ "tool_name": "Read",
193
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
194
+ }, session)
195
+ assert r.returncode == 0, r.stderr
196
+
197
+ r = _gate(GATE_READ, {
198
+ "tool_name": "Read",
199
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/login-auth.md"},
200
+ }, session)
201
+ assert r.returncode == 2
202
+
203
+ # 15. evidence file was cleaned up by mark_user_confirmed.
204
+ ev_files = list((session.parent / ".trtc-apply-evidence").glob("*.json"))
205
+ assert ev_files == [], (
206
+ "evidence directory should be empty after a clean confirm; "
207
+ f"found {[f.name for f in ev_files]}"
208
+ )
209
+
210
+
211
+ def test_apply_failure_path(session_factory, project_factory):
212
+ """Code that never wires up the slice entry → apply_failed → Stop blocked → Edit allowed."""
213
+ session = session_factory(confirmed_plan=["conference/login-auth"])
214
+ proj = project_factory()
215
+ target = proj / "src" / "views" / "MeetingRoom.vue"
216
+
217
+ _run_cmd(INIT, "--session", session)
218
+ _run_cmd(NEXT, "--session", session, "advance", "mark_slice_read")
219
+
220
+ # Write real code that never references the login-auth entry (useLoginState).
221
+ target.parent.mkdir(parents=True, exist_ok=True)
222
+ target.write_text(
223
+ '<template><div /></template>\n<script setup lang="ts">\n'
224
+ 'import { ref } from "vue";\nconst ready = ref(true);\n</script>'
225
+ )
226
+
227
+ _run_cmd(NEXT, "--session", session, "advance", "mark_code_written")
228
+
229
+ r = _run_cmd(APPLY, "--slice", "conference/login-auth", "--session", session, "--project", proj)
230
+ assert r.returncode == 1
231
+ data = yaml.safe_load(session.read_text())
232
+ assert data["current_execution_state"] == "apply_failed"
233
+
234
+ # Stop blocked.
235
+ r = _stop(session)
236
+ assert r.returncode == 2
237
+
238
+ # Edit gate ALLOWED in apply_failed (V1 walk-back: no slice-reread gate).
239
+ r = _gate(GATE_WRITE, {
240
+ "tool_name": "Edit",
241
+ "tool_input": {"file_path": str(target)},
242
+ }, session)
243
+ assert r.returncode == 0
244
+
245
+
246
+ def test_auto_advance_loop_skips_user_confirm(session_factory, project_factory):
247
+ """With pause_on_failure, apply pass auto-advances cursor to the next slice.
248
+
249
+ The AI never has to call mark_user_confirmed for clean slices — apply.py
250
+ does it. The state machine, gates and evidence file all still operate.
251
+ """
252
+ session = session_factory(
253
+ confirmed_plan=["conference/login-auth", "conference/room-lifecycle"]
254
+ )
255
+ proj = project_factory()
256
+ target = proj / "src" / "views" / "MeetingRoom.vue"
257
+ target.parent.mkdir(parents=True, exist_ok=True)
258
+ target.write_text(_PASSING_LOGIN_VUE)
259
+
260
+ # Set policy on the session.
261
+ data = yaml.safe_load(session.read_text())
262
+ data["auto_advance_policy"] = "pause_on_failure"
263
+ session.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True))
264
+
265
+ # init + read + code_written + apply (pass).
266
+ assert _run_cmd(INIT, "--session", session).returncode == 0
267
+ assert _run_cmd(NEXT, "--session", session, "advance", "mark_slice_read").returncode == 0
268
+ assert _run_cmd(NEXT, "--session", session, "advance", "mark_code_written").returncode == 0
269
+
270
+ r = _run_cmd(APPLY, "--slice", "conference/login-auth", "--session", session, "--project", proj)
271
+ assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}"
272
+ assert "auto-advanced" in r.stdout
273
+
274
+ # Cursor should be on slice 1, not_started — no user confirm needed.
275
+ data = yaml.safe_load(session.read_text())
276
+ assert data["current_execution_index"] == 1
277
+ assert data["current_execution_state"] == "not_started"
278
+
279
+ # Read gate now allows slice 1's file (and blocks slice 0's).
280
+ r = _gate(GATE_READ, {
281
+ "tool_name": "Read",
282
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
283
+ }, session)
284
+ assert r.returncode == 0
285
+
286
+ r = _gate(GATE_READ, {
287
+ "tool_name": "Read",
288
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/login-auth.md"},
289
+ }, session)
290
+ assert r.returncode == 2
291
+
292
+ # Stop hook: in not_started → allowed.
293
+ assert _stop(session).returncode == 0
294
+
295
+ # Slice 1: write code that never wires up the room-lifecycle entry
296
+ # (useRoomState); apply must still pause on failure even with auto-advance.
297
+ target.write_text("<template><div /></template>\n<script setup>\n// no room APIs\n</script>")
298
+ assert _run_cmd(NEXT, "--session", session, "advance", "mark_slice_read").returncode == 0
299
+ assert _run_cmd(NEXT, "--session", session, "advance", "mark_code_written").returncode == 0
300
+ r = _run_cmd(APPLY, "--slice", "conference/room-lifecycle", "--session", session, "--project", proj)
301
+ assert r.returncode == 1
302
+ data = yaml.safe_load(session.read_text())
303
+ assert data["current_execution_state"] == "apply_failed"
304
+ # Stop is still blocked on apply_failed — the safety net survives.
305
+ assert _stop(session).returncode == 2
@@ -0,0 +1,51 @@
1
+ """Tests for finalize_session.py session normalization."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import yaml
9
+
10
+
11
+ FINALIZE_SCRIPT = (
12
+ Path(__file__).resolve().parents[1] / "scripts" / "finalize_session.py"
13
+ )
14
+
15
+
16
+ def test_finalize_session_normalizes_completed_topic_session(session_factory):
17
+ path = session_factory(
18
+ status="active",
19
+ current_step="topic-handoff",
20
+ completed_steps=[
21
+ "conference/login-auth",
22
+ "conference/room-lifecycle",
23
+ "conference/room-lifecycle",
24
+ ],
25
+ execution_queue=[
26
+ {"id": "conference/login-auth", "type": "slice", "slices": ["conference/login-auth"], "status": "done"},
27
+ {"id": "foundation", "type": "unit", "slices": ["conference/room-lifecycle", "conference/video-layout"], "status": "pending"},
28
+ ],
29
+ current_execution_index=2,
30
+ current_execution_state="all_done",
31
+ )
32
+
33
+ result = subprocess.run(
34
+ [sys.executable, str(FINALIZE_SCRIPT), "--session", str(path)],
35
+ text=True,
36
+ capture_output=True,
37
+ )
38
+
39
+ assert result.returncode == 0, result.stderr
40
+ data = yaml.safe_load(path.read_text())
41
+ assert data["status"] == "completed"
42
+ assert data["current_step"] == "completed"
43
+ assert data["current_execution_index"] == 2
44
+ assert data["current_execution_state"] == "all_done"
45
+ assert data["completed_steps"] == [
46
+ "conference/login-auth",
47
+ "conference/room-lifecycle",
48
+ "conference/video-layout",
49
+ ]
50
+ assert [entry["status"] for entry in data["execution_queue"]] == ["done", "done"]
51
+ assert "last_recap" in data
@@ -0,0 +1,316 @@
1
+ """Tests for the PreToolUse gates that enforce the slice loop.
2
+
3
+ Both gates read a JSON object on stdin (Claude Code's PreToolUse contract):
4
+ {
5
+ "tool_name": "Read" | "Write" | "Edit" | ...,
6
+ "tool_input": { "file_path": "...", ... }
7
+ }
8
+
9
+ They exit:
10
+ 0 — allow the tool call (out of scope, or in-bounds)
11
+ 2 — block the tool call; stderr explains why
12
+ other — treated as configuration error; AI sees stderr but Claude Code
13
+ does not block (we never produce these intentionally).
14
+
15
+ Out-of-scope cases that MUST exit 0:
16
+ - session file missing
17
+ - execution_queue not initialised (topic flow not active)
18
+ - tool_name doesn't match what the gate guards
19
+ - file_path falls outside the gate's domain
20
+ (slice-read gate: only guards knowledge-base/slices/*.md)
21
+ (slice-write gate: only guards user-project source files)
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import subprocess
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ import yaml
31
+
32
+ GUARDRAILS_DIR = Path(__file__).resolve().parents[1] / "guardrails"
33
+ GATE_READ = GUARDRAILS_DIR / "gate_slice_read.py"
34
+ GATE_WRITE = GUARDRAILS_DIR / "gate_slice_write.py"
35
+
36
+ # state_machine helper for advancing test sessions to a specific state
37
+ STATE_MACHINE_DIR = Path(__file__).resolve().parents[1] / "scripts" / "lib"
38
+ sys.path.insert(0, str(STATE_MACHINE_DIR))
39
+ import state_machine # noqa: E402
40
+ sys.path.pop(0)
41
+
42
+
43
+ def _run_gate(script: Path, payload: dict, session_path: Path) -> subprocess.CompletedProcess:
44
+ """Invoke a gate script with the given JSON stdin payload.
45
+
46
+ The session path is passed via TRTC_SESSION_PATH env var so the gate
47
+ doesn't have to walk the filesystem looking for one. Real hooks use the
48
+ project's repo root via $CLAUDE_PROJECT_DIR; tests use a tmp_path session.
49
+ """
50
+ return subprocess.run(
51
+ [sys.executable, str(script)],
52
+ input=json.dumps(payload),
53
+ text=True,
54
+ capture_output=True,
55
+ env={"TRTC_SESSION_PATH": str(session_path), "PATH": "/usr/bin:/bin"},
56
+ )
57
+
58
+
59
+ # ---------- gate_slice_read ------------------------------------------------
60
+
61
+ class TestGateSliceRead:
62
+ def test_allows_when_session_missing(self, tmp_path):
63
+ """No session file → not in topic flow → must allow."""
64
+ payload = {
65
+ "tool_name": "Read",
66
+ "tool_input": {"file_path": str(tmp_path / "knowledge-base/slices/conference/web/room-chat.md")},
67
+ }
68
+ result = _run_gate(GATE_READ, payload, tmp_path / "missing.yaml")
69
+ assert result.returncode == 0, result.stderr
70
+
71
+ def test_allows_when_queue_not_initialised(self, session_factory):
72
+ """Session exists but no execution_queue -> topic flow not active."""
73
+ path = session_factory()
74
+ payload = {
75
+ "tool_name": "Read",
76
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/room-chat.md"},
77
+ }
78
+ result = _run_gate(GATE_READ, payload, path)
79
+ assert result.returncode == 0, result.stderr
80
+
81
+ def test_allows_non_read_tool(self, session_factory):
82
+ """Gate only guards Read; Write should pass through."""
83
+ path = session_factory()
84
+ state_machine.init_queue(path)
85
+ payload = {
86
+ "tool_name": "Write",
87
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/room-chat.md"},
88
+ }
89
+ result = _run_gate(GATE_READ, payload, path)
90
+ assert result.returncode == 0
91
+
92
+ def test_allows_non_slice_file(self, session_factory):
93
+ """Reading a non-slice file (e.g., README) is none of the gate's business."""
94
+ path = session_factory()
95
+ state_machine.init_queue(path)
96
+ payload = {
97
+ "tool_name": "Read",
98
+ "tool_input": {"file_path": "README.md"},
99
+ }
100
+ result = _run_gate(GATE_READ, payload, path)
101
+ assert result.returncode == 0
102
+
103
+ def test_allows_current_slice_overview(self, session_factory):
104
+ """Product-level overview for the current slice is allowed."""
105
+ path = session_factory()
106
+ state_machine.init_queue(path)
107
+ payload = {
108
+ "tool_name": "Read",
109
+ "tool_input": {"file_path": "knowledge-base/slices/conference/login-auth.md"},
110
+ }
111
+ result = _run_gate(GATE_READ, payload, path)
112
+ assert result.returncode == 0, result.stderr
113
+
114
+ def test_allows_current_slice_platform_file(self, session_factory):
115
+ """Platform-specific file for the current slice is allowed."""
116
+ path = session_factory()
117
+ state_machine.init_queue(path)
118
+ payload = {
119
+ "tool_name": "Read",
120
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/login-auth.md"},
121
+ }
122
+ result = _run_gate(GATE_READ, payload, path)
123
+ assert result.returncode == 0, result.stderr
124
+
125
+ def test_blocks_next_slice_read(self, session_factory):
126
+ """Reading the SECOND slice while cursor is on the first → exit 2."""
127
+ path = session_factory()
128
+ state_machine.init_queue(path)
129
+ payload = {
130
+ "tool_name": "Read",
131
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
132
+ }
133
+ result = _run_gate(GATE_READ, payload, path)
134
+ assert result.returncode == 2
135
+ assert "current slice" in result.stderr.lower() or "login-auth" in result.stderr
136
+
137
+ def test_blocks_unrelated_slice(self, session_factory):
138
+ """Reading a slice not even in the queue → exit 2."""
139
+ path = session_factory()
140
+ state_machine.init_queue(path)
141
+ payload = {
142
+ "tool_name": "Read",
143
+ "tool_input": {"file_path": "knowledge-base/slices/live/coguest-apply.md"},
144
+ }
145
+ result = _run_gate(GATE_READ, payload, path)
146
+ assert result.returncode == 2
147
+
148
+ def test_unit_mode_allows_any_slice_in_current_unit(self, session_factory):
149
+ path = session_factory(
150
+ execution_granularity="unit",
151
+ confirmed_plan=[
152
+ "conference/login-auth",
153
+ "conference/room-lifecycle",
154
+ "conference/room-chat",
155
+ ],
156
+ )
157
+ state_machine.init_queue(path)
158
+ payload = {
159
+ "tool_name": "Read",
160
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
161
+ }
162
+ result = _run_gate(GATE_READ, payload, path)
163
+ assert result.returncode == 0, result.stderr
164
+
165
+ def test_unit_mode_blocks_slice_outside_current_unit(self, session_factory):
166
+ path = session_factory(
167
+ execution_granularity="unit",
168
+ confirmed_plan=[
169
+ "conference/login-auth",
170
+ "conference/room-lifecycle",
171
+ "conference/room-chat",
172
+ ],
173
+ )
174
+ state_machine.init_queue(path)
175
+ payload = {
176
+ "tool_name": "Read",
177
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/room-chat.md"},
178
+ }
179
+ result = _run_gate(GATE_READ, payload, path)
180
+ assert result.returncode == 2
181
+ assert "current unit" in result.stderr or "foundation" in result.stderr
182
+
183
+ def test_allows_absolute_path_to_current_slice(self, session_factory, tmp_path):
184
+ """Absolute paths must also be matched, not just relative ones."""
185
+ path = session_factory()
186
+ state_machine.init_queue(path)
187
+ abs_path = tmp_path / "anywhere/knowledge-base/slices/conference/web/login-auth.md"
188
+ payload = {"tool_name": "Read", "tool_input": {"file_path": str(abs_path)}}
189
+ result = _run_gate(GATE_READ, payload, path)
190
+ assert result.returncode == 0, result.stderr
191
+
192
+ def test_advances_to_next_slice_after_confirm(self, session_factory):
193
+ """After full cycle, the second slice should be readable."""
194
+ path = session_factory()
195
+ state_machine.init_queue(path)
196
+ for transition in [
197
+ "mark_slice_read",
198
+ "mark_code_written",
199
+ "mark_apply_passed",
200
+ "mark_user_confirmed",
201
+ ]:
202
+ state_machine.advance(path, transition)
203
+ # Now cursor is on conference/room-lifecycle.
204
+ payload = {
205
+ "tool_name": "Read",
206
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/room-lifecycle.md"},
207
+ }
208
+ result = _run_gate(GATE_READ, payload, path)
209
+ assert result.returncode == 0, result.stderr
210
+
211
+ # And login-auth (the previous slice) is now blocked, since we've moved on.
212
+ payload2 = {
213
+ "tool_name": "Read",
214
+ "tool_input": {"file_path": "knowledge-base/slices/conference/web/login-auth.md"},
215
+ }
216
+ result2 = _run_gate(GATE_READ, payload2, path)
217
+ assert result2.returncode == 2
218
+
219
+
220
+ # ---------- gate_slice_write ------------------------------------------------
221
+
222
+ class TestGateSliceWrite:
223
+ def _project_file(self, session_factory) -> tuple[Path, Path]:
224
+ """Return (session_path, file_path-inside-project-src)."""
225
+ session_path = session_factory()
226
+ data = yaml.safe_load(session_path.read_text())
227
+ proj_root = Path(data["project_state"]["project_root"])
228
+ proj_root.mkdir(parents=True, exist_ok=True)
229
+ return session_path, proj_root / "src" / "views" / "MeetingRoom.vue"
230
+
231
+ def test_allows_when_session_missing(self, tmp_path):
232
+ payload = {
233
+ "tool_name": "Write",
234
+ "tool_input": {"file_path": str(tmp_path / "user-project/src/foo.vue")},
235
+ }
236
+ result = _run_gate(GATE_WRITE, payload, tmp_path / "missing.yaml")
237
+ assert result.returncode == 0
238
+
239
+ def test_allows_when_queue_not_initialised(self, session_factory):
240
+ path, target = self._project_file(session_factory)
241
+ payload = {"tool_name": "Write", "tool_input": {"file_path": str(target)}}
242
+ result = _run_gate(GATE_WRITE, payload, path)
243
+ assert result.returncode == 0
244
+
245
+ def test_allows_non_write_tool(self, session_factory):
246
+ path, target = self._project_file(session_factory)
247
+ state_machine.init_queue(path)
248
+ payload = {"tool_name": "Read", "tool_input": {"file_path": str(target)}}
249
+ result = _run_gate(GATE_WRITE, payload, path)
250
+ assert result.returncode == 0
251
+
252
+ def test_allows_writes_outside_user_project(self, session_factory, tmp_path):
253
+ """Writing to .claude/, knowledge-base/, /tmp etc. is not the gate's domain."""
254
+ path, _target = self._project_file(session_factory)
255
+ state_machine.init_queue(path)
256
+ # current_execution_state == not_started would block a project write,
257
+ # but this write is OUTSIDE the project root.
258
+ outside = tmp_path / "elsewhere" / "scratch.md"
259
+ payload = {"tool_name": "Write", "tool_input": {"file_path": str(outside)}}
260
+ result = _run_gate(GATE_WRITE, payload, path)
261
+ assert result.returncode == 0, result.stderr
262
+
263
+ def test_blocks_write_when_state_not_started(self, session_factory):
264
+ """Cursor sits on not_started → AI hasn't read the slice yet → block."""
265
+ path, target = self._project_file(session_factory)
266
+ state_machine.init_queue(path)
267
+ payload = {"tool_name": "Write", "tool_input": {"file_path": str(target)}}
268
+ result = _run_gate(GATE_WRITE, payload, path)
269
+ assert result.returncode == 2
270
+ assert "not_started" in result.stderr or "read" in result.stderr.lower()
271
+
272
+ def test_allows_write_when_state_slice_read(self, session_factory):
273
+ path, target = self._project_file(session_factory)
274
+ state_machine.init_queue(path)
275
+ state_machine.advance(path, "mark_slice_read")
276
+ payload = {"tool_name": "Write", "tool_input": {"file_path": str(target)}}
277
+ result = _run_gate(GATE_WRITE, payload, path)
278
+ assert result.returncode == 0, result.stderr
279
+
280
+ def test_allows_edit_when_state_code_written(self, session_factory):
281
+ """Edit (during apply retry) is allowed in code_written state."""
282
+ path, target = self._project_file(session_factory)
283
+ state_machine.init_queue(path)
284
+ state_machine.advance(path, "mark_slice_read")
285
+ state_machine.advance(path, "mark_code_written")
286
+ payload = {"tool_name": "Edit", "tool_input": {"file_path": str(target)}}
287
+ result = _run_gate(GATE_WRITE, payload, path)
288
+ assert result.returncode == 0, result.stderr
289
+
290
+ def test_allows_edit_in_apply_failed(self, session_factory):
291
+ """apply_failed → Edit is ALLOWED so the AI can retry after a fail.
292
+
293
+ We deliberately do NOT enforce a "must re-read the slice first" rule
294
+ here — the V1 walk-back removed that conditional gate (see
295
+ docs/apply-skill-long-term-design.md §6.4 'walked back' list).
296
+ """
297
+ path, target = self._project_file(session_factory)
298
+ state_machine.init_queue(path)
299
+ state_machine.advance(path, "mark_slice_read")
300
+ state_machine.advance(path, "mark_code_written")
301
+ state_machine.advance(path, "mark_apply_failed")
302
+ payload = {"tool_name": "Edit", "tool_input": {"file_path": str(target)}}
303
+ result = _run_gate(GATE_WRITE, payload, path)
304
+ assert result.returncode == 0, result.stderr
305
+
306
+ def test_blocks_write_in_apply_passed(self, session_factory):
307
+ """apply_passed → AI must wait for user confirmation, not keep editing."""
308
+ path, target = self._project_file(session_factory)
309
+ state_machine.init_queue(path)
310
+ state_machine.advance(path, "mark_slice_read")
311
+ state_machine.advance(path, "mark_code_written")
312
+ state_machine.advance(path, "mark_apply_passed")
313
+ payload = {"tool_name": "Write", "tool_input": {"file_path": str(target)}}
314
+ result = _run_gate(GATE_WRITE, payload, path)
315
+ assert result.returncode == 2
316
+ assert "apply_passed" in result.stderr or "confirm" in result.stderr.lower()