@tyroneross/build-loop 0.30.3

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 (389) hide show
  1. package/.agents/plugins/marketplace.json +10 -0
  2. package/.claude-plugin/marketplace.json +38 -0
  3. package/.claude-plugin/plugin.json +30 -0
  4. package/.codex-plugin/plugin.json +41 -0
  5. package/.cursor/rules/build-loop-surface.mdc +22 -0
  6. package/AGENTS.md +498 -0
  7. package/LICENSE +202 -0
  8. package/README.md +403 -0
  9. package/agents/alignment-checker.md +124 -0
  10. package/agents/api-assessor.md +136 -0
  11. package/agents/architecture-scout.md +185 -0
  12. package/agents/assessment-orchestrator.md +163 -0
  13. package/agents/build-orchestrator.md +198 -0
  14. package/agents/database-assessor.md +133 -0
  15. package/agents/design-contract-specialist.md +239 -0
  16. package/agents/fact-checker.md +69 -0
  17. package/agents/fix-critique.md +161 -0
  18. package/agents/frontend-assessor.md +136 -0
  19. package/agents/implementer.md +185 -0
  20. package/agents/independent-auditor.md +143 -0
  21. package/agents/mock-scanner.md +78 -0
  22. package/agents/optimize-runner.md +112 -0
  23. package/agents/overfitting-reviewer.md +96 -0
  24. package/agents/performance-assessor.md +159 -0
  25. package/agents/plan-critic.md +85 -0
  26. package/agents/promotion-reviewer.md +121 -0
  27. package/agents/recurring-pattern-detector.md +248 -0
  28. package/agents/retrospective-synthesizer.md +109 -0
  29. package/agents/root-cause-investigator.md +285 -0
  30. package/agents/scope-auditor.md +121 -0
  31. package/agents/security-reviewer.md +151 -0
  32. package/agents/self-improvement-architect.md +125 -0
  33. package/agents/synthesis-critic.md +99 -0
  34. package/agents/transcript-pattern-miner.md +165 -0
  35. package/agents/ui-validator.md +193 -0
  36. package/codex-skills/build-loop/SKILL.md +20 -0
  37. package/codex-skills/debug-loop/SKILL.md +18 -0
  38. package/codex-skills/handoff/SKILL.md +22 -0
  39. package/codex-skills/knowledge/SKILL.md +21 -0
  40. package/codex-skills/optimize/SKILL.md +18 -0
  41. package/codex-skills/research/SKILL.md +18 -0
  42. package/commands/agent-rally-point.md +301 -0
  43. package/commands/assess.md +41 -0
  44. package/commands/debug.md +20 -0
  45. package/commands/debugger-detail.md +34 -0
  46. package/commands/debugger-scan.md +15 -0
  47. package/commands/debugger-status.md +20 -0
  48. package/commands/debugger.md +32 -0
  49. package/commands/handoff.md +113 -0
  50. package/commands/knowledge-review.md +16 -0
  51. package/commands/optimize.md +31 -0
  52. package/commands/plan-verify.md +39 -0
  53. package/commands/promote-experiment.md +129 -0
  54. package/commands/research.md +22 -0
  55. package/commands/run.md +12 -0
  56. package/commands/self-review.md +51 -0
  57. package/commands/start-prd.md +57 -0
  58. package/commands/test.md +34 -0
  59. package/dist/src/assessment-orchestrator.d.ts +78 -0
  60. package/dist/src/assessment-orchestrator.d.ts.map +1 -0
  61. package/dist/src/assessment-orchestrator.js +304 -0
  62. package/dist/src/assessment-orchestrator.js.map +1 -0
  63. package/dist/src/audit-miner.d.ts +21 -0
  64. package/dist/src/audit-miner.d.ts.map +1 -0
  65. package/dist/src/audit-miner.js +421 -0
  66. package/dist/src/audit-miner.js.map +1 -0
  67. package/dist/src/batch-operations.d.ts +30 -0
  68. package/dist/src/batch-operations.d.ts.map +1 -0
  69. package/dist/src/batch-operations.js +339 -0
  70. package/dist/src/batch-operations.js.map +1 -0
  71. package/dist/src/config.d.ts +83 -0
  72. package/dist/src/config.d.ts.map +1 -0
  73. package/dist/src/config.js +176 -0
  74. package/dist/src/config.js.map +1 -0
  75. package/dist/src/context-engine.d.ts +45 -0
  76. package/dist/src/context-engine.d.ts.map +1 -0
  77. package/dist/src/context-engine.js +135 -0
  78. package/dist/src/context-engine.js.map +1 -0
  79. package/dist/src/debug-wrapper.d.ts +58 -0
  80. package/dist/src/debug-wrapper.d.ts.map +1 -0
  81. package/dist/src/debug-wrapper.js +285 -0
  82. package/dist/src/debug-wrapper.js.map +1 -0
  83. package/dist/src/index.d.ts +31 -0
  84. package/dist/src/index.d.ts.map +1 -0
  85. package/dist/src/index.js +149 -0
  86. package/dist/src/index.js.map +1 -0
  87. package/dist/src/interactive-verifier.d.ts +26 -0
  88. package/dist/src/interactive-verifier.d.ts.map +1 -0
  89. package/dist/src/interactive-verifier.js +435 -0
  90. package/dist/src/interactive-verifier.js.map +1 -0
  91. package/dist/src/lessons.d.ts +59 -0
  92. package/dist/src/lessons.d.ts.map +1 -0
  93. package/dist/src/lessons.js +172 -0
  94. package/dist/src/lessons.js.map +1 -0
  95. package/dist/src/log-reader.d.ts +39 -0
  96. package/dist/src/log-reader.d.ts.map +1 -0
  97. package/dist/src/log-reader.js +291 -0
  98. package/dist/src/log-reader.js.map +1 -0
  99. package/dist/src/logger.d.ts +59 -0
  100. package/dist/src/logger.d.ts.map +1 -0
  101. package/dist/src/logger.js +328 -0
  102. package/dist/src/logger.js.map +1 -0
  103. package/dist/src/parallel-retrieval.d.ts +52 -0
  104. package/dist/src/parallel-retrieval.d.ts.map +1 -0
  105. package/dist/src/parallel-retrieval.js +309 -0
  106. package/dist/src/parallel-retrieval.js.map +1 -0
  107. package/dist/src/pattern-extractor.d.ts +30 -0
  108. package/dist/src/pattern-extractor.d.ts.map +1 -0
  109. package/dist/src/pattern-extractor.js +386 -0
  110. package/dist/src/pattern-extractor.js.map +1 -0
  111. package/dist/src/result-aggregator.d.ts +44 -0
  112. package/dist/src/result-aggregator.d.ts.map +1 -0
  113. package/dist/src/result-aggregator.js +315 -0
  114. package/dist/src/result-aggregator.js.map +1 -0
  115. package/dist/src/retrieval.d.ts +139 -0
  116. package/dist/src/retrieval.d.ts.map +1 -0
  117. package/dist/src/retrieval.js +724 -0
  118. package/dist/src/retrieval.js.map +1 -0
  119. package/dist/src/setup/auto-setup.d.ts +15 -0
  120. package/dist/src/setup/auto-setup.d.ts.map +1 -0
  121. package/dist/src/setup/auto-setup.js +157 -0
  122. package/dist/src/setup/auto-setup.js.map +1 -0
  123. package/dist/src/setup/configure-hooks.d.ts +8 -0
  124. package/dist/src/setup/configure-hooks.d.ts.map +1 -0
  125. package/dist/src/setup/configure-hooks.js +16 -0
  126. package/dist/src/setup/configure-hooks.js.map +1 -0
  127. package/dist/src/setup/create-slash-commands.d.ts +8 -0
  128. package/dist/src/setup/create-slash-commands.d.ts.map +1 -0
  129. package/dist/src/setup/create-slash-commands.js +101 -0
  130. package/dist/src/setup/create-slash-commands.js.map +1 -0
  131. package/dist/src/setup/index.d.ts +8 -0
  132. package/dist/src/setup/index.d.ts.map +1 -0
  133. package/dist/src/setup/index.js +17 -0
  134. package/dist/src/setup/index.js.map +1 -0
  135. package/dist/src/setup/inject-claude-md.d.ts +10 -0
  136. package/dist/src/setup/inject-claude-md.d.ts.map +1 -0
  137. package/dist/src/setup/inject-claude-md.js +150 -0
  138. package/dist/src/setup/inject-claude-md.js.map +1 -0
  139. package/dist/src/setup/uninstall.d.ts +22 -0
  140. package/dist/src/setup/uninstall.d.ts.map +1 -0
  141. package/dist/src/setup/uninstall.js +235 -0
  142. package/dist/src/setup/uninstall.js.map +1 -0
  143. package/dist/src/storage.d.ts +192 -0
  144. package/dist/src/storage.d.ts.map +1 -0
  145. package/dist/src/storage.js +1037 -0
  146. package/dist/src/storage.js.map +1 -0
  147. package/dist/src/string-similarity.d.ts +8 -0
  148. package/dist/src/string-similarity.d.ts.map +1 -0
  149. package/dist/src/string-similarity.js +57 -0
  150. package/dist/src/string-similarity.js.map +1 -0
  151. package/dist/src/traces/adapters/browser.d.ts +69 -0
  152. package/dist/src/traces/adapters/browser.d.ts.map +1 -0
  153. package/dist/src/traces/adapters/browser.js +280 -0
  154. package/dist/src/traces/adapters/browser.js.map +1 -0
  155. package/dist/src/traces/adapters/index.d.ts +23 -0
  156. package/dist/src/traces/adapters/index.d.ts.map +1 -0
  157. package/dist/src/traces/adapters/index.js +67 -0
  158. package/dist/src/traces/adapters/index.js.map +1 -0
  159. package/dist/src/traces/adapters/langchain.d.ts +42 -0
  160. package/dist/src/traces/adapters/langchain.d.ts.map +1 -0
  161. package/dist/src/traces/adapters/langchain.js +151 -0
  162. package/dist/src/traces/adapters/langchain.js.map +1 -0
  163. package/dist/src/traces/adapters/opentelemetry.d.ts +46 -0
  164. package/dist/src/traces/adapters/opentelemetry.d.ts.map +1 -0
  165. package/dist/src/traces/adapters/opentelemetry.js +164 -0
  166. package/dist/src/traces/adapters/opentelemetry.js.map +1 -0
  167. package/dist/src/traces/adapters/sentry.d.ts +58 -0
  168. package/dist/src/traces/adapters/sentry.d.ts.map +1 -0
  169. package/dist/src/traces/adapters/sentry.js +212 -0
  170. package/dist/src/traces/adapters/sentry.js.map +1 -0
  171. package/dist/src/traces/index.d.ts +10 -0
  172. package/dist/src/traces/index.d.ts.map +1 -0
  173. package/dist/src/traces/index.js +45 -0
  174. package/dist/src/traces/index.js.map +1 -0
  175. package/dist/src/traces/storage.d.ts +78 -0
  176. package/dist/src/traces/storage.d.ts.map +1 -0
  177. package/dist/src/traces/storage.js +399 -0
  178. package/dist/src/traces/storage.js.map +1 -0
  179. package/dist/src/traces/summarizer.d.ts +83 -0
  180. package/dist/src/traces/summarizer.d.ts.map +1 -0
  181. package/dist/src/traces/summarizer.js +247 -0
  182. package/dist/src/traces/summarizer.js.map +1 -0
  183. package/dist/src/traces/types.d.ts +365 -0
  184. package/dist/src/traces/types.d.ts.map +1 -0
  185. package/dist/src/traces/types.js +14 -0
  186. package/dist/src/traces/types.js.map +1 -0
  187. package/dist/src/types.d.ts +495 -0
  188. package/dist/src/types.d.ts.map +1 -0
  189. package/dist/src/types.js +27 -0
  190. package/dist/src/types.js.map +1 -0
  191. package/docs/agent-surface-policy.md +45 -0
  192. package/docs/memory-setup.md +226 -0
  193. package/hooks/_arch_scan_bg.py +88 -0
  194. package/hooks/_plugin_drift_check_bg.py +85 -0
  195. package/hooks/_session_start_lib.sh +42 -0
  196. package/hooks/git/pre-push +141 -0
  197. package/hooks/hooks.json +126 -0
  198. package/hooks/post-commit +30 -0
  199. package/hooks/pre-commit +29 -0
  200. package/hooks/pre-edit-apppulse.sh +6 -0
  201. package/hooks/pre-edit-architecture.sh +106 -0
  202. package/hooks/pre-edit-rally-point.sh +92 -0
  203. package/hooks/session-start-apppulse.sh +6 -0
  204. package/hooks/session-start-architecture.sh +10 -0
  205. package/hooks/session-start-git-hooks.sh +12 -0
  206. package/hooks/session-start-plugin-drift.sh +27 -0
  207. package/hooks/session-start-rally-point.sh +44 -0
  208. package/hooks/session-start-retrieval.sh +24 -0
  209. package/hooks/session-start-worktree-gc.sh +105 -0
  210. package/hooks/test_apppulse_hooks.sh +6 -0
  211. package/hooks/test_rally_point_hooks.sh +388 -0
  212. package/hooks/test_sec008_hooks.sh +66 -0
  213. package/package.json +111 -0
  214. package/scripts/README.md +301 -0
  215. package/scripts/_db_url.py +61 -0
  216. package/scripts/_paths.py +445 -0
  217. package/scripts/blm.py +162 -0
  218. package/scripts/blm_api.py +263 -0
  219. package/scripts/check_cache_sync.py +490 -0
  220. package/scripts/embed_backend.py +475 -0
  221. package/scripts/install_memory.py +556 -0
  222. package/scripts/lessons_index/__init__.py +77 -0
  223. package/scripts/lessons_index/__main__.py +135 -0
  224. package/scripts/lessons_index/ingest.py +288 -0
  225. package/scripts/lessons_index/query.py +285 -0
  226. package/scripts/lessons_index/schema.py +114 -0
  227. package/scripts/memory_context/__init__.py +610 -0
  228. package/scripts/memory_graph/__init__.py +393 -0
  229. package/scripts/project_resolver.py +172 -0
  230. package/scripts/prune_codex_plugin_cache.py +23 -0
  231. package/scripts/prune_plugin_cache.py +359 -0
  232. package/scripts/semantic_index/__init__.py +482 -0
  233. package/scripts/semantic_index/_bench_hybrid.py +175 -0
  234. package/scripts/semantic_index/backfill.py +263 -0
  235. package/scripts/semantic_index/hybrid.py +174 -0
  236. package/scripts/semantic_index/test_backfill.py +292 -0
  237. package/scripts/semantic_index/test_hybrid.py +541 -0
  238. package/scripts/sync_navgator_lessons.py +666 -0
  239. package/scripts/sync_plugin_cache.py +388 -0
  240. package/skills/agent-rally-point/SKILL.md +165 -0
  241. package/skills/agent-rally-watcher/SKILL.md +83 -0
  242. package/skills/api-registry-bridge/SKILL.md +59 -0
  243. package/skills/architecture/dead/SKILL.md +56 -0
  244. package/skills/architecture/impact/SKILL.md +88 -0
  245. package/skills/architecture/review/SKILL.md +227 -0
  246. package/skills/architecture/rules/SKILL.md +72 -0
  247. package/skills/architecture/scan/SKILL.md +79 -0
  248. package/skills/architecture/trace/SKILL.md +65 -0
  249. package/skills/attribution-standard/SKILL.md +122 -0
  250. package/skills/authentication/SKILL.md +81 -0
  251. package/skills/authentication/references/better-auth-magic-link.md +135 -0
  252. package/skills/authentication/references/better-auth-setup.md +224 -0
  253. package/skills/authentication/references/google-calendar-sync.md +278 -0
  254. package/skills/authentication/references/google-cloud-console.md +159 -0
  255. package/skills/authentication/references/google-geocoding-directions.md +126 -0
  256. package/skills/authentication/references/google-lessons-example-web-app.md +177 -0
  257. package/skills/authentication/references/google-maps.md +162 -0
  258. package/skills/authentication/references/google-oauth-setup.md +343 -0
  259. package/skills/authentication/references/google-places.md +173 -0
  260. package/skills/authentication/references/lessons-example-web-app-better-auth.md +126 -0
  261. package/skills/authentication/references/resend-email.md +227 -0
  262. package/skills/authentication/references/resend-otp-magic-link.md +153 -0
  263. package/skills/authentication/references/supabase-auth.md +172 -0
  264. package/skills/auto-decision-capture/SKILL.md +419 -0
  265. package/skills/build-loop/SKILL.md +516 -0
  266. package/skills/build-loop/detect-plugins.mjs +82 -0
  267. package/skills/build-loop/eval-guide.md +65 -0
  268. package/skills/build-loop/fallbacks.md +549 -0
  269. package/skills/build-loop/phases/fact-check.md +42 -0
  270. package/skills/build-loop/phases/ui-validation.md +267 -0
  271. package/skills/build-loop/references/apple-native-planning.md +439 -0
  272. package/skills/build-loop/references/capability-routing.md +181 -0
  273. package/skills/build-loop/references/codex-subagents.md +98 -0
  274. package/skills/build-loop/references/coordination.md +161 -0
  275. package/skills/build-loop/references/correction-aware-capture.md +177 -0
  276. package/skills/build-loop/references/independent-auditor.md +72 -0
  277. package/skills/build-loop/references/intent-capability-pack.md +170 -0
  278. package/skills/build-loop/references/intent-exploration-prompts.md +96 -0
  279. package/skills/build-loop/references/leadership.md +72 -0
  280. package/skills/build-loop/references/memory.md +284 -0
  281. package/skills/build-loop/references/modular-systems-pack.md +96 -0
  282. package/skills/build-loop/references/output-style.md +129 -0
  283. package/skills/build-loop/references/pay-it-forward-arch.md +98 -0
  284. package/skills/build-loop/references/phase-1-assess.md +218 -0
  285. package/skills/build-loop/references/phase-2-plan.md +77 -0
  286. package/skills/build-loop/references/phase-3-execute.md +47 -0
  287. package/skills/build-loop/references/phase-4-review.md +310 -0
  288. package/skills/build-loop/references/phase-5-iterate.md +71 -0
  289. package/skills/build-loop/references/phase-6-learn.md +56 -0
  290. package/skills/build-loop/references/recent-design-structures.md +274 -0
  291. package/skills/build-loop/references/refactor-history/ASSESSMENT.md +85 -0
  292. package/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +149 -0
  293. package/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +32 -0
  294. package/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +48 -0
  295. package/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +60 -0
  296. package/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +51 -0
  297. package/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +71 -0
  298. package/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +52 -0
  299. package/skills/build-loop/references/refactor-history/trace-comparison.md +202 -0
  300. package/skills/build-loop/references/self-recursive-dev.md +77 -0
  301. package/skills/build-loop/references/self-review.md +232 -0
  302. package/skills/build-loop/references/ui-io-contract.md +116 -0
  303. package/skills/build-loop/scanners/audit-design-rules.mjs +476 -0
  304. package/skills/build-loop/scanners/require-visual-evidence.mjs +239 -0
  305. package/skills/build-loop/templates/backlog-item.md +35 -0
  306. package/skills/build-loop/templates/codex-worker-prompt.md +89 -0
  307. package/skills/build-loop/templates/ui-subagent-prompt.md +179 -0
  308. package/skills/build-loop/templates/ux-fix-plan.md +40 -0
  309. package/skills/building-with-deepagents/SKILL.md +195 -0
  310. package/skills/building-with-deepagents/references/anti-patterns.md +141 -0
  311. package/skills/building-with-deepagents/references/api-quick-reference.md +184 -0
  312. package/skills/capabilities/SKILL.md +91 -0
  313. package/skills/debug-loop/SKILL.md +278 -0
  314. package/skills/debug-loop/references/convergence-rules.md +145 -0
  315. package/skills/debug-loop/references/swiftui-macos-clunky-ui-debugger.html +823 -0
  316. package/skills/debugging/assess/SKILL.md +111 -0
  317. package/skills/debugging/debug-loop/SKILL.md +231 -0
  318. package/skills/debugging/memory/SKILL.md +147 -0
  319. package/skills/debugging/store/SKILL.md +153 -0
  320. package/skills/debugging-memory/SKILL.md +440 -0
  321. package/skills/debugging-memory/examples/incident-example.json +67 -0
  322. package/skills/debugging-memory/references/incident-documentation.md +280 -0
  323. package/skills/debugging-memory/references/ios-notification-alarm-playbook.md +300 -0
  324. package/skills/debugging-memory/references/pattern-extraction.md +163 -0
  325. package/skills/debugging-memory/references/subagent-integration.md +312 -0
  326. package/skills/defenseclaw-bridge/SKILL.md +156 -0
  327. package/skills/defenseclaw-bridge/references/dc-config-mapping.md +162 -0
  328. package/skills/defenseclaw-bridge/references/output-format.md +108 -0
  329. package/skills/handoff/SKILL.md +94 -0
  330. package/skills/ibr-bridge/SKILL.md +68 -0
  331. package/skills/knowledge/SKILL.md +127 -0
  332. package/skills/knowledge/references/recall-integration.md +83 -0
  333. package/skills/knowledge/templates/madr-minimal.md +56 -0
  334. package/skills/knowledge-review/SKILL.md +112 -0
  335. package/skills/logging-tracer/SKILL.md +202 -0
  336. package/skills/logging-tracer/references/log-analysis.md +128 -0
  337. package/skills/logging-tracer/references/stack-templates.md +274 -0
  338. package/skills/logging-tracer-bridge/SKILL.md +70 -0
  339. package/skills/mcp-builder/SKILL.md +278 -0
  340. package/skills/mcp-builder/references/mcp-security.md +106 -0
  341. package/skills/model-tiering/SKILL.md +216 -0
  342. package/skills/native-ax-driver/SKILL.md +220 -0
  343. package/skills/native-ax-driver/scripts/layout_fill.py +222 -0
  344. package/skills/native-ax-driver/scripts/native_driver.py +495 -0
  345. package/skills/native-ax-driver/swift/bl-ax-driver/Package.swift +20 -0
  346. package/skills/native-ax-driver/swift/bl-ax-driver/Sources/main.swift +542 -0
  347. package/skills/optimize/SKILL.md +202 -0
  348. package/skills/optimize/profiles.md +61 -0
  349. package/skills/plan-verify/SKILL.md +115 -0
  350. package/skills/plan-verify/test-fixtures/example-app-v20-findings.json +27 -0
  351. package/skills/plan-verify/test-fixtures/example-app-v20.md +42 -0
  352. package/skills/plan-verify/test-fixtures/example-app-v22-findings.json +15 -0
  353. package/skills/plan-verify/test-fixtures/example-app-v22.md +1608 -0
  354. package/skills/plan-verify/test-fixtures/unrelated-good-plan-findings.json +15 -0
  355. package/skills/plan-verify/test-fixtures/unrelated-good-plan.md +55 -0
  356. package/skills/plugin-builder/SKILL.md +329 -0
  357. package/skills/plugin-builder/references/authoritative-sources.md +104 -0
  358. package/skills/plugin-builder/references/build-loop-phase-guidance.md +94 -0
  359. package/skills/plugin-builder/references/components-guide.md +221 -0
  360. package/skills/plugin-builder/references/distribution.md +317 -0
  361. package/skills/plugin-builder/references/dual-host-claude-codex.md +220 -0
  362. package/skills/plugin-builder/references/hooks-reference.md +210 -0
  363. package/skills/plugin-builder/references/manifest-schema.md +111 -0
  364. package/skills/plugin-builder/references/plugin-hygiene-lessons.md +307 -0
  365. package/skills/plugin-tests/SKILL.md +94 -0
  366. package/skills/prd-bridge/SKILL.md +135 -0
  367. package/skills/research/SKILL.md +80 -0
  368. package/skills/security-methodology/SKILL.md +105 -0
  369. package/skills/security-methodology/references/cross-source-matrix.md +77 -0
  370. package/skills/security-methodology/references/mitre-atlas-starter.md +83 -0
  371. package/skills/security-methodology/references/nist-600-1-mapping.md +119 -0
  372. package/skills/security-methodology/references/owasp-agentic-top-10.md +157 -0
  373. package/skills/security-methodology/references/owasp-llm-top-10.md +144 -0
  374. package/skills/security-methodology/references/owasp-web-top-10.md +65 -0
  375. package/skills/self-improve/SKILL.md +298 -0
  376. package/skills/spec-writing/SKILL.md +548 -0
  377. package/skills/spec-writing/scripts/check_checklist.py +478 -0
  378. package/skills/sync-skills/SKILL.md +132 -0
  379. package/skills/telemetry/SKILL.md +66 -0
  380. package/skills/ui-design/SKILL.md +75 -0
  381. package/skills/ui-design/references/design-patterns-multi.md +165 -0
  382. package/skills/ui-design/references/design-preferences-from-owned-apps.md +150 -0
  383. package/skills/ui-design/references/evidence-capture-policy.md +55 -0
  384. package/skills/ui-design/references/ui-guidance-sources.md +330 -0
  385. package/skills/ui-design/references/universal-design-principles.alt.md +182 -0
  386. package/skills/ui-design/references/universal-design-principles.md +419 -0
  387. package/templates/memory/MEMORY.md.template +55 -0
  388. package/templates/memory/constitution.md.template +64 -0
  389. package/templates/memory/manifest.json +47 -0
