@stravica-ai/digital-operator-pa 0.1.4 → 0.1.6
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-core/src/desk/team-scope.mjs +63 -14
- package/dist/engineering-pa-scaffold/src/generator.mjs +5 -0
- package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +1 -1
- package/dist/engineering-pa-workspace/app/index.html +4 -4
- package/dist/engineering-pa-workspace/src/nav.mjs +7 -3
- package/package.json +1 -1
|
@@ -106,6 +106,59 @@ export function filterProjectsByTeam(projects, opts = {}) {
|
|
|
106
106
|
* Absent/blank -> null, which means "do not team-scope nav" (fail open,
|
|
107
107
|
* backwards compatible: pre-team devices show every nav fragment).
|
|
108
108
|
*/
|
|
109
|
+
/**
|
|
110
|
+
* Read + JSON-parse a file, returning the parsed value or null on any error
|
|
111
|
+
* (missing file, unreadable, malformed JSON). Shared by the base + overlay
|
|
112
|
+
* reads so both fail-open identically.
|
|
113
|
+
*/
|
|
114
|
+
function readJsonSync(path) {
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function readJsonAsync(path) {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Shallow-merge the optional device-local overlay
|
|
132
|
+
* (operator/config/preferences.local.json) OVER the parsed base
|
|
133
|
+
* preferences.json.
|
|
134
|
+
*
|
|
135
|
+
* The overlay is device-local state that must NOT round-trip through
|
|
136
|
+
* library-sync. preferences.json is down-path-materialised from a shared
|
|
137
|
+
* canonical source (see the estate's .library-sync manifest — the barry
|
|
138
|
+
* `config/` mount), so a device-scoped field (e.g. `team`) written into the
|
|
139
|
+
* synced file would be clobbered on the next cycle, and a canonical edit
|
|
140
|
+
* would stamp one device's identity onto every device. The overlay lives
|
|
141
|
+
* outside every mount source, so down-path reconciliation never tracks or
|
|
142
|
+
* deletes it (computeDeletions is manifest-driven — it only removes
|
|
143
|
+
* previously-materialised files), and it is gitignored so estate git-sync
|
|
144
|
+
* never carries it between hosts (see scaffold buildGitignore).
|
|
145
|
+
*
|
|
146
|
+
* Precedence: overlay keys win; keys the overlay omits fall through to the
|
|
147
|
+
* base. An absent/malformed overlay leaves the base exactly as-is, so a
|
|
148
|
+
* device with no overlay behaves identically to pre-overlay releases.
|
|
149
|
+
*
|
|
150
|
+
* `base`/`overlay` are already-parsed values (a plain object, or null when
|
|
151
|
+
* the file was missing/malformed). Returns null only when BOTH are absent,
|
|
152
|
+
* preserving the "no config at all -> fail-open defaults" path through
|
|
153
|
+
* normaliseTeamScopeConfig.
|
|
154
|
+
*/
|
|
155
|
+
function mergePreferences(base, overlay) {
|
|
156
|
+
const b = base && typeof base === 'object' && !Array.isArray(base) ? base : null;
|
|
157
|
+
const o = overlay && typeof overlay === 'object' && !Array.isArray(overlay) ? overlay : null;
|
|
158
|
+
if (!b && !o) return null;
|
|
159
|
+
return { ...(b || {}), ...(o || {}) };
|
|
160
|
+
}
|
|
161
|
+
|
|
109
162
|
function normaliseTeamScopeConfig(config) {
|
|
110
163
|
const c = config && typeof config === 'object' ? config : {};
|
|
111
164
|
const paName = c.paName || c.agentName || 'pa';
|
|
@@ -136,13 +189,11 @@ function normaliseTeamScopeConfig(config) {
|
|
|
136
189
|
* @returns {Promise<{ paName: string, teamIdentities: string[], scopeEnabled: boolean, team: string|null }>}
|
|
137
190
|
*/
|
|
138
191
|
export async function readTeamScopeConfig(repoRoot) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
return normaliseTeamScopeConfig(config);
|
|
192
|
+
// Base (synced) preferences, then the device-local overlay merged OVER it.
|
|
193
|
+
// Missing/malformed either file -> null for that layer -> defaults.
|
|
194
|
+
const base = await readJsonAsync(join(repoRoot, 'operator', 'config', 'preferences.json'));
|
|
195
|
+
const overlay = await readJsonAsync(join(repoRoot, 'operator', 'config', 'preferences.local.json'));
|
|
196
|
+
return normaliseTeamScopeConfig(mergePreferences(base, overlay));
|
|
146
197
|
}
|
|
147
198
|
|
|
148
199
|
/**
|
|
@@ -156,13 +207,11 @@ export async function readTeamScopeConfig(repoRoot) {
|
|
|
156
207
|
* @returns {{ paName: string, teamIdentities: string[], scopeEnabled: boolean, team: string|null }}
|
|
157
208
|
*/
|
|
158
209
|
export function readTeamScopeConfigSync(repoRoot) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
return normaliseTeamScopeConfig(config);
|
|
210
|
+
// Base (synced) preferences, then the device-local overlay merged OVER it.
|
|
211
|
+
// Missing/malformed either file -> null for that layer -> defaults.
|
|
212
|
+
const base = readJsonSync(join(repoRoot, 'operator', 'config', 'preferences.json'));
|
|
213
|
+
const overlay = readJsonSync(join(repoRoot, 'operator', 'config', 'preferences.local.json'));
|
|
214
|
+
return normaliseTeamScopeConfig(mergePreferences(base, overlay));
|
|
166
215
|
}
|
|
167
216
|
|
|
168
217
|
/**
|
|
@@ -157,6 +157,11 @@ export function buildGitignore() {
|
|
|
157
157
|
'# Workspace runtime',
|
|
158
158
|
'.ws.pid',
|
|
159
159
|
'',
|
|
160
|
+
'# Device-local preferences overlay — per-host identity (e.g. `team`)',
|
|
161
|
+
'# shallow-merged OVER the synced preferences.json. Device-local by',
|
|
162
|
+
'# nature, so it is never committed or synced between hosts.',
|
|
163
|
+
'operator/config/preferences.local.json',
|
|
164
|
+
'',
|
|
160
165
|
].join('\n');
|
|
161
166
|
}
|
|
162
167
|
|
|
@@ -184,7 +184,7 @@ Stage observations, update journal, update work items, update spawn-log outcomes
|
|
|
184
184
|
| Operator knowledge | `operator/knowledge/` |
|
|
185
185
|
| Platform core docs | `@stravica-ai/engineering-pa-core/docs/` |
|
|
186
186
|
| Platform core knowledge (bucket scaffolds) | `@stravica-ai/engineering-pa-core/knowledge/` |
|
|
187
|
-
| Config / preferences | `operator/config/preferences.json` |
|
|
187
|
+
| Config / preferences | `operator/config/preferences.json` (+ device-local `preferences.local.json` overlay) |
|
|
188
188
|
| PA run folder | `agents/{PA_NAME}/` |
|
|
189
189
|
| Autonomy envelope | `agents/{PA_NAME}/autonomy-envelope.md` |
|
|
190
190
|
| Work items | `work/w-YYYY-MM-DD-NNN.json` |
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>
|
|
6
|
+
<title>Digital Operator</title>
|
|
7
7
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%236366f1'/%3E%3Ctext x='8' y='12' font-family='monospace' font-size='11' font-weight='bold' text-anchor='middle' fill='%23121218'%3ED%3C/text%3E%3C/svg%3E">
|
|
8
8
|
<script src="/banner.js" defer></script>
|
|
9
9
|
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
@@ -347,14 +347,14 @@
|
|
|
347
347
|
var name = data.paName;
|
|
348
348
|
var capName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
349
349
|
brandName.textContent = capName;
|
|
350
|
-
document.title = capName + ' —
|
|
350
|
+
document.title = capName + ' — Digital Operator';
|
|
351
351
|
} else {
|
|
352
352
|
brandName.textContent = 'Workspace';
|
|
353
353
|
}
|
|
354
|
-
brandSub.textContent = '
|
|
354
|
+
brandSub.textContent = 'Digital Operator';
|
|
355
355
|
}).catch(function() {
|
|
356
356
|
document.getElementById('brand-name').textContent = 'Workspace';
|
|
357
|
-
document.getElementById('brand-subtitle').textContent = '
|
|
357
|
+
document.getElementById('brand-subtitle').textContent = 'Digital Operator';
|
|
358
358
|
});
|
|
359
359
|
</script>
|
|
360
360
|
</body>
|
|
@@ -33,9 +33,13 @@
|
|
|
33
33
|
*
|
|
34
34
|
* Team scoping (display)
|
|
35
35
|
* ----------------------
|
|
36
|
-
* The device declares its team via `
|
|
37
|
-
*
|
|
38
|
-
* `
|
|
36
|
+
* The device declares its team via the `team` key in
|
|
37
|
+
* `operator/config/preferences.json` — or, preferred, the device-local
|
|
38
|
+
* `operator/config/preferences.local.json` overlay shallow-merged over it
|
|
39
|
+
* (device identity is device-local, and preferences.json is synced from a
|
|
40
|
+
* shared canonical source; see readTeamScopeConfig in team-scope.mjs). It is
|
|
41
|
+
* resolved by the server through readTeamScopeConfigSync and passed in as
|
|
42
|
+
* `opts.team`. The default scoped view keeps entries whose team is the
|
|
39
43
|
* device team OR untagged (null). Untagged-always-shown keeps core nav and
|
|
40
44
|
* shared/company pages visible everywhere and makes the whole mechanism
|
|
41
45
|
* backwards compatible: a device with no `team` configured, or fragments
|
package/package.json
CHANGED