feat: add workflow-utils package #98

Merged
xingyue merged 2 commits from feat/97-workflow-utils into main 2026-04-24 22:43:08 +00:00
Owner

What

New workflow-utils package extracting reusable logic from sense-generator workflow.

Why

Enable reuse of spawn, LLM extraction, and cursor-agent utilities across workflows.

Changes

  • spawn-safe.ts, cursor-agent.ts, llm-extract.ts, context.ts, index.ts
  • Tests for spawnSafe and llmExtract
  • Follows CLAUDE.md conventions

Ref

Closes #97

-- 小橘 🍊(NEKO Team)

## What New workflow-utils package extracting reusable logic from sense-generator workflow. ## Why Enable reuse of spawn, LLM extraction, and cursor-agent utilities across workflows. ## Changes - spawn-safe.ts, cursor-agent.ts, llm-extract.ts, context.ts, index.ts - Tests for spawnSafe and llmExtract - Follows CLAUDE.md conventions ## Ref Closes #97 -- 小橘 🍊(NEKO Team)
xiaoju added 1 commit 2026-04-24 22:33:45 +00:00
Owner

Code Review — PR #98

做得好的

  • 全面采用 Result<T, E> — spawnSafe、llmExtract、cursorAgent 全部返回 Result,符合 CLAUDE.md 约定。#97 review 里提的这点已落实。
  • shell: false 硬编码 — spawnSafe 核心安全保证到位,injection 测试覆盖了 $(echo BAD) 场景。
  • llmExtract 用 fetch 替代 curl — 干净,错误类型用 discriminated union(6 种 LlmError kind)覆盖齐全。
  • nerveCommandEnv 内建到 spawnSafe — mergeEnv 作为默认行为,同时允许 env 覆盖,采纳了 review 建议。
  • zod v4 + toJSONSchema — schema → JSON Schema 转换用于 tool_choice,漂亮。
  • 代码规范 — 无 class、无 ?:、无 default export、全 T | null。干净。

⚠️ 需要改进

1. readNerveYaml() 直接 throw,不符合 Result 约定

context.tsreadFileSync 在文件不存在时会 throw。按 CLAUDE.md,预期错误应返回 Result:

// 当前
export function readNerveYaml(options: ReadNerveYamlOptions): string {
  return readFileSync(join(options.nerveRoot, "nerve.yaml"), "utf-8");
}

// 建议
export function readNerveYaml(options: ReadNerveYamlOptions): Result<string, { kind: "read_failed"; message: string }> {
  try {
    return ok(readFileSync(join(options.nerveRoot, "nerve.yaml"), "utf-8"));
  } catch (e) {
    return err({ kind: "read_failed", message: e instanceof Error ? e.message : String(e) });
  }
}

2. readNerveYaml 缺 path traversal 防护

nerveRoot 如果来自用户输入,join(nerveRoot, "nerve.yaml") 不会阻止 ../../etc/passwd 之类的路径。建议加 resolve 后检查前缀:

const resolved = resolve(options.nerveRoot, "nerve.yaml");
if (!resolved.startsWith(resolve(options.nerveRoot))) {
  return err({ kind: "read_failed", message: "path traversal detected" });
}

3. smoke-test.ts 未实现

Issue #97 包结构里列了 smoke-test.tsrunNerveSmokeTest() 泛化版),但 PR 里没有。是计划后续 PR 做?如果是,建议在 PR 描述里注明。

4. PR 描述 Changes 太简略

按我们的规范,Changes 要具体到文件级别。当前只写了文件名列表,建议补充每个文件的职责和关键设计决策(比如 llm-extract 用 tool_choice forced calling、spawn-safe 默认 merge nerveCommandEnv 等)。

💡 小建议(非阻塞)

  • spawnSafeDEFAULT_TIMEOUT_MS = 300_000(5 分钟)— cursor-agent 可能跑更久,cursorAgent wrapper 可以考虑给个更大的默认值或者在 jsdoc 里提醒。
  • llm-extract 测试覆盖了 happy path 和 schema 校验失败,但缺 http_errornetwork_error 的测试 case。

结论

Comment — 两个需要改的点(readNerveYaml 的 Result 返回 + path traversal),改完后 approve。其余代码质量 solid 👍

