@agile-vibe-coding/avc 0.1.1 → 0.3.1

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 (239) hide show
  1. package/cli/agent-loader.js +21 -0
  2. package/cli/agents/agent-selector.md +152 -0
  3. package/cli/agents/architecture-recommender.md +418 -0
  4. package/cli/agents/code-implementer.md +117 -0
  5. package/cli/agents/code-validator.md +80 -0
  6. package/cli/agents/context-reviewer-epic.md +101 -0
  7. package/cli/agents/context-reviewer-story.md +92 -0
  8. package/cli/agents/context-writer-epic.md +145 -0
  9. package/cli/agents/context-writer-story.md +111 -0
  10. package/cli/agents/database-deep-dive.md +470 -0
  11. package/cli/agents/database-recommender.md +634 -0
  12. package/cli/agents/doc-distributor.md +176 -0
  13. package/cli/agents/doc-writer-epic.md +42 -0
  14. package/cli/agents/doc-writer-story.md +43 -0
  15. package/cli/agents/documentation-updater.md +203 -0
  16. package/cli/agents/duplicate-detector.md +110 -0
  17. package/cli/agents/epic-story-decomposer.md +559 -0
  18. package/cli/agents/feature-context-generator.md +91 -0
  19. package/cli/agents/gap-checker-epic.md +52 -0
  20. package/cli/agents/impact-checker-story.md +51 -0
  21. package/cli/agents/migration-guide-generator.md +305 -0
  22. package/cli/agents/mission-scope-generator.md +143 -0
  23. package/cli/agents/mission-scope-validator.md +146 -0
  24. package/cli/agents/project-context-extractor.md +122 -0
  25. package/cli/agents/project-documentation-creator.json +226 -0
  26. package/cli/agents/project-documentation-creator.md +595 -0
  27. package/cli/agents/question-prefiller.md +269 -0
  28. package/cli/agents/refiner-epic.md +39 -0
  29. package/cli/agents/refiner-story.md +42 -0
  30. package/cli/agents/scaffolding-generator.md +99 -0
  31. package/cli/agents/seed-validator.md +71 -0
  32. package/cli/agents/story-doc-enricher.md +133 -0
  33. package/cli/agents/story-scope-reviewer.md +147 -0
  34. package/cli/agents/story-splitter.md +83 -0
  35. package/cli/agents/suggestion-business-analyst.md +88 -0
  36. package/cli/agents/suggestion-deployment-architect.md +263 -0
  37. package/cli/agents/suggestion-product-manager.md +129 -0
  38. package/cli/agents/suggestion-security-specialist.md +156 -0
  39. package/cli/agents/suggestion-technical-architect.md +269 -0
  40. package/cli/agents/suggestion-ux-researcher.md +93 -0
  41. package/cli/agents/task-subtask-decomposer.md +188 -0
  42. package/cli/agents/validator-documentation.json +183 -0
  43. package/cli/agents/validator-documentation.md +455 -0
  44. package/cli/agents/validator-selector.md +211 -0
  45. package/cli/ansi-colors.js +21 -0
  46. package/cli/api-reference-tool.js +368 -0
  47. package/cli/build-docs.js +29 -8
  48. package/cli/ceremony-history.js +369 -0
  49. package/cli/checks/catalog.json +76 -0
  50. package/cli/checks/code/quality.json +26 -0
  51. package/cli/checks/code/testing.json +14 -0
  52. package/cli/checks/code/traceability.json +26 -0
  53. package/cli/checks/cross-refs/epic.json +171 -0
  54. package/cli/checks/cross-refs/story.json +149 -0
  55. package/cli/checks/epic/api.json +114 -0
  56. package/cli/checks/epic/backend.json +126 -0
  57. package/cli/checks/epic/cloud.json +126 -0
  58. package/cli/checks/epic/data.json +102 -0
  59. package/cli/checks/epic/database.json +114 -0
  60. package/cli/checks/epic/developer.json +182 -0
  61. package/cli/checks/epic/devops.json +174 -0
  62. package/cli/checks/epic/frontend.json +162 -0
  63. package/cli/checks/epic/mobile.json +102 -0
  64. package/cli/checks/epic/qa.json +90 -0
  65. package/cli/checks/epic/security.json +184 -0
  66. package/cli/checks/epic/solution-architect.json +192 -0
  67. package/cli/checks/epic/test-architect.json +90 -0
  68. package/cli/checks/epic/ui.json +102 -0
  69. package/cli/checks/epic/ux.json +90 -0
  70. package/cli/checks/fixes/epic-fix-template.md +10 -0
  71. package/cli/checks/fixes/story-fix-template.md +10 -0
  72. package/cli/checks/story/api.json +186 -0
  73. package/cli/checks/story/backend.json +102 -0
  74. package/cli/checks/story/cloud.json +102 -0
  75. package/cli/checks/story/data.json +210 -0
  76. package/cli/checks/story/database.json +102 -0
  77. package/cli/checks/story/developer.json +168 -0
  78. package/cli/checks/story/devops.json +102 -0
  79. package/cli/checks/story/frontend.json +174 -0
  80. package/cli/checks/story/mobile.json +102 -0
  81. package/cli/checks/story/qa.json +210 -0
  82. package/cli/checks/story/security.json +198 -0
  83. package/cli/checks/story/solution-architect.json +230 -0
  84. package/cli/checks/story/test-architect.json +210 -0
  85. package/cli/checks/story/ui.json +102 -0
  86. package/cli/checks/story/ux.json +102 -0
  87. package/cli/coding-order.js +401 -0
  88. package/cli/command-logger.js +49 -12
  89. package/cli/components/static-output.js +63 -0
  90. package/cli/console-output-manager.js +94 -0
  91. package/cli/dependency-checker.js +72 -0
  92. package/cli/docs-sync.js +306 -0
  93. package/cli/epic-story-validator.js +659 -0
  94. package/cli/evaluation-prompts.js +1008 -0
  95. package/cli/execution-context.js +195 -0
  96. package/cli/generate-summary-table.js +340 -0
  97. package/cli/init-model-config.js +704 -0
  98. package/cli/init.js +1737 -278
  99. package/cli/kanban-server-manager.js +227 -0
  100. package/cli/llm-claude.js +150 -1
  101. package/cli/llm-gemini.js +109 -0
  102. package/cli/llm-local.js +493 -0
  103. package/cli/llm-mock.js +233 -0
  104. package/cli/llm-openai.js +454 -0
  105. package/cli/llm-provider.js +379 -3
  106. package/cli/llm-token-limits.js +211 -0
  107. package/cli/llm-verifier.js +662 -0
  108. package/cli/llm-xiaomi.js +143 -0
  109. package/cli/message-constants.js +49 -0
  110. package/cli/message-manager.js +334 -0
  111. package/cli/message-types.js +96 -0
  112. package/cli/messaging-api.js +291 -0
  113. package/cli/micro-check-fixer.js +335 -0
  114. package/cli/micro-check-runner.js +449 -0
  115. package/cli/micro-check-scorer.js +148 -0
  116. package/cli/micro-check-validator.js +538 -0
  117. package/cli/model-pricing.js +192 -0
  118. package/cli/model-query-engine.js +468 -0
  119. package/cli/model-recommendation-analyzer.js +495 -0
  120. package/cli/model-selector.js +270 -0
  121. package/cli/output-buffer.js +107 -0
  122. package/cli/process-manager.js +73 -2
  123. package/cli/prompt-logger.js +57 -0
  124. package/cli/repl-ink.js +4625 -1094
  125. package/cli/repl-old.js +3 -4
  126. package/cli/seed-processor.js +962 -0
  127. package/cli/sprint-planning-processor.js +4162 -0
  128. package/cli/template-processor.js +2149 -105
  129. package/cli/templates/project.md +25 -8
  130. package/cli/templates/vitepress-config.mts.template +5 -4
  131. package/cli/token-tracker.js +547 -0
  132. package/cli/tools/generate-story-validators.js +317 -0
  133. package/cli/tools/generate-validators.js +669 -0
  134. package/cli/update-checker.js +19 -17
  135. package/cli/update-notifier.js +4 -4
  136. package/cli/validation-router.js +667 -0
  137. package/cli/verification-tracker.js +563 -0
  138. package/cli/worktree-runner.js +654 -0
  139. package/kanban/README.md +386 -0
  140. package/kanban/client/README.md +205 -0
  141. package/kanban/client/components.json +20 -0
  142. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  143. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  144. package/kanban/client/dist/index.html +16 -0
  145. package/kanban/client/dist/vite.svg +1 -0
  146. package/kanban/client/index.html +15 -0
  147. package/kanban/client/package-lock.json +9442 -0
  148. package/kanban/client/package.json +44 -0
  149. package/kanban/client/postcss.config.js +6 -0
  150. package/kanban/client/public/vite.svg +1 -0
  151. package/kanban/client/src/App.jsx +651 -0
  152. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  153. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
  154. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
  155. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
  156. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  157. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  158. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
  159. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
  160. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  161. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
  162. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  163. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  164. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  165. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
  166. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
  167. package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
  168. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  169. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  170. package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
  171. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  172. package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
  173. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  174. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
  175. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  176. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  177. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  178. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  179. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  180. package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
  181. package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
  182. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
  183. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  184. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
  185. package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
  186. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  187. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  188. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  189. package/kanban/client/src/components/stats/CostModal.jsx +384 -0
  190. package/kanban/client/src/components/ui/badge.jsx +27 -0
  191. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  192. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  193. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  194. package/kanban/client/src/hooks/useGrouping.js +177 -0
  195. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  196. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  197. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  198. package/kanban/client/src/lib/api.js +515 -0
  199. package/kanban/client/src/lib/status-grouping.js +154 -0
  200. package/kanban/client/src/lib/utils.js +11 -0
  201. package/kanban/client/src/main.jsx +10 -0
  202. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  203. package/kanban/client/src/store/ceremonyStore.js +172 -0
  204. package/kanban/client/src/store/filterStore.js +201 -0
  205. package/kanban/client/src/store/kanbanStore.js +123 -0
  206. package/kanban/client/src/store/processStore.js +65 -0
  207. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  208. package/kanban/client/src/styles/globals.css +59 -0
  209. package/kanban/client/tailwind.config.js +77 -0
  210. package/kanban/client/vite.config.js +28 -0
  211. package/kanban/client/vitest.config.js +28 -0
  212. package/kanban/dev-start.sh +47 -0
  213. package/kanban/package.json +12 -0
  214. package/kanban/server/index.js +537 -0
  215. package/kanban/server/routes/ceremony.js +454 -0
  216. package/kanban/server/routes/costs.js +163 -0
  217. package/kanban/server/routes/openai-oauth.js +366 -0
  218. package/kanban/server/routes/processes.js +50 -0
  219. package/kanban/server/routes/settings.js +736 -0
  220. package/kanban/server/routes/websocket.js +281 -0
  221. package/kanban/server/routes/work-items.js +487 -0
  222. package/kanban/server/services/CeremonyService.js +1441 -0
  223. package/kanban/server/services/FileSystemScanner.js +95 -0
  224. package/kanban/server/services/FileWatcher.js +144 -0
  225. package/kanban/server/services/HierarchyBuilder.js +196 -0
  226. package/kanban/server/services/ProcessRegistry.js +122 -0
  227. package/kanban/server/services/TaskRunnerService.js +261 -0
  228. package/kanban/server/services/WorkItemReader.js +123 -0
  229. package/kanban/server/services/WorkItemRefineService.js +510 -0
  230. package/kanban/server/start.js +49 -0
  231. package/kanban/server/utils/kanban-logger.js +132 -0
  232. package/kanban/server/utils/markdown.js +91 -0
  233. package/kanban/server/utils/status-grouping.js +107 -0
  234. package/kanban/server/workers/run-task-worker.js +121 -0
  235. package/kanban/server/workers/seed-worker.js +94 -0
  236. package/kanban/server/workers/sponsor-call-worker.js +92 -0
  237. package/kanban/server/workers/sprint-planning-worker.js +212 -0
  238. package/package.json +19 -7
  239. package/cli/agents/documentation.md +0 -302
