arkaos 4.22.0 → 4.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/THE-ARKAOS-GUIDE.md +72 -0
  2. package/VERSION +1 -1
  3. package/arka/SKILL.md +28 -6
  4. package/arka/skills/bootstrap-agent/SKILL.md +3 -2
  5. package/arka/skills/flow/SKILL.md +3 -2
  6. package/arka/skills/forge/SKILL.md +3 -2
  7. package/arka/skills/recipes/SKILL.md +3 -2
  8. package/arka/skills/refine/SKILL.md +3 -2
  9. package/arka/skills/research/SKILL.md +3 -2
  10. package/bin/arka-doctor +8 -0
  11. package/core/cognition/insights/store.py +15 -4
  12. package/core/forge/persistence.py +27 -9
  13. package/core/forge/schema.py +45 -22
  14. package/core/governance/evidence_checks.py +101 -9
  15. package/core/governance/kb_cite_check.py +16 -6
  16. package/core/governance/stop_lint.py +38 -11
  17. package/core/governance/stop_lint_telemetry.py +11 -2
  18. package/core/hooks/user_prompt_submit.py +21 -23
  19. package/core/synapse/engine.py +46 -16
  20. package/core/synapse/graph_context_layer.py +7 -2
  21. package/core/synapse/kb_cache.py +63 -16
  22. package/core/synapse/layers.py +93 -30
  23. package/core/synapse/layers_base.py +16 -0
  24. package/core/synapse/layers_kb.py +325 -19
  25. package/dashboard/app/layouts/default.vue +7 -0
  26. package/dashboard/app/pages/plan-canvas.vue +630 -0
  27. package/departments/brand/SKILL.md +3 -2
  28. package/departments/brand/skills/archetype-finder/SKILL.md +3 -2
  29. package/departments/brand/skills/colors/SKILL.md +3 -2
  30. package/departments/brand/skills/design-system/SKILL.md +3 -2
  31. package/departments/brand/skills/identity-system/SKILL.md +3 -2
  32. package/departments/brand/skills/logo-brief/SKILL.md +3 -2
  33. package/departments/brand/skills/mockup-generate/SKILL.md +3 -2
  34. package/departments/brand/skills/naming-evaluate/SKILL.md +3 -2
  35. package/departments/brand/skills/positioning-statement/SKILL.md +3 -2
  36. package/departments/brand/skills/primal-audit/SKILL.md +3 -2
  37. package/departments/brand/skills/ux-audit/SKILL.md +3 -2
  38. package/departments/brand/skills/voice-guide/SKILL.md +3 -2
  39. package/departments/brand/skills/wireframe/SKILL.md +3 -2
  40. package/departments/community/SKILL.md +3 -2
  41. package/departments/community/skills/ai-community/SKILL.md +3 -2
  42. package/departments/community/skills/betting-setup/SKILL.md +3 -2
  43. package/departments/community/skills/business-model/SKILL.md +3 -2
  44. package/departments/community/skills/content-calendar/SKILL.md +3 -2
  45. package/departments/community/skills/events-plan/SKILL.md +3 -2
  46. package/departments/community/skills/gamification-design/SKILL.md +3 -2
  47. package/departments/community/skills/growth-plan/SKILL.md +3 -2
  48. package/departments/community/skills/metrics-track/SKILL.md +3 -2
  49. package/departments/community/skills/moderation/SKILL.md +3 -2
  50. package/departments/community/skills/monetize-plan/SKILL.md +3 -2
  51. package/departments/community/skills/niche-setup/SKILL.md +3 -2
  52. package/departments/community/skills/onboarding-flow/SKILL.md +3 -2
  53. package/departments/community/skills/platform-select/SKILL.md +3 -2
  54. package/departments/content/SKILL.md +3 -2
  55. package/departments/content/skills/ai-workflow/SKILL.md +3 -2
  56. package/departments/content/skills/analytics/SKILL.md +3 -2
  57. package/departments/content/skills/calendar/SKILL.md +3 -2
  58. package/departments/content/skills/content-system/SKILL.md +3 -2
  59. package/departments/content/skills/monetization-plan/SKILL.md +3 -2
  60. package/departments/content/skills/newsletter-write/SKILL.md +3 -2
  61. package/departments/content/skills/platform-optimize/SKILL.md +3 -2
  62. package/departments/content/skills/repurpose-plan/SKILL.md +3 -2
  63. package/departments/content/skills/research-compile/SKILL.md +3 -2
  64. package/departments/content/skills/script-structure/SKILL.md +3 -2
  65. package/departments/content/skills/short-form/SKILL.md +3 -2
  66. package/departments/content/skills/thumbnail-package/SKILL.md +3 -2
  67. package/departments/content/skills/trend-hunt/SKILL.md +3 -2
  68. package/departments/content/skills/video-produce/SKILL.md +3 -2
  69. package/departments/content/skills/video-setup/SKILL.md +3 -2
  70. package/departments/content/skills/viral-design/SKILL.md +3 -2
  71. package/departments/content/skills/youtube-strategy/SKILL.md +3 -2
  72. package/departments/dev/SKILL.md +3 -2
  73. package/departments/dev/skills/ai-assisted-dev/SKILL.md +3 -2
  74. package/departments/dev/skills/animated-website/SKILL.md +7 -0
  75. package/departments/dev/skills/architecture-design/SKILL.md +3 -2
  76. package/departments/dev/skills/code-review/SKILL.md +3 -2
  77. package/departments/dev/skills/db-design/SKILL.md +3 -2
  78. package/departments/dev/skills/ddd-model/SKILL.md +3 -2
  79. package/departments/dev/skills/demo-gif/SKILL.md +3 -2
  80. package/departments/dev/skills/deploy/SKILL.md +3 -2
  81. package/departments/dev/skills/devops-pipeline/SKILL.md +3 -2
  82. package/departments/dev/skills/docs/SKILL.md +3 -2
  83. package/departments/dev/skills/mcp/SKILL.md +3 -2
  84. package/departments/dev/skills/performance-audit/SKILL.md +3 -2
  85. package/departments/dev/skills/refactor-plan/SKILL.md +3 -2
  86. package/departments/dev/skills/research/SKILL.md +3 -2
  87. package/departments/dev/skills/security-compliance/SKILL.md +3 -2
  88. package/departments/dev/skills/stack-check/SKILL.md +3 -2
  89. package/departments/ecom/SKILL.md +3 -2
  90. package/departments/ecom/skills/analytics/SKILL.md +3 -2
  91. package/departments/ecom/skills/browse-competitor/SKILL.md +3 -2
  92. package/departments/ecom/skills/cart-recovery/SKILL.md +3 -2
  93. package/departments/ecom/skills/cro-optimize/SKILL.md +3 -2
  94. package/departments/ecom/skills/customer-journey/SKILL.md +3 -2
  95. package/departments/ecom/skills/fulfillment-plan/SKILL.md +3 -2
  96. package/departments/ecom/skills/marketplace-manage/SKILL.md +3 -2
  97. package/departments/ecom/skills/pricing-strategy/SKILL.md +3 -2
  98. package/departments/ecom/skills/product-launch/SKILL.md +3 -2
  99. package/departments/ecom/skills/rfm-segment/SKILL.md +3 -2
  100. package/departments/ecom/skills/social-commerce/SKILL.md +3 -2
  101. package/departments/ecom/skills/store-audit/SKILL.md +3 -2
  102. package/departments/ecom/skills/subscription-model/SKILL.md +3 -2
  103. package/departments/finance/SKILL.md +3 -2
  104. package/departments/finance/skills/budget-plan/SKILL.md +3 -2
  105. package/departments/finance/skills/cashflow-forecast/SKILL.md +3 -2
  106. package/departments/finance/skills/ciso-advisor/SKILL.md +3 -2
  107. package/departments/finance/skills/financial-model/SKILL.md +3 -2
  108. package/departments/finance/skills/pitch-deck/SKILL.md +3 -2
  109. package/departments/finance/skills/scenario-analysis/SKILL.md +3 -2
  110. package/departments/finance/skills/unit-economics/SKILL.md +3 -2
  111. package/departments/finance/skills/valuation-model/SKILL.md +3 -2
  112. package/departments/kb/SKILL.md +3 -2
  113. package/departments/kb/skills/ai-research/SKILL.md +3 -2
  114. package/departments/kb/skills/competitive-intel/SKILL.md +3 -2
  115. package/departments/kb/skills/doc-extraction/SKILL.md +3 -2
  116. package/departments/kb/skills/doc-redaction/SKILL.md +3 -2
  117. package/departments/kb/skills/knowledge-ops/SKILL.md +3 -2
  118. package/departments/kb/skills/knowledge-review/SKILL.md +3 -2
  119. package/departments/kb/skills/learn-content/SKILL.md +3 -2
  120. package/departments/kb/skills/moc-create/SKILL.md +3 -2
  121. package/departments/kb/skills/persona-build/SKILL.md +3 -2
  122. package/departments/kb/skills/research-plan/SKILL.md +3 -2
  123. package/departments/kb/skills/search-kb/SKILL.md +3 -2
  124. package/departments/kb/skills/source-evaluate/SKILL.md +3 -2
  125. package/departments/kb/skills/taxonomy-manage/SKILL.md +3 -2
  126. package/departments/kb/skills/write-as-persona/SKILL.md +3 -2
  127. package/departments/landing/SKILL.md +3 -2
  128. package/departments/landing/skills/ab-test/SKILL.md +3 -2
  129. package/departments/landing/skills/affiliate-bridge/SKILL.md +3 -2
  130. package/departments/landing/skills/awareness-diagnose/SKILL.md +3 -2
  131. package/departments/landing/skills/email-sequence/SKILL.md +3 -2
  132. package/departments/landing/skills/funnel-metrics/SKILL.md +3 -2
  133. package/departments/landing/skills/headline-write/SKILL.md +3 -2
  134. package/departments/landing/skills/launch-sequence/SKILL.md +3 -2
  135. package/departments/landing/skills/offer-create/SKILL.md +3 -2
  136. package/departments/landing/skills/optimize-page/SKILL.md +3 -2
  137. package/departments/landing/skills/page-architect/SKILL.md +3 -2
  138. package/departments/landing/skills/persuasion-apply/SKILL.md +3 -2
  139. package/departments/landing/skills/webinar-funnel/SKILL.md +3 -2
  140. package/departments/leadership/SKILL.md +3 -2
  141. package/departments/leadership/skills/change-manage/SKILL.md +3 -2
  142. package/departments/leadership/skills/conflict-resolve/SKILL.md +3 -2
  143. package/departments/leadership/skills/culture-audit/SKILL.md +3 -2
  144. package/departments/leadership/skills/delegation-matrix/SKILL.md +3 -2
  145. package/departments/leadership/skills/disc-assess/SKILL.md +3 -2
  146. package/departments/leadership/skills/feedback-give/SKILL.md +3 -2
  147. package/departments/leadership/skills/hiring-plan/SKILL.md +3 -2
  148. package/departments/leadership/skills/performance-review/SKILL.md +3 -2
  149. package/departments/marketing/SKILL.md +3 -2
  150. package/departments/marketing/skills/ab-test/SKILL.md +3 -2
  151. package/departments/marketing/skills/analytics-report/SKILL.md +3 -2
  152. package/departments/marketing/skills/audience-segment/SKILL.md +3 -2
  153. package/departments/marketing/skills/calendar-plan/SKILL.md +3 -2
  154. package/departments/marketing/skills/competitor-analysis/SKILL.md +3 -2
  155. package/departments/marketing/skills/content-audit/SKILL.md +3 -2
  156. package/departments/marketing/skills/email-sequence/SKILL.md +3 -2
  157. package/departments/marketing/skills/growth-loop/SKILL.md +3 -2
  158. package/departments/marketing/skills/marketing-automation/SKILL.md +3 -2
  159. package/departments/marketing/skills/paid-campaign/SKILL.md +3 -2
  160. package/departments/marketing/skills/programmatic-seo/SKILL.md +3 -2
  161. package/departments/marketing/skills/seo-audit/SKILL.md +3 -2
  162. package/departments/marketing/skills/social-strategy/SKILL.md +3 -2
  163. package/departments/ops/SKILL.md +3 -2
  164. package/departments/ops/skills/bottleneck-find/SKILL.md +3 -2
  165. package/departments/ops/skills/dashboard-build/SKILL.md +3 -2
  166. package/departments/ops/skills/gdpr-compliance/SKILL.md +3 -2
  167. package/departments/ops/skills/github-ops/SKILL.md +3 -2
  168. package/departments/ops/skills/gtd-setup/SKILL.md +3 -2
  169. package/departments/ops/skills/integration-design/SKILL.md +3 -2
  170. package/departments/ops/skills/iso27001/SKILL.md +3 -2
  171. package/departments/ops/skills/lean-audit/SKILL.md +3 -2
  172. package/departments/ops/skills/metrics-dashboard/SKILL.md +3 -2
  173. package/departments/ops/skills/n8n-flow/SKILL.md +3 -2
  174. package/departments/ops/skills/quality-management/SKILL.md +3 -2
  175. package/departments/ops/skills/risk-management/SKILL.md +3 -2
  176. package/departments/ops/skills/soc2-compliance/SKILL.md +3 -2
  177. package/departments/ops/skills/sop-create/SKILL.md +3 -2
  178. package/departments/ops/skills/terminal-ops/SKILL.md +3 -2
  179. package/departments/ops/skills/workflow-automate/SKILL.md +3 -2
  180. package/departments/ops/skills/zapier-flow/SKILL.md +3 -2
  181. package/departments/org/SKILL.md +3 -2
  182. package/departments/org/skills/compensation-plan/SKILL.md +3 -2
  183. package/departments/org/skills/culture-define/SKILL.md +3 -2
  184. package/departments/org/skills/decision-framework/SKILL.md +3 -2
  185. package/departments/org/skills/hiring-plan/SKILL.md +3 -2
  186. package/departments/org/skills/meeting-optimize/SKILL.md +3 -2
  187. package/departments/org/skills/onboarding-design/SKILL.md +3 -2
  188. package/departments/org/skills/org-design/SKILL.md +3 -2
  189. package/departments/org/skills/remote-setup/SKILL.md +3 -2
  190. package/departments/org/skills/sop-process/SKILL.md +3 -2
  191. package/departments/org/skills/team-assess/SKILL.md +3 -2
  192. package/departments/pm/SKILL.md +3 -2
  193. package/departments/pm/skills/backlog-groom/SKILL.md +3 -2
  194. package/departments/pm/skills/discovery-plan/SKILL.md +3 -2
  195. package/departments/pm/skills/estimate-forecast/SKILL.md +3 -2
  196. package/departments/pm/skills/impact-map/SKILL.md +3 -2
  197. package/departments/pm/skills/kanban-setup/SKILL.md +3 -2
  198. package/departments/pm/skills/risk-register/SKILL.md +3 -2
  199. package/departments/pm/skills/roadmap-build/SKILL.md +3 -2
  200. package/departments/pm/skills/sprint-plan/SKILL.md +3 -2
  201. package/departments/pm/skills/stakeholder-map/SKILL.md +3 -2
  202. package/departments/pm/skills/standup-run/SKILL.md +3 -2
  203. package/departments/pm/skills/story-write/SKILL.md +3 -2
  204. package/departments/saas/SKILL.md +3 -2
  205. package/departments/saas/skills/benchmark-compare/SKILL.md +3 -2
  206. package/departments/saas/skills/churn-analysis/SKILL.md +3 -2
  207. package/departments/saas/skills/customer-success/SKILL.md +3 -2
  208. package/departments/saas/skills/growth-plan/SKILL.md +3 -2
  209. package/departments/saas/skills/gtm-strategy/SKILL.md +3 -2
  210. package/departments/saas/skills/launch-execute/SKILL.md +3 -2
  211. package/departments/saas/skills/metrics-dashboard/SKILL.md +3 -2
  212. package/departments/saas/skills/micro-saas-stack/SKILL.md +3 -2
  213. package/departments/saas/skills/mvp-build/SKILL.md +3 -2
  214. package/departments/saas/skills/niche-evaluate/SKILL.md +3 -2
  215. package/departments/saas/skills/onboarding-optimize/SKILL.md +3 -2
  216. package/departments/saas/skills/plg-setup/SKILL.md +3 -2
  217. package/departments/saas/skills/pricing-strategy/SKILL.md +3 -2
  218. package/departments/saas/skills/validate-idea/SKILL.md +3 -2
  219. package/departments/sales/SKILL.md +3 -2
  220. package/departments/sales/skills/challenger-sell/SKILL.md +3 -2
  221. package/departments/sales/skills/deal-qualify/SKILL.md +3 -2
  222. package/departments/sales/skills/discovery-call/SKILL.md +3 -2
  223. package/departments/sales/skills/forecast-revenue/SKILL.md +3 -2
  224. package/departments/sales/skills/negotiate-plan/SKILL.md +3 -2
  225. package/departments/sales/skills/objection-handle/SKILL.md +3 -2
  226. package/departments/sales/skills/pipeline-manage/SKILL.md +3 -2
  227. package/departments/sales/skills/pricing-negotiate/SKILL.md +3 -2
  228. package/departments/strategy/SKILL.md +3 -2
  229. package/departments/strategy/skills/blue-ocean/SKILL.md +3 -2
  230. package/departments/strategy/skills/bmc/SKILL.md +3 -2
  231. package/departments/strategy/skills/board-advisor/SKILL.md +3 -2
  232. package/departments/strategy/skills/cto-advisor/SKILL.md +3 -2
  233. package/departments/strategy/skills/extract-data/SKILL.md +3 -2
  234. package/departments/strategy/skills/five-forces/SKILL.md +3 -2
  235. package/departments/strategy/skills/growth-strategy/SKILL.md +3 -2
  236. package/departments/strategy/skills/moat-analysis/SKILL.md +3 -2
  237. package/departments/strategy/skills/position/SKILL.md +3 -2
  238. package/departments/strategy/skills/scenario-plan/SKILL.md +3 -2
  239. package/installer/config-seed.js +81 -25
  240. package/installer/graphify.js +340 -0
  241. package/installer/index.js +8 -1
  242. package/installer/keys.js +18 -3
  243. package/installer/update.js +8 -1
  244. package/knowledge/skills-manifest.json +1 -1
  245. package/mcps/registry.json +9 -9
  246. package/mcps/scripts/apply-mcps.sh +6 -2
  247. package/package.json +2 -1
  248. package/pyproject.toml +1 -1
  249. package/scripts/dashboard-api.py +336 -124
  250. package/scripts/guide_gen.py +139 -0
  251. package/scripts/migrate_skills_kb_first.py +3 -2
  252. package/scripts/synapse-bridge.py +9 -1
