Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e287e07dab | |||
| 239dfffb28 | |||
| 6ccb33bf40 | |||
| 0c95a9d716 | |||
| aa64ea86ca | |||
| 431627019a | |||
| 915584ff11 | |||
| 3644cce2c8 |
@@ -12,6 +12,12 @@ export type {
|
||||
ComputeResult,
|
||||
} from "./config.js";
|
||||
export type { Signal, SenseInfo } from "./sense.js";
|
||||
export type {
|
||||
SenseBlobStore,
|
||||
SenseComputeOptions,
|
||||
SensePeerMap,
|
||||
SenseComputeFn,
|
||||
} from "./sense-contract.js";
|
||||
export { labelSenseTrigger, senseTriggerLabels } from "./sense-trigger-labels.js";
|
||||
export type {
|
||||
WorkflowMessage,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ComputeResult } from "./config.js";
|
||||
|
||||
/**
|
||||
* Minimal read/write/exists interface for the CAS blob store injected into
|
||||
* sense compute functions (RFC-001 §8). Matches BlobStore from @uncaged/nerve-store.
|
||||
*/
|
||||
export type SenseBlobStore = {
|
||||
write: (content: string | Uint8Array | Buffer) => string;
|
||||
read: (hash: string) => Buffer | null;
|
||||
exists: (hash: string) => boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options injected by the engine into every compute invocation.
|
||||
* `signal` is always present; `blobs` is only available when running
|
||||
* inside the sense worker (RFC-001 §8).
|
||||
*/
|
||||
export type SenseComputeOptions = {
|
||||
signal: AbortSignal;
|
||||
blobs: SenseBlobStore | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Read-only map of peer sense name → their Drizzle DB.
|
||||
* `TDb` defaults to `unknown` so callers that don't need peer queries can
|
||||
* leave it unspecified.
|
||||
*/
|
||||
export type SensePeerMap<TDb = unknown> = Readonly<Record<string, TDb>>;
|
||||
|
||||
/**
|
||||
* The function signature every sense `src/index.ts` must export as a named
|
||||
* `compute` export.
|
||||
*
|
||||
* - `db` — the sense's own Drizzle DB instance (read-write).
|
||||
* - `peers` — read-only map of peer sense name → Drizzle DB.
|
||||
* - `options` — injected options; `options.blobs` is available inside the
|
||||
* sense worker; `options.signal` carries the AbortSignal.
|
||||
*
|
||||
* Return `null` to stay silent, or `{ signal, workflow }` to emit a Signal
|
||||
* (and optionally trigger a Workflow).
|
||||
*/
|
||||
export type SenseComputeFn<T = unknown, TDb = unknown> = (
|
||||
db: TDb,
|
||||
peers: SensePeerMap<TDb>,
|
||||
options: SenseComputeOptions,
|
||||
) => Promise<ComputeResult<T>>;
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@uncaged/nerve-role-committer",
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": ["dist"],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
|
||||
"build": "rslib build",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/nerve-core": "workspace:*",
|
||||
"@uncaged/nerve-workflow-utils": "workspace:*",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rslib/core": "^0.21.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from "@rslib/core";
|
||||
|
||||
export default defineConfig({
|
||||
lib: [
|
||||
{
|
||||
format: "esm",
|
||||
dts: true,
|
||||
},
|
||||
],
|
||||
source: {
|
||||
entry: {
|
||||
index: "src/index.ts",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
target: "node",
|
||||
cleanDistPath: true,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole, decorateRole, onFail, withDryRun } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const committerMetaSchema = z.object({
|
||||
committed: z
|
||||
.boolean()
|
||||
.describe("true if branch created, changes committed, and pushed successfully"),
|
||||
});
|
||||
export type CommitterMeta = z.infer<typeof committerMetaSchema>;
|
||||
|
||||
function committerPrompt(threadId: string): string {
|
||||
return `You are the committer agent. The coder finished with a passing build; your job is to branch, commit, and push.
|
||||
|
||||
1. Read the workflow thread: \`nerve thread show ${threadId}\` — understand what was planned, coded, and reviewed.
|
||||
2. Run \`git status\`. If nothing to commit, set committed=false.
|
||||
3. Create a feature branch: infer a good \`fix/<slug>\` or \`feat/<slug>\` name from the thread context.
|
||||
4. \`git add -A\`
|
||||
5. Write a conventional commit message based on the thread context.
|
||||
6. \`git commit -m "<message>"\` — do NOT pass \`--author\`, use repo git config.
|
||||
7. \`git push -u origin <branch>\`
|
||||
|
||||
**committed=true** only if branch was created, commit succeeded, and **push** succeeded.
|
||||
|
||||
End your reply with a JSON line:
|
||||
\`\`\`json
|
||||
{ "committed": true }
|
||||
\`\`\`
|
||||
or
|
||||
\`\`\`json
|
||||
{ "committed": false }
|
||||
\`\`\``;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a committer role that branches, commits, and pushes changes.
|
||||
* The agent reads the workflow thread to infer branch name, scope, and commit message.
|
||||
*/
|
||||
export function createCommitterRole(
|
||||
adapter: AgentFn,
|
||||
extract: LlmExtractorConfig,
|
||||
): Role<CommitterMeta> {
|
||||
const inner = createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => committerPrompt(start.meta.threadId),
|
||||
committerMetaSchema,
|
||||
extract,
|
||||
);
|
||||
|
||||
return decorateRole(inner, [
|
||||
withDryRun({ label: "committer", meta: { committed: true } as CommitterMeta }),
|
||||
onFail({ label: "committer", meta: { committed: false } as CommitterMeta }),
|
||||
]) as Role<CommitterMeta>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@uncaged/nerve-role-reviewer",
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": ["dist"],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
|
||||
"build": "rslib build",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/nerve-core": "workspace:*",
|
||||
"@uncaged/nerve-workflow-utils": "workspace:*",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rslib/core": "^0.21.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from "@rslib/core";
|
||||
|
||||
export default defineConfig({
|
||||
lib: [
|
||||
{
|
||||
format: "esm",
|
||||
dts: true,
|
||||
},
|
||||
],
|
||||
source: {
|
||||
entry: {
|
||||
index: "src/index.ts",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
target: "node",
|
||||
cleanDistPath: true,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export { createReviewerRole, reviewerMetaSchema } from "./reviewer.js";
|
||||
export type { ReviewerMeta, ReviewerConfig } from "./reviewer.js";
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const reviewerMetaSchema = z.object({
|
||||
approved: z.boolean().describe("true if the diff is clean and ready to merge"),
|
||||
});
|
||||
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
|
||||
|
||||
export type ReviewerConfig = {
|
||||
/** Working directory of the project */
|
||||
cwd: string;
|
||||
/** Path to conventions/standards file, relative to cwd. Default: "CONVENTIONS.md" */
|
||||
conventionsPath: string | null;
|
||||
/** Extra checklist items appended to the review criteria */
|
||||
extraChecks: ReadonlyArray<string>;
|
||||
};
|
||||
|
||||
const defaults: ReviewerConfig = {
|
||||
cwd: ".",
|
||||
conventionsPath: "CONVENTIONS.md",
|
||||
extraChecks: [],
|
||||
};
|
||||
|
||||
function reviewerPrompt({
|
||||
threadId,
|
||||
config,
|
||||
}: { threadId: string; config: ReviewerConfig }): string {
|
||||
const { cwd, conventionsPath, extraChecks } = config;
|
||||
|
||||
const conventionsBlock = conventionsPath
|
||||
? `Read project conventions: \`cat ${cwd}/${conventionsPath}\`\n`
|
||||
: "";
|
||||
|
||||
const extraBlock =
|
||||
extraChecks.length > 0
|
||||
? `\n### 📋 Project-specific checks\n${extraChecks.map((c) => `- ${c}`).join("\n")}\n`
|
||||
: "";
|
||||
|
||||
return `You are a **code reviewer**. You run after the coder and before the tester.
|
||||
|
||||
**IMPORTANT: The project is at \`${cwd}\`. Always \`cd ${cwd}\` first.**
|
||||
|
||||
Read the workflow thread for context: \`nerve thread ${threadId}\`
|
||||
${conventionsBlock}
|
||||
## Your job — static analysis of the git diff
|
||||
|
||||
Run these commands and analyze the output:
|
||||
|
||||
1. **\`cd ${cwd} && git diff --stat\`** — see what files changed
|
||||
2. **\`cd ${cwd} && git diff\`** — read the actual diff
|
||||
3. **\`cd ${cwd} && git status --short\`** — check for untracked files
|
||||
|
||||
## Checklist
|
||||
|
||||
### 🔴 Reject (approved: false) — tell coder exactly what to fix
|
||||
- **Garbage files**: build artifacts, lockfiles, IDE config that shouldn't be committed
|
||||
- **Secrets/credentials**: API keys, tokens, passwords hardcoded in the diff
|
||||
- **Unrelated changes**: files modified outside the scope of the task
|
||||
${conventionsPath ? `- **Convention violations**: patterns that contradict ${conventionsPath}\n` : ""}${extraBlock}
|
||||
### ✅ Approve (approved: true) — no comment needed
|
||||
- Diff is clean, focused, follows project standards
|
||||
|
||||
End with:
|
||||
\`\`\`json
|
||||
{ "approved": true }
|
||||
\`\`\`
|
||||
or
|
||||
\`\`\`json
|
||||
{ "approved": false }
|
||||
\`\`\``;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reviewer role that performs static analysis on git diffs.
|
||||
* Checks for garbage files, secrets, unrelated changes, and project conventions.
|
||||
*/
|
||||
export function createReviewerRole(
|
||||
adapter: AgentFn,
|
||||
extract: LlmExtractorConfig,
|
||||
config: Partial<ReviewerConfig> = {},
|
||||
): Role<ReviewerMeta> {
|
||||
const resolved: ReviewerConfig = { ...defaults, ...config };
|
||||
return createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => reviewerPrompt({ threadId: start.meta.threadId, config: resolved }),
|
||||
reviewerMetaSchema,
|
||||
extract,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@uncaged/nerve-workflow-meta",
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": ["dist"],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
|
||||
"build": "rslib build",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/nerve-core": "workspace:*",
|
||||
"@uncaged/nerve-role-committer": "workspace:*",
|
||||
"@uncaged/nerve-role-reviewer": "workspace:*",
|
||||
"@uncaged/nerve-workflow-utils": "workspace:*",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rslib/core": "^0.21.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from "@rslib/core";
|
||||
|
||||
export default defineConfig({
|
||||
lib: [
|
||||
{
|
||||
format: "esm",
|
||||
dts: true,
|
||||
},
|
||||
],
|
||||
source: {
|
||||
entry: {
|
||||
index: "src/index.ts",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
target: "node",
|
||||
cleanDistPath: true,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { AgentFn, WorkflowDefinition } from "@uncaged/nerve-core";
|
||||
import { createCommitterRole } from "@uncaged/nerve-role-committer";
|
||||
import { createReviewerRole } from "@uncaged/nerve-role-reviewer";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
|
||||
import { moderator } from "./moderator.js";
|
||||
import type { SenseMeta } from "./moderator.js";
|
||||
import { createCoderRole } from "./roles/coder.js";
|
||||
import { createPlannerRole } from "./roles/planner.js";
|
||||
import { createTesterRole } from "./roles/tester.js";
|
||||
|
||||
export type CreateDevelopSenseDeps = {
|
||||
defaultAdapter: AgentFn;
|
||||
adapters: Partial<Record<keyof SenseMeta, AgentFn>> | null;
|
||||
extract: LlmExtractorConfig;
|
||||
cwd: string;
|
||||
};
|
||||
|
||||
export function createDevelopSenseWorkflow({
|
||||
defaultAdapter,
|
||||
adapters,
|
||||
extract,
|
||||
cwd,
|
||||
}: CreateDevelopSenseDeps): WorkflowDefinition<SenseMeta> {
|
||||
const a = (role: keyof SenseMeta) => adapters?.[role] ?? defaultAdapter;
|
||||
const roles = {
|
||||
planner: createPlannerRole(a("planner"), extract),
|
||||
coder: createCoderRole(a("coder"), extract),
|
||||
reviewer: createReviewerRole(a("reviewer"), extract, {
|
||||
cwd,
|
||||
conventionsPath: "CONVENTIONS.md",
|
||||
}),
|
||||
tester: createTesterRole(a("tester"), extract, cwd),
|
||||
committer: createCommitterRole(a("committer"), extract),
|
||||
};
|
||||
|
||||
return {
|
||||
name: "develop-sense",
|
||||
roles,
|
||||
moderator,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { END } from "@uncaged/nerve-core";
|
||||
import type { Moderator } from "@uncaged/nerve-core";
|
||||
import type { CommitterMeta } from "@uncaged/nerve-role-committer";
|
||||
import type { ReviewerMeta } from "@uncaged/nerve-role-reviewer";
|
||||
import type { CoderMeta } from "./roles/coder.js";
|
||||
import type { PlannerMeta } from "./roles/planner.js";
|
||||
import type { TesterMeta } from "./roles/tester.js";
|
||||
|
||||
export type SenseMeta = {
|
||||
planner: PlannerMeta;
|
||||
coder: CoderMeta;
|
||||
reviewer: ReviewerMeta;
|
||||
tester: TesterMeta;
|
||||
committer: CommitterMeta;
|
||||
};
|
||||
|
||||
const MAX_CODER_ROUNDS = 20;
|
||||
const MAX_TOTAL_REJECTIONS = 10;
|
||||
|
||||
function coderRounds(steps: { role: string }[]): number {
|
||||
return steps.filter((s) => s.role === "coder").length;
|
||||
}
|
||||
|
||||
function totalRejections(steps: { role: string; meta: unknown }[]): number {
|
||||
return steps.filter((s) => {
|
||||
if (s.role === "reviewer") return !(s.meta as Record<string, boolean>).approved;
|
||||
if (s.role === "tester") return !(s.meta as Record<string, boolean>).passed;
|
||||
if (s.role === "committer") return !(s.meta as Record<string, boolean>).committed;
|
||||
return false;
|
||||
}).length;
|
||||
}
|
||||
|
||||
function canRetryCoder(steps: { role: string; meta: unknown }[]): boolean {
|
||||
return coderRounds(steps) < MAX_CODER_ROUNDS && totalRejections(steps) < MAX_TOTAL_REJECTIONS;
|
||||
}
|
||||
|
||||
export const moderator: Moderator<SenseMeta> = (context) => {
|
||||
if (context.steps.length === 0) return "planner";
|
||||
|
||||
const last = context.steps[context.steps.length - 1];
|
||||
|
||||
if (last.role === "planner") return "coder";
|
||||
|
||||
if (last.role === "coder") {
|
||||
if (last.meta.filesCreated) return "reviewer";
|
||||
return canRetryCoder(context.steps) ? "coder" : END;
|
||||
}
|
||||
|
||||
if (last.role === "reviewer") {
|
||||
if (last.meta.approved) return "tester";
|
||||
return canRetryCoder(context.steps) ? "coder" : END;
|
||||
}
|
||||
|
||||
if (last.role === "tester") {
|
||||
if (last.meta.passed) return "committer";
|
||||
return canRetryCoder(context.steps) ? "coder" : END;
|
||||
}
|
||||
|
||||
if (last.role === "committer") {
|
||||
if (last.meta.committed) return END;
|
||||
return canRetryCoder(context.steps) ? "coder" : END;
|
||||
}
|
||||
|
||||
return END;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const coderMetaSchema = z.object({
|
||||
filesCreated: z.boolean().describe("true if the sense files were created"),
|
||||
});
|
||||
export type CoderMeta = z.infer<typeof coderMetaSchema>;
|
||||
|
||||
export function coderPrompt({ threadId }: { threadId: string }): string {
|
||||
return `Read the workflow thread for the planner's sense design and any tester feedback: \`nerve thread ${threadId}\`
|
||||
Read the nerve-dev skill for sense file structure and conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\`
|
||||
|
||||
## Your task
|
||||
|
||||
Implement (or fix) the sense the planner designed. If there is tester feedback in the thread, fix the issues it identified.
|
||||
|
||||
## Multi-step approach
|
||||
|
||||
You do NOT need to finish everything in one pass. You may return \`done: false\` to continue in the next iteration.
|
||||
|
||||
## File structure for each sense
|
||||
|
||||
- \`senses/<name>/src/index.ts\` — TypeScript compute source; import schema as \`./schema.ts\`
|
||||
- \`senses/<name>/src/schema.ts\` — Drizzle schema (TypeScript)
|
||||
- \`senses/<name>/migrations/\` — Drizzle migration files (at sense root, not inside src/)
|
||||
- \`senses/<name>/package.json\` — with esbuild build script
|
||||
- \`senses/<name>/index.js\` — bundled output generated by \`pnpm build\` (do NOT edit by hand)
|
||||
|
||||
Look at existing senses for the package.json template and patterns.
|
||||
|
||||
## When to return done: true
|
||||
|
||||
Return \`done: true\` ONLY when ALL of the following are true:
|
||||
- All required files are created
|
||||
- \`pnpm install --no-cache && pnpm build\` succeeds (run it!)
|
||||
- \`nerve.yaml\` is updated with the sense config
|
||||
|
||||
Return \`done: false\` if you made progress but there is still work to do.`;
|
||||
}
|
||||
|
||||
export function createCoderRole(adapter: AgentFn, extract: LlmExtractorConfig): Role<CoderMeta> {
|
||||
return createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => coderPrompt({ threadId: start.meta.threadId }),
|
||||
coderMetaSchema,
|
||||
extract,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const plannerMetaSchema = z.object({
|
||||
senseName: z.string().describe("kebab-case sense name from the plan"),
|
||||
});
|
||||
export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
|
||||
|
||||
export function plannerPrompt({ threadId }: { threadId: string }): string {
|
||||
return `You are planning a new Nerve sense.
|
||||
|
||||
Read the workflow thread for the user's request: \`nerve thread ${threadId}\`
|
||||
Read the nerve-dev skill for sense conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\`
|
||||
Also look at existing senses in the \`senses/\` directory for patterns.
|
||||
|
||||
Pick a good kebab-case name for this sense. Produce a PLAN (not code) in markdown:
|
||||
|
||||
## Sense Design
|
||||
### Name — kebab-case
|
||||
### Fields — name, type (integer/real/text), description
|
||||
### Compute Logic — step-by-step, specific Node.js APIs or shell commands
|
||||
### Trigger Config — group, interval, throttle, timeout
|
||||
|
||||
Output ONLY the plan. Be precise and implementation-ready.`;
|
||||
}
|
||||
|
||||
export function createPlannerRole(
|
||||
adapter: AgentFn,
|
||||
extract: LlmExtractorConfig,
|
||||
): Role<PlannerMeta> {
|
||||
return createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }),
|
||||
plannerMetaSchema,
|
||||
extract,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const testerMetaSchema = z.object({
|
||||
passed: z.boolean().describe("true if all e2e checks passed"),
|
||||
});
|
||||
export type TesterMeta = z.infer<typeof testerMetaSchema>;
|
||||
|
||||
export function testerPrompt({
|
||||
threadId,
|
||||
nerveRoot,
|
||||
}: { threadId: string; nerveRoot: string }): string {
|
||||
return `You are testing a newly created Nerve sense end-to-end.
|
||||
|
||||
**IMPORTANT: The Nerve workspace is at \`${nerveRoot}\`. All paths below are relative to this directory. Always \`cd ${nerveRoot}\` first.**
|
||||
|
||||
Read the workflow thread for context: \`nerve thread ${threadId}\`
|
||||
Read the nerve-dev skill for expected file structure: \`cat ${nerveRoot}/node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\`
|
||||
|
||||
Verify the full lifecycle in this order:
|
||||
|
||||
1. **File check** — all required sense files exist:
|
||||
- \`senses/<name>/src/index.ts\`
|
||||
- \`senses/<name>/src/schema.ts\`
|
||||
- \`senses/<name>/migrations/\`
|
||||
- \`senses/<name>/package.json\`
|
||||
|
||||
2. **Build** — run inside the sense directory:
|
||||
\`\`\`
|
||||
cd ${nerveRoot}/senses/<name> && pnpm install --no-cache && pnpm build
|
||||
\`\`\`
|
||||
Must produce \`index.js\` at sense root without errors.
|
||||
|
||||
3. **Config check** — \`nerve validate\` passes, confirming nerve.yaml is valid.
|
||||
|
||||
4. **Sense list** — \`nerve sense list\` shows the sense.
|
||||
|
||||
5. **Trigger** — \`nerve sense trigger <name>\` completes without error.
|
||||
|
||||
6. **Query** — \`nerve sense query <name>\` — retry up to 20s until rows appear.
|
||||
|
||||
If any step fails, include the relevant error output.
|
||||
|
||||
Output a clear summary: what you checked, what passed, what failed, and why.`;
|
||||
}
|
||||
|
||||
export function createTesterRole(
|
||||
adapter: AgentFn,
|
||||
extract: LlmExtractorConfig,
|
||||
nerveRoot: string,
|
||||
): Role<TesterMeta> {
|
||||
return createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => testerPrompt({ threadId: start.meta.threadId, nerveRoot }),
|
||||
testerMetaSchema,
|
||||
extract,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { AgentFn, WorkflowDefinition } from "@uncaged/nerve-core";
|
||||
import { createCommitterRole } from "@uncaged/nerve-role-committer";
|
||||
import { createReviewerRole } from "@uncaged/nerve-role-reviewer";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
|
||||
import { moderator } from "./moderator.js";
|
||||
import type { WorkflowMeta } from "./moderator.js";
|
||||
import { createCoderRole } from "./roles/coder.js";
|
||||
import { createPlannerRole } from "./roles/planner.js";
|
||||
import { createTesterRole } from "./roles/tester.js";
|
||||
|
||||
export type CreateDevelopWorkflowDeps = {
|
||||
defaultAdapter: AgentFn;
|
||||
adapters: Partial<Record<keyof WorkflowMeta, AgentFn>> | null;
|
||||
extract: LlmExtractorConfig;
|
||||
nerveRoot: string;
|
||||
};
|
||||
|
||||
export function createDevelopWorkflowWorkflow({
|
||||
defaultAdapter,
|
||||
adapters,
|
||||
extract,
|
||||
nerveRoot,
|
||||
}: CreateDevelopWorkflowDeps): WorkflowDefinition<WorkflowMeta> {
|
||||
const a = (role: keyof WorkflowMeta) => adapters?.[role] ?? defaultAdapter;
|
||||
const roles = {
|
||||
planner: createPlannerRole(a("planner"), extract),
|
||||
coder: createCoderRole(a("coder"), extract),
|
||||
reviewer: createReviewerRole(a("reviewer"), extract, {
|
||||
cwd: nerveRoot,
|
||||
conventionsPath: "CONVENTIONS.md",
|
||||
}),
|
||||
tester: createTesterRole(a("tester"), extract, nerveRoot),
|
||||
committer: createCommitterRole(a("committer"), extract),
|
||||
};
|
||||
|
||||
return {
|
||||
name: "develop-workflow",
|
||||
roles,
|
||||
moderator,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { END } from "@uncaged/nerve-core";
|
||||
import type { Moderator } from "@uncaged/nerve-core";
|
||||
import type { CommitterMeta } from "@uncaged/nerve-role-committer";
|
||||
import type { ReviewerMeta } from "@uncaged/nerve-role-reviewer";
|
||||
import type { CoderMeta } from "./roles/coder.js";
|
||||
import type { PlannerMeta } from "./roles/planner.js";
|
||||
import type { TesterMeta } from "./roles/tester.js";
|
||||
|
||||
export type WorkflowMeta = {
|
||||
planner: PlannerMeta;
|
||||
coder: CoderMeta;
|
||||
reviewer: ReviewerMeta;
|
||||
tester: TesterMeta;
|
||||
committer: CommitterMeta;
|
||||
};
|
||||
|
||||
const MAX_CODER_ROUNDS = 20;
|
||||
const MAX_TOTAL_REJECTIONS = 10;
|
||||
|
||||
function coderRounds(steps: { role: string }[]): number {
|
||||
return steps.filter((s) => s.role === "coder").length;
|
||||
}
|
||||
|
||||
function totalRejections(steps: { role: string; meta: unknown }[]): number {
|
||||
return steps.filter((s) => {
|
||||
if (s.role === "reviewer") return !(s.meta as Record<string, boolean>).approved;
|
||||
if (s.role === "tester") return !(s.meta as Record<string, boolean>).passed;
|
||||
if (s.role === "committer") return !(s.meta as Record<string, boolean>).committed;
|
||||
return false;
|
||||
}).length;
|
||||
}
|
||||
|
||||
function canRetryCoder(steps: { role: string; meta: unknown }[]): boolean {
|
||||
return coderRounds(steps) < MAX_CODER_ROUNDS && totalRejections(steps) < MAX_TOTAL_REJECTIONS;
|
||||
}
|
||||
|
||||
export const moderator: Moderator<WorkflowMeta> = (context) => {
|
||||
if (context.steps.length === 0) return "planner";
|
||||
|
||||
const last = context.steps[context.steps.length - 1];
|
||||
|
||||
if (last.role === "planner") {
|
||||
return last.meta.ready ? "coder" : END;
|
||||
}
|
||||
|
||||
if (last.role === "coder") {
|
||||
if (last.meta.done) return "reviewer";
|
||||
return canRetryCoder(context.steps) ? "coder" : END;
|
||||
}
|
||||
|
||||
if (last.role === "reviewer") {
|
||||
if (last.meta.approved) return "tester";
|
||||
return canRetryCoder(context.steps) ? "coder" : END;
|
||||
}
|
||||
|
||||
if (last.role === "tester") {
|
||||
if (last.meta.passed) return "committer";
|
||||
return canRetryCoder(context.steps) ? "coder" : END;
|
||||
}
|
||||
|
||||
if (last.role === "committer") {
|
||||
if (last.meta.committed) return END;
|
||||
return canRetryCoder(context.steps) ? "coder" : END;
|
||||
}
|
||||
|
||||
return END;
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const coderMetaSchema = z.object({
|
||||
done: z.boolean().describe("true if the workflow files were created and build passes"),
|
||||
});
|
||||
export type CoderMeta = z.infer<typeof coderMetaSchema>;
|
||||
|
||||
export function coderPrompt({ threadId }: { threadId: string }): string {
|
||||
return `Read the workflow thread to get the planner's design and any reviewer/tester/committer feedback: \`nerve thread ${threadId}\`
|
||||
Read the nerve-dev skill for workflow file structure and conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\`
|
||||
Also look at existing workflows in the \`workflows/\` directory for patterns.
|
||||
|
||||
## Your task
|
||||
|
||||
Implement the planner's design. This may be **creating a new workflow** or **modifying an existing one**. If there is reviewer, tester, or committer feedback in the thread, fix the issues they identified.
|
||||
|
||||
**IMPORTANT:** The thread contains both the **initial user prompt** (the first message) and the **planner's design**. Read both carefully:
|
||||
- The **initial prompt** contains the user's specific requirements for role behavior, tools to use, and acceptance criteria
|
||||
- The **planner's design** contains the architecture, file structure, and routing logic
|
||||
- When writing role prompts, follow the user's behavioral requirements from the initial prompt — do not invent your own interpretation
|
||||
|
||||
## Multi-step approach
|
||||
|
||||
You do NOT need to finish everything in one pass. You may return \`done: false\` to continue in the next iteration. For example:
|
||||
1. First pass: scaffold files / make structural changes
|
||||
2. Second pass: implement role logic
|
||||
3. Third pass: fix build/lint errors
|
||||
|
||||
## Workflow file structure
|
||||
|
||||
Each workflow must have:
|
||||
- \`workflows/<name>/index.ts\` — WorkflowDefinition default export
|
||||
- \`workflows/<name>/build.ts\` — factory function
|
||||
- \`workflows/<name>/moderator.ts\` — moderator + meta types
|
||||
- \`workflows/<name>/roles/<role>.ts\` — meta schema and prompt function per role
|
||||
- \`workflows/<name>/package.json\` — with esbuild build script
|
||||
- \`workflows/<name>/tsconfig.json\` — TypeScript config
|
||||
|
||||
For **new workflows**, also update \`nerve.yaml\` with \`workflows.<name>\`.
|
||||
|
||||
## Rules
|
||||
|
||||
- Keep the WorkflowDefinition<WorkflowMeta> pattern
|
||||
- No dynamic import()
|
||||
- Use types (not interfaces)
|
||||
- Meta should be simple routing signals (single boolean per role)
|
||||
- Write compile-ready TypeScript
|
||||
|
||||
## When to return done: true
|
||||
|
||||
Return \`done: true\` ONLY when ALL of the following are true:
|
||||
- All changes from the plan are implemented
|
||||
- \`cd workflows/<name> && pnpm install --no-cache && pnpm build\` succeeds (run it!)
|
||||
- No lint or type errors remain
|
||||
|
||||
Return \`done: false\` if you made progress but there is still work to do, or if build/lint has errors you plan to fix in the next iteration.`;
|
||||
}
|
||||
|
||||
export function createCoderRole(adapter: AgentFn, extract: LlmExtractorConfig): Role<CoderMeta> {
|
||||
return createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => coderPrompt({ threadId: start.meta.threadId }),
|
||||
coderMetaSchema,
|
||||
extract,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const plannerMetaSchema = z.object({
|
||||
ready: z.boolean().describe("true if requirements are clear and a workflow can be implemented"),
|
||||
});
|
||||
export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
|
||||
|
||||
export function plannerPrompt({ threadId }: { threadId: string }): string {
|
||||
return `You are a Nerve workflow planner. You can **create new workflows** or **modify existing ones**.
|
||||
|
||||
Read the workflow thread for the user's request: \`nerve thread ${threadId}\`
|
||||
Read the nerve-dev skill for workflow conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\`
|
||||
List existing workflows: \`ls workflows/\`
|
||||
|
||||
## Determine the task type
|
||||
|
||||
1. If the user wants to **modify an existing workflow** — read its current code (\`cat workflows/<name>/moderator.ts\`, \`cat workflows/<name>/build.ts\`, \`ls workflows/<name>/roles/\`, etc.) and understand its current structure before planning changes.
|
||||
2. If the user wants to **create a new workflow** — look at existing workflows in \`workflows/\` for patterns to follow.
|
||||
|
||||
## Produce a PLAN (not code) in markdown
|
||||
|
||||
For **new workflows**:
|
||||
- Workflow name (kebab-case)
|
||||
- Roles list (name, purpose, tool)
|
||||
- Flow transitions / moderator routing logic
|
||||
- Validation loops design
|
||||
- External dependencies
|
||||
- Data flow between roles
|
||||
|
||||
For **modifications to existing workflows**:
|
||||
- Workflow name (existing)
|
||||
- What changes are needed and why
|
||||
- Files to add/modify/delete
|
||||
- Impact on moderator routing logic (this workflow's typical order is planner → coder → reviewer → tester → committer)
|
||||
- Backward compatibility considerations (if any)
|
||||
|
||||
**For every role (new or modified)**, include a **Role Behavior** section that describes:
|
||||
- What the role should do, check, or produce
|
||||
- What tools or commands it should use
|
||||
- What criteria determine its meta output (e.g. approved/passed/done)
|
||||
- Preserve the user's specific requirements verbatim — do NOT summarize away details
|
||||
|
||||
If requirements are NOT clear, describe what is missing or ambiguous.
|
||||
|
||||
End your response with a JSON block:
|
||||
\`\`\`json
|
||||
{ "ready": true }
|
||||
\`\`\`
|
||||
or
|
||||
\`\`\`json
|
||||
{ "ready": false }
|
||||
\`\`\``;
|
||||
}
|
||||
|
||||
export function createPlannerRole(
|
||||
adapter: AgentFn,
|
||||
extract: LlmExtractorConfig,
|
||||
): Role<PlannerMeta> {
|
||||
return createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }),
|
||||
plannerMetaSchema,
|
||||
extract,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const testerMetaSchema = z.object({
|
||||
passed: z.boolean().describe("true if all validation checks passed"),
|
||||
});
|
||||
export type TesterMeta = z.infer<typeof testerMetaSchema>;
|
||||
|
||||
export function testerPrompt({
|
||||
threadId,
|
||||
nerveRoot,
|
||||
}: { threadId: string; nerveRoot: string }): string {
|
||||
return `You are testing a Nerve workflow — either newly created or recently modified.
|
||||
|
||||
**IMPORTANT: The Nerve workspace is at \`${nerveRoot}\`. All paths below are relative to this directory. Always \`cd ${nerveRoot}\` first.**
|
||||
|
||||
Read the workflow thread for context: \`nerve thread ${threadId}\`
|
||||
Read the nerve-dev skill for expected file structure: \`cat ${nerveRoot}/node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\`
|
||||
|
||||
Get the workflow name from the thread (the planner's output).
|
||||
|
||||
Verify the full lifecycle in this order:
|
||||
|
||||
1. **File check** — all required workflow files exist (under \`${nerveRoot}/\`):
|
||||
- \`workflows/<name>/index.ts\`
|
||||
- \`workflows/<name>/build.ts\`
|
||||
- \`workflows/<name>/moderator.ts\`
|
||||
- \`workflows/<name>/roles/\` with one \`.ts\` file per role
|
||||
- \`workflows/<name>/package.json\`
|
||||
|
||||
2. **Build** — run inside the workflow directory:
|
||||
\`\`\`
|
||||
cd ${nerveRoot}/workflows/<name> && pnpm install --no-cache && pnpm build
|
||||
\`\`\`
|
||||
Must produce \`dist/index.js\` without errors.
|
||||
|
||||
3. **Config check** — \`cd ${nerveRoot} && nerve validate\` passes, confirming nerve.yaml is valid.
|
||||
|
||||
4. **Workflow list** — \`nerve workflow list\` shows the workflow.
|
||||
|
||||
5. **Trigger test** — \`nerve workflow trigger <name> --dry-run\` if available, otherwise just confirm the workflow appears in \`nerve workflow status\`.
|
||||
|
||||
If any step fails, include the relevant error output.
|
||||
|
||||
Output a clear summary: what you checked, what passed, what failed, and why.`;
|
||||
}
|
||||
|
||||
export function createTesterRole(
|
||||
adapter: AgentFn,
|
||||
extract: LlmExtractorConfig,
|
||||
nerveRoot: string,
|
||||
): Role<TesterMeta> {
|
||||
return createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => testerPrompt({ threadId: start.meta.threadId, nerveRoot }),
|
||||
testerMetaSchema,
|
||||
extract,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { createDevelopSenseWorkflow } from "./develop-sense/build.js";
|
||||
export type { CreateDevelopSenseDeps } from "./develop-sense/build.js";
|
||||
export { createDevelopWorkflowWorkflow } from "./develop-workflow/build.js";
|
||||
export type { CreateDevelopWorkflowDeps } from "./develop-workflow/build.js";
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,24 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type {
|
||||
Role,
|
||||
RoleResult,
|
||||
StartStep,
|
||||
WorkflowMessage,
|
||||
} from "@uncaged/nerve-core";
|
||||
import type { Role, StartStep } from "@uncaged/nerve-core";
|
||||
|
||||
import { START } from "@uncaged/nerve-core";
|
||||
import { decorateRole, onFail, withDryRun } from "../role-decorators.js";
|
||||
|
||||
type TestMeta = { ok: boolean };
|
||||
type TestMeta = Record<string, unknown> & { ok: boolean };
|
||||
|
||||
function fakeStart(dryRun: boolean): StartStep {
|
||||
return {
|
||||
role: "test",
|
||||
role: START,
|
||||
content: "",
|
||||
meta: {
|
||||
threadId: "t1",
|
||||
dryRun,
|
||||
startedAt: new Date().toISOString(),
|
||||
maxRounds: 10,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@ import { isDryRun } from "./role-types.js";
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** A role decorator: takes a role, returns an enhanced role. */
|
||||
export type RoleDecorator<M extends Record<string, unknown>> = (
|
||||
role: Role<M>,
|
||||
) => Role<M>;
|
||||
export type RoleDecorator<M extends Record<string, unknown>> = (role: Role<M>) => Role<M>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// decorateRole — compose a chain of decorators
|
||||
@@ -49,16 +47,15 @@ export type WithDryRunOptions<M> = {
|
||||
export function withDryRun<M extends Record<string, unknown>>(
|
||||
opts: WithDryRunOptions<M>,
|
||||
): RoleDecorator<M> {
|
||||
return (role) =>
|
||||
async (start: StartStep, messages: WorkflowMessage[]) => {
|
||||
if (isDryRun(start)) {
|
||||
return {
|
||||
content: `[dry-run] ${opts.label} skipped`,
|
||||
meta: opts.meta,
|
||||
};
|
||||
}
|
||||
return role(start, messages);
|
||||
};
|
||||
return (role) => async (start: StartStep, messages: WorkflowMessage[]) => {
|
||||
if (isDryRun(start)) {
|
||||
return {
|
||||
content: `[dry-run] ${opts.label} skipped`,
|
||||
meta: opts.meta,
|
||||
};
|
||||
}
|
||||
return role(start, messages);
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -79,16 +76,15 @@ export type OnFailOptions<M> = {
|
||||
export function onFail<M extends Record<string, unknown>>(
|
||||
opts: OnFailOptions<M>,
|
||||
): RoleDecorator<M> {
|
||||
return (role) =>
|
||||
async (start: StartStep, messages: WorkflowMessage[]) => {
|
||||
try {
|
||||
return await role(start, messages);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return {
|
||||
content: `${opts.label} failed: ${msg}`,
|
||||
meta: opts.meta,
|
||||
};
|
||||
}
|
||||
};
|
||||
return (role) => async (start: StartStep, messages: WorkflowMessage[]) => {
|
||||
try {
|
||||
return await role(start, messages);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return {
|
||||
content: `${opts.label} failed: ${msg}`,
|
||||
meta: opts.meta,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Generated
+72
@@ -156,6 +156,50 @@ importers:
|
||||
specifier: ^4.14.0
|
||||
version: 4.85.0(@cloudflare/workers-types@4.20260425.1)
|
||||
|
||||
packages/role-committer:
|
||||
dependencies:
|
||||
'@uncaged/nerve-core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@uncaged/nerve-workflow-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../workflow-utils
|
||||
zod:
|
||||
specifier: ^4.3.6
|
||||
version: 4.3.6
|
||||
devDependencies:
|
||||
'@rslib/core':
|
||||
specifier: ^0.21.3
|
||||
version: 0.21.3(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5(@types/node@25.6.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3))
|
||||
|
||||
packages/role-reviewer:
|
||||
dependencies:
|
||||
'@uncaged/nerve-core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@uncaged/nerve-workflow-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../workflow-utils
|
||||
zod:
|
||||
specifier: ^4.3.6
|
||||
version: 4.3.6
|
||||
devDependencies:
|
||||
'@rslib/core':
|
||||
specifier: ^0.21.3
|
||||
version: 0.21.3(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5(@types/node@25.6.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3))
|
||||
|
||||
packages/skills: {}
|
||||
|
||||
packages/store:
|
||||
@@ -174,6 +218,34 @@ importers:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5(@types/node@22.19.17)(vite@8.0.9(@types/node@22.19.17)(esbuild@0.27.7)(yaml@2.8.3))
|
||||
|
||||
packages/workflow-meta:
|
||||
dependencies:
|
||||
'@uncaged/nerve-core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@uncaged/nerve-role-committer':
|
||||
specifier: workspace:*
|
||||
version: link:../role-committer
|
||||
'@uncaged/nerve-role-reviewer':
|
||||
specifier: workspace:*
|
||||
version: link:../role-reviewer
|
||||
'@uncaged/nerve-workflow-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../workflow-utils
|
||||
zod:
|
||||
specifier: ^4.3.6
|
||||
version: 4.3.6
|
||||
devDependencies:
|
||||
'@rslib/core':
|
||||
specifier: ^0.21.3
|
||||
version: 0.21.3(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5(@types/node@25.6.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3))
|
||||
|
||||
packages/workflow-utils:
|
||||
dependencies:
|
||||
'@uncaged/nerve-adapter-cursor':
|
||||
|
||||
Reference in New Issue
Block a user