feinai 0.8.3 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
+ ## [0.8.4] - 2026-09-02
7
+
8
+ ### Fixed
9
+
10
+ - **Detección de agentes activos no reflejaba agentes reales.** El matching en 0.8.3 seguía dependiendo de encontrar el substring `TASK-...` en la línea de comando del proceso — pero sesiones reales (`claude`, `opencode` corriendo cd'd al worktree de la tarea) casi nunca tienen el task id en su `argv`, así que nunca matcheaban. Además `task.worktree` se guarda relativo al root del proyecto (`.worktrees/TASK-XXX`) y se comparaba tal cual contra rutas absolutas de `/proc/:pid/cwd`, que tampoco matcheaban nunca. `listAgentProcesses` ahora usa el cwd del proceso como señal primaria: escanea `/proc/*/cwd` de todos los procesos y los compara contra el worktree absoluto de cada tarea `in_progress` (resuelto en `server.ts` con `resolve(process.cwd(), task.worktree)`). El grep por `TASK-...` en el comando queda sólo como fallback de baja confianza (`verified: false`) para tareas sin worktree.
11
+
6
12
  ## [0.8.3] - 2026-09-02
7
13
 
8
14
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feinai",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Task & spec manager for AI agents — parallel worktrees, live dashboard, SDD skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- import { readlink } from "node:fs/promises";
1
+ import { readdir, readlink } from "node:fs/promises";
2
2
 
3
3
  export interface AgentProcess {
4
4
  pid: number;
@@ -12,68 +12,97 @@ export interface AgentProcess {
12
12
 
13
13
  export interface ExpectedAgent {
14
14
  taskId: string;
15
+ /** absolute path to the task's worktree, or null if it has none */
15
16
  worktree: string | null;
16
17
  }
17
18
 
18
- /**
19
- * Snapshot of processes that look like they're working on active tasks.
20
- *
21
- * Matching is done two ways:
22
- * 1. By worktree cwd (`/proc/:pid/cwd`) against each expected task's worktree
23
- * path — this is the precise signal and sets `verified: true`.
24
- * 2. By a `TASK-...` substring in the command line — kept as a fallback so we
25
- * don't lose visibility into agents whose cwd we can't resolve (e.g. the
26
- * process already exited, or /proc isn't readable), but reported as
27
- * `verified: false` since a substring match can be coincidental or belong
28
- * to an unrelated project using the same task ID scheme.
29
- */
30
- export async function listAgentProcesses(
31
- expected: ExpectedAgent[] = [],
32
- ): Promise<AgentProcess[]> {
33
- let lines: string[];
19
+ interface PsRow {
20
+ pid: number;
21
+ cpu: number;
22
+ mem: number;
23
+ command: string;
24
+ }
25
+
26
+ async function psSnapshot(): Promise<PsRow[]> {
34
27
  try {
35
28
  const result = await Bun.$`ps -eo pid,pcpu,pmem,command`.text();
36
- lines = result.trim().split("\n").slice(1).filter(Boolean);
29
+ return result
30
+ .trim()
31
+ .split("\n")
32
+ .slice(1)
33
+ .map((line) => {
34
+ const parts = line.trim().split(/\s+/);
35
+ return {
36
+ pid: parseInt(parts[0]!, 10),
37
+ cpu: parseFloat(parts[1]!),
38
+ mem: parseFloat(parts[2]!),
39
+ command: parts.slice(3).join(" "),
40
+ };
41
+ })
42
+ .filter((p) => Number.isFinite(p.pid));
37
43
  } catch {
38
44
  return [];
39
45
  }
46
+ }
40
47
 
41
- const worktreeByTask = new Map(
42
- expected
43
- .filter((e): e is ExpectedAgent & { worktree: string } => !!e.worktree)
44
- .map((e) => [e.taskId, e.worktree]),
45
- );
48
+ /**
49
+ * Snapshot of processes working on active tasks.
50
+ *
51
+ * Real agent sessions here (an interactive `claude` or `opencode` process cd'd
52
+ * into a task's git worktree) rarely put the task ID anywhere in their
53
+ * command line — so grepping ps output for `TASK-...` misses them entirely.
54
+ * The reliable signal is the process's current working directory: for each
55
+ * in_progress task with a worktree, we check every process's
56
+ * `/proc/:pid/cwd` against that worktree path. A cwd match is `verified: true`.
57
+ *
58
+ * As a fallback (e.g. a task has no worktree yet, or /proc isn't readable),
59
+ * we also look for a literal `TASK-...` substring in the command line —
60
+ * kept as `verified: false` since it's coincidental at best.
61
+ */
62
+ export async function listAgentProcesses(
63
+ expected: ExpectedAgent[] = [],
64
+ ): Promise<AgentProcess[]> {
65
+ const rows = await psSnapshot();
66
+ if (!rows.length) return [];
46
67
 
47
- const candidates: Omit<AgentProcess, "verified">[] = [];
48
- for (const line of lines) {
49
- const parts = line.trim().split(/\s+/);
50
- if (parts.length < 4) continue;
51
- const pid = parseInt(parts[0]!, 10);
52
- const cpu = parseFloat(parts[1]!);
53
- const mem = parseFloat(parts[2]!);
54
- const command = parts.slice(3).join(" ");
55
- if (!Number.isFinite(pid)) continue;
56
- const taskMatch = command.match(/TASK-[A-Z0-9-]+/);
57
- const taskId = taskMatch ? taskMatch[0] : null;
58
- if (!taskId) continue;
59
- candidates.push({ pid, cpu, mem, command, taskId });
60
- }
68
+ const pids = await readdir("/proc").catch(() => [] as string[]);
69
+ const numericPids = new Set(pids.filter((p) => /^\d+$/.test(p)).map(Number));
61
70
 
62
- const results = await Promise.all(
63
- candidates.map(async (c) => {
64
- const worktree = c.taskId ? worktreeByTask.get(c.taskId) : undefined;
65
- let verified = false;
66
- if (worktree) {
71
+ const cwdByPid = new Map<number, string>();
72
+ await Promise.all(
73
+ rows
74
+ .filter((r) => numericPids.has(r.pid))
75
+ .map(async (r) => {
67
76
  try {
68
- const cwd = await readlink(`/proc/${c.pid}/cwd`);
69
- verified = cwd === worktree || cwd.startsWith(worktree + "/");
77
+ cwdByPid.set(r.pid, await readlink(`/proc/${r.pid}/cwd`));
70
78
  } catch {
71
- verified = false;
79
+ // process exited or unreadable — no cwd signal for it
72
80
  }
73
- }
74
- return { ...c, verified };
75
- }),
81
+ }),
76
82
  );
77
83
 
84
+ const results: AgentProcess[] = [];
85
+ const matchedPids = new Set<number>();
86
+
87
+ for (const { taskId, worktree } of expected) {
88
+ if (!worktree) continue;
89
+ for (const row of rows) {
90
+ if (matchedPids.has(row.pid)) continue;
91
+ const cwd = cwdByPid.get(row.pid);
92
+ if (!cwd) continue;
93
+ if (cwd === worktree || cwd.startsWith(worktree + "/")) {
94
+ matchedPids.add(row.pid);
95
+ results.push({ ...row, taskId, verified: true });
96
+ }
97
+ }
98
+ }
99
+
100
+ for (const row of rows) {
101
+ if (matchedPids.has(row.pid)) continue;
102
+ const taskMatch = row.command.match(/TASK-[A-Z0-9-]+/);
103
+ if (!taskMatch) continue;
104
+ results.push({ ...row, taskId: taskMatch[0], verified: false });
105
+ }
106
+
78
107
  return results;
79
108
  }
package/src/cli.ts CHANGED
@@ -57,7 +57,7 @@ import {
57
57
  type OrphanKillResult,
58
58
  } from "./server-state";
59
59
 
60
- const VERSION = "0.8.3";
60
+ const VERSION = "0.8.4";
61
61
 
62
62
  interface ParsedArgs {
63
63
  positional: string[];
package/src/server.ts CHANGED
@@ -441,10 +441,16 @@ export function startServer(opts: ServerOptions): { url: string; stop: () => voi
441
441
  return json(status);
442
442
  }
443
443
 
444
- // GET /api/agents — running opencode processes with task matching
444
+ // GET /api/agents — running agent processes with task matching
445
445
  if (path === "/api/agents" && method === "GET") {
446
446
  const inProgress = listTasks(db, { status: "in_progress" });
447
- const expected = inProgress.map((t) => ({ taskId: t.id, worktree: t.worktree }));
447
+ // task.worktree is stored relative to the project root the server
448
+ // was launched from — resolve it so it can be compared against the
449
+ // absolute paths /proc/:pid/cwd returns.
450
+ const expected = inProgress.map((t) => ({
451
+ taskId: t.id,
452
+ worktree: t.worktree ? resolve(process.cwd(), t.worktree) : null,
453
+ }));
448
454
  const agents = await listAgentProcesses(expected);
449
455
  return json(agents);
450
456
  }