load affordances and append them to every claude session
The platform hosts the agent and never introduces itself. An agent gets a shell, a filesystem and a prompt; everything about the CLIENT on the other end has to be guessed. It writes , nothing renders, and nothing says why — because an <img> carries no bearer token and it is the user's DEVICE that fetches the URL. servers/affordances/load.ts reads capabilities/affordances/ and compiles one block; claude-manager passes it as systemPrompt.append with the claude_code preset, so the harness keeps its own prompt. Passed there rather than written to a file because it is the one place every session goes through, owner and member alike. A generated CLAUDE.md cannot do that — user-instance.ts deliberately refuses to write one for the owner, since it would pollute the personal ~/.claude/CLAUDE.md they maintain themselves. Read per session, not cached: capabilities/ is already read fresh each session, so a plugin installed an hour ago is live on the next message. Synchronous because createSession is not async, and making it so to read a few kilobytes would ripple through every caller. No filtering. Everything is loaded into every conversation, because the axis to filter on is not one dimension — for image rendering it is what the CLIENT renders, for gitea what TOOLING exists on the machine. Fine at two files, not at twenty; TODO.md carries the argument. Opencode gets nothing, and that is recorded as a gap rather than hidden: an affordance that holds on one harness is the drift this removes. Deliberate — opencode is paused pending v2 and the work would be thrown away. Nine tests, aimed at the failure that matters: a file that silently does not reach the agent looks exactly like one nobody wrote. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { query, type Query, type SpawnOptions, type SpawnedProcess } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { ChatEvent, PromptImage } from '../../api/chat/types';
|
||||
import { affordanceBlock } from '../../affordances/load';
|
||||
import type { ClaudeSpawnParams, ClaudeSpawnStreamingParams, ClaudeCodeResult, LiveClaudeSession } from '../protocol';
|
||||
import { setClaudeSession, clearClaudeSession, getClaudeSession } from './state';
|
||||
import { countsAsOutput, diagnoseExit, stderrReason } from './exit-diagnosis';
|
||||
@@ -520,6 +521,10 @@ function createSession(params: ClaudeSpawnStreamingParams, onEvent: (event: Chat
|
||||
`as=${params.member?.osUser ?? 'owner'} model=${params.model ?? 'default'}`,
|
||||
);
|
||||
|
||||
// Read here rather than at module load: a plugin installed an hour ago should be live on the next
|
||||
// message, and this costs a few kilobytes once per session.
|
||||
const affordances = affordanceBlock();
|
||||
|
||||
const q = query({
|
||||
prompt: input.gen as AsyncIterable<SdkUserMessage>,
|
||||
options: {
|
||||
@@ -593,6 +598,27 @@ function createSession(params: ClaudeSpawnStreamingParams, onEvent: (event: Chat
|
||||
spawnClaudeCodeProcess: watchChild(spawnClaudeAsOwner, session),
|
||||
}),
|
||||
settingSources: ['user', 'project', 'local'],
|
||||
/**
|
||||
* What the platform tells the agent about the surface it is speaking through.
|
||||
*
|
||||
* `preset` keeps Claude Code's own system prompt and APPENDS to it — replacing it would throw away
|
||||
* everything the harness knows about its own tools.
|
||||
*
|
||||
* Passed here rather than written to a file because this is the one place every session goes
|
||||
* through, owner and member alike. A generated CLAUDE.md cannot do that: `user-instance.ts`
|
||||
* deliberately refuses to write one for the owner, since it would pollute the personal
|
||||
* `~/.claude/CLAUDE.md` they maintain themselves.
|
||||
*
|
||||
* Read per session, not cached: `capabilities/` is already read fresh each agent session, so a
|
||||
* plugin installed an hour ago is live on the next message without a restart.
|
||||
*
|
||||
* OPENCODE DOES NOT GET THIS, and that is a real gap rather than an oversight — an affordance that
|
||||
* holds on one harness is the drift this exists to remove. Deliberate as of 2026-08-19: opencode
|
||||
* development is paused pending its v2 migration, so the work would be thrown away. See TODO.md.
|
||||
*/
|
||||
...(affordances
|
||||
? { systemPrompt: { type: 'preset' as const, preset: 'claude_code' as const, append: affordances } }
|
||||
: {}),
|
||||
env: cleanEnv as Record<string, string>,
|
||||
stderr: (d: string) => {
|
||||
if (!d.trim()) return;
|
||||
|
||||
Reference in New Issue
Block a user