@ouro.bot/cli 0.1.0-alpha.156 → 0.1.0-alpha.158
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 +25 -1
- package/dist/heart/daemon/daemon-cli.js +81 -1
- package/dist/heart/daemon/habit-scheduler.js +18 -15
- package/dist/mind/prompt.js +36 -0
- package/dist/repertoire/guardrails.js +1 -0
- package/dist/repertoire/tasks/board.js +31 -5
- package/dist/repertoire/tasks/fix.js +182 -0
- package/dist/repertoire/tasks/index.js +5 -1
- package/dist/repertoire/tasks/lifecycle.js +2 -2
- package/dist/repertoire/tasks/parser.js +3 -0
- package/dist/repertoire/tasks/scanner.js +194 -37
- package/dist/repertoire/tasks/transitions.js +10 -8
- package/dist/repertoire/tools-base.js +2 -0
- package/dist/senses/habit-turn-message.js +108 -0
- package/dist/senses/inner-dialog.js +37 -78
- package/dist/senses/pipeline.js +2 -0
- package/package.json +1 -1
- package/dist/senses/contextual-heartbeat.js +0 -106
package/changelog.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
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.158",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Task scanner v2: explicit identity via kind: task field. Scanner only parses files that declare themselves as task cards — doing docs, planning docs, and artifacts silently skipped. Eliminates 184 false parse errors on real bundles.",
|
|
8
|
+
"Typed issue model: every scanner issue has a code, description, proposed fix, confidence (safe/needs_review), and category (live/migration). Replaces flat parseErrors/invalidFilenames arrays.",
|
|
9
|
+
"Board health line: compact board shows health: clean or health: 1 live, 10 migration. Live vs migration split prevents cleanup noise from looking like breakage.",
|
|
10
|
+
"Fix command: ouro task fix (dry-run), ouro task fix --safe (apply deterministic fixes), ouro task fix <id> (inspect/apply individual issues). Currently auto-fixes schema-missing-kind.",
|
|
11
|
+
"Cancelled status: new terminal state reachable from any active status. Auto-archives with work directory, hidden from active board view.",
|
|
12
|
+
"Derived child_tasks: computed at scan time from parent_task links. child_tasks removed from authored schema — no more hand-maintained stale arrays.",
|
|
13
|
+
"Work directory awareness: same-stem directories detected and listed on TaskFile (hasWorkDir, workDirFiles). Scanner never descends into them.",
|
|
14
|
+
"Collection root clutter detection: non-task support docs at collection root summarized as one aggregated migration issue per collection.",
|
|
15
|
+
"Root-only scanning: flat directory reads replace recursive walks. Faster and correct."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"version": "0.1.0-alpha.157",
|
|
20
|
+
"changes": [
|
|
21
|
+
"Habit turns as awareness: unified buildHabitTurnMessage replaces contextual-heartbeat. Continuity-first format (checkpoint leads, not elapsed time). Same format for all habits \u2014 no heartbeat special-casing.",
|
|
22
|
+
"First beat experience: new habits get \"your [Title] is alive. this is its first breath\" on first fire.",
|
|
23
|
+
"Fix: reconcile() now fires new/overdue habits immediately (was start()-only). New habits created via write_file fire within seconds.",
|
|
24
|
+
"Rhythm awareness across all channels: rhythmStatusSection() in system prompt shows heartbeat health in every conversation.",
|
|
25
|
+
"Removed contextual-heartbeat.ts entirely. Dead lastSurfaceAt parameter removed."
|
|
26
|
+
]
|
|
27
|
+
},
|
|
4
28
|
{
|
|
5
29
|
"version": "0.1.0-alpha.156",
|
|
6
30
|
"changes": [
|
|
@@ -1033,4 +1057,4 @@
|
|
|
1033
1057
|
]
|
|
1034
1058
|
}
|
|
1035
1059
|
]
|
|
1036
|
-
}
|
|
1060
|
+
}
|
|
@@ -305,6 +305,7 @@ function usage() {
|
|
|
305
305
|
" ouro task create <title> [--type <type>] [--agent <name>]",
|
|
306
306
|
" ouro task update <id> <status> [--agent <name>]",
|
|
307
307
|
" ouro task show <id> [--agent <name>]",
|
|
308
|
+
" ouro task fix [--safe|--all] [<id> [--option <N>]] [--agent <name>]",
|
|
308
309
|
" ouro task actionable|deps|sessions [--agent <name>]",
|
|
309
310
|
" ouro reminder create <title> --body <body> [--at <iso>] [--cadence <interval>] [--category <category>] [--agent <name>]",
|
|
310
311
|
" ouro friend list [--agent <name>]",
|
|
@@ -737,6 +738,31 @@ function parseTaskCommand(args) {
|
|
|
737
738
|
return { kind: "task.deps", ...(agent ? { agent } : {}) };
|
|
738
739
|
if (sub === "sessions")
|
|
739
740
|
return { kind: "task.sessions", ...(agent ? { agent } : {}) };
|
|
741
|
+
if (sub === "fix") {
|
|
742
|
+
// fix --safe | fix --all | fix <id> [--option N] | fix (dry-run)
|
|
743
|
+
if (rest.length === 0)
|
|
744
|
+
return { kind: "task.fix", mode: "dry-run", ...(agent ? { agent } : {}) };
|
|
745
|
+
const first = rest[0];
|
|
746
|
+
if (first === "--safe" || first === "--all") {
|
|
747
|
+
return { kind: "task.fix", mode: "safe", ...(agent ? { agent } : {}) };
|
|
748
|
+
}
|
|
749
|
+
// first arg is an issue ID (contains a colon, e.g. schema-missing-kind:one-shots/foo.md)
|
|
750
|
+
const issueId = first;
|
|
751
|
+
let option;
|
|
752
|
+
for (let i = 1; i < rest.length; i++) {
|
|
753
|
+
if (rest[i] === "--option" && rest[i + 1]) {
|
|
754
|
+
option = parseInt(rest[i + 1], 10);
|
|
755
|
+
i += 1;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
kind: "task.fix",
|
|
760
|
+
mode: "single",
|
|
761
|
+
issueId,
|
|
762
|
+
...(option !== undefined ? { option } : {}),
|
|
763
|
+
...(agent ? { agent } : {}),
|
|
764
|
+
};
|
|
765
|
+
}
|
|
740
766
|
throw new Error(`Usage\n${usage()}`);
|
|
741
767
|
}
|
|
742
768
|
function parseAuthCommand(args) {
|
|
@@ -1796,6 +1822,60 @@ function executeTaskCommand(command, taskMod) {
|
|
|
1796
1822
|
const lines = taskMod.boardDeps();
|
|
1797
1823
|
return lines.length > 0 ? lines.join("\n") : "no unresolved dependencies";
|
|
1798
1824
|
}
|
|
1825
|
+
if (command.kind === "task.fix") {
|
|
1826
|
+
try {
|
|
1827
|
+
const fixOptions = {
|
|
1828
|
+
mode: command.mode,
|
|
1829
|
+
...(command.issueId ? { issueId: command.issueId } : {}),
|
|
1830
|
+
...(command.option !== undefined ? { option: command.option } : {}),
|
|
1831
|
+
};
|
|
1832
|
+
const result = taskMod.fix(fixOptions);
|
|
1833
|
+
if (command.mode === "dry-run") {
|
|
1834
|
+
if (result.remaining.length === 0) {
|
|
1835
|
+
return `task health: clean`;
|
|
1836
|
+
}
|
|
1837
|
+
const safeIssues = result.remaining.filter((i) => i.confidence === "safe");
|
|
1838
|
+
const reviewIssues = result.remaining.filter((i) => i.confidence === "needs_review");
|
|
1839
|
+
const lines = [`${result.remaining.length} issues found`];
|
|
1840
|
+
if (safeIssues.length > 0) {
|
|
1841
|
+
lines.push("", `safe fixes (${safeIssues.length}):`);
|
|
1842
|
+
for (const issue of safeIssues) {
|
|
1843
|
+
lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (reviewIssues.length > 0) {
|
|
1847
|
+
lines.push("", `needs review (${reviewIssues.length}):`);
|
|
1848
|
+
for (const issue of reviewIssues) {
|
|
1849
|
+
lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
lines.push("", `task health: ${result.health}`);
|
|
1853
|
+
return lines.join("\n");
|
|
1854
|
+
}
|
|
1855
|
+
// safe, single, or --all modes: show what was done
|
|
1856
|
+
const lines = [];
|
|
1857
|
+
if (result.applied.length > 0) {
|
|
1858
|
+
lines.push(`${result.applied.length} applied:`);
|
|
1859
|
+
for (const issue of result.applied) {
|
|
1860
|
+
lines.push(` ${issue.code}:${issue.target}`);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (result.remaining.length > 0) {
|
|
1864
|
+
lines.push(`${result.remaining.length} remaining:`);
|
|
1865
|
+
for (const issue of result.remaining) {
|
|
1866
|
+
lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (result.applied.length === 0 && result.remaining.length === 0) {
|
|
1870
|
+
lines.push("no issues");
|
|
1871
|
+
}
|
|
1872
|
+
lines.push(`task health: ${result.health}`);
|
|
1873
|
+
return lines.join("\n");
|
|
1874
|
+
}
|
|
1875
|
+
catch (error) {
|
|
1876
|
+
return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1799
1879
|
// command.kind === "task.sessions"
|
|
1800
1880
|
const lines = taskMod.boardSessions();
|
|
1801
1881
|
return lines.length > 0 ? lines.join("\n") : "no active sessions";
|
|
@@ -2580,7 +2660,7 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
2580
2660
|
// ── task subcommands (local, no daemon socket needed) ──
|
|
2581
2661
|
if (command.kind === "task.board" || command.kind === "task.create" || command.kind === "task.update" ||
|
|
2582
2662
|
command.kind === "task.show" || command.kind === "task.actionable" || command.kind === "task.deps" ||
|
|
2583
|
-
command.kind === "task.sessions") {
|
|
2663
|
+
command.kind === "task.sessions" || command.kind === "task.fix") {
|
|
2584
2664
|
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
2585
2665
|
const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
|
|
2586
2666
|
/* v8 ignore stop */
|
|
@@ -73,7 +73,24 @@ class HabitScheduler {
|
|
|
73
73
|
const jobs = this.buildJobs(habits);
|
|
74
74
|
this.osCronManager.sync(jobs);
|
|
75
75
|
this.verifyCronAndCreateFallbacks(jobs);
|
|
76
|
-
|
|
76
|
+
this.fireOverdueHabits(habits);
|
|
77
|
+
}
|
|
78
|
+
reconcile() {
|
|
79
|
+
(0, runtime_1.emitNervesEvent)({
|
|
80
|
+
component: "daemon",
|
|
81
|
+
event: "daemon.habit_scheduler_reconcile",
|
|
82
|
+
message: "habit scheduler reconciling",
|
|
83
|
+
meta: { agent: this.agent },
|
|
84
|
+
});
|
|
85
|
+
// Clear ALL existing timers FIRST to prevent overlap window
|
|
86
|
+
this.clearAllTimerFallbacks();
|
|
87
|
+
const habits = this.scanHabits();
|
|
88
|
+
const jobs = this.buildJobs(habits);
|
|
89
|
+
this.osCronManager.sync(jobs);
|
|
90
|
+
this.verifyCronAndCreateFallbacks(jobs);
|
|
91
|
+
this.fireOverdueHabits(habits);
|
|
92
|
+
}
|
|
93
|
+
fireOverdueHabits(habits) {
|
|
77
94
|
for (const habit of habits) {
|
|
78
95
|
if (habit.status !== "active")
|
|
79
96
|
continue;
|
|
@@ -106,20 +123,6 @@ class HabitScheduler {
|
|
|
106
123
|
}
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
|
-
reconcile() {
|
|
110
|
-
(0, runtime_1.emitNervesEvent)({
|
|
111
|
-
component: "daemon",
|
|
112
|
-
event: "daemon.habit_scheduler_reconcile",
|
|
113
|
-
message: "habit scheduler reconciling",
|
|
114
|
-
meta: { agent: this.agent },
|
|
115
|
-
});
|
|
116
|
-
// Clear ALL existing timers FIRST to prevent overlap window
|
|
117
|
-
this.clearAllTimerFallbacks();
|
|
118
|
-
const habits = this.scanHabits();
|
|
119
|
-
const jobs = this.buildJobs(habits);
|
|
120
|
-
this.osCronManager.sync(jobs);
|
|
121
|
-
this.verifyCronAndCreateFallbacks(jobs);
|
|
122
|
-
}
|
|
123
126
|
stop() {
|
|
124
127
|
(0, runtime_1.emitNervesEvent)({
|
|
125
128
|
component: "daemon",
|
package/dist/mind/prompt.js
CHANGED
|
@@ -51,6 +51,7 @@ exports.channelNatureSection = channelNatureSection;
|
|
|
51
51
|
exports.groupChatParticipationSection = groupChatParticipationSection;
|
|
52
52
|
exports.feedbackSignalSection = feedbackSignalSection;
|
|
53
53
|
exports.mixedTrustGroupSection = mixedTrustGroupSection;
|
|
54
|
+
exports.rhythmStatusSection = rhythmStatusSection;
|
|
54
55
|
exports.buildSystem = buildSystem;
|
|
55
56
|
const fs = __importStar(require("fs"));
|
|
56
57
|
const path = __importStar(require("path"));
|
|
@@ -71,6 +72,7 @@ const session_activity_1 = require("../heart/session-activity");
|
|
|
71
72
|
const active_work_1 = require("../heart/active-work");
|
|
72
73
|
const commitments_1 = require("../heart/commitments");
|
|
73
74
|
const obligation_steering_1 = require("./obligation-steering");
|
|
75
|
+
const daemon_health_1 = require("../heart/daemon/daemon-health");
|
|
74
76
|
// Lazy-loaded psyche text cache
|
|
75
77
|
let _psycheCache = null;
|
|
76
78
|
let _senseStatusLinesCache = null;
|
|
@@ -886,6 +888,39 @@ function mixedTrustGroupSection(context) {
|
|
|
886
888
|
return "";
|
|
887
889
|
return "## mixed trust group\nin this group chat, my capabilities depend on who's talking. some people here have full trust, others don't — i adjust what i can do based on who's asking.";
|
|
888
890
|
}
|
|
891
|
+
function formatElapsedBrief(ms) {
|
|
892
|
+
const minutes = Math.floor(ms / 60000);
|
|
893
|
+
if (minutes < 60)
|
|
894
|
+
return `${minutes}m ago`;
|
|
895
|
+
const hours = Math.floor(minutes / 60);
|
|
896
|
+
return `${hours}h ago`;
|
|
897
|
+
}
|
|
898
|
+
function rhythmStatusSection() {
|
|
899
|
+
try {
|
|
900
|
+
const health = (0, daemon_health_1.readHealth)((0, daemon_health_1.getDefaultHealthPath)());
|
|
901
|
+
if (!health)
|
|
902
|
+
return "";
|
|
903
|
+
const habitNames = Object.keys(health.habits);
|
|
904
|
+
if (habitNames.length === 0)
|
|
905
|
+
return "";
|
|
906
|
+
const nowMs = Date.now();
|
|
907
|
+
const parts = [];
|
|
908
|
+
for (const name of habitNames) {
|
|
909
|
+
const h = health.habits[name];
|
|
910
|
+
const lastFired = h.lastFired ? formatElapsedBrief(nowMs - new Date(h.lastFired).getTime()) : "never";
|
|
911
|
+
parts.push(`${name} last fired ${lastFired}`);
|
|
912
|
+
}
|
|
913
|
+
const degradedNote = health.degraded.length > 0
|
|
914
|
+
? health.degraded.map((d) => `${d.component}: ${d.reason}`).join("; ") + "."
|
|
915
|
+
: "healthy.";
|
|
916
|
+
return `my rhythms: ${parts.join(". ")}. ${degradedNote}`;
|
|
917
|
+
/* v8 ignore start -- defensive: readHealth handles its own errors; this catch is a safety net for truly unexpected failures @preserve */
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
920
|
+
return "";
|
|
921
|
+
}
|
|
922
|
+
/* v8 ignore stop */
|
|
923
|
+
}
|
|
889
924
|
async function buildSystem(channel = "cli", options, context) {
|
|
890
925
|
(0, runtime_1.emitNervesEvent)({
|
|
891
926
|
event: "mind.step_start",
|
|
@@ -906,6 +941,7 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
906
941
|
channel === "inner" ? journalSection((0, identity_1.getAgentRoot)()) : "",
|
|
907
942
|
loopOrientationSection(channel),
|
|
908
943
|
runtimeInfoSection(channel),
|
|
944
|
+
rhythmStatusSection(),
|
|
909
945
|
channelNatureSection((0, channel_1.getChannelCapabilities)(channel)),
|
|
910
946
|
providerSection(),
|
|
911
947
|
dateSection(),
|
|
@@ -7,14 +7,21 @@ exports.boardDeps = boardDeps;
|
|
|
7
7
|
exports.boardSessions = boardSessions;
|
|
8
8
|
const runtime_1 = require("../../nerves/runtime");
|
|
9
9
|
const transitions_1 = require("./transitions");
|
|
10
|
-
const
|
|
10
|
+
const BOARD_ACTIVE_ORDER = [
|
|
11
11
|
"blocked",
|
|
12
12
|
"processing",
|
|
13
13
|
"collaborating",
|
|
14
14
|
"drafting",
|
|
15
15
|
"validating",
|
|
16
16
|
"paused",
|
|
17
|
+
];
|
|
18
|
+
const BOARD_TERMINAL_ORDER = [
|
|
17
19
|
"done",
|
|
20
|
+
"cancelled",
|
|
21
|
+
];
|
|
22
|
+
const BOARD_STATUS_ORDER = [
|
|
23
|
+
...BOARD_ACTIVE_ORDER,
|
|
24
|
+
...BOARD_TERMINAL_ORDER,
|
|
18
25
|
];
|
|
19
26
|
function compactName(task) {
|
|
20
27
|
const suffix = task.stem.replace(/^\d{4}-\d{2}-\d{2}-\d{4}-/, "");
|
|
@@ -29,6 +36,7 @@ function groupByStatus(tasks) {
|
|
|
29
36
|
paused: [],
|
|
30
37
|
blocked: [],
|
|
31
38
|
done: [],
|
|
39
|
+
cancelled: [],
|
|
32
40
|
};
|
|
33
41
|
for (const task of tasks) {
|
|
34
42
|
grouped[task.status].push(compactName(task));
|
|
@@ -65,8 +73,20 @@ function activeBridgeLines(tasks) {
|
|
|
65
73
|
.map((task) => `${task.stem} -> ${String(task.frontmatter.active_bridge).trim()}`)
|
|
66
74
|
.sort();
|
|
67
75
|
}
|
|
76
|
+
function buildHealthLine(issues) {
|
|
77
|
+
if (issues.length === 0)
|
|
78
|
+
return "health: clean";
|
|
79
|
+
const liveCount = issues.filter((i) => i.category === "live").length;
|
|
80
|
+
const migrationCount = issues.filter((i) => i.category === "migration").length;
|
|
81
|
+
const parts = [];
|
|
82
|
+
if (liveCount > 0)
|
|
83
|
+
parts.push(`${liveCount} live`);
|
|
84
|
+
if (migrationCount > 0)
|
|
85
|
+
parts.push(`${migrationCount} migration`);
|
|
86
|
+
return `health: ${parts.join(", ")}`;
|
|
87
|
+
}
|
|
68
88
|
function actionRequired(index, byStatus) {
|
|
69
|
-
const actions =
|
|
89
|
+
const actions = index.issues.map((issue) => `${issue.code}: ${issue.description} [${issue.target}]`);
|
|
70
90
|
if (byStatus.blocked.length > 0) {
|
|
71
91
|
actions.push(`blocked tasks: ${byStatus.blocked.join(", ")}`);
|
|
72
92
|
}
|
|
@@ -83,14 +103,19 @@ function buildTaskBoard(index) {
|
|
|
83
103
|
meta: { taskCount: index.tasks.length },
|
|
84
104
|
});
|
|
85
105
|
const byStatus = groupByStatus(index.tasks);
|
|
86
|
-
const
|
|
106
|
+
const activeCounts = BOARD_ACTIVE_ORDER.map((status) => `${status}:${byStatus[status].length}`).join(" ");
|
|
87
107
|
const processing = byStatus.processing.length > 0 ? `\n processing: ${byStatus.processing.join(", ")}` : "";
|
|
88
108
|
const blocked = byStatus.blocked.length > 0 ? `\n blocked: ${byStatus.blocked.join(", ")}` : "";
|
|
89
|
-
const
|
|
109
|
+
const terminalParts = BOARD_TERMINAL_ORDER
|
|
110
|
+
.filter((status) => byStatus[status].length > 0)
|
|
111
|
+
.map((status) => `${status}:${byStatus[status].length}`);
|
|
112
|
+
const terminalLine = terminalParts.length > 0 ? `\n terminal: ${terminalParts.join(" ")}` : "";
|
|
113
|
+
const healthLine = `\n ${buildHealthLine(index.issues)}`;
|
|
114
|
+
const compact = `[Tasks] ${activeCounts}${processing}${blocked}${terminalLine}${healthLine}`;
|
|
90
115
|
const fullLines = [];
|
|
91
116
|
for (const status of BOARD_STATUS_ORDER) {
|
|
92
117
|
const names = byStatus[status];
|
|
93
|
-
if (status === "done" && names.length === 0)
|
|
118
|
+
if ((status === "done" || status === "cancelled") && names.length === 0)
|
|
94
119
|
continue;
|
|
95
120
|
fullLines.push(`## ${status}`);
|
|
96
121
|
fullLines.push(names.length > 0 ? names.map((name) => `- ${name}`).join("\n") : "- (none)");
|
|
@@ -114,6 +139,7 @@ function buildTaskBoard(index) {
|
|
|
114
139
|
compact,
|
|
115
140
|
full: fullLines.join("\n\n"),
|
|
116
141
|
byStatus,
|
|
142
|
+
issues: index.issues,
|
|
117
143
|
actionRequired: actionRequired(index, byStatus),
|
|
118
144
|
unresolvedDependencies: unresolved,
|
|
119
145
|
activeSessions: active,
|
|
@@ -0,0 +1,182 @@
|
|
|
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.addKindToLegacyCard = addKindToLegacyCard;
|
|
37
|
+
exports.applyFixes = applyFixes;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
41
|
+
const scanner_1 = require("./scanner");
|
|
42
|
+
const parser_1 = require("./parser");
|
|
43
|
+
function issueId(issue) {
|
|
44
|
+
return `${issue.code}:${issue.target}`;
|
|
45
|
+
}
|
|
46
|
+
function addKindToLegacyCard(root, issue) {
|
|
47
|
+
(0, runtime_1.emitNervesEvent)({
|
|
48
|
+
event: "repertoire.fix_apply_start",
|
|
49
|
+
component: "repertoire",
|
|
50
|
+
message: "adding kind: task to legacy card",
|
|
51
|
+
meta: { target: issue.target },
|
|
52
|
+
});
|
|
53
|
+
const filePath = path.join(root, issue.target);
|
|
54
|
+
if (!fs.existsSync(filePath))
|
|
55
|
+
return false;
|
|
56
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
57
|
+
const frontmatter = (0, scanner_1.tryExtractFrontmatter)(content);
|
|
58
|
+
if (!frontmatter)
|
|
59
|
+
return false;
|
|
60
|
+
// Add kind: task to frontmatter
|
|
61
|
+
frontmatter.kind = "task";
|
|
62
|
+
// Extract body (everything after the second ---)
|
|
63
|
+
const lines = content.split(/\r?\n/);
|
|
64
|
+
const firstDelim = lines.findIndex((line) => line.trim() === "---");
|
|
65
|
+
const secondDelim = lines.findIndex((line, idx) => idx > firstDelim && line.trim() === "---");
|
|
66
|
+
const body = lines.slice(secondDelim + 1).join("\n").replace(/^\n/, "");
|
|
67
|
+
const rendered = (0, parser_1.renderTaskFile)(frontmatter, body);
|
|
68
|
+
fs.writeFileSync(filePath, rendered, "utf-8");
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
function applySingleFix(root, issue) {
|
|
72
|
+
switch (issue.code) {
|
|
73
|
+
case "schema-missing-kind":
|
|
74
|
+
return addKindToLegacyCard(root, issue);
|
|
75
|
+
default:
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function applyFixes(options, root) {
|
|
80
|
+
(0, runtime_1.emitNervesEvent)({
|
|
81
|
+
event: "repertoire.fix_start",
|
|
82
|
+
component: "repertoire",
|
|
83
|
+
message: `applying fixes in ${options.mode} mode`,
|
|
84
|
+
meta: { mode: options.mode, root },
|
|
85
|
+
});
|
|
86
|
+
(0, scanner_1.clearTaskScanCache)();
|
|
87
|
+
const index = (0, scanner_1.scanTasks)(root);
|
|
88
|
+
const issues = index.issues;
|
|
89
|
+
if (options.mode === "dry-run") {
|
|
90
|
+
(0, runtime_1.emitNervesEvent)({
|
|
91
|
+
event: "repertoire.fix_complete",
|
|
92
|
+
component: "repertoire",
|
|
93
|
+
message: "dry-run complete, no changes made",
|
|
94
|
+
meta: { issueCount: issues.length },
|
|
95
|
+
});
|
|
96
|
+
const health = issues.length === 0 ? "clean" : buildHealthSummary(issues);
|
|
97
|
+
return {
|
|
98
|
+
applied: [],
|
|
99
|
+
remaining: issues,
|
|
100
|
+
skipped: [],
|
|
101
|
+
health,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (options.mode === "single") {
|
|
105
|
+
const targetId = options.issueId ?? "";
|
|
106
|
+
const match = issues.find((i) => issueId(i) === targetId);
|
|
107
|
+
if (!match) {
|
|
108
|
+
(0, runtime_1.emitNervesEvent)({
|
|
109
|
+
event: "repertoire.fix_complete",
|
|
110
|
+
component: "repertoire",
|
|
111
|
+
message: "single fix: issue not found",
|
|
112
|
+
meta: { issueId: targetId },
|
|
113
|
+
});
|
|
114
|
+
const health = issues.length === 0 ? "clean" : buildHealthSummary(issues);
|
|
115
|
+
return {
|
|
116
|
+
applied: [],
|
|
117
|
+
remaining: issues,
|
|
118
|
+
skipped: [],
|
|
119
|
+
health,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const success = applySingleFix(root, match);
|
|
123
|
+
(0, scanner_1.clearTaskScanCache)();
|
|
124
|
+
const afterIndex = (0, scanner_1.scanTasks)(root);
|
|
125
|
+
const afterIssues = afterIndex.issues;
|
|
126
|
+
const health = afterIssues.length === 0 ? "clean" : buildHealthSummary(afterIssues);
|
|
127
|
+
(0, runtime_1.emitNervesEvent)({
|
|
128
|
+
event: "repertoire.fix_complete",
|
|
129
|
+
component: "repertoire",
|
|
130
|
+
message: `single fix ${success ? "applied" : "failed"}`,
|
|
131
|
+
meta: { issueId: targetId, success },
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
applied: success ? [match] : [],
|
|
135
|
+
remaining: afterIssues,
|
|
136
|
+
skipped: success ? [] : [match],
|
|
137
|
+
health,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// safe mode: apply all safe-confidence fixes
|
|
141
|
+
const applied = [];
|
|
142
|
+
const skipped = [];
|
|
143
|
+
for (const issue of issues) {
|
|
144
|
+
if (issue.confidence !== "safe") {
|
|
145
|
+
skipped.push(issue);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const success = applySingleFix(root, issue);
|
|
149
|
+
if (success) {
|
|
150
|
+
applied.push(issue);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
skipped.push(issue);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
(0, scanner_1.clearTaskScanCache)();
|
|
157
|
+
const afterIndex = (0, scanner_1.scanTasks)(root);
|
|
158
|
+
const afterIssues = afterIndex.issues;
|
|
159
|
+
const health = afterIssues.length === 0 ? "clean" : buildHealthSummary(afterIssues);
|
|
160
|
+
(0, runtime_1.emitNervesEvent)({
|
|
161
|
+
event: "repertoire.fix_complete",
|
|
162
|
+
component: "repertoire",
|
|
163
|
+
message: `safe fixes complete: ${applied.length} applied, ${skipped.length} skipped`,
|
|
164
|
+
meta: { applied: applied.length, skipped: skipped.length },
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
applied,
|
|
168
|
+
remaining: afterIssues,
|
|
169
|
+
skipped,
|
|
170
|
+
health,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function buildHealthSummary(issues) {
|
|
174
|
+
const liveCount = issues.filter((i) => i.category === "live").length;
|
|
175
|
+
const migrationCount = issues.filter((i) => i.category === "migration").length;
|
|
176
|
+
const parts = [];
|
|
177
|
+
if (liveCount > 0)
|
|
178
|
+
parts.push(`${liveCount} live`);
|
|
179
|
+
if (migrationCount > 0)
|
|
180
|
+
parts.push(`${migrationCount} migration`);
|
|
181
|
+
return parts.join(", ");
|
|
182
|
+
}
|
|
@@ -40,6 +40,7 @@ const path = __importStar(require("path"));
|
|
|
40
40
|
const config_1 = require("../../heart/config");
|
|
41
41
|
const runtime_1 = require("../../nerves/runtime");
|
|
42
42
|
const board_1 = require("./board");
|
|
43
|
+
const fix_1 = require("./fix");
|
|
43
44
|
const lifecycle_1 = require("./lifecycle");
|
|
44
45
|
const parser_1 = require("./parser");
|
|
45
46
|
const scanner_1 = require("./scanner");
|
|
@@ -97,6 +98,7 @@ class FileTaskModule {
|
|
|
97
98
|
const filePath = path.join(root, collection, filename);
|
|
98
99
|
const today = formatDate();
|
|
99
100
|
const frontmatter = {
|
|
101
|
+
kind: "task",
|
|
100
102
|
type,
|
|
101
103
|
category: input.category || "infrastructure",
|
|
102
104
|
title: input.title,
|
|
@@ -108,7 +110,6 @@ class FileTaskModule {
|
|
|
108
110
|
lastRun: input.lastRun ?? null,
|
|
109
111
|
created: today,
|
|
110
112
|
updated: today,
|
|
111
|
-
child_tasks: [],
|
|
112
113
|
artifacts: [],
|
|
113
114
|
};
|
|
114
115
|
if (type === "one-shot") {
|
|
@@ -192,6 +193,9 @@ class FileTaskModule {
|
|
|
192
193
|
}
|
|
193
194
|
return (0, middleware_1.validateSpawn)(task, spawnType);
|
|
194
195
|
}
|
|
196
|
+
fix(options) {
|
|
197
|
+
return (0, fix_1.applyFixes)(options, (0, scanner_1.getTaskRoot)());
|
|
198
|
+
}
|
|
195
199
|
detectStale(thresholdDays) {
|
|
196
200
|
return (0, lifecycle_1.detectStaleTasks)(this.scan(), thresholdDays);
|
|
197
201
|
}
|
|
@@ -48,7 +48,7 @@ function archiveCompletedTasks(index) {
|
|
|
48
48
|
const archived = [];
|
|
49
49
|
const failures = [];
|
|
50
50
|
for (const task of index.tasks) {
|
|
51
|
-
if (task.status !== "done")
|
|
51
|
+
if (task.status !== "done" && task.status !== "cancelled")
|
|
52
52
|
continue;
|
|
53
53
|
const archiveDir = path.join(index.root, "archive", task.collection);
|
|
54
54
|
const archiveFile = path.join(archiveDir, task.name);
|
|
@@ -73,7 +73,7 @@ function detectStaleTasks(index, thresholdDays, now = new Date()) {
|
|
|
73
73
|
const updated = Date.parse(task.updated);
|
|
74
74
|
if (Number.isNaN(updated))
|
|
75
75
|
return false;
|
|
76
|
-
if (task.status === "done")
|
|
76
|
+
if (task.status === "done" || task.status === "cancelled")
|
|
77
77
|
return false;
|
|
78
78
|
return updated < staleCutoffMs;
|
|
79
79
|
});
|
|
@@ -136,6 +136,9 @@ function parseTaskFile(content, filePath) {
|
|
|
136
136
|
_isCanonicalFilename: (0, transitions_1.isCanonicalTaskFilename)(name),
|
|
137
137
|
},
|
|
138
138
|
body: parsed.body,
|
|
139
|
+
hasWorkDir: false,
|
|
140
|
+
workDirFiles: [],
|
|
141
|
+
derivedChildren: [],
|
|
139
142
|
};
|
|
140
143
|
}
|
|
141
144
|
function formatFrontmatterValue(value) {
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getTaskRoot = getTaskRoot;
|
|
37
37
|
exports.ensureTaskLayout = ensureTaskLayout;
|
|
38
|
+
exports.tryExtractFrontmatter = tryExtractFrontmatter;
|
|
38
39
|
exports.clearTaskScanCache = clearTaskScanCache;
|
|
39
40
|
exports.scanTasks = scanTasks;
|
|
40
41
|
const fs = __importStar(require("fs"));
|
|
@@ -59,30 +60,57 @@ function ensureTaskLayout(root = getTaskRoot()) {
|
|
|
59
60
|
fs.mkdirSync(dir, { recursive: true });
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Attempt to extract frontmatter from markdown content.
|
|
65
|
+
* Returns parsed frontmatter dict or null. Never throws.
|
|
66
|
+
*/
|
|
67
|
+
function tryExtractFrontmatter(content) {
|
|
68
|
+
(0, runtime_1.emitNervesEvent)({
|
|
69
|
+
event: "repertoire.frontmatter_extract_start",
|
|
70
|
+
component: "repertoire",
|
|
71
|
+
message: "attempting frontmatter extraction",
|
|
72
|
+
});
|
|
73
|
+
const lines = content.split(/\r?\n/);
|
|
74
|
+
if (lines[0]?.trim() !== "---") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
78
|
+
if (closing === -1) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const rawFrontmatter = lines.slice(1, closing).join("\n");
|
|
82
|
+
return (0, parser_1.parseFrontmatter)(rawFrontmatter);
|
|
83
|
+
}
|
|
84
|
+
const LEGACY_TASK_TYPES = ["one-shot", "ongoing", "habit"];
|
|
85
|
+
function buildFingerprint(root) {
|
|
86
|
+
(0, runtime_1.emitNervesEvent)({
|
|
87
|
+
event: "repertoire.fingerprint_build_start",
|
|
88
|
+
component: "repertoire",
|
|
89
|
+
message: "building scan fingerprint",
|
|
90
|
+
meta: { root },
|
|
91
|
+
});
|
|
92
|
+
const segments = [];
|
|
93
|
+
for (const collection of transitions_1.TASK_CANONICAL_COLLECTIONS) {
|
|
94
|
+
const collDir = path.join(root, collection);
|
|
95
|
+
const entries = fs.readdirSync(collDir, { withFileTypes: true });
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
69
98
|
continue;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
if (entry.name.endsWith(".md")) {
|
|
75
|
-
acc.push(path.join(dir, entry.name));
|
|
99
|
+
const filePath = path.join(collDir, entry.name);
|
|
100
|
+
const stat = fs.statSync(filePath);
|
|
101
|
+
segments.push(`${filePath}:${stat.mtimeMs}:${stat.size}`);
|
|
76
102
|
}
|
|
77
103
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
.
|
|
104
|
+
// Also include root-level md files for orphan detection fingerprinting
|
|
105
|
+
const rootEntries = fs.readdirSync(root, { withFileTypes: true });
|
|
106
|
+
for (const entry of rootEntries) {
|
|
107
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
108
|
+
continue;
|
|
109
|
+
const filePath = path.join(root, entry.name);
|
|
82
110
|
const stat = fs.statSync(filePath);
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
111
|
+
segments.push(`${filePath}:${stat.mtimeMs}:${stat.size}`);
|
|
112
|
+
}
|
|
113
|
+
segments.sort();
|
|
86
114
|
return segments.join("|");
|
|
87
115
|
}
|
|
88
116
|
function clearTaskScanCache() {
|
|
@@ -96,35 +124,164 @@ function scanTasks(root = getTaskRoot()) {
|
|
|
96
124
|
meta: { root },
|
|
97
125
|
});
|
|
98
126
|
ensureTaskLayout(root);
|
|
99
|
-
const
|
|
100
|
-
for (const collection of transitions_1.TASK_CANONICAL_COLLECTIONS) {
|
|
101
|
-
walkMarkdownFiles(path.join(root, collection), files);
|
|
102
|
-
}
|
|
103
|
-
const fingerprint = buildFingerprint(files);
|
|
127
|
+
const fingerprint = buildFingerprint(root);
|
|
104
128
|
if (scanCache && scanCache.fingerprint === fingerprint) {
|
|
105
129
|
return scanCache.index;
|
|
106
130
|
}
|
|
107
131
|
const tasks = [];
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
for (const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
const issues = [];
|
|
133
|
+
// Scan each collection with flat directory reads (no recursion)
|
|
134
|
+
for (const collection of transitions_1.TASK_CANONICAL_COLLECTIONS) {
|
|
135
|
+
const collDir = path.join(root, collection);
|
|
136
|
+
const entries = fs.readdirSync(collDir, { withFileTypes: true });
|
|
137
|
+
const dirNames = new Set();
|
|
138
|
+
// Collect directory names for work dir detection
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (entry.isDirectory()) {
|
|
141
|
+
dirNames.add(entry.name);
|
|
142
|
+
}
|
|
114
143
|
}
|
|
115
|
-
|
|
144
|
+
// Process only .md files at collection root (flat, no recursion)
|
|
145
|
+
let supportDocCount = 0;
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
148
|
+
continue;
|
|
149
|
+
const filePath = path.join(collDir, entry.name);
|
|
116
150
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
117
|
-
|
|
151
|
+
// Step 1: Try to extract frontmatter
|
|
152
|
+
const frontmatter = tryExtractFrontmatter(content);
|
|
153
|
+
if (!frontmatter) {
|
|
154
|
+
// No frontmatter — not a task, count as support doc clutter
|
|
155
|
+
supportDocCount += 1;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// Step 2: Check kind: task
|
|
159
|
+
const isKindTask = frontmatter.kind === "task";
|
|
160
|
+
const fmType = typeof frontmatter.type === "string" ? frontmatter.type.trim().toLowerCase() : "";
|
|
161
|
+
const isLegacyTask = !isKindTask && LEGACY_TASK_TYPES.includes(fmType);
|
|
162
|
+
if (!isKindTask && !isLegacyTask) {
|
|
163
|
+
// Has frontmatter but not a task — skip silently
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// Step 3: Emit migration issue for legacy task
|
|
167
|
+
if (isLegacyTask) {
|
|
168
|
+
const relPath = path.relative(root, filePath);
|
|
169
|
+
issues.push({
|
|
170
|
+
target: relPath,
|
|
171
|
+
code: "schema-missing-kind",
|
|
172
|
+
description: "Task card missing kind: task field",
|
|
173
|
+
fix: "Add kind: task to frontmatter",
|
|
174
|
+
confidence: "safe",
|
|
175
|
+
category: "migration",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// Step 4: Parse the task file
|
|
179
|
+
try {
|
|
180
|
+
const task = (0, parser_1.parseTaskFile)(content, filePath);
|
|
181
|
+
// Check filename canonicality
|
|
182
|
+
const base = path.basename(filePath);
|
|
183
|
+
if (!(0, transitions_1.isCanonicalTaskFilename)(base)) {
|
|
184
|
+
const relPath = path.relative(root, filePath);
|
|
185
|
+
issues.push({
|
|
186
|
+
target: relPath,
|
|
187
|
+
code: "filename-not-canonical",
|
|
188
|
+
description: `Non-canonical filename: ${base}`,
|
|
189
|
+
fix: `Rename to canonical format (YYYY-MM-DD-HHMM-slug.md)`,
|
|
190
|
+
confidence: "safe",
|
|
191
|
+
category: "migration",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// Work dir detection
|
|
195
|
+
const stem = base.replace(/\.md$/i, "");
|
|
196
|
+
if (dirNames.has(stem)) {
|
|
197
|
+
task.hasWorkDir = true;
|
|
198
|
+
const workDirPath = path.join(collDir, stem);
|
|
199
|
+
task.workDirFiles = fs.readdirSync(workDirPath).sort();
|
|
200
|
+
}
|
|
201
|
+
tasks.push(task);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
// Parse error on a file we identified as a task — real issue
|
|
205
|
+
const relPath = path.relative(root, filePath);
|
|
206
|
+
issues.push({
|
|
207
|
+
target: relPath,
|
|
208
|
+
code: "schema-invalid",
|
|
209
|
+
description: `Parse error: ${error instanceof Error ? error.message : String(error)}`,
|
|
210
|
+
fix: "Fix the task card schema (ensure required fields: type, status, etc.)",
|
|
211
|
+
confidence: "needs_review",
|
|
212
|
+
category: "live",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Summarize non-task support docs at collection root as one migration issue
|
|
217
|
+
if (supportDocCount > 0) {
|
|
218
|
+
(0, runtime_1.emitNervesEvent)({
|
|
219
|
+
event: "repertoire.support_docs_detected",
|
|
220
|
+
component: "repertoire",
|
|
221
|
+
message: "non-task support documents at collection root",
|
|
222
|
+
meta: { collection, count: supportDocCount },
|
|
223
|
+
});
|
|
224
|
+
issues.push({
|
|
225
|
+
target: collection,
|
|
226
|
+
code: "org-collection-root-clutter",
|
|
227
|
+
description: `${supportDocCount} non-task file${supportDocCount === 1 ? "" : "s"} at ${collection}/ root (doing docs, planning docs, etc.)`,
|
|
228
|
+
fix: "Move support docs into same-stem work directories for their parent tasks",
|
|
229
|
+
confidence: "needs_review",
|
|
230
|
+
category: "migration",
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Orphan detection: root-level .md files outside any canonical collection
|
|
235
|
+
const rootEntries = fs.readdirSync(root, { withFileTypes: true });
|
|
236
|
+
const collectionSet = new Set(transitions_1.TASK_CANONICAL_COLLECTIONS);
|
|
237
|
+
for (const entry of rootEntries) {
|
|
238
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
239
|
+
continue;
|
|
240
|
+
const filePath = path.join(root, entry.name);
|
|
241
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
242
|
+
const frontmatter = tryExtractFrontmatter(content);
|
|
243
|
+
// Only flag as orphan if it has task-like frontmatter
|
|
244
|
+
if (!frontmatter)
|
|
245
|
+
continue;
|
|
246
|
+
const fmType = typeof frontmatter.type === "string" ? frontmatter.type.trim().toLowerCase() : "";
|
|
247
|
+
const hasTaskLikeContent = frontmatter.kind === "task" ||
|
|
248
|
+
LEGACY_TASK_TYPES.includes(fmType) ||
|
|
249
|
+
(typeof frontmatter.status === "string" && typeof frontmatter.title === "string");
|
|
250
|
+
if (hasTaskLikeContent) {
|
|
251
|
+
(0, runtime_1.emitNervesEvent)({
|
|
252
|
+
event: "repertoire.orphan_detected",
|
|
253
|
+
component: "repertoire",
|
|
254
|
+
message: "root-level orphan document detected",
|
|
255
|
+
meta: { filePath },
|
|
256
|
+
});
|
|
257
|
+
issues.push({
|
|
258
|
+
target: entry.name,
|
|
259
|
+
code: "org-root-level-doc",
|
|
260
|
+
description: `Root-level document outside any collection: ${entry.name}`,
|
|
261
|
+
fix: `Move to appropriate collection directory (${[...collectionSet].join(", ")})`,
|
|
262
|
+
confidence: "needs_review",
|
|
263
|
+
category: "migration",
|
|
264
|
+
});
|
|
118
265
|
}
|
|
119
|
-
|
|
120
|
-
|
|
266
|
+
}
|
|
267
|
+
// Populate derivedChildren from parent_task links
|
|
268
|
+
const stemToTask = new Map();
|
|
269
|
+
for (const task of tasks) {
|
|
270
|
+
stemToTask.set(task.stem, task);
|
|
271
|
+
}
|
|
272
|
+
for (const task of tasks) {
|
|
273
|
+
const parentStem = typeof task.frontmatter.parent_task === "string"
|
|
274
|
+
? task.frontmatter.parent_task.trim()
|
|
275
|
+
: "";
|
|
276
|
+
if (parentStem && stemToTask.has(parentStem)) {
|
|
277
|
+
const parent = stemToTask.get(parentStem);
|
|
278
|
+
parent.derivedChildren.push(task.stem);
|
|
121
279
|
}
|
|
122
280
|
}
|
|
123
281
|
const index = {
|
|
124
282
|
root,
|
|
125
283
|
tasks,
|
|
126
|
-
|
|
127
|
-
parseErrors,
|
|
284
|
+
issues,
|
|
128
285
|
fingerprint,
|
|
129
286
|
};
|
|
130
287
|
scanCache = { fingerprint, index };
|
|
@@ -16,15 +16,17 @@ exports.TASK_VALID_STATUSES = [
|
|
|
16
16
|
"paused",
|
|
17
17
|
"blocked",
|
|
18
18
|
"done",
|
|
19
|
+
"cancelled",
|
|
19
20
|
];
|
|
20
21
|
exports.TASK_STATUS_TRANSITIONS = {
|
|
21
|
-
drafting: ["processing", "collaborating"],
|
|
22
|
-
processing: ["validating", "paused", "blocked"],
|
|
23
|
-
validating: ["done", "processing", "collaborating"],
|
|
24
|
-
collaborating: ["processing", "validating", "paused"],
|
|
25
|
-
paused: ["processing", "blocked"],
|
|
26
|
-
blocked: ["processing", "paused"],
|
|
22
|
+
drafting: ["processing", "collaborating", "cancelled"],
|
|
23
|
+
processing: ["validating", "paused", "blocked", "cancelled"],
|
|
24
|
+
validating: ["done", "processing", "collaborating", "cancelled"],
|
|
25
|
+
collaborating: ["processing", "validating", "paused", "cancelled"],
|
|
26
|
+
paused: ["processing", "blocked", "cancelled"],
|
|
27
|
+
blocked: ["processing", "paused", "cancelled"],
|
|
27
28
|
done: [],
|
|
29
|
+
cancelled: [],
|
|
28
30
|
};
|
|
29
31
|
exports.TASK_CANONICAL_TYPES = [
|
|
30
32
|
"one-shot",
|
|
@@ -42,6 +44,7 @@ exports.TASK_RESERVED_DIRECTORIES = ["templates", ".trash", "archive"];
|
|
|
42
44
|
exports.TASK_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}-\d{4}-[a-z0-9][a-z0-9-]*\.md$/;
|
|
43
45
|
exports.TASK_REQUIRED_TEMPLATE_FIELDS = {
|
|
44
46
|
"one-shot": [
|
|
47
|
+
"kind",
|
|
45
48
|
"type",
|
|
46
49
|
"category",
|
|
47
50
|
"title",
|
|
@@ -55,10 +58,10 @@ exports.TASK_REQUIRED_TEMPLATE_FIELDS = {
|
|
|
55
58
|
"updated",
|
|
56
59
|
"parent_task",
|
|
57
60
|
"depends_on",
|
|
58
|
-
"child_tasks",
|
|
59
61
|
"artifacts",
|
|
60
62
|
],
|
|
61
63
|
ongoing: [
|
|
64
|
+
"kind",
|
|
62
65
|
"type",
|
|
63
66
|
"category",
|
|
64
67
|
"title",
|
|
@@ -70,7 +73,6 @@ exports.TASK_REQUIRED_TEMPLATE_FIELDS = {
|
|
|
70
73
|
"lastRun",
|
|
71
74
|
"created",
|
|
72
75
|
"updated",
|
|
73
|
-
"child_tasks",
|
|
74
76
|
"artifacts",
|
|
75
77
|
],
|
|
76
78
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildHabitTurnMessage = buildHabitTurnMessage;
|
|
4
|
+
const runtime_1 = require("../nerves/runtime");
|
|
5
|
+
function formatElapsed(ms) {
|
|
6
|
+
const minutes = Math.floor(ms / 60000);
|
|
7
|
+
if (minutes < 60) {
|
|
8
|
+
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
|
|
9
|
+
}
|
|
10
|
+
const hours = Math.floor(minutes / 60);
|
|
11
|
+
return `${hours} ${hours === 1 ? "hour" : "hours"}`;
|
|
12
|
+
}
|
|
13
|
+
function buildHabitTurnMessage(options) {
|
|
14
|
+
const { habitName, habitTitle, habitBody, lastRun, checkpoint, alsoDue, staleObligations, parseErrors, degradedComponents, now, } = options;
|
|
15
|
+
const hasBody = habitBody !== undefined && habitBody !== "";
|
|
16
|
+
// First beat: lastRun is null
|
|
17
|
+
if (lastRun === null) {
|
|
18
|
+
// Cold start: no checkpoint, no body — bare awareness
|
|
19
|
+
if (!checkpoint && !hasBody) {
|
|
20
|
+
(0, runtime_1.emitNervesEvent)({
|
|
21
|
+
component: "senses",
|
|
22
|
+
event: "senses.habit_turn_message_built",
|
|
23
|
+
message: "habit turn message built (cold start)",
|
|
24
|
+
meta: { habitName, coldStart: true },
|
|
25
|
+
});
|
|
26
|
+
return "...time passing. anything stirring?";
|
|
27
|
+
}
|
|
28
|
+
if (!hasBody) {
|
|
29
|
+
// First beat with no body: nudge
|
|
30
|
+
const sections = [
|
|
31
|
+
`your ${habitTitle} fired but has no instructions \u2014 add a body to \`habits/${habitName}.md\``,
|
|
32
|
+
];
|
|
33
|
+
appendTrailingExtras(sections, alsoDue, staleObligations, parseErrors, degradedComponents);
|
|
34
|
+
(0, runtime_1.emitNervesEvent)({
|
|
35
|
+
component: "senses",
|
|
36
|
+
event: "senses.habit_turn_message_built",
|
|
37
|
+
message: "habit turn message built (first beat, no body)",
|
|
38
|
+
meta: { habitName, firstBeat: true, hasBody: false },
|
|
39
|
+
});
|
|
40
|
+
return sections.join("\n\n");
|
|
41
|
+
}
|
|
42
|
+
const sections = [
|
|
43
|
+
`your ${habitTitle} is alive. this is its first breath.`,
|
|
44
|
+
habitBody,
|
|
45
|
+
];
|
|
46
|
+
appendTrailingExtras(sections, alsoDue, staleObligations, parseErrors, degradedComponents);
|
|
47
|
+
(0, runtime_1.emitNervesEvent)({
|
|
48
|
+
component: "senses",
|
|
49
|
+
event: "senses.habit_turn_message_built",
|
|
50
|
+
message: "habit turn message built (first beat)",
|
|
51
|
+
meta: { habitName, firstBeat: true },
|
|
52
|
+
});
|
|
53
|
+
return sections.join("\n\n");
|
|
54
|
+
}
|
|
55
|
+
// Normal turn
|
|
56
|
+
const sections = [];
|
|
57
|
+
// 1. Checkpoint
|
|
58
|
+
if (checkpoint) {
|
|
59
|
+
sections.push(`you were thinking about ${checkpoint}.`);
|
|
60
|
+
}
|
|
61
|
+
// 2. Elapsed time
|
|
62
|
+
const nowMs = now().getTime();
|
|
63
|
+
const lastRunMs = new Date(lastRun).getTime();
|
|
64
|
+
const elapsed = nowMs - lastRunMs;
|
|
65
|
+
sections.push(`${formatElapsed(elapsed)} have passed.`);
|
|
66
|
+
// 3. Body or no-body nudge
|
|
67
|
+
if (hasBody) {
|
|
68
|
+
sections.push(habitBody);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
sections.push(`your ${habitTitle} fired but has no instructions \u2014 add a body to \`habits/${habitName}.md\``);
|
|
72
|
+
}
|
|
73
|
+
// 4-7. Trailing extras
|
|
74
|
+
appendTrailingExtras(sections, alsoDue, staleObligations, parseErrors, degradedComponents);
|
|
75
|
+
(0, runtime_1.emitNervesEvent)({
|
|
76
|
+
component: "senses",
|
|
77
|
+
event: "senses.habit_turn_message_built",
|
|
78
|
+
message: "habit turn message built",
|
|
79
|
+
meta: {
|
|
80
|
+
habitName,
|
|
81
|
+
hasCheckpoint: !!checkpoint,
|
|
82
|
+
hasBody: hasBody,
|
|
83
|
+
staleObligationCount: staleObligations.length,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
return sections.join("\n\n");
|
|
87
|
+
}
|
|
88
|
+
function appendTrailingExtras(sections, alsoDue, staleObligations, parseErrors, degradedComponents) {
|
|
89
|
+
// 4. Also-due
|
|
90
|
+
if (alsoDue) {
|
|
91
|
+
sections.push(alsoDue);
|
|
92
|
+
}
|
|
93
|
+
// 5. Stale obligations
|
|
94
|
+
if (staleObligations.length > 0) {
|
|
95
|
+
const lines = staleObligations.map((o) => `something for ${o.friendName} has been sitting for ${formatElapsed(o.stalenessMs)}`);
|
|
96
|
+
sections.push(lines.join("\n"));
|
|
97
|
+
}
|
|
98
|
+
// 6. Parse errors
|
|
99
|
+
if (parseErrors.length > 0) {
|
|
100
|
+
const lines = parseErrors.map((e) => `I noticed my habit file \`${e.file}\` has invalid frontmatter \u2014 I should fix it. (${e.error})`);
|
|
101
|
+
sections.push(lines.join("\n"));
|
|
102
|
+
}
|
|
103
|
+
// 7. Degraded state
|
|
104
|
+
if (degradedComponents.length > 0) {
|
|
105
|
+
const reasons = degradedComponents.map((d) => `${d.component}: ${d.reason}`).join("; ");
|
|
106
|
+
sections.push(`[note: my scheduling is degraded: ${reasons}]`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -67,7 +67,7 @@ const manager_1 = require("../heart/bridges/manager");
|
|
|
67
67
|
const session_activity_1 = require("../heart/session-activity");
|
|
68
68
|
const bluebubbles_1 = require("./bluebubbles");
|
|
69
69
|
const obligations_2 = require("../heart/obligations");
|
|
70
|
-
const
|
|
70
|
+
const habit_turn_message_1 = require("./habit-turn-message");
|
|
71
71
|
const journal_index_1 = require("../mind/journal-index");
|
|
72
72
|
const habit_parser_1 = require("../heart/daemon/habit-parser");
|
|
73
73
|
const cadence_1 = require("../heart/daemon/cadence");
|
|
@@ -559,106 +559,65 @@ async function runInnerDialogTurn(options) {
|
|
|
559
559
|
const habitFilePath = path.join(agentRoot, "habits", `${habitName}.md`);
|
|
560
560
|
// Read and parse the habit file
|
|
561
561
|
let habitBody;
|
|
562
|
+
let habitTitle = habitName;
|
|
562
563
|
let habitLastRun = null;
|
|
563
564
|
try {
|
|
564
565
|
const habitContent = fs.readFileSync(habitFilePath, "utf-8");
|
|
565
566
|
const parsed = (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath);
|
|
566
|
-
habitBody = parsed.body;
|
|
567
|
+
habitBody = parsed.body || undefined;
|
|
568
|
+
habitTitle = parsed.title || habitName;
|
|
567
569
|
habitLastRun = parsed.lastRun;
|
|
568
570
|
}
|
|
569
571
|
catch {
|
|
570
572
|
// Habit file missing or unreadable
|
|
571
573
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const raw = fs.readFileSync(runtimeStatePath, "utf-8");
|
|
579
|
-
const runtimeState = JSON.parse(raw);
|
|
580
|
-
lastCompletedAt = typeof runtimeState.lastCompletedAt === "string" ? runtimeState.lastCompletedAt : undefined;
|
|
581
|
-
}
|
|
582
|
-
catch {
|
|
583
|
-
// no runtime state — will get cold start fallback
|
|
584
|
-
}
|
|
574
|
+
// If the habit file couldn't be read at all (no body, no title parsed), error message
|
|
575
|
+
if (habitBody === undefined && habitTitle === habitName) {
|
|
576
|
+
userContent = `habit "${habitName}" could not be read (file not found or unreadable). check habits/${habitName}.md exists.`;
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
// Unified path: gather context for ALL habits (heartbeat included)
|
|
585
580
|
const obligations = (0, obligations_1.listActiveObligations)(agentName);
|
|
586
581
|
const nowMs = now().getTime();
|
|
587
|
-
const
|
|
588
|
-
id: o.id,
|
|
589
|
-
content: o.delegatedContent,
|
|
582
|
+
const staleObligations = obligations.map((o) => ({
|
|
590
583
|
friendName: o.origin.friendId,
|
|
591
|
-
|
|
592
|
-
|
|
584
|
+
content: o.delegatedContent,
|
|
585
|
+
stalenessMs: nowMs - o.createdAt,
|
|
593
586
|
}));
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
587
|
+
const alsoDue = buildAlsoDueLine(agentRoot, habitName, now);
|
|
588
|
+
// Degraded state (best-effort: never crash)
|
|
589
|
+
let degradedComponents = [];
|
|
590
|
+
try {
|
|
591
|
+
const health = (0, daemon_health_1.readHealth)((0, daemon_health_1.getDefaultHealthPath)());
|
|
592
|
+
if (health && health.degraded.length > 0) {
|
|
593
|
+
degradedComponents = health.degraded.map((d) => ({ component: d.component, reason: d.reason }));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
// Best-effort: missing file or parse error -> empty array, no crash
|
|
598
|
+
}
|
|
599
|
+
userContent = (0, habit_turn_message_1.buildHabitTurnMessage)({
|
|
600
|
+
habitName,
|
|
601
|
+
habitTitle,
|
|
602
602
|
habitBody,
|
|
603
|
+
lastRun: habitLastRun,
|
|
604
|
+
checkpoint: displayCheckpoint(state.checkpoint),
|
|
605
|
+
alsoDue: alsoDue || undefined,
|
|
606
|
+
staleObligations,
|
|
607
|
+
parseErrors: options?.parseErrors ?? [],
|
|
608
|
+
degradedComponents,
|
|
609
|
+
now,
|
|
603
610
|
});
|
|
604
|
-
// Build also-due line
|
|
605
|
-
const alsoDue = buildAlsoDueLine(agentRoot, habitName, now);
|
|
606
|
-
userContent = alsoDue ? `${heartbeatContent}\n\n${alsoDue}` : heartbeatContent;
|
|
607
|
-
// Parse error nudge
|
|
608
|
-
const parseErrorNudge = buildParseErrorNudge(options?.parseErrors ?? []);
|
|
609
|
-
if (parseErrorNudge)
|
|
610
|
-
userContent = `${userContent}\n\n${parseErrorNudge}`;
|
|
611
611
|
// Piggyback journal embedding indexing (best-effort, fire-and-forget)
|
|
612
|
+
const journalDir = path.join(agentRoot, "journal");
|
|
612
613
|
/* v8 ignore start -- journal indexing piggyback: embedding provider may not be available; tested via journal-index unit tests @preserve */
|
|
613
614
|
void (0, journal_index_1.indexJournalFiles)(journalDir, path.join(journalDir, ".index.json"), {
|
|
614
615
|
embed: async () => [],
|
|
615
616
|
}).catch(() => {
|
|
616
|
-
// swallowed: indexing failure must never block
|
|
617
|
+
// swallowed: indexing failure must never block habit turn
|
|
617
618
|
});
|
|
618
619
|
/* v8 ignore stop */
|
|
619
620
|
}
|
|
620
|
-
else if (habitBody !== undefined) {
|
|
621
|
-
// Non-heartbeat habit: basic context + body
|
|
622
|
-
const sections = [];
|
|
623
|
-
// Elapsed time since last run
|
|
624
|
-
if (habitLastRun) {
|
|
625
|
-
const lastRunMs = new Date(habitLastRun).getTime();
|
|
626
|
-
const elapsed = now().getTime() - lastRunMs;
|
|
627
|
-
const minutes = Math.floor(elapsed / 60000);
|
|
628
|
-
if (minutes < 60) {
|
|
629
|
-
sections.push(`it's been ${minutes} ${minutes === 1 ? "minute" : "minutes"} since ${habitName} last ran.`);
|
|
630
|
-
}
|
|
631
|
-
else {
|
|
632
|
-
const hours = Math.floor(minutes / 60);
|
|
633
|
-
sections.push(`it's been ${hours} ${hours === 1 ? "hour" : "hours"} since ${habitName} last ran.`);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
sections.push(`## habit: ${habitName}\n${habitBody}`);
|
|
637
|
-
// Also due line
|
|
638
|
-
const alsoDue = buildAlsoDueLine(agentRoot, habitName, now);
|
|
639
|
-
if (alsoDue)
|
|
640
|
-
sections.push(alsoDue);
|
|
641
|
-
// Parse error nudge
|
|
642
|
-
const parseErrorNudge = buildParseErrorNudge(options?.parseErrors ?? []);
|
|
643
|
-
if (parseErrorNudge)
|
|
644
|
-
sections.push(parseErrorNudge);
|
|
645
|
-
userContent = sections.join("\n\n");
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
// Habit file not found
|
|
649
|
-
userContent = `habit "${habitName}" could not be read (file not found or unreadable). check habits/${habitName}.md exists.`;
|
|
650
|
-
}
|
|
651
|
-
// Degraded state nudge (best-effort: never crash on health file read failure)
|
|
652
|
-
try {
|
|
653
|
-
const health = (0, daemon_health_1.readHealth)((0, daemon_health_1.getDefaultHealthPath)());
|
|
654
|
-
if (health && health.degraded.length > 0) {
|
|
655
|
-
const reasons = health.degraded.map((d) => `${d.component}: ${d.reason}`).join("; ");
|
|
656
|
-
userContent = `${userContent}\n\n[note: my scheduling is degraded: ${reasons}]`;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
catch {
|
|
660
|
-
// Best-effort: missing file or parse error -> no nudge, no crash
|
|
661
|
-
}
|
|
662
621
|
}
|
|
663
622
|
else {
|
|
664
623
|
userContent = buildInstinctUserMessage(instincts, reason, state);
|
package/dist/senses/pipeline.js
CHANGED
package/package.json
CHANGED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildContextualHeartbeat = buildContextualHeartbeat;
|
|
4
|
-
const runtime_1 = require("../nerves/runtime");
|
|
5
|
-
const STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
|
|
6
|
-
const MAX_JOURNAL_FILES = 10;
|
|
7
|
-
function formatElapsed(ms) {
|
|
8
|
-
const minutes = Math.floor(ms / 60000);
|
|
9
|
-
if (minutes < 60) {
|
|
10
|
-
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
|
|
11
|
-
}
|
|
12
|
-
const hours = Math.floor(minutes / 60);
|
|
13
|
-
return `${hours} ${hours === 1 ? "hour" : "hours"}`;
|
|
14
|
-
}
|
|
15
|
-
function formatRelativeTime(nowMs, mtimeMs) {
|
|
16
|
-
const diffMs = nowMs - mtimeMs;
|
|
17
|
-
const minutes = Math.floor(diffMs / 60000);
|
|
18
|
-
if (minutes < 1)
|
|
19
|
-
return "just now";
|
|
20
|
-
if (minutes < 60)
|
|
21
|
-
return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`;
|
|
22
|
-
const hours = Math.floor(minutes / 60);
|
|
23
|
-
if (hours < 24)
|
|
24
|
-
return `${hours} ${hours === 1 ? "hour" : "hours"} ago`;
|
|
25
|
-
const days = Math.floor(hours / 24);
|
|
26
|
-
return `${days} ${days === 1 ? "day" : "days"} ago`;
|
|
27
|
-
}
|
|
28
|
-
function buildContextualHeartbeat(options) {
|
|
29
|
-
const { lastCompletedAt, pendingObligations, lastSurfaceAt, checkpoint, now, readJournalDir, habitBody, } = options;
|
|
30
|
-
const nowDate = now();
|
|
31
|
-
const nowMs = nowDate.getTime();
|
|
32
|
-
const journalFiles = readJournalDir();
|
|
33
|
-
// Cold start: no journal files and no runtime state
|
|
34
|
-
if (journalFiles.length === 0 && !lastCompletedAt) {
|
|
35
|
-
const lines = ["...time passing. anything stirring?"];
|
|
36
|
-
if (checkpoint) {
|
|
37
|
-
lines.push(`\nlast i remember: ${checkpoint}`);
|
|
38
|
-
}
|
|
39
|
-
(0, runtime_1.emitNervesEvent)({
|
|
40
|
-
component: "senses",
|
|
41
|
-
event: "senses.contextual_heartbeat_built",
|
|
42
|
-
message: "contextual heartbeat built (cold start)",
|
|
43
|
-
meta: { coldStart: true },
|
|
44
|
-
});
|
|
45
|
-
return lines.join("\n");
|
|
46
|
-
}
|
|
47
|
-
const sections = [];
|
|
48
|
-
// Elapsed time since last turn
|
|
49
|
-
if (lastCompletedAt) {
|
|
50
|
-
const lastMs = new Date(lastCompletedAt).getTime();
|
|
51
|
-
const elapsed = nowMs - lastMs;
|
|
52
|
-
sections.push(`it's been ${formatElapsed(elapsed)} since your last turn.`);
|
|
53
|
-
}
|
|
54
|
-
// Habit body (instructions from habit file)
|
|
55
|
-
if (habitBody) {
|
|
56
|
-
sections.push(habitBody);
|
|
57
|
-
}
|
|
58
|
-
// Pending attention count
|
|
59
|
-
if (pendingObligations.length > 0) {
|
|
60
|
-
const count = pendingObligations.length;
|
|
61
|
-
sections.push(`you're holding ${count} ${count === 1 ? "thought" : "thoughts"}.`);
|
|
62
|
-
}
|
|
63
|
-
// Journal index
|
|
64
|
-
if (journalFiles.length > 0) {
|
|
65
|
-
const sorted = [...journalFiles].sort((a, b) => b.mtime - a.mtime).slice(0, MAX_JOURNAL_FILES);
|
|
66
|
-
const indexLines = ["## journal"];
|
|
67
|
-
for (const file of sorted) {
|
|
68
|
-
const ago = formatRelativeTime(nowMs, file.mtime);
|
|
69
|
-
const previewClause = file.preview ? ` — ${file.preview}` : "";
|
|
70
|
-
indexLines.push(`- ${file.name} (${ago})${previewClause}`);
|
|
71
|
-
}
|
|
72
|
-
sections.push(indexLines.join("\n"));
|
|
73
|
-
}
|
|
74
|
-
// Journal entries since last surface
|
|
75
|
-
if (lastSurfaceAt && journalFiles.length > 0) {
|
|
76
|
-
const surfaceMs = new Date(lastSurfaceAt).getTime();
|
|
77
|
-
const entriesSinceSurface = journalFiles.filter((f) => f.mtime > surfaceMs).length;
|
|
78
|
-
if (entriesSinceSurface > 0) {
|
|
79
|
-
const surfaceElapsed = formatElapsed(nowMs - surfaceMs);
|
|
80
|
-
sections.push(`${entriesSinceSurface} journal ${entriesSinceSurface === 1 ? "entry" : "entries"} since you last surfaced, ${surfaceElapsed} ago.`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// Stale obligation alerts
|
|
84
|
-
const staleObligations = pendingObligations.filter((o) => o.staleness >= STALE_THRESHOLD_MS);
|
|
85
|
-
if (staleObligations.length > 0) {
|
|
86
|
-
for (const obligation of staleObligations) {
|
|
87
|
-
sections.push(`this has been sitting for ${formatElapsed(obligation.staleness)}: ${obligation.content}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
// Checkpoint
|
|
91
|
-
if (checkpoint) {
|
|
92
|
-
sections.push(`last i remember: ${checkpoint}`);
|
|
93
|
-
}
|
|
94
|
-
(0, runtime_1.emitNervesEvent)({
|
|
95
|
-
component: "senses",
|
|
96
|
-
event: "senses.contextual_heartbeat_built",
|
|
97
|
-
message: "contextual heartbeat built",
|
|
98
|
-
meta: {
|
|
99
|
-
hasJournal: journalFiles.length > 0,
|
|
100
|
-
hasLastCompleted: !!lastCompletedAt,
|
|
101
|
-
obligationCount: pendingObligations.length,
|
|
102
|
-
staleCount: staleObligations.length,
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
return sections.join("\n\n");
|
|
106
|
-
}
|