feinai 0.7.1 → 0.8.0

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,31 @@
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.0] - 2026-09-02
7
+
8
+ ### Changed
9
+
10
+ - **Dashboard rediseñado: el detalle de spec/task pasa de modal overlay a páginas navegables.** Rutas por hash, linkeables y compartibles: `#/`, `#/specs`, `#/tasks`, `#/spec/:id`, `#/task/:id`, `#/new/spec`, `#/new/task`. El botón atrás del navegador funciona.
11
+ - **Eliminados los scrolls anidados.** El documento es el único contenedor con scroll: se quitaron los `max-height` de `.list` (600px), `.markdown` (400px), `.modal` (90vh), la lista de archivos del worktree (120px) y los payloads de eventos (120px). Sobrevive sólo el dropdown de búsqueda (es un menú) y el scroll horizontal de bloques de código y tablas.
12
+ - Breadcrumbs jerárquicos (`feinai / Specs / SPEC-012 / TASK-012-B`) con el crumb actual teñido por el estado real de la entidad.
13
+ - Los filtros viven en la URL (`#/tasks?status=pending`); los stats del header linkean a su lista filtrada.
14
+ - `prompt()` / `confirm()` nativos reemplazados por un diálogo dentro de la página.
15
+
16
+ ### Added
17
+
18
+ - Soporte de tablas markdown en el renderer del dashboard (antes se veían como texto crudo con pipes).
19
+ - Los IDs en `blocked_by` son links a la task correspondiente.
20
+
21
+ ### Fixed
22
+
23
+ - `feinai server --down` sólo detiene el servidor registrado de este proyecto, no cualquier proceso en el puerto.
24
+ - Lifecycle de specs huérfanas (SPEC-124) y normalización de estados a inglés.
25
+
26
+ ## [0.7.2] - 2026-06-22
27
+
28
+ ### Fixed
29
+ - `isPortInUse` / `pidsOnPort` / `portOfPid` usaban `result.exitCode` en vez de `result.status` (API de `child_process.spawnSync`), causando que `findFreePort` nunca detectara puertos ocupados
30
+
6
31
  ## [0.7.1] - 2026-06-22
7
32
 
8
33
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feinai",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Task & spec manager for AI agents — parallel worktrees, live dashboard, SDD skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -64,14 +64,30 @@ Run the gates defined in the task (`quality_gates`). If the task does not specif
64
64
 
65
65
  Gates pass:
66
66
  ```bash
67
- # From the worktree:
67
+ # From the worktree — include feinai.db in the task commit:
68
+ feinai done <TASK-ID> --result "gates ✓"
69
+ feinai git add .feinai/feinai.db
70
+ feinai git commit --amend --no-edit
71
+
68
72
  feinai git push origin HEAD:main
73
+ ```
69
74
 
75
+ If push is rejected (another agent pushed first — conflict on `.feinai/feinai.db`):
76
+ ```bash
77
+ # Accept origin's db (it's the authoritative state), re-apply done, re-push:
78
+ feinai git fetch origin main
79
+ feinai git checkout origin/main -- .feinai/feinai.db
80
+ feinai done <TASK-ID> --result "gates ✓"
81
+ feinai git add .feinai/feinai.db
82
+ feinai git commit --amend --no-edit
83
+ feinai git push origin HEAD:main
84
+ ```
85
+
86
+ Push succeeds:
87
+ ```bash
70
88
  # From the repo root:
71
89
  feinai git worktree remove .worktrees/<TASK-ID>
72
90
  feinai git complete
73
-
74
- feinai done <TASK-ID> --result "gates ✓"
75
91
  ```
76
92
 
77
93
  Gates fail → follow "If something fails".
package/src/cli.ts CHANGED
@@ -53,9 +53,11 @@ import {
53
53
  clearServerState,
54
54
  findFreePort,
55
55
  repairServerState,
56
+ killOrphansByProjectDir,
57
+ type OrphanKillResult,
56
58
  } from "./server-state";
57
59
 
58
- const VERSION = "0.7.1";
60
+ const VERSION = "0.8.0";
59
61
 
