aw-spec-cli 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/assets/.appweaver/DESIGN-FORMAT.md +200 -0
  2. package/assets/.appweaver/ORCH-DIALECT.md +156 -0
  3. package/assets/.appweaver/RULE-PARTITION.md +175 -0
  4. package/assets/.appweaver/STACK-PROFILES.md +461 -0
  5. package/assets/.appweaver/ai-rules.md +180 -0
  6. package/assets/.appweaver/domain.json +1 -0
  7. package/assets/.appweaver/extensions/.registry +19 -0
  8. package/assets/.appweaver/extensions/agent-context/README.md +57 -0
  9. package/assets/.appweaver/extensions/agent-context/agent-context-config.yml +4 -0
  10. package/assets/.appweaver/extensions/agent-context/commands/aw-spec.agent-context.update.md +26 -0
  11. package/assets/.appweaver/extensions/agent-context/extension.yml +37 -0
  12. package/assets/.appweaver/extensions/agent-context/scripts/bash/update-agent-context.sh +201 -0
  13. package/assets/.appweaver/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +237 -0
  14. package/assets/.appweaver/extensions.yml +16 -0
  15. package/assets/.appweaver/feature.json +3 -0
  16. package/assets/.appweaver/init-options.json +9 -0
  17. package/assets/.appweaver/integration.json +15 -0
  18. package/assets/.appweaver/integrations/aw-spec.manifest.json +9 -0
  19. package/assets/.appweaver/interpreter/run-skill.py +1176 -0
  20. package/assets/.appweaver/memory/constitution.md +50 -0
  21. package/assets/.appweaver/orchestrator/.smoke-out/smoke.md +6 -0
  22. package/assets/.appweaver/orchestrator/AGENT-PROFILE.md +94 -0
  23. package/assets/.appweaver/orchestrator/README.md +300 -0
  24. package/assets/.appweaver/orchestrator/adapters/claude_agent_sdk.py +74 -0
  25. package/assets/.appweaver/orchestrator/adapters/google_antigravity.py +74 -0
  26. package/assets/.appweaver/orchestrator/modules/delta-design.orch.yaml +105 -0
  27. package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +101 -0
  28. package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +90 -0
  29. package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +126 -0
  30. package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +123 -0
  31. package/assets/.appweaver/orchestrator/orchestrator.py +1721 -0
  32. package/assets/.appweaver/orchestrator/rev-templates/ai-rules.reference.md +40 -0
  33. package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +101 -0
  34. package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +245 -0
  35. package/assets/.appweaver/orchestrator/rev-templates/domain-constitution.skeleton.md +63 -0
  36. package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +48 -0
  37. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +59 -0
  38. package/assets/.appweaver/orchestrator/rev-templates/domain-rule-file.skeleton.md +39 -0
  39. package/assets/.appweaver/orchestrator/rev-templates/frontend-rule-file.skeleton.md +63 -0
  40. package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +116 -0
  41. package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +106 -0
  42. package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +54 -0
  43. package/assets/.appweaver/orchestrator/rev-templates/ui-constitution.skeleton.md +125 -0
  44. package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +123 -0
  45. package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +294 -0
  46. package/assets/.appweaver/orchestrator/workflows/_smoke.orch.yaml +160 -0
  47. package/assets/.appweaver/orchestrator/workflows/aw-spec-add-component.orch.yaml +270 -0
  48. package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +934 -0
  49. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +251 -0
  50. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +265 -0
  51. package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +437 -0
  52. package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +197 -0
  53. package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +475 -0
  54. package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +181 -0
  55. package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +727 -0
  56. package/assets/.appweaver/orchestrator/workflows/aw-spec-demo.orch.yaml +100 -0
  57. package/assets/.appweaver/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +69 -0
  58. package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +85 -0
  59. package/assets/.appweaver/orchestrator/workflows/aw-spec-design-ui.orch.yaml +386 -0
  60. package/assets/.appweaver/orchestrator/workflows/aw-spec-design.orch.yaml +382 -0
  61. package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +462 -0
  62. package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +265 -0
  63. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +253 -0
  64. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-ui.orch.yaml +267 -0
  65. package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +287 -0
  66. package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +238 -0
  67. package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +1822 -0
  68. package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +293 -0
  69. package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +194 -0
  70. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +83 -0
  71. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +128 -0
  72. package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +296 -0
  73. package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +439 -0
  74. package/assets/.appweaver/scripts/bash/aw-inventory.sh +17 -0
  75. package/assets/.appweaver/scripts/bash/aw-parity.sh +13 -0
  76. package/assets/.appweaver/scripts/bash/aw-rev-constitution.sh +13 -0
  77. package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +2626 -0
  78. package/assets/.appweaver/scripts/bash/aw-xref.sh +15 -0
  79. package/assets/.appweaver/scripts/bash/common.sh +533 -0
  80. package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +17 -0
  81. package/assets/.appweaver/scripts/powershell/aw-parity.ps1 +13 -0
  82. package/assets/.appweaver/scripts/powershell/aw-rev-constitution.ps1 +13 -0
  83. package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +2232 -0
  84. package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +15 -0
  85. package/assets/.appweaver/scripts/python/aw-component-catalog.py +121 -0
  86. package/assets/.appweaver/scripts/python/aw-inventory.py +1180 -0
  87. package/assets/.appweaver/scripts/python/aw-parity.py +380 -0
  88. package/assets/.appweaver/scripts/python/aw-rev-constitution.py +329 -0
  89. package/assets/.appweaver/scripts/python/aw-stack.py +1251 -0
  90. package/assets/.appweaver/scripts/python/aw-xref.py +1330 -0
  91. package/assets/.appweaver/scripts/python/validate_okf.py +352 -0
  92. package/assets/.appweaver/stack-profiles/angular.json +16 -0
  93. package/assets/.appweaver/stack-profiles/blazor-wasm.json +17 -0
  94. package/assets/.appweaver/stack-profiles/dotnet-webapi.json +15 -0
  95. package/assets/.appweaver/stack-profiles/go-chi.json +14 -0
  96. package/assets/.appweaver/stack-profiles/java-spring.json +16 -0
  97. package/assets/.appweaver/stack-profiles/nextjs.json +16 -0
  98. package/assets/.appweaver/stack-profiles/node-express.json +15 -0
  99. package/assets/.appweaver/stack-profiles/python-fastapi.json +16 -0
  100. package/assets/.appweaver/stack-profiles/react-vite.json +17 -0
  101. package/assets/.appweaver/stack-profiles/sveltekit.json +16 -0
  102. package/assets/.appweaver/stack-profiles/vue-vite.json +17 -0
  103. package/assets/.appweaver/stacks.json +3 -0
  104. package/assets/.appweaver/templates/constitution-domain-template.md +116 -0
  105. package/assets/.appweaver/templates/constitution-technical-template.md +108 -0
  106. package/assets/.appweaver/templates/constitution-ui-template.md +116 -0
  107. package/assets/.appweaver/templates/delta-design-template.md +69 -0
  108. package/assets/.appweaver/templates/delta-spec-template.md +63 -0
  109. package/assets/.appweaver/templates/design-template.md +383 -0
  110. package/assets/.appweaver/templates/okf-concept-template.md +114 -0
  111. package/assets/.appweaver/templates/parity-manifest-template.json +49 -0
  112. package/assets/.appweaver/templates/source-map-template.md +85 -0
  113. package/assets/.appweaver/templates/spec-template.md +309 -0
  114. package/assets/.appweaver/templates/ui-design-template.md +273 -0
  115. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.Reference.pack.txt +25248 -0
  116. package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +5726 -0
  117. package/assets/foundations/dotnet/ModularMonolith/pack-foundation.ps1 +145 -0
  118. package/assets/foundations/dotnet/ModularMonolith/pack-foundation.sh +196 -0
  119. package/assets/foundations/dotnet/ModularMonolith/pack-reference-foundation.ps1 +157 -0
  120. package/assets/foundations/dotnet/ModularMonolith/pack-reference-foundation.sh +209 -0
  121. package/assets/foundations/dotnet/ModularMonolith/unpack-foundation.ps1 +126 -0
  122. package/assets/foundations/dotnet/ModularMonolith/unpack-foundation.sh +217 -0
  123. package/assets/foundations/dotnet/ModularMonolith/unpack-reference-foundation.ps1 +129 -0
  124. package/assets/foundations/dotnet/ModularMonolith/unpack-reference-foundation.sh +218 -0
  125. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.Reference.pack.txt +19318 -0
  126. package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2833 -0
  127. package/assets/foundations/dotnet/VerticalSlice/pack-foundation.ps1 +145 -0
  128. package/assets/foundations/dotnet/VerticalSlice/pack-foundation.sh +196 -0
  129. package/assets/foundations/dotnet/VerticalSlice/pack-reference-foundation.ps1 +157 -0
  130. package/assets/foundations/dotnet/VerticalSlice/pack-reference-foundation.sh +209 -0
  131. package/assets/foundations/dotnet/VerticalSlice/unpack-foundation.ps1 +126 -0
  132. package/assets/foundations/dotnet/VerticalSlice/unpack-foundation.sh +217 -0
  133. package/assets/foundations/dotnet/VerticalSlice/unpack-reference-foundation.ps1 +129 -0
  134. package/assets/foundations/dotnet/VerticalSlice/unpack-reference-foundation.sh +218 -0
  135. package/assets/foundations/dotnet/components/Email/README.md +119 -0
  136. package/assets/foundations/dotnet/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +84 -0
  137. package/assets/foundations/dotnet/components/Email/integration.md +300 -0
  138. package/assets/foundations/dotnet/components/Jobs/README.md +113 -0
  139. package/assets/foundations/dotnet/components/Jobs/integration.md +259 -0
  140. package/assets/foundations/dotnet/components/QrGenerator/README.md +85 -0
  141. package/assets/foundations/dotnet/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +57 -0
  142. package/assets/foundations/dotnet/components/QrGenerator/integration.md +230 -0
  143. package/assets/foundations/dotnet/components/README.md +74 -0
  144. package/assets/guardrails/README.md +222 -0
  145. package/assets/guardrails/domain/healthcare/ai-rules.domain.md +34 -0
  146. package/assets/guardrails/domain/healthcare/api-versioning.domain.md +17 -0
  147. package/assets/guardrails/domain/healthcare/auditing.domain.md +97 -0
  148. package/assets/guardrails/domain/healthcare/authorization.domain.md +26 -0
  149. package/assets/guardrails/domain/healthcare/clinical-terms.md +20 -0
  150. package/assets/guardrails/domain/healthcare/coding-standards.domain.md +26 -0
  151. package/assets/guardrails/domain/healthcare/consent.md +19 -0
  152. package/assets/guardrails/domain/healthcare/cqrs.domain.md +12 -0
  153. package/assets/guardrails/domain/healthcare/data-classification.domain.md +28 -0
  154. package/assets/guardrails/domain/healthcare/dependency-policy.domain.md +11 -0
  155. package/assets/guardrails/domain/healthcare/dicom.md +15 -0
  156. package/assets/guardrails/domain/healthcare/domain-constitution.md +119 -0
  157. package/assets/guardrails/domain/healthcare/domain-driven-design.domain.md +28 -0
  158. package/assets/guardrails/domain/healthcare/eventing.domain.md +18 -0
  159. package/assets/guardrails/domain/healthcare/fhir.md +36 -0
  160. package/assets/guardrails/domain/healthcare/folder-structure.domain.md +25 -0
  161. package/assets/guardrails/domain/healthcare/hipaa.md +34 -0
  162. package/assets/guardrails/domain/healthcare/hl7.md +15 -0
  163. package/assets/guardrails/domain/healthcare/ihe-conformance.md +31 -0
  164. package/assets/guardrails/domain/healthcare/integration-terms.md +21 -0
  165. package/assets/guardrails/domain/healthcare/persistence.domain.md +20 -0
  166. package/assets/guardrails/domain/healthcare/terminology.md +30 -0
  167. package/assets/guardrails/domain/healthcare/testing.domain.md +15 -0
  168. package/assets/guardrails/domain/healthcare/ux/accessibility-standards.md +36 -0
  169. package/assets/guardrails/domain/healthcare/ux/content-design.md +32 -0
  170. package/assets/guardrails/domain/healthcare/ux/interaction-states.md +44 -0
  171. package/assets/guardrails/domain/healthcare/ux/journey-mapping.md +40 -0
  172. package/assets/guardrails/domain/healthcare/vertical-slices.domain.md +18 -0
  173. package/assets/guardrails/domain/healthcare/zero-trust.domain.md +12 -0
  174. package/assets/guardrails/domain/pharma/ai-rules.domain.md +32 -0
  175. package/assets/guardrails/domain/pharma/api-versioning.domain.md +20 -0
  176. package/assets/guardrails/domain/pharma/atc.md +22 -0
  177. package/assets/guardrails/domain/pharma/auditing.domain.md +90 -0
  178. package/assets/guardrails/domain/pharma/authorization.domain.md +25 -0
  179. package/assets/guardrails/domain/pharma/coding-standards.domain.md +28 -0
  180. package/assets/guardrails/domain/pharma/cqrs.domain.md +16 -0
  181. package/assets/guardrails/domain/pharma/data-classification.domain.md +24 -0
  182. package/assets/guardrails/domain/pharma/data-provenance.md +21 -0
  183. package/assets/guardrails/domain/pharma/dependency-policy.domain.md +11 -0
  184. package/assets/guardrails/domain/pharma/domain-constitution.md +113 -0
  185. package/assets/guardrails/domain/pharma/domain-driven-design.domain.md +36 -0
  186. package/assets/guardrails/domain/pharma/domain-requirements.sample.md +103 -0
  187. package/assets/guardrails/domain/pharma/eventing.domain.md +18 -0
  188. package/assets/guardrails/domain/pharma/fhir.md +31 -0
  189. package/assets/guardrails/domain/pharma/folder-structure.domain.md +26 -0
  190. package/assets/guardrails/domain/pharma/hl7.md +17 -0
  191. package/assets/guardrails/domain/pharma/idmp.md +26 -0
  192. package/assets/guardrails/domain/pharma/integration-terms.md +21 -0
  193. package/assets/guardrails/domain/pharma/ndc.md +28 -0
  194. package/assets/guardrails/domain/pharma/persistence.domain.md +23 -0
  195. package/assets/guardrails/domain/pharma/pharma-terms.md +27 -0
  196. package/assets/guardrails/domain/pharma/product-lifecycle.domain.md +30 -0
  197. package/assets/guardrails/domain/pharma/regulatory-compliance.md +19 -0
  198. package/assets/guardrails/domain/pharma/terminology.md +32 -0
  199. package/assets/guardrails/domain/pharma/testing.domain.md +19 -0
  200. package/assets/guardrails/domain/pharma/vertical-slices.domain.md +17 -0
  201. package/assets/guardrails/domain/pharma/zero-trust.domain.md +12 -0
  202. package/assets/guardrails/projects/dotnet-default-project/domain/domain-requirements.md +58 -0
  203. package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +113 -0
  204. package/assets/guardrails/projects/dotnet-default-project/frontend/ui-requirements.md +43 -0
  205. package/assets/guardrails/projects/dotnet-default-project/service/technical-requirements.md +40 -0
  206. package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +263 -0
  207. package/assets/guardrails/requirement-spec.txt +27 -0
  208. package/assets/guardrails/technology/dotnet-webapi/PACKAGING.md +184 -0
  209. package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +44 -0
  210. package/assets/guardrails/technology/dotnet-webapi/auditing.md +96 -0
  211. package/assets/guardrails/technology/dotnet-webapi/authorization-matrix.md +14 -0
  212. package/assets/guardrails/technology/dotnet-webapi/authorization.md +39 -0
  213. package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +54 -0
  214. package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +87 -0
  215. package/assets/guardrails/technology/dotnet-webapi/cqrs.md +63 -0
  216. package/assets/guardrails/technology/dotnet-webapi/data-classification.md +38 -0
  217. package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +40 -0
  218. package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +91 -0
  219. package/assets/guardrails/technology/dotnet-webapi/eventing.md +57 -0
  220. package/assets/guardrails/technology/dotnet-webapi/examples/api-versioning.md +75 -0
  221. package/assets/guardrails/technology/dotnet-webapi/examples/auditing.md +84 -0
  222. package/assets/guardrails/technology/dotnet-webapi/examples/authorization.md +68 -0
  223. package/assets/guardrails/technology/dotnet-webapi/examples/clean-architecture.md +37 -0
  224. package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +97 -0
  225. package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +141 -0
  226. package/assets/guardrails/technology/dotnet-webapi/examples/eventing.md +46 -0
  227. package/assets/guardrails/technology/dotnet-webapi/examples/persistence.md +207 -0
  228. package/assets/guardrails/technology/dotnet-webapi/examples/vertical-slices.md +151 -0
  229. package/assets/guardrails/technology/dotnet-webapi/examples/zero-trust.md +48 -0
  230. package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +61 -0
  231. package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +47 -0
  232. package/assets/guardrails/technology/dotnet-webapi/persistence.md +82 -0
  233. package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +162 -0
  234. package/assets/guardrails/technology/dotnet-webapi/testing.md +55 -0
  235. package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +70 -0
  236. package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +48 -0
  237. package/assets/guardrails/technology/java-spring/README.md +62 -0
  238. package/assets/guardrails/technology/nextjs/accessibility-implementation.md +56 -0
  239. package/assets/guardrails/technology/nextjs/api-client.md +56 -0
  240. package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +174 -0
  241. package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +203 -0
  242. package/assets/guardrails/technology/nextjs/auth-session.md +68 -0
  243. package/assets/guardrails/technology/nextjs/client-security.md +69 -0
  244. package/assets/guardrails/technology/nextjs/component-architecture.md +60 -0
  245. package/assets/guardrails/technology/nextjs/component-conventions.md +58 -0
  246. package/assets/guardrails/technology/nextjs/design-system.md +60 -0
  247. package/assets/guardrails/technology/nextjs/error-handling.md +72 -0
  248. package/assets/guardrails/technology/nextjs/forms-and-validation.md +75 -0
  249. package/assets/guardrails/technology/nextjs/frontend-testing.md +94 -0
  250. package/assets/guardrails/technology/nextjs/i18n.md +57 -0
  251. package/assets/guardrails/technology/nextjs/observability.md +67 -0
  252. package/assets/guardrails/technology/nextjs/performance-budgets.md +66 -0
  253. package/assets/guardrails/technology/nextjs/routing-navigation.md +52 -0
  254. package/assets/guardrails/technology/nextjs/state-management.md +70 -0
  255. package/assets/guardrails/technology/nextjs/ui-constitution.md +166 -0
  256. package/dist/commands/init.js +2 -2
  257. package/dist/commands/init.js.map +1 -1
  258. package/dist/utils/copy-assets.js +8 -8
  259. package/dist/utils/copy-assets.js.map +1 -1
  260. package/package.json +1 -1
  261. package/assets/docs/pharma-constitution.my-ref.md +0 -86
