@remnic/core 9.3.763 → 9.3.765

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/access-admin-ops-surface.d.ts +2 -2
  2. package/dist/access-admin-ops-surface.js +1 -1
  3. package/dist/access-boundary.d.ts +2 -2
  4. package/dist/access-boundary.js +2 -2
  5. package/dist/access-cli.js +11 -11
  6. package/dist/access-http.d.ts +2 -2
  7. package/dist/access-http.js +6 -6
  8. package/dist/access-identity-continuity-surface.d.ts +1 -1
  9. package/dist/access-identity-continuity-surface.js +1 -1
  10. package/dist/access-lcm-surface.d.ts +2 -2
  11. package/dist/access-lcm-surface.js +1 -1
  12. package/dist/access-mcp.d.ts +2 -2
  13. package/dist/access-mcp.js +5 -5
  14. package/dist/access-observe-write-surface.d.ts +2 -2
  15. package/dist/access-observe-write-surface.js +1 -1
  16. package/dist/access-operations-batch.js +3 -3
  17. package/dist/access-operations.d.ts +6 -6
  18. package/dist/access-operations.js +4 -4
  19. package/dist/access-recall-surface.d.ts +2 -2
  20. package/dist/access-recall-surface.js +1 -1
  21. package/dist/access-schema.d.ts +68 -68
  22. package/dist/{access-service-CaWJP_f6.d.ts → access-service-CAoJ4qN8.d.ts} +1 -1
  23. package/dist/access-service.d.ts +2 -2
  24. package/dist/access-service.js +1 -1
  25. package/dist/access-surface-catalog.d.ts +2 -2
  26. package/dist/active-recall.js +1 -1
  27. package/dist/bootstrap.d.ts +1 -1
  28. package/dist/{chunk-LDCFFJ67.js → chunk-422OELQ3.js} +4 -4
  29. package/dist/{chunk-A4FMQIST.js → chunk-5JF4DIKZ.js} +2 -2
  30. package/dist/{chunk-K4EDULCP.js → chunk-CGLRKKMF.js} +3 -3
  31. package/dist/{chunk-6ZFE2TPO.js → chunk-P6ZZ4GX4.js} +3 -3
  32. package/dist/{chunk-6YEENRE3.js → chunk-RJCQPRLB.js} +3 -3
  33. package/dist/{chunk-D6YTILJO.js → chunk-SKT66QI5.js} +7 -7
  34. package/dist/{chunk-2HAG7JOI.js → chunk-T4AWNIXJ.js} +48 -58
  35. package/dist/chunk-T4AWNIXJ.js.map +1 -0
  36. package/dist/{chunk-N3QDLE2N.js → chunk-TOF25WKK.js} +2 -2
  37. package/dist/{chunk-BAF7MBEH.js → chunk-TUXRWLIQ.js} +15 -45
  38. package/dist/chunk-TUXRWLIQ.js.map +1 -0
  39. package/dist/{chunk-TIOXQPD7.js → chunk-UQEE4H6Z.js} +8 -8
  40. package/dist/chunk-UQEE4H6Z.js.map +1 -0
  41. package/dist/{chunk-4XL3CVII.js → chunk-WRBDUNSW.js} +2 -2
  42. package/dist/{cli-CvWpIbMD.d.ts → cli-WRmxvfc_.d.ts} +2 -2
  43. package/dist/cli.d.ts +3 -3
  44. package/dist/cli.js +10 -10
  45. package/dist/config.js +1 -1
  46. package/dist/explicit-capture.d.ts +1 -1
  47. package/dist/index.d.ts +4 -4
  48. package/dist/index.js +11 -11
  49. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  50. package/dist/operator-toolkit.js +2 -2
  51. package/dist/{orchestrator-C04Q4Zao.d.ts → orchestrator-Y9wyG4jK.d.ts} +102 -15
  52. package/dist/orchestrator.d.ts +1 -1
  53. package/dist/orchestrator.js +11 -11
  54. package/dist/resume-bundles.js +2 -2
  55. package/dist/schemas.d.ts +84 -84
  56. package/dist/shared-context/manager.d.ts +8 -8
  57. package/dist/transfer/types.d.ts +66 -66
  58. package/package.json +2 -2
  59. package/src/access-service.ts +7 -7
  60. package/src/config.ts +5 -261
  61. package/src/namespaces/scope-profile-config.test.ts +60 -0
  62. package/src/namespaces/scope-profile-config.ts +243 -0
  63. package/src/orchestration/recall-introspection.ts +3 -12
  64. package/src/orchestrator.ts +9 -57
  65. package/dist/chunk-2HAG7JOI.js.map +0 -1
  66. package/dist/chunk-BAF7MBEH.js.map +0 -1
  67. package/dist/chunk-TIOXQPD7.js.map +0 -1
  68. /package/dist/{chunk-LDCFFJ67.js.map → chunk-422OELQ3.js.map} +0 -0
  69. /package/dist/{chunk-A4FMQIST.js.map → chunk-5JF4DIKZ.js.map} +0 -0
  70. /package/dist/{chunk-K4EDULCP.js.map → chunk-CGLRKKMF.js.map} +0 -0
  71. /package/dist/{chunk-6ZFE2TPO.js.map → chunk-P6ZZ4GX4.js.map} +0 -0
  72. /package/dist/{chunk-6YEENRE3.js.map → chunk-RJCQPRLB.js.map} +0 -0
  73. /package/dist/{chunk-D6YTILJO.js.map → chunk-SKT66QI5.js.map} +0 -0
  74. /package/dist/{chunk-N3QDLE2N.js.map → chunk-TOF25WKK.js.map} +0 -0
  75. /package/dist/{chunk-4XL3CVII.js.map → chunk-WRBDUNSW.js.map} +0 -0