@@ -1,4 +1,4 @@
1
- You will be helping to create a comprehensive software application definition document. This document will serve as a foundational specification that can later be broken down into domain-specific details and related prompts for development agents.
1
+ You will be helping to create a comprehensive Project Brief. This document will serve as a foundational specification that can later be broken down into domain-specific details and related prompts for development agents.
2
2
 
3
3
  ## Project Information
4
4
 
@@ -14,10 +14,18 @@ You will be helping to create a comprehensive software application definition do
14
14
 
15
15
  {{INITIAL_SCOPE}}
16
16
 
17
+ ### Deployment Target
18
+
19
+ {{DEPLOYMENT_TARGET}}
20
+
17
21
  ### Technical Considerations
18
22
 
19
23
  {{TECHNICAL_CONSIDERATIONS}}
20
24
 
25
+ ### Technology Exclusions
26
+
27
+ {{TECHNICAL_EXCLUSIONS}}
28
+
21
29
  ### Security and Compliance
22
30
 
23
31
  {{SECURITY_AND_COMPLIANCE_REQUIREMENTS}}
@@ -39,24 +47,33 @@ In your scratchpad, consider:
39
47
  - What are the primary user journeys through the application?
40
48
  </scratchpad>
41
49
 
42
- Now, create a comprehensive application definition that includes the following sections:
50
+ Now, create a comprehensive Project Brief that includes the following sections:
43
51
 