@@ -0,0 +1,72 @@
1
+ # The ArkaOS Guide
2
+
3
+ > v4.23.0 — 86 agents, 17 departments, 292 skills, 270 commands, 16 ADRs.
4
+ > One file, everything you need to start. Generated by `scripts/guide_gen.py` — never hand-edited.
5
+
6
+ ## What it is
7
+
8
+ ArkaOS is an operating system for AI agent teams: specialized
9
+ departments with framework-backed agents, a constitution with a
10
+ Quality Gate that holds veto power, and an evidence flow where
11
+ gates pass on tool output read from disk — never on narration.
12
+
13
+ ## Start here
14
+
15
+ ```bash
16
+ npx arkaos install # detects your runtime, installs the core
17
+ npx arkaos doctor # canonical health check (venv, hooks,
18
+ # fastpath, security advisory)
19
+ ```
20
+
21
+ Then inside your runtime: `/arka setup` (profile) and `/do <what
22
+ you need>` — plain language routes to the right department.
23
+
24
+ ## Keep it updated (two steps)
25
+
26
+ ```bash
27
+ npx arkaos@latest update # step 1: core, hooks, CLI
28
+ ```
29
+
30
+ Then `/arka update` inside the runtime — step 2 syncs every
31
+ project (skills, MCP configs, settings, stack rules).
32
+
33
+ ## Departments
34
+
35
+ Every request routes through a department squad — ArkaOS never
36
+ answers as a generic assistant.
37
+
38
+ | Prefix | Commands |
39
+ |---|---|
40
+ | `/arka` | 22 |
41
+ | `/brand` | 12 |
42
+ | `/community` | 14 |
43
+ | `/content` | 19 |
44
+ | `/dev` | 36 |
45
+ | `/do` | 1 |
46
+ | `/ecom` | 18 |
47
+ | `/fin` | 10 |
48
+ | `/kb` | 22 |
49
+ | `/landing` | 14 |
50
+ | `/lead` | 10 |
51
+ | `/mkt` | 12 |
52
+ | `/ops` | 24 |
53
+ | `/org` | 10 |
54
+ | `/pm` | 12 |
55
+ | `/saas` | 14 |
56
+ | `/sales` | 10 |
57
+ | `/strat` | 10 |
58
+
59
+ ## How work ships
60
+
61
+ 1. **G1 CONTEXT** — routing + grounding before anything runs.
62
+ 2. **G2 PLAN** — explicit approval before code.
63
+ 3. **G3 EXECUTE** — real test runs with exit codes on record.
64
+ 4. **G4 REVIEW** — the Quality Gate (CQO + copy + tech reviewers)
65
+ issues a binary APPROVED/REJECTED; failing evidence can never
66
+ be overridden by narrative.
67
+
68
+ ## Go deeper
69
+
70
+ - Wiki: https://github.com/andreagroferreira/arka-os/wiki
71
+ - npm: https://www.npmjs.com/package/arkaos
72
+ - Dashboard: `npx arkaos dashboard` (localhost:3333)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.22.0
1
+ 4.23.0
package/arka/SKILL.md CHANGED
@@ -7,20 +7,42 @@ description: >
7
7
  allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, Agent, WebFetch, WebSearch]
