@piagent/platform 1.2.2 → 1.2.4

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 (73) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +34 -27
  3. package/catalog/capabilities.json +8 -8
  4. package/docs/auto-delegation-policy.md +4 -4
  5. package/docs/command-reference-vietnamese.md +123 -86
  6. package/docs/context-window-policy.md +7 -7
  7. package/docs/distribution-standard.md +49 -11
  8. package/docs/herdr-workflow.md +3 -2
  9. package/docs/memory-policy.md +6 -3
  10. package/docs/operator-manual-vietnamese.md +81 -72
  11. package/docs/package-architecture-notes.md +6 -6
  12. package/docs/project-adapters.md +4 -4
  13. package/docs/project-onboarding.md +12 -11
  14. package/docs/publishing-for-teams.md +2 -2
  15. package/docs/quickstart-vietnamese.md +25 -23
  16. package/docs/release-install-policy.md +23 -7
  17. package/docs/runtime-harness-standard.md +7 -7
  18. package/docs/runtime-policy-design.md +1 -1
  19. package/docs/runtime-quality-baseline.md +1 -1
  20. package/docs/security-threat-model.md +1 -1
  21. package/docs/subagent-orchestration-capabilities.md +3 -3
  22. package/docs/subagents-and-multiagent.md +4 -4
  23. package/docs/task-implementation-contract.md +4 -4
  24. package/docs/task-lifecycle-vietnamese.md +2 -2
  25. package/docs/team-onboarding.md +39 -19
  26. package/docs/usage-observability.md +13 -12
  27. package/docs/workflow-recipes.md +12 -9
  28. package/docs-site/architecture.html +1 -1
  29. package/docs-site/commands.html +19 -15
  30. package/docs-site/content/commands.html +18 -14
  31. package/docs-site/content/permissions.html +11 -11
  32. package/docs-site/content/quickstart.html +12 -11
  33. package/docs-site/content/team.html +1 -1
  34. package/docs-site/content/workflows.html +17 -17
  35. package/docs-site/index.html +1 -1
  36. package/docs-site/mcp-approval.html +1 -1
  37. package/docs-site/mcp-auth.html +1 -1
  38. package/docs-site/mcp-servers.html +1 -1
  39. package/docs-site/mcp.html +1 -1
  40. package/docs-site/permissions.html +12 -12
  41. package/docs-site/profiles.html +1 -1
  42. package/docs-site/quickstart.html +13 -12
  43. package/docs-site/runtime.html +1 -1
  44. package/docs-site/scope.html +1 -1
  45. package/docs-site/security.html +1 -1
  46. package/docs-site/team.html +2 -2
  47. package/docs-site/workflows.html +18 -18
  48. package/package.json +1 -1
  49. package/packages/piagent-core/README.md +27 -27
  50. package/packages/piagent-core/extensions/piagent-guard.ts +1055 -190
  51. package/packages/piagent-core/package.json +1 -1
  52. package/packages/piagent-core/prompts/be-to-fe.md +3 -3
  53. package/packages/piagent-core/prompts/discuss.md +2 -2
  54. package/packages/piagent-core/prompts/plan.md +1 -1
  55. package/packages/piagent-core/prompts/platform-improve.md +1 -1
  56. package/packages/piagent-core/prompts/pr.md +1 -1
  57. package/packages/piagent-core/prompts/review.md +1 -1
  58. package/packages/piagent-core/prompts/scout.md +2 -2
  59. package/packages/piagent-core/prompts/task.md +2 -2
  60. package/scripts/init-project.sh +4 -3
  61. package/scripts/install-global.sh +47 -1
  62. package/scripts/profile-doctor.sh +1 -1
  63. package/scripts/setup.sh +4 -3
  64. package/scripts/team-doctor.sh +6 -8
  65. package/scripts/update-global.mjs +207 -11
  66. package/scripts/verify-local.sh +18 -17
  67. package/templates/project/.pi/context-index.json +2 -2
  68. package/templates/project/.pi/project-context.md +3 -3
  69. package/templates/project/AGENTS.md +4 -4
  70. package/packages/piagent-core/prompts/memory-policy.md +0 -52
  71. package/packages/piagent-core/prompts/model-options.md +0 -68
  72. package/packages/piagent-core/prompts/onboard-project.md +0 -147
  73. package/packages/piagent-core/prompts/piagent-commands.md +0 -69
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -20,11 +20,11 @@ Use this for tasks like:
20
20
 
21
21
  Mandatory flow:
22
22
 
23
- 0. Call `piagent_context_preflight` with `workflow=be-to-fe`. If it recommends `fresh-session`, stop loading context in this session and tell the user to use `/fresh-be-to-fe <request>` unless this command already runs in a fresh session.
23
+ 0. Call `piagent_context_preflight` with `workflow=be-to-fe`. If it recommends `fresh-session`, stop loading context in this session and tell the user to use `/piagent-session fresh be-to-fe <request>` unless this command already runs in a fresh session.
24
24
  1. Call `piagent_context` with `detail=full` and confirm profile/protected backend paths.
