@stravica-ai/digital-operator-pa 0.1.44 → 0.1.46

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.
@@ -15,7 +15,7 @@ import { main as runMigrateInspect } from '../../../engineering-pa-migrate-inspe
15
15
 
16
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
17
 
18
- const USAGE = `pa — operator personal-assistant CLI
18
+ const USAGE = `pa - Digital Operator CLI
19
19
 
20
20
  Usage:
21
21
  pa session Start a PA session — chdir to agent dir, exec \`claude\`
@@ -18,8 +18,8 @@ import { existsSync } from 'node:fs';
18
18
  import { dirname, join, resolve } from 'node:path';
19
19
  import { parseArgs } from 'node:util';
20
20
 
21
- import { runSync } from './run.mjs';
22
21
  import { defaultRunner } from './runner.mjs';
22
+ import { runSyncWithStatus } from './status-record.mjs';
23
23
 
24
24
  const USAGE = `estate-sync — single-writer-per-host git sync for ESTATE-NATIVE content.
25
25
 
@@ -134,7 +134,7 @@ async function route(argv) {
134
134
  await mkdir(logDir, { recursive: true });
135
135
  const runsLog = join(logDir, 'runs.jsonl');
136
136
  const lockPath = join(repoRoot, '.cache/estate-sync/.lock');
137
- const code = await runSync({
137
+ const code = await runSyncWithStatus({
138
138
  repoRoot,
139
139
  logDir,
140
140
  runsLog,
@@ -0,0 +1,347 @@
1
+ // estate-sync — synced end-of-pass status record.
2
+ //
3
+ // After every real pass, estate-sync writes a tiny per-host status file
4
+ // at `<repoRoot>/agents/estate-sync/<paName>/<host>.json`. This path is
5
+ // TRACKED and — critically — sits OUTSIDE every registry mount `to`
6
+ // subtree, so estate-sync's own registry-scoped commit stages it (a
7
+ // path inside `agents/<paName>/` would be silently excluded by the
8
+ // mount filter and never leave the writing host). The consequence: the
9
+ // pass's own commit + push carries every host's most recent pass
10
+ // result into every clone. A reader on any host (e.g. the HQ boot-
11
+ // context hook) can then flag a stuck cron on any other host at
12
+ // session boot without ssh or a separate publisher.
13
+ //
14
+ // Location choice: `agents/estate-sync/` is not under any mount `to`
15
+ // path in the current registry (`operator/registry/repos.json` mounts
16
+ // `agents/<name>/`, one per agent, not `agents/` itself). Every host
17
+ // therefore commits its own file directly, no per-host registry
18
+ // change needed.
19
+ //
20
+ // Design notes:
21
+ // - Overwrite semantics: one JSON file per host, small (< 1 KB). The
22
+ // writer never appends; the previous pass's record is replaced.
23
+ // - The record is derived from the audit record estate-sync already
24
+ // writes to runs.jsonl (same source of truth) — so a change to the
25
+ // audit vocabulary is picked up here for free.
26
+ // - noop-locked passes DO NOT write a status record: the pass that is
27
+ // actually running (and holding the lock) will write its own record
28
+ // when it finishes, and a concurrent overwrite from a lock contender
29
+ // would race with it. Contenders exit silently.
30
+ // - The `agents/estate-sync/<paName>/` directory is created on first
31
+ // write. Failure to write fails silently — this is a status
32
+ // side-channel and must never block the sync exit code.
33
+ // - Callers wrap runSync via `runSyncWithStatus`, which reads the last
34
+ // line of runs.jsonl to derive the terminal record. This means the
35
+ // writer is layered ON TOP of runSync rather than braided into every
36
+ // one of its exit paths — the run.mjs surface stays untouched.
37
+ //
38
+ // Absence-of-work: a fresh file dated after `now - 6h` on every host is
39
+ // the healthy signal; the reader flags anything older or with alertLevel
40
+ // > 'ok'. The 6h window is chosen against the 30-minute cron cadence,
41
+ // twelve missed ticks is loud enough to warrant a session-boot line.
42
+
43
+ import { mkdir, readFile, writeFile, readdir } from 'node:fs/promises';
44
+ import { hostname as osHostname } from 'node:os';
45
+ import { join } from 'node:path';
46
+
47
+ import { runSync } from './run.mjs';
48
+ import { revParseSha } from './git.mjs';
49
+
50
+ /**
51
+ * Audit statuses that count as a healthy pass. Everything else
52
+ * (`fail-*`, unknown vocab, missing status) maps to `failed` and the
53
+ * reader is expected to flag it. `noop-locked` never reaches the
54
+ * composer (see runSyncWithStatus), but is listed here for parity so a
55
+ * future direct caller lands in the same place.
56
+ */
57
+ const OK_AUDIT_STATUSES = new Set([
58
+ 'ok',
59
+ 'noop-nothing-to-do',
60
+ 'noop-locked',
61
+ ]);
62
+
63
+ const ALERT_RANK = { ok: -1, warn: 0, error: 1, 'needs-human': 2 };
64
+
65
+ /**
66
+ * Compose the tracked status record from an audit record plus context.
67
+ * Pure — no clock, no filesystem. Tested directly.
68
+ *
69
+ * @param {object} input
70
+ * @param {object} input.audit audit record as written to runs.jsonl
71
+ * @param {string} input.paName resolved paName (e.g. 'dave')
72
+ * @param {string} input.host resolved hostname (os.hostname())
73
+ * @param {string|null} input.headBefore HEAD sha at start of pass (or null)
74
+ * @param {string|null} input.headAfter HEAD sha at end of pass (or null)
75
+ * @param {number} [input.pendingCount] count of self-heal pending entries
76
+ * @returns {{ host:string, paName:string, finishedAt:string, status:'ok'|'failed', alertLevel:'ok'|'warn'|'error'|'needs-human', attempts:number, lastError:(string|null), headBefore:(string|null), headAfter:(string|null), pending:number, auditStatus:string }}
77
+ */
78
+ export function composeStatusRecord({
79
+ audit,
80
+ paName,
81
+ host,
82
+ headBefore = null,
83
+ headAfter = null,
84
+ pendingCount = 0,
85
+ }) {
86
+ const auditStatus = typeof audit?.status === 'string' ? audit.status : 'unknown';
87
+ const status = OK_AUDIT_STATUSES.has(auditStatus) ? 'ok' : 'failed';
88
+ const alertLevel = deriveAlertLevel(audit, auditStatus);
89
+ const attempts = deriveAttempts(audit);
90
+ const lastError = deriveLastError(audit);
91
+ return {
92
+ host,
93
+ paName,
94
+ finishedAt: typeof audit?.ts === 'string' ? audit.ts : '',
95
+ status,
96
+ alertLevel,
97
+ attempts,
98
+ lastError,
99
+ headBefore: typeof headBefore === 'string' ? headBefore : null,
100
+ headAfter: typeof headAfter === 'string' ? headAfter : null,
101
+ pending: Number.isFinite(pendingCount) ? pendingCount : 0,
102
+ auditStatus,
103
+ };
104
+ }
105
+
106
+ function deriveAlertLevel(audit, auditStatus) {
107
+ // Prefer the highest-ranked alert level across queued self-heal
108
+ // entries when the audit carries them (needs-human > error > warn).
109
+ if (Array.isArray(audit?.queued) && audit.queued.length > 0) {
110
+ let best = 'warn';
111
+ for (const q of audit.queued) {
112
+ const lvl = typeof q?.alertLevel === 'string' ? q.alertLevel : 'warn';
113
+ if ((ALERT_RANK[lvl] ?? -1) > (ALERT_RANK[best] ?? -1)) best = lvl;
114
+ }
115
+ return best;
116
+ }
117
+ // Otherwise map from the status vocabulary.
118
+ switch (auditStatus) {
119
+ case 'fail-rebase-needs-human':
120
+ return 'needs-human';
121
+ case 'fail-rebase':
122
+ case 'fail-rebase-stuck':
123
+ case 'fail-push':
124
+ case 'fail-git':
125
+ case 'fail-cli-bootstrap':
126
+ return 'error';
127
+ case 'fail-rebase-queued':
128
+ return 'warn';
129
+ default:
130
+ return 'ok';
131
+ }
132
+ }
133
+
134
+ function deriveAttempts(audit) {
135
+ if (Array.isArray(audit?.queued) && audit.queued.length > 0) {
136
+ let max = 0;
137
+ for (const q of audit.queued) {
138
+ const n = typeof q?.attemptCount === 'number' ? q.attemptCount : 0;
139
+ if (n > max) max = n;
140
+ }
141
+ return max;
142
+ }
143
+ return 0;
144
+ }
145
+
146
+ function deriveLastError(audit) {
147
+ const reason = audit?.reason;
148
+ if (typeof reason !== 'string' || reason.length === 0) return null;
149
+ return reason.split(/\r?\n/)[0].slice(0, 200);
150
+ }
151
+
152
+ /**
153
+ * Write the status record to
154
+ * `<repoRoot>/agents/estate-sync/<paName>/<host>.json`. Parent dirs are
155
+ * created; the file is fully overwritten. Returns the path written.
156
+ * Failure to write is thrown to the caller — callers wrap in try/catch
157
+ * and never block on this.
158
+ *
159
+ * @param {{ repoRoot:string, paName:string, host:string, record:object }} opts
160
+ * @returns {Promise<string>}
161
+ */
162
+ export async function writeStatusRecord({ repoRoot, paName, host, record }) {
163
+ const dir = join(repoRoot, 'agents', 'estate-sync', sanitiseSegment(paName));
164
+ await mkdir(dir, { recursive: true });
165
+ const path = join(dir, `${sanitiseSegment(host)}.json`);
166
+ await writeFile(path, JSON.stringify(record, null, 2) + '\n');
167
+ return path;
168
+ }
169
+
170
+ /**
171
+ * Read paName from preferences.json with an optional preferences.local.json
172
+ * overlay (overlay wins). Returns null on any failure — the status
173
+ * writer is a side-channel and a missing paName just means the record
174
+ * is skipped, not that estate-sync itself fails.
175
+ *
176
+ * @param {string} repoRoot
177
+ * @returns {Promise<string|null>}
178
+ */
179
+ export async function readPaName(repoRoot) {
180
+ const basePath = join(repoRoot, 'operator/config/preferences.json');
181
+ const overlayPath = join(repoRoot, 'operator/config/preferences.local.json');
182
+ let base = {};
183
+ let overlay = {};
184
+ try { base = JSON.parse(await readFile(basePath, 'utf8')); } catch { /* ignore */ }
185
+ try { overlay = JSON.parse(await readFile(overlayPath, 'utf8')); } catch { /* ignore */ }
186
+ const value = overlay?.paName ?? base?.paName ?? null;
187
+ return typeof value === 'string' && value.length > 0 ? value : null;
188
+ }
189
+
190
+ /**
191
+ * `os.hostname()` with the injection seam runSync already uses for
192
+ * registry-scope tests. Empty results fall back to `unknown-host`.
193
+ */
194
+ export function resolveHost(hostnameFn) {
195
+ try {
196
+ const h = (hostnameFn || osHostname)();
197
+ return typeof h === 'string' && h.length > 0 ? h : 'unknown-host';
198
+ } catch {
199
+ return 'unknown-host';
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Read the last non-empty line of runs.jsonl and parse it as JSON.
205
+ * Returns null if the file is missing, empty, or the last line does not
206
+ * parse. Callers only care about the terminal record for the pass they
207
+ * just ran; earlier entries are irrelevant here.
208
+ *
209
+ * @param {string} runsLog
210
+ * @returns {Promise<object|null>}
211
+ */
212
+ export async function readLastAuditRecord(runsLog) {
213
+ let text;
214
+ try {
215
+ text = await readFile(runsLog, 'utf8');
216
+ } catch (err) {
217
+ if (err && err.code === 'ENOENT') return null;
218
+ throw err;
219
+ }
220
+ const lines = text.split('\n').map((s) => s.trim()).filter((s) => s.length > 0);
221
+ if (lines.length === 0) return null;
222
+ try {
223
+ return JSON.parse(lines[lines.length - 1]);
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Count entries in the self-heal pending queue directory. Returns 0 on
231
+ * missing dir. Directory entries starting with `.` (e.g. `.gitkeep`)
232
+ * are ignored, and sidecar `.json` files are counted alongside their
233
+ * data twins as a single pending — we count the DATA files only, i.e.
234
+ * entries whose name does not end in `.json`.
235
+ *
236
+ * @param {string} pendingDir
237
+ * @returns {Promise<number>}
238
+ */
239
+ export async function countPending(pendingDir) {
240
+ let entries;
241
+ try {
242
+ entries = await readdir(pendingDir);
243
+ } catch (err) {
244
+ if (err && err.code === 'ENOENT') return 0;
245
+ return 0;
246
+ }
247
+ return entries.filter((n) => !n.startsWith('.') && !n.endsWith('.json')).length;
248
+ }
249
+
250
+ /**
251
+ * Wrap `runSync` so that after each real (non-dry-run) pass the terminal
252
+ * audit record is mirrored to a tracked per-host status file. Signature
253
+ * is a superset of runSync — every existing runSync caller keeps working
254
+ * unchanged when swapped for this wrapper.
255
+ *
256
+ * Injection seams (all test-only; production defaults from prefs/env):
257
+ * - `paName` : override the paName that would otherwise come from
258
+ * preferences.json (used by tests and by callers who
259
+ * already resolved it).
260
+ * - `hostnameFn` : function returning the host string (already an
261
+ * injection seam on runSync itself, for scope tests).
262
+ * - `statusEnabled: false` disables the status write entirely (kept as
263
+ * a defensive off-switch; not user-facing).
264
+ *
265
+ * @param {object} opts every field runSync accepts, plus `paName`, `statusEnabled`.
266
+ * @returns {Promise<number>}
267
+ */
268
+ export async function runSyncWithStatus(opts) {
269
+ const {
270
+ repoRoot,
271
+ runsLog,
272
+ runner,
273
+ dryRun = false,
274
+ hostnameFn,
275
+ paName: paNameOverride,
276
+ statusEnabled = true,
277
+ stderr = process.stderr,
278
+ } = opts;
279
+
280
+ // Resolve paName + host early; if paName is unresolvable we skip the
281
+ // status write but still run the sync. Host is safe-defaulted.
282
+ const paName = statusEnabled
283
+ ? (paNameOverride || (repoRoot ? await readPaName(repoRoot) : null))
284
+ : null;
285
+ const host = resolveHost(hostnameFn);
286
+
287
+ // Capture HEAD BEFORE the pass — best-effort. A detached HEAD or an
288
+ // uninitialised repo yields null; the reader treats null as "unknown"
289
+ // and does not flag on it.
290
+ let headBefore = null;
291
+ if (paName && !dryRun && runner) {
292
+ headBefore = await safeHeadSha(runner, repoRoot);
293
+ }
294
+
295
+ const code = await runSync(opts);
296
+
297
+ // Skip status write for dry-run (nothing durable to report), for
298
+ // missing paName (nowhere to write), and for lock-contended passes
299
+ // (the pass holding the lock writes its own record; a contender would
300
+ // race).
301
+ if (!paName || dryRun) return code;
302
+
303
+ try {
304
+ const audit = await readLastAuditRecord(runsLog);
305
+ if (!audit) return code;
306
+ if (audit.status === 'noop-locked') return code;
307
+
308
+ const headAfter = await safeHeadSha(runner, repoRoot);
309
+ const pendingCount = await countPending(join(repoRoot, '.cache/estate-sync/pending'));
310
+ const record = composeStatusRecord({
311
+ audit,
312
+ paName,
313
+ host,
314
+ headBefore,
315
+ headAfter,
316
+ pendingCount,
317
+ });
318
+ await writeStatusRecord({ repoRoot, paName, host, record });
319
+ } catch (err) {
320
+ // Never block the sync exit code on a status-write failure. Log
321
+ // one line so the seam is visible in the cron output.
322
+ stderr.write(`[estate-sync] warning: failed to write synced status record: ${err && err.message ? err.message : err}\n`);
323
+ }
324
+
325
+ return code;
326
+ }
327
+
328
+ async function safeHeadSha(runner, repoRoot) {
329
+ try {
330
+ const res = await revParseSha(runner, { cwd: repoRoot, ref: 'HEAD' });
331
+ if (res && res.code === 0 && typeof res.sha === 'string' && res.sha.length > 0) {
332
+ return res.sha;
333
+ }
334
+ return null;
335
+ } catch {
336
+ return null;
337
+ }
338
+ }
339
+
340
+ function sanitiseSegment(name) {
341
+ // File-system-safe path segment: keep alnum, dot, dash, underscore.
342
+ // Anything else collapses to `-`. Empty result falls back to
343
+ // `unknown`. Prevents a hostname or paName carrying a path separator
344
+ // from writing outside the intended directory.
345
+ const cleaned = String(name || '').replace(/[^A-Za-z0-9._-]/g, '-');
346
+ return cleaned.length > 0 ? cleaned : 'unknown';
347
+ }
@@ -0,0 +1,119 @@
1
+ ---
2
+ description: First-session onboarding walkthrough — verifies the install, offers optional setup (Atlassian, workspace, alias, profile intake), and marks each step complete.
3
+ when_to_use: >-
4
+ Run on the operator's first session in a fresh scaffolded PA instance. Also
5
+ re-runnable any time later: every step is idempotent and reads the current
6
+ state from `.session/onboarding.json`, so `/onboard` picks up where it left off
7
+ and skips steps already marked done.
8
+ allowed-tools: Bash, Read, Write, Edit
9
+ argument-hint: (no arguments)
10
+ context: inline
11
+ ---
12
+
13
+ # /onboard — first-session walkthrough
14
+
15
+ You are running the onboarding walkthrough for a freshly scaffolded PA instance.
16
+ The scaffold has already run `pnpm install` and `pa status` at scaffold time, so
17
+ the install is proven working. Your job is the second half: verify the runtime,
18
+ offer the optional setup steps the guide deliberately left for you, and hand
19
+ over. Every step below is idempotent. State lives in `.session/onboarding.json`
20
+ so re-running `/onboard` picks up where it left off.
21
+
22
+ ## State file
23
+
24
+ Read `.session/onboarding.json` first. Shape:
25
+
26
+ ```
27
+ {
28
+ "version": 1,
29
+ "startedAt": "<iso>",
30
+ "steps": {
31
+ "verifyRuntime": "pending" | "done" | "skipped",
32
+ "verifyVersion": "pending" | "done" | "skipped",
33
+ "profileIntake": "pending" | "done" | "skipped",
34
+ "aliasOffer": "pending" | "done" | "skipped",
35
+ "workspaceOffer":"pending" | "done" | "skipped",
36
+ "atlassianOffer":"pending" | "done" | "skipped"
37
+ },
38
+ "completedAt": null | "<iso>"
39
+ }
40
+ ```
41
+
42
+ If the file is missing, the scaffold did not seed it — flag that and stop.
43
+
44
+ If `completedAt` is set, tell the operator onboarding is already complete and
45
+ offer to re-run a specific step by name. Do not walk the whole flow again.
46
+
47
+ ## Flow
48
+
49
+ Walk the pending steps in this order. After each, write the state file back
50
+ with the step marked `done` or `skipped`. Keep exchanges short: one question,
51
+ one answer, move on.
52
+
53
+ ### 1. verifyRuntime
54
+
55
+ Run `pnpm exec pa environment` and read the resulting `.session/environment.json`.
56
+ Report Node version, pnpm version, git version, and workspace state in one line.
57
+ If Node is below 24 or pnpm below 9, name the gap and stop; the operator needs
58
+ to upgrade before continuing.
59
+
60
+ ### 2. verifyVersion
61
+
62
+ Run `pnpm exec pa status` and capture the installed package version. Compare
63
+ against `npm view @stravica-ai/digital-operator-pa version`. On a match, mark
64
+ done and move on. On a mismatch, print both versions and the recovery line
65
+ (`pnpm install` inside the instance) — do not attempt the fix yourself.
66
+
67
+ ### 3. profileIntake
68
+
69
+ Ask the operator four questions in one turn:
70
+
71
+ 1. Your name and how you prefer to be addressed?
72
+ 2. Your role (job title, or "solo", or a short description)?
73
+ 3. Communication style you want (formal, casual, direct, verbose)?
74
+ 4. Any topic areas the PA should be strong on from day one?
75
+
76
+ Take the answers and write them into `operator/profile/profile.md`, overwriting
77
+ the placeholder. Keep it a short human-readable paragraph plus a bulleted list
78
+ of preferences. Do not invent fields the operator did not name.
79
+
80
+ ### 4. aliasOffer
81
+
82
+ The colleague probably has not aliased the PA yet. Offer one line and let them
83
+ decide whether to run it. The exact command is:
84
+
85
+ ```
86
+ echo "alias <pa-name>='cd <target-dir> && pnpm exec pa session'" >> ~/.zshrc && source ~/.zshrc
87
+ ```
88
+
89
+ Substitute `<pa-name>` from `operator/config/preferences.json` and
90
+ `<target-dir>` from `process.cwd()`. If the operator says yes, print the exact
91
+ line to run; do not run it for them (their shell rc is not our file to write).
92
+
93
+ ### 5. workspaceOffer
94
+
95
+ Ask if they want the local workspace started now. On yes, run
96
+ `pnpm exec pa workspace start` in the background and print the URL from
97
+ `operator/config/preferences.json:workspacePort` (default 4200). On no, note
98
+ that `pnpm exec pa workspace start` is the verb whenever they want it.
99
+
100
+ ### 6. atlassianOffer
101
+
102
+ Ask if they use Jira or Confluence. On no, mark skipped. On yes, walk them
103
+ through:
104
+
105
+ ```
106
+ pnpm exec secrets set JIRA_DOMAIN <site> # for <site>.atlassian.net
107
+ pnpm exec secrets set JIRA_EMAIL <email>
108
+ pnpm exec secrets set JIRA_API_TOKEN <paste-token> # id.atlassian.com/manage-profile/security/api-tokens
109
+ ```
110
+
111
+ The vault self-initialises on the first `secrets set`. After the three sets,
112
+ run `pnpm exec jira auth` and `pnpm exec confluence auth` and report the
113
+ identity each returns.
114
+
115
+ ## Completion
116
+
117
+ When every step is done or skipped, write `completedAt` and tell the operator
118
+ onboarding is done. Give them one line: "You are ready. Ask me anything, or
119
+ say `bring me up to speed` and I will run the standard session-boot briefing."
@@ -1,6 +1,6 @@
1
- # Personal Assistant -- Role Definition
1
+ # PA (Digital Operator) -- Role Definition
2
2
 
3
- The Personal Assistant (PA) is the operator's single point of contact. It answers questions, manages tasks, delegates work to specialist roles, and continuously learns the operator's preferences, judgement, and communication style. Everything the operator needs flows through the PA.
3
+ The PA (Digital Operator) is the operator's single point of contact. It answers questions, manages tasks, delegates work to specialist roles, and continuously learns the operator's preferences, judgement, and communication style. Everything the operator needs flows through the PA.
4
4
 
5
5
  The PA is domain-agnostic. It works for a developer, a marketer, a manager, a student -- anyone. The operator's profile and stated purpose shape the PA's behaviour; the role definition does not constrain the domain.
6
6
 
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
 
11
11
  export const name = './';
12
- export const version = '0.1.1';
12
+ export const version = '0.1.2';
13
13
  export const modes = ['solo', 'lead', 'member'];
14
14
 
15
15
  /**
@@ -16,7 +16,7 @@ import { parseArgs } from 'node:util';
16
16
  import { runInit } from './commands/init.mjs';
17
17
  import { runInitWorkspaceExtension } from './commands/init-workspace-extension.mjs';
18
18
 
19
- const USAGE = `pa-init: scaffold a new personal-assistant operator repo, or a single workspace extension.
19
+ const USAGE = `pa-init: scaffold a new Digital Operator instance, or a single workspace extension.
20
20
 
21
21
  Usage:
22
22
  pa-init --mode=solo [--name=PA_NAME] [--target=DIR] [--non-interactive] [--operator=NAME] [--purpose=TEXT]
@@ -114,11 +114,15 @@ function writeNextStepMessage(config) {
114
114
  const { paName, targetDir } = config;
115
115
  const lines = [
116
116
  '',
117
- 'Next step - start your PA:',
117
+ 'Next step, start your PA:',
118
118
  '',
119
119
  ` cd ${targetDir}`,
120
120
  ' pnpm exec pa session',
121
121
  '',
122
+ `Inside the session, type \`/onboard\`. ${paName} walks you through the`,
123
+ 'remaining verification and optional setup (runtime check, version check,',
124
+ 'profile intake, alias, workspace, Atlassian). Every step is idempotent.',
125
+ '',
122
126
  'Optional: add a one-word shell alias so you can start a session from anywhere.',
123
127
  'Add this line to ~/.zshrc (zsh) or ~/.bashrc (bash), then reload the shell:',
124
128
  '',
@@ -129,6 +129,25 @@ const RESEARCH_COMMAND_CANDIDATES = [
129
129
  export const RESEARCH_COMMAND_TEMPLATE_PATH =
130
130
  RESEARCH_COMMAND_CANDIDATES.find((p) => existsSync(p)) ?? RESEARCH_COMMAND_CANDIDATES[0];
131
131
 
132
+ /**
133
+ * Path to the `/onboard` slash-command doc, resolved out of the bundled
134
+ * engineering-pa-core package. Same two-candidate pattern as the research
135
+ * command so the scaffold reads the file at plan time and the copy the
136
+ * colleague gets never drifts from the packaged canonical.
137
+ *
138
+ * Rationale: the onboarding guide was rewritten in 0.1.45 to hand every
139
+ * verification and setup step past the single scaffold command to the PA
140
+ * itself. `/onboard` is the entry point; the seeded `.session/onboarding.json`
141
+ * state file is its scratchpad.
142
+ */
143
+ const ONBOARD_COMMAND_CANDIDATES = [
144
+ resolve(__dirname, '..', '..', 'engineering-pa-core', 'docs', 'commands', 'onboard.md'),
145
+ resolve(__dirname, '..', '..', 'core', 'docs', 'commands', 'onboard.md'),
146
+ ];
147
+
148
+ export const ONBOARD_COMMAND_TEMPLATE_PATH =
149
+ ONBOARD_COMMAND_CANDIDATES.find((p) => existsSync(p)) ?? ONBOARD_COMMAND_CANDIDATES[0];
150
+
132
151
  /**
133
152
  * Path to the bundled `workspace-extensions.md` PA-facing knowledge doc,
134
153
  * resolved out of the packaged core. Two candidates so this works in both
@@ -186,7 +205,7 @@ export const BASE_DOMAINS = [
186
205
  ];
187
206
 
188
207
  const DEFAULT_PURPOSE =
189
- 'Be a self-learning personal assistant who learns the operator deeply over time and acts on their behalf with increasing autonomy.';
208
+ 'Be a self-learning Digital Operator who learns the operator deeply over time and acts on their behalf with increasing autonomy.';
190
209
 
191
210
  const DEFAULT_COMMUNICATION_STYLE =
192
211
  'Casual, direct, no filler. Voice opinions with reasoning. Light humour welcome; obsequiousness is not.';
@@ -262,7 +281,7 @@ export function buildOperatorPackageJson(config) {
262
281
  version: '0.0.1',
263
282
  private: true,
264
283
  type: 'module',
265
- description: `Personal-assistant operator repo for ${config.operatorName} (PA: ${config.paName}).`,
284
+ description: `Digital Operator repo for ${config.operatorName} (PA: ${config.paName}).`,
266
285
  engines: { node: '>=24' },
267
286
  dependencies: {
268
287
  '@stravica-ai/digital-operator-pa': config.pkgVersion,
@@ -493,13 +512,53 @@ export function buildNextSessionMd(config) {
493
512
  return [
494
513
  '# Next session',
495
514
  '',
496
- `(Empty — generated at ${config.generatedAt}.)`,
515
+ '## Top priority, start here',
516
+ '',
517
+ 'First session in a fresh install. Run `/onboard` before anything else. It',
518
+ 'reads `.session/onboarding.json`, walks the operator through the remaining',
519
+ 'verification and optional setup steps (runtime check, version check,',
520
+ 'profile intake, shell alias, workspace start, Atlassian), and marks each',
521
+ 'step complete. Re-runnable at any point; already-done steps are skipped.',
497
522
  '',
498
- 'Maintain this file continuously. The "Top priority — start here" section is operator-facing.',
523
+ 'After `/onboard` reports complete, this file is the standard pickup surface',
524
+ 'for every subsequent session. Overwrite it as work rolls over between',
525
+ 'sessions.',
526
+ '',
527
+ `Generated at ${config.generatedAt}.`,
499
528
  '',
500
529
  ].join('\n');
501
530
  }
502
531
 
532
+ /**
533
+ * Build the `.session/onboarding.json` seed file.
534
+ *
535
+ * The scaffold is the "install" half of onboarding; this state file plus the
536
+ * `/onboard` slash command are the "walk through everything else" half. On a
537
+ * fresh scaffold every step is `pending`. The PA reads this file on its first
538
+ * `/onboard` invocation, walks the pending steps, and rewrites the file after
539
+ * each step. Idempotent by construction: a `done` or `skipped` step is not
540
+ * revisited.
541
+ *
542
+ * Keys are camelCase per the firm rule. Step names mirror the section headers
543
+ * in the `/onboard` slash command doc so a reader can map state to prose.
544
+ */
545
+ export function buildOnboardingStateJson(config) {
546
+ return {
547
+ version: 1,
548
+ generatedAt: config.generatedAt,
549
+ startedAt: null,
550
+ steps: {
551
+ verifyRuntime: 'pending',
552
+ verifyVersion: 'pending',
553
+ profileIntake: 'pending',
554
+ aliasOffer: 'pending',
555
+ workspaceOffer: 'pending',
556
+ atlassianOffer: 'pending',
557
+ },
558
+ completedAt: null,
559
+ };
560
+ }
561
+
503
562
  /**
504
563
  * Build the staging/.gitkeep contents. Empty directories cannot be tracked
505
564
  * by git directly; we drop a .gitkeep marker so the directory is preserved.
@@ -581,6 +640,9 @@ export async function buildPlan(config) {
581
640
  const rootClaudeTemplate = await readFile(ROOT_CLAUDE_MD_TEMPLATE_PATH, 'utf8');
582
641
  const operatorReadmeTemplate = await readFile(OPERATOR_README_TEMPLATE_PATH, 'utf8');
583
642
  const researchCommand = await readFile(RESEARCH_COMMAND_TEMPLATE_PATH, 'utf8');
643
+ const onboardCommand = existsSync(ONBOARD_COMMAND_TEMPLATE_PATH)
644
+ ? await readFile(ONBOARD_COMMAND_TEMPLATE_PATH, 'utf8')
645
+ : null;
584
646
  const workspaceExtensionsKnowledge = await readFile(WORKSPACE_EXTENSIONS_KNOWLEDGE_PATH, 'utf8');
585
647
  const rcfLiteKnowledge = await readRcfLiteKnowledge(RCF_LITE_KNOWLEDGE_DIR_PATH);
586
648
  return buildPlanFromTemplates(config, {
@@ -588,6 +650,7 @@ export async function buildPlan(config) {
588
650
  rootClaudeTemplate,
589
651
  operatorReadmeTemplate,
590
652
  researchCommand,
653
+ onboardCommand,
591
654
  workspaceExtensionsKnowledge,
592
655
  rcfLiteKnowledge,
593
656
  });
@@ -655,6 +718,7 @@ export function buildPlanFromTemplates(config, templates) {
655
718
  '.vault',
656
719
  '.claude',
657
720
  '.claude/commands',
721
+ '.session',
658
722
  ];
659
723
 
660
724
  const plan = [];
@@ -678,6 +742,25 @@ export function buildPlanFromTemplates(config, templates) {
678
742
  contents: templates.researchCommand,
679
743
  });
680
744
  }
745
+ // /onboard slash command: first-session walkthrough. Seeded alongside
746
+ // `.session/onboarding.json` (see below). Both files are read at plan
747
+ // time from the packaged core doc; the scaffold never re-authors the body.
748
+ if (templates.onboardCommand) {
749
+ plan.push({
750
+ kind: 'file',
751
+ path: '.claude/commands/onboard.md',
752
+ contents: templates.onboardCommand,
753
+ });
754
+ }
755
+ // Onboarding state seed. `.session/` is per-host and gitignored; a fresh
756
+ // scaffold seeds one file here so `/onboard` has something to read on the
757
+ // colleague's very first session. Every step is `pending`. See the doc at
758
+ // `.claude/commands/onboard.md` for the flow the PA walks.
759
+ plan.push({
760
+ kind: 'json',
761
+ path: '.session/onboarding.json',
762
+ contents: buildOnboardingStateJson(config),
763
+ });
681
764
  plan.push({
682
765
  kind: 'json',
683
766
  path: '.claude/coordinator-overlay.json',
@@ -4,7 +4,7 @@
4
4
 
5
5
  # {PA_NAME}
6
6
 
7
- You are {PA_NAME}, {OPERATOR_NAME}'s personal assistant. You are a self-learning AI PA built on Claude Code. Your purpose: {PURPOSE}
7
+ You are {PA_NAME}, {OPERATOR_NAME}'s Digital Operator. You are a self-learning AI operator built on Claude Code. Your purpose: {PURPOSE}
8
8
 
9
9
  You are a colleague, not a servant. You have opinions and you voice them -- with reasoning. If {OPERATOR_NAME} overrides you, you execute without friction and log the decision so you learn from it. You are always doing two things at once: executing the task, and learning from the interaction.
10
10
 
@@ -1,6 +1,6 @@
1
1
  # {PA_NAME}: operator repo
2
2
 
3
- Personal-assistant instance for {OPERATOR_NAME}, scaffolded with `pa-init`.
3
+ Digital Operator instance for {OPERATOR_NAME}, scaffolded with `pa-init`.
4
4
 
5
5
  This repo holds **only operator content**: your profile, knowledge, work items,
6
6
  projects, agent run state, schedules, and secrets. The PA platform code is
@@ -1,6 +1,6 @@
1
1
  # {PA_NAME}
2
2
 
3
- Personal assistant platform instance for {OPERATOR_NAME}. See `agents/{PA_NAME}/CLAUDE.md` for the full operating instructions. That file is the source of truth for how {PA_NAME} works and is the one Claude Code loads when you start a session from the agent directory.
3
+ Digital Operator instance for {OPERATOR_NAME}. See `agents/{PA_NAME}/CLAUDE.md` for the full operating instructions. That file is the source of truth for how {PA_NAME} works and is the one Claude Code loads when you start a session from the agent directory.
4
4
 
5
5
  ## Quick reference
6
6
 
@@ -1,183 +1,96 @@
1
1
  # Onboarding: solo PA
2
2
 
3
- Ten minutes end to end. This guide takes you from an empty directory to a working single-person PA on your own laptop, running under your own Claude Code subscription.
3
+ Ten minutes end to end. One command scaffolds the PA, one command starts a session, then the PA walks you through the rest.
4
4
 
5
- ## 0. Prerequisites
5
+ ## Prerequisites
6
6
 
7
- You need Node.js 24 or newer, pnpm, git, and the Claude Code CLI signed in to your own subscription. macOS example:
7
+ Working installs of the following, then sign in to Claude Code:
8
8
 
9
- ```sh
10
- brew install node@24 pnpm git
11
- curl -fsSL https://claude.ai/install.sh | bash # native installer; symlinks ~/.local/bin/claude and self-updates
12
- # alternative on macOS: brew install --cask claude-code
13
- claude # follow the interactive login, then Ctrl-C once you see the prompt
14
- ```
15
-
16
- Do not install Claude Code with `npm -g` (or `sudo npm -g`): global npm installs of Claude Code have caused problems in this estate, and running a user-scoped tool under `sudo` is wrong on principle.
9
+ - Node.js 24 or newer: https://nodejs.org (or your platform's Node 24 channel; on macOS `brew install node@24`)
10
+ - pnpm: https://pnpm.io/installation (or `brew install pnpm`)
11
+ - git: https://git-scm.com (or `brew install git`) with your identity already configured
12
+ - Claude Code CLI: https://claude.ai/install (or `brew install --cask claude-code`). Run `claude` once, sign in, then Ctrl-C.
17
13
 
18
- Linux and WSL are equivalent. Install Node 24 through your platform's normal channel.
14
+ Do not install Claude Code with `npm -g` or `sudo npm -g`.
19
15
 
20
16
  ## 1. Scaffold the PA
21
17
 
22
- ```sh
23
- mkdir ~/pa-bootstrap && cd ~/pa-bootstrap
24
- # Two files, one heredoc each. `pnpm init -y` writes a `devEngines.packageManager`
25
- # line pnpm 11.14 then rejects, and `-y` is silently ignored, so write a minimal
26
- # package.json directly. The `pnpm-workspace.yaml` is the load-bearing file:
27
- # pnpm holds packages published in the last week back by default (the
28
- # `minimumReleaseAge` supply-chain gate; it defaults to 0 on pnpm 10 and to a
29
- # non-zero window on some pnpm 11 defaults), and this repo releases often, so
30
- # without an explicit exclude an unversioned `pnpm add` silently resolves down
31
- # to a previous version with a different bin surface. pnpm reads
32
- # `minimumReleaseAge*` settings ONLY from `pnpm-workspace.yaml` (not from
33
- # `.npmrc`), and the exclude below trusts only this one package by name so
34
- # every other dependency your bootstrap ever pulls still honours the operator's
35
- # global gate.
36
- cat > package.json <<'JSON'
37
- { "name": "pa-bootstrap", "private": true }
38
- JSON
39
- cat > pnpm-workspace.yaml <<'YAML'
40
- minimumReleaseAgeExclude:
41
- - "@stravica-ai/digital-operator-pa"
42
- YAML
43
- pnpm add @stravica-ai/digital-operator-pa
44
- pnpm exec pa-init --mode=solo --name=<pa-name> --operator=<your-name> \
45
- --target=$HOME/<pa-name>-instance --non-interactive
46
- cd $HOME/<pa-name>-instance
47
- git config user.name "Your Name" # cold laptops have no global git identity
48
- git config user.email "you@example.com" # local to this repo; skip if you already have a global one
49
- git add -A && git commit -m "scaffold"
50
- ```
51
-
52
- Pick a `<pa-name>` in lowercase kebab-case (`alex`, `sam`, `hal-9000`). It becomes the agent directory (`agents/<pa-name>/`) and shows up in every log line.
53
-
54
- `pa-init` writes the instance's `package.json` with an EXACT `@stravica-ai/digital-operator-pa` dependency (the version it just installed into `pa-bootstrap`, not a caret range) and drops a `pnpm-workspace.yaml` inside the instance that excludes `@stravica-ai/digital-operator-pa` from the same age gate. Together they belt-and-brace pnpm's supply-chain gate: without them a freshly-published tarball can silently resolve down to the previous version, which may ship a different bin surface.
55
-
56
- **Sanity check the version.** `pa-init` prints `package: @stravica-ai/digital-operator-pa@<version>` right after the target line. If the version you see is older than the one `npm view @stravica-ai/digital-operator-pa version` reports, the exclude in your bootstrap `pnpm-workspace.yaml` did not take (rare; usually a stale pnpm store or a typo in the yaml). The `@latest` tag does NOT bypass the release-age gate, so the recovery is to pin the version npm names: `pnpm add @stravica-ai/digital-operator-pa@<version npm view prints>` inside the bootstrap dir, then re-run `pa-init`. When pnpm 11 sees a pinned version that would otherwise be gated, it adds an entry of its own to `pnpm-workspace.yaml` under `minimumReleaseAgeExclude`; the file the guide already asked you to write covers that write.
57
-
58
- `pnpm install` in the instance runs automatically as the last step of `pa-init` (skip it with `--skip-install` for CI). The `pa status` verification also runs at the end and prints the resolved package version; a mismatch between the scaffold pin and the installed version prints a WARNING line naming both, so a broken guard surfaces immediately instead of hiding behind a "Command not found" later.
59
-
60
- **Do not put the instance under a directory that already contains a `pnpm-workspace.yaml`.** pnpm treats the instance as a workspace member, `pnpm install` silently no-ops, and `node_modules/.bin/pa` never appears. Keep `--target` outside any pnpm workspace root (a top-level `$HOME/<pa-name>-instance`, as shown above, is safe; `~/pa-bootstrap` itself is such a workspace root now, so never nest the `--target` inside it).
61
-
62
- ## 2. Sanity-check the install
18
+ Pick a `<pa-name>` in lowercase kebab-case (`alex`, `sam`, `hal-9000`). It becomes the agent directory and shows up in every log line.
63
19
 
64
20
  ```sh
65
- pnpm exec pa environment
66
- pnpm exec pa status
67
- pnpm exec pa knowledge context # first line: library.mode = simple (variant token: simple)
21
+ npx -y -p @stravica-ai/digital-operator-pa@latest pa-init \
22
+ --mode=solo \
23
+ --name=<pa-name> \
24
+ --operator="Your Name" \
25
+ --target=$HOME/<pa-name>-instance \
26
+ --non-interactive
68
27
  ```
69
28
 
70
- `pa environment` populates `.session/environment.json` with the machine's Node, pnpm, git, and workspace state. `pa status` prints the installed platform version. `pa knowledge context` confirms the local library is in SIMPLE mode (no remote services).
29
+ That is the whole install. `pa-init` writes the instance under `$HOME/<pa-name>-instance`, pins the exact package version, runs `pnpm install`, and runs `pa status` to prove the install booted. It also seeds a `.session/onboarding.json` and a `/onboard` slash command so your PA can walk you through the rest on its first session.
71
30
 
72
- ## 3. Start the workspace
31
+ Without flags, `npx -y @stravica-ai/digital-operator-pa@latest pa-init --mode=solo` prompts for each value.
73
32
 
74
- ```sh
75
- pnpm exec pa workspace start # opens http://localhost:4200
76
- pnpm exec pa workspace start --port 4300 # pick a different port if 4200 is taken
77
- ```
78
-
79
- The workspace serves a viewer, a home page, and an onboarding form. Long notes from your PA land here rather than in chat. The default port is `4200` (localhost only); the server refuses to start with `Error: Port 4200 is already in use by another process.` when the port is bound by something else (a docker container, another dev server). Pick a free port with `--port N` or set `WORKSPACE_PORT` in your environment; `pa status` reads the resolved port from `operator/config/preferences.json` so keep the two in step for the port to persist across restarts.
80
-
81
- ## 4. Start your first session
33
+ ## 2. Start a session and run /onboard
82
34
 
83
35
  ```sh
36
+ cd $HOME/<pa-name>-instance
84
37
  pnpm exec pa session
85
38
  ```
86
39
 
87
- That changes directory into `agents/<pa-name>/` and execs `claude --dangerously-skip-permissions`. Your PA reads its own `CLAUDE.md`, the operator profile, and the autonomy envelope, then greets you.
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.
88
41
 
89
- `pa session` starts Claude Code with the per-call permission prompts switched off. The PA's operating model runs many tool calls per task, and the control layer is the autonomy envelope plus your own review of the commits it produces, not per-call yes/no prompts. Only run it on a machine and a repository you own; the flag is named that way on purpose. If you want prompts back for a session, `cd agents/<pa-name>/ && claude` runs plain Claude Code from the same directory.
42
+ ## 3. Alias (optional)
90
43
 
91
- Give your PA a one-word alias so you can start a session from anywhere on the machine. For zsh or bash, append this line to `~/.zshrc` or `~/.bashrc`:
44
+ `pa-init` prints the exact alias line at the end of a successful scaffold. Copy it into `~/.zshrc` or `~/.bashrc` and reload the shell:
92
45
 
93
46
  ```sh
94
47
  alias <pa-name>='cd $HOME/<pa-name>-instance && pnpm exec pa session'
95
48
  ```
96
49
 
97
- Then `source ~/.zshrc` (or open a new shell) and typing `<pa-name>` starts the session. `pa-init --mode=solo` prints this exact alias line at the end of a successful scaffold as a reminder.
98
-
99
- ## 5. First project and first work item
100
-
101
- Inside the session, ask your PA to open a project. It will call `pa project create` and `pa work create` on your behalf. If you would rather run them yourself:
102
-
103
- ```sh
104
- pnpm exec pa project create my-first-project --name "..." --category chore
105
- pnpm exec pa work create --title "..."
106
- pnpm exec desk overview # read-only view of what is in flight
107
- ```
50
+ After that, typing `<pa-name>` from anywhere starts a session.
108
51
 
109
52
  ## What you get
110
53
 
111
- - A generic personal-assistant operating model rendered into your own `agents/<pa-name>/CLAUDE.md` (with a short root pointer at `CLAUDE.md`). Seven buckets, dispatch-first, autonomy envelope.
54
+ - A generic Digital Operator operating model rendered into `agents/<pa-name>/CLAUDE.md` with a short root pointer at `CLAUDE.md`. Seven buckets, dispatch-first, autonomy envelope.
112
55
  - The `pa` CLI: `status`, `session`, `workspace`, `project`, `work`, `knowledge`, `research`, `autonomy`, `environment`, `agent`, `lint`, `launch`, `gsc`.
113
56
  - The read-only `desk` CLI: overview, today, projects, work.
114
- - A local workspace server on `http://localhost:4200`.
57
+ - A local workspace server on `http://localhost:4200` (start with `pnpm exec pa workspace start`).
115
58
  - A SIMPLE-mode local knowledge library: `pa knowledge search / related / domain / orphans / path / context`, seeded with the five base domains.
116
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`.
117
60
  - The `research` CLI with the light / normal / deep session store persisted under `.cache/research/`.
118
61
  - Scheduler (cron on Linux, launchd on macOS): `pnpm exec scheduler --help`.
119
62
  - Secrets vault (SOPS + age) if you opt in: `pnpm exec secrets --help`.
120
- - `.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 (the CLAUDE.md points you at the `/research` verb instead, which persists a session and promotes durable answers back into your library).
121
- - The `/research` slash command scaffolded to `.claude/commands/research.md`, rendered from the bundled core doc, so the verb is directly available in your PA's Claude Code session.
122
- - Workspace-extension prompting: `operator/knowledge/platform-tooling/workspace-extensions.md` teaches your PA when to propose a workspace page (a repeated manual view, an ask for "a page to see X", an installed tool with a natural page) and `pa-init workspace-extension <name>` scaffolds the HTML skeleton plus a `nav.json` entry so the accepted proposal costs thirty seconds instead of thirty minutes. Add `--with-api` when the page needs a server route.
123
-
124
- ## Workspace extensions
125
-
126
- Once your PA has been with you long enough to notice a pattern, expect it to propose a workspace page. Static overlays go under `operator/extensions/workspace/`, additional API routes hang off `operator/extensions/workspace/api.mjs`, and nav entries live in `operator/extensions/workspace/nav.json` (or team-scoped `nav.<team>.json` fragments if you run more than one team on the same box).
127
-
128
- To scaffold a new page:
129
-
130
- ```sh
131
- pnpm exec pa-init workspace-extension standup # HTML skeleton + nav entry
132
- pnpm exec pa-init workspace-extension jira --with-api # + api.mjs stub
133
- ```
134
-
135
- The full mechanism lives in the packaged workspace README (`node_modules/@stravica-ai/digital-operator-pa/dist/engineering-pa-workspace/README.md`, extension mechanism section) and the PA-facing prompting guide is at `operator/knowledge/platform-tooling/workspace-extensions.md`.
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.
64
+ - Two slash commands out of the box: `/research` (persisted research sessions) and `/onboard` (this walkthrough).
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.
136
66
 
137
67
  ## rcf-lite: build small apps and services
138
68
 
139
69
  Your PA has the `rcf-lite-engineer` role baked in. It builds greenfield applications from a weekend utility up to a small commercial SaaS, using the `rcf-lite` npm package as the spec-and-build spine (PRD, REQ, US, AC, FBS, with a green test suite gating each build). The link-led knowledge bucket at `operator/knowledge/rcf-lite/` seeds into every fresh solo instance and points at the current [Stravica RCF docs](https://stravica.ai/docs/rcf/); one local `create-a-small-app.md` walks a numbered command sequence from a fresh directory to a candidate build. Ask your PA to "build me a small app for X on rcf-lite" and it dispatches an `rcf-lite-engineer` instance with the deploy target, autonomy envelope, and RCF chain wired in.
140
70
 
141
- ## Optional: Jira and Confluence
142
-
143
- The `@stravica-ai/digital-operator-pa` package ships three Atlassian CLIs on `PATH`: `jira` (issues, search, transitions, worklogs), `confluence` (spaces, pages, CQL search, mirror), and `confluence-fetch` (search + fetch + save a page under `operator/notes/`). Run `pnpm exec jira --help`, `pnpm exec confluence --help`, `pnpm exec confluence-fetch --help` for the verbs.
144
-
145
- Both CLIs share three credentials pointing at the same Atlassian site. Setup:
146
-
147
- ```sh
148
- pnpm exec secrets set JIRA_DOMAIN yoursite # for yoursite.atlassian.net
149
- pnpm exec secrets set JIRA_EMAIL you@example.com # your Atlassian account
150
- pnpm exec secrets set JIRA_API_TOKEN <paste-token> # id.atlassian.com/manage-profile/security/api-tokens
151
- ```
152
-
153
- `secrets set` self-initialises the vault (age keypair, `.vault/recipients.txt`, empty encrypted store) on the first write when there is nothing at `.vault/` yet. It prints a one-line notice on stderr saying so and continues. You do not need to run `secrets init` manually first. Every `secrets set` also prints one stderr line naming which vault it wrote to; on a fresh solo install with no scope configured that says the write went to the `(legacy)` pot (no scope specified, no default configured). Informational, safe to ignore. Set `SECRETS_DEFAULT_SCOPE` in your environment or configure `defaultScope` in `~/.config/stravica-pa/defaults.json` if you want a named scope; solo mode does not require one.
154
-
155
- The age key that self-init generates lives at `~/.config/stravica-pa/age-key.txt` (one per laptop, not per instance). If that file already exists when you scaffold a second PA on the same machine, self-init reuses it, so both instances read and write the same encrypted vault content when a `.vault/` sits under both. That is the point: a single key per operator per machine. To force a fresh key, move the existing one aside (`mv ~/.config/stravica-pa/age-key.txt ~/.config/stravica-pa/age-key.txt.prev`) before the first `secrets set` in the new instance; the vault contents in the old instance will then be unreadable to the new key.
156
-
157
- Verify with `pnpm exec jira auth` and `pnpm exec confluence auth`; each prints the current user on success. Credentials also resolve from environment variables of the same names, so a `.env` or shell profile works as an alternative to `secrets`. The `atlassian` entry in `operator/extensions/manifest.json` records the extension is enabled and repeats the setup line.
158
-
159
- `jira mine` returns your open issues across every project the account can see. If you spend most of your Jira time in one project, set `JIRA_DEFAULT_PROJECT` (env var or a vault key of the same name) and `jira mine` narrows to it unless you pass `--project KEY` or `--all`:
160
-
161
- ```sh
162
- pnpm exec secrets set JIRA_DEFAULT_PROJECT PROJ # narrow `jira mine` to PROJ by default
163
- ```
164
-
165
71
  ## What is not included in solo mode
166
72
 
167
- - **`SessionStart` boot-context hook.** In some estates a `SessionStart` hook injects a live workState block (a desk overview tail, spawn-log tail, distillation health) 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`. The coordinator-hook doc at `node_modules/@stravica-ai/digital-operator-pa/dist/engineering-pa-cli/docs/coordinator-hook.md` shows the settings.json shape.
168
- - **Team modes.** `pa-init --mode=lead` and `--mode=member` exit with "not implemented". The SIMPLE-mode local library is single-operator by design. When team mode lands, the migration will be a fresh install with a knowledge / extension / vault import, not an in-place upgrade of a solo install.
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`.
74
+ - **Team modes.** `pa-init --mode=lead` and `--mode=member` exit with "not implemented". The SIMPLE-mode local library is single-operator by design.
169
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.
170
- - **HQ-specialised roles.** The bundled role set is the core seven (`pa`, `agent-ops`, `dispatch-briefer`, `librarian`, `system-admin`, `tool-developer`, `writer`) plus `product-engineer` and `rcf-lite-engineer` from the engineering types pack. Other tier-ladder roles (`web-developer`, `rcf-engineer`) and estate-specific extensions like `site-cloner`, `seo-*`, `infographic-designer`, `linkedin-writer` are not shipped.
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.
171
77
  - **Content publishing tool, Cloudflare / GSC live integrations, cross-host memory sync.** Out of scope for solo. Those all assume a multi-machine estate.
172
78
 
173
- ## Known first-run friction
79
+ ## Troubleshooting
174
80
 
175
- - **`LIBRARY_KNOWLEDGE_EXTRA_ROOTS`.** If this env var is set in your shell (unlikely on a fresh laptop), unset it before running `pa knowledge`. Set, it silently merges other knowledge trees into your local graph.
176
- - **`pa workspace stop`** on a machine that had the workspace daemon installed refuses and points you at the `launchctl unload` or `systemctl --user stop` command in its own error message. `pa workspace restart` is the intended path. The daemon unit is `com.<paName>.workspace` (launchd) / `<paName>-workspace.service` (systemd), derived from your `paName` in `operator/config/preferences.json`.
81
+ - **`npx` cannot find `pa-init`.** Node 24 must be on your PATH; run `node -v` to confirm. On corporate networks a proxy may block the tarball download; a `pnpm setup` may also be needed if pnpm itself is not on PATH.
82
+ - **`pa status` reports a version older than `npm view @stravica-ai/digital-operator-pa version`.** Rare. `pa-init` pins the exact version and writes a `pnpm-workspace.yaml` with `minimumReleaseAgeExclude` inside the instance so pnpm's release-age gate cannot filter it out. If it still slips, run `pnpm install` inside the instance again to reconcile.
83
+ - **Do not put the instance under a directory that already contains a `pnpm-workspace.yaml`.** pnpm treats the instance as a workspace member, `pnpm install` silently no-ops, and `node_modules/.bin/pa` never appears. `$HOME/<pa-name>-instance` is safe.
84
+ - **Port 4200 already in use.** `pa workspace start --port N` picks a different port. `pa status` reads the resolved port from `operator/config/preferences.json` so keep the two in step across restarts.
177
85
  - **`pa work create --status planned` is ignored on create.** The item lands as `draft`. Use `pa work status <id> planned` as a second step.
178
- - **Start sessions via `pnpm exec pa session`.** The coordinator hook (`pa-coordinator-hook`) is wired into `.claude/settings.json` as a bare bin name. `pnpm exec pa session` prepends `node_modules/.bin` to `PATH` before launching `claude`, which resolves the hook cleanly. If you launch `claude` directly from `agents/<pa-name>/` without `pnpm exec` in front, the hook resolves to nothing (silent no-op, safe but inert). Prefer the `pa session` entry point, or alias it as your PA name.
86
+ - **`LIBRARY_KNOWLEDGE_EXTRA_ROOTS`.** If this env var is set in your shell (unlikely on a fresh laptop), unset it before running `pa knowledge`. Set, it silently merges other knowledge trees into your local graph.
87
+ - **`pa workspace stop` on a machine that had the workspace daemon installed** refuses and points you at the `launchctl unload` or `systemctl --user stop` command in its own error message. `pa workspace restart` is the intended path. The daemon unit is `com.<paName>.workspace` (launchd) / `<paName>-workspace.service` (systemd).
179
88
  - **CLAUDE.md updates.** Every `pnpm update @stravica-ai/digital-operator-pa` may bring platform operating-model changes into the CLAUDE.md template. The scaffold rendered your CLAUDE.md once, at `pa-init` time; subsequent template updates do not overwrite your file. Pin the package version and read the CHANGELOG before you upgrade.
180
89
 
90
+ ## Why the single command works
91
+
92
+ `npx` fetches the tarball from npm, where there is no release-age gate. `pa-init` then writes the instance with an exact-pinned dependency plus a `pnpm-workspace.yaml` that carries `minimumReleaseAgeExclude: ["@stravica-ai/digital-operator-pa"]`, so the frozen `pnpm install` it runs next also lands on the exact version. Together they close the pnpm release-age hole that made the old bootstrap directory necessary in 0.1.41 / 0.1.42.
93
+
181
94
  ## Where things live
182
95
 
183
96
  | What | Where |
@@ -187,8 +100,10 @@ pnpm exec secrets set JIRA_DEFAULT_PROJECT PROJ # narrow `jira mine` to PROJ
187
100
  | Operator profile | `operator/profile/profile.md` |
188
101
  | Preferences | `operator/config/preferences.json` |
189
102
  | Autonomy envelope | `agents/<pa-name>/autonomy-envelope.md` |
190
- | Work items | `work/w-YYYY-MM-DD-<author>-NNN.json` (<author> is your PA name, e.g. `w-2026-09-16-alex-001`) |
103
+ | Work items | `work/w-YYYY-MM-DD-<author>-NNN.json` |
191
104
  | Projects | `projects/{id}/project.json` |
192
105
  | Session environment | `.session/environment.json` |
106
+ | Onboarding state | `.session/onboarding.json` |
193
107
  | Claude Code settings | `.claude/settings.json` |
194
108
  | Coordinator-hook overlay | `.claude/coordinator-overlay.json` |
109
+ | Slash commands | `.claude/commands/` (`onboard.md`, `research.md`) |
@@ -1,4 +1,4 @@
1
- The PA is a personal assistant that runs inside Claude Code on your laptop to handle work, keep knowledge and delegate tasks.
1
+ The PA is a Digital Operator that runs inside Claude Code on your laptop: it runs work, keeps its own knowledge and delegates tasks, and learns your judgement as you correct it.
2
2
 
3
3
  ## What it does for you day to day
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stravica-ai/digital-operator-pa",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
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",