@ouro.bot/cli 0.1.0-alpha.109 → 0.1.0-alpha.110

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
@@ -1,6 +1,13 @@
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.110",
6
+ "changes": [
7
+ "Pi-style capability salvage now lands fully inside the active-work-first architecture: coding lanes inherit a live world-state checkpoint, session recall stays evidence-based, and no parallel session-orientation self-model remains.",
8
+ "Repeated no-progress polling is now blocked across active-work, coding, and session status surfaces, while coding checkpoints/artifacts persist cleanly enough for the same agent to resume real work instead of narrating around it."
9
+ ]
10
+ },
4
11
  {
5
12
  "version": "0.1.0-alpha.109",
6
13
  "changes": [
@@ -39,6 +39,12 @@ function hasSharedObligationPressure(input) {
39
39
  function formatCodingLaneLabel(session) {
40
40
  return `${session.runner} ${session.id}`;
41
41
  }
42
+ function compactCodingCheckpoint(session) {
43
+ const checkpoint = session.checkpoint?.replace(/\s+/g, " ").trim();
44
+ if (!checkpoint)
45
+ return "";
46
+ return checkpoint.length <= 80 ? checkpoint : `${checkpoint.slice(0, 77)}...`;
47
+ }
42
48
  function describeCodingSessionScope(session, currentSession) {
43
49
  if (!session.originSession)
44
50
  return "";
@@ -159,6 +165,12 @@ function formatActiveLane(frame, obligation) {
159
165
  }
160
166
  return null;
161
167
  }
168
+ function formatCodingArtifact(session) {
169
+ const artifactPath = session?.artifactPath?.trim();
170
+ if (artifactPath)
171
+ return artifactPath;
172
+ return session ? "no PR or merge artifact yet" : null;
173
+ }
162
174
  function formatCurrentArtifact(frame, obligation) {
163
175
  if (obligation?.currentArtifact?.trim()) {
164
176
  return obligation.currentArtifact.trim();
@@ -166,8 +178,9 @@ function formatCurrentArtifact(frame, obligation) {
166
178
  if (obligation?.currentSurface?.kind === "merge" && obligation.currentSurface.label.trim()) {
167
179
  return obligation.currentSurface.label.trim();
168
180
  }
169
- if ((frame.codingSessions ?? []).length > 0) {
170
- return "no PR or merge artifact yet";
181
+ const liveCodingArtifact = formatCodingArtifact(frame.codingSessions?.[0]);
182
+ if (liveCodingArtifact) {
183
+ return liveCodingArtifact;
171
184
  }
172
185
  if (obligation) {
173
186
  return "no artifact yet";
@@ -232,8 +245,9 @@ function formatOtherSessionArtifact(obligation, codingSession) {
232
245
  if (obligation?.currentSurface?.kind === "merge" && obligation.currentSurface.label.trim()) {
233
246
  return obligation.currentSurface.label.trim();
234
247
  }
235
- if (codingSession)
236
- return "no PR or merge artifact yet";
248
+ const codingArtifact = formatCodingArtifact(codingSession);
249
+ if (codingArtifact)
250
+ return codingArtifact;
237
251
  return obligation ? "no artifact yet" : "no explicit artifact yet";
238
252
  }
239
253
  function formatOtherSessionNextAction(obligation, codingSession) {
@@ -278,7 +292,7 @@ function formatOtherActiveSessionSummaries(frame, nowMs = Date.now()) {
278
292
  .sort((left, right) => codingSessionTimestampMs(right) - codingSessionTimestampMs(left))
279
293
  .map((session) => ({
280
294
  timestampMs: codingSessionTimestampMs(session),
281
- line: formatOtherSessionLine("another session", session.status, formatCodingLaneLabel(session), "no PR or merge artifact yet", formatOtherSessionNextAction(null, session)),
295
+ line: formatOtherSessionLine("another session", session.status, formatCodingLaneLabel(session), formatCodingArtifact(session), formatOtherSessionNextAction(null, session)),
282
296
  }));
283
297
  for (const session of frame.otherCodingSessions ?? []) {
284
298
  if (!session.originSession)
@@ -533,7 +547,8 @@ function formatActiveWorkFrame(frame) {
533
547
  lines.push("");
534
548
  lines.push("## live coding work");
535
549
  for (const session of frame.codingSessions) {
536
- lines.push(`- [${session.status}] ${formatCodingLaneLabel(session)}${describeCodingSessionScope(session, frame.currentSession)}`);
550
+ const checkpoint = compactCodingCheckpoint(session);
551
+ lines.push(`- [${session.status}] ${formatCodingLaneLabel(session)}${describeCodingSessionScope(session, frame.currentSession)}${checkpoint ? `: ${checkpoint}` : ""}`);
537
552
  }
538
553
  }
539
554
  if (otherActiveSessions.length > 0) {
@@ -31,6 +31,7 @@ const pending_1 = require("../mind/pending");
31
31
  const identity_2 = require("./identity");
32
32
  const socket_client_1 = require("./daemon/socket-client");
33
33
  const obligations_1 = require("./obligations");
34
+ const tool_loop_1 = require("./tool-loop");
34
35
  let _providerRuntime = null;
35
36
  function getProviderRuntimeFingerprint() {
36
37
  const provider = (0, identity_1.loadAgentConfig)().provider;
@@ -490,6 +491,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
490
491
  let sawQuerySession = false;
491
492
  let sawBridgeManage = false;
492
493
  let sawExternalStateQuery = false;
494
+ const toolLoopState = (0, tool_loop_1.createToolLoopState)();
493
495
  // Prevent MaxListenersExceeded warning — each iteration adds a listener
494
496
  try {
495
497
  require("events").setMaxListeners(50, signal);
@@ -503,6 +505,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
503
505
  ...options.toolContext,
504
506
  supportedReasoningEfforts: providerRuntime.supportedReasoningEfforts,
505
507
  setReasoningEffort: (level) => { currentReasoningEffort = level; },
508
+ activeWorkFrame: options?.activeWorkFrame,
506
509
  }
507
510
  : undefined;
508
511
  // Rebase provider-owned turn state from canonical messages at user-turn start.
@@ -804,6 +807,15 @@ async function runAgent(messages, callbacks, channel, signal, options) {
804
807
  if (isExternalStateQuery(tc.name, args))
805
808
  sawExternalStateQuery = true;
806
809
  const argSummary = (0, tools_1.summarizeArgs)(tc.name, args);
810
+ const toolLoop = (0, tool_loop_1.detectToolLoop)(toolLoopState, tc.name, args);
811
+ if (toolLoop.stuck) {
812
+ const rejection = `loop guard: ${toolLoop.message}`;
813
+ callbacks.onToolStart(tc.name, args);
814
+ callbacks.onToolEnd(tc.name, argSummary, false);
815
+ messages.push({ role: "tool", tool_call_id: tc.id, content: rejection });
816
+ providerRuntime.appendToolOutput(tc.id, rejection);
817
+ continue;
818
+ }
807
819
  // Confirmation check for mutate tools
808
820
  if ((0, tools_1.isConfirmationRequired)(tc.name) && !options?.skipConfirmation) {
809
821
  let decision = "denied";
@@ -831,6 +843,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
831
843
  toolResult = `error: ${e}`;
832
844
  success = false;
833
845
  }
846
+ (0, tool_loop_1.recordToolOutcome)(toolLoopState, tc.name, args, toolResult, success);
834
847
  callbacks.onToolEnd(tc.name, argSummary, success);
835
848
  messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
836
849
  providerRuntime.appendToolOutput(tc.id, toolResult);
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.recallSession = recallSession;
37
+ exports.searchSessionTranscript = searchSessionTranscript;
37
38
  const fs = __importStar(require("fs"));
38
39
  const runtime_1 = require("../nerves/runtime");
39
40
  function normalizeContent(content) {
@@ -48,6 +49,19 @@ function normalizeContent(content) {
48
49
  .filter((text) => text.length > 0)
49
50
  .join("");
50
51
  }
52
+ function normalizeSessionMessages(messages) {
53
+ if (!Array.isArray(messages))
54
+ return [];
55
+ return messages
56
+ .map((message) => {
57
+ const record = message && typeof message === "object" ? message : {};
58
+ return {
59
+ role: typeof record.role === "string" ? record.role : "",
60
+ content: normalizeContent(record.content),
61
+ };
62
+ })
63
+ .filter((message) => message.role !== "system" && message.content.length > 0);
64
+ }
51
65
  function buildSummaryInstruction(friendId, channel, trustLevel) {
52
66
  if (friendId === "self" && channel === "inner") {
53
67
  return "summarize this session transcript fully and transparently. this is my own inner dialog — include all details, decisions, and reasoning.";
@@ -70,6 +84,57 @@ function buildSnapshot(summary, tailMessages) {
70
84
  }
71
85
  return lines.join("\n");
72
86
  }
87
+ function buildSearchSnapshot(query, messages, includeLatestTurn = true) {
88
+ const lines = [`history query: "${clip(query, 120)}"`];
89
+ if (!includeLatestTurn) {
90
+ return lines.join("\n");
91
+ }
92
+ const latestUser = [...messages].reverse().find((message) => message.role === "user")?.content;
93
+ const latestAssistant = [...messages].reverse().find((message) => message.role === "assistant")?.content;
94
+ if (latestUser) {
95
+ lines.push(`latest user: ${clip(latestUser)}`);
96
+ }
97
+ if (latestAssistant) {
98
+ lines.push(`latest assistant: ${clip(latestAssistant)}`);
99
+ }
100
+ return lines.join("\n");
101
+ }
102
+ function buildSearchExcerpts(messages, query, maxMatches) {
103
+ const normalizedQuery = query.trim().toLowerCase();
104
+ if (!normalizedQuery)
105
+ return [];
106
+ const candidates = [];
107
+ let lastMatchIndex = -2;
108
+ for (let i = 0; i < messages.length; i++) {
109
+ if (!messages[i].content.toLowerCase().includes(normalizedQuery))
110
+ continue;
111
+ if (i <= lastMatchIndex + 1)
112
+ continue;
113
+ lastMatchIndex = i;
114
+ const start = Math.max(0, i - 1);
115
+ const end = Math.min(messages.length, i + 2);
116
+ const excerpt = messages
117
+ .slice(start, end)
118
+ .map((message) => `[${message.role}] ${clip(message.content, 200)}`)
119
+ .join("\n");
120
+ const score = messages
121
+ .slice(start, end)
122
+ .filter((message) => message.content.toLowerCase().includes(normalizedQuery))
123
+ .length;
124
+ candidates.push({ excerpt, score, index: i });
125
+ }
126
+ const seen = new Set();
127
+ return candidates
128
+ .sort((a, b) => b.score - a.score || a.index - b.index)
129
+ .filter((candidate) => {
130
+ if (seen.has(candidate.excerpt))
131
+ return false;
132
+ seen.add(candidate.excerpt);
133
+ return true;
134
+ })
135
+ .slice(0, maxMatches)
136
+ .map((candidate) => candidate.excerpt);
137
+ }
73
138
  async function recallSession(options) {
74
139
  (0, runtime_1.emitNervesEvent)({
75
140
  component: "daemon",
@@ -90,13 +155,7 @@ async function recallSession(options) {
90
155
  return { kind: "missing" };
91
156
  }
92
157
  const parsed = JSON.parse(raw);
93
- const tailMessages = (parsed.messages ?? [])
94
- .map((message) => ({
95
- role: typeof message.role === "string" ? message.role : "",
96
- content: normalizeContent(message.content),
97
- }))
98
- .filter((message) => message.role !== "system" && message.content.length > 0)
99
- .slice(-options.messageCount);
158
+ const tailMessages = normalizeSessionMessages(parsed.messages).slice(-options.messageCount);
100
159
  if (tailMessages.length === 0) {
101
160
  return { kind: "empty" };
102
161
  }
@@ -114,3 +173,44 @@ async function recallSession(options) {
114
173
  tailMessages,
115
174
  };
116
175
  }
176
+ async function searchSessionTranscript(options) {
177
+ (0, runtime_1.emitNervesEvent)({
178
+ component: "daemon",
179
+ event: "daemon.session_search",
180
+ message: "searching session transcript",
181
+ meta: {
182
+ friendId: options.friendId,
183
+ channel: options.channel,
184
+ key: options.key,
185
+ query: options.query,
186
+ maxMatches: options.maxMatches ?? 5,
187
+ },
188
+ });
189
+ let raw;
190
+ try {
191
+ raw = fs.readFileSync(options.sessionPath, "utf-8");
192
+ }
193
+ catch {
194
+ return { kind: "missing" };
195
+ }
196
+ const parsed = JSON.parse(raw);
197
+ const messages = normalizeSessionMessages(parsed.messages);
198
+ if (messages.length === 0) {
199
+ return { kind: "empty" };
200
+ }
201
+ const query = options.query.trim();
202
+ const matches = buildSearchExcerpts(messages, query, options.maxMatches ?? 5);
203
+ if (matches.length === 0) {
204
+ return {
205
+ kind: "no_match",
206
+ query,
207
+ snapshot: buildSearchSnapshot(query, messages),
208
+ };
209
+ }
210
+ return {
211
+ kind: "ok",
212
+ query,
213
+ snapshot: buildSearchSnapshot(query, messages, false),
214
+ matches,
215
+ };
216
+ }
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.GLOBAL_CIRCUIT_BREAKER_LIMIT = exports.PING_PONG_WINDOW = exports.GENERIC_REPEAT_LIMIT = exports.POLL_NO_PROGRESS_LIMIT = exports.TOOL_LOOP_HISTORY_LIMIT = void 0;
37
+ exports.createToolLoopState = createToolLoopState;
38
+ exports.recordToolOutcome = recordToolOutcome;
39
+ exports.detectToolLoop = detectToolLoop;
40
+ const crypto = __importStar(require("crypto"));
41
+ const runtime_1 = require("../nerves/runtime");
42
+ exports.TOOL_LOOP_HISTORY_LIMIT = 30;
43
+ exports.POLL_NO_PROGRESS_LIMIT = 3;
44
+ exports.GENERIC_REPEAT_LIMIT = 4;
45
+ exports.PING_PONG_WINDOW = 6;
46
+ exports.GLOBAL_CIRCUIT_BREAKER_LIMIT = 24;
47
+ function stableStringify(value) {
48
+ if (value === null || typeof value !== "object") {
49
+ return JSON.stringify(value);
50
+ }
51
+ /* v8 ignore next -- stableStringify currently receives only objects/strings from normalized loop inputs @preserve */
52
+ if (Array.isArray(value)) {
53
+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
54
+ }
55
+ const record = value;
56
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
57
+ }
58
+ function digest(value) {
59
+ return crypto.createHash("sha256").update(stableStringify(value)).digest("hex");
60
+ }
61
+ function normalizeArgs(toolName, args) {
62
+ if (toolName === "query_active_work") {
63
+ return { toolName };
64
+ }
65
+ if (toolName === "coding_status" || toolName === "coding_tail") {
66
+ return {
67
+ toolName,
68
+ sessionId: args.sessionId ?? "",
69
+ };
70
+ }
71
+ if (toolName === "query_session") {
72
+ return {
73
+ toolName,
74
+ mode: args.mode ?? "",
75
+ friendId: args.friendId ?? "",
76
+ channel: args.channel ?? "",
77
+ key: args.key ?? "",
78
+ query: args.query ?? "",
79
+ };
80
+ }
81
+ return {
82
+ toolName,
83
+ args,
84
+ };
85
+ }
86
+ function normalizeOutcome(result, success) {
87
+ return {
88
+ success,
89
+ result: result.replace(/\s+/g, " ").trim(),
90
+ };
91
+ }
92
+ function isKnownPollTool(toolName, args) {
93
+ return toolName === "query_active_work"
94
+ || toolName === "coding_status"
95
+ || toolName === "coding_tail"
96
+ || (toolName === "query_session" && (args.mode ?? "").trim() === "status");
97
+ }
98
+ function emitDetection(detector, toolName, count, message, pairedToolName) {
99
+ (0, runtime_1.emitNervesEvent)({
100
+ level: "warn",
101
+ component: "engine",
102
+ event: "engine.tool_loop_detected",
103
+ message: "tool loop guard detected repeated non-progress work",
104
+ meta: {
105
+ detector,
106
+ toolName,
107
+ count,
108
+ pairedToolName: pairedToolName ?? null,
109
+ },
110
+ });
111
+ return {
112
+ stuck: true,
113
+ detector,
114
+ count,
115
+ message,
116
+ pairedToolName,
117
+ };
118
+ }
119
+ function countTrailingRepeats(history, toolName, callHash) {
120
+ let count = 0;
121
+ let outcomeHash;
122
+ for (let index = history.length - 1; index >= 0; index--) {
123
+ const record = history[index];
124
+ if (record.toolName !== toolName || record.callHash !== callHash) {
125
+ break;
126
+ }
127
+ if (!outcomeHash) {
128
+ outcomeHash = record.outcomeHash;
129
+ }
130
+ if (record.outcomeHash !== outcomeHash) {
131
+ break;
132
+ }
133
+ count += 1;
134
+ }
135
+ return { count, outcomeHash };
136
+ }
137
+ function detectPingPong(history, toolName, callHash) {
138
+ if (history.length < exports.PING_PONG_WINDOW) {
139
+ return { stuck: false };
140
+ }
141
+ const recent = history.slice(-exports.PING_PONG_WINDOW);
142
+ const first = recent[0];
143
+ const second = recent[1];
144
+ if (!first.isKnownPoll || !second.isKnownPoll) {
145
+ return { stuck: false };
146
+ }
147
+ if (first.toolName === second.toolName && first.callHash === second.callHash) {
148
+ return { stuck: false };
149
+ }
150
+ for (let index = 0; index < recent.length; index++) {
151
+ const expected = index % 2 === 0 ? first : second;
152
+ const actual = recent[index];
153
+ if (actual.toolName !== expected.toolName
154
+ || actual.callHash !== expected.callHash
155
+ || actual.outcomeHash !== expected.outcomeHash) {
156
+ return { stuck: false };
157
+ }
158
+ }
159
+ const matchesPair = (toolName === first.toolName && callHash === first.callHash)
160
+ || (toolName === second.toolName && callHash === second.callHash);
161
+ if (!matchesPair) {
162
+ return { stuck: false };
163
+ }
164
+ const pairedToolName = toolName === first.toolName ? second.toolName : first.toolName;
165
+ return emitDetection("ping_pong", toolName, exports.PING_PONG_WINDOW, `repeated ${first.toolName}/${second.toolName} polling is not changing. stop cycling between status checks and either act on the current state, change approach, or answer truthfully with what is known.`, pairedToolName);
166
+ }
167
+ function createToolLoopState() {
168
+ return { history: [] };
169
+ }
170
+ function recordToolOutcome(state, toolName, args, result, success) {
171
+ state.history.push({
172
+ toolName,
173
+ callHash: digest(normalizeArgs(toolName, args)),
174
+ outcomeHash: digest(normalizeOutcome(result, success)),
175
+ isKnownPoll: isKnownPollTool(toolName, args),
176
+ });
177
+ if (state.history.length > exports.TOOL_LOOP_HISTORY_LIMIT) {
178
+ state.history.splice(0, state.history.length - exports.TOOL_LOOP_HISTORY_LIMIT);
179
+ }
180
+ }
181
+ function detectToolLoop(state, toolName, args) {
182
+ if (state.history.length >= exports.GLOBAL_CIRCUIT_BREAKER_LIMIT) {
183
+ return emitDetection("global_circuit_breaker", toolName, state.history.length, `this turn has already made ${state.history.length} tool calls. stop thrashing, use the current evidence, and either change approach or answer truthfully with the best grounded status.`);
184
+ }
185
+ const callHash = digest(normalizeArgs(toolName, args));
186
+ const trailing = countTrailingRepeats(state.history, toolName, callHash);
187
+ if (isKnownPollTool(toolName, args) && trailing.count >= exports.POLL_NO_PROGRESS_LIMIT) {
188
+ return emitDetection("known_poll_no_progress", toolName, trailing.count, `repeated ${toolName} calls have returned the same state ${trailing.count} times. stop polling and either act on the current state, wait for a meaningful change, or answer truthfully with what is known.`);
189
+ }
190
+ if (trailing.count >= exports.GENERIC_REPEAT_LIMIT) {
191
+ return emitDetection("generic_repeat", toolName, trailing.count, `repeating ${toolName} with the same inputs is not producing new information. change approach, use the evidence already gathered, or answer truthfully with what is known.`);
192
+ }
193
+ return detectPingPong(state.history, toolName, callHash);
194
+ }
@@ -437,6 +437,7 @@ function memoryFriendToolContractSection() {
437
437
  2. \`memory_save\` — 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 \`memory_save\`. When in doubt, I save it.
438
438
  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\`.
439
439
  4. \`memory_search\` — When I need to recall something I learned before - a topic comes up and I want to check what I know - I call \`memory_search\`.
440
+ 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.
440
441
 
441
442
  ## what's already in my context
442
443
  - My active friend's notes are auto-loaded (I don't need \`get_friend_note\` for the person I'm talking to).
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.prepareCodingContextPack = prepareCodingContextPack;
37
+ const crypto = __importStar(require("crypto"));
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const child_process_1 = require("child_process");
41
+ const active_work_1 = require("../../heart/active-work");
42
+ const identity_1 = require("../../heart/identity");
43
+ const runtime_1 = require("../../nerves/runtime");
44
+ const skills_1 = require("../skills");
45
+ const CONTEXT_FILENAMES = ["AGENTS.md", "CLAUDE.md"];
46
+ function defaultRunCommand(command, args, cwd) {
47
+ const result = (0, child_process_1.spawnSync)(command, args, {
48
+ cwd,
49
+ encoding: "utf-8",
50
+ });
51
+ return {
52
+ status: result.status ?? 1,
53
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
54
+ stderr: typeof result.stderr === "string" ? result.stderr : "",
55
+ };
56
+ }
57
+ function stableContextKey(request) {
58
+ const payload = JSON.stringify({
59
+ runner: request.runner,
60
+ workdir: request.workdir,
61
+ taskRef: request.taskRef ?? "",
62
+ parentAgent: request.parentAgent ?? "",
63
+ obligationId: request.obligationId ?? "",
64
+ originSession: request.originSession ?? null,
65
+ });
66
+ return crypto.createHash("sha1").update(payload).digest("hex").slice(0, 12);
67
+ }
68
+ function collectProjectContextFiles(workdir, deps) {
69
+ const files = [];
70
+ const seen = new Set();
71
+ let current = path.resolve(workdir);
72
+ const root = path.parse(current).root;
73
+ while (true) {
74
+ for (const filename of CONTEXT_FILENAMES) {
75
+ const candidate = path.join(current, filename);
76
+ if (!deps.existsSync(candidate) || seen.has(candidate))
77
+ continue;
78
+ try {
79
+ const content = deps.readFileSync(candidate, "utf-8").trim();
80
+ if (content.length > 0) {
81
+ files.unshift({ path: candidate, content });
82
+ seen.add(candidate);
83
+ }
84
+ }
85
+ catch {
86
+ // Best-effort loading only.
87
+ }
88
+ }
89
+ if (current === root)
90
+ break;
91
+ current = path.dirname(current);
92
+ }
93
+ return files;
94
+ }
95
+ function captureRepoSnapshot(workdir, runCommand) {
96
+ const repoRoot = runCommand("git", ["rev-parse", "--show-toplevel"], workdir);
97
+ if (repoRoot.status !== 0) {
98
+ return {
99
+ available: false,
100
+ repoRoot: null,
101
+ branch: null,
102
+ head: null,
103
+ statusLines: [],
104
+ };
105
+ }
106
+ const branch = runCommand("git", ["rev-parse", "--abbrev-ref", "HEAD"], workdir);
107
+ const head = runCommand("git", ["rev-parse", "--short", "HEAD"], workdir);
108
+ const status = runCommand("git", ["status", "--short"], workdir);
109
+ return {
110
+ available: true,
111
+ repoRoot: repoRoot.stdout.trim() || null,
112
+ branch: branch.status === 0 ? branch.stdout.trim() || null : null,
113
+ head: head.status === 0 ? head.stdout.trim() || null : null,
114
+ statusLines: status.status === 0
115
+ ? status.stdout.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean)
116
+ : [],
117
+ };
118
+ }
119
+ function formatContextFiles(files) {
120
+ if (files.length === 0)
121
+ return "(none found)";
122
+ return files.map((file) => `### ${file.path}\n${file.content}`).join("\n\n");
123
+ }
124
+ function formatSkills(skills) {
125
+ return skills.length > 0 ? skills.join(", ") : "(none found)";
126
+ }
127
+ function formatExistingSessions(sessions) {
128
+ if (sessions.length === 0)
129
+ return "activeSessions: none";
130
+ return sessions
131
+ .map((session) => {
132
+ return [
133
+ `- ${session.id}`,
134
+ `status=${session.status}`,
135
+ `lastActivityAt=${session.lastActivityAt}`,
136
+ session.taskRef ? `taskRef=${session.taskRef}` : null,
137
+ session.checkpoint ? `checkpoint=${session.checkpoint}` : null,
138
+ session.artifactPath ? `artifact=${session.artifactPath}` : null,
139
+ ].filter(Boolean).join(" ");
140
+ })
141
+ .join("\n");
142
+ }
143
+ function formatOrigin(request) {
144
+ if (!request.originSession)
145
+ return "originSession: none";
146
+ return `originSession: ${request.originSession.channel}/${request.originSession.key} (${request.originSession.friendId})`;
147
+ }
148
+ function buildScopeContent(request, contextFiles, skills, agentName) {
149
+ return [
150
+ "# Coding Session Scope",
151
+ "",
152
+ "## Request",
153
+ `runner: ${request.runner}`,
154
+ `taskRef: ${request.taskRef ?? "unassigned"}`,
155
+ `parentAgent: ${request.parentAgent ?? agentName}`,
156
+ `workdir: ${request.workdir}`,
157
+ formatOrigin(request),
158
+ `obligationId: ${request.obligationId ?? "none"}`,
159
+ "",
160
+ "## Prompt",
161
+ request.prompt,
162
+ "",
163
+ "## Session Contract",
164
+ "- This is a focused coding lane opened by the parent Ouro agent.",
165
+ "- Execute the concrete prompt in the supplied workdir directly.",
166
+ "- Do not switch into planning/doing workflows or approval gates unless the prompt explicitly asks for them.",
167
+ "- Treat the current prompt, scope file, and live world-state checkpoint in the state file as the authoritative briefing for this lane.",
168
+ "",
169
+ "## Project Context Files",
170
+ formatContextFiles(contextFiles),
171
+ "",
172
+ "## Available Bundle Skills",
173
+ formatSkills(skills),
174
+ ].join("\n");
175
+ }
176
+ function buildStateContent(request, contextKey, generatedAt, snapshot, existingSessions, agentName, activeWorkFrame) {
177
+ const gitSection = snapshot.available
178
+ ? [
179
+ `repoRoot: ${snapshot.repoRoot ?? "unknown"}`,
180
+ `branch: ${snapshot.branch ?? "unknown"}`,
181
+ `head: ${snapshot.head ?? "unknown"}`,
182
+ "status:",
183
+ snapshot.statusLines.length > 0 ? snapshot.statusLines.join("\n") : "(clean)",
184
+ ].join("\n")
185
+ : "git: unavailable";
186
+ return [
187
+ "# Coding Session State",
188
+ `generatedAt: ${generatedAt}`,
189
+ `contextKey: ${contextKey}`,
190
+ `agent: ${request.parentAgent ?? agentName}`,
191
+ formatOrigin(request),
192
+ `obligationId: ${request.obligationId ?? "none"}`,
193
+ "",
194
+ "## Workspace Snapshot",
195
+ gitSection,
196
+ ...(activeWorkFrame ? ["", (0, active_work_1.formatLiveWorldStateCheckpoint)(activeWorkFrame)] : []),
197
+ "",
198
+ "## Related Coding Sessions",
199
+ formatExistingSessions(existingSessions),
200
+ ].join("\n");
201
+ }
202
+ function relatedSessions(request, existingSessions) {
203
+ return existingSessions.filter((session) => {
204
+ return session.runner === request.runner
205
+ && session.workdir === request.workdir
206
+ && session.taskRef === request.taskRef;
207
+ });
208
+ }
209
+ function prepareCodingContextPack(input, deps = {}) {
210
+ const agentRoot = deps.agentRoot ?? (0, identity_1.getAgentRoot)();
211
+ const agentName = deps.agentName ?? (0, identity_1.getAgentName)();
212
+ const nowIso = deps.nowIso ?? (() => new Date().toISOString());
213
+ const existsSync = deps.existsSync ?? fs.existsSync;
214
+ const readFileSync = deps.readFileSync ?? fs.readFileSync;
215
+ const writeFileSync = deps.writeFileSync ?? fs.writeFileSync;
216
+ const mkdirSync = deps.mkdirSync ?? fs.mkdirSync;
217
+ const listAvailableSkills = deps.listSkills ?? skills_1.listSkills;
218
+ const runCommand = deps.runCommand ?? defaultRunCommand;
219
+ const contextKey = stableContextKey(input.request);
220
+ const contextDir = path.join(agentRoot, "state", "coding", "context");
221
+ const scopeFile = path.join(contextDir, `${contextKey}-scope.md`);
222
+ const stateFile = path.join(contextDir, `${contextKey}-state.md`);
223
+ const contextFiles = collectProjectContextFiles(input.request.workdir, { existsSync, readFileSync });
224
+ const skills = listAvailableSkills();
225
+ const existingSessions = relatedSessions(input.request, input.existingSessions ?? []);
226
+ const snapshot = captureRepoSnapshot(input.request.workdir, runCommand);
227
+ const generatedAt = nowIso();
228
+ const scopeContent = buildScopeContent(input.request, contextFiles, skills, agentName);
229
+ const stateContent = buildStateContent(input.request, contextKey, generatedAt, snapshot, existingSessions, agentName, input.activeWorkFrame);
230
+ mkdirSync(contextDir, { recursive: true });
231
+ writeFileSync(scopeFile, `${scopeContent}\n`, "utf-8");
232
+ writeFileSync(stateFile, `${stateContent}\n`, "utf-8");
233
+ (0, runtime_1.emitNervesEvent)({
234
+ component: "repertoire",
235
+ event: "repertoire.coding_context_pack_written",
236
+ message: "prepared coding session context pack",
237
+ meta: {
238
+ contextKey,
239
+ workdir: input.request.workdir,
240
+ taskRef: input.request.taskRef ?? null,
241
+ contextFiles: contextFiles.length,
242
+ skills: skills.length,
243
+ relatedSessions: existingSessions.length,
244
+ gitAvailable: snapshot.available,
245
+ },
246
+ });
247
+ return {
248
+ contextKey,
249
+ scopeFile,
250
+ stateFile,
251
+ scopeContent,
252
+ stateContent,
253
+ };
254
+ }
@@ -131,10 +131,37 @@ function isSafeProgressSnippet(snippet) {
131
131
  && !/^parentAgent\b/i.test(normalized));
132
132
  }
133
133
  function pickUpdateSnippet(update) {
134
- return (lastMeaningfulLine(update.text)
134
+ const checkpoint = update.session.checkpoint?.trim() || null;
135
+ return (checkpoint
136
+ ?? lastMeaningfulLine(update.text)
135
137
  ?? lastMeaningfulLine(update.session.stderrTail)
136
138
  ?? lastMeaningfulLine(update.session.stdoutTail));
137
139
  }
140
+ function renderValue(text) {
141
+ const trimmed = text?.trim();
142
+ return trimmed && trimmed.length > 0 ? trimmed : "(empty)";
143
+ }
144
+ function renderPath(text) {
145
+ return text && text.trim().length > 0 ? text : "(none)";
146
+ }
147
+ function formatCodingTail(session) {
148
+ const stdout = renderValue(session.stdoutTail);
149
+ const stderr = renderValue(session.stderrTail);
150
+ return [
151
+ `sessionId: ${session.id}`,
152
+ `runner: ${session.runner}`,
153
+ `status: ${session.status}`,
154
+ `checkpoint: ${renderValue(session.checkpoint ?? undefined)}`,
155
+ `artifactPath: ${renderPath(session.artifactPath)}`,
156
+ `workdir: ${session.workdir}`,
157
+ "",
158
+ "[stdout]",
159
+ stdout,
160
+ "",
161
+ "[stderr]",
162
+ stderr,
163
+ ].join("\n");
164
+ }
138
165
  function formatUpdateMessage(update) {
139
166
  const label = formatSessionLabel(update.session);
140
167
  const snippet = pickUpdateSnippet(update);
@@ -233,22 +260,6 @@ async function wakeInnerDialogForObligation(update) {
233
260
  });
234
261
  }
235
262
  }
236
- function formatCodingTail(session) {
237
- const stdout = session.stdoutTail.trim() || "(empty)";
238
- const stderr = session.stderrTail.trim() || "(empty)";
239
- return [
240
- `sessionId: ${session.id}`,
241
- `runner: ${session.runner}`,
242
- `status: ${session.status}`,
243
- `workdir: ${session.workdir}`,
244
- "",
245
- "[stdout]",
246
- stdout,
247
- "",
248
- "[stderr]",
249
- stderr,
250
- ].join("\n");
251
- }
252
263
  function attachCodingSessionFeedback(manager, session, target) {
253
264
  let lastMessage = "";
254
265
  let closed = false;
@@ -50,6 +50,9 @@ function safeAgentName() {
50
50
  function defaultStateFilePath(agentName) {
51
51
  return path.join((0, identity_1.getAgentRoot)(agentName), "state", "coding", "sessions.json");
52
52
  }
53
+ function defaultArtifactDirPath(agentName) {
54
+ return path.join((0, identity_1.getAgentRoot)(agentName), "state", "coding", "sessions");
55
+ }
53
56
  function isPidAlive(pid) {
54
57
  try {
55
58
  process.kill(pid, 0);
@@ -63,6 +66,8 @@ function cloneSession(session) {
63
66
  return {
64
67
  ...session,
65
68
  originSession: session.originSession ? { ...session.originSession } : undefined,
69
+ checkpoint: session.checkpoint ?? null,
70
+ artifactPath: session.artifactPath,
66
71
  stdoutTail: session.stdoutTail,
67
72
  stderrTail: session.stderrTail,
68
73
  failure: session.failure
@@ -86,6 +91,46 @@ function appendTail(existing, nextChunk, maxLength = 2000) {
86
91
  const combined = `${existing}${nextChunk}`;
87
92
  return combined.length <= maxLength ? combined : combined.slice(combined.length - maxLength);
88
93
  }
94
+ function compactText(text) {
95
+ return text.replace(/\s+/g, " ").trim();
96
+ }
97
+ function clipText(text, maxLength = 240) {
98
+ return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3)}...`;
99
+ }
100
+ function latestMeaningfulLine(text) {
101
+ const lines = text
102
+ .split(/\r?\n/)
103
+ .map((line) => compactText(line))
104
+ .filter(Boolean);
105
+ if (lines.length === 0)
106
+ return null;
107
+ return clipText(lines.at(-1));
108
+ }
109
+ function fallbackCheckpoint(status, code, signal) {
110
+ switch (status) {
111
+ case "waiting_input":
112
+ return "needs input";
113
+ case "stalled":
114
+ return "no recent output";
115
+ case "completed":
116
+ return "completed";
117
+ case "failed":
118
+ if (code !== null)
119
+ return `exit code ${code}`;
120
+ if (signal)
121
+ return `terminated by ${signal}`;
122
+ return "failed";
123
+ case "killed":
124
+ return "terminated by parent agent";
125
+ default:
126
+ return null;
127
+ }
128
+ }
129
+ function deriveCheckpoint(session) {
130
+ return (latestMeaningfulLine(session.stderrTail)
131
+ ?? latestMeaningfulLine(session.stdoutTail)
132
+ ?? fallbackCheckpoint(session.status, session.lastExitCode, session.lastSignal));
133
+ }
89
134
  function isSpawnCodingResult(value) {
90
135
  return typeof value === "object" && value !== null && "process" in value;
91
136
  }
@@ -123,6 +168,7 @@ class CodingSessionManager {
123
168
  maxRestarts;
124
169
  defaultStallThresholdMs;
125
170
  stateFilePath;
171
+ artifactDirPath;
126
172
  existsSync;
127
173
  readFileSync;
128
174
  writeFileSync;
@@ -142,6 +188,8 @@ class CodingSessionManager {
142
188
  this.pidAlive = options.pidAlive ?? isPidAlive;
143
189
  this.agentName = options.agentName ?? safeAgentName();
144
190
  this.stateFilePath = options.stateFilePath ?? defaultStateFilePath(this.agentName);
191
+ this.artifactDirPath = options.artifactDirPath
192
+ ?? (options.stateFilePath ? path.dirname(options.stateFilePath) : defaultArtifactDirPath(this.agentName));
145
193
  this.loadPersistedState();
146
194
  }
147
195
  async spawnSession(request) {
@@ -162,6 +210,8 @@ class CodingSessionManager {
162
210
  obligationId: normalizedRequest.obligationId,
163
211
  scopeFile: normalizedRequest.scopeFile,
164
212
  stateFile: normalizedRequest.stateFile,
213
+ checkpoint: null,
214
+ artifactPath: this.artifactPathFor(id),
165
215
  status: "spawning",
166
216
  stdoutTail: "",
167
217
  stderrTail: "",
@@ -248,6 +298,7 @@ class CodingSessionManager {
248
298
  record.process.kill("SIGTERM");
249
299
  record.process = null;
250
300
  record.session.status = "killed";
301
+ record.session.checkpoint = "terminated by parent agent";
251
302
  record.session.endedAt = this.nowIso();
252
303
  (0, runtime_1.emitNervesEvent)({
253
304
  component: "repertoire",
@@ -270,6 +321,7 @@ class CodingSessionManager {
270
321
  continue;
271
322
  stalled += 1;
272
323
  record.session.status = "stalled";
324
+ record.session.checkpoint = deriveCheckpoint(record.session);
273
325
  (0, runtime_1.emitNervesEvent)({
274
326
  level: "warn",
275
327
  component: "repertoire",
@@ -295,6 +347,7 @@ class CodingSessionManager {
295
347
  record.process = null;
296
348
  if (record.session.status === "running" || record.session.status === "spawning") {
297
349
  record.session.status = "killed";
350
+ record.session.checkpoint = "terminated during manager shutdown";
298
351
  record.session.endedAt = this.nowIso();
299
352
  }
300
353
  }
@@ -339,6 +392,13 @@ class CodingSessionManager {
339
392
  record.session.endedAt = this.nowIso();
340
393
  updateKind = "completed";
341
394
  }
395
+ const checkpoint = latestMeaningfulLine(text);
396
+ if (checkpoint) {
397
+ record.session.checkpoint = checkpoint;
398
+ }
399
+ else if (!record.session.checkpoint) {
400
+ record.session.checkpoint = deriveCheckpoint(record.session);
401
+ }
342
402
  (0, runtime_1.emitNervesEvent)({
343
403
  component: "repertoire",
344
404
  event: "repertoire.coding_session_output",
@@ -362,12 +422,14 @@ class CodingSessionManager {
362
422
  record.session.lastSignal = signal;
363
423
  if (record.session.status === "killed" || record.session.status === "completed") {
364
424
  record.session.endedAt = this.nowIso();
425
+ record.session.checkpoint = deriveCheckpoint(record.session);
365
426
  this.persistState();
366
427
  return;
367
428
  }
368
429
  if (code === 0) {
369
430
  record.session.status = "completed";
370
431
  record.session.endedAt = this.nowIso();
432
+ record.session.checkpoint = deriveCheckpoint(record.session);
371
433
  this.persistState();
372
434
  this.notifyListeners(record.session.id, { kind: "completed", session: cloneSession(record.session) });
373
435
  return;
@@ -379,6 +441,7 @@ class CodingSessionManager {
379
441
  record.session.status = "failed";
380
442
  record.session.endedAt = this.nowIso();
381
443
  record.session.failure = defaultFailureDiagnostics(code, signal, record.command, record.args, record.stdoutTail, record.stderrTail);
444
+ record.session.checkpoint = deriveCheckpoint(record.session);
382
445
  (0, runtime_1.emitNervesEvent)({
383
446
  level: "error",
384
447
  component: "repertoire",
@@ -404,6 +467,7 @@ class CodingSessionManager {
404
467
  record.session.lastActivityAt = this.nowIso();
405
468
  record.session.endedAt = null;
406
469
  record.session.failure = null;
470
+ record.session.checkpoint = `restarted after ${reason}`;
407
471
  this.attachProcessListeners(record);
408
472
  (0, runtime_1.emitNervesEvent)({
409
473
  level: "warn",
@@ -498,6 +562,8 @@ class CodingSessionManager {
498
562
  failure: session.failure ?? null,
499
563
  stdoutTail: session.stdoutTail ?? session.failure?.stdoutTail ?? "",
500
564
  stderrTail: session.stderrTail ?? session.failure?.stderrTail ?? "",
565
+ checkpoint: typeof session.checkpoint === "string" ? session.checkpoint : null,
566
+ artifactPath: typeof session.artifactPath === "string" ? session.artifactPath : this.artifactPathFor(session.id),
501
567
  };
502
568
  if (typeof normalizedSession.pid === "number") {
503
569
  const alive = this.pidAlive(normalizedSession.pid);
@@ -510,6 +576,7 @@ class CodingSessionManager {
510
576
  normalizedSession.pid = null;
511
577
  }
512
578
  }
579
+ normalizedSession.checkpoint = normalizedSession.checkpoint ?? deriveCheckpoint(normalizedSession);
513
580
  this.records.set(normalizedSession.id, {
514
581
  request: normalizedRequest,
515
582
  session: normalizedSession,
@@ -549,6 +616,80 @@ class CodingSessionManager {
549
616
  meta: { path: this.stateFilePath, reason: error instanceof Error ? error.message : String(error) },
550
617
  });
551
618
  }
619
+ this.persistArtifacts();
620
+ }
621
+ artifactPathFor(sessionId) {
622
+ return path.join(this.artifactDirPath, `${sessionId}.md`);
623
+ }
624
+ renderArtifact(record) {
625
+ const { request, session } = record;
626
+ const stdout = session.stdoutTail.trim() || "(empty)";
627
+ const stderr = session.stderrTail.trim() || "(empty)";
628
+ const lines = [
629
+ "# Coding Session Artifact",
630
+ "",
631
+ "## Session",
632
+ `id: ${session.id}`,
633
+ `runner: ${session.runner}`,
634
+ `status: ${session.status}`,
635
+ `taskRef: ${session.taskRef ?? "unassigned"}`,
636
+ `workdir: ${session.workdir}`,
637
+ `startedAt: ${session.startedAt}`,
638
+ `lastActivityAt: ${session.lastActivityAt}`,
639
+ `endedAt: ${session.endedAt ?? "active"}`,
640
+ `pid: ${session.pid ?? "none"}`,
641
+ `restarts: ${session.restartCount}`,
642
+ `checkpoint: ${session.checkpoint ?? "none"}`,
643
+ `scopeFile: ${session.scopeFile ?? "none"}`,
644
+ `stateFile: ${session.stateFile ?? "none"}`,
645
+ "",
646
+ "## Request",
647
+ request.prompt,
648
+ "",
649
+ "## Stdout Tail",
650
+ stdout,
651
+ "",
652
+ "## Stderr Tail",
653
+ stderr,
654
+ ];
655
+ if (session.failure) {
656
+ lines.push("", "## Failure", `command: ${session.failure.command}`, `args: ${session.failure.args.join(" ") || "(none)"}`, `code: ${session.failure.code ?? "null"}`, `signal: ${session.failure.signal ?? "null"}`);
657
+ }
658
+ return `${lines.join("\n")}\n`;
659
+ }
660
+ persistArtifacts() {
661
+ try {
662
+ this.mkdirSync(this.artifactDirPath, { recursive: true });
663
+ }
664
+ catch (error) {
665
+ (0, runtime_1.emitNervesEvent)({
666
+ level: "warn",
667
+ component: "repertoire",
668
+ event: "repertoire.coding_artifact_persist_error",
669
+ message: "failed preparing coding artifact directory",
670
+ meta: { path: this.artifactDirPath, reason: error instanceof Error ? error.message : String(error) },
671
+ });
672
+ return;
673
+ }
674
+ for (const record of this.records.values()) {
675
+ try {
676
+ record.session.artifactPath = record.session.artifactPath ?? this.artifactPathFor(record.session.id);
677
+ this.writeFileSync(record.session.artifactPath, this.renderArtifact(record), "utf-8");
678
+ }
679
+ catch (error) {
680
+ (0, runtime_1.emitNervesEvent)({
681
+ level: "warn",
682
+ component: "repertoire",
683
+ event: "repertoire.coding_artifact_persist_error",
684
+ message: "failed writing coding session artifact",
685
+ meta: {
686
+ id: record.session.id,
687
+ path: record.session.artifactPath ?? this.artifactPathFor(record.session.id),
688
+ reason: error instanceof Error ? error.message : String(error),
689
+ },
690
+ });
691
+ }
692
+ }
552
693
  }
553
694
  }
554
695
  exports.CodingSessionManager = CodingSessionManager;
@@ -72,20 +72,32 @@ function buildSpawnEnv(baseEnv, homeDir) {
72
72
  PATH: pathEntries.join(path.delimiter),
73
73
  };
74
74
  }
75
+ function appendFileSection(sections, label, filePath, deps) {
76
+ if (!filePath || !deps.existsSync(filePath))
77
+ return;
78
+ const content = deps.readFileSync(filePath, "utf-8").trim();
79
+ if (content.length === 0)
80
+ return;
81
+ sections.push(`${label} (${filePath}):\n${content}`);
82
+ }
75
83
  function buildPrompt(request, deps) {
76
84
  const sections = [];
85
+ sections.push([
86
+ "Execution contract:",
87
+ "- You are a subordinate coding session launched by a parent Ouro agent.",
88
+ "- Execute the concrete request in the supplied workdir directly.",
89
+ "- Do not switch into planning/doing workflows, approval gates, or repo-management rituals unless the request explicitly asks for them.",
90
+ "- Treat the request, scope file, and state file as the authoritative briefing for this session.",
91
+ "- Prefer direct execution and verification over narration.",
92
+ ].join("\n"));
77
93
  sections.push([
78
94
  "Coding session metadata:",
79
95
  `sessionId: ${request.sessionId ?? "pending"}`,
80
96
  `parentAgent: ${request.parentAgent ?? "unknown"}`,
81
97
  `taskRef: ${request.taskRef ?? "unassigned"}`,
82
98
  ].join("\n"));
83
- if (request.stateFile && deps.existsSync(request.stateFile)) {
84
- const stateContent = deps.readFileSync(request.stateFile, "utf-8").trim();
85
- if (stateContent.length > 0) {
86
- sections.push(`State file (${request.stateFile}):\n${stateContent}`);
87
- }
88
- }
99
+ appendFileSection(sections, "Scope file", request.scopeFile, deps);
100
+ appendFileSection(sections, "State file", request.stateFile, deps);
89
101
  sections.push(request.prompt);
90
102
  return sections.join("\n\n---\n\n");
91
103
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.codingToolDefinitions = void 0;
4
4
  const index_1 = require("./index");
5
+ const context_pack_1 = require("./context-pack");
5
6
  const identity_1 = require("../../heart/identity");
6
7
  const obligations_1 = require("../../heart/obligations");
7
8
  const runtime_1 = require("../../nerves/runtime");
@@ -42,11 +43,13 @@ function matchesReusableCodingSession(session, request) {
42
43
  if (session.status !== "spawning" && session.status !== "running" && session.status !== "waiting_input" && session.status !== "stalled") {
43
44
  return false;
44
45
  }
46
+ const scopeMatches = request.scopeFile ? session.scopeFile === request.scopeFile : true;
47
+ const stateMatches = request.stateFile ? session.stateFile === request.stateFile : true;
45
48
  return (session.runner === request.runner &&
46
49
  session.workdir === request.workdir &&
47
50
  session.taskRef === request.taskRef &&
48
- session.scopeFile === request.scopeFile &&
49
- session.stateFile === request.stateFile &&
51
+ scopeMatches &&
52
+ stateMatches &&
50
53
  session.obligationId === request.obligationId &&
51
54
  sameOriginSession(request.originSession, session.originSession));
52
55
  }
@@ -221,7 +224,8 @@ exports.codingToolDefinitions = [
221
224
  if (stateFile)
222
225
  request.stateFile = stateFile;
223
226
  const manager = (0, index_1.getCodingSessionManager)();
224
- const existingSession = findReusableCodingSession(manager.listSessions(), request);
227
+ const existingSessions = manager.listSessions();
228
+ const existingSession = findReusableCodingSession(existingSessions, request);
225
229
  if (existingSession) {
226
230
  (0, runtime_1.emitNervesEvent)({
227
231
  component: "repertoire",
@@ -241,6 +245,17 @@ exports.codingToolDefinitions = [
241
245
  });
242
246
  request.obligationId = created.id;
243
247
  }
248
+ if (!request.scopeFile || !request.stateFile) {
249
+ const generated = (0, context_pack_1.prepareCodingContextPack)({
250
+ request: { ...request },
251
+ existingSessions,
252
+ activeWorkFrame: ctx?.activeWorkFrame,
253
+ });
254
+ if (!request.scopeFile)
255
+ request.scopeFile = generated.scopeFile;
256
+ if (!request.stateFile)
257
+ request.stateFile = generated.stateFile;
258
+ }
244
259
  const session = await manager.spawnSession(request);
245
260
  if (session.obligationId) {
246
261
  (0, obligations_1.advanceObligation)((0, identity_1.getAgentRoot)(), session.obligationId, {
@@ -116,6 +116,14 @@ async function recallSessionSafely(options) {
116
116
  return { kind: "missing" };
117
117
  }
118
118
  }
119
+ async function searchSessionSafely(options) {
120
+ try {
121
+ return await (0, session_recall_1.searchSessionTranscript)(options);
122
+ }
123
+ catch {
124
+ return { kind: "missing" };
125
+ }
126
+ }
119
127
  function normalizeProgressOutcome(text) {
120
128
  const trimmed = text.trim();
121
129
  /* v8 ignore next -- defensive: normalizeProgressOutcome null branch @preserve */
@@ -993,7 +1001,7 @@ exports.baseToolDefinitions = [
993
1001
  type: "function",
994
1002
  function: {
995
1003
  name: "query_session",
996
- description: "read the last messages from another session. use this to check on a conversation with a friend or review your own inner dialog.",
1004
+ description: "inspect another session. use transcript for recent context, status for self/inner progress, or search to find older history by query.",
997
1005
  parameters: {
998
1006
  type: "object",
999
1007
  properties: {
@@ -1001,7 +1009,12 @@ exports.baseToolDefinitions = [
1001
1009
  channel: { type: "string", description: "the channel: cli, teams, or inner" },
1002
1010
  key: { type: "string", description: "session key (defaults to 'session')" },
1003
1011
  messageCount: { type: "string", description: "how many recent messages to return (default 20)" },
1004
- mode: { type: "string", enum: ["transcript", "status"], description: "transcript (default) or lightweight status for self/inner checks" },
1012
+ mode: {
1013
+ type: "string",
1014
+ enum: ["transcript", "status", "search"],
1015
+ description: "transcript (default), lightweight status for self/inner checks, or search for older history",
1016
+ },
1017
+ query: { type: "string", description: "required when mode=search; search term for older session history" },
1005
1018
  },
1006
1019
  required: ["friendId", "channel"],
1007
1020
  },
@@ -1021,6 +1034,33 @@ exports.baseToolDefinitions = [
1021
1034
  const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
1022
1035
  return renderInnerProgressStatus((0, thoughts_1.readInnerDialogStatus)(sessionPath, pendingDir));
1023
1036
  }
1037
+ if (mode === "search") {
1038
+ const query = (args.query || "").trim();
1039
+ if (!query) {
1040
+ return "search mode requires a non-empty query.";
1041
+ }
1042
+ const search = await searchSessionSafely({
1043
+ sessionPath: (0, config_1.resolveSessionPath)(friendId, channel, key),
1044
+ friendId,
1045
+ channel,
1046
+ key,
1047
+ query,
1048
+ });
1049
+ if (search.kind === "missing") {
1050
+ return NO_SESSION_FOUND_MESSAGE;
1051
+ }
1052
+ if (search.kind === "empty") {
1053
+ return EMPTY_SESSION_MESSAGE;
1054
+ }
1055
+ if (search.kind === "no_match") {
1056
+ return `no matches for "${search.query}" in that session.\n\n${search.snapshot}`;
1057
+ }
1058
+ return [
1059
+ `history search: "${search.query}"`,
1060
+ search.snapshot,
1061
+ ...search.matches.map((match, index) => `match ${index + 1}\n${match}`),
1062
+ ].join("\n\n");
1063
+ }
1024
1064
  const sessFile = (0, config_1.resolveSessionPath)(friendId, channel, key);
1025
1065
  const recall = await recallSessionSafely({
1026
1066
  sessionPath: sessFile,
@@ -629,7 +629,13 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
629
629
  continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
630
630
  callbacks,
631
631
  friendResolver: { resolve: () => Promise.resolve(context) },
632
- sessionLoader: { loadOrCreate: () => Promise.resolve({ messages: sessionMessages, sessionPath: sessPath, state: existing?.state }) },
632
+ sessionLoader: {
633
+ loadOrCreate: () => Promise.resolve({
634
+ messages: sessionMessages,
635
+ sessionPath: sessPath,
636
+ state: existing?.state,
637
+ }),
638
+ },
633
639
  pendingDir,
634
640
  friendStore: store,
635
641
  provider: "imessage-handle",
@@ -827,7 +827,13 @@ async function main(agentName, options) {
827
827
  continuityIngressTexts: getCliContinuityIngressTexts(userInput),
828
828
  callbacks,
829
829
  friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
830
- sessionLoader: { loadOrCreate: () => Promise.resolve({ messages, sessionPath: sessPath, state: sessionState }) },
830
+ sessionLoader: {
831
+ loadOrCreate: () => Promise.resolve({
832
+ messages,
833
+ sessionPath: sessPath,
834
+ state: sessionState,
835
+ }),
836
+ },
831
837
  pendingDir,
832
838
  friendStore,
833
839
  provider: "local",
@@ -459,7 +459,10 @@ async function runInnerDialogTurn(options) {
459
459
  const sessionLoader = {
460
460
  loadOrCreate: async () => {
461
461
  if (existingMessages.length > 0) {
462
- return { messages: existingMessages, sessionPath: sessionFilePath };
462
+ return {
463
+ messages: existingMessages,
464
+ sessionPath: sessionFilePath,
465
+ };
463
466
  }
464
467
  // Fresh session: build system prompt
465
468
  const systemPrompt = await (0, prompt_1.buildSystem)("inner", { toolChoiceRequired: true, mcpManager });
@@ -564,7 +564,11 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
564
564
  ? existing.messages
565
565
  : [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", { mcpManager }, resolvedContext) }];
566
566
  (0, core_1.repairOrphanedToolCalls)(messages);
567
- return { messages, sessionPath: sessPath, state: existing?.state };
567
+ return {
568
+ messages,
569
+ sessionPath: sessPath,
570
+ state: existing?.state,
571
+ };
568
572
  },
569
573
  },
570
574
  pendingDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.109",
3
+ "version": "0.1.0-alpha.110",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",