specrails-core 6.0.1 → 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.
Files changed (169) hide show
  1. package/README.md +3 -3
  2. package/dist/agent-runtime/capabilities.js +3 -1
  3. package/dist/agent-runtime/change-scope.js +2 -0
  4. package/dist/agent-runtime/cli-executor.d.ts +3 -0
  5. package/dist/agent-runtime/cli-executor.js +52 -14
  6. package/dist/agent-runtime/cli-process.js +6 -2
  7. package/dist/agent-runtime/cli.d.ts +15 -0
  8. package/dist/agent-runtime/cli.js +43 -9
  9. package/dist/agent-runtime/compact/chat-client.js +2 -1
  10. package/dist/agent-runtime/config.d.ts +4 -1
  11. package/dist/agent-runtime/config.js +41 -3
  12. package/dist/agent-runtime/core-host.d.ts +9 -3
  13. package/dist/agent-runtime/core-host.js +21 -18
  14. package/dist/agent-runtime/efficiency-summary.d.ts +4 -1
  15. package/dist/agent-runtime/efficiency-summary.js +30 -27
  16. package/dist/agent-runtime/efficiency-types.d.ts +4 -4
  17. package/dist/agent-runtime/efficiency.d.ts +1 -1
  18. package/dist/agent-runtime/efficiency.js +7 -6
  19. package/dist/agent-runtime/engine/__fixtures__/implementation-component.json +90 -0
  20. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.d.ts +11 -0
  21. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +60 -0
  22. package/dist/agent-runtime/engine/__fixtures__/implementation.json +43 -0
  23. package/dist/agent-runtime/engine/budget.d.ts +9 -0
  24. package/dist/agent-runtime/engine/budget.js +33 -0
  25. package/dist/agent-runtime/engine/canonical-json.d.ts +9 -0
  26. package/dist/agent-runtime/engine/canonical-json.js +162 -0
  27. package/dist/agent-runtime/engine/checkpoint/database.d.ts +67 -0
  28. package/dist/agent-runtime/engine/checkpoint/database.js +304 -0
  29. package/dist/agent-runtime/engine/checkpoint/lease.d.ts +17 -0
  30. package/dist/agent-runtime/engine/checkpoint/lease.js +82 -0
  31. package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +133 -0
  32. package/dist/agent-runtime/engine/checkpoint/ledger.js +510 -0
  33. package/dist/agent-runtime/engine/checkpoint/saver.d.ts +25 -0
  34. package/dist/agent-runtime/engine/checkpoint/saver.js +146 -0
  35. package/dist/agent-runtime/engine/cli.d.ts +10 -0
  36. package/dist/agent-runtime/engine/cli.js +172 -0
  37. package/dist/agent-runtime/engine/compiler.d.ts +23 -0
  38. package/dist/agent-runtime/engine/compiler.js +262 -0
  39. package/dist/agent-runtime/engine/composition.d.ts +16 -0
  40. package/dist/agent-runtime/engine/composition.js +28 -0
  41. package/dist/agent-runtime/engine/concurrency.d.ts +14 -0
  42. package/dist/agent-runtime/engine/concurrency.js +55 -0
  43. package/dist/agent-runtime/engine/contracts.d.ts +273 -0
  44. package/dist/agent-runtime/engine/contracts.js +11 -0
  45. package/dist/agent-runtime/engine/definition-schema.d.ts +250 -0
  46. package/dist/agent-runtime/engine/definition-schema.js +315 -0
  47. package/dist/agent-runtime/engine/definition-types.d.ts +84 -0
  48. package/dist/agent-runtime/engine/definition-types.js +3 -0
  49. package/dist/agent-runtime/engine/definition-validator.d.ts +7 -0
  50. package/dist/agent-runtime/engine/definition-validator.js +191 -0
  51. package/dist/agent-runtime/engine/efficiency-summary.d.ts +62 -0
  52. package/dist/agent-runtime/engine/efficiency-summary.js +31 -0
  53. package/dist/agent-runtime/engine/events.d.ts +19 -0
  54. package/dist/agent-runtime/engine/events.js +88 -0
  55. package/dist/agent-runtime/engine/execution.d.ts +29 -0
  56. package/dist/agent-runtime/engine/execution.js +121 -0
  57. package/dist/agent-runtime/engine/expressions.d.ts +11 -0
  58. package/dist/agent-runtime/engine/expressions.js +185 -0
  59. package/dist/agent-runtime/engine/fork.d.ts +28 -0
  60. package/dist/agent-runtime/engine/fork.js +253 -0
  61. package/dist/agent-runtime/engine/graph-description.d.ts +48 -0
  62. package/dist/agent-runtime/engine/graph-description.js +16 -0
  63. package/dist/agent-runtime/engine/implementation-compiler.d.ts +214 -0
  64. package/dist/agent-runtime/engine/implementation-compiler.js +91 -0
  65. package/dist/agent-runtime/engine/index.d.ts +14 -0
  66. package/dist/agent-runtime/engine/index.js +14 -0
  67. package/dist/agent-runtime/engine/invocation-context.d.ts +11 -0
  68. package/dist/agent-runtime/engine/invocation-context.js +130 -0
  69. package/dist/agent-runtime/engine/otel.d.ts +15 -0
  70. package/dist/agent-runtime/engine/otel.js +132 -0
  71. package/dist/agent-runtime/engine/output.d.ts +4 -0
  72. package/dist/agent-runtime/engine/output.js +73 -0
  73. package/dist/agent-runtime/engine/piece-dependencies.d.ts +54 -0
  74. package/dist/agent-runtime/engine/piece-dependencies.js +240 -0
  75. package/dist/agent-runtime/engine/piece-registry.d.ts +19 -0
  76. package/dist/agent-runtime/engine/piece-registry.js +62 -0
  77. package/dist/agent-runtime/engine/pieces/artifact-contract.d.ts +4 -0
  78. package/dist/agent-runtime/engine/pieces/artifact-contract.js +70 -0
  79. package/dist/agent-runtime/engine/pieces/assign.d.ts +4 -0
  80. package/dist/agent-runtime/engine/pieces/assign.js +35 -0
  81. package/dist/agent-runtime/engine/pieces/control.d.ts +5 -0
  82. package/dist/agent-runtime/engine/pieces/control.js +53 -0
  83. package/dist/agent-runtime/engine/pieces/decider.d.ts +5 -0
  84. package/dist/agent-runtime/engine/pieces/decider.js +70 -0
  85. package/dist/agent-runtime/engine/pieces/implementation-binding.d.ts +13 -0
  86. package/dist/agent-runtime/engine/pieces/implementation-binding.js +40 -0
  87. package/dist/agent-runtime/engine/pieces/implementation-journal.d.ts +25 -0
  88. package/dist/agent-runtime/engine/pieces/implementation-journal.js +192 -0
  89. package/dist/agent-runtime/engine/pieces/implementation-step.d.ts +5 -0
  90. package/dist/agent-runtime/engine/pieces/implementation-step.js +47 -0
  91. package/dist/agent-runtime/engine/pieces/implementation.d.ts +27 -0
  92. package/dist/agent-runtime/engine/pieces/implementation.js +177 -0
  93. package/dist/agent-runtime/engine/pieces/index.d.ts +7 -0
  94. package/dist/agent-runtime/engine/pieces/index.js +24 -0
  95. package/dist/agent-runtime/engine/pieces/openspec.d.ts +3 -0
  96. package/dist/agent-runtime/engine/pieces/openspec.js +75 -0
  97. package/dist/agent-runtime/engine/pieces/ports.d.ts +53 -0
  98. package/dist/agent-runtime/engine/pieces/ports.js +1 -0
  99. package/dist/agent-runtime/engine/pieces/project-memory.d.ts +3 -0
  100. package/dist/agent-runtime/engine/pieces/project-memory.js +13 -0
  101. package/dist/agent-runtime/engine/pieces/prompt.d.ts +5 -0
  102. package/dist/agent-runtime/engine/pieces/prompt.js +130 -0
  103. package/dist/agent-runtime/engine/pieces/role-turn.d.ts +7 -0
  104. package/dist/agent-runtime/engine/pieces/role-turn.js +73 -0
  105. package/dist/agent-runtime/engine/pieces/shared.d.ts +67 -0
  106. package/dist/agent-runtime/engine/pieces/shared.js +40 -0
  107. package/dist/agent-runtime/engine/pieces/shell.d.ts +6 -0
  108. package/dist/agent-runtime/engine/pieces/shell.js +47 -0
  109. package/dist/agent-runtime/engine/pieces/verify.d.ts +7 -0
  110. package/dist/agent-runtime/engine/pieces/verify.js +102 -0
  111. package/dist/agent-runtime/engine/preflight.d.ts +27 -0
  112. package/dist/agent-runtime/engine/preflight.js +54 -0
  113. package/dist/agent-runtime/engine/regex.d.ts +3 -0
  114. package/dist/agent-runtime/engine/regex.js +19 -0
  115. package/dist/agent-runtime/engine/run-status.d.ts +563 -0
  116. package/dist/agent-runtime/engine/run-status.js +61 -0
  117. package/dist/agent-runtime/engine/runs.d.ts +605 -0
  118. package/dist/agent-runtime/engine/runs.js +247 -0
  119. package/dist/agent-runtime/engine/state.d.ts +64 -0
  120. package/dist/agent-runtime/engine/state.js +98 -0
  121. package/dist/agent-runtime/engine/steering/inbox.d.ts +54 -0
  122. package/dist/agent-runtime/engine/steering/inbox.js +113 -0
  123. package/dist/agent-runtime/engine/storage/fork-allocation.d.ts +21 -0
  124. package/dist/agent-runtime/engine/storage/fork-allocation.js +143 -0
  125. package/dist/agent-runtime/engine/storage/private-path.d.ts +11 -0
  126. package/dist/agent-runtime/engine/storage/private-path.js +74 -0
  127. package/dist/agent-runtime/engine/store/sqlite-store.d.ts +26 -0
  128. package/dist/agent-runtime/engine/store/sqlite-store.js +227 -0
  129. package/dist/agent-runtime/evaluation-corpus.d.ts +125 -0
  130. package/dist/agent-runtime/evaluation-corpus.js +4 -0
  131. package/dist/agent-runtime/evaluation.d.ts +9 -3
  132. package/dist/agent-runtime/evaluation.js +50 -8
  133. package/dist/agent-runtime/executor-types.d.ts +53 -3
  134. package/dist/agent-runtime/executor-types.js +35 -2
  135. package/dist/agent-runtime/executors.js +2 -2
  136. package/dist/agent-runtime/graph/artifacts.d.ts +7 -0
  137. package/dist/agent-runtime/graph/artifacts.js +82 -9
  138. package/dist/agent-runtime/graph/nodes.d.ts +2 -0
  139. package/dist/agent-runtime/graph/nodes.js +2 -2
  140. package/dist/agent-runtime/graph/roles.d.ts +8 -0
  141. package/dist/agent-runtime/graph/roles.js +25 -17
  142. package/dist/agent-runtime/kimi-acp.js +6 -6
  143. package/dist/agent-runtime/openai-executor.js +17 -12
  144. package/dist/agent-runtime/openspec.d.ts +17 -2
  145. package/dist/agent-runtime/openspec.js +54 -20
  146. package/dist/agent-runtime/prompts.d.ts +4 -3
  147. package/dist/agent-runtime/prompts.js +27 -6
  148. package/dist/agent-runtime/recovery.d.ts +1 -1
  149. package/dist/agent-runtime/repository-context.js +1 -1
  150. package/dist/agent-runtime/role-routing.js +4 -2
  151. package/dist/agent-runtime/role-state.d.ts +5 -0
  152. package/dist/agent-runtime/verification-plan.d.ts +2 -0
  153. package/dist/agent-runtime/verification-plan.js +10 -0
  154. package/dist/agent-runtime/workflow-types.d.ts +2 -0
  155. package/dist/agent-runtime/workflow.d.ts +3 -1
  156. package/dist/agent-runtime/workflow.js +7 -3
  157. package/dist/agent-runtime/workspace-tools.d.ts +2 -2
  158. package/dist/agent-runtime/workspace-tools.js +6 -6
  159. package/dist/installer/cli.js +5 -0
  160. package/dist/installer/phases/scaffold.d.ts +2 -2
  161. package/dist/installer/phases/scaffold.js +3 -7
  162. package/dist/pipeline/pipeline-state.d.ts +38 -4
  163. package/dist/pipeline/pipeline-state.js +96 -21
  164. package/integration-contract.json +111 -19
  165. package/package.json +13 -6
  166. package/schemas/agent-runtime.schema.json +93 -1
  167. package/schemas/workflow-definition.schema.json +314 -0
  168. package/templates/kimi/specrails/run-skill.mjs +15 -1
  169. package/templates/commands/specrails/batch-implement.md +0 -32