60
62
  interface ParsedArgs {
61
63
  positional: string[];
@@ -77,6 +79,7 @@ const FLAG_NAMES = new Set([
77
79
  "daemon",
78
80
  "yes",
79
81
  "clear-blocked-by",
82
+ "no-preflight",
80
83
  ]);
81
84
  const MULTI_OPTIONS = new Set(["package", "gate", "blocked-by"]);
82
85
 
@@ -257,7 +260,7 @@ SERVER:
257
260
  --port <N> Port (default: 8272 — TASC on phone keypad)
258
261
  --host <addr> Bind host (default: 127.0.0.1)
259
262
  server --daemon / -d Start detached (no job control noise)
260
- server --down Stop the running feinai server (by port)
263
+ server --down Stop this project's recorded feinai server
261
264
 
262
265
  GLOBAL FLAGS:
263
266
  --json Output as JSON
@@ -777,61 +780,48 @@ function cmdSpec(rest: string[], args: ParsedArgs, format: OutputFormat): void {
777
780
  }
778
781
 
779
782
  async function cmdServer(args: ParsedArgs): Promise<void> {
780
- const port = Number(args.options.port ?? "8272");
781
-
782
- // --down: stop the project's recorded server, fallback to default port
783
+ // --down: stop this project's recorded server AND any orphan servers that
784
+ // share the project directory, so stale/untracked processes are cleaned up.
783
785
  if (args.flags["down"]) {
784
- const db = findDbPath() ? openDb() : null;
785
- let targetPort = port;
786
- let targetPids: number[] = [];
787
-
788
- if (db) {
789
- // Try recorded server first
790
- repairServerState(db);
791
- const record = readServerState(db);
792
- if (record) {
793
- targetPort = record.port;
794
- targetPids = [record.pid];
795
- }
796
- }
797
-
798
- if (Number.isNaN(targetPort) || targetPort < 1 || targetPort > 65535) {
799
- console.error("Error: --port must be a valid port number");
800
- if (db) db.close();
801
- process.exit(1);
802
- }
803
-
804
- // If no recorded pids, fall back to lsof probe on target port
805
- if (targetPids.length === 0) {
806
- const result = Bun.spawnSync(["lsof", "-ti", `tcp:${targetPort}`]);
807
- targetPids = new TextDecoder()
808
- .decode(result.stdout)
809
- .trim()
810
- .split("\n")
811
- .filter(Boolean)
812
- .map((s) => Number(s))
813
- .filter((n) => !isNaN(n));
786
+ if (!findDbPath()) {
787
+ console.error(
788
+ "Error: no .feinai/feinai.db found. Cannot stop server without project context.",
789
+ );
790
+ process.exit(2);
814
791
  }
815
792
 
816
- if (targetPids.length === 0) {
817
- console.log(`No process found listening on port ${targetPort}.`);
818
- if (db) db.close();
819
- return;
820
- }
793
+ const dbPath = findDbPath()!;
794
+ const db = openDb();
795
+ repairServerState(db);
796
+ const record = readServerState(db);
797
+ const killed: OrphanKillResult[] = [];
821
798
 
822
- for (const pid of targetPids) {
799
+ if (record) {
823
800
  try {
824
- process.kill(Number(pid), "SIGTERM");
825
- console.log(`Stopped feinai server (PID ${pid}).`);
801
+ process.kill(record.pid, "SIGTERM");
802
+ killed.push({ pid: record.pid, port: record.port });
826
803
  } catch {
827
- console.error(`Failed to kill PID ${pid}.`);
804
+ console.error(
805
+ `Failed to stop feinai server (PID ${record.pid}, port ${record.port}).`,
806
+ );
828
807
  }
808
+ clearServerState(db);
829
809
  }
810
+ db.close();
830
811
 
831
- // Clear the row after stopping
832
- if (db) {
833
- clearServerState(db);
834
- db.close();
812
+ // Also kill any unregistered servers whose CWD matches this project.
813
+ // The DB lives at <projectDir>/.feinai/feinai.db, so go up two levels.
814
+ const projectDir = dirname(dirname(dbPath));
815
+ const orphans = killOrphansByProjectDir(projectDir);
816
+ killed.push(...orphans);
817
+
818
+ if (killed.length === 0) {
819
+ console.log("No running feinai server found for this project.");
820
+ } else {
821
+ const summary = killed
822
+ .map((k) => `PID ${k.pid}${k.port ? ` (port ${k.port})` : ""}`)
823
+ .join(", ");
824
+ console.log(`Stopped feinai server(s): ${summary}`);
835
825
  }
836
826
  return;
837
827
  }
@@ -844,17 +834,21 @@ async function cmdServer(args: ParsedArgs): Promise<void> {
844
834
 
845
835
  const db = ensureDb();
846
836
  const host = args.options.host ?? "127.0.0.1";
847
-
848
- // Pre-flight consistency check: repair stale records, reject if valid server exists
849
- repairServerState(db);
850
- const existing = readServerState(db);
851
- if (existing) {
852
- console.error(
853
- `feinai server is already running for this project at http://127.0.0.1:${existing.port}`,
854
- );
855
- console.error(`Stop it first with: feinai server --down --port ${existing.port}`);
856
- db.close();
857
- process.exit(1);
837
+ const noPreflight = args.flags["no-preflight"];
838
+
839
+ // Pre-flight consistency check: repair stale records, reject if valid server exists.
840
+ // Skipped when the child is launched by the daemon parent (it already validated).
841
+ if (!noPreflight) {
842
+ repairServerState(db);
843
+ const existing = readServerState(db);
844
+ if (existing) {
845
+ console.error(
846
+ `feinai server is already running for this project at http://127.0.0.1:${existing.port}`,
847
+ );
848
+ console.error(`Stop it first with: feinai server --down`);
849
+ db.close();
850
+ process.exit(1);
851
+ }
858
852
  }
859
853
 
860
854
  // Determine port: if --port is given, use it; otherwise auto-increment from 8272
@@ -889,14 +883,25 @@ async function cmdServer(args: ParsedArgs): Promise<void> {
889
883
  } else {
890
884
  childArgs = [process.execPath, ...process.argv.slice(1).filter(noDaemon)];
891
885
  }
892
- // Pass the resolved port so the child doesn't re-resolve (which could pick a different port)
893
- if (!args.options.port) {
894
- childArgs.push("--port", String(resolvedPort));
886
+ // Force the resolved port and the internal no-preflight flag onto the child.
887
+ // Strip any existing --port so the child always uses the port we resolved here.
888
+ for (let i = 0; i < childArgs.length; ) {
889
+ if (childArgs[i] === "--port") {
890
+ childArgs.splice(i, 2);
891
+ continue;
892
+ }
893
+ i++;
895
894
  }
895
+ childArgs.push("--port", String(resolvedPort), "--no-preflight");
896
+
896
897
  const child = Bun.spawn(childArgs, { detached: true, stdio: ["ignore", "ignore", "ignore"] });
898
+
899
+ // The parent owns the DB record: write it immediately with the child's pid
900
+ // so the server is tracked before the parent exits.
901
+ writeServerState(db, resolvedPort, child.pid);
897
902
  child.unref();
898
903
  console.log(`feinai dashboard → http://${host}:${resolvedPort}`);
899
- console.log(`Stop with: feinai server --down --port ${resolvedPort}`);
904
+ console.log(`Stop with: feinai server --down`);
900
905
  db.close();
901
906
  return;
902
907
  }
@@ -905,29 +910,27 @@ async function cmdServer(args: ParsedArgs): Promise<void> {
905
910
  const { startServer } = await import("./server");
906
911
  const server = startServer({ port: resolvedPort, host });
907
912
 
908
- // Write the server state row after successful bind
909
- writeServerState(db, resolvedPort, process.pid);
913
+ // Write the server state row after successful bind (only when the CLI is the
914
+ // actual server process; daemon children skip this because the parent wrote it).
915
+ if (!noPreflight) {
916
+ writeServerState(db, resolvedPort, process.pid);
917
+ }
910
918
  db.close();
911
919
 
912
920
  console.log(`feinai dashboard listening at ${server.url}`);
913
- console.log(`Stop with: feinai server --down --port ${resolvedPort}`);
921
+ console.log(`Stop with: feinai server --down`);
914
922
 
915
- // Keep process alive until SIGINT
916
- process.on("SIGINT", () => {
923
+ const shutdown = () => {
917
924
  console.log("\nStopping server...");
918
925
  const cleanupDb = ensureDb();
919
926
  clearServerState(cleanupDb);
920
927
  cleanupDb.close();
921
928
  server.stop();
922
929
  process.exit(0);
923
- });
930
+ };
924
931
 
925
- // Keep process alive until SIGINT
926
- process.on("SIGINT", () => {
927
- console.log("\nStopping server...");
928
- server.stop();
929
- process.exit(0);
930
- });
932
+ process.on("SIGINT", shutdown);
933
+ process.on("SIGTERM", shutdown);
931
934
  }
932
935
 
933
936
  function cmdPlan(rest: string[], args: ParsedArgs, format: OutputFormat): void {