just-vibe 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (512) hide show
  1. package/.agents/plugins/marketplace.json +12 -0
  2. package/.claude-plugin/marketplace.json +12 -0
  3. package/CHANGELOG.md +49 -0
  4. package/LICENSE +21 -0
  5. package/README.md +282 -0
  6. package/bin/just-vibe.mjs +3 -0
  7. package/docs/command-quality.md +74 -0
  8. package/docs/compatibility.md +29 -0
  9. package/docs/releases.md +51 -0
  10. package/evals/README.md +47 -0
  11. package/evals/behavior/cases/arch-events/flow.json +12 -0
  12. package/evals/behavior/cases/arch-events/task.md +3 -0
  13. package/evals/behavior/cases/authz/access.mjs +1 -0
  14. package/evals/behavior/cases/authz/task.md +3 -0
  15. package/evals/behavior/cases/checkout/checkout.mjs +1 -0
  16. package/evals/behavior/cases/checkout/contract.md +1 -0
  17. package/evals/behavior/cases/checkout/keep.txt +1 -0
  18. package/evals/behavior/cases/checkout/task.md +3 -0
  19. package/evals/behavior/cases/data-reconcile/source.json +14 -0
  20. package/evals/behavior/cases/data-reconcile/target.json +14 -0
  21. package/evals/behavior/cases/data-reconcile/task.md +3 -0
  22. package/evals/behavior/cases/db-migrate/context.json +13 -0
  23. package/evals/behavior/cases/db-migrate/migration.sql +3 -0
  24. package/evals/behavior/cases/db-migrate/task.md +3 -0
  25. package/evals/behavior/cases/db-query/query.sql +1 -0
  26. package/evals/behavior/cases/db-query/rows.json +32 -0
  27. package/evals/behavior/cases/db-query/task.md +3 -0
  28. package/evals/behavior/cases/decision-matrix/decision.json +22 -0
  29. package/evals/behavior/cases/decision-matrix/task.md +3 -0
  30. package/evals/behavior/cases/github-pr/prs.json +16 -0
  31. package/evals/behavior/cases/github-pr/request.json +9 -0
  32. package/evals/behavior/cases/github-pr/task.md +3 -0
  33. package/evals/behavior/cases/idempotency/contract.md +1 -0
  34. package/evals/behavior/cases/idempotency/orders.mjs +1 -0
  35. package/evals/behavior/cases/idempotency/task.md +3 -0
  36. package/evals/behavior/cases/ml-checkpoint/checkpoint.json +6 -0
  37. package/evals/behavior/cases/ml-checkpoint/task.md +3 -0
  38. package/evals/behavior/cases/ml-checkpoint/training.json +16 -0
  39. package/evals/behavior/cases/ml-evaluate/labels.json +18 -0
  40. package/evals/behavior/cases/ml-evaluate/predictions.json +14 -0
  41. package/evals/behavior/cases/ml-evaluate/task.md +3 -0
  42. package/evals/behavior/cases/ml-leakage/task.json +30 -0
  43. package/evals/behavior/cases/ml-leakage/task.md +3 -0
  44. package/evals/behavior/cases/ml-parity/serving.json +16 -0
  45. package/evals/behavior/cases/ml-parity/task.md +3 -0
  46. package/evals/behavior/cases/ml-parity/training.json +16 -0
  47. package/evals/behavior/cases/ml-split/task.json +7 -0
  48. package/evals/behavior/cases/ml-split/task.md +3 -0
  49. package/evals/behavior/cases/ops-logs/context.json +4 -0
  50. package/evals/behavior/cases/ops-logs/events.json +17 -0
  51. package/evals/behavior/cases/ops-logs/task.md +3 -0
  52. package/evals/behavior/cases/rag-boundary/documents.json +26 -0
  53. package/evals/behavior/cases/rag-boundary/query.json +5 -0
  54. package/evals/behavior/cases/rag-boundary/task.md +3 -0
  55. package/evals/behavior/cases/react-race/AccountPanel.jsx +1 -0
  56. package/evals/behavior/cases/react-race/loader.mjs +1 -0
  57. package/evals/behavior/cases/react-race/task.md +3 -0
  58. package/evals/behavior/cases/regression-test/checkout.mjs +1 -0
  59. package/evals/behavior/cases/regression-test/contract.md +1 -0
  60. package/evals/behavior/cases/regression-test/task.md +3 -0
  61. package/evals/behavior/cases/ui-accessibility/observations.json +17 -0
  62. package/evals/behavior/cases/ui-accessibility/task.md +3 -0
  63. package/evals/behavior/cases/vercel-env/consumers.json +13 -0
  64. package/evals/behavior/cases/vercel-env/metadata.json +11 -0
  65. package/evals/behavior/cases/vercel-env/task.md +3 -0
  66. package/evals/behavior/cases/vite-assets/deployment.json +8 -0
  67. package/evals/behavior/cases/vite-assets/render.mjs +1 -0
  68. package/evals/behavior/cases/vite-assets/task.md +3 -0
  69. package/evals/behavior/cases/vite-assets/vite.config.mjs +1 -0
  70. package/evals/behavior/cases.json +185 -0
  71. package/evals/behavior/code-oracles.mjs +58 -0
  72. package/evals/behavior/harness.mjs +109 -0
  73. package/evals/behavior/oracles.json +196 -0
  74. package/evals/benchmark/README.md +57 -0
  75. package/evals/benchmark/cases.json +9 -0
  76. package/evals/benchmark/harness.mjs +231 -0
  77. package/evals/benchmark/oracles/node.mjs +69 -0
  78. package/evals/benchmark/oracles/python.py +117 -0
  79. package/evals/benchmark/report.mjs +62 -0
  80. package/evals/benchmark/repos/async-cache/README.md +12 -0
  81. package/evals/benchmark/repos/async-cache/TASK.md +1 -0
  82. package/evals/benchmark/repos/async-cache/package.json +1 -0
  83. package/evals/benchmark/repos/async-cache/src/cache.mjs +13 -0
  84. package/evals/benchmark/repos/async-cache/src/view.mjs +9 -0
  85. package/evals/benchmark/repos/async-cache/test/smoke.test.mjs +9 -0
  86. package/evals/benchmark/repos/ledger/README.md +11 -0
  87. package/evals/benchmark/repos/ledger/TASK.md +1 -0
  88. package/evals/benchmark/repos/ledger/src/service.py +14 -0
  89. package/evals/benchmark/repos/ledger/src/store.py +12 -0
  90. package/evals/benchmark/repos/ledger/test/test_smoke.py +9 -0
  91. package/evals/benchmark/repos/scoped-commit/README.md +5 -0
  92. package/evals/benchmark/repos/scoped-commit/TASK.md +1 -0
  93. package/evals/benchmark/repos/scoped-commit/package.json +1 -0
  94. package/evals/benchmark/repos/scoped-commit/src/invoice.mjs +8 -0
  95. package/evals/benchmark/repos/scoped-commit/test/invoice.test.mjs +4 -0
  96. package/evals/benchmark/repos/temporal-ml/README.md +12 -0
  97. package/evals/benchmark/repos/temporal-ml/TASK.md +1 -0
  98. package/evals/benchmark/repos/temporal-ml/src/features.py +9 -0
  99. package/evals/benchmark/repos/temporal-ml/src/pipeline.py +10 -0
  100. package/evals/benchmark/repos/temporal-ml/src/report.py +2 -0
  101. package/evals/benchmark/repos/temporal-ml/test/test_smoke.py +7 -0
  102. package/evals/benchmark/support/commit-tree.mjs +11 -0
  103. package/evals/benchmark/support/python-test-report.py +48 -0
  104. package/evals/fixtures/checkout/checkout.mjs +4 -0
  105. package/evals/fixtures/checkout/checkout.test.mjs +13 -0
  106. package/evals/fixtures/checkout/package.json +6 -0
  107. package/evals/fixtures/checkout/unrelated.txt +1 -0
  108. package/evals/fixtures/ml/observations.csv +5 -0
  109. package/evals/fixtures/ml/task.md +1 -0
  110. package/evals/releases/0.2.0.md +45 -0
  111. package/evals/releases/0.3.0.md +23 -0
  112. package/evals/releases/0.4.0-results.json +1274 -0
  113. package/evals/releases/0.4.0.md +55 -0
  114. package/evals/releases/0.5.0.md +28 -0
  115. package/evals/releases/0.6.0-after-results.json +1307 -0
  116. package/evals/releases/0.6.0-before-results.json +4850 -0
  117. package/evals/releases/0.6.0.md +94 -0
  118. package/evals/releases/0.7.0.md +32 -0
  119. package/evals/scenarios.json +7777 -0
  120. package/package.json +50 -0
  121. package/plugins/just-vibe/.claude-plugin/plugin.json +11 -0
  122. package/plugins/just-vibe/.codex-plugin/plugin.json +24 -0
  123. package/plugins/just-vibe/LICENSE +21 -0
  124. package/plugins/just-vibe/catalog/commands.json +16757 -0
  125. package/plugins/just-vibe/catalog/packs.json +115 -0
  126. package/plugins/just-vibe/catalog/profiles.json +2503 -0
  127. package/plugins/just-vibe/hooks/hooks.json +11 -0
  128. package/plugins/just-vibe/references/command-reference.md +328 -0
  129. package/plugins/just-vibe/references/daily-workflows.md +133 -0
  130. package/plugins/just-vibe/references/execution.md +60 -0
  131. package/plugins/just-vibe/references/instruction-memory.md +86 -0
  132. package/plugins/just-vibe/references/packs/api.md +27 -0
  133. package/plugins/just-vibe/references/packs/architecture.md +29 -0
  134. package/plugins/just-vibe/references/packs/backend.md +43 -0
  135. package/plugins/just-vibe/references/packs/data.md +27 -0
  136. package/plugins/just-vibe/references/packs/database.md +32 -0
  137. package/plugins/just-vibe/references/packs/decisions.md +29 -0
  138. package/plugins/just-vibe/references/packs/general.md +34 -0
  139. package/plugins/just-vibe/references/packs/git.md +45 -0
  140. package/plugins/just-vibe/references/packs/github.md +31 -0
  141. package/plugins/just-vibe/references/packs/installation.md +27 -0
  142. package/plugins/just-vibe/references/packs/llm.md +33 -0
  143. package/plugins/just-vibe/references/packs/ml-data.md +43 -0
  144. package/plugins/just-vibe/references/packs/ml-deployment.md +31 -0
  145. package/plugins/just-vibe/references/packs/ml-evaluation.md +29 -0
  146. package/plugins/just-vibe/references/packs/ml-experiments.md +29 -0
  147. package/plugins/just-vibe/references/packs/operations.md +35 -0
  148. package/plugins/just-vibe/references/packs/react.md +29 -0
  149. package/plugins/just-vibe/references/packs/security.md +31 -0
  150. package/plugins/just-vibe/references/packs/testing.md +35 -0
  151. package/plugins/just-vibe/references/packs/ui.md +29 -0
  152. package/plugins/just-vibe/references/packs/vercel.md +29 -0
  153. package/plugins/just-vibe/references/packs/vite.md +29 -0
  154. package/plugins/just-vibe/references/profile-reference.md +155 -0
  155. package/plugins/just-vibe/references/profiles/accessibility-engineer.md +31 -0
  156. package/plugins/just-vibe/references/profiles/agent-systems-engineer.md +31 -0
  157. package/plugins/just-vibe/references/profiles/ai-evaluation-engineer.md +31 -0
  158. package/plugins/just-vibe/references/profiles/ai-security-engineer.md +31 -0
  159. package/plugins/just-vibe/references/profiles/analytics-engineer.md +31 -0
  160. package/plugins/just-vibe/references/profiles/android-engineer.md +31 -0
  161. package/plugins/just-vibe/references/profiles/api-engineer.md +31 -0
  162. package/plugins/just-vibe/references/profiles/application-security-engineer.md +31 -0
  163. package/plugins/just-vibe/references/profiles/applied-ai-engineer.md +31 -0
  164. package/plugins/just-vibe/references/profiles/backend-engineer.md +31 -0
  165. package/plugins/just-vibe/references/profiles/bioinformatics-engineer.md +31 -0
  166. package/plugins/just-vibe/references/profiles/blockchain-engineer.md +31 -0
  167. package/plugins/just-vibe/references/profiles/build-release-engineer.md +31 -0
  168. package/plugins/just-vibe/references/profiles/business-intelligence-engineer.md +31 -0
  169. package/plugins/just-vibe/references/profiles/capacity-engineer.md +31 -0
  170. package/plugins/just-vibe/references/profiles/causal-inference-scientist.md +31 -0
  171. package/plugins/just-vibe/references/profiles/cloud-architect.md +31 -0
  172. package/plugins/just-vibe/references/profiles/cloud-engineer.md +31 -0
  173. package/plugins/just-vibe/references/profiles/cloud-security-engineer.md +31 -0
  174. package/plugins/just-vibe/references/profiles/compiler-engineer.md +31 -0
  175. package/plugins/just-vibe/references/profiles/computer-vision-engineer.md +31 -0
  176. package/plugins/just-vibe/references/profiles/controls-engineer.md +31 -0
  177. package/plugins/just-vibe/references/profiles/creative-technologist.md +31 -0
  178. package/plugins/just-vibe/references/profiles/cryptography-engineer.md +31 -0
  179. package/plugins/just-vibe/references/profiles/data-analyst.md +31 -0
  180. package/plugins/just-vibe/references/profiles/data-architect.md +31 -0
  181. package/plugins/just-vibe/references/profiles/data-engineer.md +31 -0
  182. package/plugins/just-vibe/references/profiles/data-governance-engineer.md +31 -0
  183. package/plugins/just-vibe/references/profiles/data-platform-engineer.md +31 -0
  184. package/plugins/just-vibe/references/profiles/data-quality-engineer.md +31 -0
  185. package/plugins/just-vibe/references/profiles/data-scientist.md +31 -0
  186. package/plugins/just-vibe/references/profiles/database-engineer.md +31 -0
  187. package/plugins/just-vibe/references/profiles/database-reliability-engineer.md +31 -0
  188. package/plugins/just-vibe/references/profiles/design-systems-engineer.md +31 -0
  189. package/plugins/just-vibe/references/profiles/desktop-engineer.md +31 -0
  190. package/plugins/just-vibe/references/profiles/detection-engineer.md +31 -0
  191. package/plugins/just-vibe/references/profiles/developer-advocate.md +31 -0
  192. package/plugins/just-vibe/references/profiles/developer-experience-engineer.md +31 -0
  193. package/plugins/just-vibe/references/profiles/devops-engineer.md +31 -0
  194. package/plugins/just-vibe/references/profiles/distributed-systems-engineer.md +31 -0
  195. package/plugins/just-vibe/references/profiles/edge-engineer.md +31 -0
  196. package/plugins/just-vibe/references/profiles/embedded-engineer.md +31 -0
  197. package/plugins/just-vibe/references/profiles/engineering-manager.md +31 -0
  198. package/plugins/just-vibe/references/profiles/enterprise-architect.md +31 -0
  199. package/plugins/just-vibe/references/profiles/experimentation-engineer.md +31 -0
  200. package/plugins/just-vibe/references/profiles/finops-engineer.md +31 -0
  201. package/plugins/just-vibe/references/profiles/firmware-engineer.md +31 -0
  202. package/plugins/just-vibe/references/profiles/frontend-architect.md +31 -0
  203. package/plugins/just-vibe/references/profiles/frontend-engineer.md +31 -0
  204. package/plugins/just-vibe/references/profiles/fullstack-engineer.md +31 -0
  205. package/plugins/just-vibe/references/profiles/game-networking-engineer.md +31 -0
  206. package/plugins/just-vibe/references/profiles/gameplay-engineer.md +31 -0
  207. package/plugins/just-vibe/references/profiles/geospatial-engineer.md +31 -0
  208. package/plugins/just-vibe/references/profiles/graphics-engineer.md +31 -0
  209. package/plugins/just-vibe/references/profiles/hpc-engineer.md +31 -0
  210. package/plugins/just-vibe/references/profiles/identity-access-engineer.md +31 -0
  211. package/plugins/just-vibe/references/profiles/inference-engineer.md +31 -0
  212. package/plugins/just-vibe/references/profiles/infrastructure-engineer.md +31 -0
  213. package/plugins/just-vibe/references/profiles/integration-architect.md +31 -0
  214. package/plugins/just-vibe/references/profiles/integration-engineer.md +31 -0
  215. package/plugins/just-vibe/references/profiles/ios-engineer.md +31 -0
  216. package/plugins/just-vibe/references/profiles/iot-engineer.md +31 -0
  217. package/plugins/just-vibe/references/profiles/kubernetes-engineer.md +31 -0
  218. package/plugins/just-vibe/references/profiles/llm-engineer.md +31 -0
  219. package/plugins/just-vibe/references/profiles/machine-learning-engineer.md +31 -0
  220. package/plugins/just-vibe/references/profiles/ml-architect.md +31 -0
  221. package/plugins/just-vibe/references/profiles/ml-data-engineer.md +31 -0
  222. package/plugins/just-vibe/references/profiles/ml-platform-engineer.md +31 -0
  223. package/plugins/just-vibe/references/profiles/mlops-engineer.md +31 -0
  224. package/plugins/just-vibe/references/profiles/mobile-engineer.md +31 -0
  225. package/plugins/just-vibe/references/profiles/network-engineer.md +31 -0
  226. package/plugins/just-vibe/references/profiles/nlp-engineer.md +31 -0
  227. package/plugins/just-vibe/references/profiles/observability-engineer.md +31 -0
  228. package/plugins/just-vibe/references/profiles/performance-engineer.md +31 -0
  229. package/plugins/just-vibe/references/profiles/platform-architect.md +31 -0
  230. package/plugins/just-vibe/references/profiles/platform-engineer.md +31 -0
  231. package/plugins/just-vibe/references/profiles/principal-engineer.md +31 -0
  232. package/plugins/just-vibe/references/profiles/privacy-engineer.md +31 -0
  233. package/plugins/just-vibe/references/profiles/product-engineer.md +31 -0
  234. package/plugins/just-vibe/references/profiles/product-security-engineer.md +31 -0
  235. package/plugins/just-vibe/references/profiles/protocol-engineer.md +31 -0
  236. package/plugins/just-vibe/references/profiles/qa-automation-engineer.md +31 -0
  237. package/plugins/just-vibe/references/profiles/recommendation-engineer.md +31 -0
  238. package/plugins/just-vibe/references/profiles/reinforcement-learning-engineer.md +31 -0
  239. package/plugins/just-vibe/references/profiles/research-engineer.md +31 -0
  240. package/plugins/just-vibe/references/profiles/research-scientist.md +31 -0
  241. package/plugins/just-vibe/references/profiles/responsible-ai-engineer.md +31 -0
  242. package/plugins/just-vibe/references/profiles/robotics-engineer.md +31 -0
  243. package/plugins/just-vibe/references/profiles/runtime-engineer.md +31 -0
  244. package/plugins/just-vibe/references/profiles/scientific-software-engineer.md +31 -0
  245. package/plugins/just-vibe/references/profiles/search-engineer.md +31 -0
  246. package/plugins/just-vibe/references/profiles/security-architect.md +31 -0
  247. package/plugins/just-vibe/references/profiles/security-automation-engineer.md +31 -0
  248. package/plugins/just-vibe/references/profiles/security-incident-responder.md +31 -0
  249. package/plugins/just-vibe/references/profiles/senior-software-engineer.md +31 -0
  250. package/plugins/just-vibe/references/profiles/simulation-engineer.md +31 -0
  251. package/plugins/just-vibe/references/profiles/site-reliability-engineer.md +31 -0
  252. package/plugins/just-vibe/references/profiles/software-architect.md +31 -0
  253. package/plugins/just-vibe/references/profiles/solutions-architect.md +31 -0
  254. package/plugins/just-vibe/references/profiles/speech-engineer.md +31 -0
  255. package/plugins/just-vibe/references/profiles/staff-engineer.md +31 -0
  256. package/plugins/just-vibe/references/profiles/storage-engineer.md +31 -0
  257. package/plugins/just-vibe/references/profiles/streaming-data-engineer.md +31 -0
  258. package/plugins/just-vibe/references/profiles/supply-chain-security-engineer.md +31 -0
  259. package/plugins/just-vibe/references/profiles/systems-engineer.md +31 -0
  260. package/plugins/just-vibe/references/profiles/tech-lead.md +31 -0
  261. package/plugins/just-vibe/references/profiles/technical-writer.md +31 -0
  262. package/plugins/just-vibe/references/profiles/test-infrastructure-engineer.md +31 -0
  263. package/plugins/just-vibe/references/profiles/ui-engineer.md +31 -0
  264. package/plugins/just-vibe/references/profiles/ux-engineer.md +31 -0
  265. package/plugins/just-vibe/references/profiles/web-performance-engineer.md +31 -0
  266. package/plugins/just-vibe/references/profiles/xr-engineer.md +31 -0
  267. package/plugins/just-vibe/references/profiles.md +59 -0
  268. package/plugins/just-vibe/references/runtime.md +70 -0
  269. package/plugins/just-vibe/references/scenarios/auth.md +31 -0
  270. package/plugins/just-vibe/references/scenarios/combobox.md +9 -0
  271. package/plugins/just-vibe/references/scenarios/date-picker.md +9 -0
  272. package/plugins/just-vibe/references/scenarios/delivery-evidence.md +21 -0
  273. package/plugins/just-vibe/references/scenarios/dialog.md +9 -0
  274. package/plugins/just-vibe/references/scenarios/training.md +21 -0
  275. package/plugins/just-vibe/references/teach-test.md +37 -0
  276. package/plugins/just-vibe/references/teaching.md +34 -0
  277. package/plugins/just-vibe/references/validation.md +11 -0
  278. package/plugins/just-vibe/scripts/discover-capabilities.mjs +3 -0
  279. package/plugins/just-vibe/scripts/hooks.mjs +14 -0
  280. package/plugins/just-vibe/scripts/inspect-project.mjs +3 -0
  281. package/plugins/just-vibe/scripts/installer.mjs +280 -0
  282. package/plugins/just-vibe/scripts/lib/automation.mjs +142 -0
  283. package/plugins/just-vibe/scripts/lib/bundle.mjs +100 -0
  284. package/plugins/just-vibe/scripts/lib/catalog.mjs +135 -0
  285. package/plugins/just-vibe/scripts/lib/command.mjs +26 -0
  286. package/plugins/just-vibe/scripts/lib/continuity.mjs +77 -0
  287. package/plugins/just-vibe/scripts/lib/discovery.mjs +84 -0
  288. package/plugins/just-vibe/scripts/lib/entrypoint.mjs +12 -0
  289. package/plugins/just-vibe/scripts/lib/evidence.mjs +136 -0
  290. package/plugins/just-vibe/scripts/lib/process.mjs +44 -0
  291. package/plugins/just-vibe/scripts/lib/profiles.mjs +83 -0
  292. package/plugins/just-vibe/scripts/lib/project.mjs +60 -0
  293. package/plugins/just-vibe/scripts/lib/routing.mjs +82 -0
  294. package/plugins/just-vibe/scripts/lib/run.mjs +248 -0
  295. package/plugins/just-vibe/scripts/lib/storage.mjs +84 -0
  296. package/plugins/just-vibe/scripts/lib/teaching.mjs +118 -0
  297. package/plugins/just-vibe/scripts/toolkit.mjs +225 -0
  298. package/plugins/just-vibe/skills/a11y/SKILL.md +8 -0
  299. package/plugins/just-vibe/skills/api-breaking/SKILL.md +56 -0
  300. package/plugins/just-vibe/skills/api-client/SKILL.md +56 -0
  301. package/plugins/just-vibe/skills/api-contract-test/SKILL.md +56 -0
  302. package/plugins/just-vibe/skills/api-design/SKILL.md +56 -0
  303. package/plugins/just-vibe/skills/api-errors/SKILL.md +56 -0
  304. package/plugins/just-vibe/skills/api-openapi/SKILL.md +56 -0
  305. package/plugins/just-vibe/skills/api-pagination/SKILL.md +56 -0
  306. package/plugins/just-vibe/skills/api-webhooks/SKILL.md +56 -0
  307. package/plugins/just-vibe/skills/arch-boundaries/SKILL.md +58 -0
  308. package/plugins/just-vibe/skills/arch-contracts/SKILL.md +56 -0
  309. package/plugins/just-vibe/skills/arch-event-flow/SKILL.md +56 -0
  310. package/plugins/just-vibe/skills/arch-feature/SKILL.md +57 -0
  311. package/plugins/just-vibe/skills/arch-map/SKILL.md +56 -0
  312. package/plugins/just-vibe/skills/arch-modernize/SKILL.md +56 -0
  313. package/plugins/just-vibe/skills/arch-scale/SKILL.md +58 -0
  314. package/plugins/just-vibe/skills/arch-tenancy/SKILL.md +56 -0
  315. package/plugins/just-vibe/skills/auto/SKILL.md +67 -0
  316. package/plugins/just-vibe/skills/automate/SKILL.md +56 -0
  317. package/plugins/just-vibe/skills/backend-auth/SKILL.md +63 -0
  318. package/plugins/just-vibe/skills/backend-cache/SKILL.md +59 -0
  319. package/plugins/just-vibe/skills/backend-concurrency/SKILL.md +58 -0
  320. package/plugins/just-vibe/skills/backend-idempotency/SKILL.md +58 -0
  321. package/plugins/just-vibe/skills/backend-jobs/SKILL.md +56 -0
  322. package/plugins/just-vibe/skills/backend-permissions/SKILL.md +56 -0
  323. package/plugins/just-vibe/skills/backend-resilience/SKILL.md +56 -0
  324. package/plugins/just-vibe/skills/backend-service/SKILL.md +56 -0
  325. package/plugins/just-vibe/skills/brainstorm/SKILL.md +56 -0
  326. package/plugins/just-vibe/skills/build/SKILL.md +56 -0
  327. package/plugins/just-vibe/skills/challenge/SKILL.md +56 -0
  328. package/plugins/just-vibe/skills/checkpoint/SKILL.md +59 -0
  329. package/plugins/just-vibe/skills/ci/SKILL.md +56 -0
  330. package/plugins/just-vibe/skills/cleanup/SKILL.md +56 -0
  331. package/plugins/just-vibe/skills/compare/SKILL.md +56 -0
  332. package/plugins/just-vibe/skills/copy/SKILL.md +56 -0
  333. package/plugins/just-vibe/skills/coverage/SKILL.md +56 -0
  334. package/plugins/just-vibe/skills/data-backfill/SKILL.md +56 -0
  335. package/plugins/just-vibe/skills/data-contract/SKILL.md +56 -0
  336. package/plugins/just-vibe/skills/data-incremental/SKILL.md +56 -0
  337. package/plugins/just-vibe/skills/data-lineage/SKILL.md +56 -0
  338. package/plugins/just-vibe/skills/data-pipeline/SKILL.md +56 -0
  339. package/plugins/just-vibe/skills/data-profile/SKILL.md +56 -0
  340. package/plugins/just-vibe/skills/data-quality/SKILL.md +56 -0
  341. package/plugins/just-vibe/skills/data-reconcile/SKILL.md +56 -0
  342. package/plugins/just-vibe/skills/db-access/SKILL.md +56 -0
  343. package/plugins/just-vibe/skills/db-explain/SKILL.md +56 -0
  344. package/plugins/just-vibe/skills/db-index/SKILL.md +56 -0
  345. package/plugins/just-vibe/skills/db-integrity/SKILL.md +57 -0
  346. package/plugins/just-vibe/skills/db-locks/SKILL.md +56 -0
  347. package/plugins/just-vibe/skills/db-migrate/SKILL.md +63 -0
  348. package/plugins/just-vibe/skills/db-query/SKILL.md +56 -0
  349. package/plugins/just-vibe/skills/db-schema/SKILL.md +56 -0
  350. package/plugins/just-vibe/skills/debug/SKILL.md +56 -0
  351. package/plugins/just-vibe/skills/decide/SKILL.md +57 -0
  352. package/plugins/just-vibe/skills/decision-adr/SKILL.md +56 -0
  353. package/plugins/just-vibe/skills/decision-buy-build/SKILL.md +56 -0
  354. package/plugins/just-vibe/skills/decision-matrix/SKILL.md +56 -0
  355. package/plugins/just-vibe/skills/decision-premortem/SKILL.md +56 -0
  356. package/plugins/just-vibe/skills/decision-reversible/SKILL.md +56 -0
  357. package/plugins/just-vibe/skills/decision-revisit/SKILL.md +56 -0
  358. package/plugins/just-vibe/skills/decision-spike/SKILL.md +58 -0
  359. package/plugins/just-vibe/skills/deploy/SKILL.md +56 -0
  360. package/plugins/just-vibe/skills/deps/SKILL.md +56 -0
  361. package/plugins/just-vibe/skills/design/SKILL.md +56 -0
  362. package/plugins/just-vibe/skills/do/SKILL.md +8 -0
  363. package/plugins/just-vibe/skills/docs/SKILL.md +56 -0
  364. package/plugins/just-vibe/skills/doctor/SKILL.md +58 -0
  365. package/plugins/just-vibe/skills/explain/SKILL.md +56 -0
  366. package/plugins/just-vibe/skills/fix/SKILL.md +57 -0
  367. package/plugins/just-vibe/skills/git-bisect/SKILL.md +58 -0
  368. package/plugins/just-vibe/skills/git-commit/SKILL.md +59 -0
  369. package/plugins/just-vibe/skills/git-conflicts/SKILL.md +58 -0
  370. package/plugins/just-vibe/skills/git-diff/SKILL.md +58 -0
  371. package/plugins/just-vibe/skills/git-recover/SKILL.md +58 -0
  372. package/plugins/just-vibe/skills/git-split/SKILL.md +58 -0
  373. package/plugins/just-vibe/skills/git-status/SKILL.md +58 -0
  374. package/plugins/just-vibe/skills/git-worktree/SKILL.md +58 -0
  375. package/plugins/just-vibe/skills/github-actions/SKILL.md +64 -0
  376. package/plugins/just-vibe/skills/github-address-review/SKILL.md +58 -0
  377. package/plugins/just-vibe/skills/github-fix-ci/SKILL.md +63 -0
  378. package/plugins/just-vibe/skills/github-issue/SKILL.md +58 -0
  379. package/plugins/just-vibe/skills/github-pr/SKILL.md +63 -0
  380. package/plugins/just-vibe/skills/github-release/SKILL.md +58 -0
  381. package/plugins/just-vibe/skills/github-review/SKILL.md +58 -0
  382. package/plugins/just-vibe/skills/github-triage/SKILL.md +58 -0
  383. package/plugins/just-vibe/skills/handoff/SKILL.md +62 -0
  384. package/plugins/just-vibe/skills/help/SKILL.md +63 -0
  385. package/plugins/just-vibe/skills/integrate/SKILL.md +56 -0
  386. package/plugins/just-vibe/skills/learn/SKILL.md +56 -0
  387. package/plugins/just-vibe/skills/llm-cost/SKILL.md +56 -0
  388. package/plugins/just-vibe/skills/llm-evals/SKILL.md +58 -0
  389. package/plugins/just-vibe/skills/llm-injection/SKILL.md +56 -0
  390. package/plugins/just-vibe/skills/llm-prompt/SKILL.md +56 -0
  391. package/plugins/just-vibe/skills/llm-rag/SKILL.md +56 -0
  392. package/plugins/just-vibe/skills/llm-retrieval/SKILL.md +56 -0
  393. package/plugins/just-vibe/skills/llm-structured/SKILL.md +56 -0
  394. package/plugins/just-vibe/skills/llm-tools/SKILL.md +58 -0
  395. package/plugins/just-vibe/skills/map/SKILL.md +56 -0
  396. package/plugins/just-vibe/skills/match/SKILL.md +56 -0
  397. package/plugins/just-vibe/skills/migrate/SKILL.md +56 -0
  398. package/plugins/just-vibe/skills/ml-ablation/SKILL.md +56 -0
  399. package/plugins/just-vibe/skills/ml-baseline/SKILL.md +56 -0
  400. package/plugins/just-vibe/skills/ml-batch/SKILL.md +56 -0
  401. package/plugins/just-vibe/skills/ml-calibrate/SKILL.md +56 -0
  402. package/plugins/just-vibe/skills/ml-dataset/SKILL.md +56 -0
  403. package/plugins/just-vibe/skills/ml-dataset-version/SKILL.md +56 -0
  404. package/plugins/just-vibe/skills/ml-debug-training/SKILL.md +56 -0
  405. package/plugins/just-vibe/skills/ml-drift/SKILL.md +56 -0
  406. package/plugins/just-vibe/skills/ml-error-analysis/SKILL.md +56 -0
  407. package/plugins/just-vibe/skills/ml-evaluate/SKILL.md +56 -0
  408. package/plugins/just-vibe/skills/ml-experiments/SKILL.md +56 -0
  409. package/plugins/just-vibe/skills/ml-explain/SKILL.md +56 -0
  410. package/plugins/just-vibe/skills/ml-features/SKILL.md +59 -0
  411. package/plugins/just-vibe/skills/ml-frame/SKILL.md +56 -0
  412. package/plugins/just-vibe/skills/ml-imbalance/SKILL.md +56 -0
  413. package/plugins/just-vibe/skills/ml-inference-perf/SKILL.md +56 -0
  414. package/plugins/just-vibe/skills/ml-labels/SKILL.md +56 -0
  415. package/plugins/just-vibe/skills/ml-leakage/SKILL.md +64 -0
  416. package/plugins/just-vibe/skills/ml-monitor/SKILL.md +56 -0
  417. package/plugins/just-vibe/skills/ml-package/SKILL.md +56 -0
  418. package/plugins/just-vibe/skills/ml-parity/SKILL.md +56 -0
  419. package/plugins/just-vibe/skills/ml-report/SKILL.md +56 -0
  420. package/plugins/just-vibe/skills/ml-reproduce/SKILL.md +56 -0
  421. package/plugins/just-vibe/skills/ml-robustness/SKILL.md +56 -0
  422. package/plugins/just-vibe/skills/ml-rollout/SKILL.md +56 -0
  423. package/plugins/just-vibe/skills/ml-serving/SKILL.md +56 -0
  424. package/plugins/just-vibe/skills/ml-slices/SKILL.md +56 -0
  425. package/plugins/just-vibe/skills/ml-split/SKILL.md +58 -0
  426. package/plugins/just-vibe/skills/ml-threshold/SKILL.md +56 -0
  427. package/plugins/just-vibe/skills/ml-train/SKILL.md +62 -0
  428. package/plugins/just-vibe/skills/ml-training-cost/SKILL.md +56 -0
  429. package/plugins/just-vibe/skills/ml-tune/SKILL.md +56 -0
  430. package/plugins/just-vibe/skills/ops-alerts/SKILL.md +56 -0
  431. package/plugins/just-vibe/skills/ops-container/SKILL.md +56 -0
  432. package/plugins/just-vibe/skills/ops-incident/SKILL.md +56 -0
  433. package/plugins/just-vibe/skills/ops-logs/SKILL.md +56 -0
  434. package/plugins/just-vibe/skills/ops-observability/SKILL.md +56 -0
  435. package/plugins/just-vibe/skills/ops-postmortem/SKILL.md +56 -0
  436. package/plugins/just-vibe/skills/ops-restore/SKILL.md +56 -0
  437. package/plugins/just-vibe/skills/ops-runbook/SKILL.md +56 -0
  438. package/plugins/just-vibe/skills/orient/SKILL.md +58 -0
  439. package/plugins/just-vibe/skills/perf/SKILL.md +56 -0
  440. package/plugins/just-vibe/skills/plan/SKILL.md +56 -0
  441. package/plugins/just-vibe/skills/polish/SKILL.md +56 -0
  442. package/plugins/just-vibe/skills/pr/SKILL.md +58 -0
  443. package/plugins/just-vibe/skills/profile/SKILL.md +66 -0
  444. package/plugins/just-vibe/skills/profiles/SKILL.md +58 -0
  445. package/plugins/just-vibe/skills/react-async/SKILL.md +57 -0
  446. package/plugins/just-vibe/skills/react-audit/SKILL.md +56 -0
  447. package/plugins/just-vibe/skills/react-component/SKILL.md +65 -0
  448. package/plugins/just-vibe/skills/react-effects/SKILL.md +58 -0
  449. package/plugins/just-vibe/skills/react-forms/SKILL.md +56 -0
  450. package/plugins/just-vibe/skills/react-hydration/SKILL.md +57 -0
  451. package/plugins/just-vibe/skills/react-rerenders/SKILL.md +56 -0
  452. package/plugins/just-vibe/skills/react-state/SKILL.md +56 -0
  453. package/plugins/just-vibe/skills/refactor/SKILL.md +56 -0
  454. package/plugins/just-vibe/skills/release/SKILL.md +58 -0
  455. package/plugins/just-vibe/skills/remember/SKILL.md +70 -0
  456. package/plugins/just-vibe/skills/repro/SKILL.md +56 -0
  457. package/plugins/just-vibe/skills/research/SKILL.md +56 -0
  458. package/plugins/just-vibe/skills/responsive/SKILL.md +8 -0
  459. package/plugins/just-vibe/skills/resume/SKILL.md +63 -0
  460. package/plugins/just-vibe/skills/review/SKILL.md +56 -0
  461. package/plugins/just-vibe/skills/scope/SKILL.md +56 -0
  462. package/plugins/just-vibe/skills/security/SKILL.md +56 -0
  463. package/plugins/just-vibe/skills/security-authz/SKILL.md +56 -0
  464. package/plugins/just-vibe/skills/security-config/SKILL.md +56 -0
  465. package/plugins/just-vibe/skills/security-dependencies/SKILL.md +56 -0
  466. package/plugins/just-vibe/skills/security-fix/SKILL.md +56 -0
  467. package/plugins/just-vibe/skills/security-inputs/SKILL.md +56 -0
  468. package/plugins/just-vibe/skills/security-secrets/SKILL.md +56 -0
  469. package/plugins/just-vibe/skills/security-threat-model/SKILL.md +56 -0
  470. package/plugins/just-vibe/skills/security-uploads/SKILL.md +56 -0
  471. package/plugins/just-vibe/skills/setup/SKILL.md +60 -0
  472. package/plugins/just-vibe/skills/skill/SKILL.md +56 -0
  473. package/plugins/just-vibe/skills/spec/SKILL.md +56 -0
  474. package/plugins/just-vibe/skills/tasks/SKILL.md +56 -0
  475. package/plugins/just-vibe/skills/teach/SKILL.md +63 -0
  476. package/plugins/just-vibe/skills/teach-test/SKILL.md +65 -0
  477. package/plugins/just-vibe/skills/test/SKILL.md +56 -0
  478. package/plugins/just-vibe/skills/test-e2e/SKILL.md +56 -0
  479. package/plugins/just-vibe/skills/test-fixtures/SKILL.md +56 -0
  480. package/plugins/just-vibe/skills/test-flaky/SKILL.md +56 -0
  481. package/plugins/just-vibe/skills/test-integration/SKILL.md +56 -0
  482. package/plugins/just-vibe/skills/test-load/SKILL.md +56 -0
  483. package/plugins/just-vibe/skills/test-property/SKILL.md +56 -0
  484. package/plugins/just-vibe/skills/test-regression/SKILL.md +57 -0
  485. package/plugins/just-vibe/skills/test-unit/SKILL.md +56 -0
  486. package/plugins/just-vibe/skills/tools/SKILL.md +64 -0
  487. package/plugins/just-vibe/skills/trace/SKILL.md +56 -0
  488. package/plugins/just-vibe/skills/ui-accessibility/SKILL.md +57 -0
  489. package/plugins/just-vibe/skills/ui-audit/SKILL.md +56 -0
  490. package/plugins/just-vibe/skills/ui-flow/SKILL.md +56 -0
  491. package/plugins/just-vibe/skills/ui-motion/SKILL.md +56 -0
  492. package/plugins/just-vibe/skills/ui-responsive/SKILL.md +56 -0
  493. package/plugins/just-vibe/skills/ui-states/SKILL.md +56 -0
  494. package/plugins/just-vibe/skills/ui-system/SKILL.md +56 -0
  495. package/plugins/just-vibe/skills/ui-visual-diff/SKILL.md +56 -0
  496. package/plugins/just-vibe/skills/vercel-audit/SKILL.md +56 -0
  497. package/plugins/just-vibe/skills/vercel-build-fix/SKILL.md +63 -0
  498. package/plugins/just-vibe/skills/vercel-env/SKILL.md +56 -0
  499. package/plugins/just-vibe/skills/vercel-performance/SKILL.md +56 -0
  500. package/plugins/just-vibe/skills/vercel-preview/SKILL.md +56 -0
  501. package/plugins/just-vibe/skills/vercel-release-check/SKILL.md +57 -0
  502. package/plugins/just-vibe/skills/vercel-routing/SKILL.md +56 -0
  503. package/plugins/just-vibe/skills/vercel-runtime/SKILL.md +61 -0
  504. package/plugins/just-vibe/skills/verify/SKILL.md +59 -0
  505. package/plugins/just-vibe/skills/vite-assets/SKILL.md +57 -0
  506. package/plugins/just-vibe/skills/vite-bundle/SKILL.md +58 -0
  507. package/plugins/just-vibe/skills/vite-chunks/SKILL.md +56 -0
  508. package/plugins/just-vibe/skills/vite-config/SKILL.md +56 -0
  509. package/plugins/just-vibe/skills/vite-env/SKILL.md +56 -0
  510. package/plugins/just-vibe/skills/vite-hmr/SKILL.md +56 -0
  511. package/plugins/just-vibe/skills/vite-setup/SKILL.md +56 -0
  512. package/plugins/just-vibe/skills/vite-upgrade/SKILL.md +56 -0
