@piagent/platform 1.18.0 → 1.19.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 (90) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/README.vi.md +1 -1
  4. package/docs/command-reference-vietnamese.md +2 -2
  5. package/docs/distribution-standard.md +6 -6
  6. package/docs/languages.json +2 -2
  7. package/docs/operator-manual-vietnamese.md +7 -7
  8. package/docs/publishing-for-teams.md +1 -0
  9. package/docs/release-install-policy.md +2 -2
  10. package/docs/team-onboarding.md +2 -2
  11. package/docs-site/architecture.html +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/content/en/index.html +4 -4
  15. package/docs-site/content/en/whats-new.html +7 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +7 -0
  18. package/docs-site/context-engine.html +1 -1
  19. package/docs-site/ecosystem.html +1 -1
  20. package/docs-site/en/architecture.html +1 -1
  21. package/docs-site/en/benchmark.html +1 -1
  22. package/docs-site/en/commands.html +1 -1
  23. package/docs-site/en/context-engine.html +1 -1
  24. package/docs-site/en/ecosystem.html +1 -1
  25. package/docs-site/en/index.html +5 -5
  26. package/docs-site/en/mcp-approval.html +1 -1
  27. package/docs-site/en/mcp-auth.html +1 -1
  28. package/docs-site/en/mcp-servers.html +1 -1
  29. package/docs-site/en/mcp.html +1 -1
  30. package/docs-site/en/permissions.html +1 -1
  31. package/docs-site/en/profiles.html +1 -1
  32. package/docs-site/en/quickstart.html +1 -1
  33. package/docs-site/en/runtime.html +1 -1
  34. package/docs-site/en/scope.html +1 -1
  35. package/docs-site/en/security.html +1 -1
  36. package/docs-site/en/team.html +1 -1
  37. package/docs-site/en/whats-new.html +8 -1
  38. package/docs-site/en/windows.html +1 -1
  39. package/docs-site/en/workflows.html +1 -1
  40. package/docs-site/index.html +5 -5
  41. package/docs-site/mcp-approval.html +1 -1
  42. package/docs-site/mcp-auth.html +1 -1
  43. package/docs-site/mcp-servers.html +1 -1
  44. package/docs-site/mcp.html +1 -1
  45. package/docs-site/permissions.html +1 -1
  46. package/docs-site/profiles.html +1 -1
  47. package/docs-site/quickstart.html +1 -1
  48. package/docs-site/runtime.html +1 -1
  49. package/docs-site/scope.html +1 -1
  50. package/docs-site/security.html +1 -1
  51. package/docs-site/team.html +1 -1
  52. package/docs-site/whats-new.html +8 -1
  53. package/docs-site/windows.html +1 -1
  54. package/docs-site/workflows.html +1 -1
  55. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  56. package/package.json +1 -1
  57. package/packages/piagent-core/managed/call-purpose.mjs +3 -2
  58. package/packages/piagent-core/managed/command-tools.mjs +59 -0
  59. package/packages/piagent-core/managed/container-engine.mjs +102 -0
  60. package/packages/piagent-core/managed/continuation.mjs +131 -0
  61. package/packages/piagent-core/managed/doctor.mjs +7 -0
  62. package/packages/piagent-core/managed/language-environment.mjs +57 -0
  63. package/packages/piagent-core/managed/linux-sandbox.mjs +3 -1
  64. package/packages/piagent-core/managed/patch-snapshot.mjs +25 -9
  65. package/packages/piagent-core/managed/repository-operation.mjs +20 -0
  66. package/packages/piagent-core/managed/review-snapshot-worker.mjs +11 -4
  67. package/packages/piagent-core/managed/sandbox-diagnostic.mjs +6 -1
  68. package/packages/piagent-core/managed/session.mjs +50 -62
  69. package/packages/piagent-core/managed/tool-boundary.mjs +21 -10
  70. package/packages/piagent-core/managed/tool-worker.mjs +3 -3
  71. package/packages/piagent-core/managed/workflow.mjs +20 -20
  72. package/packages/piagent-core/package.json +1 -1
  73. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-DlbuyiQ3.js → MarkdownMessage-nggiOt0N.js} +1 -1
  74. package/packages/piagent-webui/dist/client/assets/{index-BGxsZHB5.js → index-CM9cs-gP.js} +16 -14
  75. package/packages/piagent-webui/dist/client/index.html +1 -1
  76. package/packages/piagent-webui/gateway/company-relay.ts +14 -2
  77. package/packages/piagent-webui/gateway/gateway-service.ts +20 -8
  78. package/packages/piagent-webui/gateway/git-repositories.ts +46 -0
  79. package/packages/piagent-webui/gateway/managed-projection.ts +9 -2
  80. package/packages/piagent-webui/gateway/project-registry.ts +10 -6
  81. package/packages/piagent-webui/gateway/session-catalog.ts +13 -6
  82. package/packages/piagent-webui/server/loopback-server.ts +1 -1
  83. package/packages/piagent-webui/server/member-routes.ts +6 -3
  84. package/packages/piagent-webui/server/tool-call-summary.ts +3 -1
  85. package/packages/piagent-webui/server/transcript-projection.ts +20 -4
  86. package/packages/piagent-webui/shared/process-copy.ts +3 -1
  87. package/packages/piagent-webui/shared/turn-end.ts +68 -0
  88. package/schemas/piagent-webui/session-catalog-v1.schema.json +11 -1
  89. package/schemas/piagent-webui/transcript-v1.schema.json +23 -2
  90. package/scripts/company-terminal.mjs +15 -0
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · EN</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · EN</span>
34
34
  <nav class="language-switch" aria-label="Language">
