From dd3d4315c42af16db2c084879b96cbd3ae763390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 24 Apr 2026 12:28:47 +0000 Subject: [PATCH 1/4] chore: add pre-push hook to run tests before push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds husky with a pre-push hook that runs `pnpm -r test` to catch test failures before they reach the remote. 小橘 🍊(NEKO Team) --- .husky/pre-push | 2 ++ package.json | 5 ++++- pnpm-lock.yaml | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100755 .husky/pre-push diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..f07764d --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,2 @@ +#!/bin/sh +pnpm -r test diff --git a/package.json b/package.json index 9b0564c..957f100 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,16 @@ "node": ">=22.5.0" }, "scripts": { + "prepare": "husky", "build": "pnpm -r run build", "check": "biome check .", - "format": "biome format --write ." + "format": "biome format --write .", + "prepare": "husky" }, "devDependencies": { "@biomejs/biome": "^1.9.0", "@rslib/core": "^0.21.3", + "husky": "^9.1.7", "typescript": "^5.5.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67546bd..b5598ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@rslib/core': specifier: ^0.21.3 version: 0.21.3(typescript@5.9.3) + husky: + specifier: ^9.1.7 + version: 9.1.7 typescript: specifier: ^5.5.0 version: 5.9.3 @@ -1010,6 +1013,11 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -2258,6 +2266,8 @@ snapshots: - supports-color optional: true + husky@9.1.7: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 -- 2.43.0 From 48c81c2e19c1665b9f2abfe747ab3ba44593469c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 24 Apr 2026 12:32:41 +0000 Subject: [PATCH 2/4] chore: add biome lint check to pre-push hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 小橘 🍊(NEKO Team) --- .husky/pre-push | 1 + 1 file changed, 1 insertion(+) diff --git a/.husky/pre-push b/.husky/pre-push index f07764d..77eee93 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,2 +1,3 @@ #!/bin/sh +pnpm check pnpm -r test -- 2.43.0 From 7cb7112ed64aa72aaaff19a7dcaa7db1dc575e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 24 Apr 2026 12:36:57 +0000 Subject: [PATCH 3/4] chore: fix biome lint errors and tune overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove duplicate 'prepare' key in package.json - Allow default exports in rslib.config.ts - Relax noExplicitAny and noNonNullAssertion in test files - Auto-fix 17 files (imports, formatting) 小橘 🍊(NEKO Team) --- biome.json | 15 +++++++++- package.json | 3 +- packages/cli/src/__tests__/logs.test.ts | 4 +-- packages/cli/src/__tests__/workflow.test.ts | 2 +- packages/cli/src/commands/logs.ts | 2 +- packages/cli/src/commands/sense.ts | 3 +- packages/cli/src/commands/workflow.ts | 28 ++++++------------- packages/core/src/index.ts | 10 +++++-- .../daemon/src/__tests__/daemon-ipc.test.ts | 5 +++- .../daemon/src/__tests__/hot-reload.test.ts | 2 +- .../src/__tests__/kernel-phase6.test.ts | 8 +++--- .../kernel-workflow-integration.test.ts | 4 +-- packages/daemon/src/__tests__/kernel.test.ts | 2 +- .../__tests__/log-store-integration.test.ts | 6 ++-- .../src/__tests__/phase6-integration.test.ts | 10 +++---- packages/daemon/src/daemon-ipc.ts | 12 ++++++-- packages/daemon/src/ipc.ts | 4 ++- packages/daemon/src/sense-runtime.ts | 6 ++-- packages/daemon/src/workflow-manager.ts | 12 ++++++-- 19 files changed, 81 insertions(+), 57 deletions(-) diff --git a/biome.json b/biome.json index 546069b..eb60de4 100644 --- a/biome.json +++ b/biome.json @@ -19,7 +19,7 @@ }, "overrides": [ { - "include": ["tsup.config.ts"], + "include": ["tsup.config.ts", "*/rslib.config.ts"], "linter": { "rules": { "style": { @@ -27,6 +27,19 @@ } } } + }, + { + "include": ["**/__tests__/**"], + "linter": { + "rules": { + "suspicious": { + "noExplicitAny": "off" + }, + "style": { + "noNonNullAssertion": "off" + } + } + } } ], "linter": { diff --git a/package.json b/package.json index 957f100..13aa047 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,7 @@ "prepare": "husky", "build": "pnpm -r run build", "check": "biome check .", - "format": "biome format --write .", - "prepare": "husky" + "format": "biome format --write ." }, "devDependencies": { "@biomejs/biome": "^1.9.0", diff --git a/packages/cli/src/__tests__/logs.test.ts b/packages/cli/src/__tests__/logs.test.ts index a15a1c2..3950784 100644 --- a/packages/cli/src/__tests__/logs.test.ts +++ b/packages/cli/src/__tests__/logs.test.ts @@ -234,7 +234,7 @@ describe("logsCommand negative offset", () => { it("exits with code 1 and writes to stderr when offset is negative", async () => { await expect( - logsCommand.run!({ + logsCommand.run?.({ args: { n: "50", offset: "-5", follow: false }, rawArgs: [], cmd: logsCommand as never, @@ -247,7 +247,7 @@ describe("logsCommand negative offset", () => { it("exits with code 1 for offset=-1", async () => { await expect( - logsCommand.run!({ + logsCommand.run?.({ args: { n: "10", offset: "-1", follow: false }, rawArgs: [], cmd: logsCommand as never, diff --git a/packages/cli/src/__tests__/workflow.test.ts b/packages/cli/src/__tests__/workflow.test.ts index a795fb5..8fa9051 100644 --- a/packages/cli/src/__tests__/workflow.test.ts +++ b/packages/cli/src/__tests__/workflow.test.ts @@ -15,6 +15,7 @@ import { join } from "node:path"; import { createLogStore } from "@uncaged/nerve-store"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { LogStore, ThreadRoundRow, WorkflowRun } from "@uncaged/nerve-store"; import { DEFAULT_THREAD_BUDGET_CHARS, buildInspectOutput, @@ -28,7 +29,6 @@ import { statusIcon, } from "../commands/workflow.js"; import { triggerWorkflowViaDaemon } from "../daemon-client.js"; -import type { LogStore, ThreadRoundRow, WorkflowRun } from "@uncaged/nerve-store"; // --------------------------------------------------------------------------- // Test helpers diff --git a/packages/cli/src/commands/logs.ts b/packages/cli/src/commands/logs.ts index b709b95..89df1c2 100644 --- a/packages/cli/src/commands/logs.ts +++ b/packages/cli/src/commands/logs.ts @@ -85,7 +85,7 @@ export function buildLogFooter(slice: LogSlice, nArg: number, logPath: string): let footer = `\n📄 ${rangeStr} | ${logPath}\n`; if (slice.nextOffset !== null) { - footer += `⏩ Earlier lines available. Fetch previous page:\n`; + footer += "⏩ Earlier lines available. Fetch previous page:\n"; footer += ` nerve logs --offset ${slice.nextOffset} -n ${nArg}\n`; } diff --git a/packages/cli/src/commands/sense.ts b/packages/cli/src/commands/sense.ts index 41eed3b..6712bfb 100644 --- a/packages/cli/src/commands/sense.ts +++ b/packages/cli/src/commands/sense.ts @@ -1,13 +1,12 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { DatabaseSync } from "node:sqlite"; +import type { DatabaseSync } from "node:sqlite"; import { type SenseInfo, isPlainRecord, parseNerveConfig } from "@uncaged/nerve-core"; import { defineCommand } from "citty"; import { listSensesViaDaemon, triggerSenseViaDaemon } from "../daemon-client.js"; import { - assertSenseDbExists, defaultPreviewSql, formatRowsAsAlignedTable, listTableSqlStatements, diff --git a/packages/cli/src/commands/workflow.ts b/packages/cli/src/commands/workflow.ts index fcd8402..5026274 100644 --- a/packages/cli/src/commands/workflow.ts +++ b/packages/cli/src/commands/workflow.ts @@ -5,8 +5,8 @@ import { isPlainRecord } from "@uncaged/nerve-core"; import { defineCommand } from "citty"; import { stringify } from "yaml"; -import { triggerWorkflowViaDaemon } from "../daemon-client.js"; import type { LogStore, ThreadRoundRow, WorkflowRun } from "@uncaged/nerve-store"; +import { triggerWorkflowViaDaemon } from "../daemon-client.js"; import { loadDaemonModule } from "../workspace-daemon.js"; import { getNerveRoot, getSocketPath, isRunning } from "../workspace.js"; @@ -218,13 +218,7 @@ export function formatThreadRoundBlock(row: ThreadRoundRow): string { const { roleStr, contentBody, meta } = partitionWorkflowMessage(row.message); const yamlBlock = Object.keys(meta).length === 0 ? "{}\n" : `${stringify(meta, { lineWidth: 100 })}\n`; - return ( - `[#${row.round} ${roleStr}] ${formatTs(row.ts)}\n` + - `---\n` + - yamlBlock + - `---\n` + - `${contentBody}\n\n` - ); + return `[#${row.round} ${roleStr}] ${formatTs(row.ts)}\n---\n${yamlBlock}---\n${contentBody}\n\n`; } export type ThreadCommandOutput = { @@ -260,14 +254,13 @@ export function buildThreadCommandOutput( const { roleStr, contentBody, meta } = partitionWorkflowMessage(row.message); const yamlBlock = Object.keys(meta).length === 0 ? "{}\n" : `${stringify(meta, { lineWidth: 100 })}\n`; - const header = - `[#${row.round} ${roleStr}] ${formatTs(row.ts)}\n` + `---\n` + yamlBlock + `---\n`; - const maxBody = Math.max(0, remaining - header.length - `[truncated]\n`.length); + const header = `[#${row.round} ${roleStr}] ${formatTs(row.ts)}\n---\n${yamlBlock}---\n`; + const maxBody = Math.max(0, remaining - header.length - "[truncated]\n".length); const truncated = maxBody > 0 && contentBody.length > maxBody ? `${contentBody.slice(0, maxBody)}\n[truncated]\n` : `${contentBody}\n[truncated]\n`; - const single = header + truncated + "\n"; + const single = `${header + truncated}\n`; const hintRound = row.round; return { lines: [...prefixLines, single], @@ -284,9 +277,7 @@ export function buildThreadCommandOutput( const shownMinRound = picked.length === 0 ? null : Math.min(...picked.map((r) => r.round)); let paginationHint: string | null = null; if (shownMinRound !== null && shownMinRound > 1) { - paginationHint = - `\n⏩ Older rounds not shown. Fetch with:\n` + - ` nerve workflow thread ${runId} --before ${String(shownMinRound)}${budgetFlag}\n`; + paginationHint = `\n⏩ Older rounds not shown. Fetch with:\n nerve workflow thread ${runId} --before ${String(shownMinRound)}${budgetFlag}\n`; } return { lines: [...prefixLines, ...blocksAsc], paginationHint }; @@ -455,10 +446,7 @@ const workflowThreadCommand = defineCommand({ const totalRoleRounds = store.getThreadRoundCount(args.runId); if (totalRoleRounds === 0) { process.stdout.write( - `🧵 Workflow thread: ${run.runId}\n` + - ` workflow: ${run.workflow}\n` + - ` status: ${run.status}\n\n` + - `📭 No role rounds recorded for this run.\n`, + `🧵 Workflow thread: ${run.runId}\n workflow: ${run.workflow}\n status: ${run.status}\n\n📭 No role rounds recorded for this run.\n`, ); return; } @@ -469,7 +457,7 @@ const workflowThreadCommand = defineCommand({ }); const prefixLines = [ - `🧵 Role rounds (workflow thread)\n`, + "🧵 Role rounds (workflow thread)\n", ` runId: ${run.runId}\n`, ` workflow: ${run.workflow}\n`, ` status: ${run.status}\n`, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8978cad..730ea75 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -24,5 +24,11 @@ export { ok, err } from "./result.js"; export { parseNerveConfig } from "./config.js"; export { isPlainRecord } from "./is-plain-record.js"; -export type { ParsedSenseWorkflowDirective, SenseComputeRoute } from "./sense-workflow-directive.js"; -export { parseSenseWorkflowDirective, routeSenseComputeOutput } from "./sense-workflow-directive.js"; +export type { + ParsedSenseWorkflowDirective, + SenseComputeRoute, +} from "./sense-workflow-directive.js"; +export { + parseSenseWorkflowDirective, + routeSenseComputeOutput, +} from "./sense-workflow-directive.js"; diff --git a/packages/daemon/src/__tests__/daemon-ipc.test.ts b/packages/daemon/src/__tests__/daemon-ipc.test.ts index a1655e4..0f0402c 100644 --- a/packages/daemon/src/__tests__/daemon-ipc.test.ts +++ b/packages/daemon/src/__tests__/daemon-ipc.test.ts @@ -158,7 +158,10 @@ describe("daemon-ipc — trigger-sense", () => { expect(resp).toEqual({ ok: true }); expect(triggerSense).not.toHaveBeenCalled(); - expect(wfManager.startWorkflow).toHaveBeenCalledWith("my-workflow", { prompt: "test prompt", maxRounds: 10 }); + expect(wfManager.startWorkflow).toHaveBeenCalledWith("my-workflow", { + prompt: "test prompt", + maxRounds: 10, + }); }); it("responds ok:false for completely unknown request type", async () => { diff --git a/packages/daemon/src/__tests__/hot-reload.test.ts b/packages/daemon/src/__tests__/hot-reload.test.ts index 692192e..98a0ed1 100644 --- a/packages/daemon/src/__tests__/hot-reload.test.ts +++ b/packages/daemon/src/__tests__/hot-reload.test.ts @@ -304,7 +304,7 @@ describe("Kernel — workflow hot reload via file-watcher (Phase 3)", () => { senses: {}, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }; kernel.reloadConfig(newConfig); diff --git a/packages/daemon/src/__tests__/kernel-phase6.test.ts b/packages/daemon/src/__tests__/kernel-phase6.test.ts index c575ec8..5fd62bc 100644 --- a/packages/daemon/src/__tests__/kernel-phase6.test.ts +++ b/packages/daemon/src/__tests__/kernel-phase6.test.ts @@ -181,7 +181,7 @@ describe("kernel — reloadConfig", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }); expect(kernel.groups.has("network")).toBe(true); @@ -198,7 +198,7 @@ describe("kernel — reloadConfig", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }; const kernel = createKernel(config, "/tmp/nerve-test"); @@ -213,7 +213,7 @@ describe("kernel — reloadConfig", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }); expect(kernel.groups.has("network")).toBe(false); @@ -236,7 +236,7 @@ describe("kernel — reloadConfig", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }); expect(kernel.getHealth().activeSenses).toBe(2); diff --git a/packages/daemon/src/__tests__/kernel-workflow-integration.test.ts b/packages/daemon/src/__tests__/kernel-workflow-integration.test.ts index 4edbbda..c28d725 100644 --- a/packages/daemon/src/__tests__/kernel-workflow-integration.test.ts +++ b/packages/daemon/src/__tests__/kernel-workflow-integration.test.ts @@ -298,7 +298,7 @@ describe("kernel + workflowManager integration", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }); const kernel = createKernel(initialConfig, "/tmp/nerve-test", { @@ -366,7 +366,7 @@ describe("kernel + workflowManager integration", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }; kernel.reloadConfig(newConfig); diff --git a/packages/daemon/src/__tests__/kernel.test.ts b/packages/daemon/src/__tests__/kernel.test.ts index 05802e0..f581740 100644 --- a/packages/daemon/src/__tests__/kernel.test.ts +++ b/packages/daemon/src/__tests__/kernel.test.ts @@ -201,7 +201,7 @@ describe("kernel — groupForSense mapping", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }; const kernel = createKernel(config, "/tmp/nerve-test"); diff --git a/packages/daemon/src/__tests__/log-store-integration.test.ts b/packages/daemon/src/__tests__/log-store-integration.test.ts index 1a06eb2..c00712d 100644 --- a/packages/daemon/src/__tests__/log-store-integration.test.ts +++ b/packages/daemon/src/__tests__/log-store-integration.test.ts @@ -30,7 +30,7 @@ describe("LogStore + ReflexScheduler integration", () => { }, reflexes: [{ kind: "sense", sense: "cpu-usage", interval: null, on: ["cpu-usage"] }], workflows: null, - maxRounds: 10, + maxRounds: 10, }; const bus = createSignalBus(); const triggered: string[] = []; @@ -58,7 +58,7 @@ describe("LogStore + ReflexScheduler integration", () => { }, reflexes: [{ kind: "sense", sense: "cpu-usage", interval: 1000, on: null }], workflows: null, - maxRounds: 10, + maxRounds: 10, }; const bus = createSignalBus(); const ref: { scheduler: ReturnType | null } = { scheduler: null }; @@ -89,7 +89,7 @@ describe("LogStore + ReflexScheduler integration", () => { }, reflexes: [{ kind: "sense", sense: "cpu-usage", interval: null, on: ["cpu-usage"] }], workflows: null, - maxRounds: 10, + maxRounds: 10, }; const bus = createSignalBus(); const triggered: string[] = []; diff --git a/packages/daemon/src/__tests__/phase6-integration.test.ts b/packages/daemon/src/__tests__/phase6-integration.test.ts index 58021f2..d129088 100644 --- a/packages/daemon/src/__tests__/phase6-integration.test.ts +++ b/packages/daemon/src/__tests__/phase6-integration.test.ts @@ -137,7 +137,7 @@ describe("phase6 — reloadConfig", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }; kernel.reloadConfig(newConfig); @@ -157,7 +157,7 @@ describe("phase6 — reloadConfig", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }; kernel = createKernel(config, "/tmp/nerve-phase6-test", { workerScript: MOCK_WORKER, @@ -172,7 +172,7 @@ describe("phase6 — reloadConfig", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }; kernel.reloadConfig(newConfig); @@ -203,7 +203,7 @@ describe("phase6 — error isolation", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }; kernel = createKernel(config, "/tmp/nerve-phase6-test", { @@ -307,7 +307,7 @@ describe("phase6 — getHealth", () => { }, reflexes: [], workflows: null, - maxRounds: 10, + maxRounds: 10, }; kernel.reloadConfig(newConfig); diff --git a/packages/daemon/src/daemon-ipc.ts b/packages/daemon/src/daemon-ipc.ts index b8ad68b..de752cb 100644 --- a/packages/daemon/src/daemon-ipc.ts +++ b/packages/daemon/src/daemon-ipc.ts @@ -59,7 +59,12 @@ function parseRequest(line: string): DaemonRequest | null { if (typeof req.workflow !== "string" || req.workflow.length === 0) return null; if (typeof req.prompt !== "string") return null; if (typeof req.maxRounds !== "number") return null; - return { type: "trigger-workflow", workflow: req.workflow, prompt: req.prompt, maxRounds: req.maxRounds }; + return { + type: "trigger-workflow", + workflow: req.workflow, + prompt: req.prompt, + maxRounds: req.maxRounds, + }; } if (req.type === "trigger-sense") { if (typeof req.sense !== "string" || req.sense.length === 0) return null; @@ -106,7 +111,10 @@ export function createDaemonIpcServer( try { if (req.type === "trigger-workflow") { - workflowManager.startWorkflow(req.workflow, { prompt: req.prompt, maxRounds: req.maxRounds }); + workflowManager.startWorkflow(req.workflow, { + prompt: req.prompt, + maxRounds: req.maxRounds, + }); const resp: DaemonResponse = { ok: true }; socket.write(`${JSON.stringify(resp)}\n`); } else if (req.type === "trigger-sense") { diff --git a/packages/daemon/src/ipc.ts b/packages/daemon/src/ipc.ts index d6121f0..ff31aa4 100644 --- a/packages/daemon/src/ipc.ts +++ b/packages/daemon/src/ipc.ts @@ -296,7 +296,9 @@ const WORKER_MSG_TYPES = new Set([ "thread-workflow-message", ]); -function parseThreadWorkflowMessageMsg(obj: Record): Result { +function parseThreadWorkflowMessageMsg( + obj: Record, +): Result { if (typeof obj.runId !== "string") { return err(new Error("Worker 'thread-workflow-message' missing string 'runId' field")); } diff --git a/packages/daemon/src/sense-runtime.ts b/packages/daemon/src/sense-runtime.ts index 495576f..5456270 100644 --- a/packages/daemon/src/sense-runtime.ts +++ b/packages/daemon/src/sense-runtime.ts @@ -110,9 +110,9 @@ export function runMigrations(sqlite: DatabaseSync, migrationsDir: string): Resu const migrationRows = sqlite.prepare("SELECT name FROM _migrations").all(); const applied = new Set( - migrationRows.filter((r): r is { name: string } => isPlainRecord(r) && typeof r.name === "string").map( - (r) => r.name, - ), + migrationRows + .filter((r): r is { name: string } => isPlainRecord(r) && typeof r.name === "string") + .map((r) => r.name), ); for (const file of filesResult.value) { diff --git a/packages/daemon/src/workflow-manager.ts b/packages/daemon/src/workflow-manager.ts index fdae7de..dddb440 100644 --- a/packages/daemon/src/workflow-manager.ts +++ b/packages/daemon/src/workflow-manager.ts @@ -14,6 +14,7 @@ import { fileURLToPath } from "node:url"; import type { NerveConfig, WorkflowConfig, WorkflowMessage } from "@uncaged/nerve-core"; import { START, isPlainRecord } from "@uncaged/nerve-core"; +import type { LogStore, WorkflowRunStatus } from "@uncaged/nerve-store"; import type { ResumeThreadMessage, ShutdownMessage, @@ -21,7 +22,6 @@ import type { ThreadEventMessage, } from "./ipc.js"; import { parseWorkerMessage } from "./ipc.js"; -import type { LogStore, WorkflowRunStatus } from "@uncaged/nerve-store"; import { formatCapturedStderrTail, formatChildExitSummary, @@ -307,7 +307,10 @@ export function createWorkflowManager( function recoverQueuedRun(workflowName: string, runId: string, state: WorkflowState): void { if (state.queue.some((q) => q.runId === runId)) return; - const launch = readLaunchFromTriggerPayload(logStore.getTriggerPayload(runId), config.maxRounds); + const launch = readLaunchFromTriggerPayload( + logStore.getTriggerPayload(runId), + config.maxRounds, + ); state.queue.push({ runId, prompt: launch.prompt, maxRounds: launch.maxRounds }); process.stderr.write( `[workflow-manager] crash-recovery: re-queued thread "${runId}" for "${workflowName}"\n`, @@ -322,7 +325,10 @@ export function createWorkflowManager( ): void { if (state.active.has(runId)) return; const rawMessages = logStore.getThreadMessages(runId); - const launch = readLaunchFromTriggerPayload(logStore.getTriggerPayload(runId), config.maxRounds); + const launch = readLaunchFromTriggerPayload( + logStore.getTriggerPayload(runId), + config.maxRounds, + ); const messages = ensureThreadMessagesWithStart(rawMessages, launch.prompt, launch.maxRounds); state.active.add(runId); const msg: ResumeThreadMessage = { -- 2.43.0 From b2c379cbfd1a6d7c3b851f0390f787690fef85ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 24 Apr 2026 12:44:39 +0000 Subject: [PATCH 4/4] refactor: reduce cognitive complexity in 3 functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract helpers to bring all functions below biome's complexity threshold (15): - store/log-store.ts: extract recordToRoundMessage() from parseRoundPayload() - cli/commands/workflow.ts: extract buildTruncatedSingleRound() from buildThreadCommandOutput() - daemon/workflow-worker.ts: extract validateRoleResult(), buildInitialLastSignal(), initChain(), executeRole() from runThread() 小橘 🍊(NEKO Team) --- packages/cli/src/commands/workflow.ts | 46 +++++---- packages/daemon/src/workflow-worker.ts | 126 +++++++++++++++---------- packages/store/src/log-store.ts | 42 +++++---- 3 files changed, 129 insertions(+), 85 deletions(-) diff --git a/packages/cli/src/commands/workflow.ts b/packages/cli/src/commands/workflow.ts index 5026274..612e18b 100644 --- a/packages/cli/src/commands/workflow.ts +++ b/packages/cli/src/commands/workflow.ts @@ -226,6 +226,33 @@ export type ThreadCommandOutput = { paginationHint: string | null; }; +function buildTruncatedSingleRound( + row: ThreadRoundRow, + remaining: number, + prefixLines: string[], + runId: string, + budgetFlag: string, +): ThreadCommandOutput { + const { roleStr, contentBody, meta } = partitionWorkflowMessage(row.message); + const yamlBlock = + Object.keys(meta).length === 0 ? "{}\n" : `${stringify(meta, { lineWidth: 100 })}\n`; + const header = `[#${row.round} ${roleStr}] ${formatTs(row.ts)}\n---\n${yamlBlock}---\n`; + const maxBody = Math.max(0, remaining - header.length - "[truncated]\n".length); + const truncated = + maxBody > 0 && contentBody.length > maxBody + ? `${contentBody.slice(0, maxBody)}\n[truncated]\n` + : `${contentBody}\n[truncated]\n`; + const single = `${header + truncated}\n`; + const hintRound = row.round; + return { + lines: [...prefixLines, single], + paginationHint: + hintRound > 1 + ? `\n⏩ Older rounds exist. Fetch with:\n nerve workflow thread ${runId} --before ${String(hintRound)}${budgetFlag}\n` + : null, + }; +} + /** * Build stdout lines for `nerve workflow thread`: newest-first selection from * `descRows` until `budgetChars` (including `prefixLines`), then chronological order. @@ -251,24 +278,7 @@ export function buildThreadCommandOutput( continue; } if (picked.length === 0) { - const { roleStr, contentBody, meta } = partitionWorkflowMessage(row.message); - const yamlBlock = - Object.keys(meta).length === 0 ? "{}\n" : `${stringify(meta, { lineWidth: 100 })}\n`; - const header = `[#${row.round} ${roleStr}] ${formatTs(row.ts)}\n---\n${yamlBlock}---\n`; - const maxBody = Math.max(0, remaining - header.length - "[truncated]\n".length); - const truncated = - maxBody > 0 && contentBody.length > maxBody - ? `${contentBody.slice(0, maxBody)}\n[truncated]\n` - : `${contentBody}\n[truncated]\n`; - const single = `${header + truncated}\n`; - const hintRound = row.round; - return { - lines: [...prefixLines, single], - paginationHint: - hintRound > 1 - ? `\n⏩ Older rounds exist. Fetch with:\n nerve workflow thread ${runId} --before ${String(hintRound)}${budgetFlag}\n` - : null, - }; + return buildTruncatedSingleRound(row, remaining, prefixLines, runId, budgetFlag); } break; } diff --git a/packages/daemon/src/workflow-worker.ts b/packages/daemon/src/workflow-worker.ts index c920f28..3264c02 100644 --- a/packages/daemon/src/workflow-worker.ts +++ b/packages/daemon/src/workflow-worker.ts @@ -71,6 +71,79 @@ function sendWorkflowMessage(runId: string, message: WorkflowMessage): void { // Thread loop (signal-driven automaton, issue #80) // --------------------------------------------------------------------------- +function validateRoleResult( + result: { content: string; meta: Record }, + roleName: string, + runId: string, +): boolean { + if (typeof result.content !== "string") { + sendWorkflowError(runId, `Role "${roleName}" returned non-string content`); + return false; + } + if (result.meta === null || typeof result.meta !== "object" || Array.isArray(result.meta)) { + sendWorkflowError(runId, `Role "${roleName}" returned invalid meta (must be a plain object)`); + return false; + } + return true; +} + +function buildInitialLastSignal(lastMsg: WorkflowMessage): ModeratorInput { + if (lastMsg.role === START) { + return { + role: START, + content: lastMsg.content, + meta: lastMsg.meta as StartSignal["meta"], + timestamp: lastMsg.timestamp, + }; + } + return { role: lastMsg.role, meta: lastMsg.meta as Record }; +} + +function initChain( + runId: string, + resumeMessages: WorkflowMessage[], + freshPrompt: string | null, + maxRounds: number, +): WorkflowMessage[] { + if (resumeMessages.length > 0) { + return [...resumeMessages]; + } + const prompt = freshPrompt ?? ""; + const startMsg: WorkflowMessage = { + role: START, + content: prompt, + meta: { maxRounds }, + timestamp: Date.now(), + }; + sendWorkflowMessage(runId, startMsg); + return [startMsg]; +} + +async function executeRole( + def: WorkflowDefinition, + nextRole: string, + chain: WorkflowMessage[], + runId: string, +): Promise<{ content: string; meta: Record } | null> { + const role = def.roles[nextRole]; + if (!role) { + sendWorkflowError(runId, `Unknown role: ${nextRole}`); + return null; + } + + let result: { content: string; meta: Record }; + try { + result = await role(chain); + } catch (e: unknown) { + const errMsg = e instanceof Error ? e.message : String(e); + sendThreadEvent(runId, "failed", { error: errMsg }); + return null; + } + + if (!validateRoleResult(result, nextRole, runId)) return null; + return result; +} + async function runThread( def: WorkflowDefinition, runId: string, @@ -78,21 +151,7 @@ async function runThread( resumeMessages: WorkflowMessage[] = [], freshPrompt: string | null = null, ): Promise { - let chain: WorkflowMessage[]; - - if (resumeMessages.length > 0) { - chain = [...resumeMessages]; - } else { - const prompt = freshPrompt ?? ""; - const startMsg: WorkflowMessage = { - role: START, - content: prompt, - meta: { maxRounds }, - timestamp: Date.now(), - }; - chain = [startMsg]; - sendWorkflowMessage(runId, startMsg); - } + const chain = initChain(runId, resumeMessages, freshPrompt, maxRounds); let roleRound = chain.filter((m) => m.role !== START).length; const lastMsg = chain[chain.length - 1]; @@ -101,17 +160,7 @@ async function runThread( return; } - const lastSignal: ModeratorInput = - lastMsg.role === START - ? { - role: START, - content: lastMsg.content, - meta: lastMsg.meta as StartSignal["meta"], - timestamp: lastMsg.timestamp, - } - : { role: lastMsg.role, meta: lastMsg.meta as Record }; - - let nextRole = def.moderator(lastSignal, roleRound, maxRounds); + let nextRole = def.moderator(buildInitialLastSignal(lastMsg), roleRound, maxRounds); if (nextRole === END) { sendThreadEvent(runId, "completed", null); @@ -119,29 +168,8 @@ async function runThread( } while (roleRound < maxRounds) { - const role = def.roles[nextRole]; - if (!role) { - sendWorkflowError(runId, `Unknown role: ${nextRole}`); - return; - } - - let result: { content: string; meta: Record }; - try { - result = await role(chain); - } catch (e: unknown) { - const errMsg = e instanceof Error ? e.message : String(e); - sendThreadEvent(runId, "failed", { error: errMsg }); - return; - } - - if (typeof result.content !== "string") { - sendWorkflowError(runId, `Role "${nextRole}" returned non-string content`); - return; - } - if (result.meta === null || typeof result.meta !== "object" || Array.isArray(result.meta)) { - sendWorkflowError(runId, `Role "${nextRole}" returned invalid meta (must be a plain object)`); - return; - } + const result = await executeRole(def, nextRole, chain, runId); + if (result === null) return; const message: WorkflowMessage = { role: nextRole, diff --git a/packages/store/src/log-store.ts b/packages/store/src/log-store.ts index f25afb0..34b94b9 100644 --- a/packages/store/src/log-store.ts +++ b/packages/store/src/log-store.ts @@ -580,6 +580,29 @@ export function createLogStore(dbPath: string): LogStore { return Number(c); } + function recordToRoundMessage( + obj: Record, + fallbackTs: number, + ): { role: string; content: string; meta: unknown; timestamp: number } | null { + if (typeof obj.role === "string" && typeof obj.content === "string") { + return { + role: obj.role, + content: obj.content, + meta: obj.meta, + timestamp: typeof obj.timestamp === "number" ? obj.timestamp : 0, + }; + } + if (typeof obj.type === "string") { + return { + role: typeof obj.role === "string" ? obj.role : obj.type, + content: typeof obj.content === "string" ? obj.content : JSON.stringify(obj), + meta: obj, + timestamp: fallbackTs, + }; + } + return null; + } + function parseRoundPayload( payload: string, fallbackTs: number, @@ -587,24 +610,7 @@ export function createLogStore(dbPath: string): LogStore { try { const parsed: unknown = JSON.parse(payload); if (!isPlainRecord(parsed)) return null; - const obj = parsed; - if (typeof obj.role === "string" && typeof obj.content === "string") { - return { - role: obj.role, - content: obj.content, - meta: obj.meta, - timestamp: typeof obj.timestamp === "number" ? obj.timestamp : 0, - }; - } - if (typeof obj.type === "string") { - return { - role: typeof obj.role === "string" ? obj.role : obj.type, - content: typeof obj.content === "string" ? obj.content : JSON.stringify(obj), - meta: obj, - timestamp: fallbackTs, - }; - } - return null; + return recordToRoundMessage(parsed, fallbackTs); } catch { return null; } -- 2.43.0