@ludi-uni/ludi-agent-kit 0.1.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.
Files changed (172) hide show
  1. package/AGENTS.md +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -0
  4. package/adapters/codex/README.md +24 -0
  5. package/adapters/codex/skill-metadata/visual-verification/agents/openai.yaml +7 -0
  6. package/adapters/pi/README.md +88 -0
  7. package/adapters/pi/browser/agent-browser.mjs +193 -0
  8. package/adapters/pi/lib/invoke.mjs +55 -0
  9. package/adapters/pi/lib/list-models.mjs +29 -0
  10. package/adapters/pi/lib/settings-proposal.mjs +34 -0
  11. package/adapters/pi/lib/subagent.mjs +175 -0
  12. package/adapters/pi/loop-guard/index.js +51 -0
  13. package/adapters/pi/maintenance-policy.json +36 -0
  14. package/adapters/pi/mcp.template.json +4 -0
  15. package/adapters/pi/model-catalog.json +97 -0
  16. package/adapters/pi/models.json +13 -0
  17. package/adapters/pi/models.local.example.json +14 -0
  18. package/adapters/pi/orchestrator-ext/command.mjs +14 -0
  19. package/adapters/pi/orchestrator-ext/index.js +150 -0
  20. package/adapters/pi/settings.template.json +7 -0
  21. package/adapters/pi/shell-gate/index.js +70 -0
  22. package/adapters/pi/sync-pi.ps1 +137 -0
  23. package/agents/README.md +26 -0
  24. package/agents/browser.md +64 -0
  25. package/agents/coder.md +31 -0
  26. package/agents/orchestrator.md +37 -0
  27. package/agents/reviewer.md +32 -0
  28. package/agents/scout.md +35 -0
  29. package/agents/tester.md +28 -0
  30. package/agents/visual.md +28 -0
  31. package/context-pack/SPEC.md +101 -0
  32. package/context-pack/context-pack.schema.json +79 -0
  33. package/context-pack/examples/example-fix.md +44 -0
  34. package/docs/architecture.md +55 -0
  35. package/docs/migration-from-codex-setting.md +44 -0
  36. package/docs/model-maintenance.md +401 -0
  37. package/docs/orchestrator.md +155 -0
  38. package/docs/phase2-report.md +39 -0
  39. package/docs/roadmap.md +27 -0
  40. package/docs/third-party.md +15 -0
  41. package/lib/agents.mjs +79 -0
  42. package/lib/context-pack.mjs +215 -0
  43. package/lib/job.mjs +312 -0
  44. package/lib/language-policy.mjs +27 -0
  45. package/lib/maintenance-exec.mjs +377 -0
  46. package/lib/maintenance-runner.mjs +266 -0
  47. package/lib/maintenance.mjs +422 -0
  48. package/lib/normalize.mjs +101 -0
  49. package/lib/observe/differ.mjs +185 -0
  50. package/lib/observe/observation.mjs +147 -0
  51. package/lib/observe/observers.mjs +134 -0
  52. package/lib/observe/sources.mjs +154 -0
  53. package/lib/orchestrator/activity.mjs +249 -0
  54. package/lib/orchestrator/api.mjs +151 -0
  55. package/lib/orchestrator/contract.mjs +68 -0
  56. package/lib/orchestrator/escalation.mjs +84 -0
  57. package/lib/orchestrator/evaluator.mjs +92 -0
  58. package/lib/orchestrator/failures.mjs +88 -0
  59. package/lib/orchestrator/health.mjs +53 -0
  60. package/lib/orchestrator/orchestrator.mjs +483 -0
  61. package/lib/orchestrator/permissions.mjs +64 -0
  62. package/lib/orchestrator/planner.mjs +194 -0
  63. package/lib/orchestrator/policy.mjs +134 -0
  64. package/lib/orchestrator/router.mjs +45 -0
  65. package/lib/orchestrator/runner.mjs +278 -0
  66. package/lib/orchestrator/shell-policy.mjs +52 -0
  67. package/lib/orchestrator/store.mjs +581 -0
  68. package/lib/orchestrator/task-store.mjs +79 -0
  69. package/lib/orchestrator/turn-budget.mjs +63 -0
  70. package/lib/orchestrator/worktree.mjs +72 -0
  71. package/lib/pipeline.mjs +279 -0
  72. package/lib/registry.mjs +63 -0
  73. package/lib/resolve.mjs +35 -0
  74. package/lib/routing.mjs +137 -0
  75. package/lib/telemetry.mjs +222 -0
  76. package/mcp/README.md +11 -0
  77. package/mcp/servers.json +13 -0
  78. package/orchestration/decision-policy.json +66 -0
  79. package/package.json +56 -0
  80. package/routing/README.md +24 -0
  81. package/routing/routing.json +81 -0
  82. package/routing/routing.schema.json +66 -0
  83. package/rules/README.md +10 -0
  84. package/rules/common.md +52 -0
  85. package/rules/loop-prevention.md +15 -0
  86. package/rules/repo-local.md +6 -0
  87. package/scripts/check-environment.ps1 +22 -0
  88. package/scripts/context-pack.mjs +17 -0
  89. package/scripts/e2e-investigate-repro.mjs +66 -0
  90. package/scripts/model-maintenance-job.mjs +59 -0
  91. package/scripts/observe-models.mjs +97 -0
  92. package/scripts/orchestrate.mjs +137 -0
  93. package/scripts/reevaluate-models.mjs +95 -0
  94. package/scripts/report-model-maintenance.mjs +70 -0
  95. package/scripts/resolve-capabilities.mjs +39 -0
  96. package/scripts/run-pipeline.mjs +56 -0
  97. package/scripts/sync-agents-md.ps1 +10 -0
  98. package/scripts/validate.mjs +71 -0
  99. package/skills/README.md +14 -0
  100. package/skills/pi-workflow/SKILL.md +26 -0
  101. package/skills/pi-workflow/references/code-investigation-and-fix.md +16 -0
  102. package/skills/pi-workflow/references/research.md +14 -0
  103. package/skills/pi-workflow/references/review.md +11 -0
  104. package/skills/pi-workflow/references/visual-work.md +14 -0
  105. package/skills/project-management/SKILL.md +106 -0
  106. package/skills/project-management/references/operations.md +52 -0
  107. package/skills/visual-verification/SKILL.md +88 -0
  108. package/skills/visual-verification/scripts/analyze-speech.ps1 +346 -0
  109. package/skills/visual-verification/scripts/backends/whisperx_backend.py +234 -0
  110. package/skills/visual-verification/scripts/common.ps1 +387 -0
  111. package/skills/visual-verification/scripts/contact-sheet.ps1 +121 -0
  112. package/skills/visual-verification/scripts/desktop-discover.ps1 +45 -0
  113. package/skills/visual-verification/scripts/desktop-inspect.ps1 +67 -0
  114. package/skills/visual-verification/scripts/desktop-record.ps1 +97 -0
  115. package/skills/visual-verification/scripts/desktop-screenshot.ps1 +65 -0
  116. package/skills/visual-verification/scripts/evaluate-sync.ps1 +249 -0
  117. package/skills/visual-verification/scripts/extract-frames.ps1 +79 -0
  118. package/skills/visual-verification/scripts/inspect-media.ps1 +138 -0
  119. package/skills/visual-verification/scripts/record-av.ps1 +102 -0
  120. package/skills/visual-verification/scripts/record.ps1 +72 -0
  121. package/skills/visual-verification/scripts/screenshot.ps1 +44 -0
  122. package/skills/visual-verification/scripts/waveform.ps1 +450 -0
  123. package/skills/visual-verification/scripts/winapp-common.ps1 +465 -0
  124. package/tests/activity.test.mjs +252 -0
  125. package/tests/attempt-budget.test.mjs +102 -0
  126. package/tests/browser.test.mjs +121 -0
  127. package/tests/context-pack.test.mjs +98 -0
  128. package/tests/dirty-gate.test.mjs +211 -0
  129. package/tests/e2e-browser.mjs +66 -0
  130. package/tests/e2e-real-orchestrator-resume.mjs +101 -0
  131. package/tests/e2e-real-orchestrator.mjs +41 -0
  132. package/tests/e2e-real-pi.mjs +27 -0
  133. package/tests/e2e-real-tool-orchestrator.mjs +66 -0
  134. package/tests/fixtures/browser-page/index.html +20 -0
  135. package/tests/fixtures/maintenance/availability.txt +5 -0
  136. package/tests/fixtures/maintenance/catalog.json +74 -0
  137. package/tests/fixtures/maintenance/events.json +13 -0
  138. package/tests/fixtures/math-repo/README.md +3 -0
  139. package/tests/fixtures/math-repo/package.json +7 -0
  140. package/tests/fixtures/math-repo/src/math.js +11 -0
  141. package/tests/fixtures/math-repo/test/math.test.js +7 -0
  142. package/tests/fixtures/observe/announcements.json +8 -0
  143. package/tests/fixtures/orch-concurrent-child.mjs +44 -0
  144. package/tests/fixtures/orch-persist-child.mjs +61 -0
  145. package/tests/job.test.mjs +230 -0
  146. package/tests/kit.test.mjs +79 -0
  147. package/tests/language-policy.test.mjs +93 -0
  148. package/tests/loop-guard.test.mjs +60 -0
  149. package/tests/maintenance-exec.test.mjs +218 -0
  150. package/tests/maintenance-runner.test.mjs +222 -0
  151. package/tests/maintenance.test.mjs +195 -0
  152. package/tests/observe.test.mjs +283 -0
  153. package/tests/observer-registry.test.mjs +157 -0
  154. package/tests/orchestrator-cleanup.test.mjs +358 -0
  155. package/tests/orchestrator-command.test.mjs +14 -0
  156. package/tests/orchestrator-persist.test.mjs +375 -0
  157. package/tests/orchestrator-tools.test.mjs +215 -0
  158. package/tests/orchestrator.test.mjs +396 -0
  159. package/tests/package.test.mjs +37 -0
  160. package/tests/pipeline.test.mjs +239 -0
  161. package/tests/planner-classification.test.mjs +81 -0
  162. package/tests/planner-split.test.mjs +67 -0
  163. package/tests/qoder-observer.test.mjs +266 -0
  164. package/tests/reassign-progression.test.mjs +104 -0
  165. package/tests/retry-escalation.test.mjs +120 -0
  166. package/tests/routing.test.mjs +110 -0
  167. package/tests/sqlite-concurrency.test.mjs +178 -0
  168. package/tests/task-global-e2e.test.mjs +63 -0
  169. package/tests/task-global-failed.test.mjs +134 -0
  170. package/tests/telemetry.test.mjs +173 -0
  171. package/tests/test-sync-pi.ps1 +56 -0
  172. package/tests/turn-budget.test.mjs +106 -0
