@ouro.bot/cli 0.1.0-alpha.696 → 0.1.0-alpha.698

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 (46) hide show
  1. package/changelog.json +18 -0
  2. package/dist/heart/agent-entry.js +32 -2
  3. package/dist/heart/autonomy-budget.js +434 -0
  4. package/dist/heart/core.js +11 -0
  5. package/dist/heart/daemon/cadence.js +110 -0
  6. package/dist/heart/daemon/cli-exec.js +20 -0
  7. package/dist/heart/daemon/cli-help.js +74 -0
  8. package/dist/heart/daemon/cli-parse.js +466 -0
  9. package/dist/heart/daemon/daemon-entry.js +104 -3
  10. package/dist/heart/daemon/daemon.js +24 -3
  11. package/dist/heart/daemon/doctor.js +151 -0
  12. package/dist/heart/daemon/stale-bundle-prune.js +23 -6
  13. package/dist/heart/habits/habit-parser.js +4 -1
  14. package/dist/heart/habits/habit-scheduler.js +46 -38
  15. package/dist/heart/habits/habit-session.js +2 -3
  16. package/dist/heart/provider-credentials.js +16 -0
  17. package/dist/heart/run-ledger.js +247 -0
  18. package/dist/heart/runtime-credentials.js +12 -3
  19. package/dist/mind/prompt-budget.js +380 -0
  20. package/dist/repertoire/tools-base.js +2 -0
  21. package/dist/repertoire/tools-rsvp.js +139 -0
  22. package/dist/rsvp/aisleplanner-client.js +354 -0
  23. package/dist/rsvp/cli.js +704 -0
  24. package/dist/rsvp/config.js +327 -0
  25. package/dist/rsvp/cutover.js +431 -0
  26. package/dist/rsvp/diagnostics.js +222 -0
  27. package/dist/rsvp/diff-renderer.js +161 -0
  28. package/dist/rsvp/habit-policy.js +85 -0
  29. package/dist/rsvp/habit-stage.js +93 -0
  30. package/dist/rsvp/incident-bundle.js +97 -0
  31. package/dist/rsvp/migration.js +204 -0
  32. package/dist/rsvp/outbound-state.js +254 -0
  33. package/dist/rsvp/query.js +118 -0
  34. package/dist/rsvp/replay.js +233 -0
  35. package/dist/rsvp/snapshot.js +232 -0
  36. package/dist/senses/bluebubbles/client.js +13 -7
  37. package/dist/senses/bluebubbles/index.js +227 -22
  38. package/dist/senses/bluebubbles/outbound-state.js +304 -0
  39. package/dist/senses/bluebubbles/replay.js +147 -0
  40. package/dist/senses/context-packet-ledger.js +245 -0
  41. package/dist/senses/context-packets.js +154 -0
  42. package/dist/senses/pipeline.js +91 -1
  43. package/dist/senses/private-runtime-worker.js +170 -31
  44. package/dist/senses/private-runtime.js +13 -1
  45. package/dist/senses/voice/twilio-phone.js +9 -5
  46. package/package.json +1 -1
@@ -68,6 +68,7 @@ const context_loss_sentinel_1 = require("../context-loss-sentinel");
68
68
  const provider_credentials_1 = require("../provider-credentials");
69
69
  const runtime_credentials_1 = require("../runtime-credentials");
70
70
  const machine_identity_1 = require("../machine-identity");
