specrails-core 6.5.0 → 6.6.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/dist/agent-runtime/capabilities.d.ts +2 -2
- package/dist/agent-runtime/cli.d.ts +4 -4
- package/dist/agent-runtime/compact/environment.d.ts +20 -2
- package/dist/agent-runtime/compact/environment.js +63 -4
- package/dist/agent-runtime/compact/step.d.ts +1 -1
- package/dist/agent-runtime/efficiency-summary.d.ts +1 -1
- package/dist/agent-runtime/efficiency-types.d.ts +1 -1
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +15 -15
- package/dist/agent-runtime/engine/contracts.d.ts +2 -1
- package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +8 -8
- package/dist/agent-runtime/engine/fork.d.ts +1 -1
- package/dist/agent-runtime/engine/graph-description.d.ts +16 -16
- package/dist/agent-runtime/engine/implementation-compiler.d.ts +68 -68
- package/dist/agent-runtime/engine/piece-dependencies.js +4 -4
- package/dist/agent-runtime/engine/pieces/implementation-step.d.ts +1 -1
- package/dist/agent-runtime/engine/pieces/role-turn.js +10 -1
- package/dist/agent-runtime/engine/pieces/shared.d.ts +2 -2
- package/dist/agent-runtime/engine/pieces/verify.js +43 -8
- package/dist/agent-runtime/engine/run-status.d.ts +165 -165
- package/dist/agent-runtime/engine/runs.d.ts +168 -168
- package/dist/agent-runtime/engine/state.d.ts +16 -16
- package/dist/agent-runtime/evaluation.d.ts +3 -3
- package/dist/agent-runtime/executor-types.d.ts +10 -10
- package/dist/agent-runtime/graph/convergence.d.ts +2 -2
- package/dist/agent-runtime/graph/convergence.js +11 -0
- package/dist/agent-runtime/graph/nodes.js +35 -4
- package/dist/agent-runtime/graph/review-policy.d.ts +1 -1
- package/dist/agent-runtime/graph/state.d.ts +6 -2
- package/dist/agent-runtime/guardrails.d.ts +1 -1
- package/dist/agent-runtime/guardrails.js +4 -1
- package/dist/agent-runtime/index.d.ts +3 -2
- package/dist/agent-runtime/index.js +1 -0
- package/dist/agent-runtime/openai-executor.d.ts +9 -9
- package/dist/agent-runtime/openspec.d.ts +4 -4
- package/dist/agent-runtime/prompts.d.ts +6 -0
- package/dist/agent-runtime/prompts.js +32 -8
- package/dist/agent-runtime/recovery.d.ts +12 -12
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +2 -2
- package/dist/agent-runtime/verification-repair.d.ts +31 -2
- package/dist/agent-runtime/verification-repair.js +58 -3
- package/dist/installer/commands/init.d.ts +1 -1
- package/dist/installer/phases/install-config.js +10 -3
- package/dist/installer/util/paths.d.ts +25 -1
- package/dist/pipeline/pipeline-state.d.ts +42 -11
- package/dist/pipeline/pipeline-state.js +47 -8
- package/package.json +13 -11
- package/schemas/agent-runtime.schema.json +1 -1
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// Correction rounds must converge. A fixer round that leaves the change as it
|
|
2
|
+
// was, the same failure surviving round after round, or a review correction
|
|
3
|
+
// that brings back a failure an earlier round had fixed, are loops: repeating
|
|
4
|
+
// the checks cannot change their outcome. The host stops and says why instead
|
|
5
|
+
// of spending another round (observed: verify ↔ fixer ↔ review cycling on a
|
|
6
|
+
// credential problem until the requester cancelled the run).
|
|
7
|
+
import { describeModifiedFiles } from '../../pipeline/pipeline-state.js';
|
|
1
8
|
import { fingerprint } from '../durable-store.js';
|
|
2
9
|
/** Verify visits since the latest explicit resume: a human continuing a blocked run grants a fresh budget, as for attempts. */
|
|
3
10
|
export function outcomesSinceResume(history, checkpoint) {
|
|
@@ -23,6 +30,10 @@ export function describeFailure(receipt, roots, finding) {
|
|
|
23
30
|
if (finding)
|
|
24
31
|
return { signature: fingerprint({ finding: normalized(finding, roots) }), summary: finding.slice(0, 300) };
|
|
25
32
|
const failed = receipt.commands.find(command => command.exitCode !== 0);
|
|
33
|
+
// A green run that rewrote its own candidate: the same files again is the same failure, whatever their content.
|
|
34
|
+
if (!failed && receipt.selfMutation?.files.length)
|
|
35
|
+
return { signature: fingerprint({ selfMutation: receipt.selfMutation.files.map(file => file.repositoryId + ':' + file.path).sort() }),
|
|
36
|
+
summary: `verification commands modified ${describeModifiedFiles(receipt.selfMutation)}`.slice(0, 400) };
|
|
26
37
|
if (!failed)
|
|
27
38
|
return { signature: fingerprint({ reason: receipt.reason ?? 'verification failed' }), summary: receipt.reason ?? 'verification failed' };
|
|
28
39
|
const name = failed.label ?? [failed.command, ...failed.args].join(' ');
|
|
@@ -3,12 +3,12 @@ import { verificationFailureSummary } from '../verification-diagnostics.js';
|
|
|
3
3
|
import { addDeveloperChecks, bindPlan, expandedPlanCommands, initializeVerificationPlan, readVerificationPlan, validateProposedChecks } from '../verification-plan.js';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { OpenSpecTools } from '../openspec.js';
|
|
6
|
-
import { candidateManifest, fingerprintCandidate, frozenAcceptanceCriteria, inspectPipeline, recordAcceptance, transitionPipeline, validateAcceptanceReport, verifyPipeline, } from '../../pipeline/pipeline-state.js';
|
|
6
|
+
import { candidateManifest, describeModifiedFiles, fingerprintCandidate, frozenAcceptanceCriteria, inspectPipeline, recordAcceptance, transitionPipeline, validateAcceptanceReport, verifyPipeline, } from '../../pipeline/pipeline-state.js';
|
|
7
7
|
import { ARCHITECT_OUTPUT_SCHEMA, DEVELOPER_OUTPUT_SCHEMA, REVIEW_OUTPUT_SCHEMA, correctionInstructions, deepenInstructions, roleInstructions } from '../prompts.js';
|
|
8
8
|
import { archive, child, journal, object, parseArchitecture, proposedVerification, write, writeDesignConfidence } from './artifacts.js';
|
|
9
9
|
import { evaluateReview } from './review-policy.js';
|
|
10
|
-
import {
|
|
11
|
-
import { hostPreconditionMessage, preconditionBlock, repairEnvironment } from '../verification-repair.js';
|
|
10
|
+
import { describeDrift } from '../compact/environment.js';
|
|
11
|
+
import { adoptedOutputNote, environmentFailure, hostPreconditionMessage, nondeterministicOutputBlocker, preconditionBlock, repairEnvironment, selfMutationOnly } from '../verification-repair.js';
|
|
12
12
|
import { unreachedTestFiles, unreachedTestsReason } from '../compact/test-reachability.js';
|
|
13
13
|
import { exitCodeContradiction, exitHonestyReason } from '../compact/exit-code-honesty.js';
|
|
14
14
|
import { boundedReviewManifest, reviewChanges } from '../review-context.js';
|
|
@@ -351,7 +351,11 @@ function verifyNode(deps) {
|
|
|
351
351
|
// the model's — for every developer: a fresh worktree has no
|
|
352
352
|
// node_modules whatever wrote the code. Install once where the failing
|
|
353
353
|
// command runs and re-verify; only a second failure becomes feedback.
|
|
354
|
-
|
|
354
|
+
const environment = !receipt.valid && guardrailEnabled(config.guardrails, 'environment-repair') ? environmentFailure(context, receipt) : undefined;
|
|
355
|
+
if (environment) {
|
|
356
|
+
const drift = environment.drift.flatMap(item => item.drift);
|
|
357
|
+
if (drift.length)
|
|
358
|
+
note('developer', `Installed dependencies do not satisfy their declared ranges (${describeDrift(drift)}); the host reinstalls them before judging the failure.`);
|
|
355
359
|
const repair = repairEnvironment(context, receipt, config.guardrails, text => note('developer', text));
|
|
356
360
|
if (repair.refused)
|
|
357
361
|
return hostBlocked(blockerMessage(repair.refused));
|
|
@@ -363,9 +367,36 @@ function verifyNode(deps) {
|
|
|
363
367
|
return hostBlocked(blockerMessage(precondition));
|
|
364
368
|
}
|
|
365
369
|
}
|
|
370
|
+
// Verification that rewrote candidate files itself is not a code failure
|
|
371
|
+
// for the fixer: reverting a regenerated file only makes the next run
|
|
372
|
+
// regenerate it. Same rule as the engine `verify` piece: adopt and verify
|
|
373
|
+
// once more, or stop with the typed blocker when the output keeps moving.
|
|
374
|
+
let adopted = [];
|
|
375
|
+
if (selfMutationOnly(receipt) && !step.signal?.aborted) {
|
|
376
|
+
const nondeterministic = (adoption) => {
|
|
377
|
+
const blocker = nondeterministicOutputBlocker(context, receipt, adoption);
|
|
378
|
+
note('fixer', 'Verification keeps modifying candidate files itself; stopping for the host instead of starting a correction round.');
|
|
379
|
+
return { status: 'blocked', error: `Verification output is not deterministic: ${blocker.reason}. ${blocker.requiredAction} (verification_nondeterministic_output)`, usage: NO_SPEND };
|
|
380
|
+
};
|
|
381
|
+
if (!guardrailEnabled(config.guardrails, 'verification-output-adoption'))
|
|
382
|
+
return nondeterministic(false);
|
|
383
|
+
const first = receipt.selfMutation;
|
|
384
|
+
note('developer', `Verification commands modified ${describeModifiedFiles(first)}; keeping them in the candidate and verifying once more.`);
|
|
385
|
+
receipt = await run();
|
|
386
|
+
if (receipt.valid) {
|
|
387
|
+
adopted = first.files;
|
|
388
|
+
note('developer', adoptedOutputNote(first));
|
|
389
|
+
}
|
|
390
|
+
else if (selfMutationOnly(receipt))
|
|
391
|
+
return nondeterministic(true);
|
|
392
|
+
else if ((precondition = preconditionBlock(context, receipt)))
|
|
393
|
+
return hostBlocked(blockerMessage(precondition));
|
|
394
|
+
}
|
|
395
|
+
const adoptedOutputs = [...new Map([...(state.verifyResult?.adoptedOutputs ?? []), ...adopted].map(file => [file.repositoryId + '\0' + file.path, file])).values()].slice(0, 200);
|
|
366
396
|
const evidence = {
|
|
367
397
|
valid: receipt.valid, ...(receipt.reason ? { reason: receipt.reason } : {}), receiptId: receipt.id, unverifiedRepositories: uncovered,
|
|
368
398
|
commands: receipt.commands.map(command => ({ ...(command.evidenceId ? { evidenceId: command.evidenceId } : {}), repositoryId: command.repositoryId, command: command.command, args: command.args, cwd: command.cwd, exitCode: command.exitCode, failureSummary: verificationFailureSummary(command), output: command.output.slice(-2000) })),
|
|
399
|
+
...(receipt.selfMutation ? { selfMutation: receipt.selfMutation } : {}), ...(adoptedOutputs.length ? { adoptedOutputs } : {}),
|
|
369
400
|
};
|
|
370
401
|
const outcome = (passed, finding) => ({ at: new Date().toISOString(), candidateHash: receipt.candidateHash, planHash: effective.planHash, passed, ...(passed ? {} : describeFailure(receipt, roots, finding)) });
|
|
371
402
|
/** Hands a failure to the fixer unless the correction loop has stopped converging. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RuntimeConfig } from '../executor-types.js';
|
|
2
2
|
import type { ReviewRecord } from './state.js';
|
|
3
|
-
export declare const REVIEW_ASPECTS: readonly [
|
|
3
|
+
export declare const REVIEW_ASPECTS: readonly ['type_correctness', 'pattern_adherence', 'test_coverage', 'security', 'architectural_alignment'];
|
|
4
4
|
export type ReviewAspect = typeof REVIEW_ASPECTS[number];
|
|
5
5
|
export interface ReviewPolicy {
|
|
6
6
|
/** Minimum overall score for approval, 0–100. */
|
|
@@ -48,6 +48,10 @@ export interface VerificationRecord {
|
|
|
48
48
|
failureSummary?: string[];
|
|
49
49
|
output: string;
|
|
50
50
|
}>;
|
|
51
|
+
/** Candidate files the verification commands modified in this run (receipt `selfMutation`). */
|
|
52
|
+
selfMutation?: import('../../pipeline/pipeline-state.js').VerificationSelfMutation;
|
|
53
|
+
/** Files verification regenerated that the host kept in the candidate; roles must not revert them. Sticky for the run. */
|
|
54
|
+
adoptedOutputs?: import('../../pipeline/pipeline-state.js').CandidateFileChange[];
|
|
51
55
|
}
|
|
52
56
|
export interface ReviewRecord {
|
|
53
57
|
sessionId?: string;
|
|
@@ -74,8 +78,8 @@ export declare const CoreState: import("@langchain/langgraph").AnnotationRoot<{
|
|
|
74
78
|
unverifiedRepositories: import("@langchain/langgraph").BaseChannel<string[], string[] | import("@langchain/langgraph").OverwriteValue<string[]>, unknown>;
|
|
75
79
|
architecture: import("@langchain/langgraph").BaseChannel<ArchitectureRecord | null, ArchitectureRecord | import("@langchain/langgraph").OverwriteValue<ArchitectureRecord | null> | null, unknown>;
|
|
76
80
|
development: import("@langchain/langgraph").BaseChannel<DeveloperRecord | null, DeveloperRecord | import("@langchain/langgraph").OverwriteValue<DeveloperRecord | null> | null, unknown>;
|
|
77
|
-
verifyResult: import("@langchain/langgraph").BaseChannel<VerificationRecord | null,
|
|
78
|
-
review: import("@langchain/langgraph").BaseChannel<ReviewRecord | null,
|
|
81
|
+
verifyResult: import("@langchain/langgraph").BaseChannel<VerificationRecord | null, import("@langchain/langgraph").OverwriteValue<VerificationRecord | null> | VerificationRecord | null, unknown>;
|
|
82
|
+
review: import("@langchain/langgraph").BaseChannel<ReviewRecord | null, import("@langchain/langgraph").OverwriteValue<ReviewRecord | null> | ReviewRecord | null, unknown>;
|
|
79
83
|
/** Answers the requester gave to architect questions, oldest first. */
|
|
80
84
|
answers: import("@langchain/langgraph").BaseChannel<string[], string[] | import("@langchain/langgraph").OverwriteValue<string[]>, unknown>;
|
|
81
85
|
/** Autonomous investigation passes the architect already spent on a low-confidence design. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const GUARDRAIL_IDS: readonly [
|
|
1
|
+
export declare const GUARDRAIL_IDS: readonly ['plan-validation', 'genuine-blocking-question', 'plan-language-hint', 'frozen-plan-writes', 'primary-language', 'duplicate-sibling', 'one-test-per-module', 'empty-write', 'evidence-gated-ticking', 'inventory-retry', 'synthetic-corrections', 'silent-group-closure', 'verify-per-group', 'test-reachability', 'exit-code-honesty', 'environment-repair', 'lockfile-repair', 'verify-idle-timeout', 'verification-output-adoption'];
|
|
2
2
|
export type GuardrailId = (typeof GUARDRAIL_IDS)[number];
|
|
3
3
|
type GuardrailPhase = 'architect' | 'developer' | 'host';
|
|
4
4
|
interface GuardrailDescriptor {
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// artifacts, one test file per module, evidence before a task is ticked, an
|
|
6
6
|
// environment repaired by the host instead of the model, a hanging test
|
|
7
7
|
// stopped by silence, a test file the verify command never runs, a runner
|
|
8
|
-
// that prints failures but exits 0
|
|
8
|
+
// that prints failures but exits 0, a file the verify commands regenerate
|
|
9
|
+
// themselves adopted into the candidate instead of reverted. All are ON by default; a project may switch one off in
|
|
9
10
|
// its runtime configuration (`guardrails: { <id>: false }`) to tune the
|
|
10
11
|
// process. Pure bug fixes (candidate fingerprint, correction counter, argument
|
|
11
12
|
// repair) are deliberately NOT here: they are correctness, not tuning.
|
|
@@ -30,6 +31,7 @@ export const GUARDRAIL_IDS = [
|
|
|
30
31
|
'environment-repair',
|
|
31
32
|
'lockfile-repair',
|
|
32
33
|
'verify-idle-timeout',
|
|
34
|
+
'verification-output-adoption',
|
|
33
35
|
];
|
|
34
36
|
/** Stable catalog (phase order = pipeline order); labels/descriptions live in the host UI so they can be localized. */
|
|
35
37
|
export const GUARDRAIL_CATALOG = [
|
|
@@ -51,6 +53,7 @@ export const GUARDRAIL_CATALOG = [
|
|
|
51
53
|
{ id: 'environment-repair', phase: 'host' },
|
|
52
54
|
{ id: 'lockfile-repair', phase: 'host' },
|
|
53
55
|
{ id: 'verify-idle-timeout', phase: 'host' },
|
|
56
|
+
{ id: 'verification-output-adoption', phase: 'host' },
|
|
54
57
|
];
|
|
55
58
|
function isGuardrailId(value) {
|
|
56
59
|
return typeof value === 'string' && GUARDRAIL_IDS.includes(value);
|
|
@@ -14,8 +14,9 @@ export { DEFAULT_REVIEW_POLICY, REVIEW_ASPECTS, evaluateReview, resolveReviewPol
|
|
|
14
14
|
export { createRoleInvoker, type Accept, type InvokeOptions, type InvokeOutcome, type RoleInvoker, type RoleInvokerDeps } from './graph/roles.js';
|
|
15
15
|
export { parseAgentObject } from './graph/artifacts.js';
|
|
16
16
|
export { FileCheckpointSaver, type GraphStoreIO, type SerializedGraphStore } from './graph-checkpointer.js';
|
|
17
|
-
export { ARCHITECT_OUTPUT_SCHEMA, DEVELOPER_OUTPUT_SCHEMA, REVIEW_OUTPUT_SCHEMA, ROLE_INSTRUCTIONS_VERSION, rolePromptDefaults, correctionInstructions, deepenInstructions, repairInstructions, roleInstructions, type FrozenCriterion, type RoleFeedback, type RoleInstructionOptions, } from './prompts.js';
|
|
17
|
+
export { ARCHITECT_OUTPUT_SCHEMA, DEVELOPER_OUTPUT_SCHEMA, REVIEW_OUTPUT_SCHEMA, ROLE_INSTRUCTIONS_VERSION, rolePromptDefaults, correctionInstructions, deepenInstructions, repairInstructions, roleInstructions, type AdoptedOutput, type FrozenCriterion, type RoleFeedback, type RoleInstructionOptions, } from './prompts.js';
|
|
18
18
|
export { OPENSPEC_VERSION, ROLE_SKILLS, type OpenSpecRoleContext } from './openspec.js';
|
|
19
19
|
export * from './efficiency-summary.js';
|
|
20
|
-
export { readVerificationEvidence, type VerificationEvidenceQuery } from '../pipeline/pipeline-state.js';
|
|
20
|
+
export { readVerificationEvidence, type CandidateFileChange, type VerificationEvidenceQuery, type VerificationSelfMutation } from '../pipeline/pipeline-state.js';
|
|
21
|
+
export { NONDETERMINISTIC_OUTPUT_CODE } from './verification-repair.js';
|
|
21
22
|
export { runRecovery, recoveryRequestSchema, type RecoveryRequest } from './recovery.js';
|
|
@@ -18,4 +18,5 @@ export { ARCHITECT_OUTPUT_SCHEMA, DEVELOPER_OUTPUT_SCHEMA, REVIEW_OUTPUT_SCHEMA,
|
|
|
18
18
|
export { OPENSPEC_VERSION, ROLE_SKILLS } from './openspec.js';
|
|
19
19
|
export * from './efficiency-summary.js';
|
|
20
20
|
export { readVerificationEvidence } from '../pipeline/pipeline-state.js';
|
|
21
|
+
export { NONDETERMINISTIC_OUTPUT_CODE } from './verification-repair.js';
|
|
21
22
|
export { runRecovery, recoveryRequestSchema } from './recovery.js';
|
|
@@ -15,25 +15,25 @@ export declare class OpenAICompatibleExecutor implements AgentExecutor {
|
|
|
15
15
|
private readonly options;
|
|
16
16
|
constructor(provider: ApiProvider, options?: OpenAICompatibleOptions);
|
|
17
17
|
capabilities(): {
|
|
18
|
-
agentLoop: "compact" | "free";
|
|
19
|
-
contextWindowTokens: number;
|
|
20
|
-
effortSupport: "supported";
|
|
21
|
-
supportedEfforts: ("high" | "medium" | "low")[];
|
|
22
|
-
continuation: "unsupported";
|
|
23
18
|
transport: string;
|
|
24
19
|
resumeRequiresFullContext?: boolean;
|
|
25
20
|
observedModel: boolean;
|
|
26
21
|
observedEffort: boolean;
|
|
27
|
-
|
|
22
|
+
continuation: 'unsupported';
|
|
23
|
+
effortSupport: 'supported';
|
|
24
|
+
supportedEfforts: ("high" | "low" | "medium")[];
|
|
28
25
|
agentLoop: "compact" | "free";
|
|
29
26
|
contextWindowTokens: number;
|
|
30
|
-
|
|
31
|
-
supportedEfforts: never[];
|
|
32
|
-
continuation: "unsupported";
|
|
27
|
+
} | {
|
|
33
28
|
transport: string;
|
|
34
29
|
resumeRequiresFullContext?: boolean;
|
|
35
30
|
observedModel: boolean;
|
|
36
31
|
observedEffort: boolean;
|
|
32
|
+
continuation: 'unsupported';
|
|
33
|
+
effortSupport: 'unsupported';
|
|
34
|
+
supportedEfforts: never[];
|
|
35
|
+
agentLoop: "compact" | "free";
|
|
36
|
+
contextWindowTokens: number;
|
|
37
37
|
};
|
|
38
38
|
validateLimits(limits: AgentLimits): void;
|
|
39
39
|
execute(request: AgentRequest): Promise<AgentResult>;
|
|
@@ -2,9 +2,9 @@ import { type AgentRole, type CliProvider, type RoleDescriptor, type AgentAccess
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
export declare const OPENSPEC_VERSION = "1.4.1";
|
|
4
4
|
export declare const ROLE_SKILLS: {
|
|
5
|
-
readonly architect:
|
|
6
|
-
readonly developer:
|
|
7
|
-
readonly reviewer:
|
|
5
|
+
readonly architect: 'openspec-ff-change';
|
|
6
|
+
readonly developer: 'openspec-apply-change';
|
|
7
|
+
readonly reviewer: 'openspec-verify-change';
|
|
8
8
|
};
|
|
9
9
|
export interface OpenSpecRoleContext {
|
|
10
10
|
evidenceScope?: {
|
|
@@ -112,7 +112,7 @@ export declare class OpenSpecTools {
|
|
|
112
112
|
assertParticipation(after?: number): void;
|
|
113
113
|
assertReady(): Promise<OpenSpecApply>;
|
|
114
114
|
}
|
|
115
|
-
export declare const OPENSPEC_TOOL_ACTIONS: readonly [
|
|
115
|
+
export declare const OPENSPEC_TOOL_ACTIONS: readonly ['load_skill', 'new', 'status', 'instructions', 'validate', 'write_artifact', 'read_progress', 'write_progress'];
|
|
116
116
|
export declare const OPENSPEC_TOOL_DEFINITION: {
|
|
117
117
|
type: string;
|
|
118
118
|
function: {
|
|
@@ -35,6 +35,12 @@ export interface RoleInstructionOptions {
|
|
|
35
35
|
planning?: 'full' | 'proportional';
|
|
36
36
|
/** The change so far, measured by Core from git against the run's base (rendered lines). */
|
|
37
37
|
changeSet?: string[];
|
|
38
|
+
/** Files the host verification regenerated and kept in the candidate; merged with `feedback.verification.adoptedOutputs`. */
|
|
39
|
+
adoptedOutputs?: AdoptedOutput[];
|
|
40
|
+
}
|
|
41
|
+
export interface AdoptedOutput {
|
|
42
|
+
repositoryId?: string;
|
|
43
|
+
path: string;
|
|
38
44
|
}
|
|
39
45
|
export declare const ARCHITECT_OUTPUT_SCHEMA: Record<string, unknown>;
|
|
40
46
|
export declare const DEVELOPER_OUTPUT_SCHEMA: Record<string, unknown>;
|
|
@@ -178,7 +178,7 @@ function architectSection(verification, definition) {
|
|
|
178
178
|
];
|
|
179
179
|
}
|
|
180
180
|
/** The FIXER stance: the developer role on a correction round — repair the exact failure, never re-implement. Shares the developer's verification tail and output contract. */
|
|
181
|
-
function fixerSection(verification, definition) {
|
|
181
|
+
function fixerSection(verification, definition, adopted = false) {
|
|
182
182
|
const lines = definition === undefined ? [
|
|
183
183
|
'## Your task: correction',
|
|
184
184
|
'',
|
|
@@ -187,7 +187,7 @@ function fixerSection(verification, definition) {
|
|
|
187
187
|
'',
|
|
188
188
|
'1. Read the host failure facts first: command, cwd, original exit code, failureSummary, expected assertion and application file/line. If the excerpt is truncated, use read_verification_evidence with the command\'s evidenceId for complete stdout/stderr and source. Read the failing test and the implementation it exercises.',
|
|
189
189
|
'2. Reproduce the failure with the narrowest test command. Compare the assertion, intended behavior, current implementation and actual diff. An unchanged file or a pre-existing test does not prove the failure is unrelated: changes can break existing tests indirectly. Classify the cause as an implementation defect, a test compatibility assumption, or an external/scope blocker. A claim that the failure existed before this change needs baseline or historical evidence; otherwise state that it is unconfirmed.',
|
|
190
|
-
'3. Patch exactly the files and lines the failure names; touch neighbouring code only when the failure cannot be fixed otherwise. Rewrite a file only when a patch cannot express the change. A correction never grows the blast radius: no new files, dependencies, renames, reformatting or clean-ups; after your turn the diff must be the previous change plus the minimal repair.',
|
|
190
|
+
'3. Patch exactly the files and lines the failure names; touch neighbouring code only when the failure cannot be fixed otherwise. Rewrite a file only when a patch cannot express the change. A correction never grows the blast radius: no new files, dependencies, renames, reformatting or clean-ups; after your turn the diff must be the previous change plus the minimal repair.' + (adopted ? ' Never revert the host-adopted verification output listed below.' : ''),
|
|
191
191
|
'4. Fix the cause, not the symptom: a repair that satisfies the assertion while leaving the defect, or that special-cases the test input, is not a fix. Fix incorrect implementation behavior. A failing mandatory host test inside the admitted repository/workspace may also receive a minimal compatibility repair when you prove that its expected behavior is unchanged. For example, a source assertion broken only by formatting may tolerate whitespace while retaining every required operand and guard. This applies even to a pre-existing test outside the current changed-file list. Explain the original expectation and why the repair still rejects the prohibited behavior. Never weaken an assertion, delete or skip a test, hardcode success, change acceptance criteria, add dependencies or widen the repair beyond the diagnosed failure.',
|
|
192
192
|
'5. A test file the host reports as never executed must be wired into the repository\'s test command (the test script or runner configuration) and then made to pass.',
|
|
193
193
|
'6. Confirm the repair with the focused test command and a negative case for any repaired assertion: removing a required safety guard or returning the wrong value must still fail. Preserve the original test exit status; never pipe test execution through grep/head or another command that masks it. Capture stdout/stderr in a temporary log and inspect that log separately. Core runs the complete verification plan after your turn.',
|
|
@@ -196,7 +196,7 @@ function fixerSection(verification, definition) {
|
|
|
196
196
|
] : [definition, ''];
|
|
197
197
|
return [...lines, ...developerTail(verification)];
|
|
198
198
|
}
|
|
199
|
-
function developerSection(verification, corrections, definition) {
|
|
199
|
+
function developerSection(verification, corrections, definition, adopted = false) {
|
|
200
200
|
const lines = definition === undefined ? [
|
|
201
201
|
'## Your task: implementation',
|
|
202
202
|
'',
|
|
@@ -209,7 +209,7 @@ function developerSection(verification, corrections, definition) {
|
|
|
209
209
|
'2. Work task by task in order. Use test-driven development: write or extend the test first, make it pass with the smallest correct change, then tidy up. Run only focused tests that cover what you touched while iterating. Core owns the complete verification plan and runs it after your turn; do not duplicate that full run. Fix the precise failures Core returns on a correction pass.',
|
|
210
210
|
'3. Immediately after completing each task, mark it `- [x]` in `tasks.md`; do not postpone all progress updates until the end of the phase. Only mark tasks whose code and tests are complete. Change nothing else in `tasks.md`, and never edit `proposal.md`, `design.md` or the specs: those documents are frozen, and editing them invalidates the run. If a task cannot be completed, leave it `- [ ]` and list it under `incomplete` with the reason.',
|
|
211
211
|
'4. Keep the implementation consistent with the repository: naming, error handling, import style, formatting and existing utilities. Do not add dependencies unless the design requires them. Change only what the tasks require: the diff should contain the requested change and its tests, nothing else. Never validate with a temporary configuration, alternate runner or local browser the host verification plan does not use, and never delete such a file to hide it: the host runs the plan as-is. When a required tool is missing, install it through the project\'s documented command or report it as a blocker.',
|
|
212
|
-
'Blast radius: the files design.md and tasks.md name are your boundary. Touch a file outside it only when a task cannot be completed otherwise; keep that edit to the strict need and explain it in summary. Inside a file, change only the lines the task needs: no reformatting, import reordering, renames, type widening, comment rewrites or "while I am here" fixes. Leave an unrelated problem alone and mention it in summary. Keep public signatures, exported contracts, schemas and persisted formats as the design states. Never delete or rewrite a test a task does not name, and never regenerate lockfiles, snapshots or generated files unless the task\'s own change requires it.',
|
|
212
|
+
'Blast radius: the files design.md and tasks.md name are your boundary. Touch a file outside it only when a task cannot be completed otherwise; keep that edit to the strict need and explain it in summary. Inside a file, change only the lines the task needs: no reformatting, import reordering, renames, type widening, comment rewrites or "while I am here" fixes. Leave an unrelated problem alone and mention it in summary. Keep public signatures, exported contracts, schemas and persisted formats as the design states. Never delete or rewrite a test a task does not name, and never regenerate lockfiles, snapshots or generated files unless the task\'s own change requires it' + (adopted ? ' (the host-adopted verification output listed below is exempt: never revert it).' : '.'),
|
|
213
213
|
'Quality bar: write code a senior maintainer would merge unchanged. Intention-revealing names from the domain vocabulary; small functions that do one thing at one level of abstraction; guard clauses over nested conditionals; no boolean flag parameters, magic values or hidden side effects; immutability by default and explicit types at module boundaries; parse and validate at the boundary, trust typed values inside; every error path handled the way neighbouring code handles it (fail fast at the boundary, never swallow, errors carry context); domain logic out of adapters and dependencies pointing inward as the repository already does; comments explain why, never what; no dead code, commented-out code, debug output, TODO placeholders or speculative options.',
|
|
214
214
|
'Engineering judgement: understand before you change and never program by coincidence (if you cannot explain why it works, you are not done); never delete or bypass a guard, branch or workaround you cannot explain. Handle what tests rarely reach: empty, huge and malformed inputs, boundary values, time zones and Unicode, partial failures, retries with idempotency, races, cancellation, timeouts and resource cleanup (handles, listeners, subscriptions, temp files). Security hygiene is non-negotiable: parameterized queries, escaped output, no secrets or personal data in code or logs, least privilege, authorization where the repository enforces it. Logs and metrics follow the repository\'s conventions; migrations and persisted-format changes stay additive and backward compatible. Verify every API, signature and option against the source or installed types, never from memory. Do the simplest thing that fully works, then refactor only inside the blast radius. Tests are the specification: one behavior per test (arrange, act, assert) covering inputs, outputs, side effects and errors; no logic in tests; mock only at real boundaries; deterministic; failing without the change; in the repository\'s existing style. A task is done only when its code, tests and every artifact it makes stale (localized strings in every shipped locale, documentation, schemas, configuration examples) are updated and its focused checks pass.',
|
|
215
215
|
'Investigation budget: consult the local reference patterns in design.md and equivalent application tests before framework internals or node_modules. After three unsuccessful experiments on the same failure, stop repeating commands: state the hypothesis, evidence and next discriminating experiment, then change approach. If three further experiments add no evidence, report the specific blocker under incomplete rather than consuming the remaining turn budget. Never weaken assertions or change acceptance criteria to make a test pass.',
|
|
@@ -234,7 +234,7 @@ function developerTail(verification) {
|
|
|
234
234
|
lines.push('', '## Durable implementation progress', '', 'When loading the official apply skill, read its savedProgress handoff before repeating investigation. Reconcile it with the current diff and tasks; it is advisory history, never accepted verification evidence. Preserve completed work and previously discovered test commands or environment blockers. Recheck a blocker only when its relevant condition changed.', 'After each completed task, and whenever the next action or a blocker changes, call the scoped workflow action write_progress with progress: {summary, completedTasks, nextTasks, checks: [{command, outcome}], blockers}. Replace the previous handoff with a concise current account (at most 8 KB); include exact focused test commands, original exit outcomes, relevant paths and the next discriminating action. Keep nextTasks concrete enough for a fresh session to continue unfinished work. Do not include full logs, credentials or claims that old checks authorize acceptance. The host persists this record outside the frozen OpenSpec artifacts. Use read_progress to refresh it. If interrupted, the next session receives it from load_skill; do not write runtime files directly.', '', '## Output contract', '', 'Finish with exactly one JSON object and nothing after it: no prose after the object, no Markdown fence.', '', '```', '{"summary":"What you implemented and how","files":["src/feature.ts"],"tests":["src/feature.test.ts"],"verification":"npm test passed (12 tests)","incomplete":[{"task":"3. …","reason":"why it could not be completed"}],"blocker":{"kind":"toolchain","command":"npx playwright test","cwd":".","evidence":"exact error","requiredAction":"Run npx playwright install chromium"}}', '```', '', '- `files` and `tests`: repository-relative paths you created or modified (test files appear in `tests`, other files in `files`).', '- `verification`: the commands you ran and their outcome, or `none` when no shell was available.', '- `incomplete`: every task still `- [ ]` in `tasks.md`, with its reason; an empty array when everything is done.', '- `blocker`: only when the cause lies outside the change; omit otherwise. The host ends the run with your `requiredAction` instead of starting another correction round.', '- Do not paste full test logs; the reviewer reads the real verification evidence separately.', '');
|
|
235
235
|
return lines;
|
|
236
236
|
}
|
|
237
|
-
function reviewerSection(policy, criteria, definition) {
|
|
237
|
+
function reviewerSection(policy, criteria, definition, adopted = false) {
|
|
238
238
|
const aspects = REVIEW_ASPECTS.map(name => `\`${name}\` ≥ ${policy.aspects[name]}`).join(', ');
|
|
239
239
|
const lines = definition === undefined ? [
|
|
240
240
|
'## Your task: review',
|
|
@@ -246,7 +246,7 @@ function reviewerSection(policy, criteria, definition) {
|
|
|
246
246
|
'1. Spec completeness: every requirement in the change specs and every acceptance criterion is implemented. Cross-reference each one against the code.',
|
|
247
247
|
'2. Task completion: every task in `tasks.md` is `- [x]` and is backed by real code and tests, not just a ticked box.',
|
|
248
248
|
'3. Test quality: new behavior has tests that assert on behavior, cover error paths, and would fail without the change. Missing tests for production code, tests without assertions and tests that restate the implementation are blocking issues.',
|
|
249
|
-
'4. Blast radius: compare the change set below with the files design.md and tasks.md name. A file the plan did not name, a hunk no task explains (reformatting, import reordering, renames, comment rewrites, type widening, deleted or rewritten tests outside the tasks, regenerated lockfiles, snapshots or generated files, new dependencies, widened public signatures) or an edit to a shared module without a stated reason is an issue to REVERT, not to polish: name the file and the hunk. Trace every criterion to the narrowest code that satisfies it; code beyond that is suspect. Unjustified blast radius lowers `architectural_alignment` and `pattern_adherence`.',
|
|
249
|
+
'4. Blast radius: compare the change set below with the files design.md and tasks.md name. A file the plan did not name, a hunk no task explains (reformatting, import reordering, renames, comment rewrites, type widening, deleted or rewritten tests outside the tasks, regenerated lockfiles, snapshots or generated files' + (adopted ? ' other than the host-adopted verification output listed below' : '') + ', new dependencies, widened public signatures) or an edit to a shared module without a stated reason is an issue to REVERT, not to polish: name the file and the hunk. Trace every criterion to the narrowest code that satisfies it; code beyond that is suspect. Unjustified blast radius lowers `architectural_alignment` and `pattern_adherence`.',
|
|
250
250
|
'5. Correctness and conventions: types and signatures fit the codebase, patterns match the repository, imports and error handling are consistent, no dead code, debug output or placeholders. Architecture: the change respects the dependency direction and boundaries the repository already follows; domain logic placed in an adapter, an adapter imported from the domain, a bypassed existing port or helper, a duplicated utility that already exists, or a new abstraction with one implementation and no variation point is an issue when it breaks a convention the code enforces and a finding otherwise. Platform correctness as far as the change touches it: web state, accessibility, loading and error states; mobile lifecycle, offline and permissions; backend transactions, idempotency and validation at the boundary.',
|
|
251
251
|
'6. Security: no secrets, injection, path traversal, unsafe deserialization, missing authorization or new attack surface. Scale scrutiny to what the change touches.',
|
|
252
252
|
'7. Performance: no obvious N+1, unbounded loops or blocking work on hot paths introduced by the change.',
|
|
@@ -298,6 +298,28 @@ function discardedSection(developer) {
|
|
|
298
298
|
return [];
|
|
299
299
|
return ['## Edits Core undid', '', 'The previous turn edited files outside the repository scope; Core restored them because they are not part of this change. Do not redo them:', ...developer.discarded.slice(0, 50).map(file => `- \`${file}\``), ''];
|
|
300
300
|
}
|
|
301
|
+
/** Host-adopted verification output from the options and the verification feedback, deduplicated and bounded. */
|
|
302
|
+
function adoptedOutputs(feedback, extra) {
|
|
303
|
+
const raw = record(feedback?.verification)?.adoptedOutputs;
|
|
304
|
+
const listed = [...(extra ?? []), ...(Array.isArray(raw) ? raw : [])].map(record).filter((item) => typeof item?.path === 'string' && item.path.length > 0);
|
|
305
|
+
return [...new Map(listed.map(item => [String(item.repositoryId ?? '') + '\0' + String(item.path), { ...(typeof item.repositoryId === 'string' ? { repositoryId: item.repositoryId } : {}), path: String(item.path).slice(0, 512) }])).values()].slice(0, 50);
|
|
306
|
+
}
|
|
307
|
+
/** Files the verification commands regenerated themselves: reverting them only makes the next verification regenerate them. */
|
|
308
|
+
function adoptedOutputSection(role, adopted) {
|
|
309
|
+
if (!adopted.length || (role !== 'reviewer' && role !== 'developer'))
|
|
310
|
+
return [];
|
|
311
|
+
return [
|
|
312
|
+
'## Host-adopted verification output',
|
|
313
|
+
'',
|
|
314
|
+
'The host verification commands regenerated these files themselves; the host kept them in the candidate and verified it again, so they belong to the change. They are exempt from the blast-radius rule on regenerated generated files:',
|
|
315
|
+
...adopted.map(file => `- ${file.repositoryId ? '`' + file.repositoryId + '`: ' : ''}\`${file.path}\``),
|
|
316
|
+
'',
|
|
317
|
+
role === 'reviewer'
|
|
318
|
+
? 'Never ask for one of them to be reverted to the base version. You may still raise an issue when an adopted file\'s content is wrong for this change.'
|
|
319
|
+
: 'Never revert them to the base version (no checkout, restore or deletion): the next verification would regenerate them. Edit one only when a reported failure lies in its content.',
|
|
320
|
+
'',
|
|
321
|
+
];
|
|
322
|
+
}
|
|
301
323
|
function feedbackSection(feedback, focused = false) {
|
|
302
324
|
const lines = [];
|
|
303
325
|
const verification = record(feedback?.verification);
|
|
@@ -399,8 +421,9 @@ export function roleInstructions(roleOrDescriptor, context, change, options = {}
|
|
|
399
421
|
}
|
|
400
422
|
const feedback = feedbackSection(options.feedback);
|
|
401
423
|
const corrections = role === 'developer' && feedback.length > 0;
|
|
424
|
+
const adopted = role === 'architect' ? [] : adoptedOutputs(options.feedback, options.adoptedOutputs);
|
|
402
425
|
const sections = [
|
|
403
|
-
...(role === 'architect' ? architectSection(options.verification, options.definition) : role === 'developer' ? (options.stance === 'fixer' ? fixerSection(options.verification, options.definition) : developerSection(options.verification, corrections, options.definition)) : reviewerSection(options.policy ?? DEFAULT_REVIEW_POLICY, options.criteria, options.definition)),
|
|
426
|
+
...(role === 'architect' ? architectSection(options.verification, options.definition) : role === 'developer' ? (options.stance === 'fixer' ? fixerSection(options.verification, options.definition, adopted.length > 0) : developerSection(options.verification, corrections, options.definition, adopted.length > 0)) : reviewerSection(options.policy ?? DEFAULT_REVIEW_POLICY, options.criteria, options.definition, adopted.length > 0)),
|
|
404
427
|
...boundarySection(role),
|
|
405
428
|
...conventionsSection(),
|
|
406
429
|
...scopeSection(context, change),
|
|
@@ -410,6 +433,7 @@ export function roleInstructions(roleOrDescriptor, context, change, options = {}
|
|
|
410
433
|
...changeSetSection(role, options.stance, options.changeSet),
|
|
411
434
|
...(role === 'developer' ? discardedSection(options.developer) : []),
|
|
412
435
|
...(role === 'reviewer' ? reReviewSection(options.reReview) : []),
|
|
436
|
+
...adoptedOutputSection(role, adopted),
|
|
413
437
|
...feedback,
|
|
414
438
|
];
|
|
415
439
|
return sections.join('\n').trimEnd() + '\n';
|
|
@@ -421,7 +445,7 @@ export function correctionInstructions(role, feedback, extra = {}) {
|
|
|
421
445
|
const instruction = extra.focusedEvidence
|
|
422
446
|
? `Continue the ${role} role with unchanged permissions and obligations. Fix the feedback, preserve correct work, finish all tasks and mark them \`- [x]\`. Return only the same JSON summary (summary, files, tests, verification, incomplete).`
|
|
423
447
|
: 'Continue the same ' + role + ' role in this session. Address the feedback below precisely, keep the already-correct work, finish every remaining task, mark completed tasks `- [x]` in `tasks.md`, and finish with the same JSON summary object as before (summary, files, tests, verification, incomplete), with nothing after it.';
|
|
424
|
-
const lines = [instruction, '', ...(role === 'developer' ? [...changeSetSection(role, undefined, extra.changeSet), ...discardedSection(extra.developer)] : []), ...feedbackSection(feedback, extra.focusedEvidence)];
|
|
448
|
+
const lines = [instruction, '', ...(role === 'developer' ? [...changeSetSection(role, undefined, extra.changeSet), ...discardedSection(extra.developer)] : []), ...adoptedOutputSection(role, adoptedOutputs(feedback, undefined)), ...feedbackSection(feedback, extra.focusedEvidence)];
|
|
425
449
|
return lines.join('\n').trimEnd() + '\n';
|
|
426
450
|
}
|
|
427
451
|
/** Asks the architect, still in its session, to resolve a low-confidence design by investigating instead of guessing. */
|
|
@@ -9,37 +9,37 @@ export declare const recoveryRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject
|
|
|
9
9
|
path: z.ZodString;
|
|
10
10
|
action: z.ZodLiteral<"list_files">;
|
|
11
11
|
}, z.core.$strict>, z.ZodObject<{
|
|
12
|
-
startLine: z.ZodOptional<z.ZodNumber>;
|
|
13
|
-
endLine: z.ZodOptional<z.ZodNumber>;
|
|
14
12
|
repositoryId: z.ZodString;
|
|
15
13
|
path: z.ZodString;
|
|
16
14
|
action: z.ZodLiteral<"read_file">;
|
|
15
|
+
startLine: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
endLine: z.ZodOptional<z.ZodNumber>;
|
|
17
17
|
}, z.core.$strict>, z.ZodObject<{
|
|
18
18
|
repositoryId: z.ZodString;
|
|
19
19
|
path: z.ZodString;
|
|
20
20
|
action: z.ZodLiteral<"diff">;
|
|
21
21
|
}, z.core.$strict>, z.ZodObject<{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
newText: z.ZodString;
|
|
22
|
+
repositoryId: z.ZodString;
|
|
23
|
+
path: z.ZodString;
|
|
25
24
|
operationId: z.ZodString;
|
|
26
25
|
reason: z.ZodString;
|
|
27
26
|
acknowledgeInterrupted: z.ZodOptional<z.ZodBoolean>;
|
|
28
27
|
changedPrecondition: z.ZodOptional<z.ZodString>;
|
|
29
|
-
repositoryId: z.ZodString;
|
|
30
|
-
path: z.ZodString;
|
|
31
28
|
action: z.ZodLiteral<"patch">;
|
|
29
|
+
expectedHash: z.ZodString;
|
|
30
|
+
oldText: z.ZodString;
|
|
31
|
+
newText: z.ZodString;
|
|
32
32
|
}, z.core.$strict>, z.ZodObject<{
|
|
33
|
-
kind: z.ZodEnum<{
|
|
34
|
-
verification: "verification";
|
|
35
|
-
openspec: "openspec";
|
|
36
|
-
}>;
|
|
37
|
-
checkId: z.ZodOptional<z.ZodString>;
|
|
38
33
|
operationId: z.ZodString;
|
|
39
34
|
reason: z.ZodString;
|
|
40
35
|
acknowledgeInterrupted: z.ZodOptional<z.ZodBoolean>;
|
|
41
36
|
changedPrecondition: z.ZodOptional<z.ZodString>;
|
|
42
37
|
action: z.ZodLiteral<"check">;
|
|
38
|
+
kind: z.ZodEnum<{
|
|
39
|
+
openspec: "openspec";
|
|
40
|
+
verification: "verification";
|
|
41
|
+
}>;
|
|
42
|
+
checkId: z.ZodOptional<z.ZodString>;
|
|
43
43
|
}, z.core.$strict>], "action">;
|
|
44
44
|
export type RecoveryRequest = z.infer<typeof recoveryRequestSchema>;
|
|
45
45
|
/** Uses the SAME cross-process lease as resume. No provider calls or checkpoint editing. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DriverOpenSpec } from '../../ports.js';
|
|
2
2
|
/** Tools that launch sub-agents in Claude Code; removed when policy disables sub-agents. */
|
|
3
|
-
export declare const CLAUDE_SUBAGENT_TOOLS: readonly [
|
|
4
|
-
export declare const CLAUDE_READ_ONLY_TOOLS: readonly [
|
|
3
|
+
export declare const CLAUDE_SUBAGENT_TOOLS: readonly ['Agent', 'Task'];
|
|
4
|
+
export declare const CLAUDE_READ_ONLY_TOOLS: readonly ['Read', 'Grep', 'Glob'];
|
|
5
5
|
/**
|
|
6
6
|
* Map an open spec + policy to Claude Code argv. Lists are passed as one
|
|
7
7
|
* comma-separated argument (variadic flags would otherwise swallow later args).
|
|
@@ -1,16 +1,45 @@
|
|
|
1
1
|
import type { spawnSync } from 'node:child_process';
|
|
2
|
-
import type
|
|
3
|
-
import { type InstallOutcome } from './compact/environment.js';
|
|
2
|
+
import { type CommandReceipt, type PipelineContext, type VerificationReceipt, type VerificationSelfMutation } from '../pipeline/pipeline-state.js';
|
|
3
|
+
import { type DriftedDependency, type InstallOutcome } from './compact/environment.js';
|
|
4
4
|
import type { HostBlocker } from './engine/contracts.js';
|
|
5
5
|
import { type GuardrailSettings } from './guardrails.js';
|
|
6
6
|
export type { HostBlocker } from './engine/contracts.js';
|
|
7
7
|
export declare function installRoots(context: PipelineContext, commands: readonly CommandReceipt[]): string[];
|
|
8
|
+
/**
|
|
9
|
+
* Installed dependencies out of their declared ranges in any package root that
|
|
10
|
+
* contains a failing command's cwd (from the cwd up to its repository). The
|
|
11
|
+
* failure such drift causes is arbitrary (a lint error on an untouched file),
|
|
12
|
+
* so the drift itself, not the output, marks the failure as environmental.
|
|
13
|
+
*/
|
|
14
|
+
export declare function driftedFailureRoots(context: PipelineContext, receipt: VerificationReceipt): Array<{
|
|
15
|
+
root: string;
|
|
16
|
+
drift: DriftedDependency[];
|
|
17
|
+
}>;
|
|
18
|
+
/** Why a failed receipt is the host's environment to repair: a missing tool or dependency in the output, or installed versions out of their declared ranges. */
|
|
19
|
+
export declare function environmentFailure(context: PipelineContext, receipt: VerificationReceipt): {
|
|
20
|
+
drift: Array<{
|
|
21
|
+
root: string;
|
|
22
|
+
drift: DriftedDependency[];
|
|
23
|
+
}>;
|
|
24
|
+
} | undefined;
|
|
8
25
|
/** A directory as the log should name it: relative to the checkout that contains it. */
|
|
9
26
|
export declare function checkoutRelative(context: PipelineContext, directory: string): string;
|
|
10
27
|
/** The legacy host-facing sentence for a blocker; engine v2 hosts read the structured blocker instead. */
|
|
11
28
|
export declare function hostPreconditionMessage(reason: string, command: string, args: readonly string[], directory: string): string;
|
|
12
29
|
/** A failed check the host must unblock (credentials, registry access, a missing variable): no correction round can repair it. */
|
|
13
30
|
export declare function preconditionBlock(context: PipelineContext, receipt: VerificationReceipt): HostBlocker | undefined;
|
|
31
|
+
/** Error code of a verification whose commands keep rewriting the candidate: the repository's or operator's to fix, never the fixer's. */
|
|
32
|
+
export declare const NONDETERMINISTIC_OUTPUT_CODE = "verification_nondeterministic_output";
|
|
33
|
+
/**
|
|
34
|
+
* Every command exited 0 and ran to completion, and the receipt is invalid only
|
|
35
|
+
* because the commands modified the candidate themselves: deterministic tool
|
|
36
|
+
* output (a regenerated mapping) the host may adopt, never a code failure.
|
|
37
|
+
*/
|
|
38
|
+
export declare function selfMutationOnly(receipt: VerificationReceipt): boolean;
|
|
39
|
+
/** The typed blocker for output that did not converge on the adoption re-run, or that the `verification-output-adoption` guardrail keeps out of the candidate. */
|
|
40
|
+
export declare function nondeterministicOutputBlocker(context: PipelineContext, receipt: VerificationReceipt, adoption: boolean): HostBlocker;
|
|
41
|
+
/** The progress line naming the files the host adopted into the candidate. */
|
|
42
|
+
export declare function adoptedOutputNote(mutation: VerificationSelfMutation): string;
|
|
14
43
|
export interface EnvironmentRepair {
|
|
15
44
|
installs: InstallOutcome[];
|
|
16
45
|
refused?: HostBlocker;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { describeModifiedFiles } from '../pipeline/pipeline-state.js';
|
|
4
|
+
import { driftedNodeDependencies, hostPrecondition, installEnvironment, isEnvironmentFailure } from './compact/environment.js';
|
|
4
5
|
import { guardrailEnabled } from './guardrails.js';
|
|
5
|
-
const BLOCKER_KINDS = new Set(['network', 'credential', 'environment-variable', 'toolchain', 'setup', 'environment', 'scope']);
|
|
6
|
+
const BLOCKER_KINDS = new Set(['network', 'credential', 'environment-variable', 'toolchain', 'setup', 'environment', 'scope', 'nondeterministic-output']);
|
|
6
7
|
const BLOCKER_TEXT_LIMIT = 2_000;
|
|
7
8
|
/** Where a missing dependency install belongs: the nearest directory with a lockfile or manifest above the failing command, else the repository root. */
|
|
8
9
|
const INSTALL_MARKERS = ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb', 'requirements.txt', 'pyproject.toml', 'go.mod', 'Cargo.toml'];
|
|
@@ -31,6 +32,35 @@ export function installRoots(context, commands) {
|
|
|
31
32
|
// Every repository root stays a candidate: its plan is a no-op when it is already installed.
|
|
32
33
|
return [...new Set([...roots, ...context.repositories.map(repository => repository.path)])];
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Installed dependencies out of their declared ranges in any package root that
|
|
37
|
+
* contains a failing command's cwd (from the cwd up to its repository). The
|
|
38
|
+
* failure such drift causes is arbitrary (a lint error on an untouched file),
|
|
39
|
+
* so the drift itself, not the output, marks the failure as environmental.
|
|
40
|
+
*/
|
|
41
|
+
export function driftedFailureRoots(context, receipt) {
|
|
42
|
+
const roots = new Set();
|
|
43
|
+
for (const command of receipt.commands) {
|
|
44
|
+
const repository = command.exitCode === 0 ? undefined : context.repositories.find(repo => repo.id === command.repositoryId);
|
|
45
|
+
if (!repository)
|
|
46
|
+
continue;
|
|
47
|
+
for (let directory = command.cwd || repository.path;; directory = path.dirname(directory)) {
|
|
48
|
+
const relative = path.relative(repository.path, directory);
|
|
49
|
+
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
50
|
+
break;
|
|
51
|
+
if (existsSync(path.join(directory, 'package.json')))
|
|
52
|
+
roots.add(directory);
|
|
53
|
+
if (!relative)
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return [...roots].map(root => ({ root, drift: driftedNodeDependencies(root) })).filter(item => item.drift.length > 0);
|
|
58
|
+
}
|
|
59
|
+
/** Why a failed receipt is the host's environment to repair: a missing tool or dependency in the output, or installed versions out of their declared ranges. */
|
|
60
|
+
export function environmentFailure(context, receipt) {
|
|
61
|
+
const drift = driftedFailureRoots(context, receipt);
|
|
62
|
+
return drift.length || receipt.commands.some(command => isEnvironmentFailure(command.exitCode, command.output)) ? { drift } : undefined;
|
|
63
|
+
}
|
|
34
64
|
/** A directory as the log should name it: relative to the checkout that contains it. */
|
|
35
65
|
export function checkoutRelative(context, directory) {
|
|
36
66
|
const repository = context.repositories.find(repo => { const relative = path.relative(repo.path, directory); return !relative.startsWith('..') && !path.isAbsolute(relative); });
|
|
@@ -52,6 +82,30 @@ export function preconditionBlock(context, receipt) {
|
|
|
52
82
|
}
|
|
53
83
|
return undefined;
|
|
54
84
|
}
|
|
85
|
+
/** Error code of a verification whose commands keep rewriting the candidate: the repository's or operator's to fix, never the fixer's. */
|
|
86
|
+
export const NONDETERMINISTIC_OUTPUT_CODE = 'verification_nondeterministic_output';
|
|
87
|
+
/**
|
|
88
|
+
* Every command exited 0 and ran to completion, and the receipt is invalid only
|
|
89
|
+
* because the commands modified the candidate themselves: deterministic tool
|
|
90
|
+
* output (a regenerated mapping) the host may adopt, never a code failure.
|
|
91
|
+
*/
|
|
92
|
+
export function selfMutationOnly(receipt) {
|
|
93
|
+
return !receipt.valid && !!receipt.selfMutation?.files.length && receipt.commands.length > 0 && !receipt.notRunEvidenceIds?.length
|
|
94
|
+
&& receipt.commands.every(command => command.exitCode === 0 && (command.outcome === undefined || command.outcome === 'passed'));
|
|
95
|
+
}
|
|
96
|
+
/** The typed blocker for output that did not converge on the adoption re-run, or that the `verification-output-adoption` guardrail keeps out of the candidate. */
|
|
97
|
+
export function nondeterministicOutputBlocker(context, receipt, adoption) {
|
|
98
|
+
const mutation = receipt.selfMutation ?? { files: [], commands: [] };
|
|
99
|
+
const command = receipt.commands.find(item => item.repositoryId === mutation.files[0]?.repositoryId && item.disposition !== 'reused') ?? receipt.commands[0];
|
|
100
|
+
return { kind: 'nondeterministic-output', reason: (adoption ? `the verification commands modified ${describeModifiedFiles(mutation)}, and modified them again when the host verified the adopted output once more`
|
|
101
|
+
: `the verification commands modified ${describeModifiedFiles(mutation)}, and the verification-output-adoption guardrail keeps verification output out of the candidate`).slice(0, BLOCKER_TEXT_LIMIT),
|
|
102
|
+
command: command?.command.slice(0, 512) ?? '', args: (command?.args ?? []).slice(0, 16).map(arg => arg.slice(0, 256)), cwd: command ? checkoutRelative(context, command.cwd) : '.',
|
|
103
|
+
requiredAction: 'Commit the generated output on the base branch or make the generator idempotent, then retry the run.' };
|
|
104
|
+
}
|
|
105
|
+
/** The progress line naming the files the host adopted into the candidate. */
|
|
106
|
+
export function adoptedOutputNote(mutation) {
|
|
107
|
+
return `[verification] adopted ${mutation.files.length + (mutation.omittedFiles ?? 0)} generated file(s): ${describeModifiedFiles(mutation).replace(/^\d+ candidate files?: /, '')}`;
|
|
108
|
+
}
|
|
55
109
|
/**
|
|
56
110
|
* Runs the planned installs for the failed commands' roots once, narrating each
|
|
57
111
|
* step through `note` exactly as the legacy graph logs it (`[environment] npm
|
|
@@ -60,7 +114,8 @@ export function preconditionBlock(context, receipt) {
|
|
|
60
114
|
*/
|
|
61
115
|
export function repairEnvironment(context, receipt, guardrails, note, spawn) {
|
|
62
116
|
const failed = receipt.commands.filter(command => command.exitCode !== 0);
|
|
63
|
-
const
|
|
117
|
+
const roots = [...new Set([...installRoots(context, failed), ...driftedFailureRoots(context, receipt).map(item => item.root)])];
|
|
118
|
+
const installs = installEnvironment(roots, { failureOutput: receipt.commands.map(command => command.output).join('\n'), lockfileRepair: guardrailEnabled(guardrails, 'lockfile-repair'),
|
|
64
119
|
...(spawn ? { spawn } : {}), onEvent: event => note(event.kind === 'text' ? event.text ?? '' : `[environment] ${event.tool ?? ''} ${event.detail ?? ''}`.trim()) });
|
|
65
120
|
const refused = installs.find(outcome => outcome.precondition);
|
|
66
121
|
if (!refused)
|
|
@@ -106,7 +106,7 @@ export declare function buildOpenSpecInvocation(repoRoot: string, provider: Prov
|
|
|
106
106
|
bin: string;
|
|
107
107
|
args: string[];
|
|
108
108
|
};
|
|
109
|
-
export declare const KIMI_REQUIRED_OPENSPEC_SKILLS: readonly [
|
|
109
|
+
export declare const KIMI_REQUIRED_OPENSPEC_SKILLS: readonly ['openspec-propose', 'openspec-explore', 'openspec-new-change', 'openspec-continue-change', 'openspec-apply-change', 'openspec-ff-change', 'openspec-sync-specs', 'openspec-archive-change', 'openspec-bulk-archive-change', 'openspec-verify-change', 'openspec-onboard'];
|
|
110
110
|
/**
|
|
111
111
|
* Move only OpenSpec-owned Kimi workflow directories from either upstream
|
|
112
112
|
* location into the artifact workspace. Every destination is copied to a
|