mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-12 19:00:58 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca19b744b3 | |||
| d2dd91b89f | |||
| 40b6a57527 | |||
| ed2e3b4c85 | |||
| c57076649a | |||
| d5dc9ef85c | |||
| 842f6ba565 |
@@ -44,7 +44,7 @@ import './discord.js';
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/discord@4.26.0
|
||||
pnpm install @chat-adapter/discord@4.29.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
@@ -44,7 +44,7 @@ import './gchat.js';
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/gchat@4.26.0
|
||||
pnpm install @chat-adapter/gchat@4.29.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
@@ -48,7 +48,7 @@ import './github.js';
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/github@4.26.0
|
||||
pnpm install @chat-adapter/github@4.29.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
@@ -87,7 +87,7 @@ Linear OAuth apps can't be @-mentioned, so the bridge's `onNewMention` handler n
|
||||
### 5. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/linear@4.26.0
|
||||
pnpm install @chat-adapter/linear@4.29.0
|
||||
```
|
||||
|
||||
### 6. Build
|
||||
|
||||
@@ -44,7 +44,7 @@ import './slack.js';
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/slack@4.26.0
|
||||
pnpm install @chat-adapter/slack@4.29.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
@@ -44,7 +44,7 @@ import './teams.js';
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/teams@4.26.0
|
||||
pnpm install @chat-adapter/teams@4.29.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
@@ -58,7 +58,7 @@ In `setup/index.ts`, add this entry to the `STEPS` map (right after the `registe
|
||||
### 5. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/telegram@4.26.0
|
||||
pnpm install @chat-adapter/telegram@4.29.0
|
||||
```
|
||||
|
||||
### 6. Build
|
||||
|
||||
@@ -44,7 +44,7 @@ import './whatsapp-cloud.js';
|
||||
### 4. Install the adapter package (pinned)
|
||||
|
||||
```bash
|
||||
pnpm install @chat-adapter/whatsapp@4.26.0
|
||||
pnpm install @chat-adapter/whatsapp@4.29.0
|
||||
```
|
||||
|
||||
### 5. Build
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { TIMEZONE, formatLocalStamp } from '../timezone.js';
|
||||
|
||||
/**
|
||||
* Per-thread conversation archive for providers with no on-disk transcript —
|
||||
* payload code, shipped with the provider that needs it. The provider's
|
||||
@@ -62,7 +64,7 @@ export function archiveProviderExchange(options: ProviderExchangeArchiveOptions)
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
`Archived: ${timestamp.toISOString()} · Status: ${options.status}`,
|
||||
`Archived: ${formatLocalStamp(timestamp, TIMEZONE)} · Status: ${options.status}`,
|
||||
'',
|
||||
`**User**: ${truncate(options.prompt)}`,
|
||||
'',
|
||||
@@ -86,7 +88,10 @@ function threadArchiveFilename(
|
||||
const dated = /^\d{4}-\d{2}-\d{2}-/;
|
||||
const existing = fs.readdirSync(dir).find((f) => dated.test(f) && f.replace(dated, '') === suffix);
|
||||
if (existing) return existing;
|
||||
return `${timestamp.toISOString().split('T')[0]}-${suffix}`;
|
||||
// Local calendar day — the agent navigates conversations/ by these
|
||||
// date-sortable names, and evening sessions west of UTC would otherwise
|
||||
// land under tomorrow's date.
|
||||
return `${formatLocalStamp(timestamp, TIMEZONE).slice(0, 10)}-${suffix}`;
|
||||
}
|
||||
|
||||
function sanitizeSlug(value: string): string {
|
||||
|
||||
@@ -48,6 +48,22 @@ export function formatLocalTime(utcIso: string, timezone: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact sortable local stamp for log lines: "YYYY-MM-DD HH:mm" in `timezone`.
|
||||
* (sv-SE is the one locale whose default rendering is this exact shape.)
|
||||
*/
|
||||
export function formatLocalStamp(date: Date, timezone: string): string {
|
||||
return date.toLocaleString('sv-SE', {
|
||||
timeZone: resolveTimezone(timezone),
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveContainerTimezone(): string {
|
||||
const candidates = [process.env.TZ, Intl.DateTimeFormat().resolvedOptions().timeZone];
|
||||
for (const tz of candidates) {
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
"@clack/prompts": "^1.2.0",
|
||||
"@onecli-sh/sdk": "^0.3.1",
|
||||
"better-sqlite3": "11.10.0",
|
||||
"chat": "^4.24.0",
|
||||
"chat": "4.29.0",
|
||||
"cron-parser": "5.5.0",
|
||||
"kleur": "^4.1.5"
|
||||
},
|
||||
|
||||
Generated
+14
-5
@@ -21,8 +21,8 @@ importers:
|
||||
specifier: 11.10.0
|
||||
version: 11.10.0
|
||||
chat:
|
||||
specifier: ^4.24.0
|
||||
version: 4.26.0
|
||||
specifier: 4.29.0
|
||||
version: 4.29.0
|
||||
cron-parser:
|
||||
specifier: 5.5.0
|
||||
version: 5.5.0
|
||||
@@ -609,8 +609,17 @@ packages:
|
||||
character-entities@2.0.2:
|
||||
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
||||
|
||||
chat@4.26.0:
|
||||
resolution: {integrity: sha512-QToDnIEGpyb8yQA6YLMHOSRK30YVk4RtsyFyuWFYyB2c4jQlyIrSWtwVK7qyvmvqzQp9uDwCdJRAhS8GtCHAGQ==}
|
||||
chat@4.29.0:
|
||||
resolution: {integrity: sha512-KdPfzaie5ivYytyRICTERg5xT+LeCbYefokvNAqTHe92eqkFaoTMXXkSitikxJVWhZIb2YoXF1b9UZHyzSzKzw==}
|
||||
engines: {node: '>=20'}
|
||||
peerDependencies:
|
||||
ai: ^6.0.182
|
||||
zod: ^3.0.0 || ^4.0.0
|
||||
peerDependenciesMeta:
|
||||
ai:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
@@ -1963,7 +1972,7 @@ snapshots:
|
||||
|
||||
character-entities@2.0.2: {}
|
||||
|
||||
chat@4.26.0:
|
||||
chat@4.29.0:
|
||||
dependencies:
|
||||
'@workflow/serde': 4.1.0-beta.2
|
||||
mdast-util-to-string: 4.0.0
|
||||
|
||||
@@ -15,7 +15,7 @@ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Keep in sync with .claude/skills/add-discord/SKILL.md.
|
||||
ADAPTER_VERSION="@chat-adapter/discord@4.26.0"
|
||||
ADAPTER_VERSION="@chat-adapter/discord@4.29.0"
|
||||
|
||||
# Resolve which remote carries the channels branch — handles forks where
|
||||
# upstream lives on a different remote than `origin`.
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Keep in sync with .claude/skills/add-slack/SKILL.md.
|
||||
ADAPTER_VERSION="@chat-adapter/slack@4.26.0"
|
||||
ADAPTER_VERSION="@chat-adapter/slack@4.29.0"
|
||||
|
||||
# Resolve which remote carries the channels branch — handles forks where
|
||||
# upstream lives on a different remote than `origin`.
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Keep in sync with .claude/skills/add-teams/SKILL.md.
|
||||
ADAPTER_VERSION="@chat-adapter/teams@4.26.0"
|
||||
ADAPTER_VERSION="@chat-adapter/teams@4.29.0"
|
||||
|
||||
# Resolve which remote carries the channels branch — handles forks where
|
||||
# upstream lives on a different remote than `origin`.
|
||||
|
||||
@@ -15,7 +15,7 @@ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Keep in sync with .claude/skills/add-telegram/SKILL.md.
|
||||
ADAPTER_VERSION="@chat-adapter/telegram@4.26.0"
|
||||
ADAPTER_VERSION="@chat-adapter/telegram@4.29.0"
|
||||
|
||||
# Resolve which remote carries the channels branch — handles forks where
|
||||
# upstream lives on a different remote than `origin`.
|
||||
|
||||
@@ -37,7 +37,7 @@ if ! grep -q "import './discord.js';" src/channels/index.ts; then
|
||||
fi
|
||||
|
||||
echo "STEP: pnpm-install"
|
||||
pnpm install @chat-adapter/discord@4.26.0
|
||||
pnpm install @chat-adapter/discord@4.29.0
|
||||
|
||||
echo "STEP: pnpm-build"
|
||||
pnpm run build
|
||||
|
||||
@@ -37,7 +37,7 @@ if ! grep -q "import './gchat.js';" src/channels/index.ts; then
|
||||
fi
|
||||
|
||||
echo "STEP: pnpm-install"
|
||||
pnpm install @chat-adapter/gchat@4.26.0
|
||||
pnpm install @chat-adapter/gchat@4.29.0
|
||||
|
||||
echo "STEP: pnpm-build"
|
||||
pnpm run build
|
||||
|
||||
@@ -37,7 +37,7 @@ if ! grep -q "import './github.js';" src/channels/index.ts; then
|
||||
fi
|
||||
|
||||
echo "STEP: pnpm-install"
|
||||
pnpm install @chat-adapter/github@4.26.0
|
||||
pnpm install @chat-adapter/github@4.29.0
|
||||
|
||||
echo "STEP: pnpm-build"
|
||||
pnpm run build
|
||||
|
||||
@@ -86,7 +86,7 @@ if ! grep -q 'if (config.catchAll) {' src/channels/chat-sdk-bridge.ts; then
|
||||
fi
|
||||
|
||||
echo "STEP: pnpm-install"
|
||||
pnpm install @chat-adapter/linear@4.26.0
|
||||
pnpm install @chat-adapter/linear@4.29.0
|
||||
|
||||
echo "STEP: pnpm-build"
|
||||
pnpm run build
|
||||
|
||||
@@ -37,7 +37,7 @@ if ! grep -q "import './slack.js';" src/channels/index.ts; then
|
||||
fi
|
||||
|
||||
echo "STEP: pnpm-install"
|
||||
pnpm install @chat-adapter/slack@4.26.0
|
||||
pnpm install @chat-adapter/slack@4.29.0
|
||||
|
||||
echo "STEP: pnpm-build"
|
||||
pnpm run build
|
||||
|
||||
@@ -37,7 +37,7 @@ if ! grep -q "import './teams.js';" src/channels/index.ts; then
|
||||
fi
|
||||
|
||||
echo "STEP: pnpm-install"
|
||||
pnpm install @chat-adapter/teams@4.26.0
|
||||
pnpm install @chat-adapter/teams@4.29.0
|
||||
|
||||
echo "STEP: pnpm-build"
|
||||
pnpm run build
|
||||
|
||||
@@ -63,7 +63,7 @@ if ! grep -q "'pair-telegram':" setup/index.ts; then
|
||||
fi
|
||||
|
||||
echo "STEP: pnpm-install"
|
||||
pnpm install @chat-adapter/telegram@4.26.0
|
||||
pnpm install @chat-adapter/telegram@4.29.0
|
||||
|
||||
echo "STEP: pnpm-build"
|
||||
pnpm run build
|
||||
|
||||
@@ -37,7 +37,7 @@ if ! grep -q "import './whatsapp-cloud.js';" src/channels/index.ts; then
|
||||
fi
|
||||
|
||||
echo "STEP: pnpm-install"
|
||||
pnpm install @chat-adapter/whatsapp@4.26.0
|
||||
pnpm install @chat-adapter/whatsapp@4.29.0
|
||||
|
||||
echo "STEP: pnpm-build"
|
||||
pnpm run build
|
||||
|
||||
@@ -19,6 +19,7 @@ vi.mock('../config.js', async (importOriginal) => ({
|
||||
import { composeGroupAgentsMd, CODEX_PROJECT_DOC_MAX_BYTES } from './codex-agents-md.js';
|
||||
import { closeDb, createAgentGroup, initTestDb, runMigrations } from '../db/index.js';
|
||||
import { ensureContainerConfig, updateContainerConfigJson } from '../db/container-configs.js';
|
||||
import { PERSONA_PREPEND_FILE } from '../group-persona.js';
|
||||
import type { AgentGroup } from '../types.js';
|
||||
|
||||
const TEST_ROOT = '/tmp/nanoclaw-agents-md-test';
|
||||
@@ -55,9 +56,10 @@ describe('composeGroupAgentsMd cap handling', () => {
|
||||
composeGroupAgentsMd(g, groupDir);
|
||||
const doc = fs.readFileSync(path.join(groupDir, 'AGENTS.md'), 'utf-8');
|
||||
expect(doc).not.toContain('Omitted for size');
|
||||
// Agent-authored skills must be told their persistent home — without
|
||||
// this, authored skills land on ephemeral container paths and vanish.
|
||||
expect(doc).toContain('/workspace/agent/skills');
|
||||
// Agent-authored skills must be told a home that is BOTH persistent and
|
||||
// codex-discovered (~/.codex/skills). /workspace/agent/skills is not
|
||||
// scanned by codex, so authored skills there never trigger.
|
||||
expect(doc).toContain('~/.codex/skills');
|
||||
expect(Buffer.byteLength(doc, 'utf-8')).toBeLessThanOrEqual(CODEX_PROJECT_DOC_MAX_BYTES);
|
||||
} finally {
|
||||
fs.rmSync(groupDir, { recursive: true, force: true });
|
||||
@@ -110,3 +112,68 @@ describe('composeGroupAgentsMd cap handling', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('composeGroupAgentsMd persona', () => {
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(TEST_ROOT)) fs.rmSync(TEST_ROOT, { recursive: true });
|
||||
fs.mkdirSync(path.join(TEST_ROOT, 'data'), { recursive: true });
|
||||
runMigrations(initTestDb());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
closeDb();
|
||||
if (fs.existsSync(TEST_ROOT)) fs.rmSync(TEST_ROOT, { recursive: true });
|
||||
});
|
||||
|
||||
it('inlines the persona as the first section, before the runtime contract', () => {
|
||||
const g = group('persona');
|
||||
createAgentGroup(g);
|
||||
ensureContainerConfig(g.id);
|
||||
const groupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-md-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(groupDir, PERSONA_PREPEND_FILE), 'You are an SDR agent.\n');
|
||||
composeGroupAgentsMd(g, groupDir);
|
||||
const doc = fs.readFileSync(path.join(groupDir, 'AGENTS.md'), 'utf-8');
|
||||
expect(doc).toContain('You are an SDR agent.');
|
||||
// First markdown heading (the HEADER is an HTML comment, not a `# ` heading).
|
||||
const firstHeading = doc.split('\n').find((line) => line.startsWith('# '));
|
||||
expect(firstHeading).toBe('# Persona');
|
||||
} finally {
|
||||
fs.rmSync(groupDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('never evicts the persona even when the doc exceeds the cap', () => {
|
||||
const g = group('persona-big');
|
||||
createAgentGroup(g);
|
||||
ensureContainerConfig(g.id);
|
||||
updateContainerConfigJson(g.id, 'mcp_servers', {
|
||||
bloat: { command: 'x', instructions: 'B'.repeat(CODEX_PROJECT_DOC_MAX_BYTES + 1024) },
|
||||
});
|
||||
const groupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-md-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(groupDir, PERSONA_PREPEND_FILE), 'PERSONA_MARKER body');
|
||||
composeGroupAgentsMd(g, groupDir);
|
||||
const doc = fs.readFileSync(path.join(groupDir, 'AGENTS.md'), 'utf-8');
|
||||
expect(Buffer.byteLength(doc, 'utf-8')).toBeLessThanOrEqual(CODEX_PROJECT_DOC_MAX_BYTES);
|
||||
expect(doc).toContain('PERSONA_MARKER'); // survived eviction
|
||||
expect(doc).toContain('Omitted for size');
|
||||
} finally {
|
||||
fs.rmSync(groupDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('omits the persona section when no prepend file is present', () => {
|
||||
const g = group('no-persona');
|
||||
createAgentGroup(g);
|
||||
ensureContainerConfig(g.id);
|
||||
const groupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-md-'));
|
||||
try {
|
||||
composeGroupAgentsMd(g, groupDir);
|
||||
const doc = fs.readFileSync(path.join(groupDir, 'AGENTS.md'), 'utf-8');
|
||||
expect(doc).not.toContain('# Persona');
|
||||
} finally {
|
||||
fs.rmSync(groupDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import path from 'path';
|
||||
|
||||
import type { McpServerConfig } from '../container-config.js';
|
||||
import { getContainerConfig } from '../db/container-configs.js';
|
||||
import { readGroupPersona } from '../group-persona.js';
|
||||
import { log } from '../log.js';
|
||||
import type { AgentGroup } from '../types.js';
|
||||
|
||||
@@ -55,7 +56,7 @@ const NATIVE_RUNTIME_SKILLS_POINTER = [
|
||||
'Selected NanoClaw runtime skills are available as Codex-native skills at `/workspace/agent/.agents/skills`.',
|
||||
'Each skill directory contains a `SKILL.md` with its trigger description plus any supporting files, and points to the read-only shared skill source under `/app/skills`.',
|
||||
'Use skill discovery to load these skills only when their descriptions match the task. Full skill instructions live in the skill directories, not in `AGENTS.md`.',
|
||||
'Skills YOU author or install yourself go in `/workspace/agent/skills/<name>/SKILL.md` — persistent, provider-neutral (they load under any agent provider this group runs on), and yours to write and update over time. They are linked into `$CODEX_HOME/skills` automatically at boot. Never write skills anywhere else: paths outside your workspace and `$CODEX_HOME` are ephemeral.',
|
||||
'Skills YOU author or install yourself go in `~/.codex/skills/<name>/SKILL.md` — persistent across sessions and discovered by Codex automatically. Never write skills elsewhere: paths outside `~/.codex` and `~/.agents` are ephemeral or not discovered.',
|
||||
].join('\n\n');
|
||||
|
||||
interface AgentsMdSection {
|
||||
@@ -80,6 +81,11 @@ export function composeGroupAgentsMd(group: AgentGroup, groupDir: string): void
|
||||
if (body) sections.push({ name, content: `# ${name}\n\n${body}` });
|
||||
};
|
||||
|
||||
// Template persona first — the top of the system prompt. 'Persona' is not a
|
||||
// droppable prefix (see fitAgentsMdToCap.isDroppable), so it is never evicted.
|
||||
const persona = readGroupPersona(groupDir);
|
||||
if (persona) pushSection('Persona', persona);
|
||||
|
||||
const sharedBase = path.join(process.cwd(), 'container', 'AGENTS.md');
|
||||
if (fs.existsSync(sharedBase)) {
|
||||
pushSection('NanoClaw Runtime Contract', fs.readFileSync(sharedBase, 'utf-8'));
|
||||
|
||||
@@ -88,11 +88,41 @@ describe('codex host contribution against real core', () => {
|
||||
|
||||
// The full mount set: codex surfaces in, default claude surfaces out.
|
||||
const session = { id: 'session-1', agent_group_id: ag.id } as Session;
|
||||
const config: ContainerConfig = { mcpServers: {}, packages: { apt: [], npm: [] }, additionalMounts: [], skills: [] };
|
||||
const config: ContainerConfig = {
|
||||
mcpServers: {},
|
||||
packages: { apt: [], npm: [] },
|
||||
additionalMounts: [],
|
||||
skills: [],
|
||||
};
|
||||
const mounts = buildMounts(ag, session, config, 'codex', contribution);
|
||||
const containerPaths = mounts.map((m) => m.containerPath);
|
||||
expect(containerPaths).toContain('/home/node/.codex');
|
||||
expect(containerPaths.some((p) => p.endsWith('AGENTS.md'))).toBe(true);
|
||||
expect(containerPaths).not.toContain('/home/node/.claude');
|
||||
});
|
||||
|
||||
it('mirrors per-group template skills from the Claude plane into .agents/skills', () => {
|
||||
const ag = group('ag-codex-skills', 'codex-skills-group');
|
||||
createAgentGroup(ag);
|
||||
ensureContainerConfig(ag.id);
|
||||
// A template stamps its skills as real dirs on the Claude plane; codex reads
|
||||
// .agents/skills (RO-mounted), so the contribution must mirror them there.
|
||||
const templateSkill = path.join(DATA_DIR, 'v2-sessions', ag.id, '.claude-shared', 'skills', 'widget');
|
||||
fs.mkdirSync(templateSkill, { recursive: true });
|
||||
fs.writeFileSync(path.join(templateSkill, 'SKILL.md'), '---\nname: widget\n---\n');
|
||||
|
||||
const contributionFn = getProviderContainerConfig('codex');
|
||||
contributionFn!({
|
||||
sessionDir: path.join(DATA_DIR, 'v2-sessions', ag.id, 'session-1'),
|
||||
agentGroupId: ag.id,
|
||||
groupDir: path.join(GROUPS_DIR, ag.folder),
|
||||
selectedSkills: [],
|
||||
hostEnv: process.env,
|
||||
});
|
||||
|
||||
const mirrored = path.join(GROUPS_DIR, ag.folder, '.agents', 'skills', 'widget');
|
||||
expect(fs.existsSync(path.join(mirrored, 'SKILL.md'))).toBe(true);
|
||||
// A real dir, not a symlink — so it survives syncCodexSkillLinks' symlink-only prune.
|
||||
expect(fs.lstatSync(mirrored).isSymbolicLink()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ import path from 'path';
|
||||
|
||||
import { DATA_DIR } from '../config.js';
|
||||
import { getAgentGroup } from '../db/agent-groups.js';
|
||||
import { materializeTemplateSkills } from '../group-skills.js';
|
||||
import { composeGroupAgentsMd } from './codex-agents-md.js';
|
||||
import { registerProviderContainerConfig } from './provider-container-registry.js';
|
||||
|
||||
@@ -52,6 +53,11 @@ registerProviderContainerConfig(
|
||||
const group = getAgentGroup(ctx.agentGroupId);
|
||||
if (group) composeGroupAgentsMd(group, ctx.groupDir);
|
||||
syncCodexSkillLinks(ctx.groupDir, ctx.selectedSkills);
|
||||
// Template skills live on the Claude plane (.claude-shared/skills); codex
|
||||
// reads .agents/skills (RO-mounted), so mirror them here, host-side, via the
|
||||
// shared provider-agnostic helper. Real dirs survive the symlink-only prune
|
||||
// above and coexist with the shared-skill symlinks it creates.
|
||||
materializeTemplateSkills(ctx.agentGroupId, path.join(ctx.groupDir, '.agents', 'skills'));
|
||||
|
||||
// No credential env here — OneCLI's container-config drives auth end to
|
||||
// end: the gateway serves a sentinel auth.json stub into ~/.codex for
|
||||
@@ -70,6 +76,14 @@ registerProviderContainerConfig(
|
||||
const agentsDir = path.join(ctx.groupDir, '.agents');
|
||||
if (fs.existsSync(agentsDir)) {
|
||||
mounts.push({ hostPath: agentsDir, containerPath: '/workspace/agent/.agents', readonly: true });
|
||||
// Codex only scans the CWD-level `.agents/skills` when the CWD is inside a
|
||||
// git repo; the agent workspace (/workspace/agent) is not one, so skills
|
||||
// materialized there are invisible. Codex DOES scan the user-level
|
||||
// `$HOME/.agents/skills` unconditionally, so mount the same dir at $HOME
|
||||
// to make the group's template + shared skills discoverable. Verified
|
||||
// against codex-cli 0.141: user-level `.agents/skills` resolves at a
|
||||
// non-git CWD. Skill materialization stays provider-neutral (group-skills.ts).
|
||||
mounts.push({ hostPath: agentsDir, containerPath: '/home/node/.agents', readonly: true });
|
||||
}
|
||||
|
||||
return { mounts };
|
||||
|
||||
Reference in New Issue
Block a user