Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4c228d36e | |||
| cb97507e9a | |||
| 4b442bb251 | |||
| ac53128ff7 | |||
| 607366c469 | |||
| 577fb27470 | |||
| 5475dd3f5c | |||
| 09b7ddf6d0 | |||
| c4e94bbe56 |
@@ -618,5 +618,65 @@ defaultModel: default
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("agentOverrides — accepts valid 3-segment path", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await cmdConfigSet(tempDir, "agentOverrides.solve-issue.planner", "claude-code");
|
||||
const value = await cmdConfigGet(tempDir, "agentOverrides.solve-issue.planner");
|
||||
expect(value).toBe("claude-code");
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("agentOverrides — rejects incomplete path (2 segments)", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(cmdConfigSet(tempDir, "agentOverrides.solve-issue", "hermes")).rejects.toThrow(
|
||||
/incomplete path|must specify a field/i,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("modelOverrides — accepts valid 2-segment path", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await cmdConfigSet(tempDir, "modelOverrides.extract", "gpt4");
|
||||
const value = await cmdConfigGet(tempDir, "modelOverrides.extract");
|
||||
expect(value).toBe("gpt4");
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("modelOverrides — rejects incomplete path (1 segment only)", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(cmdConfigSet(tempDir, "modelOverrides", "gpt4")).rejects.toThrow(
|
||||
/incomplete path|must specify a field/i,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects unknown top-level key (regression)", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
|
||||
try {
|
||||
createTestConfig(tempDir, sampleConfig);
|
||||
await expect(cmdConfigSet(tempDir, "randomKey", "value")).rejects.toThrow(
|
||||
/Unknown config key/,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,9 +7,11 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
import {
|
||||
cmdSkillActor,
|
||||
cmdSkillAdapter,
|
||||
cmdSkillArchitecture,
|
||||
cmdSkillAuthor,
|
||||
cmdSkillCli,
|
||||
cmdSkillDeveloper,
|
||||
cmdSkillList,
|
||||
cmdSkillModerator,
|
||||
cmdSkillUser,
|
||||
@@ -27,6 +29,8 @@ describe("skill commands", () => {
|
||||
expect(result).toContain("actor");
|
||||
expect(result).toContain("user");
|
||||
expect(result).toContain("author");
|
||||
expect(result).toContain("developer");
|
||||
expect(result).toContain("adapter");
|
||||
for (const name of result) {
|
||||
expect(name).toMatch(/^\S+$/);
|
||||
}
|
||||
@@ -97,6 +101,24 @@ describe("skill commands", () => {
|
||||
expect(result.length).toBeGreaterThan(500);
|
||||
});
|
||||
|
||||
test("skill developer returns non-empty markdown string", () => {
|
||||
const result = cmdSkillDeveloper();
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result).toContain("Monorepo");
|
||||
expect(result).toContain("CAS");
|
||||
expect(result).toContain("Biome");
|
||||
expect(result.length).toBeGreaterThan(500);
|
||||
});
|
||||
|
||||
test("skill adapter returns non-empty markdown string", () => {
|
||||
const result = cmdSkillAdapter();
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result).toContain("createAgent");
|
||||
expect(result).toContain("AgentContext");
|
||||
expect(result).toContain("frontmatter");
|
||||
expect(result.length).toBeGreaterThan(500);
|
||||
});
|
||||
|
||||
test("skill help subcommand is suppressed", () => {
|
||||
const output = execFileSync("bun", ["src/cli.ts", "skill", "--help"], {
|
||||
cwd: join(__dirname, "..", ".."),
|
||||
@@ -111,6 +133,8 @@ describe("skill commands", () => {
|
||||
expect(output).toContain("actor");
|
||||
expect(output).toContain("user");
|
||||
expect(output).toContain("author");
|
||||
expect(output).toContain("developer");
|
||||
expect(output).toContain("adapter");
|
||||
expect(output).toContain("list");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,9 +18,11 @@ import { cmdLogClean, cmdLogList, cmdLogShow } from "./commands/log.js";
|
||||
import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
|
||||
import {
|
||||
cmdSkillActor,
|
||||
cmdSkillAdapter,
|
||||
cmdSkillArchitecture,
|
||||
cmdSkillAuthor,
|
||||
cmdSkillCli,
|
||||
cmdSkillDeveloper,
|
||||
cmdSkillList,
|
||||
cmdSkillModerator,
|
||||
cmdSkillUser,
|
||||
@@ -513,6 +515,13 @@ skill
|
||||
console.log(cmdSkillActor());
|
||||
});
|
||||
|
||||
skill
|
||||
.command("adapter")
|
||||
.description("Print the adapter reference (building agent adapters)")
|
||||
.action(() => {
|
||||
console.log(cmdSkillAdapter());
|
||||
});
|
||||
|
||||
skill
|
||||
.command("author")
|
||||
.description("Print the author reference (workflow YAML design guide)")
|
||||
@@ -520,6 +529,13 @@ skill
|
||||
console.log(cmdSkillAuthor());
|
||||
});
|
||||
|
||||
skill
|
||||
.command("developer")
|
||||
.description("Print the developer reference (coding conventions + architecture)")
|
||||
.action(() => {
|
||||
console.log(cmdSkillDeveloper());
|
||||
});
|
||||
|
||||
skill
|
||||
.command("moderator")
|
||||
.description("Print the moderator reference")
|
||||
@@ -548,7 +564,7 @@ program
|
||||
.option("--base-url <url>", "OpenAI-compatible API base URL")
|
||||
.option("--api-key <key>", "API key")
|
||||
.option("--model <name>", "Default model name")
|
||||
.option("--agent <name>", "Default agent alias")
|
||||
.option("--agent <name>", "Default agent adapter (e.g. hermes → uwf-hermes)")
|
||||
.action(
|
||||
(opts: {
|
||||
provider?: string;
|
||||
|
||||
@@ -5,7 +5,10 @@ import { parse, stringify } from "yaml";
|
||||
/**
|
||||
* Valid configuration key schema
|
||||
*/
|
||||
const VALID_CONFIG_KEYS: Record<string, { nested: boolean; knownFields?: string[] }> = {
|
||||
const VALID_CONFIG_KEYS: Record<
|
||||
string,
|
||||
{ nested: boolean; knownFields?: string[]; minDepth?: number }
|
||||
> = {
|
||||
providers: {
|
||||
nested: true,
|
||||
knownFields: ["baseUrl", "apiKey"],
|
||||
@@ -18,6 +21,17 @@ const VALID_CONFIG_KEYS: Record<string, { nested: boolean; knownFields?: string[
|
||||
nested: true,
|
||||
knownFields: ["command", "args"],
|
||||
},
|
||||
agentOverrides: {
|
||||
nested: true,
|
||||
// agentOverrides.<workflowName>.<roleName> = agentAlias (string value)
|
||||
// No knownFields — workflow/role names are user-defined
|
||||
},
|
||||
modelOverrides: {
|
||||
nested: true,
|
||||
minDepth: 2,
|
||||
// modelOverrides.<scenario> = modelAlias (string value)
|
||||
// No knownFields — scenarios are user-defined
|
||||
},
|
||||
defaultAgent: { nested: false },
|
||||
defaultModel: { nested: false },
|
||||
};
|
||||
@@ -43,8 +57,9 @@ function validateConfigKey(path: string[]): void {
|
||||
throw new Error(`${topLevel} is a scalar key and cannot have nested properties`);
|
||||
}
|
||||
|
||||
// Nested keys must have at least 3 segments (e.g., providers.myProvider.baseUrl)
|
||||
if (schema.nested && path.length < 3) {
|
||||
// Nested keys must have at least minDepth segments (default 3)
|
||||
const minDepth = schema.minDepth ?? 3;
|
||||
if (schema.nested && path.length < minDepth) {
|
||||
const fields = schema.knownFields?.join(", ") ?? "";
|
||||
throw new Error(
|
||||
`Incomplete path for ${topLevel}. Must specify a field (e.g., ${topLevel}.<name>.<field>). Valid fields: ${fields}`,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export {
|
||||
generateActorReference as cmdSkillActor,
|
||||
generateAdapterReference as cmdSkillAdapter,
|
||||
generateArchitectureReference as cmdSkillArchitecture,
|
||||
generateAuthorReference as cmdSkillAuthor,
|
||||
generateCliReference as cmdSkillCli,
|
||||
generateDeveloperReference as cmdSkillDeveloper,
|
||||
generateModeratorReference as cmdSkillModerator,
|
||||
generateUserReference as cmdSkillUser,
|
||||
generateYamlReference as cmdSkillYaml,
|
||||
@@ -16,6 +18,8 @@ const SKILL_NAMES = [
|
||||
"actor",
|
||||
"user",
|
||||
"author",
|
||||
"developer",
|
||||
"adapter",
|
||||
] as const;
|
||||
|
||||
export function cmdSkillList(): ReadonlyArray<string> {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# @uncaged/workflow-agent-hermes
|
||||
|
||||
`uwf-hermes` agent — spawns Hermes chat via ACP and captures session detail.
|
||||
`uwf-hermes` — an **agent adapter** that bridges the `uwf` workflow engine and the Hermes CLI.
|
||||
|
||||
## Overview
|
||||
|
||||
Layer 3 agent implementation. Wraps the Hermes CLI using the Agent Client Protocol (ACP). On first visit to a role it sends a composed prompt (role definition, task, history, edge prompt); on continuation it resumes the cached session. Session transcripts and raw output are stored as CAS detail nodes.
|
||||
`uwf-hermes` is an adapter (not the Hermes CLI itself). The `uwf` engine speaks a generic agent protocol (stdin/stdout frontmatter contract); `uwf-hermes` translates that protocol into Hermes ACP (Agent Client Protocol) calls. Other adapters (e.g. `uwf-claude-code`, `uwf-cursor`) do the same for their respective CLIs.
|
||||
|
||||
On first visit to a role it sends a composed prompt (role definition, task, history, edge prompt); on continuation it resumes the cached session. Session transcripts and raw output are stored as CAS detail nodes.
|
||||
|
||||
**Dependencies:** `@uncaged/json-cas`, `@uncaged/workflow-util-agent`, `@uncaged/workflow-protocol`, `@uncaged/workflow-util`
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const PKG_ROOT = join(import.meta.dir, "..");
|
||||
|
||||
describe("Issue #551 — bin entry & engines", () => {
|
||||
test("package.json declares bun in engines", () => {
|
||||
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8"));
|
||||
expect(pkg.engines).toBeDefined();
|
||||
expect(pkg.engines.bun).toBeDefined();
|
||||
expect(pkg.engines.bun).toMatch(/^>=?\s*[\d.]+/);
|
||||
});
|
||||
|
||||
test("bin entry file has bun shebang", () => {
|
||||
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8"));
|
||||
const binPath = pkg.bin["uwf-hermes"];
|
||||
const content = readFileSync(join(PKG_ROOT, binPath), "utf-8");
|
||||
expect(content.startsWith("#!/usr/bin/env bun")).toBe(true);
|
||||
});
|
||||
|
||||
test("README.md explains uwf-hermes is an adapter", () => {
|
||||
const readme = readFileSync(join(PKG_ROOT, "README.md"), "utf-8");
|
||||
expect(readme.toLowerCase()).toContain("adapter");
|
||||
expect(readme).toMatch(/uwf-hermes/);
|
||||
expect(readme).toMatch(/hermes/);
|
||||
});
|
||||
});
|
||||
@@ -42,5 +42,8 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/shazhou-ww/uncaged-workflow/issues"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">= 1.0.0"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
export function generateAdapterReference(): string {
|
||||
return `# Adapter Reference
|
||||
|
||||
Guide for building a new agent adapter (CLI binary) for the workflow engine.
|
||||
|
||||
## What Is an Adapter
|
||||
|
||||
An adapter is a CLI command (e.g. \`uwf-hermes\`, \`uwf-builtin\`) that the engine spawns to execute a role. It bridges the workflow engine and an LLM/agent backend. The engine calls it with:
|
||||
|
||||
\`\`\`
|
||||
uwf-<name> --thread <id> --role <role> --prompt <text>
|
||||
\`\`\`
|
||||
|
||||
The adapter must produce frontmatter markdown output. The engine handles argument parsing, context building, output extraction, and CAS persistence — you just implement the LLM interaction.
|
||||
|
||||
## Quick Start
|
||||
|
||||
\`\`\`typescript
|
||||
import { createAgent } from "@uncaged/workflow-util-agent";
|
||||
import type { AgentContext, AgentRunResult, AgentContinueFn, AgentRunFn } from "@uncaged/workflow-util-agent";
|
||||
|
||||
const run: AgentRunFn = async (ctx: AgentContext): Promise<AgentRunResult> => {
|
||||
// 1. Build your prompt from ctx
|
||||
// 2. Call your LLM backend
|
||||
// 3. Return the result
|
||||
return { output: rawMarkdown, detailHash, sessionId };
|
||||
};
|
||||
|
||||
const continue_: AgentContinueFn = async (sessionId, message, store) => {
|
||||
// Resume an existing session with a correction message
|
||||
return { output: correctedMarkdown, detailHash, sessionId };
|
||||
};
|
||||
|
||||
const main = createAgent({ name: "my-agent", run, continue: continue_ });
|
||||
main();
|
||||
\`\`\`
|
||||
|
||||
## The \`createAgent\` Factory
|
||||
|
||||
\`createAgent(options)\` returns an async \`main()\` function that handles the full lifecycle:
|
||||
|
||||
1. Parses CLI args (\`--thread\`, \`--role\`, \`--prompt\`)
|
||||
2. Loads \`.env\` from storage root
|
||||
3. Builds \`AgentContext\` (thread history, workflow definition, role prompt)
|
||||
4. Injects \`outputFormatInstruction\` from the role's frontmatter schema
|
||||
5. Calls your \`run(ctx)\` function
|
||||
6. Extracts frontmatter from your output via \`tryFrontmatterFastPath()\`
|
||||
7. If extraction fails, calls your \`continue(sessionId, correctionMessage, store)\` up to 2 times
|
||||
8. Persists the validated output as a CAS step node
|
||||
9. Prints the step hash to stdout
|
||||
|
||||
You only implement \`run\` and \`continue\`.
|
||||
|
||||
## AgentOptions
|
||||
|
||||
\`\`\`typescript
|
||||
type AgentOptions = {
|
||||
name: string; // Adapter name (used in step records as "uwf-<name>")
|
||||
run: AgentRunFn; // Execute a role from scratch
|
||||
continue: AgentContinueFn; // Resume a session for frontmatter correction
|
||||
};
|
||||
\`\`\`
|
||||
|
||||
## AgentContext
|
||||
|
||||
The \`ctx\` object passed to your \`run\` function:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| \`threadId\` | \`string\` | Thread ULID |
|
||||
| \`role\` | \`string\` | Role name being executed |
|
||||
| \`edgePrompt\` | \`string\` | Moderator's task instruction for this step |
|
||||
| \`workflow\` | \`WorkflowPayload\` | Full workflow definition (roles, graph) |
|
||||
| \`start\` | \`StartNodePayload\` | Thread start data (workflow hash, user prompt) |
|
||||
| \`steps\` | \`StepContext[]\` | Previous steps with expanded outputs |
|
||||
| \`store\` | \`Store\` | CAS store for reading/writing data |
|
||||
| \`outputFormatInstruction\` | \`string\` | Frontmatter format instruction (inject into system prompt) |
|
||||
| \`isFirstVisit\` | \`boolean\` | True if this role hasn't run before in this thread |
|
||||
|
||||
## AgentRunResult
|
||||
|
||||
Your \`run\` and \`continue\` functions must return:
|
||||
|
||||
\`\`\`typescript
|
||||
type AgentRunResult = {
|
||||
output: string; // Raw markdown with frontmatter (must start with ---)
|
||||
detailHash: string; // CAS hash of session detail (turn history, metadata)
|
||||
sessionId: string; // Session ID for potential continue() calls
|
||||
};
|
||||
\`\`\`
|
||||
|
||||
## Building the Prompt
|
||||
|
||||
Use helpers from \`@uncaged/workflow-util-agent\`:
|
||||
|
||||
| Helper | Purpose |
|
||||
|--------|---------|
|
||||
| \`buildRolePrompt(roleDef)\` | Assemble Goal/Capabilities/Prepare/Procedure/Output sections |
|
||||
| \`buildContinuationPrompt(steps, role, edgePrompt)\` | For re-entry: steps since last visit + edge prompt |
|
||||
| \`ctx.outputFormatInstruction\` | Pre-built frontmatter format block (inject into system prompt) |
|
||||
|
||||
Typical system prompt structure:
|
||||
\`\`\`
|
||||
[outputFormatInstruction]
|
||||
[rolePrompt from buildRolePrompt()]
|
||||
[workflow metadata]
|
||||
\`\`\`
|
||||
|
||||
## Storing Session Detail
|
||||
|
||||
Store your turn history as a CAS merkle DAG for debugging and replay:
|
||||
|
||||
\`\`\`typescript
|
||||
// Store each turn as a CAS text node
|
||||
const turnHash = await store.put(textSchema, { content: turnData });
|
||||
|
||||
// Build a detail node referencing all turns
|
||||
const detailHash = await store.put(detailSchema, { turns: turnHashes });
|
||||
\`\`\`
|
||||
|
||||
The \`detailHash\` is preserved from the first \`run()\` call — retry \`continue()\` calls don't overwrite it.
|
||||
|
||||
## Registration
|
||||
|
||||
Register your adapter in \`~/.uncaged/workflow/config.yaml\`:
|
||||
|
||||
\`\`\`yaml
|
||||
agents:
|
||||
my-agent:
|
||||
command: uwf-my-agent
|
||||
args: []
|
||||
\`\`\`
|
||||
|
||||
Use it:
|
||||
\`\`\`bash
|
||||
uwf thread exec <thread-id> --agent my-agent
|
||||
\`\`\`
|
||||
|
||||
Or set as default:
|
||||
\`\`\`yaml
|
||||
defaultAgent: my-agent
|
||||
\`\`\`
|
||||
|
||||
## Existing Adapters
|
||||
|
||||
| Adapter | Package | Backend |
|
||||
|---------|---------|---------|
|
||||
| \`uwf-hermes\` | \`@uncaged/workflow-agent-hermes\` | Hermes ACP (chat sessions) |
|
||||
| \`uwf-builtin\` | \`@uncaged/workflow-agent-builtin\` | Direct OpenAI API (tools + loop) |
|
||||
| \`uwf-claude-code\` | \`@uncaged/workflow-agent-claude-code\` | Claude Code CLI |
|
||||
|
||||
Study these for patterns on prompt building, session management, and detail storage.
|
||||
|
||||
## Checklist
|
||||
|
||||
1. Implement \`run(ctx)\` — build prompt, call LLM, return output + detailHash + sessionId
|
||||
2. Implement \`continue(sessionId, message, store)\` — resume session for frontmatter correction
|
||||
3. Store session detail as CAS nodes (for debugging)
|
||||
4. Ensure output starts with \`---\` frontmatter block
|
||||
5. Add a \`bin\` entry in \`package.json\` for the CLI command
|
||||
6. Register in config.yaml and test with \`uwf thread exec --agent <name>\`
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
export function generateDeveloperReference(): string {
|
||||
return `# Developer Reference
|
||||
|
||||
Guide for contributing to the workflow engine codebase.
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
\`\`\`
|
||||
packages/
|
||||
workflow-protocol/ # Shared types (WorkflowPayload, StepNodePayload, etc.)
|
||||
workflow-util/ # Base32, ULID, logger, frontmatter parsing, skill references
|
||||
workflow-util-agent/ # createAgent factory, context builder, extract pipeline
|
||||
workflow-agent-hermes/ # uwf-hermes CLI (spawns Hermes chat sessions)
|
||||
workflow-agent-builtin/ # uwf-builtin CLI (direct LLM calls via OpenAI API)
|
||||
cli-workflow/ # uwf CLI (moderator, thread/step/cas/config commands)
|
||||
\`\`\`
|
||||
|
||||
Dependency layers (each only imports from packages above it):
|
||||
\`\`\`
|
||||
protocol → util → util-agent → agent-hermes / agent-builtin / cli-workflow
|
||||
\`\`\`
|
||||
|
||||
External CAS: \`@uncaged/json-cas\` (store API, hashing, schema validation) + \`@uncaged/json-cas-fs\` (filesystem backend).
|
||||
|
||||
## Coding Conventions
|
||||
|
||||
### Functional-first
|
||||
|
||||
| Rule | Description |
|
||||
|------|-------------|
|
||||
| \`type\` over \`interface\` | All type definitions use \`type\` |
|
||||
| \`function\` over \`class\` | Pure functions + closures, no class |
|
||||
| No \`this\` | Functions must not depend on \`this\` context |
|
||||
| No inheritance | No \`extends\`, \`implements\`, \`abstract\` |
|
||||
| No optional properties | Use \`T \\| null\` instead of \`?:\` |
|
||||
| Immutability first | Use \`Readonly<T>\`, \`as const\`, avoid mutation |
|
||||
|
||||
Classes allowed only when required by third-party libraries or for Error subclasses.
|
||||
|
||||
### Error Handling
|
||||
|
||||
- \`Result<T, E>\` type for expected failures (\`ok\`/\`err\` constructors from \`@uncaged/workflow-util\`)
|
||||
- \`throw\` only for unrecoverable bugs
|
||||
- No try-catch for flow control
|
||||
|
||||
### Async
|
||||
|
||||
Always \`async/await\`, never \`.then()\` chains.
|
||||
|
||||
### Logging
|
||||
|
||||
\`console.*\` is banned (Biome \`noConsole\` rule). Use the structured logger:
|
||||
|
||||
\`\`\`typescript
|
||||
import { createLogger } from "@uncaged/workflow-util";
|
||||
const log = createLogger();
|
||||
log("4KNMR2PX", "Loading workflow..."); // 8-char Crockford Base32 tag
|
||||
\`\`\`
|
||||
|
||||
Each call site gets a unique hand-written tag. \`grep "4KNMR2PX"\` in logs → instant code location.
|
||||
|
||||
CLI package (\`@uncaged/cli-workflow\`) may use \`console.log\` for user-facing output with a biome-ignore comment.
|
||||
|
||||
### No Dynamic Import
|
||||
|
||||
No \`await import()\` in production code. Always static top-level \`import\`. Test files are exempt.
|
||||
|
||||
### Naming
|
||||
|
||||
- Workflow names: verb-first kebab-case (\`solve-issue\`, \`review-code\`)
|
||||
- IDs: Crockford Base32 — CAS hash (XXH64, 13-char), Thread ID (ULID, 26-char)
|
||||
|
||||
## Development Workflow
|
||||
|
||||
\`\`\`bash
|
||||
bun install # install all workspace deps
|
||||
bun run build # tsc --build (all packages)
|
||||
bun run check # tsc + biome check + lint-log-tags
|
||||
bun run format # biome format --write
|
||||
bun test # run all tests
|
||||
\`\`\`
|
||||
|
||||
Before committing: \`bun run check\` + \`bun test\` must both pass.
|
||||
|
||||
### Testing
|
||||
|
||||
- \`cli-workflow\`: vitest
|
||||
- Other packages: \`bun test\`
|
||||
- Test files live in \`__tests__/\` directories
|
||||
|
||||
### Publishing
|
||||
|
||||
Fixed-mode versioning — all \`@uncaged/*\` packages share the same version number.
|
||||
|
||||
\`\`\`bash
|
||||
bun changeset # describe the change
|
||||
bun version # bump versions + changelogs
|
||||
bun release # build + test + publish to npmjs
|
||||
\`\`\`
|
||||
|
||||
## Key Modules
|
||||
|
||||
### Moderator (\`cli-workflow/src/moderator/\`)
|
||||
|
||||
Status-based graph evaluator. Reads \`graph[lastRole][output.$status]\` to determine the next role. Zero LLM cost.
|
||||
|
||||
### Extract Pipeline (\`workflow-util-agent/src/\`)
|
||||
|
||||
1. Agent produces frontmatter markdown
|
||||
2. \`parseFrontmatterMarkdown()\` extracts YAML frontmatter
|
||||
3. \`tryFrontmatterFastPath()\` validates against role's output schema
|
||||
4. If fast path fails, retries up to 2 times via agent continue
|
||||
5. Validated output stored as CAS node
|
||||
|
||||
### createAgent Factory (\`workflow-util-agent/src/run.ts\`)
|
||||
|
||||
Shared entry point for all agent CLIs. Handles:
|
||||
- Argument parsing (\`--thread\`, \`--role\`, \`--prompt\`)
|
||||
- Context building (thread history, workflow definition)
|
||||
- Output extraction and CAS persistence
|
||||
- Frontmatter retry loop
|
||||
|
||||
### CAS Integration
|
||||
|
||||
All data is CAS-addressed via \`@uncaged/json-cas\`:
|
||||
- \`store.put(schemaHash, data)\` → content hash
|
||||
- \`store.get(hash)\` → node
|
||||
- \`validate(store, node)\` → schema check
|
||||
- Schemas registered at workflow add time
|
||||
|
||||
## Commit Convention
|
||||
|
||||
\`\`\`
|
||||
<type>(<scope>): <description>
|
||||
|
||||
type: feat | fix | refactor | docs | chore | test
|
||||
scope: workflow | cli | moderator | util-agent | hermes | util | protocol
|
||||
\`\`\`
|
||||
`;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
export { generateActorReference } from "./actor-reference.js";
|
||||
export { generateAdapterReference } from "./adapter-reference.js";
|
||||
export { generateArchitectureReference } from "./architecture-reference.js";
|
||||
export { generateAuthorReference } from "./author-reference.js";
|
||||
export { encodeUint64AsCrockford } from "./base32.js";
|
||||
export { generateCliReference } from "./cli-reference.js";
|
||||
export { generateDeveloperReference } from "./developer-reference.js";
|
||||
export { env } from "./env.js";
|
||||
export type {
|
||||
AgentFrontmatter,
|
||||
|
||||
Reference in New Issue
Block a user