@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
- // Roster path: <repoRoot>/operator/knowledge/hr/roster/<name>.json
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
- export async function loadIdentity(repoRoot, agentName) {
10
- const path = join(repoRoot, 'operator', 'knowledge', 'hr', 'roster', `${agentName}.json`);
11
- let raw;
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
- raw = await readFile(path, 'utf8');
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 the librarian definition as an appended system
50
- prompt and the distillation instructions as the positional prompt.
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
- export function claudeArgs({ sessionName, librarianDef, prompt = DISTILL_PROMPT }) {
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(' command: claude --print --permission-mode bypassPermissions \\', { stderr });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stravica-ai/digital-operator-pa",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Digital Operator PA — single-bundled public package surface (bins + library) for the platform.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",