71
+ const auth_flow_1 = require("../auth/auth-flow");
71
72
  function parseSocketPath(argv) {
72
73
  const socketIndex = argv.indexOf("--socket");
73
74
  if (socketIndex >= 0) {
@@ -483,16 +484,116 @@ function writeStopCommandHealthState() {
483
484
  // Health writes are best-effort during shutdown.
484
485
  }
485
486
  }
487
+ function providerPreloadTargets(agent) {
488
+ try {
489
+ const { config } = (0, auth_flow_1.readAgentConfigForAgent)(agent, (0, identity_1.getAgentBundlesRoot)());
490
+ return [...new Set([config.humanFacing.provider, config.agentFacing.provider])];
491
+ }
492
+ catch (error) {
493
+ (0, runtime_1.emitNervesEvent)({
494
+ level: "warn",
495
+ component: "daemon",
496
+ event: "daemon.provider_preload_skipped",
497
+ message: "skipping provider credential preload because agent config could not be read",
498
+ meta: {
499
+ agent,
500
+ error: error instanceof Error ? error.message : /* v8 ignore next -- defensive non-Error config-read failures @preserve */ String(error),
501
+ },
502
+ });
503
+ return [];
504
+ }
505
+ }
506
+ function providerPreloadMissingTargets(result, providers) {
507
+ return providers.filter((provider) => !result.pool.providers[provider]);
508
+ }
486
509
  async function preloadProviderCredentialPools() {
487
- await Promise.all(managedAgents.map((agent) => (0, provider_credentials_1.refreshProviderCredentialPool)(agent, { preserveCachedOnFailure: true })));
510
+ await Promise.all(managedAgents.map(async (agent) => {
511
+ const providers = providerPreloadTargets(agent);
512
+ if (providers.length === 0)
513
+ return;
514
+ const result = await (0, provider_credentials_1.refreshProviderCredentialPool)(agent, { preserveCachedOnFailure: true, providers, skipCache: true });
515
+ if (result.ok) {
516
+ const missingProviders = providerPreloadMissingTargets(result, providers);
517
+ if (missingProviders.length > 0) {
518
+ (0, runtime_1.emitNervesEvent)({
519
+ level: "warn",
520
+ component: "daemon",
521
+ event: "daemon.provider_preload_unavailable",
522
+ message: "provider credential preload returned an incomplete selected provider cache",
523
+ meta: {
524
+ agent,
525
+ reason: "missing",
526
+ error: `missing selected providers: ${missingProviders.join(", ")}`,
527
+ },
528
+ });
529
+ return;
530
+ }
531
+ const records = Object.values(result.pool.providers).filter((record) => !!record);
532
+ (0, provider_credentials_1.cacheProviderCredentialRecords)(agent, records, new Date(result.pool.updatedAt));
533
+ return;
534
+ }
535
+ (0, runtime_1.emitNervesEvent)({
536
+ level: "warn",
537
+ component: "daemon",
538
+ event: "daemon.provider_preload_unavailable",
539
+ message: "provider credential preload could not refresh selected provider cache",
540
+ meta: {
541
+ agent,
542
+ reason: result.reason,
543
+ error: result.error,
544
+ },
545
+ });
546
+ }));
547
+ }
548
+ function startProviderCredentialPoolPreload() {
549
+ return preloadProviderCredentialPools().catch((error) => {
550
+ (0, runtime_1.emitNervesEvent)({
551
+ level: "error",
552
+ component: "daemon",
553
+ event: "daemon.provider_preload_error",
554
+ message: "provider credential preload failed after daemon startup",
555
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive non-Error provider preload failures @preserve */ String(error) },
556
+ });
557
+ });
558
+ }
559
+ function scheduleStartupSentinelAfterProviderPreload(agent, preload) {
560
+ void preload.then(async () => {
561
+ const result = await refreshDaemonSentinel(agent, "daemon_startup");
562
+ if (result.status === "critical") {
563
+ (0, runtime_1.emitNervesEvent)({
564
+ level: "error",
565
+ component: "daemon",
566
+ event: "daemon.startup_sentinel_error",
567
+ message: "startup Sentinel refresh reported critical after provider preload",
568
+ meta: {
569
+ agent,
570
+ error: result.message,
571
+ },
572
+ });
573
+ }
574
+ /* v8 ignore start -- refreshDaemonSentinel handles ordinary Sentinel failures; this is a promise-chain guard. @preserve */
575
+ }).catch((error) => {
576
+ (0, runtime_1.emitNervesEvent)({
577
+ level: "error",
578
+ component: "daemon",
579
+ event: "daemon.startup_sentinel_error",
580
+ message: "startup Sentinel refresh failed after provider preload",
581
+ meta: {
582
+ agent,
583
+ error: error instanceof Error ? error.message : String(error),
584
+ },
585
+ });
586
+ });
587
+ /* v8 ignore stop */
488
588
  }
489
589
  /* v8 ignore start -- habit wiring: lambdas delegate to processManager/fs; tested via HabitScheduler unit tests @preserve */
