ecoportal-api-v2 3.3.2 → 3.3.3

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.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * token-report.ts
3
+ *
4
+ * Cross-project token usage report. Reads weekly session logs from all aligned
5
+ * projects (via local/paths.md) and produces a management-ready summary.
6
+ *
7
+ * Usage:
8
+ * npx ts-node .ai-assistance/scripts/token-report.ts
9
+ * npx ts-node .ai-assistance/scripts/token-report.ts --week 2026-W24
10
+ * npx ts-node .ai-assistance/scripts/token-report.ts --format json
11
+ *
12
+ * Output: Markdown table (default) or JSON (--format json)
13
+ * The report is printed to stdout — redirect to a file or pipe to your reporting tool.
14
+ */
15
+
16
+ import * as fs from "fs";
17
+ import * as path from "path";
18
+
19
+ const SCRIPTS_DIR = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
20
+ const AI_DIR = path.join(SCRIPTS_DIR, "..");
21
+ const CWD = path.join(AI_DIR, "..", "..");
22
+
23
+ // ── Helpers ────────────────────────────────────────────────────────────────
24
+
25
+ function isoWeek(d: Date): string {
26
+ const jan4 = new Date(d.getFullYear(), 0, 4);
27
+ const s = new Date(jan4); s.setDate(jan4.getDate() - ((jan4.getDay() + 6) % 7));
28
+ return `${d.getFullYear()}-W${String(Math.ceil(((d.getTime() - s.getTime()) / 86400000 + 1) / 7)).padStart(2, "0")}`;
29
+ }
30
+
31
+ function loadJson<T>(p: string, fb: T): T {
32
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return fb; }
33
+ }
34
+
35
+ interface ProjectPath { alias: string; localPath: string; }
36
+
37
+ function loadProjectPaths(): ProjectPath[] {
38
+ const pathsFile = path.join(AI_DIR, "local", "paths.md");
39
+ if (!fs.existsSync(pathsFile)) return [{ alias: path.basename(CWD), localPath: CWD }];
40
+ const projects: ProjectPath[] = [];
41
+ const seen = new Set<string>();
42
+ for (const line of fs.readFileSync(pathsFile, "utf8").split("\n")) {
43
+ const m = line.match(/\|\s*`([^`]+)`\s*\|\s*`([^`]+)`/);
44
+ if (!m) continue;
45
+ const [, alias, rawPath] = m;
46
+ const localPath = rawPath.replace(/\//g, path.sep);
47
+ const real = fs.existsSync(localPath) ? fs.realpathSync(localPath) : localPath;
48
+ if (!seen.has(real) && fs.existsSync(localPath)) { projects.push({ alias, localPath }); seen.add(real); }
49
+ }
50
+ return projects.length ? projects : [{ alias: path.basename(CWD), localPath: CWD }];
51
+ }
52
+
53
+ interface WeeklyData {
54
+ week_id: string;
55
+ total_tokens: number;
56
+ projects: Record<string, { project: string; priority: string; tokens: number; tool_calls: number; turns: number; }>;
57
+ }
58
+
59
+ interface SessionTurn {
60
+ ts: string; session_id: string; project: string; priority: string;
61
+ week_id: string; turn_tokens: number; session_total_tokens: number;
62
+ tool_calls: number; estimated: boolean;
63
+ }
64
+
65
+ // ── Main ───────────────────────────────────────────────────────────────────
66
+
67
+ const args = process.argv.slice(2);
68
+ const weekArg = args.includes("--week") ? args[args.indexOf("--week") + 1] : null;
69
+ const format = args.includes("--format") ? args[args.indexOf("--format") + 1] : "text";
70
+ const weekId = weekArg || isoWeek(new Date());
71
+
72
+ const projects = loadProjectPaths();
73
+
74
+ // Aggregate across all projects
75
+ const byProject: Record<string, {
76
+ tokens: number; tool_calls: number; turns: number;
77
+ priority: string; sessions: number; estimated: number;
78
+ }> = {};
79
+
80
+ let grandTotal = 0;
81
+
82
+ for (const { localPath } of projects) {
83
+ const kpiDir = path.join(localPath, ".ai-assistance", "local", "kpi");
84
+ const weeklyFile = path.join(kpiDir, `weekly-${weekId}.json`);
85
+ const budget = loadJson<any>(path.join(localPath, ".ai-assistance", "token-budget.json"), {});
86
+ const projectName= budget.project?.name || path.basename(localPath);
87
+ const priority = budget.project?.priority || "medium";
88
+
89
+ if (!fs.existsSync(weeklyFile)) continue;
90
+
91
+ const weekly = loadJson<WeeklyData>(weeklyFile, { week_id: weekId, total_tokens: 0, projects: {} });
92
+
93
+ // Sum sessions for this project
94
+ let tokens = 0, toolCalls = 0, turns = 0, sessions = 0, estimated = 0;
95
+ for (const s of Object.values(weekly.projects)) {
96
+ if (s.project === projectName) {
97
+ tokens += s.tokens;
98
+ toolCalls += s.tool_calls || 0;
99
+ turns += s.turns || 0;
100
+ sessions++;
101
+ }
102
+ }
103
+
104
+ // Count estimated sessions from JSONL
105
+ const jsonl = path.join(kpiDir, `sessions-${weekId}.jsonl`);
106
+ if (fs.existsSync(jsonl)) {
107
+ const lines = fs.readFileSync(jsonl, "utf8").split("\n").filter(Boolean);
108
+ estimated = lines.filter(l => { try { return JSON.parse(l).estimated; } catch { return false; } }).length;
109
+ }
110
+
111
+ if (tokens > 0) {
112
+ byProject[projectName] = { tokens, tool_calls: toolCalls, turns, priority, sessions, estimated };
113
+ grandTotal += tokens;
114
+ }
115
+ }
116
+
117
+ if (format === "json") {
118
+ console.log(JSON.stringify({ week_id: weekId, grand_total_tokens: grandTotal, projects: byProject }, null, 2));
119
+ process.exit(0);
120
+ }
121
+
122
+ // ── Text report ────────────────────────────────────────────────────────────
123
+
124
+ const sortedProjects = Object.entries(byProject)
125
+ .sort(([, a], [, b]) => b.tokens - a.tokens);
126
+
127
+ console.log(`\n${"=".repeat(70)}`);
128
+ console.log(` Token Usage Report — ${weekId}`);
129
+ console.log(` Generated: ${new Date().toISOString().slice(0, 16)}`);
130
+ console.log(`${"=".repeat(70)}\n`);
131
+ console.log(` Grand total: ${grandTotal.toLocaleString()} tokens across ${sortedProjects.length} project(s)\n`);
132
+
133
+ console.log(` ${"Project".padEnd(30)} ${"Priority".padEnd(10)} ${"Tokens".padStart(10)} ${"Share".padStart(7)} ${"Tool calls".padStart(12)} ${"Turns".padStart(6)}`);
134
+ console.log(` ${"-".repeat(78)}`);
135
+
136
+ for (const [name, data] of sortedProjects) {
137
+ const share = grandTotal > 0 ? Math.round((data.tokens / grandTotal) * 100) : 0;
138
+ const est = data.estimated > 0 ? " ~" : " ";
139
+ console.log(` ${name.padEnd(30)} ${data.priority.padEnd(10)} ${est}${data.tokens.toLocaleString().padStart(8)} ${`${share}%`.padStart(7)} ${data.tool_calls.toString().padStart(12)} ${data.turns.toString().padStart(6)}`);
140
+ }
141
+
142
+ console.log(`\n ~ = some sessions used token estimation (transcript had no usage data)`);
143
+
144
+ // Priority allocation analysis
145
+ const targetPct = 75;
146
+ const weights = { high: 50, medium: 30, low: 20 };
147
+ console.log(`\n Priority allocation vs actuals (target utilization: ${targetPct}%):`);
148
+ for (const priority of ["high", "medium", "low"]) {
149
+ const w = (weights as any)[priority];
150
+ const actual = Object.values(byProject)
151
+ .filter(p => p.priority === priority)
152
+ .reduce((s, p) => s + p.tokens, 0);
153
+ const actualPct = grandTotal > 0 ? Math.round((actual / grandTotal) * 100) : 0;
154
+ const projects = Object.entries(byProject).filter(([, p]) => p.priority === priority).map(([n]) => n).join(", ") || "(none)";
155
+ console.log(` ${priority.padEnd(8)} target ~${w}% actual ${`${actualPct}%`.padStart(4)} — ${projects}`);
156
+ }
157
+
158
+ console.log(`\n${"=".repeat(70)}\n`);
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * token-session-start.js
4
+ *
5
+ * Claude Code SessionStart hook.
6
+ * Checks current week's token usage, warns if over allocation,
7
+ * and shows the budget status for this project.
8
+ *
9
+ * Wired in .claude/settings.json:
10
+ * "SessionStart": [{ "type": "command", "command": "node .ai-assistance/scripts/token-session-start.js" }]
11
+ */
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+
16
+ function isoWeek(d) {
17
+ const jan4 = new Date(d.getFullYear(), 0, 4);
18
+ const s = new Date(jan4); s.setDate(jan4.getDate() - ((jan4.getDay() + 6) % 7));
19
+ return `${d.getFullYear()}-W${String(Math.ceil(((d - s) / 86400000 + 1) / 7)).padStart(2, "0")}`;
20
+ }
21
+ function loadJson(p, fb) { try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return fb; } }
22
+
23
+ async function main() {
24
+ let event = {};
25
+ try { event = JSON.parse(fs.readFileSync("/dev/stdin", "utf8")); } catch {}
26
+
27
+ const cwd = event.cwd || process.cwd();
28
+ const weekId = isoWeek(new Date());
29
+ const budget = loadJson(path.join(cwd, ".ai-assistance", "token-budget.json"), {});
30
+ const weekly = loadJson(path.join(cwd, ".ai-assistance", "local", "kpi", `weekly-${weekId}.json`), { total_tokens: 0, projects: {} });
31
+
32
+ const project = budget.project?.name || path.basename(cwd);
33
+ const priority = budget.project?.priority || "medium";
34
+ const total = budget.weekly_quota?.total_tokens;
35
+ const targetPct= (budget.weekly_quota?.target_utilization_pct || 75);
36
+
37
+ // My project's tokens this week across all sessions
38
+ const myTokens = Object.values(weekly.projects)
39
+ .filter(p => p.project === project)
40
+ .reduce((s, p) => s + p.tokens, 0);
41
+
42
+ const lines = [`\n[token-budget] Week ${weekId} | Project: ${project} (${priority})`];
43
+ lines.push(` All projects this week: ${weekly.total_tokens.toLocaleString()} tokens`);
44
+ lines.push(` This project this week: ${myTokens.toLocaleString()} tokens`);
45
+
46
+ if (total) {
47
+ const usedPct = Math.round((weekly.total_tokens / total) * 100);
48
+ const weights = budget.project_allocation?.priority_weights || { high: 50, medium: 30, low: 20 };
49
+ const myAlloc = Math.round(total * (targetPct / 100) * ((weights[priority] || 30) / 100));
50
+ const myPct = myAlloc > 0 ? Math.round((myTokens / myAlloc) * 100) : 0;
51
+ lines.push(` Week quota: ${usedPct}% used (target ${targetPct}%) | This project: ${myPct}% of ${myAlloc.toLocaleString()} alloc`);
52
+ if (usedPct >= targetPct) lines.push(` !! Over weekly target — consider deferring low-priority work`);
53
+ if (myPct >= 90) lines.push(` !! This project near allocation limit`);
54
+ }
55
+
56
+ process.stderr.write(lines.join("\n") + "\n");
57
+
58
+ // Also run bridge-init if it exists
59
+ const bridgeInit = path.join(cwd, ".ai-assistance", "scripts", "bridge-init.sh");
60
+ if (fs.existsSync(bridgeInit)) {
61
+ const { execSync } = require("child_process");
62
+ try { execSync(`bash "${bridgeInit}"`, { stdio: "inherit" }); } catch {}
63
+ }
64
+ }
65
+
66
+ main().catch(() => {});
@@ -0,0 +1,262 @@
1
+ ---
2
+ name: eP-AI-Manager
3
+ version: 2.0.0
4
+ description: >
5
+ Manages alignment between this project's AI setup and the ecoPortal AI standards.
6
+ Phase 2: automated checklist checking against all standards, end-of-session KPI
7
+ and learning capture, migration plan application, token budget reporting.
8
+ Invoke at: session start, session end, or when you suspect drift.
9
+ triggers:
10
+ - check AI standards
11
+ - AI alignment
12
+ - standards update
13
+ - eP_AI_Manager
14
+ - is my AI setup current
15
+ - apply migration
16
+ - end of session
17
+ - session wrap-up
18
+ - capture learnings
19
+ - token report
20
+ standards_repo: "../ep-ai-standards"
21
+ standards_gitlab: "https://gitlab.ecoportal.co.nz/oscar/ep-ai-standards"
22
+ standards_version_file: ".ai-assistance/standards-version.json"
23
+ applicable_to:
24
+ - any
25
+ ---
26
+
27
+ # eP_AI_Manager
28
+
29
+ ## Role
30
+
31
+ You are the AI standards alignment manager for this project. You run automated
32
+ checks, capture session metrics, surface drift, and apply migration plans.
33
+ You do not make changes without explicit developer confirmation.
34
+
35
+ ---
36
+
37
+ ## On SESSION START — run automatically
38
+
39
+ ### 1. Token budget status
40
+
41
+ Read `.ai-assistance/local/kpi/weekly-<YYYY-WNN>.json` (current ISO week).
42
+ Report in one line:
43
+ ```
44
+ [token-budget] Week YYYY-WNN: N tokens used across M sessions this project | budget: X% of allocation
45
+ ```
46
+ If `.ai-assistance/token-budget.json` has `total_tokens: null`, report actuals only.
47
+ If usage ≥ 80% of the project's priority allocation: warn in bold.
48
+
49
+ ### 2. Overdue deferral check
50
+
51
+ Read `.ai-assistance/standards-version.json` → `deferred` array.
52
+ For each deferred item, compare `deferred-at` + allowed window (60 days medium,
53
+ 14 days high, 0 days critical) to today.
54
+ Report any overdue deferrals as: `[OVERDUE] <standard> deferred since <date> — must action now`
55
+
56
+ ### 3. Standards version drift
57
+
58
+ Read `.ai-assistance/standards-version.json` → `ep-ai-standards-version`.
59
+ Read `../ep-ai-standards/CHANGELOG.md` → find any releases newer than the current version.
60
+ If newer version exists: list changed standards and flag any `severity: high/critical` items.
61
+ One-line summary: `[standards] Project at v1.0.0, current is v1.2.0 — 2 standards changed (1 high)`
62
+
63
+ ---
64
+
65
+ ## On SESSION END / when explicitly invoked for wrap-up
66
+
67
+ ### 4. KPI capture prompt
68
+
69
+ Ask the developer:
70
+ ```
71
+ Session wrap-up — quick capture (skip any with 's'):
72
+
73
+ 1. Main task category today?
74
+ coding / bug_fixing / bug_prevention / documentation / communication /
75
+ post_release / troubleshooting / integration_delivery / skills_development
76
+
77
+ 2. Rough minutes saved by AI? (e.g. 60, or 's' to skip)
78
+
79
+ 3. Any skills developed? (e.g. ai-platform-architecture, s to skip)
80
+
81
+ 4. Any learnings worth capturing for the EPAI knowledge base?
82
+ Type a brief description, or 's' to skip.
83
+ ```
84
+
85
+ On answers received:
86
+ - Create/update `.ai-assistance/local/kpi/sessions-<YYYY-WNN>.jsonl` with a record
87
+ matching the schema in `../ep-ai-standards/kpi/schema.json`
88
+ - If a learning was described: create a draft at
89
+ `.ai-assistance/local/epai-drafts/EPAI-<date>-<slug>.md` using the format from
90
+ `../ep-ai-standards/standards/agents/corpus-source-taxonomy.md` → "Page template"
91
+
92
+ ### 5. Policy compliance reminder (if working on AI-related files)
93
+
94
+ If the session involved changes to `agents/`, `iam/`, `lambdas/`, `config/`, or
95
+ `.ai-assistance/skills/`:
96
+ ```
97
+ [policy-check] Run before pushing: python3 scripts/policy-check.py --changed-only
98
+ ```
99
+
100
+ ---
101
+
102
+ ## On-demand: FULL ALIGNMENT CHECK
103
+
104
+ Run when explicitly invoked with "check alignment" or "check AI standards".
105
+
106
+ For each standard in `../ep-ai-standards/standards/`:
107
+
108
+ ### standards/agents/skill-schema.md
109
+ - [ ] Every directory in `agents/` has either `SKILL.md` or `system_prompt_file` in `agent.yaml`
110
+ - [ ] SKILL.md frontmatter contains `name:`, `description:`, `triggers:`
111
+ - [ ] `name:` follows `<team>-<agent>` format (grep: `^name: [a-z]+-[a-z]`)
112
+ - [ ] Customer-facing SKILL.md has `<!-- BEGIN privacy_directive` marker
113
+
114
+ ### standards/agents/agent-manifest.md
115
+ - [ ] Every `agents/` subdirectory that has a SKILL.md also has an `agent.yaml`
116
+ - [ ] Every `agent.yaml` has: `name`, `team`, `role`, `status`, `owner`
117
+ - [ ] Specialist agents have `corpus_prefix:`
118
+ - [ ] Customer-facing agents have `automatic_learning_guard: true`
119
+ - [ ] Operator agents have `escalation:` and `access_restriction:` blocks
120
+ - [ ] No agent has `status: published` without being in the activation checklist
121
+
122
+ ### standards/agents/role-taxonomy.md
123
+ - [ ] No operator agent appears in any orchestrator's `triggers:` or skill list
124
+ - [ ] Operator agents have `escalation:` → `required: true`
125
+
126
+ ### standards/workflows/session-handoff.md
127
+ - [ ] `docs/worklog.md` exists
128
+ - [ ] `CLAUDE.md` contains the string "worklog"
129
+ - [ ] Worklog has an entry within the last 5 working sessions (check for dated `## ` headings)
130
+
131
+ ### standards/security/pii-handling.md
132
+ - [ ] No `real_value` field in any DynamoDB table definition or Lambda code
133
+ - [ ] Customer-facing SKILL.md has complete `BEGIN/END privacy_directive` block
134
+ - [ ] PII scrubber exists if corpus pipeline is used (check lambdas/)
135
+
136
+ ### standards/tooling/token-budget-management.md
137
+ - [ ] `.ai-assistance/token-budget.json` exists
138
+ - [ ] `project.name` and `project.priority` are filled in (not `{{PLACEHOLDER}}`)
139
+ - [ ] `.claude/settings.json` has `Stop` and `SessionStart` hooks with `token-logger.js`
140
+ - [ ] `.ai-assistance/local/` is in `.gitignore`
141
+
142
+ ### standards/tooling/cross-platform-ai-guidelines.md
143
+ - [ ] No bash scripts in the repo exceed ~200 lines (check with `wc -l`)
144
+ - [ ] No hardcoded `api.anthropic.com` (use AWS endpoint)
145
+
146
+ ### standards/kpi/schema.md
147
+ - [ ] `kpi/records/` or `.ai-assistance/local/` is in `.gitignore`
148
+ - [ ] At least one KPI session record exists (if AI has been used in the project)
149
+
150
+ **For each check:**
151
+ - PASS: note briefly
152
+ - FAIL: give the specific file, what's wrong, what to do
153
+ - WARN: note for awareness
154
+
155
+ After all checks: `[alignment] N pass, N warn, N fail — project at ep-ai-standards vX.Y.Z`
156
+
157
+ ---
158
+
159
+ ## On-demand: APPLY MIGRATION PLAN
160
+
161
+ When the developer says "apply migration" or "update standards":
162
+
163
+ 1. Read `.ai-assistance/standards-version.json` → current version
164
+ 2. Check if `../ep-ai-standards/migration/v{current}-to-v{target}/` exists
165
+ 3. Read `MIGRATION.md` — show the developer what will change and effort estimate
166
+ 4. **Ask for explicit confirmation before proceeding**
167
+ 5. If automated steps exist: `bash ../ep-ai-standards/migration/.../automated/apply.sh --target .`
168
+ 6. Run verify.sh — show results
169
+ 7. Update `.ai-assistance/standards-version.json` → new version + `applied-at: today`
170
+
171
+ Never run apply.sh without showing the developer its contents first.
172
+
173
+ ---
174
+
175
+ ## On-demand: STANDARDS VERSION UPDATE
176
+
177
+ When the developer says "update standards version" after manually applying changes:
178
+
179
+ 1. Ask: "Which version are you updating to? (e.g. 1.1.0)"
180
+ 2. Read `../ep-ai-standards/CHANGELOG.md` to confirm the version exists
181
+ 3. Update `.ai-assistance/standards-version.json` → `ep-ai-standards-version` and `applied-at`
182
+ 4. Confirm: "Updated to v{version}. Run full alignment check to verify?"
183
+
184
+ ---
185
+
186
+ ## Deferral recording
187
+
188
+ When a developer chooses to defer a finding:
189
+
190
+ ```json
191
+ // Add to .ai-assistance/standards-version.json → deferred array:
192
+ {
193
+ "standard": "tooling/claude-code",
194
+ "from-version": "1.0.0",
195
+ "severity": "medium",
196
+ "deferred-by": "oscar@ecoportal.co.nz",
197
+ "deferred-at": "2026-06-10",
198
+ "reason": "Bridge refactor planned for Q3",
199
+ "review-by": "2026-09-10"
200
+ }
201
+ ```
202
+
203
+ Calculate `review-by` automatically: medium = +60 days, high = +14 days.
204
+ Critical deferrals are not recorded — escalate to Oscar immediately.
205
+
206
+ ---
207
+
208
+ ## Severity handling
209
+
210
+ | Severity | Deferral | At session start |
211
+ |---|---|---|
212
+ | `low` | Unlimited | Mention only if asked |
213
+ | `medium` | 60 days | Warn after 30 days |
214
+ | `high` | 14 days | Warn every session after 7 days |
215
+ | `critical` | None | Block — escalate to Oscar |
216
+
217
+ ---
218
+
219
+ ## EPAI draft format
220
+
221
+ When capturing a learning, write to `.ai-assistance/local/epai-drafts/EPAI-<date>-<slug>.md`:
222
+
223
+ ```markdown
224
+ # [<TYPE>] <Brief title>
225
+
226
+ <!-- PAGE PROPERTIES -->
227
+ contributor_email: <developer email>
228
+ project_origin: <project name>
229
+ consent_timestamp: <today>
230
+ usage_scope: agent-corpus-eligible
231
+ evidence_link: <link to worklog session, commit, or MR>
232
+ last_verified: <today>
233
+ source_type: primary
234
+ <!-- END PAGE PROPERTIES -->
235
+
236
+ **Labels:** `epai-type:<type>` `epai-area:<area>` `epai-status:needs-review`
237
+
238
+ ## Observed Behaviour
239
+ <What actually happened — factual, specific>
240
+
241
+ ## Why it matters
242
+ <Impact if you don't know this>
243
+
244
+ ## Fix / Pattern
245
+ <What to do>
246
+
247
+ ---
248
+ *Curator: verify evidence link before promoting to Consolidated*
249
+ ```
250
+
251
+ Valid types: `gotcha`, `pattern`, `anti-pattern`, `gap`, `lesson`, `correction`,
252
+ `environment-quirk`, `prompt-trigger`.
253
+
254
+ ---
255
+
256
+ ## What this skill does NOT do
257
+
258
+ - Does not make file changes without explicit developer confirmation
259
+ - Does not apply migration scripts without showing contents first
260
+ - Does not defer `critical` findings
261
+ - Does not mark a check as PASS without verifying the actual file
262
+ - Does not create EPAI drafts without the developer providing the learning
@@ -0,0 +1,44 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "project": {
4
+ "name": "ecoportal-api-v2",
5
+ "priority": "medium",
6
+ "developer": "oscar@ecoportal.co.nz"
7
+ },
8
+ "weekly_quota": {
9
+ "total_tokens": null,
10
+ "target_utilization_pct": 75,
11
+ "reset_day": "monday",
12
+ "note": "total_tokens: null means track actuals without a hard cap. Set to e.g. 1000000 to enforce a budget."
13
+ },
14
+ "project_allocation": {
15
+ "priority_weights": {
16
+ "high": 50,
17
+ "medium": 30,
18
+ "low": 20
19
+ },
20
+ "note": "A 'high' priority project gets ~50% of the weekly budget; 'medium' gets ~30%; 'low' gets ~20%. These are soft targets — the system warns, not blocks."
21
+ },
22
+ "session_logging": {
23
+ "enabled": true,
24
+ "log_dir": ".ai-assistance/local/kpi",
25
+ "warn_at_pct": 80,
26
+ "prompt_category_at_stop": true
27
+ },
28
+ "task_categories": [
29
+ "coding",
30
+ "bug_fixing",
31
+ "bug_prevention",
32
+ "documentation",
33
+ "communication",
34
+ "post_release",
35
+ "troubleshooting",
36
+ "integration_delivery",
37
+ "skills_development"
38
+ ],
39
+
40
+ "auto_worker": {
41
+ "enabled": true,
42
+ "dangerously_skip_permissions": false
43
+ }
44
+ }
@@ -0,0 +1,107 @@
1
+ {
2
+ "permissions": {
3
+ "defaultMode": "auto",
4
+ "allowedTools": [
5
+ "Read"
6
+ ],
7
+ "permissionRules": [
8
+ {
9
+ "tool": "Write",
10
+ "pattern": "/**",
11
+ "action": "allow"
12
+ },
13
+ {
14
+ "tool": "StrReplace",
15
+ "pattern": "/**",
16
+ "action": "allow"
17
+ }
18
+ ],
19
+ "allow": [
20
+ "WebFetch(domain:anthropic.com)",
21
+ "WebFetch(domain:npmjs.com)",
22
+ "Write(.glaudeignore)",
23
+ "Update(.glaudeignore)",
24
+ "Bash(rm .ai-assistance/bridge/LOCK)",
25
+ "Read(.git/**)",
26
+ "Read(**/rubygems/**)",
27
+ "Write(*.md)",
28
+ "Edit(.ai-assistance/**)",
29
+ "Update(.ai-assistance/**)",
30
+ "Write(.ai-assistance/**)",
31
+ "Write(lib/**)",
32
+ "Write(spec/**)",
33
+ "Bash(git status)",
34
+ "Bash(git diff *)",
35
+ "Bash(git log *)",
36
+ "Bash(git add *)",
37
+ "Bash(git commit *)",
38
+ "PowerShell(git status)",
39
+ "PowerShell(git diff *)",
40
+ "PowerShell(git log *)",
41
+ "PowerShell(git add *)",
42
+ "PowerShell(git commit *)",
43
+ "Bash(npm test)",
44
+ "Bash(npm run lint)",
45
+ "Bash(npm run build)",
46
+ "Bash(vitest *)",
47
+ "Bash(jest *)",
48
+ "PowerShell(npm test)",
49
+ "PowerShell(npm run lint)",
50
+ "PowerShell(npm run build)",
51
+ "Bash(black .)",
52
+ "Bash(pytest)",
53
+ "Bash(python -m unittest)",
54
+ "Bash(ruff check *)",
55
+ "Bash(bundle exec rspec *)",
56
+ "Bash(bundle exec rubocop *)",
57
+ "PowerShell(pytest)",
58
+ "PowerShell(python -m unittest)",
59
+ "PowerShell(ruff check *)",
60
+ "PowerShell(bundle exec rspec *)",
61
+ "PowerShell(bundle exec rubocop *)",
62
+ "Bash(node .ai-assistance/scripts/token-logger.js)",
63
+ "Bash(node .ai-assistance/scripts/token-session-start.js)"
64
+ ],
65
+ "deny": [
66
+ "Read(*.env)",
67
+ "Read(./.env*)",
68
+ "Read(./secrets/**)",
69
+ "Bash(*cat *.env*)",
70
+ "Bash(*grep *.env*)",
71
+ "Bash(printenv*)",
72
+ "Bash(env)",
73
+ "Write(.git/*)",
74
+ "Edit(.git/*)",
75
+ "Bash(git push *)",
76
+ "Bash(rm -rf *)"
77
+ ]
78
+ },
79
+ "hooks": {
80
+ "SessionStart": [
81
+ {
82
+ "matcher": "",
83
+ "hooks": [
84
+ {
85
+ "type": "command",
86
+ "command": "node .ai-assistance/scripts/token-session-start.js 2>/dev/null || true"
87
+ }
88
+ ]
89
+ }
90
+ ],
91
+ "Stop": [
92
+ {
93
+ "matcher": "",
94
+ "hooks": [
95
+ {
96
+ "type": "command",
97
+ "command": "node .ai-assistance/scripts/token-logger.js 2>/dev/null || true"
98
+ },
99
+ {
100
+ "type": "command",
101
+ "command": "bash scripts/auto-worker-scheduler.sh 2>/dev/null || true"
102
+ }
103
+ ]
104
+ }
105
+ ]
106
+ }
107
+ }
data/.gitignore CHANGED
@@ -21,3 +21,5 @@ Gemfile.lock
21
21
  # rspec failure tracking
