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

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 (35) hide show
  1. package/dist/engineering-pa-cli/src/pa/cli.mjs +24 -1
  2. package/dist/engineering-pa-core/docs/commands/onboard.md +81 -7
  3. package/dist/engineering-pa-core/src/distill/cli.mjs +65 -0
  4. package/dist/engineering-pa-core/src/distill/health.mjs +322 -0
  5. package/dist/engineering-pa-scaffold/package.json +3 -2
  6. package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +37 -5
  7. package/dist/engineering-pa-scaffold/src/commands/init.mjs +7 -1
  8. package/dist/engineering-pa-scaffold/src/generator.mjs +220 -11
  9. package/dist/engineering-pa-workspace/app/assets/theme.css +3 -2
  10. package/dist/engineering-pa-workspace/app/assets/theme.js +29 -6
  11. package/dist/engineering-pa-workspace/app/banner.js +14 -2
  12. package/dist/engineering-pa-workspace/app/delegations.html +1 -1
  13. package/dist/engineering-pa-workspace/app/home.html +1 -1
  14. package/dist/engineering-pa-workspace/app/index.html +1 -1
  15. package/dist/engineering-pa-workspace/app/knowledge-map.html +1 -1
  16. package/dist/engineering-pa-workspace/app/knowledge.html +1 -1
  17. package/dist/engineering-pa-workspace/app/notes.html +1 -1
  18. package/dist/engineering-pa-workspace/app/onboarding.html +1 -1
  19. package/dist/engineering-pa-workspace/app/profile.html +1 -1
  20. package/dist/engineering-pa-workspace/app/project-detail.html +1 -1
  21. package/dist/engineering-pa-workspace/app/projects.html +1 -1
  22. package/dist/engineering-pa-workspace/app/repos.html +1 -1
  23. package/dist/engineering-pa-workspace/app/schedules.html +1 -1
  24. package/dist/engineering-pa-workspace/app/secrets.html +1 -1
  25. package/dist/engineering-pa-workspace/app/staging.html +1 -1
  26. package/dist/engineering-pa-workspace/app/tasks.html +1 -1
  27. package/dist/engineering-pa-workspace/app/viewer.html +1 -1
  28. package/dist/engineering-pa-workspace/index.mjs +16 -0
  29. package/dist/engineering-pa-workspace/package.json +3 -2
  30. package/dist/engineering-pa-workspace/src/repo-root.mjs +26 -1
  31. package/dist/engineering-pa-workspace/src/server.mjs +4 -3
  32. package/dist/engineering-pa-workspace/src/workspace-page-contract.mjs +98 -0
  33. package/dist/engineering-pa-workspace/src/ws.mjs +2 -0
  34. package/docs/onboarding-solo.md +4 -4
  35. package/package.json +2 -1
@@ -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
 
@@ -37,7 +37,7 @@ cd $HOME/<pa-name>-instance
37
37
  pnpm exec pa session
38
38
  ```
39
39
 
40
- Inside the session, type `/onboard`. The PA verifies the runtime, checks the installed version against npm, asks you a few profile questions, offers an optional shell alias, offers to start the local workspace, and offers to wire up Atlassian (Jira, Confluence) if you use it. Every step is idempotent; re-run `/onboard` anytime to redo a specific step.
40
+ Inside the session, type `/onboard`. The PA verifies the runtime, checks the installed version against npm, asks you a few profile questions, offers an optional shell alias, offers to start the local workspace, installs and proves the librarian distillation sweep as a mandatory step, and offers to wire up Atlassian (Jira, Confluence) if you use it. Every step is idempotent; re-run `/onboard` anytime to redo a specific step. Onboarding does not complete until the librarian schedule is installed and proven. That is the one piece of scheduling every solo instance needs so staged observations get distilled into knowledge over time.
41
41
 
42
42
  ## 3. Alias (optional)
43
43
 
@@ -56,11 +56,11 @@ After that, typing `<pa-name>` from anywhere starts a session.
56
56
  - The read-only `desk` CLI: overview, today, projects, work.
57
57
  - A local workspace server on `http://localhost:4200` (start with `pnpm exec pa workspace start`).
58
58
  - A SIMPLE-mode local knowledge library: `pa knowledge search / related / domain / orphans / path / context`, seeded with the five base domains.
59
- - Self-learning loop primitives: `agents/<pa-name>/staging/`, `journal.md`, `next-session.md`, `spawn-log.json`, autonomy envelope, and the Librarian distillation sweep via `pnpm exec distill run`.
59
+ - Self-learning loop primitives: `agents/<pa-name>/staging/`, `journal.md`, `next-session.md`, `spawn-log.json`, autonomy envelope, and the Librarian distillation sweep, scaffolded as `schedules/<pa-name>-distillation-sweep.json` (every 4 hours) and installed by `/onboard` on the OS-native backend (launchd on macOS, cron on Linux). One immediate run is fired at install time so the ledger at `.cache/distill/logs/runs.jsonl` proves the schedule wiring end to end before onboarding completes.
60
60
  - The `research` CLI with the light / normal / deep session store persisted under `.cache/research/`.
61
61
  - Scheduler (cron on Linux, launchd on macOS): `pnpm exec scheduler --help`.
62
62
  - Secrets vault (SOPS + age) if you opt in: `pnpm exec secrets --help`.
63
- - `.claude/settings.json` with the `pa-coordinator-hook` wired in as a `PreToolUse` hook (inert until you export `PA_COORDINATOR_MODE=1`) and `deep-research` denied at the permission layer.
63
+ - `.claude/settings.json` with the `pa-coordinator-hook` wired in as a `PreToolUse` hook (inert until you export `PA_COORDINATOR_MODE=1`), a `SessionStart` hook that injects a live `distillationHealth` block at the top of every session (schedule state + last-run age + staging count), and `deep-research` denied at the permission layer.
64
64
  - Two slash commands out of the box: `/research` (persisted research sessions) and `/onboard` (this walkthrough).
65
65
  - Workspace-extension prompting: `operator/knowledge/platform-tooling/workspace-extensions.md` teaches your PA when to propose a workspace page and `pa-init workspace-extension <name>` scaffolds the HTML skeleton plus a `nav.json` entry. Add `--with-api` when the page needs a server route.
66
66
 
@@ -70,7 +70,7 @@ Your PA has the `rcf-lite-engineer` role baked in. It builds greenfield applicat
70
70
 
71
71
  ## What is not included in solo mode
72
72
 
73
- - **`SessionStart` boot-context hook.** In some estates a `SessionStart` hook injects a live workState block at the top of every session. That hook is not in this package. If you want it, write your own `SessionStart` hook in `.claude/settings.json`.
73
+ - **A full workState boot-context hook.** In some estates a `SessionStart` hook injects the live in-flight work items, active projects, and on-you list at the top of every session. Solo mode ships the narrower distillation-health hook (schedule + last-run + staging count) rather than a full workState block. If you want the broader one, extend `.claude/settings.json` with your own `SessionStart` hook.
74
74
  - **Team modes.** `pa-init --mode=lead` and `--mode=member` exit with "not implemented". The SIMPLE-mode local library is single-operator by design.
75
75
  - **Remote knowledge library.** The `remote` library mode requires a service on a persistent host, an age keypair, and a tunnel. Solo mode stays SIMPLE and runs entirely on your one laptop.
76
76
  - **HQ-specialised roles.** The bundled role set is the core seven plus `product-engineer` and `rcf-lite-engineer` from the engineering types pack. Other tier-ladder roles and estate-specific extensions are not shipped.
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.49",
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"