@stravica-ai/digital-operator-pa 0.1.47 → 0.1.48

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 (29) hide show
  1. package/dist/engineering-pa-scaffold/package.json +3 -2
  2. package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +37 -5
  3. package/dist/engineering-pa-scaffold/src/commands/init.mjs +7 -1
  4. package/dist/engineering-pa-workspace/app/assets/theme.css +3 -2
  5. package/dist/engineering-pa-workspace/app/assets/theme.js +29 -6
  6. package/dist/engineering-pa-workspace/app/banner.js +14 -2
  7. package/dist/engineering-pa-workspace/app/delegations.html +1 -1
  8. package/dist/engineering-pa-workspace/app/home.html +1 -1
  9. package/dist/engineering-pa-workspace/app/index.html +1 -1
  10. package/dist/engineering-pa-workspace/app/knowledge-map.html +1 -1
  11. package/dist/engineering-pa-workspace/app/knowledge.html +1 -1
  12. package/dist/engineering-pa-workspace/app/notes.html +1 -1
  13. package/dist/engineering-pa-workspace/app/onboarding.html +1 -1
  14. package/dist/engineering-pa-workspace/app/profile.html +1 -1
  15. package/dist/engineering-pa-workspace/app/project-detail.html +1 -1
  16. package/dist/engineering-pa-workspace/app/projects.html +1 -1
  17. package/dist/engineering-pa-workspace/app/repos.html +1 -1
  18. package/dist/engineering-pa-workspace/app/schedules.html +1 -1
  19. package/dist/engineering-pa-workspace/app/secrets.html +1 -1
  20. package/dist/engineering-pa-workspace/app/staging.html +1 -1
  21. package/dist/engineering-pa-workspace/app/tasks.html +1 -1
  22. package/dist/engineering-pa-workspace/app/viewer.html +1 -1
  23. package/dist/engineering-pa-workspace/index.mjs +16 -0
  24. package/dist/engineering-pa-workspace/package.json +3 -2
  25. package/dist/engineering-pa-workspace/src/repo-root.mjs +26 -1
  26. package/dist/engineering-pa-workspace/src/server.mjs +4 -3
  27. package/dist/engineering-pa-workspace/src/workspace-page-contract.mjs +98 -0
  28. package/dist/engineering-pa-workspace/src/ws.mjs +2 -0
  29. package/package.json +2 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stravica-ai/engineering-pa-scaffold",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "pa-init scaffolder + onboarding flows (solo, lead, member).",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -11,7 +11,8 @@
11
11
  "pa-init": "bin/pa-init"
12
12
  },
13
13
  "dependencies": {
14
- "@stravica-ai/engineering-pa-core": "workspace:*"
14
+ "@stravica-ai/engineering-pa-core": "workspace:*",
15
+ "@stravica-ai/engineering-pa-workspace": "workspace:*"
15
16
  },
16
17
  "private": true,
17
18
  "license": "MIT",
@@ -21,6 +21,18 @@ import { existsSync } from 'node:fs';
21
21
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
22
22
  import { dirname, isAbsolute, join, resolve } from 'node:path';
23
23
 
24
+ // Direct file import (not the bare package specifier) so the build.mjs
25
+ // import-rewriter produces a full-filename relative path — Node ESM won't
26
+ // resolve a directory-only path without a package.json main entry, and
27
+ // the bundled dist tree does not carry one for internal packages.
28
+ import {
29
+ BANNER_JS_TAG,
30
+ GLOBAL_BANNER_HTML,
31
+ THEME_CSS_LINK,
32
+ THEME_JS_TAG,
33
+ prepaintScriptTag,
34
+ } from '../../../engineering-pa-workspace/index.mjs';
35
+
24
36
  /** Kebab-case validator: 2-40 chars, lowercase letters + digits + hyphen, starts with a letter. */
25
37
  const NAME_RE = /^[a-z][a-z0-9-]{1,39}$/;
26
38
 
