pi-delegation-policy 0.5.0 → 0.7.0

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,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.0 - 2026-09-06
6
+
7
+ ### Added
8
+
9
+ - Add the `orchestrator` intensity with `/delegate orchestrator` and `D:ORCH`. It delegates transferable research, detailed planning, implementation, testing, writing, review, and integration mechanics while keeping objectives, critical decisions, coordination, evidence, and final acceptance with the main agent.
10
+ - Add orchestrator guidance for batching small work, concise briefs and results with file references, and avoiding duplicate inspection without a concrete gap or risk.
11
+
12
+ ## 0.6.0 - 2026-08-29
13
+
14
+ ### Added
15
+
16
+ - Allow Small, Medium, and Large to be explicitly disabled independently while requiring one enabled ordinary role for an active policy.
17
+ - Add schema 3 defaults and session state, with in-memory schema 2 migration and guarded session writes for safer downgrade behavior. Downgrading after saving schema 3 defaults or manually editing them requires `/delegate off` in each active branch and manual conversion of ordinary `null` values.
18
+
19
+ ### Changed
20
+
21
+ - Select only enabled ordinary roles that can satisfy the task, leaving work with the main agent when none can; Small/Medium preferences are inactive when either role is disabled.
22
+
5
23
  ## 0.5.0 - 2026-08-28
6
24
 
7
25
  ### Added
package/README.md CHANGED
@@ -1,25 +1,27 @@
1
1
  # pi-delegation-policy
2
2
 
3
- A local Pi extension that helps the main agent decide **when delegation is worth it** and which exact models to use for the Small, Medium, Large, and optional Visual Design roles. It provides guidance; it is not a subagent runner.
3
+ A local Pi extension that helps the main agent decide **when delegation is worth it** and which exact models to use for Small, Medium, Large, and optional Visual Design. It provides guidance; it is not a subagent runner.
4
4
 
5
- > **Status:** Version **0.5.0** is the latest published package. The package requires Pi `>=0.84.3`; Pi `0.84.3` is the explicitly checked baseline (newer versions are not claimed as tested). The repository and npm package describe the same published behavior.
5
+ > **Status:** Version **0.7.0** is the latest published package and supports `off`, `normal`, `aggressive`, and `orchestrator`. The package requires Pi `>=0.84.3`; Pi `0.84.3` is the explicitly checked baseline.
6
6
  >
