@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.
Files changed (108) hide show
  1. package/bin/maestro.mjs +185 -88
  2. package/bin/maestro.test.mjs +175 -48
  3. package/docs/runbooks/backup-restore.md +65 -33
  4. package/framework-features.json +4 -4
  5. package/lib/backup/policy.mjs +710 -0
  6. package/lib/backup/policy.test.mjs +305 -0
  7. package/lib/budget-escalate.mjs +133 -0
  8. package/lib/budget-escalate.test.mjs +232 -0
  9. package/lib/budget-guard.envelope.test.mjs +476 -0
  10. package/lib/budget-guard.mjs +853 -75
  11. package/lib/budget-guard.test.mjs +91 -42
  12. package/lib/cadences.mjs +33 -0
  13. package/lib/channels/orgmail/adapter.mjs +88 -3
  14. package/lib/channels/orgmail/adapter.test.mjs +137 -0
  15. package/lib/channels/repeat-suppressor.mjs +198 -0
  16. package/lib/channels/repeat-suppressor.test.mjs +134 -0
  17. package/lib/comms/receipts.mjs +297 -0
  18. package/lib/cost/ledger-row.mjs +333 -0
  19. package/lib/cost/ledger-row.test.mjs +183 -0
  20. package/lib/execution/drive.mjs +28 -1
  21. package/lib/execution/effects.mjs +191 -12
  22. package/lib/execution/effects.test.mjs +50 -11
  23. package/lib/goals/admission.mjs +13 -1
  24. package/lib/goals/admission.test.mjs +26 -1
  25. package/lib/goals/loop.mjs +13 -0
  26. package/lib/kpi-sensors.test.mjs +3 -0
  27. package/lib/mandate/cache.mjs +13 -5
  28. package/lib/mandate/derive.mjs +146 -21
  29. package/lib/mandate/derive.test.mjs +50 -6
  30. package/lib/mandate/model.mjs +32 -4
  31. package/lib/mandate/refresh.test.mjs +16 -2
  32. package/lib/mcp/server.test.mjs +12 -3
  33. package/lib/model-router/economics.mjs +107 -76
  34. package/lib/model-router/economics.test.mjs +64 -46
  35. package/lib/model-router/integration-coverage.test.mjs +39 -37
  36. package/lib/model-router/ledger.mjs +75 -22
  37. package/lib/model-router/ledger.test.mjs +35 -2
  38. package/lib/org/client.mjs +14 -0
  39. package/lib/org/cost-sync.mjs +16 -2
  40. package/lib/org/doctor.mjs +62 -1
  41. package/lib/org/doctor.test.mjs +36 -3
  42. package/lib/org/email-remedy.mjs +49 -0
  43. package/lib/org/engagement-ledger.mjs +376 -0
  44. package/lib/org/engagement-ledger.test.mjs +112 -0
  45. package/lib/org/engagement.mjs +1056 -0
  46. package/lib/org/engagement.test.mjs +739 -0
  47. package/lib/org/inbound/hydrate.mjs +107 -15
  48. package/lib/org/inbound/hydrate.test.mjs +127 -0
  49. package/lib/org/messaging.mjs +230 -3
  50. package/lib/org/messaging.test.mjs +110 -1
  51. package/lib/org/param-contract.mjs +56 -2
  52. package/lib/org/param-contract.test.mjs +26 -0
  53. package/lib/org/protocol.checksum +1 -1
  54. package/lib/org/protocol.mjs +5 -0
  55. package/lib/org/protocol.test.mjs +7 -1
  56. package/lib/org/tool-surface.mjs +506 -10
  57. package/lib/org/tool-surface.test.mjs +191 -7
  58. package/lib/org/ui-parity.mjs +333 -6
  59. package/lib/org/ui-parity.test.mjs +96 -3
  60. package/lib/org/work-ledger.mjs +241 -0
  61. package/lib/org/work-ledger.test.mjs +237 -0
  62. package/lib/plan/adoption-e2e.test.mjs +366 -0
  63. package/lib/plan/budget-enforcement.test.mjs +400 -0
  64. package/lib/plan/budget-runtime.mjs +215 -0
  65. package/lib/plan/compile.mjs +201 -5
  66. package/lib/plan/compile.test.mjs +19 -5
  67. package/lib/plan/emit.mjs +8 -0
  68. package/lib/plan/emit.test.mjs +18 -0
  69. package/lib/resource-governor.mjs +58 -12
  70. package/lib/resource-governor.test.mjs +41 -1
  71. package/lib/security/audit-engine.mjs +45 -8
  72. package/lib/security/audit-engine.test.mjs +35 -0
  73. package/lib/setup/enroll-from-cohort.mjs +14 -1
  74. package/lib/setup/sections/mandate.mjs +48 -7
  75. package/lib/setup/sections/mandate.test.mjs +17 -2
  76. package/lib/setup/sections/orgmail.mjs +10 -2
  77. package/lib/setup/state.mjs +83 -2
  78. package/lib/telemetry/collect.mjs +360 -20
  79. package/lib/telemetry/collect.test.mjs +266 -0
  80. package/package.json +1 -1
  81. package/scripts/cost/track-claude-usage.mjs +207 -48
  82. package/scripts/cost/track-claude-usage.test.mjs +148 -0
  83. package/scripts/daemon/agent-daemon.mjs +315 -17
  84. package/scripts/daemon/assurance-e2e.test.mjs +421 -0
  85. package/scripts/daemon/assurance.mjs +944 -0
  86. package/scripts/daemon/assurance.test.mjs +668 -0
  87. package/scripts/daemon/cadence-consumer-governance.test.mjs +56 -0
  88. package/scripts/daemon/cadence-consumer.mjs +147 -9
  89. package/scripts/daemon/cadence-consumer.test.mjs +6 -0
  90. package/scripts/daemon/cadence-handlers.mjs +158 -0
  91. package/scripts/daemon/cadence-handlers.test.mjs +64 -0
  92. package/scripts/daemon/deliver.mjs +314 -0
  93. package/scripts/daemon/dispatcher-governance.test.mjs +10 -0
  94. package/scripts/daemon/dispatcher.mjs +64 -6
  95. package/scripts/daemon/responder-cost.test.mjs +68 -0
  96. package/scripts/daemon/responder.mjs +351 -298
  97. package/scripts/local-triggers/generate-plists.test.mjs +7 -4
  98. package/scripts/maintenance/backup-run.mjs +415 -0
  99. package/scripts/maintenance/backup-to-cloud.sh +16 -116
  100. package/scripts/org/send-orgmail.mjs +16 -0
  101. package/scripts/record-receipt.sh +63 -0
  102. package/scripts/restore-from-backup.sh +14 -3
  103. package/scripts/restore-from-backup.test.mjs +8 -5
  104. package/scripts/send-email-threaded.py +47 -0
  105. package/scripts/send-sms.sh +4 -0
  106. package/scripts/send-whatsapp.sh +4 -0
  107. package/scripts/setup/init-backup.mjs +93 -38
  108. 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 config/, state/, logs/ — idempotent (back up existing first)
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 config/, state/, logs/ into $AGENT_DIR"
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 config\/, state\/, logs\//);
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
- // Each of the three dirs is named in the plan.
79
- assert.match(out, /would restore config\//);
80
- assert.match(out, /would restore state\//);
81
- assert.match(out, /would restore logs\//);
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]")
@@ -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"
@@ -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 — Off-machine state backup wizard.
3
+ * init-backup.mjs — give this agent a DR posture, non-interactively.
4
4
  *
5
- * Configures `.maestro/backup-config.yaml` with bucket details and seeds
6
- * a daily launchd plist that runs scripts/maintenance/backup-to-cloud.sh.
7
- * Requires interactive input — does NOT auto-run.
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 cfg = join(AGENT_DIR, ".maestro/backup-config.yaml");
21
- if (existsSync(cfg)) {
22
- ok("backup-config.yaml already present");
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
- writeFileSync(cfg, `# Off-machine state backup configuration
27
- # Fill in the bucket details, then run:
28
- # maestro init backup-replication --apply
29
- #
30
- # Backups cover (configurable below):
31
- # - state/ (queues, dashboards, inboxes — minus state/tmp/)
32
- # - knowledge/
33
- # - outputs/
34
- # - logs/ (rotated archives only — current-day logs excluded)
35
- # - config/agent.json (identity SOT)
36
-
37
- enabled: false
38
- provider: gcs # gcs | s3 | rsync
39
- bucket: "" # e.g. northwind-maestro-backups
40
- prefix: agent-name-here # e.g. ravi-ai (typically the repoSlug)
41
- schedule: "0 3 * * *" # daily 03:00 local (overrides via launchd plist)
42
- include:
43
- - state
44
- - knowledge
45
- - outputs
46
- - logs
47
- - config/agent.json
48
- exclude:
49
- - state/tmp
50
- - state/rag/index/*.bin
51
- retention_days: 30
52
- `);
53
- ok(`wrote ${cfg}`);
54
- warn("Backup is configured but NOT enabled. Edit .maestro/backup-config.yaml + set enabled: true, then re-run init.");
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.");
@@ -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