8
8
  ---
9
9
 
10
- ## KB-First Research (non-negotiable)
10
+ ## KB-First Research (`always-research` SHOULD)
11
11
 
12
12
  Canonical home of the doctrine — every skill's compact
13
- `arka:kb-first-prefix` pointer references this section. Before any
13
+ `arka:kb-first-prefix` pointer references this section. The Knowledge Base
14
+ has two co-equal faces over the same corpus: **Obsidian** (note content)
15
+ and **Graphify** (the knowledge graph — how concepts connect). Before any
14
16
  external research (Context7, WebSearch, WebFetch, Firecrawl):
15
17
 
16
18
  1. Call `mcp__obsidian__search_notes` on the query first.
17
- 2. Cite relevant hits with `[[wikilinks]]` or explicitly declare a KB gap.
18
- 3. Only after (1) and (2) may external tools run.
19
+ 2. When Graphify is configured, also call `mcp__graphify__query_graph`
20
+ (or `god_nodes` / `shortest_path`) for the relationship view.
21
+ 3. Cite relevant hits with `[[wikilinks]]` and/or graph nodes, or
22
+ explicitly declare a KB gap.
23
+ 4. Only after (1)–(3) may external tools run.
24
+
25
+ **Fail-open:** Graphify runs on a per-user endpoint (home LAN, localhost,
26
+ or VPS — see `knowledge.graphify` config). When it is not configured or
27
+ not reachable, treat it as a gap and proceed with Obsidian alone; never
28
+ block a turn waiting on it. Obsidian remains the mandatory first step.
19
29
 
