@arbiterforge/ca-pi 0.15.8 → 0.15.10

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/README.md CHANGED
@@ -10,7 +10,7 @@ project context. You decide. codeArbiter enforces.
10
10
  <img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
11
11
  <img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
12
12
  <img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
13
- <img alt="version 2.23.1" src="https://img.shields.io/badge/version-2.23.1-2b7489">
13
+ <img alt="version 2.23.2" src="https://img.shields.io/badge/version-2.23.2-2b7489">
14
14
  <img alt="core lanes" src="https://img.shields.io/badge/core_lanes-18-555">
15
15
  <img alt="skills" src="https://img.shields.io/badge/skills-22-555">
16
16
  <img alt="agents" src="https://img.shields.io/badge/agents-20-555">
@@ -183,7 +183,7 @@ Replace `<version>` with the numeric suffix from the chosen tag while retaining
183
183
  it is a partial install that cannot author typed HTML specs and plans; use the npm channel for
184
184
  that.
185
185
 
186
- Pi 0.84.1 is the supported host for this release line. Generated aliases use `/ca-*`;
186
+ Pi 1.0.0 is the supported host for this release line. Generated aliases use `/ca-*`;
187
187
  `/skill:ca-*` is the host-native fallback. Every `ca-pi-v*` tag is also published to npm as
188
188
  `npm:@arbiterforge/ca-pi` with provenance (ADR-0029); the pinned Git tag remains the reproducible
189
189
  install. The [Pi runbook](./docs/pi-parity-testing.md) covers isolated install, trust, verification,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.15.8",
3
+ "version": "0.15.10",
4
4
  "license": "AGPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,29 @@ All notable changes to `ca-pi` are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.15.10] - 2026-10-03
8
+
9
+ ### Changed
10
+
11
+ - Promote the verified Pi host window through exact Pi 1.0.0. Pi 1.0.0 closes the
12
+ undici advisories present in 0.84.1; the brace-expansion advisories it still pins
13
+ were already present in 0.84.1 and are recorded as accepted under ADR-0041.
14
+
15
+ ### Fixed
16
+
17
+ - Child dispatch no longer degrades on Pi 1.0.0: the runner accepts the 1.0.0 JSON
18
+ wire (`message_update` usage, named `toolcall_start`, `deferred` completion).
19
+ - `/ca-doctor` validates the Pi 1.0.0 CLI at its declared `dist/bundle/cli.js` bin.
20
+
21
+ ## [0.15.9] - 2026-10-03
22
+
23
+ ### Fixed
24
+
25
+ - Named verification now supports a root npm script delegating once to a uniquely
26
+ selected workspace runner. The declared root command remains intact, both npm
27
+ argument boundaries are honored, and every inspected manifest is retained in
28
+ launch provenance. Unsupported or ambiguous delegation fails before execution.
29
+
7
30
  ## [0.15.8] - 2026-09-27
8
31
 
9
32
  ### Fixed
@@ -629,7 +629,7 @@ function resolvePythonCommand(platform = process.platform, probe = systemPythonP
629
629
  }
630
630
 
631
631
  // src/compatibility.ts
632
- var SUPPORTED_PI_VERSIONS = /* @__PURE__ */ new Set(["0.84.1"]);
632
+ var SUPPORTED_PI_VERSIONS = /* @__PURE__ */ new Set(["1.0.0"]);
633
633
  var MINIMUM_NODE = [22, 19, 0];
634
634
  var SEMVER_PREFIX = /^(\d+)\.(\d+)\.(\d+)(?:$|[-+])/u;
