specrails-core 6.1.0 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/agent-runtime/change-scope.js +2 -0
- package/dist/agent-runtime/cli-executor.js +2 -1
- package/dist/agent-runtime/cli.js +1 -1
- package/dist/agent-runtime/engine/checkpoint/ledger.js +2 -2
- package/dist/agent-runtime/engine/cli.js +1 -1
- package/dist/agent-runtime/engine/compiler.js +9 -8
- package/dist/agent-runtime/engine/definition-schema.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-schema.js +2 -0
- package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-types.js +2 -2
- package/dist/agent-runtime/engine/definition-validator.js +6 -2
- package/dist/agent-runtime/engine/expressions.d.ts +1 -1
- package/dist/agent-runtime/engine/expressions.js +16 -3
- package/dist/agent-runtime/engine/fork.js +51 -9
- 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-dependencies.d.ts +6 -2
- package/dist/agent-runtime/engine/piece-dependencies.js +54 -7
- 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 +4 -0
- package/dist/agent-runtime/engine/pieces/artifact-contract.js +70 -0
- package/dist/agent-runtime/engine/pieces/control.js +7 -3
- package/dist/agent-runtime/engine/pieces/implementation-binding.d.ts +1 -0
- package/dist/agent-runtime/engine/pieces/implementation-step.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/implementation-step.js +47 -0
- package/dist/agent-runtime/engine/pieces/implementation.d.ts +5 -1
- package/dist/agent-runtime/engine/pieces/implementation.js +8 -7
- package/dist/agent-runtime/engine/pieces/index.js +3 -1
- package/dist/agent-runtime/engine/pieces/openspec.js +8 -2
- package/dist/agent-runtime/engine/pieces/ports.d.ts +6 -1
- package/dist/agent-runtime/engine/pieces/prompt.js +2 -2
- package/dist/agent-runtime/engine/pieces/role-turn.js +1 -1
- package/dist/agent-runtime/engine/pieces/verify.js +53 -3
- package/dist/agent-runtime/graph/roles.d.ts +2 -0
- package/dist/agent-runtime/graph/roles.js +2 -0
- package/dist/agent-runtime/prompts.js +1 -1
- package/dist/pipeline/pipeline-state.js +2 -0
- package/integration-contract.json +32 -12
- package/package.json +2 -2
- package/schemas/workflow-definition.schema.json +2 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { artifactPath } from '../../openspec.js';
|
|
3
|
+
import { contentDigest } from '../canonical-json.js';
|
|
4
|
+
import { EngineError } from '../contracts.js';
|
|
5
|
+
import { idSchema, paramsSchema, text } from './shared.js';
|
|
6
|
+
/** A reusable artifact invariant, with no agent roles, phase order or successor policy. */
|
|
7
|
+
export function artifactContractPiece(bindings) {
|
|
8
|
+
return {
|
|
9
|
+
descriptor: { kind: 'artifact-contract', paramsSchema: paramsSchema({
|
|
10
|
+
change: { type: 'string', minLength: 1, maxLength: 128 }, contractId: idSchema, action: { enum: ['freeze', 'check'] },
|
|
11
|
+
requireCompletedTasks: { type: 'boolean' },
|
|
12
|
+
}, ['change', 'contractId', 'action']), outcomes: ['pass', 'fail', 'failed'], effect: 'read', requiresAI: false },
|
|
13
|
+
async execute(params, context) {
|
|
14
|
+
const deps = bindings(), change = text(params.change);
|
|
15
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(change) || change.length > 64)
|
|
16
|
+
throw new EngineError('invalid_arguments', 'Invalid change identifier');
|
|
17
|
+
const store = deps.artifactContracts?.(context);
|
|
18
|
+
if (!store)
|
|
19
|
+
throw new EngineError('artifact_contract_unavailable', 'Scoped artifact contracts are not bound');
|
|
20
|
+
const directory = 'openspec/changes/' + change;
|
|
21
|
+
const files = {};
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
const read = (relative) => {
|
|
24
|
+
const source = readFileSync(artifactPath(deps.context.artifactRoot, directory + '/' + relative), 'utf8');
|
|
25
|
+
bytes += Buffer.byteLength(source);
|
|
26
|
+
if (bytes > 2 * 1024 * 1024 || Object.keys(files).length >= 200)
|
|
27
|
+
throw new EngineError('output_limit', 'Artifact contract exceeds its read limit');
|
|
28
|
+
return source;
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
for (const name of ['proposal.md', 'design.md', 'tasks.md'])
|
|
32
|
+
files[name] = read(name);
|
|
33
|
+
const walk = (relative) => {
|
|
34
|
+
for (const entry of readdirSync(artifactPath(deps.context.artifactRoot, directory + '/' + relative), { withFileTypes: true })) {
|
|
35
|
+
const child = relative + '/' + entry.name;
|
|
36
|
+
if (entry.isSymbolicLink())
|
|
37
|
+
throw new EngineError('artifact_scope_mismatch', 'Artifact contracts reject symlinks');
|
|
38
|
+
if (entry.isDirectory())
|
|
39
|
+
walk(child);
|
|
40
|
+
else if (entry.isFile() && entry.name.endsWith('.md'))
|
|
41
|
+
files[child] = read(child);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
walk('specs');
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error instanceof EngineError)
|
|
48
|
+
throw error;
|
|
49
|
+
return { outcome: 'fail', output: { reason: 'required_artifacts_missing' } };
|
|
50
|
+
}
|
|
51
|
+
if (!Object.keys(files).some(name => name.startsWith('specs/')) || Object.values(files).some(source => !source.trim()))
|
|
52
|
+
return { outcome: 'fail', output: { reason: 'empty_artifacts' } };
|
|
53
|
+
const tasks = [...files['tasks.md'].matchAll(/^\s*[-*]\s+\[([ xX])\]\s+(.+)$/gm)];
|
|
54
|
+
if (!tasks.length)
|
|
55
|
+
return { outcome: 'fail', output: { reason: 'tasks_missing' } };
|
|
56
|
+
// Completion may change checkboxes, never the approved task descriptions.
|
|
57
|
+
files['tasks.md'] = files['tasks.md'].replace(/^(\s*[-*]\s+\[)[ xX](\]\s+)/gm, '$1 $2');
|
|
58
|
+
const fingerprint = contentDigest(files), key = text(params.contractId);
|
|
59
|
+
if (params.action === 'freeze')
|
|
60
|
+
store.set(key, { fingerprint });
|
|
61
|
+
else {
|
|
62
|
+
const frozen = store.get(key);
|
|
63
|
+
if (!frozen || frozen.fingerprint !== fingerprint)
|
|
64
|
+
return { outcome: 'fail', output: { reason: frozen ? 'artifacts_changed' : 'contract_missing' } };
|
|
65
|
+
}
|
|
66
|
+
const incomplete = tasks.filter(task => task[1] === ' ').length;
|
|
67
|
+
return { outcome: params.requireCompletedTasks && incomplete ? 'fail' : 'pass', output: { fingerprint, tasks: tasks.length, incomplete } };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -27,10 +27,14 @@ export function controlPieces(bindings) {
|
|
|
27
27
|
for (const kind of ['approval', 'question', 'gate']) {
|
|
28
28
|
const field = kind === 'question' ? 'text' : 'reason';
|
|
29
29
|
pieces.push({
|
|
30
|
-
descriptor: { kind, paramsSchema: paramsSchema({ [field]: { ...stringSchema, minLength: 1 } }, [field]), outcomes: ['next'], effect: 'read', requiresAI: false },
|
|
30
|
+
descriptor: { kind, paramsSchema: paramsSchema({ [field]: { ...stringSchema, minLength: 1 }, bindCandidate: { type: 'boolean' }, enabled: { type: 'boolean' } }, [field]), outcomes: ['next'], effect: 'read', requiresAI: false },
|
|
31
31
|
async execute(params, context) {
|
|
32
|
-
const
|
|
33
|
-
|
|
32
|
+
const memo = bindings().memo(context);
|
|
33
|
+
const bound = params.bindCandidate ? memo.get('approval-candidate') ?? bindings().executionSnapshot(context).candidate?.hash ?? null : undefined;
|
|
34
|
+
if (params.bindCandidate && memo.get('approval-candidate') === undefined)
|
|
35
|
+
memo.set('approval-candidate', bound);
|
|
36
|
+
const response = kind === 'approval' && params.enabled === false ? { approved: true } : context.interrupt({ kind, prompt: text(params[field]), nodePath: context.frame.nodePath, scopeId: context.frame.scope.id, attemptId: context.frame.attemptId });
|
|
37
|
+
return { outcome: 'next', output: { response, ...(params.bindCandidate ? { candidateHash: bound } : {}) }, ...(kind === 'question' ? { answers: [answerEntry(context, response)] } : {}) };
|
|
34
38
|
},
|
|
35
39
|
});
|
|
36
40
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type PipelineContext } from '../../../pipeline/pipeline-state.js';
|
|
2
2
|
import { type PieceExecutionContext } from '../contracts.js';
|
|
3
3
|
export interface ImplementationBinding {
|
|
4
|
+
shared?: true;
|
|
4
5
|
context: PipelineContext;
|
|
5
6
|
change: string;
|
|
6
7
|
parentRunId: string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Piece } from '../contracts.js';
|
|
2
|
+
import type { PieceDependencyProvider } from './ports.js';
|
|
3
|
+
/** Outcomes are facts, not destinations. Only the host definition chooses the next node. */
|
|
4
|
+
export declare const IMPLEMENTATION_STEP_OUTCOMES: readonly ["next", "incomplete", "rejected", "replan", "reverify", "rereview", "failed"];
|
|
5
|
+
export declare function implementationStepPiece(bindings: PieceDependencyProvider): Piece;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CoreState } from '../../graph/state.js';
|
|
2
|
+
import { EngineError } from '../contracts.js';
|
|
3
|
+
import { createImplementationAdapter } from './implementation.js';
|
|
4
|
+
import { json, paramsSchema } from './shared.js';
|
|
5
|
+
/** Outcomes are facts, not destinations. Only the host definition chooses the next node. */
|
|
6
|
+
export const IMPLEMENTATION_STEP_OUTCOMES = ['next', 'incomplete', 'rejected', 'replan', 'reverify', 'rereview', 'failed'];
|
|
7
|
+
export function implementationStepPiece(bindings) {
|
|
8
|
+
return {
|
|
9
|
+
descriptor: { kind: 'implementation-step', paramsSchema: paramsSchema({
|
|
10
|
+
phase: { enum: ['architect', 'developer', 'fixer', 'verify', 'reviewer', 'archive'] },
|
|
11
|
+
}, ['phase']), outcomes: [...IMPLEMENTATION_STEP_OUTCOMES], effect: 'write', requiresAI: true },
|
|
12
|
+
async execute(params, context) {
|
|
13
|
+
const deps = bindings(), phase = params.phase;
|
|
14
|
+
const change = deps.change ?? context.state.$vars.changeId;
|
|
15
|
+
if (typeof change !== 'string')
|
|
16
|
+
throw new EngineError('invalid_arguments', 'Implementation operation requires a frozen change');
|
|
17
|
+
const saved = deps.operationState?.(context);
|
|
18
|
+
const resumed = [...deps.stepContext(context).checkpoint.events].reverse().find(event => event.type === 'workflow_resumed')?.timestamp ?? '';
|
|
19
|
+
const adapter = createImplementationAdapter(deps, { change, params: {}, independent: true, revalidate: saved?.operationResume !== resumed, archiveConsent: saved?.operationRunId === deps.context.runId && typeof saved.operationConsent === 'string' ? saved.operationConsent : undefined });
|
|
20
|
+
const initialized = await adapter.initialize(context);
|
|
21
|
+
const initial = { plan: [], unverifiedRepositories: [], architecture: null, development: null,
|
|
22
|
+
verifyResult: null, review: null, answers: [], deepenPasses: 0, archived: null, verifyHistory: [] };
|
|
23
|
+
const state = { ...initial, ...saved?.operationState,
|
|
24
|
+
...(saved?.operationRunId === deps.context.runId ? {} : initialized) };
|
|
25
|
+
const response = await adapter.runNode(phase, state, context);
|
|
26
|
+
const updated = { ...state };
|
|
27
|
+
for (const [key, value] of Object.entries(response.update ?? {})) {
|
|
28
|
+
const channel = CoreState.spec[key];
|
|
29
|
+
updated[key] = channel.operator ? channel.operator(state[key], value) : value;
|
|
30
|
+
}
|
|
31
|
+
const normal = { architect: 'developer', developer: 'verify', fixer: 'verify', verify: 'reviewer', reviewer: 'archive', archive: null }[phase];
|
|
32
|
+
const next = response.next === undefined ? normal : response.next;
|
|
33
|
+
const outcome = response.status !== 'succeeded' ? 'failed' : next === normal ? 'next'
|
|
34
|
+
: next === 'architect' ? 'replan' : next === 'verify' ? 'reverify' : next === 'reviewer' ? 'rereview'
|
|
35
|
+
: next === 'developer' ? 'incomplete' : next === 'fixer' ? 'rejected' : 'failed';
|
|
36
|
+
// The summary reads host verification receipts and acceptance, never an agent's claim.
|
|
37
|
+
const evidence = await adapter.summarize(updated, context);
|
|
38
|
+
return { outcome, status: response.status,
|
|
39
|
+
...(response.output === undefined ? {} : { output: json({ ...((response.output && typeof response.output === 'object' && !Array.isArray(response.output)) ? response.output : { value: response.output }), phase }) }),
|
|
40
|
+
...(response.error ? { error: { code: response.status === 'blocked' ? 'implementation_blocked' : 'implementation_failed', message: response.error } } : {}),
|
|
41
|
+
childUpdate: { operationState: json(updated), operationRunId: deps.context.runId, operationResume: resumed, operationConsent: adapter.consentSnapshot?.(context) ?? null, journal: json(adapter.snapshot(context)) },
|
|
42
|
+
...(evidence.receipt ? { receipt: evidence.receipt, verified: phase === 'archive' && outcome === 'next' ? evidence.verified : null } : {}),
|
|
43
|
+
...(phase === 'archive' && outcome === 'next' ? { completion: evidence.completion } : {}),
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -11,13 +11,17 @@ export interface ImplementationAdapter {
|
|
|
11
11
|
initialize(context: PieceExecutionContext): Promise<CoreStateUpdate>;
|
|
12
12
|
runNode(id: CoreNodeId, state: CoreStateType, context: PieceExecutionContext): Promise<NodeResult<CoreStateType>>;
|
|
13
13
|
snapshot(context: PieceExecutionContext): ImplementationJournalSnapshot;
|
|
14
|
+
consentSnapshot?(context: PieceExecutionContext): string | undefined;
|
|
14
15
|
summarize(state: CoreStateType, context: PieceExecutionContext): Promise<PieceResult>;
|
|
15
16
|
validateCompleted(id: CoreNodeId, record: StepRecord, checkpoint: WorkflowState, context: PieceExecutionContext): Promise<boolean>;
|
|
16
17
|
}
|
|
17
18
|
/** Scope the legacy convergence/attempt view without changing durable event attribution. */
|
|
18
|
-
export declare function implementationStepContext(deps: PieceDependencies, context: PieceExecutionContext, id: CoreNodeId): WorkflowStepContext;
|
|
19
|
+
export declare function implementationStepContext(deps: PieceDependencies, context: PieceExecutionContext, id: CoreNodeId, independent?: boolean): WorkflowStepContext;
|
|
19
20
|
/** Reuse every legacy node and quality gate; this factory never runs the legacy JSON workflow host. */
|
|
20
21
|
export declare function createImplementationAdapter(deps: PieceDependencies, input: {
|
|
21
22
|
change: string;
|
|
22
23
|
params: JsonObject;
|
|
24
|
+
independent?: boolean;
|
|
25
|
+
revalidate?: boolean;
|
|
26
|
+
archiveConsent?: string;
|
|
23
27
|
}): ImplementationAdapter;
|
|
@@ -15,9 +15,9 @@ import { json } from './shared.js';
|
|
|
15
15
|
import { receiptEvidence } from './verify.js';
|
|
16
16
|
import { captureImplementationJournal } from './implementation-journal.js';
|
|
17
17
|
/** Scope the legacy convergence/attempt view without changing durable event attribution. */
|
|
18
|
-
export function implementationStepContext(deps, context, id) {
|
|
18
|
+
export function implementationStepContext(deps, context, id, independent = false) {
|
|
19
19
|
const original = deps.stepContext(context);
|
|
20
|
-
const prefix = context.frame.nodePath.slice(0, -id.length);
|
|
20
|
+
const prefix = independent ? '' : context.frame.nodePath.slice(0, -id.length);
|
|
21
21
|
const local = (value) => value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
|
22
22
|
const belongs = (value) => CORE_NODE_ORDER.includes(value) || (value.startsWith(prefix) && !value.slice(prefix.length).includes('/'));
|
|
23
23
|
const checkpoint = original.checkpoint;
|
|
@@ -46,7 +46,7 @@ export function createImplementationAdapter(deps, input) {
|
|
|
46
46
|
...(params.reviewPolicy ? { review: params.reviewPolicy } : {}) };
|
|
47
47
|
const attempts = params.attempts ?? config.limits?.maxAttempts ?? 3;
|
|
48
48
|
const bindings = new Map();
|
|
49
|
-
const instanceKey = (context) => context.frame.scope.id + '/' + context.frame.visitId;
|
|
49
|
+
const instanceKey = (context) => input.independent ? context.frame.scope.id : context.frame.scope.id + '/' + context.frame.visitId;
|
|
50
50
|
const metadata = { context: deps.context, config, change, attempts, policy: resolveReviewPolicy(config), openspec: {}, focusedCorrectionEvidence: true,
|
|
51
51
|
invoke: (() => { throw new EngineError('implementation_not_initialized', 'Implementation metadata nodes must run through the durable adapter'); }), note() { } };
|
|
52
52
|
const refresh = (binding, context) => {
|
|
@@ -64,7 +64,7 @@ export function createImplementationAdapter(deps, input) {
|
|
|
64
64
|
schema: CoreState, nodes: coreNodes(metadata), entry: 'architect',
|
|
65
65
|
async initialize(context) {
|
|
66
66
|
context.signal.throwIfAborted();
|
|
67
|
-
const binding = deps.bindImplementation(context, change);
|
|
67
|
+
const binding = deps.bindImplementation(context, change, input.independent);
|
|
68
68
|
const pipeline = binding.context;
|
|
69
69
|
const state = initializePipeline(pipeline, binding.change);
|
|
70
70
|
refresh(binding, context);
|
|
@@ -81,7 +81,7 @@ export function createImplementationAdapter(deps, input) {
|
|
|
81
81
|
}));
|
|
82
82
|
for (const role of Object.keys(openspec))
|
|
83
83
|
await deps.registry.get(resolveRoleDescriptor(config, role).provider).validateOpenSpec?.(openspec[role]);
|
|
84
|
-
bindings.set(instanceKey(context), { binding, openspec, resumeChecked: false });
|
|
84
|
+
bindings.set(instanceKey(context), { binding, openspec, resumeChecked: input.independent === true && input.revalidate === false, archiveConsent: input.archiveConsent });
|
|
85
85
|
if (existsSync(path.join(directory, 'implementation-fork.json'))) {
|
|
86
86
|
const plan = readVerificationPlan(pipeline);
|
|
87
87
|
return { ...(plan ? { plan: expandedPlanCommands(pipeline, plan) } : {}), verifyResult: null, review: null, archived: null, verifyHistory: [] };
|
|
@@ -94,7 +94,7 @@ export function createImplementationAdapter(deps, input) {
|
|
|
94
94
|
throw new EngineError('implementation_not_initialized', 'Initialize this implementation scope before executing its child graph');
|
|
95
95
|
const { binding, openspec } = prepared;
|
|
96
96
|
refresh(binding, context);
|
|
97
|
-
const step = implementationStepContext(deps, context, id);
|
|
97
|
+
const step = implementationStepContext(deps, context, id, input.independent);
|
|
98
98
|
if (!prepared.resumeChecked && (step.pending || step.checkpoint.events.some(event => event.type === 'workflow_resumed'))) {
|
|
99
99
|
// Collect archive consent first, then revalidate completed verification
|
|
100
100
|
// and review exactly as the legacy host does on explicit resume.
|
|
@@ -120,7 +120,7 @@ export function createImplementationAdapter(deps, input) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
const invoke = createRoleInvoker({ context: binding.context, config, registry: deps.registry, openspec, roleState: deps.roleState(context),
|
|
123
|
+
const invoke = createRoleInvoker({ context: binding.context, config, registry: deps.registry, openspec, roleState: deps.roleState(context), propagateInterruptions: input.independent,
|
|
124
124
|
onAgentEvent: (role, event) => context.progress({ type: 'agent-event', payload: json({ role, event }) }) });
|
|
125
125
|
const localConfig = { ...config, verification: config.verification.filter(command => binding.context.repositories.some(repository => repository.id === command.repositoryId)) };
|
|
126
126
|
const nodes = coreNodes({ ...metadata, context: binding.context, config: localConfig, change: binding.change, openspec, invoke,
|
|
@@ -130,6 +130,7 @@ export function createImplementationAdapter(deps, input) {
|
|
|
130
130
|
});
|
|
131
131
|
return nodes[id].run(state, step);
|
|
132
132
|
},
|
|
133
|
+
consentSnapshot(context) { return bindings.get(instanceKey(context))?.archiveConsent; },
|
|
133
134
|
snapshot(context) {
|
|
134
135
|
const prepared = bindings.get(context.frame.scope.id) ?? bindings.get(instanceKey(context));
|
|
135
136
|
if (!prepared)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { artifactContractPiece } from './artifact-contract.js';
|
|
1
2
|
import { PieceRegistry } from '../piece-registry.js';
|
|
2
3
|
import { controlPieces, compositionPieces } from './control.js';
|
|
3
4
|
import { deciderPiece } from './decider.js';
|
|
@@ -7,6 +8,7 @@ import { promptPiece } from './prompt.js';
|
|
|
7
8
|
import { roleTurnPiece } from './role-turn.js';
|
|
8
9
|
import { shellPiece } from './shell.js';
|
|
9
10
|
import { verifyPiece } from './verify.js';
|
|
11
|
+
import { implementationStepPiece } from './implementation-step.js';
|
|
10
12
|
import { assignPiece } from './assign.js';
|
|
11
13
|
/** The composition root binds effects once; definitions can only select this reviewed catalog. */
|
|
12
14
|
export function createPieceRegistry(deps) {
|
|
@@ -18,5 +20,5 @@ export function validationPieceRegistry() {
|
|
|
18
20
|
}
|
|
19
21
|
function registry(bindings) {
|
|
20
22
|
return new PieceRegistry([promptPiece(bindings), roleTurnPiece(bindings), deciderPiece(bindings), verifyPiece(bindings), shellPiece(bindings),
|
|
21
|
-
...openSpecPieces(bindings), ...controlPieces(bindings), assignPiece(), ...compositionPieces()]);
|
|
23
|
+
artifactContractPiece(bindings), ...openSpecPieces(bindings), ...controlPieces(bindings), assignPiece(), implementationStepPiece(bindings), ...compositionPieces()]);
|
|
22
24
|
}
|
|
@@ -42,12 +42,18 @@ export function openSpecPieces(bindings) {
|
|
|
42
42
|
return { outcome: exited && item.valid ? 'pass' : 'fail', output: json(report) };
|
|
43
43
|
},
|
|
44
44
|
}, {
|
|
45
|
-
descriptor: { kind: 'openspec-archive', paramsSchema: schema, outcomes: ['next', 'failed'], effect: 'write', requiresAI: false },
|
|
45
|
+
descriptor: { kind: 'openspec-archive', paramsSchema: { ...schema, properties: { ...schema.properties, requiresVerified: { type: 'boolean' }, reviewedCandidate: { type: 'string' }, approvedCandidate: { type: 'string' } } }, outcomes: ['next', 'failed'], effect: 'write', requiresAI: false },
|
|
46
46
|
async execute(params, context) {
|
|
47
47
|
const deps = bindings();
|
|
48
48
|
if (params.repositoryId !== undefined && params.repositoryId !== deps.context.artifactRepositoryId)
|
|
49
49
|
throw new EngineError('artifact_scope_mismatch', 'OpenSpec repository must match the frozen artifact repository');
|
|
50
|
-
const
|
|
50
|
+
const snapshot = deps.executionSnapshot(context);
|
|
51
|
+
const verified = snapshot.verified ?? deps.verifiedBeforeWrite?.(context);
|
|
52
|
+
if (params.requiresVerified && (!snapshot.candidate || !verified || verified.candidateHash !== snapshot.candidate.hash))
|
|
53
|
+
return { outcome: 'failed', output: { reason: 'unverified' } };
|
|
54
|
+
if ([params.reviewedCandidate, params.approvedCandidate].some(hash => hash !== undefined && hash !== snapshot.candidate?.hash))
|
|
55
|
+
return { outcome: 'failed', output: { reason: 'candidate_changed' } };
|
|
56
|
+
const change = changeId(text(params.change)), initial = snapshot.candidate?.hash;
|
|
51
57
|
const archived = params.allowArchived === true ? archivedOpenSpecChange(deps.context, change) : undefined;
|
|
52
58
|
if (archived)
|
|
53
59
|
return { outcome: 'next', output: { change, archived, skipped: true } };
|
|
@@ -15,15 +15,18 @@ export interface PieceStatePort {
|
|
|
15
15
|
}
|
|
16
16
|
export interface PieceDependencies {
|
|
17
17
|
context: PipelineContext;
|
|
18
|
+
readonly change?: string;
|
|
18
19
|
config: RuntimeConfig;
|
|
19
20
|
registry: ExecutorRegistry;
|
|
20
21
|
stepContext(context: PieceExecutionContext): WorkflowStepContext;
|
|
21
22
|
roleState(context: PieceExecutionContext): RoleStatePort;
|
|
23
|
+
artifactContracts?(context: PieceExecutionContext): PieceStatePort;
|
|
22
24
|
memo(context: PieceExecutionContext): PieceStatePort;
|
|
23
25
|
/** Narrow project memory is restricted by the declared piece and namespace capability. */
|
|
24
26
|
memory(context: PieceExecutionContext): ProjectMemory;
|
|
25
27
|
/** Provider settlement and the response needed for interrupt replay share one transaction. */
|
|
26
28
|
settleResult(context: PieceExecutionContext, key: string, value: JsonValue, invocation: ProviderInvocation): void;
|
|
29
|
+
verifiedBeforeWrite?(context: PieceExecutionContext): VerifiedState | null;
|
|
27
30
|
executionSnapshot(context: PieceExecutionContext): {
|
|
28
31
|
candidate: CandidateState | null;
|
|
29
32
|
verified: VerifiedState | null;
|
|
@@ -32,13 +35,15 @@ export interface PieceDependencies {
|
|
|
32
35
|
/** A scoped adapter directory for recoverable OpenSpec write sets, never state.json. */
|
|
33
36
|
artifactDirectory(context: PieceExecutionContext): string;
|
|
34
37
|
/** Registers parent-owned scoped journals for candidate inspection, recovery and fork. */
|
|
35
|
-
bindImplementation(context: PieceExecutionContext, change: string): ImplementationBinding;
|
|
38
|
+
bindImplementation(context: PieceExecutionContext, change: string, shared?: boolean): ImplementationBinding;
|
|
36
39
|
/** Current union of declared implementation artifacts and journals, including the parent run. */
|
|
37
40
|
implementationExclusions?(context: PieceExecutionContext): {
|
|
38
41
|
runtimeExclusions: readonly string[];
|
|
39
42
|
repositoryExclusions: Readonly<Record<string, readonly string[]>>;
|
|
40
43
|
};
|
|
41
44
|
openspec?(context: PieceExecutionContext): Record<string, OpenSpecRoleContext>;
|
|
45
|
+
/** Committed private operation state, inaccessible to workflow variables or fork patches. */
|
|
46
|
+
operationState?(context: PieceExecutionContext): JsonValue | undefined;
|
|
42
47
|
policies?: {
|
|
43
48
|
historyMaxChars?: number;
|
|
44
49
|
noProgress?: number;
|
|
@@ -5,7 +5,7 @@ import { contentDigest } from '../canonical-json.js';
|
|
|
5
5
|
import { EngineError } from '../contracts.js';
|
|
6
6
|
import { answerEntry, boundedText, captures, captureSchema, historyEntry, json, paramsSchema, invocationTimers, stringSchema, text } from './shared.js';
|
|
7
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']);
|
|
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'] }, maxTurns: { type: 'integer', minimum: 1 } }, ['provider']);
|
|
9
9
|
const promptSchema = {
|
|
10
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
11
|
access: { enum: ['read', 'write'] }, sentinel: { enum: ['verification', 'blocked', 'none'] }, captureVars: captureSchema,
|
|
@@ -58,7 +58,7 @@ export function promptPiece(bindings) {
|
|
|
58
58
|
prompt: native ? '' : text(params.text) + suffix,
|
|
59
59
|
...(native ? { nativeCommand: { id: native.id, args: (native.args ?? '') + suffix } } : {}),
|
|
60
60
|
cwd: deps.context.artifactRoot, allowedRoots: deps.context.repositories.map(repo => repo.path),
|
|
61
|
-
model: engine.model, effort: engine.effort, thinking: engine.thinking,
|
|
61
|
+
model: engine.model, effort: engine.effort, thinking: engine.thinking, maxTurns: engine.maxTurns,
|
|
62
62
|
timeoutMs: params.timeoutMs ?? deps.config.limits?.timeoutMs,
|
|
63
63
|
idleTimeoutMs: params.idleTimeoutMs ?? deps.config.limits?.idleTimeoutMs,
|
|
64
64
|
signal: context.signal, resumeSessionId: sessionId,
|
|
@@ -68,6 +68,6 @@ export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
|
68
68
|
await memory.put(['review', 'notes'], finalKey, { roleId, candidateHash: before?.hash ?? null,
|
|
69
69
|
text: boundedText(result.text, 4000), lastAttemptId: context.frame.attemptId });
|
|
70
70
|
});
|
|
71
|
-
return { outcome: 'next', output: { text: boundedText(result.text), ...(result.value ? { structured: json(result.value) } : {}) },
|
|
71
|
+
return { outcome: 'next', output: { candidateHash: deps.executionSnapshot(context).candidate?.hash ?? null, text: boundedText(result.text), ...(result.value ? { structured: json(result.value) } : {}) },
|
|
72
72
|
history: [historyEntry(context, result.text)], ...(params.sessionContinuity !== 'none' && session ? { session: { sessionId: session.sessionId, identity: session.identity } } : {}) };
|
|
73
73
|
}
|
|
@@ -2,7 +2,39 @@ import { contentDigest } from '../canonical-json.js';
|
|
|
2
2
|
import { advisoryMemory } from './project-memory.js';
|
|
3
3
|
import { executeVerification, validateVerificationRequest } from '../../../pipeline/pipeline-state.js';
|
|
4
4
|
import { withScopeDefault } from '../../change-scope.js';
|
|
5
|
-
import { json, paramsSchema, positiveInteger } from './shared.js';
|
|
5
|
+
import { boundedText, json, paramsSchema, positiveInteger } from './shared.js';
|
|
6
|
+
/** Keep real subprocess diagnostics in committed outputs for subsequent agents.
|
|
7
|
+
* The full command evidence remains in the receipt; prioritize failed checks
|
|
8
|
+
* when the output budget is exhausted. */
|
|
9
|
+
function verificationDiagnostics(receipt) {
|
|
10
|
+
let remaining = 32_000;
|
|
11
|
+
const outputs = new Map();
|
|
12
|
+
const priority = receipt.commands.map((command, index) => ({ command, index }))
|
|
13
|
+
.sort((a, b) => Number(b.command.exitCode !== 0) - Number(a.command.exitCode !== 0));
|
|
14
|
+
for (const { command, index } of priority) {
|
|
15
|
+
const output = command.output.slice(-Math.min(8_000, remaining));
|
|
16
|
+
// slice(-0) would include all the original text.
|
|
17
|
+
outputs.set(index, remaining > 0 ? output : '');
|
|
18
|
+
remaining -= remaining > 0 ? output.length : 0;
|
|
19
|
+
}
|
|
20
|
+
let budget = 64_000;
|
|
21
|
+
const commands = [];
|
|
22
|
+
for (const { index, command } of priority) {
|
|
23
|
+
const output = outputs.get(index) ?? '';
|
|
24
|
+
const args = command.args.slice(0, 16).map(arg => boundedText(arg, 128));
|
|
25
|
+
const diagnostic = { repositoryId: command.repositoryId, command: boundedText(command.command, 512),
|
|
26
|
+
args, cwd: boundedText(command.cwd, 512),
|
|
27
|
+
exitCode: command.exitCode, durationMs: command.durationMs, output,
|
|
28
|
+
truncated: output.length < command.output.length || command.outputTruncated === true
|
|
29
|
+
|| command.command.length > 512 || command.cwd.length > 512 || command.args.length > 16 || command.args.some(arg => arg.length > 128) };
|
|
30
|
+
const size = JSON.stringify(diagnostic).length;
|
|
31
|
+
if (size > budget)
|
|
32
|
+
break;
|
|
33
|
+
budget -= size;
|
|
34
|
+
commands.push(diagnostic);
|
|
35
|
+
}
|
|
36
|
+
return { commands, ...(commands.length < receipt.commands.length ? { omittedCommands: receipt.commands.length - commands.length } : {}) };
|
|
37
|
+
}
|
|
6
38
|
export const verificationCommandSchema = paramsSchema({
|
|
7
39
|
repositoryId: { type: 'string', minLength: 1, maxLength: 128 }, command: { type: 'string', minLength: 1, maxLength: 4096 },
|
|
8
40
|
args: { type: 'array', maxItems: 1024, items: { type: 'string', maxLength: 32_000 } }, cwd: { type: 'string', maxLength: 4096 },
|
|
@@ -19,10 +51,28 @@ export function verificationDeadline(deps, context) {
|
|
|
19
51
|
export function verifyPiece(bindings) {
|
|
20
52
|
return {
|
|
21
53
|
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' },
|
|
54
|
+
additionalCommandsFrom: { type: 'string', minLength: 1, maxLength: 128 }, unverified: { type: 'boolean' }, maxConcurrency: { type: 'integer', minimum: 1, maximum: 4 } }, ['commands']), outcomes: ['pass', 'fail', 'failed'], effect: 'write', requiresAI: false, storeAccess: 'write' },
|
|
23
55
|
async execute(params, context) {
|
|
24
56
|
const deps = bindings();
|
|
25
57
|
const commands = (params.commands === 'configured' ? deps.config.verification : params.commands).map(command => withScopeDefault(deps.context, command));
|
|
58
|
+
const hostRepositories = new Set(commands.map(command => command.repositoryId));
|
|
59
|
+
if (typeof params.additionalCommandsFrom === 'string') {
|
|
60
|
+
const source = context.state.$outputs[params.additionalCommandsFrom];
|
|
61
|
+
const proposals = source?.structured?.verification;
|
|
62
|
+
if (!Array.isArray(proposals) || proposals.length > 20)
|
|
63
|
+
throw new Error('Verification proposals must be a bounded array from a committed agent output');
|
|
64
|
+
// Host checks always stay mandatory. The same scoped command validation
|
|
65
|
+
// applies to these actual subprocesses and to configured checks below.
|
|
66
|
+
for (const proposal of proposals) {
|
|
67
|
+
if (!proposal || typeof proposal !== 'object' || Array.isArray(proposal))
|
|
68
|
+
throw new Error('Invalid verification proposal');
|
|
69
|
+
const raw = proposal;
|
|
70
|
+
if (!deps.context.repositories.some(repository => repository.id === raw.repositoryId))
|
|
71
|
+
throw new Error('Unknown verification repository');
|
|
72
|
+
if (!hostRepositories.has(raw.repositoryId))
|
|
73
|
+
commands.push(withScopeDefault(deps.context, raw));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
26
76
|
const coversAll = deps.context.repositories.every(repository => commands.some(command => command.repositoryId === repository.id));
|
|
27
77
|
const request = { kind: coversAll || params.unverified === true ? 'full' : 'scoped', commands, ...(params.unverified === true ? { unverified: true } : {}) };
|
|
28
78
|
validateVerificationRequest(deps.context, request);
|
|
@@ -45,7 +95,7 @@ export function verifyPiece(bindings) {
|
|
|
45
95
|
const outcome = infrastructure ? 'failed' : receipt.valid ? 'pass' : 'fail';
|
|
46
96
|
const verified = receipt.valid && receipt.commands.length > 0 && !receipt.unverifiedRepositories?.length;
|
|
47
97
|
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 } : {}),
|
|
98
|
+
output: { receiptId: receipt.id, valid: receipt.valid, ...(receipt.reason ? { reason: receipt.reason } : {}), ...verificationDiagnostics(receipt) },
|
|
49
99
|
receipt: receiptEvidence(receipt), ...(verified ? { verified: { receiptId: receipt.id, candidateHash: receipt.candidateHash, atTransition: context.frame.transition, revision: context.frame.transition } } : { verified: null }) };
|
|
50
100
|
},
|
|
51
101
|
};
|
|
@@ -42,6 +42,8 @@ export interface RoleInvokerDeps {
|
|
|
42
42
|
openspec?: Record<AgentRole, import('../openspec.js').OpenSpecRoleContext>;
|
|
43
43
|
/** Engine hosts isolate sessions/routes by scope and node instead of a role-global file. */
|
|
44
44
|
roleState?: RoleStatePort;
|
|
45
|
+
/** Durable independent operations must recover uncertain effects explicitly. */
|
|
46
|
+
propagateInterruptions?: boolean;
|
|
45
47
|
/** `fixer` labels the developer step's events during a correction round on the fixer stance, so a log reader sees who is acting. */
|
|
46
48
|
onAgentEvent?: (role: AgentEventRole, event: AgentEvent) => void;
|
|
47
49
|
}
|
|
@@ -192,6 +192,8 @@ export function createRoleInvoker(deps) {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
catch (error) {
|
|
195
|
+
if (deps.propagateInterruptions && error instanceof AgentExecutionError && ['aborted', 'timeout', 'idle_timeout', 'lease_lost'].includes(error.code))
|
|
196
|
+
throw error;
|
|
195
197
|
return { ok: false, error: error instanceof Error ? error.message : String(error), ...(error instanceof AgentExecutionError ? { code: error.code } : {}) };
|
|
196
198
|
}
|
|
197
199
|
};
|
|
@@ -152,7 +152,7 @@ function architectSection(verification, definition) {
|
|
|
152
152
|
'2. Decide the approach, name the exact files/modules to create or change (inside the repository scope when one is declared), and call out risks, edge cases and compatibility concerns. Plan only the change the requested work needs; unrelated clean-ups are out of scope.',
|
|
153
153
|
'3. Break the work into ordered, atomic tasks. Each task names concrete files and includes its own tests. Every task must be completable by an agent that can only edit files and run commands: never add tasks such as "run the test suite", "verify", "test manually in a browser", "commit" or "open a PR". Core runs verification and the host owns delivery.',
|
|
154
154
|
'4. Execute the official OpenSpec fast-forward skill. Query status and instructions in dependency order; author the real delta specs and other artifacts using those templates and project rules. Preserve existing requirements through OpenSpec delta semantics. Do not fabricate complete replacement specs.',
|
|
155
|
-
'5. Propose verification. For each repository listed below without a configured verification command, name the existing command that proves the change: the project\'s test script, type check, build or lint (for example `npm` with args `["test"]`, or `cargo` with `["test"]`). Only propose commands that exist in the repository today or that a task in this plan adds; omit repositories where nothing automated applies. For a repository with a scope, the command must exercise that scope: omit cwd (it then runs
|
|
155
|
+
'5. Propose verification. For each repository listed below without a configured verification command, name the existing command that proves the change: the project\'s test script, type check, build or lint (for example `npm` with args `["test"]`, or `cargo` with `["test"]`). Only propose commands that exist in the repository today or that a task in this plan adds; omit repositories where nothing automated applies. For a repository with a scope, the command must exercise that scope: omit cwd for a single scope (it then runs there), or name a directory inside it; for multiple code workspaces, propose a separate command with an explicit cwd for each selected workspace; never a command that runs every workspace of a larger checkout. Core runs them after the developer finishes and feeds failures back.',
|
|
156
156
|
'6. Score your confidence honestly: `high` when the code evidence is conclusive; `medium` when the design rests on one non-obvious assumption (name it in the design); `low` when several plausible designs exist and you cannot choose without missing information. With `low`, put the single question whose answer decides the design in `question`. Core first lets you investigate further, then either asks the requester that exact question or proceeds on your stated assumptions, depending on the project configuration.',
|
|
157
157
|
'',
|
|
158
158
|
] : [definition, '']),
|
|
@@ -814,6 +814,8 @@ function validateCommand(context, raw) {
|
|
|
814
814
|
const cwd = command.cwd === undefined ? directory(repositoryWorkingDirectory(repo)) : directory(path.resolve(repo.path, String(command.cwd)));
|
|
815
815
|
if (!within(repo.path, cwd))
|
|
816
816
|
fail('Verification cwd escapes selected repository');
|
|
817
|
+
if (!withinRepositoryScope(repo, path.relative(repo.path, cwd).split(path.sep).join('/')))
|
|
818
|
+
fail('Verification cwd escapes selected code workspace');
|
|
817
819
|
const env = command.env === undefined ? undefined : object(command.env);
|
|
818
820
|
if (env && Object.values(env).some((value) => typeof value !== 'string' || value.includes('\0')))
|
|
819
821
|
fail('Invalid verification environment');
|
|
@@ -116,14 +116,14 @@
|
|
|
116
116
|
"file": ".specrails/install-config.yaml",
|
|
117
117
|
"version": 1,
|
|
118
118
|
"fields": {
|
|
119
|
-
"version": "number
|
|
120
|
-
"provider": "string
|
|
119
|
+
"version": "number \u2014 schema version, currently 1",
|
|
120
|
+
"provider": "string \u2014 claude | codex | gemini | kimi",
|
|
121
121
|
"tier": "Deprecated legacy string, tolerated and ignored; all installs use deterministic placement.",
|
|
122
|
-
"agents.selected": "string[]
|
|
123
|
-
"agents.excluded": "string[]
|
|
124
|
-
"models.preset": "string
|
|
125
|
-
"models.defaults.model": "string
|
|
126
|
-
"models.overrides": "Record<safe-agent-id, string>
|
|
122
|
+
"agents.selected": "string[] \u2014 unique lowercase kebab-case agent ids to install (1-64 characters)",
|
|
123
|
+
"agents.excluded": "string[] \u2014 unique lowercase kebab-case agent ids to skip; must not overlap agents.selected",
|
|
124
|
+
"models.preset": "string \u2014 balanced | budget | max; resolved within the selected provider catalog",
|
|
125
|
+
"models.defaults.model": "string \u2014 exact provider model id or configured alias (overrides preset; Kimi: 1-128 characters matching [A-Za-z0-9][A-Za-z0-9._/:-]*; default: k3)",
|
|
126
|
+
"models.overrides": "Record<safe-agent-id, string> \u2014 exact per-agent provider model ids or configured aliases with the same provider-specific validation (highest priority)"
|
|
127
127
|
}
|
|
128
128
|
},
|
|
129
129
|
"checkpoints": {
|
|
@@ -246,7 +246,9 @@
|
|
|
246
246
|
"signal",
|
|
247
247
|
"cancel"
|
|
248
248
|
],
|
|
249
|
-
"cliPresentationOperations": [
|
|
249
|
+
"cliPresentationOperations": [
|
|
250
|
+
"help"
|
|
251
|
+
],
|
|
250
252
|
"apiOperation": "runtime api emits {type: runtime-api, apiVersion, coreVersion, runtimeIdentity, workflowVersions, engineVersion, engines, nodeKindsVersion, nodeKinds, capabilities, guardrails}; does not invoke providers",
|
|
251
253
|
"validationTransport": "runtime validate --stdin accepts UTF-8 configuration JSON up to 2 MiB, mutually exclusive with --config",
|
|
252
254
|
"compactStatus": "runtime status --compact omits workflow history, events and outputs, frozen pipeline context and verification command output; retains identities (runId, traceId), phase statuses and visits, pendingApproval, pendingQuestion, usage, completion, acceptance summary and verification metadata",
|
|
@@ -296,11 +298,14 @@
|
|
|
296
298
|
"documentation": "docs/agent-runtime.md",
|
|
297
299
|
"instructionsVersion": "10",
|
|
298
300
|
"workflowVersion": "7",
|
|
299
|
-
"engines": [
|
|
301
|
+
"engines": [
|
|
302
|
+
1,
|
|
303
|
+
2
|
|
304
|
+
],
|
|
300
305
|
"engine": {
|
|
301
306
|
"version": 2,
|
|
302
307
|
"definitionSchema": "schemas/workflow-definition.schema.json",
|
|
303
|
-
"nodeKindsVersion":
|
|
308
|
+
"nodeKindsVersion": 7
|
|
304
309
|
},
|
|
305
310
|
"nodeKinds": [
|
|
306
311
|
"prompt",
|
|
@@ -308,6 +313,7 @@
|
|
|
308
313
|
"decider",
|
|
309
314
|
"verify",
|
|
310
315
|
"shell",
|
|
316
|
+
"artifact-contract",
|
|
311
317
|
"openspec-validate",
|
|
312
318
|
"openspec-archive",
|
|
313
319
|
"condition",
|
|
@@ -316,15 +322,25 @@
|
|
|
316
322
|
"question",
|
|
317
323
|
"gate",
|
|
318
324
|
"assign",
|
|
325
|
+
"implementation-step",
|
|
319
326
|
"component",
|
|
320
327
|
"map",
|
|
321
328
|
"implementation",
|
|
322
329
|
"join"
|
|
323
330
|
],
|
|
331
|
+
"implementationSteps": {
|
|
332
|
+
"version": 1,
|
|
333
|
+
"kind": "implementation-step",
|
|
334
|
+
"ownership": "Host definitions compose phases and freeze role definitions; Core executes individual operations and evidence gates"
|
|
335
|
+
},
|
|
324
336
|
"engineStatePath": "<backlogRoot>/.specrails/pipeline/<runId>/agent-workflow/run.sqlite",
|
|
325
337
|
"frozenRequest": "agent-runtime-request.json carries workflow {id, version, source: builtin|definition, definitionHash, engine: 1|2}; resume and status select the engine from workflow.engine and a request without the block is a legacy (engine 1) run",
|
|
326
338
|
"builtins": [
|
|
327
|
-
{
|
|
339
|
+
{
|
|
340
|
+
"id": "specrails-implementation",
|
|
341
|
+
"version": "7",
|
|
342
|
+
"deprecated": true
|
|
343
|
+
}
|
|
328
344
|
],
|
|
329
345
|
"developerTools": "The developer role runs with each CLI's autonomous edit+shell mode (claude --tools default --dangerously-skip-permissions with Agent/Task/Skill disallowed; codex workspace-write; gemini --yolo; kimi print mode). Architect and reviewer stay read-only. Claude roles load project settings only (--setting-sources project,local).",
|
|
330
346
|
"verificationPolicy": "Configured commands are optional. The architect proposes commands for repositories without one; repositories with no automated check are admitted and recorded in the receipt as unverifiedRepositories. Unchecked tasks return to the developer as feedback instead of failing the run.",
|
|
@@ -356,12 +372,16 @@
|
|
|
356
372
|
],
|
|
357
373
|
"customAgentLoop": "free",
|
|
358
374
|
"nativeCommandUnsupported": "native_command_unsupported"
|
|
375
|
+
},
|
|
376
|
+
"workflowAgentSteps": {
|
|
377
|
+
"version": 1,
|
|
378
|
+
"ownership": "Host-defined roles, schemas and gates; Core provides scoped artifact contracts and verified candidate guards"
|
|
359
379
|
}
|
|
360
380
|
},
|
|
361
381
|
"execution": {
|
|
362
382
|
"schemaVersion": 1,
|
|
363
383
|
"contextEnv": "SPECRAILS_EXECUTION_CONTEXT",
|
|
364
|
-
"context": "Absolute JSON file: runId, backlogRoot, backlogPath?, artifactRoot, artifactRepositoryId, repositories[{id,name,path,baseSha?,scope?}], ownership{git,backlog,worktrees:host|core}, specs[{id,title,description,repositoryIds?,acceptanceCriteria?}]. scope (runtime capability repositoryScope) lists the checkout-relative directories a repository is confined to
|
|
384
|
+
"context": "Absolute JSON file: runId, backlogRoot, backlogPath?, artifactRoot, artifactRepositoryId, repositories[{id,name,path,baseSha?,scope?}], ownership{git,backlog,worktrees:host|core}, specs[{id,title,description,repositoryIds?,acceptanceCriteria?}]. scope (runtime capability repositoryScope) lists the checkout-relative directories a repository is confined to \u2014 a package registered inside a larger checkout: commands without cwd run in the first one, model-proposed checks stay inside it, edits outside it are undone after each developer/fixer turn and reviews judge the git change set against the run base.",
|
|
365
385
|
"runtime": ".specrails/runtime/pipeline.mjs",
|
|
366
386
|
"operations": [
|
|
367
387
|
"init",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specrails-core",
|
|
3
|
-
"version": "6.
|
|
4
|
-
"description": "Agent workflow engine for specrails-desktop: provider-independent installer and programmatic agent runtime for Claude Code
|
|
3
|
+
"version": "6.2.0",
|
|
4
|
+
"description": "Agent workflow engine for specrails-desktop: provider-independent installer and programmatic agent runtime for Claude Code and Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./agent-runtime": {
|