20
30
  The Synapse L2.5 layer pre-injects top KB matches on every user prompt;
21
31
  treat them as your default source. External research supplements the vault;
22
- it does not replace it. Enforcement: the Stop hook's kb-cite check
23
- measures citation compliance per turn.
32
+ it does not replace it.
33
+
34
+ **Enforcement** runs on three surfaces, in the order they fire:
35
+
36
+ | Surface | Effect |
37
+ | --- | --- |
38
+ | PreToolUse gate (`core/workflow/research_gate.py`) | DENIES the tool call in the current turn on a repeat violation |
39
+ | Quality Gate | DOD item `kb-research-cited` (`definition_of_done.universal`) is `hard: true` — a failed hard item blocks the done marker (rule `definition-of-done-per-domain`) |
40
+ | Stop hook (`core/governance/kb_cite_check.py`) | SOFT — measures citation compliance and nudges the next turn, never blocks this one |
41
+
42
+ Governing rule: `always-research` (SHOULD, `config/constitution.yaml`).
43
+ `kb-research-cited` is a Definition-of-Done item (`hard: true`), not an
44
+ entry in the must/should/non-negotiable rule lists — the two are
45
+ complementary, not alternatives.
24
46
 
25
47
  # ArkaOS — Main Orchestrator
26
48
 
@@ -16,8 +16,9 @@ allowed-tools: [Agent, Read, Write, Bash]
16
16
  ---
17
17
 
18
18
  <!-- arka:kb-first-prefix begin -->
19
- > **KB-first:** query `mcp__obsidian__search_notes` and cite
20
- > `[[wikilinks]]` — or declare the gap — BEFORE any external research.
19
+ > **KB-first:** query `mcp__obsidian__search_notes` (and
20
+ > `mcp__graphify__query_graph` when configured) and cite `[[wikilinks]]`
21
+ > or graph nodes — or declare the gap — BEFORE any external research.
21
22
  > Full doctrine: `arka/SKILL.md` (KB-First Research).
22
23
  <!-- arka:kb-first-prefix end -->
23
24
 
