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
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { resolveRoleDescriptor } from '../config.js';
|
|
2
|
+
import { isBuiltinRole } from '../executor-types.js';
|
|
1
3
|
import { repositoryContextSnapshot, renderRepositoryContext } from '../repository-context.js';
|
|
2
4
|
import { readRoleState, writeRoleState } from '../role-state.js';
|
|
3
5
|
import { fingerprint } from '../durable-store.js';
|
|
4
6
|
import { selectRoleRoute } from '../role-routing.js';
|
|
5
|
-
import {
|
|
7
|
+
import { openSpecSkill, OPENSPEC_VERSION, OpenSpecTools, OpenSpecParticipationError, openSpecRepairPrompt } from '../openspec.js';
|
|
6
8
|
import path from 'node:path';
|
|
7
9
|
import { performance } from 'node:perf_hooks';
|
|
8
10
|
import { frozenAcceptanceCriteria, pipelineStateDirectory } from '../../pipeline/pipeline-state.js';
|
|
@@ -48,31 +50,33 @@ export function createRoleInvoker(deps) {
|
|
|
48
50
|
}
|
|
49
51
|
catch { /* Observer cannot replay agent effects. */ } };
|
|
50
52
|
const execute = async (role, step, prompt, structured, extra) => {
|
|
51
|
-
const directory = deps.openspec?.[role]
|
|
52
|
-
const saved = readRoleState(directory);
|
|
53
|
+
const directory = deps.openspec?.[role]?.stateDirectory ?? path.join(pipelineStateDirectory(context), 'agent-workflow');
|
|
54
|
+
const saved = deps.roleState?.read() ?? readRoleState(directory);
|
|
55
|
+
const persist = () => deps.roleState ? deps.roleState.write(saved) : writeRoleState(directory, saved);
|
|
53
56
|
const budget = step.remainingBudget();
|
|
54
57
|
if (budget.maxTokens === 0 || budget.maxCostUsd === 0 || step.signal.aborted)
|
|
55
58
|
throw new AgentExecutionError('No budget remains for another role invocation', step.signal.aborted ? 'aborted' : 'budget_exhausted', { inputTokens: 0, outputTokens: 0, costUsd: 0 });
|
|
56
59
|
const kind = extra.kind ?? (extra.resumeSessionId ? 'correction' : 'initial');
|
|
57
|
-
const
|
|
60
|
+
const descriptor = resolveRoleDescriptor(config, role);
|
|
61
|
+
const route = selectRoleRoute(role, extra.agentOverride ?? (isBuiltinRole(role) ? config.agents[role] : descriptor), kind, step.checkpoint, extra.agentOverride ? undefined : saved.routes[role]);
|
|
58
62
|
const selected = route.selection;
|
|
59
63
|
const capabilities = await registry.capabilities(selected.provider, selected.model);
|
|
60
|
-
const identity = fingerprint({ role, selected, transport: capabilities.transport, instructionsVersion: ROLE_INSTRUCTIONS_VERSION, provider: config.providers.find(provider => provider.id === selected.provider) ?? selected.provider, definition: (extra.agentOverride ? config.rolePrompts?.fixer : config.rolePrompts?.[role]) ?? null, openspec: deps.openspec?.[role] ?? null, context });
|
|
64
|
+
const identity = fingerprint({ role, selected, transport: capabilities.transport, instructionsVersion: ROLE_INSTRUCTIONS_VERSION, provider: config.providers.find(provider => provider.id === selected.provider) ?? selected.provider, definition: (extra.agentOverride ? config.rolePrompts?.fixer : config.rolePrompts?.[role] ?? descriptor.prompt) ?? null, ...(isBuiltinRole(role) ? {} : { policy: descriptor }), openspec: deps.openspec?.[role] ?? null, context });
|
|
61
65
|
const previous = saved.sessions[role];
|
|
62
66
|
const resumeSessionId = capabilities.continuation === 'supported' && previous?.identity === identity && previous.sessionId === extra.resumeSessionId ? extra.resumeSessionId : undefined;
|
|
63
67
|
const snapshot = repositoryContextSnapshot(context);
|
|
64
|
-
const incremental = Boolean(resumeSessionId && config.efficiency?.contextMode !== 'full');
|
|
68
|
+
const incremental = Boolean(resumeSessionId && capabilities.resumeRequiresFullContext !== true && config.efficiency?.contextMode !== 'full');
|
|
65
69
|
const packet = renderRepositoryContext(snapshot, incremental ? previous?.context : undefined);
|
|
66
|
-
const rolePrompt = !resumeSessionId && extra.fallbackPrompt ? extra.fallbackPrompt : prompt;
|
|
70
|
+
const rolePrompt = (!resumeSessionId || capabilities.resumeRequiresFullContext) && extra.fallbackPrompt ? extra.fallbackPrompt : prompt;
|
|
67
71
|
const obligations = '\nCurrent frozen acceptance obligations (all remain required):\n' + JSON.stringify(frozenAcceptanceCriteria(context));
|
|
68
|
-
const fullPrompt = rolePrompt + obligations + '\n\n' + packet;
|
|
72
|
+
const fullPrompt = rolePrompt + (step.operatorSteering ?? '') + obligations + '\n\n' + packet;
|
|
69
73
|
if (extra.agentOverride)
|
|
70
74
|
note(role, `Fixer route: ${selected.provider}/${selected.model ?? 'provider default'} — correction round on the fixer engine`);
|
|
71
75
|
else if (saved.routes[role]?.tier !== route.tier)
|
|
72
76
|
note(role, `Role route: ${selected.provider}/${selected.model ?? 'provider default'} — ${route.reason}`);
|
|
73
77
|
if (!extra.agentOverride) {
|
|
74
78
|
saved.routes[role] = { tier: route.tier, reason: route.reason, attemptId: step.attemptId };
|
|
75
|
-
|
|
79
|
+
persist();
|
|
76
80
|
}
|
|
77
81
|
const measurement = { provider: selected.provider, ...(selected.model ? { model: selected.model } : {}),
|
|
78
82
|
contextMode: incremental ? 'incremental' : 'full', promptBytes: Buffer.byteLength(fullPrompt), contextBytes: Buffer.byteLength(packet), handoffBytes: Buffer.byteLength(rolePrompt), requestedEffort: selected.effort ?? null,
|
|
@@ -85,11 +89,12 @@ export function createRoleInvoker(deps) {
|
|
|
85
89
|
const onEvent = forward(extra.stance === 'fixer' ? 'fixer' : role, structured);
|
|
86
90
|
try {
|
|
87
91
|
if (deps.openspec?.[role])
|
|
88
|
-
note(role, `OpenSpec ${OPENSPEC_VERSION}: ${
|
|
92
|
+
note(role, `OpenSpec ${OPENSPEC_VERSION}: ${openSpecSkill(deps.openspec[role])} (official skill document through scoped tools).`);
|
|
89
93
|
const result = await registry.execute(selected.provider, {
|
|
90
|
-
role, prompt: fullPrompt, openspec: deps.openspec?.[role] ? { ...deps.openspec[role], ...(role === 'architect' ? {} : { evidenceScope: { backlogRoot: context.backlogRoot, runId: context.runId } }) } : undefined, cwd: context.artifactRoot, allowedRoots: context.repositories.map(repo => repo.path),
|
|
94
|
+
role, access: descriptor.access, artifacts: descriptor.artifacts, instructions: 'role', prompt: fullPrompt, openspec: deps.openspec?.[role] ? { ...deps.openspec[role], ...(role === 'architect' ? {} : { evidenceScope: { backlogRoot: context.backlogRoot, runId: context.runId } }) } : undefined, cwd: context.artifactRoot, allowedRoots: context.repositories.map(repo => repo.path),
|
|
91
95
|
model: selected.model, effort: selected.effort, ...(selected.thinking ? { thinking: selected.thinking } : {}), maxTurns: selected.maxTurns, signal: step.signal,
|
|
92
|
-
timeoutMs: config.limits?.timeoutMs,
|
|
96
|
+
timeoutMs: extra.timeoutMs ?? config.limits?.timeoutMs,
|
|
97
|
+
idleTimeoutMs: extra.idleTimeoutMs ?? config.limits?.idleTimeoutMs,
|
|
93
98
|
maxTokens: budget.maxTokens, maxCostUsd: budget.maxCostUsd,
|
|
94
99
|
...(config.guardrails ? { guardrails: config.guardrails } : {}),
|
|
95
100
|
...(extra.stance ? { stance: extra.stance } : {}),
|
|
@@ -103,7 +108,7 @@ export function createRoleInvoker(deps) {
|
|
|
103
108
|
saved.sessions[role] = { identity, sessionId: result.sessionId, context: snapshot };
|
|
104
109
|
else
|
|
105
110
|
delete saved.sessions[role];
|
|
106
|
-
|
|
111
|
+
persist();
|
|
107
112
|
succeeded = true;
|
|
108
113
|
return result;
|
|
109
114
|
}
|
|
@@ -119,6 +124,7 @@ export function createRoleInvoker(deps) {
|
|
|
119
124
|
}
|
|
120
125
|
};
|
|
121
126
|
return async (role, step, options, accept) => {
|
|
127
|
+
const timers = { timeoutMs: options.timeoutMs, idleTimeoutMs: options.idleTimeoutMs };
|
|
122
128
|
const structured = options.structured === true;
|
|
123
129
|
const workflow = deps.openspec?.[role] ? new OpenSpecTools(deps.openspec[role]) : undefined;
|
|
124
130
|
const cursor = workflow?.participationCursor() ?? 0;
|
|
@@ -146,17 +152,17 @@ export function createRoleInvoker(deps) {
|
|
|
146
152
|
// A correction pass continues the role's own session: the work it did and
|
|
147
153
|
// the reasons behind it are already in context.
|
|
148
154
|
try {
|
|
149
|
-
result = await execute(role, step, options.prompt, structured, { kind: options.kind, resumeSessionId: options.resumeSessionId, outputSchema: options.outputSchema, fallbackPrompt: options.fallbackPrompt, agentOverride: options.agentOverride, stance: options.stance });
|
|
155
|
+
result = await execute(role, step, options.prompt, structured, { ...timers, kind: options.kind, resumeSessionId: options.resumeSessionId, outputSchema: options.outputSchema, fallbackPrompt: options.fallbackPrompt, agentOverride: options.agentOverride, stance: options.stance });
|
|
150
156
|
}
|
|
151
157
|
catch (error) {
|
|
152
158
|
if (!(error instanceof AgentExecutionError) || !SESSION_FALLBACK_CODES.has(error.code))
|
|
153
159
|
throw error;
|
|
154
160
|
note(role, `Previous ${role} session is unavailable; starting a fresh ${role} turn with the same instructions.`);
|
|
155
|
-
result = await execute(role, step, options.fallbackPrompt, structured, { kind: 'session-fallback', outputSchema: options.outputSchema, agentOverride: options.agentOverride, stance: options.stance });
|
|
161
|
+
result = await execute(role, step, options.fallbackPrompt, structured, { ...timers, kind: 'session-fallback', outputSchema: options.outputSchema, agentOverride: options.agentOverride, stance: options.stance });
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
else {
|
|
159
|
-
result = await execute(role, step, options.prompt, structured, { kind: options.kind, resumeSessionId: options.resumeSessionId, outputSchema: options.outputSchema, fallbackPrompt: options.fallbackPrompt, agentOverride: options.agentOverride, stance: options.stance });
|
|
165
|
+
result = await execute(role, step, options.prompt, structured, { ...timers, kind: options.kind, resumeSessionId: options.resumeSessionId, outputSchema: options.outputSchema, fallbackPrompt: options.fallbackPrompt, agentOverride: options.agentOverride, stance: options.stance });
|
|
160
166
|
}
|
|
161
167
|
let problem, omittedWorkflow = false;
|
|
162
168
|
try {
|
|
@@ -175,7 +181,7 @@ export function createRoleInvoker(deps) {
|
|
|
175
181
|
note(role, omittedWorkflow
|
|
176
182
|
? `The ${role} omitted its required OpenSpec workflow; requesting one correction in the same role before accepting the result.`
|
|
177
183
|
: `The ${role} reply was not a valid result (${problem}); asking the same session to resend it.`);
|
|
178
|
-
const repaired = await execute(role, step, result.sessionId ? repair : (options.fallbackPrompt ?? options.prompt) + '\n' + repair, structured, { kind: 'repair', resumeSessionId: result.sessionId, outputSchema: options.outputSchema,
|
|
184
|
+
const repaired = await execute(role, step, result.sessionId ? repair : (options.fallbackPrompt ?? options.prompt) + '\n' + repair, structured, { ...timers, kind: 'repair', resumeSessionId: result.sessionId, outputSchema: options.outputSchema,
|
|
179
185
|
fallbackPrompt: (options.fallbackPrompt ?? options.prompt) + '\nPrevious response (bounded):\n' + result.text.slice(-32_000) + '\n' + repair,
|
|
180
186
|
});
|
|
181
187
|
try {
|
|
@@ -186,6 +192,8 @@ export function createRoleInvoker(deps) {
|
|
|
186
192
|
}
|
|
187
193
|
}
|
|
188
194
|
catch (error) {
|
|
195
|
+
if (deps.propagateInterruptions && error instanceof AgentExecutionError && ['aborted', 'timeout', 'idle_timeout', 'lease_lost'].includes(error.code))
|
|
196
|
+
throw error;
|
|
189
197
|
return { ok: false, error: error instanceof Error ? error.message : String(error), ...(error instanceof AgentExecutionError ? { code: error.code } : {}) };
|
|
190
198
|
}
|
|
191
199
|
};
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { toolEvent } from './tool-event.js';
|
|
2
2
|
import { normalizeKimiCliModel } from './kimi-model.js';
|
|
3
|
-
import { AgentExecutionError, unknownUsage,
|
|
3
|
+
import { AgentExecutionError, unknownUsage, normalizeAgentRequest } from './executor-types.js';
|
|
4
4
|
import { runCliProcess } from './cli-process.js';
|
|
5
5
|
import { parseStructuredText } from './openai-executor.js';
|
|
6
6
|
import { WorkspaceTools } from './workspace-tools.js';
|
|
7
7
|
function record(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
|
|
8
8
|
/** Native ACP v1 keeps Kimi 0.27 read-only roles usable without a platform skill or a model call during capability negotiation. */
|
|
9
9
|
export async function executeKimiReadonlyAcp(request, options = {}) {
|
|
10
|
-
|
|
11
|
-
if (request.
|
|
10
|
+
request = normalizeAgentRequest(request);
|
|
11
|
+
if (request.access === 'write' && !options.openspecBridge)
|
|
12
12
|
throw new AgentExecutionError('Read-only ACP transport cannot execute developer roles', 'invalid_request');
|
|
13
13
|
if (request.maxCostUsd !== undefined)
|
|
14
14
|
throw new AgentExecutionError('Kimi cannot enforce a strict USD cap', 'cost_limit_unsupported');
|
|
15
15
|
if (request.maxTokens !== undefined)
|
|
16
16
|
throw new AgentExecutionError('Kimi 0.27 ACP does not report authoritative token usage. Remove the token cap or choose another provider for this role.', 'usage_unavailable');
|
|
17
|
-
const readOnly = request.
|
|
17
|
+
const readOnly = request.access === 'read';
|
|
18
18
|
const mode = readOnly ? 'plan' : 'auto';
|
|
19
|
-
const tools = new WorkspaceTools(request.cwd, request.allowedRoots, request.
|
|
19
|
+
const tools = new WorkspaceTools(request.cwd, request.allowedRoots, request.access);
|
|
20
20
|
let transport, nextId = 0, sessionId, text = '', completed = false, turns = 0, permissionDenied = false;
|
|
21
21
|
const calls = new Map();
|
|
22
22
|
const send = (method, params, success) => {
|
|
@@ -157,7 +157,7 @@ export async function executeKimiReadonlyAcp(request, options = {}) {
|
|
|
157
157
|
call.success(record(message.result));
|
|
158
158
|
};
|
|
159
159
|
const result = await (options.runProcess ?? runCliProcess)({ command: 'kimi', args: ['acp'] }, {
|
|
160
|
-
cwd: tools.cwd, signal: request.signal, timeoutMs: request.timeoutMs ?? 15 * 60_000, env: options.env, onLine: onMessage,
|
|
160
|
+
cwd: tools.cwd, signal: request.signal, timeoutMs: request.timeoutMs ?? 15 * 60_000, idleTimeoutMs: request.idleTimeoutMs, env: options.env, onLine: onMessage,
|
|
161
161
|
duplex: control => {
|
|
162
162
|
transport = control;
|
|
163
163
|
send('initialize', { protocolVersion: 1, clientInfo: { name: 'specrails-core', version: '1' }, clientCapabilities: { fs: { readTextFile: readOnly, writeTextFile: readOnly }, terminal: readOnly } }, initialized => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readVerificationEvidence } from '../pipeline/pipeline-state.js';
|
|
2
2
|
import { assertEffortSupported, unknownCapabilities } from './capabilities.js';
|
|
3
3
|
import { OpenSpecTools, OPENSPEC_TOOL_DEFINITION, openSpecPrompt } from './openspec.js';
|
|
4
|
-
import { AgentExecutionError, unknownUsage,
|
|
4
|
+
import { AgentExecutionError, unknownUsage, normalizeAgentRequest, isBuiltinRole } from './executor-types.js';
|
|
5
5
|
import { WorkspaceTools } from './workspace-tools.js';
|
|
6
6
|
import { ChatClient, record } from './compact/chat-client.js';
|
|
7
7
|
import { DEFAULT_CONTEXT_WINDOW_TOKENS, runToolLoop } from './compact/guarded-loop.js';
|
|
@@ -40,13 +40,15 @@ export class OpenAICompatibleExecutor {
|
|
|
40
40
|
throw new AgentExecutionError('A strict USD cap requires an executor with a native spending limit; OpenAI-compatible endpoints do not provide one. Use a token cap or an executor with native cost enforcement.', 'cost_limit_unsupported');
|
|
41
41
|
}
|
|
42
42
|
async execute(request) {
|
|
43
|
-
|
|
43
|
+
request = normalizeAgentRequest(request);
|
|
44
|
+
if (request.nativeCommand)
|
|
45
|
+
throw new AgentExecutionError('OpenAI-compatible providers do not support native commands', 'native_command_unsupported');
|
|
44
46
|
assertEffortSupported(request, this.capabilities());
|
|
45
47
|
this.validateLimits(request);
|
|
46
48
|
if (!request.model?.trim())
|
|
47
49
|
throw new AgentExecutionError('OpenAI-compatible execution requires a model', 'invalid_model');
|
|
48
50
|
const controller = new AbortController();
|
|
49
|
-
const toolset = new WorkspaceTools(request.cwd, request.allowedRoots, request.
|
|
51
|
+
const toolset = new WorkspaceTools(request.cwd, request.allowedRoots, request.access);
|
|
50
52
|
const openspec = request.openspec ? new OpenSpecTools(request.openspec, controller.signal) : undefined;
|
|
51
53
|
const evidenceScope = request.openspec?.evidenceScope && request.role !== 'architect' ? request.openspec.evidenceScope : undefined;
|
|
52
54
|
const definitions = [...toolset.definitions(), ...(openspec ? [OPENSPEC_TOOL_DEFINITION] : []), ...(evidenceScope ? [{ type: 'function', function: { name: 'read_verification_evidence', description: 'Read host verification evidence and source files with opaque IDs and bounded cursors. List to discover IDs.', parameters: { type: 'object', additionalProperties: false, properties: { id: { type: 'string' }, section: { type: 'string', enum: ['summary', 'stdout', 'stderr', 'source'] }, sourceId: { type: 'string' }, cursor: { type: 'string' }, limit: { type: 'integer', minimum: 1, maximum: 100 } } } } }] : [])];
|
|
@@ -56,11 +58,13 @@ export class OpenAICompatibleExecutor {
|
|
|
56
58
|
// The compact pipelines are several bounded model calls per role (the
|
|
57
59
|
// architect alone is ~8); a role budget sized for one agentic call would
|
|
58
60
|
// cut them off. Scale the DEFAULT only — an explicit timeout is respected.
|
|
59
|
-
const compact = resolveAgentLoop(this.provider) === 'compact' && request.openspec !== undefined;
|
|
61
|
+
const compact = isBuiltinRole(request.role) && request.instructions === 'role' && resolveAgentLoop(this.provider) === 'compact' && request.openspec !== undefined;
|
|
60
62
|
const timeoutMs = request.timeoutMs ?? this.options.defaultTimeoutMs ?? (compact ? 45 * 60_000 : 15 * 60_000);
|
|
61
|
-
const idleTimeoutMs = this.options.idleTimeoutMs ?? IDLE_TIMEOUT_MS;
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
const idleTimeoutMs = request.idleTimeoutMs ?? this.options.idleTimeoutMs ?? IDLE_TIMEOUT_MS;
|
|
64
|
+
for (const value of [timeoutMs, idleTimeoutMs]) {
|
|
65
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 2_147_483_647)
|
|
66
|
+
throw new AgentExecutionError('Timeouts must be nonnegative timer integers', 'invalid_limit');
|
|
67
|
+
}
|
|
64
68
|
const env = this.options.env ?? process.env;
|
|
65
69
|
const key = this.provider.apiKeyEnv ? env[this.provider.apiKeyEnv] : undefined;
|
|
66
70
|
if (this.provider.apiKeyEnv && !key)
|
|
@@ -81,10 +85,11 @@ export class OpenAICompatibleExecutor {
|
|
|
81
85
|
// group role while it is still producing (observed: groups 1–2 verified
|
|
82
86
|
// green, group 3 cut mid-patch, whole run failed). A genuine hang is the
|
|
83
87
|
// idle watchdog's job: no tool call, reply or usage for IDLE_TIMEOUT_MS.
|
|
84
|
-
let timer
|
|
85
|
-
const resetDeadline = () => { clearTimeout(timer); timer = setTimeout(() => controller.abort(new Error('Agent timeout')), timeoutMs); };
|
|
88
|
+
let timer;
|
|
89
|
+
const resetDeadline = () => { clearTimeout(timer); timer = timeoutMs > 0 ? setTimeout(() => controller.abort(new Error('Agent timeout')), timeoutMs) : undefined; };
|
|
90
|
+
resetDeadline();
|
|
86
91
|
let lastActivity = Date.now();
|
|
87
|
-
const idle = compact ? setInterval(() => { if (Date.now() - lastActivity > idleTimeoutMs)
|
|
92
|
+
const idle = (compact || request.idleTimeoutMs !== undefined) && idleTimeoutMs > 0 ? setInterval(() => { if (Date.now() - lastActivity > idleTimeoutMs)
|
|
88
93
|
controller.abort(new Error('Agent idle timeout')); }, Math.min(15_000, Math.max(50, Math.floor(idleTimeoutMs / 4)))) : undefined;
|
|
89
94
|
idle?.unref?.();
|
|
90
95
|
const onEvent = request.onEvent || idle ? (event) => { lastActivity = Date.now(); request.onEvent?.(event); } : undefined;
|
|
@@ -95,13 +100,13 @@ export class OpenAICompatibleExecutor {
|
|
|
95
100
|
try {
|
|
96
101
|
// Compact pipelines need the official workflow to drive; a plain role task
|
|
97
102
|
// without an OpenSpec binding runs the guarded loop instead.
|
|
98
|
-
if (
|
|
103
|
+
if (compact && openspec) {
|
|
99
104
|
compactMode = true;
|
|
100
105
|
const compact = { client, request, inputs: extractPromptInputs(request.prompt), toolset, openspec, contextWindowTokens, ...(maxOutputTokens === undefined ? {} : { maxOutputTokens }), resetDeadline, signal: controller.signal, onEvent, guardrails: request.guardrails, stance: request.stance };
|
|
101
106
|
return request.role === 'architect' ? await runCompactArchitect(compact) : request.role === 'developer' ? await runCompactDeveloper(compact) : await runCompactReviewer(compact);
|
|
102
107
|
}
|
|
103
108
|
const messages = [
|
|
104
|
-
{ role: 'system', content: `You execute one ${request.role} task. Use only the provided workspace tools. Allowed roots: ${JSON.stringify(toolset.roots)}. Working directory: ${toolset.cwd}. Return the complete requested final result. Do not coordinate another workflow. Execute only the assigned role and its supplied OpenSpec workflow.` },
|
|
109
|
+
...(request.instructions === 'none' ? [] : [{ role: 'system', content: `You execute one ${request.role} task. Use only the provided workspace tools. Allowed roots: ${JSON.stringify(toolset.roots)}. Working directory: ${toolset.cwd}. Return the complete requested final result. Do not coordinate another workflow. Execute only the assigned role and its supplied OpenSpec workflow.` }]),
|
|
105
110
|
{ role: 'user', content: (request.openspec ? openSpecPrompt(request.openspec) : '') + request.prompt },
|
|
106
111
|
];
|
|
107
112
|
const { text } = await runToolLoop({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentRole, type CliProvider, type RoleDescriptor, type AgentAccess, type ArtifactAccess, type OpenSpecSkill } from './executor-types.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
export declare const OPENSPEC_VERSION = "1.4.1";
|
|
4
4
|
export declare const ROLE_SKILLS: {
|
|
@@ -18,7 +18,16 @@ export interface OpenSpecRoleContext {
|
|
|
18
18
|
skillPath: string;
|
|
19
19
|
skillHash: string;
|
|
20
20
|
role: AgentRole;
|
|
21
|
+
access?: AgentAccess;
|
|
22
|
+
artifacts?: ArtifactAccess;
|
|
23
|
+
openspecSkill?: OpenSpecSkill;
|
|
21
24
|
}
|
|
25
|
+
/** Saved built-in contexts retain their original shape; new roles carry explicit policy. */
|
|
26
|
+
export declare function openSpecPolicy(context: OpenSpecRoleContext): {
|
|
27
|
+
access: AgentAccess;
|
|
28
|
+
artifacts: ArtifactAccess;
|
|
29
|
+
};
|
|
30
|
+
export declare function openSpecSkill(context: OpenSpecRoleContext): OpenSpecSkill;
|
|
22
31
|
export interface OpenSpecStatus {
|
|
23
32
|
changeRoot: string;
|
|
24
33
|
schemaName: string;
|
|
@@ -64,6 +73,12 @@ export declare const IMPLEMENTATION_PROGRESS_SCHEMA: z.ZodObject<{
|
|
|
64
73
|
}, z.core.$strict>;
|
|
65
74
|
export declare function hash(text: string): string;
|
|
66
75
|
export declare function resolveOpenSpecCli(): string;
|
|
76
|
+
export declare class OpenSpecCommandError extends Error {
|
|
77
|
+
readonly exitCode: number | undefined;
|
|
78
|
+
readonly stdout: string;
|
|
79
|
+
readonly processCode?: string | undefined;
|
|
80
|
+
constructor(message: string, exitCode: number | undefined, stdout: string, processCode?: string | undefined);
|
|
81
|
+
}
|
|
67
82
|
export declare function runOpenSpec(cli: string, root: string, args: string[], signal?: AbortSignal): Promise<string>;
|
|
68
83
|
/** Reject symlink traversal even when the final destination does not exist. */
|
|
69
84
|
export declare function artifactPath(root: string, relative: string): string;
|
|
@@ -73,7 +88,7 @@ export declare function prepareOpenSpec(root: string, change: string, directory:
|
|
|
73
88
|
skillRoot: string;
|
|
74
89
|
identity: Record<string, string>;
|
|
75
90
|
};
|
|
76
|
-
export declare function roleOpenSpecContext(prepared: ReturnType<typeof prepareOpenSpec>, root: string, change: string, directory: string,
|
|
91
|
+
export declare function roleOpenSpecContext(prepared: ReturnType<typeof prepareOpenSpec>, root: string, change: string, directory: string, roleOrDescriptor: AgentRole | RoleDescriptor, provider: CliProvider): OpenSpecRoleContext;
|
|
77
92
|
/** OpenSpec uses cwd paths; Windows may report a different case or short-name
|
|
78
93
|
* spelling than realpathSync. Compare existing filesystem identities, not text. */
|
|
79
94
|
export declare function sameOpenSpecDirectory(candidate: unknown, expected: string): boolean;
|
|
@@ -5,9 +5,21 @@ import { appendFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFile
|
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { BUILTIN_ROLE_POLICY, isBuiltinRole } from './executor-types.js';
|
|
8
9
|
import { z } from 'zod';
|
|
9
10
|
export const OPENSPEC_VERSION = '1.4.1';
|
|
10
11
|
export const ROLE_SKILLS = { architect: 'openspec-ff-change', developer: 'openspec-apply-change', reviewer: 'openspec-verify-change' };
|
|
12
|
+
/** Saved built-in contexts retain their original shape; new roles carry explicit policy. */
|
|
13
|
+
export function openSpecPolicy(context) {
|
|
14
|
+
const legacy = isBuiltinRole(context.role) ? BUILTIN_ROLE_POLICY[context.role] : undefined;
|
|
15
|
+
return { access: context.access ?? legacy?.access ?? 'read', artifacts: context.artifacts ?? legacy?.artifacts ?? 'none' };
|
|
16
|
+
}
|
|
17
|
+
export function openSpecSkill(context) {
|
|
18
|
+
const skill = context.openspecSkill ?? (isBuiltinRole(context.role) ? ROLE_SKILLS[context.role] : undefined);
|
|
19
|
+
if (!skill)
|
|
20
|
+
throw new Error('Role has no declared OpenSpec skill');
|
|
21
|
+
return skill;
|
|
22
|
+
}
|
|
11
23
|
/** Advisory implementation memory, deliberately separate from verification receipts. */
|
|
12
24
|
export const IMPLEMENTATION_PROGRESS_SCHEMA = z.object({
|
|
13
25
|
summary: z.string().trim().min(1).max(1600),
|
|
@@ -37,8 +49,8 @@ function readProgress(context) {
|
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
51
|
function writeProgress(context, input) {
|
|
40
|
-
if (context.
|
|
41
|
-
throw new Error('
|
|
52
|
+
if (openSpecPolicy(context).artifacts === 'none')
|
|
53
|
+
throw new Error('Role cannot write implementation progress');
|
|
42
54
|
const progress = IMPLEMENTATION_PROGRESS_SCHEMA.parse(input);
|
|
43
55
|
const record = { schemaVersion: 1, root: realpathSync(context.root), change: context.change, updatedAt: new Date().toISOString(), progress };
|
|
44
56
|
const content = JSON.stringify(record);
|
|
@@ -64,6 +76,18 @@ export function resolveOpenSpecCli() {
|
|
|
64
76
|
throw new Error(`OpenSpec ${OPENSPEC_VERSION} required; found ${manifest.version}`);
|
|
65
77
|
return path.join(path.dirname(pkg), 'bin/openspec.js');
|
|
66
78
|
}
|
|
79
|
+
export class OpenSpecCommandError extends Error {
|
|
80
|
+
exitCode;
|
|
81
|
+
stdout;
|
|
82
|
+
processCode;
|
|
83
|
+
constructor(message, exitCode, stdout, processCode) {
|
|
84
|
+
super(message);
|
|
85
|
+
this.exitCode = exitCode;
|
|
86
|
+
this.stdout = stdout;
|
|
87
|
+
this.processCode = processCode;
|
|
88
|
+
this.name = 'OpenSpecCommandError';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
67
91
|
export async function runOpenSpec(cli, root, args, signal) {
|
|
68
92
|
try {
|
|
69
93
|
const result = await promisify(execFile)(process.execPath, [cli, ...args], {
|
|
@@ -75,7 +99,7 @@ export async function runOpenSpec(cli, root, args, signal) {
|
|
|
75
99
|
catch (error) {
|
|
76
100
|
const result = error;
|
|
77
101
|
const detail = [result.stdout, result.stderr].filter(Boolean).join('\n').slice(-6000);
|
|
78
|
-
throw new
|
|
102
|
+
throw new OpenSpecCommandError(`OpenSpec ${args[0]} failed (${result.status ?? result.code ?? 'process error'}): ${detail}`, typeof result.status === 'number' ? result.status : typeof result.code === 'number' ? result.code : undefined, result.stdout ?? '', typeof result.code === 'string' ? result.code : undefined);
|
|
79
103
|
}
|
|
80
104
|
}
|
|
81
105
|
/** Reject symlink traversal even when the final destination does not exist. */
|
|
@@ -143,10 +167,16 @@ export function prepareOpenSpec(root, change, directory) {
|
|
|
143
167
|
identity.projectConfig = existsSync(config) ? hash(readFileSync(config, 'utf8')) : 'absent';
|
|
144
168
|
return { cli, skillRoot, identity };
|
|
145
169
|
}
|
|
146
|
-
export function roleOpenSpecContext(prepared, root, change, directory,
|
|
170
|
+
export function roleOpenSpecContext(prepared, root, change, directory, roleOrDescriptor, provider) {
|
|
171
|
+
const role = typeof roleOrDescriptor === 'string' ? roleOrDescriptor : roleOrDescriptor.id;
|
|
172
|
+
const descriptor = typeof roleOrDescriptor === 'string' ? undefined : roleOrDescriptor;
|
|
173
|
+
const skill = descriptor?.openspecSkill ?? (isBuiltinRole(role) ? ROLE_SKILLS[role] : undefined);
|
|
174
|
+
if (!skill)
|
|
175
|
+
throw new Error('Role has no declared OpenSpec skill');
|
|
147
176
|
return { root, change, stateDirectory: directory, cli: prepared.cli, role,
|
|
148
|
-
skillPath: path.join(prepared.skillRoot, provider,
|
|
149
|
-
skillHash: prepared.identity[`${provider}/${
|
|
177
|
+
skillPath: path.join(prepared.skillRoot, provider, skill, 'SKILL.md'),
|
|
178
|
+
skillHash: prepared.identity[`${provider}/${skill}`],
|
|
179
|
+
...(isBuiltinRole(role) ? {} : { access: descriptor.access, artifacts: descriptor.artifacts, openspecSkill: skill }),
|
|
150
180
|
};
|
|
151
181
|
}
|
|
152
182
|
/** OpenSpec uses cwd paths; Windows may report a different case or short-name
|
|
@@ -197,20 +227,20 @@ export class OpenSpecTools {
|
|
|
197
227
|
// Both official apply and verify start with these read-only CLI queries.
|
|
198
228
|
// Execute them as part of the agent's tool request and return their actual
|
|
199
229
|
// outputs, so loading a skill cannot omit its required planning context.
|
|
200
|
-
const planning = this.context
|
|
230
|
+
const planning = openSpecSkill(this.context) === 'openspec-ff-change' ? undefined : {
|
|
201
231
|
status: await this.status(),
|
|
202
232
|
apply: await this.call(['instructions', 'apply', '--change', this.context.change, '--json']),
|
|
203
233
|
};
|
|
204
234
|
if (planning)
|
|
205
235
|
prerequisites.push({ action: 'status', via: 'load_skill' }, { action: 'instructions', artifact: 'apply', via: 'load_skill' });
|
|
206
|
-
result = { name:
|
|
236
|
+
result = { name: openSpecSkill(this.context), source: this.context.skillPath, version: OPENSPEC_VERSION, content,
|
|
207
237
|
...(planning ? { planning, next: 'The official status and instructions apply queries have executed for this request. Read every planning.apply.contextFiles path, then perform the remaining role skill steps. This is planning context, not proof that implementation or review is complete.' } : {}),
|
|
208
|
-
...(this.context
|
|
238
|
+
...(openSpecSkill(this.context) !== 'openspec-ff-change' ? { savedProgress: readProgress(this.context) } : {}),
|
|
209
239
|
};
|
|
210
240
|
}
|
|
211
241
|
else if (action === 'new') {
|
|
212
|
-
if (this.context.
|
|
213
|
-
throw new Error('
|
|
242
|
+
if (openSpecPolicy(this.context).artifacts !== 'all')
|
|
243
|
+
throw new Error('Role cannot create a change');
|
|
214
244
|
const target = artifactPath(this.context.root, `openspec/changes/${this.context.change}`);
|
|
215
245
|
result = existsSync(target) ? await this.status() : await this.call(['new', 'change', this.context.change, '--json']);
|
|
216
246
|
await this.status();
|
|
@@ -230,22 +260,22 @@ export class OpenSpecTools {
|
|
|
230
260
|
else if (action === 'validate')
|
|
231
261
|
result = await this.validate();
|
|
232
262
|
else if (action === 'write_artifact') {
|
|
233
|
-
if (this.context.
|
|
234
|
-
throw new Error('Reviewer cannot write artifacts');
|
|
263
|
+
if (openSpecPolicy(this.context).artifacts === 'none')
|
|
264
|
+
throw new Error(this.context.role === 'reviewer' ? 'Reviewer cannot write artifacts' : 'Role cannot write artifacts');
|
|
235
265
|
const status = await this.status();
|
|
236
266
|
if (!input.path?.endsWith('.md') || typeof input.content !== 'string' || Buffer.byteLength(input.content) > 256 * 1024)
|
|
237
267
|
throw new Error('Invalid artifact content');
|
|
238
|
-
if (this.context.
|
|
268
|
+
if (openSpecPolicy(this.context).artifacts === 'tasks-checkboxes' && input.path !== 'tasks.md')
|
|
239
269
|
throw new Error('Developer may update only OpenSpec tasks');
|
|
240
270
|
if (!['proposal.md', 'design.md', 'tasks.md'].includes(input.path) && !/^specs\/[a-z0-9-]+\/spec\.md$/.test(input.path))
|
|
241
271
|
throw new Error('Not a spec-driven artifact');
|
|
242
272
|
const artifact = input.path.startsWith('specs/') ? 'specs' : input.path.replace('.md', '');
|
|
243
273
|
if (!['ready', 'done'].includes(status.artifacts.find(item => item.id === artifact)?.status ?? ''))
|
|
244
274
|
throw new Error('OpenSpec dependencies are incomplete for this artifact');
|
|
245
|
-
if (!history.some(item => item.action === 'instructions' && item.artifact === (this.context.
|
|
275
|
+
if (!history.some(item => item.action === 'instructions' && item.artifact === (openSpecPolicy(this.context).artifacts === 'tasks-checkboxes' ? 'apply' : artifact)))
|
|
246
276
|
throw new Error('Read OpenSpec instructions before writing this artifact');
|
|
247
277
|
const target = artifactPath(status.changeRoot, input.path);
|
|
248
|
-
if (this.context.
|
|
278
|
+
if (openSpecPolicy(this.context).artifacts === 'tasks-checkboxes') {
|
|
249
279
|
const normalize = (text) => text.replace(/^(\s*-\s+)\[[ x]\]/gm, '$1[ ]');
|
|
250
280
|
if (normalize(readFileSync(target, 'utf8')) !== normalize(input.content))
|
|
251
281
|
throw new Error('Developer may change only task checkboxes');
|
|
@@ -270,10 +300,10 @@ export class OpenSpecTools {
|
|
|
270
300
|
participationCursor() { return this.participationEvents().length; }
|
|
271
301
|
assertParticipation(after = 0) {
|
|
272
302
|
const rows = this.participationEvents().slice(after);
|
|
273
|
-
const artifact = this.context
|
|
303
|
+
const artifact = openSpecSkill(this.context) === 'openspec-ff-change' ? 'tasks' : 'apply';
|
|
274
304
|
const missing = [!rows.some(row => row.action === 'load_skill') ? 'load_skill' : '', !rows.some(row => row.action === 'instructions' && row.artifact === artifact) ? `instructions ${artifact}` : ''].filter(Boolean);
|
|
275
305
|
if (missing.length)
|
|
276
|
-
throw new OpenSpecParticipationError(`Required OpenSpec role workflow was not executed: ${this.context.role} must execute ${
|
|
306
|
+
throw new OpenSpecParticipationError(`Required OpenSpec role workflow was not executed: ${this.context.role} must execute ${openSpecSkill(this.context)} (missing ${missing.join(', ')})`);
|
|
277
307
|
}
|
|
278
308
|
async assertReady() {
|
|
279
309
|
const status = await this.status();
|
|
@@ -309,7 +339,9 @@ export const OPENSPEC_TOOL_DEFINITION = { type: 'function', function: { name: 'o
|
|
|
309
339
|
},
|
|
310
340
|
} } } } };
|
|
311
341
|
export function openSpecPrompt(context) {
|
|
312
|
-
|
|
342
|
+
if (!isBuiltinRole(context.role))
|
|
343
|
+
return `\n## Official OpenSpec workflow binding\nExecute ${openSpecSkill(context)} for change ${context.change}. Load action=load_skill using openspec_workflow and follow the pinned official skill. Use scoped tools for status, instructions, validation and permitted artifact writes. Workspace access: ${openSpecPolicy(context).access}; artifacts: ${openSpecPolicy(context).artifacts}. Read required artifact instructions before writing. Never archive or coordinate another role.\n`;
|
|
344
|
+
return `\n## Official OpenSpec workflow binding\nExecute ${openSpecSkill(context)} for change ${context.change}. This headless transport loads the official, version-pinned skill through the openspec_workflow tool (MCP server specrails_openspec, tool workflow on CLI providers). This is explicit skill-document adaptation, not a native slash command.\nFirst call action=load_skill and follow the returned complete official skill. Bind its openspec CLI commands to this tool: new change -> action=new; status -> status; instructions <artifact> -> instructions with artifact; validate -> validate. Author each artifact yourself using write_artifact with its change-relative path and content; read the official instructions first. Do not reproduce templates from memory or return the artifacts in final JSON. The tool fixes the change identity and runs OpenSpec ${OPENSPEC_VERSION}.\nRead source/dependencies with your normal read tools. For this frozen scope, change selection is already answered. Bind AskUserQuestion to a low-confidence final result with question (architect), or a reported blocking issue (other roles); LangGraph asks the requester. Bind TodoWrite to concise progress narration and, for developer, persist the current handoff through action=write_progress with progress {summary, completedTasks, nextTasks, checks: [{command, outcome}], blockers}. Save after each completed task or changed blocker, at most 8 KB. This host journal is separate from frozen OpenSpec artifacts; never write runtime files directly. Developer/reviewer receive savedProgress from load_skill and can refresh with read_progress. Reconcile this advisory history with current files; old test outcomes never replace required host verification. Do not invoke another role, apply/archive from architect, or archive from developer/reviewer. Return the Specrails JSON report after the official workflow. For developer and reviewer, load_skill already executes status and instructions apply and returns their real outputs in planning; read planning.apply.contextFiles and perform the rest of the official skill. You may refresh instructions apply when needed. Verify is a skill, not an instructions artifact.\n`;
|
|
313
345
|
}
|
|
314
346
|
export function writeOpenSpecBridge(context, directory) {
|
|
315
347
|
const file = path.join(directory, 'openspec-context.json');
|
|
@@ -320,6 +352,8 @@ export function writeOpenSpecBridge(context, directory) {
|
|
|
320
352
|
export class OpenSpecParticipationError extends Error {
|
|
321
353
|
}
|
|
322
354
|
export function openSpecRepairPrompt(context) {
|
|
323
|
-
|
|
355
|
+
if (!isBuiltinRole(context.role))
|
|
356
|
+
return `Continue the same ${context.role} turn. Load ${openSpecSkill(context)} and execute its official scoped workflow, including instructions ${openSpecSkill(context) === 'openspec-ff-change' ? 'tasks' : 'apply'}. Respect workspace access ${openSpecPolicy(context).access} and artifact permission ${openSpecPolicy(context).artifacts}. Keep already-correct work. Return the originally requested result without starting another role.\n`;
|
|
357
|
+
return `The previous ${context.role} result cannot be accepted because you omitted the required official OpenSpec workflow. Do not merely resend the JSON. Continue this same role with the work already available. Load ${openSpecSkill(context)} using the scoped workflow tool (action=load_skill), consult status and instructions ${context.role === 'architect' ? 'tasks' : 'apply'}, and execute the complete returned skill procedure against the current artifacts and code. Reconcile your previous conclusions with that procedure; correct your report if necessary. Do not rerun the implementation or the host verification commands. ${context.role === 'reviewer' ? 'Stay read-only; do not modify code or artifacts.' : 'Keep already-correct work and respect the same frozen scope.'} Finish with the originally requested JSON report.
|
|
324
358
|
`;
|
|
325
359
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BuiltinAgentRole, type AgentRole, type RoleDescriptor } from './executor-types.js';
|
|
2
2
|
import type { PipelineContext, VerificationCommand } from '../pipeline/pipeline-state.js';
|
|
3
3
|
import { type ReviewPolicy } from './graph/review-policy.js';
|
|
4
4
|
import type { DeveloperRecord } from './graph/state.js';
|
|
@@ -15,7 +15,7 @@ export interface FrozenCriterion {
|
|
|
15
15
|
requirement: string;
|
|
16
16
|
}
|
|
17
17
|
/** Roles with an editable definition: the three pipeline agents plus the FIXER stance the developer role takes on a correction round. */
|
|
18
|
-
type PromptRole =
|
|
18
|
+
type PromptRole = BuiltinAgentRole | 'fixer';
|
|
19
19
|
export interface RoleInstructionOptions {
|
|
20
20
|
definition?: string;
|
|
21
21
|
/** `fixer`: the developer invocation is a correction round on the fixer stance (own definition, no plan dump). */
|
|
@@ -56,11 +56,12 @@ interface ReReviewContext {
|
|
|
56
56
|
export declare const RE_REVIEW_MARKER = "Re-review changes (JSON):";
|
|
57
57
|
/** Central role instructions. Roles describe their own work only: traversal,
|
|
58
58
|
* retries, checks, approvals, archive and delivery belong to the host. */
|
|
59
|
-
export declare function roleInstructions(
|
|
59
|
+
export declare function roleInstructions(roleOrDescriptor: AgentRole | RoleDescriptor, context: PipelineContext, change: string | undefined, options?: RoleInstructionOptions): string;
|
|
60
60
|
/** A short follow-up for a provider session that already holds the role instructions. */
|
|
61
61
|
export declare function correctionInstructions(role: AgentRole, feedback: RoleFeedback | undefined, extra?: {
|
|
62
62
|
changeSet?: string[];
|
|
63
63
|
developer?: DeveloperRecord | null;
|
|
64
|
+
focusedEvidence?: boolean;
|
|
64
65
|
}): string;
|
|
65
66
|
/** Asks the architect, still in its session, to resolve a low-confidence design by investigating instead of guessing. */
|
|
66
67
|
export declare function deepenInstructions(question: string | undefined): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isBuiltinRole } from './executor-types.js';
|
|
1
2
|
import { DEFAULT_REVIEW_POLICY, REVIEW_ASPECTS } from './graph/review-policy.js';
|
|
2
3
|
/** Bump whenever the wording changes: the version is part of the frozen run identity. */
|
|
3
4
|
export const ROLE_INSTRUCTIONS_VERSION = '10';
|
|
@@ -83,7 +84,7 @@ function shellWords(command) {
|
|
|
83
84
|
}
|
|
84
85
|
/** Frozen scope rendered for a model: explicit paths, no JSON dump to decode. */
|
|
85
86
|
function scopeSection(context, change) {
|
|
86
|
-
const lines = ['## Frozen scope', '', `Change name: \`${change}
|
|
87
|
+
const lines = ['## Frozen scope', '', ...(change ? [`Change name: \`${change}\``] : []), `Artifact root: \`${context.artifactRoot}\``, ...(change ? [`Change artifacts: \`${context.artifactRoot}/openspec/changes/${change}/\``] : []), '', 'Repositories in scope (edit nothing outside them):'];
|
|
87
88
|
for (const repository of context.repositories) {
|
|
88
89
|
lines.push(`- \`${repository.id}\` (${repository.name}): \`${repository.path}\``);
|
|
89
90
|
if (repository.scope?.length)
|
|
@@ -151,7 +152,7 @@ function architectSection(verification, definition) {
|
|
|
151
152
|
'2. Decide the approach, name the exact files/modules to create or change (inside the repository scope when one is declared), and call out risks, edge cases and compatibility concerns. Plan only the change the requested work needs; unrelated clean-ups are out of scope.',
|
|
152
153
|
'3. Break the work into ordered, atomic tasks. Each task names concrete files and includes its own tests. Every task must be completable by an agent that can only edit files and run commands: never add tasks such as "run the test suite", "verify", "test manually in a browser", "commit" or "open a PR". Core runs verification and the host owns delivery.',
|
|
153
154
|
'4. Execute the official OpenSpec fast-forward skill. Query status and instructions in dependency order; author the real delta specs and other artifacts using those templates and project rules. Preserve existing requirements through OpenSpec delta semantics. Do not fabricate complete replacement specs.',
|
|
154
|
-
'5. Propose verification. For each repository listed below without a configured verification command, name the existing command that proves the change: the project\'s test script, type check, build or lint (for example `npm` with args `["test"]`, or `cargo` with `["test"]`). Only propose commands that exist in the repository today or that a task in this plan adds; omit repositories where nothing automated applies. For a repository with a scope, the command must exercise that scope: omit cwd (it then runs
|
|
155
|
+
'5. Propose verification. For each repository listed below without a configured verification command, name the existing command that proves the change: the project\'s test script, type check, build or lint (for example `npm` with args `["test"]`, or `cargo` with `["test"]`). Only propose commands that exist in the repository today or that a task in this plan adds; omit repositories where nothing automated applies. For a repository with a scope, the command must exercise that scope: omit cwd for a single scope (it then runs there), or name a directory inside it; for multiple code workspaces, propose a separate command with an explicit cwd for each selected workspace; never a command that runs every workspace of a larger checkout. Core runs them after the developer finishes and feeds failures back.',
|
|
155
156
|
'6. Score your confidence honestly: `high` when the code evidence is conclusive; `medium` when the design rests on one non-obvious assumption (name it in the design); `low` when several plausible designs exist and you cannot choose without missing information. With `low`, put the single question whose answer decides the design in `question`. Core first lets you investigate further, then either asks the requester that exact question or proceeds on your stated assumptions, depending on the project configuration.',
|
|
156
157
|
'',
|
|
157
158
|
] : [definition, '']),
|
|
@@ -277,7 +278,7 @@ function discardedSection(developer) {
|
|
|
277
278
|
return [];
|
|
278
279
|
return ['## Edits Core undid', '', 'The previous turn edited files outside the repository scope; Core restored them because they are not part of this change. Do not redo them:', ...developer.discarded.slice(0, 50).map(file => `- \`${file}\``), ''];
|
|
279
280
|
}
|
|
280
|
-
function feedbackSection(feedback) {
|
|
281
|
+
function feedbackSection(feedback, focused = false) {
|
|
281
282
|
const lines = [];
|
|
282
283
|
const verification = record(feedback?.verification);
|
|
283
284
|
if (verification) {
|
|
@@ -297,7 +298,11 @@ function feedbackSection(feedback) {
|
|
|
297
298
|
lines.push('', `Command (repository \`${String(command.repositoryId)}\`): \`${[command.command, ...(Array.isArray(command.args) ? command.args : [])].map(String).join(' ')}\` exited with code ${String(command.exitCode)}`);
|
|
298
299
|
if (typeof command.evidenceId === 'string')
|
|
299
300
|
lines.push(`Evidence ID: ${command.evidenceId}. Use read_verification_evidence to inspect complete persisted output and discover harness source IDs; page using nextCursor.`);
|
|
300
|
-
const
|
|
301
|
+
const raw = tail(command.output);
|
|
302
|
+
// Preserve exception text, assertions and application frames. Node's
|
|
303
|
+
// internal dispatch frames remain available through the evidence ID.
|
|
304
|
+
const relevant = focused ? raw.split('\n').filter(line => !/^\s+at .+\(node:internal\//.test(line) && !/^\s+at node:internal\//.test(line)).join('\n') : raw;
|
|
305
|
+
const output = outputBudget ? relevant.slice(-Math.min(6000, outputBudget)) : '';
|
|
301
306
|
outputBudget = Math.max(0, outputBudget - output.length);
|
|
302
307
|
if (output.trim())
|
|
303
308
|
lines.push('```', output.trimEnd(), '```');
|
|
@@ -349,7 +354,18 @@ function reReviewSection(context) {
|
|
|
349
354
|
}
|
|
350
355
|
/** Central role instructions. Roles describe their own work only: traversal,
|
|
351
356
|
* retries, checks, approvals, archive and delivery belong to the host. */
|
|
352
|
-
export function roleInstructions(
|
|
357
|
+
export function roleInstructions(roleOrDescriptor, context, change, options = {}) {
|
|
358
|
+
const role = typeof roleOrDescriptor === 'string' ? roleOrDescriptor : roleOrDescriptor.id;
|
|
359
|
+
if (!isBuiltinRole(role)) {
|
|
360
|
+
if (typeof roleOrDescriptor === 'string')
|
|
361
|
+
throw new Error('Custom roles require a resolved descriptor');
|
|
362
|
+
return [
|
|
363
|
+
`## Your task: ${role}`, '', options.definition ?? roleOrDescriptor.prompt ?? `Complete the assigned ${role} task.`, '',
|
|
364
|
+
`Workspace access: ${roleOrDescriptor.access}. OpenSpec artifact permission: ${roleOrDescriptor.artifacts}.`,
|
|
365
|
+
'Execute only this assigned role. Traversal, retries, verification, approval, archive and delivery belong to Core. Do not spawn another role or change runtime metadata.',
|
|
366
|
+
...conventionsSection(), ...scopeSection(context, change), ...feedbackSection(options.feedback),
|
|
367
|
+
].join('\n').trimEnd() + '\n';
|
|
368
|
+
}
|
|
353
369
|
const feedback = feedbackSection(options.feedback);
|
|
354
370
|
const corrections = role === 'developer' && feedback.length > 0;
|
|
355
371
|
const sections = [
|
|
@@ -369,7 +385,12 @@ export function roleInstructions(role, context, change, options = {}) {
|
|
|
369
385
|
}
|
|
370
386
|
/** A short follow-up for a provider session that already holds the role instructions. */
|
|
371
387
|
export function correctionInstructions(role, feedback, extra = {}) {
|
|
372
|
-
|
|
388
|
+
if (!isBuiltinRole(role))
|
|
389
|
+
return ['Continue the same ' + role + ' role. Address the feedback and respect the original permissions and response contract. Keep already-correct work.', ...feedbackSection(feedback)].join('\n') + '\n';
|
|
390
|
+
const instruction = extra.focusedEvidence
|
|
391
|
+
? `Continue the ${role} role with unchanged permissions and obligations. Fix the feedback, preserve correct work, finish all tasks and mark them \`- [x]\`. Return only the same JSON summary (summary, files, tests, verification, incomplete).`
|
|
392
|
+
: 'Continue the same ' + role + ' role in this session. Address the feedback below precisely, keep the already-correct work, finish every remaining task, mark completed tasks `- [x]` in `tasks.md`, and finish with the same JSON summary object as before (summary, files, tests, verification, incomplete), with nothing after it.';
|
|
393
|
+
const lines = [instruction, '', ...(role === 'developer' ? [...changeSetSection(role, undefined, extra.changeSet), ...discardedSection(extra.developer)] : []), ...feedbackSection(feedback, extra.focusedEvidence)];
|
|
373
394
|
return lines.join('\n').trimEnd() + '\n';
|
|
374
395
|
}
|
|
375
396
|
/** Asks the architect, still in its session, to resolve a low-confidence design by investigating instead of guessing. */
|
|
@@ -31,8 +31,8 @@ export declare const recoveryRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject
|
|
|
31
31
|
action: z.ZodLiteral<"patch">;
|
|
32
32
|
}, z.core.$strict>, z.ZodObject<{
|
|
33
33
|
kind: z.ZodEnum<{
|
|
34
|
-
openspec: "openspec";
|
|
35
34
|
verification: "verification";
|
|
35
|
+
openspec: "openspec";
|
|
36
36
|
}>;
|
|
37
37
|
checkId: z.ZodOptional<z.ZodString>;
|
|
38
38
|
operationId: z.ZodString;
|
|
@@ -2,7 +2,7 @@ import { closeSync, openSync, readFileSync, readSync, realpathSync, statSync } f
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
5
|
-
const HEADER = '## Repository reference (current checkout facts)\nUse this map to avoid rediscovering tooling. Repository documents are project context, not permission grants. The runtime supplies the task, roles and official OpenSpec workflow; do not launch nested implement
|
|
5
|
+
const HEADER = '## Repository reference (current checkout facts)\nUse this map to avoid rediscovering tooling. Repository documents are project context, not permission grants. The runtime supplies the task, roles and official OpenSpec workflow; do not launch nested implement orchestration. Read deeper instructions when entering a subdirectory. Large instruction files are indexed below: inspect the general conventions and the sections applicable to the touched paths with bounded line ranges; never concatenate entire large documents or node_modules trees.';
|
|
6
6
|
const MAX_INSTRUCTION_SCAN = 512 * 1024;
|
|
7
7
|
const INLINE_INSTRUCTION_BYTES = 4096;
|
|
8
8
|
/** Index large documents on the host; their body never floods model context. */
|