@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
|
@@ -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
|
|
85
|
-
|
|
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
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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 */
|
|
@@ -51,12 +51,16 @@ class DaemonProcessManager {
|
|
|
51
51
|
now;
|
|
52
52
|
setTimeoutFn;
|
|
53
53
|
clearTimeoutFn;
|
|
54
|
+
cooldownRecoveryMs;
|
|
55
|
+
maxCooldownRetries;
|
|
54
56
|
existsSyncFn;
|
|
55
57
|
constructor(options) {
|
|
56
58
|
this.maxRestartsPerHour = options.maxRestartsPerHour ?? 10;
|
|
57
59
|
this.stabilityThresholdMs = options.stabilityThresholdMs ?? 60_000;
|
|
58
60
|
this.initialBackoffMs = options.initialBackoffMs ?? 1_000;
|
|
59
61
|
this.maxBackoffMs = options.maxBackoffMs ?? 60_000;
|
|
62
|
+
this.cooldownRecoveryMs = options.cooldownRecoveryMs ?? 5 * 60 * 1_000;
|
|
63
|
+
this.maxCooldownRetries = options.maxCooldownRetries ?? 3;
|
|
60
64
|
this.spawnFn = options.spawn ?? ((command, args, spawnOptions) => (0, child_process_1.spawn)(command, args, spawnOptions));
|
|
61
65
|
this.now = options.now ?? (() => Date.now());
|
|
62
66
|
this.setTimeoutFn = options.setTimeoutFn ?? ((cb, delay) => setTimeout(cb, delay));
|
|
@@ -69,6 +73,8 @@ class DaemonProcessManager {
|
|
|
69
73
|
restartTimer: null,
|
|
70
74
|
crashTimestamps: [],
|
|
71
75
|
stopRequested: false,
|
|
76
|
+
cooldownTimer: null,
|
|
77
|
+
cooldownRetryCount: 0,
|
|
72
78
|
snapshot: {
|
|
73
79
|
name: agent.name,
|
|
74
80
|
channel: agent.channel,
|
|
@@ -78,6 +84,8 @@ class DaemonProcessManager {
|
|
|
78
84
|
startedAt: null,
|
|
79
85
|
lastCrashAt: null,
|
|
80
86
|
backoffMs: this.initialBackoffMs,
|
|
87
|
+
lastExitCode: null,
|
|
88
|
+
lastSignal: null,
|
|
81
89
|
},
|
|
82
90
|
});
|
|
83
91
|
}
|
|
@@ -143,6 +151,7 @@ class DaemonProcessManager {
|
|
|
143
151
|
async stopAgent(agent) {
|
|
144
152
|
const state = this.requireAgent(agent);
|
|
145
153
|
this.clearRestartTimer(state);
|
|
154
|
+
this.clearCooldownTimer(state);
|
|
146
155
|
state.stopRequested = true;
|
|
147
156
|
if (!state.process) {
|
|
148
157
|
state.snapshot.status = "stopped";
|
|
@@ -203,6 +212,8 @@ class DaemonProcessManager {
|
|
|
203
212
|
return;
|
|
204
213
|
state.process = null;
|
|
205
214
|
state.snapshot.pid = null;
|
|
215
|
+
state.snapshot.lastExitCode = code;
|
|
216
|
+
state.snapshot.lastSignal = signal;
|
|
206
217
|
const crashed = !state.stopRequested && code !== 0;
|
|
207
218
|
const now = this.now();
|
|
208
219
|
const startedAt = state.snapshot.startedAt ? Date.parse(state.snapshot.startedAt) : now;
|
|
@@ -233,6 +244,7 @@ class DaemonProcessManager {
|
|
|
233
244
|
message: "managed agent exceeded restart limit and is marked crashed",
|
|
234
245
|
meta: { agent: state.config.name, maxRestartsPerHour: this.maxRestartsPerHour },
|
|
235
246
|
});
|
|
247
|
+
this.scheduleCooldownRecovery(state);
|
|
236
248
|
return;
|
|
237
249
|
}
|
|
238
250
|
state.snapshot.status = "starting";
|
|
@@ -250,6 +262,45 @@ class DaemonProcessManager {
|
|
|
250
262
|
this.clearTimeoutFn(state.restartTimer);
|
|
251
263
|
state.restartTimer = null;
|
|
252
264
|
}
|
|
265
|
+
scheduleCooldownRecovery(state) {
|
|
266
|
+
if (state.cooldownRetryCount >= this.maxCooldownRetries) {
|
|
267
|
+
(0, runtime_1.emitNervesEvent)({
|
|
268
|
+
level: "error",
|
|
269
|
+
component: "daemon",
|
|
270
|
+
event: "daemon.agent_permanent_failure",
|
|
271
|
+
message: "managed agent exhausted all cooldown retries — permanently stopped",
|
|
272
|
+
meta: { agent: state.config.name, cooldownRetryCount: state.cooldownRetryCount, maxCooldownRetries: this.maxCooldownRetries },
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
this.clearCooldownTimer(state);
|
|
277
|
+
state.cooldownTimer = this.setTimeoutFn(() => {
|
|
278
|
+
state.cooldownRetryCount += 1;
|
|
279
|
+
state.crashTimestamps = [];
|
|
280
|
+
state.snapshot.backoffMs = this.initialBackoffMs;
|
|
281
|
+
state.snapshot.status = "starting";
|
|
282
|
+
state.snapshot.restartCount += 1;
|
|
283
|
+
(0, runtime_1.emitNervesEvent)({
|
|
284
|
+
component: "daemon",
|
|
285
|
+
event: "daemon.agent_cooldown_recovery",
|
|
286
|
+
message: "attempting cooldown recovery for managed agent",
|
|
287
|
+
meta: { agent: state.config.name, cooldownRetryCount: state.cooldownRetryCount },
|
|
288
|
+
});
|
|
289
|
+
void this.startAgent(state.config.name);
|
|
290
|
+
}, this.cooldownRecoveryMs);
|
|
291
|
+
(0, runtime_1.emitNervesEvent)({
|
|
292
|
+
component: "daemon",
|
|
293
|
+
event: "daemon.agent_cooldown_scheduled",
|
|
294
|
+
message: `scheduled cooldown recovery in ${this.cooldownRecoveryMs}ms`,
|
|
295
|
+
meta: { agent: state.config.name, cooldownRecoveryMs: this.cooldownRecoveryMs, cooldownRetryCount: state.cooldownRetryCount },
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
clearCooldownTimer(state) {
|
|
299
|
+
if (state.cooldownTimer === null)
|
|
300
|
+
return;
|
|
301
|
+
this.clearTimeoutFn(state.cooldownTimer);
|
|
302
|
+
state.cooldownTimer = null;
|
|
303
|
+
}
|
|
253
304
|
requireAgent(agent) {
|
|
254
305
|
const state = this.agents.get(agent);
|
|
255
306
|
if (!state) {
|
|
@@ -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"
|
|
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 =
|
|
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, {
|
package/dist/mind/prompt.js
CHANGED
|
@@ -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
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
package/dist/nerves/index.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.formatTerminalEntry = formatTerminalEntry;
|
|
|
7
7
|
exports.registerSpinnerHooks = registerSpinnerHooks;
|
|
8
8
|
exports.createTerminalSink = createTerminalSink;
|
|
9
9
|
exports.createStderrSink = createStderrSink;
|
|
10
|
+
exports.rotateIfNeeded = rotateIfNeeded;
|
|
10
11
|
exports.createNdjsonFileSink = createNdjsonFileSink;
|
|
11
12
|
exports.registerGlobalLogSink = registerGlobalLogSink;
|
|
12
13
|
exports.createLogger = createLogger;
|
|
@@ -99,22 +100,66 @@ function createTerminalSink(write = (chunk) => process.stderr.write(chunk), colo
|
|
|
99
100
|
function createStderrSink(write = (chunk) => process.stderr.write(chunk)) {
|
|
100
101
|
return createTerminalSink(write);
|
|
101
102
|
}
|
|
102
|
-
|
|
103
|
+
const DEFAULT_MAX_LOG_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
|
|
104
|
+
const ROTATION_CHECK_INTERVAL_BYTES = 1024 * 1024; // ~1MB between stat checks
|
|
105
|
+
/**
|
|
106
|
+
* Rotate a log file: shift .1 -> .2, delete old .2, rename current -> .1.
|
|
107
|
+
* Uses the ndjson extension pattern: file.ndjson -> file.1.ndjson -> file.2.ndjson
|
|
108
|
+
*/
|
|
109
|
+
function rotateIfNeeded(filePath, maxSizeBytes) {
|
|
110
|
+
let size;
|
|
111
|
+
try {
|
|
112
|
+
size = (0, fs_1.statSync)(filePath).size;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (size < maxSizeBytes)
|
|
118
|
+
return false;
|
|
119
|
+
const ext = filePath.endsWith(".ndjson") ? ".ndjson" : "";
|
|
120
|
+
const base = ext ? filePath.slice(0, -ext.length) : filePath;
|
|
121
|
+
const file1 = `${base}.1${ext}`;
|
|
122
|
+
const file2 = `${base}.2${ext}`;
|
|
123
|
+
try {
|
|
124
|
+
(0, fs_1.unlinkSync)(file2);
|
|
125
|
+
}
|
|
126
|
+
catch { /* may not exist */ }
|
|
127
|
+
try {
|
|
128
|
+
(0, fs_1.renameSync)(file1, file2);
|
|
129
|
+
}
|
|
130
|
+
catch { /* may not exist */ }
|
|
131
|
+
try {
|
|
132
|
+
(0, fs_1.renameSync)(filePath, file1);
|
|
133
|
+
}
|
|
134
|
+
catch { /* best effort */ }
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
function createNdjsonFileSink(filePath, maxSizeBytes) {
|
|
103
138
|
(0, fs_1.mkdirSync)((0, path_1.dirname)(filePath), { recursive: true });
|
|
104
139
|
const queue = [];
|
|
105
140
|
let flushing = false;
|
|
141
|
+
let bytesSinceCheck = 0;
|
|
142
|
+
const maxSize = maxSizeBytes ?? DEFAULT_MAX_LOG_SIZE_BYTES;
|
|
106
143
|
function flush() {
|
|
107
144
|
if (flushing || queue.length === 0)
|
|
108
145
|
return;
|
|
109
146
|
flushing = true;
|
|
110
147
|
const line = queue.shift();
|
|
148
|
+
/* v8 ignore start — rotation triggers only after ~1MB of writes @preserve */
|
|
149
|
+
if (bytesSinceCheck >= ROTATION_CHECK_INTERVAL_BYTES) {
|
|
150
|
+
bytesSinceCheck = 0;
|
|
151
|
+
rotateIfNeeded(filePath, maxSize);
|
|
152
|
+
}
|
|
153
|
+
/* v8 ignore stop */
|
|
111
154
|
(0, fs_1.appendFile)(filePath, line, "utf8", () => {
|
|
112
155
|
flushing = false;
|
|
113
156
|
flush();
|
|
114
157
|
});
|
|
115
158
|
}
|
|
116
159
|
return (entry) => {
|
|
117
|
-
|
|
160
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
161
|
+
bytesSinceCheck += line.length;
|
|
162
|
+
queue.push(line);
|
|
118
163
|
flush();
|
|
119
164
|
};
|
|
120
165
|
}
|
|
@@ -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;
|