@knpkv/jira-clockify 1.3.0 → 1.4.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 (242) hide show
  1. package/README.md +245 -1
  2. package/dist/package.json +36 -12
  3. package/dist/src/agent/agentSettings.d.ts +18 -0
  4. package/dist/src/agent/agentSettings.d.ts.map +1 -0
  5. package/dist/src/agent/agentSettings.js +23 -0
  6. package/dist/src/agent/agentSettings.js.map +1 -0
  7. package/dist/src/agent/schedule.d.ts +20 -0
  8. package/dist/src/agent/schedule.d.ts.map +1 -0
  9. package/dist/src/agent/schedule.js +189 -0
  10. package/dist/src/agent/schedule.js.map +1 -0
  11. package/dist/src/agent/sessions.d.ts +427 -0
  12. package/dist/src/agent/sessions.d.ts.map +1 -0
  13. package/dist/src/agent/sessions.js +868 -0
  14. package/dist/src/agent/sessions.js.map +1 -0
  15. package/dist/src/agent/sourceConsumption.d.ts +79 -0
  16. package/dist/src/agent/sourceConsumption.d.ts.map +1 -0
  17. package/dist/src/agent/sourceConsumption.js +191 -0
  18. package/dist/src/agent/sourceConsumption.js.map +1 -0
  19. package/dist/src/agent/watch.d.ts +76 -0
  20. package/dist/src/agent/watch.d.ts.map +1 -0
  21. package/dist/src/agent/watch.js +45 -0
  22. package/dist/src/agent/watch.js.map +1 -0
  23. package/dist/src/agent/writePlanning.d.ts +154 -0
  24. package/dist/src/agent/writePlanning.d.ts.map +1 -0
  25. package/dist/src/agent/writePlanning.js +210 -0
  26. package/dist/src/agent/writePlanning.js.map +1 -0
  27. package/dist/src/bin.d.ts +3 -0
  28. package/dist/src/bin.d.ts.map +1 -0
  29. package/dist/src/bin.js +9 -3
  30. package/dist/src/bin.js.map +1 -1
  31. package/dist/src/cli/agentWrite.d.ts +164 -0
  32. package/dist/src/cli/agentWrite.d.ts.map +1 -0
  33. package/dist/src/cli/agentWrite.js +362 -0
  34. package/dist/src/cli/agentWrite.js.map +1 -0
  35. package/dist/src/cli/auth.d.ts +8 -0
  36. package/dist/src/cli/auth.d.ts.map +1 -0
  37. package/dist/src/cli/auth.js +10 -10
  38. package/dist/src/cli/auth.js.map +1 -1
  39. package/dist/src/cli/calendar.d.ts +42 -0
  40. package/dist/src/cli/calendar.d.ts.map +1 -0
  41. package/dist/src/cli/calendar.js +189 -0
  42. package/dist/src/cli/calendar.js.map +1 -0
  43. package/dist/src/cli/config.d.ts +12 -0
  44. package/dist/src/cli/config.d.ts.map +1 -0
  45. package/dist/src/cli/config.js +163 -21
  46. package/dist/src/cli/config.js.map +1 -1
  47. package/dist/src/cli/fetchTicket.d.ts +51 -0
  48. package/dist/src/cli/fetchTicket.d.ts.map +1 -0
  49. package/dist/src/cli/fetchTicket.js +18 -2
  50. package/dist/src/cli/fetchTicket.js.map +1 -1
  51. package/dist/src/cli/fuzzySelect.d.ts +24 -0
  52. package/dist/src/cli/fuzzySelect.d.ts.map +1 -0
  53. package/dist/src/cli/layers.d.ts +40 -0
  54. package/dist/src/cli/layers.d.ts.map +1 -0
  55. package/dist/src/cli/layers.js +57 -13
  56. package/dist/src/cli/layers.js.map +1 -1
  57. package/dist/src/cli/list.d.ts +8 -0
  58. package/dist/src/cli/list.d.ts.map +1 -0
  59. package/dist/src/cli/list.js +2 -2
  60. package/dist/src/cli/list.js.map +1 -1
  61. package/dist/src/cli/reconcile.d.ts +129 -0
  62. package/dist/src/cli/reconcile.d.ts.map +1 -0
  63. package/dist/src/cli/reconcile.js +635 -46
  64. package/dist/src/cli/reconcile.js.map +1 -1
  65. package/dist/src/cli/root.d.ts +15 -0
  66. package/dist/src/cli/root.d.ts.map +1 -0
  67. package/dist/src/cli/root.js +4 -3
  68. package/dist/src/cli/root.js.map +1 -1
  69. package/dist/src/cli/runtimeFailure.d.ts +10 -0
  70. package/dist/src/cli/runtimeFailure.d.ts.map +1 -0
  71. package/dist/src/cli/runtimeFailure.js +17 -0
  72. package/dist/src/cli/runtimeFailure.js.map +1 -0
  73. package/dist/src/cli/setup.d.ts +10 -0
  74. package/dist/src/cli/setup.d.ts.map +1 -0
  75. package/dist/src/cli/setup.js +7 -7
  76. package/dist/src/cli/setup.js.map +1 -1
  77. package/dist/src/cli/timer/discard.d.ts +4 -0
  78. package/dist/src/cli/timer/discard.d.ts.map +1 -0
  79. package/dist/src/cli/timer/discard.js +2 -2
  80. package/dist/src/cli/timer/discard.js.map +1 -1
  81. package/dist/src/cli/timer/edit.d.ts +22 -0
  82. package/dist/src/cli/timer/edit.d.ts.map +1 -0
  83. package/dist/src/cli/timer/edit.js +62 -42
  84. package/dist/src/cli/timer/edit.js.map +1 -1
  85. package/dist/src/cli/timer/index.d.ts +12 -0
  86. package/dist/src/cli/timer/index.d.ts.map +1 -0
  87. package/dist/src/cli/timer/log.d.ts +16 -0
  88. package/dist/src/cli/timer/log.d.ts.map +1 -0
  89. package/dist/src/cli/timer/log.js +6 -6
  90. package/dist/src/cli/timer/log.js.map +1 -1
  91. package/dist/src/cli/timer/start.d.ts +21 -0
  92. package/dist/src/cli/timer/start.d.ts.map +1 -0
  93. package/dist/src/cli/timer/start.js +9 -9
  94. package/dist/src/cli/timer/start.js.map +1 -1
  95. package/dist/src/cli/timer/status.d.ts +22 -0
  96. package/dist/src/cli/timer/status.d.ts.map +1 -0
  97. package/dist/src/cli/timer/status.js +3 -3
  98. package/dist/src/cli/timer/status.js.map +1 -1
  99. package/dist/src/cli/timer/stop.d.ts +48 -0
  100. package/dist/src/cli/timer/stop.d.ts.map +1 -0
  101. package/dist/src/cli/timer/stop.js +19 -19
  102. package/dist/src/cli/timer/stop.js.map +1 -1
  103. package/dist/src/cli/timer.d.ts +11 -0
  104. package/dist/src/cli/timer.d.ts.map +1 -0
  105. package/dist/src/cli/timer.js +1 -1
  106. package/dist/src/cli/timer.js.map +1 -1
  107. package/dist/src/cli/watch.d.ts +54 -0
  108. package/dist/src/cli/watch.d.ts.map +1 -0
  109. package/dist/src/cli/watch.js +412 -0
  110. package/dist/src/cli/watch.js.map +1 -0
  111. package/dist/src/cli/watchLease.d.ts +99 -0
  112. package/dist/src/cli/watchLease.d.ts.map +1 -0
  113. package/dist/src/cli/watchLease.js +204 -0
  114. package/dist/src/cli/watchLease.js.map +1 -0
  115. package/dist/src/cli/writerGuard.d.ts +37 -0
  116. package/dist/src/cli/writerGuard.d.ts.map +1 -0
  117. package/dist/src/cli/writerGuard.js +92 -0
  118. package/dist/src/cli/writerGuard.js.map +1 -0
  119. package/dist/src/index.d.ts +46 -0
  120. package/dist/src/index.d.ts.map +1 -0
  121. package/dist/src/index.js +46 -0
  122. package/dist/src/index.js.map +1 -0
  123. package/dist/src/main.d.ts +4 -0
  124. package/dist/src/main.d.ts.map +1 -0
  125. package/dist/src/services/AgentSessionReader.d.ts +121 -0
  126. package/dist/src/services/AgentSessionReader.d.ts.map +1 -0
  127. package/dist/src/services/AgentSessionReader.js +392 -0
  128. package/dist/src/services/AgentSessionReader.js.map +1 -0
  129. package/dist/src/services/ClockifyAuth.d.ts +47 -0
  130. package/dist/src/services/ClockifyAuth.d.ts.map +1 -0
  131. package/dist/src/services/ClockifyAuth.js +2 -2
  132. package/dist/src/services/ClockifyAuth.js.map +1 -1
  133. package/dist/src/services/CodexTranscript.d.ts +112 -0
  134. package/dist/src/services/CodexTranscript.d.ts.map +1 -0
  135. package/dist/src/services/CodexTranscript.js +96 -0
  136. package/dist/src/services/CodexTranscript.js.map +1 -0
  137. package/dist/src/services/ConfigService.d.ts +99 -0
  138. package/dist/src/services/ConfigService.d.ts.map +1 -0
  139. package/dist/src/services/ConfigService.js +79 -7
  140. package/dist/src/services/ConfigService.js.map +1 -1
  141. package/dist/src/services/HomeDirectory.d.ts +17 -0
  142. package/dist/src/services/HomeDirectory.d.ts.map +1 -0
  143. package/dist/src/services/HomeDirectory.js +1 -1
  144. package/dist/src/services/IssueFacts.d.ts +127 -0
  145. package/dist/src/services/IssueFacts.d.ts.map +1 -0
  146. package/dist/src/services/IssueFacts.js +277 -0
  147. package/dist/src/services/IssueFacts.js.map +1 -0
  148. package/dist/src/services/ProviderDeletion.d.ts +19 -0
  149. package/dist/src/services/ProviderDeletion.d.ts.map +1 -0
  150. package/dist/src/services/ProviderDeletion.js +37 -0
  151. package/dist/src/services/ProviderDeletion.js.map +1 -0
  152. package/dist/src/services/ReconcileService.d.ts +378 -0
  153. package/dist/src/services/ReconcileService.d.ts.map +1 -0
  154. package/dist/src/services/ReconcileService.js +1270 -79
  155. package/dist/src/services/ReconcileService.js.map +1 -1
  156. package/dist/src/services/SavedEntries.d.ts +40 -0
  157. package/dist/src/services/SavedEntries.d.ts.map +1 -0
  158. package/dist/src/services/SavedEntries.js +209 -0
  159. package/dist/src/services/SavedEntries.js.map +1 -0
  160. package/dist/src/services/SessionAttributor.d.ts +114 -0
  161. package/dist/src/services/SessionAttributor.d.ts.map +1 -0
  162. package/dist/src/services/SessionAttributor.js +240 -0
  163. package/dist/src/services/SessionAttributor.js.map +1 -0
  164. package/dist/src/services/SourceLedger.d.ts +104 -0
  165. package/dist/src/services/SourceLedger.d.ts.map +1 -0
  166. package/dist/src/services/SourceLedger.js +302 -0
  167. package/dist/src/services/SourceLedger.js.map +1 -0
  168. package/dist/src/services/StateWriter.d.ts +39 -0
  169. package/dist/src/services/StateWriter.d.ts.map +1 -0
  170. package/dist/src/services/TicketService.d.ts +63 -0
  171. package/dist/src/services/TicketService.d.ts.map +1 -0
  172. package/dist/src/services/TimerService.d.ts +145 -0
  173. package/dist/src/services/TimerService.d.ts.map +1 -0
  174. package/dist/src/services/TimerService.js +113 -80
  175. package/dist/src/services/TimerService.js.map +1 -1
  176. package/dist/src/services/internal/JiraWorklogPost.d.ts +7 -0
  177. package/dist/src/services/internal/JiraWorklogPost.d.ts.map +1 -0
  178. package/dist/src/services/internal/JiraWorklogPost.js +32 -0
  179. package/dist/src/services/internal/JiraWorklogPost.js.map +1 -0
  180. package/dist/src/testing/fakeHeadless.d.ts +318 -0
  181. package/dist/src/testing/fakeHeadless.d.ts.map +1 -0
  182. package/dist/src/testing/fakeHeadless.js +1149 -0
  183. package/dist/src/testing/fakeHeadless.js.map +1 -0
  184. package/dist/src/tui/App.d.ts +6 -0
  185. package/dist/src/tui/App.d.ts.map +1 -0
  186. package/dist/src/tui/App.js +1 -1
  187. package/dist/src/tui/App.js.map +1 -1
  188. package/dist/src/tui/atoms/runtime.d.ts +8 -0
  189. package/dist/src/tui/atoms/runtime.d.ts.map +1 -0
  190. package/dist/src/tui/atoms/runtime.js +1 -1
  191. package/dist/src/tui/atoms/runtime.js.map +1 -1
  192. package/dist/src/tui/atoms/tickets.d.ts +3 -0
  193. package/dist/src/tui/atoms/tickets.d.ts.map +1 -0
  194. package/dist/src/tui/atoms/timer.d.ts +11 -0
  195. package/dist/src/tui/atoms/timer.d.ts.map +1 -0
  196. package/dist/src/tui/atoms/timer.js +1 -1
  197. package/dist/src/tui/atoms/timer.js.map +1 -1
  198. package/dist/src/tui/atoms/ui.d.ts +11 -0
  199. package/dist/src/tui/atoms/ui.d.ts.map +1 -0
  200. package/dist/src/tui/atoms/ui.js +1 -1
  201. package/dist/src/tui/atoms/ui.js.map +1 -1
  202. package/dist/src/tui/components/BigTimer.d.ts +2 -0
  203. package/dist/src/tui/components/BigTimer.d.ts.map +1 -0
  204. package/dist/src/tui/components/Footer.d.ts +2 -0
  205. package/dist/src/tui/components/Footer.d.ts.map +1 -0
  206. package/dist/src/tui/components/Header.d.ts +2 -0
  207. package/dist/src/tui/components/Header.d.ts.map +1 -0
  208. package/dist/src/tui/components/Header.js +1 -1
  209. package/dist/src/tui/components/Header.js.map +1 -1
  210. package/dist/src/tui/components/PopupInput.d.ts +18 -0
  211. package/dist/src/tui/components/PopupInput.d.ts.map +1 -0
  212. package/dist/src/tui/components/PopupMessage.d.ts +20 -0
  213. package/dist/src/tui/components/PopupMessage.d.ts.map +1 -0
  214. package/dist/src/tui/components/TicketList.d.ts +2 -0
  215. package/dist/src/tui/components/TicketList.d.ts.map +1 -0
  216. package/dist/src/tui/components/TicketList.js +1 -1
  217. package/dist/src/tui/components/TicketList.js.map +1 -1
  218. package/dist/src/tui/components/TicketRow.d.ts +8 -0
  219. package/dist/src/tui/components/TicketRow.d.ts.map +1 -0
  220. package/dist/src/tui/components/TimerDisplay.d.ts +2 -0
  221. package/dist/src/tui/components/TimerDisplay.d.ts.map +1 -0
  222. package/dist/src/tui/components/index.d.ts +12 -0
  223. package/dist/src/tui/components/index.d.ts.map +1 -0
  224. package/dist/src/tui/context/theme.d.ts +15 -0
  225. package/dist/src/tui/context/theme.d.ts.map +1 -0
  226. package/dist/src/tui/hooks/useElapsedTimer.d.ts +6 -0
  227. package/dist/src/tui/hooks/useElapsedTimer.d.ts.map +1 -0
  228. package/dist/src/tui/hooks/useElapsedTimer.js +1 -1
  229. package/dist/src/tui/hooks/useElapsedTimer.js.map +1 -1
  230. package/dist/src/tui/hooks/useTerminalSize.d.ts +5 -0
  231. package/dist/src/tui/hooks/useTerminalSize.d.ts.map +1 -0
  232. package/dist/src/utils/hints.d.ts +12 -0
  233. package/dist/src/utils/hints.d.ts.map +1 -0
  234. package/dist/src/utils/hints.js +12 -0
  235. package/dist/src/utils/hints.js.map +1 -0
  236. package/dist/src/utils/time.d.ts +104 -0
  237. package/dist/src/utils/time.d.ts.map +1 -0
  238. package/dist/src/utils/time.js +74 -8
  239. package/dist/src/utils/time.js.map +1 -1
  240. package/dist/tsconfig.tsbuildinfo +1 -1
  241. package/package.json +36 -18
  242. package/skills/jcf/SKILL.md +93 -1
