specrails-core 6.0.0 → 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/change-scope.d.ts +63 -0
- package/dist/agent-runtime/change-scope.js +297 -0
- 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/compact/environment.d.ts +12 -0
- package/dist/agent-runtime/compact/environment.js +31 -1
- package/dist/agent-runtime/compact/test-reachability.js +6 -2
- 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 +10 -4
- package/dist/agent-runtime/core-host.js +22 -19
- 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 +86 -10
- package/dist/agent-runtime/graph/convergence.d.ts +26 -0
- package/dist/agent-runtime/graph/convergence.js +62 -0
- package/dist/agent-runtime/graph/nodes.d.ts +2 -0
- package/dist/agent-runtime/graph/nodes.js +171 -24
- package/dist/agent-runtime/graph/roles.d.ts +6 -0
- package/dist/agent-runtime/graph/roles.js +23 -17
- package/dist/agent-runtime/graph/state.d.ts +5 -0
- package/dist/agent-runtime/graph/state.js +2 -0
- 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 +11 -5
- package/dist/agent-runtime/prompts.js +71 -20
- package/dist/agent-runtime/recovery.d.ts +1 -1
- package/dist/agent-runtime/repository-context.js +20 -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 +18 -3
- 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 +49 -4
- package/dist/pipeline/pipeline-state.js +150 -23
- package/integration-contract.json +84 -12
- 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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { AgentExecutionError, unknownUsage } from '../../executor-types.js';
|
|
3
|
+
import { ROLE_INSTRUCTIONS_VERSION } from '../../prompts.js';
|
|
4
|
+
import { contentDigest } from '../canonical-json.js';
|
|
5
|
+
import { EngineError } from '../contracts.js';
|
|
6
|
+
import { answerEntry, boundedText, captures, captureSchema, historyEntry, json, paramsSchema, invocationTimers, stringSchema, text } from './shared.js';
|
|
7
|
+
const sessionFailures = new Set(['session_not_found', 'session_expired', 'session_unsupported']);
|
|
8
|
+
const engineSchema = paramsSchema({ provider: { type: 'string', minLength: 1, maxLength: 128 }, model: { type: 'string', minLength: 1, maxLength: 256 }, effort: { type: 'string', minLength: 1, maxLength: 64 }, thinking: { enum: ['on', 'off'] } }, ['provider']);
|
|
9
|
+
const promptSchema = {
|
|
10
|
+
...paramsSchema({ engine: engineSchema, text: { ...stringSchema, minLength: 1 }, nativeCommand: paramsSchema({ id: { type: 'string', pattern: '^[a-z][a-z0-9:_-]{0,63}$' }, args: stringSchema }, ['id']),
|
|
11
|
+
access: { enum: ['read', 'write'] }, sentinel: { enum: ['verification', 'blocked', 'none'] }, captureVars: captureSchema,
|
|
12
|
+
sessionContinuity: { enum: ['run', 'none'] }, ...invocationTimers, appendHistory: { type: 'boolean' }, appendSteering: { type: 'boolean' },
|
|
13
|
+
}, ['engine', 'access']),
|
|
14
|
+
oneOf: [{ properties: { text: true }, required: ['text'] }, { properties: { nativeCommand: true }, required: ['nativeCommand'] }],
|
|
15
|
+
};
|
|
16
|
+
export function verificationSentinel(output) {
|
|
17
|
+
const values = [...output.matchAll(/\bVERIFICATION:\s*(PASS|FAIL)\b/gi)];
|
|
18
|
+
return values.length ? values[values.length - 1][1].toLowerCase() : undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Free turns share accounting and executor protections, without adding role instructions. */
|
|
21
|
+
export function promptPiece(bindings) {
|
|
22
|
+
return {
|
|
23
|
+
descriptor: { kind: 'prompt', paramsSchema: promptSchema, outcomes: ['next', 'pass', 'fail', 'blocked', 'failed'], effect: 'derived', requiresAI: true },
|
|
24
|
+
getEffect: params => params.access === 'write' ? 'write' : 'read',
|
|
25
|
+
getOutcomes: params => params.sentinel === 'verification' ? ['pass', 'fail', 'failed'] : params.sentinel === 'blocked' ? ['next', 'blocked', 'failed'] : ['next', 'failed'],
|
|
26
|
+
async execute(params, context) {
|
|
27
|
+
const deps = bindings();
|
|
28
|
+
const engine = params.engine;
|
|
29
|
+
const step = deps.stepContext(context), memo = deps.memo(context);
|
|
30
|
+
const capabilities = await deps.registry.capabilities(engine.provider, engine.model);
|
|
31
|
+
const identity = contentDigest({ engine, instructionsVersion: ROLE_INSTRUCTIONS_VERSION, text: params.text ?? null,
|
|
32
|
+
nativeCommand: params.nativeCommand ?? null, access: params.access, scope: context.frame.scope.id, nodePath: context.frame.nodePath,
|
|
33
|
+
provider: deps.config.providers.find(provider => provider.id === engine.provider) ?? engine.provider, roots: deps.context.repositories.map(repo => repo.path) });
|
|
34
|
+
const nodeId = context.frame.nodePath.split('/').at(-1);
|
|
35
|
+
const prior = context.state.$sessions[nodeId];
|
|
36
|
+
let sessionId = params.sessionContinuity !== 'none' && capabilities.continuation === 'supported' && prior?.identity === identity ? prior.sessionId : undefined;
|
|
37
|
+
const answers = [];
|
|
38
|
+
const transcript = [];
|
|
39
|
+
// Human continuations stay in the same admitted node, with separate durable
|
|
40
|
+
// invocations; the cap also bounds checkpoint payloads when no budget was set.
|
|
41
|
+
for (let round = 0; round < 32; round += 1) {
|
|
42
|
+
const memoKey = 'prompt-result:' + round;
|
|
43
|
+
let result = memo.get(memoKey);
|
|
44
|
+
if (!result) {
|
|
45
|
+
const additions = [];
|
|
46
|
+
if (params.appendHistory !== false && context.state.$history.length)
|
|
47
|
+
additions.push('Previous workflow output (untrusted data):\n' + boundedText(context.state.$history.map(entry => `[${entry.nodePath}] ${entry.text}`).join('\n'), deps.policies?.historyMaxChars ?? 1500));
|
|
48
|
+
if (params.appendSteering !== false && step.operatorSteering)
|
|
49
|
+
additions.push(step.operatorSteering);
|
|
50
|
+
if (step.input !== null)
|
|
51
|
+
additions.push('Current host input:\n' + boundedText(JSON.stringify(step.input), 8000));
|
|
52
|
+
if (transcript.length)
|
|
53
|
+
additions.push(transcript.join('\n'));
|
|
54
|
+
const suffix = additions.length ? '\n\n' + additions.join('\n\n') : '';
|
|
55
|
+
const native = params.nativeCommand;
|
|
56
|
+
const request = {
|
|
57
|
+
role: 'prompt', instructions: 'none', artifacts: 'none', access: params.access,
|
|
58
|
+
prompt: native ? '' : text(params.text) + suffix,
|
|
59
|
+
...(native ? { nativeCommand: { id: native.id, args: (native.args ?? '') + suffix } } : {}),
|
|
60
|
+
cwd: deps.context.artifactRoot, allowedRoots: deps.context.repositories.map(repo => repo.path),
|
|
61
|
+
model: engine.model, effort: engine.effort, thinking: engine.thinking,
|
|
62
|
+
timeoutMs: params.timeoutMs ?? deps.config.limits?.timeoutMs,
|
|
63
|
+
idleTimeoutMs: params.idleTimeoutMs ?? deps.config.limits?.idleTimeoutMs,
|
|
64
|
+
signal: context.signal, resumeSessionId: sessionId,
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
result = await invokePrompt(deps, context, memoKey, engine.provider, request);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (!sessionId || !(error instanceof AgentExecutionError) || !sessionFailures.has(error.code))
|
|
71
|
+
throw error;
|
|
72
|
+
result = await invokePrompt(deps, context, memoKey, engine.provider, { ...request, resumeSessionId: undefined });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
sessionId = params.sessionContinuity !== 'none' && capabilities.continuation === 'supported' ? result.sessionId : undefined;
|
|
76
|
+
if (params.sentinel === 'blocked' || params.sentinel === 'verification') {
|
|
77
|
+
const question = result.blockedQuestion;
|
|
78
|
+
if (question) {
|
|
79
|
+
// The atomic response memo was committed before this interrupt. A
|
|
80
|
+
// resume reads it and asks for the answer before making another call.
|
|
81
|
+
const response = context.interrupt({ kind: 'question', prompt: boundedText(question, 4000), nodePath: context.frame.nodePath, scopeId: context.frame.scope.id, attemptId: context.frame.attemptId });
|
|
82
|
+
answers.push(answerEntry(context, response, round));
|
|
83
|
+
transcript.push('Previous blocked response:\n' + boundedText(result.text, 4000), 'Human answer:\n' + boundedText(typeof response === 'string' ? response : JSON.stringify(response), 4000));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const vars = captures(params, result.text);
|
|
88
|
+
const sentinel = params.sentinel === 'verification' ? result.verification : undefined;
|
|
89
|
+
const outcome = params.sentinel === 'verification' ? sentinel ?? 'fail' : 'next';
|
|
90
|
+
return { outcome, output: { text: boundedText(result.text), ...(result.sessionId ? { sessionId: result.sessionId } : {}),
|
|
91
|
+
...(params.sentinel === 'verification' ? { sentinel: sentinel ?? null, ...(sentinel ? {} : { reasons: ['missing_sentinel'] }) } : {}), vars },
|
|
92
|
+
vars, history: [historyEntry(context, result.text)], ...(sessionId ? { session: { sessionId, identity } } : {}), ...(answers.length ? { answers } : {}) };
|
|
93
|
+
}
|
|
94
|
+
throw new EngineError('recursion_limit', 'A prompt exceeded 32 human continuation rounds in one node visit');
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function invokePrompt(deps, context, key, provider, request) {
|
|
99
|
+
const step = deps.stepContext(context), budget = step.remainingBudget();
|
|
100
|
+
if (context.signal.aborted || budget.maxTokens === 0 || budget.maxCostUsd === 0)
|
|
101
|
+
throw new AgentExecutionError('No budget remains for another prompt', context.signal.aborted ? 'aborted' : 'budget_exhausted', { inputTokens: 0, outputTokens: 0, costUsd: 0 });
|
|
102
|
+
const measurement = { provider, ...(request.model ? { model: request.model } : {}), contextMode: request.resumeSessionId ? 'incremental' : 'full',
|
|
103
|
+
promptBytes: Buffer.byteLength(request.nativeCommand ? JSON.stringify(request.nativeCommand) : request.prompt), contextBytes: 0, handoffBytes: 0,
|
|
104
|
+
requestedEffort: request.effort ?? null, kind: request.resumeSessionId ? 'correction' : 'initial', tier: 'base', routeReason: 'workflow-prompt-engine' };
|
|
105
|
+
const invocationIdentity = await step.reportInvocationStarted?.(measurement);
|
|
106
|
+
if (!invocationIdentity)
|
|
107
|
+
throw new EngineError('invocation_port_required', 'Engine prompts require durable invocation admission');
|
|
108
|
+
const started = performance.now();
|
|
109
|
+
let toolCalls = 0;
|
|
110
|
+
let result;
|
|
111
|
+
try {
|
|
112
|
+
result = await deps.registry.execute(provider, { ...request, maxTokens: budget.maxTokens, maxCostUsd: budget.maxCostUsd,
|
|
113
|
+
onEvent: event => { if (event.kind === 'tool-start')
|
|
114
|
+
toolCalls += 1; context.progress({ type: 'agent-event', payload: json({ role: 'prompt', event }) }); } });
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const usage = error instanceof AgentExecutionError ? error.usage : unknownUsage();
|
|
118
|
+
step.reportUsage(usage);
|
|
119
|
+
await step.reportInvocation?.({ ...measurement, ...invocationIdentity, durationMs: Math.max(0, performance.now() - started), status: 'failed', toolCalls, usage });
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
step.reportUsage(result.usage);
|
|
123
|
+
// Persist only what replay needs; raw provider output never enters checkpoint state.
|
|
124
|
+
const verification = verificationSentinel(result.text);
|
|
125
|
+
const blockedQuestion = /^\s*LOOP_BLOCKED:\s*(.+)$/m.exec(result.text)?.[1]?.trim();
|
|
126
|
+
const saved = { text: boundedText(result.text), usage: result.usage, ...(result.sessionId ? { sessionId: result.sessionId } : {}),
|
|
127
|
+
...(verification ? { verification } : {}), ...(blockedQuestion ? { blockedQuestion: boundedText(blockedQuestion, 4000) } : {}) };
|
|
128
|
+
deps.settleResult(context, key, json(saved), { ...measurement, ...invocationIdentity, durationMs: Math.max(0, performance.now() - started), status: 'succeeded', toolCalls, usage: result.usage });
|
|
129
|
+
return saved;
|
|
130
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type JsonObject, type Piece, type PieceExecutionContext, type PieceResult } from '../contracts.js';
|
|
2
|
+
import type { PieceDependencies, PieceDependencyProvider } from './ports.js';
|
|
3
|
+
export declare function roleTurnPiece(bindings: PieceDependencyProvider): Piece;
|
|
4
|
+
/** One invocation path for configured roles, including routing and exactly one repair. */
|
|
5
|
+
export declare function executeRoleTurn(deps: PieceDependencies, params: JsonObject, context: PieceExecutionContext, options?: {
|
|
6
|
+
normalizeStructuredOutput?: (output: Record<string, unknown> | undefined, text: string) => Record<string, unknown> | undefined;
|
|
7
|
+
}): Promise<PieceResult>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { contentDigest } from '../canonical-json.js';
|
|
2
|
+
import { advisoryMemory } from './project-memory.js';
|
|
3
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
4
|
+
import { resolveRoleDescriptor } from '../../config.js';
|
|
5
|
+
import { AgentExecutionError } from '../../executor-types.js';
|
|
6
|
+
import { createRoleInvoker } from '../../graph/roles.js';
|
|
7
|
+
import { roleInstructions } from '../../prompts.js';
|
|
8
|
+
import { EngineError } from '../contracts.js';
|
|
9
|
+
import { boundedText, historyEntry, idSchema, json, paramsSchema, invocationTimers, stringSchema, text } from './shared.js';
|
|
10
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, validateFormats: false, ownProperties: true });
|
|
11
|
+
export function roleTurnPiece(bindings) {
|
|
12
|
+
return {
|
|
13
|
+
descriptor: { kind: 'role-turn', paramsSchema: paramsSchema({ ...invocationTimers, roleId: idSchema, prompt: { ...stringSchema, minLength: 1 }, structuredOutput: { type: 'object' }, sessionContinuity: { enum: ['run', 'none'] } }, ['roleId', 'prompt']), outcomes: ['next', 'invalid', 'failed'], effect: 'derived', requiresAI: true, storeAccess: 'write' },
|
|
14
|
+
getOutcomes: params => params.structuredOutput ? ['next', 'invalid', 'failed'] : ['next', 'failed'],
|
|
15
|
+
getEffect: (params, roles) => {
|
|
16
|
+
const role = roles[text(params.roleId)];
|
|
17
|
+
if (!role)
|
|
18
|
+
throw new EngineError('role_not_found', 'Role is not configured: ' + text(params.roleId));
|
|
19
|
+
return role.access;
|
|
20
|
+
},
|
|
21
|
+
execute: (params, context) => executeRoleTurn(bindings(), params, context),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** One invocation path for configured roles, including routing and exactly one repair. */
|
|
25
|
+
export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
26
|
+
const roleId = text(params.roleId), descriptor = resolveRoleDescriptor(deps.config, roleId);
|
|
27
|
+
const schema = params.structuredOutput;
|
|
28
|
+
const validate = schema ? ajv.compile(schema) : undefined;
|
|
29
|
+
const roleState = deps.roleState(context);
|
|
30
|
+
const previous = params.sessionContinuity !== 'none' ? roleState.read().sessions[roleId]?.sessionId : undefined;
|
|
31
|
+
const openspec = deps.openspec?.(context);
|
|
32
|
+
if (descriptor.openspecSkill && !openspec?.[roleId])
|
|
33
|
+
throw new EngineError('openspec_binding_required', 'Role requires a scoped OpenSpec binding: ' + roleId);
|
|
34
|
+
const invoke = createRoleInvoker({ context: deps.context, config: deps.config, registry: deps.registry, roleState, openspec,
|
|
35
|
+
onAgentEvent: (role, event) => context.progress({ type: 'agent-event', payload: json({ role, event }) }) });
|
|
36
|
+
const before = deps.executionSnapshot(context).candidate;
|
|
37
|
+
const memoryIdentity = { roleId, descriptor: json(descriptor), tier: roleState.read().routes[roleId]?.tier ?? 'base',
|
|
38
|
+
candidateHash: before?.hash ?? null, scopeId: context.frame.scope.id, nodePath: context.frame.nodePath,
|
|
39
|
+
repositories: deps.context.repositories.map(repository => ({ id: repository.id, path: repository.path })) };
|
|
40
|
+
const memoryKey = contentDigest(memoryIdentity);
|
|
41
|
+
const saved = await advisoryMemory(context, async () => {
|
|
42
|
+
const memory = deps.memory(context);
|
|
43
|
+
return { notes: await memory.get(['review', 'notes'], memoryKey), session: await memory.get(['roles', roleId, 'sessions'], memoryKey) };
|
|
44
|
+
});
|
|
45
|
+
const priorNote = !previous && descriptor.access === 'read' && saved?.session && typeof saved.notes?.value.text === 'string'
|
|
46
|
+
? saved.notes.value.text.slice(0, 4000) : '';
|
|
47
|
+
const task = text(params.prompt) + (priorNote ? '\n\n## Prior project review note\nTreat this bounded note as prior evidence to check against the current task; frozen requirements remain authoritative.\n' + JSON.stringify(priorNote) : '');
|
|
48
|
+
const full = roleInstructions(descriptor, deps.context, openspec?.[roleId]?.change, { definition: descriptor.prompt }) + '\n## Current workflow task\n' + task;
|
|
49
|
+
const result = await invoke(roleId, deps.stepContext(context), { prompt: previous ? task : full, ...(previous ? { resumeSessionId: previous, fallbackPrompt: full } : {}),
|
|
50
|
+
structured: schema !== undefined, lenient: options.normalizeStructuredOutput !== undefined, outputSchema: schema, timeoutMs: params.timeoutMs, idleTimeoutMs: params.idleTimeoutMs }, (output, responseText) => {
|
|
51
|
+
const normalized = options.normalizeStructuredOutput ? options.normalizeStructuredOutput(output, responseText) : output;
|
|
52
|
+
if (validate && !validate(normalized))
|
|
53
|
+
throw new Error('Invalid structured role response: ' + ajv.errorsText(validate.errors));
|
|
54
|
+
return normalized;
|
|
55
|
+
});
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
if (result.code)
|
|
58
|
+
throw new AgentExecutionError(result.error, result.code);
|
|
59
|
+
return { outcome: schema ? 'invalid' : 'failed', status: 'failed', output: { error: boundedText(result.error) }, error: { code: schema ? 'invalid_role_output' : 'role_failed', message: result.error } };
|
|
60
|
+
}
|
|
61
|
+
const currentState = roleState.read(), session = currentState.sessions[roleId];
|
|
62
|
+
const finalIdentity = { ...memoryIdentity, tier: currentState.routes[roleId]?.tier ?? 'base' }, finalKey = contentDigest(finalIdentity);
|
|
63
|
+
await advisoryMemory(context, async () => {
|
|
64
|
+
const memory = deps.memory(context);
|
|
65
|
+
await memory.put(['roles', roleId, 'sessions'], finalKey, { ...finalIdentity, runId: context.frame.runId,
|
|
66
|
+
...(session ? { sessionId: session.sessionId, identity: session.identity } : {}), lastAttemptId: context.frame.attemptId });
|
|
67
|
+
if (descriptor.access === 'read')
|
|
68
|
+
await memory.put(['review', 'notes'], finalKey, { roleId, candidateHash: before?.hash ?? null,
|
|
69
|
+
text: boundedText(result.text, 4000), lastAttemptId: context.frame.attemptId });
|
|
70
|
+
});
|
|
71
|
+
return { outcome: 'next', output: { text: boundedText(result.text), ...(result.value ? { structured: json(result.value) } : {}) },
|
|
72
|
+
history: [historyEntry(context, result.text)], ...(params.sessionContinuity !== 'none' && session ? { session: { sessionId: session.sessionId, identity: session.identity } } : {}) };
|
|
73
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { EngineAnswer, HistoryEntry, JsonObject, JsonValue, PieceExecutionContext } from '../contracts.js';
|
|
2
|
+
export declare const stringSchema: {
|
|
3
|
+
type: string;
|
|
4
|
+
maxLength: number;
|
|
5
|
+
};
|
|
6
|
+
export declare const idSchema: {
|
|
7
|
+
type: string;
|
|
8
|
+
pattern: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const positiveInteger: {
|
|
11
|
+
type: string;
|
|
12
|
+
minimum: number;
|
|
13
|
+
maximum: number;
|
|
14
|
+
};
|
|
15
|
+
export declare const invocationTimers: {
|
|
16
|
+
timeoutMs: {
|
|
17
|
+
minimum: number;
|
|
18
|
+
type: string;
|
|
19
|
+
maximum: number;
|
|
20
|
+
};
|
|
21
|
+
idleTimeoutMs: {
|
|
22
|
+
minimum: number;
|
|
23
|
+
type: string;
|
|
24
|
+
maximum: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export declare const variableNameSchema: {
|
|
28
|
+
type: string;
|
|
29
|
+
pattern: string;
|
|
30
|
+
not: {
|
|
31
|
+
enum: string[];
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export declare const captureSchema: {
|
|
35
|
+
type: string;
|
|
36
|
+
maxItems: number;
|
|
37
|
+
items: {
|
|
38
|
+
type: string;
|
|
39
|
+
additionalProperties: false;
|
|
40
|
+
required: string[];
|
|
41
|
+
properties: {
|
|
42
|
+
name: {
|
|
43
|
+
type: string;
|
|
44
|
+
pattern: string;
|
|
45
|
+
not: {
|
|
46
|
+
enum: string[];
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
pattern: {
|
|
50
|
+
type: string;
|
|
51
|
+
maxLength: number;
|
|
52
|
+
};
|
|
53
|
+
group: {
|
|
54
|
+
type: string;
|
|
55
|
+
minimum: number;
|
|
56
|
+
maximum: number;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
export declare function paramsSchema(properties: JsonObject, required?: string[]): JsonObject;
|
|
62
|
+
export declare function json(value: unknown): JsonValue;
|
|
63
|
+
export declare function text(value: JsonValue | undefined): string;
|
|
64
|
+
export declare function boundedText(value: string, limit?: number): string;
|
|
65
|
+
export declare function historyEntry(context: PieceExecutionContext, text: string, ordinal?: number): HistoryEntry;
|
|
66
|
+
export declare function answerEntry(context: PieceExecutionContext, value: JsonValue, ordinal?: number): EngineAnswer;
|
|
67
|
+
export declare function captures(params: JsonObject, output: string): Record<string, JsonValue>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { capturePattern } from '../regex.js';
|
|
2
|
+
export const stringSchema = { type: 'string', maxLength: 32_000 };
|
|
3
|
+
export const idSchema = { type: 'string', pattern: '^[a-z][a-z0-9-]{0,63}$' };
|
|
4
|
+
export const positiveInteger = { type: 'integer', minimum: 1, maximum: 2_147_483_647 };
|
|
5
|
+
export const invocationTimers = { timeoutMs: { ...positiveInteger, minimum: 0 }, idleTimeoutMs: { ...positiveInteger, minimum: 0 } };
|
|
6
|
+
export const variableNameSchema = {
|
|
7
|
+
type: 'string', pattern: '^[A-Za-z][A-Za-z0-9_-]{0,63}$', not: { enum: ['__proto__', 'prototype', 'constructor'] },
|
|
8
|
+
};
|
|
9
|
+
export const captureSchema = { type: 'array', maxItems: 64, items: {
|
|
10
|
+
type: 'object', additionalProperties: false, required: ['name', 'pattern'], properties: {
|
|
11
|
+
name: variableNameSchema,
|
|
12
|
+
pattern: { type: 'string', maxLength: 200 }, group: { type: 'integer', minimum: 0, maximum: 200 },
|
|
13
|
+
},
|
|
14
|
+
} };
|
|
15
|
+
export function paramsSchema(properties, required = []) {
|
|
16
|
+
return { type: 'object', additionalProperties: false, properties, required };
|
|
17
|
+
}
|
|
18
|
+
export function json(value) { return JSON.parse(JSON.stringify(value)); }
|
|
19
|
+
export function text(value) { return typeof value === 'string' ? value : ''; }
|
|
20
|
+
export function boundedText(value, limit = 32_000) {
|
|
21
|
+
return value.length <= limit ? value : value.slice(0, Math.max(0, limit - 12)).replace(/[\uD800-\uDBFF]$/, '') + '\n[truncated]';
|
|
22
|
+
}
|
|
23
|
+
export function historyEntry(context, text, ordinal = 0) {
|
|
24
|
+
return { id: context.frame.attemptId + ':history:' + ordinal, transition: context.frame.transition,
|
|
25
|
+
attempt: context.frame.attempt, ordinal, nodePath: context.frame.nodePath, text: boundedText(text), ...(text.length > 32_000 ? { truncated: true } : {}) };
|
|
26
|
+
}
|
|
27
|
+
export function answerEntry(context, value, ordinal = 0) {
|
|
28
|
+
return { id: context.frame.attemptId + ':answer:' + ordinal, transition: context.frame.transition,
|
|
29
|
+
attempt: context.frame.attempt, ordinal, nodePath: context.frame.nodePath, value };
|
|
30
|
+
}
|
|
31
|
+
export function captures(params, output) {
|
|
32
|
+
const values = Object.create(null);
|
|
33
|
+
for (const capture of (params.captureVars ?? [])) {
|
|
34
|
+
const match = capturePattern(capture.pattern, output);
|
|
35
|
+
const value = match?.[capture.group ?? 1];
|
|
36
|
+
if (value !== undefined)
|
|
37
|
+
values[capture.name] = value;
|
|
38
|
+
}
|
|
39
|
+
return values;
|
|
40
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type VerificationCommand } from '../../../pipeline/pipeline-state.js';
|
|
2
|
+
import type { JsonObject, Piece } from '../contracts.js';
|
|
3
|
+
import type { PieceDependencyProvider } from './ports.js';
|
|
4
|
+
/** Only explicit commandLine uses a platform shell; structured argv stays structured. */
|
|
5
|
+
export declare function shellCommand(params: JsonObject, platform?: NodeJS.Platform): VerificationCommand;
|
|
6
|
+
export declare function shellPiece(bindings: PieceDependencyProvider): Piece;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
import { executeVerification, executeVerificationCommand } from '../../../pipeline/pipeline-state.js';
|
|
3
|
+
import { assertProposalInScope, withScopeDefault } from '../../change-scope.js';
|
|
4
|
+
import { captures, captureSchema, historyEntry, paramsSchema, positiveInteger, stringSchema, text } from './shared.js';
|
|
5
|
+
import { receiptEvidence, verificationDeadline } from './verify.js';
|
|
6
|
+
const schema = { ...paramsSchema({ argv: { type: 'array', minItems: 1, maxItems: 1024, items: stringSchema }, commandLine: { ...stringSchema, minLength: 1 },
|
|
7
|
+
repositoryId: { type: 'string', minLength: 1, maxLength: 128 }, cwd: { type: 'string', maxLength: 4096 }, env: { type: 'object', additionalProperties: stringSchema },
|
|
8
|
+
timeoutMs: { ...positiveInteger, maximum: 7_200_000 }, captureVars: captureSchema, evidence: { type: 'boolean' }, outputCapBytes: { type: 'integer', minimum: 128, maximum: 1_048_576 },
|
|
9
|
+
}, ['repositoryId']), oneOf: [{ properties: { argv: true }, required: ['argv'] }, { properties: { commandLine: true }, required: ['commandLine'] }] };
|
|
10
|
+
/** Only explicit commandLine uses a platform shell; structured argv stays structured. */
|
|
11
|
+
export function shellCommand(params, platform = process.platform) {
|
|
12
|
+
const argv = params.argv;
|
|
13
|
+
const command = argv ? argv[0] : platform === 'win32' ? process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe' : '/bin/sh';
|
|
14
|
+
const args = argv ? argv.slice(1) : platform === 'win32' ? ['/d', '/s', '/c', text(params.commandLine)] : ['-c', text(params.commandLine)];
|
|
15
|
+
return { repositoryId: text(params.repositoryId), command, args, ...(typeof params.cwd === 'string' ? { cwd: params.cwd } : {}),
|
|
16
|
+
...(params.env ? { env: params.env } : {}), timeoutMs: typeof params.timeoutMs === 'number' ? params.timeoutMs : 600_000 };
|
|
17
|
+
}
|
|
18
|
+
export function shellPiece(bindings) {
|
|
19
|
+
return { descriptor: { kind: 'shell', paramsSchema: schema, outcomes: ['ok', 'fail', 'failed'], effect: 'write', requiresAI: false },
|
|
20
|
+
async execute(params, context) {
|
|
21
|
+
const deps = bindings();
|
|
22
|
+
const command = withScopeDefault(deps.context, shellCommand(params));
|
|
23
|
+
assertProposalInScope(deps.context, command, 'shell command');
|
|
24
|
+
const log = (output) => context.progress({ type: 'verification-output', payload: { text: output } });
|
|
25
|
+
let receipt, result;
|
|
26
|
+
if (params.evidence === true) {
|
|
27
|
+
const port = deps.verification(context);
|
|
28
|
+
let captured;
|
|
29
|
+
receipt = await executeVerification(deps.context, { kind: 'scoped', commands: [command] }, {
|
|
30
|
+
...port, persistCheck: async (check, plan, candidate) => { if (!check.pending)
|
|
31
|
+
captured = { ...check }; await port.persistCheck(check, plan, candidate); },
|
|
32
|
+
}, log, context.signal, { deadline: verificationDeadline(deps, context) });
|
|
33
|
+
result = captured ?? receipt.commands[0];
|
|
34
|
+
}
|
|
35
|
+
else
|
|
36
|
+
result = await executeVerificationCommand(deps.context, command, log, context.signal, { deadline: verificationDeadline(deps, context) });
|
|
37
|
+
const cap = typeof params.outputCapBytes === 'number' ? params.outputCapBytes : 262_144;
|
|
38
|
+
const bounded = (value) => new StringDecoder('utf8').write(Buffer.from(value).subarray(0, cap));
|
|
39
|
+
const stdout = bounded(result.stdout ?? ''), stderr = bounded(result.stderr ?? result.output);
|
|
40
|
+
const vars = captures(params, stdout + '\n' + stderr);
|
|
41
|
+
const failed = result.exitCode === -1 || ['timed-out', 'cancelled', 'interrupted'].includes(result.outcome ?? '');
|
|
42
|
+
return { outcome: failed ? 'failed' : result.exitCode === 0 ? 'ok' : 'fail', ...(failed ? { status: 'failed', error: { code: result.outcome === 'timed-out' ? 'timeout' : result.outcome === 'cancelled' ? 'aborted' : 'shell_execution_error', message: result.output } } : {}),
|
|
43
|
+
output: { exitCode: result.exitCode, stdout, stderr, vars, outputTruncated: result.outputTruncated === true || Buffer.byteLength(result.stdout ?? '') > cap || Buffer.byteLength(result.stderr ?? '') > cap },
|
|
44
|
+
vars, history: [historyEntry(context, stdout + (stderr ? '\n' + stderr : ''))], ...(receipt ? { receipt: receiptEvidence(receipt) } : {}) };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type VerificationReceipt } from '../../../pipeline/pipeline-state.js';
|
|
2
|
+
import type { Piece, PieceExecutionContext, ReceiptEvidence } from '../contracts.js';
|
|
3
|
+
import type { PieceDependencies, PieceDependencyProvider } from './ports.js';
|
|
4
|
+
export declare const verificationCommandSchema: import("../contracts.js").JsonObject;
|
|
5
|
+
export declare function receiptEvidence(receipt: VerificationReceipt): ReceiptEvidence;
|
|
6
|
+
export declare function verificationDeadline(deps: PieceDependencies, context: PieceExecutionContext): number | undefined;
|
|
7
|
+
export declare function verifyPiece(bindings: PieceDependencyProvider): Piece;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { contentDigest } from '../canonical-json.js';
|
|
2
|
+
import { advisoryMemory } from './project-memory.js';
|
|
3
|
+
import { executeVerification, validateVerificationRequest } from '../../../pipeline/pipeline-state.js';
|
|
4
|
+
import { withScopeDefault } from '../../change-scope.js';
|
|
5
|
+
import { json, paramsSchema, positiveInteger } from './shared.js';
|
|
6
|
+
export const verificationCommandSchema = paramsSchema({
|
|
7
|
+
repositoryId: { type: 'string', minLength: 1, maxLength: 128 }, command: { type: 'string', minLength: 1, maxLength: 4096 },
|
|
8
|
+
args: { type: 'array', maxItems: 1024, items: { type: 'string', maxLength: 32_000 } }, cwd: { type: 'string', maxLength: 4096 },
|
|
9
|
+
env: { type: 'object', additionalProperties: { type: 'string', maxLength: 32_000 } }, timeoutMs: { ...positiveInteger, maximum: 7_200_000 },
|
|
10
|
+
key: { type: 'string', maxLength: 128 }, label: { type: 'string', minLength: 1, maxLength: 256 }, policy: { type: 'object' },
|
|
11
|
+
}, ['repositoryId', 'command', 'args']);
|
|
12
|
+
export function receiptEvidence(receipt) {
|
|
13
|
+
return { id: receipt.id, candidateHash: receipt.candidateHash, valid: receipt.valid, scope: receipt.kind, evidence: json(receipt) };
|
|
14
|
+
}
|
|
15
|
+
export function verificationDeadline(deps, context) {
|
|
16
|
+
const budget = deps.stepContext(context).remainingBudget();
|
|
17
|
+
return budget.maxDurationMs === undefined ? undefined : Date.now() + Math.max(0, budget.maxDurationMs);
|
|
18
|
+
}
|
|
19
|
+
export function verifyPiece(bindings) {
|
|
20
|
+
return {
|
|
21
|
+
descriptor: { kind: 'verify', paramsSchema: paramsSchema({ commands: { oneOf: [{ const: 'configured' }, { type: 'array', maxItems: 100, items: verificationCommandSchema }] },
|
|
22
|
+
unverified: { type: 'boolean' }, maxConcurrency: { type: 'integer', minimum: 1, maximum: 4 } }, ['commands']), outcomes: ['pass', 'fail', 'failed'], effect: 'write', requiresAI: false, storeAccess: 'write' },
|
|
23
|
+
async execute(params, context) {
|
|
24
|
+
const deps = bindings();
|
|
25
|
+
const commands = (params.commands === 'configured' ? deps.config.verification : params.commands).map(command => withScopeDefault(deps.context, command));
|
|
26
|
+
const coversAll = deps.context.repositories.every(repository => commands.some(command => command.repositoryId === repository.id));
|
|
27
|
+
const request = { kind: coversAll || params.unverified === true ? 'full' : 'scoped', commands, ...(params.unverified === true ? { unverified: true } : {}) };
|
|
28
|
+
validateVerificationRequest(deps.context, request);
|
|
29
|
+
const identity = { candidateHash: deps.executionSnapshot(context).candidate?.hash ?? null, scopeId: context.frame.scope.id,
|
|
30
|
+
repositories: deps.context.repositories.map(repository => ({ id: repository.id, path: repository.path })),
|
|
31
|
+
planHash: contentDigest(json(request)), concurrency: params.maxConcurrency ?? deps.config.efficiency?.verification?.maxConcurrency ?? 1 };
|
|
32
|
+
const memoryKey = contentDigest(identity);
|
|
33
|
+
const known = await advisoryMemory(context, () => deps.memory(context).get(['verification', 'known-commands'], memoryKey));
|
|
34
|
+
const receipt = await executeVerification(deps.context, request, deps.verification(context), output => context.progress({ type: 'verification-output', payload: { text: output } }), context.signal, {
|
|
35
|
+
deadline: verificationDeadline(deps, context), maxConcurrency: params.maxConcurrency ?? deps.config.efficiency?.verification?.maxConcurrency,
|
|
36
|
+
idleTimeoutMs: deps.config.guardrails?.['verify-idle-timeout'] === false ? 0 : undefined,
|
|
37
|
+
onEvidence: async (kind, payload) => context.progress({ type: 'runtime-efficiency-event', payload: { kind, ...payload } }),
|
|
38
|
+
});
|
|
39
|
+
await advisoryMemory(context, () => deps.memory(context).put(['verification', 'known-commands'], memoryKey, {
|
|
40
|
+
...identity, observations: Math.min(1_000_000, typeof known?.value.observations === 'number' ? known.value.observations + 1 : 1),
|
|
41
|
+
lastAttemptId: context.frame.attemptId, lastReceiptId: receipt.id, lastValid: receipt.valid,
|
|
42
|
+
commands: receipt.commands.map(command => ({ repositoryId: command.repositoryId, command: command.command, exitCode: command.exitCode })),
|
|
43
|
+
}));
|
|
44
|
+
const infrastructure = receipt.commands.some(command => command.exitCode === -1 && command.outcome !== 'cancelled');
|
|
45
|
+
const outcome = infrastructure ? 'failed' : receipt.valid ? 'pass' : 'fail';
|
|
46
|
+
const verified = receipt.valid && receipt.commands.length > 0 && !receipt.unverifiedRepositories?.length;
|
|
47
|
+
return { outcome, ...(infrastructure ? { status: 'failed', error: { code: 'verification_execution_error', message: receipt.reason ?? 'A verification command could not complete' } } : {}),
|
|
48
|
+
output: { receiptId: receipt.id, valid: receipt.valid, ...(receipt.reason ? { reason: receipt.reason } : {}), commands: receipt.commands.map(command => ({ repositoryId: command.repositoryId, command: command.command, exitCode: command.exitCode })) },
|
|
49
|
+
receipt: receiptEvidence(receipt), ...(verified ? { verified: { receiptId: receipt.id, candidateHash: receipt.candidateHash, atTransition: context.frame.transition, revision: context.frame.transition } } : { verified: null }) };
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type PipelineContext } from '../../pipeline/pipeline-state.js';
|
|
2
|
+
import { type ExecutorRegistry } from '../executors.js';
|
|
3
|
+
import type { RuntimeConfig } from '../executor-types.js';
|
|
4
|
+
import type { RoleCatalog, WorkflowDefinition } from './definition-types.js';
|
|
5
|
+
export interface DefinitionRunRequest {
|
|
6
|
+
context: PipelineContext;
|
|
7
|
+
config: RuntimeConfig;
|
|
8
|
+
change?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DefinitionRunInput {
|
|
11
|
+
context: unknown;
|
|
12
|
+
config: unknown;
|
|
13
|
+
definition: unknown;
|
|
14
|
+
change?: string;
|
|
15
|
+
registry?: ExecutorRegistry;
|
|
16
|
+
}
|
|
17
|
+
export declare function configuredRoles(config: RuntimeConfig): RoleCatalog;
|
|
18
|
+
/** Complete admission is read-only; no run, journal, provider turn or skill is created. */
|
|
19
|
+
export declare function preflightDefinition(input: DefinitionRunInput): Promise<{
|
|
20
|
+
request: DefinitionRunRequest;
|
|
21
|
+
definition: WorkflowDefinition;
|
|
22
|
+
registry: ExecutorRegistry;
|
|
23
|
+
budget: import("./contracts.js").WorkflowBudget;
|
|
24
|
+
roles: Readonly<Record<string, {
|
|
25
|
+
access: import("./contracts.js").EngineEffect;
|
|
26
|
+
}>>;
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { validatePipelineContext, validateVerificationRequest } from '../../pipeline/pipeline-state.js';
|
|
2
|
+
import { normalizeRuntimeConfig, resolveRoleDescriptor, roleIds } from '../config.js';
|
|
3
|
+
import { assertEffortSupported } from '../capabilities.js';
|
|
4
|
+
import { createExecutorRegistry } from '../executors.js';
|
|
5
|
+
import { EngineError } from './contracts.js';
|
|
6
|
+
import { intersectBudgets } from './budget.js';
|
|
7
|
+
import { validateWorkflowDefinition } from './definition-validator.js';
|
|
8
|
+
import { validationPieceRegistry } from './pieces/index.js';
|
|
9
|
+
export function configuredRoles(config) {
|
|
10
|
+
return Object.fromEntries(roleIds(config).map(id => [id, { access: resolveRoleDescriptor(config, id).access }]));
|
|
11
|
+
}
|
|
12
|
+
/** Complete admission is read-only; no run, journal, provider turn or skill is created. */
|
|
13
|
+
export async function preflightDefinition(input) {
|
|
14
|
+
const context = validatePipelineContext(input.context);
|
|
15
|
+
const config = normalizeRuntimeConfig(input.config, { registeredProviderIds: input.registry?.ids() });
|
|
16
|
+
if (config.enabled === false)
|
|
17
|
+
throw new EngineError('runtime_disabled', 'The project runtime is disabled');
|
|
18
|
+
if (context.ownership.git !== 'host')
|
|
19
|
+
throw new EngineError('invalid_ownership', 'Definition delivery must be owned by the host');
|
|
20
|
+
const roles = configuredRoles(config), validation = validateWorkflowDefinition(input.definition, validationPieceRegistry(), roles, { published: true });
|
|
21
|
+
if (!validation.ok)
|
|
22
|
+
throw new EngineError('invalid_definition', 'Definition failed admission', validation.errors.map(error => ({ ...error })));
|
|
23
|
+
const definition = validation.definition;
|
|
24
|
+
if (definition.roles.some(id => resolveRoleDescriptor(config, id).openspecSkill) && !input.change)
|
|
25
|
+
throw new EngineError('invalid_arguments', 'OpenSpec roles require a frozen change name');
|
|
26
|
+
if (definition.change !== 'none' && (!input.change || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.change) || input.change.length > 64))
|
|
27
|
+
throw new EngineError('invalid_arguments', 'This definition requires a change name of at most 64 characters');
|
|
28
|
+
if (definition.journal === 'implementation' && !context.specs.length)
|
|
29
|
+
throw new EngineError('invalid_scope', 'Implementation requires frozen specs or a goal');
|
|
30
|
+
if (config.verification.length)
|
|
31
|
+
validateVerificationRequest(context, { kind: 'scoped', commands: config.verification });
|
|
32
|
+
const budget = intersectBudgets(definition.budget, config.limits);
|
|
33
|
+
const registry = input.registry ?? createExecutorRegistry(config);
|
|
34
|
+
const engines = definition.roles.flatMap(id => {
|
|
35
|
+
const role = resolveRoleDescriptor(config, id);
|
|
36
|
+
return [role, ...(role.escalation ? [{ ...role, ...role.escalation }] : [])];
|
|
37
|
+
});
|
|
38
|
+
for (const body of [definition, ...Object.values(definition.components ?? {})])
|
|
39
|
+
for (const node of Object.values(body.nodes)) {
|
|
40
|
+
if (node.kind !== 'prompt')
|
|
41
|
+
continue;
|
|
42
|
+
const engine = node.params.engine;
|
|
43
|
+
registry.validateLimits(engine.provider, budget);
|
|
44
|
+
if (engine.effort)
|
|
45
|
+
assertEffortSupported(engine, await registry.capabilities(engine.provider, engine.model));
|
|
46
|
+
}
|
|
47
|
+
for (const engine of engines) {
|
|
48
|
+
registry.validateLimits(engine.provider, budget);
|
|
49
|
+
if (engine.effort)
|
|
50
|
+
assertEffortSupported(engine, await registry.capabilities(engine.provider, engine.model));
|
|
51
|
+
}
|
|
52
|
+
const request = { context, config, ...(input.change ? { change: input.change } : {}) };
|
|
53
|
+
return { request, definition, registry, budget, roles };
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Script } from 'node:vm';
|
|
2
|
+
import { EngineError } from './contracts.js';
|
|
3
|
+
const matchScript = new Script('new RegExp(pattern).exec(text)', { filename: 'core-bounded-pattern' });
|
|
4
|
+
/** Regex is data in a constant script. V8 interrupts even catastrophic matching. */
|
|
5
|
+
export function capturePattern(pattern, text) {
|
|
6
|
+
if (pattern.length > 200)
|
|
7
|
+
throw new EngineError('invalid_expression', 'Pattern exceeds 200 characters');
|
|
8
|
+
try {
|
|
9
|
+
const match = matchScript.runInNewContext({ pattern, text: text.slice(0, 32_000) }, {
|
|
10
|
+
timeout: 50, contextCodeGeneration: { strings: false, wasm: false },
|
|
11
|
+
});
|
|
12
|
+
return match ? Array.from(match) : null;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
throw new EngineError('invalid_expression', error !== null && typeof error === 'object' && 'code' in error && error.code === 'ERR_SCRIPT_EXECUTION_TIMEOUT'
|
|
16
|
+
? 'Regular expression exceeded 50 ms' : 'Invalid regular expression');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function matchesPattern(pattern, text) { return capturePattern(pattern, text) !== null; }
|