35
35
  <a class="language-option" href="/whats-new" hreflang="vi" lang="vi">VI</a>
36
36
  <a class="language-option current" href="/en/whats-new" hreflang="en" lang="en" aria-current="true">EN</a>
@@ -104,6 +104,13 @@
104
104
  <h1>1.9.0: ask in plain language, the team's Harness checks</h1>
105
105
  <p>The task contract and workflow commands are gone. Company mode asks for checks and a review on its own; Stop, network commands on approval and clearer approval cards.</p>
106
106
  </div>
107
+ <section class="section">
108
+ <p class="section-kicker">v1.19.0 · 2026-10-08</p>
109
+ <h2 id="v1-19-0">Tasks with a checklist are carried to the end, and each turn says whether it finished</h2>
110
+ <p class="section-lead">In a company conversation, when the main agent stops while its checklist still has open steps (say, after STEP06 of the STEP22 you asked for) and the team's Harness requires a plan, the harness sends it on to the next open step, as if you had sent “continue”. It stops when the checklist is done, when you press Stop or write, when a round fails, or after 2 rounds in a row that finish no step. Every turn ends with one line: Done, Stopped midway with x of y steps open (and why), Turn stopped by a failure, or You stopped this turn, with Continue when the task is not finished; the conversation list shows it too. Steps you leave for later are marked deferred, never completed; “continue” takes up the open checklist. A turn whose change the agent committed is still checked and reviewed.</p>
111
+ <p class="section-lead">Docker in the company sandbox: when Docker runs on the machine (Docker Desktop, Colima, OrbStack…), the agent has <span class="inline-code">run_with_docker</span> to run one command you approve with internet and Docker, such as a test database with <span class="inline-code">docker compose up -d</span>, <span class="inline-code">docker build</span> or Testcontainers tests. Docker gets no registry credentials; Bypass still asks when a command or compose file mounts a folder outside the project. <span class="inline-code">./gradlew</span> and <span class="inline-code">./mvnw</span> reuse the Gradle and Maven already on the machine, without asking for network. A folder that holds several repositories (front end, back end…, each its own Git) shows “N repo”: click it to see each repository, its branch and changed files, and switch branches there; a full folder list makes room instead of failing to add a folder.</p>
112
+ </section>
113
+
107
114
  <section class="section">
108
115
  <p class="section-kicker">v1.18.0 · 2026-10-08</p>
109
116
  <h2 id="v1-18-0">Switch the Git branch from the dashboard, retries for passing failures, Claude Haiku 5.5</h2>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · EN</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · EN</span>
34
34
  <nav class="language-switch" aria-label="Language">
35
35
  <a class="language-option" href="/windows" hreflang="vi" lang="vi">VI</a>
36
36
  <a class="language-option current" href="/en/windows" hreflang="en" lang="en" aria-current="true">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · EN</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · EN</span>
34
34
  <nav class="language-switch" aria-label="Language">
35
35
  <a class="language-option" href="/workflows" hreflang="vi" lang="vi">VI</a>
36
36
  <a class="language-option current" href="/en/workflows" hreflang="en" lang="en" aria-current="true">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/" hreflang="en" lang="en">EN</a>
@@ -94,7 +94,7 @@
94
94
  <main>
95
95
  <div class="content-shell" data-article>
96
96
  <section class="hero">
97
- <p class="eyebrow">Piagent 1.18.0</p>
97
+ <p class="eyebrow">Piagent 1.19.0</p>
98
98
  <h1>Coding agent cho cả team, kiểm tra từng bước.</h1>
99
99
  <p>
100
100
  Một dashboard cho Terminal và trình duyệt. Dùng model bằng tài khoản của bạn, hoặc model của công ty qua Agent Watch và Agent Studio. Guard kiểm tra từng tool call; ở chế độ công ty, Harness của team tự yêu cầu chạy check, cho subagent review và báo kết quả ngay dưới câu trả lời.
@@ -108,7 +108,7 @@
108
108
  <div class="hero-actions">
109
109
  <a class="button primary" href="/quickstart">Bắt đầu</a>
110
110
  <a class="button" href="/ecosystem">Hệ sinh thái</a>
111
- <a class="button" href="/whats-new">Có gì mới trong 1.18.0</a>
111
+ <a class="button" href="/whats-new">Có gì mới trong 1.19.0</a>
112
112
  <a class="button" href="/commands">Xem command</a>
113
113
  </div>
114
114
  </section>
@@ -184,9 +184,9 @@ piagent-setup</code></pre>
184
184
  <h2 id="bat-dau-tu-dau">Bắt đầu từ đâu</h2>
185
185
  <div class="card-grid">
186
186
  <a class="doc-card" href="/whats-new">
187
- <span class="card-kicker">Bản 1.18.0</span>
187
+ <span class="card-kicker">Bản 1.19.0</span>
188
188
  <h3>Có gì mới</h3>
