@piagent/platform 1.1.5 → 1.1.6

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  This file records release-facing changes for Pi Agent Platform. Copy the relevant version block into GitHub Releases when publishing a tag.
4
4
 
5
+ ## v1.1.6 - 2026-07-27
6
+
7
+ Monorepo fixes found auditing how the platform scopes itself to a project. Every case was reproduced against a real directory tree before it was changed.
8
+
9
+ ### Fixed
10
+
11
+ - `be-readonly-fe` keeps the backend read-only in a monorepo. Its `readOnlyPaths` and `shellProtectedPaths` were anchored at the repository root, so `backend/**` and `apps/api/**` covered a backend sitting at the top of the tree and nothing else. Choosing the profile on a repository whose backend lives in `packages/api`, `apps/backend` or `services/payments` produced a profile that announced the backend was read-only while leaving every file in it writable — the one guarantee the profile exists to make. The backend directories under `apps/`, `packages/` and all of `services/*` are covered now.
12
+
13
+ They are enumerated rather than matched with `**/api/**`, which would have swept in `packages/web/src/api/` — the frontend's own HTTP client — and frozen the half of the repository the profile is meant to leave writable.
14
+
15
+ - The frontend verify command finds a frontend that is not at `frontend/` or `apps/web/`. It fell through to the repository root, where a workspace root has no `type-check` script, so verification reported success having run nothing. `apps/frontend`, `packages/web`, `packages/frontend` and `client` are looked at too.
16
+
17
+ - `apps/backend` is read as a backend. `apps/frontend` counted as a frontend marker while only `apps/api` and `apps/server` counted as backend ones, so a repository that named both halves symmetrically was detected as frontend-only and offered `web-frontend`. `client/` and `server/` at the root were not markers either.
18
+
19
+ - Frontend and backend are detected inside declared workspaces. Nothing read the `workspaces` field or `pnpm-workspace.yaml`, and nothing looked under `packages/`, so a pnpm monorepo fell through every marker to `generic`. Each declared package is now read through its own manifest and layout, which is what identifies `packages/storefront` and `packages/gateway` as the two halves of a fullstack repository. Only a trailing wildcard is expanded, absolute and `..` patterns are refused, and at most 64 packages are read.
20
+
21
+ ### Changed
22
+
23
+ - Profile detection lives in `packages/piagent-core/extensions/project-shape.js`. `piagent_profile_options` and `scripts/init-project.sh` each carried their own copy of the rules and had drifted; the `apps/backend` gap above was present in both, and a fix to either would have left the other wrong. The file is covered by the capability lock, because steering which profile a project is offered is a way to choose what the guard ends up enforcing.
24
+
5
25
  ## v1.1.5 - 2026-07-27
6
26
 
7
27
  ### Added
package/README.md CHANGED
@@ -283,7 +283,7 @@ This repository intentionally excludes:
283
283
 
284
284
  ## Maturity
285
285
 
286
- The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.1.5` or a reviewed commit.
286
+ The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.1.6` or a reviewed commit.
287
287
 
288
288
  Ready for:
289
289
 
@@ -32,7 +32,11 @@
32
32
  "api/**",
33
33
  "apps/api/**",
34
34
  "apps/server/**",
35
- "services/backend/**",
35
+ "apps/backend/**",
36
+ "packages/api/**",
37
+ "packages/server/**",
38
+ "packages/backend/**",
39
+ "services/*/**",
36
40
  "**/migrations/**"
37
41
  ],
38
42
  "shellProtectedPaths": [
@@ -47,7 +51,11 @@
47
51
  "api/**",
48
52
  "apps/api/**",
49
53
  "apps/server/**",
50
- "services/backend/**",
54
+ "apps/backend/**",
55
+ "packages/api/**",
56
+ "packages/server/**",
57
+ "packages/backend/**",
58
+ "services/*/**",
51
59
  "**/migrations/**",
52
60
  "**/node_modules/**",
53
61
  "**/dist/**",
@@ -85,10 +93,10 @@
85
93
  "test -s README.md"
86
94
  ],
87
95
  "frontendSource": [
88
- "if test -f frontend/package.json; then cd frontend && npm run type-check --if-present && npm run lint --if-present && npm test --if-present; elif test -f apps/web/package.json; then cd apps/web && npm run type-check --if-present && npm run lint --if-present && npm test --if-present; elif test -f package.json; then npm run type-check --if-present && npm run lint --if-present && npm test --if-present; else echo 'No frontend verify command configured; update .pi/piagent-profile.json'; fi"
96
+ "if test -f frontend/package.json; then cd frontend; elif test -f apps/web/package.json; then cd apps/web; elif test -f apps/frontend/package.json; then cd apps/frontend; elif test -f packages/web/package.json; then cd packages/web; elif test -f packages/frontend/package.json; then cd packages/frontend; elif test -f client/package.json; then cd client; elif test -f package.json; then :; else echo 'No frontend verify command configured; update .pi/piagent-profile.json'; exit 0; fi; npm run type-check --if-present && npm run lint --if-present && npm test --if-present"
89
97
  ],
90
98
  "frontendRuntime": [
91
- "if test -f frontend/package.json; then cd frontend && npm run test:e2e --if-present; elif test -f apps/web/package.json; then cd apps/web && npm run test:e2e --if-present; elif test -f package.json; then npm run test:e2e --if-present; else echo 'No frontend runtime verify command configured; update .pi/piagent-profile.json'; fi"
99
+ "if test -f frontend/package.json; then cd frontend; elif test -f apps/web/package.json; then cd apps/web; elif test -f apps/frontend/package.json; then cd apps/frontend; elif test -f packages/web/package.json; then cd packages/web; elif test -f packages/frontend/package.json; then cd packages/frontend; elif test -f client/package.json; then cd client; elif test -f package.json; then :; else echo 'No frontend runtime verify command configured; update .pi/piagent-profile.json'; exit 0; fi; npm run test:e2e --if-present"
92
100
  ]
93
101
  },