@@ -163,21 +175,41 @@ export async function mergeNavEntry({ navPath, name, label, section }) {
163
175
 
164
176
  /**
165
177
  * The static-page skeleton written for `<name>.html`. Kept minimal on
166
- * purpose: the operator's PA fills in the real content on the next
167
- * dispatch, and a heavier template would just be deleted.
178
+ * content but complete on chrome: an extension page carries the same
179
+ * head + top-of-body contract every core page ships, so a freshly
180
+ * scaffolded page renders in the operator's chosen theme and shows the
181
+ * global status banner from the first load.
182
+ *
183
+ * The head tags come from
184
+ * `@stravica-ai/engineering-pa-workspace/src/workspace-page-contract.mjs`
185
+ * so a scaffolded page and a core page can never drift on the theme
186
+ * storage key or the tag order. Previously this used a single
187
+ * `<link rel="stylesheet" href="/core/tools/workspace/app/shell.css" />`
188
+ * — that path never shipped, so every scaffolded page rendered
189
+ * unstyled and outside the theme system.
190
+ *
191
+ * Core pages still hand-copy the contract for now (their inline `<style>`
192
+ * blocks sit between the theme tags and `banner.js`, so a full migration
193
+ * is more than a mechanical substitution and is tracked as a follow-up).
194
+ * A parity test pins the tokens so a rename in either place fails fast.
168
195
  */
169
196
  export function buildHtmlSkeleton({ name, label }) {
197
+ const title = escapeHtml(label);
170
198
  return `<!doctype html>
171
199
  <html lang="en">
172
200
  <head>
173
201
  <meta charset="utf-8" />
174
202
  <meta name="viewport" content="width=device-width, initial-scale=1" />
175
- <title>${escapeHtml(label)}</title>
176
- <link rel="stylesheet" href="/core/tools/workspace/app/shell.css" />
203
+ <title>${title}</title>
204
+ ${prepaintScriptTag()}
205
+ ${THEME_CSS_LINK}
206
+ ${THEME_JS_TAG}
207
+ ${BANNER_JS_TAG}
177
208
  </head>
178
209
  <body>
210
+ ${GLOBAL_BANNER_HTML}
179
211
  <main class="page">
180
- <h1>${escapeHtml(label)}</h1>
212
+ <h1>${title}</h1>
181
213
  <!-- workspace-extension:${name} -->
182
214
  <p>Replace this with the real page. Data lives under <code>/__api/${name}/...</code> if you scaffolded the api stub.</p>
183
215
  </main>
@@ -112,6 +112,12 @@ export async function runInit(options) {
112
112
  */
113
113
  function writeNextStepMessage(config) {
114
114
  const { paName, targetDir } = config;
115
+ // Prose renders the PA name Title-Cased so a sentence starting with it
116
+ // reads naturally (e.g. "Arry walks you through..." rather than
117
+ // "arry walks you through..."). The shell-alias lines still use the
118
+ // lowercase kebab-case identifier because that's what gets typed at a
119
+ // shell prompt.
120
+ const paTitle = paName.length ? paName[0].toUpperCase() + paName.slice(1) : paName;
115
121
  const lines = [
116
122
  '',
117
123
  'Next step, start your PA:',
@@ -119,7 +125,7 @@ function writeNextStepMessage(config) {
119
125
  ` cd ${targetDir}`,
120
126
  ' pnpm exec pa session',
121
127
  '',
122
- `Inside the session, type \`/onboard\`. ${paName} walks you through the`,
128
+ `Inside the session, type \`/onboard\`. ${paTitle} walks you through the`,
123
129
  'remaining verification and optional setup (runtime check, version check,',
124
130
  'profile intake, alias, workspace, Atlassian). Every step is idempotent.',
125
131
  '',
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Two themes live here: dark (default) and light, toggled by the operator
5
5
  * via the banner button. Selection is persisted in localStorage under
6
- * `dave.theme` and applied to `documentElement.dataset.theme` before first
7
- * paint by the inline init script in each page's <head>.
6
+ * `dop.theme` (product-generic; legacy `dave.theme` is read once and
7
+ * migrated forward) and applied to `documentElement.dataset.theme` before
8
+ * first paint by the inline init script in each page's <head>.
8
9
  *
9
10
  * Token set is the union of every page's prior `:root` block. Both themes
10
11
  * MUST define the same set of names — any unilateral token becomes invisible
@@ -4,7 +4,7 @@
4
4
  * Two responsibilities split across two surfaces:
5
5
  *
6
6
  * 1. Pre-paint init. Each page also embeds a tiny inline <script> in <head>
7
- * that reads `localStorage.dave.theme` and sets
7
+ * that reads `localStorage.dop.theme` and sets
8
8
  * `documentElement.dataset.theme` BEFORE first paint. That avoids a
9
9
  * light-flash on a dark-preference user (FOUC). The inline script is the
10
10
  * duplicate of `applyStoredTheme()` below; keeping it inline is the
@@ -18,7 +18,17 @@
18
18
  * context (matching banner.js's behaviour) so a page opened directly
19
19
  * still owns its own toggle.
20
20
  *
21
- * Storage: localStorage key `dave.theme`, values 'dark' | 'light'.
21
+ * Storage: localStorage key `dop.theme`, values 'dark' | 'light'. The
22
+ * canonical token lives in `src/workspace-page-contract.mjs`; this file
23
+ * hand-copies the literal because it runs in the browser and cannot
24
+ * import ESM modules from that path. The theme parity test pins the two
25
+ * copies to the same value.
26
+ *
27
+ * Legacy: `dave.theme` is the pre-rename key. On first boot after the
28
+ * rename we read it once, copy the value to `dop.theme`, and never touch
29
+ * it again — nobody re-picks their theme just because the estate went
30
+ * product-generic. See `src/workspace-page-contract.mjs`.
31
+ *
22
32
  * Default when unset: 'dark' (operator preference — see CLAUDE.md). We do
23
33
  * NOT consult prefers-color-scheme; dark is the chosen default and we don't
24
34
  * want auto-detection overriding it on first load.
@@ -26,15 +36,22 @@
26
36
  (function () {
27
37
  'use strict';
28
38
 
29
- var STORAGE_KEY = 'dave.theme';
39
+ var STORAGE_KEY = 'dop.theme';
40
+ var LEGACY_STORAGE_KEY = 'dave.theme';
30
41
  var DEFAULT_THEME = 'dark';
31
42
  var VALID = { dark: 1, light: 1 };
32
- var MESSAGE_TYPE = 'dave.theme.set';
43
+ var MESSAGE_TYPE = 'dop.theme.set';
33
44
 
34
45
  function readStored() {
35
46
  try {
36
47
  var raw = localStorage.getItem(STORAGE_KEY);
37
48
  if (raw && VALID[raw]) return raw;
49
+ // Legacy fallback: one-time migration from the pre-rename key.
50
+ var legacy = localStorage.getItem(LEGACY_STORAGE_KEY);
51
+ if (legacy && VALID[legacy]) {
52
+ try { localStorage.setItem(STORAGE_KEY, legacy); } catch (_e) { /* no-op */ }
53
+ return legacy;
54
+ }
38
55
  } catch (_e) { /* storage unavailable — fall through to default */ }
39
56
  return DEFAULT_THEME;
40
57
  }
@@ -148,12 +165,18 @@
148
165
  boot();
149
166
  }
150
167
 
151
- // Expose for tests + the inline init helper.
152
- window.__daveTheme = {
168
+ // Expose for tests + the inline init helper. `__daveTheme` is kept as an
169
+ // alias for anything under `operator/extensions/**` that grew a direct
170
+ // reference before the rename; `__dopTheme` is the canonical name and
171
+ // is what new callers should use.
172
+ var api = {
153
173
  read: readStored,
154
174
  write: writeStored,
155
175
  apply: applyTheme,
156
176
  storageKey: STORAGE_KEY,
177
+ legacyStorageKey: LEGACY_STORAGE_KEY,
157
178
  defaultTheme: DEFAULT_THEME,
158
179
  };
180
+ window.__dopTheme = api;
181
+ window.__daveTheme = api;
159
182
  })();
@@ -22,7 +22,11 @@
22
22
 
23
23
  const ENDPOINT = '/__api/status-mon';
24
24
  const POLL_INTERVAL_MS = 30_000;
25
- const DISMISSED_KEY = 'dave.statusmon.dismissed';
25
+ // Product-generic key; the pre-rename store was 'dave.statusmon.dismissed'.
26
+ // Read-once fallback keeps existing dismissals across the rename so a
27
+ // resolved incident someone had already dismissed doesn't reappear.
28
+ const DISMISSED_KEY = 'dop.statusmon.dismissed';
29
+ const LEGACY_DISMISSED_KEY = 'dave.statusmon.dismissed';
26
30
 
27
31
  const SLOT_ID = 'global-banner';
28
32
 
@@ -36,7 +40,15 @@
36
40
 
37
41
  function readDismissed() {
38
42
  try {
39
- const raw = localStorage.getItem(DISMISSED_KEY);
43
+ var raw = localStorage.getItem(DISMISSED_KEY);
44
+ if (!raw) {
45
+ // Legacy fallback: read the pre-rename store once and migrate.
46
+ var legacy = localStorage.getItem(LEGACY_DISMISSED_KEY);
47
+ if (legacy) {
48
+ try { localStorage.setItem(DISMISSED_KEY, legacy); } catch { /* no-op */ }
49
+ raw = legacy;
50
+ }
51
+ }
40
52
  if (!raw) return [];
41
53
  const parsed = JSON.parse(raw);
42
54
  if (!Array.isArray(parsed)) return [];
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Delegations — Workspace</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Home</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -6,7 +6,7 @@
6
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
- <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>
9
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
10
10
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
11
11
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
12
12
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Knowledge Map — Workspace</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Knowledge</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Notes — Dave</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Welcome to your PA</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Profile</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Project Detail</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Projects — Dave</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Git Overview — Workspace</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Schedules — Workspace</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Secrets</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Staging Inbox — Workspace</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Tasks — Dave</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Document Viewer</title>
7
- <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>
7
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
8
8
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
9
9
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
10
10
  <style>
@@ -10,3 +10,19 @@ export const version = '0.1.1';
10
10
  export { loadNav } from './src/nav.mjs';
11
11
  export { startPoller as startStatusMonPoller } from './src/status-mon/poller.mjs';
12
12
  export { handleApi } from './src/api.mjs';
13
+
14
+ // Shared workspace-page contract — every core page and every scaffolded
15
+ // extension page carries the same head + top-of-body markup. Owning the
16
+ // tokens here stops the scaffold and the core pages drifting on the theme
17
+ // storage key.
18
+ export {
19
+ THEME_STORAGE_KEY,
20
+ THEME_LEGACY_STORAGE_KEY,
21
+ THEME_MESSAGE_TYPE,
22
+ THEME_CSS_LINK,
23
+ THEME_JS_TAG,
24
+ BANNER_JS_TAG,
25
+ GLOBAL_BANNER_HTML,
26
+ prepaintScriptTag,
27
+ standardHeadInner,
28
+ } from './src/workspace-page-contract.mjs';
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@stravica-ai/engineering-pa-workspace",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Workspace server + viewer — operator-facing local web UI for the PA platform.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
7
7
  "exports": {
8
- ".": "./index.mjs"
8
+ ".": "./index.mjs",
9
+ "./index.mjs": "./index.mjs"
9
10
  },
10
11
  "dependencies": {
11
12
  "@stravica-ai/engineering-pa-cli": "workspace:*",
@@ -55,14 +55,30 @@ export function walkUpForMarker(start) {
55
55
  return null;
56
56
  }
57
57
 
58
+ // Process-level guard so the fallback warning fires at most once per
59
+ // process, no matter how many callers hit the resolver. Tests can reset
60
+ // via the exported `_resetFallbackWarningForTests` helper below.
61
+ let _fallbackWarned = false;
62
+
63
+ /** Test-only reset for the once-per-process fallback warning latch. */
64
+ export function _resetFallbackWarningForTests() {
65
+ _fallbackWarned = false;
66
+ }
67
+
58
68
  /**
59
69
  * Resolve the operator instance root. Deterministic and synchronous; safe
60
70
  * to call at module top-level. See the header comment for the full
61
71
  * precedence rule.
62
72
  *
73
+ * When no marker is found AND no env override is set, emit ONE stderr
74
+ * warning naming the cwd we're falling back to and the two overrides that
75
+ * would silence it. The walk still returns cwd so pre-scaffold and dry-run
76
+ * flows keep working — the warning is a diagnostic, not an error.
77
+ *
63
78
  * @param {object} [opts]
64
79
  * @param {string} [opts.startDir=process.cwd()] Directory to start the walk from.
65
80
  * @param {NodeJS.ProcessEnv} [opts.env=process.env] Env source (tests inject).
81
+ * @param {(msg: string) => void} [opts.warn] Optional warn sink (tests inject).
66
82
  * @returns {string} Absolute path.
67
83
  */
68
84
  export function findRepoRootSync(opts = {}) {
@@ -71,5 +87,14 @@ export function findRepoRootSync(opts = {}) {
71
87
  const envRoot = env.DAVE_REPO_ROOT || env.PA_REPO_ROOT;
72
88
  if (envRoot) return resolve(envRoot);
73
89
  const found = walkUpForMarker(startDir);
74
- return found || resolve(startDir);
90
+ if (found) return found;
91
+ const fallback = resolve(startDir);
92
+ if (!_fallbackWarned) {
93
+ _fallbackWarned = true;
94
+ const warn = opts.warn || ((m) => process.stderr.write(m + '\n'));
95
+ warn(
96
+ `workspace: no instance marker (.git, operator/config/preferences.json, pnpm-workspace.yaml) found walking up from ${fallback}; falling back to cwd. Set DAVE_REPO_ROOT or PA_REPO_ROOT to silence.`,
97
+ );
98
+ }
99
+ return fallback;
75
100
  }
@@ -218,8 +218,9 @@ function resetIdle() {
218
218
  // palette block, which broke the theme toggle on the content-calendar
219
219
  // preview iframe (operator-reported, 2026-06-27). The fix matches what
220
220
  // viewer.html does: link the shared theme.css, defer-load theme.js, pre-paint
221
- // init reads localStorage 'dave.theme', body in humanist sans for prose, and
222
- // .md-body strong uses the new --emphasis token (matching .content strong).
221
+ // init reads localStorage 'dop.theme' (with a one-time legacy 'dave.theme'
222
+ // fallback), body in humanist sans for prose, and .md-body strong uses the
223
+ // new --emphasis token (matching .content strong).
223
224
  // Exported so unit tests can assert on the output without spinning up the
224
225
  // HTTP server.
225
226
  export function markdownPage(rawContent, filePath) {
@@ -234,7 +235,7 @@ export function markdownPage(rawContent, filePath) {
234
235
  <meta charset="UTF-8">
235
236
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
236
237
  <title>${title}</title>
237
- <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>
238
+ <script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
238
239
  <link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
239
240
  <script src="/core/tools/workspace/app/assets/theme.js" defer></script>
240
241
  <style>
@@ -0,0 +1,98 @@
1
+ // workspace-page-contract.mjs — the single source of truth for the head
2
+ // and top-of-body markup every workspace HTML page must ship.
3
+ //
4
+ // Every page served by the workspace (core pages under `packages/workspace/app/`
5
+ // and extension pages scaffolded by `pa-init workspace-extension`) has to
6
+ // carry the same four head tags in the same order:
7
+ //
8
+ // 1. Inline pre-paint script that reads the theme from localStorage and
9
+ // sets `documentElement.dataset.theme` BEFORE first paint. Without
10
+ // this, dark-preference users see a light-flash (FOUC).
11
+ // 2. Link to `/core/tools/workspace/app/assets/theme.css` (the shared
12
+ // palette + `[data-theme=...]` token blocks).
13
+ // 3. Deferred `/core/tools/workspace/app/assets/theme.js` (toggle button,
14
+ // cross-frame coordination, storage sync).
15
+ // 4. Deferred `/banner.js` (the global status banner poller).
16
+ //
17
+ // And the body's first child must be `<div id="global-banner"></div>` so
18
+ // `banner.js` has a mount point.
19
+ //
20
+ // This module owns the string forms of all four so a scaffold-generated
21
+ // extension page and a hand-authored core page can never drift. Core pages
22
+ // still hand-copy the block for now (their inline `<style>` sits after
23
+ // theme.js in the head, so a full migration is more than a mechanical
24
+ // substitution and is left as a follow-up); the scaffold and the theme
25
+ // parity test import from here.
26
+ //
27
+ // Storage-key policy: the theme key is `'dop.theme'` (product-generic —
28
+ // `dop` = Digital Operator PA). Legacy stores under `'dave.theme'` are
29
+ // migrated on first read (see `theme.js`) so nobody re-picks their theme
30
+ // after the rename.
31
+
32
+ /**
33
+ * localStorage key the theme selection persists under. Product-generic;
34
+ * shared by the pre-paint bootstrap, theme.js, and every consuming page.
35
+ */
36
+ export const THEME_STORAGE_KEY = 'dop.theme';
37
+
38
+ /**
39
+ * Legacy localStorage key. Read once on first boot when the new key is
40
+ * absent and copied forward. Never written. Kept exported so tests can
41
+ * pin the migration path without hardcoding the literal.
42
+ */
43
+ export const THEME_LEGACY_STORAGE_KEY = 'dave.theme';
44
+
45
+ /**
46
+ * postMessage type the shell uses to broadcast a theme change into
47
+ * same-origin iframed pages. Product-generic; renamed alongside the
48
+ * storage key.
49
+ */
50
+ export const THEME_MESSAGE_TYPE = 'dop.theme.set';
51
+
52
+ /**
53
+ * The inline pre-paint bootstrap. Reads the new key, falls back to the
54
+ * legacy key when the new one is unset, applies to
55
+ * `documentElement.dataset.theme` before first paint. Same shape the
56
+ * markdownPage renderer emits so a fresh direct-load never flashes.
57
+ *
58
+ * Returned as a self-contained `<script>...</script>` tag; embed it in
59
+ * `<head>` BEFORE the theme.css link and before any body content.
60
+ */
61
+ export function prepaintScriptTag() {
62
+ return `<script>(function(){try{var t=localStorage.getItem('${THEME_STORAGE_KEY}');if(!t){var legacy=localStorage.getItem('${THEME_LEGACY_STORAGE_KEY}');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('${THEME_STORAGE_KEY}',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>`;
63
+ }
64
+
65
+ /** Link tag for the shared theme stylesheet. */
66
+ export const THEME_CSS_LINK = '<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">';
67
+
68
+ /** Deferred script tag for the shared theme.js toggle + storage coordinator. */
69
+ export const THEME_JS_TAG = '<script src="/core/tools/workspace/app/assets/theme.js" defer></script>';
70
+
71
+ /** Deferred script tag for the global status banner. */
72
+ export const BANNER_JS_TAG = '<script src="/banner.js" defer></script>';
73
+
74
+ /**
75
+ * The mount point banner.js expects. Insert as the first child of `<body>`.
76
+ */
77
+ export const GLOBAL_BANNER_HTML = '<div id="global-banner"></div>';
78
+
79
+ /**
80
+ * Build the standard `<head>` inner markup a workspace page must ship.
81
+ * Charset, viewport, `<title>`, then the four theme + banner tags in the
82
+ * documented order. Callers append their own `<style>` block (if any) AFTER
83
+ * the returned string and BEFORE closing `</head>`.
84
+ *
85
+ * @param {object} opts
86
+ * @param {string} opts.title page title (already HTML-escaped by caller)
87
+ */
88
+ export function standardHeadInner({ title }) {
89
+ return [
90
+ '<meta charset="utf-8" />',
91
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
92
+ `<title>${title}</title>`,
93
+ prepaintScriptTag(),
94
+ THEME_CSS_LINK,
95
+ THEME_JS_TAG,
96
+ BANNER_JS_TAG,
97
+ ].join('\n');
98
+ }
@@ -336,6 +336,7 @@ async function cmdStart() {
336
336
  const who = holder ? ` (held by PID ${holder.pid}${holder.command ? ` — ${holder.command}` : ''})` : '';
337
337
  console.error(`Error: Stale PID file references dead PID ${existing.pid}, but port ${existing.port} is in use${who}.`);
338
338
  console.error('Refusing to kill an unrelated process. Remove the PID file manually or use a different port.');
339
+ console.error('Hint: run `pa workspace start --port <N>` with a free port, or set `workspacePort` in operator/config/preferences.json.');
339
340
  process.exit(1);
340
341
  }
341
342
  removePid();
@@ -344,6 +345,7 @@ async function cmdStart() {
344
345
  // Check if port is already in use by something else
345
346
  if (await checkPort(port)) {
346
347
  console.error(`Error: Port ${port} is already in use by another process.`);
348
+ console.error('Hint: run `pa workspace start --port <N>` with a free port, or set `workspacePort` in operator/config/preferences.json.');
347
349
  process.exit(1);
348
350
  }
349
351
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stravica-ai/digital-operator-pa",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
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",
@@ -40,6 +40,7 @@
40
40
  ],
41
41
  "license": "MIT",
42
42
  "homepage": "https://stravica.ai",
43
+ "bugs": "https://stravica.ai",
43
44
  "publishConfig": {
44
45
  "registry": "https://registry.npmjs.org/",
45
46
  "access": "public"