@ouro.bot/cli 0.1.0-alpha.12 → 0.1.0-alpha.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
  2. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  3. package/README.md +147 -205
  4. package/assets/ouroboros.png +0 -0
  5. package/changelog.json +737 -0
  6. package/dist/heart/active-work.js +622 -0
  7. package/dist/heart/bridges/manager.js +358 -0
  8. package/dist/heart/bridges/state-machine.js +135 -0
  9. package/dist/heart/bridges/store.js +123 -0
  10. package/dist/heart/commitments.js +105 -0
  11. package/dist/heart/config.js +68 -23
  12. package/dist/heart/core.js +528 -100
  13. package/dist/heart/cross-chat-delivery.js +146 -0
  14. package/dist/heart/daemon/agent-discovery.js +81 -0
  15. package/dist/heart/daemon/auth-flow.js +430 -0
  16. package/dist/heart/daemon/daemon-cli.js +1601 -207
  17. package/dist/heart/daemon/daemon-entry.js +43 -2
  18. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  19. package/dist/heart/daemon/daemon.js +226 -1
  20. package/dist/heart/daemon/hatch-animation.js +10 -3
  21. package/dist/heart/daemon/hatch-flow.js +7 -82
  22. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  23. package/dist/heart/daemon/launchd.js +159 -0
  24. package/dist/heart/daemon/log-tailer.js +4 -3
  25. package/dist/heart/daemon/message-router.js +17 -8
  26. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  27. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  28. package/dist/heart/daemon/ouro-entry.js +0 -0
  29. package/dist/heart/daemon/ouro-path-installer.js +59 -15
  30. package/dist/heart/daemon/ouro-uti.js +11 -2
  31. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  32. package/dist/heart/daemon/process-manager.js +13 -0
  33. package/dist/heart/daemon/run-hooks.js +37 -0
  34. package/dist/heart/daemon/runtime-logging.js +58 -15
  35. package/dist/heart/daemon/runtime-metadata.js +219 -0
  36. package/dist/heart/daemon/runtime-mode.js +67 -0
  37. package/dist/heart/daemon/sense-manager.js +43 -2
  38. package/dist/heart/daemon/skill-management-installer.js +94 -0
  39. package/dist/heart/daemon/socket-client.js +202 -0
  40. package/dist/heart/daemon/specialist-orchestrator.js +37 -94
  41. package/dist/heart/daemon/specialist-prompt.js +50 -12
  42. package/dist/heart/daemon/specialist-tools.js +211 -60
  43. package/dist/heart/daemon/staged-restart.js +114 -0
  44. package/dist/heart/daemon/thoughts.js +507 -0
  45. package/dist/heart/daemon/update-checker.js +111 -0
  46. package/dist/heart/daemon/update-hooks.js +138 -0
  47. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  48. package/dist/heart/delegation.js +62 -0
  49. package/dist/heart/identity.js +64 -21
  50. package/dist/heart/kicks.js +1 -19
  51. package/dist/heart/model-capabilities.js +48 -0
  52. package/dist/heart/obligations.js +197 -0
  53. package/dist/heart/progress-story.js +42 -0
  54. package/dist/heart/provider-failover.js +88 -0
  55. package/dist/heart/provider-ping.js +151 -0
  56. package/dist/heart/providers/anthropic.js +107 -20
  57. package/dist/heart/providers/azure.js +115 -9
  58. package/dist/heart/providers/github-copilot.js +157 -0
  59. package/dist/heart/providers/minimax.js +33 -3
  60. package/dist/heart/providers/openai-codex.js +49 -14
  61. package/dist/heart/safe-workspace.js +381 -0
  62. package/dist/heart/session-activity.js +169 -0
  63. package/dist/heart/session-recall.js +216 -0
  64. package/dist/heart/streaming.js +108 -24
  65. package/dist/heart/target-resolution.js +123 -0
  66. package/dist/heart/tool-loop.js +194 -0
  67. package/dist/heart/turn-coordinator.js +28 -0
  68. package/dist/mind/associative-recall.js +14 -2
  69. package/dist/mind/bundle-manifest.js +70 -0
  70. package/dist/mind/context.js +60 -14
  71. package/dist/mind/first-impressions.js +16 -2
  72. package/dist/mind/friends/channel.js +35 -0
  73. package/dist/mind/friends/group-context.js +144 -0
  74. package/dist/mind/friends/store-file.js +19 -0
  75. package/dist/mind/friends/trust-explanation.js +74 -0
  76. package/dist/mind/friends/types.js +8 -0
  77. package/dist/mind/memory.js +27 -26
  78. package/dist/mind/obligation-steering.js +221 -0
  79. package/dist/mind/pending.js +76 -9
  80. package/dist/mind/phrases.js +1 -0
  81. package/dist/mind/prompt.js +459 -77
  82. package/dist/mind/token-estimate.js +8 -12
  83. package/dist/nerves/cli-logging.js +15 -2
  84. package/dist/nerves/coverage/run-artifacts.js +1 -1
  85. package/dist/nerves/index.js +12 -0
  86. package/dist/repertoire/ado-client.js +4 -2
  87. package/dist/repertoire/coding/context-pack.js +254 -0
  88. package/dist/repertoire/coding/feedback.js +301 -0
  89. package/dist/repertoire/coding/index.js +4 -1
  90. package/dist/repertoire/coding/manager.js +210 -4
  91. package/dist/repertoire/coding/spawner.js +39 -9
  92. package/dist/repertoire/coding/tools.js +171 -4
  93. package/dist/repertoire/data/ado-endpoints.json +188 -0
  94. package/dist/repertoire/guardrails.js +290 -0
  95. package/dist/repertoire/mcp-client.js +254 -0
  96. package/dist/repertoire/mcp-manager.js +195 -0
  97. package/dist/repertoire/skills.js +3 -26
  98. package/dist/repertoire/tasks/board.js +12 -0
  99. package/dist/repertoire/tasks/index.js +23 -9
  100. package/dist/repertoire/tasks/transitions.js +1 -2
  101. package/dist/repertoire/tools-base.js +925 -250
  102. package/dist/repertoire/tools-bluebubbles.js +93 -0
  103. package/dist/repertoire/tools-teams.js +58 -25
  104. package/dist/repertoire/tools.js +106 -53
  105. package/dist/senses/bluebubbles-client.js +210 -5
  106. package/dist/senses/bluebubbles-entry.js +2 -0
  107. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  108. package/dist/senses/bluebubbles-media.js +339 -0
  109. package/dist/senses/bluebubbles-model.js +12 -4
  110. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  111. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  112. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  113. package/dist/senses/bluebubbles.js +912 -45
  114. package/dist/senses/cli-layout.js +187 -0
  115. package/dist/senses/cli.js +477 -170
  116. package/dist/senses/continuity.js +94 -0
  117. package/dist/senses/debug-activity.js +154 -0
  118. package/dist/senses/inner-dialog-worker.js +47 -18
  119. package/dist/senses/inner-dialog.js +388 -83
  120. package/dist/senses/pipeline.js +444 -0
  121. package/dist/senses/teams.js +607 -129
  122. package/dist/senses/trust-gate.js +112 -2
  123. package/package.json +14 -3
  124. package/subagents/README.md +4 -70
  125. package/dist/heart/daemon/specialist-session.js +0 -177
  126. package/dist/heart/daemon/subagent-installer.js +0 -134
  127. package/dist/inner-worker-entry.js +0 -4
  128. package/subagents/work-doer.md +0 -233
  129. package/subagents/work-merger.md +0 -624
  130. package/subagents/work-planner.md +0 -373
