@stravica-ai/digital-operator-pa 0.1.23 → 0.1.26
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/dist/engineering-pa-cli/package.json +1 -1
- package/dist/engineering-pa-cli/src/knowledge/cli.mjs +9 -0
- package/dist/engineering-pa-cli/src/knowledge/context.mjs +274 -0
- package/dist/engineering-pa-cli/src/pa/cli.mjs +1 -1
- package/dist/engineering-pa-cli/src/pa/session.mjs +29 -0
- package/dist/engineering-pa-core/package.json +1 -1
- package/dist/engineering-pa-core/src/library-config.mjs +122 -0
- package/dist/engineering-pa-scaffold/package.json +1 -1
- package/dist/engineering-pa-scaffold/src/generator.mjs +7 -0
- package/package.json +1 -1
|
@@ -49,6 +49,11 @@ Usage:
|
|
|
49
49
|
and the excluded amount is shown.
|
|
50
50
|
[--exclude-stubs] Skip null-domain nodes
|
|
51
51
|
pa knowledge path <a> <b> Shortest path between two docs
|
|
52
|
+
pa knowledge context Print the effective CLAUDE.md rule
|
|
53
|
+
[--json] variants selected by \`library.mode\`
|
|
54
|
+
(remote-library spec §5.1, §5.2).
|
|
55
|
+
Refuses cleanly on structurally
|
|
56
|
+
invalid REMOTE config.
|
|
52
57
|
pa knowledge eval [--fixture P] Score the retrieval eval set (hit@1/5,
|
|
53
58
|
[--out D] orphan gauge, delta vs last run);
|
|
54
59
|
[--full] persists to
|
|
@@ -93,6 +98,10 @@ export async function run(argv) {
|
|
|
93
98
|
case 'domain': return await doDomain(rest);
|
|
94
99
|
case 'orphans': return await doOrphans(rest);
|
|
95
100
|
case 'path': return await doPath(rest);
|
|
101
|
+
case 'context': {
|
|
102
|
+
const { runContext } = await import('./context.mjs');
|
|
103
|
+
return await runContext(rest);
|
|
104
|
+
}
|
|
96
105
|
case 'eval': {
|
|
97
106
|
const { run: runEval } = await import('./eval.mjs');
|
|
98
107
|
return await runEval(rest);
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// context.mjs — `pa knowledge context` verb.
|
|
2
|
+
//
|
|
3
|
+
// Prints the CLAUDE.md rule variants that are LIVE for this session,
|
|
4
|
+
// selected by `library.mode` in the merged operator preferences.
|
|
5
|
+
//
|
|
6
|
+
// The two-mode switch is specified in the `@stravica-ai/remote-library`
|
|
7
|
+
// spec §5.1 and §5.2 (build items 14 + 15). Item 14 landed the wiring —
|
|
8
|
+
// `resolveLibraryConfig` + `getLibraryVariant` in `packages/core/src/
|
|
9
|
+
// library-config.mjs` — and the `plan.libraryVariant` seam on
|
|
10
|
+
// `buildSessionPlan`. Item 15 (this file, plus stravica-pa's CLAUDE.md
|
|
11
|
+
// variant markers) closes the loop: agents read the CLAUDE.md variants
|
|
12
|
+
// on boot and invoke `pa knowledge context` to know WHICH is live.
|
|
13
|
+
//
|
|
14
|
+
// Rule-variant TEXT is duplicated between this module and the operator
|
|
15
|
+
// repo's CLAUDE.md. Both sources cite spec §5.2 verbatim. The
|
|
16
|
+
// duplication is a feature: each stands alone; if one drifts, running
|
|
17
|
+
// `pa knowledge context` alongside CLAUDE.md flags the drift visually.
|
|
18
|
+
//
|
|
19
|
+
// Refuse-boot semantics (spec §5.1): a structurally invalid REMOTE
|
|
20
|
+
// config (mode="remote" with missing endpoint/key, or malformed key
|
|
21
|
+
// shape) surfaces the underlying `LibraryConfigError` on stderr and
|
|
22
|
+
// exits 1. `pa knowledge context` is deliberately usable as a
|
|
23
|
+
// diagnostic when the boot path itself is refusing to open — an
|
|
24
|
+
// operator can invoke it AFTER the `pa session` refusal to inspect
|
|
25
|
+
// exactly what the mode-resolver saw.
|
|
26
|
+
//
|
|
27
|
+
// Runtime unreachability of the remote endpoint is NOT surfaced here:
|
|
28
|
+
// this verb never performs a network call. That is deliberate — spec
|
|
29
|
+
// §5.1 rules runtime unreachability non-fatal at boot; probing here
|
|
30
|
+
// would drag the boot doctrine sideways.
|
|
31
|
+
|
|
32
|
+
import { LibraryConfigError, getLibraryVariant, resolveLibraryConfig } from '../../../engineering-pa-core/src/library-config.mjs';
|
|
33
|
+
import { readPreferences } from '../../../engineering-pa-core/src/preferences.mjs';
|
|
34
|
+
|
|
35
|
+
import { getRepoRoot } from '../pa/repo-root.mjs';
|
|
36
|
+
|
|
37
|
+
// --- rule-variant text (spec §5.2 verbatim) ------------------------------
|
|
38
|
+
//
|
|
39
|
+
// Every entry mirrors an existing rule in the operator's
|
|
40
|
+
// `agents/<paName>/CLAUDE.md` under Bucket 4 (Knowledge & memory).
|
|
41
|
+
// `bucket` + `ruleNumber` + `ruleTitle` let the JSON envelope be indexed
|
|
42
|
+
// against the CLAUDE.md file without string-matching the file body.
|
|
43
|
+
// `simple` is the historic wording; the REMOTE deltas come from
|
|
44
|
+
// remote-library spec §5.2.
|
|
45
|
+
export const RULE_VARIANTS = [
|
|
46
|
+
{
|
|
47
|
+
bucket: 4,
|
|
48
|
+
ruleNumber: 2,
|
|
49
|
+
ruleTitle: 'Repo is single source of truth',
|
|
50
|
+
simple: 'Markdown for human-readable, JSON for machine-readable indexes.',
|
|
51
|
+
remote: 'The write path is `library put`. The pulled repo is a read-only projection of the event log. Never edit files inside library repos directly; write via `library put`.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
bucket: 4,
|
|
55
|
+
ruleNumber: 7,
|
|
56
|
+
ruleTitle: 'The Librarian sweeps staging during distillation',
|
|
57
|
+
simple: 'Each agent stages to its own staging dir; the Librarian routes observations to canonical destinations. Don\'t route observations directly.',
|
|
58
|
+
remote: '`library flush` sweeps local staging into the event log. The service applies the events to the git projection.',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const USAGE = `pa knowledge context — print the effective CLAUDE.md rule variants for this session
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
pa knowledge context Print the live rule variants (human-readable)
|
|
66
|
+
pa knowledge context --format json Emit the same envelope as JSON
|
|
67
|
+
|
|
68
|
+
The variant is selected by \`library.mode\` in the merged operator preferences
|
|
69
|
+
(base file + preferences.local.json overlay). Absent field defaults to "simple"
|
|
70
|
+
per spec §5.1.
|
|
71
|
+
|
|
72
|
+
A structurally invalid REMOTE config (mode="remote" with missing endpoint or
|
|
73
|
+
key, or a malformed key shape) is REFUSED here with the same message the
|
|
74
|
+
\`pa session\` boot path uses — this verb is safe to run as a diagnostic when
|
|
75
|
+
\`pa session\` itself is refusing to open.
|
|
76
|
+
|
|
77
|
+
Runtime endpoint unreachability is not probed here (spec §5.1: runtime
|
|
78
|
+
unreachability is non-fatal at boot; individual \`library\` verbs fail closed
|
|
79
|
+
and alert).
|
|
80
|
+
|
|
81
|
+
Exit codes (tooling standard §6):
|
|
82
|
+
0 ok | 1 usage/config error | 2 unexpected internal failure
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build the envelope for the resolved library variant. Pure: takes an
|
|
87
|
+
* already-resolved config plus the resolved paName, returns the
|
|
88
|
+
* printable / JSON-emittable shape.
|
|
89
|
+
*
|
|
90
|
+
* `paName` is the boot-resolved operator name (from merged preferences,
|
|
91
|
+
* same source `buildSessionPlan` uses). When null (diagnostic call with
|
|
92
|
+
* absent preferences), the rendered path degrades to the `<paName>`
|
|
93
|
+
* placeholder so the diagnostic still emits without lying about which
|
|
94
|
+
* operator's file the variants target.
|
|
95
|
+
*
|
|
96
|
+
* @param {{mode: 'simple'|'remote', remoteEndpoint?: string, remoteKey?: string}} config
|
|
97
|
+
* @param {{paName?: string|null}} [opts]
|
|
98
|
+
* @returns {{
|
|
99
|
+
* verb: 'context',
|
|
100
|
+
* libraryMode: 'simple'|'remote',
|
|
101
|
+
* libraryVariant: 'simple'|'remote',
|
|
102
|
+
* remoteEndpoint: string|null,
|
|
103
|
+
* paName: string|null,
|
|
104
|
+
* agentClaudePath: string,
|
|
105
|
+
* ruleVariants: Array<{bucket:number, ruleNumber:number, ruleTitle:string, effective:string, mode:'simple'|'remote'}>,
|
|
106
|
+
* specReference: string,
|
|
107
|
+
* }}
|
|
108
|
+
*/
|
|
109
|
+
export function buildContextEnvelope(config, opts = {}) {
|
|
110
|
+
const variant = getLibraryVariant(config);
|
|
111
|
+
const paName = typeof opts.paName === 'string' && opts.paName.length > 0
|
|
112
|
+
? opts.paName
|
|
113
|
+
: null;
|
|
114
|
+
const agentClaudePath = `agents/${paName ?? '<paName>'}/CLAUDE.md`;
|
|
115
|
+
return {
|
|
116
|
+
verb: 'context',
|
|
117
|
+
libraryMode: config.mode,
|
|
118
|
+
libraryVariant: variant,
|
|
119
|
+
remoteEndpoint: config.mode === 'remote' ? config.remoteEndpoint : null,
|
|
120
|
+
paName,
|
|
121
|
+
agentClaudePath,
|
|
122
|
+
ruleVariants: RULE_VARIANTS.map((r) => ({
|
|
123
|
+
bucket: r.bucket,
|
|
124
|
+
ruleNumber: r.ruleNumber,
|
|
125
|
+
ruleTitle: r.ruleTitle,
|
|
126
|
+
effective: variant === 'remote' ? r.remote : r.simple,
|
|
127
|
+
mode: variant,
|
|
128
|
+
})),
|
|
129
|
+
specReference: '@stravica-ai/remote-library spec §5.1 (mode switch), §5.2 (rule variants)',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Human-readable renderer. Emits one block per rule variant so an
|
|
135
|
+
* operator can eyeball which text is live.
|
|
136
|
+
* @param {ReturnType<typeof buildContextEnvelope>} envelope
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
export function formatContextTable(envelope) {
|
|
140
|
+
const lines = [];
|
|
141
|
+
lines.push(`library.mode = ${envelope.libraryMode} (variant token: ${envelope.libraryVariant})`);
|
|
142
|
+
if (envelope.libraryMode === 'remote') {
|
|
143
|
+
lines.push(`remoteEndpoint: ${envelope.remoteEndpoint}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push(`Effective CLAUDE.md rule variants (${envelope.agentClaudePath}, Bucket 4):`);
|
|
147
|
+
lines.push('');
|
|
148
|
+
for (const r of envelope.ruleVariants) {
|
|
149
|
+
lines.push(` Rule ${r.bucket}.${r.ruleNumber} — ${r.ruleTitle} [${r.mode}]`);
|
|
150
|
+
lines.push(` ${r.effective}`);
|
|
151
|
+
lines.push('');
|
|
152
|
+
}
|
|
153
|
+
lines.push(`Spec: ${envelope.specReference}`);
|
|
154
|
+
lines.push('');
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- verb entry ---------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
function parseFlags(args) {
|
|
161
|
+
const flags = {};
|
|
162
|
+
const positional = [];
|
|
163
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
164
|
+
const a = args[i];
|
|
165
|
+
if (a === '-h' || a === '--help') { flags.help = true; continue; }
|
|
166
|
+
if (a === '--json') { flags.format = 'json'; continue; }
|
|
167
|
+
if (a === '--format') {
|
|
168
|
+
const peek = args[i + 1];
|
|
169
|
+
if (peek == null || peek.startsWith('--')) {
|
|
170
|
+
const err = new Error('--format requires an argument (table|json)');
|
|
171
|
+
err.exitCode = 1;
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
flags.format = peek;
|
|
175
|
+
i += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (a.startsWith('--format=')) {
|
|
179
|
+
flags.format = a.slice('--format='.length);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
positional.push(a);
|
|
183
|
+
}
|
|
184
|
+
if (positional.length > 0) {
|
|
185
|
+
const err = new Error(`unexpected argument: ${positional[0]}`);
|
|
186
|
+
err.exitCode = 1;
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
if (flags.format != null && flags.format !== 'table' && flags.format !== 'json') {
|
|
190
|
+
const err = new Error(`unknown --format: ${flags.format} (expected table | json)`);
|
|
191
|
+
err.exitCode = 1;
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
return flags;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Verb entrypoint. Seams allow tests to inject a fake preferences reader,
|
|
199
|
+
* fake repo-root resolver, and capture streams.
|
|
200
|
+
*
|
|
201
|
+
* @param {string[]} argv args after `context`
|
|
202
|
+
* @param {object} [seam]
|
|
203
|
+
* @param {() => Promise<string>} [seam.getRepoRoot]
|
|
204
|
+
* @param {(root:string) => Promise<object|null>} [seam.readPreferences]
|
|
205
|
+
* @param {NodeJS.WritableStream} [seam.stdout]
|
|
206
|
+
* @param {NodeJS.WritableStream} [seam.stderr]
|
|
207
|
+
*/
|
|
208
|
+
export async function runContext(argv, seam = {}) {
|
|
209
|
+
const stdout = seam.stdout ?? process.stdout;
|
|
210
|
+
const stderr = seam.stderr ?? process.stderr;
|
|
211
|
+
const getRoot = seam.getRepoRoot ?? getRepoRoot;
|
|
212
|
+
const readPrefs = seam.readPreferences ?? readPreferences;
|
|
213
|
+
|
|
214
|
+
let flags;
|
|
215
|
+
try {
|
|
216
|
+
flags = parseFlags(argv);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
stderr.write(`[pa knowledge context] ${err.message}\n`);
|
|
219
|
+
stderr.write(USAGE);
|
|
220
|
+
return err.exitCode ?? 1;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (flags.help) {
|
|
224
|
+
stdout.write(USAGE);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let repoRoot;
|
|
229
|
+
try {
|
|
230
|
+
repoRoot = await getRoot();
|
|
231
|
+
} catch (err) {
|
|
232
|
+
stderr.write(`[pa knowledge context] ${err.message ?? err}\n`);
|
|
233
|
+
return err.exitCode ?? 2;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let prefs;
|
|
237
|
+
try {
|
|
238
|
+
prefs = await readPrefs(repoRoot);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
stderr.write(`[pa knowledge context] failed to read preferences at ${repoRoot}: ${err.message ?? err}\n`);
|
|
241
|
+
return 2;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let config;
|
|
245
|
+
try {
|
|
246
|
+
config = resolveLibraryConfig(prefs);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (err instanceof LibraryConfigError) {
|
|
249
|
+
stderr.write(`[pa knowledge context] ${err.message}\n`);
|
|
250
|
+
return err.exitCode ?? 1;
|
|
251
|
+
}
|
|
252
|
+
stderr.write(`[pa knowledge context] ${err.message ?? err}\n`);
|
|
253
|
+
return 2;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// paName is the boot-resolved operator name. Sourced from the same
|
|
257
|
+
// merged preferences `buildSessionPlan` consumes — no new config, no
|
|
258
|
+
// second read. Absent prefs (readPreferences === null) keeps paName
|
|
259
|
+
// null and the envelope renders the `<paName>` placeholder so the
|
|
260
|
+
// diagnostic remains usable when the boot path itself is refusing.
|
|
261
|
+
const paName = (prefs && typeof prefs.paName === 'string' && prefs.paName.length > 0)
|
|
262
|
+
? prefs.paName
|
|
263
|
+
: null;
|
|
264
|
+
|
|
265
|
+
const envelope = buildContextEnvelope(config, { paName });
|
|
266
|
+
if (flags.format === 'json') {
|
|
267
|
+
stdout.write(JSON.stringify(envelope, null, 2) + '\n');
|
|
268
|
+
} else {
|
|
269
|
+
stdout.write(formatContextTable(envelope));
|
|
270
|
+
}
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export { USAGE };
|
|
@@ -24,7 +24,7 @@ Usage:
|
|
|
24
24
|
pa project <verb> Project state writes (create | status | doc add/rm | list)
|
|
25
25
|
pa work <verb> Work-item state writes (create | status | list)
|
|
26
26
|
pa gsc <verb> Google Search Console automation (add-property | verify-via-cf-dns | submit-sitemap | sitemap-status | pull-analytics | list-properties)
|
|
27
|
-
pa knowledge <verb> Query the operator's knowledge graph (related | search | domain | orphans | path)
|
|
27
|
+
pa knowledge <verb> Query the operator's knowledge graph (related | search | domain | orphans | path | context)
|
|
28
28
|
pa research <verb> Deep-research session store (run | list | show | gates | prune | mark | paths)
|
|
29
29
|
pa autonomy <verb> Per-agent per-task-type approval state (init | get | set | list)
|
|
30
30
|
pa environment [verb] Session-boot environment cache (default | refresh | extended)
|
|
@@ -22,6 +22,11 @@ import { spawn } from 'node:child_process';
|
|
|
22
22
|
import { dirname, join, resolve } from 'node:path';
|
|
23
23
|
|
|
24
24
|
import { mergePreferences } from '../../../engineering-pa-core/src/preferences.mjs';
|
|
25
|
+
import {
|
|
26
|
+
LibraryConfigError,
|
|
27
|
+
getLibraryVariant,
|
|
28
|
+
resolveLibraryConfig,
|
|
29
|
+
} from '../../../engineering-pa-core/src/library-config.mjs';
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* Resolve the operator repo root with the priority documented in the brief:
|
|
@@ -172,6 +177,28 @@ export async function buildSessionPlan(repoRoot, seam = {}) {
|
|
|
172
177
|
);
|
|
173
178
|
}
|
|
174
179
|
|
|
180
|
+
// Item 14: resolve the `library.*` block and select the CLAUDE.md
|
|
181
|
+
// rule-variant token. `resolveLibraryConfig` throws
|
|
182
|
+
// LibraryConfigError on mode=remote misconfiguration (spec §5.1 —
|
|
183
|
+
// refuse to boot the library subsystem, no silent fallback to
|
|
184
|
+
// SIMPLE). Item 15 will consume `libraryVariant` when it renders the
|
|
185
|
+
// effective CLAUDE.md and when `pa knowledge context` prints the
|
|
186
|
+
// resolved wording; the plan carries the token forward so item 15
|
|
187
|
+
// slots content in without changing the wiring here.
|
|
188
|
+
let libraryConfig;
|
|
189
|
+
try {
|
|
190
|
+
libraryConfig = resolveLibraryConfig(merged);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (err instanceof LibraryConfigError) {
|
|
193
|
+
throw new SessionError(
|
|
194
|
+
`${err.message} (preferences at ${preferencesPath})`,
|
|
195
|
+
err.exitCode ?? 1,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
const libraryVariant = getLibraryVariant(libraryConfig);
|
|
201
|
+
|
|
175
202
|
return {
|
|
176
203
|
repoRoot,
|
|
177
204
|
preferencesPath,
|
|
@@ -179,6 +206,8 @@ export async function buildSessionPlan(repoRoot, seam = {}) {
|
|
|
179
206
|
agentDir,
|
|
180
207
|
command: 'claude',
|
|
181
208
|
args: ['--dangerously-skip-permissions'],
|
|
209
|
+
libraryConfig,
|
|
210
|
+
libraryVariant,
|
|
182
211
|
};
|
|
183
212
|
}
|
|
184
213
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stravica-ai/engineering-pa-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Type-agnostic platform spine — desk CLI, distill, memory-sync, schemas, briefing-contract framework, knowledge engine, scheduler primitives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// library-config.mjs — resolve + validate the `library.*` block in
|
|
2
|
+
// operator preferences.
|
|
3
|
+
//
|
|
4
|
+
// This module supplies the READ + REFUSE-BOOT half of the two-mode
|
|
5
|
+
// switch specified in `@stravica-ai/remote-library` spec §5.1:
|
|
6
|
+
//
|
|
7
|
+
// - `library.mode` is EXPLICIT (`"simple"` | `"remote"`); never auto-
|
|
8
|
+
// detected. Absent defaults to `"simple"`.
|
|
9
|
+
// - `mode="remote"` with missing/empty `remoteEndpoint` or `remoteKey`
|
|
10
|
+
// REFUSES to boot the library subsystem — no silent fallback to
|
|
11
|
+
// SIMPLE (falling back is a data-loss path).
|
|
12
|
+
//
|
|
13
|
+
// This lands as a token-selection module for build item 14 (see the
|
|
14
|
+
// spec's work-item breakdown §11). The CLAUDE.md variant CONTENT and
|
|
15
|
+
// the `pa knowledge context` verb both key off `getLibraryVariant`
|
|
16
|
+
// and will land in item 15; the wiring here is stable so item 15 slots
|
|
17
|
+
// content into the existing shape without changing this file.
|
|
18
|
+
//
|
|
19
|
+
// The reader is DELIBERATELY pure — it consumes an already-merged
|
|
20
|
+
// preferences object. Callers pair it with `readPreferences` /
|
|
21
|
+
// `readPreferencesSync` from ./preferences.mjs so the overlay-aware
|
|
22
|
+
// path stays a single implementation.
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default when `library.mode` is absent from preferences.
|
|
26
|
+
* @type {"simple"|"remote"}
|
|
27
|
+
*/
|
|
28
|
+
export const DEFAULT_LIBRARY_MODE = 'simple';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set of accepted `library.mode` values (spec §5.1).
|
|
32
|
+
*/
|
|
33
|
+
const VALID_MODES = new Set(['simple', 'remote']);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sentinel raised on invalid `library.*` configuration. Carries a
|
|
37
|
+
* `code` for machine-consumer disambiguation and an `exitCode` so
|
|
38
|
+
* CLI wrappers can surface it without a switch.
|
|
39
|
+
*/
|
|
40
|
+
export class LibraryConfigError extends Error {
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} message
|
|
43
|
+
* @param {string} code e.g. 'invalid-mode' | 'remote-config-missing' | 'remote-key-shape'
|
|
44
|
+
* @param {number} [exitCode] defaults to 1 (usage / configuration error)
|
|
45
|
+
*/
|
|
46
|
+
constructor(message, code, exitCode = 1) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = 'LibraryConfigError';
|
|
49
|
+
this.code = code;
|
|
50
|
+
this.exitCode = exitCode;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the `library` block from a merged preferences object.
|
|
56
|
+
* Returns `{ mode }` for SIMPLE mode and
|
|
57
|
+
* `{ mode: 'remote', remoteEndpoint, remoteKey }` for REMOTE mode.
|
|
58
|
+
* Throws {@link LibraryConfigError} on invalid shape or on REMOTE
|
|
59
|
+
* mode with missing required fields.
|
|
60
|
+
*
|
|
61
|
+
* @param {object|null} preferences merged preferences (base + overlay)
|
|
62
|
+
* @returns {{mode: 'simple'|'remote', remoteEndpoint?: string, remoteKey?: string}}
|
|
63
|
+
*/
|
|
64
|
+
export function resolveLibraryConfig(preferences) {
|
|
65
|
+
const raw =
|
|
66
|
+
preferences && typeof preferences === 'object' && !Array.isArray(preferences)
|
|
67
|
+
? preferences.library
|
|
68
|
+
: null;
|
|
69
|
+
const block = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
70
|
+
|
|
71
|
+
const mode = block.mode ?? DEFAULT_LIBRARY_MODE;
|
|
72
|
+
|
|
73
|
+
if (!VALID_MODES.has(mode)) {
|
|
74
|
+
throw new LibraryConfigError(
|
|
75
|
+
`library.mode must be "simple" or "remote", got ${JSON.stringify(mode)}`,
|
|
76
|
+
'invalid-mode',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (mode === 'simple') {
|
|
81
|
+
return { mode };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// REMOTE mode — validate required fields (spec §5.1 refuse-boot).
|
|
85
|
+
const missing = [];
|
|
86
|
+
const endpoint = block.remoteEndpoint;
|
|
87
|
+
const key = block.remoteKey;
|
|
88
|
+
if (typeof endpoint !== 'string' || endpoint.trim() === '') missing.push('library.remoteEndpoint');
|
|
89
|
+
if (typeof key !== 'string' || key.trim() === '') missing.push('library.remoteKey');
|
|
90
|
+
if (missing.length > 0) {
|
|
91
|
+
throw new LibraryConfigError(
|
|
92
|
+
`library.mode="remote" but required field(s) missing/empty: ${missing.join(', ')}. ` +
|
|
93
|
+
`Refusing to boot the library subsystem (spec §5.1: no silent fallback to SIMPLE).`,
|
|
94
|
+
'remote-config-missing',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Cheap shape check on remoteKey (`<subkeyId>:<secret>`). The server
|
|
99
|
+
// does the authoritative check; catching a paste error early spares a
|
|
100
|
+
// network round-trip and a generic 401.
|
|
101
|
+
if (!/^[^:\s]+:[^\s]+$/.test(key)) {
|
|
102
|
+
throw new LibraryConfigError(
|
|
103
|
+
'library.remoteKey must be of shape "<subkeyId>:<secret>" (no whitespace, colon separator).',
|
|
104
|
+
'remote-key-shape',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { mode, remoteEndpoint: endpoint, remoteKey: key };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Return the CLAUDE.md rule-variant token for a resolved library config.
|
|
113
|
+
* Item 15 keys off this string when composing the effective CLAUDE.md
|
|
114
|
+
* and when `pa knowledge context` prints the resolved wording.
|
|
115
|
+
*
|
|
116
|
+
* @param {{mode: 'simple'|'remote'}} config the value returned by resolveLibraryConfig
|
|
117
|
+
* @returns {'simple'|'remote'}
|
|
118
|
+
*/
|
|
119
|
+
export function getLibraryVariant(config) {
|
|
120
|
+
const mode = config?.mode ?? DEFAULT_LIBRARY_MODE;
|
|
121
|
+
return mode === 'remote' ? 'remote' : 'simple';
|
|
122
|
+
}
|
|
@@ -184,6 +184,13 @@ export function buildPreferencesJson(config) {
|
|
|
184
184
|
pa: { name: capitalise(config.paName), role: 'PA' },
|
|
185
185
|
communicationStyle: config.communicationStyle,
|
|
186
186
|
workspacePort: 4200,
|
|
187
|
+
// `@stravica-ai/remote-library` spec §5.1 — mode is EXPLICIT, never
|
|
188
|
+
// auto-detected. Fresh installs land on SIMPLE (single-operator, one
|
|
189
|
+
// local librarian sweep). Flipping to `"remote"` also requires
|
|
190
|
+
// `remoteEndpoint` and `remoteKey` (see spec §5.1); the `pa session`
|
|
191
|
+
// boot path refuses to launch when `mode="remote"` and either field
|
|
192
|
+
// is missing.
|
|
193
|
+
library: { mode: 'simple' },
|
|
187
194
|
generatedAt: config.generatedAt,
|
|
188
195
|
};
|
|
189
196
|
}
|
package/package.json
CHANGED