@dtlabs/ai-toolkit 0.6.0 → 0.8.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/bin/cli.js CHANGED
@@ -67,6 +67,92 @@ function askConfirm(question) {
67
67
  });
68
68
  }
69
69
 
70
+ // ── allowlist ─────────────────────────────────────────────────────────────────
71
+
72
+ // INFRA-T01: Canonical Bash permission lists (FTR-012, AC-11, AC-12).
73
+ // These are fixed baselines — one combined list covering base read-only,
74
+ // .NET, and npm commands. Unused entries in a project are harmless.
75
+ const CANONICAL_ALLOW = [
76
+ 'ls', 'dir', 'cat', 'head', 'tail', 'find', 'grep', 'rg', 'wc', 'echo',
77
+ 'pwd', 'which', 'date',
78
+ 'git status', 'git diff', 'git log', 'git show', 'git branch', 'git rev-parse',
79
+ 'git add', 'git commit',
80
+ 'dotnet build', 'dotnet test', 'dotnet restore',
81
+ 'npm test', 'npm run build',
82
+ ];
83
+
84
+ // Dangerous outward-facing commands that must always surface a human prompt.
85
+ // git checkout stays on ask: it runs only in the implement-feature main loop
86
+ // (Step 5), never inside pm-phase3 worker agents. If pm-phase3 ever gains
87
+ // branch-management steps, this decision must be revisited.
88
+ const CANONICAL_ASK = [
89
+ 'git push', 'gh pr create', 'rm', 'del',
90
+ 'git checkout', 'git reset', 'git clean',
91
+ ];
92
+
93
+ // INFRA-T01: Format a raw command string into a Claude Code permission token.
94
+ // Every command — including no-argument ones like 'pwd' — is formatted as
95
+ // 'Bash(<cmd>:*)' for consistency across all entries.
96
+ function commandToPermission(cmd) {
97
+ return `Bash(${cmd}:*)`;
98
+ }
99
+
100
+ // INFRA-T02: Ensure an object parsed from settings.local.json has the expected
101
+ // shape. Mutates and returns the object so callers can chain immediately.
102
+ function normalizeSettings(obj) {
103
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) obj = {};
104
+ if (!obj.permissions || typeof obj.permissions !== 'object') obj.permissions = {};
105
+ if (!obj.permissions.Bash || typeof obj.permissions.Bash !== 'object') obj.permissions.Bash = {};
106
+ if (!Array.isArray(obj.permissions.Bash.allow)) obj.permissions.Bash.allow = [];
107
+ if (!Array.isArray(obj.permissions.Bash.ask)) obj.permissions.Bash.ask = [];
108
+ return obj;
109
+ }
110
+
111
+ // INFRA-T03: Return the dedup union of two arrays (preserves insertion order,
112
+ // all elements of a appear before novel elements of b).
113
+ function mergeArrays(a, b) {
114
+ return [...new Set([...a, ...b])];
115
+ }
116
+
117
+ // INFRA-T03: Remove from allow every entry that also appears in ask.
118
+ // This enforces the ask-beats-allow invariant: a command that could be
119
+ // dangerous is never silently auto-approved just because it was also listed
120
+ // in an allow array.
121
+ function applyAskBeatsAllow(allow, ask) {
122
+ const askSet = new Set(ask);
123
+ return allow.filter(cmd => !askSet.has(cmd));
124
+ }
125
+
126
+ // INFRA-T04: Read {destDir}/.claude/settings.local.json.
127
+ // Returns a descriptor so callers can distinguish missing vs. malformed.
128
+ // { exists: false, data: null, malformed: false } — file absent
129
+ // { exists: true, data: <obj>, malformed: false } — parsed OK
130
+ // { exists: true, data: null, malformed: true } — invalid JSON
131
+ function readSettings(destDir) {
132
+ const settingsPath = path.join(destDir, '.claude', 'settings.local.json');
133
+ if (!fs.existsSync(settingsPath)) return { exists: false, data: null, malformed: false };
134
+ let raw;
135
+ try { raw = fs.readFileSync(settingsPath, 'utf8'); } catch (err) {
136
+ console.error(`Warning: could not read settings.local.json — ${err.message}`);
137
+ return { exists: true, data: null, malformed: true };
138
+ }
139
+ try {
140
+ const data = JSON.parse(raw);
141
+ return { exists: true, data, malformed: false };
142
+ } catch {
143
+ return { exists: true, data: null, malformed: true };
144
+ }
145
+ }
146
+
147
+ // INFRA-T04: Write {destDir}/.claude/settings.local.json, creating the
148
+ // .claude/ directory if it does not exist. Throws on I/O failure so callers
149
+ // can catch and report gracefully.
150
+ function writeSettings(destDir, data) {
151
+ const settingsPath = path.join(destDir, '.claude', 'settings.local.json');
152
+ ensureDir(settingsPath);
153
+ fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2), 'utf8');
154
+ }
155
+
70
156
  // ── file enumeration ─────────────────────────────────────────────────────────
