ultimate-pi 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +37 -0
  2. package/.agents/skills/harness-governor/SKILL.md +1 -1
  3. package/.agents/skills/harness-orchestration/SKILL.md +54 -0
  4. package/.agents/skills/harness-plan/SKILL.md +4 -3
  5. package/.agents/skills/harness-sentrux-setup/SKILL.md +57 -0
  6. package/.agents/skills/scrapling-web/SKILL.md +93 -0
  7. package/.pi/PACKAGING.md +2 -2
  8. package/.pi/SYSTEM.md +13 -15
  9. package/.pi/agents/harness/adversary.md +3 -0
  10. package/.pi/agents/harness/evaluator.md +3 -0
  11. package/.pi/agents/harness/executor.md +4 -1
  12. package/.pi/agents/harness/meta-optimizer.md +2 -1
  13. package/.pi/agents/harness/planner.md +22 -1
  14. package/.pi/agents/harness/sentrux-bootstrap.md +42 -0
  15. package/.pi/agents/harness/tie-breaker.md +2 -0
  16. package/.pi/extensions/harness-ask-user.ts +74 -0
  17. package/.pi/extensions/harness-subagents.ts +9 -0
  18. package/.pi/extensions/lib/ask-user/dialog.ts +260 -0
  19. package/.pi/extensions/lib/ask-user/fallback.ts +78 -0
  20. package/.pi/extensions/lib/ask-user/render.ts +66 -0
  21. package/.pi/extensions/lib/ask-user/schema.ts +69 -0
  22. package/.pi/extensions/lib/ask-user/types.ts +41 -0
  23. package/.pi/extensions/lib/ask-user/validate-core.mjs +79 -0
  24. package/.pi/extensions/lib/ask-user/validate.ts +92 -0
  25. package/.pi/extensions/lib/harness-subagents/agent-loader.ts +126 -0
  26. package/.pi/extensions/lib/harness-subagents/agent-manifest.ts +119 -0
  27. package/.pi/extensions/lib/harness-subagents/agent-parser.ts +87 -0
  28. package/.pi/extensions/lib/harness-subagents/blackboard-tool.ts +118 -0
  29. package/.pi/extensions/lib/harness-subagents/blackboard.ts +175 -0
  30. package/.pi/extensions/lib/harness-subagents/spawn-policy.ts +27 -0
  31. package/.pi/extensions/lib/harness-subagents/types-blackboard.ts +27 -0
  32. package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +553 -0
  33. package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +637 -0
  34. package/.pi/extensions/lib/harness-subagents/vendored/agent-types.ts +175 -0
  35. package/.pi/extensions/lib/harness-subagents/vendored/context.ts +59 -0
  36. package/.pi/extensions/lib/harness-subagents/vendored/cross-extension-rpc.ts +134 -0
  37. package/.pi/extensions/lib/harness-subagents/vendored/custom-agents.ts +5 -0
  38. package/.pi/extensions/lib/harness-subagents/vendored/default-agents.ts +123 -0
  39. package/.pi/extensions/lib/harness-subagents/vendored/env.ts +43 -0
  40. package/.pi/extensions/lib/harness-subagents/vendored/group-join.ts +144 -0
  41. package/.pi/extensions/lib/harness-subagents/vendored/index.ts +2447 -0
  42. package/.pi/extensions/lib/harness-subagents/vendored/invocation-config.ts +52 -0
  43. package/.pi/extensions/lib/harness-subagents/vendored/memory.ts +182 -0
  44. package/.pi/extensions/lib/harness-subagents/vendored/model-resolver.ts +92 -0
  45. package/.pi/extensions/lib/harness-subagents/vendored/output-file.ts +115 -0
  46. package/.pi/extensions/lib/harness-subagents/vendored/prompts.ts +103 -0
  47. package/.pi/extensions/lib/harness-subagents/vendored/schedule-store.ts +177 -0
  48. package/.pi/extensions/lib/harness-subagents/vendored/schedule.ts +416 -0
  49. package/.pi/extensions/lib/harness-subagents/vendored/settings.ts +210 -0
  50. package/.pi/extensions/lib/harness-subagents/vendored/skill-loader.ts +108 -0
  51. package/.pi/extensions/lib/harness-subagents/vendored/types.ts +187 -0
  52. package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +637 -0
  53. package/.pi/extensions/lib/harness-subagents/vendored/ui/conversation-viewer.ts +324 -0
  54. package/.pi/extensions/lib/harness-subagents/vendored/ui/schedule-menu.ts +110 -0
  55. package/.pi/extensions/lib/harness-subagents/vendored/usage.ts +71 -0
  56. package/.pi/extensions/lib/harness-subagents/vendored/worktree.ts +195 -0
  57. package/.pi/harness/README.md +2 -1
  58. package/.pi/harness/agents.manifest.json +80 -0
  59. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +9 -5
  60. package/.pi/harness/env.harness.template +28 -0
  61. package/.pi/harness/sentrux/architecture.manifest.json +6 -1
  62. package/.pi/prompts/harness-auto.md +2 -2
  63. package/.pi/prompts/harness-plan.md +2 -2
  64. package/.pi/prompts/harness-router-tune.md +2 -2
  65. package/.pi/prompts/harness-run.md +1 -0
  66. package/.pi/prompts/harness-setup.md +178 -339
  67. package/.pi/scripts/README.md +6 -1
  68. package/.pi/scripts/harness-agents-manifest.mjs +123 -0
  69. package/.pi/scripts/harness-cli-verify.sh +60 -11
  70. package/.pi/scripts/harness-generate-model-router.mjs +242 -0
  71. package/.pi/scripts/harness-graphify-bootstrap.sh +1 -6
  72. package/.pi/scripts/harness-resolve-up-pkg.mjs +71 -0
  73. package/.pi/scripts/harness-seed-project-contracts.mjs +33 -1
  74. package/.pi/scripts/harness-sentrux-bootstrap.mjs +146 -0
  75. package/.pi/scripts/harness-sync-env.mjs +148 -0
  76. package/.pi/scripts/harness-verify.mjs +19 -0
  77. package/.pi/scripts/harness-web-search.md +33 -0
  78. package/.pi/scripts/harness-web.py +177 -0
  79. package/.pi/scripts/harness_web/__init__.py +1 -0
  80. package/.pi/scripts/harness_web/config.py +80 -0
  81. package/.pi/scripts/harness_web/output.py +55 -0
  82. package/.pi/scripts/harness_web/scrape.py +120 -0
  83. package/.pi/scripts/harness_web/search_ddg.py +106 -0
  84. package/.pi/scripts/release.sh +338 -0
  85. package/.pi/scripts/sentrux-rules-sync.mjs +29 -7
  86. package/.pi/settings.example.json +0 -1
  87. package/.sentrux/rules.toml +1 -1
  88. package/AGENTS.md +1 -1
  89. package/CHANGELOG.md +12 -0
  90. package/THIRD_PARTY_NOTICES.md +22 -0
  91. package/package.json +12 -9
  92. package/.agents/skills/firecrawl/SKILL.md +0 -150
  93. package/.agents/skills/firecrawl/rules/install.md +0 -82
  94. package/.agents/skills/firecrawl/rules/security.md +0 -26
  95. package/.agents/skills/firecrawl-agent/SKILL.md +0 -57
  96. package/.agents/skills/firecrawl-build-interact/SKILL.md +0 -67
  97. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +0 -102
  98. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +0 -39
  99. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +0 -20
  100. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +0 -17
  101. package/.agents/skills/firecrawl-build-scrape/SKILL.md +0 -68
  102. package/.agents/skills/firecrawl-build-search/SKILL.md +0 -68
  103. package/.agents/skills/firecrawl-crawl/SKILL.md +0 -58
  104. package/.agents/skills/firecrawl-download/SKILL.md +0 -69
  105. package/.agents/skills/firecrawl-interact/SKILL.md +0 -83
  106. package/.agents/skills/firecrawl-map/SKILL.md +0 -50
  107. package/.agents/skills/firecrawl-parse/SKILL.md +0 -61
  108. package/.agents/skills/firecrawl-scrape/SKILL.md +0 -68
  109. package/.agents/skills/firecrawl-search/SKILL.md +0 -59
  110. package/firecrawl/.env.template +0 -62
  111. package/firecrawl/README.md +0 -49
  112. package/firecrawl/docker-compose.yaml +0 -201
  113. package/firecrawl/searxng/searxng.env +0 -3
  114. package/firecrawl/searxng/settings.yml +0 -85
