pi-delegation-policy 0.23.1 → 0.23.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,50 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.23.3 - 2026-10-31
6
+
7
+ ### Fixed
8
+
9
+ - The role-mode reply from `consult_advisor` starts with `Not an error:` and limits its instruction
10
+ not to call the tool again to the time when Advisor launch remains `role`.
11
+ - The reply names the exact active host tool `subagent`, discovered at invocation time through Pi's
12
+ public tool API. If it is absent or inactive, the reply tells the model to inform the user that it
13
+ cannot launch the Advisor and not to improvise foreground execution, a CLI or another agent.
14
+
15
+ ### Compatibility and verification
16
+
17
+ - Only the role-mode tool result changes. Tool definitions, the policy block and all other replies
18
+ remain unchanged from 0.23.2. No configuration migration, dependency, profile, model, permission or
19
+ Pi minimum change; upgrade and reload as usual.
20
+ - Registered-tool tests cover the exact launcher name, its absence and changing availability within
21
+ one runtime. The existing definition-identity test remains unchanged and passes. These checks do
22
+ not measure provider cache hits or establish real-model behavior with the new wording.
23
+
24
+ ## 0.23.2 - 2026-10-31
25
+
26
+ ### Fixed
27
+
28
+ - `consult_advisor` now tells the model what to do for the effective Advisor state instead of
29
+ suggesting a switch to tool mode. Off means no consultation; role mode redirects to one background
30
+ host subagent call with every valid Advisor's exact profile, model and thinking policy, and says
31
+ not to call this tool again. Unavailable entries report their existing diagnostics. Invalid saved
32
+ Advisor settings explicitly require informing the user to save them again in `/delegate`, without
33
+ retrying the tool; ordinary availability errors do not ask for configuration changes.
34
+ - Busy, blank-question and missing-session replies now state the next action. ContextShunt recovery
35
+ errors also explain whether to use another range, obtain live evidence or continue without recovery.
36
+ The reader's JSON error contract is unchanged.
37
+
38
+ ### Compatibility and verification
39
+
40
+ - No configuration, schema, dependency, profile, permission or Pi minimum change. Upgrade and reload;
41
+ no migration is needed. Tool definitions stay fixed across modes and requests. The tool description
42
+ changes once on upgrade, but no additional policy-block or tool-definition variation is introduced.
43
+ - Registered-tool tests cover off, role, invalid and valid tool calls, exact thinking rules and stable
44
+ definitions. Saved-state guidance is tested at the message constructor without corrupt files; its
45
+ connection to the registered tool was reviewed. Policy output was independently checked byte for
46
+ byte against the previous release. These checks do not measure provider cache hits or establish
47
+ real-model behavior after an accidental role-mode tool call.
48
+
5
49
  ## 0.23.1 - 2026-10-29
6
50
 
7
51
  ### Fixed
package/README.md CHANGED
@@ -6,7 +6,7 @@ general subagent runner. Only explicit tools can request reader work or Advisor
6
6
  the authorized host executor; hooks never launch children.
7
7
 