25
25
  2. Call `piagent_orchestration_policy`; keep the workflow solo-first unless bounded BE/FE scouting or review is worth the extra token/tool cost.
26
26
  3. Call `piagent_memory_status`; search memory or Field Guide for prior BE/FE mapping decisions if relevant, record citations with `piagent_memory_citation_record`, then verify against current BE/FE files.
27
- 4. Read `.pi/project-context.md`. If pending, stop and ask for `/onboard-project`.
27
+ 4. Read `.pi/project-context.md`. If pending, stop and ask for `/onboard-project run`.
28
28
  5. Read required context from the active profile and call `piagent_context_budget` for BE/FE files that look large.
29
29
  6. Classify the task:
30
30
  - BE scout: read-only.
@@ -67,7 +67,7 @@ Mandatory flow:
67
67
 
68
68
  For generic projects, use profile `be-readonly-fe` when the repo policy is “BE scout only, FE write allowed”.
69
69
 
70
- Do not ask the user to paste the mandatory flow. The platform prompt already contains it. If the user asks only for scout/audit, stay read-only and prefer `/scout` or `/fresh-scout`.
70
+ Do not ask the user to paste the mandatory flow. The platform prompt already contains it. If the user asks only for scout/audit, stay read-only and prefer `/workflow scout` or `/piagent-session fresh scout`.
71
71
 
72
72
  Final output:
73
73
 
@@ -13,7 +13,7 @@ Operating rules:
13
13
 
14
14
  1. Call `piagent_context` first when available.
15
15
  2. Call `piagent_memory_status`; use memory only to avoid repeated context questions, not as authority.
16
- 3. Read `.pi/project-context.md` if available; if it is still pending, recommend `/onboard-project` before implementation.
16
+ 3. Read `.pi/project-context.md` if available; if it is still pending, recommend `/onboard-project run` before implementation.
17
17
  4. Inspect relevant files/docs before asking; do not ask what can be answered from the project.
18
18
  5. Identify the next unresolved decision, dependency, constraint, or risk.
19
19
  6. Ask at most 3 focused questions per round.
@@ -32,4 +32,4 @@ Final output when clear:
32
32
  - implementation approach
33
33
  - required context
34
34
  - verify commands
35
- - recommended next command: `/plan` or `/task`
35
+ - recommended next command: `/workflow plan` or `/workflow task`
@@ -14,7 +14,7 @@ Rules:
14
14
  1. Call `piagent_context`.
15
15
  2. Call `piagent_orchestration_policy`; default to solo-first and choose review lenses/model roles before considering subagents.
16
16
  3. Call `piagent_memory_status`; search memory or Field Guide if it can reduce re-scouting, but treat it as advisory.
17
- 4. Read `.pi/project-context.md`; if it is missing or still pending, recommend `/onboard-project` before implementation.
17
+ 4. Read `.pi/project-context.md`; if it is missing or still pending, recommend `/onboard-project run` before implementation.
18
18
  5. Read required context.
19
19
  6. Identify protected paths and external/high-risk actions.
20
20
  7. Decide whether subagents are useful enough to justify extra token/tool cost. If the bundled `pi-subagents` parent skill is available, use it for delegation patterns and safety boundaries. If `pi-subagents`/`subagent(...)` is available, use `piagent-scout` for independent read-only repo/spec mapping before medium/large plans. Use builtin `context-builder` when the task needs a handoff context/meta-prompt. Use `piagent-oracle` for architecture/risk challenge when the decision is non-obvious. Continue single-agent for tiny plans or unavailable subagent tooling.
@@ -23,7 +23,7 @@ Mandatory flow:
23
23
  1. Call `piagent_context` with `detail=full` and inspect the active project profile.
24
24
  2. Call `piagent_orchestration_policy`; keep the platform change solo-first unless bounded scout/planning/review lanes are useful.
25
25
  3. Call `piagent_memory_status`; if prior decisions or Field Guide notes are relevant, search memory and record citations with `piagent_memory_citation_record`.
26
- 4. Read `.pi/project-context.md`. If pending, stop and ask the user to run `/onboard-project`.
26
+ 4. Read `.pi/project-context.md`. If pending, stop and ask the user to run `/onboard-project run`.
27
27
  5. Read the required context declared by the active profile. Before loading large files, call `piagent_context_budget`.
28
28
  6. Classify the task risk lane and create a task contract with `piagent_task_start`, including review lenses and a compact workPlan, before source edits.
29
29
  7. Decide whether bounded subagents are useful enough to justify extra token/tool cost:
@@ -16,7 +16,7 @@ Rules:
16
16
  1. Do not merge. Do not publish, release, deploy, or tag.
17
17
  2. Use normal shell Git/GitHub commands so the piagent guard and confirmation gates remain active.
18
18
  3. Start with `git status --short`, `git branch --show-current`, and `git remote -v`.
