@cohortapp/agent-sdk 2.5.0 → 2.6.0
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/bin/maestro.mjs +185 -88
- package/bin/maestro.test.mjs +175 -48
- package/docs/runbooks/backup-restore.md +65 -33
- package/framework-features.json +4 -4
- package/lib/backup/policy.mjs +710 -0
- package/lib/backup/policy.test.mjs +305 -0
- package/lib/budget-escalate.mjs +133 -0
- package/lib/budget-escalate.test.mjs +232 -0
- package/lib/budget-guard.envelope.test.mjs +476 -0
- package/lib/budget-guard.mjs +853 -75
- package/lib/budget-guard.test.mjs +91 -42
- package/lib/cadences.mjs +33 -0
- package/lib/channels/orgmail/adapter.mjs +88 -3
- package/lib/channels/orgmail/adapter.test.mjs +137 -0
- package/lib/channels/repeat-suppressor.mjs +198 -0
- package/lib/channels/repeat-suppressor.test.mjs +134 -0
- package/lib/comms/receipts.mjs +297 -0
- package/lib/cost/ledger-row.mjs +333 -0
- package/lib/cost/ledger-row.test.mjs +183 -0
- package/lib/execution/drive.mjs +28 -1
- package/lib/execution/effects.mjs +191 -12
- package/lib/execution/effects.test.mjs +50 -11
- package/lib/goals/admission.mjs +13 -1
- package/lib/goals/admission.test.mjs +26 -1
- package/lib/goals/loop.mjs +13 -0
- package/lib/kpi-sensors.test.mjs +3 -0
- package/lib/mandate/cache.mjs +13 -5
- package/lib/mandate/derive.mjs +146 -21
- package/lib/mandate/derive.test.mjs +50 -6
- package/lib/mandate/model.mjs +32 -4
- package/lib/mandate/refresh.test.mjs +16 -2
- package/lib/mcp/server.test.mjs +12 -3
- package/lib/model-router/economics.mjs +107 -76
- package/lib/model-router/economics.test.mjs +64 -46
- package/lib/model-router/integration-coverage.test.mjs +39 -37
- package/lib/model-router/ledger.mjs +75 -22
- package/lib/model-router/ledger.test.mjs +35 -2
- package/lib/org/client.mjs +14 -0
- package/lib/org/cost-sync.mjs +16 -2
- package/lib/org/doctor.mjs +62 -1
- package/lib/org/doctor.test.mjs +36 -3
- package/lib/org/email-remedy.mjs +49 -0
- package/lib/org/engagement-ledger.mjs +376 -0
- package/lib/org/engagement-ledger.test.mjs +112 -0
- package/lib/org/engagement.mjs +1056 -0
- package/lib/org/engagement.test.mjs +739 -0
- package/lib/org/inbound/hydrate.mjs +107 -15
- package/lib/org/inbound/hydrate.test.mjs +127 -0
- package/lib/org/messaging.mjs +230 -3
- package/lib/org/messaging.test.mjs +110 -1
- package/lib/org/param-contract.mjs +56 -2
- package/lib/org/param-contract.test.mjs +26 -0
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +5 -0
- package/lib/org/protocol.test.mjs +7 -1
- package/lib/org/tool-surface.mjs +506 -10
- package/lib/org/tool-surface.test.mjs +191 -7
- package/lib/org/ui-parity.mjs +333 -6
- package/lib/org/ui-parity.test.mjs +96 -3
- package/lib/org/work-ledger.mjs +241 -0
- package/lib/org/work-ledger.test.mjs +237 -0
- package/lib/plan/adoption-e2e.test.mjs +366 -0
- package/lib/plan/budget-enforcement.test.mjs +400 -0
- package/lib/plan/budget-runtime.mjs +215 -0
- package/lib/plan/compile.mjs +201 -5
- package/lib/plan/compile.test.mjs +19 -5
- package/lib/plan/emit.mjs +8 -0
- package/lib/plan/emit.test.mjs +18 -0
- package/lib/resource-governor.mjs +58 -12
- package/lib/resource-governor.test.mjs +41 -1
- package/lib/security/audit-engine.mjs +45 -8
- package/lib/security/audit-engine.test.mjs +35 -0
- package/lib/setup/enroll-from-cohort.mjs +14 -1
- package/lib/setup/sections/mandate.mjs +48 -7
- package/lib/setup/sections/mandate.test.mjs +17 -2
- package/lib/setup/sections/orgmail.mjs +10 -2
- package/lib/setup/state.mjs +83 -2
- package/lib/telemetry/collect.mjs +360 -20
- package/lib/telemetry/collect.test.mjs +266 -0
- package/package.json +1 -1
- package/scripts/cost/track-claude-usage.mjs +207 -48
- package/scripts/cost/track-claude-usage.test.mjs +148 -0
- package/scripts/daemon/agent-daemon.mjs +315 -17
- package/scripts/daemon/assurance-e2e.test.mjs +421 -0
- package/scripts/daemon/assurance.mjs +944 -0
- package/scripts/daemon/assurance.test.mjs +668 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +56 -0
- package/scripts/daemon/cadence-consumer.mjs +147 -9
- package/scripts/daemon/cadence-consumer.test.mjs +6 -0
- package/scripts/daemon/cadence-handlers.mjs +158 -0
- package/scripts/daemon/cadence-handlers.test.mjs +64 -0
- package/scripts/daemon/deliver.mjs +314 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +10 -0
- package/scripts/daemon/dispatcher.mjs +64 -6
- package/scripts/daemon/responder-cost.test.mjs +68 -0
- package/scripts/daemon/responder.mjs +351 -298
- package/scripts/local-triggers/generate-plists.test.mjs +7 -4
- package/scripts/maintenance/backup-run.mjs +415 -0
- package/scripts/maintenance/backup-to-cloud.sh +16 -116
- package/scripts/org/send-orgmail.mjs +16 -0
- package/scripts/record-receipt.sh +63 -0
- package/scripts/restore-from-backup.sh +14 -3
- package/scripts/restore-from-backup.test.mjs +8 -5
- package/scripts/send-email-threaded.py +47 -0
- package/scripts/send-sms.sh +4 -0
- package/scripts/send-whatsapp.sh +4 -0
- package/scripts/setup/init-backup.mjs +93 -38
- package/scripts/slack-send.sh +12 -0
package/bin/maestro.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import { execFileSync, spawnSync } from "node:child_process";
|
|
|
30
30
|
import { createHash } from "node:crypto";
|
|
31
31
|
import { homedir } from "node:os";
|
|
32
32
|
import { checkOwnershipSet } from "../lib/fs-ownership.mjs";
|
|
33
|
+
import { summariseRows as summariseCostRows } from "../lib/cost/ledger-row.mjs";
|
|
33
34
|
import { resolveArchetype, migrateLegacyArchetype } from "../lib/archetype.mjs";
|
|
34
35
|
import { runAudit, applyFixes, buildAttestation } from "../lib/security/audit-engine.mjs";
|
|
35
36
|
import { selectProvider } from "../lib/secrets/providers.mjs";
|
|
@@ -1448,70 +1449,132 @@ Per-file behaviour:
|
|
|
1448
1449
|
// in isolation from disk + the doctor's side-effecting ok/warn/fail output.
|
|
1449
1450
|
|
|
1450
1451
|
/**
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1452
|
+
* Fold already-parsed ledger rows into the billable total plus the measurement
|
|
1453
|
+
* provenance the tripwire needs.
|
|
1454
|
+
*
|
|
1455
|
+
* Bills against lib/cost/ledger-row.mjs — the CLI's authoritative
|
|
1456
|
+
* `total_cost_usd` where present, the cache-aware token estimate as a logged
|
|
1457
|
+
* fallback. It used to sum `estimated_usd` blindly, which both under-counted
|
|
1458
|
+
* (no cache tier) and treated an unmeasured session as a free one.
|
|
1459
|
+
*
|
|
1453
1460
|
* @param {Array<object>} rows
|
|
1454
|
-
* @returns {{ rowCount:
|
|
1461
|
+
* @returns {{ rowCount:number, totalUsd:number, measured:number, unmeasured:number, nonLlmRows:number, degradations:string[] }}
|
|
1455
1462
|
*/
|
|
1456
1463
|
function summariseLedgerRows(rows) {
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
rowCount
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1464
|
+
const s = summariseCostRows(rows || []);
|
|
1465
|
+
return {
|
|
1466
|
+
// rowCount stays "LLM session rows" — attribution rows for zero-model work
|
|
1467
|
+
// are not sessions and must not arm a blindness alarm on their own.
|
|
1468
|
+
rowCount: s.sessions,
|
|
1469
|
+
totalUsd: s.measuredUsd,
|
|
1470
|
+
measured: s.measured,
|
|
1471
|
+
unmeasured: s.unmeasured,
|
|
1472
|
+
// WHICH failure produced `unmeasured`: usage unread, or usage read and
|
|
1473
|
+
// unpriceable. Different bugs, different fixes, different sentences.
|
|
1474
|
+
noTokenRows: s.noTokenRows,
|
|
1475
|
+
unpricedRows: s.unpricedRows,
|
|
1476
|
+
nonLlmRows: s.nonLlmRows,
|
|
1477
|
+
degradations: s.degradations,
|
|
1478
|
+
};
|
|
1466
1479
|
}
|
|
1467
1480
|
|
|
1468
1481
|
/**
|
|
1469
|
-
* Decide the cost-telemetry posture from today's ledger rows and
|
|
1470
|
-
* sessions
|
|
1471
|
-
*
|
|
1482
|
+
* Decide the cost-telemetry posture from today's ledger rows and the number of
|
|
1483
|
+
* sessions the daemon logged today.
|
|
1484
|
+
*
|
|
1485
|
+
* The contract, restated so it detects the real defect instead of the clock:
|
|
1472
1486
|
*
|
|
1473
|
-
* RED
|
|
1474
|
-
*
|
|
1475
|
-
*
|
|
1487
|
+
* RED — sessions ran and we cannot price them. Three distinct ways:
|
|
1488
|
+
* (a) unmeasured rows exist — a caller recorded measurement:"unknown"
|
|
1489
|
+
* (b) sessions ran with NO ledger row at all — spawned but never recorded
|
|
1490
|
+
* (c) measured rows exist but bill to $0 — pricing itself is broken
|
|
1491
|
+
* GREEN — no sessions ran (nothing to measure), or every session that ran is
|
|
1492
|
+
* measured and bills above $0.
|
|
1493
|
+
*
|
|
1494
|
+
* `sessionCount` MUST be evidence that a session actually ran (a logged session
|
|
1495
|
+
* start/completion), NOT a liveness signal. The previous version accepted a
|
|
1496
|
+
* fresh daemon heartbeat, so it went RED every UTC day between midnight and the
|
|
1497
|
+
* first session — an empty ledger plus a live daemon — and printed a fix that
|
|
1498
|
+
* had already shipped. A loud wrong alarm trains the operator to ignore the one
|
|
1499
|
+
* channel meant to be trustworthy, so it is as much a defect as a quiet bug.
|
|
1476
1500
|
*
|
|
1477
1501
|
* @param {{ rows?: Array<object>, sessionCount?: number }} input
|
|
1478
|
-
*
|
|
1479
|
-
* sessionCount — sessions evidenced today from outside the ledger (e.g. a
|
|
1480
|
-
* fresh daemon heartbeat). Activity = (rows.length>0) || (sessionCount>0).
|
|
1481
|
-
* @returns {{ red: boolean, status: "red"|"green", reason: string, rowCount: number, totalUsd: number, sessionCount: number }}
|
|
1502
|
+
* @returns {{ red:boolean, status:"red"|"green", reason:string, fix:string|null, rowCount:number, totalUsd:number, sessionCount:number, unmeasured:number, unrecorded:number }}
|
|
1482
1503
|
*/
|
|
1483
|
-
function evaluateCostTripwire({ rows = [], sessionCount = 0 } = {}) {
|
|
1484
|
-
const { rowCount, totalUsd } = summariseLedgerRows(rows);
|
|
1504
|
+
function evaluateCostTripwire({ rows = [], sessionCount = 0, sessionSource = "dispatcher" } = {}) {
|
|
1505
|
+
const { rowCount, totalUsd, measured, unmeasured, noTokenRows, unpricedRows, nonLlmRows } = summariseLedgerRows(rows);
|
|
1485
1506
|
const sessions = Math.max(0, Number(sessionCount) || 0);
|
|
1486
|
-
const hadActivity = rowCount > 0 || sessions > 0;
|
|
1487
1507
|
|
|
1488
|
-
|
|
1508
|
+
// LIKE FOR LIKE. `sessionCount` comes from logs/daemon/<date>-sessions.jsonl,
|
|
1509
|
+
// which ONLY the dispatcher writes (`responder` and the cadence consumer have
|
|
1510
|
+
// no logSession). Comparing it against rows from EVERY writer meant the
|
|
1511
|
+
// responder rows this changeset added would mask a dispatcher that had stopped
|
|
1512
|
+
// recording entirely: 35 dispatcher sessions unpriced + 40 responder rows read
|
|
1513
|
+
// as `unrecorded: 0`, status green. Count only the rows from the writer the
|
|
1514
|
+
// session log belongs to.
|
|
1515
|
+
const sourceRows = rows.filter((r) => r && (r.source ?? null) === sessionSource);
|
|
1516
|
+
const sourceSessions = summariseLedgerRows(sourceRows).rowCount;
|
|
1517
|
+
const unrecorded = Math.max(0, sessions - sourceSessions);
|
|
1518
|
+
const base = { rowCount, totalUsd, sessionCount: sessions, unmeasured, unrecorded, sourceRowCount: sourceSessions };
|
|
1519
|
+
|
|
1520
|
+
if (rowCount === 0 && sessions === 0) {
|
|
1489
1521
|
return {
|
|
1522
|
+
...base,
|
|
1490
1523
|
red: false,
|
|
1491
1524
|
status: "green",
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1525
|
+
fix: null,
|
|
1526
|
+
reason: nonLlmRows > 0
|
|
1527
|
+
? `no LLM sessions ran today — nothing to measure (${nonLlmRows} zero-model attribution row(s))`
|
|
1528
|
+
: "no sessions ran today — nothing to measure",
|
|
1496
1529
|
};
|
|
1497
1530
|
}
|
|
1498
|
-
|
|
1531
|
+
|
|
1532
|
+
if (noTokenRows > 0) {
|
|
1499
1533
|
return {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
rowCount,
|
|
1504
|
-
|
|
1505
|
-
|
|
1534
|
+
...base,
|
|
1535
|
+
red: true,
|
|
1536
|
+
status: "red",
|
|
1537
|
+
reason: `spend telemetry is partially blind — ${noTokenRows}/${rowCount} session row(s) recorded NO token counts (measurement:"unknown"). Their cost is unknown, not zero.`,
|
|
1538
|
+
fix: "check logs/daemon for cost_usage_parse_failed — a caller ran a session but could not read usage from the `claude --output-format json` envelope. Each row's unmeasured_reason names the parse failure.",
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (unpricedRows > 0) {
|
|
1543
|
+
return {
|
|
1544
|
+
...base,
|
|
1545
|
+
red: true,
|
|
1546
|
+
status: "red",
|
|
1547
|
+
reason: `spend telemetry is blind — ${unpricedRows}/${rowCount} session row(s) bill to $0 despite carrying usage. Pricing is broken, not the day.`,
|
|
1548
|
+
fix: "callers must pass real --input-tokens/--output-tokens (and --cache-read-tokens/--cache-creation-tokens/--total-cost-usd) to scripts/cost/track-claude-usage.mjs, lifted from the `claude --output-format json` envelope. A model missing from the router catalog also lands here.",
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
if (unrecorded > 0) {
|
|
1553
|
+
return {
|
|
1554
|
+
...base,
|
|
1555
|
+
red: true,
|
|
1556
|
+
status: "red",
|
|
1557
|
+
reason: `spend telemetry is incomplete — the ${sessionSource} logged ${sessions} session(s) today but only ${sourceSessions} ${sessionSource} row(s) reached the cost ledger; ${unrecorded} ran unpriced.`,
|
|
1558
|
+
fix: "a spawn path is not calling scripts/cost/track-claude-usage.mjs at all. Every path that spawns `claude` must record a row — measured, or explicitly --tokens-unknown.",
|
|
1506
1559
|
};
|
|
1507
1560
|
}
|
|
1561
|
+
|
|
1562
|
+
if (measured > 0 && totalUsd <= 0) {
|
|
1563
|
+
return {
|
|
1564
|
+
...base,
|
|
1565
|
+
red: true,
|
|
1566
|
+
status: "red",
|
|
1567
|
+
reason: `spend telemetry is blind — ${measured} measured session(s) bill to $0. Pricing is broken, not the day.`,
|
|
1568
|
+
fix: "callers must pass real --input-tokens/--output-tokens (and --cache-read-tokens/--cache-creation-tokens/--total-cost-usd) to scripts/cost/track-claude-usage.mjs, lifted from the `claude --output-format json` envelope.",
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1508
1572
|
return {
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
totalUsd
|
|
1514
|
-
sessionCount: sessions,
|
|
1573
|
+
...base,
|
|
1574
|
+
red: false,
|
|
1575
|
+
status: "green",
|
|
1576
|
+
fix: null,
|
|
1577
|
+
reason: `spend telemetry live ($${totalUsd} across ${measured} measured session(s) today${unmeasured ? `, ${unmeasured} unmeasured` : ""})`,
|
|
1515
1578
|
};
|
|
1516
1579
|
}
|
|
1517
1580
|
|
|
@@ -1942,13 +2005,34 @@ async function doctor() {
|
|
|
1942
2005
|
}
|
|
1943
2006
|
} catch { /* breaker read best-effort */ }
|
|
1944
2007
|
|
|
1945
|
-
// Today's budget band.
|
|
2008
|
+
// Today's budget band + the enforcement posture it implies.
|
|
2009
|
+
//
|
|
2010
|
+
// The band is measured against the seat's hq-FUNDED envelope
|
|
2011
|
+
// (Employee.payBasis.meteredBudget → mandate body seatBudgetCents), with the
|
|
2012
|
+
// local cap as a safety net. An unfunded seat is reported as exactly that —
|
|
2013
|
+
// "no ceiling is being enforced" is a finding, not a clean bill of health.
|
|
1946
2014
|
try {
|
|
1947
2015
|
const { dailyStatus } = await import(join(MAESTRO_ROOT, "lib", "budget-guard.mjs"));
|
|
1948
2016
|
const b = dailyStatus({ agentRoot: cwd });
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2017
|
+
const where = `$${b.spentUSD.toFixed(2)}/$${b.capUSD.toFixed(2)} today (${b.pct}%, ${b.capSource})`;
|
|
2018
|
+
const monthly = b.envelope && b.envelope.monthUSD != null
|
|
2019
|
+
? `; envelope $${b.envelope.monthUSD.toFixed(2)}/mo, MTD $${b.month.spentUSD.toFixed(2)} (${b.monthPct}%)`
|
|
2020
|
+
: "";
|
|
2021
|
+
if (b.mode === "refused") { fail(`Budget REFUSED — ${where}${monthly}: spawns other than direct human replies are deferred`); issues++; }
|
|
2022
|
+
else if (b.mode === "suspended") { warn(`Budget SUSPENDED — ${where}${monthly}: OUTCOME obligations suspended; inbox + offline-safe cadences continue`); issues++; }
|
|
2023
|
+
else if (b.mode === "degraded") { warn(`Budget DEGRADED — ${where}${monthly}: cheapest model class, fan-out 1, self-directed work stopped`); issues++; }
|
|
2024
|
+
else if (b.band >= 80) { warn(`Budget at ${b.band}% — ${where}${monthly} (supervisor notified; nothing degraded yet)`); }
|
|
2025
|
+
else ok(`Budget band ${b.band}% — ${where}${monthly}`);
|
|
2026
|
+
if (!b.funded) {
|
|
2027
|
+
warn(
|
|
2028
|
+
"Seat is UNFUNDED: hq published no seatBudgetCents, so the org's ceiling is not being enforced — " +
|
|
2029
|
+
"the number above is this seat's own config/recovery.yaml. Set Employee.payBasis.meteredBudget for this member in Cohort."
|
|
2030
|
+
);
|
|
2031
|
+
issues++;
|
|
2032
|
+
}
|
|
2033
|
+
if (!b.supervisorMemberId) {
|
|
2034
|
+
warn("No supervisor edge on the mandate — budget escalations have nowhere to go, and are NOT sent to the owner by design");
|
|
2035
|
+
}
|
|
1952
2036
|
} catch { /* ledger read best-effort */ }
|
|
1953
2037
|
|
|
1954
2038
|
// Cost-telemetry tripwire (observability F2). Read today's cost ledger and
|
|
@@ -1967,25 +2051,36 @@ async function doctor() {
|
|
|
1967
2051
|
try { rows.push(JSON.parse(line)); } catch { /* skip malformed row */ }
|
|
1968
2052
|
}
|
|
1969
2053
|
}
|
|
1970
|
-
//
|
|
1971
|
-
//
|
|
1972
|
-
//
|
|
1973
|
-
|
|
2054
|
+
// Out-of-ledger session evidence: sessions the daemon LOGGED as completed
|
|
2055
|
+
// today. This deliberately replaced a liveness heartbeat. A fresh
|
|
2056
|
+
// heartbeat only proves the daemon is up, so "heartbeat + empty ledger"
|
|
2057
|
+
// fired RED every UTC day between midnight and the first session and
|
|
2058
|
+
// printed a fix that had already shipped — an alarm that cries wolf daily
|
|
2059
|
+
// is worse than no alarm, because the operator learns to skip it.
|
|
2060
|
+
// A logged completion is proof a session actually ran and therefore that
|
|
2061
|
+
// a ledger row is genuinely owed.
|
|
2062
|
+
let loggedSessions = 0;
|
|
1974
2063
|
try {
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2064
|
+
const sessionLog = join(cwd, "logs/daemon", `${ledgerDate}-sessions.jsonl`);
|
|
2065
|
+
if (existsSync(sessionLog)) {
|
|
2066
|
+
for (const line of readFileSync(sessionLog, "utf-8").split("\n")) {
|
|
2067
|
+
if (!line.trim()) continue;
|
|
2068
|
+
try {
|
|
2069
|
+
if (JSON.parse(line).event === "completed") loggedSessions++;
|
|
2070
|
+
} catch { /* skip malformed line */ }
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
} catch { /* no session log — rely on ledger rows alone */ }
|
|
1979
2074
|
|
|
1980
|
-
|
|
2075
|
+
// `sessionSource` names the writer the session log belongs to, so the two
|
|
2076
|
+
// populations being compared are the same population.
|
|
2077
|
+
const tripwire = evaluateCostTripwire({ rows, sessionCount: loggedSessions, sessionSource: "dispatcher" });
|
|
1981
2078
|
if (tripwire.red) {
|
|
1982
2079
|
fail(`Cost telemetry BLIND — ${tripwire.reason}`);
|
|
1983
|
-
fail(` Fix:
|
|
2080
|
+
if (tripwire.fix) fail(` Fix: ${tripwire.fix}`);
|
|
1984
2081
|
issues++;
|
|
1985
|
-
} else if (tripwire.rowCount === 0 && heartbeatActive === 0) {
|
|
1986
|
-
ok("Cost telemetry: no sessions recorded today (nothing to measure)");
|
|
1987
2082
|
} else {
|
|
1988
|
-
ok(`Cost telemetry
|
|
2083
|
+
ok(`Cost telemetry: ${tripwire.reason}`);
|
|
1989
2084
|
}
|
|
1990
2085
|
} catch { /* cost ledger read best-effort */ }
|
|
1991
2086
|
|
|
@@ -2325,35 +2420,37 @@ async function doctor() {
|
|
|
2325
2420
|
}
|
|
2326
2421
|
}
|
|
2327
2422
|
|
|
2328
|
-
// ── Backup
|
|
2329
|
-
//
|
|
2330
|
-
//
|
|
2331
|
-
//
|
|
2332
|
-
//
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2423
|
+
// ── Backup posture (DR, gaps-enterprise-ops G16) ────────────────────────
|
|
2424
|
+
// The verdict is NOT computed here — lib/backup/policy.mjs owns it, shared
|
|
2425
|
+
// with the nightly-backup cadence and the runner, because three call sites
|
|
2426
|
+
// each deriving their own idea of "configured" is how they drifted apart.
|
|
2427
|
+
//
|
|
2428
|
+
// The ladder it applies (see backupVerdict):
|
|
2429
|
+
// fail — no config / unparseable / disabled / nothing would be archived
|
|
2430
|
+
// fail — enabled but no run has EVER succeeded (there is no restore point)
|
|
2431
|
+
// warn — restore points are stale, OR they are local-only (a dead machine
|
|
2432
|
+
// still loses everything — named in words, not left implied)
|
|
2433
|
+
// ok — fresh AND off-machine
|
|
2434
|
+
//
|
|
2435
|
+
// "local-only" is a WARN and not a FAIL on purpose: a check that is red on
|
|
2436
|
+
// every machine in the fleet is a check nobody reads, and "no restore point
|
|
2437
|
+
// at all" is a genuinely worse state than "restore points that would not
|
|
2438
|
+
// survive a house fire". Doctor now tells you which one you have.
|
|
2439
|
+
try {
|
|
2440
|
+
const { resolveBackupPlan, backupVerdict } = await import("../lib/backup/policy.mjs");
|
|
2441
|
+
const { backupFreshness } = await import("../lib/diagnostics/backup-freshness.mjs");
|
|
2442
|
+
const plan = resolveBackupPlan({ agentRoot: cwd });
|
|
2443
|
+
const verdict = backupVerdict(plan, backupFreshness({ agentRoot: cwd }));
|
|
2444
|
+
if (verdict.level === "ok") ok(verdict.msg);
|
|
2445
|
+
else if (verdict.level === "warn") { warn(verdict.msg); issues++; }
|
|
2446
|
+
else { fail(verdict.msg); issues++; }
|
|
2447
|
+
if (verdict.fix) console.log(` fix: ${verdict.fix}`);
|
|
2448
|
+
for (const v of plan.violations || []) {
|
|
2449
|
+
warn(` backup include path "${v.path}" is DROPPED by the credential deny-list (${v.pattern}) — it will never be archived.`);
|
|
2353
2450
|
}
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
|
|
2451
|
+
} catch (err) {
|
|
2452
|
+
// Fail-open, never silent: a doctor probe must not take doctor down.
|
|
2453
|
+
warn(`Backup posture could not be evaluated (${err && err.message}) — treat as unbacked until this resolves.`);
|
|
2357
2454
|
issues++;
|
|
2358
2455
|
}
|
|
2359
2456
|
|