@ouro.bot/cli 0.1.0-alpha.146 → 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.
- package/changelog.json +27 -0
- package/dist/heart/core.js +2 -2
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/daemon-cli.js +104 -6
- package/dist/heart/daemon/daemon-entry.js +36 -15
- package/dist/heart/daemon/daemon.js +7 -0
- 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/specialist-tools.js +1 -1
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/mind/prompt.js +44 -5
- package/dist/repertoire/tasks/parser.js +0 -2
- package/dist/repertoire/tasks/transitions.js +0 -18
- package/dist/repertoire/tools-base.js +1 -1
- package/dist/senses/bluebubbles.js +22 -1
- 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/dist/senses/teams.js +190 -13
- package/package.json +1 -1
- package/dist/heart/daemon/heartbeat-timer.js +0 -132
package/changelog.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
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.148",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Habits extracted from task system as first-class bundle concept at habits/ (peer to diary/, friends/, tasks/).",
|
|
8
|
+
"Name-based identity: heartbeat.md, not YYYY-MM-DD-HHMM-heartbeat.md. Simple schema: title, cadence, status (active/paused), body, lastRun, created.",
|
|
9
|
+
"HabitScheduler with OS cron (LaunchdCronManager/CrontabCronManager): each habit fires independently via its own cron entry.",
|
|
10
|
+
"Event-driven discovery: fs.watch + CLI notify trigger reconcile. No polling.",
|
|
11
|
+
"Worker rerun queue replaces single-slot mechanism, prevents dropped pokes. Each habit turn includes also-due line for agent triage.",
|
|
12
|
+
"Contextual heartbeat includes habit body: agent sees own instructions when heartbeat fires.",
|
|
13
|
+
"Daemon auto-migration from tasks/habits/ on startup. Strips timestamp prefix, maps statuses, removes task-only fields.",
|
|
14
|
+
"ouro habit list and ouro habit create CLI commands. Parse error reporting via nerves events + inner dialog nudge.",
|
|
15
|
+
"Metacognitive framing teaches agent about habits/ directory, rhythms, schema, read_file/write_file, and CLI commands.",
|
|
16
|
+
"HeartbeatTimer deleted, replaced by HabitScheduler. habit type removed from task system entirely."
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"version": "0.1.0-alpha.147",
|
|
21
|
+
"changes": [
|
|
22
|
+
"Feedback signals as social cues: Teams thumbs-up/down and BlueBubbles reactions reach the agent as first-class social signals with full cognitive tools.",
|
|
23
|
+
"AI-generated labels: all outbound Teams messages include AIGeneratedContent entity and feedbackLoopEnabled for native AI badge and feedback UI.",
|
|
24
|
+
"BlueBubbles reaction enrichment: reactions include original message text (truncated to 80 chars) via best-effort BB API lookup.",
|
|
25
|
+
"Streaming refinements: hybrid MIN_INITIAL_CHARS (phrase rotation until 20+ real chars), proactive >4000 finalization with follow-up messages.",
|
|
26
|
+
"Welcome card: Adaptive Card with prompt starters on Teams bot install.",
|
|
27
|
+
"Observe gate: available in 1:1 chats for reaction signals (natural silence case).",
|
|
28
|
+
"Per-context prompt framing: 1:1 vs group framing for feedback signal processing."
|
|
29
|
+
]
|
|
30
|
+
},
|
|
4
31
|
{
|
|
5
32
|
"version": "0.1.0-alpha.146",
|
|
6
33
|
"changes": [
|
package/dist/heart/core.js
CHANGED
|
@@ -539,7 +539,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
539
539
|
...filteredBaseTools,
|
|
540
540
|
tools_1.ponderTool,
|
|
541
541
|
...(isInnerDialog ? [surface_tool_1.surfaceToolDef, tools_1.restTool] : []),
|
|
542
|
-
...(currentContext?.isGroupChat && !isInnerDialog ? [tools_1.observeTool] : []),
|
|
542
|
+
...((currentContext?.isGroupChat || options?.isReactionSignal) && !isInnerDialog ? [tools_1.observeTool] : []),
|
|
543
543
|
...(!isInnerDialog ? [tools_1.settleTool] : []),
|
|
544
544
|
];
|
|
545
545
|
const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
|
|
@@ -725,7 +725,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
725
725
|
(0, runtime_1.emitNervesEvent)({
|
|
726
726
|
component: "engine",
|
|
727
727
|
event: "engine.observe",
|
|
728
|
-
message: "agent
|
|
728
|
+
message: "agent observed without responding",
|
|
729
729
|
meta: { ...(reason ? { reason } : {}) },
|
|
730
730
|
});
|
|
731
731
|
callbacks.onToolEnd("observe", (0, tools_1.summarizeArgs)("observe", observeArgs), true);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_CADENCE_MS = void 0;
|
|
4
|
+
exports.parseCadenceToCron = parseCadenceToCron;
|
|
5
|
+
exports.parseCadenceToMs = parseCadenceToMs;
|
|
6
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
7
|
+
exports.DEFAULT_CADENCE_MS = 30 * 60 * 1000; // 30 minutes
|
|
8
|
+
/**
|
|
9
|
+
* Parse a cadence shorthand (e.g. "30m", "2h", "1d") into a cron string.
|
|
10
|
+
* Also accepts raw cron strings (5 space-separated fields) and passes them through.
|
|
11
|
+
* Returns null for invalid input.
|
|
12
|
+
*/
|
|
13
|
+
function parseCadenceToCron(raw) {
|
|
14
|
+
if (typeof raw !== "string")
|
|
15
|
+
return null;
|
|
16
|
+
const value = raw.trim();
|
|
17
|
+
if (!value)
|
|
18
|
+
return null;
|
|
19
|
+
// Cron format (minute hour day month weekday)
|
|
20
|
+
if (/^\S+\s+\S+\s+\S+\s+\S+\s+\S+$/.test(value)) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
const cadenceMatch = /^(\d+)(m|h|d)$/.exec(value);
|
|
24
|
+
if (!cadenceMatch)
|
|
25
|
+
return null;
|
|
26
|
+
const interval = Number.parseInt(cadenceMatch[1], 10);
|
|
27
|
+
if (!Number.isFinite(interval) || interval <= 0)
|
|
28
|
+
return null;
|
|
29
|
+
(0, runtime_1.emitNervesEvent)({
|
|
30
|
+
event: "daemon.cadence_parsed",
|
|
31
|
+
component: "daemon",
|
|
32
|
+
message: "parsed cadence shorthand to cron",
|
|
33
|
+
meta: { raw: value, interval, unit: cadenceMatch[2] },
|
|
34
|
+
});
|
|
35
|
+
const unit = cadenceMatch[2];
|
|
36
|
+
if (unit === "m")
|
|
37
|
+
return `*/${interval} * * * *`;
|
|
38
|
+
if (unit === "h")
|
|
39
|
+
return `0 */${interval} * * *`;
|
|
40
|
+
return `0 0 */${interval} * *`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse a cadence shorthand (e.g. "30m", "2h", "1d") into milliseconds.
|
|
44
|
+
* Returns null for invalid input.
|
|
45
|
+
*/
|
|
46
|
+
function parseCadenceToMs(raw) {
|
|
47
|
+
if (typeof raw !== "string")
|
|
48
|
+
return null;
|
|
49
|
+
const value = raw.trim();
|
|
50
|
+
if (!value)
|
|
51
|
+
return null;
|
|
52
|
+
const match = /^(\d+)(m|h|d)$/.exec(value);
|
|
53
|
+
if (!match)
|
|
54
|
+
return null;
|
|
55
|
+
const interval = Number.parseInt(match[1], 10);
|
|
56
|
+
if (!Number.isFinite(interval) || interval <= 0)
|
|
57
|
+
return null;
|
|
58
|
+
(0, runtime_1.emitNervesEvent)({
|
|
59
|
+
event: "daemon.cadence_parsed_ms",
|
|
60
|
+
component: "daemon",
|
|
61
|
+
message: "parsed cadence shorthand to milliseconds",
|
|
62
|
+
meta: { raw: value, interval, unit: match[2] },
|
|
63
|
+
});
|
|
64
|
+
const unit = match[2];
|
|
65
|
+
if (unit === "m")
|
|
66
|
+
return interval * 60 * 1000;
|
|
67
|
+
if (unit === "h")
|
|
68
|
+
return interval * 60 * 60 * 1000;
|
|
69
|
+
return interval * 24 * 60 * 60 * 1000;
|
|
70
|
+
}
|
|
@@ -283,6 +283,9 @@ function usage() {
|
|
|
283
283
|
" ouro chat <agent>",
|
|
284
284
|
" ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
|
|
285
285
|
" ouro poke <agent> --task <task-id>",
|
|
286
|
+
" ouro poke <agent> --habit <name>",
|
|
287
|
+
" ouro habit list [--agent <name>]",
|
|
288
|
+
" ouro habit create [--agent <name>] <name> [--cadence <interval>]",
|
|
286
289
|
" ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
|
|
287
290
|
" ouro task board [<status>] [--agent <name>]",
|
|
288
291
|
" ouro task create <title> [--type <type>] [--agent <name>]",
|
|
@@ -392,16 +395,55 @@ function parsePokeCommand(args) {
|
|
|
392
395
|
if (!agent)
|
|
393
396
|
throw new Error(`Usage\n${usage()}`);
|
|
394
397
|
let taskId;
|
|
398
|
+
let habitName;
|
|
395
399
|
for (let i = 1; i < args.length; i += 1) {
|
|
396
400
|
if (args[i] === "--task") {
|
|
397
401
|
taskId = args[i + 1];
|
|
398
402
|
i += 1;
|
|
399
403
|
}
|
|
404
|
+
if (args[i] === "--habit") {
|
|
405
|
+
habitName = args[i + 1];
|
|
406
|
+
i += 1;
|
|
407
|
+
}
|
|
400
408
|
}
|
|
409
|
+
// --habit takes priority over --task
|
|
410
|
+
if (habitName)
|
|
411
|
+
return { kind: "habit.poke", agent, habitName };
|
|
401
412
|
if (!taskId)
|
|
402
413
|
throw new Error(`Usage\n${usage()}`);
|
|
403
414
|
return { kind: "task.poke", agent, taskId };
|
|
404
415
|
}
|
|
416
|
+
function parseHabitCommand(args) {
|
|
417
|
+
const { agent, rest } = extractAgentFlag(args);
|
|
418
|
+
const sub = rest[0];
|
|
419
|
+
if (sub === "list") {
|
|
420
|
+
return { kind: "habit.list", ...(agent ? { agent } : {}) };
|
|
421
|
+
}
|
|
422
|
+
if (sub === "create") {
|
|
423
|
+
const nameArgs = rest.slice(1);
|
|
424
|
+
let name;
|
|
425
|
+
let cadence;
|
|
426
|
+
const positional = [];
|
|
427
|
+
for (let i = 0; i < nameArgs.length; i++) {
|
|
428
|
+
if (nameArgs[i] === "--cadence" && nameArgs[i + 1]) {
|
|
429
|
+
cadence = nameArgs[++i];
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
/* v8 ignore start -- defensive: --agent already extracted by extractAgentFlag; guard prevents regression if parsing flow changes @preserve */
|
|
433
|
+
if (nameArgs[i] === "--agent" && nameArgs[i + 1]) {
|
|
434
|
+
i++; // skip --agent value (already extracted)
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
/* v8 ignore stop */
|
|
438
|
+
positional.push(nameArgs[i]);
|
|
439
|
+
}
|
|
440
|
+
name = positional[0];
|
|
441
|
+
if (!name)
|
|
442
|
+
throw new Error(`Usage\n${usage()}`);
|
|
443
|
+
return { kind: "habit.create", name, ...(agent ? { agent } : {}), ...(cadence ? { cadence } : {}) };
|
|
444
|
+
}
|
|
445
|
+
throw new Error(`Usage\n${usage()}`);
|
|
446
|
+
}
|
|
405
447
|
function parseLinkCommand(args, kind = "friend.link") {
|
|
406
448
|
const agent = args[0];
|
|
407
449
|
if (!agent)
|
|
@@ -1008,6 +1050,8 @@ function parseOuroCommand(args) {
|
|
|
1008
1050
|
return parseTaskCommand(args.slice(1));
|
|
1009
1051
|
if (head === "reminder")
|
|
1010
1052
|
return parseReminderCommand(args.slice(1));
|
|
1053
|
+
if (head === "habit")
|
|
1054
|
+
return parseHabitCommand(args.slice(1));
|
|
1011
1055
|
if (head === "friend")
|
|
1012
1056
|
return parseFriendCommand(args.slice(1));
|
|
1013
1057
|
if (head === "config")
|
|
@@ -1825,7 +1869,7 @@ function executeReminderCommand(command, taskMod) {
|
|
|
1825
1869
|
try {
|
|
1826
1870
|
const created = taskMod.createTask({
|
|
1827
1871
|
title: command.title,
|
|
1828
|
-
type: command.cadence ? "
|
|
1872
|
+
type: command.cadence ? "ongoing" : "one-shot",
|
|
1829
1873
|
category: command.category ?? "reminder",
|
|
1830
1874
|
body: command.body,
|
|
1831
1875
|
scheduledAt: command.scheduledAt,
|
|
@@ -2509,6 +2553,61 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
2509
2553
|
deps.writeStdout(message);
|
|
2510
2554
|
return message;
|
|
2511
2555
|
}
|
|
2556
|
+
// ── habit subcommands (local, no daemon socket needed) ──
|
|
2557
|
+
if (command.kind === "habit.list" || command.kind === "habit.create") {
|
|
2558
|
+
const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("./habit-parser")));
|
|
2559
|
+
/* v8 ignore start -- production default: uses real bundle root @preserve */
|
|
2560
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
2561
|
+
const bundleRoot = deps.agentBundleRoot ?? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
|
|
2562
|
+
/* v8 ignore stop */
|
|
2563
|
+
const habitsDir = path.join(bundleRoot, "habits");
|
|
2564
|
+
if (command.kind === "habit.list") {
|
|
2565
|
+
let files;
|
|
2566
|
+
try {
|
|
2567
|
+
files = fs.readdirSync(habitsDir).filter((f) => f.endsWith(".md") && f !== "README.md");
|
|
2568
|
+
}
|
|
2569
|
+
catch {
|
|
2570
|
+
const message = "no habits found";
|
|
2571
|
+
deps.writeStdout(message);
|
|
2572
|
+
return message;
|
|
2573
|
+
}
|
|
2574
|
+
if (files.length === 0) {
|
|
2575
|
+
const message = "no habits found";
|
|
2576
|
+
deps.writeStdout(message);
|
|
2577
|
+
return message;
|
|
2578
|
+
}
|
|
2579
|
+
const lines = [];
|
|
2580
|
+
for (const file of files) {
|
|
2581
|
+
const content = fs.readFileSync(path.join(habitsDir, file), "utf-8");
|
|
2582
|
+
const habit = parseHabitFile(content, path.join(habitsDir, file));
|
|
2583
|
+
const lastRunStr = habit.lastRun ?? "never";
|
|
2584
|
+
lines.push(`${habit.name} cadence=${habit.cadence ?? "none"} status=${habit.status} lastRun=${lastRunStr}`);
|
|
2585
|
+
}
|
|
2586
|
+
const message = lines.join("\n");
|
|
2587
|
+
deps.writeStdout(message);
|
|
2588
|
+
return message;
|
|
2589
|
+
}
|
|
2590
|
+
// habit.create
|
|
2591
|
+
const filePath = path.join(habitsDir, `${command.name}.md`);
|
|
2592
|
+
if (fs.existsSync(filePath)) {
|
|
2593
|
+
const message = `error: habit '${command.name}' already exists`;
|
|
2594
|
+
deps.writeStdout(message);
|
|
2595
|
+
return message;
|
|
2596
|
+
}
|
|
2597
|
+
fs.mkdirSync(habitsDir, { recursive: true });
|
|
2598
|
+
const now = new Date().toISOString();
|
|
2599
|
+
const content = renderHabitFile({
|
|
2600
|
+
title: command.name,
|
|
2601
|
+
cadence: command.cadence ?? "null",
|
|
2602
|
+
status: "active",
|
|
2603
|
+
lastRun: now,
|
|
2604
|
+
created: now,
|
|
2605
|
+
}, `Habit: ${command.name}`);
|
|
2606
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
2607
|
+
const message = `created: ${filePath}`;
|
|
2608
|
+
deps.writeStdout(message);
|
|
2609
|
+
return message;
|
|
2610
|
+
}
|
|
2512
2611
|
// ── friend subcommands (local, no daemon socket needed) ──
|
|
2513
2612
|
if (command.kind === "friend.list" || command.kind === "friend.show" || command.kind === "friend.create" ||
|
|
2514
2613
|
command.kind === "friend.update" || command.kind === "friend.link" || command.kind === "friend.unlink") {
|
|
@@ -2860,7 +2959,7 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
2860
2959
|
const agentRoot = (0, identity_1.getAgentRoot)(agentName);
|
|
2861
2960
|
const { buildInnerStatusOutput } = await Promise.resolve().then(() => __importStar(require("./inner-status")));
|
|
2862
2961
|
const { sessionPath: getSessionPath } = await Promise.resolve().then(() => __importStar(require("../config")));
|
|
2863
|
-
const { parseCadenceMs, DEFAULT_CADENCE_MS } = await Promise.resolve().then(() => __importStar(require("./
|
|
2962
|
+
const { parseCadenceToMs: parseCadenceMs, DEFAULT_CADENCE_MS } = await Promise.resolve().then(() => __importStar(require("./cadence")));
|
|
2864
2963
|
const { parseFrontmatter } = await Promise.resolve().then(() => __importStar(require("../../repertoire/tasks/parser")));
|
|
2865
2964
|
const { listActiveObligations } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
|
|
2866
2965
|
// Read runtime state
|
|
@@ -2889,11 +2988,10 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
2889
2988
|
let heartbeat = null;
|
|
2890
2989
|
try {
|
|
2891
2990
|
const habitsDir = path.join(agentRoot, "habits");
|
|
2892
|
-
const
|
|
2893
|
-
const heartbeatFile = habitFiles.find((f) => f.endsWith("-heartbeat.md"));
|
|
2991
|
+
const heartbeatPath = path.join(habitsDir, "heartbeat.md");
|
|
2894
2992
|
let cadenceMs = DEFAULT_CADENCE_MS;
|
|
2895
|
-
if (
|
|
2896
|
-
const content = fs.readFileSync(
|
|
2993
|
+
if (fs.existsSync(heartbeatPath)) {
|
|
2994
|
+
const content = fs.readFileSync(heartbeatPath, "utf-8");
|
|
2897
2995
|
const lines = content.split(/\r?\n/);
|
|
2898
2996
|
if (lines[0]?.trim() === "---") {
|
|
2899
2997
|
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
@@ -47,7 +47,10 @@ const sense_manager_1 = require("./sense-manager");
|
|
|
47
47
|
const agent_discovery_1 = require("./agent-discovery");
|
|
48
48
|
const identity_1 = require("../identity");
|
|
49
49
|
const runtime_mode_1 = require("./runtime-mode");
|
|
50
|
-
const
|
|
50
|
+
const habit_scheduler_1 = require("./habit-scheduler");
|
|
51
|
+
const habit_migration_1 = require("./habit-migration");
|
|
52
|
+
const os_cron_deps_1 = require("./os-cron-deps");
|
|
53
|
+
const os_cron_1 = require("./os-cron");
|
|
51
54
|
function parseSocketPath(argv) {
|
|
52
55
|
const socketIndex = argv.indexOf("--socket");
|
|
53
56
|
if (socketIndex >= 0) {
|
|
@@ -116,24 +119,38 @@ const daemon = new daemon_1.OuroDaemon({
|
|
|
116
119
|
router,
|
|
117
120
|
mode,
|
|
118
121
|
});
|
|
119
|
-
const
|
|
120
|
-
/* v8 ignore start --
|
|
122
|
+
const habitSchedulers = [];
|
|
123
|
+
/* v8 ignore start -- habit wiring: lambdas delegate to processManager/fs; tested via HabitScheduler unit tests @preserve */
|
|
121
124
|
void daemon.start().then(() => {
|
|
122
125
|
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
126
|
+
const ouroPath = (0, os_cron_deps_1.resolveOuroBinaryPath)();
|
|
127
|
+
const osCronDeps = (0, os_cron_deps_1.createRealOsCronDeps)();
|
|
123
128
|
for (const agent of managedAgents) {
|
|
124
129
|
const bundleRoot = path.join(bundlesRoot, `${agent}.ouro`);
|
|
125
|
-
const
|
|
130
|
+
const habitsDir = path.join(bundleRoot, "habits");
|
|
131
|
+
// Migrate old tasks/habits/ to habits/ at bundle root
|
|
132
|
+
(0, habit_migration_1.migrateHabitsFromTaskSystem)(bundleRoot);
|
|
133
|
+
const osCronManager = new os_cron_1.LaunchdCronManager(osCronDeps);
|
|
134
|
+
const scheduler = new habit_scheduler_1.HabitScheduler({
|
|
126
135
|
agent,
|
|
127
|
-
|
|
136
|
+
habitsDir,
|
|
137
|
+
osCronManager,
|
|
138
|
+
onHabitFire: (habitName) => {
|
|
139
|
+
processManager.sendToAgent(agent, { type: "habit", habitName });
|
|
140
|
+
},
|
|
128
141
|
deps: {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
readdir: (dir) => fs.readdirSync(dir),
|
|
143
|
+
readFile: (p, enc) => fs.readFileSync(p, enc),
|
|
144
|
+
writeFile: (p, c, enc) => fs.writeFileSync(p, c, enc),
|
|
145
|
+
existsSync: (p) => fs.existsSync(p),
|
|
146
|
+
now: () => Date.now(),
|
|
147
|
+
ouroPath,
|
|
148
|
+
watch: (dir, cb) => fs.watch(dir, cb),
|
|
133
149
|
},
|
|
134
150
|
});
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
scheduler.start();
|
|
152
|
+
scheduler.watchForChanges();
|
|
153
|
+
habitSchedulers.push(scheduler);
|
|
137
154
|
}
|
|
138
155
|
}).catch(async () => {
|
|
139
156
|
/* v8 ignore stop */
|
|
@@ -148,12 +165,16 @@ void daemon.start().then(() => {
|
|
|
148
165
|
process.exit(1);
|
|
149
166
|
});
|
|
150
167
|
process.on("SIGINT", () => {
|
|
151
|
-
for (const
|
|
152
|
-
|
|
168
|
+
for (const s of habitSchedulers) {
|
|
169
|
+
s.stopWatching();
|
|
170
|
+
s.stop();
|
|
171
|
+
}
|
|
153
172
|
void daemon.stop().then(() => process.exit(0));
|
|
154
173
|
});
|
|
155
174
|
process.on("SIGTERM", () => {
|
|
156
|
-
for (const
|
|
157
|
-
|
|
175
|
+
for (const s of habitSchedulers) {
|
|
176
|
+
s.stopWatching();
|
|
177
|
+
s.stop();
|
|
178
|
+
}
|
|
158
179
|
void daemon.stop().then(() => process.exit(0));
|
|
159
180
|
});
|
|
@@ -606,6 +606,13 @@ class OuroDaemon {
|
|
|
606
606
|
data: receipt,
|
|
607
607
|
};
|
|
608
608
|
}
|
|
609
|
+
case "habit.poke": {
|
|
610
|
+
this.processManager.sendToAgent?.(command.agent, { type: "habit", habitName: command.habitName });
|
|
611
|
+
return {
|
|
612
|
+
ok: true,
|
|
613
|
+
message: `poked habit ${command.habitName} for ${command.agent}`,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
609
616
|
case "mcp.list": {
|
|
610
617
|
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)();
|
|
611
618
|
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
|
+
}
|