@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,96 @@
1
+ #!/usr/bin/env python3
2
+ """init_slice_queue.py — Topic-skill CLI: materialise execution_queue from session.
3
+
4
+ Usage:
5
+ python3 init_slice_queue.py # auto-resolves session path
6
+ python3 init_slice_queue.py --session PATH # explicit override
7
+
8
+ Session path resolution (in order):
9
+ 1. --session flag, if given
10
+ 2. $TRTC_SESSION_PATH env var
11
+ 3. $CLAUDE_PROJECT_DIR/.trtc-session.yaml (Claude Code sets this to the
12
+ user project root)
13
+ 4. ./.trtc-session.yaml (cwd fallback — useful when AI runs Bash from
14
+ the user project root)
15
+
16
+ Reads ``confirmed_plan`` from the session and writes:
17
+ execution_queue, current_execution_index=0, current_execution_state=not_started
18
+
19
+ Idempotent. Refuses to re-init if the plan has changed (queue is frozen).
20
+
21
+ Exit codes:
22
+ 0 — success (queue written or already up to date)
23
+ 1 — confirmed_plan missing/empty, or plan diverged from existing queue
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import os
29
+ import sys
30
+ from pathlib import Path
31
+
32
+ HERE = Path(__file__).resolve().parent
33
+ sys.path.insert(0, str(HERE / "lib"))
34
+ import state_machine # noqa: E402
35
+
36
+
37
+ def _resolve_session_path() -> Path:
38
+ """Match the resolver used by guardrails/gate_*.py:
39
+ env var → $CLAUDE_PROJECT_DIR → cwd. The session file lives in the
40
+ user project, never in the skill repo, so we never look at HERE.parents.
41
+ """
42
+ explicit = os.environ.get("TRTC_SESSION_PATH")
43
+ if explicit:
44
+ return Path(explicit)
45
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR")
46
+ if project_dir:
47
+ return Path(project_dir) / ".trtc-session.yaml"
48
+ return Path.cwd() / ".trtc-session.yaml"
49
+
50
+
51
+ def main(argv: list[str] | None = None) -> int:
52
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
53
+ parser.add_argument(
54
+ "--session",
55
+ type=Path,
56
+ default=None,
57
+ help="explicit path to .trtc-session.yaml (overrides env-based resolver)",
58
+ )
59
+ args = parser.parse_args(argv)
60
+
61
+ session_path = args.session if args.session is not None else _resolve_session_path()
62
+
63
+ if not session_path.exists():
64
+ print(
65
+ f"error: session file not found at {session_path}\n"
66
+ f" hint: cd to the user project root, or set $CLAUDE_PROJECT_DIR / "
67
+ f"$TRTC_SESSION_PATH before running this script.",
68
+ file=sys.stderr,
69
+ )
70
+ return 1
71
+
72
+ try:
73
+ state_machine.init_queue(session_path)
74
+ except RuntimeError as exc:
75
+ print(f"error: {exc}", file=sys.stderr)
76
+ return 1
77
+
78
+ st = state_machine.status(session_path)
79
+ kind = st.get("kind", "slice")
80
+ cur = st.get("current_unit_id") or st.get("current_slice_id")
81
+ if kind == "unit":
82
+ print(
83
+ f"queue initialised — {st['total']} delivery units, "
84
+ f"cursor at [{st['index']}] {cur} ({st['state']}), "
85
+ f"slices: {', '.join(st.get('slice_ids') or [])}"
86
+ )
87
+ else:
88
+ print(
89
+ f"queue initialised — {st['total']} slices, "
90
+ f"cursor at [{st['index']}] {cur} ({st['state']})"
91
+ )
92
+ return 0
93
+
94
+
95
+ if __name__ == "__main__":
96
+ sys.exit(main())
@@ -0,0 +1,328 @@
1
+ """Execution-step state machine for the topic skill.
2
+
3
+ The state machine is the enforcement mechanism for topic execution. It stores a
4
+ single ``execution_queue`` in ``.trtc-session.yaml``. Each execution step has a
5
+ uniform shape:
6
+
7
+ {id, type: "slice" | "unit", status, slices: [...]}
8
+
9
+ Slices remain the knowledge and rule source. Execution steps define how many
10
+ slices are delivered in one read/write/apply loop.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import Optional, Tuple
16
+
17
+ import yaml
18
+
19
+
20
+ _TRANSITIONS = {
21
+ ("not_started", "mark_slice_read"): "slice_read",
22
+ ("slice_read", "mark_code_written"): "code_written",
23
+ ("code_written", "mark_apply_passed"): "apply_passed",
24
+ ("code_written", "mark_apply_failed"): "apply_failed",
25
+ ("apply_failed", "mark_code_written"): "code_written",
26
+ ("apply_passed", "mark_user_confirmed"): "ADVANCE_INDEX",
27
+ }
28
+
29
+ _KNOWN_TRANSITIONS = {t for (_state, t) in _TRANSITIONS.keys()}
30
+ _TOPIC_ROOT = Path(__file__).resolve().parents[2]
31
+ _EXECUTION_UNITS_PATH = _TOPIC_ROOT / "references" / "execution-units.yaml"
32
+
33
+
34
+ def _load(path: Path) -> dict:
35
+ return yaml.safe_load(path.read_text()) or {}
36
+
37
+
38
+ def _save(path: Path, data: dict) -> None:
39
+ path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True))
40
+
41
+
42
+ def _step_id_from_slices(slice_ids: list[str]) -> str:
43
+ if len(slice_ids) == 1:
44
+ return slice_ids[0]
45
+ return "__".join(s.split("/", 1)[1] for s in slice_ids)
46
+
47
+
48
+ def _step_type(slice_ids: list[str]) -> str:
49
+ return "unit" if len(slice_ids) > 1 else "slice"
50
+
51
+
52
+ def _make_step(step_id: str, title: str, slice_ids: list[str], *, status: str = "pending") -> dict:
53
+ return {
54
+ "id": step_id,
55
+ "type": _step_type(slice_ids),
56
+ "title": title,
57
+ "status": status,
58
+ "slices": slice_ids,
59
+ }
60
+
61
+
62
+ def _build_declared_unit_steps(raw_units, plan: list[str]) -> list[dict]:
63
+ """Group only slices already present in confirmed_plan."""
64
+ configured = [
65
+ sid
66
+ for raw in raw_units or []
67
+ for sid in (raw.get("slices") or [])
68
+ ]
69
+ duplicates = sorted({sid for sid in configured if configured.count(sid) > 1})
70
+ if duplicates:
71
+ raise RuntimeError("delivery unit config contains duplicate slices: " + ", ".join(duplicates))
72
+
73
+ remaining = list(plan)
74
+ steps: list[dict] = []
75
+
76
+ for raw in raw_units or []:
77
+ group = raw.get("slices") or []
78
+ slice_ids = [sid for sid in group if sid in remaining]
79
+ if len(slice_ids) < 2:
80
+ continue
81
+ step_id = raw.get("id") or _step_id_from_slices(slice_ids)
82
+ steps.append(_make_step(step_id, raw.get("title") or step_id, slice_ids))
83
+ for sid in slice_ids:
84
+ remaining.remove(sid)
85
+
86
+ for sid in remaining:
87
+ steps.append(_make_step(sid, sid, [sid]))
88
+
89
+ order = {sid: i for i, sid in enumerate(plan)}
90
+ steps.sort(key=lambda step: min(order[sid] for sid in step["slices"]))
91
+ return steps
92
+
93
+
94
+ def _scenario_delivery_units(data: dict) -> list[dict]:
95
+ scenario = data.get("scenario")
96
+ if not scenario or not _EXECUTION_UNITS_PATH.exists():
97
+ return []
98
+ config = yaml.safe_load(_EXECUTION_UNITS_PATH.read_text()) or {}
99
+ return (
100
+ (config.get("scenarios") or {})
101
+ .get(scenario, {})
102
+ .get("delivery_units")
103
+ or []
104
+ )
105
+
106
+
107
+ def _build_slice_steps(plan: list[str]) -> list[dict]:
108
+ return [_make_step(sid, sid, [sid]) for sid in plan]
109
+
110
+
111
+ def _flatten_slices(queue: list[dict]) -> list[str]:
112
+ return [sid for step in queue for sid in step.get("slices", [])]
113
+
114
+
115
+ def _execution_queue_from_legacy(data: dict) -> list[dict] | None:
116
+ """Best-effort read compatibility for old sessions."""
117
+ if data.get("execution_queue"):
118
+ return data["execution_queue"]
119
+ if data.get("delivery_unit_queue"):
120
+ return [
121
+ _make_step(
122
+ entry.get("id") or _step_id_from_slices(entry.get("slices", [])),
123
+ entry.get("title") or entry.get("id") or "",
124
+ entry.get("slices", []),
125
+ status=entry.get("status", "pending"),
126
+ )
127
+ for entry in data["delivery_unit_queue"]
128
+ ]
129
+ if data.get("slice_queue"):
130
+ return [
131
+ _make_step(entry.get("id"), entry.get("id"), [entry.get("id")], status=entry.get("status", "pending"))
132
+ for entry in data["slice_queue"]
133
+ if entry.get("id")
134
+ ]
135
+ return None
136
+
137
+
138
+ def _current_index_state(data: dict) -> tuple[int, str]:
139
+ if "current_execution_index" in data or "current_execution_state" in data:
140
+ return data.get("current_execution_index", 0), data.get("current_execution_state") or "not_started"
141
+ if data.get("execution_granularity") in {"unit", "delivery_unit"}:
142
+ return data.get("current_unit_index", 0), data.get("current_unit_state") or "not_started"
143
+ return data.get("current_slice_index", 0), data.get("current_slice_state") or "not_started"
144
+
145
+
146
+ def init_queue(session_path) -> None:
147
+ """Materialise ``confirmed_plan`` into ``execution_queue``.
148
+
149
+ Idempotent when the queue already covers the same confirmed_plan. Refuses
150
+ re-init if the plan changed after topic took ownership.
151
+ """
152
+ path = Path(session_path)
153
+ data = _load(path)
154
+ plan = data.get("confirmed_plan")
155
+ if not plan:
156
+ raise RuntimeError(
157
+ "confirmed_plan is missing or empty in session — onboarding A2 "
158
+ "should have populated it before topic ran init_queue."
159
+ )
160
+
161
+ expected_ids = list(plan)
162
+ existing_queue = data.get("execution_queue")
163
+ if existing_queue is not None:
164
+ if _flatten_slices(existing_queue) != expected_ids:
165
+ raise RuntimeError(
166
+ "execution_queue is frozen and differs from confirmed_plan — "
167
+ "remove execution_queue / current_execution_index / "
168
+ "current_execution_state from the session file and run init again."
169
+ )
170
+ return
171
+
172
+ if data.get("execution_granularity") in {"unit", "delivery_unit"}:
173
+ raw_units = data.get("delivery_units") or _scenario_delivery_units(data)
174
+ queue = _build_declared_unit_steps(raw_units, expected_ids)
175
+ else:
176
+ queue = _build_slice_steps(expected_ids)
177
+
178
+ data["execution_queue"] = queue
179
+ data["current_execution_index"] = 0
180
+ data["current_execution_state"] = "not_started"
181
+ _save(path, data)
182
+
183
+
184
+ def current_scope(session_path) -> dict:
185
+ """Return the active execution step."""
186
+ path = Path(session_path)
187
+ if not path.exists():
188
+ return {"initialised": False, "reason": "session file missing"}
189
+
190
+ data = _load(path)
191
+ queue = _execution_queue_from_legacy(data)
192
+ if not queue:
193
+ return {"initialised": False, "reason": "execution_queue not set"}
194
+
195
+ idx, state = _current_index_state(data)
196
+ if state == "all_done":
197
+ return {
198
+ "initialised": True,
199
+ "kind": "execution",
200
+ "index": idx,
201
+ "state": "all_done",
202
+ "total": len(queue),
203
+ "id": None,
204
+ "title": None,
205
+ "type": None,
206
+ "slice_ids": [],
207
+ "queue": queue,
208
+ }
209
+ if idx is None or idx < 0 or idx >= len(queue):
210
+ return {"initialised": False, "reason": "execution cursor out of range"}
211
+
212
+ step = queue[idx]
213
+ slice_ids = step.get("slices") or [step.get("id")]
214
+ return {
215
+ "initialised": True,
216
+ "kind": step.get("type", _step_type(slice_ids)),
217
+ "index": idx,
218
+ "state": state,
219
+ "total": len(queue),
220
+ "id": step.get("id"),
221
+ "title": step.get("title") or step.get("id"),
222
+ "type": step.get("type", _step_type(slice_ids)),
223
+ "slice_ids": slice_ids,
224
+ "queue": queue,
225
+ }
226
+
227
+
228
+ def current_slice(session_path) -> Tuple[Optional[int], Optional[str], Optional[str]]:
229
+ """Compatibility helper returning the first slice in the current step."""
230
+ scope = current_scope(session_path)
231
+ if not scope.get("initialised"):
232
+ return (None, None, None)
233
+ if scope.get("state") == "all_done":
234
+ return (scope.get("index"), None, "all_done")
235
+ slice_ids = scope.get("slice_ids") or []
236
+ return (scope.get("index"), slice_ids[0] if slice_ids else None, scope.get("state"))
237
+
238
+
239
+ def advance(session_path, transition: str) -> str:
240
+ if transition not in _KNOWN_TRANSITIONS:
241
+ raise RuntimeError(
242
+ f"unknown transition '{transition}'. Known transitions: "
243
+ f"{sorted(_KNOWN_TRANSITIONS)}"
244
+ )
245
+
246
+ path = Path(session_path)
247
+ data = _load(path)
248
+ queue = data.get("execution_queue")
249
+ if not queue:
250
+ legacy = _execution_queue_from_legacy(data)
251
+ if legacy:
252
+ queue = legacy
253
+ data["execution_queue"] = queue
254
+ else:
255
+ raise RuntimeError("execution_queue not initialised — call init_queue() before advance().")
256
+
257
+ state = data.get("current_execution_state")
258
+ idx = data.get("current_execution_index", 0)
259
+ if state is None:
260
+ legacy_idx, legacy_state = _current_index_state(data)
261
+ idx = legacy_idx
262
+ state = legacy_state
263
+ data["current_execution_index"] = idx
264
+ data["current_execution_state"] = state
265
+
266
+ if state == "all_done":
267
+ raise RuntimeError(
268
+ "all_done — the execution queue is finished; no further transitions "
269
+ "are allowed. Start a new scenario or reset the session."
270
+ )
271
+
272
+ next_state = _TRANSITIONS.get((state, transition))
273
+ if next_state is None:
274
+ raise RuntimeError(
275
+ f"illegal transition '{transition}' from state '{state}'. "
276
+ f"Allowed from '{state}': "
277
+ f"{sorted(t for (s, t) in _TRANSITIONS.keys() if s == state)}"
278
+ )
279
+
280
+ if next_state == "ADVANCE_INDEX":
281
+ step = queue[idx]
282
+ step["status"] = "done"
283
+ evidence_ids = [step.get("id"), *step.get("slices", [])]
284
+ for evidence_id in [eid for eid in evidence_ids if eid]:
285
+ ev_path = path.parent / ".trtc-apply-evidence" / (evidence_id.replace("/", "__") + ".json")
286
+ try:
287
+ ev_path.unlink()
288
+ except FileNotFoundError:
289
+ pass
290
+
291
+ new_idx = idx + 1
292
+ data["execution_queue"] = queue
293
+ data["current_execution_index"] = new_idx
294
+ data["current_execution_state"] = "all_done" if new_idx >= len(queue) else "not_started"
295
+ _save(path, data)
296
+ return data["current_execution_state"]
297
+
298
+ data["execution_queue"] = queue
299
+ data["current_execution_state"] = next_state
300
+ _save(path, data)
301
+ return next_state
302
+
303
+
304
+ def status(session_path) -> dict:
305
+ path = Path(session_path)
306
+ if not path.exists():
307
+ return {"initialised": False, "reason": "session file missing"}
308
+ data = _load(path)
309
+ queue = _execution_queue_from_legacy(data)
310
+ if not queue:
311
+ return {"initialised": False, "reason": "execution_queue not set"}
312
+
313
+ idx, state = _current_index_state(data)
314
+ current = queue[idx] if idx < len(queue) else None
315
+ slice_ids = (current.get("slices") if current else None) or []
316
+ step_type = current.get("type", _step_type(slice_ids)) if current else None
317
+ return {
318
+ "initialised": True,
319
+ "kind": step_type or "execution",
320
+ "index": idx,
321
+ "state": state,
322
+ "total": len(queue),
323
+ "current_slice_id": slice_ids[0] if slice_ids else None,
324
+ "current_unit_id": current.get("id") if current and step_type == "unit" else None,
325
+ "current_id": current.get("id") if current else None,
326
+ "slice_ids": slice_ids,
327
+ "queue": queue,
328
+ }
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+ """next_slice.py — Topic-skill CLI: query and advance the execution cursor.
3
+
4
+ The topic skill drives one execution step at a time by calling this between
5
+ steps. An execution step may be one slice or one delivery unit containing
6
+ multiple slices.
7
+
8
+ Usage:
9
+ python3 next_slice.py status # print current cursor
10
+ python3 next_slice.py advance <transition> # apply a transition
11
+
12
+ Transitions (see state_machine.py for the diagram):
13
+ mark_slice_read not_started → slice_read
14
+ mark_code_written slice_read → code_written
15
+ apply_failed → code_written (retry)
16
+ mark_apply_passed code_written → apply_passed
17
+ mark_apply_failed code_written → apply_failed
18
+ mark_user_confirmed apply_passed → next execution step (or all_done)
19
+
20
+ Both subcommands print a one-line summary on stdout and exit 0 on success.
21
+ On any error (illegal transition, queue not initialised, etc.) prints to
22
+ stderr and exits 1 — topic should treat that as "abort the current step".
23
+
24
+ Optional flags:
25
+ --session PATH explicit session file path (overrides env-based resolver)
26
+ --json emit machine-readable JSON instead of the human line
27
+
28
+ Session path resolution (when --session not given):
29
+ 1. $TRTC_SESSION_PATH env var
30
+ 2. $CLAUDE_PROJECT_DIR/.trtc-session.yaml (Claude Code sets this to the
31
+ user project root)
32
+ 3. ./.trtc-session.yaml (cwd fallback)
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import argparse
37
+ import json as jsonlib
38
+ import os
39
+ import sys
40
+ from pathlib import Path
41
+
42
+ HERE = Path(__file__).resolve().parent
43
+ sys.path.insert(0, str(HERE / "lib"))
44
+ import state_machine # noqa: E402
45
+
46
+
47
+ def _resolve_session_path() -> Path:
48
+ """Match the resolver used by guardrails/gate_*.py."""
49
+ explicit = os.environ.get("TRTC_SESSION_PATH")
50
+ if explicit:
51
+ return Path(explicit)
52
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR")
53
+ if project_dir:
54
+ return Path(project_dir) / ".trtc-session.yaml"
55
+ return Path.cwd() / ".trtc-session.yaml"
56
+
57
+
58
+ def _print_status(session_path: Path, as_json: bool) -> int:
59
+ st = state_machine.status(session_path)
60
+ if as_json:
61
+ print(jsonlib.dumps(st, ensure_ascii=False))
62
+ return 0
63
+ if not st.get("initialised"):
64
+ print(f"queue not initialised: {st.get('reason')}")
65
+ return 0
66
+ kind = st.get("kind", "slice")
67
+ cur = st.get("current_unit_id") or st.get("current_slice_id")
68
+ detail = ""
69
+ if kind == "unit":
70
+ detail = f" ({', '.join(st.get('slice_ids') or [])})"
71
+ print(f"[{st['index']}/{st['total']}] {kind}:{cur}{detail} :: {st['state']}")
72
+ return 0
73
+
74
+
75
+ def _do_advance(session_path: Path, transition: str, as_json: bool) -> int:
76
+ try:
77
+ new_state = state_machine.advance(session_path, transition)
78
+ except RuntimeError as exc:
79
+ if as_json:
80
+ print(jsonlib.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
81
+ else:
82
+ print(f"error: {exc}", file=sys.stderr)
83
+ return 1
84
+
85
+ st = state_machine.status(session_path)
86
+ if as_json:
87
+ payload = {
88
+ "ok": True,
89
+ "transition": transition,
90
+ "new_state": new_state,
91
+ "index": st["index"],
92
+ "current_slice_id": st["current_slice_id"],
93
+ "current_unit_id": st.get("current_unit_id"),
94
+ "kind": st.get("kind", "slice"),
95
+ "slice_ids": st.get("slice_ids") or [],
96
+ }
97
+ print(jsonlib.dumps(payload, ensure_ascii=False))
98
+ else:
99
+ cur = st.get("current_unit_id") or st["current_slice_id"] or "(none)"
100
+ print(
101
+ f"{transition} OK → [{st['index']}/{st['total']}] "
102
+ f"{st.get('kind', 'slice')}:{cur} :: {new_state}"
103
+ )
104
+ return 0
105
+
106
+
107
+ def main(argv: list[str] | None = None) -> int:
108
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
109
+ parser.add_argument("--session", type=Path, default=None)
110
+ parser.add_argument("--json", action="store_true")
111
+ sub = parser.add_subparsers(dest="cmd", required=True)
112
+ sub.add_parser("status")
113
+ p_advance = sub.add_parser("advance")
114
+ p_advance.add_argument("transition")
115
+ args = parser.parse_args(argv)
116
+
117
+ session_path = args.session if args.session is not None else _resolve_session_path()
118
+
119
+ if not session_path.exists():
120
+ msg = (
121
+ f"error: session file not found at {session_path}\n"
122
+ f" hint: cd to the user project root, or set $CLAUDE_PROJECT_DIR / "
123
+ f"$TRTC_SESSION_PATH before running this script."
124
+ )
125
+ if args.json:
126
+ print(jsonlib.dumps({"ok": False, "error": msg}, ensure_ascii=False))
127
+ else:
128
+ print(msg, file=sys.stderr)
129
+ return 1
130
+
131
+ if args.cmd == "status":
132
+ return _print_status(session_path, args.json)
133
+ return _do_advance(session_path, args.transition, args.json)
134
+
135
+
136
+ if __name__ == "__main__":
137
+ sys.exit(main())
@@ -0,0 +1,70 @@
1
+ # Topic state-machine tests
2
+
3
+ > ⚪ **内部 — 不对外发布**。这些测试只供 TRTC 知识库维护者使用。
4
+ > 跟项目根 `tests/` 的定位一致,不会随 skill 包分发到用户机器;
5
+ > 发布脚本应当排除整个 `tests/` 目录。end user 不会跑、也不应跑这些测试。
6
+
7
+ 这套测试守护 topic skill 的 slice-loop 状态机、PreToolUse / Stop 守门员
8
+ 与 `apply.py` 的契约——也就是阻止 AI 批量读 slice、批量生成代码、跳过 `apply`
9
+ 这三类坏行为的那一层硬约束。每次改动 `scripts/`、`scripts/lib/`、
10
+ `guardrails/` 下任何一个文件都应跑一遍这套测试。
11
+
12
+ ## 运行
13
+
14
+ ```bash
15
+ # 在 repo 根
16
+ python3 -m pytest ${CLAUDE_PLUGIN_ROOT}/skills/trtc-topic/tests/
17
+
18
+ # 看详细
19
+ python3 -m pytest ${CLAUDE_PLUGIN_ROOT}/skills/trtc-topic/tests/ -v
20
+
21
+ # 单文件
22
+ python3 -m pytest ${CLAUDE_PLUGIN_ROOT}/skills/trtc-topic/tests/test_state_machine.py -v
23
+
24
+ # 单 case
25
+ python3 -m pytest \
26
+ ${CLAUDE_PLUGIN_ROOT}/skills/trtc-topic/tests/test_apply_cli.py::TestAutoAdvanceOnPass::test_pause_on_failure_pass_advances_to_next_slice -v
27
+ ```
28
+
29
+ 依赖:pytest 8+ 与 PyYAML(项目已有)。
30
+
31
+ ## 覆盖矩阵(85 cases)
32
+
33
+ | 文件 | cases | 守护对象 | 关键场景 |
34
+ |---|---|---|---|
35
+ | `test_state_machine.py` | 25 | `scripts/lib/state_machine.py` | `init_queue` / `current_slice` / `advance` 全部合法 transition;非法 transition 抛错;apply_failed → 重试;user_confirmed 后清理 evidence;queue 末尾 → all_done |
36
+ | `test_gates.py` | 19 | `guardrails/gate_slice_read.py`、`guardrails/gate_slice_write.py` | session 缺失/queue 未初始化静默放行;只放行 cursor 当前 slice 的 .md;`not_started`/`apply_passed` 状态拦 Write;`slice_read`/`code_written`/`apply_failed` 放行 Edit |
37
+ | `test_apply_cli.py` | 16 | `scripts/apply.py` | pass + fail + static-only + 4 类 usage error;`auto_advance_policy` 五种值;apply 失败永远 pause;最后一个 slice → all_done;demo-test-2 三条 regression(注释/字符串塞 pattern / fail 输出不泄漏 patterns)|
38
+ | `test_stop_require_apply.py` | 8 | `guardrails/stop_require_apply_evidence.py` | session/queue 缺失放行;`not_started` / `slice_read` / `apply_passed` / `all_done` 放行;`code_written` / `apply_failed` 拦截 |
39
+ | `test_end_to_end.py` | 3 | 全链路 | 手动 confirm 完整循环;apply 失败路径;`pause_on_failure` 自动推进路径 |
40
+ | `test_topic_skill_invariants.py` | 4 | `topic/SKILL.md` 结构不变量 | Apply Evidence Block 已删;STATE-MACHINE-GUIDE.md 存在且被引用;topic/SKILL.md 不超过 480 行;Calling apply 段保持紧凑 |
41
+ | `test_session_resolver.py` | 10 | 三个 CLI 共享的 session 路径解析 | 4 级解析链(`--session` flag → `$TRTC_SESSION_PATH` → `$CLAUDE_PROJECT_DIR/.trtc-session.yaml` → cwd);找不到时给 actionable 提示;三个 CLI 行为一致 |
42
+
43
+ ## Fixtures(`conftest.py`)
44
+
45
+ - `session_factory(**overrides)` — 在 `tmp_path` 写一个 `${CLAUDE_PROJECT_DIR}/.trtc-session.yaml`。
46
+ 默认仿真"general-conference / web 集成中"的状态。`overrides` 可换 `confirmed_plan`、
47
+ `auto_advance_policy` 等任意根级字段。
48
+ - `project_factory()` — 在 `tmp_path/user-project/src/` 造空项目骨架,
49
+ 测试可以往里写 .vue / .ts。
50
+
51
+ ## 依赖的目录布局(已对齐 apply skill)
52
+
53
+ ```
54
+ topic/
55
+ ├── guardrails/ ← Claude Code 注册的 hook
56
+ │ ├── gate_slice_read.py
57
+ │ ├── gate_slice_write.py
58
+ │ └── stop_require_apply_evidence.py
59
+ ├── scripts/ ← AI 通过 Bash 调用的 runtime CLI
60
+ │ ├── apply.py
61
+ │ ├── init_slice_queue.py
62
+ │ ├── next_slice.py
63
+ │ └── lib/
64
+ │ └── state_machine.py ← 被 import 的库(不直接跑)
65
+ └── tests/ ← ⚪ 本目录
66
+ ```
67
+
68
+ 如果你 git mv 了 `scripts/` 或 `scripts/lib/` 下的任何文件,这套测试里的
69
+ `sys.path.insert` / `parents[N]` / 命令行路径都需要同步更新——跑全套就能
70
+ 立刻发现哪些路径断了。