specrails-core 6.0.1 → 6.1.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/README.md +1 -1
- package/dist/agent-runtime/capabilities.js +3 -1
- package/dist/agent-runtime/cli-executor.d.ts +3 -0
- package/dist/agent-runtime/cli-executor.js +50 -13
- package/dist/agent-runtime/cli-process.js +6 -2
- package/dist/agent-runtime/cli.d.ts +15 -0
- package/dist/agent-runtime/cli.js +43 -9
- package/dist/agent-runtime/compact/chat-client.js +2 -1
- package/dist/agent-runtime/config.d.ts +4 -1
- package/dist/agent-runtime/config.js +41 -3
- package/dist/agent-runtime/core-host.d.ts +9 -3
- package/dist/agent-runtime/core-host.js +21 -18
- package/dist/agent-runtime/efficiency-summary.d.ts +4 -1
- package/dist/agent-runtime/efficiency-summary.js +30 -27
- package/dist/agent-runtime/efficiency-types.d.ts +4 -4
- package/dist/agent-runtime/efficiency.d.ts +1 -1
- package/dist/agent-runtime/efficiency.js +7 -6
- package/dist/agent-runtime/engine/__fixtures__/implementation-component.json +90 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.d.ts +11 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +60 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation.json +43 -0
- package/dist/agent-runtime/engine/budget.d.ts +9 -0
- package/dist/agent-runtime/engine/budget.js +33 -0
- package/dist/agent-runtime/engine/canonical-json.d.ts +9 -0
- package/dist/agent-runtime/engine/canonical-json.js +162 -0
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +67 -0
- package/dist/agent-runtime/engine/checkpoint/database.js +304 -0
- package/dist/agent-runtime/engine/checkpoint/lease.d.ts +17 -0
- package/dist/agent-runtime/engine/checkpoint/lease.js +82 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +133 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.js +510 -0
- package/dist/agent-runtime/engine/checkpoint/saver.d.ts +25 -0
- package/dist/agent-runtime/engine/checkpoint/saver.js +146 -0
- package/dist/agent-runtime/engine/cli.d.ts +10 -0
- package/dist/agent-runtime/engine/cli.js +172 -0
- package/dist/agent-runtime/engine/compiler.d.ts +23 -0
- package/dist/agent-runtime/engine/compiler.js +261 -0
- package/dist/agent-runtime/engine/composition.d.ts +16 -0
- package/dist/agent-runtime/engine/composition.js +28 -0
- package/dist/agent-runtime/engine/concurrency.d.ts +14 -0
- package/dist/agent-runtime/engine/concurrency.js +55 -0
- package/dist/agent-runtime/engine/contracts.d.ts +273 -0
- package/dist/agent-runtime/engine/contracts.js +11 -0
- package/dist/agent-runtime/engine/definition-schema.d.ts +250 -0
- package/dist/agent-runtime/engine/definition-schema.js +313 -0
- package/dist/agent-runtime/engine/definition-types.d.ts +84 -0
- package/dist/agent-runtime/engine/definition-types.js +3 -0
- package/dist/agent-runtime/engine/definition-validator.d.ts +7 -0
- package/dist/agent-runtime/engine/definition-validator.js +187 -0
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +62 -0
- package/dist/agent-runtime/engine/efficiency-summary.js +31 -0
- package/dist/agent-runtime/engine/events.d.ts +19 -0
- package/dist/agent-runtime/engine/events.js +88 -0
- package/dist/agent-runtime/engine/execution.d.ts +29 -0
- package/dist/agent-runtime/engine/execution.js +121 -0
- package/dist/agent-runtime/engine/expressions.d.ts +11 -0
- package/dist/agent-runtime/engine/expressions.js +172 -0
- package/dist/agent-runtime/engine/fork.d.ts +28 -0
- package/dist/agent-runtime/engine/fork.js +211 -0
- package/dist/agent-runtime/engine/graph-description.d.ts +48 -0
- package/dist/agent-runtime/engine/graph-description.js +16 -0
- package/dist/agent-runtime/engine/implementation-compiler.d.ts +214 -0
- package/dist/agent-runtime/engine/implementation-compiler.js +91 -0
- package/dist/agent-runtime/engine/index.d.ts +14 -0
- package/dist/agent-runtime/engine/index.js +14 -0
- package/dist/agent-runtime/engine/invocation-context.d.ts +11 -0
- package/dist/agent-runtime/engine/invocation-context.js +128 -0
- package/dist/agent-runtime/engine/otel.d.ts +15 -0
- package/dist/agent-runtime/engine/otel.js +132 -0
- package/dist/agent-runtime/engine/output.d.ts +4 -0
- package/dist/agent-runtime/engine/output.js +73 -0
- package/dist/agent-runtime/engine/piece-dependencies.d.ts +50 -0
- package/dist/agent-runtime/engine/piece-dependencies.js +193 -0
- package/dist/agent-runtime/engine/piece-registry.d.ts +19 -0
- package/dist/agent-runtime/engine/piece-registry.js +62 -0
- package/dist/agent-runtime/engine/pieces/assign.d.ts +4 -0
- package/dist/agent-runtime/engine/pieces/assign.js +35 -0
- package/dist/agent-runtime/engine/pieces/control.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/control.js +49 -0
- package/dist/agent-runtime/engine/pieces/decider.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/decider.js +70 -0
- package/dist/agent-runtime/engine/pieces/implementation-binding.d.ts +12 -0
- package/dist/agent-runtime/engine/pieces/implementation-binding.js +40 -0
- package/dist/agent-runtime/engine/pieces/implementation-journal.d.ts +25 -0
- package/dist/agent-runtime/engine/pieces/implementation-journal.js +192 -0
- package/dist/agent-runtime/engine/pieces/implementation.d.ts +23 -0
- package/dist/agent-runtime/engine/pieces/implementation.js +176 -0
- package/dist/agent-runtime/engine/pieces/index.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/index.js +22 -0
- package/dist/agent-runtime/engine/pieces/openspec.d.ts +3 -0
- package/dist/agent-runtime/engine/pieces/openspec.js +69 -0
- package/dist/agent-runtime/engine/pieces/ports.d.ts +48 -0
- package/dist/agent-runtime/engine/pieces/ports.js +1 -0
- package/dist/agent-runtime/engine/pieces/project-memory.d.ts +3 -0
- package/dist/agent-runtime/engine/pieces/project-memory.js +13 -0
- package/dist/agent-runtime/engine/pieces/prompt.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/prompt.js +130 -0
- package/dist/agent-runtime/engine/pieces/role-turn.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +73 -0
- package/dist/agent-runtime/engine/pieces/shared.d.ts +67 -0
- package/dist/agent-runtime/engine/pieces/shared.js +40 -0
- package/dist/agent-runtime/engine/pieces/shell.d.ts +6 -0
- package/dist/agent-runtime/engine/pieces/shell.js +47 -0
- package/dist/agent-runtime/engine/pieces/verify.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/verify.js +52 -0
- package/dist/agent-runtime/engine/preflight.d.ts +27 -0
- package/dist/agent-runtime/engine/preflight.js +54 -0
- package/dist/agent-runtime/engine/regex.d.ts +3 -0
- package/dist/agent-runtime/engine/regex.js +19 -0
- package/dist/agent-runtime/engine/run-status.d.ts +563 -0
- package/dist/agent-runtime/engine/run-status.js +61 -0
- package/dist/agent-runtime/engine/runs.d.ts +605 -0
- package/dist/agent-runtime/engine/runs.js +247 -0
- package/dist/agent-runtime/engine/state.d.ts +64 -0
- package/dist/agent-runtime/engine/state.js +98 -0
- package/dist/agent-runtime/engine/steering/inbox.d.ts +54 -0
- package/dist/agent-runtime/engine/steering/inbox.js +113 -0
- package/dist/agent-runtime/engine/storage/fork-allocation.d.ts +21 -0
- package/dist/agent-runtime/engine/storage/fork-allocation.js +143 -0
- package/dist/agent-runtime/engine/storage/private-path.d.ts +11 -0
- package/dist/agent-runtime/engine/storage/private-path.js +74 -0
- package/dist/agent-runtime/engine/store/sqlite-store.d.ts +26 -0
- package/dist/agent-runtime/engine/store/sqlite-store.js +227 -0
- package/dist/agent-runtime/evaluation-corpus.d.ts +125 -0
- package/dist/agent-runtime/evaluation-corpus.js +4 -0
- package/dist/agent-runtime/evaluation.d.ts +9 -3
- package/dist/agent-runtime/evaluation.js +50 -8
- package/dist/agent-runtime/executor-types.d.ts +53 -3
- package/dist/agent-runtime/executor-types.js +35 -2
- package/dist/agent-runtime/executors.js +2 -2
- package/dist/agent-runtime/graph/artifacts.d.ts +7 -0
- package/dist/agent-runtime/graph/artifacts.js +82 -9
- package/dist/agent-runtime/graph/nodes.d.ts +2 -0
- package/dist/agent-runtime/graph/nodes.js +2 -2
- package/dist/agent-runtime/graph/roles.d.ts +6 -0
- package/dist/agent-runtime/graph/roles.js +23 -17
- package/dist/agent-runtime/kimi-acp.js +6 -6
- package/dist/agent-runtime/openai-executor.js +17 -12
- package/dist/agent-runtime/openspec.d.ts +17 -2
- package/dist/agent-runtime/openspec.js +54 -20
- package/dist/agent-runtime/prompts.d.ts +4 -3
- package/dist/agent-runtime/prompts.js +26 -5
- package/dist/agent-runtime/recovery.d.ts +1 -1
- package/dist/agent-runtime/repository-context.js +1 -1
- package/dist/agent-runtime/role-routing.js +4 -2
- package/dist/agent-runtime/role-state.d.ts +5 -0
- package/dist/agent-runtime/verification-plan.d.ts +2 -0
- package/dist/agent-runtime/verification-plan.js +10 -0
- package/dist/agent-runtime/workflow-types.d.ts +2 -0
- package/dist/agent-runtime/workflow.d.ts +3 -1
- package/dist/agent-runtime/workflow.js +7 -3
- package/dist/agent-runtime/workspace-tools.d.ts +2 -2
- package/dist/agent-runtime/workspace-tools.js +6 -6
- package/dist/installer/cli.js +5 -0
- package/dist/installer/phases/scaffold.d.ts +2 -2
- package/dist/installer/phases/scaffold.js +3 -7
- package/dist/pipeline/pipeline-state.d.ts +38 -4
- package/dist/pipeline/pipeline-state.js +94 -21
- package/integration-contract.json +83 -11
- package/package.json +12 -5
- package/schemas/agent-runtime.schema.json +93 -1
- package/schemas/workflow-definition.schema.json +312 -0
- package/templates/kimi/specrails/run-skill.mjs +15 -1
- package/templates/commands/specrails/batch-implement.md +0 -32
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
The agent workflow engine behind [Specrails Desktop](https://github.com/fjpulidop/specrails-desktop). Core does two things:
|
|
8
8
|
|
|
9
|
-
1. **Installs** the specrails workflow into a project for Claude Code, Codex CLI, Gemini CLI or Kimi Code: three roles (architect, developer, reviewer), the `implement`
|
|
9
|
+
1. **Installs** the specrails workflow into a project for Claude Code, Codex CLI, Gemini CLI or Kimi Code: three roles (architect, developer, reviewer), the `implement` and `retry` entry points (`implement` runs one ticket or several tickets as one aggregate run) and the official OpenSpec skills.
|
|
10
10
|
2. **Runs** implementations through the [programmatic agent runtime](docs/agent-runtime.md): a LangGraph workflow (architect → developer → verify → reviewer → archive) with per-role providers, deterministic verification, durable recovery and OpenAI-compatible local models.
|
|
11
11
|
|
|
12
12
|
Desktop bundles a pinned Core package and owns everything around it: worktrees, commits, pull requests, backlog and the user interface. Core never ships code on its own; it prepares a reviewed candidate for the host.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveRoleDescriptor, roleIds } from './config.js';
|
|
1
2
|
import { readFileSync, statSync } from 'node:fs';
|
|
2
3
|
import { homedir, tmpdir } from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -71,7 +72,8 @@ export async function configuredCapabilities(config, registry) {
|
|
|
71
72
|
return probes.get(key);
|
|
72
73
|
};
|
|
73
74
|
const roles = [];
|
|
74
|
-
for (const
|
|
75
|
+
for (const role of roleIds(config)) {
|
|
76
|
+
const selected = resolveRoleDescriptor(config, role);
|
|
75
77
|
for (const [tier, choice] of [['base', selected], ...(selected.escalation ? [['escalation', selected.escalation]] : [])]) {
|
|
76
78
|
roles.push({ role, tier, provider: selected.provider, model: choice.model ?? null, requestedEffort: choice.effort ?? null, ...await probe(selected.provider, choice.model) });
|
|
77
79
|
}
|
|
@@ -21,6 +21,9 @@ interface CliInvocationOptions {
|
|
|
21
21
|
* developer that cannot run tests iterates blind and fails verification.
|
|
22
22
|
*/
|
|
23
23
|
export declare function buildCliInvocation(provider: CliProvider, request: AgentRequest, options?: CliInvocationOptions): CliInvocation;
|
|
24
|
+
/** Native commands are data rendered for the selected provider; never shell text. */
|
|
25
|
+
export declare function renderNativeCommand(provider: Exclude<CliProvider, 'kimi'>, command: NonNullable<AgentRequest['nativeCommand']>): string;
|
|
26
|
+
export declare function kimiNativeSkill(id: string): string;
|
|
24
27
|
interface ParsedCliResult extends AgentResult {
|
|
25
28
|
terminal: boolean;
|
|
26
29
|
failed: boolean;
|
|
@@ -2,13 +2,13 @@ import { codexOutputSchema, restoreOptionalFields } from './codex-schema.js';
|
|
|
2
2
|
import { assertEffortSupported, cliCapabilities } from './capabilities.js';
|
|
3
3
|
import { providerDiagnostic } from './provider-diagnostic.js';
|
|
4
4
|
import { toolEvent } from './tool-event.js';
|
|
5
|
-
import { openSpecPrompt, writeOpenSpecBridge } from './openspec.js';
|
|
5
|
+
import { openSpecPrompt, openSpecPolicy, writeOpenSpecBridge } from './openspec.js';
|
|
6
6
|
import { existsSync, readFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { sumCacheUsage } from './efficiency-types.js';
|
|
10
10
|
import { normalizeKimiCliModel } from './kimi-model.js';
|
|
11
|
-
import { AgentExecutionError, unknownUsage,
|
|
11
|
+
import { AgentExecutionError, unknownUsage, normalizeAgentRequest, requestPolicy, isBuiltinRole } from './executor-types.js';
|
|
12
12
|
import { runCliProcess } from './cli-process.js';
|
|
13
13
|
import { canonicalWorkspace } from './workspace-tools.js';
|
|
14
14
|
import { parseStructuredText } from './openai-executor.js';
|
|
@@ -23,22 +23,29 @@ const CLAUDE_DEVELOPER_DISALLOWED = 'Agent,Task,Skill';
|
|
|
23
23
|
* developer that cannot run tests iterates blind and fails verification.
|
|
24
24
|
*/
|
|
25
25
|
export function buildCliInvocation(provider, request, options = {}) {
|
|
26
|
-
|
|
26
|
+
if (!isBuiltinRole(request.role) || request.access !== undefined || request.nativeCommand)
|
|
27
|
+
request = normalizeAgentRequest(request);
|
|
28
|
+
if (request.nativeCommand) {
|
|
29
|
+
if (provider === 'kimi')
|
|
30
|
+
throw new AgentExecutionError('Kimi commands require skill rendering before invocation', 'native_command_unsupported');
|
|
31
|
+
request = { ...request, prompt: renderNativeCommand(provider, request.nativeCommand) };
|
|
32
|
+
}
|
|
33
|
+
const readOnly = requestPolicy(request).access === 'read';
|
|
27
34
|
// Pin the product alias; explicit IDs remain reproducible across releases.
|
|
28
35
|
const modelId = provider === 'claude' && request.model === 'opus' ? 'claude-opus-5-5' : request.model;
|
|
29
36
|
const model = modelId ? ['--model', modelId] : [];
|
|
30
37
|
const extraRoots = request.allowedRoots.filter(root => root !== request.cwd);
|
|
31
38
|
const resume = request.resumeSessionId;
|
|
32
39
|
switch (provider) {
|
|
33
|
-
case 'claude': return { command: 'claude', stdin: request.prompt, args: [
|
|
34
|
-
'-p', '--output-format', 'stream-json', '--verbose', '--max-turns', String(request.maxTurns ?? 100), ...model,
|
|
40
|
+
case 'claude': return { command: 'claude', ...(request.nativeCommand ? {} : { stdin: request.prompt }), args: [
|
|
41
|
+
'-p', ...(request.nativeCommand ? [request.prompt] : []), '--output-format', 'stream-json', '--verbose', '--max-turns', String(request.maxTurns ?? 100), ...model,
|
|
35
42
|
...(request.effort === undefined ? [] : ['--effort', request.effort]),
|
|
36
43
|
// Project instructions and rules stay visible; the user's global config,
|
|
37
44
|
// memory and plugins never leak into an autonomous role.
|
|
38
45
|
'--setting-sources', 'project,local',
|
|
39
46
|
...(readOnly
|
|
40
|
-
? ['--tools', options.mcpConfigFile ? 'Read,Grep,Glob,ToolSearch' : 'Read,Grep,Glob', '--permission-mode', options.mcpConfigFile ? 'dontAsk' : 'plan', '--strict-mcp-config', ...(options.mcpConfigFile ? ['--allowedTools', 'Read,Grep,Glob,ToolSearch,mcp__specrails_openspec__workflow,mcp__specrails_openspec__read_verification_evidence'] : [])]
|
|
41
|
-
: ['--tools', 'default', '--disallowedTools', CLAUDE_DEVELOPER_DISALLOWED, '--dangerously-skip-permissions']),
|
|
47
|
+
? ['--tools', request.nativeCommand ? 'Read,Grep,Glob,Skill' : options.mcpConfigFile ? 'Read,Grep,Glob,ToolSearch' : 'Read,Grep,Glob', '--permission-mode', options.mcpConfigFile ? 'dontAsk' : 'plan', '--strict-mcp-config', ...(options.mcpConfigFile ? ['--allowedTools', 'Read,Grep,Glob,ToolSearch,mcp__specrails_openspec__workflow,mcp__specrails_openspec__read_verification_evidence'] : [])]
|
|
48
|
+
: ['--tools', 'default', '--disallowedTools', request.nativeCommand ? 'Agent,Task' : CLAUDE_DEVELOPER_DISALLOWED, '--dangerously-skip-permissions']),
|
|
42
49
|
...(options.mcpConfigFile ? ['--mcp-config', options.mcpConfigFile] : []),
|
|
43
50
|
...(request.outputSchema ? ['--json-schema', JSON.stringify(request.outputSchema)] : []),
|
|
44
51
|
...(request.maxCostUsd === undefined ? [] : ['--max-budget-usd', String(request.maxCostUsd)]),
|
|
@@ -63,8 +70,8 @@ export function buildCliInvocation(provider, request, options = {}) {
|
|
|
63
70
|
case 'gemini': {
|
|
64
71
|
if (readOnly && !options.geminiPolicyFile)
|
|
65
72
|
throw new AgentExecutionError('Gemini architect/reviewer roles require --admin-policy support for an enforced read-only tool allowlist. Upgrade Gemini CLI or select another provider for this role.', 'provider_capability_unsupported');
|
|
66
|
-
return { command: 'gemini', stdin: request.prompt, args: [
|
|
67
|
-
'-p', 'Execute the task supplied on stdin.', '--output-format', 'stream-json', ...model,
|
|
73
|
+
return { command: 'gemini', ...(request.nativeCommand ? {} : { stdin: request.prompt }), args: [
|
|
74
|
+
'-p', request.nativeCommand ? request.prompt : 'Execute the task supplied on stdin.', '--output-format', 'stream-json', ...model,
|
|
68
75
|
// Headless Gemini cannot answer shell approvals; the developer runs with
|
|
69
76
|
// auto-approval exactly as the legacy Implement step did.
|
|
70
77
|
...(readOnly ? ['--approval-mode', 'plan'] : ['--yolo']),
|
|
@@ -86,6 +93,34 @@ export function buildCliInvocation(provider, request, options = {}) {
|
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
}
|
|
96
|
+
/** Native commands are data rendered for the selected provider; never shell text. */
|
|
97
|
+
export function renderNativeCommand(provider, command) {
|
|
98
|
+
return (provider === 'codex' ? '$' : '/') + command.id + (command.args ? ' ' + command.args : '');
|
|
99
|
+
}
|
|
100
|
+
export function kimiNativeSkill(id) {
|
|
101
|
+
const openspec = { 'opsx:ff': 'openspec-ff-change', 'opsx:apply': 'openspec-apply-change', 'opsx:verify': 'openspec-verify-change' };
|
|
102
|
+
const skill = openspec[id] ?? (id.startsWith('specrails:') ? id.replace('specrails:', 'specrails-') : id);
|
|
103
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(skill))
|
|
104
|
+
throw new AgentExecutionError('Kimi native command is unsupported', 'native_command_unsupported');
|
|
105
|
+
return skill;
|
|
106
|
+
}
|
|
107
|
+
async function renderKimiRequest(request, options) {
|
|
108
|
+
const skill = kimiNativeSkill(request.nativeCommand.id);
|
|
109
|
+
const runnerPath = path.join(request.cwd, '.kimi-code', 'specrails', 'run-skill.mjs');
|
|
110
|
+
if (!existsSync(runnerPath))
|
|
111
|
+
throw new AgentExecutionError('Kimi managed skill runner is not installed', 'native_command_unsupported');
|
|
112
|
+
const result = await (options.runProcess ?? runCliProcess)({ command: process.execPath, args: [runnerPath, '--skill', skill, '--model', request.model ?? 'kimi-for-coding', '--args', request.nativeCommand.args ?? '', ...(request.resumeSessionId ? ['--session', request.resumeSessionId] : []), '--render-only'] }, { cwd: request.cwd, signal: request.signal, timeoutMs: 10000, env: options.env });
|
|
113
|
+
if (result.exitCode !== 0)
|
|
114
|
+
throw new AgentExecutionError('Kimi native skill cannot be rendered', 'native_command_unsupported');
|
|
115
|
+
let prompt;
|
|
116
|
+
try {
|
|
117
|
+
prompt = JSON.parse(result.stdout).prompt;
|
|
118
|
+
}
|
|
119
|
+
catch { /* Invalid runner output cannot become a provider prompt. */ }
|
|
120
|
+
if (typeof prompt !== 'string' || !prompt.trim() || prompt.includes('\0') || Buffer.byteLength(prompt) > 1_048_576)
|
|
121
|
+
throw new AgentExecutionError('Invalid Kimi native skill prompt', 'native_command_unsupported');
|
|
122
|
+
return { ...request, prompt, nativeCommand: undefined };
|
|
123
|
+
}
|
|
89
124
|
function object(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
|
|
90
125
|
function number(value) { return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null; }
|
|
91
126
|
function textBlocks(value) {
|
|
@@ -244,7 +279,7 @@ export class CliExecutor {
|
|
|
244
279
|
capabilities(model) { return cliCapabilities(this.provider, model, this.options); }
|
|
245
280
|
capabilityChecks = new Map();
|
|
246
281
|
async validateOpenSpec(context) {
|
|
247
|
-
const key = context.
|
|
282
|
+
const key = openSpecPolicy(context).access;
|
|
248
283
|
let checked = this.capabilityChecks.get(key);
|
|
249
284
|
if (!checked) {
|
|
250
285
|
checked = (async () => {
|
|
@@ -266,11 +301,13 @@ export class CliExecutor {
|
|
|
266
301
|
throw new AgentExecutionError('Kimi does not report authoritative token usage. Remove the token cap or select another provider for this role.', 'usage_unavailable');
|
|
267
302
|
}
|
|
268
303
|
async execute(request) {
|
|
269
|
-
|
|
304
|
+
request = normalizeAgentRequest(request);
|
|
270
305
|
if (request.effort !== undefined)
|
|
271
306
|
assertEffortSupported(request, await this.capabilities(request.model));
|
|
272
307
|
this.validateLimits(request);
|
|
273
308
|
const scope = canonicalWorkspace(request.cwd, request.allowedRoots);
|
|
309
|
+
if (this.provider === 'kimi' && request.nativeCommand)
|
|
310
|
+
request = await renderKimiRequest(request, this.options);
|
|
274
311
|
const normalized = { ...request, prompt: (request.openspec ? openSpecPrompt(request.openspec) : '') + request.prompt, cwd: scope.cwd, allowedRoots: scope.roots };
|
|
275
312
|
const runner = this.options.runProcess ?? runCliProcess;
|
|
276
313
|
let temporary, kimiAgentFile, geminiPolicyFile, codexSchemaFile, stream = '', turns = 0, observedSession;
|
|
@@ -295,7 +332,7 @@ export class CliExecutor {
|
|
|
295
332
|
if (this.provider === 'kimi')
|
|
296
333
|
return await executeKimiReadonlyAcp(normalized, { ...this.options, openspecBridge });
|
|
297
334
|
}
|
|
298
|
-
if (this.provider === 'gemini' && request.
|
|
335
|
+
if (this.provider === 'gemini' && requestPolicy(request).access === 'read') {
|
|
299
336
|
assertGeminiAdminPolicyAvailable();
|
|
300
337
|
const help = await runner({ command: 'gemini', args: ['--help'] }, { cwd: scope.cwd, signal: request.signal, timeoutMs: 10_000, env: this.options.env });
|
|
301
338
|
if (help.exitCode !== 0 || !help.stdout.includes('--admin-policy'))
|
|
@@ -303,7 +340,7 @@ export class CliExecutor {
|
|
|
303
340
|
geminiPolicyFile = path.join(scratch(), 'readonly.toml');
|
|
304
341
|
writeFileSync(geminiPolicyFile, (openspecBridge ? '[[rule]]\nmcpName = "specrails_openspec"\ntoolName = "workflow"\ndecision = "allow"\npriority = 1000\n\n[[rule]]\nmcpName = "specrails_openspec"\ntoolName = "read_verification_evidence"\ndecision = "allow"\npriority = 1000\n\n' : '') + GEMINI_READONLY_POLICY, { mode: 0o600 });
|
|
305
342
|
}
|
|
306
|
-
if (this.provider === 'kimi' && request.
|
|
343
|
+
if (this.provider === 'kimi' && requestPolicy(request).access === 'read') {
|
|
307
344
|
const help = await runner({ command: 'kimi', args: ['--help'] }, { cwd: scope.cwd, signal: request.signal, timeoutMs: 10_000, env: this.options.env });
|
|
308
345
|
if (help.exitCode !== 0)
|
|
309
346
|
throw new AgentExecutionError('Cannot detect Kimi CLI capabilities', 'provider_capability_unsupported');
|
|
@@ -62,6 +62,10 @@ export function cliProcessEnvironment(base, platform = process.platform) {
|
|
|
62
62
|
export const runCliProcess = async (raw, options) => {
|
|
63
63
|
if (options.signal?.aborted)
|
|
64
64
|
throw new AgentExecutionError('Agent cancelled', 'aborted');
|
|
65
|
+
for (const value of [options.timeoutMs, options.idleTimeoutMs]) {
|
|
66
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0 || value > 2_147_483_647))
|
|
67
|
+
throw new AgentExecutionError('Invalid process timeout', 'invalid_limit');
|
|
68
|
+
}
|
|
65
69
|
const env = cliProcessEnvironment(options.env ?? process.env);
|
|
66
70
|
const invocation = process.platform === 'win32' && raw.command === 'kimi' ? windowsKimiInvocation(raw, { env }) : raw;
|
|
67
71
|
return new Promise((resolve, reject) => {
|
|
@@ -93,12 +97,12 @@ export const runCliProcess = async (raw, options) => {
|
|
|
93
97
|
};
|
|
94
98
|
const fail = (error) => { failure ??= error; terminate(); };
|
|
95
99
|
const abort = () => fail(new AgentExecutionError('Agent cancelled', 'aborted'));
|
|
96
|
-
const timer = setTimeout(() => fail(new AgentExecutionError(`Agent reached the total execution limit (${options.timeoutMs} ms)`, 'timeout')), options.timeoutMs);
|
|
100
|
+
const timer = options.timeoutMs > 0 ? setTimeout(() => fail(new AgentExecutionError(`Agent reached the total execution limit (${options.timeoutMs} ms)`, 'timeout')), options.timeoutMs) : undefined;
|
|
97
101
|
let idleTimer;
|
|
98
102
|
const activity = () => {
|
|
99
103
|
if (idleTimer)
|
|
100
104
|
clearTimeout(idleTimer);
|
|
101
|
-
if (options.idleTimeoutMs !== undefined && !failure)
|
|
105
|
+
if (options.idleTimeoutMs !== undefined && options.idleTimeoutMs > 0 && !failure)
|
|
102
106
|
idleTimer = setTimeout(() => fail(new AgentExecutionError(`Agent produced no output for ${options.idleTimeoutMs} ms (idle timeout)`, 'idle_timeout')), options.idleTimeoutMs);
|
|
103
107
|
};
|
|
104
108
|
activity();
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { WorkflowState } from './workflow-types.js';
|
|
2
|
+
/** Both runtime entry points emit the same machine-readable fatal result. */
|
|
3
|
+
export declare function runtimeFailure(error: unknown): {
|
|
4
|
+
type: string;
|
|
5
|
+
status: string;
|
|
6
|
+
error: string | {
|
|
7
|
+
details?: import("./workflow-types.js").JsonValue | undefined;
|
|
8
|
+
code: string;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
/** Machine operations are mirrored in integration-contract.json; help is presentation-only. */
|
|
13
|
+
export declare const RUNTIME_CLI_OPERATIONS: readonly ["api", "validate", "run", "status", "resume", "prompts", "capabilities", "evidence", "recovery", "evaluate", "workflows", "fork", "signal", "cancel", "help"];
|
|
2
14
|
/** The host-facing summary: phase status, pending interrupts and usage without accumulated outputs. */
|
|
3
15
|
export declare function compactState(state: WorkflowState | null): {
|
|
4
16
|
runId: string;
|
|
@@ -44,4 +56,7 @@ export declare function compactState(state: WorkflowState | null): {
|
|
|
44
56
|
}[];
|
|
45
57
|
metrics: import("./efficiency-types.js").RuntimeEfficiency;
|
|
46
58
|
} | null;
|
|
59
|
+
/** Engines this package can launch: the legacy built-in pipeline (1) and workflow definitions (2).
|
|
60
|
+
* Core 7 advertises [2] so hosts refuse unconverted legacy loops before admission. */
|
|
61
|
+
export declare const RUNTIME_ENGINES: readonly number[];
|
|
47
62
|
export declare function runRuntimeCommand(flags: Record<string, string | boolean>, positionals: string[], emit?: (value: unknown) => void): Promise<number>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isDefinitionCommand, NODE_KINDS_VERSION, runDefinitionCommand } from './engine/cli.js';
|
|
2
|
+
import { validationPieceRegistry } from './engine/pieces/index.js';
|
|
1
3
|
import { GUARDRAIL_CATALOG } from './guardrails.js';
|
|
2
4
|
import { runEvaluation } from './evaluation.js';
|
|
3
5
|
import { efficiencySummary } from './efficiency-summary.js';
|
|
@@ -15,6 +17,15 @@ import { runtimeEfficiency } from './efficiency.js';
|
|
|
15
17
|
import { configuredCapabilities } from './capabilities.js';
|
|
16
18
|
import { createExecutorRegistry } from './executors.js';
|
|
17
19
|
import { runRecovery } from './recovery.js';
|
|
20
|
+
import { EngineError } from './engine/contracts.js';
|
|
21
|
+
/** Both runtime entry points emit the same machine-readable fatal result. */
|
|
22
|
+
export function runtimeFailure(error) {
|
|
23
|
+
return { type: 'runtime-result', status: 'failed', error: error instanceof EngineError
|
|
24
|
+
? { code: error.code, message: error.message, ...(error.details === undefined ? {} : { details: error.details }) }
|
|
25
|
+
: error instanceof Error ? error.message : String(error) };
|
|
26
|
+
}
|
|
27
|
+
/** Machine operations are mirrored in integration-contract.json; help is presentation-only. */
|
|
28
|
+
export const RUNTIME_CLI_OPERATIONS = ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'help'];
|
|
18
29
|
function read(file) { return JSON.parse(readFileSync(file, 'utf8')); }
|
|
19
30
|
async function readStdin() {
|
|
20
31
|
const chunks = [];
|
|
@@ -87,8 +98,15 @@ function invocationUsage(state, priorCount) {
|
|
|
87
98
|
? null : attempts.reduce((total, a) => total + (a.usage?.[key] ?? 0), 0);
|
|
88
99
|
return { costUsd: sum('costUsd'), inputTokens: sum('inputTokens'), outputTokens: sum('outputTokens') };
|
|
89
100
|
}
|
|
101
|
+
/** Engines this package can launch: the legacy built-in pipeline (1) and workflow definitions (2).
|
|
102
|
+
* Core 7 advertises [2] so hosts refuse unconverted legacy loops before admission. */
|
|
103
|
+
export const RUNTIME_ENGINES = [1, 2];
|
|
90
104
|
export async function runRuntimeCommand(flags, positionals, emit = value => process.stdout.write(JSON.stringify(value) + '\n')) {
|
|
91
105
|
const command = flags.help === true ? 'help' : positionals[0] ?? 'help';
|
|
106
|
+
if (isDefinitionCommand(flags, command))
|
|
107
|
+
return runDefinitionCommand(flags, positionals, emit);
|
|
108
|
+
if (!RUNTIME_CLI_OPERATIONS.some(operation => operation === command))
|
|
109
|
+
throw new Error('Unknown runtime operation: ' + command);
|
|
92
110
|
if (command === 'prompts') {
|
|
93
111
|
emit({ type: 'runtime-role-prompts', defaults: rolePromptDefaults() });
|
|
94
112
|
return 0;
|
|
@@ -105,28 +123,42 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
|
|
|
105
123
|
if (command === 'evaluate') {
|
|
106
124
|
if (flags.real !== undefined && flags.real !== true)
|
|
107
125
|
throw new Error('--real is a boolean flag');
|
|
108
|
-
const report = await runEvaluation({ output: stringFlag(flags, 'output'), real: flags.real === true,
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
const report = await runEvaluation({ output: stringFlag(flags, 'output'), real: flags.real === true,
|
|
127
|
+
...(flags.definition === undefined ? {} : { definition: readFileSync(stringFlag(flags, 'definition')) }),
|
|
128
|
+
...(flags.config === undefined ? {} : { config: validateRuntimeConfig(read(stringFlag(flags, 'config'))) }), ...(flags['max-cost-usd'] === undefined ? {} : { maxCostUsd: Number(stringFlag(flags, 'max-cost-usd')) }), ...(flags.repetitions === undefined ? {} : { repetitions: Number(stringFlag(flags, 'repetitions')) }) });
|
|
129
|
+
emit({ type: 'runtime-evaluation', mode: report.mode, observations: report.observations.length, output: stringFlag(flags, 'output'),
|
|
130
|
+
isDefinitionEvaluation: report.isDefinitionEvaluation, allCasesAccepted: report.allCasesAccepted, acceptedCases: report.acceptedCases,
|
|
131
|
+
monetaryConclusion: report.monetaryConclusion, correctionPromptTargetMet: report.correctionPromptTargetMet, noObservedQualityDrop: report.noObservedQualityDrop, noExtraInvocations: report.noExtraInvocations, stopReason: report.stopReason });
|
|
132
|
+
return report.stopReason || !report.allCasesAccepted || (!flags.real && (!report.noObservedQualityDrop || !report.noExtraInvocations || !report.correctionPromptTargetMet)) ? 1 : 0;
|
|
111
133
|
}
|
|
112
134
|
if (command === 'help') {
|
|
113
135
|
emit({ usage: [
|
|
114
136
|
'specrails-core runtime api',
|
|
115
|
-
'specrails-core runtime evaluate --output <directory> [--repetitions <1..20>] [--real --config <json> --max-cost-usd <limit>]',
|
|
137
|
+
'specrails-core runtime evaluate --output <directory> [--definition <json>] [--repetitions <1..20>] [--real --config <json> --max-cost-usd <limit>]',
|
|
116
138
|
'specrails-core runtime capabilities --config <json>',
|
|
117
139
|
'specrails-core runtime evidence --context <json> [--id <opaqueId>] [--section summary|stdout|stderr|source] [--source-id <opaqueId>] [--cursor <cursor>] [--limit <1..100>]',
|
|
118
140
|
'specrails-core runtime prompts',
|
|
119
141
|
'specrails-core runtime validate --config <json>',
|
|
120
142
|
'specrails-core runtime validate --stdin',
|
|
121
143
|
'specrails-core runtime run --context <json> --config <json> --change <kebab-case>',
|
|
144
|
+
'specrails-core runtime run --context <json> --config <json> --definition <json> [--change <kebab-case>]',
|
|
145
|
+
'specrails-core runtime workflows list',
|
|
146
|
+
'specrails-core runtime workflows validate --stdin [--config <json> | --structural]',
|
|
122
147
|
'specrails-core runtime status --context <json> [--compact]',
|
|
123
148
|
'specrails-core runtime resume --context <json> [--approve archive] [--answer <text>] [--recover developer] [--invalidate verify]',
|
|
124
149
|
'specrails-core runtime recovery --context <json> --stdin',
|
|
150
|
+
'specrails-core runtime fork --context <json> --from <nodePath> --run-id <newRunId> [--scope-id <scope>] [--visit <number>] [--state <json>]',
|
|
151
|
+
'specrails-core runtime signal --context <json> --stdin [--request-id <id>]',
|
|
152
|
+
'specrails-core runtime cancel --context <json> [--request-id <id>]',
|
|
125
153
|
] });
|
|
126
154
|
return 0;
|
|
127
155
|
}
|
|
128
156
|
if (command === 'api') {
|
|
129
|
-
|
|
157
|
+
// Engine v2 fields come from the same validation registry that admits definitions; test-only pieces are never registered there.
|
|
158
|
+
emit({ type: 'runtime-api', apiVersion: RUNTIME_API_VERSION, coreVersion: CORE_PACKAGE_VERSION, runtimeIdentity: coreRuntimeIdentity(), workflowVersions: [CORE_WORKFLOW_VERSION],
|
|
159
|
+
engineVersion: 2, engines: RUNTIME_ENGINES, nodeKindsVersion: NODE_KINDS_VERSION, nodeKinds: validationPieceRegistry().kinds(),
|
|
160
|
+
capabilities: { openRoles: 1, scopedRecovery: 1, efficientRoleExecution: 1, reproducibleVerification: 1, implementationEfficiencyMetrics: 1, compactAgentLoop: 1, configurableGuardrails: 1, compactOutputBudget: 1, roleThinkingControl: 1, repositoryScope: 1,
|
|
161
|
+
engineV2: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1 }, guardrails: GUARDRAIL_CATALOG });
|
|
130
162
|
return 0;
|
|
131
163
|
}
|
|
132
164
|
if (command === 'validate') {
|
|
@@ -163,8 +195,6 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
|
|
|
163
195
|
process.off('SIGTERM', abort);
|
|
164
196
|
}
|
|
165
197
|
}
|
|
166
|
-
if (!['run', 'status', 'resume'].includes(command))
|
|
167
|
-
throw new Error('Unknown runtime operation: ' + command);
|
|
168
198
|
const context = validatePipelineContext(read(stringFlag(flags, 'context')));
|
|
169
199
|
const directory = path.join(pipelineStateDirectory(context), 'agent-workflow');
|
|
170
200
|
const previous = await readWorkflowState(directory, context.runId);
|
|
@@ -178,6 +208,8 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
|
|
|
178
208
|
const requestFile = path.join(pipelineStateDirectory(context), 'agent-runtime-request.json');
|
|
179
209
|
let request;
|
|
180
210
|
if (command === 'resume') {
|
|
211
|
+
if (flags.definition !== undefined)
|
|
212
|
+
throw new EngineError('invalid_arguments', 'Resume uses the frozen definition; start a new run to change it');
|
|
181
213
|
if (!previous)
|
|
182
214
|
throw new Error('No programmatic run exists for this context');
|
|
183
215
|
request = read(requestFile);
|
|
@@ -189,7 +221,9 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
|
|
|
189
221
|
else {
|
|
190
222
|
if (flags.answer !== undefined || flags.approve !== undefined)
|
|
191
223
|
throw new Error('Answers and approvals apply to runtime resume');
|
|
192
|
-
|
|
224
|
+
// The built-in workflow has no published definition hash; its resume identity is the checkpoint's workflowFingerprint.
|
|
225
|
+
request = { change: stringFlag(flags, 'change'), config: normalizeRuntimeConfig(read(stringFlag(flags, 'config'))), runtimeIdentity: coreRuntimeIdentity(),
|
|
226
|
+
workflow: { id: 'specrails-implementation', version: CORE_WORKFLOW_VERSION, source: 'builtin', definitionHash: null, engine: 1 } };
|
|
193
227
|
await preflightCoreWorkflow({ context, change: request.change, config: request.config });
|
|
194
228
|
const serialized = JSON.stringify(request, null, 2) + '\n';
|
|
195
229
|
mkdirSync(path.dirname(requestFile), { recursive: true, mode: 0o700 });
|
|
@@ -237,7 +271,7 @@ if (process.argv[1] && existsSync(process.argv[1]) && import.meta.url === pathTo
|
|
|
237
271
|
process.exitCode = await runRuntimeCommand(flags, [subcommand ?? 'help', ...positionals]);
|
|
238
272
|
}
|
|
239
273
|
catch (error) {
|
|
240
|
-
process.stdout.write(JSON.stringify(
|
|
274
|
+
process.stdout.write(JSON.stringify(runtimeFailure(error)) + '\n');
|
|
241
275
|
process.exitCode = 1;
|
|
242
276
|
}
|
|
243
277
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
1
2
|
import { AgentExecutionError } from '../executor-types.js';
|
|
2
3
|
import { sumCacheUsage } from '../efficiency-types.js';
|
|
3
4
|
/**
|
|
@@ -49,7 +50,7 @@ export function patientFetchInit() {
|
|
|
49
50
|
}
|
|
50
51
|
const RETRY_DELAY_MS = 3000;
|
|
51
52
|
function sleep(ms, signal) {
|
|
52
|
-
return
|
|
53
|
+
return delay(ms, undefined, { signal });
|
|
53
54
|
}
|
|
54
55
|
export function record(value) {
|
|
55
56
|
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RoleDescriptor, type RuntimeConfig } from './executor-types.js';
|
|
2
2
|
export declare function validateRuntimeConfig(input: unknown, options?: {
|
|
3
3
|
registeredProviderIds?: string[];
|
|
4
4
|
}): RuntimeConfig;
|
|
@@ -9,3 +9,6 @@ export declare function loadRuntimeConfig(file: string, options?: {
|
|
|
9
9
|
export declare function normalizeRuntimeConfig(input: unknown, options?: {
|
|
10
10
|
registeredProviderIds?: string[];
|
|
11
11
|
}): RuntimeConfig;
|
|
12
|
+
/** Built-ins stay ordered and custom roles are declared data, never inferred from a prompt. */
|
|
13
|
+
export declare function roleIds(config: RuntimeConfig): string[];
|
|
14
|
+
export declare function resolveRoleDescriptor(config: RuntimeConfig, id: string): RoleDescriptor;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
1
2
|
import { validateGuardrailSettings } from './guardrails.js';
|
|
2
3
|
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { BUILTIN_ROLES, BUILTIN_ROLE_POLICY, ROLE_ID, isBuiltinRole } from './executor-types.js';
|
|
3
5
|
import { DEFAULT_REVIEW_POLICY, REVIEW_ASPECTS } from './graph/review-policy.js';
|
|
4
|
-
const ROLES = [
|
|
6
|
+
const ROLES = [...BUILTIN_ROLES];
|
|
5
7
|
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
6
8
|
function fail(field, why) { throw new Error(`Invalid runtime config ${field}: ${why}`); }
|
|
7
9
|
function object(value, field) {
|
|
@@ -60,7 +62,7 @@ function score(value, field, floor) {
|
|
|
60
62
|
}
|
|
61
63
|
export function validateRuntimeConfig(input, options = {}) {
|
|
62
64
|
const config = object(input, '$');
|
|
63
|
-
keys(config, ['schemaVersion', 'enabled', 'providers', 'agents', 'fixer', 'limits', 'verification', 'approvalBeforeArchive', 'review', 'architect', 'rolePrompts', 'efficiency', 'guardrails'], '$');
|
|
65
|
+
keys(config, ['schemaVersion', 'enabled', 'providers', 'agents', 'roles', 'fixer', 'limits', 'verification', 'approvalBeforeArchive', 'review', 'architect', 'rolePrompts', 'efficiency', 'guardrails'], '$');
|
|
64
66
|
if (config.schemaVersion !== 1)
|
|
65
67
|
fail('schemaVersion', 'expected 1');
|
|
66
68
|
if (config.enabled !== undefined && typeof config.enabled !== 'boolean')
|
|
@@ -141,6 +143,30 @@ export function validateRuntimeConfig(input, options = {}) {
|
|
|
141
143
|
return structuredClone(agent);
|
|
142
144
|
};
|
|
143
145
|
const agents = Object.fromEntries(ROLES.map(role => [role, validateAgent(rawAgents[role], `agents.${role}`)]));
|
|
146
|
+
let roles;
|
|
147
|
+
if (config.roles !== undefined) {
|
|
148
|
+
const raw = object(config.roles, 'roles');
|
|
149
|
+
roles = {};
|
|
150
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
151
|
+
if (!ROLE_ID.test(id) || id === 'fixer')
|
|
152
|
+
fail(`roles.${id}`, 'expected a role id; fixer is reserved');
|
|
153
|
+
const descriptor = object(value, `roles.${id}`);
|
|
154
|
+
const { access, artifacts, prompt, openspecSkill, ...assignment } = descriptor;
|
|
155
|
+
const selected = validateAgent(assignment, `roles.${id}`);
|
|
156
|
+
choices(access, ['read', 'write'], `roles.${id}.access`);
|
|
157
|
+
choices(artifacts, ['none', 'tasks-checkboxes', 'all'], `roles.${id}.artifacts`);
|
|
158
|
+
if (prompt !== undefined && string(prompt, `roles.${id}.prompt`).length > 20000)
|
|
159
|
+
fail(`roles.${id}.prompt`, 'maximum 20000 characters');
|
|
160
|
+
if (openspecSkill !== undefined)
|
|
161
|
+
choices(openspecSkill, ['openspec-ff-change', 'openspec-apply-change', 'openspec-verify-change'], `roles.${id}.openspecSkill`);
|
|
162
|
+
if (isBuiltinRole(id)) {
|
|
163
|
+
const policy = BUILTIN_ROLE_POLICY[id];
|
|
164
|
+
if (access !== policy.access || artifacts !== policy.artifacts || openspecSkill !== policy.openspecSkill || prompt !== undefined || !isDeepStrictEqual(selected, agents[id]))
|
|
165
|
+
fail(`roles.${id}`, 'built-in descriptors must match their implicit assignment and policy');
|
|
166
|
+
}
|
|
167
|
+
roles[id] = { ...selected, access, artifacts, ...(prompt === undefined ? {} : { prompt }), ...(openspecSkill === undefined ? {} : { openspecSkill }) };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
144
170
|
// The optional fixer: the engine correction rounds run on (verify failure or
|
|
145
171
|
// review rejection); absent ⇒ the developer corrects its own work.
|
|
146
172
|
const fixer = config.fixer === undefined ? undefined : validateAgent(config.fixer, 'fixer');
|
|
@@ -259,7 +285,7 @@ export function validateRuntimeConfig(input, options = {}) {
|
|
|
259
285
|
let rolePrompts;
|
|
260
286
|
if (config.rolePrompts !== undefined) {
|
|
261
287
|
const raw = object(config.rolePrompts, 'rolePrompts');
|
|
262
|
-
keys(raw, [...ROLES, 'fixer'], 'rolePrompts');
|
|
288
|
+
keys(raw, [...ROLES, 'fixer', ...Object.keys(roles ?? {})], 'rolePrompts');
|
|
263
289
|
rolePrompts = {};
|
|
264
290
|
for (const [role, value] of Object.entries(raw)) {
|
|
265
291
|
const text = string(value, `rolePrompts.${role}`);
|
|
@@ -269,6 +295,7 @@ export function validateRuntimeConfig(input, options = {}) {
|
|
|
269
295
|
}
|
|
270
296
|
}
|
|
271
297
|
return { schemaVersion: 1, enabled: true, providers, agents, verification,
|
|
298
|
+
...(roles === undefined ? {} : { roles }),
|
|
272
299
|
...(config.efficiency === undefined ? {} : { efficiency: structuredClone(config.efficiency) }),
|
|
273
300
|
...(rolePrompts === undefined ? {} : { rolePrompts }),
|
|
274
301
|
...(config.limits === undefined ? {} : { limits: { ...config.limits } }),
|
|
@@ -284,9 +311,20 @@ export function loadRuntimeConfig(file, options) {
|
|
|
284
311
|
/** New admissions normalize once; validation of a saved document is lossless. */
|
|
285
312
|
export function normalizeRuntimeConfig(input, options) {
|
|
286
313
|
const config = JSON.parse(JSON.stringify(validateRuntimeConfig(input, options)));
|
|
314
|
+
config.roles = Object.fromEntries(roleIds(config).map(id => { const { id: _id, ...descriptor } = resolveRoleDescriptor(config, id); return [id, descriptor]; }));
|
|
287
315
|
config.efficiency = {
|
|
288
316
|
schemaVersion: 1, contextMode: 'incremental', reviewMode: 'incremental', planning: 'proportional', acceptDeveloperChecks: true,
|
|
289
317
|
...config.efficiency, verification: { maxConcurrency: 1, ...config.efficiency?.verification },
|
|
290
318
|
};
|
|
291
319
|
return config;
|
|
292
320
|
}
|
|
321
|
+
/** Built-ins stay ordered and custom roles are declared data, never inferred from a prompt. */
|
|
322
|
+
export function roleIds(config) { return [...new Set([...BUILTIN_ROLES, ...Object.keys(config.roles ?? {})])]; }
|
|
323
|
+
export function resolveRoleDescriptor(config, id) {
|
|
324
|
+
if (isBuiltinRole(id))
|
|
325
|
+
return { ...config.agents[id], ...BUILTIN_ROLE_POLICY[id], id };
|
|
326
|
+
const role = Object.hasOwn(config.roles ?? {}, id) ? config.roles[id] : undefined;
|
|
327
|
+
if (!role)
|
|
328
|
+
throw new Error(`Unknown runtime role '${id}'`);
|
|
329
|
+
return { ...role, ...(config.rolePrompts?.[id] === undefined ? {} : { prompt: config.rolePrompts[id] }), id };
|
|
330
|
+
}
|
|
@@ -2,8 +2,9 @@ import { type PipelineContext } from '../pipeline/pipeline-state.js';
|
|
|
2
2
|
import { type ExecutorRegistry } from './executors.js';
|
|
3
3
|
import type { AgentEvent, RuntimeConfig, AgentEventRole } from './executor-types.js';
|
|
4
4
|
import { parseAgentObject } from './graph/artifacts.js';
|
|
5
|
+
import { type CoreNodeId, type CoreStateType } from './graph/state.js';
|
|
5
6
|
import { type RuntimeIdentity } from './runtime-identity.js';
|
|
6
|
-
import type { WorkflowEvent, WorkflowSpan, WorkflowState } from './workflow-types.js';
|
|
7
|
+
import type { WorkflowDefinition, WorkflowEvent, WorkflowNode, WorkflowSpan, WorkflowState } from './workflow-types.js';
|
|
7
8
|
export declare const RUNTIME_API_VERSION = 1;
|
|
8
9
|
export declare const CORE_WORKFLOW_VERSION = "7";
|
|
9
10
|
export declare const CORE_PACKAGE_VERSION: string;
|
|
@@ -32,9 +33,14 @@ export declare function preflightCoreWorkflow(options: Pick<CoreWorkflowOptions,
|
|
|
32
33
|
config: RuntimeConfig;
|
|
33
34
|
registry: ExecutorRegistry;
|
|
34
35
|
}>;
|
|
36
|
+
/** The legacy graph's resume identity; extracting it must never change its fingerprint. */
|
|
37
|
+
export declare function implementationWorkflowDefinition(nodes: Record<CoreNodeId, WorkflowNode<CoreStateType>>, { attempts, compactDeveloper }: {
|
|
38
|
+
attempts: number;
|
|
39
|
+
compactDeveloper: boolean;
|
|
40
|
+
}): WorkflowDefinition<CoreStateType>;
|
|
35
41
|
/**
|
|
36
|
-
* Runs the Core implementation graph: architect → developer → verify →
|
|
37
|
-
* with bounded corrections
|
|
42
|
+
* Runs the Core implementation graph: architect → developer → verify → reviewer → archive,
|
|
43
|
+
* with bounded corrections through fixer → verify → reviewer, an autonomous investigation pass
|
|
38
44
|
* before a low-confidence design asks the requester, real verification receipts, acceptance
|
|
39
45
|
* evidence bound to the exact candidate, and an optional approval before archive.
|
|
40
46
|
*/
|
|
@@ -3,7 +3,7 @@ import { prepareOpenSpec, roleOpenSpecContext } from './openspec.js';
|
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fingerprintCandidate, initializePipeline, inspectPipeline, pipelineStateDirectory, validatePipelineContext, validateVerificationRequest, } from '../pipeline/pipeline-state.js';
|
|
6
|
-
import { normalizeRuntimeConfig } from './config.js';
|
|
6
|
+
import { normalizeRuntimeConfig, resolveRoleDescriptor, roleIds } from './config.js';
|
|
7
7
|
import { createExecutorRegistry } from './executors.js';
|
|
8
8
|
import { archive, child, journal, parseAgentObject, SLUG } from './graph/artifacts.js';
|
|
9
9
|
import { coreNodes } from './graph/nodes.js';
|
|
@@ -18,7 +18,7 @@ export const RUNTIME_API_VERSION = 1;
|
|
|
18
18
|
export const CORE_WORKFLOW_VERSION = '7';
|
|
19
19
|
export const CORE_PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
|
|
20
20
|
export function coreRuntimeIdentity() {
|
|
21
|
-
return { packageVersion: CORE_PACKAGE_VERSION, workflowVersion: CORE_WORKFLOW_VERSION, instructionsVersion: String(ROLE_INSTRUCTIONS_VERSION), packageIntegrity: runtimePackageIntegrity(), apiVersion:
|
|
21
|
+
return { packageVersion: CORE_PACKAGE_VERSION, workflowVersion: CORE_WORKFLOW_VERSION, instructionsVersion: String(ROLE_INSTRUCTIONS_VERSION), packageIntegrity: runtimePackageIntegrity(), apiVersion: RUNTIME_API_VERSION };
|
|
22
22
|
}
|
|
23
23
|
export { parseAgentObject };
|
|
24
24
|
/** Read-only admission: validate scope, limits and actual effort support before freezing a request. */
|
|
@@ -36,7 +36,7 @@ export async function preflightCoreWorkflow(options) {
|
|
|
36
36
|
if (config.verification.length)
|
|
37
37
|
validateVerificationRequest(context, { kind: 'scoped', commands: config.verification });
|
|
38
38
|
const registry = options.registry ?? createExecutorRegistry(config);
|
|
39
|
-
for (const selected of
|
|
39
|
+
for (const selected of roleIds(config).map(id => resolveRoleDescriptor(config, id))) {
|
|
40
40
|
registry.validateLimits(selected.provider, config.limits ?? {});
|
|
41
41
|
for (const tier of [selected, ...(selected.escalation ? [selected.escalation] : [])]) {
|
|
42
42
|
if (tier.effort !== undefined)
|
|
@@ -45,9 +45,19 @@ export async function preflightCoreWorkflow(options) {
|
|
|
45
45
|
}
|
|
46
46
|
return { context, config, registry };
|
|
47
47
|
}
|
|
48
|
+
/** The legacy graph's resume identity; extracting it must never change its fingerprint. */
|
|
49
|
+
export function implementationWorkflowDefinition(nodes, { attempts, compactDeveloper }) {
|
|
50
|
+
return {
|
|
51
|
+
id: 'specrails-implementation', version: CORE_WORKFLOW_VERSION, schema: CoreState, entry: 'architect',
|
|
52
|
+
// The compact developer finishes task groups over developer→verify passes
|
|
53
|
+
// that do not count as corrections (bounded: +2 passes each).
|
|
54
|
+
maxTransitions: attempts * 3 + 3 + (compactDeveloper ? attempts * 2 * 2 : 0),
|
|
55
|
+
nodes: Object.fromEntries(CORE_NODE_ORDER.map(id => [id, nodes[id]])),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
48
58
|
/**
|
|
49
|
-
* Runs the Core implementation graph: architect → developer → verify →
|
|
50
|
-
* with bounded corrections
|
|
59
|
+
* Runs the Core implementation graph: architect → developer → verify → reviewer → archive,
|
|
60
|
+
* with bounded corrections through fixer → verify → reviewer, an autonomous investigation pass
|
|
51
61
|
* before a low-confidence design asks the requester, real verification receipts, acceptance
|
|
52
62
|
* evidence bound to the exact candidate, and an optional approval before archive.
|
|
53
63
|
*/
|
|
@@ -68,12 +78,12 @@ export async function runCoreWorkflow(options) {
|
|
|
68
78
|
}
|
|
69
79
|
}
|
|
70
80
|
const prepared = prepareOpenSpec(context.artifactRoot, options.change, directory);
|
|
71
|
-
const openspec = Object.fromEntries(
|
|
72
|
-
const provider = config.providers.find(item => item.id === config
|
|
73
|
-
return [role, roleOpenSpecContext(prepared, context.artifactRoot, options.change, directory, role, provider?.kind === 'cli' ? provider.cli : 'claude')];
|
|
81
|
+
const openspec = Object.fromEntries(roleIds(config).filter(role => resolveRoleDescriptor(config, role).openspecSkill).map(role => {
|
|
82
|
+
const provider = config.providers.find(item => item.id === resolveRoleDescriptor(config, role).provider);
|
|
83
|
+
return [role, roleOpenSpecContext(prepared, context.artifactRoot, options.change, directory, resolveRoleDescriptor(config, role), provider?.kind === 'cli' ? provider.cli : 'claude')];
|
|
74
84
|
}));
|
|
75
|
-
for (const role of
|
|
76
|
-
await registry.get(config
|
|
85
|
+
for (const role of Object.keys(openspec))
|
|
86
|
+
await registry.get(resolveRoleDescriptor(config, role).provider).validateOpenSpec?.(openspec[role]);
|
|
77
87
|
const attempts = config.limits?.maxAttempts ?? 3;
|
|
78
88
|
const developerProvider = config.providers.find(item => item.id === config.agents.developer.provider);
|
|
79
89
|
const compactDeveloper = developerProvider?.kind === 'openai-compatible' && (developerProvider.agentLoop ?? 'compact') === 'compact';
|
|
@@ -127,13 +137,6 @@ export async function runCoreWorkflow(options) {
|
|
|
127
137
|
return inspection.phases.archive.status === 'done';
|
|
128
138
|
return true;
|
|
129
139
|
},
|
|
130
|
-
workflow: {
|
|
131
|
-
id: 'specrails-implementation', version: CORE_WORKFLOW_VERSION, schema: CoreState, entry: 'architect',
|
|
132
|
-
// The compact (small-model) developer finishes its task groups over
|
|
133
|
-
// several developer→verify passes that do not count as corrections, so
|
|
134
|
-
// the transition budget must cover them too (bounded: +2 passes each).
|
|
135
|
-
maxTransitions: attempts * 3 + 3 + (compactDeveloper ? attempts * 2 * 2 : 0),
|
|
136
|
-
nodes: Object.fromEntries(CORE_NODE_ORDER.map(id => [id, nodes[id]])),
|
|
137
|
-
},
|
|
140
|
+
workflow: implementationWorkflowDefinition(nodes, { attempts, compactDeveloper }),
|
|
138
141
|
});
|
|
139
142
|
}
|
|
@@ -13,6 +13,9 @@ export declare function efficiencySummary(state: WorkflowState, context: Pipelin
|
|
|
13
13
|
notRunEvidenceIds?: string[];
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
|
+
}, durableEvidence?: {
|
|
17
|
+
items: Array<Record<string, unknown>>;
|
|
18
|
+
available: boolean;
|
|
16
19
|
}): {
|
|
17
20
|
schemaVersion: 1;
|
|
18
21
|
currentEvidenceIds: string[];
|
|
@@ -24,7 +27,7 @@ export declare function efficiencySummary(state: WorkflowState, context: Pipelin
|
|
|
24
27
|
archive: import("../pipeline/pipeline-state.js").PhaseStatus;
|
|
25
28
|
delivery: "pending" | "complete" | "pending-host";
|
|
26
29
|
roles: {
|
|
27
|
-
role:
|
|
30
|
+
role: string;
|
|
28
31
|
provider: string | null;
|
|
29
32
|
model: string | null;
|
|
30
33
|
effort: string | null;
|