@cohortapp/agent-sdk 2.5.1 → 2.6.1
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 +305 -89
- package/bin/maestro.test.mjs +357 -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/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/classifier.test.mjs +18 -9
- 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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# record-receipt.sh — witness a successful outbound send for answer-assurance.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS EXISTS
|
|
5
|
+
#
|
|
6
|
+
# A spawned session is a separate `claude` process, and it is REQUIRED to reply
|
|
7
|
+
# through these CLI lanes (prompt-builder mandates them; the org MCP send tools
|
|
8
|
+
# are hard-blocked by a pre-tool hook). So when a session answers a human, the
|
|
9
|
+
# daemon that spawned it learns nothing — it sees an exit code and nothing else.
|
|
10
|
+
# That is why exit 0 was treated as proof of a reply, and why a 241-second
|
|
11
|
+
# session whose own final text was "Nothing was sent." was marked handled
|
|
12
|
+
# forever.
|
|
13
|
+
#
|
|
14
|
+
# A receipt is the evidence that crosses the process boundary. Without one on
|
|
15
|
+
# every lane the daemon's assurance goes the other way and apologises — "I
|
|
16
|
+
# didn't get a reply out to you" — to people who were answered fine.
|
|
17
|
+
#
|
|
18
|
+
# Attribution (which obligation, which session) rides in on the env the
|
|
19
|
+
# dispatcher stamps on the child: MAESTRO_OBLIGATION_KEY / MAESTRO_SESSION_ID.
|
|
20
|
+
#
|
|
21
|
+
# Usage: record-receipt.sh <service> <channel> <via> [chars]
|
|
22
|
+
# Always exits 0. Bookkeeping must never break a send.
|
|
23
|
+
set +e
|
|
24
|
+
|
|
25
|
+
SERVICE="$1"
|
|
26
|
+
CHANNEL="$2"
|
|
27
|
+
VIA="${3:-cli}"
|
|
28
|
+
CHARS="${4:-0}"
|
|
29
|
+
|
|
30
|
+
[ -n "$SERVICE" ] && [ -n "$CHANNEL" ] || exit 0
|
|
31
|
+
|
|
32
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
33
|
+
|
|
34
|
+
# Two different roots, deliberately kept apart.
|
|
35
|
+
# CODE_DIR — where this checkout's lib/ lives. Always derivable from $0, so
|
|
36
|
+
# resolution never depends on the caller's env being right.
|
|
37
|
+
# AGENT_REPO_DIR — where STATE is written. That is the agent's root, and the
|
|
38
|
+
# receipt must land in the same tree the daemon sweeps.
|
|
39
|
+
# Collapsing the two (the obvious "just use AGENT_DIR for both") silently writes
|
|
40
|
+
# no receipt whenever an agent's root is not also a maestro checkout — and a
|
|
41
|
+
# receipt that silently does not happen is the exact failure this file prevents.
|
|
42
|
+
CODE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
43
|
+
AGENT_REPO_DIR="${AGENT_DIR:-$CODE_DIR}"
|
|
44
|
+
|
|
45
|
+
RECEIPTS=""
|
|
46
|
+
for CAND in \
|
|
47
|
+
"$CODE_DIR/lib/comms/receipts.mjs" \
|
|
48
|
+
"$AGENT_REPO_DIR/lib/comms/receipts.mjs" \
|
|
49
|
+
"$AGENT_REPO_DIR/node_modules/@cohortapp/agent-sdk/lib/comms/receipts.mjs"
|
|
50
|
+
do
|
|
51
|
+
if [ -f "$CAND" ]; then RECEIPTS="$CAND"; break; fi
|
|
52
|
+
done
|
|
53
|
+
[ -n "$RECEIPTS" ] || exit 0
|
|
54
|
+
|
|
55
|
+
NODE_BIN="${MAESTRO_NODE_BIN:-/opt/homebrew/bin/node}"
|
|
56
|
+
command -v "$NODE_BIN" >/dev/null 2>&1 || NODE_BIN="node"
|
|
57
|
+
command -v "$NODE_BIN" >/dev/null 2>&1 || exit 0
|
|
58
|
+
|
|
59
|
+
AGENT_DIR="$AGENT_REPO_DIR" "$NODE_BIN" "$RECEIPTS" record \
|
|
60
|
+
--service "$SERVICE" --channel "$CHANNEL" --kind session --via "$VIA" \
|
|
61
|
+
--chars "$CHARS" --agent-root "$AGENT_REPO_DIR" >/dev/null 2>&1
|
|
62
|
+
|
|
63
|
+
exit 0
|
|
@@ -177,11 +177,17 @@ if [ "$DRY_RUN" -eq 0 ] && [ -d "$UNPACK" ]; then
|
|
|
177
177
|
fi
|
|
178
178
|
|
|
179
179
|
# ---------------------------------------------------------------------------
|
|
180
|
-
# 3. Restore
|
|
180
|
+
# 3. Restore the durable trees — idempotent (back up existing first)
|
|
181
|
+
#
|
|
182
|
+
# This list MIRRORS lib/backup/policy.mjs DEFAULT_INCLUDE. It used to be just
|
|
183
|
+
# config/state/logs, which meant the backup archived more than the restore put
|
|
184
|
+
# back: knowledge/, memory/, outputs/ and the .maestro/ markers rode along in
|
|
185
|
+
# every tarball and were silently left in the scratch dir on the way home.
|
|
186
|
+
# Absent trees are skipped, so a legacy archive still restores exactly as before.
|
|
181
187
|
# ---------------------------------------------------------------------------
|
|
182
188
|
|
|
183
|
-
step "Step 3/5: restore
|
|
184
|
-
for dir in config state logs; do
|
|
189
|
+
step "Step 3/5: restore the durable trees into $AGENT_DIR"
|
|
190
|
+
for dir in config state knowledge memory outputs logs .maestro; do
|
|
185
191
|
SRC="$UNPACK/$dir"
|
|
186
192
|
DEST="$AGENT_DIR/$dir"
|
|
187
193
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
@@ -204,6 +210,11 @@ for dir in config state logs; do
|
|
|
204
210
|
done
|
|
205
211
|
|
|
206
212
|
# Lock down secret-bearing files if they came back in the archive.
|
|
213
|
+
#
|
|
214
|
+
# Archives produced since 2026-08 CANNOT contain .env — lib/backup/policy.mjs
|
|
215
|
+
# denies it in code and the runner verifies the finished tarball. This branch
|
|
216
|
+
# stays for legacy archives (and for a .env the operator restored by hand),
|
|
217
|
+
# because an over-permissive .env is worth catching whatever put it there.
|
|
207
218
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
208
219
|
note "DRY-RUN: would chmod 600 $AGENT_DIR/.env if present"
|
|
209
220
|
elif [ -f "$AGENT_DIR/.env" ]; then
|
|
@@ -71,14 +71,17 @@ test("--dry-run lists every restore step and the manual follow-ups", () => {
|
|
|
71
71
|
// The five numbered steps are all present.
|
|
72
72
|
assert.match(out, /Step 1\/5: validate \+ decrypt/);
|
|
73
73
|
assert.match(out, /Step 2\/5: unpack/);
|
|
74
|
-
assert.match(out, /Step 3\/5: restore
|
|
74
|
+
assert.match(out, /Step 3\/5: restore the durable trees/);
|
|
75
75
|
assert.match(out, /Step 4\/5: re-establish org-server \(Cohort\) identity/);
|
|
76
76
|
assert.match(out, /Step 5\/5: remaining MANUAL steps/);
|
|
77
77
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// Every tree the DEFAULT backup include set archives must also be a tree
|
|
79
|
+
// the restore puts back — otherwise the archive carries data the recovery
|
|
80
|
+
// silently leaves in the scratch dir. (lib/backup/policy.mjs
|
|
81
|
+
// DEFAULT_INCLUDE, plus logs/ which an operator may add explicitly.)
|
|
82
|
+
for (const dir of ["config", "state", "knowledge", "memory", "outputs", "logs", "\\.maestro"]) {
|
|
83
|
+
assert.match(out, new RegExp(`would restore ${dir}/`), `restore plan omits ${dir}/`);
|
|
84
|
+
}
|
|
82
85
|
|
|
83
86
|
// The irreducibly-manual follow-ups: secret recovery + cohort re-pair.
|
|
84
87
|
assert.match(out, /maestro secrets sync/);
|
|
@@ -29,6 +29,45 @@ from email.mime.base import MIMEBase
|
|
|
29
29
|
from email import encoders
|
|
30
30
|
from email.utils import formatdate
|
|
31
31
|
|
|
32
|
+
|
|
33
|
+
def _record_receipt(service, channel, via, chars=0, kind="session"):
|
|
34
|
+
"""Witness a successful send for the daemon's answer-assurance ledger.
|
|
35
|
+
|
|
36
|
+
Shells out to lib/comms/receipts.mjs because that module owns the on-disk
|
|
37
|
+
format and this script cannot import it. Entirely best-effort and silent on
|
|
38
|
+
every failure: a send must never be broken by its own bookkeeping.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
import subprocess
|
|
42
|
+
# Two roots, kept apart: code_dir is where lib/ lives (always derivable
|
|
43
|
+
# from __file__), agent_dir is where state is written. Collapsing them
|
|
44
|
+
# writes no receipt whenever an agent root is not also a checkout.
|
|
45
|
+
code_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
46
|
+
agent_dir = os.environ.get("AGENT_DIR") or code_dir
|
|
47
|
+
mod = None
|
|
48
|
+
for cand in (os.path.join(code_dir, "lib", "comms", "receipts.mjs"),
|
|
49
|
+
os.path.join(agent_dir, "lib", "comms", "receipts.mjs"),
|
|
50
|
+
os.path.join(agent_dir, "node_modules", "@cohortapp",
|
|
51
|
+
"agent-sdk", "lib", "comms", "receipts.mjs")):
|
|
52
|
+
if os.path.exists(cand):
|
|
53
|
+
mod = cand
|
|
54
|
+
break
|
|
55
|
+
if not mod or not channel:
|
|
56
|
+
return
|
|
57
|
+
node = os.environ.get("MAESTRO_NODE_BIN") or "/opt/homebrew/bin/node"
|
|
58
|
+
if not os.path.exists(node):
|
|
59
|
+
node = "node"
|
|
60
|
+
subprocess.run(
|
|
61
|
+
[node, mod, "record", "--service", service, "--channel", str(channel),
|
|
62
|
+
"--kind", kind, "--via", via, "--chars", str(chars),
|
|
63
|
+
"--agent-root", agent_dir],
|
|
64
|
+
timeout=10, check=False,
|
|
65
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
66
|
+
env={**os.environ, "AGENT_DIR": agent_dir})
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
32
71
|
# LLM-based dedup (Layer 1) — replaces old subject+recipient lock per CEO directive ib-20260406-001
|
|
33
72
|
try:
|
|
34
73
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
@@ -369,6 +408,14 @@ def send_email(to, subject, body, reply_subject=None, attachments=None, cc=None,
|
|
|
369
408
|
except Exception:
|
|
370
409
|
pass
|
|
371
410
|
|
|
411
|
+
# Post-send: delivery receipt. This script is the mandated Gmail reply lane
|
|
412
|
+
# for a spawned session, and a session is a separate process — unless the
|
|
413
|
+
# send is witnessed here the daemon cannot tell an answered ask from a
|
|
414
|
+
# session that exited 0 in silence, and it apologises to people who were
|
|
415
|
+
# already answered. Attribution rides in on the dispatcher's env.
|
|
416
|
+
_record_receipt(service="gmail", channel=to, via="send-email-threaded.py",
|
|
417
|
+
chars=len(body or ""))
|
|
418
|
+
|
|
372
419
|
if __name__ == '__main__':
|
|
373
420
|
if len(sys.argv) < 4 and '--help' not in sys.argv and '-h' not in sys.argv:
|
|
374
421
|
print("Usage: send-email-threaded.py <to> <subject> <body> [--reply-to-subject 'original subject'] [--attachment file]")
|
package/scripts/send-sms.sh
CHANGED
|
@@ -162,6 +162,10 @@ if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
|
|
|
162
162
|
PREVIEW=$(printf '%s' "$BODY" | head -c 200)
|
|
163
163
|
"$SCRIPT_DIR/outbound-dedup.sh" confirm sms "$DEDUP_KEY" "To:$TO SID:$MESSAGE_SID $PREVIEW" 2>/dev/null || true
|
|
164
164
|
fi
|
|
165
|
+
# Witness the send for answer-assurance (see scripts/record-receipt.sh).
|
|
166
|
+
if [ -x "$SCRIPT_DIR/record-receipt.sh" ]; then
|
|
167
|
+
AGENT_DIR="$AGENT_REPO_DIR" "$SCRIPT_DIR/record-receipt.sh" sms "$TO" send-sms.sh "${#BODY}" || true
|
|
168
|
+
fi
|
|
165
169
|
echo "SMS sent successfully"
|
|
166
170
|
echo " To: $TO"
|
|
167
171
|
echo " From: $FROM_NUMBER"
|
package/scripts/send-whatsapp.sh
CHANGED
|
@@ -268,6 +268,10 @@ if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
|
|
|
268
268
|
PREVIEW=$(printf '%s' "${BODY:-template:$TEMPLATE}" | head -c 200)
|
|
269
269
|
"$SCRIPT_DIR/outbound-dedup.sh" confirm whatsapp "$DEDUP_KEY" "To:$TO SID:$MESSAGE_SID $PREVIEW" 2>/dev/null || true
|
|
270
270
|
fi
|
|
271
|
+
# Witness the send for answer-assurance (see scripts/record-receipt.sh).
|
|
272
|
+
if [ -x "$SCRIPT_DIR/record-receipt.sh" ]; then
|
|
273
|
+
AGENT_DIR="$AGENT_REPO_DIR" "$SCRIPT_DIR/record-receipt.sh" whatsapp "$TO" send-whatsapp.sh "${#BODY}" || true
|
|
274
|
+
fi
|
|
271
275
|
echo "WhatsApp message sent successfully (${MODE} mode)"
|
|
272
276
|
echo " To: $TO"
|
|
273
277
|
echo " From: $FROM_NUMBER (whatsapp:${FROM_NUMBER})"
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* init-backup.mjs —
|
|
3
|
+
* init-backup.mjs — give this agent a DR posture, non-interactively.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* DECISION (delegated authority, 2026-08-12): this step is now AUTO-RUNNABLE
|
|
6
|
+
* and writes an ENABLED config. It used to be an interactive wizard that wrote
|
|
7
|
+
* `enabled: false` and demanded a bucket + cloud credentials, which meant every
|
|
8
|
+
* agent was born failing `maestro doctor` on a check nobody could clear on the
|
|
9
|
+
* spot — so the check became noise and the fleet's real DR posture stayed at
|
|
10
|
+
* zero. The default it writes now needs no credentials, no bucket and no
|
|
11
|
+
* network: nightly LOCAL restore points in
|
|
12
|
+
* ~/Library/Application Support/Maestro/backups/<prefix>/, outside the agent
|
|
13
|
+
* repo. The off-machine tier stays opt-in and doctor WARNs (not FAILs) until
|
|
14
|
+
* it is set — see lib/backup/policy.mjs for the full reasoning and for the
|
|
15
|
+
* hard deny-list that keeps .env and .cohort-key.json out of every archive.
|
|
16
|
+
*
|
|
17
|
+
* Idempotent: an existing config is left exactly as the operator left it.
|
|
8
18
|
*/
|
|
9
|
-
import { existsSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
-
import { join, resolve, dirname } from "node:path";
|
|
19
|
+
import { existsSync, writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
20
|
+
import { join, resolve, dirname, basename } from "node:path";
|
|
11
21
|
import { fileURLToPath } from "node:url";
|
|
12
22
|
|
|
13
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -16,39 +26,84 @@ const AGENT_DIR = process.env.AGENT_ROOT || process.env.AGENT_DIR || resolve(__d
|
|
|
16
26
|
const ok = (m) => process.stdout.write(`[init-backup] ✓ ${m}\n`);
|
|
17
27
|
const warn = (m) => process.stdout.write(`[init-backup] ⚠ ${m}\n`);
|
|
18
28
|
|
|
29
|
+
/** Same resolution ladder as the runner: agent lib → installed SDK → checkout. */
|
|
30
|
+
async function loadPolicy() {
|
|
31
|
+
const candidates = [
|
|
32
|
+
join(AGENT_DIR, "lib", "backup", "policy.mjs"),
|
|
33
|
+
join(AGENT_DIR, "node_modules", "@cohortapp", "agent-sdk", "lib", "backup", "policy.mjs"),
|
|
34
|
+
join(process.env.MAESTRO_ROOT || resolve(__dirname, "..", ".."), "lib", "backup", "policy.mjs"),
|
|
35
|
+
resolve(__dirname, "..", "..", "lib", "backup", "policy.mjs"),
|
|
36
|
+
];
|
|
37
|
+
for (const c of candidates) if (existsSync(c)) return import(`file://${c}`);
|
|
38
|
+
throw new Error("lib/backup/policy.mjs not found");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Archive prefix: the repoSlug from config/agent.json, else the directory name. */
|
|
42
|
+
function derivePrefix() {
|
|
43
|
+
try {
|
|
44
|
+
const p = join(AGENT_DIR, "config", "agent.json");
|
|
45
|
+
if (existsSync(p)) {
|
|
46
|
+
const doc = JSON.parse(readFileSync(p, "utf-8"));
|
|
47
|
+
const candidate = doc.repoSlug || doc.agentId || doc.firstName;
|
|
48
|
+
if (candidate) return String(candidate);
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
/* fall through to the directory name */
|
|
52
|
+
}
|
|
53
|
+
return basename(AGENT_DIR);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const policy = await loadPolicy();
|
|
57
|
+
|
|
19
58
|
mkdirSync(join(AGENT_DIR, ".maestro"), { recursive: true });
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
59
|
+
const cfgPath = join(AGENT_DIR, ".maestro/backup-config.yaml");
|
|
60
|
+
|
|
61
|
+
if (existsSync(cfgPath)) {
|
|
62
|
+
const plan = policy.resolveBackupPlan({ agentRoot: AGENT_DIR });
|
|
63
|
+
if (!plan.readable) {
|
|
64
|
+
warn(`${cfgPath} exists but is unparseable YAML — leaving it alone. Fix it or delete it and re-run.`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
ok(`backup-config.yaml already present (tier: ${plan.tier})`);
|
|
68
|
+
if (plan.tier === "local") {
|
|
69
|
+
warn("Local restore points only — nothing survives losing this machine. Set offsite.provider + offsite.bucket to close that gap.");
|
|
70
|
+
}
|
|
23
71
|
process.exit(0);
|
|
24
72
|
}
|
|
25
73
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
enabled
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
const prefix = policy.sanitisePrefix(derivePrefix());
|
|
75
|
+
writeFileSync(cfgPath, policy.defaultConfigYaml({ prefix }));
|
|
76
|
+
ok(`wrote ${cfgPath} (prefix: ${prefix})`);
|
|
77
|
+
|
|
78
|
+
const plan = policy.resolveBackupPlan({ agentRoot: AGENT_DIR });
|
|
79
|
+
ok(`Tier 1 ON — nightly local restore points at ${plan.local.dir} (retention ${plan.local.retentionDays}d)`);
|
|
80
|
+
ok(`Archived: ${plan.include.join(", ")}`);
|
|
81
|
+
ok("NEVER archived: .env, .cohort-key.json, private keys, .claude/.credentials.json, node_modules, .git, state/tmp, RAG index");
|
|
82
|
+
|
|
83
|
+
// Take the FIRST backup right now rather than waiting for 03:10.
|
|
84
|
+
//
|
|
85
|
+
// Without this the agent is still born failing doctor ("enabled but has NEVER
|
|
86
|
+
// completed a run — there is no restore point") for up to 24 hours, which is
|
|
87
|
+
// the exact shape of the problem this whole change exists to remove. Running it
|
|
88
|
+
// here also proves the pipeline end-to-end at setup time, when a human is
|
|
89
|
+
// watching, instead of at 3am when nobody is. Cheap on a fresh agent, and a
|
|
90
|
+
// failure is REPORTED but never fatal — a broken first backup must not take the
|
|
91
|
+
// rest of `maestro init` down with it.
|
|
92
|
+
if (!process.argv.includes("--no-run")) {
|
|
93
|
+
const { spawnSync } = await import("node:child_process");
|
|
94
|
+
const runner = existsSync(join(AGENT_DIR, "scripts/maintenance/backup-run.mjs"))
|
|
95
|
+
? join(AGENT_DIR, "scripts/maintenance/backup-run.mjs")
|
|
96
|
+
: resolve(__dirname, "..", "maintenance", "backup-run.mjs");
|
|
97
|
+
const r = spawnSync(process.execPath, [runner, "--agent-dir", AGENT_DIR], {
|
|
98
|
+
encoding: "utf-8",
|
|
99
|
+
timeout: 10 * 60 * 1000,
|
|
100
|
+
});
|
|
101
|
+
if (r.status === 0) {
|
|
102
|
+
ok("first restore point taken — this agent is recoverable as of right now");
|
|
103
|
+
} else {
|
|
104
|
+
warn(`the first backup did NOT succeed (exit ${r.status ?? "?"}): ${(r.stderr || r.stdout || "").trim().split("\n").slice(-3).join(" / ")}`);
|
|
105
|
+
warn("Run `node scripts/maintenance/backup-run.mjs` and fix it — until it passes, this agent has no restore point.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
warn("Tier 2 (off-machine) is NOT configured — a dead Mac mini still loses everything. Set offsite.provider (gcs|s3|rsync) + offsite.bucket in .maestro/backup-config.yaml.");
|
package/scripts/slack-send.sh
CHANGED
|
@@ -285,3 +285,15 @@ if [ "$STATUS" = "OK" ] && [ -n "$RESPONDING_TO" ] && [ -n "$DEDUP_ACQUIRED" ];
|
|
|
285
285
|
"$SCRIPT_DIR/slack-responded.sh" confirm "$CHANNEL" "$RESPONDING_TO" "$PREVIEW" 2>/dev/null || true
|
|
286
286
|
DEDUP_CONFIRMED="1"
|
|
287
287
|
fi
|
|
288
|
+
|
|
289
|
+
# ── Delivery receipt ────────────────────────────────────────────────
|
|
290
|
+
# This script IS the Slack reply lane for a spawned session (prompt-builder
|
|
291
|
+
# mandates it; the MCP send tools are hard-blocked by a pre-tool hook). A session
|
|
292
|
+
# is a separate process, so unless the send is witnessed here the daemon cannot
|
|
293
|
+
# tell "the session answered" from "the session exited 0 saying nothing" — and
|
|
294
|
+
# its answer-assurance then apologises to a human who was already answered.
|
|
295
|
+
# Attribution (obligation key / session id) rides in on the dispatcher's env.
|
|
296
|
+
# Best-effort and always non-fatal: bookkeeping must never break a send.
|
|
297
|
+
if [ "$STATUS" = "OK" ] && [ -x "$SCRIPT_DIR/record-receipt.sh" ]; then
|
|
298
|
+
AGENT_DIR="$AGENT_REPO_DIR" "$SCRIPT_DIR/record-receipt.sh" slack "$CHANNEL" slack-send.sh "${#MESSAGE}" || true
|
|
299
|
+
fi
|