@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,581 @@
1
+ #!/usr/bin/env python3
2
+ """apply.py — Executable structural gate.
3
+
4
+ This is a STRUCTURAL GATE, not a correctness/compile verifier. It exists to
5
+ stop the AI from declaring a slice "done" and ending the turn before a
6
+ deterministic check has run — a forcing function on the state machine, paired
7
+ with the Stop hook (``guardrails/stop_require_apply_evidence.py``). The check
8
+ itself is intentionally minimal; it does NOT verify types, compilation, or
9
+ runtime behavior. Correctness comes from the slice's MUST/MUST NOT constraints
10
+ at generation time and the user running the code in their real project.
11
+
12
+ Usage:
13
+ python3 apply.py --slice conference/login-auth
14
+ python3 apply.py --unit foundation
15
+ [--session PATH]
16
+ [--project PATH]
17
+
18
+ Session path resolution (when --session not given):
19
+ 1. $TRTC_SESSION_PATH env var
20
+ 2. $CLAUDE_PROJECT_DIR/.trtc-session.yaml (Claude Code sets this to the
21
+ user project root)
22
+ 3. ./.trtc-session.yaml (cwd fallback)
23
+
24
+ What it does:
25
+
26
+ 1. Confirms the state machine is in ``code_written`` for the slice the
27
+ caller named. Anything else is a usage error (exit 2).
28
+ 2. Requires the project to contain real source (``code non-empty``): if
29
+ ``<project_root>/src/`` has no ``.vue``/``.ts`` files the run is
30
+ static-only and fails — there is nothing to gate.
31
+ 3. For each slice, checks that the slice's **entry symbol** (its composable /
32
+ component, e.g. ``useDeviceState`` for device-control) appears as a real
33
+ code identifier in some source file — **with comments and string literals
34
+ stripped first** so the entry can't be faked from a ``// comment`` or
35
+ ``"string"``. Slices with no registered entry symbol are skipped (we can't
36
+ check them mechanically; never a false-positive). This is a coarse
37
+ "did you wire up this capability's entry" check, deliberately NOT a proof
38
+ of correctness.
39
+ 4. Runs a NARROW compile-safety check for composable-destructuring name
40
+ collisions (e.g. the same symbol destructured from two ``use*()`` calls,
41
+ or destructured and then re-declared as a function). It is NOT a general
42
+ linter.
43
+ 5. Writes evidence JSON to ``<session_dir>/.trtc-apply-evidence/<slug>.json``.
44
+ 6. Advances the state machine: pass → apply_passed (exit 0); fail →
45
+ apply_failed (exit 1).
46
+
47
+ Exit codes: 0 pass / 1 fail / 2 usage error
48
+
49
+ Why an entry-symbol check and not a per-API grep: an earlier version greped
50
+ each MUST rule's backtick-quoted patterns. That was unreliable in both
51
+ directions — any-pattern-hit produced false positives, and stripping string
52
+ literals (needed for anti-cheat) produced false negatives whenever a symbol
53
+ legitimately lived inside a string (e.g. an error-code constant). The gate's
54
+ real value is the forcing function (you cannot self-certify and stop), not the
55
+ judge's precision, so the judge was reduced to the honest minimum: code exists
56
+ + the slice's entry was wired up. Correctness is delegated to the slice's
57
+ MUST/MUST NOT constraints and the customer's own build.
58
+
59
+ Why not a compiler: apply runs inside the customer's heterogeneous existing
60
+ project, where a build can fail for reasons unrelated to generated code
61
+ (missing deps, private registries, monorepo config) and is slow per slice —
62
+ so compilation is intentionally out of scope. The comment/string stripping is
63
+ kept because the demo-test-2 bug showed symbols can be stuffed into comments
64
+ or strings; entry symbols are code identifiers so this never false-negatives
65
+ on correct code.
66
+ """
67
+ from __future__ import annotations
68
+
69
+ import argparse
70
+ import json
71
+ import os
72
+ import re
73
+ import sys
74
+ from pathlib import Path
75
+
76
+ import yaml
77
+
78
+ HERE = Path(__file__).resolve().parent
79
+ LIB_DIR = HERE / "lib"
80
+
81
+ sys.path.insert(0, str(LIB_DIR))
82
+ import state_machine # noqa: E402
83
+ sys.path.pop(0)
84
+
85
+ # rule_parser lives under trtc-apply/guardrails/apply_lib (sibling skill).
86
+ # HERE = skills/trtc-topic/scripts → parent.parent = skills/
87
+ _APPLY_GUARDRAILS = HERE.parent.parent / "trtc-apply" / "guardrails"
88
+ sys.path.insert(0, str(_APPLY_GUARDRAILS))
89
+ from apply_lib.rule_parser import entry_symbols_for_slice # noqa: E402
90
+ sys.path.pop(0)
91
+
92
+
93
+ def _resolve_session_path() -> Path:
94
+ """Match the resolver used by guardrails/gate_*.py:
95
+ env var → $CLAUDE_PROJECT_DIR → cwd. The session file lives in the
96
+ user project, never in the skill repo.
97
+ """
98
+ explicit = os.environ.get("TRTC_SESSION_PATH")
99
+ if explicit:
100
+ return Path(explicit)
101
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR")
102
+ if project_dir:
103
+ return Path(project_dir) / ".trtc-session.yaml"
104
+ return Path.cwd() / ".trtc-session.yaml"
105
+
106
+
107
+ def _eprint(msg: str) -> None:
108
+ sys.stderr.write(msg.rstrip("\n") + "\n")
109
+
110
+
111
+ def _slice_id_to_slug(slice_id: str) -> str:
112
+ return slice_id.replace("/", "__")
113
+
114
+
115
+ def _load_session(session_path: Path) -> dict:
116
+ return yaml.safe_load(session_path.read_text()) or {}
117
+
118
+
119
+ def _strip_comments_and_strings(content: str) -> str:
120
+ """Replace JS/TS comments and string literals with whitespace.
121
+
122
+ Why: the demo-test-2 root cause was AI stuffing the literal pattern
123
+ (e.g. ``Math.floor(Date.now() / 1000)``) into a ``//`` comment to pass
124
+ substring grep. After stripping, only real code is searched. Whitespace
125
+ is used (not deletion) so byte offsets are preserved if anyone ever
126
+ needs to surface code locations from the stripped buffer.
127
+
128
+ Strips:
129
+ * ``// single-line``
130
+ * ``/* block */`` (multi-line)
131
+ * ``"double-quoted"`` strings
132
+ * ``'single-quoted'`` strings
133
+ * `` `template literals` ``
134
+ """
135
+ out: list[str] = []
136
+ i = 0
137
+ n = len(content)
138
+ while i < n:
139
+ # // single-line comment — skip to next newline
140
+ if content[i:i + 2] == "//":
141
+ end = content.find("\n", i)
142
+ if end == -1:
143
+ end = n
144
+ out.append(" " * (end - i))
145
+ i = end
146
+ # /* block comment */ — skip to */
147
+ elif content[i:i + 2] == "/*":
148
+ end = content.find("*/", i + 2)
149
+ if end == -1:
150
+ end = n
151
+ else:
152
+ end += 2
153
+ replaced = content[i:end]
154
+ # Preserve newlines inside the block; replace other chars with space.
155
+ out.append(re.sub(r"[^\n]", " ", replaced))
156
+ i = end
157
+ # double-quoted string
158
+ elif content[i] == '"':
159
+ j = i + 1
160
+ while j < n:
161
+ if content[j] == "\\" and j + 1 < n:
162
+ j += 2
163
+ elif content[j] == '"':
164
+ j += 1
165
+ break
166
+ else:
167
+ j += 1
168
+ out.append('""' + " " * max(0, j - i - 2))
169
+ i = j
170
+ # single-quoted string
171
+ elif content[i] == "'":
172
+ j = i + 1
173
+ while j < n:
174
+ if content[j] == "\\" and j + 1 < n:
175
+ j += 2
176
+ elif content[j] == "'":
177
+ j += 1
178
+ break
179
+ else:
180
+ j += 1
181
+ out.append("''" + " " * max(0, j - i - 2))
182
+ i = j
183
+ # template literal — note: doesn't fully parse ${...} interpolation,
184
+ # treats whole thing as a string. That's fine for our pattern matching.
185
+ elif content[i] == "`":
186
+ j = i + 1
187
+ while j < n:
188
+ if content[j] == "\\" and j + 1 < n:
189
+ j += 2
190
+ elif content[j] == "`":
191
+ j += 1
192
+ break
193
+ else:
194
+ j += 1
195
+ out.append("``" + " " * max(0, j - i - 2))
196
+ i = j
197
+ else:
198
+ out.append(content[i])
199
+ i += 1
200
+ return "".join(out)
201
+
202
+
203
+ def _scan_project_src(project_root: Path) -> tuple[str, list[tuple[Path, str]]]:
204
+ """Return (mode, [(path, stripped_content), ...]).
205
+
206
+ Each file's content is stripped of comments and string literals before
207
+ being returned, so the substring matcher only ever sees real code.
208
+
209
+ mode is either 'full' or 'static-only'. static-only means src/ doesn't
210
+ exist or has no source files.
211
+ """
212
+ src = project_root / "src"
213
+ if not src.exists():
214
+ return "static-only", []
215
+ files: list[tuple[Path, str]] = []
216
+ for ext in ("*.vue", "*.ts"):
217
+ for f in src.rglob(ext):
218
+ try:
219
+ raw = f.read_text(encoding="utf-8")
220
+ except (OSError, UnicodeDecodeError):
221
+ continue
222
+ files.append((f, _strip_comments_and_strings(raw)))
223
+ return ("full" if files else "static-only", files)
224
+
225
+
226
+ _IDENT = r"[A-Za-z_$][\w$]*"
227
+ # `const|let|var X =` / `function X` / `async function X` / `class X`
228
+ _SIMPLE_DECL_RE = re.compile(
229
+ r"\b(?:const|let|var)\s+(" + _IDENT + r")\s*[=:]"
230
+ r"|\b(?:async\s+)?function\s*\*?\s*(" + _IDENT + r")"
231
+ r"|\bclass\s+(" + _IDENT + r")"
232
+ )
233
+ # `const { a, b: c } = useXxx(...)` — destructuring whose RHS is a call.
234
+ _DESTRUCTURE_RE = re.compile(
235
+ r"\b(?:const|let|var)\s*\{([^{}]*)\}\s*=\s*([^\n;]*)"
236
+ )
237
+
238
+
239
+ def _destructured_binding_names(brace_body: str) -> list[str]:
240
+ """Extract the locally-bound identifiers from a `{ ... }` destructure body.
241
+
242
+ Handles ``a``, ``a: b`` (binds ``b``), ``a = default``, ``...rest``.
243
+ Skips nested destructuring parts (containing ``{``) — rare for composable
244
+ destructuring and not worth mis-parsing.
245
+ """
246
+ names: list[str] = []
247
+ for part in brace_body.split(","):
248
+ part = part.strip()
249
+ if not part or "{" in part:
250
+ continue
251
+ if part.startswith("..."):
252
+ part = part[3:].strip()
253
+ if ":" in part: # rename: key:binding
254
+ part = part.split(":", 1)[1]
255
+ part = part.split("=", 1)[0].strip() # drop default value
256
+ if re.fullmatch(_IDENT, part):
257
+ names.append(part)
258
+ return names
259
+
260
+
261
+ def _check_duplicate_declarations(
262
+ files: list[tuple[Path, str]], project_root: Path
263
+ ) -> list[dict]:
264
+ """Detect composable-destructuring name collisions that will not compile.
265
+
266
+ This is a deliberately NARROW, high-precision check — NOT a general
267
+ redeclaration linter. It only flags the failure mode that apply's own
268
+ MUST-symbol grep can induce: the AI adds a destructured symbol (or a
269
+ wrapper function) to satisfy the grep, colliding with a name that was
270
+ already destructured from another composable. Two real demo bugs:
271
+
272
+ * ``const { getCameraList } = useDeviceState()`` + a later
273
+ ``function getCameraList()`` in the same file.
274
+ * ``const { subscribeEvent } = useRoomParticipantState()`` +
275
+ ``const { subscribeEvent } = useRoomState()`` in the same file.
276
+
277
+ We flag a name only when it is bound by destructuring at least twice, OR
278
+ bound by destructuring AND also declared as a simple const/function/class.
279
+ Pure simple-vs-simple duplicates are NOT flagged (those may be legal
280
+ locals in different scopes — we can't tell without a real parser).
281
+ """
282
+ issues: list[dict] = []
283
+ for path, content in files:
284
+ destructured: list[str] = []
285
+ for m in _DESTRUCTURE_RE.finditer(content):
286
+ brace_body, rhs = m.group(1), m.group(2)
287
+ if "(" not in rhs: # only RHS that is a call (composable / hook)
288
+ continue
289
+ destructured.extend(_destructured_binding_names(brace_body))
290
+ simple: list[str] = []
291
+ for m in _SIMPLE_DECL_RE.finditer(content):
292
+ name = m.group(1) or m.group(2) or m.group(3)
293
+ if name:
294
+ simple.append(name)
295
+ try:
296
+ rel = str(path.resolve().relative_to(project_root.resolve()))
297
+ except (ValueError, OSError):
298
+ rel = str(path)
299
+ for name in sorted(set(destructured)):
300
+ d = destructured.count(name)
301
+ s = simple.count(name)
302
+ if d >= 2 or (d >= 1 and s >= 1):
303
+ issues.append(
304
+ {
305
+ "category": "duplicate-declaration",
306
+ "type": "critical",
307
+ "symbol": name,
308
+ "file": rel,
309
+ "rule_text": (
310
+ f"Duplicate declaration of '{name}' in {rel}: it is "
311
+ f"destructured from a composable and re-declared "
312
+ f"({d}x destructure, {s}x const/function) in the same "
313
+ f"file. This will not compile — alias one of them."
314
+ ),
315
+ "slice_id": None,
316
+ }
317
+ )
318
+ return issues
319
+
320
+
321
+ def _check_slice_entry(
322
+ slice_id: str, files: list[tuple[Path, str]]
323
+ ) -> tuple[str, list[str]]:
324
+ """Check that a slice's entry symbol appears as real code.
325
+
326
+ Returns ``(result, entry_symbols)`` where ``result`` is one of:
327
+
328
+ * ``"pass"`` — at least one entry symbol appears as an identifier in
329
+ some file's real-code text (comments/strings stripped).
330
+ * ``"fail"`` — entry symbols are known but none appear.
331
+ * ``"skipped"`` — the slice has no registered entry symbol; it cannot be
332
+ checked mechanically and is treated as pass (we never
333
+ false-positive on slices we don't know how to check).
334
+
335
+ Entry symbols are stable code identifiers (composables / components), never
336
+ string literals, so unlike the old MUST-symbol grep this is immune to the
337
+ comment/string-stripping false-negative — yet the stripping is still run so
338
+ an entry mentioned only in a ``// comment`` or ``"string"`` does not count.
339
+ """
340
+ entry_symbols = entry_symbols_for_slice(slice_id)
341
+ if not entry_symbols:
342
+ return "skipped", entry_symbols
343
+ for sym in entry_symbols:
344
+ word_re = re.compile(r"\b" + re.escape(sym) + r"\b")
345
+ for _, content in files:
346
+ if word_re.search(content):
347
+ return "pass", entry_symbols
348
+ return "fail", entry_symbols
349
+
350
+
351
+ def _write_evidence(
352
+ session_path: Path,
353
+ evidence_id: str,
354
+ payload: dict,
355
+ ) -> Path:
356
+ ev_dir = session_path.parent / ".trtc-apply-evidence"
357
+ ev_dir.mkdir(parents=True, exist_ok=True)
358
+ out = ev_dir / f"{_slice_id_to_slug(evidence_id)}.json"
359
+ out.write_text(json.dumps(payload, ensure_ascii=False, indent=2))
360
+ return out
361
+
362
+
363
+ def main(argv: list[str] | None = None) -> int:
364
+ p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
365
+ p.add_argument("--slice", dest="slice_id", default=None)
366
+ p.add_argument(
367
+ "--unit",
368
+ dest="unit_id",
369
+ default=None,
370
+ help="current delivery unit id",
371
+ )
372
+ p.add_argument(
373
+ "--session",
374
+ type=Path,
375
+ default=None,
376
+ help="explicit path to .trtc-session.yaml (overrides env-based resolver)",
377
+ )
378
+ p.add_argument("--project", type=Path, default=None)
379
+ args = p.parse_args(argv)
380
+
381
+ if not args.slice_id and not args.unit_id:
382
+ _eprint("error: provide --slice <slice_id> or --unit <unit_id>.")
383
+ return 2
384
+
385
+ session_path = args.session if args.session is not None else _resolve_session_path()
386
+
387
+ if not session_path.exists():
388
+ _eprint(
389
+ f"error: session file not found: {session_path}\n"
390
+ f" hint: cd to the user project root, or set $CLAUDE_PROJECT_DIR / "
391
+ f"$TRTC_SESSION_PATH before running this script."
392
+ )
393
+ return 2
394
+
395
+ # State machine gate.
396
+ scope = state_machine.current_scope(session_path)
397
+ if not scope.get("initialised"):
398
+ _eprint("error: queue not initialised; run init_slice_queue.py first.")
399
+ return 2
400
+ idx = scope["index"]
401
+ current_id = scope["id"]
402
+ state = scope["state"]
403
+ slice_ids = scope["slice_ids"]
404
+ kind = scope["kind"]
405
+ if args.slice_id != current_id:
406
+ if kind == "unit":
407
+ if args.unit_id:
408
+ if args.unit_id != current_id:
409
+ _eprint(
410
+ f"error: --unit '{args.unit_id}' does not match current unit "
411
+ f"[{idx}] '{current_id}'."
412
+ )
413
+ return 2
414
+ elif args.slice_id not in slice_ids:
415
+ _eprint(
416
+ f"error: --slice '{args.slice_id}' is not part of current unit "
417
+ f"[{idx}] '{current_id}' ({', '.join(slice_ids)})."
418
+ )
419
+ return 2
420
+ else:
421
+ _eprint(
422
+ f"error: --slice '{args.slice_id}' does not match current slice "
423
+ f"[{idx}] '{current_id}'."
424
+ )
425
+ return 2
426
+ if state != "code_written":
427
+ _eprint(
428
+ f"error: state must be 'code_written' to run apply; current state is '{state}'.\n"
429
+ f"hint: write the slice's code first, then run "
430
+ f"`next_slice.py advance mark_code_written` before calling apply.py."
431
+ )
432
+ return 2
433
+
434
+ # Resolve project root.
435
+ session_data = _load_session(session_path)
436
+ if args.project is not None:
437
+ project_root = args.project
438
+ else:
439
+ pr = (session_data.get("project_state") or {}).get("project_root")
440
+ project_root = Path(pr) if pr else None
441
+ if project_root is None:
442
+ _eprint("error: project root not provided and not in session.")
443
+ return 2
444
+
445
+ mode, project_files = _scan_project_src(project_root)
446
+
447
+ issues: list[dict] = []
448
+ per_slice: dict[str, dict] = {}
449
+ entries_checked = 0
450
+ for slice_id in slice_ids:
451
+ result, entry_symbols = _check_slice_entry(slice_id, project_files)
452
+ rec = {
453
+ "slice_id": slice_id,
454
+ "entry_checked": result != "skipped",
455
+ "entry_result": result,
456
+ "issues": [],
457
+ }
458
+ per_slice[slice_id] = rec
459
+ if result == "skipped":
460
+ continue
461
+ entries_checked += 1
462
+ if result == "fail":
463
+ # Naming the entry composable is safe and helpful: it is the
464
+ # slice's documented import (see its "代码生成约束 → 额外导入"
465
+ # section), not a hidden API pattern an LLM could comment-stuff.
466
+ issue = {
467
+ "rule_text": (
468
+ f"slice '{slice_id}': entry not wired up — none of its "
469
+ f"entry symbols ({', '.join(entry_symbols)}) appear in the "
470
+ f"generated code. Import and use the slice's documented "
471
+ f"entry (see its '额外导入' section)."
472
+ )[:200],
473
+ "type": "entry",
474
+ "slice_id": slice_id,
475
+ }
476
+ issues.append(issue)
477
+ rec["issues"].append(issue)
478
+
479
+ # Narrow compile-safety check: composable-destructuring name collisions.
480
+ # These are real compile errors, so they fail the gate. It is NOT a
481
+ # general redeclaration linter.
482
+ dup_issues = _check_duplicate_declarations(project_files, project_root)
483
+ for dup in dup_issues:
484
+ dup["slice_id"] = current_id
485
+ issues.append(dup)
486
+ per_slice.setdefault(
487
+ current_id,
488
+ {
489
+ "slice_id": current_id,
490
+ "entry_checked": False,
491
+ "entry_result": "skipped",
492
+ "issues": [],
493
+ },
494
+ )
495
+ per_slice[current_id]["issues"].append(dup)
496
+
497
+ status = "fail" if issues or mode == "static-only" else "pass"
498
+ evidence_id = current_id if kind == "unit" else slice_ids[0]
499
+ payload = {
500
+ "id": evidence_id,
501
+ "kind": kind,
502
+ "slice_id": slice_ids[0] if len(slice_ids) == 1 else None,
503
+ "unit_id": current_id if kind == "unit" else None,
504
+ "slice_ids": slice_ids,
505
+ "status": status,
506
+ "mode": mode,
507
+ "entries_checked": entries_checked,
508
+ "issues": issues,
509
+ "slices_checked": list(per_slice.values()),
510
+ "project_root": str(project_root),
511
+ "files_scanned": len(project_files),
512
+ }
513
+ _write_evidence(session_path, evidence_id, payload)
514
+
515
+ transition = "mark_apply_passed" if status == "pass" else "mark_apply_failed"
516
+ try:
517
+ state_machine.advance(session_path, transition)
518
+ except RuntimeError as exc:
519
+ _eprint(f"error: failed to advance state machine: {exc}")
520
+ return 2
521
+
522
+ if status == "pass":
523
+ # auto-advance policy: pause_on_failure / pause_at_end skip the
524
+ # explicit "ask user 继续?" pause when apply itself confirms success.
525
+ # pause_each (default) preserves the original per-slice prompt.
526
+ # Unknown / unset values fall back to pause_each — fail closed.
527
+ policy = session_data.get("auto_advance_policy")
528
+ if policy in {"pause_on_failure", "pause_at_end"}:
529
+ try:
530
+ new_state = state_machine.advance(session_path, "mark_user_confirmed")
531
+ except RuntimeError as exc:
532
+ _eprint(f"error: failed to auto-advance state machine: {exc}")
533
+ return 2
534
+ print(
535
+ f"apply pass: {entries_checked} slice entr"
536
+ f"{'y' if entries_checked == 1 else 'ies'} wired up for "
537
+ f"{evidence_id} — auto-advanced ({policy}); next state: {new_state}"
538
+ )
539
+ # POST-LOOP CHECKLIST: when all slices are done, remind the AI
540
+ # to execute Step 4 and Step 4.5 from topic/SKILL.md.
541
+ if new_state == "all_done":
542
+ print("")
543
+ print("=" * 60)
544
+ print("ALL SLICES COMPLETE — POST-LOOP CHECKLIST (mandatory)")
545
+ print("=" * 60)
546
+ print("The slice loop is finished, but the topic flow is NOT done.")
547
+ print("You MUST now execute these steps from topic/SKILL.md:")
548
+ print("")
549
+ print(" □ Step 4: Present the verification checklist to the user")
550
+ print(" □ Step 4.5: Offer runtime verification & telemetry")
551
+ print(" (ask consent if telemetry.opted_in is null)")
552
+ print("")
553
+ print("Do NOT output a final summary and stop. Read topic/SKILL.md")
554
+ print("Step 4 and Step 4.5 sections and execute them now.")
555
+ print("=" * 60)
556
+ return 0
557
+ print(
558
+ f"apply pass: {entries_checked} slice entr"
559
+ f"{'y' if entries_checked == 1 else 'ies'} wired up for {evidence_id}"
560
+ )
561
+ return 0
562
+
563
+ entry_failed = [i for i in issues if i.get("category") != "duplicate-declaration"]
564
+ dup_failed = [i for i in issues if i.get("category") == "duplicate-declaration"]
565
+ parts = []
566
+ if entry_failed:
567
+ parts.append(f"{len(entry_failed)} slice entr"
568
+ f"{'y' if len(entry_failed) == 1 else 'ies'} not wired up")
569
+ if dup_failed:
570
+ parts.append(f"{len(dup_failed)} duplicate-declaration issue(s)")
571
+ if mode == "static-only" and not parts:
572
+ parts.append("no source files found under src/")
573
+ summary = ", ".join(parts) if parts else "gate failed"
574
+ print(f"apply fail: {summary} for {evidence_id} (mode={mode})")
575
+ for issue in issues[:5]:
576
+ print(f" - {issue.get('rule_text', '')[:160]}")
577
+ return 1
578
+
579
+
580
+ if __name__ == "__main__":
581
+ sys.exit(main())
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """finalize_session.py — Normalize a completed TRTC topic session.
3
+
4
+ Run this only after topic Step 4 / Step 4.5 are finished and the integration
5
+ is genuinely complete. `current_execution_state=all_done` only means the
6
+ execution loop ended; this script is the explicit final handoff.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import os
12
+ import sys
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+
16
+ import yaml
17
+
18
+
19
+ def _resolve_session_path() -> Path:
20
+ explicit = os.environ.get("TRTC_SESSION_PATH")
21
+ if explicit:
22
+ return Path(explicit)
23
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR")
24
+ if project_dir:
25
+ return Path(project_dir) / ".trtc-session.yaml"
26
+ return Path.cwd() / ".trtc-session.yaml"
27
+
28
+
29
+ def _dedupe_preserve_order(values) -> list:
30
+ seen = set()
31
+ out = []
32
+ for value in values or []:
33
+ if value in seen:
34
+ continue
35
+ seen.add(value)
36
+ out.append(value)
37
+ return out
38
+
39
+
40
+ def _normalize(data: dict) -> dict:
41
+ queue = data.get("execution_queue") or []
42
+ if not queue and data.get("slice_queue"):
43
+ queue = [
44
+ {
45
+ "id": entry.get("id"),
46
+ "type": "slice",
47
+ "title": entry.get("id"),
48
+ "status": entry.get("status", "pending"),
49
+ "slices": [entry.get("id")],
50
+ }
51
+ for entry in data.get("slice_queue") or []
52
+ if entry.get("id")
53
+ ]
54
+ queue_ids = [
55
+ slice_id
56
+ for entry in queue
57
+ for slice_id in (entry.get("slices") or [entry.get("id")])
58
+ if slice_id
59
+ ]
60
+ completed = _dedupe_preserve_order(data.get("completed_steps") or [])
61
+ for slice_id in queue_ids:
62
+ if slice_id not in completed:
63
+ completed.append(slice_id)
64
+
65
+ for entry in queue:
66
+ if all(slice_id in completed for slice_id in (entry.get("slices") or [entry.get("id")])):
67
+ entry["status"] = "done"
68
+
69
+ data["status"] = "completed"
70
+ data["current_step"] = "completed"
71
+ data["completed_steps"] = completed
72
+
73
+ if queue:
74
+ data["execution_queue"] = queue
75
+ data["current_execution_index"] = len(queue)
76
+ data["current_execution_state"] = "all_done"
77
+
78
+ data["updated_at"] = datetime.now(timezone.utc).astimezone().isoformat()
79
+
80
+ product = data.get("product") or "TRTC"
81
+ scenario = data.get("scenario") or data.get("intent") or "integration"
82
+ done_count = len(completed)
83
+ data["last_recap"] = (
84
+ f"{product} {scenario} integration completed. "
85
+ f"{done_count} step{'s' if done_count != 1 else ''} completed."
86
+ )
87
+ return data
88
+
89
+
90
+ def main(argv: list[str] | None = None) -> int:
91
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
92
+ parser.add_argument(
93
+ "--session",
94
+ type=Path,
95
+ default=None,
96
+ help="explicit path to .trtc-session.yaml (overrides env-based resolver)",
97
+ )
98
+ args = parser.parse_args(argv)
99
+
100
+ session_path = args.session if args.session is not None else _resolve_session_path()
101
+ if not session_path.exists():
102
+ print(f"error: session file not found at {session_path}", file=sys.stderr)
103
+ return 1
104
+
105
+ data = yaml.safe_load(session_path.read_text()) or {}
106
+ data = _normalize(data)
107
+ session_path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True))
108
+ print(f"session finalized — status=completed, current_step=completed")
109
+ return 0
110
+
111
+
112
+ if __name__ == "__main__":
113
+ sys.exit(main())