eco-helpers 3.2.18 → 3.2.22

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9238296c1185b73d2dfa18af81387f01e7b913af4fa3732491dfeed4a7ba6caf
4
- data.tar.gz: 4b8a882badc3d22c2ba166e45284e1b4d1e3912afa8a2452072765917165fe3b
3
+ metadata.gz: 2f79a58827dc2789ddfcc56e108d93520af92c15152b94338ca51e73cc388210
4
+ data.tar.gz: e15e69fdb8fa6bd37202aad7f01bebcda80c9aadacc23333a98846a27aeddffd
5
5
  SHA512:
6
- metadata.gz: 1b0abd929fdb1827a544d18ec6c2a2a49b0807caedae00603f6d656c8e8f4498b5ea500f8f3845fcd44187f156125916456326513da21134c16a73ba7eee32ee
7
- data.tar.gz: 755a6f6efa59752b4897506be1bdc2b408a2e7f84c26330b7c192dd805ff3851b13966e8516da6e61517d86b3cb267a6e8884a26069ea20f8be962d5e1886f18
6
+ metadata.gz: 14faa786d74eb1b096cea772f79903df7f16e0dd903ce8586621f4524413795f0720bad631b0ddbc892cee01d12e6445f2af5ff6ff808d5754bb8593959f5d9a
7
+ data.tar.gz: b8b28cba91173036b005e3aa9a8e88e0143a932e1342c4aa91f085c73d4e031191e5f09c8f71928547392e41291c8b0128d0455b1b646ab93422247b9cac57b9
data/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.2.22] - 2026-08-14
6
+
7
+ ### Fixed
8
+
9
+ - Republish of `3.2.21`: its `lib/eco/version.rb` shipped with doubled carriage returns
10
+ (`
11
+ ` line endings), causing a `warning: encountered
5
12
  in middle of line` on every
13
+ load. Functionally identical otherwise; `3.2.21` will be yanked.
14
+
15
+ ## [3.2.21] - 2026-08-14
16
+
17
+ ### Fixed
18
+
19
+ - Packaging-only republish of `3.2.19` with an allowlisted gemspec (backported from `3.3.0`).
20
+ Versions `3.2.16`-`3.2.19` shipped internal repository content (`.ai-assistance/` tooling,
21
+ `.claude/settings.json`) to rubygems.org via the old denylist `spec.files`; `3.2.19` is
22
+ yanked and `3.2.16`/`3.2.18` are queued for deletion by RubyGems support. `3.2.21` is the
23
+ identical `lib/` code packaged clean, so constraints like `'~> 3.2.0', '>= 3.2.19'` keep
24
+ resolving on the 3.2 line (its graphql dependency stays `~> 1.3`, satisfied by the clean
25
+ `1.3.16`). The version number `3.2.20` is intentionally skipped: it exists as a
26
+ tagged-but-deliberately-unpublished version (see the 3.3.0-era changelog corrections).
27
+
28
+ ## [3.2.19] - 2026-07-16
29
+
30
+ Farmers / `cans-upsert` reliability adoption + an ooze KPI counter fix. **Backwards-compatible.**
31
+ Cut from the `v3.2.18` tag (not `master`, which carries the native GraphQL activity/dashboard
32
+ readers depending on the unreleased gem `1.4.0`), so this ships needing only the published
33
+ `ecoportal-api-graphql 1.3.14`.
34
+
35
+ ### Changed
36
+
37
+ - **Floor `ecoportal-api-graphql` to `>= 1.3.14`** (was `>= 1.3.11`) — pulls the HttpClient
38
+ 429/1015 resilience fix, so bulk `cans-upsert` / register-update live runs are no longer
39
+ aborted by a single Cloudflare edge rate-limit.
40
+
41
+ ### Fixed
42
+
43
+ - **Ooze update KPI counters now count GraphQL updates.** `RegisterUpdateCase` tallied
44
+ `updated`/`failed` only when the result `is_a?(Ecoportal::API::Common::Response)`, but the
45
+ GraphQL compat layer returns an `Ecoportal::API::GraphQL::Compat::Response` — it duck-types
46
+ `success?`/`status` yet is not in that class hierarchy, so every GraphQL update was silently
47
+ uncounted (`Updated 0 (attempted: N)`, `Failed 0`) even when the write applied. The guard is
48
+ now a duck-type (`respond_to?(:success?)`); `false`/`nil` returns (dry-run / no-op) still skip.
49
+ **Note:** this fixes the *report* only — the updates themselves were already applying.
50
+
6
51
  ## [3.2.18] - 2026-07-10
