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.
- package/README.md +3 -3
- package/dist/agent-runtime/capabilities.js +3 -1
- package/dist/agent-runtime/change-scope.js +2 -0
- package/dist/agent-runtime/cli-executor.d.ts +3 -0
- package/dist/agent-runtime/cli-executor.js +52 -14
- 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 +262 -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 +315 -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 +191 -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 +185 -0
- package/dist/agent-runtime/engine/fork.d.ts +28 -0
- package/dist/agent-runtime/engine/fork.js +253 -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 +130 -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 +54 -0
- package/dist/agent-runtime/engine/piece-dependencies.js +240 -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/artifact-contract.d.ts +4 -0
- package/dist/agent-runtime/engine/pieces/artifact-contract.js +70 -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 +53 -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 +13 -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-step.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/implementation-step.js +47 -0
- package/dist/agent-runtime/engine/pieces/implementation.d.ts +27 -0
- package/dist/agent-runtime/engine/pieces/implementation.js +177 -0
- package/dist/agent-runtime/engine/pieces/index.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/index.js +24 -0
- package/dist/agent-runtime/engine/pieces/openspec.d.ts +3 -0
- package/dist/agent-runtime/engine/pieces/openspec.js +75 -0
- package/dist/agent-runtime/engine/pieces/ports.d.ts +53 -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 +102 -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 +8 -0
- package/dist/agent-runtime/graph/roles.js +25 -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 +27 -6
- 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 +96 -21
- package/integration-contract.json +111 -19
- package/package.json +13 -6
- package/schemas/agent-runtime.schema.json +93 -1
- package/schemas/workflow-definition.schema.json +314 -0
- package/templates/kimi/specrails/run-skill.mjs +15 -1
- package/templates/commands/specrails/batch-implement.md +0 -32
|
@@ -10,6 +10,13 @@ import { ExecutorRegistry } from './executors.js';
|
|
|
10
10
|
import { OpenSpecTools } from './openspec.js';
|
|
11
11
|
import { validatePipelineContext } from '../pipeline/pipeline-state.js';
|
|
12
12
|
import { runtimeEfficiency } from './efficiency.js';
|
|
13
|
+
import { validateWorkflowDefinition } from './engine/definition-validator.js';
|
|
14
|
+
import { validationPieceRegistry } from './engine/pieces/index.js';
|
|
15
|
+
import { configuredRoles } from './engine/preflight.js';
|
|
16
|
+
import { createRun, definitionRunDirectory } from './engine/runs.js';
|
|
17
|
+
import { observeLedger } from './engine/run-status.js';
|
|
18
|
+
import { RunDatabase } from './engine/checkpoint/database.js';
|
|
19
|
+
import { ledgerWorkflowState } from './engine/invocation-context.js';
|
|
13
20
|
function oracle(test, file) {
|
|
14
21
|
return spawnSync(process.execPath, ['-e', 'const assert = require("node:assert/strict"); const api = require(process.argv[1]);' + test.oracle, file], { encoding: 'utf8', timeout: 10000 }).status === 0;
|
|
15
22
|
}
|
|
@@ -26,12 +33,13 @@ function prepare(test, root) {
|
|
|
26
33
|
});
|
|
27
34
|
return validatePipelineContext({ schemaVersion: 1, runId: randomUUID(), backlogRoot: root, artifactRoot: repositories[0].path, artifactRepositoryId: repositories[0].id, repositories, ownership: { git: 'host', backlog: 'host', worktrees: 'host' }, specs: [{ id: 1, title: test.id, description: test.description + ' Implement behavior in implementation.cjs in every selected repository.', repositoryIds: test.repositories, acceptanceCriteria: [test.description] }] });
|
|
28
35
|
}
|
|
29
|
-
function fixtureRegistry(test, context) {
|
|
36
|
+
function fixtureRegistry(test, context, onPrompt) {
|
|
30
37
|
let developers = 0, reviews = 0;
|
|
31
38
|
const sessions = new Map();
|
|
32
39
|
return new ExecutorRegistry().register('fixture', {
|
|
33
40
|
capabilities: () => ({ transport: 'offline-fixture', continuation: 'supported', effortSupport: 'unsupported', supportedEfforts: [], observedModel: false, observedEffort: false }),
|
|
34
41
|
async execute(request) {
|
|
42
|
+
onPrompt?.(request);
|
|
35
43
|
const sessionId = 'fixture-' + request.role;
|
|
36
44
|
if (request.resumeSessionId && !sessions.has(request.resumeSessionId))
|
|
37
45
|
throw new Error('Fixture session was not restored');
|
|
@@ -57,11 +65,11 @@ function fixtureRegistry(test, context) {
|
|
|
57
65
|
}
|
|
58
66
|
else {
|
|
59
67
|
await tools.execute({ action: 'instructions', artifact: 'apply' });
|
|
60
|
-
if (request.role === 'developer') {
|
|
68
|
+
if (request.role === 'developer' || request.role === 'fixer') {
|
|
61
69
|
developers++;
|
|
62
70
|
for (const repo of context.repositories)
|
|
63
71
|
writeFileSync(path.join(repo.path, 'implementation.cjs'), test.correction && developers === 1 ? test.defects[0] : test.solution);
|
|
64
|
-
|
|
72
|
+
await tools.execute({ action: 'write_artifact', path: 'tasks.md', content: '## 1. Implementation\n- [x] 1.1 Implement and test the requested behavior\n' });
|
|
65
73
|
output = { summary: 'Implemented fixture behavior', files: ['implementation.cjs'], tests: [], verification: 'Core executes the frozen checks', incomplete: [] };
|
|
66
74
|
}
|
|
67
75
|
else {
|
|
@@ -75,18 +83,23 @@ function fixtureRegistry(test, context) {
|
|
|
75
83
|
});
|
|
76
84
|
}
|
|
77
85
|
export async function runEvaluation(options) {
|
|
86
|
+
if (options.capturePrompts && options.real)
|
|
87
|
+
throw new Error('Prompt capture is limited to offline fixture evaluation');
|
|
78
88
|
const repetitions = options.repetitions ?? 1;
|
|
79
89
|
if (!Number.isSafeInteger(repetitions) || repetitions < 1 || repetitions > 20)
|
|
80
90
|
throw new Error('Evaluation repetitions must be 1–20');
|
|
81
91
|
if (options.real && (!options.config || !Number.isFinite(options.maxCostUsd) || options.maxCostUsd <= 0 || Object.values(options.config.agents).some(role => !role.model)))
|
|
82
92
|
throw new Error('Real evaluation requires explicit per-role models and a positive aggregate spend limit');
|
|
93
|
+
const corpus = options.caseIds ? EVALUATION_CORPUS.filter(item => options.caseIds.includes(item.id)) : EVALUATION_CORPUS;
|
|
94
|
+
if (!corpus.length || options.caseIds?.some(id => !corpus.some(item => item.id === id)))
|
|
95
|
+
throw new Error('Unknown or empty evaluation case selection');
|
|
83
96
|
const identity = coreRuntimeIdentity();
|
|
84
97
|
const observations = [];
|
|
85
|
-
let spend = 0, stopReason = null;
|
|
98
|
+
let spend = 0, stopReason = null, evaluatedDefinitionVersion = null;
|
|
86
99
|
const root = mkdtempSync(path.join(tmpdir(), 'specrails-evaluation-'));
|
|
87
100
|
try {
|
|
88
101
|
for (let repeat = 0; repeat < repetitions && !stopReason; repeat++)
|
|
89
|
-
for (const test of
|
|
102
|
+
for (const test of corpus) {
|
|
90
103
|
// Check the independent oracle itself against defective candidates.
|
|
91
104
|
const probe = path.join(root, 'oracle-probe.cjs');
|
|
92
105
|
writeFileSync(probe, test.solution);
|
|
@@ -113,7 +126,33 @@ export async function runEvaluation(options) {
|
|
|
113
126
|
const started = Date.now();
|
|
114
127
|
let state;
|
|
115
128
|
try {
|
|
116
|
-
|
|
129
|
+
let promptNumber = 0;
|
|
130
|
+
const capture = options.capturePrompts ? (request) => {
|
|
131
|
+
const directory = path.join(options.output, 'prompts', test.id + '-' + repeat + '-' + mode);
|
|
132
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
133
|
+
const filename = String(++promptNumber).padStart(2, '0') + '-' + request.role + (request.stance ? '-' + request.stance : '');
|
|
134
|
+
writeFileSync(path.join(directory, filename + '.txt'), request.prompt, { mode: 0o600 });
|
|
135
|
+
writeFileSync(path.join(directory, filename + '.json'), JSON.stringify({ role: request.role, stance: request.stance ?? null,
|
|
136
|
+
model: request.model ?? null, resumeSessionId: request.resumeSessionId ?? null, promptBytes: Buffer.byteLength(request.prompt),
|
|
137
|
+
sections: request.prompt.split(/(?=^#{1,4} )/m).map(section => ({ heading: section.split('\n')[0].slice(0, 150), bytes: Buffer.byteLength(section) })) }, null, 2) + '\n', { mode: 0o600 });
|
|
138
|
+
} : undefined;
|
|
139
|
+
const registry = options.real ? undefined : fixtureRegistry(test, context, capture);
|
|
140
|
+
if (options.definition === undefined)
|
|
141
|
+
state = await runCoreWorkflow({ context, change: 'evaluation-change', config, ...(registry ? { registry } : {}) });
|
|
142
|
+
else {
|
|
143
|
+
const validation = validateWorkflowDefinition(options.definition, validationPieceRegistry(), configuredRoles(config), { published: true });
|
|
144
|
+
if (!validation.ok)
|
|
145
|
+
throw new Error('Evaluation definition failed validation: ' + JSON.stringify(validation.errors));
|
|
146
|
+
evaluatedDefinitionVersion = validation.version;
|
|
147
|
+
await createRun({ context, change: 'evaluation-change', config, definition: validation.definition, ...(registry ? { registry } : {}) });
|
|
148
|
+
const database = await RunDatabase.open(path.join(definitionRunDirectory(context), 'run.sqlite'), { readOnly: true });
|
|
149
|
+
try {
|
|
150
|
+
state = ledgerWorkflowState(observeLedger(database), validation.definition, '*');
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
database.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
117
156
|
}
|
|
118
157
|
catch (error) {
|
|
119
158
|
observations.push({ caseId: test.id, repeat, mode, status: 'blocked', independentAccepted: false, error: error instanceof Error ? error.message : String(error), runtimeIdentity: identity, taskHash: fingerprint(test), oracleHash: fingerprint(test.oracle), repositories: context.repositories.map(({ id, baseSha }) => ({ id, baseSha })), configHash: fingerprint(config) });
|
|
@@ -145,7 +184,7 @@ export async function runEvaluation(options) {
|
|
|
145
184
|
return [mode, { samples: rows.length, independentlyAccepted: accepted, costPerAcceptedUsd: options.real && knownCost !== null && accepted ? knownCost / accepted : null, medianActiveDurationMs: median(durations), minActiveDurationMs: durations.length ? Math.min(...durations) : null, maxActiveDurationMs: durations.length ? Math.max(...durations) : null, standardDeviationMs: average === null ? null : Math.sqrt(durations.reduce((sum, value) => sum + (value - average) ** 2, 0) / durations.length) }];
|
|
146
185
|
}));
|
|
147
186
|
const full = groups.full, optimized = groups.optimized;
|
|
148
|
-
const paired = observations.length ===
|
|
187
|
+
const paired = observations.length === corpus.length * repetitions * 2 && full.samples === optimized.samples;
|
|
149
188
|
const noObservedQualityDrop = paired && optimized.independentlyAccepted >= full.independentlyAccepted;
|
|
150
189
|
const promptPairs = observations.filter(row => row.mode === 'full' && row.caseId === 'verification-correction').map(row => {
|
|
151
190
|
const other = observations.find(item => item.mode === 'optimized' && item.caseId === row.caseId && item.repeat === row.repeat);
|
|
@@ -158,7 +197,10 @@ export async function runEvaluation(options) {
|
|
|
158
197
|
return Array.isArray(row.invocations) && Array.isArray(other?.invocations) && other.invocations.length <= row.invocations.length;
|
|
159
198
|
});
|
|
160
199
|
const monetaryConclusion = options.real && paired && full.costPerAcceptedUsd !== null && optimized.costPerAcceptedUsd !== null && full.costPerAcceptedUsd > 0 ? optimized.costPerAcceptedUsd <= full.costPerAcceptedUsd * 0.8 && noObservedQualityDrop ? 'target-met-in-this-sample' : 'target-not-met-in-this-sample' : 'inconclusive';
|
|
161
|
-
const report = { schemaVersion: 1,
|
|
200
|
+
const report = { schemaVersion: 1, isDefinitionEvaluation: options.definition !== undefined,
|
|
201
|
+
...(options.definition === undefined ? {} : { definitionHash: evaluatedDefinitionVersion }),
|
|
202
|
+
acceptedCases: observations.filter(row => row.independentAccepted).length,
|
|
203
|
+
allCasesAccepted: paired && observations.every(row => row.independentAccepted), experiment: Object.values(options.config?.agents ?? {}).some(role => role.escalation) ? 'routing' : 'same-model', noExtraInvocations, mode: options.real ? 'real' : 'offline', runtimeIdentity: identity, corpusHash: fingerprint(corpus), measurement: options.real ? 'reported-provider-usage' : 'synthetic-fixture-usage; no actual AI savings measured', monetaryTarget: 0.2, monetaryConclusion, groups, noObservedQualityDrop, promptPairs, correctionPromptTargetMet: promptPairs.length > 0 && promptPairs.every(pair => pair.reduction !== null && pair.reduction >= 0.4), sampleLimitations: 'Small descriptive paired sample; does not establish a universal quality or savings guarantee.', stopReason, reportedSpendUsd: options.real ? spend : 0, observations };
|
|
162
204
|
mkdirSync(options.output, { recursive: true });
|
|
163
205
|
writeFileSync(path.join(options.output, 'evaluation.json'), JSON.stringify(report, null, 2) + '\n');
|
|
164
206
|
writeFileSync(path.join(options.output, 'evaluation.md'), `# Implementation efficiency evaluation\n\nMode: ${report.mode}. ${report.measurement}.\n\nMonetary target: 20% lower aggregate cost per independently accepted output. Conclusion: ${monetaryConclusion}.\n\n${JSON.stringify(groups, null, 2)}\n\nCorrection prompt comparison: ${JSON.stringify(promptPairs)}\n\n${observations.map(row => `- ${row.caseId}, ${row.mode}: ${row.status}; independent acceptance ${row.independentAccepted}; ${row.activeDurationMs} ms`).join('\n')}\n\n${stopReason ?? (options.real ? report.sampleLimitations : 'No paid benchmark has established monetary savings.')}\n`);
|
|
@@ -3,7 +3,31 @@ import type { CacheTokenUsage } from './efficiency-types.js';
|
|
|
3
3
|
import type { GuardrailSettings } from './guardrails.js';
|
|
4
4
|
export declare const DEFAULT_AGENT_TIMEOUT_MS: number;
|
|
5
5
|
export declare const DEFAULT_AGENT_IDLE_TIMEOUT_MS: number;
|
|
6
|
-
export
|
|
6
|
+
export declare const BUILTIN_ROLES: readonly ["architect", "developer", "reviewer"];
|
|
7
|
+
export type BuiltinAgentRole = typeof BUILTIN_ROLES[number];
|
|
8
|
+
export type AgentRole = string;
|
|
9
|
+
export type AgentAccess = 'read' | 'write';
|
|
10
|
+
export type ArtifactAccess = 'none' | 'tasks-checkboxes' | 'all';
|
|
11
|
+
export type OpenSpecSkill = 'openspec-ff-change' | 'openspec-apply-change' | 'openspec-verify-change';
|
|
12
|
+
export declare const ROLE_ID: RegExp;
|
|
13
|
+
export declare function isBuiltinRole(role: string): role is BuiltinAgentRole;
|
|
14
|
+
export declare const BUILTIN_ROLE_POLICY: {
|
|
15
|
+
readonly architect: {
|
|
16
|
+
readonly access: "read";
|
|
17
|
+
readonly artifacts: "all";
|
|
18
|
+
readonly openspecSkill: "openspec-ff-change";
|
|
19
|
+
};
|
|
20
|
+
readonly developer: {
|
|
21
|
+
readonly access: "write";
|
|
22
|
+
readonly artifacts: "tasks-checkboxes";
|
|
23
|
+
readonly openspecSkill: "openspec-apply-change";
|
|
24
|
+
};
|
|
25
|
+
readonly reviewer: {
|
|
26
|
+
readonly access: "read";
|
|
27
|
+
readonly artifacts: "none";
|
|
28
|
+
readonly openspecSkill: "openspec-verify-change";
|
|
29
|
+
};
|
|
30
|
+
};
|
|
7
31
|
/** Who an observer sees acting: a pipeline role, or the FIXER stance the developer step takes on a correction round. */
|
|
8
32
|
export type AgentEventRole = AgentRole | 'fixer';
|
|
9
33
|
export type CliProvider = 'claude' | 'codex' | 'gemini' | 'kimi';
|
|
@@ -37,6 +61,15 @@ export interface RuntimeAgentConfig {
|
|
|
37
61
|
effort?: string;
|
|
38
62
|
};
|
|
39
63
|
}
|
|
64
|
+
export interface RuntimeRoleConfig extends RuntimeAgentConfig {
|
|
65
|
+
access: AgentAccess;
|
|
66
|
+
artifacts: ArtifactAccess;
|
|
67
|
+
prompt?: string;
|
|
68
|
+
openspecSkill?: OpenSpecSkill;
|
|
69
|
+
}
|
|
70
|
+
export interface RoleDescriptor extends RuntimeRoleConfig {
|
|
71
|
+
id: AgentRole;
|
|
72
|
+
}
|
|
40
73
|
export interface EfficiencyPolicy {
|
|
41
74
|
schemaVersion: 1;
|
|
42
75
|
contextMode?: 'full' | 'incremental';
|
|
@@ -64,7 +97,8 @@ export interface RuntimeConfig {
|
|
|
64
97
|
efficiency?: EfficiencyPolicy;
|
|
65
98
|
enabled: boolean;
|
|
66
99
|
providers: RuntimeProviderConfig[];
|
|
67
|
-
agents: Record<
|
|
100
|
+
agents: Record<BuiltinAgentRole, RuntimeAgentConfig>;
|
|
101
|
+
roles?: Record<AgentRole, RuntimeRoleConfig>;
|
|
68
102
|
/** Optional engine for correction rounds (after a failed verification or a rejected review); unset ⇒ the developer corrects. */
|
|
69
103
|
fixer?: RuntimeAgentConfig;
|
|
70
104
|
limits?: {
|
|
@@ -110,6 +144,14 @@ export interface AgentEvent {
|
|
|
110
144
|
export interface AgentRequest {
|
|
111
145
|
openspec?: import('./openspec.js').OpenSpecRoleContext;
|
|
112
146
|
role: AgentRole;
|
|
147
|
+
/** Optional only for backward-compatible built-in requests. Custom roles require explicit policy. */
|
|
148
|
+
access?: AgentAccess;
|
|
149
|
+
artifacts?: ArtifactAccess;
|
|
150
|
+
instructions?: 'role' | 'none';
|
|
151
|
+
nativeCommand?: {
|
|
152
|
+
id: string;
|
|
153
|
+
args?: string;
|
|
154
|
+
};
|
|
113
155
|
prompt: string;
|
|
114
156
|
cwd: string;
|
|
115
157
|
allowedRoots: string[];
|
|
@@ -118,8 +160,9 @@ export interface AgentRequest {
|
|
|
118
160
|
/** Local engines: private thinking on/off for this invocation (see RuntimeAgentConfig.thinking). */
|
|
119
161
|
thinking?: 'on' | 'off';
|
|
120
162
|
maxTurns?: number;
|
|
163
|
+
/** Total invocation limit; zero disables this timer without disabling cancellation or run budgets. */
|
|
121
164
|
timeoutMs?: number;
|
|
122
|
-
/** Maximum silence from the provider;
|
|
165
|
+
/** Maximum silence from the provider; zero disables the idle timer. */
|
|
123
166
|
idleTimeoutMs?: number;
|
|
124
167
|
maxTokens?: number;
|
|
125
168
|
maxCostUsd?: number;
|
|
@@ -157,3 +200,10 @@ export declare class AgentExecutionError extends Error {
|
|
|
157
200
|
}
|
|
158
201
|
export declare function unknownUsage(): AgentUsage;
|
|
159
202
|
export declare function validateAgentRequest(request: AgentRequest): void;
|
|
203
|
+
/** Resolve compatible defaults without changing the caller's frozen request. */
|
|
204
|
+
export declare function requestPolicy(request: Pick<AgentRequest, 'role' | 'access' | 'artifacts' | 'instructions'>): {
|
|
205
|
+
access: AgentAccess;
|
|
206
|
+
artifacts: ArtifactAccess;
|
|
207
|
+
instructions: 'role' | 'none';
|
|
208
|
+
};
|
|
209
|
+
export declare function normalizeAgentRequest(request: AgentRequest): AgentRequest & ReturnType<typeof requestPolicy>;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export const DEFAULT_AGENT_TIMEOUT_MS = 60 * 60_000;
|
|
2
2
|
export const DEFAULT_AGENT_IDLE_TIMEOUT_MS = 15 * 60_000;
|
|
3
|
+
export const BUILTIN_ROLES = ['architect', 'developer', 'reviewer'];
|
|
4
|
+
export const ROLE_ID = /^[a-z][a-z0-9-]{0,63}$/;
|
|
5
|
+
export function isBuiltinRole(role) { return BUILTIN_ROLES.includes(role); }
|
|
6
|
+
export const BUILTIN_ROLE_POLICY = {
|
|
7
|
+
architect: { access: 'read', artifacts: 'all', openspecSkill: 'openspec-ff-change' },
|
|
8
|
+
developer: { access: 'write', artifacts: 'tasks-checkboxes', openspecSkill: 'openspec-apply-change' },
|
|
9
|
+
reviewer: { access: 'read', artifacts: 'none', openspecSkill: 'openspec-verify-change' },
|
|
10
|
+
};
|
|
3
11
|
export class AgentExecutionError extends Error {
|
|
4
12
|
code;
|
|
5
13
|
usage;
|
|
@@ -15,13 +23,21 @@ export class AgentExecutionError extends Error {
|
|
|
15
23
|
}
|
|
16
24
|
export function unknownUsage() { return { inputTokens: null, outputTokens: null, costUsd: null }; }
|
|
17
25
|
export function validateAgentRequest(request) {
|
|
18
|
-
if (
|
|
26
|
+
if (typeof request.role !== 'string' || !ROLE_ID.test(request.role) || typeof request.prompt !== 'string' || request.prompt.includes('\0') || (!request.nativeCommand && !request.prompt.trim()))
|
|
19
27
|
throw new AgentExecutionError('Invalid role request', 'invalid_request');
|
|
28
|
+
const policy = requestPolicy(request);
|
|
29
|
+
if (request.nativeCommand !== undefined) {
|
|
30
|
+
const command = request.nativeCommand;
|
|
31
|
+
if (!command || typeof command !== 'object' || Array.isArray(command) || Object.keys(command).some(key => !['id', 'args'].includes(key)) || typeof command.id !== 'string' || !/^[a-z][a-z0-9:_-]{0,63}$/.test(command.id) || (command.args !== undefined && (typeof command.args !== 'string' || command.args.includes('\0'))) || request.prompt.trim() || policy.instructions !== 'none' || request.openspec)
|
|
32
|
+
throw new AgentExecutionError('Invalid native command request', 'invalid_request');
|
|
33
|
+
}
|
|
34
|
+
if (policy.instructions === 'none' && request.openspec)
|
|
35
|
+
throw new AgentExecutionError('Free prompts cannot bind role instructions', 'invalid_request');
|
|
20
36
|
if (request.model !== undefined && (typeof request.model !== 'string' || !request.model.trim() || request.model.length > 256 || /^-/.test(request.model) || /[\0\r\n]/.test(request.model)))
|
|
21
37
|
throw new AgentExecutionError('Invalid model identifier', 'invalid_model');
|
|
22
38
|
for (const key of ['maxTurns', 'timeoutMs', 'idleTimeoutMs', 'maxTokens', 'maxCostUsd']) {
|
|
23
39
|
const value = request[key];
|
|
24
|
-
if (value !== undefined && (!Number.isFinite(value) || value
|
|
40
|
+
if (value !== undefined && (!Number.isFinite(value) || (value < 0 || (value === 0 && key !== 'timeoutMs' && key !== 'idleTimeoutMs')) || (key !== 'maxCostUsd' && !Number.isSafeInteger(value))))
|
|
25
41
|
throw new AgentExecutionError(`Invalid ${key}`, 'invalid_limit');
|
|
26
42
|
}
|
|
27
43
|
if (request.resumeSessionId !== undefined && (typeof request.resumeSessionId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(request.resumeSessionId)))
|
|
@@ -31,3 +47,20 @@ export function validateAgentRequest(request) {
|
|
|
31
47
|
if (request.signal?.aborted)
|
|
32
48
|
throw new AgentExecutionError('Agent cancelled', 'aborted');
|
|
33
49
|
}
|
|
50
|
+
/** Resolve compatible defaults without changing the caller's frozen request. */
|
|
51
|
+
export function requestPolicy(request) {
|
|
52
|
+
const legacy = isBuiltinRole(request.role) ? BUILTIN_ROLE_POLICY[request.role] : undefined;
|
|
53
|
+
const access = request.access ?? legacy?.access;
|
|
54
|
+
const artifacts = request.artifacts ?? legacy?.artifacts;
|
|
55
|
+
const instructions = request.instructions ?? (legacy ? 'role' : undefined);
|
|
56
|
+
if ((access !== 'read' && access !== 'write') || (artifacts !== 'none' && artifacts !== 'tasks-checkboxes' && artifacts !== 'all') || (instructions !== 'role' && instructions !== 'none'))
|
|
57
|
+
throw new AgentExecutionError('Explicit access, artifacts and instructions are required', 'invalid_request');
|
|
58
|
+
return { access, artifacts, instructions };
|
|
59
|
+
}
|
|
60
|
+
export function normalizeAgentRequest(request) {
|
|
61
|
+
validateAgentRequest(request);
|
|
62
|
+
const policy = requestPolicy(request);
|
|
63
|
+
const legacy = isBuiltinRole(request.role) ? BUILTIN_ROLE_POLICY[request.role] : undefined;
|
|
64
|
+
const unchangedBinding = request.openspec && request.openspec.role === request.role && request.openspec.access === undefined && request.openspec.artifacts === undefined && policy.access === legacy?.access && policy.artifacts === legacy?.artifacts;
|
|
65
|
+
return { ...request, ...policy, ...(request.openspec && !unchangedBinding ? { openspec: { ...request.openspec, access: policy.access, artifacts: policy.artifacts } } : {}) };
|
|
66
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AgentExecutionError,
|
|
1
|
+
import { AgentExecutionError, normalizeAgentRequest } from './executor-types.js';
|
|
2
2
|
import { validateRuntimeConfig } from './config.js';
|
|
3
3
|
import { CliExecutor } from './cli-executor.js';
|
|
4
4
|
import { OpenAICompatibleExecutor } from './openai-executor.js';
|
|
@@ -24,7 +24,7 @@ export class ExecutorRegistry {
|
|
|
24
24
|
async capabilities(id, model) { return await this.get(id).capabilities?.(model) ?? unknownCapabilities('registered:' + id); }
|
|
25
25
|
validateLimits(id, limits) { this.get(id).validateLimits?.(limits); }
|
|
26
26
|
async execute(id, request) {
|
|
27
|
-
|
|
27
|
+
request = normalizeAgentRequest(request);
|
|
28
28
|
if (request.effort !== undefined)
|
|
29
29
|
assertEffortSupported(request, await this.capabilities(id, request.model));
|
|
30
30
|
const result = await this.get(id).execute(request);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type PipelineContext, type PipelineState, type VerificationCommand } from '../../pipeline/pipeline-state.js';
|
|
2
2
|
import type { DesignConfidence } from './state.js';
|
|
3
3
|
export declare const SLUG: RegExp;
|
|
4
|
+
/** Historical target lookup, not an archive or verification receipt. */
|
|
5
|
+
export declare function archivedOpenSpecChange(context: PipelineContext, change: string): string | undefined;
|
|
4
6
|
export declare function object(value: unknown): Record<string, unknown>;
|
|
5
7
|
/** Accepts the object wherever the model put it: bare, fenced, or surrounded by commentary. */
|
|
6
8
|
export declare function parseAgentObject(text: string): Record<string, unknown>;
|
|
@@ -23,4 +25,9 @@ export declare function writeDesignConfidence(context: PipelineContext, change:
|
|
|
23
25
|
}): void;
|
|
24
26
|
/** Commands the architect proposed for repositories the configuration leaves uncovered. */
|
|
25
27
|
export declare function proposedVerification(context: PipelineContext, configured: VerificationCommand[], verification: unknown): VerificationCommand[];
|
|
28
|
+
/** Recoverable pinned-CLI archive for ledger-only runs; no implementation journal. */
|
|
29
|
+
export declare function archiveOpenSpecChange(context: PipelineContext, change: string, adapter: {
|
|
30
|
+
directory: string;
|
|
31
|
+
beforePublish(): void;
|
|
32
|
+
}, signal?: AbortSignal): Promise<void>;
|
|
26
33
|
export declare function archive(context: PipelineContext, change: string, signal?: AbortSignal): Promise<void>;
|
|
@@ -5,6 +5,23 @@ import path from 'node:path';
|
|
|
5
5
|
import { checkArchive, pipelineStateDirectory, transitionPipeline, validateVerificationRequest, } from '../../pipeline/pipeline-state.js';
|
|
6
6
|
import { assertProposalInScope, withScopeDefault } from '../change-scope.js';
|
|
7
7
|
export const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
8
|
+
/** Historical target lookup, not an archive or verification receipt. */
|
|
9
|
+
export function archivedOpenSpecChange(context, change) {
|
|
10
|
+
if (!SLUG.test(change) || change.length > 64)
|
|
11
|
+
throw new Error('Invalid OpenSpec change identifier');
|
|
12
|
+
if (existsSync(artifactPath(context.artifactRoot, 'openspec/changes/' + change)))
|
|
13
|
+
return undefined;
|
|
14
|
+
const relative = 'openspec/changes/archive', directory = artifactPath(context.artifactRoot, relative);
|
|
15
|
+
if (!existsSync(directory))
|
|
16
|
+
return undefined;
|
|
17
|
+
const candidate = readdirSync(directory, { withFileTypes: true }).filter(entry => entry.isDirectory() &&
|
|
18
|
+
(entry.name === change || /^\d{4}-\d{2}-\d{2}-/.test(entry.name) && entry.name.slice(11) === change))
|
|
19
|
+
.sort((left, right) => left.name.localeCompare(right.name, 'en'))[0];
|
|
20
|
+
if (!candidate)
|
|
21
|
+
return undefined;
|
|
22
|
+
artifactPath(context.artifactRoot, relative + '/' + candidate.name);
|
|
23
|
+
return relative + '/' + candidate.name;
|
|
24
|
+
}
|
|
8
25
|
export function object(value) {
|
|
9
26
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
10
27
|
throw new Error('Expected a structured JSON object from agent');
|
|
@@ -114,8 +131,8 @@ export function proposedVerification(context, configured, verification) {
|
|
|
114
131
|
}
|
|
115
132
|
/** Publish an archive prepared by the real CLI. The durable write set makes a
|
|
116
133
|
* crash between main-spec updates recoverable without applying a delta twice. */
|
|
117
|
-
async function archiveWithOpenSpec(context, change, active, signal) {
|
|
118
|
-
const directory = pipelineStateDirectory(context);
|
|
134
|
+
async function archiveWithOpenSpec(context, change, active, signal, adapter) {
|
|
135
|
+
const directory = adapter?.directory ?? pipelineStateDirectory(context);
|
|
119
136
|
const receipt = path.join(directory, 'openspec-archive.json');
|
|
120
137
|
const files = (root, prefix = '') => {
|
|
121
138
|
if (!existsSync(root))
|
|
@@ -127,7 +144,8 @@ async function archiveWithOpenSpec(context, change, active, signal) {
|
|
|
127
144
|
return entry.isDirectory() ? files(path.join(root, entry.name), relative + '/') : [relative];
|
|
128
145
|
});
|
|
129
146
|
};
|
|
130
|
-
const
|
|
147
|
+
const treeHash = (root) => hash(JSON.stringify(files(root).sort().map(file => [file, readFileSync(artifactPath(root, file), 'utf8')])));
|
|
148
|
+
const activeHash = treeHash(active);
|
|
131
149
|
let plan;
|
|
132
150
|
if (existsSync(receipt))
|
|
133
151
|
plan = JSON.parse(readFileSync(receipt, 'utf8'));
|
|
@@ -137,6 +155,22 @@ async function archiveWithOpenSpec(context, change, active, signal) {
|
|
|
137
155
|
const source = artifactPath(context.artifactRoot, 'openspec');
|
|
138
156
|
files(source); // Reject symlinks before copying or invoking the external CLI.
|
|
139
157
|
cpSync(source, path.join(staging, 'openspec'), { recursive: true });
|
|
158
|
+
// A fork prepares its delta against checkpoint-owned main specs. Publish
|
|
159
|
+
// still compares all current preimages below; no source files are rewound.
|
|
160
|
+
const forkBase = path.join(directory, 'openspec-archive-base.json');
|
|
161
|
+
let beforeRoot = path.join(source, 'specs');
|
|
162
|
+
let sourceChange;
|
|
163
|
+
if (existsSync(forkBase)) {
|
|
164
|
+
const baseline = object(JSON.parse(readFileSync(forkBase, 'utf8')));
|
|
165
|
+
if (baseline.schemaVersion !== 1 || baseline.directory !== 'agent-workflow/openspec-base-specs' || typeof baseline.snapshot !== 'string' || !/^[a-f0-9]{64}$/.test(baseline.snapshot)
|
|
166
|
+
|| typeof baseline.sourceChange !== 'string' || baseline.sourceChange.length > 64 || !SLUG.test(baseline.sourceChange))
|
|
167
|
+
throw new Error('Invalid fork archive baseline');
|
|
168
|
+
sourceChange = baseline.sourceChange;
|
|
169
|
+
beforeRoot = child(directory, baseline.directory);
|
|
170
|
+
files(beforeRoot);
|
|
171
|
+
rmSync(path.join(staging, 'openspec/specs'), { recursive: true, force: true });
|
|
172
|
+
cpSync(beforeRoot, path.join(staging, 'openspec/specs'), { recursive: true });
|
|
173
|
+
}
|
|
140
174
|
const output = await runOpenSpec(resolveOpenSpecCli(), staging, ['archive', change, '--yes'], signal);
|
|
141
175
|
const archiveRoot = path.join(staging, 'openspec/changes/archive');
|
|
142
176
|
const archives = existsSync(archiveRoot) ? readdirSync(archiveRoot).filter(name => name.endsWith('-' + change)) : [];
|
|
@@ -151,20 +185,40 @@ async function archiveWithOpenSpec(context, change, active, signal) {
|
|
|
151
185
|
const destination = 'openspec/changes/archive/' + archives[0];
|
|
152
186
|
if (existsSync(artifactPath(context.artifactRoot, destination)))
|
|
153
187
|
throw new Error('OpenSpec archive destination already exists');
|
|
154
|
-
const
|
|
188
|
+
const afterRoot = path.join(staging, 'openspec/specs');
|
|
155
189
|
const names = new Set([...files(beforeRoot), ...files(afterRoot)]);
|
|
156
190
|
const contents = (root, name) => existsSync(path.join(root, name)) ? readFileSync(path.join(root, name), 'utf8') : null;
|
|
157
|
-
plan = { activeHash, destination, writes: [...names].map(name =>
|
|
191
|
+
plan = { activeHash, destination, writes: [...names].map(name => {
|
|
192
|
+
const before = contents(beforeRoot, name);
|
|
193
|
+
let after = contents(afterRoot, name);
|
|
194
|
+
// Pinned OpenSpec inserts a change ID into the placeholder for a newly
|
|
195
|
+
// created specification. Preserve that provenance during fork replay;
|
|
196
|
+
// authored text and all byte-for-byte publication guards stay intact.
|
|
197
|
+
if (sourceChange && before === null && after !== null)
|
|
198
|
+
after = after.replace(`\n## Purpose\nTBD - created by archiving change ${change}. Update Purpose after archive.\n## Requirements\n`, `\n## Purpose\nTBD - created by archiving change ${sourceChange}. Update Purpose after archive.\n## Requirements\n`);
|
|
199
|
+
return { path: 'openspec/specs/' + name, before, after };
|
|
200
|
+
}).filter(item => item.before !== item.after) };
|
|
158
201
|
write(receipt, JSON.stringify(plan));
|
|
159
202
|
}
|
|
160
203
|
finally {
|
|
161
204
|
rmSync(staging, { recursive: true, force: true });
|
|
162
205
|
}
|
|
163
206
|
}
|
|
164
|
-
if (plan.activeHash !== activeHash)
|
|
165
|
-
throw new Error('Reviewed artifacts changed since the OpenSpec archive was prepared');
|
|
166
207
|
if (!new RegExp('^openspec/changes/archive/[0-9]{4}-[0-9]{2}-[0-9]{2}-' + change + '$').test(plan.destination))
|
|
167
208
|
throw new Error('Invalid OpenSpec archive receipt');
|
|
209
|
+
if (adapter && !existsSync(active)) {
|
|
210
|
+
const archived = artifactPath(context.artifactRoot, plan.destination);
|
|
211
|
+
if (!existsSync(archived) || treeHash(archived) !== plan.activeHash || plan.writes.some(item => {
|
|
212
|
+
if (!item.path.startsWith('openspec/specs/'))
|
|
213
|
+
throw new Error('Invalid OpenSpec archive write');
|
|
214
|
+
const target = artifactPath(context.artifactRoot, item.path);
|
|
215
|
+
return (existsSync(target) ? readFileSync(target, 'utf8') : null) !== item.after;
|
|
216
|
+
}))
|
|
217
|
+
throw new Error('OpenSpec archive recovery does not match the prepared write set');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (plan.activeHash !== activeHash)
|
|
221
|
+
throw new Error('Reviewed artifacts changed since the OpenSpec archive was prepared');
|
|
168
222
|
const targets = plan.writes.map(item => {
|
|
169
223
|
if (!item.path.startsWith('openspec/specs/'))
|
|
170
224
|
throw new Error('Invalid OpenSpec archive write');
|
|
@@ -172,16 +226,23 @@ async function archiveWithOpenSpec(context, change, active, signal) {
|
|
|
172
226
|
const current = existsSync(target) ? readFileSync(target, 'utf8') : null;
|
|
173
227
|
if (current !== item.before && current !== item.after)
|
|
174
228
|
throw new Error('Main specification changed during archive; refusing to overwrite: ' + item.path);
|
|
175
|
-
return { ...item, target };
|
|
229
|
+
return { ...item, target, unchanged: current === item.after };
|
|
176
230
|
});
|
|
177
231
|
// The external CLI ran asynchronously; recheck the candidate and artifacts before publishing.
|
|
178
232
|
signal?.throwIfAborted();
|
|
179
|
-
|
|
233
|
+
if (treeHash(active) !== activeHash)
|
|
234
|
+
throw new Error('Reviewed artifacts changed during OpenSpec archive preparation');
|
|
235
|
+
if (adapter)
|
|
236
|
+
adapter.beforePublish();
|
|
237
|
+
else
|
|
238
|
+
checkArchive(context);
|
|
180
239
|
// Validate every path and preimage before publishing the first output.
|
|
181
240
|
const destination = artifactPath(context.artifactRoot, plan.destination);
|
|
182
241
|
if (existsSync(destination))
|
|
183
242
|
throw new Error('Archive destination already exists');
|
|
184
243
|
for (const item of targets) {
|
|
244
|
+
if (item.unchanged)
|
|
245
|
+
continue;
|
|
185
246
|
if (item.after === null)
|
|
186
247
|
rmSync(item.target, { force: true });
|
|
187
248
|
else
|
|
@@ -190,6 +251,18 @@ async function archiveWithOpenSpec(context, change, active, signal) {
|
|
|
190
251
|
mkdirSync(path.dirname(destination), { recursive: true });
|
|
191
252
|
renameSync(active, destination);
|
|
192
253
|
}
|
|
254
|
+
/** Recoverable pinned-CLI archive for ledger-only runs; no implementation journal. */
|
|
255
|
+
export async function archiveOpenSpecChange(context, change, adapter, signal) {
|
|
256
|
+
if (!SLUG.test(change) || change.length > 64)
|
|
257
|
+
throw new Error('Invalid OpenSpec change identifier');
|
|
258
|
+
mkdirSync(adapter.directory, { recursive: true, mode: 0o700 });
|
|
259
|
+
if (lstatSync(adapter.directory).isSymbolicLink())
|
|
260
|
+
throw new Error('Refusing symlink archive adapter directory');
|
|
261
|
+
const active = artifactPath(context.artifactRoot, 'openspec/changes/' + change);
|
|
262
|
+
if (!existsSync(active) && !existsSync(path.join(adapter.directory, 'openspec-archive.json')))
|
|
263
|
+
throw new Error('OpenSpec active change does not exist');
|
|
264
|
+
await archiveWithOpenSpec(context, change, active, signal, adapter);
|
|
265
|
+
}
|
|
193
266
|
export async function archive(context, change, signal) {
|
|
194
267
|
const state = journal(context);
|
|
195
268
|
if (state.phases.archive.status !== 'done') {
|
|
@@ -9,6 +9,8 @@ import type { CoreNodeId, CoreStateType, DeveloperRecord, VerificationRecord } f
|
|
|
9
9
|
/** Autonomous investigation passes before a low-confidence design asks or proceeds. */
|
|
10
10
|
export declare const MAX_DEEPEN_PASSES = 1;
|
|
11
11
|
export interface CoreNodeDeps {
|
|
12
|
+
/** V2 presentation omits framework dispatch frames; original evidence stays complete. */
|
|
13
|
+
focusedCorrectionEvidence?: boolean;
|
|
12
14
|
archiveApproved?: () => boolean;
|
|
13
15
|
context: PipelineContext;
|
|
14
16
|
config: RuntimeConfig;
|
|
@@ -214,7 +214,7 @@ async function implementationVisit(deps, state, step, mode) {
|
|
|
214
214
|
kind: visits > 1 ? 'correction' : 'initial',
|
|
215
215
|
...(fixer ? { agentOverride: fixer } : {}), ...(fixing ? { stance: 'fixer' } : {}),
|
|
216
216
|
...(resumable
|
|
217
|
-
? { prompt: correctionInstructions('developer', feedback, { ...(changes ? { changeSet: changes } : {}), developer: state.development }), resumeSessionId: previous.sessionId, fallbackPrompt: full }
|
|
217
|
+
? { prompt: correctionInstructions('developer', feedback, { ...(changes ? { changeSet: changes } : {}), developer: state.development, focusedEvidence: deps.focusedCorrectionEvidence && config.efficiency?.contextMode !== 'full' }), resumeSessionId: previous.sessionId, fallbackPrompt: full }
|
|
218
218
|
: { prompt: full }),
|
|
219
219
|
structured: true, lenient: true, outputSchema: DEVELOPER_OUTPUT_SCHEMA,
|
|
220
220
|
}, (output, text, result) => {
|
|
@@ -513,7 +513,7 @@ function reviewerNode(deps) {
|
|
|
513
513
|
const changes = measuredChange(context, change);
|
|
514
514
|
const prompt = roleInstructions('reviewer', context, change, { definition: config.rolePrompts?.reviewer, feedback: feedbackFor(state), verification: state.plan, policy, criteria, developer: state.development, ...(changes ? { changeSet: changes } : {}), ...(reReview ? { reReview } : {}) });
|
|
515
515
|
const incremental = config.efficiency?.reviewMode !== 'full' && delta.mode === 'incremental' && state.review?.sessionId;
|
|
516
|
-
const followup = correctionInstructions('reviewer', feedbackFor(state)) + '\nChanges since YOUR previous reviewed candidate:\n' + JSON.stringify(delta.changes)
|
|
516
|
+
const followup = correctionInstructions('reviewer', feedbackFor(state), { focusedEvidence: deps.focusedCorrectionEvidence && config.efficiency?.contextMode !== 'full' }) + '\nChanges since YOUR previous reviewed candidate:\n' + JSON.stringify(delta.changes)
|
|
517
517
|
+ (changes ? '\nThe complete change under review (from git, against the run\'s base) — review these files only:\n' + changes.join('\n') : '')
|
|
518
518
|
+ '\nRecertify EVERY current acceptance criterion; previous met results are not current evidence:\n' + JSON.stringify(criteria)
|
|
519
519
|
+ '\nCurrent developer handoff:\n' + JSON.stringify(state.development);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RoleStatePort } from '../role-state.js';
|
|
1
2
|
import { type InvocationKind } from '../role-routing.js';
|
|
2
3
|
import { type PipelineContext } from '../../pipeline/pipeline-state.js';
|
|
3
4
|
import type { ExecutorRegistry } from '../executors.js';
|
|
@@ -17,6 +18,9 @@ export interface InvokeOptions {
|
|
|
17
18
|
/** Run this invocation on another configured engine (the fixer) with the matching stance; sessions never carry across engines. */
|
|
18
19
|
agentOverride?: RuntimeAgentConfig;
|
|
19
20
|
stance?: 'fixer';
|
|
21
|
+
/** Per-piece invocation bounds; explicit zero disables only that timer. */
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
idleTimeoutMs?: number;
|
|
20
24
|
}
|
|
21
25
|
export type InvokeOutcome<T> = {
|
|
22
26
|
ok: true;
|
|
@@ -36,6 +40,10 @@ export interface RoleInvokerDeps {
|
|
|
36
40
|
config: RuntimeConfig;
|
|
37
41
|
registry: ExecutorRegistry;
|
|
38
42
|
openspec?: Record<AgentRole, import('../openspec.js').OpenSpecRoleContext>;
|
|
43
|
+
/** Engine hosts isolate sessions/routes by scope and node instead of a role-global file. */
|
|
44
|
+
roleState?: RoleStatePort;
|
|
45
|
+
/** Durable independent operations must recover uncertain effects explicitly. */
|
|
46
|
+
propagateInterruptions?: boolean;
|
|
39
47
|
/** `fixer` labels the developer step's events during a correction round on the fixer stance, so a log reader sees who is acting. */
|
|
40
48
|
onAgentEvent?: (role: AgentEventRole, event: AgentEvent) => void;
|
|
41
49
|
}
|