@ouro.bot/cli 0.1.0-alpha.723 → 0.1.0-alpha.725

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 (55) hide show
  1. package/changelog.json +295 -275
  2. package/dist/heart/active-work.js +10 -3
  3. package/dist/heart/core.js +343 -77
  4. package/dist/heart/daemon/cli-exec.js +132 -15
  5. package/dist/heart/daemon/cli-help.js +13 -2
  6. package/dist/heart/daemon/cli-parse.js +82 -1
  7. package/dist/heart/daemon/daemon.js +286 -33
  8. package/dist/heart/daemon/doctor.js +82 -6
  9. package/dist/heart/daemon/habit-private-wake.js +6 -1
  10. package/dist/heart/habits/habit-cancel.js +810 -0
  11. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  12. package/dist/heart/habits/habit-migration.js +28 -23
  13. package/dist/heart/habits/habit-parser.js +72 -8
  14. package/dist/heart/habits/habit-runtime-state.js +3 -24
  15. package/dist/heart/habits/habit-scheduler.js +268 -50
  16. package/dist/heart/habits/habit-session.js +16 -0
  17. package/dist/heart/hatch/hatch-flow.js +6 -4
  18. package/dist/heart/mailbox/readers/runtime-readers.js +26 -27
  19. package/dist/heart/orientation-frame.js +58 -1
  20. package/dist/heart/provider-attempt.js +4 -1
  21. package/dist/heart/providers/anthropic.js +23 -2
  22. package/dist/heart/start-of-turn-packet.js +14 -11
  23. package/dist/heart/streaming.js +565 -185
  24. package/dist/mind/obligation-steering.js +15 -9
  25. package/dist/mind/prompt.js +58 -26
  26. package/dist/nerves/coverage/run-artifacts.js +55 -35
  27. package/dist/nerves/review/cli.js +2 -2
  28. package/dist/nerves/review/core.js +14 -7
  29. package/dist/repertoire/tools-base.js +2 -0
  30. package/dist/repertoire/tools-habits.js +103 -0
  31. package/dist/repertoire/tools.js +18 -3
  32. package/dist/rsvp/cli.js +126 -20
  33. package/dist/rsvp/diagnostics.js +12 -2
  34. package/dist/rsvp/habit-policy.js +7 -0
  35. package/dist/rsvp/habit-stage.js +19 -4
  36. package/dist/rsvp/native-habit-runner.js +151 -11
  37. package/dist/rsvp/outbound-state.js +360 -0
  38. package/dist/senses/bluebubbles/client.js +90 -17
  39. package/dist/senses/bluebubbles/inbound-log.js +1 -0
  40. package/dist/senses/bluebubbles/index.js +662 -425
  41. package/dist/senses/bluebubbles/model.js +57 -13
  42. package/dist/senses/bluebubbles/mutation-log.js +21 -10
  43. package/dist/senses/bluebubbles/reaction-policy.js +55 -0
  44. package/dist/senses/bluebubbles/semantic-receipts.js +1818 -0
  45. package/dist/senses/cli/tui-store.js +1 -0
  46. package/dist/senses/cli.js +1 -0
  47. package/dist/senses/habit-lifecycle-guard.js +24 -0
  48. package/dist/senses/ingress-evidence.js +2 -0
  49. package/dist/senses/mail.js +129 -0
  50. package/dist/senses/pipeline.js +90 -24
  51. package/dist/senses/private-runtime-worker.js +21 -20
  52. package/dist/senses/private-runtime.js +79 -29
  53. package/dist/senses/shared-turn.js +1 -0
  54. package/dist/senses/teams.js +1 -0
  55. package/package.json +6 -1
package/dist/rsvp/cli.js CHANGED
@@ -179,13 +179,49 @@ function reportCopyFromHabit(title, rsvp) {
179
179
  }
180
180
  return copy;
181
181
  }