package/README.md CHANGED
@@ -42,6 +42,33 @@ jcf config show # Show current config
42
42
  jcf config reset # Reset to defaults
43
43
  ```
44
44
 
45
+ ### 4. Session Roots (optional)
46
+
47
+ Only needed for `jcf sync reconcile --agent claude` and `jcf watch claude`. Nothing is read until you
48
+ opt a directory in:
49
+
50
+ ```bash
51
+ jcf config set session-root ~/dev/work # Sessions here may become proposed worklogs
52
+ jcf config set session-ticket ~/dev/work/docs KEY # Standing ticket for work with no branch
53
+ jcf config set idle-cap 300 # Longest gap still counted as work (seconds)
54
+ ```
55
+
56
+ JCF web's Agent settings controls the provider used to attribute and describe session
57
+ evidence. `JcfConfig.sessionAgent` defaults to
58
+ `{ "provider": "claude", "model": null, "effort": null }`. Choose `claude` or `codex`;
59
+ the selected executable must be installed and authenticated. This choice does not
60
+ change which session roots are read. Every attribution and description operation
61
+ reads the current settings, so changing them does not require restarting JCF.
62
+
63
+ The pure `@knpkv/jira-clockify/agent/agentSettings.js` export provides the
64
+ `SessionAgentSettings` schema and type, `defaultSessionAgentSettings`, and
65
+ `agentEfforts(provider)`. Model names must be trimmed, nonblank and at most 200
66
+ characters, or `null` for the CLI default. Effort is `null` for the CLI default or
67
+ a provider-supported value. Claude supports low, medium, high, xhigh and max;
68
+ Codex supports minimal, low, medium, high and xhigh. The selected model must support
69
+ the requested effort. Both adapters use prompt-only execution; unsupported Codex
70
+ features fail closed before attribution starts.
71
+
45
72
  ## CLI Commands
46
73
 
47
74
  ```bash
