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

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 (30) hide show
  1. package/dist/engineering-pa-cli/src/pa/cli.mjs +47 -1
  2. package/dist/engineering-pa-cli/src/pa/onboard.mjs +114 -0
  3. package/dist/engineering-pa-core/docs/commands/onboard.md +20 -20
  4. package/dist/engineering-pa-core/src/distill/health.mjs +170 -6
  5. package/dist/engineering-pa-core/src/distill/run.mjs +19 -0
  6. package/dist/engineering-pa-core/src/instance-root/find.mjs +104 -0
  7. package/dist/engineering-pa-core/src/knowledge/retrieval/backlink-indexer.mjs +6 -1
  8. package/dist/engineering-pa-core/src/knowledge/retrieval/graph-builder.mjs +6 -1
  9. package/dist/engineering-pa-core/src/knowledge/retrieval/index-builder.mjs +4 -1
  10. package/dist/engineering-pa-core/src/knowledge/retrieval/query.mjs +6 -1
  11. package/dist/engineering-pa-core/src/scheduler/install.mjs +41 -4
  12. package/dist/engineering-pa-core/src/scheduler/list.mjs +34 -13
  13. package/dist/engineering-pa-core/src/scheduler/remove.mjs +2 -1
  14. package/dist/engineering-pa-core-extensions-atlassian/package.json +3 -0
  15. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-folder.mjs +4 -2
  16. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/link.mjs +4 -2
  17. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sync-statuses.mjs +6 -2
  18. package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-chains.mjs +7 -3
  19. package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-map.mjs +9 -3
  20. package/dist/engineering-pa-scaffold/src/generator.mjs +3 -1
  21. package/dist/engineering-pa-workspace/app/notes.html +1 -1
  22. package/dist/engineering-pa-workspace/app/project-detail.html +17 -1
  23. package/dist/engineering-pa-workspace/app/projects.html +1 -1
  24. package/dist/engineering-pa-workspace/app/tasks.html +1 -1
  25. package/dist/engineering-pa-workspace/src/install-daemon.mjs +2 -2
  26. package/dist/engineering-pa-workspace/src/repo-root.mjs +13 -100
  27. package/dist/engineering-pa-workspace/src/server.mjs +2 -2
  28. package/dist/engineering-pa-workspace/src/ws.mjs +79 -10
  29. package/docs/onboarding-solo.md +1 -1
  30. package/package.json +1 -1
@@ -9,11 +9,14 @@
9
9
  import { readdir, readFile, writeFile, stat } from 'node:fs/promises';
10
10
  import { join, resolve, dirname } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
+ import { findRepoRootSync } from '../../instance-root/find.mjs';
12
13
 
13
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
15
  const DEFAULT_ROOT = process.env.DAVE_ROOT
15
16
  ? resolve(process.env.DAVE_ROOT)
16
- : resolve(__dirname, '../../../..');
17
+ : process.env.PA_ROOT
18
+ ? resolve(process.env.PA_ROOT)
19
+ : findRepoRootSync();
17
20
 
18
21
  const USAGE = `
19
22
  index-builder — Build a domain index from operator knowledge directories
@@ -17,9 +17,14 @@ import { readFile } from 'node:fs/promises';
17
17
  import { realpathSync } from 'node:fs';
18
18
  import { join, resolve, dirname, normalize } from 'node:path';
19
19
  import { fileURLToPath } from 'node:url';
20
+ import { findRepoRootSync } from '../../instance-root/find.mjs';
20
21
 
21
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
- const DEFAULT_ROOT = resolve(__dirname, '../../../..');
23
+ const DEFAULT_ROOT = process.env.DAVE_ROOT
24
+ ? resolve(process.env.DAVE_ROOT)
25
+ : process.env.PA_ROOT
26
+ ? resolve(process.env.PA_ROOT)
27
+ : findRepoRootSync();
23
28
 
24
29
  const USAGE = `
25
30
  query — Query the knowledge graph
@@ -20,6 +20,7 @@ import { execSync } from 'node:child_process';
20
20
  import { fileURLToPath } from 'node:url';
21
21
 
22
22
  import { hostMatches } from './host.mjs';
23
+ import { findRepoRootSync } from '../instance-root/find.mjs';
23
24
 
24
25
  const __filename = fileURLToPath(import.meta.url);
25
26
  const __dirname = dirname(__filename);
