@rudderhq/agent-runtime-utils 0.2.0-canary.4 → 0.2.0-canary.40
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/dist/git-identity.d.ts +32 -0
- package/dist/git-identity.d.ts.map +1 -0
- package/dist/git-identity.js +207 -0
- package/dist/git-identity.js.map +1 -0
- package/dist/git-identity.test.d.ts +2 -0
- package/dist/git-identity.test.d.ts.map +1 -0
- package/dist/git-identity.test.js +327 -0
- package/dist/git-identity.test.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/log-redaction.d.ts.map +1 -1
- package/dist/log-redaction.js +9 -0
- package/dist/log-redaction.js.map +1 -1
- package/dist/server-utils.d.ts +55 -6
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.js +431 -22
- package/dist/server-utils.js.map +1 -1
- package/dist/server-utils.test.js +378 -9
- package/dist/server-utils.test.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server-utils.js
CHANGED
|
@@ -27,6 +27,38 @@ const RUDDER_SKILL_ROOT_RELATIVE_CANDIDATES = [
|
|
|
27
27
|
"../../skills",
|
|
28
28
|
"../../../../../server/resources/bundled-skills",
|
|
29
29
|
];
|
|
30
|
+
const DEFAULT_LOCAL_CLI_CREDENTIAL_HOME_ENTRIES = [
|
|
31
|
+
".aws",
|
|
32
|
+
".azure",
|
|
33
|
+
".config/gh",
|
|
34
|
+
".config/gcloud",
|
|
35
|
+
".config/op",
|
|
36
|
+
".config/vercel",
|
|
37
|
+
".config/configstore",
|
|
38
|
+
".docker",
|
|
39
|
+
".fly",
|
|
40
|
+
".git-credentials",
|
|
41
|
+
".gnupg",
|
|
42
|
+
".kube",
|
|
43
|
+
".netrc",
|
|
44
|
+
".npmrc",
|
|
45
|
+
".ssh",
|
|
46
|
+
".vercel",
|
|
47
|
+
"Library/Application Support/gh",
|
|
48
|
+
"Library/Application Support/com.heroku.cli",
|
|
49
|
+
];
|
|
50
|
+
const DEFAULT_LOCAL_CLI_OPERATOR_HOME_SHIM_COMMANDS = [
|
|
51
|
+
{
|
|
52
|
+
command: "gh",
|
|
53
|
+
authCheckArgs: ["auth", "status"],
|
|
54
|
+
credentialEntries: [".config/gh", "Library/Application Support/gh"],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
command: "vercel",
|
|
58
|
+
authCheckArgs: ["whoami"],
|
|
59
|
+
credentialEntries: [".config/vercel", ".vercel", ".config/configstore"],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
30
62
|
function normalizePathSlashes(value) {
|
|
31
63
|
return value.replaceAll("\\", "/");
|
|
32
64
|
}
|
|
@@ -161,10 +193,68 @@ export function resolvePathValue(obj, dottedPath) {
|
|
|
161
193
|
export function renderTemplate(template, data) {
|
|
162
194
|
return template.replace(/{{\s*([a-zA-Z0-9_.-]+)\s*}}/g, (_, path) => resolvePathValue(data, path));
|
|
163
195
|
}
|
|
196
|
+
const ISSUE_DOCUMENT_PROMPT_BODY_CHAR_LIMIT = 16_000;
|
|
197
|
+
function truncateIssueDocumentBody(body) {
|
|
198
|
+
if (body.length <= ISSUE_DOCUMENT_PROMPT_BODY_CHAR_LIMIT)
|
|
199
|
+
return body;
|
|
200
|
+
return `${body.slice(0, ISSUE_DOCUMENT_PROMPT_BODY_CHAR_LIMIT).trimEnd()}\n\n[Document truncated in prompt. Fetch the full document with the Rudder CLI.]`;
|
|
201
|
+
}
|
|
202
|
+
function formatDocumentHeading(key, title) {
|
|
203
|
+
const cleanTitle = typeof title === "string" ? title.trim() : "";
|
|
204
|
+
return cleanTitle ? `### ${key} — ${cleanTitle}` : `### ${key}`;
|
|
205
|
+
}
|
|
206
|
+
function readIssueDocumentPromptIssueId(input) {
|
|
207
|
+
const planIssueId = typeof input.planDocument?.issueId === "string" ? input.planDocument.issueId.trim() : "";
|
|
208
|
+
if (planIssueId)
|
|
209
|
+
return planIssueId;
|
|
210
|
+
for (const summary of input.documentSummaries ?? []) {
|
|
211
|
+
const issueId = typeof summary.issueId === "string" ? summary.issueId.trim() : "";
|
|
212
|
+
if (issueId)
|
|
213
|
+
return issueId;
|
|
214
|
+
}
|
|
215
|
+
return "<issue-id>";
|
|
216
|
+
}
|
|
217
|
+
export function buildIssueDocumentsPrompt(input) {
|
|
218
|
+
if (!input)
|
|
219
|
+
return "";
|
|
220
|
+
const sections = [];
|
|
221
|
+
const planKey = input.planDocument?.key?.trim() || input.legacyPlanDocument?.key?.trim() || "plan";
|
|
222
|
+
const planBody = input.planDocument?.body?.trim() || input.legacyPlanDocument?.body?.trim() || "";
|
|
223
|
+
if (planBody) {
|
|
224
|
+
sections.push([
|
|
225
|
+
formatDocumentHeading(planKey, input.planDocument?.title),
|
|
226
|
+
input.legacyPlanDocument ? "Source: legacy `<plan>` block in the issue description." : `Source: issue document \`${planKey}\`.`,
|
|
227
|
+
"",
|
|
228
|
+
truncateIssueDocumentBody(planBody),
|
|
229
|
+
].join("\n"));
|
|
230
|
+
}
|
|
231
|
+
const otherDocuments = (input.documentSummaries ?? []).filter((doc) => {
|
|
232
|
+
const key = typeof doc.key === "string" ? doc.key.trim() : "";
|
|
233
|
+
return key && key !== planKey;
|
|
234
|
+
});
|
|
235
|
+
if (otherDocuments.length > 0) {
|
|
236
|
+
const issueId = readIssueDocumentPromptIssueId(input);
|
|
237
|
+
sections.push([
|
|
238
|
+
"### Additional Issue Documents",
|
|
239
|
+
...otherDocuments.map((doc) => {
|
|
240
|
+
const key = doc.key?.trim() || "document";
|
|
241
|
+
const title = doc.title?.trim();
|
|
242
|
+
const revision = typeof doc.latestRevisionNumber === "number" ? `, revision ${doc.latestRevisionNumber}` : "";
|
|
243
|
+
const titlePart = title ? ` — ${title}` : "";
|
|
244
|
+
return `- \`${key}\`${titlePart}${revision}. Fetch with \`rudder issue documents get ${issueId} ${key} --json\`.`;
|
|
245
|
+
}),
|
|
246
|
+
].join("\n"));
|
|
247
|
+
}
|
|
248
|
+
if (sections.length === 0)
|
|
249
|
+
return "";
|
|
250
|
+
return ["## Issue Documents", ...sections].join("\n\n");
|
|
251
|
+
}
|
|
164
252
|
// Default prompt templates for different wake sources
|
|
165
253
|
export const DEFAULT_AGENT_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). Continue your Rudder work.
|
|
166
254
|
|
|
167
|
-
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
255
|
+
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
256
|
+
|
|
257
|
+
{{context.issueDocumentsPrompt}}`;
|
|
168
258
|
export const ISSUE_ASSIGN_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). You have been assigned to work on an issue.
|
|
169
259
|
|
|
170
260
|
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
@@ -179,6 +269,8 @@ export const ISSUE_ASSIGN_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent
|
|
|
179
269
|
**Description:**
|
|
180
270
|
{{issue.description}}
|
|
181
271
|
|
|
272
|
+
{{context.issueDocumentsPrompt}}
|
|
273
|
+
|
|
182
274
|
Your task is to review this issue and begin working on it. Use the available tools to explore the codebase, understand the requirements, and implement a solution.`;
|
|
183
275
|
export const COMMENT_MENTION_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). You were mentioned in a comment and your attention is needed.
|
|
184
276
|
|
|
@@ -192,6 +284,8 @@ export const COMMENT_MENTION_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{ag
|
|
|
192
284
|
**Issue Description:**
|
|
193
285
|
{{issue.description}}
|
|
194
286
|
|
|
287
|
+
{{context.issueDocumentsPrompt}}
|
|
288
|
+
|
|
195
289
|
**Comment:**
|
|
196
290
|
{{comment.body}}
|
|
197
291
|
|
|
@@ -209,10 +303,31 @@ export const ISSUE_COMMENTED_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{ag
|
|
|
209
303
|
**Issue Description:**
|
|
210
304
|
{{issue.description}}
|
|
211
305
|
|
|
306
|
+
{{context.issueDocumentsPrompt}}
|
|
307
|
+
|
|
212
308
|
**Latest Comment:**
|
|
213
309
|
{{comment.body}}
|
|
214
310
|
|
|
215
311
|
Review the new comment and continue the issue from the current state. Respond or take action as needed.`;
|
|
312
|
+
export const ISSUE_CHANGES_REQUESTED_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). A reviewer requested changes on an issue you own.
|
|
313
|
+
|
|
314
|
+
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
315
|
+
|
|
316
|
+
## Context
|
|
317
|
+
|
|
318
|
+
**Issue:** {{issue.title}}
|
|
319
|
+
**ID:** {{issue.id}}
|
|
320
|
+
**Status:** {{issue.status}}
|
|
321
|
+
|
|
322
|
+
**Issue Description:**
|
|
323
|
+
{{issue.description}}
|
|
324
|
+
|
|
325
|
+
{{context.issueDocumentsPrompt}}
|
|
326
|
+
|
|
327
|
+
**Reviewer Comment:**
|
|
328
|
+
{{comment.body}}
|
|
329
|
+
|
|
330
|
+
Review the requested changes and continue the issue from the current state. Address the reviewer feedback before handing it back for review.`;
|
|
216
331
|
export const ISSUE_RECOVERY_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). This is a recovery run, not a fresh task.
|
|
217
332
|
|
|
218
333
|
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
@@ -235,6 +350,8 @@ export const ISSUE_RECOVERY_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{age
|
|
|
235
350
|
- Description:
|
|
236
351
|
{{issue.description}}
|
|
237
352
|
|
|
353
|
+
{{context.issueDocumentsPrompt}}
|
|
354
|
+
|
|
238
355
|
Before doing anything else, inspect what the previous run already completed and any side effects it may have caused. Continue the remaining work from the current state. Avoid blindly re-running the whole task.`;
|
|
239
356
|
export const RECOVERY_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). This is a recovery run, not a fresh task.
|
|
240
357
|
|
|
@@ -272,6 +389,8 @@ Reason: {{context.passiveFollowup.reason}}
|
|
|
272
389
|
- Description:
|
|
273
390
|
{{issue.description}}
|
|
274
391
|
|
|
392
|
+
{{context.issueDocumentsPrompt}}
|
|
393
|
+
|
|
275
394
|
Before changing the issue, inspect the current issue state and any side effects from the previous run. Then do exactly one close-out action: add a progress comment, mark the issue done, block it with a reason, or hand it off explicitly with explanation.`;
|
|
276
395
|
/**
|
|
277
396
|
* Selects the base heartbeat prompt template used by runtimes before final prompt assembly.
|
|
@@ -283,6 +402,9 @@ Before changing the issue, inspect the current issue state and any side effects
|
|
|
283
402
|
* - comment.mention:
|
|
284
403
|
* "You were mentioned in a comment ..."
|
|
285
404
|
* Includes issue summary plus mention comment body so the agent can respond without extra fetches.
|
|
405
|
+
* - issue_changes_requested:
|
|
406
|
+
* "A reviewer requested changes on an issue you own ..."
|
|
407
|
+
* Includes issue summary plus reviewer comment body so the assignee can act on feedback immediately.
|
|
286
408
|
* - issue_commented:
|
|
287
409
|
* "There is a new comment on an issue you own ..."
|
|
288
410
|
* Includes issue summary plus the newest comment body so the assignee can continue immediately.
|
|
@@ -331,6 +453,9 @@ export function selectPromptTemplate(configuredTemplate, context) {
|
|
|
331
453
|
if (wakeReason === "issue_passive_followup") {
|
|
332
454
|
return ISSUE_PASSIVE_FOLLOWUP_PROMPT_TEMPLATE;
|
|
333
455
|
}
|
|
456
|
+
if (wakeReason === "issue_changes_requested") {
|
|
457
|
+
return ISSUE_CHANGES_REQUESTED_PROMPT_TEMPLATE;
|
|
458
|
+
}
|
|
334
459
|
if (wakeSource === "assignment" || wakeReason === "issue_assigned") {
|
|
335
460
|
return ISSUE_ASSIGN_PROMPT_TEMPLATE;
|
|
336
461
|
}
|
|
@@ -362,10 +487,18 @@ export const RUDDER_AGENT_OPERATING_CONTRACT = [
|
|
|
362
487
|
"- Shared organization workspace root lives under `$RUDDER_ORG_WORKSPACE_ROOT`.",
|
|
363
488
|
"- Shared organization skills live under `$RUDDER_ORG_SKILLS_DIR`.",
|
|
364
489
|
"- Shared organization plans live under `$RUDDER_ORG_PLANS_DIR`.",
|
|
490
|
+
"- Shared organization artifacts live under `$RUDDER_ORG_ARTIFACTS_DIR`.",
|
|
491
|
+
"- Durable generated outputs such as screenshots, images, mockups, reports, CSVs, handoff logs, and other user-visible files should be written under `$RUDDER_ORG_ARTIFACTS_DIR` when available.",
|
|
492
|
+
"- Use `/tmp` only for transient scratch files and temporary verification artifacts; do not put durable work product there.",
|
|
493
|
+
"- Local trusted runtimes may expose the host operator home as `$RUDDER_OPERATOR_HOME`; use it only when a local skill or script intentionally needs operator-owned desktop app or CLI state. Do not replace `$HOME` with it.",
|
|
365
494
|
"- Durable shared work output should prefer these managed workspace paths instead of ad-hoc top-level `projects/` folders.",
|
|
366
495
|
"",
|
|
496
|
+
"When you create or copy a skill under `$AGENT_HOME/skills/<slug>/`, check the agent's Skills snapshot before claiming it will load in future runs. If it is installed but not enabled, say exactly that future runs will not load it until enabled, and offer to enable it with `rudder agent skills enable <agent-id> <selection-ref>` when you have permission.",
|
|
497
|
+
"",
|
|
367
498
|
"When you write issue comments or chat replies, match the language of the user's or board's most recent substantive message unless they explicitly ask for a different language.",
|
|
368
499
|
"",
|
|
500
|
+
"When an issue comment, done comment, or blocker comment cites visual evidence from a local screenshot/image path, attach the image with the Rudder CLI `--image <path>` option instead of leaving only the filesystem path in the text.",
|
|
501
|
+
"",
|
|
369
502
|
"## Memory and Planning",
|
|
370
503
|
"",
|
|
371
504
|
"You MUST use the `para-memory-files` skill for all memory operations: storing facts, writing daily notes, creating entities, running weekly synthesis, recalling past context, and managing plans. The skill defines your three-layer memory system (knowledge graph, daily notes, tacit knowledge), the PARA folder structure, atomic fact schemas, memory decay rules, and recall/planning conventions.",
|
|
@@ -379,9 +512,40 @@ export const RUDDER_AGENT_OPERATING_CONTRACT = [
|
|
|
379
512
|
"- Never exfiltrate secrets or private data.",
|
|
380
513
|
"- Do not perform any destructive commands unless explicitly requested by the board.",
|
|
381
514
|
].join("\n");
|
|
515
|
+
function toPromptPath(pathValue) {
|
|
516
|
+
return pathValue.split(path.sep).join("/");
|
|
517
|
+
}
|
|
518
|
+
function isInsidePath(parentPath, childPath) {
|
|
519
|
+
const relativePath = path.relative(parentPath, childPath);
|
|
520
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
521
|
+
}
|
|
522
|
+
function displayInstructionPath(filePath, instructionsFilePath) {
|
|
523
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
524
|
+
const resolvedInstructionsPath = path.resolve(instructionsFilePath);
|
|
525
|
+
const instructionsDir = path.dirname(resolvedInstructionsPath);
|
|
526
|
+
if (path.basename(instructionsDir) === "instructions") {
|
|
527
|
+
const agentHome = path.dirname(instructionsDir);
|
|
528
|
+
if (isInsidePath(agentHome, resolvedFilePath)) {
|
|
529
|
+
const relativePath = path.relative(agentHome, resolvedFilePath);
|
|
530
|
+
return relativePath ? `$AGENT_HOME/${toPromptPath(relativePath)}` : "$AGENT_HOME";
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return filePath;
|
|
534
|
+
}
|
|
535
|
+
function displayInstructionDir(filePath, instructionsFilePath) {
|
|
536
|
+
const displayPath = displayInstructionPath(filePath, instructionsFilePath);
|
|
537
|
+
const lastSlash = displayPath.lastIndexOf("/");
|
|
538
|
+
return lastSlash >= 0 ? `${displayPath.slice(0, lastSlash)}/` : "";
|
|
539
|
+
}
|
|
382
540
|
export async function loadAgentInstructionsPrefix(input) {
|
|
383
541
|
const instructionsFilePath = input.instructionsFilePath.trim();
|
|
384
542
|
const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
|
|
543
|
+
const displayInstructionsFilePath = instructionsFilePath
|
|
544
|
+
? displayInstructionPath(instructionsFilePath, instructionsFilePath)
|
|
545
|
+
: "";
|
|
546
|
+
const displayInstructionsDir = instructionsFilePath
|
|
547
|
+
? displayInstructionDir(instructionsFilePath, instructionsFilePath)
|
|
548
|
+
: "";
|
|
385
549
|
const warningStream = input.warningStream ?? "stdout";
|
|
386
550
|
const operatingContractSection = `${RUDDER_AGENT_OPERATING_CONTRACT}\n\n` +
|
|
387
551
|
"The above Rudder agent operating contract was injected by Rudder at runtime.";
|
|
@@ -413,29 +577,31 @@ export async function loadAgentInstructionsPrefix(input) {
|
|
|
413
577
|
loadedPaths.add(path.resolve(instructionsFilePath));
|
|
414
578
|
entrySection =
|
|
415
579
|
`${instructionsContents}\n\n` +
|
|
416
|
-
`The above agent instructions were loaded from ${
|
|
417
|
-
`Resolve any relative file references from ${
|
|
418
|
-
await input.onLog("stdout", `[rudder] Loaded agent instructions file: ${
|
|
580
|
+
`The above agent instructions were loaded from ${displayInstructionsFilePath}. ` +
|
|
581
|
+
`Resolve any relative file references from ${displayInstructionsDir}.`;
|
|
582
|
+
await input.onLog("stdout", `[rudder] Loaded agent instructions file: ${displayInstructionsFilePath}\n`);
|
|
419
583
|
}
|
|
420
584
|
catch (err) {
|
|
421
585
|
const reason = err instanceof Error ? err.message : String(err);
|
|
422
586
|
await input.onLog(warningStream, `[rudder] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`);
|
|
423
|
-
commandNotes.push(`Configured instructionsFilePath ${
|
|
587
|
+
commandNotes.push(`Configured instructionsFilePath ${displayInstructionsFilePath}, but file could not be read; continuing without injected instructions.`);
|
|
424
588
|
}
|
|
425
589
|
async function loadSiblingInstructionFile(siblingInput) {
|
|
426
590
|
const filePath = path.join(path.dirname(instructionsFilePath), siblingInput.fileName);
|
|
427
591
|
const resolvedPath = path.resolve(filePath);
|
|
592
|
+
const displayFilePath = displayInstructionPath(filePath, instructionsFilePath);
|
|
593
|
+
const displayFileDir = displayInstructionDir(filePath, instructionsFilePath);
|
|
428
594
|
if (loadedPaths.has(resolvedPath))
|
|
429
595
|
return { path: filePath, section: "" };
|
|
430
596
|
try {
|
|
431
597
|
const contents = await fs.readFile(filePath, "utf8");
|
|
432
598
|
loadedPaths.add(resolvedPath);
|
|
433
|
-
await input.onLog("stdout", `[rudder] Loaded ${siblingInput.logLabel}: ${
|
|
599
|
+
await input.onLog("stdout", `[rudder] Loaded ${siblingInput.logLabel}: ${displayFilePath}\n`);
|
|
434
600
|
return {
|
|
435
601
|
path: filePath,
|
|
436
602
|
section: `${contents}\n\n` +
|
|
437
|
-
`The above ${siblingInput.label} were loaded from ${
|
|
438
|
-
`Resolve any relative file references from ${
|
|
603
|
+
`The above ${siblingInput.label} were loaded from ${displayFilePath}. ` +
|
|
604
|
+
`Resolve any relative file references from ${displayFileDir}.`,
|
|
439
605
|
};
|
|
440
606
|
}
|
|
441
607
|
catch (err) {
|
|
@@ -451,26 +617,29 @@ export async function loadAgentInstructionsPrefix(input) {
|
|
|
451
617
|
label: "agent role and persona instructions",
|
|
452
618
|
logLabel: "agent soul instructions file",
|
|
453
619
|
});
|
|
454
|
-
if (soul.section)
|
|
455
|
-
commandNotes.push(`Loaded agent soul instructions from ${soul.path}`);
|
|
620
|
+
if (soul.section && soul.path) {
|
|
621
|
+
commandNotes.push(`Loaded agent soul instructions from ${displayInstructionPath(soul.path, instructionsFilePath)}`);
|
|
622
|
+
}
|
|
456
623
|
const tools = await loadSiblingInstructionFile({
|
|
457
624
|
fileName: "TOOLS.md",
|
|
458
625
|
label: "agent tool notes",
|
|
459
626
|
logLabel: "agent tool notes file",
|
|
460
627
|
});
|
|
461
|
-
if (tools.section)
|
|
462
|
-
commandNotes.push(`Loaded agent tool notes from ${tools.path}`);
|
|
628
|
+
if (tools.section && tools.path) {
|
|
629
|
+
commandNotes.push(`Loaded agent tool notes from ${displayInstructionPath(tools.path, instructionsFilePath)}`);
|
|
630
|
+
}
|
|
463
631
|
const memory = await loadSiblingInstructionFile({
|
|
464
632
|
fileName: "MEMORY.md",
|
|
465
633
|
label: "agent memory instructions",
|
|
466
634
|
logLabel: "agent memory instructions file",
|
|
467
635
|
});
|
|
468
|
-
if (memory.section)
|
|
469
|
-
commandNotes.push(`Loaded agent memory instructions from ${memory.path}`);
|
|
636
|
+
if (memory.section && memory.path) {
|
|
637
|
+
commandNotes.push(`Loaded agent memory instructions from ${displayInstructionPath(memory.path, instructionsFilePath)}`);
|
|
638
|
+
}
|
|
470
639
|
const memoryFilePath = memory.section ? memory.path : null;
|
|
471
640
|
const memorySection = memory.section;
|
|
472
641
|
if (entrySection)
|
|
473
|
-
commandNotes.splice(1, 0, `Loaded agent instructions from ${
|
|
642
|
+
commandNotes.splice(1, 0, `Loaded agent instructions from ${displayInstructionsFilePath}`);
|
|
474
643
|
const prefix = joinPromptSections([operatingContractSection, entrySection, soul.section, tools.section, memorySection]);
|
|
475
644
|
return {
|
|
476
645
|
prefix,
|
|
@@ -535,6 +704,15 @@ async function pathExists(candidate) {
|
|
|
535
704
|
return false;
|
|
536
705
|
}
|
|
537
706
|
}
|
|
707
|
+
async function fileExists(candidate) {
|
|
708
|
+
try {
|
|
709
|
+
await fs.access(candidate, fsConstants.F_OK);
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
538
716
|
async function resolveCommandPath(command, cwd, env) {
|
|
539
717
|
const hasPathSeparator = command.includes("/") || command.includes("\\");
|
|
540
718
|
if (hasPathSeparator) {
|
|
@@ -605,7 +783,7 @@ async function findAncestorWithFile(startDir, relativePath, maxDepth = 12) {
|
|
|
605
783
|
let current = path.resolve(startDir);
|
|
606
784
|
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
|
607
785
|
const candidate = path.join(current, relativePath);
|
|
608
|
-
if (await
|
|
786
|
+
if (await fileExists(candidate))
|
|
609
787
|
return candidate;
|
|
610
788
|
const parent = path.dirname(current);
|
|
611
789
|
if (parent === current)
|
|
@@ -631,14 +809,14 @@ async function resolveRudderCliShimTarget(moduleDir) {
|
|
|
631
809
|
const rootDir = path.dirname(path.dirname(path.dirname(repoRoot)));
|
|
632
810
|
const tsxEntry = path.join(rootDir, "cli", "node_modules", "tsx", "dist", "cli.mjs");
|
|
633
811
|
const cliSource = path.join(rootDir, "cli", "src", "index.ts");
|
|
634
|
-
if (await
|
|
812
|
+
if (await fileExists(tsxEntry)) {
|
|
635
813
|
return {
|
|
636
814
|
command: process.execPath,
|
|
637
815
|
args: [tsxEntry, cliSource],
|
|
638
816
|
};
|
|
639
817
|
}
|
|
640
818
|
const builtCliEntry = path.join(rootDir, "cli", "dist", "index.js");
|
|
641
|
-
if (await
|
|
819
|
+
if (await fileExists(builtCliEntry)) {
|
|
642
820
|
return {
|
|
643
821
|
command: process.execPath,
|
|
644
822
|
args: [builtCliEntry],
|
|
@@ -667,10 +845,6 @@ async function materializeRudderCliShim(target) {
|
|
|
667
845
|
}
|
|
668
846
|
export async function ensureRudderCliInPath(moduleDir, env) {
|
|
669
847
|
const normalized = ensurePathInEnv(env);
|
|
670
|
-
const cwd = process.cwd();
|
|
671
|
-
if (await resolveCommandPath("rudder", cwd, normalized)) {
|
|
672
|
-
return normalized;
|
|
673
|
-
}
|
|
674
848
|
const target = await resolveRudderCliShimTarget(moduleDir);
|
|
675
849
|
if (!target) {
|
|
676
850
|
return normalized;
|
|
@@ -945,6 +1119,230 @@ export function writeRudderSkillSyncPreference(config, desiredSkills) {
|
|
|
945
1119
|
next.rudderSkillSync = current;
|
|
946
1120
|
return next;
|
|
947
1121
|
}
|
|
1122
|
+
function nonEmptyEnvPath(value) {
|
|
1123
|
+
return typeof value === "string" && value.trim().length > 0 ? path.resolve(value.trim()) : null;
|
|
1124
|
+
}
|
|
1125
|
+
export function resolveLocalOperatorHome(sourceEnv = process.env) {
|
|
1126
|
+
return (nonEmptyEnvPath(sourceEnv.RUDDER_OPERATOR_HOME)
|
|
1127
|
+
?? nonEmptyEnvPath(process.env.RUDDER_OPERATOR_HOME)
|
|
1128
|
+
?? nonEmptyEnvPath(process.env.HOME)
|
|
1129
|
+
?? nonEmptyEnvPath(sourceEnv.HOME)
|
|
1130
|
+
?? path.resolve(os.homedir()));
|
|
1131
|
+
}
|
|
1132
|
+
export function applyLocalCliHomeEnv(targetEnv, sourceEnv = process.env) {
|
|
1133
|
+
const home = nonEmptyEnvPath(sourceEnv.HOME) ?? path.resolve(os.homedir());
|
|
1134
|
+
targetEnv.HOME = home;
|
|
1135
|
+
const userProfile = nonEmptyEnvPath(sourceEnv.USERPROFILE);
|
|
1136
|
+
if (userProfile) {
|
|
1137
|
+
targetEnv.USERPROFILE = userProfile;
|
|
1138
|
+
}
|
|
1139
|
+
else if (process.platform === "win32") {
|
|
1140
|
+
targetEnv.USERPROFILE = home;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async function localCliPathExists(candidate) {
|
|
1144
|
+
return fs.access(candidate).then(() => true).catch(() => false);
|
|
1145
|
+
}
|
|
1146
|
+
async function directoryIsEmpty(target) {
|
|
1147
|
+
const entries = await fs.readdir(target).catch(() => null);
|
|
1148
|
+
return Array.isArray(entries) && entries.length === 0;
|
|
1149
|
+
}
|
|
1150
|
+
async function ensureSymlinkToSource(target, source) {
|
|
1151
|
+
const existing = await fs.lstat(target).catch(() => null);
|
|
1152
|
+
if (!existing) {
|
|
1153
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
1154
|
+
await fs.symlink(source, target);
|
|
1155
|
+
return "created";
|
|
1156
|
+
}
|
|
1157
|
+
if (!existing.isSymbolicLink()) {
|
|
1158
|
+
if (existing.isDirectory() && await directoryIsEmpty(target)) {
|
|
1159
|
+
await fs.rmdir(target);
|
|
1160
|
+
await fs.symlink(source, target);
|
|
1161
|
+
return "repaired";
|
|
1162
|
+
}
|
|
1163
|
+
return "skipped";
|
|
1164
|
+
}
|
|
1165
|
+
const linkedPath = await fs.readlink(target).catch(() => null);
|
|
1166
|
+
if (!linkedPath)
|
|
1167
|
+
return "skipped";
|
|
1168
|
+
const resolvedLinkedPath = path.isAbsolute(linkedPath)
|
|
1169
|
+
? linkedPath
|
|
1170
|
+
: path.resolve(path.dirname(target), linkedPath);
|
|
1171
|
+
if (resolvedLinkedPath === source)
|
|
1172
|
+
return "skipped";
|
|
1173
|
+
await fs.unlink(target);
|
|
1174
|
+
await fs.symlink(source, target);
|
|
1175
|
+
return "repaired";
|
|
1176
|
+
}
|
|
1177
|
+
export async function syncLocalCliCredentialHomeEntries(input) {
|
|
1178
|
+
const sourceHome = nonEmptyEnvPath(input.sourceHome ?? undefined) ?? path.resolve(os.homedir());
|
|
1179
|
+
const targetHome = path.resolve(input.targetHome);
|
|
1180
|
+
const linked = [];
|
|
1181
|
+
const skipped = [];
|
|
1182
|
+
if (sourceHome === targetHome)
|
|
1183
|
+
return { linked, skipped };
|
|
1184
|
+
const entries = input.entries ?? DEFAULT_LOCAL_CLI_CREDENTIAL_HOME_ENTRIES;
|
|
1185
|
+
for (const relativeEntry of entries) {
|
|
1186
|
+
const source = path.join(sourceHome, relativeEntry);
|
|
1187
|
+
if (!(await localCliPathExists(source)))
|
|
1188
|
+
continue;
|
|
1189
|
+
const target = path.join(targetHome, relativeEntry);
|
|
1190
|
+
try {
|
|
1191
|
+
const result = await ensureSymlinkToSource(target, source);
|
|
1192
|
+
if (result === "skipped")
|
|
1193
|
+
skipped.push(relativeEntry);
|
|
1194
|
+
else
|
|
1195
|
+
linked.push(relativeEntry);
|
|
1196
|
+
}
|
|
1197
|
+
catch {
|
|
1198
|
+
skipped.push(relativeEntry);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (input.onLog && linked.length > 0) {
|
|
1202
|
+
await input.onLog("stdout", `[rudder] Shared ${linked.length} local CLI credential entr${linked.length === 1 ? "y" : "ies"} into managed HOME ${targetHome}: ${linked.join(", ")}\n`);
|
|
1203
|
+
}
|
|
1204
|
+
return { linked, skipped };
|
|
1205
|
+
}
|
|
1206
|
+
async function writeOperatorHomeShim(input) {
|
|
1207
|
+
await fs.mkdir(input.shimDir, { recursive: true });
|
|
1208
|
+
if (process.platform === "win32") {
|
|
1209
|
+
const shimPath = path.join(input.shimDir, `${input.command}.cmd`);
|
|
1210
|
+
const lines = [
|
|
1211
|
+
"@echo off",
|
|
1212
|
+
`set "HOME=${input.operatorHome}"`,
|
|
1213
|
+
`set "USERPROFILE=${input.operatorHome}"`,
|
|
1214
|
+
`${quoteForCmd(input.targetCommand)} %*`,
|
|
1215
|
+
"",
|
|
1216
|
+
];
|
|
1217
|
+
await fs.writeFile(shimPath, lines.join("\r\n"), "utf8");
|
|
1218
|
+
return shimPath;
|
|
1219
|
+
}
|
|
1220
|
+
const shimPath = path.join(input.shimDir, input.command);
|
|
1221
|
+
await fs.writeFile(shimPath, [
|
|
1222
|
+
"#!/bin/sh",
|
|
1223
|
+
`export HOME=${shellQuote(input.operatorHome)}`,
|
|
1224
|
+
`export USERPROFILE=${shellQuote(input.operatorHome)}`,
|
|
1225
|
+
`exec ${shellQuote(input.targetCommand)} "$@"`,
|
|
1226
|
+
"",
|
|
1227
|
+
].join("\n"), "utf8");
|
|
1228
|
+
await fs.chmod(shimPath, 0o755);
|
|
1229
|
+
return shimPath;
|
|
1230
|
+
}
|
|
1231
|
+
function normalizeShimCommand(input) {
|
|
1232
|
+
return typeof input === "string" ? { command: input } : input;
|
|
1233
|
+
}
|
|
1234
|
+
async function runCredentialShimAuthCheck(input) {
|
|
1235
|
+
const env = {
|
|
1236
|
+
...input.env,
|
|
1237
|
+
HOME: input.home,
|
|
1238
|
+
USERPROFILE: input.home,
|
|
1239
|
+
};
|
|
1240
|
+
return await new Promise((resolve) => {
|
|
1241
|
+
const child = spawn(input.targetCommand, [...input.args], {
|
|
1242
|
+
cwd: input.cwd,
|
|
1243
|
+
env,
|
|
1244
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
1245
|
+
});
|
|
1246
|
+
const timeout = setTimeout(() => {
|
|
1247
|
+
child.kill("SIGTERM");
|
|
1248
|
+
resolve(false);
|
|
1249
|
+
}, 1000);
|
|
1250
|
+
child.on("error", () => {
|
|
1251
|
+
clearTimeout(timeout);
|
|
1252
|
+
resolve(false);
|
|
1253
|
+
});
|
|
1254
|
+
child.on("close", (code) => {
|
|
1255
|
+
clearTimeout(timeout);
|
|
1256
|
+
resolve(code === 0);
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
async function credentialBridgeSatisfied(input) {
|
|
1261
|
+
for (const entry of input.entries) {
|
|
1262
|
+
const source = path.join(input.operatorHome, entry);
|
|
1263
|
+
const target = path.join(input.targetHome, entry);
|
|
1264
|
+
if (!(await localCliPathExists(source)) || !(await localCliPathExists(target)))
|
|
1265
|
+
continue;
|
|
1266
|
+
const [sourceRealpath, targetRealpath] = await Promise.all([
|
|
1267
|
+
fs.realpath(source).catch(() => null),
|
|
1268
|
+
fs.realpath(target).catch(() => null),
|
|
1269
|
+
]);
|
|
1270
|
+
if (sourceRealpath && targetRealpath && sourceRealpath === targetRealpath)
|
|
1271
|
+
return true;
|
|
1272
|
+
}
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
async function shouldPrepareOperatorHomeShim(input) {
|
|
1276
|
+
const authCheckArgs = input.command.authCheckArgs;
|
|
1277
|
+
if (!authCheckArgs || authCheckArgs.length === 0)
|
|
1278
|
+
return true;
|
|
1279
|
+
if (input.command.credentialEntries && input.command.credentialEntries.length > 0) {
|
|
1280
|
+
const hasOperatorCredentialEntry = await Promise.all(input.command.credentialEntries.map((entry) => localCliPathExists(path.join(input.operatorHome, entry))));
|
|
1281
|
+
if (!hasOperatorCredentialEntry.some(Boolean))
|
|
1282
|
+
return false;
|
|
1283
|
+
if (await credentialBridgeSatisfied({
|
|
1284
|
+
operatorHome: input.operatorHome,
|
|
1285
|
+
targetHome: input.targetHome,
|
|
1286
|
+
entries: input.command.credentialEntries,
|
|
1287
|
+
})) {
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
const managedHomeWorks = await runCredentialShimAuthCheck({
|
|
1292
|
+
targetCommand: input.targetCommand,
|
|
1293
|
+
args: authCheckArgs,
|
|
1294
|
+
cwd: input.cwd,
|
|
1295
|
+
env: input.env,
|
|
1296
|
+
home: input.targetHome,
|
|
1297
|
+
});
|
|
1298
|
+
if (managedHomeWorks)
|
|
1299
|
+
return false;
|
|
1300
|
+
return await runCredentialShimAuthCheck({
|
|
1301
|
+
targetCommand: input.targetCommand,
|
|
1302
|
+
args: authCheckArgs,
|
|
1303
|
+
cwd: input.cwd,
|
|
1304
|
+
env: input.env,
|
|
1305
|
+
home: input.operatorHome,
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
export async function ensureLocalCliCredentialShimsInPath(input) {
|
|
1309
|
+
const operatorHome = nonEmptyEnvPath(input.operatorHome ?? undefined);
|
|
1310
|
+
const targetHome = nonEmptyEnvPath(input.targetHome);
|
|
1311
|
+
if (!operatorHome || !targetHome || operatorHome === targetHome) {
|
|
1312
|
+
return ensurePathInEnv(input.env);
|
|
1313
|
+
}
|
|
1314
|
+
const normalized = ensurePathInEnv(input.env);
|
|
1315
|
+
const cwd = input.cwd ?? process.cwd();
|
|
1316
|
+
const commands = input.commands ?? DEFAULT_LOCAL_CLI_OPERATOR_HOME_SHIM_COMMANDS;
|
|
1317
|
+
const shimDir = path.join(targetHome, ".rudder", "local-cli-shims");
|
|
1318
|
+
const prepared = [];
|
|
1319
|
+
for (const rawCommand of commands) {
|
|
1320
|
+
const command = normalizeShimCommand(rawCommand);
|
|
1321
|
+
const targetCommand = await resolveCommandPath(command.command, cwd, normalized);
|
|
1322
|
+
if (!targetCommand)
|
|
1323
|
+
continue;
|
|
1324
|
+
if (path.dirname(targetCommand) === shimDir)
|
|
1325
|
+
continue;
|
|
1326
|
+
if (!(await shouldPrepareOperatorHomeShim({
|
|
1327
|
+
command,
|
|
1328
|
+
targetCommand,
|
|
1329
|
+
cwd,
|
|
1330
|
+
env: normalized,
|
|
1331
|
+
targetHome,
|
|
1332
|
+
operatorHome,
|
|
1333
|
+
}))) {
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
await writeOperatorHomeShim({ shimDir, command: command.command, targetCommand, operatorHome });
|
|
1337
|
+
prepared.push(command.command);
|
|
1338
|
+
}
|
|
1339
|
+
if (prepared.length === 0)
|
|
1340
|
+
return normalized;
|
|
1341
|
+
if (input.onLog) {
|
|
1342
|
+
await input.onLog("stdout", `[rudder] Prepared local CLI credential shim${prepared.length === 1 ? "" : "s"} for: ${prepared.join(", ")}\n`);
|
|
1343
|
+
}
|
|
1344
|
+
return prependPathEntry(normalized, shimDir);
|
|
1345
|
+
}
|
|
948
1346
|
export async function ensureRudderSkillSymlink(source, target, linkSkill = (linkSource, linkTarget) => fs.symlink(linkSource, linkTarget)) {
|
|
949
1347
|
const existing = await fs.lstat(target).catch(() => null);
|
|
950
1348
|
if (!existing) {
|
|
@@ -1035,6 +1433,17 @@ export async function runChildProcess(runId, command, args, opts) {
|
|
|
1035
1433
|
for (const key of CLAUDE_CODE_NESTING_VARS) {
|
|
1036
1434
|
delete rawMerged[key];
|
|
1037
1435
|
}
|
|
1436
|
+
const GIT_IDENTITY_ENV_VARS = [
|
|
1437
|
+
"GIT_AUTHOR_NAME",
|
|
1438
|
+
"GIT_AUTHOR_EMAIL",
|
|
1439
|
+
"GIT_COMMITTER_NAME",
|
|
1440
|
+
"GIT_COMMITTER_EMAIL",
|
|
1441
|
+
];
|
|
1442
|
+
for (const key of GIT_IDENTITY_ENV_VARS) {
|
|
1443
|
+
if (rawMerged[key] === "" && !Object.prototype.hasOwnProperty.call(opts.env, key)) {
|
|
1444
|
+
delete rawMerged[key];
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1038
1447
|
// When Rudder isolates HOME for child agents, don't let zsh keep using the
|
|
1039
1448
|
// host user's startup dir via an inherited ZDOTDIR. That mismatch makes
|
|
1040
1449
|
// child `zsh -lc` invocations source the host `.zshenv` with the agent HOME.
|