@@ -0,0 +1,22 @@
1
+ #requires -Version 7.0
2
+ # Read-only environment check: which runtimes/tools this machine actually has. Never contacts a model.
3
+ [CmdletBinding()] param()
4
+ $ErrorActionPreference = 'Continue'
5
+ function Probe([string]$Name, [scriptblock]$Cmd) {
6
+ $v = $null
7
+ try { $v = (& $Cmd 2>$null | Select-Object -First 1); if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { $v = $null } } catch { $v = $null }
8
+ [pscustomobject]@{ tool = $Name; present = [bool]$v; version = "$v".Trim() }
9
+ }
10
+ $rows = @(
11
+ Probe 'pwsh' { pwsh --version }
12
+ Probe 'node' { node --version }
13
+ Probe 'git' { git --version }
14
+ Probe 'pi' { pi --version }
15
+ Probe 'codex' { codex --version }
16
+ Probe 'ffmpeg' { ffmpeg -version }
17
+ Probe 'ffprobe' { ffprobe -version }
18
+ Probe 'winapp' { winapp --version }
19
+ )
20
+ $agentDir = if ($env:PI_CODING_AGENT_DIR) { $env:PI_CODING_AGENT_DIR } else { Join-Path $env:USERPROFILE '.pi/agent' }
21
+ $rows += [pscustomobject]@{ tool = 'pi agent dir'; present = (Test-Path $agentDir); version = $agentDir }
22
+ $rows | Format-Table -AutoSize
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ // Validate one Context Pack (.md or .json) and optionally print its JSON or canonical Markdown.
3
+ // Usage: node scripts/context-pack.mjs <file> [--json|--md]
4
+ import { readFileSync } from 'node:fs';
5
+ import { parseContextPackMarkdown, validateContextPack, toMarkdown } from '../lib/context-pack.mjs';
6
+
7
+ const [file, mode] = process.argv.slice(2);
8
+ if (!file) { console.error('usage: context-pack.mjs <file.md|file.json> [--json|--md]'); process.exit(2); }
9
+ const text = readFileSync(file, 'utf8');
10
+ let pack;
11
+ try { pack = file.toLowerCase().endsWith('.json') ? JSON.parse(text) : parseContextPackMarkdown(text); }
12
+ catch (e) { console.error(e.message); process.exit(1); }
13
+ const errors = validateContextPack(pack);
14
+ if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
15
+ if (mode === '--json') console.log(JSON.stringify(pack, null, 2));
16
+ else if (mode === '--md') console.log(toMarkdown(pack));
17
+ else console.log(`PASS ${file}: ${pack.relevant_files.length} files, ${(pack.relevant_snippets ?? []).length} snippets`);
@@ -0,0 +1,66 @@
1
+ // Read-only reproduction for run-muf1egtr-d13d8a: a single investigate task on a
2
+ // small fixture repo. Verifies (1) correct repo cwd, (2) cheap-code primary chain
3
+ // order, (3) local fallback after a primary failure, (4) expired health marks
4
+ // re-enter the candidate pool, (5) scout completion. No code changes.
5
+ // node scripts/e2e-investigate-repro.mjs
6
+ import { mkdtempSync, cpSync, writeFileSync } from 'node:fs';
7
+ import { spawnSync } from 'node:child_process';
8
+ import { tmpdir } from 'node:os';
9
+ import { join, resolve, dirname } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import assert from 'node:assert/strict';
12
+ import { loadRouting } from '../lib/routing.mjs';
13
+ import { loadRegistry } from '../lib/registry.mjs';
14
+ import { loadAgents } from '../lib/agents.mjs';
15
+ import { loadPolicy, mergePolicy } from '../lib/orchestrator/policy.mjs';
16
+ import { openStore } from '../lib/orchestrator/store.mjs';
17
+ import { createHealthMonitor } from '../lib/orchestrator/health.mjs';
18
+ import { createAgentRunner } from '../lib/orchestrator/runner.mjs';
19
+ import { orchestrate, formatReport } from '../lib/orchestrator/orchestrator.mjs';
20
+ import { createPiInvoker } from '../adapters/pi/lib/invoke.mjs';
21
+ import { createPiSubagentRunner } from '../adapters/pi/lib/subagent.mjs';
22
+
23
+ const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
24
+ const root = mkdtempSync(join(tmpdir(), 'ludi-repro-'));
25
+ const repo = join(root, 'repo');
26
+ const storePath = join(root, 'state.db');
27
+ cpSync(join(kit, 'tests/fixtures/math-repo'), repo, { recursive: true });
28
+ for (const args of [['init'], ['add', '-A'], ['-c', 'user.email=ludi@example.com', '-c', 'user.name=ludi', 'commit', '-m', 'fixture']]) {
29
+ const git = spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
30
+ if (git.status !== 0) { console.error(git.stderr); process.exit(git.status ?? 1); }
31
+ }
32
+
33
+ const plan = [
34
+ { id: 't1', title: 'Investigate entrypoint and test command', goal: 'Read the repository and report: the main entrypoint and the exact test command. Do not edit files.', agent: 'scout', kind: 'investigate', capability: 'cheap-code', executionMode: 'subagent', dependencies: [], acceptance: ['the main entrypoint file is named', 'the exact test command is named'] },
35
+ ];
36
+
37
+ const routing = loadRouting(join(kit, 'routing/routing.json'));
38
+ const { registry } = loadRegistry(join(kit, 'adapters/pi/models.json'), join(kit, 'adapters/pi/models.local.json'), routing);
39
+ const { agents } = loadAgents(join(kit, 'agents'), routing);
40
+ const { policy } = loadPolicy(join(kit, 'orchestration/decision-policy.json'));
41
+ const active = mergePolicy(policy, { limits: { max_retries: 1 }, agent_runtime: { max_runtime_ms: 300000, max_tool_calls: 25, max_turns: 8 } });
42
+ const session = openStore(storePath);
43
+ const health = createHealthMonitor({ session, policy: active });
44
+
45
+ // (4) Seed an EXPIRED usage_exhausted mark on the cheap primary: it must not block candidacy.
46
+ session.recordHealth({ provider: 'openai-codex', model: 'gpt-5.6-luna', state: 'usage_exhausted', reason: 'stale test mark', runId: '', ttlMs: -1000, now: new Date(Date.now() - 60_000).toISOString() });
47
+ const staleSkip = session.activeHealth({ provider: 'openai-codex', model: 'gpt-5.6-luna', runId: 'probe', now: new Date().toISOString() });
48
+ console.log('expired health mark active?', staleSkip ? `BUG still active until ${staleSkip.expiresAt}` : 'no (correctly ignored)');
49
+
50
+ const runner = createAgentRunner({
51
+ invoke: createPiInvoker(), runSubagent: createPiSubagentRunner(), agents, routing, registry, repoRoot: repo, policy: active, health,
52
+ maxModelAttempts: active.limits.model_attempts_per_task,
53
+ });
54
+ try {
55
+ const result = await orchestrate({ request: 'Investigate only: identify the main entrypoint and the test command for this repo', plan, agents, routing, registry, policy: active, runner, repoRoot: repo, session, health });
56
+ writeFileSync(join(root, 'trace.json'), JSON.stringify(result, null, 2));
57
+ console.log(formatReport(result));
58
+ const steps = result.trace.filter(e => e.type === 'result').flatMap(e => e.steps ?? []);
59
+ console.log('MODEL STEPS:', JSON.stringify(steps.map(s => ({ backend: s.backend, modelId: s.modelId, skipped: !!s.skipped, ok: s.ok, reason: s.reason })), null, 2));
60
+ const t1 = result.tasks.find(t => t.id === 't1');
61
+ console.log('t1:', t1.status, '| summary:', String(t1.result?.summary ?? '').slice(0, 300));
62
+ if (result.status === 'completed') console.log('PASS: read-only investigate E2E');
63
+ else { console.log('FAIL: investigate did not complete'); process.exitCode = 1; }
64
+ } finally {
65
+ session.close();
66
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // Phase 5 — scheduler-safe maintenance job entrypoint.
3
+ // observe -> dedupe -> catalog diff -> meaningful-change gate -> proposal ->
4
+ // maintenance preview -> optional live tiers -> notification decision -> sinks.
5
+ // Quiet by default; writes only under adapters/<x>/out/. Never touches
6
+ // model-catalog.json, routing.json, models*.json, settings.json, ~/.pi, credentials.
7
+ //
8
+ // Usage:
9
+ // node scripts/model-maintenance-job.mjs # diff store vs catalog, quiet exit if nothing meaningful
10
+ // node scripts/model-maintenance-job.mjs --source fixture --input announcements.json
11
+ // node scripts/model-maintenance-job.mjs --check-pi # + pi --list-models availability
12
+ // node scripts/model-maintenance-job.mjs --check-qoder # + qoder-models-cache.json priceFactor (free campaigns)
13
+ // node scripts/model-maintenance-job.mjs --check pi-cli,qoder-cache # generic observer list
14
+ // node scripts/model-maintenance-job.mjs --live # + real tier invocations (spends quota)
15
+ // node scripts/model-maintenance-job.mjs --shadow # observe only; no external notify unless --shadow-notify
16
+ // node scripts/model-maintenance-job.mjs --notify-command "node send.js" # explicit external sink
17
+ import { resolve, join, dirname } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { loadRouting } from '../lib/routing.mjs';
20
+ import { loadRegistry } from '../lib/registry.mjs';
21
+ import { loadAgents } from '../lib/agents.mjs';
22
+ import { loadCatalog } from '../lib/maintenance.mjs';
23
+ import { loadExecPolicy } from '../lib/maintenance-exec.mjs';
24
+ import { runMaintenanceJob } from '../lib/job.mjs';
25
+
26
+ const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
27
+ const args = process.argv.slice(2);
28
+ const opt = (name, def) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : def; };
29
+ const has = name => args.includes(`--${name}`);
30
+ const adapter = opt('adapter', 'pi');
31
+ const adapterDir = join(kit, 'adapters', adapter);
32
+ const outDir = join(adapterDir, 'out');
33
+
34
+ const routing = loadRouting(join(kit, 'routing/routing.json'));
35
+ const { registry } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing);
36
+ const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
37
+ if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
38
+ const catalog = loadCatalog(join(adapterDir, 'model-catalog.json'));
39
+ const policy = loadExecPolicy(opt('policy', join(adapterDir, 'maintenance-policy.json')));
40
+
41
+ let invoke = null;
42
+ if (has('live')) {
43
+ const { createPiInvoker } = await import(`../adapters/${adapter}/lib/invoke.mjs`);
44
+ invoke = createPiInvoker();
45
+ }
46
+
47
+ const run = await runMaintenanceJob({
48
+ outDir, adapterDir, kit, catalog, routing, registry, agents, policy,
49
+ source: opt('source', null), input: opt('input', null),
50
+ checkPi: has('check-pi'), checkQoder: has('check-qoder'), check: opt('check', null), qoderCachePath: opt('qoder-cache', null), live: has('live'), invoke,
51
+ notifyCommand: opt('notify-command', null),
52
+ shadow: has('shadow'), shadowNotify: has('shadow-notify'),
53
+ });
54
+
55
+ if (run.status === 'skipped-locked') { console.log('maintenance job: skipped (another run holds the lock)'); process.exit(0); }
56
+ if (run.status === 'error') { console.error(`maintenance job: error ${run.error}`); process.exit(1); }
57
+ console.log(`maintenance job ${run.runId}: ${run.quiet ? `quiet (${run.verdict.quietReason})` : `${run.verdict.severity} — ${run.notification?.summary ?? ''}`}`);
58
+ if (run.notification) console.log(` notification: sent=${run.notification.sent} (${run.notification.dedupeReason})${run.budgetLimited ? ' [budgetLimited]' : ''}`);
59
+ process.exit(run.quiet ? 0 : 2); // 2 = meaningful change pending human review (scheduler-friendly)
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ // Phase 4 — observation pipeline: external sources -> normalized observations ->
3
+ // JSONL store -> catalog diff -> catalog proposal -> optional maintenance preview.
4
+ // Dry-run only: writes exclusively to adapters/<x>/out/. Never touches
5
+ // model-catalog.json, routing.json, models*.json, settings.json, ~/.pi, credentials.
6
+ //
7
+ // Usage:
8
+ // node scripts/observe-models.mjs --source manual --input observations.json
9
+ // node scripts/observe-models.mjs --source fixture --input announcements.json
10
+ // node scripts/observe-models.mjs --check-pi # pi --list-models availability
11
+ // node scripts/observe-models.mjs --check-qoder # qoder-models-cache.json priceFactor (free campaigns)
12
+ // node scripts/observe-models.mjs --check pi-cli,qoder-cache # generic observer list (same ids)
13
+ // node scripts/observe-models.mjs # diff store vs catalog (no new input)
14
+ // node scripts/observe-models.mjs --preview-maintenance # + hypothetical catalog -> maintenance dry-run
15
+ import { writeFileSync, mkdirSync } from 'node:fs';
16
+ import { resolve, join, dirname } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { loadRouting } from '../lib/routing.mjs';
19
+ import { loadRegistry } from '../lib/registry.mjs';
20
+ import { loadAgents } from '../lib/agents.mjs';
21
+ import { loadCatalog } from '../lib/maintenance.mjs';
22
+ import { ingestObservations, loadObservationStore, productionObservations } from '../lib/observe/observation.mjs';
23
+ import { SOURCES } from '../lib/observe/sources.mjs';
24
+ import { OBSERVERS, resolveRequestedObservers, runObservers } from '../lib/observe/observers.mjs';
25
+ import { diffCatalog, buildCatalogProposal, applyProposalToCatalog } from '../lib/observe/differ.mjs';
26
+ import { runMaintenancePlan, loadExecPolicy } from '../lib/maintenance-exec.mjs';
27
+
28
+ const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
29
+ const args = process.argv.slice(2);
30
+ const opt = (name, def) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : def; };
31
+ const has = name => args.includes(`--${name}`);
32
+ const adapter = opt('adapter', 'pi');
33
+ const adapterDir = join(kit, 'adapters', adapter);
34
+ const outDir = join(adapterDir, 'out');
35
+ mkdirSync(outDir, { recursive: true });
36
+
37
+ const catalog = loadCatalog(join(adapterDir, 'model-catalog.json'));
38
+ const storePath = join(outDir, 'model-observations.jsonl');
39
+
40
+ // 1. collect observations — an explicit --input file, plus any requested live
41
+ // observers. Selection/execution is registry-driven (lib/observe/observers.mjs):
42
+ // --check-pi/--check-qoder/--check <list> resolve to observer ids and run once
43
+ // each, in order; one observer's failure never blocks the others.
44
+ let fresh = [];
45
+ if (opt('input', null)) {
46
+ const source = opt('source', 'manual');
47
+ const fn = SOURCES[source];
48
+ if (!fn) { console.error(`unknown --source "${source}"; expected ${Object.keys(SOURCES).join('|')}`); process.exit(1); }
49
+ fresh = fn(resolve(opt('input')));
50
+ }
51
+ const requested = resolveRequestedObservers({ checkPi: has('check-pi'), checkQoder: has('check-qoder'), check: opt('check', null) });
52
+ if (requested.unknown.length) { console.error(`unknown observer id(s): ${requested.unknown.join(', ')}; known: ${Object.keys(OBSERVERS).join(', ')}`); process.exit(1); }
53
+ if (requested.ids.length) {
54
+ const r = await runObservers(requested.ids, { catalog, outDir, observedAt: new Date().toISOString(), qoderCachePath: opt('qoder-cache', null) });
55
+ fresh.push(...r.observations);
56
+ for (const res of r.results) {
57
+ if (res.probeFailed) console.error(`${res.id}: ${res.metadata?.reason ?? 'probe failed'}; no observations ingested (unknown, not absent)`);
58
+ else if (res.metadata?.noTransition) console.log(`${res.id}: no transition; no observations ingested`);
59
+ }
60
+ }
61
+
62
+ // 2. ingest into the JSONL store (dedupe/stale handled inside)
63
+ const ingest = fresh.length ? ingestObservations(storePath, fresh) : { stored: [], duplicates: [], stale: [], invalid: [] };
64
+ const store = loadObservationStore(storePath);
65
+
66
+ // 3. diff production observations against the catalog (test/fixture records are
67
+ // stored for audit but excluded from real proposals)
68
+ const prodObs = productionObservations(store.observations);
69
+ const diff = diffCatalog(catalog, prodObs);
70
+ const diffFile = join(outDir, 'catalog-diff.json');
71
+ writeFileSync(diffFile, JSON.stringify(diff, null, 2) + '\n');
72
+
73
+ // 4. proposal
74
+ const proposal = buildCatalogProposal(catalog, diff, prodObs);
75
+ const proposalFile = join(outDir, 'model-catalog.proposal.json');
76
+ writeFileSync(proposalFile, JSON.stringify(proposal, null, 2) + '\n');
77
+
78
+ // 5. optional: hypothetical catalog -> maintenance dry-run preview
79
+ let previewFile = null;
80
+ if (has('preview-maintenance')) {
81
+ const hypothetical = applyProposalToCatalog(catalog, proposal);
82
+ const routing = loadRouting(join(kit, 'routing/routing.json'));
83
+ const { registry } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing);
84
+ const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
85
+ if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
86
+ const policy = loadExecPolicy(join(adapterDir, 'maintenance-policy.json'));
87
+ const preview = runMaintenancePlan({ routing, registry, agents, catalog: hypothetical, events: [], policy });
88
+ preview.hypothetical = true;
89
+ preview.note = 'catalog proposal applied in memory only; model-catalog.json and routing.json unchanged';
90
+ previewFile = join(outDir, 'maintenance-preview.json');
91
+ writeFileSync(previewFile, JSON.stringify(preview, null, 2) + '\n');
92
+ }
93
+
94
+ console.log(`observe: ${fresh.length} fresh, stored ${ingest.stored.length}, dup ${ingest.duplicates.length}, stale ${ingest.stale.length}, invalid ${ingest.invalid.length}`);
95
+ console.log(` diff: ${diff.diffs.length} rows (${diff.diffs.filter(d => d.status === 'proposed').length} proposed, ${diff.diffs.filter(d => d.status === 'conflict').length} conflict) -> ${diffFile}`);
96
+ console.log(` proposal: +${proposal.additions.length} add, ${proposal.updates.length} update, ${proposal.deprecations.length} deprecate, ${proposal.conflicts.length} conflict -> ${proposalFile}`);
97
+ if (previewFile) console.log(` preview: ${previewFile} (hypothetical catalog; nothing applied)`);
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ // Orchestrate one high-level request, or resume a persisted run.
3
+ // Usage:
4
+ // node scripts/orchestrate.mjs [--dry-run] [--planner rules|model] [--repo <dir>] [--apply]
5
+ // [--policy <file>] [--store <db>] [--out <dir>] [--json] [--trace] "<request>"
6
+ // node scripts/orchestrate.mjs --list [--status <run-status>] [--store <db>]
7
+ // node scripts/orchestrate.mjs --decisions [--resume <run-id>] [--store <db>]
8
+ // node scripts/orchestrate.mjs --show <run-id> [--store <db>]
9
+ // node scripts/orchestrate.mjs --resume <run-id> [--answer <decision-id> "<text>"] [--store <db>] [--repo <dir>] [--apply]
10
+ // node scripts/orchestrate.mjs --prune [--older-than 7d] [--store <db>] # delete terminal run history
11
+ // node scripts/orchestrate.mjs --delete <run-id> [--force] [--store <db>] # preview, or delete with --force
12
+ // node scripts/orchestrate.mjs --clear [--force] [--include-active] [--store <db>] # preview, or delete with --force
13
+ // --dry-run plans and routes only. A real run is stored under .orchestration/state.db (or --store / LUDI_ORCHESTRATION_STORE).
14
+ // Cleanup touches terminal runs only (completed/failed/cancelled with no pending decisions); --include-active
15
+ // additionally removes running/waiting_for_user runs that have no pending decisions. Global decision memory,
16
+ // protocol stats and global backend health are never deleted.
17
+ import { resolve, join, dirname } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { mkdirSync, writeFileSync } from 'node:fs';
20
+ import { loadRouting } from '../lib/routing.mjs';
21
+ import { loadRegistry } from '../lib/registry.mjs';
22
+ import { loadAgents } from '../lib/agents.mjs';
23
+ import { loadPolicy } from '../lib/orchestrator/policy.mjs';
24
+ import { dryRun, formatPlan } from '../lib/orchestrator/orchestrator.mjs';
25
+ import {
26
+ defaultStorePath, loadOrchestrationContext, listOrchestrationRuns, pendingDecisions, showRun,
27
+ createRunHealth, createRunRunner, startOrchestration, resumeOrchestration, formatReport, formatRunList,
28
+ parseOlderThan, previewRunCleanup, pruneOrchestrationRuns, deleteOrchestrationRun, clearOrchestrationRuns,
29
+ formatCleanup,
30
+ } from '../lib/orchestrator/api.mjs';
31
+ import { createPiInvoker } from '../adapters/pi/lib/invoke.mjs';
32
+ import { createPiSubagentRunner } from '../adapters/pi/lib/subagent.mjs';
33
+
34
+ const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
35
+ const args = process.argv.slice(2);
36
+ const VALUED = new Set(['planner', 'repo', 'policy', 'out', 'request', 'store', 'resume', 'status', 'show', 'delete', 'older-than']);
37
+ const opts = {}, positional = [];
38
+ opts.answers = [];
39
+ for (let i = 0; i < args.length; i++) {
40
+ const a = args[i];
41
+ if (!a.startsWith('--')) { positional.push(a); continue; }
42
+ const name = a.slice(2);
43
+ if (name === 'answer') { opts.answers.push({ decisionId: args[++i], answer: args[++i] }); continue; }
44
+ opts[name] = VALUED.has(name) ? args[++i] : true;
45
+ }
46
+ const cleanupMode = !!(opts.prune || opts.delete || opts.clear);
47
+ const request = opts.request ?? (opts.resume || opts.list || opts.decisions || opts.show || cleanupMode ? '' : positional.join(' '));
48
+ const storePath = resolve(opts.store ?? defaultStorePath(kit));
49
+ const usage = 'usage: orchestrate.mjs [--dry-run] [--planner rules|model] [--repo dir] [--apply] [--policy file] [--store db] [--out dir] [--json] [--trace] "<request>" | --list [--status] | --decisions [--resume id] | --show id | --resume id [--answer decision-id text] | --prune [--older-than 7d] | --delete id [--force] | --clear [--force] [--include-active]';
50
+ if (!request && !opts.resume && !opts.list && !opts.decisions && !opts.show && !cleanupMode) { console.error(usage); process.exit(2); }
51
+ // Cleanup flag validation.
52
+ const cleanupFlags = [opts.prune && '--prune', opts.delete && '--delete', opts.clear && '--clear'].filter(Boolean);
53
+ if (cleanupFlags.length > 1) { console.error(`cleanup modes are exclusive: ${cleanupFlags.join(' ')}`); process.exit(2); }
54
+ if (opts['older-than'] && !opts.prune) { console.error('--older-than requires --prune'); process.exit(2); }
55
+ if (opts['include-active'] && !opts.clear) { console.error('--include-active requires --clear'); process.exit(2); }
56
+ if (opts.force && !opts.delete && !opts.clear) { console.error('--force requires --delete or --clear'); process.exit(2); }
57
+ let olderThanMs = null;
58
+ if (opts['older-than']) {
59
+ try { olderThanMs = parseOlderThan(opts['older-than']); }
60
+ catch (e) { console.error(e.message); process.exit(2); }
61
+ }
62
+ const planner = opts.planner ?? 'rules';
63
+ if (!['rules', 'model'].includes(planner)) { console.error(`unknown planner "${planner}"`); process.exit(2); }
64
+
65
+ const routing = loadRouting(join(kit, 'routing/routing.json'));
66
+ const { registry } = loadRegistry(join(kit, 'adapters/pi/models.json'), join(kit, 'adapters/pi/models.local.json'), routing);
67
+ const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
68
+ if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
69
+ const { policy } = opts.policy
70
+ ? loadPolicy(join(kit, 'orchestration/decision-policy.json'), resolve(opts.policy))
71
+ : loadPolicy(join(kit, 'orchestration/decision-policy.json'), join(kit, 'orchestration/decision-policy.local.json'));
72
+ const repoRoot = opts.repo ? resolve(opts.repo) : null;
73
+ const outDir = resolve(opts.out ?? join(kit, 'adapters/pi/out/orchestrate'));
74
+
75
+ if (opts['dry-run'] && !cleanupMode) {
76
+ const invoke = planner === 'model' ? createPiInvoker() : null;
77
+ const dry = await dryRun(request, { planner, agents, routing, registry, policy, invoke, cwd: repoRoot ?? process.cwd() });
78
+ console.log(opts.json ? JSON.stringify(dry, null, 2) : formatPlan(dry));
79
+ process.exitCode = dry.errors.length ? 1 : 0;
80
+ } else {
81
+ const ctx = loadOrchestrationContext({
82
+ kit, storePath,
83
+ policyPath: opts.policy ? resolve(opts.policy) : null,
84
+ localPolicyPath: opts.policy ? null : join(kit, 'orchestration/decision-policy.local.json'),
85
+ });
86
+ if (ctx.errors.length) { console.error(ctx.errors.join('\n')); ctx.session.close(); process.exit(1); }
87
+ try {
88
+ if (opts.list) {
89
+ const rows = listOrchestrationRuns(ctx, { status: opts.status ?? null });
90
+ console.log(opts.json ? JSON.stringify(rows, null, 2) : formatRunList(rows));
91
+ } else if (opts.decisions) {
92
+ const rows = pendingDecisions(ctx, { runId: opts.resume ?? null });
93
+ console.log(opts.json ? JSON.stringify(rows, null, 2) : (rows.length ? rows.map(d => `${d.runId} ${d.id} [${d.taskId}] ${d.question}`).join('\n') : 'decisions: none'));
94
+ } else if (opts.show) {
95
+ const shown = showRun(ctx, opts.show);
96
+ console.log(opts.json ? JSON.stringify(shown, null, 2) : formatReport(shown));
97
+ } else if (opts.prune) {
98
+ try {
99
+ const result = opts['dry-run']
100
+ ? previewRunCleanup(ctx, { olderThan: new Date(Date.now() - (olderThanMs ?? 7 * 86_400_000)).toISOString() })
101
+ : pruneOrchestrationRuns(ctx, { olderThanMs });
102
+ console.log(opts.json ? JSON.stringify(result, null, 2) : formatCleanup(result, { verb: '削除', hint: '--prune' }));
103
+ } catch (e) { console.error(`整理できません: ${e.message}`); process.exitCode = 1; }
104
+ } else if (opts.delete) {
105
+ const id = opts.delete === true ? positional[0] : opts.delete;
106
+ if (!id || String(id).startsWith('--')) { console.error('--delete requires a run id'); process.exit(2); }
107
+ try {
108
+ const result = deleteOrchestrationRun(ctx, id, { force: !!opts.force });
109
+ console.log(opts.json ? JSON.stringify(result, null, 2) : `削除しました: ${result.id} (tasks:${result.deleted.tasks} decisions:${result.deleted.decisions} trace:${result.deleted.trace} health:${result.deleted.health})`);
110
+ } catch (e) { console.error(`削除できません: ${e.message}`); process.exitCode = 1; }
111
+ } else if (opts.clear) {
112
+ try {
113
+ const result = clearOrchestrationRuns(ctx, { force: !!opts.force, includeActive: !!opts['include-active'] });
114
+ console.log(opts.json ? JSON.stringify(result, null, 2) : formatCleanup(result, { verb: '削除' }));
115
+ } catch (e) { console.error(`整理できません: ${e.message}`); process.exitCode = 1; }
116
+ } else {
117
+ const health = createRunHealth(ctx);
118
+ const invoke = createPiInvoker();
119
+ const runner = createRunRunner(ctx, { invoke, runSubagent: createPiSubagentRunner(), repoRoot, outDir, apply: !!opts.apply, health });
120
+ const result = opts.resume
121
+ ? await resumeOrchestration(ctx, { runId: opts.resume, answers: opts.answers, repoRoot, runner, invoke, health })
122
+ : await startOrchestration(ctx, { request, repoRoot, planner, runner, invoke, health });
123
+ mkdirSync(outDir, { recursive: true });
124
+ const traceFile = join(outDir, 'orchestration-trace.json');
125
+ writeFileSync(traceFile, JSON.stringify(result, null, 2));
126
+ if (opts.json) console.log(JSON.stringify({ ...result, trace: opts.trace ? result.trace : undefined }, null, 2));
127
+ else {
128
+ console.log(formatReport(result));
129
+ console.log(`\nrun: ${result.runId}`);
130
+ if (opts.trace) console.log(`trace: ${traceFile}`);
131
+ }
132
+ process.exitCode = result.status === 'completed' ? 0 : 1;
133
+ }
134
+ } finally {
135
+ ctx.session.close();
136
+ }
137
+ }
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // Periodic maintenance: re-evaluate capability -> backend -> model bindings when provider
3
+ // conditions change (free campaign end, quota/rate-limit change, deprecation, price change,
4
+ // new model added). Phase 2 adds execution tiers: monitor -> evaluate -> reconfigure,
5
+ // each selecting a model under the cheapest-sufficient policy (free first, then cheapest
6
+ // eligible cloud, then local fallback). This run is a dry-run decision layer: it selects
7
+ // which model each tier WOULD use and records the rationale — no model is invoked.
8
+ //
9
+ // Reads routing.json, agents/, adapters/<x>/models.json + models.local.json,
10
+ // model-catalog.json, maintenance-policy.json and an events file. Writes ONLY to
11
+ // adapters/<x>/out/: model-maintenance.proposal.json (Phase 1 proposal) and
12
+ // model-maintenance.run.json (per-tier selection + escalation audit). Never touches
13
+ // ~/.pi, settings.json, models.json, models.local.json or routing.json.
14
+ //
15
+ // Usage:
16
+ // node scripts/reevaluate-models.mjs [--adapter pi] [--events <events.json>]
17
+ // [--availability-file <list.txt>] [--check-availability] [--margin <n>]
18
+ // [--policy <maintenance-policy.json>] [--live] [--stdout]
19
+ //
20
+ // --live: actually invoke the selected tier models through the pi CLI (spends quota).
21
+ // Without it the run is a pure dry-run decision layer. Even --live writes only
22
+ // out/ reports — never ~/.pi, settings.json, models*.json or routing.json.
23
+ import { writeFileSync, mkdirSync } from 'node:fs';
24
+ import { resolve, join, dirname } from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+ import { loadRouting } from '../lib/routing.mjs';
27
+ import { loadRegistry } from '../lib/registry.mjs';
28
+ import { loadAgents } from '../lib/agents.mjs';
29
+ import { loadCatalog, loadEvents, loadAvailabilityFile } from '../lib/maintenance.mjs';
30
+ import { runMaintenancePlan, loadExecPolicy } from '../lib/maintenance-exec.mjs';
31
+
32
+ const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
33
+ const args = process.argv.slice(2);
34
+ const opt = (name, def) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : def; };
35
+ const has = name => args.includes(`--${name}`);
36
+ const adapter = opt('adapter', 'pi');
37
+ const adapterDir = join(kit, 'adapters', adapter);
38
+
39
+ const routing = loadRouting(join(kit, 'routing/routing.json'));
40
+ const { registry, sources } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing);
41
+ const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
42
+ if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
43
+ const catalog = loadCatalog(join(adapterDir, 'model-catalog.json'));
44
+ const events = opt('events', null) ? loadEvents(resolve(opt('events'))) : [];
45
+ const policy = loadExecPolicy(opt('policy', join(adapterDir, 'maintenance-policy.json')));
46
+
47
+ let availability = null, availabilitySource = 'not-checked';
48
+ if (has('check-availability')) {
49
+ availabilitySource = 'pi --list-models (failed)';
50
+ try {
51
+ const { fetchPiAvailability } = await import(`../adapters/${adapter}/lib/list-models.mjs`);
52
+ availability = fetchPiAvailability();
53
+ if (availability) availabilitySource = availability.source;
54
+ } catch { availability = null; }
55
+ } else if (opt('availability-file', null)) {
56
+ availability = loadAvailabilityFile(resolve(opt('availability-file')));
57
+ availabilitySource = availability.source;
58
+ }
59
+
60
+ const margin = Number(opt('margin', NaN)) || undefined;
61
+ let run;
62
+ if (has('live')) {
63
+ const { createPiInvoker } = await import(`../adapters/${adapter}/lib/invoke.mjs`);
64
+ const { runMaintenanceLive } = await import('../lib/maintenance-runner.mjs');
65
+ const invoke = createPiInvoker();
66
+ run = await runMaintenanceLive({ routing, registry, agents, catalog, events, availability, policy, margin, invoke });
67
+ } else {
68
+ run = runMaintenancePlan({ routing, registry, agents, catalog, events, availability, policy, margin });
69
+ }
70
+ run.adapter = adapter;
71
+ run.inputs = { registrySources: sources, catalog: join(adapterDir, 'model-catalog.json'), policy: opt('policy', join(adapterDir, 'maintenance-policy.json')), events: opt('events', null), availabilitySource };
72
+ run.apply = {
73
+ auto: false,
74
+ how: 'edit adapters/<x>/models.local.json per change.proposedModel, then re-run scripts/resolve-capabilities.mjs and merge out/settings.proposal.json into live settings.json',
75
+ never: ['~/.pi/agent/settings.json', 'adapters/*/models.json', 'adapters/*/models.local.json', 'routing/routing.json', 'provider credentials'],
76
+ };
77
+
78
+ const outDir = join(adapterDir, 'out');
79
+ mkdirSync(outDir, { recursive: true });
80
+ const runFile = join(outDir, 'model-maintenance.run.json');
81
+ writeFileSync(runFile, JSON.stringify(run, null, 2) + '\n');
82
+ let proposalFile = null;
83
+ if (run.proposal) {
84
+ proposalFile = join(outDir, 'model-maintenance.proposal.json');
85
+ writeFileSync(proposalFile, JSON.stringify(run.proposal, null, 2) + '\n');
86
+ }
87
+
88
+ const tierSummary = run.tiers.map(t => `${t.role}:${t.selected?.model ?? 'none'}($${t.selected?.effectiveCostUsd ?? '?'})`).join(' -> ');
89
+ console.log(`model-maintenance: ${run.outcome}; tiers ${tierSummary}; est decision cost $${run.estimatedDecisionCostUsd}`);
90
+ console.log(` run report -> ${runFile}${proposalFile ? `; proposal -> ${proposalFile}` : ''}`);
91
+ if (run.escalation) console.log(` escalation: ${run.escalation.sourceTier} -> ${run.escalation.targetTier}: ${run.escalation.escalationReason}`);
92
+ if (run.invocations?.length) {
93
+ for (const inv of run.invocations) console.log(` invoked ${inv.tier}: ${inv.selectedModel ?? 'none'}${inv.fallbackOccurred ? ' (fallback)' : ''}${inv.degradedToDeterministic ? ' [deterministic-only]' : ''}`);
94
+ }
95
+ if (has('stdout')) console.log(JSON.stringify(run, null, 2));
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ // Phase 6 — calibration report: aggregate telemetry -> human-readable report +
3
+ // deterministic warnings + counterfactuals + policy calibration proposal.
4
+ // Read-only w.r.t. policy/catalog/routing; writes only out/ artifacts.
5
+ //
6
+ // Usage:
7
+ // node scripts/report-model-maintenance.mjs [--adapter pi] [--days 14] [--json]
8
+ // node scripts/report-model-maintenance.mjs --compact --days 30 # retention compaction
9
+ import { writeFileSync, mkdirSync } from 'node:fs';
10
+ import { resolve, join, dirname } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { loadTelemetry, calibrationWarnings, counterfactuals, calibrationProposal, compactTelemetry } from '../lib/telemetry.mjs';
13
+ import { loadExecPolicy } from '../lib/maintenance-exec.mjs';
14
+
15
+ const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
16
+ const args = process.argv.slice(2);
17
+ const opt = (name, def) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : def; };
18
+ const has = name => args.includes(`--${name}`);
19
+ const adapter = opt('adapter', 'pi');
20
+ const adapterDir = join(kit, 'adapters', adapter);
21
+ const outDir = join(adapterDir, 'out');
22
+ const days = Number(opt('days', 14));
23
+ const policy = loadExecPolicy(opt('policy', join(adapterDir, 'maintenance-policy.json')));
24
+
25
+ if (has('compact')) {
26
+ const r = compactTelemetry(outDir, { days });
27
+ console.log(`retention: compacted ${r.compacted} old run(s), kept ${r.kept}`);
28
+ }
29
+
30
+ const t = loadTelemetry(outDir);
31
+ const warnings = calibrationWarnings(t, policy);
32
+ const cf = counterfactuals(t, 10);
33
+ const cal = calibrationProposal(t, policy);
34
+ mkdirSync(outDir, { recursive: true });
35
+ const calFile = join(outDir, 'maintenance-policy.calibration.proposal.json');
36
+ writeFileSync(calFile, JSON.stringify(cal, null, 2) + '\n');
37
+
38
+ const pct = (a, b) => (b ? `${((a / b) * 100).toFixed(0)}%` : 'n/a');
39
+ const avg = t.runs ? t.cost.totalUsd / t.runs : 0;
40
+ const avgMargin = t.qualityMargins.length ? t.qualityMargins.reduce((a, b) => a + b, 0) / t.qualityMargins.length : null;
41
+ const top = (obj, n = 5) => Object.entries(obj ?? {}).sort((a, b) => b[1] - a[1]).slice(0, n);
42
+
43
+ const report = {
44
+ activity: { totalRuns: t.runs, quietRuns: t.quietRuns, quietRatio: pct(t.quietRuns, t.runs), meaningful: t.meaningfulRuns, notifications: t.notifications, deduped: t.dedupedNotifications, dedupeRate: pct(t.dedupedNotifications, t.notifications) },
45
+ cost: { totalUsd: +t.cost.totalUsd.toFixed(4), apiUsd: +t.cost.apiUsd.toFixed(4), localElectricityUsd: +t.cost.localElectricityUsd.toFixed(4), avgPerRun: +avg.toFixed(4), premiumInvocations: t.premiumInvocations, budgetLimited: t.budgetLimited },
46
+ routing: { invocations: t.invocations, fallbacks: t.fallbacks, localFallbacks: t.localFallbacks, topSelected: top(t.selectedModels), topRejected: top(t.rejectedReasons) },
47
+ quality: { avgQualityMargin: avgMargin === null ? null : +avgMargin.toFixed(1), borderlineSelections: t.borderline.length, insufficientData: t.insufficientData, degradedToDeterministic: t.degradedToDeterministic },
48
+ noise: { duplicateObservations: t.duplicateObservations, staleObservations: t.staleObservations, probeFailures: t.probeFailures, conflicts: t.conflicts, unchangedProposals: t.unchangedProposals },
49
+ escalation: { count: t.escalations.length, rate: pct(t.escalations.length, t.runs), reasons: t.escalations.slice(-5).map(e => e.reasons) },
50
+ warnings,
51
+ counterfactuals: cf,
52
+ calibration: { status: cal.status, file: calFile, proposals: cal.proposals?.length ?? 0 },
53
+ };
54
+
55
+ if (has('json')) { console.log(JSON.stringify(report, null, 2)); process.exit(0); }
56
+
57
+ console.log(`=== Model maintenance calibration (${days}d window, ${t.runs} runs) ===`);
58
+ console.log(`Activity : ${t.runs} runs, ${report.activity.quietRatio} quiet, ${t.meaningfulRuns} meaningful, ${t.notifications} notifications (${report.activity.dedupeRate} deduped)`);
59
+ console.log(`Cost : $${report.cost.totalUsd} total (api $${report.cost.apiUsd}, local $${report.cost.localElectricityUsd}), avg $${report.cost.avgPerRun}/run, premium x${t.premiumInvocations}, budgetLimited x${t.budgetLimited}`);
60
+ console.log(`Routing : invocations mon=${t.invocations.monitor} eval=${t.invocations.evaluate} rec=${t.invocations.reconfigure}; fallbacks ${t.fallbacks} (local ${t.localFallbacks})`);
61
+ if (report.routing.topSelected.length) console.log(` top models: ${report.routing.topSelected.map(([m, c]) => `${m}x${c}`).join(', ')}`);
62
+ console.log(`Quality : avg margin ${report.quality.avgQualityMargin ?? 'n/a'}, borderline ${t.borderline.length}, insufficient-data ${t.insufficientData}, degraded ${t.degradedToDeterministic}`);
63
+ console.log(`Noise : dup ${t.duplicateObservations}, stale ${t.staleObservations}, probe-fail ${t.probeFailures}, conflicts ${t.conflicts}`);
64
+ console.log(`Escalation: ${t.escalations.length} (${report.escalation.rate} of runs)`);
65
+ console.log(`Warnings : ${warnings.length ? warnings.map(w => `${w.kind}(${w.evidence})`).join('; ') : 'none'}`);
66
+ console.log(`Calibration: ${cal.status}${cal.status === 'ok' ? ` -> ${cal.proposals.length} proposal(s) at ${calFile}` : ` (${cal.runs}/${cal.required.minRuns} runs, ${cal.meaningfulRuns}/${cal.required.minMeaningfulEvents} meaningful)`}`);
67
+ if (cf.length) {
68
+ console.log('Counterfactual (latest):');
69
+ for (const c of cf.slice(-3)) console.log(` ${c.tier}: ${c.actual.model} $${c.actual.estimatedCostUsd} q${c.actual.quality} | vs ${c.counterfactual.map(a => `${a.model} $${a.estimatedCostUsd} q${a.quality} [${a.verdict}]`).join(', ')}`);
70
+ }
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ // Resolve agents -> capabilities -> concrete pi models using models.json + models.local.json,
3
+ // and render the pi-subagents settings proposal to adapters/pi/out/. Read-only w.r.t. ~/.pi.
4
+ // Usage: node scripts/resolve-capabilities.mjs [--adapter pi] [--capability name] [--live-settings <path>]
5
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
6
+ import { resolve, join, dirname } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { homedir } from 'node:os';
9
+ import { loadRouting } from '../lib/routing.mjs';
10
+ import { loadRegistry } from '../lib/registry.mjs';
11
+ import { loadAgents } from '../lib/agents.mjs';
12
+ import { resolveAgents, resolveCapability } from '../lib/resolve.mjs';
13
+
14
+ const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
15
+ const args = process.argv.slice(2);
16
+ const opt = (name, def) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : def; };
17
+ const adapter = opt('adapter', 'pi');
18
+ const adapterDir = join(kit, 'adapters', adapter);
19
+ const routing = loadRouting(join(kit, 'routing/routing.json'));
20
+ const { registry, sources } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing);
21
+ const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
22
+ if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
23
+
24
+ const only = opt('capability', null);
25
+ const out = { registrySources: sources, agents: only ? undefined : resolveAgents(agents, routing, registry), capability: only ? resolveCapability(routing, registry, only) : undefined };
26
+
27
+ if (adapter === 'pi' && !only) {
28
+ const { buildSettingsProposal } = await import('../adapters/pi/lib/settings-proposal.mjs');
29
+ const agentDir = process.env.PI_CODING_AGENT_DIR || join(homedir(), '.pi', 'agent');
30
+ const livePath = opt('live-settings', join(agentDir, 'settings.json'));
31
+ const live = existsSync(livePath) ? JSON.parse(readFileSync(livePath, 'utf8')) : null;
32
+ const proposal = buildSettingsProposal(out.agents, { liveSettings: live });
33
+ const outDir = join(adapterDir, 'out');
34
+ mkdirSync(outDir, { recursive: true });
35
+ writeFileSync(join(outDir, 'settings.proposal.json'), JSON.stringify(proposal.proposal, null, 2) + '\n');
36
+ writeFileSync(join(outDir, 'capabilities.resolved.json'), JSON.stringify(out, null, 2) + '\n');
37
+ out.settingsProposal = { file: join(outDir, 'settings.proposal.json'), diffAgainst: live ? livePath : null, diff: proposal.diff, notes: proposal.notes, target: proposal.target };
38
+ }
39
+ console.log(JSON.stringify(out, null, 2));