@stravica-ai/digital-operator-pa 0.1.53 → 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/engineering-pa-cli/src/coordinator-hook/config.mjs +2 -2
  2. package/dist/engineering-pa-cli/src/environment/probes.mjs +23 -34
  3. package/dist/engineering-pa-cli/src/pa/cli.mjs +71 -51
  4. package/dist/engineering-pa-cli/src/pa/projects.mjs +7 -0
  5. package/dist/engineering-pa-cli/src/pa/work-cli.mjs +26 -0
  6. package/dist/engineering-pa-cli/src/pa/work.mjs +9 -0
  7. package/dist/engineering-pa-cli/src/secrets/cli.mjs +18 -16
  8. package/dist/engineering-pa-cli/src/secrets/commands/get.mjs +6 -1
  9. package/dist/engineering-pa-cli/src/secrets/commands/set.mjs +3 -3
  10. package/dist/engineering-pa-cli/src/secrets/commands/status.mjs +17 -0
  11. package/dist/engineering-pa-cli/src/secrets/vault.mjs +90 -52
  12. package/dist/engineering-pa-core/docs/autonomy-envelope-tracker.md +22 -22
  13. package/dist/engineering-pa-core/docs/blueprints/git-identity/blueprint.md +21 -19
  14. package/dist/engineering-pa-core/docs/blueprints/scheduled-distillation/blueprint.md +2 -2
  15. package/dist/engineering-pa-core/docs/commands/init.md +1 -1
  16. package/dist/engineering-pa-core/docs/commands/onboard.md +46 -39
  17. package/dist/engineering-pa-core/docs/commands/research.md +66 -66
  18. package/dist/engineering-pa-core/docs/commands/upgrade.md +4 -4
  19. package/dist/engineering-pa-core/docs/entity-location-map.md +4 -4
  20. package/dist/engineering-pa-core/docs/lock.md +18 -18
  21. package/dist/engineering-pa-core/docs/memory-sync.md +1 -1
  22. package/dist/engineering-pa-core/docs/pa-operating-model.md +87 -87
  23. package/dist/engineering-pa-core/docs/scheduling.md +1 -1
  24. package/dist/engineering-pa-core/docs/standards/README.md +2 -2
  25. package/dist/engineering-pa-core/docs/standards/briefing-contract.md +18 -18
  26. package/dist/engineering-pa-core/docs/standards/tooling.md +41 -41
  27. package/dist/engineering-pa-core/docs/tools/distill.md +19 -19
  28. package/dist/engineering-pa-core/docs/tools/memory-sync.md +14 -14
  29. package/dist/engineering-pa-core/docs/tools/scheduler.md +15 -15
  30. package/dist/engineering-pa-core/docs/tools/slice.md +18 -18
  31. package/dist/engineering-pa-core/docs/upstream-issue-filing.md +6 -6
  32. package/dist/engineering-pa-core/docs/working-directory.md +3 -3
  33. package/dist/engineering-pa-core/docs/workspace-operations.md +11 -11
  34. package/dist/engineering-pa-core/package.json +3 -3
  35. package/dist/engineering-pa-core/src/capabilities/registry.mjs +88 -0
  36. package/dist/engineering-pa-core/src/distill/health.mjs +16 -6
  37. package/dist/engineering-pa-core/src/home/resolve.mjs +48 -0
  38. package/dist/engineering-pa-core/src/instance-root/find.mjs +14 -3
  39. package/dist/engineering-pa-core/src/memory-sync/repo.mjs +2 -2
  40. package/dist/engineering-pa-core/src/scheduler/install.mjs +8 -7
  41. package/dist/engineering-pa-core/src/scheduler/list.mjs +2 -2
  42. package/dist/engineering-pa-core/src/scheduler/remove.mjs +65 -11
  43. package/dist/engineering-pa-core/src/scheduler/verify.mjs +2 -2
  44. package/dist/engineering-pa-core/src/vault/require.mjs +13 -3
  45. package/dist/engineering-pa-core/src/workspace/resolve.mjs +184 -0
  46. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-folder.mjs +3 -2
  47. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/link.mjs +2 -1
  48. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sync-statuses.mjs +5 -1
  49. package/dist/engineering-pa-library-cli/package.json +1 -0
  50. package/dist/engineering-pa-library-cli/src/pull-clones.mjs +12 -3
  51. package/dist/engineering-pa-library-tunnel/package.json +3 -0
  52. package/dist/engineering-pa-library-tunnel/src/config.mjs +2 -2
  53. package/dist/engineering-pa-migrate-inspect/package.json +4 -1
  54. package/dist/engineering-pa-migrate-inspect/src/age-key-migrate.mjs +2 -2
  55. package/dist/engineering-pa-migrate-inspect/src/inspector.mjs +2 -2
  56. package/dist/engineering-pa-migrate-inspect/src/platforms/macos.mjs +2 -1
  57. package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +8 -0
  58. package/dist/engineering-pa-scaffold/src/generator.mjs +22 -1
  59. package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +2 -0
  60. package/dist/engineering-pa-workspace/app/notes.html +16 -3
  61. package/dist/engineering-pa-workspace/app/project-detail.html +7 -0
  62. package/dist/engineering-pa-workspace/app/reviews.html +19 -2
  63. package/dist/engineering-pa-workspace/app/viewer.html +5 -0
  64. package/dist/engineering-pa-workspace/src/api.mjs +49 -15
  65. package/dist/engineering-pa-workspace/src/install-daemon.mjs +6 -5
  66. package/dist/engineering-pa-workspace/src/review/api-handler.mjs +53 -2
  67. package/dist/engineering-pa-workspace/src/supervisor.mjs +5 -4
  68. package/docs/onboarding-solo.md +7 -0
  69. package/package.json +7 -5