19
- 4. If there are uncommitted changes, stop and ask the operator to run `/commit` first, unless the operator explicitly asks this command to include a commit.
19
+ 4. If there are uncommitted changes, stop and ask the operator to run `/workflow commit` first, unless the operator explicitly asks this command to include a commit.
20
20
  5. Run the relevant verification command before opening the PR. At minimum use `git diff --check`; for source or policy changes, run the project verify/test/typecheck commands that apply.
21
21
  6. If the branch is not pushed, ask for explicit operator confirmation before `git push -u origin <branch>`.
22
22
  7. Create the PR only after the operator explicitly confirms the external GitHub action. Prefer `gh pr create --draft` with a clear title/body derived from the current diff and commits, unless the operator explicitly asks for a ready-for-review PR.
@@ -11,7 +11,7 @@ $ARGUMENTS
11
11
 
12
12
  Rules:
13
13
 
14
- 0. Call `piagent_context_preflight` with `workflow=review` for large diffs or long requests. If it recommends `fresh-session`, prefer `/fresh-scout` for read-only evidence gathering or ask the user to resume in a fresh session.
14
+ 0. Call `piagent_context_preflight` with `workflow=review` for large diffs or long requests. If it recommends `fresh-session`, prefer `/piagent-session fresh scout` for read-only evidence gathering or ask the user to resume in a fresh session.
15
15
  1. Call `piagent_context`.
16
16
  2. Call `piagent_orchestration_policy` and select explicit review lenses before reading the diff deeply.
17
17
  3. Read `.pi/project-context.md` if available.
@@ -14,11 +14,11 @@ Use this when the user needs evidence before deciding whether to implement. Do n
14
14
  Preflight first:
15
15
 
16
16
  1. Call `piagent_context_preflight` with `workflow=scout`.
17
- 2. If it recommends `fresh-session`, stop loading context in this session and tell the user to use `/fresh-scout <request>` unless this command already runs in a fresh session.
17
+ 2. If it recommends `fresh-session`, stop loading context in this session and tell the user to use `/piagent-session fresh scout <request>` unless this command already runs in a fresh session.
18
18
  3. Call `piagent_context` with `detail=full`.
19
19
  4. Call `piagent_orchestration_policy`; keep scout solo-first unless independent read-only lanes are useful.
20
20
  5. Call `piagent_memory_status`; cite relevant memory or Field Guide only with `piagent_memory_citation_record`, then verify against current repo files.
21
- 6. Read `.pi/project-context.md`; if pending, stop and ask for `/onboard-project`.
21
+ 6. Read `.pi/project-context.md`; if pending, stop and ask for `/onboard-project run`.
22
22
  7. Create a read-only task contract with `piagent_task_start` before broad scouting:
23
23
  - risk lane;
24
24
  - expected output;
@@ -11,11 +11,11 @@ $ARGUMENTS
11
11
 
12
12
  Mandatory flow:
13
13
 
14
- 0. Call `piagent_context_preflight` with `workflow=task`. If it recommends `fresh-session`, stop loading context in this session and tell the user to use `/fresh-task <request>` unless this command already runs in a fresh session.
14
+ 0. Call `piagent_context_preflight` with `workflow=task`. If it recommends `fresh-session`, stop loading context in this session and tell the user to use `/piagent-session fresh task <request>` unless this command already runs in a fresh session.
15
15
  1. Call `piagent_context` and read the project profile/runtime policy.
16
16
  2. Call `piagent_orchestration_policy` and keep the task solo-first unless the policy and task shape make bounded subagents clearly useful.
17
17
  3. Call `piagent_memory_status`. If memory or the Field Guide is enabled and relevant to the task, search/read it as advisory context, record citations with `piagent_memory_citation_record`, then verify against current repo files.
18
- 4. Call `piagent_context_index_status` when available, then read `.pi/project-context.md`. If `.pi/project-context.md` is missing or still says `Generated: not yet`, stop and ask the user to run `/onboard-project` after login/model selection before implementation.
18
+ 4. Call `piagent_context_index_status` when available, then read `.pi/project-context.md`. If `.pi/project-context.md` is missing or still says `Generated: not yet`, stop and ask the user to run `/onboard-project run` after login/model selection before implementation.
19
19
  - Use `piagent_context_index_search` for navigation hints when the task touches an unfamiliar module/tech/risk area.
20
20
  - Treat context-index hits as advisory; open and verify cited files before editing.
21
21
  5. Build a Task Implementation Contract with `piagent_task_start` before editing:
@@ -315,10 +315,11 @@ echo "Next:"
315
315
  echo " cd \"$PROJECT_PATH\""
316
316
  echo " pi"
317
317
  echo " /login # first time only"
318
- echo " /onboard-project # first project-read after model selection"
318
+ echo " /onboard # open onboarding menu/status"
319
+ echo " /onboard run # first project-read after model selection"
319
320
  echo " /mcp # MCP servers come from the global install, not from this project"
