@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/lib/plan/compile.mjs
CHANGED
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* + ALTITUDE_CADENCES → SCHEDULE, origin "archetype"
|
|
18
18
|
* 3. each ADOPTED objective w/ sensor → SCHEDULE `schedule.measure.<key>`
|
|
19
19
|
* + OUTCOME `outcome.<key>`
|
|
20
|
+
* "adopted" means `state:"active"` AND
|
|
21
|
+
* adopted by somebody other than the
|
|
22
|
+
* owner — see rule 3 below
|
|
20
23
|
* 4. each adopted objective with a
|
|
21
24
|
* REVIEWER collaborator → SCHEDULE `schedule.review.<key>`
|
|
22
25
|
* 5. mandate `reactsTo[]` + the four
|
|
@@ -41,9 +44,18 @@
|
|
|
41
44
|
import { createHash } from "node:crypto";
|
|
42
45
|
import { canonical } from "../mandate/cache.mjs";
|
|
43
46
|
import { assertMandateContract } from "../mandate/contract.mjs";
|
|
47
|
+
import { adoptedObjectives, adoptionDefect, memberIdOf, validateMandateBody } from "../mandate/model.mjs";
|
|
48
|
+
import { isHumanLaneCadence } from "../cadences.mjs";
|
|
44
49
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Bump when emission rules change — it is part of `inputsHash`.
|
|
52
|
+
*
|
|
53
|
+
* 2: adopted objectives are now filtered through `mandate/model.adoptedObjectives`
|
|
54
|
+
* (the client-side half of the anti-Goodhart law) instead of a bare
|
|
55
|
+
* `state === "active"` check, so a self-adopted or structurally-invalid node
|
|
56
|
+
* compiles nothing and surfaces as drift.
|
|
57
|
+
*/
|
|
58
|
+
export const COMPILER_VERSION = 2;
|
|
47
59
|
|
|
48
60
|
/** Read primitives every session-spawning obligation may use. */
|
|
49
61
|
export const BASELINE_TOOLS = Object.freeze(["Read", "Grep", "Glob", "LS"]);
|
|
@@ -320,6 +332,10 @@ export function compilePlan(input = {}) {
|
|
|
320
332
|
uses,
|
|
321
333
|
budget_cents_per_period: defaultBudget,
|
|
322
334
|
offline_safe: c.mode === "inline",
|
|
335
|
+
// The human-reply lane — see isHumanLaneObligation. Stamped here (not
|
|
336
|
+
// inferred at enforcement time) so the plan on disk SAYS which obligations
|
|
337
|
+
// a budget breach may never stop, and an operator can read it.
|
|
338
|
+
human_lane: isHumanLaneCadence(c.id),
|
|
323
339
|
status: "active",
|
|
324
340
|
});
|
|
325
341
|
}
|
|
@@ -351,8 +367,43 @@ export function compilePlan(input = {}) {
|
|
|
351
367
|
}
|
|
352
368
|
|
|
353
369
|
// ── 3/4. adopted objectives → measure + outcome (+ review) ──────────────
|
|
354
|
-
|
|
355
|
-
|
|
370
|
+
// THE ANTI-GOODHART LAW, CLIENT SIDE. This used to be `state === "active"` and
|
|
371
|
+
// nothing else — which meant the compiler trusted the snapshot's state column
|
|
372
|
+
// and NOTHING about who put it there. hq now refuses self-adoption and
|
|
373
|
+
// peer-adoption at the handler, but the compiler is the offline half of the
|
|
374
|
+
// same law: an agent that is partitioned, replaying a cached body, or handed a
|
|
375
|
+
// hand-edited state file must not compile a graded OUTCOME obligation from a
|
|
376
|
+
// number it adopted for itself. `adoptedObjectives` drops self-adopted and
|
|
377
|
+
// structurally-invalid nodes; `validateMandateBody` says why, out loud.
|
|
378
|
+
const owner = memberIdOf(mandate) || input.memberId || null;
|
|
379
|
+
const validation = validateMandateBody(mandate, { ownerMemberId: owner });
|
|
380
|
+
for (const err of validation.errors) note("error", `[compilePlan] mandate: ${err}`);
|
|
381
|
+
for (const w of validation.warnings) note("warn", `[compilePlan] mandate: ${w}`);
|
|
382
|
+
const adopted = adoptedObjectives(mandate, {
|
|
383
|
+
ownerMemberId: owner,
|
|
384
|
+
badKeys: validation.badKeys,
|
|
385
|
+
}).filter((o) => o.kind !== "PILLAR");
|
|
386
|
+
// Named separately so a REFUSAL is never indistinguishable from an absence:
|
|
387
|
+
// "you have 4 active objectives and I compiled 0" is a bug report, and it has
|
|
388
|
+
// to be legible from the plan alone.
|
|
389
|
+
for (const o of (Array.isArray(mandate.objectives) ? mandate.objectives : [])) {
|
|
390
|
+
if (!o || o.state !== "active" || o.kind === "PILLAR") continue;
|
|
391
|
+
if (adopted.includes(o)) continue;
|
|
392
|
+
const defect = adoptionDefect(o, owner);
|
|
393
|
+
const reason =
|
|
394
|
+
defect === "self_adopted"
|
|
395
|
+
? `adoptedById equals the owning seat (${owner}) \u2014 a seat cannot define, measure and be graded on the same number`
|
|
396
|
+
: defect === "missing_adoption_provenance"
|
|
397
|
+
? "the objective is active but carries NO adoptedById \u2014 nobody is on record as having handed this seat the number, so it cannot drive graded work (a deleted field is the cheapest tamper there is, and it used to fail open)"
|
|
398
|
+
: "the objective did not validate; see plan warnings";
|
|
399
|
+
drift.push({
|
|
400
|
+
kind: defect || "invalid_objective",
|
|
401
|
+
key: o.key ? `outcome.${o.key}` : null,
|
|
402
|
+
detail: { objectiveKey: o.key || null, reason },
|
|
403
|
+
});
|
|
404
|
+
note("error",
|
|
405
|
+
`[compilePlan] objective "${o.key || "?"}" is active but compiles NOTHING: ${reason}`);
|
|
406
|
+
}
|
|
356
407
|
const proposedCount = (Array.isArray(mandate.objectives) ? mandate.objectives : []).filter((o) => o && o.state === "proposed").length;
|
|
357
408
|
|
|
358
409
|
// Budget envelope: highest-weight objectives get the envelope first; the rest
|
|
@@ -544,6 +595,150 @@ export function compilePlan(input = {}) {
|
|
|
544
595
|
};
|
|
545
596
|
}
|
|
546
597
|
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
// The RUNTIME budget transition
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Apply a live budget posture to an already-compiled plan.
|
|
604
|
+
*
|
|
605
|
+
* `compilePlan` already suspends an objective whose apportioned share does not
|
|
606
|
+
* fit the seat envelope — but that is a PLAN-TIME decision made once, against
|
|
607
|
+
* the mandate's arithmetic. It cannot see what the seat actually spent since.
|
|
608
|
+
* This is the same transition made at RUNTIME, against the ledger, so the 125 %
|
|
609
|
+
* rung of the ladder ("suspend the obligations, not the seat") is real rather
|
|
610
|
+
* than a sentence in a design doc.
|
|
611
|
+
*
|
|
612
|
+
* WHAT IT SUSPENDS: obligations that exist because of an adopted objective —
|
|
613
|
+
* `OUTCOME` rows and the `measure`/`review` SCHEDULEs that serve them (they carry
|
|
614
|
+
* `objective_id`). Those are the seat's discretionary, funded work.
|
|
615
|
+
*
|
|
616
|
+
* WHAT IT NEVER SUSPENDS, at any band:
|
|
617
|
+
* - `mode:"inline"` obligations. They are deterministic local computation with
|
|
618
|
+
* no model call — they cost nothing, so suspending them buys nothing and
|
|
619
|
+
* stops the seat's heartbeat.
|
|
620
|
+
* - standard cadences and REACTs with no objective behind them — the seat's
|
|
621
|
+
* inbox and its ability to answer a human.
|
|
622
|
+
* Invariant #1: we degrade, we never brick.
|
|
623
|
+
*
|
|
624
|
+
* `offline_safe` IS NOT AN EXEMPTION, and the trap is worth naming: the compiler
|
|
625
|
+
* marks every OUTCOME and every `schedule.measure.*` as `offline_safe:true`
|
|
626
|
+
* (they can run without the network), so treating that flag as "free" exempts
|
|
627
|
+
* precisely the obligations this rung exists to suspend — the ceiling compiles,
|
|
628
|
+
* fires, and suspends nothing. Offline-safe means "does not need hq"; it says
|
|
629
|
+
* nothing about whether the work spends money.
|
|
630
|
+
*
|
|
631
|
+
* PURE and idempotent: applying the same posture twice yields the same plan and
|
|
632
|
+
* the same single drift row per obligation. Never throws — a governance bug must
|
|
633
|
+
* not be able to wedge a plan.
|
|
634
|
+
*
|
|
635
|
+
* @param {object} plan a `compilePlan` result (or `{obligations}`)
|
|
636
|
+
* @param {object} posture `lib/budget-guard.postureForBand()` output
|
|
637
|
+
* @param {object} [detail] extra fields folded into each drift record (spend, cap…)
|
|
638
|
+
* @returns {{obligations:object[], drift:object[], suspended:string[], counts:object}}
|
|
639
|
+
*/
|
|
640
|
+
export function applyBudgetPosture(plan, posture, detail = {}) {
|
|
641
|
+
const obligations = Array.isArray(plan && plan.obligations) ? plan.obligations : Array.isArray(plan) ? plan : [];
|
|
642
|
+
const drift = [];
|
|
643
|
+
const suspended = [];
|
|
644
|
+
const suspend = !!(posture && posture.suspendOutcomeObligations);
|
|
645
|
+
|
|
646
|
+
const out = obligations.map((ob) => {
|
|
647
|
+
if (!ob || typeof ob !== "object") return ob;
|
|
648
|
+
if (!suspend) return ob;
|
|
649
|
+
if (ob.status !== "active") return ob; // already suspended at plan time
|
|
650
|
+
if (ob.mode === "inline") return ob; // deterministic, zero-cost, load-bearing
|
|
651
|
+
// "Because of an adopted objective" is the test — not the obligation kind.
|
|
652
|
+
// A `schedule.measure.*` costs the seat money on the same warrant its
|
|
653
|
+
// OUTCOME does, and leaving it running would keep spending while the thing
|
|
654
|
+
// it measures is suspended.
|
|
655
|
+
const fromObjective = ob.kind === "OUTCOME" || (ob.objective_id != null && ob.scope === "mandate");
|
|
656
|
+
if (!fromObjective) return ob;
|
|
657
|
+
|
|
658
|
+
suspended.push(ob.key);
|
|
659
|
+
drift.push({
|
|
660
|
+
kind: "budget_breach",
|
|
661
|
+
key: ob.key,
|
|
662
|
+
detail: {
|
|
663
|
+
reason: "runtime spend breached the seat envelope",
|
|
664
|
+
band: posture.band,
|
|
665
|
+
mode: posture.mode,
|
|
666
|
+
objective_id: ob.objective_id || null,
|
|
667
|
+
budget_cents_per_period: Number.isFinite(ob.budget_cents_per_period) ? ob.budget_cents_per_period : null,
|
|
668
|
+
...detail,
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
return { ...ob, status: "suspended", suspended_by: "budget", suspended_band: posture.band };
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
obligations: out,
|
|
676
|
+
drift,
|
|
677
|
+
suspended,
|
|
678
|
+
counts: {
|
|
679
|
+
total: out.length,
|
|
680
|
+
suspended: out.filter((o) => o && o.status === "suspended").length,
|
|
681
|
+
active: out.filter((o) => o && o.status === "active").length,
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* May THIS obligation run right now, under this posture?
|
|
688
|
+
*
|
|
689
|
+
* The single-obligation form of {@link applyBudgetPosture}, for the hot path:
|
|
690
|
+
* the cadence consumer holds a registry entry, not a plan. Same rule, same
|
|
691
|
+
* exemptions, so the daemon and the plan can never disagree about what is
|
|
692
|
+
* suspended.
|
|
693
|
+
*
|
|
694
|
+
* @param {{kind?:string, scope?:string, objective_id?:string|null,
|
|
695
|
+
* mode?:string, status?:string}} ob
|
|
696
|
+
* @param {object} posture
|
|
697
|
+
* @returns {{allowed:boolean, reason:string}}
|
|
698
|
+
*/
|
|
699
|
+
export function obligationAllowedUnderPosture(ob = {}, posture = {}) {
|
|
700
|
+
if (ob.status && ob.status !== "active") {
|
|
701
|
+
return { allowed: false, reason: `obligation status is ${ob.status}` };
|
|
702
|
+
}
|
|
703
|
+
// Only INLINE work is unconditionally free. `offline_safe` is not an
|
|
704
|
+
// exemption — see applyBudgetPosture; every OUTCOME carries it.
|
|
705
|
+
if (ob.mode === "inline") {
|
|
706
|
+
return { allowed: true, reason: "inline work is deterministic and costs nothing — it continues at every band" };
|
|
707
|
+
}
|
|
708
|
+
if (posture.refuseNonHumanSpawn && !isHumanLaneObligation(ob)) {
|
|
709
|
+
return {
|
|
710
|
+
allowed: false,
|
|
711
|
+
reason: `budget band ${posture.band}% — refusing spawns that are not a direct human reply`,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const fromObjective = ob.kind === "OUTCOME" || (ob.objective_id != null && ob.scope === "mandate");
|
|
715
|
+
if (posture.suspendOutcomeObligations && fromObjective) {
|
|
716
|
+
return { allowed: false, reason: `budget band ${posture.band}% — OUTCOME obligations are suspended` };
|
|
717
|
+
}
|
|
718
|
+
return { allowed: true, reason: "within the seat envelope" };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Is a human waiting on the other end of this obligation?
|
|
723
|
+
*
|
|
724
|
+
* The 150 % rung refuses "anything that is not a direct human reply", and that
|
|
725
|
+
* predicate used to be `kind === "REACT"`. Every cadence the consumer evaluates
|
|
726
|
+
* is a SCHEDULE, so at band 150 the gate blocked `inbox-processor` and
|
|
727
|
+
* `messaging-inbound` — the cadence that pulls org DMs and @mentions into the
|
|
728
|
+
* inbox pipeline at all. A budget rung that stops the seat reading its inbox is
|
|
729
|
+
* precisely the bricking Invariant #1 forbids, so the lane is now explicit:
|
|
730
|
+
*
|
|
731
|
+
* REACT — fires on an org event somebody else caused. Human-facing
|
|
732
|
+
* by construction (an assignment, an approval request, a DM).
|
|
733
|
+
* human_lane:true — stamped by the compiler from lib/cadences.HUMAN_LANE_CADENCES.
|
|
734
|
+
*
|
|
735
|
+
* Kept as a positive allow-list rather than a deny-list: a new cadence added
|
|
736
|
+
* next quarter defaults to "deferrable under a breach", which is the safe side.
|
|
737
|
+
*/
|
|
738
|
+
export function isHumanLaneObligation(ob = {}) {
|
|
739
|
+
return ob.human_lane === true || ob.kind === "REACT";
|
|
740
|
+
}
|
|
741
|
+
|
|
547
742
|
/** The identity-bearing projection an obligationsHash is taken over. */
|
|
548
743
|
function identityOf(ob) {
|
|
549
744
|
return {
|
|
@@ -583,5 +778,6 @@ function usesForArchetypeCadence(c, reachable) {
|
|
|
583
778
|
|
|
584
779
|
export default {
|
|
585
780
|
COMPILER_VERSION, BASELINE_TOOLS, STANDARD_CADENCE_USES, STANDARD_REACTS,
|
|
586
|
-
compilePlan,
|
|
781
|
+
compilePlan, applyBudgetPosture, obligationAllowedUnderPosture, isHumanLaneObligation,
|
|
782
|
+
solveSlot, busyWindows, allowedToolsFor, actionClassesFor, toolNameFor, rankOf,
|
|
587
783
|
};
|
|
@@ -42,7 +42,10 @@ function objective(over = {}) {
|
|
|
42
42
|
key: "pipeline-coverage", kind: "OBJECTIVE", text: "Pipeline coverage", state: "active",
|
|
43
43
|
metric: "pipeline_coverage_x", unit: "x", direction: "up", target: 3, tolerance: 0.2,
|
|
44
44
|
weight: 1, cadence: "weekly", sensor: { capability: "crm_list_deals", params: { stage: "open" }, source: "method" },
|
|
45
|
-
|
|
45
|
+
// `adoptedById` is part of the fixture because it is part of the LAW: an
|
|
46
|
+
// active objective with no adopter on record compiles nothing (see
|
|
47
|
+
// lib/mandate/model.adoptionDefect). It must differ from the body's memberId.
|
|
48
|
+
collaborators: [], id: "obj_7Kx", charterSectionId: "cs_12", adoptedById: "M-SUPERVISOR", ...over,
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -314,14 +317,23 @@ test("deriveMandate: charter pillars become PILLARs, KPI categories become OBJEC
|
|
|
314
317
|
{ kind: "PILLAR", order: 0, body: "1) Own the agent-platform substrate — runtime, memory, tools." },
|
|
315
318
|
{ kind: "PILLAR", order: 1, body: "2) Make the fleet governable: every capability observable." },
|
|
316
319
|
],
|
|
317
|
-
kpiCategories: [
|
|
320
|
+
kpiCategories: [
|
|
321
|
+
{ id: "execution", name: "Fleet execution & follow-through", examples: ["Commitment-closure rate"] },
|
|
322
|
+
// Overlaps neither pillar's words. It is CARRIED but left unplaced —
|
|
323
|
+
// the old deriver silently parented it onto pillar #1.
|
|
324
|
+
{ id: "warehouse-latency", name: "Warehouse latency", examples: ["p95 ms"] },
|
|
325
|
+
],
|
|
318
326
|
manifest: manifest(),
|
|
319
327
|
});
|
|
320
328
|
assert.equal(d.counts.pillars, 2);
|
|
321
|
-
assert.equal(d.counts.objectives,
|
|
329
|
+
assert.equal(d.counts.objectives, 2);
|
|
322
330
|
const exec = d.objectives.find((o) => o.key === "execution");
|
|
323
331
|
assert.equal(exec.kind, "OBJECTIVE");
|
|
324
|
-
assert.ok(exec.parentKey, "every objective hangs off a pillar (provenance)");
|
|
332
|
+
assert.ok(exec.parentKey, "every PLACEABLE objective hangs off a pillar (provenance)");
|
|
333
|
+
assert.ok(exec.charterSectionId !== undefined);
|
|
334
|
+
const orphan = d.objectives.find((o) => o.key === "warehouse-latency");
|
|
335
|
+
assert.equal(orphan.parentKey, null, "no fabricated pillar edge");
|
|
336
|
+
assert.ok(d.degradations.some((x) => x.kind === "no_pillar_trace" && x.key === "warehouse-latency"));
|
|
325
337
|
assert.equal(exec.state, "proposed", "NOTHING is adopted by the agent");
|
|
326
338
|
assert.equal(exec.sensor.source, "method");
|
|
327
339
|
assert.equal(exec.sensor.capability, "board_ready", "bound to a REACHABLE capability");
|
|
@@ -359,7 +371,9 @@ test("end to end: derive → compile produces a plan whose obligations trace bac
|
|
|
359
371
|
manifest: manifest(),
|
|
360
372
|
});
|
|
361
373
|
// Simulate the human adoption step in hq.
|
|
362
|
-
|
|
374
|
+
// Simulating hq's adopt handler means simulating ALL of it — including the
|
|
375
|
+
// `adoptedById` column it stamps. Without one the compiler refuses the row.
|
|
376
|
+
const adopted = d.objectives.map((o) => (o.kind === "OBJECTIVE" ? { ...o, state: "active", target: 0.95, id: `obj_${o.key}`, adoptedById: "M-SUPERVISOR" } : o));
|
|
363
377
|
const p = compilePlan({ mandate: toMandateBody({ objectives: adopted }), manifest: manifest(), standardCadences: STANDARD });
|
|
364
378
|
const outcome = p.obligations.find((o) => o.kind === "OUTCOME");
|
|
365
379
|
assert.ok(outcome, "adoption turns the proposal into a real obligation");
|
package/lib/plan/emit.mjs
CHANGED
|
@@ -95,6 +95,14 @@ export function toRegistry(obligations) {
|
|
|
95
95
|
guardModule: ob.guard_module || null,
|
|
96
96
|
budgetCents: Number.isFinite(ob.budget_cents_per_period) ? ob.budget_cents_per_period : null,
|
|
97
97
|
obligationKey: ob.key,
|
|
98
|
+
// The three fields the RUNTIME budget gate needs (see
|
|
99
|
+
// `lib/plan/compile.obligationAllowedUnderPosture`). The consumer holds a
|
|
100
|
+
// registry entry, not a plan, and without these it could not tell a
|
|
101
|
+
// funded objective's cadence from the seat's heartbeat — so a budget
|
|
102
|
+
// breach either suspended everything or nothing.
|
|
103
|
+
scope: ob.scope || "mandate",
|
|
104
|
+
objectiveId: ob.objective_id || null,
|
|
105
|
+
offlineSafe: ob.offline_safe === true,
|
|
98
106
|
};
|
|
99
107
|
}
|
|
100
108
|
return reg;
|
package/lib/plan/emit.test.mjs
CHANGED
|
@@ -42,6 +42,7 @@ const ARCHETYPE = [
|
|
|
42
42
|
function adoptedObjective() {
|
|
43
43
|
return {
|
|
44
44
|
key: "pipeline-coverage", kind: "OBJECTIVE", text: "Pipeline coverage", state: "active", id: "obj_7Kx",
|
|
45
|
+
adoptedById: "M-SUPERVISOR",
|
|
45
46
|
metric: "pipeline_coverage_x", direction: "up", target: 3, tolerance: 0.2, weight: 1, cadence: "weekly",
|
|
46
47
|
sensor: { capability: "crm_list_deals", params: { stage: "open" }, source: "method" }, collaborators: [],
|
|
47
48
|
};
|
|
@@ -184,6 +185,23 @@ test("SUSPENDED obligations are in plan.yaml but never in the schedule artefacts
|
|
|
184
185
|
assert.deepEqual(toTsvLines(obs), ["b|60|"]);
|
|
185
186
|
});
|
|
186
187
|
|
|
188
|
+
test("the registry carries the provenance the RUNTIME budget gate needs", () => {
|
|
189
|
+
const obs = [
|
|
190
|
+
{ key: "schedule.measure.gm", kind: "SCHEDULE", cadence_id: "measure-gm", scope: "mandate", objective_id: "obj_7Kx", mode: "guarded", prompt: "p.md", schedule: { interval: 60 }, status: "active", uses: [], allowed_tools: ["Read"], action_classes: [], offline_safe: true, source: { origin: "mandate", ref: "gm" }, budget_cents_per_period: 100 },
|
|
191
|
+
{ key: "schedule.cadence-bus-heartbeat", kind: "SCHEDULE", cadence_id: "cadence-bus-heartbeat", scope: "standard", mode: "inline", prompt: "p.md", schedule: { interval: 300 }, status: "active", uses: [], allowed_tools: [], action_classes: [], offline_safe: false, source: { origin: "standard", ref: "cadence-bus-heartbeat" }, budget_cents_per_period: null },
|
|
192
|
+
];
|
|
193
|
+
const reg = toRegistry(obs);
|
|
194
|
+
// Without these three the consumer cannot tell a funded objective's cadence
|
|
195
|
+
// from the seat's own heartbeat, so a budget breach would suspend either
|
|
196
|
+
// everything or nothing.
|
|
197
|
+
assert.equal(reg["measure-gm"].scope, "mandate");
|
|
198
|
+
assert.equal(reg["measure-gm"].objectiveId, "obj_7Kx");
|
|
199
|
+
assert.equal(reg["measure-gm"].offlineSafe, true);
|
|
200
|
+
assert.equal(reg["cadence-bus-heartbeat"].scope, "standard");
|
|
201
|
+
assert.equal(reg["cadence-bus-heartbeat"].objectiveId, null);
|
|
202
|
+
assert.equal(reg["cadence-bus-heartbeat"].offlineSafe, false);
|
|
203
|
+
});
|
|
204
|
+
|
|
187
205
|
// ---------------------------------------------------------------------------
|
|
188
206
|
// The no-op guard (SPEC §6.4: "if obligationsHash is unchanged, nothing on disk
|
|
189
207
|
// moves"). config/plan.yaml is TRACKED, so a recompile that changes nothing must
|
|
@@ -29,9 +29,13 @@
|
|
|
29
29
|
* liveRSS + 750MB > 0.60*totalmem → QUEUE
|
|
30
30
|
* else → ADMIT
|
|
31
31
|
*
|
|
32
|
-
* Plus
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Plus the budget ladder (`mode`, from `lib/budget-guard.dailyStatus().mode`),
|
|
33
|
+
* banded against the seat's hq-funded envelope rather than a local config cap:
|
|
34
|
+
* degraded (≥100%) source==="backlog" → DEFER (D3 stops)
|
|
35
|
+
* suspended (≥125%) source==="backlog"|"cadence" → DEFER
|
|
36
|
+
* refused (>150%) anything that is not a direct human reply → DEFER
|
|
37
|
+
* Inbox always goes through the normal ADMIT/QUEUE path — never go dark on the
|
|
38
|
+
* user — and no band ever hard-refuses: DEFER leaves the work on disk.
|
|
35
39
|
*
|
|
36
40
|
* Purity: `admit` is a pure function of its injected `deps` snapshot. The
|
|
37
41
|
* default `deps` reads real os.* + a 2s-cached `liveClaudeStats()` (via ps) +
|
|
@@ -256,13 +260,33 @@ const ADMIT = "ADMIT";
|
|
|
256
260
|
const QUEUE = "QUEUE";
|
|
257
261
|
const DEFER = "DEFER";
|
|
258
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Normalise a budget mode onto the governance ladder.
|
|
265
|
+
*
|
|
266
|
+
* `lib/budget-guard.dailyStatus().mode` is the authority; the legacy
|
|
267
|
+
* `"essential-only"` string (and the `essentialOnly:true` flag) map onto
|
|
268
|
+
* `"suspended"`, which is exactly what that mode always did — defer non-inbox
|
|
269
|
+
* work, keep answering the human. Anything unrecognised is `"normal"`: an
|
|
270
|
+
* unknown mode must not silently become a stricter one.
|
|
271
|
+
*/
|
|
272
|
+
function budgetModeOf(req) {
|
|
273
|
+
const raw = String(req.mode || (req.essentialOnly ? "essential-only" : "") || "").toLowerCase();
|
|
274
|
+
if (raw === "refused") return "refused";
|
|
275
|
+
if (raw === "suspended" || raw === "essential-only") return "suspended";
|
|
276
|
+
if (raw === "degraded") return "degraded";
|
|
277
|
+
return "normal";
|
|
278
|
+
}
|
|
279
|
+
|
|
259
280
|
/**
|
|
260
281
|
* The admission decision.
|
|
261
282
|
*
|
|
262
|
-
* @param {object} req { source, priority, mode }
|
|
263
|
-
* source:
|
|
264
|
-
* priority:
|
|
265
|
-
* mode:
|
|
283
|
+
* @param {object} req { source, priority, mode, humanReply }
|
|
284
|
+
* source: "inbox" | "backlog" | "cadence" (default "backlog")
|
|
285
|
+
* priority: "critical"|"high"|"normal"|"low" (default "normal")
|
|
286
|
+
* mode: budget posture — "normal" | "degraded" | "suspended" | "refused"
|
|
287
|
+
* (legacy "essential-only" ≡ "suspended")
|
|
288
|
+
* humanReply: this spawn is a direct reply to a human. Only `source:"inbox"`
|
|
289
|
+
* work qualifies by default; pass it explicitly to be certain.
|
|
266
290
|
* @param {object} [deps] injected snapshot; defaults to defaultDeps()
|
|
267
291
|
* { freemem, totalmem, loadavg, cpus, liveClaude:{count,rssMB}, throttleCeiling }
|
|
268
292
|
* @returns {{ decision:"ADMIT"|"QUEUE"|"DEFER", reason:string, snapshot:object }}
|
|
@@ -270,7 +294,11 @@ const DEFER = "DEFER";
|
|
|
270
294
|
export function admit(req = {}, deps) {
|
|
271
295
|
const d = deps || defaultDeps();
|
|
272
296
|
const source = req.source || "backlog";
|
|
273
|
-
const
|
|
297
|
+
const budgetMode = budgetModeOf(req);
|
|
298
|
+
const mode = budgetMode === "normal" ? null : budgetMode;
|
|
299
|
+
// A direct human reply is the ONE thing no budget band may stop. Invariant #1:
|
|
300
|
+
// we degrade, we never go dark on the user.
|
|
301
|
+
const humanReply = req.humanReply === true || source === "inbox";
|
|
274
302
|
|
|
275
303
|
const totalmem = numField(d.totalmem, os.totalmem());
|
|
276
304
|
const freemem = numField(d.freemem, os.freemem());
|
|
@@ -300,10 +328,28 @@ export function admit(req = {}, deps) {
|
|
|
300
328
|
|
|
301
329
|
const decide = (decision, reason) => ({ decision, reason, snapshot });
|
|
302
330
|
|
|
303
|
-
// (0)
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
331
|
+
// (0) THE BUDGET LADDER. Three rungs, and none of them is a seat-level refuse:
|
|
332
|
+
//
|
|
333
|
+
// refused (>150% of the funded envelope) — DEFER everything that is not a
|
|
334
|
+
// direct human reply. This is the "REFUSE, at spawn level only"
|
|
335
|
+
// rung: the work stays on disk and the sweep retries it, the seat
|
|
336
|
+
// keeps talking to its human. A hard refuse here would brick the
|
|
337
|
+
// seat, which Invariant #1 forbids.
|
|
338
|
+
// suspended (≥125%) — DEFER backlog + cadence work; inbox falls through.
|
|
339
|
+
// (This is what the legacy "essential-only" mode always did.)
|
|
340
|
+
// degraded (≥100%) — self-directed BACKLOG work stops, but obligations
|
|
341
|
+
// (cadences) keep running on the cheapest model class. The rung
|
|
342
|
+
// that used to be missing entirely: enforcement was binary.
|
|
343
|
+
//
|
|
344
|
+
// Inbox work always falls through to the normal pressure gating below.
|
|
345
|
+
if (mode === "refused" && !humanReply) {
|
|
346
|
+
return decide(DEFER, `budget refused (>150% of the seat envelope): deferring ${source} work; direct human replies continue`);
|
|
347
|
+
}
|
|
348
|
+
if (mode === "suspended" && (source === "backlog" || source === "cadence")) {
|
|
349
|
+
return decide(DEFER, "budget suspended (>=125% of the seat envelope): deferring non-inbox work");
|
|
350
|
+
}
|
|
351
|
+
if (mode === "degraded" && source === "backlog") {
|
|
352
|
+
return decide(DEFER, "budget degraded (>=100% of the seat envelope): self-directed backlog work stopped; obligations + inbox continue");
|
|
307
353
|
}
|
|
308
354
|
|
|
309
355
|
// (1) Concurrency ceiling → QUEUE (work re-derivable from the queue).
|
|
@@ -172,9 +172,11 @@ test("admit: load exactly at the threshold is still ADMIT (strict >)", () => {
|
|
|
172
172
|
// ---------------------------------------------------------------------------
|
|
173
173
|
|
|
174
174
|
test("essential-only: backlog → DEFER without burning resources", () => {
|
|
175
|
+
// The legacy mode name still maps onto the SUSPENDED rung — same behaviour it
|
|
176
|
+
// always had, so no caller loses its gate during the rollout.
|
|
175
177
|
const r = admit({ source: "backlog", mode: "essential-only" }, deps());
|
|
176
178
|
assert.equal(r.decision, DECISIONS.DEFER);
|
|
177
|
-
assert.match(r.reason, /
|
|
179
|
+
assert.match(r.reason, /budget suspended/);
|
|
178
180
|
});
|
|
179
181
|
|
|
180
182
|
test("essential-only: cadence → DEFER", () => {
|
|
@@ -280,3 +282,41 @@ test("admit reads a real throttle.json from agentRoot and clamps", async () => {
|
|
|
280
282
|
assert.equal(r.decision, DECISIONS.QUEUE);
|
|
281
283
|
} finally { await fsp.rm(dir, { recursive: true, force: true }); }
|
|
282
284
|
});
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// the budget ladder (banded against the seat's hq-funded envelope)
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
test("degraded (>=100%): self-directed backlog stops, obligations + inbox continue", () => {
|
|
291
|
+
assert.equal(admit({ source: "backlog", mode: "degraded" }, deps()).decision, "DEFER");
|
|
292
|
+
assert.match(admit({ source: "backlog", mode: "degraded" }, deps()).reason, /self-directed backlog work stopped/);
|
|
293
|
+
assert.equal(admit({ source: "cadence", mode: "degraded" }, deps()).decision, "ADMIT",
|
|
294
|
+
"an obligation is not discretionary — it degrades, it does not stop");
|
|
295
|
+
assert.equal(admit({ source: "inbox", mode: "degraded" }, deps()).decision, "ADMIT");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("suspended (>=125%): backlog + cadence DEFER, inbox continues", () => {
|
|
299
|
+
assert.equal(admit({ source: "backlog", mode: "suspended" }, deps()).decision, "DEFER");
|
|
300
|
+
assert.equal(admit({ source: "cadence", mode: "suspended" }, deps()).decision, "DEFER");
|
|
301
|
+
assert.equal(admit({ source: "inbox", mode: "suspended" }, deps()).decision, "ADMIT");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("refused (>150%): everything that is not a human reply DEFERs — and it DEFERs, never refuses", () => {
|
|
305
|
+
for (const source of ["backlog", "cadence", "sweep"]) {
|
|
306
|
+
const r = admit({ source, mode: "refused" }, deps());
|
|
307
|
+
assert.equal(r.decision, "DEFER", `${source} must defer`);
|
|
308
|
+
assert.match(r.reason, /direct human replies continue/);
|
|
309
|
+
}
|
|
310
|
+
// The one thing no band may stop.
|
|
311
|
+
assert.equal(admit({ source: "inbox", mode: "refused" }, deps()).decision, "ADMIT");
|
|
312
|
+
assert.equal(admit({ source: "cadence", mode: "refused", humanReply: true }, deps()).decision, "ADMIT",
|
|
313
|
+
"an explicit human reply is admitted whatever its source");
|
|
314
|
+
// DEFER leaves the work on disk for the sweep; nothing is dropped, and the
|
|
315
|
+
// seat is never bricked (Invariant #1).
|
|
316
|
+
assert.notEqual(admit({ source: "backlog", mode: "refused" }, deps()).decision, "REFUSE");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("an unrecognised budget mode is treated as normal, never as something stricter", () => {
|
|
320
|
+
assert.equal(admit({ source: "backlog", mode: "catastrophic" }, deps()).decision, "ADMIT");
|
|
321
|
+
assert.equal(admit({ source: "backlog", mode: "normal" }, deps()).decision, "ADMIT");
|
|
322
|
+
});
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import * as nodeFs from "node:fs";
|
|
37
37
|
import { join } from "node:path";
|
|
38
38
|
import { createHash } from "node:crypto";
|
|
39
|
+
import { resolveBackupPlan } from "../backup/policy.mjs";
|
|
39
40
|
|
|
40
41
|
// Severity ordering — used to decide which unsuppressed failures fail the run
|
|
41
42
|
// and to sort the summary. `info` never fails a run on its own.
|
|
@@ -316,27 +317,63 @@ export const CHECKS = [
|
|
|
316
317
|
{
|
|
317
318
|
id: "backup-configured",
|
|
318
319
|
severity: "medium",
|
|
319
|
-
title: "
|
|
320
|
+
title: "Disaster-recovery backup is configured and enabled",
|
|
321
|
+
// ASKS THE DR CODE, rather than re-deriving "configured" from regexes.
|
|
322
|
+
//
|
|
323
|
+
// The regex form false-greened on two configs the DR code itself calls
|
|
324
|
+
// broken, because both patterns were anchored with `^\s*` under /m:
|
|
325
|
+
// - `bucket: ""` — the value THIS PROJECT SHIPS in its own default config
|
|
326
|
+
// — satisfied /^\s*bucket:\s*\S/m, because `\S` matches the quote
|
|
327
|
+
// character. The audit reported "off-machine tier set" while
|
|
328
|
+
// resolveBackupPlan reported offsiteConfigured:false and the runner
|
|
329
|
+
// logged "offsite is NOT configured".
|
|
330
|
+
// - top-level `enabled: false` with a nested `local.enabled: true` passed
|
|
331
|
+
// /^\s*enabled:\s*true/m on the INDENTED key, so the audit was green on
|
|
332
|
+
// a machine where backup-run.mjs refuses to run and doctor FAILs.
|
|
333
|
+
// Three subsystems, three answers, one file. lib/backup/policy.mjs is the
|
|
334
|
+
// single source of truth by design; this check now uses it. It costs one
|
|
335
|
+
// js-yaml import, which the module already ships as a dependency.
|
|
320
336
|
run(agentRoot, deps) {
|
|
321
337
|
const { fs } = resolveDeps(deps);
|
|
322
|
-
const
|
|
323
|
-
if (!
|
|
338
|
+
const plan = resolveBackupPlan({ agentRoot, fs });
|
|
339
|
+
if (!plan.present) {
|
|
324
340
|
return {
|
|
325
341
|
ok: false,
|
|
326
342
|
detail: "no .maestro/backup-config.yaml — a dead Mac mini loses all audit/cost history (G6/G16)",
|
|
327
|
-
fix: "
|
|
343
|
+
fix: "maestro init backup-replication --apply (writes the default: nightly local restore points, no credentials needed)",
|
|
328
344
|
fixable: false,
|
|
329
345
|
};
|
|
330
346
|
}
|
|
331
|
-
if (
|
|
347
|
+
if (!plan.readable) {
|
|
332
348
|
return {
|
|
333
349
|
ok: false,
|
|
334
|
-
detail: "backup
|
|
335
|
-
fix: "
|
|
350
|
+
detail: ".maestro/backup-config.yaml is present but unparseable — the nightly backup cannot run",
|
|
351
|
+
fix: "fix the YAML, or delete it and re-run: maestro init backup-replication --apply",
|
|
336
352
|
fixable: false,
|
|
337
353
|
};
|
|
338
354
|
}
|
|
339
|
-
|
|
355
|
+
if (!plan.enabled) {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
detail: "backup config present but enabled is not true — backup-run.mjs REFUSES to run",
|
|
359
|
+
fix: "set enabled: true in .maestro/backup-config.yaml",
|
|
360
|
+
fixable: false,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (!plan.include.length) {
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
detail: `backup enabled but every include path was rejected (${plan.violations.map((v) => `${v.path} → ${v.pattern}`).join(", ") || "empty include list"}) — the archive would be empty`,
|
|
367
|
+
fix: "restore the include list (state, knowledge, memory, outputs, config, .maestro)",
|
|
368
|
+
fixable: false,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
ok: true,
|
|
373
|
+
detail: plan.tier === "offsite"
|
|
374
|
+
? `DR backup configured, off-machine tier set (${plan.offsite.provider}://${plan.offsite.bucket})`
|
|
375
|
+
: "DR backup configured — LOCAL restore points only; no off-machine copy (set offsite.provider + offsite.bucket)",
|
|
376
|
+
};
|
|
340
377
|
},
|
|
341
378
|
},
|
|
342
379
|
|
|
@@ -211,6 +211,41 @@ test("runAudit: backup not configured is a failure", async () => {
|
|
|
211
211
|
} finally { cleanup(root); }
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
test("runAudit: backup — an EMPTY bucket is not an off-machine tier", async () => {
|
|
215
|
+
// The regex form matched /^\s*bucket:\s*\S/m against `bucket: ""` — `\S` hits
|
|
216
|
+
// the quote character — and that is the value THIS PROJECT SHIPS in its own
|
|
217
|
+
// default config. The audit said "off-machine tier set" while
|
|
218
|
+
// resolveBackupPlan said offsiteConfigured:false and the runner logged
|
|
219
|
+
// "offsite is NOT configured": three subsystems, three answers, one file.
|
|
220
|
+
const root = makeGoodAgent();
|
|
221
|
+
try {
|
|
222
|
+
writeFileSync(
|
|
223
|
+
join(root, ".maestro/backup-config.yaml"),
|
|
224
|
+
"enabled: true\nprefix: acme\nlocal:\n enabled: true\noffsite:\n provider: gcs\n bucket: \"\"\n"
|
|
225
|
+
);
|
|
226
|
+
const r = await runAudit(root, { env: { MAESTRO_SCOPED_PERMISSIONS: "1" } });
|
|
227
|
+
const b = r.results.find((x) => x.id === "backup-configured");
|
|
228
|
+
assert.equal(b.ok, true, "local restore points are a real posture");
|
|
229
|
+
assert.match(b.detail, /LOCAL restore points only/, "but it must not claim off-machine");
|
|
230
|
+
} finally { cleanup(root); }
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("runAudit: backup — top-level `enabled: false` is a FAILURE even with a nested local.enabled: true", async () => {
|
|
234
|
+
// `^\s*` under /m matched the INDENTED key, so the audit passed on a machine
|
|
235
|
+
// where backup-run.mjs refuses to run and doctor FAILs.
|
|
236
|
+
const root = makeGoodAgent();
|
|
237
|
+
try {
|
|
238
|
+
writeFileSync(
|
|
239
|
+
join(root, ".maestro/backup-config.yaml"),
|
|
240
|
+
"enabled: false\nprefix: acme\nlocal:\n enabled: true\n"
|
|
241
|
+
);
|
|
242
|
+
const r = await runAudit(root, { env: { MAESTRO_SCOPED_PERMISSIONS: "1" } });
|
|
243
|
+
const b = r.results.find((x) => x.id === "backup-configured");
|
|
244
|
+
assert.equal(b.ok, false);
|
|
245
|
+
assert.match(b.detail, /REFUSES to run/);
|
|
246
|
+
} finally { cleanup(root); }
|
|
247
|
+
});
|
|
248
|
+
|
|
214
249
|
test("runAudit: a check that throws becomes an ok:false posture row, not a crash", async () => {
|
|
215
250
|
const root = makeGoodAgent();
|
|
216
251
|
try {
|