44
52
  1. **Application Overview**: A clear, concise summary of what the application does and its primary purpose
45
53
 
46
54
  2. **Target Users and Stakeholders**: Identify the different types of users who will interact with this application, their roles, and their key needs
47
55
 
48
- 3. **Key Features and Functionality**: Describe the essential features organized by functional area or domain (e.g., user management, data processing, reporting, etc.)
56
+ 3. **Initial Scope**: Describe the essential features organized by functional area or domain (e.g., user management, data processing, reporting, etc.)
49
57
 
50
58
  4. **User Workflows**: Outline the primary user journeys or workflows through the application with step-by-step descriptions
51
59
 
52
- 5. **Technical Architecture**: Note any important technical requirements, constraints, or preferences (e.g., platform, technology stack, scalability needs, performance requirements)
60
+ 5. **UI/UX Design**: Describe the frontend technology approach, user interface requirements, and user experience considerations
61
+ - Frontend framework/technology selection (React, Vue, Angular, VitePress, Next.js, etc.)
62
+ - UI component library or design system approach (Material-UI, Tailwind, custom design system)
63
+ - Responsive design strategy (mobile-first, desktop-first, adaptive)
64
+ - Accessibility requirements (WCAG compliance level, screen reader support, keyboard navigation)
65
+ - User experience patterns (navigation structure, form design, loading states, error handling)
66
+ - Visual design considerations (branding, color scheme, typography)
67
+ - Internationalization needs (multi-language support, RTL layouts)
68
+
69
+ 6. **Technical Architecture**: Note any important backend and infrastructure requirements, constraints, or preferences (e.g., backend technology stack, database, hosting platform, scalability needs, performance requirements)
53
70
 
54
- 6. **Integration Requirements**: Identify any external systems, APIs, or data sources the application needs to connect with
71
+ 7. **Integration Requirements**: Identify any external systems, APIs, or data sources the application needs to connect with
55
72
 
