@piagent/platform 1.51.1 → 1.52.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 +28 -0
  2. package/docs/command-reference-vietnamese.md +2 -2
  3. package/docs/distribution-standard.md +6 -6
  4. package/docs/harness-conformance.md +13 -13
  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 +2 -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 +15 -15
  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 +54 -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 +15 -15
  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 +65 -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 +54 -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 +1 -1
  56. package/packages/piagent-core/managed/check-questions.mjs +2 -46
  57. package/packages/piagent-core/managed/copies.mjs +10 -10
  58. package/packages/piagent-core/managed/general-clone.mjs +111 -0
  59. package/packages/piagent-core/managed/helper-roles.mjs +33 -42
  60. package/packages/piagent-core/managed/helper-run.mjs +21 -15
  61. package/packages/piagent-core/managed/helper-settlement.mjs +6 -10
  62. package/packages/piagent-core/managed/language-environment.mjs +6 -2
  63. package/packages/piagent-core/managed/ledger.mjs +11 -10
  64. package/packages/piagent-core/managed/objections.mjs +8 -25
  65. package/packages/piagent-core/managed/role-names.mjs +6 -0
  66. package/packages/piagent-core/managed/session.mjs +13 -13
  67. package/packages/piagent-core/managed/test-scope.mjs +170 -0
  68. package/packages/piagent-core/managed/tool-boundary.mjs +4 -4
  69. package/packages/piagent-core/managed/workflow.mjs +2 -39
  70. package/packages/piagent-core/package.json +1 -1
  71. package/packages/piagent-core/runtime/managed-failure.d.mts +1 -1
  72. package/packages/piagent-core/runtime/managed-failure.mjs +3 -3
  73. package/packages/piagent-webui/dist/client/assets/{App-C9tlGPMb.js → App-DQwj6l-F.js} +1 -1
  74. package/packages/piagent-webui/dist/client/assets/{GatewayApp-Dvo96qoX.js → GatewayApp-CP8GpZsK.js} +3 -3
  75. package/packages/piagent-webui/dist/client/assets/{InspectionApp-CGgHsOyC.js → InspectionApp-BZqu1pjw.js} +1 -1
  76. package/packages/piagent-webui/dist/client/assets/{SessionAgentWorkspace-n8UMMcrP.js → SessionAgentWorkspace-4nJSbzE9.js} +1 -1
  77. package/packages/piagent-webui/dist/client/assets/{SessionInspectorDrawer-B62kkr8J.js → SessionInspectorDrawer-DEb_TBE8.js} +2 -2
  78. package/packages/piagent-webui/dist/client/assets/SettingsNavigation-BF4X5xkR.js +3 -0
  79. package/packages/piagent-webui/dist/client/assets/{SettingsPage-DKg0SfZQ.js → SettingsPage-CS8LsdhS.js} +1 -1
  80. package/packages/piagent-webui/dist/client/assets/{TerminalPanel-BPTVsNjP.js → TerminalPanel-CiF2_woF.js} +1 -1
  81. package/packages/piagent-webui/dist/client/assets/{WorkspaceCodeEditor-BHGFFBc2.js → WorkspaceCodeEditor-OFxSEbBs.js} +1 -1
  82. package/packages/piagent-webui/dist/client/assets/{WorkspaceExplorer-nd4kghwK.js → WorkspaceExplorer-ZYIi-FWr.js} +3 -3
  83. package/packages/piagent-webui/dist/client/assets/{index-BiLVX8S-.js → index-CfJC4Unf.js} +2 -2
  84. package/packages/piagent-webui/dist/client/index.html +1 -1
  85. package/packages/piagent-webui/server/transcript-projection.ts +3 -3
  86. package/packages/piagent-webui/shared/company-copy.ts +3 -1
  87. package/packages/piagent-webui/shared/turn-end.ts +1 -1
  88. package/schemas/piagent-webui/transcript-v1.schema.json +3 -3
  89. package/scripts/runtime-advisory-exceptions.json +27 -27
  90. package/packages/piagent-webui/dist/client/assets/SettingsNavigation-BZijmuPt.js +0 -3
