argus-code 0.4.3__tar.gz → 0.4.4__tar.gz

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 (202) hide show
  1. argus_code-0.4.4/AGENTS.md +119 -0
  2. argus_code-0.4.4/CLAUDE.md +14 -0
  3. {argus_code-0.4.3 → argus_code-0.4.4}/PKG-INFO +1 -1
  4. {argus_code-0.4.3 → argus_code-0.4.4}/SECURITY.md +22 -7
  5. argus_code-0.4.4/dashboard/AGENTS.md +35 -0
  6. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/tools.astro +5 -1
  7. argus_code-0.4.4/dashboard-dist/_astro/tools.astro_astro_type_script_index_0_lang.DHTUv2RL.js +12 -0
  8. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/tools/index.html +1 -1
  9. {argus_code-0.4.3 → argus_code-0.4.4}/pyproject.toml +1 -1
  10. argus_code-0.4.4/python/argus/AGENTS.md +50 -0
  11. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/__init__.py +1 -1
  12. argus_code-0.4.4/python/argus/adapters/AGENTS.md +44 -0
  13. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/adapter.py +14 -3
  14. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/discover.py +14 -1
  15. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/cli.py +24 -1
  16. argus_code-0.4.4/python/argus/collector/AGENTS.md +37 -0
  17. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/daemon/process.py +6 -0
  18. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/scaffold/scaffolder.py +29 -0
  19. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/scaffold/snapshot.py +42 -2
  20. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/scaffold/storage.py +17 -1
  21. argus_code-0.4.4/python/argus/server/AGENTS.md +46 -0
  22. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/server/api.py +36 -12
  23. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/server/app.py +58 -1
  24. argus_code-0.4.4/python/argus/store/AGENTS.md +43 -0
  25. argus_code-0.4.4/tests/AGENTS.md +35 -0
  26. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_discover.py +5 -5
  27. argus_code-0.4.4/tests/adapters/claude_code/test_path_containment.py +154 -0
  28. {argus_code-0.4.3 → argus_code-0.4.4}/tests/cli/test_indexing_cli.py +47 -47
  29. {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/test_process.py +3 -1
  30. argus_code-0.4.4/tests/daemon/test_spawn_safe_path.py +64 -0
  31. argus_code-0.4.4/tests/dashboard/test_chart_tooltip_escaping.py +87 -0
  32. argus_code-0.4.4/tests/scaffold/test_scaffold_containment.py +157 -0
  33. argus_code-0.4.4/tests/scaffold/test_snapshot_secrets.py +103 -0
  34. {argus_code-0.4.3 → argus_code-0.4.4}/tests/server/test_api.py +11 -7
  35. {argus_code-0.4.3 → argus_code-0.4.4}/tests/server/test_api_search.py +5 -1
  36. argus_code-0.4.4/tests/server/test_param_bounds.py +99 -0
  37. {argus_code-0.4.3 → argus_code-0.4.4}/tests/server/test_server.py +153 -6
  38. argus_code-0.4.4/tests/store/__init__.py +0 -0
  39. {argus_code-0.4.3 → argus_code-0.4.4}/tests/test_e2e.py +5 -1
  40. {argus_code-0.4.3 → argus_code-0.4.4}/uv.lock +4 -4
  41. argus_code-0.4.3/dashboard-dist/_astro/tools.astro_astro_type_script_index_0_lang.OvyHH6MH.js +0 -12
  42. {argus_code-0.4.3 → argus_code-0.4.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  43. {argus_code-0.4.3 → argus_code-0.4.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  44. {argus_code-0.4.3 → argus_code-0.4.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  45. {argus_code-0.4.3 → argus_code-0.4.4}/.github/pull_request_template.md +0 -0
  46. {argus_code-0.4.3 → argus_code-0.4.4}/.gitignore +0 -0
  47. {argus_code-0.4.3 → argus_code-0.4.4}/.npmrc +0 -0
  48. {argus_code-0.4.3 → argus_code-0.4.4}/ARCHITECTURE.md +0 -0
  49. {argus_code-0.4.3 → argus_code-0.4.4}/CONTRIBUTING.md +0 -0
  50. {argus_code-0.4.3 → argus_code-0.4.4}/LICENSE +0 -0
  51. {argus_code-0.4.3 → argus_code-0.4.4}/NOTICE +0 -0
  52. {argus_code-0.4.3 → argus_code-0.4.4}/PRD.md +0 -0
  53. {argus_code-0.4.3 → argus_code-0.4.4}/README.md +0 -0
  54. {argus_code-0.4.3 → argus_code-0.4.4}/TESTING.md +0 -0
  55. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/.astro/content-assets.mjs +0 -0
  56. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/.astro/content-modules.mjs +0 -0
  57. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/.astro/content.d.ts +0 -0
  58. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/.astro/types.d.ts +0 -0
  59. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/astro.config.mjs +0 -0
  60. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/package-lock.json +0 -0
  61. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/package.json +0 -0
  62. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/public/styles/global.css +0 -0
  63. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/layouts/Default.astro +0 -0
  64. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/index.astro +0 -0
  65. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/models.astro +0 -0
  66. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/prompts.astro +0 -0
  67. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/session.astro +0 -0
  68. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/sessions/index.astro +0 -0
  69. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/settings.astro +0 -0
  70. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/pages/trends.astro +0 -0
  71. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/scripts/alerts.ts +0 -0
  72. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/scripts/api.ts +0 -0
  73. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/scripts/charts.ts +0 -0
  74. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/scripts/format.ts +0 -0
  75. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/src/styles/global.css +0 -0
  76. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard/tsconfig.json +0 -0
  77. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/charts.36uaSAWt.js +0 -0
  78. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/format.BexCUztC.js +0 -0
  79. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.BF0nZzma.js +0 -0
  80. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.D-QKQJAF.js +0 -0
  81. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/installCanvasRenderer.XK5yP7fW.js +0 -0
  82. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/models.astro_astro_type_script_index_0_lang.0Aczy7Vm.js +0 -0
  83. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/prompts.astro_astro_type_script_index_0_lang.B1i5N4zM.js +0 -0
  84. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/session.CGJ-XRoC.css +0 -0
  85. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/session.astro_astro_type_script_index_0_lang.SRsknMTL.js +0 -0
  86. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/settings.astro_astro_type_script_index_0_lang.DyHh8NQi.js +0 -0
  87. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/_astro/trends.astro_astro_type_script_index_0_lang.CBtUcxGK.js +0 -0
  88. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/index.html +0 -0
  89. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/models/index.html +0 -0
  90. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/prompts/index.html +0 -0
  91. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/session/index.html +0 -0
  92. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/sessions/index.html +0 -0
  93. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/settings/index.html +0 -0
  94. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/styles/global.css +0 -0
  95. {argus_code-0.4.3 → argus_code-0.4.4}/dashboard-dist/trends/index.html +0 -0
  96. {argus_code-0.4.3 → argus_code-0.4.4}/pricing/2026-05-02.json +0 -0
  97. {argus_code-0.4.3 → argus_code-0.4.4}/pricing/2026-06-12.json +0 -0
  98. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/__init__.py +0 -0
  99. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/base.py +0 -0
  100. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/__init__.py +0 -0
  101. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/extract_tool_calls.py +0 -0
  102. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/extract_transcript.py +0 -0
  103. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/extract_turns.py +0 -0
  104. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/history_jsonl.py +0 -0
  105. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/ingest_file.py +0 -0
  106. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/model.py +0 -0
  107. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/claude_code/schemas.py +0 -0
  108. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/adapters/registry.py +0 -0
  109. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/__init__.py +0 -0
  110. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/aggregate.py +0 -0
  111. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/first_run.py +0 -0
  112. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/pipeline.py +0 -0
  113. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/rollup_subagents.py +0 -0
  114. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/scheduler.py +0 -0
  115. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/search_backfill.py +0 -0
  116. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/collector/watcher.py +0 -0
  117. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/core/__init__.py +0 -0
  118. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/core/runtime.py +0 -0
  119. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/daemon/__init__.py +0 -0
  120. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/daemon/logging.py +0 -0
  121. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/daemon/pidfile.py +0 -0
  122. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/daemon/service.py +0 -0
  123. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/detectors/__init__.py +0 -0
  124. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/detectors/base.py +0 -0
  125. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/detectors/registry.py +0 -0
  126. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/detectors/tool_error_rate_spike.py +0 -0
  127. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/pricing/__init__.py +0 -0
  128. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/pricing/compute.py +0 -0
  129. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/pricing/load.py +0 -0
  130. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/pricing/refresh.py +0 -0
  131. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/pricing/types.py +0 -0
  132. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/scaffold/__init__.py +0 -0
  133. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/schema/__init__.py +0 -0
  134. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/schema/types.py +0 -0
  135. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/server/__init__.py +0 -0
  136. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/store/__init__.py +0 -0
  137. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/store/db.py +0 -0
  138. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/store/migrations/__init__.py +0 -0
  139. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/store/migrations/inline.py +0 -0
  140. {argus_code-0.4.3 → argus_code-0.4.4}/python/argus/store/repository.py +0 -0
  141. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/agents/code-reviewer.md +0 -0
  142. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/agents/security-auditor.md +0 -0
  143. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/commands/commit.md +0 -0
  144. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/commands/deploy.md +0 -0
  145. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/commands/fix-issue.md +0 -0
  146. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/commands/pr.md +0 -0
  147. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/commands/review.md +0 -0
  148. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/rules/api-conventions.md +0 -0
  149. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/rules/code-style.md +0 -0
  150. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/rules/testing.md +0 -0
  151. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/settings.json +0 -0
  152. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/.claude/skills/example/SKILL.md +0 -0
  153. {argus_code-0.4.3 → argus_code-0.4.4}/templates/default/CLAUDE.md +0 -0
  154. {argus_code-0.4.3 → argus_code-0.4.4}/tests/__init__.py +0 -0
  155. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/__init__.py +0 -0
  156. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/__init__.py +0 -0
  157. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_adapter.py +0 -0
  158. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_extract_tool_calls.py +0 -0
  159. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_extract_transcript.py +0 -0
  160. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_extract_turns.py +0 -0
  161. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_history_jsonl.py +0 -0
  162. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_ingest_file.py +0 -0
  163. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_integration.py +0 -0
  164. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_model.py +0 -0
  165. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/claude_code/test_schemas.py +0 -0
  166. {argus_code-0.4.3 → argus_code-0.4.4}/tests/adapters/test_registry.py +0 -0
  167. {argus_code-0.4.3 → argus_code-0.4.4}/tests/cli/__init__.py +0 -0
  168. {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/__init__.py +0 -0
  169. {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_aggregate.py +0 -0
  170. {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_first_run.py +0 -0
  171. {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_pipeline.py +0 -0
  172. {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_rollup_subagents.py +0 -0
  173. {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_scheduler.py +0 -0
  174. {argus_code-0.4.3 → argus_code-0.4.4}/tests/collector/test_watcher.py +0 -0
  175. {argus_code-0.4.3 → argus_code-0.4.4}/tests/conftest.py +0 -0
  176. {argus_code-0.4.3 → argus_code-0.4.4}/tests/core/__init__.py +0 -0
  177. {argus_code-0.4.3 → argus_code-0.4.4}/tests/core/test_runtime.py +0 -0
  178. {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/__init__.py +0 -0
  179. {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/test_logs.py +0 -0
  180. {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/test_pidfile.py +0 -0
  181. {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/test_service.py +0 -0
  182. {argus_code-0.4.3 → argus_code-0.4.4}/tests/daemon/test_yield.py +0 -0
  183. {argus_code-0.4.3/tests/detectors → argus_code-0.4.4/tests/dashboard}/__init__.py +0 -0
  184. {argus_code-0.4.3/tests/pricing → argus_code-0.4.4/tests/detectors}/__init__.py +0 -0
  185. {argus_code-0.4.3 → argus_code-0.4.4}/tests/detectors/test_registry.py +0 -0
  186. {argus_code-0.4.3 → argus_code-0.4.4}/tests/detectors/test_tool_error_rate_spike.py +0 -0
  187. {argus_code-0.4.3/tests/scaffold → argus_code-0.4.4/tests/pricing}/__init__.py +0 -0
  188. {argus_code-0.4.3 → argus_code-0.4.4}/tests/pricing/test_compute.py +0 -0
  189. {argus_code-0.4.3 → argus_code-0.4.4}/tests/pricing/test_load.py +0 -0
  190. {argus_code-0.4.3 → argus_code-0.4.4}/tests/pricing/test_refresh.py +0 -0
  191. {argus_code-0.4.3/tests/schema → argus_code-0.4.4/tests/scaffold}/__init__.py +0 -0
  192. {argus_code-0.4.3 → argus_code-0.4.4}/tests/scaffold/test_bundled_template.py +0 -0
  193. {argus_code-0.4.3 → argus_code-0.4.4}/tests/scaffold/test_cli_claude.py +0 -0
  194. {argus_code-0.4.3 → argus_code-0.4.4}/tests/scaffold/test_scaffolder.py +0 -0
  195. {argus_code-0.4.3 → argus_code-0.4.4}/tests/scaffold/test_snapshot.py +0 -0
  196. {argus_code-0.4.3 → argus_code-0.4.4}/tests/scaffold/test_storage.py +0 -0
  197. {argus_code-0.4.3/tests/server → argus_code-0.4.4/tests/schema}/__init__.py +0 -0
  198. {argus_code-0.4.3 → argus_code-0.4.4}/tests/schema/test_types.py +0 -0
  199. {argus_code-0.4.3/tests/store → argus_code-0.4.4/tests/server}/__init__.py +0 -0
  200. {argus_code-0.4.3 → argus_code-0.4.4}/tests/server/test_week_of.py +0 -0
  201. {argus_code-0.4.3 → argus_code-0.4.4}/tests/store/test_db.py +0 -0
  202. {argus_code-0.4.3 → argus_code-0.4.4}/tests/store/test_repository.py +0 -0
@@ -0,0 +1,119 @@
1
+ # AGENTS.md — Argus (argus-code) root work contract
2
+
3
+ > This repository uses the **DOX framework**: AGENTS.md files are binding work
4
+ > contracts for their subtree. Before editing any path, read this file and every
5
+ > AGENTS.md on the route from the repo root down to that path. The nearest doc
6
+ > controls local detail; this root controls repo-wide rules. No child doc may
7
+ > weaken DOX. Re-read the chain in-session; do not rely on memory.
8
+ >
9
+ > **After a meaningful change, run the DOX pass:** update the nearest owning
10
+ > AGENTS.md (and any affected parent/child index) when you change purpose, scope,
11
+ > contracts, workflows, inputs/outputs, or the doc tree itself; delete stale text;
12
+ > then re-check changed paths against the chain. Pure no-behavior edits may leave
13
+ > docs unchanged, but the pass still happens.
14
+
15
+ ## Purpose
16
+
17
+ Argus is a **local-first** analytics dashboard for Claude Code (and other coding
18
+ agents). A watcher ingests Claude Code's `.jsonl` session transcripts into a local
19
+ SQLite archive (`~/.argus/argus.db`); a FastAPI server serves a static Astro
20
+ dashboard at `http://localhost:4242`. No network, no telemetry, no LLM calls
21
+ (except opt-in `argus pricing refresh`). Backend + dashboard ship together in the
22
+ `argus-code` wheel on PyPI.
23
+
24
+ Orientation docs (prose, not contracts — read but don't duplicate into AGENTS.md):
25
+ - `ARCHITECTURE.md` — how the pieces fit together (read before adding a feature).
26
+ - `CONTRIBUTING.md` — dev quick start (`uv sync`, `uv run argus start`).
27
+ - `TESTING.md` — test catalog and conventions.
28
+
29
+ ## Ownership
30
+
31
+ This root owns: the development workflow below, global engineering rules, the
32
+ release process, and the top-level Child DOX Index. Code-level contracts live in
33
+ the nearest child AGENTS.md.
34
+
35
+ ## Local Contracts
36
+
37
+ ### Development workflow (feature / bug → PR → merge → release → tag → publish)
38
+
39
+ Follow this end-to-end for every feature or bug fix.
40
+
41
+ 1. **Branch.** Never commit feature/bug work directly to `main`. Branch from an
42
+ up-to-date `main`: `fix/<slug>` for bugs, `feat/<slug>` for features.
43
+ 2. **Implement test-first.** Reproduce the bug (or specify the feature) with a
44
+ failing test, then make it pass. See `tests/AGENTS.md`.
45
+ 3. **Commit** in small, logical units. Conventional-commit subjects
46
+ (`fix:`, `feat:`, `test:`, `release:`, `docs:`). End every commit message with:
47
+ ```
48
+ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49
+ ```
50
+ 4. **Verify** before opening the PR — see Verification below. Tests must be green
51
+ (the one known Windows flake is the scheduler timing test; confirm it passes in
52
+ isolation).
53
+ 5. **Push & open PR** against `main`: `gh pr create --base main --head <branch>`.
54
+ End the PR body with:
55
+ ```
56
+ 🤖 Generated with [Claude Code](https://claude.com/claude-code)
57
+ ```
58
+ 6. **Merge** with squash and delete the branch:
59
+ `gh pr merge <n> --squash --delete-branch`. Then sync local `main`.
60
+ 7. **Release** (only when cutting a version — see Release process below).
61
+ 8. **Tag + GitHub release:** `gh release create vX.Y.Z --target main --title vX.Y.Z --notes "..."`.
62
+ 9. **PyPI publish is the maintainer's manual step** — see Release process.
63
+
64
+ ### Release process (manual — no CI release, no stored credentials)
65
+
66
+ When cutting version `X.Y.Z`:
67
+ 1. Bump the version in **both** files with the Edit tool (not PowerShell
68
+ `Set-Content` — it injects a BOM): `pyproject.toml` (`version = "X.Y.Z"`) and
69
+ `python/argus/__init__.py` (`__version__ = "X.Y.Z"`).
70
+ 2. `uv lock` to sync `uv.lock` to the new version.
71
+ 3. If the dashboard changed, rebuild and refresh the shipped copy — see
72
+ `dashboard/AGENTS.md`.
73
+ 4. Clean-build the artifacts: **`rm -rf dist && uv build`** (always clear `dist/`
74
+ first, or stale files get uploaded).
75
+ 5. Confirm the wheel ships the right version + dashboard before publishing.
76
+ 6. Tag + GitHub release (workflow step 8).
77
+ 7. **Publish to PyPI — maintainer only:** `uv publish --token pypi-<token>`. The
78
+ token is the maintainer's secret; **an agent must never run this or handle the
79
+ token.** Stop and hand off.
80
+
81
+ ## Work Guidance
82
+
83
+ Global engineering rules (binding):
84
+ - **Never destroy user data in `~/.argus/argus.db`.** Migrations must be
85
+ non-destructive, idempotent, and self-healing. See `python/argus/store/AGENTS.md`.
86
+ - **No hacks to make a test pass.** Fix the real cause, or correct a stale test
87
+ expectation to match the real contract (and say so). This is a prod package used
88
+ by many; never hardcode a value just to go green.
89
+ - **Backend runs from source** (`uv run argus start`) — no build step for Python.
90
+ Only the dashboard has a build step.
91
+ - Plans/specs under `docs/superpowers/` are scratch and gitignored — do not commit them.
92
+
93
+ ## Verification
94
+
95
+ - Backend: `uv run pytest` (must be green; ~290 tests, ~25s on Windows).
96
+ - Dashboard: `cd dashboard && npm run build` (must succeed).
97
+ - Built form smoke test: see `CONTRIBUTING.md` "Running the built form".
98
+
99
+ ## User Preferences
100
+
101
+ - Releases are cut deliberately by the maintainer; agents prepare everything up to
102
+ (and including the GitHub tag/release when asked) but never run `uv publish`.
103
+ - Prefer honest reporting over green-at-any-cost: surface flakes, skips, and
104
+ pre-existing failures explicitly.
105
+
106
+ ## Child DOX Index
107
+
108
+ - `python/argus/AGENTS.md` — Python backend package; owns the subsystems without
109
+ their own doc (`core`, `daemon`, `detectors`, `pricing`, `scaffold`, `schema`)
110
+ and indexes the four below.
111
+ - `python/argus/store/AGENTS.md` — SQLite, migrations, repository; data-safety
112
+ and path-normalization contracts.
113
+ - `python/argus/collector/AGENTS.md` — ingest pipeline and the missing-data
114
+ backfill (incl. sub-agent segment re-read).
115
+ - `python/argus/adapters/AGENTS.md` — Claude Code adapter and transcript-segment
116
+ extraction; sub-agent file layout.
117
+ - `python/argus/server/AGENTS.md` — FastAPI routes, static serving, clean shutdown.
118
+ - `dashboard/AGENTS.md` — Astro dashboard and the `dashboard-dist` ship chain.
119
+ - `tests/AGENTS.md` — pytest conventions and platform quirks.
@@ -0,0 +1,14 @@
1
+ # CLAUDE.md
2
+
3
+ This repository uses the **DOX framework** — an `AGENTS.md` hierarchy of binding
4
+ work contracts. `AGENTS.md` (repo root) is the source of truth, including the full
5
+ development workflow (branch → PR → squash-merge → release → tag → publish).
6
+
7
+ **Before editing any path:** read the root `AGENTS.md`, then every `AGENTS.md` on
8
+ the route down to that path; the nearest doc controls local detail. **After a
9
+ meaningful change:** run the DOX closeout (update the nearest owning doc and any
10
+ affected parent/child indexes). Re-read the chain in-session — don't rely on memory.
11
+
12
+ The root contract (imported in full below):
13
+
14
+ @AGENTS.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argus-code
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: Local-first dashboard for Claude Code cost, token, tool-usage, and full-text search analytics
5
5
  Project-URL: Homepage, https://github.com/KrishBhimani/argus-code
6
6
  Project-URL: Repository, https://github.com/KrishBhimani/argus-code.git
@@ -33,8 +33,10 @@ Argus is built for one user, one machine. It assumes:
33
33
  before rendering in the dashboard, and never executed.
34
34
  - **Untrusted browsers visiting localhost.** Other webpages the user
35
35
  loads while Argus is running are not trusted. They cannot read
36
- Argus data (Same-Origin Policy on responses) and cannot trigger
37
- state changes (CSRF Origin check on POST).
36
+ Argus data (Same-Origin Policy on responses, backed by a loopback
37
+ `Host` allowlist so DNS rebinding can't turn a hostile domain into
38
+ a same-origin one) and cannot trigger state changes (CSRF Origin
39
+ check on POST).
38
40
 
39
41
  ## Defaults that matter
40
42
 
@@ -78,14 +80,23 @@ Things this codebase deliberately does to reduce attack surface:
78
80
  `escapeHtml()` before reaching `innerHTML`. The only HTML allowed
79
81
  through is `<mark>...</mark>` from FTS5 `snippet()`, applied via a
80
82
  separate `safeSnippet()` helper that escapes everything else.
81
- - `watchdog` does not follow symlinks by default. `discover_session_files`
82
- additionally calls `Path.resolve(strict=True)` on each candidate
83
- and rejects anything that doesn't canonicalise under the
84
- `~/.claude/` tree.
83
+ - `watchdog` does not follow symlinks by default. Every read additionally
84
+ goes through `ClaudeCodeAdapter.ingest_file`, which calls
85
+ `Path.resolve(strict=True)` and refuses anything that doesn't
86
+ canonicalise under the `~/.claude/` tree — symlink or NTFS junction.
87
+ The check sits at that single choke point (not at each caller) so
88
+ discovery, the watcher, and the sub-agent walk are all covered by
89
+ construction; `sub_agent_files_for` filters as well.
85
90
  - Per-tick read cap of 64 MiB on session JSONL and `history.jsonl`,
86
91
  so a runaway or hostile multi-GB file can't OOM the process.
87
92
  - CSRF Origin check on all non-GET API routes (FastAPI middleware in
88
93
  `python/argus/server/app.py`).
94
+ - Loopback `Host` header allowlist on **every** route, static mount
95
+ included (`is_loopback_host` in `python/argus/server/app.py`). This is
96
+ what stops DNS rebinding: a page on `evil.com` that repoints its own
97
+ DNS at `127.0.0.1` still sends `Host: evil.com` and gets a 421, so it
98
+ never reaches the GET routes the Origin check deliberately exempts.
99
+ Skipped only under `--host 0.0.0.0`, which is already a warned opt-out.
89
100
  - No build hooks running on user machines. The wheel ships pure Python
90
101
  + bundled data (`dashboard-dist/`, `pricing/`, `templates/`).
91
102
  Installation runs no arbitrary code.
@@ -95,7 +106,11 @@ Things this codebase deliberately does to reduce attack surface:
95
106
  - `argus claude` scaffolding is a pure file copy — no templating,
96
107
  substitution, or code execution. It writes only to the path you hand
97
108
  `init` (created if absent) or under `~/.argus/templates/`, and never
98
- overwrites an existing `CLAUDE.md`.
109
+ overwrites an existing `CLAUDE.md`. That containment is enforced, not
110
+ just intended: every destination must canonicalise under the target
111
+ directory, so a hostile repo shipping `.claude/` as a symlink or NTFS
112
+ junction can't redirect the write, and template names are validated as
113
+ plain directory names so `--template ../../x` can't escape the store.
99
114
 
100
115
  If you spot a regression on any of the above, that's a real bug and
101
116
  worth reporting.
@@ -0,0 +1,35 @@
1
+ # AGENTS.md — dashboard (Astro frontend + ship chain)
2
+
3
+ Parent: repo-root `AGENTS.md`. The statically-built UI served by `server/`.
4
+
5
+ ## Purpose
6
+
7
+ Astro + ECharts dashboard. Source pages live in `src/pages/*.astro`
8
+ (`session.astro`, `settings.astro`, `overview`, `timeline`, …) with shared
9
+ scripts in `src/scripts/`.
10
+
11
+ ## Local Contracts
12
+
13
+ - **Three "dist" directories — don't conflate them:**
14
+ - `dashboard/dist/` — raw Astro build output. **Gitignored.**
15
+ - `dashboard-dist/` (repo root) — the **tracked** copy that ships in the wheel
16
+ via `pyproject.toml` force-include (`"dashboard-dist" = "argus/dashboard-dist"`).
17
+ - `dist/` (repo root) — `uv build` output uploaded to PyPI. **Gitignored.**
18
+ - **A dashboard source edit is invisible to the package until `dashboard-dist` is
19
+ refreshed and committed.** After changing anything under `dashboard/src/`:
20
+ ```sh
21
+ cd dashboard && npm run build && cd ..
22
+ git rm -r --quiet dashboard-dist && cp -r dashboard/dist dashboard-dist && git add -A dashboard-dist
23
+ ```
24
+ Commit the rebuilt `dashboard-dist` alongside the source change. Astro filenames
25
+ are content-hashed, so a real change shows as new `_astro/*.js|css` plus updated
26
+ `*/index.html` — verify the new asset (and absence of stale strings) in
27
+ `dashboard-dist` before committing.
28
+ - Avoid em dashes / non-ASCII in strings that surface in the **terminal**
29
+ (CLI/console) — they mojibake on the Windows console. (UI text is fine.)
30
+
31
+ ## Verification
32
+
33
+ `cd dashboard && npm run build` must succeed. After refreshing `dashboard-dist`,
34
+ optionally confirm the wheel ships it (`rm -rf dist && uv build`, then inspect the
35
+ `.whl`).
@@ -72,7 +72,11 @@ import Default from '../layouts/Default.astro';
72
72
  const i = p[0].dataIndex;
73
73
  const calls = values[i];
74
74
  const err = errors?.[i] ?? 0;
75
- return `${labels[i]}<br>Calls: ${calls.toLocaleString()}` +
75
+ // ECharts renders a custom tooltip formatter's return value as
76
+ // HTML, so the label -- a tool / MCP-server / sub-agent name read
77
+ // verbatim from .jsonl -- must be escaped here too, exactly like
78
+ // every other innerHTML sink on this page.
79
+ return `${escapeHtml(labels[i])}<br>Calls: ${calls.toLocaleString()}` +
76
80
  (err ? `<br>Errors: ${err.toLocaleString()} (${((err/calls)*100).toFixed(1)}%)` : '');
77
81
  },
78
82
  },
@@ -0,0 +1,12 @@
1
+ import{a as C,n,e as u}from"./format.BexCUztC.js";import{u as S,i as L,a as k,b as M,c as T,d as z,e as E}from"./installCanvasRenderer.XK5yP7fW.js";S([L,k,M,T,z]);const a=e=>document.querySelector(e),f=a("#w");let $,g,w;function h(e,t,o,l,c){const i=E(e);return i.setOption({textStyle:{color:"#9ba6b3"},tooltip:{trigger:"axis",axisPointer:{type:"shadow"},backgroundColor:"#1c222d",borderColor:"#262d3a",textStyle:{color:"#e6edf3",fontSize:12},formatter:d=>{const p=d[0].dataIndex,b=o[p],r=c?.[p]??0;return`${u(t[p])}<br>Calls: ${b.toLocaleString()}`+(r?`<br>Errors: ${r.toLocaleString()} (${(r/b*100).toFixed(1)}%)`:"")}},grid:{left:160,right:30,top:8,bottom:28},xAxis:{type:"value",axisLabel:{color:"#6b7585",fontSize:11},splitLine:{lineStyle:{color:"#1c222d"}},axisLine:{show:!1}},yAxis:{type:"category",data:t.slice().reverse(),axisLabel:{color:"#6b7585",fontSize:11},axisLine:{show:!1},axisTick:{show:!1}},series:[{type:"bar",data:o.slice().reverse(),itemStyle:{color:l,borderRadius:[0,4,4,0]},barWidth:"60%"}]}),i}function v(e,t,o,l="scalar"){const c=l==="name"?"font-size:1.05rem;font-family:'SF Mono',Consolas,monospace;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;display:block;color:var(--text-0);":"",i=l==="name"?`title="${t.replace(/"/g,"&quot;")}"`:"";return`<div class="card"><div class="kpi">
2
+ <div class="kpi-label">${e}</div>
3
+ <div class="kpi-value" style="${c}" ${i}>${t}</div>
4
+ ${o?`<div class="kpi-sub">${o}</div>`:""}
5
+ </div></div>`}async function x(){const e=await C.toolsOverview(f.value),t=e.total_calls,o=e.total_errors,l=t>0?(o/t*100).toFixed(1)+"%":"—",c=e.tool_leaderboard[0]?.name??"—",i=e.mcp_servers[0]?.server??"—";if(a("#kpis").innerHTML=[v("Tool calls",n(t),f.value==="all"?"all time":f.value),v("Error rate",l,o>0?`${n(o)} errors`:"no errors"),v("Top tool",u(c),e.tool_leaderboard[0]?`${n(e.tool_leaderboard[0].calls)} calls`:"","name"),v("Top MCP",u(i),e.mcp_servers[0]?`${n(e.mcp_servers[0].calls)} calls`:"no MCP usage","name")].join(""),$?.dispose(),e.tool_leaderboard.length===0)a("#leaderboard").innerHTML='<p class="empty">No tool activity in this window.</p>';else{const s=e.tool_leaderboard.slice(0,12);$=h(a("#leaderboard"),s.map(m=>m.name),s.map(m=>m.calls),"#f0883e",s.map(m=>m.errors))}const d=a("#splits"),p=d.children[0],b=d.children[1],r=e.mcp_servers.length>0,y=e.subagents.length>0;g?.dispose(),w?.dispose(),d.style.display=r||y?"":"none",p.style.display=r?"":"none",b.style.display=y?"":"none",r&&(g=h(a("#mcp"),e.mcp_servers.map(s=>s.server),e.mcp_servers.map(s=>s.calls),"#58a6ff",e.mcp_servers.map(s=>s.errors))),y&&(w=h(a("#subagents"),e.subagents.map(s=>s.type),e.subagents.map(s=>s.calls),"#bc8cff",e.subagents.map(s=>s.errors)));const _=a("#tools-table tbody");_.innerHTML=e.tool_leaderboard.length===0?'<tr><td colspan="4" class="empty">—</td></tr>':e.tool_leaderboard.map(s=>`
6
+ <tr>
7
+ <td><code>${u(s.name)}</code></td>
8
+ <td class="num">${n(s.calls)}</td>
9
+ <td class="num">${n(s.errors)}</td>
10
+ <td class="num">${(s.error_rate*100).toFixed(1)}%</td>
11
+ </tr>
12
+ `).join("")}f.addEventListener("change",x);x();window.addEventListener("resize",()=>[$,g,w].forEach(e=>e?.resize()));
@@ -1 +1 @@
1
- <!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Argus — Tools</title><link rel="stylesheet" href="/styles/global.css"></head> <body> <header class="app-header"> <a href="/" class="brand">ARGUS<span class="dot">●</span></a> <nav class="app-nav"> <a href="/" class>Overview</a> <a href="/sessions" class>Sessions</a> <a href="/tools" class>Tools</a> <a href="/prompts" class>Prompts</a> <a href="/trends" class>Trends</a> <a href="/models" class>Models</a> <a href="/settings" class>Settings</a> </nav> </header> <main> <h2>Tool usage</h2> <div class="card" style="margin-bottom:1rem;display:flex;gap:0.6rem;align-items:center;"> <label style="color:var(--text-2);font-size:0.8rem;">Window</label> <select id="w"> <option value="today">Today</option> <option value="7d" selected>Last 7 days</option> <option value="30d">Last 30 days</option> <option value="all">All time</option> </select> </div> <div class="grid-cards" id="kpis"></div> <div class="card" style="margin-top:1.2rem;"> <h3>Tool leaderboard</h3> <div id="leaderboard" class="chart-tall"></div> </div> <div class="grid-2" style="margin-top:1.2rem;" id="splits"> <div class="card"> <h3>MCP servers</h3> <div id="mcp" class="chart"></div> </div> <div class="card"> <h3>Sub-agent invocations</h3> <div id="subagents" class="chart"></div> </div> </div> <div class="card" style="margin-top:1.2rem;"> <h3>Per-tool details</h3> <table id="tools-table"> <thead> <tr> <th>Tool</th> <th class="num">Calls</th> <th class="num">Errors</th> <th class="num">Error rate</th> </tr> </thead> <tbody></tbody> </table> </div> <script type="module" src="/_astro/tools.astro_astro_type_script_index_0_lang.OvyHH6MH.js"></script> </main> <footer class="app-footer" id="footer"> <span class="status-pill" id="ingest-status">Loading…</span> <span style="margin: 0 0.8rem;">·</span> <span id="pricing-info">Pricing —</span> <span id="daemon-status" style="margin-left: 0.8rem; display: none;" title="Ingestion is handled by the argusd background daemon">· Powered by argusd ●</span> </footer> <script type="module">async function c(){try{const[n,e]=await Promise.all([fetch("/api/pricing").then(s=>s.json()),fetch("/api/ingest/status").then(s=>s.json())]),o=document.getElementById("pricing-info");o&&(o.innerHTML=`Costs estimated from pricing table v<code>${n.version}</code> — tokens are exact; <code>argus pricing refresh</code> to update`);const t=document.getElementById("ingest-status");if(t)if(e.foregroundComplete&&e.pending===0){const s=typeof e.sessionCount=="number"?e.sessionCount:e.processed;t.textContent=`Tracking ${s} sessions`,t.classList.remove("busy")}else e.foregroundComplete?(t.textContent=`Backfilling history... ${e.processed}/${e.total}`,t.classList.add("busy")):(t.textContent=`Ingesting recent... ${e.processed}/${e.total}`,t.classList.add("busy"));const i=document.getElementById("daemon-status");i&&(i.style.display=e.daemon===!0?"inline":"none")}catch{}}c();setInterval(c,4e3);</script> </body> </html>
1
+ <!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Argus — Tools</title><link rel="stylesheet" href="/styles/global.css"></head> <body> <header class="app-header"> <a href="/" class="brand">ARGUS<span class="dot">●</span></a> <nav class="app-nav"> <a href="/" class>Overview</a> <a href="/sessions" class>Sessions</a> <a href="/tools" class>Tools</a> <a href="/prompts" class>Prompts</a> <a href="/trends" class>Trends</a> <a href="/models" class>Models</a> <a href="/settings" class>Settings</a> </nav> </header> <main> <h2>Tool usage</h2> <div class="card" style="margin-bottom:1rem;display:flex;gap:0.6rem;align-items:center;"> <label style="color:var(--text-2);font-size:0.8rem;">Window</label> <select id="w"> <option value="today">Today</option> <option value="7d" selected>Last 7 days</option> <option value="30d">Last 30 days</option> <option value="all">All time</option> </select> </div> <div class="grid-cards" id="kpis"></div> <div class="card" style="margin-top:1.2rem;"> <h3>Tool leaderboard</h3> <div id="leaderboard" class="chart-tall"></div> </div> <div class="grid-2" style="margin-top:1.2rem;" id="splits"> <div class="card"> <h3>MCP servers</h3> <div id="mcp" class="chart"></div> </div> <div class="card"> <h3>Sub-agent invocations</h3> <div id="subagents" class="chart"></div> </div> </div> <div class="card" style="margin-top:1.2rem;"> <h3>Per-tool details</h3> <table id="tools-table"> <thead> <tr> <th>Tool</th> <th class="num">Calls</th> <th class="num">Errors</th> <th class="num">Error rate</th> </tr> </thead> <tbody></tbody> </table> </div> <script type="module" src="/_astro/tools.astro_astro_type_script_index_0_lang.DHTUv2RL.js"></script> </main> <footer class="app-footer" id="footer"> <span class="status-pill" id="ingest-status">Loading…</span> <span style="margin: 0 0.8rem;">·</span> <span id="pricing-info">Pricing —</span> <span id="daemon-status" style="margin-left: 0.8rem; display: none;" title="Ingestion is handled by the argusd background daemon">· Powered by argusd ●</span> </footer> <script type="module">async function c(){try{const[n,e]=await Promise.all([fetch("/api/pricing").then(s=>s.json()),fetch("/api/ingest/status").then(s=>s.json())]),o=document.getElementById("pricing-info");o&&(o.innerHTML=`Costs estimated from pricing table v<code>${n.version}</code> — tokens are exact; <code>argus pricing refresh</code> to update`);const t=document.getElementById("ingest-status");if(t)if(e.foregroundComplete&&e.pending===0){const s=typeof e.sessionCount=="number"?e.sessionCount:e.processed;t.textContent=`Tracking ${s} sessions`,t.classList.remove("busy")}else e.foregroundComplete?(t.textContent=`Backfilling history... ${e.processed}/${e.total}`,t.classList.add("busy")):(t.textContent=`Ingesting recent... ${e.processed}/${e.total}`,t.classList.add("busy"));const i=document.getElementById("daemon-status");i&&(i.style.display=e.daemon===!0?"inline":"none")}catch{}}c();setInterval(c,4e3);</script> </body> </html>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "argus-code"
3
- version = "0.4.3"
3
+ version = "0.4.4"
4
4
  description = "Local-first dashboard for Claude Code cost, token, tool-usage, and full-text search analytics"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,50 @@
1
+ # AGENTS.md — Python backend package (`argus`)
2
+
3
+ Parent: repo-root `AGENTS.md`. Read it first for the workflow and global rules.
4
+
5
+ ## Purpose
6
+
7
+ The `argus` package: ingest Claude Code transcripts, store them in SQLite, serve
8
+ the dashboard, and provide the `argus` CLI. Runs directly from source
9
+ (`uv run argus start`) — no build step. Entry point: `argus = "argus.cli:app"`.
10
+
11
+ ## Ownership
12
+
13
+ This doc owns backend-wide rules and the subsystems that have no child doc:
14
+ - `core/` — runtime wiring (`RuntimeOptions`, building the watcher + server).
15
+ - `daemon/` — `argusd` background service: pidfile, process lifecycle, logging.
16
+ - `detectors/` — alert detectors (registry + individual rules like tool-error-rate spike).
17
+ - `pricing/` — pricing table load / refresh / compute; bundled JSON under repo `pricing/`.
18
+ - `scaffold/` — `argus claude` scaffolding (templates, snapshot, storage).
19
+ - `schema/` — shared pydantic types (`Session`, `Turn`, …). Changing a stored
20
+ field ripples into `store/` (columns) and `server/` (serialization).
21
+
22
+ Delegated subtrees (see their own AGENTS.md): `store/`, `collector/`, `adapters/`, `server/`.
23
+
24
+ ## Local Contracts
25
+
26
+ - **Version lives in two files** that must stay in lock-step: `__init__.py`
27
+ (`__version__`) and repo-root `pyproject.toml`. Bump both on release (root doc).
28
+ - **CLI command groups** are Typer apps wired in `cli.py`. When renaming a
29
+ user-facing command, keep the old name as a `hidden=True` alias and nudge to the
30
+ new one (precedent: `search` → `indexing`). Update help strings, the daemon
31
+ read-only API hint, and dashboard copy together.
32
+ - **Privacy stays intact:** no network calls or telemetry beyond opt-in
33
+ `argus pricing refresh`. Don't add outbound calls.
34
+
35
+ ## Work Guidance
36
+
37
+ - Follow existing module patterns; keep files focused and small.
38
+ - Python ≥ 3.11. Dependencies are deliberately minimal (fastapi, uvicorn,
39
+ pydantic, watchdog, typer, httpx) — don't add deps without cause.
40
+
41
+ ## Verification
42
+
43
+ `uv run pytest` (full suite). Subsystem tests live under `tests/<subsystem>/`.
44
+
45
+ ## Child DOX Index
46
+
47
+ - `store/AGENTS.md` — SQLite, migrations, repository; data-safety + path normalization.
48
+ - `collector/AGENTS.md` — ingest pipeline and missing-data backfill.
49
+ - `adapters/AGENTS.md` — Claude Code adapter and transcript-segment extraction.
50
+ - `server/AGENTS.md` — FastAPI routes, static serving, clean shutdown.
@@ -1,3 +1,3 @@
1
1
  """Argus — local-first analytics for Claude Code and other coding agents."""
2
2
 
3
- __version__ = "0.4.3"
3
+ __version__ = "0.4.4"
@@ -0,0 +1,44 @@
1
+ # AGENTS.md — adapters (Claude Code transcript adapter)
2
+
3
+ Parent: `python/argus/AGENTS.md`. Parses agent transcript files into typed results.
4
+
5
+ ## Purpose
6
+
7
+ `claude_code/` reads Claude Code's `.jsonl` transcripts: `adapter.py` discovers and
8
+ ingests files, `schemas.py` validates each line (`AssistantLine`, `UserLine`, …),
9
+ `extract_transcript.py` derives searchable segments.
10
+
11
+ ## Local Contracts
12
+
13
+ - **Discovery excludes sub-agent files.** `discover_session_files()` returns
14
+ top-level session files only; sub-agent transcripts are reached via
15
+ `sub_session_files_for(parent_file)`, which walks the parent's `subagents/`
16
+ directory (including the nested `subagents/workflows/<wf>/agent-*.jsonl` layout).
17
+ - **Sub-agent session id** = `<parent_session_id>/<sub_file_stem>` (e.g.
18
+ `claude_code:<parent>/agent-<hex>`). The `/` is what marks a row as a sub-agent
19
+ everywhere downstream.
20
+ - **`extract_transcript_segments`** emits `RawSegment`s with roles `assistant`,
21
+ `thinking`, `user`, and `tool_result`; each is byte-capped (`_cap_text`). The
22
+ first `user` segment of a sub-agent is its "Task given" in the dashboard.
23
+ - **Known data limitation — do not design against it:** `subagent_type` is empty
24
+ for every row, sub-agent ids are exactly one level deep, and there is no stored
25
+ workflow grouping or spawn-turn link. Don't build features that assume a nested
26
+ sub-agent tree, type pills, or a spawn→child edge; the data isn't there.
27
+ - **`ingest_file(path, offset)` is pure-ish:** it parses from a byte offset and
28
+ returns `(result, new_offset)`. It does not write to the DB — `collector/` owns
29
+ persistence and offset bookkeeping.
30
+ - **Path containment is enforced at `ClaudeCodeAdapter.ingest_file`, and that is
31
+ deliberate.** It is the one choke point every read reaches — discovery, the
32
+ watcher's fs events, and the pipeline's sub-agent walk — so a path that doesn't
33
+ `resolve()` under the adapter's root gets an empty result and a logged warning
34
+ instead of being read. Don't move this check out to the callers: it lived only
35
+ in `discover_session_files()` once, and `sub_agent_files_for()` plus the watcher
36
+ silently bypassed it into an arbitrary-file-read (bytes reached `parse_errors`,
37
+ which `GET /api/parse-errors` serves out). `sub_agent_files_for()` now *also*
38
+ takes `claude_root` and filters — required, not optional, so a new caller must
39
+ confront it. Covers symlinks and NTFS junctions alike (both resolve out).
40
+
41
+ ## Verification
42
+
43
+ `uv run pytest tests/adapters`. Real-`~/.claude/` tests are gated by
44
+ `ARGUS_REAL_CLAUDE_ROOT` and skipped otherwise.
@@ -1,19 +1,22 @@
1
1
  """The Claude Code adapter — façade that wires the package together."""
2
2
  from __future__ import annotations
3
3
 
4
+ import logging
4
5
  from pathlib import Path
5
6
  from typing import TYPE_CHECKING
6
7
 
7
8
  from ..base import AdapterIngestResult
8
9
  from ..registry import register
9
- from .discover import discover_session_files, sub_agent_files_for
10
+ from .discover import _safe_realpath_under, discover_session_files, sub_agent_files_for
10
11
  from .history_jsonl import ingest_history_file
11
- from .ingest_file import ingest_claude_code_file
12
+ from .ingest_file import _empty_result, ingest_claude_code_file
12
13
  from .model import canonicalize_claude_model
13
14
 
14
15
  if TYPE_CHECKING:
15
16
  from ...store.repository import Repository
16
17
 
18
+ logger = logging.getLogger(__name__)
19
+
17
20
 
18
21
  def _default_root() -> Path:
19
22
  return Path.home() / ".claude"
@@ -38,6 +41,14 @@ class ClaudeCodeAdapter:
38
41
  def ingest_file(
39
42
  self, path: Path, from_offset: int = 0
40
43
  ) -> tuple[AdapterIngestResult, int]:
44
+ # Containment lives here, at the one choke point every read passes
45
+ # through -- discovery, the watcher's fs events, and the sub-agent
46
+ # walk in the pipeline all land on this method. Checking here rather
47
+ # than at each caller is what stops a future call site from quietly
48
+ # reopening the arbitrary-file-read hole (see discover.py's rules).
49
+ if _safe_realpath_under(path, self._root) is None:
50
+ logger.warning("refusing to ingest a path outside the claude root: %s", path)
51
+ return _empty_result(path), from_offset
41
52
  return ingest_claude_code_file(path, from_offset)
42
53
 
43
54
  # ─── Extension-point overrides ─────────────────────────────────────
@@ -52,7 +63,7 @@ class ClaudeCodeAdapter:
52
63
  ingest_history_file(path, repo)
53
64
 
54
65
  def sub_session_files_for(self, session_file: Path) -> list[Path]:
55
- return sub_agent_files_for(session_file)
66
+ return sub_agent_files_for(session_file, self._root)
56
67
 
57
68
  def should_skip(self, path: Path) -> bool:
58
69
  # The subagents/ tree is walked as part of the parent's ingest,
@@ -66,7 +66,7 @@ def discover_session_files(claude_root: Path) -> list[Path]:
66
66
  return out
67
67
 
68
68
 
69
- def sub_agent_files_for(session_file: Path) -> list[Path]:
69
+ def sub_agent_files_for(session_file: Path, claude_root: Path) -> list[Path]:
70
70
  """Return the sub-agent transcript JSONLs for a session.
71
71
 
72
72
  Two on-disk layouts coexist under ``<session_dir>/<sid>/subagents/``:
@@ -88,12 +88,25 @@ def sub_agent_files_for(session_file: Path) -> list[Path]:
88
88
  sub = session_file.parent / sid / "subagents"
89
89
  if not sub.exists():
90
90
  return []
91
+ try:
92
+ canonical_root = claude_root.resolve(strict=True)
93
+ except (OSError, RuntimeError):
94
+ return []
91
95
  out: set[Path] = set()
92
96
  # Flat transcripts directly under subagents/ (dirs like workflows/ have
93
97
  # no .jsonl suffix, so they're skipped here).
94
98
  out.update(f for f in sub.iterdir() if f.is_file() and f.suffix == ".jsonl")
95
99
  # Nested Workflow sub-agent transcripts, any depth below subagents/.
96
100
  out.update(f for f in sub.rglob("agent-*.jsonl") if f.is_file())
101
+ # Same containment rule as discover_session_files: a symlink planted in
102
+ # subagents/ must not turn into an arbitrary file read. (rglob does not
103
+ # follow directory symlinks, but a symlinked *file* still resolves out.)
104
+ dropped = {f for f in out if _safe_realpath_under(f, canonical_root) is None}
105
+ for f in sorted(dropped):
106
+ logger.warning(
107
+ "skipping sub-agent transcript that escapes the claude root: %s", f
108
+ )
109
+ out -= dropped
97
110
 
98
111
  if not out and any(c.is_dir() for c in sub.iterdir()):
99
112
  # subagents/ has subdirectories but nothing matched — the layout
@@ -15,7 +15,11 @@ import argus.detectors # noqa: F401 — triggers @register side-effects
15
15
  from .pricing.load import load_pricing_table
16
16
  from .pricing.refresh import diff_pricing, fetch_litellm_table
17
17
  from .scaffold.scaffolder import scaffold_project
18
- from .scaffold.snapshot import snapshot_candidates, snapshot_template
18
+ from .scaffold.snapshot import (
19
+ secret_files_in,
20
+ snapshot_candidates,
21
+ snapshot_template,
22
+ )
19
23
  from .scaffold.storage import RESERVED_TEMPLATE_NAMES, list_templates, resolve_template
20
24
  from .server.app import ServerOpts, build_app, serve_blocking
21
25
  from .store.db import open_db
@@ -288,6 +292,14 @@ def claude_init(
288
292
  """Scaffold a .claude/ setup (and CLAUDE.md) into a project."""
289
293
  try:
290
294
  template_dir = resolve_template(template, data_dir)
295
+ except ValueError:
296
+ # Path-like name (e.g. ../../x) — rejected before it touches the disk.
297
+ typer.echo(
298
+ f"Invalid template name '{template}'. "
299
+ "Use a plain name: letters, digits, dot, dash, underscore.",
300
+ err=True,
301
+ )
302
+ raise typer.Exit(code=1)
291
303
  except KeyError:
292
304
  avail = ", ".join(list_templates(data_dir)) or "(none)"
293
305
  typer.echo(f"Unknown template '{template}'. Available: {avail}", err=True)
@@ -358,6 +370,17 @@ def template_create(
358
370
  typer.echo(str(e), err=True)
359
371
  raise typer.Exit(code=1)
360
372
  typer.echo(f"Created template '{name}' at {target}")
373
+ # Say what was withheld. A credential filter that drops files silently is
374
+ # its own bug — the user needs to know the template is deliberately
375
+ # incomplete, and why.
376
+ withheld = secret_files_in(project / ".claude")
377
+ if withheld:
378
+ typer.echo(
379
+ "\nLeft out of the template (looks like credentials): "
380
+ + ", ".join(withheld)
381
+ + "\nTemplates get copied into every project you scaffold, so "
382
+ "secrets are never snapshotted."
383
+ )
361
384
 
362
385
 
363
386
  @app.command()
@@ -0,0 +1,37 @@
1
+ # AGENTS.md — collector (ingest pipeline + backfill)
2
+
3
+ Parent: `python/argus/AGENTS.md`. Turns transcript files into stored rows.
4
+
5
+ ## Purpose
6
+
7
+ `pipeline.py` ingests one file tick; `first_run.py` orchestrates the first-pass
8
+ ingest (foreground recent files, background older files) and backfills derived data
9
+ after a schema/feature upgrade.
10
+
11
+ ## Local Contracts
12
+
13
+ - **Offset-driven reads.** `ingest_file` reads only bytes after the stored file
14
+ offset, upserts turns/tool_calls/segments, then recomputes the session. A
15
+ fully-read file sits at EOF and is **not** re-read on the next tick. To force a
16
+ re-extract you MUST reset its offset to 0 (`repo.set_file_offset(path, 0)`) —
17
+ re-calling `ingest_file` alone does nothing for an EOF file.
18
+ - **Sub-agents are walked via the parent.** A parent ingest discovers
19
+ `adapter.sub_session_files_for(parent)` and ingests any that **grew past their
20
+ offset**. Sub-agent session ids contain `/` (`<parent>/agent-<hex>`).
21
+ - **Segments are gated on indexing.** Parent and sub-agent segments are written
22
+ only when `repo.is_search_indexing_enabled()` (see `store/AGENTS.md`). So
23
+ enabling indexing *after* ingest requires re-reading the relevant files.
24
+ - **Backfill (`_backfill_missing_derived_data`) routes through parents.** Ids with
25
+ `/` are skipped by the processing loop ("walked via parents"). Anything needing a
26
+ sub-agent file re-read must add the **parent** id to both the work set and
27
+ `deep_reset` (deep_reset zeroes the sub-agent file offsets). Missing-segment
28
+ sessions therefore map to their parent + deep_reset — otherwise sub-agent
29
+ segments never backfill and the dashboard's "Task given" stays empty.
30
+ - **Bounded per run.** Backfill candidates are capped (200) per `argus start`;
31
+ large installs may need several restarts to converge. If you add a new bounded
32
+ sweep, surface what was deferred rather than silently truncating.
33
+
34
+ ## Verification
35
+
36
+ `uv run pytest tests/collector` — covers incremental re-ingest, sub-agent
37
+ backfill, segment gating, and the "enable indexing late" path.
@@ -20,6 +20,12 @@ from . import pidfile
20
20
  def daemon_argv(data_dir: Path) -> list[str]:
21
21
  return [
22
22
  sys.executable,
23
+ # -P (3.11+) keeps the *caller's* cwd off the child's sys.path. Without
24
+ # it, `-m` prepends cwd, so running `argus daemon start` from inside a
25
+ # checkout that ships its own `argus/` directory would run that code
26
+ # instead of the installed package -- arbitrary execution in a detached
27
+ # process that outlives the shell.
28
+ "-P",
23
29
  "-m",
24
30
  "argus.cli",
25
31
  "daemon",
@@ -1,6 +1,7 @@
1
1
  """Copy a template directory into a project — the `argus claude init` core."""
2
2
  from __future__ import annotations
3
3
 
4
+ import os
4
5
  import shutil
5
6
  from dataclasses import dataclass, field
6
7
  from pathlib import Path
@@ -9,6 +10,25 @@ from pathlib import Path
9
10
  _CLAUDE_MD = "CLAUDE.md"
10
11
 
11
12
 
13
+ def _resolves_inside(target: Path, root: Path) -> bool:
14
+ """True if ``target`` canonicalises to ``root`` or below it.
15
+
16
+ ``resolve()`` follows every symlink and NTFS junction on the path —
17
+ including the final component, and including a *dangling* link — which is
18
+ exactly what we need. ``shutil.copyfile`` writes *through* a symlink, and
19
+ the caller's ``target.exists()`` guard follows one too (so a dangling link
20
+ reports "doesn't exist" and then gets created on write). Comparing
21
+ canonical paths is the only check that catches all of those.
22
+ """
23
+ try:
24
+ resolved = target.resolve()
25
+ except (OSError, RuntimeError):
26
+ return False
27
+ a = os.path.normcase(str(resolved))
28
+ b = os.path.normcase(str(root))
29
+ return a == b or a.startswith(b + os.sep)
30
+
31
+
12
32
  @dataclass
13
33
  class ScaffoldResult:
14
34
  created: list[Path] = field(default_factory=list)
@@ -26,11 +46,20 @@ def scaffold_project(
26
46
  - ``CLAUDE.md`` is NEVER overwritten, even with ``force``.
27
47
  """
28
48
  result = ScaffoldResult()
49
+ dest_root = dest.resolve()
29
50
  for src in sorted(p for p in template_dir.rglob("*") if p.is_file()):
30
51
  rel = src.relative_to(template_dir)
31
52
  target = dest / rel
32
53
  is_claude_md = rel.as_posix() == _CLAUDE_MD
33
54
 
55
+ # Never write outside the destination. A hostile repo can ship
56
+ # `.claude/` as a symlink/junction, or `.claude/settings.json` as a
57
+ # link to ~/.ssh/authorized_keys; cloning it and running
58
+ # `argus claude init` would otherwise write through that link.
59
+ if not _resolves_inside(target, dest_root):
60
+ result.skipped.append((target, "would write outside the project"))
61
+ continue
62
+
34
63
  if target.exists():
35
64
  if is_claude_md:
36
65
  result.skipped.append((target, "CLAUDE.md exists, left untouched"))