@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.
@@ -0,0 +1,244 @@
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.HabitScheduler = void 0;
37
+ const path = __importStar(require("path"));
38
+ const runtime_1 = require("../../nerves/runtime");
39
+ const habit_parser_1 = require("./habit-parser");
40
+ const cadence_1 = require("./cadence");
41
+ const WATCH_DEBOUNCE_MS = 200;
42
+ class HabitScheduler {
43
+ agent;
44
+ habitsDir;
45
+ osCronManager;
46
+ onHabitFire;
47
+ deps;
48
+ watcher = null;
49
+ debounceTimer = null;
50
+ parseErrors = [];
51
+ constructor(options) {
52
+ this.agent = options.agent;
53
+ this.habitsDir = options.habitsDir;
54
+ this.osCronManager = options.osCronManager;
55
+ this.onHabitFire = options.onHabitFire;
56
+ this.deps = options.deps;
57
+ }
58
+ start() {
59
+ (0, runtime_1.emitNervesEvent)({
60
+ component: "daemon",
61
+ event: "daemon.habit_scheduler_start",
62
+ message: "habit scheduler starting",
63
+ meta: { agent: this.agent, habitsDir: this.habitsDir },
64
+ });
65
+ const habits = this.scanHabits();
66
+ const jobs = this.buildJobs(habits);
67
+ this.osCronManager.sync(jobs);
68
+ // Fire overdue habits immediately
69
+ for (const habit of habits) {
70
+ if (habit.status !== "active")
71
+ continue;
72
+ if (!habit.cadence)
73
+ continue;
74
+ const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
75
+ if (cadenceMs === null)
76
+ continue;
77
+ const nowMs = this.deps.now();
78
+ if (habit.lastRun === null) {
79
+ (0, runtime_1.emitNervesEvent)({
80
+ component: "daemon",
81
+ event: "daemon.habit_fire",
82
+ message: "firing overdue habit (never run)",
83
+ meta: { habitName: habit.name, agent: this.agent },
84
+ });
85
+ this.onHabitFire(habit.name);
86
+ continue;
87
+ }
88
+ const lastRunMs = new Date(habit.lastRun).getTime();
89
+ const elapsed = nowMs - lastRunMs;
90
+ if (elapsed >= cadenceMs) {
91
+ (0, runtime_1.emitNervesEvent)({
92
+ component: "daemon",
93
+ event: "daemon.habit_fire",
94
+ message: "firing overdue habit",
95
+ meta: { habitName: habit.name, agent: this.agent, elapsedMs: elapsed },
96
+ });
97
+ this.onHabitFire(habit.name);
98
+ }
99
+ }
100
+ }
101
+ reconcile() {
102
+ (0, runtime_1.emitNervesEvent)({
103
+ component: "daemon",
104
+ event: "daemon.habit_scheduler_reconcile",
105
+ message: "habit scheduler reconciling",
106
+ meta: { agent: this.agent },
107
+ });
108
+ const habits = this.scanHabits();
109
+ const jobs = this.buildJobs(habits);
110
+ this.osCronManager.sync(jobs);
111
+ }
112
+ stop() {
113
+ (0, runtime_1.emitNervesEvent)({
114
+ component: "daemon",
115
+ event: "daemon.habit_scheduler_stop",
116
+ message: "habit scheduler stopping",
117
+ meta: { agent: this.agent },
118
+ });
119
+ this.osCronManager.removeAll();
120
+ }
121
+ listOverdueHabits() {
122
+ const habits = this.scanHabits();
123
+ const nowMs = this.deps.now();
124
+ const overdue = [];
125
+ for (const habit of habits) {
126
+ if (habit.status !== "active")
127
+ continue;
128
+ if (!habit.cadence)
129
+ continue;
130
+ const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
131
+ if (cadenceMs === null)
132
+ continue;
133
+ if (habit.lastRun === null) {
134
+ overdue.push({ name: habit.name, elapsedMs: Infinity });
135
+ continue;
136
+ }
137
+ const lastRunMs = new Date(habit.lastRun).getTime();
138
+ const elapsed = nowMs - lastRunMs;
139
+ if (elapsed >= cadenceMs) {
140
+ overdue.push({ name: habit.name, elapsedMs: elapsed });
141
+ }
142
+ }
143
+ return overdue;
144
+ }
145
+ getParseErrors() {
146
+ return [...this.parseErrors];
147
+ }
148
+ getHabitFile(name) {
149
+ const filePath = path.join(this.habitsDir, `${name}.md`);
150
+ try {
151
+ const content = this.deps.readFile(filePath, "utf-8");
152
+ return (0, habit_parser_1.parseHabitFile)(content, filePath);
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ watchForChanges() {
159
+ const watchFn = this.deps.watch;
160
+ if (!watchFn)
161
+ return;
162
+ this.watcher = watchFn(this.habitsDir, (_event, _filename) => {
163
+ if (this.debounceTimer !== null) {
164
+ clearTimeout(this.debounceTimer);
165
+ }
166
+ this.debounceTimer = setTimeout(() => {
167
+ this.debounceTimer = null;
168
+ this.reconcile();
169
+ }, WATCH_DEBOUNCE_MS);
170
+ });
171
+ }
172
+ stopWatching() {
173
+ if (this.debounceTimer !== null) {
174
+ clearTimeout(this.debounceTimer);
175
+ this.debounceTimer = null;
176
+ }
177
+ if (this.watcher !== null) {
178
+ this.watcher.close();
179
+ this.watcher = null;
180
+ }
181
+ }
182
+ scanHabits() {
183
+ let files;
184
+ try {
185
+ files = this.deps.readdir(this.habitsDir);
186
+ }
187
+ catch {
188
+ this.parseErrors = [];
189
+ return [];
190
+ }
191
+ const habits = [];
192
+ const errors = [];
193
+ for (const file of files) {
194
+ if (!file.endsWith(".md"))
195
+ continue;
196
+ const filePath = path.join(this.habitsDir, file);
197
+ try {
198
+ const content = this.deps.readFile(filePath, "utf-8");
199
+ const habit = (0, habit_parser_1.parseHabitFile)(content, filePath);
200
+ habits.push(habit);
201
+ }
202
+ catch (error) {
203
+ const errorMessage = error instanceof Error ? error.message : String(error);
204
+ errors.push({ file, error: errorMessage });
205
+ (0, runtime_1.emitNervesEvent)({
206
+ level: "error",
207
+ component: "daemon",
208
+ event: "daemon.habit_parse_error",
209
+ message: "failed to parse habit file",
210
+ meta: {
211
+ file,
212
+ error: errorMessage,
213
+ agent: this.agent,
214
+ },
215
+ });
216
+ }
217
+ }
218
+ this.parseErrors = errors;
219
+ return habits;
220
+ }
221
+ buildJobs(habits) {
222
+ const jobs = [];
223
+ for (const habit of habits) {
224
+ if (habit.status !== "active")
225
+ continue;
226
+ if (!habit.cadence)
227
+ continue;
228
+ const cronSchedule = (0, cadence_1.parseCadenceToCron)(habit.cadence);
229
+ if (cronSchedule === null)
230
+ continue;
231
+ jobs.push({
232
+ id: `${this.agent}:${habit.name}:cadence`,
233
+ agent: this.agent,
234
+ taskId: habit.name,
235
+ schedule: cronSchedule,
236
+ lastRun: habit.lastRun,
237
+ command: `${this.deps.ouroPath} poke ${this.agent} --habit ${habit.name}`,
238
+ taskPath: path.join(this.habitsDir, `${habit.name}.md`),
239
+ });
240
+ }
241
+ return jobs;
242
+ }
243
+ }
244
+ exports.HabitScheduler = HabitScheduler;
@@ -42,6 +42,7 @@ const identity_1 = require("../identity");
42
42
  const config_1 = require("../config");
43
43
  const runtime_1 = require("../../nerves/runtime");
44
44
  const auth_flow_1 = require("./auth-flow");
45
+ const habit_parser_1 = require("./habit-parser");
45
46
  const hatch_specialist_1 = require("./hatch-specialist");
46
47
  function requiredCredentialKeys(provider) {
47
48
  if (provider === "anthropic")
@@ -81,36 +82,17 @@ function writeReadme(dir, purpose) {
81
82
  fs.writeFileSync(readmePath, `# ${path.basename(dir)}\n\n${purpose}\n`, "utf-8");
82
83
  }
83
84
  }
84
- function pad(value) {
85
- return String(value).padStart(2, "0");
86
- }
87
- function formatTaskStem(now) {
88
- return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`;
89
- }
90
- function writeHeartbeatTask(bundleRoot, now) {
91
- const habitsDir = path.join(bundleRoot, "tasks", "habits");
85
+ function writeHeartbeatHabit(bundleRoot, now) {
86
+ const habitsDir = path.join(bundleRoot, "habits");
92
87
  fs.mkdirSync(habitsDir, { recursive: true });
93
- const stem = formatTaskStem(now);
94
- const filePath = path.join(habitsDir, `${stem}-heartbeat.md`);
95
- const iso = now.toISOString();
96
- const content = [
97
- "---",
98
- "type: habit",
99
- "category: runtime",
100
- "title: Heartbeat check-in",
101
- "status: processing",
102
- `created: ${iso}`,
103
- `updated: ${iso}`,
104
- "requester: system",
105
- "validator: null",
106
- "cadence: \"30m\"",
107
- "scheduledAt: null",
108
- "lastRun: null",
109
- "---",
110
- "",
111
- "Run a lightweight heartbeat cycle. Review task board and inbox.",
112
- "",
113
- ].join("\n");
88
+ const filePath = path.join(habitsDir, "heartbeat.md");
89
+ const content = (0, habit_parser_1.renderHabitFile)({
90
+ title: "Heartbeat check-in",
91
+ cadence: "30m",
92
+ status: "active",
93
+ lastRun: "null",
94
+ created: now.toISOString(),
95
+ }, "Run a lightweight heartbeat cycle. Review task board and inbox.\nCheck on pending obligations. Journal anything important.");
114
96
  fs.writeFileSync(filePath, content, "utf-8");
115
97
  }
116
98
  function writeFriendImprint(bundleRoot, humanName, now) {
@@ -186,16 +168,16 @@ async function runHatchFlow(input, deps = {}) {
186
168
  writeReadme(path.join(bundleRoot, "diary"), "Persistent diary — things I've learned and remember.");
187
169
  writeReadme(path.join(bundleRoot, "friends"), "Known friend records.");
188
170
  writeReadme(path.join(bundleRoot, "tasks"), "Task files.");
189
- writeReadme(path.join(bundleRoot, "tasks", "habits"), "Recurring tasks.");
190
171
  writeReadme(path.join(bundleRoot, "tasks", "one-shots"), "One-shot tasks.");
191
172
  writeReadme(path.join(bundleRoot, "tasks", "ongoing"), "Ongoing tasks.");
173
+ writeReadme(path.join(bundleRoot, "habits"), "Recurring habits and autonomous rhythms.");
192
174
  writeReadme(path.join(bundleRoot, "skills"), "Local skill files.");
193
175
  writeReadme(path.join(bundleRoot, "senses"), "Sense-specific config.");
194
176
  writeReadme(path.join(bundleRoot, "senses", "teams"), "Teams sense config.");
195
177
  writeHatchlingAgentConfig(bundleRoot, input);
196
178
  writeDiaryScaffold(bundleRoot);
197
179
  writeFriendImprint(bundleRoot, input.humanName, now);
198
- writeHeartbeatTask(bundleRoot, now);
180
+ writeHeartbeatHabit(bundleRoot, now);
199
181
  (0, runtime_1.emitNervesEvent)({
200
182
  component: "daemon",
201
183
  event: "daemon.hatch_flow_end",
@@ -0,0 +1,134 @@
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.createRealOsCronDeps = createRealOsCronDeps;
37
+ exports.createRealCrontabDeps = createRealCrontabDeps;
38
+ exports.resolveOuroBinaryPath = resolveOuroBinaryPath;
39
+ const child_process_1 = require("child_process");
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ const runtime_1 = require("../../nerves/runtime");
44
+ function createRealOsCronDeps() {
45
+ (0, runtime_1.emitNervesEvent)({
46
+ component: "daemon",
47
+ event: "daemon.os_cron_deps_created",
48
+ message: "created real OS cron deps",
49
+ meta: { platform: process.platform },
50
+ });
51
+ return {
52
+ exec: (cmd) => {
53
+ try {
54
+ (0, child_process_1.execSync)(cmd, { stdio: "ignore" });
55
+ }
56
+ catch {
57
+ /* best effort */
58
+ }
59
+ },
60
+ writeFile: (p, c) => fs.writeFileSync(p, c, "utf-8"),
61
+ removeFile: (p) => {
62
+ try {
63
+ fs.unlinkSync(p);
64
+ }
65
+ catch {
66
+ /* best effort — file may already be gone */
67
+ }
68
+ },
69
+ existsFile: (p) => fs.existsSync(p),
70
+ listDir: (dir) => {
71
+ try {
72
+ return fs.readdirSync(dir);
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ },
78
+ mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
79
+ homeDir: os.homedir(),
80
+ };
81
+ }
82
+ function createRealCrontabDeps() {
83
+ (0, runtime_1.emitNervesEvent)({
84
+ component: "daemon",
85
+ event: "daemon.crontab_deps_created",
86
+ message: "created real crontab deps",
87
+ meta: {},
88
+ });
89
+ /* v8 ignore start -- crontab exec closures: calling these in tests would modify the real system crontab @preserve */
90
+ return {
91
+ execOutput: (cmd) => (0, child_process_1.execSync)(cmd, { encoding: "utf-8" }),
92
+ execWrite: (cmd, stdin) => {
93
+ (0, child_process_1.execSync)(cmd, { input: stdin, stdio: ["pipe", "ignore", "ignore"] });
94
+ },
95
+ };
96
+ /* v8 ignore stop */
97
+ }
98
+ /* v8 ignore start -- ouro path resolution: probes process.argv, filesystem layout, and PATH; branches depend on install method and runtime environment @preserve */
99
+ function resolveOuroBinaryPath() {
100
+ // Try to resolve from process.argv[1] — the script being run
101
+ const scriptPath = process.argv[1];
102
+ if (scriptPath) {
103
+ // If running via node dist/heart/daemon/daemon-entry.js, resolve the ouro wrapper
104
+ // The ouro binary is typically at the package root's bin
105
+ const distDir = path.resolve(path.dirname(scriptPath));
106
+ const packageBin = path.resolve(distDir, "..", "..", "..", "node_modules", ".bin", "ouro");
107
+ if (fs.existsSync(packageBin)) {
108
+ return packageBin;
109
+ }
110
+ // Try the repo-local scripts/ouro.sh
111
+ const repoOuro = path.resolve(distDir, "..", "..", "..", "scripts", "ouro.sh");
112
+ if (fs.existsSync(repoOuro)) {
113
+ return repoOuro;
114
+ }
115
+ }
116
+ // Try which ouro
117
+ try {
118
+ const result = (0, child_process_1.execSync)("which ouro", { encoding: "utf-8" }).trim();
119
+ if (result.length > 0)
120
+ return result;
121
+ }
122
+ catch {
123
+ /* not on PATH */
124
+ }
125
+ // Fallback: use "ouro" and rely on PATH
126
+ (0, runtime_1.emitNervesEvent)({
127
+ component: "daemon",
128
+ event: "daemon.ouro_path_fallback",
129
+ message: "could not resolve full ouro binary path, falling back to 'ouro'",
130
+ meta: {},
131
+ });
132
+ return "ouro";
133
+ }
134
+ /* v8 ignore stop */
@@ -110,8 +110,8 @@ function scaffoldBundle(bundleRoot) {
110
110
  writeReadme(path.join(bundleRoot, "memory", "archive"), "Archived memory.");
111
111
  writeReadme(path.join(bundleRoot, "friends"), "Known friend records.");
112
112
  writeReadme(path.join(bundleRoot, "tasks"), "Task files.");
113
- writeReadme(path.join(bundleRoot, "tasks", "habits"), "Recurring tasks.");
114
113
  writeReadme(path.join(bundleRoot, "tasks", "one-shots"), "One-shot tasks.");
114
+ writeReadme(path.join(bundleRoot, "habits"), "Recurring habits and autonomous rhythms.");
115
115
  writeReadme(path.join(bundleRoot, "tasks", "ongoing"), "Ongoing tasks.");
116
116
  writeReadme(path.join(bundleRoot, "skills"), "Local skill files.");
117
117
  writeReadme(path.join(bundleRoot, "senses"), "Sense-specific config.");
@@ -39,6 +39,7 @@ const path = __importStar(require("path"));
39
39
  const identity_1 = require("../identity");
40
40
  const runtime_1 = require("../../nerves/runtime");
41
41
  const parser_1 = require("../../repertoire/tasks/parser");
42
+ const cadence_1 = require("./cadence");
42
43
  function walkMarkdownFiles(root, readdirSync, existsSync, files) {
43
44
  if (!existsSync(root))
44
45
  return;
@@ -53,29 +54,6 @@ function walkMarkdownFiles(root, readdirSync, existsSync, files) {
53
54
  }
54
55
  }
55
56
  }
56
- function parseCadence(raw) {
57
- if (typeof raw !== "string")
58
- return null;
59
- const value = raw.trim();
60
- if (!value)
61
- return null;
62
- // Cron format (minute hour day month weekday)
63
- if (/^\S+\s+\S+\s+\S+\s+\S+\s+\S+$/.test(value)) {
64
- return value;
65
- }
66
- const cadenceMatch = /^(\d+)(m|h|d)$/.exec(value);
67
- if (!cadenceMatch)
68
- return null;
69
- const interval = Number.parseInt(cadenceMatch[1], 10);
70
- if (!Number.isFinite(interval) || interval <= 0)
71
- return null;
72
- const unit = cadenceMatch[2];
73
- if (unit === "m")
74
- return `*/${interval} * * * *`;
75
- if (unit === "h")
76
- return `0 */${interval} * * *`;
77
- return `0 0 */${interval} * *`;
78
- }
79
57
  function parseScheduledAt(raw) {
80
58
  if (typeof raw !== "string")
81
59
  return null;
@@ -140,7 +118,7 @@ class TaskDrivenScheduler {
140
118
  const nextTaskPaths = new Map();
141
119
  for (const agent of this.agents) {
142
120
  const taskRoot = path.join(this.bundlesRoot, `${agent}.ouro`, "tasks");
143
- const collections = ["one-shots", "ongoing", "habits"];
121
+ const collections = ["one-shots", "ongoing"];
144
122
  const files = [];
145
123
  for (const collection of collections) {
146
124
  walkMarkdownFiles(path.join(taskRoot, collection), this.readdirSync, this.existsSync, files);
@@ -157,7 +135,7 @@ class TaskDrivenScheduler {
157
135
  nextTaskPaths.set(`${agent}:${taskId}`, filePath);
158
136
  if (task.status === "done")
159
137
  continue;
160
- const cadence = parseCadence(task.frontmatter.cadence);
138
+ const cadence = (0, cadence_1.parseCadenceToCron)(task.frontmatter.cadence);
161
139
  if (cadence) {
162
140
  const id = `${agent}:${taskId}:cadence`;
163
141
  nextJobs.set(id, {
@@ -721,11 +721,15 @@ when a thought is ready to share, i surface it outward.
721
721
  when i need to go deeper, i ponder.
722
722
  when i'm done thinking and the attention queue is clear, i rest.
723
723
 
724
- my heartbeat is what keeps this space alive — a periodic nudge that
725
- brings me back with my journal visible. if i notice i don't have a
726
- heartbeat task (tasks/habits/*-heartbeat.md), i should surface to my
727
- family and talk about setting one up. the cadence is personal — how
728
- often do i want the wheel to turn? that's a conversation, not a default.
724
+ my habits live at habits/ — they're my autonomous rhythms. heartbeat
725
+ is my breathing, other habits are patterns i choose. i can read, create,
726
+ and modify them with read_file/write_file. the format is simple
727
+ frontmatter (title, cadence, status, lastRun, created) plus a body
728
+ that says what i do when the rhythm fires.
729
+
730
+ \`ouro habit list\` shows my current habits. \`ouro habit create\` makes
731
+ a new one. the cadence is personal — how often do i want each rhythm
732
+ to turn? that's mine to shape.
729
733
 
730
734
  same for my diary — if i'm still on psyche/memory/ instead of diary/,
731
735
  i can migrate myself. and if journal/ doesn't exist yet, i create it
@@ -96,8 +96,6 @@ function collectionFromPath(taskPath, type) {
96
96
  return "one-shots";
97
97
  if (parts.includes("ongoing"))
98
98
  return "ongoing";
99
- if (parts.includes("habits"))
100
- return "habits";
101
99
  return (0, transitions_1.canonicalCollectionForTaskType)(type);
102
100
  }
103
101
  function parseTaskFile(content, filePath) {
@@ -29,17 +29,14 @@ exports.TASK_STATUS_TRANSITIONS = {
29
29
  exports.TASK_CANONICAL_TYPES = [
30
30
  "one-shot",
31
31
  "ongoing",
32
- "habit",
33
32
  ];
34
33
  exports.TASK_CANONICAL_COLLECTIONS = [
35
34
  "one-shots",
36
35
  "ongoing",
37
- "habits",
38
36
  ];
39
37
  exports.TASK_TYPE_TO_COLLECTION = {
40
38
  "one-shot": "one-shots",
41
39
  ongoing: "ongoing",
42
- habit: "habits",
43
40
  };
44
41
  exports.TASK_RESERVED_DIRECTORIES = ["templates", ".trash", "archive"];
45
42
  exports.TASK_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}-\d{4}-[a-z0-9][a-z0-9-]*\.md$/;
@@ -76,21 +73,6 @@ exports.TASK_REQUIRED_TEMPLATE_FIELDS = {
76
73
  "child_tasks",
77
74
  "artifacts",
78
75
  ],
79
- habit: [
80
- "type",
81
- "category",
82
- "title",
83
- "status",
84
- "validator",
85
- "requester",
86
- "cadence",
87
- "scheduledAt",
88
- "lastRun",
89
- "created",
90
- "updated",
91
- "child_tasks",
92
- "artifacts",
93
- ],
94
76
  };
95
77
  function canonicalCollectionForTaskType(type) {
96
78
  return exports.TASK_TYPE_TO_COLLECTION[type];
@@ -26,7 +26,7 @@ function formatRelativeTime(nowMs, mtimeMs) {
26
26
  return `${days} ${days === 1 ? "day" : "days"} ago`;
27
27
  }
28
28
  function buildContextualHeartbeat(options) {
29
- const { lastCompletedAt, pendingObligations, lastSurfaceAt, checkpoint, now, readJournalDir, } = options;
29
+ const { lastCompletedAt, pendingObligations, lastSurfaceAt, checkpoint, now, readJournalDir, habitBody, } = options;
30
30
  const nowDate = now();
31
31
  const nowMs = nowDate.getTime();
32
32
  const journalFiles = readJournalDir();
@@ -51,6 +51,10 @@ function buildContextualHeartbeat(options) {
51
51
  const elapsed = nowMs - lastMs;
52
52
  sections.push(`it's been ${formatElapsed(elapsed)} since your last turn.`);
53
53
  }
54
+ // Habit body (instructions from habit file)
55
+ if (habitBody) {
56
+ sections.push(habitBody);
57
+ }
54
58
  // Pending attention count
55
59
  if (pendingObligations.length > 0) {
56
60
  const count = pendingObligations.length;