189
- <p>Đổi nhánh Git trên dashboard, tự thử lại khi lỗi tạm thời, Claude Haiku 5.5; ở 1.17.0: dashboard hiện nhánh Git của project; ở 1.16.3: dashboard cập nhật xong vẫn tự kiểm tra bản mới; ở 1.16.2: Terminal nói giống dashboard: hỏi cập nhật, /bypass, lỗi và ghi chú Harness bằng tiếng Việt; ở 1.16.1: bản mới được báo bằng dialog và kiểm tra mỗi giờ, tab dashboard không còn mất đăng nhập; ở 1.16.0: Bypass cho cuộc trò chuyện công ty, đổi Harness không làm gián đoạn; ở 1.15.2: cuộc trò chuyện dài tự thu gọn trở lại; ở 1.15.1: Studio thấy mỗi call của model dùng để làm gì, nhiều cuộc trò chuyện trong một folder không nhận nhầm thay đổi của nhau; ở 1.15.0: subagent bị từ chối nói đúng lý do; ở 1.14: cập nhật giữ model bạn đã chọn; ở 1.13: cuộc trò chuyện giữ chỗ đang đọc, chữ của agent có định dạng, Piagent trên Windows qua WSL2: sandbox công ty bằng bubblewrap và Agent Watch cho Windows; ở 1.12: @ chọn file và folder trên máy, skill và command dùng bằng /, nhánh cuộc trò chuyện công ty gửi được tin nhắn.</p>
189
+ <p>Task công ty có checklist được làm đến hết, cuối mỗi lượt báo rõ xong, dừng giữa chừng hay lỗi, Docker trong sandbox công ty, folder chứa nhiều repo; ở 1.18.0: đổi nhánh Git trên dashboard, tự thử lại khi lỗi tạm thời, Claude Haiku 5.5; ở 1.17.0: dashboard hiện nhánh Git của project; ở 1.16.3: dashboard cập nhật xong vẫn tự kiểm tra bản mới; ở 1.16.2: Terminal nói giống dashboard: hỏi cập nhật, /bypass, lỗi và ghi chú Harness bằng tiếng Việt; ở 1.16.1: bản mới được báo bằng dialog và kiểm tra mỗi giờ, tab dashboard không còn mất đăng nhập; ở 1.16.0: Bypass cho cuộc trò chuyện công ty, đổi Harness không làm gián đoạn; ở 1.15.2: cuộc trò chuyện dài tự thu gọn trở lại; ở 1.15.1: Studio thấy mỗi call của model dùng để làm gì, nhiều cuộc trò chuyện trong một folder không nhận nhầm thay đổi của nhau; ở 1.15.0: subagent bị từ chối nói đúng lý do; ở 1.14: cập nhật giữ model bạn đã chọn; ở 1.13: cuộc trò chuyện giữ chỗ đang đọc, chữ của agent có định dạng, Piagent trên Windows qua WSL2: sandbox công ty bằng bubblewrap và Agent Watch cho Windows; ở 1.12: @ chọn file và folder trên máy, skill và command dùng bằng /, nhánh cuộc trò chuyện công ty gửi được tin nhắn.</p>
190
190
  </a>
191
191
  <a class="doc-card" href="/ecosystem">
192
192
  <span class="card-kicker">Get started</span>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/mcp-approval" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/mcp-approval" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/mcp-auth" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/mcp-auth" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/mcp-servers" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/mcp-servers" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/mcp" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/mcp" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/permissions" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/permissions" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/profiles" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/profiles" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/quickstart" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/quickstart" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/runtime" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/runtime" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/scope" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/scope" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/security" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/security" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/team" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/team" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/whats-new" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/whats-new" hreflang="en" lang="en">EN</a>
@@ -104,6 +104,13 @@
104
104
  <h1>Bản 1.9.0: nói việc bằng lời, Harness của team kiểm tra</h1>
105
105
  <p>Bỏ task contract và lệnh workflow. Chế độ công ty tự yêu cầu check và review; nút Dừng, lệnh có mạng theo lần duyệt và thẻ duyệt dễ đọc hơn.</p>
106
106
  </div>
107
+ <section class="section">
108
+ <p class="section-kicker">v1.19.0 · 2026-10-08</p>
109
+ <h2 id="v1-19-0">Task có checklist được làm đến hết, và mỗi lượt báo rõ đã xong hay chưa</h2>
110
+ <p class="section-lead">Trong cuộc trò chuyện công ty, khi main agent tự dừng lúc checklist còn bước (ví dụ làm xong STEP06 rồi dừng, dù bạn giao đến STEP22) và Harness của team bắt buộc plan, Harness tự cho agent làm tiếp bước kế tiếp, như bạn gửi “tiếp tục”. Nó dừng khi checklist xong, khi bạn bấm Stop hoặc nhắn tin, khi gặp lỗi, hoặc sau 2 vòng liên tiếp không xong thêm bước nào. Cuối mỗi lượt có một dòng: Hoàn thành, Dừng giữa chừng còn x/y bước (kèm lý do), Lượt dừng vì lỗi, hoặc Bạn đã dừng lượt này, với nút Tiếp tục khi task chưa xong; danh sách cuộc trò chuyện cũng hiện trạng thái này. Bước bạn bảo để sau được đánh dấu “để sau”, không bao giờ bị ghi là xong; gửi “tiếp tục” là agent nhận lại checklist đang dở. Lượt mà agent đã commit vẫn được chạy check và review.</p>
111
+ <p class="section-lead">Sandbox công ty dùng được Docker: khi máy đang chạy Docker (Docker Desktop, Colima, OrbStack…), agent có <span class="inline-code">run_with_docker</span> để chạy một lệnh bạn duyệt với internet và Docker, ví dụ bật database cho test bằng <span class="inline-code">docker compose up -d</span>, <span class="inline-code">docker build</span> hay test Testcontainers. Docker không mang theo thông tin đăng nhập registry; Bypass vẫn hỏi khi lệnh hoặc file compose mount thư mục ngoài project. <span class="inline-code">./gradlew</span> và <span class="inline-code">./mvnw</span> dùng lại bản Gradle/Maven đã có trên máy nên không phải xin mạng. Folder chứa nhiều repo (FE, BE… mỗi folder một git) hiện “N repo”: bấm vào để xem từng repo, nhánh và số file thay đổi, rồi đổi nhánh trong repo đó; danh sách folder đầy thì tự bỏ folder đã mất hoặc lâu nhất, không còn báo “Không thể thêm thư mục”.</p>
112
+ </section>
113
+
107
114
  <section class="section">
