@piagent/platform 1.1.9 → 1.2.1

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 (81) hide show
  1. package/CHANGELOG.md +175 -1
  2. package/README.md +2 -2
  3. package/docs/command-reference-vietnamese.md +19 -5
  4. package/docs/distribution-standard.md +7 -7
  5. package/docs/mcp-and-tools.md +154 -2
  6. package/docs/model-options.md +10 -8
  7. package/docs/operator-manual-vietnamese.md +25 -13
  8. package/docs/publishing-for-teams.md +2 -0
  9. package/docs/quickstart-vietnamese.md +2 -2
  10. package/docs/release-install-policy.md +6 -6
  11. package/docs/security-threat-model.md +4 -3
  12. package/docs/team-onboarding.md +4 -4
  13. package/docs/usage-observability.md +29 -0
  14. package/docs/vercel-docs-site.md +34 -4
  15. package/docs-site/architecture.html +152 -0
  16. package/docs-site/assets/docs.css +1001 -0
  17. package/docs-site/assets/docs.js +84 -0
  18. package/docs-site/commands.html +203 -0
  19. package/docs-site/content/architecture.html +29 -0
  20. package/docs-site/content/commands.html +80 -0
  21. package/docs-site/content/index.html +80 -0
  22. package/docs-site/content/mcp-approval.html +133 -0
  23. package/docs-site/content/mcp-auth.html +76 -0
  24. package/docs-site/content/mcp-servers.html +111 -0
  25. package/docs-site/content/mcp.html +120 -0
  26. package/docs-site/content/permissions.html +54 -0
  27. package/docs-site/content/profiles.html +68 -0
  28. package/docs-site/content/quickstart.html +144 -0
  29. package/docs-site/content/runtime.html +45 -0
  30. package/docs-site/content/scope.html +71 -0
  31. package/docs-site/content/security.html +51 -0
  32. package/docs-site/content/team.html +23 -0
  33. package/docs-site/content/workflows.html +47 -0
  34. package/docs-site/index.html +131 -1534
  35. package/docs-site/mcp-approval.html +256 -0
  36. package/docs-site/mcp-auth.html +199 -0
  37. package/docs-site/mcp-servers.html +234 -0
  38. package/docs-site/mcp.html +243 -0
  39. package/docs-site/permissions.html +177 -0
  40. package/docs-site/profiles.html +191 -0
  41. package/docs-site/quickstart.html +267 -0
  42. package/docs-site/runtime.html +168 -0
  43. package/docs-site/scope.html +194 -0
  44. package/docs-site/security.html +174 -0
  45. package/docs-site/team.html +142 -0
  46. package/docs-site/workflows.html +170 -0
  47. package/package.json +4 -2
  48. package/packages/piagent-core/capabilities/capability-core.js +58 -5
  49. package/packages/piagent-core/capabilities/capability-sources.js +73 -12
  50. package/packages/piagent-core/extensions/document-intake.ts +5 -5
  51. package/packages/piagent-core/extensions/piagent-guard.ts +432 -2
  52. package/packages/piagent-core/extensions/policy-core.js +1586 -98
  53. package/packages/piagent-core/mcp/mcp-approval-store.js +216 -0
  54. package/packages/piagent-core/mcp/mcp-auth-readiness.js +225 -0
  55. package/packages/piagent-core/mcp/mcp-command-actions.js +400 -0
  56. package/packages/piagent-core/mcp/mcp-config-layers.js +466 -0
  57. package/packages/piagent-core/mcp/mcp-import-kinds.js +240 -0
  58. package/packages/piagent-core/mcp/mcp-server-catalog.js +150 -0
  59. package/packages/piagent-core/mcp/mcp-server-entry.js +293 -0
  60. package/packages/piagent-core/mcp/mcp-session-view.js +236 -0
  61. package/packages/piagent-core/mcp/mcp-tool-naming.js +90 -0
  62. package/packages/piagent-core/package.json +7 -3
  63. package/packages/piagent-core/prompts/model-options.md +8 -8
  64. package/scripts/build-docs-site.mjs +421 -0
  65. package/scripts/configure-model-scope.sh +21 -5
  66. package/scripts/configure-subagents.sh +18 -5
  67. package/scripts/init-project.sh +15 -8
  68. package/scripts/install-global.sh +5 -5
  69. package/scripts/mcp-manage.mjs +656 -0
  70. package/scripts/pi-model-catalog.sh +11 -1
  71. package/scripts/pi-session-stats.sh +27 -1
  72. package/scripts/pi-usage-history.mjs +567 -0
  73. package/scripts/piagent-cli.mjs +1 -1
  74. package/scripts/preview-docs-site.mjs +88 -0
  75. package/scripts/quality-benchmark.sh +37 -9
  76. package/scripts/setup.sh +23 -15
  77. package/scripts/team-doctor.sh +18 -2
  78. package/scripts/uninstall-global.sh +10 -4
  79. package/scripts/verify-local.sh +65 -19
  80. package/scripts/verify-release-identity.mjs +37 -2
  81. package/scripts/configure-mcp.sh +0 -349
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import http from "node:http";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ // Serves docs-site/ locally the way Vercel serves it.
8
+ //
9
+ // The deployed site runs with `cleanUrls: true`, so every internal link points
10
+ // at /quickstart rather than /quickstart.html. A plain static file server
11
+ // answers 404 for all of them, which makes a local preview useless for
12
+ // checking navigation. This resolves the extensionless path the same way.
13
+
14
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+ const siteRoot = path.join(repoRoot, "docs-site");
16
+
17
+ const CONTENT_TYPES = {
18
+ ".css": "text/css; charset=utf-8",
19
+ ".html": "text/html; charset=utf-8",
20
+ ".ico": "image/x-icon",
21
+ ".js": "text/javascript; charset=utf-8",
22
+ ".json": "application/json; charset=utf-8",
23
+ ".png": "image/png",
24
+ ".svg": "image/svg+xml",
25
+ ".txt": "text/plain; charset=utf-8",
26
+ ".webmanifest": "application/manifest+json"
27
+ };
28
+
29
+ function portFromArgs(argv) {
30
+ const flag = argv.indexOf("--port");
31
+ const raw = flag >= 0 ? argv[flag + 1] : process.env.PORT;
32
+ if (!raw) return 4321;
33
+ const port = Number.parseInt(raw, 10);
34
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
35
+ throw new Error(`invalid port: ${raw}`);
36
+ }
37
+ return port;
38
+ }
39
+
40
+ /** Resolve a request path to a file inside the site root, or undefined. */
41
+ function resolveFile(urlPath) {
42
+ const requested = urlPath === "/" ? "/index.html" : urlPath;
43
+ const candidate = path.resolve(siteRoot, `.${requested}`);
44
+ // A traversal such as /../../.ssh/id_rsa escapes the site root; refuse it
45
+ // rather than reading whatever it lands on.
46
+ if (candidate !== siteRoot && !candidate.startsWith(siteRoot + path.sep)) return undefined;
47
+ for (const file of [candidate, `${candidate}.html`, path.join(candidate, "index.html")]) {
48
+ if (fs.existsSync(file) && fs.statSync(file).isFile()) return file;
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ const port = portFromArgs(process.argv.slice(2));
54
+
55
+ const server = http.createServer((req, res) => {
56
+ if (req.method !== "GET" && req.method !== "HEAD") {
57
+ res.writeHead(405, { allow: "GET, HEAD" });
58
+ res.end();
59
+ return;
60
+ }
61
+ let urlPath;
62
+ try {
63
+ urlPath = decodeURIComponent(new URL(req.url, `http://localhost:${port}`).pathname);
64
+ } catch {
65
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
66
+ res.end("bad request\n");
67
+ return;
68
+ }
69
+ const file = resolveFile(urlPath);
70
+ if (!file) {
71
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
72
+ res.end(`not found: ${urlPath}\n`);
73
+ return;
74
+ }
75
+ const body = fs.readFileSync(file);
76
+ res.writeHead(200, {
77
+ "content-type": CONTENT_TYPES[path.extname(file)] ?? "application/octet-stream",
78
+ "content-length": body.length,
79
+ "cache-control": "no-store"
80
+ });
81
+ res.end(req.method === "HEAD" ? undefined : body);
82
+ });
83
+
84
+ // Loopback only: this serves local files and has no business being reachable
85
+ // from the network.
86
+ server.listen(port, "127.0.0.1", () => {
87
+ process.stdout.write(`docs-site preview on http://localhost:${port} (Ctrl+C to stop)\n`);
88
+ });
@@ -31,6 +31,14 @@ if [[ -z "$PROJECT_PATH" ]]; then
31
31
  usage
32
32
  exit 2
33
33
  fi
34
+ # The project path is positional and comes first, so omitting it hands the first
35
+ # flag to `cd` and the run dies somewhere further down complaining about an
36
+ # option. Say what is actually wrong instead.
37
+ if [[ "$PROJECT_PATH" == -* ]]; then
38
+ echo "FAIL: the project path comes first, before any flag; received: $PROJECT_PATH" >&2
39
+ usage >&2
40
+ exit 2
41
+ fi
34
42
  shift || true
35
43
 
36
44
  PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
@@ -45,6 +53,17 @@ COST=""
45
53
  DURATION=""
46
54
  NOTES=""
47
55
 
56
+ # A flag left without a value otherwise consumes the next flag as its value, and
57
+ # the run continues on a record that says something nobody typed.
58
+ require_value() {
59
+ local option="$1"
60
+ local value="${2:-}"
61
+ if [[ -z "$value" || "$value" == --* ]]; then
62
+ echo "Missing value for $option" >&2
63
+ exit 2
64
+ fi
65
+ }
66
+
48
67
  while [[ $# -gt 0 ]]; do
49
68
  case "$1" in
50
69
  --init)
@@ -56,39 +75,48 @@ while [[ $# -gt 0 ]]; do
56
75
  shift
57
76
  ;;
58
77
  --scenario)
59
- SCENARIO="${2:-}"
78
+ require_value "$1" "${2:-}"
79
+ SCENARIO="$2"
60
80
  shift 2
61
81
  ;;
62
82
  --surface|--agent)
63
- SURFACE="${2:-}"
83
+ require_value "$1" "${2:-}"
84
+ SURFACE="$2"
64
85
  shift 2
65
86
  ;;
66
87
  --result)
67
- RESULT="${2:-}"
88
+ require_value "$1" "${2:-}"
89
+ RESULT="$2"
68
90
  shift 2
69
91
  ;;
70
92
  --task-file)
71
- TASK_FILE="${2:-}"
93
+ require_value "$1" "${2:-}"
94
+ TASK_FILE="$2"
72
95
  shift 2
73
96
  ;;
74
97
  --verify)
