@piagent/platform 1.43.9 → 1.44.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 (78) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/docs/command-reference-vietnamese.md +8 -2
  3. package/docs/company-dev-commands.md +2 -0
  4. package/docs/distribution-standard.md +6 -6
  5. package/docs/operator-manual-vietnamese.md +7 -7
  6. package/docs/publishing-for-teams.md +2 -0
  7. package/docs/release-install-policy.md +4 -2
  8. package/docs/team-onboarding.md +2 -2
  9. package/docs-site/architecture.html +1 -1
  10. package/docs-site/assets/search-en.json +1 -1
  11. package/docs-site/assets/search-vi.json +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/context-engine.html +1 -1
  15. package/docs-site/ecosystem.html +1 -1
  16. package/docs-site/en/architecture.html +1 -1
  17. package/docs-site/en/benchmark.html +1 -1
  18. package/docs-site/en/commands.html +1 -1
  19. package/docs-site/en/context-engine.html +1 -1
  20. package/docs-site/en/ecosystem.html +1 -1
  21. package/docs-site/en/index.html +9 -9
  22. package/docs-site/en/mcp-approval.html +1 -1
  23. package/docs-site/en/mcp-auth.html +1 -1
  24. package/docs-site/en/mcp-servers.html +1 -1
  25. package/docs-site/en/mcp.html +1 -1
  26. package/docs-site/en/permissions.html +1 -1
  27. package/docs-site/en/profiles.html +1 -1
  28. package/docs-site/en/quickstart.html +1 -1
  29. package/docs-site/en/runtime.html +1 -1
  30. package/docs-site/en/scope.html +1 -1
  31. package/docs-site/en/security.html +1 -1
  32. package/docs-site/en/team.html +1 -1
  33. package/docs-site/en/update.html +3 -3
  34. package/docs-site/en/whats-new.html +55 -31
  35. package/docs-site/en/windows.html +1 -1
  36. package/docs-site/en/workflows.html +1 -1
  37. package/docs-site/index.html +9 -9
  38. package/docs-site/mcp-approval.html +1 -1
  39. package/docs-site/mcp-auth.html +1 -1
  40. package/docs-site/mcp-servers.html +1 -1
  41. package/docs-site/mcp.html +1 -1
  42. package/docs-site/permissions.html +1 -1
  43. package/docs-site/profiles.html +1 -1
  44. package/docs-site/quickstart.html +1 -1
  45. package/docs-site/releases.json +77 -0
  46. package/docs-site/runtime.html +1 -1
  47. package/docs-site/scope.html +1 -1
  48. package/docs-site/security.html +1 -1
  49. package/docs-site/team.html +1 -1
  50. package/docs-site/update.html +3 -3
  51. package/docs-site/whats-new.html +55 -31
  52. package/docs-site/windows.html +1 -1
  53. package/docs-site/workflows.html +1 -1
  54. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  55. package/package.json +4 -1
  56. package/packages/piagent-core/extensions/piagent-guard.ts +3 -0
  57. package/packages/piagent-core/managed/pi-host.mjs +60 -1
  58. package/packages/piagent-core/managed/resource-loader.mjs +3 -2
  59. package/packages/piagent-core/managed/session.mjs +2 -1
  60. package/packages/piagent-core/package.json +1 -1
  61. package/packages/piagent-core/runtime/resources/plugin-skills.mjs +33 -4
  62. package/packages/piagent-core/runtime/terminal/terminal-style.d.mts +7 -0
  63. package/packages/piagent-core/runtime/terminal/terminal-style.mjs +99 -0
  64. package/packages/piagent-core/themes/piagent-contrast.json +78 -0
  65. package/packages/piagent-core/themes/piagent-focus.json +78 -0
  66. package/packages/piagent-core/themes/piagent-ocean.json +78 -0
  67. package/packages/piagent-core/themes/piagent-paper.json +78 -0
  68. package/packages/piagent-webui/gateway/pi-host.ts +13 -1
  69. package/scripts/company-terminal.mjs +6 -2
  70. package/scripts/install-global.sh +13 -1
  71. package/scripts/pi-host.mjs +109 -0
  72. package/scripts/piagent-cli.mjs +17 -6
  73. package/scripts/piagent-dashboard.mjs +2 -2
  74. package/scripts/runtime-advisory-exceptions.json +40 -0
  75. package/scripts/setup.sh +19 -1
  76. package/scripts/skill-sources.mjs +188 -0
  77. package/scripts/uninstall-global.sh +18 -1
  78. package/scripts/update-global.mjs +13 -5
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
+ "name": "piagent-focus",
4
+ "appearance": "dark",
5
+ "vars": {
6
+ "text": "okhsl(60 4% 88%)",
7
+ "muted": "okhsl(60 3% 64%)",
8
+ "violet": "okhsl(70 80% 70%)",
9
+ "blue": "okhsl(60 10% 78%)",
10
+ "green": "okhsl(140 30% 66%)",
11
+ "red": "okhsl(25 60% 66%)",
12
+ "yellow": "okhsl(70 80% 70%)",
13
+ "blueBg": "okhsl(60 6% 22%)"
14
+ },
15
+ "colors": {
16
+ "accent": "violet",
17
+ "border": "okhsl(231 14% 65%)",
18
+ "borderAccent": "okhsl(295 13% 64%)",
19
+ "borderMuted": "okhsl(229 2% 53%)",
20
+ "success": "green",
21
+ "error": "red",
22
+ "warning": "yellow",
23
+ "muted": "muted",
24
+ "dim": "okhsl(229 2% 56%)",
25
+ "text": "text",
26
+ "thinkingText": "okhsl(226 2% 65%)",
27
+ "selectedBg": "blueBg",
28
+ "scrollbarTrack": "okhsl(237 2% 33%)",
29
+ "scrollbarThumb": "okhsl(232 2% 65%)",
30
+ "searchMatchBg": "okhsl(53 13% 24%)",
31
+ "searchMatchText": "muted",
32
+ "userMessageBg": "blueBg",
33
+ "userMessageText": "text",
34
+ "customMessageBg": "okhsl(295 10% 24%)",
35
+ "customMessageText": "muted",
36
+ "customMessageLabel": "violet",
37
+ "toolPendingBg": "okhsl(229 1% 24%)",
38
+ "toolSuccessBg": "okhsl(158 12% 25%)",
39
+ "toolErrorBg": "okhsl(19 14% 25%)",
40
+ "toolTitle": "text",
41
+ "toolOutput": "muted",
42
+ "mdHeading": "yellow",
43
+ "mdLink": "blue",
44
+ "mdLinkUrl": "muted",
45
+ "mdCode": "violet",
46
+ "mdCodeBlock": "green",
47
+ "mdCodeBlockBorder": "muted",
48
+ "mdQuote": "muted",
49
+ "mdQuoteBorder": "muted",
50
+ "mdHr": "muted",
51
+ "mdListBullet": "violet",
52
+ "toolDiffAdded": "green",
53
+ "toolDiffRemoved": "red",
54
+ "toolDiffContext": "muted",
55
+ "syntaxComment": "muted",
56
+ "syntaxKeyword": "blue",
57
+ "syntaxFunction": "yellow",
58
+ "syntaxVariable": "okhsl(202 14% 67%)",
59
+ "syntaxString": "okhsl(52 17% 67%)",
60
+ "syntaxNumber": "green",
61
+ "syntaxType": "violet",
62
+ "syntaxOperator": "muted",
63
+ "syntaxPunctuation": "muted",
64
+ "thinkingOff": "okhsl(229 2% 49%)",
65
+ "thinkingMinimal": "okhsl(232 5% 52%)",
66
+ "thinkingLow": "okhsl(232 11% 54%)",
67
+ "thinkingMedium": "okhsl(263 15% 56%)",
68
+ "thinkingHigh": "okhsl(295 18% 59%)",
69
+ "thinkingXhigh": "okhsl(337 20% 61%)",
70
+ "thinkingMax": "okhsl(20 25% 63%)",
71
+ "bashMode": "okhsl(159 16% 65%)"
72
+ },
73
+ "export": {
74
+ "pageBg": "okhsl(262 4% 16%)",
75
+ "cardBg": "okhsl(264 3% 19%)",
76
+ "infoBg": "okhsl(53 13% 24%)"
77
+ }
78
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
+ "name": "piagent-ocean",
4
+ "appearance": "dark",
5
+ "vars": {
6
+ "text": "okhsl(200 8% 90%)",
7
+ "muted": "okhsl(200 10% 68%)",
8
+ "violet": "okhsl(185 70% 68%)",
9
+ "blue": "okhsl(215 70% 70%)",
10
+ "green": "okhsl(150 60% 68%)",
11
+ "red": "okhsl(15 75% 68%)",
12
+ "yellow": "okhsl(75 85% 70%)",
13
+ "blueBg": "okhsl(205 45% 22%)"
14
+ },
15
+ "colors": {
16
+ "accent": "violet",
17
+ "border": "okhsl(191 51% 65%)",
18
+ "borderAccent": "okhsl(255 48% 64%)",
19
+ "borderMuted": "okhsl(189 7% 53%)",
20
+ "success": "green",
21
+ "error": "red",
22
+ "warning": "yellow",
23
+ "muted": "muted",
24
+ "dim": "okhsl(189 7% 56%)",
25
+ "text": "text",
26
+ "thinkingText": "okhsl(186 6% 65%)",
27
+ "selectedBg": "blueBg",
28
+ "scrollbarTrack": "okhsl(197 6% 33%)",
29
+ "scrollbarThumb": "okhsl(192 6% 65%)",
30
+ "searchMatchBg": "okhsl(13 46% 24%)",
31
+ "searchMatchText": "muted",
32
+ "userMessageBg": "blueBg",
33
+ "userMessageText": "text",
34
+ "customMessageBg": "okhsl(255 38% 24%)",
35
+ "customMessageText": "muted",
36
+ "customMessageLabel": "violet",
37
+ "toolPendingBg": "okhsl(189 4% 24%)",
38
+ "toolSuccessBg": "okhsl(118 41% 25%)",
39
+ "toolErrorBg": "okhsl(339 49% 25%)",
40
+ "toolTitle": "text",
41
+ "toolOutput": "muted",
42
+ "mdHeading": "yellow",
43
+ "mdLink": "blue",
44
+ "mdLinkUrl": "muted",
45
+ "mdCode": "violet",
46
+ "mdCodeBlock": "green",
47
+ "mdCodeBlockBorder": "muted",
48
+ "mdQuote": "muted",
49
+ "mdQuoteBorder": "muted",
50
+ "mdHr": "muted",
51
+ "mdListBullet": "violet",
52
+ "toolDiffAdded": "green",
53
+ "toolDiffRemoved": "red",
54
+ "toolDiffContext": "muted",
55
+ "syntaxComment": "muted",
56
+ "syntaxKeyword": "blue",
57
+ "syntaxFunction": "yellow",
58
+ "syntaxVariable": "okhsl(162 52% 67%)",
59
+ "syntaxString": "okhsl(12 60% 67%)",
60
+ "syntaxNumber": "green",
61
+ "syntaxType": "violet",
62
+ "syntaxOperator": "muted",
63
+ "syntaxPunctuation": "muted",
64
+ "thinkingOff": "okhsl(189 7% 49%)",
65
+ "thinkingMinimal": "okhsl(192 18% 52%)",
66
+ "thinkingLow": "okhsl(192 40% 54%)",
67
+ "thinkingMedium": "okhsl(223 53% 56%)",
68
+ "thinkingHigh": "okhsl(255 66% 59%)",
69
+ "thinkingXhigh": "okhsl(297 73% 61%)",
70
+ "thinkingMax": "okhsl(340 89% 63%)",
71
+ "bashMode": "okhsl(119 58% 65%)"
72
+ },
73
+ "export": {
74
+ "pageBg": "okhsl(222 13% 16%)",
75
+ "cardBg": "okhsl(224 12% 19%)",
76
+ "infoBg": "okhsl(13 46% 24%)"
77
+ }
78
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
+ "name": "piagent-paper",
4
+ "appearance": "light",
5
+ "vars": {
6
+ "text": "okhsl(40 10% 22%)",
7
+ "muted": "okhsl(40 8% 44%)",
8
+ "violet": "okhsl(330 55% 42%)",
9
+ "blue": "okhsl(225 65% 42%)",
10
+ "green": "okhsl(150 70% 36%)",
11
+ "red": "okhsl(25 85% 44%)",
12
+ "yellow": "okhsl(70 95% 40%)",
13
+ "blueBg": "okhsl(50 30% 90%)"
14
+ },
15
+ "colors": {
16
+ "accent": "violet",
17
+ "border": "okhsl(231 60% 55%)",
18
+ "borderAccent": "okhsl(295 53% 55%)",
19
+ "borderMuted": "okhsl(235 6% 66%)",
20
+ "success": "green",
21
+ "error": "red",
22
+ "warning": "yellow",
23
+ "muted": "muted",
24
+ "dim": "okhsl(229 6% 59%)",
25
+ "text": "text",
26
+ "thinkingText": "okhsl(234 7% 55%)",
27
+ "selectedBg": "blueBg",
28
+ "scrollbarTrack": "okhsl(248 3% 90%)",
29
+ "scrollbarThumb": "okhsl(226 6% 65%)",
30
+ "searchMatchBg": "okhsl(56 20% 91%)",
31
+ "searchMatchText": "muted",
32
+ "userMessageBg": "blueBg",
33
+ "userMessageText": "text",
34
+ "customMessageBg": "okhsl(295 22% 91%)",
35
+ "customMessageText": "muted",
36
+ "customMessageLabel": "violet",
37
+ "toolPendingBg": "okhsl(248 3% 91%)",
38
+ "toolSuccessBg": "okhsl(156 19% 91%)",
39
+ "toolErrorBg": "okhsl(24 21% 91%)",
40
+ "toolTitle": "text",
41
+ "toolOutput": "muted",
42
+ "mdHeading": "yellow",
43
+ "mdLink": "blue",
44
+ "mdLinkUrl": "muted",
45
+ "mdCode": "violet",
46
+ "mdCodeBlock": "green",
47
+ "mdCodeBlockBorder": "muted",
48
+ "mdQuote": "muted",
49
+ "mdQuoteBorder": "muted",
50
+ "mdHr": "muted",
51
+ "mdListBullet": "violet",
52
+ "toolDiffAdded": "green",
53
+ "toolDiffRemoved": "red",
54
+ "toolDiffContext": "muted",
55
+ "syntaxComment": "muted",
56
+ "syntaxKeyword": "blue",
57
+ "syntaxFunction": "yellow",
58
+ "syntaxVariable": "okhsl(203 66% 46%)",
59
+ "syntaxString": "okhsl(52 76% 46%)",
60
+ "syntaxNumber": "green",
61
+ "syntaxType": "violet",
62
+ "syntaxOperator": "muted",
63
+ "syntaxPunctuation": "muted",
64
+ "thinkingOff": "okhsl(223 4% 80%)",
65
+ "thinkingMinimal": "okhsl(229 13% 78%)",
66
+ "thinkingLow": "okhsl(232 30% 76%)",
67
+ "thinkingMedium": "okhsl(264 43% 74%)",
68
+ "thinkingHigh": "okhsl(295 56% 72%)",
69
+ "thinkingXhigh": "okhsl(337 67% 70%)",
70
+ "thinkingMax": "okhsl(20 88% 68%)",
71
+ "bashMode": "okhsl(159 67% 55%)"
72
+ },
73
+ "export": {
74
+ "pageBg": "okhsl(17 3% 94%)",
75
+ "cardBg": "okhsl(17 4% 97%)",
76
+ "infoBg": "okhsl(56 20% 91%)"
77
+ }
78
+ }
@@ -4,6 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { createRequire } from "node:module";
6
6
  import { pathToFileURL } from "node:url";
