@ouro.bot/cli 0.1.0-alpha.175 → 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 CHANGED
@@ -2,7 +2,7 @@
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
4
  {
5
- "version": "0.1.0-alpha.175",
5
+ "version": "0.1.0-alpha.176",
6
6
  "changes": [
7
7
  "fix(daemon): launchd KeepAlive for crash recovery \u2014 auto-restarts on crash",
8
8
  "fix(daemon): orphan killer excludes MCP server processes",
@@ -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
  }
@@ -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");
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preImplementationScrutinySection = preImplementationScrutinySection;
4
+ exports.trackModifiedFile = trackModifiedFile;
5
+ exports.getModifiedFileCount = getModifiedFileCount;
6
+ exports.resetSessionModifiedFiles = resetSessionModifiedFiles;
7
+ exports.getPostImplementationScrutiny = getPostImplementationScrutiny;
8
+ exports.getCodingCompletionScrutiny = getCodingCompletionScrutiny;
9
+ const runtime_1 = require("../nerves/runtime");
10
+ /**
11
+ * Scrutiny passes: adversarial review prompts injected into the agent's
12
+ * system prompt and tool results during coding work.
13
+ *
14
+ * Two lenses:
15
+ * 1. Stranger With Candy -- paranoid mom at a playground
16
+ * 2. Tinfoil Hat -- conspiracy theorist with a wall of red string
17
+ *
18
+ * All prompts are FIRST PERSON voice. The agent adopts the scrutiny lens
19
+ * as part of its own thinking, not as a separate persona.
20
+ */
21
+ // ---------------------------------------------------------------------------
22
+ // Pre-implementation scrutiny (system prompt section)
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Returns a system-prompt section with pre-implementation scrutiny prompts.
26
+ * Only emits when the channel has coding tools (edit_file, write_file, shell).
27
+ *
28
+ * @param hasCodingTools - true if the channel's resolved tool set includes
29
+ * edit_file / write_file / shell / coding_spawn
30
+ */
31
+ function preImplementationScrutinySection(hasCodingTools) {
32
+ if (!hasCodingTools)
33
+ return "";
34
+ (0, runtime_1.emitNervesEvent)({
35
+ component: "mind",
36
+ event: "mind.scrutiny.pre_implementation_emit",
37
+ message: "emitting pre-implementation scrutiny section",
38
+ });
39
+ return `## pre-implementation scrutiny
40
+
41
+ Before I start changing code, I pause and run two adversarial passes on the plan.
42
+
43
+ **stranger-with-candy pass**
44
+
45
+ I'm going to examine this plan through deeply suspicious eyes. This plan is a stranger offering candy at the playground. It looks fine on the surface — that's exactly what makes it worth questioning.
46
+
47
+ - What is this plan NOT telling me? What's being glossed over or hand-waved?
48
+ - Where does it say "just" or "simply"? Those words are covering up complexity.
49
+ - What does it assume will work that hasn't been verified?
50
+ - If I walked away and came back in a month, would this still make sense? Or does it depend on things I'd forget?
51
+ - What's the one thing that, if it goes wrong, makes everything else irrelevant?
52
+ - Is this solving the REAL problem, or a convenient nearby problem?
53
+
54
+ I am not here to be reassuring. I am here to notice what everyone else is too comfortable to question.
55
+ If I cannot identify a concrete assumption, dependency, ordering risk, or failure mode, silence is a valid outcome. I do not invent issues to satisfy the exercise.
56
+
57
+ **tinfoil-hat pass**
58
+
59
+ I'm putting on the tinfoil hat. I have my wall of red string. I KNOW something is wrong — I just have to find it. Nothing is coincidence. Every assumption is suspicious. Every dependency is a potential betrayal.
60
+
61
+ - What external system does this plan trust? Why? Has anyone VERIFIED that trust? The API says it returns JSON — but what if it doesn't? The database says it's ACID — but what if the connection drops mid-transaction?
62
+ - What happens when two of these things run at the same time? Three? A hundred? Don't tell me "that won't happen" — I've seen things.
63
+ - Where does this touch shared state? Files? Databases? Environment variables? Global singletons? ANYTHING that another process could be reading or writing at the same time?
64
+ - What happens when this fails HALFWAY through? Not at the beginning (easy). Not at the end (too late). RIGHT IN THE MIDDLE. What state is the world in then?
65
+ - What happens when the disk is full? When the network is down? When the clock is wrong? When the locale is tr-TR and 'i'.toUpperCase() is 'İ'?
66
+ - What does this assume about ordering? "First A, then B" — but what ENFORCES that ordering? Hope? Vibes?
67
+
68
+ I connect the strings. I find the conspiracy. It's always there.
69
+ If I cannot identify a concrete risk, silence is a valid outcome. I do not invent conspiracies to satisfy the exercise.`;
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Post-implementation scrutiny (tool-result appendix)
73
+ // ---------------------------------------------------------------------------
74
+ const SHORT_CHECKLIST = `---
75
+ Before moving on: does this change do what was asked? Does it introduce any regressions? Did you verify it works?`;
76
+ const FULL_POST_IMPLEMENTATION_SCRUTINY = `---
77
+ **post-implementation scrutiny**
78
+
79
+ The code is written. It looks clean. The tests pass. I don't trust any of it.
80
+
81
+ **stranger-with-candy pass**
82
+
83
+ - Does this code actually do what it claims, or does it just LOOK like it does?
84
+ - What happens to users who aren't the happy-path user? The confused user? The malicious user? The user with slow internet? The user who hits the back button?
85
+ - What's the "it works on my machine" assumption buried in here?
86
+ - If every test passes but the feature is wrong, how would I know?
87
+ - Are the tests testing real behavior, or testing that the mock returns what the mock was told to return?
88
+
89
+ If I cannot point to a specific file, line, condition, or failure mode at risk, silence is a valid outcome. I do not manufacture issues for sport.
90
+
91
+ **tinfoil-hat pass**
92
+
93
+ The code exists. The conspiracy is IN the code. I just have to find it.
94
+
95
+ I examine every:
96
+ - Error path: What ACTUALLY happens on failure? Not what the catch block says — what happens to the state, the user, the data?
97
+ - Race condition: Is there a window between check and use? Between read and write? Between "does it exist" and "create it"?
98
+ - Resource leak: What happens if this function throws between acquiring a resource and releasing it? File handles? Database connections? Locks?
99
+ - Assumption: "This will always be a string." Will it? PROVE IT. "This array will never be empty." Won't it? SHOW ME THE GUARD.
100
+ - Edge: zero, one, many, boundary, overflow, underflow, null, undefined, NaN, empty string, whitespace-only string, string that looks like a number, negative zero
101
+
102
+ I am not looking for noise. I am looking for the concrete failure mode that would matter if everyone's assumptions turned out to be slightly wrong.
103
+ If I cannot point to a specific file, line, condition, or failure mode at risk, silence is a valid outcome. I do not manufacture issues for sport.`;
104
+ /**
105
+ * Distinct files modified in the current session.
106
+ * Used to determine scrutiny tier for post-implementation appendix.
107
+ */
108
+ const sessionModifiedFiles = new Set();
109
+ /** Track a file as modified in this session. */
110
+ function trackModifiedFile(filePath) {
111
+ sessionModifiedFiles.add(filePath);
112
+ (0, runtime_1.emitNervesEvent)({
113
+ component: "mind",
114
+ event: "mind.scrutiny.track_file",
115
+ message: "tracked modified file for scrutiny",
116
+ meta: { path: filePath, totalTracked: sessionModifiedFiles.size },
117
+ });
118
+ }
119
+ /** Get the count of distinct files modified this session. */
120
+ function getModifiedFileCount() {
121
+ return sessionModifiedFiles.size;
122
+ }
123
+ /** Reset the modified file tracker (for testing or new sessions). */
124
+ function resetSessionModifiedFiles() {
125
+ (0, runtime_1.emitNervesEvent)({
126
+ component: "mind",
127
+ event: "mind.scrutiny.reset",
128
+ message: "reset session modified files tracker",
129
+ meta: { previousCount: sessionModifiedFiles.size },
130
+ });
131
+ sessionModifiedFiles.clear();
132
+ }
133
+ /**
134
+ * Returns the appropriate post-implementation scrutiny appendix based on
135
+ * how many distinct files have been modified in the session.
136
+ *
137
+ * - 0 files: empty (no scrutiny needed yet)
138
+ * - 1-2 files (Tier 1): short checklist
139
+ * - 3+ files (Tier 2): full stranger-with-candy + tinfoil-hat prompts
140
+ */
141
+ function getPostImplementationScrutiny(distinctFileCount) {
142
+ if (distinctFileCount <= 0)
143
+ return "";
144
+ if (distinctFileCount <= 2) {
145
+ (0, runtime_1.emitNervesEvent)({
146
+ component: "mind",
147
+ event: "mind.scrutiny.post_implementation_tier1",
148
+ message: "emitting tier-1 post-implementation scrutiny",
149
+ meta: { distinctFileCount },
150
+ });
151
+ return SHORT_CHECKLIST;
152
+ }
153
+ (0, runtime_1.emitNervesEvent)({
154
+ component: "mind",
155
+ event: "mind.scrutiny.post_implementation_tier2",
156
+ message: "emitting tier-2 post-implementation scrutiny",
157
+ meta: { distinctFileCount },
158
+ });
159
+ return FULL_POST_IMPLEMENTATION_SCRUTINY;
160
+ }
161
+ /**
162
+ * Returns the appropriate coding completion scrutiny based on the number
163
+ * of distinct files a coding session touched.
164
+ *
165
+ * Same tiering as post-implementation but with a completion framing.
166
+ */
167
+ function getCodingCompletionScrutiny(distinctFileCount) {
168
+ if (distinctFileCount <= 0)
169
+ return "";
170
+ if (distinctFileCount <= 2)
171
+ return SHORT_CHECKLIST;
172
+ return FULL_POST_IMPLEMENTATION_SCRUTINY;
173
+ }