8
8
  > **Docs:** [yivas.github.io/pi-delegation-policy](https://yivas.github.io/pi-delegation-policy/).
9
- > Version **0.23.1** requires Pi `0.87.1` or later (`>=0.87.1`); it re-applies its marked policy block in the final provider payload.
9
+ > Version **0.23.3** requires Pi `0.87.1` or later (`>=0.87.1`); it re-applies its marked policy block in the final provider payload.
10
10
  > Version `0.14.1` supports Pi `0.84.3` or later (`>=0.84.3`).
11
11
  >
12
12
  > Independent Advisor mode and its companion file are available from version `0.17.0`.
@@ -15,7 +15,7 @@ the authorized host executor; hooks never launch children.
15
15
  ## Install
16
16
 
17
17
  ```bash
18
- pi install npm:pi-delegation-policy@0.23.1
18
+ pi install npm:pi-delegation-policy@0.23.3
19
19
  # restart Pi, or run /reload
20
20
  ```
21
21
 
package/SECURITY.md CHANGED
@@ -69,4 +69,4 @@ Include the affected version or commit, operating system, Pi version, reproducti
69
69
 
70
70
  ## Supported versions
71
71
 
72
- Only the latest published version is supported. Version `0.23.1` requires Pi `0.87.1` or later; version `0.14.1` requires Pi `0.84.3` or later.
72
+ Only the latest published version is supported. Version `0.23.3` requires Pi `0.87.1` or later; version `0.14.1` requires Pi `0.84.3` or later.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-delegation-policy",
3
- "version": "0.23.1",
3
+ "version": "0.23.3",
4
4
  "private": false,
5
5
  "description": "A Pi extension for configurable delegation intensity and exact subagent role model references.",
6
6
  "type": "module",
@@ -132,7 +132,8 @@ export class ContextShuntAdapter {
132
132
  ): Promise<{ text?: string; error?: string; range?: string }> {
133
133
  if (settings.mode !== "enforce") {
134
134
  return {
135
- error: "ContextShunt recovery is unavailable while enforcement is off.",
135
+ error:
136
+ "ContextShunt recovery is unavailable while enforcement is off. Continue without recovery; do not ask the user to enable enforcement unless they raise it.",
136
137
  };
137
138
  }
138
139
  const result = await this.artifacts.recover(request, settings.limits.targetedReadBytes);
@@ -393,19 +393,26 @@ export class ArtifactStore {
393
393
 
394
394
  async recover(request: RecoveryRequest, maxRangeBytes: number): Promise<RecoveryResult> {
395
395
  return this.withLock(async () => {
396
- if (this.closed) return { error: "Recovery artifact is unavailable or expired." };
396
+ const unavailable = {
397
+ error:
398
+ "Recovery artifact is unavailable or expired. Do not retry this artifact ID; use a new live artifact from an authorized bounded read if the evidence is still needed.",
399
+ };
400
+ if (this.closed) return unavailable;
397
401
  const hasLines = request.lineOffset !== undefined || request.lineLimit !== undefined;
398
402
  const hasBytes = request.byteOffset !== undefined || request.maxBytes !== undefined;
399
- if (hasLines === hasBytes) return { error: "Choose either a line range or a byte range." };
403
+ if (hasLines === hasBytes)
404
+ return {
405
+ error:
406
+ "Choose either a line range or a byte range. Call again with lineOffset/lineLimit or byteOffset/maxBytes, not both.",
407
+ };
400
408
 
401
409
  await this.purgeUnlocked();
402
410
  const artifact = this.artifacts.get(request.artifactId);
403
- if (!artifact) return { error: "Recovery artifact is unavailable or expired." };
411
+ if (!artifact) return unavailable;
404
412
  if (artifact.kind === "derived") {
405
413
  if (!artifact.sourceId || !(await this.readSourceUnlocked(artifact.sourceId)))
406
- return { error: "Recovery artifact is unavailable or expired." };
407
- if (!this.artifacts.has(request.artifactId))
408
- return { error: "Recovery artifact is unavailable or expired." };
414
+ return unavailable;
415
+ if (!this.artifacts.has(request.artifactId)) return unavailable;
409
416
  }
410
417
 
411
418
  let source: Buffer;
@@ -413,11 +420,11 @@ export class ArtifactStore {
413
420
  source = await readFile(artifact.path);
414
421
  } catch {
415
422
  await this.remove(request.artifactId, artifact);
416
- return { error: "Recovery artifact is unavailable or expired." };
423
+ return unavailable;
417
424
  }
418
425
  if (digest(source) !== artifact.digest) {
419
426
  await this.remove(request.artifactId, artifact);
420
- return { error: "Recovery artifact is unavailable or expired." };
427
+ return unavailable;
421
428
  }
422
429
 
423
430
  if (hasBytes) {
@@ -431,11 +438,16 @@ export class ArtifactStore {
431
438
  offset > source.length ||
432
439
  offset > Number.MAX_SAFE_INTEGER - length
433
440
  ) {
434
- return { error: "Invalid byte range." };
441
+ return {
442
+ error: `Invalid byte range. Call again with a nonnegative integer byteOffset within the artifact and positive integer maxBytes no greater than ${maxRangeBytes}.`,
443
+ };
435
444
  }
436
445
  const end = Math.min(source.length, offset + length);
437
446
  if (!isUtf8Boundary(source, offset) || !isUtf8Boundary(source, end)) {
438
- return { error: "Byte ranges must start and end at UTF-8 character boundaries." };
447
+ return {
448
+ error:
449
+ "Byte ranges must start and end at UTF-8 character boundaries. Call again with adjusted byte boundaries or use a bounded line range.",
450
+ };
439
451
  }
440
452
  return {
441
453
  text: source.subarray(offset, end).toString("utf8"),
@@ -446,10 +458,16 @@ export class ArtifactStore {
446
458
  const offset = request.lineOffset ?? 0;
447
459
  const limit = request.lineLimit ?? 1;
448
460
  if (!Number.isSafeInteger(offset) || offset < 0 || !positiveInteger(limit) || limit > 250) {
449
- return { error: "Invalid line range." };
461
+ return {
462
+ error:
463
+ "Invalid line range. Call again with a nonnegative integer lineOffset and an integer lineLimit from 1 to 250.",
464
+ };
450
465
  }
451
466
  const lines = splitLines(source.toString("utf8"));
452
- if (offset >= lines.length) return { error: "Invalid line range." };
467
+ if (offset >= lines.length)
468
+ return {
469
+ error: `Invalid line range. Call again with lineOffset less than ${lines.length}.`,
470
+ };
453
471
  const text = lines.slice(offset, offset + limit).join("");
454
472
  if (Buffer.byteLength(text, "utf8") > maxRangeBytes) {
455
473
  return { error: "Requested line range exceeds the byte budget; use byte recovery." };
package/src/index.ts CHANGED
@@ -39,7 +39,12 @@ import {
39
39
  readerToolError,
40
40
  type ReaderThinking,
41
41
  } from "./context-shunt-reader.ts";
42
- import { type AdvisorContext, buildDelegationPolicy, selectAdvisorContext } from "./prompt.ts";
42
+ import {
43
+ type AdvisorContext,
44
+ advisorRoleLines,
45
+ buildDelegationPolicy,
46
+ selectAdvisorContext,
47
+ } from "./prompt.ts";
43
48
  import { updateProviderPolicy } from "./provider-policy.ts";
44
49
  import {
45
50
  advisorStatusLabel,
@@ -310,6 +315,20 @@ export function statusText(state: RuntimeState): string {
310
315
  if (errors.length) details.push(`details=${errors.join("; ")}`);
311
316
  return details.join(" | ");
312
317
  }
318
+ export function advisorUnavailableText(state: RuntimeState): string {
319
+ const entryErrors = state.advisorEntries?.flatMap((entry) => entry.errors) ?? [];
320
+ const diagnostics = [
321
+ ...(state.advisorErrors ?? []).filter((error) => !entryErrors.includes(error)),
322
+ ...(state.advisorEntries ?? []).flatMap((entry) =>
323
+ entry.errors.map((error) => `Advisor ${entry.index + 1}: ${error}`),
324
+ ),
325
+ ];
326
+ const action = state.diagnostics.some((diagnostic) => diagnostic.scope === "advisor")
327
+ ? "Continue without Advisor consultation. Tell the user that the saved Advisor configuration is invalid and must be saved again in /delegate. Do not retry this tool."
328
+ : "Continue without Advisor consultation. Do not retry with the same settings or ask the user to change them unless they raise it.";
329
+ return `Advisor unavailable: no valid Advisors.\n${diagnostics.join("\n")}\n${action}`;
330
+ }
331
+
313
332
  function notifyAppendFailure(ctx: ExtensionCommandContext, result: GuardedAppendResult): void {
314
333
  ctx.ui.notify(
315
334
  result === "guard-failed"
@@ -525,7 +544,7 @@ export function createPiDelegationPolicy(options: PiDelegationPolicyOptions = {}
525
544
  name: "consult_advisor",
526
545
  label: "Consult Advisor",
527
546
  description:
528
- "Consult the configured advisors after orientation before substantive work, when stuck, before changing approach, and before declaring a multi-step task done (ask for remaining risks, not approval). Skip short reactive steps and mechanics within a settled approach. Supply a self-contained brief for fresh context or a rejected fork. Advice only; may send the conversation to each provider and incur cost.",
547
+ "Use only when advisor-launch=tool; in role mode launch Advisor with the host subagent tool instead. Consult the configured advisors after orientation before substantive work, when stuck, before changing approach, and before declaring a multi-step task done (ask for remaining risks, not approval). Skip short reactive steps and mechanics within a settled approach. Supply a self-contained brief for fresh context or a rejected fork. Advice only; may send the conversation to each provider and incur cost.",
529
548
  parameters: Type.Object(
530
549
  {
531
550
  question: Type.String({ minLength: 1 }),
@@ -540,22 +559,39 @@ export function createPiDelegationPolicy(options: PiDelegationPolicyOptions = {}
540
559
  });
541
560
  const state = await refreshRuntime(ctx);
542
561
  const advisors = validAdvisors(state);
543
- if (state.effective.advisorLaunch !== "tool" || !advisors.length)
562
+ if (!isAdvisorEnabled(state.effective))
563
+ return text(
564
+ "Advisor is off in /delegate for this session. Do not consult it and do not ask the user to enable it unless they raise it.",
565
+ );
566
+ if (!advisors.length) return text(advisorUnavailableText(state));
567
+ if (state.effective.advisorLaunch === "role") {
568
+ const subagentTool = pi.getActiveTools().find((name) => name === "subagent");
569
+ if (!subagentTool)
570
+ return text(
571
+ 'Not an error: Advisor launch is "role", so this tool does not launch Advisors; do not call this tool again while Advisor launch is "role". The host subagent tool "subagent" is not available. Tell the user that you cannot launch the Advisor with the available host tools. Do not improvise another mechanism: do not use foreground execution, a CLI, or another agent.',
572
+ );
544
573
  return text(
545
- "Advisor unavailable: select tool launch and enable at least one valid Advisor in /delegate.",
574
+ `Not an error: Advisor launch is "role", so this tool does not launch Advisors; do not call this tool again while Advisor launch is "role". Launch every listed Advisor with the host subagent tool "${subagentTool}", using each exact agent profile and model below, in the background, all listed Advisors with the same question in one call, as the delegation policy describes. Replace LEVEL with a supported thinking level chosen per run under that Advisor's thinking policy; a fixed level must not change.\n${advisorRoleLines(state)}`,
546
575
  );
576
+ }
547
577
  if (advisorExecutor.busy)
548
578
  return text(
549
- "Advisor busy: one consultation group is already in flight for this session.",
579
+ "Advisor busy: one consultation group is already in flight for this session. Wait for its result before starting another consultation; do not duplicate the pending group.",
580
+ );
581
+ if (!input.question.trim())
582
+ return text(
583
+ "Advisor error: question must not be blank. Call again with a specific question.",
550
584
  );
551
- if (!input.question.trim()) return text("Advisor error: question must not be blank.");
552
585
  const context = resolveAdvisorContext(ctx, state);
553
586
  if (context === "fresh" && !input.brief?.trim())
554
587
  return text(
555
588
  "Advisor error: fresh context requires brief. Supply a short self-contained objective, constraints, relevant facts, options, and uncertainty.",
556
589
  );
557
590
  const sessionId = currentSessionId(ctx);
558
- if (!sessionId) return text("Advisor unavailable: host session identity is missing.");
591
+ if (!sessionId)
592
+ return text(
593
+ "Advisor unavailable: host session identity is missing. Continue without Advisor consultation; do not retry until the host supplies a session identity.",
594
+ );
559
595
  const results = await advisorExecutor.execute(
560
596
  advisors.map((entry) => ({
561
597
  question: input.question,
@@ -717,7 +753,8 @@ export function createPiDelegationPolicy(options: PiDelegationPolicyOptions = {}
717
753
  const result = latestRuntime
718
754
  ? await shunt.recover(input, latestRuntime.effective.contextShunt)
719
755
  : {
720
- error: "ContextShunt recovery is unavailable until session state is initialized.",
756
+ error:
757
+ "ContextShunt recovery is unavailable until session state is initialized. Continue without recovery; retry only after the host initializes the session.",
721
758
  };
722
759
  return {
723
760
  content: [
package/src/prompt.ts CHANGED
@@ -261,14 +261,10 @@ export function buildPolicyPreview(
261
261
  ];
262
262
  }
263
263
 
264
- export function buildDelegationPolicy(
265
- state: RuntimeState,
266
- advisorContext: AdvisorContext = "fresh",
267
- ): string | undefined {
264
+ /** Shared with role-mode tool redirection so launch models and thinking stay identical. */
265
+ export function advisorRoleLines(state: RuntimeState): string {
268
266
  const { effective } = state;
269
- const advisorEnabled = isAdvisorEnabled(effective) && !hasAdvisorError(state);
270
- const advisors = advisorEnabled ? validAdvisors(state) : [];
271
- const advisorRoleLine = advisors
267
+ return validAdvisors(state)
272
268
  .map(({ index, setting }) => {
273
269
  const thinking = roleThinking(setting.thinking);
274
270
  const label =
@@ -277,6 +273,16 @@ export function buildDelegationPolicy(
277
273
  - ${label}: ${formatReference(setting)}; exact model base: ${formatLaunchModel(setting)}; pi-subagents form: ${formatThinkingLaunchModel(setting, thinking)}; thinking policy: ${thinkingPolicyText(thinking)}; agent profile: ${promptString(advisorAgentName(setting))}${setting.providerExtension ? "; provider extension explicitly enabled: normal configuration, startup hook and account diagnostic command are trusted host code, not model tools. Never invoke the diagnostic automatically." : ""}`;
278
274
  })
279
275
  .join("");
276
+ }
277
+
278
+ export function buildDelegationPolicy(
279
+ state: RuntimeState,
280
+ advisorContext: AdvisorContext = "fresh",
281
+ ): string | undefined {
282
+ const { effective } = state;
283
+ const advisorEnabled = isAdvisorEnabled(effective) && !hasAdvisorError(state);
284
+ const advisors = advisorEnabled ? validAdvisors(state) : [];
285
+ const advisorRoleLine = advisorEnabled ? advisorRoleLines(state) : "";
280
286
  if (effective.intensity === "off" || hasRuntimeError(state)) {
281
287
  if (!advisorRoleLine) return undefined;
282
288
  return `<delegation_policy>