@stravica-ai/digital-operator-pa 0.1.10 → 0.1.11
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.
|
@@ -1,32 +1,98 @@
|
|
|
1
1
|
// identity.mjs — pure: load + validate an agent identity from the operator roster.
|
|
2
|
-
//
|
|
2
|
+
//
|
|
3
|
+
// Roster resolution is mount-aware (w-2026-07-29-007). Rosters live at two
|
|
4
|
+
// levels of the operator knowledge tree:
|
|
5
|
+
// 1. Flat (HQ) path: <repoRoot>/operator/knowledge/hr/roster/<name>.json
|
|
6
|
+
// 2. Team-mount subtrees: <repoRoot>/operator/knowledge/<team>/hr/roster/<name>.json
|
|
7
|
+
// (e.g. WSD's dex at operator/knowledge/wsd/hr/roster/dex.json)
|
|
8
|
+
//
|
|
9
|
+
// The flat path is checked first, then every team mount. A single hit --
|
|
10
|
+
// wherever it lives -- resolves. The SAME agent name present in more than one
|
|
11
|
+
// roster (flat + team, or two teams) is an ambiguity the operator must fix:
|
|
12
|
+
// the loader errors listing every matching path rather than silently picking
|
|
13
|
+
// one. Silent precedence is exactly the failure mode this guards against -- a
|
|
14
|
+
// stale flat copy shadowing a team roster (or vice versa) would launch an
|
|
15
|
+
// agent under the wrong identity with no signal.
|
|
16
|
+
//
|
|
3
17
|
// Required fields: id, name, roleId, runFolder, status
|
|
4
|
-
import { readFile } from 'node:fs/promises';
|
|
18
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
5
19
|
import { join } from 'node:path';
|
|
6
20
|
|
|
7
21
|
const REQUIRED_FIELDS = ['id', 'name', 'roleId', 'runFolder', 'status'];
|
|
8
22
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
const ROSTER_SEGMENTS = ['hr', 'roster'];
|
|
24
|
+
|
|
25
|
+
function flatRosterPath(repoRoot, agentName) {
|
|
26
|
+
return join(repoRoot, 'operator', 'knowledge', ...ROSTER_SEGMENTS, `${agentName}.json`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Enumerate team-mount roster candidates: operator/knowledge/<team>/hr/roster/<name>.json.
|
|
30
|
+
// Directory listing is sorted so error output is deterministic across
|
|
31
|
+
// filesystems. The flat tree's own `hr/` directory is skipped --
|
|
32
|
+
// `operator/knowledge/hr` is the flat roster's parent, not a team mount, and
|
|
33
|
+
// matching it would double-count the flat path.
|
|
34
|
+
async function teamRosterPaths(repoRoot, agentName) {
|
|
35
|
+
const knowledgeRoot = join(repoRoot, 'operator', 'knowledge');
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = await readdir(knowledgeRoot, { withFileTypes: true });
|
|
39
|
+
} catch {
|
|
40
|
+
return []; // no knowledge tree at all -- nothing to scan
|
|
41
|
+
}
|
|
42
|
+
return entries
|
|
43
|
+
.filter((e) => e.isDirectory() && e.name !== 'hr')
|
|
44
|
+
.map((e) => e.name)
|
|
45
|
+
.sort()
|
|
46
|
+
.map((team) => join(knowledgeRoot, team, ...ROSTER_SEGMENTS, `${agentName}.json`));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Read a roster file's raw content, distinguishing "not there" (null) from
|
|
50
|
+
// real read failures (thrown, exitCode 3).
|
|
51
|
+
async function readRosterFile(path, agentName) {
|
|
12
52
|
try {
|
|
13
|
-
|
|
53
|
+
return await readFile(path, 'utf8');
|
|
14
54
|
} catch (err) {
|
|
15
|
-
if (err.code === 'ENOENT')
|
|
16
|
-
const e = new Error(`agent "${agentName}" not found in roster (${path})`);
|
|
17
|
-
e.exitCode = 2;
|
|
18
|
-
throw e;
|
|
19
|
-
}
|
|
55
|
+
if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return null;
|
|
20
56
|
const e = new Error(`could not read roster file for "${agentName}": ${err.message}`);
|
|
21
57
|
e.exitCode = 3;
|
|
22
58
|
throw e;
|
|
23
59
|
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function loadIdentity(repoRoot, agentName) {
|
|
63
|
+
const flatPath = flatRosterPath(repoRoot, agentName);
|
|
64
|
+
const candidates = [flatPath, ...(await teamRosterPaths(repoRoot, agentName))];
|
|
65
|
+
|
|
66
|
+
const hits = [];
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
const content = await readRosterFile(candidate, agentName);
|
|
69
|
+
if (content !== null) hits.push({ path: candidate, content });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (hits.length === 0) {
|
|
73
|
+
const e = new Error(
|
|
74
|
+
`agent "${agentName}" not found in roster (searched ${flatPath} and team mounts operator/knowledge/*/hr/roster/)`,
|
|
75
|
+
);
|
|
76
|
+
e.exitCode = 2;
|
|
77
|
+
throw e;
|
|
78
|
+
}
|
|
79
|
+
if (hits.length > 1) {
|
|
80
|
+
const e = new Error(
|
|
81
|
+
`agent "${agentName}" is ambiguous -- found in ${hits.length} rosters: ${hits
|
|
82
|
+
.map((h) => h.path)
|
|
83
|
+
.join(', ')}. Remove the duplicate roster entry (or rename one agent) before launching.`,
|
|
84
|
+
);
|
|
85
|
+
e.exitCode = 2;
|
|
86
|
+
throw e;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { path: resolvedPath, content: raw } = hits[0];
|
|
24
90
|
|
|
25
91
|
let data;
|
|
26
92
|
try {
|
|
27
93
|
data = JSON.parse(raw);
|
|
28
94
|
} catch (err) {
|
|
29
|
-
const e = new Error(`roster file for "${agentName}" is not valid JSON: ${err.message}`);
|
|
95
|
+
const e = new Error(`roster file for "${agentName}" is not valid JSON (${resolvedPath}): ${err.message}`);
|
|
30
96
|
e.exitCode = 2;
|
|
31
97
|
throw e;
|
|
32
98
|
}
|
|
@@ -46,13 +46,36 @@ any file changes distill produced.
|
|
|
46
46
|
2. Count markdown observations under `agents/*/staging/` (depth ≤ 2,
|
|
47
47
|
excluding `_archived/`). If zero, append a `noop-empty-staging`
|
|
48
48
|
audit record and exit 0.
|
|
49
|
-
3. Invoke `claude` with
|
|
50
|
-
|
|
49
|
+
3. Invoke `claude` with an explicit `--model` (see Model selection below),
|
|
50
|
+
the librarian definition as an appended system prompt, and the
|
|
51
|
+
distillation instructions as the positional prompt.
|
|
51
52
|
Output is written to `<repoRoot>/.cache/distill/logs/<ts>.log`.
|
|
52
53
|
4. Append a one-line JSON audit record to
|
|
53
54
|
`<repoRoot>/.cache/distill/logs/runs.jsonl` (schema below).
|
|
54
55
|
5. Exit 0 (ok) or 3 (claude transport failure).
|
|
55
56
|
|
|
57
|
+
### Model selection
|
|
58
|
+
|
|
59
|
+
The sweep passes `--model claude-opus-4-7` explicitly (operator ruling
|
|
60
|
+
2026-07-29). Before the pin, no `--model` was passed and the Librarian
|
|
61
|
+
inherited whatever the host CLI defaulted to -- so the model running the
|
|
62
|
+
sweep was an accident of host configuration, and a host-side default bump
|
|
63
|
+
silently changed sweep behaviour across the estate. The pin makes the
|
|
64
|
+
choice deliberate. The reasoning behind 4.7 specifically: it follows
|
|
65
|
+
briefs better than 4.8/5 in estate experience, and brief-following
|
|
66
|
+
matters more for a scripted sweep than raw capability.
|
|
67
|
+
|
|
68
|
+
Override per host or per run with the `DISTILL_MODEL` environment
|
|
69
|
+
variable:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
DISTILL_MODEL=claude-opus-4-8 pnpm exec distill run
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
An empty or whitespace-only `DISTILL_MODEL` is treated as unset. The
|
|
76
|
+
dry-run plan reports the effective model and whether it came from the
|
|
77
|
+
pinned default or the override.
|
|
78
|
+
|
|
56
79
|
### Librarian definition resolution
|
|
57
80
|
|
|
58
81
|
The system prompt appended to the sweep session is resolved in this
|
|
@@ -77,11 +77,30 @@ export function buildDistillPrompt({
|
|
|
77
77
|
*/
|
|
78
78
|
export const DISTILL_PROMPT = buildDistillPrompt();
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
// The model the distillation sweep runs on, pinned explicitly (Baz ruling
|
|
81
|
+
// 2026-07-29). Before this pin, claudeArgs passed no `--model`, so the
|
|
82
|
+
// Librarian inherited whatever the host CLI happened to default to -- the
|
|
83
|
+
// model choice was an accident of host config, and a host-side default bump
|
|
84
|
+
// silently changed sweep behaviour. The pin makes the choice deliberate:
|
|
85
|
+
// claude-opus-4-7 follows briefs better than 4.8/5 in estate experience,
|
|
86
|
+
// which matters more for a scripted sweep than raw capability.
|
|
87
|
+
//
|
|
88
|
+
// Override per host or per run via the DISTILL_MODEL environment variable
|
|
89
|
+
// (documented in docs/tools/distill.md).
|
|
90
|
+
export const DEFAULT_DISTILL_MODEL = 'claude-opus-4-7';
|
|
91
|
+
|
|
92
|
+
export function resolveDistillModel(env = process.env) {
|
|
93
|
+
const override = env.DISTILL_MODEL;
|
|
94
|
+
if (typeof override === 'string' && override.trim() !== '') return override.trim();
|
|
95
|
+
return DEFAULT_DISTILL_MODEL;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function claudeArgs({ sessionName, librarianDef, prompt = DISTILL_PROMPT, model = resolveDistillModel() }) {
|
|
81
99
|
// Positional prompt last. No shell interpolation — this array is
|
|
82
100
|
// handed directly to child_process.spawn.
|
|
83
101
|
return [
|
|
84
102
|
'--print',
|
|
103
|
+
'--model', model,
|
|
85
104
|
'--permission-mode', 'bypassPermissions',
|
|
86
105
|
'--no-session-persistence',
|
|
87
106
|
'--name', sessionName,
|
|
@@ -37,7 +37,7 @@ import { basename, join } from 'node:path';
|
|
|
37
37
|
|
|
38
38
|
import { resolvePrereqs } from './prereqs.mjs';
|
|
39
39
|
import { countStagedObservations } from './staging.mjs';
|
|
40
|
-
import { buildDistillPrompt, claudeArgs } from './prompt.mjs';
|
|
40
|
+
import { buildDistillPrompt, claudeArgs, resolveDistillModel } from './prompt.mjs';
|
|
41
41
|
|
|
42
42
|
function log(line, { stderr = process.stderr } = {}) {
|
|
43
43
|
stderr.write(`[distill] ${line}\n`);
|
|
@@ -172,7 +172,8 @@ export async function runDistill({
|
|
|
172
172
|
log(` operator supplement: ${supplementPlanLine}`, { stderr });
|
|
173
173
|
log(` composed total: ${librarianIdentity.length} bytes`, { stderr });
|
|
174
174
|
log(` prompt: <inline> (${prompt.length} bytes, --root ${repoRoot})`, { stderr });
|
|
175
|
-
log(
|
|
175
|
+
log(` model: ${resolveDistillModel()}${process.env.DISTILL_MODEL ? ' (DISTILL_MODEL override)' : ' (pinned default)'}`, { stderr });
|
|
176
|
+
log(` command: claude --print --model ${resolveDistillModel()} --permission-mode bypassPermissions \\`, { stderr });
|
|
176
177
|
log(` --no-session-persistence --name ${sessionName} \\`, { stderr });
|
|
177
178
|
log(' --append-system-prompt <librarian definition> \\', { stderr });
|
|
178
179
|
log(' <distill prompt as positional arg>', { stderr });
|
package/package.json
CHANGED