@leg3ndy/otto-bridge 0.9.2 → 1.0.1
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/README.md +78 -17
- package/dist/agentic_runtime/patch/structured_patch.js +240 -0
- package/dist/agentic_runtime/workspace/manager.js +1044 -0
- package/dist/chat_cli_client.js +91 -0
- package/dist/cli_terminal.js +668 -0
- package/dist/executors/native_macos.js +2778 -115
- package/dist/local_automations.js +33 -11
- package/dist/main.js +25 -3
- package/dist/runtime.js +136 -32
- package/dist/runtime_cli_client.js +18 -0
- package/dist/runtime_contract.js +516 -0
- package/dist/tool_catalog.js +148 -1
- package/dist/types.js +2 -2
- package/package.json +7 -2
- package/scripts/postinstall.mjs +35 -0
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { collectStructuredPatchTargets } from "../patch/structured_patch.js";
|
|
7
|
+
const MAX_INSTRUCTION_CHARS = 12_000;
|
|
8
|
+
const DEFAULT_REPO_MARKERS = [".git"];
|
|
9
|
+
const DEFAULT_REPO_MANIFEST_PATHS = [
|
|
10
|
+
"AGENTS.md",
|
|
11
|
+
"README.md",
|
|
12
|
+
"package.json",
|
|
13
|
+
"pyproject.toml",
|
|
14
|
+
"requirements.txt",
|
|
15
|
+
"Cargo.toml",
|
|
16
|
+
"go.mod",
|
|
17
|
+
];
|
|
18
|
+
const DEFAULT_REPO_LOCKFILE_PATHS = [
|
|
19
|
+
"package-lock.json",
|
|
20
|
+
"pnpm-lock.yaml",
|
|
21
|
+
"yarn.lock",
|
|
22
|
+
"bun.lockb",
|
|
23
|
+
"poetry.lock",
|
|
24
|
+
"Pipfile.lock",
|
|
25
|
+
"Cargo.lock",
|
|
26
|
+
"go.sum",
|
|
27
|
+
];
|
|
28
|
+
const DEFAULT_IGNORED_DIRECTORY_NAMES = [
|
|
29
|
+
".git",
|
|
30
|
+
".next",
|
|
31
|
+
".venv",
|
|
32
|
+
"__pycache__",
|
|
33
|
+
"build",
|
|
34
|
+
"coverage",
|
|
35
|
+
"dist",
|
|
36
|
+
"node_modules",
|
|
37
|
+
"venv",
|
|
38
|
+
];
|
|
39
|
+
const DEFAULT_KEY_FILE_PATTERNS = [
|
|
40
|
+
"AGENTS.md",
|
|
41
|
+
"README.md",
|
|
42
|
+
"package.json",
|
|
43
|
+
"package-lock.json",
|
|
44
|
+
"pnpm-lock.yaml",
|
|
45
|
+
"yarn.lock",
|
|
46
|
+
"bun.lockb",
|
|
47
|
+
"tsconfig.json",
|
|
48
|
+
"pyproject.toml",
|
|
49
|
+
"requirements.txt",
|
|
50
|
+
"poetry.lock",
|
|
51
|
+
"Pipfile.lock",
|
|
52
|
+
"Cargo.toml",
|
|
53
|
+
"Cargo.lock",
|
|
54
|
+
"go.mod",
|
|
55
|
+
"go.sum",
|
|
56
|
+
];
|
|
57
|
+
const WORKSPACE_INDEX_EXTENSION_LIMIT = 8;
|
|
58
|
+
const WORKSPACE_INDEX_TOP_LEVEL_LIMIT = 16;
|
|
59
|
+
const LOCKFILE_PACKAGE_MANAGER_MAP = {
|
|
60
|
+
"package-lock.json": "npm",
|
|
61
|
+
"pnpm-lock.yaml": "pnpm",
|
|
62
|
+
"yarn.lock": "yarn",
|
|
63
|
+
"bun.lockb": "bun",
|
|
64
|
+
"poetry.lock": "poetry",
|
|
65
|
+
"Pipfile.lock": "pipenv",
|
|
66
|
+
"Cargo.lock": "cargo",
|
|
67
|
+
"go.sum": "go",
|
|
68
|
+
};
|
|
69
|
+
const WORKSPACE_OBSERVE_ONLY_ACTIONS = new Set([
|
|
70
|
+
"filesystem_inspect",
|
|
71
|
+
"list_files",
|
|
72
|
+
"count_files",
|
|
73
|
+
"read_file",
|
|
74
|
+
"git_status",
|
|
75
|
+
"git_diff",
|
|
76
|
+
]);
|
|
77
|
+
const WORKSPACE_DEV_ASSIST_ACTIONS = new Set([
|
|
78
|
+
...WORKSPACE_OBSERVE_ONLY_ACTIONS,
|
|
79
|
+
"run_shell",
|
|
80
|
+
"run_tests",
|
|
81
|
+
"git_clone",
|
|
82
|
+
"git_fetch",
|
|
83
|
+
"git_checkout",
|
|
84
|
+
]);
|
|
85
|
+
const WORKSPACE_CODING_ACTIONS = new Set([
|
|
86
|
+
"write_text_file",
|
|
87
|
+
"write_json_file",
|
|
88
|
+
"apply_patch",
|
|
89
|
+
"mkdir",
|
|
90
|
+
"move_file",
|
|
91
|
+
]);
|
|
92
|
+
const WORKSPACE_RELEASE_ACTIONS = new Set([
|
|
93
|
+
"trash_path",
|
|
94
|
+
"delete_file",
|
|
95
|
+
"git_add",
|
|
96
|
+
"git_commit",
|
|
97
|
+
"git_push",
|
|
98
|
+
"git_rebase",
|
|
99
|
+
"git_merge",
|
|
100
|
+
"git_tag",
|
|
101
|
+
]);
|
|
102
|
+
const WORKSPACE_POLICY_ALL_ACTIONS = new Set([
|
|
103
|
+
...WORKSPACE_DEV_ASSIST_ACTIONS,
|
|
104
|
+
...WORKSPACE_CODING_ACTIONS,
|
|
105
|
+
...WORKSPACE_RELEASE_ACTIONS,
|
|
106
|
+
]);
|
|
107
|
+
const WORKSPACE_POLICY_ALLOWED_ACTIONS = {
|
|
108
|
+
observe_only: new Set([...WORKSPACE_OBSERVE_ONLY_ACTIONS]),
|
|
109
|
+
dev_assist: new Set([...WORKSPACE_DEV_ASSIST_ACTIONS]),
|
|
110
|
+
workspace_coding: new Set([...WORKSPACE_DEV_ASSIST_ACTIONS, ...WORKSPACE_CODING_ACTIONS]),
|
|
111
|
+
release_operator: new Set([...WORKSPACE_POLICY_ALL_ACTIONS]),
|
|
112
|
+
};
|
|
113
|
+
const WORKSPACE_POLICY_CONFIRM_ACTIONS = new Set([
|
|
114
|
+
"git_clone",
|
|
115
|
+
"git_fetch",
|
|
116
|
+
"git_checkout",
|
|
117
|
+
...WORKSPACE_RELEASE_ACTIONS,
|
|
118
|
+
]);
|
|
119
|
+
const WORKSPACE_POLICY_TITLES = {
|
|
120
|
+
observe_only: "Observe Only",
|
|
121
|
+
dev_assist: "Dev Assist",
|
|
122
|
+
workspace_coding: "Workspace Coding",
|
|
123
|
+
release_operator: "Release Operator",
|
|
124
|
+
};
|
|
125
|
+
function asString(value) {
|
|
126
|
+
return typeof value === "string" ? value.trim() : "";
|
|
127
|
+
}
|
|
128
|
+
function uniqueStrings(values) {
|
|
129
|
+
return Array.from(new Set(values.map((item) => asString(item)).filter(Boolean)));
|
|
130
|
+
}
|
|
131
|
+
function clampInteger(value, fallback, min, max) {
|
|
132
|
+
const numeric = Number(value);
|
|
133
|
+
if (!Number.isFinite(numeric)) {
|
|
134
|
+
return fallback;
|
|
135
|
+
}
|
|
136
|
+
return Math.min(max, Math.max(min, Math.trunc(numeric)));
|
|
137
|
+
}
|
|
138
|
+
function clipInstructionText(value) {
|
|
139
|
+
if (value.length <= MAX_INSTRUCTION_CHARS) {
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
return `${value.slice(0, MAX_INSTRUCTION_CHARS)}\n\n[truncado: mostrando ${MAX_INSTRUCTION_CHARS} de ${value.length} caracteres.]`;
|
|
143
|
+
}
|
|
144
|
+
function toRelativePath(basePath, targetPath) {
|
|
145
|
+
const relativePath = path.relative(basePath, targetPath);
|
|
146
|
+
return relativePath || ".";
|
|
147
|
+
}
|
|
148
|
+
function uniqueRuntimeStrings(values, limit) {
|
|
149
|
+
const normalized = values
|
|
150
|
+
.map((item) => asString(item))
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
return Array.from(new Set(normalized)).slice(0, limit);
|
|
153
|
+
}
|
|
154
|
+
function normalizeWorkspaceMemory(value, workspaceId) {
|
|
155
|
+
if (!value) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const jobCount = Number.isFinite(Number(value.job_count)) ? Number(value.job_count) : 0;
|
|
159
|
+
const summary = asString(value.summary)
|
|
160
|
+
|| `Memoria curta do workspace ${workspaceId} com ${jobCount} job${jobCount === 1 ? "" : "s"} recente${jobCount === 1 ? "" : "s"}.`;
|
|
161
|
+
return {
|
|
162
|
+
memory_id: asString(value.memory_id) || `workspace_memory.${workspaceId}`,
|
|
163
|
+
workspace_id: asString(value.workspace_id) || workspaceId,
|
|
164
|
+
source: asString(value.source) || "device_job_history",
|
|
165
|
+
job_count: Math.max(0, jobCount),
|
|
166
|
+
recent_job_ids: uniqueRuntimeStrings(value.recent_job_ids || [], 12),
|
|
167
|
+
recent_action_types: uniqueRuntimeStrings(value.recent_action_types || [], 16),
|
|
168
|
+
recent_target_paths: uniqueRuntimeStrings(value.recent_target_paths || [], 16),
|
|
169
|
+
recent_artifact_kinds: uniqueRuntimeStrings(value.recent_artifact_kinds || [], 16),
|
|
170
|
+
latest_job_id: asString(value.latest_job_id) || undefined,
|
|
171
|
+
latest_job_status: asString(value.latest_job_status) || undefined,
|
|
172
|
+
latest_job_at: asString(value.latest_job_at) || undefined,
|
|
173
|
+
instruction_digest: asString(value.instruction_digest) || undefined,
|
|
174
|
+
repo_root: asString(value.repo_root) || undefined,
|
|
175
|
+
scoped_root_path: asString(value.scoped_root_path) || undefined,
|
|
176
|
+
summary,
|
|
177
|
+
status_counts: value.status_counts && typeof value.status_counts === "object" ? value.status_counts : undefined,
|
|
178
|
+
retention: value.retention ? {
|
|
179
|
+
max_job_count: Number.isFinite(Number(value.retention.max_job_count)) ? Number(value.retention.max_job_count) : undefined,
|
|
180
|
+
max_age_days: Number.isFinite(Number(value.retention.max_age_days)) ? Number(value.retention.max_age_days) : undefined,
|
|
181
|
+
filtered_job_count: Number.isFinite(Number(value.retention.filtered_job_count)) ? Number(value.retention.filtered_job_count) : undefined,
|
|
182
|
+
window_started_at: asString(value.retention.window_started_at) || undefined,
|
|
183
|
+
last_cleared_at: asString(value.retention.last_cleared_at) || undefined,
|
|
184
|
+
last_cleared_reason: asString(value.retention.last_cleared_reason) || undefined,
|
|
185
|
+
} : undefined,
|
|
186
|
+
usage_policy: value.usage_policy ? {
|
|
187
|
+
profile_id: asString(value.usage_policy.profile_id) || undefined,
|
|
188
|
+
summary_mode: asString(value.usage_policy.summary_mode) || undefined,
|
|
189
|
+
include_action_types: typeof value.usage_policy.include_action_types === "boolean" ? value.usage_policy.include_action_types : undefined,
|
|
190
|
+
include_target_paths: typeof value.usage_policy.include_target_paths === "boolean" ? value.usage_policy.include_target_paths : undefined,
|
|
191
|
+
include_artifact_kinds: typeof value.usage_policy.include_artifact_kinds === "boolean" ? value.usage_policy.include_artifact_kinds : undefined,
|
|
192
|
+
include_failed_jobs: typeof value.usage_policy.include_failed_jobs === "boolean" ? value.usage_policy.include_failed_jobs : undefined,
|
|
193
|
+
resettable: typeof value.usage_policy.resettable === "boolean" ? value.usage_policy.resettable : undefined,
|
|
194
|
+
} : undefined,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function inferWorkspacePolicyProfile(actionType, isDestructivePatch = false) {
|
|
198
|
+
if (WORKSPACE_RELEASE_ACTIONS.has(actionType) || (actionType === "apply_patch" && isDestructivePatch)) {
|
|
199
|
+
return "release_operator";
|
|
200
|
+
}
|
|
201
|
+
if (WORKSPACE_CODING_ACTIONS.has(actionType)) {
|
|
202
|
+
return "workspace_coding";
|
|
203
|
+
}
|
|
204
|
+
if (WORKSPACE_DEV_ASSIST_ACTIONS.has(actionType)) {
|
|
205
|
+
return "dev_assist";
|
|
206
|
+
}
|
|
207
|
+
return "observe_only";
|
|
208
|
+
}
|
|
209
|
+
function normalizeWorkspacePolicy(value, workspaceId, actionTypes, destructiveActionTypes) {
|
|
210
|
+
const normalizedActionTypes = uniqueRuntimeStrings(actionTypes, 48);
|
|
211
|
+
const explicitProfileId = asString(value?.profile_id).toLowerCase();
|
|
212
|
+
const inferredProfileId = normalizedActionTypes.reduce((current, actionType) => {
|
|
213
|
+
const next = inferWorkspacePolicyProfile(actionType, destructiveActionTypes.has(actionType));
|
|
214
|
+
const currentAllowed = WORKSPACE_POLICY_ALLOWED_ACTIONS[current] || WORKSPACE_POLICY_ALLOWED_ACTIONS.observe_only;
|
|
215
|
+
const nextAllowed = WORKSPACE_POLICY_ALLOWED_ACTIONS[next] || WORKSPACE_POLICY_ALLOWED_ACTIONS.observe_only;
|
|
216
|
+
return nextAllowed.size > currentAllowed.size ? next : current;
|
|
217
|
+
}, "observe_only");
|
|
218
|
+
const profileId = explicitProfileId && WORKSPACE_POLICY_ALLOWED_ACTIONS[explicitProfileId]
|
|
219
|
+
? explicitProfileId
|
|
220
|
+
: inferredProfileId;
|
|
221
|
+
const allowedActionTypes = uniqueRuntimeStrings(value?.allowed_action_types?.length
|
|
222
|
+
? value.allowed_action_types
|
|
223
|
+
: Array.from(WORKSPACE_POLICY_ALLOWED_ACTIONS[profileId] || []), 64);
|
|
224
|
+
const blockedActionTypes = uniqueRuntimeStrings(value?.blocked_action_types?.length
|
|
225
|
+
? value.blocked_action_types
|
|
226
|
+
: Array.from(WORKSPACE_POLICY_ALL_ACTIONS).filter((item) => !allowedActionTypes.includes(item)), 64);
|
|
227
|
+
const requiresConfirmationActionTypes = uniqueRuntimeStrings(value?.requires_confirmation_action_types?.length
|
|
228
|
+
? value.requires_confirmation_action_types
|
|
229
|
+
: normalizedActionTypes.filter((item) => WORKSPACE_POLICY_CONFIRM_ACTIONS.has(item) || destructiveActionTypes.has(item)), 32);
|
|
230
|
+
if (!profileId && normalizedActionTypes.length === 0) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const allowShell = typeof value?.allow_shell === "boolean"
|
|
234
|
+
? value.allow_shell
|
|
235
|
+
: !["observe_only"].includes(profileId);
|
|
236
|
+
const allowCodeWrite = typeof value?.allow_code_write === "boolean"
|
|
237
|
+
? value.allow_code_write
|
|
238
|
+
: ["workspace_coding", "release_operator"].includes(profileId);
|
|
239
|
+
const allowDestructive = typeof value?.allow_destructive === "boolean"
|
|
240
|
+
? value.allow_destructive
|
|
241
|
+
: profileId === "release_operator";
|
|
242
|
+
const allowRelease = typeof value?.allow_release === "boolean"
|
|
243
|
+
? value.allow_release
|
|
244
|
+
: profileId === "release_operator";
|
|
245
|
+
const title = asString(value?.title) || WORKSPACE_POLICY_TITLES[profileId] || "Workspace Policy";
|
|
246
|
+
const summary = asString(value?.summary)
|
|
247
|
+
|| `Policy ${profileId} carregada para o workspace ${workspaceId} com ${allowedActionTypes.length} action types permitidos.`;
|
|
248
|
+
return {
|
|
249
|
+
profile_id: profileId || "observe_only",
|
|
250
|
+
title,
|
|
251
|
+
summary,
|
|
252
|
+
allowed_action_types: allowedActionTypes,
|
|
253
|
+
blocked_action_types: blockedActionTypes,
|
|
254
|
+
requires_confirmation_action_types: requiresConfirmationActionTypes,
|
|
255
|
+
rationale: asString(value?.rationale) || (normalizedActionTypes.length > 0
|
|
256
|
+
? `Perfil inferido a partir das actions do runtime: ${normalizedActionTypes.join(", ")}.`
|
|
257
|
+
: undefined),
|
|
258
|
+
allow_shell: allowShell,
|
|
259
|
+
allow_code_write: allowCodeWrite,
|
|
260
|
+
allow_destructive: allowDestructive,
|
|
261
|
+
allow_release: allowRelease,
|
|
262
|
+
enforced_by: uniqueRuntimeStrings(value?.enforced_by?.length ? value.enforced_by : ["bridge_workspace_runtime"], 8),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function isPathInsideRoot(candidatePath, rootPath) {
|
|
266
|
+
const relativePath = path.relative(rootPath, candidatePath);
|
|
267
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
268
|
+
}
|
|
269
|
+
function findBestMatchingRoot(candidatePath, roots) {
|
|
270
|
+
return roots
|
|
271
|
+
.filter((root) => isPathInsideRoot(candidatePath, root.resolved_path))
|
|
272
|
+
.sort((left, right) => right.resolved_path.length - left.resolved_path.length)[0];
|
|
273
|
+
}
|
|
274
|
+
function dirnameForTarget(target, resolvedPath) {
|
|
275
|
+
if (target.kind === "directory" || target.kind === "shell_cwd") {
|
|
276
|
+
return resolvedPath;
|
|
277
|
+
}
|
|
278
|
+
return path.dirname(resolvedPath);
|
|
279
|
+
}
|
|
280
|
+
function looksLikeFilePath(targetPath) {
|
|
281
|
+
const trimmed = targetPath.trim().replace(/[\\/]+$/, "");
|
|
282
|
+
if (!trimmed || trimmed === "~" || trimmed === "." || trimmed === path.sep) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return path.basename(trimmed).includes(".");
|
|
286
|
+
}
|
|
287
|
+
function inferRuntimeWorkspaceContextFromActions(actions) {
|
|
288
|
+
const roots = [];
|
|
289
|
+
const targets = [];
|
|
290
|
+
const rootKeys = new Set();
|
|
291
|
+
let targetIndex = 1;
|
|
292
|
+
const pushRoot = (rootPath, kind, scopeReason, actionType) => {
|
|
293
|
+
const key = `${kind}:${rootPath}`;
|
|
294
|
+
if (rootKeys.has(key)) {
|
|
295
|
+
const existing = roots.find((item) => `${item.kind || ""}:${item.path}` === key);
|
|
296
|
+
if (existing) {
|
|
297
|
+
existing.action_types = uniqueStrings([...(existing.action_types || []), actionType]);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
rootKeys.add(key);
|
|
302
|
+
roots.push({
|
|
303
|
+
root_id: `workspace_root_${String(roots.length + 1).padStart(2, "0")}`,
|
|
304
|
+
path: rootPath,
|
|
305
|
+
kind,
|
|
306
|
+
scope_reason: scopeReason,
|
|
307
|
+
action_types: [actionType],
|
|
308
|
+
source: "bridge_action_fallback",
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
for (const action of actions) {
|
|
312
|
+
const actionType = asString(action.type).toLowerCase();
|
|
313
|
+
if (!actionType) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (actionType === "git_clone") {
|
|
317
|
+
const destinationPath = asString(action.destination_path)
|
|
318
|
+
|| asString(action.destination)
|
|
319
|
+
|| asString(action.path);
|
|
320
|
+
if (!destinationPath) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const expandedDestinationPath = expandUserPathLike(destinationPath);
|
|
324
|
+
const parentPath = path.dirname(expandedDestinationPath);
|
|
325
|
+
pushRoot(parentPath, "declared_path", "git_clone_destination_parent", actionType);
|
|
326
|
+
targets.push({
|
|
327
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
328
|
+
path: parentPath,
|
|
329
|
+
kind: "shell_cwd",
|
|
330
|
+
access_mode: "execute",
|
|
331
|
+
action_type: actionType,
|
|
332
|
+
source: "bridge_action_fallback",
|
|
333
|
+
});
|
|
334
|
+
targetIndex += 1;
|
|
335
|
+
targets.push({
|
|
336
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
337
|
+
path: expandedDestinationPath,
|
|
338
|
+
kind: "directory",
|
|
339
|
+
access_mode: "write",
|
|
340
|
+
action_type: actionType,
|
|
341
|
+
source: "bridge_action_fallback",
|
|
342
|
+
});
|
|
343
|
+
targetIndex += 1;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (actionType === "run_shell" || actionType === "git_status" || actionType === "git_diff" || actionType === "git_add" || actionType === "git_commit" || actionType === "git_push" || actionType === "git_fetch" || actionType === "git_checkout" || actionType === "git_rebase" || actionType === "git_merge" || actionType === "git_tag" || actionType === "run_tests" || actionType === "apply_patch") {
|
|
347
|
+
const cwd = asString(action.cwd) || ".";
|
|
348
|
+
const scopeReason = actionType === "run_shell"
|
|
349
|
+
? "shell_cwd_scope"
|
|
350
|
+
: actionType === "apply_patch"
|
|
351
|
+
? "patch_workspace_scope"
|
|
352
|
+
: actionType === "run_tests"
|
|
353
|
+
? "test_runner_scope"
|
|
354
|
+
: "git_repo_scope";
|
|
355
|
+
pushRoot(cwd, "process_cwd", scopeReason, actionType);
|
|
356
|
+
if (actionType === "apply_patch") {
|
|
357
|
+
const patchTargets = collectStructuredPatchTargets(asString(action.patch) || "");
|
|
358
|
+
if (patchTargets.length > 0) {
|
|
359
|
+
for (const patchTarget of patchTargets) {
|
|
360
|
+
targets.push({
|
|
361
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
362
|
+
path: patchTarget.path,
|
|
363
|
+
kind: "path",
|
|
364
|
+
access_mode: patchTarget.accessMode,
|
|
365
|
+
action_type: actionType,
|
|
366
|
+
source: "bridge_action_fallback",
|
|
367
|
+
});
|
|
368
|
+
targetIndex += 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
targets.push({
|
|
373
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
374
|
+
path: cwd,
|
|
375
|
+
kind: "path",
|
|
376
|
+
access_mode: "write",
|
|
377
|
+
action_type: actionType,
|
|
378
|
+
source: "bridge_action_fallback",
|
|
379
|
+
});
|
|
380
|
+
targetIndex += 1;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else if (actionType === "git_add") {
|
|
384
|
+
targets.push({
|
|
385
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
386
|
+
path: cwd,
|
|
387
|
+
kind: "shell_cwd",
|
|
388
|
+
access_mode: "execute",
|
|
389
|
+
action_type: actionType,
|
|
390
|
+
source: "bridge_action_fallback",
|
|
391
|
+
});
|
|
392
|
+
targetIndex += 1;
|
|
393
|
+
const rawPaths = Array.isArray(action.paths)
|
|
394
|
+
? action.paths || []
|
|
395
|
+
: [];
|
|
396
|
+
for (const rawPath of rawPaths) {
|
|
397
|
+
const targetPath = asString(rawPath);
|
|
398
|
+
if (!targetPath) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
targets.push({
|
|
402
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
403
|
+
path: expandUserPathLike(targetPath, expandUserPathLike(cwd)),
|
|
404
|
+
kind: "path",
|
|
405
|
+
access_mode: "write",
|
|
406
|
+
action_type: actionType,
|
|
407
|
+
source: "bridge_action_fallback",
|
|
408
|
+
});
|
|
409
|
+
targetIndex += 1;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
targets.push({
|
|
414
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
415
|
+
path: cwd,
|
|
416
|
+
kind: "shell_cwd",
|
|
417
|
+
access_mode: "execute",
|
|
418
|
+
action_type: actionType,
|
|
419
|
+
source: "bridge_action_fallback",
|
|
420
|
+
});
|
|
421
|
+
targetIndex += 1;
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const targetPath = asString(action.path)
|
|
426
|
+
|| (actionType === "move_file"
|
|
427
|
+
? asString(action.source_path)
|
|
428
|
+
: "");
|
|
429
|
+
if (!targetPath) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (actionType === "read_file") {
|
|
433
|
+
pushRoot(path.dirname(targetPath), "declared_path", "file_parent_scope", actionType);
|
|
434
|
+
targets.push({
|
|
435
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
436
|
+
path: targetPath,
|
|
437
|
+
kind: "file",
|
|
438
|
+
access_mode: "read",
|
|
439
|
+
action_type: actionType,
|
|
440
|
+
source: "bridge_action_fallback",
|
|
441
|
+
});
|
|
442
|
+
targetIndex += 1;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (actionType === "write_text_file") {
|
|
446
|
+
const filename = asString(action.filename) || undefined;
|
|
447
|
+
const rootPath = filename
|
|
448
|
+
? targetPath
|
|
449
|
+
: (looksLikeFilePath(targetPath) ? path.dirname(targetPath) : targetPath);
|
|
450
|
+
pushRoot(rootPath, "declared_path", "write_scope", actionType);
|
|
451
|
+
targets.push({
|
|
452
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
453
|
+
path: targetPath,
|
|
454
|
+
kind: "file",
|
|
455
|
+
access_mode: "write",
|
|
456
|
+
action_type: actionType,
|
|
457
|
+
source: "bridge_action_fallback",
|
|
458
|
+
filename,
|
|
459
|
+
});
|
|
460
|
+
targetIndex += 1;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (actionType === "write_json_file") {
|
|
464
|
+
const filename = asString(action.filename) || undefined;
|
|
465
|
+
const rootPath = filename
|
|
466
|
+
? targetPath
|
|
467
|
+
: (looksLikeFilePath(targetPath) ? path.dirname(targetPath) : targetPath);
|
|
468
|
+
pushRoot(rootPath, "declared_path", "write_scope", actionType);
|
|
469
|
+
targets.push({
|
|
470
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
471
|
+
path: targetPath,
|
|
472
|
+
kind: "file",
|
|
473
|
+
access_mode: "write",
|
|
474
|
+
action_type: actionType,
|
|
475
|
+
source: "bridge_action_fallback",
|
|
476
|
+
filename,
|
|
477
|
+
});
|
|
478
|
+
targetIndex += 1;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (actionType === "mkdir") {
|
|
482
|
+
pushRoot(path.dirname(targetPath), "declared_path", "directory_target_parent", actionType);
|
|
483
|
+
targets.push({
|
|
484
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
485
|
+
path: targetPath,
|
|
486
|
+
kind: "directory",
|
|
487
|
+
access_mode: "write",
|
|
488
|
+
action_type: actionType,
|
|
489
|
+
source: "bridge_action_fallback",
|
|
490
|
+
});
|
|
491
|
+
targetIndex += 1;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (actionType === "move_file") {
|
|
495
|
+
const sourcePath = targetPath;
|
|
496
|
+
const destinationPath = asString(action.destination_path)
|
|
497
|
+
|| asString(action.destination);
|
|
498
|
+
if (sourcePath) {
|
|
499
|
+
pushRoot(path.dirname(sourcePath), "declared_path", "move_source_parent", actionType);
|
|
500
|
+
}
|
|
501
|
+
if (destinationPath) {
|
|
502
|
+
pushRoot(path.dirname(destinationPath), "declared_path", "move_destination_parent", actionType);
|
|
503
|
+
}
|
|
504
|
+
if (sourcePath) {
|
|
505
|
+
targets.push({
|
|
506
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
507
|
+
path: sourcePath,
|
|
508
|
+
kind: "path",
|
|
509
|
+
access_mode: "write",
|
|
510
|
+
action_type: actionType,
|
|
511
|
+
source: "bridge_action_fallback",
|
|
512
|
+
});
|
|
513
|
+
targetIndex += 1;
|
|
514
|
+
}
|
|
515
|
+
if (destinationPath) {
|
|
516
|
+
targets.push({
|
|
517
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
518
|
+
path: destinationPath,
|
|
519
|
+
kind: "path",
|
|
520
|
+
access_mode: "write",
|
|
521
|
+
action_type: actionType,
|
|
522
|
+
source: "bridge_action_fallback",
|
|
523
|
+
});
|
|
524
|
+
targetIndex += 1;
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (actionType === "trash_path") {
|
|
529
|
+
pushRoot(path.dirname(targetPath), "declared_path", "delete_target_parent", actionType);
|
|
530
|
+
targets.push({
|
|
531
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
532
|
+
path: targetPath,
|
|
533
|
+
kind: "path",
|
|
534
|
+
access_mode: "delete",
|
|
535
|
+
action_type: actionType,
|
|
536
|
+
source: "bridge_action_fallback",
|
|
537
|
+
});
|
|
538
|
+
targetIndex += 1;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (actionType === "delete_file") {
|
|
542
|
+
pushRoot(path.dirname(targetPath), "declared_path", "delete_target_parent", actionType);
|
|
543
|
+
targets.push({
|
|
544
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
545
|
+
path: targetPath,
|
|
546
|
+
kind: "path",
|
|
547
|
+
access_mode: "delete",
|
|
548
|
+
action_type: actionType,
|
|
549
|
+
source: "bridge_action_fallback",
|
|
550
|
+
});
|
|
551
|
+
targetIndex += 1;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (actionType === "filesystem_inspect" || actionType === "list_files" || actionType === "count_files") {
|
|
555
|
+
pushRoot(targetPath, "declared_path", "filesystem_scope", actionType);
|
|
556
|
+
targets.push({
|
|
557
|
+
target_id: `workspace_target_${String(targetIndex).padStart(2, "0")}`,
|
|
558
|
+
path: targetPath,
|
|
559
|
+
kind: "directory",
|
|
560
|
+
access_mode: "enumerate",
|
|
561
|
+
action_type: actionType,
|
|
562
|
+
source: "bridge_action_fallback",
|
|
563
|
+
});
|
|
564
|
+
targetIndex += 1;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (targets.length === 0) {
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
workspace_id: "workspace_bridge_fallback",
|
|
572
|
+
roots,
|
|
573
|
+
targets,
|
|
574
|
+
instruction_bundle: {
|
|
575
|
+
resolver: "bridge_local_agents_md",
|
|
576
|
+
entrypoints: ["AGENTS.md"],
|
|
577
|
+
precedence: ["nearest_target_agents_md", "workspace_repo_agents_md", "monorepo_root_agents_md"],
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
export function expandUserPathLike(value, baseCwd = process.cwd()) {
|
|
582
|
+
const trimmed = asString(value);
|
|
583
|
+
if (!trimmed) {
|
|
584
|
+
return os.homedir();
|
|
585
|
+
}
|
|
586
|
+
if (trimmed === "~") {
|
|
587
|
+
return os.homedir();
|
|
588
|
+
}
|
|
589
|
+
if (trimmed.startsWith("~/")) {
|
|
590
|
+
return path.join(os.homedir(), trimmed.slice(2));
|
|
591
|
+
}
|
|
592
|
+
if (path.isAbsolute(trimmed)) {
|
|
593
|
+
return trimmed;
|
|
594
|
+
}
|
|
595
|
+
return path.resolve(baseCwd, trimmed);
|
|
596
|
+
}
|
|
597
|
+
async function findRepoRoot(startPath) {
|
|
598
|
+
let current = startPath;
|
|
599
|
+
while (true) {
|
|
600
|
+
try {
|
|
601
|
+
const gitStat = await stat(path.join(current, ".git"));
|
|
602
|
+
if (gitStat.isDirectory() || gitStat.isFile()) {
|
|
603
|
+
return current;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
// Keep walking upwards.
|
|
608
|
+
}
|
|
609
|
+
const parent = path.dirname(current);
|
|
610
|
+
if (parent === current) {
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
current = parent;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function resolveWorkspaceScanBasePath(roots, repoRoot) {
|
|
617
|
+
if (repoRoot) {
|
|
618
|
+
const matchingRoot = findBestMatchingRoot(repoRoot, roots);
|
|
619
|
+
if (matchingRoot) {
|
|
620
|
+
return {
|
|
621
|
+
basePath: repoRoot,
|
|
622
|
+
repoRootWithinWorkspace: true,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
basePath: roots[0]?.resolved_path || process.cwd(),
|
|
628
|
+
repoRootWithinWorkspace: false,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
async function pathExists(candidatePath) {
|
|
632
|
+
try {
|
|
633
|
+
await stat(candidatePath);
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
export async function buildWorkspaceIndex(options) {
|
|
641
|
+
if (options.roots.length === 0) {
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
644
|
+
const descriptor = options.descriptor;
|
|
645
|
+
const { basePath } = resolveWorkspaceScanBasePath(options.roots, options.repoRoot);
|
|
646
|
+
const resolver = asString(descriptor?.resolver) || "bridge_workspace_scan";
|
|
647
|
+
const maxDepth = clampInteger(descriptor?.max_depth, 3, 1, 8);
|
|
648
|
+
const maxDirectories = clampInteger(descriptor?.max_directories, 48, 1, 256);
|
|
649
|
+
const maxFiles = clampInteger(descriptor?.max_files, 160, 1, 2000);
|
|
650
|
+
const ignoredDirectoryNames = new Set(uniqueStrings([
|
|
651
|
+
...(descriptor?.ignored_directory_names || []),
|
|
652
|
+
...DEFAULT_IGNORED_DIRECTORY_NAMES,
|
|
653
|
+
]));
|
|
654
|
+
const keyFilePatterns = new Set(uniqueStrings([
|
|
655
|
+
...(descriptor?.key_file_patterns || []),
|
|
656
|
+
...DEFAULT_KEY_FILE_PATTERNS,
|
|
657
|
+
]));
|
|
658
|
+
const queue = [{ directoryPath: basePath, depth: 0 }];
|
|
659
|
+
const topLevelEntries = [];
|
|
660
|
+
const keyFiles = new Set();
|
|
661
|
+
const extensionCounts = new Map();
|
|
662
|
+
let scannedDirectoryCount = 0;
|
|
663
|
+
let scannedFileCount = 0;
|
|
664
|
+
let truncated = false;
|
|
665
|
+
while (queue.length > 0) {
|
|
666
|
+
if (scannedDirectoryCount >= maxDirectories || scannedFileCount >= maxFiles) {
|
|
667
|
+
truncated = true;
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
const next = queue.shift();
|
|
671
|
+
if (!next) {
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
scannedDirectoryCount += 1;
|
|
675
|
+
let entries;
|
|
676
|
+
try {
|
|
677
|
+
entries = await readdir(next.directoryPath, { withFileTypes: true });
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
683
|
+
for (const entry of entries) {
|
|
684
|
+
const candidatePath = path.join(next.directoryPath, entry.name);
|
|
685
|
+
const relativePath = toRelativePath(basePath, candidatePath);
|
|
686
|
+
if (next.depth === 0 && topLevelEntries.length < WORKSPACE_INDEX_TOP_LEVEL_LIMIT) {
|
|
687
|
+
topLevelEntries.push({
|
|
688
|
+
path: relativePath,
|
|
689
|
+
kind: entry.isDirectory() ? "directory" : "file",
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
if (entry.isDirectory()) {
|
|
693
|
+
if (ignoredDirectoryNames.has(entry.name)) {
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (next.depth + 1 <= maxDepth) {
|
|
697
|
+
queue.push({
|
|
698
|
+
directoryPath: candidatePath,
|
|
699
|
+
depth: next.depth + 1,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
truncated = true;
|
|
704
|
+
}
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (!entry.isFile()) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
scannedFileCount += 1;
|
|
711
|
+
if (keyFilePatterns.has(entry.name)) {
|
|
712
|
+
keyFiles.add(relativePath);
|
|
713
|
+
}
|
|
714
|
+
const extension = path.extname(entry.name).toLowerCase() || "[no_ext]";
|
|
715
|
+
extensionCounts.set(extension, (extensionCounts.get(extension) || 0) + 1);
|
|
716
|
+
if (scannedFileCount >= maxFiles) {
|
|
717
|
+
truncated = true;
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const dominantExtensions = Array.from(extensionCounts.entries())
|
|
723
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
724
|
+
.slice(0, WORKSPACE_INDEX_EXTENSION_LIMIT)
|
|
725
|
+
.map(([extension, count]) => ({ extension, count }));
|
|
726
|
+
return {
|
|
727
|
+
resolver,
|
|
728
|
+
workspace_id: options.workspaceId,
|
|
729
|
+
base_path: basePath,
|
|
730
|
+
scanned_directory_count: scannedDirectoryCount,
|
|
731
|
+
scanned_file_count: scannedFileCount,
|
|
732
|
+
truncated,
|
|
733
|
+
top_level_entries: topLevelEntries,
|
|
734
|
+
key_files: Array.from(keyFiles).sort((left, right) => left.localeCompare(right)),
|
|
735
|
+
dominant_extensions: dominantExtensions,
|
|
736
|
+
summary: `Indexei ${scannedFileCount} arquivo${scannedFileCount === 1 ? "" : "s"} e ${scannedDirectoryCount} diretorio${scannedDirectoryCount === 1 ? "" : "s"} em ${path.basename(basePath) || basePath}.`,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
export async function buildRepoManifest(options) {
|
|
740
|
+
if (options.roots.length === 0) {
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
const descriptor = options.descriptor;
|
|
744
|
+
const { basePath, repoRootWithinWorkspace } = resolveWorkspaceScanBasePath(options.roots, options.repoRoot);
|
|
745
|
+
const resolver = asString(descriptor?.resolver) || "bridge_workspace_repo_probe";
|
|
746
|
+
const markerPaths = uniqueStrings([...(descriptor?.marker_paths || []), ...DEFAULT_REPO_MARKERS]);
|
|
747
|
+
const manifestPaths = uniqueStrings([...(descriptor?.manifest_paths || []), ...DEFAULT_REPO_MANIFEST_PATHS]);
|
|
748
|
+
const lockfilePaths = uniqueStrings([...(descriptor?.lockfile_paths || []), ...DEFAULT_REPO_LOCKFILE_PATHS]);
|
|
749
|
+
const includeInstructionSources = descriptor?.include_instruction_sources !== false;
|
|
750
|
+
const detectedMarkers = [];
|
|
751
|
+
const manifestFiles = [];
|
|
752
|
+
const lockfiles = [];
|
|
753
|
+
for (const relativePath of markerPaths) {
|
|
754
|
+
if (await pathExists(path.join(basePath, relativePath))) {
|
|
755
|
+
detectedMarkers.push(relativePath);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
for (const relativePath of manifestPaths) {
|
|
759
|
+
if (await pathExists(path.join(basePath, relativePath))) {
|
|
760
|
+
manifestFiles.push(relativePath);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
for (const relativePath of lockfilePaths) {
|
|
764
|
+
if (await pathExists(path.join(basePath, relativePath))) {
|
|
765
|
+
lockfiles.push(relativePath);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const packageManagers = uniqueStrings(lockfiles.map((item) => LOCKFILE_PACKAGE_MANAGER_MAP[item] || ""))
|
|
769
|
+
.sort((left, right) => left.localeCompare(right));
|
|
770
|
+
const instructionSourcePaths = includeInstructionSources
|
|
771
|
+
? uniqueStrings(options.instructionBundle?.sources.map((item) => toRelativePath(basePath, item.path)) || [])
|
|
772
|
+
.sort((left, right) => left.localeCompare(right))
|
|
773
|
+
: [];
|
|
774
|
+
const targetPaths = uniqueStrings(options.targets.map((target) => toRelativePath(basePath, target.resolved_path)))
|
|
775
|
+
.sort((left, right) => left.localeCompare(right));
|
|
776
|
+
const vcs = asString(descriptor?.vcs_hint) || (options.repoRoot ? "git" : "filesystem");
|
|
777
|
+
return {
|
|
778
|
+
resolver,
|
|
779
|
+
workspace_id: options.workspaceId,
|
|
780
|
+
scoped_root_path: basePath,
|
|
781
|
+
detected_repo_root: options.repoRoot || undefined,
|
|
782
|
+
repo_root_within_workspace: repoRootWithinWorkspace,
|
|
783
|
+
repo_name: path.basename(basePath) || basePath,
|
|
784
|
+
vcs,
|
|
785
|
+
detected_markers: detectedMarkers,
|
|
786
|
+
manifest_files: manifestFiles,
|
|
787
|
+
lockfiles,
|
|
788
|
+
package_managers: packageManagers,
|
|
789
|
+
instruction_source_paths: instructionSourcePaths,
|
|
790
|
+
target_paths: targetPaths,
|
|
791
|
+
summary: `Repo manifest para ${path.basename(basePath) || basePath} com ${manifestFiles.length} manifest${manifestFiles.length === 1 ? "" : "s"} e ${lockfiles.length} lockfile${lockfiles.length === 1 ? "" : "s"}.`,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
export function buildWorkspaceMemory(options) {
|
|
795
|
+
const workspaceId = asString(options.workspaceId) || options.workspace?.workspaceId || "workspace_local_runtime";
|
|
796
|
+
const workspace = options.workspace || undefined;
|
|
797
|
+
const priorMemory = options.priorMemory;
|
|
798
|
+
const currentActionTypes = uniqueRuntimeStrings((options.actions || []).map((action) => asString(action.type).toLowerCase()), 16);
|
|
799
|
+
const currentTargetPaths = uniqueRuntimeStrings((workspace?.targets || []).map((target) => target.resolved_path || target.path), 16);
|
|
800
|
+
const currentArtifactKinds = uniqueRuntimeStrings((options.artifacts || []).map((artifact) => asString(artifact.kind).toLowerCase()), 16);
|
|
801
|
+
const recentJobIds = uniqueRuntimeStrings([options.jobId, ...(priorMemory?.recent_job_ids || [])], 12);
|
|
802
|
+
const recentActionTypes = uniqueRuntimeStrings([...currentActionTypes, ...(priorMemory?.recent_action_types || [])], 16);
|
|
803
|
+
const recentTargetPaths = uniqueRuntimeStrings([...currentTargetPaths, ...(priorMemory?.recent_target_paths || [])], 16);
|
|
804
|
+
const recentArtifactKinds = uniqueRuntimeStrings([...currentArtifactKinds, ...(priorMemory?.recent_artifact_kinds || [])], 16);
|
|
805
|
+
const jobCount = Math.max(recentJobIds.length, (priorMemory?.job_count || 0) + (options.jobId && options.jobId !== priorMemory?.latest_job_id ? 1 : 0));
|
|
806
|
+
const latestJobStatus = asString(options.jobStatus) || priorMemory?.latest_job_status || undefined;
|
|
807
|
+
const scopedRootPath = workspace?.repoManifest?.scoped_root_path
|
|
808
|
+
|| priorMemory?.scoped_root_path
|
|
809
|
+
|| workspace?.roots[0]?.resolved_path
|
|
810
|
+
|| undefined;
|
|
811
|
+
const repoRoot = workspace?.repoManifest?.detected_repo_root
|
|
812
|
+
|| workspace?.repoRoot
|
|
813
|
+
|| priorMemory?.repo_root
|
|
814
|
+
|| undefined;
|
|
815
|
+
const instructionDigest = workspace?.instructionBundle?.digest || priorMemory?.instruction_digest || undefined;
|
|
816
|
+
const updatedAt = asString(options.updatedAt) || new Date().toISOString();
|
|
817
|
+
return {
|
|
818
|
+
memory_id: `workspace_memory.${workspaceId}`,
|
|
819
|
+
workspace_id: workspaceId,
|
|
820
|
+
source: workspace ? "bridge_workspace_runtime" : (priorMemory?.source || "device_job_history"),
|
|
821
|
+
job_count: Math.max(0, jobCount),
|
|
822
|
+
recent_job_ids: recentJobIds,
|
|
823
|
+
recent_action_types: recentActionTypes,
|
|
824
|
+
recent_target_paths: recentTargetPaths,
|
|
825
|
+
recent_artifact_kinds: recentArtifactKinds,
|
|
826
|
+
latest_job_id: asString(options.jobId) || priorMemory?.latest_job_id || undefined,
|
|
827
|
+
latest_job_status: latestJobStatus,
|
|
828
|
+
latest_job_at: updatedAt,
|
|
829
|
+
instruction_digest: instructionDigest,
|
|
830
|
+
repo_root: repoRoot,
|
|
831
|
+
scoped_root_path: scopedRootPath,
|
|
832
|
+
summary: `Memoria curta do workspace ${workspaceId} com ${Math.max(0, jobCount)} job${jobCount === 1 ? "" : "s"} recente${jobCount === 1 ? "" : "s"}; ultima execucao ${latestJobStatus || "desconhecida"}.`,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
async function buildInstructionBundle(options) {
|
|
836
|
+
const descriptor = options.descriptor;
|
|
837
|
+
const entrypoints = descriptor?.entrypoints?.length ? descriptor.entrypoints : ["AGENTS.md"];
|
|
838
|
+
const precedence = descriptor?.precedence?.length
|
|
839
|
+
? descriptor.precedence
|
|
840
|
+
: ["nearest_target_agents_md", "workspace_repo_agents_md", "monorepo_root_agents_md"];
|
|
841
|
+
const resolver = asString(descriptor?.resolver) || "bridge_local_agents_md";
|
|
842
|
+
const discovered = [];
|
|
843
|
+
const seen = new Set();
|
|
844
|
+
const repoRoot = options.repoRoot;
|
|
845
|
+
for (const target of options.targets) {
|
|
846
|
+
let current = target.directory_path;
|
|
847
|
+
const upperBound = repoRoot || findBestMatchingRoot(current, options.roots)?.resolved_path || current;
|
|
848
|
+
while (true) {
|
|
849
|
+
for (const entrypoint of entrypoints) {
|
|
850
|
+
const candidate = path.join(current, entrypoint);
|
|
851
|
+
if (seen.has(candidate)) {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
const fileStat = await stat(candidate);
|
|
856
|
+
if (!fileStat.isFile()) {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const rawContent = await readFile(candidate, "utf8");
|
|
860
|
+
seen.add(candidate);
|
|
861
|
+
const scope = current === target.directory_path
|
|
862
|
+
? "nearest_target"
|
|
863
|
+
: repoRoot && current === repoRoot
|
|
864
|
+
? "repo_root"
|
|
865
|
+
: options.roots.some((root) => root.resolved_path === current)
|
|
866
|
+
? "workspace_root"
|
|
867
|
+
: "ancestor";
|
|
868
|
+
discovered.push({
|
|
869
|
+
path: candidate,
|
|
870
|
+
scope,
|
|
871
|
+
content: clipInstructionText(rawContent),
|
|
872
|
+
content_char_count: rawContent.length,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
// Ignore missing entrypoints.
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (current === upperBound) {
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
const parent = path.dirname(current);
|
|
883
|
+
if (parent === current) {
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
current = parent;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (discovered.length === 0) {
|
|
890
|
+
return undefined;
|
|
891
|
+
}
|
|
892
|
+
const digest = createHash("sha1")
|
|
893
|
+
.update(discovered.map((item) => `${item.path}:${item.content_char_count}:${item.content}`).join("\n"))
|
|
894
|
+
.digest("hex");
|
|
895
|
+
return {
|
|
896
|
+
resolver,
|
|
897
|
+
entrypoints,
|
|
898
|
+
precedence,
|
|
899
|
+
digest,
|
|
900
|
+
source_count: discovered.length,
|
|
901
|
+
sources: discovered,
|
|
902
|
+
summary: `Carreguei ${discovered.length} arquivo${discovered.length === 1 ? "" : "s"} ${entrypoints.join(", ")} para o workspace atual.`,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
export async function resolveWorkspaceContext(options) {
|
|
906
|
+
const baseCwd = options.baseCwd || process.cwd();
|
|
907
|
+
const declared = options.workspaceContext || inferRuntimeWorkspaceContextFromActions(options.actions || []);
|
|
908
|
+
if (!declared || !Array.isArray(declared.roots) || declared.roots.length === 0) {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
const resolvedRoots = declared.roots
|
|
912
|
+
.map((root, index) => {
|
|
913
|
+
const resolvedPath = expandUserPathLike(root.path, baseCwd);
|
|
914
|
+
return {
|
|
915
|
+
...root,
|
|
916
|
+
root_id: asString(root.root_id) || `workspace_root_${String(index + 1).padStart(2, "0")}`,
|
|
917
|
+
resolved_path: resolvedPath,
|
|
918
|
+
action_types: root.action_types || [],
|
|
919
|
+
};
|
|
920
|
+
})
|
|
921
|
+
.filter((root) => Boolean(root.resolved_path));
|
|
922
|
+
if (resolvedRoots.length === 0) {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
const resolvedTargets = [];
|
|
926
|
+
for (const [index, target] of (declared.targets || []).entries()) {
|
|
927
|
+
const expandedBasePath = expandUserPathLike(target.path, baseCwd);
|
|
928
|
+
const resolvedPath = target.filename
|
|
929
|
+
? path.join(expandedBasePath, target.filename)
|
|
930
|
+
: expandedBasePath;
|
|
931
|
+
const directoryPath = dirnameForTarget(target, resolvedPath);
|
|
932
|
+
const matchedRoot = findBestMatchingRoot(resolvedPath, resolvedRoots)
|
|
933
|
+
|| findBestMatchingRoot(directoryPath, resolvedRoots);
|
|
934
|
+
if (!matchedRoot) {
|
|
935
|
+
throw new Error(`O caminho ${target.path} fica fora do workspace permitido.`);
|
|
936
|
+
}
|
|
937
|
+
resolvedTargets.push({
|
|
938
|
+
...target,
|
|
939
|
+
target_id: asString(target.target_id) || `workspace_target_${String(index + 1).padStart(2, "0")}`,
|
|
940
|
+
resolved_path: resolvedPath,
|
|
941
|
+
directory_path: directoryPath,
|
|
942
|
+
root_id: matchedRoot.root_id,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
const repoRoot = await findRepoRoot(resolvedTargets[0]?.directory_path || resolvedRoots[0].resolved_path);
|
|
946
|
+
const workspaceId = asString(declared.workspace_id) || "workspace_local_runtime";
|
|
947
|
+
const runtimeActionTypes = uniqueStrings((options.actions || []).map((action) => asString(action.type).toLowerCase()));
|
|
948
|
+
const targetActionTypes = uniqueStrings((declared.targets || []).map((target) => asString(target.action_type).toLowerCase()));
|
|
949
|
+
const destructiveActionTypes = new Set();
|
|
950
|
+
for (const target of declared.targets || []) {
|
|
951
|
+
if (asString(target.access_mode).toLowerCase() === "delete" && asString(target.action_type)) {
|
|
952
|
+
destructiveActionTypes.add(asString(target.action_type).toLowerCase());
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
for (const action of options.actions || []) {
|
|
956
|
+
const actionType = asString(action.type).toLowerCase();
|
|
957
|
+
if (!actionType) {
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (actionType === "trash_path" || actionType === "delete_file") {
|
|
961
|
+
destructiveActionTypes.add(actionType);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (actionType === "apply_patch") {
|
|
965
|
+
const patchTargets = collectStructuredPatchTargets(asString(action.patch) || "");
|
|
966
|
+
if (patchTargets.some((item) => item.accessMode === "delete")) {
|
|
967
|
+
destructiveActionTypes.add(actionType);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const workspaceMemory = normalizeWorkspaceMemory(declared.workspace_memory, workspaceId);
|
|
972
|
+
const workspacePolicy = normalizeWorkspacePolicy(declared.workspace_policy, workspaceId, [...targetActionTypes, ...runtimeActionTypes], destructiveActionTypes);
|
|
973
|
+
const instructionBundle = await buildInstructionBundle({
|
|
974
|
+
descriptor: declared.instruction_bundle,
|
|
975
|
+
roots: resolvedRoots,
|
|
976
|
+
targets: resolvedTargets,
|
|
977
|
+
repoRoot,
|
|
978
|
+
});
|
|
979
|
+
const workspaceIndex = await buildWorkspaceIndex({
|
|
980
|
+
workspaceId,
|
|
981
|
+
roots: resolvedRoots,
|
|
982
|
+
repoRoot,
|
|
983
|
+
descriptor: declared.workspace_index,
|
|
984
|
+
});
|
|
985
|
+
const repoManifest = await buildRepoManifest({
|
|
986
|
+
workspaceId,
|
|
987
|
+
roots: resolvedRoots,
|
|
988
|
+
targets: resolvedTargets,
|
|
989
|
+
repoRoot,
|
|
990
|
+
instructionBundle,
|
|
991
|
+
descriptor: declared.repo_manifest,
|
|
992
|
+
});
|
|
993
|
+
const defaultCwd = resolvedTargets.find((target) => target.kind === "shell_cwd")?.resolved_path
|
|
994
|
+
|| resolvedRoots[0]?.resolved_path
|
|
995
|
+
|| baseCwd;
|
|
996
|
+
return {
|
|
997
|
+
workspaceId,
|
|
998
|
+
roots: resolvedRoots,
|
|
999
|
+
targets: resolvedTargets,
|
|
1000
|
+
repoRoot,
|
|
1001
|
+
defaultCwd,
|
|
1002
|
+
instructionBundle,
|
|
1003
|
+
repoManifest,
|
|
1004
|
+
workspaceIndex,
|
|
1005
|
+
workspaceMemory,
|
|
1006
|
+
workspacePolicy,
|
|
1007
|
+
summary: `Workspace ${workspaceId} com ${resolvedRoots.length} root${resolvedRoots.length === 1 ? "" : "s"} e ${resolvedTargets.length} target${resolvedTargets.length === 1 ? "" : "s"}.`,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
export function assertPathInsideWorkspace(workspace, targetPath, options) {
|
|
1011
|
+
const resolvedPath = options?.filename
|
|
1012
|
+
? path.join(expandUserPathLike(targetPath, options?.baseCwd || workspace.defaultCwd), options.filename)
|
|
1013
|
+
: expandUserPathLike(targetPath, options?.baseCwd || workspace.defaultCwd);
|
|
1014
|
+
const matchedRoot = findBestMatchingRoot(resolvedPath, workspace.roots)
|
|
1015
|
+
|| findBestMatchingRoot(path.dirname(resolvedPath), workspace.roots);
|
|
1016
|
+
if (!matchedRoot) {
|
|
1017
|
+
throw new Error(`O caminho ${targetPath} fica fora do workspace permitido (${workspace.workspaceId}).`);
|
|
1018
|
+
}
|
|
1019
|
+
return resolvedPath;
|
|
1020
|
+
}
|
|
1021
|
+
export function assertCwdInsideWorkspace(workspace, cwd) {
|
|
1022
|
+
const resolvedCwd = expandUserPathLike(cwd || workspace.defaultCwd, workspace.defaultCwd);
|
|
1023
|
+
const matchedRoot = findBestMatchingRoot(resolvedCwd, workspace.roots);
|
|
1024
|
+
if (!matchedRoot) {
|
|
1025
|
+
throw new Error(`O diretorio ${cwd || workspace.defaultCwd} fica fora do workspace permitido (${workspace.workspaceId}).`);
|
|
1026
|
+
}
|
|
1027
|
+
return resolvedCwd;
|
|
1028
|
+
}
|
|
1029
|
+
export function assertActionAllowedByWorkspacePolicy(workspace, actionType) {
|
|
1030
|
+
const normalizedActionType = asString(actionType).toLowerCase();
|
|
1031
|
+
if (!normalizedActionType || !WORKSPACE_POLICY_ALL_ACTIONS.has(normalizedActionType) && normalizedActionType !== "apply_patch") {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const workspacePolicy = workspace.workspacePolicy;
|
|
1035
|
+
if (!workspacePolicy) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (workspacePolicy.blocked_action_types.includes(normalizedActionType)) {
|
|
1039
|
+
throw new Error(`A action ${normalizedActionType} foi bloqueada pela policy ${workspacePolicy.profile_id} do workspace ${workspace.workspaceId}.`);
|
|
1040
|
+
}
|
|
1041
|
+
if (!workspacePolicy.allowed_action_types.includes(normalizedActionType)) {
|
|
1042
|
+
throw new Error(`A action ${normalizedActionType} nao esta permitida pela policy ${workspacePolicy.profile_id} do workspace ${workspace.workspaceId}.`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|