490
- void preloadProviderCredentialPools().then(() => daemon.start()).then(async () => {
590
+ void daemon.start().then(async () => {
591
+ const providerPreload = startProviderCredentialPoolPreload();
491
592
  const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
492
593
  const ouroPath = (0, os_cron_deps_1.resolveOuroBinaryPath)();
493
594
  const osCronDeps = (0, os_cron_deps_1.createRealOsCronDeps)();
494
595
  for (const agent of managedAgents) {
495
- await refreshDaemonSentinel(agent, "daemon_startup");
596
+ scheduleStartupSentinelAfterProviderPreload(agent, providerPreload);
496
597
  const bundleRoot = path.join(bundlesRoot, `${agent}.ouro`);
497
598
  const habitsDir = path.join(bundleRoot, "habits");
498
599
  const degradedComponent = `habits:${agent}`;
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.OuroDaemon = void 0;
37
+ exports.messageFromHabitPokeError = messageFromHabitPokeError;
37
38
  exports.parseOrphanPidsFromPs = parseOrphanPidsFromPs;
38
39
  exports.filterPidfilePidsToActualOrphans = filterPidfilePidsToActualOrphans;
39
40
  exports.mergeUniqueOrphanPids = mergeUniqueOrphanPids;
@@ -74,6 +75,7 @@ const habit_private_wake_1 = require("./habit-private-wake");
74
75
  const habit_parser_1 = require("../habits/habit-parser");
75
76
  const habit_runtime_state_1 = require("../habits/habit-runtime-state");
76
77
  const router_1 = require("../external-events/router");
78
+ const habit_policy_1 = require("../../rsvp/habit-policy");
77
79
  const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
78
80
  /**
79
81
  * Defense-in-depth: detect if we're running under vitest. The pidfile lives
@@ -95,6 +97,12 @@ function isVitestProcess() {
95
97
  return false;
96
98
  return process.argv.some((arg) => typeof arg === "string" && arg.includes("vitest"));
97
99
  }
100
+ function messageFromHabitPokeError(error) {
101
+ if (error instanceof Error)
102
+ return error.message;
103
+ /* v8 ignore next -- defensive fallback for hostile/non-Error throws from filesystem or parsers @preserve */
104
+ return String(error);
105
+ }
98
106
  /**
99
107
  * Scan `ps -eo pid,ppid,command` output for daemon-owned entry points whose
100
108
  * parent has died (PPID reparented to init/PID 1). Returns the list of PIDs
@@ -1180,13 +1188,26 @@ class OuroDaemon {
1180
1188
  };
1181
1189
  }
1182
1190
  resolveHabitPokeOccurrenceId(command, trigger) {
1183
- if (trigger === "poke" || trigger === "manual")
1184
- return undefined;
1185
1191
  const agentRoot = path.join(this.bundlesRoot, `${command.agent}.ouro`);
1186
1192
  const habitPath = path.join(agentRoot, "habits", `${command.habitName}.md`);
1193
+ let parsedHabit = null;
1194
+ if ((0, habit_policy_1.isRsvpHabitName)(command.habitName)) {
1195
+ if (!fs.existsSync(habitPath))
1196
+ return { skipReason: "RSVP habit file not found" };
1197
+ try {
1198
+ parsedHabit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
1199
+ }
1200
+ catch (error) {
1201
+ return { skipReason: `RSVP habit metadata invalid: ${messageFromHabitPokeError(error)}` };
1202
+ }
1203
+ if (!parsedHabit.rsvp)
1204
+ return { skipReason: "RSVP habit metadata is required before private runtime wake" };
1205
+ }
1206
+ if (trigger === "poke" || trigger === "manual")
1207
+ return undefined;
1187
1208
  if (!fs.existsSync(habitPath))
1188
1209
  return { skipReason: "habit file not found" };
1189
- const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
1210
+ const habit = parsedHabit ?? (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
1190
1211
  if (habit.cadence === null)
1191
1212
  return { skipReason: "habit has no cadence" };
1192
1213
  const cadence = habit.cadence;
@@ -13,6 +13,7 @@ exports.checkDaemon = checkDaemon;
13
13
  exports.checkAgents = checkAgents;
14
14
  exports.checkSenses = checkSenses;
15
15
  exports.checkHabits = checkHabits;
16
+ exports.checkRsvp = checkRsvp;
16
17
  exports.checkSecurity = checkSecurity;
17
18
  exports.checkTrips = checkTrips;
18
19
  exports.checkMailroom = checkMailroom;
@@ -28,6 +29,9 @@ const machine_identity_1 = require("../machine-identity");
28
29
  const habit_parser_1 = require("../habits/habit-parser");
29
30
  const cadence_1 = require("./cadence");
30
31
  const nerves_1 = require("../../nerves");
32
+ const config_1 = require("../../rsvp/config");
33
+ const cutover_1 = require("../../rsvp/cutover");
34
+ const diagnostics_1 = require("../../rsvp/diagnostics");
31
35
  const DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS = 30_000;
32
36
  // ── Category checkers ──
33
37
  function checkCliPath(deps) {
@@ -397,6 +401,152 @@ function checkHabits(deps) {
397
401
  }
398
402
  return { name: "Habits", checks };
399
403
  }
404
+ function hasRsvpHabitSignal(deps, agentPath) {
405
+ const habitsDir = `${agentPath}/habits`;
406
+ if (!deps.existsSync(habitsDir))
407
+ return false;
408
+ try {
409
+ return deps.readdirSync(habitsDir).some((name) => name.toLowerCase().includes("rsvp") && name.endsWith(".md"));
410
+ }
411
+ catch {
412
+ return false;
413
+ }
414
+ }
415
+ function rsvpDoctorLabel(agentDir, checkId) {
416
+ const suffix = {
417
+ "rsvp.native_config": "RSVP native config",
418
+ "rsvp.aisleplanner_source": "RSVP AislePlanner source",
419
+ "rsvp.aisleplanner_credentials": "RSVP AislePlanner credentials",
420
+ "rsvp.bluebubbles_route": "RSVP BlueBubbles route",
421
+ "rsvp.bluebubbles_attachment": "RSVP BlueBubbles attachment",
422
+ };
423
+ return `${agentDir} ${suffix[checkId]}`;
424
+ }
425
+ function rsvpDoctorId(checkId) {
426
+ const ids = {
427
+ "rsvp.native_config": "rsvp.native_config",
428
+ "rsvp.aisleplanner_source": "rsvp.aisleplanner.source",
429
+ "rsvp.aisleplanner_credentials": "rsvp.aisleplanner.credentials",
430
+ "rsvp.bluebubbles_route": "rsvp.bluebubbles.route",
431
+ "rsvp.bluebubbles_attachment": "rsvp.bluebubbles.attachment",
432
+ };
433
+ return ids[checkId];
434
+ }
435
+ function rsvpDoctorDetail(readinessCheck) {
436
+ return readinessCheck.id === "rsvp.bluebubbles_route" && readinessCheck.status === "pass"
437
+ ? "configured"
438
+ : readinessCheck.detail;
439
+ }
440
+ function doctorStatus(status) {
441
+ return status;
442
+ }
443
+ function discoverRsvpCutoverLegacyRoot(deps, config) {
444
+ const configured = typeof config?.cutover?.legacyRoot === "string" ? config.cutover.legacyRoot.trim() : "";
445
+ if (configured)
446
+ return configured;
447
+ if (deps.rsvpCutoverLegacyRoot)
448
+ return deps.rsvpCutoverLegacyRoot;
449
+ const candidate = `${deps.homedir}/Projects/rsvp-tracker`;
450
+ return deps.existsSync(`${candidate}/config.json`) ? candidate : null;
451
+ }
452
+ function rsvpCutoverDetail(checks, sendAllowed, denialCount) {
453
+ return [
454
+ `sendAllowed=${sendAllowed}`,
455
+ `launchAgentInactive=${checks.launchAgentInactive}`,
456
+ `legacyProcessInactive=${checks.legacyProcessInactive}`,
457
+ `legacyConfigSendInactive=${checks.legacyConfigSendInactive}`,
458
+ `legacyLiveSendInactive=${checks.legacyLiveSendInactive}`,
459
+ `nativeBlueBubblesCredentialHealthy=${checks.nativeBlueBubblesCredentialHealthy}`,
460
+ `denialCount=${denialCount}`,
461
+ ].join("; ");
462
+ }
463
+ async function checkRsvp(deps) {
464
+ const checks = [];
465
+ const agents = discoverAgents(deps);
466
+ if (agents.length === 0) {
467
+ checks.push({ label: "RSVP", status: "warn", detail: "no agent bundles found" });
468
+ return { name: "RSVP", checks };
469
+ }
470
+ for (const agentDir of agents) {
471
+ const agentName = agentDir.replace(/\.ouro$/, "");
472
+ const agentPath = `${deps.bundlesRoot}/${agentDir}`;
473
+ const config = (0, config_1.readRsvpConfig)(agentPath);
474
+ const hasRsvpSignal = config.ok || hasRsvpHabitSignal(deps, agentPath);
475
+ if (!hasRsvpSignal) {
476
+ checks.push({ label: `${agentDir} RSVP`, status: "pass", detail: "not configured" });
477
+ continue;
478
+ }
479
+ const machineId = (0, machine_identity_1.loadOrCreateMachineIdentity)({ homeDir: deps.homedir }).machineId;
480
+ const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agentName, { preserveCachedOnFailure: true });
481
+ const machineRuntimeConfig = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agentName, machineId, { preserveCachedOnFailure: true });
482
+ const readiness = (0, config_1.validateRsvpReadiness)({
483
+ agent: agentName,
484
+ agentRoot: agentPath,
485
+ runtimeConfig,
486
+ machineRuntimeConfig,
487
+ config,
488
+ });
489
+ for (const readinessCheck of readiness.checks) {
490
+ checks.push({
491
+ id: rsvpDoctorId(readinessCheck.id),
492
+ label: rsvpDoctorLabel(agentDir, readinessCheck.id),
493
+ status: readinessCheck.status === "pass" ? "pass" : "fail",
494
+ detail: rsvpDoctorDetail(readinessCheck),
495
+ });
496
+ }
497
+ const attachment = readiness.checks.find((check) => check.id === "rsvp.bluebubbles_attachment");
498
+ if (attachment) {
499
+ checks.push({
500
+ id: "rsvp.bluebubbles.attachment_identity",
501
+ label: `${agentDir} RSVP BlueBubbles attachment identity`,
502
+ status: attachment.status === "pass" ? "pass" : "fail",
503
+ detail: attachment.status === "pass" ? "machine attachment credential present" : attachment.detail,
504
+ });
505
+ }
506
+ const diagnostics = (0, diagnostics_1.collectRsvpDiagnostics)(agentPath, deps);
507
+ checks.push({
508
+ id: "rsvp.context_packet_ledger",
509
+ label: `${agentDir} RSVP context packet ledger`,
510
+ status: doctorStatus(diagnostics.contextPacketLedger.status),
511
+ detail: diagnostics.contextPacketLedger.detail,
512
+ }, {
513
+ id: "rsvp.habit.schedule",
514
+ label: `${agentDir} RSVP habit schedule`,
515
+ status: doctorStatus(diagnostics.habitSchedule.status),
516
+ detail: diagnostics.habitSchedule.detail,
517
+ }, {
518
+ id: "rsvp.latest_fetch",
519
+ label: `${agentDir} RSVP latest fetch`,
520
+ status: doctorStatus(diagnostics.latestFetch.status),
521
+ detail: diagnostics.latestFetch.detail,
522
+ }, {
523
+ id: "rsvp.delivery.reconciliation",
524
+ label: `${agentDir} RSVP delivery reconciliation`,
525
+ status: doctorStatus(diagnostics.deliveryReconciliation.status),
526
+ detail: diagnostics.deliveryReconciliation.detail,
527
+ }, {
528
+ id: "rsvp.spend_timeline",
529
+ label: `${agentDir} RSVP spend timeline`,
530
+ status: doctorStatus(diagnostics.spendTimeline.status),
531
+ detail: diagnostics.spendTimeline.detail,
532
+ });
533
+ const legacyRoot = discoverRsvpCutoverLegacyRoot(deps, config.ok ? config.config : undefined);
534
+ if (config.ok && legacyRoot) {
535
+ const cutover = await (0, cutover_1.checkRsvpCutover)({
536
+ agent: agentName,
537
+ legacyRoot,
538
+ ...(deps.rsvpCutoverDeps ? { deps: deps.rsvpCutoverDeps } : {}),
539
+ });
540
+ checks.push({
541
+ id: "rsvp.cutover.live_send_preflight",
542
+ label: `${agentDir} RSVP legacy live-send preflight`,
543
+ status: cutover.sendAllowed ? "pass" : "fail",
544
+ detail: rsvpCutoverDetail(cutover.checks, cutover.sendAllowed, cutover.denialReasons.length),
545
+ });
546
+ }
547
+ }
548
+ return { name: "RSVP", checks };
549
+ }
400
550
  function checkSecurity(deps) {
401
551
  const checks = [];
402
552
  const agents = discoverAgents(deps);
@@ -934,6 +1084,7 @@ const CATEGORY_CHECKERS = [
934
1084
  { name: "Agents", fn: checkAgents },
935
1085
  { name: "Senses", fn: checkSenses },
936
1086
  { name: "Habits", fn: checkHabits },
1087
+ { name: "RSVP", fn: checkRsvp },
937
1088
  { name: "Security", fn: checkSecurity },
938
1089
  { name: "Trips", fn: checkTrips },
939
1090
  { name: "Mailroom", fn: checkMailroom },
@@ -39,13 +39,16 @@ const path = __importStar(require("path"));
39
39
  const identity_1 = require("../identity");
40
40
  const runtime_1 = require("../../nerves/runtime");
41
41
  /**
42
- * Scan the bundles root for `.ouro` directories that have no `agent.json`
43
- * file (definitively dead ephemeral bundles) and delete them.
42
+ * Scan the bundles root for `.ouro` directories that have no bundle contract
43
+ * or durable state markers (definitively dead ephemeral bundles) and delete
44
+ * them.
44
45
  *
45
46
  * Returns a list of pruned bundle directory names (e.g. `["stale.ouro"]`)
46
47
  * for display purposes. Bundles that have `agent.json` -- even if disabled
47
- * -- are never deleted. Errors on individual bundles are swallowed so that
48
- * one permission-denied doesn't block pruning the rest.
48
+ * -- are never deleted. Task-doc-only bundles are also durable work state, so
49
+ * directories with state roots such as `tasks`, `desk`, `arc`, `state`, or
50
+ * `.git` are preserved. Errors on individual bundles are swallowed so that one
51
+ * permission-denied doesn't block pruning the rest.
49
52
  */
50
53
  function pruneStaleEphemeralBundles(deps = {}) {
51
54
  const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
@@ -64,8 +67,7 @@ function pruneStaleEphemeralBundles(deps = {}) {
64
67
  if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
65
68
  continue;
66
69
  const bundlePath = path.join(bundlesRoot, entry.name);
67
- const agentJsonPath = path.join(bundlePath, "agent.json");
68
- if (existsSync(agentJsonPath))
70
+ if (hasDurableBundleMarker(bundlePath, existsSync))
69
71
  continue;
70
72
  try {
71
73
  rmSync(bundlePath, { recursive: true, force: true });
@@ -94,3 +96,18 @@ function pruneStaleEphemeralBundles(deps = {}) {
94
96
  }
95
97
  return pruned;
96
98
  }
99
+ const DURABLE_BUNDLE_MARKERS = [
100
+ "agent.json",
101
+ "tasks",
102
+ "desk",
103
+ "arc",
104
+ "state",
105
+ "habits",
106
+ "friends",
107
+ "facts",
108
+ "vault",
109
+ ".git",
110
+ ];
111
+ function hasDurableBundleMarker(bundlePath, existsSync) {
112
+ return DURABLE_BUNDLE_MARKERS.some((marker) => existsSync(path.join(bundlePath, marker)));
113
+ }
@@ -38,6 +38,7 @@ exports.renderHabitFile = renderHabitFile;
38
38
  const path = __importStar(require("path"));
39
39
  const frontmatter_1 = require("../../util/frontmatter");
40
40
  const runtime_1 = require("../../nerves/runtime");
41
+ const habit_policy_1 = require("../../rsvp/habit-policy");
41
42
  function isHabitStatus(value) {
42
43
  return value === "active" || value === "paused";
43
44
  }
@@ -157,7 +158,8 @@ function parseHabitFile(content, filePath) {
157
158
  const lastRun = typeof rawLastRun === "string" && rawLastRun.length > 0 ? rawLastRun : null;
158
159
  const rawCreated = frontmatter.created;
159
160
  const created = typeof rawCreated === "string" && rawCreated.length > 0 ? rawCreated : null;
160
- const tools = parseToolsField(frontmatter.tools);
161
+ const rsvp = (0, habit_policy_1.parseRsvpHabitMetadata)(frontmatter.rsvp);
162
+ const tools = rsvp ? [...habit_policy_1.RSVP_HABIT_ALLOWED_TOOLS] : parseToolsField(frontmatter.tools);
161
163
  const origin = parseOrigin(frontmatter.origin);
162
164
  const surface = parseSurface(frontmatter.surface);
163
165
  const continuity = parseContinuity(frontmatter.continuity);
@@ -169,6 +171,7 @@ function parseHabitFile(content, filePath) {
169
171
  lastRun,
170
172
  created,
171
173
  tools,
174
+ ...(rsvp ? { rsvp } : {}),
172
175
  origin,
173
176
  surface,
174
177
  continuity,
@@ -39,6 +39,7 @@ const runtime_1 = require("../../nerves/runtime");
39
39
  const habit_parser_1 = require("./habit-parser");
40
40
  const habit_runtime_state_1 = require("./habit-runtime-state");
41
41
  const cadence_1 = require("../daemon/cadence");
42
+ const habit_policy_1 = require("../../rsvp/habit-policy");
42
43
  const WATCH_DEBOUNCE_MS = 200;
43
44
  class HabitScheduler {
44
45
  agent;
@@ -97,33 +98,21 @@ class HabitScheduler {
97
98
  continue;
98
99
  if (!habit.cadence)
99
100
  continue;
100
- const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
101
- if (cadenceMs === null)
101
+ if (this.rejectInvalidRsvpHabit(habit))
102
102
  continue;
103
103
  const nowMs = this.deps.now();
104
- if (habit.lastRun === null) {
105
- (0, runtime_1.emitNervesEvent)({
106
- component: "daemon",
107
- event: "daemon.habit_fire",
108
- message: "firing overdue habit (never run)",
109
- meta: { habitName: habit.name, agent: this.agent },
110
- });
111
- this.onHabitFire(habit.name, "overdue", {
112
- occurrenceId: this.overdueOccurrenceId(habit),
113
- });
104
+ const dueState = (0, cadence_1.evaluateCadenceDue)(habit.cadence, habit.lastRun, nowMs);
105
+ if (dueState === null)
114
106
  continue;
115
- }
116
- const lastRunMs = new Date(habit.lastRun).getTime();
117
- const elapsed = nowMs - lastRunMs;
118
- if (elapsed >= cadenceMs) {
107
+ if (dueState.due) {
119
108
  (0, runtime_1.emitNervesEvent)({
120
109
  component: "daemon",
121
110
  event: "daemon.habit_fire",
122
- message: "firing overdue habit",
123
- meta: { habitName: habit.name, agent: this.agent, elapsedMs: elapsed },
111
+ message: habit.lastRun === null ? "firing overdue habit (never run)" : "firing overdue habit",
112
+ meta: { habitName: habit.name, agent: this.agent, elapsedMs: dueState.elapsedMs },
124
113
  });
125
114
  this.onHabitFire(habit.name, "overdue", {
126
- occurrenceId: this.overdueOccurrenceId(habit),
115
+ occurrenceId: dueState.occurrenceId ?? this.overdueOccurrenceId(habit),
127
116
  });
128
117
  }
129
118
  }
@@ -150,18 +139,11 @@ class HabitScheduler {
150
139
  continue;
151
140
  if (!habit.cadence)
152
141
  continue;
153
- const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
154
- if (cadenceMs === null)
142
+ if (this.rejectInvalidRsvpHabit(habit))
155
143
  continue;
156
- if (habit.lastRun === null) {
157
- overdue.push({ name: habit.name, elapsedMs: Infinity });
158
- continue;
159
- }
160
- const lastRunMs = new Date(habit.lastRun).getTime();
161
- const elapsed = nowMs - lastRunMs;
162
- if (elapsed >= cadenceMs) {
163
- overdue.push({ name: habit.name, elapsedMs: elapsed });
164
- }
144
+ const dueState = (0, cadence_1.evaluateCadenceDue)(habit.cadence, habit.lastRun, nowMs);
145
+ if (dueState?.due)
146
+ overdue.push({ name: habit.name, elapsedMs: dueState.elapsedMs });
165
147
  }
166
148
  return overdue;
167
149
  }
@@ -278,9 +260,9 @@ class HabitScheduler {
278
260
  });
279
261
  // Parse cadence from the original habit file for timer interval
280
262
  const habitFile = this.getHabitFile(job.taskId);
281
- const ms = habitFile?.cadence ? (0, cadence_1.parseCadenceToMs)(habitFile.cadence) : null;
263
+ const ms = habitFile?.cadence ? (0, cadence_1.cadenceFallbackDelayMs)(habitFile.cadence, this.deps.now()) : null;
282
264
  if (ms !== null) {
283
- this.createTimerFallback(job.taskId, ms);
265
+ this.createTimerFallback(job.taskId, habitFile.cadence, ms);
284
266
  }
285
267
  this.degradedHabitNames.set(job.taskId, "cron registration failed — using timer fallback");
286
268
  }
@@ -315,14 +297,15 @@ class HabitScheduler {
315
297
  }
316
298
  return verified;
317
299
  }
318
- createTimerFallback(habitName, cadenceMs) {
300
+ createTimerFallback(habitName, cadence, firstDelayMs) {
319
301
  const schedule = () => {
302
+ const delayMs = (0, cadence_1.cadenceFallbackDelayMs)(cadence, this.deps.now()) ?? firstDelayMs;
320
303
  const timer = setTimeout(() => {
321
304
  this.onHabitFire(habitName, "overdue", {
322
- occurrenceId: this.timerOccurrenceId(habitName, cadenceMs),
305
+ occurrenceId: this.timerOccurrenceId(habitName, cadence),
323
306
  });
324
307
  schedule();
325
- }, cadenceMs);
308
+ }, delayMs);
326
309
  this.timerFallbacks.set(habitName, timer);
327
310
  };
328
311
  schedule();
@@ -380,6 +363,8 @@ class HabitScheduler {
380
363
  continue;
381
364
  if (!habit.cadence)
382
365
  continue;
366
+ if (this.rejectInvalidRsvpHabit(habit))
367
+ continue;
383
368
  const cronSchedule = (0, cadence_1.parseCadenceToCron)(habit.cadence);
384
369
  if (cronSchedule === null)
385
370
  continue;
@@ -395,6 +380,24 @@ class HabitScheduler {
395
380
  }
396
381
  return jobs;
397
382
  }
383
+ rejectInvalidRsvpHabit(habit) {
384
+ if (!(0, habit_policy_1.isRsvpHabitName)(habit.name) || habit.rsvp)
385
+ return false;
386
+ this.recordHabitParseError(`${habit.name}.md`, "RSVP habit metadata is required before scheduling");
387
+ return true;
388
+ }
389
+ recordHabitParseError(file, error) {
390
+ if (this.parseErrors.some((existing) => existing.file === file && existing.error === error))
391
+ return;
392
+ this.parseErrors.push({ file, error });
393
+ (0, runtime_1.emitNervesEvent)({
394
+ level: "error",
395
+ component: "daemon",
396
+ event: "daemon.habit_parse_error",
397
+ message: "failed to parse habit file",
398
+ meta: { file, error, agent: this.agent },
399
+ });
400
+ }
398
401
  overdueOccurrenceId(habit) {
399
402
  if (habit.lastRun === null)
400
403
  return `overdue:first-run:${habit.cadence}`;
@@ -403,9 +406,14 @@ class HabitScheduler {
403
406
  jobOccurrenceId(job, trigger) {
404
407
  return `job:${job.id}:${trigger}:last-run:${job.lastRun ?? "never"}`;
405
408
  }
406
- timerOccurrenceId(habitName, cadenceMs) {
407
- const slot = Math.floor(this.deps.now() / cadenceMs);
408
- return `timer:${habitName}:cadence-ms:${cadenceMs}:slot:${slot}`;
409
+ timerOccurrenceId(habitName, cadence) {
410
+ const cadenceMs = (0, cadence_1.parseCadenceToMs)(cadence);
411
+ if (cadenceMs !== null) {
412
+ const slot = Math.floor(this.deps.now() / cadenceMs);
413
+ return `timer:${habitName}:cadence-ms:${cadenceMs}:slot:${slot}`;
414
+ }
415
+ const slot = (0, cadence_1.nextCadenceRunAt)(cadence, this.deps.now()) ?? "unknown";
416
+ return `timer:${habitName}:cadence:${cadence}:slot:${slot}`;
409
417
  }
410
418
  }
411
419
  exports.HabitScheduler = HabitScheduler;
@@ -453,11 +453,10 @@ function filterHabitToolsForEnvelope(definitions, requestedTools, envelope, risk
453
453
  function computeNextRunAt(habit, endedAt) {
454
454
  if (habit.status !== "active" || !habit.cadence)
455
455
  return null;
456
- const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
457
456
  const endedMs = Date.parse(endedAt);
458
- if (cadenceMs === null || !Number.isFinite(endedMs))
457
+ if (!Number.isFinite(endedMs))
459
458
  return null;
460
- return new Date(endedMs + cadenceMs).toISOString();
459
+ return (0, cadence_1.nextCadenceRunAt)(habit.cadence, endedMs);
461
460
  }
462
461
  function defaultSummarySnapshot(input) {
463
462
  const errors = input.errors ?? [];
@@ -241,6 +241,7 @@ function readProviderCredentialPool(agentName) {
241
241
  }
242
242
  async function refreshProviderCredentialPool(agentName, options = {}) {
243
243
  const providersToRead = selectedProviders(options.providers);
244
+ const requiresAllSelectedProviders = !!options.providers && options.providers.length > 0;
244
245
  try {
245
246
  const store = (0, credential_access_1.getCredentialStore)(agentName);
246
247
  options.onProgress?.(`reading vault items for ${agentName}...`);
@@ -266,6 +267,21 @@ async function refreshProviderCredentialPool(agentName, options = {}) {
266
267
  updatedAt = record.updatedAt;
267
268
  }
268
269
  options.onProgress?.("parsing provider credentials...");
270
+ const missingRequestedProviders = requiresAllSelectedProviders
271
+ ? providersToRead.filter((provider) => !providers[provider])
272
+ : [];
273
+ if (missingRequestedProviders.length > 0) {
274
+ const cached = cachedPools.get(agentName);
275
+ const result = {
276
+ ok: false,
277
+ reason: "missing",
278
+ poolPath: providerCredentialsVaultPath(agentName),
279
+ error: `requested provider credentials missing from ${providerCredentialsVaultPath(agentName)}: ${missingRequestedProviders.join(", ")}`,
280
+ };
281
+ if (options.preserveCachedOnFailure && cached?.ok)
282
+ return cached;
283
+ return options.skipCache ? result : cacheResult(agentName, result);
284
+ }
269
285
  const pool = {
270
286
  schemaVersion: 1,
271
287
  updatedAt,