@@ -18,8 +18,9 @@ allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, Agent, WebFetch, WebSearch]
18
18
  ---
19
19
 
20
20
  <!-- arka:kb-first-prefix begin -->
21
- > **KB-first:** query `mcp__obsidian__search_notes` and cite
22
- > `[[wikilinks]]` — or declare the gap — BEFORE any external research.
21
+ > **KB-first:** query `mcp__obsidian__search_notes` (and
22
+ > `mcp__graphify__query_graph` when configured) and cite `[[wikilinks]]`
23
+ > or graph nodes — or declare the gap — BEFORE any external research.
23
24
  > Full doctrine: `arka/SKILL.md` (KB-First Research).
24
25
  <!-- arka:kb-first-prefix end -->
25
26
 
@@ -19,8 +19,9 @@ allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, Agent, WebFetch, WebSearch]
19
19
  ---
20
20
 
21
21
  <!-- arka:kb-first-prefix begin -->
22
- > **KB-first:** query `mcp__obsidian__search_notes` and cite
23
- > `[[wikilinks]]` — or declare the gap — BEFORE any external research.
22
+ > **KB-first:** query `mcp__obsidian__search_notes` (and
23
+ > `mcp__graphify__query_graph` when configured) and cite `[[wikilinks]]`
24
+ > or graph nodes — or declare the gap — BEFORE any external research.
24
25
  > Full doctrine: `arka/SKILL.md` (KB-First Research).
25
26
  <!-- arka:kb-first-prefix end -->
26
27
 
@@ -14,8 +14,9 @@ allowed-tools: [Read, Bash]
14
14
  ---
15
15
 
16
16
  <!-- arka:kb-first-prefix begin -->
17
- > **KB-first:** query `mcp__obsidian__search_notes` and cite
18
- > `[[wikilinks]]` — or declare the gap — BEFORE any external research.
17
+ > **KB-first:** query `mcp__obsidian__search_notes` (and
18
+ > `mcp__graphify__query_graph` when configured) and cite `[[wikilinks]]`
19
+ > or graph nodes — or declare the gap — BEFORE any external research.
19
20
  > Full doctrine: `arka/SKILL.md` (KB-First Research).
20
21
  <!-- arka:kb-first-prefix end -->
21
22
 
@@ -15,8 +15,9 @@ allowed-tools: [Read, AskUserQuestion, Skill]
15
15
  ---
16
16
 
17
17
  <!-- arka:kb-first-prefix begin -->
18
- > **KB-first:** query `mcp__obsidian__search_notes` and cite
19
- > `[[wikilinks]]` — or declare the gap — BEFORE any external research.
18
+ > **KB-first:** query `mcp__obsidian__search_notes` (and
19
+ > `mcp__graphify__query_graph` when configured) and cite `[[wikilinks]]`
20
+ > or graph nodes — or declare the gap — BEFORE any external research.
20
21
  > Full doctrine: `arka/SKILL.md` (KB-First Research).
21
22
  <!-- arka:kb-first-prefix end -->
22
23
 
@@ -17,8 +17,9 @@ allowed-tools: [Agent, Read, Write, mcp__obsidian__search_notes]
17
17
  ---
18
18
 
19
19
  <!-- arka:kb-first-prefix begin -->
20
- > **KB-first:** query `mcp__obsidian__search_notes` and cite
21
- > `[[wikilinks]]` — or declare the gap — BEFORE any external research.
20
+ > **KB-first:** query `mcp__obsidian__search_notes` (and
21
+ > `mcp__graphify__query_graph` when configured) and cite `[[wikilinks]]`
22
+ > or graph nodes — or declare the gap — BEFORE any external research.
22
23
  > Full doctrine: `arka/SKILL.md` (KB-First Research).
23
24
  <!-- arka:kb-first-prefix end -->
24
25
 
package/bin/arka-doctor CHANGED
@@ -3,6 +3,11 @@
3
3
  # ARKA OS — Doctor (Health Check System)
4
4
  # Usage: arka doctor [--fix] [--json]
5
5
  # 18 modular checks: fail = critical, warn = advisory
6
+ #
7
+ # SCOPE: this doctor audits the legacy v1 layout (~/.arka-os +
8
+ # ~/.claude/skills). The CANONICAL doctor for the engine (venv, hooks,
9
+ # fastpath, security advisory) is `npx arkaos doctor` — see issue #358
10
+ # for the consolidation/migration map.
6
11
  # ============================================================================
7
12
  set -euo pipefail
8
13
 
@@ -421,4 +426,7 @@ else
421
426
  echo -e " ${G}All checks passed!${N} ARKA OS is healthy."
422
427
  fi
423
428
  echo ""
429
+ echo -e " ${C}Note:${N} this audits the legacy v1 layout (~/.arka-os + ~/.claude/skills)."
430
+ echo -e " For the engine (venv, hooks, fastpath, security advisory) run: ${C}npx arkaos doctor${N}"
431
+ echo ""
424
432
  fi
@@ -4,7 +4,9 @@ Persists ActionableInsight instances with support for status-based retrieval,
4
4
  project filtering, presentation lifecycle, and dismissal analytics.
