akemon 0.3.6 → 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.
Files changed (51) hide show
  1. package/DATA_POLICY.md +11 -3
  2. package/README.md +133 -21
  3. package/dist/akemon-home.js +56 -0
  4. package/dist/akemon-message.js +107 -0
  5. package/dist/best-effort.js +8 -0
  6. package/dist/cli.js +1188 -100
  7. package/dist/cognitive-artifact-store.js +101 -0
  8. package/dist/cognitive-event-log.js +47 -0
  9. package/dist/config.js +45 -9
  10. package/dist/context.js +27 -6
  11. package/dist/core/contracts/layers.js +1 -0
  12. package/dist/core/contracts/permission.js +1 -0
  13. package/dist/core/contracts/workspace.js +1 -0
  14. package/dist/core-cognitive-module.js +768 -0
  15. package/dist/engine-peripheral.js +127 -26
  16. package/dist/engine-routing.js +58 -17
  17. package/dist/interactive-session.js +361 -0
  18. package/dist/local-interconnect.js +156 -0
  19. package/dist/local-registry.js +178 -0
  20. package/dist/mcp-server.js +4 -1
  21. package/dist/memory-proposal.js +379 -0
  22. package/dist/memory-recorder.js +368 -0
  23. package/dist/orphan-scan.js +36 -24
  24. package/dist/passive-reflection-cognitive-module.js +172 -0
  25. package/dist/peripheral-registry.js +235 -0
  26. package/dist/permission-audit.js +132 -0
  27. package/dist/relay-client.js +68 -9
  28. package/dist/relay-mode.js +34 -0
  29. package/dist/relay-peripheral.js +139 -49
  30. package/dist/runtime-platform.js +122 -0
  31. package/dist/secretariat/client.js +87 -0
  32. package/dist/self.js +15 -6
  33. package/dist/server.js +3675 -512
  34. package/dist/social-discovery.js +231 -0
  35. package/dist/software-agent-peripheral.js +185 -244
  36. package/dist/software-agent-transport.js +177 -0
  37. package/dist/task-module.js +243 -0
  38. package/dist/task-registry.js +756 -0
  39. package/dist/vendor/xterm/addon-fit.js +2 -0
  40. package/dist/vendor/xterm/addon-search.js +2 -0
  41. package/dist/vendor/xterm/addon-web-links.js +2 -0
  42. package/dist/vendor/xterm/xterm.css +285 -0
  43. package/dist/vendor/xterm/xterm.js +2 -0
  44. package/dist/work-memory.js +59 -15
  45. package/dist/workbench-peripheral-guide.js +79 -0
  46. package/dist/workbench-session.js +1074 -0
  47. package/dist/workbench.html +4011 -0
  48. package/package.json +8 -3
  49. package/scripts/build.cjs +24 -0
  50. package/scripts/check-architecture-baseline.cjs +68 -0
  51. package/scripts/test.cjs +38 -0
@@ -3,13 +3,16 @@ import { appendFile, mkdir, readdir, readFile } from "fs/promises";
3
3
  import { basename, dirname, join } from "path";
4
4
  import { localNow } from "./self.js";
5
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";
6
10
  const DEFAULT_CONTEXT_BUDGET = 12_000;
7
11
  const MIN_CONTEXT_BUDGET = 1_000;
8
12
  const MAX_CONTEXT_BUDGET = 80_000;
9
13
  const MAX_SECTION_CHARS = 4_000;
10
14
  const MAX_INDEX_FILES = 200;
11
15
  const MAX_INDEX_DEPTH = 4;
12
- const WORK_DIR = "work";
13
16
  const WORK_INBOX_FILE = "inbox.md";
14
17
  const DEFAULT_CONTEXT_FILES = [
15
18
  "README.md",
@@ -29,15 +32,26 @@ const INDEX_FILE_EXTENSIONS = new Set([
29
32
  ".yml",
30
33
  ]);
31
34
  export function workMemoryDir(workdir, agentName) {
32
- return join(workdir, ".akemon", "agents", cleanAgentName(agentName), WORK_DIR);
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);
33
43
  }
34
44
  export function workMemoryInboxPath(workdir, agentName) {
35
45
  return join(workMemoryDir(workdir, agentName), WORK_INBOX_FILE);
36
46
  }
