akemon 0.3.2 → 0.3.4

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.
@@ -0,0 +1,141 @@
1
+ import { buildLLMContext, loadConversation } from "./context.js";
2
+ import { buildRoleContext, loadRoles, resolveRoles } from "./role-module.js";
3
+ const DEFAULT_CONTEXT_BUDGET = 6000;
4
+ export async function buildSoftwareAgentMemorySummary(opts) {
5
+ const budget = opts.contextBudget ?? DEFAULT_CONTEXT_BUDGET;
6
+ const parts = [
7
+ "[Akemon memory boundary]",
8
+ `Role scope: ${opts.envelope.roleScope}`,
9
+ `Memory scope: ${opts.envelope.memoryScope}`,
10
+ boundaryDescription(opts.envelope.roleScope, opts.envelope.memoryScope),
11
+ ];
12
+ if (opts.envelope.memoryScope === "none") {
13
+ parts.push("No Akemon memory/context is included for this task.");
14
+ return parts.join("\n");
15
+ }
16
+ const request = normalizeRequest(opts.request);
17
+ const roleTrigger = readRequestString(request, "roleTrigger") || triggerForRoleScope(opts.envelope.roleScope);
18
+ const productName = readRequestString(request, "productName");
19
+ const productId = readRequestString(request, "productId");
20
+ const rolePolicy = await resolveRoleMemoryPolicy(opts.workdir, opts.agentName, roleTrigger);
21
+ if (rolePolicy.exclude.length) {
22
+ parts.push(`Active role exclusions: ${rolePolicy.exclude.join(", ")}`);
23
+ }
24
+ const roleContext = await buildRoleContext(opts.workdir, opts.agentName, roleTrigger, productName, productId);
25
+ if (roleContext.trim()) {
26
+ parts.push("");
27
+ parts.push("[Role/product context]");
28
+ parts.push(limitText(roleContext.trim(), Math.floor(budget * 0.55)));
29
+ }
30
+ const taskContext = readRequestString(request, "taskContext");
31
+ if (taskContext) {
32
+ parts.push("");
33
+ parts.push("[Task-provided context]");
34
+ parts.push(limitText(taskContext, Math.floor(budget * 0.25)));
35
+ }
36
+ const conversationId = readRequestString(request, "conversationId");
37
+ if (conversationId && canIncludeConversation(opts.envelope.roleScope, opts.envelope.memoryScope)) {
38
+ const conv = await loadConversation(opts.workdir, opts.agentName, conversationId);
39
+ const { text } = buildLLMContext(conv, Math.floor(budget * 0.3));
40
+ if (text.trim()) {
41
+ parts.push("");
42
+ parts.push("[Conversation context]");
43
+ parts.push(text.trim());
44
+ }
45
+ }
46
+ else if (conversationId) {
47
+ parts.push("");
48
+ parts.push("[Excluded conversation context]");
49
+ parts.push("A conversationId was supplied, but conversation memory is only included for owner-scoped software-agent tasks in v1.");
50
+ }
51
+ const ownerMemory = readRequestString(request, "memorySummary");
52
+ if (ownerMemory && canIncludeOwnerMemory(opts.envelope.roleScope, opts.envelope.memoryScope)) {
53
+ if (roleExcludesOwnerMemory(rolePolicy)) {
54
+ parts.push("");
55
+ parts.push("[Role-excluded owner memory]");
56
+ parts.push(`The active role (${rolePolicy.roleName || "unknown"}) excludes ${rolePolicy.exclude.join(", ")}, so owner-provided memory was not included.`);
57
+ }
58
+ else {
59
+ parts.push("");
60
+ parts.push("[Owner-visible memory]");
61
+ parts.push(limitText(ownerMemory, Math.floor(budget * 0.35)));
62
+ }
63
+ }
64
+ else if (ownerMemory) {
65
+ parts.push("");
66
+ parts.push("[Excluded owner memory]");
67
+ parts.push("A memorySummary was supplied, but it was not included because this envelope is not owner/owner scoped.");
68
+ }
69
+ return limitText(parts.join("\n"), budget);
70
+ }
71
+ export function canIncludeOwnerMemory(roleScope, memoryScope) {
72
+ return roleScope === "owner" && memoryScope === "owner";
73
+ }
74
+ function canIncludeConversation(roleScope, memoryScope) {
75
+ return roleScope === "owner" && (memoryScope === "owner" || memoryScope === "task");
76
+ }
77
+ function triggerForRoleScope(roleScope) {
78
+ switch (roleScope) {
79
+ case "owner": return "trigger:chat:owner";
80
+ case "public": return "trigger:chat:public";
81
+ case "order": return "trigger:order";
82
+ case "agent": return "trigger:agent_call";
83
+ case "system": return "trigger:system";
84
+ }
85
+ }
86
+ function boundaryDescription(roleScope, memoryScope) {
87
+ if (roleScope === "owner" && memoryScope === "owner") {
88
+ return "Owner-scoped task: owner-visible memory may be included after Akemon-side selection.";
89
+ }
90
+ if (memoryScope === "none") {
91
+ return "No-memory task: do not use Akemon private memory, conversation history, or subjective state.";
92
+ }
93
+ return "Non-owner task: exclude owner private conversations, personal notes, bio state, diary, subjective impressions, and owner-only memory.";
94
+ }
95
+ async function resolveRoleMemoryPolicy(workdir, agentName, roleTrigger) {
96
+ const roles = await loadRoles(workdir, agentName);
97
+ const { primary } = resolveRoles(roles, roleTrigger);
98
+ return {
99
+ roleName: primary?.name || null,
100
+ exclude: primary?.exclude || [],
101
+ };
102
+ }
103
+ function roleExcludesOwnerMemory(policy) {
104
+ return policy.exclude.some((item) => {
105
+ const normalized = item.toLowerCase();
106
+ return normalized.includes("owner")
107
+ || normalized.includes("private")
108
+ || normalized.includes("personal")
109
+ || normalized.includes("note")
110
+ || normalized.includes("diary")
111
+ || normalized.includes("bio")
112
+ || normalized.includes("全部记忆")
113
+ || normalized.includes("个人")
114
+ || normalized.includes("笔记")
115
+ || normalized.includes("日记")
116
+ || normalized.includes("状态");
117
+ });
118
+ }
119
+ function normalizeRequest(value) {
120
+ if (value === undefined || value === null)
121
+ return {};
122
+ if (typeof value !== "object" || Array.isArray(value)) {
123
+ throw new Error("Invalid request: expected object");
124
+ }
125
+ return value;
126
+ }
127
+ function readRequestString(request, field) {
128
+ const value = request[field];
129
+ if (value === undefined || value === null)
130
+ return "";
131
+ if (typeof value !== "string")
132
+ throw new Error(`Invalid ${field}: expected string`);
133
+ return value.trim();
134
+ }
135
+ function limitText(text, maxChars) {
136
+ if (text.length <= maxChars)
137
+ return text;
138
+ const head = Math.floor(maxChars * 0.45);
139
+ const tail = Math.max(0, maxChars - head - 40);
140
+ return `${text.slice(0, head)}\n[truncated ${text.length - head - tail} chars]\n${text.slice(-tail)}`;
141
+ }
@@ -11,7 +11,9 @@
11
11
  * transport to app-server or a true persistent interactive session.