5
5
  """
6
6
 
7
+ import contextlib
7
8
  import sqlite3
9
+ from collections.abc import Iterator
8
10
  from datetime import UTC, datetime
9
11
  from pathlib import Path
10
12
 
@@ -24,11 +26,20 @@ class InsightStore:
24
26
  self._db_path = db_path
25
27
  self._init_db()
26
28
 
27
- def _conn(self) -> sqlite3.Connection:
29
+ @contextlib.contextmanager
30
+ def _conn(self) -> Iterator[sqlite3.Connection]:
31
+ """Per-operation connection: transaction semantics AND a real
32
+ close. ``with sqlite3.connect(...)`` alone only commits/rolls
33
+ back — it never closes, leaking one connection per operation
34
+ (ResourceWarning under coverage, amplified by ingest loops)."""
28
35
  conn = sqlite3.connect(self._db_path)
29
- conn.row_factory = sqlite3.Row
30
- conn.execute("PRAGMA journal_mode=WAL")
31
- return conn
36
+ try:
37
+ conn.row_factory = sqlite3.Row
38
+ conn.execute("PRAGMA journal_mode=WAL")
39
+ with conn:
40
+ yield conn
41
+ finally:
42
+ conn.close()
32
43
 
33
44
  # Instinct columns added after the original 12-column schema. An
34
45
  # operator's existing DB has the old shape, so these are ALTER-added
@@ -4,8 +4,9 @@ import os
4
4
  import re as _re
5
5
  from pathlib import Path
6
6
  from tempfile import NamedTemporaryFile
7
- from typing import Optional
7
+
8
8
  import yaml
9
+
9
10
  from core.forge.schema import ForgePlan, ForgeStatus
10
11
 
11
12
 
@@ -23,7 +24,9 @@ def save_plan(plan: ForgePlan) -> Path:
23
24
  plans.mkdir(parents=True, exist_ok=True)
24
25
  target = plans / f"{plan.id}.yaml"
25
26
  data = plan.model_dump(mode="json")
26
- fd = NamedTemporaryFile(mode="w", dir=str(plans), suffix=".tmp", delete=False, encoding="utf-8")
27
+ fd = NamedTemporaryFile( # noqa: SIM115 — handle outlives the block: os.replace consumes fd.name
28
+ mode="w", dir=str(plans), suffix=".tmp", delete=False, encoding="utf-8"
29
+ )
27
30
  try:
28
31
  yaml.dump(data, fd, default_flow_style=False, allow_unicode=True)
29
32
  fd.close()
@@ -35,7 +38,7 @@ def save_plan(plan: ForgePlan) -> Path:
35
38
  return target
36
39
 
37
40
 
38
- def load_plan(plan_id: str) -> Optional[ForgePlan]:
41
+ def load_plan(plan_id: str) -> ForgePlan | None:
39
42
  """Load a forge plan by ID. Returns None if not found."""
40
43
  path = _plans_dir() / f"{plan_id}.yaml"
41
44
  if not path.exists():
@@ -54,12 +57,20 @@ def list_plans() -> list[dict]:
54
57
  if path.name == "active.yaml":
55
58
  continue
56
59
  data = yaml.safe_load(path.read_text(encoding="utf-8"))
60
+ if not isinstance(data, dict):
61
+ continue
62
+ # Pre-v4 plans stored complexity/critic as scalars — a summary
63
+ # must tolerate every shape that ever hit disk.
64
+ complexity = data.get("complexity")
65
+ critic = data.get("critic")
57
66
  results.append({
58
67
  "id": data.get("id", path.stem),
59
68
  "name": data.get("name", ""),
60
69
  "status": data.get("status", "draft"),
61
- "tier": data.get("complexity", {}).get("tier", "shallow"),
62
- "confidence": data.get("critic", {}).get("confidence", 0.0),
70
+ "tier": complexity.get("tier", "shallow")
71
+ if isinstance(complexity, dict) else "shallow",
72
+ "confidence": critic.get("confidence", 0.0)
73
+ if isinstance(critic, dict) else 0.0,
63
74
  "created_at": data.get("created_at", ""),
64
75
  })
65
76
  return results
@@ -72,7 +83,7 @@ def set_active_plan(plan_id: str) -> None:
72
83
  link.write_text(plan_id, encoding="utf-8")
73
84
 
74
85
 
75
- def get_active_plan() -> Optional[ForgePlan]:
86
+ def get_active_plan() -> ForgePlan | None:
76
87
  """Get the currently active forge plan."""
77
88
  link = _active_link()
78
89
  if not link.exists():
@@ -94,7 +105,10 @@ def clear_active_plan() -> None:
94
105
 
95
106
  def _obsidian_forge_dir() -> Path:
96
107
  """Obsidian vault path for Forge documents."""
97
- return Path.home() / "Documents" / "Personal" / "Projects" / "WizardingCode Internal" / "ArkaOS" / "Forge"
108
+ return (
109
+ Path.home() / "Documents" / "Personal" / "Projects"
110
+ / "WizardingCode Internal" / "ArkaOS" / "Forge"
111
+ )
98
112
 
99
113
 
100
114
  def export_to_obsidian(plan: ForgePlan) -> Path:
@@ -150,7 +164,8 @@ def _render_obsidian_context(plan: ForgePlan) -> list[str]:
150
164
  ctx = plan.context
151
165
  return [
152
166
  "## Context",
153
- f"Repo: {ctx.repo} | Branch: {ctx.branch} | Commit: {ctx.commit_at_forge} | ArkaOS: {ctx.arkaos_version}",
167
+ f"Repo: {ctx.repo} | Branch: {ctx.branch} | "
168
+ f"Commit: {ctx.commit_at_forge} | ArkaOS: {ctx.arkaos_version}",
154
169
  "",
155
170
  "## Prompt",
156
171
  f"> {ctx.prompt}",
@@ -195,7 +210,10 @@ def _render_obsidian_critic(plan: ForgePlan) -> list[str]:
195
210
  if critic.risks:
196
211
  lines.append("### Risks")
197
212
  for risk in critic.risks:
198
- lines.append(f"- **{risk.risk}** ({risk.severity.value}) — Mitigation: {risk.mitigation}")
213
+ lines.append(
214
+ f"- **{risk.risk}** ({risk.severity.value}) — "
215
+ f"Mitigation: {risk.mitigation}"
216
+ )
199
217
  lines.append("")
200
218
  return lines
201
219
 
@@ -6,23 +6,21 @@ structured ForgePlan that downstream agents consume.
6
6
  """
7
7
 
8
8
  from enum import Enum
9
- from typing import List, Optional
10
9
 
11
10
  from pydantic import BaseModel, Field, field_validator
12
11
 
13
-
14
12
  # ---------------------------------------------------------------------------
15
13
  # Enums
16
14
  # ---------------------------------------------------------------------------
17
15
 
18
- class ForgeTier(str, Enum):
16
+ class ForgeTier(str, Enum): # noqa: UP042 — StrEnum flips str()/format(); migration is its own change
19
17
  """Execution tier determined by complexity score."""
20
18
  SHALLOW = "shallow"
21
19
  STANDARD = "standard"
22
20
  DEEP = "deep"
23
21
 
24
22
 
25
- class ForgeStatus(str, Enum):
23
+ class ForgeStatus(str, Enum): # noqa: UP042 — StrEnum flips str()/format(); migration is its own change
26
24
  """Lifecycle status of a ForgePlan."""
27
25
  DRAFT = "draft"
28
26
  REVIEWING = "reviewing"
@@ -34,21 +32,21 @@ class ForgeStatus(str, Enum):
34
32
  ARCHIVED = "archived"
35
33
 
36
34
 
37
- class ExplorerLens(str, Enum):
35
+ class ExplorerLens(str, Enum): # noqa: UP042 — StrEnum flips str()/format(); migration is its own change
38
36
  """The analytical perspective used when exploring a plan."""
