@xulthekl/team-flow 0.22.4

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 (405) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.agents/skills +1 -0
  3. package/.claude/always/phase-guard.md +3 -0
  4. package/.claude-plugin/marketplace.json +20 -0
  5. package/.claude-plugin/plugin.json +31 -0
  6. package/.codex-plugin/plugin.json +45 -0
  7. package/.codexignore +6 -0
  8. package/.cursor-plugin/marketplace.json +17 -0
  9. package/.cursor-plugin/plugin.json +31 -0
  10. package/.github/copilot-instructions.md +15 -0
  11. package/.github/dependabot.yml +10 -0
  12. package/.github/plugin/marketplace.json +42 -0
  13. package/.github/workflows/ci.yml +98 -0
  14. package/.github/workflows/hol-plugin-scanner.yml +17 -0
  15. package/.opencode/INSTALL.md +61 -0
  16. package/.opencode/plugins/team-flow.js +70 -0
  17. package/AGENTS.md +280 -0
  18. package/CHANGELOG.md +635 -0
  19. package/CONTRIBUTING.md +115 -0
  20. package/GEMINI.md +14 -0
  21. package/HANDOFF.md +219 -0
  22. package/INSTALL.md +810 -0
  23. package/LICENSE +21 -0
  24. package/README.md +185 -0
  25. package/SECURITY.md +37 -0
  26. package/agents/bug-investigator.md +180 -0
  27. package/agents/change-split-auditor.md +257 -0
  28. package/agents/code-reviewer.md +171 -0
  29. package/agents/cross-change-consistency-checker.md +215 -0
  30. package/agents/prd-completeness-reviewer.md +197 -0
  31. package/agents/prototype-builder.md +230 -0
  32. package/agents/prototype-env-scout.md +156 -0
  33. package/agents/prototype-reviewer.md +226 -0
  34. package/assets/icon.svg +6 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.js +5 -0
  37. package/dist/parsing/change-parser.d.ts +12 -0
  38. package/dist/parsing/change-parser.js +54 -0
  39. package/dist/parsing/requirement-blocks.d.ts +31 -0
  40. package/dist/parsing/requirement-blocks.js +192 -0
  41. package/dist/schema/base.d.ts +7 -0
  42. package/dist/schema/base.js +1 -0
  43. package/dist/schema/change.d.ts +25 -0
  44. package/dist/schema/change.js +1 -0
  45. package/dist/schema/index.d.ts +3 -0
  46. package/dist/schema/index.js +1 -0
  47. package/dist/schema/spec.d.ts +11 -0
  48. package/dist/schema/spec.js +1 -0
  49. package/dist/validation/constants.d.ts +42 -0
  50. package/dist/validation/constants.js +42 -0
  51. package/dist/validation/tokenizer.d.ts +17 -0
  52. package/dist/validation/tokenizer.js +173 -0
  53. package/dist/validation/types.d.ts +40 -0
  54. package/dist/validation/types.js +1 -0
  55. package/dist/validation/validator.d.ts +21 -0
  56. package/dist/validation/validator.js +489 -0
  57. package/docs/README_en.md +358 -0
  58. package/docs/artifact-contract.md +96 -0
  59. package/docs/decision-points.md +88 -0
  60. package/docs/e2e-integration-design.md +207 -0
  61. package/docs/examples/README.md +25 -0
  62. package/docs/examples/add-dark-mode/README.md +13 -0
  63. package/docs/examples/add-dark-mode/design.md +49 -0
  64. package/docs/examples/add-dark-mode/execution-contract.md +56 -0
  65. package/docs/examples/add-dark-mode/proposal.md +46 -0
  66. package/docs/examples/add-dark-mode/specs/ui-theme/spec.md +35 -0
  67. package/docs/examples/add-dark-mode/tasks.md +26 -0
  68. package/docs/examples/refactor-auth-boundary/README.md +15 -0
  69. package/docs/examples/refactor-auth-boundary/design.md +41 -0
  70. package/docs/examples/refactor-auth-boundary/execution-contract.md +61 -0
  71. package/docs/examples/refactor-auth-boundary/proposal.md +47 -0
  72. package/docs/examples/refactor-auth-boundary/specs/auth-boundary/spec.md +45 -0
  73. package/docs/examples/refactor-auth-boundary/tasks.md +25 -0
  74. package/docs/plans/2026-07-07-fix-batch-design.md +68 -0
  75. package/docs/plans/2026-07-20-001-refactor-skill-collaboration-protocol-plan.md +128 -0
  76. package/docs/platform-matrix.md +45 -0
  77. package/docs/prototype-design-research.md +206 -0
  78. package/docs/release-checklist.md +96 -0
  79. package/docs/showcase.html +699 -0
  80. package/docs/skill-rename-v0.8.0.md +49 -0
  81. package/docs/state-machine.md +162 -0
  82. package/gemini-extension.json +6 -0
  83. package/hooks/hooks-cursor.json +8 -0
  84. package/hooks/hooks.json +26 -0
  85. package/hooks/pre-tool-use-guard +116 -0
  86. package/hooks/session-start +14 -0
  87. package/llms.txt +51 -0
  88. package/package.json +48 -0
  89. package/plugin.json +27 -0
  90. package/prd/v1/plan.md +443 -0
  91. package/prd/v1/prd.md +580 -0
  92. package/scripts/check-update.mjs +91 -0
  93. package/scripts/check-version-consistency.mjs +345 -0
  94. package/scripts/ensure-branch.mjs +71 -0
  95. package/scripts/get-config +31 -0
  96. package/scripts/guard/checks/artifacts-exist.mjs +36 -0
  97. package/scripts/guard/checks/contract-current.mjs +33 -0
  98. package/scripts/guard/checks/contract-fresh.mjs +17 -0
  99. package/scripts/guard/checks/dp-gate-passed.mjs +35 -0
  100. package/scripts/guard/checks/dp3-approved.mjs +22 -0
  101. package/scripts/guard/checks/execution-plan-ready.mjs +40 -0
  102. package/scripts/guard/checks/execution-reviews-passed.mjs +36 -0
  103. package/scripts/guard/checks/schema-valid.mjs +63 -0
  104. package/scripts/guard/checks/specs-merged.mjs +51 -0
  105. package/scripts/guard/checks/tasks-complete.mjs +31 -0
  106. package/scripts/guard/checks/tests-passing.mjs +31 -0
  107. package/scripts/guard/design-token-guard.mjs +237 -0
  108. package/scripts/guard/guard.mjs +205 -0
  109. package/scripts/infer-workflow.mjs +136 -0
  110. package/scripts/install-amazon-q.mjs +9 -0
  111. package/scripts/install-cline.mjs +9 -0
  112. package/scripts/install-continue.mjs +9 -0
  113. package/scripts/install-cursor.mjs +265 -0
  114. package/scripts/install-git-hooks.mjs +82 -0
  115. package/scripts/install-kiro.mjs +9 -0
  116. package/scripts/install-pi.mjs +9 -0
  117. package/scripts/install-qoder.mjs +9 -0
  118. package/scripts/install-qwen.mjs +9 -0
  119. package/scripts/install-roocode.mjs +9 -0
  120. package/scripts/install-windsurf.mjs +9 -0
  121. package/scripts/install-zcode.mjs +265 -0
  122. package/scripts/lib/cmd-audit.mjs +127 -0
  123. package/scripts/lib/cmd-checkpoint.mjs +118 -0
  124. package/scripts/lib/cmd-config.mjs +88 -0
  125. package/scripts/lib/cmd-doctor.mjs +267 -0
  126. package/scripts/lib/cmd-execution.mjs +206 -0
  127. package/scripts/lib/cmd-handoff.mjs +94 -0
  128. package/scripts/lib/cmd-inject.mjs +310 -0
  129. package/scripts/lib/cmd-install-amazon-q.mjs +11 -0
  130. package/scripts/lib/cmd-install-cline.mjs +11 -0
  131. package/scripts/lib/cmd-install-continue.mjs +11 -0
  132. package/scripts/lib/cmd-install-cursor.mjs +14 -0
  133. package/scripts/lib/cmd-install-kiro.mjs +11 -0
  134. package/scripts/lib/cmd-install-pi.mjs +11 -0
  135. package/scripts/lib/cmd-install-qoder.mjs +11 -0
  136. package/scripts/lib/cmd-install-qwen.mjs +11 -0
  137. package/scripts/lib/cmd-install-roocode.mjs +11 -0
  138. package/scripts/lib/cmd-install-windsurf.mjs +11 -0
  139. package/scripts/lib/cmd-install-workbuddy.mjs +359 -0
  140. package/scripts/lib/cmd-install-zcode.mjs +14 -0
  141. package/scripts/lib/cmd-isolate.mjs +37 -0
  142. package/scripts/lib/cmd-list.mjs +71 -0
  143. package/scripts/lib/cmd-runtime.mjs +85 -0
  144. package/scripts/lib/cmd-solutions.mjs +71 -0
  145. package/scripts/lib/cmd-state.mjs +231 -0
  146. package/scripts/lib/cmd-sync.mjs +102 -0
  147. package/scripts/lib/cmd-validate.mjs +105 -0
  148. package/scripts/lib/cmd-version.mjs +148 -0
  149. package/scripts/lib/config-loader.mjs +107 -0
  150. package/scripts/lib/execution-plan.mjs +492 -0
  151. package/scripts/lib/execution-recommendation.mjs +221 -0
  152. package/scripts/lib/hash.mjs +74 -0
  153. package/scripts/lib/install.mjs +267 -0
  154. package/scripts/lib/platform-runtime-inventory.mjs +27 -0
  155. package/scripts/lib/platforms.mjs +165 -0
  156. package/scripts/lib/prototype-sync.mjs +192 -0
  157. package/scripts/lib/sdd-overlay.mjs +262 -0
  158. package/scripts/lib/shell-quote.mjs +4 -0
  159. package/scripts/lib/solutions-capture.mjs +102 -0
  160. package/scripts/lib/solutions-index-gen.mjs +112 -0
  161. package/scripts/lib/solutions-inject.mjs +77 -0
  162. package/scripts/lib/solutions-promote.mjs +145 -0
  163. package/scripts/lib/spec-paths.mjs +85 -0
  164. package/scripts/lib/state-loader.mjs +154 -0
  165. package/scripts/lint/lint-skills.mjs +149 -0
  166. package/scripts/lint/rules/behavior-consistency.mjs +56 -0
  167. package/scripts/lint/rules/dp-trigger-points.mjs +60 -0
  168. package/scripts/lint/rules/exception-handling.mjs +59 -0
  169. package/scripts/lint/rules/no-contradictory-instructions.mjs +67 -0
  170. package/scripts/lint/rules/no-redundant-checks.mjs +50 -0
  171. package/scripts/lint/rules/token-rules.mjs +122 -0
  172. package/scripts/review-package +44 -0
  173. package/scripts/task-brief +40 -0
  174. package/scripts/team-flow.mjs +164 -0
  175. package/scripts/token-baseline.mjs +158 -0
  176. package/scripts/validate-artifacts +96 -0
  177. package/scripts/verify-marketplace-release.mjs +59 -0
  178. package/skills/architecture-design/SKILL.md +140 -0
  179. package/skills/architecture-design/chapters/ch01-4a-domains.md +27 -0
  180. package/skills/architecture-design/chapters/ch02-change-cascade.md +27 -0
  181. package/skills/architecture-design/chapters/ch03-architecture-outputs.md +25 -0
  182. package/skills/architecture-design/chapters/ch04-entity-to-aggregate.md +27 -0
  183. package/skills/architecture-design/chapters/ch05-cqrs.md +32 -0
  184. package/skills/architecture-design/chapters/ch06-integration.md +54 -0
  185. package/skills/architecture-design/cheatsheet.md +51 -0
  186. package/skills/architecture-design/glossary.md +23 -0
  187. package/skills/architecture-design/patterns.md +32 -0
  188. package/skills/bug-investigator/SKILL.md +77 -0
  189. package/skills/build-executor/SKILL.md +163 -0
  190. package/skills/build-executor/implementer-prompt.md +162 -0
  191. package/skills/build-executor/references/execution-modes.md +86 -0
  192. package/skills/build-executor/task-reviewer-prompt.md +195 -0
  193. package/skills/ce-brainstorm/SKILL.md +415 -0
  194. package/skills/ce-brainstorm/references/agents/slack-researcher.md +127 -0
  195. package/skills/ce-brainstorm/references/blindspot-pass.md +70 -0
  196. package/skills/ce-brainstorm/references/brainstorm-sections.md +285 -0
  197. package/skills/ce-brainstorm/references/handoff.md +168 -0
  198. package/skills/ce-brainstorm/references/html-rendering.md +634 -0
  199. package/skills/ce-brainstorm/references/markdown-rendering.md +241 -0
  200. package/skills/ce-brainstorm/references/model-tiers.md +9 -0
  201. package/skills/ce-brainstorm/references/prd-mapping.md +71 -0
  202. package/skills/ce-brainstorm/references/product-pressure-test.md +42 -0
  203. package/skills/ce-brainstorm/references/reasoning-elevation.md +63 -0
  204. package/skills/ce-brainstorm/references/settled-decisions.md +45 -0
  205. package/skills/ce-brainstorm/references/synthesis-summary.md +282 -0
  206. package/skills/ce-brainstorm/references/universal-brainstorming.md +73 -0
  207. package/skills/ce-brainstorm/references/verdict-routing.md +26 -0
  208. package/skills/ce-brainstorm/references/visual-probes.md +158 -0
  209. package/skills/ce-brainstorm/scripts/visual-probe-server.js +418 -0
  210. package/skills/ce-compound/SKILL.md +114 -0
  211. package/skills/ce-compound/assets/resolution-template.md +94 -0
  212. package/skills/ce-compound/references/agents/best-practices-researcher.md +115 -0
  213. package/skills/ce-compound/references/agents/data-integrity-guardian.md +68 -0
  214. package/skills/ce-compound/references/agents/framework-docs-researcher.md +93 -0
  215. package/skills/ce-compound/references/agents/pattern-recognition-specialist.md +55 -0
  216. package/skills/ce-compound/references/agents/performance-oracle.md +108 -0
  217. package/skills/ce-compound/references/agents/security-sentinel.md +91 -0
  218. package/skills/ce-compound/references/agents/session-historian.md +83 -0
  219. package/skills/ce-compound/references/concepts-vocabulary.md +78 -0
  220. package/skills/ce-compound/references/full-mode-workflow.md +413 -0
  221. package/skills/ce-compound/references/grounding-validation.md +84 -0
  222. package/skills/ce-compound/references/lightweight-mode.md +53 -0
  223. package/skills/ce-compound/references/output-standards.md +182 -0
  224. package/skills/ce-compound/references/promotion-rules.md +38 -0
  225. package/skills/ce-compound/references/schema.yaml +231 -0
  226. package/skills/ce-compound/references/three-tier-index.md +55 -0
  227. package/skills/ce-compound/references/v0.5-upgrade.md +32 -0
  228. package/skills/ce-compound/references/write-flow.md +36 -0
  229. package/skills/ce-compound/references/yaml-schema.md +118 -0
  230. package/skills/ce-compound/scripts/session-history/discover-sessions.sh +130 -0
  231. package/skills/ce-compound/scripts/session-history/extract-errors.py +254 -0
  232. package/skills/ce-compound/scripts/session-history/extract-metadata.py +456 -0
  233. package/skills/ce-compound/scripts/session-history/extract-skeleton.py +575 -0
  234. package/skills/ce-compound/scripts/validate-doc-claims.py +346 -0
  235. package/skills/ce-compound/scripts/validate-frontmatter.py +137 -0
  236. package/skills/ce-ideate/SKILL.md +402 -0
  237. package/skills/ce-ideate/references/agents/issue-intelligence-analyst.md +200 -0
  238. package/skills/ce-ideate/references/agents/learnings-researcher.md +247 -0
  239. package/skills/ce-ideate/references/agents/slack-researcher.md +127 -0
  240. package/skills/ce-ideate/references/agents/web-researcher.md +121 -0
  241. package/skills/ce-ideate/references/divergent-ideation.md +89 -0
  242. package/skills/ce-ideate/references/html-rendering.md +634 -0
  243. package/skills/ce-ideate/references/ideation-sections.md +191 -0
  244. package/skills/ce-ideate/references/markdown-rendering.md +238 -0
  245. package/skills/ce-ideate/references/post-ideation-workflow.md +167 -0
  246. package/skills/ce-ideate/references/universal-ideation.md +107 -0
  247. package/skills/ce-ideate/references/web-research-cache.md +56 -0
  248. package/skills/ce-plan/SKILL.md +137 -0
  249. package/skills/ce-plan/references/agents/agent-native-planning-strategist.md +62 -0
  250. package/skills/ce-plan/references/agents/architecture-strategist.md +46 -0
  251. package/skills/ce-plan/references/agents/best-practices-researcher.md +114 -0
  252. package/skills/ce-plan/references/agents/data-integrity-guardian.md +68 -0
  253. package/skills/ce-plan/references/agents/data-migration-reviewer.md +103 -0
  254. package/skills/ce-plan/references/agents/deployment-verification-agent.md +157 -0
  255. package/skills/ce-plan/references/agents/framework-docs-researcher.md +93 -0
  256. package/skills/ce-plan/references/agents/git-history-analyzer.md +40 -0
  257. package/skills/ce-plan/references/agents/learnings-researcher.md +247 -0
  258. package/skills/ce-plan/references/agents/pattern-recognition-specialist.md +55 -0
  259. package/skills/ce-plan/references/agents/performance-oracle.md +108 -0
  260. package/skills/ce-plan/references/agents/repo-research-analyst.md +258 -0
  261. package/skills/ce-plan/references/agents/security-sentinel.md +91 -0
  262. package/skills/ce-plan/references/agents/slack-researcher.md +127 -0
  263. package/skills/ce-plan/references/agents/spec-flow-analyzer.md +80 -0
  264. package/skills/ce-plan/references/agents/web-researcher.md +121 -0
  265. package/skills/ce-plan/references/approach-altitude.md +55 -0
  266. package/skills/ce-plan/references/change-splitting.md +65 -0
  267. package/skills/ce-plan/references/completion-contract.md +33 -0
  268. package/skills/ce-plan/references/core-principles.md +40 -0
  269. package/skills/ce-plan/references/deepening-workflow.md +263 -0
  270. package/skills/ce-plan/references/dependency-graph.md +36 -0
  271. package/skills/ce-plan/references/html-rendering.md +634 -0
  272. package/skills/ce-plan/references/intake-routing.md +189 -0
  273. package/skills/ce-plan/references/markdown-rendering.md +238 -0
  274. package/skills/ce-plan/references/plan-handoff.md +150 -0
  275. package/skills/ce-plan/references/plan-sections.md +427 -0
  276. package/skills/ce-plan/references/plan-structure.md +120 -0
  277. package/skills/ce-plan/references/planning-modes.md +69 -0
  278. package/skills/ce-plan/references/reasoning-elevation.md +63 -0
  279. package/skills/ce-plan/references/research-workflow.md +165 -0
  280. package/skills/ce-plan/references/review-and-write.md +127 -0
  281. package/skills/ce-plan/references/settled-decisions.md +45 -0
  282. package/skills/ce-plan/references/synthesis-summary.md +418 -0
  283. package/skills/ce-plan/references/universal-planning.md +168 -0
  284. package/skills/ce-proof/SKILL.md +346 -0
  285. package/skills/ce-strategy/SKILL.md +97 -0
  286. package/skills/ce-strategy/references/interview.md +143 -0
  287. package/skills/ce-strategy/references/strategy-template.md +89 -0
  288. package/skills/code-reviewer/SKILL.md +84 -0
  289. package/skills/code-reviewer/code-reviewer-prompt.md +195 -0
  290. package/skills/contract-builder/SKILL.md +72 -0
  291. package/skills/design-system/SKILL.md +75 -0
  292. package/skills/design-system/references/agents/design-system-architect.md +138 -0
  293. package/skills/design-system/references/creation-flow.md +170 -0
  294. package/skills/design-system/references/preview-template.html +319 -0
  295. package/skills/design-system/references/token-derivation.md +115 -0
  296. package/skills/design-system/references/variant-schema.md +60 -0
  297. package/skills/e2e/SKILL.md +56 -0
  298. package/skills/e2e/references/test-verifier.md +40 -0
  299. package/skills/need-explorer/SKILL.md +79 -0
  300. package/skills/prototype/SKILL.md +98 -0
  301. package/skills/prototype/references/agents/design-system-architect.md +122 -0
  302. package/skills/prototype/references/checklist.md +82 -0
  303. package/skills/prototype/references/craft/accessibility-baseline.md +71 -0
  304. package/skills/prototype/references/craft/anti-ai-slop.md +87 -0
  305. package/skills/prototype/references/craft/laws-of-ux.md +60 -0
  306. package/skills/prototype/references/craft/state-coverage.md +81 -0
  307. package/skills/prototype/references/craft/typography-hierarchy.md +50 -0
  308. package/skills/prototype/references/layouts.md +223 -0
  309. package/skills/prototype/references/orchestration-flow.md +101 -0
  310. package/skills/prototype/references/prototype-scaffold/assets/design-tokens.css +19 -0
  311. package/skills/prototype/references/prototype-scaffold/components/button.html +3 -0
  312. package/skills/prototype/references/prototype-scaffold/design-system.md +7 -0
  313. package/skills/prototype/references/prototype-scaffold/flow.md +11 -0
  314. package/skills/prototype/references/prototype-scaffold/index.html +20 -0
  315. package/skills/prototype/references/prototype-scaffold/pages/index.html +11 -0
  316. package/skills/prototype/references/template.html +396 -0
  317. package/skills/release-archivist/SKILL.md +166 -0
  318. package/skills/release-archivist/references/closing-procedures.md +81 -0
  319. package/skills/session-handoff/SKILL.md +110 -0
  320. package/skills/session-handoff/references/context-extraction.md +70 -0
  321. package/skills/session-handoff/references/handoff-template.md +90 -0
  322. package/skills/session-handoff/references/skill-recommendation.md +43 -0
  323. package/skills/spec-merger/SKILL.md +66 -0
  324. package/skills/spec-writer/SKILL.md +149 -0
  325. package/skills/workflow-bootstrap/SKILL.md +148 -0
  326. package/skills/workflow-bootstrap/references/agents/codebase-recon-analyst.md +121 -0
  327. package/skills/workflow-bootstrap/references/b1-reconnaissance.md +80 -0
  328. package/skills/workflow-bootstrap/scripts/recon-probe.sh +328 -0
  329. package/skills/workflow-feedback/SKILL.md +120 -0
  330. package/skills/workflow-feedback/references/category-guide.md +121 -0
  331. package/skills/workflow-feedback/references/feedback-template.md +73 -0
  332. package/skills/workflow-feedback/references/review-mode.md +92 -0
  333. package/skills/workflow-orchestrator/SKILL.md +94 -0
  334. package/skills/workflow-orchestrator/references/feedback-loops.md +56 -0
  335. package/skills/workflow-orchestrator/references/s1-path-router.md +81 -0
  336. package/skills/workflow-orchestrator/references/s2-prd-prototype-loop.md +96 -0
  337. package/skills/workflow-orchestrator/references/s3-plan-pipeline.md +50 -0
  338. package/skills/workflow-orchestrator/references/s4-split-validate.md +108 -0
  339. package/skills/workflow-orchestrator/references/s5-monitoring.md +61 -0
  340. package/skills/workflow-orchestrator/references/state-model.md +185 -0
  341. package/skills/workflow-start/SKILL.md +147 -0
  342. package/skills/workflow-start/references/routing-rules.md +155 -0
  343. package/src/index.ts +16 -0
  344. package/src/parsing/change-parser.ts +79 -0
  345. package/src/parsing/requirement-blocks.ts +244 -0
  346. package/src/schema/base.ts +8 -0
  347. package/src/schema/change.ts +29 -0
  348. package/src/schema/index.ts +3 -0
  349. package/src/schema/spec.ts +12 -0
  350. package/src/validation/constants.ts +53 -0
  351. package/src/validation/tokenizer.ts +180 -0
  352. package/src/validation/types.ts +47 -0
  353. package/src/validation/validator.ts +552 -0
  354. package/templates/abandonment-summary.md +29 -0
  355. package/templates/design.md +37 -0
  356. package/templates/execution-contract.md +99 -0
  357. package/templates/plan.md +86 -0
  358. package/templates/prd.md +468 -0
  359. package/templates/proposal.md +37 -0
  360. package/templates/spec.md +31 -0
  361. package/templates/tasks.md +47 -0
  362. package/tests/e2e.test.mjs +266 -0
  363. package/tests/lib/cmd-audit.test.mjs +202 -0
  364. package/tests/lib/cmd-checkpoint.test.mjs +98 -0
  365. package/tests/lib/cmd-config.test.mjs +101 -0
  366. package/tests/lib/cmd-doctor.test.mjs +450 -0
  367. package/tests/lib/cmd-execution.test.mjs +647 -0
  368. package/tests/lib/cmd-handoff.test.mjs +137 -0
  369. package/tests/lib/cmd-inject.test.mjs +218 -0
  370. package/tests/lib/cmd-install-workbuddy.test.mjs +113 -0
  371. package/tests/lib/cmd-install-zcode.test.mjs +42 -0
  372. package/tests/lib/cmd-isolate.test.mjs +68 -0
  373. package/tests/lib/cmd-list.test.mjs +127 -0
  374. package/tests/lib/cmd-runtime.test.mjs +83 -0
  375. package/tests/lib/cmd-state-missing.test.mjs +65 -0
  376. package/tests/lib/cmd-state.test.mjs +365 -0
  377. package/tests/lib/cmd-sync-paths.test.mjs +84 -0
  378. package/tests/lib/cmd-validate-paths.test.mjs +90 -0
  379. package/tests/lib/config-loader.test.mjs +175 -0
  380. package/tests/lib/ensure-branch.test.mjs +59 -0
  381. package/tests/lib/execution-control-plane.test.mjs +168 -0
  382. package/tests/lib/execution-plan.test.mjs +474 -0
  383. package/tests/lib/guard-specs-merged.test.mjs +108 -0
  384. package/tests/lib/guard-tests-passing.test.mjs +112 -0
  385. package/tests/lib/guard-transitions.test.mjs +262 -0
  386. package/tests/lib/guard.test.mjs +611 -0
  387. package/tests/lib/hash.test.mjs +138 -0
  388. package/tests/lib/infer-workflow.test.mjs +161 -0
  389. package/tests/lib/install-git-hooks.test.mjs +20 -0
  390. package/tests/lib/marketplace-release-docs.test.mjs +31 -0
  391. package/tests/lib/minimality-discipline.test.mjs +37 -0
  392. package/tests/lib/model-profiles-docs.test.mjs +33 -0
  393. package/tests/lib/node20-compatibility.test.mjs +73 -0
  394. package/tests/lib/node20-test-entry.test.mjs +17 -0
  395. package/tests/lib/platform-runtime-distribution.test.mjs +150 -0
  396. package/tests/lib/raw-mode-smoke.test.mjs +42 -0
  397. package/tests/lib/sdd-overlay.test.mjs +118 -0
  398. package/tests/lib/spec-paths.test.mjs +80 -0
  399. package/tests/lib/state-loader.test.mjs +251 -0
  400. package/tests/lib/token-baseline.test.mjs +72 -0
  401. package/tests/lib/token-rules.test.mjs +94 -0
  402. package/tests/lib/verify-marketplace-release.test.mjs +128 -0
  403. package/tests/tsconfig.json +12 -0
  404. package/token-baseline.json +95 -0
  405. package/tsconfig.json +14 -0
