specrails-core 6.0.1 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent-runtime/capabilities.js +3 -1
- package/dist/agent-runtime/cli-executor.d.ts +3 -0
- package/dist/agent-runtime/cli-executor.js +50 -13
- package/dist/agent-runtime/cli-process.js +6 -2
- package/dist/agent-runtime/cli.d.ts +15 -0
- package/dist/agent-runtime/cli.js +43 -9
- package/dist/agent-runtime/compact/chat-client.js +2 -1
- package/dist/agent-runtime/config.d.ts +4 -1
- package/dist/agent-runtime/config.js +41 -3
- package/dist/agent-runtime/core-host.d.ts +9 -3
- package/dist/agent-runtime/core-host.js +21 -18
- package/dist/agent-runtime/efficiency-summary.d.ts +4 -1
- package/dist/agent-runtime/efficiency-summary.js +30 -27
- package/dist/agent-runtime/efficiency-types.d.ts +4 -4
- package/dist/agent-runtime/efficiency.d.ts +1 -1
- package/dist/agent-runtime/efficiency.js +7 -6
- package/dist/agent-runtime/engine/__fixtures__/implementation-component.json +90 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.d.ts +11 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +60 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation.json +43 -0
- package/dist/agent-runtime/engine/budget.d.ts +9 -0
- package/dist/agent-runtime/engine/budget.js +33 -0
- package/dist/agent-runtime/engine/canonical-json.d.ts +9 -0
- package/dist/agent-runtime/engine/canonical-json.js +162 -0
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +67 -0
- package/dist/agent-runtime/engine/checkpoint/database.js +304 -0
- package/dist/agent-runtime/engine/checkpoint/lease.d.ts +17 -0
- package/dist/agent-runtime/engine/checkpoint/lease.js +82 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +133 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.js +510 -0
- package/dist/agent-runtime/engine/checkpoint/saver.d.ts +25 -0
- package/dist/agent-runtime/engine/checkpoint/saver.js +146 -0
- package/dist/agent-runtime/engine/cli.d.ts +10 -0
- package/dist/agent-runtime/engine/cli.js +172 -0
- package/dist/agent-runtime/engine/compiler.d.ts +23 -0
- package/dist/agent-runtime/engine/compiler.js +261 -0
- package/dist/agent-runtime/engine/composition.d.ts +16 -0
- package/dist/agent-runtime/engine/composition.js +28 -0
- package/dist/agent-runtime/engine/concurrency.d.ts +14 -0
- package/dist/agent-runtime/engine/concurrency.js +55 -0
- package/dist/agent-runtime/engine/contracts.d.ts +273 -0
- package/dist/agent-runtime/engine/contracts.js +11 -0
- package/dist/agent-runtime/engine/definition-schema.d.ts +250 -0
- package/dist/agent-runtime/engine/definition-schema.js +313 -0
- package/dist/agent-runtime/engine/definition-types.d.ts +84 -0
- package/dist/agent-runtime/engine/definition-types.js +3 -0
- package/dist/agent-runtime/engine/definition-validator.d.ts +7 -0
- package/dist/agent-runtime/engine/definition-validator.js +187 -0
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +62 -0
- package/dist/agent-runtime/engine/efficiency-summary.js +31 -0
- package/dist/agent-runtime/engine/events.d.ts +19 -0
- package/dist/agent-runtime/engine/events.js +88 -0
- package/dist/agent-runtime/engine/execution.d.ts +29 -0
- package/dist/agent-runtime/engine/execution.js +121 -0
- package/dist/agent-runtime/engine/expressions.d.ts +11 -0
- package/dist/agent-runtime/engine/expressions.js +172 -0
- package/dist/agent-runtime/engine/fork.d.ts +28 -0
- package/dist/agent-runtime/engine/fork.js +211 -0
- package/dist/agent-runtime/engine/graph-description.d.ts +48 -0
- package/dist/agent-runtime/engine/graph-description.js +16 -0
- package/dist/agent-runtime/engine/implementation-compiler.d.ts +214 -0
- package/dist/agent-runtime/engine/implementation-compiler.js +91 -0
- package/dist/agent-runtime/engine/index.d.ts +14 -0
- package/dist/agent-runtime/engine/index.js +14 -0
- package/dist/agent-runtime/engine/invocation-context.d.ts +11 -0
- package/dist/agent-runtime/engine/invocation-context.js +128 -0
- package/dist/agent-runtime/engine/otel.d.ts +15 -0
- package/dist/agent-runtime/engine/otel.js +132 -0
- package/dist/agent-runtime/engine/output.d.ts +4 -0
- package/dist/agent-runtime/engine/output.js +73 -0
- package/dist/agent-runtime/engine/piece-dependencies.d.ts +50 -0
- package/dist/agent-runtime/engine/piece-dependencies.js +193 -0
- package/dist/agent-runtime/engine/piece-registry.d.ts +19 -0
- package/dist/agent-runtime/engine/piece-registry.js +62 -0
- package/dist/agent-runtime/engine/pieces/assign.d.ts +4 -0
- package/dist/agent-runtime/engine/pieces/assign.js +35 -0
- package/dist/agent-runtime/engine/pieces/control.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/control.js +49 -0
- package/dist/agent-runtime/engine/pieces/decider.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/decider.js +70 -0
- package/dist/agent-runtime/engine/pieces/implementation-binding.d.ts +12 -0
- package/dist/agent-runtime/engine/pieces/implementation-binding.js +40 -0
- package/dist/agent-runtime/engine/pieces/implementation-journal.d.ts +25 -0
- package/dist/agent-runtime/engine/pieces/implementation-journal.js +192 -0
- package/dist/agent-runtime/engine/pieces/implementation.d.ts +23 -0
- package/dist/agent-runtime/engine/pieces/implementation.js +176 -0
- package/dist/agent-runtime/engine/pieces/index.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/index.js +22 -0
- package/dist/agent-runtime/engine/pieces/openspec.d.ts +3 -0
- package/dist/agent-runtime/engine/pieces/openspec.js +69 -0
- package/dist/agent-runtime/engine/pieces/ports.d.ts +48 -0
- package/dist/agent-runtime/engine/pieces/ports.js +1 -0
- package/dist/agent-runtime/engine/pieces/project-memory.d.ts +3 -0
- package/dist/agent-runtime/engine/pieces/project-memory.js +13 -0
- package/dist/agent-runtime/engine/pieces/prompt.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/prompt.js +130 -0
- package/dist/agent-runtime/engine/pieces/role-turn.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +73 -0
- package/dist/agent-runtime/engine/pieces/shared.d.ts +67 -0
- package/dist/agent-runtime/engine/pieces/shared.js +40 -0
- package/dist/agent-runtime/engine/pieces/shell.d.ts +6 -0
- package/dist/agent-runtime/engine/pieces/shell.js +47 -0
- package/dist/agent-runtime/engine/pieces/verify.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/verify.js +52 -0
- package/dist/agent-runtime/engine/preflight.d.ts +27 -0
- package/dist/agent-runtime/engine/preflight.js +54 -0
- package/dist/agent-runtime/engine/regex.d.ts +3 -0
- package/dist/agent-runtime/engine/regex.js +19 -0
- package/dist/agent-runtime/engine/run-status.d.ts +563 -0
- package/dist/agent-runtime/engine/run-status.js +61 -0
- package/dist/agent-runtime/engine/runs.d.ts +605 -0
- package/dist/agent-runtime/engine/runs.js +247 -0
- package/dist/agent-runtime/engine/state.d.ts +64 -0
- package/dist/agent-runtime/engine/state.js +98 -0
- package/dist/agent-runtime/engine/steering/inbox.d.ts +54 -0
- package/dist/agent-runtime/engine/steering/inbox.js +113 -0
- package/dist/agent-runtime/engine/storage/fork-allocation.d.ts +21 -0
- package/dist/agent-runtime/engine/storage/fork-allocation.js +143 -0
- package/dist/agent-runtime/engine/storage/private-path.d.ts +11 -0
- package/dist/agent-runtime/engine/storage/private-path.js +74 -0
- package/dist/agent-runtime/engine/store/sqlite-store.d.ts +26 -0
- package/dist/agent-runtime/engine/store/sqlite-store.js +227 -0
- package/dist/agent-runtime/evaluation-corpus.d.ts +125 -0
- package/dist/agent-runtime/evaluation-corpus.js +4 -0
- package/dist/agent-runtime/evaluation.d.ts +9 -3
- package/dist/agent-runtime/evaluation.js +50 -8
- package/dist/agent-runtime/executor-types.d.ts +53 -3
- package/dist/agent-runtime/executor-types.js +35 -2
- package/dist/agent-runtime/executors.js +2 -2
- package/dist/agent-runtime/graph/artifacts.d.ts +7 -0
- package/dist/agent-runtime/graph/artifacts.js +82 -9
- package/dist/agent-runtime/graph/nodes.d.ts +2 -0
- package/dist/agent-runtime/graph/nodes.js +2 -2
- package/dist/agent-runtime/graph/roles.d.ts +6 -0
- package/dist/agent-runtime/graph/roles.js +23 -17
- package/dist/agent-runtime/kimi-acp.js +6 -6
- package/dist/agent-runtime/openai-executor.js +17 -12
- package/dist/agent-runtime/openspec.d.ts +17 -2
- package/dist/agent-runtime/openspec.js +54 -20
- package/dist/agent-runtime/prompts.d.ts +4 -3
- package/dist/agent-runtime/prompts.js +26 -5
- package/dist/agent-runtime/recovery.d.ts +1 -1
- package/dist/agent-runtime/repository-context.js +1 -1
- package/dist/agent-runtime/role-routing.js +4 -2
- package/dist/agent-runtime/role-state.d.ts +5 -0
- package/dist/agent-runtime/verification-plan.d.ts +2 -0
- package/dist/agent-runtime/verification-plan.js +10 -0
- package/dist/agent-runtime/workflow-types.d.ts +2 -0
- package/dist/agent-runtime/workflow.d.ts +3 -1
- package/dist/agent-runtime/workflow.js +7 -3
- package/dist/agent-runtime/workspace-tools.d.ts +2 -2
- package/dist/agent-runtime/workspace-tools.js +6 -6
- package/dist/installer/cli.js +5 -0
- package/dist/installer/phases/scaffold.d.ts +2 -2
- package/dist/installer/phases/scaffold.js +3 -7
- package/dist/pipeline/pipeline-state.d.ts +38 -4
- package/dist/pipeline/pipeline-state.js +94 -21
- package/integration-contract.json +83 -11
- package/package.json +12 -5
- package/schemas/agent-runtime.schema.json +93 -1
- package/schemas/workflow-definition.schema.json +312 -0
- package/templates/kimi/specrails/run-skill.mjs +15 -1
- package/templates/commands/specrails/batch-implement.md +0 -32
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { initializePipeline, inspectPipeline, pipelineStateDirectory, validatePipelineContext } from '../../../pipeline/pipeline-state.js';
|
|
5
|
+
import { child, write } from '../../graph/artifacts.js';
|
|
6
|
+
import { expandedPlanCommands, forkVerificationPlan, readVerificationPlan } from '../../verification-plan.js';
|
|
7
|
+
import { contentDigest } from '../canonical-json.js';
|
|
8
|
+
import { EngineError } from '../contracts.js';
|
|
9
|
+
const MAX_FILES = 8192, MAX_BYTES = 256 * 1024 * 1024, MAX_MANIFEST = 1024 * 1024;
|
|
10
|
+
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
11
|
+
const objects = (binding) => child(binding.directory, 'agent-workflow/implementation-snapshots/objects');
|
|
12
|
+
function fail(message) { throw new EngineError('implementation_snapshot_invalid', message); }
|
|
13
|
+
function validateBinding(binding) {
|
|
14
|
+
const context = validatePipelineContext(binding.context);
|
|
15
|
+
if (path.resolve(binding.directory) !== pipelineStateDirectory(context) || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(binding.change))
|
|
16
|
+
fail('Snapshot journal binding is invalid');
|
|
17
|
+
}
|
|
18
|
+
function durableBlob(directory, digest, bytes) {
|
|
19
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
20
|
+
const target = child(directory, digest);
|
|
21
|
+
if (existsSync(target)) {
|
|
22
|
+
if (hash(readFileSync(target)) !== digest)
|
|
23
|
+
fail('Immutable snapshot object changed');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const temporary = child(directory, digest + '.' + randomUUID() + '.tmp');
|
|
27
|
+
const fd = openSync(temporary, 'wx', 0o600);
|
|
28
|
+
try {
|
|
29
|
+
writeFileSync(fd, bytes);
|
|
30
|
+
fsyncSync(fd);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
closeSync(fd);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
renameSync(temporary, target);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
rmSync(temporary, { force: true });
|
|
40
|
+
}
|
|
41
|
+
// Windows does not support opening directories for fsync; file contents are
|
|
42
|
+
// already flushed and rename is atomic. Unix also persists the directory entry.
|
|
43
|
+
if (process.platform !== 'win32') {
|
|
44
|
+
const dir = openSync(directory, 'r');
|
|
45
|
+
try {
|
|
46
|
+
fsyncSync(dir);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
closeSync(dir);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Immutable bytes precede the SQLite terminal which publishes this exact manifest. */
|
|
54
|
+
export function captureImplementationJournal(binding) {
|
|
55
|
+
validateBinding(binding);
|
|
56
|
+
const journal = inspectPipeline(binding.context);
|
|
57
|
+
const state = JSON.parse(readFileSync(child(binding.directory, 'state.json'), 'utf8'));
|
|
58
|
+
if (state.change !== binding.change || journal.runId !== binding.context.runId)
|
|
59
|
+
fail('Snapshot does not belong to the admitted implementation');
|
|
60
|
+
const entries = [];
|
|
61
|
+
let total = 0;
|
|
62
|
+
const add = (root, relative, area) => {
|
|
63
|
+
const target = child(root, relative);
|
|
64
|
+
if (!existsSync(target))
|
|
65
|
+
return;
|
|
66
|
+
const stat = lstatSync(target);
|
|
67
|
+
if (stat.isSymbolicLink())
|
|
68
|
+
fail('Snapshot refuses symlink artifacts');
|
|
69
|
+
if (stat.isDirectory()) {
|
|
70
|
+
for (const name of readdirSync(target).sort())
|
|
71
|
+
add(root, relative + '/' + name, area);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!stat.isFile() || entries.length >= MAX_FILES || (total += stat.size) > MAX_BYTES)
|
|
75
|
+
fail('Implementation snapshot exceeds its bounded file budget');
|
|
76
|
+
const bytes = readFileSync(target), digest = hash(bytes);
|
|
77
|
+
durableBlob(objects(binding), digest, bytes);
|
|
78
|
+
entries.push({ area, path: relative, hash: digest, bytes: bytes.length });
|
|
79
|
+
};
|
|
80
|
+
for (const relative of ['state.json', 'context.json', 'verification', 'receipts', 'openspec-archive.json', 'openspec-archive-base.json'])
|
|
81
|
+
add(binding.directory, relative, 'journal');
|
|
82
|
+
const active = child(binding.context.artifactRoot, 'openspec/changes/' + binding.change);
|
|
83
|
+
const changeRoot = existsSync(active) ? active : state.archivePath;
|
|
84
|
+
if (changeRoot && existsSync(changeRoot)) {
|
|
85
|
+
const safe = child(binding.context.artifactRoot, path.relative(binding.context.artifactRoot, changeRoot));
|
|
86
|
+
for (const name of readdirSync(safe).sort())
|
|
87
|
+
add(safe, name, 'change');
|
|
88
|
+
}
|
|
89
|
+
const mainSpecs = existsSync(child(binding.directory, 'openspec-archive-base.json'))
|
|
90
|
+
? child(binding.directory, 'agent-workflow/openspec-base-specs')
|
|
91
|
+
: child(binding.context.artifactRoot, 'openspec/specs');
|
|
92
|
+
if (existsSync(mainSpecs))
|
|
93
|
+
for (const name of readdirSync(mainSpecs).sort())
|
|
94
|
+
add(mainSpecs, name, 'main-spec');
|
|
95
|
+
const payload = { schemaVersion: 1, bindingHash: contentDigest(binding), files: entries };
|
|
96
|
+
const snapshot = { ...payload, digest: contentDigest(payload) };
|
|
97
|
+
if (Buffer.byteLength(JSON.stringify(snapshot)) > MAX_MANIFEST)
|
|
98
|
+
fail('Implementation snapshot manifest exceeds 1 MiB');
|
|
99
|
+
return snapshot;
|
|
100
|
+
}
|
|
101
|
+
/** Restore only checkpoint-owned objects, preserving the source and current repository files. */
|
|
102
|
+
export function forkImplementationJournal(snapshot, source, target, reserveChange) {
|
|
103
|
+
validateBinding(source);
|
|
104
|
+
validateBinding(target);
|
|
105
|
+
const { digest, ...payload } = snapshot;
|
|
106
|
+
if (snapshot.schemaVersion !== 1 || snapshot.bindingHash !== contentDigest(source) || digest !== contentDigest(payload)
|
|
107
|
+
|| !Array.isArray(snapshot.files) || snapshot.files.length > MAX_FILES || Buffer.byteLength(JSON.stringify(snapshot)) > MAX_MANIFEST)
|
|
108
|
+
fail('Implementation snapshot manifest failed its identity check');
|
|
109
|
+
if (source.directory === target.directory || source.context.runId === target.context.runId || source.change === target.change)
|
|
110
|
+
fail('A fork requires new journal and change identities');
|
|
111
|
+
const targetChange = child(target.context.artifactRoot, 'openspec/changes/' + target.change);
|
|
112
|
+
if (existsSync(child(target.directory, 'state.json')) || existsSync(targetChange))
|
|
113
|
+
fail('Fork refuses to overwrite an existing implementation');
|
|
114
|
+
const files = new Map(), seen = new Set();
|
|
115
|
+
let total = 0;
|
|
116
|
+
for (const item of snapshot.files) {
|
|
117
|
+
if (!['journal', 'change', 'main-spec'].includes(item.area) || !/^[a-f0-9]{64}$/.test(item.hash) || !Number.isSafeInteger(item.bytes) || item.bytes < 0 || (total += item.bytes) > MAX_BYTES)
|
|
118
|
+
fail('Snapshot object metadata is invalid');
|
|
119
|
+
const key = item.area + '/' + item.path;
|
|
120
|
+
if (seen.has(key))
|
|
121
|
+
fail('Snapshot contains duplicate paths');
|
|
122
|
+
seen.add(key);
|
|
123
|
+
child(item.area === 'journal' ? target.directory : item.area === 'change' ? targetChange : child(target.directory, 'agent-workflow/openspec-base-specs'), item.path);
|
|
124
|
+
if (item.area === 'journal' && !/^(?:state\.json|context\.json|openspec-archive(?:-base)?\.json|verification\/.+|receipts\/.+)$/.test(item.path))
|
|
125
|
+
fail('Snapshot contains an undeclared journal artifact');
|
|
126
|
+
const bytes = readFileSync(child(objects(source), item.hash));
|
|
127
|
+
if (bytes.length !== item.bytes || hash(bytes) !== item.hash)
|
|
128
|
+
fail('Snapshot object is missing or corrupt');
|
|
129
|
+
files.set(key, bytes);
|
|
130
|
+
}
|
|
131
|
+
const stateBytes = files.get('journal/state.json');
|
|
132
|
+
if (!stateBytes)
|
|
133
|
+
fail('Snapshot has no implementation journal');
|
|
134
|
+
const state = JSON.parse(stateBytes.toString('utf8'));
|
|
135
|
+
if (state.runId !== source.context.runId || state.change !== source.change)
|
|
136
|
+
fail('Snapshot state belongs to another implementation');
|
|
137
|
+
const priorBaseline = files.get('journal/openspec-archive-base.json');
|
|
138
|
+
let sourceChange = source.change;
|
|
139
|
+
if (priorBaseline) {
|
|
140
|
+
const baseline = JSON.parse(priorBaseline.toString('utf8'));
|
|
141
|
+
if (baseline.schemaVersion !== 1 || baseline.directory !== 'agent-workflow/openspec-base-specs' || typeof baseline.snapshot !== 'string' || !/^[a-f0-9]{64}$/.test(baseline.snapshot)
|
|
142
|
+
|| typeof baseline.sourceChange !== 'string' || baseline.sourceChange.length > 64 || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(baseline.sourceChange))
|
|
143
|
+
fail('Snapshot archive baseline has invalid provenance');
|
|
144
|
+
sourceChange = baseline.sourceChange;
|
|
145
|
+
}
|
|
146
|
+
reserveChange?.();
|
|
147
|
+
const fresh = initializePipeline(target.context, target.change);
|
|
148
|
+
for (const [key, bytes] of files) {
|
|
149
|
+
if (['journal/state.json', 'journal/context.json', 'journal/openspec-archive.json', 'journal/openspec-archive-base.json', 'journal/verification/plan.json'].includes(key))
|
|
150
|
+
continue;
|
|
151
|
+
const area = key.slice(0, key.indexOf('/')), relative = key.slice(key.indexOf('/') + 1);
|
|
152
|
+
const destination = child(area === 'journal' ? target.directory : area === 'change' ? targetChange : child(target.directory, 'agent-workflow/openspec-base-specs'), relative);
|
|
153
|
+
mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
154
|
+
writeFileSync(destination, bytes, { flag: 'wx', mode: 0o600 });
|
|
155
|
+
}
|
|
156
|
+
const baseSpecs = child(target.directory, 'agent-workflow/openspec-base-specs');
|
|
157
|
+
mkdirSync(baseSpecs, { recursive: true, mode: 0o700 });
|
|
158
|
+
const archiveBytes = files.get('journal/openspec-archive.json');
|
|
159
|
+
if (archiveBytes) {
|
|
160
|
+
const archive = JSON.parse(archiveBytes.toString('utf8'));
|
|
161
|
+
for (const item of archive.writes) {
|
|
162
|
+
if (!item.path.startsWith('openspec/specs/'))
|
|
163
|
+
fail('Snapshot archive baseline has an invalid path');
|
|
164
|
+
const file = child(baseSpecs, item.path.slice('openspec/specs/'.length));
|
|
165
|
+
if (item.before === null)
|
|
166
|
+
rmSync(file, { force: true });
|
|
167
|
+
else
|
|
168
|
+
write(file, item.before);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
write(child(target.directory, 'openspec-archive-base.json'), JSON.stringify({ schemaVersion: 1, snapshot: snapshot.digest, directory: 'agent-workflow/openspec-base-specs', sourceChange }) + '\n');
|
|
172
|
+
const planBytes = files.get('journal/verification/plan.json');
|
|
173
|
+
if (planBytes)
|
|
174
|
+
forkVerificationPlan(target.context, JSON.parse(planBytes.toString('utf8')), source.context);
|
|
175
|
+
const prepared = JSON.parse(readFileSync(child(target.directory, 'state.json'), 'utf8'));
|
|
176
|
+
const restored = { ...fresh, revision: state.revision + 1, updatedAt: new Date().toISOString(),
|
|
177
|
+
phases: { ...fresh.phases, architect: state.phases.architect, developer: state.phases.developer },
|
|
178
|
+
artifactExclusions: [...new Set([...state.artifactExclusions, ...fresh.artifactExclusions])],
|
|
179
|
+
...(prepared.verificationPlan ? { verificationPlan: prepared.verificationPlan } : {}) };
|
|
180
|
+
write(child(target.directory, 'state.json'), JSON.stringify(restored) + '\n');
|
|
181
|
+
write(child(target.directory, 'agent-workflow/implementation-fork.json'), JSON.stringify({ sourceRunId: source.context.runId, sourceChange: source.change, snapshot: snapshot.digest }) + '\n');
|
|
182
|
+
}
|
|
183
|
+
/** Public checkpoint projection; the caller updates the actual child namespace at its cut node. */
|
|
184
|
+
export function projectImplementationFork(target) {
|
|
185
|
+
validateBinding(target);
|
|
186
|
+
const inspection = inspectPipeline(target.context), plan = readVerificationPlan(target.context);
|
|
187
|
+
const next = inspection.phases.architect.status !== 'done' ? 'architect' : inspection.phases.developer.status !== 'done' ? 'developer' : 'verify';
|
|
188
|
+
return { next, update: { $next: next, $nodeResult: null, verifyResult: null, review: null, archived: null, verifyHistory: [],
|
|
189
|
+
...(plan ? { plan: expandedPlanCommands(target.context, plan) } : {}),
|
|
190
|
+
...(inspection.phases.architect.status !== 'done' ? { architecture: null } : {}),
|
|
191
|
+
...(inspection.phases.developer.status !== 'done' ? { development: null } : {}) } };
|
|
192
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CoreState, type CoreNodeId, type CoreStateType, type CoreStateUpdate } from '../../graph/state.js';
|
|
2
|
+
import type { NodeResult, StepRecord, WorkflowNode, WorkflowState, WorkflowStepContext } from '../../workflow-types.js';
|
|
3
|
+
import { type JsonObject, type PieceExecutionContext, type PieceResult } from '../contracts.js';
|
|
4
|
+
import type { PieceDependencies } from './ports.js';
|
|
5
|
+
import { type ImplementationJournalSnapshot } from './implementation-journal.js';
|
|
6
|
+
/** Child graph contract; the compiler supplies the actual durable LangGraph boundaries. */
|
|
7
|
+
export interface ImplementationAdapter {
|
|
8
|
+
schema: typeof CoreState;
|
|
9
|
+
nodes: Record<CoreNodeId, WorkflowNode<CoreStateType>>;
|
|
10
|
+
entry: CoreNodeId;
|
|
11
|
+
initialize(context: PieceExecutionContext): Promise<CoreStateUpdate>;
|
|
12
|
+
runNode(id: CoreNodeId, state: CoreStateType, context: PieceExecutionContext): Promise<NodeResult<CoreStateType>>;
|
|
13
|
+
snapshot(context: PieceExecutionContext): ImplementationJournalSnapshot;
|
|
14
|
+
summarize(state: CoreStateType, context: PieceExecutionContext): Promise<PieceResult>;
|
|
15
|
+
validateCompleted(id: CoreNodeId, record: StepRecord, checkpoint: WorkflowState, context: PieceExecutionContext): Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
/** Scope the legacy convergence/attempt view without changing durable event attribution. */
|
|
18
|
+
export declare function implementationStepContext(deps: PieceDependencies, context: PieceExecutionContext, id: CoreNodeId): WorkflowStepContext;
|
|
19
|
+
/** Reuse every legacy node and quality gate; this factory never runs the legacy JSON workflow host. */
|
|
20
|
+
export declare function createImplementationAdapter(deps: PieceDependencies, input: {
|
|
21
|
+
change: string;
|
|
22
|
+
params: JsonObject;
|
|
23
|
+
}): ImplementationAdapter;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { expandedPlanCommands, readVerificationPlan } from '../../verification-plan.js';
|
|
4
|
+
import { bindImplementationExclusions, initializePipeline, inspectPipeline, pipelineStateDirectory } from '../../../pipeline/pipeline-state.js';
|
|
5
|
+
import { resolveRoleDescriptor, roleIds } from '../../config.js';
|
|
6
|
+
import { journal } from '../../graph/artifacts.js';
|
|
7
|
+
import { coreNodes } from '../../graph/nodes.js';
|
|
8
|
+
import { resolveReviewPolicy } from '../../graph/review-policy.js';
|
|
9
|
+
import { createRoleInvoker } from '../../graph/roles.js';
|
|
10
|
+
import { CORE_NODE_ORDER, CoreState } from '../../graph/state.js';
|
|
11
|
+
import { prepareOpenSpec, roleOpenSpecContext } from '../../openspec.js';
|
|
12
|
+
import { EngineError } from '../contracts.js';
|
|
13
|
+
import { contentDigest } from '../canonical-json.js';
|
|
14
|
+
import { json } from './shared.js';
|
|
15
|
+
import { receiptEvidence } from './verify.js';
|
|
16
|
+
import { captureImplementationJournal } from './implementation-journal.js';
|
|
17
|
+
/** Scope the legacy convergence/attempt view without changing durable event attribution. */
|
|
18
|
+
export function implementationStepContext(deps, context, id) {
|
|
19
|
+
const original = deps.stepContext(context);
|
|
20
|
+
const prefix = context.frame.nodePath.slice(0, -id.length);
|
|
21
|
+
const local = (value) => value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
|
22
|
+
const belongs = (value) => CORE_NODE_ORDER.includes(value) || (value.startsWith(prefix) && !value.slice(prefix.length).includes('/'));
|
|
23
|
+
const checkpoint = original.checkpoint;
|
|
24
|
+
return { ...original, stepId: id, checkpoint: { ...checkpoint,
|
|
25
|
+
nextStep: checkpoint.nextStep === null ? id : local(checkpoint.nextStep),
|
|
26
|
+
steps: Object.fromEntries(Object.entries(checkpoint.steps).filter(([key]) => belongs(key)).map(([key, value]) => [local(key), { ...value, id: local(value.id) }])),
|
|
27
|
+
history: checkpoint.history.filter(entry => belongs(entry.stepId)).map(entry => ({ ...entry, stepId: local(entry.stepId) })),
|
|
28
|
+
events: checkpoint.events.filter(event => !event.stepId || belongs(event.stepId)).map(event => ({ ...event, ...(event.stepId ? { stepId: local(event.stepId) } : {}) })),
|
|
29
|
+
...(checkpoint.pendingApproval ? { pendingApproval: { ...checkpoint.pendingApproval, stepId: local(checkpoint.pendingApproval.stepId) } } : {}),
|
|
30
|
+
...(checkpoint.pendingQuestion ? { pendingQuestion: { ...checkpoint.pendingQuestion, stepId: local(checkpoint.pendingQuestion.stepId) } } : {}),
|
|
31
|
+
}, interrupt: (request) => {
|
|
32
|
+
const value = context.interrupt({ kind: request.kind, prompt: request.kind === 'question' ? request.question : request.reason,
|
|
33
|
+
nodePath: context.frame.nodePath, scopeId: context.frame.scope.id, attemptId: context.frame.attemptId });
|
|
34
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || (request.kind === 'question' ? typeof value.answer !== 'string' : value.approved !== true))
|
|
35
|
+
throw new EngineError('invalid_resume', 'Implementation interrupt requires a matching answer or approval');
|
|
36
|
+
return value;
|
|
37
|
+
} };
|
|
38
|
+
}
|
|
39
|
+
/** Reuse every legacy node and quality gate; this factory never runs the legacy JSON workflow host. */
|
|
40
|
+
export function createImplementationAdapter(deps, input) {
|
|
41
|
+
const { change, params } = input;
|
|
42
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(change) || change.length > 64)
|
|
43
|
+
throw new EngineError('invalid_arguments', 'Implementation requires an admitted OpenSpec change');
|
|
44
|
+
const config = { ...deps.config,
|
|
45
|
+
...(params.approvalBeforeArchive === undefined ? {} : { approvalBeforeArchive: params.approvalBeforeArchive === true }),
|
|
46
|
+
...(params.reviewPolicy ? { review: params.reviewPolicy } : {}) };
|
|
47
|
+
const attempts = params.attempts ?? config.limits?.maxAttempts ?? 3;
|
|
48
|
+
const bindings = new Map();
|
|
49
|
+
const instanceKey = (context) => context.frame.scope.id + '/' + context.frame.visitId;
|
|
50
|
+
const metadata = { context: deps.context, config, change, attempts, policy: resolveReviewPolicy(config), openspec: {}, focusedCorrectionEvidence: true,
|
|
51
|
+
invoke: (() => { throw new EngineError('implementation_not_initialized', 'Implementation metadata nodes must run through the durable adapter'); }), note() { } };
|
|
52
|
+
const refresh = (binding, context) => {
|
|
53
|
+
const exclusions = deps.implementationExclusions?.(context);
|
|
54
|
+
if (exclusions)
|
|
55
|
+
bindImplementationExclusions(binding.context, exclusions);
|
|
56
|
+
};
|
|
57
|
+
const consent = (binding) => {
|
|
58
|
+
const pipeline = journal(binding.context);
|
|
59
|
+
return contentDigest(json({ candidate: inspectPipeline(binding.context).candidateHash, plan: pipeline.verificationPlan?.hash ?? null,
|
|
60
|
+
criteria: pipeline.acceptance?.criteria.map(({ evidence: _evidence, ...criterion }) => criterion) ?? null,
|
|
61
|
+
checks: pipeline.acceptance?.checks.map(({ evidence: _evidence, ...check }) => check) ?? null }));
|
|
62
|
+
};
|
|
63
|
+
const adapter = {
|
|
64
|
+
schema: CoreState, nodes: coreNodes(metadata), entry: 'architect',
|
|
65
|
+
async initialize(context) {
|
|
66
|
+
context.signal.throwIfAborted();
|
|
67
|
+
const binding = deps.bindImplementation(context, change);
|
|
68
|
+
const pipeline = binding.context;
|
|
69
|
+
const state = initializePipeline(pipeline, binding.change);
|
|
70
|
+
refresh(binding, context);
|
|
71
|
+
if (state.phases.archive.status === 'done') {
|
|
72
|
+
const inspection = inspectPipeline(pipeline);
|
|
73
|
+
if (!inspection.verification.valid || !inspection.acceptance.valid || (inspection.resumePhase && !['ship', 'ci'].includes(inspection.resumePhase)))
|
|
74
|
+
throw new EngineError('incompatible_resume', 'Archived implementation evidence changed; start a new run');
|
|
75
|
+
}
|
|
76
|
+
const directory = path.join(pipelineStateDirectory(pipeline), 'agent-workflow');
|
|
77
|
+
const prepared = prepareOpenSpec(pipeline.artifactRoot, binding.change, directory);
|
|
78
|
+
const openspec = Object.fromEntries(roleIds(config).filter(role => resolveRoleDescriptor(config, role).openspecSkill).map(role => {
|
|
79
|
+
const descriptor = resolveRoleDescriptor(config, role), provider = config.providers.find(item => item.id === descriptor.provider);
|
|
80
|
+
return [role, roleOpenSpecContext(prepared, pipeline.artifactRoot, binding.change, directory, descriptor, provider?.kind === 'cli' ? provider.cli : 'claude')];
|
|
81
|
+
}));
|
|
82
|
+
for (const role of Object.keys(openspec))
|
|
83
|
+
await deps.registry.get(resolveRoleDescriptor(config, role).provider).validateOpenSpec?.(openspec[role]);
|
|
84
|
+
bindings.set(instanceKey(context), { binding, openspec, resumeChecked: false });
|
|
85
|
+
if (existsSync(path.join(directory, 'implementation-fork.json'))) {
|
|
86
|
+
const plan = readVerificationPlan(pipeline);
|
|
87
|
+
return { ...(plan ? { plan: expandedPlanCommands(pipeline, plan) } : {}), verifyResult: null, review: null, archived: null, verifyHistory: [] };
|
|
88
|
+
}
|
|
89
|
+
return {};
|
|
90
|
+
},
|
|
91
|
+
async runNode(id, state, context) {
|
|
92
|
+
const prepared = bindings.get(context.frame.scope.id);
|
|
93
|
+
if (!prepared)
|
|
94
|
+
throw new EngineError('implementation_not_initialized', 'Initialize this implementation scope before executing its child graph');
|
|
95
|
+
const { binding, openspec } = prepared;
|
|
96
|
+
refresh(binding, context);
|
|
97
|
+
const step = implementationStepContext(deps, context, id);
|
|
98
|
+
if (!prepared.resumeChecked && (step.pending || step.checkpoint.events.some(event => event.type === 'workflow_resumed'))) {
|
|
99
|
+
// Collect archive consent first, then revalidate completed verification
|
|
100
|
+
// and review exactly as the legacy host does on explicit resume.
|
|
101
|
+
if (id === 'archive' && step.pending?.kind === 'approval') {
|
|
102
|
+
step.interrupt(step.pending);
|
|
103
|
+
const inspection = inspectPipeline(binding.context);
|
|
104
|
+
if (inspection.verification.valid && inspection.acceptance.valid && inspection.phases.reviewer.status === 'done')
|
|
105
|
+
prepared.archiveConsent = consent(binding);
|
|
106
|
+
if (step.checkpoint.pendingApproval)
|
|
107
|
+
step.checkpoint.pendingApproval.grantedAt = new Date().toISOString();
|
|
108
|
+
}
|
|
109
|
+
prepared.resumeChecked = true;
|
|
110
|
+
for (const completed of CORE_NODE_ORDER) {
|
|
111
|
+
const record = step.checkpoint.steps[completed];
|
|
112
|
+
if (completed === id || record?.status !== 'succeeded')
|
|
113
|
+
continue;
|
|
114
|
+
if (!await adapter.validateCompleted(completed, record, step.checkpoint, context)) {
|
|
115
|
+
const update = completed === 'architect'
|
|
116
|
+
? { architecture: null, development: null, verifyResult: null, review: null, archived: null, plan: [] }
|
|
117
|
+
: completed === 'verify' ? { verifyResult: null, review: null, archived: null }
|
|
118
|
+
: completed === 'reviewer' ? { review: null, archived: null } : {};
|
|
119
|
+
return { status: 'succeeded', next: completed, update, output: { revalidated: false, resumeFrom: completed } };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const invoke = createRoleInvoker({ context: binding.context, config, registry: deps.registry, openspec, roleState: deps.roleState(context),
|
|
124
|
+
onAgentEvent: (role, event) => context.progress({ type: 'agent-event', payload: json({ role, event }) }) });
|
|
125
|
+
const localConfig = { ...config, verification: config.verification.filter(command => binding.context.repositories.some(repository => repository.id === command.repositoryId)) };
|
|
126
|
+
const nodes = coreNodes({ ...metadata, context: binding.context, config: localConfig, change: binding.change, openspec, invoke,
|
|
127
|
+
archiveApproved: () => prepared.archiveConsent !== undefined && prepared.archiveConsent === consent(binding),
|
|
128
|
+
note: (role, text) => context.progress({ type: 'agent-event', payload: { role, event: { kind: 'text', text } } }),
|
|
129
|
+
onVerificationOutput: text => context.progress({ type: 'verification-output', payload: { text } }),
|
|
130
|
+
});
|
|
131
|
+
return nodes[id].run(state, step);
|
|
132
|
+
},
|
|
133
|
+
snapshot(context) {
|
|
134
|
+
const prepared = bindings.get(context.frame.scope.id) ?? bindings.get(instanceKey(context));
|
|
135
|
+
if (!prepared)
|
|
136
|
+
throw new EngineError('implementation_not_initialized', 'Implementation snapshot requires its initialized scope');
|
|
137
|
+
refresh(prepared.binding, context);
|
|
138
|
+
return captureImplementationJournal(prepared.binding);
|
|
139
|
+
},
|
|
140
|
+
async summarize(state, context) {
|
|
141
|
+
const prepared = bindings.get(instanceKey(context));
|
|
142
|
+
if (!prepared)
|
|
143
|
+
throw new EngineError('implementation_not_initialized', 'Implementation summary requires its initialized scope');
|
|
144
|
+
refresh(prepared.binding, context);
|
|
145
|
+
const inspection = inspectPipeline(prepared.binding.context), pipeline = journal(prepared.binding.context);
|
|
146
|
+
const archived = inspection.phases.archive.status === 'done' && inspection.completion.implementation === 'complete' && inspection.verification.valid && inspection.acceptance.valid;
|
|
147
|
+
const outcome = archived ? 'next' : state.review?.approved === false || state.verifyResult?.valid === false ? 'rejected' : 'failed';
|
|
148
|
+
const receipt = pipeline.verification;
|
|
149
|
+
const verified = receipt !== undefined && inspection.verification.valid && !receipt.unverifiedRepositories?.length && receipt.commands.length > 0;
|
|
150
|
+
return { outcome, ...(outcome === 'failed' ? { status: 'failed' } : {}),
|
|
151
|
+
output: json({ completion: inspection.completion, review: state.review, archived: state.archived }),
|
|
152
|
+
completion: { ok: archived, verified, reasons: inspection.completion.reasons },
|
|
153
|
+
...(receipt ? { receipt: receiptEvidence({ ...receipt, valid: inspection.verification.valid }),
|
|
154
|
+
verified: verified ? { receiptId: receipt.id, candidateHash: receipt.candidateHash, atTransition: context.frame.transition, revision: context.frame.transition } : null } : {}) };
|
|
155
|
+
},
|
|
156
|
+
async validateCompleted(id, record, checkpoint, context) {
|
|
157
|
+
const prepared = bindings.get(context.frame.scope.id);
|
|
158
|
+
if (!prepared)
|
|
159
|
+
throw new EngineError('implementation_not_initialized', 'Implementation validation requires its initialized scope');
|
|
160
|
+
const inspection = inspectPipeline(prepared.binding.context);
|
|
161
|
+
if (id === 'architect')
|
|
162
|
+
return inspection.phases.architect.status === 'done' && inspection.resumePhase !== 'architect';
|
|
163
|
+
const retryingArchive = checkpoint.nextStep === 'archive' && checkpoint.steps.archive?.status === 'failed';
|
|
164
|
+
if (id === 'verify' && !retryingArchive && checkpoint.status !== 'succeeded' && inspection.phases.archive.status !== 'done' && !(checkpoint.pendingApproval?.stepId === 'archive' && !checkpoint.pendingApproval.grantedAt))
|
|
165
|
+
return false;
|
|
166
|
+
if (id === 'verify')
|
|
167
|
+
return record.output?.valid !== false && inspection.verification.valid;
|
|
168
|
+
if (id === 'reviewer')
|
|
169
|
+
return inspection.phases.reviewer.status === 'done' && inspection.verification.valid && !['architect', 'developer', 'reviewer'].includes(inspection.resumePhase ?? '');
|
|
170
|
+
if (id === 'archive')
|
|
171
|
+
return inspection.phases.archive.status === 'done';
|
|
172
|
+
return true;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
return adapter;
|
|
176
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PieceRegistry } from '../piece-registry.js';
|
|
2
|
+
import type { PieceDependencies } from './ports.js';
|
|
3
|
+
export type { PieceDependencies, PieceStatePort } from './ports.js';
|
|
4
|
+
/** The composition root binds effects once; definitions can only select this reviewed catalog. */
|
|
5
|
+
export declare function createPieceRegistry(deps: PieceDependencies): PieceRegistry;
|
|
6
|
+
/** Exactly the production descriptors and validators, without creating a host or an executor. */
|
|
7
|
+
export declare function validationPieceRegistry(): PieceRegistry;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PieceRegistry } from '../piece-registry.js';
|
|
2
|
+
import { controlPieces, compositionPieces } from './control.js';
|
|
3
|
+
import { deciderPiece } from './decider.js';
|
|
4
|
+
import { openSpecPieces } from './openspec.js';
|
|
5
|
+
import { EngineError } from '../contracts.js';
|
|
6
|
+
import { promptPiece } from './prompt.js';
|
|
7
|
+
import { roleTurnPiece } from './role-turn.js';
|
|
8
|
+
import { shellPiece } from './shell.js';
|
|
9
|
+
import { verifyPiece } from './verify.js';
|
|
10
|
+
import { assignPiece } from './assign.js';
|
|
11
|
+
/** The composition root binds effects once; definitions can only select this reviewed catalog. */
|
|
12
|
+
export function createPieceRegistry(deps) {
|
|
13
|
+
return registry(() => deps);
|
|
14
|
+
}
|
|
15
|
+
/** Exactly the production descriptors and validators, without creating a host or an executor. */
|
|
16
|
+
export function validationPieceRegistry() {
|
|
17
|
+
return registry(() => { throw new EngineError('read_only', 'Catalog registry cannot execute workflow effects'); });
|
|
18
|
+
}
|
|
19
|
+
function registry(bindings) {
|
|
20
|
+
return new PieceRegistry([promptPiece(bindings), roleTurnPiece(bindings), deciderPiece(bindings), verifyPiece(bindings), shellPiece(bindings),
|
|
21
|
+
...openSpecPieces(bindings), ...controlPieces(bindings), assignPiece(), ...compositionPieces()]);
|
|
22
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { archivedOpenSpecChange, archiveOpenSpecChange } from '../../graph/artifacts.js';
|
|
2
|
+
import { OpenSpecCommandError, resolveOpenSpecCli, runOpenSpec } from '../../openspec.js';
|
|
3
|
+
import { EngineError } from '../contracts.js';
|
|
4
|
+
import { boundedText, json, paramsSchema, text } from './shared.js';
|
|
5
|
+
const schema = paramsSchema({ change: { type: 'string', minLength: 1, maxLength: 128 }, allowArchived: { type: 'boolean' }, repositoryId: { type: 'string', minLength: 1, maxLength: 128 } }, ['change']);
|
|
6
|
+
function changeId(value) {
|
|
7
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value) || value.length > 64)
|
|
8
|
+
throw new EngineError('invalid_arguments', 'Invalid OpenSpec change identifier');
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
export function openSpecPieces(bindings) {
|
|
12
|
+
return [{
|
|
13
|
+
descriptor: { kind: 'openspec-validate', paramsSchema: schema, outcomes: ['pass', 'fail', 'failed'], effect: 'read', requiresAI: false },
|
|
14
|
+
async execute(params, context) {
|
|
15
|
+
const deps = bindings();
|
|
16
|
+
if (params.repositoryId !== undefined && params.repositoryId !== deps.context.artifactRepositoryId)
|
|
17
|
+
throw new EngineError('artifact_scope_mismatch', 'OpenSpec repository must match the frozen artifact repository');
|
|
18
|
+
const change = changeId(text(params.change));
|
|
19
|
+
const archived = params.allowArchived === true ? archivedOpenSpecChange(deps.context, change) : undefined;
|
|
20
|
+
if (archived)
|
|
21
|
+
return { outcome: 'pass', output: { change, archived, skipped: true } };
|
|
22
|
+
let stdout, exited = true;
|
|
23
|
+
try {
|
|
24
|
+
stdout = await runOpenSpec(resolveOpenSpecCli(), deps.context.artifactRoot, ['validate', change, '--strict', '--json'], context.signal);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (!(error instanceof OpenSpecCommandError) || error.exitCode === undefined || error.exitCode < 1)
|
|
28
|
+
throw error;
|
|
29
|
+
stdout = error.stdout;
|
|
30
|
+
exited = false;
|
|
31
|
+
}
|
|
32
|
+
let report;
|
|
33
|
+
try {
|
|
34
|
+
report = JSON.parse(stdout);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new EngineError('openspec_invalid_output', 'OpenSpec did not return its validation report');
|
|
38
|
+
}
|
|
39
|
+
const item = report.items?.find(entry => entry.id === change && entry.type === 'change');
|
|
40
|
+
if (!item || typeof item.valid !== 'boolean')
|
|
41
|
+
throw new EngineError('openspec_invalid_output', 'OpenSpec validation did not report the requested change');
|
|
42
|
+
return { outcome: exited && item.valid ? 'pass' : 'fail', output: json(report) };
|
|
43
|
+
},
|
|
44
|
+
}, {
|
|
45
|
+
descriptor: { kind: 'openspec-archive', paramsSchema: schema, outcomes: ['next', 'failed'], effect: 'write', requiresAI: false },
|
|
46
|
+
async execute(params, context) {
|
|
47
|
+
const deps = bindings();
|
|
48
|
+
if (params.repositoryId !== undefined && params.repositoryId !== deps.context.artifactRepositoryId)
|
|
49
|
+
throw new EngineError('artifact_scope_mismatch', 'OpenSpec repository must match the frozen artifact repository');
|
|
50
|
+
const change = changeId(text(params.change)), initial = deps.executionSnapshot(context).candidate?.hash;
|
|
51
|
+
const archived = params.allowArchived === true ? archivedOpenSpecChange(deps.context, change) : undefined;
|
|
52
|
+
if (archived)
|
|
53
|
+
return { outcome: 'next', output: { change, archived, skipped: true } };
|
|
54
|
+
try {
|
|
55
|
+
await archiveOpenSpecChange(deps.context, change, { directory: deps.artifactDirectory(context), beforePublish: () => {
|
|
56
|
+
if (initial !== deps.executionSnapshot(context).candidate?.hash)
|
|
57
|
+
throw new EngineError('candidate_changed', 'Candidate changed while preparing the OpenSpec archive');
|
|
58
|
+
} }, context.signal);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (!(error instanceof OpenSpecCommandError) || error.exitCode === undefined || error.exitCode < 1)
|
|
62
|
+
throw error;
|
|
63
|
+
return { outcome: 'failed', status: 'failed', error: { code: 'openspec_command_failed', message: boundedText(error.message, 4000) },
|
|
64
|
+
output: { change, exitCode: error.exitCode, stdout: boundedText(error.stdout) } };
|
|
65
|
+
}
|
|
66
|
+
return { outcome: 'next', output: { change, archived: true }, history: [{ id: context.frame.attemptId + ':archive', transition: context.frame.transition, attempt: context.frame.attempt, ordinal: 0, nodePath: context.frame.nodePath, text: boundedText('Archived OpenSpec change ' + change) }] };
|
|
67
|
+
},
|
|
68
|
+
}];
|
|
69
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PipelineContext, VerificationEvidencePort } from '../../../pipeline/pipeline-state.js';
|
|
2
|
+
import type { RuntimeConfig } from '../../executor-types.js';
|
|
3
|
+
import type { ExecutorRegistry } from '../../executors.js';
|
|
4
|
+
import type { ProviderInvocation } from '../../efficiency-types.js';
|
|
5
|
+
import type { OpenSpecRoleContext } from '../../openspec.js';
|
|
6
|
+
import type { RoleStatePort } from '../../role-state.js';
|
|
7
|
+
import type { WorkflowStepContext } from '../../workflow-types.js';
|
|
8
|
+
import type { CandidateState, JsonValue, PieceExecutionContext, VerifiedState } from '../contracts.js';
|
|
9
|
+
import type { ProjectMemory } from '../store/sqlite-store.js';
|
|
10
|
+
import type { ImplementationBinding } from './implementation-binding.js';
|
|
11
|
+
/** One physical attempt's durable notes; never a cache shared between providers or runs. */
|
|
12
|
+
export interface PieceStatePort {
|
|
13
|
+
get(key: string): JsonValue | undefined;
|
|
14
|
+
set(key: string, value: JsonValue): void;
|
|
15
|
+
}
|
|
16
|
+
export interface PieceDependencies {
|
|
17
|
+
context: PipelineContext;
|
|
18
|
+
config: RuntimeConfig;
|
|
19
|
+
registry: ExecutorRegistry;
|
|
20
|
+
stepContext(context: PieceExecutionContext): WorkflowStepContext;
|
|
21
|
+
roleState(context: PieceExecutionContext): RoleStatePort;
|
|
22
|
+
memo(context: PieceExecutionContext): PieceStatePort;
|
|
23
|
+
/** Narrow project memory is restricted by the declared piece and namespace capability. */
|
|
24
|
+
memory(context: PieceExecutionContext): ProjectMemory;
|
|
25
|
+
/** Provider settlement and the response needed for interrupt replay share one transaction. */
|
|
26
|
+
settleResult(context: PieceExecutionContext, key: string, value: JsonValue, invocation: ProviderInvocation): void;
|
|
27
|
+
executionSnapshot(context: PieceExecutionContext): {
|
|
28
|
+
candidate: CandidateState | null;
|
|
29
|
+
verified: VerifiedState | null;
|
|
30
|
+
};
|
|
31
|
+
verification(context: PieceExecutionContext): VerificationEvidencePort;
|
|
32
|
+
/** A scoped adapter directory for recoverable OpenSpec write sets, never state.json. */
|
|
33
|
+
artifactDirectory(context: PieceExecutionContext): string;
|
|
34
|
+
/** Registers parent-owned scoped journals for candidate inspection, recovery and fork. */
|
|
35
|
+
bindImplementation(context: PieceExecutionContext, change: string): ImplementationBinding;
|
|
36
|
+
/** Current union of declared implementation artifacts and journals, including the parent run. */
|
|
37
|
+
implementationExclusions?(context: PieceExecutionContext): {
|
|
38
|
+
runtimeExclusions: readonly string[];
|
|
39
|
+
repositoryExclusions: Readonly<Record<string, readonly string[]>>;
|
|
40
|
+
};
|
|
41
|
+
openspec?(context: PieceExecutionContext): Record<string, OpenSpecRoleContext>;
|
|
42
|
+
policies?: {
|
|
43
|
+
historyMaxChars?: number;
|
|
44
|
+
noProgress?: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Evaluated only at execution; catalog listing and validation are effect-free. */
|
|
48
|
+
export type PieceDependencyProvider = () => PieceDependencies;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PieceExecutionContext } from '../contracts.js';
|
|
2
|
+
/** Project notes are advisory: failure must never replay an already billed provider response. */
|
|
3
|
+
export declare function advisoryMemory<T>(context: PieceExecutionContext, operation: () => Promise<T>): Promise<T | undefined>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Project notes are advisory: failure must never replay an already billed provider response. */
|
|
2
|
+
export async function advisoryMemory(context, operation) {
|
|
3
|
+
try {
|
|
4
|
+
return await operation();
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
try {
|
|
8
|
+
context.progress({ type: 'span', payload: { name: 'project-memory', status: 'unavailable', nodePath: context.frame.nodePath } });
|
|
9
|
+
}
|
|
10
|
+
catch { /* Diagnostics cannot fail the node. */ }
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Piece } from '../contracts.js';
|
|
2
|
+
import type { PieceDependencyProvider } from './ports.js';
|
|
3
|
+
export declare function verificationSentinel(output: string): 'pass' | 'fail' | undefined;
|
|
4
|
+
/** Free turns share accounting and executor protections, without adding role instructions. */
|
|
5
|
+
export declare function promptPiece(bindings: PieceDependencyProvider): Piece;
|