7
7
  > **Docs:** [Read the documentation site](https://yivas.github.io/pi-delegation-policy/).
8
8
 
9
9
  ## Value and boundary
10
10
 
11
- - Choose `off`, `normal`, or `aggressive` delegation intensity globally or for the current session branch.
12
- - Configure exact provider/model references for Small, Medium, Large, and optionally Visual Design.
11
+ - Choose `off`, `normal`, `aggressive`, or `orchestrator` globally or for the current session branch.
12
+ - Configure an exact provider/model reference or explicitly disable each ordinary role.
13
13
  - Keep global defaults and session-branch overrides across reload, resume, and tree navigation.
14
14
  - Validate active configurations before injecting one policy block through Pi's public `before_agent_start` event.
15
15
 
16
- The extension guides the main agent. It never creates, launches, routes, supervises, or blocks subagents; changes Pi's main model or thinking; stores credentials; intercepts tools; or makes its own network requests. It has no model fallback, telemetry, project configuration, presets, or external skill loading.
16
+ The extension guides the main agent. It never creates, launches, routes, supervises, or blocks subagents; changes Pi's main model or thinking; stores credentials; intercepts tools; or makes its own network requests. `orchestrator` delegates transferable execution detail and batches small work while the main agent retains objectives, critical decisions, coordination, integration responsibility, requested evidence, and final acceptance. It has no model fallback, telemetry, project configuration, presets, or external skill loading.
17
17
 
18
- Visual Design is an optional specialist for direction, assets, bounded presentation-layer implementation, and visual review. Use it only when behavior and data contracts are already defined and unchanged, the affected surface is bounded, and visual quality or user experience is the primary acceptance criterion. Route business logic, data, APIs, routes, application architecture, tooling, interaction behavior, and cross-system integration to Small, Medium, or Large by task fit. The main agent retains final integration and acceptance.
18
+ A valid active policy requires an explicit decision for Small, Medium, and Large: an exact model reference or disabled. At least one ordinary role must remain enabled. A disabled role is not validated. An absent role, an invalid enabled reference, or no enabled ordinary role produces `D:ERR` and injects no policy. `off` always produces `D:OFF` without injection.
19
19
 
20
- ## Install and start
20
+ The policy considers only enabled ordinary roles, chooses the least costly role that can satisfy the task's acceptance criteria and evidence, and keeps work with the main agent when none can. It never invents a model or role. `efficient` and `intensive` are tie-breaks only when both Small and Medium are enabled; otherwise their bias is inactive.
21
+
22
+ Visual Design is an independent optional specialist for direction, assets, bounded presentation-layer implementation, and visual review. Use it only when behavior and data contracts are already defined and unchanged, the affected surface is bounded, and visual quality or user experience is the primary acceptance criterion. It does not count as an ordinary role or replace one. Route business logic, data, APIs, routes, application architecture, tooling, interaction behavior, and cross-system integration to an enabled ordinary role by task fit. The main agent retains final integration and acceptance.
21
23
 
22
- Install the published package in Pi's user settings, then reload Pi:
24
+ ## Install and start
23
25
 
24
26
  ```bash
25
27
  pi install npm:pi-delegation-policy
@@ -27,12 +29,16 @@ pi install npm:pi-delegation-policy
27
29
  ```
28
30
 
29
31
  1. Open `/delegate` (or press `Alt+G` in Pi's TUI).
30
- 2. Configure exact, authenticated Small, Medium, and Large provider/model references. Visual Design is optional.
31
- 3. Select `normal` or `aggressive`, then choose **Apply changes**.
32
- 4. Run `/delegate status` and inspect the exact role references and their sources. `D:ERR` means an active required role is invalid; no policy is injected.
33
- 5. The applied state affects the **next** agent run. An agent already running is not rewritten.
32
+ 2. For Small, Medium, and Large, select an exact authenticated provider/model or **Disable for this session**. Keep at least one enabled.
33
+ 3. Select `normal`, `aggressive`, or `orchestrator`, then choose **Apply changes**.
34
+ 4. Run `/delegate status`. `disabled`, `not configured`, and exact references remain distinct. `D:ERR` means no policy is injected.
35
+ 5. The applied state affects the **next** agent run.
36
+
37
+ Global defaults are stored at `~/.pi/agent/delegation-policy.json` and use schema version 3. Schema 2 defaults and session entries are read and normalized in memory without rewriting them. Schema 3 stores `null` for an explicitly disabled ordinary role. Session changes write a schema 2 `off` guard before the schema 3 state; saving defaults changes only the global file.
34
38
 
35
- See the [end-to-end getting-started guide](https://yivas.github.io/pi-delegation-policy/getting-started/) and [configuration reference](https://yivas.github.io/pi-delegation-policy/configuration/) for details.
39
+ Before downgrading to `0.6.0`, change the global intensity to `off`, `normal`, or `aggressive` and run `/delegate off` in every active branch. For `<=0.5.0`, also convert global defaults to schema 2 and replace ordinary `null` values with exact model references. Schema 2 never accepts `orchestrator`. See the configuration reference for details.
40
+
41
+ See the [getting-started guide](https://yivas.github.io/pi-delegation-policy/getting-started/) and [configuration reference](https://yivas.github.io/pi-delegation-policy/configuration/).
36
42
 
37
43
  ## Essential commands
38
44
 
@@ -41,17 +47,12 @@ See the [end-to-end getting-started guide](https://yivas.github.io/pi-delegation
41
47
  /delegate off Disable policy for this session branch
42
48
  /delegate normal Enable balanced delegation guidance
43
49
  /delegate aggressive Enable delegation-first guidance
50
+ /delegate orchestrator Minimize main-agent execution and narration
44
51
  /delegate status Show effective session state
45
52
  /delegate reset Reset this branch to off and other fields to global defaults
46
53
  ```
47
54
 
48
- The editor is a bounded, keyboard-first panel. It shows model ID first and `[provider]` last, fuzzy-searches provider, model ID, and display name, and shows at most 10 model rows. It also shows a compact effective-policy preview, field explanations, and public model metadata when Pi supplies it. Changes are drafts until **Apply changes**; saving effective configuration as defaults updates the global file without applying the draft, and closing a modified draft requires explicit discard.
49
-
50
- ## Configuration and safety
51
-
52
- Global defaults are stored at `~/.pi/agent/delegation-policy.json` (schema version 2). The file stores optional intensity, preference, exact role references, and an optional Visual Design reference under the stable `uiDesign` key; it never stores thinking. A valid active mode requires exact, available, in-scope, authenticated Small, Medium, and Large references. Before every delegated launch, the policy requires the selected role's exact `provider/model` base and a thinking level chosen for that task instead of relying on ambient defaults. With `pi-subagents`, the main agent passes both as `model: "provider/model:LEVEL"`; other launchers may expose a separate per-run thinking field. Invalid active configuration fails closed as `D:ERR` with no policy injection; `off` is always `D:OFF`.
53
-
54
- The extension stores only policy settings and provider/model identifiers in local defaults and session entries. Review local configuration before sharing diagnostics, and remove credentials, prompts, personal paths, session files, and unredacted logs from reports. See the [limits and privacy reference](https://yivas.github.io/pi-delegation-policy/limits-and-privacy/) and [security policy](https://github.com/Yivas/pi-delegation-policy/blob/main/SECURITY.md).
55
+ The editor is a bounded, keyboard-first panel. Every model selector pins **Use global default** and **Disable for this session** before searchable models. It shows model ID first and `[provider]` last, fuzzy-searches provider, model ID, and display name, and shows at most 10 model rows. It also shows a compact effective-policy preview, field explanations, and public model metadata when Pi supplies it. Changes are drafts until **Apply changes**; saving effective configuration as defaults updates only the global file without applying the draft, and closing a modified draft requires explicit discard.
55
56
 
56
57
  ## Development
57
58
 
@@ -65,8 +66,8 @@ npm run build
65
66
  npm run pack:check
66
67
  ```
67
68
 
68
- Tests use local mocks and do not make paid model calls or network requests. See [CONTRIBUTING.md](https://github.com/Yivas/pi-delegation-policy/blob/main/CONTRIBUTING.md) for contribution guidance.
69
+ Tests use local mocks and do not make paid model calls or network requests. See [CONTRIBUTING.md](https://github.com/Yivas/pi-delegation-policy/blob/main/CONTRIBUTING.md).
69
70
 
70
71
  ## License
71
72
 
72
- MIT. See [LICENSE](https://github.com/Yivas/pi-delegation-policy/blob/main/LICENSE).
73
+ MIT. See [LICENSE](LICENSE).
package/SECURITY.md CHANGED
@@ -14,4 +14,4 @@ Include the affected version or commit, operating system, Pi version, reproducti
14
14
 
15
15
  ## Supported versions
16
16
 
17
- Only the latest published version is supported. Version 0.5.0 is the current supported release.
17
+ Only the latest published version is supported. Version 0.7.0 is the current supported release.
@@ -1,5 +1,5 @@
1
1
  {
2
- "schemaVersion": 2,
2
+ "schemaVersion": 3,
3
3
  "intensity": "normal",
4
4
  "preference": "standard",
5
5
  "small": {
@@ -10,10 +10,7 @@
10
10
  "provider": "example-provider",
11
11
  "model": "example-medium"
12
12
  },
13
- "large": {
14
- "provider": "example-provider",
15
- "model": "example-large"
16
- },
13
+ "large": null,
17
14
  "uiDesign": {
18
15
  "provider": "example-provider",
19
16
  "model": "example-ui-design"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-delegation-policy",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "description": "A Pi extension for configurable delegation intensity and exact subagent role model references.",
6
6
  "type": "module",
@@ -44,10 +44,10 @@
44
44
  "@earendil-works/pi-ai": "0.84.3",
45
45
  "@earendil-works/pi-coding-agent": "0.84.3",
46
46
  "@earendil-works/pi-tui": "0.84.3",
47
- "@eslint/js": "^9.39.5",
48
- "@types/node": "^22.0.0",
47
+ "@eslint/js": "^10.0.1",
48
+ "@types/node": "^26.2.0",
49
49
  "ajv": "^8.20.0",
50
- "eslint": "^9.0.0",
50
+ "eslint": "^10.9.1",
51
51
  "prettier": "^3.0.0",
52
52
  "typescript": "^5.7.0",
53
53
  "typescript-eslint": "^8.68.0"
@@ -5,12 +5,12 @@
5
5
  "type": "object",
6
6
  "required": ["schemaVersion"],
7
7
  "properties": {
8
- "schemaVersion": { "const": 2 },
9
- "intensity": { "enum": ["off", "normal", "aggressive"] },
8
+ "schemaVersion": { "const": 3 },
9
+ "intensity": { "enum": ["off", "normal", "aggressive", "orchestrator"] },
10
10
  "preference": { "enum": ["efficient", "standard", "intensive"] },
11
- "small": { "$ref": "#/$defs/modelRef" },
12
- "medium": { "$ref": "#/$defs/modelRef" },
13
- "large": { "$ref": "#/$defs/modelRef" },
11
+ "small": { "$ref": "#/$defs/ordinaryRole" },
12
+ "medium": { "$ref": "#/$defs/ordinaryRole" },
13
+ "large": { "$ref": "#/$defs/ordinaryRole" },
14
14
  "uiDesign": { "$ref": "#/$defs/modelRef" }
15
15
  },
16
16
  "$defs": {
@@ -22,6 +22,9 @@
22
22
  "model": { "type": "string", "minLength": 1 }
23
23
  },
24
24
  "additionalProperties": false
25
+ },
26
+ "ordinaryRole": {
27
+ "anyOf": [{ "$ref": "#/$defs/modelRef" }, { "type": "null" }]
25
28
  }
26
29
  },
27
30
  "additionalProperties": false
package/src/config.ts CHANGED
@@ -2,12 +2,16 @@ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import {
5
+ CURRENT_SCHEMA_VERSION,
6
+ INTENSITIES,
5
7
  emptyGlobalDefaults,
6
8
  emptySessionState,
7
9
  type EffectiveDelegateState,
8
10
  type GlobalDefaults,
9
11
  type Intensity,
10
12
  type ModelRef,
13
+ type ModelRole,
14
+ type OrdinaryRoleSetting,
11
15
  type Preference,
12
16
  type SessionDelegateState,
13
17
  type ValueSource,
@@ -19,11 +23,23 @@ export const SESSION_ENTRY_TYPE = "pi-delegation-policy:session";
19
23
  export const GLOBAL_CONFIG_NAME = "delegation-policy.json";
20
24
 
21
25
  const LEGACY_SCHEMA_MESSAGE =
22
- "Global defaults use schema version 1. Configure schema version 2 with /delegate before activating delegation.";
26
+ "Global defaults use schema version 1. Configure schema version 3 with /delegate before activating delegation.";
23
27
  const INVALID_CONFIG_MESSAGE = "Global defaults are invalid. Configure them again with /delegate.";
28
+ const INVALID_SESSION_MESSAGE =
29
+ "The latest delegation session state is invalid or unsupported. Delegation is off for safety.";
30
+ const CONFIG_KEYS = [
31
+ "schemaVersion",
32
+ "intensity",
33
+ "preference",
34
+ "small",
35
+ "medium",
36
+ "large",
37
+ "uiDesign",
38
+ ] as const;
24
39
 
25
40
  export type ConfigDiagnostic = {
26
41
  message: string;
42
+ reportWhenOff?: boolean;
27
43
  };
28
44
 
29
45
  export type LoadedDefaults = {
@@ -31,6 +47,26 @@ export type LoadedDefaults = {
31
47
  diagnostics: ConfigDiagnostic[];
32
48
  };
33
49
 
50
+ export type RestoredSessionState = {
51
+ session: SessionDelegateState;
52
+ diagnostics: ConfigDiagnostic[];
53
+ };
54
+
55
+ export type SessionEntryWriter = { appendEntry: (type: string, data?: unknown) => void };
56
+ export type GuardedAppendResult = "success" | "guard-failed" | "state-failed";
57
+
58
+ type Schema2GlobalDefaults = {
59
+ schemaVersion: 2;
60
+ intensity?: LegacyIntensity;
61
+ preference?: Preference;
62
+ small?: ModelRef;
63
+ medium?: ModelRef;
64
+ large?: ModelRef;
65
+ uiDesign?: ModelRef;
66
+ };
67
+
68
+ type Schema2SessionState = Omit<Schema2GlobalDefaults, "uiDesign"> & { uiDesign?: ModelRef | null };
69
+
34
70
  function isRecord(value: unknown): value is Record<string, unknown> {
35
71
  return typeof value === "object" && value !== null && !Array.isArray(value);
36
72
  }
@@ -40,7 +76,14 @@ function hasOnlyKeys(value: Record<string, unknown>, allowedKeys: readonly strin
40
76
  }
41
77
 
42
78
  function isIntensity(value: unknown): value is Intensity {
43
- return value === "off" || value === "normal" || value === "aggressive";
79
+ return INTENSITIES.some((intensity) => intensity === value);
80
+ }
81
+
82
+ const LEGACY_INTENSITIES = ["off", "normal", "aggressive"] as const;
83
+ type LegacyIntensity = (typeof LEGACY_INTENSITIES)[number];
84
+
85
+ function isLegacyIntensity(value: unknown): value is LegacyIntensity {
86
+ return LEGACY_INTENSITIES.some((intensity) => intensity === value);
44
87
  }
45
88
 
46
89
  function isPreference(value: unknown): value is Preference {
@@ -60,33 +103,31 @@ function parseModelRef(value: unknown): ModelRef | undefined {
60
103
  return { provider: value.provider, model: value.model };
61
104
  }
62
105
 
63
- function copyModelRef(value: ModelRef | undefined): ModelRef | undefined {
64
- return value ? { ...value } : undefined;
106
+ function copyRoleSetting(value: OrdinaryRoleSetting | undefined): OrdinaryRoleSetting | undefined {
107
+ return value === null ? null : value ? { ...value } : undefined;
65
108
  }
66
109
 
67
- export function parseConfig(value: unknown): GlobalDefaults | undefined {
68
- if (
69
- !isRecord(value) ||
70
- !hasOnlyKeys(value, [
71
- "schemaVersion",
72
- "intensity",
73
- "preference",
74
- "small",
75
- "medium",
76
- "large",
77
- "uiDesign",
78
- ]) ||
79
- value.schemaVersion !== 2 ||
80
- (value.intensity !== undefined && !isIntensity(value.intensity)) ||
81
- (value.preference !== undefined && !isPreference(value.preference))
82
- )
83
- return undefined;
110
+ function hasValidEnvelope(
111
+ value: unknown,
112
+ schemaVersion: 2 | 3,
113
+ intensityValidator: (value: unknown) => boolean = isIntensity,
114
+ ): value is Record<string, unknown> {
115
+ return (
116
+ isRecord(value) &&
117
+ hasOnlyKeys(value, CONFIG_KEYS) &&
118
+ value.schemaVersion === schemaVersion &&
119
+ (value.intensity === undefined || intensityValidator(value.intensity)) &&
120
+ (value.preference === undefined || isPreference(value.preference))
121
+ );
122
+ }
84
123
 
124
+ function parseSchema2Roles(
125
+ value: Record<string, unknown>,
126
+ ): Pick<Schema2GlobalDefaults, "small" | "medium" | "large" | "uiDesign"> | undefined {
85
127
  const small = value.small === undefined ? undefined : parseModelRef(value.small);
86
128
  const medium = value.medium === undefined ? undefined : parseModelRef(value.medium);
87
129
  const large = value.large === undefined ? undefined : parseModelRef(value.large);
88
130
  const uiDesign = value.uiDesign === undefined ? undefined : parseModelRef(value.uiDesign);
89
-
90
131
  if (
91
132
  (value.small !== undefined && !small) ||
92
133
  (value.medium !== undefined && !medium) ||
@@ -94,11 +135,7 @@ export function parseConfig(value: unknown): GlobalDefaults | undefined {
94
135
  (value.uiDesign !== undefined && !uiDesign)
95
136
  )
96
137
  return undefined;
97
-
98
138
  return {
99
- schemaVersion: 2,
100
- ...(value.intensity ? { intensity: value.intensity } : {}),
101
- ...(value.preference ? { preference: value.preference } : {}),
102
139
  ...(small ? { small } : {}),
103
140
  ...(medium ? { medium } : {}),
104
141
  ...(large ? { large } : {}),
@@ -106,51 +143,147 @@ export function parseConfig(value: unknown): GlobalDefaults | undefined {
106
143
  };
107
144
  }
108
145
 
109
- export function parseSessionState(value: unknown): SessionDelegateState | undefined {
146
+ export function parseSchema2Config(value: unknown): Schema2GlobalDefaults | undefined {
147
+ if (!hasValidEnvelope(value, 2, isLegacyIntensity)) return undefined;
148
+ const intensity = value.intensity;
149
+ if (intensity !== undefined && !isLegacyIntensity(intensity)) return undefined;
150
+ const roles = parseSchema2Roles(value);
151
+ if (!roles) return undefined;
152
+ return {
153
+ schemaVersion: 2,
154
+ ...(intensity ? { intensity } : {}),
155
+ ...(value.preference ? { preference: value.preference as Preference } : {}),
156
+ ...roles,
157
+ };
158
+ }
159
+
160
+ export function parseSchema3Config(value: unknown): GlobalDefaults | undefined {
161
+ if (!hasValidEnvelope(value, CURRENT_SCHEMA_VERSION)) return undefined;
162
+ const intensity = value.intensity;
163
+ if (intensity !== undefined && !isIntensity(intensity)) return undefined;
164
+ const parseOrdinary = (setting: unknown): OrdinaryRoleSetting | undefined =>
165
+ setting === null ? null : parseModelRef(setting);
166
+ const small = value.small === undefined ? undefined : parseOrdinary(value.small);
167
+ const medium = value.medium === undefined ? undefined : parseOrdinary(value.medium);
168
+ const large = value.large === undefined ? undefined : parseOrdinary(value.large);
169
+ const uiDesign = value.uiDesign === undefined ? undefined : parseModelRef(value.uiDesign);
110
170
  if (
111
- !isRecord(value) ||
112
- !hasOnlyKeys(value, [
113
- "schemaVersion",
114
- "intensity",
115
- "preference",
116
- "small",
117
- "medium",
118
- "large",
119
- "uiDesign",
120
- ]) ||
121
- value.schemaVersion !== 2 ||
122
- (value.intensity !== undefined && !isIntensity(value.intensity)) ||
123
- (value.preference !== undefined && !isPreference(value.preference))
171
+ (value.small !== undefined && small === undefined) ||
172
+ (value.medium !== undefined && medium === undefined) ||
173
+ (value.large !== undefined && large === undefined) ||
174
+ (value.uiDesign !== undefined && !uiDesign)
124
175
  )
125
176
  return undefined;
177
+ return {
178
+ schemaVersion: CURRENT_SCHEMA_VERSION,
179
+ ...(intensity ? { intensity } : {}),
180
+ ...(value.preference ? { preference: value.preference as Preference } : {}),
181
+ ...(small !== undefined ? { small } : {}),
182
+ ...(medium !== undefined ? { medium } : {}),
183
+ ...(large !== undefined ? { large } : {}),
184
+ ...(uiDesign ? { uiDesign } : {}),
185
+ };
186
+ }
126
187
 
127
- const small = value.small === undefined ? undefined : parseModelRef(value.small);
128
- const medium = value.medium === undefined ? undefined : parseModelRef(value.medium);
129
- const large = value.large === undefined ? undefined : parseModelRef(value.large);
188
+ function migrateSchema2Config(value: Schema2GlobalDefaults): GlobalDefaults {
189
+ return {
190
+ schemaVersion: CURRENT_SCHEMA_VERSION,
191
+ ...(value.intensity ? { intensity: value.intensity } : {}),
192
+ ...(value.preference ? { preference: value.preference as Preference } : {}),
193
+ ...(value.small ? { small: { ...value.small } } : {}),
194
+ ...(value.medium ? { medium: { ...value.medium } } : {}),
195
+ ...(value.large ? { large: { ...value.large } } : {}),
196
+ ...(value.uiDesign ? { uiDesign: { ...value.uiDesign } } : {}),
197
+ };
198
+ }
199
+
200
+ export function parseConfig(value: unknown): GlobalDefaults | undefined {
201
+ return (
202
+ parseSchema3Config(value) ??
203
+ (() => {
204
+ const schema2 = parseSchema2Config(value);
205
+ return schema2 ? migrateSchema2Config(schema2) : undefined;
206
+ })()
207
+ );
208
+ }
209
+
210
+ function parseSchema2SessionState(value: unknown): Schema2SessionState | undefined {
211
+ if (!hasValidEnvelope(value, 2, isLegacyIntensity)) return undefined;
212
+ const intensity = value.intensity;
213
+ if (intensity !== undefined && !isLegacyIntensity(intensity)) return undefined;
130
214
  const uiDesign =
131
215
  value.uiDesign === undefined || value.uiDesign === null
132
216
  ? value.uiDesign
133
217
  : parseModelRef(value.uiDesign);
218
+ const roles = parseSchema2Roles({ ...value, uiDesign: undefined });
219
+ if (!roles || (value.uiDesign !== undefined && value.uiDesign !== null && !uiDesign))
220
+ return undefined;
221
+ return {
222
+ schemaVersion: 2,
223
+ ...(intensity ? { intensity } : {}),
224
+ ...(value.preference ? { preference: value.preference as Preference } : {}),
225
+ ...roles,
226
+ ...(uiDesign === null ? { uiDesign: null } : uiDesign ? { uiDesign } : {}),
227
+ };
228
+ }
134
229
 
230
+ function parseSchema3SessionState(value: unknown): SessionDelegateState | undefined {
231
+ if (!hasValidEnvelope(value, CURRENT_SCHEMA_VERSION)) return undefined;
232
+ const intensity = value.intensity;
233
+ if (intensity !== undefined && !isIntensity(intensity)) return undefined;
234
+ const parseOrdinary = (setting: unknown): OrdinaryRoleSetting | undefined =>
235
+ setting === null ? null : parseModelRef(setting);
236
+ const small = value.small === undefined ? undefined : parseOrdinary(value.small);
237
+ const medium = value.medium === undefined ? undefined : parseOrdinary(value.medium);
238
+ const large = value.large === undefined ? undefined : parseOrdinary(value.large);
239
+ const uiDesign =
240
+ value.uiDesign === undefined || value.uiDesign === null
241
+ ? value.uiDesign
242
+ : parseModelRef(value.uiDesign);
135
243
  if (
136
- (value.small !== undefined && !small) ||
137
- (value.medium !== undefined && !medium) ||
138
- (value.large !== undefined && !large) ||
244
+ (value.small !== undefined && small === undefined) ||
245
+ (value.medium !== undefined && medium === undefined) ||
246
+ (value.large !== undefined && large === undefined) ||
139
247
  (value.uiDesign !== undefined && value.uiDesign !== null && !uiDesign)
140
248
  )
141
249
  return undefined;
250
+ return {
251
+ schemaVersion: CURRENT_SCHEMA_VERSION,
252
+ ...(intensity ? { intensity } : {}),
253
+ ...(value.preference ? { preference: value.preference as Preference } : {}),
254
+ ...(small !== undefined ? { small } : {}),
255
+ ...(medium !== undefined ? { medium } : {}),
256
+ ...(large !== undefined ? { large } : {}),
257
+ ...(uiDesign === null ? { uiDesign: null } : uiDesign ? { uiDesign } : {}),
258
+ };
259
+ }
142
260
 
261
+ function migrateSchema2SessionState(value: Schema2SessionState): SessionDelegateState {
143
262
  return {
144
- schemaVersion: 2,
263
+ schemaVersion: CURRENT_SCHEMA_VERSION,
145
264
  ...(value.intensity ? { intensity: value.intensity } : {}),
146
- ...(value.preference ? { preference: value.preference } : {}),
147
- ...(small ? { small } : {}),
148
- ...(medium ? { medium } : {}),
149
- ...(large ? { large } : {}),
150
- ...(uiDesign === null ? { uiDesign: null } : uiDesign ? { uiDesign } : {}),
265
+ ...(value.preference ? { preference: value.preference as Preference } : {}),
266
+ ...(value.small ? { small: { ...value.small } } : {}),
267
+ ...(value.medium ? { medium: { ...value.medium } } : {}),
268
+ ...(value.large ? { large: { ...value.large } } : {}),
269
+ ...(value.uiDesign === null
270
+ ? { uiDesign: null }
271
+ : value.uiDesign
272
+ ? { uiDesign: { ...value.uiDesign } }
273
+ : {}),
151
274
  };
152
275
  }
153
276
 
277
+ export function parseSessionState(value: unknown): SessionDelegateState | undefined {
278
+ return (
279
+ parseSchema3SessionState(value) ??
280
+ (() => {
281
+ const schema2 = parseSchema2SessionState(value);
282
+ return schema2 ? migrateSchema2SessionState(schema2) : undefined;
283
+ })()
284
+ );
285
+ }
286
+
154
287
  function isLegacyConfig(value: unknown): boolean {
155
288
  return isRecord(value) && value.schemaVersion === 1;
156
289
  }
@@ -195,64 +328,111 @@ async function atomicWrite(path: string, value: unknown): Promise<void> {
195
328
  }
196
329
 
197
330
  export async function writeConfig(path: string, defaults: GlobalDefaults): Promise<void> {
198
- const parsed = parseConfig(defaults);
331
+ const parsed = parseSchema3Config(defaults);
199
332
  if (!parsed) throw new Error("Refusing to write invalid delegation policy defaults.");
200
333
  await atomicWrite(path, parsed);
201
334
  }
202
335
 
203
- export function restoreSessionState(entries: unknown[]): SessionDelegateState {
336
+ export function restoreSessionStateWithDiagnostics(entries: unknown[]): RestoredSessionState {
204
337
  for (let index = entries.length - 1; index >= 0; index -= 1) {
205
338
  const entry = entries[index] as Record<string, unknown> | undefined;
206
339
  if (entry?.type !== "custom" || entry.customType !== SESSION_ENTRY_TYPE) continue;
207
- const state = parseSessionState(entry.data);
208
- if (state) return state;
340
+ const session = parseSessionState(entry.data);
341
+ if (session) return { session, diagnostics: [] };
342
+ return {
343
+ session: { schemaVersion: CURRENT_SCHEMA_VERSION, intensity: "off" },
344
+ diagnostics: [{ message: INVALID_SESSION_MESSAGE, reportWhenOff: true }],
345
+ };
209
346
  }
210
- return emptySessionState();
347
+ return { session: emptySessionState(), diagnostics: [] };
211
348
  }
212
349
 
213
- function sourceFor<T>(sessionValue: T | undefined, globalValue: T | undefined): ValueSource {
214
- if (sessionValue !== undefined) return "session";
350
+ export function restoreSessionState(entries: unknown[]): SessionDelegateState {
351
+ return restoreSessionStateWithDiagnostics(entries).session;
352
+ }
353
+
354
+ function sourceFor<T>(
355
+ session: Record<string, unknown>,
356
+ globalValue: T | undefined,
357
+ key: string,
358
+ ): ValueSource {
359
+ if (Object.hasOwn(session, key)) return "session";
215
360
  if (globalValue !== undefined) return "global";
216
361
  return "default";
217
362
  }
218
363
 
364
+ function resolveRole(
365
+ globalValue: OrdinaryRoleSetting | undefined,
366
+ session: SessionDelegateState,
367
+ role: ModelRole,
368
+ ): OrdinaryRoleSetting | undefined {
369
+ return copyRoleSetting(Object.hasOwn(session, role) ? session[role] : globalValue);
370
+ }
371
+
219
372
  export function resolveDelegateState(
220
373
  defaults: GlobalDefaults,
221
374
  session: SessionDelegateState,
222
375
  ): EffectiveDelegateState {
223
376
  const uiDesign =
224
377
  session.uiDesign === undefined
225
- ? copyModelRef(defaults.uiDesign)
378
+ ? defaults.uiDesign
379
+ ? { ...defaults.uiDesign }
380
+ : undefined
226
381
  : session.uiDesign === null
227
382
  ? undefined
228
- : copyModelRef(session.uiDesign);
383
+ : { ...session.uiDesign };
384
+ const small = resolveRole(defaults.small, session, "small");
385
+ const medium = resolveRole(defaults.medium, session, "medium");
386
+ const large = resolveRole(defaults.large, session, "large");
229
387
 
230
388
  return {
231
389
  intensity: session.intensity ?? defaults.intensity ?? "off",
232
390
  preference: session.preference ?? defaults.preference ?? "standard",
233
- small: copyModelRef(session.small ?? defaults.small),
234
- medium: copyModelRef(session.medium ?? defaults.medium),
235
- large: copyModelRef(session.large ?? defaults.large),
391
+ ...(small !== undefined ? { small } : {}),
392
+ ...(medium !== undefined ? { medium } : {}),
393
+ ...(large !== undefined ? { large } : {}),
236
394
  ...(uiDesign ? { uiDesign } : {}),
237
395
  source: {
238
- intensity: sourceFor(session.intensity, defaults.intensity),
239
- preference: sourceFor(session.preference, defaults.preference),
240
- small: sourceFor(session.small, defaults.small),
241
- medium: sourceFor(session.medium, defaults.medium),
242
- large: sourceFor(session.large, defaults.large),
243
- uiDesign: sourceFor(session.uiDesign, defaults.uiDesign),
396
+ intensity: sourceFor(session, defaults.intensity, "intensity"),
397
+ preference: sourceFor(session, defaults.preference, "preference"),
398
+ small: sourceFor(session, defaults.small, "small"),
399
+ medium: sourceFor(session, defaults.medium, "medium"),
400
+ large: sourceFor(session, defaults.large, "large"),
401
+ uiDesign: sourceFor(session, defaults.uiDesign, "uiDesign"),
244
402
  },
245
403
  };
246
404
  }
247
405
 
248
406
  export function defaultsFromEffectiveState(state: EffectiveDelegateState): GlobalDefaults {
407
+ const role = (setting: OrdinaryRoleSetting | undefined) =>
408
+ setting === undefined ? {} : setting === null ? { value: null } : { value: { ...setting } };
409
+ const small = role(state.small);
410
+ const medium = role(state.medium);
411
+ const large = role(state.large);
249
412
  return {
250
- schemaVersion: 2,
413
+ schemaVersion: CURRENT_SCHEMA_VERSION,
251
414
  intensity: state.intensity,
252
415
  preference: state.preference,
253
- ...(state.small ? { small: { ...state.small } } : {}),
254
- ...(state.medium ? { medium: { ...state.medium } } : {}),
255
- ...(state.large ? { large: { ...state.large } } : {}),
416
+ ...("value" in small ? { small: small.value } : {}),
417
+ ...("value" in medium ? { medium: medium.value } : {}),
418
+ ...("value" in large ? { large: large.value } : {}),
256
419
  ...(state.uiDesign ? { uiDesign: { ...state.uiDesign } } : {}),
257
420
  };
258
421
  }
422
+
423
+ export function appendGuardedSessionState(
424
+ pi: SessionEntryWriter,
425
+ session: SessionDelegateState,
426
+ ): GuardedAppendResult {
427
+ try {
428
+ pi.appendEntry(SESSION_ENTRY_TYPE, { schemaVersion: 2, intensity: "off" });
429
+ } catch {
430
+ return "guard-failed";
431
+ }
432
+ try {
433
+ pi.appendEntry(SESSION_ENTRY_TYPE, session);
434
+ return "success";
435
+ } catch {
436
+ return "state-failed";
437
+ }
438
+ }