@ouro.bot/cli 0.1.0-alpha.147 → 0.1.0-alpha.149
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 +27 -0
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/daemon-cli.js +141 -17
- package/dist/heart/daemon/daemon-entry.js +67 -17
- package/dist/heart/daemon/daemon-tombstone.js +106 -0
- package/dist/heart/daemon/daemon.js +71 -2
- package/dist/heart/daemon/habit-migration.js +181 -0
- package/dist/heart/daemon/habit-parser.js +119 -0
- package/dist/heart/daemon/habit-scheduler.js +244 -0
- package/dist/heart/daemon/hatch-flow.js +13 -31
- package/dist/heart/daemon/os-cron-deps.js +134 -0
- package/dist/heart/daemon/process-manager.js +51 -0
- package/dist/heart/daemon/specialist-tools.js +1 -1
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/mind/prompt.js +9 -5
- package/dist/nerves/index.js +47 -2
- package/dist/repertoire/tasks/parser.js +0 -2
- package/dist/repertoire/tasks/transitions.js +0 -18
- package/dist/senses/contextual-heartbeat.js +5 -1
- package/dist/senses/inner-dialog-worker.js +84 -20
- package/dist/senses/inner-dialog.js +145 -39
- package/package.json +1 -1
- package/dist/heart/daemon/heartbeat-timer.js +0 -132
|
@@ -135,6 +135,8 @@ function buildWorkerRows(snapshots) {
|
|
|
135
135
|
pid: snapshot.pid,
|
|
136
136
|
restartCount: snapshot.restartCount,
|
|
137
137
|
startedAt: snapshot.startedAt,
|
|
138
|
+
lastExitCode: snapshot.lastExitCode ?? null,
|
|
139
|
+
lastSignal: snapshot.lastSignal ?? null,
|
|
138
140
|
}));
|
|
139
141
|
}
|
|
140
142
|
function formatStatusSummary(payload) {
|
|
@@ -269,12 +271,36 @@ class OuroDaemon {
|
|
|
269
271
|
this.server = net.createServer((connection) => {
|
|
270
272
|
let raw = "";
|
|
271
273
|
let responded = false;
|
|
274
|
+
/* v8 ignore start — connection error handler requires real socket error @preserve */
|
|
275
|
+
connection.on("error", (err) => {
|
|
276
|
+
(0, runtime_1.emitNervesEvent)({
|
|
277
|
+
level: "warn",
|
|
278
|
+
component: "daemon",
|
|
279
|
+
event: "daemon.connection_error",
|
|
280
|
+
message: "socket connection error",
|
|
281
|
+
meta: { error: err.message, code: err.code ?? null },
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
/* v8 ignore stop */
|
|
272
285
|
const flushResponse = async () => {
|
|
273
286
|
if (responded)
|
|
274
287
|
return;
|
|
275
288
|
responded = true;
|
|
276
289
|
const response = await this.handleRawPayload(raw);
|
|
277
|
-
|
|
290
|
+
try {
|
|
291
|
+
connection.end(response);
|
|
292
|
+
/* v8 ignore start — EPIPE catch requires real socket disconnect @preserve */
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
(0, runtime_1.emitNervesEvent)({
|
|
296
|
+
level: "warn",
|
|
297
|
+
component: "daemon",
|
|
298
|
+
event: "daemon.connection_end_error",
|
|
299
|
+
message: "failed to send response to client (EPIPE)",
|
|
300
|
+
meta: { error: err instanceof Error ? err.message : String(err) },
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/* v8 ignore stop */
|
|
278
304
|
};
|
|
279
305
|
connection.on("data", (chunk) => {
|
|
280
306
|
raw += chunk.toString("utf-8");
|
|
@@ -287,7 +313,22 @@ class OuroDaemon {
|
|
|
287
313
|
const server = this.server;
|
|
288
314
|
await new Promise((resolve, reject) => {
|
|
289
315
|
server.once("error", reject);
|
|
290
|
-
server.listen(this.socketPath, () =>
|
|
316
|
+
server.listen(this.socketPath, () => {
|
|
317
|
+
// Replace the one-time error listener with a persistent one after successful listen
|
|
318
|
+
server.removeAllListeners("error");
|
|
319
|
+
/* v8 ignore start — server error after listen requires real socket race condition @preserve */
|
|
320
|
+
server.on("error", (err) => {
|
|
321
|
+
(0, runtime_1.emitNervesEvent)({
|
|
322
|
+
level: "error",
|
|
323
|
+
component: "daemon",
|
|
324
|
+
event: "daemon.server_error",
|
|
325
|
+
message: "daemon server error after listen",
|
|
326
|
+
meta: { error: err.message, code: err.code ?? null },
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
/* v8 ignore stop */
|
|
330
|
+
resolve();
|
|
331
|
+
});
|
|
291
332
|
});
|
|
292
333
|
}
|
|
293
334
|
async drainPendingBundleMessages() {
|
|
@@ -467,6 +508,27 @@ class OuroDaemon {
|
|
|
467
508
|
message: "handling daemon command",
|
|
468
509
|
meta: { kind: command.kind },
|
|
469
510
|
});
|
|
511
|
+
try {
|
|
512
|
+
return await this.handleCommandInner(command);
|
|
513
|
+
/* v8 ignore start — command error catch tested in daemon-command-error.test; instanceof branches defensive @preserve */
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
(0, runtime_1.emitNervesEvent)({
|
|
517
|
+
level: "error",
|
|
518
|
+
component: "daemon",
|
|
519
|
+
event: "daemon.command_error",
|
|
520
|
+
message: "unexpected error handling daemon command",
|
|
521
|
+
meta: {
|
|
522
|
+
kind: command.kind,
|
|
523
|
+
error: error instanceof Error ? error.message : String(error),
|
|
524
|
+
stack: error instanceof Error ? error.stack ?? null : null,
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
/* v8 ignore stop */
|
|
530
|
+
}
|
|
531
|
+
async handleCommandInner(command) {
|
|
470
532
|
switch (command.kind) {
|
|
471
533
|
case "daemon.start":
|
|
472
534
|
await this.start();
|
|
@@ -606,6 +668,13 @@ class OuroDaemon {
|
|
|
606
668
|
data: receipt,
|
|
607
669
|
};
|
|
608
670
|
}
|
|
671
|
+
case "habit.poke": {
|
|
672
|
+
this.processManager.sendToAgent?.(command.agent, { type: "habit", habitName: command.habitName });
|
|
673
|
+
return {
|
|
674
|
+
ok: true,
|
|
675
|
+
message: `poked habit ${command.habitName} for ${command.agent}`,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
609
678
|
case "mcp.list": {
|
|
610
679
|
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)();
|
|
611
680
|
if (!mcpManager) {
|
|
@@ -0,0 +1,181 @@
|
|
|
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.migrateHabitsFromTaskSystem = migrateHabitsFromTaskSystem;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
40
|
+
const habit_parser_1 = require("./habit-parser");
|
|
41
|
+
const parser_1 = require("../../repertoire/tasks/parser");
|
|
42
|
+
/** Fields that belong to the task system and should be stripped from migrated habits. */
|
|
43
|
+
const TASK_ONLY_FIELDS = new Set([
|
|
44
|
+
"type",
|
|
45
|
+
"category",
|
|
46
|
+
"requester",
|
|
47
|
+
"validator",
|
|
48
|
+
"scheduledAt",
|
|
49
|
+
"updated",
|
|
50
|
+
"depends_on",
|
|
51
|
+
"parent_task",
|
|
52
|
+
"artifacts",
|
|
53
|
+
]);
|
|
54
|
+
/** Regex matching the YYYY-MM-DD-HHMM- timestamp prefix in task filenames. */
|
|
55
|
+
const TIMESTAMP_PREFIX = /^\d{4}-\d{2}-\d{2}-\d{4}-/;
|
|
56
|
+
/** Map old task statuses to habit statuses. */
|
|
57
|
+
function mapStatus(taskStatus) {
|
|
58
|
+
if (taskStatus === "processing")
|
|
59
|
+
return "active";
|
|
60
|
+
if (taskStatus === "paused")
|
|
61
|
+
return "paused";
|
|
62
|
+
if (taskStatus === "done")
|
|
63
|
+
return null; // skip done habits
|
|
64
|
+
return "active"; // default to active for unknown statuses
|
|
65
|
+
}
|
|
66
|
+
/** Strip timestamp prefix from filename to get the slug name. */
|
|
67
|
+
function stripTimestampPrefix(filename) {
|
|
68
|
+
return filename.replace(TIMESTAMP_PREFIX, "");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Migrate habit files from the old `tasks/habits/` location to the new `habits/` bundle root.
|
|
72
|
+
* - Strips timestamp prefix from filenames
|
|
73
|
+
* - Maps task statuses to habit statuses
|
|
74
|
+
* - Strips task-only frontmatter fields
|
|
75
|
+
* - Preserves body text
|
|
76
|
+
* - Skips done habits, README files, and already-migrated habits
|
|
77
|
+
* - No-op if `tasks/habits/` does not exist
|
|
78
|
+
*/
|
|
79
|
+
function migrateHabitsFromTaskSystem(bundleRoot) {
|
|
80
|
+
const oldHabitsDir = path.join(bundleRoot, "tasks", "habits");
|
|
81
|
+
if (!fs.existsSync(oldHabitsDir)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
let files;
|
|
85
|
+
try {
|
|
86
|
+
files = fs.readdirSync(oldHabitsDir);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* v8 ignore next -- race condition: dir removed between existsSync and readdirSync @preserve */
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const mdFiles = files.filter((f) => f.endsWith(".md") && f !== "README.md");
|
|
93
|
+
if (mdFiles.length === 0)
|
|
94
|
+
return;
|
|
95
|
+
const newHabitsDir = path.join(bundleRoot, "habits");
|
|
96
|
+
fs.mkdirSync(newHabitsDir, { recursive: true });
|
|
97
|
+
let migratedCount = 0;
|
|
98
|
+
for (const file of mdFiles) {
|
|
99
|
+
const slugName = stripTimestampPrefix(file);
|
|
100
|
+
const targetPath = path.join(newHabitsDir, slugName);
|
|
101
|
+
// Skip if already migrated
|
|
102
|
+
if (fs.existsSync(targetPath)) {
|
|
103
|
+
(0, runtime_1.emitNervesEvent)({
|
|
104
|
+
component: "daemon",
|
|
105
|
+
event: "daemon.habit_migration_skip",
|
|
106
|
+
message: "habit already exists at target, skipping",
|
|
107
|
+
meta: { file, targetPath },
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const sourcePath = path.join(oldHabitsDir, file);
|
|
112
|
+
let content;
|
|
113
|
+
try {
|
|
114
|
+
content = fs.readFileSync(sourcePath, "utf-8");
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Parse frontmatter and body
|
|
120
|
+
const lines = content.split(/\r?\n/);
|
|
121
|
+
if (lines[0]?.trim() !== "---")
|
|
122
|
+
continue;
|
|
123
|
+
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
124
|
+
if (closing === -1)
|
|
125
|
+
continue;
|
|
126
|
+
const rawFrontmatter = lines.slice(1, closing).join("\n");
|
|
127
|
+
const body = lines.slice(closing + 1).join("\n").trim();
|
|
128
|
+
const frontmatter = (0, parser_1.parseFrontmatter)(rawFrontmatter);
|
|
129
|
+
// Check status — skip done habits
|
|
130
|
+
const rawStatus = typeof frontmatter.status === "string" ? frontmatter.status : "processing";
|
|
131
|
+
const habitStatus = mapStatus(rawStatus);
|
|
132
|
+
if (habitStatus === null) {
|
|
133
|
+
(0, runtime_1.emitNervesEvent)({
|
|
134
|
+
component: "daemon",
|
|
135
|
+
event: "daemon.habit_migration_skip",
|
|
136
|
+
message: "skipping done habit",
|
|
137
|
+
meta: { file, status: rawStatus },
|
|
138
|
+
});
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Build new frontmatter, stripping task-only fields
|
|
142
|
+
const newFrontmatter = {};
|
|
143
|
+
if (typeof frontmatter.title === "string")
|
|
144
|
+
newFrontmatter.title = frontmatter.title;
|
|
145
|
+
if (typeof frontmatter.cadence === "string")
|
|
146
|
+
newFrontmatter.cadence = frontmatter.cadence;
|
|
147
|
+
newFrontmatter.status = habitStatus;
|
|
148
|
+
newFrontmatter.lastRun = typeof frontmatter.lastRun === "string" && frontmatter.lastRun !== "null"
|
|
149
|
+
? frontmatter.lastRun
|
|
150
|
+
: "null";
|
|
151
|
+
newFrontmatter.created = typeof frontmatter.created === "string" ? frontmatter.created : "null";
|
|
152
|
+
// Add any other non-task fields from original
|
|
153
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
154
|
+
if (TASK_ONLY_FIELDS.has(key))
|
|
155
|
+
continue;
|
|
156
|
+
if (key in newFrontmatter)
|
|
157
|
+
continue;
|
|
158
|
+
/* v8 ignore next -- dead code: status is caught by `key in newFrontmatter` above since newFrontmatter.status is always set @preserve */
|
|
159
|
+
if (key === "status")
|
|
160
|
+
continue; // already mapped
|
|
161
|
+
newFrontmatter[key] = value;
|
|
162
|
+
}
|
|
163
|
+
const rendered = (0, habit_parser_1.renderHabitFile)(newFrontmatter, body);
|
|
164
|
+
fs.writeFileSync(targetPath, rendered, "utf-8");
|
|
165
|
+
migratedCount++;
|
|
166
|
+
(0, runtime_1.emitNervesEvent)({
|
|
167
|
+
component: "daemon",
|
|
168
|
+
event: "daemon.habit_migrated",
|
|
169
|
+
message: "migrated habit from task system",
|
|
170
|
+
meta: { from: sourcePath, to: targetPath },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (migratedCount > 0) {
|
|
174
|
+
(0, runtime_1.emitNervesEvent)({
|
|
175
|
+
component: "daemon",
|
|
176
|
+
event: "daemon.habit_migration_complete",
|
|
177
|
+
message: "habit migration complete",
|
|
178
|
+
meta: { bundleRoot, count: migratedCount },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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.parseHabitFile = parseHabitFile;
|
|
37
|
+
exports.renderHabitFile = renderHabitFile;
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const parser_1 = require("../../repertoire/tasks/parser");
|
|
40
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
41
|
+
function isHabitStatus(value) {
|
|
42
|
+
return value === "active" || value === "paused";
|
|
43
|
+
}
|
|
44
|
+
function extractFrontmatterAndBody(content) {
|
|
45
|
+
const lines = content.split(/\r?\n/);
|
|
46
|
+
if (lines[0]?.trim() !== "---") {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
50
|
+
if (closing === -1) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const rawFrontmatter = lines.slice(1, closing).join("\n");
|
|
54
|
+
const body = lines.slice(closing + 1).join("\n").trim();
|
|
55
|
+
return { frontmatter: (0, parser_1.parseFrontmatter)(rawFrontmatter), body };
|
|
56
|
+
}
|
|
57
|
+
function parseHabitFile(content, filePath) {
|
|
58
|
+
(0, runtime_1.emitNervesEvent)({
|
|
59
|
+
event: "daemon.habit_parse",
|
|
60
|
+
component: "daemon",
|
|
61
|
+
message: "parsing habit file",
|
|
62
|
+
meta: { filePath },
|
|
63
|
+
});
|
|
64
|
+
const stem = path.basename(filePath, ".md");
|
|
65
|
+
const parsed = extractFrontmatterAndBody(content);
|
|
66
|
+
if (!parsed) {
|
|
67
|
+
return {
|
|
68
|
+
name: stem,
|
|
69
|
+
title: stem,
|
|
70
|
+
cadence: null,
|
|
71
|
+
status: "active",
|
|
72
|
+
lastRun: null,
|
|
73
|
+
created: null,
|
|
74
|
+
body: content.trim(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const { frontmatter, body } = parsed;
|
|
78
|
+
const rawTitle = frontmatter.title;
|
|
79
|
+
const title = typeof rawTitle === "string" && rawTitle.length > 0 ? rawTitle : stem;
|
|
80
|
+
const rawCadence = frontmatter.cadence;
|
|
81
|
+
const cadence = typeof rawCadence === "string" && rawCadence.length > 0 ? rawCadence : null;
|
|
82
|
+
const rawStatus = frontmatter.status;
|
|
83
|
+
const status = typeof rawStatus === "string" && isHabitStatus(rawStatus) ? rawStatus : "active";
|
|
84
|
+
const rawLastRun = frontmatter.lastRun;
|
|
85
|
+
const lastRun = typeof rawLastRun === "string" && rawLastRun.length > 0 ? rawLastRun : null;
|
|
86
|
+
const rawCreated = frontmatter.created;
|
|
87
|
+
const created = typeof rawCreated === "string" && rawCreated.length > 0 ? rawCreated : null;
|
|
88
|
+
return {
|
|
89
|
+
name: stem,
|
|
90
|
+
title,
|
|
91
|
+
cadence,
|
|
92
|
+
status,
|
|
93
|
+
lastRun,
|
|
94
|
+
created,
|
|
95
|
+
body,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function formatFrontmatterValue(value) {
|
|
99
|
+
if (value === null || value === undefined)
|
|
100
|
+
return "null";
|
|
101
|
+
return String(value);
|
|
102
|
+
}
|
|
103
|
+
function renderHabitFile(frontmatter, body) {
|
|
104
|
+
(0, runtime_1.emitNervesEvent)({
|
|
105
|
+
event: "daemon.habit_render",
|
|
106
|
+
component: "daemon",
|
|
107
|
+
message: "rendering habit file",
|
|
108
|
+
meta: {},
|
|
109
|
+
});
|
|
110
|
+
const lines = ["---"];
|
|
111
|
+
for (const key of Object.keys(frontmatter)) {
|
|
112
|
+
lines.push(`${key}: ${formatFrontmatterValue(frontmatter[key])}`);
|
|
113
|
+
}
|
|
114
|
+
lines.push("---");
|
|
115
|
+
lines.push("");
|
|
116
|
+
lines.push(body.trim());
|
|
117
|
+
lines.push("");
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|
|
@@ -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;
|