@@ -84,7 +85,7 @@ function readSchedule(filePath) {
84
85
  * @returns {string}
85
86
  */
86
87
  function computeDaveRoot() {
87
- return process.env.DAVE_ROOT || resolve(__dirname, '..', '..', '..');
88
+ return process.env.DAVE_ROOT || process.env.PA_ROOT || findRepoRootSync();
88
89
  }
89
90
 
90
91
  /**
@@ -95,7 +96,9 @@ function computeDaveRoot() {
95
96
  function resolveWorkDir(workDir) {
96
97
  const daveRoot = computeDaveRoot();
97
98
  if (!workDir) return daveRoot;
98
- return workDir.replace(/\$\{DAVE_ROOT\}/g, daveRoot);
99
+ return workDir
100
+ .replace(/\$\{DAVE_ROOT\}/g, daveRoot)
101
+ .replace(/\$\{PA_ROOT\}/g, daveRoot);
99
102
  }
100
103
 
101
104
  /**
@@ -112,6 +115,7 @@ function substituteVars(value) {
112
115
  const user = process.env.USER || process.env.USERNAME || '';
113
116
  return value
114
117
  .replace(/\$\{DAVE_ROOT\}/g, daveRoot)
118
+ .replace(/\$\{PA_ROOT\}/g, daveRoot)
115
119
  .replace(/\$\{HOME\}/g, home)
116
120
  .replace(/\$\{USER\}/g, user);
117
121
  }
@@ -347,6 +351,29 @@ export function defaultCronLogPath(sched) {
347
351
  * @param {string} [opts.logPath]
348
352
  * @returns {{ inner: string, wrapped: string }}
349
353
  */
354
+ /**
355
+ * When the schedule's argv[0] is a bare `node`, swap in the absolute
356
+ * interpreter path so cron does not depend on the shell PATH surfacing the
357
+ * right node. Absolute argv[0] passes through unchanged. Non-node bare
358
+ * binaries are left alone (there is no equivalent for arbitrary tools).
359
+ *
360
+ * @param {string} command
361
+ * @param {string} execPath
362
+ * @returns {string}
363
+ */
364
+ export function resolveCronArgv0Command(command, execPath) {
365
+ if (typeof command !== 'string' || command.length === 0) return command;
366
+ const parts = command.split(/\s+/);
367
+ const argv0 = parts[0];
368
+ if (!argv0) return command;
369
+ if (isAbsolute(argv0)) return command;
370
+ if (argv0 === 'node' && typeof execPath === 'string' && execPath.length > 0) {
371
+ parts[0] = execPath;
372
+ return parts.join(' ');
373
+ }
374
+ return command;
375
+ }
376
+
350
377
  export function buildCronCommand(sched, workDir, env, opts = {}) {
351
378
  const hasNvm = !!opts.hasNvm;
352
379
  const logPath = opts.logPath || defaultCronLogPath(sched);
@@ -354,6 +381,13 @@ export function buildCronCommand(sched, workDir, env, opts = {}) {
354
381
  const exportStmts = Object.entries(env)
355
382
  .map(([k, v]) => `export ${k}='${String(v).replace(/'/g, `'\\''`)}'; `)
356
383
  .join('');
384
+ // Resolve argv[0] to an absolute path when we can. cron minimal-env spawns
385
+ // /bin/sh without a full user PATH, so a bare `node` may fail to find the
386
+ // interpreter the host actually runs under. When argv[0] is `node`, swap
387
+ // in `process.execPath` (the interpreter running the installer). Absolute
388
+ // paths pass through. Other bare argv[0] values are left alone; the nvm
389
+ // preamble (when present) and cron's own PATH remain the fallback.
390
+ const resolvedCommand = resolveCronArgv0Command(sched.command, opts.execPath || process.execPath);
357
391
  // Whole rendered chain is braced so `>> logPath 2>&1` binds to the group,
358
392
  // not just the last simple command. Without the braces bash parses
359
393
  // `A && B && cmd >> log 2>&1` with the redirect bound to `cmd` alone, so
@@ -366,7 +400,7 @@ export function buildCronCommand(sched, workDir, env, opts = {}) {
366
400
  // the crontab line through /bin/sh -c '...' which parses the double-quoted
367
401
  // bash-c argument and would eagerly expand $(...) at dispatch time, not
368
402
  // at run time.
369
- const chain = `date -u; ${preamble}${exportStmts}cd ${workDir} && ${sched.command}`;
403
+ const chain = `date -u; ${preamble}${exportStmts}cd ${workDir} && ${resolvedCommand}`;
370
404
  const inner = `{ ${chain}; } >> ${logPath} 2>&1`;
371
405
  const wrapped = `bash -c "${inner.replace(/"/g, '\\"')}"`;
372
406
  return { inner, wrapped };
@@ -428,10 +462,12 @@ function installCron(sched, workDir) {
428
462
  * @returns {Record<string,string>}
429
463
  */
430
464
  export function withLaunchdDefaults(explicitEnv) {
465
+ const root = computeDaveRoot();
431
466
  const defaults = {
432
467
  PATH: process.env.PATH || '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin',
433
468
  HOME: homedir(),
434
- DAVE_ROOT: computeDaveRoot(),
469
+ DAVE_ROOT: root,
470
+ PA_ROOT: root,
435
471
  };
436
472
  return { ...defaults, ...explicitEnv };
437
473
  }
@@ -673,6 +709,7 @@ export function buildLaunchdPlist(sched, workDir, cron) {
673
709
 
674
710
  const mergedEnv = withLaunchdDefaults(resolveEnvironment(sched));
675
711
  validateLaunchdDaveRoot(mergedEnv.DAVE_ROOT);
712
+ validateLaunchdDaveRoot(mergedEnv.PA_ROOT);
676
713
  const envBlock = buildLaunchdEnvBlock(mergedEnv);
677
714
 
678
715
  // launchd does NOT PATH-resolve argv[0]; a bare `node`/`pnpm`/etc. fails
@@ -17,25 +17,46 @@ import { detect } from './detect.mjs';
17
17
  import { launchdLabelPrefix } from './install.mjs';
18
18
 
19
19
  /**
20
- * List Dave-managed schedules for a given backend.
20
+ * Render the leading name for the listing header. Uses the agent id when
21
+ * one is supplied, else a generic label so instances without an agent id
22
+ * (rare, but possible for pre-scaffold flows) still get a sensible line.
23
+ *
24
+ * Tag prefixes on the crontab / launchd / systemd / schtasks side are NOT
25
+ * renamed here \u2014 those are load-bearing for removal on existing installs
26
+ * (they are still `dave-` for backward compat).
27
+ *
28
+ * @param {string} [agent]
29
+ * @returns {string}
30
+ */
31
+ function headerName(agent) {
32
+ if (typeof agent === 'string' && agent.length > 0) return agent;
33
+ return 'Schedules';
34
+ }
35
+
36
+ /**
37
+ * List schedules for a given backend.
21
38
  * @param {string} backend - Backend name (cron, launchd, systemd, schtasks)
22
39
  * @param {object} [opts]
23
- * @param {string} [opts.agent] - launchd only: filter to this agent's label prefix. Defaults to 'dave' (unchanged pre-feature behaviour).
40
+ * @param {string} [opts.agent] - The agent id used for the header AND, on
41
+ * launchd, the label prefix to filter by. Defaults to 'dave' for the
42
+ * launchd label prefix (unchanged pre-feature behaviour); header defaults
43
+ * to 'Schedules' when omitted.
24
44
  * @returns {string} Human-readable listing
25
45
  */
26
46
  export function list(backend, opts = {}) {
47
+ const agent = opts.agent;
27
48
  switch (backend) {
28
- case 'cron': return listCron();
29
- case 'launchd': return listLaunchd(opts.agent);
30
- case 'systemd': return listSystemd();
31
- case 'schtasks': return listSchtasks();
49
+ case 'cron': return listCron(agent);
50
+ case 'launchd': return listLaunchd(agent);
51
+ case 'systemd': return listSystemd(agent);
52
+ case 'schtasks': return listSchtasks(agent);
32
53
  default:
33
54
  throw new Error(`Unknown backend: ${backend}`);
34
55
  }
35
56
  }
36
57
 
37
- function listCron() {
38
- const lines = ['Dave schedules (cron):', '\u2500'.repeat(37)];
58
+ function listCron(agent) {
59
+ const lines = [`${headerName(agent)} schedules (cron):`, '\u2500'.repeat(37)];
39
60
  let found = false;
40
61
 
41
62
  try {
@@ -55,7 +76,7 @@ function listCron() {
55
76
  }
56
77
 
57
78
  function listLaunchd(agent) {
58
- const lines = ['Dave schedules (launchd):', '\u2500'.repeat(37)];
79
+ const lines = [`${headerName(agent)} schedules (launchd):`, '\u2500'.repeat(37)];
59
80
  let found = false;
60
81
 
61
82
  const prefix = launchdLabelPrefix(agent);
@@ -82,8 +103,8 @@ function listLaunchd(agent) {
82
103
  return lines.join('\n');
83
104
  }
84
105
 
85
- function listSystemd() {
86
- const lines = ['Dave schedules (systemd):', '\u2500'.repeat(37)];
106
+ function listSystemd(agent) {
107
+ const lines = [`${headerName(agent)} schedules (systemd):`, '\u2500'.repeat(37)];
87
108
  let found = false;
88
109
 
89
110
  try {
@@ -102,8 +123,8 @@ function listSystemd() {
102
123
  return lines.join('\n');
103
124
  }
104
125
 
105
- function listSchtasks() {
106
- const lines = ['Dave schedules (schtasks):', '\u2500'.repeat(37)];
126
+ function listSchtasks(agent) {
127
+ const lines = [`${headerName(agent)} schedules (schtasks):`, '\u2500'.repeat(37)];
107
128
  let found = false;
108
129
 
109
130
  try {
@@ -15,6 +15,7 @@ import { homedir } from 'node:os';
15
15
  import { execSync } from 'node:child_process';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { launchdLabelPrefix } from './install.mjs';
18
+ import { findRepoRootSync } from '../instance-root/find.mjs';
18
19
 
19
20
  const __filename = fileURLToPath(import.meta.url);
20
21
  const __dirname = dirname(__filename);
@@ -130,7 +131,7 @@ export function remove(schedId, backend, options = {}) {
130
131
  }
131
132
 
132
133
  if (options.deleteFile) {
133
- const daveRoot = process.env.DAVE_ROOT || resolve(__dirname, '..', '..', '..');
134
+ const daveRoot = process.env.DAVE_ROOT || process.env.PA_ROOT || findRepoRootSync();
134
135
  const schedFile = join(daveRoot, 'schedules', `${schedId}.json`);
135
136
  if (existsSync(schedFile)) {
136
137
  unlinkSync(schedFile);
@@ -16,6 +16,9 @@
16
16
  },
17
17
  "private": true,
18
18
  "license": "MIT",
19
+ "dependencies": {
20
+ "@stravica-ai/engineering-pa-core": "workspace:*"
21
+ },
19
22
  "engines": {
20
23
  "node": ">=24"
21
24
  },
@@ -25,6 +25,7 @@ import { basename, join, resolve, dirname } from 'node:path';
25
25
  import { fileURLToPath } from 'node:url';
26
26
  import { spawnSync } from 'node:child_process';
27
27
 
28
+ import { findRepoRootSync } from '../../../../engineering-pa-core/src/instance-root/find.mjs';
28
29
  import { createClient, ConfluenceError } from '../client.mjs';
29
30
  import { storageToMarkdown } from '../storage.mjs';
30
31
  import {
@@ -39,8 +40,9 @@ import {
39
40
  } from '../mirror.mjs';
40
41
 
41
42
  const __dirname = dirname(fileURLToPath(import.meta.url));
42
- // From operator/extensions/tools/confluence/src/commands/ → five levels up.
43
- const DEFAULT_REPO_ROOT = resolve(__dirname, '..', '..', '..', '..', '..', '..');
43
+ // Route through the shared instance-root primitive; PA_ROOT is recognised
44
+ // as an alias of DAVE_ROOT since 0.1.50 (item 5).
45
+ const DEFAULT_REPO_ROOT = process.env.PA_ROOT || findRepoRootSync();
44
46
 
45
47
  const HELP = `confluence mirror-folder - Mirror a Confluence folder to local markdown
46
48
 
@@ -14,13 +14,15 @@ import { resolvePaName } from '../../common/pa-name.mjs';
14
14
  import { resolve, dirname } from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
16
  import { readFile as fsReadFile, writeFile as fsWriteFile } from 'node:fs/promises';
17
+ import { findRepoRootSync } from '../../../../engineering-pa-core/src/instance-root/find.mjs';
17
18
 
18
19
  // ── Path resolution ──────────────────────────────────────────────────
19
20
 
20
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
22
 
22
- // From operator/extensions/tools/jira/src/commands/ → six levels up to repo root
23
- const PROJECT_ROOT = resolve(__dirname, '..', '..', '..', '..', '..', '..');
23
+ // Route through the shared instance-root primitive; PA_ROOT and DAVE_ROOT
24
+ // are recognised env overrides (0.1.50, item 5).
25
+ const PROJECT_ROOT = process.env.DAVE_ROOT || process.env.PA_ROOT || findRepoRootSync();
24
26
 
25
27
  const HELP = `jira link - Cross-link work items with Jira tickets
26
28
 
@@ -14,13 +14,17 @@ import { formatTable, bold, green, yellow, dim, cyan } from '../format.mjs';
14
14
  import { resolve, dirname } from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
16
  import { readFile as fsReadFile, writeFile as fsWriteFile } from 'node:fs/promises';
17
+ import { findRepoRootSync } from '../../../../engineering-pa-core/src/instance-root/find.mjs';
17
18
 
18
19
  // ── Path resolution ──────────────────────────────────────────────────
19
20
 
20
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
22
 
22
- // From operator/extensions/tools/jira/src/commands/ → six levels up to repo root
23
- const PROJECT_ROOT = resolve(__dirname, '..', '..', '..', '..', '..', '..');
23
+ // Route through the shared instance-root primitive; PA_ROOT and DAVE_ROOT
24
+ // are recognised env overrides (0.1.50, item 5). The old six-levels-up
25
+ // walk landed inside the pnpm-store package directory under the published
26
+ // tarball layout.
27
+ const PROJECT_ROOT = process.env.DAVE_ROOT || process.env.PA_ROOT || findRepoRootSync();
24
28
 
25
29
  const MAPPINGS_REL_PATH = 'operator/config/jira-status-mappings.json';
26
30
 
@@ -40,15 +40,19 @@
40
40
  import { resolve, dirname } from 'node:path';
41
41
  import { fileURLToPath } from 'node:url';
42
42
  import { readFile as fsReadFile } from 'node:fs/promises';
43
+ import { findRepoRootSync } from '../../../engineering-pa-core/src/instance-root/find.mjs';
43
44
 
44
45
  // ── Path resolution ──────────────────────────────────────────────────
45
46
 
46
47
  const __dirname = dirname(fileURLToPath(import.meta.url));
47
48
 
48
- // Project root: env override, else cwd. The tool is invoked from a
49
- // workspace root so cwd is the correct default.
49
+ // Project root: env override, else the shared instance-root primitive.
50
+ // PA_ROOT is honoured as an alias since 0.1.50 (item 5).
50
51
  const DEFAULT_PROJECT_ROOT =
51
- process.env.PA_REPO_ROOT || process.env.DAVE_ROOT || process.cwd();
52
+ process.env.PA_REPO_ROOT
53
+ || process.env.DAVE_ROOT
54
+ || process.env.PA_ROOT
55
+ || findRepoRootSync();
52
56
 
53
57
  // Operator override, relative to the project root. This path is a
54
58
  // composition mount (fed from the operator entity), so the tool must not
@@ -9,15 +9,21 @@
9
9
  import { resolve, dirname } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { readFile as fsReadFile } from 'node:fs/promises';
12
+ import { findRepoRootSync } from '../../../engineering-pa-core/src/instance-root/find.mjs';
12
13
 
13
14
  // ── Path resolution ──────────────────────────────────────────────────
14
15
 
15
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
17
 
17
- // Project root: env override, else cwd. The tool is invoked from a
18
- // workspace root so cwd is the correct default.
18
+ // Project root: env override, else the shared instance-root primitive.
19
+ // PA_ROOT, DAVE_ROOT and PA_REPO_ROOT are all honoured (PA_ROOT since 0.1.50,
20
+ // item 5; findRepoRootSync walks up for `.git`, preferences or workspace
21
+ // markers before falling back to cwd).
19
22
  const DEFAULT_PROJECT_ROOT =
20
- process.env.PA_REPO_ROOT || process.env.DAVE_ROOT || process.cwd();
23
+ process.env.PA_REPO_ROOT
24
+ || process.env.DAVE_ROOT
25
+ || process.env.PA_ROOT
26
+ || findRepoRootSync();
21
27
 
22
28
  const MAPPINGS_REL_PATH = 'operator/config/jira-status-mappings.json';
23
29
 
@@ -740,7 +740,9 @@ export function buildDistillationScheduleJson(config, seams = {}) {
740
740
  // the schedule closes both cases (w-2026-07-15-003 + w-2026-07-16-006
741
741
  // trap class). PATH is only added when `claude` lives outside the
742
742
  // launchd default PATH set.
743
- environment: { DAVE_ROOT: config.targetDir },
743
+ // PA_ROOT baked alongside DAVE_ROOT so schedule authors and consumers
744
+ // can reference either (0.1.50, item 5). Both point at the same target.
745
+ environment: { DAVE_ROOT: config.targetDir, PA_ROOT: config.targetDir },
744
746
  };
745
747
 
746
748
  if (claudeBinDir && !LAUNCHD_DEFAULT_PATHS.includes(claudeBinDir)) {
@@ -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>Notes — Dave</title>
6
+ <title>Notes</title>
7
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>
@@ -119,6 +119,10 @@
119
119
  </div>
120
120
  <script>
121
121
  var operatorName = null;
122
+ // 0.1.50 (item 9): paName renders the "Next (<PA>)" label so the workspace
123
+ // never says "Dave" on non-Dave instances. Falls back to a neutral label
124
+ // when overview did not carry a name.
125
+ var paName = null;
122
126
  var projectWorkItems = [];
123
127
  function navTo(path) { if (window.parent && window.parent.navigate) window.parent.navigate(path); else window.location.href = path; }
124
128
  function openModal(idx) {
@@ -247,7 +251,14 @@
247
251
  html += '<div class="ws-next"><span class="ws-next-label">Next (you): </span><span class="ws-next-text">' + esc(ws.nextOperator) + '</span></div>';
248
252
  }
249
253
  if (ws.nextDave) {
250
- html += '<div class="ws-next"><span class="ws-next-label">Next (Dave): </span><span class="ws-next-text">' + esc(ws.nextDave) + '</span></div>';
254
+ // The desk API field name (`nextDave`) is kept for backward-compat
255
+ // with existing consumers; renaming to `nextPa` is deferred to a
256
+ // later release with a consumer sweep. The rendered label uses the
257
+ // capitalised paName (falling back to "PA" when it is unknown).
258
+ var paLabel = paName
259
+ ? (paName.charAt(0).toUpperCase() + paName.slice(1))
260
+ : 'PA';
261
+ html += '<div class="ws-next"><span class="ws-next-label">Next (' + esc(paLabel) + '): </span><span class="ws-next-text">' + esc(ws.nextDave) + '</span></div>';
251
262
  }
252
263
 
253
264
  // Docs list
@@ -276,6 +287,11 @@
276
287
  // Fetch operator name, project data, and work items in parallel
277
288
  var opPromise = fetch('/__api/overview').then(function(r) { return r.json(); }).then(function(d) {
278
289
  if (d.operatorName) operatorName = d.operatorName;
290
+ // 0.1.50 (item 9): drop the hardcoded "Dave" from the Next label
291
+ // in favour of the paName the operator scaffolded with. Capitalise
292
+ // the first letter to match the rest of the workspace's rendering
293
+ // (`Digital Operator` heading is capitalised the same way).
294
+ if (d.paName) paName = d.paName;
279
295
  }).catch(function() {});
280
296
 
281
297
  var projPromise = fetch('/__api/file?path=' + encodeURIComponent('projects/' + projectId + '/project.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>Projects — Dave</title>
6
+ <title>Projects</title>
7
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>
@@ -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>Tasks — Dave</title>
6
+ <title>Tasks</title>
7
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>
@@ -612,8 +612,8 @@ function linuxStatus({ names }) {
612
612
 
613
613
  function windowsNotSupported() {
614
614
  console.error('Error: Windows service installation is not yet implemented.');
615
- console.error('Open an issue on Stravica/dave if you need this, or run the');
616
- console.error('workspace manually with: pa workspace start (or node node_modules/@stravica-ai/engineering-pa-workspace/src/ws.mjs start)');
615
+ console.error('See https://stravica.ai for support, or run the workspace');
616
+ console.error('manually with: pa workspace start (or node node_modules/@stravica-ai/engineering-pa-workspace/src/ws.mjs start)');
617
617
  return 2;
618
618
  }
619
619
 
@@ -1,100 +1,13 @@
1
- // repo-root.mjs — shared sync resolver for the operator instance root.
2
- //
3
- // Under the published tarball layout, `@stravica-ai/engineering-pa-workspace`
4
- // lives at `node_modules/@stravica-ai/digital-operator-pa/dist/engineering-pa-workspace/`.
5
- // A `resolve(__dirname, '..', '..', '..')` walk from `src/` therefore lands
6
- // inside the `digital-operator-pa` package directory (a pnpm-store hard-link),
7
- // NOT at the operator repo root — silently misdirecting every file the
8
- // workspace derives from that path (`.ws.pid`, `.ws.log`, preferences overlay
9
- // reads, `paName` lookups, the server subprocess cwd).
10
- //
11
- // The resolver mirrors the precedence the rest of the workspace package
12
- // already uses (`server.mjs`, `install-daemon.mjs`):
13
- //
14
- // 1. `DAVE_REPO_ROOT` or `PA_REPO_ROOT` env override (tests + operator-set).
15
- // 2. Walk up from `process.cwd()` looking for a marker that identifies an
16
- // operator instance root:
17
- // - `.git` (present in every scaffolded instance — pa-init runs `git
18
- // init` at scaffold time).
19
- // - `operator/config/preferences.json` (the canonical instance marker;
20
- // matches instances that lost their `.git` for any reason).
21
- // - `pnpm-workspace.yaml` (matches monorepo checkouts, e.g. this
22
- // engineering-pa-dev repo itself, so package-local tests still work).
23
- // 3. Fall back to `process.cwd()` so pre-scaffold and dry-run flows still
24
- // have a stable value to render.
25
- //
26
- // The `startDir` argument is injectable for tests and callers that need to
27
- // resolve from a directory other than `process.cwd()`. Prod callers use
28
- // the default.
29
-
30
- import { existsSync } from 'node:fs';
31
- import { join, resolve } from 'node:path';
32
-
33
- const MARKERS = [
34
- '.git',
35
- join('operator', 'config', 'preferences.json'),
36
- 'pnpm-workspace.yaml',
37
- ];
38
-
39
- /**
40
- * Walk up from `start` looking for the first directory that contains any
41
- * marker in {@link MARKERS}. Returns the directory path, or null when the
42
- * walk reaches the filesystem root without a hit.
43
- *
44
- * @param {string} start
45
- * @returns {string|null}
46
- */
47
- export function walkUpForMarker(start) {
48
- let d = resolve(start);
49
- while (d && d !== resolve(d, '..')) {
50
- for (const marker of MARKERS) {
51
- if (existsSync(join(d, marker))) return d;
52
- }
53
- d = resolve(d, '..');
54
- }
55
- return null;
56
- }
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
-
68
- /**
69
- * Resolve the operator instance root. Deterministic and synchronous; safe
70
- * to call at module top-level. See the header comment for the full
71
- * precedence rule.
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
- *
78
- * @param {object} [opts]
79
- * @param {string} [opts.startDir=process.cwd()] Directory to start the walk from.
80
- * @param {NodeJS.ProcessEnv} [opts.env=process.env] Env source (tests inject).
81
- * @param {(msg: string) => void} [opts.warn] Optional warn sink (tests inject).
82
- * @returns {string} Absolute path.
83
- */
84
- export function findRepoRootSync(opts = {}) {
85
- const env = opts.env || process.env;
86
- const startDir = opts.startDir || process.cwd();
87
- const envRoot = env.DAVE_REPO_ROOT || env.PA_REPO_ROOT;
88
- if (envRoot) return resolve(envRoot);
89
- const found = walkUpForMarker(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;
100
- }
1
+ // repo-root.mjs — thin re-export of the core instance-root primitive.
2
+ //
3
+ // The resolver used to live here. It was promoted to
4
+ // `@stravica-ai/engineering-pa-core/src/instance-root/find.mjs` in 0.1.50
5
+ // so scheduler and knowledge-retrieval consumers share the same walk
6
+ // (item 14, w-2026-09-17-dave-017). Exports keep the same names so every
7
+ // existing importer keeps working unchanged.
8
+
9
+ export {
10
+ findRepoRootSync,
11
+ walkUpForMarker,
12
+ _resetFallbackWarningForTests,
13
+ } from '../../engineering-pa-core/src/instance-root/find.mjs';
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Dave Workspace -- persistent dev server.
4
+ * Digital Operator Workspace -- persistent dev server.
5
5
  *
6
- * Serves the dave repo as static HTML pages plus a JSON API.
6
+ * Serves the operator repo as static HTML pages plus a JSON API.
7
7
  * Auto-shuts down after 2 hours of idle time.
8
8
  *
9
9
  * Live-reload / SSE was removed pending a workspace rebuild — see PR