47
+ export function globalWorkMemoryInboxPath(agentName) {
48
+ return join(globalAgentWorkMemoryDir(agentName), WORK_INBOX_FILE);
49
+ }
37
50
  export async function buildWorkMemoryContext(opts) {
38
51
  const budget = normalizeBudget(opts.budget);
39
52
  const agentName = cleanAgentName(opts.agentName);
40
- const root = workMemoryDir(opts.workdir, agentName);
53
+ const root = resolveWorkMemoryDir(opts.workdir, agentName, opts.global === true);
54
+ const workMemoryScope = opts.global === true ? "global" : "project";
41
55
  const generatedAt = localNow();
42
56
  const purpose = cleanSingleLine(opts.purpose || "external software-agent work context", 180);
43
57
  const sections = await collectWorkContextSections(root);
@@ -45,6 +59,7 @@ export async function buildWorkMemoryContext(opts) {
45
59
  agentName,
46
60
  workdir: opts.workdir,
47
61
  workMemoryDir: root,
62
+ workMemoryScope,
48
63
  generatedAt,
49
64
  purpose,
50
65
  budget,
@@ -54,6 +69,7 @@ export async function buildWorkMemoryContext(opts) {
54
69
  agentName,
55
70
  workdir: opts.workdir,
56
71
  workMemoryDir: root,
72
+ workMemoryScope,
57
73
  generatedAt,
58
74
  purpose,
59
75
  budget,
@@ -80,11 +96,46 @@ export async function appendWorkMemoryNote(input) {
80
96
  if (target)
81
97
  note.target = target;
82
98
  const redacted = redactSecrets(note);
99
+ const root = resolveWorkMemoryDir(input.workdir, note.agentName, input.global === true);
83
100
  const path = target
84
- ? join(workMemoryDir(input.workdir, note.agentName), target)
85
- : workMemoryInboxPath(input.workdir, note.agentName);
101
+ ? join(root, target)
102
+ : join(root, WORK_INBOX_FILE);
86
103
  await mkdir(dirname(path), { recursive: true });
87
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
+ }
88
139
  return { note: redacted, path };
89
140
  }
90
141
  async function collectWorkContextSections(root) {
@@ -168,11 +219,13 @@ async function walk(root, relativeDir, depth, files) {
168
219
  }
169
220
  }
170
221
  function renderWorkMemoryContext(packet) {
222
+ const globalFlag = packet.workMemoryScope === "global" ? " --global" : "";
171
223
  const lines = [
172
224
  "# Akemon Work Memory Context",
173
225
  "",
174
226
  `Generated at: ${packet.generatedAt}`,
175
227
  `Agent: ${packet.agentName}`,
228
+ `Scope: ${packet.workMemoryScope}`,
176
229
  `Purpose: ${packet.purpose}`,
177
230
  `Workdir: ${packet.workdir}`,
178
231
  `Work memory directory: ${packet.workMemoryDir}`,
@@ -188,7 +241,7 @@ function renderWorkMemoryContext(packet) {
188
241
  "## Suggested Update Command",
189
242
  "",
190
243
  "```bash",
191
- `akemon work-note \"<durable work memory>\" --source codex --kind decision`,
244
+ `akemon work-note${globalFlag} \"<durable work memory>\" --source codex --kind decision`,
192
245
  "```",
193
246
  ];
194
247
  if (!packet.sections.length) {
@@ -247,15 +300,6 @@ function cleanToken(value, field, maxChars) {
247
300
  }
248
301
  return cleaned;
249
302
  }
250
- function cleanAgentName(value) {
251
- const cleaned = cleanSingleLine(value, 120);
252
- if (!cleaned)
253
- throw new Error("Missing required agentName");
254
- if (cleaned === "." || cleaned === ".." || cleaned.includes("/") || cleaned.includes("\\") || cleaned.includes("\0")) {
255
- throw new Error("Invalid agentName: path separators and NUL bytes are not allowed");
256
- }
257
- return cleaned;
258
- }
259
303
  function cleanOptionalToken(value, field, maxChars) {
260
304
  if (value === undefined || value === null || value.trim() === "")
261
305
  return undefined;
@@ -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");