@ouro.bot/cli 0.1.0-alpha.428 → 0.1.0-alpha.429

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,15 @@
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.429",
6
+ "changes": [
7
+ "Habit `lastRun` now lives in bundle runtime state under `state/habits/<habit>.json` instead of being rewritten into tracked `habits/*.md` files after every habit turn.",
8
+ "The habit worker, scheduler, inner-dialog habit turns, outlook habit readers, and `ouro habit list` now resolve `lastRun` through one shared helper, with legacy habit frontmatter kept as read-only fallback until runtime state exists.",
9
+ "New habit definitions created by hatch flow or `ouro habit create` stop emitting `lastRun` in tracked frontmatter, and task-system habit migration now preserves historical `lastRun`/`last_run` values by importing them into runtime state instead.",
10
+ "Habit runtime-state coverage now explicitly protects the defensive strip/fallback paths, and the agent-facing prompt text now tells the truth that runtime timestamps live in `state/habits/` so tracked habit files stay declarative."
11
+ ]
12
+ },
4
13
  {
5
14
  "version": "0.1.0-alpha.428",
6
15
  "changes": [
@@ -4007,6 +4007,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4007
4007
  // ── habit subcommands (local, no daemon socket needed) ──
4008
4008
  if (command.kind === "habit.list" || command.kind === "habit.create") {
4009
4009
  const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("../habits/habit-parser")));
4010
+ const { applyHabitRuntimeState } = await Promise.resolve().then(() => __importStar(require("../habits/habit-runtime-state")));
4010
4011
  /* v8 ignore start -- production default: uses real bundle root @preserve */
4011
4012
  const bundleRoot = deps.agentBundleRoot ?? path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
4012
4013
  /* v8 ignore stop */
@@ -4029,7 +4030,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4029
4030
  const lines = [];
4030
4031
  for (const file of files) {
4031
4032
  const fileContent = fs.readFileSync(path.join(habitsDir, file), "utf-8");
4032
- const habit = parseHabitFile(fileContent, path.join(habitsDir, file));
4033
+ const habit = applyHabitRuntimeState(bundleRoot, parseHabitFile(fileContent, path.join(habitsDir, file)));
4033
4034
  const lastRunStr = habit.lastRun ?? "never";
4034
4035
  lines.push(`${habit.name} cadence=${habit.cadence ?? "none"} status=${habit.status} lastRun=${lastRunStr}`);
4035
4036
  }
@@ -4050,7 +4051,6 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4050
4051
  title: command.name,
4051
4052
  cadence: command.cadence ?? "null",
4052
4053
  status: "active",
4053
- lastRun: now,
4054
4054
  created: now,
4055
4055
  }, `Habit: ${command.name}`);
4056
4056
  fs.writeFileSync(filePath, habitContent, "utf-8");
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const runtime_1 = require("../../nerves/runtime");
40
40
  const habit_parser_1 = require("./habit-parser");
41
+ const habit_runtime_state_1 = require("./habit-runtime-state");
41
42
  const parser_1 = require("../../repertoire/tasks/parser");
42
43
  /** Fields that belong to the task system and should be stripped from migrated habits. */
43
44
  const TASK_ONLY_FIELDS = new Set([
@@ -138,6 +139,11 @@ function migrateHabitsFromTaskSystem(bundleRoot) {
138
139
  });
139
140
  continue;
140
141
  }
142
+ const legacyLastRun = typeof frontmatter.lastRun === "string" && frontmatter.lastRun !== "null"
143
+ ? frontmatter.lastRun
144
+ : typeof frontmatter.last_run === "string" && frontmatter.last_run !== "null"
145
+ ? frontmatter.last_run
146
+ : null;
141
147
  // Build new frontmatter, stripping task-only fields
142
148
  const newFrontmatter = {};
143
149
  if (typeof frontmatter.title === "string")