@@ -58,8 +85,167 @@ jcf timer edit # Edit running timer
58
85
  jcf timer status # Show current timer status
59
86
  jcf issue list [--json] # List Jira tickets from configured JQL
60
87
  jcf auth status # Show auth status for both services
88
+ jcf watch claude # Log Claude Code session time as it happens
89
+ ```
90
+
91
+ ### Reconciling
92
+
93
+ ```bash
94
+ jcf sync reconcile clockify-to-jira # Fill Jira from Clockify (--day|--week|--since|--until)
95
+ jcf sync reconcile jira-to-clockify # Fill Clockify from Jira
96
+ jcf sync reconcile --agent claude # Propose worklogs from local Claude Code sessions
97
+ jcf sync reconcile --agent claude --calendar # ...with an hour-by-hour grid of when it happened
98
+ jcf sync reconcile --agent claude --json # Reporting only: one JSON value, nothing logged
99
+ ```
100
+
101
+ `--agent claude` is for time _neither_ side recorded: it reads Claude Code and Codex transcripts,
102
+ keeps work inside your session roots, and works out which issue each
103
+ session belongs to and how long it accounts for, subtracts
104
+ what Clockify and Jira already hold, and offers each remaining gap for confirmation. Every row shows
105
+ the signal behind it — the git branch, the directory path, a standing ticket, or a reading of the
106
+ transcript — so a wrong attribution is visible before you accept it. It is a mode switch, not a
107
+ direction, so combining it with `clockify-to-jira` is an error.
108
+
109
+ Worth being exact about what "only those" means, because it is a _local read_ boundary and a
110
+ _disclosure_ boundary, and they are not in the same place. Claude names each project directory after
111
+ the working directory with the separators flattened, so `/a/b-c` and `/a/b/c` both become `-a-b-c`.
112
+ A directory whose name cannot have come from any of your session roots is never opened at all, which
113
+ is nearly all of them. One that collides with a root's encoding is opened, and its sessions are
114
+ discarded once the working directory inside says they were out of scope. So an out-of-scope session
115
+ can be read from disk by jcf; it can never reach a coding agent, a proposal, or either of the two
116
+ systems.
117
+
118
+ Codex rollouts live under `~/.codex/sessions` in creation-date directories. Those names cannot
119
+ identify the working project, so JCF decodes their metadata locally before applying session roots
120
+ to each stretch of work. Resumed rollouts in older directories remain eligible for the current
121
+ week. Large tool payloads are discarded while streaming the file. Current `UserMessage` events
122
+ and older `user_message` events count as presence; injected instructions, response-item prompt
123
+ copies, compacted history and native subagent rollouts do not. Selecting Claude or Codex as the
124
+ matching agent does not restrict which of these two transcript sources is scanned.
125
+
126
+ Only messages _you typed_ count as presence — the agent's own output, its tool results, and the
127
+ prompts it sends its own subagents do not, since they show it was busy rather than that you were
128
+ working. On one real day that was 66 events out
129
+ of 1641, and the difference between 1h 53m and 3h 51m. A session counts as active between its own
130
+ consecutive prompts, gaps longer than the idle cap are
131
+ credited to nothing, and any moment you were working on several tickets at once is divided equally
132
+ between them — so a day's proposals can never add up to more than that day's wall clock. Time
133
+ spent reading and thinking between prompts is therefore under-counted rather than over-counted; a day
134
+ with a timer still running is reported but never proposed.
135
+
136
+ Everything you need to judge a row is on the row itself, in the picker: when the work item started
137
+ and ended, the issue summary, its assignee, the attribution signal, what each side already holds, and
138
+ how many blocks the total spans. So the number is checkable against your memory of the day rather
139
+ than taken on trust, and time credited to someone else's ticket stands out before it is written. Rows
140
+ are laid out for your terminal's width; a wider one spends the room on the issue title and the block
141
+ times. `--calendar` draws the same evidence as a
142
+ grid — one column per minute, one row per hour, only the hours with credited time:
143
+
144
+ ```text
145
+ 2026-07-25 # PROJ-5663
146
+ :00 :05 :10 :15 :20 :25 :30 :35 :40 :45 :50 :55
147
+ 00h .................###########################################
148
+ 01h ##########......###################################.....####
149
+ ~~~ 8h with nothing credited
150
+ 11h ........#############.......................................
61
151
  ```
62
152
 
153
+ The grid answers _when_ at minute resolution, so a span shorter than a minute still shows as one
154
+ cell; the row above it remains the authority on _how much_.
155
+
156
+ Each entry it writes says what the time went on. The Clockify description and the Jira worklog comment
157
+ carry the issue title and one sentence, read off the session's own prompts, describing what was
158
+ actually done:
159
+
160
+ ```text
161
+ 2026-07-25 PROJ-5663 +2h 35m to both
162
+ Add an endpoint for a caller's own permissions — Read the identity provider's OIDC docs and designed a route returning the applications the caller may open (Reconciled from Claude Agent Session)
163
+ ✓ created Clockify entry
164
+ ✓ posted to Jira
165
+ ```
166
+
167
+ The text is printed before it is written, since it lands in two systems other people read. Notes are
168
+ asked for only about the rows you confirm, in one batched call. If the coding agent cannot be reached
169
+ the entry falls back to the title and its provenance — a missing sentence never costs the write — and
170
+ a session whose prompts do not say what was done gets no sentence rather than an invented one.
171
+
172
+ Session consumption is private to `~/.jcf/source-consumption.v1.json` (owner-only mode). A confirmed
173
+ write binds its source block to the returned provider entry ID and account scope; later description
174
+ edits, including removal of the source suffix, do not reset its consumed seconds. A complete provider
175
+ read still observes genuine duration changes and deletions. The suffix can import a legacy entry on
176
+ first read, but text alone does not override an existing ID binding.
177
+
178
+ The same private file now stores version 3: it also remembers the IDs and original starts of ordinary
179
+ entries seen in a newly reviewed forward interval, separately by provider and account. Repeated or
180
+ overlapping review can recognize those entries. A new unmarked entry in older coverage or a changed
181
+ start for a remembered ID needs manual review; the tool does not infer its origin from a timestamp.
182
+ Versions 1 and 2 load without inventing ordinary entries and upgrade only on a successful atomic
183
+ write. New Clockify evidence is scoped to the configured endpoint, workspace, and user verified by
184
+ that credential. Older Clockify scopes cannot prove the endpoint: affected windows, observations,
185
+ pending intents, and bindings remain intact and held for private manual review. Neither switching
186
+ the endpoint nor editing a description turns them into a fresh empty account. Jira evidence remains
187
+ independent.
188
+
189
+ First use with earlier unmarked entries fails closed. Review the affected provider/window privately,
190
+ back up the ledger, then record the exact reviewed window, provider/account-scoped ordinary entry
191
+ IDs and starts, and any recoverable entry-ID bindings in its versioned JSON. If those facts cannot
192
+ be recovered, leave the window held; do not guess links or paste account and entry IDs into tickets
193
+ or logs. A pending intent means a create may have succeeded before its ID was recorded: verify the provider entry before
194
+ resolving it, or leave the window held. Removing the ledger or pending intent is not a safe retry.
195
+ On Linux, a lock whose recorded process has ended in the same PID namespace can be recovered under
196
+ an exclusive recovery guard. Legacy, unreadable, or uncertain locks and leftover temporary ledger
197
+ files still need private manual review; never remove a lock while its holder may be running.
198
+
199
+ ### Watching as you work
200
+
201
+ ```bash
202
+ jcf watch claude # Log settled blocks as they happen (Ctrl-C to stop)
203
+ jcf watch claude --dry-run # Same, printing what it would write instead of writing it
204
+ jcf watch claude --interval 60
205
+ ```
206
+
207
+ Same evidence, same arithmetic, no picker. It looks every five minutes and writes a block of work
208
+ once it has been quiet for one idle cap — six minutes by default — because until then the block can
209
+ still grow, and its share of any parallel work can still change. So entries land at natural breaks
210
+ rather than a minute at a time, and each says what the time went on exactly as a confirmed row does:
211
+
212
+ ```text
213
+ jcf watch claude
214
+ Looking every 5m, from 10:32. A block is written once it has been quiet for 6m.
215
+ Only branch-, path-, and standing-attributed work is written. Earlier work and anything needing review: jcf sync reconcile --agent claude
216
+ 11:48 PROJ-5663 +1h 24m to both
217
+ Add OpenTelemetry spans to the ingest worker — Traced the ingest worker end to end and added spans around the batch flush (Reconciled from Claude Agent Session)
218
+ ✓ created Clockify entry
219
+ ✓ posted to Jira
220
+ ```
221
+
222
+ It writes only what it can defend without you: blocks placed by a branch name, a worktree path, or a
223
+ standing ticket. It never wakes the coding agent to guess at an attribution — a session's ticket does
224
+ not change, so asking every five minutes would spend a call to be told the same thing — and time no
225
+ deliberate signal places is named on screen and left for `jcf sync reconcile --agent claude`. The
226
+ coding agent is woken only to describe a block it is about to write.
227
+
228
+ It covers time since it started, plus whatever a previous watch had reached but not yet written — it
229
+ leaves a cursor behind, so a restart resumes instead of dropping the block it was holding. A
230
+ first-ever run has no cursor and reaches back for nothing. Older than that is `reconcile`'s job,
231
+ where you see the rows first.
232
+
233
+ Only one watch writes at a time. A second one tells you who has been running since when and stops,
234
+ because both would otherwise derive the same gap and write it twice. The lease is never taken over
235
+ while its file exists: a stale-looking timestamp cannot make a read-then-overwrite race safe. After
236
+ an ungraceful process death, verify no watch is running and remove `~/.jcf/watch.lease` manually. If
237
+ the lease cannot be written at all, the watch refuses to start: an unwritable config directory means
238
+ nothing would stop a second one. That is machine-local: two watches on two machines against one
239
+ Clockify account is not something this can see. Nothing is remembered between looks — a proposal is always the
240
+ gap the two sides still have — so a failed write, a closed laptop, or a restart costs nothing but a
241
+ delay. If Jira rejects the login it stops rather than logging to Clockify alone all afternoon; the
242
+ half-written block stays behind the cursor, so logging back in and restarting finishes it rather than
243
+ skipping it.
244
+
245
+ JCF web confirmations, manual entries and saved-entry edits use the same machine writer guard. They
246
+ are refused while `jcf watch` holds it; a web mutation releases its short-lived guard without writing
247
+ a watch resume cursor.
248
+
63
249
  ## TUI Keybindings
64
250
 
65
251
  | Key | Action |
@@ -117,7 +303,7 @@ Stored in `~/.jcf/`:
117
303
 
118
304
  ```
