specrails-core 6.6.5 → 6.7.1
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/dist/agent-runtime/capabilities.d.ts +5 -2
- package/dist/agent-runtime/capabilities.js +8 -2
- package/dist/agent-runtime/change-scope.d.ts +2 -0
- package/dist/agent-runtime/change-scope.js +5 -0
- package/dist/agent-runtime/cli-executor.js +8 -1
- package/dist/agent-runtime/cli.d.ts +6 -1
- package/dist/agent-runtime/cli.js +15 -4
- package/dist/agent-runtime/compact/reviewer.js +3 -1
- package/dist/agent-runtime/config.js +16 -5
- package/dist/agent-runtime/efficiency-summary.d.ts +7 -0
- package/dist/agent-runtime/efficiency-summary.js +7 -1
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +1 -1
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +9 -0
- package/dist/agent-runtime/engine/checkpoint/database.js +40 -3
- package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +1 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.js +7 -2
- package/dist/agent-runtime/engine/cli.d.ts +12 -0
- package/dist/agent-runtime/engine/cli.js +15 -4
- package/dist/agent-runtime/engine/definition-schema.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-schema.js +1 -0
- package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-types.js +1 -1
- package/dist/agent-runtime/engine/definition-validator.js +8 -0
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +7 -0
- package/dist/agent-runtime/engine/graph-description.d.ts +3 -3
- package/dist/agent-runtime/engine/invocation-context.js +2 -0
- package/dist/agent-runtime/engine/piece-registry.d.ts +1 -1
- package/dist/agent-runtime/engine/piece-registry.js +1 -1
- package/dist/agent-runtime/engine/pieces/artifact-contract.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/artifact-contract.js +6 -2
- package/dist/agent-runtime/engine/pieces/derivation-coverage.d.ts +30 -0
- package/dist/agent-runtime/engine/pieces/derivation-coverage.js +95 -0
- package/dist/agent-runtime/engine/pieces/derive-change.d.ts +50 -0
- package/dist/agent-runtime/engine/pieces/derive-change.js +209 -0
- package/dist/agent-runtime/engine/pieces/index.js +2 -1
- package/dist/agent-runtime/engine/pieces/openspec.d.ts +16 -1
- package/dist/agent-runtime/engine/pieces/openspec.js +39 -30
- package/dist/agent-runtime/engine/pieces/role-turn.js +26 -5
- package/dist/agent-runtime/engine/preflight.d.ts +2 -1
- package/dist/agent-runtime/engine/preflight.js +3 -1
- package/dist/agent-runtime/engine/regex.d.ts +12 -2
- package/dist/agent-runtime/engine/regex.js +25 -11
- package/dist/agent-runtime/engine/run-status.d.ts +34 -0
- package/dist/agent-runtime/engine/run-status.js +7 -1
- package/dist/agent-runtime/engine/runs.d.ts +56 -0
- package/dist/agent-runtime/engine/runs.js +88 -5
- package/dist/agent-runtime/engine/steering/inbox.d.ts +15 -0
- package/dist/agent-runtime/engine/steering/inbox.js +21 -1
- package/dist/agent-runtime/engine/storage/private-path.js +7 -1
- package/dist/agent-runtime/evaluation.js +1 -1
- package/dist/agent-runtime/executor-types.d.ts +19 -0
- package/dist/agent-runtime/executor-types.js +10 -0
- package/dist/agent-runtime/failure-classification.d.ts +51 -0
- package/dist/agent-runtime/failure-classification.js +110 -0
- package/dist/agent-runtime/graph/artifacts.d.ts +3 -1
- package/dist/agent-runtime/graph/artifacts.js +5 -1
- package/dist/agent-runtime/graph/baseline.d.ts +50 -0
- package/dist/agent-runtime/graph/baseline.js +145 -0
- package/dist/agent-runtime/graph/capability-map.d.ts +29 -0
- package/dist/agent-runtime/graph/capability-map.js +65 -0
- package/dist/agent-runtime/graph/clarifications.d.ts +75 -0
- package/dist/agent-runtime/graph/clarifications.js +158 -0
- package/dist/agent-runtime/graph/nodes.js +178 -19
- package/dist/agent-runtime/graph/review-brief-record.d.ts +25 -0
- package/dist/agent-runtime/graph/review-brief-record.js +26 -0
- package/dist/agent-runtime/graph/review-brief.d.ts +73 -0
- package/dist/agent-runtime/graph/review-brief.js +127 -0
- package/dist/agent-runtime/graph/roles.js +5 -3
- package/dist/agent-runtime/graph/spec-trace-record.d.ts +33 -0
- package/dist/agent-runtime/graph/spec-trace-record.js +65 -0
- package/dist/agent-runtime/graph/state.d.ts +8 -0
- package/dist/agent-runtime/index.d.ts +7 -0
- package/dist/agent-runtime/index.js +7 -0
- package/dist/agent-runtime/openspec.d.ts +1 -0
- package/dist/agent-runtime/openspec.js +1 -1
- package/dist/agent-runtime/project-rules.d.ts +139 -0
- package/dist/agent-runtime/project-rules.js +683 -0
- package/dist/agent-runtime/prompts.d.ts +32 -1
- package/dist/agent-runtime/prompts.js +182 -7
- package/dist/agent-runtime/role-routing.d.ts +2 -1
- package/dist/agent-runtime/session/application/active-session.d.ts +43 -2
- package/dist/agent-runtime/session/application/active-session.js +163 -7
- package/dist/agent-runtime/session/application/session-service.d.ts +2 -2
- package/dist/agent-runtime/session/domain/descriptor.js +5 -0
- package/dist/agent-runtime/session/domain/interruption.js +1 -0
- package/dist/agent-runtime/session/domain/policy.js +2 -0
- package/dist/agent-runtime/session/domain/redaction.d.ts +19 -0
- package/dist/agent-runtime/session/domain/redaction.js +71 -0
- package/dist/agent-runtime/session/domain/types.d.ts +17 -1
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +14 -0
- package/dist/agent-runtime/session/drivers/claude/argv.js +19 -2
- package/dist/agent-runtime/session/drivers/claude/driver.js +11 -1
- package/dist/agent-runtime/session/drivers/claude/translator.d.ts +4 -0
- package/dist/agent-runtime/session/drivers/claude/translator.js +16 -3
- package/dist/agent-runtime/session/drivers/codex/driver.js +2 -0
- package/dist/agent-runtime/session/drivers/executor/driver.js +2 -0
- package/dist/agent-runtime/session/host/protocol.d.ts +12 -0
- package/dist/agent-runtime/session/host/protocol.js +10 -2
- package/dist/agent-runtime/session/host/server.js +6 -5
- package/dist/agent-runtime/session/index.d.ts +1 -1
- package/dist/agent-runtime/session/journal/sqlite-journal.js +6 -2
- package/dist/agent-runtime/session/ports.d.ts +6 -2
- package/dist/agent-runtime/session/testing/driver-conformance.js +16 -2
- package/dist/agent-runtime/session/testing/memory-journal.js +4 -2
- package/dist/installer/cli.js +10 -1
- package/dist/installer/commands/init.js +14 -3
- package/dist/installer/phases/scaffold.js +56 -5
- package/dist/pipeline/pipeline-state.d.ts +22 -0
- package/dist/pipeline/pipeline-state.js +55 -4
- package/dist/pipeline/spec-trace.d.ts +134 -0
- package/dist/pipeline/spec-trace.js +392 -0
- package/dist/shared/secret-redaction.d.ts +42 -0
- package/dist/shared/secret-redaction.js +184 -0
- package/integration-contract.json +177 -7
- package/package.json +7 -7
- package/schemas/agent-runtime.schema.json +3 -1
- package/schemas/workflow-definition.schema.json +1 -0
- package/templates/settings/codex-config.toml +3 -2
|
@@ -2,8 +2,11 @@ import { type CliProcessRunner } from './cli-process.js';
|
|
|
2
2
|
import { type CliProvider, type ExecutorCapabilities, type RuntimeAgentConfig, type RuntimeConfig } from './executor-types.js';
|
|
3
3
|
import type { ExecutorRegistry } from './executors.js';
|
|
4
4
|
export declare function unknownCapabilities(transport: string): ExecutorCapabilities;
|
|
5
|
-
/** Introspection never calls an inference endpoint.
|
|
6
|
-
*
|
|
5
|
+
/** Introspection never calls an inference endpoint. Two resume contracts are
|
|
6
|
+
* recognized: Codex `exec resume`, whose possible cold fallback needs full
|
|
7
|
+
* context, and Claude `-p --resume <id>`, which either continues the stored
|
|
8
|
+
* conversation of that ID or fails with "No conversation found" (mapped to
|
|
9
|
+
* `session_not_found`, so the role falls back to a fresh full turn). */
|
|
7
10
|
export declare function cliCapabilities(provider: CliProvider, model: string | undefined, options?: {
|
|
8
11
|
runProcess?: CliProcessRunner;
|
|
9
12
|
env?: NodeJS.ProcessEnv;
|
|
@@ -7,8 +7,11 @@ import { AgentExecutionError } from './executor-types.js';
|
|
|
7
7
|
export function unknownCapabilities(transport) {
|
|
8
8
|
return { transport, continuation: 'unknown', effortSupport: 'unknown', supportedEfforts: null, observedModel: false, observedEffort: false };
|
|
9
9
|
}
|
|
10
|
-
/** Introspection never calls an inference endpoint.
|
|
11
|
-
*
|
|
10
|
+
/** Introspection never calls an inference endpoint. Two resume contracts are
|
|
11
|
+
* recognized: Codex `exec resume`, whose possible cold fallback needs full
|
|
12
|
+
* context, and Claude `-p --resume <id>`, which either continues the stored
|
|
13
|
+
* conversation of that ID or fails with "No conversation found" (mapped to
|
|
14
|
+
* `session_not_found`, so the role falls back to a fresh full turn). */
|
|
12
15
|
export async function cliCapabilities(provider, model, options = {}) {
|
|
13
16
|
const result = unknownCapabilities(provider === 'kimi' ? 'kimi-acp' : `${provider}-cli`);
|
|
14
17
|
if (provider === 'kimi')
|
|
@@ -30,6 +33,9 @@ export async function cliCapabilities(provider, model, options = {}) {
|
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
if (provider === 'claude') {
|
|
36
|
+
// Claude Code 2.1.309 (recorded): an unknown ID fails before inference with "No conversation found"; it never starts cold.
|
|
37
|
+
if (/^\s*(?:-r, )?--resume\b/m.test(help.stdout))
|
|
38
|
+
result.continuation = 'supported';
|
|
33
39
|
const section = /--effort\s+<[^>]+>([\s\S]*?)(?=\n\s+--|$)/.exec(help.stdout)?.[1];
|
|
34
40
|
const levels = section?.match(/\(([^)]+)\)/)?.[1].split(',').map(value => value.trim());
|
|
35
41
|
if (levels?.length && levels.every(value => /^[a-z][a-z0-9_-]{0,31}$/.test(value)))
|
|
@@ -27,6 +27,8 @@ export declare function assertProposalInScope(context: PipelineContext, command:
|
|
|
27
27
|
}, what?: string): void;
|
|
28
28
|
/** Base commit per repository id; the first call records it. */
|
|
29
29
|
export declare function changeBases(context: PipelineContext): Record<string, string>;
|
|
30
|
+
/** The commit HEAD names in a repository checkout, or null when it has none. */
|
|
31
|
+
export declare function repositoryHead(root: string): string | null;
|
|
30
32
|
/** Every path that differs from the base: tracked edits (staged or not), deletions and new untracked files. */
|
|
31
33
|
export declare function repositoryChanges(context: PipelineContext, repository: PipelineRepository, base: string, change?: string): ChangedFile[];
|
|
32
34
|
/** What this run changed, per repository and bounded: untracked files that predate the run are not part of it. */
|
|
@@ -119,6 +119,11 @@ function baseRecords(context) {
|
|
|
119
119
|
export function changeBases(context) {
|
|
120
120
|
return Object.fromEntries(Object.entries(baseRecords(context)).map(([id, record]) => [id, record.base]));
|
|
121
121
|
}
|
|
122
|
+
/** The commit HEAD names in a repository checkout, or null when it has none. */
|
|
123
|
+
export function repositoryHead(root) {
|
|
124
|
+
const head = git(root, ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'], true);
|
|
125
|
+
return head.ok && /^[a-f0-9]{40,64}$/.test(head.stdout.trim()) ? head.stdout.trim() : null;
|
|
126
|
+
}
|
|
122
127
|
/** Host and runtime state, the change's own OpenSpec artifacts and generated trees are never part of the code change. */
|
|
123
128
|
function ignored(context, repository, change, file, fresh) {
|
|
124
129
|
if (file === '.specrails' || file.startsWith('.specrails/'))
|
|
@@ -14,6 +14,8 @@ import { canonicalWorkspace } from './workspace-tools.js';
|
|
|
14
14
|
import { parseStructuredText } from './openai-executor.js';
|
|
15
15
|
import { executeKimiReadonlyAcp } from './kimi-acp.js';
|
|
16
16
|
import { assertGeminiAdminPolicyAvailable, GEMINI_READONLY_POLICY } from './gemini-policy.js';
|
|
17
|
+
/** Claude's print-mode answer to `--resume <id>` when no transcript exists for that ID in this project. */
|
|
18
|
+
const CLAUDE_SESSION_MISSING = /No conversation found with session ID/i;
|
|
17
19
|
/** Tools a Claude developer may not use: nested agents and platform skills would start a second, unobserved workflow. */
|
|
18
20
|
const CLAUDE_DEVELOPER_DISALLOWED = 'Agent,Task,Skill';
|
|
19
21
|
/**
|
|
@@ -32,7 +34,7 @@ export function buildCliInvocation(provider, request, options = {}) {
|
|
|
32
34
|
}
|
|
33
35
|
const readOnly = requestPolicy(request).access === 'read';
|
|
34
36
|
// Pin the product alias; explicit IDs remain reproducible across releases.
|
|
35
|
-
const claudeModelIds = { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5', haiku: 'claude-haiku-5-5' };
|
|
37
|
+
const claudeModelIds = { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5', haiku: 'claude-haiku-5-5', fable: 'claude-fable-5-1' };
|
|
36
38
|
const modelId = provider === 'claude' && request.model ? claudeModelIds[request.model] ?? request.model : request.model;
|
|
37
39
|
const model = modelId ? ['--model', modelId] : [];
|
|
38
40
|
const extraRoots = request.allowedRoots.filter(root => root !== request.cwd);
|
|
@@ -419,6 +421,11 @@ export class CliExecutor {
|
|
|
419
421
|
throw new AgentExecutionError(message, parsed.failureKind, parsed.usage);
|
|
420
422
|
}
|
|
421
423
|
if (result.exitCode !== 0 || parsed.failed) {
|
|
424
|
+
// A resumed Claude session whose transcript is gone fails before any
|
|
425
|
+
// inference; the role layer then starts a fresh full turn.
|
|
426
|
+
if (this.provider === 'claude' && request.resumeSessionId && CLAUDE_SESSION_MISSING.test(result.stdout + '\n' + result.stderr)) {
|
|
427
|
+
throw new AgentExecutionError('claude has no stored conversation for the resumed session', 'session_not_found', parsed.usage);
|
|
428
|
+
}
|
|
422
429
|
const diagnostic = providerDiagnostic(result.stdout, result.stderr, { ...process.env, ...this.options.env });
|
|
423
430
|
throw new AgentExecutionError(`${this.provider} execution failed${result.exitCode !== 0 ? ` (exit ${result.exitCode})` : ''}.${diagnostic ? ' ' + diagnostic : ' The provider did not report a diagnostic.'}`, 'provider_execution_error', parsed.usage);
|
|
424
431
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FailureClassification } from './failure-classification.js';
|
|
1
2
|
import type { WorkflowState } from './workflow-types.js';
|
|
2
3
|
/** Both runtime entry points emit the same machine-readable fatal result. */
|
|
3
4
|
export declare function runtimeFailure(error: unknown): {
|
|
@@ -10,7 +11,11 @@ export declare function runtimeFailure(error: unknown): {
|
|
|
10
11
|
};
|
|
11
12
|
};
|
|
12
13
|
/** 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', 'host', 'help'];
|
|
14
|
+
export declare const RUNTIME_CLI_OPERATIONS: readonly ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'pause', 'host', 'help'];
|
|
15
|
+
/** `failure` (contract 5.8): the structured classification of a failed or blocked engine 1 run; absent otherwise. */
|
|
16
|
+
export declare function failureField(state: WorkflowState | null): {
|
|
17
|
+
failure?: FailureClassification;
|
|
18
|
+
};
|
|
14
19
|
/** The host-facing summary: phase status, pending interrupts and usage without accumulated outputs. */
|
|
15
20
|
export declare function compactState(state: WorkflowState | null): {
|
|
16
21
|
runId: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyFailure } from './failure-classification.js';
|
|
1
2
|
import { isDefinitionCommand, NODE_KINDS_VERSION, runDefinitionCommand } from './engine/cli.js';
|
|
2
3
|
import { validationPieceRegistry } from './engine/pieces/index.js';
|
|
3
4
|
import { GUARDRAIL_CATALOG } from './guardrails.js';
|
|
@@ -14,6 +15,10 @@ import { CORE_PACKAGE_VERSION, CORE_WORKFLOW_VERSION, RUNTIME_API_VERSION, coreR
|
|
|
14
15
|
import { sameRuntimeIdentity } from './runtime-identity.js';
|
|
15
16
|
import { readWorkflowState } from './workflow.js';
|
|
16
17
|
import { runtimeEfficiency } from './efficiency.js';
|
|
18
|
+
import { readReviewBriefRecord } from './graph/review-brief-record.js';
|
|
19
|
+
import { readClarificationsRecord } from './graph/clarifications.js';
|
|
20
|
+
import { readSpecTraceReport } from './graph/spec-trace-record.js';
|
|
21
|
+
import { readBaselineRecord } from './graph/baseline.js';
|
|
17
22
|
import { configuredCapabilities } from './capabilities.js';
|
|
18
23
|
import { createExecutorRegistry } from './executors.js';
|
|
19
24
|
import { runRecovery } from './recovery.js';
|
|
@@ -69,7 +74,7 @@ export function runtimeFailure(error) {
|
|
|
69
74
|
: error instanceof Error ? error.message : String(error) };
|
|
70
75
|
}
|
|
71
76
|
/** Machine operations are mirrored in integration-contract.json; help is presentation-only. */
|
|
72
|
-
export const RUNTIME_CLI_OPERATIONS = ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'host', 'help'];
|
|
77
|
+
export const RUNTIME_CLI_OPERATIONS = ['api', 'validate', 'run', 'status', 'resume', 'prompts', 'capabilities', 'evidence', 'recovery', 'evaluate', 'workflows', 'fork', 'signal', 'cancel', 'pause', 'host', 'help'];
|
|
73
78
|
function read(file) { return JSON.parse(readFileSync(file, 'utf8')); }
|
|
74
79
|
async function readStdin() {
|
|
75
80
|
const chunks = [];
|
|
@@ -83,6 +88,11 @@ async function readStdin() {
|
|
|
83
88
|
}
|
|
84
89
|
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
85
90
|
}
|
|
91
|
+
/** `failure` (contract 5.8): the structured classification of a failed or blocked engine 1 run; absent otherwise. */
|
|
92
|
+
export function failureField(state) {
|
|
93
|
+
const failure = state ? classifyFailure({ status: state.status, error: state.error, steps: state.steps, nextStep: state.nextStep }) : undefined;
|
|
94
|
+
return failure ? { failure } : {};
|
|
95
|
+
}
|
|
86
96
|
/** The host-facing summary: phase status, pending interrupts and usage without accumulated outputs. */
|
|
87
97
|
export function compactState(state) {
|
|
88
98
|
if (!state)
|
|
@@ -194,6 +204,7 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
|
|
|
194
204
|
'specrails-core runtime fork --context <json> --from <nodePath> --run-id <newRunId> [--scope-id <scope>] [--visit <number>] [--state <json>]',
|
|
195
205
|
'specrails-core runtime signal --context <json> --stdin [--request-id <id>]',
|
|
196
206
|
'specrails-core runtime cancel --context <json> [--request-id <id>]',
|
|
207
|
+
'specrails-core runtime pause --context <json> [--request-id <id>] [--reason requested|budget|<token>]',
|
|
197
208
|
'specrails-core runtime host --stdio [--scope <projectKey|global>]',
|
|
198
209
|
] });
|
|
199
210
|
return 0;
|
|
@@ -203,7 +214,7 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
|
|
|
203
214
|
emit({ type: 'runtime-api', apiVersion: RUNTIME_API_VERSION, coreVersion: CORE_PACKAGE_VERSION, runtimeIdentity: coreRuntimeIdentity(), workflowVersions: [CORE_WORKFLOW_VERSION],
|
|
204
215
|
engineVersion: 2, engines: RUNTIME_ENGINES, nodeKindsVersion: NODE_KINDS_VERSION, nodeKinds: validationPieceRegistry().kinds(),
|
|
205
216
|
capabilities: { openRoles: 1, scopedRecovery: 1, efficientRoleExecution: 1, reproducibleVerification: 1, implementationEfficiencyMetrics: 1, compactAgentLoop: 1, configurableGuardrails: 1, compactOutputBudget: 1, roleThinkingControl: 1, repositoryScope: 1,
|
|
206
|
-
engineV2: 1, workflowAgentSteps: 1, implementationSteps: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1, hostBlockers: 1, setupCommands: 1, sessions: 1 }, guardrails: GUARDRAIL_CATALOG });
|
|
217
|
+
engineV2: 1, workflowAgentSteps: 1, implementationSteps: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1, hostBlockers: 1, setupCommands: 1, sessions: 1, flowSessions: 1, reviewBrief: 1, deriveChange: 1, architectClarifications: 1, specTrace: 1, baselineOnTouch: 1, rulesInventory: 1, pauseRuns: 1, failureClassification: 1, findingLocations: 1 }, guardrails: GUARDRAIL_CATALOG });
|
|
207
218
|
return 0;
|
|
208
219
|
}
|
|
209
220
|
if (command === 'host')
|
|
@@ -249,7 +260,7 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
|
|
|
249
260
|
const pipeline = previous ? inspectPipeline(context) : null;
|
|
250
261
|
const requestPath = path.join(pipelineStateDirectory(context), 'agent-runtime-request.json');
|
|
251
262
|
const config = existsSync(requestPath) ? read(requestPath).config : undefined;
|
|
252
|
-
emit({ type: 'runtime-status', state: flags.compact ? compactState(previous) : previous, pipeline: flags.compact ? compactPipeline(pipeline) : pipeline, ...(previous ? { metrics: runtimeEfficiency(previous), efficiencySummary: efficiencySummary(previous, context, config, pipeline?.completion, pipeline ?? undefined) } : {}) });
|
|
263
|
+
emit({ type: 'runtime-status', ...failureField(previous), state: flags.compact ? compactState(previous) : previous, pipeline: flags.compact ? compactPipeline(pipeline) : pipeline, ...(previous ? { metrics: runtimeEfficiency(previous), efficiencySummary: efficiencySummary(previous, context, config, pipeline?.completion, pipeline ?? undefined) } : {}) });
|
|
253
264
|
return 0;
|
|
254
265
|
}
|
|
255
266
|
const requestFile = path.join(pipelineStateDirectory(context), 'agent-runtime-request.json');
|
|
@@ -304,7 +315,7 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
|
|
|
304
315
|
inspection = inspectPipeline(context);
|
|
305
316
|
}
|
|
306
317
|
catch { /* A failed scope inspection must not erase recorded spend. */ }
|
|
307
|
-
emit({ type: 'runtime-result', runId: state.runId, traceId: state.traceId, status: state.status, nextStep: state.nextStep, error: state.error, pendingApproval: state.pendingApproval, pendingQuestion: state.pendingQuestion, usage: state.usage, invocationUsage: invocationUsage(state, previous?.history.length ?? 0), metrics: runtimeEfficiency(state), efficiencySummary: efficiencySummary(state, context, request.config, inspection?.completion, inspection) });
|
|
318
|
+
emit({ type: 'runtime-result', runId: state.runId, traceId: state.traceId, status: state.status, nextStep: state.nextStep, error: state.error, pendingApproval: state.pendingApproval, pendingQuestion: state.pendingQuestion, usage: state.usage, invocationUsage: invocationUsage(state, previous?.history.length ?? 0), metrics: runtimeEfficiency(state), efficiencySummary: efficiencySummary(state, context, request.config, inspection?.completion, inspection), reviewBrief: readReviewBriefRecord(context), clarifications: readClarificationsRecord(context), specTrace: readSpecTraceReport(context), baseline: readBaselineRecord(context), ...failureField(state) });
|
|
308
319
|
return state.status === 'succeeded' ? 0 : state.status === 'paused' ? 2 : 1;
|
|
309
320
|
}
|
|
310
321
|
finally {
|
|
@@ -171,7 +171,9 @@ export async function runCompactReviewer(env, options = {}) {
|
|
|
171
171
|
approved: (raw.approved === true || resolved) && !uncertified && issues.length === 0,
|
|
172
172
|
summary: text(raw.summary).slice(0, 8000), issues, score: lift(raw.score, gate?.minScore),
|
|
173
173
|
aspects: Object.fromEntries(REVIEW_ASPECTS.map(name => [name, lift(aspectsRaw[name], gate?.aspects[name])])),
|
|
174
|
-
acceptance: { criteria: certified, checks: [], findings: strings(acceptanceRaw.findings, 50, 1000) },
|
|
174
|
+
acceptance: { criteria: certified, checks: [], findings: strings(acceptanceRaw.findings, 50, 1000), ...(Array.isArray(acceptanceRaw.findingLocations) ? { findingLocations: acceptanceRaw.findingLocations } : {}) },
|
|
175
|
+
// The optional risk brief passes through untouched; the reviewer node validates it.
|
|
176
|
+
...(raw.brief !== undefined && raw.brief !== null ? { brief: raw.brief } : {}),
|
|
175
177
|
};
|
|
176
178
|
return { text: JSON.stringify(structured), usage: env.client.usage, structured };
|
|
177
179
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
2
|
import { validateGuardrailSettings } from './guardrails.js';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { BUILTIN_ROLES, BUILTIN_ROLE_POLICY, ROLE_ID, isBuiltinRole } from './executor-types.js';
|
|
4
|
+
import { BUILTIN_ROLES, BUILTIN_ROLE_POLICY, ENGINE_ROLE_POLICY, ROLE_ID, isBuiltinRole, isEngineRole } from './executor-types.js';
|
|
5
5
|
import { DEFAULT_REVIEW_POLICY, REVIEW_ASPECTS } from './graph/review-policy.js';
|
|
6
6
|
const ROLES = [...BUILTIN_ROLES];
|
|
7
7
|
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
@@ -119,7 +119,7 @@ function uniqueKeys(commands, list) {
|
|
|
119
119
|
}
|
|
120
120
|
export function validateRuntimeConfig(input, options = {}) {
|
|
121
121
|
const config = object(input, '$');
|
|
122
|
-
keys(config, ['schemaVersion', 'enabled', 'providers', 'agents', 'roles', 'fixer', 'limits', 'verification', 'setup', 'approvalBeforeArchive', 'review', 'architect', 'rolePrompts', 'efficiency', 'guardrails'], '$');
|
|
122
|
+
keys(config, ['schemaVersion', 'enabled', 'providers', 'agents', 'roles', 'fixer', 'limits', 'verification', 'setup', 'approvalBeforeArchive', 'baselineOnTouch', 'review', 'architect', 'rolePrompts', 'efficiency', 'guardrails'], '$');
|
|
123
123
|
if (config.schemaVersion !== 1)
|
|
124
124
|
fail('schemaVersion', 'expected 1');
|
|
125
125
|
if (config.enabled !== undefined && typeof config.enabled !== 'boolean')
|
|
@@ -205,8 +205,8 @@ export function validateRuntimeConfig(input, options = {}) {
|
|
|
205
205
|
const raw = object(config.roles, 'roles');
|
|
206
206
|
roles = {};
|
|
207
207
|
for (const [id, value] of Object.entries(raw)) {
|
|
208
|
-
if (!ROLE_ID.test(id) || id === 'fixer')
|
|
209
|
-
fail(`roles.${id}`, 'expected a role id; fixer
|
|
208
|
+
if (!ROLE_ID.test(id) || id === 'fixer' || isEngineRole(id))
|
|
209
|
+
fail(`roles.${id}`, 'expected a role id; fixer and Core engine roles (deriver) are reserved');
|
|
210
210
|
const descriptor = object(value, `roles.${id}`);
|
|
211
211
|
const { access, artifacts, prompt, openspecSkill, ...assignment } = descriptor;
|
|
212
212
|
const selected = validateAgent(assignment, `roles.${id}`);
|
|
@@ -272,6 +272,8 @@ export function validateRuntimeConfig(input, options = {}) {
|
|
|
272
272
|
}
|
|
273
273
|
if (config.approvalBeforeArchive !== undefined && typeof config.approvalBeforeArchive !== 'boolean')
|
|
274
274
|
fail('approvalBeforeArchive', 'expected boolean');
|
|
275
|
+
if (config.baselineOnTouch !== undefined && typeof config.baselineOnTouch !== 'boolean')
|
|
276
|
+
fail('baselineOnTouch', 'expected boolean');
|
|
275
277
|
let review;
|
|
276
278
|
if (config.review !== undefined) {
|
|
277
279
|
const raw = object(config.review, 'review');
|
|
@@ -288,13 +290,18 @@ export function validateRuntimeConfig(input, options = {}) {
|
|
|
288
290
|
let architect;
|
|
289
291
|
if (config.architect !== undefined) {
|
|
290
292
|
const raw = object(config.architect, 'architect');
|
|
291
|
-
keys(raw, ['onLowConfidence'], 'architect');
|
|
293
|
+
keys(raw, ['onLowConfidence', 'clarifications'], 'architect');
|
|
292
294
|
architect = {};
|
|
293
295
|
if (raw.onLowConfidence !== undefined) {
|
|
294
296
|
if (raw.onLowConfidence !== 'ask' && raw.onLowConfidence !== 'proceed')
|
|
295
297
|
fail('architect.onLowConfidence', 'expected "ask" or "proceed"');
|
|
296
298
|
architect.onLowConfidence = raw.onLowConfidence;
|
|
297
299
|
}
|
|
300
|
+
if (raw.clarifications !== undefined) {
|
|
301
|
+
if (raw.clarifications !== 'ask-on-high' && raw.clarifications !== 'always-proceed')
|
|
302
|
+
fail('architect.clarifications', 'expected "ask-on-high" or "always-proceed"');
|
|
303
|
+
architect.clarifications = raw.clarifications;
|
|
304
|
+
}
|
|
298
305
|
}
|
|
299
306
|
let rolePrompts;
|
|
300
307
|
if (config.rolePrompts !== undefined) {
|
|
@@ -315,6 +322,7 @@ export function validateRuntimeConfig(input, options = {}) {
|
|
|
315
322
|
...(rolePrompts === undefined ? {} : { rolePrompts }),
|
|
316
323
|
...(config.limits === undefined ? {} : { limits: { ...config.limits } }),
|
|
317
324
|
...(config.approvalBeforeArchive === undefined ? {} : { approvalBeforeArchive: config.approvalBeforeArchive }),
|
|
325
|
+
...(config.baselineOnTouch === undefined ? {} : { baselineOnTouch: config.baselineOnTouch }),
|
|
318
326
|
...(review === undefined ? {} : { review }),
|
|
319
327
|
...(architect === undefined ? {} : { architect }),
|
|
320
328
|
...(guardrails === undefined ? {} : { guardrails }),
|
|
@@ -338,6 +346,9 @@ export function roleIds(config) { return [...new Set([...BUILTIN_ROLES, ...Objec
|
|
|
338
346
|
export function resolveRoleDescriptor(config, id) {
|
|
339
347
|
if (isBuiltinRole(id))
|
|
340
348
|
return { ...config.agents[id], ...BUILTIN_ROLE_POLICY[id], id };
|
|
349
|
+
// Engine roles reuse the architect's assignment; their instructions are Core's (prompts.ts), never configuration.
|
|
350
|
+
if (isEngineRole(id))
|
|
351
|
+
return { ...config.agents.architect, ...ENGINE_ROLE_POLICY[id], id };
|
|
341
352
|
const role = Object.hasOwn(config.roles ?? {}, id) ? config.roles[id] : undefined;
|
|
342
353
|
if (!role)
|
|
343
354
|
throw new Error(`Unknown runtime role '${id}'`);
|
|
@@ -48,6 +48,13 @@ export declare function efficiencySummary(state: WorkflowState, context: Pipelin
|
|
|
48
48
|
fullContexts: number;
|
|
49
49
|
incrementalContexts: number;
|
|
50
50
|
};
|
|
51
|
+
baseline: {
|
|
52
|
+
invocations: number;
|
|
53
|
+
roles: string[];
|
|
54
|
+
costUsd: number | null;
|
|
55
|
+
inputTokens: number | null;
|
|
56
|
+
outputTokens: number | null;
|
|
57
|
+
};
|
|
51
58
|
escalations: {
|
|
52
59
|
role: string;
|
|
53
60
|
attemptId: string;
|
|
@@ -5,7 +5,7 @@ import { runtimeEfficiency } from './efficiency.js';
|
|
|
5
5
|
/** Compact derived view. Durable invocation/evidence IDs remain the accounting authority. */
|
|
6
6
|
export function efficiencySummary(state, context, config, completion, identity, durableEvidence) {
|
|
7
7
|
const invocations = state.history.flatMap(attempt => [...(attempt.invocations ?? []), ...(attempt.pendingInvocations ?? [])].map(call => ({ ...call, role: attempt.stepId, attemptId: attempt.id })));
|
|
8
|
-
const kinds = ['initial', 'correction', 'repair', 'deepen', 'session-fallback'];
|
|
8
|
+
const kinds = ['initial', 'correction', 'repair', 'deepen', 'session-fallback', 'baseline'];
|
|
9
9
|
const byKind = Object.fromEntries(kinds.map(kind => [kind, invocations.filter(call => call.kind === kind).length]));
|
|
10
10
|
const measured = state.history.every(attempt => !attempt.pendingInvocations?.length) && state.history.every(attempt => !(config ? roleIds(config) : [...BUILTIN_ROLES]).includes(attempt.stepId) || attempt.invocations !== undefined) && invocations.every(call => call.kind !== undefined && call.promptBytes !== undefined);
|
|
11
11
|
const bytes = (key) => invocations.every(call => call[key] !== undefined) ? invocations.reduce((sum, call) => sum + call[key], 0) : null;
|
|
@@ -47,8 +47,14 @@ export function efficiencySummary(state, context, config, completion, identity,
|
|
|
47
47
|
return { role, provider: latest?.provider ?? selected?.provider ?? null, model: latest?.model ?? selected?.model ?? null, effort: latest ? latest.requestedEffort ?? null : selected?.effort ?? null, observedModel: null, observedEffort: null, origin: 'frozen-runtime-config', tier: latest?.tier ?? 'base' };
|
|
48
48
|
}),
|
|
49
49
|
invocations: { byKind, total: invocations.length, complete: measured, promptBytes: bytes('promptBytes'), contextBytes: bytes('contextBytes'), handoffBytes: bytes('handoffBytes'), fullContexts: invocations.filter(call => call.contextMode === 'full').length, incrementalContexts: invocations.filter(call => call.contextMode === 'incremental').length },
|
|
50
|
+
baseline: baselineSpend(invocations.filter(call => call.kind === 'baseline')),
|
|
50
51
|
escalations: escalations.slice(-16), escalationsTruncated: escalations.length > 16,
|
|
51
52
|
checks: { invalidated: identity && evidenceAvailable ? checks.filter(check => check.planHash !== identity.planHash || check.candidateHash !== identity.candidateHash || check.reuseReason === 'snapshot-inputs-changed-during-verification').length : null, available: evidenceAvailable, complete: checksComplete, executed: checksComplete ? checks.filter(check => check.disposition === 'executed').length : null, reused: evidenceAvailable ? checks.filter(check => check.disposition === 'reused').length : null, notRun: evidenceAvailable ? checks.filter(check => check.disposition === 'not-run').length : null, durationMs: checksComplete ? checks.reduce((sum, check) => sum + (typeof check.durationMs === 'number' ? check.durationMs : 0), 0) : null },
|
|
52
53
|
metrics: runtimeEfficiency(state, config ? roleIds(config) : undefined),
|
|
53
54
|
};
|
|
54
55
|
}
|
|
56
|
+
/** Spend of the baseline-on-touch turns, kept apart from the change's own cost; unknown values stay unknown. */
|
|
57
|
+
function baselineSpend(calls) {
|
|
58
|
+
const sum = (key) => calls.some(call => call.usage?.[key] == null) ? null : calls.reduce((total, call) => total + call.usage[key], 0);
|
|
59
|
+
return { invocations: calls.length, roles: [...new Set(calls.map(call => call.role))], costUsd: sum('costUsd'), inputTokens: sum('inputTokens'), outputTokens: sum('outputTokens') };
|
|
60
|
+
}
|
|
@@ -34,7 +34,7 @@ export function implementationFixture(approval = false) {
|
|
|
34
34
|
await tools.execute({ action: 'new' });
|
|
35
35
|
const files = [
|
|
36
36
|
['proposal', 'proposal.md', '# Why\nReturn two.\n# What Changes\nUpdate the function.\n# Capabilities\n## New Capabilities\n- feature: Return the required value.\n# Impact\nOne function.'],
|
|
37
|
-
['design', 'design.md', '# Design\nUpdate code.cjs and verify the return value.'],
|
|
37
|
+
['design', 'design.md', '# Design\nUpdate code.cjs and verify the return value.\n## Non-Goals\n- Nothing outside code.cjs.\n'],
|
|
38
38
|
['specs', `specs/${capability}/spec.md`, '## ADDED Requirements\n### Requirement: Return the required value\nThe function SHALL return 2.\n#### Scenario: Calling the function\n- **WHEN** code.cjs is loaded\n- **THEN** it returns 2\n'],
|
|
39
39
|
['tasks', 'tasks.md', '- [ ] 1. Implement the return value and verify behavior\n'],
|
|
40
40
|
];
|
|
@@ -22,6 +22,8 @@ declare const keys: {
|
|
|
22
22
|
readonly writes: readonly ['thread_id', 'checkpoint_ns', 'checkpoint_id', 'task_id', 'idx'];
|
|
23
23
|
};
|
|
24
24
|
export type RevisionTable = keyof typeof keys;
|
|
25
|
+
/** Current run database schema. Version 1 lacks the 'pause' control kind and stays readable. */
|
|
26
|
+
export declare const RUN_DATABASE_SCHEMA = 2;
|
|
25
27
|
/** Short synchronous transactions own graph writes and ledger evidence together. */
|
|
26
28
|
export declare class RunDatabase {
|
|
27
29
|
readonly filename: string;
|
|
@@ -34,6 +36,13 @@ export declare class RunDatabase {
|
|
|
34
36
|
create?: boolean;
|
|
35
37
|
readOnly?: boolean;
|
|
36
38
|
}): Promise<RunDatabase>;
|
|
39
|
+
get schemaVersion(): number;
|
|
40
|
+
/**
|
|
41
|
+
* Upgrades a version 1 database in place. Only callers that already proved the
|
|
42
|
+
* run belongs to this Core package call it: a retained older Core must keep
|
|
43
|
+
* opening the databases of its own runs.
|
|
44
|
+
*/
|
|
45
|
+
upgradeSchema(): void;
|
|
37
46
|
get revision(): number;
|
|
38
47
|
get transactionRevision(): number;
|
|
39
48
|
/** Undefined reason is reserved for leases/read reservations, which do not change graph history. */
|
|
@@ -61,7 +61,7 @@ CREATE TABLE interrupts (
|
|
|
61
61
|
attempt_id TEXT NOT NULL REFERENCES attempts(attempt_id), node_path TEXT NOT NULL, scope_id TEXT NOT NULL, branch_id TEXT,
|
|
62
62
|
kind TEXT NOT NULL, requested_at TEXT NOT NULL, payload_json TEXT NOT NULL, answered_at TEXT, answer_json TEXT);
|
|
63
63
|
CREATE TABLE control_inbox (
|
|
64
|
-
request_id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(run_id), kind TEXT NOT NULL CHECK(kind IN ('cancel','steer')),
|
|
64
|
+
request_id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(run_id), kind TEXT NOT NULL CHECK(kind IN ('cancel','steer','pause')),
|
|
65
65
|
payload_json TEXT NOT NULL, created_at TEXT NOT NULL, received_sequence INTEGER NOT NULL DEFAULT 0, consumed_by_attempt_id TEXT, consumed_at TEXT);
|
|
66
66
|
CREATE TABLE role_sessions (
|
|
67
67
|
session_key TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(run_id), scope_id TEXT NOT NULL, node_path TEXT NOT NULL,
|
|
@@ -95,7 +95,22 @@ CREATE INDEX visits_run ON visits(run_id,node_path,scope_id,local_visit);
|
|
|
95
95
|
CREATE INDEX attempts_visit ON attempts(visit_id,attempt);
|
|
96
96
|
CREATE INDEX events_cursor ON events(run_id,sequence);
|
|
97
97
|
CREATE INDEX revisions_cut ON row_versions(revision,table_name);
|
|
98
|
-
PRAGMA user_version=
|
|
98
|
+
PRAGMA user_version=2;
|
|
99
|
+
`;
|
|
100
|
+
/** Current run database schema. Version 1 lacks the 'pause' control kind and stays readable. */
|
|
101
|
+
export const RUN_DATABASE_SCHEMA = 2;
|
|
102
|
+
/**
|
|
103
|
+
* Version 1 → 2 only widens the control_inbox kind CHECK to admit 'pause'.
|
|
104
|
+
* SQLite cannot alter a CHECK constraint, so the table is rebuilt in place.
|
|
105
|
+
*/
|
|
106
|
+
const migrateV1ToV2 = `
|
|
107
|
+
CREATE TABLE control_inbox_v2 (
|
|
108
|
+
request_id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(run_id), kind TEXT NOT NULL CHECK(kind IN ('cancel','steer','pause')),
|
|
109
|
+
payload_json TEXT NOT NULL, created_at TEXT NOT NULL, received_sequence INTEGER NOT NULL DEFAULT 0, consumed_by_attempt_id TEXT, consumed_at TEXT);
|
|
110
|
+
INSERT INTO control_inbox_v2 SELECT request_id,run_id,kind,payload_json,created_at,received_sequence,consumed_by_attempt_id,consumed_at FROM control_inbox;
|
|
111
|
+
DROP TABLE control_inbox;
|
|
112
|
+
ALTER TABLE control_inbox_v2 RENAME TO control_inbox;
|
|
113
|
+
PRAGMA user_version=2;
|
|
99
114
|
`;
|
|
100
115
|
function encodeRow(row) {
|
|
101
116
|
return JSON.stringify(Object.fromEntries(Object.entries(row).map(([key, value]) => [key,
|
|
@@ -137,7 +152,7 @@ export class RunDatabase {
|
|
|
137
152
|
throw error;
|
|
138
153
|
}
|
|
139
154
|
}
|
|
140
|
-
else if (version !== 1)
|
|
155
|
+
else if (version !== 1 && version !== RUN_DATABASE_SCHEMA)
|
|
141
156
|
throw new PersistenceError('resume_incompatible', `Unsupported run database schema ${version}`);
|
|
142
157
|
return new RunDatabase(target, sqlite);
|
|
143
158
|
}
|
|
@@ -146,6 +161,28 @@ export class RunDatabase {
|
|
|
146
161
|
throw error;
|
|
147
162
|
}
|
|
148
163
|
}
|
|
164
|
+
get schemaVersion() { return Number(this.sqlite.prepare('PRAGMA user_version').get()?.user_version); }
|
|
165
|
+
/**
|
|
166
|
+
* Upgrades a version 1 database in place. Only callers that already proved the
|
|
167
|
+
* run belongs to this Core package call it: a retained older Core must keep
|
|
168
|
+
* opening the databases of its own runs.
|
|
169
|
+
*/
|
|
170
|
+
upgradeSchema() {
|
|
171
|
+
if (this.schemaVersion === RUN_DATABASE_SCHEMA)
|
|
172
|
+
return;
|
|
173
|
+
if (this.inTransaction)
|
|
174
|
+
throw new PersistenceError('internal', 'Schema upgrades cannot run inside a transaction');
|
|
175
|
+
this.sqlite.exec('BEGIN IMMEDIATE');
|
|
176
|
+
try {
|
|
177
|
+
if (this.schemaVersion === 1)
|
|
178
|
+
this.sqlite.exec(migrateV1ToV2);
|
|
179
|
+
this.sqlite.exec('COMMIT');
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
this.sqlite.exec('ROLLBACK');
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
149
186
|
get revision() { return Number(this.sqlite.prepare('SELECT COALESCE(MAX(revision),0) AS value FROM durable_revisions').get().value); }
|
|
150
187
|
get transactionRevision() {
|
|
151
188
|
if (this.activeRevision === undefined)
|
|
@@ -33,6 +33,7 @@ export declare class RunLedger {
|
|
|
33
33
|
private event;
|
|
34
34
|
/** Public node namespace differs from the parent saver namespace; never parse opaque namespace strings. */
|
|
35
35
|
private bindPendingTask;
|
|
36
|
+
/** `pause` only at attempt admission: an admitted attempt finishes all of its invocations. */
|
|
36
37
|
private checkAdmission;
|
|
37
38
|
enter(input: NodeAdmission): AttemptFrame;
|
|
38
39
|
resultFor(frame: AttemptFrame): PieceResult | undefined;
|
|
@@ -70,10 +70,15 @@ export class RunLedger {
|
|
|
70
70
|
this.db.put('visits', { ...visit, saver_checkpoint_ns: task.taskCheckpointNs });
|
|
71
71
|
return visit;
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
/** `pause` only at attempt admission: an admitted attempt finishes all of its invocations. */
|
|
74
|
+
checkAdmission(options = {}) {
|
|
74
75
|
const row = this.run(), budget = this.db.get('budget', { run_id: this.runId });
|
|
75
76
|
if (this.db.sqlite.prepare("SELECT 1 FROM control_inbox WHERE run_id=? AND kind='cancel' AND consumed_at IS NULL").get(this.runId))
|
|
76
77
|
throw new EngineError('aborted', 'Cancellation requested');
|
|
78
|
+
// A cooperative pause lets the current attempts settle and refuses the next one.
|
|
79
|
+
const pause = options.pause ? new ControlInbox(this.db, this.runId).pauseRequested() : undefined;
|
|
80
|
+
if (pause)
|
|
81
|
+
throw new EngineError('paused', 'Pause requested', { requestId: pause.requestId, reason: pause.reason });
|
|
77
82
|
if (this.options.failFast && Number(row.consecutive_failures) >= this.options.failFast)
|
|
78
83
|
throw new EngineError('fail_fast', 'Consecutive AI failure limit reached');
|
|
79
84
|
const duration = this.activeDurationMs();
|
|
@@ -112,7 +117,7 @@ export class RunLedger {
|
|
|
112
117
|
this.db.put('runs', { ...this.run(), status: 'running', active_started_at: this.run().active_started_at ?? at });
|
|
113
118
|
return frame;
|
|
114
119
|
}
|
|
115
|
-
this.checkAdmission();
|
|
120
|
+
this.checkAdmission({ pause: true });
|
|
116
121
|
if (prior?.status === 'running' && prior.lease_epoch === this.token.epoch && !prior.inherited)
|
|
117
122
|
throw new EngineError('attempt_running', 'The task is already executing');
|
|
118
123
|
if (prior && (prior.status === 'running' || prior.status === 'interrupted') && input.effect === 'write' && !prior.recovery_authorized)
|
|
@@ -7,4 +7,16 @@ type Flags = Record<string, string | boolean>;
|
|
|
7
7
|
* created before its request was materialized) still resolves to this engine.
|
|
8
8
|
*/
|
|
9
9
|
export declare function isDefinitionCommand(flags: Flags, command: string): boolean;
|
|
10
|
+
/** `failure` (contract 5.8): the structured classification of a failed or blocked definition run; absent otherwise. */
|
|
11
|
+
export declare function definitionFailure(state: {
|
|
12
|
+
status: string;
|
|
13
|
+
steps?: unknown;
|
|
14
|
+
recentFailures?: unknown;
|
|
15
|
+
nextStep?: unknown;
|
|
16
|
+
error?: unknown;
|
|
17
|
+
}): {
|
|
18
|
+
failure: import("../failure-classification.js").FailureClassification;
|
|
19
|
+
} | {
|
|
20
|
+
failure?: undefined;
|
|
21
|
+
};
|
|
10
22
|
export declare function runDefinitionCommand(flags: Flags, positionals: string[], emit: (value: unknown) => void): Promise<number>;
|
|
@@ -12,8 +12,9 @@ import { validateWorkflowDefinition } from './definition-validator.js';
|
|
|
12
12
|
import { configuredRoles } from './preflight.js';
|
|
13
13
|
import { validationPieceRegistry } from './pieces/index.js';
|
|
14
14
|
import { NODE_KINDS_VERSION } from './piece-registry.js';
|
|
15
|
-
import { cancelRun, createRun, definitionRunDirectory, freezeRunRequest, frozenRequestEngine, frozenRequestFile, resumeRun, signalRun } from './runs.js';
|
|
15
|
+
import { cancelRun, createRun, pauseRun, definitionRunDirectory, freezeRunRequest, frozenRequestEngine, frozenRequestFile, resumeRun, signalRun } from './runs.js';
|
|
16
16
|
import { statusRun } from './run-status.js';
|
|
17
|
+
import { classifyFailure } from '../failure-classification.js';
|
|
17
18
|
import { forkRun } from './fork.js';
|
|
18
19
|
export { NODE_KINDS_VERSION };
|
|
19
20
|
const required = (flags, key) => {
|
|
@@ -48,7 +49,7 @@ function directory(flags) {
|
|
|
48
49
|
* created before its request was materialized) still resolves to this engine.
|
|
49
50
|
*/
|
|
50
51
|
export function isDefinitionCommand(flags, command) {
|
|
51
|
-
if (['workflows', 'fork', 'signal', 'cancel'].includes(command) || flags.definition !== undefined && command === 'run')
|
|
52
|
+
if (['workflows', 'fork', 'signal', 'cancel', 'pause'].includes(command) || flags.definition !== undefined && command === 'run')
|
|
52
53
|
return true;
|
|
53
54
|
if (!['resume', 'status'].includes(command))
|
|
54
55
|
return false;
|
|
@@ -63,6 +64,11 @@ export function isDefinitionCommand(flags, command) {
|
|
|
63
64
|
}
|
|
64
65
|
/** Public workflow identity carried by status and results, matching the frozen request block. */
|
|
65
66
|
const workflowIdentity = (workflow) => ({ ...workflow, definitionHash: workflow.version, engine: 2 });
|
|
67
|
+
/** `failure` (contract 5.8): the structured classification of a failed or blocked definition run; absent otherwise. */
|
|
68
|
+
export function definitionFailure(state) {
|
|
69
|
+
const failure = classifyFailure({ status: state.status, error: state.error, steps: state.steps, recentFailures: state.recentFailures, nextStep: state.nextStep });
|
|
70
|
+
return failure ? { failure } : {};
|
|
71
|
+
}
|
|
66
72
|
export async function runDefinitionCommand(flags, positionals, emit) {
|
|
67
73
|
const command = positionals[0];
|
|
68
74
|
if (command === 'workflows') {
|
|
@@ -82,7 +88,7 @@ export async function runDefinitionCommand(flags, positionals, emit) {
|
|
|
82
88
|
}
|
|
83
89
|
if (command === 'status') {
|
|
84
90
|
const status = await statusRun(directory(flags), flags.compact === true);
|
|
85
|
-
emit({ ...status, workflow: workflowIdentity(status.workflow) });
|
|
91
|
+
emit({ ...status, ...definitionFailure(status.state), workflow: workflowIdentity(status.workflow) });
|
|
86
92
|
return 0;
|
|
87
93
|
}
|
|
88
94
|
if (command === 'signal') {
|
|
@@ -93,6 +99,11 @@ export async function runDefinitionCommand(flags, positionals, emit) {
|
|
|
93
99
|
emit({ type: 'runtime-signal-accepted', id: message.id, acceptedAt: message.createdAt });
|
|
94
100
|
return 0;
|
|
95
101
|
}
|
|
102
|
+
if (command === 'pause') {
|
|
103
|
+
// Exit 0 for every outcome: a terminal or already stopped run is a reported no-op.
|
|
104
|
+
emit({ type: 'runtime-pause-result', ...await pauseRun(directory(flags), { requestId: optional(flags, 'request-id'), reason: optional(flags, 'reason') }) });
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
96
107
|
if (command === 'cancel') {
|
|
97
108
|
emit({ type: 'runtime-cancellation-accepted', ...await cancelRun(directory(flags), optional(flags, 'request-id')) });
|
|
98
109
|
return 0;
|
|
@@ -160,7 +171,7 @@ export async function runDefinitionCommand(flags, positionals, emit) {
|
|
|
160
171
|
const current = result.state.usage[key], prior = previous ? previous.state.usage[key] : 0;
|
|
161
172
|
return current === null || prior === null ? null : Math.max(0, current - prior);
|
|
162
173
|
};
|
|
163
|
-
emit({ type: 'runtime-result', engineVersion: 2, ...result.state, revision: result.revision, eventCursor: result.eventCursor, workflow: workflowIdentity(result.workflow), completion: result.completion,
|
|
174
|
+
emit({ type: 'runtime-result', engineVersion: 2, ...result.state, ...definitionFailure({ ...result.state, ...('error' in result ? { error: result.error } : {}) }), revision: result.revision, eventCursor: result.eventCursor, workflow: workflowIdentity(result.workflow), completion: result.completion,
|
|
164
175
|
...('forkOf' in result ? { forkOf: result.forkOf } : {}), ...('error' in result ? { error: result.error } : {}),
|
|
165
176
|
usage: result.state.usage, invocationUsage: { costUsd: delta('costUsd'), inputTokens: delta('inputTokens'), outputTokens: delta('outputTokens') }, metrics: result.metrics, efficiencySummary: result.efficiencySummary });
|
|
166
177
|
return result.state.status === 'succeeded' ? 0 : result.state.status === 'paused' ? 2 : 1;
|
|
@@ -192,7 +192,7 @@ export declare const workflowDefinitionSchema: {
|
|
|
192
192
|
readonly required: readonly ["kind", "params", "ends"];
|
|
193
193
|
readonly properties: {
|
|
194
194
|
readonly kind: {
|
|
195
|
-
readonly enum: readonly ["prompt", "role-turn", "decider", "condition", "assign", "verify", "shell", "openspec-validate", "openspec-archive", "approval", "question", "gate", "map", "join", "component", "implementation", "implementation-step", "artifact-contract", "end"];
|
|
195
|
+
readonly enum: readonly ["prompt", "role-turn", "decider", "condition", "assign", "verify", "shell", "openspec-validate", "openspec-archive", "approval", "question", "gate", "map", "join", "component", "implementation", "implementation-step", "derive-change", "artifact-contract", "end"];
|
|
196
196
|
};
|
|
197
197
|
readonly params: {
|
|
198
198
|
readonly type: "object";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EngineEffect, JsonObject, WorkflowBudget } from './contracts.js';
|
|
2
|
-
export declare const PIECE_KINDS: readonly ['prompt', 'role-turn', 'decider', 'condition', 'assign', 'verify', 'shell', 'artifact-contract', 'openspec-validate', 'openspec-archive', 'approval', 'question', 'gate', 'map', 'join', 'component', 'implementation', 'implementation-step', 'end'];
|
|
2
|
+
export declare const PIECE_KINDS: readonly ['prompt', 'role-turn', 'decider', 'condition', 'assign', 'verify', 'shell', 'artifact-contract', 'openspec-validate', 'openspec-archive', 'approval', 'question', 'gate', 'map', 'join', 'component', 'implementation', 'implementation-step', 'end', 'derive-change'];
|
|
3
3
|
export type PieceKind = typeof PIECE_KINDS[number];
|
|
4
4
|
export interface DefinitionNode {
|
|
5
5
|
kind: PieceKind;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export const PIECE_KINDS = ['prompt', 'role-turn', 'decider', 'condition', 'assign', 'verify', 'shell',
|
|
2
2
|
'artifact-contract', 'openspec-validate', 'openspec-archive', 'approval', 'question', 'gate', 'map', 'join',
|
|
3
|
-
'component', 'implementation', 'implementation-step', 'end'];
|
|
3
|
+
'component', 'implementation', 'implementation-step', 'end', 'derive-change'];
|
|
@@ -116,6 +116,14 @@ export function validateWorkflowDefinition(input, registry, roles = {}, options
|
|
|
116
116
|
add('role_not_found', path + '/params/roleId', 'Used role is not configured');
|
|
117
117
|
if (node.kind === 'decider' && typeof roleId === 'string' && roles[roleId]?.access !== 'read')
|
|
118
118
|
add('invalid_role_access', path + '/params/roleId', 'Decider role must be read-only');
|
|
119
|
+
// A correction round edits code. Portable (structural) validation cannot know an unresolved role's access; execution validates again.
|
|
120
|
+
if (node.kind === 'role-turn' && node.params.stance === 'fixer' && typeof roleId === 'string' && Object.hasOwn(roles, roleId) && !unresolved.includes(roleId) && roles[roleId]?.access !== 'write')
|
|
121
|
+
add('invalid_role_access', path + '/params/stance', 'The fixer stance requires a role with write access');
|
|
122
|
+
}
|
|
123
|
+
// derive-change always invokes Core's engine role `deriver`.
|
|
124
|
+
if (node.kind === 'derive-change') {
|
|
125
|
+
if (!definition.roles.includes('deriver'))
|
|
126
|
+
add('role_undeclared', path, 'derive-change requires the deriver role in definition.roles');
|
|
119
127
|
}
|
|
120
128
|
const effect = effectOf(node);
|
|
121
129
|
writes ||= effect === 'write';
|
|
@@ -33,6 +33,13 @@ export declare function definitionEfficiencySummary(ledger: RunLedger, state: Wo
|
|
|
33
33
|
fullContexts: number;
|
|
34
34
|
incrementalContexts: number;
|
|
35
35
|
};
|
|
36
|
+
baseline: {
|
|
37
|
+
invocations: number;
|
|
38
|
+
roles: string[];
|
|
39
|
+
costUsd: number | null;
|
|
40
|
+
inputTokens: number | null;
|
|
41
|
+
outputTokens: number | null;
|
|
42
|
+
};
|
|
36
43
|
escalations: {
|
|
37
44
|
role: string;
|
|
38
45
|
attemptId: string;
|