71
157
 
72
158
  function expandMappings(mappings) {
@@ -155,9 +241,71 @@ async function checkVersion(destRoot, force) {
155
241
  return true;
156
242
  }
157
243
 
244
+ // ── manifest ──────────────────────────────────────────────────────────────────
245
+
246
+ const MANIFEST_FILE = '.ai-toolkit-manifest.json';
247
+
248
+ function readManifest(destRoot) {
249
+ const manifestPath = path.join(destRoot, '.claude', MANIFEST_FILE);
250
+ if (!fs.existsSync(manifestPath)) return { files: [] };
251
+ try {
252
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
253
+ if (!parsed || !Array.isArray(parsed.files)) return { files: [] };
254
+ parsed.files = parsed.files.map(f => f.replace(/\\/g, '/'));
255
+ return parsed;
256
+ } catch {
257
+ console.log(dim('Previous manifest is corrupt; treating as empty'));
258
+ return { files: [] };
259
+ }
260
+ }
261
+
262
+ function computeOrphans(oldFiles, newFiles) {
263
+ const newSet = new Set(newFiles.map(f => f.replace(/\\/g, '/')));
264
+ return oldFiles
265
+ .map(f => f.replace(/\\/g, '/'))
266
+ .filter(f => !newSet.has(f));
267
+ }
268
+
269
+ function moveToTrash(destRoot, relativePath) {
270
+ const source = path.join(destRoot, relativePath);
271
+ if (!fs.existsSync(source)) return;
272
+ const trashPath = path.join(destRoot, '.claude', '.ai-toolkit-trash', relativePath);
273
+ ensureDir(trashPath);
274
+ try {
275
+ fs.renameSync(source, trashPath);
276
+ } catch (err) {
277
+ if (err.code === 'EXDEV') {
278
+ fs.copyFileSync(source, trashPath);
279
+ fs.unlinkSync(source);
280
+ } else {
281
+ throw err;
282
+ }
283
+ }
284
+ }
285
+
286
+ function writeManifest(destRoot, fileList) {
287
+ const manifestPath = path.join(destRoot, '.claude', MANIFEST_FILE);
288
+ const trashDir = path.join(destRoot, '.claude', '.ai-toolkit-trash');
289
+ const filtered = fileList.filter(rel => {
290
+ const abs = path.join(destRoot, rel);
291
+ return !abs.startsWith(trashDir + path.sep) && abs !== trashDir;
292
+ });
293
+ const manifest = {
294
+ version: TOOLKIT_VERSION,
295
+ installedAt: new Date().toISOString(),
296
+ files: filtered.map(f => f.replace(/\\/g, '/')),
297
+ };
298
+ try {
299
+ ensureDir(manifestPath);
300
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
301
+ } catch (err) {
302
+ console.log(dim(`Warning: could not write manifest — ${err.message}`));
303
+ }
304
+ }
305
+
158
306
  // ── install ───────────────────────────────────────────────────────────────────
159
307
 
160
- async function runInstall(label, mappings, force) {
308
+ async function runInstall(label, mappings, force, destRoot) {
161
309
  const files = expandMappings(mappings);
162
310
  const entries = categorize(files);
163
311
 
@@ -165,6 +313,58 @@ async function runInstall(label, mappings, force) {
165
313
  const modified = entries.filter(e => e.status === 'modified');
166
314
  const same = entries.filter(e => e.status === 'same');
167
315
 
316
+ // ── prune phase (before file copy) ───────────────────────────────────────────
317
+ const oldManifest = readManifest(destRoot);
318
+ const newFileSet = files.map(f => path.relative(destRoot, f.dest).replace(/\\/g, '/'));
319
+ const orphans = computeOrphans(oldManifest.files, newFileSet);
320
+
321
+ const existingOrphans = orphans.filter(o => fs.existsSync(path.join(destRoot, o)));
322
+
323
+ if (existingOrphans.length > 0) {
324
+ console.log();
325
+ console.log(`${bold('📦 Orphan cleanup')} ${clr('gray', '→')} ${clr('red', `${existingOrphans.length} stale file(s) found`)}`);
326
+ console.log(divider());
327
+ for (const orphan of existingOrphans) {
328
+ console.log(` ${clr('red', '∅')} ${clr('red', 'REMOVED ')} ${orphan}`);
329
+ }
330
+ console.log(divider());
331
+ }
332
+
333
+ let removedCount = 0;
334
+ let keptCount = 0;
335
+
336
+ if (force) {
337
+ for (const orphan of existingOrphans) {
338
+ moveToTrash(destRoot, orphan);
339
+ removedCount++;
340
+ console.log(` ${clr('red', '∅')} ${dim(orphan)}`);
341
+ }
342
+ } else {
343
+ for (const orphan of existingOrphans) {
344
+ const fullPath = path.join(destRoot, orphan);
345
+ if (!fs.existsSync(fullPath)) continue;
346
+ const ok = await askConfirm(` Move to trash ${clr('red', orphan)}?`);
347
+ if (ok) {
348
+ moveToTrash(destRoot, orphan);
349
+ console.log(` ${clr('green', '✔')} Moved to trash\n`);
350
+ removedCount++;
351
+ } else {
352
+ console.log(` ${clr('gray', '✖')} Kept as-is\n`);
353
+ keptCount++;
354
+ }
355
+ }
356
+ }
357
+
358
+ if (existingOrphans.length > 0) {
359
+ console.log(divider());
360
+ console.log(
361
+ ` ${clr('red', `∅ Moved: ${removedCount}`)}` +
362
+ ` ${clr('gray', `↪ .claude/.ai-toolkit-trash/`)}` +
363
+ ` ${clr('gray', `✖ Kept: ${keptCount}`)}\n`
364
+ );
365
+ }
366
+
367
+ // ── install plan display ──────────────────────────────────────────────────────
168
368
  console.log();
169
369
  console.log(`${bold('📦 Install plan')} ${clr('gray', '→')} ${clr('cyan', label)}`);
170
370
  console.log(divider());
@@ -186,6 +386,7 @@ async function runInstall(label, mappings, force) {
186
386
 
187
387
  if (modified.length === 0) {
188
388
  console.log(` ${clr('green', '✔')} All new files copied. No conflicts.\n`);
389
+ writeManifest(destRoot, newFileSet);
189
390
  return;
190
391
  }
191
392
 
@@ -196,6 +397,7 @@ async function runInstall(label, mappings, force) {
196
397
  fs.copyFileSync(e.src, e.dest);
197
398
  console.log(` ${clr('yellow', '↺')} ${dim(path.relative(process.cwd(), e.dest))}`);
198
399
  }
400
+ writeManifest(destRoot, newFileSet);
199
401
  return;
200
402
  }
201
403
 
@@ -223,6 +425,8 @@ async function runInstall(label, mappings, force) {
223
425
  ` ${clr('green', `✔ Overwritten: ${overwritten}`)}` +
224
426
  ` ${clr('gray', `✖ Kept as-is: ${skipped}`)}\n`
225
427
  );
428
+
429
+ writeManifest(destRoot, newFileSet);
226
430
  }
227
431
 
228
432
  // ── subagent spawn-depth check (verify & advise only — never write) ────────────
@@ -271,6 +475,175 @@ function checkSpawnDepth(destRoot) {
271
475
  console.log(` ${dim('Then restart Claude Code so the variable is loaded.')}\n`);
272
476
  }
273
477
 
478
+ // ── mergeAllowlist ────────────────────────────────────────────────────────────
479
+
480
+ // US-01-T01 / US-02-T01 / US-02-T02 / US-03-T01:
481
+ // Core pure function — creates or merges the Bash permission allowlist in
482
+ // {destDir}/.claude/settings.local.json using the canonical lists above.
483
+ //
484
+ // Return values:
485
+ // { status: 'written' } — fresh file created
486
+ // { status: 'merged', preserved: N } — merged with N pre-existing rules
487
+ // { status: 'reset', reason: 'malformed' } — invalid JSON was replaced
488
+ // { status: 'error', message: '...' } — I/O failure
489
+ function mergeAllowlist(destDir) {
490
+ if (!destDir || typeof destDir !== 'string') {
491
+ return { status: 'error', message: 'destDir must be a non-empty string' };
492
+ }
493
+ if (!fs.existsSync(destDir) || !fs.statSync(destDir).isDirectory()) {
494
+ return { status: 'error', message: `destination does not exist or is not a directory: ${destDir}` };
495
+ }
496
+
497
+ const canonicalAllow = CANONICAL_ALLOW.map(commandToPermission);
498
+ const canonicalAsk = CANONICAL_ASK.map(commandToPermission);
499
+
500
+ const { exists, data, malformed } = readSettings(destDir);
501
+
502
+ // Malformed JSON recovery (AC-05)
503
+ if (malformed) {
504
+ console.log('Warning: settings.local.json is not valid JSON; resetting to default');
505
+ const fresh = normalizeSettings({});
506
+ fresh.permissions.Bash.allow = canonicalAllow;
507
+ fresh.permissions.Bash.ask = canonicalAsk;
508
+ try {
509
+ writeSettings(destDir, fresh);
510
+ } catch (err) {
511
+ return { status: 'error', message: err.message };
512
+ }
513
+ return { status: 'reset', reason: 'malformed' };
514
+ }
515
+
516
+ // Fresh install: no existing file (AC-01)
517
+ if (!exists) {
518
+ const fresh = normalizeSettings({});
519
+ fresh.permissions.Bash.allow = canonicalAllow;
520
+ fresh.permissions.Bash.ask = canonicalAsk;
521
+ try {
522
+ writeSettings(destDir, fresh);
523
+ } catch (err) {
524
+ return { status: 'error', message: err.message };
525
+ }
526
+ return { status: 'written' };
527
+ }
528
+
529
+ // Merge path: existing file, valid JSON (AC-02, AC-03, AC-04)
530
+ const existing = normalizeSettings(data);
531
+ const existingAllow = existing.permissions.Bash.allow;
532
+ const existingAsk = existing.permissions.Bash.ask;
533
+
534
+ const countBefore = existingAllow.length + existingAsk.length;
535
+
536
+ let mergedAllow = mergeArrays(existingAllow, canonicalAllow);
537
+ const mergedAsk = mergeArrays(existingAsk, canonicalAsk);
538
+
539
+ // ask-beats-allow: strip from allow any cmd that is now in ask
540
+ mergedAllow = applyAskBeatsAllow(mergedAllow, mergedAsk);
541
+
542
+ existing.permissions.Bash.allow = mergedAllow;
543
+ existing.permissions.Bash.ask = mergedAsk;
544
+
545
+ try {
546
+ writeSettings(destDir, existing);
547
+ } catch (err) {
548
+ return { status: 'error', message: err.message };
549
+ }
550
+
551
+ const preserved = countBefore;
552
+ return { status: 'merged', preserved };
553
+ }
554
+
555
+ // INFRA-T01 (FTR-013):
556
+ // Append a new entry to {featureDir}/{prefix}-token-ledger.json atomically.
557
+ // If the file does not exist it is created. If JSON is malformed the file is
558
+ // overwritten with a single-element array containing the new entry.
559
+ //
560
+ // Algorithm:
561
+ // 1. Read and parse the existing ledger (or start with [])
562
+ // 2. Push the new entry
563
+ // 3. Write the full array back in one synchronous write (atomic)
564
+ function appendLedgerEntry(featureDir, prefix, entry) {
565
+ const filePath = path.join(featureDir, `${prefix}-token-ledger.json`);
566
+ let ledger = [];
567
+ if (fs.existsSync(filePath)) {
568
+ try {
569
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
570
+ ledger = Array.isArray(parsed) ? parsed : [];
571
+ } catch (_) {
572
+ console.log(`Warning: could not parse token ledger at ${filePath} — starting fresh`);
573
+ ledger = [];
574
+ }
575
+ }
576
+ ledger.push(entry);
577
+ const dir = path.dirname(filePath);
578
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
579
+ fs.writeFileSync(filePath, JSON.stringify(ledger, null, 2), 'utf8');
580
+ }
581
+
582
+ // INFRA-T02 (FTR-013):
583
+ // Find and update an existing entry in {featureDir}/{prefix}-token-ledger.json
584
+ // by agent key, atomically. Searches from the end of the array so that the most
585
+ // recent entry for a given key is updated (handles any accidental duplicates).
586
+ // If the file does not exist or the key is not found the call is a silent no-op.
587
+ //
588
+ // Algorithm:
589
+ // 1. Read and parse the existing ledger (silent return on missing/malformed)
590
+ // 2. Find the last entry where entry.agent === agentKey
591
+ // 3. Object.assign the updates onto that entry
592
+ // 4. Write the full array back in one synchronous write (atomic)
593
+ function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
594
+ const filePath = path.join(featureDir, `${prefix}-token-ledger.json`);
595
+ if (!fs.existsSync(filePath)) return;
596
+ let ledger;
597
+ try {
598
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
599
+ ledger = Array.isArray(parsed) ? parsed : [];
600
+ } catch (_) {
601
+ console.log(`Warning: could not parse token ledger at ${filePath} — skipping update for "${agentKey}"`);
602
+ return;
603
+ }
604
+ let idx = -1;
605
+ for (let i = ledger.length - 1; i >= 0; i--) {
606
+ if (ledger[i] && ledger[i].agent === agentKey) { idx = i; break; }
607
+ }
608
+ if (idx === -1) {
609
+ console.log(`Warning: ledger entry not found for agent key "${agentKey}"`);
610
+ return;
611
+ }
612
+ Object.assign(ledger[idx], updates);
613
+ fs.writeFileSync(filePath, JSON.stringify(ledger, null, 2), 'utf8');
614
+ }
615
+
616
+ // US-05-T01:
617
+ // Idempotently append `.claude/settings.local.json` to {destDir}/.gitignore.
618
+ // Creates .gitignore if it does not exist (AC-06, AC-07).
619
+ //
620
+ // Return values:
621
+ // { status: 'appended' } — line was added
622
+ // { status: 'already' } — line was already present
623
+ // { status: 'created' } — .gitignore did not exist; created with line
624
+ // { status: 'error', message } — I/O failure
625
+ function updateGitignore(destDir) {
626
+ const GITIGNORE_ENTRY = '.claude/settings.local.json';
627
+ const gitignorePath = path.join(destDir, '.gitignore');
628
+
629
+ try {
630
+ if (!fs.existsSync(gitignorePath)) {
631
+ fs.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}\n`, 'utf8');
632
+ return { status: 'created' };
633
+ }
634
+
635
+ const content = fs.readFileSync(gitignorePath, 'utf8');
636
+ const lines = content.split('\n').map(l => l.trim());
637
+ if (lines.includes(GITIGNORE_ENTRY)) return { status: 'already' };
638
+
639
+ const trailing = content.endsWith('\n') ? '' : '\n';
640
+ fs.writeFileSync(gitignorePath, `${content}${trailing}${GITIGNORE_ENTRY}\n`, 'utf8');
641
+ return { status: 'appended' };
642
+ } catch (err) {
643
+ return { status: 'error', message: err.message };
644
+ }
645
+ }
646
+
274
647
  // ── entry points ──────────────────────────────────────────────────────────────
275
648
 
276
649
  async function installLocal(targetDir, force) {
@@ -283,7 +656,7 @@ async function installLocal(targetDir, force) {
283
656
  { src: path.join(packageRoot, 'docs'), dest: path.join(targetDir, 'docs') },
284
657
  { src: path.join(packageRoot, 'CLAUDE.md'), dest: path.join(targetDir, 'CLAUDE.md') },
285
658
  ];
286
- await runInstall(`local project`, mappings, force);
659
+ await runInstall(`local project`, mappings, force, targetDir);
287
660
  writeInstalledVersion(targetDir);
288
661
  console.log(` ${clr('green', '✔')} ${bold('Install complete.')}\n`);
289
662
  checkSpawnDepth(targetDir);
@@ -309,7 +682,7 @@ async function installGlobal(force) {
309
682
  { src: path.join(packageRoot, 'docs'), dest: path.join(target, 'docs') },
310
683
  { src: path.join(packageRoot, 'CLAUDE.global.md'), dest: path.join(target, 'CLAUDE.md') },
311
684
  ];
312
- await runInstall('global Claude folder', mappings, force);
685
+ await runInstall('global Claude folder', mappings, force, target);
313
686
  writeInstalledVersion(target);
314
687
  console.log(` ${clr('green', '✔')} ${bold('Global install complete.')}\n`);
315
688
  checkSpawnDepth(homedir);
@@ -349,6 +722,42 @@ async function main() {
349
722
  await installGlobal(force);
350
723
  } else if (argv[0] === 'help' || argv[0] === '--help') {
351
724
  help();
725
+ } else if (argv[0] === 'merge-allowlist') {
726
+ const destDir = argv[1];
727
+ if (!destDir) {
728
+ console.error('Error: merge-allowlist requires a destination directory');
729
+ process.exit(1);
730
+ }
731
+ try {
732
+ const result = mergeAllowlist(destDir);
733
+ if (result.status === 'error') {
734
+ console.error(`Error: ${result.message}`);
735
+ process.exit(1);
736
+ }
737
+ console.log(`Allowlist: ${result.status}${result.preserved !== undefined ? ` (${result.preserved} rules preserved)` : ''}`);
738
+ process.exit(0);
739
+ } catch (err) {
740
+ console.error(`Error: ${err.message}`);
741
+ process.exit(1);
742
+ }
743
+ } else if (argv[0] === 'update-gitignore') {
744
+ const destDir = argv[1];
745
+ if (!destDir) {
746
+ console.error('Error: update-gitignore requires a destination directory');
747
+ process.exit(1);
748
+ }
749
+ try {
750
+ const result = updateGitignore(destDir);
751
+ if (result.status === 'error') {
752
+ console.error(`Error: ${result.message}`);
753
+ process.exit(1);
754
+ }
755
+ console.log(`Gitignore: ${result.status}`);
756
+ process.exit(0);
757
+ } catch (err) {
758
+ console.error(`Error: ${err.message}`);
759
+ process.exit(1);
760
+ }
352
761
  } else if (fs.existsSync(argv[0]) && fs.statSync(argv[0]).isDirectory()) {
353
762
  await installLocal(argv[0], force);
354
763
  } else {
@@ -371,5 +780,21 @@ if (require.main === module) {
371
780
  categorize,
372
781
  readInstalledVersion,
373
782
  NEVER_COPY,
783
+ readManifest,
784
+ computeOrphans,
785
+ moveToTrash,
786
+ writeManifest,
787
+ CANONICAL_ALLOW,
788
+ CANONICAL_ASK,
789
+ commandToPermission,
790
+ normalizeSettings,
791
+ mergeArrays,
792
+ applyAskBeatsAllow,
793
+ readSettings,
794
+ writeSettings,
795
+ mergeAllowlist,
796
+ updateGitignore,
797
+ appendLedgerEntry,
798
+ updateLedgerEntry,
374
799
  };
375
800
  }
@@ -0,0 +1,21 @@
1
+ {
2
+ "_comment": "Anthropic API pricing (USD per 1M tokens). Update when prices change. EUR conversion uses the rate below.",
3
+ "_source": "https://www.anthropic.com/pricing — last updated 2026-07-31",
4
+ "_split": "No per-agent input/output breakdown available in the ledger. Assume 80% input / 20% output as a reasonable approximation.",
5
+ "usd_to_eur": 0.92,
6
+ "usd_to_eur_date": "2026-07-31",
7
+ "models": {
8
+ "haiku": {
9
+ "input_per_1m_usd": 0.80,
10
+ "output_per_1m_usd": 4.00
11
+ },
12
+ "sonnet": {
13
+ "input_per_1m_usd": 3.00,
14
+ "output_per_1m_usd": 15.00
15
+ },
16
+ "opus": {
17
+ "input_per_1m_usd": 15.00,
18
+ "output_per_1m_usd": 75.00
19
+ }
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtlabs/ai-toolkit",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
5
5
  "files": [
6
6
  "docs/",
@@ -1,115 +0,0 @@
1
- # Token Optimization — Backlog di feature atomiche
2
-
3
- > Deriva da `docs/token-optimization.md`. Data: 2026-07-24.
4
- > Ogni voce è **atomica**: implementabile e verificabile in isolamento.
5
- > "Criticità" = priorità di ottimizzazione (impatto/rischio), NON severità di difetto.
6
- > Nessun intervento è un bug attivo → nessun `CRITICAL`.
7
- > Sequenza consigliata = ordine degli ID.
8
-
9
- ## Quadro sintetico
10
-
11
- | ID | Intervento | Criticità | Sforzo | Rischio | File toccati | Dipende da |
12
- |----|-----------|-----------|--------|---------|--------------|-----------|
13
- | OPT-01 ✅ | `model:` esplicito su ogni agent (→ FTR-007, PR #20) | **HIGH** | Basso | Basso | 15 agent (frontmatter) | — |
14
- | OPT-02 | Deduplicare CLAUDE.md globale vs progetto | **HIGH** | Minimo | Zero | `~/.claude/CLAUDE.md` | — |
15
- | OPT-03 | Blocco "Compact instructions" in CLAUDE.md | MEDIUM | Minimo | Zero | `CLAUDE.md` (progetto) | — |
16
- | OPT-04 | Spostare i cataloghi da CLAUDE.md a reference.md | MEDIUM | Basso | Basso | `CLAUDE.md`, `docs/reference.md` | — |
17
- | OPT-05 | Procedure on-demand — assessment-manager | MEDIUM | Medio | Basso | `assessment-manager.md` | — |
18
- | OPT-06 | Procedure on-demand — project-manager | MEDIUM | Medio | Basso | `project-manager.md` | — |
19
- | OPT-07 | Resource-budget testuale negli assessment agent | MEDIUM | Basso | Basso | 3+ assessment agent | — |
20
- | OPT-08 | Summary/full split negli output di assessment | **HIGH** | Alto | Medio | assessment agents + int-doc + manager | OPT-07 |
21
- | OPT-09 | Estrarre calcolo deterministico da assessment-manager | **HIGH** | Alto | Medio | `assessment-manager.md` + nuovo script | — |
22
- | OPT-10 | generic-software-assessment → ruolo triage | MEDIUM | Medio | Medio | `generic-software-assessment.md` + manager | OPT-08 |
23
- | OPT-11 | Delegation policy negli orchestratori | LOW | Basso | Basso | 2 orchestratori | — |
24
- | OPT-12 | Hook di filtro output verboso (opzionale) | LOW | Basso | Basso | `settings.json` + script hook | — |
25
-
26
- ---
27
-
28
- ## Feature atomiche
29
-
30
- ### OPT-01 — `model:` esplicito su ogni agent
31
- - **Criticità:** HIGH · **Sforzo:** Basso · **Rischio:** Basso
32
- - **Obiettivo:** ogni agent dichiara il modello adatto al suo ruolo, così nessuno eredita Opus per default.
33
- - **Scope (solo frontmatter):** aggiungere `model:` ai 15 agent che ne sono privi. Mappatura:
34
- - `sonnet`: `assessment-manager`, `project-manager`, `developer-backend`, `developer-frontend`, `developer-testing`, `god-class-decomposition`, `domain-model-refactoring`, `dependency-injection-refactoring`, `security-hardening`, `dependency-supply-chain-security`, `define-feature`, `init-agents-md`, `install-toolkit`, `intervention-documentation-standard`
35
- - `opus`: `review-solution` (unico ruolo con giudizio architetturale profondo)
36
- - **Fuori scope:** i 7 agent già `haiku` restano invariati.
37
- - **Criteri di accettazione:** `grep -L "^model:" .claude/agents/*.md` non restituisce nulla; ogni valore ∈ {haiku, sonnet, opus}.
38
- - **Dipendenze:** nessuna.
39
-
40
- ### OPT-02 — Deduplicare CLAUDE.md globale vs progetto
41
- - **Criticità:** HIGH · **Sforzo:** Minimo · **Rischio:** Zero
42
- - **Obiettivo:** eliminare il duplicato caricato a ogni sessione; il `CLAUDE.md` di progetto resta la fonte unica del catalogo toolkit.
43
- - **Scope:** ridurre `~/.claude/CLAUDE.md` alle sole preferenze cross-progetto (lingua, stile, convenzioni personali). Rimuovere il catalogo toolkit duplicato.
44
- - **Criteri di accettazione:** `diff ~/.claude/CLAUDE.md ./CLAUDE.md` NON è più identico; il globale non contiene più le tabelle Skills/Commands/Agents; il Gate Protocol resta accessibile (nel progetto).
45
- - **Dipendenze:** nessuna. ⚠️ File utente globale — confermare con l'utente prima di modificare.
46
-
47
- ### OPT-03 — Blocco "Compact instructions" in CLAUDE.md
48
- - **Criticità:** MEDIUM · **Sforzo:** Minimo · **Rischio:** Zero
49
- - **Obiettivo:** guidare l'auto-compaction a preservare ciò che conta (decisioni, file, finding IDs) e scartare rumore.
50
- - **Scope:** aggiungere al `CLAUDE.md` di progetto una sezione `# Compact instructions` (preserva: obiettivo corrente, decisioni confermate, risposte utente, file modificati, errori aperti, finding/artifact IDs; scarta: risultati grep grezzi, output tool riusciti, spiegazioni ripetute, piani superati).
51
- - **Criteri di accettazione:** la sezione esiste in `CLAUDE.md`; nessun'altra modifica.
52
- - **Dipendenze:** nessuna. (Coordinare con OPT-04 se si tocca lo stesso file.)
53
-
54
- ### OPT-04 — Spostare i cataloghi da CLAUDE.md a reference.md
55
- - **Criticità:** MEDIUM · **Sforzo:** Basso · **Rischio:** Basso
56
- - **Obiettivo:** CLAUDE.md sotto ~200 righe, solo essenziali operativi; il catalogo di scoperta vive in reference.md.
57
- - **Scope:** spostare le tabelle Skills / Commands / Agents / Procedures in `docs/reference.md`. In `CLAUDE.md` restano: Dependencies, How it works, override procedure, **Gate Protocol**. Aggiungere in CLAUDE.md un puntatore a reference.md.
58
- - **Criteri di accettazione:** `CLAUDE.md` < 200 righe; nessuna tabella-catalogo persa (presente in reference.md); Gate Protocol ancora in CLAUDE.md.
59
- - **Dipendenze:** nessuna.
60
-
61
- ### OPT-05 — Procedure on-demand (assessment-manager)
62
- - **Criticità:** MEDIUM · **Sforzo:** Medio · **Rischio:** Basso
63
- - **Obiettivo:** leggere ogni procedura nella fase in cui serve, non tutte all'avvio.
64
- - **Scope:** in `assessment-manager.md`, spostare le istruzioni "read at startup" così che `token-estimation.md`/`pricing.md` si leggano in Phase 3-4, `assessment-findings-gate.md` in Phase 5, ecc.
65
- - **Criteri di accettazione:** nessuna procedura letta prima della fase che la usa; il comportamento delle fasi resta invariato.
66
- - **Dipendenze:** nessuna. (Se si fa OPT-09, `token-estimation.md` sparisce del tutto dal contesto LLM.)
67
-
68
- ### OPT-06 — Procedure on-demand (project-manager)
69
- - **Criticità:** MEDIUM · **Sforzo:** Medio · **Rischio:** Basso
70
- - **Obiettivo:** stesso principio di OPT-05 applicato al `project-manager` (che legge ancora più procedure).
71
- - **Scope:** riorganizzare `project-manager.md` per caricare ogni procedura alla fase pertinente (PR-procedure alla fase PR, ecc.).
72
- - **Criteri di accettazione:** nessuna procedura caricata anticipatamente; pipeline invariata.
73
- - **Dipendenze:** nessuna.
74
-
75
- ### OPT-07 — Resource-budget testuale negli assessment agent
76
- - **Criticità:** MEDIUM · **Sforzo:** Basso · **Rischio:** Basso
77
- - **Obiettivo:** limiti di lettura/output come **istruzioni nel prompt** (NON frontmatter — `budget:` non è supportato dall'harness).
78
- - **Scope:** aggiungere una sezione `## Resource budget` in prosa agli assessment agent (es. "Inspect at most ~25 files; ≤2 evidence snippets per finding; stop when evidence is sufficient — do not explore to increase coverage").
79
- - **Criteri di accettazione:** la sezione è testo del prompt, non YAML; presente in `generic-software-assessment`, `layered-architecture-assessment`, `concurrency-safety-assessment`.
80
- - **Dipendenze:** nessuna.
81
-
82
- ### OPT-08 — Summary/full split negli output di assessment
83
- - **Criticità:** HIGH · **Sforzo:** Alto · **Rischio:** Medio
84
- - **Obiettivo:** l'orchestratore legge un **summary compatto**, non il documento intero. Il guadagno reale (non il formato JSON in sé).
85
- - **Scope:** ogni assessment agent produce `{PREFIX}-{area}.full.md` (dettagli) + `{PREFIX}-{area}.summary` (conteggi per severità, finding IDs, path del full). `assessment-manager` e `intervention-documentation-standard` consumano il summary; il full si apre solo per i finding accettati.
86
- - **Criteri di accettazione:** ogni agent scrive entrambi i file; il manager non legge i `.full` in Phase 3; l'int-doc apre i full solo per i finding selezionati.
87
- - **Dipendenze:** OPT-07 (budget di output coerente).
88
-
89
- ### OPT-09 — Estrarre calcolo deterministico da assessment-manager
90
- - **Criticità:** HIGH · **Sforzo:** Alto · **Rischio:** Medio
91
- - **Obiettivo:** togliere dal prompt ~3.500 parole di pseudo-codice (calcolo token/effort, registry write) che un LLM esegue in modo costoso e fragile.
92
- - **Scope:** spostare Token-Estimate / Effort-Estimate / registry-write in uno script (`scripts/estimate.*`) invocato via Bash; il prompt dell'agent si riduce a "esegui lo script con questi input e verifica l'output".
93
- - **Criteri di accettazione:** i tre file prodotti sono identici (per input noti) a prima; `assessment-manager.md` perde le sezioni di calcolo passo-passo; nessun errore aritmetico possibile lato LLM.
94
- - **Dipendenze:** nessuna hard; sinergica con OPT-05.
95
-
96
- ### OPT-10 — generic-software-assessment → ruolo triage
97
- - **Criticità:** MEDIUM · **Sforzo:** Medio · **Rischio:** Medio
98
- - **Obiettivo:** evitare che il generic duplichi il lavoro degli specialisti (layer, security, concurrency) sullo stesso codebase.
99
- - **Scope:** trasformare `generic-software-assessment` in triage: scansione superficiale che decide quali specialisti attivare e copre solo le aree non coperte (Opzione A della gap analysis).
100
- - **Criteri di accettazione:** il generic non riesamina aree già coperte da uno specialista attivo; l'output indica gli specialisti raccomandati.
101
- - **Dipendenze:** meglio dopo OPT-08 (contratto di output stabile).
102
-
103
- ### OPT-11 — Delegation policy negli orchestratori
104
- - **Criticità:** LOW · **Sforzo:** Basso · **Rischio:** Basso
105
- - **Obiettivo:** regola esplicita "delega un subagent solo se serve" (task indipendente/parallelo, output voluminoso, riuso multiplo); non delegare ciò che una grep/read risolve.
106
- - **Scope:** aggiungere una sezione `## Delegation policy` a `assessment-manager` e `project-manager`.
107
- - **Criteri di accettazione:** entrambi gli orchestratori contengono i criteri delega/non-delega.
108
- - **Dipendenze:** nessuna.
109
-
110
- ### OPT-12 — Hook di filtro output verboso (opzionale)
111
- - **Criticità:** LOW · **Sforzo:** Basso · **Rischio:** Basso
112
- - **Obiettivo:** filtrare output verbosi (build/test/log) prima che entrino nel contesto.
113
- - **Scope:** `PreToolUse` su Bash in `settings.json` + script che riduce l'output di test/build alle sole righe rilevanti.
114
- - **Criteri di accettazione:** l'hook appare in `/hooks`; un comando di test mostra solo failures.
115
- - **Dipendenze:** nessuna. Fare solo se gli assessment inizieranno a lanciare build/test.