182
+ function explicitHabitLifecycleError(habitName, habit, prefix) {
183
+ const degradedReason = habit.status === "degraded" ? habit.degradedReason : null;
184
+ const degradedDetail = habit.status === "degraded" ? habit.degradedDetail : null;
185
+ const reasonText = degradedReason ? ` reason=${degradedReason}` : "";
186
+ const detailText = degradedDetail ? ` detail=${degradedDetail}` : "";
187
+ const message = prefix
188
+ ? `${prefix}; status=${habit.status}${reasonText}${detailText}`
189
+ : `RSVP habit ${habitName} lifecycle rejected: status=${habit.status}${reasonText}${detailText}`;
190
+ (0, runtime_1.emitNervesEvent)({
191
+ level: "warn",
192
+ component: "rsvp",
193
+ event: "rsvp.habit_lifecycle_rejected",
194
+ message,
195
+ meta: {
196
+ entryPoint: "explicit_refresh",
197
+ habitName,
198
+ status: habit.status,
199
+ degradedReason,
200
+ degradedDetail,
201
+ },
202
+ });
203
+ return new Error(message);
204
+ }
182
205
  function requireRsvpHabitReportCopy(agentRoot, habitName) {
183
206
  const habitPath = path.join(agentRoot, "habits", `${habitName}.md`);
184
- if (!fs.existsSync(habitPath))
185
- throw new Error(`RSVP habit ${habitName} not found at ${habitPath}`);
186
- const habit = (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath);
187
- if (!habit.rsvp)
188
- throw new Error(`RSVP habit ${habitName} metadata is required before explicit refresh`);
207
+ if (!fs.existsSync(habitPath)) {
208
+ const missing = (0, habit_parser_1.createDegradedHabitFile)(habitPath, "read_error", "", "habit file not found");
209
+ throw explicitHabitLifecycleError(habitName, missing, `RSVP habit ${habitName} not found at ${habitPath}`);
210
+ }
211
+ let habit;
212
+ try {
213
+ habit = (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath);
214
+ }
215
+ catch (error) {
216
+ const unreadable = (0, habit_parser_1.createDegradedHabitFile)(habitPath, "read_error", "", String(error));
217
+ throw explicitHabitLifecycleError(habitName, unreadable);
218
+ }
219
+ if (habit.status !== "active")
220
+ throw explicitHabitLifecycleError(habitName, habit);
221
+ if (!habit.rsvp) {
222
+ const untyped = (0, habit_parser_1.createDegradedHabitFile)(habitPath, "invalid_metadata", habit.body, "RSVP habit metadata is required");
223
+ throw explicitHabitLifecycleError(habitName, untyped, `RSVP habit ${habitName} metadata is required before explicit refresh`);
224
+ }
189
225
  return reportCopyFromHabit(habit.title, habit.rsvp);
190
226
  }
191
227
  function activeRsvpHabitReportCopy(agentRoot) {
@@ -553,7 +589,7 @@ async function executeHabitStage(command, deps) {
553
589
  requires: "--agent",
554
590
  };
555
591
  }
