specrails-core 5.2.3 → 5.4.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 -0
- package/bin/specrails-core.mjs +5 -0
- package/dist/agent-runtime/capabilities.d.ts +27 -0
- package/dist/agent-runtime/capabilities.js +73 -0
- package/dist/agent-runtime/capabilities.js.map +1 -0
- package/dist/agent-runtime/cli-executor.d.ts +43 -0
- package/dist/agent-runtime/cli-executor.js +405 -0
- package/dist/agent-runtime/cli-executor.js.map +1 -0
- package/dist/agent-runtime/cli-process.d.ts +32 -0
- package/dist/agent-runtime/cli-process.js +159 -0
- package/dist/agent-runtime/cli-process.js.map +1 -0
- package/dist/agent-runtime/cli.d.ts +39 -0
- package/dist/agent-runtime/cli.js +216 -0
- package/dist/agent-runtime/cli.js.map +1 -0
- package/dist/agent-runtime/codex-schema.d.ts +4 -0
- package/dist/agent-runtime/codex-schema.js +31 -0
- package/dist/agent-runtime/codex-schema.js.map +1 -0
- package/dist/agent-runtime/config.d.ts +11 -0
- package/dist/agent-runtime/config.js +263 -0
- package/dist/agent-runtime/config.js.map +1 -0
- package/dist/agent-runtime/core-host.d.ts +41 -0
- package/dist/agent-runtime/core-host.js +132 -0
- package/dist/agent-runtime/core-host.js.map +1 -0
- package/dist/agent-runtime/durable-store.d.ts +22 -0
- package/dist/agent-runtime/durable-store.js +205 -0
- package/dist/agent-runtime/durable-store.js.map +1 -0
- package/dist/agent-runtime/efficiency-summary.d.ts +67 -0
- package/dist/agent-runtime/efficiency-summary.js +52 -0
- package/dist/agent-runtime/efficiency-summary.js.map +1 -0
- package/dist/agent-runtime/efficiency-types.d.ts +60 -0
- package/dist/agent-runtime/efficiency-types.js +8 -0
- package/dist/agent-runtime/efficiency-types.js.map +1 -0
- package/dist/agent-runtime/efficiency.d.ts +5 -0
- package/dist/agent-runtime/efficiency.js +35 -0
- package/dist/agent-runtime/efficiency.js.map +1 -0
- package/dist/agent-runtime/evaluation-corpus.d.ts +13 -0
- package/dist/agent-runtime/evaluation-corpus.js +8 -0
- package/dist/agent-runtime/evaluation-corpus.js.map +1 -0
- package/dist/agent-runtime/evaluation.d.ts +43 -0
- package/dist/agent-runtime/evaluation.js +171 -0
- package/dist/agent-runtime/evaluation.js.map +1 -0
- package/dist/agent-runtime/executor-types.d.ts +125 -0
- package/dist/agent-runtime/executor-types.js +29 -0
- package/dist/agent-runtime/executor-types.js.map +1 -0
- package/dist/agent-runtime/executors.d.ts +23 -0
- package/dist/agent-runtime/executors.js +48 -0
- package/dist/agent-runtime/executors.js.map +1 -0
- package/dist/agent-runtime/gemini-policy.d.ts +8 -0
- package/dist/agent-runtime/gemini-policy.js +36 -0
- package/dist/agent-runtime/gemini-policy.js.map +1 -0
- package/dist/agent-runtime/graph/artifacts.d.ts +26 -0
- package/dist/agent-runtime/graph/artifacts.js +205 -0
- package/dist/agent-runtime/graph/artifacts.js.map +1 -0
- package/dist/agent-runtime/graph/nodes.d.ts +39 -0
- package/dist/agent-runtime/graph/nodes.js +321 -0
- package/dist/agent-runtime/graph/nodes.js.map +1 -0
- package/dist/agent-runtime/graph/review-policy.d.ts +18 -0
- package/dist/agent-runtime/graph/review-policy.js +32 -0
- package/dist/agent-runtime/graph/review-policy.js.map +1 -0
- package/dist/agent-runtime/graph/roles.d.ts +47 -0
- package/dist/agent-runtime/graph/roles.js +190 -0
- package/dist/agent-runtime/graph/roles.js.map +1 -0
- package/dist/agent-runtime/graph/state.d.ts +83 -0
- package/dist/agent-runtime/graph/state.js +23 -0
- package/dist/agent-runtime/graph/state.js.map +1 -0
- package/dist/agent-runtime/graph-checkpointer.d.ts +61 -0
- package/dist/agent-runtime/graph-checkpointer.js +137 -0
- package/dist/agent-runtime/graph-checkpointer.js.map +1 -0
- package/dist/agent-runtime/index.d.ts +20 -0
- package/dist/agent-runtime/index.js +21 -0
- package/dist/agent-runtime/index.js.map +1 -0
- package/dist/agent-runtime/kimi-acp.d.ts +11 -0
- package/dist/agent-runtime/kimi-acp.js +190 -0
- package/dist/agent-runtime/kimi-acp.js.map +1 -0
- package/dist/agent-runtime/openai-executor.d.ts +26 -0
- package/dist/agent-runtime/openai-executor.js +192 -0
- package/dist/agent-runtime/openai-executor.js.map +1 -0
- package/dist/agent-runtime/openspec-tool-server.d.ts +1 -0
- package/dist/agent-runtime/openspec-tool-server.js +37 -0
- package/dist/agent-runtime/openspec-tool-server.js.map +1 -0
- package/dist/agent-runtime/openspec.d.ts +123 -0
- package/dist/agent-runtime/openspec.js +263 -0
- package/dist/agent-runtime/openspec.js.map +1 -0
- package/dist/agent-runtime/prompts.d.ts +44 -0
- package/dist/agent-runtime/prompts.js +310 -0
- package/dist/agent-runtime/prompts.js.map +1 -0
- package/dist/agent-runtime/provider-diagnostic.d.ts +2 -0
- package/dist/agent-runtime/provider-diagnostic.js +25 -0
- package/dist/agent-runtime/provider-diagnostic.js.map +1 -0
- package/dist/agent-runtime/repository-context.d.ts +25 -0
- package/dist/agent-runtime/repository-context.js +100 -0
- package/dist/agent-runtime/repository-context.js.map +1 -0
- package/dist/agent-runtime/review-context.d.ts +14 -0
- package/dist/agent-runtime/review-context.js +41 -0
- package/dist/agent-runtime/review-context.js.map +1 -0
- package/dist/agent-runtime/role-routing.d.ts +10 -0
- package/dist/agent-runtime/role-routing.js +29 -0
- package/dist/agent-runtime/role-routing.js.map +1 -0
- package/dist/agent-runtime/role-state.d.ts +19 -0
- package/dist/agent-runtime/role-state.js +24 -0
- package/dist/agent-runtime/role-state.js.map +1 -0
- package/dist/agent-runtime/runtime-identity.d.ts +11 -0
- package/dist/agent-runtime/runtime-identity.js +33 -0
- package/dist/agent-runtime/runtime-identity.js.map +1 -0
- package/dist/agent-runtime/tool-event.d.ts +3 -0
- package/dist/agent-runtime/tool-event.js +24 -0
- package/dist/agent-runtime/tool-event.js.map +1 -0
- package/dist/agent-runtime/verification-plan.d.ts +55 -0
- package/dist/agent-runtime/verification-plan.js +206 -0
- package/dist/agent-runtime/verification-plan.js.map +1 -0
- package/dist/agent-runtime/workflow-types.d.ts +226 -0
- package/dist/agent-runtime/workflow-types.js +2 -0
- package/dist/agent-runtime/workflow-types.js.map +1 -0
- package/dist/agent-runtime/workflow.d.ts +17 -0
- package/dist/agent-runtime/workflow.js +636 -0
- package/dist/agent-runtime/workflow.js.map +1 -0
- package/dist/agent-runtime/workspace-tools.d.ts +29 -0
- package/dist/agent-runtime/workspace-tools.js +282 -0
- package/dist/agent-runtime/workspace-tools.js.map +1 -0
- package/dist/installer/cli.d.ts +35 -0
- package/dist/installer/cli.js +3 -0
- package/dist/installer/cli.js.map +1 -1
- package/dist/installer/commands/doctor.d.ts +27 -0
- package/dist/installer/commands/framework.d.ts +85 -0
- package/dist/installer/commands/init.d.ts +147 -0
- package/dist/installer/commands/update.d.ts +56 -0
- package/dist/installer/commands/v5-migration.d.ts +32 -0
- package/dist/installer/phases/framework-lifecycle.d.ts +53 -0
- package/dist/installer/phases/install-config.d.ts +64 -0
- package/dist/installer/phases/manifest.d.ts +45 -0
- package/dist/installer/phases/prereqs.d.ts +51 -0
- package/dist/installer/phases/provider-detect.d.ts +89 -0
- package/dist/installer/phases/scaffold.d.ts +211 -0
- package/dist/installer/phases/scaffold.js +39 -109
- package/dist/installer/phases/scaffold.js.map +1 -1
- package/dist/installer/runtime/kimi.d.ts +84 -0
- package/dist/installer/runtime/pipeline-state.d.ts +351 -0
- package/dist/installer/runtime/pipeline-state.js +514 -41
- package/dist/installer/runtime/pipeline-state.js.map +1 -1
- package/dist/installer/util/errors.d.ts +46 -0
- package/dist/installer/util/exec.d.ts +41 -0
- package/dist/installer/util/fs.d.ts +153 -0
- package/dist/installer/util/git.d.ts +44 -0
- package/dist/installer/util/install-transaction.d.ts +29 -0
- package/dist/installer/util/logger.d.ts +31 -0
- package/dist/installer/util/paths.d.ts +34 -0
- package/dist/installer/util/prompts.d.ts +23 -0
- package/dist/installer/util/registry.d.ts +174 -0
- package/dist/installer/util/template.d.ts +23 -0
- package/docs/README.md +1 -0
- package/docs/agent-runtime-efficiency.md +65 -0
- package/docs/agent-runtime.md +339 -0
- package/integration-contract.json +80 -7
- package/package.json +20 -2
- package/schemas/agent-runtime.schema.json +77 -0
- package/schemas/fixtures/runtime-efficiency-summary.v1.json +790 -0
- package/templates/codex-skills/batch-implement/SKILL.md +33 -58
- package/templates/codex-skills/implement/SKILL.md +21 -124
- package/templates/codex-skills/retry/SKILL.md +8 -34
- package/templates/commands/specrails/batch-implement.md +21 -16
- package/templates/commands/specrails/implement.md +17 -276
- package/templates/commands/specrails/retry.md +6 -34
- package/templates/gemini-commands/batch-implement.toml +34 -28
- package/templates/gemini-commands/implement.toml +34 -55
- package/templates/gemini-commands/retry.toml +10 -16
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Specrails Core installs specification-driven development workflows into a projec
|
|
|
8
8
|
|
|
9
9
|
Use Core directly from Claude Code, Codex CLI, Gemini CLI or Kimi Code. For mission conversations, a shared project board, execution loops and delivery controls, use [Specrails Desktop](https://github.com/fjpulidop/specrails-desktop).
|
|
10
10
|
|
|
11
|
+
The current source also includes an opt-in [programmatic agent runtime](docs/agent-runtime.md): a shared LangGraph workflow with per-role providers, durable recovery and local OpenAI-compatible models. Its TypeScript API and CLI keep phase control in Core while the caller owns delivery.
|
|
12
|
+
|
|
11
13
|
This README describes the current source tree. `npx specrails-core@latest` installs the published package, not unmerged changes. Check the [release notes](https://github.com/fjpulidop/specrails-core/releases), or [build this checkout](#develop-from-source) when testing unreleased work.
|
|
12
14
|
|
|
13
15
|
## Quick start
|
|
@@ -158,6 +160,7 @@ Core's configuration, specs and run state are local files. Provider CLIs still s
|
|
|
158
160
|
|
|
159
161
|
- [CLI reference](docs/user-docs/cli-reference.md)
|
|
160
162
|
- [Provider pipeline contracts](docs/user-docs/provider-pipelines.md)
|
|
163
|
+
- [Programmatic agent runtime](docs/agent-runtime.md)
|
|
161
164
|
- [Core update consistency](docs/user-docs/core-updates.md)
|
|
162
165
|
- [Local tickets](docs/local-tickets.md)
|
|
163
166
|
- [Documentation index](docs/README.md)
|
package/bin/specrails-core.mjs
CHANGED
|
@@ -62,6 +62,7 @@ const KNOWN_SUBCOMMANDS = new Set([
|
|
|
62
62
|
'update',
|
|
63
63
|
'doctor',
|
|
64
64
|
'pipeline',
|
|
65
|
+
'runtime',
|
|
65
66
|
'install-framework',
|
|
66
67
|
'swap-current',
|
|
67
68
|
'assemble',
|
|
@@ -399,6 +400,10 @@ async function runNodeCli(argv) {
|
|
|
399
400
|
process.exit(1)
|
|
400
401
|
}
|
|
401
402
|
const code = await mod.main(argv)
|
|
403
|
+
// Let pipe writes drain before exiting; runtime status can exceed a pipe buffer.
|
|
404
|
+
await Promise.all([process.stdout, process.stderr].map(stream =>
|
|
405
|
+
new Promise((resolve, reject) => stream.write('', error => error ? reject(error) : resolve()))
|
|
406
|
+
))
|
|
402
407
|
process.exit(code)
|
|
403
408
|
}
|
|
404
409
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type CliProcessRunner } from './cli-process.js';
|
|
2
|
+
import { type CliProvider, type ExecutorCapabilities, type RuntimeAgentConfig, type RuntimeConfig } from './executor-types.js';
|
|
3
|
+
import type { ExecutorRegistry } from './executors.js';
|
|
4
|
+
export declare function unknownCapabilities(transport: string): ExecutorCapabilities;
|
|
5
|
+
/** Introspection never calls an inference endpoint. A flag is not proof that
|
|
6
|
+
* a transport restores history, so built-in continuation stays conservative. */
|
|
7
|
+
export declare function cliCapabilities(provider: CliProvider, model: string | undefined, options?: {
|
|
8
|
+
runProcess?: CliProcessRunner;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
}): Promise<ExecutorCapabilities>;
|
|
11
|
+
export declare function assertEffortSupported(selection: Pick<RuntimeAgentConfig, 'effort'>, capability: ExecutorCapabilities): void;
|
|
12
|
+
export declare function configuredCapabilities(config: RuntimeConfig, registry: ExecutorRegistry): Promise<{
|
|
13
|
+
schemaVersion: 1;
|
|
14
|
+
roles: {
|
|
15
|
+
transport: string;
|
|
16
|
+
continuation: "supported" | "unsupported" | "unknown";
|
|
17
|
+
effortSupport: "supported" | "unsupported" | "unknown";
|
|
18
|
+
supportedEfforts: string[] | null;
|
|
19
|
+
observedModel: boolean;
|
|
20
|
+
observedEffort: boolean;
|
|
21
|
+
role: string;
|
|
22
|
+
tier: string;
|
|
23
|
+
provider: string;
|
|
24
|
+
model: string | null;
|
|
25
|
+
requestedEffort: string | null;
|
|
26
|
+
}[];
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir, tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { runCliProcess } from './cli-process.js';
|
|
5
|
+
import { AgentExecutionError } from './executor-types.js';
|
|
6
|
+
export function unknownCapabilities(transport) {
|
|
7
|
+
return { transport, continuation: 'unknown', effortSupport: 'unknown', supportedEfforts: null, observedModel: false, observedEffort: false };
|
|
8
|
+
}
|
|
9
|
+
/** Introspection never calls an inference endpoint. A flag is not proof that
|
|
10
|
+
* a transport restores history, so built-in continuation stays conservative. */
|
|
11
|
+
export async function cliCapabilities(provider, model, options = {}) {
|
|
12
|
+
const result = unknownCapabilities(provider === 'kimi' ? 'kimi-acp' : `${provider}-cli`);
|
|
13
|
+
if (provider === 'kimi')
|
|
14
|
+
return { ...result, continuation: 'unsupported', effortSupport: 'unsupported', supportedEfforts: [] };
|
|
15
|
+
if (provider === 'gemini')
|
|
16
|
+
return result;
|
|
17
|
+
const env = options.env ?? process.env;
|
|
18
|
+
try {
|
|
19
|
+
const runner = options.runProcess ?? runCliProcess;
|
|
20
|
+
const help = await runner({ command: provider, args: ['--help'] }, { cwd: tmpdir(), env, timeoutMs: 10_000 });
|
|
21
|
+
if (help.exitCode !== 0)
|
|
22
|
+
return result;
|
|
23
|
+
if (provider === 'claude') {
|
|
24
|
+
const section = /--effort\s+<[^>]+>([\s\S]*?)(?=\n\s+--|$)/.exec(help.stdout)?.[1];
|
|
25
|
+
const levels = section?.match(/\(([^)]+)\)/)?.[1].split(',').map(value => value.trim());
|
|
26
|
+
if (levels?.length && levels.every(value => /^[a-z][a-z0-9_-]{0,31}$/.test(value)))
|
|
27
|
+
return { ...result, effortSupport: 'supported', supportedEfforts: levels };
|
|
28
|
+
}
|
|
29
|
+
else if (model && help.stdout.includes('--config')) {
|
|
30
|
+
// Use the installed client's own bounded, fresh model catalog. Do not
|
|
31
|
+
// infer reasoning support from a model prefix or another provider.
|
|
32
|
+
const file = path.join(env.CODEX_HOME ?? path.join(env.HOME ?? homedir(), '.codex'), 'models_cache.json');
|
|
33
|
+
if (statSync(file).size > 2 * 1024 * 1024)
|
|
34
|
+
return result;
|
|
35
|
+
const cache = JSON.parse(readFileSync(file, 'utf8'));
|
|
36
|
+
const age = Date.now() - Date.parse(cache.fetched_at);
|
|
37
|
+
if (!Number.isFinite(age) || age < -60_000 || age > 24 * 60 * 60_000)
|
|
38
|
+
return result;
|
|
39
|
+
const version = await runner({ command: provider, args: ['--version'] }, { cwd: tmpdir(), env, timeoutMs: 10_000 });
|
|
40
|
+
if (version.exitCode !== 0 || version.stdout.trim() !== `codex-cli ${cache.client_version}`)
|
|
41
|
+
return result;
|
|
42
|
+
const entry = Array.isArray(cache.models) ? cache.models.find((item) => item?.slug === model) : undefined;
|
|
43
|
+
const levels = entry?.supported_reasoning_levels?.map((item) => item?.effort);
|
|
44
|
+
if (Array.isArray(levels) && levels.length && levels.every(value => typeof value === 'string' && /^[a-z][a-z0-9_-]{0,31}$/.test(value))) {
|
|
45
|
+
return { ...result, effortSupport: 'supported', supportedEfforts: [...new Set(levels)] };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch { /* Missing/stale metadata is unknown, never optimistic support. */ }
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
export function assertEffortSupported(selection, capability) {
|
|
53
|
+
if (selection.effort !== undefined && (capability.effortSupport !== 'supported' || !capability.supportedEfforts?.includes(selection.effort))) {
|
|
54
|
+
throw new AgentExecutionError(`Requested effort '${selection.effort}' is not confirmed for ${capability.transport}. Select a supported effort or provider default.`, 'provider_capability_unsupported');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function configuredCapabilities(config, registry) {
|
|
58
|
+
const probes = new Map();
|
|
59
|
+
const probe = (provider, model) => {
|
|
60
|
+
const key = JSON.stringify([provider, model ?? null]);
|
|
61
|
+
if (!probes.has(key))
|
|
62
|
+
probes.set(key, registry.capabilities(provider, model));
|
|
63
|
+
return probes.get(key);
|
|
64
|
+
};
|
|
65
|
+
const roles = [];
|
|
66
|
+
for (const [role, selected] of Object.entries(config.agents)) {
|
|
67
|
+
for (const [tier, choice] of [['base', selected], ...(selected.escalation ? [['escalation', selected.escalation]] : [])]) {
|
|
68
|
+
roles.push({ role, tier, provider: selected.provider, model: choice.model ?? null, requestedEffort: choice.effort ?? null, ...await probe(selected.provider, choice.model) });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { schemaVersion: 1, roles };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=capabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../../src/agent-runtime/capabilities.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type AgentEvent, type AgentExecutor, type AgentLimits, type AgentRequest, type AgentResult, type CliProvider } from './executor-types.js';
|
|
2
|
+
import { type CliInvocation, type CliProcessRunner } from './cli-process.js';
|
|
3
|
+
export interface CliExecutorOptions {
|
|
4
|
+
runProcess?: CliProcessRunner;
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
}
|
|
7
|
+
export interface CliInvocationOptions {
|
|
8
|
+
kimiAgentFile?: string;
|
|
9
|
+
geminiPolicyFile?: string;
|
|
10
|
+
codexSchemaFile?: string;
|
|
11
|
+
openspecBridge?: {
|
|
12
|
+
command: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
};
|
|
15
|
+
mcpConfigFile?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build the exact argv for one role. Read-only roles use each CLI's native
|
|
19
|
+
* read-only boundary. The developer role gets the same autonomy the legacy
|
|
20
|
+
* Implement step had (edits and shell inside the CLI's own sandbox), because a
|
|
21
|
+
* developer that cannot run tests iterates blind and fails verification.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildCliInvocation(provider: CliProvider, request: AgentRequest, options?: CliInvocationOptions): CliInvocation;
|
|
24
|
+
interface ParsedCliResult extends AgentResult {
|
|
25
|
+
terminal: boolean;
|
|
26
|
+
failed: boolean;
|
|
27
|
+
turns: number;
|
|
28
|
+
failureKind?: 'max_turns' | 'cost_budget' | 'structured_output_retries';
|
|
29
|
+
}
|
|
30
|
+
export declare function parseCliOutput(provider: CliProvider, stdout: string): ParsedCliResult;
|
|
31
|
+
/** Live tool activity for one streamed JSON line, or undefined when the line carries none. */
|
|
32
|
+
export declare function cliToolEvents(provider: CliProvider, event: Record<string, unknown>): AgentEvent[];
|
|
33
|
+
export declare class CliExecutor implements AgentExecutor {
|
|
34
|
+
private readonly provider;
|
|
35
|
+
private readonly options;
|
|
36
|
+
constructor(provider: CliProvider, options?: CliExecutorOptions);
|
|
37
|
+
capabilities(model?: string): Promise<import("./executor-types.js").ExecutorCapabilities>;
|
|
38
|
+
private readonly capabilityChecks;
|
|
39
|
+
validateOpenSpec(context: import('./openspec.js').OpenSpecRoleContext): Promise<void>;
|
|
40
|
+
validateLimits(limits: AgentLimits): void;
|
|
41
|
+
execute(request: AgentRequest): Promise<AgentResult>;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { codexOutputSchema, restoreOptionalFields } from './codex-schema.js';
|
|
2
|
+
import { assertEffortSupported, cliCapabilities } from './capabilities.js';
|
|
3
|
+
import { providerDiagnostic } from './provider-diagnostic.js';
|
|
4
|
+
import { toolEvent } from './tool-event.js';
|
|
5
|
+
import { openSpecPrompt, writeOpenSpecBridge } from './openspec.js';
|
|
6
|
+
import { existsSync, readFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { sumCacheUsage } from './efficiency-types.js';
|
|
10
|
+
import { normalizeKimiCliModel } from '../installer/runtime/kimi.js';
|
|
11
|
+
import { AgentExecutionError, unknownUsage, validateAgentRequest } from './executor-types.js';
|
|
12
|
+
import { runCliProcess } from './cli-process.js';
|
|
13
|
+
import { canonicalWorkspace } from './workspace-tools.js';
|
|
14
|
+
import { parseStructuredText } from './openai-executor.js';
|
|
15
|
+
import { executeKimiReadonlyAcp } from './kimi-acp.js';
|
|
16
|
+
import { assertGeminiAdminPolicyAvailable, GEMINI_READONLY_POLICY } from './gemini-policy.js';
|
|
17
|
+
/** Tools a Claude developer may not use: nested agents and platform skills would start a second, unobserved workflow. */
|
|
18
|
+
const CLAUDE_DEVELOPER_DISALLOWED = 'Agent,Task,Skill';
|
|
19
|
+
/**
|
|
20
|
+
* Build the exact argv for one role. Read-only roles use each CLI's native
|
|
21
|
+
* read-only boundary. The developer role gets the same autonomy the legacy
|
|
22
|
+
* Implement step had (edits and shell inside the CLI's own sandbox), because a
|
|
23
|
+
* developer that cannot run tests iterates blind and fails verification.
|
|
24
|
+
*/
|
|
25
|
+
export function buildCliInvocation(provider, request, options = {}) {
|
|
26
|
+
const readOnly = request.role !== 'developer';
|
|
27
|
+
const model = request.model ? ['--model', request.model] : [];
|
|
28
|
+
const extraRoots = request.allowedRoots.filter(root => root !== request.cwd);
|
|
29
|
+
const resume = request.resumeSessionId;
|
|
30
|
+
switch (provider) {
|
|
31
|
+
case 'claude': return { command: 'claude', stdin: request.prompt, args: [
|
|
32
|
+
'-p', '--output-format', 'stream-json', '--verbose', '--max-turns', String(request.maxTurns ?? 100), ...model,
|
|
33
|
+
...(request.effort === undefined ? [] : ['--effort', request.effort]),
|
|
34
|
+
// Project instructions and rules stay visible; the user's global config,
|
|
35
|
+
// memory and plugins never leak into an autonomous role.
|
|
36
|
+
'--setting-sources', 'project,local',
|
|
37
|
+
...(readOnly
|
|
38
|
+
? ['--tools', options.mcpConfigFile ? 'Read,Grep,Glob,ToolSearch' : 'Read,Grep,Glob', '--permission-mode', options.mcpConfigFile ? 'dontAsk' : 'plan', '--strict-mcp-config', ...(options.mcpConfigFile ? ['--allowedTools', 'Read,Grep,Glob,ToolSearch,mcp__specrails_openspec__workflow,mcp__specrails_openspec__read_verification_evidence'] : [])]
|
|
39
|
+
: ['--tools', 'default', '--disallowedTools', CLAUDE_DEVELOPER_DISALLOWED, '--dangerously-skip-permissions']),
|
|
40
|
+
...(options.mcpConfigFile ? ['--mcp-config', options.mcpConfigFile] : []),
|
|
41
|
+
...(request.outputSchema ? ['--json-schema', JSON.stringify(request.outputSchema)] : []),
|
|
42
|
+
...(request.maxCostUsd === undefined ? [] : ['--max-budget-usd', String(request.maxCostUsd)]),
|
|
43
|
+
...extraRoots.flatMap(root => ['--add-dir', root]),
|
|
44
|
+
...(resume ? ['--resume', resume] : []),
|
|
45
|
+
] };
|
|
46
|
+
case 'codex': {
|
|
47
|
+
const sandbox = readOnly ? 'read-only' : 'workspace-write';
|
|
48
|
+
const common = ['--json', '--skip-git-repo-check', '-c', 'approval_policy="never"', ...model,
|
|
49
|
+
...(request.effort === undefined ? [] : ['-c', 'model_reasoning_effort=' + JSON.stringify(request.effort)]),
|
|
50
|
+
...(options.openspecBridge ? ['-c', 'mcp_servers.specrails_openspec.command=' + JSON.stringify(options.openspecBridge.command), '-c', 'mcp_servers.specrails_openspec.args=' + JSON.stringify(options.openspecBridge.args), '-c', 'mcp_servers.specrails_openspec.default_tools_approval_mode="approve"', '-c', 'mcp_servers.specrails_openspec.required=true'] : []),
|
|
51
|
+
];
|
|
52
|
+
// `codex exec resume` has no --sandbox flag; the same policy travels as a config override.
|
|
53
|
+
if (resume)
|
|
54
|
+
return { command: 'codex', stdin: request.prompt, args: ['exec', 'resume', ...common, '-c', `sandbox_mode="${sandbox}"`, '-c', 'sandbox_workspace_write.writable_roots=' + JSON.stringify(readOnly ? [] : request.allowedRoots), resume, '-'] };
|
|
55
|
+
return { command: 'codex', stdin: request.prompt, args: [
|
|
56
|
+
'exec', ...common, '--sandbox', sandbox,
|
|
57
|
+
...(options.codexSchemaFile ? ['--output-schema', options.codexSchemaFile] : []),
|
|
58
|
+
...extraRoots.flatMap(root => ['--add-dir', root]), '-',
|
|
59
|
+
] };
|
|
60
|
+
}
|
|
61
|
+
case 'gemini': {
|
|
62
|
+
if (readOnly && !options.geminiPolicyFile)
|
|
63
|
+
throw new AgentExecutionError('Gemini architect/reviewer roles require --admin-policy support for an enforced read-only tool allowlist. Upgrade Gemini CLI or select another provider for this role.', 'provider_capability_unsupported');
|
|
64
|
+
return { command: 'gemini', stdin: request.prompt, args: [
|
|
65
|
+
'-p', 'Execute the task supplied on stdin.', '--output-format', 'stream-json', ...model,
|
|
66
|
+
// Headless Gemini cannot answer shell approvals; the developer runs with
|
|
67
|
+
// auto-approval exactly as the legacy Implement step did.
|
|
68
|
+
...(readOnly ? ['--approval-mode', 'plan'] : ['--yolo']),
|
|
69
|
+
...(options.geminiPolicyFile ? ['--admin-policy', options.geminiPolicyFile] : []),
|
|
70
|
+
...extraRoots.flatMap(root => ['--include-directories', root]),
|
|
71
|
+
...(resume ? ['--resume', resume] : []),
|
|
72
|
+
] };
|
|
73
|
+
}
|
|
74
|
+
case 'kimi': {
|
|
75
|
+
if (readOnly && !options.kimiAgentFile)
|
|
76
|
+
throw new AgentExecutionError('Kimi read-only print mode requires --agent-file and enforced tool allowlists. Use CliExecutor to select the ACP read-only transport on older CLIs.', 'provider_capability_unsupported');
|
|
77
|
+
return { command: 'kimi', args: [
|
|
78
|
+
...(request.model ? ['-m', normalizeKimiCliModel(request.model)] : []),
|
|
79
|
+
...(resume ? [`--session=${resume}`] : []),
|
|
80
|
+
'-p', request.prompt, '--output-format', 'stream-json',
|
|
81
|
+
...(options.kimiAgentFile ? ['--agent-file', options.kimiAgentFile] : []),
|
|
82
|
+
...extraRoots.flatMap(root => ['--add-dir', root]),
|
|
83
|
+
] };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function object(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
|
|
88
|
+
function number(value) { return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null; }
|
|
89
|
+
function textBlocks(value) {
|
|
90
|
+
return Array.isArray(value) ? value.map(raw => object(raw)).filter(block => block.type === 'text' && typeof block.text === 'string').map(block => block.text).join('') : '';
|
|
91
|
+
}
|
|
92
|
+
function claudeCacheUsage(reported) {
|
|
93
|
+
if (reported.cache_read_input_tokens === undefined && reported.cache_creation_input_tokens === undefined)
|
|
94
|
+
return {};
|
|
95
|
+
return { uncachedInputTokens: number(reported.input_tokens), cacheReadInputTokens: number(reported.cache_read_input_tokens), cacheWriteInputTokens: number(reported.cache_creation_input_tokens) };
|
|
96
|
+
}
|
|
97
|
+
function inclusiveCacheUsage(reported) {
|
|
98
|
+
const cached = number(reported.cached_input_tokens), input = number(reported.input_tokens);
|
|
99
|
+
if (reported.cached_input_tokens === undefined)
|
|
100
|
+
return {};
|
|
101
|
+
return { cacheReadInputTokens: cached !== null && input !== null && cached <= input ? cached : null, uncachedInputTokens: cached !== null && input !== null && cached <= input ? input - cached : null, cacheWriteInputTokens: null };
|
|
102
|
+
}
|
|
103
|
+
export function parseCliOutput(provider, stdout) {
|
|
104
|
+
let text = '', terminal = false, failed = false, sessionId, turns = 0;
|
|
105
|
+
let usage = unknownUsage();
|
|
106
|
+
let failureKind;
|
|
107
|
+
let structured;
|
|
108
|
+
const claudeMessages = new Map();
|
|
109
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
110
|
+
let event;
|
|
111
|
+
try {
|
|
112
|
+
event = object(JSON.parse(line));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (typeof event.session_id === 'string')
|
|
118
|
+
sessionId = event.session_id;
|
|
119
|
+
if (typeof event.thread_id === 'string')
|
|
120
|
+
sessionId = event.thread_id;
|
|
121
|
+
if (['error', 'turn.failed', 'system.error'].includes(String(event.type)))
|
|
122
|
+
failed = true;
|
|
123
|
+
if (provider === 'claude') {
|
|
124
|
+
if (event.type === 'assistant') {
|
|
125
|
+
const message = object(event.message), reported = object(message.usage);
|
|
126
|
+
const messageText = textBlocks(message.content);
|
|
127
|
+
if (messageText)
|
|
128
|
+
text = messageText;
|
|
129
|
+
const id = typeof message.id === 'string' ? message.id : `anonymous-${claudeMessages.size}`;
|
|
130
|
+
const input = number(reported.input_tokens);
|
|
131
|
+
claudeMessages.set(id, { inputTokens: input === null ? null : input + (number(reported.cache_read_input_tokens) ?? 0) + (number(reported.cache_creation_input_tokens) ?? 0), outputTokens: number(reported.output_tokens), costUsd: null, ...claudeCacheUsage(reported) });
|
|
132
|
+
turns = claudeMessages.size;
|
|
133
|
+
}
|
|
134
|
+
if (event.type === 'result' && object(event.origin).kind !== 'task-notification') {
|
|
135
|
+
terminal = true;
|
|
136
|
+
failed ||= event.is_error === true || (typeof event.subtype === 'string' && event.subtype !== 'success');
|
|
137
|
+
if (event.subtype === 'error_max_turns')
|
|
138
|
+
failureKind = 'max_turns';
|
|
139
|
+
if (event.subtype === 'error_max_budget_usd')
|
|
140
|
+
failureKind = 'cost_budget';
|
|
141
|
+
if (event.subtype === 'error_max_structured_output_retries')
|
|
142
|
+
failureKind = 'structured_output_retries';
|
|
143
|
+
if (typeof event.result === 'string')
|
|
144
|
+
text = event.result;
|
|
145
|
+
// --json-schema returns the validated object separately from the text.
|
|
146
|
+
if (event.structured_output && typeof event.structured_output === 'object' && !Array.isArray(event.structured_output))
|
|
147
|
+
structured = event.structured_output;
|
|
148
|
+
const reported = object(event.usage), input = number(reported.input_tokens);
|
|
149
|
+
usage = { inputTokens: input === null ? null : input + (number(reported.cache_read_input_tokens) ?? 0) + (number(reported.cache_creation_input_tokens) ?? 0), outputTokens: number(reported.output_tokens), costUsd: number(event.total_cost_usd), ...claudeCacheUsage(reported) };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (provider === 'codex') {
|
|
153
|
+
if (event.type === 'turn.failed' || event.type === 'error')
|
|
154
|
+
failed = true;
|
|
155
|
+
if (event.type === 'item.completed') {
|
|
156
|
+
const item = object(event.item);
|
|
157
|
+
if (item.type === 'agent_message' && typeof item.text === 'string')
|
|
158
|
+
text = item.text;
|
|
159
|
+
if (item.type === 'agent_message' || ['command_execution', 'mcp_tool_call', 'function_call', 'local_shell_call'].includes(String(item.type)))
|
|
160
|
+
turns++;
|
|
161
|
+
}
|
|
162
|
+
if (event.type === 'turn.completed') {
|
|
163
|
+
terminal = true;
|
|
164
|
+
failed = false;
|
|
165
|
+
const reported = object(event.usage);
|
|
166
|
+
usage = { inputTokens: number(reported.input_tokens), outputTokens: number(reported.output_tokens), costUsd: null, ...inclusiveCacheUsage(reported) };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (provider === 'gemini') {
|
|
170
|
+
if (event.type === 'message' && event.role === 'assistant' && typeof event.content === 'string') {
|
|
171
|
+
text += event.content;
|
|
172
|
+
if (!event.delta)
|
|
173
|
+
turns++;
|
|
174
|
+
}
|
|
175
|
+
// Assistant deltas before a tool belong to an intermediate turn. Keep
|
|
176
|
+
// streaming them to observers, but return only the final response.
|
|
177
|
+
if (event.type === 'tool_use') {
|
|
178
|
+
turns++;
|
|
179
|
+
text = '';
|
|
180
|
+
}
|
|
181
|
+
if (event.type === 'result') {
|
|
182
|
+
terminal = true;
|
|
183
|
+
failed ||= event.status !== 'success';
|
|
184
|
+
const reported = object(event.stats);
|
|
185
|
+
usage = { inputTokens: number(reported.input_tokens), outputTokens: number(reported.output_tokens), costUsd: null };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
if (event.role === 'assistant') {
|
|
190
|
+
turns++;
|
|
191
|
+
const content = typeof event.content === 'string' ? event.content : textBlocks(event.content);
|
|
192
|
+
if (content)
|
|
193
|
+
text = content;
|
|
194
|
+
}
|
|
195
|
+
if (event.type === 'session.resume_hint')
|
|
196
|
+
terminal = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (provider === 'claude' && !terminal && claudeMessages.size) {
|
|
200
|
+
const messages = [...claudeMessages.values()];
|
|
201
|
+
usage = { inputTokens: messages.some(message => message.inputTokens === null) ? null : messages.reduce((sum, message) => sum + (message.inputTokens ?? 0), 0), outputTokens: messages.some(message => message.outputTokens === null) ? null : messages.reduce((sum, message) => sum + (message.outputTokens ?? 0), 0), costUsd: null, ...sumCacheUsage(messages) };
|
|
202
|
+
}
|
|
203
|
+
if (structured && !text.trim())
|
|
204
|
+
text = JSON.stringify(structured);
|
|
205
|
+
return { text, terminal, failed, turns, usage, sessionId, structured: structured ?? parseStructuredText(text), ...(failureKind ? { failureKind } : {}) };
|
|
206
|
+
}
|
|
207
|
+
/** Live tool activity for one streamed JSON line, or undefined when the line carries none. */
|
|
208
|
+
export function cliToolEvents(provider, event) {
|
|
209
|
+
if (provider === 'claude' && event.type === 'assistant') {
|
|
210
|
+
const content = object(event.message).content;
|
|
211
|
+
return Array.isArray(content) ? content.map(object).filter(block => block.type === 'tool_use' && typeof block.name === 'string').map(block => toolEvent(String(block.name), block.input)) : [];
|
|
212
|
+
}
|
|
213
|
+
if (provider === 'codex' && event.type === 'item.started') {
|
|
214
|
+
const item = object(event.item);
|
|
215
|
+
if (['command_execution', 'local_shell_call'].includes(String(item.type)))
|
|
216
|
+
return [toolEvent('shell', { command: item.command, cwd: item.cwd })];
|
|
217
|
+
if (['mcp_tool_call', 'function_call'].includes(String(item.type)))
|
|
218
|
+
return [toolEvent(typeof item.name === 'string' ? item.name : 'tool', item.arguments)];
|
|
219
|
+
if (item.type === 'file_change')
|
|
220
|
+
return [toolEvent('edit', { paths: Array.isArray(item.changes) ? item.changes.map(change => object(change).path) : [] })];
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
if (provider === 'gemini' && event.type === 'tool_use')
|
|
224
|
+
return [toolEvent(typeof event.tool_name === 'string' ? event.tool_name : 'tool', event.parameters)];
|
|
225
|
+
if (provider === 'kimi' && event.role === 'assistant' && Array.isArray(event.tool_calls)) {
|
|
226
|
+
return event.tool_calls.map(object).map(call => { const fn = object(call.function); let input; try {
|
|
227
|
+
input = typeof fn.arguments === 'string' ? JSON.parse(fn.arguments) : fn.arguments;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
input = undefined;
|
|
231
|
+
} return toolEvent(typeof fn.name === 'string' ? fn.name : 'tool', input); });
|
|
232
|
+
}
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
export class CliExecutor {
|
|
236
|
+
provider;
|
|
237
|
+
options;
|
|
238
|
+
constructor(provider, options = {}) {
|
|
239
|
+
this.provider = provider;
|
|
240
|
+
this.options = options;
|
|
241
|
+
}
|
|
242
|
+
capabilities(model) { return cliCapabilities(this.provider, model, this.options); }
|
|
243
|
+
capabilityChecks = new Map();
|
|
244
|
+
async validateOpenSpec(context) {
|
|
245
|
+
const key = context.role === 'developer' ? 'write' : 'read';
|
|
246
|
+
let checked = this.capabilityChecks.get(key);
|
|
247
|
+
if (!checked) {
|
|
248
|
+
checked = (async () => {
|
|
249
|
+
if (this.provider === 'gemini' && key === 'read')
|
|
250
|
+
assertGeminiAdminPolicyAvailable();
|
|
251
|
+
const help = await (this.options.runProcess ?? runCliProcess)({ command: this.provider, args: this.provider === 'kimi' ? ['acp', '--help'] : ['--help'] }, { cwd: context.root, timeoutMs: 10000, env: this.options.env });
|
|
252
|
+
const flags = this.provider === 'claude' ? ['--mcp-config', '--allowedTools'] : this.provider === 'codex' ? ['--config'] : this.provider === 'gemini' && key === 'read' ? ['--admin-policy'] : [];
|
|
253
|
+
if (help.exitCode !== 0 || flags.some(flag => !help.stdout.includes(flag)))
|
|
254
|
+
throw new AgentExecutionError(`The installed ${this.provider} CLI lacks the required OpenSpec transport. Upgrade this CLI or select another provider.`, 'provider_capability_unsupported');
|
|
255
|
+
})();
|
|
256
|
+
this.capabilityChecks.set(key, checked);
|
|
257
|
+
}
|
|
258
|
+
await checked;
|
|
259
|
+
}
|
|
260
|
+
validateLimits(limits) {
|
|
261
|
+
if (limits.maxCostUsd !== undefined && this.provider !== 'claude')
|
|
262
|
+
throw new AgentExecutionError(`A strict USD cap is unsupported by the ${this.provider} CLI. Use Claude's native cap or remove the dollar cap.`, 'cost_limit_unsupported');
|
|
263
|
+
if (limits.maxTokens !== undefined && this.provider === 'kimi')
|
|
264
|
+
throw new AgentExecutionError('Kimi does not report authoritative token usage. Remove the token cap or select another provider for this role.', 'usage_unavailable');
|
|
265
|
+
}
|
|
266
|
+
async execute(request) {
|
|
267
|
+
validateAgentRequest(request);
|
|
268
|
+
if (request.effort !== undefined)
|
|
269
|
+
assertEffortSupported(request, await this.capabilities(request.model));
|
|
270
|
+
this.validateLimits(request);
|
|
271
|
+
const scope = canonicalWorkspace(request.cwd, request.allowedRoots);
|
|
272
|
+
const normalized = { ...request, prompt: (request.openspec ? openSpecPrompt(request.openspec) : '') + request.prompt, cwd: scope.cwd, allowedRoots: scope.roots };
|
|
273
|
+
const runner = this.options.runProcess ?? runCliProcess;
|
|
274
|
+
let temporary, kimiAgentFile, geminiPolicyFile, codexSchemaFile, stream = '', turns = 0;
|
|
275
|
+
const assistantIds = new Set();
|
|
276
|
+
const scratch = () => temporary ??= mkdtempSync(path.join(tmpdir(), 'specrails-' + this.provider + '-role-'));
|
|
277
|
+
try {
|
|
278
|
+
const openspecBridge = request.openspec ? writeOpenSpecBridge(request.openspec, scratch()) : undefined;
|
|
279
|
+
let mcpConfigFile;
|
|
280
|
+
let executionEnv = this.options.env;
|
|
281
|
+
if (openspecBridge) {
|
|
282
|
+
mcpConfigFile = path.join(scratch(), 'mcp.json');
|
|
283
|
+
writeFileSync(mcpConfigFile, JSON.stringify({ mcpServers: { specrails_openspec: openspecBridge } }), { mode: 0o600 });
|
|
284
|
+
if (this.provider === 'gemini') {
|
|
285
|
+
const env = this.options.env ?? process.env;
|
|
286
|
+
const systemPath = env.GEMINI_CLI_SYSTEM_SETTINGS_PATH ?? (process.platform === 'darwin' ? '/Library/Application Support/GeminiCli/settings.json' : process.platform === 'win32' ? 'C:\\ProgramData\\gemini-cli\\settings.json' : '/etc/gemini-cli/settings.json');
|
|
287
|
+
const settings = existsSync(systemPath) ? JSON.parse(readFileSync(systemPath, 'utf8')) : {};
|
|
288
|
+
if (settings.mcp?.allowed || settings.mcp?.excluded || settings.admin?.mcp?.enabled === false)
|
|
289
|
+
throw new AgentExecutionError('Gemini administrator MCP restrictions require explicit OpenSpec server admission', 'provider_capability_unsupported');
|
|
290
|
+
writeFileSync(mcpConfigFile, JSON.stringify({ ...settings, mcpServers: { ...settings.mcpServers, specrails_openspec: { ...openspecBridge, trust: true, includeTools: ['workflow', 'read_verification_evidence'] } } }), { mode: 0o600 });
|
|
291
|
+
executionEnv = { ...env, GEMINI_CLI_SYSTEM_SETTINGS_PATH: mcpConfigFile };
|
|
292
|
+
}
|
|
293
|
+
if (this.provider === 'kimi')
|
|
294
|
+
return await executeKimiReadonlyAcp(normalized, { ...this.options, openspecBridge });
|
|
295
|
+
}
|
|
296
|
+
if (this.provider === 'gemini' && request.role !== 'developer') {
|
|
297
|
+
assertGeminiAdminPolicyAvailable();
|
|
298
|
+
const help = await runner({ command: 'gemini', args: ['--help'] }, { cwd: scope.cwd, signal: request.signal, timeoutMs: 10_000, env: this.options.env });
|
|
299
|
+
if (help.exitCode !== 0 || !help.stdout.includes('--admin-policy'))
|
|
300
|
+
throw new AgentExecutionError('Gemini architect/reviewer roles require --admin-policy support for an enforced read-only tool allowlist. Upgrade Gemini CLI or select another provider for this role.', 'provider_capability_unsupported');
|
|
301
|
+
geminiPolicyFile = path.join(scratch(), 'readonly.toml');
|
|
302
|
+
writeFileSync(geminiPolicyFile, (openspecBridge ? '[[rule]]\nmcpName = "specrails_openspec"\ntoolName = "workflow"\ndecision = "allow"\npriority = 1000\n\n[[rule]]\nmcpName = "specrails_openspec"\ntoolName = "read_verification_evidence"\ndecision = "allow"\npriority = 1000\n\n' : '') + GEMINI_READONLY_POLICY, { mode: 0o600 });
|
|
303
|
+
}
|
|
304
|
+
if (this.provider === 'kimi' && request.role !== 'developer') {
|
|
305
|
+
const help = await runner({ command: 'kimi', args: ['--help'] }, { cwd: scope.cwd, signal: request.signal, timeoutMs: 10_000, env: this.options.env });
|
|
306
|
+
if (help.exitCode !== 0)
|
|
307
|
+
throw new AgentExecutionError('Cannot detect Kimi CLI capabilities', 'provider_capability_unsupported');
|
|
308
|
+
if (!help.stdout.includes('--agent-file'))
|
|
309
|
+
return await executeKimiReadonlyAcp(request, this.options);
|
|
310
|
+
kimiAgentFile = path.join(scratch(), 'readonly.md');
|
|
311
|
+
writeFileSync(kimiAgentFile, '---\nname: specrails-readonly\ndescription: Execute one read-only Specrails role\ntools:\n - Read\n - Grep\n - Glob\nsubagents: []\n---\nExecute the supplied task and return its complete result. All instructions are supplied in the task.\n', { mode: 0o600 });
|
|
312
|
+
}
|
|
313
|
+
if (this.provider === 'codex' && request.outputSchema && !request.resumeSessionId) {
|
|
314
|
+
codexSchemaFile = path.join(scratch(), 'output-schema.json');
|
|
315
|
+
writeFileSync(codexSchemaFile, JSON.stringify(codexOutputSchema(request.outputSchema)), { mode: 0o600 });
|
|
316
|
+
}
|
|
317
|
+
const result = await runner(buildCliInvocation(this.provider, normalized, { kimiAgentFile, geminiPolicyFile, codexSchemaFile, openspecBridge, mcpConfigFile }), {
|
|
318
|
+
cwd: scope.cwd, signal: request.signal, timeoutMs: request.timeoutMs ?? 15 * 60_000, env: executionEnv,
|
|
319
|
+
onLine: line => {
|
|
320
|
+
stream += line + '\n';
|
|
321
|
+
let event;
|
|
322
|
+
try {
|
|
323
|
+
event = object(JSON.parse(line));
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
let delta = '';
|
|
329
|
+
if (this.provider === 'claude' && event.type === 'assistant') {
|
|
330
|
+
const message = object(event.message);
|
|
331
|
+
const id = typeof message.id === 'string' ? message.id : `anonymous-${turns}`;
|
|
332
|
+
if (!assistantIds.has(id)) {
|
|
333
|
+
assistantIds.add(id);
|
|
334
|
+
turns++;
|
|
335
|
+
}
|
|
336
|
+
delta = textBlocks(message.content);
|
|
337
|
+
}
|
|
338
|
+
else if (this.provider === 'codex' && event.type === 'item.completed') {
|
|
339
|
+
const item = object(event.item);
|
|
340
|
+
if (item.type === 'agent_message' || ['command_execution', 'mcp_tool_call', 'function_call', 'local_shell_call'].includes(String(item.type)))
|
|
341
|
+
turns++;
|
|
342
|
+
if (item.type === 'agent_message' && typeof item.text === 'string')
|
|
343
|
+
delta = item.text;
|
|
344
|
+
}
|
|
345
|
+
else if (this.provider === 'gemini') {
|
|
346
|
+
if (event.type === 'tool_use')
|
|
347
|
+
turns++;
|
|
348
|
+
if (event.type === 'message' && event.role === 'assistant' && typeof event.content === 'string')
|
|
349
|
+
delta = event.content;
|
|
350
|
+
}
|
|
351
|
+
else if (this.provider === 'kimi' && event.role === 'assistant') {
|
|
352
|
+
turns++;
|
|
353
|
+
delta = typeof event.content === 'string' ? event.content : textBlocks(event.content);
|
|
354
|
+
}
|
|
355
|
+
if (turns > (request.maxTurns ?? 100))
|
|
356
|
+
throw new AgentExecutionError('CLI exceeded its configured turn/tool limit', 'max_turns', parseCliOutput(this.provider, stream).usage);
|
|
357
|
+
for (const tool of cliToolEvents(this.provider, event))
|
|
358
|
+
request.onEvent?.(tool);
|
|
359
|
+
if (delta)
|
|
360
|
+
request.onEvent?.({ kind: 'text', text: delta });
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
const parsed = parseCliOutput(this.provider, result.stdout);
|
|
364
|
+
request.onEvent?.({ kind: 'usage', usage: parsed.usage });
|
|
365
|
+
if (parsed.failureKind) {
|
|
366
|
+
const message = parsed.failureKind === 'max_turns'
|
|
367
|
+
? `${this.provider} reached the configured limit of ${request.maxTurns ?? 100} turns for ${request.role}. Inspect partial changes before recovery; a higher limit requires a new run configuration.`
|
|
368
|
+
: parsed.failureKind === 'cost_budget' ? `${this.provider} reached its configured cost budget.` : `${this.provider} exhausted structured output retries.`;
|
|
369
|
+
throw new AgentExecutionError(message, parsed.failureKind, parsed.usage);
|
|
370
|
+
}
|
|
371
|
+
if (result.exitCode !== 0 || parsed.failed) {
|
|
372
|
+
const diagnostic = providerDiagnostic(result.stdout, result.stderr, { ...process.env, ...this.options.env });
|
|
373
|
+
throw new AgentExecutionError(`${this.provider} execution failed${result.exitCode !== 0 ? ` (exit ${result.exitCode})` : ''}.${diagnostic ? ' ' + diagnostic : ' The provider did not report a diagnostic.'}`, 'provider_execution_error', parsed.usage);
|
|
374
|
+
}
|
|
375
|
+
if (!parsed.terminal || !parsed.text.trim())
|
|
376
|
+
throw new AgentExecutionError(`${this.provider} exited without a successful final result`, 'incomplete_response', parsed.usage);
|
|
377
|
+
if (request.maxTokens !== undefined) {
|
|
378
|
+
if (parsed.usage.inputTokens === null || parsed.usage.outputTokens === null)
|
|
379
|
+
throw new AgentExecutionError(`${this.provider} did not report usage needed for the token limit`, 'usage_unavailable', parsed.usage);
|
|
380
|
+
if (parsed.usage.inputTokens + parsed.usage.outputTokens > request.maxTokens)
|
|
381
|
+
throw new AgentExecutionError('Agent token budget exceeded', 'token_budget', parsed.usage);
|
|
382
|
+
}
|
|
383
|
+
// The final text was already streamed as it arrived; re-emitting it would
|
|
384
|
+
// print every summary twice in host logs.
|
|
385
|
+
if (this.provider === 'codex' && request.outputSchema && parsed.structured) {
|
|
386
|
+
parsed.structured = restoreOptionalFields(parsed.structured, request.outputSchema);
|
|
387
|
+
parsed.text = JSON.stringify(parsed.structured);
|
|
388
|
+
}
|
|
389
|
+
return { text: parsed.text, usage: parsed.usage, sessionId: parsed.sessionId, structured: parsed.structured };
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
if (error instanceof AgentExecutionError) {
|
|
393
|
+
const usage = parseCliOutput(this.provider, stream).usage;
|
|
394
|
+
if (error.usage.inputTokens === null && usage.inputTokens !== null)
|
|
395
|
+
throw new AgentExecutionError(error.message, error.code, usage);
|
|
396
|
+
}
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
if (temporary)
|
|
401
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
//# sourceMappingURL=cli-executor.js.map
|