@ouro.bot/cli 0.1.0-alpha.82 → 0.1.0-alpha.84

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,21 @@
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.84",
6
+ "changes": [
7
+ "Safe workspace routing now covers repo-local shell commands too, so agents that discover a harness friction through `shell` land in the dedicated worktree instead of wandering in the shared checkout.",
8
+ "The new `safe_workspace` tool and prompt guidance make the chosen workspace path, branch, and first concrete repo action explicit before the first edit, tightening the visible OODA loop."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.83",
13
+ "changes": [
14
+ "Visible OODA loop substrate: persistent return obligations now surface where work is happening, whether the agent is still investigating, and what still needs to come back to the originating session.",
15
+ "Coding-session provenance now carries the originating human-facing session and obligation through spawn, persistence, and feedback, so hidden coding work is legible in active work instead of disappearing behind a tool session.",
16
+ "Runtime closure is explicit after self-fix updates: `ouro up` now prints a changelog follow-up command, and the runtime prompt reminds the agent to report that it updated and review what changed before treating the loop as closed."
17
+ ]
18
+ },
4
19
  {
5
20
  "version": "0.1.0-alpha.82",
6
21
  "changes": [
@@ -5,6 +5,7 @@ exports.buildActiveWorkFrame = buildActiveWorkFrame;
5
5
  exports.formatActiveWorkFrame = formatActiveWorkFrame;
6
6
  const runtime_1 = require("../nerves/runtime");
7
7
  const state_machine_1 = require("./bridges/state-machine");
8
+ const obligations_1 = require("./obligations");
8
9
  const target_resolution_1 = require("./target-resolution");
9
10
  function activityPriority(source) {
10
11
  return source === "friend-facing" ? 0 : 1;
@@ -31,6 +32,23 @@ function hasSharedObligationPressure(input) {
31
32
  && input.currentObligation.trim().length > 0) || input.mustResolveBeforeHandoff
32
33
  || summarizeLiveTasks(input.taskBoard).length > 0;
33
34
  }
35
+ function activeObligationCount(obligations) {
36
+ return (obligations ?? []).filter((ob) => (0, obligations_1.isOpenObligationStatus)(ob.status)).length;
37
+ }
38
+ function formatObligationSurface(obligation) {
39
+ if (!obligation.currentSurface?.label)
40
+ return "";
41
+ switch (obligation.status) {
42
+ case "investigating":
43
+ return ` (working in ${obligation.currentSurface.label})`;
44
+ case "waiting_for_merge":
45
+ return ` (waiting at ${obligation.currentSurface.label})`;
46
+ case "updating_runtime":
47
+ return ` (updating via ${obligation.currentSurface.label})`;
48
+ default:
49
+ return ` (${obligation.currentSurface.label})`;
50
+ }
51
+ }
34
52
  function suggestBridgeForActiveWork(input) {
35
53
  const targetCandidates = (input.targetCandidates ?? [])
36
54
  .filter((candidate) => {
@@ -87,9 +105,10 @@ function buildActiveWorkFrame(input) {
87
105
  : [];
88
106
  const liveTaskNames = summarizeLiveTasks(input.taskBoard);
89
107
  const activeBridgePresent = input.bridges.some(isActiveBridge);
108
+ const openObligations = activeObligationCount(input.pendingObligations);
90
109
  const centerOfGravity = activeBridgePresent
91
110
  ? "shared-work"
92
- : (input.inner.status === "running" || input.inner.hasPending || input.mustResolveBeforeHandoff)
111
+ : (input.inner.status === "running" || input.inner.hasPending || input.mustResolveBeforeHandoff || openObligations > 0)
93
112
  ? "inward-work"
94
113
  : "local-turn";
95
114
  const frame = {
@@ -129,6 +148,7 @@ function buildActiveWorkFrame(input) {
129
148
  bridges: frame.bridges.length,
130
149
  liveTasks: frame.taskPressure.liveTaskNames.length,
131
150
  liveSessions: frame.friendActivity.otherLiveSessionsForCurrentFriend.length,
151
+ pendingObligations: openObligations,
132
152
  hasBridgeSuggestion: frame.bridgeSuggestion !== null,
133
153
  },
134
154
  });
@@ -204,6 +224,19 @@ function formatActiveWorkFrame(frame) {
204
224
  lines.push("");
205
225
  lines.push(targetCandidatesBlock);
206
226
  }
227
+ if ((frame.pendingObligations ?? []).length > 0) {
228
+ lines.push("");
229
+ lines.push("## return obligations");
230
+ for (const obligation of frame.pendingObligations) {
231
+ if (!(0, obligations_1.isOpenObligationStatus)(obligation.status))
232
+ continue;
233
+ let obligationLine = `- [${obligation.status}] ${obligation.origin.friendId}/${obligation.origin.channel}/${obligation.origin.key}: ${obligation.content}${formatObligationSurface(obligation)}`;
234
+ if (obligation.latestNote?.trim()) {
235
+ obligationLine += `\n latest: ${obligation.latestNote.trim()}`;
236
+ }
237
+ lines.push(obligationLine);
238
+ }
239
+ }
207
240
  // Bridge suggestion
208
241
  if (frame.bridgeSuggestion) {
209
242
  lines.push("");
@@ -2,17 +2,37 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.deriveCommitments = deriveCommitments;
4
4
  exports.formatCommitments = formatCommitments;
5
+ const obligations_1 = require("./obligations");
5
6
  const runtime_1 = require("../nerves/runtime");
7
+ function describeActiveObligation(obligation) {
8
+ if (obligation.status === "pending") {
9
+ return `i owe ${obligation.origin.friendId}: ${obligation.content}`;
10
+ }
11
+ const surface = obligation.currentSurface?.label;
12
+ const statusText = obligation.status.replaceAll("_", " ");
13
+ if (surface) {
14
+ return `i owe ${obligation.origin.friendId}: ${obligation.content} (${statusText} in ${surface})`;
15
+ }
16
+ return `i owe ${obligation.origin.friendId}: ${obligation.content} (${statusText})`;
17
+ }
6
18
  function deriveCommitments(activeWorkFrame, innerJob, pendingObligations) {
7
19
  const committedTo = [];
8
20
  const completionCriteria = [];
9
21
  const safeToIgnore = [];
10
22
  // Persistent obligations from the obligation store
11
23
  if (pendingObligations && pendingObligations.length > 0) {
24
+ let hasAdvancedObligation = false;
12
25
  for (const ob of pendingObligations) {
13
- committedTo.push(`i owe ${ob.origin.friendId}: ${ob.content}`);
26
+ if (!(0, obligations_1.isOpenObligationStatus)(ob.status))
27
+ continue;
28
+ committedTo.push(describeActiveObligation(ob));
29
+ if (ob.status !== "pending")
30
+ hasAdvancedObligation = true;
14
31
  }
15
32
  completionCriteria.push("fulfill my outstanding obligations");
33
+ if (hasAdvancedObligation) {
34
+ completionCriteria.push("close my active obligation loops");
35
+ }
16
36
  }
17
37
  // Obligation (from current turn -- kept for backward compat)
18
38
  if (typeof activeWorkFrame.currentObligation === "string" && activeWorkFrame.currentObligation.trim().length > 0) {
@@ -1828,6 +1828,11 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1828
1828
  await deps.installCliVersion(updateResult.latestVersion);
1829
1829
  deps.activateCliVersion(updateResult.latestVersion);
1830
1830
  deps.writeStdout(`ouro updated to ${updateResult.latestVersion} (was ${currentVersion})`);
1831
+ const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(currentVersion, updateResult.latestVersion);
1832
+ /* v8 ignore next -- buildChangelogCommand is non-null when an actual newer version is installed @preserve */
1833
+ if (changelogCommand) {
1834
+ deps.writeStdout(`review changes with: ${changelogCommand}`);
1835
+ }
1831
1836
  pendingReExec = true;
1832
1837
  }
1833
1838
  /* v8 ignore start -- update check error: tested via daemon-cli-update-flow.test.ts @preserve */
@@ -1873,6 +1878,11 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1873
1878
  /* v8 ignore start -- CLI update detection: tested via daemon-cli-version-detect.test.ts @preserve */
1874
1879
  if (previousCliVersion && previousCliVersion !== currentVersion) {
1875
1880
  deps.writeStdout(`ouro updated to ${currentVersion} (was ${previousCliVersion})`);
1881
+ const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(previousCliVersion, currentVersion);
1882
+ /* v8 ignore next -- buildChangelogCommand is non-null when previous/current runtime versions differ @preserve */
1883
+ if (changelogCommand) {
1884
+ deps.writeStdout(`review changes with: ${changelogCommand}`);
1885
+ }
1876
1886
  }
1877
1887
  /* v8 ignore stop */
1878
1888
  if (updateSummary.updated.length > 0) {
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getOuroCliHome = getOuroCliHome;
37
37
  exports.getCurrentVersion = getCurrentVersion;
38
38
  exports.getPreviousVersion = getPreviousVersion;
39
+ exports.buildChangelogCommand = buildChangelogCommand;
39
40
  exports.listInstalledVersions = listInstalledVersions;
40
41
  exports.installVersion = installVersion;
41
42
  exports.activateVersion = activateVersion;
@@ -73,6 +74,12 @@ function getPreviousVersion(deps) {
73
74
  return null;
74
75
  }
75
76
  }
77
+ function buildChangelogCommand(previousVersion, currentVersion) {
78
+ if (!previousVersion || !currentVersion || previousVersion === currentVersion) {
79
+ return null;
80
+ }
81
+ return `ouro changelog --from ${previousVersion}`;
82
+ }
76
83
  function listInstalledVersions(deps) {
77
84
  const cliHome = getOuroCliHome(deps.homeDir);
78
85
  /* v8 ignore next -- dep default: tests always inject @preserve */
@@ -33,9 +33,12 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isOpenObligationStatus = isOpenObligationStatus;
37
+ exports.isOpenObligation = isOpenObligation;
36
38
  exports.createObligation = createObligation;
37
39
  exports.readObligations = readObligations;
38
40
  exports.readPendingObligations = readPendingObligations;
41
+ exports.advanceObligation = advanceObligation;
39
42
  exports.fulfillObligation = fulfillObligation;
40
43
  exports.findPendingObligationForOrigin = findPendingObligationForOrigin;
41
44
  const fs = __importStar(require("fs"));
@@ -52,7 +55,14 @@ function generateId() {
52
55
  const random = Math.random().toString(36).slice(2, 10);
53
56
  return `${timestamp}-${random}`;
54
57
  }
58
+ function isOpenObligationStatus(status) {
59
+ return status !== "fulfilled";
60
+ }
61
+ function isOpenObligation(obligation) {
62
+ return isOpenObligationStatus(obligation.status);
63
+ }
55
64
  function createObligation(agentRoot, input) {
65
+ const now = new Date().toISOString();
56
66
  const id = generateId();
57
67
  const obligation = {
58
68
  id,
@@ -60,7 +70,8 @@ function createObligation(agentRoot, input) {
60
70
  ...(input.bridgeId ? { bridgeId: input.bridgeId } : {}),
61
71
  content: input.content,
62
72
  status: "pending",
63
- createdAt: new Date().toISOString(),
73
+ createdAt: now,
74
+ updatedAt: now,
64
75
  };
65
76
  const dir = obligationsDir(agentRoot);
66
77
  fs.mkdirSync(dir, { recursive: true });
@@ -107,9 +118,9 @@ function readObligations(agentRoot) {
107
118
  return obligations;
108
119
  }
109
120
  function readPendingObligations(agentRoot) {
110
- return readObligations(agentRoot).filter((ob) => ob.status === "pending");
121
+ return readObligations(agentRoot).filter(isOpenObligation);
111
122
  }
112
- function fulfillObligation(agentRoot, obligationId) {
123
+ function advanceObligation(agentRoot, obligationId, update) {
113
124
  const filePath = obligationFilePath(agentRoot, obligationId);
114
125
  let obligation;
115
126
  try {
@@ -119,9 +130,48 @@ function fulfillObligation(agentRoot, obligationId) {
119
130
  catch {
120
131
  return;
121
132
  }
122
- obligation.status = "fulfilled";
123
- obligation.fulfilledAt = new Date().toISOString();
133
+ const previousStatus = obligation.status;
134
+ if (update.status) {
135
+ obligation.status = update.status;
136
+ if (update.status === "fulfilled") {
137
+ obligation.fulfilledAt = new Date().toISOString();
138
+ }
139
+ }
140
+ if (update.currentSurface) {
141
+ obligation.currentSurface = update.currentSurface;
142
+ }
143
+ if (typeof update.latestNote === "string") {
144
+ obligation.latestNote = update.latestNote;
145
+ }
146
+ obligation.updatedAt = new Date().toISOString();
124
147
  fs.writeFileSync(filePath, JSON.stringify(obligation, null, 2), "utf-8");
148
+ (0, runtime_1.emitNervesEvent)({
149
+ component: "engine",
150
+ event: "engine.obligation_advanced",
151
+ message: "obligation advanced",
152
+ meta: {
153
+ obligationId,
154
+ previousStatus,
155
+ status: obligation.status,
156
+ friendId: obligation.origin.friendId,
157
+ channel: obligation.origin.channel,
158
+ key: obligation.origin.key,
159
+ surfaceKind: obligation.currentSurface?.kind ?? null,
160
+ surfaceLabel: obligation.currentSurface?.label ?? null,
161
+ },
162
+ });
163
+ }
164
+ function fulfillObligation(agentRoot, obligationId) {
165
+ advanceObligation(agentRoot, obligationId, { status: "fulfilled" });
166
+ const filePath = obligationFilePath(agentRoot, obligationId);
167
+ let obligation;
168
+ try {
169
+ const raw = fs.readFileSync(filePath, "utf-8");
170
+ obligation = JSON.parse(raw);
171
+ }
172
+ catch {
173
+ return;
174
+ }
125
175
  (0, runtime_1.emitNervesEvent)({
126
176
  component: "engine",
127
177
  event: "engine.obligation_fulfilled",
@@ -37,6 +37,7 @@ exports.resetSafeWorkspaceSelection = resetSafeWorkspaceSelection;
37
37
  exports.getActiveSafeWorkspaceSelection = getActiveSafeWorkspaceSelection;
38
38
  exports.ensureSafeRepoWorkspace = ensureSafeRepoWorkspace;
39
39
  exports.resolveSafeRepoPath = resolveSafeRepoPath;
40
+ exports.resolveSafeShellExecution = resolveSafeShellExecution;
40
41
  const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
42
43
  const child_process_1 = require("child_process");
@@ -96,7 +97,7 @@ function createDedicatedWorktree(repoRoot, workspaceRoot, branchSuffix, existsSy
96
97
  rmSync(workspaceRoot, { recursive: true, force: true });
97
98
  }
98
99
  assertGitOk(runGit(repoRoot, ["worktree", "add", "-B", branchName, workspaceRoot, "origin/main"], spawnSync), "git worktree add");
99
- return { workspaceRoot, created: true };
100
+ return { workspaceRoot, created: true, branchName };
100
101
  }
101
102
  function createScratchClone(workspaceRoot, cloneUrl, existsSync, mkdirSync, rmSync, spawnSync) {
102
103
  mkdirSync(path.dirname(workspaceRoot), { recursive: true });
@@ -107,7 +108,11 @@ function createScratchClone(workspaceRoot, cloneUrl, existsSync, mkdirSync, rmSy
107
108
  stdio: ["ignore", "pipe", "pipe"],
108
109
  });
109
110
  assertGitOk(result, "git clone");
110
- return { workspaceRoot, created: true };
111
+ return { workspaceRoot, created: true, branchName: "main" };
112
+ }
113
+ const REPO_LOCAL_SHELL_COMMAND = /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(git|npm|npx|node|pnpm|yarn|bun|rg|sed|cat|ls|find|grep|vitest|tsc|eslint)\b/;
114
+ function looksRepoLocalShellCommand(command) {
115
+ return REPO_LOCAL_SHELL_COMMAND.test(command.trim());
111
116
  }
112
117
  function registerCleanupHook(options) {
113
118
  if (cleanupHookRegistered)
@@ -160,6 +165,7 @@ function ensureSafeRepoWorkspace(options = {}) {
160
165
  runtimeKind: "clone-main",
161
166
  repoRoot,
162
167
  workspaceRoot: created.workspaceRoot,
168
+ workspaceBranch: created.branchName,
163
169
  sourceBranch: branch,
164
170
  sourceCloneUrl: canonicalRepoUrl,
165
171
  cleanupAfterMerge: false,
@@ -174,6 +180,7 @@ function ensureSafeRepoWorkspace(options = {}) {
174
180
  runtimeKind: "clone-non-main",
175
181
  repoRoot,
176
182
  workspaceRoot: created.workspaceRoot,
183
+ workspaceBranch: created.branchName,
177
184
  sourceBranch: branch,
178
185
  sourceCloneUrl: canonicalRepoUrl,
179
186
  cleanupAfterMerge: false,
@@ -189,6 +196,7 @@ function ensureSafeRepoWorkspace(options = {}) {
189
196
  runtimeKind: "installed-runtime",
190
197
  repoRoot,
191
198
  workspaceRoot: created.workspaceRoot,
199
+ workspaceBranch: created.branchName,
192
200
  sourceBranch: null,
193
201
  sourceCloneUrl: canonicalRepoUrl,
194
202
  cleanupAfterMerge: true,
@@ -205,6 +213,7 @@ function ensureSafeRepoWorkspace(options = {}) {
205
213
  runtimeKind: selection.runtimeKind,
206
214
  repoRoot: selection.repoRoot,
207
215
  workspaceRoot: selection.workspaceRoot,
216
+ workspaceBranch: selection.workspaceBranch,
208
217
  sourceBranch: selection.sourceBranch,
209
218
  sourceCloneUrl: selection.sourceCloneUrl,
210
219
  cleanupAfterMerge: selection.cleanupAfterMerge,
@@ -226,3 +235,27 @@ function resolveSafeRepoPath(options) {
226
235
  const resolvedPath = relativePath ? path.join(selection.workspaceRoot, relativePath) : selection.workspaceRoot;
227
236
  return { selection, resolvedPath };
228
237
  }
238
+ function resolveSafeShellExecution(command, options = {}) {
239
+ const trimmed = command.trim();
240
+ if (!trimmed) {
241
+ return { selection: activeSelection, command };
242
+ }
243
+ if (activeSelection && command.includes(activeSelection.workspaceRoot)) {
244
+ return { selection: activeSelection, command, cwd: activeSelection.workspaceRoot };
245
+ }
246
+ const repoRoot = path.resolve(options.repoRoot ?? (0, identity_1.getRepoRoot)());
247
+ const mentionsRepoRoot = command.includes(repoRoot);
248
+ const shouldRoute = mentionsRepoRoot || looksRepoLocalShellCommand(trimmed);
249
+ if (!shouldRoute) {
250
+ return { selection: activeSelection, command };
251
+ }
252
+ const selection = ensureSafeRepoWorkspace(options);
253
+ const rewrittenCommand = mentionsRepoRoot
254
+ ? command.split(repoRoot).join(selection.workspaceRoot)
255
+ : command;
256
+ return {
257
+ selection,
258
+ command: rewrittenCommand,
259
+ cwd: selection.workspaceRoot,
260
+ };
261
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findActivePersistentObligation = findActivePersistentObligation;
4
+ exports.renderActiveObligationSteering = renderActiveObligationSteering;
5
+ const runtime_1 = require("../nerves/runtime");
6
+ function findActivePersistentObligation(frame) {
7
+ if (!frame)
8
+ return null;
9
+ return (frame.pendingObligations ?? []).find((ob) => ob.status !== "pending" && ob.status !== "fulfilled") ?? null;
10
+ }
11
+ function renderActiveObligationSteering(obligation) {
12
+ (0, runtime_1.emitNervesEvent)({
13
+ component: "mind",
14
+ event: "mind.obligation_steering_rendered",
15
+ message: "rendered active obligation steering",
16
+ meta: {
17
+ hasObligation: Boolean(obligation),
18
+ hasSurface: Boolean(obligation?.currentSurface?.label),
19
+ },
20
+ });
21
+ if (!obligation)
22
+ return "";
23
+ const name = obligation.origin.friendId;
24
+ const surfaceLine = obligation.currentSurface?.label
25
+ ? `\nright now that work is happening in ${obligation.currentSurface.label}.`
26
+ : "";
27
+ return `## where my attention is
28
+ i'm already working on something i owe ${name}.${surfaceLine}
29
+
30
+ i should close that loop before i act like this is a fresh blank turn.`;
31
+ }
@@ -52,6 +52,7 @@ exports.buildSystem = buildSystem;
52
52
  const fs = __importStar(require("fs"));
53
53
  const path = __importStar(require("path"));
54
54
  const core_1 = require("../heart/core");
55
+ const ouro_version_manager_1 = require("../heart/daemon/ouro-version-manager");
55
56
  const tools_1 = require("../repertoire/tools");
56
57
  const skills_1 = require("../repertoire/skills");
57
58
  const identity_1 = require("../heart/identity");
@@ -65,6 +66,7 @@ const tasks_1 = require("../repertoire/tasks");
65
66
  const session_activity_1 = require("../heart/session-activity");
66
67
  const active_work_1 = require("../heart/active-work");
67
68
  const commitments_1 = require("../heart/commitments");
69
+ const obligation_steering_1 = require("./obligation-steering");
68
70
  // Lazy-loaded psyche text cache
69
71
  let _psycheCache = null;
70
72
  let _senseStatusLinesCache = null;
@@ -253,6 +255,11 @@ function runtimeInfoSection(channel) {
253
255
  const bundleMeta = readBundleMeta();
254
256
  if (bundleMeta?.previousRuntimeVersion && bundleMeta.previousRuntimeVersion !== currentVersion) {
255
257
  lines.push(`previously: ${bundleMeta.previousRuntimeVersion}`);
258
+ const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(bundleMeta.previousRuntimeVersion, currentVersion);
259
+ /* v8 ignore next -- buildChangelogCommand is non-null when previous/current runtime versions differ @preserve */
260
+ if (changelogCommand) {
261
+ lines.push(`if i'm closing a self-fix loop, i should tell them i updated and review changes with \`${changelogCommand}\`.`);
262
+ }
256
263
  }
257
264
  lines.push(`changelog available at: ${(0, bundle_manifest_1.getChangelogPath)()}`);
258
265
  lines.push(`cwd: ${process.cwd()}`);
@@ -460,7 +467,11 @@ function centerOfGravitySteeringSection(channel, options) {
460
467
  if (cog === "local-turn")
461
468
  return "";
462
469
  const job = frame.inner?.job;
470
+ const activeObligation = (0, obligation_steering_1.findActivePersistentObligation)(frame);
463
471
  if (cog === "inward-work") {
472
+ if (activeObligation) {
473
+ return (0, obligation_steering_1.renderActiveObligationSteering)(activeObligation);
474
+ }
464
475
  if (job?.status === "queued" || job?.status === "running") {
465
476
  const originClause = job.origin
466
477
  ? ` ${job.origin.friendName ?? job.origin.friendId} asked about something and i wanted to give it real thought before responding.`
@@ -554,6 +565,16 @@ tool_choice is set to "required" -- i must call a tool on every turn.
554
565
  \`final_answer\` must be the ONLY tool call in that turn. do not combine it with other tool calls.
555
566
  do NOT call no-op tools just before \`final_answer\`. if i am done, i call \`final_answer\` directly.`;
556
567
  }
568
+ function workspaceDisciplineSection() {
569
+ return `## repo workspace discipline
570
+ when a shared-harness or local code fix needs repo work, i get the real workspace first with \`safe_workspace\`.
571
+ \`read_file\`, \`write_file\`, and \`edit_file\` already map repo paths into that workspace. shell commands that target the harness run there too.
572
+
573
+ before the first repo edit, i tell the user in 1-2 short lines:
574
+ - the friction i'm fixing
575
+ - the workspace path/branch i'm using
576
+ - the first concrete action i'm taking`;
577
+ }
557
578
  function contextSection(context, options) {
558
579
  if (!context)
559
580
  return "";
@@ -688,6 +709,7 @@ async function buildSystem(channel = "cli", options, context) {
688
709
  toolsSection(channel, options, context),
689
710
  mcpToolsSection(options?.mcpManager),
690
711
  reasoningEffortSection(options),
712
+ workspaceDisciplineSection(),
691
713
  toolRestrictionSection(context),
692
714
  trustContextSection(context),
693
715
  mixedTrustGroupSection(context),
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatCodingTail = formatCodingTail;
4
4
  exports.attachCodingSessionFeedback = attachCodingSessionFeedback;
5
+ const identity_1 = require("../../heart/identity");
6
+ const obligations_1 = require("../../heart/obligations");
5
7
  const runtime_1 = require("../../nerves/runtime");
6
8
  const TERMINAL_UPDATE_KINDS = new Set(["completed", "failed", "killed"]);
7
9
  function clip(text, maxLength = 280) {
@@ -42,7 +44,10 @@ function lastMeaningfulLine(text) {
42
44
  return clip(lines.at(-1));
43
45
  }
44
46
  function formatSessionLabel(session) {
45
- return `${session.runner} ${session.id}`;
47
+ const origin = session.originSession
48
+ ? ` for ${session.originSession.channel}/${session.originSession.key}`
49
+ : "";
50
+ return `${session.runner} ${session.id}${origin}`;
46
51
  }
47
52
  function isSafeProgressSnippet(snippet) {
48
53
  const wordCount = snippet.split(/\s+/).filter(Boolean).length;
@@ -81,6 +86,44 @@ function formatUpdateMessage(update) {
81
86
  return `${label} started`;
82
87
  }
83
88
  }
89
+ function obligationNoteFromUpdate(update) {
90
+ const snippet = pickUpdateSnippet(update);
91
+ switch (update.kind) {
92
+ case "spawned":
93
+ return update.session.originSession
94
+ ? `coding session started for ${update.session.originSession.channel}/${update.session.originSession.key}`
95
+ : "coding session started";
96
+ case "progress":
97
+ return snippet ? `coding session progress: ${snippet}` : null;
98
+ case "waiting_input":
99
+ return snippet ? `coding session waiting: ${snippet}` : "coding session waiting for input";
100
+ case "stalled":
101
+ return snippet ? `coding session stalled: ${snippet}` : "coding session stalled";
102
+ case "completed":
103
+ return snippet
104
+ ? `coding session completed: ${snippet}; merge/update still pending`
105
+ : "coding session completed; merge/update still pending";
106
+ case "failed":
107
+ return snippet ? `coding session failed: ${snippet}` : "coding session failed";
108
+ case "killed":
109
+ return "coding session killed";
110
+ }
111
+ }
112
+ function syncObligationFromUpdate(update) {
113
+ const obligationId = update.session.obligationId;
114
+ if (!obligationId)
115
+ return;
116
+ try {
117
+ (0, obligations_1.advanceObligation)((0, identity_1.getAgentRoot)(), obligationId, {
118
+ status: "investigating",
119
+ currentSurface: { kind: "coding", label: `${update.session.runner} ${update.session.id}` },
120
+ latestNote: obligationNoteFromUpdate(update) ?? undefined,
121
+ });
122
+ }
123
+ catch {
124
+ // Detached feedback should still reach the human even if obligation sync is unavailable.
125
+ }
126
+ }
84
127
  function formatCodingTail(session) {
85
128
  const stdout = session.stdoutTail.trim() || "(empty)";
86
129
  const stderr = session.stderrTail.trim() || "(empty)";
@@ -119,8 +162,11 @@ function attachCodingSessionFeedback(manager, session, target) {
119
162
  });
120
163
  });
121
164
  };
122
- sendMessage(formatUpdateMessage({ kind: "spawned", session }));
165
+ const spawnedUpdate = { kind: "spawned", session };
166
+ syncObligationFromUpdate(spawnedUpdate);
167
+ sendMessage(formatUpdateMessage(spawnedUpdate));
123
168
  unsubscribe = manager.subscribe(session.id, async (update) => {
169
+ syncObligationFromUpdate(update);
124
170
  sendMessage(formatUpdateMessage(update));
125
171
  if (TERMINAL_UPDATE_KINDS.has(update.kind)) {
126
172
  closed = true;
@@ -62,6 +62,7 @@ function isPidAlive(pid) {
62
62
  function cloneSession(session) {
63
63
  return {
64
64
  ...session,
65
+ originSession: session.originSession ? { ...session.originSession } : undefined,
65
66
  stdoutTail: session.stdoutTail,
66
67
  stderrTail: session.stderrTail,
67
68
  failure: session.failure
@@ -157,6 +158,8 @@ class CodingSessionManager {
157
158
  runner: normalizedRequest.runner,
158
159
  workdir: normalizedRequest.workdir,
159
160
  taskRef: normalizedRequest.taskRef,
161
+ originSession: normalizedRequest.originSession ? { ...normalizedRequest.originSession } : undefined,
162
+ obligationId: normalizedRequest.obligationId,
160
163
  scopeFile: normalizedRequest.scopeFile,
161
164
  stateFile: normalizedRequest.stateFile,
162
165
  status: "spawning",
@@ -482,12 +485,16 @@ class CodingSessionManager {
482
485
  }
483
486
  const normalizedRequest = {
484
487
  ...request,
488
+ originSession: request.originSession ? { ...request.originSession } : undefined,
485
489
  sessionId: request.sessionId ?? session.id,
490
+ obligationId: request.obligationId,
486
491
  parentAgent: request.parentAgent ?? this.agentName,
487
492
  };
488
493
  const normalizedSession = {
489
494
  ...session,
490
495
  taskRef: session.taskRef ?? normalizedRequest.taskRef,
496
+ originSession: session.originSession ?? normalizedRequest.originSession,
497
+ obligationId: session.obligationId ?? normalizedRequest.obligationId,
491
498
  failure: session.failure ?? null,
492
499
  stdoutTail: session.stdoutTail ?? session.failure?.stdoutTail ?? "",
493
500
  stderrTail: session.stderrTail ?? session.failure?.stderrTail ?? "",
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.codingToolDefinitions = void 0;
4
4
  const index_1 = require("./index");
5
+ const identity_1 = require("../../heart/identity");
6
+ const obligations_1 = require("../../heart/obligations");
5
7
  const runtime_1 = require("../../nerves/runtime");
6
8
  const RUNNERS = ["claude", "codex"];
7
9
  function requireArg(args, key) {
@@ -130,6 +132,17 @@ exports.codingToolDefinitions = [
130
132
  prompt,
131
133
  taskRef,
132
134
  };
135
+ if (ctx?.currentSession && ctx.currentSession.channel !== "inner") {
136
+ request.originSession = {
137
+ friendId: ctx.currentSession.friendId,
138
+ channel: ctx.currentSession.channel,
139
+ key: ctx.currentSession.key,
140
+ };
141
+ const obligation = (0, obligations_1.findPendingObligationForOrigin)((0, identity_1.getAgentRoot)(), request.originSession);
142
+ if (obligation) {
143
+ request.obligationId = obligation.id;
144
+ }
145
+ }
133
146
  const scopeFile = optionalArg(args, "scopeFile");
134
147
  if (scopeFile)
135
148
  request.scopeFile = scopeFile;
@@ -138,6 +151,15 @@ exports.codingToolDefinitions = [
138
151
  request.stateFile = stateFile;
139
152
  const manager = (0, index_1.getCodingSessionManager)();
140
153
  const session = await manager.spawnSession(request);
154
+ if (session.obligationId) {
155
+ (0, obligations_1.advanceObligation)((0, identity_1.getAgentRoot)(), session.obligationId, {
156
+ status: "investigating",
157
+ currentSurface: { kind: "coding", label: `${session.runner} ${session.id}` },
158
+ latestNote: session.originSession
159
+ ? `coding session started for ${session.originSession.channel}/${session.originSession.key}`
160
+ : "coding session started",
161
+ });
162
+ }
141
163
  if (args.runner === "codex" && args.taskRef) {
142
164
  (0, runtime_1.emitNervesEvent)({
143
165
  component: "repertoire",
@@ -404,6 +404,29 @@ exports.baseToolDefinitions = [
404
404
  return allResults.join("\n");
405
405
  },
406
406
  },
407
+ {
408
+ tool: {
409
+ type: "function",
410
+ function: {
411
+ name: "safe_workspace",
412
+ description: "acquire or inspect the safe harness repo workspace for local edits. returns the real workspace path, branch, and why it was chosen.",
413
+ parameters: {
414
+ type: "object",
415
+ properties: {},
416
+ },
417
+ },
418
+ },
419
+ handler: () => {
420
+ const selection = (0, safe_workspace_1.ensureSafeRepoWorkspace)();
421
+ return [
422
+ `workspace: ${selection.workspaceRoot}`,
423
+ `branch: ${selection.workspaceBranch}`,
424
+ `runtime: ${selection.runtimeKind}`,
425
+ `cleanup_after_merge: ${selection.cleanupAfterMerge ? "yes" : "no"}`,
426
+ `note: ${selection.note}`,
427
+ ].join("\n");
428
+ },
429
+ },
407
430
  {
408
431
  tool: {
409
432
  type: "function",
@@ -417,7 +440,14 @@ exports.baseToolDefinitions = [
417
440
  },
418
441
  },
419
442
  },
420
- handler: (a) => (0, child_process_1.execSync)(a.command, { encoding: "utf-8", timeout: 30000 }),
443
+ handler: (a) => {
444
+ const prepared = (0, safe_workspace_1.resolveSafeShellExecution)(a.command);
445
+ return (0, child_process_1.execSync)(prepared.command, {
446
+ encoding: "utf-8",
447
+ timeout: 30000,
448
+ ...(prepared.cwd ? { cwd: prepared.cwd } : {}),
449
+ });
450
+ },
421
451
  },
422
452
  {
423
453
  tool: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.82",
3
+ "version": "0.1.0-alpha.84",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",