@ouro.bot/cli 0.1.0-alpha.775 → 0.1.0-alpha.777

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.
@@ -45,6 +45,160 @@ const presence_1 = require("../arc/presence");
45
45
  const intentions_1 = require("../arc/intentions");
46
46
  const steward_policy_1 = require("../heart/steward-policy");
47
47
  const await_parser_1 = require("../heart/awaiting/await-parser");
48
+ const externalEventDispositionProperties = {
49
+ recordPath: { type: "string", description: "Exact receipt path from the external-event message" },
50
+ expectedGeneration: { type: "number", description: "Exact generation shown in the external-event turn" },
51
+ classifiedRevision: { type: "string", description: "Exact observation revision investigated in this turn" },
52
+ classification: { type: "string", enum: ["expected", "needs_attention", "adopted", "snoozed", "dismissed_until_change", "resolved"] },
53
+ stewardPolicyKind: { type: "string", enum: ["current", "none"], description: "Use current with the exact live policy key/version, or none only for a fresh observation with no applicable policy" },
54
+ stewardPolicyKey: { type: "string", description: "Exact current policy key used for this decision" },
55
+ stewardPolicyVersion: { type: "number", description: "Exact current policy version used for this decision" },
56
+ decision: { type: "string", enum: ["silent", "act", "ask", "report"] },
57
+ reason: { type: "string", description: "Short plain-language reason for the decision" },
58
+ nextWake: { type: "string", enum: ["on_change", "on_escalation", "on_recovery", "at"] },
59
+ wakeAt: { type: "string", description: "ISO time required when nextWake=at" },
60
+ awaitId: { type: "string", description: "Existing await receipt required when nextWake=at" },
61
+ careId: { type: "string", description: "Existing Care adopted for this incident, if any" },
62
+ actionRefs: { type: "array", items: { type: "string" } },
63
+ verificationRefs: { type: "array", items: { type: "string" } },
64
+ };
65
+ const externalEventDispositionRequired = ["recordPath", "expectedGeneration", "classifiedRevision", "classification", "stewardPolicyKind", "decision", "reason", "nextWake"];
66
+ function prepareExternalEventDisposition(a, ctx) {
67
+ const agentName = (0, identity_1.getAgentName)();
68
+ const agentEventRoot = path.resolve((0, router_1.getExternalEventRoot)(), agentName);
69
+ const recordPath = path.resolve(String(a.recordPath ?? ""));
70
+ const relative = path.relative(agentEventRoot, recordPath);
71
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative) || !recordPath.endsWith(".json")) {
72
+ throw new Error("External event receipt does not belong to the current agent");
73
+ }
74
+ const record = (0, router_1.readExternalEventRecord)(recordPath);
75
+ if (record.agent !== agentName)
76
+ throw new Error("External event receipt does not belong to the current agent");
77
+ const turnContext = ctx?.currentExternalEvent;
78
+ const turnEvent = turnContext
79
+ ? [turnContext, ...(turnContext.relatedEvents ?? [])].find((event) => path.resolve(event.recordPath) === recordPath)
80
+ : undefined;
81
+ const expectedGeneration = Number(a.expectedGeneration);
82
+ const classifiedRevision = String(a.classifiedRevision ?? "");
83
+ if (!turnEvent || turnEvent.recordPath !== recordPath || turnEvent.agent !== agentName
84
+ || turnEvent.generation !== expectedGeneration || turnEvent.observationRevision !== classifiedRevision
85
+ || record.generation !== expectedGeneration || record.observationRevision !== classifiedRevision
86
+ || record.executionState !== "running" || record.claimOwner !== turnEvent.claimOwner) {
87
+ throw new Error("External event disposition is not authorized for this exact turn lease");
88
+ }
89
+ const classification = String(a.classification);
90
+ const decision = String(a.decision);
91
+ const nextWake = String(a.nextWake);
92
+ const policyVersion = Number(a.stewardPolicyVersion);
93
+ const policyKind = String(a.stewardPolicyKind ?? "");
94
+ const reason = String(a.reason ?? "").trim();
95
+ if (!reason || (policyKind !== "none" && (!Number.isSafeInteger(policyVersion) || policyVersion < 1))
96
+ || !["current", "none"].includes(policyKind)
97
+ || !["expected", "needs_attention", "adopted", "snoozed", "dismissed_until_change", "resolved"].includes(classification)
98
+ || !["silent", "act", "ask", "report"].includes(decision)
99
+ || !["on_change", "on_escalation", "on_recovery", "at"].includes(nextWake)) {
100
+ throw new Error("External event disposition is invalid");
101
+ }
102
+ const wake = nextWake === "at" ? { kind: "at", at: String(a.wakeAt ?? "") } : { kind: nextWake };
103
+ const actionRefs = Array.isArray(a.actionRefs) ? a.actionRefs.map(String) : [];
104
+ const verificationRefs = Array.isArray(a.verificationRefs) ? a.verificationRefs.map(String) : [];
105
+ const agentRoot = (0, identity_1.getAgentRoot)();
106
+ const policy = (0, steward_policy_1.readStewardPolicy)(agentRoot);
107
+ let stewardPolicy;
108
+ if (policyKind === "none") {
109
+ if (record.transition !== "opened")
110
+ throw new Error("External event no-policy disposition requires a fresh observation");
111
+ stewardPolicy = { kind: "none" };
112
+ }
113
+ else {
114
+ const key = String(a.stewardPolicyKey ?? "").trim();
115
+ if (!key)
116
+ throw new Error("External event disposition is invalid");
117
+ if (policy.version !== policyVersion || (!policy.desiredStates[key] && !policy.routineActionGrants[key])) {
118
+ throw new Error("External event steward policy is not the exact current key/version");
119
+ }
120
+ stewardPolicy = { kind: "current", key, version: policyVersion };
121
+ }
122
+ if (typeof a.careId === "string" && a.careId) {
123
+ const care = (0, cares_1.readCares)(agentRoot).find((candidate) => candidate.id === a.careId);
124
+ const binding = care?.incidentBindings?.find((candidate) => candidate.source === record.source && candidate.incidentKey === record.eventId && candidate.classifiedRevision === classifiedRevision);
125
+ if (!care || !binding)
126
+ throw new Error("External event Care does not belong to this agent and incident revision");
127
+ }
128
+ if (classification === "adopted" && !(typeof a.careId === "string" && a.careId)) {
129
+ throw new Error("External event adopted disposition requires a Care");
130
+ }
131
+ if (nextWake === "at") {
132
+ const awaitId = typeof a.awaitId === "string" ? a.awaitId : "";
133
+ if (!/^[A-Za-z0-9_-]+$/u.test(awaitId))
134
+ throw new Error("External event timed disposition requires a current pending Await");
135
+ const awaitPath = path.join(agentRoot, "awaiting", `${awaitId}.md`);
136
+ let pendingAwait;
137
+ try {
138
+ const stat = fs.lstatSync(awaitPath);
139
+ if (!stat.isFile() || stat.isSymbolicLink())
140
+ throw new Error("unsafe Await");
141
+ pendingAwait = (0, await_parser_1.parseAwaitFile)(fs.readFileSync(awaitPath, "utf8"), awaitPath);
142
+ }
143
+ catch {
144
+ throw new Error("External event timed disposition requires a current pending Await");
145
+ }
146
+ const expectedOwnerId = ctx?.context?.friend.id;
147
+ if (pendingAwait.filed_from !== "external-event" || pendingAwait.filed_from_key !== record.recordPath
148
+ || (expectedOwnerId && pendingAwait.filed_for_friend_id !== expectedOwnerId)) {
149
+ throw new Error("External event timed disposition Await is not owned by this exact external event");
150
+ }
151
+ if (pendingAwait.status !== "pending" || pendingAwait.wake_at !== String(a.wakeAt)) {
152
+ throw new Error("External event timed disposition Await does not match the exact wake time");
153
+ }
154
+ }
155
+ const authority = ctx?.externalEventAuthority?.authorizeDisposition({
156
+ event: turnEvent,
157
+ classification,
158
+ decision,
159
+ stewardPolicy,
160
+ nextWake,
161
+ wakeAt: nextWake === "at" ? String(a.wakeAt) : null,
162
+ awaitId: typeof a.awaitId === "string" && a.awaitId ? a.awaitId : null,
163
+ careId: typeof a.careId === "string" && a.careId ? a.careId : null,
164
+ actionRefs,
165
+ verificationRefs,
166
+ });
167
+ if (!authority?.allowed)
168
+ throw new Error(`External event disposition authority denied: ${authority?.reason ?? "authority unavailable"}`);
169
+ return {
170
+ recordPath,
171
+ finish: async () => {
172
+ if (decision === "ask" || decision === "report") {
173
+ if (Buffer.byteLength(reason, "utf8") > 1_200)
174
+ throw new Error("External event owner message must be phone-sized");
175
+ if (!ctx?.externalEventEffects)
176
+ throw new Error("External event owner delivery is unavailable");
177
+ await ctx.externalEventEffects.deliverOwnerDecision({ source: record.source, eventId: record.eventId, generation: record.generation, text: reason });
178
+ }
179
+ const handled = (0, router_1.commitExternalEventDisposition)(recordPath, {
180
+ owner: turnEvent.claimOwner,
181
+ expectedVersion: record.version,
182
+ expectedGeneration: record.generation,
183
+ disposition: {
184
+ classifiedRevision,
185
+ classification: classification,
186
+ stewardPolicy,
187
+ decision: decision,
188
+ reason,
189
+ nextWake: wake,
190
+ careId: typeof a.careId === "string" && a.careId ? a.careId : null,
191
+ awaitId: typeof a.awaitId === "string" && a.awaitId ? a.awaitId : null,
192
+ actionRefs,
193
+ verificationRefs,
194
+ },
195
+ });
196
+ ctx?.externalEventAuthority?.recordCommittedDisposition?.(turnEvent);
197
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.external_event_disposition", message: "external event disposition recorded", meta: { agentName, eventId: record.eventId, generation: record.generation, classification, decision } });
198
+ return handled;
199
+ },
200
+ };
201
+ }
48
202
  exports.continuityToolDefinitions = [
49
203
  // ── Continuity tools ──────────────────────────────────────────────
50
204
  {
@@ -56,159 +210,58 @@ exports.continuityToolDefinitions = [
56
210
  parameters: {
57
211
  type: "object",
58
212
  properties: {
59
- recordPath: { type: "string", description: "Exact receipt path from the external-event message" },
60
- expectedGeneration: { type: "number", description: "Exact generation shown in the external-event turn" },
61
- classifiedRevision: { type: "string", description: "Exact observation revision investigated in this turn" },
62
- classification: { type: "string", enum: ["expected", "needs_attention", "adopted", "snoozed", "dismissed_until_change", "resolved"] },
63
- stewardPolicyKind: { type: "string", enum: ["current", "none"], description: "Use current with the exact live policy key/version, or none only for a fresh observation with no applicable policy" },
64
- stewardPolicyKey: { type: "string", description: "Exact current policy key used for this decision" },
65
- stewardPolicyVersion: { type: "number", description: "Exact current policy version used for this decision" },
66
- decision: { type: "string", enum: ["silent", "act", "ask", "report"] },
67
- reason: { type: "string", description: "Short plain-language reason for the decision" },
68
- nextWake: { type: "string", enum: ["on_change", "on_escalation", "on_recovery", "at"] },
69
- wakeAt: { type: "string", description: "ISO time required when nextWake=at" },
70
- awaitId: { type: "string", description: "Existing await receipt required when nextWake=at" },
71
- careId: { type: "string", description: "Existing Care adopted for this incident, if any" },
72
- actionRefs: { type: "array", items: { type: "string" } },
73
- verificationRefs: { type: "array", items: { type: "string" } },
213
+ ...externalEventDispositionProperties,
214
+ batch: {
215
+ type: "array",
216
+ minItems: 1,
217
+ maxItems: 32,
218
+ description: "All exact leases from one coalesced external-event turn",
219
+ items: { type: "object", properties: externalEventDispositionProperties, required: externalEventDispositionRequired, additionalProperties: false },
220
+ },
74
221
  },
75
- required: ["recordPath", "expectedGeneration", "classifiedRevision", "classification", "stewardPolicyKind", "decision", "reason", "nextWake"],
222
+ oneOf: [
223
+ { required: externalEventDispositionRequired },
224
+ { required: ["batch"] },
225
+ ],
226
+ additionalProperties: false,
76
227
  },
77
228
  },
78
229
  },
79
230
  handler: (a, ctx) => {
80
- const agentName = (0, identity_1.getAgentName)();
81
- const agentEventRoot = path.resolve((0, router_1.getExternalEventRoot)(), agentName);
82
- const recordPath = path.resolve(String(a.recordPath ?? ""));
83
- const relative = path.relative(agentEventRoot, recordPath);
84
- if (!relative || relative.startsWith("..") || path.isAbsolute(relative) || !recordPath.endsWith(".json")) {
85
- throw new Error("External event receipt does not belong to the current agent");
86
- }
87
- const record = (0, router_1.readExternalEventRecord)(recordPath);
88
- if (record.agent !== agentName)
89
- throw new Error("External event receipt does not belong to the current agent");
90
- const turnContext = ctx?.currentExternalEvent;
91
- const turnEvent = turnContext
92
- ? [turnContext, ...(turnContext.relatedEvents ?? [])].find((event) => path.resolve(event.recordPath) === recordPath)
93
- : undefined;
94
- const expectedGeneration = Number(a.expectedGeneration);
95
- const classifiedRevision = String(a.classifiedRevision ?? "");
96
- if (!turnEvent || turnEvent.recordPath !== recordPath || turnEvent.agent !== agentName
97
- || turnEvent.generation !== expectedGeneration || turnEvent.observationRevision !== classifiedRevision
98
- || record.generation !== expectedGeneration || record.observationRevision !== classifiedRevision
99
- || record.executionState !== "running" || record.claimOwner !== turnEvent.claimOwner) {
100
- throw new Error("External event disposition is not authorized for this exact turn lease");
101
- }
102
- const classification = String(a.classification);
103
- const decision = String(a.decision);
104
- const nextWake = String(a.nextWake);
105
- const policyVersion = Number(a.stewardPolicyVersion);
106
- const policyKind = String(a.stewardPolicyKind ?? "");
107
- const reason = String(a.reason ?? "").trim();
108
- if (!reason || (policyKind !== "none" && (!Number.isSafeInteger(policyVersion) || policyVersion < 1))
109
- || !["current", "none"].includes(policyKind)
110
- || !["expected", "needs_attention", "adopted", "snoozed", "dismissed_until_change", "resolved"].includes(classification)
111
- || !["silent", "act", "ask", "report"].includes(decision)
112
- || !["on_change", "on_escalation", "on_recovery", "at"].includes(nextWake)) {
113
- throw new Error("External event disposition is invalid");
114
- }
115
- const wake = nextWake === "at" ? { kind: "at", at: String(a.wakeAt ?? "") } : { kind: nextWake };
116
- const actionRefs = Array.isArray(a.actionRefs) ? a.actionRefs.map(String) : [];
117
- const verificationRefs = Array.isArray(a.verificationRefs) ? a.verificationRefs.map(String) : [];
118
- const agentRoot = (0, identity_1.getAgentRoot)();
119
- const policy = (0, steward_policy_1.readStewardPolicy)(agentRoot);
120
- let stewardPolicy;
121
- if (policyKind === "none") {
122
- if (record.transition !== "opened")
123
- throw new Error("External event no-policy disposition requires a fresh observation");
124
- stewardPolicy = { kind: "none" };
125
- }
126
- else {
127
- const key = String(a.stewardPolicyKey ?? "").trim();
128
- if (!key)
129
- throw new Error("External event disposition is invalid");
130
- if (policy.version !== policyVersion || (!policy.desiredStates[key] && !policy.routineActionGrants[key])) {
131
- throw new Error("External event steward policy is not the exact current key/version");
231
+ const input = a;
232
+ if (input.batch !== undefined) {
233
+ if (Object.keys(input).some((key) => key !== "batch"))
234
+ throw new Error("External event disposition single and batch forms are mutually exclusive");
235
+ if (!Array.isArray(input.batch) || input.batch.length < 1 || input.batch.length > 32)
236
+ throw new Error("External event disposition batch must contain 1 to 32 items");
237
+ if (input.batch.some((item) => !item || typeof item !== "object" || Array.isArray(item)))
238
+ throw new Error("External event disposition batch item is invalid");
239
+ const recordPaths = input.batch.map((item) => path.resolve(String(item.recordPath ?? "")));
240
+ if (new Set(recordPaths).size !== recordPaths.length)
241
+ throw new Error("External event disposition batch contains a duplicate receipt");
242
+ const activeFrame = ctx?.currentExternalEvent
243
+ ? [ctx.currentExternalEvent, ...(ctx.currentExternalEvent.relatedEvents ?? [])]
244
+ : [];
245
+ const activeRecordPaths = new Set(activeFrame.map((event) => path.resolve(event.recordPath)));
246
+ if (activeRecordPaths.size !== recordPaths.length || recordPaths.some((recordPath) => !activeRecordPaths.has(recordPath))) {
247
+ throw new Error("External event disposition batch must contain every lease in the active turn frame");
132
248
  }
133
- stewardPolicy = { kind: "current", key, version: policyVersion };
249
+ const prepared = input.batch.map((item) => prepareExternalEventDisposition(item, ctx));
250
+ return (async () => {
251
+ const results = [];
252
+ for (const item of prepared) {
253
+ try {
254
+ results.push({ recordPath: item.recordPath, ok: true, record: await item.finish() });
255
+ }
256
+ catch (error) {
257
+ results.push({ recordPath: item.recordPath, ok: false, error: (error instanceof Error ? error.message : String(error)).slice(0, 500) });
258
+ }
259
+ }
260
+ return JSON.stringify({ results }, null, 2);
261
+ })();
134
262
  }
135
- if (typeof a.careId === "string" && a.careId) {
136
- const care = (0, cares_1.readCares)(agentRoot).find((candidate) => candidate.id === a.careId);
137
- const binding = care?.incidentBindings?.find((candidate) => candidate.source === record.source && candidate.incidentKey === record.eventId && candidate.classifiedRevision === classifiedRevision);
138
- if (!care || !binding)
139
- throw new Error("External event Care does not belong to this agent and incident revision");
140
- }
141
- if (classification === "adopted" && !(typeof a.careId === "string" && a.careId)) {
142
- throw new Error("External event adopted disposition requires a Care");
143
- }
144
- if (nextWake === "at") {
145
- const awaitId = typeof a.awaitId === "string" ? a.awaitId : "";
146
- if (!/^[A-Za-z0-9_-]+$/u.test(awaitId))
147
- throw new Error("External event timed disposition requires a current pending Await");
148
- const awaitPath = path.join(agentRoot, "awaiting", `${awaitId}.md`);
149
- let pendingAwait;
150
- try {
151
- const stat = fs.lstatSync(awaitPath);
152
- if (!stat.isFile() || stat.isSymbolicLink())
153
- throw new Error("unsafe Await");
154
- pendingAwait = (0, await_parser_1.parseAwaitFile)(fs.readFileSync(awaitPath, "utf8"), awaitPath);
155
- }
156
- catch {
157
- throw new Error("External event timed disposition requires a current pending Await");
158
- }
159
- const expectedOwnerId = ctx?.context?.friend.id;
160
- if (pendingAwait.filed_from !== "external-event" || pendingAwait.filed_from_key !== record.recordPath
161
- || (expectedOwnerId && pendingAwait.filed_for_friend_id !== expectedOwnerId)) {
162
- throw new Error("External event timed disposition Await is not owned by this exact external event");
163
- }
164
- if (pendingAwait.status !== "pending" || pendingAwait.wake_at !== String(a.wakeAt)) {
165
- throw new Error("External event timed disposition Await does not match the exact wake time");
166
- }
167
- }
168
- const authority = ctx?.externalEventAuthority?.authorizeDisposition({
169
- event: turnEvent,
170
- classification,
171
- decision,
172
- stewardPolicy,
173
- nextWake,
174
- wakeAt: nextWake === "at" ? String(a.wakeAt) : null,
175
- awaitId: typeof a.awaitId === "string" && a.awaitId ? a.awaitId : null,
176
- careId: typeof a.careId === "string" && a.careId ? a.careId : null,
177
- actionRefs,
178
- verificationRefs,
179
- });
180
- if (!authority?.allowed)
181
- throw new Error(`External event disposition authority denied: ${authority?.reason ?? "authority unavailable"}`);
182
- const finish = async () => {
183
- if (decision === "ask" || decision === "report") {
184
- if (Buffer.byteLength(reason, "utf8") > 1_200)
185
- throw new Error("External event owner message must be phone-sized");
186
- if (!ctx?.externalEventEffects)
187
- throw new Error("External event owner delivery is unavailable");
188
- await ctx.externalEventEffects.deliverOwnerDecision({ source: record.source, eventId: record.eventId, generation: record.generation, text: reason });
189
- }
190
- const handled = (0, router_1.commitExternalEventDisposition)(recordPath, {
191
- owner: turnEvent.claimOwner,
192
- expectedVersion: record.version,
193
- expectedGeneration: record.generation,
194
- disposition: {
195
- classifiedRevision,
196
- classification: classification,
197
- stewardPolicy,
198
- decision: decision,
199
- reason,
200
- nextWake: wake,
201
- careId: typeof a.careId === "string" && a.careId ? a.careId : null,
202
- awaitId: typeof a.awaitId === "string" && a.awaitId ? a.awaitId : null,
203
- actionRefs,
204
- verificationRefs,
205
- },
206
- });
207
- ctx?.externalEventAuthority?.recordCommittedDisposition?.(turnEvent);
208
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.external_event_disposition", message: "external event disposition recorded", meta: { agentName, eventId: record.eventId, generation: record.generation, classification, decision } });
209
- return JSON.stringify(handled, null, 2);
210
- };
211
- return finish();
263
+ const prepared = prepareExternalEventDisposition(input, ctx);
264
+ return prepared.finish().then((handled) => JSON.stringify(handled, null, 2));
212
265
  },
213
266
  riskProfile: { mutates: "durable_state_write", risk: "high", reason: "records the agent's classification on an existing external-event receipt" },
214
267
  },