@ouro.bot/cli 0.1.0-alpha.147 → 0.1.0-alpha.148

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.
@@ -1,33 +1,63 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.createInnerDialogWorker = createInnerDialogWorker;
4
37
  exports.startInnerDialogWorker = startInnerDialogWorker;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
5
40
  const inner_dialog_1 = require("./inner-dialog");
6
41
  const runtime_1 = require("../nerves/runtime");
7
42
  const identity_1 = require("../heart/identity");
8
43
  const pending_1 = require("../mind/pending");
44
+ const habit_parser_1 = require("../heart/daemon/habit-parser");
9
45
  function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options), hasPendingWork = () => (0, pending_1.hasPendingMessages)((0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)()))) {
10
46
  let running = false;
11
- let rerunRequested = false;
12
- let rerunReason = "instinct";
13
- let rerunTaskId;
14
- async function run(reason, taskId) {
47
+ const queue = [];
48
+ async function run(reason, taskId, habitName) {
15
49
  if (running) {
16
- rerunRequested = true;
17
- rerunReason = reason;
18
- if (taskId !== undefined) {
19
- rerunTaskId = taskId;
20
- }
50
+ queue.push({ reason, taskId, habitName });
21
51
  return;
22
52
  }
23
53
  running = true;
24
54
  try {
25
55
  let nextReason = reason;
26
56
  let nextTaskId = taskId;
57
+ let nextHabitName = habitName;
27
58
  do {
28
- rerunRequested = false;
29
59
  try {
30
- await runTurn({ reason: nextReason, taskId: nextTaskId });
60
+ await runTurn({ reason: nextReason, taskId: nextTaskId, habitName: nextHabitName });
31
61
  }
32
62
  catch (error) {
33
63
  (0, runtime_1.emitNervesEvent)({
@@ -41,15 +71,44 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
41
71
  },
42
72
  });
43
73
  }
44
- if (!rerunRequested && hasPendingWork()) {
45
- rerunRequested = true;
46
- rerunReason = "instinct";
74
+ // Update lastRun in habit frontmatter after a habit turn
75
+ if (nextReason === "habit" && nextHabitName) {
76
+ try {
77
+ const agentRoot = (0, identity_1.getAgentRoot)();
78
+ const habitFilePath = path.join(agentRoot, "habits", `${nextHabitName}.md`);
79
+ const content = fs.readFileSync(habitFilePath, "utf-8");
80
+ const parsed = (0, habit_parser_1.parseHabitFile)(content, habitFilePath);
81
+ const frontmatter = {
82
+ title: parsed.title,
83
+ cadence: parsed.cadence,
84
+ status: parsed.status,
85
+ lastRun: new Date().toISOString(),
86
+ created: parsed.created,
87
+ };
88
+ const rendered = (0, habit_parser_1.renderHabitFile)(frontmatter, parsed.body);
89
+ fs.writeFileSync(habitFilePath, rendered, "utf-8");
90
+ }
91
+ catch {
92
+ // Habit file may have been deleted during the turn — skip gracefully
93
+ }
94
+ }
95
+ // Drain queue first
96
+ if (queue.length > 0) {
97
+ const next = queue.shift();
98
+ nextReason = next.reason;
99
+ nextTaskId = next.taskId;
100
+ nextHabitName = next.habitName;
101
+ continue;
102
+ }
103
+ // Then check hasPendingWork fallback
104
+ if (hasPendingWork()) {
105
+ nextReason = "instinct";
106
+ nextTaskId = undefined;
107
+ nextHabitName = undefined;
108
+ continue;
47
109
  }
48
- nextReason = rerunReason;
49
- nextTaskId = rerunTaskId;
50
- rerunReason = "instinct";
51
- rerunTaskId = undefined;
52
- } while (rerunRequested);
110
+ break;
111
+ } while (true);
53
112
  }
54
113
  finally {
55
114
  running = false;
@@ -59,8 +118,13 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
59
118
  if (!message || typeof message !== "object")
60
119
  return;
61
120
  const maybeMessage = message;
121
+ if (maybeMessage.type === "habit") {
122
+ await run("habit", undefined, maybeMessage.habitName);
123
+ return;
124
+ }
62
125
  if (maybeMessage.type === "heartbeat") {
63
- await run("heartbeat");
126
+ // Backward compatibility: heartbeat -> habit/heartbeat
127
+ await run("habit", undefined, "heartbeat");
64
128
  return;
65
129
  }
66
130
  if (maybeMessage.type === "poke") {
@@ -43,6 +43,7 @@ exports.deriveResumeCheckpoint = deriveResumeCheckpoint;
43
43
  exports.innerDialogSessionPath = innerDialogSessionPath;
44
44
  exports.enrichDelegatedFromWithBridge = enrichDelegatedFromWithBridge;
45
45
  exports.routeDelegatedCompletion = routeDelegatedCompletion;
46
+ exports.buildParseErrorNudge = buildParseErrorNudge;
46
47
  exports.runInnerDialogTurn = runInnerDialogTurn;
47
48
  const fs = __importStar(require("fs"));
48
49
  const path = __importStar(require("path"));
@@ -68,6 +69,8 @@ const bluebubbles_1 = require("./bluebubbles");
68
69
  const obligations_2 = require("../heart/obligations");
69
70
  const contextual_heartbeat_1 = require("./contextual-heartbeat");
70
71
  const journal_index_1 = require("../mind/journal-index");
72
+ const habit_parser_1 = require("../heart/daemon/habit-parser");
73
+ const cadence_1 = require("../heart/daemon/cadence");
71
74
  const DEFAULT_INNER_DIALOG_INSTINCTS = [
72
75
  {
73
76
  id: "heartbeat_checkin",
@@ -127,9 +130,10 @@ function buildInstinctUserMessage(instincts, _reason, state) {
127
130
  return lines.join("\n");
128
131
  }
129
132
  function readTaskFile(agentRoot, taskId) {
130
- // Task files live in collection subdirectories (one-shots, ongoing, habits).
133
+ // Task files live in collection subdirectories (one-shots, ongoing).
131
134
  // Try each collection, then fall back to root tasks/ for legacy layout.
132
- const collections = ["one-shots", "ongoing", "habits", ""];
135
+ // Habits are no longer in tasks/ — they live at bundle root habits/.
136
+ const collections = ["one-shots", "ongoing", ""];
133
137
  for (const collection of collections) {
134
138
  try {
135
139
  return fs.readFileSync(path.join(agentRoot, "tasks", collection, `${taskId}.md`), "utf8").trim();
@@ -460,9 +464,57 @@ function createNoOpFriendStore() {
460
464
  findByExternalId: async () => null,
461
465
  };
462
466
  }
467
+ function buildParseErrorNudge(parseErrors) {
468
+ if (parseErrors.length === 0)
469
+ return "";
470
+ const lines = parseErrors.map((e) => `I noticed my habit file \`${e.file}\` has invalid frontmatter — I should fix it. (${e.error})`);
471
+ return lines.join("\n");
472
+ }
473
+ function buildAlsoDueLine(agentRoot, currentHabitName, now) {
474
+ const habitsDir = path.join(agentRoot, "habits");
475
+ let files;
476
+ try {
477
+ files = fs.readdirSync(habitsDir);
478
+ }
479
+ catch {
480
+ return "";
481
+ }
482
+ const nowMs = now().getTime();
483
+ const alsoDue = [];
484
+ for (const file of files) {
485
+ if (!file.endsWith(".md"))
486
+ continue;
487
+ const stem = file.replace(/\.md$/, "");
488
+ if (stem === currentHabitName)
489
+ continue;
490
+ try {
491
+ const content = fs.readFileSync(path.join(habitsDir, file), "utf-8");
492
+ const habit = (0, habit_parser_1.parseHabitFile)(content, path.join(habitsDir, file));
493
+ if (habit.status !== "active" || !habit.cadence)
494
+ continue;
495
+ const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
496
+ if (cadenceMs === null)
497
+ continue;
498
+ if (habit.lastRun === null) {
499
+ alsoDue.push(stem);
500
+ continue;
501
+ }
502
+ const lastRunMs = new Date(habit.lastRun).getTime();
503
+ if (nowMs - lastRunMs >= cadenceMs) {
504
+ alsoDue.push(stem);
505
+ }
506
+ }
507
+ catch {
508
+ // skip unreadable habits
509
+ }
510
+ }
511
+ if (alsoDue.length === 0)
512
+ return "";
513
+ return `also due: ${alsoDue.join(", ")}`;
514
+ }
463
515
  async function runInnerDialogTurn(options) {
464
516
  const now = options?.now ?? (() => new Date());
465
- const reason = options?.reason ?? "heartbeat";
517
+ const reason = options?.reason ?? "instinct";
466
518
  const sessionFilePath = innerDialogSessionPath();
467
519
  const agentName = (0, identity_1.getAgentName)();
468
520
  writeInnerDialogRuntimeState(sessionFilePath, {
@@ -500,47 +552,101 @@ async function runInnerDialogTurn(options) {
500
552
  const taskContent = readTaskFile((0, identity_1.getAgentRoot)(), options.taskId);
501
553
  userContent = buildTaskTriggeredMessage(options.taskId, taskContent, state.checkpoint);
502
554
  }
503
- else if (reason === "heartbeat") {
555
+ else if (reason === "habit" && options?.habitName) {
504
556
  const agentRoot = (0, identity_1.getAgentRoot)();
505
- const journalDir = path.join(agentRoot, "journal");
506
- const runtimeStatePath = innerDialogRuntimeStatePath(sessionFilePath);
507
- // Read lastCompletedAt from current runtime state
508
- let lastCompletedAt;
557
+ const habitName = options.habitName;
558
+ const habitFilePath = path.join(agentRoot, "habits", `${habitName}.md`);
559
+ // Read and parse the habit file
560
+ let habitBody;
561
+ let habitLastRun = null;
509
562
  try {
510
- const raw = fs.readFileSync(runtimeStatePath, "utf-8");
511
- const runtimeState = JSON.parse(raw);
512
- lastCompletedAt = typeof runtimeState.lastCompletedAt === "string" ? runtimeState.lastCompletedAt : undefined;
563
+ const habitContent = fs.readFileSync(habitFilePath, "utf-8");
564
+ const parsed = (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath);
565
+ habitBody = parsed.body;
566
+ habitLastRun = parsed.lastRun;
513
567
  }
514
568
  catch {
515
- // no runtime state — will get cold start fallback
569
+ // Habit file missing or unreadable
570
+ }
571
+ if (habitName === "heartbeat") {
572
+ // Heartbeat habit: use contextual heartbeat builder with habit body
573
+ const journalDir = path.join(agentRoot, "journal");
574
+ const runtimeStatePath = innerDialogRuntimeStatePath(sessionFilePath);
575
+ let lastCompletedAt;
576
+ try {
577
+ const raw = fs.readFileSync(runtimeStatePath, "utf-8");
578
+ const runtimeState = JSON.parse(raw);
579
+ lastCompletedAt = typeof runtimeState.lastCompletedAt === "string" ? runtimeState.lastCompletedAt : undefined;
580
+ }
581
+ catch {
582
+ // no runtime state — will get cold start fallback
583
+ }
584
+ const obligations = (0, obligations_1.listActiveObligations)(agentName);
585
+ const nowMs = now().getTime();
586
+ const pendingObligations = obligations.map((o) => ({
587
+ id: o.id,
588
+ content: o.delegatedContent,
589
+ friendName: o.origin.friendId,
590
+ timestamp: o.createdAt,
591
+ staleness: nowMs - o.createdAt,
592
+ }));
593
+ const heartbeatContent = (0, contextual_heartbeat_1.buildContextualHeartbeat)({
594
+ journalDir,
595
+ lastCompletedAt,
596
+ pendingObligations,
597
+ lastSurfaceAt: undefined,
598
+ checkpoint: state.checkpoint,
599
+ now,
600
+ readJournalDir: () => (0, prompt_1.readJournalFiles)(journalDir),
601
+ habitBody,
602
+ });
603
+ // Build also-due line
604
+ const alsoDue = buildAlsoDueLine(agentRoot, habitName, now);
605
+ userContent = alsoDue ? `${heartbeatContent}\n\n${alsoDue}` : heartbeatContent;
606
+ // Parse error nudge
607
+ const parseErrorNudge = buildParseErrorNudge(options?.parseErrors ?? []);
608
+ if (parseErrorNudge)
609
+ userContent = `${userContent}\n\n${parseErrorNudge}`;
610
+ // Piggyback journal embedding indexing (best-effort, fire-and-forget)
611
+ /* v8 ignore start -- journal indexing piggyback: embedding provider may not be available; tested via journal-index unit tests @preserve */
612
+ void (0, journal_index_1.indexJournalFiles)(journalDir, path.join(journalDir, ".index.json"), {
613
+ embed: async () => [],
614
+ }).catch(() => {
615
+ // swallowed: indexing failure must never block heartbeat
616
+ });
617
+ /* v8 ignore stop */
618
+ }
619
+ else if (habitBody !== undefined) {
620
+ // Non-heartbeat habit: basic context + body
621
+ const sections = [];
622
+ // Elapsed time since last run
623
+ if (habitLastRun) {
624
+ const lastRunMs = new Date(habitLastRun).getTime();
625
+ const elapsed = now().getTime() - lastRunMs;
626
+ const minutes = Math.floor(elapsed / 60000);
627
+ if (minutes < 60) {
628
+ sections.push(`it's been ${minutes} ${minutes === 1 ? "minute" : "minutes"} since ${habitName} last ran.`);
629
+ }
630
+ else {
631
+ const hours = Math.floor(minutes / 60);
632
+ sections.push(`it's been ${hours} ${hours === 1 ? "hour" : "hours"} since ${habitName} last ran.`);
633
+ }
634
+ }
635
+ sections.push(`## habit: ${habitName}\n${habitBody}`);
636
+ // Also due line
637
+ const alsoDue = buildAlsoDueLine(agentRoot, habitName, now);
638
+ if (alsoDue)
639
+ sections.push(alsoDue);
640
+ // Parse error nudge
641
+ const parseErrorNudge = buildParseErrorNudge(options?.parseErrors ?? []);
642
+ if (parseErrorNudge)
643
+ sections.push(parseErrorNudge);
644
+ userContent = sections.join("\n\n");
645
+ }
646
+ else {
647
+ // Habit file not found
648
+ userContent = `habit "${habitName}" could not be read (file not found or unreadable). check habits/${habitName}.md exists.`;
516
649
  }
517
- // Gather pending obligations for display
518
- const obligations = (0, obligations_1.listActiveObligations)(agentName);
519
- const nowMs = now().getTime();
520
- const pendingObligations = obligations.map((o) => ({
521
- id: o.id,
522
- content: o.delegatedContent,
523
- friendName: o.origin.friendId,
524
- timestamp: o.createdAt,
525
- staleness: nowMs - o.createdAt,
526
- }));
527
- userContent = (0, contextual_heartbeat_1.buildContextualHeartbeat)({
528
- journalDir,
529
- lastCompletedAt,
530
- pendingObligations,
531
- lastSurfaceAt: undefined, // TODO: wire when surface tracking is added
532
- checkpoint: state.checkpoint,
533
- now,
534
- readJournalDir: () => (0, prompt_1.readJournalFiles)(journalDir),
535
- });
536
- // Piggyback journal embedding indexing (best-effort, fire-and-forget)
537
- /* v8 ignore start -- journal indexing piggyback: embedding provider may not be available; tested via journal-index unit tests @preserve */
538
- void (0, journal_index_1.indexJournalFiles)(journalDir, path.join(journalDir, ".index.json"), {
539
- embed: async () => [],
540
- }).catch(() => {
541
- // swallowed: indexing failure must never block heartbeat
542
- });
543
- /* v8 ignore stop */
544
650
  }
545
651
  else {
546
652
  userContent = buildInstinctUserMessage(instincts, reason, state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.147",
3
+ "version": "0.1.0-alpha.148",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -1,132 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HeartbeatTimer = exports.DEFAULT_CADENCE_MS = void 0;
4
- exports.parseCadenceMs = parseCadenceMs;
5
- const runtime_1 = require("../../nerves/runtime");
6
- const parser_1 = require("../../repertoire/tasks/parser");
7
- exports.DEFAULT_CADENCE_MS = 30 * 60 * 1000; // 30 minutes
8
- function parseCadenceMs(raw) {
9
- if (typeof raw !== "string")
10
- return null;
11
- const value = raw.trim();
12
- if (!value)
13
- return null;
14
- const match = /^(\d+)(m|h|d)$/.exec(value);
15
- if (!match)
16
- return null;
17
- const interval = Number.parseInt(match[1], 10);
18
- if (!Number.isFinite(interval) || interval <= 0)
19
- return null;
20
- const unit = match[2];
21
- if (unit === "m")
22
- return interval * 60 * 1000;
23
- if (unit === "h")
24
- return interval * 60 * 60 * 1000;
25
- return interval * 24 * 60 * 60 * 1000;
26
- }
27
- class HeartbeatTimer {
28
- agent;
29
- sendToAgent;
30
- readFileSync;
31
- readdirSync;
32
- heartbeatTaskDir;
33
- runtimeStatePath;
34
- now;
35
- pendingTimer = null;
36
- constructor(options) {
37
- this.agent = options.agent;
38
- this.sendToAgent = options.sendToAgent;
39
- this.readFileSync = options.deps.readFileSync;
40
- this.readdirSync = options.deps.readdirSync;
41
- this.heartbeatTaskDir = options.deps.heartbeatTaskDir;
42
- this.runtimeStatePath = options.deps.runtimeStatePath;
43
- this.now = options.deps.now ?? (() => Date.now());
44
- }
45
- start() {
46
- this.scheduleNext();
47
- }
48
- stop() {
49
- if (this.pendingTimer !== null) {
50
- clearTimeout(this.pendingTimer);
51
- this.pendingTimer = null;
52
- }
53
- }
54
- scheduleNext() {
55
- const cadenceMs = this.readCadence();
56
- const lastCompletedAt = this.readLastCompletedAt();
57
- const nowMs = this.now();
58
- let delay;
59
- if (lastCompletedAt === null) {
60
- // Never run before — fire once after a full cadence (not immediately, to avoid spin loop
61
- // when the agent has no inner dialog state to write lastCompletedAt)
62
- delay = cadenceMs;
63
- }
64
- else {
65
- const elapsed = nowMs - lastCompletedAt;
66
- delay = Math.max(0, cadenceMs - elapsed);
67
- }
68
- this.pendingTimer = setTimeout(() => {
69
- this.fire();
70
- }, delay);
71
- }
72
- fire() {
73
- this.pendingTimer = null;
74
- (0, runtime_1.emitNervesEvent)({
75
- component: "daemon",
76
- event: "daemon.heartbeat_fire",
77
- message: "heartbeat timer fired",
78
- meta: { agent: this.agent },
79
- });
80
- this.sendToAgent(this.agent, { type: "heartbeat" });
81
- this.scheduleNext();
82
- }
83
- readCadence() {
84
- // Scan habits dir for *-heartbeat.md
85
- let files;
86
- try {
87
- files = this.readdirSync(this.heartbeatTaskDir);
88
- }
89
- catch {
90
- return exports.DEFAULT_CADENCE_MS;
91
- }
92
- const heartbeatFile = files.find((f) => f.endsWith("-heartbeat.md"));
93
- if (!heartbeatFile)
94
- return exports.DEFAULT_CADENCE_MS;
95
- try {
96
- const content = this.readFileSync(`${this.heartbeatTaskDir}/${heartbeatFile}`, "utf-8");
97
- const cadence = this.extractCadenceFromTaskFile(content);
98
- return cadence ?? exports.DEFAULT_CADENCE_MS;
99
- }
100
- catch {
101
- return exports.DEFAULT_CADENCE_MS;
102
- }
103
- }
104
- extractCadenceFromTaskFile(content) {
105
- // Parse frontmatter from task file to get cadence value
106
- const lines = content.split(/\r?\n/);
107
- if (lines[0]?.trim() !== "---")
108
- return null;
109
- const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
110
- if (closing === -1)
111
- return null;
112
- const rawFrontmatter = lines.slice(1, closing).join("\n");
113
- const frontmatter = (0, parser_1.parseFrontmatter)(rawFrontmatter);
114
- return parseCadenceMs(frontmatter.cadence);
115
- }
116
- readLastCompletedAt() {
117
- try {
118
- const raw = this.readFileSync(this.runtimeStatePath, "utf-8");
119
- const state = JSON.parse(raw);
120
- if (typeof state.lastCompletedAt !== "string")
121
- return null;
122
- const ms = new Date(state.lastCompletedAt).getTime();
123
- if (Number.isNaN(ms))
124
- return null;
125
- return ms;
126
- }
127
- catch {
128
- return null;
129
- }
130
- }
131
- }
132
- exports.HeartbeatTimer = HeartbeatTimer;