@stravica-ai/digital-operator-pa 0.1.44 → 0.1.45
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-cli/src/sync/cli.mjs +2 -2
- package/dist/engineering-pa-cli/src/sync/status-record.mjs +347 -0
- package/dist/engineering-pa-core/docs/commands/onboard.md +119 -0
- package/dist/engineering-pa-scaffold/index.mjs +1 -1
- package/dist/engineering-pa-scaffold/src/commands/init.mjs +5 -1
- package/dist/engineering-pa-scaffold/src/generator.mjs +85 -2
- package/docs/onboarding-solo.md +45 -130
- package/package.json +1 -1
|
@@ -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
|
|
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."
|
|
@@ -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.
|
|
12
|
+
export const version = '0.1.2';
|
|
13
13
|
export const modes = ['solo', 'lead', 'member'];
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -114,11 +114,15 @@ function writeNextStepMessage(config) {
|
|
|
114
114
|
const { paName, targetDir } = config;
|
|
115
115
|
const lines = [
|
|
116
116
|
'',
|
|
117
|
-
'Next step
|
|
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
|
|
@@ -493,13 +512,53 @@ export function buildNextSessionMd(config) {
|
|
|
493
512
|
return [
|
|
494
513
|
'# Next session',
|
|
495
514
|
'',
|
|
496
|
-
|
|
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
|
-
'
|
|
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',
|
package/docs/onboarding-solo.md
CHANGED
|
@@ -1,183 +1,96 @@
|
|
|
1
1
|
# Onboarding: solo PA
|
|
2
2
|
|
|
3
|
-
Ten minutes end to end.
|
|
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
|
-
##
|
|
5
|
+
## Prerequisites
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Working installs of the following, then sign in to Claude Code:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
brew install
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
14
|
+
Do not install Claude Code with `npm -g` or `sudo npm -g`.
|
|
19
15
|
|
|
20
16
|
## 1. Scaffold the PA
|
|
21
17
|
|
|
22
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
Without flags, `npx -y @stravica-ai/digital-operator-pa@latest pa-init --mode=solo` prompts for each value.
|
|
73
32
|
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
## 3. Alias (optional)
|
|
90
43
|
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
54
|
+
- A generic personal-assistant 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
|
|
121
|
-
-
|
|
122
|
-
- Workspace-extension prompting: `operator/knowledge/platform-tooling/workspace-extensions.md` teaches your PA when to propose a workspace page
|
|
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
|
|
168
|
-
- **Team modes.** `pa-init --mode=lead` and `--mode=member` exit with "not implemented". The SIMPLE-mode local library is single-operator by design.
|
|
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
|
|
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
|
-
##
|
|
79
|
+
## Troubleshooting
|
|
174
80
|
|
|
175
|
-
- **`
|
|
176
|
-
- **`pa
|
|
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
|
-
-
|
|
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`
|
|
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`) |
|
package/package.json
CHANGED