mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
feat: Read My Mind phase 1, per-case intent profiles (capture + API + skill)
Per-case profiles of user intent (docs/readmymind-plan.md): user-stated goals plus the user's recently submitted prompts, captured from the Claude session transcript behind the new synced readMyMindEnabled setting (default OFF). - intent-store.ts: keyed by owner + realpath(workingDir), FIFO/size caps, consecutive-dupe collapse, atomic 0600 writes to ~/.codeman/intents.json - transcript-watcher.ts: new transcript:user_prompt event for typed user turns (tool_result-only entries stay silent); capture wiring in server.ts is claude-only and gated on the setting per event - readmymind-routes.ts: GET/PUT/DELETE /api/sessions/:id/intent, ownership via findSessionOrFail, strict Zod schema - agent skill: SKILL.md recipe + endpoints.md rows so agents can read and record intent (PUT replaces: read + merge; never delete unprompted) - groundwork for the phase-2 predictor button; nothing is ever auto-sent Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ export { registerCronRoutes } from './cron-routes.js';
|
||||
export { registerSystemRoutes } from './system-routes.js';
|
||||
export { registerHookEventRoutes } from './hook-event-routes.js';
|
||||
export { registerApprovalRoutes } from './approval-routes.js';
|
||||
export { registerReadMyMindRoutes } from './readmymind-routes.js';
|
||||
export { registerStatusTelemetryRoutes } from './status-telemetry-routes.js';
|
||||
export { registerCaseRoutes } from './case-routes.js';
|
||||
export { registerSessionRoutes } from './session-routes.js';
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @fileoverview Read My Mind intent routes.
|
||||
*
|
||||
* Per-case intent profiles feeding the Read My Mind predictor
|
||||
* (docs/readmymind-plan.md):
|
||||
* - `GET /api/sessions/:id/intent`: the profile for the session's case
|
||||
* - `PUT /api/sessions/:id/intent`: replace the goals text
|
||||
* - `DELETE /api/sessions/:id/intent`: forget the case's profile
|
||||
*
|
||||
* The profile is keyed by owner + workingDir, so multi-user scoping is
|
||||
* structural; session ownership is still enforced via `findSessionOrFail`
|
||||
* (with `req`, so a foreign session id 404s) to keep the session-routes
|
||||
* no-existence-leak policy.
|
||||
*
|
||||
* Deliberately session-scoped rather than a raw `/api/intents/:key` surface:
|
||||
* the session resolves owner + workingDir server-side, so a caller can never
|
||||
* address another case's profile by guessing keys.
|
||||
*
|
||||
* Registrations use the bare `app.<method>('path', ...)` + `req.params as`
|
||||
* shape (session-routes style): these endpoints are documented in the agent
|
||||
* skill, and the endpoints.md drift test's scanner does not see registrations
|
||||
* with a generic between the method and the path.
|
||||
*/
|
||||
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { IntentGoalsSchema } from '../schemas.js';
|
||||
import { parseBody, findSessionOrFail } from '../route-helpers.js';
|
||||
import { intentStore } from '../../intent-store.js';
|
||||
import type { SessionPort } from '../ports/index.js';
|
||||
|
||||
export function registerReadMyMindRoutes(app: FastifyInstance, ctx: SessionPort): void {
|
||||
app.get('/api/sessions/:id/intent', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
return { success: true, data: { intent: intentStore.getProfile(session.owner, session.workingDir) } };
|
||||
});
|
||||
|
||||
app.put('/api/sessions/:id/intent', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const body = parseBody(IntentGoalsSchema, req.body);
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
return { success: true, data: { intent: intentStore.setGoals(session.owner, session.workingDir, body.goals) } };
|
||||
});
|
||||
|
||||
app.delete('/api/sessions/:id/intent', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
return { success: true, data: { deleted: intentStore.deleteProfile(session.owner, session.workingDir) } };
|
||||
});
|
||||
}
|
||||
@@ -700,6 +700,16 @@ export const ApprovalAnswerSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Body of PUT /api/sessions/:id/intent (Read My Mind). The 8192 cap mirrors
|
||||
* MAX_GOALS_CHARS in intent-store.ts.
|
||||
*/
|
||||
export const IntentGoalsSchema = z
|
||||
.object({
|
||||
goals: z.string().max(8192),
|
||||
})
|
||||
.strict();
|
||||
|
||||
// ========== Configuration ==========
|
||||
|
||||
/**
|
||||
@@ -809,6 +819,13 @@ export const SettingsUpdateSchema = z
|
||||
* already pending immediately.
|
||||
*/
|
||||
approvalsInboxEnabled: z.boolean().optional(),
|
||||
/**
|
||||
* Read My Mind (docs/readmymind-plan.md): capture the user's submitted
|
||||
* prompts into per-case intent profiles. SYNCED, default OFF (opt-in:
|
||||
* captured prompts are sensitive). OFF stops capture immediately; already
|
||||
* stored profiles stay until DELETE /api/sessions/:id/intent.
|
||||
*/
|
||||
readMyMindEnabled: z.boolean().optional(),
|
||||
tunnelEnabled: z.boolean().optional(),
|
||||
// Action field (NOT persisted): explicit per-request acknowledgment that the
|
||||
// operator accepts exposing an UNAUTHENTICATED public tunnel (no CODEMAN_PASSWORD).
|
||||
|
||||
+26
-1
@@ -85,7 +85,8 @@ import {
|
||||
attachSessionListeners,
|
||||
detachSessionListeners,
|
||||
} from './session-listener-wiring.js';
|
||||
import { sessionWaits } from './session-wait-registry.js';
|
||||
import { sessionWaits, hooksAvailableForMode } from './session-wait-registry.js';
|
||||
import { intentStore } from '../intent-store.js';
|
||||
import { approvalInbox } from './approval-inbox.js';
|
||||
import {
|
||||
wireRespawnListeners,
|
||||
@@ -149,6 +150,7 @@ import {
|
||||
registerScheduledRoutes,
|
||||
registerHookEventRoutes,
|
||||
registerApprovalRoutes,
|
||||
registerReadMyMindRoutes,
|
||||
registerStatusTelemetryRoutes,
|
||||
registerSystemRoutes,
|
||||
registerCaseRoutes,
|
||||
@@ -955,6 +957,7 @@ export class WebServer extends EventEmitter {
|
||||
registerScheduledRoutes(this.app, ctx);
|
||||
registerHookEventRoutes(this.app, ctx);
|
||||
registerApprovalRoutes(this.app, ctx);
|
||||
registerReadMyMindRoutes(this.app, ctx);
|
||||
registerStatusTelemetryRoutes(this.app, ctx);
|
||||
registerSystemRoutes(this.app, ctx);
|
||||
registerCaseRoutes(this.app, ctx);
|
||||
@@ -1022,6 +1025,10 @@ export class WebServer extends EventEmitter {
|
||||
console.error(`[Transcript] Error for session ${sessionId}:`, error.message);
|
||||
});
|
||||
|
||||
watcher.on('transcript:user_prompt', (text: string) => {
|
||||
void this.captureIntentPrompt(sessionId, text);
|
||||
});
|
||||
|
||||
this.transcriptWatchers.set(sessionId, watcher);
|
||||
}
|
||||
|
||||
@@ -1029,6 +1036,24 @@ export class WebServer extends EventEmitter {
|
||||
watcher.updatePath(transcriptPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read My Mind intent capture: fold one transcript user prompt into the
|
||||
* case's intent profile (docs/readmymind-plan.md). Opt-in via
|
||||
* `readMyMindEnabled` (default OFF) and claude-only; the mode gate is
|
||||
* belt-and-braces since only hook-fed sessions have a transcript watcher.
|
||||
*/
|
||||
private async captureIntentPrompt(sessionId: string, text: string): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session || !hooksAvailableForMode(session.mode)) return;
|
||||
try {
|
||||
const settings = await this.readSettings();
|
||||
if (settings.readMyMindEnabled !== true) return;
|
||||
intentStore.recordPrompt(session.owner, session.workingDir, sessionId, text);
|
||||
} catch (err) {
|
||||
console.warn(`[IntentStore] Capture failed for session ${sessionId}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the transcript watcher for a session.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user