akemon 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DATA_POLICY.md +128 -0
- package/README.md +156 -19
- package/TRADEMARK.md +74 -0
- package/dist/akemon-home.js +56 -0
- package/dist/akemon-message.js +107 -0
- package/dist/best-effort.js +8 -0
- package/dist/cli.js +1411 -132
- package/dist/cognitive-artifact-store.js +101 -0
- package/dist/cognitive-event-log.js +47 -0
- package/dist/config.js +45 -9
- package/dist/context.js +27 -6
- package/dist/core/contracts/layers.js +1 -0
- package/dist/core/contracts/permission.js +1 -0
- package/dist/core/contracts/workspace.js +1 -0
- package/dist/core-cognitive-module.js +768 -0
- package/dist/engine-peripheral.js +127 -26
- package/dist/engine-routing.js +58 -17
- package/dist/interactive-session.js +361 -0
- package/dist/local-interconnect.js +156 -0
- package/dist/local-registry.js +178 -0
- package/dist/mcp-server.js +4 -1
- package/dist/memory-proposal.js +379 -0
- package/dist/memory-recorder.js +368 -0
- package/dist/orphan-scan.js +36 -24
- package/dist/passive-reflection-cognitive-module.js +172 -0
- package/dist/peripheral-registry.js +235 -0
- package/dist/permission-audit.js +132 -0
- package/dist/relay-client.js +68 -9
- package/dist/relay-mode.js +34 -0
- package/dist/relay-peripheral.js +139 -49
- package/dist/runtime-platform.js +122 -0
- package/dist/secretariat/client.js +87 -0
- package/dist/self.js +15 -6
- package/dist/server.js +3695 -439
- package/dist/social-discovery.js +231 -0
- package/dist/software-agent-peripheral.js +314 -235
- package/dist/software-agent-result-cli.js +69 -0
- package/dist/software-agent-stream-cli.js +23 -0
- package/dist/software-agent-transport.js +177 -0
- package/dist/task-module.js +243 -0
- package/dist/task-registry.js +756 -0
- package/dist/vendor/xterm/addon-fit.js +2 -0
- package/dist/vendor/xterm/addon-search.js +2 -0
- package/dist/vendor/xterm/addon-web-links.js +2 -0
- package/dist/vendor/xterm/xterm.css +285 -0
- package/dist/vendor/xterm/xterm.js +2 -0
- package/dist/work-memory.js +339 -0
- package/dist/workbench-peripheral-guide.js +79 -0
- package/dist/workbench-session.js +1074 -0
- package/dist/workbench.html +4011 -0
- package/package.json +11 -4
- package/scripts/build.cjs +24 -0
- package/scripts/check-architecture-baseline.cjs +68 -0
- package/scripts/test.cjs +38 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { appendFile, mkdir, readdir, readFile } from "fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "path";
|
|
4
|
+
import { localNow } from "./self.js";
|
|
5
|
+
import { redactSecrets, redactText } from "./redaction.js";
|
|
6
|
+
import { actorRef } from "./akemon-message.js";
|
|
7
|
+
import { appendPermissionAuditRecord } from "./permission-audit.js";
|
|
8
|
+
import { logBestEffortError } from "./best-effort.js";
|
|
9
|
+
import { cleanAgentName, globalWorkMemoryDir, projectWorkMemoryDir, } from "./akemon-home.js";
|
|
10
|
+
const DEFAULT_CONTEXT_BUDGET = 12_000;
|
|
11
|
+
const MIN_CONTEXT_BUDGET = 1_000;
|
|
12
|
+
const MAX_CONTEXT_BUDGET = 80_000;
|
|
13
|
+
const MAX_SECTION_CHARS = 4_000;
|
|
14
|
+
const MAX_INDEX_FILES = 200;
|
|
15
|
+
const MAX_INDEX_DEPTH = 4;
|
|
16
|
+
const WORK_INBOX_FILE = "inbox.md";
|
|
17
|
+
const DEFAULT_CONTEXT_FILES = [
|
|
18
|
+
"README.md",
|
|
19
|
+
"current.md",
|
|
20
|
+
"handoff.md",
|
|
21
|
+
"decisions.md",
|
|
22
|
+
"commands.md",
|
|
23
|
+
"notes.md",
|
|
24
|
+
WORK_INBOX_FILE,
|
|
25
|
+
];
|
|
26
|
+
const INDEX_FILE_EXTENSIONS = new Set([
|
|
27
|
+
".md",
|
|
28
|
+
".txt",
|
|
29
|
+
".json",
|
|
30
|
+
".jsonl",
|
|
31
|
+
".yaml",
|
|
32
|
+
".yml",
|
|
33
|
+
]);
|
|
34
|
+
export function workMemoryDir(workdir, agentName) {
|
|
35
|
+
cleanAgentName(agentName);
|
|
36
|
+
return projectWorkMemoryDir(workdir);
|
|
37
|
+
}
|
|
38
|
+
export function globalAgentWorkMemoryDir(agentName) {
|
|
39
|
+
return globalWorkMemoryDir(agentName);
|
|
40
|
+
}
|
|
41
|
+
export function resolveWorkMemoryDir(workdir, agentName, global = false) {
|
|
42
|
+
return global ? globalAgentWorkMemoryDir(agentName) : workMemoryDir(workdir, agentName);
|
|
43
|
+
}
|
|
44
|
+
export function workMemoryInboxPath(workdir, agentName) {
|
|
45
|
+
return join(workMemoryDir(workdir, agentName), WORK_INBOX_FILE);
|
|
46
|
+
}
|
|
47
|
+
export function globalWorkMemoryInboxPath(agentName) {
|
|
48
|
+
return join(globalAgentWorkMemoryDir(agentName), WORK_INBOX_FILE);
|
|
49
|
+
}
|
|
50
|
+
export async function buildWorkMemoryContext(opts) {
|
|
51
|
+
const budget = normalizeBudget(opts.budget);
|
|
52
|
+
const agentName = cleanAgentName(opts.agentName);
|
|
53
|
+
const root = resolveWorkMemoryDir(opts.workdir, agentName, opts.global === true);
|
|
54
|
+
const workMemoryScope = opts.global === true ? "global" : "project";
|
|
55
|
+
const generatedAt = localNow();
|
|
56
|
+
const purpose = cleanSingleLine(opts.purpose || "external software-agent work context", 180);
|
|
57
|
+
const sections = await collectWorkContextSections(root);
|
|
58
|
+
const text = renderWorkMemoryContext({
|
|
59
|
+
agentName,
|
|
60
|
+
workdir: opts.workdir,
|
|
61
|
+
workMemoryDir: root,
|
|
62
|
+
workMemoryScope,
|
|
63
|
+
generatedAt,
|
|
64
|
+
purpose,
|
|
65
|
+
budget,
|
|
66
|
+
sections,
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
agentName,
|
|
70
|
+
workdir: opts.workdir,
|
|
71
|
+
workMemoryDir: root,
|
|
72
|
+
workMemoryScope,
|
|
73
|
+
generatedAt,
|
|
74
|
+
purpose,
|
|
75
|
+
budget,
|
|
76
|
+
sections,
|
|
77
|
+
text: fitText(text, budget),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export async function appendWorkMemoryNote(input) {
|
|
81
|
+
const text = cleanMultiline(input.text, 8_000);
|
|
82
|
+
if (!text)
|
|
83
|
+
throw new Error("Missing required work memory note text");
|
|
84
|
+
const note = {
|
|
85
|
+
id: `work_${Date.now()}_${randomUUID().slice(0, 8)}`,
|
|
86
|
+
ts: localNow(),
|
|
87
|
+
agentName: cleanAgentName(input.agentName),
|
|
88
|
+
source: cleanToken(input.source || "user", "source", 80),
|
|
89
|
+
kind: cleanToken(input.kind || "note", "kind", 80),
|
|
90
|
+
text,
|
|
91
|
+
};
|
|
92
|
+
const sessionId = cleanOptionalToken(input.sessionId, "sessionId", 120);
|
|
93
|
+
if (sessionId)
|
|
94
|
+
note.sessionId = sessionId;
|
|
95
|
+
const target = cleanOptionalPathHint(input.target);
|
|
96
|
+
if (target)
|
|
97
|
+
note.target = target;
|
|
98
|
+
const redacted = redactSecrets(note);
|
|
99
|
+
const root = resolveWorkMemoryDir(input.workdir, note.agentName, input.global === true);
|
|
100
|
+
const path = target
|
|
101
|
+
? join(root, target)
|
|
102
|
+
: join(root, WORK_INBOX_FILE);
|
|
103
|
+
await mkdir(dirname(path), { recursive: true });
|
|
104
|
+
await appendFile(path, renderWorkMemoryNote(redacted), "utf-8");
|
|
105
|
+
try {
|
|
106
|
+
await appendPermissionAuditRecord(note.agentName, {
|
|
107
|
+
actionKind: "memory-write",
|
|
108
|
+
action: "work-memory.note.append",
|
|
109
|
+
requestedBy: note.source === "user"
|
|
110
|
+
? actorRef("owner", "owner", "local")
|
|
111
|
+
: actorRef("external", note.source, "unknown"),
|
|
112
|
+
performedBy: actorRef("agent", note.agentName, "local"),
|
|
113
|
+
target: actorRef("system", input.global === true ? "global-work-memory" : "project-work-memory", "local"),
|
|
114
|
+
riskLevel: "low",
|
|
115
|
+
memoryScope: "task",
|
|
116
|
+
workdir: input.workdir,
|
|
117
|
+
projectScope: input.global === true ? "global" : "inside-workdir",
|
|
118
|
+
transport: "local",
|
|
119
|
+
decision: {
|
|
120
|
+
result: "allowed",
|
|
121
|
+
mode: note.source === "user" ? "owner-approved" : "automatic",
|
|
122
|
+
reason: "Work memory note target was validated under the configured work memory directory.",
|
|
123
|
+
},
|
|
124
|
+
references: {
|
|
125
|
+
noteId: note.id,
|
|
126
|
+
path,
|
|
127
|
+
sessionId: note.sessionId,
|
|
128
|
+
},
|
|
129
|
+
metadata: {
|
|
130
|
+
kind: note.kind,
|
|
131
|
+
target: note.target,
|
|
132
|
+
global: input.global === true,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
logBestEffortError("work-memory audit append", error);
|
|
138
|
+
}
|
|
139
|
+
return { note: redacted, path };
|
|
140
|
+
}
|
|
141
|
+
async function collectWorkContextSections(root) {
|
|
142
|
+
const sections = [];
|
|
143
|
+
for (const file of DEFAULT_CONTEXT_FILES) {
|
|
144
|
+
const section = await readWorkContextFile(root, file);
|
|
145
|
+
if (section)
|
|
146
|
+
sections.push(section);
|
|
147
|
+
}
|
|
148
|
+
const index = await buildWorkFileIndex(root);
|
|
149
|
+
if (index)
|
|
150
|
+
sections.push(index);
|
|
151
|
+
return sections;
|
|
152
|
+
}
|
|
153
|
+
async function readWorkContextFile(root, relativePath) {
|
|
154
|
+
const path = join(root, relativePath);
|
|
155
|
+
let raw;
|
|
156
|
+
try {
|
|
157
|
+
raw = await readFile(path, "utf-8");
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const redacted = redactText(selectUsefulFileContent(relativePath, raw));
|
|
163
|
+
const trimmed = redacted.trim();
|
|
164
|
+
const content = fitText(trimmed, MAX_SECTION_CHARS);
|
|
165
|
+
return {
|
|
166
|
+
title: relativePath,
|
|
167
|
+
path,
|
|
168
|
+
relativePath,
|
|
169
|
+
chars: trimmed.length,
|
|
170
|
+
truncated: content.length < trimmed.length,
|
|
171
|
+
content,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function buildWorkFileIndex(root) {
|
|
175
|
+
const files = await listWorkMemoryFiles(root);
|
|
176
|
+
if (!files.length)
|
|
177
|
+
return null;
|
|
178
|
+
const content = files.map((file) => `- ${file}`).join("\n");
|
|
179
|
+
return {
|
|
180
|
+
title: "work memory file index",
|
|
181
|
+
path: root,
|
|
182
|
+
relativePath: ".",
|
|
183
|
+
chars: content.length,
|
|
184
|
+
truncated: files.length >= MAX_INDEX_FILES,
|
|
185
|
+
content,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function listWorkMemoryFiles(root) {
|
|
189
|
+
const files = [];
|
|
190
|
+
await walk(root, "", 0, files);
|
|
191
|
+
return files.sort((a, b) => a.localeCompare(b)).slice(0, MAX_INDEX_FILES);
|
|
192
|
+
}
|
|
193
|
+
async function walk(root, relativeDir, depth, files) {
|
|
194
|
+
if (depth > MAX_INDEX_DEPTH || files.length >= MAX_INDEX_FILES)
|
|
195
|
+
return;
|
|
196
|
+
const dir = relativeDir ? join(root, relativeDir) : root;
|
|
197
|
+
let entries;
|
|
198
|
+
try {
|
|
199
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
205
|
+
if (files.length >= MAX_INDEX_FILES)
|
|
206
|
+
return;
|
|
207
|
+
if (entry.name.startsWith("."))
|
|
208
|
+
continue;
|
|
209
|
+
const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
210
|
+
if (entry.isDirectory()) {
|
|
211
|
+
await walk(root, relativePath, depth + 1, files);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!entry.isFile())
|
|
215
|
+
continue;
|
|
216
|
+
if (!INDEX_FILE_EXTENSIONS.has(extensionOf(entry.name)))
|
|
217
|
+
continue;
|
|
218
|
+
files.push(relativePath);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function renderWorkMemoryContext(packet) {
|
|
222
|
+
const globalFlag = packet.workMemoryScope === "global" ? " --global" : "";
|
|
223
|
+
const lines = [
|
|
224
|
+
"# Akemon Work Memory Context",
|
|
225
|
+
"",
|
|
226
|
+
`Generated at: ${packet.generatedAt}`,
|
|
227
|
+
`Agent: ${packet.agentName}`,
|
|
228
|
+
`Scope: ${packet.workMemoryScope}`,
|
|
229
|
+
`Purpose: ${packet.purpose}`,
|
|
230
|
+
`Workdir: ${packet.workdir}`,
|
|
231
|
+
`Work memory directory: ${packet.workMemoryDir}`,
|
|
232
|
+
"",
|
|
233
|
+
"## Boundary",
|
|
234
|
+
"",
|
|
235
|
+
"- This is user-owned work memory for engineering and task continuity.",
|
|
236
|
+
"- External tools such as Codex or Claude Code may read this work directory as task context.",
|
|
237
|
+
"- External tools may update this work directory when the user or task asks them to maintain work memory.",
|
|
238
|
+
"- Do not read or edit Akemon self memory through this work-memory interface.",
|
|
239
|
+
"- Use grep, direct file reading, semantic review, or your own tool workflow as appropriate for the task.",
|
|
240
|
+
"",
|
|
241
|
+
"## Suggested Update Command",
|
|
242
|
+
"",
|
|
243
|
+
"```bash",
|
|
244
|
+
`akemon work-note${globalFlag} \"<durable work memory>\" --source codex --kind decision`,
|
|
245
|
+
"```",
|
|
246
|
+
];
|
|
247
|
+
if (!packet.sections.length) {
|
|
248
|
+
lines.push("", "## Included Work Memory", "", "No work memory files were found yet.");
|
|
249
|
+
lines.push("", "Create files under the work memory directory or use `akemon work-note` to append a quick note.");
|
|
250
|
+
return lines.join("\n");
|
|
251
|
+
}
|
|
252
|
+
lines.push("", "## Included Work Memory");
|
|
253
|
+
for (const section of packet.sections) {
|
|
254
|
+
lines.push("");
|
|
255
|
+
lines.push(`### ${section.title}`);
|
|
256
|
+
if (section.relativePath)
|
|
257
|
+
lines.push(`Path: ${section.relativePath}`);
|
|
258
|
+
if (section.truncated)
|
|
259
|
+
lines.push(`Truncated from ${section.chars} chars.`);
|
|
260
|
+
lines.push("");
|
|
261
|
+
lines.push("```");
|
|
262
|
+
lines.push(section.content || "(empty)");
|
|
263
|
+
lines.push("```");
|
|
264
|
+
}
|
|
265
|
+
return lines.join("\n");
|
|
266
|
+
}
|
|
267
|
+
function renderWorkMemoryNote(note) {
|
|
268
|
+
const lines = [
|
|
269
|
+
"",
|
|
270
|
+
`## ${note.ts} ${note.kind}`,
|
|
271
|
+
"",
|
|
272
|
+
`Source: ${note.source}`,
|
|
273
|
+
];
|
|
274
|
+
if (note.sessionId)
|
|
275
|
+
lines.push(`Session: ${note.sessionId}`);
|
|
276
|
+
if (note.target)
|
|
277
|
+
lines.push(`Target: ${note.target}`);
|
|
278
|
+
lines.push("", note.text.trim(), "");
|
|
279
|
+
return lines.join("\n");
|
|
280
|
+
}
|
|
281
|
+
function selectUsefulFileContent(relativePath, raw) {
|
|
282
|
+
if (!relativePath.endsWith(".jsonl"))
|
|
283
|
+
return raw;
|
|
284
|
+
const lines = raw.trim().split(/\r?\n/).filter(Boolean);
|
|
285
|
+
return lines.slice(-20).join("\n");
|
|
286
|
+
}
|
|
287
|
+
function normalizeBudget(value) {
|
|
288
|
+
if (value === undefined || value === null)
|
|
289
|
+
return DEFAULT_CONTEXT_BUDGET;
|
|
290
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
291
|
+
throw new Error("Context budget must be a positive integer");
|
|
292
|
+
return Math.max(MIN_CONTEXT_BUDGET, Math.min(MAX_CONTEXT_BUDGET, value));
|
|
293
|
+
}
|
|
294
|
+
function cleanToken(value, field, maxChars) {
|
|
295
|
+
const cleaned = cleanSingleLine(value, maxChars);
|
|
296
|
+
if (!cleaned)
|
|
297
|
+
throw new Error(`Missing required ${field}`);
|
|
298
|
+
if (!/^[A-Za-z0-9_.:@-]+$/.test(cleaned)) {
|
|
299
|
+
throw new Error(`Invalid ${field}: expected letters, numbers, dot, underscore, colon, at, or hyphen`);
|
|
300
|
+
}
|
|
301
|
+
return cleaned;
|
|
302
|
+
}
|
|
303
|
+
function cleanOptionalToken(value, field, maxChars) {
|
|
304
|
+
if (value === undefined || value === null || value.trim() === "")
|
|
305
|
+
return undefined;
|
|
306
|
+
return cleanToken(value, field, maxChars);
|
|
307
|
+
}
|
|
308
|
+
function cleanOptionalPathHint(value) {
|
|
309
|
+
if (value === undefined || value === null || value.trim() === "")
|
|
310
|
+
return undefined;
|
|
311
|
+
const cleaned = cleanSingleLine(value, 240);
|
|
312
|
+
if (cleaned.includes("\0") || cleaned.startsWith("/") || cleaned.includes("\\") || cleaned.split("/").includes("..")) {
|
|
313
|
+
throw new Error("Invalid target path");
|
|
314
|
+
}
|
|
315
|
+
const base = basename(cleaned);
|
|
316
|
+
if (base === "." || base === "..")
|
|
317
|
+
throw new Error("Invalid target path");
|
|
318
|
+
return cleaned;
|
|
319
|
+
}
|
|
320
|
+
function cleanSingleLine(value, maxChars) {
|
|
321
|
+
return fitText(String(value || "").replace(/\s+/g, " ").trim(), maxChars);
|
|
322
|
+
}
|
|
323
|
+
function cleanMultiline(value, maxChars) {
|
|
324
|
+
return fitText(String(value || "").replace(/\0/g, "").trim(), maxChars);
|
|
325
|
+
}
|
|
326
|
+
function fitText(text, maxChars) {
|
|
327
|
+
if (text.length <= maxChars)
|
|
328
|
+
return text;
|
|
329
|
+
if (maxChars <= 20)
|
|
330
|
+
return text.slice(0, maxChars);
|
|
331
|
+
const omitted = text.length - maxChars;
|
|
332
|
+
const head = Math.floor(maxChars * 0.65);
|
|
333
|
+
const tail = Math.max(0, maxChars - head - 40);
|
|
334
|
+
return `${text.slice(0, head)}\n[truncated ${omitted} chars]\n${text.slice(-tail)}`;
|
|
335
|
+
}
|
|
336
|
+
function extensionOf(file) {
|
|
337
|
+
const index = file.lastIndexOf(".");
|
|
338
|
+
return index >= 0 ? file.slice(index).toLowerCase() : "";
|
|
339
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const WORKBENCH_PERIPHERAL_GUIDE = [
|
|
2
|
+
"[Interactive Session Peripheral Usage Guide]",
|
|
3
|
+
"Workbench is the current local surface for the interactive-session peripheral.",
|
|
4
|
+
"The interactive session capability owns local Claude, Codex, shell, cursor, and custom PTY sessions; Workbench displays and controls it.",
|
|
5
|
+
"",
|
|
6
|
+
"Use interactive-session when:",
|
|
7
|
+
"- the owner asks to start or observe a local Claude, Codex, shell, or custom PTY session",
|
|
8
|
+
"- the owner asks what interactive sessions are running",
|
|
9
|
+
"- the owner asks to send text to an existing session",
|
|
10
|
+
"- the owner wants to take over, stop, or resize a local interactive session",
|
|
11
|
+
"",
|
|
12
|
+
"Do not use Workbench when:",
|
|
13
|
+
"- a direct Core CM answer is enough",
|
|
14
|
+
"- the task is primarily a durable memory update",
|
|
15
|
+
"- the requested action is outside the adapter's exposed capabilities",
|
|
16
|
+
"",
|
|
17
|
+
"Current exposed capabilities:",
|
|
18
|
+
"- list_sessions: inspect tracked Workbench sessions",
|
|
19
|
+
"- inspect_session: inspect one tracked Workbench session's status and recent output",
|
|
20
|
+
"- start_session: start a codex, claude, cursor, shell, or custom session",
|
|
21
|
+
"- send_input: send text or an Enter key to an existing session",
|
|
22
|
+
"- set_input_mode: switch a session between line input and TUI input",
|
|
23
|
+
"- resize_session: resize an existing interactive session",
|
|
24
|
+
"- stop_session: stop an existing session",
|
|
25
|
+
"- capture_output: capture recent output from an existing session",
|
|
26
|
+
"",
|
|
27
|
+
"Important behavior rules:",
|
|
28
|
+
"- Do not claim that a session action happened until Akemon receives an adapter observation.",
|
|
29
|
+
"- If a session action is needed but has not happened yet, say what action should be taken next.",
|
|
30
|
+
"- Explain interactive session actions to the owner in natural language.",
|
|
31
|
+
"- Do not present slash commands as the normal user interface unless the owner explicitly asks for manual/debug commands.",
|
|
32
|
+
"- Slash commands remain available only as a debug/manual control path.",
|
|
33
|
+
].join("\n");
|
|
34
|
+
export const WORKBENCH_ACTION_PROPOSAL_GUIDE = [
|
|
35
|
+
"[Interactive Session Action Proposal Contract]",
|
|
36
|
+
"When the owner's request requires an interactive session action, output only JSON in this exact shape:",
|
|
37
|
+
"{",
|
|
38
|
+
" \"kind\": \"use_peripheral\",",
|
|
39
|
+
" \"peripheral\": \"interactive-session\",",
|
|
40
|
+
" \"capability\": \"list_sessions\" | \"inspect_session\" | \"start_session\" | \"send_input\" | \"set_input_mode\" | \"stop_session\" | \"resize_session\" | \"capture_output\",",
|
|
41
|
+
" \"args\": { ... },",
|
|
42
|
+
" \"reason\": \"short machine-readable reason\",",
|
|
43
|
+
" \"processNote\": \"one concise owner-facing sentence explaining why Core CM is taking this step\"",
|
|
44
|
+
"}",
|
|
45
|
+
"",
|
|
46
|
+
"When no interactive session action is needed, output only JSON in this exact shape:",
|
|
47
|
+
"{",
|
|
48
|
+
" \"kind\": \"answer\",",
|
|
49
|
+
" \"answer\": \"natural-language answer to the owner\",",
|
|
50
|
+
" \"processNote\": \"one concise owner-facing sentence explaining how Core CM handled this request\"",
|
|
51
|
+
"}",
|
|
52
|
+
"",
|
|
53
|
+
"Allowed args:",
|
|
54
|
+
"- list_sessions: {}",
|
|
55
|
+
"- inspect_session: { \"sessionId\": string }",
|
|
56
|
+
"- start_session: { \"tool\": \"codex\" | \"claude\" | \"cursor\" | \"shell\" | \"custom\", \"args\"?: string[], \"command\"?: string, \"workdir\"?: string, \"label\"?: string, \"inputMode\"?: \"line\" | \"tui\" }",
|
|
57
|
+
"- send_input: { \"sessionId\": string, \"input\": string, \"waitMs\"?: number }",
|
|
58
|
+
"- set_input_mode: { \"sessionId\": string, \"inputMode\": \"line\" | \"tui\" }",
|
|
59
|
+
"- stop_session: { \"sessionId\": string }",
|
|
60
|
+
"- resize_session: { \"sessionId\": string, \"cols\": number, \"rows\": number }",
|
|
61
|
+
"- capture_output: { \"sessionId\": string }",
|
|
62
|
+
"",
|
|
63
|
+
"Rules:",
|
|
64
|
+
"- Output JSON only. No markdown fences.",
|
|
65
|
+
"- Always use peripheral \"interactive-session\". Legacy \"workbench\" proposals are accepted only for compatibility, not as new output.",
|
|
66
|
+
"- Always write processNote in the configured owner-facing language. It is for the CM area, not a debug log.",
|
|
67
|
+
"- Do not put JSON keys, taskKind, capability names, adapter names, token counts, or event ids in processNote unless the owner explicitly asks for debug details.",
|
|
68
|
+
"- Use inspect_session when the owner asks what a specific session returned, what it is doing, or whether it has new output.",
|
|
69
|
+
"- Use capture_output when the owner specifically asks for raw recent output instead of a natural-language inspection.",
|
|
70
|
+
"- Use start_session only when the owner asks to start a local interactive session.",
|
|
71
|
+
"- Use send_input only when the owner asks to send text to an existing session.",
|
|
72
|
+
"- Use resize_session when the owner asks Akemon to resize a terminal session.",
|
|
73
|
+
"- Use set_input_mode when the owner says a shell/custom session is now running Codex, Cursor, or another interactive TUI and input is not submitting.",
|
|
74
|
+
"- For send_input, include the text exactly as the owner wants it sent; Akemon will append Enter if needed.",
|
|
75
|
+
"- Input modes: line is normal terminal input. tui uses bracketed paste plus delayed Enter for interactive TUIs. Do not guess TUI mode from output unless the owner gives a clear clue.",
|
|
76
|
+
"- For send_input, set waitMs when the owner expects Akemon to observe the immediate result. Use 3000-8000 for typical CLI commands, and omit it for fire-and-forget interactive input. Maximum is 10000.",
|
|
77
|
+
"- Do not invent session ids. Use ids from the current interactive session context, or use sessionId \"latest\" when the owner clearly means the most recent session.",
|
|
78
|
+
"- Session aliases: \"active\" and \"current\" mean the browser-selected Workbench session when available; \"latest\" and \"last\" mean the most recently started matching session; \"recent\" means the most recently updated matching session.",
|
|
79
|
+
].join("\n");
|