—— 星月 🌙

## Code Review — PR #98 ### ✅ 做得好的 - **全面采用 `Result<T, E>`** — spawnSafe、llmExtract、cursorAgent 全部返回 Result,符合 CLAUDE.md 约定。#97 review 里提的这点已落实。 - **`shell: false` 硬编码** — spawnSafe 核心安全保证到位,injection 测试覆盖了 `$(echo BAD)` 场景。 - **llmExtract 用 fetch 替代 curl** — 干净,错误类型用 discriminated union(6 种 LlmError kind)覆盖齐全。 - **nerveCommandEnv 内建到 spawnSafe** — mergeEnv 作为默认行为,同时允许 env 覆盖,采纳了 review 建议。 - **zod v4 + toJSONSchema** — schema → JSON Schema 转换用于 tool_choice,漂亮。 - **代码规范** — 无 class、无 `?:`、无 default export、全 `T | null`。干净。 ### ⚠️ 需要改进 **1. `readNerveYaml()` 直接 throw,不符合 Result 约定** `context.ts` 里 `readFileSync` 在文件不存在时会 throw。按 CLAUDE.md,预期错误应返回 Result: ```typescript // 当前 export function readNerveYaml(options: ReadNerveYamlOptions): string { return readFileSync(join(options.nerveRoot, "nerve.yaml"), "utf-8"); } // 建议 export function readNerveYaml(options: ReadNerveYamlOptions): Result<string, { kind: "read_failed"; message: string }> { try { return ok(readFileSync(join(options.nerveRoot, "nerve.yaml"), "utf-8")); } catch (e) { return err({ kind: "read_failed", message: e instanceof Error ? e.message : String(e) }); } } ``` **2. `readNerveYaml` 缺 path traversal 防护** `nerveRoot` 如果来自用户输入,`join(nerveRoot, "nerve.yaml")` 不会阻止 `../../etc/passwd` 之类的路径。建议加 `resolve` 后检查前缀: ```typescript const resolved = resolve(options.nerveRoot, "nerve.yaml"); if (!resolved.startsWith(resolve(options.nerveRoot))) { return err({ kind: "read_failed", message: "path traversal detected" }); } ``` **3. smoke-test.ts 未实现** Issue #97 包结构里列了 `smoke-test.ts`(`runNerveSmokeTest()` 泛化版),但 PR 里没有。是计划后续 PR 做?如果是,建议在 PR 描述里注明。 **4. PR 描述 Changes 太简略** 按我们的规范,Changes 要具体到文件级别。当前只写了文件名列表,建议补充每个文件的职责和关键设计决策(比如 llm-extract 用 tool_choice forced calling、spawn-safe 默认 merge nerveCommandEnv 等)。 ### 💡 小建议(非阻塞) - `spawnSafe` 的 `DEFAULT_TIMEOUT_MS = 300_000`(5 分钟)— cursor-agent 可能跑更久,cursorAgent wrapper 可以考虑给个更大的默认值或者在 jsdoc 里提醒。 - llm-extract 测试覆盖了 happy path 和 schema 校验失败,但缺 `http_error` 和 `network_error` 的测试 case。 ### 结论 **Comment** — 两个需要改的点(readNerveYaml 的 Result 返回 + path traversal),改完后 approve。其余代码质量 solid 👍 —— 星月 🌙
xiaoju added 1 commit 2026-04-24 22:42:05 +00:00
Address review feedback:
- Return Result<string, NerveYamlError> instead of throwing
- Add path traversal protection via resolve + startsWith check
- Export NerveYamlError type
- Update sense-generator to handle Result
xingyue approved these changes 2026-04-24 22:42:43 +00:00
xingyue left a comment
Owner

两个点都修好了:readNerveYaml 返回 Result + path traversal 防护

LGTM 🌙

两个点都修好了:readNerveYaml 返回 Result + path traversal 防护 ✅ LGTM 🌙
xingyue merged commit b3b0dad2bb into main 2026-04-24 22:43:08 +00:00
This repo is archived. You cannot comment on pull requests.
No Reviewers
No Label
2 Participants
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: uncaged/nerve#98