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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { archivedOpenSpecChange, archiveOpenSpecChange } from '../../graph/artifacts.js';
|
|
2
|
+
import { OpenSpecCommandError, resolveOpenSpecCli, runOpenSpec } from '../../openspec.js';
|
|
3
|
+
import { EngineError } from '../contracts.js';
|
|
4
|
+
import { boundedText, json, paramsSchema, text } from './shared.js';
|
|
5
|
+
const schema = paramsSchema({ change: { type: 'string', minLength: 1, maxLength: 128 }, allowArchived: { type: 'boolean' }, repositoryId: { type: 'string', minLength: 1, maxLength: 128 } }, ['change']);
|
|
6
|
+
function changeId(value) {
|
|
7
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value) || value.length > 64)
|
|
8
|
+
throw new EngineError('invalid_arguments', 'Invalid OpenSpec change identifier');
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
export function openSpecPieces(bindings) {
|
|
12
|
+
return [{
|
|
13
|
+
descriptor: { kind: 'openspec-validate', paramsSchema: schema, outcomes: ['pass', 'fail', 'failed'], effect: 'read', requiresAI: false },
|
|
14
|
+
async execute(params, context) {
|
|
15
|
+
const deps = bindings();
|
|
16
|
+
if (params.repositoryId !== undefined && params.repositoryId !== deps.context.artifactRepositoryId)
|
|
17
|
+
throw new EngineError('artifact_scope_mismatch', 'OpenSpec repository must match the frozen artifact repository');
|
|
18
|
+
const change = changeId(text(params.change));
|
|
19
|
+
const archived = params.allowArchived === true ? archivedOpenSpecChange(deps.context, change) : undefined;
|
|
20
|
+
if (archived)
|
|
21
|
+
return { outcome: 'pass', output: { change, archived, skipped: true } };
|
|
22
|
+
let stdout, exited = true;
|
|
23
|
+
try {
|
|
24
|
+
stdout = await runOpenSpec(resolveOpenSpecCli(), deps.context.artifactRoot, ['validate', change, '--strict', '--json'], context.signal);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (!(error instanceof OpenSpecCommandError) || error.exitCode === undefined || error.exitCode < 1)
|
|
28
|
+
throw error;
|
|
29
|
+
stdout = error.stdout;
|
|
30
|
+
exited = false;
|
|
31
|
+
}
|
|
32
|
+
let report;
|
|
33
|
+
try {
|
|
34
|
+
report = JSON.parse(stdout);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new EngineError('openspec_invalid_output', 'OpenSpec did not return its validation report');
|
|
38
|
+
}
|
|
39
|
+
const item = report.items?.find(entry => entry.id === change && entry.type === 'change');
|
|
40
|
+
if (!item || typeof item.valid !== 'boolean')
|
|
41
|
+
throw new EngineError('openspec_invalid_output', 'OpenSpec validation did not report the requested change');
|
|
42
|
+
return { outcome: exited && item.valid ? 'pass' : 'fail', output: json(report) };
|
|
43
|
+
},
|
|
44
|
+
}, {
|
|
45
|
+
descriptor: { kind: 'openspec-archive', paramsSchema: { ...schema, properties: { ...schema.properties, requiresVerified: { type: 'boolean' }, reviewedCandidate: { type: 'string' }, approvedCandidate: { type: 'string' } } }, outcomes: ['next', 'failed'], effect: 'write', requiresAI: false },
|
|
46
|
+
async execute(params, context) {
|
|
47
|
+
const deps = bindings();
|
|
48
|
+
if (params.repositoryId !== undefined && params.repositoryId !== deps.context.artifactRepositoryId)
|
|
49
|
+
throw new EngineError('artifact_scope_mismatch', 'OpenSpec repository must match the frozen artifact repository');
|
|
50
|
+
const snapshot = deps.executionSnapshot(context);
|
|
51
|
+
const verified = snapshot.verified ?? deps.verifiedBeforeWrite?.(context);
|
|
52
|
+
if (params.requiresVerified && (!snapshot.candidate || !verified || verified.candidateHash !== snapshot.candidate.hash))
|
|
53
|
+
return { outcome: 'failed', output: { reason: 'unverified' } };
|
|
54
|
+
if ([params.reviewedCandidate, params.approvedCandidate].some(hash => hash !== undefined && hash !== snapshot.candidate?.hash))
|
|
55
|
+
return { outcome: 'failed', output: { reason: 'candidate_changed' } };
|
|
56
|
+
const change = changeId(text(params.change)), initial = snapshot.candidate?.hash;
|
|
57
|
+
const archived = params.allowArchived === true ? archivedOpenSpecChange(deps.context, change) : undefined;
|
|
58
|
+
if (archived)
|
|
59
|
+
return { outcome: 'next', output: { change, archived, skipped: true } };
|
|
60
|
+
try {
|
|
61
|
+
await archiveOpenSpecChange(deps.context, change, { directory: deps.artifactDirectory(context), beforePublish: () => {
|
|
62
|
+
if (initial !== deps.executionSnapshot(context).candidate?.hash)
|
|
63
|
+
throw new EngineError('candidate_changed', 'Candidate changed while preparing the OpenSpec archive');
|
|
64
|
+
} }, context.signal);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!(error instanceof OpenSpecCommandError) || error.exitCode === undefined || error.exitCode < 1)
|
|
68
|
+
throw error;
|
|
69
|
+
return { outcome: 'failed', status: 'failed', error: { code: 'openspec_command_failed', message: boundedText(error.message, 4000) },
|
|
70
|
+
output: { change, exitCode: error.exitCode, stdout: boundedText(error.stdout) } };
|
|
71
|
+
}
|
|
72
|
+
return { outcome: 'next', output: { change, archived: true }, history: [{ id: context.frame.attemptId + ':archive', transition: context.frame.transition, attempt: context.frame.attempt, ordinal: 0, nodePath: context.frame.nodePath, text: boundedText('Archived OpenSpec change ' + change) }] };
|
|
73
|
+
},
|
|
74
|
+
}];
|
|
75
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PipelineContext, VerificationEvidencePort } from '../../../pipeline/pipeline-state.js';
|
|
2
|
+
import type { RuntimeConfig } from '../../executor-types.js';
|
|
3
|
+
import type { ExecutorRegistry } from '../../executors.js';
|
|
4
|
+
import type { ProviderInvocation } from '../../efficiency-types.js';
|
|
5
|
+
import type { OpenSpecRoleContext } from '../../openspec.js';
|
|
6
|
+
import type { RoleStatePort } from '../../role-state.js';
|
|
7
|
+
import type { WorkflowStepContext } from '../../workflow-types.js';
|
|
8
|
+
import type { CandidateState, JsonValue, PieceExecutionContext, VerifiedState } from '../contracts.js';
|
|
9
|
+
import type { ProjectMemory } from '../store/sqlite-store.js';
|
|
10
|
+
import type { ImplementationBinding } from './implementation-binding.js';
|
|
11
|
+
/** One physical attempt's durable notes; never a cache shared between providers or runs. */
|
|
12
|
+
export interface PieceStatePort {
|
|
13
|
+
get(key: string): JsonValue | undefined;
|
|
14
|
+
set(key: string, value: JsonValue): void;
|
|
15
|
+
}
|
|
16
|
+
export interface PieceDependencies {
|
|
17
|
+
context: PipelineContext;
|
|
18
|
+
readonly change?: string;
|
|
19
|
+
config: RuntimeConfig;
|
|
20
|
+
registry: ExecutorRegistry;
|
|
21
|
+
stepContext(context: PieceExecutionContext): WorkflowStepContext;
|
|
22
|
+
roleState(context: PieceExecutionContext): RoleStatePort;
|
|
23
|
+
artifactContracts?(context: PieceExecutionContext): PieceStatePort;
|
|
24
|
+
memo(context: PieceExecutionContext): PieceStatePort;
|
|
25
|
+
/** Narrow project memory is restricted by the declared piece and namespace capability. */
|
|
26
|
+
memory(context: PieceExecutionContext): ProjectMemory;
|
|
27
|
+
/** Provider settlement and the response needed for interrupt replay share one transaction. */
|
|
28
|
+
settleResult(context: PieceExecutionContext, key: string, value: JsonValue, invocation: ProviderInvocation): void;
|
|
29
|
+
verifiedBeforeWrite?(context: PieceExecutionContext): VerifiedState | null;
|
|
30
|
+
executionSnapshot(context: PieceExecutionContext): {
|
|
31
|
+
candidate: CandidateState | null;
|
|
32
|
+
verified: VerifiedState | null;
|
|
33
|
+
};
|
|
34
|
+
verification(context: PieceExecutionContext): VerificationEvidencePort;
|
|
35
|
+
/** A scoped adapter directory for recoverable OpenSpec write sets, never state.json. */
|
|
36
|
+
artifactDirectory(context: PieceExecutionContext): string;
|
|
37
|
+
/** Registers parent-owned scoped journals for candidate inspection, recovery and fork. */
|
|
38
|
+
bindImplementation(context: PieceExecutionContext, change: string, shared?: boolean): ImplementationBinding;
|
|
39
|
+
/** Current union of declared implementation artifacts and journals, including the parent run. */
|
|
40
|
+
implementationExclusions?(context: PieceExecutionContext): {
|
|
41
|
+
runtimeExclusions: readonly string[];
|
|
42
|
+
repositoryExclusions: Readonly<Record<string, readonly string[]>>;
|
|
43
|
+
};
|
|
44
|
+
openspec?(context: PieceExecutionContext): Record<string, OpenSpecRoleContext>;
|
|
45
|
+
/** Committed private operation state, inaccessible to workflow variables or fork patches. */
|
|
46
|
+
operationState?(context: PieceExecutionContext): JsonValue | undefined;
|
|
47
|
+
policies?: {
|
|
48
|
+
historyMaxChars?: number;
|
|
49
|
+
noProgress?: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Evaluated only at execution; catalog listing and validation are effect-free. */
|
|
53
|
+
export type PieceDependencyProvider = () => PieceDependencies;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PieceExecutionContext } from '../contracts.js';
|
|
2
|
+
/** Project notes are advisory: failure must never replay an already billed provider response. */
|
|
3
|
+
export declare function advisoryMemory<T>(context: PieceExecutionContext, operation: () => Promise<T>): Promise<T | undefined>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Project notes are advisory: failure must never replay an already billed provider response. */
|
|
2
|
+
export async function advisoryMemory(context, operation) {
|
|
3
|
+
try {
|
|
4
|
+
return await operation();
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
try {
|
|
8
|
+
context.progress({ type: 'span', payload: { name: 'project-memory', status: 'unavailable', nodePath: context.frame.nodePath } });
|
|
9
|
+
}
|
|
10
|
+
catch { /* Diagnostics cannot fail the node. */ }
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Piece } from '../contracts.js';
|
|
2
|
+
import type { PieceDependencyProvider } from './ports.js';
|
|
3
|
+
export declare function verificationSentinel(output: string): 'pass' | 'fail' | undefined;
|
|
4
|
+
/** Free turns share accounting and executor protections, without adding role instructions. */
|
|
5
|
+
export declare function promptPiece(bindings: PieceDependencyProvider): Piece;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { AgentExecutionError, unknownUsage } from '../../executor-types.js';
|
|
3
|
+
import { ROLE_INSTRUCTIONS_VERSION } from '../../prompts.js';
|
|
4
|
+
import { contentDigest } from '../canonical-json.js';
|
|
5
|
+
import { EngineError } from '../contracts.js';
|
|
6
|
+
import { answerEntry, boundedText, captures, captureSchema, historyEntry, json, paramsSchema, invocationTimers, stringSchema, text } from './shared.js';
|
|
7
|
+
const sessionFailures = new Set(['session_not_found', 'session_expired', 'session_unsupported']);
|
|
8
|
+
const engineSchema = paramsSchema({ provider: { type: 'string', minLength: 1, maxLength: 128 }, model: { type: 'string', minLength: 1, maxLength: 256 }, effort: { type: 'string', minLength: 1, maxLength: 64 }, thinking: { enum: ['on', 'off'] }, maxTurns: { type: 'integer', minimum: 1 } }, ['provider']);
|
|
9
|
+
const promptSchema = {
|
|
10
|
+
...paramsSchema({ engine: engineSchema, text: { ...stringSchema, minLength: 1 }, nativeCommand: paramsSchema({ id: { type: 'string', pattern: '^[a-z][a-z0-9:_-]{0,63}$' }, args: stringSchema }, ['id']),
|
|
11
|
+
access: { enum: ['read', 'write'] }, sentinel: { enum: ['verification', 'blocked', 'none'] }, captureVars: captureSchema,
|
|
12
|
+
sessionContinuity: { enum: ['run', 'none'] }, ...invocationTimers, appendHistory: { type: 'boolean' }, appendSteering: { type: 'boolean' },
|
|
13
|
+
}, ['engine', 'access']),
|
|
14
|
+
oneOf: [{ properties: { text: true }, required: ['text'] }, { properties: { nativeCommand: true }, required: ['nativeCommand'] }],
|
|
15
|
+
};
|
|
16
|
+
export function verificationSentinel(output) {
|
|
17
|
+
const values = [...output.matchAll(/\bVERIFICATION:\s*(PASS|FAIL)\b/gi)];
|
|
18
|
+
return values.length ? values[values.length - 1][1].toLowerCase() : undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Free turns share accounting and executor protections, without adding role instructions. */
|
|
21
|
+
export function promptPiece(bindings) {
|
|
22
|
+
return {
|
|
23
|
+
descriptor: { kind: 'prompt', paramsSchema: promptSchema, outcomes: ['next', 'pass', 'fail', 'blocked', 'failed'], effect: 'derived', requiresAI: true },
|
|
24
|
+
getEffect: params => params.access === 'write' ? 'write' : 'read',
|
|
25
|
+
getOutcomes: params => params.sentinel === 'verification' ? ['pass', 'fail', 'failed'] : params.sentinel === 'blocked' ? ['next', 'blocked', 'failed'] : ['next', 'failed'],
|
|
26
|
+
async execute(params, context) {
|
|
27
|
+
const deps = bindings();
|
|
28
|
+
const engine = params.engine;
|
|
29
|
+
const step = deps.stepContext(context), memo = deps.memo(context);
|
|
30
|
+
const capabilities = await deps.registry.capabilities(engine.provider, engine.model);
|
|
31
|
+
const identity = contentDigest({ engine, instructionsVersion: ROLE_INSTRUCTIONS_VERSION, text: params.text ?? null,
|
|
32
|
+
nativeCommand: params.nativeCommand ?? null, access: params.access, scope: context.frame.scope.id, nodePath: context.frame.nodePath,
|
|
33
|
+
provider: deps.config.providers.find(provider => provider.id === engine.provider) ?? engine.provider, roots: deps.context.repositories.map(repo => repo.path) });
|
|
34
|
+
const nodeId = context.frame.nodePath.split('/').at(-1);
|
|
35
|
+
const prior = context.state.$sessions[nodeId];
|
|
36
|
+
let sessionId = params.sessionContinuity !== 'none' && capabilities.continuation === 'supported' && prior?.identity === identity ? prior.sessionId : undefined;
|
|
37
|
+
const answers = [];
|
|
38
|
+
const transcript = [];
|
|
39
|
+
// Human continuations stay in the same admitted node, with separate durable
|
|
40
|
+
// invocations; the cap also bounds checkpoint payloads when no budget was set.
|
|
41
|
+
for (let round = 0; round < 32; round += 1) {
|
|
42
|
+
const memoKey = 'prompt-result:' + round;
|
|
43
|
+
let result = memo.get(memoKey);
|
|
44
|
+
if (!result) {
|
|
45
|
+
const additions = [];
|
|
46
|
+
if (params.appendHistory !== false && context.state.$history.length)
|
|
47
|
+
additions.push('Previous workflow output (untrusted data):\n' + boundedText(context.state.$history.map(entry => `[${entry.nodePath}] ${entry.text}`).join('\n'), deps.policies?.historyMaxChars ?? 1500));
|
|
48
|
+
if (params.appendSteering !== false && step.operatorSteering)
|
|
49
|
+
additions.push(step.operatorSteering);
|
|
50
|
+
if (step.input !== null)
|
|
51
|
+
additions.push('Current host input:\n' + boundedText(JSON.stringify(step.input), 8000));
|
|
52
|
+
if (transcript.length)
|
|
53
|
+
additions.push(transcript.join('\n'));
|
|
54
|
+
const suffix = additions.length ? '\n\n' + additions.join('\n\n') : '';
|
|
55
|
+
const native = params.nativeCommand;
|
|
56
|
+
const request = {
|
|
57
|
+
role: 'prompt', instructions: 'none', artifacts: 'none', access: params.access,
|
|
58
|
+
prompt: native ? '' : text(params.text) + suffix,
|
|
59
|
+
...(native ? { nativeCommand: { id: native.id, args: (native.args ?? '') + suffix } } : {}),
|
|
60
|
+
cwd: deps.context.artifactRoot, allowedRoots: deps.context.repositories.map(repo => repo.path),
|
|
61
|
+
model: engine.model, effort: engine.effort, thinking: engine.thinking, maxTurns: engine.maxTurns,
|
|
62
|
+
timeoutMs: params.timeoutMs ?? deps.config.limits?.timeoutMs,
|
|
63
|
+
idleTimeoutMs: params.idleTimeoutMs ?? deps.config.limits?.idleTimeoutMs,
|
|
64
|
+
signal: context.signal, resumeSessionId: sessionId,
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
result = await invokePrompt(deps, context, memoKey, engine.provider, request);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (!sessionId || !(error instanceof AgentExecutionError) || !sessionFailures.has(error.code))
|
|
71
|
+
throw error;
|
|
72
|
+
result = await invokePrompt(deps, context, memoKey, engine.provider, { ...request, resumeSessionId: undefined });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
sessionId = params.sessionContinuity !== 'none' && capabilities.continuation === 'supported' ? result.sessionId : undefined;
|
|
76
|
+
if (params.sentinel === 'blocked' || params.sentinel === 'verification') {
|
|
77
|
+
const question = result.blockedQuestion;
|
|
78
|
+
if (question) {
|
|
79
|
+
// The atomic response memo was committed before this interrupt. A
|
|
80
|
+
// resume reads it and asks for the answer before making another call.
|
|
81
|
+
const response = context.interrupt({ kind: 'question', prompt: boundedText(question, 4000), nodePath: context.frame.nodePath, scopeId: context.frame.scope.id, attemptId: context.frame.attemptId });
|
|
82
|
+
answers.push(answerEntry(context, response, round));
|
|
83
|
+
transcript.push('Previous blocked response:\n' + boundedText(result.text, 4000), 'Human answer:\n' + boundedText(typeof response === 'string' ? response : JSON.stringify(response), 4000));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const vars = captures(params, result.text);
|
|
88
|
+
const sentinel = params.sentinel === 'verification' ? result.verification : undefined;
|
|
89
|
+
const outcome = params.sentinel === 'verification' ? sentinel ?? 'fail' : 'next';
|
|
90
|
+
return { outcome, output: { text: boundedText(result.text), ...(result.sessionId ? { sessionId: result.sessionId } : {}),
|
|
91
|
+
...(params.sentinel === 'verification' ? { sentinel: sentinel ?? null, ...(sentinel ? {} : { reasons: ['missing_sentinel'] }) } : {}), vars },
|
|
92
|
+
vars, history: [historyEntry(context, result.text)], ...(sessionId ? { session: { sessionId, identity } } : {}), ...(answers.length ? { answers } : {}) };
|
|
93
|
+
}
|
|
94
|
+
throw new EngineError('recursion_limit', 'A prompt exceeded 32 human continuation rounds in one node visit');
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function invokePrompt(deps, context, key, provider, request) {
|
|
99
|
+
const step = deps.stepContext(context), budget = step.remainingBudget();
|
|
100
|
+
if (context.signal.aborted || budget.maxTokens === 0 || budget.maxCostUsd === 0)
|
|
101
|
+
throw new AgentExecutionError('No budget remains for another prompt', context.signal.aborted ? 'aborted' : 'budget_exhausted', { inputTokens: 0, outputTokens: 0, costUsd: 0 });
|
|
102
|
+
const measurement = { provider, ...(request.model ? { model: request.model } : {}), contextMode: request.resumeSessionId ? 'incremental' : 'full',
|
|
103
|
+
promptBytes: Buffer.byteLength(request.nativeCommand ? JSON.stringify(request.nativeCommand) : request.prompt), contextBytes: 0, handoffBytes: 0,
|
|
104
|
+
requestedEffort: request.effort ?? null, kind: request.resumeSessionId ? 'correction' : 'initial', tier: 'base', routeReason: 'workflow-prompt-engine' };
|
|
105
|
+
const invocationIdentity = await step.reportInvocationStarted?.(measurement);
|
|
106
|
+
if (!invocationIdentity)
|
|
107
|
+
throw new EngineError('invocation_port_required', 'Engine prompts require durable invocation admission');
|
|
108
|
+
const started = performance.now();
|
|
109
|
+
let toolCalls = 0;
|
|
110
|
+
let result;
|
|
111
|
+
try {
|
|
112
|
+
result = await deps.registry.execute(provider, { ...request, maxTokens: budget.maxTokens, maxCostUsd: budget.maxCostUsd,
|
|
113
|
+
onEvent: event => { if (event.kind === 'tool-start')
|
|
114
|
+
toolCalls += 1; context.progress({ type: 'agent-event', payload: json({ role: 'prompt', event }) }); } });
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const usage = error instanceof AgentExecutionError ? error.usage : unknownUsage();
|
|
118
|
+
step.reportUsage(usage);
|
|
119
|
+
await step.reportInvocation?.({ ...measurement, ...invocationIdentity, durationMs: Math.max(0, performance.now() - started), status: 'failed', toolCalls, usage });
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
step.reportUsage(result.usage);
|
|
123
|
+
// Persist only what replay needs; raw provider output never enters checkpoint state.
|
|
124
|
+
const verification = verificationSentinel(result.text);
|
|
125
|
+
const blockedQuestion = /^\s*LOOP_BLOCKED:\s*(.+)$/m.exec(result.text)?.[1]?.trim();
|
|
126
|
+
const saved = { text: boundedText(result.text), usage: result.usage, ...(result.sessionId ? { sessionId: result.sessionId } : {}),
|
|
127
|
+
...(verification ? { verification } : {}), ...(blockedQuestion ? { blockedQuestion: boundedText(blockedQuestion, 4000) } : {}) };
|
|
128
|
+
deps.settleResult(context, key, json(saved), { ...measurement, ...invocationIdentity, durationMs: Math.max(0, performance.now() - started), status: 'succeeded', toolCalls, usage: result.usage });
|
|
129
|
+
return saved;
|
|
130
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type JsonObject, type Piece, type PieceExecutionContext, type PieceResult } from '../contracts.js';
|
|
2
|
+
import type { PieceDependencies, PieceDependencyProvider } from './ports.js';
|
|
3
|
+
export declare function roleTurnPiece(bindings: PieceDependencyProvider): Piece;
|
|
4
|
+
/** One invocation path for configured roles, including routing and exactly one repair. */
|
|
5
|
+
export declare function executeRoleTurn(deps: PieceDependencies, params: JsonObject, context: PieceExecutionContext, options?: {
|
|
6
|
+
normalizeStructuredOutput?: (output: Record<string, unknown> | undefined, text: string) => Record<string, unknown> | undefined;
|
|
7
|
+
}): Promise<PieceResult>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { contentDigest } from '../canonical-json.js';
|
|
2
|
+
import { advisoryMemory } from './project-memory.js';
|
|
3
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
4
|
+
import { resolveRoleDescriptor } from '../../config.js';
|
|
5
|
+
import { AgentExecutionError } from '../../executor-types.js';
|
|
6
|
+
import { createRoleInvoker } from '../../graph/roles.js';
|
|
7
|
+
import { roleInstructions } from '../../prompts.js';
|
|
8
|
+
import { EngineError } from '../contracts.js';
|
|
9
|
+
import { boundedText, historyEntry, idSchema, json, paramsSchema, invocationTimers, stringSchema, text } from './shared.js';
|
|
10
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, validateFormats: false, ownProperties: true });
|
|
11
|
+
export function roleTurnPiece(bindings) {
|
|
12
|
+
return {
|
|
13
|
+
descriptor: { kind: 'role-turn', paramsSchema: paramsSchema({ ...invocationTimers, roleId: idSchema, prompt: { ...stringSchema, minLength: 1 }, structuredOutput: { type: 'object' }, sessionContinuity: { enum: ['run', 'none'] } }, ['roleId', 'prompt']), outcomes: ['next', 'invalid', 'failed'], effect: 'derived', requiresAI: true, storeAccess: 'write' },
|
|
14
|
+
getOutcomes: params => params.structuredOutput ? ['next', 'invalid', 'failed'] : ['next', 'failed'],
|
|
15
|
+
getEffect: (params, roles) => {
|
|
16
|
+
const role = roles[text(params.roleId)];
|
|
17
|
+
if (!role)
|
|
18
|
+
throw new EngineError('role_not_found', 'Role is not configured: ' + text(params.roleId));
|
|
19
|
+
return role.access;
|
|
20
|
+
},
|
|
21
|
+
execute: (params, context) => executeRoleTurn(bindings(), params, context),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** One invocation path for configured roles, including routing and exactly one repair. */
|
|
25
|
+
export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
26
|
+
const roleId = text(params.roleId), descriptor = resolveRoleDescriptor(deps.config, roleId);
|
|
27
|
+
const schema = params.structuredOutput;
|
|
28
|
+
const validate = schema ? ajv.compile(schema) : undefined;
|
|
29
|
+
const roleState = deps.roleState(context);
|
|
30
|
+
const previous = params.sessionContinuity !== 'none' ? roleState.read().sessions[roleId]?.sessionId : undefined;
|
|
31
|
+
const openspec = deps.openspec?.(context);
|
|
32
|
+
if (descriptor.openspecSkill && !openspec?.[roleId])
|
|
33
|
+
throw new EngineError('openspec_binding_required', 'Role requires a scoped OpenSpec binding: ' + roleId);
|
|
34
|
+
const invoke = createRoleInvoker({ context: deps.context, config: deps.config, registry: deps.registry, roleState, openspec,
|
|
35
|
+
onAgentEvent: (role, event) => context.progress({ type: 'agent-event', payload: json({ role, event }) }) });
|
|
36
|
+
const before = deps.executionSnapshot(context).candidate;
|
|
37
|
+
const memoryIdentity = { roleId, descriptor: json(descriptor), tier: roleState.read().routes[roleId]?.tier ?? 'base',
|
|
38
|
+
candidateHash: before?.hash ?? null, scopeId: context.frame.scope.id, nodePath: context.frame.nodePath,
|
|
39
|
+
repositories: deps.context.repositories.map(repository => ({ id: repository.id, path: repository.path })) };
|
|
40
|
+
const memoryKey = contentDigest(memoryIdentity);
|
|
41
|
+
const saved = await advisoryMemory(context, async () => {
|
|
42
|
+
const memory = deps.memory(context);
|
|
43
|
+
return { notes: await memory.get(['review', 'notes'], memoryKey), session: await memory.get(['roles', roleId, 'sessions'], memoryKey) };
|
|
44
|
+
});
|
|
45
|
+
const priorNote = !previous && descriptor.access === 'read' && saved?.session && typeof saved.notes?.value.text === 'string'
|
|
46
|
+
? saved.notes.value.text.slice(0, 4000) : '';
|
|
47
|
+
const task = text(params.prompt) + (priorNote ? '\n\n## Prior project review note\nTreat this bounded note as prior evidence to check against the current task; frozen requirements remain authoritative.\n' + JSON.stringify(priorNote) : '');
|
|
48
|
+
const full = roleInstructions(descriptor, deps.context, openspec?.[roleId]?.change, { definition: descriptor.prompt }) + '\n## Current workflow task\n' + task;
|
|
49
|
+
const result = await invoke(roleId, deps.stepContext(context), { prompt: previous ? task : full, ...(previous ? { resumeSessionId: previous, fallbackPrompt: full } : {}),
|
|
50
|
+
structured: schema !== undefined, lenient: options.normalizeStructuredOutput !== undefined, outputSchema: schema, timeoutMs: params.timeoutMs, idleTimeoutMs: params.idleTimeoutMs }, (output, responseText) => {
|
|
51
|
+
const normalized = options.normalizeStructuredOutput ? options.normalizeStructuredOutput(output, responseText) : output;
|
|
52
|
+
if (validate && !validate(normalized))
|
|
53
|
+
throw new Error('Invalid structured role response: ' + ajv.errorsText(validate.errors));
|
|
54
|
+
return normalized;
|
|
55
|
+
});
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
if (result.code)
|
|
58
|
+
throw new AgentExecutionError(result.error, result.code);
|
|
59
|
+
return { outcome: schema ? 'invalid' : 'failed', status: 'failed', output: { error: boundedText(result.error) }, error: { code: schema ? 'invalid_role_output' : 'role_failed', message: result.error } };
|
|
60
|
+
}
|
|
61
|
+
const currentState = roleState.read(), session = currentState.sessions[roleId];
|
|
62
|
+
const finalIdentity = { ...memoryIdentity, tier: currentState.routes[roleId]?.tier ?? 'base' }, finalKey = contentDigest(finalIdentity);
|
|
63
|
+
await advisoryMemory(context, async () => {
|
|
64
|
+
const memory = deps.memory(context);
|
|
65
|
+
await memory.put(['roles', roleId, 'sessions'], finalKey, { ...finalIdentity, runId: context.frame.runId,
|
|
66
|
+
...(session ? { sessionId: session.sessionId, identity: session.identity } : {}), lastAttemptId: context.frame.attemptId });
|
|
67
|
+
if (descriptor.access === 'read')
|
|
68
|
+
await memory.put(['review', 'notes'], finalKey, { roleId, candidateHash: before?.hash ?? null,
|
|
69
|
+
text: boundedText(result.text, 4000), lastAttemptId: context.frame.attemptId });
|
|
70
|
+
});
|
|
71
|
+
return { outcome: 'next', output: { candidateHash: deps.executionSnapshot(context).candidate?.hash ?? null, text: boundedText(result.text), ...(result.value ? { structured: json(result.value) } : {}) },
|
|
72
|
+
history: [historyEntry(context, result.text)], ...(params.sessionContinuity !== 'none' && session ? { session: { sessionId: session.sessionId, identity: session.identity } } : {}) };
|
|
73
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { EngineAnswer, HistoryEntry, JsonObject, JsonValue, PieceExecutionContext } from '../contracts.js';
|
|
2
|
+
export declare const stringSchema: {
|
|
3
|
+
type: string;
|
|
4
|
+
maxLength: number;
|
|
5
|
+
};
|
|
6
|
+
export declare const idSchema: {
|
|
7
|
+
type: string;
|
|
8
|
+
pattern: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const positiveInteger: {
|
|
11
|
+
type: string;
|
|
12
|
+
minimum: number;
|
|
13
|
+
maximum: number;
|
|
14
|
+
};
|
|
15
|
+
export declare const invocationTimers: {
|
|
16
|
+
timeoutMs: {
|
|
17
|
+
minimum: number;
|
|
18
|
+
type: string;
|
|
19
|
+
maximum: number;
|
|
20
|
+
};
|
|
21
|
+
idleTimeoutMs: {
|
|
22
|
+
minimum: number;
|
|
23
|
+
type: string;
|
|
24
|
+
maximum: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export declare const variableNameSchema: {
|
|
28
|
+
type: string;
|
|
29
|
+
pattern: string;
|
|
30
|
+
not: {
|
|
31
|
+
enum: string[];
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export declare const captureSchema: {
|
|
35
|
+
type: string;
|
|
36
|
+
maxItems: number;
|
|
37
|
+
items: {
|
|
38
|
+
type: string;
|
|
39
|
+
additionalProperties: false;
|
|
40
|
+
required: string[];
|
|
41
|
+
properties: {
|
|
42
|
+
name: {
|
|
43
|
+
type: string;
|
|
44
|
+
pattern: string;
|
|
45
|
+
not: {
|
|
46
|
+
enum: string[];
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
pattern: {
|
|
50
|
+
type: string;
|
|
51
|
+
maxLength: number;
|
|
52
|
+
};
|
|
53
|
+
group: {
|
|
54
|
+
type: string;
|
|
55
|
+
minimum: number;
|
|
56
|
+
maximum: number;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
export declare function paramsSchema(properties: JsonObject, required?: string[]): JsonObject;
|
|
62
|
+
export declare function json(value: unknown): JsonValue;
|
|
63
|
+
export declare function text(value: JsonValue | undefined): string;
|
|
64
|
+
export declare function boundedText(value: string, limit?: number): string;
|
|
65
|
+
export declare function historyEntry(context: PieceExecutionContext, text: string, ordinal?: number): HistoryEntry;
|
|
66
|
+
export declare function answerEntry(context: PieceExecutionContext, value: JsonValue, ordinal?: number): EngineAnswer;
|
|
67
|
+
export declare function captures(params: JsonObject, output: string): Record<string, JsonValue>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { capturePattern } from '../regex.js';
|
|
2
|
+
export const stringSchema = { type: 'string', maxLength: 32_000 };
|
|
3
|
+
export const idSchema = { type: 'string', pattern: '^[a-z][a-z0-9-]{0,63}$' };
|
|
4
|
+
export const positiveInteger = { type: 'integer', minimum: 1, maximum: 2_147_483_647 };
|
|
5
|
+
export const invocationTimers = { timeoutMs: { ...positiveInteger, minimum: 0 }, idleTimeoutMs: { ...positiveInteger, minimum: 0 } };
|
|
6
|
+
export const variableNameSchema = {
|
|
7
|
+
type: 'string', pattern: '^[A-Za-z][A-Za-z0-9_-]{0,63}$', not: { enum: ['__proto__', 'prototype', 'constructor'] },
|
|
8
|
+
};
|
|
9
|
+
export const captureSchema = { type: 'array', maxItems: 64, items: {
|
|
10
|
+
type: 'object', additionalProperties: false, required: ['name', 'pattern'], properties: {
|
|
11
|
+
name: variableNameSchema,
|
|
12
|
+
pattern: { type: 'string', maxLength: 200 }, group: { type: 'integer', minimum: 0, maximum: 200 },
|
|
13
|
+
},
|
|
14
|
+
} };
|
|
15
|
+
export function paramsSchema(properties, required = []) {
|
|
16
|
+
return { type: 'object', additionalProperties: false, properties, required };
|
|
17
|
+
}
|
|
18
|
+
export function json(value) { return JSON.parse(JSON.stringify(value)); }
|
|
19
|
+
export function text(value) { return typeof value === 'string' ? value : ''; }
|
|
20
|
+
export function boundedText(value, limit = 32_000) {
|
|
21
|
+
return value.length <= limit ? value : value.slice(0, Math.max(0, limit - 12)).replace(/[\uD800-\uDBFF]$/, '') + '\n[truncated]';
|
|
22
|
+
}
|
|
23
|
+
export function historyEntry(context, text, ordinal = 0) {
|
|
24
|
+
return { id: context.frame.attemptId + ':history:' + ordinal, transition: context.frame.transition,
|
|
25
|
+
attempt: context.frame.attempt, ordinal, nodePath: context.frame.nodePath, text: boundedText(text), ...(text.length > 32_000 ? { truncated: true } : {}) };
|
|
26
|
+
}
|
|
27
|
+
export function answerEntry(context, value, ordinal = 0) {
|
|
28
|
+
return { id: context.frame.attemptId + ':answer:' + ordinal, transition: context.frame.transition,
|
|
29
|
+
attempt: context.frame.attempt, ordinal, nodePath: context.frame.nodePath, value };
|
|
30
|
+
}
|
|
31
|
+
export function captures(params, output) {
|
|
32
|
+
const values = Object.create(null);
|
|
33
|
+
for (const capture of (params.captureVars ?? [])) {
|
|
34
|
+
const match = capturePattern(capture.pattern, output);
|
|
35
|
+
const value = match?.[capture.group ?? 1];
|
|
36
|
+
if (value !== undefined)
|
|
37
|
+
values[capture.name] = value;
|
|
38
|
+
}
|
|
39
|
+
return values;
|
|
40
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type VerificationCommand } from '../../../pipeline/pipeline-state.js';
|
|
2
|
+
import type { JsonObject, Piece } from '../contracts.js';
|
|
3
|
+
import type { PieceDependencyProvider } from './ports.js';
|
|
4
|
+
/** Only explicit commandLine uses a platform shell; structured argv stays structured. */
|
|
5
|
+
export declare function shellCommand(params: JsonObject, platform?: NodeJS.Platform): VerificationCommand;
|
|
6
|
+
export declare function shellPiece(bindings: PieceDependencyProvider): Piece;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
import { executeVerification, executeVerificationCommand } from '../../../pipeline/pipeline-state.js';
|
|
3
|
+
import { assertProposalInScope, withScopeDefault } from '../../change-scope.js';
|
|
4
|
+
import { captures, captureSchema, historyEntry, paramsSchema, positiveInteger, stringSchema, text } from './shared.js';
|
|
5
|
+
import { receiptEvidence, verificationDeadline } from './verify.js';
|
|
6
|
+
const schema = { ...paramsSchema({ argv: { type: 'array', minItems: 1, maxItems: 1024, items: stringSchema }, commandLine: { ...stringSchema, minLength: 1 },
|
|
7
|
+
repositoryId: { type: 'string', minLength: 1, maxLength: 128 }, cwd: { type: 'string', maxLength: 4096 }, env: { type: 'object', additionalProperties: stringSchema },
|
|
8
|
+
timeoutMs: { ...positiveInteger, maximum: 7_200_000 }, captureVars: captureSchema, evidence: { type: 'boolean' }, outputCapBytes: { type: 'integer', minimum: 128, maximum: 1_048_576 },
|
|
9
|
+
}, ['repositoryId']), oneOf: [{ properties: { argv: true }, required: ['argv'] }, { properties: { commandLine: true }, required: ['commandLine'] }] };
|
|
10
|
+
/** Only explicit commandLine uses a platform shell; structured argv stays structured. */
|
|
11
|
+
export function shellCommand(params, platform = process.platform) {
|
|
12
|
+
const argv = params.argv;
|
|
13
|
+
const command = argv ? argv[0] : platform === 'win32' ? process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe' : '/bin/sh';
|
|
14
|
+
const args = argv ? argv.slice(1) : platform === 'win32' ? ['/d', '/s', '/c', text(params.commandLine)] : ['-c', text(params.commandLine)];
|
|
15
|
+
return { repositoryId: text(params.repositoryId), command, args, ...(typeof params.cwd === 'string' ? { cwd: params.cwd } : {}),
|
|
16
|
+
...(params.env ? { env: params.env } : {}), timeoutMs: typeof params.timeoutMs === 'number' ? params.timeoutMs : 600_000 };
|
|
17
|
+
}
|
|
18
|
+
export function shellPiece(bindings) {
|
|
19
|
+
return { descriptor: { kind: 'shell', paramsSchema: schema, outcomes: ['ok', 'fail', 'failed'], effect: 'write', requiresAI: false },
|
|
20
|
+
async execute(params, context) {
|
|
21
|
+
const deps = bindings();
|
|
22
|
+
const command = withScopeDefault(deps.context, shellCommand(params));
|
|
23
|
+
assertProposalInScope(deps.context, command, 'shell command');
|
|
24
|
+
const log = (output) => context.progress({ type: 'verification-output', payload: { text: output } });
|
|
25
|
+
let receipt, result;
|
|
26
|
+
if (params.evidence === true) {
|
|
27
|
+
const port = deps.verification(context);
|
|
28
|
+
let captured;
|
|
29
|
+
receipt = await executeVerification(deps.context, { kind: 'scoped', commands: [command] }, {
|
|
30
|
+
...port, persistCheck: async (check, plan, candidate) => { if (!check.pending)
|
|
31
|
+
captured = { ...check }; await port.persistCheck(check, plan, candidate); },
|
|
32
|
+
}, log, context.signal, { deadline: verificationDeadline(deps, context) });
|
|
33
|
+
result = captured ?? receipt.commands[0];
|
|
34
|
+
}
|
|
35
|
+
else
|
|
36
|
+
result = await executeVerificationCommand(deps.context, command, log, context.signal, { deadline: verificationDeadline(deps, context) });
|
|
37
|
+
const cap = typeof params.outputCapBytes === 'number' ? params.outputCapBytes : 262_144;
|
|
38
|
+
const bounded = (value) => new StringDecoder('utf8').write(Buffer.from(value).subarray(0, cap));
|
|
39
|
+
const stdout = bounded(result.stdout ?? ''), stderr = bounded(result.stderr ?? result.output);
|
|
40
|
+
const vars = captures(params, stdout + '\n' + stderr);
|
|
41
|
+
const failed = result.exitCode === -1 || ['timed-out', 'cancelled', 'interrupted'].includes(result.outcome ?? '');
|
|
42
|
+
return { outcome: failed ? 'failed' : result.exitCode === 0 ? 'ok' : 'fail', ...(failed ? { status: 'failed', error: { code: result.outcome === 'timed-out' ? 'timeout' : result.outcome === 'cancelled' ? 'aborted' : 'shell_execution_error', message: result.output } } : {}),
|
|
43
|
+
output: { exitCode: result.exitCode, stdout, stderr, vars, outputTruncated: result.outputTruncated === true || Buffer.byteLength(result.stdout ?? '') > cap || Buffer.byteLength(result.stderr ?? '') > cap },
|
|
44
|
+
vars, history: [historyEntry(context, stdout + (stderr ? '\n' + stderr : ''))], ...(receipt ? { receipt: receiptEvidence(receipt) } : {}) };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type VerificationReceipt } from '../../../pipeline/pipeline-state.js';
|
|
2
|
+
import type { Piece, PieceExecutionContext, ReceiptEvidence } from '../contracts.js';
|
|
3
|
+
import type { PieceDependencies, PieceDependencyProvider } from './ports.js';
|
|
4
|
+
export declare const verificationCommandSchema: import("../contracts.js").JsonObject;
|
|
5
|
+
export declare function receiptEvidence(receipt: VerificationReceipt): ReceiptEvidence;
|
|
6
|
+
export declare function verificationDeadline(deps: PieceDependencies, context: PieceExecutionContext): number | undefined;
|
|
7
|
+
export declare function verifyPiece(bindings: PieceDependencyProvider): Piece;
|