7
+ import { privateHostRoot } from "../../piagent-core/managed/pi-host.mjs";
7
8
 
8
9
  const require = createRequire(import.meta.url);
9
10
 
@@ -73,8 +74,19 @@ export function installedPiHostRoot(): string {
73
74
  throw new Error("pi-host-unavailable");
74
75
  }
75
76
 
77
+ // The host of the pinned release: the installed one when it is that release,
78
+ // else Piagent's private copy, kept beside a member's own newer Pi
79
+ // (packages/piagent-core/managed/pi-host.mjs).
80
+ export function pinnedPiHostRoot(expectedVersion: string): string {
81
+ const own = privateHostRoot(expectedVersion) as string | null;
82
+ let root: string;
83
+ try { root = installedPiHostRoot(); } catch (error) { if (own) return own; throw error; }
84
+ try { if ((JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as { version?: string }).version === expectedVersion) return root; } catch { /* unreadable: the private copy, if any */ }
85
+ return own ?? root;
86
+ }
87
+
76
88
  export async function loadPinnedPiHost(expectedVersion: string): Promise<any> {
77
- const root = installedPiHostRoot();
89
+ const root = pinnedPiHostRoot(expectedVersion);
78
90
  const actual = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as { version?: string };
79
91
  if (actual.version !== expectedVersion) throw new Error("pi-host-version-mismatch");
80
92
  return await import(pathToFileURL(path.join(root, "dist", "index.js")).href);
@@ -9,6 +9,7 @@ import { execFile } from 'node:child_process';
9
9
  import { pathToFileURL } from 'node:url';
10
10
 
11
11
  import { registerTerminalUpdateOffer } from '../packages/piagent-core/runtime/update/terminal-update-offer.mjs';
12
+ import { registerStyleCommand } from '../packages/piagent-core/runtime/terminal/terminal-style.mjs';
12
13
  import { parseFailure } from '../packages/piagent-core/runtime/managed-failure.mjs';
13
14
  import { checkpointsOf } from '../packages/piagent-core/managed/checkpoints.mjs';
14
15
  import { bypassAllowed, effectivePermission } from '../packages/piagent-core/managed/permission.mjs';
@@ -59,7 +60,7 @@ export function memoryResultLines(text, expanded = false) {
59
60
  const branchOf = (cwd) => new Promise((resolve) => execFile('git', ['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD'], { timeout: 3000 },
60
61
  (error, out) => resolve(error ? null : String(out).trim() || null)));
61
62
  export const HELP_TEXT = [
62
- 'Cuộc trò chuyện công ty: /undo hoàn tác file lượt trước đã sửa · /bypass on|full|off|status quyền của cuộc trò chuyện · /fast on|off|status (khi Harness cho phép) · /skill:<tên> hoặc /<lệnh> của bạn và của project.',
63
+ 'Cuộc trò chuyện công ty: /undo hoàn tác file lượt trước đã sửa · /bypass on|full|off|status quyền của cuộc trò chuyện · /fast on|off|status (khi Harness cho phép) · /style giao diện Terminal · /skill:<tên> hoặc /<lệnh> của bạn và của project.',
63
64
  'Pi: /session thông tin cuộc trò chuyện · /resume mở cuộc trò chuyện công ty khác · /new cuộc mới · /compact nén ngữ cảnh · /tree, /fork nhánh · /name đặt tên · /copy, /export · /hotkeys toàn bộ phím tắt · /quit thoát.',
64
65
  'Phím: ! <lệnh> chạy shell · Esc dừng lượt đang chạy · Ctrl+C xoá ô nhập, hai lần để thoát · Ctrl+O xem thêm.',
65
66
  'Mở lại sau: piagent studio --project <thư mục>, rồi /resume. Đừng mở cuộc trò chuyện công ty bằng pi cá nhân.',
@@ -112,7 +113,7 @@ export function companyCommandText(name, summary = {}) {
112
113
  case 'login': case 'logout': return 'Cuộc trò chuyện công ty dùng tài khoản AI của team qua Agent Watch: không cần /login hay /logout. Tài khoản và API key cá nhân chỉ dùng trong pi cá nhân.';
113
114
  case 'model': case 'scoped-models': return `Model do Harness của team quyết định${main}; Terminal không chọn model. ${ADMIN}`;
114
115
  case 'thinking': return `Mức suy luận do Harness của team quyết định${main}. ${ADMIN}`;
115
- case 'settings': return `Cài đặt của cuộc trò chuyện công ty (compaction, retry…) do Harness quyết định. Phím tắt: /hotkeys. ${ADMIN}`;
116
+ case 'settings': return `Cài đặt của cuộc trò chuyện công ty (compaction, retry…) do Harness quyết định. Giao diện Terminal: /style. Phím tắt: /hotkeys. ${ADMIN}`;
116
117
  default: return null;
117
118
  }
118
119
  }
@@ -288,6 +289,9 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
288
289
  registerTerminalUpdateOffer(pi, { installed, packageRoot, restart: 'piagent studio',
289
290
  afterUpdate: () => bindingChanged() ? 'Bản mới đổi trình khởi chạy công ty: Agent Watch tự cập nhật cấu hình (cần Agent Watch đang chạy); piagent studio chờ việc này khi mở lại.' : null });
290
291
 
292
+ // The member's pick of look (/style); /settings is not offered here.
293
+ registerStyleCommand(pi, { remember: true });
294
+
291
295
  // Ask first or Bypass, kept with the conversation like the dashboard's switch.
292
296
  const showPermission = (ctx) => { const mode = managed() ? effectivePermission(managed()) : null; ctx.ui?.setStatus?.(PERMISSION_STATUS, mode && mode !== 'workspace-write' ? PERMISSION_NAME[mode] : undefined); };
293
297
  pi.on('session_start', async (_event, ctx) => showPermission(ctx));
@@ -701,7 +701,19 @@ else
701
701
  if [[ "$pi_version_output" =~ ([0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?) ]]; then
702
702
  current_pi_version="${BASH_REMATCH[1]}"
703
703
  fi
704
- if [[ "$current_pi_version" != "$EXPECTED_PI_VERSION" ]]; then
704
+ pi_host_plan="$(node --input-type=module -e 'const { hostPlan } = await import(process.argv[1]); process.stdout.write(hostPlan(process.argv[2] || null));' "$PLATFORM_ROOT/packages/piagent-core/managed/pi-host.mjs" "$current_pi_version")"
705
+ if [[ "$pi_host_plan" == "private" ]]; then
706
+ # The member's own, newer pi stays: it loads the Piagent package as any Pi
707
+ # package, and company sessions and `piagent pi` run the private pinned host.
708
+ echo "NOTE: your pi $current_pi_version stays as it is; Piagent is qualified on Pi $EXPECTED_PI_VERSION." >&2
709
+ pi_host_args=(ensure --force)
710
+ [[ "$DRY_RUN" == true ]] && pi_host_args+=(--dry-run)
711
+ if ! node "$PLATFORM_ROOT/scripts/pi-host.mjs" "${pi_host_args[@]}" >&2; then
712
+ echo "FAIL: could not install the private Pi $EXPECTED_PI_VERSION." >&2
713
+ exit 1
714
+ fi
715
+ echo "NOTE: run \`piagent pi\` for the qualified release; \`pi\` ($current_pi_version) loads Piagent too, unqualified." >&2
716
+ elif [[ "$current_pi_version" != "$EXPECTED_PI_VERSION" ]]; then
705
717
  echo "FAIL: Pi Coding Agent $EXPECTED_PI_VERSION is required; found ${current_pi_version:-${pi_version_output:-unknown}}." >&2
706
718
  echo "Install the supported host with: npm install -g --ignore-scripts @earendil-works/pi-coding-agent@$EXPECTED_PI_VERSION" >&2
707
719
  exit 1
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ // The Pi host Piagent runs on, when the member keeps a Pi of their own.
3
+ //
4
+ // Piagent pins one Pi release (package.json peerDependencies). A member who
5
+ // already uses Pi may run a newer one: setup used to replace it with the
6
+ // pinned release. Now theirs stays as it is, and the pinned release is
7
+ // installed privately (packages/piagent-core/managed/pi-host.mjs); company
8
+ // sessions and the dashboard run it, and `piagent pi` opens it for personal use.
9
+ //
10
+ // pi-host.mjs status [--json] the pinned host, the `pi` on PATH, the private copy
11
+ // pi-host.mjs ensure [--dry-run] install the private copy when PATH has another Pi
12
+ // pi-host.mjs root the host root Piagent runs (empty when none)
13
+ // pi-host.mjs run [-- args] run Pi on the pinned host
14
+ import { spawn, spawnSync } from 'node:child_process';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { PI_HOST_VERSION, hostCli, hostPlan, installPrivateHost, privateHostRoot } from '../packages/piagent-core/managed/pi-host.mjs';
19
+
20
+ const HOST_NAME = '@earendil-works/pi-coding-agent';
21
+
22
+ function usage() {
23
+ return `Usage:
24
+ piagent pi [pi arguments] Run Pi ${PI_HOST_VERSION} (the release Piagent is qualified on)
25
+ piagent pi-host status [--json] Show the pinned host, your own pi and the private copy
26
+ piagent pi-host ensure [--dry-run]
27
+ Install the pinned host privately when your pi is another release
28
+ `;
29
+ }
30
+
31
+ // The `pi` on PATH: its package root and version, or nulls.
32
+ export function pathHost(env = process.env, { probe = true } = {}) {
33
+ for (const dir of (env.PATH ?? '').split(path.delimiter)) {
34
+ if (!path.isAbsolute(dir)) continue;
35
+ let root;
36
+ try { root = path.dirname(fs.realpathSync(path.join(dir, process.platform === 'win32' ? 'pi.cmd' : 'pi'))); } catch { continue; }
37
+ for (let depth = 0; depth < 4; depth += 1, root = path.dirname(root)) {
38
+ try {
39
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
40
+ if (manifest.name === HOST_NAME) return { root, version: manifest.version ?? null };
41
+ } catch { /* keep walking up */ }
42
+ }
43
+ }
44
+ if (!probe) return { root: null, version: null };
45
+ // A `pi` that is not an npm package (a standalone build): ask it.
46
+ const output = spawnSync('pi', ['--version'], { encoding: 'utf8', timeout: 5_000, shell: process.platform === 'win32' });
47
+ const version = output.status === 0 ? /(\d+\.\d+\.\d+(?:[+-][0-9A-Za-z.-]+)?)/.exec(output.stdout)?.[1] ?? null : null;
48
+ return { root: null, version };
49
+ }
50
+
51
+ // The host Piagent runs: the `pi` on PATH when it is the pinned release, else
52
+ // the private copy, else (not installed yet) whatever PATH has.
53
+ export function activeHostRoot({ env = process.env, home } = {}) {
54
+ const own = pathHost(env, { probe: false });
55
+ if (own.root && own.version === PI_HOST_VERSION) return own.root;
56
+ return privateHostRoot(PI_HOST_VERSION, home) ?? own.root;
57
+ }
58
+
59
+ export function status({ env = process.env, home } = {}) {
60
+ const own = pathHost(env), privateRoot = privateHostRoot(PI_HOST_VERSION, home);
61
+ return { pinned: PI_HOST_VERSION, own: own.version, ownRoot: own.root, privateRoot, plan: hostPlan(own.version), active: activeHostRoot({ env, home }) };
62
+ }
63
+
64
+ function describe(report) {
65
+ const lines = [`Piagent runs on Pi ${report.pinned}.`];
66
+ if (!report.own) lines.push('No `pi` on PATH.');
67
+ else if (report.plan === 'current') lines.push(`Your pi is ${report.own}: the same release, used as it is.`);
68
+ else if (report.plan === 'private') lines.push(`Your pi is ${report.own}: kept as it is. Company sessions and \`piagent pi\` use a private Pi ${report.pinned}.`);
69
+ else lines.push(`Your pi is ${report.own}: older than ${report.pinned}; piagent-setup updates it.`);
70
+ lines.push(report.privateRoot ? `Private copy: ${report.privateRoot}` : 'Private copy: not installed');
71
+ return lines.join('\n');
72
+ }
73
+
74
+ async function main(argv) {
75
+ const [command, ...rest] = argv;
76
+ if ([undefined, 'help', '--help', '-h'].includes(command)) { process.stdout.write(usage()); return 0; }
77
+ if (command === 'status') {
78
+ const report = status();
79
+ process.stdout.write(rest.includes('--json') ? `${JSON.stringify(report, null, 2)}\n` : `${describe(report)}\n`);
80
+ return 0;
81
+ }
82
+ if (command === 'root') { process.stdout.write(`${activeHostRoot() ?? ''}\n`); return 0; }
83
+ if (command === 'ensure') {
84
+ const report = status(), dryRun = rest.includes('--dry-run');
85
+ if (report.plan !== 'private' && !rest.includes('--force')) { process.stdout.write(`${describe(report)}\n`); return 0; }
86
+ try {
87
+ const root = installPrivateHost({ dryRun, log: (line) => process.stdout.write(`${line}\n`) });
88
+ process.stdout.write(`${dryRun ? 'DRY RUN: ' : ''}Pi ${PI_HOST_VERSION} for Piagent: ${root}\n`);
89
+ if (report.own) process.stdout.write(`Your pi ${report.own} is unchanged.\n`);
90
+ return 0;
91
+ } catch (error) { process.stderr.write(`FAIL: ${error.message}\n`); return 1; }
92
+ }
93
+ if (command === 'run') {
94
+ const args = rest[0] === '--' ? rest.slice(1) : rest;
95
+ const root = activeHostRoot();
96
+ if (!root || JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version !== PI_HOST_VERSION) {
97
+ process.stderr.write(`FAIL: Pi ${PI_HOST_VERSION} is not installed for Piagent. Run: piagent pi-host ensure --force\n`);
98
+ return 1;
99
+ }
100
+ const child = spawn(process.execPath, [hostCli(root), ...args], { stdio: 'inherit' });
101
+ for (const signal of ['SIGTERM', 'SIGHUP']) process.on(signal, () => { try { child.kill(signal); } catch { /* gone */ } });
102
+ process.on('SIGINT', () => {});
103
+ return await new Promise((resolve) => child.once('exit', (code, signal) => resolve(signal ? 1 : code ?? 1)));
104
+ }
105
+ process.stderr.write(usage());
106
+ return 2;
107
+ }
108
+
109
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = await main(process.argv.slice(2));
@@ -26,7 +26,9 @@ const scriptByCommand = {
26
26
  "piagent-context": "scripts/context-engine.mjs",
27
27
  "piagent-explain": "scripts/explain-command.mjs",
28
28
  "piagent-webui": "scripts/piagent-webui-launcher.mjs",
29
- "piagent-dashboard": "scripts/piagent-dashboard.mjs"
29
+ "piagent-dashboard": "scripts/piagent-dashboard.mjs",
30
+ "piagent-pi-host": "scripts/pi-host.mjs",
31
+ "piagent-skills": "scripts/skill-sources.mjs"
30
32
  };
31
33
 
32
34
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -42,6 +44,10 @@ if (invokedAs === "piagent") {
42
44
  } else if (subcommand === "dashboard") {
43
45
  script = "scripts/piagent-dashboard.mjs";
44
46
  forwardedArgs = forwardedArgs.slice(1);
47
+ } else if (subcommand === "pi") {
48
+ // Pi itself, on the release Piagent is qualified on, whatever `pi` the member keeps.
49
+ script = "scripts/pi-host.mjs";
50
+ forwardedArgs = ["run", "--", ...forwardedArgs.slice(1)];
45
51
  } else if (subcommand === "explain") {
46
52
  script = "scripts/explain-command.mjs";
47
53
  forwardedArgs = forwardedArgs.slice(1);
@@ -64,6 +70,9 @@ if (invokedAs === "piagent") {
64
70
  console.log("Commands:");
65
71
  console.log(" dashboard Open and manage the local session hub");
66
72
  console.log(" studio Open the managed company agent imported by Agent Watch");
73
+ console.log(" pi Run Pi on the release Piagent is qualified on (your own pi stays as it is)");
74
+ console.log(" pi-host Show or install the Pi host Piagent runs");
75
+ console.log(" skills Add, list or remove skill sources such as mattpocock/skills");
67
76
  console.log(" explain Say why the guard would allow or block a shell command");
68
77
  console.log(" approve-verification Preview or approve an independent verification plan");
69
78
  console.log(" select-verification Preview reusable contracts for exact task criteria");
@@ -95,8 +104,10 @@ function benchmarkSourceRoot() {
95
104
  process.exit(1);
96
105
  }
97
106
 
98
- // The Pi host this member runs: the `pi` found on PATH, as its package root.
99
- function piHost() {
107
+ // The Pi host this member runs: the `pi` on PATH when it is the pinned release,
108
+ // else Piagent's private copy (a member keeping a newer Pi of their own).
109
+ async function piHost() {
110
+ try { return (await import("./pi-host.mjs")).activeHostRoot(); } catch { /* the `pi` on PATH, below */ }
100
111
  for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
101
112
  if (!path.isAbsolute(dir)) continue;
102
113
  let root;
@@ -113,7 +124,7 @@ function piHost() {
113
124
  // entrypoint, the Node running it and the Pi host. Source checkouts never
114
125
  // register, and a failure here never blocks the command. Asking for help or the
115
126
  // version changes nothing in the operator's agent directory.
116
- function recordRuntime() {
127
+ async function recordRuntime() {
117
128
  try {
118
129
  if (forwardedArgs.some((value) => ["--help", "-h", "--version", "-v"].includes(value))) return;
119
130
  if (!packageRoot.split(path.sep).includes("node_modules")) return;
@@ -129,7 +140,7 @@ function recordRuntime() {
129
140
  version: JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version,
130
141
  entrypoint: fs.realpathSync(path.join(packageRoot, "scripts", "piagent-studio.mjs")),
131
142
  node: fs.realpathSync(process.execPath),
132
- pi_sdk_root: piHost()
143
+ pi_sdk_root: await piHost()
133
144
  }, null, 2)}\n`;
134
145
  const file = path.join(agentDir, "piagent-runtime.json");
135
146
  try { if (fs.readFileSync(file, "utf8") === body) return; } catch { /* first run */ }
@@ -138,7 +149,7 @@ function recordRuntime() {
138
149
  fs.renameSync(temporary, file);
139
150
  } catch { /* optional: Agent Watch falls back to known install locations */ }
140
151
  }
141
- recordRuntime();
152
+ await recordRuntime();
142
153
 
143
154
  const sourceRoot = invokedAs === "piagent-benchmark" ? benchmarkSourceRoot() : packageRoot;
144
155
  const target = path.join(sourceRoot, script);
@@ -7,7 +7,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
8
  import { requestGatewayControl } from "../packages/piagent-webui/gateway/control-socket.ts";
9
9
  import { browserCommand } from "../packages/piagent-core/runtime/host/browser-opener.mjs";
10
- import { installedPiHostRoot } from "../packages/piagent-webui/gateway/pi-host.ts";
10
+ import { pinnedPiHostRoot } from "../packages/piagent-webui/gateway/pi-host.ts";
11
11
  import { gatewayProfileState, profileRef, readGatewayDescriptor, readOrCreateCatalogKey,
12
12
  removeGatewayDescriptor } from "../packages/piagent-webui/gateway/profile-state.ts";
13
13
  import { startPiagentGateway } from "../packages/piagent-webui/gateway/gateway-service.ts";
@@ -266,7 +266,7 @@ async function main() {
266
266
  const key = readOrCreateCatalogKey(state), descriptor = readGatewayDescriptor(state);
267
267
  let actual = null;
268
268
  try {
269
- const hostRoot = installedPiHostRoot();
269
+ const hostRoot = pinnedPiHostRoot(expectedPiVersion);
270
270
  actual = JSON.parse(fs.readFileSync(path.join(hostRoot, "package.json"), "utf8")).version;
271
271
  } catch { /* reported as a failed check below */ }
272
272
  let health = null;
@@ -24,6 +24,46 @@
24
24
  "decided": "2026-10-24",
25
25
  "until": "2026-11-07",
26
26
  "reason": "Subresource Integrity check bypassed with a non-lowercase algorithm token; Pi does not fetch with integrity metadata. Pinned by the Pi host as above; fixed in undici 8.12.2."
27
+ },
28
+ {
29
+ "id": "GHSA-4w7w-5xrc-9h26",
30
+ "package": "undici",
31
+ "node": "node_modules/undici",
32
+ "decided": "2026-10-28",
33
+ "until": "2026-11-07",
34
+ "reason": "Cache poisoning and cross-user disclosure through cross-origin redirects in undici's cache; Pi does not share an HTTP cache between users. Pinned by the Pi host as above (8.10.2 in 1.3.0 and 1.4.0); fixed in undici 8.12.2."
35
+ },
36
+ {
37
+ "id": "GHSA-2m5v-pch6-74hx",
38
+ "package": "undici",
39
+ "node": "node_modules/undici",
40
+ "decided": "2026-10-28",
41
+ "until": "2026-11-07",
42
+ "reason": "TLS certificate validation bypassed when an Agent drops its tls options; affects callers that pass custom tls settings to an undici Agent. Pinned by the Pi host as above; fixed in undici 8.12.2. Owner accepted on 2026-10-28 to keep releases going; revisit as soon as a Pi host carries undici >= 8.12.2."
43
+ },
44
+ {
45
+ "id": "GHSA-qrxm-fqhp-jwvm",
46
+ "package": "undici",
47
+ "node": "node_modules/undici",
48
+ "decided": "2026-10-28",
49
+ "until": "2026-11-07",
50
+ "reason": "Denial of service through a TLS 1.2 certificate-changing renegotiation by a hostile server; no data exposure. Pinned by the Pi host as above; fixed in undici 8.12.2."
51
+ },
52
+ {
53
+ "id": "GHSA-9fm3-97px-rjf6",
54
+ "package": "undici",
55
+ "node": "node_modules/undici",
56
+ "decided": "2026-10-28",
57
+ "until": "2026-11-07",
58
+ "reason": "Denial of service from unbounded automatic PONG buffering in the WebSocket client fed by a hostile server; no data exposure. Pinned by the Pi host as above; fixed in undici 8.12.2."
59
+ },
60
+ {
61
+ "id": "GHSA-55wc-3pg8-jfw6",
62
+ "package": "undici",
63
+ "node": "node_modules/undici",
64
+ "decided": "2026-10-28",
65
+ "until": "2026-11-07",
66
+ "reason": "Denial of service from an unhandled exception in the decompress interceptor; no data exposure. Pinned by the Pi host as above; fixed in undici 8.12.2."
27
67
  }
28
68
  ]
29
69
  }
package/scripts/setup.sh CHANGED
@@ -80,6 +80,7 @@ CONFIGURE_MODEL_SCOPE=true
80
80
  MODEL_SCOPE_PRESET="full"
81
81
  DEFAULT_MODEL="openai-codex/gpt-6-sol:high"
82
82
  AUTO_INSTALL_PI=true
83
+ PI_HOST_PRIVATE=false
83
84
  FORCE_PROFILE=false
84
85
  FORCE_SETTINGS=false
85
86
  SKIP_AGENTS=false
@@ -347,6 +348,19 @@ ensure_pi_cli() {
347
348
  if [[ "$current_version" == "$expected_pi_version" ]]; then
348
349
  return
349
350
  fi
351
+ # A newer pi is the member's own: it stays, and the pinned host is
352
+ # installed privately beside it (scripts/pi-host.mjs).
353
+ if [[ -n "$current_version" ]] && [[ "$(node -e 'const [a,b]=process.argv.slice(1).map(v=>v.split(/[+-]/)[0].split(".").map(Number)); for (let i=0;i<3;i++) if (a[i]!==b[i]) { process.stdout.write(a[i]>b[i]?"newer":"older"); process.exit(0); } process.stdout.write("same");' "$current_version" "$expected_pi_version")" == "newer" ]]; then
354
+ echo "Pi CLI $current_version is your own and stays as it is; Piagent runs on a private Pi $expected_pi_version."
355
+ local ensure_args=(ensure --force)
356
+ [[ "$DRY_RUN" == true ]] && ensure_args+=(--dry-run)
357
+ if ! node "$PLATFORM_ROOT/scripts/pi-host.mjs" "${ensure_args[@]}"; then
358
+ echo "FAIL: could not install the private Pi $expected_pi_version." >&2
359
+ exit 1
360
+ fi
361
+ PI_HOST_PRIVATE=true
362
+ return
363
+ fi
350
364
  if [[ "$AUTO_INSTALL_PI" == false ]]; then
351
365
  echo "FAIL: Pi Coding Agent $expected_pi_version is required; found ${current_version:-${current_output:-unknown}}. Remove --no-install-pi or upgrade it manually." >&2
352
366
  exit 1
@@ -490,7 +504,11 @@ echo "Next:"
490
504
  if [[ "$DO_PROJECT" == true ]]; then
491
505
  echo " cd \"$PROJECT_PATH\""
492
506
  fi
493
- echo " pi"
507
+ if [[ "$PI_HOST_PRIVATE" == true ]]; then
508
+ echo " piagent pi # Pi $(node -p 'require(process.argv[1]).peerDependencies["@earendil-works/pi-coding-agent"]' "$PLATFORM_ROOT/package.json"), the release Piagent is qualified on; your own pi also loads Piagent"
509
+ else
510
+ echo " pi"
511
+ fi
494
512
  echo " /login # first time only"
495
513
  if [[ "$DO_PROJECT" == true ]]; then
496
514
  echo " /model # or Ctrl+L: select provider/model from Pi selector"