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
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
The agent workflow engine behind [Specrails Desktop](https://github.com/fjpulidop/specrails-desktop). Core does two things:
|
|
8
8
|
|
|
9
|
-
1. **Installs** the specrails workflow into a project for Claude Code
|
|
10
|
-
2. **
|
|
9
|
+
1. **Installs** the specrails workflow into a project for Claude Code or Codex CLI: three roles (architect, developer, reviewer), the `implement` and `retry` entry points (`implement` runs one ticket or several tickets as one aggregate run) and the official OpenSpec skills.
|
|
10
|
+
2. **Executes** Desktop-owned workflow definitions through the [programmatic agent runtime](docs/agent-runtime.md): independent agent and implementation operations, per-role providers, deterministic verification, durable recovery and OpenAI-compatible local models. Desktop owns workflow composition and editable loop agents; Core retains the old builtin for saved runs.
|
|
11
11
|
|
|
12
12
|
Desktop bundles a pinned Core package and owns everything around it: worktrees, commits, pull requests, backlog and the user interface. Core never ships code on its own; it prepares a reviewed candidate for the host.
|
|
13
13
|
|
|
@@ -31,7 +31,7 @@ bin/specrails-core.mjs npm bin shim → dist/installer/cli.js
|
|
|
31
31
|
src/installer/ CLI, framework lifecycle, provider rendering
|
|
32
32
|
src/pipeline/ pipeline journal, gates and verification receipts
|
|
33
33
|
src/agent-runtime/ programmatic runtime: graph, executors, compact loop, recovery
|
|
34
|
-
templates/ role definitions, workflow commands, provider settings
|
|
34
|
+
templates/ role definitions, workflow commands, provider settings
|
|
35
35
|
schemas/ runtime configuration schema and fixtures
|
|
36
36
|
integration-contract.json Desktop ⇄ Core contract
|
|
37
37
|
```
|
|
@@ -45,6 +45,8 @@ export function withScopeDefault(context, command) {
|
|
|
45
45
|
if (command.cwd !== undefined)
|
|
46
46
|
return command;
|
|
47
47
|
const scope = context.repositories.find(repo => repo.id === command.repositoryId)?.scope;
|
|
48
|
+
if (scope && scope.length > 1)
|
|
49
|
+
throw new Error('Verification must name an explicit cwd for each selected code workspace');
|
|
48
50
|
return scope?.length ? { ...command, cwd: scope[0] } : command;
|
|
49
51
|
}
|
|
50
52
|
/** Model-proposed checks stay inside the repository scope: they prove the change, not the whole checkout. */
|
|
@@ -32,7 +32,8 @@ export function buildCliInvocation(provider, request, options = {}) {
|
|
|
32
32
|
}
|
|
33
33
|
const readOnly = requestPolicy(request).access === 'read';
|
|
34
34
|
// Pin the product alias; explicit IDs remain reproducible across releases.
|
|
35
|
-
const
|
|
35
|
+
const claudeModelIds = { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5' };
|
|
36
|
+
const modelId = provider === 'claude' && request.model ? claudeModelIds[request.model] ?? request.model : request.model;
|
|
36
37
|
const model = modelId ? ['--model', modelId] : [];
|
|
37
38
|
const extraRoots = request.allowedRoots.filter(root => root !== request.cwd);
|
|
38
39
|
const resume = request.resumeSessionId;
|
|
@@ -158,7 +158,7 @@ export async function runRuntimeCommand(flags, positionals, emit = value => proc
|
|
|
158
158
|
emit({ type: 'runtime-api', apiVersion: RUNTIME_API_VERSION, coreVersion: CORE_PACKAGE_VERSION, runtimeIdentity: coreRuntimeIdentity(), workflowVersions: [CORE_WORKFLOW_VERSION],
|
|
159
159
|
engineVersion: 2, engines: RUNTIME_ENGINES, nodeKindsVersion: NODE_KINDS_VERSION, nodeKinds: validationPieceRegistry().kinds(),
|
|
160
160
|
capabilities: { openRoles: 1, scopedRecovery: 1, efficientRoleExecution: 1, reproducibleVerification: 1, implementationEfficiencyMetrics: 1, compactAgentLoop: 1, configurableGuardrails: 1, compactOutputBudget: 1, roleThinkingControl: 1, repositoryScope: 1,
|
|
161
|
-
engineV2: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1 }, guardrails: GUARDRAIL_CATALOG });
|
|
161
|
+
engineV2: 1, workflowAgentSteps: 1, implementationSteps: 1, workflowDefinitions: 1, fanOut: 1, fork: 1, forkIdempotency: 1, steeringInbox: 1 }, guardrails: GUARDRAIL_CATALOG });
|
|
162
162
|
return 0;
|
|
163
163
|
}
|
|
164
164
|
if (command === 'validate') {
|
|
@@ -198,7 +198,7 @@ export class RunLedger {
|
|
|
198
198
|
else if (result.verified) {
|
|
199
199
|
const receipt = result.receipt, verified = result.verified;
|
|
200
200
|
const evidence = receipt?.evidence && typeof receipt.evidence === 'object' && !Array.isArray(receipt.evidence) ? receipt.evidence : undefined;
|
|
201
|
-
if (!['verify', 'implementation'].includes(String(visit.kind)) || !receipt?.valid || receipt.scope !== 'full' || !candidate ||
|
|
201
|
+
if (!['verify', 'implementation', 'implementation-step'].includes(String(visit.kind)) || !receipt?.valid || receipt.scope !== 'full' || !candidate ||
|
|
202
202
|
!Array.isArray(evidence?.commands) || evidence.commands.length === 0 ||
|
|
203
203
|
(evidence.unverifiedRepositories !== undefined && (!Array.isArray(evidence.unverifiedRepositories) || evidence.unverifiedRepositories.length !== 0)) ||
|
|
204
204
|
verified.receiptId !== receipt.id || verified.candidateHash !== candidate.hash || verified.revision !== candidate.revision || verified.atTransition !== frame.transition) {
|
|
@@ -460,7 +460,7 @@ export class RunLedger {
|
|
|
460
460
|
}
|
|
461
461
|
sessionOwner(frame, ownerNodePath, key) {
|
|
462
462
|
const owner = ownerNodePath.replace(/\/$/, '');
|
|
463
|
-
if (!key.startsWith('session:') || (owner !== '' && owner !== frame.nodePath && !frame.nodePath.startsWith(owner + '/'))) {
|
|
463
|
+
if ((!key.startsWith('session:') && !key.startsWith('contract:artifacts:') && key !== 'binding:implementation') || (owner !== '' && owner !== frame.nodePath && !frame.nodePath.startsWith(owner + '/'))) {
|
|
464
464
|
throw new EngineError('piece_state_scope', 'Shared sessions require an ancestor node owner in this execution scope');
|
|
465
465
|
}
|
|
466
466
|
return owner;
|
|
@@ -68,7 +68,7 @@ export async function runDefinitionCommand(flags, positionals, emit) {
|
|
|
68
68
|
if (command === 'workflows') {
|
|
69
69
|
if (!positionals[1] || positionals[1] === 'list') {
|
|
70
70
|
emit({ type: 'runtime-workflows', nodeKindsVersion: NODE_KINDS_VERSION, nodeKinds: validationPieceRegistry().catalog(), definitionSchema: workflowDefinitionSchema,
|
|
71
|
-
builtins: [{ id: 'specrails-implementation', version: CORE_WORKFLOW_VERSION, deprecated:
|
|
71
|
+
builtins: [{ id: 'specrails-implementation', version: CORE_WORKFLOW_VERSION, deprecated: true }] });
|
|
72
72
|
return 0;
|
|
73
73
|
}
|
|
74
74
|
if (positionals[1] !== 'validate' || flags.stdin !== true)
|
|
@@ -35,19 +35,21 @@ export function compileWorkflowDefinition(definition, registry, execution, optio
|
|
|
35
35
|
const outcomes = node.kind === 'component' ? componentBody?.outputs ?? ['next', 'failed'] : registry.outcomes(node.kind, node.params);
|
|
36
36
|
const branchName = '_core_map_' + id;
|
|
37
37
|
const mapPredecessor = node.kind === 'join' ? Object.entries(body.nodes).find(([, value]) => value.kind === 'map' && value.ends.next === id)?.[0] : undefined;
|
|
38
|
-
const
|
|
38
|
+
const requiresAI = piece.descriptor.requiresAI && !(node.kind === 'implementation-step' && ['verify', 'archive'].includes(String(node.params.phase)));
|
|
39
|
+
const retry = { maxAttempts: node.retry?.maxAttempts ?? (requiresAI ? 2 : 1), retrySafe: effect === 'read', initialIntervalMs: node.retry?.backoffMs ?? 5000 };
|
|
39
40
|
graph.addNode(id, async (state, config) => {
|
|
40
41
|
const info = config.executionInfo;
|
|
41
42
|
if (!info?.threadId)
|
|
42
43
|
throw new EngineError('execution_identity_missing', 'Durable definitions require public task identity and a checkpoint thread');
|
|
43
44
|
const nodePath = state.$scope.nodePathPrefix ? state.$scope.nodePathPrefix + '/' + id : id;
|
|
44
|
-
const frame = await execution.enter({ nodePath, kind: node.kind, effect, requiresAI
|
|
45
|
-
acceptsSteering:
|
|
45
|
+
const frame = await execution.enter({ nodePath, kind: node.kind, effect, requiresAI,
|
|
46
|
+
acceptsSteering: requiresAI && (['prompt', 'role-turn'].includes(node.kind) || node.kind === 'implementation-step' && !['verify', 'archive'].includes(String(node.params.phase))) && node.params.appendSteering !== false, scope: state.$scope,
|
|
46
47
|
task: { checkpointThreadId: info.threadId, checkpointId: info.checkpointId, taskCheckpointNs: info.checkpointNs, taskId: info.taskId }, retry });
|
|
47
48
|
let result;
|
|
49
|
+
let childEvidence;
|
|
48
50
|
let mapPlan;
|
|
49
51
|
try {
|
|
50
|
-
const interpolated = interpolateParams(node.params, state.$vars);
|
|
52
|
+
const interpolated = interpolateParams(node.params, state.$vars, state);
|
|
51
53
|
const params = node.kind === 'end' && root ? { ...interpolated, requiresVerified: interpolated.requiresVerified === true || frozen.delivery?.requiresVerified === true } : interpolated;
|
|
52
54
|
if (node.kind === 'map') {
|
|
53
55
|
const items = mapItems(params.over, state, options.collections);
|
|
@@ -82,11 +84,10 @@ export function compileWorkflowDefinition(definition, registry, execution, optio
|
|
|
82
84
|
const exit = completed.$exit;
|
|
83
85
|
if (!exit)
|
|
84
86
|
throw new EngineError('component_exit_missing', `Component ${nodePath} did not produce an exit`);
|
|
87
|
+
childEvidence = { $candidate: completed.$candidate, $verified: completed.$verified };
|
|
85
88
|
result = { outcome: exit.outcome, status: exit.status, output: { outputs: completed.$outputs, completion: { ...exit.completion } },
|
|
86
|
-
|
|
89
|
+
usage: completed.$usage, completion: exit.completion,
|
|
87
90
|
...(exit.error ? { error: exit.error } : {}) };
|
|
88
|
-
if (!result.candidate)
|
|
89
|
-
delete result.candidate;
|
|
90
91
|
}
|
|
91
92
|
else
|
|
92
93
|
result = await execution.execute(frame, effect, signal => piece.execute(params, {
|
|
@@ -129,7 +130,7 @@ export function compileWorkflowDefinition(definition, registry, execution, optio
|
|
|
129
130
|
result.completion = { ...result.completion, ok: false, reasons: [...new Set([...result.completion.reasons, 'unverified'])] };
|
|
130
131
|
}
|
|
131
132
|
const marker = execution.terminal(frame, result);
|
|
132
|
-
const patch = resultPatch(id, frame, marker.result, marker);
|
|
133
|
+
const patch = { ...resultPatch(id, frame, marker.result, marker), ...childEvidence };
|
|
133
134
|
if (mapPlan)
|
|
134
135
|
patch.$maps = { [id]: mapPlan };
|
|
135
136
|
if (node.kind === 'join' && mapPredecessor && state.$maps[mapPredecessor])
|
|
@@ -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", "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", "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", "openspec-validate", "openspec-archive", "approval", "question", "gate", "map", "join", "component", "implementation", "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"];
|
|
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
|
-
'openspec-validate', 'openspec-archive', 'approval', 'question', 'gate', 'map', 'join',
|
|
3
|
-
'component', 'implementation', 'end'];
|
|
2
|
+
'artifact-contract', 'openspec-validate', 'openspec-archive', 'approval', 'question', 'gate', 'map', 'join',
|
|
3
|
+
'component', 'implementation', 'implementation-step', 'end'];
|
|
@@ -34,6 +34,7 @@ export function validateWorkflowDefinition(input, registry, roles = {}, options
|
|
|
34
34
|
if (!Object.hasOwn(roles, role))
|
|
35
35
|
add('role_not_found', '/roles', `Role ${role} is not configured`);
|
|
36
36
|
let implementations = 0;
|
|
37
|
+
let operationCount = 0;
|
|
37
38
|
let writes = false;
|
|
38
39
|
const successfulEnds = [];
|
|
39
40
|
const bodies = definition.components ?? {};
|
|
@@ -133,6 +134,9 @@ export function validateWorkflowDefinition(input, registry, roles = {}, options
|
|
|
133
134
|
}
|
|
134
135
|
if (node.kind === 'implementation')
|
|
135
136
|
implementations += 1;
|
|
137
|
+
if (node.kind === 'implementation-step') {
|
|
138
|
+
operationCount += 1;
|
|
139
|
+
}
|
|
136
140
|
if (node.kind === 'end' && node.params.outcome === 'success')
|
|
137
141
|
successfulEnds.push({ path, verified: node.params.requiresVerified === true });
|
|
138
142
|
if (node.kind === 'end' && node.params.exit !== undefined) {
|
|
@@ -173,8 +177,8 @@ export function validateWorkflowDefinition(input, registry, roles = {}, options
|
|
|
173
177
|
inspectReferences(definition, [], '');
|
|
174
178
|
for (const [name, body] of Object.entries(bodies))
|
|
175
179
|
inspectReferences(body, [name], '/components/' + name);
|
|
176
|
-
if ((definition.journal === 'ledger-only' && implementations !== 0) || (definition.journal === 'implementation' && (implementations !== 1 || definition.change === 'none')))
|
|
177
|
-
add('journal_mismatch', '/journal', 'Implementation journals require
|
|
180
|
+
if ((definition.journal === 'ledger-only' && (implementations !== 0 || operationCount > 0)) || (definition.journal === 'implementation' && ((implementations !== 1 && operationCount === 0) || (implementations > 0 && operationCount > 0) || definition.change === 'none')))
|
|
181
|
+
add('journal_mismatch', '/journal', 'Implementation journals require independent implementation steps or one legacy implementation wrapper, and an existing or new change');
|
|
178
182
|
if (writes && definition.delivery?.requiresVerified !== false && definition.delivery?.requiresVerified !== true)
|
|
179
183
|
for (const end of successfulEnds)
|
|
180
184
|
if (!end.verified)
|
|
@@ -7,5 +7,5 @@ export declare function boundedPattern(pattern: string): RegExp;
|
|
|
7
7
|
/** Recursive descent over a closed grammar; no eval or executable expression callbacks. */
|
|
8
8
|
export declare function parseExpression(source: string): Expression;
|
|
9
9
|
export declare function validateInterpolations(value: JsonValue): void;
|
|
10
|
-
export declare function interpolateParams(params: JsonObject, vars: Record<string, JsonValue
|
|
10
|
+
export declare function interpolateParams(params: JsonObject, vars: Record<string, JsonValue>, state?: CoreDefinitionState): JsonObject;
|
|
11
11
|
export {};
|
|
@@ -140,21 +140,34 @@ export function parseExpression(source) {
|
|
|
140
140
|
export function validateInterpolations(value) {
|
|
141
141
|
if (typeof value === 'string') {
|
|
142
142
|
const tokens = value.replaceAll('{{{{', '').match(/\{\{[^}]*\}\}/g) ?? [];
|
|
143
|
-
for (const token of tokens)
|
|
143
|
+
for (const token of tokens) {
|
|
144
|
+
if (token.startsWith('{{outputs.') && (!/^\{\{outputs\.[A-Za-z][A-Za-z0-9_.-]*\}\}$/.test(token) || token.slice(10, -2).split('.').some(part => forbiddenKeys.has(part))))
|
|
145
|
+
throw new EngineError('invalid_interpolation', 'Invalid workflow output reference');
|
|
144
146
|
if (token.startsWith('{{run.') && !/^\{\{run\.[A-Za-z][A-Za-z0-9_-]*\}\}$/.test(token))
|
|
145
147
|
throw new EngineError('invalid_interpolation', `Invalid runtime variable ${token}`);
|
|
148
|
+
}
|
|
146
149
|
}
|
|
147
150
|
else if (Array.isArray(value))
|
|
148
151
|
value.forEach(validateInterpolations);
|
|
149
152
|
else if (value && typeof value === 'object')
|
|
150
153
|
Object.values(value).forEach(validateInterpolations);
|
|
151
154
|
}
|
|
152
|
-
export function interpolateParams(params, vars) {
|
|
155
|
+
export function interpolateParams(params, vars, state) {
|
|
153
156
|
function resolve(value) {
|
|
154
157
|
if (typeof value === 'string')
|
|
155
|
-
return value.replace(/\{\{\{\{|\{\{run\.([A-Za-z][A-Za-z0-9_-]*)\}\}/g, (token, name) => {
|
|
158
|
+
return value.replace(/\{\{\{\{|\{\{run\.([A-Za-z][A-Za-z0-9_-]*)\}\}|\{\{outputs\.([A-Za-z][A-Za-z0-9_.-]*)\}\}|\{\{history\.text\}\}/g, (token, name, output) => {
|
|
156
159
|
if (token === '{{{{')
|
|
157
160
|
return '{{';
|
|
161
|
+
if (token === '{{history.text}}')
|
|
162
|
+
return (state?.$history ?? []).map(entry => `[${entry.nodePath}] ${entry.text}`).join('\n').slice(-32_000);
|
|
163
|
+
if (output) {
|
|
164
|
+
if (output.split('.').some(part => forbiddenKeys.has(part)))
|
|
165
|
+
throw new EngineError('invalid_interpolation', 'Prototype output paths are forbidden');
|
|
166
|
+
const resolved = state ? lookup(state, '$outputs.' + output) : undefined;
|
|
167
|
+
if (resolved === undefined)
|
|
168
|
+
throw new EngineError('output_missing', `Workflow output ${output} is missing`);
|
|
169
|
+
return typeof resolved === 'string' ? resolved : JSON.stringify(resolved);
|
|
170
|
+
}
|
|
158
171
|
if (!name || !Object.hasOwn(vars, name))
|
|
159
172
|
throw new EngineError('run_var_missing', `Runtime variable ${name ?? ''} is missing`);
|
|
160
173
|
const replacement = vars[name];
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync, renameSync, rmSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, renameSync, rmSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { pipelineStateDirectory, validatePipelineContext } from '../../pipeline/pipeline-state.js';
|
|
5
5
|
import { createExecutorRegistry } from '../executors.js';
|
|
6
|
+
import { artifactPath } from '../openspec.js';
|
|
6
7
|
import { coreRuntimeIdentity } from '../core-host.js';
|
|
7
8
|
import { sameRuntimeIdentity } from '../runtime-identity.js';
|
|
8
9
|
import { canonicalJson, contentDigest } from './canonical-json.js';
|
|
@@ -33,6 +34,38 @@ function rebind(source, parent, change) {
|
|
|
33
34
|
const context = validatePipelineContext({ ...source.context, runId: standalone ? parent.runId : parent.runId.slice(0, 80) + '-impl-' + digest });
|
|
34
35
|
return { ...source, context, parentRunId: parent.runId, change: standalone ? change : change.slice(0, 38).replace(/-+$/, '') + '-' + digest, directory: pipelineStateDirectory(context) };
|
|
35
36
|
}
|
|
37
|
+
function copyActiveChange(context, source, target, allocation) {
|
|
38
|
+
const from = artifactPath(context.artifactRoot, 'openspec/changes/' + source);
|
|
39
|
+
if (!existsSync(from))
|
|
40
|
+
return; // A cut before planning has no authored artifacts yet.
|
|
41
|
+
const to = artifactPath(context.artifactRoot, 'openspec/changes/' + target);
|
|
42
|
+
if (existsSync(to))
|
|
43
|
+
throw new EngineError('run_exists', 'Fork refuses to replace an existing change');
|
|
44
|
+
allocation.reserve(to);
|
|
45
|
+
let bytes = 0, files = 0;
|
|
46
|
+
const copy = (relative) => {
|
|
47
|
+
const directory = relative ? artifactPath(from, relative) : from;
|
|
48
|
+
mkdirSync(relative ? artifactPath(to, relative) : to, { recursive: true, mode: 0o700 });
|
|
49
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
50
|
+
const child = relative ? relative + '/' + entry.name : entry.name;
|
|
51
|
+
if (entry.isSymbolicLink())
|
|
52
|
+
throw new EngineError('artifact_scope_mismatch', 'Fork rejects symlinked change artifacts');
|
|
53
|
+
if (entry.isDirectory())
|
|
54
|
+
copy(child);
|
|
55
|
+
else if (entry.isFile()) {
|
|
56
|
+
const content = readFileSync(artifactPath(from, child));
|
|
57
|
+
bytes += content.length;
|
|
58
|
+
files++;
|
|
59
|
+
if (bytes > 2 * 1024 * 1024 || files > 200)
|
|
60
|
+
throw new EngineError('output_limit', 'Fork artifacts exceed their copy limit');
|
|
61
|
+
writeFileSync(artifactPath(to, child), content, { flag: 'wx', mode: 0o600 });
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
throw new EngineError('artifact_scope_mismatch', 'Fork requires ordinary change artifacts');
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
copy('');
|
|
68
|
+
}
|
|
36
69
|
/** A fork publishes an independently leased historical cut; the source is opened read-only. */
|
|
37
70
|
export async function forkRun(directory, options) {
|
|
38
71
|
const state = patchState(options.state);
|
|
@@ -93,20 +126,27 @@ export async function forkRun(directory, options) {
|
|
|
93
126
|
const change = request.change ? request.change.slice(0, 42).replace(/-+$/, '') + '-fork-' + contentDigest(context.runId).slice(0, 12) : undefined;
|
|
94
127
|
const forkRequest = { ...request, context, ...(change ? { change } : {}) };
|
|
95
128
|
const sourceBindings = destination.sqlite.prepare("SELECT * FROM piece_state WHERE key='binding:implementation'").all();
|
|
129
|
+
const genericChange = !!change && sourceBindings.length === 0 && definition.journal === 'ledger-only';
|
|
130
|
+
if (genericChange)
|
|
131
|
+
copyActiveChange(context, request.change, change, allocation);
|
|
96
132
|
const restored = [];
|
|
97
133
|
for (const row of sourceBindings) {
|
|
98
134
|
// A completed implementation is inherited evidence. Rebinding its journal would
|
|
99
135
|
// create a new candidate and repeat verified work even when the workspace is unchanged.
|
|
100
136
|
const completed = destination.sqlite.prepare('SELECT 1 FROM attempts WHERE visit_id=? AND terminal_digest IS NOT NULL').get(row.visit_id);
|
|
101
|
-
|
|
137
|
+
const binding = JSON.parse(String(row.value_json));
|
|
138
|
+
if (completed && !binding.shared)
|
|
139
|
+
continue;
|
|
140
|
+
const childScope = binding.shared ? binding.scopeId : binding.scopeId + '/' + String(row.visit_id);
|
|
141
|
+
const results = destination.sqlite.prepare('SELECT a.output_json FROM attempts a JOIN visits v ON a.visit_id=v.visit_id WHERE a.scope_id=? AND a.output_json IS NOT NULL ORDER BY v.global_transition DESC,a.attempt DESC').all(childScope);
|
|
142
|
+
const snapshot = results.map(result => JSON.parse(String(result.output_json)).childUpdate?.journal).find(value => value !== undefined);
|
|
143
|
+
const lastOperation = results.map(result => JSON.parse(String(result.output_json))).find(result => result.childUpdate?.operationState !== undefined);
|
|
144
|
+
if (binding.shared && lastOperation?.completion?.ok)
|
|
102
145
|
continue;
|
|
103
|
-
const
|
|
146
|
+
const target = rebind(binding, context, change);
|
|
104
147
|
const changeDirectory = path.join(target.context.artifactRoot, 'openspec', 'changes', target.change);
|
|
105
148
|
if ((target.directory !== targetRoot && existsSync(target.directory)) || existsSync(changeDirectory))
|
|
106
149
|
throw new EngineError('run_exists', 'Fork refuses to replace an existing implementation journal or change');
|
|
107
|
-
const childScope = binding.scopeId + '/' + String(row.visit_id);
|
|
108
|
-
const results = destination.sqlite.prepare('SELECT a.output_json FROM attempts a JOIN visits v ON a.visit_id=v.visit_id WHERE a.scope_id=? AND a.output_json IS NOT NULL ORDER BY v.global_transition DESC,a.attempt DESC').all(childScope);
|
|
109
|
-
const snapshot = results.map(result => JSON.parse(String(result.output_json)).childUpdate?.journal).find(value => value !== undefined);
|
|
110
150
|
if (snapshot) {
|
|
111
151
|
if (target.directory !== targetRoot)
|
|
112
152
|
allocation.reserve(target.directory);
|
|
@@ -120,7 +160,7 @@ export async function forkRun(directory, options) {
|
|
|
120
160
|
destination.transaction('fork-rebound', () => {
|
|
121
161
|
lease.assert(token);
|
|
122
162
|
destination.put('runs', { ...ledger.run(), request_json: canonicalJson(forkRequest), status: 'running', completion_json: null,
|
|
123
|
-
...(Object.keys(state).length || restored.length ? { verified_json: null } : {}) });
|
|
163
|
+
...(Object.keys(state).length || restored.length || genericChange ? { verified_json: null } : {}) });
|
|
124
164
|
for (const binding of restored)
|
|
125
165
|
destination.put('piece_state', { ...binding.row, value_json: canonicalJson(binding.target) });
|
|
126
166
|
destination.put('piece_state', { run_id: context.runId, scope_id: 'root', node_path: '', key: 'lineage:fork', value_json: canonicalJson({
|
|
@@ -141,7 +181,7 @@ export async function forkRun(directory, options) {
|
|
|
141
181
|
heads.set(namespace, tuple);
|
|
142
182
|
}
|
|
143
183
|
for (const binding of restored) {
|
|
144
|
-
if (!existsSync(path.join(binding.target.directory, 'state.json')))
|
|
184
|
+
if (!existsSync(path.join(binding.target.directory, 'state.json')) || binding.source.shared)
|
|
145
185
|
continue;
|
|
146
186
|
const scope = binding.source.scopeId + '/' + String(binding.row.visit_id);
|
|
147
187
|
const matching = [...heads.values()].filter(tuple => tuple.checkpoint.channel_values.$scope?.id === scope && '$next' in tuple.checkpoint.channel_values);
|
|
@@ -154,11 +194,13 @@ export async function forkRun(directory, options) {
|
|
|
154
194
|
throw new EngineError('fork_checkpoint_missing', 'Implementation checkpoint has no unambiguous public previous node');
|
|
155
195
|
await runtime.graph.updateState(tuple.config, projection.update, writer);
|
|
156
196
|
}
|
|
157
|
-
if (Object.keys(state).length) {
|
|
197
|
+
if (Object.keys(state).length || genericChange) {
|
|
158
198
|
const tuple = await runtime.saver.getTuple(targetConfig);
|
|
159
199
|
if (!tuple)
|
|
160
200
|
throw new EngineError('fork_checkpoint_missing', 'Target checkpoint is not present in the historical cut');
|
|
161
201
|
const values = tuple.checkpoint.channel_values;
|
|
202
|
+
if (genericChange)
|
|
203
|
+
state.$vars = { ...(values.$outer?.$vars ?? values.$vars), ...state.$vars, changeId: change };
|
|
162
204
|
const outer = values.$outer;
|
|
163
205
|
const update = outer ? { $outer: { ...outer,
|
|
164
206
|
...(state.$vars ? { $vars: { ...outer.$vars, ...state.$vars } } : {}),
|
|
@@ -8,7 +8,7 @@ export declare function describeDefinition(runId: string, definition: WorkflowDe
|
|
|
8
8
|
workflowId: string;
|
|
9
9
|
nodes: {
|
|
10
10
|
path: string;
|
|
11
|
-
kind: "join" | "map" | "verify" | "shell" | "end" | "approval" | "question" | "prompt" | "gate" | "implementation" | "role-turn" | "decider" | "condition" | "assign" | "openspec-validate" | "openspec-archive" | "component";
|
|
11
|
+
kind: "join" | "map" | "verify" | "shell" | "end" | "approval" | "question" | "prompt" | "gate" | "implementation" | "role-turn" | "decider" | "condition" | "assign" | "openspec-validate" | "openspec-archive" | "component" | "implementation-step" | "artifact-contract";
|
|
12
12
|
label: string;
|
|
13
13
|
}[];
|
|
14
14
|
edges: {
|
|
@@ -24,7 +24,7 @@ export declare function describeDefinition(runId: string, definition: WorkflowDe
|
|
|
24
24
|
nodes: {
|
|
25
25
|
component?: string | undefined;
|
|
26
26
|
id: string;
|
|
27
|
-
kind: "join" | "map" | "verify" | "shell" | "end" | "approval" | "question" | "prompt" | "gate" | "implementation" | "role-turn" | "decider" | "condition" | "assign" | "openspec-validate" | "openspec-archive" | "component";
|
|
27
|
+
kind: "join" | "map" | "verify" | "shell" | "end" | "approval" | "question" | "prompt" | "gate" | "implementation" | "role-turn" | "decider" | "condition" | "assign" | "openspec-validate" | "openspec-archive" | "component" | "implementation-step" | "artifact-contract";
|
|
28
28
|
label: string;
|
|
29
29
|
ends: {
|
|
30
30
|
[x: string]: string | null;
|
|
@@ -36,7 +36,7 @@ export declare function describeDefinition(runId: string, definition: WorkflowDe
|
|
|
36
36
|
nodes: {
|
|
37
37
|
component?: string | undefined;
|
|
38
38
|
id: string;
|
|
39
|
-
kind: "join" | "map" | "verify" | "shell" | "end" | "approval" | "question" | "prompt" | "gate" | "implementation" | "role-turn" | "decider" | "condition" | "assign" | "openspec-validate" | "openspec-archive" | "component";
|
|
39
|
+
kind: "join" | "map" | "verify" | "shell" | "end" | "approval" | "question" | "prompt" | "gate" | "implementation" | "role-turn" | "decider" | "condition" | "assign" | "openspec-validate" | "openspec-archive" | "component" | "implementation-step" | "artifact-contract";
|
|
40
40
|
label: string;
|
|
41
41
|
ends: {
|
|
42
42
|
[x: string]: string | null;
|
|
@@ -18,6 +18,8 @@ export function definitionNodeAt(definition, nodePath) {
|
|
|
18
18
|
}
|
|
19
19
|
export function roleAt(definition, nodePath) {
|
|
20
20
|
const node = definitionNodeAt(definition, nodePath);
|
|
21
|
+
if (node?.kind === 'implementation-step')
|
|
22
|
+
return String(node.params.phase);
|
|
21
23
|
if (node?.kind === 'role-turn' || node?.kind === 'decider')
|
|
22
24
|
return String(node.params.roleId);
|
|
23
25
|
return node?.kind === 'prompt' ? 'prompt' : nodePath.split('/').at(-1);
|
|
@@ -18,13 +18,13 @@ export declare class RunPieceDependencies implements PieceDependencies {
|
|
|
18
18
|
private readonly definition;
|
|
19
19
|
private readonly ledger;
|
|
20
20
|
private readonly directory;
|
|
21
|
-
|
|
21
|
+
readonly change?: string | undefined;
|
|
22
22
|
private readonly projectMemory?;
|
|
23
23
|
readonly policies: PieceDependencies['policies'];
|
|
24
24
|
constructor(context: PipelineContext, config: RuntimeConfig, registry: ExecutorRegistry, definition: WorkflowDefinition, ledger: RunLedger, directory: string, change?: string | undefined, projectMemory?: SqliteProjectStore | undefined);
|
|
25
25
|
memory(context: PieceExecutionContext): ProjectMemory;
|
|
26
26
|
private implementationBindings;
|
|
27
|
-
bindImplementation(context: PieceExecutionContext, change: string): ImplementationBinding;
|
|
27
|
+
bindImplementation(context: PieceExecutionContext, change: string, shared?: boolean): ImplementationBinding;
|
|
28
28
|
private candidateScope;
|
|
29
29
|
private inheritedCandidateScope;
|
|
30
30
|
private candidate;
|
|
@@ -38,8 +38,12 @@ export declare class RunPieceDependencies implements PieceDependencies {
|
|
|
38
38
|
candidate: CandidateState;
|
|
39
39
|
verified: import("./contracts.js").VerifiedState | null;
|
|
40
40
|
};
|
|
41
|
+
/** Write admission revokes certification; the archive may check its immutable pre-write checkpoint against a real receipt. */
|
|
42
|
+
verifiedBeforeWrite(context: PieceExecutionContext): import("./contracts.js").VerifiedState | null;
|
|
41
43
|
/** Only the execution owner proposes protected candidate/receipt channels. */
|
|
42
44
|
finalize(frame: AttemptFrame, result: PieceResult, effect: EngineEffect): PieceResult;
|
|
45
|
+
operationState(context: PieceExecutionContext): JsonValue | undefined;
|
|
46
|
+
artifactContracts(context: PieceExecutionContext): PieceStatePort;
|
|
43
47
|
memo(context: PieceExecutionContext): PieceStatePort;
|
|
44
48
|
roleState(context: PieceExecutionContext): RoleStatePort;
|
|
45
49
|
stepContext(context: PieceExecutionContext): import("../workflow-types.js").WorkflowStepContext;
|
|
@@ -42,13 +42,20 @@ export class RunPieceDependencies {
|
|
|
42
42
|
.map(row => JSON.parse(String(row.value_json)))
|
|
43
43
|
.filter(binding => binding.parentRunId === this.context.runId);
|
|
44
44
|
}
|
|
45
|
-
bindImplementation(context, change) {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
45
|
+
bindImplementation(context, change, shared = false) {
|
|
46
|
+
const scoped = shared ? { ...context, frame: { ...context.frame, nodePath: context.frame.scope.nodePathPrefix } } : context;
|
|
47
|
+
const binding = deriveImplementationBinding(this.context, scoped, change);
|
|
48
|
+
if (shared)
|
|
49
|
+
binding.shared = true;
|
|
50
|
+
const previous = shared ? this.ledger.readScopedPieceState(context.frame, binding.nodePath, 'binding:implementation') : this.ledger.readPieceState(context.frame, 'binding:implementation');
|
|
48
51
|
if (previous !== undefined && contentDigest(previous) !== contentDigest(binding))
|
|
49
52
|
throw new EngineError('scope_mismatch', 'Implementation binding changed within a frozen run');
|
|
50
|
-
if (previous === undefined)
|
|
51
|
-
|
|
53
|
+
if (previous === undefined) {
|
|
54
|
+
if (shared)
|
|
55
|
+
this.ledger.writeScopedPieceState(context.frame, binding.nodePath, 'binding:implementation', json(binding));
|
|
56
|
+
else
|
|
57
|
+
this.ledger.writePieceState(context.frame, 'binding:implementation', json(binding));
|
|
58
|
+
}
|
|
52
59
|
return binding;
|
|
53
60
|
}
|
|
54
61
|
candidateScope() {
|
|
@@ -58,6 +65,8 @@ export class RunPieceDependencies {
|
|
|
58
65
|
? readCandidateScope(this.context)
|
|
59
66
|
: inherited ? this.inheritedCandidateScope(inherited)
|
|
60
67
|
: { context: this.context, scopeHash: contentDigest(this.context), artifactExclusions: this.change ? ['openspec/changes/' + this.change] : [] };
|
|
68
|
+
if (inherited && this.change)
|
|
69
|
+
base.artifactExclusions = [...new Set([...base.artifactExclusions, 'openspec/changes/' + this.change])];
|
|
61
70
|
const bindings = this.implementationBindings(), repositoryExclusions = Object.fromEntries(Object.entries(base.repositoryExclusions ?? {}).map(([id, paths]) => [id, [...paths]]));
|
|
62
71
|
for (const binding of bindings) {
|
|
63
72
|
const exclusions = existsSync(path.join(binding.directory, 'state.json')) ? readCandidateScope(binding.context).artifactExclusions : ['openspec/changes/' + binding.change];
|
|
@@ -103,6 +112,17 @@ export class RunPieceDependencies {
|
|
|
103
112
|
const candidate = snapshot.candidate?.hash === hash ? snapshot.candidate : this.candidate(context.frame.transition, Number(this.ledger.run().candidate_revision) + 1);
|
|
104
113
|
return { candidate, verified: snapshot.verified?.candidateHash === hash ? snapshot.verified : null };
|
|
105
114
|
}
|
|
115
|
+
/** Write admission revokes certification; the archive may check its immutable pre-write checkpoint against a real receipt. */
|
|
116
|
+
verifiedBeforeWrite(context) {
|
|
117
|
+
const verified = context.state.$verified;
|
|
118
|
+
if (!verified || verified.candidateHash !== this.fingerprint())
|
|
119
|
+
return null;
|
|
120
|
+
const receipt = this.ledger.db.get('receipts', { receipt_id: verified.receiptId });
|
|
121
|
+
if (!receipt || receipt.run_id !== this.ledger.runId || receipt.valid !== 1 || receipt.candidate_hash !== verified.candidateHash)
|
|
122
|
+
return null;
|
|
123
|
+
const evidence = JSON.parse(String(receipt.receipt_json));
|
|
124
|
+
return evidence.scope === 'full' ? verified : null;
|
|
125
|
+
}
|
|
106
126
|
/** Only the execution owner proposes protected candidate/receipt channels. */
|
|
107
127
|
finalize(frame, result, effect) {
|
|
108
128
|
const hash = this.fingerprint(), snapshot = this.ledger.scopeSnapshot(frame.scope.id);
|
|
@@ -116,7 +136,7 @@ export class RunPieceDependencies {
|
|
|
116
136
|
const node = definitionNodeAt(this.definition, frame.nodePath);
|
|
117
137
|
const kind = node?.kind ?? this.ledger.db.get('visits', { visit_id: frame.visitId })?.kind;
|
|
118
138
|
const evidence = receipt?.evidence;
|
|
119
|
-
const certifies = result.verified !== null && ['verify', 'implementation'].includes(String(kind)) && receipt?.valid && receipt.scope === 'full' && !!evidence?.commands?.length && !evidence.unverifiedRepositories?.length;
|
|
139
|
+
const certifies = result.verified !== null && ['verify', 'implementation', 'implementation-step'].includes(String(kind)) && receipt?.valid && receipt.scope === 'full' && !!evidence?.commands?.length && !evidence.unverifiedRepositories?.length;
|
|
120
140
|
if (receipt?.valid && receipt.candidateHash !== hash)
|
|
121
141
|
return { ...result, outcome: 'fail',
|
|
122
142
|
...(effect === 'write' ? { candidate } : {}), verified: null,
|
|
@@ -127,6 +147,16 @@ export class RunPieceDependencies {
|
|
|
127
147
|
: effect === 'write' || snapshot.verified?.candidateHash !== hash ? { verified: null } : {}),
|
|
128
148
|
};
|
|
129
149
|
}
|
|
150
|
+
operationState(context) {
|
|
151
|
+
return [...this.ledger.scopeSnapshot(context.frame.scope.id).attempts].reverse()
|
|
152
|
+
.find(attempt => attempt.result?.childUpdate?.operationState !== undefined)?.result?.childUpdate;
|
|
153
|
+
}
|
|
154
|
+
artifactContracts(context) {
|
|
155
|
+
const owner = context.frame.scope.nodePathPrefix;
|
|
156
|
+
const keyOf = (key) => 'contract:artifacts:' + key;
|
|
157
|
+
return { get: key => this.ledger.readScopedPieceState(context.frame, owner, keyOf(key)),
|
|
158
|
+
set: (key, value) => this.ledger.writeScopedPieceState(context.frame, owner, keyOf(key), value) };
|
|
159
|
+
}
|
|
130
160
|
memo(context) {
|
|
131
161
|
const keyOf = (key) => 'memo:' + context.frame.visitId + ':' + key;
|
|
132
162
|
return { get: key => this.ledger.readPieceState(context.frame, keyOf(key)), set: (key, value) => this.ledger.writePieceState(context.frame, keyOf(key), value) };
|
|
@@ -144,7 +174,24 @@ export class RunPieceDependencies {
|
|
|
144
174
|
write: value => this.ledger.writeScopedPieceState(context.frame, context.frame.scope.nodePathPrefix, 'session:roles', json(value)),
|
|
145
175
|
};
|
|
146
176
|
}
|
|
147
|
-
stepContext(context) {
|
|
177
|
+
stepContext(context) {
|
|
178
|
+
const step = invocationStepContext(this.ledger, this.definition, context);
|
|
179
|
+
if (definitionNodeAt(this.definition, context.frame.nodePath)?.kind !== 'implementation-step')
|
|
180
|
+
return step;
|
|
181
|
+
const phase = (path) => {
|
|
182
|
+
const node = definitionNodeAt(this.definition, path);
|
|
183
|
+
return node?.kind === 'implementation-step' ? String(node.params.phase) : path;
|
|
184
|
+
};
|
|
185
|
+
const checkpoint = step.checkpoint;
|
|
186
|
+
return { ...step, checkpoint: { ...checkpoint,
|
|
187
|
+
nextStep: checkpoint.nextStep ? phase(checkpoint.nextStep) : null,
|
|
188
|
+
steps: Object.fromEntries(Object.entries(checkpoint.steps).map(([key, value]) => [phase(key), { ...value, id: phase(key) }])),
|
|
189
|
+
history: checkpoint.history.map(value => ({ ...value, stepId: phase(value.stepId) })),
|
|
190
|
+
events: checkpoint.events.map(value => ({ ...value, ...(value.stepId ? { stepId: phase(value.stepId) } : {}) })),
|
|
191
|
+
...(checkpoint.pendingApproval ? { pendingApproval: { ...checkpoint.pendingApproval, stepId: phase(checkpoint.pendingApproval.stepId) } } : {}),
|
|
192
|
+
...(checkpoint.pendingQuestion ? { pendingQuestion: { ...checkpoint.pendingQuestion, stepId: phase(checkpoint.pendingQuestion.stepId) } } : {}),
|
|
193
|
+
} };
|
|
194
|
+
}
|
|
148
195
|
settleResult(context, key, value, invocation) {
|
|
149
196
|
if (!invocation.invocationId)
|
|
150
197
|
throw new EngineError('invocation_mismatch', 'A response memo requires its durable invocation ID');
|
|
@@ -4,7 +4,7 @@ import type { DefinitionIssue, RoleCatalog } from './definition-types.js';
|
|
|
4
4
|
* Version of the published piece catalog and observable piece behavior. Bump
|
|
5
5
|
* when either changes; retained runtime packages preserve earlier semantics.
|
|
6
6
|
*/
|
|
7
|
-
export declare const NODE_KINDS_VERSION =
|
|
7
|
+
export declare const NODE_KINDS_VERSION = 7;
|
|
8
8
|
/** Immutable bindings supplied by Core's composition root; user definitions cannot register code. */
|
|
9
9
|
export declare class PieceRegistry {
|
|
10
10
|
private readonly entries;
|
|
@@ -11,7 +11,7 @@ function freezeData(value) {
|
|
|
11
11
|
* Version of the published piece catalog and observable piece behavior. Bump
|
|
12
12
|
* when either changes; retained runtime packages preserve earlier semantics.
|
|
13
13
|
*/
|
|
14
|
-
export const NODE_KINDS_VERSION =
|
|
14
|
+
export const NODE_KINDS_VERSION = 7;
|
|
15
15
|
/** Immutable bindings supplied by Core's composition root; user definitions cannot register code. */
|
|
16
16
|
export class PieceRegistry {
|
|
17
17
|
entries = new Map();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Piece } from '../contracts.js';
|
|
2
|
+
import type { PieceDependencyProvider } from './ports.js';
|
|
3
|
+
/** A reusable artifact invariant, with no agent roles, phase order or successor policy. */
|
|
4
|
+
export declare function artifactContractPiece(bindings: PieceDependencyProvider): Piece;
|