@@ -3,12 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.classifyOpenAICodexError = classifyOpenAICodexError;
6
7
  exports.createOpenAICodexProviderRuntime = createOpenAICodexProviderRuntime;
7
8
  const openai_1 = __importDefault(require("openai"));
8
9
  const config_1 = require("../config");
9
10
  const identity_1 = require("../identity");
10
11
  const runtime_1 = require("../../nerves/runtime");
11
12
  const streaming_1 = require("../streaming");
13
+ const model_capabilities_1 = require("../model-capabilities");
12
14
  const OPENAI_CODEX_AUTH_FAILURE_MARKERS = [
13
15
  "authentication failed",
14
16
  "unauthorized",
@@ -27,11 +29,11 @@ function getOpenAICodexOAuthInstructions() {
27
29
  const agentName = getOpenAICodexAgentNameForGuidance();
28
30
  return [
29
31
  "Fix:",
30
- ` 1. Run \`npm run auth:openai-codex -- --agent ${agentName}\``,
31
- " (or run `codex login` and set the OAuth token manually)",
32
+ ` 1. Run \`ouro auth --agent ${agentName}\``,
32
33
  ` 2. Open ${getOpenAICodexSecretsPathForGuidance()}`,
33
34
  " 3. Confirm providers.openai-codex.oauthAccessToken is set",
34
35
  " 4. This provider uses chatgpt.com/backend-api/codex/responses (not api.openai.com/responses).",
36
+ " 5. After reauth, retry the failed ouro command or reconnect this session.",
35
37
  ].join("\n");
36
38
  }
37
39
  function getOpenAICodexReauthGuidance(reason) {
@@ -41,6 +43,33 @@ function getOpenAICodexReauthGuidance(reason) {
41
43
  getOpenAICodexOAuthInstructions(),
42
44
  ].join("\n");
43
45
  }
46
+ /* v8 ignore start -- shared network error utility, tested via classification tests @preserve */
47
+ function isNetworkError(error) {
48
+ const code = error.code || "";
49
+ if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
50
+ "EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
51
+ return true;
52
+ const msg = error.message || "";
53
+ return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
54
+ }
55
+ /* v8 ignore stop */
56
+ function classifyOpenAICodexError(error) {
57
+ const status = error.status;
58
+ if (status === 401 || status === 403 || isOpenAICodexAuthFailure(error))
59
+ return "auth-failure";
60
+ if (status === 429) {
61
+ const lower = error.message.toLowerCase();
62
+ if (lower.includes("usage") || lower.includes("quota") || lower.includes("exceeded your"))
63
+ return "usage-limit";
64
+ return "rate-limit";
65
+ }
66
+ if (status && status >= 500)
67
+ return "server-error";
68
+ if (isNetworkError(error))
69
+ return "network-error";
70
+ return "unknown";
71
+ }
72
+ /* v8 ignore start -- auth detection: only called from classifyOpenAICodexError which always passes Error @preserve */
44
73
  function isOpenAICodexAuthFailure(error) {
45
74
  if (!(error instanceof Error))
46
75
  return false;
@@ -50,13 +79,7 @@ function isOpenAICodexAuthFailure(error) {
50
79
  const lower = error.message.toLowerCase();
51
80
  return OPENAI_CODEX_AUTH_FAILURE_MARKERS.some((marker) => lower.includes(marker));
52
81
  }
53
- function withOpenAICodexAuthGuidance(error) {
54
- const base = error instanceof Error ? error.message : String(error);
55
- if (isOpenAICodexAuthFailure(error)) {
56
- return new Error(getOpenAICodexReauthGuidance(`OpenAI Codex authentication failed (${base}).`));
57
- }
58
- return error instanceof Error ? error : new Error(String(error));
59
- }
82
+ /* v8 ignore stop */
60
83
  function decodeJwtPayload(token) {
61
84
  const parts = token.split(".");
62
85
  if (parts.length < 2)
@@ -87,14 +110,14 @@ function getChatGPTAccountIdFromToken(token) {
87
110
  return "";
88
111
  return accountId.trim();
89
112
  }
90
- function createOpenAICodexProviderRuntime() {
113
+ function createOpenAICodexProviderRuntime(config) {
91
114
  (0, runtime_1.emitNervesEvent)({
92
115
  component: "engine",
93
116
  event: "engine.provider_init",
94
117
  message: "openai-codex provider init",
95
118
  meta: { provider: "openai-codex" },
96
119
  });
97
- const codexConfig = (0, config_1.getOpenAICodexConfig)();
120
+ const codexConfig = config ?? (0, config_1.getOpenAICodexConfig)();
98
121
  if (!(codexConfig.model && codexConfig.oauthAccessToken)) {
99
122
  throw new Error(getOpenAICodexReauthGuidance("provider 'openai-codex' is selected in agent.json but providers.openai-codex.model/oauthAccessToken is incomplete in secrets.json."));
100
123
  }
@@ -106,6 +129,12 @@ function createOpenAICodexProviderRuntime() {
106
129
  if (!chatgptAccountId) {
107
130
  throw new Error(getOpenAICodexReauthGuidance("OpenAI Codex OAuth access token is missing a chatgpt_account_id claim required for chatgpt.com/backend-api/codex."));
108
131
  }
132
+ const modelCaps = (0, model_capabilities_1.getModelCapabilities)(codexConfig.model);
133
+ const capabilities = new Set();
134
+ if (modelCaps.reasoningEffort)
135
+ capabilities.add("reasoning-effort");
136
+ if (modelCaps.phase)
137
+ capabilities.add("phase-annotation");
109
138
  const client = new openai_1.default({
110
139
  apiKey: token,
111
140
  baseURL: OPENAI_CODEX_BACKEND_BASE_URL,
@@ -123,6 +152,8 @@ function createOpenAICodexProviderRuntime() {
123
152
  id: "openai-codex",
124
153
  model: codexConfig.model,
125
154
  client,
155
+ capabilities,
156
+ supportedReasoningEfforts: modelCaps.reasoningEffort,
126
157
  resetTurnState(messages) {
127
158
  const { instructions, input } = (0, streaming_1.toResponsesInput)(messages);
128
159
  nativeInput = input;
@@ -141,7 +172,7 @@ function createOpenAICodexProviderRuntime() {
141
172
  input: nativeInput,
142
173
  instructions: nativeInstructions,
143
174
  tools: (0, streaming_1.toResponsesTools)(request.activeTools),
144
- reasoning: { effort: "medium", summary: "detailed" },
175
+ reasoning: { effort: request.reasoningEffort ?? "medium", summary: "detailed" },
145
176
  stream: true,
146
177
  store: false,
147
178
  include: ["reasoning.encrypted_content"],
@@ -149,14 +180,18 @@ function createOpenAICodexProviderRuntime() {
149
180
  if (request.toolChoiceRequired)
150
181
  params.tool_choice = "required";
151
182
  try {
152
- const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal);
183
+ const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal, request.eagerFinalAnswerStreaming);
153
184
  for (const item of result.outputItems)
154
185
  nativeInput.push(item);
155
186
  return result;
156
187
  }
157
188
  catch (error) {
158
- throw withOpenAICodexAuthGuidance(error);
189
+ throw error instanceof Error ? error : new Error(String(error));
159
190
  }
160
191
  },
192
+ /* v8 ignore next 3 -- delegation: classification logic tested via classifyOpenAICodexError @preserve */
193
+ classifyError(error) {
194
+ return classifyOpenAICodexError(error);
195
+ },
161
196
  };
162
197
  }
@@ -0,0 +1,381 @@
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.resetSafeWorkspaceSelection = resetSafeWorkspaceSelection;
37
+ exports.getActiveSafeWorkspaceSelection = getActiveSafeWorkspaceSelection;
38
+ exports.ensureSafeRepoWorkspace = ensureSafeRepoWorkspace;
39
+ exports.resolveSafeRepoPath = resolveSafeRepoPath;
40
+ exports.resolveSafeShellExecution = resolveSafeShellExecution;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const child_process_1 = require("child_process");
44
+ const identity_1 = require("./identity");
45
+ const runtime_1 = require("../nerves/runtime");
46
+ let activeSelection = null;
47
+ let cleanupHookRegistered = false;
48
+ function workspaceSelectionStateFile(workspaceBase) {
49
+ return path.join(workspaceBase, ".active-safe-workspace.json");
50
+ }
51
+ function getOptionalFsFn(name) {
52
+ try {
53
+ return fs[name];
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
59
+ function shouldPersistSelection(options) {
60
+ return options.persistSelection ?? options.workspaceRoot === undefined;
61
+ }
62
+ function isPersistedSelectionShape(value) {
63
+ if (!value || typeof value !== "object")
64
+ return false;
65
+ const candidate = value;
66
+ return (typeof candidate.runtimeKind === "string"
67
+ && typeof candidate.repoRoot === "string"
68
+ && typeof candidate.workspaceRoot === "string"
69
+ && typeof candidate.workspaceBranch === "string"
70
+ && (candidate.sourceBranch === null || typeof candidate.sourceBranch === "string")
71
+ && typeof candidate.sourceCloneUrl === "string"
72
+ && typeof candidate.cleanupAfterMerge === "boolean"
73
+ && typeof candidate.created === "boolean"
74
+ && typeof candidate.note === "string");
75
+ }
76
+ function loadPersistedSelection(workspaceBase, options) {
77
+ const existsSync = options.existsSync ?? fs.existsSync;
78
+ const readFileSync = options.readFileSync ?? getOptionalFsFn("readFileSync");
79
+ const unlinkSync = options.unlinkSync ?? getOptionalFsFn("unlinkSync");
80
+ const stateFile = workspaceSelectionStateFile(workspaceBase);
81
+ if (!existsSync(stateFile))
82
+ return null;
83
+ if (!readFileSync)
84
+ return null;
85
+ try {
86
+ const raw = readFileSync(stateFile, "utf-8");
87
+ const parsed = JSON.parse(raw);
88
+ if (!isPersistedSelectionShape(parsed) || !existsSync(parsed.workspaceRoot)) {
89
+ try {
90
+ unlinkSync?.(stateFile);
91
+ }
92
+ catch {
93
+ // best effort
94
+ }
95
+ return null;
96
+ }
97
+ return parsed;
98
+ }
99
+ catch {
100
+ try {
101
+ unlinkSync?.(stateFile);
102
+ }
103
+ catch {
104
+ // best effort
105
+ }
106
+ return null;
107
+ }
108
+ }
109
+ function persistSelectionState(workspaceBase, selection, options) {
110
+ const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
111
+ const writeFileSync = options.writeFileSync ?? getOptionalFsFn("writeFileSync");
112
+ if (!writeFileSync)
113
+ return;
114
+ mkdirSync(workspaceBase, { recursive: true });
115
+ writeFileSync(workspaceSelectionStateFile(workspaceBase), JSON.stringify(selection, null, 2), "utf-8");
116
+ }
117
+ function defaultNow() {
118
+ return Date.now();
119
+ }
120
+ function resolveAgentName(explicit) {
121
+ if (explicit && explicit.trim().length > 0)
122
+ return explicit.trim();
123
+ try {
124
+ return (0, identity_1.getAgentName)();
125
+ }
126
+ catch {
127
+ return "slugger";
128
+ }
129
+ }
130
+ function runGit(cwd, args, spawnSync) {
131
+ return spawnSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
132
+ }
133
+ function readStdout(result) {
134
+ return (result.stdout ?? Buffer.from("")).toString("utf-8").trim();
135
+ }
136
+ function readStderr(result) {
137
+ return (result.stderr ?? Buffer.from("")).toString("utf-8").trim();
138
+ }
139
+ function assertGitOk(result, action) {
140
+ if (result.error) {
141
+ throw result.error;
142
+ }
143
+ if (result.status !== 0) {
144
+ const detail = readStderr(result) || readStdout(result) || `exit ${result.status ?? "unknown"}`;
145
+ throw new Error(`${action} failed: ${detail}`);
146
+ }
147
+ return readStdout(result);
148
+ }
149
+ function isGitClone(repoRoot, spawnSync) {
150
+ const result = runGit(repoRoot, ["rev-parse", "--is-inside-work-tree"], spawnSync);
151
+ return result.status === 0 && readStdout(result) === "true";
152
+ }
153
+ function readCurrentBranch(repoRoot, spawnSync) {
154
+ return assertGitOk(runGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"], spawnSync), "git branch read");
155
+ }
156
+ function ensureFetchedOrigin(repoRoot, spawnSync) {
157
+ assertGitOk(runGit(repoRoot, ["fetch", "origin"], spawnSync), "git fetch origin");
158
+ }
159
+ function ensureMainFastForward(repoRoot, spawnSync) {
160
+ assertGitOk(runGit(repoRoot, ["pull", "--ff-only", "origin", "main"], spawnSync), "git pull --ff-only origin main");
161
+ }
162
+ function createDedicatedWorktree(repoRoot, workspaceRoot, branchSuffix, existsSync, mkdirSync, rmSync, spawnSync) {
163
+ mkdirSync(path.dirname(workspaceRoot), { recursive: true });
164
+ const branchName = `slugger/${branchSuffix}`;
165
+ if (existsSync(workspaceRoot)) {
166
+ rmSync(workspaceRoot, { recursive: true, force: true });
167
+ }
168
+ assertGitOk(runGit(repoRoot, ["worktree", "add", "-B", branchName, workspaceRoot, "origin/main"], spawnSync), "git worktree add");
169
+ return { workspaceRoot, created: true, branchName };
170
+ }
171
+ function createScratchClone(workspaceRoot, cloneUrl, existsSync, mkdirSync, rmSync, spawnSync) {
172
+ mkdirSync(path.dirname(workspaceRoot), { recursive: true });
173
+ if (existsSync(workspaceRoot)) {
174
+ rmSync(workspaceRoot, { recursive: true, force: true });
175
+ }
176
+ const result = spawnSync("git", ["clone", "--depth", "1", "--branch", "main", cloneUrl, workspaceRoot], {
177
+ stdio: ["ignore", "pipe", "pipe"],
178
+ });
179
+ assertGitOk(result, "git clone");
180
+ return { workspaceRoot, created: true, branchName: "main" };
181
+ }
182
+ 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/;
183
+ function looksRepoLocalShellCommand(command) {
184
+ return REPO_LOCAL_SHELL_COMMAND.test(command.trim());
185
+ }
186
+ function registerCleanupHook(options) {
187
+ if (cleanupHookRegistered)
188
+ return;
189
+ cleanupHookRegistered = true;
190
+ process.on("exit", () => {
191
+ if (!activeSelection?.cleanupAfterMerge)
192
+ return;
193
+ try {
194
+ options.rmSync(activeSelection.workspaceRoot, { recursive: true, force: true });
195
+ }
196
+ catch {
197
+ // best effort
198
+ }
199
+ });
200
+ }
201
+ function resetSafeWorkspaceSelection(options = {}) {
202
+ activeSelection = null;
203
+ if (!options.keepCleanupHookRegistered) {
204
+ cleanupHookRegistered = false;
205
+ }
206
+ }
207
+ function getActiveSafeWorkspaceSelection() {
208
+ return activeSelection;
209
+ }
210
+ function refreshSelectionWorkspaceBranch(selection, spawnSync) {
211
+ try {
212
+ if (!isGitClone(selection.workspaceRoot, spawnSync)) {
213
+ return selection;
214
+ }
215
+ const liveBranch = readCurrentBranch(selection.workspaceRoot, spawnSync);
216
+ if (liveBranch === selection.workspaceBranch) {
217
+ return selection;
218
+ }
219
+ return { ...selection, workspaceBranch: liveBranch };
220
+ }
221
+ catch {
222
+ return selection;
223
+ }
224
+ }
225
+ function ensureSafeRepoWorkspace(options = {}) {
226
+ const agentName = resolveAgentName(options.agentName);
227
+ const workspaceBase = options.workspaceRoot ?? (0, identity_1.getAgentRepoWorkspacesRoot)(agentName);
228
+ const persistSelection = shouldPersistSelection(options);
229
+ const spawnSync = options.spawnSync ?? child_process_1.spawnSync;
230
+ const existsSync = options.existsSync ?? fs.existsSync;
231
+ const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
232
+ const rmSync = options.rmSync ?? fs.rmSync;
233
+ if (activeSelection) {
234
+ const refreshed = refreshSelectionWorkspaceBranch(activeSelection, spawnSync);
235
+ activeSelection = refreshed;
236
+ return refreshed;
237
+ }
238
+ const repoRoot = options.repoRoot ?? (0, identity_1.getRepoRoot)();
239
+ const canonicalRepoUrl = options.canonicalRepoUrl ?? identity_1.HARNESS_CANONICAL_REPO_URL;
240
+ const now = options.now ?? defaultNow;
241
+ const stamp = String(now());
242
+ registerCleanupHook({ rmSync });
243
+ if (persistSelection) {
244
+ const restored = loadPersistedSelection(workspaceBase, options);
245
+ if (restored) {
246
+ const refreshed = refreshSelectionWorkspaceBranch(restored, spawnSync);
247
+ activeSelection = refreshed;
248
+ persistSelectionState(workspaceBase, refreshed, options);
249
+ (0, runtime_1.emitNervesEvent)({
250
+ component: "workspace",
251
+ event: "workspace.safe_repo_restored",
252
+ message: "restored safe repo workspace after runtime restart",
253
+ meta: {
254
+ runtimeKind: refreshed.runtimeKind,
255
+ repoRoot: refreshed.repoRoot,
256
+ workspaceRoot: refreshed.workspaceRoot,
257
+ workspaceBranch: refreshed.workspaceBranch,
258
+ sourceBranch: refreshed.sourceBranch,
259
+ cleanupAfterMerge: refreshed.cleanupAfterMerge,
260
+ },
261
+ });
262
+ return refreshed;
263
+ }
264
+ }
265
+ let selection;
266
+ if (isGitClone(repoRoot, spawnSync)) {
267
+ const branch = readCurrentBranch(repoRoot, spawnSync);
268
+ ensureFetchedOrigin(repoRoot, spawnSync);
269
+ if (branch === "main") {
270
+ ensureMainFastForward(repoRoot, spawnSync);
271
+ const worktreeRoot = path.join(workspaceBase, `ouroboros-main-${stamp}`);
272
+ const created = createDedicatedWorktree(repoRoot, worktreeRoot, `safe-workspace-${stamp}`, existsSync, mkdirSync, rmSync, spawnSync);
273
+ selection = {
274
+ runtimeKind: "clone-main",
275
+ repoRoot,
276
+ workspaceRoot: created.workspaceRoot,
277
+ workspaceBranch: created.branchName,
278
+ sourceBranch: branch,
279
+ sourceCloneUrl: canonicalRepoUrl,
280
+ cleanupAfterMerge: false,
281
+ created: created.created,
282
+ note: `running from clone on main; fast-forwarded and created dedicated worktree ${created.workspaceRoot}`,
283
+ };
284
+ }
285
+ else {
286
+ const worktreeRoot = path.join(workspaceBase, `ouroboros-origin-main-${stamp}`);
287
+ const created = createDedicatedWorktree(repoRoot, worktreeRoot, `safe-workspace-${stamp}`, existsSync, mkdirSync, rmSync, spawnSync);
288
+ selection = {
289
+ runtimeKind: "clone-non-main",
290
+ repoRoot,
291
+ workspaceRoot: created.workspaceRoot,
292
+ workspaceBranch: created.branchName,
293
+ sourceBranch: branch,
294
+ sourceCloneUrl: canonicalRepoUrl,
295
+ cleanupAfterMerge: false,
296
+ created: created.created,
297
+ note: `running from branch ${branch}; defaulted new work from origin/main in dedicated worktree ${created.workspaceRoot}`,
298
+ };
299
+ }
300
+ }
301
+ else {
302
+ const scratchRoot = path.join(workspaceBase, `ouroboros-scratch-${stamp}`);
303
+ const created = createScratchClone(scratchRoot, canonicalRepoUrl, existsSync, mkdirSync, rmSync, spawnSync);
304
+ selection = {
305
+ runtimeKind: "installed-runtime",
306
+ repoRoot,
307
+ workspaceRoot: created.workspaceRoot,
308
+ workspaceBranch: created.branchName,
309
+ sourceBranch: null,
310
+ sourceCloneUrl: canonicalRepoUrl,
311
+ cleanupAfterMerge: true,
312
+ created: created.created,
313
+ note: `running from installed runtime/wrapper; created scratch clone ${created.workspaceRoot} from ${canonicalRepoUrl}`,
314
+ };
315
+ }
316
+ activeSelection = selection;
317
+ if (persistSelection) {
318
+ persistSelectionState(workspaceBase, selection, options);
319
+ }
320
+ (0, runtime_1.emitNervesEvent)({
321
+ component: "workspace",
322
+ event: "workspace.safe_repo_acquired",
323
+ message: "acquired safe repo workspace before local edits",
324
+ meta: {
325
+ runtimeKind: selection.runtimeKind,
326
+ repoRoot: selection.repoRoot,
327
+ workspaceRoot: selection.workspaceRoot,
328
+ workspaceBranch: selection.workspaceBranch,
329
+ sourceBranch: selection.sourceBranch,
330
+ sourceCloneUrl: selection.sourceCloneUrl,
331
+ cleanupAfterMerge: selection.cleanupAfterMerge,
332
+ },
333
+ });
334
+ return selection;
335
+ }
336
+ function resolveSafeRepoPath(options) {
337
+ const rawRequestedPath = options.requestedPath;
338
+ const repoRoot = path.resolve(options.repoRoot ?? (0, identity_1.getRepoRoot)());
339
+ if (!path.isAbsolute(rawRequestedPath) && !rawRequestedPath.startsWith("~")) {
340
+ const selection = activeSelection ?? ensureSafeRepoWorkspace(options);
341
+ return {
342
+ selection,
343
+ resolvedPath: path.resolve(selection.workspaceRoot, rawRequestedPath),
344
+ };
345
+ }
346
+ const requestedPath = path.resolve(rawRequestedPath);
347
+ if (activeSelection && requestedPath.startsWith(activeSelection.workspaceRoot + path.sep)) {
348
+ return { selection: activeSelection, resolvedPath: requestedPath };
349
+ }
350
+ if (requestedPath !== repoRoot && !requestedPath.startsWith(repoRoot + path.sep)) {
351
+ return { selection: activeSelection, resolvedPath: requestedPath };
352
+ }
353
+ const selection = ensureSafeRepoWorkspace(options);
354
+ const relativePath = requestedPath === repoRoot ? "" : path.relative(repoRoot, requestedPath);
355
+ const resolvedPath = relativePath ? path.join(selection.workspaceRoot, relativePath) : selection.workspaceRoot;
356
+ return { selection, resolvedPath };
357
+ }
358
+ function resolveSafeShellExecution(command, options = {}) {
359
+ const trimmed = command.trim();
360
+ if (!trimmed) {
361
+ return { selection: activeSelection, command };
362
+ }
363
+ if (activeSelection && command.includes(activeSelection.workspaceRoot)) {
364
+ return { selection: activeSelection, command, cwd: activeSelection.workspaceRoot };
365
+ }
366
+ const repoRoot = path.resolve(options.repoRoot ?? (0, identity_1.getRepoRoot)());
367
+ const mentionsRepoRoot = command.includes(repoRoot);
368
+ const shouldRoute = mentionsRepoRoot || looksRepoLocalShellCommand(trimmed);
369
+ if (!shouldRoute) {
370
+ return { selection: activeSelection, command };
371
+ }
372
+ const selection = ensureSafeRepoWorkspace(options);
373
+ const rewrittenCommand = mentionsRepoRoot
374
+ ? command.split(repoRoot).join(selection.workspaceRoot)
375
+ : command;
376
+ return {
377
+ selection,
378
+ command: rewrittenCommand,
379
+ cwd: selection.workspaceRoot,
380
+ };
381
+ }
@@ -0,0 +1,169 @@
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.listSessionActivity = listSessionActivity;
37
+ exports.findFreshestFriendSession = findFreshestFriendSession;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const runtime_1 = require("../nerves/runtime");
41
+ const DEFAULT_ACTIVE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
42
+ function activityPriority(source) {
43
+ return source === "friend-facing" ? 0 : 1;
44
+ }
45
+ function resolveFriendName(friendId, friendsDir, agentName) {
46
+ if (friendId === "self")
47
+ return agentName;
48
+ try {
49
+ const raw = fs.readFileSync(path.join(friendsDir, `${friendId}.json`), "utf-8");
50
+ const parsed = JSON.parse(raw);
51
+ return parsed.name ?? friendId;
52
+ }
53
+ catch {
54
+ return friendId;
55
+ }
56
+ }
57
+ function parseFriendActivity(sessionPath) {
58
+ let mtimeMs;
59
+ try {
60
+ mtimeMs = fs.statSync(sessionPath).mtimeMs;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ try {
66
+ const raw = fs.readFileSync(sessionPath, "utf-8");
67
+ const parsed = JSON.parse(raw);
68
+ const explicit = parsed?.state?.lastFriendActivityAt;
69
+ if (typeof explicit === "string") {
70
+ const parsedMs = Date.parse(explicit);
71
+ if (Number.isFinite(parsedMs)) {
72
+ return {
73
+ lastActivityMs: parsedMs,
74
+ lastActivityAt: new Date(parsedMs).toISOString(),
75
+ activitySource: "friend-facing",
76
+ };
77
+ }
78
+ }
79
+ }
80
+ catch {
81
+ // fall back to file mtime below
82
+ }
83
+ return {
84
+ lastActivityMs: mtimeMs,
85
+ lastActivityAt: new Date(mtimeMs).toISOString(),
86
+ activitySource: "mtime-fallback",
87
+ };
88
+ }
89
+ function listSessionActivity(query) {
90
+ const { sessionsDir, friendsDir, agentName, activeThresholdMs = DEFAULT_ACTIVE_THRESHOLD_MS, currentSession = null, } = query;
91
+ (0, runtime_1.emitNervesEvent)({
92
+ component: "daemon",
93
+ event: "daemon.session_activity_scan",
94
+ message: "scanning session activity",
95
+ meta: {
96
+ sessionsDir,
97
+ currentSession: currentSession ? `${currentSession.friendId}/${currentSession.channel}/${currentSession.key}` : null,
98
+ },
99
+ });
100
+ if (!fs.existsSync(sessionsDir))
101
+ return [];
102
+ const now = Date.now();
103
+ const results = [];
104
+ let friendDirs;
105
+ try {
106
+ friendDirs = fs.readdirSync(sessionsDir);
107
+ }
108
+ catch {
109
+ return [];
110
+ }
111
+ for (const friendId of friendDirs) {
112
+ const friendPath = path.join(sessionsDir, friendId);
113
+ let channels;
114
+ try {
115
+ channels = fs.readdirSync(friendPath);
116
+ }
117
+ catch {
118
+ continue;
119
+ }
120
+ for (const channel of channels) {
121
+ const channelPath = path.join(friendPath, channel);
122
+ let keys;
123
+ try {
124
+ keys = fs.readdirSync(channelPath);
125
+ }
126
+ catch {
127
+ continue;
128
+ }
129
+ for (const keyFile of keys) {
130
+ if (!keyFile.endsWith(".json"))
131
+ continue;
132
+ const key = keyFile.replace(/\.json$/, "");
133
+ if (currentSession && friendId === currentSession.friendId && channel === currentSession.channel && key === currentSession.key) {
134
+ continue;
135
+ }
136
+ const sessionPath = path.join(channelPath, keyFile);
137
+ const activity = parseFriendActivity(sessionPath);
138
+ if (!activity)
139
+ continue;
140
+ if (now - activity.lastActivityMs > activeThresholdMs)
141
+ continue;
142
+ results.push({
143
+ friendId,
144
+ friendName: resolveFriendName(friendId, friendsDir, agentName),
145
+ channel,
146
+ key,
147
+ sessionPath,
148
+ lastActivityAt: activity.lastActivityAt,
149
+ lastActivityMs: activity.lastActivityMs,
150
+ activitySource: activity.activitySource,
151
+ });
152
+ }
153
+ }
154
+ }
155
+ return results.sort((a, b) => {
156
+ const sourceDiff = activityPriority(a.activitySource) - activityPriority(b.activitySource);
157
+ if (sourceDiff !== 0)
158
+ return sourceDiff;
159
+ return b.lastActivityMs - a.lastActivityMs;
160
+ });
161
+ }
162
+ function findFreshestFriendSession(query) {
163
+ const { activeOnly = false, activeThresholdMs = DEFAULT_ACTIVE_THRESHOLD_MS, ...rest } = query;
164
+ const currentSession = rest.currentSession ?? null;
165
+ const all = activeOnly
166
+ ? listSessionActivity({ ...rest, activeThresholdMs, currentSession })
167
+ : listSessionActivity({ ...rest, activeThresholdMs: Number.MAX_SAFE_INTEGER, currentSession });
168
+ return all.find((entry) => entry.friendId === query.friendId) ?? null;
169
+ }