@@ -23,7 +23,12 @@ From **Typescript extensions**, use `resolveHarnessScript()` / `getHarnessPackag
23
23
  | Graphify bootstrap | `bash "$UP_PKG/.pi/scripts/harness-graphify-bootstrap.sh"` |
24
24
  | CLI tool install + smoke tests | `bash "$UP_PKG/.pi/scripts/harness-cli-verify.sh"` |
25
25
  | Deterministic harness checks | `node "$UP_PKG/.pi/scripts/harness-verify.mjs"` |
26
- | Sentrux rules from manifest | `node "$UP_PKG/.pi/scripts/sentrux-rules-sync.mjs" --force` |
26
+ | Sentrux rules bootstrap (harness-setup) | `node "$UP_PKG/.pi/scripts/harness-sentrux-bootstrap.mjs"` |
27
+ | Sentrux rules re-sync after manifest edit | `node "$UP_PKG/.pi/scripts/harness-sentrux-bootstrap.mjs" --force` or `/harness-sentrux-sync` |
28
+ | Sentrux rules drift check (CI) | `node "$UP_PKG/.pi/scripts/sentrux-rules-sync.mjs" --check` |
29
+ | Resolve package root (`UP_PKG`) | `node "$UP_PKG/.pi/scripts/harness-resolve-up-pkg.mjs"` |
30
+ | Model-router config (Pi auth) | `node "$UP_PKG/.pi/scripts/harness-generate-model-router.mjs"` |
31
+ | Project `.env` (append-only) | `node "$UP_PKG/.pi/scripts/harness-sync-env.mjs"` (`--create-missing` after user confirms) |
27
32
  | Model-router / Pi defaults | `harness-sync-model-router.mjs` (Step 3.5 of `/harness-setup`) |
28
33
  | Vendor router sync (this repo only) | `bash .pi/scripts/vendor-sync-pi-model-router.sh` or `npm run vendor:sync-router` |
29
34
  | Meta-optimizer (JSONL proposals) | `node "$UP_PKG/.pi/harness/evolution/meta-optimizer.mjs"` |
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Regenerate or verify .pi/harness/agents.manifest.json from package agents.
4
+ *
5
+ * Usage:
6
+ * node .pi/scripts/harness-agents-manifest.mjs --write
7
+ * node .pi/scripts/harness-agents-manifest.mjs --check
8
+ */
9
+
10
+ import { readFile, writeFile } from "node:fs/promises";
11
+ import { join, dirname } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import {
14
+ isSafeAgentId,
15
+ sha256Content,
16
+ walkAgentsDir,
17
+ } from "../../test/harness-subagents-loader.core.mjs";
18
+
19
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
20
+ const MANIFEST_PATH = join(ROOT, ".pi", "harness", "agents.manifest.json");
21
+ const PACKAGE_AGENTS = join(ROOT, ".pi", "agents");
22
+
23
+ async function readPackageMeta() {
24
+ const pkg = JSON.parse(
25
+ await readFile(join(ROOT, "package.json"), "utf-8"),
26
+ );
27
+ return { name: pkg.name ?? "ultimate-pi", version: pkg.version ?? "0.0.0" };
28
+ }
29
+
30
+ function buildManifest(packageFiles, packageName, packageVersion) {
31
+ const agents = {};
32
+ for (const f of packageFiles.values()) {
33
+ agents[f.id] = {
34
+ path: `.pi/agents/${f.id}.md`,
35
+ sha256: sha256Content(f.content),
36
+ };
37
+ }
38
+ return {
39
+ schema_version: "1.0.0",
40
+ package: packageName,
41
+ package_version: packageVersion,
42
+ generated_at: new Date().toISOString(),
43
+ agents,
44
+ };
45
+ }
46
+
47
+ function getDriftReport(manifest, packageFiles) {
48
+ const items = [];
49
+ if (!manifest) {
50
+ return { ok: false, items: [{ id: "*", kind: "missing_on_disk" }] };
51
+ }
52
+ for (const [id, file] of packageFiles) {
53
+ const expected = manifest.agents[id];
54
+ const actual = sha256Content(file.content);
55
+ if (!expected) {
56
+ items.push({ id, kind: "missing_in_manifest" });
57
+ continue;
58
+ }
59
+ if (expected.sha256 !== actual) {
60
+ items.push({ id, kind: "hash_mismatch", expected: expected.sha256, actual });
61
+ }
62
+ }
63
+ for (const id of Object.keys(manifest.agents)) {
64
+ if (!packageFiles.has(id)) {
65
+ items.push({ id, kind: "missing_on_disk" });
66
+ }
67
+ }
68
+ return { ok: items.length === 0, items };
69
+ }
70
+
71
+ async function loadPackageFiles() {
72
+ const files = new Map();
73
+ walkAgentsDir(PACKAGE_AGENTS, "package", files);
74
+ for (const id of files.keys()) {
75
+ if (!isSafeAgentId(id)) files.delete(id);
76
+ }
77
+ return files;
78
+ }
79
+
80
+ async function main() {
81
+ const mode = process.argv.includes("--check") ? "check" : "write";
82
+ const { name, version } = await readPackageMeta();
83
+ const packageFiles = await loadPackageFiles();
84
+ const built = buildManifest(packageFiles, name, version);
85
+
86
+ if (mode === "write") {
87
+ await writeFile(MANIFEST_PATH, `${JSON.stringify(built, null, 2)}\n`, "utf-8");
88
+ console.log(
89
+ `Wrote ${MANIFEST_PATH} (${Object.keys(built.agents).length} agents)`,
90
+ );
91
+ return;
92
+ }
93
+
94
+ let onDisk;
95
+ try {
96
+ onDisk = JSON.parse(await readFile(MANIFEST_PATH, "utf-8"));
97
+ } catch {
98
+ console.error("agents.manifest.json missing — run with --write");
99
+ process.exit(1);
100
+ }
101
+
102
+ const drift = getDriftReport(onDisk, packageFiles);
103
+ if (!drift.ok) {
104
+ for (const item of drift.items) {
105
+ console.error(`drift: ${item.id} (${item.kind})`);
106
+ }
107
+ process.exit(1);
108
+ }
109
+
110
+ if (onDisk.package_version !== version) {
111
+ console.error(
112
+ `package_version mismatch: manifest=${onDisk.package_version} package=${version}`,
113
+ );
114
+ process.exit(1);
115
+ }
116
+
117
+ console.log(`agents.manifest.json OK (${Object.keys(built.agents).length} agents)`);
118
+ }
119
+
120
+ main().catch((err) => {
121
+ console.error(err);
122
+ process.exit(1);
123
+ });
@@ -153,13 +153,62 @@ verify_agent_browser() {
153
153
  fi
154
154
  }
155
155
 
156
- verify_firecrawl() {
157
- log "[firecrawl-cli]"
158
- npm_global_install "firecrawl-cli@latest" "firecrawl" || { fail "firecrawl-cli npm install"; return; }
159
- if firecrawl --status &>/dev/null; then
160
- pass "firecrawl $(firecrawl --status 2>/dev/null | head -1 || echo ok)"
156
+ scrapling_installed() {
157
+ command -v scrapling &>/dev/null && return 0
158
+ command -v uv &>/dev/null && uv tool list 2>/dev/null | grep -qE '(^|[[:space:]])scrapling([[:space:]]|$)' && return 0
159
+ return 1
160
+ }
161
+
162
+ install_scrapling() {
163
+ if command -v uv &>/dev/null; then
164
+ log " installing scrapling via uv tool..."
165
+ uv tool install "scrapling[fetchers]" || return 1
166
+ elif command -v pip3 &>/dev/null; then
167
+ log " installing scrapling via pip3 --user..."
168
+ pip3 install --user "scrapling[fetchers]" || return 1
169
+ elif command -v pip &>/dev/null; then
170
+ pip install --user "scrapling[fetchers]" || return 1
171
+ else
172
+ fail "need uv or pip to install scrapling"
173
+ return 1
174
+ fi
175
+ export PATH="${HOME}/.local/bin:${PATH}"
176
+ return 0
177
+ }
178
+
179
+ verify_scrapling() {
180
+ log "[scrapling / harness-web]"
181
+ ensure_linux_browser_deps 2>/dev/null || true
182
+ if [ "$FORCE" = true ] || ! scrapling_installed; then
183
+ install_scrapling || { fail 'scrapling install (run: uv tool install "scrapling[fetchers]")'; return; }
184
+ fi
185
+ if ! scrapling_installed; then
186
+ fail "scrapling not on PATH after install"
187
+ return
188
+ fi
189
+ if ! scrapling --help &>/dev/null; then
190
+ fail "scrapling --help failed"
191
+ return
192
+ fi
193
+ pass "scrapling CLI"
194
+ if [ "$FORCE" = true ]; then
195
+ scrapling install 2>/dev/null || warn "scrapling install (browsers) failed — use harness-web scrape --fast for smoke"
196
+ fi
197
+ _hw="${ROOT}/.pi/scripts/harness-web.py"
198
+ if [ ! -f "$_hw" ]; then
199
+ warn "harness-web.py missing in package"
200
+ return
201
+ fi
202
+ mkdir -p .web
203
+ if python3 "$_hw" search "ultimate-pi harness" -o .web/verify-search.json --limit 2 2>/dev/null | grep -q wrote; then
204
+ pass "harness-web search smoke"
205
+ else
206
+ fail "harness-web search smoke failed"
207
+ fi
208
+ if python3 "$_hw" scrape "https://example.com" -o .web/verify-page.md --fast 2>/dev/null | grep -q wrote; then
209
+ pass "harness-web scrape --fast smoke"
161
210
  else
162
- fail "firecrawl --status failed (run: firecrawl login)"
211
+ warn "harness-web scrape smoke failed (stealth needs: scrapling install + OS browser libs)"
163
212
  fi
164
213
  }
165
214
 
@@ -259,10 +308,10 @@ verify_sentrux() {
259
308
  return
260
309
  fi
261
310
  sentrux plugin add-standard 2>/dev/null || warn "sentrux plugin add-standard skipped"
262
- _sync_script="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/sentrux-rules-sync.mjs"
263
- if [ -f "$_sync_script" ]; then
264
- node "$_sync_script" --force 2>/dev/null ||
265
- warn "sentrux rules sync failed (see .pi/scripts/README.md)"
311
+ _bootstrap="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/harness-sentrux-bootstrap.mjs"
312
+ if [ -f "$_bootstrap" ]; then
313
+ node "$_bootstrap" --force 2>/dev/null ||
314
+ warn "sentrux rules bootstrap failed (see harness-sentrux-setup skill)"
266
315
  fi
267
316
  if sentrux check . &>/dev/null; then
268
317
  pass "sentrux $(sentrux --version 2>/dev/null | head -1)"
@@ -274,7 +323,7 @@ verify_sentrux() {
274
323
  log "Harness CLI verification (cwd: $ROOT)"
275
324
  log ""
276
325
 
277
- verify_firecrawl
326
+ verify_scrapling
278
327
  verify_ctx7
279
328
  verify_agent_browser
280
329
  verify_ck
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate `.pi/model-router.json` from Pi's authenticated providers (auth.json + env),
4
+ * not from raw env-var heuristics alone.
5
+ *
6
+ * Uses @mariozechner/pi-coding-agent ModelRegistry.getAvailable() — same source as /login.
7
+ *
8
+ * Usage: node harness-generate-model-router.mjs [--force] [--dry-run]
9
+ * --force overwrite existing .pi/model-router.json
10
+ * --dry-run print JSON to stdout, do not write
11
+ *
12
+ * Requires @mariozechner/pi-coding-agent (peer of ultimate-pi; bundled with pi).
13
+ */
14
+
15
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
16
+ import { createRequire } from "node:module";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath, pathToFileURL } from "node:url";
19
+
20
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
21
+ const UP_PKG = join(SCRIPT_DIR, "..", "..");
22
+ const OUT_PATH = join(process.cwd(), ".pi", "model-router.json");
23
+
24
+ const PROVIDER_PRIORITY = [
25
+ "opencode-go",
26
+ "anthropic",
27
+ "openai",
28
+ "google",
29
+ "openrouter",
30
+ "groq",
31
+ "mistral",
32
+ "amazon",
33
+ ];
34
+
35
+ /** Substring hints per tier (first match in available ids wins). */
36
+ const TIER_HINTS = {
37
+ high: [
38
+ "deepseek-v4-pro",
39
+ "gpt-5.4-pro",
40
+ "claude-opus",
41
+ "sonnet-4",
42
+ "gemini-2.5-pro",
43
+ "pro",
44
+ ],
45
+ medium: [
46
+ "qwen3.6-plus",
47
+ "kimi-k2.6",
48
+ "gpt-5.4",
49
+ "claude-sonnet",
50
+ "gemini-flash",
51
+ "plus",
52
+ ],
53
+ low: [
54
+ "deepseek-v4-flash",
55
+ "gpt-5.4-nano",
56
+ "haiku",
57
+ "flash-lite",
58
+ "flash",
59
+ "mini",
60
+ ],
61
+ };
62
+
63
+ function fail(msg) {
64
+ console.error(`harness-generate-model-router: ${msg}`);
65
+ process.exit(1);
66
+ }
67
+
68
+ async function loadPiCodingAgent() {
69
+ const agentRoots = [
70
+ join(UP_PKG, "node_modules", "@mariozechner", "pi-coding-agent"),
71
+ join(UP_PKG, ".pi", "npm", "node_modules", "@mariozechner", "pi-coding-agent"),
72
+ ];
73
+ for (const root of agentRoots) {
74
+ const entry = join(root, "dist", "index.js");
75
+ if (existsSync(entry)) {
76
+ return import(pathToFileURL(entry).href);
77
+ }
78
+ }
79
+ for (const base of [UP_PKG, process.cwd()]) {
80
+ try {
81
+ const req = createRequire(join(base, "package.json"));
82
+ return req("@mariozechner/pi-coding-agent");
83
+ } catch {
84
+ /* try next */
85
+ }
86
+ }
87
+ fail(
88
+ "@mariozechner/pi-coding-agent not found (install pi or npm i in ultimate-pi). Peer: @mariozechner/pi-coding-agent",
89
+ );
90
+ }
91
+
92
+ function canonicalRef(provider, modelId) {
93
+ return `${provider}/${modelId}`;
94
+ }
95
+
96
+ function pickTierModel(models, tier) {
97
+ const hints = TIER_HINTS[tier];
98
+ const ids = models.map((m) => m.id);
99
+ for (const hint of hints) {
100
+ const match = models.find((m) => m.id.includes(hint));
101
+ if (match) return canonicalRef(match.provider, match.id);
102
+ }
103
+ if (models.length === 0) return null;
104
+ if (tier === "high") {
105
+ const reasoning = models.find((m) => m.reasoning);
106
+ if (reasoning) return canonicalRef(reasoning.provider, reasoning.id);
107
+ }
108
+ if (tier === "low") {
109
+ return canonicalRef(models[models.length - 1].provider, models[models.length - 1].id);
110
+ }
111
+ return canonicalRef(models[0].provider, models[0].id);
112
+ }
113
+
114
+ function choosePrimaryProvider(available) {
115
+ const byProvider = new Map();
116
+ for (const m of available) {
117
+ if (!byProvider.has(m.provider)) byProvider.set(m.provider, []);
118
+ byProvider.get(m.provider).push(m);
119
+ }
120
+ for (const p of PROVIDER_PRIORITY) {
121
+ if (byProvider.has(p)) return { provider: p, models: byProvider.get(p) };
122
+ }
123
+ const first = [...byProvider.keys()].sort()[0];
124
+ return { provider: first, models: byProvider.get(first) ?? [] };
125
+ }
126
+
127
+ function buildFallbacks(available, primaryProvider, highModel) {
128
+ const fallbacks = [];
129
+ for (const p of ["anthropic", "google", "openai"]) {
130
+ if (p === primaryProvider) continue;
131
+ const alt = available.filter((m) => m.provider === p);
132
+ if (alt.length === 0) continue;
133
+ const ref = pickTierModel(alt, "medium");
134
+ if (ref && ref !== highModel) fallbacks.push(ref);
135
+ }
136
+ return fallbacks.slice(0, 3);
137
+ }
138
+
139
+ async function main() {
140
+ const force = process.argv.includes("--force");
141
+ const dryRun = process.argv.includes("--dry-run");
142
+
143
+ if (existsSync(OUT_PATH) && !force) {
144
+ console.log(
145
+ "✓ .pi/model-router.json already exists — preserving (use --force to regenerate)",
146
+ );
147
+ process.exit(0);
148
+ }
149
+
150
+ const { AuthStorage, ModelRegistry } = await loadPiCodingAgent();
151
+ const authStorage = AuthStorage.create();
152
+ const modelRegistry = ModelRegistry.create(authStorage);
153
+ const available = await modelRegistry.getAvailable();
154
+
155
+ if (available.length === 0) {
156
+ console.log(
157
+ "✗ No authenticated Pi providers — skip model-router.json",
158
+ );
159
+ console.log(
160
+ " Log in inside pi: /login (or set API keys in ~/.pi/agent/auth.json)",
161
+ );
162
+ const providers = authStorage.list();
163
+ if (providers.length > 0) {
164
+ console.log(
165
+ ` Stored providers in auth.json (may need refresh): ${providers.join(", ")}`,
166
+ );
167
+ }
168
+ process.exit(0);
169
+ }
170
+
171
+ const { provider: primaryProvider, models: primaryModels } =
172
+ choosePrimaryProvider(available);
173
+
174
+ const highModel = pickTierModel(primaryModels, "high");
175
+ const mediumModel = pickTierModel(primaryModels, "medium");
176
+ const lowModel = pickTierModel(primaryModels, "low");
177
+
178
+ if (!highModel || !mediumModel || !lowModel) {
179
+ fail("could not assign tier models from available registry");
180
+ }
181
+
182
+ const fallbacks = buildFallbacks(available, primaryProvider, highModel);
183
+
184
+ const config = {
185
+ defaultProfile: "auto",
186
+ debug: false,
187
+ classifierModel: mediumModel,
188
+ phaseBias: 0.5,
189
+ maxSessionBudget: 1.0,
190
+ largeContextThreshold: 100000,
191
+ rules: [
192
+ {
193
+ matches: ["deploy", "production", "release"],
194
+ tier: "high",
195
+ reason: "Safety check for production tasks",
196
+ },
197
+ { matches: "changelog", tier: "low" },
198
+ ],
199
+ profiles: {
200
+ auto: {
201
+ high: { model: highModel, thinking: "high", fallbacks },
202
+ medium: { model: mediumModel, thinking: "medium" },
203
+ low: { model: lowModel, thinking: "low" },
204
+ },
205
+ cheap: {
206
+ high: { model: mediumModel, thinking: "low" },
207
+ medium: { model: lowModel, thinking: "off" },
208
+ low: { model: lowModel, thinking: "off" },
209
+ },
210
+ deep: {
211
+ high: { model: highModel, thinking: "xhigh", fallbacks },
212
+ medium: { model: mediumModel, thinking: "medium" },
213
+ low: { model: lowModel, thinking: "low" },
214
+ },
215
+ },
216
+ };
217
+
218
+ const json = `${JSON.stringify(config, null, 2)}\n`;
219
+ const providerSet = [...new Set(available.map((m) => m.provider))].sort();
220
+
221
+ if (dryRun) {
222
+ process.stdout.write(json);
223
+ process.exit(0);
224
+ }
225
+
226
+ mkdirSync(dirname(OUT_PATH), { recursive: true });
227
+ writeFileSync(OUT_PATH, json, "utf8");
228
+
229
+ console.log("✓ Generated .pi/model-router.json from Pi authenticated providers:");
230
+ console.log(` Primary provider: ${primaryProvider}`);
231
+ console.log(` Authenticated providers: ${providerSet.join(", ")}`);
232
+ console.log(` Available models: ${available.length}`);
233
+ console.log(` High tier: ${highModel}`);
234
+ console.log(` Medium tier: ${mediumModel}`);
235
+ console.log(` Low tier: ${lowModel}`);
236
+ if (fallbacks.length) console.log(` Fallbacks: ${fallbacks.join(", ")}`);
237
+ }
238
+
239
+ main().catch((err) => {
240
+ console.error(err);
241
+ process.exit(1);
242
+ });
@@ -58,13 +58,8 @@ install_graphify() {
58
58
  }
59
59
 
60
60
  graphify_platform_install() {
61
+ # Pi harness only — no codex/cursor installs (codex writes .codex/hooks.json).
61
62
  graphify install --platform pi 2>/dev/null || graphify pi install 2>/dev/null || true
62
- if [ -d .cursor ]; then
63
- graphify cursor install 2>/dev/null || true
64
- fi
65
- if [ -f AGENTS.md ] || [ -d .pi ]; then
66
- graphify codex install 2>/dev/null || true
67
- fi
68
63
  }
69
64
 
70
65
  graph_is_valid() {
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Print absolute path to the installed ultimate-pi package root (UP_PKG).
4
+ * Used by /harness-setup and shell scripts in external repos.
5
+ *
6
+ * Resolution order:
7
+ * 1. ULTIMATE_PI_PKG env override
8
+ * 2. require.resolve('ultimate-pi/package.json') from cwd
9
+ * 3. Global npm prefix: $(npm root -g)/ultimate-pi
10
+ * 4. Script location (this file ships inside the package)
11
+ *
12
+ * Exit 0 and prints path; exit 1 if not found.
13
+ */
14
+
15
+ import { createRequire } from "node:module";
16
+ import { existsSync } from "node:fs";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { execSync } from "node:child_process";
20
+
21
+ const requireFromCwd = createRequire(join(process.cwd(), "package.json"));
22
+
23
+ const SCRIPT_UP_PKG = join(
24
+ dirname(fileURLToPath(import.meta.url)),
25
+ "..",
26
+ "..",
27
+ );
28
+
29
+ function hasHarnessScripts(root) {
30
+ return existsSync(join(root, ".pi", "scripts", "harness-cli-verify.sh"));
31
+ }
32
+
33
+ function tryResolveUltimatePi() {
34
+ if (process.env.ULTIMATE_PI_PKG) {
35
+ const envRoot = process.env.ULTIMATE_PI_PKG;
36
+ if (hasHarnessScripts(envRoot)) return envRoot;
37
+ }
38
+
39
+ try {
40
+ const pkg = requireFromCwd.resolve("ultimate-pi/package.json");
41
+ const root = dirname(pkg);
42
+ if (hasHarnessScripts(root)) return root;
43
+ } catch {
44
+ /* continue */
45
+ }
46
+
47
+ try {
48
+ const globalRoot = execSync("npm root -g", {
49
+ encoding: "utf8",
50
+ stdio: ["ignore", "pipe", "ignore"],
51
+ }).trim();
52
+ const globalPkg = join(globalRoot, "ultimate-pi");
53
+ if (hasHarnessScripts(globalPkg)) return globalPkg;
54
+ } catch {
55
+ /* continue */
56
+ }
57
+
58
+ if (hasHarnessScripts(SCRIPT_UP_PKG)) return SCRIPT_UP_PKG;
59
+
60
+ return null;
61
+ }
62
+
63
+ const root = tryResolveUltimatePi();
64
+ if (!root) {
65
+ console.error(
66
+ "harness-resolve-up-pkg: ultimate-pi not found. Install: pi install npm:ultimate-pi (or npm i -g ultimate-pi)",
67
+ );
68
+ process.exit(1);
69
+ }
70
+
71
+ process.stdout.write(root);
@@ -11,16 +11,40 @@
11
11
  * (the script always lives under the shipped ultimate-pi package).
12
12
  */
13
13
 
14
- import { copyFile, mkdir, readdir } from "node:fs/promises";
14
+ import { copyFile, mkdir, readdir, access } from "node:fs/promises";
15
+ import { constants } from "node:fs";
15
16
  import { join, dirname } from "node:path";
16
17
  import { fileURLToPath } from "node:url";
17
18
 
18
19
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
19
20
  const UP_PKG = join(SCRIPT_DIR, "..", "..");
20
21
  const SPEC_SRC = join(UP_PKG, ".pi", "harness", "specs");
22
+ const SENTRUX_TEMPLATE = join(
23
+ UP_PKG,
24
+ ".pi",
25
+ "harness",
26
+ "sentrux",
27
+ "architecture.manifest.json",
28
+ );
21
29
 
22
30
  const projectRoot = process.argv[2] || process.cwd();
23
31
  const specDest = join(projectRoot, ".pi", "harness", "specs");
32
+ const sentruxDest = join(
33
+ projectRoot,
34
+ ".pi",
35
+ "harness",
36
+ "sentrux",
37
+ "architecture.manifest.json",
38
+ );
39
+
40
+ async function fileExists(path) {
41
+ try {
42
+ await access(path, constants.R_OK);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
24
48
 
25
49
  async function main() {
26
50
  const names = await readdir(SPEC_SRC);
@@ -41,6 +65,14 @@ async function main() {
41
65
  console.log(
42
66
  `harness-seed-project-contracts: copied ${toCopy.length} file(s) -> ${specDest}`,
43
67
  );
68
+
69
+ if (!(await fileExists(sentruxDest)) && (await fileExists(SENTRUX_TEMPLATE))) {
70
+ await mkdir(dirname(sentruxDest), { recursive: true });
71
+ await copyFile(SENTRUX_TEMPLATE, sentruxDest);
72
+ console.log(
73
+ `harness-seed-project-contracts: seeded Sentrux manifest -> ${sentruxDest} (run harness-sentrux-bootstrap.mjs to sync rules.toml)`,
74
+ );
75
+ }
44
76
  }
45
77
 
46
78
  main().catch((err) => {