@ouro.bot/cli 0.1.0-alpha.845 → 0.1.0-alpha.847

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,18 @@
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.847",
6
+ "changes": [
7
+ "Bundle sync: post-turn push now removes an empty git index.lock older than 10 minutes (left by a git process that died mid-write) before committing, and any other failed local commit is recorded in state/pending-sync.json so the start-of-turn packet surfaces it. One stale lock had silently blocked every bundle commit for three months while untracked files piled up; a lock that cannot be removed is reported as a warning."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.846",
12
+ "changes": [
13
+ "Sanctuary: a natural question about the Butler's version ('what version are you on?') now requires a fresh install-state read, and any version it states must match that read. After an upgrade it had answered 'Just checked the live state: 0.1.0-alpha.841' from earlier conversation while running 845, with no tool call."
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.845",
6
18
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.845",
2
+ "runtimeVersion": "0.1.0-alpha.847",
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.845</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.847</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.STALE_INDEX_LOCK_MS = void 0;
36
37
  exports.preTurnPull = preTurnPull;
37
38
  exports.postTurnPush = postTurnPush;
38
39
  exports.preTurnPullAsync = preTurnPullAsync;
@@ -87,6 +88,52 @@ function ensureGitRepo(agentRoot) {
87
88
  const error = `bundle is not a git repo; run \`git init\` inside ${agentRoot} to enable sync (or disable sync in agent.json)`;
88
89
  return { ok: false, error };
89
90
  }
91
+ /**
92
+ * Git holds `.git/index.lock` for milliseconds to seconds, and every git call
93
+ * in this module has a timeout of 30s or less. A lock older than this was left
94
+ * behind by a git process that died mid-write; while it exists every `git add`
95
+ * fails, which silently stopped one bundle's sync for three months.
96
+ */
97
+ exports.STALE_INDEX_LOCK_MS = 10 * 60 * 1000;
98
+ // Only an empty lock is removed: git writes the new index into the lock right
99
+ // after creating it, while a live `git commit` waiting in an editor holds a
100
+ // full, unchanging lock that can be older than the threshold. When `.git` is a
101
+ // gitdir file (worktree), statSync fails and the lock is left alone.
102
+ function clearStaleIndexLock(agentRoot) {
103
+ const lockPath = path.join(agentRoot, ".git", "index.lock");
104
+ let ageMs;
105
+ try {
106
+ const stat = fs.statSync(lockPath);
107
+ if (stat.size !== 0)
108
+ return;
109
+ ageMs = Date.now() - stat.mtimeMs;
110
+ }
111
+ catch {
112
+ return;
113
+ }
114
+ if (ageMs < exports.STALE_INDEX_LOCK_MS)
115
+ return;
116
+ try {
117
+ fs.rmSync(lockPath, { force: true });
118
+ }
119
+ catch (err) {
120
+ (0, runtime_1.emitNervesEvent)({
121
+ level: "warn",
122
+ component: "heart",
123
+ event: "heart.sync_stale_lock_clear_failed",
124
+ message: "post-turn push: could not remove stale git index.lock",
125
+ meta: { agentRoot, lockPath, error: err instanceof Error ? err.message : /* v8 ignore next -- defensive non-Error catch @preserve */ String(err) },
126
+ });
127
+ return;
128
+ }
129
+ (0, runtime_1.emitNervesEvent)({
130
+ level: "warn",
131
+ component: "heart",
132
+ event: "heart.sync_stale_lock_cleared",
133
+ message: "post-turn push: removed stale git index.lock",
134
+ meta: { agentRoot, lockPath, ageMs },
135
+ });
136
+ }
90
137
  /**
91
138
  * Pre-turn pull: sync the agent bundle from remote before assembling the start-of-turn packet.
92
139
  *
@@ -224,6 +271,7 @@ function postTurnPush(agentRoot, config) {
224
271
  return { ok: true };
225
272
  }
226
273
  const changedCount = statusOutput.split("\n").length;
274
+ clearStaleIndexLock(agentRoot);
227
275
  try {
228
276
  (0, child_process_1.execFileSync)("git", ["add", "-A"], {
229
277
  cwd: agentRoot,
@@ -322,6 +370,14 @@ function postTurnPush(agentRoot, config) {
322
370
  }
323
371
  catch (err) {
324
372
  const error = err instanceof Error ? err.message : String(err);
373
+ // Record it so the start-of-turn packet surfaces the failure; otherwise a
374
+ // failing local commit is visible only in nerves logs.
375
+ try {
376
+ writePendingSync(agentRoot, error, "unknown", []);
377
+ }
378
+ catch {
379
+ // Best effort: the nerves event below still records the failure.
380
+ }
325
381
  (0, runtime_1.emitNervesEvent)({
326
382
  component: "heart",
327
383
  event: "heart.sync_push_error",
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sanctuaryInstallStateRequiredToolCalls = sanctuaryInstallStateRequiredToolCalls;
4
+ exports.sanctuaryVersionQuestionRequiredToolCalls = sanctuaryVersionQuestionRequiredToolCalls;
4
5
  const runtime_1 = require("../nerves/runtime");
5
6
  const TOOL_NAME = "sanctuary_get_install_state";
6
7
  const REQUEST = /\b(?:use|run|call)\s+sanctuary_get_install_state\b/iu;
@@ -124,3 +125,45 @@ function sanctuaryInstallStateRequiredToolCalls(request, advertisedToolNames) {
124
125
  },
125
126
  };
126
127
  }
128
+ const VERSION_QUESTION = /\b(?:what|which)(?:'s|\s+is)?\s+(?:your\s+|the\s+)?(?:current\s+|live\s+|installed\s+|running\s+)?version\b|\bversion\s+(?:are|r)\s+(?:you|u)\b|\bwhat\s+(?:are\s+you|you're)\s+(?:on|running)\b|\bare\s+you\s+(?:on|running)\s+(?:the\s+)?(?:latest|newest|new|current)\b/iu;
129
+ const VERSION_CLAIM = /\bv?\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?\b/gu;
130
+ /**
131
+ * A natural question about the Butler's own version must be answered from a fresh
132
+ * install-state read. After an upgrade the Butler once answered "Just checked the live
133
+ * state: 0.1.0-alpha.841" from earlier conversation while it was running 845, with no
134
+ * tool call at all. Unlike the explicit install-state contract, the wording stays free:
135
+ * only the version it states is checked against the read.
136
+ */
137
+ function sanctuaryVersionQuestionRequiredToolCalls(request, advertisedToolNames) {
138
+ const normalizedRequest = request.normalize("NFKC").replace(/[‘’]/gu, "'");
139
+ if (!advertisedToolNames.includes(TOOL_NAME) || REQUEST.test(normalizedRequest) || !VERSION_QUESTION.test(normalizedRequest))
140
+ return undefined;
141
+ let current;
142
+ (0, runtime_1.emitNervesEvent)({
143
+ component: "senses",
144
+ event: "senses.sanctuary_version_question_required",
145
+ message: "a question about the running version requires a fresh install-state read",
146
+ meta: { requiredToolName: TOOL_NAME },
147
+ });
148
+ return {
149
+ names: [TOOL_NAME],
150
+ retryMessage: "Call sanctuary_get_install_state with empty arguments now, then state the version from that fresh result, not from earlier conversation.",
151
+ requireSuccessfulResults: true,
152
+ validateRequiredToolResult: (name, result, args) => {
153
+ if (name !== TOOL_NAME || Object.keys(args).length !== 0)
154
+ return false;
155
+ current = parseInstallState(result);
156
+ return current !== undefined;
157
+ },
158
+ validateTerminalAnswer: (answer) => {
159
+ if (!current)
160
+ return "Call sanctuary_get_install_state and state the version from that fresh result.";
161
+ const known = new Set([current.runtimePackageVersion, current.packagedBundleVersion, ...(current.liveBundleVersion ? [current.liveBundleVersion] : [])].map((version) => version.replace(/^v/u, "")));
162
+ const claimed = [...answer.normalize("NFKC").matchAll(VERSION_CLAIM)].map((match) => match[0].replace(/^v/u, ""));
163
+ if (claimed.length === 0)
164
+ return `State the running version from the fresh result: ${current.runtimePackageVersion}.`;
165
+ const stale = claimed.filter((version) => !known.has(version));
166
+ return stale.length === 0 ? undefined : `The fresh install-state result says ${current.runtimePackageVersion}; do not state ${stale.join(", ")} as the current version.`;
167
+ },
168
+ };
169
+ }
@@ -1234,9 +1234,12 @@ function createTelegramSenseApp(options) {
1234
1234
  const installState = isSanctuaryOwner
1235
1235
  ? (0, sanctuary_install_state_contract_1.sanctuaryInstallStateRequiredToolCalls)(input.userMessage, relationshipAuthorization.advertisedToolNames)
1236
1236
  : undefined;
1237
+ const versionQuestion = isSanctuaryOwner
1238
+ ? (0, sanctuary_install_state_contract_1.sanctuaryVersionQuestionRequiredToolCalls)(input.userMessage, relationshipAuthorization.advertisedToolNames)
1239
+ : undefined;
1237
1240
  if (input.fullVisibilityProgress && fullVisibility)
1238
1241
  input.fullVisibilityProgress.fallback = fullVisibility.emptyResponseFallback;
1239
- const contracts = [storageOptimization, fullVisibility, staleDockerCare, mediaCatalog, installState].filter(Boolean);
1242
+ const contracts = [storageOptimization, fullVisibility, staleDockerCare, mediaCatalog, installState, versionQuestion].filter(Boolean);
1240
1243
  const ownedNames = contracts.map((contract) => new Set(contract.names));
1241
1244
  const requiredToolCalls = contracts.length === 0 ? undefined : {
1242
1245
  names: [...new Set(contracts.flatMap((contract) => [...contract.names]))],
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.845",
3
+ "version": "0.1.0-alpha.847",
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.845",
9
+ "version": "0.1.0-alpha.847",
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.845",
3
+ "version": "0.1.0-alpha.847",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },