@agile-vibe-coding/avc 0.2.3 → 0.3.2

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 (262) hide show
  1. package/README.md +475 -3
  2. package/cli/agents/agent-selector.md +23 -0
  3. package/cli/agents/code-implementer.md +117 -0
  4. package/cli/agents/code-validator.md +80 -0
  5. package/cli/agents/context-reviewer-epic.md +101 -0
  6. package/cli/agents/context-reviewer-story.md +92 -0
  7. package/cli/agents/context-writer-epic.md +145 -0
  8. package/cli/agents/context-writer-story.md +111 -0
  9. package/cli/agents/doc-writer-epic.md +42 -0
  10. package/cli/agents/doc-writer-story.md +43 -0
  11. package/cli/agents/duplicate-detector.md +110 -0
  12. package/cli/agents/epic-story-decomposer.md +318 -39
  13. package/cli/agents/mission-scope-generator.md +68 -4
  14. package/cli/agents/mission-scope-validator.md +40 -6
  15. package/cli/agents/project-context-extractor.md +21 -6
  16. package/cli/agents/scaffolding-generator.md +99 -0
  17. package/cli/agents/seed-validator.md +71 -0
  18. package/cli/agents/story-scope-reviewer.md +147 -0
  19. package/cli/agents/story-splitter.md +83 -0
  20. package/cli/agents/validator-documentation.json +31 -0
  21. package/cli/agents/validator-documentation.md +3 -1
  22. package/cli/api-reference-tool.js +368 -0
  23. package/cli/checks/catalog.json +76 -0
  24. package/cli/checks/code/quality.json +26 -0
  25. package/cli/checks/code/testing.json +14 -0
  26. package/cli/checks/code/traceability.json +26 -0
  27. package/cli/checks/cross-refs/epic.json +171 -0
  28. package/cli/checks/cross-refs/story.json +149 -0
  29. package/cli/checks/epic/api.json +114 -0
  30. package/cli/checks/epic/backend.json +126 -0
  31. package/cli/checks/epic/cloud.json +126 -0
  32. package/cli/checks/epic/data.json +102 -0
  33. package/cli/checks/epic/database.json +114 -0
  34. package/cli/checks/epic/developer.json +182 -0
  35. package/cli/checks/epic/devops.json +174 -0
  36. package/cli/checks/epic/frontend.json +162 -0
  37. package/cli/checks/epic/mobile.json +102 -0
  38. package/cli/checks/epic/qa.json +90 -0
  39. package/cli/checks/epic/security.json +184 -0
  40. package/cli/checks/epic/solution-architect.json +192 -0
  41. package/cli/checks/epic/test-architect.json +90 -0
  42. package/cli/checks/epic/ui.json +102 -0
  43. package/cli/checks/epic/ux.json +90 -0
  44. package/cli/checks/fixes/epic-fix-template.md +10 -0
  45. package/cli/checks/fixes/story-fix-template.md +10 -0
  46. package/cli/checks/story/api.json +186 -0
  47. package/cli/checks/story/backend.json +102 -0
  48. package/cli/checks/story/cloud.json +102 -0
  49. package/cli/checks/story/data.json +210 -0
  50. package/cli/checks/story/database.json +102 -0
  51. package/cli/checks/story/developer.json +168 -0
  52. package/cli/checks/story/devops.json +102 -0
  53. package/cli/checks/story/frontend.json +174 -0
  54. package/cli/checks/story/mobile.json +102 -0
  55. package/cli/checks/story/qa.json +210 -0
  56. package/cli/checks/story/security.json +198 -0
  57. package/cli/checks/story/solution-architect.json +230 -0
  58. package/cli/checks/story/test-architect.json +210 -0
  59. package/cli/checks/story/ui.json +102 -0
  60. package/cli/checks/story/ux.json +102 -0
  61. package/cli/coding-order.js +401 -0
  62. package/cli/dependency-checker.js +72 -0
  63. package/cli/epic-story-validator.js +284 -799
  64. package/cli/index.js +0 -0
  65. package/cli/init-model-config.js +17 -10
  66. package/cli/init.js +514 -92
  67. package/cli/kanban-server-manager.js +1 -2
  68. package/cli/llm-claude.js +98 -31
  69. package/cli/llm-gemini.js +29 -5
  70. package/cli/llm-local.js +493 -0
  71. package/cli/llm-openai.js +262 -41
  72. package/cli/llm-provider.js +147 -8
  73. package/cli/llm-token-limits.js +113 -4
  74. package/cli/llm-verifier.js +209 -1
  75. package/cli/llm-xiaomi.js +143 -0
  76. package/cli/message-constants.js +3 -12
  77. package/cli/messaging-api.js +6 -12
  78. package/cli/micro-check-fixer.js +335 -0
  79. package/cli/micro-check-runner.js +449 -0
  80. package/cli/micro-check-scorer.js +148 -0
  81. package/cli/micro-check-validator.js +538 -0
  82. package/cli/model-pricing.js +23 -0
  83. package/cli/model-selector.js +3 -2
  84. package/cli/prompt-logger.js +57 -0
  85. package/cli/repl-ink.js +106 -346
  86. package/cli/repl-old.js +1 -2
  87. package/cli/seed-processor.js +194 -24
  88. package/cli/sprint-planning-processor.js +2638 -289
  89. package/cli/template-processor.js +50 -3
  90. package/cli/token-tracker.js +50 -23
  91. package/cli/tools/generate-story-validators.js +1 -1
  92. package/cli/validation-router.js +70 -8
  93. package/cli/worktree-runner.js +654 -0
  94. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  95. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  96. package/kanban/client/dist/index.html +2 -2
  97. package/kanban/client/src/App.jsx +43 -14
  98. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
  99. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
  100. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
  101. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  102. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
  103. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
  104. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
  105. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
  106. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
  107. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
  108. package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
  109. package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
  110. package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
  111. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
  112. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  113. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  114. package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
  115. package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
  116. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
  117. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  118. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
  119. package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
  120. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  121. package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
  122. package/kanban/client/src/components/stats/CostModal.jsx +34 -3
  123. package/kanban/client/src/hooks/useGrouping.js +59 -0
  124. package/kanban/client/src/lib/api.js +118 -4
  125. package/kanban/client/src/lib/status-grouping.js +10 -0
  126. package/kanban/client/src/store/kanbanStore.js +8 -0
  127. package/kanban/server/index.js +23 -2
  128. package/kanban/server/routes/ceremony.js +153 -4
  129. package/kanban/server/routes/costs.js +9 -3
  130. package/kanban/server/routes/openai-oauth.js +366 -0
  131. package/kanban/server/routes/settings.js +447 -14
  132. package/kanban/server/routes/websocket.js +7 -2
  133. package/kanban/server/routes/work-items.js +141 -1
  134. package/kanban/server/services/CeremonyService.js +275 -24
  135. package/kanban/server/services/TaskRunnerService.js +261 -0
  136. package/kanban/server/workers/run-task-worker.js +121 -0
  137. package/kanban/server/workers/seed-worker.js +94 -0
  138. package/kanban/server/workers/sponsor-call-worker.js +14 -6
  139. package/kanban/server/workers/sprint-planning-worker.js +94 -12
  140. package/package.json +2 -3
  141. package/cli/agents/solver-epic-api.json +0 -15
  142. package/cli/agents/solver-epic-api.md +0 -39
  143. package/cli/agents/solver-epic-backend.json +0 -15
  144. package/cli/agents/solver-epic-backend.md +0 -39
  145. package/cli/agents/solver-epic-cloud.json +0 -15
  146. package/cli/agents/solver-epic-cloud.md +0 -39
  147. package/cli/agents/solver-epic-data.json +0 -15
  148. package/cli/agents/solver-epic-data.md +0 -39
  149. package/cli/agents/solver-epic-database.json +0 -15
  150. package/cli/agents/solver-epic-database.md +0 -39
  151. package/cli/agents/solver-epic-developer.json +0 -15
  152. package/cli/agents/solver-epic-developer.md +0 -39
  153. package/cli/agents/solver-epic-devops.json +0 -15
  154. package/cli/agents/solver-epic-devops.md +0 -39
  155. package/cli/agents/solver-epic-frontend.json +0 -15
  156. package/cli/agents/solver-epic-frontend.md +0 -39
  157. package/cli/agents/solver-epic-mobile.json +0 -15
  158. package/cli/agents/solver-epic-mobile.md +0 -39
  159. package/cli/agents/solver-epic-qa.json +0 -15
  160. package/cli/agents/solver-epic-qa.md +0 -39
  161. package/cli/agents/solver-epic-security.json +0 -15
  162. package/cli/agents/solver-epic-security.md +0 -39
  163. package/cli/agents/solver-epic-solution-architect.json +0 -15
  164. package/cli/agents/solver-epic-solution-architect.md +0 -39
  165. package/cli/agents/solver-epic-test-architect.json +0 -15
  166. package/cli/agents/solver-epic-test-architect.md +0 -39
  167. package/cli/agents/solver-epic-ui.json +0 -15
  168. package/cli/agents/solver-epic-ui.md +0 -39
  169. package/cli/agents/solver-epic-ux.json +0 -15
  170. package/cli/agents/solver-epic-ux.md +0 -39
  171. package/cli/agents/solver-story-api.json +0 -15
  172. package/cli/agents/solver-story-api.md +0 -39
  173. package/cli/agents/solver-story-backend.json +0 -15
  174. package/cli/agents/solver-story-backend.md +0 -39
  175. package/cli/agents/solver-story-cloud.json +0 -15
  176. package/cli/agents/solver-story-cloud.md +0 -39
  177. package/cli/agents/solver-story-data.json +0 -15
  178. package/cli/agents/solver-story-data.md +0 -39
  179. package/cli/agents/solver-story-database.json +0 -15
  180. package/cli/agents/solver-story-database.md +0 -39
  181. package/cli/agents/solver-story-developer.json +0 -15
  182. package/cli/agents/solver-story-developer.md +0 -39
  183. package/cli/agents/solver-story-devops.json +0 -15
  184. package/cli/agents/solver-story-devops.md +0 -39
  185. package/cli/agents/solver-story-frontend.json +0 -15
  186. package/cli/agents/solver-story-frontend.md +0 -39
  187. package/cli/agents/solver-story-mobile.json +0 -15
  188. package/cli/agents/solver-story-mobile.md +0 -39
  189. package/cli/agents/solver-story-qa.json +0 -15
  190. package/cli/agents/solver-story-qa.md +0 -39
  191. package/cli/agents/solver-story-security.json +0 -15
  192. package/cli/agents/solver-story-security.md +0 -39
  193. package/cli/agents/solver-story-solution-architect.json +0 -15
  194. package/cli/agents/solver-story-solution-architect.md +0 -39
  195. package/cli/agents/solver-story-test-architect.json +0 -15
  196. package/cli/agents/solver-story-test-architect.md +0 -39
  197. package/cli/agents/solver-story-ui.json +0 -15
  198. package/cli/agents/solver-story-ui.md +0 -39
  199. package/cli/agents/solver-story-ux.json +0 -15
  200. package/cli/agents/solver-story-ux.md +0 -39
  201. package/cli/agents/validator-epic-api.json +0 -93
  202. package/cli/agents/validator-epic-api.md +0 -137
  203. package/cli/agents/validator-epic-backend.json +0 -93
  204. package/cli/agents/validator-epic-backend.md +0 -130
  205. package/cli/agents/validator-epic-cloud.json +0 -93
  206. package/cli/agents/validator-epic-cloud.md +0 -137
  207. package/cli/agents/validator-epic-data.json +0 -93
  208. package/cli/agents/validator-epic-data.md +0 -130
  209. package/cli/agents/validator-epic-database.json +0 -93
  210. package/cli/agents/validator-epic-database.md +0 -137
  211. package/cli/agents/validator-epic-developer.json +0 -74
  212. package/cli/agents/validator-epic-developer.md +0 -153
  213. package/cli/agents/validator-epic-devops.json +0 -74
  214. package/cli/agents/validator-epic-devops.md +0 -153
  215. package/cli/agents/validator-epic-frontend.json +0 -74
  216. package/cli/agents/validator-epic-frontend.md +0 -153
  217. package/cli/agents/validator-epic-mobile.json +0 -93
  218. package/cli/agents/validator-epic-mobile.md +0 -130
  219. package/cli/agents/validator-epic-qa.json +0 -93
  220. package/cli/agents/validator-epic-qa.md +0 -130
  221. package/cli/agents/validator-epic-security.json +0 -74
  222. package/cli/agents/validator-epic-security.md +0 -154
  223. package/cli/agents/validator-epic-solution-architect.json +0 -74
  224. package/cli/agents/validator-epic-solution-architect.md +0 -156
  225. package/cli/agents/validator-epic-test-architect.json +0 -93
  226. package/cli/agents/validator-epic-test-architect.md +0 -130
  227. package/cli/agents/validator-epic-ui.json +0 -93
  228. package/cli/agents/validator-epic-ui.md +0 -130
  229. package/cli/agents/validator-epic-ux.json +0 -93
  230. package/cli/agents/validator-epic-ux.md +0 -130
  231. package/cli/agents/validator-story-api.json +0 -104
  232. package/cli/agents/validator-story-api.md +0 -152
  233. package/cli/agents/validator-story-backend.json +0 -104
  234. package/cli/agents/validator-story-backend.md +0 -152
  235. package/cli/agents/validator-story-cloud.json +0 -104
  236. package/cli/agents/validator-story-cloud.md +0 -152
  237. package/cli/agents/validator-story-data.json +0 -104
  238. package/cli/agents/validator-story-data.md +0 -152
  239. package/cli/agents/validator-story-database.json +0 -104
  240. package/cli/agents/validator-story-database.md +0 -152
  241. package/cli/agents/validator-story-developer.json +0 -104
  242. package/cli/agents/validator-story-developer.md +0 -152
  243. package/cli/agents/validator-story-devops.json +0 -104
  244. package/cli/agents/validator-story-devops.md +0 -152
  245. package/cli/agents/validator-story-frontend.json +0 -104
  246. package/cli/agents/validator-story-frontend.md +0 -152
  247. package/cli/agents/validator-story-mobile.json +0 -104
  248. package/cli/agents/validator-story-mobile.md +0 -152
  249. package/cli/agents/validator-story-qa.json +0 -104
  250. package/cli/agents/validator-story-qa.md +0 -152
  251. package/cli/agents/validator-story-security.json +0 -104
  252. package/cli/agents/validator-story-security.md +0 -152
  253. package/cli/agents/validator-story-solution-architect.json +0 -104
  254. package/cli/agents/validator-story-solution-architect.md +0 -152
  255. package/cli/agents/validator-story-test-architect.json +0 -104
  256. package/cli/agents/validator-story-test-architect.md +0 -152
  257. package/cli/agents/validator-story-ui.json +0 -104
  258. package/cli/agents/validator-story-ui.md +0 -152
  259. package/cli/agents/validator-story-ux.json +0 -104
  260. package/cli/agents/validator-story-ux.md +0 -152
  261. package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
  262. package/kanban/client/dist/assets/index-nLh0m82Q.css +0 -1