75
- VERIFY_CMD="${2:-}"
98
+ require_value "$1" "${2:-}"
99
+ VERIFY_CMD="$2"
76
100
  shift 2
77
101
  ;;
78
102
  --tokens)
79
- TOKENS="${2:-}"
103
+ require_value "$1" "${2:-}"
104
+ TOKENS="$2"
80
105
  shift 2
81
106
  ;;
82
107
  --cost)
83
- COST="${2:-}"
108
+ require_value "$1" "${2:-}"
109
+ COST="$2"
84
110
  shift 2
85
111
  ;;
86
112
  --duration)
87
- DURATION="${2:-}"
113
+ require_value "$1" "${2:-}"
114
+ DURATION="$2"
88
115
  shift 2
89
116
  ;;
90
117
  --notes)
91
- NOTES="${2:-}"
118
+ require_value "$1" "${2:-}"
119
+ NOTES="$2"
92
120
  shift 2
93
121
  ;;
94
122
  -h|--help)
package/scripts/setup.sh CHANGED
@@ -28,11 +28,11 @@ Options:
28
28
  Install/skip pi-subagents during global install (default: install)
29
29
  --subagents-preset <minimal|safe|async|parallel>
30
30
  Configure pi-subagents runtime baseline (default: safe)
31
- --subagents-model-scope <none|piagent|codex|claude>
31
+ --subagents-model-scope <none|piagent|codex|claude> model family for subagents
32
32
  Optional subagent model allowlist (default: none)
