ultimate-pi 0.16.0 → 0.18.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/skills/harness-context/SKILL.md +13 -6
- package/.agents/skills/harness-debate-plan/SKILL.md +37 -20
- package/.agents/skills/harness-eval/SKILL.md +6 -21
- package/.agents/skills/harness-governor/SKILL.md +4 -3
- package/.agents/skills/harness-orchestration/SKILL.md +39 -51
- package/.agents/skills/harness-plan/SKILL.md +23 -12
- package/.agents/skills/harness-review/SKILL.md +52 -0
- package/.agents/skills/harness-sentrux-setup/SKILL.md +13 -1
- package/.agents/skills/harness-steer/SKILL.md +14 -0
- package/.pi/agents/harness/adversary.md +3 -10
- package/.pi/agents/harness/evaluator.md +3 -12
- package/.pi/agents/harness/executor.md +12 -14
- package/.pi/agents/harness/planning/decompose.md +7 -4
- package/.pi/agents/harness/planning/hypothesis-validator.md +2 -0
- package/.pi/agents/harness/planning/hypothesis.md +4 -2
- package/.pi/agents/harness/planning/implementation-researcher.md +1 -1
- package/.pi/agents/harness/planning/plan-adversary.md +2 -0
- package/.pi/agents/harness/planning/plan-evaluator.md +2 -0
- package/.pi/agents/harness/planning/plan-synthesizer.md +25 -0
- package/.pi/agents/harness/planning/planning-context.md +48 -0
- package/.pi/agents/harness/planning/review-integrator.md +2 -0
- package/.pi/agents/harness/planning/scout-graphify.md +3 -1
- package/.pi/agents/harness/planning/scout-semantic.md +3 -1
- package/.pi/agents/harness/planning/scout-structure.md +3 -1
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
- package/.pi/agents/harness/sentrux-steward.md +51 -0
- package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
- package/.pi/extensions/harness-debate-tools.ts +12 -3
- package/.pi/extensions/harness-live-widget.ts +27 -1
- package/.pi/extensions/harness-plan-approval.ts +62 -56
- package/.pi/extensions/harness-run-context.ts +553 -84
- package/.pi/extensions/harness-subagent-submit.ts +43 -33
- package/.pi/extensions/harness-telemetry.ts +29 -4
- package/.pi/extensions/lib/debate-bus-core.ts +15 -9
- package/.pi/extensions/lib/harness-artifact-gate.ts +182 -0
- package/.pi/extensions/lib/harness-posthog.ts +9 -5
- package/.pi/extensions/lib/harness-spawn-topology.ts +188 -0
- package/.pi/extensions/lib/harness-subagent-auth.ts +105 -19
- package/.pi/extensions/lib/harness-subagent-policy.ts +37 -19
- package/.pi/extensions/lib/harness-subagent-precheck.ts +35 -9
- package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
- package/.pi/extensions/lib/harness-subagent-submit-registry.ts +21 -3
- package/.pi/extensions/lib/harness-subagents-bridge.ts +91 -28
- package/.pi/extensions/lib/harness-subprocess-bootstrap.ts +73 -0
- package/.pi/extensions/lib/plan-approval/create-plan.ts +2 -3
- package/.pi/extensions/lib/plan-approval/resolve-disk.ts +102 -0
- package/.pi/extensions/lib/plan-approval/schema.ts +22 -8
- package/.pi/extensions/lib/plan-approval/types.ts +1 -1
- package/.pi/extensions/lib/plan-approval/validate.ts +2 -2
- package/.pi/extensions/lib/plan-approval-readiness.ts +241 -0
- package/.pi/extensions/lib/plan-debate-eligibility.ts +67 -7
- package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
- package/.pi/extensions/lib/plan-debate-gate.ts +101 -17
- package/.pi/extensions/lib/plan-debate-lanes.ts +57 -3
- package/.pi/extensions/lib/plan-debate-round-status.ts +18 -7
- package/.pi/extensions/lib/plan-messenger.ts +4 -0
- package/.pi/extensions/lib/plan-review-gate.ts +59 -0
- package/.pi/extensions/lib/posthog-client.ts +76 -0
- package/.pi/extensions/policy-gate.ts +24 -19
- package/.pi/extensions/trace-recorder.ts +1 -0
- package/.pi/harness/agents.manifest.json +24 -16
- package/.pi/harness/corpus/cron.example +8 -0
- package/.pi/harness/corpus/graphify-kb-updater.config.json +159 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.env.template +4 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.service +17 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.timer +11 -0
- package/.pi/harness/docs/adrs/0001-harness-constitution.md +2 -1
- package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +7 -6
- package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +6 -1
- package/.pi/harness/docs/adrs/0031-harness-run-context.md +1 -1
- package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -0
- package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +3 -3
- package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +8 -5
- package/.pi/harness/docs/adrs/0039-harness-post-run-review-gate.md +47 -0
- package/.pi/harness/docs/adrs/0040-practice-grounded-orchestration.md +40 -0
- package/.pi/harness/docs/adrs/0041-intelligent-planning-reconnaissance.md +39 -0
- package/.pi/harness/docs/adrs/0042-agent-native-orchestration.md +35 -0
- package/.pi/harness/docs/adrs/0043-path-first-harness-tools.md +38 -0
- package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +36 -0
- package/.pi/harness/docs/adrs/README.md +10 -0
- package/.pi/harness/docs/graphify-kb-updater-runbook.md +157 -0
- package/.pi/harness/docs/practice-map.md +110 -0
- package/.pi/harness/env.harness.template +5 -3
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/implementation-research.yaml +28 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/review-round-consolidated.yaml +25 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-packet.yaml +196 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-review.md +14 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/research-brief.yaml +62 -0
- package/.pi/harness/evals/smoke/sentrux-stub.json +1 -1
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +43 -17
- package/.pi/harness/specs/README.md +1 -1
- package/.pi/harness/specs/harness-run-context.schema.json +11 -0
- package/.pi/harness/specs/harness-spawn-context.schema.json +14 -0
- package/.pi/harness/specs/plan-execution-plan.schema.json +39 -1
- package/.pi/harness/specs/plan-packet.schema.json +4 -0
- package/.pi/harness/specs/plan-phase-status.schema.json +17 -0
- package/.pi/harness/specs/plan-phase-waiver.schema.json +25 -0
- package/.pi/harness/specs/plan-planning-context.schema.json +50 -0
- package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
- package/.pi/harness/specs/repair-brief.schema.json +45 -0
- package/.pi/harness/specs/review-outcome.schema.json +46 -0
- package/.pi/harness/specs/sentrux-manifest-proposal.schema.json +80 -0
- package/.pi/harness/specs/sentrux-signal.schema.json +43 -0
- package/.pi/harness/specs/steer-state.schema.json +20 -0
- package/.pi/lib/harness-context-mode-policy.ts +256 -0
- package/.pi/lib/harness-repair-brief.ts +145 -0
- package/.pi/lib/harness-run-context.ts +591 -32
- package/.pi/lib/harness-ui-state.ts +87 -9
- package/.pi/model-router.example.json +13 -4
- package/.pi/prompts/harness-auto.md +9 -9
- package/.pi/prompts/harness-critic.md +3 -30
- package/.pi/prompts/harness-eval.md +4 -37
- package/.pi/prompts/harness-plan.md +139 -57
- package/.pi/prompts/harness-review.md +150 -15
- package/.pi/prompts/harness-run.md +62 -10
- package/.pi/prompts/harness-sentrux-steward.md +55 -0
- package/.pi/prompts/harness-setup.md +4 -4
- package/.pi/prompts/harness-steer.md +30 -0
- package/.pi/scripts/graphify-kb-updater.mjs +358 -0
- package/.pi/scripts/harness-generate-model-router.mjs +118 -36
- package/.pi/scripts/harness-model-router-routing.test.mjs +97 -0
- package/.pi/scripts/harness-sync-model-router.mjs +15 -2
- package/.pi/scripts/harness-verify.mjs +51 -6
- package/.pi/scripts/harness-web-policy-guard.mjs +68 -0
- package/.pi/scripts/validate-plan-dag.mjs +3 -3
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +22 -0
- package/package.json +5 -4
- package/vendor/pi-model-router/UPSTREAM_PIN.md +3 -1
- package/vendor/pi-model-router/extensions/commands.ts +4 -4
- package/vendor/pi-model-router/extensions/index.ts +21 -0
- package/vendor/pi-model-router/extensions/provider.ts +130 -79
- package/vendor/pi-model-router/extensions/routing.ts +148 -0
- package/vendor/pi-model-router/extensions/state.ts +3 -0
- package/vendor/pi-model-router/extensions/types.ts +9 -0
- package/vendor/pi-model-router/extensions/ui.ts +16 -2
- package/.pi/prompts/git-sync.md +0 -124
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* graphify-kb-updater — conservative local updater for Graphify source corpus.
|
|
4
|
+
*
|
|
5
|
+
* Daily automation may auto-promote only explicitly approved allowlisted public
|
|
6
|
+
* sources with complete provenance and rights/access metadata. Risky or unclear
|
|
7
|
+
* classes (books, transcripts, YouTube, paid/mirrored/unknown content) remain
|
|
8
|
+
* staged until manually approved.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { basename, extname, join, relative, resolve } from "node:path";
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
|
|
16
|
+
let ROOT = resolve(new URL("../..", import.meta.url).pathname);
|
|
17
|
+
const DEFAULT_CONFIG = ".pi/harness/corpus/graphify-kb-updater.config.json";
|
|
18
|
+
const DEFAULT_STATE_DIR = ".pi/harness/corpus/graphify-kb-updater-state";
|
|
19
|
+
const DEFAULT_RAW_DIR = "raw/graphify-kb-updates";
|
|
20
|
+
const DEFAULT_DATA_DIR = "data";
|
|
21
|
+
const DEFAULT_GRAPH_DIR = "graphify-out";
|
|
22
|
+
const REQUIRED_RIGHTS = ["license", "access", "approved_by", "approved_at"];
|
|
23
|
+
const RISKY_KINDS = new Set(["book", "transcript", "youtube"]);
|
|
24
|
+
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const args = {
|
|
27
|
+
dryRun: true,
|
|
28
|
+
apply: false,
|
|
29
|
+
config: DEFAULT_CONFIG,
|
|
30
|
+
stateDir: DEFAULT_STATE_DIR,
|
|
31
|
+
rawDir: DEFAULT_RAW_DIR,
|
|
32
|
+
dataDir: DEFAULT_DATA_DIR,
|
|
33
|
+
graphDir: DEFAULT_GRAPH_DIR,
|
|
34
|
+
refreshGraph: false,
|
|
35
|
+
skipGraph: false,
|
|
36
|
+
pilotReport: false,
|
|
37
|
+
schedulerSmoke: false,
|
|
38
|
+
maxPromotions: 25,
|
|
39
|
+
projectRoot: ROOT,
|
|
40
|
+
};
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const a = argv[i];
|
|
43
|
+
if (a === "--dry-run") args.dryRun = true;
|
|
44
|
+
else if (a === "--apply") { args.apply = true; args.dryRun = false; }
|
|
45
|
+
else if (a === "--refresh-graph") args.refreshGraph = true;
|
|
46
|
+
else if (a === "--skip-graph") args.skipGraph = true;
|
|
47
|
+
else if (a === "--pilot-report") args.pilotReport = true;
|
|
48
|
+
else if (a === "--scheduler-smoke") args.schedulerSmoke = true;
|
|
49
|
+
else if (a === "--config") args.config = argv[++i];
|
|
50
|
+
else if (a === "--state-dir") args.stateDir = argv[++i];
|
|
51
|
+
else if (a === "--raw-dir") args.rawDir = argv[++i];
|
|
52
|
+
else if (a === "--data-dir") args.dataDir = argv[++i];
|
|
53
|
+
else if (a === "--graph-dir") args.graphDir = argv[++i];
|
|
54
|
+
else if (a === "--project-root") args.projectRoot = argv[++i];
|
|
55
|
+
else if (a === "--max-promotions") args.maxPromotions = Number(argv[++i]);
|
|
56
|
+
else if (a === "--help") usage(0);
|
|
57
|
+
else throw new Error(`unknown argument: ${a}`);
|
|
58
|
+
}
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function usage(code) {
|
|
63
|
+
console.log(`Usage: node .pi/scripts/graphify-kb-updater.mjs [--dry-run|--apply] [options]\n\nOptions:\n --config PATH JSON source policy config\n --state-dir PATH durable registry/run-log directory\n --raw-dir PATH stable promoted source corpus root\n --data-dir PATH local books/transcripts root\n --project-root PATH root used for corpus/state paths\n --refresh-graph run graphify update . after promotions\n --skip-graph never run graphify\n --pilot-report print frontier recall/precision/noise/graph proxy metrics\n --scheduler-smoke validate scheduler-oriented env without promotion\n --max-promotions N cap apply promotions per run (default 25)`);
|
|
64
|
+
process.exit(code);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readJson(path, fallback) {
|
|
68
|
+
if (!existsSync(path)) return fallback;
|
|
69
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sortDeep(value) {
|
|
73
|
+
if (Array.isArray(value)) return value.map(sortDeep);
|
|
74
|
+
if (!value || typeof value !== "object") return value;
|
|
75
|
+
return Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortDeep(value[k])]));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function stableJson(obj) { return `${JSON.stringify(sortDeep(obj), null, 2)}\n`; }
|
|
79
|
+
function sha256(text) { return createHash("sha256").update(text).digest("hex"); }
|
|
80
|
+
function slugify(s) { return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 90) || "source"; }
|
|
81
|
+
function nowIso() { return new Date().toISOString(); }
|
|
82
|
+
function rel(path) { return relative(ROOT, path) || "."; }
|
|
83
|
+
|
|
84
|
+
function loadConfig(args) {
|
|
85
|
+
const path = resolve(ROOT, args.config);
|
|
86
|
+
const cfg = readJson(path, {});
|
|
87
|
+
return {
|
|
88
|
+
schemaVersion: cfg.schema_version ?? "1.0.0",
|
|
89
|
+
policy: cfg.policy ?? "conservative-staged-review",
|
|
90
|
+
sourceTaxonomy: cfg.source_taxonomy ?? {},
|
|
91
|
+
competitorTaxonomy: cfg.competitor_taxonomy ?? {},
|
|
92
|
+
allowlist: (cfg.allowlist ?? []).map((entry) => typeof entry === "string" ? { domain: entry, approved: true } : entry),
|
|
93
|
+
reviewQueue: cfg.review_queue ?? [],
|
|
94
|
+
articleQueries: cfg.article_queries ?? [],
|
|
95
|
+
paperFeeds: cfg.paper_feeds ?? [],
|
|
96
|
+
localBooks: cfg.local_books ?? [{ path: "data/books" }],
|
|
97
|
+
localTranscripts: cfg.local_transcripts ?? [{ path: "data/youtube-transcripts" }],
|
|
98
|
+
youtubeCandidates: cfg.youtube_candidates ?? [],
|
|
99
|
+
autoPromoteAllowlist: cfg.auto_promote_allowlist === true,
|
|
100
|
+
path,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function walkFiles(root, exts, max = 200) {
|
|
105
|
+
const out = [];
|
|
106
|
+
function walk(dir) {
|
|
107
|
+
if (out.length >= max || !existsSync(dir)) return;
|
|
108
|
+
for (const name of readdirSync(dir)) {
|
|
109
|
+
const p = join(dir, name);
|
|
110
|
+
const st = statSync(p);
|
|
111
|
+
if (st.isDirectory()) walk(p);
|
|
112
|
+
else if (exts.includes(extname(name).toLowerCase())) out.push(p);
|
|
113
|
+
if (out.length >= max) break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
walk(root);
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function rightsFromSidecar(file) {
|
|
121
|
+
const json = `${file}.rights.json`;
|
|
122
|
+
if (existsSync(json)) return readJson(json, null);
|
|
123
|
+
const metaJson = file.replace(/\.[^.]+$/, ".meta.json");
|
|
124
|
+
if (existsSync(metaJson)) return readJson(metaJson, null)?.rights_access ?? null;
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hasRightsApproval(candidate) {
|
|
129
|
+
const r = candidate.rights_access;
|
|
130
|
+
return Boolean(r && REQUIRED_RIGHTS.every((k) => typeof r[k] === "string" && r[k].trim()));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function urlDomain(url) {
|
|
134
|
+
try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function allowlistEntry(cfg, domain) {
|
|
138
|
+
return cfg.allowlist.find((entry) => entry.domain === domain || domain.endsWith(`.${entry.domain}`));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function competitorLabels(cfg, candidate) {
|
|
142
|
+
const haystack = `${candidate.title ?? ""} ${candidate.url ?? ""} ${candidate.path ?? ""}`.toLowerCase();
|
|
143
|
+
const labels = [];
|
|
144
|
+
for (const [category, spec] of Object.entries(cfg.competitorTaxonomy ?? {})) {
|
|
145
|
+
const terms = Array.isArray(spec) ? spec : (spec.keywords ?? []);
|
|
146
|
+
if (terms.some((term) => haystack.includes(String(term).toLowerCase()))) labels.push(category);
|
|
147
|
+
}
|
|
148
|
+
return labels;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function candidateId(candidate) {
|
|
152
|
+
return sha256([candidate.kind, candidate.source_type, candidate.url ?? candidate.path ?? candidate.query ?? "", candidate.title ?? ""].join("\n")).slice(0, 16);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeCandidate(cfg, raw) {
|
|
156
|
+
const domain = raw.domain ?? urlDomain(raw.url);
|
|
157
|
+
const allow = domain ? allowlistEntry(cfg, domain) : null;
|
|
158
|
+
const taxonomy = cfg.sourceTaxonomy?.[raw.kind] ?? {};
|
|
159
|
+
const candidate = {
|
|
160
|
+
...raw,
|
|
161
|
+
domain,
|
|
162
|
+
category: raw.category ?? taxonomy.category ?? raw.kind,
|
|
163
|
+
risk_class: raw.risk_class ?? taxonomy.risk_class ?? (RISKY_KINDS.has(raw.kind) ? "high" : "medium"),
|
|
164
|
+
provenance: raw.provenance ?? { origin: raw.source_type, discovered_by: "graphify-kb-updater", locator: raw.url ?? raw.path ?? raw.query ?? null },
|
|
165
|
+
rights_access: raw.rights_access ?? null,
|
|
166
|
+
allowlist_state: allow ? { allowed: true, domain: allow.domain, approved: allow.approved === true, approved_by: allow.approved_by ?? null, approved_at: allow.approved_at ?? null } : { allowed: false },
|
|
167
|
+
approval_state: raw.approved === true ? "approved" : "not_approved",
|
|
168
|
+
};
|
|
169
|
+
candidate.competitor_labels = raw.competitor_labels ?? competitorLabels(cfg, candidate);
|
|
170
|
+
candidate.id = raw.id ?? candidateId(candidate);
|
|
171
|
+
candidate.content_hash = sha256(sourceBody(candidate));
|
|
172
|
+
return candidate;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function discoverCandidates(cfg, args) {
|
|
176
|
+
const candidates = [];
|
|
177
|
+
for (const query of cfg.articleQueries) candidates.push({ kind: "article", source_type: "web_search_query", title: query, query, review_required: true, promotion_policy: "stage_only" });
|
|
178
|
+
for (const feed of cfg.paperFeeds) candidates.push({ kind: "paper", source_type: "feed", title: feed.title ?? feed.url, url: feed.url, rights_access: feed.rights_access ?? null, review_required: true, promotion_policy: "stage_only", provenance: feed.provenance });
|
|
179
|
+
for (const entry of cfg.reviewQueue) {
|
|
180
|
+
const domain = urlDomain(entry.url);
|
|
181
|
+
const allow = allowlistEntry(cfg, domain);
|
|
182
|
+
const explicit = cfg.autoPromoteAllowlist && allow?.approved === true && entry.approved === true;
|
|
183
|
+
candidates.push({ ...entry, kind: entry.kind ?? "article", source_type: "review_queue", domain, review_required: !explicit, promotion_policy: explicit ? "allowlist_auto_promote" : "manual_review", rights_access: entry.rights_access ?? null });
|
|
184
|
+
}
|
|
185
|
+
for (const spec of cfg.localBooks) for (const file of walkFiles(resolve(ROOT, spec.path), [".md", ".txt", ".pdf"], spec.max_files ?? 50)) candidates.push({ kind: "book", source_type: "local_file", title: basename(file), path: rel(file), rights_access: rightsFromSidecar(file), review_required: true, promotion_policy: "manual_review" });
|
|
186
|
+
for (const spec of cfg.localTranscripts) for (const file of walkFiles(resolve(ROOT, spec.path), [".md", ".txt", ".vtt"], spec.max_files ?? 80)) candidates.push({ kind: "transcript", source_type: "local_file", title: basename(file), path: rel(file), rights_access: rightsFromSidecar(file), review_required: true, promotion_policy: "manual_review" });
|
|
187
|
+
for (const yt of cfg.youtubeCandidates) candidates.push({ ...yt, kind: "youtube", source_type: "youtube_candidate", review_required: true, promotion_policy: "manual_review", rights_access: yt.rights_access ?? null });
|
|
188
|
+
return candidates.map((c) => normalizeCandidate(cfg, c));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function loadRegistry(args) {
|
|
192
|
+
const dir = resolve(ROOT, args.stateDir);
|
|
193
|
+
return readJson(join(dir, "registry.json"), { schema_version: "1.1.0", candidates: {}, runs: [] });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function writeRegistry(args, registry) {
|
|
197
|
+
const dir = resolve(ROOT, args.stateDir);
|
|
198
|
+
mkdirSync(dir, { recursive: true });
|
|
199
|
+
writeFileSync(join(dir, "registry.json"), stableJson(registry));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sourceBody(candidate) {
|
|
203
|
+
if (candidate.source_type === "local_file" && candidate.path) {
|
|
204
|
+
const abs = resolve(ROOT, candidate.path);
|
|
205
|
+
if (existsSync(abs) && extname(abs).toLowerCase() !== ".pdf") return readFileSync(abs, "utf8");
|
|
206
|
+
}
|
|
207
|
+
return `# ${candidate.title ?? candidate.id}\n\nSource staged by graphify-kb-updater. Fetch/parse content via approved harness web/API workflow before broad use.\n`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function promotionAllowed(candidate) {
|
|
211
|
+
if (!hasRightsApproval(candidate)) return { ok: false, reason: "missing_rights_access_approval" };
|
|
212
|
+
if (RISKY_KINDS.has(candidate.kind) && candidate.approved !== true) return { ok: false, reason: "manual_approval_required" };
|
|
213
|
+
if (candidate.source_type === "review_queue" && candidate.promotion_policy === "allowlist_auto_promote" && candidate.allowlist_state.allowed && candidate.allowlist_state.approved) return { ok: true };
|
|
214
|
+
return candidate.approved === true ? { ok: true } : { ok: false, reason: "manual_approval_required" };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function promote(candidate, args) {
|
|
218
|
+
const body = sourceBody(candidate);
|
|
219
|
+
const contentHash = sha256(body);
|
|
220
|
+
const dir = resolve(ROOT, args.rawDir, candidate.kind);
|
|
221
|
+
const base = `${new Date().toISOString().slice(0, 10)}-${slugify(candidate.title ?? candidate.id)}-${contentHash.slice(0, 8)}`;
|
|
222
|
+
const md = join(dir, `${base}.md`);
|
|
223
|
+
const prov = join(dir, `${base}.provenance.json`);
|
|
224
|
+
mkdirSync(dir, { recursive: true });
|
|
225
|
+
const header = `---\nsource_id: ${candidate.id}\nkind: ${candidate.kind}\ncategory: ${candidate.category}\ncontent_sha256: ${contentHash}\nrights_license: ${candidate.rights_access.license}\nrights_access: ${candidate.rights_access.access}\napproved_by: ${candidate.rights_access.approved_by}\napproved_at: ${candidate.rights_access.approved_at}\ncompetitor_labels: ${JSON.stringify(candidate.competitor_labels)}\n---\n\n`;
|
|
226
|
+
writeFileSync(md, header + body);
|
|
227
|
+
writeFileSync(prov, stableJson({ ...candidate, content_sha256: contentHash, promoted_path: rel(md), promoted_at: nowIso() }));
|
|
228
|
+
return { path: rel(md), provenance_path: rel(prov), content_hash: contentHash };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function refreshGraph(args, changedCount) {
|
|
232
|
+
if (args.skipGraph) return { action: "skipped_by_flag" };
|
|
233
|
+
if (!args.refreshGraph) return { action: changedCount > 0 ? "planned" : "skipped_noop" };
|
|
234
|
+
if (changedCount === 0) return { action: "skipped_noop" };
|
|
235
|
+
const run = spawnSync("graphify", ["update", "."], { cwd: ROOT, encoding: "utf8", timeout: 20 * 60 * 1000 });
|
|
236
|
+
const report = resolve(ROOT, args.graphDir, "GRAPH_REPORT.md");
|
|
237
|
+
return { action: "graphify_update", exit_status: run.status, ok: run.status === 0 && existsSync(report), report: rel(report), stderr: (run.stderr ?? "").slice(0, 1200) };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function appendRunLog(args, summary) {
|
|
241
|
+
const dir = resolve(ROOT, args.stateDir, "logs");
|
|
242
|
+
mkdirSync(dir, { recursive: true });
|
|
243
|
+
writeFileSync(join(dir, `${summary.run_id}.json`), stableJson(summary));
|
|
244
|
+
const line = `${JSON.stringify(summary)}\n`;
|
|
245
|
+
const jsonl = join(dir, "runs.jsonl");
|
|
246
|
+
writeFileSync(jsonl, existsSync(jsonl) ? readFileSync(jsonl, "utf8") + line : line);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function byCount(items, keyFn) {
|
|
250
|
+
const out = {};
|
|
251
|
+
for (const item of items) for (const key of [keyFn(item)].flat().filter(Boolean)) out[key] = (out[key] ?? 0) + 1;
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function pilotMetrics(summary) {
|
|
256
|
+
const considered = Math.max(summary.candidate_count, 1);
|
|
257
|
+
const promoted = summary.promoted_count;
|
|
258
|
+
const duplicates = summary.duplicate_skips;
|
|
259
|
+
return {
|
|
260
|
+
frontier_recall_proxy: Number(((summary.candidate_count - duplicates) / considered).toFixed(3)),
|
|
261
|
+
promoted_precision_proxy: promoted === 0 ? 1 : Number((promoted / Math.max(promoted + summary.failure_count, 1)).toFixed(3)),
|
|
262
|
+
duplicate_noise_rate: Number((duplicates / considered).toFixed(3)),
|
|
263
|
+
graphify_success: ["skipped_noop", "planned", "skipped_by_flag"].includes(summary.graph.action) || summary.graph.ok === true,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function schedulerSmoke() {
|
|
268
|
+
const service = readFileSync(resolve(ROOT, ".pi/harness/corpus/systemd/graphify-kb-updater.service"), "utf8");
|
|
269
|
+
const timer = readFileSync(resolve(ROOT, ".pi/harness/corpus/systemd/graphify-kb-updater.timer"), "utf8");
|
|
270
|
+
const cron = readFileSync(resolve(ROOT, ".pi/harness/corpus/cron.example"), "utf8");
|
|
271
|
+
const checks = {
|
|
272
|
+
systemd_daily: /OnCalendar=\*-\*-\*\s+08:30:00|OnCalendar=daily/i.test(timer),
|
|
273
|
+
cron_daily: /^30\s+8\s+\*\s+\*\s+\*/m.test(cron),
|
|
274
|
+
bounded_timeout: /timeout 45m/.test(service) && /timeout 45m/.test(cron),
|
|
275
|
+
locked_no_overlap: /flock -n/.test(service) && /flock -n/.test(cron),
|
|
276
|
+
explicit_env: /EnvironmentFile/.test(service) && /UP_ROOT/.test(cron),
|
|
277
|
+
logged: /StandardOutput=append/.test(service) && /HARNESS_GRAPHIFY_KB_LOG/.test(cron),
|
|
278
|
+
refresh_intent: /--refresh-graph/.test(cron),
|
|
279
|
+
};
|
|
280
|
+
const ok = Object.values(checks).every(Boolean);
|
|
281
|
+
console.log(JSON.stringify({ ok, checks }, null, 2));
|
|
282
|
+
process.exit(ok ? 0 : 1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function main() {
|
|
286
|
+
const started = Date.now();
|
|
287
|
+
const args = parseArgs(process.argv.slice(2));
|
|
288
|
+
ROOT = resolve(args.projectRoot);
|
|
289
|
+
if (args.schedulerSmoke) schedulerSmoke();
|
|
290
|
+
const cfg = loadConfig(args);
|
|
291
|
+
const registry = loadRegistry(args);
|
|
292
|
+
const candidates = discoverCandidates(cfg, args);
|
|
293
|
+
let duplicates = 0, promoted = 0, failed = 0, changedExisting = 0;
|
|
294
|
+
const planned = [], blocked = [], promotedRefs = [], skipped = [];
|
|
295
|
+
const runAt = nowIso();
|
|
296
|
+
|
|
297
|
+
for (const c of candidates) {
|
|
298
|
+
const prior = registry.candidates[c.id];
|
|
299
|
+
const contentChanged = Boolean(prior?.content_hash && prior.content_hash !== c.content_hash);
|
|
300
|
+
if (prior?.status === "promoted" && !contentChanged) {
|
|
301
|
+
duplicates++;
|
|
302
|
+
registry.candidates[c.id] = { ...prior, last_seen_at: runAt, content_state: "unchanged" };
|
|
303
|
+
skipped.push({ id: c.id, reason: "duplicate_unchanged", content_state: "unchanged" });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (contentChanged) changedExisting++;
|
|
307
|
+
const gate = promotionAllowed(c);
|
|
308
|
+
registry.candidates[c.id] = { ...(prior ?? {}), ...c, first_seen_at: prior?.first_seen_at ?? runAt, last_seen_at: runAt, status: gate.ok ? "promotable" : "review_required", block_reason: gate.reason ?? null, content_state: contentChanged ? "changed" : "new" };
|
|
309
|
+
if (!gate.ok) { blocked.push({ id: c.id, title: c.title, reason: gate.reason, allowlist_state: c.allowlist_state, category: c.category, competitor_labels: c.competitor_labels }); continue; }
|
|
310
|
+
planned.push(c);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (args.apply) {
|
|
314
|
+
for (const c of planned.slice(0, args.maxPromotions)) {
|
|
315
|
+
try {
|
|
316
|
+
const ref = promote(c, args);
|
|
317
|
+
registry.candidates[c.id] = { ...registry.candidates[c.id], ...ref, status: "promoted", promoted_at: nowIso() };
|
|
318
|
+
promotedRefs.push(ref); promoted++;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
failed++;
|
|
321
|
+
registry.candidates[c.id] = { ...registry.candidates[c.id], status: "failed", error: String(err?.message ?? err) };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const c of planned.slice(args.maxPromotions)) skipped.push({ id: c.id, reason: "max_promotions_cap" });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const graph = refreshGraph(args, promoted);
|
|
328
|
+
const stale = registry.runs.at?.(-1)?.run_id ? [] : ["no_prior_apply_run_recorded"];
|
|
329
|
+
const summary = {
|
|
330
|
+
run_id: `kb-${Date.now()}`,
|
|
331
|
+
last_run_at: runAt,
|
|
332
|
+
mode: args.dryRun ? "dry-run" : "apply",
|
|
333
|
+
candidate_count: candidates.length,
|
|
334
|
+
planned_promotions: planned.length,
|
|
335
|
+
promoted_count: promoted,
|
|
336
|
+
duplicate_skips: duplicates,
|
|
337
|
+
blocked_count: blocked.length,
|
|
338
|
+
skipped_count: skipped.length,
|
|
339
|
+
failure_count: failed,
|
|
340
|
+
changed_existing_count: changedExisting,
|
|
341
|
+
runtime_ms: Date.now() - started,
|
|
342
|
+
counts: { by_kind: byCount(candidates, (c) => c.kind), by_source_type: byCount(candidates, (c) => c.source_type), by_competitor_label: byCount(candidates, (c) => c.competitor_labels), allowlisted: candidates.filter((c) => c.allowlist_state.allowed).length },
|
|
343
|
+
stale_warnings: stale,
|
|
344
|
+
graph,
|
|
345
|
+
exit_status: failed || graph.ok === false ? 1 : 0,
|
|
346
|
+
promoted: promotedRefs,
|
|
347
|
+
blocked: blocked.slice(0, 50),
|
|
348
|
+
skipped: skipped.slice(0, 50),
|
|
349
|
+
config: rel(cfg.path),
|
|
350
|
+
};
|
|
351
|
+
if (args.pilotReport) summary.pilot = pilotMetrics(summary);
|
|
352
|
+
registry.runs.push(summary);
|
|
353
|
+
if (args.apply) { writeRegistry(args, registry); appendRunLog(args, summary); }
|
|
354
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
355
|
+
process.exit(summary.exit_status);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try { main(); } catch (err) { console.error(`graphify-kb-updater: ${err.stack ?? err}`); process.exit(2); }
|
|
@@ -22,9 +22,9 @@ const UP_PKG = join(SCRIPT_DIR, "..", "..");
|
|
|
22
22
|
const OUT_PATH = join(process.cwd(), ".pi", "model-router.json");
|
|
23
23
|
|
|
24
24
|
const PROVIDER_PRIORITY = [
|
|
25
|
+
"openai",
|
|
25
26
|
"opencode-go",
|
|
26
27
|
"anthropic",
|
|
27
|
-
"openai",
|
|
28
28
|
"google",
|
|
29
29
|
"openrouter",
|
|
30
30
|
"groq",
|
|
@@ -35,6 +35,7 @@ const PROVIDER_PRIORITY = [
|
|
|
35
35
|
/** Substring hints per tier (first match in available ids wins). */
|
|
36
36
|
const TIER_HINTS = {
|
|
37
37
|
high: [
|
|
38
|
+
"gpt-5.5-pro",
|
|
38
39
|
"deepseek-v4-pro",
|
|
39
40
|
"gpt-5.4-pro",
|
|
40
41
|
"claude-opus",
|
|
@@ -43,6 +44,7 @@ const TIER_HINTS = {
|
|
|
43
44
|
"pro",
|
|
44
45
|
],
|
|
45
46
|
medium: [
|
|
47
|
+
"gpt-5.5",
|
|
46
48
|
"qwen3.6-plus",
|
|
47
49
|
"kimi-k2.6",
|
|
48
50
|
"gpt-5.4",
|
|
@@ -98,7 +100,10 @@ function canonicalRef(provider, modelId) {
|
|
|
98
100
|
|
|
99
101
|
function pickTierModel(models, tier) {
|
|
100
102
|
const hints = TIER_HINTS[tier];
|
|
101
|
-
const
|
|
103
|
+
for (const hint of hints) {
|
|
104
|
+
const exact = models.find((m) => m.id === hint);
|
|
105
|
+
if (exact) return canonicalRef(exact.provider, exact.id);
|
|
106
|
+
}
|
|
102
107
|
for (const hint of hints) {
|
|
103
108
|
const match = models.find((m) => m.id.includes(hint));
|
|
104
109
|
if (match) return canonicalRef(match.provider, match.id);
|
|
@@ -114,6 +119,10 @@ function pickTierModel(models, tier) {
|
|
|
114
119
|
return canonicalRef(models[0].provider, models[0].id);
|
|
115
120
|
}
|
|
116
121
|
|
|
122
|
+
function modelsForProvider(available, provider) {
|
|
123
|
+
return available.filter((m) => m.provider === provider);
|
|
124
|
+
}
|
|
125
|
+
|
|
117
126
|
function choosePrimaryProvider(available) {
|
|
118
127
|
const byProvider = new Map();
|
|
119
128
|
for (const m of available) {
|
|
@@ -129,7 +138,7 @@ function choosePrimaryProvider(available) {
|
|
|
129
138
|
|
|
130
139
|
function buildFallbacks(available, primaryProvider, highModel) {
|
|
131
140
|
const fallbacks = [];
|
|
132
|
-
for (const p of ["anthropic", "google", "openai"]) {
|
|
141
|
+
for (const p of ["anthropic", "google", "openai", "opencode-go"]) {
|
|
133
142
|
if (p === primaryProvider) continue;
|
|
134
143
|
const alt = available.filter((m) => m.provider === p);
|
|
135
144
|
if (alt.length === 0) continue;
|
|
@@ -139,6 +148,76 @@ function buildFallbacks(available, primaryProvider, highModel) {
|
|
|
139
148
|
return fallbacks.slice(0, 3);
|
|
140
149
|
}
|
|
141
150
|
|
|
151
|
+
/** Session-locked router: one model SKU per profile; tiers vary thinking only. */
|
|
152
|
+
function buildRoutedProfile(available, provider) {
|
|
153
|
+
const models = modelsForProvider(available, provider);
|
|
154
|
+
if (models.length === 0) return null;
|
|
155
|
+
const sku =
|
|
156
|
+
pickTierModel(models, "medium") ??
|
|
157
|
+
pickTierModel(models, "high") ??
|
|
158
|
+
pickTierModel(models, "low");
|
|
159
|
+
if (!sku) return null;
|
|
160
|
+
const fallbacks = buildFallbacks(available, provider, sku);
|
|
161
|
+
const high = { model: sku, thinking: "high" };
|
|
162
|
+
if (fallbacks.length) high.fallbacks = fallbacks;
|
|
163
|
+
return {
|
|
164
|
+
high,
|
|
165
|
+
medium: { model: sku, thinking: "medium" },
|
|
166
|
+
low: { model: sku, thinking: "low" },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function addCheapDeepProfiles(profiles, available, provider) {
|
|
171
|
+
const models = modelsForProvider(available, provider);
|
|
172
|
+
if (models.length === 0) return;
|
|
173
|
+
const sku =
|
|
174
|
+
pickTierModel(models, "medium") ??
|
|
175
|
+
pickTierModel(models, "high") ??
|
|
176
|
+
pickTierModel(models, "low");
|
|
177
|
+
if (!sku) return;
|
|
178
|
+
const fallbacks = buildFallbacks(available, provider, sku);
|
|
179
|
+
const deepHigh = { model: sku, thinking: "xhigh" };
|
|
180
|
+
if (fallbacks.length) deepHigh.fallbacks = fallbacks;
|
|
181
|
+
profiles.cheap = {
|
|
182
|
+
high: { model: sku, thinking: "low" },
|
|
183
|
+
medium: { model: sku, thinking: "off" },
|
|
184
|
+
low: { model: sku, thinking: "off" },
|
|
185
|
+
};
|
|
186
|
+
profiles.deep = {
|
|
187
|
+
high: deepHigh,
|
|
188
|
+
medium: { model: sku, thinking: "medium" },
|
|
189
|
+
low: { model: sku, thinking: "low" },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveClassifierModel(available) {
|
|
194
|
+
const openaiModels = modelsForProvider(available, "openai");
|
|
195
|
+
if (openaiModels.length > 0) {
|
|
196
|
+
return (
|
|
197
|
+
pickTierModel(openaiModels, "low") ??
|
|
198
|
+
canonicalRef(openaiModels[openaiModels.length - 1].provider, openaiModels[openaiModels.length - 1].id)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const { models } = choosePrimaryProvider(available);
|
|
202
|
+
return pickTierModel(models, "medium");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** OpenAI-backed default profile name exposed as `router/auto`. */
|
|
206
|
+
const OPENAI_PROFILE_NAME = "auto";
|
|
207
|
+
|
|
208
|
+
function routerProfileName(provider) {
|
|
209
|
+
return provider === "openai" ? OPENAI_PROFILE_NAME : provider;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolveDefaultProfile(profiles) {
|
|
213
|
+
if (profiles[OPENAI_PROFILE_NAME]) return OPENAI_PROFILE_NAME;
|
|
214
|
+
if (profiles["opencode-go"]) return "opencode-go";
|
|
215
|
+
return (
|
|
216
|
+
Object.keys(profiles).find((name) => name !== "cheap" && name !== "deep") ??
|
|
217
|
+
OPENAI_PROFILE_NAME
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
142
221
|
async function main() {
|
|
143
222
|
const force = process.argv.includes("--force");
|
|
144
223
|
const dryRun = process.argv.includes("--dry-run");
|
|
@@ -171,23 +250,37 @@ async function main() {
|
|
|
171
250
|
process.exit(0);
|
|
172
251
|
}
|
|
173
252
|
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const lowModel = pickTierModel(primaryModels, "low");
|
|
253
|
+
const profiles = {};
|
|
254
|
+
for (const provider of ["openai", "opencode-go"]) {
|
|
255
|
+
const profile = buildRoutedProfile(available, provider);
|
|
256
|
+
if (profile) profiles[routerProfileName(provider)] = profile;
|
|
257
|
+
}
|
|
180
258
|
|
|
181
|
-
if (
|
|
182
|
-
|
|
259
|
+
if (Object.keys(profiles).length === 0) {
|
|
260
|
+
const { provider: primaryProvider, models: primaryModels } =
|
|
261
|
+
choosePrimaryProvider(available);
|
|
262
|
+
const profile = buildRoutedProfile(available, primaryProvider);
|
|
263
|
+
if (!profile) {
|
|
264
|
+
fail("could not assign tier models from available registry");
|
|
265
|
+
}
|
|
266
|
+
profiles[primaryProvider] = profile;
|
|
183
267
|
}
|
|
184
268
|
|
|
185
|
-
const
|
|
269
|
+
const cheapDeepSource = profiles["opencode-go"]
|
|
270
|
+
? "opencode-go"
|
|
271
|
+
: resolveDefaultProfile(profiles);
|
|
272
|
+
addCheapDeepProfiles(profiles, available, cheapDeepSource);
|
|
273
|
+
|
|
274
|
+
const defaultProfile = resolveDefaultProfile(profiles);
|
|
275
|
+
const classifierModel = resolveClassifierModel(available);
|
|
276
|
+
if (!classifierModel) {
|
|
277
|
+
fail("could not assign classifier model from available registry");
|
|
278
|
+
}
|
|
186
279
|
|
|
187
280
|
const config = {
|
|
188
|
-
defaultProfile
|
|
281
|
+
defaultProfile,
|
|
189
282
|
debug: false,
|
|
190
|
-
classifierModel
|
|
283
|
+
classifierModel,
|
|
191
284
|
phaseBias: 0.5,
|
|
192
285
|
maxSessionBudget: 1.0,
|
|
193
286
|
largeContextThreshold: 100000,
|
|
@@ -199,27 +292,13 @@ async function main() {
|
|
|
199
292
|
},
|
|
200
293
|
{ matches: "changelog", tier: "low" },
|
|
201
294
|
],
|
|
202
|
-
profiles
|
|
203
|
-
auto: {
|
|
204
|
-
high: { model: highModel, thinking: "high", fallbacks },
|
|
205
|
-
medium: { model: mediumModel, thinking: "medium" },
|
|
206
|
-
low: { model: lowModel, thinking: "low" },
|
|
207
|
-
},
|
|
208
|
-
cheap: {
|
|
209
|
-
high: { model: mediumModel, thinking: "low" },
|
|
210
|
-
medium: { model: lowModel, thinking: "off" },
|
|
211
|
-
low: { model: lowModel, thinking: "off" },
|
|
212
|
-
},
|
|
213
|
-
deep: {
|
|
214
|
-
high: { model: highModel, thinking: "xhigh", fallbacks },
|
|
215
|
-
medium: { model: mediumModel, thinking: "medium" },
|
|
216
|
-
low: { model: lowModel, thinking: "low" },
|
|
217
|
-
},
|
|
218
|
-
},
|
|
295
|
+
profiles,
|
|
219
296
|
};
|
|
220
297
|
|
|
221
298
|
const json = `${JSON.stringify(config, null, 2)}\n`;
|
|
222
299
|
const providerSet = [...new Set(available.map((m) => m.provider))].sort();
|
|
300
|
+
const autoProfile = profiles[OPENAI_PROFILE_NAME];
|
|
301
|
+
const opencodeProfile = profiles["opencode-go"];
|
|
223
302
|
|
|
224
303
|
if (dryRun) {
|
|
225
304
|
process.stdout.write(json);
|
|
@@ -230,13 +309,16 @@ async function main() {
|
|
|
230
309
|
writeFileSync(OUT_PATH, json, "utf8");
|
|
231
310
|
|
|
232
311
|
console.log("✓ Generated .pi/model-router.json from Pi authenticated providers:");
|
|
233
|
-
console.log(`
|
|
312
|
+
console.log(` Default profile: ${defaultProfile}`);
|
|
313
|
+
console.log(` Classifier: ${classifierModel}`);
|
|
234
314
|
console.log(` Authenticated providers: ${providerSet.join(", ")}`);
|
|
235
315
|
console.log(` Available models: ${available.length}`);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (
|
|
316
|
+
if (autoProfile) {
|
|
317
|
+
console.log(` auto (openai) high: ${autoProfile.high.model}`);
|
|
318
|
+
}
|
|
319
|
+
if (opencodeProfile) {
|
|
320
|
+
console.log(` opencode-go high: ${opencodeProfile.high.model}`);
|
|
321
|
+
}
|
|
240
322
|
}
|
|
241
323
|
|
|
242
324
|
main().catch((err) => {
|