@knpkv/jira-clockify 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +245 -1
- package/dist/package.json +36 -12
- package/dist/src/agent/agentSettings.d.ts +18 -0
- package/dist/src/agent/agentSettings.d.ts.map +1 -0
- package/dist/src/agent/agentSettings.js +23 -0
- package/dist/src/agent/agentSettings.js.map +1 -0
- package/dist/src/agent/schedule.d.ts +20 -0
- package/dist/src/agent/schedule.d.ts.map +1 -0
- package/dist/src/agent/schedule.js +189 -0
- package/dist/src/agent/schedule.js.map +1 -0
- package/dist/src/agent/sessions.d.ts +427 -0
- package/dist/src/agent/sessions.d.ts.map +1 -0
- package/dist/src/agent/sessions.js +868 -0
- package/dist/src/agent/sessions.js.map +1 -0
- package/dist/src/agent/sourceConsumption.d.ts +79 -0
- package/dist/src/agent/sourceConsumption.d.ts.map +1 -0
- package/dist/src/agent/sourceConsumption.js +191 -0
- package/dist/src/agent/sourceConsumption.js.map +1 -0
- package/dist/src/agent/watch.d.ts +76 -0
- package/dist/src/agent/watch.d.ts.map +1 -0
- package/dist/src/agent/watch.js +45 -0
- package/dist/src/agent/watch.js.map +1 -0
- package/dist/src/agent/writePlanning.d.ts +154 -0
- package/dist/src/agent/writePlanning.d.ts.map +1 -0
- package/dist/src/agent/writePlanning.js +210 -0
- package/dist/src/agent/writePlanning.js.map +1 -0
- package/dist/src/bin.d.ts +3 -0
- package/dist/src/bin.d.ts.map +1 -0
- package/dist/src/bin.js +9 -3
- package/dist/src/bin.js.map +1 -1
- package/dist/src/cli/agentWrite.d.ts +164 -0
- package/dist/src/cli/agentWrite.d.ts.map +1 -0
- package/dist/src/cli/agentWrite.js +362 -0
- package/dist/src/cli/agentWrite.js.map +1 -0
- package/dist/src/cli/auth.d.ts +8 -0
- package/dist/src/cli/auth.d.ts.map +1 -0
- package/dist/src/cli/auth.js +10 -10
- package/dist/src/cli/auth.js.map +1 -1
- package/dist/src/cli/calendar.d.ts +42 -0
- package/dist/src/cli/calendar.d.ts.map +1 -0
- package/dist/src/cli/calendar.js +189 -0
- package/dist/src/cli/calendar.js.map +1 -0
- package/dist/src/cli/config.d.ts +12 -0
- package/dist/src/cli/config.d.ts.map +1 -0
- package/dist/src/cli/config.js +163 -21
- package/dist/src/cli/config.js.map +1 -1
- package/dist/src/cli/fetchTicket.d.ts +51 -0
- package/dist/src/cli/fetchTicket.d.ts.map +1 -0
- package/dist/src/cli/fetchTicket.js +18 -2
- package/dist/src/cli/fetchTicket.js.map +1 -1
- package/dist/src/cli/fuzzySelect.d.ts +24 -0
- package/dist/src/cli/fuzzySelect.d.ts.map +1 -0
- package/dist/src/cli/layers.d.ts +40 -0
- package/dist/src/cli/layers.d.ts.map +1 -0
- package/dist/src/cli/layers.js +56 -12
- package/dist/src/cli/layers.js.map +1 -1
- package/dist/src/cli/list.d.ts +8 -0
- package/dist/src/cli/list.d.ts.map +1 -0
- package/dist/src/cli/list.js +2 -2
- package/dist/src/cli/list.js.map +1 -1
- package/dist/src/cli/reconcile.d.ts +129 -0
- package/dist/src/cli/reconcile.d.ts.map +1 -0
- package/dist/src/cli/reconcile.js +635 -46
- package/dist/src/cli/reconcile.js.map +1 -1
- package/dist/src/cli/root.d.ts +15 -0
- package/dist/src/cli/root.d.ts.map +1 -0
- package/dist/src/cli/root.js +4 -3
- package/dist/src/cli/root.js.map +1 -1
- package/dist/src/cli/runtimeFailure.d.ts +10 -0
- package/dist/src/cli/runtimeFailure.d.ts.map +1 -0
- package/dist/src/cli/runtimeFailure.js +17 -0
- package/dist/src/cli/runtimeFailure.js.map +1 -0
- package/dist/src/cli/setup.d.ts +10 -0
- package/dist/src/cli/setup.d.ts.map +1 -0
- package/dist/src/cli/setup.js +7 -7
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/timer/discard.d.ts +4 -0
- package/dist/src/cli/timer/discard.d.ts.map +1 -0
- package/dist/src/cli/timer/discard.js +2 -2
- package/dist/src/cli/timer/discard.js.map +1 -1
- package/dist/src/cli/timer/edit.d.ts +22 -0
- package/dist/src/cli/timer/edit.d.ts.map +1 -0
- package/dist/src/cli/timer/edit.js +62 -42
- package/dist/src/cli/timer/edit.js.map +1 -1
- package/dist/src/cli/timer/index.d.ts +12 -0
- package/dist/src/cli/timer/index.d.ts.map +1 -0
- package/dist/src/cli/timer/log.d.ts +16 -0
- package/dist/src/cli/timer/log.d.ts.map +1 -0
- package/dist/src/cli/timer/log.js +6 -6
- package/dist/src/cli/timer/log.js.map +1 -1
- package/dist/src/cli/timer/start.d.ts +21 -0
- package/dist/src/cli/timer/start.d.ts.map +1 -0
- package/dist/src/cli/timer/start.js +9 -9
- package/dist/src/cli/timer/start.js.map +1 -1
- package/dist/src/cli/timer/status.d.ts +22 -0
- package/dist/src/cli/timer/status.d.ts.map +1 -0
- package/dist/src/cli/timer/status.js +3 -3
- package/dist/src/cli/timer/status.js.map +1 -1
- package/dist/src/cli/timer/stop.d.ts +48 -0
- package/dist/src/cli/timer/stop.d.ts.map +1 -0
- package/dist/src/cli/timer/stop.js +19 -19
- package/dist/src/cli/timer/stop.js.map +1 -1
- package/dist/src/cli/timer.d.ts +11 -0
- package/dist/src/cli/timer.d.ts.map +1 -0
- package/dist/src/cli/timer.js +1 -1
- package/dist/src/cli/timer.js.map +1 -1
- package/dist/src/cli/watch.d.ts +54 -0
- package/dist/src/cli/watch.d.ts.map +1 -0
- package/dist/src/cli/watch.js +412 -0
- package/dist/src/cli/watch.js.map +1 -0
- package/dist/src/cli/watchLease.d.ts +99 -0
- package/dist/src/cli/watchLease.d.ts.map +1 -0
- package/dist/src/cli/watchLease.js +204 -0
- package/dist/src/cli/watchLease.js.map +1 -0
- package/dist/src/cli/writerGuard.d.ts +37 -0
- package/dist/src/cli/writerGuard.d.ts.map +1 -0
- package/dist/src/cli/writerGuard.js +92 -0
- package/dist/src/cli/writerGuard.js.map +1 -0
- package/dist/src/index.d.ts +46 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +46 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/main.d.ts +4 -0
- package/dist/src/main.d.ts.map +1 -0
- package/dist/src/services/AgentSessionReader.d.ts +121 -0
- package/dist/src/services/AgentSessionReader.d.ts.map +1 -0
- package/dist/src/services/AgentSessionReader.js +392 -0
- package/dist/src/services/AgentSessionReader.js.map +1 -0
- package/dist/src/services/ClockifyAuth.d.ts +47 -0
- package/dist/src/services/ClockifyAuth.d.ts.map +1 -0
- package/dist/src/services/ClockifyAuth.js +2 -2
- package/dist/src/services/ClockifyAuth.js.map +1 -1
- package/dist/src/services/CodexTranscript.d.ts +112 -0
- package/dist/src/services/CodexTranscript.d.ts.map +1 -0
- package/dist/src/services/CodexTranscript.js +96 -0
- package/dist/src/services/CodexTranscript.js.map +1 -0
- package/dist/src/services/ConfigService.d.ts +99 -0
- package/dist/src/services/ConfigService.d.ts.map +1 -0
- package/dist/src/services/ConfigService.js +79 -7
- package/dist/src/services/ConfigService.js.map +1 -1
- package/dist/src/services/HomeDirectory.d.ts +17 -0
- package/dist/src/services/HomeDirectory.d.ts.map +1 -0
- package/dist/src/services/HomeDirectory.js +1 -1
- package/dist/src/services/IssueFacts.d.ts +127 -0
- package/dist/src/services/IssueFacts.d.ts.map +1 -0
- package/dist/src/services/IssueFacts.js +277 -0
- package/dist/src/services/IssueFacts.js.map +1 -0
- package/dist/src/services/ProviderDeletion.d.ts +19 -0
- package/dist/src/services/ProviderDeletion.d.ts.map +1 -0
- package/dist/src/services/ProviderDeletion.js +37 -0
- package/dist/src/services/ProviderDeletion.js.map +1 -0
- package/dist/src/services/ReconcileService.d.ts +378 -0
- package/dist/src/services/ReconcileService.d.ts.map +1 -0
- package/dist/src/services/ReconcileService.js +1270 -79
- package/dist/src/services/ReconcileService.js.map +1 -1
- package/dist/src/services/SavedEntries.d.ts +40 -0
- package/dist/src/services/SavedEntries.d.ts.map +1 -0
- package/dist/src/services/SavedEntries.js +209 -0
- package/dist/src/services/SavedEntries.js.map +1 -0
- package/dist/src/services/SessionAttributor.d.ts +114 -0
- package/dist/src/services/SessionAttributor.d.ts.map +1 -0
- package/dist/src/services/SessionAttributor.js +240 -0
- package/dist/src/services/SessionAttributor.js.map +1 -0
- package/dist/src/services/SourceLedger.d.ts +104 -0
- package/dist/src/services/SourceLedger.d.ts.map +1 -0
- package/dist/src/services/SourceLedger.js +302 -0
- package/dist/src/services/SourceLedger.js.map +1 -0
- package/dist/src/services/StateWriter.d.ts +39 -0
- package/dist/src/services/StateWriter.d.ts.map +1 -0
- package/dist/src/services/TicketService.d.ts +63 -0
- package/dist/src/services/TicketService.d.ts.map +1 -0
- package/dist/src/services/TimerService.d.ts +145 -0
- package/dist/src/services/TimerService.d.ts.map +1 -0
- package/dist/src/services/TimerService.js +113 -80
- package/dist/src/services/TimerService.js.map +1 -1
- package/dist/src/services/internal/JiraWorklogPost.d.ts +7 -0
- package/dist/src/services/internal/JiraWorklogPost.d.ts.map +1 -0
- package/dist/src/services/internal/JiraWorklogPost.js +32 -0
- package/dist/src/services/internal/JiraWorklogPost.js.map +1 -0
- package/dist/src/testing/fakeHeadless.d.ts +318 -0
- package/dist/src/testing/fakeHeadless.d.ts.map +1 -0
- package/dist/src/testing/fakeHeadless.js +1149 -0
- package/dist/src/testing/fakeHeadless.js.map +1 -0
- package/dist/src/tui/App.d.ts +6 -0
- package/dist/src/tui/App.d.ts.map +1 -0
- package/dist/src/tui/App.js +1 -1
- package/dist/src/tui/App.js.map +1 -1
- package/dist/src/tui/atoms/runtime.d.ts +8 -0
- package/dist/src/tui/atoms/runtime.d.ts.map +1 -0
- package/dist/src/tui/atoms/runtime.js +1 -1
- package/dist/src/tui/atoms/runtime.js.map +1 -1
- package/dist/src/tui/atoms/tickets.d.ts +3 -0
- package/dist/src/tui/atoms/tickets.d.ts.map +1 -0
- package/dist/src/tui/atoms/timer.d.ts +11 -0
- package/dist/src/tui/atoms/timer.d.ts.map +1 -0
- package/dist/src/tui/atoms/timer.js +1 -1
- package/dist/src/tui/atoms/timer.js.map +1 -1
- package/dist/src/tui/atoms/ui.d.ts +11 -0
- package/dist/src/tui/atoms/ui.d.ts.map +1 -0
- package/dist/src/tui/atoms/ui.js +1 -1
- package/dist/src/tui/atoms/ui.js.map +1 -1
- package/dist/src/tui/components/BigTimer.d.ts +2 -0
- package/dist/src/tui/components/BigTimer.d.ts.map +1 -0
- package/dist/src/tui/components/Footer.d.ts +2 -0
- package/dist/src/tui/components/Footer.d.ts.map +1 -0
- package/dist/src/tui/components/Header.d.ts +2 -0
- package/dist/src/tui/components/Header.d.ts.map +1 -0
- package/dist/src/tui/components/Header.js +1 -1
- package/dist/src/tui/components/Header.js.map +1 -1
- package/dist/src/tui/components/PopupInput.d.ts +18 -0
- package/dist/src/tui/components/PopupInput.d.ts.map +1 -0
- package/dist/src/tui/components/PopupMessage.d.ts +20 -0
- package/dist/src/tui/components/PopupMessage.d.ts.map +1 -0
- package/dist/src/tui/components/TicketList.d.ts +2 -0
- package/dist/src/tui/components/TicketList.d.ts.map +1 -0
- package/dist/src/tui/components/TicketList.js +1 -1
- package/dist/src/tui/components/TicketList.js.map +1 -1
- package/dist/src/tui/components/TicketRow.d.ts +8 -0
- package/dist/src/tui/components/TicketRow.d.ts.map +1 -0
- package/dist/src/tui/components/TimerDisplay.d.ts +2 -0
- package/dist/src/tui/components/TimerDisplay.d.ts.map +1 -0
- package/dist/src/tui/components/index.d.ts +12 -0
- package/dist/src/tui/components/index.d.ts.map +1 -0
- package/dist/src/tui/context/theme.d.ts +15 -0
- package/dist/src/tui/context/theme.d.ts.map +1 -0
- package/dist/src/tui/hooks/useElapsedTimer.d.ts +6 -0
- package/dist/src/tui/hooks/useElapsedTimer.d.ts.map +1 -0
- package/dist/src/tui/hooks/useElapsedTimer.js +1 -1
- package/dist/src/tui/hooks/useElapsedTimer.js.map +1 -1
- package/dist/src/tui/hooks/useTerminalSize.d.ts +5 -0
- package/dist/src/tui/hooks/useTerminalSize.d.ts.map +1 -0
- package/dist/src/utils/hints.d.ts +12 -0
- package/dist/src/utils/hints.d.ts.map +1 -0
- package/dist/src/utils/hints.js +12 -0
- package/dist/src/utils/hints.js.map +1 -0
- package/dist/src/utils/time.d.ts +104 -0
- package/dist/src/utils/time.d.ts.map +1 -0
- package/dist/src/utils/time.js +74 -8
- package/dist/src/utils/time.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -18
- 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
|
+
"version": "1.4.0",
|
|
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
|
|
29
|
-
"@effect/platform-node": "4.0.0
|
|
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.
|
|
36
|
-
"@opentui/react": "0.5.
|
|
37
|
-
"effect": "4.0.0
|
|
38
|
-
"react": "^19.
|
|
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
|
|
65
|
+
"@effect/vitest": "4.0.0",
|
|
42
66
|
"@types/node": "latest",
|
|
43
|
-
"@types/react": "^19.
|
|
44
|
-
"tsx": "^4.23.
|
|
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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"}
|