33
33
  --with-web-access Install pi-web-access for builtin `researcher` subagent (default: skip)
34
34
  --with-herdr / --no-herdr Install/skip Herdr Pi integration if herdr exists (default: install)
35
- --model-scope <full|codex|claude>
35
+ --model-scope <full|codex|claude> model families to enable
36
36
  Configure Pi enabledModels for selector/cycling (default: full)
37
37
  --default-model <provider/model[:thinking]>
38
38
  Configure default provider/model/thinking (default: openai-codex/gpt-5.5:xhigh)
@@ -85,21 +85,24 @@ SKIP_AGENTS=false
85
85
  SKIP_REVIEW=false
86
86
  DRY_RUN=false
87
87
 
88
+ require_value() {
89
+ local option="$1"
90
+ local value="${2:-}"
91
+ if [[ -z "$value" || "$value" == --* ]]; then
92
+ echo "Missing value for $option" >&2
93
+ exit 2
94
+ fi
95
+ }
96
+
88
97
  while [[ $# -gt 0 ]]; do
89
98
  case "$1" in
90
99
  --profile)
91
- if [[ $# -lt 2 ]]; then
92
- echo "Missing value for --profile" >&2
93
- exit 2
94
- fi
100
+ require_value "$1" "${2:-}"
95
101
  PROFILE_INPUT="$2"
96
102
  shift 2
97
103
  ;;
98
104
  --package-source)
99
- if [[ $# -lt 2 ]]; then
100
- echo "Missing value for --package-source" >&2
101
- exit 2
102
- fi
105
+ require_value "$1" "${2:-}"
103
106
  PACKAGE_SOURCE="$2"
104
107
  shift 2
105
108
  ;;
@@ -124,7 +127,8 @@ while [[ $# -gt 0 ]]; do
124
127
  shift
125
128
  ;;
126
129
  --mcp-preset)
127
- MCP_PRESET="${2:-}"
130
+ require_value "$1" "${2:-}"
131
+ MCP_PRESET="$2"
128
132
  MCP_PRESET_EXPLICIT=true
129
133
  shift 2
130
134
  ;;
@@ -137,11 +141,13 @@ while [[ $# -gt 0 ]]; do
137
141
  shift
138
142
  ;;
139
143
  --subagents-preset)
140
- SUBAGENTS_PRESET="${2:-}"
144
+ require_value "$1" "${2:-}"
145
+ SUBAGENTS_PRESET="$2"
141
146
  shift 2
142
147
  ;;
143
148
  --subagents-model-scope)
144
- SUBAGENTS_MODEL_SCOPE="${2:-}"
149
+ require_value "$1" "${2:-}"
150
+ SUBAGENTS_MODEL_SCOPE="$2"
145
151
  shift 2
146
152
  ;;
147
153
  --with-web-access)
@@ -157,11 +163,13 @@ while [[ $# -gt 0 ]]; do
157
163
  shift
158
164
  ;;
159
165
  --model-scope)
160
- MODEL_SCOPE_PRESET="${2:-}"
166
+ require_value "$1" "${2:-}"
167
+ MODEL_SCOPE_PRESET="$2"
161
168
  shift 2
162
169
  ;;
163
170
  --default-model)
164
- DEFAULT_MODEL="${2:-}"
171
+ require_value "$1" "${2:-}"
172
+ DEFAULT_MODEL="$2"
165
173
  shift 2
166
174
  ;;
167
175
  --no-model-scope)
@@ -137,7 +137,7 @@ function runtimeMatrixStatus() {
137
137
  surface: `${platform}/${arch}`,
138
138
  status: "outside-release-matrix",
139
139
  teamRolloutReady: false,
140
- note: "This runtime is outside the documented v1.1.9 support matrix."
140
+ note: "This runtime is outside the documented v1.2.0 support matrix."
141
141
  };
142
142
  }
143
143
 
@@ -169,7 +169,7 @@ for (const rel of [
169
169
  "templates/project/.pi/memory/memory_summary.md",
170
170
  "templates/project/.pi/memory/MEMORY.md",
171
171
  "scripts/setup.sh",
172
- "scripts/configure-mcp.sh",
172
+ "scripts/mcp-manage.mjs",
173
173
  "scripts/configure-subagents.sh"
174
174
  ]) {
175
175
  if (!exists(rel)) errors.push(`missing platform file: ${rel}`);
@@ -247,6 +247,22 @@ if (piHasMcpAdapter && totalMcpServers === 0) {
247
247
  warnings.push("Pi MCP adapter is installed but no MCP servers are configured; run `piagent-mcp --preset core --scope global` or `/mcp setup`");
248
248
  }
249
249
 
250
+ // Counting servers says nothing about whether any of them can answer. A server
251
+ // whose token is not exported, or which this repository defines and nobody
252
+ // approved, is configured and unusable, and finding that out when a call fails
253
+ // is the thing this check exists to prevent.
254
+ try {
255
+ const { collectServers } = await import(pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "mcp", "mcp-config-layers.js")).href);
256
+ const { evaluateServerReadiness, BLOCKING_STATES } = await import(pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "mcp", "mcp-auth-readiness.js")).href);
257
+ for (const server of collectServers({ projectPath })) {
258
+ const readiness = evaluateServerReadiness(server, { projectPath });
259
+ if (!BLOCKING_STATES.has(readiness.state)) continue;
260
+ warnings.push(`MCP server ${server.name} (${server.scope}) is configured but not usable: ${readiness.detail}${readiness.remedy ? `; ${readiness.remedy}` : ""}`);
261
+ }
262
+ } catch (error) {
263
+ warnings.push(`MCP readiness check could not run: ${error instanceof Error ? error.message : String(error)}`);
264
+ }
265
+
250
266
  const subagentConfigPath = path.join(process.env.PI_CODING_AGENT_DIR || path.join(process.env.HOME || "", ".pi", "agent"), "extensions", "subagent", "config.json");