635
635
  function atLeast(version, minimum) {
@@ -644,7 +644,7 @@ function atLeast(version, minimum) {
644
644
  }
645
645
  function compatibilityDirection(input) {
646
646
  if (!SUPPORTED_PI_VERSIONS.has(input.piVersion)) {
647
- return "codeArbiter requires Pi 0.84.1; install a supported Pi version and run /ca-doctor.";
647
+ return "codeArbiter requires Pi 1.0.0; install a supported Pi version and run /ca-doctor.";
648
648
  }
649
649
  if (!atLeast(input.nodeVersion, MINIMUM_NODE)) {
650
650
  return "codeArbiter requires Node >=22.19.0 for Pi; upgrade Node and run /ca-doctor.";
@@ -2,7 +2,7 @@
2
2
  var define_CODEARBITER_PI_PERMISSION_POLICY_SURFACES_default = { "ca-plan": "planning-write", codearbiter_background_bash: "background-launch" };
3
3
 
4
4
  // <define:__CODEARBITER_PI_SKILL_EXPANSION_FINGERPRINTS__>
5
- var define_CODEARBITER_PI_SKILL_EXPANSION_FINGERPRINTS_default = { "0.84.1": "12632f365440b07d5183cff871d889b796a3c711b6b49df20f95d9bc198d6c51" };
5
+ var define_CODEARBITER_PI_SKILL_EXPANSION_FINGERPRINTS_default = { "1.0.0": "12632f365440b07d5183cff871d889b796a3c711b6b49df20f95d9bc198d6c51" };
6
6
 
7
7
  // <define:__CODEARBITER_PI_TOOL_CLASSES__>
8
8
  var define_CODEARBITER_PI_TOOL_CLASSES_default = { bash: "EXEC", codearbiter_background_bash: "EXEC", codearbiter_dispatch: "EXEC", codearbiter_farm_preview: "EXEC", write: "WRITE", edit: "EDIT", read: "READ" };
@@ -16,7 +16,7 @@ import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL2 } from
16
16
  import { types as utilTypes9 } from "node:util";
17
17
 
18
18
  // src/compatibility.ts
19
- var SUPPORTED_PI_VERSIONS = /* @__PURE__ */ new Set(["0.84.1"]);
19
+ var SUPPORTED_PI_VERSIONS = /* @__PURE__ */ new Set(["1.0.0"]);
20
20
  var MINIMUM_NODE = [22, 19, 0];
21
21
  var SEMVER_PREFIX = /^(\d+)\.(\d+)\.(\d+)(?:$|[-+])/u;
22
22
  function atLeast(version, minimum) {
@@ -31,7 +31,7 @@ function atLeast(version, minimum) {
31
31
  }
32
32
  function compatibilityDirection(input) {
33
33
  if (!SUPPORTED_PI_VERSIONS.has(input.piVersion)) {
34
- return "codeArbiter requires Pi 0.84.1; install a supported Pi version and run /ca-doctor.";
34
+ return "codeArbiter requires Pi 1.0.0; install a supported Pi version and run /ca-doctor.";
35
35
  }
36
36
  if (!atLeast(input.nodeVersion, MINIMUM_NODE)) {
37
37
  return "codeArbiter requires Node >=22.19.0 for Pi; upgrade Node and run /ca-doctor.";
@@ -6881,10 +6881,10 @@ async function collectPiDoctorInput(dependencies) {
6881
6881
  var REMEDIATION = {
6882
6882
  package: "Reinstall ca-pi from the approved pinned Git tag, then restart Pi.",
6883
6883
  trust: "Run /trust in Pi, inspect the project, grant trust only if you accept it, then start a new session.",
6884
- version: "Upgrade Pi to 0.84.1 and Node to >=22.19.0, then restart Pi.",
6884
+ version: "Upgrade Pi to 1.0.0 and Node to >=22.19.0, then restart Pi.",
6885
6885
  python: "Upgrade or install Python 3, then run /ca-doctor again.",
6886
6886
  core: "Reinstall ca-pi to restore the generated shared core, then run /ca-doctor again.",
6887
- commands: "Remove conflicting command owners or run Pi 0.84.1, then restart Pi and run /ca-doctor.",
6887
+ commands: "Remove conflicting command owners or run Pi 1.0.0, then restart Pi and run /ca-doctor.",
6888
6888
  bridge: "Reinstall ca-pi and Python 3, then run /ca-doctor again.",
6889
6889
  child: "Reinstall ca-pi if the hardened child artifact is missing or tampered, then run /ca-doctor again.",
6890
6890
  "ambient-marker": "Remove CODEARBITER_SUBAGENT from the parent environment and restart Pi.",
@@ -6913,7 +6913,7 @@ function diagnosePi(input) {
6913
6913
  const packageHealthy = input.package.declared && input.package.name === "@arbiterforge/ca-pi" && existsSync(input.package.root) && existsSync(input.package.extensionPath) && samePath2(input.package.extensionPath, expectedExtension) && canonicallyInside(input.package.extensionPath, input.package.root);
6914
6914
  const trustHealthy = input.trust.inspected && (!input.trust.required || input.trust.projectTrusted);
6915
6915
  const waitingForTrust = input.trust.required && !input.trust.projectTrusted;
6916
- const versionHealthy = input.runtime.piVersion === "0.84.1" && atLeast(input.runtime.nodeVersion, [22, 19, 0]);
6916
+ const versionHealthy = input.runtime.piVersion === "1.0.0" && atLeast(input.runtime.nodeVersion, [22, 19, 0]);
6917
6917
  const piBelowMinimum = !atLeast(input.runtime.piVersion, [0, 80, 5]);
6918
6918
  const supportedExpansion = input.commands.expansionVerifiedVersions.includes(input.runtime.piVersion);
6919
6919
  const expectedDoctorSkill = resolve9(input.package.root, "skills", "ca-doctor", "SKILL.md");
@@ -6924,7 +6924,7 @@ function diagnosePi(input) {
6924
6924
  resolve9(input.package.root, "extensions", "codearbiter-child.js")
6925
6925
  ) && canonicallyInside(input.child.path, input.package.root) && existsSync(input.child.path);
6926
6926
  const coreHealthy = input.core.present && existsSync(input.core.bridgeScript) && samePath2(input.core.bridgeScript, resolve9(input.package.root, "hooks", "pi-bridge.py")) && canonicallyInside(input.core.bridgeScript, input.package.root);
6927
- const runtimeIdentityHealthy = existsSync(input.runtime.cliEntry) && existsSync(input.runtime.moduleEntry) && canonicallyInside(input.runtime.cliEntry, input.runtime.packageRoot) && canonicallyInside(input.runtime.moduleEntry, input.runtime.packageRoot) && samePath2(input.runtime.cliEntry, resolve9(input.runtime.packageRoot, "dist", "cli.js")) && samePath2(input.runtime.moduleEntry, resolve9(input.runtime.packageRoot, "dist", "index.js"));
6927
+ const runtimeIdentityHealthy = existsSync(input.runtime.cliEntry) && existsSync(input.runtime.moduleEntry) && canonicallyInside(input.runtime.cliEntry, input.runtime.packageRoot) && canonicallyInside(input.runtime.moduleEntry, input.runtime.packageRoot) && samePath2(input.runtime.cliEntry, resolve9(input.runtime.packageRoot, "dist", "bundle", "cli.js")) && samePath2(input.runtime.moduleEntry, resolve9(input.runtime.packageRoot, "dist", "index.js"));
6928
6928
  const mutators = ["bash", "write", "edit"];
6929
6929
  const wrapperHealthy = input.finalArguments.wrapperSourcePath !== void 0 && existsSync(input.finalArguments.wrapperSourcePath) && samePath2(input.finalArguments.wrapperSourcePath, expectedExtension) && mutators.every((name) => input.finalArguments.activeTools?.includes(name) === true) && mutators.every((name) => {
6930
6930
  const path = input.finalArguments.toolSources?.[name];
@@ -7034,7 +7034,7 @@ function diagnosePi(input) {
7034
7034
  {
7035
7035
  id: "active-dispatch",
7036
7036
  state: "degraded",
7037
- message: "Supported Pi 0.84.1 public extension APIs cannot submit this deterministic self-test through the active dispatcher; the wrapper self-test does not exercise active dispatch.",
7037
+ message: "Supported Pi 1.0.0 public extension APIs cannot submit this deterministic self-test through the active dispatcher; the wrapper self-test does not exercise active dispatch.",
7038
7038
  remediation: REMEDIATION["active-dispatch"]
7039
7039
  }
7040
7040
  ];
@@ -8296,17 +8296,17 @@ function validContentBlock(value, kind) {
8296
8296
  case "text":
8297
8297
  return exactKeys4(value, ["type", "text", "textSignature"], ["type", "text"]) && boundedString2(value.text) && (value.textSignature === void 0 || boundedString2(value.textSignature));
8298
8298
  case "image":
8299
- return kind !== "assistant" && exactKeys4(value, ["type", "data", "mimeType"]) && boundedString2(value.data) && boundedString2(value.mimeType);
8299
+ return (kind === "user" || kind === "toolResult") && exactKeys4(value, ["type", "data", "mimeType"]) && boundedString2(value.data) && boundedString2(value.mimeType);
8300
8300
  case "thinking":
8301
8301
  return kind === "assistant" && exactKeys4(value, ["type", "thinking", "thinkingSignature", "redacted"], ["type", "thinking"]) && boundedString2(value.thinking) && (value.thinkingSignature === void 0 || boundedString2(value.thinkingSignature)) && (value.redacted === void 0 || typeof value.redacted === "boolean");
8302
8302
  case "toolCall":
8303
- return kind === "assistant" && exactKeys4(value, ["type", "id", "name", "arguments", "thoughtSignature"], ["type", "id", "name", "arguments"]) && boundedString2(value.id) && boundedString2(value.name) && validOpaqueJson(value.arguments) && (value.thoughtSignature === void 0 || boundedString2(value.thoughtSignature));
8303
+ return kind === "assistant" && exactKeys4(value, ["type", "id", "name", "arguments", "thoughtSignature", "namespace"], ["type", "id", "name", "arguments"]) && boundedString2(value.id) && boundedString2(value.name) && validOpaqueJson(value.arguments) && (value.thoughtSignature === void 0 || boundedString2(value.thoughtSignature)) && (value.namespace === void 0 || boundedString2(value.namespace));
8304
8304
  default:
8305
8305
  return false;
8306
8306
  }
8307
8307
  }
8308
8308
  function validContent(value, kind) {
8309
- if (kind === "user" && typeof value === "string") return boundedString2(value);
8309
+ if ((kind === "user" || kind === "system") && typeof value === "string") return boundedString2(value);
8310
8310
  return Array.isArray(value) && value.length <= MAX_JSON_ARRAY && value.every((block) => validContentBlock(block, kind));
8311
8311
  }
8312
8312
  function validUsage(value) {
@@ -8335,24 +8335,32 @@ function validDeferredHandle(value) {
8335
8335
  ["provider", "modelId", "api", "id"]
8336
8336
  ) && ["provider", "modelId", "api", "id"].every((key) => boundedString2(value[key])) && (value.expiresAt === void 0 || typeof value.expiresAt === "number" && Number.isFinite(value.expiresAt)) && (value.pollAfterMs === void 0 || typeof value.pollAfterMs === "number" && Number.isFinite(value.pollAfterMs)) && (value.data === void 0 || validOpaqueJson(value.data));
8337
8337
  }
8338
+ function validNestedCalls(value) {
8339
+ return isRecord(value) && exactKeys4(value, ["calls", "complete"]) && typeof value.complete === "boolean" && Array.isArray(value.calls) && value.calls.length <= MAX_JSON_ARRAY && value.calls.every((call) => isRecord(call) && exactKeys4(call, ["id", "name", "arguments", "argumentsBytes", "status", "durationMs", "error"], ["id", "name", "status"]) && boundedString2(call.id) && boundedString2(call.name) && (call.status === "ok" || call.status === "error" || call.status === "unfinished") && (call.arguments === void 0 || isRecord(call.arguments) && validOpaqueJson(call.arguments)) && (call.argumentsBytes === void 0 || typeof call.argumentsBytes === "number" && Number.isFinite(call.argumentsBytes)) && (call.durationMs === void 0 || typeof call.durationMs === "number" && Number.isFinite(call.durationMs)) && (call.error === void 0 || boundedString2(call.error)));
8340
+ }
8341
+ function validSystemMessage(value) {
8342
+ const sections = value.sections;
8343
+ return exactKeys4(value, ["role", "content", "sections", "toolsAdded", "toolsRemoved", "timestamp"], ["role", "content", "timestamp"]) && validContent(value.content, "system") && (sections === void 0 || isRecord(sections) && Object.keys(sections).length <= MAX_JSON_KEYS && Object.keys(sections).every((key) => boundedString2(key) && (sections[key] === null || boundedString2(sections[key])))) && (value.toolsAdded === void 0 || Array.isArray(value.toolsAdded) && value.toolsAdded.length <= MAX_JSON_ARRAY && value.toolsAdded.every((tool) => isRecord(tool) && boundedString2(tool.name) && validOpaqueJson(tool))) && (value.toolsRemoved === void 0 || Array.isArray(value.toolsRemoved) && value.toolsRemoved.length <= MAX_JSON_ARRAY && value.toolsRemoved.every((tool) => isRecord(tool) && exactKeys4(tool, ["name"]) && boundedString2(tool.name))) && typeof value.timestamp === "number" && Number.isFinite(value.timestamp);
8344
+ }
8338
8345
  function validMessage(value) {
8339
8346
  if (!isRecord(value) || typeof value.role !== "string") return false;
8347
+ if (value.role === "system") return validSystemMessage(value);
8340
8348
  if (value.role === "user") {
8341
8349
  return exactKeys4(value, ["role", "content", "timestamp"]) && validContent(value.content, "user") && typeof value.timestamp === "number" && Number.isFinite(value.timestamp);
8342
8350
  }
8343
8351
  if (value.role === "assistant") {
8344
8352
  return exactKeys4(
8345
8353
  value,
8346
- ["role", "content", "api", "provider", "model", "responseModel", "responseId", "diagnostics", "usage", "stopReason", "errorMessage", "rawStopReason", "deferred", "timestamp"],
8354
+ ["role", "content", "api", "provider", "model", "responseModel", "responseId", "providerThinkingLevel", "thinkingLevel", "diagnostics", "usage", "stopReason", "errorMessage", "rawStopReason", "deferred", "endTurn", "timestamp"],
8347
8355
  ["role", "content", "api", "provider", "model", "usage", "stopReason", "timestamp"]
8348
- ) && validContent(value.content, "assistant") && ["api", "provider", "model", "stopReason"].every((key) => typeof value[key] === "string") && (value.responseModel === void 0 || boundedString2(value.responseModel)) && (value.responseId === void 0 || boundedString2(value.responseId)) && (value.errorMessage === void 0 || boundedString2(value.errorMessage)) && (value.rawStopReason === void 0 || boundedString2(value.rawStopReason)) && (value.deferred === void 0 || validDeferredHandle(value.deferred)) && (value.diagnostics === void 0 || Array.isArray(value.diagnostics) && value.diagnostics.length <= MAX_JSON_ARRAY && value.diagnostics.every(validDiagnostic)) && validUsage(value.usage) && typeof value.timestamp === "number" && Number.isFinite(value.timestamp);
8356
+ ) && validContent(value.content, "assistant") && ["api", "provider", "model", "stopReason"].every((key) => typeof value[key] === "string") && (value.responseModel === void 0 || boundedString2(value.responseModel)) && (value.responseId === void 0 || boundedString2(value.responseId)) && (value.errorMessage === void 0 || boundedString2(value.errorMessage)) && (value.rawStopReason === void 0 || boundedString2(value.rawStopReason)) && (value.providerThinkingLevel === void 0 || boundedString2(value.providerThinkingLevel)) && (value.thinkingLevel === void 0 || boundedString2(value.thinkingLevel)) && (value.endTurn === void 0 || typeof value.endTurn === "boolean") && (value.deferred === void 0 || validDeferredHandle(value.deferred)) && (value.diagnostics === void 0 || Array.isArray(value.diagnostics) && value.diagnostics.length <= MAX_JSON_ARRAY && value.diagnostics.every(validDiagnostic)) && validUsage(value.usage) && typeof value.timestamp === "number" && Number.isFinite(value.timestamp);
8349
8357
  }
8350
8358
  if (value.role === "toolResult") {
8351
8359
  return exactKeys4(
8352
8360
  value,
8353
- ["role", "toolCallId", "toolName", "content", "details", "isError", "usage", "timestamp"],
8361
+ ["role", "toolCallId", "toolName", "content", "details", "isError", "usage", "nestedCalls", "timestamp"],
8354
8362
  ["role", "toolCallId", "toolName", "content", "isError", "timestamp"]
8355
- ) && typeof value.toolCallId === "string" && typeof value.toolName === "string" && validContent(value.content, "toolResult") && (value.details === void 0 || validOpaqueJson(value.details)) && (value.usage === void 0 || validUsage(value.usage)) && typeof value.isError === "boolean" && typeof value.timestamp === "number" && Number.isFinite(value.timestamp);
8363
+ ) && typeof value.toolCallId === "string" && typeof value.toolName === "string" && validContent(value.content, "toolResult") && (value.details === void 0 || validOpaqueJson(value.details)) && (value.usage === void 0 || validUsage(value.usage)) && (value.nestedCalls === void 0 || validNestedCalls(value.nestedCalls)) && typeof value.isError === "boolean" && typeof value.timestamp === "number" && Number.isFinite(value.timestamp);
8356
8364
  }
8357
8365
  return false;
8358
8366
  }
@@ -8401,8 +8409,9 @@ function validAssistantEvent(value) {
8401
8409
  return exactWithOptionalPartial(["type"]);
8402
8410
  case "text_start":
8403
8411
  case "thinking_start":
8404
- case "toolcall_start":
8405
8412
  return exactWithOptionalPartial(["type", "contentIndex"]) && contentIndex();
8413
+ case "toolcall_start":
8414
+ return ("id" in value || "toolName" in value ? exactKeys4(value, ["type", "contentIndex", "id", "toolName"]) && boundedString2(value.id) && boundedString2(value.toolName) : exactWithOptionalPartial(["type", "contentIndex"])) && contentIndex();
8406
8415
  case "text_delta":
8407
8416
  case "thinking_delta":
8408
8417
  case "toolcall_delta":
@@ -8413,7 +8422,7 @@ function validAssistantEvent(value) {
8413
8422
  case "toolcall_end":
8414
8423
  return exactWithOptionalPartial(["type", "contentIndex", "toolCall"]) && contentIndex() && validContentBlock(value.toolCall, "assistant");
8415
8424
  case "done":
8416
- return exactKeys4(value, ["type", "reason", "message"]) && ["stop", "length", "toolUse"].includes(value.reason) && validMessage(value.message) && value.message.role === "assistant";
8425
+ return exactKeys4(value, ["type", "reason", "message"]) && ["stop", "length", "toolUse", "deferred"].includes(value.reason) && validMessage(value.message) && value.message.role === "assistant";
8417
8426
  case "error":
8418
8427
  return exactKeys4(value, ["type", "reason", "error"]) && ["aborted", "error"].includes(value.reason) && validMessage(value.error) && value.error.role === "assistant";
8419
8428
  default:
@@ -8435,7 +8444,8 @@ function parseChildJsonLine(line) {
8435
8444
  case "response":
8436
8445
  if (typeof record2.id !== "string" || record2.command !== "prompt" || typeof record2.success !== "boolean") invalidProtocol();
8437
8446
  if (record2.success === true) {
8438
- if (!exactKeys4(record2, ["type", "id", "command", "success"])) invalidProtocol();
8447
+ const data = record2.data;
8448
+ if (!exactKeys4(record2, ["type", "id", "command", "success", "data"], ["type", "id", "command", "success"]) || data !== void 0 && !(isRecord(data) && exactKeys4(data, ["disposition"]) && (data.disposition === "started" || data.disposition === "handled" || data.disposition === "queued"))) invalidProtocol();
8439
8449
  } else if (!exactKeys4(record2, ["type", "id", "command", "success", "error"]) || typeof record2.error !== "string") invalidProtocol();
8440
8450
  break;
8441
8451
  case "agent_start":
@@ -8454,16 +8464,16 @@ function parseChildJsonLine(line) {
8454
8464
  if (!exactKeys4(record2, ["type", "message"]) || !validMessage(record2.message) && !validPartialAssistantMessage(record2.message)) invalidProtocol();
8455
8465
  break;
8456
8466
  case "message_update":
8457
- if ("message" in record2 ? !exactKeys4(record2, ["type", "message", "assistantMessageEvent"]) || !validPartialAssistantMessage(record2.message) || !validAssistantEvent(record2.assistantMessageEvent) : !exactKeys4(record2, ["type", "assistantMessageEvent"]) || !validAssistantEvent(record2.assistantMessageEvent)) invalidProtocol();
8467
+ if ("message" in record2 ? !exactKeys4(record2, ["type", "message", "assistantMessageEvent"]) || !validPartialAssistantMessage(record2.message) || !validAssistantEvent(record2.assistantMessageEvent) : "usage" in record2 ? !exactKeys4(record2, ["type", "usage", "assistantMessageEvent"]) || !validUsage(record2.usage) || !validAssistantEvent(record2.assistantMessageEvent) : !exactKeys4(record2, ["type", "assistantMessageEvent"]) || !validAssistantEvent(record2.assistantMessageEvent)) invalidProtocol();
8458
8468
  break;
8459
8469
  case "tool_execution_start":
8460
- if (!exactKeys4(record2, ["type", "toolCallId", "toolName", "args"]) || typeof record2.toolCallId !== "string" || typeof record2.toolName !== "string" || !validOpaqueJson(record2.args)) invalidProtocol();
8470
+ if (!exactKeys4(record2, ["type", "toolCallId", "toolName", "args", "parentToolCallId"], ["type", "toolCallId", "toolName", "args"]) || record2.parentToolCallId !== void 0 && !boundedString2(record2.parentToolCallId) || typeof record2.toolCallId !== "string" || typeof record2.toolName !== "string" || !validOpaqueJson(record2.args)) invalidProtocol();
8461
8471
  break;
8462
8472
  case "tool_execution_update":
8463
- if (!exactKeys4(record2, ["type", "toolCallId", "toolName", "args", "partialResult"]) || typeof record2.toolCallId !== "string" || typeof record2.toolName !== "string" || !validOpaqueJson(record2.args) || !validOpaqueJson(record2.partialResult)) invalidProtocol();
8473
+ if (!exactKeys4(record2, ["type", "toolCallId", "toolName", "args", "partialResult", "parentToolCallId"], ["type", "toolCallId", "toolName", "args", "partialResult"]) || record2.parentToolCallId !== void 0 && !boundedString2(record2.parentToolCallId) || typeof record2.toolCallId !== "string" || typeof record2.toolName !== "string" || !validOpaqueJson(record2.args) || !validOpaqueJson(record2.partialResult)) invalidProtocol();
8464
8474
  break;
8465
8475
  case "tool_execution_end":
8466
- if (!exactKeys4(record2, ["type", "toolCallId", "toolName", "result", "isError"]) || typeof record2.toolCallId !== "string" || typeof record2.toolName !== "string" || !validOpaqueJson(record2.result) || typeof record2.isError !== "boolean") invalidProtocol();
8476
+ if (!exactKeys4(record2, ["type", "toolCallId", "toolName", "result", "isError", "parentToolCallId"], ["type", "toolCallId", "toolName", "result", "isError"]) || record2.parentToolCallId !== void 0 && !boundedString2(record2.parentToolCallId) || typeof record2.toolCallId !== "string" || typeof record2.toolName !== "string" || !validOpaqueJson(record2.result) || typeof record2.isError !== "boolean") invalidProtocol();
8467
8477
  break;
8468
8478
  case "extension_error":
8469
8479
  if (!exactKeys4(record2, ["type", "extensionPath", "event", "error"]) || typeof record2.extensionPath !== "string" || typeof record2.event !== "string" || typeof record2.error !== "string") invalidProtocol();
@@ -10588,7 +10598,7 @@ async function codeArbiterPi(pi) {
10588
10598
  activeTools: pi.getActiveTools(),
10589
10599
  allTools: pi.getAllTools(),
10590
10600
  expansionFingerprints,
10591
- childFingerprint: "22e5231b36a0af4a4202ced48a20e7dc9de435b6c6af985c6007bff96c5e04d1"
10601
+ childFingerprint: "86a08a0d5c29aa6693a07065f4ab2d6ad1d43293c65277cee944389886363d67"
10592
10602
  });
10593
10603
  const wrapperSelfTest = await runPiWrapperSelfTest({
10594
10604
  enabled: enabledForDoctor,
@@ -6,32 +6,32 @@
6
6
  "binaries": {
7
7
  "darwin/amd64": {
8
8
  "file": "ca-artifact-darwin-amd64",
9
- "sha256": "645fbde59c9db28a51d52dd7c975750f97613516c4003b7f4a3057359893687d",
9
+ "sha256": "f25d64dc4fae4874f97c6a982382fcd0685552b62ecc29714d2f8f41280c20b1",
10
10
  "native_tested": true
11
11
  },
12
12
  "darwin/arm64": {
13
13
  "file": "ca-artifact-darwin-arm64",
14
- "sha256": "390a6e9b65c2220bf52a6459eab4ec79283d4d560715a305280027f20e7a9d47",
14
+ "sha256": "92eb0c6394176508ed569d4969f2d5058b45c0b650cf538e6ea43f011ade5e1c",
15
15
  "native_tested": true
16
16
  },
17
17
  "linux/amd64": {
18
18
  "file": "ca-artifact-linux-amd64",
19
- "sha256": "65d39e119b446987ad8e8bdd592a1819165a95fe2c00ed887a1349a4fe692f86",
19
+ "sha256": "57135caf8e274ea0929a0ddae8d3f520473b9342398ab7db25c5688ebcb77347",
20
20
  "native_tested": true
21
21
  },
22
22
  "linux/arm64": {
23
23
  "file": "ca-artifact-linux-arm64",
24
- "sha256": "c5ae37567f9929b58beba88a3cd11e9e2de66bbb11a8f0fafbbae3bbcb0ba53f",
24
+ "sha256": "265c2d77b074017b30a2a979fd8926add4f0ede19a73bc83b77eb8a1147fecdc",
25
25
  "native_tested": true
26
26
  },
27
27
  "windows/amd64": {
28
28
  "file": "ca-artifact-windows-amd64.exe",
29
- "sha256": "b381d900520c5024ec20c86333a29f007fb6f8a2737214c77916912836c3e762",
29
+ "sha256": "a4ec0eeccca1c840a19ad8a0da57a660f365c20d565e72edfae8f1bf18498e7e",
30
30
  "native_tested": true
31
31
  },
32
32
  "windows/arm64": {
33
33
  "file": "ca-artifact-windows-arm64.exe",
34
- "sha256": "ccf2ca3a3ed103d247baee0275a8ce2c62af050a6af46d39c4186d8fd8ff2872",
34
+ "sha256": "3ac1d16a8030ffb8f12565f39b4a70245aa263048efa91e2f6fb5f613b3361f7",
35
35
  "native_tested": true
36
36
  }
37
37
  }
@@ -933,8 +933,11 @@ def _bind_commands(
933
933
  raise AuthorityError("UNSUPPORTED_EXECUTABLE", "declared Node runner entrypoint is unavailable") from exc
934
934
  launch_files.append(_launch_file(script, "runner-entrypoint"))
935
935
  if definition["required_tests"] and _command_name(definition["argv"][0]) in {"npm", "npm.cmd"}:
936
- _runner, manifest = _npm_runner(definition["argv"], cwd)
937
- launch_files.append(_launch_file(manifest, "npm-manifest"))
936
+ _runner, manifests = _npm_runner(definition["argv"], cwd)
937
+ if len(manifests) > 1:
938
+ launch_files.extend(_validate_nested_npm_launch(executable, manifests[0].parent))
939
+ launch_files.extend(_launch_file(manifest, "npm-manifest" if index == 0 else "npm-workspace-manifest")
940
+ for index, manifest in enumerate(manifests))
938
941
  bindings.append({
939
942
  "definition_sha256": command["definition_sha256"],
940
943
  "argv": argv,
@@ -1433,7 +1436,147 @@ def _command_tokens(command: str) -> list[str]:
1433
1436
  return list(lexer)
1434
1437
 
1435
1438
 
1436
- def _npm_runner(argv: list[str], cwd: Path | None) -> tuple[list[str], Path]:
1439
+ def _npm_package(manifest: Path) -> dict[str, Any]:
1440
+ _path_identity(manifest)
1441
+ with manifest.open("rb") as stream:
1442
+ raw = stream.read(MAX_STATE + 1)
1443
+ if len(raw) > MAX_STATE:
1444
+ raise ValueError("oversized manifest")
1445
+ package = json.loads(raw.decode("utf-8"), object_pairs_hook=_unique_json_pairs,
1446
+ parse_constant=_invalid_json_constant)
1447
+ if not isinstance(package, dict):
1448
+ raise ValueError("manifest is not an object")
1449
+ return package
1450
+
1451
+
1452
+ def _npm_script(package: dict[str, Any], name: str) -> list[str]:
1453
+ scripts = package["scripts"]
1454
+ script = scripts[name]
1455
+ if not isinstance(script, str) or any(marker in script for marker in ("\n", "\r", ";", "&", "|", ">", "<", "`", "$")):
1456
+ raise ValueError("compound script")
1457
+ if os.name == "nt" and any(marker in script for marker in ("^", "%", "!", "'")):
1458
+ raise ValueError("unsupported cmd.exe expansion or quoting")
1459
+ if scripts.get("pre" + name) or scripts.get("post" + name):
1460
+ raise ValueError("lifecycle scripts")
1461
+ tokens = _command_tokens(script)
1462
+ if not tokens:
1463
+ raise ValueError("empty script")
1464
+ return tokens
1465
+
1466
+
1467
+ def _npm_workspace_runner(tokens: list[str], package: dict[str, Any], manifest: Path) -> tuple[list[str], list[Path]]:
1468
+ # Exactly one explicit workspace selector and one delegation level. These
1469
+ # tokens select a collector; they never replace the declared root invocation.
1470
+ if (tokens[0] not in {"npm", "npm.cmd"} or (tokens[0] == "npm.cmd" and os.name != "nt")
1471
+ or len(tokens) < 5 or tokens[1] not in {"-w", "--workspace"}
1472
+ or tokens[3] not in {"run", "run-script"}
1473
+ or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9:._-]*", tokens[4])):
1474
+ raise ValueError("unsupported workspace delegation")
1475
+ selector = tokens[2]
1476
+ name_pattern = r"(?:@[a-z0-9._-]+/)?[a-z0-9][a-z0-9._-]*"
1477
+ if not re.fullmatch(name_pattern, selector):
1478
+ raise ValueError("workspace selector must be an exact package name")
1479
+ forwarded = tokens[5:]
1480
+ if forwarded:
1481
+ if forwarded[0] != "--":
1482
+ raise ValueError("workspace runner arguments must follow --")
1483
+ forwarded = forwarded[1:]
1484
+ workspaces = package.get("workspaces")
1485
+ if not isinstance(workspaces, list) or not 1 <= len(workspaces) <= 32:
1486
+ raise ValueError("workspaces must be a bounded array of explicit directories")
1487
+ inspected, names, paths, selected = [], set(), set(), None
1488
+ root = manifest.parent
1489
+ selector_path = root / selector
1490
+ for value in workspaces:
1491
+ if (not isinstance(value, str) or not value or any(c in value for c in "\\!*?[]{}()")
1492
+ or Path(value).anchor or PureWindowsPath(value).anchor
1493
+ or any(part in {"", ".", ".."} for part in value.split("/"))):
1494
+ raise ValueError("workspace path is not an explicit contained directory")
1495
+ directory = root
1496
+ for part in value.split("/"):
1497
+ directory /= part
1498
+ _path_identity(directory)
1499
+ directory.resolve(strict=True).relative_to(root.resolve(strict=True))
1500
+ workspace_manifest = directory / "package.json"
1501
+ identity = _path_identity(workspace_manifest)
1502
+ if identity in paths:
1503
+ raise ValueError("duplicate workspace directory")
1504
+ paths.add(identity)
1505
+ workspace = _npm_package(workspace_manifest)
1506
+ name = workspace.get("name")
1507
+ if not isinstance(name, str) or not re.fullmatch(name_pattern, name) or name in names:
1508
+ raise ValueError("missing or duplicate workspace name")
1509
+ names.add(name)
1510
+ inspected.append(workspace_manifest)
1511
+ # npm also treats selectors as directory filters. Refuse a name that
1512
+ # could select an additional workspace by its path or parent directory.
1513
+ if name != selector and (directory == selector_path or selector_path in directory.parents):
1514
+ raise ValueError("ambiguous workspace name and directory selector")
1515
+ if name == selector:
1516
+ selected = workspace
1517
+ if selected is None:
1518
+ raise ValueError("workspace name was not found")
1519
+ runner = _npm_script(selected, tokens[4])
1520
+ if _command_name(runner[0]) in {"npm", "npm.cmd"}:
1521
+ raise ValueError("recursive npm delegation")
1522
+ return [*runner, *forwarded], inspected
1523
+
1524
+
1525
+ def _validate_nested_npm_launch(executable: Path, cwd: Path) -> list[dict[str, str]]:
1526
+ # npm prepends every ancestor's node_modules/.bin to its script PATH.
1527
+ # A nested npm must resolve to the same qualified executable as its parent.
1528
+ names = ("npm", "npm.cmd", "npm.bat", "npm.exe", "npm.com") if os.name == "nt" else ("npm",)
1529
+ directories = [parent / "node_modules" / ".bin" for parent in (cwd, *cwd.parents)]
1530
+ if os.name == "nt":
1531
+ directories.insert(0, cwd) # cmd.exe searches its working directory first.
1532
+ if any(os.path.lexists(directory / name) for directory in directories for name in names):
1533
+ raise AuthorityError("UNSUPPORTED_EXECUTABLE", "nested npm is shadowed by a local executable")
1534
+ if _resolve_executable("npm", cwd) != executable:
1535
+ raise AuthorityError("UNSUPPORTED_EXECUTABLE", "nested npm resolves to a different executable")
1536
+ if os.name != "nt":
1537
+ return []
1538
+ # The outer launch pins npm-cli.js directly, but a script's npm.cmd consults
1539
+ # npm-prefix.js and may redirect to another installation. Qualify that same
1540
+ # helper in the manifest directory, and retain its identity with the CLI.
1541
+ node = executable.parent / "node.exe"
1542
+ cli = executable.parent / "node_modules" / "npm" / "bin" / "npm-cli.js"
1543
+ helper = cli.with_name("npm-prefix.js")
1544
+ try:
1545
+ helper_binding = _launch_file(helper, "npm-prefix")
1546
+ except (OSError, AuthorityError) as exc:
1547
+ raise AuthorityError("UNSUPPORTED_EXECUTABLE", "nested npm prefix helper is unavailable") from exc
1548
+ environment, _environment_digest = _minimal_environment()
1549
+
1550
+ def probe(arguments: list[str]) -> list[str]:
1551
+ try:
1552
+ result = _run_contained([str(node), *arguments], cwd=str(cwd), env=environment,
1553
+ timeout_seconds=15)
1554
+ if result.returncode or result.stderr or len(result.stdout) > 32768:
1555
+ raise ValueError("unsuccessful or oversized qualification output")
1556
+ return result.stdout.decode("utf-8", errors="strict").splitlines()
1557
+ except (OSError, ValueError, subprocess.SubprocessError, AuthorityError) as exc:
1558
+ raise AuthorityError("UNSUPPORTED_EXECUTABLE", "nested npm launch qualification failed") from exc
1559
+
1560
+ config = probe([str(cli), "config", "get", "script-shell", "node-options"])
1561
+ if config != ["script-shell=null", "node-options=null"]:
1562
+ raise AuthorityError("UNSUPPORTED_EXECUTABLE", "nested npm requires default script-shell and node-options")
1563
+ prefixes = probe([str(helper)])
1564
+ if (len(prefixes) != 1 or not Path(prefixes[0]).is_absolute()
1565
+ or any(ord(character) < 32 or character in '\"<>|?*' for character in prefixes[0])):
1566
+ raise AuthorityError("UNSUPPORTED_EXECUTABLE", "nested npm prefix qualification returned an invalid path")
1567
+ redirected_cli = Path(prefixes[0]) / "node_modules" / "npm" / "bin" / "npm-cli.js"
1568
+ if (os.path.lexists(redirected_cli)
1569
+ and os.path.normcase(os.path.normpath(redirected_cli)) != os.path.normcase(os.path.normpath(cli))):
1570
+ raise AuthorityError("UNSUPPORTED_EXECUTABLE", "nested npm prefix would redirect to a different CLI")
1571
+ try:
1572
+ if _launch_file(helper, "npm-prefix") != helper_binding:
1573
+ raise OSError("prefix helper changed during qualification")
1574
+ except (OSError, AuthorityError) as exc:
1575
+ raise AuthorityError("UNSUPPORTED_EXECUTABLE", "nested npm prefix helper changed during qualification") from exc
1576
+ return [helper_binding]
1577
+
1578
+
1579
+ def _npm_runner(argv: list[str], cwd: Path | None) -> tuple[list[str], list[Path]]:
1437
1580
  message = "declare a direct test runner with an explicit supported reporter; npm scripts must be a single runner command"
1438
1581
  if cwd is None:
1439
1582
  raise AuthorityError("UNSUPPORTED_COLLECTOR", "npm runner requires an inspectable package.json; " + message)
@@ -1468,25 +1611,22 @@ def _npm_runner(argv: list[str], cwd: Path | None) -> tuple[list[str], Path]:
1468
1611
  directory /= part
1469
1612
  _path_identity(directory)
1470
1613
  manifest.parent.resolve(strict=True).relative_to(cwd.resolve(strict=True))
1471
- _path_identity(manifest)
1472
- with manifest.open("rb") as stream:
1473
- raw = stream.read(MAX_STATE + 1)
1474
- if len(raw) > MAX_STATE:
1475
- raise ValueError("oversized manifest")
1476
- package = json.loads(raw.decode("utf-8"), object_pairs_hook=_unique_json_pairs,
1477
- parse_constant=_invalid_json_constant)
1478
- scripts = package["scripts"]
1479
- script = scripts[script_name]
1480
- if not isinstance(script, str) or any(marker in script for marker in ("\n", "\r", ";", "&", "|", ">", "<", "`", "$")):
1481
- raise ValueError("compound script")
1482
- if scripts.get("pre" + script_name) or scripts.get("post" + script_name):
1483
- raise ValueError("lifecycle scripts")
1484
- tokens = _command_tokens(script)
1485
- if not tokens or _command_name(tokens[0]) in {"npm", "npm.cmd"}:
1486
- raise ValueError("nested npm script")
1614
+ package = _npm_package(manifest)
1615
+ tokens = _npm_script(package, script_name)
1616
+ manifests = [manifest]
1487
1617
  except (OSError, ValueError, KeyError, TypeError, RecursionError, AuthorityError) as exc:
1488
1618
  raise AuthorityError("UNSUPPORTED_COLLECTOR", "npm package script is unavailable or compound; " + message) from exc
1489
- return [*tokens, *forwarded], manifest
1619
+ if _command_name(tokens[0]) in {"npm", "npm.cmd"}:
1620
+ # --prefix changes npm's config lookup and the environment inherited by
1621
+ # its script. Until those contexts are bound, qualify root delegation only.
1622
+ if relative != Path("."):
1623
+ raise AuthorityError("UNSUPPORTED_COLLECTOR", "nested npm workspace delegation requires --prefix .")
1624
+ try:
1625
+ runner, inspected = _npm_workspace_runner([*tokens, *forwarded], package, manifest)
1626
+ return runner, [manifest, *inspected]
1627
+ except (OSError, ValueError, KeyError, TypeError, RecursionError, AuthorityError) as exc:
1628
+ raise AuthorityError("UNSUPPORTED_COLLECTOR", "npm package script is unavailable or compound; " + message) from exc
1629
+ return [*tokens, *forwarded], manifests
1490
1630
 
1491
1631
 
1492
1632
  def validate_command_definition(definition: dict[str, Any], *, cwd: Path | None = None) -> str:
@@ -11,7 +11,7 @@ import hostapi # noqa: E402
11
11
  class PiHost(hostapi.Host):
12
12
  name = "pi"
13
13
  adapter_name = "@arbiterforge/ca-pi"
14
- adapter_version = "0.15.8"
14
+ adapter_version = "0.15.10"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "2.23.1"
316
+ adapter_version = "2.23.2"
317
317
 
318
318
  # Update-notifier descriptor. Each independently versioned host overrides
319
319
  # these three values in its per-plugin _host.py. Keeping the target,
@@ -262,13 +262,27 @@ the filename, not a leaf-name alias. Named unittest commands require `-v` or
262
262
  Plain exit checks cannot attest named tests.
263
263
 
264
264
  Named npm commands support inspectable single-runner scripts, a contained
265
- repository-relative `--prefix`, and arguments forwarded after `--`. Compound
265
+ repository-relative `--prefix`, and arguments forwarded after `--`. A root script
266
+ may delegate once through `npm -w <name> run <script>` or
267
+ `npm --workspace <name> run <script>` when the name resolves uniquely in an array
268
+ of at most 32 explicit, contained workspace directories. The runner binds the root
269
+ manifest and every inspected workspace manifest. Globs, linked directories,
270
+ multiple workspace selectors and recursive npm delegation are unsupported.
271
+ Workspace delegation uses the declared cwd; a non-dot `--prefix` is unsupported
272
+ for this shape.
273
+ Preserve both npm argument boundaries when adding a reporter: for a root
274
+ `test:client` script containing `npm -w @example/client run test`, declare
275
+ `npm run test:client -- -- --reporter=verbose` to forward the reporter to Vitest.
276
+ The original root command still executes the full script. Compound
266
277
  scripts and pre/post lifecycle scripts require separately declared direct-runner
267
278
  commands; retain every check from the original suite when splitting commands.
268
279
  On Windows, only the qualified standard `npm.cmd` layout is adapted: the runner
269
280
  pins the wrapper, sibling `node.exe`, sibling `node_modules/npm/bin/npm-cli.js`,
270
281
  and script manifest. It deliberately selects that sibling CLI rather than the
271
282
  shim's optional global-prefix redirection. Unknown batch wrappers fail closed.
283
+ For workspace delegation, it also pins `npm-prefix.js` and checks that the nested
284
+ wrapper uses the same CLI, the default script shell, and no configured Node
285
+ options. Local executables shadowing nested npm are rejected.
272
286
  Direct Node script entrypoints are also pinned. These launch-file bindings do
273
287
  not establish transitive dependency closure for deliberately excluded inputs.
274
288
 
@@ -74,7 +74,7 @@ file maps those actions to Pi's extension API.
74
74
  operator-launched Pi CLI, imported module, package root, and reported version.
75
75
  It does not prove publisher authenticity. Verify the source separately with
76
76
  `pi list` and `pi config`.
77
- - The supported promotion target is exact Pi 0.84.1. The package is
77
+ - The supported promotion target is exact Pi 1.0.0. The package is
78
78
  published as `npm:@arbiterforge/ca-pi` on every `ca-pi-v*` tag (ADR-0029);
79
79
  the pinned Git tag remains the reproducible install. A Pi-native embedded
80
80
  farm worker is a future spike, not an installed dependency.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.15.8",
3
+ "version": "0.15.10",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -32,7 +32,7 @@ repository binary, mutate an artifact, or silently fall back to Markdown.
32
32
  Pi bash wrapper. The exact shared-core `[H-03]` block is healthy and cannot stage files; execution
33
33
  or a different block is unhealthy. This self-test does not traverse Pi's active dispatcher. Do not
34
34
  rerun or respell it.
35
- 4. Its `active-dispatch` row remains degraded because supported Pi 0.84.1 public extension
35
+ 4. Its `active-dispatch` row remains degraded because supported Pi 1.0.0 public extension
36
36
  APIs cannot submit that deterministic call through the active dispatcher. PI-AC-28 remains blocked
37
37
  until supported-version real-host promotion/CI evidence closes the gap.
38
38
  5. Resolve Python once, run `<plugin-root>/hooks/doctor.py`, and present its mechanical report