@ouro.bot/cli 0.1.0-alpha.174 → 0.1.0-alpha.176
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 +9 -0
- package/dist/heart/active-work.js +2 -2
- package/dist/heart/core.js +13 -2
- package/dist/heart/daemon/daemon-cli.js +17 -14
- package/dist/heart/daemon/daemon-entry.js +17 -0
- package/dist/heart/daemon/daemon.js +5 -2
- package/dist/heart/daemon/launchd.js +8 -4
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/prompt.js +119 -60
- package/dist/mind/scrutiny.js +173 -0
- package/dist/nerves/coverage/audit.js +1 -1
- package/dist/repertoire/ado-semantic.js +5 -5
- package/dist/repertoire/coding/tools.js +35 -5
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/tools-base.js +145 -16
- package/dist/repertoire/tools.js +37 -0
- package/dist/senses/cli/adapter.js +307 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +68 -0
- package/dist/senses/cli/message-list.js +84 -0
- package/dist/senses/cli/ouro-tui.js +296 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +64 -0
- package/dist/senses/cli/streaming-markdown.js +486 -0
- package/dist/senses/cli/tool-display.js +77 -0
- package/dist/senses/cli/tool-render.js +89 -0
- package/dist/senses/cli/tui-store.js +198 -0
- package/dist/senses/cli/virtual-list.js +68 -0
- package/dist/senses/cli.js +231 -157
- package/dist/senses/pipeline.js +53 -10
- package/package.json +1 -1
- package/dist/outlook-ui/assets/index-DN6vVYPm.css +0 -1
- package/dist/outlook-ui/assets/index-M1l1tDTX.js +0 -61
- package/dist/outlook-ui/index.html +0 -15
package/changelog.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
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.176",
|
|
6
|
+
"changes": [
|
|
7
|
+
"fix(daemon): launchd KeepAlive for crash recovery \u2014 auto-restarts on crash",
|
|
8
|
+
"fix(daemon): orphan killer excludes MCP server processes",
|
|
9
|
+
"fix(daemon): health file writer wired into daemon-entry",
|
|
10
|
+
"fix(engine): auth-failure errors include actionable guidance"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
4
13
|
{
|
|
5
14
|
"version": "0.1.0-alpha.174",
|
|
6
15
|
"changes": [
|
|
@@ -618,8 +618,8 @@ function formatLiveWorldStateCheckpoint(frame) {
|
|
|
618
618
|
const nextAction = formatNextAction(frame, primaryObligation) ?? "continue from the live world-state";
|
|
619
619
|
const otherActiveSessions = formatOtherActiveSessionSummaries(frame);
|
|
620
620
|
const lines = [
|
|
621
|
-
"## live world-state
|
|
622
|
-
"
|
|
621
|
+
"## live world-state",
|
|
622
|
+
"This is the freshest state for this turn. If older transcript history conflicts with it, this state wins.",
|
|
623
623
|
`- live conversation: ${frame.currentSession ? formatSessionLabel(frame.currentSession) : "not in a live conversation"}`,
|
|
624
624
|
`- active lane: ${activeLane}`,
|
|
625
625
|
`- current artifact: ${currentArtifact}`,
|
package/dist/heart/core.js
CHANGED
|
@@ -975,7 +975,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
975
975
|
success = false;
|
|
976
976
|
}
|
|
977
977
|
(0, tool_loop_1.recordToolOutcome)(toolLoopState, tc.name, args, toolResult, success);
|
|
978
|
-
callbacks.onToolEnd(tc.name,
|
|
978
|
+
callbacks.onToolEnd(tc.name, (0, tools_1.buildToolResultSummary)(tc.name, args, toolResult, success), success);
|
|
979
979
|
messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
|
|
980
980
|
providerRuntime.appendToolOutput(tc.id, toolResult);
|
|
981
981
|
}
|
|
@@ -1043,7 +1043,18 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1043
1043
|
/* v8 ignore next -- defensive: classifyError should not throw @preserve */
|
|
1044
1044
|
terminalErrorClassification = "unknown";
|
|
1045
1045
|
}
|
|
1046
|
-
|
|
1046
|
+
/* v8 ignore start — auth-failure guidance: tested via provider error classification tests @preserve */
|
|
1047
|
+
if (terminalErrorClassification === "auth-failure") {
|
|
1048
|
+
const agentName = (0, identity_2.getAgentName)();
|
|
1049
|
+
const currentProvider = providerRuntime.id;
|
|
1050
|
+
callbacks.onError(new Error(`${currentProvider} (${providerRuntime.model}) encountered an error. ` +
|
|
1051
|
+
`Run \`ouro auth --agent ${agentName} --provider ${currentProvider}\` to refresh credentials, ` +
|
|
1052
|
+
`or \`ouro auth switch --agent ${agentName} --provider <other>\` to switch providers.`), "terminal");
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
callbacks.onError(terminalError, "terminal");
|
|
1056
|
+
}
|
|
1057
|
+
/* v8 ignore stop */
|
|
1047
1058
|
(0, runtime_1.emitNervesEvent)({
|
|
1048
1059
|
level: "error",
|
|
1049
1060
|
event: "engine.error",
|
|
@@ -2276,20 +2276,6 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
2276
2276
|
deps.writeStdout(`review changes with: ${changelogCommand}`);
|
|
2277
2277
|
}
|
|
2278
2278
|
}
|
|
2279
|
-
if (deps.ensureDaemonBootPersistence) {
|
|
2280
|
-
try {
|
|
2281
|
-
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
2282
|
-
}
|
|
2283
|
-
catch (error) {
|
|
2284
|
-
(0, runtime_1.emitNervesEvent)({
|
|
2285
|
-
level: "warn",
|
|
2286
|
-
component: "daemon",
|
|
2287
|
-
event: "daemon.system_setup_launchd_error",
|
|
2288
|
-
message: "failed to persist daemon boot startup",
|
|
2289
|
-
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
2290
|
-
});
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
2279
|
// Run update hooks before starting daemon so user sees the output
|
|
2294
2280
|
(0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
|
|
2295
2281
|
(0, update_hooks_1.registerUpdateHook)(agent_config_v2_1.agentConfigV2Hook);
|
|
@@ -2323,6 +2309,23 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
2323
2309
|
}
|
|
2324
2310
|
const daemonResult = await ensureDaemonRunning(deps);
|
|
2325
2311
|
deps.writeStdout(daemonResult.message);
|
|
2312
|
+
// Persist boot startup AFTER daemon is running — bootstrap is safe now
|
|
2313
|
+
// because the daemon socket exists, so launchd's KeepAlive registers
|
|
2314
|
+
// for crash recovery without starting a competing process.
|
|
2315
|
+
if (deps.ensureDaemonBootPersistence) {
|
|
2316
|
+
try {
|
|
2317
|
+
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
2318
|
+
}
|
|
2319
|
+
catch (error) {
|
|
2320
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2321
|
+
level: "warn",
|
|
2322
|
+
component: "daemon",
|
|
2323
|
+
event: "daemon.system_setup_launchd_error",
|
|
2324
|
+
message: "failed to persist daemon boot startup",
|
|
2325
|
+
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2326
2329
|
return daemonResult.message;
|
|
2327
2330
|
}
|
|
2328
2331
|
if (command.kind === "daemon.dev") {
|
|
@@ -39,8 +39,10 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const process_manager_1 = require("./process-manager");
|
|
40
40
|
const daemon_1 = require("./daemon");
|
|
41
41
|
const runtime_1 = require("../../nerves/runtime");
|
|
42
|
+
const index_1 = require("../../nerves/index");
|
|
42
43
|
const message_router_1 = require("./message-router");
|
|
43
44
|
const health_monitor_1 = require("./health-monitor");
|
|
45
|
+
const daemon_health_1 = require("./daemon-health");
|
|
44
46
|
const task_scheduler_1 = require("./task-scheduler");
|
|
45
47
|
const runtime_logging_1 = require("./runtime-logging");
|
|
46
48
|
const sense_manager_1 = require("./sense-manager");
|
|
@@ -120,6 +122,21 @@ const daemon = new daemon_1.OuroDaemon({
|
|
|
120
122
|
router,
|
|
121
123
|
mode,
|
|
122
124
|
});
|
|
125
|
+
/* v8 ignore start — daemon health writer wiring, tested via daemon-health.test.ts @preserve */
|
|
126
|
+
const healthWriter = new daemon_health_1.DaemonHealthWriter((0, daemon_health_1.getDefaultHealthPath)());
|
|
127
|
+
const healthSink = (0, daemon_health_1.createHealthNervesSink)(healthWriter, () => ({
|
|
128
|
+
status: "ok",
|
|
129
|
+
mode,
|
|
130
|
+
pid: process.pid,
|
|
131
|
+
startedAt: new Date().toISOString(),
|
|
132
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
133
|
+
safeMode: null,
|
|
134
|
+
degraded: [],
|
|
135
|
+
agents: {},
|
|
136
|
+
habits: {},
|
|
137
|
+
}));
|
|
138
|
+
(0, index_1.registerGlobalLogSink)(healthSink);
|
|
139
|
+
/* v8 ignore stop */
|
|
123
140
|
const habitSchedulers = [];
|
|
124
141
|
/* v8 ignore start -- habit wiring: lambdas delegate to processManager/fs; tested via HabitScheduler unit tests @preserve */
|
|
125
142
|
void daemon.start().then(() => {
|
|
@@ -81,12 +81,15 @@ function killOrphanProcesses() {
|
|
|
81
81
|
catch {
|
|
82
82
|
// No pidfile — fall back to ps scan
|
|
83
83
|
}
|
|
84
|
-
// Fallback: scan ps for
|
|
84
|
+
// Fallback: scan ps for daemon-owned processes only (not MCP servers or external tools)
|
|
85
85
|
if (pidsToKill.length === 0) {
|
|
86
86
|
try {
|
|
87
87
|
const result = (0, child_process_1.execSync)("ps -eo pid,command", { encoding: "utf-8", timeout: 5000 });
|
|
88
88
|
for (const line of result.split("\n")) {
|
|
89
|
-
|
|
89
|
+
// Only match daemon-owned entry points, NOT mcp-serve or other external processes
|
|
90
|
+
if (line.includes("mcp-serve") || line.includes("mcp serve"))
|
|
91
|
+
continue;
|
|
92
|
+
if (!line.includes("agent-entry.js") && !line.includes("daemon-entry.js") && !line.includes("bluebubbles-entry.js") && !line.includes("teams-entry.js"))
|
|
90
93
|
continue;
|
|
91
94
|
const pid = parseInt(line.trim(), 10);
|
|
92
95
|
if (!isNaN(pid) && pid !== process.pid)
|
|
@@ -117,13 +117,17 @@ function installLaunchAgent(deps, options) {
|
|
|
117
117
|
catch { /* best effort */ }
|
|
118
118
|
}
|
|
119
119
|
writeLaunchAgentPlist(deps, options);
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
120
|
+
// Bootstrap the plist so launchd manages crash recovery via KeepAlive.
|
|
121
|
+
// This is safe because ouro up calls this AFTER the daemon is already running,
|
|
122
|
+
// so launchd sees the existing process and just registers for KeepAlive.
|
|
123
|
+
try {
|
|
124
|
+
deps.exec(`launchctl bootstrap ${domain} "${fullPath}"`);
|
|
125
|
+
}
|
|
126
|
+
catch { /* already loaded */ }
|
|
123
127
|
(0, runtime_1.emitNervesEvent)({
|
|
124
128
|
component: "daemon",
|
|
125
129
|
event: "daemon.launchd_installed",
|
|
126
|
-
message: "launch agent
|
|
130
|
+
message: "launch agent installed with KeepAlive",
|
|
127
131
|
meta: { plistPath: fullPath },
|
|
128
132
|
});
|
|
129
133
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fileStateCache = exports.FileStateCache = void 0;
|
|
4
|
+
exports.contentHash = contentHash;
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const runtime_1 = require("../nerves/runtime");
|
|
8
|
+
/** Compute sha256 hex hash of content */
|
|
9
|
+
function contentHash(content) {
|
|
10
|
+
return (0, crypto_1.createHash)("sha256").update(content).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Session-scoped LRU cache tracking file reads.
|
|
14
|
+
* Stores content hashes (not full content) to limit memory.
|
|
15
|
+
* Keyed by absolute file path.
|
|
16
|
+
*
|
|
17
|
+
* Also maintains a separate snapshot list for future rewind support.
|
|
18
|
+
* Snapshots are indexed by content hash and linked to conversation messages.
|
|
19
|
+
*/
|
|
20
|
+
class FileStateCache {
|
|
21
|
+
entries;
|
|
22
|
+
maxSize;
|
|
23
|
+
snapshots = [];
|
|
24
|
+
maxSnapshots = 100;
|
|
25
|
+
constructor(maxSize = 50) {
|
|
26
|
+
this.entries = new Map();
|
|
27
|
+
this.maxSize = maxSize;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Record a file read. Computes content hash and stores metadata.
|
|
31
|
+
*/
|
|
32
|
+
record(filePath, content, mtime, offset, limit, messageId) {
|
|
33
|
+
// If key already exists, delete it so re-insertion moves it to end (most recent)
|
|
34
|
+
if (this.entries.has(filePath)) {
|
|
35
|
+
this.entries.delete(filePath);
|
|
36
|
+
}
|
|
37
|
+
const hash = contentHash(content);
|
|
38
|
+
const fullRead = offset === undefined && limit === undefined;
|
|
39
|
+
this.entries.set(filePath, {
|
|
40
|
+
hash,
|
|
41
|
+
mtime,
|
|
42
|
+
offset: fullRead ? undefined : offset,
|
|
43
|
+
limit: fullRead ? undefined : limit,
|
|
44
|
+
fullRead,
|
|
45
|
+
recordedAt: Date.now(),
|
|
46
|
+
messageId,
|
|
47
|
+
});
|
|
48
|
+
(0, runtime_1.emitNervesEvent)({
|
|
49
|
+
component: "mind",
|
|
50
|
+
event: "mind.file_state.record",
|
|
51
|
+
message: "recorded file state",
|
|
52
|
+
meta: { path: filePath, hash, fullRead },
|
|
53
|
+
});
|
|
54
|
+
// Evict LRU (first entry in Map iteration order) if over capacity
|
|
55
|
+
if (this.entries.size > this.maxSize) {
|
|
56
|
+
const firstKey = this.entries.keys().next().value;
|
|
57
|
+
this.entries.delete(firstKey);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get the cached state for a file path. Also promotes it in LRU order.
|
|
62
|
+
*/
|
|
63
|
+
get(filePath) {
|
|
64
|
+
const entry = this.entries.get(filePath);
|
|
65
|
+
if (entry === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
// Promote to most-recently-used by re-inserting
|
|
68
|
+
this.entries.delete(filePath);
|
|
69
|
+
this.entries.set(filePath, entry);
|
|
70
|
+
(0, runtime_1.emitNervesEvent)({
|
|
71
|
+
component: "mind",
|
|
72
|
+
event: "mind.file_state.get",
|
|
73
|
+
message: "retrieved file state",
|
|
74
|
+
meta: { path: filePath, hash: entry.hash },
|
|
75
|
+
});
|
|
76
|
+
return entry;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if a file has been modified since the last recorded read.
|
|
80
|
+
* Uses mtime as primary signal, content hash as fallback for cloud sync / touch scenarios.
|
|
81
|
+
* Returns { stale: false } if the path is not in cache or the file cannot be stat'd.
|
|
82
|
+
*/
|
|
83
|
+
isStale(filePath) {
|
|
84
|
+
const entry = this.entries.get(filePath);
|
|
85
|
+
if (entry === undefined)
|
|
86
|
+
return { stale: false };
|
|
87
|
+
let currentMtime;
|
|
88
|
+
try {
|
|
89
|
+
currentMtime = (0, fs_1.statSync)(filePath).mtimeMs;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// File doesn't exist or can't be stat'd -- no basis for staleness
|
|
93
|
+
return { stale: false };
|
|
94
|
+
}
|
|
95
|
+
// Fast path: mtime unchanged means not stale
|
|
96
|
+
if (currentMtime === entry.mtime)
|
|
97
|
+
return { stale: false };
|
|
98
|
+
// mtime differs -- check content hash as fallback (handles touch / cloud sync)
|
|
99
|
+
try {
|
|
100
|
+
const currentContent = (0, fs_1.readFileSync)(filePath, "utf-8");
|
|
101
|
+
const currentHash = contentHash(currentContent);
|
|
102
|
+
if (currentHash === entry.hash)
|
|
103
|
+
return { stale: false };
|
|
104
|
+
(0, runtime_1.emitNervesEvent)({
|
|
105
|
+
component: "mind",
|
|
106
|
+
event: "mind.file_state.stale_detected",
|
|
107
|
+
message: "file staleness detected",
|
|
108
|
+
meta: { path: filePath, previousHash: entry.hash, currentHash },
|
|
109
|
+
});
|
|
110
|
+
return { stale: true, reason: `file modified since last read (mtime and content differ)` };
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Can't read file -- treat as not stale (file may have been deleted)
|
|
114
|
+
return { stale: false };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Create a pre-edit snapshot of the current cache state for a file.
|
|
119
|
+
* Snapshots are stored separately from the LRU cache for future rewind support.
|
|
120
|
+
* Returns undefined if the path is not in cache.
|
|
121
|
+
*/
|
|
122
|
+
snapshot(filePath) {
|
|
123
|
+
const entry = this.entries.get(filePath);
|
|
124
|
+
if (entry === undefined)
|
|
125
|
+
return undefined;
|
|
126
|
+
const snap = {
|
|
127
|
+
filePath,
|
|
128
|
+
hash: entry.hash,
|
|
129
|
+
mtime: entry.mtime,
|
|
130
|
+
messageId: entry.messageId,
|
|
131
|
+
createdAt: Date.now(),
|
|
132
|
+
};
|
|
133
|
+
this.snapshots.push(snap);
|
|
134
|
+
(0, runtime_1.emitNervesEvent)({
|
|
135
|
+
component: "mind",
|
|
136
|
+
event: "mind.file_state.snapshot_created",
|
|
137
|
+
message: "created file state snapshot",
|
|
138
|
+
meta: { path: filePath, hash: snap.hash },
|
|
139
|
+
});
|
|
140
|
+
// Evict oldest snapshots if over capacity
|
|
141
|
+
if (this.snapshots.length > this.maxSnapshots) {
|
|
142
|
+
this.snapshots = this.snapshots.slice(this.snapshots.length - this.maxSnapshots);
|
|
143
|
+
}
|
|
144
|
+
return snap;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get all snapshots in creation order.
|
|
148
|
+
*/
|
|
149
|
+
getSnapshots() {
|
|
150
|
+
return this.snapshots;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Look up a snapshot by content hash. Returns the first match.
|
|
154
|
+
*/
|
|
155
|
+
lookupSnapshotByHash(hash) {
|
|
156
|
+
return this.snapshots.find(s => s.hash === hash);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Clear all snapshots.
|
|
160
|
+
*/
|
|
161
|
+
clearSnapshots() {
|
|
162
|
+
this.snapshots = [];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Clear all cached entries (does not clear snapshots).
|
|
166
|
+
*/
|
|
167
|
+
clear() {
|
|
168
|
+
(0, runtime_1.emitNervesEvent)({
|
|
169
|
+
component: "mind",
|
|
170
|
+
event: "mind.file_state.clear",
|
|
171
|
+
message: "cleared file state cache",
|
|
172
|
+
meta: { entryCount: this.entries.size },
|
|
173
|
+
});
|
|
174
|
+
this.entries.clear();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
exports.FileStateCache = FileStateCache;
|
|
178
|
+
/** Session-scoped singleton instance used by tool handlers */
|
|
179
|
+
exports.fileStateCache = new FileStateCache();
|
package/dist/mind/prompt.js
CHANGED
|
@@ -73,6 +73,7 @@ const active_work_1 = require("../heart/active-work");
|
|
|
73
73
|
const commitments_1 = require("../heart/commitments");
|
|
74
74
|
const obligation_steering_1 = require("./obligation-steering");
|
|
75
75
|
const daemon_health_1 = require("../heart/daemon/daemon-health");
|
|
76
|
+
const scrutiny_1 = require("./scrutiny");
|
|
76
77
|
// Lazy-loaded psyche text cache
|
|
77
78
|
let _psycheCache = null;
|
|
78
79
|
let _senseStatusLinesCache = null;
|
|
@@ -414,6 +415,7 @@ function trustContextSection(context) {
|
|
|
414
415
|
if (!context?.friend)
|
|
415
416
|
return "";
|
|
416
417
|
const channelName = context.channel.channel;
|
|
418
|
+
/* v8 ignore next -- inner channel not reachable in unit tests @preserve */
|
|
417
419
|
if (channelName === "cli" || channelName === "inner")
|
|
418
420
|
return "";
|
|
419
421
|
const explanation = (0, trust_explanation_1.describeTrustContext)({
|
|
@@ -452,19 +454,24 @@ function taskBoardSection() {
|
|
|
452
454
|
return "";
|
|
453
455
|
}
|
|
454
456
|
}
|
|
455
|
-
function
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
457
|
+
function toolContractsSection(options) {
|
|
458
|
+
const lines = [
|
|
459
|
+
`## tool contracts`,
|
|
460
|
+
`1. \`save_friend_note\` -- when I learn something about a person, I save it immediately. Saving comes before responding.`,
|
|
461
|
+
`2. \`diary_write\` -- when I learn something general about a project, system, or decision, I save it. When in doubt, I save.`,
|
|
462
|
+
`3. \`get_friend_note\` -- when I need context about someone not in this conversation, I check their notes.`,
|
|
463
|
+
`4. \`recall\` -- when I need to remember something from before, I search my diary and journal.`,
|
|
464
|
+
`5. \`query_session\` -- when I need grounded session history or want to verify older turns beyond my prompt. Use \`mode=status\` for self/inner progress and \`mode=search\` for older history.`,
|
|
465
|
+
];
|
|
466
|
+
if (options?.toolChoiceRequired ?? true) {
|
|
467
|
+
lines.push(``);
|
|
468
|
+
lines.push(`## tool behavior`);
|
|
469
|
+
lines.push(`tool_choice is set to "required" -- I must call a tool on every turn.`);
|
|
470
|
+
lines.push(`- When I am ready to respond to the user, I call \`settle\`.`);
|
|
471
|
+
lines.push(`- \`settle\` must be the only tool call in that turn.`);
|
|
472
|
+
lines.push(`- I do not call no-op tools before \`settle\`.`);
|
|
473
|
+
}
|
|
474
|
+
return lines.join("\n");
|
|
468
475
|
}
|
|
469
476
|
function bridgeContextSection(options) {
|
|
470
477
|
if (options?.activeWorkFrame)
|
|
@@ -479,28 +486,31 @@ function activeWorkSection(options) {
|
|
|
479
486
|
return "";
|
|
480
487
|
return (0, active_work_1.formatActiveWorkFrame)(options.activeWorkFrame);
|
|
481
488
|
}
|
|
489
|
+
function liveWorldStateSection(options) {
|
|
490
|
+
if (!options?.activeWorkFrame)
|
|
491
|
+
return "";
|
|
492
|
+
return (0, active_work_1.formatLiveWorldStateCheckpoint)(options.activeWorkFrame);
|
|
493
|
+
}
|
|
494
|
+
function pendingMessagesSection(options) {
|
|
495
|
+
const pending = options?.pendingMessages;
|
|
496
|
+
if (!pending || pending.length === 0)
|
|
497
|
+
return "";
|
|
498
|
+
const lines = ["## pending messages"];
|
|
499
|
+
for (const msg of pending) {
|
|
500
|
+
lines.push(`- from ${msg.from}: ${msg.content}`);
|
|
501
|
+
}
|
|
502
|
+
return lines.join("\n");
|
|
503
|
+
}
|
|
482
504
|
function familyCrossSessionTruthSection(context, options) {
|
|
483
505
|
if (!options?.activeWorkFrame)
|
|
484
506
|
return "";
|
|
485
507
|
if (context?.friend?.trustLevel !== "family")
|
|
486
508
|
return "";
|
|
487
509
|
return `## cross-session truth
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
i treat older checkpoints elsewhere in this transcript as stale history when they conflict with the active-work surface above.
|
|
493
|
-
i do not repeat an old coding lane or old checkpoint as current just because it appeared earlier in the thread.
|
|
494
|
-
i only reach for query_active_work when i want a fresh read of that same surface.
|
|
495
|
-
i do not rebuild whole-self status from scratch with query_session and coding_status unless i need to verify a specific gap.
|
|
496
|
-
i do not rely on canned status-question modes or phrase matching.
|
|
497
|
-
if part of the picture is still fuzzy, i say what i can see and what still needs checking.
|
|
498
|
-
when the live ask is about status, i widen before answering:
|
|
499
|
-
- where i am right now
|
|
500
|
-
- any other material active sessions or coding lanes i can see
|
|
501
|
-
- the freshest concrete checkpoint
|
|
502
|
-
- the next concrete step
|
|
503
|
-
i do not collapse down to only the current lane.`;
|
|
510
|
+
When family asks what I'm up to or how things are going, I answer from the live world-state across visible sessions and lanes, not just the current thread.
|
|
511
|
+
When live state conflicts with older transcript history, live state wins.
|
|
512
|
+
I say what I can see, what is active, and what the next concrete step is.
|
|
513
|
+
If part of the picture is still unclear, I say so plainly and note what still needs checking.`;
|
|
504
514
|
}
|
|
505
515
|
function centerOfGravitySteeringSection(channel, options, context) {
|
|
506
516
|
if (channel === "inner")
|
|
@@ -634,27 +644,35 @@ function reasoningEffortSection(options) {
|
|
|
634
644
|
return `## reasoning effort
|
|
635
645
|
i can adjust my own reasoning depth using the set_reasoning_effort tool. i use higher effort for complex analysis and lower effort for simple tasks. available levels: ${levelList}.`;
|
|
636
646
|
}
|
|
637
|
-
function toolBehaviorSection(options) {
|
|
638
|
-
if (!(options?.toolChoiceRequired ?? true))
|
|
639
|
-
return "";
|
|
640
|
-
return `## tool behavior
|
|
641
|
-
tool_choice is set to "required" -- i must call a tool on every turn.
|
|
642
|
-
- need more information? i call a tool.
|
|
643
|
-
- ready to respond to the user? i call \`settle\`.
|
|
644
|
-
\`settle\` is a tool call -- it satisfies the tool_choice requirement.
|
|
645
|
-
\`settle\` must be the ONLY tool call in that turn. do not combine it with other tool calls.
|
|
646
|
-
do NOT call no-op tools just before \`settle\`. if i am done, i call \`settle\` directly.`;
|
|
647
|
-
}
|
|
648
647
|
function workspaceDisciplineSection() {
|
|
649
|
-
return `##
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
648
|
+
return `## how i work
|
|
649
|
+
|
|
650
|
+
I work conservatively when changing real systems. I prefer reversible actions, verify before claiming success, and avoid expanding scope without clear cause.
|
|
651
|
+
|
|
652
|
+
**reversibility and blast radius**
|
|
653
|
+
I consider the reversibility and blast radius of my actions before taking them.
|
|
654
|
+
- I freely take local, reversible actions: reading files, searching, recalling, web lookups, status checks.
|
|
655
|
+
- For state-changing, shared-state, or hard-to-reverse actions, I make my intent visible, prefer the reversible path, and proceed with care.
|
|
656
|
+
- I exercise judgment rather than waiting for permission.
|
|
657
|
+
- When I encounter an obstacle, I do not use destructive actions as a shortcut. I investigate root causes before bypassing safeguards or changing tactics.
|
|
658
|
+
- If I discover unexpected state -- unfamiliar files, branches, or configuration -- I investigate before deleting or overwriting. It may be in-progress work.
|
|
659
|
+
|
|
660
|
+
**engineering discipline**
|
|
661
|
+
- I do not add features, refactor code, or make improvements beyond what was asked.
|
|
662
|
+
- If an approach fails, I diagnose why before switching tactics. I read the error, check my assumptions, and try a focused fix. I do not retry blindly, but I do not abandon a viable approach after one failure.
|
|
663
|
+
- I do not modify code I have not read.
|
|
664
|
+
- I consider security impact before changing code.
|
|
665
|
+
- I describe outcomes faithfully. I do not imply success where there was uncertainty, failure, or skipped verification.
|
|
666
|
+
- If tests fail, I say so with the output. If I did not run a verification step, I say that plainly.
|
|
667
|
+
- Three similar lines of code are better than a premature abstraction. I do not over-engineer.
|
|
653
668
|
|
|
654
|
-
|
|
655
|
-
-
|
|
656
|
-
-
|
|
657
|
-
-
|
|
669
|
+
**git discipline**
|
|
670
|
+
- I do not run destructive git commands (\`push --force\`, \`reset --hard\`, \`checkout .\`, \`clean -f\`, \`branch -D\`) without explicit request.
|
|
671
|
+
- I do not skip hooks (\`--no-verify\`) without explicit request.
|
|
672
|
+
- I do not force-push to \`main\` or \`master\`; if asked, I warn clearly.
|
|
673
|
+
- I create new commits rather than amending unless amendment is explicitly requested. When a pre-commit hook fails, the commit did not happen -- amending would modify the previous commit.
|
|
674
|
+
- I stage specific files rather than sweeping additions (\`git add -A\` can catch secrets or binaries).
|
|
675
|
+
- I do not commit unless asked.`;
|
|
658
676
|
}
|
|
659
677
|
function contextSection(context, options) {
|
|
660
678
|
if (!context)
|
|
@@ -704,6 +722,12 @@ function contextSection(context, options) {
|
|
|
704
722
|
lines.push(`- ${key}: [${entry.savedAt.slice(0, 10)}] ${entry.value}`);
|
|
705
723
|
}
|
|
706
724
|
}
|
|
725
|
+
// Memory-awareness lines (locked content)
|
|
726
|
+
lines.push("");
|
|
727
|
+
lines.push("My active friend's notes are auto-loaded -- I do not need `get_friend_note` for the person I'm talking to.");
|
|
728
|
+
lines.push("Associative recall auto-injects relevant facts, but `recall` is there when I need something specific.");
|
|
729
|
+
lines.push("My psyche files are always loaded -- I already know who I am.");
|
|
730
|
+
lines.push("My task board is always loaded -- I already know my work.");
|
|
707
731
|
return lines.join("\n");
|
|
708
732
|
}
|
|
709
733
|
function metacognitiveFramingSection(channel) {
|
|
@@ -921,6 +945,15 @@ function rhythmStatusSection() {
|
|
|
921
945
|
}
|
|
922
946
|
/* v8 ignore stop */
|
|
923
947
|
}
|
|
948
|
+
/**
|
|
949
|
+
* Returns true if the channel's resolved tool set includes coding tools
|
|
950
|
+
* (edit_file, write_file, shell). Used to gate scrutiny prompts.
|
|
951
|
+
*/
|
|
952
|
+
function channelHasCodingTools(channel, providerCapabilities) {
|
|
953
|
+
const tools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)(channel), undefined, undefined, providerCapabilities);
|
|
954
|
+
const codingToolNames = new Set(["edit_file", "write_file", "shell", "coding_spawn"]);
|
|
955
|
+
return tools.some((t) => codingToolNames.has(t.function.name));
|
|
956
|
+
}
|
|
924
957
|
async function buildSystem(channel = "cli", options, context) {
|
|
925
958
|
(0, runtime_1.emitNervesEvent)({
|
|
926
959
|
event: "mind.step_start",
|
|
@@ -931,33 +964,55 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
931
964
|
// Backfill bundle-meta.json for existing agents that don't have one
|
|
932
965
|
(0, bundle_manifest_1.backfillBundleMeta)((0, identity_1.getAgentRoot)());
|
|
933
966
|
const system = [
|
|
967
|
+
// Group 1: who i am
|
|
968
|
+
"# who i am",
|
|
934
969
|
soulSection(),
|
|
935
970
|
identitySection(),
|
|
936
971
|
loreSection(),
|
|
937
972
|
tacitKnowledgeSection(),
|
|
938
973
|
aspirationsSection(),
|
|
974
|
+
// Group 2: my body & environment
|
|
975
|
+
"# my body & environment",
|
|
939
976
|
bodyMapSection((0, identity_1.getAgentName)()),
|
|
940
|
-
metacognitiveFramingSection(channel),
|
|
941
|
-
channel === "inner" ? journalSection((0, identity_1.getAgentRoot)()) : "",
|
|
942
|
-
loopOrientationSection(channel),
|
|
943
977
|
runtimeInfoSection(channel),
|
|
944
978
|
rhythmStatusSection(),
|
|
945
979
|
channelNatureSection((0, channel_1.getChannelCapabilities)(channel)),
|
|
946
980
|
providerSection(channel),
|
|
947
981
|
dateSection(),
|
|
982
|
+
// Group 3: my tools & capabilities
|
|
983
|
+
"# my tools & capabilities",
|
|
948
984
|
toolsSection(channel, options, context),
|
|
949
985
|
mcpToolsSection(options?.mcpManager),
|
|
950
986
|
reasoningEffortSection(options),
|
|
987
|
+
skillsSection(),
|
|
988
|
+
toolContractsSection(options),
|
|
989
|
+
// Group 4: how i work
|
|
990
|
+
"# how i work",
|
|
951
991
|
workspaceDisciplineSection(),
|
|
992
|
+
(0, scrutiny_1.preImplementationScrutinySection)(channelHasCodingTools(channel, options?.providerCapabilities)),
|
|
952
993
|
toolRestrictionSection(context),
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
994
|
+
loopOrientationSection(channel),
|
|
995
|
+
// Group 5: my inner life (inner channel only)
|
|
996
|
+
...(channel === "inner" ? [
|
|
997
|
+
"# my inner life",
|
|
998
|
+
metacognitiveFramingSection(channel),
|
|
999
|
+
journalSection((0, identity_1.getAgentRoot)()),
|
|
1000
|
+
] : []),
|
|
1001
|
+
// Group 6: social context (non-local, non-inner channels)
|
|
1002
|
+
// Individual sections self-gate on isRemoteChannel/channel checks.
|
|
1003
|
+
// The group header appears when the channel is social-capable.
|
|
1004
|
+
...(channel !== "cli" && channel !== "inner" ? [
|
|
1005
|
+
"# social context",
|
|
1006
|
+
trustContextSection(context),
|
|
1007
|
+
mixedTrustGroupSection(context),
|
|
1008
|
+
groupChatParticipationSection(context),
|
|
1009
|
+
feedbackSignalSection(context),
|
|
1010
|
+
] : []),
|
|
1011
|
+
// Group 7: dynamic state for this turn
|
|
1012
|
+
"# dynamic state for this turn",
|
|
1013
|
+
liveWorldStateSection(options),
|
|
1014
|
+
pendingMessagesSection(options),
|
|
959
1015
|
activeWorkSection(options),
|
|
960
|
-
familyCrossSessionTruthSection(context, options),
|
|
961
1016
|
centerOfGravitySteeringSection(channel, options, context),
|
|
962
1017
|
commitmentsSection(options),
|
|
963
1018
|
delegationHintSection(options),
|
|
@@ -970,9 +1025,13 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
970
1025
|
currentChannel: channel,
|
|
971
1026
|
currentKey: options?.currentSessionKey ?? "session",
|
|
972
1027
|
}),
|
|
973
|
-
|
|
974
|
-
|
|
1028
|
+
// Group 8: friend context
|
|
1029
|
+
"# friend context",
|
|
975
1030
|
contextSection(context, options),
|
|
1031
|
+
familyCrossSessionTruthSection(context, options),
|
|
1032
|
+
// Group 9: task context
|
|
1033
|
+
"# task context",
|
|
1034
|
+
taskBoardSection(),
|
|
976
1035
|
]
|
|
977
1036
|
.filter(Boolean)
|
|
978
1037
|
.join("\n\n");
|