@ouro.bot/cli 0.1.0-alpha.101 → 0.1.0-alpha.103

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,20 @@
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.103",
6
+ "changes": [
7
+ "Status-check turns now answer in a fixed live-state shape that names the current conversation, active lane, artifact, latest checkpoint, and next action instead of drifting into broad mission summaries.",
8
+ "Those status turns now hold final streaming until the exact reply is ready and turn report-backs into same-thread obligation updates, so 'I'll report back here' comes back as a real in-thread follow-up instead of hidden background work."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.102",
13
+ "changes": [
14
+ "Live obligations now persist a concrete current artifact and next action, so status answers can anchor on the actual active lane, artifact, and immediate step instead of drifting into broad mission statements.",
15
+ "Obligation-bound coding feedback now turns PR milestones into structured report-backs and lifecycle updates, which keeps the originating live conversation informed when child work opens a PR, waits for merge, or needs a runtime update."
16
+ ]
17
+ },
4
18
  {
5
19
  "version": "0.1.0-alpha.101",
6
20
  "changes": [
@@ -63,6 +63,88 @@ function formatObligationSurface(obligation) {
63
63
  return ` (${obligation.currentSurface.label})`;
64
64
  }
65
65
  }
66
+ function mergeArtifactFallback(obligation) {
67
+ const trimmed = obligation.content.trim();
68
+ if (!trimmed)
69
+ return "the fix";
70
+ const stripped = trimmed.replace(/^merge(?:\s+|$)/i, "").trim();
71
+ return stripped || "the fix";
72
+ }
73
+ function formatMergeArtifact(obligation) {
74
+ const currentArtifact = obligation.currentArtifact?.trim();
75
+ if (currentArtifact)
76
+ return currentArtifact;
77
+ if (obligation.currentSurface?.kind === "merge") {
78
+ const surfaceLabel = obligation.currentSurface.label.trim();
79
+ if (surfaceLabel)
80
+ return surfaceLabel;
81
+ }
82
+ return mergeArtifactFallback(obligation);
83
+ }
84
+ function findPrimaryOpenObligation(frame) {
85
+ return (frame.pendingObligations ?? []).find((ob) => ob.status !== "pending" && ob.status !== "fulfilled")
86
+ ?? (frame.pendingObligations ?? []).find(obligations_1.isOpenObligation)
87
+ ?? null;
88
+ }
89
+ function formatActiveLane(frame, obligation) {
90
+ const liveCodingSession = frame.codingSessions?.[0];
91
+ if (liveCodingSession) {
92
+ return `${formatCodingLaneLabel(liveCodingSession)}${describeCodingSessionScope(liveCodingSession, frame.currentSession)}`;
93
+ }
94
+ if (obligation?.currentSurface?.label) {
95
+ return obligation.currentSurface.label;
96
+ }
97
+ if (frame.inner?.job?.status === "running") {
98
+ return "inner dialog";
99
+ }
100
+ if (typeof frame.currentObligation === "string" && frame.currentObligation.trim().length > 0 && frame.currentSession) {
101
+ return "this same thread";
102
+ }
103
+ return null;
104
+ }
105
+ function formatCurrentArtifact(frame, obligation) {
106
+ if (obligation?.currentArtifact?.trim()) {
107
+ return obligation.currentArtifact.trim();
108
+ }
109
+ if (obligation?.currentSurface?.kind === "merge" && obligation.currentSurface.label.trim()) {
110
+ return obligation.currentSurface.label.trim();
111
+ }
112
+ if ((frame.codingSessions ?? []).length > 0) {
113
+ return "no PR or merge artifact yet";
114
+ }
115
+ if (typeof frame.currentObligation === "string" && frame.currentObligation.trim().length > 0) {
116
+ return "no artifact yet";
117
+ }
118
+ return null;
119
+ }
120
+ function formatNextAction(frame, obligation) {
121
+ if (obligation?.nextAction?.trim()) {
122
+ return obligation.nextAction.trim();
123
+ }
124
+ const liveCodingSession = frame.codingSessions?.[0];
125
+ if (liveCodingSession?.status === "waiting_input") {
126
+ return `answer ${formatCodingLaneLabel(liveCodingSession)} and continue`;
127
+ }
128
+ if (liveCodingSession?.status === "stalled") {
129
+ return `unstick ${formatCodingLaneLabel(liveCodingSession)} and continue`;
130
+ }
131
+ if (liveCodingSession) {
132
+ return "finish the coding pass and bring the result back here";
133
+ }
134
+ if (obligation?.status === "waiting_for_merge") {
135
+ return `wait for checks, merge ${formatMergeArtifact(obligation)}, then update runtime`;
136
+ }
137
+ if (obligation?.status === "updating_runtime") {
138
+ return "update runtime, verify version/changelog, then re-observe";
139
+ }
140
+ if (obligation) {
141
+ return "continue the active loop and bring the result back here";
142
+ }
143
+ if (typeof frame.currentObligation === "string" && frame.currentObligation.trim().length > 0) {
144
+ return `work on "${frame.currentObligation.trim()}" and bring back a concrete artifact`;
145
+ }
146
+ return null;
147
+ }
66
148
  function suggestBridgeForActiveWork(input) {
67
149
  const targetCandidates = (input.targetCandidates ?? [])
68
150
  .filter((candidate) => {
@@ -173,6 +255,10 @@ function buildActiveWorkFrame(input) {
173
255
  }
174
256
  function formatActiveWorkFrame(frame) {
175
257
  const lines = ["## what i'm holding"];
258
+ const primaryObligation = findPrimaryOpenObligation(frame);
259
+ const activeLane = formatActiveLane(frame, primaryObligation);
260
+ const currentArtifact = formatCurrentArtifact(frame, primaryObligation);
261
+ const nextAction = formatNextAction(frame, primaryObligation);
176
262
  // Session line
177
263
  if (frame.currentSession) {
178
264
  let sessionLine = `i'm in a conversation on ${formatSessionLabel(frame.currentSession)}.`;
@@ -189,6 +275,22 @@ function formatActiveWorkFrame(frame) {
189
275
  lines.push("");
190
276
  lines.push("i'm not in a conversation right now.");
191
277
  }
278
+ if (activeLane || currentArtifact || nextAction) {
279
+ lines.push("");
280
+ lines.push("## current concrete state");
281
+ if (frame.currentSession) {
282
+ lines.push(`- live conversation: ${formatSessionLabel(frame.currentSession)}`);
283
+ }
284
+ if (activeLane) {
285
+ lines.push(`- active lane: ${activeLane}`);
286
+ }
287
+ if (currentArtifact) {
288
+ lines.push(`- current artifact: ${currentArtifact}`);
289
+ }
290
+ if (nextAction) {
291
+ lines.push(`- next action: ${nextAction}`);
292
+ }
293
+ }
192
294
  // Inner status block
193
295
  const job = frame.inner?.job;
194
296
  if (job) {
@@ -27,6 +27,7 @@ const azure_1 = require("./providers/azure");
27
27
  const minimax_1 = require("./providers/minimax");
28
28
  const openai_codex_1 = require("./providers/openai-codex");
29
29
  const github_copilot_1 = require("./providers/github-copilot");
30
+ const obligation_steering_1 = require("../mind/obligation-steering");
30
31
  const pending_1 = require("../mind/pending");
31
32
  const identity_2 = require("./identity");
32
33
  const socket_client_1 = require("./daemon/socket-client");
@@ -272,6 +273,37 @@ function getFinalAnswerRetryError(mustResolveBeforeHandoff, intent, sawSteeringF
272
273
  }
273
274
  return null;
274
275
  }
276
+ function hasExactStatusReplyShape(answer) {
277
+ const lines = answer.trimEnd().split(/\r?\n/);
278
+ if (lines.length !== 5)
279
+ return false;
280
+ return (/^live conversation:\s+\S.+$/i.test(lines[0])
281
+ && /^active lane:\s+\S.+$/i.test(lines[1])
282
+ && /^current artifact:\s+\S.+$/i.test(lines[2])
283
+ && /^latest checkpoint:\s+\S.+$/i.test(lines[3])
284
+ && /^next action:\s+\S.+$/i.test(lines[4]));
285
+ }
286
+ function extractLatestCheckpoint(answer) {
287
+ const latestLine = answer.trimEnd().split(/\r?\n/)[3];
288
+ return latestLine.replace(/^latest checkpoint:\s*/i, "").trim();
289
+ }
290
+ function getStatusReplyRetryError(answer, statusCheckRequested, activeWorkFrame) {
291
+ if (!statusCheckRequested || answer == null)
292
+ return null;
293
+ if (hasExactStatusReplyShape(answer))
294
+ return null;
295
+ if (!activeWorkFrame) {
296
+ return `the user asked for current status. call final_answer again using exactly these five non-empty lines and nothing else:
297
+ live conversation: ...
298
+ active lane: ...
299
+ current artifact: ...
300
+ latest checkpoint: ...
301
+ next action: ...`;
302
+ }
303
+ return `the user asked for current status right now.
304
+ ${(0, obligation_steering_1.renderExactStatusReplyContract)(activeWorkFrame, (0, obligation_steering_1.findStatusObligation)(activeWorkFrame))}
305
+ call final_answer again using that exact five-line shape and nothing else.`;
306
+ }
275
307
  // Re-export kick utilities for backward compat
276
308
  var kicks_1 = require("./kicks");
277
309
  Object.defineProperty(exports, "hasToolIntent", { enumerable: true, get: function () { return kicks_1.hasToolIntent; } });
@@ -551,6 +583,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
551
583
  traceId,
552
584
  toolChoiceRequired,
553
585
  reasoningEffort: currentReasoningEffort,
586
+ eagerFinalAnswerStreaming: !options?.statusCheckRequested,
554
587
  });
555
588
  // Track usage from the latest API call
556
589
  if (result.usage)
@@ -599,15 +632,21 @@ async function runAgent(messages, callbacks, channel, signal, options) {
599
632
  // Supports: {"answer":"text","intent":"..."} or "text" (JSON string).
600
633
  const { answer, intent } = parseFinalAnswerPayload(result.toolCalls[0].arguments);
601
634
  const retryError = getFinalAnswerRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp, options?.delegationDecision, sawSendMessageSelf, sawGoInward, sawQuerySession, options?.currentObligation ?? null, options?.activeWorkFrame?.inner?.job, sawExternalStateQuery);
635
+ const statusReplyRetryError = getStatusReplyRetryError(answer, options?.statusCheckRequested, options?.activeWorkFrame);
636
+ const exactStatusReplyAccepted = Boolean(options?.statusCheckRequested) && !statusReplyRetryError && answer != null;
637
+ const deliveredAnswer = exactStatusReplyAccepted && options?.activeWorkFrame
638
+ ? (0, obligation_steering_1.buildExactStatusReply)(options.activeWorkFrame, (0, obligation_steering_1.findStatusObligation)(options.activeWorkFrame), extractLatestCheckpoint(answer))
639
+ : answer;
602
640
  const validDirectReply = mustResolveBeforeHandoffActive && intent === "direct_reply" && sawSteeringFollowUp;
603
641
  const validTerminalIntent = intent === "complete" || intent === "blocked";
604
- const validClosure = answer != null
605
- && !retryError
606
- && (!mustResolveBeforeHandoffActive || validDirectReply || validTerminalIntent);
642
+ const validClosure = deliveredAnswer != null
643
+ && (exactStatusReplyAccepted || !retryError)
644
+ && !statusReplyRetryError
645
+ && (exactStatusReplyAccepted || !mustResolveBeforeHandoffActive || validDirectReply || validTerminalIntent);
607
646
  if (validClosure) {
608
647
  completion = {
609
- answer,
610
- intent: validDirectReply ? "direct_reply" : intent === "blocked" ? "blocked" : "complete",
648
+ answer: deliveredAnswer,
649
+ intent: validDirectReply && !exactStatusReplyAccepted ? "direct_reply" : intent === "blocked" ? "blocked" : "complete",
611
650
  };
612
651
  if (result.finalAnswerStreamed) {
613
652
  // The streaming layer already parsed and emitted the answer
@@ -619,10 +658,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
619
658
  callbacks.onClearText?.();
620
659
  // Emit the answer through the callback pipeline so channels receive it.
621
660
  // Never truncate -- channel adapters handle splitting long messages.
622
- callbacks.onTextChunk(answer);
661
+ callbacks.onTextChunk(deliveredAnswer);
623
662
  }
624
663
  messages.push(msg);
625
- if (validDirectReply) {
664
+ if (validDirectReply && !exactStatusReplyAccepted) {
626
665
  const resumeWork = "direct reply delivered. resume the unresolved obligation now and keep working until you can finish or clearly report that you are blocked.";
627
666
  messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: resumeWork });
628
667
  providerRuntime.appendToolOutput(result.toolCalls[0].id, resumeWork);
@@ -641,8 +680,11 @@ async function runAgent(messages, callbacks, channel, signal, options) {
641
680
  // assistant msg + error tool result and let the model try again.
642
681
  callbacks.onClearText?.();
643
682
  messages.push(msg);
644
- messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: retryError ?? "your final_answer was incomplete or malformed. call final_answer again with your complete response." });
645
- providerRuntime.appendToolOutput(result.toolCalls[0].id, retryError ?? "your final_answer was incomplete or malformed. call final_answer again with your complete response.");
683
+ const toolRetryMessage = statusReplyRetryError
684
+ ?? retryError
685
+ ?? "your final_answer was incomplete or malformed. call final_answer again with your complete response.";
686
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: toolRetryMessage });
687
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, toolRetryMessage);
646
688
  }