119
305
  ~/.jcf/
120
- ├── config.json # JQL, project, billable defaults
306
+ ├── config.json # JQL, project, billable defaults, session roots
121
307
  ├── clockify.json # Clockify API key, workspace, user
122
308
  ├── poll.lock # Kernel lock held by the active managed poll
123
309
  ├── poll.stamp # Last managed poll attempt, including failures
@@ -129,3 +315,61 @@ Jira OAuth credentials stored via `@knpkv/atlassian-common` in `~/.config/atlass
129
315
  ## License
130
316
 
131
317
  MIT
318
+
319
+ ### Reusing session evidence
320
+
321
+ `ReconcileService.proposeFromSessions` retains attributed credits on its report,
322
+ including credits that currently have no gap or are withheld by a running timer.
323
+ `refreshRecordedTime(period, report)` rereads only the report's selected providers
324
+ and running timers, then recalculates proposals from those credits. It does not
325
+ read transcripts or call the attribution agent. JCF web uses this after writes;
326
+ an explicit full read collects new session evidence.
327
+
328
+ ### Ticket overlap and live matching
329
+
330
+ With the default 900-second dwell floor, overlapping attributed work is allocated
331
+ in sequential blocks of at least fifteen minutes when the stretch permits it.
332
+ Tickets share the stretch evenly; if fewer slots fit, the strongest credited
333
+ tickets retain them. Mixed stretches reserve unplaced credit before allocating known tickets. Separate
334
+ work, idle gaps, wholly unplaced stretches and midnight stay
335
+ separate. Short standalone work is never inflated. Setting dwell to zero retains
336
+ raw overlap sharing. Proposal blocks and confirmed amounts use the same timeline;
337
+ `activeSeconds` retains the original activity duration.
338
+
339
+ `SessionAttributor.attribute` accepts an optional activity observer. Reconciliation
340
+ forwards visible text and process status as `AgentActivity`, tagged with the batch
341
+ number and batch count. JCF web streams these events during matching, while the CLI
342
+ prints process milestones to stderr. Final matches remain schema-validated.
343
+
344
+ Closed Clockify entries without a ticket key are retained in the session proposal
345
+ report as `unlinkedClockify`. They remain read-only and never become Jira
346
+ reconciliation candidates. The web calendar includes them in Clockify totals and
347
+ holds the Clockify side of any overlapping proposed block for manual review, without
348
+ guessing that the entry consumed that session's time. Jira and nonoverlapping blocks
349
+ remain independently writable. Running entries remain outside
350
+ totals; their days are excluded from proposals until the timer stops.
351
+
352
+ ### Editing recorded entries
353
+
354
+ `SavedEntries.layer` is included in `Headless.layer`. Its `update` operation takes a
355
+ server-retained `RecordedEntry` snapshot plus new start/end milliseconds and description,
356
+ rechecks provider ownership and the current snapshot, and returns the actual saved
357
+ entry. `SavedEntryError.reason` distinguishes validation, conflict and provider failures.
358
+ Recorded intervals carry optional whole-entry metadata, including exact original
359
+ bounds and description, even when the calendar slices an entry at midnight.
360
+
361
+ Changed times require whole seconds and positive duration. Jira requires at least
362
+ 60 seconds when changing time; description-only edits can retain shorter legacy time.
363
+ Clockify updates preserve project, task, tags, billable state and entry type. Running,
364
+ locked and time-off entries are refused; custom-field entries are refused because the
365
+ generated API schema cannot safely round-trip their values. Jira time-only updates keep
366
+ the original rich comment; changed descriptions become plain paragraphs, replacing
367
+ formatting and embeds. Mention and emoji labels are readable in the editor; cards and
368
+ media have no plain-text representation. Updates leave
369
+ Jira estimates and visibility unchanged. The service serializes local updates, but the
370
+ provider APIs offer no atomic protection from external edits between re-read and save.
371
+
372
+ Session reports optionally retain `sessionEvidence` with each session's ticket and
373
+ individual active intervals. This lets web description requests correlate saved time
374
+ without another scan. `SessionAttributor.describe` accepts a nullable ticket key for
375
+ unkeyed time. Legacy reports without this evidence cannot support precise correlation.
package/dist/package.json CHANGED
@@ -1,15 +1,37 @@
1
1
  {
2
2
  "name": "@knpkv/jira-clockify",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "TUI for Jira-Clockify time tracking, attachable to neovim",
5
5
  "license": "MIT",
6
6
  "author": "knpkv",
7
7
  "type": "module",
8
+ "main": "src/index.ts",
9
+ "exports": {
10
+ ".": "./src/index.ts",
11
+ "./testing.js": "./src/testing/fakeHeadless.ts",
12
+ "./*.js": "./src/*.ts"
13
+ },
8
14
  "bin": {
9
15
  "jcf": "dist/src/bin.js"
10
16
  },
11
17
  "publishConfig": {
12
- "access": "public"
18
+ "access": "public",
19
+ "main": "dist/src/index.js",
20
+ "types": "dist/src/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/src/index.d.ts",
24
+ "default": "./dist/src/index.js"
25
+ },
26
+ "./testing.js": {
27
+ "types": "./dist/src/testing/fakeHeadless.d.ts",
28
+ "default": "./dist/src/testing/fakeHeadless.js"
29
+ },
30
+ "./*.js": {
31
+ "types": "./dist/src/*.d.ts",
32
+ "default": "./dist/src/*.js"
33
+ }
34
+ }
13
35
  },
14
36
  "files": [
15
37
  "dist",
@@ -20,28 +42,30 @@
20
42
  "scripts": {
21
43
  "start": "tsx src/bin.ts",
22
44
  "build": "tsc -b && chmod +x dist/src/bin.js",
23
- "check": "tsc -b tsconfig.json",
45
+ "check": "tsc -b tsconfig.json && tsc --noEmit -p test/tsconfig.json",
24
46
  "test": "vitest",
25
47
  "lint": "eslint \"{src,test}/**/*.{ts,tsx}\""
26
48
  },
27
49
  "dependencies": {
28
- "@effect/atom-react": "4.0.0-rc.109",
29
- "@effect/platform-node": "4.0.0-rc.109",
50
+ "@effect/atom-react": "4.0.0",
51
+ "@effect/platform-node": "4.0.0",
30
52
  "@knpkv/agent-skills": "workspace:^",
53
+ "@knpkv/ai-claude": "workspace:^",
54
+ "@knpkv/ai-codex": "workspace:^",
31
55
  "@knpkv/atlassian-common": "workspace:^",
32
56
  "@knpkv/clockify-api-client": "workspace:^",
33
57
  "@knpkv/jira-api-client": "workspace:^",
34
58
  "@knpkv/jira-cli": "workspace:^",
35
- "@opentui/core": "0.5.1",
36
- "@opentui/react": "0.5.1",
37
- "effect": "4.0.0-rc.109",
38
- "react": "^19.2.8"
59
+ "@opentui/core": "0.5.14",
60
+ "@opentui/react": "0.5.14",
61
+ "effect": "4.0.0",
62
+ "react": "^19.3.0"
39
63
  },
40
64
  "devDependencies": {
41
- "@effect/vitest": "4.0.0-rc.109",
65
+ "@effect/vitest": "4.0.0",
42
66
  "@types/node": "latest",
43
- "@types/react": "^19.2.18",
44
- "tsx": "^4.23.12",
67
+ "@types/react": "^19.3.0",
68
+ "tsx": "^4.23.15",
45
69
  "vitest": "catalog:"
46
70
  }
47
71
  }
@@ -0,0 +1,18 @@
1
+ /** Provider selection shared by config persistence, the session engine and settings controls. */
2
+ import * as Schema from "effect/Schema";
3
+ /** Complete stored settings. Null model and effort leave those choices to the selected CLI. */
4
+ export declare const SessionAgentSettings: Schema.Union<readonly [Schema.Struct<{
5
+ readonly provider: Schema.Literal<"claude">;
6
+ readonly model: Schema.NullOr<Schema.String>;
7
+ readonly effort: Schema.NullOr<Schema.Literals<readonly ["low", "medium", "high", "xhigh", "max"]>>;
8
+ }>, Schema.Struct<{
9
+ readonly provider: Schema.Literal<"codex">;
10
+ readonly model: Schema.NullOr<Schema.String>;
11
+ readonly effort: Schema.NullOr<Schema.Literals<readonly ["minimal", "low", "medium", "high", "xhigh"]>>;
12
+ }>]>;
13
+ export type SessionAgentSettings = typeof SessionAgentSettings.Type;
14
+ /** Existing installations keep Claude with its default model and effort. */
15
+ export declare const defaultSessionAgentSettings: SessionAgentSettings;
16
+ /** Supported explicit efforts for a provider; controls add their own null/default option. */
17
+ export declare const agentEfforts: (provider: SessionAgentSettings["provider"]) => readonly ["low", "medium", "high", "xhigh", "max"] | readonly ["minimal", "low", "medium", "high", "xhigh"];
18
+ //# sourceMappingURL=agentSettings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentSettings.d.ts","sourceRoot":"","sources":["../../../src/agent/agentSettings.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAUvC,+FAA+F;AAC/F,eAAO,MAAM,oBAAoB;;;;;;;;IAW/B,CAAA;AAEF,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAA;AAEnE,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,EAAE,oBAAwE,CAAA;AAElH,6FAA6F;AAC7F,eAAO,MAAM,YAAY,aAAc,oBAAoB,CAAC,UAAU,CAAC,gHACD,CAAA"}
@@ -0,0 +1,23 @@
1
+ /** Provider selection shared by config persistence, the session engine and settings controls. */
2
+ import * as Schema from "effect/Schema";
3
+ const ClaudeEffort = Schema.Literals(["low", "medium", "high", "xhigh", "max"]);
4
+ const CodexEffort = Schema.Literals(["minimal", "low", "medium", "high", "xhigh"]);
5
+ const Model = Schema.NullOr(Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(200), Schema.isTrimmed()));
6
+ /** Complete stored settings. Null model and effort leave those choices to the selected CLI. */
7
+ export const SessionAgentSettings = Schema.Union([
8
+ Schema.Struct({
9
+ provider: Schema.Literal("claude"),
10
+ model: Model,
11
+ effort: Schema.NullOr(ClaudeEffort)
12
+ }),
13
+ Schema.Struct({
14
+ provider: Schema.Literal("codex"),
15
+ model: Model,
16
+ effort: Schema.NullOr(CodexEffort)
17
+ })
18
+ ]);
19
+ /** Existing installations keep Claude with its default model and effort. */
20
+ export const defaultSessionAgentSettings = { provider: "claude", model: null, effort: null };
21
+ /** Supported explicit efforts for a provider; controls add their own null/default option. */
22
+ export const agentEfforts = (provider) => provider === "claude" ? ClaudeEffort.literals : CodexEffort.literals;
23
+ //# sourceMappingURL=agentSettings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentSettings.js","sourceRoot":"","sources":["../../../src/agent/agentSettings.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAA;AAC/E,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAClF,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAC7C,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EACrB,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,EACvB,MAAM,CAAC,SAAS,EAAE,CACnB,CAAC,CAAA;AAEF,+FAA+F;AAC/F,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC,KAAK,CAAC;IAC/C,MAAM,CAAC,MAAM,CAAC;QACZ,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;QAClC,KAAK,EAAE,KAAK;QACZ,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC;KACpC,CAAC;IACF,MAAM,CAAC,MAAM,CAAC;QACZ,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;QACjC,KAAK,EAAE,KAAK;QACZ,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC;KACnC,CAAC;CACH,CAAC,CAAA;AAIF,4EAA4E;AAC5E,MAAM,CAAC,MAAM,2BAA2B,GAAyB,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;AAElH,6FAA6F;AAC7F,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAA0C,EAAE,EAAE,CACzE,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAA"}
@@ -0,0 +1,20 @@
1
+ interface Run {
2
+ readonly startMs: number;
3
+ readonly endMs: number;
4
+ readonly bucketIds: ReadonlyArray<string>;
5
+ }
6
+ export interface ScheduledRun extends Run {
7
+ /** Earliest source instant needed to reconstruct this allocation. */
8
+ readonly sourceStartMs: number;
9
+ /** Last source instant that can still redistribute this allocation. */
10
+ readonly settlementEndMs: number;
11
+ }
12
+ /**
13
+ * Arrange a contiguous stretch into sequential allocations. Keep the strongest tickets when there
14
+ * is not enough time for every ticket to receive the floor. Wholly unplaced time and midnight are barriers. Mixed stretches reserve their unplaced credit
15
+ * before allocating known tickets; changing unplaced shares must not fragment every known ticket.
16
+ * A genuinely shorter stretch stays short: allocation must never invent time or bridge idle gaps.
17
+ */
18
+ export declare const scheduleRuns: (runs: ReadonlyArray<Run>, minimumSeconds: number, attributed: (id: string) => boolean) => ReadonlyArray<ScheduledRun>;
19
+ export {};
20
+ //# sourceMappingURL=schedule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../../../src/agent/schedule.ts"],"names":[],"mappings":"AAGA,UAAU,GAAG;IACX,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAC1C;AAID,MAAM,WAAW,YAAa,SAAQ,GAAG;IACvC,qEAAqE;IACrE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CACjC;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,SACjB,aAAa,CAAC,GAAG,CAAC,kBACR,MAAM,cACV,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,KAClC,aAAa,CAAC,YAAY,CA6K5B,CAAA"}
@@ -0,0 +1,189 @@
1
+ import { Data } from "effect";
2
+ import { localDay } from "../utils/time.js";
3
+ class ScheduleAllocationError extends Data.TaggedError("ScheduleAllocationError") {
4
+ }
5
+ /**
6
+ * Arrange a contiguous stretch into sequential allocations. Keep the strongest tickets when there
7
+ * is not enough time for every ticket to receive the floor. Wholly unplaced time and midnight are barriers. Mixed stretches reserve their unplaced credit
8
+ * before allocating known tickets; changing unplaced shares must not fragment every known ticket.
9
+ * A genuinely shorter stretch stays short: allocation must never invent time or bridge idle gaps.
10
+ */
11
+ export const scheduleRuns = (runs, minimumSeconds, attributed) => {
12
+ if (minimumSeconds <= 0) {
13
+ return runs.map((run) => ({ ...run, sourceStartMs: run.startMs, settlementEndMs: run.endMs }));
14
+ }
15
+ const settlementEndByRun = new Map();
16
+ let connectedStart = 0;
17
+ for (let index = 1; index <= runs.length; index++) {
18
+ const previous = runs[index - 1];
19
+ const current = runs[index];
20
+ if (previous !== undefined &&
21
+ (current === undefined || previous.endMs !== current.startMs ||
22
+ localDay(new Date(previous.startMs)) !== localDay(new Date(current.startMs)))) {
23
+ for (let at = connectedStart; at < index; at++) {
24
+ const run = runs[at];
25
+ if (run !== undefined)
26
+ settlementEndByRun.set(run, previous.endMs);
27
+ }
28
+ connectedStart = index;
29
+ }
30
+ }
31
+ const result = [];
32
+ let cluster = [];
33
+ const flush = () => {
34
+ const first = cluster[0];
35
+ const last = cluster.at(-1);
36
+ if (first === undefined || last === undefined)
37
+ return;
38
+ const settlementEndMs = settlementEndByRun.get(first) ?? last.endMs;
39
+ const weights = new Map();
40
+ const active = new Map();
41
+ for (const run of cluster) {
42
+ for (const id of run.bucketIds) {
43
+ weights.set(id, (weights.get(id) ?? 0) + (run.endMs - run.startMs) / run.bucketIds.length);
44
+ active.set(id, (active.get(id) ?? 0) + run.endMs - run.startMs);
45
+ }
46
+ }
47
+ const duration = Math.floor((last.endMs - first.startMs) / 1000);
48
+ const unplaced = [...weights].filter(([id]) => !attributed(id));
49
+ const reserved = Math.floor(unplaced.reduce((sum, [, weight]) => sum + weight, 0) / 1000);
50
+ const available = duration - reserved;
51
+ const limit = Math.max(1, Math.floor(available / minimumSeconds));
52
+ const rankedOwners = [...weights].filter(([id]) => attributed(id))
53
+ .sort(([a, weightA], [b, weightB]) => weightB - weightA || a.localeCompare(b));
54
+ const owners = rankedOwners.slice(0, limit);
55
+ const allocations = new Map();
56
+ let remaining = available;
57
+ let remainingWeight = owners.reduce((sum, [, weight]) => sum + weight, 0);
58
+ for (const [index, [id, weight]] of owners.entries()) {
59
+ const proportional = index === owners.length - 1
60
+ ? remaining
61
+ : Math.floor(remaining * weight / remainingWeight);
62
+ const seconds = Math.min(proportional, Math.floor((active.get(id) ?? 0) / 1000));
63
+ allocations.set(id, seconds);
64
+ remaining -= seconds;
65
+ remainingWeight -= weight;
66
+ }
67
+ // An activity cap can leave part of a proportional share unused. Offer that residual to every
68
+ // attributable owner, including one omitted by the normal minimum-size limit, rather than
69
+ // silently losing credited time. The residual itself may be shorter than the normal floor.
70
+ for (const [id] of rankedOwners) {
71
+ if (remaining <= 0)
72
+ break;
73
+ const allocated = allocations.get(id) ?? 0;
74
+ const capacity = Math.floor((active.get(id) ?? 0) / 1000) - allocated;
75
+ const seconds = Math.min(remaining, Math.max(0, capacity));
76
+ allocations.set(id, allocated + seconds);
77
+ remaining -= seconds;
78
+ }
79
+ const remainingMs = new Map([...allocations].map(([id, seconds]) => [id, seconds * 1000]));
80
+ const futureActiveMs = new Map(active);
81
+ const lastActiveMs = new Map();
82
+ for (const run of cluster)
83
+ for (const id of run.bucketIds)
84
+ lastActiveMs.set(id, run.endMs);
85
+ const placementOrder = rankedOwners.map(([id]) => id).sort((a, b) => (lastActiveMs.get(a) ?? 0) - (lastActiveMs.get(b) ?? 0));
86
+ const gaps = [];
87
+ for (const run of cluster) {
88
+ const runMs = run.endMs - run.startMs;
89
+ for (const id of run.bucketIds)
90
+ futureActiveMs.set(id, (futureActiveMs.get(id) ?? 0) - runMs);
91
+ const present = placementOrder.filter((id) => run.bucketIds.includes(id) && (remainingMs.get(id) ?? 0) > 0);
92
+ let cursor = run.startMs;
93
+ for (const id of present) {
94
+ const reservedForOthers = present.reduce((sum, other) => other === id ?
95
+ sum :
96
+ sum + Math.max(0, (remainingMs.get(other) ?? 0) - (futureActiveMs.get(other) ?? 0)), 0);
97
+ const placedMs = Math.min(remainingMs.get(id) ?? 0, Math.max(0, run.endMs - cursor - reservedForOthers));
98
+ if (placedMs <= 0)
99
+ continue;
100
+ result.push({
101
+ bucketIds: [id],
102
+ startMs: cursor,
103
+ endMs: cursor + placedMs,
104
+ sourceStartMs: first.startMs,
105
+ settlementEndMs
106
+ });
107
+ remainingMs.set(id, (remainingMs.get(id) ?? 0) - placedMs);
108
+ cursor += placedMs;
109
+ }
110
+ if (cursor < run.endMs) {
111
+ gaps.push({ startMs: cursor, endMs: run.endMs, bucketIds: run.bucketIds });
112
+ }
113
+ }
114
+ let gapIndex = 0;
115
+ let gapCursor = gaps[0]?.startMs ?? last.endMs;
116
+ const nextGap = () => {
117
+ gapIndex++;
118
+ gapCursor = gaps[gapIndex]?.startMs ?? last.endMs;
119
+ };
120
+ let offset = available;
121
+ for (const [index, [id, weight]] of unplaced.entries()) {
122
+ const seconds = index === unplaced.length - 1 ? duration - offset : Math.floor(weight / 1000);
123
+ let milliseconds = seconds * 1000;
124
+ while (milliseconds > 0) {
125
+ const gap = gaps[gapIndex];
126
+ if (gap === undefined)
127
+ throw new ScheduleAllocationError({ message: "Unplaced credit exceeds source activity" });
128
+ const placedMs = Math.min(milliseconds, gap.endMs - gapCursor);
129
+ result.push({
130
+ bucketIds: [id],
131
+ startMs: gapCursor,
132
+ endMs: gapCursor + placedMs,
133
+ sourceStartMs: first.startMs,
134
+ settlementEndMs
135
+ });
136
+ milliseconds -= placedMs;
137
+ gapCursor += placedMs;
138
+ if (gapCursor === gap.endMs)
139
+ nextGap();
140
+ }
141
+ offset += seconds;
142
+ }
143
+ let residualMs = [...remainingMs.values()].reduce((sum, milliseconds) => sum + milliseconds, 0);
144
+ for (let index = gapIndex; index < gaps.length && residualMs > 0; index++) {
145
+ const gap = gaps[index];
146
+ if (gap === undefined)
147
+ continue;
148
+ if (residualMs <= 0)
149
+ break;
150
+ const recipient = rankedOwners.find(([id]) => gap.bucketIds.includes(id))?.[0];
151
+ if (recipient === undefined)
152
+ continue;
153
+ const startMs = index === gapIndex ? gapCursor : gap.startMs;
154
+ const placedMs = Math.min(residualMs, gap.endMs - startMs);
155
+ result.push({
156
+ bucketIds: [recipient],
157
+ startMs,
158
+ endMs: startMs + placedMs,
159
+ sourceStartMs: first.startMs,
160
+ settlementEndMs
161
+ });
162
+ residualMs -= placedMs;
163
+ }
164
+ if (residualMs > 0) {
165
+ throw new ScheduleAllocationError({ message: "Attribution cannot fit inside its source activity" });
166
+ }
167
+ cluster = [];
168
+ };
169
+ for (const run of runs) {
170
+ const previous = cluster.at(-1);
171
+ if (previous !== undefined &&
172
+ (!previous.bucketIds.some((id) => run.bucketIds.includes(id)) || previous.endMs !== run.startMs ||
173
+ localDay(new Date(previous.startMs)) !== localDay(new Date(run.startMs))))
174
+ flush();
175
+ if (!run.bucketIds.some(attributed)) {
176
+ flush();
177
+ result.push({
178
+ ...run,
179
+ sourceStartMs: run.startMs,
180
+ settlementEndMs: settlementEndByRun.get(run) ?? run.endMs
181
+ });
182
+ }
183
+ else
184
+ cluster.push(run);
185
+ }
186
+ flush();
187
+ return result.sort((left, right) => left.startMs - right.startMs || left.endMs - right.endMs);
188
+ };
189
+ //# sourceMappingURL=schedule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.js","sourceRoot":"","sources":["../../../src/agent/schedule.ts"],"names":[],"mappings":"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"}