12
12
  */
13
13
  import { randomUUID } from "crypto";
14
- import { spawn } from "child_process";
14
+ import { spawn, spawnSync } from "child_process";
15
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
16
+ import { isAbsolute, join, relative, resolve as resolvePath } from "path";
15
17
  import { StringDecoder } from "string_decoder";
16
18
  import { SIG, sig } from "./types.js";
17
19
  import { sendTaskEnd, sendTaskStart, sendTaskStream } from "./relay-client.js";
@@ -30,6 +32,9 @@ const DEFAULT_OWNER_FORBIDDEN_ACTIONS = [
30
32
  const ROLE_SCOPES = ["owner", "public", "order", "agent", "system"];
31
33
  const MEMORY_SCOPES = ["none", "public", "task", "owner"];
32
34
  const RISK_LEVELS = ["low", "medium", "high"];
35
+ const MAX_STREAM_SUMMARY_CHARS = 12_000;
36
+ const STREAM_SUMMARY_HEAD_CHARS = 4_000;
37
+ const STREAM_SUMMARY_TAIL_CHARS = 8_000;
33
38
  export class CodexSoftwareAgentPeripheral {
34
39
  id;
35
40
  name;
@@ -39,6 +44,7 @@ export class CodexSoftwareAgentPeripheral {
39
44
  bus = null;
40
45
  activeChild = null;
41
46
  activeTaskId = null;
47
+ activeWorkdir = null;
42
48
  sessionId = randomUUID();
43
49
  constructor(config) {
44
50
  this.config = config;
@@ -73,15 +79,21 @@ export class CodexSoftwareAgentPeripheral {
73
79
  }
74
80
  this.activeChild = null;
75
81
  this.activeTaskId = null;
82
+ this.activeWorkdir = null;
76
83
  this.sessionId = randomUUID();
77
84
  }
78
85
  getState() {
86
+ const currentWorkdir = this.activeWorkdir || resolvePath(this.config.workdir);
79
87
  return {
80
88
  id: this.id,
81
89
  sessionId: this.sessionId,
82
90
  activeTaskId: this.activeTaskId,
91
+ activeWorkdir: this.activeWorkdir,
83
92
  busy: !!this.activeChild,
84
93
  transport: "codex-exec",
94
+ baseWorkdir: resolvePath(this.config.workdir),
95
+ workdirStatus: this.collectWorkdirStatus(currentWorkdir),
96
+ taskLedgerDir: this.config.taskLedgerDir,
85
97
  };
86
98
  }
87
99
  async send(signal) {
@@ -97,13 +109,16 @@ export class CodexSoftwareAgentPeripheral {
97
109
  const result = await this.sendTask(envelope);
98
110
  return sig(SIG.SOFTWARE_AGENT_RESPONSE, { ...result }, this.id);
99
111
  }
100
- async sendTask(envelope, signal) {
112
+ async sendTask(envelope, taskOptions) {
101
113
  if (this.activeChild) {
102
114
  throw new Error(`Software agent busy (task=${this.activeTaskId})`);
103
115
  }
116
+ const { signal, observer } = normalizeSoftwareAgentTaskOptions(taskOptions);
104
117
  const taskId = envelope.taskId || `sw_${Date.now()}_${randomUUID().slice(0, 8)}`;
105
- const workdir = envelope.workdir || this.config.workdir;
106
- const prompt = buildTaskEnvelopePrompt({ ...envelope, taskId, workdir });
118
+ const workdirSafety = resolveWorkdirSafety(this.config.workdir, envelope.workdir || this.config.workdir, envelope.workdirSafety?.allowOutsideWorkdir || false);
119
+ const workdir = workdirSafety.effectiveWorkdir;
120
+ const effectiveEnvelope = { ...envelope, taskId, workdir, workdirSafety };
121
+ const prompt = buildTaskEnvelopePrompt(effectiveEnvelope);
107
122
  const { cmd, args } = buildCodexExecCommand({
108
123
  command: this.config.command || "codex",
109
124
  workdir,
@@ -111,17 +126,38 @@ export class CodexSoftwareAgentPeripheral {
111
126
  sandbox: this.config.sandbox || "workspace-write",
112
127
  });
113
128
  const startedAt = Date.now();
129
+ const startedAtIso = new Date(startedAt).toISOString();
114
130
  const relay = this.config.taskRelay || defaultTaskRelay;
115
131
  const commandLine = [cmd, ...args].join(" ");
116
132
  const spawnImpl = this.config.spawnImpl || spawn;
117
133
  const timeoutMs = envelope.timeoutMs || this.config.defaultTimeoutMs || DEFAULT_TIMEOUT_MS;
134
+ const workdirStatus = this.collectWorkdirStatus(workdir);
135
+ const baseTaskRecord = () => ({
136
+ schemaVersion: 1,
137
+ taskId,
138
+ agentId: this.id,
139
+ sessionId: this.sessionId,
140
+ transport: "codex-exec",
141
+ commandLine,
142
+ envelope: effectiveEnvelope,
143
+ startedAt: startedAtIso,
144
+ workdirStatus,
145
+ });
118
146
  return new Promise((resolve) => {
119
147
  this.activeTaskId = taskId;
120
- relay.sendTaskStart(taskId, "software_agent", commandLine);
148
+ this.activeWorkdir = workdir;
149
+ this.writeTaskRecord({
150
+ ...baseTaskRecord(),
151
+ status: "running",
152
+ updatedAt: startedAtIso,
153
+ });
154
+ const origin = "software_agent";
155
+ relay.sendTaskStart(taskId, origin, commandLine);
156
+ observer?.onStart?.({ taskId, origin, commandLine });
121
157
  this.bus?.emit(SIG.TASK_STARTED, sig(SIG.TASK_STARTED, {
122
158
  taskId,
123
159
  taskType: "software_agent",
124
- description: envelope.goal,
160
+ description: effectiveEnvelope.goal,
125
161
  peripheral: this.id,
126
162
  sessionId: this.sessionId,
127
163
  }, this.id));
@@ -137,8 +173,8 @@ export class CodexSoftwareAgentPeripheral {
137
173
  catch (err) {
138
174
  this.activeChild = null;
139
175
  this.activeTaskId = null;
176
+ this.activeWorkdir = null;
140
177
  const durationMs = Date.now() - startedAt;
141
- relay.sendTaskEnd(taskId, null, durationMs);
142
178
  const result = {
143
179
  success: false,
144
180
  taskId,
@@ -147,6 +183,19 @@ export class CodexSoftwareAgentPeripheral {
147
183
  exitCode: null,
148
184
  durationMs,
149
185
  };
186
+ relay.sendTaskEnd(taskId, null, durationMs);
187
+ observer?.onEnd?.({ taskId, exitCode: null, durationMs, result });
188
+ const completedAt = new Date().toISOString();
189
+ this.writeTaskRecord({
190
+ ...baseTaskRecord(),
191
+ status: "failed",
192
+ updatedAt: completedAt,
193
+ completedAt,
194
+ durationMs,
195
+ result,
196
+ stdoutSummary: summarizeText(""),
197
+ stderrSummary: summarizeText(result.error || ""),
198
+ });
150
199
  this.bus?.emit(SIG.TASK_FAILED, sig(SIG.TASK_FAILED, result, this.id));
151
200
  resolve(result);
152
201
  return;
@@ -169,15 +218,17 @@ export class CodexSoftwareAgentPeripheral {
169
218
  if (tailOut) {
170
219
  stdout += tailOut;
171
220
  relay.sendTaskStream(taskId, "stdout", tailOut);
221
+ observer?.onStream?.({ taskId, stream: "stdout", chunk: tailOut });
172
222
  }
173
223
  if (tailErr) {
174
224
  stderr += tailErr;
175
225
  relay.sendTaskStream(taskId, "stderr", tailErr);
226
+ observer?.onStream?.({ taskId, stream: "stderr", chunk: tailErr });
176
227
  }
177
228
  const durationMs = Date.now() - startedAt;
178
- relay.sendTaskEnd(taskId, exitCode, durationMs);
179
229
  this.activeChild = null;
180
230
  this.activeTaskId = null;
231
+ this.activeWorkdir = null;
181
232
  const output = stdout.trim() || stderr.trim();
182
233
  const success = !error && !aborted && exitCode === 0;
183
234
  const result = {
@@ -188,6 +239,19 @@ export class CodexSoftwareAgentPeripheral {
188
239
  exitCode,
189
240
  durationMs,
190
241
  };
242
+ relay.sendTaskEnd(taskId, exitCode, durationMs);
243
+ observer?.onEnd?.({ taskId, exitCode, durationMs, result });
244
+ const completedAt = new Date().toISOString();
245
+ this.writeTaskRecord({
246
+ ...baseTaskRecord(),
247
+ status: success ? "completed" : "failed",
248
+ updatedAt: completedAt,
249
+ completedAt,
250
+ durationMs,
251
+ result,
252
+ stdoutSummary: summarizeText(stdout),
253
+ stderrSummary: summarizeText(stderr),
254
+ });
191
255
  this.bus?.emit(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, sig(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, {
192
256
  ...result,
193
257
  taskLabel: `software_agent:${this.id}`,
@@ -239,6 +303,7 @@ export class CodexSoftwareAgentPeripheral {
239
303
  return;
240
304
  stdout += text;
241
305
  relay.sendTaskStream(taskId, "stdout", text);
306
+ observer?.onStream?.({ taskId, stream: "stdout", chunk: text });
242
307
  });
243
308
  child.stderr?.on("data", (chunk) => {
244
309
  const text = errDecoder.write(chunk);
@@ -246,6 +311,7 @@ export class CodexSoftwareAgentPeripheral {
246
311
  return;
247
312
  stderr += text;
248
313
  relay.sendTaskStream(taskId, "stderr", text);
314
+ observer?.onStream?.({ taskId, stream: "stderr", chunk: text });
249
315
  });
250
316
  child.on("close", (code) => {
251
317
  child.unref();
@@ -257,6 +323,48 @@ export class CodexSoftwareAgentPeripheral {
257
323
  });
258
324
  });
259
325
  }
326
+ writeTaskRecord(record) {
327
+ const dir = this.config.taskLedgerDir;
328
+ if (!dir)
329
+ return;
330
+ try {
331
+ mkdirSync(dir, { recursive: true });
332
+ const safeTaskId = safeTaskFilename(record.taskId);
333
+ writeFileSync(join(dir, `${safeTaskId}.json`), `${JSON.stringify(record, null, 2)}\n`);
334
+ }
335
+ catch (err) {
336
+ console.error(`[software-agent] Failed to write task ledger: ${err.message || String(err)}`);
337
+ }
338
+ }
339
+ collectWorkdirStatus(workdir) {
340
+ const impl = this.config.gitStatusImpl || readGitWorktreeStatus;
341
+ return impl(workdir);
342
+ }
343
+ }
344
+ export function summarizeText(text, maxChars = MAX_STREAM_SUMMARY_CHARS) {
345
+ const normalized = text || "";
346
+ const chars = normalized.length;
347
+ const bytes = Buffer.byteLength(normalized, "utf8");
348
+ const lines = normalized ? normalized.split(/\r\n|\r|\n/).length : 0;
349
+ if (chars <= maxChars) {
350
+ return { chars, bytes, lines, text: normalized, truncated: false };
351
+ }
352
+ const headChars = Math.min(STREAM_SUMMARY_HEAD_CHARS, maxChars);
353
+ const tailChars = Math.max(0, maxChars - headChars);
354
+ const omittedChars = chars - headChars - tailChars;
355
+ const tailText = tailChars > 0 ? normalized.slice(-tailChars) : "";
356
+ return {
357
+ chars,
358
+ bytes,
359
+ lines,
360
+ text: [
361
+ normalized.slice(0, headChars),
362
+ `[truncated ${omittedChars} chars]`,
363
+ tailText,
364
+ ].join("\n"),
365
+ truncated: true,
366
+ omittedChars,
367
+ };
260
368
  }
261
369
  export function buildTaskEnvelopePrompt(envelope) {
262
370
  const lines = [
@@ -269,11 +377,15 @@ export function buildTaskEnvelopePrompt(envelope) {
269
377
  `Memory scope: ${envelope.memoryScope}`,
270
378
  `Risk level: ${envelope.riskLevel}`,
271
379
  `Workdir: ${envelope.workdir}`,
272
- "",
273
- "Goal:",
274
- envelope.goal,
275
- "",
276
380
  ];
381
+ if (envelope.workdirSafety) {
382
+ lines.push(`Base workdir: ${envelope.workdirSafety.baseWorkdir}`);
383
+ lines.push(`Requested workdir: ${envelope.workdirSafety.requestedWorkdir}`);
384
+ lines.push(`Effective workdir: ${envelope.workdirSafety.effectiveWorkdir}`);
385
+ lines.push(`Outside base workdir: ${envelope.workdirSafety.outsideBaseWorkdir ? "yes" : "no"}`);
386
+ lines.push(`Outside workdir explicitly allowed: ${envelope.workdirSafety.allowOutsideWorkdir ? "yes" : "no"}`);
387
+ }
388
+ lines.push("", "Goal:", envelope.goal, "");
277
389
  if (envelope.memorySummary?.trim()) {
278
390
  lines.push("Visible Akemon memory/context:");
279
391
  lines.push(envelope.memorySummary.trim());
@@ -307,27 +419,123 @@ export function createOwnerTaskEnvelope(body, defaultWorkdir) {
307
419
  const goal = typeof body?.goal === "string" ? body.goal.trim() : "";
308
420
  if (!goal)
309
421
  throw new Error("Missing required string field: goal");
310
- const callerForbiddenActions = readOptionalStringArray(body.forbiddenActions, "forbiddenActions");
422
+ const callerForbiddenActions = readOptionalStringArray(body?.forbiddenActions, "forbiddenActions");
423
+ const requestedWorkdir = readOptionalString(body?.workdir, "workdir") || defaultWorkdir;
424
+ const workdirSafety = resolveWorkdirSafety(defaultWorkdir, requestedWorkdir, readOptionalBoolean(body?.allowOutsideWorkdir, "allowOutsideWorkdir") || false);
311
425
  return {
312
- taskId: readOptionalString(body.taskId, "taskId"),
426
+ taskId: readOptionalString(body?.taskId, "taskId"),
313
427
  sourceModule: "owner-http",
314
- purpose: readOptionalString(body.purpose, "purpose") || "owner software-agent task",
428
+ purpose: readOptionalString(body?.purpose, "purpose") || "owner software-agent task",
315
429
  goal,
316
- workdir: readOptionalString(body.workdir, "workdir") || defaultWorkdir,
317
- roleScope: readEnum(body.roleScope, "roleScope", ROLE_SCOPES, "owner"),
318
- memoryScope: readEnum(body.memoryScope, "memoryScope", MEMORY_SCOPES, "owner"),
319
- riskLevel: readEnum(body.riskLevel, "riskLevel", RISK_LEVELS, "medium"),
320
- allowedActions: body.allowedActions !== undefined
321
- ? readOptionalStringArray(body.allowedActions, "allowedActions")
430
+ workdir: workdirSafety.effectiveWorkdir,
431
+ workdirSafety,
432
+ roleScope: readEnum(body?.roleScope, "roleScope", ROLE_SCOPES, "owner"),
433
+ memoryScope: readEnum(body?.memoryScope, "memoryScope", MEMORY_SCOPES, "owner"),
434
+ riskLevel: readEnum(body?.riskLevel, "riskLevel", RISK_LEVELS, "medium"),
435
+ allowedActions: body?.allowedActions !== undefined
436
+ ? readOptionalStringArray(body?.allowedActions, "allowedActions")
322
437
  : [...DEFAULT_OWNER_ALLOWED_ACTIONS],
323
438
  forbiddenActions: [...new Set([...DEFAULT_OWNER_FORBIDDEN_ACTIONS, ...callerForbiddenActions])],
324
- memorySummary: typeof body.memorySummary === "string" ? body.memorySummary : "",
325
- deliverable: typeof body.deliverable === "string"
439
+ memorySummary: typeof body?.memorySummary === "string" ? body.memorySummary : "",
440
+ deliverable: typeof body?.deliverable === "string"
326
441
  ? body.deliverable
327
442
  : "Return a concise engineering summary with changes, verification, and remaining risks.",
328
- timeoutMs: readTimeoutMs(body.timeoutMs),
443
+ timeoutMs: readTimeoutMs(body?.timeoutMs),
329
444
  };
330
445
  }
446
+ export function resolveWorkdirSafety(baseWorkdir, requestedWorkdir, allowOutsideWorkdir = false) {
447
+ const base = resolvePath(baseWorkdir);
448
+ const requested = isAbsolute(requestedWorkdir)
449
+ ? resolvePath(requestedWorkdir)
450
+ : resolvePath(base, requestedWorkdir);
451
+ const rel = relative(base, requested);
452
+ const outsideBaseWorkdir = !!rel && (rel.startsWith("..") || isAbsolute(rel));
453
+ if (outsideBaseWorkdir && !allowOutsideWorkdir) {
454
+ throw new Error(`Invalid workdir: ${requested} is outside base workdir ${base}`);
455
+ }
456
+ return {
457
+ baseWorkdir: base,
458
+ requestedWorkdir,
459
+ effectiveWorkdir: requested,
460
+ allowOutsideWorkdir,
461
+ outsideBaseWorkdir,
462
+ };
463
+ }
464
+ export function readGitWorktreeStatus(workdir) {
465
+ const resolvedWorkdir = resolvePath(workdir);
466
+ try {
467
+ const rootResult = spawnSync("git", ["-C", resolvedWorkdir, "rev-parse", "--show-toplevel"], {
468
+ encoding: "utf8",
469
+ timeout: 5000,
470
+ });
471
+ if (rootResult.status !== 0) {
472
+ return {
473
+ workdir: resolvedWorkdir,
474
+ isRepo: false,
475
+ dirty: false,
476
+ changedFiles: [],
477
+ error: summarizeGitError(rootResult.stderr, rootResult.error),
478
+ };
479
+ }
480
+ const root = String(rootResult.stdout || "").trim();
481
+ const statusResult = spawnSync("git", ["-C", resolvedWorkdir, "status", "--short"], {
482
+ encoding: "utf8",
483
+ timeout: 5000,
484
+ });
485
+ if (statusResult.status !== 0) {
486
+ return {
487
+ workdir: resolvedWorkdir,
488
+ isRepo: true,
489
+ dirty: false,
490
+ changedFiles: [],
491
+ root,
492
+ error: summarizeGitError(statusResult.stderr, statusResult.error),
493
+ };
494
+ }
495
+ const changedFiles = String(statusResult.stdout || "")
496
+ .split(/\r?\n/)
497
+ .map((line) => line.trimEnd())
498
+ .filter(Boolean)
499
+ .map((line) => line.slice(3).trim())
500
+ .filter(Boolean);
501
+ return {
502
+ workdir: resolvedWorkdir,
503
+ isRepo: true,
504
+ dirty: changedFiles.length > 0,
505
+ changedFiles,
506
+ root,
507
+ };
508
+ }
509
+ catch (err) {
510
+ return {
511
+ workdir: resolvedWorkdir,
512
+ isRepo: false,
513
+ dirty: false,
514
+ changedFiles: [],
515
+ error: err.message || String(err),
516
+ };
517
+ }
518
+ }
519
+ export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
520
+ const safeLimit = normalizeTaskRecordLimit(limit);
521
+ try {
522
+ if (!existsSync(taskLedgerDir))
523
+ return [];
524
+ return readdirSync(taskLedgerDir, { withFileTypes: true })
525
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
526
+ .map((entry) => readSoftwareAgentTaskRecordFile(join(taskLedgerDir, entry.name)))
527
+ .filter((record) => !!record)
528
+ .sort(compareSoftwareAgentTaskRecords)
529
+ .slice(0, safeLimit);
530
+ }
531
+ catch {
532
+ return [];
533
+ }
534
+ }
535
+ export function readSoftwareAgentTaskRecord(taskLedgerDir, taskId) {
536
+ const file = join(taskLedgerDir, `${safeTaskFilename(taskId)}.json`);
537
+ return readSoftwareAgentTaskRecordFile(file);
538
+ }
331
539
  function readOptionalString(value, field) {
332
540
  if (value === undefined || value === null)
333
541
  return undefined;
@@ -336,6 +544,19 @@ function readOptionalString(value, field) {
336
544
  const trimmed = value.trim();
337
545
  return trimmed || undefined;
338
546
  }
547
+ function normalizeSoftwareAgentTaskOptions(options) {
548
+ if (!options)
549
+ return {};
550
+ if (isAbortSignal(options)) {
551
+ return { signal: options };
552
+ }
553
+ return options;
554
+ }
555
+ function isAbortSignal(value) {
556
+ return !!value
557
+ && typeof value.aborted === "boolean"
558
+ && typeof value.addEventListener === "function";
559
+ }
339
560
  function readOptionalStringArray(value, field) {
340
561
  if (value === undefined || value === null)
341
562
  return [];
@@ -348,6 +569,13 @@ function readOptionalStringArray(value, field) {
348
569
  return item.trim();
349
570
  });
350
571
  }
572
+ function readOptionalBoolean(value, field) {
573
+ if (value === undefined || value === null)
574
+ return undefined;
575
+ if (typeof value !== "boolean")
576
+ throw new Error(`Invalid ${field}: expected boolean`);
577
+ return value;
578
+ }
351
579
  function readEnum(value, field, allowed, fallback) {
352
580
  if (value === undefined || value === null || value === "")
353
581
  return fallback;
@@ -367,6 +595,51 @@ function readTimeoutMs(value) {
367
595
  }
368
596
  return value;
369
597
  }
598
+ function safeTaskFilename(taskId) {
599
+ const safe = taskId.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 200);
600
+ return safe || "task";
601
+ }
602
+ function summarizeGitError(stderr, error) {
603
+ if (error)
604
+ return error.message;
605
+ const text = typeof stderr === "string" ? stderr.trim() : "";
606
+ return text || undefined;
607
+ }
608
+ function normalizeTaskRecordLimit(limit) {
609
+ if (!Number.isInteger(limit) || limit <= 0)
610
+ return 20;
611
+ return Math.min(limit, 100);
612
+ }
613
+ function readSoftwareAgentTaskRecordFile(file) {
614
+ try {
615
+ if (!existsSync(file))
616
+ return null;
617
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
618
+ if (!isSoftwareAgentTaskRecord(parsed))
619
+ return null;
620
+ return parsed;
621
+ }
622
+ catch {
623
+ return null;
624
+ }
625
+ }
626
+ function isSoftwareAgentTaskRecord(value) {
627
+ return value
628
+ && value.schemaVersion === 1
629
+ && typeof value.taskId === "string"
630
+ && (value.status === "running" || value.status === "completed" || value.status === "failed")
631
+ && typeof value.startedAt === "string"
632
+ && typeof value.updatedAt === "string"
633
+ && value.envelope
634
+ && typeof value.envelope.goal === "string";
635
+ }
636
+ function compareSoftwareAgentTaskRecords(a, b) {
637
+ const bTime = Date.parse(b.updatedAt || b.startedAt) || 0;
638
+ const aTime = Date.parse(a.updatedAt || a.startedAt) || 0;
639
+ if (bTime !== aTime)
640
+ return bTime - aTime;
641
+ return b.taskId.localeCompare(a.taskId);
642
+ }
370
643
  function buildCodexExecCommand(opts) {
371
644
  const args = [
372
645
  "exec",
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/lhead/akemon"
9
+ "url": "git+https://github.com/lhead/akemon.git"
10
10
  },
11
11
  "keywords": [
12
12
  "ai",
@@ -18,7 +18,7 @@
18
18
  "gemini"
19
19
  ],
20
20
  "bin": {
21
- "akemon": "./dist/cli.js"
21
+ "akemon": "dist/cli.js"
22
22
  },
23
23
  "files": [
24
24
  "dist",
@@ -26,7 +26,7 @@
26
26
  "README.md"
27
27
  ],
28
28
  "scripts": {
29
- "build": "tsc && cp src/live.html dist/live.html",
29
+ "build": "rm -rf dist && tsc && cp src/live.html dist/live.html",
30
30
  "dev": "tsc --watch",
31
31
  "start": "node dist/cli.js",
32
32
  "postinstall": "node scripts/fix-node-pty.cjs",