647
689
  continue;
648
690
  }
@@ -140,6 +140,12 @@ function advanceObligation(agentRoot, obligationId, update) {
140
140
  if (update.currentSurface) {
141
141
  obligation.currentSurface = update.currentSurface;
142
142
  }
143
+ if (typeof update.currentArtifact === "string") {
144
+ obligation.currentArtifact = update.currentArtifact;
145
+ }
146
+ if (typeof update.nextAction === "string") {
147
+ obligation.nextAction = update.nextAction;
148
+ }
143
149
  if (typeof update.latestNote === "string") {
144
150
  obligation.latestNote = update.latestNote;
145
151
  }
@@ -238,7 +238,7 @@ async function streamAnthropicMessages(client, model, request) {
238
238
  const toolCalls = new Map();
239
239
  const thinkingBlocks = new Map();
240
240
  const redactedBlocks = new Map();
241
- const answerStreamer = new streaming_1.FinalAnswerStreamer(request.callbacks);
241
+ const answerStreamer = new streaming_1.FinalAnswerStreamer(request.callbacks, request.eagerFinalAnswerStreaming);
242
242
  try {
243
243
  for await (const event of response) {
244
244
  if (request.signal?.aborted)
@@ -136,7 +136,7 @@ function createAzureProviderRuntime() {
136
136
  params.metadata = { trace_id: request.traceId };
137
137
  if (request.toolChoiceRequired)
138
138
  params.tool_choice = "required";
139
- const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal);
139
+ const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal, request.eagerFinalAnswerStreaming);
140
140
  for (const item of result.outputItems)
141
141
  nativeInput.push(item);
142
142
  return result;
@@ -88,7 +88,7 @@ function createGithubCopilotProviderRuntime() {
88
88
  if (request.toolChoiceRequired)
89
89
  params.tool_choice = "required";
90
90
  try {
91
- return await (0, streaming_1.streamChatCompletion)(this.client, params, request.callbacks, request.signal);
91
+ return await (0, streaming_1.streamChatCompletion)(this.client, params, request.callbacks, request.signal, request.eagerFinalAnswerStreaming);
92
92
  }
93
93
  catch (error) {
94
94
  throw withAuthGuidance(error);
@@ -135,7 +135,7 @@ function createGithubCopilotProviderRuntime() {
135
135
  if (request.toolChoiceRequired)
136
136
  params.tool_choice = "required";
137
137
  try {
138
- const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal);
138
+ const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal, request.eagerFinalAnswerStreaming);
139
139
  for (const item of result.outputItems)
140
140
  nativeInput.push(item);
141
141
  return result;
@@ -51,7 +51,7 @@ function createMinimaxProviderRuntime() {
51
51
  params.metadata = { trace_id: request.traceId };
52
52
  if (request.toolChoiceRequired)
53
53
  params.tool_choice = "required";
54
- return (0, streaming_1.streamChatCompletion)(this.client, params, request.callbacks, request.signal);
54
+ return (0, streaming_1.streamChatCompletion)(this.client, params, request.callbacks, request.signal, request.eagerFinalAnswerStreaming);
55
55
  },
56
56
  };
57
57
  }
@@ -158,7 +158,7 @@ function createOpenAICodexProviderRuntime() {
158
158
  if (request.toolChoiceRequired)
159
159
  params.tool_choice = "required";
160
160
  try {
161
- const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal);
161
+ const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal, request.eagerFinalAnswerStreaming);
162
162
  for (const item of result.outputItems)
163
163
  nativeInput.push(item);
164
164
  return result;
@@ -84,13 +84,17 @@ class FinalAnswerStreamer {
84
84
  parser = new FinalAnswerParser();
85
85
  _detected = false;
86
86
  callbacks;
87
- constructor(callbacks) {
87
+ enabled;
88
+ constructor(callbacks, enabled = true) {
88
89
  this.callbacks = callbacks;
90
+ this.enabled = enabled;
89
91
  }
90
92
  get detected() { return this._detected; }
91
93
  get streamed() { return this.parser.active; }
92
94
  /** Mark final_answer as detected. Calls onClearText on the callbacks. */
93
95
  activate() {
96
+ if (!this.enabled)
97
+ return;
94
98
  if (this._detected)
95
99
  return;
96
100
  this._detected = true;
@@ -98,6 +102,8 @@ class FinalAnswerStreamer {
98
102
  }
99
103
  /** Feed an argument delta through the parser. Emits text via onTextChunk. */
100
104
  processDelta(delta) {
105
+ if (!this.enabled)
106
+ return;
101
107
  if (!this._detected)
102
108
  return;
103
109
  const text = this.parser.process(delta);
@@ -227,7 +233,7 @@ function toResponsesTools(ccTools) {
227
233
  strict: false,
228
234
  }));
229
235
  }
230
- async function streamChatCompletion(client, createParams, callbacks, signal) {
236
+ async function streamChatCompletion(client, createParams, callbacks, signal, eagerFinalAnswerStreaming = true) {
231
237
  (0, runtime_1.emitNervesEvent)({
232
238
  component: "engine",
233
239
  event: "engine.stream_start",
@@ -241,7 +247,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
241
247
  let toolCalls = {};
242
248
  let streamStarted = false;
243
249
  let usage;
244
- const answerStreamer = new FinalAnswerStreamer(callbacks);
250
+ const answerStreamer = new FinalAnswerStreamer(callbacks, eagerFinalAnswerStreaming);
245
251
  // State machine for parsing inline <think> tags (MiniMax pattern)
246
252
  let contentBuf = "";
247
253
  let inThinkTag = false;
@@ -387,7 +393,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
387
393
  finalAnswerStreamed: answerStreamer.streamed,
388
394
  };
389
395
  }
390
- async function streamResponsesApi(client, createParams, callbacks, signal) {
396
+ async function streamResponsesApi(client, createParams, callbacks, signal, eagerFinalAnswerStreaming = true) {
391
397
  (0, runtime_1.emitNervesEvent)({
392
398
  component: "engine",
393
399
  event: "engine.stream_start",
@@ -402,7 +408,7 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
402
408
  const outputItems = [];
403
409
  let currentToolCall = null;
404
410
  let usage;
405
- const answerStreamer = new FinalAnswerStreamer(callbacks);
411
+ const answerStreamer = new FinalAnswerStreamer(callbacks, eagerFinalAnswerStreaming);
406
412
  let functionCallCount = 0;
407
413
  for await (const event of response) {
408
414
  if (signal?.aborted)
@@ -1,13 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.findActivePersistentObligation = findActivePersistentObligation;
4
+ exports.findStatusObligation = findStatusObligation;
4
5
  exports.renderActiveObligationSteering = renderActiveObligationSteering;
6
+ exports.renderConcreteStatusGuidance = renderConcreteStatusGuidance;
7
+ exports.renderLiveThreadStatusShape = renderLiveThreadStatusShape;
8
+ exports.buildExactStatusReply = buildExactStatusReply;
9
+ exports.renderExactStatusReplyContract = renderExactStatusReplyContract;
5
10
  const runtime_1 = require("../nerves/runtime");
6
11
  function findActivePersistentObligation(frame) {
7
12
  if (!frame)
8
13
  return null;
9
14
  return (frame.pendingObligations ?? []).find((ob) => ob.status !== "pending" && ob.status !== "fulfilled") ?? null;
10
15
  }
16
+ function obligationTimestampMs(obligation) {
17
+ return Date.parse(obligation.updatedAt ?? obligation.createdAt);
18
+ }
19
+ function newestObligationFirst(left, right) {
20
+ return obligationTimestampMs(right) - obligationTimestampMs(left);
21
+ }
22
+ function matchesCurrentSession(frame, obligation) {
23
+ return Boolean(frame.currentSession
24
+ && obligation.origin.friendId === frame.currentSession.friendId
25
+ && obligation.origin.channel === frame.currentSession.channel
26
+ && obligation.origin.key === frame.currentSession.key);
27
+ }
28
+ function findStatusObligation(frame) {
29
+ if (!frame)
30
+ return null;
31
+ const openObligations = [...(frame.pendingObligations ?? [])]
32
+ .filter((obligation) => obligation.status !== "fulfilled")
33
+ .sort(newestObligationFirst);
34
+ const sameSession = openObligations.find((obligation) => matchesCurrentSession(frame, obligation));
35
+ if (sameSession)
36
+ return sameSession;
37
+ return openObligations[0] ?? null;
38
+ }
11
39
  function renderActiveObligationSteering(obligation) {
12
40
  (0, runtime_1.emitNervesEvent)({
13
41
  component: "mind",
@@ -29,3 +57,137 @@ i'm already working on something i owe ${name}.${surfaceLine}
29
57
 
30
58
  i should close that loop before i act like this is a fresh blank turn.`;
31
59
  }
60
+ function mergeArtifactFallback(obligation) {
61
+ const trimmed = obligation.content.trim();
62
+ if (!trimmed)
63
+ return "the fix";
64
+ const stripped = trimmed.replace(/^merge(?:\s+|$)/i, "").trim();
65
+ return stripped || "the fix";
66
+ }
67
+ function formatMergeArtifact(obligation) {
68
+ const currentArtifact = obligation.currentArtifact?.trim();
69
+ if (currentArtifact)
70
+ return currentArtifact;
71
+ if (obligation.currentSurface?.kind === "merge") {
72
+ const surfaceLabel = obligation.currentSurface.label.trim();
73
+ if (surfaceLabel)
74
+ return surfaceLabel;
75
+ }
76
+ return mergeArtifactFallback(obligation);
77
+ }
78
+ function formatActiveLane(frame, obligation) {
79
+ const liveCodingSession = frame.codingSessions?.[0];
80
+ if (liveCodingSession) {
81
+ const sameThread = frame.currentSession
82
+ && liveCodingSession.originSession
83
+ && liveCodingSession.originSession.friendId === frame.currentSession.friendId
84
+ && liveCodingSession.originSession.channel === frame.currentSession.channel
85
+ && liveCodingSession.originSession.key === frame.currentSession.key;
86
+ return sameThread
87
+ ? `${liveCodingSession.runner} ${liveCodingSession.id} for this same thread`
88
+ : liveCodingSession.originSession
89
+ ? `${liveCodingSession.runner} ${liveCodingSession.id} for ${liveCodingSession.originSession.channel}/${liveCodingSession.originSession.key}`
90
+ : `${liveCodingSession.runner} ${liveCodingSession.id}`;
91
+ }
92
+ return obligation.currentSurface?.label
93
+ || (frame.currentObligation?.trim() ? "this same thread" : "this live loop");
94
+ }
95
+ function formatCurrentArtifact(frame, obligation) {
96
+ if (obligation?.currentArtifact)
97
+ return obligation.currentArtifact;
98
+ if (obligation?.currentSurface?.kind === "merge")
99
+ return obligation.currentSurface.label;
100
+ if (frame.currentObligation?.trim())
101
+ return "no artifact yet";
102
+ if ((frame.codingSessions ?? []).length > 0)
103
+ return "no PR or merge artifact yet";
104
+ return obligation ? "no explicit artifact yet" : "";
105
+ }
106
+ function isStatusCheckPrompt(text) {
107
+ const trimmed = text?.trim();
108
+ if (!trimmed)
109
+ return false;
110
+ return /^(what are you doing|what(?:'|’)s your status|status|status update|what changed|where are you at|where things stand)\??$/i.test(trimmed);
111
+ }
112
+ function formatNextAction(frame, obligation) {
113
+ if (obligation?.nextAction)
114
+ return obligation.nextAction;
115
+ const currentObligation = frame.currentObligation?.trim() ?? "";
116
+ const statusCheckPrompt = isStatusCheckPrompt(currentObligation);
117
+ const liveCodingSession = frame.codingSessions?.[0];
118
+ if (liveCodingSession?.status === "waiting_input") {
119
+ return `answer ${liveCodingSession.runner} ${liveCodingSession.id} and continue`;
120
+ }
121
+ if (liveCodingSession?.status === "stalled") {
122
+ return `unstick ${liveCodingSession.runner} ${liveCodingSession.id} and continue`;
123
+ }
124
+ if (liveCodingSession) {
125
+ return "finish the coding pass and bring the result back here";
126
+ }
127
+ if (obligation?.status === "waiting_for_merge") {
128
+ return `wait for checks, merge ${formatMergeArtifact(obligation)}, then update runtime`;
129
+ }
130
+ if (obligation?.status === "updating_runtime") {
131
+ return "update runtime, verify version/changelog, then re-observe";
132
+ }
133
+ if (currentObligation && !statusCheckPrompt) {
134
+ return `work on "${currentObligation}" and bring back a concrete artifact`;
135
+ }
136
+ return obligation ? "continue the active loop and bring the result back here" : "";
137
+ }
138
+ function renderConcreteStatusGuidance(frame, obligation) {
139
+ const activeLane = obligation ? formatActiveLane(frame, obligation) : (frame.currentObligation?.trim() ? "this same thread" : "");
140
+ const currentArtifact = formatCurrentArtifact(frame, obligation);
141
+ const nextAction = formatNextAction(frame, obligation);
142
+ const liveConversation = frame.currentSession
143
+ ? `${frame.currentSession.channel}/${frame.currentSession.key}`
144
+ : "";
145
+ if (!activeLane && !currentArtifact && !nextAction)
146
+ return "";
147
+ return `if someone asks what i'm doing or for status mid-task, i answer from these live facts instead of copying a canned block.
148
+ the live conversation is ${liveConversation || "not in a live conversation"}.
149
+ the active lane is ${activeLane}.
150
+ the current artifact is ${currentArtifact}.
151
+ if i just finished or verified something concrete in this live lane, i name that as the latest checkpoint.
152
+ the next action is ${nextAction}.
153
+
154
+ i use those facts to answer naturally unless this turn is an explicit direct status check, where the separate exact five-line contract applies.`;
155
+ }
156
+ function renderLiveThreadStatusShape(frame) {
157
+ if (!frame.currentSession)
158
+ return "";
159
+ return `if someone asks what i'm doing or for status mid-task in this live thread, i answer in these exact lines, in order, with no intro paragraph:
160
+ live conversation: ${frame.currentSession.channel}/${frame.currentSession.key}
161
+ active lane: this same thread
162
+ current artifact: <actual artifact or "no artifact yet">
163
+ latest checkpoint: <freshest concrete thing i just finished or verified>
164
+ next action: <smallest concrete next step i'm taking now>
165
+
166
+ no recap paragraph before those lines.
167
+ no option list.
168
+ present tense only.
169
+ if a finished step matters, i label it "just finished" instead of presenting it as current work.`;
170
+ }
171
+ function buildExactStatusReply(frame, obligation, latestCheckpoint) {
172
+ const liveConversation = frame.currentSession
173
+ ? `${frame.currentSession.channel}/${frame.currentSession.key}`
174
+ : "not in a live conversation";
175
+ const activeLane = obligation
176
+ ? formatActiveLane(frame, obligation)
177
+ : (frame.currentSession ? "this same thread" : "this live loop");
178
+ const currentArtifact = formatCurrentArtifact(frame, obligation) || 'no artifact yet';
179
+ const nextAction = formatNextAction(frame, obligation) || "continue the active loop and bring the result back here";
180
+ const latest = latestCheckpoint.trim() || "<freshest concrete thing i just finished or verified>";
181
+ return [
182
+ `live conversation: ${liveConversation}`,
183
+ `active lane: ${activeLane}`,
184
+ `current artifact: ${currentArtifact}`,
185
+ `latest checkpoint: ${latest}`,
186
+ `next action: ${nextAction}`,
187
+ ].join("\n");
188
+ }
189
+ function renderExactStatusReplyContract(frame, obligation) {
190
+ return `reply using exactly these five lines and nothing else:
191
+ ${buildExactStatusReply(frame, obligation, "<freshest concrete thing i just finished or verified>")}
192
+ `;
193
+ }
@@ -464,13 +464,18 @@ function centerOfGravitySteeringSection(channel, options) {
464
464
  if (!frame)
465
465
  return "";
466
466
  const cog = frame.centerOfGravity;
467
- if (cog === "local-turn")
468
- return "";
469
467
  const job = frame.inner?.job;
470
468
  const activeObligation = (0, obligation_steering_1.findActivePersistentObligation)(frame);
469
+ const statusObligation = (0, obligation_steering_1.findStatusObligation)(frame);
470
+ const genericConcreteStatus = (0, obligation_steering_1.renderConcreteStatusGuidance)(frame, statusObligation);
471
+ if (cog === "local-turn") {
472
+ return genericConcreteStatus || (0, obligation_steering_1.renderLiveThreadStatusShape)(frame);
473
+ }
471
474
  if (cog === "inward-work") {
472
475
  if (activeObligation) {
473
- return (0, obligation_steering_1.renderActiveObligationSteering)(activeObligation);
476
+ return `${(0, obligation_steering_1.renderActiveObligationSteering)(activeObligation)}
477
+
478
+ ${genericConcreteStatus}`;
474
479
  }
475
480
  if (job?.status === "queued" || job?.status === "running") {
476
481
  const originClause = job.origin
@@ -512,6 +517,9 @@ i already have coding work running in ${liveCodingSession.runner} ${liveCodingSe
512
517
 
513
518
  i should orient around that live lane first, then decide what still needs to come back here.`;
514
519
  }
520
+ if (genericConcreteStatus) {
521
+ return genericConcreteStatus;
522
+ }
515
523
  return `## where my attention is
516
524
  i have unfinished work that needs attention before i move on.
517
525
 
@@ -527,6 +535,17 @@ i should keep the different sides aligned. what i learn here may matter there, a
527
535
  /* v8 ignore next -- unreachable: all center-of-gravity modes covered above @preserve */
528
536
  return "";
529
537
  }
538
+ function statusCheckSection(channel, options) {
539
+ if (channel === "inner" || !options?.statusCheckRequested)
540
+ return "";
541
+ const frame = options.activeWorkFrame;
542
+ if (!frame)
543
+ return "";
544
+ const activeObligation = (0, obligation_steering_1.findStatusObligation)(frame);
545
+ return `## status question on this turn
546
+ the user is asking for current status right now.
547
+ ${(0, obligation_steering_1.renderExactStatusReplyContract)(frame, activeObligation)}`;
548
+ }
530
549
  function commitmentsSection(options) {
531
550
  if (!options?.activeWorkFrame)
532
551
  return "";
@@ -734,6 +753,7 @@ async function buildSystem(channel = "cli", options, context) {
734
753
  skillsSection(),
735
754
  taskBoardSection(),
736
755
  activeWorkSection(options),
756
+ statusCheckSection(channel, options),
737
757
  centerOfGravitySteeringSection(channel, options),
738
758
  commitmentsSection(options),
739
759
  delegationHintSection(options),
@@ -14,6 +14,8 @@ const OBLIGATION_WAKE_UPDATE_KINDS = new Set([
14
14
  "failed",
15
15
  "killed",
16
16
  ]);
17
+ const PULL_REQUEST_NUMBER_PATTERN = /\bPR\s*#(\d+)\b/i;
18
+ const PULL_REQUEST_URL_PATTERN = /\/pull\/(\d+)(?:\b|\/)?/i;
17
19
  function clip(text, maxLength = 280) {
18
20
  const trimmed = text.trim();
19
21
  if (trimmed.length <= maxLength)
@@ -57,17 +59,76 @@ function formatSessionLabel(session) {
57
59
  : "";
58
60
  return `${session.runner} ${session.id}${origin}`;
59
61
  }
62
+ function extractPullRequestLabel(snippet) {
63
+ if (!snippet)
64
+ return null;
65
+ const numberMatch = snippet.match(PULL_REQUEST_NUMBER_PATTERN);
66
+ if (numberMatch)
67
+ return `PR #${numberMatch[1]}`;
68
+ const urlMatch = snippet.match(PULL_REQUEST_URL_PATTERN);
69
+ if (urlMatch)
70
+ return `PR #${urlMatch[1]}`;
71
+ return null;
72
+ }
73
+ function isMergedPullRequestSnippet(snippet) {
74
+ return /\bmerged\b/i.test(snippet) || /\blanded\b/i.test(snippet);
75
+ }
76
+ function deriveObligationMilestone(update) {
77
+ const snippet = pickUpdateSnippet(update);
78
+ const pullRequest = extractPullRequestLabel(snippet);
79
+ if (update.kind === "completed" && snippet && pullRequest && isMergedPullRequestSnippet(snippet)) {
80
+ return {
81
+ status: "updating_runtime",
82
+ currentSurface: { kind: "runtime", label: "ouro up" },
83
+ currentArtifact: pullRequest,
84
+ nextAction: "update runtime, verify version/changelog, then re-observe",
85
+ };
86
+ }
87
+ if (update.kind === "completed" && pullRequest) {
88
+ return {
89
+ status: "waiting_for_merge",
90
+ currentSurface: { kind: "merge", label: pullRequest },
91
+ currentArtifact: pullRequest,
92
+ nextAction: `wait for checks, merge ${pullRequest}, then update runtime`,
93
+ };
94
+ }
95
+ if (update.kind === "waiting_input") {
96
+ return {
97
+ status: "investigating",
98
+ currentSurface: { kind: "coding", label: `${update.session.runner} ${update.session.id}` },
99
+ nextAction: `answer ${update.session.runner} ${update.session.id} and continue`,
100
+ };
101
+ }
102
+ if (update.kind === "stalled") {
103
+ return {
104
+ status: "investigating",
105
+ currentSurface: { kind: "coding", label: `${update.session.runner} ${update.session.id}` },
106
+ nextAction: `unstick ${update.session.runner} ${update.session.id} and continue`,
107
+ };
108
+ }
109
+ if (update.kind === "progress" || update.kind === "spawned" || update.kind === "failed" || update.kind === "killed" || update.kind === "completed") {
110
+ return {
111
+ status: "investigating",
112
+ currentSurface: { kind: "coding", label: `${update.session.runner} ${update.session.id}` },
113
+ };
114
+ }
115
+ return null;
116
+ }
60
117
  function isSafeProgressSnippet(snippet) {
118
+ const normalized = snippet.trim();
61
119
  const wordCount = snippet.split(/\s+/).filter(Boolean).length;
62
- return (snippet.length <= 80
120
+ return (normalized.length <= 80
121
+ && wordCount >= 2
63
122
  && wordCount <= 8
64
- && !snippet.includes(":")
65
- && !snippet.startsWith("**")
66
- && !/^Respond with\b/i.test(snippet)
67
- && !/^Coding session metadata\b/i.test(snippet)
68
- && !/^sessionId\b/i.test(snippet)
69
- && !/^taskRef\b/i.test(snippet)
70
- && !/^parentAgent\b/i.test(snippet));
123
+ && /[A-Za-z]{3,}/.test(normalized)
124
+ && !normalized.includes(":")
125
+ && !/[{}\[\]();]/.test(normalized)
126
+ && !normalized.startsWith("**")
127
+ && !/^Respond with\b/i.test(normalized)
128
+ && !/^Coding session metadata\b/i.test(normalized)
129
+ && !/^sessionId\b/i.test(normalized)
130
+ && !/^taskRef\b/i.test(normalized)
131
+ && !/^parentAgent\b/i.test(normalized));
71
132
  }
72
133
  function pickUpdateSnippet(update) {
73
134
  return (lastMeaningfulLine(update.text)
@@ -94,6 +155,22 @@ function formatUpdateMessage(update) {
94
155
  return `${label} started`;
95
156
  }
96
157
  }
158
+ function formatReportBackMessage(update, baseMessage) {
159
+ if (!baseMessage)
160
+ return null;
161
+ if (!update.session.obligationId || !update.session.originSession) {
162
+ return baseMessage;
163
+ }
164
+ const milestone = deriveObligationMilestone(update);
165
+ const extraLines = [];
166
+ if (milestone?.currentArtifact) {
167
+ extraLines.push(`current artifact: ${milestone.currentArtifact}`);
168
+ }
169
+ if (milestone?.nextAction) {
170
+ extraLines.push(`next: ${milestone.nextAction}`);
171
+ }
172
+ return extraLines.length > 0 ? `${baseMessage}\n${extraLines.join("\n")}` : baseMessage;
173
+ }
97
174
  function obligationNoteFromUpdate(update) {
98
175
  const snippet = pickUpdateSnippet(update);
99
176
  switch (update.kind) {
@@ -121,10 +198,13 @@ function syncObligationFromUpdate(update) {
121
198
  const obligationId = update.session.obligationId;
122
199
  if (!obligationId)
123
200
  return;
201
+ const milestone = deriveObligationMilestone(update);
124
202
  try {
125
203
  (0, obligations_1.advanceObligation)((0, identity_1.getAgentRoot)(), obligationId, {
126
- status: "investigating",
127
- currentSurface: { kind: "coding", label: `${update.session.runner} ${update.session.id}` },
204
+ status: milestone?.status ?? "investigating",
205
+ currentSurface: milestone?.currentSurface ?? { kind: "coding", label: `${update.session.runner} ${update.session.id}` },
206
+ currentArtifact: milestone?.currentArtifact,
207
+ nextAction: milestone?.nextAction,
128
208
  latestNote: obligationNoteFromUpdate(update) ?? undefined,
129
209
  });
130
210
  }
@@ -193,10 +273,10 @@ function attachCodingSessionFeedback(manager, session, target) {
193
273
  };
194
274
  const spawnedUpdate = { kind: "spawned", session };
195
275
  syncObligationFromUpdate(spawnedUpdate);
196
- sendMessage(formatUpdateMessage(spawnedUpdate));
276
+ sendMessage(formatReportBackMessage(spawnedUpdate, formatUpdateMessage(spawnedUpdate)));
197
277
  unsubscribe = manager.subscribe(session.id, async (update) => {
198
278
  syncObligationFromUpdate(update);
199
- sendMessage(formatUpdateMessage(update));
279
+ sendMessage(formatReportBackMessage(update, formatUpdateMessage(update)));
200
280
  await wakeInnerDialogForObligation(update);
201
281
  if (TERMINAL_UPDATE_KINDS.has(update.kind)) {
202
282
  closed = true;
@@ -99,6 +99,9 @@ function selectCodingStatusSessions(sessions, currentSession) {
99
99
  }
100
100
  return [...sessions].sort(latestSessionFirst);
101
101
  }
102
+ function buildCodingObligationContent(taskRef) {
103
+ return `finish ${taskRef} and bring the result back`;
104
+ }
102
105
  const codingSpawnTool = {
103
106
  type: "function",
104
107
  function: {
@@ -231,6 +234,13 @@ exports.codingToolDefinitions = [
231
234
  }
232
235
  return JSON.stringify({ ...existingSession, reused: true });
233
236
  }
237
+ if (request.originSession && !request.obligationId) {
238
+ const created = (0, obligations_1.createObligation)((0, identity_1.getAgentRoot)(), {
239
+ origin: request.originSession,
240
+ content: buildCodingObligationContent(taskRef),
241
+ });
242
+ request.obligationId = created.id;
243
+ }
234
244
  const session = await manager.spawnSession(request);
235
245
  if (session.obligationId) {
236
246
  (0, obligations_1.advanceObligation)((0, identity_1.getAgentRoot)(), session.obligationId, {
@@ -38,6 +38,23 @@ function emptyTaskBoard() {
38
38
  activeBridges: [],
39
39
  };
40
40
  }
41
+ const STATUS_CHECK_PATTERNS = [
42
+ /^\s*what are you doing\??\s*$/i,
43
+ /^\s*what'?s your status\??\s*$/i,
44
+ /^\s*status\??\s*$/i,
45
+ /^\s*status update\??\s*$/i,
46
+ /^\s*what changed\??\s*$/i,
47
+ /^\s*where (?:are you at|things stand)\??\s*$/i,
48
+ ];
49
+ function isStatusCheckRequested(ingressTexts) {
50
+ const latest = ingressTexts
51
+ ?.map((text) => text.trim())
52
+ .filter((text) => text.length > 0)
53
+ .at(-1);
54
+ if (!latest)
55
+ return false;
56
+ return STATUS_CHECK_PATTERNS.some((pattern) => pattern.test(latest));
57
+ }
41
58
  function readInnerWorkState() {
42
59
  const defaultJob = {
43
60
  status: "idle",
@@ -271,6 +288,7 @@ async function handleInboundTurn(input) {
271
288
  }
272
289
  // Step 5: runAgent
273
290
  const existingToolContext = input.runAgentOptions?.toolContext;
291
+ const statusCheckRequested = isStatusCheckRequested(input.continuityIngressTexts);
274
292
  const runAgentOptions = {
275
293
  ...input.runAgentOptions,
276
294
  bridgeContext,
@@ -278,6 +296,8 @@ async function handleInboundTurn(input) {
278
296
  delegationDecision,
279
297
  currentSessionKey: currentSession.key,
280
298
  currentObligation,
299
+ statusCheckRequested,
300
+ toolChoiceRequired: statusCheckRequested ? true : input.runAgentOptions?.toolChoiceRequired,
281
301
  mustResolveBeforeHandoff,
282
302
  setMustResolveBeforeHandoff: (value) => {
283
303
  mustResolveBeforeHandoff = value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.101",
3
+ "version": "0.1.0-alpha.103",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",