Compare commits

...

7 Commits

Author SHA1 Message Date
gavrielc ca19b744b3 fix: exchange archives stamp local time
Archived: lines and archive filename dates in agent-readable conversation
markdown render in the install timezone instead of raw UTC ISO — evening
sessions west of UTC no longer archive under tomorrow's date.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2YZQDTw9TQrH3m8NBtVAW
2026-07-10 21:03:49 +03:00
gavrielc d2dd91b89f Merge pull request #2908 from amit-shafnir/providers-agnostic-surfaces
feat(codex): persona prepend + git-independent skill discovery for template agents
2026-07-05 15:14:30 +03:00
Amit Shafnir 40b6a57527 fix(codex): expose group skills at $HOME/.agents/skills so codex discovers them
Codex only scans the CWD-level `.agents/skills` when the CWD is inside a git
repo. The agent workspace (/workspace/agent) is not a git repo, so template and
shared skills materialized into /workspace/agent/.agents/skills were never
discovered. Codex DOES scan the user-level `$HOME/.agents/skills`
unconditionally (verified against codex-cli 0.141 at a non-git CWD), so mount
the group's `.agents` dir at /home/node/.agents as well. Skill materialization
stays in the provider-neutral group-skills.ts seam; only the exposure is here.

Also correct the AGENTS.md self-authored-skills pointer: `/workspace/agent/skills`
is persistent but not codex-scanned, and the "linked into $CODEX_HOME/skills at
boot" claim was never implemented. Point authored skills at `~/.codex/skills`
(writable via the .codex-shared mount and scanned by codex).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:17:53 +03:00
Amit Shafnir ed2e3b4c85 feat(codex): persona prepend + provider-agnostic skills mirror
codex-agents-md.ts inlines the group persona (readGroupPersona from the main seam)
as the first, never-evicted section of AGENTS.md. codex.ts mirrors template skills
into .agents/skills via the shared materializeTemplateSkills helper. Tests added
for both. Local-only commit to enable NANOCLAW_PROVIDERS_REF testing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:57:26 +03:00
gavrielc c57076649a Merge pull request #2836 from nanocoai/chore/bump-chat-sdk-4.29.0-providers
chore(deps): bump chat SDK to 4.29.0 (providers)
2026-06-23 15:28:18 +03:00
Gabi Simons d5dc9ef85c chore(deps): bump chat SDK to 4.29.0 (providers branch)
Companion to the core `chat` 4.29.0 bump on main and the @chat-adapter bump on
the channels branch. Pins `chat` and the channel-adapter install references on
the providers branch to the matched 4.29.0 release, so a provider-branch install
that also adds a channel stays on the same Chat SDK generation as main's bridge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:37:23 +03:00
omri-maya 842f6ba565 Merge pull request #2772 from nanocoai/fix/codex-per-thread-archive
fix(codex): per-thread conversation archive (CDX-004)
2026-06-15 17:00:50 +03:00
28 changed files with 180 additions and 33 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 {
+16
View File
@@ -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
View File
@@ -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"
},
+14 -5
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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`.
+1 -1
View File
@@ -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`.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+70 -3
View File
@@ -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 });
}
});
});
+7 -1
View File
@@ -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'));
+31 -1
View File
@@ -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);
});
});
+14
View File
@@ -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 };