@@ -0,0 +1,610 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com>
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """Fast, host-neutral context surface for build-loop-memory.
5
+
6
+ This is the core API behind the `blm` CLI. It deliberately stays file-first:
7
+ no MCP server, no HTTP daemon, and no Postgres requirement. The generated
8
+ `CURRENT.json`/`CURRENT.md` files are the L0 hot capsule; expand mode can add
9
+ the existing SQLite lessons index without changing the capsule contract.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import re
16
+ import subprocess
17
+ import sys
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ HERE = Path(__file__).resolve().parent
23
+ SCRIPTS_DIR = HERE.parent
24
+ if str(SCRIPTS_DIR) not in sys.path:
25
+ sys.path.insert(0, str(SCRIPTS_DIR))
26
+
27
+ from _paths import ( # type: ignore # noqa: E402
28
+ memory_indexes_dir,
29
+ memory_store_root,
30
+ project_decisions_dir,
31
+ project_lessons_dir,
32
+ project_root,
33
+ top_level_lessons_dir,
34
+ )
35
+ from project_resolver import resolve_project # type: ignore # noqa: E402
36
+
37
+ SCHEMA_VERSION = "1.0.0"
38
+ KIND_CURRENT = "build-loop-memory-current"
39
+ KIND_CONTEXT = "build-loop-memory-context"
40
+ KIND_STATUS = "build-loop-memory-status"
41
+ VALID_MODES = {"fast", "expand"}
42
+ DEFAULT_LIMIT = 5
43
+ DEFAULT_MAX_CHARS = 900
44
+
45
+
46
+ def utc_now() -> str:
47
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
48
+
49
+
50
+ def _read_text(path: Path, max_chars: int | None = None) -> tuple[str | None, str | None]:
51
+ try:
52
+ text = path.read_text(encoding="utf-8")
53
+ except FileNotFoundError:
54
+ return None, f"missing: {path}"
55
+ except OSError as exc:
56
+ return None, f"read_error: {path}: {exc}"
57
+ if max_chars is not None and len(text) > max_chars:
58
+ return text[:max_chars], f"truncated: {path}: first {max_chars} chars"
59
+ return text, None
60
+
61
+
62
+ def _atomic_write_json(path: Path, data: dict[str, Any]) -> None:
63
+ path.parent.mkdir(parents=True, exist_ok=True)
64
+ tmp = path.with_name(f".{path.name}.tmp")
65
+ tmp.write_text(json.dumps(data, indent=2, sort_keys=True, default=str) + "\n", encoding="utf-8")
66
+ os.replace(tmp, path)
67
+
68
+
69
+ def _atomic_write_text(path: Path, text: str) -> None:
70
+ path.parent.mkdir(parents=True, exist_ok=True)
71
+ tmp = path.with_name(f".{path.name}.tmp")
72
+ tmp.write_text(text, encoding="utf-8")
73
+ os.replace(tmp, path)
74
+
75
+
76
+ def _git_info(path: Path) -> dict[str, Any]:
77
+ def run(args: list[str]) -> tuple[bool, str]:
78
+ try:
79
+ proc = subprocess.run(
80
+ ["git", *args],
81
+ cwd=str(path),
82
+ check=True,
83
+ capture_output=True,
84
+ text=True,
85
+ )
86
+ except (OSError, subprocess.CalledProcessError) as exc:
87
+ return False, str(exc)
88
+ return True, proc.stdout.strip()
89
+
90
+ ok_branch, branch = run(["rev-parse", "--abbrev-ref", "HEAD"])
91
+ ok_commit, commit = run(["rev-parse", "--short", "HEAD"])
92
+ ok_dirty, status = run(["status", "--porcelain"])
93
+ if not (ok_branch and ok_commit):
94
+ return {
95
+ "ok": False,
96
+ "branch": "",
97
+ "commit": "",
98
+ "dirty_count": None,
99
+ "error": branch if not ok_branch else commit,
100
+ }
101
+ return {
102
+ "ok": True,
103
+ "branch": branch,
104
+ "commit": commit,
105
+ "dirty_count": len([line for line in status.splitlines() if line.strip()]) if ok_dirty else None,
106
+ }
107
+
108
+
109
+ def _parse_frontmatter(text: str) -> tuple[dict[str, Any], str]:
110
+ if not text.startswith("---\n"):
111
+ return {}, text
112
+ end = text.find("\n---\n", 4)
113
+ if end < 0:
114
+ return {}, text
115
+ block = text[4:end]
116
+ body = text[end + 5 :]
117
+ out: dict[str, Any] = {}
118
+ for line in block.splitlines():
119
+ if ":" not in line or line.lstrip().startswith("#"):
120
+ continue
121
+ key, value = line.split(":", 1)
122
+ out[key.strip()] = value.strip().strip('"').strip("'")
123
+ return out, body
124
+
125
+
126
+ def _first_heading_or_stem(path: Path, text: str, fm: dict[str, Any]) -> str:
127
+ for key in ("title", "name", "id", "canonical_id"):
128
+ if fm.get(key):
129
+ return str(fm[key])
130
+ for line in text.splitlines():
131
+ stripped = line.strip()
132
+ if stripped.startswith("#"):
133
+ return stripped.lstrip("#").strip() or path.stem
134
+ return path.stem
135
+
136
+
137
+ def _compact_body(body: str, max_chars: int = DEFAULT_MAX_CHARS) -> str:
138
+ cleaned = re.sub(r"\n{3,}", "\n\n", body.strip())
139
+ if len(cleaned) <= max_chars:
140
+ return cleaned
141
+ return cleaned[: max_chars - 3].rstrip() + "..."
142
+
143
+
144
+ def _recent_markdown(
145
+ directory: Path,
146
+ *,
147
+ prefix: str,
148
+ limit: int,
149
+ max_chars: int,
150
+ ) -> list[dict[str, Any]]:
151
+ if not directory.is_dir():
152
+ return []
153
+ candidates = [p for p in directory.glob("*.md") if p.name not in {"INDEX.md", "README.md", "MEMORY.md"}]
154
+ candidates.sort(key=lambda p: p.stat().st_mtime if p.exists() else 0, reverse=True)
155
+ rows: list[dict[str, Any]] = []
156
+ for path in candidates[:limit]:
157
+ text, reason = _read_text(path, max_chars=max_chars * 2)
158
+ if text is None:
159
+ rows.append({"id": f"{prefix}:{path.stem}", "path": str(path), "exists": False, "reason": reason})
160
+ continue
161
+ fm, body = _parse_frontmatter(text)
162
+ rows.append(
163
+ {
164
+ "id": str(fm.get("canonical_id") or fm.get("id") or f"{prefix}:{path.stem}"),
165
+ "title": _first_heading_or_stem(path, text, fm),
166
+ "path": str(path),
167
+ "summary": _compact_body(body, max_chars=max_chars),
168
+ }
169
+ )
170
+ return rows
171
+
172
+
173
+ def _context_summary(project_dir: Path, max_chars: int) -> dict[str, Any]:
174
+ path = project_dir / "context" / "CONTEXT.md"
175
+ text, reason = _read_text(path, max_chars=max_chars * 4)
176
+ if text is None:
177
+ return {"path": str(path), "exists": False, "summary": "", "reason": reason}
178
+ _fm, body = _parse_frontmatter(text)
179
+ summary = ""
180
+ marker = "## Governing Summary"
181
+ if marker in body:
182
+ after = body.split(marker, 1)[1]
183
+ next_section = re.split(r"\n##\s+", after, maxsplit=1)[0]
184
+ summary = next_section.strip()
185
+ if not summary:
186
+ summary = body.strip()
187
+ return {
188
+ "id": "context:CONTEXT",
189
+ "path": str(path),
190
+ "exists": True,
191
+ "summary": _compact_body(summary, max_chars=max_chars),
192
+ }
193
+
194
+
195
+ def _count_jsonl(path: Path) -> int | None:
196
+ if not path.is_file():
197
+ return None
198
+ try:
199
+ return sum(1 for line in path.read_text(encoding="utf-8").splitlines() if line.strip())
200
+ except OSError:
201
+ return None
202
+
203
+
204
+ def _index_state() -> dict[str, Any]:
205
+ idx = memory_indexes_dir()
206
+ index_path = idx / "INDEX.jsonl"
207
+ graph_nodes = idx / "graph-nodes.jsonl"
208
+ graph_edges = idx / "graph-edges.jsonl"
209
+ try:
210
+ index_mtime = datetime.fromtimestamp(index_path.stat().st_mtime, timezone.utc).isoformat() if index_path.exists() else None
211
+ except OSError:
212
+ index_mtime = None
213
+ return {
214
+ "index_path": str(index_path),
215
+ "index_rows": _count_jsonl(index_path),
216
+ "graph_nodes": _count_jsonl(graph_nodes),
217
+ "graph_edges": _count_jsonl(graph_edges),
218
+ "index_updated_at": index_mtime,
219
+ }
220
+
221
+
222
+ def _freshness(workdir: Path, project: str, project_dir: Path, warnings: list[str]) -> dict[str, Any]:
223
+ source_git = _git_info(workdir)
224
+ memory_root = memory_store_root().resolve()
225
+ memory_git = _git_info(memory_root)
226
+ validity = "clean"
227
+ if warnings:
228
+ validity = "unknown"
229
+ for info in (source_git, memory_git):
230
+ if not info.get("ok"):
231
+ validity = "unknown"
232
+ elif info.get("dirty_count"):
233
+ validity = "stale"
234
+ if not project_dir.exists():
235
+ validity = "unknown"
236
+ return {
237
+ "validity": validity,
238
+ "generated_at": utc_now(),
239
+ "source_workdir": str(workdir),
240
+ "source_branch": source_git.get("branch", ""),
241
+ "source_commit": source_git.get("commit", ""),
242
+ "source_dirty_count": source_git.get("dirty_count"),
243
+ "memory_root": str(memory_root),
244
+ "memory_project": project,
245
+ "memory_project_dir": str(project_dir),
246
+ "memory_branch": memory_git.get("branch", ""),
247
+ "memory_commit": memory_git.get("commit", ""),
248
+ "memory_dirty_count": memory_git.get("dirty_count"),
249
+ "index": _index_state(),
250
+ }
251
+
252
+
253
+ def _evidence_from_current(current: dict[str, Any]) -> list[dict[str, str]]:
254
+ evidence: list[dict[str, str]] = []
255
+ ctx = current.get("context") or {}
256
+ if ctx.get("exists") and ctx.get("path"):
257
+ evidence.append({"id": ctx.get("id", "context:CONTEXT"), "path": ctx["path"], "type": "context"})
258
+ for lane in ("decisions", "lessons"):
259
+ for item in current.get(lane, []):
260
+ if item.get("id") and item.get("path"):
261
+ evidence.append({"id": str(item["id"]), "path": str(item["path"]), "type": lane[:-1]})
262
+ return evidence
263
+
264
+
265
+ def build_current(
266
+ workdir: Path | str,
267
+ query: str = "",
268
+ *,
269
+ project: str | None = None,
270
+ limit: int = DEFAULT_LIMIT,
271
+ max_chars: int = DEFAULT_MAX_CHARS,
272
+ ) -> dict[str, Any]:
273
+ """Build the L0 hot capsule without writing it."""
274
+ workdir = Path(workdir).resolve()
275
+ project_tag = project or resolve_project(workdir)
276
+ proj_dir = project_root(project_tag)
277
+ warnings: list[str] = []
278
+ if not proj_dir.exists():
279
+ warnings.append(f"project_memory_missing: {proj_dir}")
280
+
281
+ context = _context_summary(proj_dir, max_chars=max_chars)
282
+ if not context.get("exists"):
283
+ warnings.append(str(context.get("reason") or "context_missing"))
284
+
285
+ project_decisions = _recent_markdown(
286
+ project_decisions_dir(project_tag),
287
+ prefix="decision",
288
+ limit=limit,
289
+ max_chars=max_chars,
290
+ )
291
+ project_lessons = _recent_markdown(
292
+ project_lessons_dir(project_tag),
293
+ prefix="lesson",
294
+ limit=limit,
295
+ max_chars=max_chars,
296
+ )
297
+ global_lessons = _recent_markdown(
298
+ top_level_lessons_dir(),
299
+ prefix="lesson",
300
+ limit=max(0, limit - len(project_lessons)),
301
+ max_chars=max_chars,
302
+ )
303
+ lessons = project_lessons + global_lessons
304
+
305
+ current: dict[str, Any] = {
306
+ "schema_version": SCHEMA_VERSION,
307
+ "kind": KIND_CURRENT,
308
+ "project": project_tag,
309
+ "workdir": str(workdir),
310
+ "query": query,
311
+ "generated_at": utc_now(),
312
+ "context": context,
313
+ "decisions": project_decisions,
314
+ "lessons": lessons[:limit],
315
+ "next_commands": [
316
+ 'blm context --workdir "$PWD" --mode fast --json',
317
+ 'blm context --workdir "$PWD" --mode expand --json',
318
+ "blm open --id <evidence-id>",
319
+ ],
320
+ "warnings": warnings,
321
+ }
322
+ current["freshness"] = _freshness(workdir, project_tag, proj_dir, warnings)
323
+ current["evidence"] = _evidence_from_current(current)
324
+ validate_current(current)
325
+ return current
326
+
327
+
328
+ def current_paths(project: str) -> dict[str, Path]:
329
+ context_dir = project_root(project) / "context"
330
+ return {
331
+ "json": context_dir / "CURRENT.json",
332
+ "markdown": context_dir / "CURRENT.md",
333
+ "freshness": context_dir / "freshness.json",
334
+ }
335
+
336
+
337
+ def render_current_markdown(current: dict[str, Any]) -> str:
338
+ freshness = current.get("freshness", {})
339
+ lines = [
340
+ f"# Current Context: {current.get('project', '_unscoped')}",
341
+ "",
342
+ f"- Generated: {current.get('generated_at', '')}",
343
+ f"- Validity: {freshness.get('validity', 'unknown')}",
344
+ f"- Source: {freshness.get('source_branch', '')}@{freshness.get('source_commit', '')}",
345
+ f"- Memory: {freshness.get('memory_branch', '')}@{freshness.get('memory_commit', '')}",
346
+ "",
347
+ "## Immediate Context",
348
+ "",
349
+ str((current.get("context") or {}).get("summary") or "(none)"),
350
+ "",
351
+ ]
352
+ if current.get("decisions"):
353
+ lines.extend(["## Recent Decisions", ""])
354
+ for item in current["decisions"]:
355
+ lines.append(f"- {item.get('title') or item.get('id')} ({item.get('id')})")
356
+ lines.append("")
357
+ if current.get("lessons"):
358
+ lines.extend(["## Relevant Lessons", ""])
359
+ for item in current["lessons"]:
360
+ lines.append(f"- {item.get('title') or item.get('id')} ({item.get('id')})")
361
+ lines.append("")
362
+ if current.get("warnings"):
363
+ lines.extend(["## Warnings", ""])
364
+ for warning in current["warnings"]:
365
+ lines.append(f"- {warning}")
366
+ lines.append("")
367
+ return "\n".join(lines).rstrip() + "\n"
368
+
369
+
370
+ def write_current(current: dict[str, Any]) -> dict[str, str]:
371
+ validate_current(current)
372
+ paths = current_paths(str(current["project"]))
373
+ _atomic_write_json(paths["json"], current)
374
+ _atomic_write_text(paths["markdown"], render_current_markdown(current))
375
+ _atomic_write_json(paths["freshness"], current["freshness"])
376
+ return {key: str(path) for key, path in paths.items()}
377
+
378
+
379
+ def describe_access(
380
+ workdir: Path | str,
381
+ *,
382
+ project: str | None = None,
383
+ host: str = "127.0.0.1",
384
+ port: int = 8777,
385
+ ) -> dict[str, Any]:
386
+ """Return the fast-access map agents can use before deeper retrieval."""
387
+ workdir = Path(workdir).resolve()
388
+ project_tag = project or resolve_project(workdir)
389
+ paths = current_paths(project_tag)
390
+ return {
391
+ "schema_version": SCHEMA_VERSION,
392
+ "kind": KIND_STATUS,
393
+ "project": project_tag,
394
+ "workdir": str(workdir),
395
+ "memory_root": str(memory_store_root().resolve()),
396
+ "project_dir": str(project_root(project_tag)),
397
+ "current_paths": {key: str(path) for key, path in paths.items()},
398
+ "current_exists": {key: path.exists() for key, path in paths.items()},
399
+ "cli": {
400
+ "fast": 'python3 scripts/blm.py context --workdir "$PWD" --mode fast --json',
401
+ "expand": 'python3 scripts/blm.py context --workdir "$PWD" --mode expand --json',
402
+ "open": 'python3 scripts/blm.py open --id <evidence-id> --workdir "$PWD" --json',
403
+ "status": 'python3 scripts/blm.py status --workdir "$PWD" --json',
404
+ },
405
+ "api": {
406
+ "default_host": host,
407
+ "default_port": port,
408
+ "base_url": f"http://{host}:{port}",
409
+ "serve": f"python3 scripts/blm.py serve --host {host} --port {port}",
410
+ "endpoints": [
411
+ "GET /health",
412
+ "GET /context?workdir=<path>&query=<goal>&mode=fast",
413
+ "POST /context",
414
+ "GET /open?id=<evidence-id>&workdir=<path>",
415
+ "POST /open",
416
+ ],
417
+ "write_default": False,
418
+ },
419
+ }
420
+
421
+
422
+ def validate_current(current: dict[str, Any]) -> None:
423
+ required = {"schema_version", "kind", "project", "workdir", "generated_at", "context", "freshness", "evidence", "warnings"}
424
+ missing = required - set(current)
425
+ if missing:
426
+ raise ValueError(f"CURRENT missing required fields: {sorted(missing)}")
427
+ if current["schema_version"] != SCHEMA_VERSION:
428
+ raise ValueError(f"unsupported CURRENT schema_version: {current['schema_version']!r}")
429
+ if current["kind"] != KIND_CURRENT:
430
+ raise ValueError(f"unsupported CURRENT kind: {current['kind']!r}")
431
+ if not isinstance(current.get("freshness"), dict):
432
+ raise ValueError("CURRENT freshness must be an object")
433
+ if current["freshness"].get("validity") not in {"clean", "stale", "unknown"}:
434
+ raise ValueError("CURRENT freshness.validity must be clean|stale|unknown")
435
+ if not isinstance(current.get("evidence"), list):
436
+ raise ValueError("CURRENT evidence must be a list")
437
+
438
+
439
+ def _expand_with_lessons(query: str, project: str, limit: int) -> dict[str, Any]:
440
+ reasons: list[str] = []
441
+ try:
442
+ import lessons_index as li # type: ignore # noqa: PLC0415
443
+ except ImportError as exc:
444
+ return {"lessons": [], "stats": {}, "reasons": [f"lessons_index_import_failed: {exc}"]}
445
+ try:
446
+ ingest_global = li.ingest(project=None)
447
+ ingest_project = li.ingest(project=project)
448
+ except Exception as exc: # noqa: BLE001
449
+ reasons.append(f"lessons_index_ingest_failed: {exc}")
450
+ ingest_global = {}
451
+ ingest_project = {}
452
+ try:
453
+ lessons = li.query(goal_text=query or project, project=project, limit=limit)
454
+ except Exception as exc: # noqa: BLE001
455
+ reasons.append(f"lessons_index_query_failed: {exc}")
456
+ lessons = []
457
+ try:
458
+ stats = li.stats()
459
+ except Exception as exc: # noqa: BLE001
460
+ stats = {"error": str(exc)}
461
+ return {
462
+ "lessons": lessons,
463
+ "stats": stats,
464
+ "ingest": {"global": ingest_global, "project": ingest_project},
465
+ "reasons": reasons,
466
+ }
467
+
468
+
469
+ def _expand_with_graph(current: dict[str, Any], limit: int) -> dict[str, Any]:
470
+ project = str(current.get("project") or "")
471
+ seeds = [f"project:{project}"] if project else []
472
+ if not seeds:
473
+ for item in current.get("evidence", []):
474
+ item_id = str(item.get("id") or "")
475
+ if item_id and not item_id.startswith("context:"):
476
+ seeds.append(item_id)
477
+ # Preserve order while deduping.
478
+ seeds = list(dict.fromkeys(seeds))
479
+ if not seeds:
480
+ return {"related": [], "stats": {}, "seeds": [], "reasons": ["graph_no_seeds"]}
481
+
482
+ try:
483
+ from memory_graph import GraphStore # type: ignore # noqa: PLC0415
484
+ except ImportError as exc:
485
+ return {
486
+ "related": [],
487
+ "stats": {},
488
+ "seeds": seeds,
489
+ "reasons": [f"memory_graph_import_failed: {exc}"],
490
+ }
491
+ graph = None
492
+ try:
493
+ graph = GraphStore.open()
494
+ result = graph.related(seeds, depth=2, limit=max(limit * 5, 25), project=project)
495
+ except Exception as exc: # noqa: BLE001
496
+ return {
497
+ "related": [],
498
+ "stats": {},
499
+ "seeds": seeds,
500
+ "reasons": [f"memory_graph_query_failed: {exc}"],
501
+ }
502
+ finally:
503
+ if graph is not None and hasattr(graph, "close"):
504
+ graph.close()
505
+
506
+ related = []
507
+ for node in result.get("nodes", []):
508
+ if not node.get("path"):
509
+ continue
510
+ related.append(
511
+ {
512
+ "id": node.get("id"),
513
+ "title": node.get("title"),
514
+ "path": str(memory_store_root() / str(node.get("path"))),
515
+ "project": node.get("project"),
516
+ "memory_type": node.get("memory_type"),
517
+ "hop": node.get("hop"),
518
+ "score": node.get("score"),
519
+ }
520
+ )
521
+ related = related[:limit]
522
+ return {
523
+ "backend": result.get("backend"),
524
+ "query_shape": result.get("query_shape"),
525
+ "related": related,
526
+ "stats": result.get("stats", {}),
527
+ "seeds": result.get("seeds", seeds),
528
+ "reasons": result.get("reasons", []),
529
+ }
530
+
531
+
532
+ def build_context(
533
+ workdir: Path | str,
534
+ query: str = "",
535
+ *,
536
+ mode: str = "fast",
537
+ project: str | None = None,
538
+ write: bool = True,
539
+ limit: int = DEFAULT_LIMIT,
540
+ max_chars: int = DEFAULT_MAX_CHARS,
541
+ ) -> dict[str, Any]:
542
+ """Return a context envelope and optionally persist the L0 current capsule."""
543
+ if mode not in VALID_MODES:
544
+ raise ValueError(f"invalid mode {mode!r}; expected one of {sorted(VALID_MODES)}")
545
+ current = build_current(workdir, query=query, project=project, limit=limit, max_chars=max_chars)
546
+ written: dict[str, str] = {}
547
+ if write:
548
+ written = write_current(current)
549
+ envelope: dict[str, Any] = {
550
+ "schema_version": SCHEMA_VERSION,
551
+ "kind": KIND_CONTEXT,
552
+ "mode": mode,
553
+ "generated_at": utc_now(),
554
+ "current": current,
555
+ "written": written,
556
+ }
557
+ if mode == "expand":
558
+ expansion = _expand_with_lessons(query, current["project"], limit)
559
+ expansion["graph"] = _expand_with_graph(current, limit)
560
+ envelope["expansion"] = expansion
561
+ return envelope
562
+
563
+
564
+ def open_artifact(
565
+ artifact_id: str,
566
+ *,
567
+ workdir: Path | str,
568
+ project: str | None = None,
569
+ max_chars: int = 8000,
570
+ ) -> dict[str, Any]:
571
+ """Resolve an evidence id or safe memory-store path and return text."""
572
+ current = build_current(workdir, project=project)
573
+ by_id = {item["id"]: item for item in current.get("evidence", [])}
574
+ target: Path | None = None
575
+ if artifact_id in by_id:
576
+ target = Path(by_id[artifact_id]["path"])
577
+ else:
578
+ raw = Path(os.path.expanduser(artifact_id))
579
+ if not raw.is_absolute():
580
+ raw = memory_store_root() / raw
581
+ try:
582
+ root = memory_store_root().resolve()
583
+ resolved = raw.resolve()
584
+ if str(resolved) == str(root) or str(resolved).startswith(str(root) + os.sep):
585
+ target = resolved
586
+ except (OSError, RuntimeError):
587
+ target = None
588
+ if target is None:
589
+ return {"id": artifact_id, "exists": False, "reason": "unresolved"}
590
+ text, reason = _read_text(target, max_chars=max_chars)
591
+ return {
592
+ "id": artifact_id,
593
+ "path": str(target),
594
+ "exists": text is not None,
595
+ "text": text or "",
596
+ "truncated": bool(reason and reason.startswith("truncated:")),
597
+ "reason": reason,
598
+ }
599
+
600
+
601
+ __all__ = [
602
+ "build_context",
603
+ "build_current",
604
+ "current_paths",
605
+ "describe_access",
606
+ "open_artifact",
607
+ "render_current_markdown",
608
+ "validate_current",
609
+ "write_current",
610
+ ]