@openclawbrain/cli 0.4.13 → 0.4.14

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.
@@ -4054,15 +4054,36 @@ function normalizeOutcomeMatchText(value) {
4054
4054
  ? value.replace(/\s+/g, " ").trim().toLowerCase()
4055
4055
  : null;
4056
4056
  }
4057
+ function normalizeObservationOptionalString(value) {
4058
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
4059
+ }
4060
+ function buildObservationSelectionDigestKey(selectionDigest, activePackGraphChecksum) {
4061
+ const normalizedSelectionDigest = normalizeObservationOptionalString(selectionDigest);
4062
+ const normalizedGraphChecksum = normalizeObservationOptionalString(activePackGraphChecksum);
4063
+ if (normalizedSelectionDigest === null || normalizedGraphChecksum === null) {
4064
+ return null;
4065
+ }
4066
+ return `${normalizedGraphChecksum}|${normalizedSelectionDigest}`;
4067
+ }
4057
4068
  function parseObservationRouteMetadata(value) {
4058
4069
  if (typeof value !== "string" || value.trim().length === 0) {
4059
4070
  return {
4071
+ serveDecisionRecordId: null,
4072
+ selectionDigest: null,
4073
+ turnCompileEventId: null,
4074
+ activePackId: null,
4075
+ activePackGraphChecksum: null,
4060
4076
  selectedNodeIds: [],
4061
4077
  selectedPathNodeIds: []
4062
4078
  };
4063
4079
  }
4064
4080
  try {
4065
4081
  const parsed = JSON.parse(value);
4082
+ const serveDecisionRecordId = normalizeObservationOptionalString(parsed?.serveDecisionRecordId);
4083
+ const selectionDigest = normalizeObservationOptionalString(parsed?.selectionDigest);
4084
+ const turnCompileEventId = normalizeObservationOptionalString(parsed?.turnCompileEventId);
4085
+ const activePackId = normalizeObservationOptionalString(parsed?.activePackId);
4086
+ const activePackGraphChecksum = normalizeObservationOptionalString(parsed?.activePackGraphChecksum);
4066
4087
  const selectedNodeIds = Array.isArray(parsed?.selectedNodeIds)
4067
4088
  ? parsed.selectedNodeIds.filter((entry) => typeof entry === "string")
4068
4089
  : [];
@@ -4070,12 +4091,22 @@ function parseObservationRouteMetadata(value) {
4070
4091
  ? parsed.selectedPathNodeIds.filter((entry) => typeof entry === "string")
4071
4092
  : [];
4072
4093
  return {
4094
+ serveDecisionRecordId,
4095
+ selectionDigest,
4096
+ turnCompileEventId,
4097
+ activePackId,
4098
+ activePackGraphChecksum,
4073
4099
  selectedNodeIds,
4074
4100
  selectedPathNodeIds
4075
4101
  };
4076
4102
  }
4077
4103
  catch {
4078
4104
  return {
4105
+ serveDecisionRecordId: null,
4106
+ selectionDigest: null,
4107
+ turnCompileEventId: null,
4108
+ activePackId: null,
4109
+ activePackGraphChecksum: null,
4079
4110
  selectedNodeIds: [],
4080
4111
  selectedPathNodeIds: []
4081
4112
  };
@@ -4099,8 +4130,24 @@ function loadTeacherObservationOutcomesFromActivation(activationRoot) {
4099
4130
  if (table === undefined) {
4100
4131
  return [];
4101
4132
  }
4133
+ const columns = new Set(db.prepare("PRAGMA table_info(brain_observations)").all().map((column) => String(column.name ?? "")));
4134
+ const selectableColumns = [
4135
+ "id",
4136
+ "query_text",
4137
+ "route_metadata_json",
4138
+ "final_score",
4139
+ "confidence",
4140
+ "created_at",
4141
+ "updated_at",
4142
+ "evaluated_at",
4143
+ columns.has("serve_decision_record_id") ? "serve_decision_record_id" : "NULL AS serve_decision_record_id",
4144
+ columns.has("selection_digest") ? "selection_digest" : "NULL AS selection_digest",
4145
+ columns.has("turn_compile_event_id") ? "turn_compile_event_id" : "NULL AS turn_compile_event_id",
4146
+ columns.has("active_pack_id") ? "active_pack_id" : "NULL AS active_pack_id",
4147
+ columns.has("active_pack_graph_checksum") ? "active_pack_graph_checksum" : "NULL AS active_pack_graph_checksum"
4148
+ ];
4102
4149
  const rows = db.prepare(`
4103
- SELECT id, query_text, route_metadata_json, final_score, confidence, created_at, updated_at, evaluated_at
4150
+ SELECT ${selectableColumns.join(", ")}
4104
4151
  FROM brain_observations
4105
4152
  WHERE final_score IS NOT NULL
4106
4153
  ORDER BY created_at DESC
@@ -4116,6 +4163,11 @@ function loadTeacherObservationOutcomesFromActivation(activationRoot) {
4116
4163
  createdAt: Number.isFinite(Number(row.created_at)) ? Number(row.created_at) : null,
4117
4164
  updatedAt: Number.isFinite(Number(row.updated_at)) ? Number(row.updated_at) : null,
4118
4165
  evaluatedAt: Number.isFinite(Number(row.evaluated_at)) ? Number(row.evaluated_at) : null,
4166
+ serveDecisionRecordId: normalizeObservationOptionalString(row.serve_decision_record_id) ?? routeMetadata.serveDecisionRecordId,
4167
+ selectionDigest: normalizeObservationOptionalString(row.selection_digest) ?? routeMetadata.selectionDigest,
4168
+ turnCompileEventId: normalizeObservationOptionalString(row.turn_compile_event_id) ?? routeMetadata.turnCompileEventId,
4169
+ activePackId: normalizeObservationOptionalString(row.active_pack_id) ?? routeMetadata.activePackId,
4170
+ activePackGraphChecksum: normalizeObservationOptionalString(row.active_pack_graph_checksum) ?? routeMetadata.activePackGraphChecksum,
4119
4171
  selectedNodeIds: routeMetadata.selectedNodeIds,
4120
4172
  selectedPathNodeIds: routeMetadata.selectedPathNodeIds
4121
4173
  };
@@ -4197,45 +4249,172 @@ function compareDecisionObservationMatch(left, right) {
4197
4249
  }
4198
4250
  return 0;
4199
4251
  }
4252
+ function emptyTeacherObservationBindingStats() {
4253
+ return {
4254
+ totalObservationCount: 0,
4255
+ nonZeroObservationCount: 0,
4256
+ skippedZeroRewardCount: 0,
4257
+ matched: {
4258
+ exactDecisionId: 0,
4259
+ exactSelectionDigest: 0,
4260
+ turnCompileEventId: 0,
4261
+ legacyHeuristic: 0
4262
+ },
4263
+ unmatched: {
4264
+ exactDecisionId: 0,
4265
+ exactSelectionDigest: 0,
4266
+ turnCompileEventId: 0,
4267
+ legacyHeuristic: 0
4268
+ },
4269
+ ambiguous: {
4270
+ exactDecisionId: 0,
4271
+ exactSelectionDigest: 0,
4272
+ turnCompileEventId: 0,
4273
+ legacyHeuristic: 0
4274
+ }
4275
+ };
4276
+ }
4277
+ function summarizeTeacherObservationBindingStats(stats) {
4278
+ return [
4279
+ `matched(exact_decision_id=${stats.matched.exactDecisionId},exact_selection_digest=${stats.matched.exactSelectionDigest},turn_compile_event_id=${stats.matched.turnCompileEventId},legacy_heuristic=${stats.matched.legacyHeuristic})`,
4280
+ `unmatched(exact_decision_id=${stats.unmatched.exactDecisionId},exact_selection_digest=${stats.unmatched.exactSelectionDigest},turn_compile_event_id=${stats.unmatched.turnCompileEventId},legacy_heuristic=${stats.unmatched.legacyHeuristic})`,
4281
+ `ambiguous(exact_decision_id=${stats.ambiguous.exactDecisionId},exact_selection_digest=${stats.ambiguous.exactSelectionDigest},turn_compile_event_id=${stats.ambiguous.turnCompileEventId},legacy_heuristic=${stats.ambiguous.legacyHeuristic})`,
4282
+ `non_zero=${stats.nonZeroObservationCount}`,
4283
+ `skipped_zero=${stats.skippedZeroRewardCount}`
4284
+ ].join(" ");
4285
+ }
4286
+ function buildTeacherObservationDecisionIndexes(decisions) {
4287
+ const byRecordId = new Map();
4288
+ const bySelectionDigest = new Map();
4289
+ const ambiguousSelectionDigests = new Set();
4290
+ const byTurnCompileEventId = new Map();
4291
+ const ambiguousTurnCompileEventIds = new Set();
4292
+ for (const decision of decisions) {
4293
+ byRecordId.set(decision.recordId, decision);
4294
+ const selectionDigestKey = buildObservationSelectionDigestKey(decision.selectionDigest, decision.activePackGraphChecksum);
4295
+ if (selectionDigestKey !== null) {
4296
+ if (bySelectionDigest.has(selectionDigestKey)) {
4297
+ bySelectionDigest.delete(selectionDigestKey);
4298
+ ambiguousSelectionDigests.add(selectionDigestKey);
4299
+ }
4300
+ else if (!ambiguousSelectionDigests.has(selectionDigestKey)) {
4301
+ bySelectionDigest.set(selectionDigestKey, decision);
4302
+ }
4303
+ }
4304
+ const turnCompileEventId = normalizeObservationOptionalString(decision.turnCompileEventId);
4305
+ if (turnCompileEventId !== null) {
4306
+ if (byTurnCompileEventId.has(turnCompileEventId)) {
4307
+ byTurnCompileEventId.delete(turnCompileEventId);
4308
+ ambiguousTurnCompileEventIds.add(turnCompileEventId);
4309
+ }
4310
+ else if (!ambiguousTurnCompileEventIds.has(turnCompileEventId)) {
4311
+ byTurnCompileEventId.set(turnCompileEventId, decision);
4312
+ }
4313
+ }
4314
+ }
4315
+ return {
4316
+ byRecordId,
4317
+ bySelectionDigest,
4318
+ ambiguousSelectionDigests,
4319
+ byTurnCompileEventId,
4320
+ ambiguousTurnCompileEventIds
4321
+ };
4322
+ }
4323
+ function resolveLegacyTeacherObservationMatch(decisions, observation) {
4324
+ const matches = decisions
4325
+ .map((decision) => {
4326
+ const key = decisionObservationMatchKey(decision, observation);
4327
+ return key === null
4328
+ ? null
4329
+ : {
4330
+ decision,
4331
+ key
4332
+ };
4333
+ })
4334
+ .filter((entry) => entry !== null)
4335
+ .sort((left, right) => compareDecisionObservationMatch(left.key, right.key));
4336
+ const best = matches[0] ?? null;
4337
+ const runnerUp = matches[1] ?? null;
4338
+ if (best === null) {
4339
+ return { kind: "unmatched", mode: "legacyHeuristic" };
4340
+ }
4341
+ if (runnerUp !== null && compareDecisionObservationMatch(best.key, runnerUp.key) === 0) {
4342
+ return { kind: "ambiguous", mode: "legacyHeuristic" };
4343
+ }
4344
+ return {
4345
+ kind: "matched",
4346
+ mode: "legacyHeuristic",
4347
+ decision: best.decision
4348
+ };
4349
+ }
4350
+ function resolveTeacherObservationDecisionMatch(decisions, observation, indexes) {
4351
+ const serveDecisionRecordId = normalizeObservationOptionalString(observation.serveDecisionRecordId);
4352
+ if (serveDecisionRecordId !== null) {
4353
+ const decision = indexes.byRecordId.get(serveDecisionRecordId) ?? null;
4354
+ return decision === null
4355
+ ? { kind: "unmatched", mode: "exactDecisionId" }
4356
+ : { kind: "matched", mode: "exactDecisionId", decision };
4357
+ }
4358
+ const selectionDigestKey = buildObservationSelectionDigestKey(observation.selectionDigest, observation.activePackGraphChecksum);
4359
+ if (selectionDigestKey !== null) {
4360
+ if (indexes.ambiguousSelectionDigests.has(selectionDigestKey)) {
4361
+ return { kind: "ambiguous", mode: "exactSelectionDigest" };
4362
+ }
4363
+ const decision = indexes.bySelectionDigest.get(selectionDigestKey) ?? null;
4364
+ return decision === null
4365
+ ? { kind: "unmatched", mode: "exactSelectionDigest" }
4366
+ : { kind: "matched", mode: "exactSelectionDigest", decision };
4367
+ }
4368
+ if (normalizeObservationOptionalString(observation.selectionDigest) !== null
4369
+ || normalizeObservationOptionalString(observation.activePackGraphChecksum) !== null) {
4370
+ return { kind: "unmatched", mode: "exactSelectionDigest" };
4371
+ }
4372
+ const turnCompileEventId = normalizeObservationOptionalString(observation.turnCompileEventId);
4373
+ if (turnCompileEventId !== null) {
4374
+ if (indexes.ambiguousTurnCompileEventIds.has(turnCompileEventId)) {
4375
+ return { kind: "ambiguous", mode: "turnCompileEventId" };
4376
+ }
4377
+ const decision = indexes.byTurnCompileEventId.get(turnCompileEventId) ?? null;
4378
+ return decision === null
4379
+ ? { kind: "unmatched", mode: "turnCompileEventId" }
4380
+ : { kind: "matched", mode: "turnCompileEventId", decision };
4381
+ }
4382
+ return resolveLegacyTeacherObservationMatch(decisions, observation);
4383
+ }
4200
4384
  function joinDecisionsWithTeacherObservationOutcomes(decisions, observationOutcomes) {
4201
4385
  const outcomes = new Map();
4386
+ const stats = emptyTeacherObservationBindingStats();
4202
4387
  for (const decision of decisions) {
4203
4388
  outcomes.set(decision.recordId, 0);
4204
4389
  }
4205
4390
  if (!Array.isArray(observationOutcomes) || observationOutcomes.length === 0) {
4206
- return outcomes;
4391
+ return { outcomes, stats };
4207
4392
  }
4393
+ const indexes = buildTeacherObservationDecisionIndexes(decisions);
4394
+ stats.totalObservationCount = observationOutcomes.length;
4208
4395
  for (const observation of observationOutcomes) {
4209
- const matches = decisions
4210
- .map((decision) => {
4211
- const key = decisionObservationMatchKey(decision, observation);
4212
- return key === null
4213
- ? null
4214
- : {
4215
- decision,
4216
- key
4217
- };
4218
- })
4219
- .filter((entry) => entry !== null)
4220
- .sort((left, right) => compareDecisionObservationMatch(left.key, right.key));
4221
- const best = matches[0] ?? null;
4222
- const runnerUp = matches[1] ?? null;
4223
- if (best === null) {
4396
+ const reward = clampTeacherObservationOutcome(observation.finalScore);
4397
+ if (reward === 0) {
4398
+ stats.skippedZeroRewardCount += 1;
4224
4399
  continue;
4225
4400
  }
4226
- if (runnerUp !== null && compareDecisionObservationMatch(best.key, runnerUp.key) === 0) {
4401
+ stats.nonZeroObservationCount += 1;
4402
+ const match = resolveTeacherObservationDecisionMatch(decisions, observation, indexes);
4403
+ if (match.kind === "unmatched") {
4404
+ stats.unmatched[match.mode] += 1;
4227
4405
  continue;
4228
4406
  }
4229
- const reward = clampTeacherObservationOutcome(observation.finalScore);
4230
- if (reward === 0) {
4407
+ if (match.kind === "ambiguous") {
4408
+ stats.ambiguous[match.mode] += 1;
4231
4409
  continue;
4232
4410
  }
4233
- const current = outcomes.get(best.decision.recordId) ?? 0;
4411
+ stats.matched[match.mode] += 1;
4412
+ const current = outcomes.get(match.decision.recordId) ?? 0;
4234
4413
  if (current === 0 || Math.abs(reward) > Math.abs(current)) {
4235
- outcomes.set(best.decision.recordId, reward);
4414
+ outcomes.set(match.decision.recordId, reward);
4236
4415
  }
4237
4416
  }
4238
- return outcomes;
4417
+ return { outcomes, stats };
4239
4418
  }
4240
4419
 
4241
4420
  export function joinDecisionsWithFeedback(decisions, eventExport, maxDelayMs = 300_000) {
@@ -4627,8 +4806,8 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4627
4806
  const remappedServeTimeDecisions = serveTimeDecisions.map((decision) => remapServeTimeDecisionToGraph(decision, resolveBlockId));
4628
4807
  // 2. Join serve-time decisions with explicit feedback first, then let teacher-v2 observations override when available.
4629
4808
  const outcomeMap = joinDecisionsWithFeedback(remappedServeTimeDecisions, eventExport);
4630
- const teacherObservationOutcomeMap = joinDecisionsWithTeacherObservationOutcomes(remappedServeTimeDecisions, teacherObservationOutcomes);
4631
- for (const [decisionId, reward] of teacherObservationOutcomeMap.entries()) {
4809
+ const teacherObservationBindings = joinDecisionsWithTeacherObservationOutcomes(remappedServeTimeDecisions, teacherObservationOutcomes);
4810
+ for (const [decisionId, reward] of teacherObservationBindings.outcomes.entries()) {
4632
4811
  if (reward !== 0) {
4633
4812
  outcomeMap.set(decisionId, reward);
4634
4813
  }
@@ -4695,7 +4874,8 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4695
4874
  if (fallback.policyUpdates.length > 0) {
4696
4875
  return {
4697
4876
  artifact: fallback,
4698
- updatedBaseline: currentBaseline
4877
+ updatedBaseline: currentBaseline,
4878
+ observationBindingStats: teacherObservationBindings.stats
4699
4879
  };
4700
4880
  }
4701
4881
  }
@@ -4717,7 +4897,7 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4717
4897
  : remappedServeTimeDecisions.length === 0
4718
4898
  ? "no serve-time decisions supplied for V2 learned routing refresh"
4719
4899
  : supervisedTrajectoryCount === 0
4720
- ? "no outcomes found for serve-time decisions"
4900
+ ? `no outcomes found for serve-time decisions (${summarizeTeacherObservationBindingStats(teacherObservationBindings.stats)})`
4721
4901
  : "trajectory updates produced no learned routing delta";
4722
4902
  const artifact = {
4723
4903
  routerIdentity: `${packId}:route_fn`,
@@ -4765,7 +4945,11 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4765
4945
  traces,
4766
4946
  policyUpdates
4767
4947
  };
4768
- return { artifact, updatedBaseline: currentBaseline };
4948
+ return {
4949
+ artifact,
4950
+ updatedBaseline: currentBaseline,
4951
+ observationBindingStats: teacherObservationBindings.stats
4952
+ };
4769
4953
  }
4770
4954
  function resolveEventExport(input) {
4771
4955
  if (input.eventExports === undefined) {
@@ -4922,6 +5106,7 @@ export function buildCandidatePack(input) {
4922
5106
  const vectors = createVectorsPayload(graph);
4923
5107
  let router = null;
4924
5108
  let updatedBaseline = null;
5109
+ let observationBindingStats = null;
4925
5110
  const teacherObservationOutcomes = input.activationRoot === undefined
4926
5111
  ? []
4927
5112
  : loadTeacherObservationOutcomesFromActivation(input.activationRoot);
@@ -4930,6 +5115,7 @@ export function buildCandidatePack(input) {
4930
5115
  const v2Result = createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, input.serveTimeDecisions, input.baselineState ?? initBaseline(), input.sparseFeedback, input.principalBacklog, teacherObservationOutcomes);
4931
5116
  router = v2Result.artifact;
4932
5117
  updatedBaseline = v2Result.updatedBaseline;
5118
+ observationBindingStats = v2Result.observationBindingStats;
4933
5119
  }
4934
5120
  else {
4935
5121
  router = createRouterArtifact(packId, builtAt, graph, vectors, eventExport, input.sparseFeedback, input.principalBacklog);
@@ -5045,7 +5231,8 @@ export function buildCandidatePack(input) {
5045
5231
  pgVersionUsed: input.learnedRouting ? (useV2 ? "v2" : "v1") : null,
5046
5232
  decisionLogCount,
5047
5233
  fallbackReason,
5048
- updatedBaseline
5234
+ updatedBaseline,
5235
+ observationBindingStats
5049
5236
  },
5050
5237
  summary: {
5051
5238
  packId,
@@ -19,6 +19,22 @@ function normalizeOptionalCliString(value) {
19
19
  return trimmed.length > 0 ? trimmed : null;
20
20
  }
21
21
 
22
+ function normalizeReportedProofPath(filePath) {
23
+ const normalizedPath = normalizeOptionalCliString(filePath);
24
+ if (normalizedPath === null) {
25
+ return null;
26
+ }
27
+ if (normalizedPath === "~") {
28
+ return homedir();
29
+ }
30
+ if (normalizedPath.startsWith("~/")) {
31
+ return path.join(homedir(), normalizedPath.slice(2));
32
+ }
33
+ return path.isAbsolute(normalizedPath)
34
+ ? normalizedPath
35
+ : path.resolve(normalizedPath);
36
+ }
37
+
22
38
  function canonicalizeExistingProofPath(filePath) {
23
39
  const resolvedPath = path.resolve(filePath);
24
40
  try {
@@ -191,6 +207,12 @@ function readJsonSnapshot(filePath) {
191
207
  }
192
208
  }
193
209
 
210
+ function describeStepWarning(step) {
211
+ return step.captureState === "partial"
212
+ ? `${step.stepId} ended as ${step.resultClass} with partial capture`
213
+ : `${step.stepId} ended as ${step.resultClass}`;
214
+ }
215
+
194
216
  function extractStartupBreadcrumbs(logText, bundleStartedAtIso) {
195
217
  if (!logText) {
196
218
  return { all: [], afterBundleStart: [] };
@@ -230,6 +252,7 @@ function extractStatusSignals(statusText) {
230
252
  serveActivePack: /serve\s+state=serving_active_pack/.test(statusText),
231
253
  routeFnAvailable: /routeFn\s+available=yes/.test(statusText),
232
254
  proofPath: statusText.match(/proofPath=([^\s]+)/)?.[1] ?? null,
255
+ proofError: statusText.match(/proofError=([^\s]+)/)?.[1] ?? null,
233
256
  };
234
257
  }
235
258
 
@@ -238,69 +261,106 @@ function hasPackagedHookSource(pluginInspectText) {
238
261
  }
239
262
 
240
263
  function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, breadcrumbs, runtimeLoadProofSnapshot, openclawHome }) {
241
- const failedStep = steps.find((step) => step.resultClass !== "success" && step.skipped !== true);
242
- if (failedStep) {
243
- return {
244
- verdict: "command_failed",
245
- severity: "blocking",
246
- why: `${failedStep.stepId} exited as ${failedStep.resultClass}`,
247
- };
248
- }
264
+ const failedSteps = steps.filter((step) => step.resultClass !== "success" && step.skipped !== true);
265
+ const failedDetailedStatusStep = failedSteps.find((step) => step.stepId === "05-detailed-status");
249
266
  const gatewayHealthy = /Runtime:\s+running/m.test(gatewayStatus) && /RPC probe:\s+ok/m.test(gatewayStatus);
250
267
  const pluginLoaded = /Status:\s+loaded/m.test(pluginInspect);
251
268
  const packagedHookPath = hasPackagedHookSource(pluginInspect);
252
269
  const breadcrumbLoaded = breadcrumbs.afterBundleStart.some((entry) => entry.kind === "loaded");
253
270
  const runtimeProofMatched = Array.isArray(runtimeLoadProofSnapshot?.value?.profiles)
254
271
  && runtimeLoadProofSnapshot.value.profiles.some((profile) => canonicalizeExistingProofPath(profile?.openclawHome ?? "") === canonicalizeExistingProofPath(openclawHome));
255
- const missingProofs = [];
256
- if (!gatewayHealthy)
257
- missingProofs.push("gateway_health");
258
- if (!pluginLoaded)
259
- missingProofs.push("plugin_loaded");
260
- if (!packagedHookPath)
261
- missingProofs.push("packaged_hook_path");
272
+ const runtimeTruthGaps = [];
262
273
  if (!statusSignals.statusOk)
263
- missingProofs.push("status_ok");
274
+ runtimeTruthGaps.push("status_ok");
264
275
  if (!statusSignals.loadProofReady)
265
- missingProofs.push("load_proof");
276
+ runtimeTruthGaps.push("load_proof");
266
277
  if (!statusSignals.runtimeProven)
267
- missingProofs.push("runtime_proven");
278
+ runtimeTruthGaps.push("runtime_proven");
268
279
  if (!statusSignals.serveActivePack)
269
- missingProofs.push("serve_active_pack");
280
+ runtimeTruthGaps.push("serve_active_pack");
270
281
  if (!statusSignals.routeFnAvailable)
271
- missingProofs.push("route_fn");
272
- if (!breadcrumbLoaded)
273
- missingProofs.push("startup_breadcrumb");
274
- if (!runtimeProofMatched)
275
- missingProofs.push("runtime_load_proof_record");
276
- if (missingProofs.length === 0) {
282
+ runtimeTruthGaps.push("route_fn");
283
+ const warningCodes = [];
284
+ const warnings = [];
285
+ if (!gatewayHealthy) {
286
+ warningCodes.push("gateway_health");
287
+ warnings.push("gateway status did not confirm runtime running and RPC probe ok");
288
+ }
289
+ if (!pluginLoaded) {
290
+ warningCodes.push("plugin_loaded");
291
+ warnings.push("plugin inspect did not report Status: loaded");
292
+ }
293
+ if (!packagedHookPath) {
294
+ warningCodes.push("packaged_hook_path");
295
+ warnings.push("plugin inspect did not confirm the packaged hook source");
296
+ }
297
+ if (!breadcrumbLoaded) {
298
+ warningCodes.push("startup_breadcrumb");
299
+ warnings.push("startup log did not contain a post-bundle [openclawbrain] BRAIN LOADED breadcrumb");
300
+ }
301
+ if (!runtimeProofMatched) {
302
+ warningCodes.push("runtime_load_proof_record");
303
+ warnings.push(runtimeLoadProofSnapshot.error !== null
304
+ ? `runtime-load-proof snapshot was unreadable: ${runtimeLoadProofSnapshot.error}`
305
+ : runtimeLoadProofSnapshot.exists
306
+ ? "runtime-load-proof snapshot did not include the current openclaw home"
307
+ : "runtime-load-proof snapshot was missing");
308
+ }
309
+ if (statusSignals.proofError !== null && statusSignals.proofError !== "none") {
310
+ warningCodes.push(`proof_error:${statusSignals.proofError}`);
311
+ warnings.push(`detailed status reported proofError=${statusSignals.proofError}`);
312
+ }
313
+ for (const step of failedSteps) {
314
+ warningCodes.push(`step:${step.stepId}:${step.resultClass}:${step.captureState}`);
315
+ warnings.push(describeStepWarning(step));
316
+ }
317
+ const uniqueWarningCodes = [...new Set(warningCodes)];
318
+ const uniqueWarnings = [...new Set(warnings)];
319
+ if (runtimeTruthGaps.length === 0 && uniqueWarningCodes.length === 0) {
277
320
  return {
278
321
  verdict: "success_and_proven",
279
322
  severity: "none",
280
323
  why: "install, restart, gateway health, plugin load, startup breadcrumb, runtime-load-proof record, and detailed status all aligned",
324
+ missingProofs: [],
325
+ warnings: [],
326
+ };
327
+ }
328
+ if (runtimeTruthGaps.length === 0) {
329
+ return {
330
+ verdict: "success_but_proof_incomplete",
331
+ severity: "degraded",
332
+ why: `status/runtime evidence stayed healthy, but proof warnings remained: ${uniqueWarningCodes.join(", ")}`,
333
+ missingProofs: uniqueWarningCodes,
334
+ warnings: uniqueWarnings,
335
+ };
336
+ }
337
+ const hasUsableStatusTruth = statusSignals.statusOk
338
+ || statusSignals.loadProofReady
339
+ || statusSignals.runtimeProven
340
+ || statusSignals.serveActivePack
341
+ || statusSignals.routeFnAvailable;
342
+ if (failedDetailedStatusStep && !hasUsableStatusTruth) {
343
+ return {
344
+ verdict: "command_failed",
345
+ severity: "blocking",
346
+ why: `${failedDetailedStatusStep.stepId} ended as ${failedDetailedStatusStep.resultClass} before runtime truth could be established`,
347
+ missingProofs: runtimeTruthGaps,
348
+ warnings: uniqueWarnings,
281
349
  };
282
350
  }
283
- const blocking = missingProofs.some((item) => [
284
- "gateway_health",
285
- "plugin_loaded",
286
- "packaged_hook_path",
287
- "status_ok",
288
- "load_proof",
289
- "runtime_proven",
290
- "serve_active_pack",
291
- "route_fn",
292
- ].includes(item));
293
351
  return {
294
- verdict: blocking ? "degraded_or_failed_proof" : "success_but_proof_incomplete",
295
- severity: blocking ? "blocking" : "degraded",
296
- why: `missing or conflicting proofs: ${missingProofs.join(", ")}`,
297
- missingProofs,
352
+ verdict: "degraded_or_failed_proof",
353
+ severity: "blocking",
354
+ why: `missing or conflicting runtime truths: ${runtimeTruthGaps.join(", ")}`,
355
+ missingProofs: [...new Set([...runtimeTruthGaps, ...uniqueWarningCodes])],
356
+ warnings: uniqueWarnings,
298
357
  };
299
358
  }
300
359
 
301
360
  function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspectText, statusSignals, breadcrumbs, runtimeLoadProofSnapshot }) {
302
361
  const passed = [];
303
362
  const missing = [];
363
+ const warnings = Array.isArray(verdict.warnings) ? verdict.warnings : [];
304
364
  if (steps.find((step) => step.stepId === "01-install")?.resultClass === "success") {
305
365
  passed.push("install command succeeded");
306
366
  }
@@ -350,6 +410,9 @@ function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspec
350
410
  "## Missing / incomplete",
351
411
  ...(missing.length === 0 ? ["- none"] : missing.map((item) => `- ${item}`)),
352
412
  "",
413
+ "## Warnings",
414
+ ...(warnings.length === 0 ? ["- none"] : warnings.map((item) => `- ${item}`)),
415
+ "",
353
416
  "## Step ledger",
354
417
  ...steps.map((step) => `- ${step.stepId}: ${step.skipped ? "skipped" : `${step.resultClass} (${step.captureState})`} - ${step.summary}`),
355
418
  ];
@@ -570,11 +633,12 @@ export function captureOperatorProofBundle(options) {
570
633
  const statusCapture = addStep("05-detailed-status", "detailed status", cliInvocation.command, [...cliInvocation.args, "status", "--openclaw-home", options.openclawHome, "--detailed"]);
571
634
  const gatewayLogPath = extractGatewayLogPath(gatewayStatusCapture.stdout);
572
635
  const activationRoot = extractActivationRoot(statusCapture.stdout, options.activationRoot ?? null);
573
- const runtimeLoadProofPath = path.join(activationRoot, "attachment-truth", "runtime-load-proofs.json");
636
+ const statusSignals = extractStatusSignals(statusCapture.stdout);
637
+ const runtimeLoadProofPath = normalizeReportedProofPath(statusSignals.proofPath)
638
+ ?? path.join(activationRoot, "attachment-truth", "runtime-load-proofs.json");
574
639
  const runtimeLoadProofSnapshot = readJsonSnapshot(runtimeLoadProofPath);
575
640
  const gatewayLogText = readTextIfExists(gatewayLogPath);
576
641
  const breadcrumbs = extractStartupBreadcrumbs(gatewayLogText, bundleStartedAt);
577
- const statusSignals = extractStatusSignals(statusCapture.stdout);
578
642
  writeText(path.join(bundleDir, "extracted-startup-breadcrumbs.log"), breadcrumbs.all.length === 0
579
643
  ? "<no matching breadcrumbs found>\n"
580
644
  : `${breadcrumbs.all.map((entry) => entry.line).join("\n")}\n`);