@ouro.bot/cli 0.1.0-alpha.870 → 0.1.0-alpha.871

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.871",
6
+ "changes": [
7
+ "Restating an already-recorded steward desired state is now a no-op (no version bump, audit row or source rewrite) and the tool says unchanged; the replay gate's no-telegram check fails only on owner notices attributable to the replay run."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.870",
6
12
  "changes": [
@@ -86,14 +86,40 @@ export function isStalledQueueItem(item, nowMs = Date.now()) {
86
86
  return size > 0 && left > 0 && noEta && Number.isFinite(addedAt) && nowMs - addedAt >= STALL_MIN_AGE_HOURS * 3_600_000
87
87
  }
88
88
 
89
- /** Telegram effect records written during [start, end] that would have reached Ari's chat from a replay. */
90
- export function telegramLeaks(effects, start, end) {
89
+ /** Telegram effect records written during [start, end] that are owner notices, before attribution. */
90
+ export function ownerNoticeEffects(effects, start, end) {
91
91
  return effects.filter((effect) => {
92
92
  const at = Date.parse(effect.createdAt)
93
93
  return Number.isFinite(at) && at >= start && at <= end && String(effect.idempotencyKey ?? "").startsWith("owner-notice:")
94
94
  })
95
95
  }
96
96
 
97
+ const AWAIT_NOTICE_KEY = /^owner-notice:await:(.+):(resolved|expired|asked_owner):(\d{4}-\d{2}-\d{2}T[^:]*:[^:]*:[^:]*Z)$/
98
+ const unquote = (value) => String(value ?? "").replace(/^["']|["']$/g, "")
99
+
100
+ /**
101
+ * Splits the in-window owner notices into leaks (attributable to the replay run, or not attributable at all, which fails
102
+ * closed) and info (an await notice whose await was filed by a friend that is not a replay peer: the Butler's real owner
103
+ * work, which correctly reached Ari during the window). awaits: entries { name, createdAt, filedFor } from awaiting/ and
104
+ * awaiting/.done/. replayFriends: the replay peers' friend ids.
105
+ */
106
+ export function classifyOwnerNotices(effects, start, end, { replayFriends = [], awaits = [] } = {}) {
107
+ const leaks = []
108
+ const info = []
109
+ for (const effect of ownerNoticeEffects(effects, start, end)) {
110
+ const key = String(effect.idempotencyKey)
111
+ if (key.startsWith("owner-notice:delegated:")) { leaks.push({ key, reason: "delegated command notice (replay peers are the only delegating senders)" }); continue }
112
+ const match = AWAIT_NOTICE_KEY.exec(key)
113
+ if (!match) { leaks.push({ key, reason: "unattributable owner notice" }); continue }
114
+ const [, name, , createdAt] = match
115
+ const found = awaits.find((entry) => String(entry.name).replace(/\.md$/, "") === name && unquote(entry.createdAt) === createdAt)
116
+ if (!found || !found.filedFor) { leaks.push({ key, reason: "await not found or has no filing friend" }); continue }
117
+ if (replayFriends.includes(found.filedFor)) leaks.push({ key, reason: `await filed by replay friend ${found.filedFor}` })
118
+ else info.push({ key, reason: `await filed by non-replay friend ${found.filedFor}` })
119
+ }
120
+ return { leaks, info }
121
+ }
122
+
97
123
  // ---- the cases ---------------------------------------------------------------------------------------------------
98
124
  // Each case: id, words, sender, delegated, optional applicable(before) -> reason|null, optional poll, readback(ctx) -> checks.
99
125
  // ctx: { trace, reply, error, before, after, sink (lines inside this case's window), friends: { principal, stranger } }
@@ -278,10 +304,11 @@ export async function runSuite(host, { cases = CASES.map((entry) => entry.id), p
278
304
  }
279
305
  const runEnd = host.now()
280
306
  const finalObservation = await host.observe()
281
- const leaks = telegramLeaks(finalObservation.effects, runStart, runEnd)
307
+ const { leaks, info } = classifyOwnerNotices(finalObservation.effects, runStart, runEnd, { replayFriends: [host.friends.principal, host.friends.stranger], awaits: [...(finalObservation.awaiting ?? []), ...(finalObservation.done ?? [])] })
282
308
  const readable = finalObservation.effectsReadable === true
283
309
  const clean = readable && leaks.length === 0
284
- const telegram = { id: "no-telegram", status: clean ? "pass" : "fail", checks: [check("no owner-notice Telegram effect was recorded during the run", clean, readable ? leaks.map((leak) => leak.idempotencyKey).join(",") : "state/telegram/effects is not readable, so the absence of a leak cannot be shown")] }
310
+ const detail = readable ? [...leaks.map((leak) => `${leak.key} (${leak.reason})`), ...info.map((entry) => `info: ${entry.key} (${entry.reason})`)].join(", ") : "state/telegram/effects is not readable, so the absence of a leak cannot be shown"
311
+ const telegram = { id: "no-telegram", status: clean ? "pass" : "fail", checks: [check("no owner-notice Telegram effect attributable to the replay run was recorded", clean, detail)] }
285
312
  const all = [...results, telegram]
286
313
  const failed = all.filter((entry) => entry.status === "fail").map((entry) => entry.id)
287
314
  return { results: all, summary: { ok: failed.length === 0, passed: all.filter((entry) => entry.status === "pass").length, skipped: all.filter((entry) => entry.status === "skipped").length, failed } }
@@ -354,9 +381,17 @@ export function selfTest() {
354
381
  const stalled = { trackedDownloadStatus: "warning" }
355
382
  if (CASES.find((entry) => entry.id === "stall-kept").applicable(emptyObservation({ queue: [{ id: 1 }] })) === null) problems.push("stall-kept ran with no stalled item")
356
383
  if (CASES.find((entry) => entry.id === "stall-kept").applicable(emptyObservation({ queue: [stalled] })) !== null) problems.push("stall-kept skipped with a stalled item")
357
- const leaked = telegramLeaks([{ idempotencyKey: "owner-notice:delegated:x", createdAt: new Date(500).toISOString() }, { idempotencyKey: "other", createdAt: new Date(500).toISOString() }], 0, 1000)
358
- if (leaked.length !== 1) problems.push("the telegram leak check missed an in-window owner notice")
359
- if (telegramLeaks([{ idempotencyKey: "owner-notice:x", createdAt: new Date(5000).toISOString() }], 0, 1000).length !== 0) problems.push("the telegram leak check counted an out-of-window record")
384
+ const at = new Date(500).toISOString()
385
+ const created = "2026-10-08T03:03:00.876Z"
386
+ const ctx = { replayFriends: ["p", "s"], awaits: [{ name: "replay-wait.md", createdAt: created, filedFor: "p" }, { name: "chef_show_s2_landed.md", createdAt: created, filedFor: "real-friend" }] }
387
+ const classify = (key) => classifyOwnerNotices([{ idempotencyKey: key, createdAt: at }, { idempotencyKey: "other", createdAt: at }], 0, 1000, ctx)
388
+ if (classify("owner-notice:delegated:x").leaks.length !== 1) problems.push("no-telegram missed a replay-delegated owner notice")
389
+ if (classify(`owner-notice:await:replay-wait:asked_owner:${created}`).leaks.length !== 1) problems.push("no-telegram missed an await notice filed by a replay friend")
390
+ const real = classify(`owner-notice:await:chef_show_s2_landed:asked_owner:${created}`)
391
+ if (real.leaks.length !== 0 || real.info.length !== 1) problems.push("no-telegram did not pass a real owner await notice with an info detail")
392
+ if (classify("owner-notice:mystery").leaks.length !== 1) problems.push("no-telegram did not fail closed on an unknown owner-notice key")
393
+ if (classify(`owner-notice:await:missing:asked_owner:${created}`).leaks.length !== 1) problems.push("no-telegram did not fail closed on an await it could not find")
394
+ if (classifyOwnerNotices([{ idempotencyKey: "owner-notice:x", createdAt: new Date(5000).toISOString() }], 0, 1000, ctx).leaks.length !== 0) problems.push("no-telegram counted an out-of-window record")
360
395
  return problems
361
396
  }
362
397
 
@@ -378,7 +413,7 @@ export function makeHost({ bundle = DEFAULT_BUNDLE, cardUrl, log = console.error
378
413
  const list = (dir) => (existsSync(dir) ? readdirSync(dir) : [])
379
414
  const awaitEntries = (dir) => list(dir).filter((name) => name.endsWith(".md")).map((name) => {
380
415
  const text = readFileSync(path.join(dir, name), "utf8")
381
- return { name, status: /^status:\s*(\S+)/m.exec(text)?.[1] ?? "pending", createdAt: /^created_at:\s*(\S+)/m.exec(text)?.[1] ?? "", reason: /^cancel_reason:\s*(.+)$/m.exec(text)?.[1]?.trim() ?? "" }
416
+ return { name, status: /^status:\s*(\S+)/m.exec(text)?.[1] ?? "pending", createdAt: /^created_at:\s*(\S+)/m.exec(text)?.[1] ?? "", filedFor: unquote(/^filed_for_friend_id:\s*(\S+)/m.exec(text)?.[1] ?? ""), reason: /^cancel_reason:\s*(.+)$/m.exec(text)?.[1]?.trim() ?? "" }
382
417
  })
383
418
  return {
384
419
  friends: { principal: provisioned.principal.friendId, stranger: provisioned.stranger.friendId },
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.870",
2
+ "runtimeVersion": "0.1.0-alpha.871",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.870</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.871</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -381,6 +381,13 @@ function updateStewardPolicy(agentRoot, input) {
381
381
  }
382
382
  if (current.version !== input.expectedVersion)
383
383
  throw new Error(`steward policy version changed: expected ${input.expectedVersion}, got ${current.version}`);
384
+ if (input.mutation.kind === "set_desired_state") {
385
+ const existing = current.desiredStates[key];
386
+ const sameValue = existing !== undefined && existing.provenance === input.mutation.provenance && existing.value === affectedKeyResult.value && existing.expiresAt === expiresAt;
387
+ // Restating what is already recorded is a no-op: no version bump, no audit row, no provenance rewrite.
388
+ if (sameValue)
389
+ return current;
390
+ }
384
391
  optionalExpiry(expiresAt, now);
385
392
  const postimage = JSON.stringify(next, null, 2);
386
393
  if (Buffer.byteLength(postimage) > MAX_POLICY_BYTES)
@@ -117,6 +117,7 @@ exports.stewardPolicyToolDefinition = {
117
117
  || ctx.currentSession?.key !== sessionKey || !sessionMatches() || ctx.currentExternalEvent) {
118
118
  throw new Error("steward policy owner authorization or session changed before mutation");
119
119
  }
120
+ const versionBefore = (0, steward_policy_1.readStewardPolicy)(agentRoot).version;
120
121
  const result = (0, steward_policy_1.updateStewardPolicy)(agentRoot, {
121
122
  expectedVersion: expectedVersion ?? (0, steward_policy_1.readStewardPolicy)(agentRoot).version,
122
123
  actor: {
@@ -127,6 +128,11 @@ exports.stewardPolicyToolDefinition = {
127
128
  mutation,
128
129
  });
129
130
  (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.steward_policy_tool_call", message: "updated steward policy", meta: { action: args.action } });
131
+ if (mutation.kind === "set_desired_state" && result.version === versionBefore) {
132
+ // A same-value restatement writes nothing; the entry is necessarily present.
133
+ const existing = result.desiredStates[mutation.key];
134
+ return JSON.stringify({ ...result, unchanged: true, note: `already recorded as ${existing.value} (source: ${existing.source})` });
135
+ }
130
136
  return JSON.stringify(result);
131
137
  });
132
138
  },
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.870",
3
+ "version": "0.1.0-alpha.871",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ouro.bot/cli",
9
- "version": "0.1.0-alpha.870",
9
+ "version": "0.1.0-alpha.871",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.870",
3
+ "version": "0.1.0-alpha.871",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },