@stravica-ai/digital-operator-pa 0.1.46 → 0.1.47
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/docs/commands/onboard.md +19 -2
- package/dist/engineering-pa-scaffold/src/prompts.mjs +47 -3
- package/dist/engineering-pa-workspace/src/install-daemon.mjs +8 -19
- package/dist/engineering-pa-workspace/src/repo-root.mjs +75 -0
- package/dist/engineering-pa-workspace/src/server.mjs +8 -22
- package/dist/engineering-pa-workspace/src/status-mon/run.mjs +9 -8
- package/dist/engineering-pa-workspace/src/ws.mjs +11 -1
- package/docs/what-it-can-do.md +4 -4
- package/package.json +2 -6
|
@@ -94,8 +94,25 @@ line to run; do not run it for them (their shell rc is not our file to write).
|
|
|
94
94
|
|
|
95
95
|
Ask if they want the local workspace started now. On yes, run
|
|
96
96
|
`pnpm exec pa workspace start` in the background and print the URL from
|
|
97
|
-
`operator/config/preferences.json:workspacePort` (default 4200).
|
|
98
|
-
|
|
97
|
+
`operator/config/preferences.json:workspacePort` (default 4200).
|
|
98
|
+
|
|
99
|
+
If the start fails because the configured port is already in use (the
|
|
100
|
+
operator has something else on 4200, or the previous session did not
|
|
101
|
+
shut down cleanly), do NOT stop the walkthrough. Instead:
|
|
102
|
+
|
|
103
|
+
1. Pick a free port at or above 4210 (probe 4210, 4211, 4212 ... in
|
|
104
|
+
order until one is free).
|
|
105
|
+
2. Retry `pnpm exec pa workspace start --port N` with that port.
|
|
106
|
+
3. On success, update `operator/config/preferences.json` with
|
|
107
|
+
`"workspacePort": N` so subsequent sessions start on the same port
|
|
108
|
+
without asking again. Preserve the rest of the file exactly, and
|
|
109
|
+
round-trip through `JSON.parse` / `JSON.stringify` with a two-space
|
|
110
|
+
indent to keep the on-disk shape stable.
|
|
111
|
+
4. Tell the operator you moved the workspace off 4200 and named the
|
|
112
|
+
port that stuck.
|
|
113
|
+
|
|
114
|
+
On no, note that `pnpm exec pa workspace start` is the verb whenever
|
|
115
|
+
they want it.
|
|
99
116
|
|
|
100
117
|
### 6. atlassianOffer
|
|
101
118
|
|
|
@@ -25,10 +25,54 @@ export function validatePaName(value) {
|
|
|
25
25
|
return { ok: true, value: trimmed };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Strip one wrapping pair of straight or typographic quotes around a
|
|
30
|
+
* string, if present, then trim. Applied to name-class identity fields so
|
|
31
|
+
* an operator who types their name with wrapping quotes ("Barry" or
|
|
32
|
+
* U+201CBarryU+201D) never lands a quoted value in preferences.json — the
|
|
33
|
+
* greeting renderer is faithful, so a quoted store shows up on the home
|
|
34
|
+
* screen ("Evening, "Barry"").
|
|
35
|
+
*
|
|
36
|
+
* Handled pairs (any single pair around the whole value, whitespace
|
|
37
|
+
* tolerated on the outside):
|
|
38
|
+
* - ASCII double quote U+0022
|
|
39
|
+
* - ASCII single quote U+0027
|
|
40
|
+
* - Left/right double quote U+201C U+201D
|
|
41
|
+
* - Left/right single quote U+2018 U+2019
|
|
42
|
+
* - Left/right guillemet U+00AB U+00BB
|
|
43
|
+
*
|
|
44
|
+
* Only ONE wrapping pair is stripped; a value that is deliberately quoted
|
|
45
|
+
* for another reason (e.g. `'''`) still surfaces as data. Whitespace
|
|
46
|
+
* inside the quotes is preserved by the trim after the strip.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} value
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
export function stripWrappingQuotes(value) {
|
|
52
|
+
if (typeof value !== 'string') return value;
|
|
53
|
+
let s = value.trim();
|
|
54
|
+
const pairs = [
|
|
55
|
+
['"', '"'],
|
|
56
|
+
["'", "'"],
|
|
57
|
+
['“', '”'],
|
|
58
|
+
['‘', '’'],
|
|
59
|
+
['«', '»'],
|
|
60
|
+
];
|
|
61
|
+
for (const [open, close] of pairs) {
|
|
62
|
+
if (s.length >= 2 && s.startsWith(open) && s.endsWith(close)) {
|
|
63
|
+
s = s.slice(open.length, s.length - close.length).trim();
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return s;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Non-empty, 1-80 chars, trimmed, wrapping quotes stripped. Allows letters / spaces / punctuation. */
|
|
29
71
|
export function validateOperatorName(value) {
|
|
30
72
|
if (typeof value !== 'string') return { ok: false, reason: 'must be a string' };
|
|
31
|
-
|
|
73
|
+
// Strip wrapping quotes BEFORE the length check so a padded typographic
|
|
74
|
+
// pair around a short name never trips the max-length ceiling.
|
|
75
|
+
const trimmed = stripWrappingQuotes(value);
|
|
32
76
|
if (trimmed.length < 1) return { ok: false, reason: 'must not be empty' };
|
|
33
77
|
if (trimmed.length > 80) return { ok: false, reason: 'must be at most 80 characters' };
|
|
34
78
|
return { ok: true, value: trimmed };
|
|
@@ -64,7 +108,7 @@ export async function promptValues(seam, defaults) {
|
|
|
64
108
|
out.operatorName = await ensureValid(
|
|
65
109
|
seam,
|
|
66
110
|
defaults.operatorName,
|
|
67
|
-
|
|
111
|
+
'Operator name (the human this PA serves, e.g. Baz): ',
|
|
68
112
|
validateOperatorName,
|
|
69
113
|
);
|
|
70
114
|
|
|
@@ -46,6 +46,7 @@ import { homedir, platform } from 'node:os';
|
|
|
46
46
|
import { dirname, join, resolve } from 'node:path';
|
|
47
47
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
48
48
|
import { readPreferencesSync } from '../../engineering-pa-core/src/preferences.mjs';
|
|
49
|
+
import { findRepoRootSync } from './repo-root.mjs';
|
|
49
50
|
import {
|
|
50
51
|
darwinLogBasenames,
|
|
51
52
|
launchdLabelFor,
|
|
@@ -60,25 +61,13 @@ import {
|
|
|
60
61
|
const __filename = fileURLToPath(import.meta.url);
|
|
61
62
|
const __dirname = dirname(__filename);
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
// checkouts)
|
|
71
|
-
function findRepoRoot(start) {
|
|
72
|
-
let d = start;
|
|
73
|
-
while (d && d !== resolve(d, '..')) {
|
|
74
|
-
if (existsSync(join(d, '.git')) || existsSync(join(d, 'pnpm-workspace.yaml'))) return d;
|
|
75
|
-
d = resolve(d, '..');
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const envRoot = process.env.DAVE_REPO_ROOT || process.env.PA_REPO_ROOT;
|
|
81
|
-
const REPO_ROOT = envRoot ? resolve(envRoot) : (findRepoRoot(process.cwd()) || process.cwd());
|
|
64
|
+
// Instance-root resolution routed through the shared helper (see
|
|
65
|
+
// `./repo-root.mjs`). Under the published tarball layout, `__dirname`-relative
|
|
66
|
+
// walks land inside `node_modules/@stravica-ai/digital-operator-pa/dist/` and
|
|
67
|
+
// mis-target every derived path; the shared resolver walks up from cwd for
|
|
68
|
+
// `.git`, `operator/config/preferences.json`, or `pnpm-workspace.yaml`, and
|
|
69
|
+
// honours `DAVE_REPO_ROOT` / `PA_REPO_ROOT` overrides.
|
|
70
|
+
const REPO_ROOT = findRepoRootSync();
|
|
82
71
|
const SERVER_SCRIPT = join(__dirname, 'server.mjs');
|
|
83
72
|
|
|
84
73
|
/**
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
/**
|
|
59
|
+
* Resolve the operator instance root. Deterministic and synchronous; safe
|
|
60
|
+
* to call at module top-level. See the header comment for the full
|
|
61
|
+
* precedence rule.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} [opts]
|
|
64
|
+
* @param {string} [opts.startDir=process.cwd()] Directory to start the walk from.
|
|
65
|
+
* @param {NodeJS.ProcessEnv} [opts.env=process.env] Env source (tests inject).
|
|
66
|
+
* @returns {string} Absolute path.
|
|
67
|
+
*/
|
|
68
|
+
export function findRepoRootSync(opts = {}) {
|
|
69
|
+
const env = opts.env || process.env;
|
|
70
|
+
const startDir = opts.startDir || process.cwd();
|
|
71
|
+
const envRoot = env.DAVE_REPO_ROOT || env.PA_REPO_ROOT;
|
|
72
|
+
if (envRoot) return resolve(envRoot);
|
|
73
|
+
const found = walkUpForMarker(startDir);
|
|
74
|
+
return found || resolve(startDir);
|
|
75
|
+
}
|
|
@@ -33,32 +33,18 @@ import { buildAllowedHosts, buildAllowedOrigins, checkRequestGuards, isGuardedPa
|
|
|
33
33
|
import { readTeamScopeConfigSync } from '../../engineering-pa-core/src/desk/team-scope.mjs';
|
|
34
34
|
import { readPreferencesSync } from '../../engineering-pa-core/src/preferences.mjs';
|
|
35
35
|
import { startPoller as startStatusMonPoller } from './status-mon/poller.mjs';
|
|
36
|
+
import { findRepoRootSync } from './repo-root.mjs';
|
|
36
37
|
|
|
37
38
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
38
39
|
|
|
39
40
|
// In v2 the workspace package is installed inside the operator repo at
|
|
40
|
-
// `node_modules/@stravica-ai/engineering-pa-workspace/`. The operator repo
|
|
41
|
-
// is
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
while (d && d !== prev) {
|
|
48
|
-
try {
|
|
49
|
-
const s = existsSync(join(d, '.git'));
|
|
50
|
-
if (s) return d;
|
|
51
|
-
} catch { /* ignore */ }
|
|
52
|
-
prev = d;
|
|
53
|
-
d = resolve(d, '..');
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const envRepoRoot = process.env.DAVE_REPO_ROOT || process.env.PA_REPO_ROOT;
|
|
59
|
-
const REPO_ROOT = envRepoRoot
|
|
60
|
-
? resolve(envRepoRoot)
|
|
61
|
-
: (findRepoRootSync(process.cwd()) || process.cwd());
|
|
41
|
+
// `node_modules/@stravica-ai/engineering-pa-workspace/`. The operator repo
|
|
42
|
+
// root is resolved via the shared helper (`./repo-root.mjs`), which walks
|
|
43
|
+
// up from cwd for `.git`, `operator/config/preferences.json`, or
|
|
44
|
+
// `pnpm-workspace.yaml`, and honours `DAVE_REPO_ROOT` / `PA_REPO_ROOT`
|
|
45
|
+
// overrides. The package's own assets live at `../app/` (one level up
|
|
46
|
+
// from src/).
|
|
47
|
+
const REPO_ROOT = findRepoRootSync();
|
|
62
48
|
const APP_DIR = resolve(__dirname, '..', 'app');
|
|
63
49
|
|
|
64
50
|
// Silent by default. Set WS_DEBUG=1 to re-enable startup banner, file-change
|
|
@@ -29,6 +29,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
29
29
|
|
|
30
30
|
import { readPreferencesSync } from '../../../engineering-pa-core/src/preferences.mjs';
|
|
31
31
|
import { poll, resolveSources, mergeWithPrevious } from './poll.mjs';
|
|
32
|
+
import { findRepoRootSync } from '../repo-root.mjs';
|
|
32
33
|
|
|
33
34
|
const __filename = fileURLToPath(import.meta.url);
|
|
34
35
|
const __dirname = dirname(__filename);
|
|
@@ -61,15 +62,15 @@ function parseArgs(argv) {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
function findRepoRoot() {
|
|
65
|
+
// DAVE_ROOT is the historical status-mon env; keep it as a first-class
|
|
66
|
+
// override so scheduled runs already setting it are unaffected. Otherwise
|
|
67
|
+
// route through the shared resolver (`../repo-root.mjs`), which walks up
|
|
68
|
+
// from cwd for the instance markers and also honours DAVE_REPO_ROOT /
|
|
69
|
+
// PA_REPO_ROOT. The old fallback (`resolve(__dirname, '..', '..', '..')`)
|
|
70
|
+
// was broken under the installed tarball layout — see
|
|
71
|
+
// `w-2026-09-17-dave-016`.
|
|
64
72
|
if (process.env.DAVE_ROOT) return process.env.DAVE_ROOT;
|
|
65
|
-
|
|
66
|
-
while (dir !== '/' && dir !== '.') {
|
|
67
|
-
if (existsSync(join(dir, '.git'))) return dir;
|
|
68
|
-
const parent = dirname(dir);
|
|
69
|
-
if (parent === dir) break;
|
|
70
|
-
dir = parent;
|
|
71
|
-
}
|
|
72
|
-
return resolve(__dirname, '..', '..', '..');
|
|
73
|
+
return findRepoRootSync();
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
function readPreviousState(statePath) {
|
|
@@ -28,10 +28,20 @@ import {
|
|
|
28
28
|
restartCommand,
|
|
29
29
|
} from './supervisor.mjs';
|
|
30
30
|
import { resolvePaName } from './unit-name.mjs';
|
|
31
|
+
import { findRepoRootSync } from './repo-root.mjs';
|
|
31
32
|
|
|
32
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
33
34
|
const __dirname = dirname(__filename);
|
|
34
|
-
|
|
35
|
+
// Instance root — NOT `resolve(__dirname, '..', '..', '..')`. Under the
|
|
36
|
+
// published `@stravica-ai/digital-operator-pa` tarball layout the workspace
|
|
37
|
+
// package lives under `dist/engineering-pa-workspace/`, so a three-parent
|
|
38
|
+
// __dirname walk lands inside the pnpm-store hard-linked package directory
|
|
39
|
+
// rather than at the operator instance root. `findRepoRootSync` walks up
|
|
40
|
+
// from `process.cwd()` for `.git` / `operator/config/preferences.json` /
|
|
41
|
+
// `pnpm-workspace.yaml` and honours `DAVE_REPO_ROOT` / `PA_REPO_ROOT`
|
|
42
|
+
// overrides. See `./repo-root.mjs` for the full precedence rule and
|
|
43
|
+
// `w-2026-09-17-dave-016` for the P1 that caused this to be extracted.
|
|
44
|
+
const REPO_ROOT = findRepoRootSync();
|
|
35
45
|
|
|
36
46
|
const SERVER_SCRIPT = join(__dirname, 'server.mjs');
|
|
37
47
|
// Env overrides let tests point at a temp directory so the real repo's
|
package/docs/what-it-can-do.md
CHANGED
|
@@ -43,7 +43,7 @@ The PA is a Digital Operator that runs inside Claude Code on your laptop: it run
|
|
|
43
43
|
|
|
44
44
|
- The dispatch-briefer analyses your request and writes an eight-section brief in fresh context, covering boundaries, outputs and completion criteria.
|
|
45
45
|
- Work can go to a single agent or a task group with a coordinator and workers, or stay in the current conversation.
|
|
46
|
-
- Parallel workers use separate git worktrees in the
|
|
46
|
+
- Parallel workers use separate git worktrees in the operator's own repositories; writes to downstream project repositories run sequentially.
|
|
47
47
|
- A coordinator hook enforces tool-use policy inside Claude Code.
|
|
48
48
|
|
|
49
49
|
## The roles it comes with
|
|
@@ -74,12 +74,12 @@ The PA is a Digital Operator that runs inside Claude Code on your laptop: it run
|
|
|
74
74
|
|
|
75
75
|
- Solo use is the shipped setup; team lead and member modes are not implemented.
|
|
76
76
|
- The remote library is dormant in the default local mode.
|
|
77
|
-
-
|
|
77
|
+
- Specialised roles and the session-boot context injections used on the first-party estate are not part of the shipped package.
|
|
78
78
|
- Connections to services through MCP need your own Claude Code configuration.
|
|
79
79
|
- You supply your voice guide, personal writing rules, principles and profile knowledge; the starter content is structure and placeholders.
|
|
80
80
|
- Sharing repositories or memory across machines needs a remote and coordination that you set up.
|
|
81
81
|
|
|
82
82
|
## How to start
|
|
83
83
|
|
|
84
|
-
Open
|
|
85
|
-
Follow it to set up
|
|
84
|
+
Open [docs/onboarding-solo.md](./onboarding-solo.md).
|
|
85
|
+
Follow it to set up your Digital Operator on your laptop.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stravica-ai/digital-operator-pa",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.47",
|
|
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",
|
|
@@ -39,11 +39,7 @@
|
|
|
39
39
|
"LICENSE"
|
|
40
40
|
],
|
|
41
41
|
"license": "MIT",
|
|
42
|
-
"
|
|
43
|
-
"type": "git",
|
|
44
|
-
"url": "git+https://github.com/Stravica/digital-operator-pa.git",
|
|
45
|
-
"directory": "packages/engineering-pa"
|
|
46
|
-
},
|
|
42
|
+
"homepage": "https://stravica.ai",
|
|
47
43
|
"publishConfig": {
|
|
48
44
|
"registry": "https://registry.npmjs.org/",
|
|
49
45
|
"access": "public"
|