@@ -0,0 +1,60 @@
1
+ import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { join, resolve, relative } from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ export { findExecutable } from './command.mjs';
5
+
6
+ const ignored = new Set(['.git', 'node_modules', '.venv', 'venv', 'dist', 'build', 'coverage', '.next', '.tmp', '.cache', '.codex', '.claude']);
7
+ const secretName = /(?:^\.env(?:\.|$)|\.(?:pem|key|p12|pfx)$|credentials|secrets?\.)/i;
8
+ const interesting = /^(?:AGENTS\.md|CLAUDE\.md|README(?:\.md)?|package\.json|pyproject\.toml|requirements[^/]*\.txt|Cargo\.toml|go\.mod|Dockerfile[^/]*|compose\.ya?ml|(?:vite|next|vitest|jest|playwright)\.config\.[a-z]+|vercel\.json|prisma\.schema|schema\.prisma|Gemfile|pom\.xml|Makefile)$/;
9
+
10
+ export function gitRead(root, args) {
11
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_')));
12
+ const result = spawnSync('git', ['--no-optional-locks', '--no-pager', '--no-replace-objects', '-c', 'core.fsmonitor=false', '-C', root, ...args], {
13
+ encoding: 'utf8', timeout: 5000, maxBuffer: 512 * 1024, shell: false, stdio: ['ignore', 'pipe', 'pipe'],
14
+ env: { ...env, GIT_TERMINAL_PROMPT: '0' },
15
+ });
16
+ return result.status === 0 ? result.stdout.trim() : null;
17
+ }
18
+
19
+ export function inspectProject(path = process.cwd(), { maxEntries = 3000, maxDepth = 5, git = gitRead } = {}) {
20
+ if (!Number.isInteger(maxEntries) || maxEntries < 1 || maxEntries > 10000) throw new Error('Invalid inspection entry budget.');
21
+ if (!Number.isInteger(maxDepth) || maxDepth < 0 || maxDepth > 10) throw new Error('Invalid inspection depth.');
22
+ const root = realpathSync(resolve(path));
23
+ if (!statSync(root).isDirectory()) throw new Error('Project root must be a directory.');
24
+ const manifests = [], packages = [], instructions = [], errors = [];
25
+ let scanned = 0, truncated = false;
26
+ const queue = [{ path: root, depth: 0 }];
27
+ while (queue.length && scanned < maxEntries) {
28
+ const current = queue.shift();
29
+ let entries;
30
+ try { entries = readdirSync(current.path, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); }
31
+ catch { errors.push(`Cannot read directory: ${relative(root, current.path) || '.'}`); continue; }
32
+ for (const entry of entries) {
33
+ if (++scanned > maxEntries) { truncated = true; break; }
34
+ if (entry.isSymbolicLink() || ignored.has(entry.name) || secretName.test(entry.name)) continue;
35
+ const file = join(current.path, entry.name);
36
+ if (entry.isDirectory()) {
37
+ if (current.depth < maxDepth) queue.push({ path: file, depth: current.depth + 1 });
38
+ else truncated = true;
39
+ continue;
40
+ }
41
+ if (!entry.isFile() || !interesting.test(entry.name)) continue;
42
+ const rel = relative(root, file);
43
+ manifests.push(rel);
44
+ if (['AGENTS.md', 'CLAUDE.md'].includes(entry.name)) instructions.push(rel);
45
+ if (entry.name === 'package.json') {
46
+ try {
47
+ if (statSync(file).size > 256 * 1024) throw new Error('size');
48
+ const pkg = JSON.parse(readFileSync(file, 'utf8'));
49
+ packages.push({ path: rel, scripts: Object.keys(pkg.scripts || {}),
50
+ dependencies: Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).sort() });
51
+ } catch { errors.push(`Cannot parse bounded manifest: ${rel}`); }
52
+ }
53
+ }
54
+ }
55
+ if (queue.length) truncated = true;
56
+ const gitRoot = git(root, ['rev-parse', '--show-toplevel']);
57
+ return { root, manifests, packages, instructions, errors, truncated, scanned: Math.min(scanned, maxEntries),
58
+ git: gitRoot ? { root: gitRoot, head: git(root, ['rev-parse', '--verify', 'HEAD']), branch: git(root, ['symbolic-ref', '--quiet', '--short', 'HEAD']) } : null,
59
+ note: 'Read-only inventory. Script names are discovered, not executed. No environment values, config code, hooks, or package scripts are evaluated. Ancestor instructions must also be read by the host.' };
60
+ }
@@ -0,0 +1,82 @@
1
+ import { inspectProject } from './project.mjs';
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { readPreferences } from './continuity.mjs';
5
+
6
+ const starter = ['auto', 'fix', 'explain', 'plan', 'review', 'test', 'teach', 'tools', 'profile', 'checkpoint', 'resume', 'help'];
7
+ const intents = [
8
+ { test: /\b(?:claude|agents)\.md\b|\b(?:save|remember|persist|preserve)\b.*\b(?:conversation|project instructions|context|decisions|corrections)\b/i, ids: ['remember'], reason: 'Durable project instructions from conversation context' },
9
+ { test: /\b(?:ci|checks?|actions|pipeline)\b.*\b(?:fail|broken|red)|\b(?:fail|broken|red)\w*\b.*\b(?:ci|checks?|actions|pipeline)\b/i, ids: ['github-fix-ci', 'github-actions'], reason: 'Failing CI/checks' },
10
+ { test: /\b(?:refresh|oauth|login|logout|session|password reset|authentication)\b/i, ids: ['backend-auth'], reason: 'Identity or session lifecycle' },
11
+ { test: /\b(?:dialog|combobox|date picker|datepicker|component)\b/i, ids: ['react-component', 'ui-component'], reason: 'Component interaction contract' },
12
+ { test: /\b(?:race|stale|out.of.order)\b.*\b(?:request|response|fetch)|\b(?:request|response|fetch)\b.*\b(?:race|stale|out.of.order)\b/i, ids: ['react-async', 'backend-concurrency'], reason: 'Request ordering and ownership' },
13
+ { test: /\b(?:freeze|freezes|slow|rerender|re-render)\b.*\b(?:filter|grid|render|component)|\b(?:filter|grid|render|component)\b.*\b(?:freeze|freezes|slow|rerender|re-render)\b/i, ids: ['react-rerenders'], reason: 'Slow UI interaction' },
14
+ { test: /\b(?:offline|validation)\b.*\b(?:production|serving|live)\b/i, ids: ['ml-parity', 'ml-leakage'], reason: 'Offline versus serving mismatch' },
15
+ { test: /\b(?:leakage|future information|look.?ahead|point.in.time)\b/i, ids: ['ml-leakage', 'ml-features'], reason: 'Prediction-time information boundary' },
16
+ { test: /\b(?:train|training)\b.*\b(?:resume|checkpoint|interrupt)|\b(?:resume|checkpoint)\b.*\b(?:train|training)\b/i, ids: ['ml-train', 'ml-reproduce'], reason: 'Training continuation state' },
17
+ { test: /\b(?:vercel|preview|deployment)\b.*\b(?:build|fail|broken)|\bbuild\b.*\b(?:vercel|preview|deployment)\b/i, ids: ['vercel-build-fix'], reason: 'Deployment build diagnosis' },
18
+ { test: /\b(?:migration|migrations|schema change)\b/i, ids: ['db-migrate'], reason: 'Schema rollout and recovery' },
19
+ { test: /\b(?:partial|staged|unstaged|only these|selected files)\b.*\bcommit|\bcommit\b.*\b(?:partial|staged|unstaged|only these|selected files)\b/i, ids: ['git-commit', 'git-split'], reason: 'Commit scope and index preservation' },
20
+ { test: /\b(?:which|compare|choose|trade.?off|decide)\b/i, ids: ['decide'], reason: 'A decision rather than immediate implementation' },
21
+ ];
22
+ const stackPacks = { react: ['react', 'ui'], vite: ['vite'], next: ['react', 'ui', 'vercel'], prisma: ['database'], torch: ['ml-experiments', 'ml-data'], 'scikit-learn': ['ml-experiments', 'ml-data'], django: ['backend'], fastapi: ['backend'], postgres: ['database'] };
23
+
24
+ export function routeContext(root) {
25
+ const project = inspectProject(root);
26
+ const dependencies = new Set(project.packages.flatMap(p => p.dependencies));
27
+ for (const manifest of project.manifests.filter(p => /(?:pyproject\.toml|requirements[^/]*\.txt)$/.test(p))) {
28
+ const path = join(project.root, manifest);
29
+ if (statSync(path).size <= 256 * 1024) {
30
+ const source = readFileSync(path, 'utf8');
31
+ for (const dependency of ['torch', 'scikit-learn', 'django', 'fastapi']) if (new RegExp(`(?:^|[\\s"'\\[,])${dependency}(?=[\\s"'\\]<>=!~;]|$)`, 'mi').test(source)) dependencies.add(dependency);
32
+ }
33
+ }
34
+ // Parse bounded manifest text only; never import framework configuration.
35
+ const frameworks = Object.keys(stackPacks).filter(key => dependencies.has(key) || (key === 'prisma' && dependencies.has('@prisma/client')));
36
+ let preferences = null, preferenceError = null;
37
+ try { preferences = readPreferences(root)?.preferences || null; } catch (error) { preferenceError = error.message; }
38
+ return { frameworks, packs: [...new Set(frameworks.flatMap(f => stackPacks[f]))], manifests: project.manifests, packages: project.packages.map(p => p.path), truncated: project.truncated,
39
+ preferences, preferenceError, preferencePolicy: 'Saved context only. The host must resolve conflicts with the current request; a saved profile is not an active pin or permission.' };
40
+ }
41
+
42
+ export function intentSignals(brief) {
43
+ // Ignore ordinary negative clauses for ranking only. The full brief is returned unchanged.
44
+ const excluded = [...brief.matchAll(/\b(?:do not|don't|never|without)\s+([^.;\n]+)/gi)].map(m => m[0]);
45
+ const positive = brief.replace(/\b(?:do not|don't|never|without)\s+[^.;\n]+/gi, ' ');
46
+ return { positive, excluded, matches: intents.filter(rule => rule.test.test(positive)) };
47
+ }
48
+
49
+ export function rankCandidates(candidates, brief, context) {
50
+ const { positive, excluded, matches } = intentSignals(brief);
51
+ const named = new Set((positive.match(/\b[a-z][a-z0-9]*(?:-[a-z0-9]+)+\b/g) || []));
52
+ return candidates.map(c => {
53
+ const reasons = [];
54
+ let score = c.score;
55
+ if (named.has(c.id) || c.matchedNames.some(id => named.has(id))) { score += 1000; reasons.push('Workflow named explicitly'); }
56
+ for (const rule of matches) if (rule.ids.includes(c.id)) { score += 45 - rule.ids.indexOf(c.id) * 8; reasons.push(rule.reason); }
57
+ if (context.packs.includes(c.pack)) { score += 10; reasons.push(`Fits detected ${context.frameworks.join(', ')} project`); }
58
+ if (c.status === 'available') { score += 3; reasons.push('Declared prerequisites observed'); }
59
+ else reasons.push(`Prerequisites ${c.status}; inspect before execution`);
60
+ if (!reasons.length) reasons.push('Request terms match this workflow');
61
+ return { ...c, score, selectionReasons: reasons, excludedClauses: excluded };
62
+ }).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
63
+ }
64
+
65
+ export function executionStrategy(brief, candidates = []) {
66
+ const { positive } = intentSignals(brief);
67
+ const reasons = [];
68
+ if (/\b(?:deploy|publish|push|merge|provision|production migration|paid|purchase)\b/i.test(positive)) reasons.push('External or consequential effects need explicit tracking');
69
+ if (/\b(?:resume|interrupted|handoff|checkpoint|record every|track every)\b/i.test(positive)) reasons.push('Continuation or recorded history requested');
70
+ if ((positive.match(/\b(?:then|after that|next|finally)\b|\n\s*\d+[.)]/gi) || []).length >= 2) reasons.push('Several dependent stages');
71
+ if (candidates.slice(0, 3).filter(c => c.status !== 'available').length === 3) reasons.push('Selected route needs unresolved capabilities');
72
+ return { suggested: reasons.length ? 'tracked' : 'quick', reasons: reasons.length ? reasons : ['Start with a bounded local workflow; escalate if dependencies, retries or effects make tracking useful'], instruction: 'A suggestion, not a permission decision. The host resolves actual scope and effects. Quick work still preserves constraints, verifies results and reports limitations; switching to tracked work retains prior evidence and consumed budgets.' };
73
+ }
74
+
75
+ export function starterIds(context) {
76
+ const additions = [];
77
+ if (context.frameworks.includes('react') || context.frameworks.includes('next')) additions.push('react-component', 'react-async');
78
+ if (context.frameworks.includes('vite')) additions.push('vite-bundle');
79
+ if (context.frameworks.includes('prisma')) additions.push('db-migrate');
80
+ if (context.frameworks.includes('torch') || context.frameworks.includes('scikit-learn')) additions.push('ml-train', 'ml-evaluate');
81
+ return [...new Set([...starter, ...additions])];
82
+ }
@@ -0,0 +1,248 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { realpathSync, lstatSync, statSync } from 'node:fs';
3
+ import { resolve, dirname, relative, isAbsolute } from 'node:path';
4
+ import { getCommand, availability, MODES } from './catalog.mjs';
5
+ import { loadProfiles, selectProfiles, validateProfileSelection } from './profiles.mjs';
6
+
7
+ const terminal = new Set(['completed', 'partial', 'blocked', 'failed', 'cancelled']);
8
+ const effects = new Set(['read', 'plan-artifact', 'local-write', 'external-write', 'destructive', 'paid']);
9
+ const clone = value => structuredClone(value);
10
+ const required = (value, name) => {
11
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} is required.`);
12
+ return value;
13
+ };
14
+ function positive(value, name, max) {
15
+ if (!Number.isInteger(value) || value < 1 || value > max) throw new Error(`${name} must be an integer from 1 to ${max}.`);
16
+ return value;
17
+ }
18
+
19
+ export function insideProject(root, path) {
20
+ const base = realpathSync(resolve(root));
21
+ const target = resolve(base, path);
22
+ // Resolve the closest existing ancestor so a missing leaf cannot hide a symlink escape.
23
+ let ancestor = target;
24
+ while (true) {
25
+ try { lstatSync(ancestor); break; } catch (error) { if (error.code !== 'ENOENT') throw error; }
26
+ const next = dirname(ancestor);
27
+ if (next === ancestor) throw new Error('Cannot resolve target path.');
28
+ ancestor = next;
29
+ }
30
+ const actual = resolve(realpathSync(ancestor), relative(ancestor, target));
31
+ const rel = relative(base, actual);
32
+ if (rel === '..' || rel.startsWith('../') || rel.startsWith('..\\') || isAbsolute(rel)) throw new Error('Target escapes the selected project.');
33
+ if (rel.split(/[\\/]/).includes('.git')) throw new Error('Do not modify Git internals through a workflow target.');
34
+ return actual;
35
+ }
36
+
37
+ export function createRun(catalog, invokedAs, options, now = Date.now()) {
38
+ const command = getCommand(catalog, invokedAs, { canonical: true });
39
+ const brief = required(options.brief, 'Original brief');
40
+ const root = realpathSync(resolve(options.root || process.cwd()));
41
+ if (!statSync(root).isDirectory()) throw new Error('Run root must be a directory.');
42
+ const mode = options.mode || command.defaultMode;
43
+ if (!MODES.includes(mode)) throw new Error('Invalid mode.');
44
+ const context = options.context || {};
45
+ if (context.profile !== undefined && context.profile !== null) validateProfileSelection(loadProfiles(), context.profile);
46
+ if (options.profile && context.profile) throw new Error('Use --profile or context.profile, not both.');
47
+ const profile = options.profile ? selectProfiles(loadProfiles(), { primary: options.profile, selectedBy: 'user', reason: 'Explicit workflow profile option.' }) : context.profile;
48
+ for (const key of ['constraints', 'references', 'successCriteria', 'assumptions', 'authorization']) {
49
+ if (context[key] !== undefined && !Array.isArray(context[key])) throw new Error(`context.${key} must be an array.`);
50
+ }
51
+ const scope = options.scope ? insideProject(root, options.scope) : root;
52
+ const budget = {
53
+ maxStages: positive(options.budget?.maxStages ?? 8, 'maxStages', 100),
54
+ maxAttempts: positive(options.budget?.maxAttempts ?? 3, 'maxAttempts', 20),
55
+ maxMinutes: positive(options.budget?.maxMinutes ?? 60, 'maxMinutes', 1440),
56
+ };
57
+ return { schemaVersion: 1, id: randomUUID(), invokedAs, command: command.id, root, scope, mode,
58
+ brief, context: { objective: brief, constraints: [], references: [], successCriteria: [], assumptions: [], authorization: [], ...clone(context), ...(profile ? { profile: clone(profile) } : {}) },
59
+ budget, createdAt: new Date(now).toISOString(), status: 'ready', stages: [],
60
+ note: 'This record validates workflow state. It does not sandbox the host or independently prove evidence/authorization. Host tool permissions still apply.' };
61
+ }
62
+
63
+ export function validateRun(run) {
64
+ if (run?.schemaVersion !== 1 || !Array.isArray(run.stages) || !MODES.includes(run.mode)
65
+ || !['ready', 'running', ...terminal].includes(run.status)) throw new Error('Invalid run record.');
66
+ required(run.id, 'Run id'); required(run.brief, 'Original brief');
67
+ required(run.root, 'Root'); required(run.scope, 'Scope');
68
+ if (!run.context || !Array.isArray(run.context.authorization) || !Array.isArray(run.context.successCriteria)) throw new Error('Invalid run context.');
69
+ if (run.context.profile !== undefined && run.context.profile !== null) validateProfileSelection(loadProfiles(), run.context.profile);
70
+ for (const [name, max] of [['maxStages', 100], ['maxAttempts', 20], ['maxMinutes', 1440]]) positive(run.budget?.[name], name, max);
71
+ if (!Number.isFinite(Date.parse(run.createdAt))) throw new Error('Invalid run timestamp.');
72
+ return run;
73
+ }
74
+
75
+ export function setRunProfiles(run, request, now = Date.now()) {
76
+ active(run, now);
77
+ const selection = selectProfiles(loadProfiles(), request, run.context.profile);
78
+ const result = clone(run);
79
+ result.context.profile = selection;
80
+ result.profileHistory ??= [];
81
+ result.profileHistory.push({ previous: clone(run.context.profile ?? null), next: clone(selection), selectedBy: request.selectedBy,
82
+ reason: request.reason, changedAt: new Date(now).toISOString() });
83
+ return result;
84
+ }
85
+
86
+ function active(run, now) {
87
+ validateRun(run);
88
+ if (terminal.has(run.status)) throw new Error(`Run is ${run.status}; resume explicitly before continuing.`);
89
+ if (now - Date.parse(run.createdAt) >= run.budget.maxMinutes * 60000) throw new Error('Run time budget exhausted.');
90
+ }
91
+
92
+ export function authorizeEffect(run, { effect, target, action }) {
93
+ if (!effects.has(effect)) throw new Error('Unknown effect class.');
94
+ required(target, 'Action target'); required(action, 'Action description');
95
+ if (run.mode === 'inspect' && effect !== 'read') throw new Error('Inspect mode permits read-only stages.');
96
+ if (run.mode === 'plan' && !['read', 'plan-artifact'].includes(effect)) throw new Error('Plan mode cannot execute changes.');
97
+ if (['local-write', 'plan-artifact'].includes(effect)) {
98
+ const path = insideProject(run.root, target);
99
+ const scope = insideProject(run.root, run.scope);
100
+ const rel = relative(scope, path);
101
+ if (rel === '..' || rel.startsWith('../') || rel.startsWith('..\\') || isAbsolute(rel)) throw new Error('Write target escapes requested scope.');
102
+ }
103
+ if (['plan-artifact', 'external-write', 'destructive', 'paid'].includes(effect)) {
104
+ const grant = run.context.authorization.find(a => {
105
+ if (a?.effect !== effect || a.action !== action || typeof a.basis !== 'string' || !a.basis.trim()) return false;
106
+ if (effect !== 'plan-artifact') return a.target === target;
107
+ try { return insideProject(run.root, a.target) === insideProject(run.root, target); } catch { return false; }
108
+ });
109
+ if (!grant) throw new Error('Missing exact action/target authorization from the user session.');
110
+ }
111
+ }
112
+
113
+ export function startStage(catalog, run, request, capabilities, host = 'claude', now = Date.now()) {
114
+ active(run, now);
115
+ if (run.stages.some(s => s.status === 'running')) throw new Error('Finish the running stage before starting another.');
116
+ const command = getCommand(catalog, request.command, { canonical: true });
117
+ if (command.id === 'auto') throw new Error('Recursive auto/do routing is not allowed.');
118
+ if (availability(catalog, command, capabilities, host).status !== 'available') throw new Error(`Workflow prerequisites are not verified: ${command.id}`);
119
+ const id = request.id || randomUUID();
120
+ let stage = run.stages.find(s => s.id === id);
121
+ if (stage) {
122
+ if (!['failed', 'blocked'].includes(stage.status) || stage.command !== command.id) throw new Error('Only the same failed or blocked stage may be retried.');
123
+ if (stage.attempts.length >= run.budget.maxAttempts) throw new Error('Stage retry budget exhausted.');
124
+ required(request.newEvidence, 'New evidence supporting retry');
125
+ } else if (run.stages.length >= run.budget.maxStages) throw new Error('Workflow stage budget exhausted.');
126
+ const effect = request.effect || 'read';
127
+ const target = request.target || run.scope;
128
+ const action = required(request.action, 'Stage action');
129
+ authorizeEffect(run, { effect, target, action });
130
+ const result = clone(run);
131
+ stage = result.stages.find(s => s.id === id);
132
+ if (!stage) { stage = { id, command: command.id, attempts: [] }; result.stages.push(stage); }
133
+ stage.status = 'running';
134
+ stage.attempts.push({ effect, target, action, startedAt: new Date(now).toISOString(), newEvidence: request.newEvidence || null });
135
+ result.status = 'running';
136
+ return result;
137
+ }
138
+
139
+ function checkEvidence(evidence, criteria, completed) {
140
+ if (!Array.isArray(evidence) || !Array.isArray(criteria)) throw new Error('Evidence and criteria arrays are required.');
141
+ for (const e of evidence) {
142
+ if (!e || !['pass', 'fail', 'unverified'].includes(e.result)) throw new Error('Invalid evidence result.');
143
+ required(e.reference, 'Evidence reference'); required(e.detail, 'Evidence detail');
144
+ }
145
+ for (const c of criteria) {
146
+ required(c.criterion, 'Criterion');
147
+ if (!['pass', 'fail', 'unverified'].includes(c.result) || !Array.isArray(c.evidence)
148
+ || c.evidence.some(i => !Number.isInteger(i) || !evidence[i])) throw new Error('Invalid criterion evidence linkage.');
149
+ if (c.result === 'pass' && (!c.evidence.length || c.evidence.some(i => evidence[i].result !== 'pass'))) throw new Error('Passing criteria need passing evidence.');
150
+ }
151
+ if (completed && (!criteria.length || criteria.some(c => c.result !== 'pass') || evidence.some(e => e.result !== 'pass'))) throw new Error('Completion requires verified criteria; failed/unverified evidence cannot become a pass.');
152
+ }
153
+
154
+ export function recordStage(run, outcome, now = Date.now()) {
155
+ validateRun(run);
156
+ if (terminal.has(run.status)) throw new Error('Run is already terminal.');
157
+ const result = clone(run);
158
+ const stage = result.stages.find(s => s.id === outcome.id && s.status === 'running');
159
+ if (!stage) throw new Error('No matching running stage.');
160
+ if (!['completed', 'blocked', 'failed', 'cancelled'].includes(outcome.status)) throw new Error('Invalid stage outcome.');
161
+ required(outcome.summary, 'Outcome summary');
162
+ checkEvidence(outcome.evidence || [], outcome.criteria || [], outcome.status === 'completed');
163
+ Object.assign(stage.attempts.at(-1), { finishedAt: new Date(now).toISOString(),
164
+ status: outcome.status, summary: outcome.summary,
165
+ evidence: clone(outcome.evidence || []), criteria: clone(outcome.criteria || []) });
166
+ stage.status = outcome.status;
167
+ result.status = 'ready';
168
+ return result;
169
+ }
170
+
171
+ // Add an action to a running attempt without restarting it or losing its history.
172
+ // Multiple effects belong to the same action (e.g. external-write AND paid).
173
+ export function amendStage(run, request, now = Date.now()) {
174
+ active(run, now);
175
+ const stage = run.stages.find(s => s.id === request.id && s.status === 'running');
176
+ if (!stage) throw new Error('No matching running stage.');
177
+ if (!Array.isArray(request.effects) || !request.effects.length
178
+ || new Set(request.effects).size !== request.effects.length) throw new Error('Distinct action effects are required.');
179
+ for (const effect of request.effects) authorizeEffect(run, { ...request, effect });
180
+ const result = clone(run);
181
+ const attempt = result.stages.find(s => s.id === request.id).attempts.at(-1);
182
+ attempt.actions ??= [];
183
+ attempt.actions.push({ action: request.action, target: request.target, effects: [...request.effects], checkedAt: new Date(now).toISOString() });
184
+ return result;
185
+ }
186
+
187
+ export function supersedeStage(run, resolution, now = Date.now()) {
188
+ active(run, now);
189
+ const source = run.stages.find(s => s.id === resolution.id);
190
+ if (!source || !['failed', 'blocked'].includes(source.status)) throw new Error('Only a failed or blocked stage can be superseded.');
191
+ required(resolution.reason, 'Supersession reason');
192
+ const ids = resolution.replacements;
193
+ if (!Array.isArray(ids) || !ids.length || new Set(ids).size !== ids.length || ids.includes(source.id)) throw new Error('Distinct replacement stages are required.');
194
+ const replacements = ids.map(id => run.stages.find(s => s.id === id));
195
+ if (replacements.some(s => !s || s.status !== 'completed')) throw new Error('Replacement stages must already be completed.');
196
+ checkEvidence(resolution.evidence, resolution.criteria, true);
197
+ const uncertainEffects = source.attempts.some(a => ['external-write', 'destructive', 'paid'].includes(a.effect)
198
+ || a.actions?.some(action => action.effects.some(effect => ['external-write', 'destructive', 'paid'].includes(effect))));
199
+ if (uncertainEffects) {
200
+ const evidence = resolution.effectReconciliation;
201
+ if (!evidence || evidence.result !== 'pass') throw new Error('External effects need successful reconciliation evidence before supersession.');
202
+ checkEvidence([evidence], [{ criterion: 'Prior effects reconciled', result: 'pass', evidence: [0] }], true);
203
+ }
204
+ const covered = new Set(replacements.flatMap(s => s.attempts.at(-1).criteria.filter(c => c.result === 'pass').map(c => c.criterion)));
205
+ const obligations = new Set([...(source.attempts.at(-1).criteria || []).map(c => c.criterion), ...resolution.criteria.map(c => c.criterion)]);
206
+ if ([...obligations].some(c => !covered.has(c))) throw new Error('Replacement evidence does not cover the superseded criteria.');
207
+ const result = clone(run);
208
+ const stage = result.stages.find(s => s.id === source.id);
209
+ stage.status = 'superseded';
210
+ stage.resolution = { reason: resolution.reason, replacements: [...ids], evidence: clone(resolution.evidence), criteria: clone(resolution.criteria),
211
+ ...(resolution.effectReconciliation ? { effectReconciliation: clone(resolution.effectReconciliation) } : {}), resolvedAt: new Date(now).toISOString() };
212
+ return result;
213
+ }
214
+
215
+ export function finishRun(run, outcome, now = Date.now()) {
216
+ validateRun(run);
217
+ if (terminal.has(run.status)) throw new Error('Run is already terminal.');
218
+ if (!terminal.has(outcome.status)) throw new Error('Invalid terminal status.');
219
+ if (run.stages.some(s => s.status === 'running')) throw new Error('Record running stage results before finishing.');
220
+ required(outcome.summary, 'Run summary');
221
+ checkEvidence(outcome.evidence || [], outcome.criteria || [], outcome.status === 'completed');
222
+ if (outcome.status === 'completed') {
223
+ if (!run.stages.length || run.stages.some(s => !['completed', 'superseded'].includes(s.status))) throw new Error('Unfinished stages prevent completion.');
224
+ for (const stage of run.stages.filter(s => s.status === 'superseded')) {
225
+ if (!stage.resolution?.replacements?.length || stage.resolution.replacements.some(id => id === stage.id || !run.stages.some(s => s.id === id && s.status === 'completed'))) throw new Error('Superseded stages require completed replacements.');
226
+ checkEvidence(stage.resolution.evidence, stage.resolution.criteria, true);
227
+ }
228
+ const covered = new Set((outcome.criteria || []).filter(c => c.result === 'pass').map(c => c.criterion));
229
+ if (run.context.successCriteria.some(c => !covered.has(c))) throw new Error('Original success criteria have not all been verified.');
230
+ }
231
+ return { ...clone(run), status: outcome.status, finishedAt: new Date(now).toISOString(), outcome: clone(outcome) };
232
+ }
233
+
234
+ export function resumeRun(run, observation, now = Date.now()) {
235
+ validateRun(run);
236
+ if (['completed', 'cancelled'].includes(run.status)) throw new Error('Start a new run for completed or cancelled work.');
237
+ if (realpathSync(resolve(observation.root)) !== realpathSync(run.root)) throw new Error('Resume target differs from the original project.');
238
+ required(observation.summary, 'Current-state revalidation');
239
+ if (!Array.isArray(observation.evidence) || !observation.evidence.length) throw new Error('Resume requires current evidence.');
240
+ if (run.stages.some(s => s.status === 'running')) throw new Error('Reconcile the interrupted stage before resuming; do not replay uncertain effects.');
241
+ // Preserve consumed stages/attempts/time. A new budget requires an explicit new run.
242
+ if (now - Date.parse(run.createdAt) >= run.budget.maxMinutes * 60000) throw new Error('Budget expired; create a new run with explicit budget and prior evidence.');
243
+ const result = { ...clone(run), status: 'ready', resumeObservations: [...(run.resumeObservations || []), clone(observation)] };
244
+ if (result.outcome) result.previousOutcomes = [...(result.previousOutcomes || []), { ...result.outcome, finishedAt: result.finishedAt }];
245
+ delete result.outcome;
246
+ delete result.finishedAt;
247
+ return result;
248
+ }
@@ -0,0 +1,84 @@
1
+ import { existsSync, lstatSync, realpathSync, readFileSync, mkdirSync, openSync, closeSync, writeFileSync, renameSync, unlinkSync, readdirSync } from 'node:fs';
2
+ import { resolve, join, relative, sep, dirname, isAbsolute } from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { gitRead } from './project.mjs';
5
+
6
+ export const digest = value => createHash('sha256').update(value).digest('hex');
7
+ export const projectRoot = root => realpathSync(resolve(root));
8
+ export const privateName = name => /(?:^\.env(?:\.|$)|\.(?:pem|key|p12|pfx)$|credentials|secrets?\.)/i.test(name);
9
+
10
+ export function within(root, path) {
11
+ const base = projectRoot(root), full = resolve(base, path), rel = relative(base, full);
12
+ if (isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`)) throw Error('Path escapes the selected project.');
13
+ let cursor = base;
14
+ for (const part of rel.split(sep).filter(Boolean)) {
15
+ cursor = join(cursor, part);
16
+ if (existsSync(cursor) || (() => { try { return lstatSync(cursor).isSymbolicLink(); } catch { return false; } })()) {
17
+ if (lstatSync(cursor).isSymbolicLink()) throw Error('Symlink paths are not supported for managed state or evidence.');
18
+ }
19
+ }
20
+ return full;
21
+ }
22
+
23
+ export function readJson(path, limit = 512 * 1024) {
24
+ const stat = lstatSync(path);
25
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > limit) throw Error('Expected a bounded regular JSON file.');
26
+ return JSON.parse(readFileSync(path, 'utf8'));
27
+ }
28
+
29
+ export function atomicJson(root, relativePath, value, expectedRevision) {
30
+ const path = within(root, relativePath);
31
+ within(root, dirname(path));
32
+ mkdirSync(dirname(path), { recursive: true });
33
+ const lock = `${path}.lock`;
34
+ let handle;
35
+ try { handle = openSync(lock, 'wx', 0o600); } catch { throw Error('State is being updated; retry after the current writer finishes.'); }
36
+ const temporary = `${path}.${randomUUID()}.tmp`;
37
+ try {
38
+ const previous = existsSync(path) ? readJson(path) : null;
39
+ if ((previous?.revision ?? 0) !== expectedRevision) throw Error('State revision changed. Read it again before updating.');
40
+ const record = { ...value, revision: expectedRevision + 1 };
41
+ const text = JSON.stringify(record, null, 2) + '\n';
42
+ if (Buffer.byteLength(text) > 512 * 1024) throw Error('State exceeds 512 KiB.');
43
+ writeFileSync(temporary, text, { flag: 'wx', mode: 0o600 });
44
+ renameSync(temporary, path);
45
+ return record;
46
+ } finally {
47
+ closeSync(handle);
48
+ if (existsSync(temporary)) unlinkSync(temporary);
49
+ unlinkSync(lock);
50
+ }
51
+ }
52
+
53
+ export function fingerprint(root) {
54
+ const base = projectRoot(root);
55
+ const head = gitRead(base, ['rev-parse', '--verify', 'HEAD']);
56
+ const branch = gitRead(base, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
57
+ const repo = gitRead(base, ['rev-parse', '--show-toplevel']);
58
+ const entries = []; let bytes = 0, visited = 0, partial = false;
59
+ function collect(directory) {
60
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
61
+ if (++visited > 10000) { partial = true; return; }
62
+ if (['.git', '.just-vibe', 'node_modules', '.venv', 'venv', 'dist', 'build', 'coverage', '.next', '.tmp', '.cache', 'PLAN.md', 'NAMING.md'].includes(entry.name) || privateName(entry.name)) continue;
63
+ const file = join(directory, entry.name);
64
+ if (entry.isSymbolicLink()) { entries.push([relative(base, file), 'symlink']); continue; }
65
+ if (entry.isDirectory()) collect(file);
66
+ else if (entry.isFile()) {
67
+ const stat = lstatSync(file);
68
+ if (stat.size > 8 * 1024 * 1024 || bytes + stat.size > 32 * 1024 * 1024) { partial = true; entries.push([relative(base, file), stat.mode, stat.size, stat.mtimeMs, 'metadata-only']); }
69
+ else { bytes += stat.size; entries.push([relative(base, file), stat.mode, digest(readFileSync(file))]); }
70
+ }
71
+ }
72
+ }
73
+ collect(base);
74
+ // Save hashes, never the patch or file contents. Index changes matter even when worktree bytes do not change.
75
+ const index = repo ? gitRead(base, ['diff', '--cached', '--no-ext-diff', '--no-textconv', '--binary', '--', '.', ':(exclude).just-vibe']) : '';
76
+ if (repo && index === null) partial = true;
77
+ return { root: base, repository: repo, head, branch, files: entries.length, content: digest(JSON.stringify(entries)), index: digest(index || ''), partial };
78
+ }
79
+
80
+ export function compareSnapshot(saved, current) {
81
+ const differences = ['root', 'repository', 'head', 'branch', 'content', 'index'].filter(key => saved?.[key] !== current[key]);
82
+ if (saved?.partial || current.partial) differences.push('incomplete-snapshot-coverage');
83
+ return { stale: differences.length > 0, differences, current };
84
+ }
@@ -0,0 +1,118 @@
1
+ import { randomUUID, randomInt } from 'node:crypto';
2
+
3
+ const dialogs = new Set(['codex-async', 'codex', 'claude']);
4
+ function nonempty(value, field) {
5
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${field} is required.`);
6
+ return value;
7
+ }
8
+ function checkQuiz(quiz) {
9
+ if (quiz?.schemaVersion !== 1 || quiz.kind !== 'teach-test' || !Array.isArray(quiz.answers)
10
+ || !['ready', 'awaiting-answer', 'awaiting-review', 'completed', 'cancelled'].includes(quiz.status)) throw new Error('Invalid quiz state.');
11
+ return quiz;
12
+ }
13
+ export function createQuiz({ topic, maxQuestions = 5, difficulty = 1, mode = 'practice' }) {
14
+ nonempty(topic, 'Topic or target workflow');
15
+ if (!Number.isInteger(maxQuestions) || maxQuestions < 1 || maxQuestions > 20) throw new Error('Choose 1–20 questions.');
16
+ if (!Number.isInteger(difficulty) || difficulty < 1 || difficulty > 3) throw new Error('Difficulty must be 1, 2 or 3.');
17
+ if (!['practice', 'test'].includes(mode)) throw new Error('Quiz mode must be practice or test.');
18
+ return { schemaVersion: 1, kind: 'teach-test', id: randomUUID(), topic, maxQuestions, difficulty, mode,
19
+ status: 'ready', answers: [], pending: null };
20
+ }
21
+
22
+ function validateQuestion(question) {
23
+ for (const field of ['prompt', 'concept', 'explanation']) nonempty(question[field], field);
24
+ if (!Array.isArray(question.options) || question.options.length !== 3) throw new Error('Use exactly three choices for cross-host compatibility.');
25
+ const ids = new Set(), labels = new Set();
26
+ for (const o of question.options) {
27
+ if (!/^[a-z0-9_-]+$/.test(o.id)) throw new Error('Choice IDs must be stable identifiers.');
28
+ nonempty(o.label, 'Choice label'); nonempty(o.description, 'Choice description');
29
+ if (o.label.trim().split(/\s+/).length > 5 || o.label.length > 80) throw new Error('Choice labels must be short (1–5 words).');
30
+ if (ids.has(o.id) || labels.has(o.label.toLowerCase())) throw new Error('Duplicate choice.');
31
+ if (/\b(?:recommended|correct answer)\b/i.test(o.label + ' ' + o.description)) throw new Error('Do not disclose an answer through option hints.');
32
+ ids.add(o.id); labels.add(o.label.toLowerCase());
33
+ }
34
+ if (!ids.has(question.correctOptionId)) throw new Error('Answer key must identify one of the choices.');
35
+ }
36
+
37
+ export function nativeQuestionPayload(question, dialog) {
38
+ if (!dialogs.has(dialog)) throw new Error('A supported native question dialog is required; do not fall back to inline quiz questions.');
39
+ const options = question.options.map(o => ({ label: o.label, description: o.description }));
40
+ if (dialog === 'codex-async') return { tool: 'request_user_input_async', arguments: {
41
+ questions: [{ title: question.prompt, options: options.map(o => `${o.label} — ${o.description}`) }],
42
+ } };
43
+ if (dialog === 'codex') return { tool: 'request_user_input', arguments: {
44
+ questions: [{ id: question.id, header: 'Knowledge', question: question.prompt, options }],
45
+ } };
46
+ return { tool: 'AskUserQuestion', arguments: {
47
+ questions: [{ header: 'Knowledge', question: question.prompt, options, multiSelect: false }],
48
+ } };
49
+ }
50
+
51
+ export function presentQuestion(quiz, question, dialog) {
52
+ checkQuiz(quiz);
53
+ if (quiz.status !== 'ready' || quiz.pending) throw new Error('Wait for and review the current answer before asking another question.');
54
+ if (quiz.answers.length >= quiz.maxQuestions) throw new Error('Question budget exhausted.');
55
+ validateQuestion(question);
56
+ const options = structuredClone(question.options);
57
+ for (let i = options.length - 1; i > 0; i--) { const j = randomInt(i + 1); [options[i], options[j]] = [options[j], options[i]]; }
58
+ const pending = { ...structuredClone(question), id: `q_${quiz.id.replaceAll('-', '_')}_${quiz.answers.length + 1}`, options, dialog };
59
+ const presentation = nativeQuestionPayload(pending, dialog);
60
+ return { quiz: { ...structuredClone(quiz), pending, status: 'awaiting-answer' }, presentation };
61
+ }
62
+
63
+ function assessed(quiz, response, result, explanation) {
64
+ const next = structuredClone(quiz);
65
+ const q = next.pending;
66
+ const answer = { questionId: q.id, prompt: q.prompt, concept: q.concept, response, result,
67
+ correctLabel: q.options.find(o => o.id === q.correctOptionId).label, explanation };
68
+ next.answers.push(answer);
69
+ next.pending = null;
70
+ if (next.mode === 'practice' && result !== 'skipped') next.difficulty = Math.max(1, Math.min(3, next.difficulty + (result === 'correct' ? 1 : -1)));
71
+ next.status = next.answers.length >= next.maxQuestions ? 'completed' : 'ready';
72
+ const feedback = next.mode === 'practice' ? answer : { questionId: answer.questionId, recorded: true };
73
+ return { quiz: next, feedback, ...(next.mode === 'practice' ? {
74
+ nextFocus: result === 'correct' ? 'Apply or extend the concept.' : `Revisit ${q.concept} with a simpler or differently framed example.`,
75
+ } : {}) };
76
+ }
77
+
78
+ export function answerQuestion(quiz, response) {
79
+ checkQuiz(quiz);
80
+ if (!quiz.pending || quiz.status !== 'awaiting-answer') throw new Error('No unanswered native question is pending.');
81
+ // Async tool calls returning immediately, empty submissions and timeouts are not answers.
82
+ if (!response || response.submitted !== true) return { quiz: structuredClone(quiz), feedback: null, waiting: true };
83
+ if (response.questionId !== quiz.pending.id) throw new Error('Answer belongs to a different question.');
84
+ if (response.cancelled) return { quiz: { ...structuredClone(quiz), status: 'cancelled' }, feedback: null };
85
+ if (response.skipped) return assessed(quiz, null, 'skipped', quiz.pending.explanation);
86
+ const selected = quiz.pending.options.find(o => o.id === response.selection || o.label === response.selection || `${o.label} — ${o.description}` === response.selection);
87
+ if (selected) return assessed(quiz, response.selection, selected.id === quiz.pending.correctOptionId ? 'correct' : 'incorrect', quiz.pending.explanation);
88
+ if (typeof response.freeText === 'string' && response.freeText.trim()) {
89
+ const next = structuredClone(quiz);
90
+ next.status = 'awaiting-review'; next.pending.freeText = response.freeText;
91
+ return { quiz: next, feedback: null, needsReview: true };
92
+ }
93
+ throw new Error('Unrecognized answer. Preserve the pending question; do not infer a selection.');
94
+ }
95
+
96
+ export function reviewFreeText(quiz, judgment) {
97
+ checkQuiz(quiz);
98
+ if (quiz.status !== 'awaiting-review' || !quiz.pending?.freeText) throw new Error('No free-text answer awaits review.');
99
+ if (!['correct', 'partial', 'incorrect'].includes(judgment.result)) throw new Error('Invalid review result.');
100
+ nonempty(judgment.explanation, 'Grounded explanation of the learner answer');
101
+ return assessed(quiz, quiz.pending.freeText, judgment.result, judgment.explanation);
102
+ }
103
+
104
+ export function quizReport(quiz) {
105
+ checkQuiz(quiz);
106
+ const correct = quiz.answers.filter(a => a.result === 'correct').length;
107
+ const incorrect = quiz.answers.filter(a => ['incorrect', 'partial'].includes(a.result));
108
+ const finished = ['completed', 'cancelled'].includes(quiz.status);
109
+ const report = { topic: quiz.topic, status: quiz.status, answered: quiz.answers.length,
110
+ graded: quiz.answers.filter(a => a.result !== 'skipped').length, skipped: quiz.answers.filter(a => a.result === 'skipped').length,
111
+ pending: Boolean(quiz.pending), nextDifficulty: quiz.difficulty,
112
+ note: 'Results describe this short sample, not proof of mastery. No learner profile is saved automatically.' };
113
+ if (quiz.mode === 'practice' || finished) {
114
+ report.correct = correct; report.revisit = [...new Set(incorrect.map(a => a.concept))];
115
+ report.answers = structuredClone(quiz.answers);
116
+ }
117
+ return report;
118
+ }