@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.
- package/AGENTS.md +55 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/adapters/codex/README.md +24 -0
- package/adapters/codex/skill-metadata/visual-verification/agents/openai.yaml +7 -0
- package/adapters/pi/README.md +88 -0
- package/adapters/pi/browser/agent-browser.mjs +193 -0
- package/adapters/pi/lib/invoke.mjs +55 -0
- package/adapters/pi/lib/list-models.mjs +29 -0
- package/adapters/pi/lib/settings-proposal.mjs +34 -0
- package/adapters/pi/lib/subagent.mjs +175 -0
- package/adapters/pi/loop-guard/index.js +51 -0
- package/adapters/pi/maintenance-policy.json +36 -0
- package/adapters/pi/mcp.template.json +4 -0
- package/adapters/pi/model-catalog.json +97 -0
- package/adapters/pi/models.json +13 -0
- package/adapters/pi/models.local.example.json +14 -0
- package/adapters/pi/orchestrator-ext/command.mjs +14 -0
- package/adapters/pi/orchestrator-ext/index.js +150 -0
- package/adapters/pi/settings.template.json +7 -0
- package/adapters/pi/shell-gate/index.js +70 -0
- package/adapters/pi/sync-pi.ps1 +137 -0
- package/agents/README.md +26 -0
- package/agents/browser.md +64 -0
- package/agents/coder.md +31 -0
- package/agents/orchestrator.md +37 -0
- package/agents/reviewer.md +32 -0
- package/agents/scout.md +35 -0
- package/agents/tester.md +28 -0
- package/agents/visual.md +28 -0
- package/context-pack/SPEC.md +101 -0
- package/context-pack/context-pack.schema.json +79 -0
- package/context-pack/examples/example-fix.md +44 -0
- package/docs/architecture.md +55 -0
- package/docs/migration-from-codex-setting.md +44 -0
- package/docs/model-maintenance.md +401 -0
- package/docs/orchestrator.md +155 -0
- package/docs/phase2-report.md +39 -0
- package/docs/roadmap.md +27 -0
- package/docs/third-party.md +15 -0
- package/lib/agents.mjs +79 -0
- package/lib/context-pack.mjs +215 -0
- package/lib/job.mjs +312 -0
- package/lib/language-policy.mjs +27 -0
- package/lib/maintenance-exec.mjs +377 -0
- package/lib/maintenance-runner.mjs +266 -0
- package/lib/maintenance.mjs +422 -0
- package/lib/normalize.mjs +101 -0
- package/lib/observe/differ.mjs +185 -0
- package/lib/observe/observation.mjs +147 -0
- package/lib/observe/observers.mjs +134 -0
- package/lib/observe/sources.mjs +154 -0
- package/lib/orchestrator/activity.mjs +249 -0
- package/lib/orchestrator/api.mjs +151 -0
- package/lib/orchestrator/contract.mjs +68 -0
- package/lib/orchestrator/escalation.mjs +84 -0
- package/lib/orchestrator/evaluator.mjs +92 -0
- package/lib/orchestrator/failures.mjs +88 -0
- package/lib/orchestrator/health.mjs +53 -0
- package/lib/orchestrator/orchestrator.mjs +483 -0
- package/lib/orchestrator/permissions.mjs +64 -0
- package/lib/orchestrator/planner.mjs +194 -0
- package/lib/orchestrator/policy.mjs +134 -0
- package/lib/orchestrator/router.mjs +45 -0
- package/lib/orchestrator/runner.mjs +278 -0
- package/lib/orchestrator/shell-policy.mjs +52 -0
- package/lib/orchestrator/store.mjs +581 -0
- package/lib/orchestrator/task-store.mjs +79 -0
- package/lib/orchestrator/turn-budget.mjs +63 -0
- package/lib/orchestrator/worktree.mjs +72 -0
- package/lib/pipeline.mjs +279 -0
- package/lib/registry.mjs +63 -0
- package/lib/resolve.mjs +35 -0
- package/lib/routing.mjs +137 -0
- package/lib/telemetry.mjs +222 -0
- package/mcp/README.md +11 -0
- package/mcp/servers.json +13 -0
- package/orchestration/decision-policy.json +66 -0
- package/package.json +56 -0
- package/routing/README.md +24 -0
- package/routing/routing.json +81 -0
- package/routing/routing.schema.json +66 -0
- package/rules/README.md +10 -0
- package/rules/common.md +52 -0
- package/rules/loop-prevention.md +15 -0
- package/rules/repo-local.md +6 -0
- package/scripts/check-environment.ps1 +22 -0
- package/scripts/context-pack.mjs +17 -0
- package/scripts/e2e-investigate-repro.mjs +66 -0
- package/scripts/model-maintenance-job.mjs +59 -0
- package/scripts/observe-models.mjs +97 -0
- package/scripts/orchestrate.mjs +137 -0
- package/scripts/reevaluate-models.mjs +95 -0
- package/scripts/report-model-maintenance.mjs +70 -0
- package/scripts/resolve-capabilities.mjs +39 -0
- package/scripts/run-pipeline.mjs +56 -0
- package/scripts/sync-agents-md.ps1 +10 -0
- package/scripts/validate.mjs +71 -0
- package/skills/README.md +14 -0
- package/skills/pi-workflow/SKILL.md +26 -0
- package/skills/pi-workflow/references/code-investigation-and-fix.md +16 -0
- package/skills/pi-workflow/references/research.md +14 -0
- package/skills/pi-workflow/references/review.md +11 -0
- package/skills/pi-workflow/references/visual-work.md +14 -0
- package/skills/project-management/SKILL.md +106 -0
- package/skills/project-management/references/operations.md +52 -0
- package/skills/visual-verification/SKILL.md +88 -0
- package/skills/visual-verification/scripts/analyze-speech.ps1 +346 -0
- package/skills/visual-verification/scripts/backends/whisperx_backend.py +234 -0
- package/skills/visual-verification/scripts/common.ps1 +387 -0
- package/skills/visual-verification/scripts/contact-sheet.ps1 +121 -0
- package/skills/visual-verification/scripts/desktop-discover.ps1 +45 -0
- package/skills/visual-verification/scripts/desktop-inspect.ps1 +67 -0
- package/skills/visual-verification/scripts/desktop-record.ps1 +97 -0
- package/skills/visual-verification/scripts/desktop-screenshot.ps1 +65 -0
- package/skills/visual-verification/scripts/evaluate-sync.ps1 +249 -0
- package/skills/visual-verification/scripts/extract-frames.ps1 +79 -0
- package/skills/visual-verification/scripts/inspect-media.ps1 +138 -0
- package/skills/visual-verification/scripts/record-av.ps1 +102 -0
- package/skills/visual-verification/scripts/record.ps1 +72 -0
- package/skills/visual-verification/scripts/screenshot.ps1 +44 -0
- package/skills/visual-verification/scripts/waveform.ps1 +450 -0
- package/skills/visual-verification/scripts/winapp-common.ps1 +465 -0
- package/tests/activity.test.mjs +252 -0
- package/tests/attempt-budget.test.mjs +102 -0
- package/tests/browser.test.mjs +121 -0
- package/tests/context-pack.test.mjs +98 -0
- package/tests/dirty-gate.test.mjs +211 -0
- package/tests/e2e-browser.mjs +66 -0
- package/tests/e2e-real-orchestrator-resume.mjs +101 -0
- package/tests/e2e-real-orchestrator.mjs +41 -0
- package/tests/e2e-real-pi.mjs +27 -0
- package/tests/e2e-real-tool-orchestrator.mjs +66 -0
- package/tests/fixtures/browser-page/index.html +20 -0
- package/tests/fixtures/maintenance/availability.txt +5 -0
- package/tests/fixtures/maintenance/catalog.json +74 -0
- package/tests/fixtures/maintenance/events.json +13 -0
- package/tests/fixtures/math-repo/README.md +3 -0
- package/tests/fixtures/math-repo/package.json +7 -0
- package/tests/fixtures/math-repo/src/math.js +11 -0
- package/tests/fixtures/math-repo/test/math.test.js +7 -0
- package/tests/fixtures/observe/announcements.json +8 -0
- package/tests/fixtures/orch-concurrent-child.mjs +44 -0
- package/tests/fixtures/orch-persist-child.mjs +61 -0
- package/tests/job.test.mjs +230 -0
- package/tests/kit.test.mjs +79 -0
- package/tests/language-policy.test.mjs +93 -0
- package/tests/loop-guard.test.mjs +60 -0
- package/tests/maintenance-exec.test.mjs +218 -0
- package/tests/maintenance-runner.test.mjs +222 -0
- package/tests/maintenance.test.mjs +195 -0
- package/tests/observe.test.mjs +283 -0
- package/tests/observer-registry.test.mjs +157 -0
- package/tests/orchestrator-cleanup.test.mjs +358 -0
- package/tests/orchestrator-command.test.mjs +14 -0
- package/tests/orchestrator-persist.test.mjs +375 -0
- package/tests/orchestrator-tools.test.mjs +215 -0
- package/tests/orchestrator.test.mjs +396 -0
- package/tests/package.test.mjs +37 -0
- package/tests/pipeline.test.mjs +239 -0
- package/tests/planner-classification.test.mjs +81 -0
- package/tests/planner-split.test.mjs +67 -0
- package/tests/qoder-observer.test.mjs +266 -0
- package/tests/reassign-progression.test.mjs +104 -0
- package/tests/retry-escalation.test.mjs +120 -0
- package/tests/routing.test.mjs +110 -0
- package/tests/sqlite-concurrency.test.mjs +178 -0
- package/tests/task-global-e2e.test.mjs +63 -0
- package/tests/task-global-failed.test.mjs +134 -0
- package/tests/telemetry.test.mjs +173 -0
- package/tests/test-sync-pi.ps1 +56 -0
- 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));
|