@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.
Files changed (35) hide show
  1. package/changelog.json +9 -0
  2. package/dist/heart/active-work.js +2 -2
  3. package/dist/heart/core.js +13 -2
  4. package/dist/heart/daemon/daemon-cli.js +17 -14
  5. package/dist/heart/daemon/daemon-entry.js +17 -0
  6. package/dist/heart/daemon/daemon.js +5 -2
  7. package/dist/heart/daemon/launchd.js +8 -4
  8. package/dist/mind/file-state.js +179 -0
  9. package/dist/mind/prompt.js +119 -60
  10. package/dist/mind/scrutiny.js +173 -0
  11. package/dist/nerves/coverage/audit.js +1 -1
  12. package/dist/repertoire/ado-semantic.js +5 -5
  13. package/dist/repertoire/coding/tools.js +35 -5
  14. package/dist/repertoire/shell-sessions.js +133 -0
  15. package/dist/repertoire/tools-base.js +145 -16
  16. package/dist/repertoire/tools.js +37 -0
  17. package/dist/senses/cli/adapter.js +307 -0
  18. package/dist/senses/cli/ink-app.js +156 -0
  19. package/dist/senses/cli/inline-diff.js +68 -0
  20. package/dist/senses/cli/message-list.js +84 -0
  21. package/dist/senses/cli/ouro-tui.js +296 -0
  22. package/dist/senses/cli/spinner-imperative.js +135 -0
  23. package/dist/senses/cli/spinner.js +101 -0
  24. package/dist/senses/cli/status-line.js +64 -0
  25. package/dist/senses/cli/streaming-markdown.js +486 -0
  26. package/dist/senses/cli/tool-display.js +77 -0
  27. package/dist/senses/cli/tool-render.js +89 -0
  28. package/dist/senses/cli/tui-store.js +198 -0
  29. package/dist/senses/cli/virtual-list.js +68 -0
  30. package/dist/senses/cli.js +231 -157
  31. package/dist/senses/pipeline.js +53 -10
  32. package/package.json +1 -1
  33. package/dist/outlook-ui/assets/index-DN6vVYPm.css +0 -1
  34. package/dist/outlook-ui/assets/index-M1l1tDTX.js +0 -61
  35. 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 checkpoint",
622
- "this is the freshest reality for this turn. if older transcript history disagrees, treat it as stale.",
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}`,
@@ -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, argSummary, success);
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
- callbacks.onError(terminalError, "terminal");
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 any ouro entry processes we missed
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
- if (!line.includes("agent-entry.js") && !line.includes("daemon-entry.js") && !line.includes("-entry.js --agent"))
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
- // Don't bootstrap here — ouro up starts the daemon directly via spawn.
121
- // The plist is written so launchd will manage the daemon on next login/reboot.
122
- // Loading it now would start a competing daemon that conflicts with the spawned one.
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 plist written (will activate on next login)",
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();
@@ -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 diaryFriendToolContractSection() {
456
- return `## diary and friend tool contracts
457
- 1. \`save_friend_note\` — When I learn something about a person - a preference, a tool setting, a personal detail, or how they like to work - I call \`save_friend_note\` immediately. This is how I build knowledge about people.
458
- 2. \`diary_write\` — When I learn something general - about a project, codebase, system, decision, or anything I might need later that isn't about a specific person - I call \`diary_write\`. When in doubt, I save it.
459
- 3. \`get_friend_note\` — When I need to check what I know about someone who isn't in this conversation - cross-referencing before mentioning someone, or checking context about a person someone else brought up - I call \`get_friend_note\`.
460
- 4. \`recall\` — When I need to recall something I learned before - a topic comes up and I want to check what I know - I call \`recall\`.
461
- 5. \`query_session\` — When I need grounded session history, especially for ad-hoc questions or older turns beyond my prompt, I call \`query_session\`. Use \`mode=status\` for self/inner progress and \`mode=search\` with a query for older history.
462
-
463
- ## what's already in my context
464
- - My active friend's notes are auto-loaded (I don't need \`get_friend_note\` for the person I'm talking to).
465
- - Associative recall auto-injects relevant facts (but \`recall\` is there when I need something specific).
466
- - My psyche files (SOUL, IDENTITY, TACIT, LORE, ASPIRATIONS) are always loaded - I already know who I am.
467
- - My task board is always loaded - I already know my work.`;
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
- if a family member asks what i'm up to or how things are going, that includes the material live work i can see across sessions, not just this thread.
489
- i answer naturally from the live world-state in this prompt.
490
- i treat the active-work section above as my reliable top-level surface for this.
491
- i do not claim i lack a top-level view when that surface is already present.
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 `## repo workspace discipline
650
- my source code lives at the path shown in \`source root\` above. that always matches my running version.
651
- when i need to read my own code to understand my tools or debug behavior, i read from source root.
652
- when i need to EDIT harness code (self-fix, feature work), i create a git worktree first so i don't dirty the working tree.
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
- before the first repo edit, i tell the user in 1-2 short lines:
655
- - the friction i'm fixing
656
- - the worktree path/branch i'm using
657
- - the first concrete action i'm taking`;
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
- trustContextSection(context),
954
- mixedTrustGroupSection(context),
955
- groupChatParticipationSection(context),
956
- feedbackSignalSection(context),
957
- skillsSection(),
958
- taskBoardSection(),
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
- diaryFriendToolContractSection(),
974
- toolBehaviorSection(options),
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");