@sdods/agents 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/adapter/claude-code.d.ts +29 -0
- package/dist/adapter/claude-code.js +195 -0
- package/dist/adapter/claude.d.ts +42 -0
- package/dist/adapter/claude.js +216 -0
- package/dist/adapter/cli-common.d.ts +46 -0
- package/dist/adapter/cli-common.js +145 -0
- package/dist/adapter/codex.d.ts +31 -0
- package/dist/adapter/codex.js +252 -0
- package/dist/adapter/factory.d.ts +41 -0
- package/dist/adapter/factory.js +102 -0
- package/dist/adapter/fake.d.ts +39 -0
- package/dist/adapter/fake.js +91 -0
- package/dist/adapter/index.d.ts +11 -0
- package/dist/adapter/index.js +11 -0
- package/dist/adapter/json-schema.d.ts +11 -0
- package/dist/adapter/json-schema.js +17 -0
- package/dist/adapter/mcp-bridge.d.ts +35 -0
- package/dist/adapter/mcp-bridge.js +122 -0
- package/dist/adapter/ollama.d.ts +62 -0
- package/dist/adapter/ollama.js +272 -0
- package/dist/adapter/openai-compat.d.ts +44 -0
- package/dist/adapter/openai-compat.js +200 -0
- package/dist/adapter/profile.d.ts +54 -0
- package/dist/adapter/profile.js +42 -0
- package/dist/adapter/tool-loop.d.ts +64 -0
- package/dist/adapter/tool-loop.js +192 -0
- package/dist/adapter/types.d.ts +109 -0
- package/dist/adapter/types.js +20 -0
- package/dist/claude-code/install.d.ts +36 -0
- package/dist/claude-code/install.js +183 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jobs/runner.d.ts +80 -0
- package/dist/jobs/runner.js +197 -0
- package/dist/roles/index.d.ts +50 -0
- package/dist/roles/index.js +125 -0
- package/dist/roles/steps.d.ts +28 -0
- package/dist/roles/steps.js +75 -0
- package/package.json +50 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type CodingAgentTarget = 'claude' | 'codex' | 'all';
|
|
2
|
+
export interface InstallCodingAgentsOptions {
|
|
3
|
+
for?: CodingAgentTarget;
|
|
4
|
+
project?: string;
|
|
5
|
+
env?: string;
|
|
6
|
+
force?: boolean;
|
|
7
|
+
/** also write `.mcp.json` for Claude Code (the CLI normally registers MCP via `sdods mcp install`) */
|
|
8
|
+
mcp?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Set up coding-agent CLIs for an SDODS repo:
|
|
12
|
+
* - Claude Code: `.claude/agents/sdods-<role>.md` (one subagent per role) + `CLAUDE.md`
|
|
13
|
+
* - Codex: `AGENTS.md` (Codex reads it as project instructions; roles become sections)
|
|
14
|
+
* - both: `AGENT.md` and `SKILL.md` with the conventions
|
|
15
|
+
* Returns the paths written. Existing files are kept unless `force`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function installCodingAgents(rootDir: string, opts?: InstallCodingAgentsOptions): string[];
|
|
18
|
+
/** Backwards-compatible alias: Claude Code files plus `.mcp.json`. */
|
|
19
|
+
export declare function installClaudeCode(rootDir: string, opts?: {
|
|
20
|
+
project?: string;
|
|
21
|
+
env?: string;
|
|
22
|
+
force?: boolean;
|
|
23
|
+
}): string[];
|
|
24
|
+
/** CLAUDE.md: what Claude Code loads automatically. Short pointer plus the rules that matter. */
|
|
25
|
+
export declare function claudeMdContent(opts?: {
|
|
26
|
+
project?: string;
|
|
27
|
+
env?: string;
|
|
28
|
+
}): string;
|
|
29
|
+
/** AGENTS.md: Codex reads this as project instructions; roles become sections. */
|
|
30
|
+
export declare function agentsMdContent(opts?: {
|
|
31
|
+
project?: string;
|
|
32
|
+
env?: string;
|
|
33
|
+
}): string;
|
|
34
|
+
export declare function agentMdContent(): string;
|
|
35
|
+
export declare function skillMdContent(): string;
|
|
36
|
+
//# sourceMappingURL=install.d.ts.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { CONVENTIONS, ROLE_PROMPTS, installClientConfig } from '@sdods/mcp';
|
|
4
|
+
const TAGLINE = 'SDODS is an automation and orchestration platform with a reusable architecture: BDD for UI, API and hybrid flows.';
|
|
5
|
+
function roleUse(role) {
|
|
6
|
+
return role === 'planner'
|
|
7
|
+
? 'plan tests'
|
|
8
|
+
: role === 'generator'
|
|
9
|
+
? 'write or generate features'
|
|
10
|
+
: role === 'healer'
|
|
11
|
+
? 'fix a failing scenario'
|
|
12
|
+
: role === 'upgrader'
|
|
13
|
+
? 'update tests after code changes'
|
|
14
|
+
: 'review feature files';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Set up coding-agent CLIs for an SDODS repo:
|
|
18
|
+
* - Claude Code: `.claude/agents/sdods-<role>.md` (one subagent per role) + `CLAUDE.md`
|
|
19
|
+
* - Codex: `AGENTS.md` (Codex reads it as project instructions; roles become sections)
|
|
20
|
+
* - both: `AGENT.md` and `SKILL.md` with the conventions
|
|
21
|
+
* Returns the paths written. Existing files are kept unless `force`.
|
|
22
|
+
*/
|
|
23
|
+
export function installCodingAgents(rootDir, opts = {}) {
|
|
24
|
+
const target = opts.for ?? 'all';
|
|
25
|
+
const written = [];
|
|
26
|
+
const writeIfMissing = (file, content) => {
|
|
27
|
+
if (existsSync(file) && !opts.force)
|
|
28
|
+
return;
|
|
29
|
+
mkdirSync(join(file, '..'), { recursive: true });
|
|
30
|
+
writeFileSync(file, content);
|
|
31
|
+
written.push(file);
|
|
32
|
+
};
|
|
33
|
+
if (target === 'claude' || target === 'all') {
|
|
34
|
+
const agentsDir = join(rootDir, '.claude', 'agents');
|
|
35
|
+
for (const role of Object.keys(ROLE_PROMPTS)) {
|
|
36
|
+
const p = ROLE_PROMPTS[role];
|
|
37
|
+
writeIfMissing(join(agentsDir, `sdods-${role}.md`), `---\nname: sdods-${role}\ndescription: ${p.description} Use when the user asks SDODS to ${roleUse(role)}.\ntools: Read, Glob, Grep, mcp__sdods__*, mcp__playwright__*\n---\n\n${CONVENTIONS}\n\n${p.body}\n`);
|
|
38
|
+
}
|
|
39
|
+
writeIfMissing(join(rootDir, 'CLAUDE.md'), claudeMdContent(opts));
|
|
40
|
+
if (opts.mcp) {
|
|
41
|
+
const mcp = installClientConfig(rootDir, 'claude', {
|
|
42
|
+
project: opts.project,
|
|
43
|
+
env: opts.env,
|
|
44
|
+
args: ['sdods', 'mcp'],
|
|
45
|
+
});
|
|
46
|
+
written.push(mcp.file);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (target === 'codex' || target === 'all') {
|
|
50
|
+
writeIfMissing(join(rootDir, 'AGENTS.md'), agentsMdContent(opts));
|
|
51
|
+
}
|
|
52
|
+
writeIfMissing(join(rootDir, 'AGENT.md'), agentMdContent());
|
|
53
|
+
writeIfMissing(join(rootDir, 'SKILL.md'), skillMdContent());
|
|
54
|
+
return written;
|
|
55
|
+
}
|
|
56
|
+
/** Backwards-compatible alias: Claude Code files plus `.mcp.json`. */
|
|
57
|
+
export function installClaudeCode(rootDir, opts = {}) {
|
|
58
|
+
return installCodingAgents(rootDir, { ...opts, for: 'claude', mcp: true });
|
|
59
|
+
}
|
|
60
|
+
function projectFlags(opts) {
|
|
61
|
+
return `${opts.project ? ` -p ${opts.project}` : ''}${opts.env ? ` -e ${opts.env}` : ''}`;
|
|
62
|
+
}
|
|
63
|
+
/** CLAUDE.md: what Claude Code loads automatically. Short pointer plus the rules that matter. */
|
|
64
|
+
export function claudeMdContent(opts = {}) {
|
|
65
|
+
return `# CLAUDE.md — SDODS
|
|
66
|
+
|
|
67
|
+
${TAGLINE} Read \`AGENT.md\` for the mental model and \`SKILL.md\` for the command reference.
|
|
68
|
+
|
|
69
|
+
## Working in this repo
|
|
70
|
+
|
|
71
|
+
- Use the SDODS MCP server (\`sdods mcp\`) for anything about projects, features, steps, runs and results — it is registered in \`.mcp.json\` (\`sdods mcp install claude\` re-registers it).
|
|
72
|
+
- Subagents live in \`.claude/agents/sdods-*.md\`: planner, generator, healer, upgrader, reviewer. Delegate matching requests to them.
|
|
73
|
+
- Never edit features/steps/pages directly when acting as an SDODS agent: write a proposal (\`write_proposal\` / \`feature_write\`) and let a person accept it with \`sdods proposals accept <id>\`.
|
|
74
|
+
- Verify with \`sdods lint${projectFlags(opts)}\` and \`sdods run${projectFlags(opts)} -l api\` / \`-l ui -b chromium -t @smoke\` before claiming done.
|
|
75
|
+
|
|
76
|
+
## Rules
|
|
77
|
+
|
|
78
|
+
${CONVENTIONS.split('\n').slice(3).join('\n')}
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
/** AGENTS.md: Codex reads this as project instructions; roles become sections. */
|
|
82
|
+
export function agentsMdContent(opts = {}) {
|
|
83
|
+
const roles = Object.keys(ROLE_PROMPTS)
|
|
84
|
+
.map((role) => `### sdods-${role}\n\n${ROLE_PROMPTS[role].description}\n\n${ROLE_PROMPTS[role].body}`)
|
|
85
|
+
.join('\n\n');
|
|
86
|
+
return `# AGENTS.md — SDODS instructions for Codex
|
|
87
|
+
|
|
88
|
+
${TAGLINE} This file is read by the OpenAI Codex CLI. \`AGENT.md\` holds the mental model and \`SKILL.md\` the command reference.
|
|
89
|
+
|
|
90
|
+
## Tools
|
|
91
|
+
|
|
92
|
+
- The SDODS MCP server is registered as \`sdods\` (\`sdods mcp install codex\` re-registers it in \`~/.codex/config.toml\`); use its tools for projects, features, steps, runs, results, proposals.
|
|
93
|
+
- The bundled Playwright MCP server is registered as \`playwright\` for driving a browser.
|
|
94
|
+
- Commands: \`sdods lint${projectFlags(opts)}\`, \`sdods run${projectFlags(opts)} -l api\`, \`sdods run${projectFlags(opts)} -l ui -b chromium -t @smoke\`, \`sdods steps list${projectFlags(opts)}\`, \`sdods proposals list|show|accept\`.
|
|
95
|
+
|
|
96
|
+
## Rules
|
|
97
|
+
|
|
98
|
+
${CONVENTIONS.split('\n').slice(3).join('\n')}
|
|
99
|
+
|
|
100
|
+
## Roles
|
|
101
|
+
|
|
102
|
+
Pick the role that matches the request and follow its instructions. Every role only writes proposals; a person accepts them.
|
|
103
|
+
|
|
104
|
+
${roles}
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
export function agentMdContent() {
|
|
108
|
+
return `# AGENT.md — SDODS for coding agents
|
|
109
|
+
|
|
110
|
+
${TAGLINE} This file orients an AI coding assistant working in this repository.
|
|
111
|
+
|
|
112
|
+
## Mental model
|
|
113
|
+
|
|
114
|
+
- **Hierarchy**: organization → workspace(s) → project (\`projects/<slug>\`) → module (\`features/<module>/\`). \`sdods.workspace.yaml\` at the root names the org and workspaces; each project has \`sdods.project.yaml\` and \`envs/<env>.yaml\`.
|
|
115
|
+
- **Layers**: \`@ui\`, \`@api\`, \`@hybrid\` scenarios share ONE merged BDD fixture set (\`@sdods/core/fixtures\`). A scenario can seed through the API and assert in the browser.
|
|
116
|
+
- **Processes**: named run recipes (pr-check, nightly-regression, release-gate). \`sdods run --process <name>\`.
|
|
117
|
+
- **Everything is CLI-first**: \`sdods run\`, \`lint\`, \`steps list\`, \`features list\`, \`report\`, \`heal report\`, \`record\`, \`har\`, \`db\`, \`mcp\`, \`agent\`, \`proposals\`, \`serve\`.
|
|
118
|
+
- **Agents never edit the working tree**; they write proposals under \`proposals/<id>/\` that a person accepts with \`sdods proposals accept <id>\`.
|
|
119
|
+
|
|
120
|
+
## Conventions
|
|
121
|
+
|
|
122
|
+
${CONVENTIONS.split('\n').slice(3).join('\n')}
|
|
123
|
+
|
|
124
|
+
## Where things live
|
|
125
|
+
|
|
126
|
+
| Path | Purpose |
|
|
127
|
+
|---|---|
|
|
128
|
+
| \`packages/contracts\` | schemas, ids, attachment names, scopes (leaf package) |
|
|
129
|
+
| \`packages/core\` | config precedence, registry, fixtures, step library, data, screenshots, heal, recorder, lint, analyze |
|
|
130
|
+
| \`packages/cli\` | the \`sdods\` command |
|
|
131
|
+
| \`packages/db\` | Kysely, SQLite ⇄ Postgres, ingest, insights |
|
|
132
|
+
| \`packages/mcp\` | tool registry, MCP server (stdio + HTTP), proposals, prompts |
|
|
133
|
+
| \`packages/agents\` | LLM adapters, roles, jobs |
|
|
134
|
+
| \`packages/integrations\` | GitHub, Jira |
|
|
135
|
+
| \`packages/server\`, \`packages/web\` | Fastify API and React UI |
|
|
136
|
+
| \`projects/demo-shop\` | reference project and release acceptance suite |
|
|
137
|
+
|
|
138
|
+
## Verify before you claim done
|
|
139
|
+
|
|
140
|
+
\`\`\`bash
|
|
141
|
+
bun run typecheck && bun run lint && bun run test
|
|
142
|
+
bun run sdods lint -p <slug>
|
|
143
|
+
bun run sdods run -p <slug> -e <env> -l api
|
|
144
|
+
bun run sdods run -p <slug> -e <env> -l ui -b chromium -t @smoke
|
|
145
|
+
\`\`\`
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
export function skillMdContent() {
|
|
149
|
+
return `---
|
|
150
|
+
name: sdods
|
|
151
|
+
description: Work inside an SDODS repository — an automation and orchestration platform with a reusable architecture. Use for writing or fixing Gherkin features, page objects, step definitions, project/env yaml, running suites, reading results, and driving the sdods CLI or MCP tools.
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
# SDODS skill
|
|
155
|
+
|
|
156
|
+
## Quick reference
|
|
157
|
+
|
|
158
|
+
| Task | Command |
|
|
159
|
+
|---|---|
|
|
160
|
+
| See projects / hierarchy | \`sdods project list\`, \`sdods workspace tree\` |
|
|
161
|
+
| Resolve config with provenance | \`sdods config show -p <slug> -e <env> --explain\` |
|
|
162
|
+
| Validate features | \`sdods lint -p <slug>\` |
|
|
163
|
+
| Run a slice | \`sdods run -p <slug> -e <env> -l api\` · \`-l ui -b chromium -t @smoke\` · \`--process pr-check\` |
|
|
164
|
+
| Results | \`sdods report --last\`, \`sdods heal report --last\` |
|
|
165
|
+
| Steps available | \`sdods steps list -p <slug>\` |
|
|
166
|
+
| Record / replay | \`sdods record -p <slug> -e <env> --user <role> --name <name>\`, \`sdods har replay --strict\` |
|
|
167
|
+
| Agents | \`sdods agent plan|generate|heal|upgrade|review -p <slug> [--dry-run]\`, \`sdods proposals list|show|accept|reject\` |
|
|
168
|
+
| MCP | \`sdods mcp --project <slug>\`, \`sdods mcp install claude\` |
|
|
169
|
+
|
|
170
|
+
## Rules
|
|
171
|
+
|
|
172
|
+
${CONVENTIONS.split('\n').slice(3).join('\n')}
|
|
173
|
+
|
|
174
|
+
## Writing a scenario (checklist)
|
|
175
|
+
|
|
176
|
+
1. Pick the module directory under \`features/\` and the module tag.
|
|
177
|
+
2. Tag the Feature: one layer (\`@ui|@api|@hybrid\`) + one suite (\`@smoke|@regression|@sanity\`).
|
|
178
|
+
3. Reuse steps from \`sdods steps list\`; add project steps in \`steps/<module>.steps.ts\` only when needed.
|
|
179
|
+
4. Data through datasets (\`Given I load dataset "users" row 1\`) or \`@user:<role>\`; never literal secrets.
|
|
180
|
+
5. \`sdods lint -p <slug>\` then run the scenario; check before/after screenshots in \`.sdods/runs/<runId>/\`.
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=install.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type ToolContext, type ToolRegistry } from '@sdods/mcp';
|
|
2
|
+
import { type AgentEvent, type LlmAdapter, type Provider } from '../adapter/index.js';
|
|
3
|
+
import { roleTools, type RoleInput, type RoleName } from '../roles/index.js';
|
|
4
|
+
import { type ProfileSettings } from '../adapter/profile.js';
|
|
5
|
+
export type JobStatus = 'queued' | 'running' | 'awaiting_review' | 'completed' | 'failed' | 'cancelled';
|
|
6
|
+
export interface AgentJob {
|
|
7
|
+
id: string;
|
|
8
|
+
role: RoleName;
|
|
9
|
+
project?: string;
|
|
10
|
+
env?: string;
|
|
11
|
+
status: JobStatus;
|
|
12
|
+
provider: Provider;
|
|
13
|
+
model: string;
|
|
14
|
+
input: RoleInput;
|
|
15
|
+
startedAt: string;
|
|
16
|
+
finishedAt?: string;
|
|
17
|
+
costUsd?: number;
|
|
18
|
+
turns?: number;
|
|
19
|
+
toolCalls?: number;
|
|
20
|
+
proposalIds: string[];
|
|
21
|
+
error?: string;
|
|
22
|
+
resultText?: string;
|
|
23
|
+
budgetUsd: number;
|
|
24
|
+
maxTurns: number;
|
|
25
|
+
dryRun?: boolean;
|
|
26
|
+
events?: AgentEvent[];
|
|
27
|
+
}
|
|
28
|
+
export interface RunJobOptions {
|
|
29
|
+
role: RoleName;
|
|
30
|
+
input: RoleInput;
|
|
31
|
+
rootDir?: string;
|
|
32
|
+
provider?: string;
|
|
33
|
+
model?: string;
|
|
34
|
+
/** Model server endpoint, for a provider you host yourself. */
|
|
35
|
+
baseUrl?: string;
|
|
36
|
+
/** Context window to load a local model with. */
|
|
37
|
+
contextTokens?: number;
|
|
38
|
+
maxTurns?: number;
|
|
39
|
+
budgetUsd?: number;
|
|
40
|
+
/** auto | full | small — how much of the platform to show the model. */
|
|
41
|
+
profile?: string;
|
|
42
|
+
/** Force the browser server on or off for a role that would otherwise follow the profile. */
|
|
43
|
+
browser?: boolean;
|
|
44
|
+
dryRun?: boolean;
|
|
45
|
+
adapter?: LlmAdapter;
|
|
46
|
+
registry?: ToolRegistry;
|
|
47
|
+
onEvent?: (e: AgentEvent) => void;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
keepEvents?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export declare const JOBS_DIR = ".sdods/agent-jobs";
|
|
52
|
+
export declare class JobJournal {
|
|
53
|
+
readonly dir: string;
|
|
54
|
+
constructor(rootDir: string);
|
|
55
|
+
save(job: AgentJob): void;
|
|
56
|
+
get(id: string): AgentJob | undefined;
|
|
57
|
+
list(limit?: number): AgentJob[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the prompt/tool set for a role without running (used by --dry-run and tests).
|
|
61
|
+
*
|
|
62
|
+
* Asynchronous because grounding the prompt in the project's real step patterns means asking the
|
|
63
|
+
* project for them, which compiles it. The result is cached, so the second call is free.
|
|
64
|
+
*/
|
|
65
|
+
export declare function prepareJob(o: RunJobOptions): Promise<{
|
|
66
|
+
job: AgentJob;
|
|
67
|
+
ctx: ToolContext;
|
|
68
|
+
registry: ToolRegistry;
|
|
69
|
+
system: string;
|
|
70
|
+
prompt: string;
|
|
71
|
+
tools: ReturnType<typeof roleTools>;
|
|
72
|
+
adapter: LlmAdapter;
|
|
73
|
+
mcpServers: Record<string, any>;
|
|
74
|
+
/** True for the roles whose work is looking at the application. */
|
|
75
|
+
needsBrowser: boolean;
|
|
76
|
+
/** Which menu the model was given, and why. */
|
|
77
|
+
profile: ProfileSettings;
|
|
78
|
+
}>;
|
|
79
|
+
export declare function runJob(o: RunJobOptions): Promise<AgentJob>;
|
|
80
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { newId } from '@sdods/contracts';
|
|
4
|
+
import { ProposalStore, buildToolContext, createRegistry, findRepoRoot, readYaml, } from '@sdods/mcp';
|
|
5
|
+
import { createAdapter, } from '../adapter/index.js';
|
|
6
|
+
import { ROLES, rolePrompt, roleSystemPrompt, roleTools, } from '../roles/index.js';
|
|
7
|
+
import { stepVocabulary } from '../roles/steps.js';
|
|
8
|
+
import { resolveProfile } from '../adapter/profile.js';
|
|
9
|
+
export const JOBS_DIR = '.sdods/agent-jobs';
|
|
10
|
+
export class JobJournal {
|
|
11
|
+
dir;
|
|
12
|
+
constructor(rootDir) {
|
|
13
|
+
this.dir = join(rootDir, JOBS_DIR);
|
|
14
|
+
}
|
|
15
|
+
save(job) {
|
|
16
|
+
mkdirSync(this.dir, { recursive: true });
|
|
17
|
+
writeFileSync(join(this.dir, `${job.id}.json`), JSON.stringify(job, null, 2));
|
|
18
|
+
}
|
|
19
|
+
get(id) {
|
|
20
|
+
const f = join(this.dir, `${id}.json`);
|
|
21
|
+
return existsSync(f) ? JSON.parse(readFileSync(f, 'utf8')) : undefined;
|
|
22
|
+
}
|
|
23
|
+
list(limit = 50) {
|
|
24
|
+
if (!existsSync(this.dir))
|
|
25
|
+
return [];
|
|
26
|
+
return readdirSync(this.dir)
|
|
27
|
+
.filter((f) => f.endsWith('.json'))
|
|
28
|
+
.map((f) => JSON.parse(readFileSync(join(this.dir, f), 'utf8')))
|
|
29
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt))
|
|
30
|
+
.slice(0, limit);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the prompt/tool set for a role without running (used by --dry-run and tests).
|
|
35
|
+
*
|
|
36
|
+
* Asynchronous because grounding the prompt in the project's real step patterns means asking the
|
|
37
|
+
* project for them, which compiles it. The result is cached, so the second call is free.
|
|
38
|
+
*/
|
|
39
|
+
export async function prepareJob(o) {
|
|
40
|
+
const rootDir = o.rootDir ?? findRepoRoot();
|
|
41
|
+
const yaml = o.input.project
|
|
42
|
+
? readYaml(join(rootDir, process.env.SDODS_PROJECTS_DIR ?? 'projects', o.input.project, 'sdods.project.yaml'))
|
|
43
|
+
: undefined;
|
|
44
|
+
const def = ROLES[o.role];
|
|
45
|
+
const provider = o.dryRun ? 'fake' : o.provider;
|
|
46
|
+
const adapter = o.adapter ??
|
|
47
|
+
createAdapter({
|
|
48
|
+
provider,
|
|
49
|
+
model: o.model ?? yaml?.agents?.models?.[o.role] ?? yaml?.agents?.models?.default,
|
|
50
|
+
baseUrl: o.baseUrl ?? yaml?.agents?.local?.baseUrl,
|
|
51
|
+
contextTokens: o.contextTokens ?? yaml?.agents?.local?.contextTokens,
|
|
52
|
+
temperature: yaml?.agents?.local?.temperature,
|
|
53
|
+
timeoutMs: yaml?.agents?.local?.requestTimeoutMs,
|
|
54
|
+
rootDir,
|
|
55
|
+
project: o.input.project,
|
|
56
|
+
env: o.input.env,
|
|
57
|
+
onAutoDetect: (p, reason) => o.onEvent?.({ type: 'status', message: `adapter auto-selected: ${p} (${reason})` }),
|
|
58
|
+
}, yaml?.agents?.provider);
|
|
59
|
+
const registry = o.registry ?? createRegistry();
|
|
60
|
+
const ctx = buildToolContext({
|
|
61
|
+
rootDir,
|
|
62
|
+
caps: 'all',
|
|
63
|
+
project: o.input.project,
|
|
64
|
+
env: o.input.env,
|
|
65
|
+
onProgress: (p) => o.onEvent?.({ type: 'status', message: p.message ?? '' }),
|
|
66
|
+
});
|
|
67
|
+
// The profile decides how much of the platform the model is shown.
|
|
68
|
+
const profile = resolveProfile({
|
|
69
|
+
configured: (o.profile ?? yaml?.agents?.profile),
|
|
70
|
+
provider: adapter.provider,
|
|
71
|
+
model: o.model ?? yaml?.agents?.models?.[o.role] ?? adapter.defaultModel,
|
|
72
|
+
baseUrl: o.baseUrl ?? yaml?.agents?.local?.baseUrl,
|
|
73
|
+
contextTokens: o.contextTokens ?? yaml?.agents?.local?.contextTokens,
|
|
74
|
+
});
|
|
75
|
+
const tools = roleTools(o.role, registry, ctx, profile.profile);
|
|
76
|
+
const vocabulary = await stepVocabulary({
|
|
77
|
+
registry,
|
|
78
|
+
ctx,
|
|
79
|
+
project: o.input.project,
|
|
80
|
+
focus: [o.input.goal, o.input.plan, o.input.spec].filter(Boolean).join('\n').slice(0, 2000),
|
|
81
|
+
limit: profile.stepVocabularyLimit,
|
|
82
|
+
});
|
|
83
|
+
const system = roleSystemPrompt(o.role, o.input, vocabulary.selected, profile.profile);
|
|
84
|
+
const prompt = rolePrompt(o.role, o.input, profile.profile);
|
|
85
|
+
const mcpServers = {};
|
|
86
|
+
for (const [name, s] of Object.entries(yaml?.mcp?.servers ?? {})) {
|
|
87
|
+
if (s.enabled === false || name === 'playwright')
|
|
88
|
+
continue;
|
|
89
|
+
const env = Object.fromEntries(Object.entries(s.envFrom ?? {}).map(([k, v]) => [k, process.env[v] ?? '']));
|
|
90
|
+
const headers = Object.fromEntries(Object.entries(s.headersFrom ?? {}).map(([k, v]) => [k, process.env[v] ?? '']));
|
|
91
|
+
mcpServers[name] = {
|
|
92
|
+
transport: s.transport ?? 'stdio',
|
|
93
|
+
command: s.command,
|
|
94
|
+
args: s.args,
|
|
95
|
+
url: s.url,
|
|
96
|
+
env,
|
|
97
|
+
headers,
|
|
98
|
+
allowedTools: s.allowedTools,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const job = {
|
|
102
|
+
id: `${new Date().toISOString().slice(0, 19).replace(/[-:T]/g, '')}-${o.role}-${newId().slice(-6)}`,
|
|
103
|
+
role: o.role,
|
|
104
|
+
project: o.input.project,
|
|
105
|
+
env: o.input.env,
|
|
106
|
+
status: 'queued',
|
|
107
|
+
provider: adapter.provider,
|
|
108
|
+
model: o.model ??
|
|
109
|
+
yaml?.agents?.models?.[o.role] ??
|
|
110
|
+
yaml?.agents?.models?.default ??
|
|
111
|
+
adapter.defaultModel,
|
|
112
|
+
input: o.input,
|
|
113
|
+
startedAt: new Date().toISOString(),
|
|
114
|
+
proposalIds: [],
|
|
115
|
+
budgetUsd: o.budgetUsd ??
|
|
116
|
+
yaml?.agents?.budgetUsd?.[o.role] ??
|
|
117
|
+
yaml?.agents?.budgetUsd?.default ??
|
|
118
|
+
def.defaults.budgetUsd,
|
|
119
|
+
maxTurns: o.maxTurns ?? yaml?.agents?.maxTurns?.[o.role] ?? def.defaults.maxTurns,
|
|
120
|
+
dryRun: o.dryRun,
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
job,
|
|
124
|
+
ctx,
|
|
125
|
+
registry,
|
|
126
|
+
system,
|
|
127
|
+
prompt,
|
|
128
|
+
tools,
|
|
129
|
+
adapter,
|
|
130
|
+
mcpServers,
|
|
131
|
+
needsBrowser: def.needsBrowser,
|
|
132
|
+
profile,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export async function runJob(o) {
|
|
136
|
+
const prep = await prepareJob(o);
|
|
137
|
+
const { job, ctx, adapter, system, prompt, tools, mcpServers, needsBrowser, profile } = prep;
|
|
138
|
+
const journal = new JobJournal(ctx.rootDir);
|
|
139
|
+
const before = new Set(new ProposalStore(ctx.rootDir).list().map((p) => p.id));
|
|
140
|
+
const events = [];
|
|
141
|
+
job.status = 'running';
|
|
142
|
+
journal.save(job);
|
|
143
|
+
o.onEvent?.({
|
|
144
|
+
type: 'status',
|
|
145
|
+
message: `profile ${profile.profile} (${profile.reason}) · ${tools.length} tool(s)`,
|
|
146
|
+
});
|
|
147
|
+
try {
|
|
148
|
+
const result = await adapter.runAgent({
|
|
149
|
+
system,
|
|
150
|
+
prompt,
|
|
151
|
+
tools,
|
|
152
|
+
mcpServers,
|
|
153
|
+
// A browser role keeps its browser unless the profile (or the caller) says otherwise.
|
|
154
|
+
needsBrowser: needsBrowser && (o.browser ?? profile.attachBrowser),
|
|
155
|
+
maxToolCallsPerTurn: profile.maxToolCallsPerTurn,
|
|
156
|
+
firstTurnToolChoice: profile.firstTurnToolChoice,
|
|
157
|
+
maxBridgedTools: profile.maxBridgedTools,
|
|
158
|
+
recoverTextToolCalls: profile.recoverTextToolCalls,
|
|
159
|
+
maxTokens: profile.maxTokens,
|
|
160
|
+
model: job.model,
|
|
161
|
+
maxTurns: profile.maxTurns ? Math.min(profile.maxTurns, job.maxTurns) : job.maxTurns,
|
|
162
|
+
maxBudgetUsd: job.budgetUsd,
|
|
163
|
+
cwd: ctx.rootDir,
|
|
164
|
+
signal: o.signal,
|
|
165
|
+
onEvent: (e) => {
|
|
166
|
+
if (o.keepEvents)
|
|
167
|
+
events.push(e);
|
|
168
|
+
o.onEvent?.(e);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
job.costUsd = result.costUsd;
|
|
172
|
+
job.turns = result.turns;
|
|
173
|
+
job.toolCalls = result.toolCalls;
|
|
174
|
+
job.resultText = result.text;
|
|
175
|
+
job.proposalIds = new ProposalStore(ctx.rootDir)
|
|
176
|
+
.list()
|
|
177
|
+
.map((p) => p.id)
|
|
178
|
+
.filter((id) => !before.has(id));
|
|
179
|
+
job.status = result.isError
|
|
180
|
+
? 'failed'
|
|
181
|
+
: job.proposalIds.length
|
|
182
|
+
? 'awaiting_review'
|
|
183
|
+
: 'completed';
|
|
184
|
+
if (result.isError)
|
|
185
|
+
job.error = result.stopReason;
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
job.status = o.signal?.aborted ? 'cancelled' : 'failed';
|
|
189
|
+
job.error = e.message;
|
|
190
|
+
}
|
|
191
|
+
job.finishedAt = new Date().toISOString();
|
|
192
|
+
if (o.keepEvents)
|
|
193
|
+
job.events = events;
|
|
194
|
+
journal.save(job);
|
|
195
|
+
return job;
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ROLE_PROMPTS, type RoleName } from '@sdods/mcp/prompts';
|
|
2
|
+
import { type ToolContext, type ToolRegistry } from '@sdods/mcp';
|
|
3
|
+
import type { ProfileName } from '../adapter/profile.js';
|
|
4
|
+
export type { RoleName };
|
|
5
|
+
export { ROLE_PROMPTS };
|
|
6
|
+
export interface RoleInput {
|
|
7
|
+
project?: string;
|
|
8
|
+
env?: string;
|
|
9
|
+
goal?: string;
|
|
10
|
+
plan?: string;
|
|
11
|
+
scenario?: string;
|
|
12
|
+
runId?: string;
|
|
13
|
+
diff?: string;
|
|
14
|
+
files?: string[];
|
|
15
|
+
spec?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RoleDefinition {
|
|
18
|
+
role: RoleName;
|
|
19
|
+
/** which tools the role may call (name prefixes) */
|
|
20
|
+
toolPrefixes: string[];
|
|
21
|
+
/**
|
|
22
|
+
* The tools this role keeps under the small profile — named exactly, not by prefix, because
|
|
23
|
+
* the point is a menu a 7B model can read. Ordered by when the role should reach for them.
|
|
24
|
+
*/
|
|
25
|
+
smallTools: string[];
|
|
26
|
+
needsBrowser: boolean;
|
|
27
|
+
/** budget defaults when the project yaml does not set them */
|
|
28
|
+
defaults: {
|
|
29
|
+
maxTurns: number;
|
|
30
|
+
budgetUsd: number;
|
|
31
|
+
};
|
|
32
|
+
buildPrompt(input: RoleInput): string;
|
|
33
|
+
/**
|
|
34
|
+
* The same request for a small model: one action, and only tools the small profile actually
|
|
35
|
+
* has. The full prompts name a sequence of three calls, which a 7B model answers by describing
|
|
36
|
+
* all three instead of making the first.
|
|
37
|
+
*/
|
|
38
|
+
buildSmallPrompt(input: RoleInput): string;
|
|
39
|
+
}
|
|
40
|
+
export declare const ROLES: Record<RoleName, RoleDefinition>;
|
|
41
|
+
/** The user-facing request for a role, in the shape the profile can act on. */
|
|
42
|
+
export declare function rolePrompt(role: RoleName, input: RoleInput, profile?: ProfileName): string;
|
|
43
|
+
export declare function roleSystemPrompt(role: RoleName, input: RoleInput, steps?: string[], profile?: ProfileName): string;
|
|
44
|
+
/**
|
|
45
|
+
* Tools visible to a role. The full profile filters by prefix; the small profile takes the
|
|
46
|
+
* role's named allowlist, in its declared order, so the model reads a short list of things it is
|
|
47
|
+
* expected to do rather than a catalogue.
|
|
48
|
+
*/
|
|
49
|
+
export declare function roleTools(role: RoleName, registry: ToolRegistry, ctx: ToolContext, profile?: ProfileName): import("@sdods/mcp").AgentSdkToolDef[];
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|