fix: report current M4 M5 runtime blockers

This commit is contained in:
Code Queue Review
2026-05-23 06:17:22 +00:00
parent 66586ccf10
commit d62e911bfb
16 changed files with 1261 additions and 475 deletions
+242 -13
View File
@@ -5,6 +5,12 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { activeReportLifecycle, reportIsHistorical, reportLifecycleState } from "../../internal/dev-report-lifecycle.mjs";
import {
RUNTIME_DURABLE_ADAPTER_MISSING,
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
RUNTIME_STORE_KIND_POSTGRES
} from "../../internal/db/runtime-store.mjs";
import { CLOUD_CORE_MIGRATION_ID } from "../../internal/db/schema.mjs";
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -189,7 +195,7 @@ function normalizeBlocker(blocker, sourceReport, fallbackPriority = "P3") {
scope: blocker.scope,
status: blocker.status ?? "open",
source: sourceReport.path,
sourceIssue: sourceReport.issue,
sourceIssue: blocker.sourceIssue ?? sourceReport.issue,
summary: oneLine(blocker.summary),
...(nextTask ? { nextTask } : {})
};
@@ -198,7 +204,7 @@ function normalizeBlocker(blocker, sourceReport, fallbackPriority = "P3") {
function dedupeBlockers(blockers) {
const byKey = new Map();
for (const blocker of blockers) {
const key = `${blocker.type}:${blocker.scope}:${blocker.summary}`;
const key = `${blocker.type}:${blocker.scope}`;
const existing = byKey.get(key);
if (!existing) {
byKey.set(key, blocker);
@@ -310,6 +316,26 @@ function applyPriority(blocker) {
};
}
if (blocker.scope === "runtime-durable-adapter") {
return {
...blocker,
priority: "P1",
unblockOrder: 5,
unblocks: [issue(37), issue(39), issue(164)],
rationale: "M4 and M5 cannot claim live agent or MVP evidence until the cloud-api postgres runtime adapter proves schema, migration, and read-query durability; DB live evidence alone is not durability evidence."
};
}
if (blocker.scope === "skills-commit-version-injection" || blocker.scope === "agent-mgr-health") {
return {
...blocker,
priority: "P1",
unblockOrder: 5,
unblocks: [issue(37), issue(39), issue(164)],
rationale: "M4/M5 agent evidence needs deployed manager, skills, and worker paths to expose explicit skills commit and version; session-mode-only worker templates are not enough."
};
}
if (isEdgeOrFrpBlocker(blocker)) {
return {
...blocker,
@@ -629,18 +655,27 @@ function collectBlockers(reports) {
if (isStaleLegacyIngressBlocker(blocker, report, reports)) {
continue;
}
if (isStaleReadinessBlocker(blocker, reports)) {
continue;
}
blockers.push(normalizeBlocker(blocker, report));
}
for (const blocker of report.artifactPublish?.blockers ?? []) {
if (isStaleLegacyIngressBlocker(blocker, report, reports)) {
continue;
}
if (isStaleReadinessBlocker(blocker, reports)) {
continue;
}
blockers.push(normalizeBlocker(blocker, report));
}
for (const blocker of report.devDeployApply?.remainingBlockers ?? []) {
if (isStaleLegacyIngressBlocker(blocker, report, reports)) {
continue;
}
if (isStaleReadinessBlocker(blocker, reports)) {
continue;
}
blockers.push(normalizeBlocker(blocker, report));
}
}
@@ -722,6 +757,29 @@ function isStaleLegacyIngressBlocker(blocker, sourceReport, reports) {
sourceReport.path !== reportPaths.devM5Gate;
}
function isStaleReadinessBlocker(blocker, reports) {
const scope = blocker.scope ?? "";
const db = cloudApiDbStatus(reports);
const provider = codeAgentProviderStatus(reports);
if (
(
scope === "cloud-api-db" ||
scope === "cloud-api-db-health-gate" ||
scope === "cloud-api-db-live" ||
scope === "db-live"
) &&
db.ready &&
db.connected &&
db.liveDbEvidence
) {
return true;
}
if ((scope === "code-agent-provider-secret" || scope === "code-agent-provider") && provider.ready) {
return true;
}
return false;
}
function deriveMilestones(evidence, blockers) {
const evidenceByMilestone = new Map();
for (const item of evidence) {
@@ -786,7 +844,7 @@ function buildMilestoneLevelClassification(milestones) {
M1: "Local smoke is green; it is not DEV-LIVE.",
M2: "Current public frontend/API route evidence is DEV-LIVE for route/front-end reachability only.",
M3: "DEV-LIVE hardware trusted loop is blocked; local/source shape is not acceptance.",
M4: "DEV-LIVE agent loop is blocked at DB live readiness; local smoke is not acceptance.",
M4: "DEV-LIVE agent loop is blocked at runtime durable adapter or skills injection readiness; local smoke is not acceptance.",
M5: "Dry-run is green, but bounded DEV-LIVE MVP e2e is blocked."
};
@@ -918,6 +976,18 @@ function buildDoD(reports, milestones, blockers) {
}
function cloudApiDbStatus(reports) {
const liveDb = reports.devM4Agent.publicEntrypoints?.api?.live?.db ?? null;
if (liveDb?.ready === true && liveDb?.connected === true && liveDb?.liveDbEvidence === true) {
return {
status: liveDb.status ?? "ok",
ready: true,
connected: true,
configReady: liveDb.configReady === true,
connectionChecked: liveDb.connectionChecked === true,
endpointSource: liveDb.endpointSource ?? liveDb.connection?.endpointSource ?? "secret-url-host",
liveDbEvidence: true
};
}
const db = reports.devEdgeHealth.edgeHealth?.contracts?.deploy?.cloudApiDb ?? {};
return {
status: db.status ?? "unknown",
@@ -930,10 +1000,91 @@ function cloudApiDbStatus(reports) {
};
}
function codeAgentProviderStatus(reports) {
const codeAgent = reports.devM4Agent.publicEntrypoints?.api?.live?.codeAgent ??
reports.devM4Agent.publicEntrypoints?.api?.health?.codeAgent ??
null;
return {
ready: Boolean(codeAgent?.ready || codeAgent?.status === "ready" || codeAgent?.status === "available"),
status: codeAgent?.status ?? "unknown",
provider: codeAgent?.provider ?? "unknown"
};
}
function runtimeDurabilityStatusFromReport(report = {}) {
const runtime = report.runtime ??
report.livePreflight?.runtime ??
report.publicEntrypoints?.api?.live?.runtime ??
report.readiness?.durability ??
{};
const dependency = report.dependencyState?.runtimeDurableAdapter ?? {};
const component = report.componentEvidence?.runtimeDurableAdapter ?? {};
const blocker = report.blockers?.find((item) => item.scope === "runtime-durable-adapter") ?? null;
const migration = runtime.migration ?? {};
return {
adapter: runtime.adapter ?? "unknown",
durable: runtime.durable === true,
durableRequested: Boolean(
runtime.durableRequested ||
runtime.adapter === RUNTIME_STORE_KIND_POSTGRES ||
blocker ||
dependency.status === "blocked" ||
component.status === "blocked"
),
ready: Boolean(runtime.ready === true && runtime.liveRuntimeEvidence === true && runtime.durable === true),
runtimeStatus: runtime.status ?? (blocker ? "blocked" : "unknown"),
blocker: blocker?.classification ?? runtime.blocker ?? report.readiness?.durability?.blocker ?? RUNTIME_DURABLE_ADAPTER_MISSING,
blockedLayer: runtime.durabilityContract?.blockedLayer ?? report.readiness?.durability?.blockedLayer ?? null,
requiredEvidence: runtime.durabilityContract?.requiredEvidence ??
report.readiness?.durability?.requiredEvidence ??
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
migration: {
checked: migration.checked ?? null,
ready: migration.ready ?? null,
missing: migration.missing ?? null,
requiredMigrationId: migration.requiredMigrationId ?? CLOUD_CORE_MIGRATION_ID,
appliedMigrationId: migration.appliedMigrationId ?? null
},
queryAttempted: runtime.connection?.queryAttempted ?? null,
queryResult: runtime.connection?.queryResult ?? "unknown",
summary: blocker?.summary ?? dependency.summary ?? component.summary ?? "No runtime durable adapter evidence was attached."
};
}
function runtimeDurabilityStatus(reports) {
return runtimeDurabilityStatusFromReport(reports.devM4Agent);
}
function m4SkillsInjectionStatus(reports) {
const native = reports.devM4Agent.d601K3sNative ?? {};
const injection = native.skillsInjection ?? {};
const blockers = reports.devM4Agent.blockers ?? [];
const injectionBlocker = blockers.find((blocker) => blocker.scope === "skills-commit-version-injection") ?? null;
const managerBlocker = blockers.find((blocker) => blocker.scope === "agent-mgr-health") ?? null;
const missing = Array.isArray(injection.missing) ? injection.missing : [];
const managerMissing = native.agentManager?.health?.missingSkills ?? [];
return {
status: injectionBlocker || managerBlocker || injection.status === "blocked" ? "blocked" : "pass",
serviceCommitId: injection.serviceCommitId ?? null,
serviceVersion: injection.serviceVersion ?? null,
workerDryRunCommitId: injection.workerDryRunCommitId ?? null,
workerDryRunVersion: injection.workerDryRunVersion ?? null,
missing: [...new Set([...missing, ...managerMissing])],
summary: injectionBlocker?.summary ??
managerBlocker?.summary ??
(injection.status === "blocked"
? `DEV skills injection is incomplete: missing ${missing.join(", ")}.`
: "DEV skills commit and version injection evidence is present.")
};
}
function buildCurrentDevLayering(reports, blockers) {
const m2EndpointLive = hasCurrentM2EndpointEvidence(reports.devM2Smoke);
const edgeLive = hasCurrentLiveEdgeEvidence(reports.devEdgeHealth);
const cloudDb = cloudApiDbStatus(reports);
const provider = codeAgentProviderStatus(reports);
const runtimeDurability = runtimeDurabilityStatus(reports);
const skillsInjection = m4SkillsInjectionStatus(reports);
const dbReady = cloudDb.ready && cloudDb.connected && cloudDb.endpointSource === "secret-url-host" && cloudDb.liveDbEvidence;
const m3Live = statusIsPass(reports.devM3Hardware.liveOperation?.status);
const m4Live = statusIsPass(reports.devM4Agent.livePreflight?.status);
@@ -971,6 +1122,41 @@ function buildCurrentDevLayering(reports, blockers) {
evidence: reports.devM5Gate.devPreconditions?.evidence?.filter((line) => line.includes("/health/live") || line.includes("DB")) ?? [],
nextRequired: "Provide live DB connection evidence through redacted health output; route reachability alone is insufficient."
},
runtimeDurableAdapter: {
label: "Runtime durable adapter",
status: runtimeDurability.ready ? "pass" : "blocked",
evidenceLevel: runtimeDurability.ready ? "DEV-LIVE" : "BLOCKED",
summary: `runtime adapter=${runtimeDurability.adapter}; durableRequested=${runtimeDurability.durableRequested}; durable=${runtimeDurability.durable}; ready=${runtimeDurability.ready}; blocker=${runtimeDurability.blocker}; migration=${runtimeDurability.migration.requiredMigrationId} checked=${runtimeDurability.migration.checked ?? "unknown"} ready=${runtimeDurability.migration.ready ?? "unknown"}; queryResult=${runtimeDurability.queryResult}.`,
evidence: [
`requiredEvidence=${runtimeDurability.requiredEvidence}`,
`blockedLayer=${runtimeDurability.blockedLayer ?? "unknown"}`,
`migration=${runtimeDurability.migration.requiredMigrationId}`,
`queryAttempted=${runtimeDurability.queryAttempted ?? "unknown"}`
],
nextRequired: "Repair the postgres durable runtime adapter query/migration readiness and rerun health/M4 preflight without treating DB live as durability proof."
},
agentSkillsInjection: {
label: "Agent skills injection",
status: skillsInjection.status,
evidenceLevel: skillsInjection.status === "pass" ? "DEV-LIVE" : "BLOCKED",
summary: skillsInjection.summary,
evidence: [
`skills.commit=${skillsInjection.serviceCommitId ?? "missing"}`,
`skills.version=${skillsInjection.serviceVersion ?? "missing"}`,
`worker.commit=${skillsInjection.workerDryRunCommitId ?? "missing"}`,
`worker.version=${skillsInjection.workerDryRunVersion ?? "missing"}`,
`missing=${skillsInjection.missing.join(",") || "none"}`
],
nextRequired: "Inject explicit skill commit and version into hwlab-agent-mgr, hwlab-agent-skills, and scheduled worker jobs before claiming M4/M5 agent evidence."
},
codeAgentProvider: {
label: "Code Agent provider",
status: provider.ready ? "pass" : "blocked",
evidenceLevel: provider.ready ? "DEV-LIVE" : "BLOCKED",
summary: `code-agent provider=${provider.provider}; status=${provider.status}; ready=${provider.ready}.`,
evidence: [`provider=${provider.provider}`, `status=${provider.status}`],
nextRequired: "Keep provider-backed Code Agent readiness separate from DB live and runtime durable adapter readiness."
},
d601RunnerObservability: {
label: "D601 runner observability",
status: reports.d601Observability.runnerKubeconfigReadable === false
@@ -1005,7 +1191,7 @@ function buildCurrentDevLayering(reports, blockers) {
evidenceLevel: m4Live ? "DEV-LIVE" : "BLOCKED",
summary: reports.devM4Agent.livePreflight?.summary ?? reports.devM4Agent.devPreconditions?.summary ?? "No live M4 observation was recorded.",
evidence: reports.devM4Agent.livePreflight?.evidence ?? [],
nextRequired: "Do not schedule or claim the agent loop as live until DB live and required runtime/evidence preconditions pass."
nextRequired: "Do not schedule or claim the agent loop as live until DB live, runtime durable adapter, skills injection, and required evidence preconditions pass."
},
artifactDesiredStateSource: {
label: "artifact/desired-state source",
@@ -1024,6 +1210,8 @@ function buildCurrentDevLayering(reports, blockers) {
function buildMilestoneBlockerClassification(reports) {
const cloudDb = cloudApiDbStatus(reports);
const runtimeDurability = runtimeDurabilityStatus(reports);
const skillsInjection = m4SkillsInjectionStatus(reports);
const m3Live = statusIsPass(reports.devM3Hardware.liveOperation?.status);
const m4Live = statusIsPass(reports.devM4Agent.livePreflight?.status);
const m3ServiceDiscoveryBlocked = reports.devM3Hardware.blockers?.some((blocker) =>
@@ -1045,6 +1233,23 @@ function buildMilestoneBlockerClassification(reports) {
? "patch-panel-m3-wiring-missing"
: "hardware-loop-runtime";
const m4RuntimeDurableBlocked = reports.devM4Agent.blockers?.some((blocker) =>
blocker.scope === "runtime-durable-adapter"
) === true || (!m4Live && runtimeDurability.durableRequested && !runtimeDurability.ready);
const m4SkillsInjectionBlocked = skillsInjection.status === "blocked";
const m4DbLiveBlocked = reports.devM4Agent.blockers?.some((blocker) =>
blocker.scope === "db-live"
) === true;
const m4BlockerClass = m4Live
? "cleared"
: m4RuntimeDurableBlocked
? runtimeDurability.blocker
: m4SkillsInjectionBlocked
? "skills-commit-version-injection"
: m4DbLiveBlocked
? "db-live-readiness"
: "agent-loop-live-preflight";
return [
{
milestone: "M3",
@@ -1083,31 +1288,51 @@ function buildMilestoneBlockerClassification(reports) {
milestone: "M4",
status: m4Live ? "pass" : "blocked",
currentLevel: m4Live ? "DEV-LIVE" : "BLOCKED",
blockerClass: m4Live ? "cleared" : "db-live-readiness",
dependency: "Cloud API /health/live must report DB ready=true, connected=true, endpointSource=secret-url-host, and liveDbEvidence=true before live agent scheduling/evidence closure.",
blockerClass: m4BlockerClass,
dependency: m4RuntimeDurableBlocked
? "Cloud API /health/live must prove the postgres durable runtime adapter schema, 0001_cloud_core_skeleton migration ledger, and read-query readiness before live agent scheduling/evidence closure."
: m4SkillsInjectionBlocked
? "Agent manager, skills service, and worker jobs must expose explicit skill commit and version fields before M4 live evidence can be trusted."
: "Cloud API /health/live must report DB ready=true, connected=true, endpointSource=secret-url-host, and liveDbEvidence=true before live agent scheduling/evidence closure.",
evidence: [
`db.status=${cloudDb.status}`,
`db.ready=${cloudDb.ready}`,
`db.connected=${cloudDb.connected}`,
`db.endpointSource=${cloudDb.endpointSource}`,
`db.liveDbEvidence=${cloudDb.liveDbEvidence}`
`db.liveDbEvidence=${cloudDb.liveDbEvidence}`,
`runtime.adapter=${runtimeDurability.adapter}`,
`runtime.blocker=${runtimeDurability.blocker}`,
`runtime.migration=${runtimeDurability.migration.requiredMigrationId}`,
`runtime.migration.ready=${runtimeDurability.migration.ready ?? "unknown"}`,
`runtime.queryResult=${runtimeDurability.queryResult}`,
`skills.missing=${skillsInjection.missing.join(",") || "none"}`
],
nextRequired: "Repair DB live readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass.",
nonPromotionReason: "Frontend revision and read-only route reachability do not prove DB-backed agent runtime readiness."
nextRequired: m4RuntimeDurableBlocked
? "Repair the runtime durable adapter query/migration readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass."
: m4SkillsInjectionBlocked
? "Inject skill commit/version into agent manager, skills service, and worker jobs, then rerun the M4 read-only/live preflight."
: "Repair DB live readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass.",
nonPromotionReason: "Frontend revision and read-only route reachability do not prove DB-backed durable agent runtime readiness or skill version injection."
},
{
milestone: "M5",
status: "blocked",
currentLevel: "BLOCKED",
blockerClass: "composite-db-m3-m4-live",
dependency: "M5 needs DB live readiness, M3 trusted-loop DEV evidence, M4 live preflight/evidence closure, and current source/artifact coverage.",
blockerClass: m4RuntimeDurableBlocked
? "composite-runtime-durable-m3-m4-live"
: m4SkillsInjectionBlocked
? "composite-skills-injection-m3-m4-live"
: "composite-db-m3-m4-live",
dependency: "M5 needs DB live readiness, runtime durable adapter readiness, skills commit/version injection, M3 trusted-loop DEV evidence, M4 live preflight/evidence closure, and current source/artifact coverage.",
evidence: [
"M5 dry-run is green",
`db.ready=${cloudDb.ready}`,
`runtime.blocker=${runtimeDurability.blocker}`,
`skills.status=${skillsInjection.status}`,
`m3.live=${m3Live}`,
`m4.live=${m4Live}`
],
nextRequired: "After DB/M3/M4 blockers are cleared, run only the bounded DEV MVP live gate command with explicit DEV/non-PROD confirmations.",
nextRequired: "After DB/runtime durable adapter/skills/M3/M4 blockers are cleared, run only the bounded DEV MVP live gate command with explicit DEV/non-PROD confirmations.",
nonPromotionReason: "No frontend, route-only, local, or dry-run evidence is allowed to stand in for bounded DEV-LIVE MVP e2e acceptance."
}
];
@@ -1165,6 +1390,8 @@ function fallbackAction(scope) {
if (scope.includes("edge") || scope.includes("ingress") || scope.includes("frp")) return "Repair frp/master-edge/D601 router path and rerun read-only DEV edge health.";
if (scope.includes("cloud-api-db")) return "Configure DEV cloud-api DB env readiness and Secret URL host connectivity, then rerun health/preflight without exposing secrets.";
if (scope === "db-live") return "Repair DEV cloud-api DB live readiness, then rerun the read-only health and M4 preflight reports without exposing secret values.";
if (scope === "runtime-durable-adapter") return "Repair DEV cloud-api postgres durable runtime adapter schema/migration/read-query readiness, then rerun /health/live and the M4 preflight without exposing secret values.";
if (scope === "skills-commit-version-injection" || scope === "agent-mgr-health") return "Inject explicit skills commit and version into agent manager, skills service, and worker jobs, then rerun the M4 read-only/live preflight.";
if (scope === "m3-patch-panel-wiring") return "Load/apply DEV patch-panel wiring for res_boxsimu_1:DO1 -> res_boxsimu_2:DI1, then rerun the bounded DEV M3 smoke.";
if (scope === "m3-box-simu-identity") return "Fix DEV box-simu instance identity so direct endpoints expose distinct res_boxsimu_1 and res_boxsimu_2 resources.";
if (scope === "m3-gateway-simu-identity") return "Fix DEV gateway-simu instance identity so direct endpoints expose two distinct gateway sessions.";
@@ -1181,6 +1408,8 @@ function evidenceRequiredFor(scope) {
if (scope.includes("kubectl") || scope.includes("k3s")) return "Read-only kubectl/k3s report proving pods/services/configmaps are observable in hwlab-dev without reading Secrets.";
if (scope.includes("cloud-api-db")) return "Cloud API health/live output showing DB env ready, endpointSource=secret-url-host, redacted secret references, and no secret material.";
if (scope === "db-live") return "Cloud API /health/live output with ready=true, connected=true, endpointSource=secret-url-host, liveDbEvidence=true, and redacted secret references.";
if (scope === "runtime-durable-adapter") return "Cloud API /health/live output with runtime.adapter=postgres, durable=true, ready=true, liveRuntimeEvidence=true, migration 0001_cloud_core_skeleton ready, and read-query readiness passing.";
if (scope === "skills-commit-version-injection" || scope === "agent-mgr-health") return "Read-only hwlab-agent-mgr/hwlab-agent-skills health plus worker dry-run manifest showing skillCommitId/skillVersion and HWLAB_SKILL_COMMIT_ID/HWLAB_SKILL_VERSION injected.";
if (scope === "m3-patch-panel-wiring") return "Read-only patch-panel /status and /wiring showing active res_boxsimu_1:DO1 -> res_boxsimu_2:DI1 before a bounded write/read smoke records operation, trace, audit, and evidence IDs.";
if (scope === "m3-box-simu-identity") return "Read-only direct box-simu /health/live and /status output showing distinct res_boxsimu_1 and res_boxsimu_2 resources.";
if (scope === "m3-gateway-simu-identity") return "Read-only direct gateway-simu /health/live and /status output showing two distinct gateway identities/sessions.";
@@ -1299,7 +1528,7 @@ export async function buildReport() {
green: dod.green,
reason: dod.green
? "All #9 DEV DoD checks are green."
: `Frontend revision ${latestFrontendRevision} is usable in degraded/read-only mode and EDGE/ROUTE DEV-LIVE evidence exists on :16666/:16667, but M5 remains blocked by artifact source drift, API/runtime durability degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight.`
: `Frontend revision ${latestFrontendRevision} is usable in degraded/read-only mode and EDGE/ROUTE DEV-LIVE evidence exists on :16666/:16667, but M5 remains blocked by artifact source drift, runtime durable adapter or skills injection readiness, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight.`
},
latestFrontendDevFact: {
...frontendDevFact,
@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import test from "node:test";
import { buildReport } from "./dev-evidence-blocker-aggregator.mjs";
test("aggregator reports M4 current blocker as durable runtime or skills injection, not stale DB live", async () => {
const report = await buildReport();
const m4 = report.milestoneBlockerClassification.find((item) => item.milestone === "M4");
const m5 = report.milestoneBlockerClassification.find((item) => item.milestone === "M5");
const dbLayer = report.currentDevLayering.dbLive;
const dbCheck = report.dod.checks.find((item) => item.id === "cloud-api-db-ready");
assert.ok(m4, "M4 blocker classification is present");
assert.ok(m5, "M5 blocker classification is present");
assert.equal(dbLayer.status, "pass");
assert.equal(dbLayer.evidenceLevel, "DEV-LIVE");
assert.equal(dbCheck.status, "pass");
assert.equal(dbCheck.evidenceLevel, "DEV-LIVE");
assert.equal(
report.blockers.some((blocker) =>
["cloud-api-db", "cloud-api-db-health-gate", "cloud-api-db-live", "db-live"].includes(blocker.scope)
),
false,
"DB live blockers must be stale once current DB live evidence is pass"
);
assert.notEqual(m4.blockerClass, "db-live-readiness");
assert.ok(
["runtime_durable_adapter_query_blocked", "skills-commit-version-injection"].includes(m4.blockerClass),
`unexpected M4 blocker class ${m4.blockerClass}`
);
assert.match(m4.dependency, /durable runtime adapter|skill commit and version/u);
assert.match(m4.nonPromotionReason, /durable agent runtime readiness|skill version injection/u);
assert.match(m5.dependency, /runtime durable adapter readiness, skills commit\/version injection/u);
});
+186 -6
View File
@@ -9,6 +9,12 @@ import {
AGENT_SKILLS_SERVICE_ID,
AGENT_WORKER_SERVICE_ID
} from "../../internal/agent/index.mjs";
import {
RUNTIME_DURABLE_ADAPTER_MISSING,
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
RUNTIME_STORE_KIND_POSTGRES
} from "../../internal/db/runtime-store.mjs";
import { CLOUD_CORE_MIGRATION_ID } from "../../internal/db/schema.mjs";
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -20,6 +26,183 @@ export const d601K3sReadonlyCommand =
export const workerServerDryRunCommand =
"KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n hwlab-dev create -f - --dry-run=server -o json";
export function isDbLiveReady(body) {
const db = body?.db;
return Boolean(db?.ready && db?.connected && db?.liveConnected !== false && db?.liveDbEvidence);
}
export function isDbLiveBlocked(body) {
const db = body?.db;
if (!db || typeof db !== "object") {
return false;
}
if (isDbLiveReady(body)) {
return false;
}
return db.connected === false ||
db.ready === false ||
db.liveConnected === false ||
db.liveDbEvidence === false ||
["blocked", "degraded", "failed"].includes(db.status);
}
export function summarizeDbBlocker(body) {
const db = body?.db ?? {};
const missing = Array.isArray(db.missingEnv) && db.missingEnv.length > 0
? ` missing ${db.missingEnv.join(", ")}`
: "";
return `cloud-api /health/live reports DB ${db.status ?? "not ready"}; connected=${db.connected ?? "unknown"}; ready=${db.ready ?? "unknown"}; liveDbEvidence=${db.liveDbEvidence ?? "unknown"}.${missing}`;
}
export function runtimeDurabilityFromHealth(body) {
const runtime = body?.runtime ?? {};
const durability = body?.readiness?.durability ?? {};
const migration = runtime.migration ?? {};
const migrationGate = runtime.gates?.migration ?? durability.gates?.migration ?? {};
const blocker = runtime.blocker ??
durability.blocker ??
(Array.isArray(body?.blockerCodes)
? body.blockerCodes.find((code) => String(code).startsWith("runtime_durable_adapter_"))
: null) ??
RUNTIME_DURABLE_ADAPTER_MISSING;
const durableRequested = Boolean(
runtime.durableRequested ||
durability.durableRequested ||
runtime.adapter === RUNTIME_STORE_KIND_POSTGRES ||
durability.adapter === RUNTIME_STORE_KIND_POSTGRES
);
const ready = Boolean(
runtime.durable === true &&
runtime.ready === true &&
runtime.liveRuntimeEvidence === true &&
durability.ready !== false
);
return {
adapter: runtime.adapter ?? durability.adapter ?? "unknown",
durable: Boolean(runtime.durable),
durableRequested,
ready,
runtimeStatus: runtime.status ?? durability.runtimeStatus ?? "unknown",
blocker,
blockedLayer: runtime.durabilityContract?.blockedLayer ?? durability.blockedLayer ?? null,
requiredEvidence: runtime.durabilityContract?.requiredEvidence ??
durability.requiredEvidence ??
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
migration: {
checked: migration.checked ?? migrationGate.checked ?? false,
ready: migration.ready ?? migrationGate.ready ?? false,
missing: migration.missing ?? (migration.ready === false ? true : null),
requiredMigrationId: migration.requiredMigrationId ?? CLOUD_CORE_MIGRATION_ID,
appliedMigrationId: migration.appliedMigrationId ?? null
},
queryAttempted: Boolean(runtime.connection?.queryAttempted ?? durability.queryAttempted),
queryResult: runtime.connection?.queryResult ?? durability.queryResult ?? "unknown"
};
}
export function isRuntimeDurableAdapterBlocked(body) {
const runtime = runtimeDurabilityFromHealth(body);
if (!runtime.durableRequested) {
return false;
}
return !runtime.ready ||
["blocked", "degraded", "failed"].includes(runtime.runtimeStatus) ||
String(runtime.blocker).startsWith("runtime_durable_adapter_");
}
export function summarizeRuntimeDurableBlocker(body) {
const runtime = runtimeDurabilityFromHealth(body);
const migration = runtime.migration;
return `cloud-api durable runtime adapter is blocked: ${runtime.blocker}; adapter=${runtime.adapter}; durable=${runtime.durable}; ready=${runtime.ready}; migration=${migration.requiredMigrationId} checked=${migration.checked} ready=${migration.ready} missing=${migration.missing ?? "unknown"}; queryResult=${runtime.queryResult}.`;
}
export function publicDbLiveSummary(body) {
const db = body?.db ?? null;
if (!db || typeof db !== "object") {
return null;
}
return {
status: db.status ?? null,
connected: db.connected ?? null,
liveConnected: db.liveConnected ?? null,
ready: db.ready ?? null,
configReady: db.configReady ?? null,
connectionChecked: db.connectionChecked ?? null,
connectionAttempted: db.connectionAttempted ?? null,
connectionResult: db.connectionResult ?? null,
endpointSource: db.endpointSource ?? db.connection?.endpointSource ?? null,
liveDbEvidence: db.liveDbEvidence ?? null,
evidence: db.evidence ?? null
};
}
export function publicRuntimeDurabilitySummary(body) {
if (!body?.runtime && !body?.readiness?.durability) {
return null;
}
const runtime = runtimeDurabilityFromHealth(body);
return {
adapter: runtime.adapter,
durable: runtime.durable,
durableRequested: runtime.durableRequested,
ready: runtime.ready,
status: runtime.runtimeStatus,
blocker: runtime.blocker,
blockedLayer: runtime.blockedLayer,
requiredEvidence: runtime.requiredEvidence,
liveRuntimeEvidence: Boolean(runtime.ready),
migration: runtime.migration,
queryAttempted: runtime.queryAttempted,
queryResult: runtime.queryResult
};
}
export function classifyCloudApiLiveReadiness(body) {
if (isDbLiveBlocked(body)) {
return {
blocked: true,
blockerType: "runtime_blocker",
blockerScope: "db-live",
sourceIssue: "pikasTech/HWLAB#49",
blockedClassification: "DB live",
blockerSummary: summarizeDbBlocker(body),
summary: "Blocked at DB live readiness before scheduling a DEV agent task.",
evidenceSuffix: "db-blocked",
dbLiveReady: false,
runtimeDurableReady: false
};
}
if (isRuntimeDurableAdapterBlocked(body)) {
const runtime = runtimeDurabilityFromHealth(body);
return {
blocked: true,
blockerType: "runtime_blocker",
blockerScope: "runtime-durable-adapter",
sourceIssue: "pikasTech/HWLAB#164",
blockedClassification: runtime.blocker,
blockerSummary: summarizeRuntimeDurableBlocker(body),
summary: "Blocked at runtime durable adapter readiness before scheduling a DEV agent task.",
evidenceSuffix: `runtime-durable-adapter-blocked:${runtime.blocker}`,
dbLiveReady: isDbLiveReady(body),
runtimeDurableReady: false
};
}
return {
blocked: false,
blockerType: null,
blockerScope: null,
blockedClassification: "none",
blockerSummary: "",
summary: "Live DEV preflight observed DB live and durable runtime adapter readiness.",
evidenceSuffix: null,
dbLiveReady: isDbLiveReady(body),
runtimeDurableReady: true
};
}
export function requestJson(urlString, { method = "GET", headers = {}, body, timeoutMs = 5000 } = {}) {
const url = new URL(urlString);
const client = url.protocol === "https:" ? httpsRequest : httpRequest;
@@ -348,12 +531,9 @@ export async function collectPublicEntrypoints() {
serviceId: apiLive.body?.serviceId ?? null,
environment: apiLive.body?.environment ?? null,
status: apiLive.body?.status ?? null,
db: apiLive.body?.db ? {
status: apiLive.body.db.status ?? null,
connected: apiLive.body.db.connected ?? null,
ready: apiLive.body.db.ready ?? null,
evidence: apiLive.body.db.evidence ?? null
} : null,
db: publicDbLiveSummary(apiLive.body),
runtime: publicRuntimeDurabilitySummary(apiLive.body),
blockerCodes: Array.isArray(apiLive.body?.blockerCodes) ? apiLive.body.blockerCodes : [],
error: apiLive.error ?? null
}
},
@@ -0,0 +1,157 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
classifyCloudApiLiveReadiness,
publicDbLiveSummary,
publicRuntimeDurabilitySummary,
runtimeDurabilityFromHealth
} from "./dev-m4-agent-loop-smoke-lib.mjs";
test("M4 live readiness classifies durable adapter query blocker after DB live is ready", () => {
const payload = {
db: {
ready: true,
connected: true,
liveConnected: true,
liveDbEvidence: true,
status: "ok"
},
runtime: {
adapter: "postgres",
durable: false,
durableRequested: true,
ready: false,
status: "blocked",
blocker: "runtime_durable_adapter_query_blocked",
reason: "durable runtime read query is blocked",
liveRuntimeEvidence: false,
connection: {
queryAttempted: true,
queryResult: "query_blocked"
},
migration: {
checked: true,
ready: false,
missing: true,
requiredMigrationId: "0001_cloud_core_skeleton"
}
},
readiness: {
durability: {
blockedLayer: "durability_query",
requiredEvidence: "runtime_adapter_schema_migration_read_query"
}
}
};
const classification = classifyCloudApiLiveReadiness(payload);
assert.equal(classification.blocked, true);
assert.equal(classification.blockerScope, "runtime-durable-adapter");
assert.equal(classification.blockedClassification, "runtime_durable_adapter_query_blocked");
assert.match(classification.blockerSummary, /0001_cloud_core_skeleton/u);
assert.match(classification.evidenceSuffix, /runtime_durable_adapter_query_blocked/u);
const runtime = runtimeDurabilityFromHealth(payload);
assert.equal(runtime.blocker, "runtime_durable_adapter_query_blocked");
assert.equal(runtime.migration.checked, true);
assert.equal(runtime.migration.ready, false);
assert.equal(runtime.queryResult, "query_blocked");
});
test("M4 live readiness keeps DB live blocker when DB is not connected", () => {
const classification = classifyCloudApiLiveReadiness({
db: {
ready: false,
connected: false,
liveDbEvidence: false,
status: "degraded"
},
runtime: {
adapter: "postgres",
durableRequested: true,
blocker: "runtime_durable_adapter_query_blocked"
}
});
assert.equal(classification.blockerScope, "db-live");
assert.equal(classification.blockedClassification, "DB live");
});
test("public DB live summary preserves DB pass fields apart from durable adapter blocker", () => {
const summary = publicDbLiveSummary({
db: {
status: "ok",
connected: true,
liveConnected: true,
ready: true,
configReady: true,
connectionChecked: true,
endpointSource: "secret-url-host",
liveDbEvidence: true,
evidence: "live_db_tcp_connection_ready"
},
runtime: {
adapter: "postgres",
durable: false,
ready: false,
blocker: "runtime_durable_adapter_query_blocked"
}
});
assert.equal(summary.status, "ok");
assert.equal(summary.connected, true);
assert.equal(summary.liveConnected, true);
assert.equal(summary.ready, true);
assert.equal(summary.configReady, true);
assert.equal(summary.connectionChecked, true);
assert.equal(summary.endpointSource, "secret-url-host");
assert.equal(summary.liveDbEvidence, true);
assert.equal(summary.evidence, "live_db_tcp_connection_ready");
});
test("public runtime durability summary preserves durable adapter fields separately from DB live", () => {
const summary = publicRuntimeDurabilitySummary({
db: {
ready: true,
connected: true,
liveConnected: true,
liveDbEvidence: true
},
runtime: {
adapter: "postgres",
durable: false,
durableRequested: true,
ready: false,
status: "blocked",
blocker: "runtime_durable_adapter_query_blocked",
liveRuntimeEvidence: false,
connection: {
queryAttempted: true,
queryResult: "query_blocked"
},
migration: {
checked: false,
ready: false,
missing: true,
requiredMigrationId: "0001_cloud_core_skeleton"
},
durabilityContract: {
blockedLayer: "durability_query",
requiredEvidence: "runtime_adapter_schema_migration_read_query"
}
}
});
assert.equal(summary.adapter, "postgres");
assert.equal(summary.durableRequested, true);
assert.equal(summary.durable, false);
assert.equal(summary.ready, false);
assert.equal(summary.liveRuntimeEvidence, false);
assert.equal(summary.blocker, "runtime_durable_adapter_query_blocked");
assert.equal(summary.blockedLayer, "durability_query");
assert.equal(summary.requiredEvidence, "runtime_adapter_schema_migration_read_query");
assert.equal(summary.queryAttempted, true);
assert.equal(summary.queryResult, "query_blocked");
assert.equal(summary.migration.requiredMigrationId, "0001_cloud_core_skeleton");
});