package/src/config.ts CHANGED
@@ -19,10 +19,6 @@ import type {
19
19
  RecallPipelineConfig,
20
20
  RecallSectionConfig,
21
21
  ReasoningEffort,
22
- ScopeProfileConfig,
23
- ScopeProfileLayerId,
24
- ScopeProfilePromotionTarget,
25
- ScopeTeamConfig,
26
22
  SemanticChunkingConfigShape,
27
23
  SessionObserverBandConfig,
28
24
  SlotBehaviorConfig,
@@ -51,6 +47,11 @@ import { parseCodingKnowledgeConfig } from "./coding/coding-knowledge-config.js"
51
47
  import { parseChatConfig } from "./chat/chat-config.js";
52
48
  import { parseCorrectionIntentConfig, parseFaithfulnessGateConfig } from "./faithfulness-config.js";
53
49
  import { parseOfflineSyncExcludes } from "./offline-sync.js";
50
+ import {
51
+ parseScopeProfiles,
52
+ parseScopeTeams,
53
+ validateScopeProfileTeamReferences,
54
+ } from "./namespaces/scope-profile-config.js";
54
55
  const DEFAULT_MEMORY_DIR = path.join(
55
56
  resolveHomeDir(),
56
57
  ".openclaw",
@@ -73,263 +74,6 @@ const LEGACY_ACTIVE_RECALL_CUSTOM_FIELD = [
73
74
  "Prompt",
74
75
  "Override",
75
76
  ].join("") as "activeRecallPromptOverride";
76
- const SCOPE_PROFILE_LAYER_IDS = [
77
- "userProject",
78
- "teamProject",
79
- "userGlobal",
80
- "serverShared",
81
- ] as const satisfies readonly ScopeProfileLayerId[];
82
- const SCOPE_PROFILE_PROMOTION_TARGETS = [
83
- ...SCOPE_PROFILE_LAYER_IDS,
84
- ] as const satisfies readonly ScopeProfilePromotionTarget[];
85
- const SCOPE_PROFILE_AUTO_PROMOTE_CATEGORIES = [
86
- "fact",
87
- "correction",
88
- "decision",
89
- "preference",
90
- "rule",
91
- "procedure",
92
- ] as const;
93
- const CONFIDENCE_TIERS = [
94
- "explicit",
95
- "implied",
96
- "inferred",
97
- "speculative",
98
- ] as const;
99
-
100
- function isRecord(value: unknown): value is Record<string, unknown> {
101
- return typeof value === "object" && value !== null && !Array.isArray(value);
102
- }
103
-
104
- function parseStringList(value: unknown, keyName: string): string[] {
105
- if (value === undefined || value === null) return [];
106
- if (!Array.isArray(value)) {
107
- throw new Error(keyName + " must be an array");
108
- }
109
- const out: string[] = [];
110
- for (const entry of value) {
111
- if (typeof entry !== "string" || entry.length === 0) {
112
- throw new Error(keyName + " must contain only non-empty strings");
113
- }
114
- out.push(entry);
115
- }
116
- return out;
117
- }
118
-
119
- function parseScopeProfileLayerList(
120
- value: unknown,
121
- keyName: string,
122
- fallback: ScopeProfileLayerId[],
123
- ): ScopeProfileLayerId[] {
124
- if (value === undefined || value === null) return [...fallback];
125
- if (!Array.isArray(value)) {
126
- throw new Error(`${keyName} must be an array`);
127
- }
128
- const out: ScopeProfileLayerId[] = [];
129
- for (const entry of value) {
130
- if (!SCOPE_PROFILE_LAYER_IDS.includes(entry as ScopeProfileLayerId)) {
131
- throw new Error(`${keyName} contains unsupported layer: ${String(entry)}`);
132
- }
133
- if (!out.includes(entry as ScopeProfileLayerId)) {
134
- out.push(entry as ScopeProfileLayerId);
135
- }
136
- }
137
- return out;
138
- }
139
-
140
- function parseScopeProfilePromotionTargets(
141
- value: unknown,
142
- keyName: string,
143
- ): ScopeProfilePromotionTarget[] {
144
- if (value === undefined || value === null) return [];
145
- if (!Array.isArray(value)) {
146
- throw new Error(`${keyName} must be an array`);
147
- }
148
- const out: ScopeProfilePromotionTarget[] = [];
149
- for (const entry of value) {
150
- if (!SCOPE_PROFILE_PROMOTION_TARGETS.includes(entry as ScopeProfilePromotionTarget)) {
151
- throw new Error(`${keyName} contains unsupported target: ${String(entry)}`);
152
- }
153
- if (!out.includes(entry as ScopeProfilePromotionTarget)) {
154
- out.push(entry as ScopeProfilePromotionTarget);
155
- }
156
- }
157
- return out;
158
- }
159
-
160
- function parseScopeProfiles(value: unknown): Record<string, ScopeProfileConfig> {
161
- if (value === undefined || value === null) return {};
162
- if (!isRecord(value)) {
163
- throw new Error("scopeProfiles must be an object");
164
- }
165
- const profiles: Record<string, ScopeProfileConfig> = {};
166
- for (const [profileId, rawProfile] of Object.entries(value)) {
167
- if (profileId.trim().length === 0) {
168
- throw new Error("scopeProfiles keys must not be empty");
169
- }
170
- if (!isRecord(rawProfile)) {
171
- throw new Error(`scopeProfiles.${profileId} must be an object`);
172
- }
173
- const readOrder = parseScopeProfileLayerList(
174
- rawProfile.readOrder,
175
- `scopeProfiles.${profileId}.readOrder`,
176
- ["userProject", "userGlobal", "serverShared"],
177
- );
178
- const writeDefault =
179
- rawProfile.writeDefault === undefined || rawProfile.writeDefault === null
180
- ? "userProject"
181
- : rawProfile.writeDefault;
182
- if (!SCOPE_PROFILE_LAYER_IDS.includes(writeDefault as ScopeProfileLayerId)) {
183
- throw new Error(`scopeProfiles.${profileId}.writeDefault contains unsupported layer: ${String(writeDefault)}`);
184
- }
185
- if (
186
- rawProfile.teamProject !== undefined &&
187
- rawProfile.teamProject !== null &&
188
- !isRecord(rawProfile.teamProject)
189
- ) {
190
- throw new Error("scopeProfiles." + profileId + ".teamProject must be an object");
191
- }
192
- const teamProject = (() => {
193
- if (!isRecord(rawProfile.teamProject)) return undefined;
194
- const out: NonNullable<ScopeProfileConfig["teamProject"]> = {};
195
- if (rawProfile.teamProject.namespaceTemplate !== undefined) {
196
- if (
197
- typeof rawProfile.teamProject.namespaceTemplate !== "string" ||
198
- rawProfile.teamProject.namespaceTemplate.length === 0
199
- ) {
200
- throw new Error(
201
- `scopeProfiles.${profileId}.teamProject.namespaceTemplate must be a non-empty string`,
202
- );
203
- }
204
- out.namespaceTemplate = rawProfile.teamProject.namespaceTemplate;
205
- }
206
- if (rawProfile.teamProject.teamId !== undefined) {
207
- if (
208
- typeof rawProfile.teamProject.teamId !== "string" ||
209
- rawProfile.teamProject.teamId.length === 0
210
- ) {
211
- throw new Error(
212
- `scopeProfiles.${profileId}.teamProject.teamId must be a non-empty string`,
213
- );
214
- }
215
- out.teamId = rawProfile.teamProject.teamId;
216
- }
217
- return out;
218
- })();
219
- if (
220
- rawProfile.autoPromote !== undefined &&
221
- rawProfile.autoPromote !== null &&
222
- !isRecord(rawProfile.autoPromote)
223
- ) {
224
- throw new Error(`scopeProfiles.${profileId}.autoPromote must be an object`);
225
- }
226
- const rawAutoPromote = isRecord(rawProfile.autoPromote) ? rawProfile.autoPromote : {};
227
- const hasAutoPromoteEnabled = Object.prototype.hasOwnProperty.call(
228
- rawAutoPromote,
229
- "enabled",
230
- );
231
- const autoPromoteEnabled = coerceBool(rawAutoPromote.enabled);
232
- if (hasAutoPromoteEnabled && autoPromoteEnabled === undefined) {
233
- throw new Error(
234
- `scopeProfiles.${profileId}.autoPromote.enabled must be a boolean or boolean-like string`,
235
- );
236
- }
237
- const minConfidenceTier = (() => {
238
- const rawTier = rawAutoPromote.minConfidenceTier;
239
- if (rawTier === undefined || rawTier === null) return "explicit";
240
- if (typeof rawTier !== "string" || !CONFIDENCE_TIERS.includes(rawTier as any)) {
241
- throw new Error(
242
- `scopeProfiles.${profileId}.autoPromote.minConfidenceTier must be one of: ${CONFIDENCE_TIERS.join(", ")}`,
243
- );
244
- }
245
- return rawTier as typeof CONFIDENCE_TIERS[number];
246
- })();
247
- const autoPromoteCategories: Array<typeof SCOPE_PROFILE_AUTO_PROMOTE_CATEGORIES[number]> = (() => {
248
- if (rawAutoPromote.categories === undefined || rawAutoPromote.categories === null) {
249
- return ["fact", "correction", "decision", "preference"];
250
- }
251
- if (!Array.isArray(rawAutoPromote.categories)) {
252
- throw new Error(
253
- `scopeProfiles.${profileId}.autoPromote.categories must be an array`,
254
- );
255
- }
256
- const categories: Array<typeof SCOPE_PROFILE_AUTO_PROMOTE_CATEGORIES[number]> = [];
257
- for (const entry of rawAutoPromote.categories) {
258
- if (typeof entry !== "string" || !SCOPE_PROFILE_AUTO_PROMOTE_CATEGORIES.includes(entry as any)) {
259
- throw new Error(
260
- "scopeProfiles." + profileId + ".autoPromote.categories must contain only: " +
261
- SCOPE_PROFILE_AUTO_PROMOTE_CATEGORIES.join(", "),
262
- );
263
- }
264
- categories.push(entry as typeof SCOPE_PROFILE_AUTO_PROMOTE_CATEGORIES[number]);
265
- }
266
- return categories;
267
- })();
268
- profiles[profileId] = {
269
- readOrder,
270
- writeDefault: writeDefault as ScopeProfileLayerId,
271
- promotionTargets: parseScopeProfilePromotionTargets(
272
- rawProfile.promotionTargets,
273
- `scopeProfiles.${profileId}.promotionTargets`,
274
- ),
275
- ...(teamProject && Object.keys(teamProject).length > 0 ? { teamProject } : {}),
276
- autoPromote: {
277
- enabled: autoPromoteEnabled === true,
278
- targets: parseScopeProfilePromotionTargets(
279
- rawAutoPromote.targets,
280
- `scopeProfiles.${profileId}.autoPromote.targets`,
281
- ),
282
- categories: autoPromoteCategories,
283
- minConfidenceTier,
284
- },
285
- };
286
- }
287
- return profiles;
288
- }
289
-
290
- function parseScopeTeams(value: unknown): Record<string, ScopeTeamConfig> {
291
- if (value === undefined || value === null) return {};
292
- if (!isRecord(value)) {
293
- throw new Error("teams must be an object");
294
- }
295
- const teams: Record<string, ScopeTeamConfig> = {};
296
- for (const [teamId, rawTeam] of Object.entries(value)) {
297
- if (teamId.trim().length === 0) {
298
- throw new Error("teams keys must not be empty");
299
- }
300
- if (!isRecord(rawTeam)) {
301
- throw new Error(`teams.${teamId} must be an object`);
302
- }
303
- const projectNamespaceTemplate = rawTeam.projectNamespaceTemplate;
304
- if (projectNamespaceTemplate !== undefined) {
305
- if (typeof projectNamespaceTemplate !== "string" || projectNamespaceTemplate.length === 0) {
306
- throw new Error(`teams.${teamId}.projectNamespaceTemplate must be a non-empty string`);
307
- }
308
- }
309
- teams[teamId] = {
310
- principals: parseStringList(rawTeam.principals, `teams.${teamId}.principals`),
311
- ...(projectNamespaceTemplate !== undefined ? { projectNamespaceTemplate } : {}),
312
- read: parseStringList(rawTeam.read, `teams.${teamId}.read`),
313
- write: parseStringList(rawTeam.write, `teams.${teamId}.write`),
314
- promote: parseStringList(rawTeam.promote, `teams.${teamId}.promote`),
315
- };
316
- }
317
- return teams;
318
- }
319
-
320
- function validateScopeProfileTeamReferences(
321
- profiles: Record<string, ScopeProfileConfig>,
322
- teams: Record<string, ScopeTeamConfig>,
323
- ): void {
324
- for (const [profileId, profile] of Object.entries(profiles)) {
325
- const teamId = profile.teamProject?.teamId;
326
- if (teamId && !Object.prototype.hasOwnProperty.call(teams, teamId)) {
327
- throw new Error(
328
- `scopeProfiles.${profileId}.teamProject.teamId references unknown team: ${teamId}`,
329
- );
330
- }
331
- }
332
- }
333
77
 
334
78
  function parseBoundedIntegerMs(
335
79
  value: unknown,
@@ -0,0 +1,60 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseScopeProfiles, parseScopeTeams, validateScopeProfileTeamReferences } from "./scope-profile-config.js";
5
+
6
+ test("scope profile config parser preserves defaults and boolean-string coercion", () => {
7
+ const profiles = parseScopeProfiles({
8
+ hosted: {
9
+ readOrder: ["userProject", "userProject", "userGlobal"],
10
+ autoPromote: { enabled: "false" },
11
+ },
12
+ });
13
+
14
+ assert.deepEqual(profiles.hosted, {
15
+ readOrder: ["userProject", "userGlobal"],
16
+ writeDefault: "userProject",
17
+ promotionTargets: [],
18
+ autoPromote: {
19
+ enabled: false,
20
+ targets: [],
21
+ categories: ["fact", "correction", "decision", "preference"],
22
+ minConfidenceTier: "explicit",
23
+ },
24
+ });
25
+ });
26
+
27
+ test("scope team parser preserves declared access lists", () => {
28
+ const teams = parseScopeTeams({
29
+ platform: {
30
+ principals: ["agent-a"],
31
+ read: ["agent-a", "agent-b"],
32
+ write: ["agent-a"],
33
+ promote: ["agent-a"],
34
+ projectNamespaceTemplate: "team-{teamId}-{projectHash}",
35
+ },
36
+ });
37
+
38
+ assert.deepEqual(teams.platform, {
39
+ principals: ["agent-a"],
40
+ projectNamespaceTemplate: "team-{teamId}-{projectHash}",
41
+ read: ["agent-a", "agent-b"],
42
+ write: ["agent-a"],
43
+ promote: ["agent-a"],
44
+ });
45
+ });
46
+
47
+ test("scope profile config validation rejects unknown team references", () => {
48
+ const profiles = parseScopeProfiles({
49
+ hosted: {
50
+ readOrder: ["teamProject"],
51
+ writeDefault: "teamProject",
52
+ teamProject: { teamId: "missing" },
53
+ },
54
+ });
55
+
56
+ assert.throws(
57
+ () => validateScopeProfileTeamReferences(profiles, {}),
58
+ /scopeProfiles\.hosted\.teamProject\.teamId references unknown team: missing/
59
+ );
60
+ });
@@ -0,0 +1,243 @@
1
+ import { coerceBool } from "../connectors/coerce.js";
2
+ import type {
3
+ ScopeProfileConfig,
4
+ ScopeProfileLayerId,
5
+ ScopeProfilePromotionTarget,
6
+ ScopeTeamConfig,
7
+ } from "../types.js";
8
+
9
+ const SCOPE_PROFILE_LAYER_IDS = [
10
+ "userProject",
11
+ "teamProject",
12
+ "userGlobal",
13
+ "serverShared",
14
+ ] as const satisfies readonly ScopeProfileLayerId[];
15
+ const SCOPE_PROFILE_PROMOTION_TARGETS = [
16
+ ...SCOPE_PROFILE_LAYER_IDS,
17
+ ] as const satisfies readonly ScopeProfilePromotionTarget[];
18
+ const SCOPE_PROFILE_AUTO_PROMOTE_CATEGORIES = [
19
+ "fact",
20
+ "correction",
21
+ "decision",
22
+ "preference",
23
+ "rule",
24
+ "procedure",
25
+ ] as const;
26
+ const CONFIDENCE_TIERS = ["explicit", "implied", "inferred", "speculative"] as const;
27
+
28
+ type ConfidenceTier = (typeof CONFIDENCE_TIERS)[number];
29
+ type AutoPromoteCategory = (typeof SCOPE_PROFILE_AUTO_PROMOTE_CATEGORIES)[number];
30
+
31
+ function isConfidenceTier(value: unknown): value is ConfidenceTier {
32
+ return typeof value === "string" && (CONFIDENCE_TIERS as readonly string[]).includes(value);
33
+ }
34
+
35
+ function isAutoPromoteCategory(value: unknown): value is AutoPromoteCategory {
36
+ return typeof value === "string" && (SCOPE_PROFILE_AUTO_PROMOTE_CATEGORIES as readonly string[]).includes(value);
37
+ }
38
+
39
+ function isRecord(value: unknown): value is Record<string, unknown> {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
42
+
43
+ function parseStringList(value: unknown, keyName: string): string[] {
44
+ if (value === undefined || value === null) return [];
45
+ if (!Array.isArray(value)) {
46
+ throw new Error(`${keyName} must be an array`);
47
+ }
48
+ const out: string[] = [];
49
+ for (const entry of value) {
50
+ if (typeof entry !== "string" || entry.length === 0) {
51
+ throw new Error(`${keyName} must contain only non-empty strings`);
52
+ }
53
+ out.push(entry);
54
+ }
55
+ return out;
56
+ }
57
+
58
+ function parseScopeProfileLayerList(
59
+ value: unknown,
60
+ keyName: string,
61
+ fallback: ScopeProfileLayerId[]
62
+ ): ScopeProfileLayerId[] {
63
+ if (value === undefined || value === null) return [...fallback];
64
+ if (!Array.isArray(value)) {
65
+ throw new Error(`${keyName} must be an array`);
66
+ }
67
+ const out: ScopeProfileLayerId[] = [];
68
+ for (const entry of value) {
69
+ if (!SCOPE_PROFILE_LAYER_IDS.includes(entry as ScopeProfileLayerId)) {
70
+ throw new Error(`${keyName} contains unsupported layer: ${String(entry)}`);
71
+ }
72
+ if (!out.includes(entry as ScopeProfileLayerId)) {
73
+ out.push(entry as ScopeProfileLayerId);
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+
79
+ function parseScopeProfilePromotionTargets(value: unknown, keyName: string): ScopeProfilePromotionTarget[] {
80
+ if (value === undefined || value === null) return [];
81
+ if (!Array.isArray(value)) {
82
+ throw new Error(`${keyName} must be an array`);
83
+ }
84
+ const out: ScopeProfilePromotionTarget[] = [];
85
+ for (const entry of value) {
86
+ if (!SCOPE_PROFILE_PROMOTION_TARGETS.includes(entry as ScopeProfilePromotionTarget)) {
87
+ throw new Error(`${keyName} contains unsupported target: ${String(entry)}`);
88
+ }
89
+ if (!out.includes(entry as ScopeProfilePromotionTarget)) {
90
+ out.push(entry as ScopeProfilePromotionTarget);
91
+ }
92
+ }
93
+ return out;
94
+ }
95
+
96
+ export function parseScopeProfiles(value: unknown): Record<string, ScopeProfileConfig> {
97
+ if (value === undefined || value === null) return {};
98
+ if (!isRecord(value)) {
99
+ throw new Error("scopeProfiles must be an object");
100
+ }
101
+ const profiles: Record<string, ScopeProfileConfig> = {};
102
+ for (const [profileId, rawProfile] of Object.entries(value)) {
103
+ if (profileId.trim().length === 0) {
104
+ throw new Error("scopeProfiles keys must not be empty");
105
+ }
106
+ if (!isRecord(rawProfile)) {
107
+ throw new Error(`scopeProfiles.${profileId} must be an object`);
108
+ }
109
+ const readOrder = parseScopeProfileLayerList(rawProfile.readOrder, `scopeProfiles.${profileId}.readOrder`, [
110
+ "userProject",
111
+ "userGlobal",
112
+ "serverShared",
113
+ ]);
114
+ const writeDefault =
115
+ rawProfile.writeDefault === undefined || rawProfile.writeDefault === null
116
+ ? "userProject"
117
+ : rawProfile.writeDefault;
118
+ if (!SCOPE_PROFILE_LAYER_IDS.includes(writeDefault as ScopeProfileLayerId)) {
119
+ throw new Error(`scopeProfiles.${profileId}.writeDefault contains unsupported layer: ${String(writeDefault)}`);
120
+ }
121
+ if (rawProfile.teamProject !== undefined && rawProfile.teamProject !== null && !isRecord(rawProfile.teamProject)) {
122
+ throw new Error(`scopeProfiles.${profileId}.teamProject must be an object`);
123
+ }
124
+ const teamProject = (() => {
125
+ if (!isRecord(rawProfile.teamProject)) return undefined;
126
+ const out: NonNullable<ScopeProfileConfig["teamProject"]> = {};
127
+ if (rawProfile.teamProject.namespaceTemplate !== undefined) {
128
+ if (
129
+ typeof rawProfile.teamProject.namespaceTemplate !== "string" ||
130
+ rawProfile.teamProject.namespaceTemplate.length === 0
131
+ ) {
132
+ throw new Error(`scopeProfiles.${profileId}.teamProject.namespaceTemplate must be a non-empty string`);
133
+ }
134
+ out.namespaceTemplate = rawProfile.teamProject.namespaceTemplate;
135
+ }
136
+ if (rawProfile.teamProject.teamId !== undefined) {
137
+ if (typeof rawProfile.teamProject.teamId !== "string" || rawProfile.teamProject.teamId.length === 0) {
138
+ throw new Error(`scopeProfiles.${profileId}.teamProject.teamId must be a non-empty string`);
139
+ }
140
+ out.teamId = rawProfile.teamProject.teamId;
141
+ }
142
+ return out;
143
+ })();
144
+ if (rawProfile.autoPromote !== undefined && rawProfile.autoPromote !== null && !isRecord(rawProfile.autoPromote)) {
145
+ throw new Error(`scopeProfiles.${profileId}.autoPromote must be an object`);
146
+ }
147
+ const rawAutoPromote = isRecord(rawProfile.autoPromote) ? rawProfile.autoPromote : {};
148
+ const hasAutoPromoteEnabled = Object.prototype.hasOwnProperty.call(rawAutoPromote, "enabled");
149
+ const autoPromoteEnabled = coerceBool(rawAutoPromote.enabled);
150
+ if (hasAutoPromoteEnabled && autoPromoteEnabled === undefined) {
151
+ throw new Error(`scopeProfiles.${profileId}.autoPromote.enabled must be a boolean or boolean-like string`);
152
+ }
153
+ const minConfidenceTier = (() => {
154
+ const rawTier = rawAutoPromote.minConfidenceTier;
155
+ if (rawTier === undefined || rawTier === null) return "explicit";
156
+ if (!isConfidenceTier(rawTier)) {
157
+ throw new Error(
158
+ `scopeProfiles.${profileId}.autoPromote.minConfidenceTier must be one of: ${CONFIDENCE_TIERS.join(", ")}`
159
+ );
160
+ }
161
+ return rawTier;
162
+ })();
163
+ const autoPromoteCategories: AutoPromoteCategory[] = (() => {
164
+ if (rawAutoPromote.categories === undefined || rawAutoPromote.categories === null) {
165
+ return ["fact", "correction", "decision", "preference"];
166
+ }
167
+ if (!Array.isArray(rawAutoPromote.categories)) {
168
+ throw new Error(`scopeProfiles.${profileId}.autoPromote.categories must be an array`);
169
+ }
170
+ const categories: AutoPromoteCategory[] = [];
171
+ for (const entry of rawAutoPromote.categories) {
172
+ if (!isAutoPromoteCategory(entry)) {
173
+ throw new Error(
174
+ `scopeProfiles.${profileId}.autoPromote.categories must contain only: ${SCOPE_PROFILE_AUTO_PROMOTE_CATEGORIES.join(", ")}`
175
+ );
176
+ }
177
+ categories.push(entry);
178
+ }
179
+ return categories;
180
+ })();
181
+ profiles[profileId] = {
182
+ readOrder,
183
+ writeDefault: writeDefault as ScopeProfileLayerId,
184
+ promotionTargets: parseScopeProfilePromotionTargets(
185
+ rawProfile.promotionTargets,
186
+ `scopeProfiles.${profileId}.promotionTargets`
187
+ ),
188
+ ...(teamProject && Object.keys(teamProject).length > 0 ? { teamProject } : {}),
189
+ autoPromote: {
190
+ enabled: autoPromoteEnabled === true,
191
+ targets: parseScopeProfilePromotionTargets(
192
+ rawAutoPromote.targets,
193
+ `scopeProfiles.${profileId}.autoPromote.targets`
194
+ ),
195
+ categories: autoPromoteCategories,
196
+ minConfidenceTier,
197
+ },
198
+ };
199
+ }
200
+ return profiles;
201
+ }
202
+
203
+ export function parseScopeTeams(value: unknown): Record<string, ScopeTeamConfig> {
204
+ if (value === undefined || value === null) return {};
205
+ if (!isRecord(value)) {
206
+ throw new Error("teams must be an object");
207
+ }
208
+ const teams: Record<string, ScopeTeamConfig> = {};
209
+ for (const [teamId, rawTeam] of Object.entries(value)) {
210
+ if (teamId.trim().length === 0) {
211
+ throw new Error("teams keys must not be empty");
212
+ }
213
+ if (!isRecord(rawTeam)) {
214
+ throw new Error(`teams.${teamId} must be an object`);
215
+ }
216
+ const projectNamespaceTemplate = rawTeam.projectNamespaceTemplate;
217
+ if (projectNamespaceTemplate !== undefined) {
218
+ if (typeof projectNamespaceTemplate !== "string" || projectNamespaceTemplate.length === 0) {
219
+ throw new Error(`teams.${teamId}.projectNamespaceTemplate must be a non-empty string`);
220
+ }
221
+ }
222
+ teams[teamId] = {
223
+ principals: parseStringList(rawTeam.principals, `teams.${teamId}.principals`),
224
+ ...(projectNamespaceTemplate !== undefined ? { projectNamespaceTemplate } : {}),
225
+ read: parseStringList(rawTeam.read, `teams.${teamId}.read`),
226
+ write: parseStringList(rawTeam.write, `teams.${teamId}.write`),
227
+ promote: parseStringList(rawTeam.promote, `teams.${teamId}.promote`),
228
+ };
229
+ }
230
+ return teams;
231
+ }
232
+
233
+ export function validateScopeProfileTeamReferences(
234
+ profiles: Record<string, ScopeProfileConfig>,
235
+ teams: Record<string, ScopeTeamConfig>
236
+ ): void {
237
+ for (const [profileId, profile] of Object.entries(profiles)) {
238
+ const teamId = profile.teamProject?.teamId;
239
+ if (teamId && !Object.prototype.hasOwnProperty.call(teams, teamId)) {
240
+ throw new Error(`scopeProfiles.${profileId}.teamProject.teamId references unknown team: ${teamId}`);
241
+ }
242
+ }
243
+ }
@@ -54,15 +54,6 @@ export interface RecallIntrospectionDeps {
54
54
  effectiveCronRecallInstructionHeavyTokenCap(): number;
55
55
  readonly faithfulnessCounters: FaithfulnessGateCounters;
56
56
  getCodingContextForSession(sessionKey: string | undefined): CodingContext | null;
57
- getLastGraphRecallSnapshot(
58
- namespace?: string,
59
- ): Promise<GraphRecallSnapshot | null>;
60
- getLastIntentSnapshot(
61
- namespace?: string,
62
- ): Promise<IntentDebugSnapshot | null>;
63
- getLastQmdRecallSnapshot(
64
- namespace?: string,
65
- ): Promise<QmdRecallSnapshot | null>;
66
57
  getStorage(namespace?: string): Promise<StorageManager>;
67
58
  readonly graphRecallCoordinator: GraphRecallCoordinator;
68
59
  readonly lastRecall: LastRecallStore;
@@ -299,7 +290,7 @@ export class RecallIntrospectionCoordinator {
299
290
  namespace?: string;
300
291
  maxExpanded?: number;
301
292
  }): Promise<string> {
302
- const snapshot = await this.deps.getLastGraphRecallSnapshot(options?.namespace);
293
+ const snapshot = await this.getLastGraphRecallSnapshot(options?.namespace);
303
294
  if (!snapshot) return "No graph-recall snapshot found yet.";
304
295
  const maxExpanded = Math.max(1, Math.min(50, options?.maxExpanded ?? 10));
305
296
  const expanded = snapshot.expanded.slice(0, maxExpanded);
@@ -353,7 +344,7 @@ export class RecallIntrospectionCoordinator {
353
344
  }
354
345
 
355
346
  async explainLastIntent(options?: { namespace?: string }): Promise<string> {
356
- const snapshot = await this.deps.getLastIntentSnapshot(options?.namespace);
347
+ const snapshot = await this.getLastIntentSnapshot(options?.namespace);
357
348
  if (!snapshot) return "No intent-debug snapshot found yet.";
358
349
  return [
359
350
  "## Last Intent Debug",
@@ -375,7 +366,7 @@ export class RecallIntrospectionCoordinator {
375
366
  namespace?: string;
376
367
  maxResults?: number;
377
368
  }): Promise<string> {
378
- const snapshot = await this.deps.getLastQmdRecallSnapshot(options?.namespace);
369
+ const snapshot = await this.getLastQmdRecallSnapshot(options?.namespace);
379
370
  if (!snapshot) return "No QMD recall snapshot found yet.";
380
371
  const maxResults = Math.max(1, Math.min(25, options?.maxResults ?? 10));
381
372
  const shown = snapshot.results.slice(0, maxResults);