251
267
  const subagentConfig = readJsonIfPresent(subagentConfigPath);
252
268
  if (piHasSubagents && !subagentConfig) {
@@ -39,16 +39,22 @@ todos, and project memory. Those are operator data, not platform state.
39
39
  USAGE
40
40
  }
41
41
 
42
+ require_value() {
43
+ local option="$1"
44
+ local value="${2:-}"
45
+ if [[ -z "$value" || "$value" == --* ]]; then
46
+ echo "Missing value for $option" >&2
47
+ exit 2
48
+ fi
49
+ }
50
+
42
51
  while [[ $# -gt 0 ]]; do
43
52
  case "$1" in
44
53
  --apply) APPLY=true; shift ;;
45
54
  --with-addons) WITH_ADDONS=true; shift ;;
46
55
  --with-host) WITH_HOST=true; shift ;;
47
56
  --project)
48
- if [[ $# -lt 2 || -z "${2:-}" ]]; then
49
- echo "Missing value for --project" >&2
50
- exit 2
51
- fi
57
+ require_value "$1" "${2:-}"
52
58
  PROJECT_PATH="$2"
53
59
  shift 2
54
60
  ;;
@@ -126,9 +126,16 @@ required_files=(
126
126
  "$ROOT/docs/capability-packs.md"
127
127
  "$ROOT/docs/runtime-policy-design.md"
128
128
  "$ROOT/docs-site/index.html"
129
+ "$ROOT/docs-site/mcp.html"
129
130
  "$ROOT/docs-site/favicon.svg"
130
131
  "$ROOT/docs-site/assets/piagent-logo.svg"
132
+ "$ROOT/docs-site/assets/docs.css"
133
+ "$ROOT/docs-site/assets/docs.js"
134
+ "$ROOT/docs-site/content/index.html"
135
+ "$ROOT/docs-site/content/mcp.html"
131
136
  "$ROOT/docs-site/vercel.json"
137
+ "$ROOT/scripts/build-docs-site.mjs"
138
+ "$ROOT/scripts/preview-docs-site.mjs"
132
139
  "$ROOT/schemas/project-profile.schema.json"
133
140
  "$ROOT/schemas/task-contract.schema.json"
134
141
  "$ROOT/schemas/capability-pack.schema.json"
@@ -157,10 +164,11 @@ required_files=(
157
164
  "$ROOT/scripts/quality-benchmark.sh"
158
165
  "$ROOT/scripts/runtime-policy-smoke.sh"
159
166
  "$ROOT/scripts/pi-session-stats.sh"
167
+ "$ROOT/scripts/pi-usage-history.mjs"
160
168
  "$ROOT/scripts/pi-auto.sh"
161
169
  "$ROOT/scripts/pi-model-catalog.sh"
162
170
  "$ROOT/scripts/configure-model-scope.sh"
163
- "$ROOT/scripts/configure-mcp.sh"
171
+ "$ROOT/scripts/mcp-manage.mjs"
164
172
  "$ROOT/scripts/configure-subagents.sh"
165
173
  "$ROOT/scripts/capability-catalog.mjs"
166
174
  "$ROOT/scripts/migrate-project-state.mjs"
@@ -182,6 +190,9 @@ required_files=(
182
190
  "$ROOT/tests/document-intake.test.mjs"
183
191
  "$ROOT/tests/runtime-advisories.test.mjs"
184
192
  "$ROOT/scripts/check-runtime-advisories.mjs"
193
+ "$ROOT/packages/piagent-core/mcp/mcp-session-view.js"
194
+ "$ROOT/packages/piagent-core/mcp/mcp-command-actions.js"
195
+ "$ROOT/tests/mcp-session-command.test.mjs"
185
196
  "$ROOT/tests/migrate-project-state.test.mjs"
186
197
  "$ROOT/tests/uninstall-global.test.mjs"
187
198
  "$ROOT/tests/import-agent-instructions.test.mjs"
@@ -413,33 +424,33 @@ require_documented "gpt-5.6" "$ROOT/README.md" "$ROOT/docs/model-options.md" "$R
413
424
  require_documented "claude-fable-5" "$ROOT/README.md" "$ROOT/docs/model-options.md" "$ROOT/packages/piagent-core/prompts/model-options.md"
414
425
  require_documented "piagent-models" "$ROOT/README.md" "$ROOT/docs/model-options.md"
415
426
  require_documented "enabledModels" "$ROOT/templates/global/settings.json" "$ROOT/docs/model-options.md" "$ROOT/scripts/configure-model-scope.sh"
416
- require_documented "piagent-mcp" "$ROOT/README.md" "$ROOT/docs/mcp-and-tools.md" "$ROOT/scripts/configure-mcp.sh"
427
+ require_documented "piagent-mcp" "$ROOT/README.md" "$ROOT/docs/mcp-and-tools.md" "$ROOT/scripts/mcp-manage.mjs"
417
428
  require_documented "pi-mcp-adapter" "$ROOT/README.md" "$ROOT/docs/mcp-and-tools.md" "$ROOT/scripts/install-global.sh"
418
429
  require_documented "piagent-subagents" "$ROOT/README.md" "$ROOT/docs/subagents-and-multiagent.md" "$ROOT/scripts/configure-subagents.sh"
419
430
  require_documented "subagents-fleet" "$ROOT/docs/command-reference-vietnamese.md" "$ROOT/docs/subagents-and-multiagent.md" "$ROOT/README.md"
420
431
  require_documented "health check" "$ROOT/docs/command-reference-vietnamese.md" "$ROOT/docs/subagents-and-multiagent.md" "$ROOT/README.md"
421
432
  require_documented "pi-subagents" "$ROOT/README.md" "$ROOT/docs/subagents-and-multiagent.md" "$ROOT/scripts/install-global.sh" "$ROOT/scripts/setup.sh"
422
433
  require_documented "piagent-scout" "$ROOT/README.md" "$ROOT/docs/subagents-and-multiagent.md" "$ROOT/packages/piagent-core/subagents"
423
- grep -F "@upstash/context7-mcp@3.2.4" "$ROOT/scripts/configure-mcp.sh" >/dev/null
434
+ grep -F "@upstash/context7-mcp@3.2.4" "$ROOT/packages/piagent-core/mcp/mcp-server-catalog.js" >/dev/null
424
435
  grep -F "@upstash/context7-mcp@3.2.4" "$ROOT/docs/mcp-and-tools.md" >/dev/null
425
- grep -F "chrome-devtools-mcp@1.6.0" "$ROOT/scripts/configure-mcp.sh" >/dev/null
436
+ grep -F "chrome-devtools-mcp@1.6.0" "$ROOT/packages/piagent-core/mcp/mcp-server-catalog.js" >/dev/null
426
437
  grep -F "chrome-devtools-mcp@1.6.0" "$ROOT/docs/mcp-and-tools.md" >/dev/null
427
- grep -F "@playwright/mcp@0.0.78" "$ROOT/scripts/configure-mcp.sh" >/dev/null
438
+ grep -F "@playwright/mcp@0.0.78" "$ROOT/packages/piagent-core/mcp/mcp-server-catalog.js" >/dev/null
428
439
  grep -F "@playwright/mcp@0.0.78" "$ROOT/docs/mcp-and-tools.md" >/dev/null
429
- require_documented "https://mcp.figma.com/mcp" "$ROOT/scripts/configure-mcp.sh" "$ROOT/docs/mcp-and-tools.md"
430
- grep -F "ghcr.io/github/github-mcp-server@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3" "$ROOT/scripts/configure-mcp.sh" >/dev/null
440
+ require_documented "https://mcp.figma.com/mcp" "$ROOT/packages/piagent-core/mcp/mcp-server-catalog.js" "$ROOT/docs/mcp-and-tools.md"
441
+ grep -F "ghcr.io/github/github-mcp-server@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3" "$ROOT/packages/piagent-core/mcp/mcp-server-catalog.js" >/dev/null
431
442
  grep -F "ghcr.io/github/github-mcp-server@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3" "$ROOT/docs/mcp-and-tools.md" >/dev/null
432
- grep -F '"GITHUB_READ_ONLY=1"' "$ROOT/scripts/configure-mcp.sh" >/dev/null
443
+ grep -F '"GITHUB_READ_ONLY=1"' "$ROOT/packages/piagent-core/mcp/mcp-server-catalog.js" >/dev/null
433
444
  grep -F '"GITHUB_READ_ONLY=1"' "$ROOT/docs/mcp-and-tools.md" >/dev/null
434
- grep -F '"GITHUB_LOCKDOWN_MODE=1"' "$ROOT/scripts/configure-mcp.sh" >/dev/null
445
+ grep -F '"GITHUB_LOCKDOWN_MODE=1"' "$ROOT/packages/piagent-core/mcp/mcp-server-catalog.js" >/dev/null
435
446
  grep -F '"GITHUB_LOCKDOWN_MODE=1"' "$ROOT/docs/mcp-and-tools.md" >/dev/null
436
- grep -F 'CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: "1"' "$ROOT/scripts/configure-mcp.sh" >/dev/null
447
+ grep -F 'CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: "1"' "$ROOT/packages/piagent-core/mcp/mcp-server-catalog.js" >/dev/null
437
448
  grep -F '"CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS": "1"' "$ROOT/docs/mcp-and-tools.md" >/dev/null
438
- grep -F 'CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS: "1"' "$ROOT/scripts/configure-mcp.sh" >/dev/null
449
+ grep -F 'CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS: "1"' "$ROOT/packages/piagent-core/mcp/mcp-server-catalog.js" >/dev/null
439
450
  grep -F '"CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS": "1"' "$ROOT/docs/mcp-and-tools.md" >/dev/null
440
- grep -F 'configure-mcp.sh" --scope global --preset "$MCP_PRESET" --replace' "$ROOT/scripts/install-global.sh" >/dev/null
441
- grep -F 'piagent-mcp --preset core --scope global --replace' "$ROOT/scripts/configure-mcp.sh" >/dev/null
442
- if grep -E '@latest|"@upstash/context7-mcp"|"chrome-devtools-mcp"|"@playwright/mcp"|"ghcr\.io/github/github-mcp-server"' "$ROOT/scripts/configure-mcp.sh" >/dev/null; then
451
+ grep -F 'mcp-manage.mjs" --scope global --preset "$MCP_PRESET" --replace' "$ROOT/scripts/install-global.sh" >/dev/null
452
+ grep -F 'piagent-mcp --preset core --scope global --replace' "$ROOT/scripts/mcp-manage.mjs" >/dev/null
453
+ if grep -E '@latest|"@upstash/context7-mcp"|"chrome-devtools-mcp"|"@playwright/mcp"|"ghcr\.io/github/github-mcp-server"' "$ROOT/packages/piagent-core/mcp/mcp-server-catalog.js" >/dev/null; then
443
454
  echo "MCP production presets contain a mutable dependency source"
444
455
  exit 1
445
456
  fi
@@ -459,6 +470,23 @@ require_documented "piagent-capabilities" "$ROOT/README.md" "$ROOT/docs/capabili
459
470
  require_documented ".pi-subagents/" "$ROOT/.gitignore" "$ROOT/docs/subagents-and-multiagent.md" "$ROOT/docs/distribution-standard.md" "$ROOT/scripts/init-project.sh"
460
471
  test -s "$ROOT/tests/policy-core.test.mjs"
461
472
 
473
+ # Every gate below this line is a grep, and grep skips a file it decides is
474
+ # binary. One control byte written into a string literal — a NUL used as a key
475
+ # separator is the easy way to do it by accident — takes the whole file out of
476
+ # every one of them, and out of the diff a reviewer reads on a pull request. The
477
+ # file still runs, so nothing else notices. Escapes carry the same bytes and keep
478
+ # the file readable.
479
+ non_text_sources="$(
480
+ cd "$ROOT" && git ls-files -z -- '*.js' '*.mjs' '*.ts' '*.json' '*.md' '*.sh' '*.html' '*.css' \
481
+ | LC_ALL=C xargs -0 -I{} sh -c 'grep -qI "" "{}" 2>/dev/null || echo "{}"'
482
+ )"
483
+ if [[ -n "$non_text_sources" ]]; then
484
+ echo "Source files contain control bytes, so grep-based gates and diffs skip them:"
485
+ echo "$non_text_sources" | sed 's/^/ /'
486
+ echo "Write the byte as an escape (\\u0000, \\x03) instead of embedding it."
487
+ exit 1
488
+ fi
489
+
462
490
  public_wording_pattern="$(
463
491
  node --input-type=module <<'NODE'
464
492
  const terms = [
@@ -471,6 +499,9 @@ const terms = [
471
499
  ["mit", "suhiko"].join(""),
472
500
  ["Cod", "ex CLI"].join(""),
473
501
  ["Claude", " CLI"].join(""),
502
+ ["Claude", " Code"].join(""),
503
+ ["claude", " mcp"].join(""),
504
+ ["codex", " mcp"].join(""),
474
505
  ["Cod", "ex-inspired"].join(""),
475
506
  ["Cod", "ex-grade"].join(""),
476
507
  ["Pi vs ", "Codex"].join(""),
@@ -485,12 +516,21 @@ console.log(terms.map(escapeRegex).join("|"));
485
516
  NODE
486
517
  )"
487
518
 
519
+ # Every surface an outsider reads: the published docs, the site, and the strings
520
+ # the runtime prints back at an operator. The site is checked at its source,
521
+ # since `build-docs-site.mjs --check` separately proves the committed pages match
522
+ # it; the builder itself is listed too, because navigation labels and page titles
523
+ # live there rather than in a content fragment.
488
524
  if grep -R -E -i \
489
525
  "$public_wording_pattern" \
490
526
  "$ROOT/README.md" \
491
527
  "$ROOT/docs" \
528
+ "$ROOT/docs-site/content" \
529
+ "$ROOT/scripts/build-docs-site.mjs" \
492
530
  "$ROOT/packages/piagent-core/README.md" \
493
531
  "$ROOT/packages/piagent-core/prompts" \
532
+ "$ROOT/packages/piagent-core/mcp" \
533
+ "$ROOT/packages/piagent-core/extensions" \
494
534
  "$ROOT/templates/project/AGENTS.md" >/dev/null; then
495
535
  echo "Public docs contain non-neutral platform wording"
496
536
  exit 1
@@ -505,10 +545,13 @@ node --check "$ROOT/packages/piagent-core/capabilities/capability-core.js" >/dev
505
545
  node --check "$ROOT/packages/piagent-core/capabilities/capability-sources.js" >/dev/null
506
546
  node --check "$ROOT/scripts/capability-catalog.mjs" >/dev/null
507
547
  node --check "$ROOT/scripts/piagent-cli.mjs" >/dev/null
548
+ node --check "$ROOT/scripts/pi-usage-history.mjs" >/dev/null
508
549
  node --check "$ROOT/scripts/migrate-project-state.mjs" >/dev/null
509
550
  node --check "$ROOT/scripts/import-agent-instructions.mjs" >/dev/null
510
551
  node --check "$ROOT/scripts/check-runtime-advisories.mjs" >/dev/null
511
552
  node --check "$ROOT/scripts/check-published-site.mjs" >/dev/null
553
+ node --check "$ROOT/scripts/build-docs-site.mjs" >/dev/null
554
+ node --check "$ROOT/scripts/preview-docs-site.mjs" >/dev/null
512
555
  run_quietly "npm test" npm test
513
556
  if [[ -x "$ROOT/node_modules/.bin/tsc" ]]; then
514
557
  run_quietly "npm run typecheck" npm run typecheck
@@ -519,7 +562,9 @@ bash -n "$ROOT/scripts/pi-session-stats.sh"
519
562
  bash -n "$ROOT/scripts/pi-auto.sh"
520
563
  bash -n "$ROOT/scripts/pi-model-catalog.sh"
521
564
  bash -n "$ROOT/scripts/configure-model-scope.sh"
522
- bash -n "$ROOT/scripts/configure-mcp.sh"
565
+ node --check "$ROOT/scripts/mcp-manage.mjs"
566
+ node --check "$ROOT/packages/piagent-core/mcp/mcp-session-view.js"
567
+ node --check "$ROOT/packages/piagent-core/mcp/mcp-command-actions.js"
523
568
  bash -n "$ROOT/scripts/configure-subagents.sh"
524
569
  bash -n "$ROOT/scripts/install-global.sh"
525
570
  bash -n "$ROOT/scripts/init-project.sh"
@@ -531,8 +576,8 @@ else
531
576
  bash "$ROOT/scripts/pi-model-catalog.sh" --json >/dev/null
532
577
  fi
533
578
  bash "$ROOT/scripts/configure-model-scope.sh" --dry-run --preset full --default-model openai-codex/gpt-5.5:xhigh >/dev/null
534
- bash "$ROOT/scripts/configure-mcp.sh" --list >/dev/null
535
- bash "$ROOT/scripts/configure-mcp.sh" --dry-run --preset popular --scope project --project "$ROOT" >/dev/null
579
+ node "$ROOT/scripts/mcp-manage.mjs" --list >/dev/null
580
+ node "$ROOT/scripts/mcp-manage.mjs" --dry-run --preset popular --scope project --project "$ROOT" >/dev/null
536
581
  node --input-type=module - "$ROOT" <<'NODE'
537
582
  import assert from "node:assert/strict";
538
583
  import { execFileSync } from "node:child_process";
@@ -553,8 +598,8 @@ try {
553
598
  internal: { url: "https://mcp.example.invalid/api" }
554
599
  }
555
600
  }, null, 2)}\n`);
556
- execFileSync("bash", [
557
- path.join(root, "scripts", "configure-mcp.sh"),
601
+ execFileSync(process.execPath, [
602
+ path.join(root, "scripts", "mcp-manage.mjs"),
558
603
  "--config", configPath,
559
604
  "--preset", "popular",
560
605
  "--replace"
@@ -577,6 +622,7 @@ bash "$ROOT/scripts/configure-subagents.sh" --list >/dev/null
577
622
  bash "$ROOT/scripts/configure-subagents.sh" --dry-run --preset safe >/dev/null
578
623
  bash "$ROOT/scripts/runtime-policy-smoke.sh" >/dev/null
579
624
 
625
+ node "$ROOT/scripts/build-docs-site.mjs" --check >/dev/null
580
626
  node "$ROOT/scripts/capability-catalog.mjs" catalog --check >/dev/null
581
627
  node "$ROOT/scripts/capability-catalog.mjs" doctor --profile "$ROOT/.pi/piagent-profile.json" --lock "$ROOT/.pi/piagent-profile.lock.json" >/dev/null
582
628
  node "$ROOT/scripts/capability-catalog.mjs" doctor --profile "$ROOT/adapters/generic/profile.json" >/dev/null
@@ -71,8 +71,43 @@ if (capabilityLock.core?.packageVersion !== version) fail("capability lock packa
71
71
  const changelog = fs.readFileSync(path.join(root, "CHANGELOG.md"), "utf8");
72
72
  if (!changelog.includes(`## ${expectedTag} -`)) fail(`CHANGELOG.md has no ${expectedTag} release section`);
73
73
 
74
- const docsSite = fs.readFileSync(path.join(root, "docs-site/index.html"), "utf8");
75
- if (!docsSite.includes(`${expectedTag} docs`)) fail(`docs-site version badge does not identify ${expectedTag}`);
74
+ // The badge is generated onto every page, so a stale build shows up as one page
75
+ // still carrying the previous version rather than as a single missed edit.
76
+ const docsPages = fs.readdirSync(path.join(root, "docs-site")).filter((name) => name.endsWith(".html"));
77
+ if (docsPages.length === 0) fail("docs-site has no generated pages");
78
+ for (const page of docsPages) {
79
+ const html = fs.readFileSync(path.join(root, "docs-site", page), "utf8");
80
+ if (!html.includes(`${expectedTag} docs`)) fail(`docs-site/${page} version badge does not identify ${expectedTag}`);
81
+ }
82
+
83
+ // A version written into an install command does not come from the build, so it
84
+ // goes stale silently and keeps telling readers to install the previous release.
85
+ // The badge check above cannot see these: they are sentences, not generated
86
+ // markup.
87
+ //
88
+ // Only the command shapes are matched, not every version-looking string. Prose
89
+ // naming an old release is usually saying something true about history — when a
90
+ // behaviour arrived, which release a mistake shipped in — and rewriting those to
91
+ // the current number would make them false. Files that exist to list past
92
+ // versions are exempt outright.
93
+ const VERSION_HISTORY_FILES = new Set(["CHANGELOG.md", "docs/publishing-for-teams.md"]);
94
+ const RELEASE_TAG_MENTION = /(?:@piagent\/platform@|Vt-mmm\/piagent@v?|--version )(\d+\.\d+\.\d+)/g;
95
+
96
+ function proseFiles() {
97
+ const files = fs.readdirSync(path.join(root, "docs")).filter((name) => name.endsWith(".md")).map((name) => `docs/${name}`);
98
+ files.push("README.md");
99
+ for (const name of fs.readdirSync(path.join(root, "docs-site", "content"))) {
100
+ if (name.endsWith(".html")) files.push(`docs-site/content/${name}`);
101
+ }
102
+ return files.filter((file) => !VERSION_HISTORY_FILES.has(file));
103
+ }
104
+
105
+ for (const file of proseFiles()) {
106
+ const text = fs.readFileSync(path.join(root, file), "utf8");
107
+ for (const [mention, named] of text.matchAll(RELEASE_TAG_MENTION)) {
108
+ if (named !== version) fail(`${file} tells the reader to install ${named} (${mention.trim()}), but this release is ${version}`);
109
+ }
110
+ }
76
111
 
77
112
  if (options.tag) {
78
113
  if (options.tag !== expectedTag) fail(`release tag ${options.tag} does not match package version ${expectedTag}`);