@piagent/platform 1.1.5 → 1.1.7

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.
@@ -6,19 +6,37 @@
6
6
  "required": [
7
7
  "schemaVersion",
8
8
  "projectId",
9
- "displayName",
10
- "mode",
11
- "rootMarkers",
12
- "protectedPaths",
13
- "requiredContext",
14
- "verifyCommands",
15
- "mcpCapabilities"
9
+ "displayName"
16
10
  ],
11
+ "if": {
12
+ "required": [
13
+ "extends"
14
+ ]
15
+ },
16
+ "then": {
17
+ "description": "A profile that names an adapter stores identity plus overrides. Every policy field it omits comes from the installed adapter at runtime, so none of them are required here."
18
+ },
19
+ "else": {
20
+ "required": [
21
+ "mode",
22
+ "rootMarkers",
23
+ "protectedPaths",
24
+ "requiredContext",
25
+ "verifyCommands",
26
+ "mcpCapabilities"
27
+ ]
28
+ },
17
29
  "properties": {
18
30
  "schemaVersion": {
19
31
  "type": "integer",
20
32
  "const": 1
21
33
  },
34
+ "extends": {
35
+ "description": "Name of a built-in adapter under adapters/ in the installed platform. Its profile is the base; every other key in this file replaces the adapter's key of the same name.",
36
+ "type": "string",
37
+ "minLength": 1,
38
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
39
+ },
22
40
  "projectId": {
23
41
  "type": "string",
24
42
  "minLength": 1,
@@ -20,6 +20,10 @@ import {
20
20
  validateCapabilitySources,
21
21
  vendorRemoteSource
22
22
  } from "../packages/piagent-core/capabilities/capability-sources.js";
23
+ import {
24
+ buildExtendingProfile,
25
+ resolveProjectProfileDocument
26
+ } from "../packages/piagent-core/capabilities/project-profile.js";
23
27
 
24
28
  const platformRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
25
29
  const catalogPath = path.join(platformRoot, "catalog", "capabilities.json");
@@ -28,7 +32,7 @@ function usage() {
28
32
  process.stdout.write(`Usage:
29
33
  piagent-capabilities catalog [--check | --write]
30
34
  piagent-capabilities resolve --profile <profile.json> [--output <lock.json>] [--package-source <source>]
31
- piagent-capabilities apply-profile --profile <source.json> --target <piagent-profile.json> [--package-source <source>] [--force]
35
+ piagent-capabilities apply-profile --profile <source.json> --target <piagent-profile.json> [--extends <adapter>] [--package-source <source>] [--force]
32
36
  piagent-capabilities doctor [--profile <profile.json>] [--lock <lock.json>] [--package-source <source>]
33
37
  piagent-capabilities vendor --profile <profile.json> [--source <name>]
34
38
  piagent-capabilities validate-source --package-source <source>
@@ -58,7 +62,7 @@ function parseArguments(values) {
58
62
  output.flags.set(value, true);
59
63
  continue;
60
64
  }
61
- if (!["--profile", "--output", "--target", "--lock", "--file", "--package-source", "--source"].includes(value)) throw new CapabilityValidationError(`unknown option ${value}`);
65
+ if (!["--profile", "--extends", "--output", "--target", "--lock", "--file", "--package-source", "--source"].includes(value)) throw new CapabilityValidationError(`unknown option ${value}`);
62
66
  if (output.flags.has(value)) throw new CapabilityValidationError(`duplicate option ${value}`);
63
67
  const next = values[index + 1];
64
68
  if (!next || next.startsWith("--")) throw new CapabilityValidationError(`${value} requires a value`);
@@ -157,7 +161,7 @@ function runResolve(flags) {
157
161
  }
158
162
 
159
163
  function runApplyProfile(flags) {
160
- assertAllowedFlags(flags, new Set(["--profile", "--target", "--package-source", "--force"]));
164
+ assertAllowedFlags(flags, new Set(["--profile", "--extends", "--target", "--package-source", "--force"]));
161
165
  const source = requireExistingFile(flags.get("--profile"), "--profile");
162
166
  const targetValue = flags.get("--target");
163
167
  if (!targetValue) throw new CapabilityValidationError("--target is required");
@@ -166,17 +170,27 @@ function runApplyProfile(flags) {
166
170
  const parent = fs.realpathSync(path.dirname(target));
167
171
  const normalizedTarget = path.join(parent, path.basename(target));
168
172
  if (fs.existsSync(normalizedTarget) && !flags.has("--force")) throw new CapabilityValidationError("target profile already exists; pass --force to replace it");
169
- const profile = readJson(source);
173
+ const adapter = readJson(source);
174
+ const base = flags.get("--extends");
175
+ // With --extends the project records which adapter it follows and keeps only
176
+ // its own identity, so a later platform correction reaches it. Without it the
177
+ // source document is copied whole, which is what a caller supplying its own
178
+ // profile file wants.
179
+ const stored = base === undefined
180
+ ? adapter
181
+ : buildExtendingProfile(base, adapter.projectId, adapter.displayName);
182
+ const profile = resolveProjectProfileDocument(platformRoot, stored).profile;
170
183
  const packageSource = flags.get("--package-source") ?? "workspace";
171
184
  const lock = resolveCapabilityProfileDocument(platformRoot, profile, {
172
185
  profileFile: path.basename(normalizedTarget),
186
+ storedProfile: stored,
173
187
  packageSource,
174
188
  extraRoots: profile.capabilitySources === undefined
175
189
  ? undefined
176
190
  : resolveCapabilitySourceRoots(projectRootForProfile(normalizedTarget), profile.capabilitySources)
177
191
  });
178
192
  const lockTarget = path.join(parent, "piagent-profile.lock.json");
179
- writeProfileLockAtomic(normalizedTarget, profile, lockTarget, lock);
193
+ writeProfileLockAtomic(normalizedTarget, stored, lockTarget, lock);
180
194
  process.stdout.write(`${JSON.stringify({ ok: true, profile: normalizedTarget, lock: lockTarget, packs: lock.packs.length })}\n`);
181
195
  }
182
196
 
@@ -210,11 +224,18 @@ function runDoctor(flags) {
210
224
  const verification = verifyCapabilityLock(platformRoot, profile, readJson(lockFile), { packageSource, extraRoots });
211
225
  report.lock = {
212
226
  path: lockFile,
213
- current: verification.ok,
227
+ current: verification.status === "current",
228
+ status: verification.status,
229
+ reasons: verification.reasons,
214
230
  expectedDigest: verification.expectedDigest,
215
231
  actualDigest: verification.actualDigest
216
232
  };
217
- if (!verification.ok) throw new CapabilityValidationError("capability lock is stale or has been modified");
233
+ // A re-pinnable lock is behind the installed build but grants exactly what
234
+ // it agreed to, so it is reported rather than treated as a failure. The
235
+ // guard rewrites it on the next session.
236
+ if (verification.status === "blocked") {
237
+ throw new CapabilityValidationError(`capability lock no longer matches what the project agreed to: ${verification.reasons.join("; ")}`);
238
+ }
218
239
  }
219
240
  } else if (flags.has("--lock")) {
220
241
  throw new CapabilityValidationError("--lock requires --profile");
@@ -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() {
@@ -296,6 +219,13 @@ if [[ "$FORCE_PROFILE" == true || ! -f "$PROFILE_TARGET" ]]; then
296
219
  --target "$PROFILE_TARGET"
297
220
  --package-source "$PACKAGE_SOURCE"
298
221
  )
222
+ # A built-in adapter is referenced, not copied, so a later platform correction
223
+ # reaches this project. A caller supplying its own profile file still gets that
224
+ # file written whole.
225
+ if [[ "$PROFILE_PATH" == "$PLATFORM_ROOT/adapters/"*"/profile.json" ]]; then
226
+ adapter_name="${PROFILE_PATH#"$PLATFORM_ROOT/adapters/"}"
227
+ apply_profile_args+=(--extends "${adapter_name%/profile.json}")
228
+ fi
299
229
  if [[ "$FORCE_PROFILE" == true ]]; then
300
230
  apply_profile_args+=(--force)
301
231
  fi
@@ -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.7 --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.7 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.7 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
  ;;
@@ -19,12 +19,22 @@ if [[ ! -f "$PROFILE_PATH" ]]; then
19
19
  exit 1
20
20
  fi
21
21
 
22
- node --input-type=module - "$PROJECT_PATH" "$PROFILE_PATH" <<'NODE'
22
+ PLATFORM_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
23
+
24
+ node --input-type=module - "$PROJECT_PATH" "$PROFILE_PATH" "$PLATFORM_ROOT" <<'NODE'
23
25
  import fs from "node:fs";
24
26
  import path from "node:path";
27
+ import { pathToFileURL } from "node:url";
28
+
29
+ const [projectPath, profilePath, platformRoot] = process.argv.slice(2);
30
+ const stored = JSON.parse(fs.readFileSync(profilePath, "utf8"));
25
31
 
26
- const [projectPath, profilePath] = process.argv.slice(2);
27
- const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
32
+ // A profile that names an adapter is checked as the document that will actually
33
+ // be enforced, not as the few lines the project happens to store.
34
+ const { resolveProjectProfileDocument } = await import(
35
+ pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "project-profile.js")).href
36
+ );
37
+ const profile = resolveProjectProfileDocument(platformRoot, stored).profile;
28
38
 
29
39
  const errors = [];
30
40
  const warnings = [];
@@ -25,11 +25,20 @@ bash "$ROOT/scripts/profile-doctor.sh" "$PROJECT" >/dev/null
25
25
  bash "$ROOT/scripts/quality-benchmark.sh" "$PROJECT" --init >/dev/null
26
26
  bash "$ROOT/scripts/quality-benchmark.sh" "$PROJECT" --record --scenario smoke --surface pi --result pass --tokens 1 --verify "test -s README.md" >/dev/null
27
27
 
28
- node --input-type=module - "$PROJECT/.pi/piagent-profile.json" "$PROJECT/.pi/benchmarks/quality-runs.jsonl" <<'NODE'
28
+ node --input-type=module - "$PROJECT/.pi/piagent-profile.json" "$PROJECT/.pi/benchmarks/quality-runs.jsonl" "$ROOT" <<'NODE'
29
29
  import fs from "node:fs";
30
+ import path from "node:path";
31
+ import { pathToFileURL } from "node:url";
30
32
 
31
- const [profilePath, benchmarkPath] = process.argv.slice(2);
32
- const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
33
+ const [profilePath, benchmarkPath, platformRoot] = process.argv.slice(2);
34
+ const stored = JSON.parse(fs.readFileSync(profilePath, "utf8"));
35
+
36
+ // A project that names an adapter stores identity and overrides only. The policy
37
+ // the runtime enforces is the resolved document, so that is what gets checked.
38
+ const { resolveProjectProfileDocument } = await import(
39
+ pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "project-profile.js")).href
40
+ );
41
+ const profile = resolveProjectProfileDocument(platformRoot, stored).profile;
33
42
  for (const capability of ["filesystem-readonly", "filesystem-write", "shell", "memory"]) {
34
43
  if (!profile.mcpCapabilities.includes(capability)) {
35
44
  throw new Error(`missing capability ${capability}`);
@@ -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.7 support matrix."
141
141
  };
142
142
  }
143
143
 
@@ -313,7 +313,12 @@ if (foreignAgentConfig.length > 0) {
313
313
  const projectProfilePath = path.join(projectPath, ".pi", "piagent-profile.json");
314
314
  let projectProfile = null;
315
315
  if (fs.existsSync(projectProfilePath)) {
316
- const profile = readJson(projectProfilePath);
316
+ // Check the document that will be enforced. A profile that names an adapter
317
+ // stores only its own identity and overrides.
318
+ const { resolveProjectProfileDocument } = await import(
319
+ pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "project-profile.js")).href
320
+ );
321
+ const profile = resolveProjectProfileDocument(platformRoot, readJson(projectProfilePath)).profile;
317
322
  projectProfile = profile;
318
323
  for (const field of ["schemaVersion", "projectId", "displayName", "mode"]) {
319
324
  if (profile[field] === undefined || profile[field] === "") errors.push(`project profile missing ${field}`);
@@ -333,7 +338,11 @@ if (fs.existsSync(projectProfilePath)) {
333
338
  try {
334
339
  const lock = readJson(projectLockPath);
335
340
  const verification = verifyCapabilityLock(platformRoot, projectProfilePath, lock, { packageSource: projectPackageSource });
336
- if (!verification.ok) errors.push("project capability lock is stale or does not match the configured package source");
341
+ if (verification.status === "blocked") {
342
+ errors.push(`project capability lock no longer matches what the project agreed to: ${verification.reasons.join("; ")}`);
343
+ } else if (verification.status === "repin") {
344
+ warnings.push(`project capability lock is behind the installed platform and will be re-pinned on the next Pi session (${verification.reasons.join("; ")})`);
345
+ }
337
346
  } catch (error) {
338
347
  errors.push(`project capability lock validation failed: ${error instanceof Error ? error.message : String(error)}`);
339
348
  }