7
52
 
8
53
  Version-identity + regression-guard release. **No behaviour change** vs the fixed `3.2.17` build —
@@ -54,7 +54,10 @@ class Eco::API::UseCases::OozeSamples::RegisterUpdateCase < Eco::API::UseCases::
54
54
  return unless (pending = queue_shift(ooze_id))
55
55
 
56
56
  update_ooze(pending).tap do |result|
57
- if result.is_a?(Ecoportal::API::Common::Response)
57
+ # Duck-type, not is_a?: the GraphQL Compat::Response responds to success?/status but is
58
+ # NOT an Ecoportal::API::Common::Response, so is_a? silently skipped every GraphQL update
59
+ # (updated/failed stuck at 0). false/nil returns (dry-run / no-op) still fall through.
60
+ if result.respond_to?(:success?)
58
61
  if result.success?
59
62
  @updated_oozes += 1
60
63
  else
@@ -188,7 +191,8 @@ class Eco::API::UseCases::OozeSamples::RegisterUpdateCase < Eco::API::UseCases::
188
191
  def update_oozes(batched_oozes = batch_queue)
189
192
  batched_oozes.each do |ooze|
190
193
  update_ooze(ooze).tap do |result|
191
- if result.is_a?(Ecoportal::API::Common::Response)
194
+ # Duck-type, not is_a? — see #before_loading_new_target (GraphQL Compat::Response).
195
+ if result.respond_to?(:success?)
192
196
  if result.success?
193
197
  @updated_oozes += 1
194
198
  else
data/lib/eco/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Eco
2
- VERSION = '3.2.18'.freeze
2
+ VERSION = '3.2.22'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eco-helpers
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.18
4
+ version: 3.2.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Segura
@@ -262,7 +262,7 @@ dependencies:
262
262
  version: '1.3'
263
263
  - - ">="
264
264
  - !ruby/object:Gem::Version
265
- version: 1.3.11
265
+ version: 1.3.14
266
266
  type: :runtime
267
267
  prerelease: false
268
268
  version_requirements: !ruby/object:Gem::Requirement
@@ -272,7 +272,7 @@ dependencies:
272
272
  version: '1.3'
273
273
  - - ">="
274
274
  - !ruby/object:Gem::Version
275
- version: 1.3.11
275
+ version: 1.3.14
276
276
  - !ruby/object:Gem::Dependency
277
277
  name: ecoportal-api-v2
278
278
  requirement: !ruby/object:Gem::Requirement
@@ -541,29 +541,9 @@ executables: []
541
541
  extensions: []
542
542
  extra_rdoc_files: []
543
543
  files:
544
- - ".ai-assistance/conventions/code-working-tree-protocol.md"
545
- - ".ai-assistance/scripts/token-logger.js"
546
- - ".ai-assistance/scripts/token-report.ts"
547
- - ".ai-assistance/scripts/token-session-start.js"
548
- - ".ai-assistance/skills/ep-ai-manager/SKILL.md"
549
- - ".ai-assistance/skills/ruby-scripting/SKILL.md"
550
- - ".ai-assistance/standards-version.json"
551
- - ".ai-assistance/token-budget.json"
552
- - ".claude/settings.json"
553
- - ".gitignore"
554
- - ".idea/.gitignore"
555
- - ".markdownlint.json"
556
- - ".rspec"
557
- - ".rubocop.yml"
558
- - ".ruby-version"
559
- - ".yardopts"
560
544
  - CHANGELOG.md
561
- - CLAUDE.md
562
- - Gemfile
563
545
  - LICENSE
564
546
  - README.md
565
- - Rakefile
566
- - eco-helpers.gemspec
567
547
  - lib/eco-helpers.rb
568
548
  - lib/eco/api.rb
569
549
  - lib/eco/api/common.rb