556
- const result = (0, habit_stage_1.stageRsvpHabit)({
592
+ const result = await (0, habit_stage_1.stageRsvpHabit)({
557
593
  agent: command.agent,
558
594
  agentRoot,
559
595
  ...(command.habitName ? { habitName: command.habitName } : {}),
@@ -566,7 +602,7 @@ async function executeHabitStage(command, deps) {
566
602
  habit: result,
567
603
  });
568
604
  }
569
- async function executeRefresh(command, deps) {
605
+ async function executeRefreshInner(command, deps, transportCounter, hardNoSend) {
570
606
  const agentRoot = maybeAgentRoot(command, deps);
571
607
  if (!command.agent || !agentRoot) {
572
608
  return {
@@ -645,8 +681,9 @@ async function executeRefresh(command, deps) {
645
681
  const delta = (0, diff_renderer_1.computeRsvpDelta)(previousSnapshot, currentSnapshot);
646
682
  const reportText = (0, diff_renderer_1.renderRsvpReport)(delta, reportCopy);
647
683
  const outboundDecision = (0, outbound_state_1.decideRsvpOutboundReport)({ agentRoot, currentSnapshot, reportText });
648
- const wantsLiveSend = command.mode === "live" && command.allowSend === true && outboundDecision.action === "send";
649
- if (wantsLiveSend) {
684
+ const requestedLiveSend = command.mode === "live" && command.allowSend === true && outboundDecision.action === "send";
685
+ const sendAllowedBeforeCutover = requestedLiveSend && !hardNoSend;
686
+ if (sendAllowedBeforeCutover) {
650
687
  const cutover = await checkLiveSendCutover({
651
688
  agent: command.agent,
652
689
  config: configResult.config,
@@ -663,32 +700,90 @@ async function executeRefresh(command, deps) {
663
700
  });
664
701
  }
665
702
  }
666
- const sendAllowed = wantsLiveSend;
703
+ const sendAllowed = requestedLiveSend && !hardNoSend;
667
704
  let delivery;
705
+ let sendBoundary;
706
+ let sendBoundaryMessage;
668
707
  if (sendAllowed) {
708
+ if (!command.habitName) {
709
+ return {
710
+ ...basePayload(command, false, "RSVP live refresh send requires --habit <name>", {
711
+ allowSend: true,
712
+ sendAllowed: false,
713
+ requires: "--habit <name>",
714
+ refresh: {
715
+ snapshotId: currentSnapshot.snapshotId,
716
+ reportText,
717
+ outboundDecision: outboundDecision,
718
+ },
719
+ }),
720
+ ok: false,
721
+ };
722
+ }
669
723
  const bluebubbles = rsvpBlueBubblesClientConfig(configResult.config, machineRuntimeConfig);
670
- const sent = await (0, client_1.createBlueBubblesClient)(bluebubbles.config, bluebubbles.channelConfig).sendText({
671
- chat: blueBubblesChatFor(configResult.config),
672
- text: reportText,
673
- tempGuid: outboundDecision.idempotencyKey,
674
- });
675
- delivery = normalizeDelivery(sent);
724
+ const client = (0, client_1.createBlueBubblesClient)(bluebubbles.config, bluebubbles.channelConfig);
725
+ sendBoundary = await (0, outbound_state_1.executeRsvpSendBoundary)({
726
+ agentRoot,
727
+ habitId: command.habitName,
728
+ outboundIdempotencyKey: outboundDecision.idempotencyKey,
729
+ noSend: hardNoSend,
730
+ invokeTransport: async (markTransportInvoked) => client.sendText({
731
+ chat: blueBubblesChatFor(configResult.config),
732
+ text: reportText,
733
+ tempGuid: outboundDecision.idempotencyKey,
734
+ onTransportInvocation: () => {
735
+ transportCounter.count += 1;
736
+ markTransportInvoked();
737
+ },
738
+ }),
739
+ }, deps.rsvpSendBoundaryDeps);
740
+ const cleanupFailed = sendBoundary.errorCode === "lifecycle_lock_release_failed";
741
+ const acceptedProof = sendBoundary.boundaryState === "crossed" && !cleanupFailed;
742
+ sendBoundaryMessage = cleanupFailed
743
+ ? "RSVP transport crossed but lifecycle lock cleanup failed"
744
+ : sendBoundary.boundaryState === "crossed"
745
+ ? "RSVP refresh completed"
746
+ : sendBoundary.boundaryState === "not_crossed"
747
+ ? "RSVP transport was definitively rejected"
748
+ : "RSVP transport delivery is unknown";
749
+ delivery = acceptedProof
750
+ ? {
751
+ status: "accepted",
752
+ ...normalizeDelivery({ messageGuid: sendBoundary.transportResult.messageGuid }),
753
+ }
754
+ : cleanupFailed
755
+ ? {
756
+ status: "failed",
757
+ error: sendBoundaryMessage,
758
+ ...normalizeDelivery({ messageGuid: sendBoundary.transportResult.messageGuid }),
759
+ }
760
+ : sendBoundary.boundaryState === "not_crossed"
761
+ ? { status: "failed", error: sendBoundaryMessage }
762
+ : { status: "pending-manual-verification", error: sendBoundaryMessage };
676
763
  (0, outbound_state_1.recordRsvpOutboundAttempt)({
677
764
  agentRoot,
678
765
  currentSnapshot,
679
766
  reportText,
680
767
  bluebubblesRecord: {
681
768
  recordId: outboundDecision.idempotencyKey,
682
- status: "accepted",
769
+ status: acceptedProof
770
+ ? "accepted"
771
+ : sendBoundary.boundaryState === "not_crossed" || cleanupFailed
772
+ ? "failed"
773
+ : "pending-manual-verification",
683
774
  tempGuid: outboundDecision.idempotencyKey,
684
- ...(delivery.guid ? { messageGuid: String(delivery.guid) } : {}),
775
+ ...(sendBoundary.transportResult.messageGuid
776
+ ? { messageGuid: sendBoundary.transportResult.messageGuid }
777
+ : {}),
685
778
  },
686
779
  recordedAt: new Date().toISOString(),
687
780
  });
688
781
  }
689
- return basePayload(command, sendAllowed, "RSVP refresh completed", {
690
- allowSend: sendAllowed,
782
+ const message = sendBoundaryMessage ?? "RSVP refresh completed";
783
+ const payload = basePayload(command, sendBoundary?.transportInvoked === true && sendBoundary.replayed === false, message, {
784
+ allowSend: command.mode === "live" && command.allowSend === true,
691
785
  sendAllowed,
786
+ ...(sendBoundary ? { sendBoundary: sendBoundary } : {}),
692
787
  refresh: {
693
788
  snapshotId: currentSnapshot.snapshotId,
694
789
  reportText,
@@ -696,6 +791,17 @@ async function executeRefresh(command, deps) {
696
791
  ...(delivery ? { delivery } : {}),
697
792
  },
698
793
  });
794
+ return sendBoundary && !sendBoundary.ok ? { ...payload, ok: false } : payload;
795
+ }
796
+ async function executeRefresh(command, deps) {
797
+ const transportCounter = { count: 0 };
798
+ const hardNoSend = command.noSend === true;
799
+ const payload = await executeRefreshInner(command, deps, transportCounter, hardNoSend);
800
+ return {
801
+ ...payload,
802
+ ...(hardNoSend ? { noSend: true } : {}),
803
+ transportInvocationCount: transportCounter.count,
804
+ };
699
805
  }
700
806
  async function executeCompare(command) {
701
807
  if (!fs.existsSync(command.nativePath) || !fs.existsSync(command.legacyPath)) {
@@ -118,15 +118,24 @@ function habitScheduleHealth(agentRoot, deps) {
118
118
  try {
119
119
  const rsvpHabits = deps.readdirSync(habitsDir)
120
120
  .filter((name) => name.endsWith(".md") && (0, habit_policy_1.isRsvpHabitName)(name.replace(/\.md$/, "")))
121
+ .sort((left, right) => left.localeCompare(right))
121
122
  .map((name) => {
122
123
  const filePath = path.join(habitsDir, name);
123
124
  return (0, habit_parser_1.parseHabitFile)(deps.readFileSync(filePath), filePath);
124
125
  });
126
+ const habitStates = rsvpHabits.map((habit) => ({
127
+ name: habit.name,
128
+ status: habit.status,
129
+ degradedReason: habit.status === "degraded" ? habit.degradedReason : null,
130
+ }));
131
+ if (rsvpHabits.some((habit) => habit.status === "degraded")) {
132
+ return { status: "fail", detail: "RSVP habit definitions degraded", habitStates };
133
+ }
125
134
  const active = rsvpHabits.find((habit) => habit.status === "active");
126
135
  if (!active)
127
- return { status: "warn", detail: "no active RSVP habit found" };
136
+ return { status: "warn", detail: "no active RSVP habit found", habitStates };
128
137
  if (!active.rsvp)
129
- return { status: "fail", detail: `activeHabit=${active.name}; typed RSVP habit metadata missing` };
138
+ return { status: "fail", detail: `activeHabit=${active.name}; typed RSVP habit metadata missing`, habitStates };
130
139
  const cadence = active.cadence ?? "unspecified";
131
140
  return {
132
141
  status: "pass",
@@ -147,6 +156,7 @@ function habitScheduleHealth(agentRoot, deps) {
147
156
  outboundStateRef: active.rsvp.outboundStateRef,
148
157
  budgetRef: active.rsvp.budgetRef,
149
158
  idempotencyRef: active.rsvp.idempotencyRef,
159
+ habitStates,
150
160
  };
151
161
  }
152
162
  catch (error) {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RSVP_HABIT_ALLOWED_TOOLS = exports.DEFAULT_RSVP_HABIT_NAME = exports.RSVP_HABIT_POLICY_VERSION = void 0;
4
4
  exports.isRsvpHabitName = isRsvpHabitName;
5
+ exports.rsvpHabitMetadataErrorDetail = rsvpHabitMetadataErrorDetail;
5
6
  exports.parseRsvpHabitMetadata = parseRsvpHabitMetadata;
6
7
  exports.rsvpHabitRuntimePolicy = rsvpHabitRuntimePolicy;
7
8
  const runtime_1 = require("../nerves/runtime");
@@ -35,6 +36,12 @@ function optionalString(record, key) {
35
36
  function isRsvpHabitName(name) {
36
37
  return name.startsWith("rsvp-");
37
38
  }
39
+ function rsvpHabitMetadataErrorDetail(error) {
40
+ if (error instanceof Error && error.message.startsWith("RSVP habit metadata ")) {
41
+ return error.message;
42
+ }
43
+ return "RSVP habit metadata is invalid";
44
+ }
38
45
  function parseRsvpHabitMetadata(raw) {
39
46
  if (raw === undefined || raw === null)
40
47
  return null;
@@ -33,14 +33,24 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.RsvpHabitStageError = void 0;
36
37
  exports.stageRsvpHabit = stageRsvpHabit;
37
- const fs = __importStar(require("node:fs"));
38
38
  const path = __importStar(require("node:path"));
39
+ const habit_lifecycle_1 = require("../heart/habits/habit-lifecycle");
39
40
  const habit_parser_1 = require("../heart/habits/habit-parser");
40
41
  const runtime_1 = require("../nerves/runtime");
41
42
  const outbound_state_1 = require("./outbound-state");
42
43
  const spend_ledger_1 = require("./spend-ledger");
43
44
  const habit_policy_1 = require("./habit-policy");
45
+ class RsvpHabitStageError extends Error {
46
+ code;
47
+ constructor(code) {
48
+ super(code);
49
+ this.name = "RsvpHabitStageError";
50
+ this.code = code;
51
+ }
52
+ }
53
+ exports.RsvpHabitStageError = RsvpHabitStageError;
44
54
  function defaultRsvpHabitMetadata(mode, reportTitle) {
45
55
  return {
46
56
  policyVersion: habit_policy_1.RSVP_HABIT_POLICY_VERSION,
@@ -69,7 +79,7 @@ function normalizeHabitName(name) {
69
79
  }
70
80
  return normalized;
71
81
  }
72
- function stageRsvpHabit(input) {
82
+ async function stageRsvpHabit(input, deps = {}) {
73
83
  const habitName = normalizeHabitName(input.habitName);
74
84
  const title = input.title?.trim() || "RSVP Updates";
75
85
  const rsvp = defaultRsvpHabitMetadata(input.mode, input.reportTitle?.trim() || title);
@@ -84,8 +94,13 @@ function stageRsvpHabit(input) {
84
94
  continuity: { mode: "stateful" },
85
95
  rsvp,
86
96
  }, stagedHabitBody());
87
- fs.mkdirSync(path.dirname(habitPath), { recursive: true });
88
- fs.writeFileSync(habitPath, content, "utf-8");
97
+ const publication = (0, habit_lifecycle_1.publishNewHabitDefinition)({
98
+ agentRoot: input.agentRoot,
99
+ habitId: habitName,
100
+ bytes: content,
101
+ }, deps.lifecycle);
102
+ if (publication === "exists")
103
+ throw new RsvpHabitStageError("habit_stage_exists");
89
104
  (0, outbound_state_1.ensureRsvpOutboundState)(input.agentRoot, created);
90
105
  (0, spend_ledger_1.ensureRsvpSpendLedger)(input.agentRoot, created);
91
106
  (0, runtime_1.emitNervesEvent)({
@@ -61,6 +61,47 @@ function nativeRunId(input) {
61
61
  })).slice(0, 24);
62
62
  return `rsvp-${input.trigger}-${digest}`;
63
63
  }
64
+ function rejectionErrorCode(habit) {
65
+ return habit.status === "degraded"
66
+ ? `habit_${habit.degradedReason}`
67
+ : `habit_status_${habit.status}`;
68
+ }
69
+ function rejectionMessage(habitName, habit) {
70
+ const reason = habit.status === "degraded" ? ` reason=${habit.degradedReason}` : "";
71
+ return `native RSVP habit ${habitName} rejected by lifecycle: status=${habit.status}${reason}`;
72
+ }
73
+ function resolveNativeRsvpHabit(agentRoot, habitPath, habitName) {
74
+ let habit;
75
+ if (!fs.existsSync(habitPath)) {
76
+ habit = (0, habit_parser_1.createDegradedHabitFile)(habitPath, "read_error", "", "habit file not found");
77
+ }
78
+ else {
79
+ try {
80
+ habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
81
+ }
82
+ catch (error) {
83
+ habit = (0, habit_parser_1.createDegradedHabitFile)(habitPath, "read_error", "", String(error));
84
+ }
85
+ }
86
+ if (habit.status !== "active") {
87
+ return {
88
+ kind: "rejected",
89
+ habit,
90
+ errorCode: rejectionErrorCode(habit),
91
+ message: rejectionMessage(habitName, habit),
92
+ };
93
+ }
94
+ if (!habit.rsvp) {
95
+ const degraded = (0, habit_parser_1.createDegradedHabitFile)(habitPath, "invalid_metadata", habit.body, "RSVP habit metadata is required");
96
+ return {
97
+ kind: "rejected",
98
+ habit: degraded,
99
+ errorCode: rejectionErrorCode(degraded),
100
+ message: rejectionMessage(habitName, degraded),
101
+ };
102
+ }
103
+ return { kind: "ready", habit: habit };
104
+ }
64
105
  function noopCliDeps(input, agentRoot) {
65
106
  return {
66
107
  socketPath: "",
@@ -92,7 +133,8 @@ function refreshCommandFor(input, sendAllowed, mode) {
92
133
  habitName: input.habitName,
93
134
  mode,
94
135
  json: true,
95
- ...(sendAllowed ? { allowSend: true } : { noSend: true }),
136
+ ...(sendAllowed ? { allowSend: true } : {}),
137
+ ...(input.noSend || !sendAllowed ? { noSend: true } : {}),
96
138
  };
97
139
  }
98
140
  function permissionEnvelope(sendAllowed) {
@@ -139,6 +181,16 @@ function surfaceAttemptsFor(payload, requestedSend, errorMessage) {
139
181
  if (payload.sendAllowed === true) {
140
182
  const delivery = (refreshRecord(payload)?.delivery ?? {});
141
183
  const rawStatus = stringField(delivery, "status") ?? stringField(delivery, "result");
184
+ if (rawStatus === "pending-manual-verification") {
185
+ return [{
186
+ recipient: "rsvp",
187
+ channel: "bluebubbles",
188
+ reason: "status",
189
+ result: "unavailable",
190
+ rawStatus,
191
+ error: "RSVP transport delivery is unknown",
192
+ }];
193
+ }
142
194
  if (rawStatus === "failed" || rawStatus === "error") {
143
195
  return [{
144
196
  recipient: "rsvp",
@@ -327,6 +379,8 @@ function recordNativeRunLedger(input) {
327
379
  ...(input.lifecycle === "completed" || input.lifecycle === "error"
328
380
  ? { usage: (0, run_ledger_1.usageMetadataFromUsageData)(undefined, "none") }
329
381
  : {}),
382
+ ...(input.errorName ? { errorName: input.errorName } : {}),
383
+ ...(input.errorCode ? { errorCode: input.errorCode } : {}),
330
384
  target,
331
385
  idempotencyScope: target,
332
386
  sessionRef: {
@@ -365,12 +419,6 @@ async function runNativeRsvpHabit(input) {
365
419
  runId,
366
420
  },
367
421
  });
368
- const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
369
- if (!habit.rsvp)
370
- throw new Error(`RSVP habit metadata is required before native execution: ${input.habitName}`);
371
- const policy = (0, habit_policy_1.rsvpHabitRuntimePolicy)(habit.rsvp);
372
- const command = refreshCommandFor(input, policy.sendAllowed, habit.rsvp.mode);
373
- const deps = noopCliDeps(input, agentRoot);
374
422
  recordNativeRunLedger({
375
423
  agentRoot,
376
424
  agent: input.agent,
@@ -381,11 +429,102 @@ async function runNativeRsvpHabit(input) {
381
429
  startedAt,
382
430
  lifecycle: "started",
383
431
  });
432
+ const resolution = resolveNativeRsvpHabit(agentRoot, habitPath, input.habitName);
433
+ if (resolution.kind === "rejected") {
434
+ const endedAt = nowIso(input.now);
435
+ const degradedReason = resolution.habit.status === "degraded"
436
+ ? resolution.habit.degradedReason
437
+ : null;
438
+ const degradedDetail = resolution.habit.status === "degraded"
439
+ ? resolution.habit.degradedDetail
440
+ : null;
441
+ const payload = {
442
+ ok: false,
443
+ command: "rsvp.refresh",
444
+ sideEffect: false,
445
+ agent: input.agent,
446
+ requires: "active RSVP habit",
447
+ message: resolution.message,
448
+ status: resolution.habit.status,
449
+ degradedReason,
450
+ degradedDetail,
451
+ };
452
+ recordNativeRunLedger({
453
+ agentRoot,
454
+ agent: input.agent,
455
+ habitName: input.habitName,
456
+ runId,
457
+ trigger: input.trigger,
458
+ occurrenceId: input.occurrenceId,
459
+ startedAt,
460
+ endedAt,
461
+ lifecycle: "error",
462
+ errorName: "HabitLifecycleRejected",
463
+ errorCode: resolution.errorCode,
464
+ });
465
+ const rejectionMeta = {
466
+ entryPoint: "native_runner",
467
+ agent: input.agent,
468
+ habitName: input.habitName,
469
+ trigger: input.trigger,
470
+ occurrenceId: input.occurrenceId ?? null,
471
+ runId,
472
+ status: resolution.habit.status,
473
+ degradedReason,
474
+ degradedDetail,
475
+ errorCode: resolution.errorCode,
476
+ };
477
+ (0, runtime_1.emitNervesEvent)({
478
+ level: "warn",
479
+ component: "rsvp",
480
+ event: "rsvp.habit_lifecycle_rejected",
481
+ message: resolution.message,
482
+ meta: rejectionMeta,
483
+ });
484
+ (0, runtime_1.emitNervesEvent)({
485
+ level: "error",
486
+ component: "rsvp",
487
+ event: "rsvp.native_habit_error",
488
+ message: resolution.message,
489
+ meta: {
490
+ ...rejectionMeta,
491
+ lifecycle: "error",
492
+ receiptWritten: false,
493
+ runtimeStateRecorded: false,
494
+ },
495
+ });
496
+ return {
497
+ ok: false,
498
+ message: resolution.message,
499
+ lifecycle: "error",
500
+ runId,
501
+ payload,
502
+ };
503
+ }
504
+ const habit = resolution.habit;
505
+ const policy = (0, habit_policy_1.rsvpHabitRuntimePolicy)(habit.rsvp);
506
+ const effectiveSendAllowed = policy.sendAllowed && input.noSend !== true;
507
+ const command = refreshCommandFor(input, policy.sendAllowed, habit.rsvp.mode);
508
+ const deps = noopCliDeps(input, agentRoot);
384
509
  let payload;
385
510
  let errorMessage = null;
386
511
  try {
387
512
  payload = parseRefreshPayload(await (input.runRefresh ?? defaultRunRefresh)(command, deps));
513
+ payload = { ...payload, status: habit.status };
388
514
  errorMessage = payloadError(payload);
515
+ if (input.noSend === true
516
+ && (payload.noSend !== true
517
+ || payload.transportInvocationCount !== 0
518
+ || payload.sendAllowed === true
519
+ || payload.sideEffect === true)) {
520
+ errorMessage = "RSVP refresh violated the immutable no-send result contract";
521
+ payload = {
522
+ ...payload,
523
+ ok: false,
524
+ status: habit.status,
525
+ message: errorMessage,
526
+ };
527
+ }
389
528
  }
390
529
  catch (error) {
391
530
  payload = {
@@ -394,13 +533,14 @@ async function runNativeRsvpHabit(input) {
394
533
  sideEffect: false,
395
534
  agent: input.agent,
396
535
  mode: habit.rsvp.mode,
536
+ status: habit.status,
397
537
  message: error instanceof Error ? error.message : String(error),
398
538
  };
399
539
  errorMessage = stringField(payload, "message") ?? "RSVP refresh failed";
400
540
  }
401
541
  const endedAt = nowIso(input.now);
402
542
  const lifecycle = errorMessage ? "error" : "completed";
403
- const surfaceAttempts = surfaceAttemptsFor(payload, policy.sendAllowed, errorMessage);
543
+ const surfaceAttempts = surfaceAttemptsFor(payload, effectiveSendAllowed, errorMessage);
404
544
  const errors = errorMessage ? [errorMessage] : [];
405
545
  const producedRefs = producedRefsFor(payload);
406
546
  recordNativeRunLedger({
@@ -419,7 +559,7 @@ async function runNativeRsvpHabit(input) {
419
559
  trigger: input.trigger,
420
560
  mode: habit.rsvp.mode,
421
561
  source: habit.rsvp.source,
422
- sendAllowed: policy.sendAllowed,
562
+ sendAllowed: effectiveSendAllowed,
423
563
  startedAt,
424
564
  endedAt,
425
565
  payload,
@@ -436,8 +576,8 @@ async function runNativeRsvpHabit(input) {
436
576
  startedAt,
437
577
  endedAt,
438
578
  operationId: `rsvp-native:${input.trigger}:${input.occurrenceId ?? runId}`,
439
- permissionEnvelope: permissionEnvelope(policy.sendAllowed),
440
- toolPolicy: toolPolicy(policy.sendAllowed),
579
+ permissionEnvelope: permissionEnvelope(effectiveSendAllowed),
580
+ toolPolicy: toolPolicy(effectiveSendAllowed),
441
581
  producedRefs,
442
582
  surfaceAttempts,
443
583
  traceSteps,