@@ -0,0 +1,507 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { X, RotateCcw, ChevronDown, ChevronRight, Plus, Trash2, Info } from 'lucide-react';
3
+ import { getCheckContent, saveCheckContent, resetCheck } from '../../lib/api';
4
+
5
+ // ── Severity config ──────────────────────────────────────────────────────────
6
+
7
+ const SEVERITY_OPTIONS = ['critical', 'major', 'minor'];
8
+ const SEVERITY_STYLE = {
9
+ critical: 'bg-red-100 text-red-800 border-red-200',
10
+ major: 'bg-amber-100 text-amber-800 border-amber-200',
11
+ minor: 'bg-slate-100 text-slate-600 border-slate-200',
12
+ };
13
+
14
+ // ── Intro / help panel ───────────────────────────────────────────────────────
15
+
16
+ function HelpPanel({ isCrossRef }) {
17
+ const [open, setOpen] = useState(false);
18
+
19
+ return (
20
+ <div className="mb-2">
21
+ <button
22
+ type="button"
23
+ onClick={() => setOpen(!open)}
24
+ className="flex items-center gap-1.5 text-[11px] text-blue-600 hover:text-blue-800 transition-colors"
25
+ >
26
+ <Info className="w-3.5 h-3.5" />
27
+ {open ? 'Hide guide' : 'How micro-checks work & field reference'}
28
+ </button>
29
+ {open && (
30
+ <div className="mt-2 text-[11px] leading-relaxed text-slate-600 bg-blue-50/60 border border-blue-100 rounded-lg px-4 py-3 space-y-3">
31
+ <div>
32
+ <p className="font-semibold text-slate-700 mb-1">How micro-checks are used in ceremonies</p>
33
+ <p>
34
+ During Sprint Planning validation, each epic and story is evaluated by
35
+ hundreds of small, independent checks instead of a single monolithic LLM call.
36
+ {isCrossRef
37
+ ? ' Cross-reference checks (Tier 2) run after all domain checks complete. They verify consistency across perspectives — for example, that security requirements match API endpoint access controls, or that database PII fields align with privacy policies.'
38
+ : ' Domain checks (Tier 1) run first, in parallel. Each check makes 1–2 short LLM calls: an optional applicability gate, then a YES/NO quality question. Failed checks are scored deterministically and critical/major failures trigger atomic auto-fixes (Tier 3).'}
39
+ </p>
40
+ </div>
41
+
42
+ <div>
43
+ <p className="font-semibold text-slate-700 mb-1">Severity levels</p>
44
+ <ul className="list-none space-y-0.5 ml-1">
45
+ <li><span className="font-mono font-semibold text-red-700">critical</span> — Failure blocks the score below 70. A single critical failure caps the score at 60; each additional one drops it by 10. Auto-fix is always attempted.</li>
46
+ <li><span className="font-mono font-semibold text-amber-700">major</span> — Failure caps the score between 70–89. Each major failure reduces the cap by 5. Auto-fix is attempted.</li>
47
+ <li><span className="font-mono font-semibold text-slate-500">minor</span> — Only minor failures allow scores of 95–100. Minor failures are reported but not auto-fixed.</li>
48
+ </ul>
49
+ </div>
50
+
51
+ <div>
52
+ <p className="font-semibold text-slate-700 mb-1">Field reference</p>
53
+ <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 ml-1">
54
+ <dt className="font-mono text-slate-700">severity</dt>
55
+ <dd>How much a failure impacts the score (see above).</dd>
56
+ <dt className="font-mono text-slate-700">category</dt>
57
+ <dd>Grouping label for reporting (e.g. "security", "data-modeling"). Helps cluster related failures.</dd>
58
+ {!isCrossRef && <>
59
+ <dt className="font-mono text-slate-700">applicabilityQuestion</dt>
60
+ <dd>Optional gate question asked before the main check. If the LLM answers NO, the check is skipped as not applicable. Leave empty to always run. Example: "Does this epic involve user authentication?"</dd>
61
+ <dt className="font-mono text-slate-700">universal</dt>
62
+ <dd>If true, the applicability gate is skipped and the check always runs. Useful for checks that apply to every epic/story regardless of domain.</dd>
63
+ </>}
64
+ <dt className="font-mono text-slate-700">question</dt>
65
+ <dd>The YES/NO quality question sent to the LLM. A YES answer means the check passes. Write it so that YES = good quality. Example: "Does this story include acceptance criteria that cover error scenarios?"</dd>
66
+ <dt className="font-mono text-slate-700">failDescription</dt>
67
+ <dd>Human-readable explanation shown when the check fails. Describes what quality gap was detected.</dd>
68
+ <dt className="font-mono text-slate-700">failSuggestion</dt>
69
+ <dd>Actionable guidance for the auto-fixer (or a human) on how to address the failure. Be specific — this drives the Tier 3 atomic fix prompt.</dd>
70
+ {isCrossRef && <>
71
+ <dt className="font-mono text-slate-700">perspectives</dt>
72
+ <dd>Which domain perspectives this cross-reference check bridges (e.g. ["security", "api"]). Read-only — set in the JSON directly.</dd>
73
+ <dt className="font-mono text-slate-700">dependsOn</dt>
74
+ <dd>Tier 1 check IDs whose evidence this check needs. The cross-ref check only runs if all dependencies have results. Read-only — set in the JSON directly.</dd>
75
+ </>}
76
+ </dl>
77
+ </div>
78
+
79
+ <p className="text-[10px] text-slate-400 italic">
80
+ Customized checks are saved to <span className="font-mono">.avc/customized-agents/checks/</span> and
81
+ override the built-in defaults. Use "Reset to default" to revert.
82
+ </p>
83
+ </div>
84
+ )}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ // ── Field editor components ──────────────────────────────────────────────────
90
+
91
+ function TextField({ label, value, onChange, placeholder, rows = 2 }) {
92
+ return (
93
+ <label className="block">
94
+ <span className="text-[11px] font-medium text-slate-500 uppercase tracking-wide">{label}</span>
95
+ <textarea
96
+ value={value || ''}
97
+ onChange={e => onChange(e.target.value)}
98
+ placeholder={placeholder}
99
+ rows={rows}
100
+ className="mt-0.5 w-full text-xs text-slate-800 border border-slate-200 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400 resize-none leading-relaxed"
101
+ />
102
+ </label>
103
+ );
104
+ }
105
+
106
+ function SeveritySelect({ value, onChange }) {
107
+ return (
108
+ <label className="block">
109
+ <span className="text-[11px] font-medium text-slate-500 uppercase tracking-wide">Severity</span>
110
+ <select
111
+ value={value || 'major'}
112
+ onChange={e => onChange(e.target.value)}
113
+ className="mt-0.5 w-full text-xs border border-slate-200 rounded-md px-2 py-1.5 bg-white text-slate-800 focus:outline-none focus:ring-1 focus:ring-blue-400"
114
+ >
115
+ {SEVERITY_OPTIONS.map(s => (
116
+ <option key={s} value={s}>{s.charAt(0).toUpperCase() + s.slice(1)}</option>
117
+ ))}
118
+ </select>
119
+ </label>
120
+ );
121
+ }
122
+
123
+ function InlineField({ label, value }) {
124
+ return (
125
+ <span className="text-[10px] text-slate-400">
126
+ <span className="font-medium">{label}:</span> <span className="font-mono text-slate-500">{value}</span>
127
+ </span>
128
+ );
129
+ }
130
+
131
+ // ── Single check card ────────────────────────────────────────────────────────
132
+
133
+ function CheckCard({ check, index, isCrossRef, expanded, onToggle, onChange, onDelete }) {
134
+ const sev = SEVERITY_STYLE[check.severity] || SEVERITY_STYLE.minor;
135
+
136
+ const update = (field, value) => {
137
+ onChange(index, { ...check, [field]: value });
138
+ };
139
+
140
+ // Truncate question for collapsed header
141
+ const shortQuestion = (check.question || '').length > 90
142
+ ? check.question.slice(0, 90) + '…'
143
+ : (check.question || '(no question)');
144
+
145
+ return (
146
+ <div className={`border rounded-lg overflow-hidden ${expanded ? 'border-blue-200 shadow-sm' : 'border-slate-200'}`}>
147
+ {/* Header — always visible */}
148
+ <button
149
+ type="button"
150
+ onClick={onToggle}
151
+ className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-colors ${expanded ? 'bg-blue-50/50' : 'bg-white hover:bg-slate-50'}`}
152
+ >
153
+ {expanded
154
+ ? <ChevronDown className="w-3 h-3 text-slate-400 flex-shrink-0" />
155
+ : <ChevronRight className="w-3 h-3 text-slate-400 flex-shrink-0" />
156
+ }
157
+ <span className={`flex-shrink-0 text-[10px] font-semibold px-1.5 py-0.5 rounded border ${sev}`}>
158
+ {check.severity || 'major'}
159
+ </span>
160
+ <span className="flex-shrink-0 text-[10px] font-mono text-slate-400">{check.id}</span>
161
+ <span className="flex-1 text-xs text-slate-600 truncate min-w-0">{shortQuestion}</span>
162
+ </button>
163
+
164
+ {/* Expanded form */}
165
+ {expanded && (
166
+ <div className="px-3 pb-3 pt-1 bg-white border-t border-slate-100">
167
+ {/* Read-only metadata row */}
168
+ <div className="flex items-center gap-3 mb-2 flex-wrap">
169
+ <InlineField label="id" value={check.id} />
170
+ {check.category && <InlineField label="category" value={check.category} />}
171
+ {isCrossRef && check.perspectives && (
172
+ <InlineField label="perspectives" value={check.perspectives.join(', ')} />
173
+ )}
174
+ {isCrossRef && check.dependsOn && (
175
+ <InlineField label="dependsOn" value={check.dependsOn.join(', ')} />
176
+ )}
177
+ {!isCrossRef && check.universal !== undefined && (
178
+ <InlineField label="universal" value={check.universal ? 'yes' : 'no'} />
179
+ )}
180
+ </div>
181
+
182
+ <div className="grid grid-cols-[1fr_auto] gap-x-3 gap-y-2">
183
+ <div className="col-span-2 sm:col-span-1">
184
+ <SeveritySelect value={check.severity} onChange={v => update('severity', v)} />
185
+ </div>
186
+ <div className="col-span-2 sm:col-span-1">
187
+ <label className="block">
188
+ <span className="text-[11px] font-medium text-slate-500 uppercase tracking-wide">Category</span>
189
+ <input
190
+ type="text"
191
+ value={check.category || ''}
192
+ onChange={e => update('category', e.target.value)}
193
+ className="mt-0.5 w-full text-xs border border-slate-200 rounded-md px-2.5 py-1.5 text-slate-800 focus:outline-none focus:ring-1 focus:ring-blue-400"
194
+ />
195
+ </label>
196
+ </div>
197
+ </div>
198
+
199
+ <div className="mt-2 flex flex-col gap-2">
200
+ {!isCrossRef && !check.universal && (
201
+ <TextField
202
+ label="Applicability Question"
203
+ value={check.applicabilityQuestion}
204
+ onChange={v => update('applicabilityQuestion', v)}
205
+ placeholder="When should this check run? (leave empty = always)"
206
+ rows={2}
207
+ />
208
+ )}
209
+ <TextField
210
+ label="Question"
211
+ value={check.question}
212
+ onChange={v => update('question', v)}
213
+ placeholder="The YES/NO quality question asked to the LLM"
214
+ rows={3}
215
+ />
216
+ <TextField
217
+ label="Fail Description"
218
+ value={check.failDescription}
219
+ onChange={v => update('failDescription', v)}
220
+ placeholder="What's wrong when this check fails"
221
+ rows={2}
222
+ />
223
+ <TextField
224
+ label="Fail Suggestion"
225
+ value={check.failSuggestion}
226
+ onChange={v => update('failSuggestion', v)}
227
+ placeholder="How to fix the failure"
228
+ rows={2}
229
+ />
230
+ </div>
231
+
232
+ {/* Delete button */}
233
+ <div className="mt-3 flex justify-end">
234
+ <button
235
+ type="button"
236
+ onClick={() => onDelete(index)}
237
+ className="flex items-center gap-1 text-[11px] text-red-500 hover:text-red-700 transition-colors"
238
+ >
239
+ <Trash2 className="w-3 h-3" />
240
+ Remove check
241
+ </button>
242
+ </div>
243
+ </div>
244
+ )}
245
+ </div>
246
+ );
247
+ }
248
+
249
+ // ── Main popup ───────────────────────────────────────────────────────────────
250
+
251
+ export function CheckEditorPopup({ scope, perspective, onClose, onSaved, onReset }) {
252
+ const [fileData, setFileData] = useState(null); // raw API response
253
+ const [parsed, setParsed] = useState(null); // full parsed object
254
+ const [checks, setChecks] = useState([]); // the checks array being edited
255
+ const [expandedIdx, setExpandedIdx] = useState(null); // which card is expanded
256
+ const [loading, setLoading] = useState(true);
257
+ const [saving, setSaving] = useState(false);
258
+ const [saved, setSaved] = useState(false);
259
+ const [error, setError] = useState(null);
260
+ const [pendingReset, setPendingReset] = useState(false);
261
+
262
+ const isCrossRef = scope === 'cross-refs';
263
+ const displayName = isCrossRef
264
+ ? `cross-refs/${perspective}.json`
265
+ : `${scope}/${perspective}.json`;
266
+
267
+ useEffect(() => {
268
+ setLoading(true);
269
+ setError(null);
270
+ setPendingReset(false);
271
+ setExpandedIdx(null);
272
+ getCheckContent(scope, perspective)
273
+ .then(d => {
274
+ setFileData(d);
275
+ try {
276
+ const obj = JSON.parse(d.content);
277
+ setParsed(obj);
278
+ setChecks(JSON.parse(JSON.stringify(obj.checks || [])));
279
+ } catch {
280
+ setError('Failed to parse check file');
281
+ }
282
+ })
283
+ .catch(err => setError(err.message))
284
+ .finally(() => setLoading(false));
285
+ }, [scope, perspective]);
286
+
287
+ // Dirty detection: compare current checks to original
288
+ const isDirty = (() => {
289
+ if (!fileData) return false;
290
+ try {
291
+ const original = JSON.parse(fileData.content);
292
+ return JSON.stringify(checks) !== JSON.stringify(original.checks || []);
293
+ } catch {
294
+ return false;
295
+ }
296
+ })();
297
+
298
+ const handleCheckChange = (index, updatedCheck) => {
299
+ setChecks(prev => prev.map((c, i) => i === index ? updatedCheck : c));
300
+ setPendingReset(false);
301
+ };
302
+
303
+ const handleDelete = (index) => {
304
+ setChecks(prev => prev.filter((_, i) => i !== index));
305
+ if (expandedIdx === index) setExpandedIdx(null);
306
+ else if (expandedIdx > index) setExpandedIdx(expandedIdx - 1);
307
+ setPendingReset(false);
308
+ };
309
+
310
+ const handleAdd = () => {
311
+ const prefix = isCrossRef ? 'xref' : (scope === 'epic' ? perspective.slice(0, 3) + '-epic' : perspective.slice(0, 3) + '-story');
312
+ const newId = `${prefix}-${String(checks.length + 1).padStart(2, '0')}`;
313
+ const newCheck = isCrossRef
314
+ ? { id: newId, tier: 2, perspectives: [], severity: 'major', category: 'consistency', dependsOn: [], question: '', failDescription: '', failSuggestion: '' }
315
+ : { id: newId, tier: 1, perspective, severity: 'major', category: '', universal: false, applicabilityQuestion: '', question: '', failDescription: '', failSuggestion: '' };
316
+ setChecks(prev => [...prev, newCheck]);
317
+ setExpandedIdx(checks.length);
318
+ setPendingReset(false);
319
+ };
320
+
321
+ const handleSave = async () => {
322
+ if (!fileData) return;
323
+ setSaving(true);
324
+ setError(null);
325
+ try {
326
+ if (pendingReset) {
327
+ await resetCheck(scope, perspective);
328
+ const obj = JSON.parse(fileData.defaultContent);
329
+ setParsed(obj);
330
+ setChecks(JSON.parse(JSON.stringify(obj.checks || [])));
331
+ setFileData(prev => ({ ...prev, content: prev.defaultContent, isCustomized: false }));
332
+ setPendingReset(false);
333
+ onReset?.();
334
+ } else {
335
+ // Rebuild full JSON preserving top-level fields
336
+ const output = { ...parsed, checks };
337
+ const content = JSON.stringify(output, null, 2);
338
+ await saveCheckContent(scope, perspective, content);
339
+ setFileData(prev => ({ ...prev, content, isCustomized: true }));
340
+ onSaved?.();
341
+ }
342
+ setSaved(true);
343
+ setTimeout(() => setSaved(false), 2000);
344
+ } catch (err) {
345
+ setError(err.message);
346
+ } finally {
347
+ setSaving(false);
348
+ }
349
+ };
350
+
351
+ const handleReset = () => {
352
+ if (!fileData?.defaultContent) return;
353
+ try {
354
+ const obj = JSON.parse(fileData.defaultContent);
355
+ setChecks(JSON.parse(JSON.stringify(obj.checks || [])));
356
+ } catch {}
357
+ setPendingReset(true);
358
+ setExpandedIdx(null);
359
+ };
360
+
361
+ const canReset = fileData?.isCustomized && !pendingReset;
362
+
363
+ // Severity summary
364
+ const counts = { critical: 0, major: 0, minor: 0 };
365
+ checks.forEach(c => { if (counts[c.severity] !== undefined) counts[c.severity]++; });
366
+
367
+ return (
368
+ <div
369
+ className="fixed inset-0 z-[90] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
370
+ onClick={e => { if (e.target === e.currentTarget) onClose(); }}
371
+ >
372
+ <div
373
+ className="w-full max-w-3xl bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden"
374
+ style={{ height: '85vh' }}
375
+ >
376
+ {/* Header */}
377
+ <div className="flex items-center justify-between px-5 py-3 border-b border-slate-100 flex-shrink-0">
378
+ <div className="flex items-center gap-2 min-w-0 flex-wrap">
379
+ <span className="text-sm font-mono font-medium text-slate-700 truncate">
380
+ {displayName}
381
+ </span>
382
+ <span className="flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded bg-blue-50 text-blue-600">
383
+ {checks.length} checks
384
+ </span>
385
+ {counts.critical > 0 && (
386
+ <span className={`flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded border ${SEVERITY_STYLE.critical}`}>
387
+ {counts.critical} critical
388
+ </span>
389
+ )}
390
+ {counts.major > 0 && (
391
+ <span className={`flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded border ${SEVERITY_STYLE.major}`}>
392
+ {counts.major} major
393
+ </span>
394
+ )}
395
+ {counts.minor > 0 && (
396
+ <span className={`flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded border ${SEVERITY_STYLE.minor}`}>
397
+ {counts.minor} minor
398
+ </span>
399
+ )}
400
+ {fileData?.isCustomized && !pendingReset && (
401
+ <span className="flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">
402
+ Custom
403
+ </span>
404
+ )}
405
+ {pendingReset && (
406
+ <span className="flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded bg-blue-100 text-blue-700">
407
+ Reset pending — save to apply
408
+ </span>
409
+ )}
410
+ </div>
411
+ <button
412
+ type="button"
413
+ onClick={onClose}
414
+ className="text-slate-400 hover:text-slate-600 transition-colors ml-4 flex-shrink-0"
415
+ aria-label="Close"
416
+ >
417
+ <X className="w-4 h-4" />
418
+ </button>
419
+ </div>
420
+
421
+ {/* Body */}
422
+ {loading ? (
423
+ <div className="flex-1 flex items-center justify-center text-sm text-slate-400">
424
+ <span className="w-4 h-4 border border-slate-300 border-t-slate-600 rounded-full animate-spin mr-2" />
425
+ Loading…
426
+ </div>
427
+ ) : error && !fileData ? (
428
+ <div className="flex-1 flex items-center justify-center text-sm text-red-500 px-6 text-center">
429
+ {error}
430
+ </div>
431
+ ) : (
432
+ <div className="flex-1 overflow-y-auto min-h-0 px-4 py-3">
433
+ <div className="flex flex-col gap-1.5">
434
+ <HelpPanel isCrossRef={isCrossRef} />
435
+ {checks.map((check, i) => (
436
+ <CheckCard
437
+ key={check.id || i}
438
+ check={check}
439
+ index={i}
440
+ isCrossRef={isCrossRef}
441
+ expanded={expandedIdx === i}
442
+ onToggle={() => setExpandedIdx(expandedIdx === i ? null : i)}
443
+ onChange={handleCheckChange}
444
+ onDelete={handleDelete}
445
+ />
446
+ ))}
447
+
448
+ {/* Add check button */}
449
+ <button
450
+ type="button"
451
+ onClick={handleAdd}
452
+ className="flex items-center justify-center gap-1.5 py-2 border-2 border-dashed border-slate-200 rounded-lg text-xs text-slate-400 hover:text-blue-600 hover:border-blue-300 transition-colors"
453
+ >
454
+ <Plus className="w-3.5 h-3.5" />
455
+ Add check
456
+ </button>
457
+ </div>
458
+ </div>
459
+ )}
460
+
461
+ {/* Footer */}
462
+ <div className="flex items-center justify-between px-5 py-3 border-t border-slate-100 flex-shrink-0">
463
+ <div>
464
+ {error && !loading && (
465
+ <p className="text-xs text-red-600">{error}</p>
466
+ )}
467
+ {saved && (
468
+ <p className="text-xs text-green-600 font-medium">Saved</p>
469
+ )}
470
+ </div>
471
+ <div className="flex items-center gap-2">
472
+ <button
473
+ type="button"
474
+ onClick={handleReset}
475
+ disabled={!canReset}
476
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-slate-500 hover:text-amber-600 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
477
+ title={canReset ? 'Revert to built-in defaults' : 'Only available for customized checks'}
478
+ >
479
+ <RotateCcw className="w-3 h-3" />
480
+ Reset to default
481
+ </button>
482
+ <button
483
+ type="button"
484
+ onClick={onClose}
485
+ className="px-3 py-1.5 text-xs font-medium text-slate-500 hover:text-slate-700 transition-colors"
486
+ >
487
+ Cancel
488
+ </button>
489
+ <button
490
+ type="button"
491
+ onClick={handleSave}
492
+ disabled={(!isDirty && !pendingReset) || saving}
493
+ className="px-3 py-1.5 text-xs font-medium bg-slate-900 text-white rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40"
494
+ >
495
+ {saving ? (
496
+ <span className="inline-flex items-center gap-1">
497
+ <span className="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin" />
498
+ Saving
499
+ </span>
500
+ ) : pendingReset ? 'Save & Reset' : 'Save'}
501
+ </button>
502
+ </div>
503
+ </div>
504
+ </div>
505
+ </div>
506
+ );
507
+ }
@@ -3,8 +3,9 @@ import { saveCostThresholds } from '../../lib/api';
3
3
 
4
4
  const CEREMONIES = [
5
5
  { key: 'sponsor-call', label: 'Sponsor Call', desc: 'Wizard to define project mission, scope, and architecture' },
6
- { key: 'sprint-planning', label: 'Sprint Planning', desc: 'Generates epics, stories, and feature contexts' },
7
- { key: 'seed', label: 'Seed', desc: 'Populates initial epics and stories from a seed document' },
6
+ { key: 'sprint-planning', label: 'Sprint Planning', desc: 'Decomposes project scope into epics, stories, and contexts' },
7
+ { key: 'seed', label: 'Seed', desc: 'Decomposes stories into implementable tasks and subtasks' },
8
+ { key: 'run', label: 'Run', desc: 'Implements task code in worktree with AI-powered generation and validation' },
8
9
  ];
9
10
 
10
11
  function initState(costThresholds) {
@@ -2,10 +2,20 @@ import { useState } from 'react';
2
2
  import { ExternalLink } from 'lucide-react';
3
3
  import { saveModelPricing } from '../../lib/api';
4
4
 
5
+ const PROVIDERS = ['claude', 'gemini', 'openai', 'xiaomi'];
6
+ const PROVIDER_LABELS = { claude: 'Claude', gemini: 'Gemini', openai: 'OpenAI', xiaomi: 'Xiaomi MiMo' };
7
+ const PROVIDER_TAB_COLORS = {
8
+ claude: { active: 'border-orange-500 text-orange-700', badge: 'bg-orange-50 text-orange-700 border-orange-200' },
9
+ gemini: { active: 'border-blue-500 text-blue-700', badge: 'bg-blue-50 text-blue-700 border-blue-200' },
10
+ openai: { active: 'border-green-500 text-green-700', badge: 'bg-green-50 text-green-700 border-green-200' },
11
+ xiaomi: { active: 'border-purple-500 text-purple-700', badge: 'bg-purple-50 text-purple-700 border-purple-200' },
12
+ };
13
+
5
14
  const PROVIDER_COLORS = {
6
15
  claude: 'bg-orange-50 text-orange-700 border-orange-200',
7
16
  gemini: 'bg-blue-50 text-blue-700 border-blue-200',
8
17
  openai: 'bg-green-50 text-green-700 border-green-200',
18
+ xiaomi: 'bg-purple-50 text-purple-700 border-purple-200',
9
19
  };
10
20
 
11
21
  const UNIT_OPTIONS = [
@@ -17,8 +27,9 @@ function initState(models) {
17
27
  const state = {};
18
28
  for (const [modelId, info] of Object.entries(models)) {
19
29
  state[modelId] = {
20
- input: String(info.pricing?.input ?? ''),
21
- output: String(info.pricing?.output ?? ''),
30
+ input: String(info.pricing?.input ?? ''),
31
+ inputCached: String(info.pricing?.inputCached ?? ''),
32
+ output: String(info.pricing?.output ?? ''),
22
33
  unit: info.pricing?.unit ?? 'million',
23
34
  source: info.pricing?.source ?? '',
24
35
  lastUpdated: info.pricing?.lastUpdated ?? '',
@@ -38,7 +49,15 @@ export function ModelPricingTab({ settings, onSaved }) {
38
49
  const [pricing, setPricing] = useState(() => initState(models));
39
50
  const [status, setStatus] = useState(null); // null | 'saving' | 'saved' | 'error'
40
51
 
41
- const modelEntries = Object.entries(models);
52
+ // Determine which providers actually have models, preserve stable order
53
+ const availableProviders = PROVIDERS.filter(p =>
54
+ Object.values(models).some(m => m.provider === p)
55
+ );
56
+ const [activeProvider, setActiveProvider] = useState(() => availableProviders[0] || 'claude');
57
+
58
+ const modelEntries = Object.entries(models).filter(
59
+ ([, info]) => info.provider === activeProvider
60
+ );
42
61
 
43
62
  const update = (modelId, field, value) => {
44
63
  setPricing((prev) => ({
@@ -55,10 +74,11 @@ export function ModelPricingTab({ settings, onSaved }) {
55
74
  for (const [modelId, p] of Object.entries(pricing)) {
56
75
  payload[modelId] = {
57
76
  pricing: {
58
- input: parseFloat(p.input) || 0,
59
- output: parseFloat(p.output) || 0,
60
- unit: p.unit,
61
- source: p.source,
77
+ input: parseFloat(p.input) || 0,
78
+ inputCached: parseFloat(p.inputCached) || 0,
79
+ output: parseFloat(p.output) || 0,
80
+ unit: p.unit,
81
+ source: p.source,
62
82
  },
63
83
  };
64
84
  }
@@ -89,6 +109,36 @@ export function ModelPricingTab({ settings, onSaved }) {
89
109
  to estimate LLM spend. Prices are in <strong>USD</strong>.
90
110
  </p>
91
111
 
112
+ {/* Provider tabs */}
113
+ {availableProviders.length > 1 && (
114
+ <div className="flex border-b border-slate-200 -mx-5 px-5">
115
+ {availableProviders.map((p) => {
116
+ const colors = PROVIDER_TAB_COLORS[p] || { active: 'border-slate-900 text-slate-900' };
117
+ const isActive = p === activeProvider;
118
+ const count = Object.values(models).filter(m => m.provider === p).length;
119
+ return (
120
+ <button
121
+ key={p}
122
+ type="button"
123
+ onClick={() => setActiveProvider(p)}
124
+ className={`px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors ${
125
+ isActive
126
+ ? colors.active
127
+ : 'border-transparent text-slate-500 hover:text-slate-700'
128
+ }`}
129
+ >
130
+ {PROVIDER_LABELS[p] || p}
131
+ <span className={`ml-1.5 text-xs px-1.5 py-0.5 rounded-full border ${
132
+ isActive ? colors.badge : 'bg-slate-100 text-slate-400 border-slate-200'
133
+ }`}>
134
+ {count}
135
+ </span>
136
+ </button>
137
+ );
138
+ })}
139
+ </div>
140
+ )}
141
+
92
142
  <div className="flex flex-col gap-2">
93
143
  {modelEntries.map(([modelId, info]) => {
94
144
  const p = pricing[modelId] ?? { input: '', output: '', unit: 'million' };
@@ -140,6 +190,21 @@ export function ModelPricingTab({ settings, onSaved }) {
140
190
  ))}
141
191
  </select>
142
192
 
193
+ {/* Cache Read row */}
194
+ <label className="text-slate-600 font-medium">Cache Read</label>
195
+ <input
196
+ type="number"
197
+ min="0"
198
+ step="0.01"
199
+ value={p.inputCached}
200
+ onChange={(e) => update(modelId, 'inputCached', e.target.value)}
201
+ placeholder="0.00"
202
+ className="rounded-md border border-slate-300 px-2 py-1.5 text-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full max-w-[120px]"
203
+ />
204
+ <span className="text-slate-400 text-xs">
205
+ {UNIT_OPTIONS.find((o) => o.value === p.unit)?.label} · discounted
206
+ </span>
207
+
143
208
  {/* Output row */}
144
209
  <label className="text-slate-600 font-medium">Output</label>
145
210
  <input