39
37
  PRAGMATIC = "pragmatic" # Focus on fastest viable path
40
38
  ARCHITECTURAL = "architectural" # Focus on long-term design health
41
39
  CONTRARIAN = "contrarian" # Challenge assumptions, surface risks
42
40
 
43
41
 
44
- class RiskSeverity(str, Enum):
42
+ class RiskSeverity(str, Enum): # noqa: UP042 — StrEnum flips str()/format(); migration is its own change
45
43
  """Severity level for identified risks."""
46
44
  LOW = "low"
47
45
  MEDIUM = "medium"
48
46
  HIGH = "high"
49
47
 
50
48
 
51
- class ExecutionPathType(str, Enum):
49
+ class ExecutionPathType(str, Enum): # noqa: UP042 — StrEnum flips str()/format(); migration is its own change
52
50
  """Type of execution artefact that fulfils a plan step."""
53
51
  SKILL = "skill"
54
52
  WORKFLOW = "workflow"
@@ -60,12 +58,22 @@ class ExecutionPathType(str, Enum):
60
58
  # ---------------------------------------------------------------------------
61
59
 
62
60
  class ComplexityDimensions(BaseModel):
63
- """Five-axis complexity breakdown, each scored 0100."""
61
+ """Five-axis complexity breakdown, each scored 0-100."""
64
62
 
65
- scope: int = Field(default=0, description="Breadth of change across the codebase or system.")
66
- dependencies: int = Field(default=0, description="Number and criticality of upstream/downstream dependencies.")
67
- ambiguity: int = Field(default=0, description="How unclear or under-specified the requirements are.")
68
- risk: int = Field(default=0, description="Potential for breakage, data loss, or security impact.")
63
+ scope: int = Field(
64
+ default=0, description="Breadth of change across the codebase or system."
65
+ )
66
+ dependencies: int = Field(
67
+ default=0,
68
+ description="Number and criticality of upstream/downstream dependencies.",
69
+ )
70
+ ambiguity: int = Field(
71
+ default=0, description="How unclear or under-specified the requirements are."
72
+ )
73
+ risk: int = Field(
74
+ default=0,
75
+ description="Potential for breakage, data loss, or security impact.",
76
+ )
69
77
  novelty: int = Field(default=0, description="How unlike existing patterns this work is.")
70
78
 
71
79
  @field_validator("scope", "dependencies", "ambiguity", "risk", "novelty", mode="before")
@@ -78,14 +86,19 @@ class ComplexityDimensions(BaseModel):
78
86
  class ComplexityScore(BaseModel):
79
87
  """Aggregated complexity result produced by the Complexity Scorer."""
80
88
 
81
- score: int = Field(default=0, description="Composite 0100 score derived from all dimensions.")
82
- tier: ForgeTier = Field(default=ForgeTier.SHALLOW, description="Execution tier selected based on the composite score.")
83
- dimensions: ComplexityDimensions = Field(default_factory=ComplexityDimensions, description="Per-dimension breakdown.")
84
- similar_plans: List[str] = Field(
89
+ score: int = Field(default=0, description="Composite 0-100 score derived from all dimensions.")
90
+ tier: ForgeTier = Field(
91
+ default=ForgeTier.SHALLOW,
92
+ description="Execution tier selected based on the composite score.",
93
+ )
94
+ dimensions: ComplexityDimensions = Field(
95
+ default_factory=ComplexityDimensions, description="Per-dimension breakdown."
96
+ )
97
+ similar_plans: list[str] = Field(
85
98
  default_factory=list,
86
99
  description="IDs of previously completed plans with similar profiles.",
87
100
  )
88
- reused_patterns: List[str] = Field(
101
+ reused_patterns: list[str] = Field(
89
102
  default_factory=list,
90
103
  description="Named patterns from the ArkaOS pattern library reused in this plan.",
91
104
  )
@@ -213,13 +226,23 @@ class ForgePlan(BaseModel):
213
226
  "contains only constitution-enforced phases and confidence is 0.0."
214
227
  ),
215
228
  )