22
22
  .rspec_status
23
23
  scratch.rb
24
+
25
+ .ai-assistance/local/
data/CHANGELOG.md CHANGED
@@ -2,14 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [3.3.3] - 2026-06-xx
6
-
7
- ### Added
8
-
9
- ### Changed
5
+ ## [3.3.3] - 2026-07-05
10
6
 
11
7
  ### Fixed
12
8
 
9
+ - **`_cascaded_attributes_trace` — Ruby 3.x block-param order (TypeError crash).** In
10
+ `DoubleModel::Attributable::Nesting::CascadedCallback`, `_cascaded_attributes.each.with_object(out_trace)`
11
+ yields `(element, memo)`, but the block read them swapped (`|out, (attribute, obj_k)|`), so
12
+ `(attribute, obj_k)` destructured the **accumulator hash** and `attribute` became the trace `Hash` →
13
+ `send(<hash>)` raised `"… is not a symbol nor a string" (TypeError)` on **any** cascaded operation
14
+ (e.g. `Diffable#as_update` / `DiffService`) over a model with nested attributes. Latent because
15
+ Ruby 2.x tolerated the arg order; fatal under Ruby 3.x (crashed the live act-gov TOOCS integration
16
+ via the `ecoportal-api-graphql` consumer). Swapped to `|(attribute, obj_k), out|` (commit `6a2b1b5`).
17
+ **Regression spec added** (`spec/…/double_model_spec.rb`).
18
+
13
19
  ## [3.3.2] - 2026-06-05
14
20
 
15
21
  ### Added
@@ -178,7 +178,7 @@ module Ecoportal::API::Common::Content::DoubleModel::Attributable::Nesting
178
178
  nested_attrs: _cascaded_attributes.keys,
179
179
  attributes: {}
180
180
  }.merge(trace || {}).tap do |out_trace|
181
- _cascaded_attributes.each.with_object(out_trace) do |out, (attribute, obj_k)|
181
+ _cascaded_attributes.each.with_object(out_trace) do |(attribute, obj_k), out|
182
182
  next unless (obj = send(attribute))
183
183
 
184
184
  obj_k_path = key_path.dup.push(obj_k)
@@ -1,5 +1,5 @@
1
1
  module Ecoportal
2
2
  module API
3
- GEM2_VERSION = '3.3.2'.freeze
3
+ GEM2_VERSION = '3.3.3'.freeze
4
4
  end
5
5
  end