@@ -0,0 +1,575 @@
1
+ #!/usr/bin/env python3
2
+ """Extract the conversation skeleton from a Claude Code, Codex, Cursor, or Pi JSONL session file.
3
+
4
+ Usage:
5
+ cat <session.jsonl> | python3 extract-skeleton.py
6
+ cat <session.jsonl> | python3 extract-skeleton.py --output PATH
7
+
8
+ Auto-detects platform (Claude Code, Codex, Cursor, or Pi) from the JSONL structure.
9
+ Extracts:
10
+ - User messages (text only, no tool results)
11
+ - Assistant text (no thinking/reasoning blocks)
12
+ - Collapsed tool call summaries (consecutive same-tool calls grouped)
13
+
14
+ Consecutive tool calls of the same type are collapsed:
15
+ 3+ Read calls -> "[tools] 3x Read (file1, file2, +1 more) -> all ok"
16
+ Codex call/result pairs are deduplicated (only the result with status is kept).
17
+
18
+ When --output PATH is given, the extracted skeleton is written to PATH and
19
+ stdout receives only a one-line JSON status (_meta with wrote/bytes/stats).
20
+ This lets callers route bulk content to a scratch file without round-tripping
21
+ extraction bytes through orchestrator tool results.
22
+
23
+ Without --output, extracted content goes to stdout and ends with a _meta line.
24
+ """
25
+ import argparse
26
+ import io
27
+ import os
28
+ import sys
29
+ import json
30
+ import re
31
+
32
+ parser = argparse.ArgumentParser(add_help=True)
33
+ parser.add_argument(
34
+ "--output",
35
+ metavar="PATH",
36
+ help="Write extracted skeleton to PATH instead of stdout. Stdout receives a one-line _meta status.",
37
+ )
38
+ args = parser.parse_args()
39
+
40
+ # Capture-and-redirect when --output is set: prints in the rest of the script
41
+ # go to the buffer; at the end the buffer is written to PATH and a status
42
+ # line is emitted to the real stdout.
43
+ _original_stdout = sys.stdout
44
+ if args.output:
45
+ sys.stdout = io.StringIO()
46
+
47
+ stats = {"lines": 0, "parse_errors": 0, "user": 0, "assistant": 0, "tool": 0, "summary": 0}
48
+
49
+ # Claude Code wrapper tags to strip from user message content.
50
+ # Strip entirely (tag + content): framework noise and raw command output.
51
+ # Strip tags only (keep content): command-message, command-name, command-args, user_query.
52
+ _STRIP_BLOCK = re.compile(
53
+ r"<(?:task-notification|local-command-caveat|local-command-stdout|local-command-stderr|system-reminder)[^>]*>.*?</(?:task-notification|local-command-caveat|local-command-stdout|local-command-stderr|system-reminder)>",
54
+ re.DOTALL,
55
+ )
56
+ _STRIP_TAG = re.compile(
57
+ r"</?(?:command-message|command-name|command-args|user_query)[^>]*>"
58
+ )
59
+
60
+
61
+ def clean_text(text):
62
+ """Strip framework wrapper tags from message text (Claude and Cursor)."""
63
+ text = _STRIP_BLOCK.sub("", text)
64
+ text = _STRIP_TAG.sub("", text)
65
+ text = re.sub(r"\n{3,}", "\n\n", text).strip()
66
+ return text
67
+
68
+ # Buffer for pending tool entries: [{"ts", "name", "target", "status"}]
69
+ pending_tools = []
70
+
71
+
72
+ def flush_tools():
73
+ """Print buffered tool entries, collapsing consecutive same-name groups."""
74
+ if not pending_tools:
75
+ return
76
+
77
+ # Group consecutive entries by tool name
78
+ groups = []
79
+ for entry in pending_tools:
80
+ if groups and groups[-1][0]["name"] == entry["name"]:
81
+ groups[-1].append(entry)
82
+ else:
83
+ groups.append([entry])
84
+
85
+ for group in groups:
86
+ name = group[0]["name"]
87
+ if len(group) <= 2:
88
+ # Print individually
89
+ for e in group:
90
+ status = f" -> {e['status']}" if e.get("status") else ""
91
+ ts_prefix = f"[{e['ts']}] " if e.get("ts") else ""
92
+ print(f"{ts_prefix}[tool] {name} {e['target']}{status}")
93
+ stats["tool"] += 1
94
+ else:
95
+ # Collapse
96
+ ts = group[0].get("ts", "")
97
+ targets = [e["target"] for e in group if e.get("target")]
98
+ ok = sum(1 for e in group if e.get("status") == "ok")
99
+ err = sum(1 for e in group if e.get("status") and e["status"] != "ok")
100
+ no_status = len(group) - ok - err
101
+
102
+ # Show first 2 targets, then "+N more"
103
+ if len(targets) > 2:
104
+ target_str = ", ".join(targets[:2]) + f", +{len(targets) - 2} more"
105
+ elif targets:
106
+ target_str = ", ".join(targets)
107
+ else:
108
+ target_str = ""
109
+
110
+ if no_status == len(group):
111
+ status_str = ""
112
+ elif err == 0:
113
+ status_str = " -> all ok"
114
+ else:
115
+ status_str = f" -> {ok} ok, {err} error"
116
+
117
+ ts_prefix = f"[{ts}] " if ts else ""
118
+ print(f"{ts_prefix}[tools] {len(group)}x {name} ({target_str}){status_str}")
119
+ stats["tool"] += len(group)
120
+
121
+ pending_tools.clear()
122
+
123
+
124
+ def _safe_slice(value, n):
125
+ """Slice value if it is a string; otherwise return ''.
126
+
127
+ Some Claude Code / MCP tool inputs put structured data (dicts, lists) in
128
+ fields like `query` or `prompt`. `dict[:N]` raises TypeError, so guard
129
+ every slice with an isinstance check.
130
+ """
131
+ return value[:n] if isinstance(value, str) else ""
132
+
133
+
134
+ def summarize_claude_tool(block):
135
+ """Extract name and target from a Claude Code tool_use block."""
136
+ name = block.get("name", "unknown")
137
+ inp = block.get("input", {})
138
+ fp = inp.get("file_path")
139
+ p = inp.get("path")
140
+ target = (
141
+ (fp if isinstance(fp, str) else None)
142
+ or (p if isinstance(p, str) else None)
143
+ or _safe_slice(inp.get("command"), 120)
144
+ or _safe_slice(inp.get("pattern"), 200)
145
+ or _safe_slice(inp.get("query"), 80)
146
+ or _safe_slice(inp.get("prompt"), 80)
147
+ or ""
148
+ )
149
+ if isinstance(target, str) and len(target) > 120:
150
+ target = target[:120]
151
+ return name, target
152
+
153
+
154
+ def handle_claude(obj):
155
+ msg_type = obj.get("type")
156
+ ts = obj.get("timestamp", "")[:19]
157
+
158
+ if msg_type == "user":
159
+ msg = obj.get("message", {})
160
+ content = msg.get("content", "")
161
+
162
+ if isinstance(content, list):
163
+ for block in content:
164
+ if block.get("type") == "tool_result":
165
+ is_error = block.get("is_error", False)
166
+ status = "error" if is_error else "ok"
167
+ tool_use_id = block.get("tool_use_id")
168
+ matched = False
169
+ if tool_use_id:
170
+ for entry in pending_tools:
171
+ if entry.get("id") == tool_use_id:
172
+ entry["status"] = status
173
+ matched = True
174
+ break
175
+ if not matched:
176
+ # Fallback: assign to earliest pending entry without a status
177
+ for entry in pending_tools:
178
+ if not entry.get("status"):
179
+ entry["status"] = status
180
+ break
181
+
182
+ texts = [
183
+ c.get("text", "")
184
+ for c in content
185
+ if c.get("type") == "text" and len(c.get("text", "")) > 10
186
+ ]
187
+ content = " ".join(texts)
188
+
189
+ if isinstance(content, str):
190
+ content = clean_text(content)
191
+ if len(content) > 15:
192
+ flush_tools()
193
+ print(f"[{ts}] [user] {content[:800]}")
194
+ print("---")
195
+ stats["user"] += 1
196
+
197
+ elif msg_type == "assistant":
198
+ msg = obj.get("message", {})
199
+ content = msg.get("content", [])
200
+ if isinstance(content, list):
201
+ has_text = False
202
+ for block in content:
203
+ if block.get("type") == "text":
204
+ text = clean_text(block.get("text", ""))
205
+ if len(text) > 20:
206
+ if not has_text:
207
+ flush_tools()
208
+ has_text = True
209
+ print(f"[{ts}] [assistant] {text[:800]}")
210
+ print("---")
211
+ stats["assistant"] += 1
212
+ elif block.get("type") == "tool_use":
213
+ name, target = summarize_claude_tool(block)
214
+ entry = {"ts": ts, "name": name, "target": target}
215
+ tool_id = block.get("id")
216
+ if tool_id:
217
+ entry["id"] = tool_id
218
+ pending_tools.append(entry)
219
+
220
+
221
+ def handle_codex(obj):
222
+ msg_type = obj.get("type")
223
+ ts = obj.get("timestamp", "")[:19]
224
+
225
+ if msg_type == "event_msg":
226
+ p = obj.get("payload", {})
227
+ if p.get("type") == "user_message":
228
+ text = p.get("message", "")
229
+ if isinstance(text, str) and len(text) > 15:
230
+ parts = text.split("</system_instruction>")
231
+ user_text = parts[-1].strip() if parts else text
232
+ if len(user_text) > 15:
233
+ flush_tools()
234
+ print(f"[{ts}] [user] {user_text[:800]}")
235
+ print("---")
236
+ stats["user"] += 1
237
+
238
+ elif p.get("type") == "exec_command_end":
239
+ # This is the deduplicated result — has status info
240
+ command = p.get("command", [])
241
+ cmd_str = command[-1] if command else ""
242
+ output = p.get("aggregated_output", "")
243
+
244
+ status = "ok"
245
+ if "Process exited with code " in output:
246
+ try:
247
+ code = int(output.split("Process exited with code ")[1].split("\n")[0])
248
+ if code != 0:
249
+ status = f"error(exit {code})"
250
+ except (IndexError, ValueError):
251
+ pass
252
+
253
+ if cmd_str:
254
+ # Shorten common patterns for readability
255
+ short_cmd = cmd_str[:120]
256
+ pending_tools.append({"ts": ts, "name": "exec", "target": short_cmd, "status": status})
257
+
258
+ elif msg_type == "response_item":
259
+ p = obj.get("payload", {})
260
+ if p.get("type") == "message" and p.get("role") == "assistant":
261
+ for block in p.get("content", []):
262
+ if block.get("type") == "output_text" and len(block.get("text", "")) > 20:
263
+ flush_tools()
264
+ print(f"[{ts}] [assistant] {block['text'][:800]}")
265
+ print("---")
266
+ stats["assistant"] += 1
267
+
268
+ # Skip function_call — exec_command_end is the deduplicated version with status
269
+
270
+
271
+ def _pi_text_content(content):
272
+ if isinstance(content, str):
273
+ return [content]
274
+ if not isinstance(content, list):
275
+ return []
276
+ return [
277
+ block.get("text", "")
278
+ for block in content
279
+ if isinstance(block, dict) and block.get("type") == "text"
280
+ ]
281
+
282
+
283
+ def _pi_active_path_objects(objects):
284
+ """Return only entries on Pi's active leaf-to-root path."""
285
+ by_id = {
286
+ obj.get("id"): obj
287
+ for obj in objects
288
+ if isinstance(obj.get("id"), str) and obj.get("type") != "session"
289
+ }
290
+ leaf_id = None
291
+ for obj in objects:
292
+ if obj.get("type") != "session" and isinstance(obj.get("id"), str):
293
+ leaf_id = obj["id"]
294
+ if not leaf_id:
295
+ return objects
296
+
297
+ active_ids = set()
298
+ current = leaf_id
299
+ while isinstance(current, str) and current and current not in active_ids:
300
+ active_ids.add(current)
301
+ parent = by_id.get(current, {}).get("parentId")
302
+ current = parent if isinstance(parent, str) else None
303
+ return [
304
+ obj
305
+ for obj in objects
306
+ if obj.get("type") == "session" or obj.get("id") in active_ids
307
+ ]
308
+
309
+
310
+ def _pi_context_objects(objects):
311
+ """Return Pi entries that participate in active LLM context."""
312
+ active = _pi_active_path_objects(objects)
313
+ compactions = [obj for obj in active if obj.get("type") == "compaction"]
314
+ if not compactions:
315
+ return active
316
+
317
+ first_kept = compactions[-1].get("firstKeptEntryId")
318
+ if not isinstance(first_kept, str):
319
+ return active
320
+
321
+ latest_compaction_id = compactions[-1].get("id")
322
+ started = False
323
+ found_first_kept = False
324
+ context = [obj for obj in active if obj.get("type") == "session"]
325
+ context.append(compactions[-1])
326
+ for obj in active:
327
+ if obj.get("type") == "session":
328
+ continue
329
+ if obj.get("id") == first_kept:
330
+ started = True
331
+ found_first_kept = True
332
+ if obj.get("id") == latest_compaction_id:
333
+ continue
334
+ if started:
335
+ context.append(obj)
336
+ return context if found_first_kept and len(context) > 1 else active
337
+
338
+
339
+ def handle_pi(obj):
340
+ """Pi sessions: type='message' with message.role and content blocks."""
341
+ entry_type = obj.get("type")
342
+ ts = obj.get("timestamp", "")[:19]
343
+
344
+ if entry_type in ("compaction", "branch_summary"):
345
+ text = clean_text(obj.get("summary", ""))
346
+ if len(text) > 15:
347
+ flush_tools()
348
+ print(f"[{ts}] [summary] {text[:800]}")
349
+ print("---")
350
+ stats["summary"] += 1
351
+ return
352
+
353
+ if entry_type == "custom_message":
354
+ text = clean_text(" ".join(_pi_text_content(obj.get("content", []))))
355
+ if len(text) > 15:
356
+ flush_tools()
357
+ print(f"[{ts}] [summary] {text[:800]}")
358
+ print("---")
359
+ stats["summary"] += 1
360
+ return
361
+
362
+ if entry_type != "message":
363
+ return
364
+
365
+ msg = obj.get("message", {})
366
+ role = msg.get("role", "")
367
+ content = msg.get("content", [])
368
+
369
+ if role == "bashExecution":
370
+ exit_code = msg.get("exitCode")
371
+ if msg.get("cancelled"):
372
+ status = "cancelled"
373
+ elif exit_code in (None, 0):
374
+ status = "ok"
375
+ else:
376
+ status = f"error(exit {exit_code})"
377
+ command = _safe_slice(msg.get("command"), 120)
378
+ pending_tools.append({"ts": ts, "name": "bash", "target": command, "status": status})
379
+ return
380
+
381
+ if role == "custom":
382
+ text = clean_text(" ".join(_pi_text_content(content)))
383
+ if len(text) > 15:
384
+ flush_tools()
385
+ print(f"[{ts}] [summary] {text[:800]}")
386
+ print("---")
387
+ stats["summary"] += 1
388
+ return
389
+
390
+ if role == "user":
391
+ text = clean_text(" ".join(_pi_text_content(content)))
392
+ if len(text) > 15:
393
+ flush_tools()
394
+ print(f"[{ts}] [user] {text[:800]}")
395
+ print("---")
396
+ stats["user"] += 1
397
+
398
+ elif role == "assistant":
399
+ if isinstance(content, str):
400
+ text = clean_text(content)
401
+ if len(text) > 20:
402
+ flush_tools()
403
+ print(f"[{ts}] [assistant] {text[:800]}")
404
+ print("---")
405
+ stats["assistant"] += 1
406
+ return
407
+
408
+ has_text = False
409
+ for block in (content if isinstance(content, list) else []):
410
+ if not isinstance(block, dict):
411
+ continue
412
+ if block.get("type") == "text":
413
+ text = clean_text(block.get("text", ""))
414
+ if len(text) > 20:
415
+ if not has_text:
416
+ flush_tools()
417
+ has_text = True
418
+ print(f"[{ts}] [assistant] {text[:800]}")
419
+ print("---")
420
+ stats["assistant"] += 1
421
+ elif block.get("type") == "toolCall":
422
+ name = block.get("name", "unknown")
423
+ args = block.get("arguments", {})
424
+ if not isinstance(args, dict):
425
+ args = {}
426
+ target = (
427
+ _safe_slice(args.get("path"), 200)
428
+ or _safe_slice(args.get("file_path"), 200)
429
+ or _safe_slice(args.get("command"), 120)
430
+ or _safe_slice(args.get("pattern"), 200)
431
+ or _safe_slice(args.get("query"), 80)
432
+ or _safe_slice(args.get("prompt"), 80)
433
+ or ""
434
+ )
435
+ if isinstance(target, str) and len(target) > 120:
436
+ target = target[:120]
437
+ entry = {"ts": ts, "name": name, "target": target}
438
+ tool_id = block.get("id")
439
+ if tool_id:
440
+ entry["id"] = tool_id
441
+ pending_tools.append(entry)
442
+
443
+ elif role == "toolResult":
444
+ tool_call_id = msg.get("toolCallId")
445
+ is_error = bool(msg.get("isError"))
446
+ if isinstance(content, list):
447
+ is_error = is_error or any(
448
+ isinstance(block, dict) and block.get("type") == "toolError"
449
+ for block in content
450
+ )
451
+ status = "error" if is_error else "ok"
452
+ if tool_call_id:
453
+ for entry in pending_tools:
454
+ if entry.get("id") == tool_call_id:
455
+ entry["status"] = status
456
+ break
457
+ else:
458
+ for entry in pending_tools:
459
+ if not entry.get("status"):
460
+ entry["status"] = status
461
+ break
462
+
463
+
464
+ def handle_cursor(obj):
465
+ """Cursor agent transcripts: role-based, no timestamps, same content structure as Claude."""
466
+ role = obj.get("role")
467
+ content = obj.get("message", {}).get("content", [])
468
+
469
+ if role == "user":
470
+ texts = []
471
+ for block in (content if isinstance(content, list) else []):
472
+ if block.get("type") == "text":
473
+ texts.append(block.get("text", ""))
474
+ text = clean_text(" ".join(texts))
475
+ if len(text) > 15:
476
+ flush_tools()
477
+ # No timestamps available in Cursor transcripts
478
+ print(f"[user] {text[:800]}")
479
+ print("---")
480
+ stats["user"] += 1
481
+
482
+ elif role == "assistant":
483
+ has_text = False
484
+ for block in (content if isinstance(content, list) else []):
485
+ if block.get("type") == "text":
486
+ text = block.get("text", "")
487
+ # Skip [REDACTED] placeholder blocks
488
+ if len(text) > 20 and text.strip() != "[REDACTED]":
489
+ if not has_text:
490
+ flush_tools()
491
+ has_text = True
492
+ print(f"[assistant] {text[:800]}")
493
+ print("---")
494
+ stats["assistant"] += 1
495
+ elif block.get("type") == "tool_use":
496
+ name = block.get("name", "unknown")
497
+ inp = block.get("input", {})
498
+ p = inp.get("path")
499
+ fp = inp.get("file_path")
500
+ target = (
501
+ (p if isinstance(p, str) else None)
502
+ or (fp if isinstance(fp, str) else None)
503
+ or _safe_slice(inp.get("command"), 120)
504
+ or _safe_slice(inp.get("pattern"), 200)
505
+ or _safe_slice(inp.get("glob_pattern"), 200)
506
+ or _safe_slice(inp.get("target_directory"), 200)
507
+ or ""
508
+ )
509
+ if isinstance(target, str) and len(target) > 120:
510
+ target = target[:120]
511
+ # No status info available — Cursor doesn't log tool results
512
+ pending_tools.append({"ts": "", "name": name, "target": target})
513
+
514
+
515
+ # Auto-detect platform from first few lines, then process all
516
+ detected = None
517
+ buffer = []
518
+
519
+ for line in sys.stdin:
520
+ line = line.strip()
521
+ if not line:
522
+ continue
523
+ buffer.append(line)
524
+ stats["lines"] += 1
525
+
526
+ # Scan until a decisive record appears (short-circuits once detected).
527
+ # Claude Code sessions front-load 10+ non-message metadata records
528
+ # (last-prompt, custom-title, attachments, ...), so capping detection to
529
+ # the first N lines silently misroutes them to the codex handler and
530
+ # produces an empty skeleton with parse_errors: 0 (issue #923).
531
+ if not detected:
532
+ try:
533
+ obj = json.loads(line)
534
+ if obj.get("type") == "session" and "cwd" in obj:
535
+ detected = "pi"
536
+ elif obj.get("type") in ("user", "assistant"):
537
+ detected = "claude"
538
+ elif obj.get("type") in ("session_meta", "turn_context", "response_item", "event_msg"):
539
+ detected = "codex"
540
+ elif obj.get("role") in ("user", "assistant") and "type" not in obj:
541
+ detected = "cursor"
542
+ except (json.JSONDecodeError, KeyError):
543
+ pass
544
+
545
+ handlers = {"claude": handle_claude, "codex": handle_codex, "cursor": handle_cursor, "pi": handle_pi}
546
+ handler = handlers.get(detected, handle_codex)
547
+
548
+ objects = []
549
+ for line in buffer:
550
+ try:
551
+ objects.append(json.loads(line))
552
+ except (json.JSONDecodeError, KeyError):
553
+ stats["parse_errors"] += 1
554
+
555
+ if detected == "pi":
556
+ objects = _pi_context_objects(objects)
557
+
558
+ for obj in objects:
559
+ try:
560
+ handler(obj)
561
+ except KeyError:
562
+ stats["parse_errors"] += 1
563
+
564
+ # Flush any remaining buffered tools
565
+ flush_tools()
566
+
567
+ print(json.dumps({"_meta": True, **stats}))
568
+
569
+ if args.output:
570
+ body = sys.stdout.getvalue()
571
+ sys.stdout = _original_stdout
572
+ with open(args.output, "w") as f:
573
+ f.write(body)
574
+ bytes_written = os.path.getsize(args.output)
575
+ print(json.dumps({"_meta": True, "wrote": args.output, "bytes": bytes_written, **stats}))