94
102
  "mcpCapabilities": [
@@ -419,8 +419,8 @@ Các lệnh này chạy ngoài Pi.
419
419
  | Command | Dùng khi nào |
420
420
  |---|---|
421
421
  | `npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0` | Cài Pi CLI tương thích với release hiện tại. |
422
- | `npm install -g --ignore-scripts @piagent/platform@1.1.5` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
423
- | `pi install git:github.com/Vt-mmm/piagent@v1.1.5` | Cài pinned release khi cần reproducible team setup. |
422
+ | `npm install -g --ignore-scripts @piagent/platform@1.1.6` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
423
+ | `pi install git:github.com/Vt-mmm/piagent@v1.1.6` | Cài pinned release khi cần reproducible team setup. |
424
424
  | `pi install git:github.com/Vt-mmm/piagent` | Cài latest package platform cho máy cá nhân/sandbox. |
425
425
  | `piagent-update --check` | Báo version hiện tại vs version sẽ lên cho cả ba thành phần; không cài gì. |
426
426
  | `piagent-update` | Full update một lệnh: Pi host → npm-global helper → Pi package, đúng thứ tự release yêu cầu. Thêm `--project <path>` để chạy doctor sau. |
@@ -14,9 +14,9 @@ Repo này phải chạy được cho nhiều project/domain khác nhau. Vì vậ
14
14
 
15
15
  | Use case | Package source |
16
16
  |---|---|
17
- | Team pinned release | `git:github.com/Vt-mmm/piagent@v1.1.5` |
17
+ | Team pinned release | `git:github.com/Vt-mmm/piagent@v1.1.6` |
18
18
  | Personal/sandbox dev | `git:github.com/Vt-mmm/piagent` |
19
- | Registry distribution | `npm:@piagent/platform@1.1.5` |
19
+ | Registry distribution | `npm:@piagent/platform@1.1.6` |
20
20
  | Enterprise npm (fork under another scope) | `npm:@your-scope/platform@x.y.z` |
21
21
  | Local platform dev | `/path/to/piagent` |
22
22
 
@@ -26,7 +26,7 @@ Latest tiện cho máy cá nhân muốn nhận cập nhật nhanh. Pin tag/commi
26
26
 
27
27
  | Channel | Command | Target | Policy |
28
28
  |---|---|---|---|
29
- | `stable` | `bash scripts/install-global.sh --stable` | Helper release tag resolved to commit SHA, currently `v1.1.5` | Install the Pi package matching the helper. |
29
+ | `stable` | `bash scripts/install-global.sh --stable` | Helper release tag resolved to commit SHA, currently `v1.1.6` | Install the Pi package matching the helper. |
30
30
  | `exact` | `bash scripts/install-global.sh --version vX.Y.Z --resolve-tag` | Requested tag resolved to commit SHA | Pi-package-only rollout, rollback, and incident recovery. |
31
31
  | `dev` | `bash scripts/install-global.sh --dev` | Moving Git source | Personal/sandbox only; do not commit into project settings. |
32
32
  | `local` | `bash scripts/install-global.sh --local` | Current checkout path | Platform development only. |
@@ -53,7 +53,7 @@ Root `package.json` có `pi` manifest trỏ tới:
53
53
  Do đó team có thể:
54
54
 
55
55
  ```bash
56
- pi install git:github.com/Vt-mmm/piagent@v1.1.5
56
+ pi install git:github.com/Vt-mmm/piagent@v1.1.6
57
57
  ```
58
58
 
59
59
  Không cần biết internal folder `packages/piagent-core`. Source không pin chỉ dành cho personal/sandbox như bảng channel ở trên.
@@ -65,12 +65,12 @@ Team nên install global package một lần:
65
65
  ```bash
66
66
  node --version # >= 22.19.0
67
67
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
68
- npm install -g --ignore-scripts @piagent/platform@1.1.5
68
+ npm install -g --ignore-scripts @piagent/platform@1.1.6
69
69
  piagent-install --stable --dry-run
70
70
  piagent-install --stable
71
71
  ```
72
72
 
73
- `pi install git:github.com/Vt-mmm/piagent@v1.1.5` vẫn hợp lệ nếu chỉ cần cài Pi package. Lệnh đó không tự tạo các binary terminal `piagent-*`; muốn có helper global thì dùng `npm install -g --ignore-scripts @piagent/platform@1.1.5`.
73
+ `pi install git:github.com/Vt-mmm/piagent@v1.1.6` vẫn hợp lệ nếu chỉ cần cài Pi package. Lệnh đó không tự tạo các binary terminal `piagent-*`; muốn có helper global thì dùng `npm install -g --ignore-scripts @piagent/platform@1.1.6`.
74
74
 
75
75
  Support matrix hiện tại: macOS Apple Silicon + Bash và Linux x64 + Bash đã được verify; macOS Intel + Bash và Linux ARM64 + Bash là supported target cần chạy smoke trước rollout; native Windows chưa phải target rollout team; WSL2 experimental. Chi tiết và version runtime nằm trong [release/install policy](release-install-policy.md).
76
76
 
@@ -103,7 +103,7 @@ Nếu muốn commit sẵn `.pi/piagent-profile.json` vào repo hoặc bootstrap
103
103
  ```bash
104
104
  bash scripts/setup.sh /path/to/project \
105
105
  --profile be-readonly-fe \
106
- --package-source git:github.com/Vt-mmm/piagent@v1.1.5 \
106
+ --package-source git:github.com/Vt-mmm/piagent@v1.1.6 \
107
107
  --mcp-preset core \
108
108
  --subagents-preset safe
109
109
  ```
@@ -18,12 +18,12 @@ Support matrix release hiện tại: macOS Apple Silicon + Bash và Linux x64 +
18
18
  ```bash
19
19
  node --version # >= 22.19.0
20
20
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
21
- npm install -g --ignore-scripts @piagent/platform@1.1.5
21
+ npm install -g --ignore-scripts @piagent/platform@1.1.6
22
22
  piagent-install --stable --dry-run
23
23
  piagent-install --stable
24
24
  ```
25
25
 
26
- Khi seed `.pi/settings.json` cho team/repo cần audit lặp lại, giữ package source dạng pinned tag như `git:github.com/Vt-mmm/piagent@v1.1.5`. Máy cá nhân có thể dùng `git:github.com/Vt-mmm/piagent` để theo latest.
26
+ Khi seed `.pi/settings.json` cho team/repo cần audit lặp lại, giữ package source dạng pinned tag như `git:github.com/Vt-mmm/piagent@v1.1.6`. Máy cá nhân có thể dùng `git:github.com/Vt-mmm/piagent` để theo latest.
27
27
 
28
28
  Nếu đang ở source checkout của platform, dùng helper theo channel để preview trước khi đổi:
29
29
 
@@ -99,7 +99,7 @@ install package once
99
99
 
100
100
  ```bash
101
101
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
102
- npm install -g --ignore-scripts @piagent/platform@1.1.5
102
+ npm install -g --ignore-scripts @piagent/platform@1.1.6
103
103
  piagent-install --stable --dry-run
104
104
  piagent-install --stable
105
105
  ```
@@ -281,7 +281,7 @@ Không phải daily default. Dùng khi muốn tạo sẵn `.pi` files cho projec
281
281
  bash /path/to/piagent/scripts/setup.sh /path/to/project \
282
282
  --project-only \
283
283
  --profile auto \
284
- --package-source git:github.com/Vt-mmm/piagent@v1.1.5 \
284
+ --package-source git:github.com/Vt-mmm/piagent@v1.1.6 \
285
285
  --mcp-preset core \
286
286
  --subagents-preset safe
287
287
  ```
@@ -854,8 +854,8 @@ Watchdog là optional adversarial reviewer ở cuối turn, không bật mặc
854
854
 
855
855
  | Command | Dùng để |
856
856
  |---|---|
857
- | `npm install -g --ignore-scripts @piagent/platform@1.1.5` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
858
- | `pi install git:github.com/Vt-mmm/piagent@v1.1.5` | Install pinned release cho reproducible team setup. |
857
+ | `npm install -g --ignore-scripts @piagent/platform@1.1.6` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
858
+ | `pi install git:github.com/Vt-mmm/piagent@v1.1.6` | Install pinned release cho reproducible team setup. |
859
859
  | `pi install git:github.com/Vt-mmm/piagent` | Install latest platform package cho máy cá nhân/sandbox. |
860
860
  | Cài exact Pi host của release, rồi `npm install -g --ignore-scripts @piagent/platform@X.Y.Z` và `piagent-install --stable` | Full update: đồng bộ host, terminal helper và Pi package. Mỗi release pin một Pi host chính xác; lấy đúng version của release đang cài trong [release/install policy](release-install-policy.md). |
861
861
  | Cài exact Pi host ghi trong release cũ, rồi helper `vPREVIOUS` và `piagent-install --stable` | Full rollback; đánh giá lại dependency findings của host cũ trước khi hạ version. |
@@ -952,7 +952,7 @@ Mở lại Pi session sau khi install.
952
952
  Cài lại terminal helper đúng release rồi kiểm tra `PATH`:
953
953
 
954
954
  ```bash
955
- npm install -g --ignore-scripts @piagent/platform@1.1.5
955
+ npm install -g --ignore-scripts @piagent/platform@1.1.6
956
956
  command -v piagent-install
957
957
  ```
958
958
 
@@ -68,6 +68,22 @@ Path policy fields have distinct meanings:
68
68
  - `readOnlyPaths`: allow `read`/`grep`/`find`/`ls`, but block `write`/`edit` and shell access.
69
69
  - `shellProtectedPaths`: block shell access only. Do not rely on this field to block write/edit; `profile-doctor` and `team-doctor` warn when a path is present only here.
70
70
 
71
+ Glob trong các field này neo từ project root, nên `backend/**` chỉ khớp backend nằm ở root. Với monorepo, phải liệt kê đúng vị trí thật (`packages/api/**`, `services/*/**`). Không dùng `**/api/**`: nó khớp luôn `packages/web/src/api/` — thư mục HTTP client của frontend — và biến chính phần được phép sửa thành read-only.
72
+
73
+ ## Monorepo và workspace
74
+
75
+ Một project = một thư mục mở `pi`. Guard đọc profile tại `<cwd>/.pi/piagent-profile.json` và không đi ngược lên tìm, nên mở `pi` ở thư mục cha chứa nhiều project sẽ bỏ qua profile của từng project con và chạy unprofiled.
76
+
77
+ Detect FE/BE đọc theo thứ tự:
78
+
79
+ 1. Tên thư mục ở root: `frontend`, `web`, `client`, `ui` cho FE; `backend`, `server`, `api` cho BE.
80
+ 2. Cùng các tên đó ở một cấp dưới `apps/`, `packages/`, `services/`.
81
+ 3. Package khai báo trong `workspaces` (package.json, cả dạng mảng lẫn `{ "packages": [...] }`) hoặc `pnpm-workspace.yaml` — mỗi package đọc dependency và config của chính nó, nên `packages/storefront` + `packages/gateway` vẫn ra `fullstack` dù tên không gợi ý gì.
82
+
83
+ Chỉ wildcard ở segment cuối được mở rộng (`packages/*`, `apps/**`); pattern tuyệt đối, pattern có `..`, và pattern loại trừ `!` bị bỏ qua. Số package đọc tối đa 64.
84
+
85
+ Rule nằm ở `packages/piagent-core/extensions/project-shape.js`. Cả `piagent_profile_options` lẫn `scripts/init-project.sh` cùng gọi file này; trước đây mỗi bên giữ một bản riêng và đã lệch nhau.
86
+
71
87
  ## Built-in adapters
72
88
 
73
89
  | Profile | Dùng cho | Verify mặc định |
@@ -84,6 +100,11 @@ Path policy fields have distinct meanings:
84
100
  | `mobile` | React Native/Flutter | npm test / flutter test nếu tool có |
85
101
  | `docs` | Docs portal/manual | markdown diff check + test nếu project có |
86
102
 
103
+ `be-readonly-fe` giữ read-only cho backend ở root (`backend`, `server`, `api`), ở một cấp dưới `apps/` và `packages/` (`api`, `server`, `backend`), toàn bộ `services/*`, và mọi `**/migrations/**`. Profile built-in là điểm khởi đầu, không phải danh sách đầy đủ — sửa `readOnlyPaths` **và** `shellProtectedPaths` trong `.pi/piagent-profile.json` cho đúng repo:
104
+
105
+ - backend đặt tên khác (`apps/gateway`, `packages/core-service`): thêm path vào cả hai field.
106
+ - frontend nằm trong `services/` (ví dụ `services/web`): `services/*/**` sẽ khoá nhầm nó. Thay pattern đó bằng danh sách service backend cụ thể. Triệu chứng là một write bị chặn kèm lý do trỏ tới `readOnlyPaths`, không phải lỗi im lặng.
107
+
87
108
  ## Runtime profile selection
88
109
 
89
110
  Default UX không bắt buộc chạy bash để set profile. Sau global install, vào project chạy:
@@ -48,6 +48,7 @@ pi install /path/to/piagent
48
48
  - `1.1.3`: command mang secret không còn được dùng làm verify evidence, document reader từ chối FIFO thay vì treo, redact secret ngắn, redirect phải về đúng trang được kiểm.
49
49
  - `1.1.4`: nhận diện secret dạng CLI option (`--token`, `--password`, `--api-key`) và Authorization header ở mọi vị trí argument.
50
50
  - `1.1.5`: thêm `piagent-update` — một lệnh update cả Pi host, terminal helper và Pi package đúng thứ tự.
51
+ - `1.1.6`: `be-readonly-fe` giữ được backend read-only trong monorepo, detect FE/BE đọc cả workspace, và rule detect gộp về một chỗ.
51
52
 
52
53
  ## Không publish
53
54
 
@@ -10,7 +10,7 @@ This file is the canonical install, update, rollback, and release checklist. Oth
10
10
 
11
11
  All supported environments require Node.js `>=22.19.0` and Pi Coding Agent `0.82.0`. The Pi host is installed as a Node CLI; Pi Agent Platform still defines its own release matrix because the terminal helpers and shell policy rely on Bash/POSIX behavior.
12
12
 
13
- | Surface | Status for v1.1.5 | Rollout guidance |
13
+ | Surface | Status for v1.1.6 | Rollout guidance |
14
14
  |---|---|---|
15
15
  | macOS Apple Silicon (`darwin/arm64`) + Bash | Verified for this release. | Safe default for team rollout after normal project smoke tests. |
16
16
  | Linux x64 + Bash | Verified in GitHub Actions. | Safe default for CI/server usage after normal project smoke tests. |
@@ -33,7 +33,7 @@ These three components are versioned independently. A full install, update, or r
33
33
 
34
34
  | Channel | Source shape | Mutability | Use when |
35
35
  |---|---|---:|---|
36
- | `stable` | `git:github.com/Vt-mmm/piagent@<resolved-commit-sha>` from `v1.1.5` | Fixed after resolution | Default for team rollout. |
36
+ | `stable` | `git:github.com/Vt-mmm/piagent@<resolved-commit-sha>` from `v1.1.6` | Fixed after resolution | Default for team rollout. |
37
37
  | `exact` | Tag, reviewed commit, or a tag resolved with `--resolve-tag` | Fixed when using a commit SHA | Pi-package-only roll forward, rollback, or reproduction. |
38
38
  | `dev` | `git:github.com/Vt-mmm/piagent` | Moving | Personal machine or sandbox only. |
39
39
  | `local` | `/path/to/piagent` | Local workspace | Platform development and dry-run validation. |
@@ -53,7 +53,7 @@ Use this flow when the team needs both terminal commands and the Pi package:
53
53
  ```bash
54
54
  node --version # >= 22.19.0
55
55
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
56
- npm install -g --ignore-scripts @piagent/platform@1.1.5
56
+ npm install -g --ignore-scripts @piagent/platform@1.1.6
57
57
  piagent-install --stable --dry-run
58
58
  piagent-install --stable
59
59
  ```
@@ -61,8 +61,8 @@ piagent-install --stable
61
61
  The stable preview and apply output includes:
62
62
 
63
63
  ```text
64
- currentRelease: v1.1.5 (helper package version)
65
- tag: v1.1.5
64
+ currentRelease: v1.1.6 (helper package version)
65
+ tag: v1.1.6
66
66
  resolvedCommit: <40-char-sha>
67
67
  source: git:github.com/Vt-mmm/piagent@<40-char-sha>
68
68
  ```
@@ -79,7 +79,7 @@ bash scripts/install-global.sh --stable
79
79
  Use this only when terminal commands are not needed:
80
80
 
81
81
  ```bash
82
- pi install git:github.com/Vt-mmm/piagent@v1.1.5
82
+ pi install git:github.com/Vt-mmm/piagent@v1.1.6
83
83
  ```
84
84
 
85
85
  Direct `pi install` does not create `piagent-*` commands on `PATH`.
@@ -21,7 +21,7 @@ This document covers the packaged guard, project profiles, capability locks, ins
21
21
  - The operator, operating-system account, Pi host, and explicitly trusted project repository are trusted to execute code.
22
22
  - Project-local Pi resources are loaded only after Pi project trust or an explicit operator override.
23
23
  - A human remains available to approve destructive and external-provider actions.
24
- - Supported release verification covers macOS Apple Silicon + Bash, Linux x64 + Bash, Node.js `>=22.19.0`, and the exact Pi host version declared by the release. macOS Intel + Bash and Linux ARM64 + Bash are supported targets that need local smoke verification before broad rollout. Native Windows is not a team-rollout target for v1.1.5, and WSL2 is experimental/unverified.
24
+ - Supported release verification covers macOS Apple Silicon + Bash, Linux x64 + Bash, Node.js `>=22.19.0`, and the exact Pi host version declared by the release. macOS Intel + Bash and Linux ARM64 + Bash are supported targets that need local smoke verification before broad rollout. Native Windows is not a team-rollout target for v1.1.6, and WSL2 is experimental/unverified.
25
25
  - Registry, GitHub, model-provider, MCP-provider, and Vercel controls are external dependencies. Their account security and platform guarantees are not replaced by this repository.
26
26
 
27
27
  ## Threat actors and failure modes
@@ -9,7 +9,7 @@ Một thành viên mới không cần biết local path của maintainer. Luồn
9
9
  ```bash
10
10
  node --version # >= 22.19.0
11
11
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
12
- npm install -g --ignore-scripts @piagent/platform@1.1.5
12
+ npm install -g --ignore-scripts @piagent/platform@1.1.6
13
13
  piagent-install --stable
14
14
  cd /path/to/project
15
15
  pi
@@ -42,7 +42,7 @@ Mặc định team dùng stable helper:
42
42
  ```bash
43
43
  node --version # >= 22.19.0
44
44
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
45
- npm install -g --ignore-scripts @piagent/platform@1.1.5
45
+ npm install -g --ignore-scripts @piagent/platform@1.1.6
46
46
  piagent-install --stable --dry-run
47
47
  piagent-install --stable
48
48
  ```
@@ -70,7 +70,7 @@ piagent-update --check
70
70
  piagent-update --project /path/to/project
71
71
  ```
72
72
 
73
- `--check` chỉ báo version, không đụng gì. Lên đúng một bản cụ thể thì `piagent-update --version 1.1.5`.
73
+ `--check` chỉ báo version, không đụng gì. Lên đúng một bản cụ thể thì `piagent-update --version 1.1.6`.
74
74
 
75
75
  Sequence thủ công vẫn dùng được, và bắt buộc theo thứ tự này — `piagent-install` **fail** nếu Pi host lệch bản pin, nó không tự nâng host:
76
76
 
@@ -798,7 +798,7 @@
798
798
  <span>Pi Agent Platform</span>
799
799
  </a>
800
800
  <div class="top-actions">
801
- <span class="pill"><span class="pill-dot"></span>v1.1.5 docs</span>
801
+ <span class="pill"><span class="pill-dot"></span>v1.1.6 docs</span>
802
802
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
803
803
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
804
804
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -934,7 +934,7 @@
934
934
  <article class="step">
935
935
  <div>
936
936
  <h3>Cài — hai lệnh</h3>
937
- <p>Cần Node.js 22.19.0 trở lên. Không cần cài Pi Coding Agent trước: <span class="inline-code">piagent-setup</span> tự cài đúng bản <span class="inline-code">0.82.0</span> mà release <span class="inline-code">v1.1.5</span> đang pin. Chạy nó ngay trong thư mục project — nó cài Pi package, khởi tạo <span class="inline-code">.pi/</span>, cài MCP baseline cùng subagents, rồi chạy doctor. Thêm <span class="inline-code">--no-mcp</span>, <span class="inline-code">--no-subagents</span>, <span class="inline-code">--no-herdr</span> hoặc <span class="inline-code">--global-only</span> để bỏ bớt.</p>
937
+ <p>Cần Node.js 22.19.0 trở lên. Không cần cài Pi Coding Agent trước: <span class="inline-code">piagent-setup</span> tự cài đúng bản <span class="inline-code">0.82.0</span> mà release <span class="inline-code">v1.1.6</span> đang pin. Chạy nó ngay trong thư mục project — nó cài Pi package, khởi tạo <span class="inline-code">.pi/</span>, cài MCP baseline cùng subagents, rồi chạy doctor. Thêm <span class="inline-code">--no-mcp</span>, <span class="inline-code">--no-subagents</span>, <span class="inline-code">--no-herdr</span> hoặc <span class="inline-code">--global-only</span> để bỏ bớt.</p>
938
938
  <div class="table-wrap compact-table">
939
939
  <table>
940
940
  <thead>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/platform",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -795,7 +795,11 @@ function buildCoreIntegrity(root) {
795
795
  "packages/piagent-core/extensions/guard-shell-analysis.ts",
796
796
  "packages/piagent-core/extensions/policy-core.js",
797
797
  "packages/piagent-core/extensions/redaction-core.js",
798
- "packages/piagent-core/extensions/runtime-evidence.js"
798
+ "packages/piagent-core/extensions/runtime-evidence.js",
799
+ // project-shape.js picks the profile a project is offered, and the profile is
800
+ // the document that carries protectedPaths and readOnlyPaths. Steering the
801
+ // recommendation is a way to choose what the guard will end up enforcing.
802
+ "packages/piagent-core/extensions/project-shape.js"
799
803
  ];
800
804
  return files.map((relativePath) => {
801
805
  const resolved = resolveRegularFile(root, relativePath, MAX_ARTIFACT_BYTES);
@@ -39,6 +39,7 @@ import {
39
39
  redactForStorage,
40
40
  redactSensitiveText
41
41
  } from "./redaction-core.js";
42
+ import { detectProfileName } from "./project-shape.js";
42
43
  import {
43
44
  resolveCapabilityProfileDocument,
44
45
  verifyCapabilityLock,
@@ -2636,75 +2637,6 @@ function readAdapterProfiles(extensionDir: string): Array<{ name: string; profil
2636
2637
  .sort((a, b) => a.name.localeCompare(b.name));
2637
2638
  }
2638
2639
 
2639
- function packageHasAny(cwd: string, pattern: RegExp): boolean {
2640
- try {
2641
- return pattern.test(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
2642
- } catch {
2643
- return false;
2644
- }
2645
- }
2646
-
2647
- function detectProfileName(cwd: string, intent?: string): { name: string; reason: string } {
2648
- const normalizedIntent = intent?.trim().toLowerCase();
2649
- if (normalizedIntent === "be-readonly-fe") return { name: "be-readonly-fe", reason: "User intent says backend should be read-only while frontend is the write target." };
2650
- if (normalizedIntent === "frontend-only") return { name: "web-frontend", reason: "User intent says frontend-only." };
2651
- if (normalizedIntent === "backend-only") return { name: "backend-api", reason: "User intent says backend-only." };
2652
- if (normalizedIntent === "docs") return { name: "docs", reason: "User intent says docs/docs-only." };
2653
-
2654
- const hasPackage = fs.existsSync(path.join(cwd, "package.json"));
2655
- let frontend = false;
2656
- let backend = false;
2657
- let data = false;
2658
- let mobile = false;
2659
- let infra = false;
2660
- let docs = false;
2661
-
2662
- if (fs.existsSync(path.join(cwd, "pubspec.yaml")) || (fs.existsSync(path.join(cwd, "android")) && fs.existsSync(path.join(cwd, "ios")))) mobile = true;
2663
- if (fs.existsSync(path.join(cwd, "dbt_project.yml")) || fs.existsSync(path.join(cwd, "dvc.yaml")) || fs.existsSync(path.join(cwd, "notebooks")) || fs.existsSync(path.join(cwd, "data"))) data = true;
2664
- if (fs.existsSync(path.join(cwd, "Dockerfile")) || fs.existsSync(path.join(cwd, "docker-compose.yml")) || fs.existsSync(path.join(cwd, "compose.yml")) || fs.existsSync(path.join(cwd, "compose.yaml")) || fs.existsSync(path.join(cwd, "terraform")) || fs.existsSync(path.join(cwd, "infra")) || fs.existsSync(path.join(cwd, "k8s")) || fs.existsSync(path.join(cwd, "helm"))) infra = true;
2665
- if (fs.existsSync(path.join(cwd, "docs")) || fs.existsSync(path.join(cwd, "mkdocs.yml")) || fs.existsSync(path.join(cwd, "mint.json")) || fs.existsSync(path.join(cwd, "docusaurus.config.js"))) docs = true;
2666
-
2667
- if (hasPackage) {
2668
- if (fs.existsSync(path.join(cwd, "frontend")) || fs.existsSync(path.join(cwd, "apps/web")) || fs.existsSync(path.join(cwd, "apps/frontend"))) frontend = true;
2669
- if (fs.existsSync(path.join(cwd, "backend")) || fs.existsSync(path.join(cwd, "apps/api")) || fs.existsSync(path.join(cwd, "apps/server"))) backend = true;
2670
- if (packageHasAny(cwd, /"(next|react|vite|vue|svelte|astro|@angular\/core|remix)"/i)
2671
- || fs.existsSync(path.join(cwd, "next.config.js"))
2672
- || fs.existsSync(path.join(cwd, "next.config.mjs"))
2673
- || fs.existsSync(path.join(cwd, "next.config.ts"))
2674
- || fs.existsSync(path.join(cwd, "vite.config.js"))
2675
- || fs.existsSync(path.join(cwd, "vite.config.ts"))
2676
- || fs.existsSync(path.join(cwd, "src/app"))
2677
- || fs.existsSync(path.join(cwd, "pages"))
2678
- || fs.existsSync(path.join(cwd, "public"))) frontend = true;
2679
- if (packageHasAny(cwd, /"(@nestjs|express|fastify|hono|koa|apollo-server|graphql-yoga|prisma|typeorm|sequelize|drizzle-orm)"/i)
2680
- || fs.existsSync(path.join(cwd, "nest-cli.json"))
2681
- || fs.existsSync(path.join(cwd, "prisma"))
2682
- || fs.existsSync(path.join(cwd, "src/server"))
2683
- || fs.existsSync(path.join(cwd, "src/api"))) backend = true;
2684
- }
2685
-
2686
- if (fs.existsSync(path.join(cwd, "pom.xml")) || fs.existsSync(path.join(cwd, "build.gradle")) || fs.existsSync(path.join(cwd, "build.gradle.kts")) || fs.existsSync(path.join(cwd, "src/main/java")) || fs.existsSync(path.join(cwd, "src/main/kotlin"))) backend = true;
2687
-
2688
- if (fs.existsSync(path.join(cwd, "pyproject.toml"))) {
2689
- try {
2690
- if (/(fastapi|flask|django|litestar|starlite)/i.test(fs.readFileSync(path.join(cwd, "pyproject.toml"), "utf8"))) backend = true;
2691
- } catch {
2692
- // ignore unreadable pyproject for recommendation
2693
- }
2694
- }
2695
-
2696
- if (mobile) return { name: "mobile", reason: "Mobile markers found." };
2697
- if (frontend && backend) return { name: "fullstack", reason: "Frontend and backend markers both found. Pick be-readonly-fe instead if backend must be read-only." };
2698
- if (frontend) return { name: "web-frontend", reason: "Frontend framework markers found." };
2699
- if (backend) return { name: "backend-api", reason: "Backend/API markers found." };
2700
- if (data) return { name: "data", reason: "Data/ETL markers found." };
2701
- if (fs.existsSync(path.join(cwd, "pyproject.toml"))) return { name: "python", reason: "Python pyproject.toml found." };
2702
- if (hasPackage && fs.existsSync(path.join(cwd, "tsconfig.json"))) return { name: "node-typescript", reason: "Node TypeScript markers found." };
2703
- if (infra) return { name: "devops", reason: "Infrastructure markers found." };
2704
- if (docs) return { name: "docs", reason: "Documentation markers found." };
2705
- return { name: "generic", reason: "No stronger project markers found." };
2706
- }
2707
-
2708
2640
  function profileDescription(name: string): string {
2709
2641
  const descriptions: Record<string, string> = {
2710
2642
  generic: "Safe baseline for unknown repos.",
@@ -0,0 +1,285 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ // The shape of a repository, in the only terms the profiles care about: is there
5
+ // a frontend, is there a backend, and what else is sitting at the root. Both the
6
+ // runtime recommendation (`piagent_profile_options`) and the shell initialiser
7
+ // (`scripts/init-project.sh`) read it from here. They used to carry separate
8
+ // copies of the same rules, which is how `apps/backend` ended up being invisible
9
+ // to one of them for several releases.
10
+
11
+ // Directories that name a side of the stack outright. These are matched at the
12
+ // repository root and one level under each workspace root, never at arbitrary
13
+ // depth: `src/api` inside a frontend package is an HTTP client, not a backend.
14
+ const FRONTEND_DIR_NAMES = ["frontend", "web", "client", "ui"];
15
+ const BACKEND_DIR_NAMES = ["backend", "server", "api"];
16
+ const WORKSPACE_ROOTS = ["apps", "packages", "services"];
17
+
18
+ const FRONTEND_DEPENDENCIES = /"(next|react|vite|vue|svelte|astro|@angular\/core|remix)"/i;
19
+ const BACKEND_DEPENDENCIES = /"(@nestjs|express|fastify|hono|koa|apollo-server|graphql-yoga|prisma|typeorm|sequelize|drizzle-orm)"/i;
20
+ const PYTHON_BACKEND_FRAMEWORKS = /(fastapi|flask|django|litestar|starlite)/i;
21
+
22
+ const FRONTEND_MARKER_FILES = [
23
+ "next.config.js",
24
+ "next.config.mjs",
25
+ "next.config.ts",
26
+ "vite.config.js",
27
+ "vite.config.ts",
28
+ "src/app",
29
+ "pages",
30
+ "public"
31
+ ];
32
+ const NODE_BACKEND_MARKER_FILES = ["nest-cli.json", "prisma", "src/server", "src/api"];
33
+ // Markers that identify a backend on their own, with no manifest to lean on.
34
+ const NATIVE_BACKEND_MARKER_FILES = [
35
+ "pom.xml",
36
+ "build.gradle",
37
+ "build.gradle.kts",
38
+ "src/main/java",
39
+ "src/main/kotlin"
40
+ ];
41
+
42
+ // A workspace file can declare any number of packages. Reading a bounded number
43
+ // of them keeps a repository with hundreds of packages from turning profile
44
+ // recommendation into a directory walk.
45
+ const MAX_WORKSPACE_PACKAGES = 64;
46
+
47
+ function exists(dir, relative) {
48
+ try {
49
+ return fs.existsSync(path.join(dir, relative));
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ // The stack-naming markers are directories. A file called `api` says nothing
56
+ // about a repository having a backend in it.
57
+ function isDirectory(dir, relative) {
58
+ try {
59
+ return fs.statSync(path.join(dir, relative)).isDirectory();
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ function readFile(dir, relative) {
66
+ try {
67
+ return fs.readFileSync(path.join(dir, relative), "utf8");
68
+ } catch {
69
+ return undefined;
70
+ }
71
+ }
72
+
73
+ function readJson(dir, relative) {
74
+ const text = readFile(dir, relative);
75
+ if (text === undefined) return undefined;
76
+ try {
77
+ return JSON.parse(text);
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ function stripQuotes(value) {
84
+ return value.trim().replace(/^['"]/, "").replace(/['"]$/, "").trim();
85
+ }
86
+
87
+ // npm and yarn put the globs in package.json, pnpm puts them in its own file.
88
+ // Both are read: a repository is free to carry either, and some carry both.
89
+ function readWorkspacePatterns(cwd) {
90
+ const patterns = [];
91
+
92
+ const declared = readJson(cwd, "package.json")?.workspaces;
93
+ if (Array.isArray(declared)) {
94
+ patterns.push(...declared);
95
+ } else if (Array.isArray(declared?.packages)) {
96
+ patterns.push(...declared.packages);
97
+ }
98
+
99
+ patterns.push(...readPnpmWorkspacePatterns(cwd));
100
+ return patterns.filter((pattern) => typeof pattern === "string");
101
+ }
102
+
103
+ // A three-line YAML reader rather than a YAML dependency: this package ships
104
+ // with no runtime dependencies, and the shape being read is a single list of
105
+ // strings under one key.
106
+ function readPnpmWorkspacePatterns(cwd) {
107
+ const text = readFile(cwd, "pnpm-workspace.yaml") ?? readFile(cwd, "pnpm-workspace.yml");
108
+ if (text === undefined) return [];
109
+
110
+ const patterns = [];
111
+ let insidePackages = false;
112
+ for (const rawLine of text.split(/\r?\n/)) {
113
+ const line = rawLine.replace(/#.*$/, "");
114
+ if (/^packages\s*:/.test(line)) {
115
+ insidePackages = true;
116
+ continue;
117
+ }
118
+ if (!insidePackages) continue;
119
+
120
+ const item = line.match(/^\s+-\s*(.+?)\s*$/);
121
+ if (item) {
122
+ patterns.push(stripQuotes(item[1]));
123
+ continue;
124
+ }
125
+ // A blank line stays inside the list; anything else starts the next key.
126
+ if (line.trim().length > 0) break;
127
+ }
128
+ return patterns;
129
+ }
130
+
131
+ function expandWorkspacePattern(cwd, pattern) {
132
+ const normalized = pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
133
+ // `!` excludes rather than includes, and neither an absolute path nor one that
134
+ // climbs out of the repository is a workspace of this repository.
135
+ if (!normalized || normalized.startsWith("!") || normalized.startsWith("/")) return [];
136
+
137
+ const segments = normalized.split("/").filter((segment) => segment && segment !== ".");
138
+ if (segments.length === 0 || segments.includes("..")) return [];
139
+
140
+ const wildcardAt = segments.findIndex((segment) => segment.includes("*"));
141
+ if (wildcardAt === -1) return [segments.join("/")];
142
+ // Only a trailing wildcard is expanded. `packages/*` and `apps/**` are what
143
+ // workspace files actually declare; anything deeper is left to the directory
144
+ // and dependency markers instead of growing a glob engine in here.
145
+ if (wildcardAt !== segments.length - 1) return [];
146
+
147
+ const base = segments.slice(0, wildcardAt).join("/");
148
+ let entries;
149
+ try {
150
+ entries = fs.readdirSync(base ? path.join(cwd, base) : cwd, { withFileTypes: true });
151
+ } catch {
152
+ return [];
153
+ }
154
+ return entries
155
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
156
+ .map((entry) => (base ? `${base}/${entry.name}` : entry.name));
157
+ }
158
+
159
+ export function readWorkspacePackageDirs(cwd) {
160
+ const found = [];
161
+ const seen = new Set();
162
+ for (const pattern of readWorkspacePatterns(cwd)) {
163
+ for (const relative of expandWorkspacePattern(cwd, pattern)) {
164
+ if (seen.has(relative)) continue;
165
+ if (!exists(cwd, relative)) continue;
166
+ seen.add(relative);
167
+ found.push(relative);
168
+ if (found.length >= MAX_WORKSPACE_PACKAGES) return found;
169
+ }
170
+ }
171
+ return found;
172
+ }
173
+
174
+ // What one directory says about itself, from its manifest and its own layout.
175
+ // Applied to the repository root and to every declared workspace package, so a
176
+ // pnpm monorepo whose packages are named `web` and `api` is read the same way a
177
+ // single-package repository is.
178
+ function directorySignals(dir) {
179
+ const manifest = readFile(dir, "package.json");
180
+ let frontend = false;
181
+ let backend = false;
182
+
183
+ if (manifest !== undefined) {
184
+ frontend = FRONTEND_DEPENDENCIES.test(manifest);
185
+ backend = BACKEND_DEPENDENCIES.test(manifest);
186
+ // Node layout markers only mean something next to a manifest. `public/` and
187
+ // `pages/` are ordinary directory names in repositories that have nothing to
188
+ // do with a web framework.
189
+ if (!frontend) frontend = FRONTEND_MARKER_FILES.some((marker) => exists(dir, marker));
190
+ if (!backend) backend = NODE_BACKEND_MARKER_FILES.some((marker) => exists(dir, marker));
191
+ }
192
+
193
+ if (!backend) backend = NATIVE_BACKEND_MARKER_FILES.some((marker) => exists(dir, marker));
194
+
195
+ const pyproject = readFile(dir, "pyproject.toml");
196
+ if (pyproject !== undefined && PYTHON_BACKEND_FRAMEWORKS.test(pyproject)) backend = true;
197
+
198
+ return { frontend, backend };
199
+ }
200
+
201
+ function shapeFromDirName(name) {
202
+ const normalized = name.toLowerCase();
203
+ if (FRONTEND_DIR_NAMES.includes(normalized)) return "frontend";
204
+ if (BACKEND_DIR_NAMES.includes(normalized)) return "backend";
205
+ return undefined;
206
+ }
207
+
208
+ export function detectProjectShape(cwd) {
209
+ const hasPackage = exists(cwd, "package.json");
210
+ const shape = {
211
+ hasPackage,
212
+ frontend: false,
213
+ backend: false,
214
+ data: false,
215
+ mobile: false,
216
+ infra: false,
217
+ docs: false,
218
+ workspacePackages: []
219
+ };
220
+
221
+ if (exists(cwd, "pubspec.yaml") || (exists(cwd, "android") && exists(cwd, "ios"))) shape.mobile = true;
222
+ if (exists(cwd, "dbt_project.yml") || exists(cwd, "dvc.yaml") || exists(cwd, "notebooks") || exists(cwd, "data")) {
223
+ shape.data = true;
224
+ }
225
+ if (exists(cwd, "Dockerfile") || exists(cwd, "docker-compose.yml") || exists(cwd, "compose.yml")
226
+ || exists(cwd, "compose.yaml") || exists(cwd, "terraform") || exists(cwd, "infra")
227
+ || exists(cwd, "k8s") || exists(cwd, "helm")) {
228
+ shape.infra = true;
229
+ }
230
+ if (exists(cwd, "docs") || exists(cwd, "mkdocs.yml") || exists(cwd, "mint.json") || exists(cwd, "docusaurus.config.js")) {
231
+ shape.docs = true;
232
+ }
233
+
234
+ // Directory names are read whether or not the root carries a package.json.
235
+ // A Django and React repository has `frontend/` and `backend/` and no root
236
+ // manifest at all; gating this on package.json used to hide both sides of it.
237
+ for (const name of [...FRONTEND_DIR_NAMES, ...BACKEND_DIR_NAMES]) {
238
+ const side = shapeFromDirName(name);
239
+ if (isDirectory(cwd, name)) shape[side] = true;
240
+ for (const root of WORKSPACE_ROOTS) {
241
+ if (isDirectory(cwd, `${root}/${name}`)) shape[side] = true;
242
+ }
243
+ }
244
+
245
+ const rootSignals = directorySignals(cwd);
246
+ if (rootSignals.frontend) shape.frontend = true;
247
+ if (rootSignals.backend) shape.backend = true;
248
+
249
+ shape.workspacePackages = readWorkspacePackageDirs(cwd);
250
+ for (const relative of shape.workspacePackages) {
251
+ const side = shapeFromDirName(path.posix.basename(relative));
252
+ if (side) shape[side] = true;
253
+
254
+ const signals = directorySignals(path.join(cwd, relative));
255
+ if (signals.frontend) shape.frontend = true;
256
+ if (signals.backend) shape.backend = true;
257
+ }
258
+
259
+ return shape;
260
+ }
261
+
262
+ export function detectProfileName(cwd, intent) {
263
+ const normalizedIntent = typeof intent === "string" ? intent.trim().toLowerCase() : "";
264
+ if (normalizedIntent === "be-readonly-fe") {
265
+ return { name: "be-readonly-fe", reason: "User intent says backend should be read-only while frontend is the write target." };
266
+ }
267
+ if (normalizedIntent === "frontend-only") return { name: "web-frontend", reason: "User intent says frontend-only." };
268
+ if (normalizedIntent === "backend-only") return { name: "backend-api", reason: "User intent says backend-only." };
269
+ if (normalizedIntent === "docs") return { name: "docs", reason: "User intent says docs/docs-only." };
270
+
271
+ const shape = detectProjectShape(cwd);
272
+
273
+ if (shape.mobile) return { name: "mobile", reason: "Mobile markers found." };
274
+ if (shape.frontend && shape.backend) {
275
+ return { name: "fullstack", reason: "Frontend and backend markers both found. Pick be-readonly-fe instead if backend must be read-only." };
276
+ }
277
+ if (shape.frontend) return { name: "web-frontend", reason: "Frontend framework markers found." };
278
+ if (shape.backend) return { name: "backend-api", reason: "Backend/API markers found." };
279
+ if (shape.data) return { name: "data", reason: "Data/ETL markers found." };
280
+ if (exists(cwd, "pyproject.toml")) return { name: "python", reason: "Python pyproject.toml found." };
281
+ if (shape.hasPackage && exists(cwd, "tsconfig.json")) return { name: "node-typescript", reason: "Node TypeScript markers found." };
282
+ if (shape.infra) return { name: "devops", reason: "Infrastructure markers found." };
283
+ if (shape.docs) return { name: "docs", reason: "Documentation markers found." };
284
+ return { name: "generic", reason: "No stronger project markers found." };
285
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -102,97 +102,20 @@ fi
102
102
 
103
103
  PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
104
104
 
105
- package_has_any() {
106
- local project="$1"
107
- local pattern="$2"
108
- [[ -f "$project/package.json" ]] && grep -Eiq "$pattern" "$project/package.json"
109
- }
110
-
105
+ # Profile detection lives in packages/piagent-core/extensions/project-shape.js so
106
+ # that this script and the runtime `piagent_profile_options` tool recommend the
107
+ # same profile. They used to hold separate copies of the rules and drifted apart.
111
108
  detect_profile() {
112
109
  local project="$1"
113
- local has_package=false
114
- local frontend=false
115
- local backend=false
116
- local data=false
117
- local mobile=false
118
- local devops=false
119
- local infra=false
120
- local docs=false
121
-
122
- [[ -f "$project/package.json" ]] && has_package=true
123
-
124
- if [[ -f "$project/pubspec.yaml" ]] || { [[ -d "$project/android" ]] && [[ -d "$project/ios" ]]; }; then
125
- mobile=true
126
- fi
127
-
128
- if [[ -f "$project/dbt_project.yml" ]] || [[ -f "$project/dvc.yaml" ]] || [[ -d "$project/notebooks" ]] || [[ -d "$project/data" ]]; then
129
- data=true
130
- fi
131
-
132
- if [[ -f "$project/Dockerfile" ]] || [[ -f "$project/docker-compose.yml" ]] || [[ -f "$project/compose.yml" ]] || [[ -f "$project/compose.yaml" ]] || [[ -d "$project/terraform" ]] || [[ -d "$project/infra" ]] || [[ -d "$project/k8s" ]] || [[ -d "$project/helm" ]]; then
133
- infra=true
134
- devops=true
135
- elif [[ -d "$project/.github/workflows" ]]; then
136
- devops=true
137
- fi
138
-
139
- if [[ -d "$project/docs" ]] || [[ -f "$project/mkdocs.yml" ]] || [[ -f "$project/mint.json" ]] || [[ -f "$project/docusaurus.config.js" ]]; then
140
- docs=true
141
- fi
142
-
143
- if [[ "$has_package" == true ]]; then
144
- if [[ -d "$project/frontend" ]] || [[ -d "$project/apps/web" ]] || [[ -d "$project/apps/frontend" ]]; then
145
- frontend=true
146
- fi
147
-
148
- if [[ -d "$project/backend" ]] || [[ -d "$project/apps/api" ]] || [[ -d "$project/apps/server" ]]; then
149
- backend=true
150
- fi
151
-
152
- if package_has_any "$project" '"(next|react|vite|vue|svelte|astro|@angular/core|remix)"' \
153
- || [[ -f "$project/next.config.js" ]] || [[ -f "$project/next.config.mjs" ]] || [[ -f "$project/next.config.ts" ]] \
154
- || [[ -f "$project/vite.config.js" ]] || [[ -f "$project/vite.config.ts" ]] \
155
- || [[ -d "$project/src/app" ]] || [[ -d "$project/pages" ]] || [[ -d "$project/public" ]]; then
156
- frontend=true
157
- fi
158
-
159
- if package_has_any "$project" '"(@nestjs|express|fastify|hono|koa|apollo-server|graphql-yoga|prisma|typeorm|sequelize|drizzle-orm)"' \
160
- || [[ -f "$project/nest-cli.json" ]] || [[ -d "$project/prisma" ]] || [[ -d "$project/src/server" ]] || [[ -d "$project/src/api" ]]; then
161
- backend=true
162
- fi
163
- fi
164
-
165
- if [[ -f "$project/pom.xml" ]] || [[ -f "$project/build.gradle" ]] || [[ -f "$project/build.gradle.kts" ]] || [[ -d "$project/src/main/java" ]] || [[ -d "$project/src/main/kotlin" ]]; then
166
- backend=true
167
- fi
168
-
169
- if [[ -f "$project/pyproject.toml" ]] && grep -Eiq "(fastapi|flask|django|litestar|starlite)" "$project/pyproject.toml"; then
170
- backend=true
171
- fi
172
-
173
- if [[ "$mobile" == true ]]; then
174
- printf '%s\n' "mobile"
175
- elif [[ "$frontend" == true && "$backend" == true ]]; then
176
- printf '%s\n' "fullstack"
177
- elif [[ "$frontend" == true ]]; then
178
- printf '%s\n' "web-frontend"
179
- elif [[ "$backend" == true ]]; then
180
- printf '%s\n' "backend-api"
181
- elif [[ "$data" == true ]]; then
182
- printf '%s\n' "data"
183
- elif [[ -f "$project/pyproject.toml" ]]; then
184
- printf '%s\n' "python"
185
- elif [[ "$has_package" == true && -f "$project/tsconfig.json" ]]; then
186
- printf '%s\n' "node-typescript"
187
- elif [[ "$infra" == true ]]; then
188
- printf '%s\n' "devops"
189
- elif [[ "$docs" == true ]]; then
190
- printf '%s\n' "docs"
191
- elif [[ "$devops" == true ]]; then
192
- printf '%s\n' "devops"
193
- else
194
- printf '%s\n' "generic"
195
- fi
110
+ node --input-type=module - "$PLATFORM_ROOT" "$project" <<'NODE'
111
+ import path from "node:path";
112
+ import { pathToFileURL } from "node:url";
113
+
114
+ const [platformRoot, project] = process.argv.slice(2);
115
+ const modulePath = path.join(platformRoot, "packages/piagent-core/extensions/project-shape.js");
116
+ const { detectProfileName } = await import(pathToFileURL(modulePath).href);
117
+ process.stdout.write(`${detectProfileName(project).name}\n`);
118
+ NODE
196
119
  }
197
120
 
198
121
  resolve_profile_path() {
@@ -15,7 +15,7 @@ Package source examples:
15
15
  # Installs the stable tag after resolving it to a commit SHA.
16
16
 
17
17
  # Pin a specific release:
18
- scripts/install-global.sh --version v1.1.5 --resolve-tag
18
+ scripts/install-global.sh --version v1.1.6 --resolve-tag
19
19
 
20
20
  # Preview the planned install/update without changing user config:
21
21
  scripts/install-global.sh --stable --dry-run
@@ -127,7 +127,7 @@ detect_runtime_surface() {
127
127
  printf 'native-windows/%s (not release-gated; terminal helpers require Bash semantics)' "$arch_name"
128
128
  ;;
129
129
  *)
130
- printf '%s/%s (outside v1.1.5 release matrix)' "$os_name" "$arch_name"
130
+ printf '%s/%s (outside v1.1.6 release matrix)' "$os_name" "$arch_name"
131
131
  ;;
132
132
  esac
133
133
  }
@@ -135,7 +135,7 @@ detect_runtime_surface() {
135
135
  warn_runtime_surface_if_needed() {
136
136
  local surface="$1"
137
137
  case "$surface" in
138
- *"supported target"*|*"experimental"*|*"not release-gated"*|*"outside v1.1.5 release matrix"*)
138
+ *"supported target"*|*"experimental"*|*"not release-gated"*|*"outside v1.1.6 release matrix"*)
139
139
  echo "WARN: runtime surface is $surface." >&2
140
140
  echo "WARN: For team rollout, run piagent-doctor plus the project smoke/verify suite on this machine before relying on it." >&2
141
141
  ;;
@@ -137,7 +137,7 @@ function runtimeMatrixStatus() {
137
137
  surface: `${platform}/${arch}`,
138
138
  status: "outside-release-matrix",
139
139
  teamRolloutReady: false,
140
- note: "This runtime is outside the documented v1.1.5 support matrix."
140
+ note: "This runtime is outside the documented v1.1.6 support matrix."
141
141
  };
142
142
  }
143
143