mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 23:49:41 +02:00
fix(agent-skill): write the skill atomically and stop swallowing refusals
Two ways the injection could go wrong quietly.
`installAgentSkillInto()` wrote each file with a bare `writeFile`, no lock and no
temp+rename, while every sibling mutator in hooks-config.ts goes through
`withSettingsLock`. Two Claude sessions created concurrently in one repo both wrote the
same ~16KB SKILL.md, and any reader loading it mid-write could observe a truncated
file. Writes now go through a temp+rename helper under the same lock the neighbours
use, so a reader sees either the old file or the new one.
Both server call sites discarded the outcome with `.catch(() => {})`, so the two
refusal results were invisible: `foreign` (a user-authored skills/codeman is present,
so we declined to touch it) and `symlink` (the skill dir or its parent is a symlink, so
we declined to write through it). Turning `agentSkillEnabled` on, seeing nothing appear
and having no way to find out why was the reportable-as-a-bug outcome. Refusals are now
logged with the path and what to do about it. The boring outcomes stay silent, since
they happen on every session create. Injection remains best-effort: a refusal or a
thrown error still cannot fail session creation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -556,6 +556,37 @@ function abortOnClientHangUp(reply: FastifyReply): AbortController {
|
||||
return controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the agent skill into a case on create, surfacing only the REFUSALS.
|
||||
*
|
||||
* `applyAgentSkill` declines two shapes rather than writing through them ('foreign':
|
||||
* an unmarked skills/codeman the user authored; 'symlink': the skill dir or its
|
||||
* parent is a link). Both were silent: the user flips `agentSkillEnabled` on, nothing
|
||||
* appears in the case, and there is nowhere to look for why. The ordinary outcomes
|
||||
* ('installed'/'refreshed'/'unchanged') stay unlogged since they would print on every
|
||||
* single session create.
|
||||
*
|
||||
* Injection is best-effort and stays that way: neither a refusal nor a thrown error
|
||||
* may fail the create.
|
||||
*/
|
||||
async function injectAgentSkill(casePath: string): Promise<void> {
|
||||
const skillDir = join(casePath, '.claude', 'skills', 'codeman');
|
||||
try {
|
||||
const result = await applyAgentSkill(casePath, true);
|
||||
if (result === 'foreign') {
|
||||
console.warn(
|
||||
`[agent-skill] not injected: ${skillDir} exists but is not Codeman-managed (no marker), refusing to touch it. Remove that copy if you want the packaged skill there.`
|
||||
);
|
||||
} else if (result === 'symlink') {
|
||||
console.warn(
|
||||
`[agent-skill] not injected: ${skillDir} (or its parent) is a symlink, refusing to write through it. Replace it with a real directory to let Codeman install the skill.`
|
||||
);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.warn(`[agent-skill] injection failed for ${skillDir}: ${getErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function registerSessionRoutes(
|
||||
app: FastifyInstance,
|
||||
ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort
|
||||
@@ -705,7 +736,7 @@ export function registerSessionRoutes(
|
||||
// skill from under other live sessions in the repo. Marker-guarded, so a
|
||||
// user's own skills/codeman is never touched.
|
||||
if (await ctx.getAgentSkillEnabled()) {
|
||||
await applyAgentSkill(workingDir, true).catch(() => {});
|
||||
await injectAgentSkill(workingDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2780,7 +2811,7 @@ export function registerSessionRoutes(
|
||||
// casePath lives on another host. Docker cases qualify: hostWorkspacePath is a
|
||||
// real host dir and the skill crosses the bind mount like the rest of `.claude/`.
|
||||
if (!remote && mode === 'claude' && (await ctx.getAgentSkillEnabled())) {
|
||||
await applyAgentSkill(resolvedCasePath, true).catch(() => {});
|
||||
await injectAgentSkill(resolvedCasePath);
|
||||
}
|
||||
|
||||
// Docker cases: the workspace is a REAL host dir bind-mounted into the container.
|
||||
|
||||
Reference in New Issue
Block a user