@@ -145,14 +151,13 @@ function migrateHabitsFromTaskSystem(bundleRoot) {
145
151
  if (typeof frontmatter.cadence === "string")
146
152
  newFrontmatter.cadence = frontmatter.cadence;
147
153
  newFrontmatter.status = habitStatus;
148
- newFrontmatter.lastRun = typeof frontmatter.lastRun === "string" && frontmatter.lastRun !== "null"
149
- ? frontmatter.lastRun
150
- : "null";
151
154
  newFrontmatter.created = typeof frontmatter.created === "string" ? frontmatter.created : "null";
152
155
  // Add any other non-task fields from original
153
156
  for (const [key, value] of Object.entries(frontmatter)) {
154
157
  if (TASK_ONLY_FIELDS.has(key))
155
158
  continue;
159
+ if (key === "lastRun" || key === "last_run")
160
+ continue;
156
161
  if (key in newFrontmatter)
157
162
  continue;
158
163
  /* v8 ignore next -- dead code: status is caught by `key in newFrontmatter` above since newFrontmatter.status is always set @preserve */
@@ -162,6 +167,9 @@ function migrateHabitsFromTaskSystem(bundleRoot) {
162
167
  }
163
168
  const rendered = (0, habit_parser_1.renderHabitFile)(newFrontmatter, body);
164
169
  fs.writeFileSync(targetPath, rendered, "utf-8");
170
+ if (legacyLastRun) {
171
+ (0, habit_runtime_state_1.writeHabitLastRun)(bundleRoot, path.basename(slugName, ".md"), legacyLastRun);
172
+ }
165
173
  migratedCount++;
166
174
  (0, runtime_1.emitNervesEvent)({
167
175
  component: "daemon",
@@ -98,7 +98,7 @@ function parseHabitFile(content, filePath) {
98
98
  const cadence = typeof rawCadence === "string" && rawCadence.length > 0 ? rawCadence : null;
99
99
  const rawStatus = frontmatter.status;
100
100
  const status = typeof rawStatus === "string" && isHabitStatus(rawStatus) ? rawStatus : "active";
101
- const rawLastRun = frontmatter.lastRun;
101
+ const rawLastRun = frontmatter.lastRun ?? frontmatter.last_run;
102
102
  const lastRun = typeof rawLastRun === "string" && rawLastRun.length > 0 ? rawLastRun : null;
103
103
  const rawCreated = frontmatter.created;
104
104
  const created = typeof rawCreated === "string" && rawCreated.length > 0 ? rawCreated : null;
@@ -0,0 +1,100 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readHabitLastRun = readHabitLastRun;
37
+ exports.applyHabitRuntimeState = applyHabitRuntimeState;
38
+ exports.writeHabitLastRun = writeHabitLastRun;
39
+ exports.recordHabitRun = recordHabitRun;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const json_store_1 = require("../../arc/json-store");
43
+ const runtime_1 = require("../../nerves/runtime");
44
+ function habitRuntimeStateDir(agentRoot) {
45
+ return path.join(agentRoot, "state", "habits");
46
+ }
47
+ function isNonEmptyString(value) {
48
+ return typeof value === "string" && value.trim().length > 0;
49
+ }
50
+ function stripLegacyLastRunFromDefinition(definitionPath) {
51
+ const content = fs.readFileSync(definitionPath, "utf-8");
52
+ const lines = content.split(/\r?\n/);
53
+ if (lines[0]?.trim() !== "---")
54
+ return;
55
+ const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
56
+ if (closing === -1)
57
+ return;
58
+ const frontmatterLines = lines.slice(1, closing);
59
+ const filtered = frontmatterLines.filter((line) => !/^\s*lastRun\s*:/.test(line) && !/^\s*last_run\s*:/.test(line));
60
+ if (filtered.length === frontmatterLines.length)
61
+ return;
62
+ const nextContent = ["---", ...filtered, "---", ...lines.slice(closing + 1)].join("\n");
63
+ fs.writeFileSync(definitionPath, nextContent, "utf-8");
64
+ }
65
+ function readHabitLastRun(agentRoot, habitName) {
66
+ const record = (0, json_store_1.readJsonFile)(habitRuntimeStateDir(agentRoot), habitName);
67
+ return isNonEmptyString(record?.lastRun) ? record.lastRun : null;
68
+ }
69
+ function applyHabitRuntimeState(agentRoot, habit) {
70
+ const runtimeLastRun = readHabitLastRun(agentRoot, habit.name);
71
+ if (runtimeLastRun === null)
72
+ return habit;
73
+ return { ...habit, lastRun: runtimeLastRun };
74
+ }
75
+ function writeHabitLastRun(agentRoot, habitName, lastRun, updatedAt = lastRun) {
76
+ const record = {
77
+ schemaVersion: 1,
78
+ name: habitName,
79
+ lastRun,
80
+ updatedAt,
81
+ };
82
+ (0, json_store_1.writeJsonFile)(habitRuntimeStateDir(agentRoot), habitName, record);
83
+ (0, runtime_1.emitNervesEvent)({
84
+ component: "daemon",
85
+ event: "daemon.habit_runtime_state_write",
86
+ message: "wrote habit runtime state",
87
+ meta: { agentRoot, habitName, lastRun, updatedAt },
88
+ });
89
+ }
90
+ function recordHabitRun(agentRoot, habitName, lastRun, options = {}) {
91
+ writeHabitLastRun(agentRoot, habitName, lastRun);
92
+ if (!options.definitionPath)
93
+ return;
94
+ try {
95
+ stripLegacyLastRunFromDefinition(options.definitionPath);
96
+ }
97
+ catch {
98
+ // Missing/deleted habit files should never block runtime-state recording.
99
+ }
100
+ }
@@ -37,6 +37,7 @@ exports.HabitScheduler = void 0;
37
37
  const path = __importStar(require("path"));
38
38
  const runtime_1 = require("../../nerves/runtime");
39
39
  const habit_parser_1 = require("./habit-parser");
40
+ const habit_runtime_state_1 = require("./habit-runtime-state");
40
41
  const cadence_1 = require("../daemon/cadence");
41
42
  const WATCH_DEBOUNCE_MS = 200;
42
43
  class HabitScheduler {
@@ -167,7 +168,7 @@ class HabitScheduler {
167
168
  const filePath = path.join(this.habitsDir, `${name}.md`);
168
169
  try {
169
170
  const content = this.deps.readFile(filePath, "utf-8");
170
- return (0, habit_parser_1.parseHabitFile)(content, filePath);
171
+ return (0, habit_runtime_state_1.applyHabitRuntimeState)(path.dirname(this.habitsDir), (0, habit_parser_1.parseHabitFile)(content, filePath));
171
172
  }
172
173
  catch {
173
174
  return null;
@@ -323,7 +324,7 @@ class HabitScheduler {
323
324
  const filePath = path.join(this.habitsDir, file);
324
325
  try {
325
326
  const content = this.deps.readFile(filePath, "utf-8");
326
- const habit = (0, habit_parser_1.parseHabitFile)(content, filePath);
327
+ const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(path.dirname(this.habitsDir), (0, habit_parser_1.parseHabitFile)(content, filePath));
327
328
  habits.push(habit);
328
329
  }
329
330
  catch (error) {
@@ -85,7 +85,6 @@ function writeHeartbeatHabit(bundleRoot, now) {
85
85
  title: "Heartbeat check-in",
86
86
  cadence: "30m",
87
87
  status: "active",
88
- lastRun: "null",
89
88
  created: now.toISOString(),
90
89
  }, "Run a lightweight heartbeat cycle. Review task board and inbox.\nCheck on pending obligations. Journal anything important.");
91
90
  fs.writeFileSync(filePath, content, "utf-8");
@@ -46,6 +46,8 @@ exports.readDeskPrefs = readDeskPrefs;
46
46
  const fs = __importStar(require("fs"));
47
47
  const path = __importStar(require("path"));
48
48
  const runtime_1 = require("../../../nerves/runtime");
49
+ const habit_parser_1 = require("../../habits/habit-parser");
50
+ const habit_runtime_state_1 = require("../../habits/habit-runtime-state");
49
51
  const identity_1 = require("../../identity");
50
52
  const shared_1 = require("./shared");
51
53
  const agent_machine_1 = require("./agent-machine");
@@ -467,10 +469,11 @@ function readHabitView(agentRoot, options = {}) {
467
469
  if (!file.endsWith(".md"))
468
470
  continue;
469
471
  try {
470
- const raw = fs.readFileSync(path.join(habitsDir, file), "utf-8");
471
- const habit = parseHabitFrontmatter(raw);
472
- if (!habit)
472
+ const filePath = path.join(habitsDir, file);
473
+ const raw = fs.readFileSync(filePath, "utf-8");
474
+ if (!/^---\r?\n[\s\S]*?\r?\n---/.test(raw))
473
475
  continue;
476
+ const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(raw, filePath));
474
477
  const cadenceMs = parseCadenceMs(habit.cadence);
475
478
  let isOverdue = false;
476
479
  let overdueMs = null;
@@ -482,10 +485,10 @@ function readHabitView(agentRoot, options = {}) {
482
485
  }
483
486
  }
484
487
  items.push({
485
- name: habit.name ?? file.slice(0, -3),
486
- title: habit.title ?? file.slice(0, -3),
488
+ name: habit.name,
489
+ title: habit.title,
487
490
  cadence: habit.cadence,
488
- status: habit.status === "paused" ? "paused" : "active",
491
+ status: habit.status,
489
492
  lastRun: habit.lastRun,
490
493
  bodyExcerpt: (0, shared_1.truncateExcerpt)(habit.body, 120),
491
494
  isDegraded: false,
@@ -514,25 +517,6 @@ function readHabitView(agentRoot, options = {}) {
514
517
  items,
515
518
  };
516
519
  }
517
- function parseHabitFrontmatter(content) {
518
- const fmMatch = /^---\n([\s\S]*?)\n---/.exec(content);
519
- if (!fmMatch)
520
- return null;
521
- const fm = fmMatch[1];
522
- const body = content.slice(fmMatch[0].length).trim() || null;
523
- function extract(key) {
524
- const match = new RegExp(`^${key}:\\s*(.+)$`, "m").exec(fm);
525
- return match ? match[1].trim() : null;
526
- }
527
- return {
528
- name: extract("name"),
529
- title: extract("title"),
530
- cadence: extract("cadence"),
531
- status: extract("status"),
532
- lastRun: extract("lastRun") ?? extract("last_run"),
533
- body,
534
- };
535
- }
536
520
  function readNeedsMeView(agentName, options = {}) {
537
521
  const bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
538
522
  const now = options.now?.() ?? new Date();
@@ -1044,8 +1044,10 @@ when i'm done thinking and the attention queue is clear, i rest.
1044
1044
  my habits live at habits/ — they're my autonomous rhythms. heartbeat
1045
1045
  is my breathing, other habits are patterns i choose. i can read, create,
1046
1046
  and modify them with read_file/write_file. the format is simple
1047
- frontmatter (title, cadence, status, lastRun, created) plus a body
1047
+ frontmatter (title, cadence, status, created) plus a body
1048
1048
  that says what i do when the rhythm fires.
1049
+ runtime timestamps like lastRun live under state/habits/ so my tracked
1050
+ habit files stay declarative.
1049
1051
 
1050
1052
  \`ouro habit list\` shows my current habits. \`ouro habit create\` makes
1051
1053
  a new one. the cadence is personal — how often do i want each rhythm
@@ -35,13 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.createInnerDialogWorker = createInnerDialogWorker;
37
37
  exports.startInnerDialogWorker = startInnerDialogWorker;
38
- const fs = __importStar(require("fs"));
39
38
  const path = __importStar(require("path"));
40
39
  const inner_dialog_1 = require("./inner-dialog");
41
40
  const runtime_1 = require("../nerves/runtime");
42
41
  const identity_1 = require("../heart/identity");
43
42
  const pending_1 = require("../mind/pending");
44
- const habit_parser_1 = require("../heart/habits/habit-parser");
43
+ const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
45
44
  function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options), hasPendingWork = () => (0, pending_1.hasPendingMessages)((0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)()))) {
46
45
  let running = false;
47
46
  const queue = [];
@@ -71,25 +70,16 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
71
70
  },
72
71
  });
73
72
  }
74
- // Update lastRun in habit frontmatter after a habit turn
73
+ // Record lastRun after a habit turn without dirtying the tracked habit file.
75
74
  if (nextReason === "habit" && nextHabitName) {
76
75
  try {
77
76
  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");
77
+ (0, habit_runtime_state_1.recordHabitRun)(agentRoot, nextHabitName, new Date().toISOString(), {
78
+ definitionPath: path.join(agentRoot, "habits", `${nextHabitName}.md`),
79
+ });
90
80
  }
91
81
  catch {
92
- // Habit file may have been deleted during the turn — skip gracefully
82
+ // Habit file/state may be unavailable during the turn — skip gracefully
93
83
  }
94
84
  }
95
85
  // Drain queue first
@@ -71,6 +71,7 @@ const bluebubbles_1 = require("./bluebubbles");
71
71
  const habit_turn_message_1 = require("./habit-turn-message");
72
72
  const journal_index_1 = require("../mind/journal-index");
73
73
  const habit_parser_1 = require("../heart/habits/habit-parser");
74
+ const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
74
75
  const cadence_1 = require("../heart/daemon/cadence");
75
76
  const daemon_health_1 = require("../heart/daemon/daemon-health");
76
77
  const DEFAULT_INNER_DIALOG_INSTINCTS = [
@@ -493,7 +494,7 @@ function buildAlsoDueLine(agentRoot, currentHabitName, now) {
493
494
  continue;
494
495
  try {
495
496
  const content = fs.readFileSync(path.join(habitsDir, file), "utf-8");
496
- const habit = (0, habit_parser_1.parseHabitFile)(content, path.join(habitsDir, file));
497
+ const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(content, path.join(habitsDir, file)));
497
498
  if (habit.status !== "active" || !habit.cadence)
498
499
  continue;
499
500
  const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
@@ -568,7 +569,7 @@ async function runInnerDialogTurn(options) {
568
569
  let habitLastRun = null;
569
570
  try {
570
571
  const habitContent = fs.readFileSync(habitFilePath, "utf-8");
571
- const parsed = (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath);
572
+ const parsed = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath));
572
573
  habitBody = parsed.body || undefined;
573
574
  habitTitle = parsed.title || habitName;
574
575
  habitLastRun = parsed.lastRun;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.428",
3
+ "version": "0.1.0-alpha.429",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",