56
- 7. **Security and Compliance**: Highlight any security, privacy, or regulatory compliance requirements
73
+ 8. **Security and Compliance**: Highlight any security, privacy, or regulatory compliance requirements
57
74
 
58
- 8. **Success Criteria**: Define what success looks like for this application
75
+ 9. **Success Criteria**: Define what success looks like for this application
59
76
 
60
- Your final output should be a complete, well-structured application definition document that provides enough detail to serve as a foundation for creating domain-specific specifications and work items for AI agents, while remaining at a high enough level to give a comprehensive view of the entire application.
77
+ Your final output should be a complete, well-structured Project Brief that provides enough detail to serve as a foundation for creating domain-specific specifications and work items for AI agents, while remaining at a high enough level to give a comprehensive view of the entire project.
61
78
 
62
79
  Use clear markdown formatting with headers, bullet points, and emphasis where appropriate. Do not include the scratchpad in your final output - only include the sections listed above.
@@ -4,6 +4,7 @@ export default defineConfig({
4
4
  title: '{{PROJECT_NAME}}',
5
5
  description: 'Project documentation powered by Agile Vibe Coding',
6
6
  base: '/',
7
+ ignoreDeadLinks: true,
7
8
 
8
9
  // Custom head tags for AVC documentation identification
9
10
  head: [
@@ -13,17 +14,17 @@ export default defineConfig({
13
14
 
14
15
  themeConfig: {
15
16
  nav: [
16
- { text: 'Home', link: '/' },
17
- { text: 'Epics', link: '/epics' }
17
+ { text: 'Home', link: '/' }
18
18
  ],
19
19
 
20
20
  sidebar: [
21
21
  {
22
- text: 'Project',
23
22
  items: [
24
- { text: 'Overview', link: '/' }
23
+ { text: 'Project Brief', link: '/' }
25
24
  ]
26
25
  }
26
+ // @@AVC-WORK-ITEMS-START@@
27
+ // @@AVC-WORK-ITEMS-END@@
27
28
  ],
28
29
 
29
30
  socialLinks: [
@@ -0,0 +1,547 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export class TokenTracker {
5
+ constructor(avcPath = path.join(process.cwd(), '.avc')) {
6
+ this.avcPath = avcPath;
7
+ this.tokenHistoryPath = path.join(avcPath, 'token-history.json');
8
+ this.data = null;
9
+ }
10
+
11
+ /**
12
+ * Initialize token history file if it doesn't exist
13
+ */
14
+ init() {
15
+ // Ensure .avc directory exists
16
+ if (!fs.existsSync(this.avcPath)) {
17
+ fs.mkdirSync(this.avcPath, { recursive: true });
18
+ }
19
+
20
+ if (!fs.existsSync(this.tokenHistoryPath)) {
21
+ const initialData = {
22
+ version: "1.0",
23
+ lastUpdated: new Date().toISOString(),
24
+ totals: {
25
+ daily: {},
26
+ weekly: {},
27
+ monthly: {},
28
+ allTime: {
29
+ input: 0,
30
+ output: 0,
31
+ total: 0,
32
+ executions: 0
33
+ }
34
+ }
35
+ };
36
+ this._writeData(initialData);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Load token history from disk
42
+ */
43
+ load() {
44
+ if (fs.existsSync(this.tokenHistoryPath)) {
45
+ this.data = JSON.parse(fs.readFileSync(this.tokenHistoryPath, 'utf8'));
46
+ } else {
47
+ this.init();
48
+ this.data = JSON.parse(fs.readFileSync(this.tokenHistoryPath, 'utf8'));
49
+ }
50
+ return this.data;
51
+ }
52
+
53
+ /**
54
+ * Write data to disk atomically
55
+ */
56
+ _writeData(data) {
57
+ // Ensure directory exists before writing (handles race conditions in tests)
58
+ if (!fs.existsSync(this.avcPath)) {
59
+ fs.mkdirSync(this.avcPath, { recursive: true });
60
+ }
61
+
62
+ try {
63
+ const tempPath = this.tokenHistoryPath + '.tmp';
64
+ fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8');
65
+ fs.renameSync(tempPath, this.tokenHistoryPath);
66
+ } catch (error) {
67
+ // Fallback to direct write if atomic write fails
68
+ try {
69
+ fs.writeFileSync(this.tokenHistoryPath, JSON.stringify(data, null, 2), 'utf8');
70
+ } catch (fallbackError) {
71
+ console.error(`Failed to write token history: ${fallbackError.message}`);
72
+ console.error(`Path: ${this.tokenHistoryPath}`);
73
+ throw fallbackError;
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Read avc.json configuration
80
+ */
81
+ readConfig() {
82
+ try {
83
+ const configPath = path.join(this.avcPath, 'avc.json');
84
+ if (fs.existsSync(configPath)) {
85
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
86
+ }
87
+ } catch (error) {
88
+ console.warn(`Could not read avc.json: ${error.message}`);
89
+ }
90
+ return { settings: {} };
91
+ }
92
+
93
+ /**
94
+ * Calculate cost for token usage based on model pricing
95
+ * @param {number} inputTokens - Input tokens consumed (includes cached tokens)
96
+ * @param {number} outputTokens - Output tokens consumed
97
+ * @param {string} modelId - Model identifier (e.g., 'claude-sonnet-4-5-20250929')
98
+ * @param {number} cachedTokens - Subset of inputTokens served from cache (billed at inputCached rate)
99
+ * @returns {Object} Cost breakdown { input, output, total, saved }
100
+ */
101
+ calculateCost(inputTokens, outputTokens, modelId, cachedTokens = 0) {
102
+ const config = this.readConfig();
103
+ const modelConfig = config.settings?.models?.[modelId];
104
+
105
+ if (!modelConfig || !modelConfig.pricing) {
106
+ // Model not found or no pricing - return zero cost
107
+ return { input: 0, output: 0, total: 0, saved: 0 };
108
+ }
109
+
110
+ const pricing = modelConfig.pricing;
111
+ const divisor = pricing.unit === 'million' ? 1_000_000 : 1_000;
112
+
113
+ // Cached tokens billed at reduced rate; fall back to full input rate if not configured
114
+ const cachedRate = pricing.inputCached ?? pricing.input;
115
+ const nonCachedInput = Math.max(0, inputTokens - cachedTokens);
116
+
117
+ const inputCost = (pricing.input * nonCachedInput + cachedRate * cachedTokens) / divisor;
118
+ const outputCost = (pricing.output * outputTokens) / divisor;
119
+ // Savings vs. paying full rate for cached tokens
120
+ const saved = ((pricing.input - cachedRate) * cachedTokens) / divisor;
121
+
122
+ return {
123
+ input: inputCost,
124
+ output: outputCost,
125
+ total: inputCost + outputCost,
126
+ saved,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Add tokens from a completed ceremony execution
132
+ * @param {string} ceremonyType - e.g., 'sponsor-call'
133
+ * @param {Object} tokens - { input, output }
134
+ * @param {string} modelId - Model identifier (optional)
135
+ */
136
+ addExecution(ceremonyType, tokens, modelId = null) {
137
+ try {
138
+ this.load();
139
+
140
+ const now = new Date();
141
+ const dateKey = now.toISOString().split('T')[0]; // YYYY-MM-DD
142
+ const weekKey = this._getWeekKey(now); // YYYY-Www
143
+ const monthKey = dateKey.substring(0, 7); // YYYY-MM
144
+ const timestamp = now.toISOString();
145
+
146
+ const tokenData = {
147
+ input: tokens.input || 0,
148
+ output: tokens.output || 0,
149
+ cached: tokens.cached || 0,
150
+ cacheWrite: tokens.cacheWrite || 0,
151
+ total: (tokens.input || 0) + (tokens.output || 0),
152
+ provider: tokens.provider || 'unknown',
153
+ model: tokens.model || modelId || 'unknown'
154
+ };
155
+
156
+ // Calculate cost: prefer per-model pricing from avc.json, fall back to provider estimate
157
+ // Skip cost calculation for OAuth calls (flat-rate subscription — no per-token billing)
158
+ let costData = null;
159
+ if (!tokens.skipCost) {
160
+ const effectiveModelId = tokens.model || modelId;
161
+ if (effectiveModelId) {
162
+ costData = this.calculateCost(tokenData.input, tokenData.output, effectiveModelId, tokenData.cached);
163
+ }
164
+ // If no per-model pricing configured but provider sent a pre-computed estimate, use it
165
+ if ((!costData || costData.total === 0) && tokens.estimatedCost) {
166
+ costData = { input: 0, output: 0, total: tokens.estimatedCost };
167
+ }
168
+ }
169
+
170
+ console.log(` → Tracking tokens for ${ceremonyType}: ${tokenData.input} input, ${tokenData.output} output (${tokenData.provider})`);
171
+ if (costData && costData.total > 0) {
172
+ console.log(` → Estimated cost: $${costData.total.toFixed(4)} (${effectiveModelId})`);
173
+ }
174
+
175
+ // Update totals (global)
176
+ this._updateAggregation(this.data.totals, dateKey, weekKey, monthKey, tokenData, timestamp, costData);
177
+
178
+ // Update ceremony-specific aggregations
179
+ if (!this.data[ceremonyType]) {
180
+ this.data[ceremonyType] = {
181
+ daily: {},
182
+ weekly: {},
183
+ monthly: {},
184
+ allTime: {
185
+ input: 0,
186
+ output: 0,
187
+ total: 0,
188
+ executions: 0,
189
+ cost: { input: 0, output: 0, total: 0 }
190
+ }
191
+ };
192
+ }
193
+ this._updateAggregation(this.data[ceremonyType], dateKey, weekKey, monthKey, tokenData, timestamp, costData);
194
+
195
+ // Update lastUpdated
196
+ this.data.lastUpdated = timestamp;
197
+
198
+ // Write to disk
199
+ this._writeData(this.data);
200
+ console.log(` → Token history saved to ${this.tokenHistoryPath}`);
201
+ } catch (error) {
202
+ console.error(`Failed to add token execution: ${error.message}`);
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Record tokens from a single LLM API call without incrementing the executions counter.
209
+ * Called after every individual LLM call so tokens are persisted crash-safely.
210
+ * @param {string} ceremonyType - e.g., 'sprint-planning'
211
+ * @param {Object} delta - { input, output, provider, model, estimatedCost? }
212
+ */
213
+ addIncremental(ceremonyType, delta) {
214
+ if (!delta.input && !delta.output) return;
215
+ try {
216
+ this.load();
217
+ const now = new Date();
218
+ const dateKey = now.toISOString().split('T')[0];
219
+ const weekKey = this._getWeekKey(now);
220
+ const monthKey = dateKey.substring(0, 7);
221
+ const timestamp = now.toISOString();
222
+
223
+ const tokenData = {
224
+ input: delta.input || 0,
225
+ output: delta.output || 0,
226
+ cached: delta.cached || 0,
227
+ cacheWrite: delta.cacheWrite || 0,
228
+ total: (delta.input || 0) + (delta.output || 0),
229
+ provider: delta.provider || 'unknown',
230
+ model: delta.model || 'unknown'
231
+ };
232
+
233
+ let costData = null;
234
+ if (!delta.skipCost) {
235
+ if (delta.model) {
236
+ costData = this.calculateCost(tokenData.input, tokenData.output, delta.model, tokenData.cached);
237
+ }
238
+ if ((!costData || costData.total === 0) && delta.estimatedCost) {
239
+ costData = { input: 0, output: 0, total: delta.estimatedCost };
240
+ }
241
+ }
242
+
243
+ if (!this.data[ceremonyType]) {
244
+ this.data[ceremonyType] = {
245
+ daily: {}, weekly: {}, monthly: {},
246
+ allTime: { input: 0, output: 0, total: 0, executions: 0, cost: { input: 0, output: 0, total: 0 } }
247
+ };
248
+ }
249
+
250
+ this._updateAggregation(this.data.totals, dateKey, weekKey, monthKey, tokenData, timestamp, costData, false);
251
+ this._updateAggregation(this.data[ceremonyType], dateKey, weekKey, monthKey, tokenData, timestamp, costData, false);
252
+ this.data.lastUpdated = timestamp;
253
+ this._writeData(this.data);
254
+ } catch (error) {
255
+ console.error(`addIncremental failed: ${error.message}`);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Increment executions counter to mark a completed run.
261
+ * Call this once per ceremony run after all LLM calls are done.
262
+ * Tokens are already persisted by addIncremental() — this only bumps the count.
263
+ * @param {string} ceremonyType - e.g., 'sprint-planning'
264
+ */
265
+ finalizeRun(ceremonyType) {
266
+ try {
267
+ this.load();
268
+ const now = new Date();
269
+ const dateKey = now.toISOString().split('T')[0];
270
+ const weekKey = this._getWeekKey(now);
271
+ const monthKey = dateKey.substring(0, 7);
272
+ const timestamp = now.toISOString();
273
+
274
+ if (!this.data[ceremonyType]) {
275
+ this.data[ceremonyType] = {
276
+ daily: {}, weekly: {}, monthly: {},
277
+ allTime: { input: 0, output: 0, total: 0, executions: 0, cost: { input: 0, output: 0, total: 0 } }
278
+ };
279
+ }
280
+
281
+ // Zero-token update — only increments executions counters
282
+ const zero = { input: 0, output: 0, total: 0, provider: 'unknown', model: 'unknown' };
283
+ this._updateAggregation(this.data.totals, dateKey, weekKey, monthKey, zero, timestamp, null, true);
284
+ this._updateAggregation(this.data[ceremonyType], dateKey, weekKey, monthKey, zero, timestamp, null, true);
285
+ this.data.lastUpdated = timestamp;
286
+ this._writeData(this.data);
287
+ console.log(` → Ceremony run finalized for ${ceremonyType}`);
288
+ } catch (error) {
289
+ console.error(`finalizeRun failed: ${error.message}`);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Update aggregations for a given scope (totals or ceremony-type)
295
+ * @param {boolean} incExec - Whether to increment executions counters (default true)
296
+ */
297
+ _updateAggregation(scope, dateKey, weekKey, monthKey, tokenData, timestamp, costData = null, incExec = true) {
298
+ // Update daily
299
+ if (!scope.daily[dateKey]) {
300
+ scope.daily[dateKey] = {
301
+ date: dateKey,
302
+ input: 0,
303
+ output: 0,
304
+ cached: 0,
305
+ total: 0,
306
+ executions: 0,
307
+ cost: { input: 0, output: 0, total: 0, saved: 0 }
308
+ };
309
+ }
310
+ scope.daily[dateKey].input += tokenData.input;
311
+ scope.daily[dateKey].output += tokenData.output;
312
+ scope.daily[dateKey].cached = (scope.daily[dateKey].cached || 0) + (tokenData.cached || 0);
313
+ scope.daily[dateKey].total += tokenData.total;
314
+ if (incExec) scope.daily[dateKey].executions++;
315
+ if (costData) {
316
+ scope.daily[dateKey].cost.input += costData.input;
317
+ scope.daily[dateKey].cost.output += costData.output;
318
+ scope.daily[dateKey].cost.total += costData.total;
319
+ scope.daily[dateKey].cost.saved = (scope.daily[dateKey].cost.saved || 0) + (costData.saved || 0);
320
+ }
321
+
322
+ // Update weekly
323
+ if (!scope.weekly[weekKey]) {
324
+ scope.weekly[weekKey] = {
325
+ week: weekKey,
326
+ input: 0,
327
+ output: 0,
328
+ cached: 0,
329
+ total: 0,
330
+ executions: 0,
331
+ cost: { input: 0, output: 0, total: 0, saved: 0 }
332
+ };
333
+ }
334
+ scope.weekly[weekKey].input += tokenData.input;
335
+ scope.weekly[weekKey].output += tokenData.output;
336
+ scope.weekly[weekKey].cached = (scope.weekly[weekKey].cached || 0) + (tokenData.cached || 0);
337
+ scope.weekly[weekKey].total += tokenData.total;
338
+ if (incExec) scope.weekly[weekKey].executions++;
339
+ if (costData) {
340
+ scope.weekly[weekKey].cost.input += costData.input;
341
+ scope.weekly[weekKey].cost.output += costData.output;
342
+ scope.weekly[weekKey].cost.total += costData.total;
343
+ scope.weekly[weekKey].cost.saved = (scope.weekly[weekKey].cost.saved || 0) + (costData.saved || 0);
344
+ }
345
+
346
+ // Update monthly
347
+ if (!scope.monthly[monthKey]) {
348
+ scope.monthly[monthKey] = {
349
+ month: monthKey,
350
+ input: 0,
351
+ output: 0,
352
+ cached: 0,
353
+ total: 0,
354
+ executions: 0,
355
+ cost: { input: 0, output: 0, total: 0, saved: 0 }
356
+ };
357
+ }
358
+ scope.monthly[monthKey].input += tokenData.input;
359
+ scope.monthly[monthKey].output += tokenData.output;
360
+ scope.monthly[monthKey].cached = (scope.monthly[monthKey].cached || 0) + (tokenData.cached || 0);
361
+ scope.monthly[monthKey].total += tokenData.total;
362
+ if (incExec) scope.monthly[monthKey].executions++;
363
+ if (costData) {
364
+ scope.monthly[monthKey].cost.input += costData.input;
365
+ scope.monthly[monthKey].cost.output += costData.output;
366
+ scope.monthly[monthKey].cost.total += costData.total;
367
+ scope.monthly[monthKey].cost.saved = (scope.monthly[monthKey].cost.saved || 0) + (costData.saved || 0);
368
+ }
369
+
370
+ // Update all-time
371
+ scope.allTime.input += tokenData.input;
372
+ scope.allTime.output += tokenData.output;
373
+ scope.allTime.cached = (scope.allTime.cached || 0) + (tokenData.cached || 0);
374
+ scope.allTime.total += tokenData.total;
375
+ if (incExec) scope.allTime.executions++;
376
+
377
+ // Initialize cost tracking if not present
378
+ if (!scope.allTime.cost) {
379
+ scope.allTime.cost = { input: 0, output: 0, total: 0, saved: 0 };
380
+ }
381
+ if (costData) {
382
+ scope.allTime.cost.input += costData.input;
383
+ scope.allTime.cost.output += costData.output;
384
+ scope.allTime.cost.total += costData.total;
385
+ scope.allTime.cost.saved = (scope.allTime.cost.saved || 0) + (costData.saved || 0);
386
+ }
387
+
388
+ if (!scope.allTime.firstExecution) {
389
+ scope.allTime.firstExecution = timestamp;
390
+ }
391
+ scope.allTime.lastExecution = timestamp;
392
+
393
+ // Cleanup old rolling windows
394
+ this._cleanupRollingWindows();
395
+ }
396
+
397
+ /**
398
+ * Get ISO week key (YYYY-Www)
399
+ */
400
+ _getWeekKey(date) {
401
+ const tempDate = new Date(date.getTime());
402
+ tempDate.setHours(0, 0, 0, 0);
403
+ tempDate.setDate(tempDate.getDate() + 3 - (tempDate.getDay() + 6) % 7);
404
+ const week1 = new Date(tempDate.getFullYear(), 0, 4);
405
+ const weekNum = 1 + Math.round(((tempDate - week1) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7);
406
+ return `${tempDate.getFullYear()}-W${String(weekNum).padStart(2, '0')}`;
407
+ }
408
+
409
+ /**
410
+ * Cleanup old entries from rolling windows
411
+ */
412
+ _cleanupRollingWindows() {
413
+ const now = new Date();
414
+
415
+ // Keep last 31 days
416
+ const cutoffDaily = new Date(now);
417
+ cutoffDaily.setDate(cutoffDaily.getDate() - 31);
418
+
419
+ // Clean totals.daily
420
+ Object.keys(this.data.totals.daily).forEach(key => {
421
+ if (new Date(key) < cutoffDaily) {
422
+ delete this.data.totals.daily[key];
423
+ }
424
+ });
425
+
426
+ // Clean ceremony-type daily
427
+ Object.keys(this.data).forEach(key => {
428
+ if (key !== 'version' && key !== 'lastUpdated' && key !== 'totals' && this.data[key].daily) {
429
+ Object.keys(this.data[key].daily).forEach(dateKey => {
430
+ if (new Date(dateKey) < cutoffDaily) {
431
+ delete this.data[key].daily[dateKey];
432
+ }
433
+ });
434
+ }
435
+ });
436
+
437
+ // Keep last 12 weeks
438
+ const cutoffWeekly = new Date(now);
439
+ cutoffWeekly.setDate(cutoffWeekly.getDate() - 84);
440
+
441
+ Object.keys(this.data.totals.weekly).forEach(key => {
442
+ const [year, week] = key.split('-W');
443
+ const weekDate = this._getDateFromWeek(parseInt(year), parseInt(week));
444
+ if (weekDate < cutoffWeekly) {
445
+ delete this.data.totals.weekly[key];
446
+ }
447
+ });
448
+
449
+ Object.keys(this.data).forEach(key => {
450
+ if (key !== 'version' && key !== 'lastUpdated' && key !== 'totals' && this.data[key].weekly) {
451
+ Object.keys(this.data[key].weekly).forEach(weekKey => {
452
+ const [year, week] = weekKey.split('-W');
453
+ const weekDate = this._getDateFromWeek(parseInt(year), parseInt(week));
454
+ if (weekDate < cutoffWeekly) {
455
+ delete this.data[key].weekly[weekKey];
456
+ }
457
+ });
458
+ }
459
+ });
460
+
461
+ // Keep last 12 months
462
+ const cutoffMonthly = new Date(now);
463
+ cutoffMonthly.setMonth(cutoffMonthly.getMonth() - 12);
464
+
465
+ Object.keys(this.data.totals.monthly).forEach(key => {
466
+ if (new Date(key + '-01') < cutoffMonthly) {
467
+ delete this.data.totals.monthly[key];
468
+ }
469
+ });
470
+
471
+ Object.keys(this.data).forEach(key => {
472
+ if (key !== 'version' && key !== 'lastUpdated' && key !== 'totals' && this.data[key].monthly) {
473
+ Object.keys(this.data[key].monthly).forEach(monthKey => {
474
+ if (new Date(monthKey + '-01') < cutoffMonthly) {
475
+ delete this.data[key].monthly[monthKey];
476
+ }
477
+ });
478
+ }
479
+ });
480
+ }
481
+
482
+ /**
483
+ * Get date from ISO week
484
+ */
485
+ _getDateFromWeek(year, week) {
486
+ const simple = new Date(year, 0, 1 + (week - 1) * 7);
487
+ const dow = simple.getDay();
488
+ const ISOweekStart = simple;
489
+ if (dow <= 4) ISOweekStart.setDate(simple.getDate() - simple.getDay() + 1);
490
+ else ISOweekStart.setDate(simple.getDate() + 8 - simple.getDay());
491
+ return ISOweekStart;
492
+ }
493
+
494
+ // Query methods
495
+ getTotalsToday() {
496
+ this.load();
497
+ const today = new Date().toISOString().split('T')[0];
498
+ return this.data.totals.daily[today] || { date: today, input: 0, output: 0, total: 0, executions: 0 };
499
+ }
500
+
501
+ getTotalsThisWeek() {
502
+ this.load();
503
+ const week = this._getWeekKey(new Date());
504
+ return this.data.totals.weekly[week] || { week, input: 0, output: 0, total: 0, executions: 0 };
505
+ }
506
+
507
+ getTotalsThisMonth() {
508
+ this.load();
509
+ const month = new Date().toISOString().substring(0, 7);
510
+ return this.data.totals.monthly[month] || { month, input: 0, output: 0, total: 0, executions: 0 };
511
+ }
512
+
513
+ getTotalsAllTime() {
514
+ this.load();
515
+ return this.data.totals.allTime;
516
+ }
517
+
518
+ getCeremonyToday(ceremonyType) {
519
+ this.load();
520
+ const today = new Date().toISOString().split('T')[0];
521
+ return this.data[ceremonyType]?.daily[today] || { date: today, input: 0, output: 0, total: 0, executions: 0 };
522
+ }
523
+
524
+ getCeremonyThisWeek(ceremonyType) {
525
+ this.load();
526
+ const week = this._getWeekKey(new Date());
527
+ return this.data[ceremonyType]?.weekly[week] || { week, input: 0, output: 0, total: 0, executions: 0 };
528
+ }
529
+
530
+ getCeremonyThisMonth(ceremonyType) {
531
+ this.load();
532
+ const month = new Date().toISOString().substring(0, 7);
533
+ return this.data[ceremonyType]?.monthly[month] || { month, input: 0, output: 0, total: 0, executions: 0 };
534
+ }
535
+
536
+ getCeremonyAllTime(ceremonyType) {
537
+ this.load();
538
+ return this.data[ceremonyType]?.allTime || { input: 0, output: 0, total: 0, executions: 0 };
539
+ }
540
+
541
+ getAllCeremonyTypes() {
542
+ this.load();
543
+ return Object.keys(this.data).filter(key =>
544
+ key !== 'version' && key !== 'lastUpdated' && key !== 'totals'
545
+ );
546
+ }
547
+ }