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 +25 -0
- package/package.json +1 -1
- package/skills/feinai-implement/SKILL.md +19 -3
- package/src/cli.ts +78 -75
- package/src/dashboard.html +1283 -1304
- package/src/db.ts +15 -1
- package/src/format.ts +1 -4
- package/src/server-state.ts +75 -11
- package/src/specs.ts +9 -9
- package/src/tasks.ts +2 -2
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
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
793
|
+
const dbPath = findDbPath()!;
|
|
794
|
+
const db = openDb();
|
|
795
|
+
repairServerState(db);
|
|
796
|
+
const record = readServerState(db);
|
|
797
|
+
const killed: OrphanKillResult[] = [];
|
|
821
798
|
|
|
822
|
-
|
|
799
|
+
if (record) {
|
|
823
800
|
try {
|
|
824
|
-
process.kill(
|
|
825
|
-
|
|
801
|
+
process.kill(record.pid, "SIGTERM");
|
|
802
|
+
killed.push({ pid: record.pid, port: record.port });
|
|
826
803
|
} catch {
|
|
827
|
-
console.error(
|
|
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
|
-
//
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if (
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
)
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
//
|
|
893
|
-
|
|
894
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
921
|
+
console.log(`Stop with: feinai server --down`);
|
|
914
922
|
|
|
915
|
-
|
|
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
|
-
|
|
926
|
-
process.on("
|
|
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 {
|