216
- dispatch_errors: List[str] = Field(
229
+ dispatch_errors: list[str] = Field(
217
230
  default_factory=list,
218
231
  description="Dispatch failures collected during forge (lens: error class: message).",
219
232
  )
220
233
 
221
234
  status: ForgeStatus = ForgeStatus.DRAFT
222
- approved_at: Optional[str] = None
223
- approved_by: Optional[str] = None
224
- executed_at: Optional[str] = None
225
- completion_notes: Optional[str] = None
235
+ approved_at: str | None = None
236
+ approved_by: str | None = None
237
+ rejected_at: str | None = None
238
+ rejected_by: str | None = None
239
+ executed_at: str | None = None
240
+ completion_notes: str | None = None
241
+ review_note: str | None = Field(
242
+ default=None,
243
+ description=(
244
+ "Operator note attached at decision time (plan-canvas review) "
245
+ "— the 'ponto-e-anota' trail; distinct from completion_notes, "
246
+ "which belongs to execution."
247
+ ),
248
+ )
@@ -23,6 +23,8 @@ CLI (for hooks/skills)::
23
23
  from __future__ import annotations
24
24
 
25
25
  import argparse
26
+ import configparser
27
+ import fnmatch
26
28
  import importlib.util
27
29
  import json
28
30
  import re
@@ -31,7 +33,7 @@ import shutil
31
33
  import subprocess
32
34
  import sys
33
35
  import time
34
- from dataclasses import asdict, dataclass, field
36
+ from dataclasses import asdict, dataclass, field, replace
35
37
  from pathlib import Path
36
38
 
37
39
  from core.shared.test_evidence import coverage_percent_from_xml
@@ -218,18 +220,22 @@ def _labelled(result: CheckResult, label: str) -> CheckResult:
218
220
  return result
219
221
 
220
222
 
221
- def _tool_cmd(tool: str) -> list[str] | None:
223
+ def _tool_cmd(tool: str, module: str | None = None) -> list[str] | None:
222
224
  """Resolve a Python tool: PATH binary, else the interpreter's module.
223
225
 
224
226
  Operator machines install ruff/pytest into the ArkaOS venv with no
225
227
  PATH binary — keying on shutil.which alone silently downgraded
226
228
  Python lint to eslint over installer/*.js, a FALSE GREEN on a
227
229
  NON-NEGOTIABLE gate (QG findings, F1-B2/F1-C1 reviews).
230
+
231
+ ``module`` covers tools whose import name differs from the command
232
+ (codespell ships ``codespell_lib``); without it the module fallback
233
+ misses and the check skips forever on a venv install.
228
234
  """
229
235
  if shutil.which(tool):
230
236
  return [tool]
231
- if importlib.util.find_spec(tool) is not None:
232
- return [sys.executable, "-m", tool]
237
+ if importlib.util.find_spec(module or tool) is not None:
238
+ return [sys.executable, "-m", module or tool]
233
239
  return None
234
240
 
235
241
 
@@ -335,15 +341,59 @@ def _check_typecheck(
335
341
  return _skip("typecheck", "no typecheck configuration detected")
336
342
 
337
343
 
344
+ def _project_pytest(project_dir: Path) -> list[str] | None:
345
+ """pytest belonging to the target project's own environment."""
346
+ for rel in (".venv/bin/pytest", "venv/bin/pytest"):
347
+ candidate = project_dir / rel
348
+ if candidate.is_file():
349
+ return [str(candidate)]
350
+ return None
351
+
352
+
353
+ def _foreign_pytest_can_collect(
354
+ pytest_cmd: list[str], project_dir: Path, timeout: int,
355
+ ) -> bool:
356
+ """A PATH/venv-foreign pytest must prove it can import the project.
357
+
358
+ Running a foreign-env pytest blind produced false FAILs on projects
359
+ whose dependencies live in their own venv (import errors read as
360
+ test failures — issue #283, a blind gate on mandatory-qa). Exit 5
361
+ ("no tests collected") still counts as a working import path.
362
+ """
363
+ try:
364
+ probe = subprocess.run(
365
+ [*pytest_cmd, "--collect-only", "-q"], cwd=project_dir,
366
+ capture_output=True, text=True, timeout=min(timeout, 60),
367
+ )
368
+ except (OSError, subprocess.TimeoutExpired):
369
+ return False
370
+ return probe.returncode in (0, 5)
371
+
372
+
338
373
  def _check_tests(
339
374
  project_dir: Path, changed: list[str] | None,
340
375
  test_command: str | None, timeout: int,
341
376
  ) -> CheckResult:
342
377
  if test_command:
343
378
  return _run("tests", shlex.split(test_command), project_dir, timeout)
344
- pytest_cmd = _tool_cmd("pytest")
345
- if _has_python(project_dir, changed) and pytest_cmd:
346
- return _run("tests", [*pytest_cmd, "-q"], project_dir, timeout)
379
+ if _has_python(project_dir, changed):
380
+ local_pytest = _project_pytest(project_dir)
381
+ if local_pytest:
382
+ return _labelled(
383
+ _run("tests", [*local_pytest, "-q"], project_dir, timeout),
384
+ "tests(project-venv)",
385
+ )
386
+ pytest_cmd = _tool_cmd("pytest")
387
+ if pytest_cmd and _foreign_pytest_can_collect(
388
+ pytest_cmd, project_dir, timeout
389
+ ):
390
+ return _run("tests", [*pytest_cmd, "-q"], project_dir, timeout)
391
+ if pytest_cmd:
392
+ return _skip(
393
+ "tests",
394
+ "pytest resolved outside the project env cannot import "
395
+ "it — pin --test-command",
396
+ )
347
397
  if _package_json_script(project_dir, "test"):
348
398
  return _run(
349
399
  "tests", ["npm", "test", "--silent"], project_dir, timeout,
@@ -501,12 +551,54 @@ def _check_spellcheck(
501
551
  project_dir: Path, changed: list[str] | None,
502
552
  test_command: str | None, timeout: int,
503
553
  ) -> CheckResult:
504
- if not shutil.which("codespell"):
554
+ # Resolve like lint/tests do. A bare shutil.which lookup left this check
555
+ # dark on venv installs (the binary is not on the harness PATH), so three
556
+ # consecutive QG rounds shipped with NO machine spellcheck signal.
557
+ cmd = _tool_cmd("codespell", module="codespell_lib")
558
+ if cmd is None:
505
559
  return _skip("spellcheck", "codespell not installed")
506
560
  md_files = [f for f in changed or [] if f.endswith(".md")]
507
561
  if not md_files:
508
562
  return _skip("spellcheck", "no changed .md files")
509
- return _run("spellcheck", ["codespell", *md_files], project_dir, timeout)
563
+ result = _run("spellcheck", [*cmd, *md_files], project_dir, timeout)
564
+ # codespell honours .codespellrc `skip` even for explicitly listed files,
565
+ # so a PASS here can cover far fewer files than were handed in. Reporting
566
+ # the raw count as coverage is how a narrowed scope reads as a full green
567
+ # (QG finding: 8 of 220 inspected, reported as 220). State both.
568
+ inspected = _spellcheck_inspected_count(project_dir, md_files)
569
+ scope = f"{inspected} of {len(md_files)} changed .md inspected"
570
+ if inspected < len(md_files):
571
+ scope += f"; {len(md_files) - inspected} excluded by .codespellrc skip"
572
+ summary = f"{scope}\n{result.summary}" if result.summary else scope
573
+ return replace(result, summary=summary)
574
+
575
+
576
+ def _spellcheck_inspected_count(project_dir: Path, md_files: list[str]) -> int:
577
+ """How many of ``md_files`` codespell actually reads after `skip`.
578
+
579
+ Asks codespell itself (``--count`` on a per-file basis is too slow; a
580
+ skipped file simply produces no output for any planted probe), so instead
581
+ we replay its own glob semantics from the config.
582
+ """
583
+ patterns = _codespell_skip_globs(project_dir)
584
+ if not patterns:
585
+ return len(md_files)
586
+ return sum(
587
+ 1 for f in md_files
588
+ if not any(fnmatch.fnmatch(f, p) or fnmatch.fnmatch(f"./{f}", p) for p in patterns)
589
+ )
590
+
591
+
592
+ def _codespell_skip_globs(project_dir: Path) -> list[str]:
593
+ """`skip` globs from .codespellrc, or [] when unreadable/absent."""
594
+ cfg = project_dir / ".codespellrc"
595
+ try:
596
+ parser = configparser.ConfigParser()
597
+ parser.read(cfg, encoding="utf-8")
598
+ raw = parser.get("codespell", "skip", fallback="")
599
+ except (OSError, configparser.Error):
600
+ return []
601
+ return [p.strip() for p in raw.split(",") if p.strip()]
510
602
 
511
603
 
512
604
  def _check_ui_screenshot(