108
115
  <p class="section-kicker">v1.18.0 · 2026-10-08</p>
109
116
  <h2 id="v1-18-0">Đổi nhánh Git trên dashboard, tự thử lại khi lỗi tạm thời, Claude Haiku 5.5</h2>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/windows" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/windows" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.18.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.19.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/workflows" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/workflows" hreflang="en" lang="en">EN</a>
@@ -256,8 +256,8 @@
256
256
  },
257
257
  {
258
258
  "path": "tests",
259
- "fileCount": 527,
260
- "sha256": "0af093aea0e49917dc63c09c71888c178bccfe842069d34d681ef7d04a61fba3"
259
+ "fileCount": 529,
260
+ "sha256": "bf1347157d8de533df6eca703df76ab279f60abd03535ff8ab798f6bb8f2c2a8"
261
261
  }
262
262
  ],
263
263
  "custodianComparisonRules": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/platform",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -25,14 +25,15 @@ export function jobTitle(task, title) {
25
25
  }
26
26
 
27
27
  // The headers of one call. `owner.nextPurpose` is set by the harness when it
28
- // sends the main agent back (checks, review findings, a missing plan).
28
+ // sends the main agent back (checks, review findings: "harness"; a checklist
29
+ // with open steps: "continue").
29
30
  export function purposeHeaders(owner, runtime, role, context) {
30
31
  const messages = context?.messages ?? [], last = messages.at(-1);
31
32
  let purpose, after = '';
32
33
  if (systemText(context).startsWith(SUMMARY_PROMPT)) purpose = 'summary';
33
34
  else if (last?.role === 'toolResult') { purpose = 'tool_step'; after = afterTools(messages.findLast(m => m.role === 'assistant')); }
34
35
  else if (role !== 'main') purpose = 'brief';
35
- else if (owner.nextPurpose === 'harness') { purpose = 'harness'; owner.nextPurpose = null; }
36
+ else if (['harness', 'continue'].includes(owner.nextPurpose)) { purpose = owner.nextPurpose; owner.nextPurpose = null; }
36
37
  else purpose = 'answer';
37
38
  const title = purpose === 'brief' ? owner.jobTitles?.get(runtime) : '';
38
39
  return { 'X-Agent-Purpose': purpose, ...(after ? { 'X-Agent-After': after } : {}), ...(title ? { 'X-Agent-Task': encodeURIComponent(title) } : {}) };
@@ -0,0 +1,59 @@
1
+ import { mustConfirm } from '../runtime/policy/bypass-policy.mjs';
2
+ import { dockerConfirmation } from './container-engine.mjs';
3
+ import { networkConfirmation } from './permission.mjs';
4
+
5
+ // Commands that leave the plain sandbox, each after the member approves that
6
+ // exact command: run_with_network (internet, a local server or browser) and,
7
+ // when this machine has a Docker engine, run_with_docker (the same plus the
8
+ // engine's socket). Bypass runs them without asking, except what must be
9
+ // confirmed: for Docker also a container given host folders outside the
10
+ // project, the engine's socket, or the host's processes or network.
11
+ const PARAMETERS = { type: 'object', properties: { command: { type: 'string', minLength: 1, maxLength: 4000 }, reason: { type: 'string', minLength: 1, maxLength: 300 }, timeout: { type: 'number', minimum: 1, maximum: 1800 } }, required: ['command', 'reason'], additionalProperties: false };
12
+
13
+ const KINDS = {
14
+ network: {
15
+ name: 'run_with_network', label: 'Run with network', scope: 'network-command-once', provider: 'network', riskClass: 'medium',
16
+ description: 'Run one shell command that needs the internet (package install, download, git pull of a public repository), or that starts a local server or a browser (end-to-end tests with Playwright\'s Chromium against a server on 127.0.0.1), after the user approves that exact command. Normal bash has no network and cannot listen on a port. Credentials (.npmrc, SSH keys, Keychain) stay unavailable, so private registries and git push are not possible here.',
17
+ allow: 'Run this exact command once, with internet access; a server it starts accepts connections while it runs.',
18
+ title: 'Chạy lệnh có internet', allowText: 'chạy đúng lệnh này một lần, có internet; server nó mở nhận kết nối trong lúc chạy.',
19
+ confirm: (self, command) => networkConfirmation(self, command),
20
+ },
21
+ docker: {
22
+ name: 'run_with_docker', label: 'Run with Docker', scope: 'docker-command-once', provider: 'docker', riskClass: 'high',
23
+ description: 'Run one shell command that needs the Docker engine of this machine: docker, docker compose (a database or service for tests), docker build, or tests that start containers themselves (Testcontainers), after the user approves that exact command. It has internet access too. Normal bash and run_with_network cannot reach Docker. Mount only folders inside the project. Registry credentials are unavailable: public images pull, private registries and docker push do not. Containers started in the background (-d, compose up -d) keep running after the command: stop them (docker compose down) when the work is done unless the member wants them kept.',
24
+ allow: 'Run this exact command once, with internet access and the Docker engine; a container it starts can read folders Docker shares from this machine and may keep running.',
25
+ title: 'Chạy lệnh dùng Docker', allowText: 'chạy đúng lệnh này một lần, có internet và Docker; container nó tạo có thể đọc thư mục Docker được chia sẻ trên máy và có thể tiếp tục chạy.',
26
+ confirm: async (self, command) => self.permission !== 'trusted-full-access' ? 'ask'
27
+ : await mustConfirm(command) ?? dockerConfirmation(command, { cwd: self.cwd, home: self.boundary.userHome }),
28
+ },
29
+ };
30
+
31
+ function commandTool(self, kind) {
32
+ const spec = KINDS[kind];
33
+ return { name: spec.name, label: spec.label, description: spec.description, parameters: PARAMETERS,
34
+ execute: async (id, args, signal, onUpdate, ctx) => {
35
+ const confirm = await spec.confirm(self, args.command);
36
+ const decision = confirm === null ? { allowed: true, consume: () => true } : await (await import('../runtime/inspection/approval-broker.ts')).piApprovalBroker.request({ cwd: self.cwd, rawSessionId: self.session.sessionManager.getSessionId(), toolCallId: id,
37
+ action: { kind: 'external-provider-action', preconditionClass: 'runtime-only', toolName: spec.name, rawAction: { command: args.command }, commandPreview: String(args.command),
38
+ targetPaths: [self.cwd], provider: spec.provider, urlOrigin: null, requestedScope: spec.scope, reason: (confirm === 'ask' ? String(args.reason) : `Bypass still asks: ${confirm}. ${args.reason}`).slice(0, 300), riskClass: spec.riskClass,
39
+ allowConsequence: spec.allow, denyConsequence: 'The command does not run; the agent is told you declined.' },
40
+ terminalConfirm: () => ctx?.ui?.confirm?.(spec.title, `${args.command}\n\nLý do: ${args.reason}${confirm === 'ask' ? '' : `\nBypass vẫn hỏi: ${confirm}`}\n\nĐồng ý: ${spec.allowText}\nTừ chối: lệnh không chạy, agent được báo bạn đã từ chối.`) ?? Promise.resolve(false),
41
+ unavailableFallback: 'terminal-confirm', recheck: () => !signal?.aborted && Boolean(self.grant) });
42
+ if (!decision.allowed || !decision.consume() || signal?.aborted) throw Error('managed-operation-denied');
43
+ let ok = false;
44
+ const before = await self.changedFiles();
45
+ try { const result = await self.boundary.invoke('bash', { command: args.command, ...(args.timeout ? { timeout: args.timeout } : {}) }, signal, onUpdate, ctx?.model, kind === 'docker' ? { docker: true } : { network: true }); ok = !result?.isError; return result; }
46
+ finally { await self.claimChanges(before); await self.run?.afterTool('bash', args, ok, () => self.digest()); await self.refreshReview(); }
47
+ } };
48
+ }
49
+
50
+ export function commandTools(self) {
51
+ return [commandTool(self, 'network'), ...(self.boundary.engine ? [commandTool(self, 'docker')] : [])];
52
+ }
53
+
54
+ // What the system prompt says about services: Docker when the machine has an
55
+ // engine (run_with_docker), else the member starts them.
56
+ export function servicesPrompt(boundary) {
57
+ if (boundary.engine) return ' Docker is available through run_with_docker (the member approves each command): a database or service for tests (docker compose up -d, then the tests through bash or run_with_network), docker build, or Testcontainers tests. Stop what you started when done unless the member wants it kept. The iOS Simulator is not available in the sandbox: ask the member to run those tests.';
58
+ return ' Docker is not running on this machine and the iOS Simulator is not available in the sandbox: ask the member to start those services (or Docker, after which a new conversation has run_with_docker) or run those tests.';
59
+ }
@@ -0,0 +1,102 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ // The Docker engine a company command may use once the member approves it
5
+ // (run_with_docker): its socket, the CLI and its plugins (compose, buildx).
6
+ // The socket is the only thing opened beyond an approved network command:
7
+ // containers run in the engine (Docker Desktop's VM, Colima, OrbStack, the
8
+ // Linux daemon, Podman's compatible socket), not in this sandbox.
9
+ // The CLI gets a configuration folder of its own in the sandbox's home: no
10
+ // registry credentials (credsStore, auths), so public images are pulled and
11
+ // private registries are not, as with .npmrc.
12
+ const isSocket = file => { try { return fs.statSync(file).isSocket(); } catch { return false; } };
13
+ const real = file => { try { return fs.realpathSync(file); } catch { return null; } };
14
+
15
+ function socketCandidates(userHome, env, system) {
16
+ const host = /^unix:\/\/(\/.+)$/.exec(env.DOCKER_HOST ?? '')?.[1];
17
+ const runtime = env.XDG_RUNTIME_DIR;
18
+ return [host, path.join(userHome, '.docker/run/docker.sock'), ...(system ? ['/var/run/docker.sock'] : []), path.join(userHome, '.colima/default/docker.sock'),
19
+ path.join(userHome, '.orbstack/run/docker.sock'), ...(runtime ? [path.join(runtime, 'docker.sock'), path.join(runtime, 'podman/podman.sock')] : [])].filter(Boolean);
20
+ }
21
+
22
+ // The member's CLI plugins (links into the app bundle on macOS).
23
+ function cliPlugins(userHome, system) {
24
+ const dirs = [path.join(userHome, '.docker/cli-plugins'), ...!system ? [] : ['/usr/local/lib/docker/cli-plugins', '/usr/libexec/docker/cli-plugins', '/usr/lib/docker/cli-plugins',
25
+ '/Applications/Docker.app/Contents/Resources/cli-plugins', '/opt/homebrew/lib/docker/cli-plugins']];
26
+ const plugins = new Map();
27
+ for (const dir of dirs) {
28
+ let names = [];
29
+ try { names = fs.readdirSync(dir); } catch { continue; }
30
+ for (const name of names) {
31
+ if (!/^docker-[a-z0-9-]+$/.test(name) || plugins.has(name)) continue;
32
+ const file = real(path.join(dir, name));
33
+ try { if (file && fs.statSync(file).isFile()) plugins.set(name, file); } catch { /* broken link */ }
34
+ }
35
+ }
36
+ return plugins;
37
+ }
38
+
39
+ // `system`: also the machine-wide socket and plugin folders (tests leave them out).
40
+ export function containerEngine({ userHome, env = process.env, paths = ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/Applications/Docker.app/Contents/Resources/bin'], system = true } = {}) {
41
+ const socket = socketCandidates(userHome, env, system).map(real).find(file => file && isSocket(file));
42
+ const cli = paths.map(dir => real(path.join(dir, 'docker'))).find(Boolean);
43
+ if (!socket || !cli) return null;
44
+ const plugins = cliPlugins(userHome, system);
45
+ // Read roots: the folders the CLI and plugins live in (/usr is open already).
46
+ const readRoots = [...new Set([cli, ...plugins.values()].map(file => {
47
+ const bundle = /^(\/Applications\/[^/]+\.app)\//.exec(file)?.[1];
48
+ return bundle ?? path.dirname(file);
49
+ }))].filter(dir => !/^\/(usr|bin|sbin|opt\/homebrew)(\/|$)/.test(dir));
50
+ return { socket, cli, plugins, readRoots };
51
+ }
52
+
53
+ // The CLI's configuration in the sandbox's home, and the environment that
54
+ // points it (and Testcontainers) at the engine.
55
+ export function engineEnvironment(engine, home) {
56
+ const config = path.join(home, '.docker');
57
+ fs.mkdirSync(path.join(config, 'cli-plugins'), { recursive: true, mode: 0o700 });
58
+ fs.writeFileSync(path.join(config, 'config.json'), '{}\n', { mode: 0o600 });
59
+ for (const [name, file] of engine.plugins) {
60
+ try { fs.symlinkSync(file, path.join(config, 'cli-plugins', name)); } catch { /* already there */ }
61
+ }
62
+ return {
63
+ DOCKER_HOST: `unix://${engine.socket}`, DOCKER_CONFIG: config, DOCKER_CLI_HINTS: 'false',
64
+ // Testcontainers' reaper mounts the engine's socket inside its VM, where
65
+ // Docker Desktop and Colima name it /var/run/docker.sock.
66
+ ...(process.platform === 'darwin' ? { TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE: '/var/run/docker.sock' } : {}),
67
+ };
68
+ }
69
+
70
+ // Why an approved-in-Bypass Docker command must still be asked about: host
71
+ // folders outside the project mounted into a container, the engine's own
72
+ // socket, or a container given the host's processes or network. The
73
+ // project's compose files are read for the same (a volume naming ~ or an
74
+ // absolute path outside the project). null when none applies.
75
+ const COMPOSE_FILES = ['compose.yaml', 'compose.yml', 'docker-compose.yaml', 'docker-compose.yml', 'compose.override.yaml', 'compose.override.yml', 'docker-compose.override.yaml', 'docker-compose.override.yml'];
76
+ function outside(source, cwd, home) {
77
+ if (!source || /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(source)) return false; // a named volume
78
+ const expanded = source.replace(/^~(?=\/|$)/, home).replace(/^\$\{?HOME\}?(?=\/|$)/, home);
79
+ if (/\$/.test(expanded)) return true;
80
+ const resolved = path.resolve(cwd, expanded);
81
+ return /docker\.sock$/.test(resolved) || !(resolved === cwd || resolved.startsWith(cwd + path.sep));
82
+ }
83
+ export function dockerConfirmation(command, { cwd, home }) {
84
+ const text = String(command);
85
+ if (/--privileged\b|--pid[= ]host\b|--network[= ]host\b|--net[= ]host\b|--userns[= ]host\b|--cap-add\b/.test(text)) return 'the container gets the host\'s processes, network or extra privileges';
86
+ const mounts = [...text.matchAll(/(?:^|\s)(?:-v|--volume)(?:=|\s+)(['"]?)([^\s'"]+)\1/g)].map(match => match[2].split(':')[0]);
87
+ for (const match of text.matchAll(/--mount(?:=|\s+)(['"]?)([^\s'"]+)\1/g)) {
88
+ const source = /(?:^|,)(?:source|src)=([^,]+)/.exec(match[2])?.[1];
89
+ if (/(?:^|,)type=bind\b/.test(match[2])) mounts.push(source ?? '');
90
+ }
91
+ if (mounts.some(source => outside(source, cwd, home))) return 'it mounts a folder outside the project into a container';
92
+ if (/\bcompose\b/.test(text)) {
93
+ for (const name of COMPOSE_FILES) {
94
+ let content;
95
+ try { content = fs.readFileSync(path.join(cwd, name), 'utf8'); } catch { continue; }
96
+ if (/^\s*(privileged:\s*true|pid:\s*["']?host|network_mode:\s*["']?host)/m.test(content)) return `${name} gives a container the host's processes, network or privileges`;
97
+ const sources = [...content.matchAll(/^\s*-\s*["']?([^\s"':]+):[^\s]/gm), ...content.matchAll(/^\s*source:\s*["']?([^\s"']+)/gm)].map(match => match[1]);
98
+ if (sources.some(source => /^(\/|~|\.|\$)/.test(source) && outside(source, cwd, home))) return `${name} mounts a folder outside the project into a container`;
99
+ }
100
+ }
101
+ return null;
102
+ }
@@ -0,0 +1,131 @@
1
+ import { unfinished, planText, currentPlan, workflowPolicy } from './workflow.mjs';
2
+ import { failureCode } from '../runtime/managed-failure.mjs';
3
+
4
+ // A main agent often ends its turn between the steps of its own checklist
5
+ // ("STEP06 done, STEP07 next"), though the member asked for all of them.
6
+ // Under plan "require" the harness sends it back to the next open step, as a
7
+ // new run (as if the member had sent "continue": each round gets its own
8
+ // helpers, checks and review), until the checklist is done, the member stops
9
+ // it, a round fails, or IDLE_ROUNDS rounds in a row change nothing.
10
+ export const IDLE_ROUNDS = 2;
11
+ // A checklist holds at most 30 steps: one round per step at most.
12
+ export const MAX_ROUNDS = 30;
13
+ // How the member's message ended, shown under the last answer.
14
+ export const TURN_END_ENTRY = 'agent-watch-turn-end';
15
+ export const TURN_END_STATES = ['done', 'midway', 'failed', 'cancelled'];
16
+
17
+ const completed = plan => plan?.plan?.filter(p => p.status === 'completed').length ?? 0;
18
+ const deferred = plan => plan?.plan?.filter(p => p.status === 'deferred').length ?? 0;
19
+
20
+ export class Continuation {
21
+ constructor(policy) {
22
+ Object.assign(this, { policy, rounds: 0, idle: 0, touched: false, reason: null, startDone: 0 });
23
+ }
24
+ // A round is about to start: what its progress is measured from.
25
+ begin(plan) { this.startDone = completed(plan); }
26
+ // A round ended (its run closed). The message that starts the next round,
27
+ // or null: then `reason` says why the harness stopped sending it back.
28
+ next({ plan, planUpdated, changed, stopped, failed }) {
29
+ if (planUpdated) this.touched = true;
30
+ if (stopped) { this.reason = 'stopped'; return null; }
31
+ if (failed) { this.reason = 'failed'; return null; }
32
+ if (!this.touched || !unfinished(plan)) { this.reason = null; return null; }
33
+ if (this.policy.plan !== 'require') { this.reason = 'policy'; return null; }
34
+ // The member's own round always gets one round back; a harness round
35
+ // that neither completed a step nor changed code counts as idle.
36
+ if (this.rounds > 0) this.idle = completed(plan) > this.startDone || changed ? 0 : this.idle + 1;
37
+ if (this.idle >= IDLE_ROUNDS) { this.reason = 'idle'; return null; }
38
+ if (this.rounds >= MAX_ROUNDS) { this.reason = 'limit'; return null; }
39
+ this.rounds += 1;
40
+ const steps = plan.plan.length, done = completed(plan), open = steps - done - deferred(plan);
41
+ return { customType: 'agent-watch-process', display: true,
42
+ content: `Harness: your checklist still has ${open} open step(s) (${done}/${steps} completed):\n${planText(plan)}\nThe member asked for all of it, unless they told you to stop at this point. Continue with the next open step now and keep going until every step is completed; update the checklist with update_plan as you go. Do not stop to report progress between steps. If the member asked you to stop here (to review a plan before any change, for example) or you cannot go on without them (a decision, access, a service that is not running), ask with ask_user instead of going on. If a step is no longer needed, take it out of the checklist and say why in your final answer.`,
43
+ details: { phase: 'continue', round: this.rounds, maxRounds: MAX_ROUNDS, planOpen: open, planDone: done, planSteps: steps, ...(this.idle ? { idle: this.idle } : {}) } };
44
+ }
45
+ }
46
+
47
+ // How a member's message ended: every step done (or answered without a
48
+ // checklist), stopped with steps open (and why the harness did not go on),
49
+ // failed (which role, which kind) or stopped by the member.
50
+ export function turnEnd(continuation, plan, { stopped, failure }) {
51
+ const steps = continuation.touched ? plan?.plan?.length ?? 0 : 0, done = continuation.touched ? completed(plan) : 0;
52
+ const state = stopped ? 'cancelled' : failure ? 'failed' : continuation.touched && unfinished(plan) ? 'midway' : 'done';
53
+ const later = continuation.touched ? deferred(plan) : 0;
54
+ return { state, planSteps: steps, planDone: done, ...(later ? { planDeferred: later } : {}), rounds: continuation.rounds,
55
+ ...(state === 'midway' && continuation.reason ? { reason: continuation.reason } : {}),
56
+ ...(state === 'failed' ? { role: failure.role ?? 'main', code: String(failure.code || 'unknown').toLowerCase().replace(/[^a-z0-9_]+/g, '_').slice(0, 64) } : {}),
57
+ at: new Date().toISOString() };
58
+ }
59
+
60
+ // One member message: its round, then the rounds the harness adds while the
61
+ // checklist has open steps. `runRound(text, deliver)` runs one Studio run and
62
+ // returns { result, preflight, planUpdated, changed, closeFailed }. A message the member
63
+ // sends while a round closes (no model runs then) becomes the next round;
64
+ // Stop ends the turn. Listeners hear "settled" once, after the last round.
65
+ // "tiếp tục", "continue", the dashboard's Continue button: the member asks
66
+ // for the open checklist, so it is this message's work even before the agent
67
+ // touches it again.
68
+ const CONTINUE = /^(tiep tuc|lam tiep|lam not|continue|go on|keep going|resume)\b/;
69
+ export function asksToContinue(text) {
70
+ const normal = String(text ?? '').normalize('NFD').replace(/\p{M}/gu, '').replace(/[đĐ]/g, 'd').toLowerCase().trim();
71
+ return normal.length <= 60 && CONTINUE.test(normal);
72
+ }
73
+ const fresh = (self, manager, text) => {
74
+ const continuation = new Continuation(workflowPolicy(self.manifest));
75
+ if (asksToContinue(text) && unfinished(currentPlan(manager))) continuation.touched = true;
76
+ return continuation;
77
+ };
78
+
79
+ export async function memberTurn(self, { manager, prompt, runRound }, text, deliver) {
80
+ let continuation = fresh(self, manager, text), result, waiting = null, ended = null;
81
+ self.stopRequested = false; self.queued = [];
82
+ try {
83
+ for (let first = true; ; first = false) {
84
+ continuation.begin(currentPlan(manager));
85
+ self.between = false;
86
+ let round;
87
+ try { round = await runRound(text, deliver); }
88
+ catch (error) { waiting?.reject(error); waiting = null; throw error; }
89
+ if (first) result = round.result; else waiting?.resolve(round.result);
90
+ waiting = null;
91
+ const last = self.session.messages.findLast(m => m.role === 'assistant');
92
+ const stopped = self.stopRequested || last?.stopReason === 'aborted';
93
+ const failure = round.preflight || last?.stopReason === 'error' ? { role: 'main', code: failureCode(last?.errorMessage ?? '') }
94
+ : round.closeFailed ? { role: 'main', code: round.closeFailed } : null;
95
+ const plan = currentPlan(manager), message = continuation.next({ plan, planUpdated: round.planUpdated, changed: round.changed, stopped, failed: !!failure });
96
+ if (self.queued.length) {
97
+ // The member wrote while the round closed: theirs is the next round,
98
+ // a new request (Stop before it applied to the round before).
99
+ waiting = self.queued.shift(); self.stopRequested = false;
100
+ const next = waiting; text = next.text;
101
+ deliver = () => prompt(next.text, { ...next.options, expandPromptTemplates: false });
102
+ continuation = fresh(self, manager, next.text);
103
+ } else if (message) {
104
+ text = message.content;
105
+ deliver = () => { self.nextPurpose = 'continue'; return self.session.sendCustomMessage(message, { triggerTurn: true }); };
106
+ } else { ended = turnEnd(continuation, plan, { stopped, failure }); break; }
107
+ }
108
+ } finally {
109
+ self.between = false;
110
+ for (const queued of self.queued.splice(0)) queued.reject(Error('managed-turn-stopped'));
111
+ // How the member's message ended: an entry of the conversation (not a
112
+ // message: the model never reads it), shown under the last answer, and
113
+ // told to listeners (the company Terminal).
114
+ if (ended) {
115
+ try { manager.appendCustomEntry(TURN_END_ENTRY, ended); } catch { /* the answer stands */ }
116
+ self.notify({ type: 'managed_turn_end', end: ended, text: turnEndText(ended) });
117
+ }
118
+ self.activePrompt = false;
119
+ self.notify({ type: 'agent_settled' });
120
+ }
121
+ return result;
122
+ }
123
+
124
+ // The plain text of a turn's end (the Terminal shows it; the WebUI reads details).
125
+ export function turnEndText(end) {
126
+ if (end.state === 'cancelled') return 'Turn stopped by the member.';
127
+ if (end.state === 'failed') return `Turn failed: ${end.role} agent, ${end.code}. Send "continue" to go on.`;
128
+ if (end.state === 'midway') return `Turn ended with ${end.planSteps - end.planDone - (end.planDeferred ?? 0)} of ${end.planSteps} checklist step(s) open${end.reason === 'idle' ? `: the agent stopped ${IDLE_ROUNDS} times in a row without progress` : end.reason === 'limit' ? `: ${MAX_ROUNDS} rounds reached` : ''}. Send "continue" to go on.`;
129
+ if (end.planDeferred) return `Done: ${end.planDone} of ${end.planSteps} checklist steps completed, ${end.planDeferred} left for later.`;
130
+ return end.planSteps ? `Done: all ${end.planSteps} checklist steps completed.` : 'Done.';
131
+ }
@@ -86,6 +86,13 @@ export async function runDoctor({ sdkRoot, origin, broker, network = true }) {
86
86
  try { return await bash('node loopback.mjs'); }
87
87
  catch (error) { throw Error(/loopback-blocked/.test(String(error?.message)) ? 'a local proxy listens on this Mac: tests that use localhost need run_with_network' : first(String(error?.message))); }
88
88
  });
89
+ // run_with_docker: the engine answers through its socket, and only there.
90
+ await check('Docker for approved commands (run_with_docker)', false, async () => {
91
+ if (!boundary.engine) throw Error('no Docker engine running (Docker Desktop, Colima, OrbStack…): start it, then open a new conversation');
92
+ let refused = false; try { await bash('docker version --format "{{.Server.Version}}"'); } catch { refused = true; }
93
+ if (!refused) throw Error('a plain command reached the Docker engine');
94
+ return `engine ${await bash('docker version --format "{{.Server.Version}}"', { docker: true })}`;
95
+ });
89
96
  await check('package caches kept for the project', false, async () => { await bash('mkdir -p "$npm_config_cache" && touch "$npm_config_cache/.piagent-doctor"'); return boundary.packageCache; });
90
97
  // The languages installed on this Mac, each started once in the sandbox.
91
98
  // (macOS ships stubs for some, such as javac without a JDK: a probe decides.)