@@ -20,7 +20,7 @@
20
20
  import { readFile } from 'node:fs/promises';
21
21
  import { dirname, join, resolve } from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
- import { homedir } from 'node:os';
23
+ import { resolveHome } from '../../../engineering-pa-core/src/home/resolve.mjs';
24
24
 
25
25
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
26
  const CORE_CONFIG_PATH = resolve(__dirname, '..', '..', 'config', 'coordinator-allowlist.json');
@@ -98,7 +98,7 @@ export function mergeOverlay(config, overlay, warnings = []) {
98
98
  * lives at `.claude/coordinator-overlay.json` under that directory.
99
99
  */
100
100
  export function overlayPaths({ repoRoot, cwd, env = process.env }) {
101
- const home = env.HOME || homedir();
101
+ const home = resolveHome({ env });
102
102
  const workspaceBase = repoRoot || cwd;
103
103
  return {
104
104
  home: home ? join(home, '.claude', 'coordinator-overlay.json') : null,
@@ -6,11 +6,12 @@
6
6
  import { execFile } from 'node:child_process';
7
7
  import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
8
8
  import { promisify } from 'node:util';
9
- import { arch, platform, release, homedir } from 'node:os';
9
+ import { arch, platform, release } from 'node:os';
10
10
  import { basename, join, resolve, dirname } from 'node:path';
11
11
  import { resolve4 } from 'node:dns/promises';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { readPreferencesSync } from '../../../engineering-pa-core/src/preferences.mjs';
14
+ import { resolveWorkspace as coreResolveWorkspace } from '../../../engineering-pa-core/src/workspace/resolve.mjs';
14
15
 
15
16
  const execFileP = promisify(execFile);
16
17
 
@@ -204,45 +205,33 @@ async function withTimeout(promise, ms) {
204
205
  }
205
206
 
206
207
  /**
207
- * Workspace port + reachability. Reads port from operator preferences if
208
- * present; probes with a TCP connect (fast — no HTTP round-trip). Timeout
209
- * capped at 1s.
208
+ * Workspace port + identity, resolved through the ONE shared resolver
209
+ * in @stravica-ai/engineering-pa-core/src/workspace/resolve.mjs (0.1.54
210
+ * fix, w-2026-09-21-dave-002, F6/F7b). The 0.1.53 probe read a bare TCP
211
+ * connect on the default port and called any accepting listener
212
+ * `running` — which reported Docker on 4200 as our workspace and made
213
+ * every viewer link the PA handed out wrong. The shared resolver reads
214
+ * the persisted `workspacePort`, honours the running-port pidfile, and
215
+ * identifies the incumbent via `/__api/identity` before reporting
216
+ * `running`.
210
217
  */
211
218
  export async function probeWorkspace(repoRoot) {
212
- const port = readWorkspacePort(repoRoot);
213
- const net = await import('node:net');
214
- const running = await new Promise((res) => {
215
- const socket = new net.Socket();
216
- let done = false;
217
- const finish = (value) => {
218
- if (done) return;
219
- done = true;
220
- try { socket.destroy(); } catch { /* ignore */ }
221
- res(value);
222
- };
223
- socket.setTimeout(1000);
224
- socket.once('connect', () => finish(true));
225
- socket.once('error', () => finish(false));
226
- socket.once('timeout', () => finish(false));
227
- socket.connect(port, '127.0.0.1');
228
- });
219
+ const resolved = await coreResolveWorkspace({ repoRoot });
229
220
  return {
230
- port,
231
- running,
232
- url: `http://localhost:${port}`,
221
+ port: resolved.effectivePort,
222
+ configuredPort: resolved.configuredPort,
223
+ runningPort: resolved.runningPort,
224
+ // Legacy boolean kept for callers that still read `running`. True
225
+ // only when identity matched — a foreign process on the port is
226
+ // never running as far as this probe is concerned.
227
+ running: resolved.state === 'running',
228
+ state: resolved.state,
229
+ identity: resolved.identity,
230
+ detail: resolved.detail,
231
+ url: resolved.url,
233
232
  };
234
233
  }
235
234
 
236
- function readWorkspacePort(repoRoot) {
237
- try {
238
- const prefs = readPreferencesSync(repoRoot);
239
- if (prefs && typeof prefs.workspacePort === 'number') return prefs.workspacePort;
240
- } catch {
241
- // fall through
242
- }
243
- return 4200;
244
- }
245
-
246
235
  /**
247
236
  * Working directory + project root resolution. Search-upward for an
248
237
  * `agents/<name>/CLAUDE.md` marker or a `.git` ancestor. Env var override
@@ -17,6 +17,10 @@ import {
17
17
  } from '../../../engineering-pa-core/src/distill/health.mjs';
18
18
  import { vaultState } from '../../../engineering-pa-core/src/vault/require.mjs';
19
19
  import { enabledToolsNeedingSecrets } from '../../../engineering-pa-core/src/vault/tool-needs.mjs';
20
+ import {
21
+ resolveWorkspace as coreResolveWorkspace,
22
+ formatIncumbentClause,
23
+ } from '../../../engineering-pa-core/src/workspace/resolve.mjs';
20
24
  import { resolveDefaultScope } from '../secrets/vault.mjs';
21
25
  import { main as runMigrateInspect } from '../../../engineering-pa-migrate-inspect/src/cli.mjs';
22
26
  import { doOnboard } from './onboard.mjs';
@@ -63,6 +67,34 @@ export async function main(argv) {
63
67
  return 0;
64
68
  }
65
69
 
70
+ // 0.1.55 item 4 (w-2026-09-21-dave-004): `pa --version` / `pa -v`
71
+ // prints the installed package version. Before this landing there
72
+ // was no way to ask an installed CLI which version it was, so the
73
+ // 0.1.54 sweep had to `cat node_modules/.../package.json` by hand.
74
+ if (argv[0] === '--version' || argv[0] === '-v') {
75
+ const { readFile } = await import('node:fs/promises');
76
+ const { fileURLToPath } = await import('node:url');
77
+ const { dirname: dn, join: jn } = await import('node:path');
78
+ try {
79
+ // packages/cli/src/pa/cli.mjs -> ../../..(three-up)/package.json
80
+ // works in-repo. The installed tarball layout resolves the
81
+ // package.json via the exports map added in this same release.
82
+ let pkgJson;
83
+ try {
84
+ pkgJson = await import('@stravica-ai/digital-operator-pa/package.json', { with: { type: 'json' } });
85
+ } catch {
86
+ const here = dn(fileURLToPath(import.meta.url));
87
+ const local = jn(here, '..', '..', '..', 'engineering-pa', 'package.json');
88
+ pkgJson = { default: JSON.parse(await readFile(local, 'utf8')) };
89
+ }
90
+ process.stdout.write((pkgJson.default?.version || 'unknown') + '\n');
91
+ return 0;
92
+ } catch (e) {
93
+ process.stderr.write(`[pa] failed to resolve version: ${e?.message || e}\n`);
94
+ return 1;
95
+ }
96
+ }
97
+
66
98
  const verb = argv[0];
67
99
  const rest = argv.slice(1);
68
100
 
@@ -184,23 +216,32 @@ async function doStatus(opts = {}) {
184
216
 
185
217
  // 3. Workspace reachability probe.
186
218
  //
187
- // 0.1.50 (item 12): report BOTH the running port (from `.ws.pid` when
188
- // present) and the configured port when they differ, so the operator
189
- // can see when the auto-probe moved the server onto a different port
190
- // than preferences declare. Probe the running port when available,
191
- // falling back to the configured port.
219
+ // 0.1.54 (w-2026-09-21-dave-002, A2/A3): resolved through the ONE
220
+ // shared workspace resolver in core/workspace/resolve.mjs so `pa
221
+ // status`, `pa environment` and `pa workspace start`'s auto-probe
222
+ // all report the same fact. The 0.1.53 F1 finding — the "nothing of
223
+ // yours to reclaim" clause landed only on `workspace start`'s
224
+ // hard-fail branch and never on `pa status` — is closed by rendering
225
+ // the clause here whenever the incumbent is not ours.
192
226
  out.write('\nWorkspace:\n');
193
- const runningPort = readRunningWorkspacePort(repoRoot);
194
- const effectivePort = runningPort ?? workspacePort;
195
- const probeUrl = `http://localhost:${effectivePort}/__api/identity`;
196
- const reachability = await probeWorkspace(probeUrl, opts.fetchImpl);
227
+ const resolved = await coreResolveWorkspace({ repoRoot, fetchImpl: opts.fetchImpl });
228
+ const probeUrl = `${resolved.url}/__api/identity`;
197
229
  out.write(` ${probeUrl}\n`);
198
- if (runningPort !== null && runningPort !== workspacePort) {
199
- out.write(` running port: ${runningPort} (configured: ${workspacePort})\n`);
230
+ if (resolved.runningPort !== null && resolved.runningPort !== resolved.configuredPort) {
231
+ out.write(` running port: ${resolved.runningPort} (configured: ${resolved.configuredPort})\n`);
200
232
  }
201
- out.write(` status: ${reachability.status}`);
202
- if (reachability.detail) out.write(` (${reachability.detail})`);
233
+ const statusLabel = resolved.state === 'running'
234
+ ? 'reachable'
235
+ : resolved.state === 'other-process'
236
+ ? 'in use by another process'
237
+ : 'not running';
238
+ out.write(` status: ${statusLabel}`);
239
+ if (resolved.detail) out.write(` (${resolved.detail})`);
203
240
  out.write('\n');
241
+ const incumbent = formatIncumbentClause(resolved);
242
+ if (incumbent) {
243
+ out.write(` Note: ${incumbent}.\n`);
244
+ }
204
245
  // Workspace-not-running is OK — operators may run pa status before
205
246
  // starting the workspace server.
206
247
 
@@ -323,48 +364,27 @@ function resolveWorkspacePort(repoRoot, opts = {}) {
323
364
  * (see packages/workspace/src/api.mjs). Any other body on the port means
324
365
  * the port is bound by an unrelated process, not our workspace.
325
366
  */
326
- const WORKSPACE_IDENTITY_SERVER = 'digital-operator-pa-workspace';
367
+ import { probeWorkspaceIdentity, WORKSPACE_IDENTITY_SERVER as CORE_WORKSPACE_IDENTITY_SERVER } from '../../../engineering-pa-core/src/workspace/resolve.mjs';
368
+ const WORKSPACE_IDENTITY_SERVER = CORE_WORKSPACE_IDENTITY_SERVER;
327
369
 
370
+ /**
371
+ * Legacy signature retained for existing tests. All new callers go via
372
+ * `coreResolveWorkspace` in packages/core/src/workspace/resolve.mjs.
373
+ * The `{status, detail}` shape is preserved for the WSD-laptop probe
374
+ * tests; internally it delegates to the shared identity probe.
375
+ */
328
376
  async function probeWorkspace(url, fetchImpl) {
329
- const fetchFn = fetchImpl ?? globalThis.fetch;
330
- if (typeof fetchFn !== 'function') {
331
- return { status: 'error', detail: 'fetch unavailable' };
377
+ const probe = await probeWorkspaceIdentity(url, { fetchImpl });
378
+ if (probe.state === 'running') {
379
+ return { status: 'reachable', detail: probe.detail };
332
380
  }
333
- const controller = new AbortController();
334
- const timer = setTimeout(() => controller.abort(), 1500);
335
- try {
336
- const res = await fetchFn(url, { signal: controller.signal });
337
- // Any HTTP response has to be positively identified as OUR workspace
338
- // before the probe reports "reachable". WSD-laptop cold install of
339
- // 0.1.41 (2026-09-16): a docker container bound to :4200 answered
340
- // /__api/identity with HTTP 404 and `pa status` reported the
341
- // workspace "reachable (HTTP 404)". Identity-gate the probe so a
342
- // foreign process on the port surfaces as "in use by another
343
- // process" instead of a false green.
344
- if (res.status >= 200 && res.status < 500) {
345
- let body = null;
346
- try {
347
- body = await res.json();
348
- } catch { /* not JSON — falls through to the identity-mismatch branch */ }
349
- if (body && body.server === WORKSPACE_IDENTITY_SERVER) {
350
- return { status: 'reachable', detail: `HTTP ${res.status}` };
351
- }
352
- return {
353
- status: 'in use by another process',
354
- detail: `HTTP ${res.status}, /__api/identity did not identify as ${WORKSPACE_IDENTITY_SERVER}`,
355
- };
356
- }
357
- return { status: 'error', detail: `HTTP ${res.status}` };
358
- } catch (e) {
359
- // ECONNREFUSED / abort / DNS — server is not running.
360
- const code = e && (e.cause?.code || e.code);
361
- if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || e?.name === 'AbortError') {
362
- return { status: 'not running', detail: null };
363
- }
364
- return { status: 'error', detail: e?.message || String(e) };
365
- } finally {
366
- clearTimeout(timer);
381
+ if (probe.state === 'other-process') {
382
+ return { status: 'in use by another process', detail: probe.detail };
383
+ }
384
+ if (probe.detail === null) {
385
+ return { status: 'not running', detail: null };
367
386
  }
387
+ return { status: probe.detail && probe.detail.startsWith('HTTP') ? 'error' : 'error', detail: probe.detail };
368
388
  }
369
389
 
370
390
  export { doStatus, probeWorkspace, resolveWorkspacePort };
@@ -17,6 +17,7 @@ import { writeFile } from 'node:fs/promises';
17
17
  import { join } from 'node:path';
18
18
 
19
19
  import { getRepoRoot } from './repo-root.mjs';
20
+ import { findRepoRootSync } from '../../../engineering-pa-core/src/instance-root/find.mjs';
20
21
  import { readJson, writeJson, ensureDir, listSubdirs, today } from './io.mjs';
21
22
  import { addDoc, removeDocByPath, setDocStatus, DEFAULT_GROUP, VALID_DOC_STATUSES } from './doc-groups.mjs';
22
23
  import { normaliseDocPath } from './doc-path.mjs';
@@ -111,6 +112,12 @@ export async function createProject(opts) {
111
112
  validateCategory(opts.category);
112
113
  validatePriority(opts.priority);
113
114
 
115
+ // 0.1.55 F1: refuse a project create from outside any operator instance
116
+ // with the strict findRepoRootSync error (exit 2, message naming the
117
+ // start directory and the markers walked). Keeps the failure shape
118
+ // aligned with pa work create and the scaffold write paths.
119
+ findRepoRootSync({ strict: true });
120
+
114
121
  const repoRoot = await getRepoRoot();
115
122
  const projDir = join(repoRoot, 'projects', opts.id);
116
123
  const projectJsonPath = join(projDir, 'project.json');
@@ -126,8 +126,34 @@ async function readPreferencesPaName() {
126
126
  }
127
127
  }
128
128
 
129
+ // 0.1.55 item 4 (w-2026-09-21-dave-004): refuse unknown flags loudly.
130
+ // The pre-0.1.55 parser silently dropped anything not consumed downstream,
131
+ // so a typo like `--status` (only valid on `work status`) or an assumed
132
+ // `--description` (there is no such flag; the field is `summary`) went
133
+ // unnoticed and the created record was missing the value the operator
134
+ // meant to set. Recorded as an incident in w-2026-09-21-dave-002's
135
+ // summary.
136
+ const CREATE_ALLOWED_FLAGS = new Set([
137
+ 'project',
138
+ 'title',
139
+ 'category',
140
+ 'priority',
141
+ 'summary',
142
+ 'owner',
143
+ 'assignee',
144
+ 'author',
145
+ ]);
146
+
129
147
  async function doCreate(args) {
130
148
  const { flags } = parseFlags(args);
149
+ const unknown = Object.keys(flags).filter((k) => !CREATE_ALLOWED_FLAGS.has(k));
150
+ if (unknown.length > 0) {
151
+ const list = unknown.map((k) => `--${k}`).join(', ');
152
+ const known = Array.from(CREATE_ALLOWED_FLAGS).map((k) => `--${k}`).sort().join(', ');
153
+ process.stderr.write(`[pa work create] unknown flag(s): ${list}\n`);
154
+ process.stderr.write(` accepted flags: ${known}\n`);
155
+ return 1;
156
+ }
131
157
  const priority = flags.priority != null && flags.priority !== true
132
158
  ? Number(flags.priority)
133
159
  : null;
@@ -25,6 +25,7 @@ import {
25
25
  maxNForAuthorOnDate,
26
26
  } from '../../../engineering-pa-core/src/workItemId/index.mjs';
27
27
 
28
+ import { findRepoRootSync } from '../../../engineering-pa-core/src/instance-root/find.mjs';
28
29
  import { getRepoRoot } from './repo-root.mjs';
29
30
  import {
30
31
  readJson,
@@ -128,6 +129,14 @@ export async function createWork(opts) {
128
129
  validateCategory(opts.category);
129
130
  validatePriority(opts.priority);
130
131
 
132
+ // 0.1.55 F1: refuse a mint from outside any operator instance BEFORE the
133
+ // author-token check. Pre-fix the flow bailed with the author-token error
134
+ // (exit 1), which read as a config problem in the caller's terminal; the
135
+ // real cause is that the cwd carries none of the instance markers. Strict
136
+ // findRepoRootSync throws with exit 2 and names both the start directory
137
+ // and the markers walked, matching the sibling rule in projects.mjs.
138
+ findRepoRootSync({ strict: true });
139
+
131
140
  let author;
132
141
  try {
133
142
  author = resolveAuthorToken({
@@ -19,7 +19,7 @@ import { runFind } from './commands/find.mjs';
19
19
  import { runVerify } from './commands/verify.mjs';
20
20
  import { resolveScopePaths, VaultError } from './vault.mjs';
21
21
 
22
- const HELP = `secrets — SOPS+age secrets management
22
+ const HELP = `secrets: SOPS+age secrets management
23
23
 
24
24
  Usage:
25
25
  secrets init Initialise the vault (generate keys, create empty secrets file).
@@ -27,7 +27,8 @@ Usage:
27
27
  --stdin-json: after init, seed KEYs from a JSON object
28
28
  read on stdin (values never echoed; refuses a TTY stdin).
29
29
  secrets get KEY Retrieve a secret (add --exists for a present/absent check that never prints the value)
30
- secrets set KEY VALUE Store or update a secret (use \`-\` as VALUE to read from stdin)
30
+ secrets set KEY - Read the value from stdin. Safe form: keeps the value out of shell history and argv (0.1.54, w-2026-09-21-dave-002, D4)
31
+ secrets set KEY VALUE Store or update a secret (the argv form is retained but records the value in shell history and the process listing)
31
32
  Since 0.1.20, canonical-first dual-write is
32
33
  the DEFAULT for scoped writes when a canonical
33
34
  clone for the scope is discoverable on this
@@ -45,7 +46,7 @@ Usage:
45
46
  secrets status Check vault health and report key info
46
47
  secrets backup Show QR code of master key for backup (opens browser)
47
48
  secrets vaults List every vault visible from the workspace
48
- (scope, layout, key count, path — no decryption)
49
+ (scope, layout, key count, path; no decryption)
49
50
  secrets find KEY Report which vault(s) contain KEY by name
50
51
  (alias: which; names/paths only, never values;
51
52
  exits 1 when not found)
@@ -53,7 +54,7 @@ Usage:
53
54
  Byte-level comparison of DECRYPTED values
54
55
  between this vault and a reference vault at
55
56
  <path>. Reports per-key same/differs/missing.
56
- --values is required — decrypt-only is never
57
+ --values is required; decrypt-only is never
57
58
  a valid pass (see below). Never prints values.
58
59
 
59
60
  Age private key resolution (0.1.51, w-2026-09-18-dave-007). One shared
@@ -75,13 +76,13 @@ SOPS config: .vault/.sops.yaml (committed to repo)
75
76
  Workspace resolution (all verbs), in order:
76
77
  1. Walk up from the current directory for a vault-bearing workspace
77
78
  (.vault/ or vault/*.enc.json). A git repo WITHOUT a vault does not
78
- stop resolution — it falls through to the next tiers with a stderr
79
+ stop resolution; it falls through to the next tiers with a stderr
79
80
  notice naming the store actually targeted.
80
81
  2. $DAVE_ROOT (or $DAVE_REPO_ROOT / $PA_REPO_ROOT).
81
82
  3. "workspaceRoot" in ~/.config/stravica-pa/defaults.json (written by
82
83
  you or provisioning; the CLI never creates it).
83
84
  4. Error listing what was tried.
84
- NOTE: this precedence is a change — the environment variable used to be
85
+ NOTE: this precedence is a change; the environment variable used to be
85
86
  checked BEFORE the directory walk. Standing inside a workspace now
86
87
  always targets THAT workspace, even when $DAVE_ROOT points elsewhere.
87
88
  Exception: 'secrets init' inside a vault-less git repo initialises the
@@ -93,7 +94,7 @@ Scoped vaults:
93
94
  up to the repo root and checking, in order:
94
95
  .vault/scopes/NAME.enc.json (workspace layout)
95
96
  vault/NAME.enc.json (canonical layout)
96
- Both present is an error — pass --file to
97
+ Both present is an error; pass --file to
97
98
  disambiguate. Supported on: get, set, list,
98
99
  delete, rotate, status, vaults, find.
99
100
  --file PATH Bypass scope resolution and operate directly on
@@ -145,38 +146,38 @@ Default scope (get/set only, when neither --scope nor --file is given):
145
146
  in order:
146
147
  1. $SECRETS_DEFAULT_SCOPE env var.
147
148
  2. "defaultScope" in ~/.config/stravica-pa/defaults.json (same file
148
- as "workspaceRoot" above; written by you or provisioning — the
149
+ as "workspaceRoot" above; written by you or provisioning; the
149
150
  CLI never creates it).
150
151
  3. The calling PA's own agents/<name>/identity.json "defaultScope"
151
152
  field, found by walking up from the current directory (or, for
152
153
  Dave specifically, $DAVE_ROOT/agents/dave/identity.json). A
153
154
  malformed identity.json is a stderr warning, not a crash.
154
- 4. Legacy fallback: the single-file .vault/ vault — the notice says
155
+ 4. Legacy fallback: the single-file .vault/ vault; the notice says
155
156
  so explicitly ("no scope specified and no default configured").
156
157
  Example notices:
157
158
  note: writing to 'hq-estate' pot (default from SECRETS_DEFAULT_SCOPE env var)
158
159
  note: writing to 'wsd' pot (default from /home/dex/agents/barry/dex/identity.json)
159
- note: writing to '(legacy)' pot — no scope specified and no default configured. Use --scope <name> or configure a default.
160
+ note: writing to '(legacy)' pot (no scope specified and no default configured). Use --scope <name> or configure a default.
160
161
 
161
162
  Canonical-first writes (set only):
162
- Canonical-first dual-write is DEFAULT for scoped writes since 0.1.20 —
163
+ Canonical-first dual-write is DEFAULT for scoped writes since 0.1.20:
163
164
  a plain 'secrets set' on a scoped vault writes canonical FIRST and
164
165
  workspace SECOND whenever a canonical clone for the scope is
165
166
  discoverable on this host. Discovery order, first hit wins:
166
167
  1. Explicit --canonical-clone PATH (always wins, bootstraps a
167
168
  missing file).
168
- 2. "canonicalClone" in ~/.config/stravica-pa/defaults.json — either:
169
+ 2. "canonicalClone" in ~/.config/stravica-pa/defaults.json, in one of two shapes:
169
170
  object (scope → file map):
170
171
  { "canonicalClone": { "seo-ops": "/abs/vault/seo-ops.enc.json" } }
171
172
  string (root dir), derives <root>/vault/<scope>.enc.json:
172
173
  { "canonicalClone": "/Users/thefoot/stravica-canonical/stravica-operator-barry" }
173
174
  3. Estate convention: ~/stravica-canonical/*/vault/<scope>.enc.json
174
175
  (exactly one match; ambiguity or zero matches skips this tier).
175
- Absence is a NOTICE, never a failure — a host with no canonical layout
176
+ Absence is a NOTICE, never a failure; a host with no canonical layout
176
177
  keeps writing to workspace only, with a one-line stderr line naming why
177
178
  nothing resolved. If any canonical write fails (existing store
178
179
  undecryptable, sops encrypt failure), the command aborts with exit 2
179
- BEFORE touching the workspace copy — the ordering invariant is
180
+ BEFORE touching the workspace copy; the ordering invariant is
180
181
  identical to the flag path since it always was.
181
182
 
182
183
  --canonical-clone PATH Force PATH as the canonical target (wins over
@@ -187,13 +188,13 @@ Canonical-first writes (set only):
187
188
  Value-compare verification:
188
189
  secrets verify --values --against <path>
189
190
  Decrypts BOTH the resolved vault and the vault at <path>, then
190
- compares every key's VALUE byte-for-byte — never decrypt-only. A
191
+ compares every key's VALUE byte-for-byte; never decrypt-only. A
191
192
  vault clobbered by an always-take-canonical materialisation still
192
193
  decrypts cleanly with placeholder/stale values (same recipients);
193
194
  only a value comparison against a known-good reference catches that.
194
195
  Reports per-key same/differs/missing-in-working/missing-in-reference.
195
196
  Exits 2 if any key differs or is missing on either side. Never
196
- prints a decrypted value — only the comparison result per key name.
197
+ prints a decrypted value; only the comparison result per key name.
197
198
  `;
198
199
 
199
200
  const USAGE = `Usage: secrets {init|get|set|list|delete|rotate|status|backup|vaults|find|verify} [KEY] [VALUE]
@@ -201,6 +202,7 @@ const USAGE = `Usage: secrets {init|get|set|list|delete|rotate|status|backup|vau
201
202
  Commands:
202
203
  init Initialise the vault (generate keys, create config)
203
204
  get KEY Retrieve a secret value
205
+ set KEY - Read the value from stdin (safe form; 0.1.54)
204
206
  set KEY VALUE Store or update a secret (canonical-first by default; --no-canonical to opt out)
205
207
  list List all secret key names
206
208
  delete KEY Remove a secret
@@ -30,7 +30,12 @@ export async function runGet(args, overrides = {}) {
30
30
  // resolveCommandPaths prints the interim stderr notice (naming the
31
31
  // target vault) whenever --scope/--file wasn't given upstream in
32
32
  // cli.mjs. See vault.mjs's default-scope resolution section.
33
- const paths = resolveCommandPaths(overrides);
33
+ //
34
+ // 0.1.54 fix (w-2026-09-21-dave-002, D4): suppress the legacy-pot
35
+ // notice on value-blind reads. `--exists` is scripted-friendly and
36
+ // an interactive operator running `get KEY --exists` doesn't need
37
+ // the routing tip on every call.
38
+ const paths = resolveCommandPaths({ ...overrides, suppressLegacyNotice: exists });
34
39
  const key = rest[0];
35
40
 
36
41
  if (!key) {
@@ -65,7 +65,7 @@ export async function runSet(args, overrides = {}) {
65
65
  let value = rest[1];
66
66
 
67
67
  if (!key || value == null || value === '') {
68
- console.error('Usage: secrets set KEY VALUE [--canonical-clone PATH | --no-canonical] (use `-` as VALUE to read from stdin)');
68
+ console.error('Usage: secrets set KEY - [--canonical-clone PATH | --no-canonical] (reads the value from stdin; the safe form)\n secrets set KEY VALUE [--canonical-clone PATH | --no-canonical] (records the value in shell history and argv)');
69
69
  throw new VaultError('usage', 1);
70
70
  }
71
71
 
@@ -82,7 +82,7 @@ export async function runSet(args, overrides = {}) {
82
82
  // legitimately contains it.
83
83
  value = raw.replace(/\r?\n$/, '');
84
84
  if (value === '') {
85
- console.error('Empty value on stdin — refusing to store an empty secret.');
85
+ console.error('Empty value on stdin; refusing to store an empty secret.');
86
86
  throw new VaultError('empty stdin', 1);
87
87
  }
88
88
  }
@@ -184,7 +184,7 @@ export async function runSet(args, overrides = {}) {
184
184
  const tail = resolved.tried && resolved.tried.length > 0
185
185
  ? ` (tried: ${resolved.tried.join('; ')})`
186
186
  : '';
187
- console.error(`notice: no canonical clone configured for scope '${paths.scope}' on this host — workspace-only write${tail}`);
187
+ console.error(`notice: no canonical clone configured for scope '${paths.scope}' on this host; workspace-only write${tail}`);
188
188
  }
189
189
  }
190
190
  }
@@ -13,6 +13,7 @@ import {
13
13
  sopsConfigExists,
14
14
  sopsDecrypt,
15
15
  } from '../vault.mjs';
16
+ import { resolveKeyFile as coreResolveKeyFile } from '../../../../engineering-pa-core/src/vault/require.mjs';
16
17
 
17
18
  async function printDepVersions() {
18
19
  console.log('');
@@ -42,6 +43,22 @@ export async function runStatus(overrides = {}) {
42
43
  console.log('============');
43
44
  console.log('');
44
45
 
46
+ // 0.1.54 (w-2026-09-21-dave-002, A1 diagnostic): print the resolver
47
+ // datapoint on one line so anyone hitting a decrypt-failure has the
48
+ // resolved keyFile + source + mode value-blind. Barry ruling
49
+ // 2026-09-21 (post-QA): the secrets surfaces make the datapoint
50
+ // visible without asking; the F4 root cause is only findable when
51
+ // every failing host reports the same shape.
52
+ try {
53
+ const keyResolved = coreResolveKeyFile({
54
+ repoRoot: paths.root,
55
+ home: paths.home,
56
+ env: {},
57
+ mode: 'auto',
58
+ });
59
+ console.log(`Key resolver: ${keyResolved.keyFile} (source: ${keyResolved.source}, mode: ${keyResolved.mode})`);
60
+ } catch { /* non-fatal; render nothing */ }
61
+
45
62
  // Private key
46
63
  if (keyFileExists(paths)) {
47
64
  console.log(`Private key: ${paths.keyFile} (present)`);