@@ -1,4 +1,69 @@
1
1
  [
2
+ {
3
+ "version": "1.52.0",
4
+ "date": "2026-11-08",
5
+ "status": "stable",
6
+ "title": {
7
+ "en": "Company helpers get an advisor and a general helper that works in its own clone",
8
+ "vi": "Helper công ty có thêm advisor và general làm việc trong bản clone riêng"
9
+ },
10
+ "summary": {
11
+ "en": "The company Harness declares every helper, and the main agent now calls them by working names: explore, scout, advisor, general and review. General changes code only in its own clone and hands back a git ref to merge.",
12
+ "vi": "Harness công ty khai báo mọi helper, và agent chính gọi chúng bằng tên công việc: explore, scout, advisor, general và review. General chỉ sửa code trong bản clone riêng và trả về một git ref để merge."
13
+ },
14
+ "requires": {
15
+ "en": "Agent Studio 0.51.0 and Agent Watch 0.15.20 (Windows 0.1.19) for the advisor and general helpers",
16
+ "vi": "Agent Studio 0.51.0 và Agent Watch 0.15.20 (Windows 0.1.19) để dùng helper advisor và general"
17
+ },
18
+ "items": [
19
+ {
20
+ "kind": "new",
21
+ "en": "advisor gives a second opinion before a hard decision: a recommendation, the options it rejected and the risks.",
22
+ "vi": "advisor cho ý kiến thứ hai trước quyết định khó: khuyến nghị, các phương án đã loại và rủi ro."
23
+ },
24
+ {
25
+ "kind": "new",
26
+ "en": "general takes large work on macOS: it edits, runs checks and commits in its own clone, never your files, and returns refs/general/<id>.",
27
+ "vi": "general nhận việc lớn trên macOS: sửa, chạy check và commit trong clone riêng, không đụng file của bạn, rồi trả về refs/general/<id>."
28
+ },
29
+ {
30
+ "kind": "breaking",
31
+ "en": "Helpers are called explore, scout, advisor, general and review; the old names research and verify are gone, and the verify helper is removed.",
32
+ "vi": "Helper được gọi là explore, scout, advisor, general và review; tên cũ research và verify không còn, helper verify bị bỏ."
33
+ },
34
+ {
35
+ "kind": "fixed",
36
+ "en": "The WebUI names an advisor or general failure correctly instead of calling it a review failure.",
37
+ "vi": "WebUI ghi đúng lỗi của advisor hay general thay vì gọi là lỗi của review."
38
+ }
39
+ ]
40
+ },
41
+ {
42
+ "version": "1.51.2",
43
+ "date": "2026-11-03",
44
+ "status": "stable",
45
+ "title": {
46
+ "en": "Running only the tests that cover a change works in every test tool",
47
+ "vi": "Chạy riêng phần test liên quan dùng được với mọi công cụ test"
48
+ },
49
+ "summary": {
50
+ "en": "When you chose to run only the related tests, the harness took many single-test commands for the whole suite and refused them. It now reads one file, filter or module the same way in each tool.",
51
+ "vi": "Khi bạn chọn chỉ chạy phần test liên quan, harness hiểu nhầm nhiều lệnh chạy một test là chạy toàn bộ rồi chặn lại. Giờ lệnh chạy một file, một bộ lọc hay một module được đọc đúng ở từng công cụ."
52
+ },
53
+ "requires": null,
54
+ "items": [
55
+ {
56
+ "kind": "fixed",
57
+ "en": "Maven -Dtest and -Dit.test before the goal, Gradle --tests and :module:test, node --import tsx --test, flutter, cargo test NAME and others now count as part of the suite.",
58
+ "vi": "Maven -Dtest và -Dit.test đặt trước goal, Gradle --tests và :module:test, node --import tsx --test, flutter, cargo test TÊN và các lệnh khác giờ được tính là một phần."
59
+ },
60
+ {
61
+ "kind": "fixed",
62
+ "en": "Commands that only compile or list tests (test-compile, -DskipTests, --listTests) no longer count as a test run, and a line that also runs the whole suite still asks.",
63
+ "vi": "Lệnh chỉ biên dịch hay liệt kê test (test-compile, -DskipTests, --listTests) không còn bị tính là chạy test; dòng lệnh có kèm chạy toàn bộ vẫn được hỏi."
64
+ }
65
+ ]
66
+ },
2
67
  {
3
68
  "version": "1.51.1",
4
69
  "date": "2026-11-03",
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.51.1" title="v1.51.1 docs · VI"><span class="dot"></span>v1.51.1</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.52.0" title="v1.52.0 docs · VI"><span class="dot"></span>v1.52.0</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new">Changelog</a>
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.51.1" title="v1.51.1 docs · VI"><span class="dot"></span>v1.51.1</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.52.0" title="v1.52.0 docs · VI"><span class="dot"></span>v1.52.0</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new">Changelog</a>
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.51.1" title="v1.51.1 docs · VI"><span class="dot"></span>v1.51.1</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.52.0" title="v1.52.0 docs · VI"><span class="dot"></span>v1.52.0</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new">Changelog</a>
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.51.1" title="v1.51.1 docs · VI"><span class="dot"></span>v1.51.1</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.52.0" title="v1.52.0 docs · VI"><span class="dot"></span>v1.52.0</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new">Changelog</a>
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.51.1" title="v1.51.1 docs · VI"><span class="dot"></span>v1.51.1</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.52.0" title="v1.52.0 docs · VI"><span class="dot"></span>v1.52.0</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new">Changelog</a>
@@ -153,9 +153,9 @@
153
153
  <h2 id="pin-cho-team">Pin một bản cho cả team</h2>
154
154
  <p>Khi rollout cho team, cài đúng bản Pi host và đúng bản platform, rồi chạy installer ở chế độ stable. Bước <code>--dry-run</code> in ra những gì sẽ đổi.</p>
155
155
  <div class="command-card">
156
- <header><span>Cài pin theo bản</span><button class="clip-btn" data-clip="npm install -g --ignore-scripts @earendil-works/pi-coding-agent@1.3.0&#10;npm install -g --ignore-scripts @piagent/platform@1.51.1&#10;piagent-install --stable --dry-run&#10;piagent-install --stable">Lấy lệnh</button></header>
156
+ <header><span>Cài pin theo bản</span><button class="clip-btn" data-clip="npm install -g --ignore-scripts @earendil-works/pi-coding-agent@1.3.0&#10;npm install -g --ignore-scripts @piagent/platform@1.52.0&#10;piagent-install --stable --dry-run&#10;piagent-install --stable">Lấy lệnh</button></header>
157
157
  <pre><code>npm install -g --ignore-scripts @earendil-works/pi-coding-agent@1.3.0
158
- npm install -g --ignore-scripts @piagent/platform@1.51.1
158
+ npm install -g --ignore-scripts @piagent/platform@1.52.0
159
159
  piagent-install --stable --dry-run
160
160
  piagent-install --stable</code></pre>
161
161
  </div>
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.51.1" title="v1.51.1 docs · VI"><span class="dot"></span>v1.51.1</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.52.0" title="v1.52.0 docs · VI"><span class="dot"></span>v1.52.0</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new" aria-current="page">Changelog</a>
@@ -108,7 +108,32 @@
108
108
  <div class="release-filter" role="toolbar" aria-label="Lọc theo loại thay đổi"><button type="button" data-filter-kind="all" aria-pressed="true">Tất cả</button><button type="button" data-filter-kind="new" aria-pressed="false">Mới</button><button type="button" data-filter-kind="improved" aria-pressed="false">Cải thiện</button><button type="button" data-filter-kind="fixed" aria-pressed="false">Sửa lỗi</button><button type="button" data-filter-kind="security" aria-pressed="false">Bảo mật</button></div>
109
109
  <section class="section releases" data-releases>
110
110
  <h2 id="latest" class="sr-only">Mới nhất</h2>
111
- <article class="release latest" id="v1-51-1" data-release>
111
+ <article class="release latest" id="v1-52-0" data-release>
112
+ <div class="release-meta"><a class="release-version" href="#v1-52-0">v1.52.0</a><time class="release-date" datetime="2026-11-08">08/11/2026</time></div>
113
+ <div class="release-body">
114
+ <h3 data-toc-skip id="v1-52-0-title">Helper công ty có thêm advisor và general làm việc trong bản clone riêng</h3>
115
+ <p>Harness công ty khai báo mọi helper, và agent chính gọi chúng bằng tên công việc: explore, scout, advisor, general và review. General chỉ sửa code trong bản clone riêng và trả về một git ref để merge.</p>
116
+ <ul class="release-items">
117
+ <li data-kind="breaking"><span class="kind kind-breaking">Thay đổi lớn</span><span>Helper được gọi là explore, scout, advisor, general và review; tên cũ research và verify không còn, helper verify bị bỏ.</span></li>
118
+ <li data-kind="new"><span class="kind kind-new">Mới</span><span>advisor cho ý kiến thứ hai trước quyết định khó: khuyến nghị, các phương án đã loại và rủi ro.</span></li>
119
+ <li data-kind="new"><span class="kind kind-new">Mới</span><span>general nhận việc lớn trên macOS: sửa, chạy check và commit trong clone riêng, không đụng file của bạn, rồi trả về refs/general/&lt;id&gt;.</span></li>
120
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>WebUI ghi đúng lỗi của advisor hay general thay vì gọi là lỗi của review.</span></li>
121
+ </ul>
122
+ <p class="release-requires"><strong>Cần:</strong> Agent Studio 0.51.0 và Agent Watch 0.15.20 (Windows 0.1.19) để dùng helper advisor và general</p>
123
+ </div>
124
+ </article>
125
+ <article class="release" id="v1-51-2" data-release>
126
+ <div class="release-meta"><a class="release-version" href="#v1-51-2">v1.51.2</a><time class="release-date" datetime="2026-11-03">03/11/2026</time></div>
127
+ <div class="release-body">
128
+ <h3 data-toc-skip id="v1-51-2-title">Chạy riêng phần test liên quan dùng được với mọi công cụ test</h3>
129
+ <p>Khi bạn chọn chỉ chạy phần test liên quan, harness hiểu nhầm nhiều lệnh chạy một test là chạy toàn bộ rồi chặn lại. Giờ lệnh chạy một file, một bộ lọc hay một module được đọc đúng ở từng công cụ.</p>
130
+ <ul class="release-items">
131
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Maven -Dtest và -Dit.test đặt trước goal, Gradle --tests và :module:test, node --import tsx --test, flutter, cargo test TÊN và các lệnh khác giờ được tính là một phần.</span></li>
132
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Lệnh chỉ biên dịch hay liệt kê test (test-compile, -DskipTests, --listTests) không còn bị tính là chạy test; dòng lệnh có kèm chạy toàn bộ vẫn được hỏi.</span></li>
133
+ </ul>
134
+ </div>
135
+ </article>
136
+ <article class="release" id="v1-51-1" data-release>
112
137
  <div class="release-meta"><a class="release-version" href="#v1-51-1">v1.51.1</a><time class="release-date" datetime="2026-11-03">03/11/2026</time></div>
113
138
  <div class="release-body">
114
139
  <h3 data-toc-skip id="v1-51-1-title">Task để chạy qua đêm không còn đứng chờ câu trả lời tới sáng</h3>
@@ -397,40 +422,38 @@
397
422
  </ul>
398
423
  </div>
399
424
  </article>
400
- <article class="release" id="v1-44-0" data-release>
401
- <div class="release-meta"><a class="release-version" href="#v1-44-0">v1.44.0</a><time class="release-date" datetime="2026-10-28">28/10/2026</time></div>
402
- <div class="release-body">
403
- <h3 data-toc-skip id="v1-44-0-title">Giữ Pi của bạn, chọn giao diện Terminal, thêm repo skill <span class="tag">không phát hành</span></h3>
404
- <p>Đã tag nhưng chưa publish: advisory undici mới trong Pi host làm kiểm tra runtime fail. Thay bằng 1.44.1.</p>
405
- <ul class="release-items">
406
- <li data-kind="new"><span class="kind kind-new">Mới</span><span>/style chọn giao diện Terminal: system, dark, light của Pi hoặc ocean, paper, contrast, focus của Piagent. Terminal công ty nhớ lựa chọn.</span></li>
407
- <li data-kind="new"><span class="kind kind-new">Mới</span><span>piagent skills add mattpocock/skills thêm repo skill GitHub ghim một commit; skill dùng được dạng /skill:tên trong Pi, Terminal công ty và dashboard.</span></li>
408
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Setup, install, update không còn hạ pi mới hơn của dev (vd 1.4.0) về bản pin 1.3.0: Piagent giữ bản riêng trong ~/.piagent/pi-host cho phiên công ty, dashboard và lệnh piagent pi.</span></li>
409
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>piagent-uninstall --with-host gỡ bản Pi riêng của Piagent và giữ nguyên pi mới hơn của dev.</span></li>
410
- </ul>
411
- </div>
412
- </article>
413
- <article class="release" id="v1-43-9" data-release>
414
- <div class="release-meta"><a class="release-version" href="#v1-43-9">v1.43.9</a><time class="release-date" datetime="2026-10-27">27/10/2026</time></div>
415
- <div class="release-body">
416
- <h3 data-toc-skip id="v1-43-9-title">Đủ skill đã cài, và mở file agent gửi lại</h3>
417
- <p>Skill trong plugin Claude và Codex, như superpowers, đến được agent; file agent gửi lại mở ngay trong web; hộp chọn thư mục chạy trên Windows.</p>
418
- <ul class="release-items">
419
- <li data-kind="new"><span class="kind kind-new">Mới</span><span>File agent gắn link trong câu trả lời mở ngay trong explorer: code đúng dòng, ảnh hiện hình, PDF và file office hiện nội dung chữ.</span></li>
420
- <li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Skill và lệnh trong plugin Claude và Codex của dev được liệt kê và đọc được, lấy bản mới nhất; gõ /plugin:skill cũng được.</span></li>
421
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Chọn thư mục project mở lại được hộp chọn thư mục của Windows, kể cả khi dashboard chạy nền.</span></li>
422
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Lượt chỉ sửa tài liệu hay file Spec Kit không bị harness bắt chạy check của repo, và approval cho check đã bị từ chối thì không bị hỏi lại.</span></li>
423
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Câu trả lời agent lặp lại sau mỗi lần harness trả về chỉ hiện một lần, không lặp theo từng vòng.</span></li>
424
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Trên Linux và Windows (WSL2), check tìm được JDK ngoài PATH: SDKMAN, /usr/lib/jvm hoặc ~/.jdks.</span></li>
425
- </ul>
426
- </div>
427
- </article>
428
425
  </section>
429
426
  <section class="section archive">
430
427
  <h2 id="archive">Các bản trước</h2>
431
428
  <p class="section-lead">Bản cũ hơn, gom theo tháng. Mở một tháng để xem từng thay đổi.</p>
432
429
  <details class="release-archive">
433
- <summary><span>Tháng 10/2026</span><span class="archive-count">73</span></summary>
430
+ <summary><span>Tháng 10/2026</span><span class="archive-count">75</span></summary>
431
+ <div class="archive-row" id="v1-44-0">
432
+ <div class="archive-head"><a class="release-version" href="#v1-44-0">v1.44.0</a> <span class="release-date">28/10/2026</span></div>
433
+ <div class="archive-body">
434
+ <p><strong>Giữ Pi của bạn, chọn giao diện Terminal, thêm repo skill</strong> <span class="tag">không phát hành</span> — Đã tag nhưng chưa publish: advisory undici mới trong Pi host làm kiểm tra runtime fail. Thay bằng 1.44.1.</p>
435
+ <ul class="release-items">
436
+ <li data-kind="new"><span class="kind kind-new">Mới</span><span>/style chọn giao diện Terminal: system, dark, light của Pi hoặc ocean, paper, contrast, focus của Piagent. Terminal công ty nhớ lựa chọn.</span></li>
437
+ <li data-kind="new"><span class="kind kind-new">Mới</span><span>piagent skills add mattpocock/skills thêm repo skill GitHub ghim một commit; skill dùng được dạng /skill:tên trong Pi, Terminal công ty và dashboard.</span></li>
438
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Setup, install, update không còn hạ pi mới hơn của dev (vd 1.4.0) về bản pin 1.3.0: Piagent giữ bản riêng trong ~/.piagent/pi-host cho phiên công ty, dashboard và lệnh piagent pi.</span></li>
439
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>piagent-uninstall --with-host gỡ bản Pi riêng của Piagent và giữ nguyên pi mới hơn của dev.</span></li>
440
+ </ul>
441
+ </div>
442
+ </div>
443
+ <div class="archive-row" id="v1-43-9">
444
+ <div class="archive-head"><a class="release-version" href="#v1-43-9">v1.43.9</a> <span class="release-date">27/10/2026</span></div>
445
+ <div class="archive-body">
446
+ <p><strong>Đủ skill đã cài, và mở file agent gửi lại</strong> — Skill trong plugin Claude và Codex, như superpowers, đến được agent; file agent gửi lại mở ngay trong web; hộp chọn thư mục chạy trên Windows.</p>
447
+ <ul class="release-items">
448
+ <li data-kind="new"><span class="kind kind-new">Mới</span><span>File agent gắn link trong câu trả lời mở ngay trong explorer: code đúng dòng, ảnh hiện hình, PDF và file office hiện nội dung chữ.</span></li>
449
+ <li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Skill và lệnh trong plugin Claude và Codex của dev được liệt kê và đọc được, lấy bản mới nhất; gõ /plugin:skill cũng được.</span></li>
450
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Chọn thư mục project mở lại được hộp chọn thư mục của Windows, kể cả khi dashboard chạy nền.</span></li>
451
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Lượt chỉ sửa tài liệu hay file Spec Kit không bị harness bắt chạy check của repo, và approval cho check đã bị từ chối thì không bị hỏi lại.</span></li>
452
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Câu trả lời agent lặp lại sau mỗi lần harness trả về chỉ hiện một lần, không lặp theo từng vòng.</span></li>
453
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Trên Linux và Windows (WSL2), check tìm được JDK ngoài PATH: SDKMAN, /usr/lib/jvm hoặc ~/.jdks.</span></li>
454
+ </ul>
455
+ </div>
456
+ </div>
434
457
  <div class="archive-row" id="v1-43-8">
435
458
  <div class="archive-head"><a class="release-version" href="#v1-43-8">v1.43.8</a> <span class="release-date">27/10/2026</span></div>
436
459
  <div class="archive-body">
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.51.1" title="v1.51.1 docs · VI"><span class="dot"></span>v1.51.1</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.52.0" title="v1.52.0 docs · VI"><span class="dot"></span>v1.52.0</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new">Changelog</a>
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.51.1" title="v1.51.1 docs · VI"><span class="dot"></span>v1.51.1</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.52.0" title="v1.52.0 docs · VI"><span class="dot"></span>v1.52.0</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new">Changelog</a>
@@ -256,8 +256,8 @@
256
256
  },
257
257
  {
258
258
  "path": "tests",
259
- "fileCount": 651,
260
- "sha256": "3e29d3643596a6ed8d960833b13df079ee337aaa431e059e81af1b662311bcb8"
259
+ "fileCount": 653,
260
+ "sha256": "577758e269e6cb8d79d32cec4c354be0ea343404c897c24280dc50f86002ea21"
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.51.1",
3
+ "version": "1.52.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { memberQuestions } from './member-questions.mjs';
3
3
  import { alwaysBypass, effectivePermission } from './permission.mjs';
4
+ import { checkScope, suitePart, unquoted } from './test-scope.mjs';
4
5
  import { digestsOnce, knownFast, markCheckStart, missingSuites, pushQuestion, pushRepositories, reviewBeforePush } from './check-evidence.mjs';
5
6
 
6
7
  // How much to test is the member's decision: a one-line fix should not cost a
@@ -12,53 +13,8 @@ import { digestsOnce, knownFast, markCheckStart, missingSuites, pushQuestion, pu
12
13
  // overnight) is taken back and the cheap answer goes on: only the tests that
13
14
  // cover the change, and no push. Before this a turn waited until morning.
14
15
  export const UNANSWERED_MS = 10 * 60_000;
16
+ export { checkScope, suitePart };
15
17
 
16
- // How much of the suite a test command covers: 'partial' (a filter, one file, one
17
- // package), 'suite' (the whole test command) or null when it runs no tests.
18
- const VERSION_ONLY = /(?:^|\s)(?:--version|-V|--help)(?:\s|$)/;
19
- const FILTERED = /(?:^|\s)(?:-k|-t|--grep|--testNamePattern|--test-name-pattern|--filter|--only|--testPathPattern)(?:[\s=]|$)|\bgo\s+test\b.*\s-run[\s=]|\bpytest\b.*\s::|\.only\b/;
20
- const TEST_RUNNER = /\b(npm|pnpm|yarn|bun)\s+(run\s+)?test(:\S+)?(?=\s|$)|\bnode\s+--test\b|\b(vitest|jest|mocha|pytest|rspec|phpunit)\b|\bplaywright\s+test\b|\bpython3?\s+-m\s+(pytest|unittest)\b|\b(go|cargo|swift|dotnet|deno)\s+test\b|\bmake\s+test\b|\bxcodebuild\b.*\btest\b|\b(gradle|gradlew|mvn|mvnw)\b.*\b(test|verify)\b/;
21
- // Test commands start a command: `npm install -D vitest` or `rm jest.config.ts` only name one.
22
- const TEST_START = new RegExp(`^(?:${TEST_RUNNER.source})`);
23
- // A function body, a group or a loop body opens a part as well.
24
- const LAUNCHER = /^(?:[\w-]+\s*\(\)\s*\{?\s*|\{\s+|\(\s*|(?:do|then|else)\s+|\w+=\S*\s+|time\s+|nohup\s+|timeout\s+\S+\s+|npx\s+(?:-\S+\s+)*|bunx\s+|pnpm\s+(?:exec|dlx)\s+|uv\s+run\s+|poetry\s+run\s+|yarn\s+(?=jest|vitest|mocha|playwright)|node_modules\/\.bin\/|\.\/)+/;
25
- const partsOf = bare => bare.split(/\s*(?:\|\|?|&&|;)\s*/).map(part => part.replace(/\s\d*>.*$/, '').replace(/[\s)}]+$/, '').trim());
26
- const NOT_TESTS = /\b(lint|typecheck|type-check|tsc|eslint|biome|ruff|mypy|pyright|vet|clippy|format|build)\b/;
27
- const TARGET_FLAG = /^(-p|--package|-w|--workspace|--tests)$|^(-Dtest=|-only-testing:|--workspace=|--package=)/;
28
- // `go test -p 4` sets how many packages run at once; for cargo -p names a package.
29
- const GO_TEST = /^go\s+test\b/;
30
- const VALUE_FLAG = /^(-c|--config|--reporter|--project|--workers|--timeout|--rootDir|--root|-f|--file|-o|--output)$/;
31
- // A file or folder named on the command line; a flag's own value names none.
32
- const targetsPart = (args, go = false) => args.some((a, i) => {
33
- // A variable: a function's arguments, a loop's file, $(go list …). The caller chose them, so this is not the whole command.
34
- if (a.startsWith('$')) return true;
35
- if (go && a === '-p') return false;
36
- if (TARGET_FLAG.test(a)) return true;
37
- if (go && args[i - 1] === '-p') return false;
38
- if (a.startsWith('-') || VALUE_FLAG.test(args[i - 1] ?? '') || a.includes('*') || a.endsWith('...')) return false;
39
- return a.includes('/') || /\.(test|spec)\.\w+$|_test\.\w+$|^test_\w+\.\w+$/.test(a);
40
- });
41
- // What a command runs, without the text it merely quotes (a commit message, an echo).
42
- const unquoted = text => text.replace(/"[^"]*"|'[^']*'/g, quoted => quoted.startsWith('"$') ? '$' : '""');
43
- export function checkScope(command, declared = []) {
44
- const text = String(command ?? '').replace(/\s+/g, ' ').trim();
45
- if (!text || /^(echo|printf|grep|rg|cat|sed|awk|head|tail|less|git|ls|find)\b/.test(text) || VERSION_ONLY.test(text) || /--collect-only/.test(text)) return null;
46
- const bare = unquoted(text);
47
- for (const part of partsOf(bare)) {
48
- const own = part.replace(LAUNCHER, ''), runner = own.match(TEST_START);
49
- if (!runner) continue;
50
- if (FILTERED.test(own)) return 'partial';
51
- return !declared.includes(text) && targetsPart(own.slice(runner[0].length).split(' ').filter(Boolean), GO_TEST.test(own)) ? 'partial' : 'suite';
52
- }
53
- // A script the repository declares: it cannot be narrowed from here.
54
- const named = declared.find(d => partsOf(bare).some(part => part.startsWith(unquoted(d))));
55
- return named && !NOT_TESTS.test(named) ? 'suite' : null;
56
- }
57
- // The test step of a command, without what surrounds it: the same step costs the same.
58
- export function suitePart(command) {
59
- for (const part of partsOf(unquoted(String(command ?? '').replace(/\s+/g, ' ').trim()))) { const own = part.replace(LAUNCHER, ''); if (TEST_START.test(own)) return own; }
60
- return String(command ?? '').trim();
61
- }
62
18
  // The member's own words asking for the whole suite: nothing to ask them then.
63
19
  const RUN_ALL = String.raw`(?:full|all(?:\s+the)?|whole(?:\s+the)?|entire(?:\s+the)?|the\s+(?:full|whole|entire))`;
64
20
  const FULL_WORDS = new RegExp([
@@ -1,3 +1,4 @@
1
+ import { callName } from './role-names.mjs';
1
2
  import { memberMessages, memberRequest } from './objections.mjs';
2
3
 
3
4
  // Several helpers of one role, at the member's request only ("use 3
@@ -30,7 +31,7 @@ export function fanOut(manager, role, args, harness) {
30
31
  if (harness) throw Error('managed-copies-invalid: the harness runs one helper per call.');
31
32
  const words = normal(args?.member_words);
32
33
  if (words.length < 3 || !memberRequest(memberMessages(manager)).some(text => normal(text).includes(words)) || namedCount(words) < n || !HELPER_WORDS.test(words)) return { dropped: n };
33
- if (n > MAX_COPIES) throw Error(`managed-copies-invalid: ask for 2 to ${MAX_COPIES} ${role} helpers (copies, or one split line per helper).`);
34
+ if (n > MAX_COPIES) throw Error(`managed-copies-invalid: ask for 2 to ${MAX_COPIES} ${callName(role)} helpers (copies, or one split line per helper).`);
34
35
  return { n, split: split.length ? split : null };
35
36
  }
36
37
  // The helper's answer, with what the main agent reads above it when it asked
@@ -38,12 +39,12 @@ export function fanOut(manager, role, args, harness) {
38
39
  export function withDroppedNote(result, role, asked) {
39
40
  const n = asked?.dropped, first = result?.content?.[0];
40
41
  if (!n || first?.type !== 'text') return result;
41
- const note = `One ${role} helper ran, not ${n}: the member's own words did not ask for ${n} ${role} helpers. Go on with this answer; run several only when the member asks for that many.\n\n`;
42
+ const note = `One ${callName(role)} helper ran, not ${n}: the member's own words did not ask for ${n} ${callName(role)} helpers. Go on with this answer; run several only when the member asks for that many.\n\n`;
42
43
  return { ...result, content: [{ ...first, text: note + first.text }, ...result.content.slice(1)] };
43
44
  }
44
45
  // What one copy reads under its brief.
45
46
  export function copyNote(role, i, fan) {
46
- return `\n\nThe member asked for ${fan.n} ${role} helpers; you are ${role} helper ${i + 1} of ${fan.n}. ${fan.split ? `Your share of the work: ${fan.split[i]}. The others cover the rest.` : 'The others have the same brief: answer on your own judgement.'}`;
47
+ return `\n\nThe member asked for ${fan.n} ${callName(role)} helpers; you are ${callName(role)} helper ${i + 1} of ${fan.n}. ${fan.split ? `Your share of the work: ${fan.split[i]}. The others cover the rest.` : 'The others have the same brief: answer on your own judgement.'}`;
47
48
  }
48
49
 
49
50
  const jsonOf = reply => {
@@ -53,18 +54,17 @@ const jsonOf = reply => {
53
54
  const unique = (list, key) => { const seen = new Set(); return list.filter(x => { const k = key(x); if (seen.has(k)) return false; seen.add(k); return true; }); };
54
55
  const SEVERITY = { blocking: 0, major: 1, minor: 2 };
55
56
  // One answer from the copies: each helper's prose (its JSON left out), then
56
- // one JSON block joining their findings, verdicts and objections, so the
57
- // review, verify and objection readers see every copy. `parsed`: every copy
58
- // gave its JSON. A finding or a failed claim any copy reports stands.
59
- export function mergeCopies(role, label, answers, { findingsOf, issuesOf, verdictsOf }) {
57
+ // one JSON block joining their findings and objections, so the
58
+ // review and objection readers see every copy. `parsed`: every copy
59
+ // gave its JSON. A finding any copy reports stands.
60
+ export function mergeCopies(role, label, answers, { findingsOf, issuesOf }) {
60
61
  const n = answers.length, room = Math.floor(12000 / n);
61
62
  const findings = role === 'review' ? unique(answers.flatMap(a => findingsOf(a.reply) ?? []), f => `${f.severity}|${f.file}|${f.line}|${f.issue}`).sort((a, b) => SEVERITY[a.severity] - SEVERITY[b.severity]) : null;
62
- const verdicts = role === 'verify' ? unique(answers.flatMap(a => verdictsOf(a.reply) ?? []), v => `${v.status}|${v.claim}`) : null;
63
63
  const issues = unique(answers.flatMap(a => issuesOf(a.reply) ?? []), i => `${i.kind}|${i.detail}`).slice(0, 5);
64
64
  const parsed = answers.every(a => jsonOf(a.reply));
65
65
  const summaries = answers.map(a => jsonOf(a.reply)?.summary).filter(s => typeof s === 'string').map(s => s.slice(0, 200));
66
- const json = { ...(findings ? { findings: findings.slice(0, 40) } : {}), ...(verdicts ? { verdicts: verdicts.slice(0, 40) } : {}), brief_issues: issues,
67
- summary: `${n} ${role} helpers${summaries.length ? ': ' + summaries.join(' | ') : ''}`.slice(0, 900) };
66
+ const json = { ...(findings ? { findings: findings.slice(0, 40) } : {}), brief_issues: issues,
67
+ summary: `${n} ${callName(role)} helpers${summaries.length ? ': ' + summaries.join(' | ') : ''}`.slice(0, 900) };
68
68
  const body = answers.map((a, i) => `### ${label} helper ${i + 1} of ${n}\n${String(a.reply).replace(/```json\s*[\s\S]*?```/g, '').trim().slice(0, room)}`).join('\n\n');
69
69
  return { reply: `${body}\n\n\`\`\`json\n${JSON.stringify(json)}\n\`\`\``, usage: answers.reduce((total, a) => total + a.usage, 0),
70
70
  ...(findings ? { findings, parsed, blocking: findings.filter(f => f.severity === 'blocking').length } : {}) };
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ import { cacheDirectory } from './language-environment.mjs';
8
+
9
+ // The general helper changes files, so it never works in the member's
10
+ // project. It works in its own clone: a separate repository with its own
11
+ // .git (refs, config, hooks), no remote, and no writable path to the project
12
+ // or its refs. A linked worktree would share the project's .git, and the
13
+ // sandbox opens a shared git directory for writing. The clone is `--shared`: it
14
+ // borrows the project's object files (read-only inside the sandbox, by the
15
+ // boundary's readRoots) instead of copying them, so it costs about what a
16
+ // worktree costs. The only crossing is takeBack below, run by this runtime,
17
+ // not by the helper: it copies the clone's commits into the project as one
18
+ // private ref, refs/general/<id>, and touches nothing else. The main agent
19
+ // merges that ref itself.
20
+ export const GENERAL_ENTRY = 'agent-watch-general';
21
+ const OWNER = '.piagent-owner';
22
+ const run = promisify(execFile);
23
+ // The clone's own configuration is the helper's to write. The runtime
24
+ // therefore reads it only through git commands that cannot start a program
25
+ // the clone names: these override what a clone's config could set.
26
+ const SAFE = ['-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', '-c', 'core.alternateRefsCommand=', '-c', 'protocol.file.allow=always'];
27
+ const gitEnv = () => ({ PATH: '/usr/bin:/bin', HOME: os.tmpdir(), GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0', LANG: 'en_US.UTF-8' });
28
+ const git = (exe, cwd, args) => run(exe, [...SAFE, ...args], { cwd, env: gitEnv(), maxBuffer: 4 << 20, timeout: 120_000 }).then(r => r.stdout.trim());
29
+
30
+ // A clone of the project's HEAD. Work not yet committed in the project is not
31
+ // in it: `dirty` lists those paths so the main agent can be told.
32
+ export async function cloneProject(managed) {
33
+ // The clone's isolation is proven under the macOS sandbox only; the Linux (bubblewrap) path is not, so it stays closed.
34
+ if (process.platform !== 'darwin') throw Error('managed-general-unavailable: the general helper is not verified on this platform yet; do the work yourself.');
35
+ const exe = managed.boundary.git, project = managed.cwd;
36
+ let top, base, dirty, objects;
37
+ try {
38
+ top = fs.realpathSync(await git(exe, project, ['rev-parse', '--show-toplevel']));
39
+ base = await git(exe, project, ['rev-parse', '--verify', 'HEAD']);
40
+ objects = fs.realpathSync(path.resolve(project, await git(exe, project, ['rev-parse', '--git-path', 'objects'])));
41
+ dirty = (await git(exe, project, ['status', '--porcelain', '-uall'])).split('\n').filter(Boolean).map(l => l.replace(/^\S{1,2}\s+/, '')).slice(0, 50);
42
+ } catch { throw Error('managed-general-needs-git: the general helper needs the project to be a git repository with a commit; commit first or do the work yourself.'); }
43
+ if (top !== project) throw Error('managed-general-needs-git: the general helper needs the project folder to be the repository root.');
44
+ const id = randomUUID().slice(0, 8), dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'piagent-general-'));
45
+ try {
46
+ await git(exe, dir, ['clone', '-q', '--shared', '--no-tags', '--template=', '--', project, path.join(dir, 'work')]);
47
+ await git(exe, path.join(dir, 'work'), ['remote', 'remove', 'origin']);
48
+ } catch (error) { fs.rmSync(dir, { recursive: true, force: true }); throw Error(`managed-general-clone-failed: ${String(error?.message).slice(0, 200)}`); }
49
+ const token = randomUUID(); // the proof, in the folder and in the session's record, that this runtime made it
50
+ fs.writeFileSync(path.join(dir, OWNER), token, { mode: 0o600 });
51
+ const real = fs.realpathSync(path.join(dir, 'work'));
52
+ return { id, base, dirty, token, objects, cache: cacheDirectory(managed.boundary.userHome, real), root: dir, dir: real };
53
+ }
54
+
55
+ // The clone's folder, and the package cache the sandbox keyed to it (no later run shares it).
56
+ export function discardClone(clone) { fs.rmSync(clone.root, { recursive: true, force: true }); fs.rmSync(clone.cache, { recursive: true, force: true }); }
57
+
58
+ // Bring the clone's commits into the project as refs/general/<id> and
59
+ // discard the clone. Returns what the main agent needs to merge them.
60
+ export async function takeBack(managed, clone) {
61
+ const exe = managed.boundary.git, project = managed.cwd;
62
+ try {
63
+ // The helper writes the clone's .git: a gitfile or alternates would point
64
+ // the fetch at a path outside it.
65
+ const dotgit = path.join(clone.dir, '.git');
66
+ const alternates = path.join(dotgit, 'objects/info/alternates');
67
+ if (!fs.lstatSync(dotgit).isDirectory() || fs.readFileSync(alternates, 'utf8').trim() !== clone.objects) throw Error('the clone was changed to point outside itself');
68
+ const ref = `refs/general/${clone.id}`;
69
+ await git(exe, project, ['fetch', '-q', '--no-tags', '--no-recurse-submodules', '--no-write-fetch-head', clone.dir, `+HEAD:${ref}`]);
70
+ const head = await git(exe, project, ['rev-parse', '--verify', ref]);
71
+ if (head === clone.base) { await git(exe, project, ['update-ref', '-d', ref]); return { ref: null, head, base: clone.base, files: [], note: 'The general helper made no commit, so nothing came back. Only committed work is returned.' }; }
72
+ if (!(await git(exe, project, ['merge-base', '--is-ancestor', clone.base, head]).then(() => true, () => false))) { await git(exe, project, ['update-ref', '-d', ref]); throw Error('the clone no longer descends from the commit it started from'); }
73
+ const files = (await git(exe, project, ['diff', '--numstat', `${clone.base}..${head}`])).split('\n').filter(Boolean).map(l => { const [added, deleted, ...name] = l.split('\t'); return `+${added} -${deleted} ${name.join('\t')}`; }).slice(0, 80);
74
+ return { ref, head, base: clone.base, files, note: null };
75
+ } finally { discardClone(clone); }
76
+ }
77
+
78
+ // What the main agent reads with the helper's answer.
79
+ export function takeBackText(clone, back) {
80
+ const dirty = clone.dirty.length ? ` The helper did not see ${clone.dirty.length} uncommitted path(s) in the project (${clone.dirty.slice(0, 8).join(', ')}${clone.dirty.length > 8 ? ', …' : ''}).` : '';
81
+ if (!back.ref) return `\n\n${back.note}${dirty}`;
82
+ return `\n\nThe general helper's commits are in ${back.ref} (${back.base.slice(0, 9)}..${back.head.slice(0, 9)}); your files are unchanged. Merge or cherry-pick that ref yourself, run the checks on the result, then ask for review; delete the ref afterwards (git update-ref -d ${back.ref}). Changed:\n${back.files.join('\n')}${dirty}`;
83
+ }
84
+
85
+ // One entry per general run, so a later turn knows what ran and what came back;
86
+ // "running" with the folder and its token is also the record of what to recover.
87
+ export function recordGeneral(managed, clone, status, back = null) {
88
+ managed.session.sessionManager.appendCustomEntry(GENERAL_ENTRY, { id: clone.id, base: clone.base, status, root: clone.root, token: clone.token, ...(back?.ref ? { ref: back.ref, head: back.head } : {}), at: new Date().toISOString() });
89
+ }
90
+ // A crash or a kill skips the cleanup above. When the conversation next runs a
91
+ // general helper, a run its record still calls "running" (and this process
92
+ // is not running) has its clone removed only if the folder is provably the one
93
+ // this runtime made: a real folder, ours, directly in the temporary folder,
94
+ // holding the token the record names. Anything else is only reported.
95
+ export function sweepGeneral(managed) {
96
+ const last = new Map(), tmp = fs.realpathSync(os.tmpdir()), removed = [], unproven = [];
97
+ for (const e of managed.session.sessionManager.getEntries?.() ?? []) if (e.type === 'custom' && e.customType === GENERAL_ENTRY) last.set(e.data.id, e.data);
98
+ for (const d of last.values()) {
99
+ if (d.status !== 'running' || typeof d.root !== 'string' || managed.liveClones?.has(d.root)) continue;
100
+ let st; try { st = fs.lstatSync(d.root); } catch { continue; } // already gone
101
+ let owned = false;
102
+ try {
103
+ owned = st.isDirectory() && !st.isSymbolicLink() && st.uid === process.getuid() && path.dirname(d.root) === tmp && path.basename(d.root).startsWith('piagent-general-')
104
+ && fs.readFileSync(path.join(d.root, OWNER), 'utf8') === d.token;
105
+ } catch { /* no marker: not proven */ }
106
+ if (owned) { fs.rmSync(d.root, { recursive: true, force: true }); removed.push(d.id); } else unproven.push(d.root);
107
+ }
108
+ return { removed, unproven };
109
+ }
110
+ export const sweepText = ({ removed, unproven }) => (removed.length ? `\n\nAn earlier general run (${removed.join(', ')}) was interrupted; its clone folder was removed and any work in it was not returned.` : '')
111
+ + (unproven.length ? `\n\nA folder an earlier general run recorded could not be proven to be the one it made, so it was left in place: ${unproven.join(', ')}.` : '');