@@ -0,0 +1,1176 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ run-skill.py — reference interpreter for aw-spec declarative skills
4
+ (dialect: aw-spec.declarative/v1).
5
+
6
+ ╔══════════════════════════════════════════════════════════════════════════════╗
7
+ ║ RETIRED AS AN ENTRY POINT — BUT DO NOT DELETE THIS FILE. ║
8
+ ║ ║
9
+ ║ The declarative tier it drove (the `aw-spec-decl-*` skills across .claude/, ║
10
+ ║ .agents/ and .kilocode/) was removed 2026-08-03; nothing launches this ║
11
+ ║ interpreter any more. It survives for ONE reason: ║
12
+ ║ ║
13
+ ║ .appweaver/orchestrator/orchestrator.py imports this module at load time ║
14
+ ║ for its YAML-subset loader (`load_yaml`) — see orchestrator.py ~L112. ║
15
+ ║ ║
16
+ ║ Deleting this file makes EVERY orchestrator workflow fail with ║
17
+ ║ FileNotFoundError before a single step runs. Fold `load_yaml` into ║
18
+ ║ orchestrator.py first if you want this gone. ║
19
+ ╚══════════════════════════════════════════════════════════════════════════════╝
20
+
21
+ This was the "no-prompt" execution path: it PARSES a skill.yaml and EXECUTES the
22
+ `workflow:` itself. The model is invoked in exactly one place — the `llm_generate`
23
+ action — and only to author content. Everything else (loading context, verifying,
24
+ running the shared scripts, dispatching hooks, interactive selection) is done here.
25
+
26
+ Stdlib only — no PyYAML dependency. A compact YAML-subset loader (loader tuned to
27
+ the dialect) is bundled below and validated against a full YAML parser in tests.
28
+
29
+ Usage:
30
+ run-skill.py <skill.yaml> [--input "<user input>"] [--select name=value ...]
31
+ [--llm mock|command:<cmd>|agent] [--os posix|windows] [--dry-run] [--json]
32
+ run-skill.py <skill.yaml> --validate [--json] # lint without executing
33
+
34
+ LLM backends:
35
+ --llm mock (default) writes a deterministic placeholder file; no model call.
36
+ --llm command:<cmd> pipes the resolved prompt on stdin to <cmd>; stdout is the content.
37
+ --llm agent defers each llm_generate to the CALLING agent (e.g. Claude):
38
+ the interpreter records the request in `pending_generation`
39
+ and continues; the agent authors the content afterward.
40
+ """
41
+ import argparse
42
+ import json
43
+ import os
44
+ import re
45
+ import subprocess
46
+ import sys
47
+
48
+ # ─────────────────────────────────────────────────────────────────────────────
49
+ # Minimal YAML-subset loader
50
+ #
51
+ # Supports the constructs the dialect uses: nested block mappings, block
52
+ # sequences (items may sit at the parent key's indent), inline flow maps/lists,
53
+ # literal block scalars ("|"), quoted/plain scalars, comments, bool/int/null.
54
+ # Not a general YAML parser — intentionally small and predictable.
55
+ # ─────────────────────────────────────────────────────────────────────────────
56
+ class YamlError(Exception):
57
+ pass
58
+
59
+
60
+ def _indent(line):
61
+ return len(line) - len(line.lstrip(" "))
62
+
63
+
64
+ def _strip_comment(s):
65
+ """Strip a trailing ' #...' comment that is outside quotes/flow."""
66
+ out, i, depth = [], 0, 0
67
+ quote = None
68
+ while i < len(s):
69
+ c = s[i]
70
+ if quote:
71
+ out.append(c)
72
+ if c == quote:
73
+ quote = None
74
+ elif c in ("'", '"'):
75
+ quote = c
76
+ out.append(c)
77
+ elif c in "[{":
78
+ depth += 1
79
+ out.append(c)
80
+ elif c in "]}":
81
+ depth = max(0, depth - 1)
82
+ out.append(c)
83
+ elif c == "#" and depth == 0 and (i == 0 or s[i - 1] == " "):
84
+ break
85
+ else:
86
+ out.append(c)
87
+ i += 1
88
+ return "".join(out).rstrip()
89
+
90
+
91
+ def _scalar(tok):
92
+ tok = tok.strip()
93
+ if tok == "" or tok == "~" or tok.lower() == "null":
94
+ return None
95
+ if len(tok) >= 2 and tok[0] == tok[-1] and tok[0] in "'\"":
96
+ return tok[1:-1]
97
+ low = tok.lower()
98
+ if low == "true":
99
+ return True
100
+ if low == "false":
101
+ return False
102
+ if re.fullmatch(r"-?\d+", tok):
103
+ return int(tok)
104
+ if re.fullmatch(r"-?\d+\.\d+", tok):
105
+ return float(tok)
106
+ return tok
107
+
108
+
109
+ def _parse_flow(s):
110
+ """Parse a JSON-ish flow scalar: {a: b, c: [x, y]} / [a, b] / plain.
111
+
112
+ A flow collection lives ENTIRELY on the line it opens on: this parser is
113
+ handed one line's text and never reads ahead. An unterminated `[`/`{` is
114
+ therefore a syntax error and is raised as one — silently returning the
115
+ partial collection would truncate the value AND swallow every line the
116
+ caller has not consumed yet, which is invisible at every later stage
117
+ (a `--validate` pass lints whatever survived the parse, not what was written).
118
+ """
119
+ s = s.strip()
120
+ pos = [0]
121
+
122
+ def skip_ws():
123
+ while pos[0] < len(s) and s[pos[0]] in " \t":
124
+ pos[0] += 1
125
+
126
+ def parse_value():
127
+ skip_ws()
128
+ if pos[0] >= len(s):
129
+ return None
130
+ c = s[pos[0]]
131
+ if c == "{":
132
+ return parse_map()
133
+ if c == "[":
134
+ return parse_list()
135
+ return _scalar(read_until(",]}"))
136
+
137
+ def read_until(stops):
138
+ start = pos[0]
139
+ quote = None
140
+ while pos[0] < len(s):
141
+ ch = s[pos[0]]
142
+ if quote:
143
+ if ch == quote:
144
+ quote = None
145
+ elif ch in "'\"":
146
+ quote = ch
147
+ elif ch in stops:
148
+ break
149
+ pos[0] += 1
150
+ return s[start:pos[0]]
151
+
152
+ def parse_map():
153
+ obj = {}
154
+ pos[0] += 1 # {
155
+ skip_ws()
156
+ if pos[0] < len(s) and s[pos[0]] == "}":
157
+ pos[0] += 1
158
+ return obj
159
+ while True:
160
+ skip_ws()
161
+ key = read_until(":").strip()
162
+ if pos[0] < len(s) and s[pos[0]] == ":":
163
+ pos[0] += 1
164
+ val = parse_value()
165
+ obj[_scalar(key)] = val
166
+ skip_ws()
167
+ if pos[0] < len(s) and s[pos[0]] == ",":
168
+ pos[0] += 1
169
+ continue
170
+ if pos[0] < len(s) and s[pos[0]] == "}":
171
+ pos[0] += 1
172
+ break
173
+ raise YamlError(
174
+ "unterminated flow mapping — a '{' must close with '}' on the "
175
+ f"SAME line; put the whole mapping on one line: {s!r}")
176
+ return obj
177
+
178
+ def parse_list():
179
+ arr = []
180
+ pos[0] += 1 # [
181
+ skip_ws()
182
+ if pos[0] < len(s) and s[pos[0]] == "]":
183
+ pos[0] += 1
184
+ return arr
185
+ while True:
186
+ arr.append(parse_value())
187
+ skip_ws()
188
+ if pos[0] < len(s) and s[pos[0]] == ",":
189
+ pos[0] += 1
190
+ skip_ws()
191
+ if pos[0] < len(s) and s[pos[0]] == "]": # tolerated trailing comma
192
+ pos[0] += 1
193
+ break
194
+ continue
195
+ if pos[0] < len(s) and s[pos[0]] == "]":
196
+ pos[0] += 1
197
+ break
198
+ raise YamlError(
199
+ "unterminated flow sequence — a '[' must close with ']' on the "
200
+ f"SAME line; put the whole sequence on one line: {s!r}")
201
+ return arr
202
+
203
+ if s and s[0] in "[{":
204
+ return parse_value()
205
+ return _scalar(s)
206
+
207
+
208
+ class _Lines:
209
+ def __init__(self, text):
210
+ self.raw = text.splitlines()
211
+ self.i = 0
212
+
213
+ def peek(self):
214
+ while self.i < len(self.raw):
215
+ line = self.raw[self.i]
216
+ stripped = line.strip()
217
+ if stripped == "" or stripped.startswith("#"):
218
+ self.i += 1
219
+ continue
220
+ return line
221
+ return None
222
+
223
+ def next(self):
224
+ line = self.peek()
225
+ if line is not None:
226
+ self.i += 1
227
+ return line
228
+
229
+
230
+ def _parse_block(lines, min_indent):
231
+ line = lines.peek()
232
+ if line is None or _indent(line) < min_indent:
233
+ return None
234
+ indent = _indent(line)
235
+ if line.strip().startswith("- "):
236
+ return _parse_seq(lines, indent)
237
+ return _parse_map(lines, indent)
238
+
239
+
240
+ def _parse_map(lines, indent):
241
+ obj = {}
242
+ while True:
243
+ line = lines.peek()
244
+ if line is None or _indent(line) != indent or line.strip().startswith("- "):
245
+ break
246
+ content = _strip_comment(line.strip())
247
+ m = re.match(r"^(.*?):(?:\s+(.*))?$", content)
248
+ if not m:
249
+ raise YamlError(f"bad mapping line: {line!r}")
250
+ key = _scalar(m.group(1))
251
+ rest = m.group(2)
252
+ lines.next()
253
+ if rest is None or rest == "":
254
+ child = _parse_block(lines, indent + 1)
255
+ obj[key] = child
256
+ elif rest in ("|", "|-", "|+", ">", ">-", ">+"):
257
+ obj[key] = _parse_block_scalar(lines, indent, folded=rest[0] == ">",
258
+ chomp=(rest[1:] or ""))
259
+ elif rest[0] in "[{":
260
+ obj[key] = _parse_flow(rest)
261
+ else:
262
+ obj[key] = _scalar(rest)
263
+ return obj
264
+
265
+
266
+ def _parse_seq(lines, indent):
267
+ arr = []
268
+ while True:
269
+ line = lines.peek()
270
+ if line is None or _indent(line) != indent or not line.strip().startswith("- "):
271
+ break
272
+ content = _strip_comment(line.strip()[2:])
273
+ lines.next()
274
+ if content == "":
275
+ arr.append(_parse_block(lines, indent + 1))
276
+ elif content[0] in "[{":
277
+ arr.append(_parse_flow(content))
278
+ elif re.match(r"^.*?:(?:\s+.*)?$", content) and ":" in content:
279
+ # inline map entry starting on the "- " line; parse it + following keys
280
+ m = re.match(r"^(.*?):(?:\s+(.*))?$", content)
281
+ item = {}
282
+ key = _scalar(m.group(1))
283
+ rest = m.group(2)
284
+ if rest is None or rest == "":
285
+ item[key] = _parse_block(lines, indent + 2)
286
+ elif rest[0] in "[{":
287
+ item[key] = _parse_flow(rest)
288
+ else:
289
+ item[key] = _scalar(rest)
290
+ # subsequent sibling keys at indent+2
291
+ sub = _parse_map(lines, indent + 2)
292
+ if sub:
293
+ item.update(sub)
294
+ arr.append(item)
295
+ else:
296
+ arr.append(_scalar(content))
297
+ return arr
298
+
299
+
300
+ def load_yaml(text):
301
+ lines = _Lines(text)
302
+ result = _parse_block(lines, 0)
303
+ return result if result is not None else {}
304
+
305
+
306
+ def _parse_block_scalar(lines, parent_indent, folded, chomp):
307
+ """Collect a block scalar ('|' literal or '>' folded) with chomping.
308
+
309
+ chomp: '' clip (single trailing \\n), '-' strip, '+' keep all trailing \\n.
310
+ """
311
+ block, block_indent = [], None
312
+ while lines.i < len(lines.raw):
313
+ raw = lines.raw[lines.i]
314
+ if raw.strip() == "":
315
+ block.append("")
316
+ lines.i += 1
317
+ continue
318
+ if _indent(raw) <= parent_indent:
319
+ break
320
+ if block_indent is None:
321
+ block_indent = _indent(raw)
322
+ block.append(raw[block_indent:])
323
+ lines.i += 1
324
+
325
+ trailing = 0
326
+ while block and block[-1] == "":
327
+ block.pop()
328
+ trailing += 1
329
+
330
+ if folded:
331
+ text = ""
332
+ for i, ln in enumerate(block):
333
+ if i == 0:
334
+ text = ln
335
+ else:
336
+ text += ("\n" if (block[i - 1] == "" or ln == "") else " ") + ln
337
+ else:
338
+ text = "\n".join(block)
339
+
340
+ if chomp == "-":
341
+ return text
342
+ if chomp == "+":
343
+ return text + "\n" * (trailing + 1 if block else trailing)
344
+ return text + "\n" # clip
345
+
346
+
347
+ # ─────────────────────────────────────────────────────────────────────────────
348
+ # Interpreter
349
+ # ─────────────────────────────────────────────────────────────────────────────
350
+ class Stop(Exception):
351
+ pass
352
+
353
+
354
+ class Suspend(Exception):
355
+ """Raised in checkpoint mode when the interpreter needs the agent (generate a
356
+ file / answer a query / ask a question). Carries the typed request."""
357
+ def __init__(self, request):
358
+ self.request = request
359
+
360
+
361
+ class Interp:
362
+ def __init__(self, skill, opts):
363
+ self.skill = skill
364
+ self.opts = opts
365
+ self.vars = {}
366
+ self.context = [] # ordered list of loaded file paths
367
+ self.log = []
368
+ self.pending = [] # deferred llm_generate requests (--llm agent)
369
+ # checkpoint mode: replay + response cache keyed by invocation path
370
+ self.checkpoint = getattr(opts, "checkpoint", None)
371
+ self.cache = getattr(opts, "_cache", {}) or {}
372
+ self.key_stack = [] # loop-aware invocation-key prefix
373
+ # session context ledger: files loaded by earlier commands this session
374
+ self.session_loaded = set(getattr(opts, "_session_loaded", None) or set())
375
+ self.ledger_path = getattr(opts, "_ledger_path", None)
376
+ self.session_id = getattr(opts, "session", None)
377
+
378
+ # -- helpers --------------------------------------------------------------
379
+ def emit(self, msg):
380
+ self.log.append(msg)
381
+ if not self.opts.json:
382
+ print(msg)
383
+
384
+ def _lookup(self, key):
385
+ """Dotted lookup into vars: 'q.text', 'items.0'. Returns (found, value)."""
386
+ parts = key.split(".")
387
+ if parts[0] not in self.vars:
388
+ return (False, None)
389
+ cur = self.vars[parts[0]]
390
+ for pth in parts[1:]:
391
+ if isinstance(cur, dict) and pth in cur:
392
+ cur = cur[pth]
393
+ elif isinstance(cur, list) and pth.isdigit() and int(pth) < len(cur):
394
+ cur = cur[int(pth)]
395
+ else:
396
+ return (False, None)
397
+ return (True, cur)
398
+
399
+ def resolve(self, val):
400
+ """Recursively substitute {{name}} / {{a.b}} in strings using self.vars."""
401
+ if isinstance(val, str):
402
+ def sub(m):
403
+ found, v = self._lookup(m.group(1).strip())
404
+ if not found:
405
+ return m.group(0)
406
+ return v if isinstance(v, str) else json.dumps(v)
407
+ return re.sub(r"\{\{\s*([^}]+?)\s*\}\}", sub, val)
408
+ if isinstance(val, list):
409
+ return [self.resolve(x) for x in val]
410
+ if isinstance(val, dict):
411
+ return {k: self.resolve(v) for k, v in val.items()}
412
+ return val
413
+
414
+ def resolve_value(self, x):
415
+ """Like resolve, but if x is exactly one {{ref}} return the raw object
416
+ (list/dict/bool), not a stringified form."""
417
+ if isinstance(x, str):
418
+ m = re.fullmatch(r"\{\{\s*([^}]+?)\s*\}\}", x.strip())
419
+ if m:
420
+ found, v = self._lookup(m.group(1).strip())
421
+ return v if found else x
422
+ return self.resolve(x)
423
+ return x
424
+
425
+ def truthy(self, v):
426
+ if isinstance(v, str):
427
+ return v.strip() not in ("", "false", "False", "None", "null", "[]")
428
+ return bool(v)
429
+
430
+ def eval_condition(self, expr):
431
+ expr = self.resolve(expr).strip()
432
+ for op in ("==", "!="):
433
+ if op in expr:
434
+ lhs, rhs = [s.strip().strip("'\"") for s in expr.split(op, 1)]
435
+ return (lhs == rhs) if op == "==" else (lhs != rhs)
436
+ return self.truthy(expr)
437
+
438
+ # -- checkpoint helpers ---------------------------------------------------
439
+ def _key(self, step):
440
+ """Stable, loop-aware invocation key for a step (for the replay cache)."""
441
+ return "/".join(self.key_stack + [step.get("id", "?")])
442
+
443
+ def _suspend(self, request):
444
+ """Persist a pending agent-request and raise Suspend."""
445
+ request["response_path"] = self.checkpoint + ".response"
446
+ raise Suspend(request)
447
+
448
+ def script_path(self, ref):
449
+ s = self.skill.get("scripts", {}).get(ref, {})
450
+ variant = "ps1" if self.opts.os == "windows" else "sh"
451
+ return s.get(variant)
452
+
453
+ def act_include(self, step):
454
+ """Run a reusable workflow module inline — shares vars, context, cache, and
455
+ the caller's `scripts:` map. `with:` seeds module inputs; the module's steps
456
+ are namespaced under the include id for stable checkpoint keys."""
457
+ module_path = self.resolve(step.get("module", ""))
458
+ for k, v in (step.get("with", {}) or {}).items():
459
+ self.vars[k] = self.resolve_value(v)
460
+ try:
461
+ with open(module_path, encoding="utf-8") as fh:
462
+ mod = load_yaml(fh.read())
463
+ except OSError as e:
464
+ raise YamlError(f"include: module not found: {module_path} ({e})")
465
+ self.emit(f"[include] {module_path}")
466
+ self.key_stack.append("inc:" + step.get("id", "include"))
467
+ try:
468
+ self._run_steps(mod.get("workflow", []) or [])
469
+ finally:
470
+ self.key_stack.pop()
471
+
472
+ def json_get(self, obj, path):
473
+ if not path.startswith("$."):
474
+ return obj.get(path) if isinstance(obj, dict) else None
475
+ cur = obj
476
+ for part in path[2:].split("."):
477
+ if isinstance(cur, dict):
478
+ cur = cur.get(part)
479
+ else:
480
+ return None
481
+ return cur
482
+
483
+ # -- actions --------------------------------------------------------------
484
+ def run(self):
485
+ self._run_steps(self.skill.get("workflow", []))
486
+ return 0
487
+
488
+ def _run_steps(self, steps):
489
+ """Run a list of steps. Returns 'break' if a break bubbled up, else None."""
490
+ for step in steps:
491
+ cond = step.get("condition")
492
+ if cond is not None and not self.eval_condition(cond):
493
+ self.emit(f"[skip] {step.get('id')} (condition false)")
494
+ continue
495
+ action = step.get("action")
496
+ if action == "break":
497
+ self.emit(f"[break] {step.get('id', '')}")
498
+ return "break"
499
+ if action == "loop":
500
+ self.act_loop(step) # a break inside is consumed by the loop itself
501
+ continue
502
+ fn = getattr(self, f"act_{action}", None)
503
+ if fn is None:
504
+ raise YamlError(f"unknown action: {action}")
505
+ fn(step)
506
+ return None
507
+
508
+ def act_loop(self, step):
509
+ """foreach (`over:` a list var) or while (`while:` + `max_iterations`)."""
510
+ as_name = step.get("as", "item")
511
+ max_it = step.get("max_iterations")
512
+ substeps = step.get("steps", []) or []
513
+ if "over" in step:
514
+ items = self.resolve_value(step["over"]) or []
515
+ if not isinstance(items, list):
516
+ raise YamlError(f"loop 'over' is not a list: {step.get('over')}")
517
+ loop_id = step.get("id", "loop")
518
+ for idx, item in enumerate(items):
519
+ if max_it is not None and idx >= max_it:
520
+ break
521
+ self.vars[as_name] = item
522
+ self.vars[as_name + "_index"] = idx
523
+ self.key_stack.append(f"{loop_id}#{idx}")
524
+ try:
525
+ brk = self._run_steps(substeps)
526
+ finally:
527
+ self.key_stack.pop()
528
+ if brk == "break":
529
+ break # break exits THIS loop (consumed here), not the caller
530
+ return None
531
+ # while variant
532
+ cond = step.get("while", "true")
533
+ loop_id = step.get("id", "loop")
534
+ i = 0
535
+ while self.eval_condition(cond):
536
+ if max_it is not None and i >= max_it:
537
+ break
538
+ self.vars[as_name + "_index"] = i
539
+ self.key_stack.append(f"{loop_id}#{i}")
540
+ try:
541
+ brk = self._run_steps(substeps)
542
+ finally:
543
+ self.key_stack.pop()
544
+ if brk == "break":
545
+ break # break exits THIS while-loop (consumed here)
546
+ i += 1
547
+ return None
548
+
549
+ def _bind_outputs(self, step, data):
550
+ for out in step.get("outputs", []) or []:
551
+ self.vars[out["name"]] = self.json_get(data, out["from"])
552
+
553
+ def act_load_context(self, step):
554
+ p = step.get("parameters", {})
555
+ files = p.get("files") or []
556
+ if p.get("files_from_variable"):
557
+ var = self.vars.get(p["files_from_variable"]) or []
558
+ files = [{"path": x} for x in var]
559
+ step_optional = p.get("optional", False)
560
+ for f in files:
561
+ path = self.resolve(f["path"] if isinstance(f, dict) else f)
562
+ optional = f.get("optional", step_optional) if isinstance(f, dict) else step_optional
563
+ if p.get("deduplicate") and path in self.context:
564
+ self.emit(f"[load_context] dedup skip {path}")
565
+ continue
566
+ # session ledger: a file already loaded by an EARLIER command in this
567
+ # agent session is already in the model's context — don't reload/re-attach.
568
+ if path in self.session_loaded:
569
+ self.emit(f"[load_context] session-dedup skip {path} (loaded earlier this session)")
570
+ continue
571
+ if os.path.exists(path):
572
+ self.context.append(path)
573
+ self.emit(f"[load_context] loaded {path}")
574
+ elif optional:
575
+ self.emit(f"[load_context] optional missing, skip {path}")
576
+ else:
577
+ raise YamlError(f"required context file missing: {path}")
578
+
579
+ def act_verify_context(self, step):
580
+ p = step.get("parameters", {})
581
+ if p.get("require_non_empty"):
582
+ var = p["require_non_empty"]
583
+ val = self.vars.get(var)
584
+ if not val:
585
+ self.emit(f"[verify_context] {var} is empty: {p.get('on_empty_message','')}")
586
+ if p.get("on_empty") == "stop":
587
+ raise Stop(p.get("on_empty_message", "stopped"))
588
+ else:
589
+ self.emit(f"[verify_context] {var} non-empty ({len(val)})")
590
+ if p.get("require_true"):
591
+ var = p["require_true"]
592
+ if not self.truthy(self.vars.get(var)):
593
+ msg = self.resolve(p.get("on_false_message", f"{var} is not satisfied"))
594
+ self.emit(f"[verify_context] gate failed ({var}): {msg}")
595
+ if p.get("on_false", "stop") == "stop":
596
+ raise Stop(msg)
597
+ else:
598
+ self.emit(f"[verify_context] gate ok ({var})")
599
+ if p.get("require_value"):
600
+ var = p["require_value"]
601
+ actual = str(self.vars.get(var, ""))
602
+ expected = str(self.resolve(p.get("equals", "")))
603
+ if actual != expected:
604
+ msg = self.resolve(p.get("on_mismatch_message", f"{var} != {expected}"))
605
+ self.emit(f"[verify_context] value gate failed ({var}={actual!r}): {msg}")
606
+ if p.get("on_mismatch", "stop") == "stop":
607
+ raise Stop(msg)
608
+ else:
609
+ self.emit(f"[verify_context] value gate ok ({var}=={expected})")
610
+ for rf in p.get("required_files", []) or []:
611
+ rf = self.resolve(rf)
612
+ present = rf in self.context or os.path.exists(rf)
613
+ if present:
614
+ self.emit(f"[verify_context] present {rf}")
615
+ elif p.get("allow_missing"):
616
+ self.emit(f"[verify_context] missing (allowed) {rf}")
617
+ else:
618
+ raise YamlError(f"required file not in context: {rf}")
619
+
620
+ def act_execute_script(self, step):
621
+ p = step.get("parameters", {})
622
+ ref = p["script"]["ref"]
623
+ script = self.script_path(ref)
624
+ if not script or not os.path.exists(script):
625
+ raise YamlError(f"script not found for ref '{ref}': {script}")
626
+ args = [self.resolve(a) for a in (p.get("args") or [])]
627
+ # checkpoint replay: reuse cached output, don't re-run the side effect
628
+ ck = self._key(step)
629
+ if self.checkpoint and ck in self.cache:
630
+ self._bind_outputs(step, self.cache[ck])
631
+ self.emit(f"[execute_script] {p['subcommand']} (cached)")
632
+ return
633
+ runner = ["pwsh", "-NoProfile", "-File", script] if self.opts.os == "windows" else ["bash", script]
634
+ cmd = runner + [p["subcommand"]] + args
635
+ self.emit(f"[execute_script] {p['subcommand']} {' '.join(args)}")
636
+ if self.opts.dry_run:
637
+ self._bind_outputs(step, {})
638
+ return
639
+ proc = subprocess.run(cmd, capture_output=True, text=True)
640
+ if proc.returncode != 0:
641
+ raise YamlError(f"script failed ({p['subcommand']}): {proc.stderr.strip()}")
642
+ try:
643
+ data = json.loads(proc.stdout.strip().splitlines()[-1])
644
+ except Exception as e:
645
+ raise YamlError(f"script did not return JSON ({p['subcommand']}): {proc.stdout!r} ({e})")
646
+ self._bind_outputs(step, data)
647
+ if self.checkpoint:
648
+ self.cache[ck] = data
649
+ self.emit(f"[execute_script] -> {json.dumps(data)}")
650
+
651
+ def act_dispatch_hooks(self, step):
652
+ p = step.get("parameters", {})
653
+ # checkpoint replay: don't re-dispatch hooks already handled
654
+ ck = self._key(step)
655
+ if self.checkpoint and ck in self.cache:
656
+ self.emit("[dispatch_hooks] (cached)")
657
+ return
658
+ hooks = self.vars.get(p.get("hooks_from_variable")) or []
659
+ if hooks:
660
+ for h in hooks:
661
+ kind = "mandatory" if not h.get("optional") else "optional"
662
+ self.emit(f"[dispatch_hooks] {kind}: {h.get('slash')} ({h.get('extension')})")
663
+ # An agent runs the slash command here; the interpreter records intent.
664
+ else:
665
+ self.emit("[dispatch_hooks] none")
666
+ if self.checkpoint:
667
+ self.cache[ck] = True
668
+
669
+ def act_user_select(self, step):
670
+ p = step.get("parameters", {})
671
+ options = self.vars.get(p.get("options_from_variable")) or []
672
+ # Direct-argument fast path
673
+ pre = p.get("preselect_from")
674
+ pre_val = self.vars.get(pre) if pre else None
675
+ chosen = None
676
+ if pre_val and pre_val in options:
677
+ chosen = pre_val
678
+ elif self.opts.selects.get(step.get("id")):
679
+ chosen = self.opts.selects[step["id"]]
680
+ elif len(options) == 1 and p.get("single_option_confirm"):
681
+ chosen = options[0]
682
+ elif self.opts.non_interactive:
683
+ chosen = options[0] if options else None
684
+ else:
685
+ chosen = self._prompt_choice(p.get("prompt", "Select:"), options)
686
+ if not chosen:
687
+ raise Stop("selection cancelled")
688
+ self.vars["selection"] = chosen
689
+ for out in step.get("outputs", []) or []:
690
+ src = out["from"]
691
+ self.vars[out["name"]] = chosen if src == "selection" else self.vars.get(src)
692
+ self.emit(f"[user_select] chosen: {chosen}")
693
+
694
+ def _prompt_choice(self, prompt, options):
695
+ print(prompt)
696
+ for i, o in enumerate(options, 1):
697
+ print(f" {i}) {o}")
698
+ try:
699
+ raw = input("> ").strip()
700
+ except EOFError:
701
+ return None
702
+ if raw.isdigit() and 1 <= int(raw) <= len(options):
703
+ return options[int(raw) - 1]
704
+ return raw if raw in options else None
705
+
706
+ def act_ask(self, step):
707
+ """Single or multi-select question. Answer sources, in order:
708
+ --answer <key>=<value> (key = resolved `answer_key`, else step id)
709
+ interactive stdin (unless --non-interactive)
710
+ Binds the answer to `answer_var` (default 'answer')."""
711
+ p = step.get("parameters", {})
712
+ prompt = self.resolve(p.get("prompt", "?"))
713
+ options = []
714
+ if p.get("options_from"):
715
+ options = self.resolve_value(p["options_from"]) or []
716
+ elif p.get("options_from_variable"):
717
+ options = self.vars.get(p["options_from_variable"]) or []
718
+ elif p.get("options"):
719
+ options = [self.resolve(o) for o in p["options"]]
720
+ multi = bool(p.get("multi"))
721
+ key = self.resolve(p["answer_key"]) if p.get("answer_key") else step.get("id", "")
722
+ var = p.get("answer_var", "answer")
723
+
724
+ ck = self._key(step)
725
+ if self.checkpoint and ck in self.cache:
726
+ self.vars[var] = self.cache[ck]
727
+ self.emit(f"[ask] {key} -> {self.cache[ck]} (cached)")
728
+ return
729
+
730
+ raw = self.opts.answers.get(key)
731
+ if raw is None and self.checkpoint:
732
+ # need the agent/user — suspend with an ask request
733
+ self._suspend({"kind": "ask", "key": key, "step": ck,
734
+ "prompt": prompt, "options": options, "multi": multi})
735
+ if raw is None and self.opts.non_interactive:
736
+ raw = ",".join(str(o) for o in options) if multi else (options[0] if options else "")
737
+ if raw is None:
738
+ raw = self._prompt_choice(prompt, options) if options else \
739
+ (input(prompt + " ").strip() if not self.opts.non_interactive else "")
740
+ if raw is None:
741
+ raise Stop("question cancelled")
742
+ # multi-select: split on commas and DROP empties, so "" => [] (empty selection)
743
+ answer = [s.strip() for s in str(raw).split(",") if s.strip()] if multi else raw
744
+ self.vars[var] = answer
745
+ if self.checkpoint:
746
+ self.cache[ck] = answer
747
+ self.emit(f"[ask] {key} -> {answer}")
748
+
749
+ def act_set(self, step):
750
+ """Set variables from literals/expressions (parameters: {name: value})."""
751
+ for k, v in (step.get("parameters", {}) or {}).items():
752
+ self.vars[k] = self.resolve_value(v)
753
+ self.emit(f"[set] {list((step.get('parameters') or {}).keys())}")
754
+
755
+ def act_llm_query(self, step):
756
+ """In-run LLM call that returns STRUCTURED JSON bound to vars via outputs[].
757
+ Requires a synchronous backend (mock/command); not available with --llm agent."""
758
+ p = step.get("parameters", {})
759
+ ck = self._key(step)
760
+ if self.checkpoint and ck in self.cache:
761
+ self._bind_outputs(step, self.cache[ck])
762
+ self.emit(f"[llm_query] (cached) -> {json.dumps(self.cache[ck])[:120]}")
763
+ return
764
+ prompt = self.resolve(p.get("prompt", ""))
765
+ ctx_files = [self.resolve_value(c) for c in (p.get("context") or [])]
766
+ ctx_files = [c for c in ctx_files if isinstance(c, str)]
767
+ if p.get("include_loaded_context", True):
768
+ for c in self.context:
769
+ if c not in ctx_files:
770
+ ctx_files.append(c)
771
+ if self.checkpoint:
772
+ self._suspend({"kind": "llm_query", "step": ck,
773
+ "prompt": prompt, "context": ctx_files})
774
+ if self.opts.llm == "agent":
775
+ raise YamlError("llm_query needs a synchronous --llm backend "
776
+ "(mock/command:...) or --checkpoint, not --llm agent")
777
+ raw = self._invoke_llm(prompt, ctx_files, None)
778
+ try:
779
+ data = json.loads(raw.strip().splitlines()[-1] if raw.strip() else "{}")
780
+ except Exception as e:
781
+ raise YamlError(f"llm_query did not return JSON: {raw!r} ({e})")
782
+ self._bind_outputs(step, data)
783
+ self.emit(f"[llm_query] -> {json.dumps(data)[:200]}")
784
+
785
+ def act_llm_generate(self, step):
786
+ p = step.get("parameters", {})
787
+ prompt = self.resolve(p.get("prompt", ""))
788
+ ctx_files = [self.resolve(c) for c in (p.get("context") or [])]
789
+ # Fix #1: augment with everything pulled in by load_context so the standards
790
+ # and domain rules actually reach generation (dedup, order-preserving).
791
+ if p.get("include_loaded_context", True):
792
+ for c in self.context:
793
+ if c not in ctx_files:
794
+ ctx_files.append(c)
795
+ target = self.resolve(p["output_target"]["path"])
796
+ ck = self._key(step)
797
+ # checkpoint replay: rewrite the target from cached content (idempotent)
798
+ if self.checkpoint and ck in self.cache:
799
+ os.makedirs(os.path.dirname(target) or ".", exist_ok=True)
800
+ with open(target, "w", encoding="utf-8") as fh:
801
+ fh.write(self.cache[ck])
802
+ self.emit(f"[llm_generate] (cached) rewrote {target}")
803
+ return
804
+ self.emit(f"[llm_generate] context={ctx_files} -> {target}")
805
+ if self.checkpoint:
806
+ self._suspend({"kind": "llm_generate", "step": ck, "target": target,
807
+ "prompt": prompt, "context": ctx_files})
808
+ if self.opts.llm == "agent":
809
+ # Defer generation to the calling agent (e.g. Claude). The interpreter
810
+ # does NOT write the file; it records the request and continues the
811
+ # remaining deterministic steps. The agent fulfills these afterward.
812
+ self.pending.append({"target": target, "context": ctx_files, "prompt": prompt})
813
+ self.emit("[llm_generate] deferred to agent (see pending_generation)")
814
+ return
815
+ content = self._invoke_llm(prompt, ctx_files, target)
816
+ if self.opts.dry_run:
817
+ self.emit("[llm_generate] dry-run: not writing")
818
+ return
819
+ os.makedirs(os.path.dirname(target) or ".", exist_ok=True)
820
+ with open(target, "w", encoding="utf-8") as fh:
821
+ fh.write(content)
822
+ self.emit(f"[llm_generate] wrote {len(content)} chars")
823
+
824
+ def _invoke_llm(self, prompt, ctx_files, target):
825
+ backend = self.opts.llm
826
+ if backend.startswith("command:"):
827
+ cmd = backend[len("command:"):]
828
+ ctx_blob = ""
829
+ for c in ctx_files:
830
+ if os.path.exists(c):
831
+ ctx_blob += f"\n\n===== CONTEXT: {c} =====\n" + open(c, encoding="utf-8").read()
832
+ proc = subprocess.run(cmd, shell=True, input=prompt + ctx_blob,
833
+ capture_output=True, text=True)
834
+ if proc.returncode != 0:
835
+ raise YamlError(f"llm command failed: {proc.stderr.strip()}")
836
+ return proc.stdout
837
+ # mock backend — deterministic, no model call
838
+ return (
839
+ "<!-- generated by run-skill.py mock LLM backend -->\n"
840
+ f"<!-- target: {target} -->\n"
841
+ f"<!-- context files: {', '.join(ctx_files)} -->\n"
842
+ f"<!-- prompt chars: {len(prompt)} -->\n\n"
843
+ "# (mock content)\n\nReplace --llm mock with a real backend to author this file.\n"
844
+ )
845
+
846
+
847
+ # ─────────────────────────────────────────────────────────────────────────────
848
+ # Static validator (--validate): lints a skill.yaml without executing it.
849
+ # ─────────────────────────────────────────────────────────────────────────────
850
+ KNOWN_ACTIONS = {
851
+ "load_context", "verify_context", "execute_script",
852
+ "dispatch_hooks", "user_select", "llm_generate",
853
+ "loop", "ask", "llm_query", "set", "break", "include",
854
+ }
855
+ _SPECIAL_VARS = {"selection", "answer"}
856
+
857
+
858
+ def _refs_in(v):
859
+ found = set()
860
+ if isinstance(v, str):
861
+ for m in re.findall(r"\{\{\s*([^}]+?)\s*\}\}", v):
862
+ found.add(m.strip())
863
+ elif isinstance(v, list):
864
+ for x in v:
865
+ found |= _refs_in(x)
866
+ elif isinstance(v, dict):
867
+ for x in v.values():
868
+ found |= _refs_in(x)
869
+ return found
870
+
871
+
872
+ def validate_skill(skill, base_dir):
873
+ errors, warnings = [], []
874
+ if not isinstance(skill, dict):
875
+ return (["top-level YAML is not a mapping"], [])
876
+
877
+ wf = skill.get("workflow")
878
+ if not isinstance(wf, list) or not wf:
879
+ errors.append("missing or empty 'workflow' list")
880
+ wf = wf if isinstance(wf, list) else []
881
+ scripts = skill.get("scripts", {}) or {}
882
+
883
+ def walk(steps):
884
+ for s in steps or []:
885
+ if isinstance(s, dict):
886
+ yield s
887
+ if s.get("action") == "loop":
888
+ yield from walk(s.get("steps"))
889
+
890
+ produced = set(_SPECIAL_VARS)
891
+ for _, spec in (skill.get("inputs") or {}).items():
892
+ if isinstance(spec, dict) and spec.get("bind"):
893
+ produced.add(spec["bind"])
894
+ # pre-scan everything that produces variables (recursively) so ordering /
895
+ # forward references don't false-alarm
896
+ for step in walk(wf):
897
+ for o in step.get("outputs") or []:
898
+ if isinstance(o, dict) and o.get("name"):
899
+ produced.add(o["name"])
900
+ p = step.get("parameters", {}) or {}
901
+ act = step.get("action")
902
+ if act == "ask":
903
+ produced.add(p.get("answer_var", "answer"))
904
+ elif act == "set":
905
+ produced.update((step.get("parameters") or {}).keys())
906
+ elif act == "loop" and step.get("as"):
907
+ produced.add(step["as"])
908
+ produced.add(step["as"] + "_index")
909
+ elif act == "include":
910
+ # vars the module seeds + produces become available to the caller
911
+ produced.update((step.get("with") or {}).keys())
912
+ mpath = step.get("module", "")
913
+ for cand in (mpath, os.path.join(base_dir, mpath)):
914
+ if mpath and os.path.exists(cand):
915
+ try:
916
+ mwf = load_yaml(open(cand, encoding="utf-8").read()).get("workflow", [])
917
+ for ms in walk(mwf):
918
+ for o in ms.get("outputs") or []:
919
+ if isinstance(o, dict) and o.get("name"):
920
+ produced.add(o["name"])
921
+ mp = ms.get("parameters", {}) or {}
922
+ if ms.get("action") == "ask":
923
+ produced.add(mp.get("answer_var", "answer"))
924
+ elif ms.get("action") == "set":
925
+ produced.update((ms.get("parameters") or {}).keys())
926
+ except Exception:
927
+ pass
928
+ break
929
+
930
+ seen_ids = set()
931
+ for step in walk(wf):
932
+ loc = step.get("id") or "step"
933
+ sid = step.get("id")
934
+ if not sid:
935
+ warnings.append(f"{loc}: missing id")
936
+ elif sid in seen_ids:
937
+ errors.append(f"duplicate step id: {sid}")
938
+ else:
939
+ seen_ids.add(sid)
940
+
941
+ action = step.get("action")
942
+ if action not in KNOWN_ACTIONS:
943
+ errors.append(f"{loc}: unknown action '{action}' (known: {', '.join(sorted(KNOWN_ACTIONS))})")
944
+ p = step.get("parameters", {}) or {}
945
+
946
+ if action == "loop" and "over" not in step and "while" not in step:
947
+ errors.append(f"{loc}: loop needs 'over:' or 'while:'")
948
+ if action == "ask" and not (p.get("options_from") or p.get("options_from_variable") or p.get("options")):
949
+ warnings.append(f"{loc}: ask has no options source (free-text input)")
950
+ if action == "include":
951
+ m = step.get("module")
952
+ if not m:
953
+ errors.append(f"{loc}: include needs 'module'")
954
+ elif not (os.path.exists(m) or os.path.exists(os.path.join(base_dir, m))):
955
+ warnings.append(f"{loc}: include module not found on disk: {m}")
956
+ if action == "llm_query" and not (step.get("outputs")):
957
+ warnings.append(f"{loc}: llm_query has no outputs[] to bind")
958
+
959
+ if action == "execute_script":
960
+ ref = (p.get("script") or {}).get("ref")
961
+ if not ref:
962
+ errors.append(f"{loc}: execute_script needs parameters.script.ref")
963
+ elif ref not in scripts:
964
+ errors.append(f"{loc}: script ref '{ref}' not declared in top-level scripts:")
965
+ else:
966
+ variants = scripts.get(ref) or {}
967
+ found = False
968
+ for v in ("sh", "ps1"):
969
+ path = variants.get(v)
970
+ if path and (os.path.exists(path) or os.path.exists(os.path.join(base_dir, path))):
971
+ found = True
972
+ if not found:
973
+ warnings.append(f"{loc}: no script variant file found on disk for ref '{ref}'")
974
+ if not p.get("subcommand"):
975
+ errors.append(f"{loc}: execute_script needs parameters.subcommand")
976
+
977
+ if action == "user_select":
978
+ if not p.get("options_from_variable"):
979
+ errors.append(f"{loc}: user_select needs parameters.options_from_variable")
980
+
981
+ if action == "llm_generate":
982
+ if not p.get("prompt"):
983
+ warnings.append(f"{loc}: llm_generate has an empty prompt")
984
+ if not (p.get("output_target") or {}).get("path"):
985
+ errors.append(f"{loc}: llm_generate needs parameters.output_target.path")
986
+
987
+ used = _refs_in(wf) | _refs_in(skill.get("outputs", {}))
988
+ for name in sorted(used):
989
+ root = name.split(".")[0]
990
+ if root not in produced:
991
+ warnings.append(f"variable {{{{{name}}}}} referenced but never produced")
992
+
993
+ return errors, warnings
994
+
995
+
996
+ def main(argv=None):
997
+ ap = argparse.ArgumentParser(description="aw-spec declarative skill interpreter")
998
+ ap.add_argument("skill")
999
+ ap.add_argument("--input", default="")
1000
+ ap.add_argument("--select", action="append", default=[], help="stepid=value")
1001
+ ap.add_argument("--answer", action="append", default=[], help="key=value (for ask steps)")
1002
+ ap.add_argument("--llm", default="mock")
1003
+ ap.add_argument("--os", choices=["posix", "windows"],
1004
+ default="windows" if os.name == "nt" else "posix")
1005
+ ap.add_argument("--dry-run", action="store_true")
1006
+ ap.add_argument("--non-interactive", action="store_true")
1007
+ ap.add_argument("--validate", action="store_true",
1008
+ help="lint the skill (parse + static checks) without executing")
1009
+ ap.add_argument("--checkpoint", metavar="STATEFILE",
1010
+ help="suspend/resume mode: persist state to STATEFILE and "
1011
+ "suspend when the agent is needed (see docs)")
1012
+ ap.add_argument("--session", default=os.environ.get("AW_SPEC_SESSION"),
1013
+ help="agent session id (or $AW_SPEC_SESSION); enables cross-command "
1014
+ "context dedup via .appweaver/.context-ledger.json")
1015
+ ap.add_argument("--session-reset", action="store_true",
1016
+ help="clear the session context ledger and exit")
1017
+ ap.add_argument("--json", action="store_true")
1018
+ opts = ap.parse_args(argv)
1019
+ opts.selects = dict(s.split("=", 1) for s in opts.select if "=" in s)
1020
+ opts.answers = dict(s.split("=", 1) for s in opts.answer if "=" in s)
1021
+
1022
+ # ---- session context ledger --------------------------------------------
1023
+ # Files loaded to context by an earlier command in the SAME agent session are
1024
+ # already in the model's window; later commands skip re-loading / re-attaching
1025
+ # them. Opt-in via --session / $AW_SPEC_SESSION (safe no-op when unset — e.g.
1026
+ # stateless headless runs where each model call has no memory).
1027
+ LEDGER = ".appweaver/.context-ledger.json"
1028
+ if opts.session_reset:
1029
+ try:
1030
+ os.remove(LEDGER)
1031
+ msg = "context ledger reset"
1032
+ except FileNotFoundError:
1033
+ msg = "no context ledger to reset"
1034
+ print(json.dumps({"status": "ok", "message": msg}) if opts.json else msg)
1035
+ return 0
1036
+ opts._ledger_path = None
1037
+ opts._session_loaded = set()
1038
+ if opts.session:
1039
+ opts._ledger_path = LEDGER
1040
+ if os.path.exists(LEDGER):
1041
+ try:
1042
+ led = json.load(open(LEDGER, encoding="utf-8"))
1043
+ # only reuse the ledger when it belongs to THIS session id
1044
+ if led.get("session") == opts.session:
1045
+ opts._session_loaded = set(led.get("loaded", []))
1046
+ except Exception:
1047
+ pass
1048
+
1049
+ # --validate: parse + static checks only, never execute.
1050
+ if opts.validate:
1051
+ try:
1052
+ with open(opts.skill, encoding="utf-8") as fh:
1053
+ skill = load_yaml(fh.read())
1054
+ except (OSError, YamlError) as e:
1055
+ print(json.dumps({"status": "invalid", "errors": [f"parse: {e}"]})
1056
+ if opts.json else f"ERROR: parse: {e}")
1057
+ return 1
1058
+ errors, warnings = validate_skill(skill, os.path.dirname(os.path.abspath(opts.skill)))
1059
+ if opts.json:
1060
+ print(json.dumps({"status": "valid" if not errors else "invalid",
1061
+ "errors": errors, "warnings": warnings}, indent=2))
1062
+ else:
1063
+ for w in warnings:
1064
+ print("WARN: ", w)
1065
+ for e in errors:
1066
+ print("ERROR:", e)
1067
+ print(f"\n{opts.skill}: {len(errors)} error(s), {len(warnings)} warning(s)")
1068
+ return 1 if errors else 0
1069
+
1070
+ with open(opts.skill, encoding="utf-8") as fh:
1071
+ skill = load_yaml(fh.read())
1072
+
1073
+ # ---- checkpoint mode: load prior state + ingest the agent's response ------
1074
+ opts._cache = {}
1075
+ if opts.checkpoint and os.path.exists(opts.checkpoint):
1076
+ with open(opts.checkpoint, encoding="utf-8") as fh:
1077
+ state = json.load(fh)
1078
+ opts._cache = state.get("cache", {})
1079
+ # merge stored inputs; CLI-provided --answer/--input override/extend them
1080
+ if not opts.input:
1081
+ opts.input = state.get("input", "")
1082
+ merged = dict(state.get("answers", {}))
1083
+ merged.update(opts.answers)
1084
+ opts.answers = merged
1085
+ # ingest the response the agent wrote for the pending request
1086
+ pending = state.get("pending")
1087
+ if pending:
1088
+ rp = pending.get("response_path")
1089
+ if rp and os.path.exists(rp):
1090
+ raw = open(rp, encoding="utf-8").read()
1091
+ kind = pending["kind"]
1092
+ if kind == "llm_query":
1093
+ opts._cache[pending["step"]] = json.loads(
1094
+ raw.strip().splitlines()[-1] if raw.strip() else "{}")
1095
+ elif kind == "llm_generate":
1096
+ opts._cache[pending["step"]] = raw
1097
+ elif kind == "ask":
1098
+ val = raw.strip()
1099
+ opts._cache[pending["step"]] = (
1100
+ [s.strip() for s in val.split(",") if s.strip()] if pending.get("multi") else val)
1101
+ os.remove(rp)
1102
+ else:
1103
+ # no response yet — re-emit the same pending request
1104
+ _print_result({"status": "suspended", "pending": pending}, opts)
1105
+ return 2
1106
+
1107
+ interp = Interp(skill, opts)
1108
+ # bind declared inputs
1109
+ for _, spec in (skill.get("inputs") or {}).items():
1110
+ if isinstance(spec, dict) and spec.get("bind"):
1111
+ interp.vars[spec["bind"]] = opts.input
1112
+
1113
+ def _persist_ledger():
1114
+ # Record this run's loaded files into the session ledger — ONLY at a terminal
1115
+ # end (ok/stop), never on suspend (mid-run), so a checkpoint run's own resumes
1116
+ # keep seeing the pre-run snapshot and reload their context consistently.
1117
+ if not opts._ledger_path:
1118
+ return
1119
+ merged = sorted(set(opts._session_loaded) | set(interp.context))
1120
+ try:
1121
+ with open(opts._ledger_path, "w", encoding="utf-8") as fh:
1122
+ json.dump({"session": opts.session, "loaded": merged,
1123
+ "updated_at": __import__("time").strftime("%Y-%m-%dT%H:%M:%S")}, fh)
1124
+ except OSError:
1125
+ pass
1126
+
1127
+ rc = 0
1128
+ try:
1129
+ interp.run()
1130
+ result = {"status": "ok",
1131
+ "outputs": interp.resolve(skill.get("outputs", {})),
1132
+ "pending_generation": interp.pending,
1133
+ "vars": interp.vars}
1134
+ if opts.checkpoint and os.path.exists(opts.checkpoint):
1135
+ os.remove(opts.checkpoint) # finished — clear state
1136
+ _persist_ledger()
1137
+ except Suspend as e:
1138
+ # persist state and emit the request for the agent to fulfill (NOT the ledger)
1139
+ with open(opts.checkpoint, "w", encoding="utf-8") as fh:
1140
+ json.dump({"input": opts.input, "answers": opts.answers,
1141
+ "cache": interp.cache, "pending": e.request}, fh, indent=2)
1142
+ _print_result({"status": "suspended", "pending": e.request}, opts)
1143
+ return 2
1144
+ except Stop as e:
1145
+ result = {"status": "stopped", "reason": str(e), "vars": interp.vars}
1146
+ if opts.checkpoint and os.path.exists(opts.checkpoint):
1147
+ os.remove(opts.checkpoint)
1148
+ _persist_ledger()
1149
+ except YamlError as e:
1150
+ result = {"status": "error", "error": str(e), "vars": interp.vars}
1151
+ rc = 1
1152
+
1153
+ _print_result(result, opts)
1154
+ return rc
1155
+
1156
+
1157
+ def _print_result(result, opts):
1158
+ if opts.json:
1159
+ print(json.dumps(result, indent=2))
1160
+ return
1161
+ print(f"\n=== {result['status'].upper()} ===")
1162
+ if result.get("pending"):
1163
+ print("PENDING (agent action needed):")
1164
+ print(json.dumps(result["pending"], indent=2))
1165
+ if result.get("outputs"):
1166
+ print(json.dumps(result["outputs"], indent=2))
1167
+ if result.get("pending_generation"):
1168
+ print("PENDING_GENERATION:", json.dumps(result["pending_generation"], indent=2))
1169
+ if result.get("reason"):
1170
+ print(result["reason"])
1171
+ if result.get("error"):
1172
+ print("ERROR:", result["error"])
1173
+
1174
+
1175
+ if __name__ == "__main__":
1176
+ sys.exit(main())