@@ -0,0 +1,40 @@
1
+ import { pipelineStateDirectory } from '../../../pipeline/pipeline-state.js';
2
+ import { contentDigest } from '../canonical-json.js';
3
+ import { EngineError } from '../contracts.js';
4
+ /** Bind one journal to one declared implementation instance without changing backlog semantics. */
5
+ export function deriveImplementationBinding(parent, execution, change) {
6
+ const { frame, state } = execution;
7
+ const standalone = frame.scope.id === 'root' && !frame.nodePath.includes('/') && state.$item === null;
8
+ let specs = parent.specs, repositories = parent.repositories;
9
+ if (state.$item !== null) {
10
+ const item = state.$item.value;
11
+ if (!item || typeof item !== 'object' || Array.isArray(item))
12
+ throw new EngineError('implementation_item_scope', 'Implementation map items must identify a frozen ticket or repository');
13
+ if (typeof item.path === 'string') {
14
+ const repository = parent.repositories.find(value => value.id === item.id && value.path === item.path);
15
+ if (!repository)
16
+ throw new EngineError('implementation_item_scope', 'Repository item is outside the frozen execution scope');
17
+ repositories = [repository];
18
+ specs = parent.specs.filter(spec => !spec.repositoryIds?.length || spec.repositoryIds.includes(repository.id)).map(spec => ({ ...spec, repositoryIds: [repository.id] }));
19
+ }
20
+ else {
21
+ const spec = parent.specs.find(value => String(value.id) === String(item.id));
22
+ if (!spec || typeof item.title !== 'string')
23
+ throw new EngineError('implementation_item_scope', 'Ticket item is outside the frozen execution scope');
24
+ specs = [spec];
25
+ if (spec.repositoryIds?.length)
26
+ repositories = parent.repositories.filter(repository => spec.repositoryIds.includes(repository.id));
27
+ }
28
+ if (!specs.length || !repositories.length)
29
+ throw new EngineError('implementation_item_scope', 'Implementation item has no frozen obligations or repositories');
30
+ }
31
+ const digest = contentDigest({ parentRunId: parent.runId, scopeId: frame.scope.id, nodePath: frame.nodePath }).slice(0, 24);
32
+ const artifact = repositories.find(repository => repository.id === parent.artifactRepositoryId) ?? repositories[0];
33
+ const context = standalone ? structuredClone(parent) : { ...structuredClone(parent),
34
+ runId: parent.runId.slice(0, 80) + '-impl-' + digest,
35
+ artifactRoot: artifact.id === parent.artifactRepositoryId ? parent.artifactRoot : artifact.path,
36
+ artifactRepositoryId: artifact.id, repositories: structuredClone(repositories), specs: structuredClone(specs),
37
+ };
38
+ return { context, change: standalone ? change : change.slice(0, 38).replace(/-+$/, '') + '-' + digest,
39
+ parentRunId: parent.runId, scopeId: frame.scope.id, nodePath: frame.nodePath, directory: pipelineStateDirectory(context) };
40
+ }
@@ -0,0 +1,25 @@
1
+ import type { CoreNodeId, CoreStateType } from '../../graph/state.js';
2
+ import type { ImplementationBinding } from './implementation-binding.js';
3
+ export interface ImplementationJournalSnapshot {
4
+ schemaVersion: 1;
5
+ bindingHash: string;
6
+ files: Array<{
7
+ area: 'journal' | 'change' | 'main-spec';
8
+ path: string;
9
+ hash: string;
10
+ bytes: number;
11
+ }>;
12
+ digest: string;
13
+ }
14
+ /** Immutable bytes precede the SQLite terminal which publishes this exact manifest. */
15
+ export declare function captureImplementationJournal(binding: ImplementationBinding): ImplementationJournalSnapshot;
16
+ /** Restore only checkpoint-owned objects, preserving the source and current repository files. */
17
+ export declare function forkImplementationJournal(snapshot: ImplementationJournalSnapshot, source: ImplementationBinding, target: ImplementationBinding, reserveChange?: () => void): void;
18
+ /** Public checkpoint projection; the caller updates the actual child namespace at its cut node. */
19
+ export declare function projectImplementationFork(target: ImplementationBinding): {
20
+ next: CoreNodeId;
21
+ update: Partial<CoreStateType> & {
22
+ $next: CoreNodeId;
23
+ $nodeResult: null;
24
+ };
25
+ };
@@ -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,5 @@
1
+ import { type Piece } from '../contracts.js';
2
+ import type { PieceDependencyProvider } from './ports.js';
3
+ /** Outcomes are facts, not destinations. Only the host definition chooses the next node. */
4
+ export declare const IMPLEMENTATION_STEP_OUTCOMES: readonly ["next", "incomplete", "rejected", "replan", "reverify", "rereview", "failed"];
5
+ export declare function implementationStepPiece(bindings: PieceDependencyProvider): Piece;
@@ -0,0 +1,47 @@
1
+ import { CoreState } from '../../graph/state.js';
2
+ import { EngineError } from '../contracts.js';
3
+ import { createImplementationAdapter } from './implementation.js';
4
+ import { json, paramsSchema } from './shared.js';
5
+ /** Outcomes are facts, not destinations. Only the host definition chooses the next node. */
6
+ export const IMPLEMENTATION_STEP_OUTCOMES = ['next', 'incomplete', 'rejected', 'replan', 'reverify', 'rereview', 'failed'];
7
+ export function implementationStepPiece(bindings) {
8
+ return {
9
+ descriptor: { kind: 'implementation-step', paramsSchema: paramsSchema({
10
+ phase: { enum: ['architect', 'developer', 'fixer', 'verify', 'reviewer', 'archive'] },
11
+ }, ['phase']), outcomes: [...IMPLEMENTATION_STEP_OUTCOMES], effect: 'write', requiresAI: true },
12
+ async execute(params, context) {
13
+ const deps = bindings(), phase = params.phase;
14
+ const change = deps.change ?? context.state.$vars.changeId;
15
+ if (typeof change !== 'string')
16
+ throw new EngineError('invalid_arguments', 'Implementation operation requires a frozen change');
17
+ const saved = deps.operationState?.(context);
18
+ const resumed = [...deps.stepContext(context).checkpoint.events].reverse().find(event => event.type === 'workflow_resumed')?.timestamp ?? '';
19
+ const adapter = createImplementationAdapter(deps, { change, params: {}, independent: true, revalidate: saved?.operationResume !== resumed, archiveConsent: saved?.operationRunId === deps.context.runId && typeof saved.operationConsent === 'string' ? saved.operationConsent : undefined });
20
+ const initialized = await adapter.initialize(context);
21
+ const initial = { plan: [], unverifiedRepositories: [], architecture: null, development: null,
22
+ verifyResult: null, review: null, answers: [], deepenPasses: 0, archived: null, verifyHistory: [] };
23
+ const state = { ...initial, ...saved?.operationState,
24
+ ...(saved?.operationRunId === deps.context.runId ? {} : initialized) };
25
+ const response = await adapter.runNode(phase, state, context);
26
+ const updated = { ...state };
27
+ for (const [key, value] of Object.entries(response.update ?? {})) {
28
+ const channel = CoreState.spec[key];
29
+ updated[key] = channel.operator ? channel.operator(state[key], value) : value;
30
+ }
31
+ const normal = { architect: 'developer', developer: 'verify', fixer: 'verify', verify: 'reviewer', reviewer: 'archive', archive: null }[phase];
32
+ const next = response.next === undefined ? normal : response.next;
33
+ const outcome = response.status !== 'succeeded' ? 'failed' : next === normal ? 'next'
34
+ : next === 'architect' ? 'replan' : next === 'verify' ? 'reverify' : next === 'reviewer' ? 'rereview'
35
+ : next === 'developer' ? 'incomplete' : next === 'fixer' ? 'rejected' : 'failed';
36
+ // The summary reads host verification receipts and acceptance, never an agent's claim.
37
+ const evidence = await adapter.summarize(updated, context);
38
+ return { outcome, status: response.status,
39
+ ...(response.output === undefined ? {} : { output: json({ ...((response.output && typeof response.output === 'object' && !Array.isArray(response.output)) ? response.output : { value: response.output }), phase }) }),
40
+ ...(response.error ? { error: { code: response.status === 'blocked' ? 'implementation_blocked' : 'implementation_failed', message: response.error } } : {}),
41
+ childUpdate: { operationState: json(updated), operationRunId: deps.context.runId, operationResume: resumed, operationConsent: adapter.consentSnapshot?.(context) ?? null, journal: json(adapter.snapshot(context)) },
42
+ ...(evidence.receipt ? { receipt: evidence.receipt, verified: phase === 'archive' && outcome === 'next' ? evidence.verified : null } : {}),
43
+ ...(phase === 'archive' && outcome === 'next' ? { completion: evidence.completion } : {}),
44
+ };
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,27 @@
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
+ consentSnapshot?(context: PieceExecutionContext): string | undefined;
15
+ summarize(state: CoreStateType, context: PieceExecutionContext): Promise<PieceResult>;
16
+ validateCompleted(id: CoreNodeId, record: StepRecord, checkpoint: WorkflowState, context: PieceExecutionContext): Promise<boolean>;
17
+ }
18
+ /** Scope the legacy convergence/attempt view without changing durable event attribution. */
19
+ export declare function implementationStepContext(deps: PieceDependencies, context: PieceExecutionContext, id: CoreNodeId, independent?: boolean): WorkflowStepContext;
20
+ /** Reuse every legacy node and quality gate; this factory never runs the legacy JSON workflow host. */
21
+ export declare function createImplementationAdapter(deps: PieceDependencies, input: {
22
+ change: string;
23
+ params: JsonObject;
24
+ independent?: boolean;
25
+ revalidate?: boolean;
26
+ archiveConsent?: string;
27
+ }): ImplementationAdapter;
@@ -0,0 +1,177 @@
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, independent = false) {
19
+ const original = deps.stepContext(context);
20
+ const prefix = independent ? '' : 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) => input.independent ? context.frame.scope.id : 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, input.independent);
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: input.independent === true && input.revalidate === false, archiveConsent: input.archiveConsent });
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, input.independent);
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), propagateInterruptions: input.independent,
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
+ consentSnapshot(context) { return bindings.get(instanceKey(context))?.archiveConsent; },
134
+ snapshot(context) {
135
+ const prepared = bindings.get(context.frame.scope.id) ?? bindings.get(instanceKey(context));
136
+ if (!prepared)
137
+ throw new EngineError('implementation_not_initialized', 'Implementation snapshot requires its initialized scope');
138
+ refresh(prepared.binding, context);
139
+ return captureImplementationJournal(prepared.binding);
140
+ },
141
+ async summarize(state, context) {
142
+ const prepared = bindings.get(instanceKey(context));
143
+ if (!prepared)
144
+ throw new EngineError('implementation_not_initialized', 'Implementation summary requires its initialized scope');
145
+ refresh(prepared.binding, context);
146
+ const inspection = inspectPipeline(prepared.binding.context), pipeline = journal(prepared.binding.context);
147
+ const archived = inspection.phases.archive.status === 'done' && inspection.completion.implementation === 'complete' && inspection.verification.valid && inspection.acceptance.valid;
148
+ const outcome = archived ? 'next' : state.review?.approved === false || state.verifyResult?.valid === false ? 'rejected' : 'failed';
149
+ const receipt = pipeline.verification;
150
+ const verified = receipt !== undefined && inspection.verification.valid && !receipt.unverifiedRepositories?.length && receipt.commands.length > 0;
151
+ return { outcome, ...(outcome === 'failed' ? { status: 'failed' } : {}),
152
+ output: json({ completion: inspection.completion, review: state.review, archived: state.archived }),
153
+ completion: { ok: archived, verified, reasons: inspection.completion.reasons },
154
+ ...(receipt ? { receipt: receiptEvidence({ ...receipt, valid: inspection.verification.valid }),
155
+ verified: verified ? { receiptId: receipt.id, candidateHash: receipt.candidateHash, atTransition: context.frame.transition, revision: context.frame.transition } : null } : {}) };
156
+ },
157
+ async validateCompleted(id, record, checkpoint, context) {
158
+ const prepared = bindings.get(context.frame.scope.id);
159
+ if (!prepared)
160
+ throw new EngineError('implementation_not_initialized', 'Implementation validation requires its initialized scope');
161
+ const inspection = inspectPipeline(prepared.binding.context);
162
+ if (id === 'architect')
163
+ return inspection.phases.architect.status === 'done' && inspection.resumePhase !== 'architect';
164
+ const retryingArchive = checkpoint.nextStep === 'archive' && checkpoint.steps.archive?.status === 'failed';
165
+ if (id === 'verify' && !retryingArchive && checkpoint.status !== 'succeeded' && inspection.phases.archive.status !== 'done' && !(checkpoint.pendingApproval?.stepId === 'archive' && !checkpoint.pendingApproval.grantedAt))
166
+ return false;
167
+ if (id === 'verify')
168
+ return record.output?.valid !== false && inspection.verification.valid;
169
+ if (id === 'reviewer')
170
+ return inspection.phases.reviewer.status === 'done' && inspection.verification.valid && !['architect', 'developer', 'reviewer'].includes(inspection.resumePhase ?? '');
171
+ if (id === 'archive')
172
+ return inspection.phases.archive.status === 'done';
173
+ return true;
174
+ },
175
+ };
176
+ return adapter;
177
+ }
@@ -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,24 @@
1
+ import { artifactContractPiece } from './artifact-contract.js';
2
+ import { PieceRegistry } from '../piece-registry.js';
3
+ import { controlPieces, compositionPieces } from './control.js';
4
+ import { deciderPiece } from './decider.js';
5
+ import { openSpecPieces } from './openspec.js';
6
+ import { EngineError } from '../contracts.js';
7
+ import { promptPiece } from './prompt.js';
8
+ import { roleTurnPiece } from './role-turn.js';
9
+ import { shellPiece } from './shell.js';
10
+ import { verifyPiece } from './verify.js';
11
+ import { implementationStepPiece } from './implementation-step.js';
12
+ import { assignPiece } from './assign.js';
13
+ /** The composition root binds effects once; definitions can only select this reviewed catalog. */
14
+ export function createPieceRegistry(deps) {
15
+ return registry(() => deps);
16
+ }
17
+ /** Exactly the production descriptors and validators, without creating a host or an executor. */
18
+ export function validationPieceRegistry() {
19
+ return registry(() => { throw new EngineError('read_only', 'Catalog registry cannot execute workflow effects'); });
20
+ }
21
+ function registry(bindings) {
22
+ return new PieceRegistry([promptPiece(bindings), roleTurnPiece(bindings), deciderPiece(bindings), verifyPiece(bindings), shellPiece(bindings),
23
+ artifactContractPiece(bindings), ...openSpecPieces(bindings), ...controlPieces(bindings), assignPiece(), implementationStepPiece(bindings), ...compositionPieces()]);
24
+ }
@@ -0,0 +1,3 @@
1
+ import { type Piece } from '../contracts.js';
2
+ import type { PieceDependencyProvider } from './ports.js';
3
+ export declare function openSpecPieces(bindings: PieceDependencyProvider): Piece[];