diff --git a/CLAUDE.md b/CLAUDE.md index 712b134..82a4fe6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,7 +109,7 @@ import type { SenseTrigger } from "@uncaged/nerve-core"; // ✅ Good — sense modules return explicit next state + optional trigger (workflow or shell) type SenseComputeReturn = { state: S; - workflow: SenseTrigger | null; + trigger: SenseTrigger | null; }; ``` diff --git a/packages/cli/skills/claude/CLAUDE.md b/packages/cli/skills/claude/CLAUDE.md index e5cdf76..5083893 100644 --- a/packages/cli/skills/claude/CLAUDE.md +++ b/packages/cli/skills/claude/CLAUDE.md @@ -226,11 +226,11 @@ export const initialState: MyState = { lastRun: null, count: 0 }; export async function compute(state: MyState): Promise<{ state: MyState; - workflow: WorkflowTrigger | null; + trigger: WorkflowTrigger | null; }> { return { state: { lastRun: Date.now(), count: state.count + 1 }, - workflow: null, + trigger: null, }; } ``` @@ -247,8 +247,8 @@ export async function compute(state: MyState): Promise<{ ### 返回值 ```typescript -// workflow: null → 不触发 workflow -// workflow: WorkflowTrigger → 触发 workflow +// trigger: null → 不触发 workflow +// trigger: WorkflowTrigger → 触发 workflow type WorkflowTrigger = { name: string; // workflow 名称(对应 nerve.yaml 中的 key) @@ -271,7 +271,7 @@ export const initialState: MyState = { ... }; // 2. compute 函数 export async function compute(state: MyState): Promise<{ state: MyState; - workflow: WorkflowTrigger | null; + trigger: WorkflowTrigger | null; }> { // ... } @@ -304,12 +304,12 @@ export const initialState: CpuState = { samples: [] }; export async function compute(state: CpuState): Promise<{ state: CpuState; - workflow: null; + trigger: null; }> { const [oneMin] = loadavg(); const value = typeof oneMin === "number" && !Number.isNaN(oneMin) ? oneMin : 0; const newSamples = [...state.samples.slice(-99), { ts: Date.now(), value }]; - return { state: { samples: newSamples }, workflow: null }; + return { state: { samples: newSamples }, trigger: null }; } ``` diff --git a/packages/cli/skills/hermes/SKILL.md b/packages/cli/skills/hermes/SKILL.md index 0377c1f..d2854cd 100644 --- a/packages/cli/skills/hermes/SKILL.md +++ b/packages/cli/skills/hermes/SKILL.md @@ -250,7 +250,7 @@ export async function compute(): Promise> { // 返回非 null = 发出 signal(并写入业务表),可选触发 workflow type ComputeResult = | null - | { signal: T; workflow: WorkflowTrigger | null }; + | { signal: T; trigger: WorkflowTrigger | null }; type WorkflowTrigger = { name: string; // workflow 名称(对应 nerve.yaml 中的 key) @@ -260,7 +260,7 @@ type WorkflowTrigger = { }; ``` -若返回值是普通对象且不含 `signal` 字段,内核会按 shorthand 视为 `{ signal: payload, workflow: null }`(见 core 的 `routeSenseComputeOutput`)。 +若返回值是普通对象且不含 `signal` 字段,内核会按 shorthand 视为 `{ signal: payload, trigger: null }`(见 core 的 `routeSenseComputeOutput`)。 ### Sense 模块导出 @@ -273,7 +273,7 @@ type Row = { ts: number; value: number }; export async function compute(): Promise> { const row: Row = { ts: Date.now(), value: Math.random() }; // 替换为真实观测逻辑 - return { signal: row, workflow: null }; + return { signal: row, trigger: null }; } export { table }; @@ -325,7 +325,7 @@ type Row = { ts: number; value: number }; export async function compute(): Promise> { const oneMin = os.loadavg()[0]; - return { signal: { ts: Date.now(), value: oneMin }, workflow: null }; + return { signal: { ts: Date.now(), value: oneMin }, trigger: null }; } export { table }; diff --git a/packages/cli/src/__tests__/create-sense.test.ts b/packages/cli/src/__tests__/create-sense.test.ts index 93e0a61..f94d792 100644 --- a/packages/cli/src/__tests__/create-sense.test.ts +++ b/packages/cli/src/__tests__/create-sense.test.ts @@ -26,7 +26,7 @@ describe("buildSenseIndexTs", () => { expect(ts).toContain("type SenseState"); expect(ts).toContain("export const initialState"); expect(ts).toContain("export async function compute"); - expect(ts).toContain("workflow: null"); + expect(ts).toContain("trigger: null"); expect(ts).toContain("lastRun"); }); }); diff --git a/packages/cli/src/__tests__/e2e-harness.ts b/packages/cli/src/__tests__/e2e-harness.ts index 11dced8..aebfb7e 100644 --- a/packages/cli/src/__tests__/e2e-harness.ts +++ b/packages/cli/src/__tests__/e2e-harness.ts @@ -120,7 +120,7 @@ const counterIndexJs = `export const initialState = { count: 0 }; export async function compute(state) { return { state: { count: state.count + 1 }, - workflow: null, + trigger: null, }; } `; @@ -132,7 +132,7 @@ export async function compute(state) { if (!state.launched) { return { state: { launched: true, idleTicks: state.idleTicks }, - workflow: { + trigger: { kind: "workflow", name: "noop", maxRounds: 3, @@ -143,7 +143,7 @@ export async function compute(state) { } return { state: { launched: state.launched, idleTicks: state.idleTicks + 1 }, - workflow: null, + trigger: null, }; } `; diff --git a/packages/cli/src/commands/create.ts b/packages/cli/src/commands/create.ts index 48a6179..dde6d82 100644 --- a/packages/cli/src/commands/create.ts +++ b/packages/cli/src/commands/create.ts @@ -94,12 +94,12 @@ export const initialState: SenseState = { lastRun: null }; export async function compute(state: SenseState): Promise<{ state: SenseState; - workflow: null; + trigger: null; }> { // TODO: implement sense logic return { state: { lastRun: Date.now() }, - workflow: null, + trigger: null, }; } `; diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index c00914c..48c530d 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -224,12 +224,12 @@ export const initialState: CpuState = { samples: [] }; export async function compute(state: CpuState): Promise<{ state: CpuState; - workflow: null; + trigger: null; }> { const [oneMin] = loadavg(); const value = typeof oneMin === "number" && !Number.isNaN(oneMin) ? oneMin : 0; const newSamples = [...state.samples.slice(-99), { ts: Date.now(), value }]; - return { state: { samples: newSamples }, workflow: null }; + return { state: { samples: newSamples }, trigger: null }; } `; diff --git a/packages/core/src/sense.ts b/packages/core/src/sense.ts index 2e5a65e..ddb6076 100644 --- a/packages/core/src/sense.ts +++ b/packages/core/src/sense.ts @@ -16,11 +16,11 @@ export type SenseInfo = { * `compute` export. * * Pure: no DB, no peers. - * Returns the next sense state and an optional trigger (`workflow: null` means no side effect). + * Returns the next sense state and an optional trigger (`trigger: null` means no side effect). */ export type SenseComputeFn = ( state: S, -) => Promise<{ state: S; workflow: SenseTrigger | null }>; +) => Promise<{ state: S; trigger: SenseTrigger | null }>; /** * The full shape a sense module (`src/index.ts`) must export. @@ -103,9 +103,7 @@ function parseShellTriggerBranch(value: Record): Result { if (!isPlainRecord(value)) { return err(new Error("sense trigger must be a plain object")); diff --git a/packages/daemon/src/__tests__/file-watcher.test.ts b/packages/daemon/src/__tests__/file-watcher.test.ts index f29f30f..baad62f 100644 --- a/packages/daemon/src/__tests__/file-watcher.test.ts +++ b/packages/daemon/src/__tests__/file-watcher.test.ts @@ -79,7 +79,7 @@ describe("createFileWatcher", () => { await new Promise((r) => setTimeout(r, 100)); writeFileSync( join(root, "senses", "cpu-usage", "index.js"), - "export const initialState = {}; export async function compute(state) { return { state, workflow: null }; }", + "export const initialState = {}; export async function compute(state) { return { state, trigger: null }; }", ); await waitFor(() => changes.length > 0, 3000); diff --git a/packages/daemon/src/__tests__/fixtures/crash-once-worker.mjs b/packages/daemon/src/__tests__/fixtures/crash-once-worker.mjs index 4324163..cddfdd0 100644 --- a/packages/daemon/src/__tests__/fixtures/crash-once-worker.mjs +++ b/packages/daemon/src/__tests__/fixtures/crash-once-worker.mjs @@ -35,7 +35,7 @@ process.on("message", (msg) => { type: "compute-result", sense: msg.sense, state: 42, - workflow: null, + trigger: null, }); } }); diff --git a/packages/daemon/src/__tests__/fixtures/mock-worker.mjs b/packages/daemon/src/__tests__/fixtures/mock-worker.mjs index da7b3e3..f2af59c 100644 --- a/packages/daemon/src/__tests__/fixtures/mock-worker.mjs +++ b/packages/daemon/src/__tests__/fixtures/mock-worker.mjs @@ -9,7 +9,7 @@ * * Behaviour: * - Sends { type: "ready" } on startup - * - On { type: "compute", sense } → sends back compute-result with state + workflow:null + * - On { type: "compute", sense } → sends back compute-result with state + trigger:null * - On { type: "shutdown" } → exits cleanly with code 0 */ @@ -27,7 +27,7 @@ process.on("message", (msg) => { type: "compute-result", sense: msg.sense, state: 42, - workflow: null, + trigger: null, }); } }); diff --git a/packages/daemon/src/__tests__/fixtures/slow-worker.mjs b/packages/daemon/src/__tests__/fixtures/slow-worker.mjs index e974bd4..2894b6e 100644 --- a/packages/daemon/src/__tests__/fixtures/slow-worker.mjs +++ b/packages/daemon/src/__tests__/fixtures/slow-worker.mjs @@ -22,7 +22,7 @@ process.on("message", (msg) => { type: "compute-result", sense: msg.sense, state: "late", - workflow: null, + trigger: null, }); }, 10_000); } diff --git a/packages/daemon/src/__tests__/kernel-workflow-integration.test.ts b/packages/daemon/src/__tests__/kernel-workflow-integration.test.ts index 3546ec7..6fd1a9d 100644 --- a/packages/daemon/src/__tests__/kernel-workflow-integration.test.ts +++ b/packages/daemon/src/__tests__/kernel-workflow-integration.test.ts @@ -57,7 +57,7 @@ function makeMockChild(pid = 1): MockChild { type: "compute-result", sense: m.sense, state: 42, - workflow: null, + trigger: null, }); }); } @@ -183,7 +183,7 @@ describe("kernel + workflowManager integration", () => { type: "compute-result", sense: "cpu-usage", state: { reason: "test" }, - workflow: { + trigger: { kind: "workflow", name: "my-workflow", maxRounds: 10, @@ -240,7 +240,7 @@ describe("kernel + workflowManager integration", () => { type: "compute-result", sense: "cpu-usage", state: { level: "critical" }, - workflow: { + trigger: { kind: "workflow", name: "alert-workflow", maxRounds: 5, @@ -295,7 +295,7 @@ describe("kernel + workflowManager integration", () => { type: "compute-result", sense: "cpu-usage", state: { seq: 1 }, - workflow: { + trigger: { kind: "workflow", name: "order-wf", maxRounds: 2, @@ -358,7 +358,7 @@ describe("kernel + workflowManager integration", () => { type: "compute-result", sense: "cpu-usage", state: 50, - workflow: null, + trigger: null, }); } @@ -394,7 +394,7 @@ describe("kernel + workflowManager integration", () => { type: "compute-result", sense: "cpu-usage", state: {}, - workflow: { + trigger: { kind: "shell", command: "echo nerve-shell-test", }, @@ -454,7 +454,7 @@ describe("kernel + workflowManager integration", () => { type: "compute-result", sense: "cpu-usage", state: { note: "log" }, - workflow: { + trigger: { kind: "workflow", name: "log-test-workflow", maxRounds: 10, @@ -527,7 +527,7 @@ describe("kernel + workflowManager integration", () => { type: "compute-result", sense: "cpu-usage", state: { phase: "reload" }, - workflow: { + trigger: { kind: "workflow", name: "new-workflow", maxRounds: 10, @@ -609,7 +609,7 @@ describe("kernel + workflowManager integration", () => { type: "compute-result", sense: "cpu-usage", state: { stale: true }, - workflow: { + trigger: { kind: "workflow", name: "old-workflow", maxRounds: 10, @@ -668,7 +668,7 @@ describe("kernel + workflowManager integration", () => { type: "compute-result", sense: "cpu-usage", state: { shutdownCase: true }, - workflow: { + trigger: { kind: "workflow", name: "shutdown-test", maxRounds: 10, diff --git a/packages/daemon/src/__tests__/kernel.test.ts b/packages/daemon/src/__tests__/kernel.test.ts index 1e894f5..21eeea8 100644 --- a/packages/daemon/src/__tests__/kernel.test.ts +++ b/packages/daemon/src/__tests__/kernel.test.ts @@ -41,7 +41,7 @@ function makeMockChild(pid = 1): MockChild { type: "compute-result", sense: m.sense, state: 42, - workflow: null, + trigger: null, }); }); } @@ -140,7 +140,7 @@ describe("kernel — message routing", () => { type: "compute-result", sense: "cpu-usage", state: 42, - workflow: null, + trigger: null, }); }).not.toThrow(); @@ -171,7 +171,7 @@ describe("kernel — message routing", () => { type: "compute-result", sense: "cpu-usage", state: 123, - workflow: null, + trigger: null, }); const rows = logStore.query({ source: "sense", diff --git a/packages/daemon/src/__tests__/sense-runtime.test.ts b/packages/daemon/src/__tests__/sense-runtime.test.ts index a7a7f22..93180d3 100644 --- a/packages/daemon/src/__tests__/sense-runtime.test.ts +++ b/packages/daemon/src/__tests__/sense-runtime.test.ts @@ -63,7 +63,7 @@ describe("executeCompute", () => { it("passes state into compute and persists returned state", async () => { const path = makeTempStatePath(); const runtime = makeRuntime( - async (s) => ({ state: { n: s.n + 1 }, workflow: null }), + async (s) => ({ state: { n: s.n + 1 }, trigger: null }), { n: 0 }, path, ); @@ -71,7 +71,7 @@ describe("executeCompute", () => { const result = await executeCompute(runtime); expect(result.ok).toBe(true); if (!result.ok) return; - expect(result.value).toEqual({ state: { n: 1 }, workflow: null }); + expect(result.value).toEqual({ state: { n: 1 }, trigger: null }); expect(runtime.state).toEqual({ n: 1 }); expect(JSON.parse(readFileSync(path, "utf8"))).toEqual({ n: 1 }); }); @@ -93,7 +93,7 @@ describe("executeCompute", () => { it("returns err when compute exceeds timeoutMs", async () => { const runtime = makeRuntime( async (s) => - new Promise((resolve) => setTimeout(() => resolve({ state: s, workflow: null }), 5_000)), + new Promise((resolve) => setTimeout(() => resolve({ state: s, trigger: null }), 5_000)), { n: 0 }, ); @@ -104,7 +104,7 @@ describe("executeCompute", () => { }); it("completes within timeout when compute is fast", async () => { - const runtime = makeRuntime(async (s) => ({ state: { n: s.n }, workflow: null }), { n: 42 }); + const runtime = makeRuntime(async (s) => ({ state: { n: s.n }, trigger: null }), { n: 42 }); const result = await executeCompute(runtime, 5_000); expect(result.ok).toBe(true); if (!result.ok) return; diff --git a/packages/daemon/src/__tests__/worker-pool.test.ts b/packages/daemon/src/__tests__/worker-pool.test.ts index 04a50d4..2a90b49 100644 --- a/packages/daemon/src/__tests__/worker-pool.test.ts +++ b/packages/daemon/src/__tests__/worker-pool.test.ts @@ -84,13 +84,13 @@ describe("createSenseWorkerPool", () => { type: "compute-result", sense: "s", state: 1, - workflow: null, + trigger: null, }); expect(onWorkerMessage).toHaveBeenCalledWith({ type: "compute-result", sense: "s", state: 1, - workflow: null, + trigger: null, }); }); diff --git a/packages/daemon/src/ipc.ts b/packages/daemon/src/ipc.ts index cf3f2fb..48a6237 100644 --- a/packages/daemon/src/ipc.ts +++ b/packages/daemon/src/ipc.ts @@ -70,7 +70,7 @@ export type ComputeResultMessage = { type: "compute-result"; sense: string; state: unknown; - workflow: SenseTrigger | null; + trigger: SenseTrigger | null; }; /** Worker → Parent: sense compute result includes a workflow to start */ @@ -255,26 +255,26 @@ function parseComputeResultMsg(obj: Record): Result> { +): Promise> { const controller = new AbortController(); let timer: ReturnType | undefined; diff --git a/packages/daemon/src/sense-worker.ts b/packages/daemon/src/sense-worker.ts index 0bf561f..4a4787b 100644 --- a/packages/daemon/src/sense-worker.ts +++ b/packages/daemon/src/sense-worker.ts @@ -43,9 +43,9 @@ function sendReady(): void { function sendComputeResult( sense: string, - value: { state: unknown; workflow: SenseTrigger | null }, + value: { state: unknown; trigger: SenseTrigger | null }, ): void { - send({ type: "compute-result", sense, state: value.state, workflow: value.workflow }); + send({ type: "compute-result", sense, state: value.state, trigger: value.trigger }); } function executeShellTriggerIfNeeded(nerveRoot: string, trigger: SenseTrigger | null): void { @@ -54,11 +54,24 @@ function executeShellTriggerIfNeeded(nerveRoot: string, trigger: SenseTrigger | shell: true, cwd: nerveRoot, detached: true, - stdio: "ignore", + stdio: ["ignore", "ignore", "pipe"], }); child.on("error", (err) => { process.stderr.write(`[sense-worker] shell trigger failed: ${err.message}\n`); }); + if (child.stderr) { + let stderrBuf = ""; + child.stderr.on("data", (chunk: Buffer) => { + stderrBuf += chunk.toString(); + }); + child.on("close", (code) => { + if (code !== null && code !== 0 && stderrBuf.length > 0) { + process.stderr.write( + `[sense-worker] shell trigger exited with code ${code}: ${stderrBuf.trimEnd()}\n`, + ); + } + }); + } child.unref(); } @@ -159,7 +172,7 @@ async function runCompute( return; } clearGracePeriodTimer(senseName); - executeShellTriggerIfNeeded(nerveRoot, result.value.workflow); + executeShellTriggerIfNeeded(nerveRoot, result.value.trigger); sendComputeResult(senseName, result.value); } catch (e: unknown) { const errMsg = e instanceof Error ? e.message : String(e);