@stravica-ai/digital-operator-pa 0.1.41 → 0.1.42
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/pa/cli.mjs +25 -1
- package/dist/engineering-pa-cli/src/secrets/commands/set.mjs +36 -1
- package/dist/engineering-pa-core/src/distill/run.mjs +45 -2
- package/dist/engineering-pa-scaffold/src/commands/init.mjs +58 -3
- package/dist/engineering-pa-scaffold/src/generator.mjs +111 -1
- package/docs/onboarding-solo.md +19 -5
- package/package.json +1 -1
|
@@ -205,6 +205,13 @@ function resolveWorkspacePort(repoRoot, opts = {}) {
|
|
|
205
205
|
return 4200;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* The `server` field the workspace's own /__api/identity endpoint returns
|
|
210
|
+
* (see packages/workspace/src/api.mjs). Any other body on the port means
|
|
211
|
+
* the port is bound by an unrelated process, not our workspace.
|
|
212
|
+
*/
|
|
213
|
+
const WORKSPACE_IDENTITY_SERVER = 'digital-operator-pa-workspace';
|
|
214
|
+
|
|
208
215
|
async function probeWorkspace(url, fetchImpl) {
|
|
209
216
|
const fetchFn = fetchImpl ?? globalThis.fetch;
|
|
210
217
|
if (typeof fetchFn !== 'function') {
|
|
@@ -214,8 +221,25 @@ async function probeWorkspace(url, fetchImpl) {
|
|
|
214
221
|
const timer = setTimeout(() => controller.abort(), 1500);
|
|
215
222
|
try {
|
|
216
223
|
const res = await fetchFn(url, { signal: controller.signal });
|
|
224
|
+
// Any HTTP response has to be positively identified as OUR workspace
|
|
225
|
+
// before the probe reports "reachable". WSD-laptop cold install of
|
|
226
|
+
// 0.1.41 (2026-09-16): a docker container bound to :4200 answered
|
|
227
|
+
// /__api/identity with HTTP 404 and `pa status` reported the
|
|
228
|
+
// workspace "reachable (HTTP 404)". Identity-gate the probe so a
|
|
229
|
+
// foreign process on the port surfaces as "in use by another
|
|
230
|
+
// process" instead of a false green.
|
|
217
231
|
if (res.status >= 200 && res.status < 500) {
|
|
218
|
-
|
|
232
|
+
let body = null;
|
|
233
|
+
try {
|
|
234
|
+
body = await res.json();
|
|
235
|
+
} catch { /* not JSON — falls through to the identity-mismatch branch */ }
|
|
236
|
+
if (body && body.server === WORKSPACE_IDENTITY_SERVER) {
|
|
237
|
+
return { status: 'reachable', detail: `HTTP ${res.status}` };
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
status: 'in use by another process',
|
|
241
|
+
detail: `HTTP ${res.status}, /__api/identity did not identify as ${WORKSPACE_IDENTITY_SERVER}`,
|
|
242
|
+
};
|
|
219
243
|
}
|
|
220
244
|
return { status: 'error', detail: `HTTP ${res.status}` };
|
|
221
245
|
} catch (e) {
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
sopsEncryptScoped,
|
|
36
36
|
VaultError,
|
|
37
37
|
} from '../vault.mjs';
|
|
38
|
+
import { runInit } from './init.mjs';
|
|
38
39
|
|
|
39
40
|
// --canonical-clone / --no-canonical may appear anywhere in argv, matching
|
|
40
41
|
// the --scope/--file convention in cli.mjs. Local to `set` (not global
|
|
@@ -91,7 +92,41 @@ export async function runSet(args, overrides = {}) {
|
|
|
91
92
|
throw new VaultError('missing dependencies', 1);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
let vaultState = checkVault(paths);
|
|
96
|
+
// Self-init on `secrets set` when the legacy vault has not been
|
|
97
|
+
// initialised on this host yet. WSD-laptop cold install of 0.1.41
|
|
98
|
+
// (2026-09-16): the onboarding guide's Atlassian setup asks the
|
|
99
|
+
// colleague to run three `secrets set` commands, and the first one
|
|
100
|
+
// died with `Error: No recipients file at .vault/recipients.txt —
|
|
101
|
+
// Run 'secrets init' to set up the vault.` The guide's two-step
|
|
102
|
+
// Atlassian setup never named that mandatory extra step. `set` now
|
|
103
|
+
// runs the same `runInit()` code path in-process before the first
|
|
104
|
+
// write when the vault is uninitialised, emitting one stderr notice
|
|
105
|
+
// so the operator can see what happened.
|
|
106
|
+
//
|
|
107
|
+
// The self-init fires only when it is SAFE — the legacy (non-scoped)
|
|
108
|
+
// vault, no encrypted store already on disk (otherwise a fresh key
|
|
109
|
+
// would orphan the existing contents; that failure mode still surfaces
|
|
110
|
+
// as `missing-key` exit 2 with the "restore your key from backup"
|
|
111
|
+
// guidance). `missing-recipients` on a solo host is always safe (a
|
|
112
|
+
// present store with no recipients file would already have failed
|
|
113
|
+
// decryption elsewhere; the case the WSD laptop hit was "no vault at
|
|
114
|
+
// all yet"). `missing-key` on a solo host is also safe when the
|
|
115
|
+
// encrypted store is absent — pure first-run state. Scoped vaults
|
|
116
|
+
// (`paths.scoped`) are provisioned by scope-registration tooling and
|
|
117
|
+
// are never self-inited here; a `secrets set --scope <name>` against
|
|
118
|
+
// a missing scope surfaces the not-provisioned error unchanged.
|
|
119
|
+
const canSelfInit = !vaultState.ok
|
|
120
|
+
&& !paths.scoped
|
|
121
|
+
&& (vaultState.reason === 'missing-recipients' || vaultState.reason === 'missing-key')
|
|
122
|
+
&& !secretsFileExists(paths);
|
|
123
|
+
if (canSelfInit) {
|
|
124
|
+
console.error(
|
|
125
|
+
`notice: no vault at ${paths.vaultDir}: self-initialising before first write (equivalent to running \`secrets init\`).`,
|
|
126
|
+
);
|
|
127
|
+
await runInit({ paths });
|
|
128
|
+
vaultState = checkVault(paths);
|
|
129
|
+
}
|
|
95
130
|
if (!vaultState.ok) {
|
|
96
131
|
console.error(vaultState.message);
|
|
97
132
|
throw new VaultError(vaultState.reason || 'vault not initialised', vaultState.exitCode ?? 1);
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// omitted on `ok`. `log` is null on exits that happened before the
|
|
33
33
|
// per-run log file was created.
|
|
34
34
|
|
|
35
|
-
import { appendFile, readFile } from 'node:fs/promises';
|
|
35
|
+
import { appendFile, readFile, unlink } from 'node:fs/promises';
|
|
36
36
|
import { basename, join } from 'node:path';
|
|
37
37
|
|
|
38
38
|
import { resolvePrereqs } from './prereqs.mjs';
|
|
@@ -211,7 +211,21 @@ export async function runDistill({
|
|
|
211
211
|
log(`distillation OK (${durationSec}s) — log: ${runLogPath}`, { stderr });
|
|
212
212
|
log('changes (if any) will be committed by the next sync run', { stderr });
|
|
213
213
|
|
|
214
|
-
// Step
|
|
214
|
+
// Step 4a — invalidate the retrieval artefacts so the next
|
|
215
|
+
// `pa knowledge` call rebuilds them and sees any doc the librarian just
|
|
216
|
+
// promoted. Macbook re-proof of 0.1.41 (2026-09-16): on a solo install
|
|
217
|
+
// there is no `library-sync-cycle:graph-rebuild` leg running behind the
|
|
218
|
+
// scenes, so a distilled knowledge doc stayed invisible to
|
|
219
|
+
// `pa knowledge search` until `.graph.json` was deleted by hand. Delete
|
|
220
|
+
// is safer than in-process rebuild here: the retrieval loader in
|
|
221
|
+
// `packages/cli/src/knowledge/graph.mjs` already self-heals on a
|
|
222
|
+
// missing artefact (`loadGraph:on-demand`), and a full-mode host has
|
|
223
|
+
// its scheduled graph-rebuild leg rebuild both files on the next cycle
|
|
224
|
+
// regardless. Both are untracked per the 2026-07-29 per-device ruling,
|
|
225
|
+
// so a deletion at commit time is invisible to git.
|
|
226
|
+
await invalidateRetrievalArtefacts(repoRoot, stderr);
|
|
227
|
+
|
|
228
|
+
// Step 4b — append success audit. Sync will pick up any file changes
|
|
215
229
|
// the librarian produced on its next 30-minute tick.
|
|
216
230
|
await writeAudit(runsLog, {
|
|
217
231
|
ts: tsIso,
|
|
@@ -224,6 +238,35 @@ export async function runDistill({
|
|
|
224
238
|
return 0;
|
|
225
239
|
}
|
|
226
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Delete `operator/knowledge/.graph.json` and `.backlinks.json` under
|
|
243
|
+
* `root` if they exist. Missing files are a no-op; any other error is
|
|
244
|
+
* swallowed with a stderr note so a stale artefact never masks an
|
|
245
|
+
* otherwise-successful distillation exit code. Exported for tests.
|
|
246
|
+
*/
|
|
247
|
+
export async function invalidateRetrievalArtefacts(root, stderr = process.stderr) {
|
|
248
|
+
const targets = [
|
|
249
|
+
join(root, 'operator', 'knowledge', '.graph.json'),
|
|
250
|
+
join(root, 'operator', 'knowledge', '.backlinks.json'),
|
|
251
|
+
];
|
|
252
|
+
const removed = [];
|
|
253
|
+
for (const path of targets) {
|
|
254
|
+
try {
|
|
255
|
+
await unlink(path);
|
|
256
|
+
removed.push(basename(path));
|
|
257
|
+
} catch (e) {
|
|
258
|
+
if (e && e.code === 'ENOENT') continue;
|
|
259
|
+
log(`warning: failed to invalidate ${basename(path)}: ${e.message}`, { stderr });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (removed.length > 0) {
|
|
263
|
+
log(
|
|
264
|
+
`invalidated retrieval artefacts (${removed.join(', ')}); pa knowledge rebuilds on demand`,
|
|
265
|
+
{ stderr },
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
227
270
|
function isoCompactForSession(d) {
|
|
228
271
|
// Matches the historical `$(date -u +%Y%m%d-%H%M)` shape used pre-Node.
|
|
229
272
|
const pad = (n) => String(n).padStart(2, '0');
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Errors carry an `exitCode`. The CLI translates to `process.exit`.
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
14
15
|
|
|
15
|
-
import { buildPlan, buildRenderConfig } from '../generator.mjs';
|
|
16
|
+
import { buildPlan, buildRenderConfig, resolveInstalledDopVersion } from '../generator.mjs';
|
|
16
17
|
import {
|
|
17
18
|
defaultSeam,
|
|
18
19
|
promptValues,
|
|
@@ -35,7 +36,14 @@ export async function runInit(options) {
|
|
|
35
36
|
const config = await gatherConfig(options);
|
|
36
37
|
|
|
37
38
|
process.stdout.write(`\npa-init: scaffolding ${config.paName} for ${config.operatorName}\n`);
|
|
38
|
-
process.stdout.write(` target:
|
|
39
|
+
process.stdout.write(` target: ${config.targetDir}\n`);
|
|
40
|
+
// Announce the exact version pa-init pinned into the instance's
|
|
41
|
+
// package.json (or `<default>` when the resolver could not verify the
|
|
42
|
+
// installed umbrella version — dev-only edge case). This is the
|
|
43
|
+
// load-bearing signal for the "you got the right version" contract.
|
|
44
|
+
process.stdout.write(
|
|
45
|
+
` package: @stravica-ai/digital-operator-pa@${config.pkgVersion}\n`,
|
|
46
|
+
);
|
|
39
47
|
|
|
40
48
|
await assertTargetIsEmptyOrMissing(config.targetDir);
|
|
41
49
|
|
|
@@ -63,6 +71,25 @@ export async function runInit(options) {
|
|
|
63
71
|
return 2;
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
// Version-drift safety net (WSD-laptop cold install of 0.1.41). The
|
|
75
|
+
// scaffold pins an exact version and ships a `.npmrc` scope override,
|
|
76
|
+
// so pnpm should never resolve a different version — but if the guard
|
|
77
|
+
// ever misses, this prints one line naming the mismatch so a colleague
|
|
78
|
+
// can see it immediately instead of hitting a "Command X not found" on
|
|
79
|
+
// an older bin surface. Read from the SAME source `pa status` reads —
|
|
80
|
+
// the resolved package.json inside the instance's node_modules.
|
|
81
|
+
const scaffoldedVersion = config.pkgVersion;
|
|
82
|
+
const installedVersion = readInstalledDopVersion(config.targetDir);
|
|
83
|
+
if (scaffoldedVersion && installedVersion && installedVersion !== scaffoldedVersion) {
|
|
84
|
+
process.stderr.write(
|
|
85
|
+
`pa-init: WARNING — scaffold pinned @stravica-ai/digital-operator-pa@${scaffoldedVersion} `
|
|
86
|
+
+ `but pnpm resolved ${installedVersion}. This usually means pnpm's `
|
|
87
|
+
+ `minimumReleaseAge gate filtered the pinned version out; the shipped `
|
|
88
|
+
+ `.npmrc scopes minimumReleaseAge=0 for @stravica-ai which normally cures it. `
|
|
89
|
+
+ `Run \`pnpm install\` inside ${config.targetDir} again to reconcile.\n`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
66
93
|
process.stdout.write('\npa-init: running pa status …\n');
|
|
67
94
|
const statusCode = await paStatus({ targetDir: config.targetDir });
|
|
68
95
|
if (statusCode !== 0) {
|
|
@@ -142,6 +169,11 @@ async function gatherConfig(options) {
|
|
|
142
169
|
operatorName: resolvedValues.operatorName,
|
|
143
170
|
targetDir: resolvedValues.targetDir,
|
|
144
171
|
purpose: options.purpose,
|
|
172
|
+
// Pin the EXACT version of `@stravica-ai/digital-operator-pa` that is
|
|
173
|
+
// running pa-init right now. `resolveInstalledDopVersion()` returns
|
|
174
|
+
// null when it cannot verify a version (dev-only edge case); the
|
|
175
|
+
// downstream default in `buildRenderConfig` catches that safely.
|
|
176
|
+
pkgVersion: resolveInstalledDopVersion() ?? undefined,
|
|
145
177
|
});
|
|
146
178
|
}
|
|
147
179
|
|
|
@@ -168,3 +200,26 @@ function applyValidatorsOrThrow(supplied) {
|
|
|
168
200
|
function countEntries(plan) {
|
|
169
201
|
return plan.length;
|
|
170
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Read the installed version of `@stravica-ai/digital-operator-pa` from
|
|
206
|
+
* the instance's own `node_modules`. Returns null if the package is
|
|
207
|
+
* missing or its `package.json` cannot be parsed — the caller treats
|
|
208
|
+
* null as "no verifiable install" and does not warn.
|
|
209
|
+
*/
|
|
210
|
+
function readInstalledDopVersion(targetDir) {
|
|
211
|
+
const pkgPath = join(
|
|
212
|
+
targetDir,
|
|
213
|
+
'node_modules',
|
|
214
|
+
'@stravica-ai',
|
|
215
|
+
'digital-operator-pa',
|
|
216
|
+
'package.json',
|
|
217
|
+
);
|
|
218
|
+
if (!existsSync(pkgPath)) return null;
|
|
219
|
+
try {
|
|
220
|
+
const meta = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
221
|
+
return typeof meta.version === 'string' ? meta.version : null;
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -7,14 +7,92 @@
|
|
|
7
7
|
// kind: 'dir' (mkdir -p) | 'file' (write file with contents) | 'json'
|
|
8
8
|
// (stringify + write).
|
|
9
9
|
|
|
10
|
-
import { existsSync } from 'node:fs';
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
11
|
import { readFile, readdir } from 'node:fs/promises';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
12
13
|
import { dirname, join, resolve } from 'node:path';
|
|
13
14
|
import { fileURLToPath } from 'node:url';
|
|
14
15
|
|
|
15
16
|
import { emptyEnvelope, renderEnvelope } from '../../engineering-pa-core/src/autonomy/envelope.mjs';
|
|
16
17
|
|
|
17
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the exact version of `@stravica-ai/digital-operator-pa` that is
|
|
23
|
+
* running `pa-init` right now. Two lookup paths:
|
|
24
|
+
*
|
|
25
|
+
* 1. `require.resolve('@stravica-ai/digital-operator-pa/package.json')` — hits
|
|
26
|
+
* when pa-init runs out of a real installed tarball
|
|
27
|
+
* (`node_modules/@stravica-ai/digital-operator-pa/dist/engineering-pa-scaffold/src/generator.mjs`
|
|
28
|
+
* → `node_modules/@stravica-ai/digital-operator-pa/package.json`).
|
|
29
|
+
* 2. Sibling walk to `packages/engineering-pa/package.json` — hits when
|
|
30
|
+
* pa-init runs out of the source monorepo (dev + CI test runs).
|
|
31
|
+
*
|
|
32
|
+
* Returns the version string, or `null` if neither path resolves. `null`
|
|
33
|
+
* is the "no verifiable version" outcome — the caller falls back to the
|
|
34
|
+
* old `^0.1.0` default so a broken lookup never breaks the scaffold.
|
|
35
|
+
*
|
|
36
|
+
* Why this exists: on the WSD-laptop cold install of 0.1.41, pnpm's
|
|
37
|
+
* default `minimumReleaseAge` gated the freshly-published tarball out of
|
|
38
|
+
* `^0.1.0` and the instance silently resolved to 0.1.39 — a version with
|
|
39
|
+
* no `jira` / `confluence` / `confluence-fetch` bins. Pinning the exact
|
|
40
|
+
* version the running scaffolder came from kills that class of drift.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveInstalledDopVersion() {
|
|
43
|
+
// Path 1: `require.resolve` from another consumer's tree — hits when
|
|
44
|
+
// pa-init is invoked from the bootstrap dir, where the tarball sits at
|
|
45
|
+
// `node_modules/@stravica-ai/digital-operator-pa/`. The scaffold module
|
|
46
|
+
// itself lives INSIDE the tarball, so this resolve typically fails
|
|
47
|
+
// when this module runs from within `dist/engineering-pa-scaffold/`;
|
|
48
|
+
// path 3 catches that case.
|
|
49
|
+
try {
|
|
50
|
+
const pkgPath = requireFromHere.resolve('@stravica-ai/digital-operator-pa/package.json');
|
|
51
|
+
const meta = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
52
|
+
if (meta && typeof meta.version === 'string' && meta.version.length > 0) {
|
|
53
|
+
return meta.version;
|
|
54
|
+
}
|
|
55
|
+
} catch { /* fall through */ }
|
|
56
|
+
// Path 2: monorepo sibling walk — hits when pa-init runs directly
|
|
57
|
+
// from the source tree (dev + CI test runs). `packages/scaffold/src/`
|
|
58
|
+
// -> `packages/engineering-pa/package.json`.
|
|
59
|
+
const monorepoUmbrella = resolve(__dirname, '..', '..', 'engineering-pa', 'package.json');
|
|
60
|
+
if (existsSync(monorepoUmbrella)) {
|
|
61
|
+
try {
|
|
62
|
+
const meta = JSON.parse(readFileSync(monorepoUmbrella, 'utf8'));
|
|
63
|
+
if (meta && typeof meta.version === 'string' && meta.version.length > 0) {
|
|
64
|
+
return meta.version;
|
|
65
|
+
}
|
|
66
|
+
} catch { /* fall through */ }
|
|
67
|
+
}
|
|
68
|
+
// Path 3: packed-tarball walk — hits when this module runs from
|
|
69
|
+
// inside the umbrella tarball's dist tree. Layout is:
|
|
70
|
+
// <install>/node_modules/@stravica-ai/digital-operator-pa/package.json <-- want this
|
|
71
|
+
// <install>/node_modules/@stravica-ai/digital-operator-pa/dist/engineering-pa-scaffold/src/generator.mjs
|
|
72
|
+
// Walk upwards from `__dirname` looking for a `package.json` whose
|
|
73
|
+
// `name` is `@stravica-ai/digital-operator-pa`.
|
|
74
|
+
let cursor = __dirname;
|
|
75
|
+
for (let i = 0; i < 8; i += 1) {
|
|
76
|
+
const candidate = resolve(cursor, 'package.json');
|
|
77
|
+
if (existsSync(candidate)) {
|
|
78
|
+
try {
|
|
79
|
+
const meta = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
80
|
+
if (
|
|
81
|
+
meta
|
|
82
|
+
&& meta.name === '@stravica-ai/digital-operator-pa'
|
|
83
|
+
&& typeof meta.version === 'string'
|
|
84
|
+
&& meta.version.length > 0
|
|
85
|
+
) {
|
|
86
|
+
return meta.version;
|
|
87
|
+
}
|
|
88
|
+
} catch { /* keep walking */ }
|
|
89
|
+
}
|
|
90
|
+
const next = resolve(cursor, '..');
|
|
91
|
+
if (next === cursor) break;
|
|
92
|
+
cursor = next;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
18
96
|
|
|
19
97
|
/** Path to the CLAUDE.md template (relative to this module). */
|
|
20
98
|
export const CLAUDE_MD_TEMPLATE_PATH = resolve(__dirname, '..', 'templates', 'CLAUDE.md.template');
|
|
@@ -248,6 +326,37 @@ export function buildGitignore() {
|
|
|
248
326
|
].join('\n');
|
|
249
327
|
}
|
|
250
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Build the instance-level `.npmrc` contents.
|
|
331
|
+
*
|
|
332
|
+
* Overrides pnpm's default `minimumReleaseAge` (in effect since pnpm 10)
|
|
333
|
+
* FOR THE `@stravica-ai` scope only. Rationale from the WSD-laptop cold
|
|
334
|
+
* install of 0.1.41 (2026-09-16): pnpm's supply-chain gate silently
|
|
335
|
+
* resolves a freshly-published tarball's caret range down to the previous
|
|
336
|
+
* version — the instance ended up on 0.1.39 with no `jira` / `confluence`
|
|
337
|
+
* / `confluence-fetch` bins, and the guide's Atlassian step failed with
|
|
338
|
+
* "Command jira not found" on the very version the guide had just
|
|
339
|
+
* installed. The scaffold already pins an EXACT dependency spec (see
|
|
340
|
+
* `buildOperatorPackageJson` and `resolveInstalledDopVersion`), but an
|
|
341
|
+
* exact spec can still be filtered out under `minimumReleaseAge`; the
|
|
342
|
+
* scoped override belt-and-braces the pin so `pnpm install` inside the
|
|
343
|
+
* scaffolded instance always resolves the version the pin names.
|
|
344
|
+
* Non-`@stravica-ai` packages continue to honour the operator's own
|
|
345
|
+
* `minimumReleaseAge` if they have one set globally.
|
|
346
|
+
*/
|
|
347
|
+
export function buildInstanceNpmrc() {
|
|
348
|
+
return [
|
|
349
|
+
'# @stravica-ai scope override for pnpm supply-chain gating.',
|
|
350
|
+
'# pa-init pins an exact @stravica-ai/digital-operator-pa version in package.json.',
|
|
351
|
+
'# pnpm\'s default minimumReleaseAge can otherwise filter a freshly-published',
|
|
352
|
+
'# tarball out of resolution and drop the instance onto a previous version',
|
|
353
|
+
'# with a different bin surface (WSD-laptop cold-install regression, 0.1.41).',
|
|
354
|
+
'# Non-@stravica-ai packages still honour any minimumReleaseAge set globally.',
|
|
355
|
+
'@stravica-ai:minimumReleaseAge=0',
|
|
356
|
+
'',
|
|
357
|
+
].join('\n');
|
|
358
|
+
}
|
|
359
|
+
|
|
251
360
|
/**
|
|
252
361
|
* Build the operator/config/preferences.json contents.
|
|
253
362
|
*
|
|
@@ -546,6 +655,7 @@ export function buildPlanFromTemplates(config, templates) {
|
|
|
546
655
|
plan.push({ kind: 'json', path: 'package.json', contents: pkg });
|
|
547
656
|
plan.push({ kind: 'file', path: '.gitignore', contents: gitignore });
|
|
548
657
|
plan.push({ kind: 'file', path: '.nvmrc', contents: '24\n' });
|
|
658
|
+
plan.push({ kind: 'file', path: '.npmrc', contents: buildInstanceNpmrc() });
|
|
549
659
|
|
|
550
660
|
plan.push({ kind: 'json', path: '.claude/settings.json', contents: buildClaudeSettingsJson() });
|
|
551
661
|
// /research slash command: rendered from the packaged core doc so the
|
package/docs/onboarding-solo.md
CHANGED
|
@@ -21,17 +21,28 @@ Linux and WSL are equivalent. Install Node 24 through your platform's normal cha
|
|
|
21
21
|
|
|
22
22
|
```sh
|
|
23
23
|
mkdir ~/pa-bootstrap && cd ~/pa-bootstrap
|
|
24
|
-
pnpm init -y
|
|
24
|
+
# pnpm 11.14 rejects the `devEngines.packageManager` line `pnpm init -y` writes,
|
|
25
|
+
# and `-y` is silently ignored, so write a minimal package.json directly:
|
|
26
|
+
cat > package.json <<'JSON'
|
|
27
|
+
{ "name": "pa-bootstrap", "private": true }
|
|
28
|
+
JSON
|
|
25
29
|
pnpm add @stravica-ai/digital-operator-pa
|
|
26
30
|
pnpm exec pa-init --mode=solo --name=<pa-name> --operator=<your-name> \
|
|
27
31
|
--target=$HOME/<pa-name>-instance --non-interactive
|
|
28
32
|
cd $HOME/<pa-name>-instance
|
|
29
|
-
|
|
33
|
+
git config user.name "Your Name" # cold laptops have no global git identity
|
|
34
|
+
git config user.email "you@example.com" # local to this repo; skip if you already have a global one
|
|
30
35
|
git add -A && git commit -m "scaffold"
|
|
31
36
|
```
|
|
32
37
|
|
|
33
38
|
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.
|
|
34
39
|
|
|
40
|
+
`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 an `.npmrc` in the instance with `@stravica-ai:minimumReleaseAge=0`. Both 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.
|
|
41
|
+
|
|
42
|
+
`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.
|
|
43
|
+
|
|
44
|
+
**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).
|
|
45
|
+
|
|
35
46
|
## 2. Sanity-check the install
|
|
36
47
|
|
|
37
48
|
```sh
|
|
@@ -45,10 +56,11 @@ pnpm exec pa knowledge context # first line: library.mode = simple (variant
|
|
|
45
56
|
## 3. Start the workspace
|
|
46
57
|
|
|
47
58
|
```sh
|
|
48
|
-
pnpm exec pa workspace start
|
|
59
|
+
pnpm exec pa workspace start # opens http://localhost:4200
|
|
60
|
+
pnpm exec pa workspace start --port 4300 # pick a different port if 4200 is taken
|
|
49
61
|
```
|
|
50
62
|
|
|
51
|
-
The workspace serves a viewer, a home page, and an onboarding form. Long notes from your PA land here rather than in chat.
|
|
63
|
+
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.
|
|
52
64
|
|
|
53
65
|
## 4. Start your first session
|
|
54
66
|
|
|
@@ -114,7 +126,7 @@ Your PA has the `rcf-lite-engineer` role baked in. It builds greenfield applicat
|
|
|
114
126
|
|
|
115
127
|
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.
|
|
116
128
|
|
|
117
|
-
Both CLIs share three credentials pointing at the same Atlassian site.
|
|
129
|
+
Both CLIs share three credentials pointing at the same Atlassian site. Setup:
|
|
118
130
|
|
|
119
131
|
```sh
|
|
120
132
|
pnpm exec secrets set JIRA_DOMAIN yoursite # for yoursite.atlassian.net
|
|
@@ -122,6 +134,8 @@ pnpm exec secrets set JIRA_EMAIL you@example.com # your Atlassian accoun
|
|
|
122
134
|
pnpm exec secrets set JIRA_API_TOKEN <paste-token> # id.atlassian.com/manage-profile/security/api-tokens
|
|
123
135
|
```
|
|
124
136
|
|
|
137
|
+
`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.
|
|
138
|
+
|
|
125
139
|
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.
|
|
126
140
|
|
|
127
141
|
`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`:
|
package/package.json
CHANGED