320
- echo " /context-index # inspect compact project context index"
321
- echo " /memory-policy # inspect project memory policy when needed"
321
+ echo " /context index # inspect compact project context index"
322
+ echo " /memory # inspect project memory policy when needed"
322
323
  echo
323
324
  echo "Note: .mcp.json and .pi/mcp.json are written empty on purpose. They exist so this"
324
325
  echo "project can add its own servers later; the shared baseline lives in the global"
@@ -178,6 +178,50 @@ run_cmd() {
178
178
  fi
179
179
  }
180
180
 
181
+ resolve_user_package_path() {
182
+ local source="$1"
183
+ local pi_agent_dir="${PI_CODING_AGENT_DIR:-"${HOME}/.pi/agent"}"
184
+ node --input-type=module - "$pi_agent_dir" "$source" <<'NODE'
185
+ import path from "node:path";
186
+
187
+ const base = process.argv[2];
188
+ const source = process.argv[3];
189
+ console.log(path.resolve(base, source));
190
+ NODE
191
+ }
192
+
193
+ is_piagent_platform_source() {
194
+ local source="$1"
195
+ case "$source" in
196
+ git:github.com/Vt-mmm/piagent|git:github.com/Vt-mmm/piagent@*|https://github.com/Vt-mmm/piagent*|npm:@piagent/platform|npm:@piagent/platform@*)
197
+ return 0
198
+ ;;
199
+ npm:*|git:*|http:*|https:*)
200
+ return 1
201
+ ;;
202
+ esac
203
+
204
+ local resolved_source
205
+ resolved_source="$(resolve_user_package_path "$source" 2>/dev/null || true)"
206
+ [[ "$resolved_source" == "$PLATFORM_ROOT" ]]
207
+ }
208
+
209
+ remove_existing_piagent_platform_sources() {
210
+ local source
211
+ while IFS= read -r source; do
212
+ [[ -n "$source" ]] || continue
213
+ if is_piagent_platform_source "$source"; then
214
+ run_cmd pi remove "$source"
215
+ fi
216
+ done < <(
217
+ pi list --approve 2>/dev/null | awk '
218
+ $0 == "User packages:" { in_user = 1; next }
219
+ $0 == "Project packages:" { in_user = 0; next }
220
+ in_user && $0 ~ /^ [^ ]/ { sub(/^ /, ""); print }
221
+ '
222
+ )
223
+ }
224
+
181
225
  verify_npm_integrity() {
182
226
  local source="$1"
183
227
  local expected="$2"
@@ -496,6 +540,8 @@ if [[ -n "$RESOLVED_PACKAGE_COMMIT" ]]; then
496
540
  echo " resolvedCommit: $RESOLVED_PACKAGE_COMMIT"
497
541
  fi
498
542
  echo " source: $PACKAGE_SOURCE"
543
+ echo "Removing previous Pi Agent Platform package registrations:"
544
+ remove_existing_piagent_platform_sources
499
545
  run_cmd pi install "$PACKAGE_SOURCE"
500
546
 
501
547
  if [[ "$WITH_MCP" == true ]]; then
@@ -562,4 +608,4 @@ if [[ "$WITH_MCP" == true ]]; then
562
608
  echo " /mcp # inspect MCP servers; authenticate Figma/GitHub only when needed"
563
609
  fi
564
610
  echo " /subagents-doctor"
565
- echo " /task Implement <task> # parent may auto-delegate scout/planner/reviewer"
611
+ echo " /workflow task Implement <task> # parent may auto-delegate scout/planner/reviewer"
@@ -198,7 +198,7 @@ if (contextIndex.enabled && contextIndex.writePolicy !== "off") {
198
198
  if (relativeIndexPath.startsWith("..") || path.isAbsolute(relativeIndexPath)) {
199
199
  errors.push(`contextIndex.path escapes project root: ${contextIndex.path}`);
200
200
  } else if (!fs.existsSync(indexPath)) {
201
- warnings.push(`context index missing: ${contextIndex.path}; run /onboard-project or record piagent_context_index_record`);
201
+ warnings.push(`context index missing: ${contextIndex.path}; run /onboard run or record piagent_context_index_record`);
202
202
  }
203
203
  }
204
204
 
package/scripts/setup.sh CHANGED
@@ -452,12 +452,13 @@ if [[ "$DO_PROJECT" == true ]]; then
452
452
  echo " /scoped-models # optional: edit Ctrl+P model cycle scope"
453
453
  echo " /mcp # inspect MCP servers; run /mcp setup for guided changes"
454
454
  echo " /subagents-doctor # inspect subagent setup"
455
- echo " /onboard-project # first project-read snapshot before implementation"
456
- echo " /memory-policy # inspect project memory policy when needed"
455
+ echo " /onboard # open onboarding menu/status"
456
+ echo " /onboard run # first project-read snapshot before implementation"
457
+ echo " /memory # inspect project memory policy when needed"
457
458
  fi
458
459
  echo
459
460
  echo "Daily flow after setup:"
460
461
  echo " herdr # optional"
461
462
  echo " cd <project>"
462
463
  echo " pi"
463
- echo " /task Implement <task> # parent may auto-delegate scout/planner/reviewer"
464
+ echo " /workflow task Implement <task> # parent may auto-delegate scout/planner/reviewer"
@@ -149,8 +149,6 @@ if (!runtime.teamRolloutReady) {
149
149
  for (const rel of [
150
150
  "package.json",
151
151
  "packages/piagent-core/extensions/piagent-guard.ts",
152
- "packages/piagent-core/prompts/onboard-project.md",
153
- "packages/piagent-core/prompts/memory-policy.md",
154
152
  "packages/piagent-core/prompts/platform-improve.md",
155
153
  "packages/piagent-core/prompts/be-to-fe.md",
156
154
  "packages/piagent-core/prompts/task.md",
@@ -374,10 +372,10 @@ const projectContextPath = path.join(projectPath, ".pi", "project-context.md");
374
372
  if (fs.existsSync(projectContextPath)) {
375
373
  const projectContext = fs.readFileSync(projectContextPath, "utf8");
376
374
  if (/Generated:\s*not yet/i.test(projectContext)) {
377
- warnings.push("project onboarding snapshot is still pending; run /onboard-project in Pi after login/model selection");
375
+ warnings.push("project onboarding snapshot is still pending; run /onboard run in Pi after login/model selection");
378
376
  }
379
377
  } else {
380
- warnings.push("project has no .pi/project-context.md; run setup/init or /onboard-project");
378
+ warnings.push("project has no .pi/project-context.md; run setup/init or /onboard run");
381
379
  }
382
380
 
383
381
  const contextIndexConfig = projectProfile && typeof projectProfile.contextIndex === "object" && !Array.isArray(projectProfile.contextIndex)
@@ -402,8 +400,8 @@ if (contextIndexEscapes) {
402
400
  const edges = Array.isArray(contextIndex.edges) ? contextIndex.edges : [];
403
401
  const citations = Array.isArray(contextIndex.citations) ? contextIndex.citations : [];
404
402
  if (contextIndex.schemaVersion !== 1) warnings.push("project context index schemaVersion should be 1");
405
- if (contextIndex.summary === "Pending. Run /onboard-project after login/model selection to generate the compact project context index.") {
406
- warnings.push("project context index is still pending; run /onboard-project after login/model selection");
403
+ if (/Pending\.\s+Run \/onboard(?: run)? after login\/model selection/i.test(String(contextIndex.summary ?? ""))) {
404
+ warnings.push("project context index is still pending; run /onboard run after login/model selection");
407
405
  }
408
406
  if (nodes.length === 0) warnings.push("project context index has no nodes yet");
409
407
  if (contextIndex?.policy?.requireCitations !== false && citations.length === 0) warnings.push("project context index has no citations yet");
@@ -411,13 +409,13 @@ if (contextIndexEscapes) {
411
409
  if (edges.length > (contextIndex?.policy?.maxEdges ?? 240)) warnings.push("project context index has more edges than policy maxEdges");
412
410
  }
413
411
  } else {
414
- warnings.push(`project has no ${contextIndexRelativePath}; run setup/init or /onboard-project`);
412
+ warnings.push(`project has no ${contextIndexRelativePath}; run setup/init or /onboard run`);
415
413
  }
416
414
 
417
415
  const memorySummaryPath = path.join(projectPath, ".pi", "memory", "memory_summary.md");
418
416
  const memoryHandbookPath = path.join(projectPath, ".pi", "memory", "MEMORY.md");
419
417
  if (!fs.existsSync(memorySummaryPath) || !fs.existsSync(memoryHandbookPath)) {
420
- warnings.push("project has no .pi/memory scaffold; run setup/init or use /memory-policy before relying on project memory");
418
+ warnings.push("project has no .pi/memory scaffold; run setup/init or use /memory before relying on project memory");
421
419
  }
422
420
 
423
421
  if (strictShare) {
@@ -17,6 +17,7 @@
17
17
  // it asked for.
18
18
 
19
19
  import fs from "node:fs";
20
+ import os from "node:os";
20
21
  import path from "node:path";
21
22
  import { spawnSync } from "node:child_process";
22
23
  import { fileURLToPath } from "node:url";
@@ -39,13 +40,18 @@ Options:
39
40
  --dry-run Print every command that would run, change nothing
40
41
  --version <x.y.z> Update to an exact helper version instead of latest
41
42
  --force Reinstall even when already on the target version
42
- --project <path> Run the team doctor against this project afterwards
43
+ --project <path> Optional: after the global update, migrate and doctor this project
43
44
  --no-host Do not touch the Pi host
44
45
  --no-package Do not reinstall the Pi package
46
+ --no-migrate With --project, skip project state migration
47
+ --npm-prefix <path> Use this npm global prefix for host/helper installs
45
48
  -h, --help Show this help
46
49
 
47
50
  Anything after -- is passed through to piagent-install, for example:
48
51
  piagent-update -- --no-mcp
52
+
53
+ Bootstrap a machine that does not have piagent-update yet:
54
+ npm exec -y --package @piagent/platform@X.Y.Z -- piagent-update --version X.Y.Z --force
49
55
  `;
50
56
  }
51
57
 
@@ -81,6 +87,8 @@ function parseArguments(argv) {
81
87
  project: undefined,
82
88
  host: true,
83
89
  package: true,
90
+ migrate: true,
91
+ npmPrefix: undefined,
84
92
  passthrough: []
85
93
  };
86
94
 
@@ -111,6 +119,15 @@ function parseArguments(argv) {
111
119
  case "--no-package":
112
120
  options.package = false;
113
121
  break;
122
+ case "--no-migrate":
123
+ options.migrate = false;
124
+ break;
125
+ case "--npm-prefix": {
126
+ const value = argv[index += 1];
127
+ if (!value) fail("--npm-prefix needs a path", EXIT_USAGE);
128
+ options.npmPrefix = value;
129
+ break;
130
+ }
114
131
  case "--version": {
115
132
  const value = normalizeVersion(argv[index += 1]);
116
133
  if (!isReleaseVersion(value)) fail(`--version needs an exact release such as 1.1.4, got ${argv[index] ?? "nothing"}`, EXIT_USAGE);
@@ -140,6 +157,167 @@ function run(command, args, { capture = false } = {}) {
140
157
  return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
141
158
  }
142
159
 
160
+ function expandHome(value) {
161
+ if (value === "~") return os.homedir();
162
+ if (value?.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
163
+ return value;
164
+ }
165
+
166
+ function configuredNpmPrefix() {
167
+ const result = run("npm", ["config", "get", "prefix"], { capture: true });
168
+ if (!result.ok) return undefined;
169
+ const prefix = result.stdout.trim();
170
+ return prefix && prefix !== "undefined" && prefix !== "null" ? path.resolve(expandHome(prefix)) : undefined;
171
+ }
172
+
173
+ function fallbackNpmRoot(prefix) {
174
+ return process.platform === "win32"
175
+ ? path.join(prefix, "node_modules")
176
+ : path.join(prefix, "lib", "node_modules");
177
+ }
178
+
179
+ function npmRoot(prefix) {
180
+ const args = ["root", "-g"];
181
+ if (prefix) args.push("--prefix", prefix);
182
+ const result = run("npm", args, { capture: true });
183
+ if (result.ok && result.stdout.trim()) return path.resolve(expandHome(result.stdout.trim()));
184
+ return prefix ? fallbackNpmRoot(prefix) : undefined;
185
+ }
186
+
187
+ function nearestExistingPath(target) {
188
+ let current = target;
189
+ while (current && !fs.existsSync(current)) {
190
+ const parent = path.dirname(current);
191
+ if (parent === current) return undefined;
192
+ current = parent;
193
+ }
194
+ return current || undefined;
195
+ }
196
+
197
+ function canWriteOrCreate(target) {
198
+ const nearest = nearestExistingPath(target);
199
+ if (!nearest) return false;
200
+ try {
201
+ fs.accessSync(nearest, fs.constants.W_OK);
202
+ return true;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+
208
+ function userNpmPrefix() {
209
+ return path.join(os.homedir(), ".pi", "npm-global");
210
+ }
211
+
212
+ function binForPrefix(prefix) {
213
+ return process.platform === "win32" ? prefix : path.join(prefix, "bin");
214
+ }
215
+
216
+ function prependPath(directory) {
217
+ process.env.PATH = `${directory}${path.delimiter}${process.env.PATH ?? ""}`;
218
+ }
219
+
220
+ function resolveNpmInstallTarget(options, needsGlobalInstall) {
221
+ const explicitPrefix = options.npmPrefix ? path.resolve(expandHome(options.npmPrefix)) : undefined;
222
+ if (explicitPrefix) {
223
+ if (!options.dryRun) fs.mkdirSync(explicitPrefix, { recursive: true });
224
+ return {
225
+ prefix: explicitPrefix,
226
+ root: npmRoot(explicitPrefix),
227
+ bin: binForPrefix(explicitPrefix),
228
+ reason: "explicit"
229
+ };
230
+ }
231
+
232
+ const defaultPrefix = configuredNpmPrefix();
233
+ const defaultRoot = npmRoot(defaultPrefix);
234
+ if (!needsGlobalInstall || (defaultRoot && canWriteOrCreate(defaultRoot))) {
235
+ return { prefix: undefined, root: defaultRoot, bin: undefined, reason: "default" };
236
+ }
237
+
238
+ const prefix = userNpmPrefix();
239
+ if (!options.dryRun) fs.mkdirSync(prefix, { recursive: true });
240
+ return {
241
+ prefix,
242
+ root: npmRoot(prefix),
243
+ bin: binForPrefix(prefix),
244
+ reason: "user-prefix",
245
+ blockedRoot: defaultRoot
246
+ };
247
+ }
248
+
249
+ function npmInstallArgs(specifier, npmTarget) {
250
+ const args = ["install", "-g", "--ignore-scripts"];
251
+ if (npmTarget.prefix) args.push("--prefix", npmTarget.prefix);
252
+ args.push(specifier);
253
+ return args;
254
+ }
255
+
256
+ function helperRootFromNpmTarget(npmTarget) {
257
+ return npmTarget.root ? path.join(npmTarget.root, "@piagent", "platform") : undefined;
258
+ }
259
+
260
+ function helperVersionAt(root) {
261
+ return root ? readJson(path.join(root, "package.json"))?.version : undefined;
262
+ }
263
+
264
+ function resolveInstalledHelperRoot(targetHelper, npmTarget) {
265
+ const npmHelperRoot = helperRootFromNpmTarget(npmTarget);
266
+ if (helperVersionAt(npmHelperRoot) === targetHelper) return npmHelperRoot;
267
+ if (helperVersionAt(platformRoot) === targetHelper) return platformRoot;
268
+ const checked = [];
269
+ for (const root of [npmHelperRoot, platformRoot]) {
270
+ if (!root || checked.some((entry) => entry.root === root)) continue;
271
+ checked.push({ root, version: helperVersionAt(root) ?? "nothing" });
272
+ }
273
+ const details = checked.map((entry) => `${entry.root} reports ${entry.version}`).join("; ");
274
+ fail(`installed ${HELPER_PACKAGE}@${targetHelper}, but ${details || "the helper paths did not report that version"}. `
275
+ + "The Pi package was left untouched; rerun once the helper is correct.");
276
+ }
277
+
278
+ function parseJsonOutput(result, label) {
279
+ try {
280
+ return JSON.parse(result.stdout.trim() || "{}");
281
+ } catch {
282
+ fail(`${label} did not return valid JSON:\n${result.stdout.trim()}`);
283
+ }
284
+ }
285
+
286
+ function summarizeMigration(report, dryRun) {
287
+ const renamed = report.renamed?.length ?? 0;
288
+ const removed = report.removedOld?.length ?? 0;
289
+ const rewrote = report.rewrote?.length ?? report.wouldRewrite?.length ?? 0;
290
+ const wouldRename = report.wouldRename?.length ?? 0;
291
+ const alreadyMigrated = report.alreadyMigrated?.length ?? 0;
292
+
293
+ if (dryRun) {
294
+ const planned = wouldRename + rewrote + alreadyMigrated;
295
+ return planned > 0
296
+ ? `project migration: would update legacy layout (${wouldRename} rename, ${rewrote} rewrite, ${alreadyMigrated} already copied)`
297
+ : "project migration: already current";
298
+ }
299
+ if (report.migrated === false) return "project migration: already current";
300
+ return `project migration: applied (${renamed} rename, ${rewrote} rewrite, ${removed} old path removed)`;
301
+ }
302
+
303
+ function runProjectMigration(project, options, activePlatformRoot) {
304
+ const migrator = path.join(activePlatformRoot, "scripts/migrate-project-state.mjs");
305
+ if (!options.dryRun && !fs.existsSync(migrator)) {
306
+ fail(`the updated helper has no project migrator at ${migrator}`);
307
+ }
308
+ const args = options.dryRun
309
+ ? [migrator, project]
310
+ : [migrator, project, "--apply", "--remove-old"];
311
+ describeStep(options.dryRun, process.execPath, args);
312
+ const result = run(process.execPath, args, { capture: true });
313
+ if (!result.ok) fail(`project migration failed:\n${result.stderr.trim() || result.stdout.trim()}`);
314
+ const report = parseJsonOutput(result, "project migration");
315
+ if ((report.skipped?.length ?? 0) > 0) {
316
+ fail(`project migration skipped protected entries: ${report.skipped.map((entry) => `${entry.path} (${entry.reason})`).join(", ")}`);
317
+ }
318
+ console.log(` ${summarizeMigration(report, options.dryRun)}`);
319
+ }
320
+
143
321
  function npmView(specifier, field) {
144
322
  const result = run("npm", ["view", specifier, field, "--json"], { capture: true });
145
323
  if (!result.ok) return undefined;
@@ -196,6 +374,9 @@ function main() {
196
374
  console.log(` helper: ${currentHelper} -> ${targetHelper}${helperNeedsChange ? "" : " (already current)"}`);
197
375
  console.log(` host: ${currentHost ?? "not installed"} -> ${targetHost}${hostNeedsChange ? "" : " (already current)"}`);
198
376
  console.log(` package: reinstalled from v${targetHelper} as a resolved commit${options.package ? "" : " (skipped)"}`);
377
+ if (options.project) {
378
+ console.log(` project: ${path.resolve(options.project)} (${options.migrate ? "migrate + doctor" : "doctor only"})`);
379
+ }
199
380
 
200
381
  if (options.check) {
201
382
  const pending = [helperNeedsChange && "helper", hostNeedsChange && "host"].filter(Boolean);
@@ -203,7 +384,7 @@ function main() {
203
384
  return;
204
385
  }
205
386
 
206
- if (!helperNeedsChange && !hostNeedsChange && !options.package) {
387
+ if (!helperNeedsChange && !hostNeedsChange && !options.package && !options.project) {
207
388
  console.log("\nNothing to do.");
208
389
  return;
209
390
  }
@@ -215,33 +396,38 @@ function main() {
215
396
  + ` bash ${path.join(platformRoot, "scripts/install-global.sh")} --local`);
216
397
  }
217
398
 
399
+ let activePlatformRoot = platformRoot;
400
+ const npmTarget = resolveNpmInstallTarget(options, hostNeedsChange || helperNeedsChange);
401
+ if (npmTarget.bin) prependPath(npmTarget.bin);
402
+ if (npmTarget.reason === "user-prefix") {
403
+ console.log(` npm: default global root is not writable (${npmTarget.blockedRoot ?? "unknown"}); using ${npmTarget.prefix}`);
404
+ } else if (npmTarget.reason === "explicit") {
405
+ console.log(` npm: using explicit global prefix ${npmTarget.prefix}`);
406
+ }
407
+
218
408
  // Host first: piagent-install refuses to run against a host that does not match
219
409
  // the version its package.json pins, so a helper installed ahead of the host
220
410
  // would fail on its very next step.
221
411
  if (hostNeedsChange) {
222
- const args = ["install", "-g", "--ignore-scripts", `${HOST_PACKAGE}@${targetHost}`];
412
+ const args = npmInstallArgs(`${HOST_PACKAGE}@${targetHost}`, npmTarget);
223
413
  describeStep(options.dryRun, "npm", args);
224
414
  if (!options.dryRun && !run("npm", args).ok) fail(`could not install ${HOST_PACKAGE}@${targetHost}`);
225
415
  }
226
416
 
227
417
  if (helperNeedsChange) {
228
- const args = ["install", "-g", "--ignore-scripts", `${HELPER_PACKAGE}@${targetHelper}`];
418
+ const args = npmInstallArgs(`${HELPER_PACKAGE}@${targetHelper}`, npmTarget);
229
419
  describeStep(options.dryRun, "npm", args);
230
420
  if (!options.dryRun) {
231
421
  if (!run("npm", args).ok) fail(`could not install ${HELPER_PACKAGE}@${targetHelper}`);
232
422
  // The helper has just replaced itself on disk. Everything after this point
233
423
  // runs the newly installed scripts, so confirm they are the ones asked for
234
424
  // rather than trusting that npm did what it said.
235
- const landed = readJson(path.join(platformRoot, "package.json"))?.version;
236
- if (landed !== targetHelper) {
237
- fail(`installed ${HELPER_PACKAGE}@${targetHelper} but ${platformRoot} reports ${landed ?? "nothing"}. `
238
- + "The Pi package was left untouched; rerun once the helper is correct.");
239
- }
425
+ activePlatformRoot = resolveInstalledHelperRoot(targetHelper, npmTarget);
240
426
  }
241
427
  }
242
428
 
243
429
  if (options.package) {
244
- const installer = path.join(platformRoot, "scripts/install-global.sh");
430
+ const installer = path.join(activePlatformRoot, "scripts/install-global.sh");
245
431
  if (!options.dryRun && !fs.existsSync(installer)) {
246
432
  fail(`the updated helper has no installer at ${installer}`);
247
433
  }
@@ -251,12 +437,22 @@ function main() {
251
437
  }
252
438
 
253
439
  if (options.project) {
254
- const doctor = path.join(platformRoot, "scripts/team-doctor.sh");
440
+ if (options.migrate) {
441
+ runProjectMigration(options.project, options, activePlatformRoot);
442
+ } else {
443
+ console.log(" project migration: skipped");
444
+ }
445
+ const doctor = path.join(activePlatformRoot, "scripts/team-doctor.sh");
255
446
  const args = [doctor, options.project, "--strict-share"];
256
447
  describeStep(options.dryRun, "bash", args);
257
448
  if (!options.dryRun && !run("bash", args).ok) fail("the team doctor reported a problem");
258
449
  }
259
450
 
451
+ if (npmTarget.reason === "user-prefix") {
452
+ console.log(`\nPATH note: add this once if your shell cannot find piagent-* commands later:`);
453
+ console.log(` export PATH="${npmTarget.bin}:$PATH"`);
454
+ }
455
+
260
456
  console.log(options.dryRun
261
457
  ? `\nDRY RUN: nothing was installed. Rerun without --dry-run to apply v${targetHelper}.`
262
458
  : `\nPASS: updated to v${targetHelper}.`);