@@ -1,176 +0,0 @@
1
- # Code Working Tree Protocol
2
-
3
- When Claude Code needs to make changes to files **outside** `bridge/inbox/`, it must
4
- follow this protocol. This prevents Code's changes from mixing with in-progress CoWork
5
- edits and ensures a clean, traceable commit history.
6
-
7
- ---
8
-
9
- ## When this applies
10
-
11
- Any time Code intends to modify files in the working tree that are not bridge task files
12
- (i.e., not `.ai-assistance/bridge/inbox/` or `.ai-assistance/bridge/outbox/`).
13
-
14
- This includes: editing source files, updating documentation, changing scripts,
15
- modifying capabilities files, etc.
16
-
17
- ---
18
-
19
- ## Protocol
20
-
21
- ### 0. Check for a lock
22
-
23
- ```bash
24
- cat .ai-assistance/bridge/LOCK 2>/dev/null || echo "NO_LOCK"
25
- ```
26
-
27
- - **No lock:** proceed to step 1
28
- - **Lock exists, EXPIRES is in the future:** stop. Tell the user:
29
- > "Working tree is locked by [AGENT] ([USER]) since [ACQUIRED], working on: [INTENT].
30
- > Expires at [EXPIRES]. Please wait or check if the other session is still active."
31
- - **Lock exists, EXPIRES is in the past:** stale lock — safe to overwrite, proceed to step 1
32
-
33
- ---
34
-
35
- ### 1. Acquire the lock
36
-
37
- Write `.ai-assistance/bridge/LOCK` with full watermark:
38
-
39
- ```
40
- AGENT: code
41
- USER: [git config user.name, lowercased]
42
- ACQUIRED: [ISO 8601 now]
43
- EXPIRES: [ISO 8601 now + 30 minutes]
44
- INTENT: [one sentence — what you are about to change and why]
45
- FILES: [comma-separated list of files you plan to modify]
46
- ```
47
-
48
- Example:
49
- ```
50
- AGENT: code
51
- USER: oscar
52
- ACQUIRED: 2026-06-04T10:00:00Z
53
- EXPIRES: 2026-06-04T10:30:00Z
54
- INTENT: Update gitlab-mcp.md with new PAT scopes and rotation info
55
- FILES: .ai-assistance/integrations/gitlab-mcp.md
56
- ```
57
-
58
- ---
59
-
60
- ### 2. Check for unstaged changes that overlap with your planned files
61
-
62
- ```bash
63
- git status --short
64
- ```
65
-
66
- If the working tree is clean, skip to step 3.
67
-
68
- If there are unstaged/staged changes, compare them against the files listed in your LOCK:
69
-
70
- ```bash
71
- git diff --name-only HEAD
72
- git diff --cached --name-only
73
- ```
74
-
75
- - **No overlap with your FILES:** proceed — the changes are unrelated and won't pollute history
76
- - **Overlap with one or more of your FILES:** commit the unstaged changes first.
77
- Derive the commit message by running `git diff HEAD` on the overlapping files and
78
- writing a short imperative summary of what actually changed — do not use a generic
79
- message. Format: `wip: <what changed, e.g. "rename .claude to .ai-assistance across scripts">`
80
-
81
- ```bash
82
- git add -A
83
- git commit -m "wip: <derived from actual diff>"
84
- ```
85
-
86
- This keeps Code's subsequent commit clean and ensures both sets of changes build
87
- on the correct base. On a feature branch, `wip:` commits are fine — squash before MR.
88
-
89
- ---
90
-
91
- ### 3. Apply your changes
92
-
93
- Make the intended file edits. Stay within the scope declared in INTENT and FILES
94
- when you acquired the lock. If scope expands, update the LOCK file before proceeding.
95
-
96
- ---
97
-
98
- ### 4. Commit your changes
99
-
100
- ```bash
101
- git add -A
102
- git commit -m "[descriptive message — what Code changed and why]"
103
- ```
104
-
105
- Commit message should be specific enough that a teammate can understand the change
106
- without reading the diff. Example:
107
- ```
108
- docs: update gitlab-mcp.md scopes and rotation info for new PAT (April 2027 expiry)
109
- ```
110
-
111
- **Commit authorship — developer only by default:**
112
-
113
- Commits are authored by the developer alone (git's `user.name` / `user.email` config).
114
- Do NOT add `Co-Authored-By: Claude ...` to commit messages unless the developer
115
- explicitly requests it.
116
-
117
- Rationale: the commit history is the developer's professional record. Co-authorship is
118
- opt-in, not opt-out. If the developer wants to attribute AI involvement, they can add
119
- it themselves or ask Claude to include it for a specific commit.
120
-
121
- Before adding any co-authorship attribution, ask:
122
- > "Would you like to add AI co-authorship to this commit, or keep it as your commit alone?"
123
-
124
- Default answer if not asked: **developer only**.
125
-
126
- ---
127
-
128
- ### 5. Release the lock
129
-
130
- ```bash
131
- rm .ai-assistance/bridge/LOCK
132
- ```
133
-
134
- ---
135
-
136
- ## Quick reference
137
-
138
- ```bash
139
- # 0. Check lock
140
- cat .ai-assistance/bridge/LOCK 2>/dev/null || echo "NO_LOCK"
141
-
142
- # 1. Acquire lock
143
- cat > .ai-assistance/bridge/LOCK << EOF
144
- AGENT: code
145
- USER: oscar
146
- ACQUIRED: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
147
- EXPIRES: $(date -u -d "+30 minutes" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+30M +"%Y-%m-%dT%H:%M:%SZ")
148
- INTENT: <what you are changing>
149
- FILES: <files>
150
- EOF
151
-
152
- # 2. Check for overlapping unstaged changes
153
- git diff --name-only HEAD && git diff --cached --name-only
154
- # If any of those files overlap with your planned FILES → commit them first:
155
- git add -A && git commit -m "wip: <description of CoWork's in-progress work>"
156
- # If no overlap → skip, proceed directly
157
-
158
- # 3. Apply changes
159
- # ... make edits ...
160
-
161
- # 4. Commit your changes
162
- git add -A && git commit -m "<descriptive message>"
163
-
164
- # 5. Release lock
165
- rm .ai-assistance/bridge/LOCK
166
- ```
167
-
168
- ---
169
-
170
- ## Notes
171
-
172
- - If Code crashes mid-protocol, the LOCK will expire naturally (30 min timeout)
173
- - The `wip:` commit prefix signals to teammates that this was an auto-committed
174
- in-progress state — safe to squash or amend later
175
- - This protocol does not apply to bridge task processing (reading inbox, writing outbox)
176
- — those are read/write of bridge files only and don't touch the working tree
@@ -1,220 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * token-logger.js
4
- *
5
- * Claude Code Stop hook — fires after every AI response turn.
6
- * Reads the session transcript, extracts token usage, accumulates weekly totals,
7
- * and warns when approaching the project's budget allocation.
8
- *
9
- * Wired in .claude/settings.json:
10
- * "Stop": [{ "type": "command", "command": "node .ai-assistance/scripts/token-logger.js" }]
11
- *
12
- * Reads: stdin (Stop event JSON with session_id, transcript_path, cwd)
13
- * .ai-assistance/token-budget.json
14
- * .ai-assistance/local/kpi/session-<id>.json (running session state)
15
- * .ai-assistance/local/kpi/weekly-<YYYY-WNN>.json (weekly totals)
16
- *
17
- * Writes: .ai-assistance/local/kpi/session-<id>.json (updated state)
18
- * .ai-assistance/local/kpi/weekly-<YYYY-WNN>.json (updated totals)
19
- * .ai-assistance/local/kpi/sessions-<YYYY-WNN>.jsonl (completed turns)
20
- */
21
-
22
- const fs = require("fs");
23
- const path = require("path");
24
- const os = require("os");
25
-
26
- // ── Helpers ────────────────────────────────────────────────────────────────
27
-
28
- function isoWeek(d) {
29
- const jan4 = new Date(d.getFullYear(), 0, 4);
30
- const startOfWeek = new Date(jan4);
31
- startOfWeek.setDate(jan4.getDate() - ((jan4.getDay() + 6) % 7));
32
- const weekNum = Math.ceil(((d - startOfWeek) / 86400000 + 1) / 7);
33
- return `${d.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
34
- }
35
-
36
- function loadJson(p, fallback) {
37
- try { return JSON.parse(fs.readFileSync(p, "utf8")); }
38
- catch { return fallback; }
39
- }
40
-
41
- function saveJson(p, data) {
42
- fs.mkdirSync(path.dirname(p), { recursive: true });
43
- fs.writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf8");
44
- }
45
-
46
- function appendJsonl(p, obj) {
47
- fs.mkdirSync(path.dirname(p), { recursive: true });
48
- fs.appendFileSync(p, JSON.stringify(obj) + "\n", "utf8");
49
- }
50
-
51
- // ── Extract token usage from transcript JSONL ──────────────────────────────
52
-
53
- function extractUsageFromTranscript(transcriptPath) {
54
- if (!transcriptPath || !fs.existsSync(transcriptPath)) return null;
55
-
56
- let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheCreateTokens = 0;
57
- let toolCalls = 0, turns = 0, found = false;
58
-
59
- try {
60
- const lines = fs.readFileSync(transcriptPath, "utf8").split("\n").filter(Boolean);
61
- for (const line of lines) {
62
- try {
63
- const entry = JSON.parse(line);
64
- // Extract usage from any entry that has it
65
- const usage = entry.usage || entry.message?.usage;
66
- if (usage) {
67
- inputTokens += usage.input_tokens || 0;
68
- outputTokens += usage.output_tokens || 0;
69
- cacheReadTokens += usage.cache_read_input_tokens || 0;
70
- cacheCreateTokens+= usage.cache_creation_input_tokens|| 0;
71
- found = true;
72
- }
73
- // Count tool uses
74
- if (entry.type === "tool_use" || entry.tool_name) toolCalls++;
75
- // Count assistant turns
76
- if (entry.role === "assistant" || entry.type === "assistant") turns++;
77
- } catch { /* skip malformed lines */ }
78
- }
79
- } catch { return null; }
80
-
81
- if (!found) return null;
82
- return { inputTokens, outputTokens, cacheReadTokens, cacheCreateTokens, toolCalls, turns };
83
- }
84
-
85
- // ── Estimate tokens when transcript doesn't have usage data ───────────────
86
-
87
- function estimateFromTranscript(transcriptPath) {
88
- if (!transcriptPath || !fs.existsSync(transcriptPath)) {
89
- return { inputTokens: 0, outputTokens: 0, toolCalls: 0, turns: 0, estimated: true };
90
- }
91
- let inputChars = 0, outputChars = 0, toolCalls = 0, turns = 0;
92
- try {
93
- const lines = fs.readFileSync(transcriptPath, "utf8").split("\n").filter(Boolean);
94
- for (const line of lines) {
95
- try {
96
- const entry = JSON.parse(line);
97
- const content = JSON.stringify(entry.content || entry.text || "");
98
- if (entry.role === "user" || entry.type === "user") { inputChars += content.length; }
99
- if (entry.role === "assistant" || entry.type === "assistant") { outputChars += content.length; turns++; }
100
- if (entry.type === "tool_use" || entry.tool_name) { toolCalls++; inputChars += 500 * 4; }
101
- } catch { /* skip */ }
102
- }
103
- } catch {}
104
- return {
105
- inputTokens: Math.round(inputChars / 4),
106
- outputTokens: Math.round(outputChars / 4),
107
- cacheReadTokens: 0, cacheCreateTokens: 0,
108
- toolCalls, turns, estimated: true
109
- };
110
- }
111
-
112
- // ── Main ───────────────────────────────────────────────────────────────────
113
-
114
- async function main() {
115
- let event = {};
116
- try {
117
- const raw = fs.readFileSync("/dev/stdin", "utf8");
118
- event = JSON.parse(raw);
119
- } catch { /* no stdin or parse error — use empty event */ }
120
-
121
- const cwd = event.cwd || process.cwd();
122
- const sessionId = event.session_id || `unknown-${Date.now()}`;
123
- const transcriptPath = event.transcript_path;
124
-
125
- const budgetFile = path.join(cwd, ".ai-assistance", "token-budget.json");
126
- const kpiDir = path.join(cwd, ".ai-assistance", "local", "kpi");
127
- const weekId = isoWeek(new Date());
128
- const sessionFile = path.join(kpiDir, `session-${sessionId}.json`);
129
- const weeklyFile = path.join(kpiDir, `weekly-${weekId}.json`);
130
- const turnLogFile = path.join(kpiDir, `sessions-${weekId}.jsonl`);
131
-
132
- const budget = loadJson(budgetFile, {});
133
- const project = (budget.project?.name || path.basename(cwd));
134
- const priority= (budget.project?.priority || "medium");
135
- const targetPct = (budget.weekly_quota?.target_utilization_pct || 75) / 100;
136
- const warnAt = (budget.session_logging?.warn_at_pct || 80) / 100;
137
-
138
- // Extract usage from transcript
139
- const transcriptUsage = extractUsageFromTranscript(transcriptPath)
140
- || estimateFromTranscript(transcriptPath);
141
-
142
- // Load previous session state (accumulate across turns in a session)
143
- const prevSession = loadJson(sessionFile, {
144
- session_id: sessionId, project, priority,
145
- started_at: new Date().toISOString(),
146
- week_id: weekId,
147
- input_tokens: 0, output_tokens: 0,
148
- cache_read_tokens: 0, cache_create_tokens: 0,
149
- tool_calls: 0, turns: 0, estimated: false,
150
- });
151
-
152
- // Use transcript totals (they accumulate naturally) not deltas
153
- const sessionNow = {
154
- ...prevSession,
155
- input_tokens: transcriptUsage.inputTokens,
156
- output_tokens: transcriptUsage.outputTokens,
157
- cache_read_tokens: transcriptUsage.cacheReadTokens || 0,
158
- cache_create_tokens:transcriptUsage.cacheCreateTokens || 0,
159
- tool_calls: transcriptUsage.toolCalls,
160
- turns: transcriptUsage.turns,
161
- estimated: transcriptUsage.estimated || false,
162
- last_updated_at: new Date().toISOString(),
163
- };
164
-
165
- saveJson(sessionFile, sessionNow);
166
-
167
- // Update weekly totals — replace session contribution (re-compute from sessions)
168
- const weekly = loadJson(weeklyFile, { week_id: weekId, projects: {}, total_tokens: 0 });
169
- const sessionTotal = sessionNow.input_tokens + sessionNow.output_tokens;
170
- const prevContrib = (weekly.projects[sessionId]?.tokens || 0);
171
- weekly.projects[sessionId] = {
172
- project, priority, tokens: sessionTotal,
173
- tool_calls: sessionNow.tool_calls, turns: sessionNow.turns,
174
- updated_at: new Date().toISOString()
175
- };
176
- weekly.total_tokens = Object.values(weekly.projects).reduce((s, p) => s + p.tokens, 0);
177
- saveJson(weeklyFile, weekly);
178
-
179
- // Log the turn to the weekly JSONL (for cross-session analysis)
180
- appendJsonl(turnLogFile, {
181
- ts: new Date().toISOString(), session_id: sessionId, project, priority, week_id: weekId,
182
- turn_tokens: sessionTotal - prevContrib,
183
- session_total_tokens: sessionTotal,
184
- tool_calls: sessionNow.tool_calls,
185
- estimated: sessionNow.estimated,
186
- });
187
-
188
- // ── Budget warnings ────────────────────────────────────────────────────
189
-
190
- const totalTokens = budget.weekly_quota?.total_tokens;
191
- if (totalTokens) {
192
- const usedPct = weekly.total_tokens / totalTokens;
193
- const targetTokens = totalTokens * targetPct;
194
-
195
- // Priority-based soft allocation
196
- const weights = budget.project_allocation?.priority_weights || { high: 50, medium: 30, low: 20 };
197
- const myWeight = (weights[priority] || 30) / 100;
198
- const myBudget = totalTokens * targetPct * myWeight;
199
- const myUsed = Object.values(weekly.projects)
200
- .filter(p => p.project === project)
201
- .reduce((s, p) => s + p.tokens, 0);
202
- const myPct = myBudget > 0 ? myUsed / myBudget : 0;
203
-
204
- if (usedPct >= warnAt) {
205
- process.stderr.write(
206
- `\n[token-budget] ⚠ Week ${weekId}: ${Math.round(usedPct * 100)}% of quota used` +
207
- ` (${weekly.total_tokens.toLocaleString()}/${totalTokens.toLocaleString()} tokens)` +
208
- ` — target was ${Math.round(targetPct * 100)}%\n`
209
- );
210
- }
211
- if (myPct >= warnAt) {
212
- process.stderr.write(
213
- `[token-budget] ⚠ Project "${project}" (${priority}): ${Math.round(myPct * 100)}% of allocation` +
214
- ` (${myUsed.toLocaleString()}/${Math.round(myBudget).toLocaleString()} tokens)\n`
215
- );
216
- }
217
- }
218
- }
219
-
220
- main().catch(() => { /* never crash the hook */ });