@ouro.bot/cli 0.1.0-alpha.340 → 0.1.0-alpha.342

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/changelog.json CHANGED
@@ -1,6 +1,18 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.342",
6
+ "changes": [
7
+ "The nerves coverage gate now writes append-only per-test NDJSON records across Vitest workers, emits a redaction-safe heartbeat for every executed test, fails closed when per-test capture is missing or malformed, and scopes start/end pairing to explicit process lifecycle events so the full-suite audit is truthful instead of passable through empty artifacts."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.341",
12
+ "changes": [
13
+ "`ouro up` and `ouro doctor` now detect when PATH resolves `ouro` to a stale external launcher before the managed `~/.ouro-cli/bin/ouro` wrapper, then print exact path-specific remediation instead of leaving users stuck on an older shim."
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.340",
6
18
  "changes": [
@@ -485,6 +485,10 @@ async function performSystemSetup(deps) {
485
485
  if (installResult.repairedOldLauncher) {
486
486
  deps.writeStdout("repaired stale ouro launcher at ~/.local/bin/ouro");
487
487
  }
488
+ if (installResult.pathResolution?.status === "shadowed") {
489
+ deps.writeStdout(`fix ouro PATH: ${installResult.pathResolution.detail}; ` +
490
+ `fix: ${installResult.pathResolution.remediation}`);
491
+ }
488
492
  }
489
493
  catch (error) {
490
494
  (0, runtime_1.emitNervesEvent)({
@@ -2252,6 +2256,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2252
2256
  bundlesRoot: deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(),
2253
2257
  secretsRoot: deps.secretsRoot ?? path.join(os.homedir(), ".agentsecrets"),
2254
2258
  homedir: os.homedir(),
2259
+ envPath: process.env.PATH ?? "",
2255
2260
  };
2256
2261
  const doctorResult = await (0, doctor_1.runDoctorChecks)(doctorDeps);
2257
2262
  const output = (0, cli_render_doctor_1.formatDoctorOutput)(doctorResult);
@@ -7,6 +7,7 @@
7
7
  * "fail" check and the remaining categories still run.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.checkCliPath = checkCliPath;
10
11
  exports.checkDaemon = checkDaemon;
11
12
  exports.checkAgents = checkAgents;
12
13
  exports.checkSenses = checkSenses;
@@ -16,8 +17,32 @@ exports.checkDisk = checkDisk;
16
17
  exports.runDoctorChecks = runDoctorChecks;
17
18
  const runtime_1 = require("../../nerves/runtime");
18
19
  const bluebubbles_health_diagnostics_1 = require("./bluebubbles-health-diagnostics");
20
+ const ouro_path_installer_1 = require("../versioning/ouro-path-installer");
19
21
  const DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS = 30_000;
20
22
  // ── Category checkers ──
23
+ function checkCliPath(deps) {
24
+ const resolution = (0, ouro_path_installer_1.diagnoseOuroPath)({
25
+ homeDir: deps.homedir,
26
+ envPath: deps.envPath ?? "",
27
+ existsSync: deps.existsSync,
28
+ readFileSync: (p) => deps.readFileSync(p),
29
+ });
30
+ const status = resolution.status === "ok"
31
+ ? "pass"
32
+ : resolution.status === "shadowed"
33
+ ? "fail"
34
+ : "warn";
35
+ return {
36
+ name: "CLI",
37
+ checks: [{
38
+ label: "ouro PATH resolution",
39
+ status,
40
+ detail: resolution.remediation
41
+ ? `${resolution.detail}; fix: ${resolution.remediation}`
42
+ : resolution.detail,
43
+ }],
44
+ };
45
+ }
21
46
  async function checkDaemon(deps) {
22
47
  const checks = [];
23
48
  const socketExists = deps.existsSync(deps.socketPath);
@@ -365,6 +390,7 @@ function computeSummary(categories) {
365
390
  return { passed, warnings, failed };
366
391
  }
367
392
  const CATEGORY_CHECKERS = [
393
+ { name: "CLI", fn: checkCliPath },
368
394
  { name: "Daemon", fn: checkDaemon },
369
395
  { name: "Agents", fn: checkAgents },
370
396
  { name: "Senses", fn: checkSenses },
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.diagnoseOuroPath = diagnoseOuroPath;
36
37
  exports.installOuroCommand = installOuroCommand;
37
38
  const fs = __importStar(require("fs"));
38
39
  const os = __importStar(require("os"));
@@ -80,6 +81,9 @@ function detectShellProfile(homeDir, shell) {
80
81
  function isBinDirInPath(binDir, envPath) {
81
82
  return envPath.split(path.delimiter).some((p) => p === binDir);
82
83
  }
84
+ function samePath(a, b) {
85
+ return path.resolve(a) === path.resolve(b);
86
+ }
83
87
  function buildPathExportLine(binDir, shell) {
84
88
  const base = shell ? path.basename(shell) : /* v8 ignore next -- unreachable: only called when detectShellProfile returns non-null, which requires shell @preserve */ "";
85
89
  if (base === "fish") {
@@ -97,6 +101,56 @@ function isWrapperCurrent(scriptPath, existsSync, readFileSync) {
97
101
  return false;
98
102
  }
99
103
  }
104
+ function firstOuroOnPath(envPath, existsSync) {
105
+ for (const dir of envPath.split(path.delimiter)) {
106
+ if (!dir)
107
+ continue;
108
+ const candidate = path.join(dir, "ouro");
109
+ if (existsSync(candidate))
110
+ return candidate;
111
+ }
112
+ return null;
113
+ }
114
+ function diagnoseOuroPath(deps) {
115
+ const binDir = path.join(deps.homeDir, ".ouro-cli", "bin");
116
+ const expectedPath = path.join(binDir, "ouro");
117
+ const resolvedPath = firstOuroOnPath(deps.envPath, deps.existsSync);
118
+ if (!resolvedPath) {
119
+ return {
120
+ status: "missing",
121
+ expectedPath,
122
+ resolvedPath: null,
123
+ detail: `PATH does not resolve ouro; expected ${expectedPath}`,
124
+ remediation: `add ${binDir} to PATH or open a new shell after ouro up updates your shell profile`,
125
+ };
126
+ }
127
+ if (samePath(resolvedPath, expectedPath)) {
128
+ return {
129
+ status: "ok",
130
+ expectedPath,
131
+ resolvedPath,
132
+ detail: `PATH resolves ouro to ${expectedPath}`,
133
+ remediation: null,
134
+ };
135
+ }
136
+ if (isWrapperCurrent(resolvedPath, deps.existsSync, deps.readFileSync)) {
137
+ return {
138
+ status: "ok",
139
+ expectedPath,
140
+ resolvedPath,
141
+ detail: `PATH resolves ouro through a compatible wrapper at ${resolvedPath}`,
142
+ remediation: null,
143
+ };
144
+ }
145
+ const shadowDir = path.dirname(resolvedPath);
146
+ return {
147
+ status: "shadowed",
148
+ expectedPath,
149
+ resolvedPath,
150
+ detail: `PATH resolves ouro to ${resolvedPath} before ${expectedPath}`,
151
+ remediation: `move ${binDir} before ${shadowDir} in PATH, or remove/replace ${resolvedPath} after confirming it is the stale ouro launcher`,
152
+ };
153
+ }
100
154
  function installOuroCommand(deps = {}) {
101
155
  /* v8 ignore start -- dep defaults: only used in real runtime, tests always inject @preserve */
102
156
  const platform = deps.platform ?? process.platform;
@@ -126,6 +180,7 @@ function installOuroCommand(deps = {}) {
126
180
  const binDir = path.join(homeDir, ".ouro-cli", "bin");
127
181
  const scriptPath = path.join(binDir, "ouro");
128
182
  const oldScriptPath = path.join(homeDir, ".local", "bin", "ouro");
183
+ const resolvePath = () => diagnoseOuroPath({ homeDir, envPath, existsSync, readFileSync });
129
184
  const modernCurrent = isWrapperCurrent(scriptPath, existsSync, readFileSync);
130
185
  const oldExists = existsSync(oldScriptPath);
131
186
  const oldCurrent = oldExists && isWrapperCurrent(oldScriptPath, existsSync, readFileSync);
@@ -152,13 +207,23 @@ function installOuroCommand(deps = {}) {
152
207
  }
153
208
  // ── Fast-path: modern wrapper already current ──
154
209
  if (modernCurrent) {
210
+ const pathResolution = resolvePath();
211
+ if (pathResolution.status === "shadowed") {
212
+ (0, runtime_1.emitNervesEvent)({
213
+ level: "warn",
214
+ component: "daemon",
215
+ event: "daemon.ouro_path_shadowed",
216
+ message: "PATH resolves ouro to a stale external launcher",
217
+ meta: { resolvedPath: pathResolution.resolvedPath, expectedPath: pathResolution.expectedPath, remediation: pathResolution.remediation },
218
+ });
219
+ }
155
220
  (0, runtime_1.emitNervesEvent)({
156
221
  component: "daemon",
157
222
  event: "daemon.ouro_path_install_skip",
158
223
  message: "ouro command already installed",
159
- meta: { scriptPath },
224
+ meta: { scriptPath, pathStatus: pathResolution.status, resolvedPath: pathResolution.resolvedPath },
160
225
  });
161
- return { installed: false, scriptPath, pathReady: isBinDirInPath(binDir, envPath), shellProfileUpdated: null, skippedReason: "already-installed", repairedOldLauncher };
226
+ return { installed: false, scriptPath, pathReady: isBinDirInPath(binDir, envPath), shellProfileUpdated: null, skippedReason: "already-installed", repairedOldLauncher, pathResolution };
162
227
  }
163
228
  (0, runtime_1.emitNervesEvent)({
164
229
  component: "daemon",
@@ -211,11 +276,21 @@ function installOuroCommand(deps = {}) {
211
276
  }
212
277
  }
213
278
  }
279
+ const pathResolution = resolvePath();
280
+ if (pathResolution.status === "shadowed") {
281
+ (0, runtime_1.emitNervesEvent)({
282
+ level: "warn",
283
+ component: "daemon",
284
+ event: "daemon.ouro_path_shadowed",
285
+ message: "PATH resolves ouro to a stale external launcher",
286
+ meta: { resolvedPath: pathResolution.resolvedPath, expectedPath: pathResolution.expectedPath, remediation: pathResolution.remediation },
287
+ });
288
+ }
214
289
  (0, runtime_1.emitNervesEvent)({
215
290
  component: "daemon",
216
291
  event: "daemon.ouro_path_install_end",
217
292
  message: "ouro command installed",
218
- meta: { scriptPath, pathReady, shellProfileUpdated, oldScriptPath: oldExists ? oldScriptPath : null },
293
+ meta: { scriptPath, pathReady, shellProfileUpdated, oldScriptPath: oldExists ? oldScriptPath : null, pathStatus: pathResolution.status, resolvedPath: pathResolution.resolvedPath },
219
294
  });
220
- return { installed: true, scriptPath, pathReady, shellProfileUpdated, repairedOldLauncher };
295
+ return { installed: true, scriptPath, pathReady, shellProfileUpdated, repairedOldLauncher, pathResolution };
221
296
  }
@@ -2,16 +2,25 @@
2
2
  /**
3
3
  * Per-test audit rules for nerves event coverage.
4
4
  *
5
- * Rule 1: every-test-emits -- every test must emit at least one event
6
- * Rule 2: start/end pairing -- _start events must have matching _end or _error
5
+ * Rule 1: every-test-emits -- every captured test must emit at least one event
6
+ * Rule 2: lifecycle start/end pairing -- process-scoped _start events must have matching _end or _error
7
7
  * Rule 3: error context -- error-level events must have non-empty meta
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LIFECYCLE_PAIRED_STARTS = void 0;
10
11
  exports.checkEveryTestEmits = checkEveryTestEmits;
11
12
  exports.checkStartEndPairing = checkStartEndPairing;
12
13
  exports.checkErrorContext = checkErrorContext;
14
+ // Only these starts represent process-scoped lifecycle contracts. Most nerves
15
+ // `_start` events are local operation markers that can be legitimately observed
16
+ // by a narrow unit test without driving the whole operation to completion.
17
+ exports.LIFECYCLE_PAIRED_STARTS = new Set([
18
+ "daemon.server_start",
19
+ "daemon.update_checker_start",
20
+ "daemon.apply_pending_updates_start",
21
+ ]);
13
22
  /**
14
- * Rule 1: Every test must emit at least one nerves event.
23
+ * Rule 1: Every captured test must emit at least one nerves event.
15
24
  */
16
25
  function checkEveryTestEmits(data) {
17
26
  if (!data || typeof data !== "object") {
@@ -22,13 +31,13 @@ function checkEveryTestEmits(data) {
22
31
  .filter(([, events]) => !Array.isArray(events) || events.length === 0)
23
32
  .map(([name]) => name);
24
33
  return {
25
- status: silent.length === 0 ? "pass" : "fail",
34
+ status: entries.length > 0 && silent.length === 0 ? "pass" : "fail",
26
35
  total_tests: entries.length,
27
36
  silent_tests: silent,
28
37
  };
29
38
  }
30
39
  /**
31
- * Rule 2: _start events must have matching _end or _error within the same test.
40
+ * Rule 2: Process-scoped lifecycle _start events must have matching _end or _error within the same test.
32
41
  */
33
42
  function checkStartEndPairing(data) {
34
43
  if (!data || typeof data !== "object") {
@@ -39,7 +48,7 @@ function checkStartEndPairing(data) {
39
48
  if (!Array.isArray(events))
40
49
  continue;
41
50
  const eventNames = events.map((e) => e.event);
42
- const startEvents = eventNames.filter((name) => name.endsWith("_start"));
51
+ const startEvents = eventNames.filter((name) => exports.LIFECYCLE_PAIRED_STARTS.has(name));
43
52
  for (const startEvent of startEvents) {
44
53
  const prefix = startEvent.slice(0, -"_start".length);
45
54
  const hasEnd = eventNames.some((name) => name === `${prefix}_end`);
@@ -69,12 +69,38 @@ function readPerTestData(perTestPath) {
69
69
  if (!perTestPath || !(0, fs_1.existsSync)(perTestPath))
70
70
  return null;
71
71
  try {
72
- return JSON.parse((0, fs_1.readFileSync)(perTestPath, "utf8"));
72
+ const raw = (0, fs_1.readFileSync)(perTestPath, "utf8").trim();
73
+ if (!raw)
74
+ return null;
75
+ try {
76
+ const parsed = JSON.parse(raw);
77
+ if (isPerTestRecord(parsed)) {
78
+ return { [parsed.testName]: parsed.events };
79
+ }
80
+ return parsed;
81
+ }
82
+ catch {
83
+ const perTestData = {};
84
+ for (const line of raw.split("\n").map((entry) => entry.trim()).filter(Boolean)) {
85
+ const parsed = JSON.parse(line);
86
+ if (!isPerTestRecord(parsed))
87
+ return null;
88
+ const existing = perTestData[parsed.testName] ?? [];
89
+ perTestData[parsed.testName] = existing.concat(parsed.events);
90
+ }
91
+ return perTestData;
92
+ }
73
93
  }
74
94
  catch {
75
95
  return null;
76
96
  }
77
97
  }
98
+ function isPerTestRecord(value) {
99
+ if (!value || typeof value !== "object")
100
+ return false;
101
+ const record = value;
102
+ return typeof record.testName === "string" && Array.isArray(record.events);
103
+ }
78
104
  function scanSourceFiles(sourceRoot) {
79
105
  const filesWithKeys = new Map();
80
106
  const fileContents = new Map();
@@ -35,7 +35,7 @@ function runAuditCli(argv) {
35
35
  return 2;
36
36
  }
37
37
  const eventsPath = args.eventsPath ?? (0, path_1.join)(runDir, "vitest-events.ndjson");
38
- const perTestPath = args.perTestPath ?? (0, path_1.join)(runDir, "vitest-events-per-test.json");
38
+ const perTestPath = args.perTestPath ?? (0, path_1.join)(runDir, "vitest-events-per-test.ndjson");
39
39
  const sourceRoot = args.sourceRoot ?? (0, path_1.resolve)("src");
40
40
  const outputPath = args.output ?? (0, path_1.join)(runDir, "nerves-coverage.json");
41
41
  const report = (0, audit_1.auditNervesCoverage)({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.340",
3
+ "version": "0.1.0-alpha.342",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",