@ouro.bot/cli 0.1.0-alpha.663 → 0.1.0-alpha.665

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.665",
6
+ "changes": [
7
+ "Ignore Sentinel-owned daemon-health git dirt when computing the Sentinel bundle signal, preventing a self-sustaining health-receipt loop while keeping real bundle dirt visible."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.664",
12
+ "changes": [
13
+ "Coalesce unchanged periodic Arc Sentinel daemon-health receipts so healthy daemons do not keep synced bundles dirty."
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.663",
6
18
  "changes": [
@@ -126,6 +126,103 @@ function compareReceiptOrder(left, right) {
126
126
  function shouldReplaceReceipt(existing, candidate) {
127
127
  return existing === null || compareReceiptOrder(candidate, existing) >= 0;
128
128
  }
129
+ function orderFromReceipt(receipt) {
130
+ return { generatedAt: receipt.generatedAt, id: receipt.id };
131
+ }
132
+ function compareOrder(left, right) {
133
+ const leftTime = Date.parse(left.generatedAt);
134
+ const rightTime = Date.parse(right.generatedAt);
135
+ if (leftTime !== rightTime)
136
+ return leftTime - rightTime;
137
+ return left.id.localeCompare(right.id);
138
+ }
139
+ function maxOrder(left, right) {
140
+ if (!left)
141
+ return right;
142
+ if (!right)
143
+ return left;
144
+ return compareOrder(left, right) >= 0 ? left : right;
145
+ }
146
+ function isOrder(value) {
147
+ if (!value || typeof value !== "object" || Array.isArray(value))
148
+ return false;
149
+ const record = value;
150
+ return typeof record.generatedAt === "string"
151
+ && isValidTimestamp(record.generatedAt)
152
+ && typeof record.id === "string";
153
+ }
154
+ function sentinelWatermarkPath(agentRoot) {
155
+ return path.join(agentRoot, "state", "arc", "context-loss-sentinel-watermark.json");
156
+ }
157
+ function readWatermark(agentRoot) {
158
+ const filePath = sentinelWatermarkPath(agentRoot);
159
+ try {
160
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
161
+ return {
162
+ schemaVersion: 1,
163
+ latest: isOrder(parsed.latest) ? parsed.latest : null,
164
+ latestReady: isOrder(parsed.latestReady) ? parsed.latestReady : null,
165
+ };
166
+ }
167
+ catch {
168
+ return { schemaVersion: 1, latest: null, latestReady: null };
169
+ }
170
+ }
171
+ function writeWatermark(agentRoot, watermark) {
172
+ atomicWriteJson(sentinelWatermarkPath(agentRoot), watermark);
173
+ }
174
+ function updateWatermarkOrder(watermark, key, candidate) {
175
+ watermark[key] = maxOrder(watermark[key], orderFromReceipt(candidate));
176
+ }
177
+ function stableReceiptState(receipt) {
178
+ return JSON.stringify({
179
+ verdict: receipt.verdict,
180
+ summary: receipt.summary,
181
+ latestReadyLocator: receipt.latestReadyLocator,
182
+ recoveryAnchor: receipt.recoveryAnchor,
183
+ gauntlet: receipt.gauntlet,
184
+ signals: receipt.signals,
185
+ resumeSnapshot: receipt.resumeSnapshot,
186
+ });
187
+ }
188
+ function sameRecoveryState(left, right) {
189
+ return stableReceiptState(left) === stableReceiptState(right);
190
+ }
191
+ function setReceiptFileMtime(filePath, receipt) {
192
+ const timestamp = new Date(receipt.generatedAt);
193
+ fs.utimesSync(filePath, timestamp, timestamp);
194
+ }
195
+ function selectLatestReadyReceipt(existingReady, candidate, latestReadyOrder) {
196
+ if (candidate.verdict !== "ready")
197
+ return existingReady;
198
+ if (latestReadyOrder && compareOrder(orderFromReceipt(candidate), latestReadyOrder) < 0)
199
+ return existingReady;
200
+ return candidate;
201
+ }
202
+ function shouldReplaceLatestReceipt(existingLatest, candidate, latestOrder) {
203
+ if (latestOrder && compareOrder(orderFromReceipt(candidate), latestOrder) < 0)
204
+ return false;
205
+ if (!existingLatest)
206
+ return true;
207
+ return shouldReplaceReceipt(existingLatest, candidate);
208
+ }
209
+ function coalescedDaemonHealthReceipt(agentRoot, existingLatest, existingReady, candidate, watermark) {
210
+ if (candidate.trigger !== "daemon_health")
211
+ return null;
212
+ if (!existingLatest || !sameRecoveryState(existingLatest, candidate))
213
+ return null;
214
+ if (candidate.verdict === "ready" && (!existingReady || !sameRecoveryState(existingReady, candidate)))
215
+ return null;
216
+ const candidateOrder = orderFromReceipt(candidate);
217
+ if (watermark.latest && compareOrder(candidateOrder, watermark.latest) <= 0)
218
+ return existingLatest;
219
+ updateWatermarkOrder(watermark, "latest", candidate);
220
+ if (candidate.verdict === "ready" && existingReady) {
221
+ updateWatermarkOrder(watermark, "latestReady", candidate);
222
+ }
223
+ writeWatermark(agentRoot, watermark);
224
+ return existingLatest;
225
+ }
129
226
  function readJson(filePath) {
130
227
  try {
131
228
  return { ok: true, value: JSON.parse(fs.readFileSync(filePath, "utf-8")) };
@@ -385,7 +482,7 @@ function healthSignals(results) {
385
482
  }
386
483
  function defaultGitStatus(agentRoot) {
387
484
  try {
388
- const porcelain = (0, child_process_1.execFileSync)("git", ["status", "--porcelain"], {
485
+ const porcelain = (0, child_process_1.execFileSync)("git", ["status", "--porcelain=v1", "-uall"], {
389
486
  cwd: agentRoot,
390
487
  encoding: "utf-8",
391
488
  stdio: ["ignore", "pipe", "pipe"],
@@ -397,7 +494,50 @@ function defaultGitStatus(agentRoot) {
397
494
  return { ok: false, error: String(error) };
398
495
  }
399
496
  }
400
- function bundleSignal(status) {
497
+ function gitStatusEntries(porcelain) {
498
+ return porcelain.split(/\r?\n/).filter((line) => line.trim().length > 0);
499
+ }
500
+ function normalizeGitStatusPath(entryPath) {
501
+ return entryPath.trim().replace(/^"|"$/g, "");
502
+ }
503
+ function gitStatusPaths(entry) {
504
+ const rawPath = entry.slice(3).trim();
505
+ const paths = [];
506
+ let start = 0;
507
+ let inQuote = false;
508
+ let escaped = false;
509
+ for (let index = 0; index < rawPath.length; index += 1) {
510
+ const char = rawPath[index];
511
+ if (escaped) {
512
+ escaped = false;
513
+ continue;
514
+ }
515
+ if (inQuote && char === "\\") {
516
+ escaped = true;
517
+ continue;
518
+ }
519
+ if (char === "\"") {
520
+ inQuote = !inQuote;
521
+ continue;
522
+ }
523
+ if (!inQuote && rawPath.startsWith(" -> ", index)) {
524
+ paths.push(rawPath.slice(start, index));
525
+ start = index + 4;
526
+ index += 3;
527
+ }
528
+ }
529
+ paths.push(rawPath.slice(start));
530
+ return paths.map(normalizeGitStatusPath);
531
+ }
532
+ function isSentinelGitStatusEntry(entry) {
533
+ const sentinelRoot = relativeSentinelRoot();
534
+ return gitStatusPaths(entry).every((entryPath) => {
535
+ const normalizedPath = entryPath.replace(/\/$/, "");
536
+ return normalizedPath === sentinelRoot || normalizedPath.startsWith(`${sentinelRoot}/`);
537
+ });
538
+ }
539
+ function bundleSignal(status, options = {}) {
540
+ const gitStatusCommand = "git status --porcelain=v1 -uall";
401
541
  if (!status.ok) {
402
542
  return {
403
543
  id: "bundle:git",
@@ -406,11 +546,12 @@ function bundleSignal(status) {
406
546
  severity: "warn",
407
547
  verdictImpact: "watch",
408
548
  summary: `bundle git status unavailable: ${status.error}`,
409
- source: { kind: "git", locator: "git status --porcelain" },
410
- repair: repair("agent-runnable", "bundle-cleanup", "Inspect bundle git state before assuming the local state is clean.", "git status --porcelain"),
549
+ source: { kind: "git", locator: gitStatusCommand },
550
+ repair: repair("agent-runnable", "bundle-cleanup", "Inspect bundle git state before assuming the local state is clean.", gitStatusCommand),
411
551
  };
412
552
  }
413
- const dirtyEntries = status.porcelain.split(/\r?\n/).filter((line) => line.trim().length > 0);
553
+ const dirtyEntries = gitStatusEntries(status.porcelain)
554
+ .filter((entry) => !(options.ignoreSentinelDirtyEntries && isSentinelGitStatusEntry(entry)));
414
555
  if (dirtyEntries.length > 0) {
415
556
  return {
416
557
  id: "bundle:git",
@@ -419,8 +560,8 @@ function bundleSignal(status) {
419
560
  severity: "warn",
420
561
  verdictImpact: "watch",
421
562
  summary: `bundle has ${dirtyEntries.length} uncommitted git status entr${dirtyEntries.length === 1 ? "y" : "ies"}`,
422
- source: { kind: "git", locator: "git status --porcelain" },
423
- repair: repair("agent-runnable", "bundle-cleanup", "Resolve or intentionally preserve local bundle changes before handoff.", "git status --porcelain"),
563
+ source: { kind: "git", locator: gitStatusCommand },
564
+ repair: repair("agent-runnable", "bundle-cleanup", "Resolve or intentionally preserve local bundle changes before handoff.", gitStatusCommand),
424
565
  meta: { dirtyEntries },
425
566
  };
426
567
  }
@@ -431,7 +572,7 @@ function bundleSignal(status) {
431
572
  severity: "info",
432
573
  verdictImpact: "none",
433
574
  summary: "bundle git status clean",
434
- source: { kind: "git", locator: "git status --porcelain" },
575
+ source: { kind: "git", locator: gitStatusCommand },
435
576
  };
436
577
  }
437
578
  function sentinelVerdict(signals) {
@@ -536,7 +677,9 @@ function makeReceipt(agentName, agentRoot, options, generatedAt) {
536
677
  gauntletSignal(report),
537
678
  ...deriveContextLossSentinelProviderSignals(providerVisibility),
538
679
  ...healthSignals(options.daemonHealthResults ?? []),
539
- bundleSignal((options.gitStatus ?? (() => defaultGitStatus(agentRoot)))()),
680
+ bundleSignal((options.gitStatus ?? (() => defaultGitStatus(agentRoot)))(), {
681
+ ignoreSentinelDirtyEntries: options.trigger === "daemon_health",
682
+ }),
540
683
  ];
541
684
  const verdict = sentinelVerdict(signals);
542
685
  const latestReady = readLatestReady(agentRoot);
@@ -619,26 +762,38 @@ function recordBlockedReceiptEvent(agentRoot, receipt) {
619
762
  }
620
763
  async function persistReceipt(agentRoot, receipt, lockTimeoutMs) {
621
764
  const paths = contextLossSentinelPaths(agentRoot);
622
- await withFileLock(paths.lock, lockTimeoutMs, async () => {
765
+ return withFileLock(paths.lock, lockTimeoutMs, async () => {
623
766
  ensureSentinelDirs(paths);
624
767
  const existingLatest = readReceiptFile(paths.latest, "latest.json", []);
625
768
  const existingReady = readLatestReady(agentRoot);
626
- const nextReady = receipt.verdict === "ready" && shouldReplaceReceipt(existingReady, receipt)
627
- ? receipt
628
- : existingReady;
769
+ const watermark = readWatermark(agentRoot);
770
+ const latestOrder = maxOrder(maxOrder(maxOrder(existingLatest ? orderFromReceipt(existingLatest) : null, existingReady ? orderFromReceipt(existingReady) : null), watermark.latest), watermark.latestReady);
771
+ const latestReadyOrder = maxOrder(maxOrder(existingReady ? orderFromReceipt(existingReady) : null, watermark.latestReady), existingLatest?.verdict === "ready" ? orderFromReceipt(existingLatest) : null);
772
+ const nextReady = selectLatestReadyReceipt(existingReady, receipt, latestReadyOrder);
629
773
  syncLatestReadyState(receipt, nextReady);
774
+ const coalesced = coalescedDaemonHealthReceipt(agentRoot, existingLatest, existingReady, receipt, watermark);
775
+ if (coalesced)
776
+ return coalesced;
630
777
  atomicWriteJson(path.join(paths.receiptsDir, `${receipt.id}.json`), receipt);
631
778
  appendHistory(paths, receipt);
632
- if (shouldReplaceReceipt(existingLatest, receipt)) {
779
+ if (shouldReplaceLatestReceipt(existingLatest, receipt, latestOrder)) {
633
780
  atomicWriteJson(paths.latest, receipt);
781
+ updateWatermarkOrder(watermark, "latest", receipt);
782
+ writeWatermark(agentRoot, watermark);
783
+ setReceiptFileMtime(paths.latest, receipt);
634
784
  recordBlockedReceiptEvent(agentRoot, receipt);
635
785
  }
636
- else if (existingLatest && receipt.verdict === "ready") {
637
- atomicWriteJson(paths.latest, syncLatestReadyState(existingLatest, nextReady));
786
+ else if (existingLatest && nextReady === receipt) {
787
+ const updatedLatest = syncLatestReadyState(existingLatest, nextReady);
788
+ atomicWriteJson(paths.latest, updatedLatest);
638
789
  }
639
790
  if (receipt.verdict === "ready" && nextReady === receipt) {
640
791
  atomicWriteJson(paths.latestReady, receipt);
792
+ updateWatermarkOrder(watermark, "latestReady", receipt);
793
+ writeWatermark(agentRoot, watermark);
794
+ setReceiptFileMtime(paths.latestReady, receipt);
641
795
  }
796
+ return receipt;
642
797
  });
643
798
  }
644
799
  async function refreshContextLossSentinel(agentName, agentRoot, options) {
@@ -647,7 +802,7 @@ async function refreshContextLossSentinel(agentName, agentRoot, options) {
647
802
  if (options.delayBeforeWriteMs && options.delayBeforeWriteMs > 0) {
648
803
  await sleep(options.delayBeforeWriteMs);
649
804
  }
650
- await persistReceipt(agentRoot, receipt, options.lockTimeoutMs ?? 5_000);
805
+ const persistedReceipt = await persistReceipt(agentRoot, receipt, options.lockTimeoutMs ?? 5_000);
651
806
  (0, runtime_1.emitNervesEvent)({
652
807
  component: "engine",
653
808
  event: "engine.context_loss_sentinel_refreshed",
@@ -655,13 +810,13 @@ async function refreshContextLossSentinel(agentName, agentRoot, options) {
655
810
  meta: {
656
811
  agentName,
657
812
  trigger: options.trigger,
658
- verdict: receipt.verdict,
659
- receiptId: receipt.id,
660
- blockedSignals: receipt.signals.filter((entry) => entry.verdictImpact === "blocked").map((entry) => entry.id),
661
- watchSignals: receipt.signals.filter((entry) => entry.verdictImpact === "watch").map((entry) => entry.id),
813
+ verdict: persistedReceipt.verdict,
814
+ receiptId: persistedReceipt.id,
815
+ blockedSignals: persistedReceipt.signals.filter((entry) => entry.verdictImpact === "blocked").map((entry) => entry.id),
816
+ watchSignals: persistedReceipt.signals.filter((entry) => entry.verdictImpact === "watch").map((entry) => entry.id),
662
817
  },
663
818
  });
664
- return receipt;
819
+ return persistedReceipt;
665
820
  }
666
821
  function readHistory(paths, limit, issues) {
667
822
  if (!fs.existsSync(paths.historyDir))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.663",
3
+ "version": "0.1.0-alpha.665",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",