@ouro.bot/cli 0.1.0-alpha.9 → 0.1.0-alpha.91
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/AdoptionSpecialist.ouro/agent.json +70 -9
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +147 -205
- package/assets/ouroboros.png +0 -0
- package/changelog.json +536 -0
- package/dist/heart/active-work.js +251 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/commitments.js +109 -0
- package/dist/heart/config.js +68 -23
- package/dist/heart/core.js +452 -93
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/auth-flow.js +430 -0
- package/dist/heart/daemon/daemon-cli.js +1738 -269
- package/dist/heart/daemon/daemon-entry.js +55 -6
- package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
- package/dist/heart/daemon/daemon.js +216 -10
- package/dist/heart/daemon/hatch-animation.js +10 -3
- package/dist/heart/daemon/hatch-flow.js +7 -82
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/launchd.js +159 -0
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/ouro-bot-entry.js +0 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-entry.js +0 -0
- package/dist/heart/daemon/ouro-path-installer.js +260 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/ouro-version-manager.js +171 -0
- package/dist/heart/daemon/process-manager.js +14 -1
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-logging.js +58 -15
- package/dist/heart/daemon/runtime-metadata.js +219 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/sense-manager.js +307 -0
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +53 -84
- package/dist/heart/daemon/specialist-prompt.js +63 -11
- package/dist/heart/daemon/specialist-tools.js +211 -60
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/thoughts.js +507 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +126 -21
- package/dist/heart/kicks.js +1 -19
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/obligations.js +191 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/providers/anthropic.js +74 -9
- package/dist/heart/providers/azure.js +86 -7
- package/dist/heart/providers/github-copilot.js +149 -0
- package/dist/heart/providers/minimax.js +4 -0
- package/dist/heart/providers/openai-codex.js +12 -3
- package/dist/heart/safe-workspace.js +362 -0
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/session-activity.js +169 -0
- package/dist/heart/session-recall.js +116 -0
- package/dist/heart/streaming.js +100 -22
- package/dist/heart/target-resolution.js +123 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +14 -2
- package/dist/mind/bundle-manifest.js +70 -0
- package/dist/mind/context.js +57 -11
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +35 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/memory.js +27 -26
- package/dist/mind/obligation-steering.js +31 -0
- package/dist/mind/pending.js +76 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +467 -77
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/index.js +12 -0
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/feedback.js +180 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +69 -4
- package/dist/repertoire/coding/spawner.js +21 -3
- package/dist/repertoire/coding/tools.js +105 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/guardrails.js +290 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +195 -0
- package/dist/repertoire/skills.js +3 -26
- package/dist/repertoire/tasks/board.js +12 -0
- package/dist/repertoire/tasks/index.js +23 -9
- package/dist/repertoire/tasks/transitions.js +1 -2
- package/dist/repertoire/tools-base.js +714 -249
- package/dist/repertoire/tools-bluebubbles.js +93 -0
- package/dist/repertoire/tools-teams.js +58 -25
- package/dist/repertoire/tools.js +106 -53
- package/dist/senses/bluebubbles-client.js +210 -5
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +12 -4
- package/dist/senses/bluebubbles-mutation-log.js +45 -5
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +894 -45
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +400 -164
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/debug-activity.js +154 -0
- package/dist/senses/inner-dialog-worker.js +47 -18
- package/dist/senses/inner-dialog.js +377 -83
- package/dist/senses/pipeline.js +307 -0
- package/dist/senses/teams.js +573 -129
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +14 -3
- package/subagents/README.md +4 -70
- package/dist/heart/daemon/specialist-session.js +0 -142
- package/dist/heart/daemon/subagent-installer.js +0 -125
- package/dist/inner-worker-entry.js +0 -4
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -624
- package/subagents/work-planner.md +0 -373
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createGithubCopilotProviderRuntime = createGithubCopilotProviderRuntime;
|
|
7
|
+
const openai_1 = __importDefault(require("openai"));
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
const identity_1 = require("../identity");
|
|
10
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
11
|
+
const streaming_1 = require("../streaming");
|
|
12
|
+
const model_capabilities_1 = require("../model-capabilities");
|
|
13
|
+
/* v8 ignore start -- auth guidance helpers: tested via mock-driven provider tests @preserve */
|
|
14
|
+
function isAuthFailure(error) {
|
|
15
|
+
if (!(error instanceof Error))
|
|
16
|
+
return false;
|
|
17
|
+
const status = error.status;
|
|
18
|
+
return status === 401 || status === 403;
|
|
19
|
+
}
|
|
20
|
+
function getReauthGuidance(reason) {
|
|
21
|
+
const agentName = (0, identity_1.getAgentName)();
|
|
22
|
+
return [
|
|
23
|
+
`provider github-copilot failed (${reason}).`,
|
|
24
|
+
`Run \`ouro auth verify --agent ${agentName}\` to check all configured providers,`,
|
|
25
|
+
`\`ouro auth switch --agent ${agentName} --provider <other>\` to switch,`,
|
|
26
|
+
`or \`ouro auth --agent ${agentName} --provider github-copilot\` to reconfigure.`,
|
|
27
|
+
].join(" ");
|
|
28
|
+
}
|
|
29
|
+
function withAuthGuidance(error) {
|
|
30
|
+
const base = error instanceof Error ? error.message : String(error);
|
|
31
|
+
if (isAuthFailure(error)) {
|
|
32
|
+
return new Error(getReauthGuidance(base));
|
|
33
|
+
}
|
|
34
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
35
|
+
}
|
|
36
|
+
/* v8 ignore stop */
|
|
37
|
+
function createGithubCopilotProviderRuntime() {
|
|
38
|
+
(0, runtime_1.emitNervesEvent)({
|
|
39
|
+
component: "engine",
|
|
40
|
+
event: "engine.provider_init",
|
|
41
|
+
message: "github-copilot provider init",
|
|
42
|
+
meta: { provider: "github-copilot" },
|
|
43
|
+
});
|
|
44
|
+
const config = (0, config_1.getGithubCopilotConfig)();
|
|
45
|
+
if (!config.githubToken) {
|
|
46
|
+
throw new Error("provider 'github-copilot' is selected in agent.json but providers.github-copilot.githubToken is missing in secrets.json.");
|
|
47
|
+
}
|
|
48
|
+
if (!config.baseUrl) {
|
|
49
|
+
throw new Error("provider 'github-copilot' is selected in agent.json but providers.github-copilot.baseUrl is missing in secrets.json.");
|
|
50
|
+
}
|
|
51
|
+
const isCompletionsModel = config.model.startsWith("claude");
|
|
52
|
+
const modelCaps = (0, model_capabilities_1.getModelCapabilities)(config.model);
|
|
53
|
+
const capabilities = new Set();
|
|
54
|
+
/* v8 ignore next -- branch: capability detection tested via unit test @preserve */
|
|
55
|
+
if (modelCaps.reasoningEffort)
|
|
56
|
+
capabilities.add("reasoning-effort");
|
|
57
|
+
const client = new openai_1.default({
|
|
58
|
+
apiKey: config.githubToken,
|
|
59
|
+
baseURL: config.baseUrl,
|
|
60
|
+
timeout: 30000,
|
|
61
|
+
maxRetries: 0,
|
|
62
|
+
});
|
|
63
|
+
if (isCompletionsModel) {
|
|
64
|
+
// Chat completions path (Claude models via Copilot)
|
|
65
|
+
return {
|
|
66
|
+
id: "github-copilot",
|
|
67
|
+
model: config.model,
|
|
68
|
+
client,
|
|
69
|
+
capabilities,
|
|
70
|
+
supportedReasoningEfforts: modelCaps.reasoningEffort,
|
|
71
|
+
resetTurnState(_messages) {
|
|
72
|
+
// No provider-owned turn state for chat-completions path.
|
|
73
|
+
},
|
|
74
|
+
appendToolOutput(_callId, _output) {
|
|
75
|
+
// Chat-completions providers rely on canonical messages only.
|
|
76
|
+
},
|
|
77
|
+
/* v8 ignore start -- streamTurn: tested via mock assertions in github-copilot.test.ts @preserve */
|
|
78
|
+
async streamTurn(request) {
|
|
79
|
+
const params = {
|
|
80
|
+
messages: request.messages,
|
|
81
|
+
tools: request.activeTools,
|
|
82
|
+
stream: true,
|
|
83
|
+
};
|
|
84
|
+
if (this.model)
|
|
85
|
+
params.model = this.model;
|
|
86
|
+
if (request.traceId)
|
|
87
|
+
params.metadata = { trace_id: request.traceId };
|
|
88
|
+
if (request.toolChoiceRequired)
|
|
89
|
+
params.tool_choice = "required";
|
|
90
|
+
try {
|
|
91
|
+
return await (0, streaming_1.streamChatCompletion)(this.client, params, request.callbacks, request.signal);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw withAuthGuidance(error);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
/* v8 ignore stop */
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Responses API path (GPT models via Copilot)
|
|
101
|
+
let nativeInput = null;
|
|
102
|
+
let nativeInstructions = "";
|
|
103
|
+
return {
|
|
104
|
+
id: "github-copilot",
|
|
105
|
+
model: config.model,
|
|
106
|
+
client,
|
|
107
|
+
capabilities,
|
|
108
|
+
supportedReasoningEfforts: modelCaps.reasoningEffort,
|
|
109
|
+
/* v8 ignore start -- responses path: tested via mock assertions in github-copilot.test.ts @preserve */
|
|
110
|
+
resetTurnState(messages) {
|
|
111
|
+
const { instructions, input } = (0, streaming_1.toResponsesInput)(messages);
|
|
112
|
+
nativeInput = input;
|
|
113
|
+
nativeInstructions = instructions;
|
|
114
|
+
},
|
|
115
|
+
appendToolOutput(callId, output) {
|
|
116
|
+
if (!nativeInput)
|
|
117
|
+
return;
|
|
118
|
+
nativeInput.push({ type: "function_call_output", call_id: callId, output });
|
|
119
|
+
},
|
|
120
|
+
async streamTurn(request) {
|
|
121
|
+
if (!nativeInput)
|
|
122
|
+
this.resetTurnState(request.messages);
|
|
123
|
+
const params = {
|
|
124
|
+
model: this.model,
|
|
125
|
+
input: nativeInput,
|
|
126
|
+
instructions: nativeInstructions,
|
|
127
|
+
tools: (0, streaming_1.toResponsesTools)(request.activeTools),
|
|
128
|
+
reasoning: { effort: request.reasoningEffort ?? "medium", summary: "detailed" },
|
|
129
|
+
stream: true,
|
|
130
|
+
store: false,
|
|
131
|
+
include: ["reasoning.encrypted_content"],
|
|
132
|
+
};
|
|
133
|
+
if (request.traceId)
|
|
134
|
+
params.metadata = { trace_id: request.traceId };
|
|
135
|
+
if (request.toolChoiceRequired)
|
|
136
|
+
params.tool_choice = "required";
|
|
137
|
+
try {
|
|
138
|
+
const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal);
|
|
139
|
+
for (const item of result.outputItems)
|
|
140
|
+
nativeInput.push(item);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
throw withAuthGuidance(error);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
/* v8 ignore stop */
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -8,6 +8,7 @@ const openai_1 = __importDefault(require("openai"));
|
|
|
8
8
|
const config_1 = require("../config");
|
|
9
9
|
const runtime_1 = require("../../nerves/runtime");
|
|
10
10
|
const streaming_1 = require("../streaming");
|
|
11
|
+
const model_capabilities_1 = require("../model-capabilities");
|
|
11
12
|
function createMinimaxProviderRuntime() {
|
|
12
13
|
(0, runtime_1.emitNervesEvent)({
|
|
13
14
|
component: "engine",
|
|
@@ -19,6 +20,8 @@ function createMinimaxProviderRuntime() {
|
|
|
19
20
|
if (!minimaxConfig.apiKey) {
|
|
20
21
|
throw new Error("provider 'minimax' is selected in agent.json but providers.minimax.apiKey is missing in secrets.json.");
|
|
21
22
|
}
|
|
23
|
+
// Registry consulted; MiniMax models return empty defaults (no capabilities to derive)
|
|
24
|
+
(0, model_capabilities_1.getModelCapabilities)(minimaxConfig.model);
|
|
22
25
|
const client = new openai_1.default({
|
|
23
26
|
apiKey: minimaxConfig.apiKey,
|
|
24
27
|
baseURL: "https://api.minimaxi.chat/v1",
|
|
@@ -29,6 +32,7 @@ function createMinimaxProviderRuntime() {
|
|
|
29
32
|
id: "minimax",
|
|
30
33
|
model: minimaxConfig.model,
|
|
31
34
|
client,
|
|
35
|
+
capabilities: new Set(),
|
|
32
36
|
resetTurnState(_messages) {
|
|
33
37
|
// No provider-owned turn state for chat-completions providers.
|
|
34
38
|
},
|
|
@@ -9,6 +9,7 @@ const config_1 = require("../config");
|
|
|
9
9
|
const identity_1 = require("../identity");
|
|
10
10
|
const runtime_1 = require("../../nerves/runtime");
|
|
11
11
|
const streaming_1 = require("../streaming");
|
|
12
|
+
const model_capabilities_1 = require("../model-capabilities");
|
|
12
13
|
const OPENAI_CODEX_AUTH_FAILURE_MARKERS = [
|
|
13
14
|
"authentication failed",
|
|
14
15
|
"unauthorized",
|
|
@@ -27,11 +28,11 @@ function getOpenAICodexOAuthInstructions() {
|
|
|
27
28
|
const agentName = getOpenAICodexAgentNameForGuidance();
|
|
28
29
|
return [
|
|
29
30
|
"Fix:",
|
|
30
|
-
` 1. Run \`
|
|
31
|
-
" (or run `codex login` and set the OAuth token manually)",
|
|
31
|
+
` 1. Run \`ouro auth --agent ${agentName}\``,
|
|
32
32
|
` 2. Open ${getOpenAICodexSecretsPathForGuidance()}`,
|
|
33
33
|
" 3. Confirm providers.openai-codex.oauthAccessToken is set",
|
|
34
34
|
" 4. This provider uses chatgpt.com/backend-api/codex/responses (not api.openai.com/responses).",
|
|
35
|
+
" 5. After reauth, retry the failed ouro command or reconnect this session.",
|
|
35
36
|
].join("\n");
|
|
36
37
|
}
|
|
37
38
|
function getOpenAICodexReauthGuidance(reason) {
|
|
@@ -106,6 +107,12 @@ function createOpenAICodexProviderRuntime() {
|
|
|
106
107
|
if (!chatgptAccountId) {
|
|
107
108
|
throw new Error(getOpenAICodexReauthGuidance("OpenAI Codex OAuth access token is missing a chatgpt_account_id claim required for chatgpt.com/backend-api/codex."));
|
|
108
109
|
}
|
|
110
|
+
const modelCaps = (0, model_capabilities_1.getModelCapabilities)(codexConfig.model);
|
|
111
|
+
const capabilities = new Set();
|
|
112
|
+
if (modelCaps.reasoningEffort)
|
|
113
|
+
capabilities.add("reasoning-effort");
|
|
114
|
+
if (modelCaps.phase)
|
|
115
|
+
capabilities.add("phase-annotation");
|
|
109
116
|
const client = new openai_1.default({
|
|
110
117
|
apiKey: token,
|
|
111
118
|
baseURL: OPENAI_CODEX_BACKEND_BASE_URL,
|
|
@@ -123,6 +130,8 @@ function createOpenAICodexProviderRuntime() {
|
|
|
123
130
|
id: "openai-codex",
|
|
124
131
|
model: codexConfig.model,
|
|
125
132
|
client,
|
|
133
|
+
capabilities,
|
|
134
|
+
supportedReasoningEfforts: modelCaps.reasoningEffort,
|
|
126
135
|
resetTurnState(messages) {
|
|
127
136
|
const { instructions, input } = (0, streaming_1.toResponsesInput)(messages);
|
|
128
137
|
nativeInput = input;
|
|
@@ -141,7 +150,7 @@ function createOpenAICodexProviderRuntime() {
|
|
|
141
150
|
input: nativeInput,
|
|
142
151
|
instructions: nativeInstructions,
|
|
143
152
|
tools: (0, streaming_1.toResponsesTools)(request.activeTools),
|
|
144
|
-
reasoning: { effort: "medium", summary: "detailed" },
|
|
153
|
+
reasoning: { effort: request.reasoningEffort ?? "medium", summary: "detailed" },
|
|
145
154
|
stream: true,
|
|
146
155
|
store: false,
|
|
147
156
|
include: ["reasoning.encrypted_content"],
|
|
@@ -0,0 +1,362 @@
|
|
|
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 ensureSafeRepoWorkspace(options = {}) {
|
|
211
|
+
if (activeSelection) {
|
|
212
|
+
return activeSelection;
|
|
213
|
+
}
|
|
214
|
+
const repoRoot = options.repoRoot ?? (0, identity_1.getRepoRoot)();
|
|
215
|
+
const agentName = resolveAgentName(options.agentName);
|
|
216
|
+
const canonicalRepoUrl = options.canonicalRepoUrl ?? identity_1.HARNESS_CANONICAL_REPO_URL;
|
|
217
|
+
const workspaceBase = options.workspaceRoot ?? (0, identity_1.getAgentRepoWorkspacesRoot)(agentName);
|
|
218
|
+
const persistSelection = shouldPersistSelection(options);
|
|
219
|
+
const spawnSync = options.spawnSync ?? child_process_1.spawnSync;
|
|
220
|
+
const existsSync = options.existsSync ?? fs.existsSync;
|
|
221
|
+
const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
|
|
222
|
+
const rmSync = options.rmSync ?? fs.rmSync;
|
|
223
|
+
const now = options.now ?? defaultNow;
|
|
224
|
+
const stamp = String(now());
|
|
225
|
+
registerCleanupHook({ rmSync });
|
|
226
|
+
if (persistSelection) {
|
|
227
|
+
const restored = loadPersistedSelection(workspaceBase, options);
|
|
228
|
+
if (restored) {
|
|
229
|
+
activeSelection = restored;
|
|
230
|
+
(0, runtime_1.emitNervesEvent)({
|
|
231
|
+
component: "workspace",
|
|
232
|
+
event: "workspace.safe_repo_restored",
|
|
233
|
+
message: "restored safe repo workspace after runtime restart",
|
|
234
|
+
meta: {
|
|
235
|
+
runtimeKind: restored.runtimeKind,
|
|
236
|
+
repoRoot: restored.repoRoot,
|
|
237
|
+
workspaceRoot: restored.workspaceRoot,
|
|
238
|
+
workspaceBranch: restored.workspaceBranch,
|
|
239
|
+
sourceBranch: restored.sourceBranch,
|
|
240
|
+
cleanupAfterMerge: restored.cleanupAfterMerge,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
return restored;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
let selection;
|
|
247
|
+
if (isGitClone(repoRoot, spawnSync)) {
|
|
248
|
+
const branch = readCurrentBranch(repoRoot, spawnSync);
|
|
249
|
+
ensureFetchedOrigin(repoRoot, spawnSync);
|
|
250
|
+
if (branch === "main") {
|
|
251
|
+
ensureMainFastForward(repoRoot, spawnSync);
|
|
252
|
+
const worktreeRoot = path.join(workspaceBase, `ouroboros-main-${stamp}`);
|
|
253
|
+
const created = createDedicatedWorktree(repoRoot, worktreeRoot, `safe-workspace-${stamp}`, existsSync, mkdirSync, rmSync, spawnSync);
|
|
254
|
+
selection = {
|
|
255
|
+
runtimeKind: "clone-main",
|
|
256
|
+
repoRoot,
|
|
257
|
+
workspaceRoot: created.workspaceRoot,
|
|
258
|
+
workspaceBranch: created.branchName,
|
|
259
|
+
sourceBranch: branch,
|
|
260
|
+
sourceCloneUrl: canonicalRepoUrl,
|
|
261
|
+
cleanupAfterMerge: false,
|
|
262
|
+
created: created.created,
|
|
263
|
+
note: `running from clone on main; fast-forwarded and created dedicated worktree ${created.workspaceRoot}`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const worktreeRoot = path.join(workspaceBase, `ouroboros-origin-main-${stamp}`);
|
|
268
|
+
const created = createDedicatedWorktree(repoRoot, worktreeRoot, `safe-workspace-${stamp}`, existsSync, mkdirSync, rmSync, spawnSync);
|
|
269
|
+
selection = {
|
|
270
|
+
runtimeKind: "clone-non-main",
|
|
271
|
+
repoRoot,
|
|
272
|
+
workspaceRoot: created.workspaceRoot,
|
|
273
|
+
workspaceBranch: created.branchName,
|
|
274
|
+
sourceBranch: branch,
|
|
275
|
+
sourceCloneUrl: canonicalRepoUrl,
|
|
276
|
+
cleanupAfterMerge: false,
|
|
277
|
+
created: created.created,
|
|
278
|
+
note: `running from branch ${branch}; defaulted new work from origin/main in dedicated worktree ${created.workspaceRoot}`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const scratchRoot = path.join(workspaceBase, `ouroboros-scratch-${stamp}`);
|
|
284
|
+
const created = createScratchClone(scratchRoot, canonicalRepoUrl, existsSync, mkdirSync, rmSync, spawnSync);
|
|
285
|
+
selection = {
|
|
286
|
+
runtimeKind: "installed-runtime",
|
|
287
|
+
repoRoot,
|
|
288
|
+
workspaceRoot: created.workspaceRoot,
|
|
289
|
+
workspaceBranch: created.branchName,
|
|
290
|
+
sourceBranch: null,
|
|
291
|
+
sourceCloneUrl: canonicalRepoUrl,
|
|
292
|
+
cleanupAfterMerge: true,
|
|
293
|
+
created: created.created,
|
|
294
|
+
note: `running from installed runtime/wrapper; created scratch clone ${created.workspaceRoot} from ${canonicalRepoUrl}`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
activeSelection = selection;
|
|
298
|
+
if (persistSelection) {
|
|
299
|
+
persistSelectionState(workspaceBase, selection, options);
|
|
300
|
+
}
|
|
301
|
+
(0, runtime_1.emitNervesEvent)({
|
|
302
|
+
component: "workspace",
|
|
303
|
+
event: "workspace.safe_repo_acquired",
|
|
304
|
+
message: "acquired safe repo workspace before local edits",
|
|
305
|
+
meta: {
|
|
306
|
+
runtimeKind: selection.runtimeKind,
|
|
307
|
+
repoRoot: selection.repoRoot,
|
|
308
|
+
workspaceRoot: selection.workspaceRoot,
|
|
309
|
+
workspaceBranch: selection.workspaceBranch,
|
|
310
|
+
sourceBranch: selection.sourceBranch,
|
|
311
|
+
sourceCloneUrl: selection.sourceCloneUrl,
|
|
312
|
+
cleanupAfterMerge: selection.cleanupAfterMerge,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
return selection;
|
|
316
|
+
}
|
|
317
|
+
function resolveSafeRepoPath(options) {
|
|
318
|
+
const rawRequestedPath = options.requestedPath;
|
|
319
|
+
const repoRoot = path.resolve(options.repoRoot ?? (0, identity_1.getRepoRoot)());
|
|
320
|
+
if (!path.isAbsolute(rawRequestedPath) && !rawRequestedPath.startsWith("~")) {
|
|
321
|
+
const selection = activeSelection ?? ensureSafeRepoWorkspace(options);
|
|
322
|
+
return {
|
|
323
|
+
selection,
|
|
324
|
+
resolvedPath: path.resolve(selection.workspaceRoot, rawRequestedPath),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const requestedPath = path.resolve(rawRequestedPath);
|
|
328
|
+
if (activeSelection && requestedPath.startsWith(activeSelection.workspaceRoot + path.sep)) {
|
|
329
|
+
return { selection: activeSelection, resolvedPath: requestedPath };
|
|
330
|
+
}
|
|
331
|
+
if (requestedPath !== repoRoot && !requestedPath.startsWith(repoRoot + path.sep)) {
|
|
332
|
+
return { selection: activeSelection, resolvedPath: requestedPath };
|
|
333
|
+
}
|
|
334
|
+
const selection = ensureSafeRepoWorkspace(options);
|
|
335
|
+
const relativePath = requestedPath === repoRoot ? "" : path.relative(repoRoot, requestedPath);
|
|
336
|
+
const resolvedPath = relativePath ? path.join(selection.workspaceRoot, relativePath) : selection.workspaceRoot;
|
|
337
|
+
return { selection, resolvedPath };
|
|
338
|
+
}
|
|
339
|
+
function resolveSafeShellExecution(command, options = {}) {
|
|
340
|
+
const trimmed = command.trim();
|
|
341
|
+
if (!trimmed) {
|
|
342
|
+
return { selection: activeSelection, command };
|
|
343
|
+
}
|
|
344
|
+
if (activeSelection && command.includes(activeSelection.workspaceRoot)) {
|
|
345
|
+
return { selection: activeSelection, command, cwd: activeSelection.workspaceRoot };
|
|
346
|
+
}
|
|
347
|
+
const repoRoot = path.resolve(options.repoRoot ?? (0, identity_1.getRepoRoot)());
|
|
348
|
+
const mentionsRepoRoot = command.includes(repoRoot);
|
|
349
|
+
const shouldRoute = mentionsRepoRoot || looksRepoLocalShellCommand(trimmed);
|
|
350
|
+
if (!shouldRoute) {
|
|
351
|
+
return { selection: activeSelection, command };
|
|
352
|
+
}
|
|
353
|
+
const selection = ensureSafeRepoWorkspace(options);
|
|
354
|
+
const rewrittenCommand = mentionsRepoRoot
|
|
355
|
+
? command.split(repoRoot).join(selection.workspaceRoot)
|
|
356
|
+
: command;
|
|
357
|
+
return {
|
|
358
|
+
selection,
|
|
359
|
+
command: rewrittenCommand,
|
|
360
|
+
cwd: selection.workspaceRoot,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getSenseInventory = getSenseInventory;
|
|
4
|
+
const runtime_1 = require("../nerves/runtime");
|
|
5
|
+
const identity_1 = require("./identity");
|
|
6
|
+
const SENSES = [
|
|
7
|
+
{ sense: "cli", label: "CLI", daemonManaged: false },
|
|
8
|
+
{ sense: "teams", label: "Teams", daemonManaged: true },
|
|
9
|
+
{ sense: "bluebubbles", label: "BlueBubbles", daemonManaged: true },
|
|
10
|
+
];
|
|
11
|
+
function configuredSenses(senses) {
|
|
12
|
+
return senses ?? {
|
|
13
|
+
cli: { ...identity_1.DEFAULT_AGENT_SENSES.cli },
|
|
14
|
+
teams: { ...identity_1.DEFAULT_AGENT_SENSES.teams },
|
|
15
|
+
bluebubbles: { ...identity_1.DEFAULT_AGENT_SENSES.bluebubbles },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function resolveStatus(enabled, daemonManaged, runtimeInfo) {
|
|
19
|
+
if (!enabled) {
|
|
20
|
+
return "disabled";
|
|
21
|
+
}
|
|
22
|
+
if (!daemonManaged) {
|
|
23
|
+
return "interactive";
|
|
24
|
+
}
|
|
25
|
+
if (runtimeInfo?.runtime === "error") {
|
|
26
|
+
return "error";
|
|
27
|
+
}
|
|
28
|
+
if (runtimeInfo?.runtime === "running") {
|
|
29
|
+
return "running";
|
|
30
|
+
}
|
|
31
|
+
if (runtimeInfo?.configured === false) {
|
|
32
|
+
return "needs_config";
|
|
33
|
+
}
|
|
34
|
+
return "ready";
|
|
35
|
+
}
|
|
36
|
+
function getSenseInventory(agent, runtime = {}) {
|
|
37
|
+
const senses = configuredSenses(agent.senses);
|
|
38
|
+
const inventory = SENSES.map(({ sense, label, daemonManaged }) => {
|
|
39
|
+
const enabled = senses[sense].enabled;
|
|
40
|
+
return {
|
|
41
|
+
sense,
|
|
42
|
+
label,
|
|
43
|
+
enabled,
|
|
44
|
+
daemonManaged,
|
|
45
|
+
status: resolveStatus(enabled, daemonManaged, runtime[sense]),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
(0, runtime_1.emitNervesEvent)({
|
|
49
|
+
component: "channels",
|
|
50
|
+
event: "channel.sense_inventory_built",
|
|
51
|
+
message: "built sense inventory",
|
|
52
|
+
meta: {
|
|
53
|
+
senses: inventory.map((entry) => ({
|
|
54
|
+
sense: entry.sense,
|
|
55
|
+
enabled: entry.enabled,
|
|
56
|
+
status: entry.status,
|
|
57
|
+
})),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
return inventory;
|
|
61
|
+
}
|