specrails-core 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent-runtime/capabilities.js +3 -1
- package/dist/agent-runtime/change-scope.d.ts +63 -0
- package/dist/agent-runtime/change-scope.js +297 -0
- package/dist/agent-runtime/cli-executor.d.ts +3 -0
- package/dist/agent-runtime/cli-executor.js +50 -13
- package/dist/agent-runtime/cli-process.js +6 -2
- package/dist/agent-runtime/cli.d.ts +15 -0
- package/dist/agent-runtime/cli.js +43 -9
- package/dist/agent-runtime/compact/chat-client.js +2 -1
- package/dist/agent-runtime/compact/environment.d.ts +12 -0
- package/dist/agent-runtime/compact/environment.js +31 -1
- package/dist/agent-runtime/compact/test-reachability.js +6 -2
- 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 +10 -4
- package/dist/agent-runtime/core-host.js +22 -19
- package/dist/agent-runtime/efficiency-summary.d.ts +4 -1
- package/dist/agent-runtime/efficiency-summary.js +30 -27
- package/dist/agent-runtime/efficiency-types.d.ts +4 -4
- package/dist/agent-runtime/efficiency.d.ts +1 -1
- package/dist/agent-runtime/efficiency.js +7 -6
- package/dist/agent-runtime/engine/__fixtures__/implementation-component.json +90 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.d.ts +11 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +60 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation.json +43 -0
- package/dist/agent-runtime/engine/budget.d.ts +9 -0
- package/dist/agent-runtime/engine/budget.js +33 -0
- package/dist/agent-runtime/engine/canonical-json.d.ts +9 -0
- package/dist/agent-runtime/engine/canonical-json.js +162 -0
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +67 -0
- package/dist/agent-runtime/engine/checkpoint/database.js +304 -0
- package/dist/agent-runtime/engine/checkpoint/lease.d.ts +17 -0
- package/dist/agent-runtime/engine/checkpoint/lease.js +82 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +133 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.js +510 -0
- package/dist/agent-runtime/engine/checkpoint/saver.d.ts +25 -0
- package/dist/agent-runtime/engine/checkpoint/saver.js +146 -0
- package/dist/agent-runtime/engine/cli.d.ts +10 -0
- package/dist/agent-runtime/engine/cli.js +172 -0
- package/dist/agent-runtime/engine/compiler.d.ts +23 -0
- package/dist/agent-runtime/engine/compiler.js +261 -0
- package/dist/agent-runtime/engine/composition.d.ts +16 -0
- package/dist/agent-runtime/engine/composition.js +28 -0
- package/dist/agent-runtime/engine/concurrency.d.ts +14 -0
- package/dist/agent-runtime/engine/concurrency.js +55 -0
- package/dist/agent-runtime/engine/contracts.d.ts +273 -0
- package/dist/agent-runtime/engine/contracts.js +11 -0
- package/dist/agent-runtime/engine/definition-schema.d.ts +250 -0
- package/dist/agent-runtime/engine/definition-schema.js +313 -0
- package/dist/agent-runtime/engine/definition-types.d.ts +84 -0
- package/dist/agent-runtime/engine/definition-types.js +3 -0
- package/dist/agent-runtime/engine/definition-validator.d.ts +7 -0
- package/dist/agent-runtime/engine/definition-validator.js +187 -0
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +62 -0
- package/dist/agent-runtime/engine/efficiency-summary.js +31 -0
- package/dist/agent-runtime/engine/events.d.ts +19 -0
- package/dist/agent-runtime/engine/events.js +88 -0
- package/dist/agent-runtime/engine/execution.d.ts +29 -0
- package/dist/agent-runtime/engine/execution.js +121 -0
- package/dist/agent-runtime/engine/expressions.d.ts +11 -0
- package/dist/agent-runtime/engine/expressions.js +172 -0
- package/dist/agent-runtime/engine/fork.d.ts +28 -0
- package/dist/agent-runtime/engine/fork.js +211 -0
- package/dist/agent-runtime/engine/graph-description.d.ts +48 -0
- package/dist/agent-runtime/engine/graph-description.js +16 -0
- package/dist/agent-runtime/engine/implementation-compiler.d.ts +214 -0
- package/dist/agent-runtime/engine/implementation-compiler.js +91 -0
- package/dist/agent-runtime/engine/index.d.ts +14 -0
- package/dist/agent-runtime/engine/index.js +14 -0
- package/dist/agent-runtime/engine/invocation-context.d.ts +11 -0
- package/dist/agent-runtime/engine/invocation-context.js +128 -0
- package/dist/agent-runtime/engine/otel.d.ts +15 -0
- package/dist/agent-runtime/engine/otel.js +132 -0
- package/dist/agent-runtime/engine/output.d.ts +4 -0
- package/dist/agent-runtime/engine/output.js +73 -0
- package/dist/agent-runtime/engine/piece-dependencies.d.ts +50 -0
- package/dist/agent-runtime/engine/piece-dependencies.js +193 -0
- package/dist/agent-runtime/engine/piece-registry.d.ts +19 -0
- package/dist/agent-runtime/engine/piece-registry.js +62 -0
- package/dist/agent-runtime/engine/pieces/assign.d.ts +4 -0
- package/dist/agent-runtime/engine/pieces/assign.js +35 -0
- package/dist/agent-runtime/engine/pieces/control.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/control.js +49 -0
- package/dist/agent-runtime/engine/pieces/decider.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/decider.js +70 -0
- package/dist/agent-runtime/engine/pieces/implementation-binding.d.ts +12 -0
- package/dist/agent-runtime/engine/pieces/implementation-binding.js +40 -0
- package/dist/agent-runtime/engine/pieces/implementation-journal.d.ts +25 -0
- package/dist/agent-runtime/engine/pieces/implementation-journal.js +192 -0
- package/dist/agent-runtime/engine/pieces/implementation.d.ts +23 -0
- package/dist/agent-runtime/engine/pieces/implementation.js +176 -0
- package/dist/agent-runtime/engine/pieces/index.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/index.js +22 -0
- package/dist/agent-runtime/engine/pieces/openspec.d.ts +3 -0
- package/dist/agent-runtime/engine/pieces/openspec.js +69 -0
- package/dist/agent-runtime/engine/pieces/ports.d.ts +48 -0
- package/dist/agent-runtime/engine/pieces/ports.js +1 -0
- package/dist/agent-runtime/engine/pieces/project-memory.d.ts +3 -0
- package/dist/agent-runtime/engine/pieces/project-memory.js +13 -0
- package/dist/agent-runtime/engine/pieces/prompt.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/prompt.js +130 -0
- package/dist/agent-runtime/engine/pieces/role-turn.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +73 -0
- package/dist/agent-runtime/engine/pieces/shared.d.ts +67 -0
- package/dist/agent-runtime/engine/pieces/shared.js +40 -0
- package/dist/agent-runtime/engine/pieces/shell.d.ts +6 -0
- package/dist/agent-runtime/engine/pieces/shell.js +47 -0
- package/dist/agent-runtime/engine/pieces/verify.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/verify.js +52 -0
- package/dist/agent-runtime/engine/preflight.d.ts +27 -0
- package/dist/agent-runtime/engine/preflight.js +54 -0
- package/dist/agent-runtime/engine/regex.d.ts +3 -0
- package/dist/agent-runtime/engine/regex.js +19 -0
- package/dist/agent-runtime/engine/run-status.d.ts +563 -0
- package/dist/agent-runtime/engine/run-status.js +61 -0
- package/dist/agent-runtime/engine/runs.d.ts +605 -0
- package/dist/agent-runtime/engine/runs.js +247 -0
- package/dist/agent-runtime/engine/state.d.ts +64 -0
- package/dist/agent-runtime/engine/state.js +98 -0
- package/dist/agent-runtime/engine/steering/inbox.d.ts +54 -0
- package/dist/agent-runtime/engine/steering/inbox.js +113 -0
- package/dist/agent-runtime/engine/storage/fork-allocation.d.ts +21 -0
- package/dist/agent-runtime/engine/storage/fork-allocation.js +143 -0
- package/dist/agent-runtime/engine/storage/private-path.d.ts +11 -0
- package/dist/agent-runtime/engine/storage/private-path.js +74 -0
- package/dist/agent-runtime/engine/store/sqlite-store.d.ts +26 -0
- package/dist/agent-runtime/engine/store/sqlite-store.js +227 -0
- package/dist/agent-runtime/evaluation-corpus.d.ts +125 -0
- package/dist/agent-runtime/evaluation-corpus.js +4 -0
- package/dist/agent-runtime/evaluation.d.ts +9 -3
- package/dist/agent-runtime/evaluation.js +50 -8
- package/dist/agent-runtime/executor-types.d.ts +53 -3
- package/dist/agent-runtime/executor-types.js +35 -2
- package/dist/agent-runtime/executors.js +2 -2
- package/dist/agent-runtime/graph/artifacts.d.ts +7 -0
- package/dist/agent-runtime/graph/artifacts.js +86 -10
- package/dist/agent-runtime/graph/convergence.d.ts +26 -0
- package/dist/agent-runtime/graph/convergence.js +62 -0
- package/dist/agent-runtime/graph/nodes.d.ts +2 -0
- package/dist/agent-runtime/graph/nodes.js +171 -24
- package/dist/agent-runtime/graph/roles.d.ts +6 -0
- package/dist/agent-runtime/graph/roles.js +23 -17
- package/dist/agent-runtime/graph/state.d.ts +5 -0
- package/dist/agent-runtime/graph/state.js +2 -0
- 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 +11 -5
- package/dist/agent-runtime/prompts.js +71 -20
- package/dist/agent-runtime/recovery.d.ts +1 -1
- package/dist/agent-runtime/repository-context.js +20 -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 +18 -3
- 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 +49 -4
- package/dist/pipeline/pipeline-state.js +150 -23
- package/integration-contract.json +84 -12
- package/package.json +12 -5
- package/schemas/agent-runtime.schema.json +93 -1
- package/schemas/workflow-definition.schema.json +312 -0
- package/templates/kimi/specrails/run-skill.mjs +15 -1
- package/templates/commands/specrails/batch-implement.md +0 -32
|
@@ -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>;
|
|
@@ -3,7 +3,25 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { checkArchive, pipelineStateDirectory, transitionPipeline, validateVerificationRequest, } from '../../pipeline/pipeline-state.js';
|
|
6
|
+
import { assertProposalInScope, withScopeDefault } from '../change-scope.js';
|
|
6
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
|
+
}
|
|
7
25
|
export function object(value) {
|
|
8
26
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
9
27
|
throw new Error('Expected a structured JSON object from agent');
|
|
@@ -105,14 +123,16 @@ export function proposedVerification(context, configured, verification) {
|
|
|
105
123
|
throw new Error('Invalid proposed verification command');
|
|
106
124
|
return { repositoryId: command.repositoryId, command: command.command, args: command.args, ...(typeof command.cwd === 'string' && command.cwd ? { cwd: command.cwd } : {}) };
|
|
107
125
|
}).filter(command => !covered.has(command.repositoryId) && context.repositories.some(repository => repository.id === command.repositoryId));
|
|
126
|
+
for (const command of proposed)
|
|
127
|
+
assertProposalInScope(context, command, 'proposed verification command');
|
|
108
128
|
if (proposed.length)
|
|
109
|
-
validateVerificationRequest(context, { kind: 'scoped', commands: proposed });
|
|
129
|
+
validateVerificationRequest(context, { kind: 'scoped', commands: proposed.map(command => withScopeDefault(context, command)) });
|
|
110
130
|
return proposed;
|
|
111
131
|
}
|
|
112
132
|
/** Publish an archive prepared by the real CLI. The durable write set makes a
|
|
113
133
|
* crash between main-spec updates recoverable without applying a delta twice. */
|
|
114
|
-
async function archiveWithOpenSpec(context, change, active, signal) {
|
|
115
|
-
const directory = pipelineStateDirectory(context);
|
|
134
|
+
async function archiveWithOpenSpec(context, change, active, signal, adapter) {
|
|
135
|
+
const directory = adapter?.directory ?? pipelineStateDirectory(context);
|
|
116
136
|
const receipt = path.join(directory, 'openspec-archive.json');
|
|
117
137
|
const files = (root, prefix = '') => {
|
|
118
138
|
if (!existsSync(root))
|
|
@@ -124,7 +144,8 @@ async function archiveWithOpenSpec(context, change, active, signal) {
|
|
|
124
144
|
return entry.isDirectory() ? files(path.join(root, entry.name), relative + '/') : [relative];
|
|
125
145
|
});
|
|
126
146
|
};
|
|
127
|
-
const
|
|
147
|
+
const treeHash = (root) => hash(JSON.stringify(files(root).sort().map(file => [file, readFileSync(artifactPath(root, file), 'utf8')])));
|
|
148
|
+
const activeHash = treeHash(active);
|
|
128
149
|
let plan;
|
|
129
150
|
if (existsSync(receipt))
|
|
130
151
|
plan = JSON.parse(readFileSync(receipt, 'utf8'));
|
|
@@ -134,6 +155,22 @@ async function archiveWithOpenSpec(context, change, active, signal) {
|
|
|
134
155
|
const source = artifactPath(context.artifactRoot, 'openspec');
|
|
135
156
|
files(source); // Reject symlinks before copying or invoking the external CLI.
|
|
136
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
|
+
}
|
|
137
174
|
const output = await runOpenSpec(resolveOpenSpecCli(), staging, ['archive', change, '--yes'], signal);
|
|
138
175
|
const archiveRoot = path.join(staging, 'openspec/changes/archive');
|
|
139
176
|
const archives = existsSync(archiveRoot) ? readdirSync(archiveRoot).filter(name => name.endsWith('-' + change)) : [];
|
|
@@ -148,20 +185,40 @@ async function archiveWithOpenSpec(context, change, active, signal) {
|
|
|
148
185
|
const destination = 'openspec/changes/archive/' + archives[0];
|
|
149
186
|
if (existsSync(artifactPath(context.artifactRoot, destination)))
|
|
150
187
|
throw new Error('OpenSpec archive destination already exists');
|
|
151
|
-
const
|
|
188
|
+
const afterRoot = path.join(staging, 'openspec/specs');
|
|
152
189
|
const names = new Set([...files(beforeRoot), ...files(afterRoot)]);
|
|
153
190
|
const contents = (root, name) => existsSync(path.join(root, name)) ? readFileSync(path.join(root, name), 'utf8') : null;
|
|
154
|
-
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) };
|
|
155
201
|
write(receipt, JSON.stringify(plan));
|
|
156
202
|
}
|
|
157
203
|
finally {
|
|
158
204
|
rmSync(staging, { recursive: true, force: true });
|
|
159
205
|
}
|
|
160
206
|
}
|
|
161
|
-
if (plan.activeHash !== activeHash)
|
|
162
|
-
throw new Error('Reviewed artifacts changed since the OpenSpec archive was prepared');
|
|
163
207
|
if (!new RegExp('^openspec/changes/archive/[0-9]{4}-[0-9]{2}-[0-9]{2}-' + change + '$').test(plan.destination))
|
|
164
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');
|
|
165
222
|
const targets = plan.writes.map(item => {
|
|
166
223
|
if (!item.path.startsWith('openspec/specs/'))
|
|
167
224
|
throw new Error('Invalid OpenSpec archive write');
|
|
@@ -169,16 +226,23 @@ async function archiveWithOpenSpec(context, change, active, signal) {
|
|
|
169
226
|
const current = existsSync(target) ? readFileSync(target, 'utf8') : null;
|
|
170
227
|
if (current !== item.before && current !== item.after)
|
|
171
228
|
throw new Error('Main specification changed during archive; refusing to overwrite: ' + item.path);
|
|
172
|
-
return { ...item, target };
|
|
229
|
+
return { ...item, target, unchanged: current === item.after };
|
|
173
230
|
});
|
|
174
231
|
// The external CLI ran asynchronously; recheck the candidate and artifacts before publishing.
|
|
175
232
|
signal?.throwIfAborted();
|
|
176
|
-
|
|
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);
|
|
177
239
|
// Validate every path and preimage before publishing the first output.
|
|
178
240
|
const destination = artifactPath(context.artifactRoot, plan.destination);
|
|
179
241
|
if (existsSync(destination))
|
|
180
242
|
throw new Error('Archive destination already exists');
|
|
181
243
|
for (const item of targets) {
|
|
244
|
+
if (item.unchanged)
|
|
245
|
+
continue;
|
|
182
246
|
if (item.after === null)
|
|
183
247
|
rmSync(item.target, { force: true });
|
|
184
248
|
else
|
|
@@ -187,6 +251,18 @@ async function archiveWithOpenSpec(context, change, active, signal) {
|
|
|
187
251
|
mkdirSync(path.dirname(destination), { recursive: true });
|
|
188
252
|
renameSync(active, destination);
|
|
189
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
|
+
}
|
|
190
266
|
export async function archive(context, change, signal) {
|
|
191
267
|
const state = journal(context);
|
|
192
268
|
if (state.phases.archive.status !== 'done') {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { VerificationReceipt } from '../../pipeline/pipeline-state.js';
|
|
2
|
+
import type { WorkflowState } from '../workflow-types.js';
|
|
3
|
+
/** One verify visit, as the convergence guard remembers it. */
|
|
4
|
+
export interface VerifyOutcome {
|
|
5
|
+
at: string;
|
|
6
|
+
candidateHash: string;
|
|
7
|
+
planHash: string;
|
|
8
|
+
passed: boolean;
|
|
9
|
+
/** Normalized identity of the first failure: the command plus its output with volatile details removed. */
|
|
10
|
+
signature?: string;
|
|
11
|
+
summary?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Verify visits since the latest explicit resume: a human continuing a blocked run grants a fresh budget, as for attempts. */
|
|
14
|
+
export declare function outcomesSinceResume(history: readonly VerifyOutcome[], checkpoint: WorkflowState): VerifyOutcome[];
|
|
15
|
+
/** Identity and one-line description of a failed receipt (or of a host finding that failed a green receipt). */
|
|
16
|
+
export declare function describeFailure(receipt: Pick<VerificationReceipt, 'commands' | 'reason'>, roots: readonly string[], finding?: string): {
|
|
17
|
+
signature: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Why the failure just recorded (`current`, not yet in `history`) proves the
|
|
22
|
+
* correction loop is not converging, or undefined when another round can help.
|
|
23
|
+
*/
|
|
24
|
+
export declare function divergence(history: readonly VerifyOutcome[], current: VerifyOutcome): string | undefined;
|
|
25
|
+
/** The same test for a candidate that has not been verified again yet: an unchanged failed candidate is not worth a new run. */
|
|
26
|
+
export declare function unchangedFailure(history: readonly VerifyOutcome[], candidateHash: string, planHash: string): VerifyOutcome | undefined;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { fingerprint } from '../durable-store.js';
|
|
2
|
+
/** Verify visits since the latest explicit resume: a human continuing a blocked run grants a fresh budget, as for attempts. */
|
|
3
|
+
export function outcomesSinceResume(history, checkpoint) {
|
|
4
|
+
const resumed = [...checkpoint.events].reverse().find(event => event.type === 'workflow_resumed');
|
|
5
|
+
return history.filter(entry => !resumed || entry.at >= resumed.timestamp);
|
|
6
|
+
}
|
|
7
|
+
function normalized(output, roots) {
|
|
8
|
+
let text = output.replace(/\u001b\[[0-9;?]*[A-Za-z]/g, '');
|
|
9
|
+
for (const root of [...roots].sort((a, b) => b.length - a.length))
|
|
10
|
+
text = text.split(root).join('<repo>');
|
|
11
|
+
return text.split(/\r?\n/)
|
|
12
|
+
.map(line => line.replace(/\b[0-9a-f]{7,64}\b/gi, '#').replace(/\d+(?:[.,]\d+)*/g, '0').replace(/\s+/g, ' ').trim())
|
|
13
|
+
.filter(Boolean).slice(-40).join('\n');
|
|
14
|
+
}
|
|
15
|
+
/** The last line that says what went wrong, bounded. */
|
|
16
|
+
function headline(output) {
|
|
17
|
+
const lines = output.replace(/\u001b\[[0-9;?]*[A-Za-z]/g, '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
18
|
+
const telling = [...lines].reverse().find(line => /error|fail|✕|×|not found|cannot|denied|refused|forbidden|unauthori[sz]ed/i.test(line));
|
|
19
|
+
return (telling ?? lines.at(-1) ?? '').slice(0, 300);
|
|
20
|
+
}
|
|
21
|
+
/** Identity and one-line description of a failed receipt (or of a host finding that failed a green receipt). */
|
|
22
|
+
export function describeFailure(receipt, roots, finding) {
|
|
23
|
+
if (finding)
|
|
24
|
+
return { signature: fingerprint({ finding: normalized(finding, roots) }), summary: finding.slice(0, 300) };
|
|
25
|
+
const failed = receipt.commands.find(command => command.exitCode !== 0);
|
|
26
|
+
if (!failed)
|
|
27
|
+
return { signature: fingerprint({ reason: receipt.reason ?? 'verification failed' }), summary: receipt.reason ?? 'verification failed' };
|
|
28
|
+
const name = failed.label ?? [failed.command, ...failed.args].join(' ');
|
|
29
|
+
const detail = headline(failed.output);
|
|
30
|
+
return {
|
|
31
|
+
signature: fingerprint({ check: failed.key ?? [failed.command, ...failed.args, failed.cwd], exitCode: failed.exitCode, output: normalized(failed.output, roots) }),
|
|
32
|
+
summary: `${name} exited ${failed.exitCode}${detail ? `: ${detail}` : ''}`.slice(0, 400),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Why the failure just recorded (`current`, not yet in `history`) proves the
|
|
37
|
+
* correction loop is not converging, or undefined when another round can help.
|
|
38
|
+
*/
|
|
39
|
+
export function divergence(history, current) {
|
|
40
|
+
if (current.passed || !current.signature)
|
|
41
|
+
return undefined;
|
|
42
|
+
const cycle = history.find(entry => !entry.passed && entry.candidateHash === current.candidateHash && entry.planHash === current.planHash);
|
|
43
|
+
if (cycle)
|
|
44
|
+
return `The correction round returned the change to a state that already failed verification (${current.summary}). Running the same checks again cannot pass.`;
|
|
45
|
+
let repeats = 0;
|
|
46
|
+
for (const entry of [...history].reverse()) {
|
|
47
|
+
if (entry.passed || entry.signature !== current.signature)
|
|
48
|
+
break;
|
|
49
|
+
repeats++;
|
|
50
|
+
}
|
|
51
|
+
if (repeats >= 2)
|
|
52
|
+
return `The same verification failure survived ${repeats} correction rounds unchanged (${current.summary}). It is most likely outside what this change can fix: a pre-existing failure, another package, or the environment.`;
|
|
53
|
+
const previous = history.at(-1);
|
|
54
|
+
if (previous?.passed && history.some(entry => !entry.passed && entry.signature === current.signature)) {
|
|
55
|
+
return `A review correction brought back a verification failure that an earlier correction had fixed (${current.summary}). The review request and the checks pull in opposite directions; decide which one applies before resuming.`;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/** The same test for a candidate that has not been verified again yet: an unchanged failed candidate is not worth a new run. */
|
|
60
|
+
export function unchangedFailure(history, candidateHash, planHash) {
|
|
61
|
+
return [...history].reverse().find(entry => !entry.passed && entry.candidateHash === candidateHash && entry.planHash === planHash);
|
|
62
|
+
}
|
|
@@ -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;
|