specrails-core 5.2.2 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/bin/specrails-core.mjs +5 -0
- package/dist/agent-runtime/cli-executor.d.ts +34 -0
- package/dist/agent-runtime/cli-executor.js +335 -0
- package/dist/agent-runtime/cli-executor.js.map +1 -0
- package/dist/agent-runtime/cli-process.d.ts +32 -0
- package/dist/agent-runtime/cli-process.js +159 -0
- package/dist/agent-runtime/cli-process.js.map +1 -0
- package/dist/agent-runtime/cli.d.ts +38 -0
- package/dist/agent-runtime/cli.js +168 -0
- package/dist/agent-runtime/cli.js.map +1 -0
- package/dist/agent-runtime/config.d.ts +7 -0
- package/dist/agent-runtime/config.js +162 -0
- package/dist/agent-runtime/config.js.map +1 -0
- package/dist/agent-runtime/core-host.d.ts +33 -0
- package/dist/agent-runtime/core-host.js +97 -0
- package/dist/agent-runtime/core-host.js.map +1 -0
- package/dist/agent-runtime/durable-store.d.ts +22 -0
- package/dist/agent-runtime/durable-store.js +205 -0
- package/dist/agent-runtime/durable-store.js.map +1 -0
- package/dist/agent-runtime/executor-types.d.ts +92 -0
- package/dist/agent-runtime/executor-types.js +29 -0
- package/dist/agent-runtime/executor-types.js.map +1 -0
- package/dist/agent-runtime/executors.d.ts +22 -0
- package/dist/agent-runtime/executors.js +44 -0
- package/dist/agent-runtime/executors.js.map +1 -0
- package/dist/agent-runtime/gemini-policy.d.ts +8 -0
- package/dist/agent-runtime/gemini-policy.js +36 -0
- package/dist/agent-runtime/gemini-policy.js.map +1 -0
- package/dist/agent-runtime/graph/artifacts.d.ts +40 -0
- package/dist/agent-runtime/graph/artifacts.js +179 -0
- package/dist/agent-runtime/graph/artifacts.js.map +1 -0
- package/dist/agent-runtime/graph/nodes.d.ts +36 -0
- package/dist/agent-runtime/graph/nodes.js +274 -0
- package/dist/agent-runtime/graph/nodes.js.map +1 -0
- package/dist/agent-runtime/graph/review-policy.d.ts +18 -0
- package/dist/agent-runtime/graph/review-policy.js +32 -0
- package/dist/agent-runtime/graph/review-policy.js.map +1 -0
- package/dist/agent-runtime/graph/roles.d.ts +44 -0
- package/dist/agent-runtime/graph/roles.js +126 -0
- package/dist/agent-runtime/graph/roles.js.map +1 -0
- package/dist/agent-runtime/graph/state.d.ts +75 -0
- package/dist/agent-runtime/graph/state.js +23 -0
- package/dist/agent-runtime/graph/state.js.map +1 -0
- package/dist/agent-runtime/graph-checkpointer.d.ts +61 -0
- package/dist/agent-runtime/graph-checkpointer.js +137 -0
- package/dist/agent-runtime/graph-checkpointer.js.map +1 -0
- package/dist/agent-runtime/index.d.ts +14 -0
- package/dist/agent-runtime/index.js +15 -0
- package/dist/agent-runtime/index.js.map +1 -0
- package/dist/agent-runtime/kimi-acp.d.ts +7 -0
- package/dist/agent-runtime/kimi-acp.js +179 -0
- package/dist/agent-runtime/kimi-acp.js.map +1 -0
- package/dist/agent-runtime/openai-executor.d.ts +18 -0
- package/dist/agent-runtime/openai-executor.js +174 -0
- package/dist/agent-runtime/openai-executor.js.map +1 -0
- package/dist/agent-runtime/prompts.d.ts +40 -0
- package/dist/agent-runtime/prompts.js +291 -0
- package/dist/agent-runtime/prompts.js.map +1 -0
- package/dist/agent-runtime/workflow-types.d.ts +210 -0
- package/dist/agent-runtime/workflow-types.js +2 -0
- package/dist/agent-runtime/workflow-types.js.map +1 -0
- package/dist/agent-runtime/workflow.d.ts +17 -0
- package/dist/agent-runtime/workflow.js +609 -0
- package/dist/agent-runtime/workflow.js.map +1 -0
- package/dist/agent-runtime/workspace-tools.d.ts +25 -0
- package/dist/agent-runtime/workspace-tools.js +120 -0
- package/dist/agent-runtime/workspace-tools.js.map +1 -0
- package/dist/installer/cli.d.ts +35 -0
- package/dist/installer/cli.js +3 -0
- package/dist/installer/cli.js.map +1 -1
- package/dist/installer/commands/doctor.d.ts +27 -0
- package/dist/installer/commands/framework.d.ts +85 -0
- package/dist/installer/commands/init.d.ts +147 -0
- package/dist/installer/commands/update.d.ts +56 -0
- package/dist/installer/commands/v5-migration.d.ts +32 -0
- package/dist/installer/phases/framework-lifecycle.d.ts +53 -0
- package/dist/installer/phases/install-config.d.ts +64 -0
- package/dist/installer/phases/manifest.d.ts +45 -0
- package/dist/installer/phases/prereqs.d.ts +51 -0
- package/dist/installer/phases/provider-detect.d.ts +89 -0
- package/dist/installer/phases/scaffold.d.ts +211 -0
- package/dist/installer/runtime/kimi.d.ts +84 -0
- package/dist/installer/runtime/pipeline-state.d.ts +210 -0
- package/dist/installer/runtime/pipeline-state.js +107 -38
- package/dist/installer/runtime/pipeline-state.js.map +1 -1
- package/dist/installer/util/errors.d.ts +46 -0
- package/dist/installer/util/exec.d.ts +41 -0
- package/dist/installer/util/fs.d.ts +153 -0
- package/dist/installer/util/git.d.ts +44 -0
- package/dist/installer/util/install-transaction.d.ts +29 -0
- package/dist/installer/util/logger.d.ts +31 -0
- package/dist/installer/util/paths.d.ts +34 -0
- package/dist/installer/util/prompts.d.ts +23 -0
- package/dist/installer/util/registry.d.ts +174 -0
- package/dist/installer/util/template.d.ts +23 -0
- package/docs/README.md +1 -0
- package/docs/agent-runtime.md +268 -0
- package/integration-contract.json +79 -7
- package/package.json +16 -1
- package/schemas/agent-runtime.schema.json +55 -0
- package/templates/runtime/provider-pipeline.md +16 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
export type PipelinePhase = 'architect' | 'developer' | 'reviewer' | 'archive' | 'ship' | 'ci';
|
|
2
|
+
export type PhaseStatus = 'pending' | 'running' | 'done' | 'blocked' | 'failed' | 'skipped';
|
|
3
|
+
export interface PipelineSpec {
|
|
4
|
+
id: string | number;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
repositoryIds?: string[];
|
|
8
|
+
acceptanceCriteria?: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface PipelineRepository {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
path: string;
|
|
14
|
+
baseSha?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface PipelineContext {
|
|
17
|
+
schemaVersion: 1;
|
|
18
|
+
runId: string;
|
|
19
|
+
backlogRoot: string;
|
|
20
|
+
backlogPath?: string;
|
|
21
|
+
artifactRoot: string;
|
|
22
|
+
artifactRepositoryId: string;
|
|
23
|
+
repositories: PipelineRepository[];
|
|
24
|
+
ownership: {
|
|
25
|
+
git: 'host' | 'core';
|
|
26
|
+
backlog: 'host' | 'core';
|
|
27
|
+
worktrees: 'host' | 'core';
|
|
28
|
+
};
|
|
29
|
+
specs: PipelineSpec[];
|
|
30
|
+
}
|
|
31
|
+
export interface VerificationCommand {
|
|
32
|
+
repositoryId: string;
|
|
33
|
+
command: string;
|
|
34
|
+
args: string[];
|
|
35
|
+
cwd?: string;
|
|
36
|
+
env?: Record<string, string>;
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
}
|
|
39
|
+
/** `unverified` admits a full request whose commands do not cover every repository
|
|
40
|
+
* (or cover none): the receipt then records which repositories ran no check. */
|
|
41
|
+
export interface VerificationRequest {
|
|
42
|
+
kind: 'full' | 'scoped';
|
|
43
|
+
commands: VerificationCommand[];
|
|
44
|
+
unverified?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface CommandReceipt {
|
|
47
|
+
repositoryId: string;
|
|
48
|
+
command: string;
|
|
49
|
+
args: string[];
|
|
50
|
+
cwd: string;
|
|
51
|
+
environmentPolicy?: 'isolated-transport-v1';
|
|
52
|
+
environmentHash: string;
|
|
53
|
+
environmentKeys: string[];
|
|
54
|
+
environmentOverrideKeys: string[];
|
|
55
|
+
environmentOverridesHash: string;
|
|
56
|
+
exitCode: number;
|
|
57
|
+
durationMs: number;
|
|
58
|
+
output: string;
|
|
59
|
+
}
|
|
60
|
+
export interface VerificationReceipt {
|
|
61
|
+
id: string;
|
|
62
|
+
kind: 'full' | 'scoped';
|
|
63
|
+
scopeHash: string;
|
|
64
|
+
candidateHash: string;
|
|
65
|
+
commands: CommandReceipt[];
|
|
66
|
+
completedAt: string;
|
|
67
|
+
valid: boolean;
|
|
68
|
+
reason?: string;
|
|
69
|
+
/** Repositories in scope that this receipt ran no command for (explicitly admitted by the request). */
|
|
70
|
+
unverifiedRepositories?: string[];
|
|
71
|
+
}
|
|
72
|
+
export interface AcceptanceCriterion {
|
|
73
|
+
specId: string;
|
|
74
|
+
criterionIndex: number;
|
|
75
|
+
requirement: string;
|
|
76
|
+
status: 'met' | 'exception' | 'blocked' | 'pending';
|
|
77
|
+
evidence: string[];
|
|
78
|
+
exception?: {
|
|
79
|
+
reason: string;
|
|
80
|
+
impact: string;
|
|
81
|
+
material: boolean;
|
|
82
|
+
acceptedBy: 'reviewer' | 'user' | 'host';
|
|
83
|
+
approvalEvidence: string;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export interface AcceptanceCheck {
|
|
87
|
+
name: string;
|
|
88
|
+
status: 'passed' | 'failed' | 'unavailable';
|
|
89
|
+
required: boolean;
|
|
90
|
+
evidence: string[];
|
|
91
|
+
/** What was measured and what remains outside the measurement. */
|
|
92
|
+
scope: string;
|
|
93
|
+
limitations: string;
|
|
94
|
+
}
|
|
95
|
+
export interface AcceptanceReport {
|
|
96
|
+
criteria: AcceptanceCriterion[];
|
|
97
|
+
checks: AcceptanceCheck[];
|
|
98
|
+
findings: string[];
|
|
99
|
+
}
|
|
100
|
+
export interface AcceptanceReceipt extends AcceptanceReport {
|
|
101
|
+
scopeHash: string;
|
|
102
|
+
candidateHash: string;
|
|
103
|
+
artifactHash: string;
|
|
104
|
+
recordedAt: string;
|
|
105
|
+
}
|
|
106
|
+
export interface PipelineCompletion {
|
|
107
|
+
implementation: 'complete' | 'incomplete';
|
|
108
|
+
validation: 'verified' | 'with-exceptions' | 'pending' | 'blocked';
|
|
109
|
+
archive: PhaseStatus;
|
|
110
|
+
delivery: 'pending-host' | 'complete' | 'pending';
|
|
111
|
+
reasons: string[];
|
|
112
|
+
}
|
|
113
|
+
interface PhaseRecord {
|
|
114
|
+
startedAt?: string;
|
|
115
|
+
durationMs?: number;
|
|
116
|
+
attempts?: number;
|
|
117
|
+
status: PhaseStatus;
|
|
118
|
+
reason?: string;
|
|
119
|
+
candidateHash?: string;
|
|
120
|
+
artifactHash?: string;
|
|
121
|
+
completedAt?: string;
|
|
122
|
+
}
|
|
123
|
+
export interface PipelineState {
|
|
124
|
+
schemaVersion: 1;
|
|
125
|
+
runId: string;
|
|
126
|
+
change: string;
|
|
127
|
+
context: PipelineContext;
|
|
128
|
+
scopeHash: string;
|
|
129
|
+
revision: number;
|
|
130
|
+
createdAt: string;
|
|
131
|
+
updatedAt: string;
|
|
132
|
+
phases: Record<PipelinePhase, PhaseRecord>;
|
|
133
|
+
verification?: VerificationReceipt;
|
|
134
|
+
acceptance?: AcceptanceReceipt;
|
|
135
|
+
archivePath?: string;
|
|
136
|
+
archiveApproval?: {
|
|
137
|
+
candidateHash: string;
|
|
138
|
+
artifactHash: string;
|
|
139
|
+
confidenceHash: string;
|
|
140
|
+
acceptanceHash: string;
|
|
141
|
+
};
|
|
142
|
+
artifactExclusions: string[];
|
|
143
|
+
preview?: {
|
|
144
|
+
baseHash: string;
|
|
145
|
+
files: PreviewFile[];
|
|
146
|
+
createdAt: string;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export interface PreviewFile {
|
|
150
|
+
repositoryId: string;
|
|
151
|
+
path: string;
|
|
152
|
+
operation: 'write' | 'delete';
|
|
153
|
+
sourcePath?: string;
|
|
154
|
+
contentHash?: string;
|
|
155
|
+
}
|
|
156
|
+
export declare function verificationEnvironment(env: NodeJS.ProcessEnv, overrides?: Record<string, string>, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
|
|
157
|
+
export declare function validatePipelineContext(input: unknown): PipelineContext;
|
|
158
|
+
export declare function pipelineStateDirectory(context: PipelineContext): string;
|
|
159
|
+
export declare function fingerprintCandidate(state: PipelineState): string;
|
|
160
|
+
export declare function initializePipeline(contextInput: unknown, change: string): PipelineState;
|
|
161
|
+
declare function inspectReceipt(state: PipelineState, env?: NodeJS.ProcessEnv): {
|
|
162
|
+
valid: boolean;
|
|
163
|
+
reasons: string[];
|
|
164
|
+
receipt?: VerificationReceipt;
|
|
165
|
+
};
|
|
166
|
+
/** The frozen requirements an acceptance report must certify, in scope order. */
|
|
167
|
+
export declare function frozenAcceptanceCriteria(contextInput: unknown): Array<Pick<AcceptanceCriterion, 'specId' | 'criterionIndex' | 'requirement'>>;
|
|
168
|
+
/** Validates a report against the frozen scope without recording it; throws the same errors `recordAcceptance` would. */
|
|
169
|
+
export declare function validateAcceptanceReport(contextInput: unknown, input: unknown): AcceptanceReport;
|
|
170
|
+
declare function inspectAcceptance(state: PipelineState): {
|
|
171
|
+
valid: boolean;
|
|
172
|
+
status: PipelineCompletion['validation'];
|
|
173
|
+
reasons: string[];
|
|
174
|
+
receipt?: AcceptanceReceipt;
|
|
175
|
+
};
|
|
176
|
+
export declare function recordAcceptance(contextInput: unknown, input: unknown): PipelineState;
|
|
177
|
+
export declare function checkArchive(contextInput: unknown): PipelineState;
|
|
178
|
+
export declare function transitionPipeline(contextInput: unknown, phase: PipelinePhase, status: PhaseStatus, reason?: string): PipelineState;
|
|
179
|
+
export declare function inspectPipeline(contextInput: unknown): {
|
|
180
|
+
schemaVersion: 1;
|
|
181
|
+
runId: string;
|
|
182
|
+
change: string;
|
|
183
|
+
context: PipelineContext;
|
|
184
|
+
stateDir: string;
|
|
185
|
+
resumePhase: PipelinePhase | null;
|
|
186
|
+
phases: PipelineState['phases'];
|
|
187
|
+
verification: ReturnType<typeof inspectReceipt>;
|
|
188
|
+
acceptance: ReturnType<typeof inspectAcceptance>;
|
|
189
|
+
completion: PipelineCompletion;
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Native executables retain structured argv; Windows script shims need cmd.
|
|
193
|
+
* This runtime is copied as one standalone module into .specrails/runtime,
|
|
194
|
+
* without installer util/exec. Keep this builtins-only equivalent local rather
|
|
195
|
+
* than importing a helper absent from installed projects. Like runCommand, it
|
|
196
|
+
* handles Windows shims; unlike a shell string it refuses ambiguous arguments.
|
|
197
|
+
*/
|
|
198
|
+
export declare function verificationInvocation(command: string, args: string[], cwd: string, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): {
|
|
199
|
+
command: string;
|
|
200
|
+
args: string[];
|
|
201
|
+
windowsVerbatimArguments?: boolean;
|
|
202
|
+
};
|
|
203
|
+
export declare function verifyPipeline(contextInput: unknown, raw: unknown, log?: (text: string) => void, signal?: AbortSignal): Promise<VerificationReceipt>;
|
|
204
|
+
/** Validate a check plan without creating state or executing commands. */
|
|
205
|
+
export declare function validateVerificationRequest(contextInput: unknown, raw: unknown): void;
|
|
206
|
+
export declare function preparePreview(contextInput: unknown, raw: unknown): PipelineState;
|
|
207
|
+
export declare function applyPreview(contextInput: unknown, verificationRequest: unknown, log?: (text: string) => void): Promise<VerificationReceipt>;
|
|
208
|
+
export declare function runPipelineCommand(flags: Record<string, string | boolean>, positionals: string[]): Promise<number>;
|
|
209
|
+
export declare function runPipelineCli(argv: string[]): Promise<number>;
|
|
210
|
+
export {};
|
|
@@ -4,15 +4,34 @@ import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, re
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
const PHASES = ['architect', 'developer', 'reviewer', 'archive', 'ship', 'ci'];
|
|
6
6
|
// Session identity belongs to the agent transport, not the checked application.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// Remove these from check subprocesses too, so ignored identity cannot become
|
|
8
|
+
// an unrecorded test input. Explicit command.env inputs remain bound overrides.
|
|
9
|
+
// Keep provider configuration, application inputs and host scope/configuration
|
|
10
|
+
// (including SPECRAILS_*) in the evidence; never exclude an entire prefix.
|
|
9
11
|
const TRANSPORT_ENV_KEYS = new Set([
|
|
10
12
|
'_', 'PWD', 'OLDPWD', 'SHLVL',
|
|
13
|
+
'AI_AGENT', 'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT', 'CLAUDE_CODE_EXECPATH', 'CLAUDE_EFFORT',
|
|
11
14
|
'CLAUDE_PID', 'CLAUDE_CODE_SESSION_ID', 'CLAUDE_CODE_MESSAGING_SOCKET',
|
|
12
15
|
'CLAUDE_CODE_MESSAGING_TOKEN', 'CLAUDE_CODE_CHILD_SESSION',
|
|
13
16
|
]);
|
|
14
17
|
function verificationEnvironmentKeys(env, overrideKeys = []) {
|
|
15
|
-
return Object.keys(env).filter((key) =>
|
|
18
|
+
return Object.keys(normalizeVerificationEnvironment(env)).filter((key) => !TRANSPORT_ENV_KEYS.has(key) && !overrideKeys.includes(key)).sort();
|
|
19
|
+
}
|
|
20
|
+
function normalizeVerificationEnvironment(env, platform = process.platform) {
|
|
21
|
+
const result = Object.create(null);
|
|
22
|
+
for (const [raw, value] of Object.entries(env)) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
continue;
|
|
25
|
+
const key = platform === 'win32' ? raw.toUpperCase() : raw;
|
|
26
|
+
if (Object.hasOwn(result, key) && result[key] !== value)
|
|
27
|
+
fail('Ambiguous verification environment key: ' + key);
|
|
28
|
+
result[key] = value;
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
export function verificationEnvironment(env, overrides = {}, platform = process.platform) {
|
|
33
|
+
const normalized = normalizeVerificationEnvironment(env, platform);
|
|
34
|
+
return { ...Object.fromEntries(Object.entries(normalized).filter(([key]) => !TRANSPORT_ENV_KEYS.has(key))), ...normalizeVerificationEnvironment(overrides, platform) };
|
|
16
35
|
}
|
|
17
36
|
const ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
18
37
|
const slug = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -255,15 +274,28 @@ function trackedFiles(repo) {
|
|
|
255
274
|
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_')));
|
|
256
275
|
env.GIT_CONFIG_NOSYSTEM = '1';
|
|
257
276
|
env.GIT_CONFIG_GLOBAL = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
277
|
+
const list = (args) => {
|
|
278
|
+
const result = spawnSync('git', ['-c', 'core.excludesFile=', '-C', repo.path, 'ls-files', '-z', ...args], { env, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, windowsHide: true });
|
|
279
|
+
if (result.error || result.status !== 0)
|
|
280
|
+
fail('Cannot fingerprint repository ' + repo.name + ': ' + (result.error?.message ?? result.stderr));
|
|
281
|
+
return result.stdout.split('\0').filter(Boolean);
|
|
282
|
+
};
|
|
283
|
+
const tracked = new Set(list(['--cached']));
|
|
284
|
+
return [...new Set([...tracked, ...list(['--others', '--exclude-standard'])])].sort().map(file => ({ file, tracked: tracked.has(file) }));
|
|
285
|
+
}
|
|
286
|
+
function untrackedAgentMemory(relative) {
|
|
287
|
+
// Native project memory can be written automatically after a role finishes,
|
|
288
|
+
// even when the role keeps explicit notes under stateDir. Exclude only known
|
|
289
|
+
// generated memory roots; tracked files and provider settings/skills still
|
|
290
|
+
// belong to the candidate. Do not hide arbitrary provider directory content.
|
|
291
|
+
return ['.claude', '.codex', '.gemini', '.kimi-code', '.specrails'].some(provider => relative === provider + '/agent-memory' || relative.startsWith(provider + '/agent-memory/'));
|
|
262
292
|
}
|
|
263
293
|
export function fingerprintCandidate(state) {
|
|
264
294
|
const entries = state.context.repositories.map((repo) => ({
|
|
265
295
|
id: repo.id, path: repo.path,
|
|
266
|
-
files: trackedFiles(repo)
|
|
296
|
+
files: trackedFiles(repo)
|
|
297
|
+
.filter(({ file, tracked }) => !excluded(state, repo, relativeUnix(file)) && (tracked || !untrackedAgentMemory(relativeUnix(file))))
|
|
298
|
+
.map(({ file }) => [relativeUnix(file), fileFingerprint(path.join(repo.path, file))]),
|
|
267
299
|
}));
|
|
268
300
|
return digest(canonical(entries));
|
|
269
301
|
}
|
|
@@ -314,7 +346,8 @@ export function initializePipeline(contextInput, change) {
|
|
|
314
346
|
});
|
|
315
347
|
}
|
|
316
348
|
function environmentHash(keys, overrides = {}, env = process.env) {
|
|
317
|
-
|
|
349
|
+
const normalized = normalizeVerificationEnvironment(env);
|
|
350
|
+
return digest(canonical(Object.fromEntries(keys.map((key) => [key, overrides[key] ?? normalized[key] ?? null]))));
|
|
318
351
|
}
|
|
319
352
|
function inspectReceipt(state, env = process.env) {
|
|
320
353
|
const receipt = state.verification;
|
|
@@ -330,9 +363,19 @@ function inspectReceipt(state, env = process.env) {
|
|
|
330
363
|
for (const command of receipt.commands) {
|
|
331
364
|
if (command.exitCode !== 0)
|
|
332
365
|
reasons.push('Command failed: ' + command.command);
|
|
366
|
+
if (command.environmentPolicy !== 'isolated-transport-v1')
|
|
367
|
+
reasons.push('Verification environment policy changed; run full verification again: ' + command.command);
|
|
333
368
|
const currentKeys = verificationEnvironmentKeys(env, command.environmentOverrideKeys ?? []);
|
|
334
|
-
if (canonical(currentKeys) !== canonical(command.environmentKeys) || command.environmentHash !== environmentHash(currentKeys, {}, env))
|
|
335
|
-
|
|
369
|
+
if (canonical(currentKeys) !== canonical(command.environmentKeys) || command.environmentHash !== environmentHash(currentKeys, {}, env)) {
|
|
370
|
+
const previousKeys = command.environmentKeys ?? [];
|
|
371
|
+
const added = currentKeys.filter(key => !previousKeys.includes(key));
|
|
372
|
+
const removed = previousKeys.filter(key => !currentKeys.includes(key));
|
|
373
|
+
// Names explain process handoff drift without retaining or revealing
|
|
374
|
+
// values. A changed aggregate hash cannot identify which value changed.
|
|
375
|
+
const summarize = (keys) => keys.slice(0, 10).map(key => JSON.stringify(key)).join(', ') + (keys.length > 10 ? ` (+${keys.length - 10} more)` : '');
|
|
376
|
+
const details = [added.length ? 'added keys: ' + summarize(added) : '', removed.length ? 'removed keys: ' + summarize(removed) : ''].filter(Boolean);
|
|
377
|
+
reasons.push('Verification environment changed: ' + command.command + ' (' + (details.join('; ') || 'recorded environment values differ') + ')');
|
|
378
|
+
}
|
|
336
379
|
}
|
|
337
380
|
return { valid: reasons.length === 0, reasons: [...new Set(reasons)], receipt };
|
|
338
381
|
}
|
|
@@ -374,6 +417,14 @@ function frozenCriteria(context) {
|
|
|
374
417
|
return context.specs.flatMap(spec => (spec.acceptanceCriteria?.length ? spec.acceptanceCriteria : [spec.description || spec.title])
|
|
375
418
|
.map((requirement, criterionIndex) => ({ specId: String(spec.id), criterionIndex, requirement })));
|
|
376
419
|
}
|
|
420
|
+
/** The frozen requirements an acceptance report must certify, in scope order. */
|
|
421
|
+
export function frozenAcceptanceCriteria(contextInput) {
|
|
422
|
+
return frozenCriteria(validatePipelineContext(contextInput));
|
|
423
|
+
}
|
|
424
|
+
/** Validates a report against the frozen scope without recording it; throws the same errors `recordAcceptance` would. */
|
|
425
|
+
export function validateAcceptanceReport(contextInput, input) {
|
|
426
|
+
return acceptanceReport(validatePipelineContext(contextInput), input);
|
|
427
|
+
}
|
|
377
428
|
function nonempty(value) { return typeof value === 'string' && value.trim().length > 0; }
|
|
378
429
|
function evidence(value) { return Array.isArray(value) && value.length > 0 && value.every(nonempty); }
|
|
379
430
|
function acceptanceReport(context, input) {
|
|
@@ -651,12 +702,14 @@ export function verificationInvocation(command, args, cwd, platform = process.pl
|
|
|
651
702
|
const quote = (value) => '"' + value + '"';
|
|
652
703
|
return { command: env.ComSpec ?? env.COMSPEC ?? 'cmd.exe', args: ['/d', '/s', '/c', '"' + [resolved, ...args].map(quote).join(' ') + '"'], windowsVerbatimArguments: true };
|
|
653
704
|
}
|
|
654
|
-
async function executeCheck(command, log) {
|
|
705
|
+
async function executeCheck(command, log, signal) {
|
|
655
706
|
const started = Date.now();
|
|
656
|
-
const
|
|
707
|
+
const overrides = normalizeVerificationEnvironment(command.env ?? {});
|
|
708
|
+
const overrideKeys = Object.keys(overrides).sort();
|
|
657
709
|
const keys = verificationEnvironmentKeys(process.env, overrideKeys);
|
|
658
710
|
const hash = environmentHash(keys);
|
|
659
|
-
const overridesHash = digest(canonical(
|
|
711
|
+
const overridesHash = digest(canonical(overrides));
|
|
712
|
+
const env = verificationEnvironment(process.env, overrides);
|
|
660
713
|
let output = '';
|
|
661
714
|
let exitCode = -1;
|
|
662
715
|
await new Promise((resolve) => {
|
|
@@ -665,10 +718,29 @@ async function executeCheck(command, log) {
|
|
|
665
718
|
let done = false;
|
|
666
719
|
const finish = (code) => { if (done)
|
|
667
720
|
return; done = true; exitCode = code; if (timer)
|
|
668
|
-
clearTimeout(timer); resolve(); };
|
|
721
|
+
clearTimeout(timer); signal?.removeEventListener('abort', abort); resolve(); };
|
|
722
|
+
const stop = (reason) => {
|
|
723
|
+
output += '\n' + reason;
|
|
724
|
+
if (child?.pid) {
|
|
725
|
+
if (process.platform === 'win32')
|
|
726
|
+
spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true });
|
|
727
|
+
else {
|
|
728
|
+
try {
|
|
729
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
730
|
+
}
|
|
731
|
+
catch { /* already exited */ }
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
finish(-1);
|
|
735
|
+
};
|
|
736
|
+
const abort = () => stop('Verification command cancelled');
|
|
737
|
+
if (signal?.aborted) {
|
|
738
|
+
abort();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
669
741
|
try {
|
|
670
|
-
const invocation = verificationInvocation(command.command, command.args, command.cwd);
|
|
671
|
-
child = spawn(invocation.command, invocation.args, { windowsVerbatimArguments: invocation.windowsVerbatimArguments, cwd: command.cwd, env
|
|
742
|
+
const invocation = verificationInvocation(command.command, command.args, command.cwd, process.platform, env);
|
|
743
|
+
child = spawn(invocation.command, invocation.args, { windowsVerbatimArguments: invocation.windowsVerbatimArguments, cwd: command.cwd, env, shell: false, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
672
744
|
}
|
|
673
745
|
catch (error) {
|
|
674
746
|
output = String(error);
|
|
@@ -680,31 +752,19 @@ async function executeCheck(command, log) {
|
|
|
680
752
|
child.stderr?.on('data', receive);
|
|
681
753
|
child.on('error', (error) => { output += error.message; finish(-1); });
|
|
682
754
|
child.on('close', (code) => finish(code ?? -1));
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (child.pid) {
|
|
686
|
-
if (process.platform === 'win32')
|
|
687
|
-
spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true });
|
|
688
|
-
else {
|
|
689
|
-
try {
|
|
690
|
-
process.kill(-child.pid, 'SIGKILL');
|
|
691
|
-
}
|
|
692
|
-
catch { /* already exited */ }
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
finish(-1);
|
|
696
|
-
}, command.timeoutMs);
|
|
755
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
756
|
+
timer = setTimeout(() => stop('Verification command timed out'), command.timeoutMs);
|
|
697
757
|
});
|
|
698
|
-
return { repositoryId: command.repositoryId, command: command.command, args: command.args, cwd: command.cwd, environmentHash: hash, environmentKeys: keys, environmentOverrideKeys: overrideKeys, environmentOverridesHash: overridesHash, exitCode, durationMs: Date.now() - started, output };
|
|
758
|
+
return { repositoryId: command.repositoryId, command: command.command, args: command.args, cwd: command.cwd, environmentPolicy: 'isolated-transport-v1', environmentHash: hash, environmentKeys: keys, environmentOverrideKeys: overrideKeys, environmentOverridesHash: overridesHash, exitCode, durationMs: Date.now() - started, output };
|
|
699
759
|
}
|
|
700
|
-
export async function verifyPipeline(contextInput, raw, log = () => { }) {
|
|
760
|
+
export async function verifyPipeline(contextInput, raw, log = () => { }, signal) {
|
|
701
761
|
const context = validatePipelineContext(contextInput);
|
|
702
|
-
const { request, commands } = verificationPlan(context, raw);
|
|
762
|
+
const { request, commands, unverifiedRepositories } = verificationPlan(context, raw);
|
|
703
763
|
const state = readState(context);
|
|
704
764
|
const candidateHash = fingerprintCandidate(state);
|
|
705
765
|
const results = [];
|
|
706
766
|
for (const command of commands) {
|
|
707
|
-
results.push(await executeCheck(command, log));
|
|
767
|
+
results.push(await executeCheck(command, log, signal));
|
|
708
768
|
if (results[results.length - 1].exitCode !== 0)
|
|
709
769
|
break;
|
|
710
770
|
}
|
|
@@ -715,6 +775,7 @@ export async function verifyPipeline(contextInput, raw, log = () => { }) {
|
|
|
715
775
|
id: randomUUID(), kind: request.kind, scopeHash: state.scopeHash, candidateHash, commands: results,
|
|
716
776
|
completedAt: new Date().toISOString(), valid: !changed && results.length === commands.length && results.every((result) => result.exitCode === 0),
|
|
717
777
|
...(changed ? { reason: 'Candidate changed during verification' } : results.some((result) => result.exitCode !== 0) ? { reason: 'A verification command failed' } : {}),
|
|
778
|
+
...(unverifiedRepositories.length ? { unverifiedRepositories } : {}),
|
|
718
779
|
};
|
|
719
780
|
atomicJson(safeChild(pipelineStateDirectory(context), 'receipts/' + receipt.id + '.json'), receipt);
|
|
720
781
|
if (receipt.kind === 'full' || !receipt.valid || !current.verification)
|
|
@@ -725,12 +786,20 @@ export async function verifyPipeline(contextInput, raw, log = () => { }) {
|
|
|
725
786
|
}
|
|
726
787
|
function verificationPlan(context, raw) {
|
|
727
788
|
const request = object(raw);
|
|
728
|
-
|
|
789
|
+
const unverified = request.unverified === true;
|
|
790
|
+
if (request.unverified !== undefined && typeof request.unverified !== 'boolean')
|
|
791
|
+
fail('Verification unverified flag must be boolean');
|
|
792
|
+
if (!['full', 'scoped'].includes(String(request.kind)) || !Array.isArray(request.commands) || (request.commands.length === 0 && !unverified) || request.commands.length > 100)
|
|
729
793
|
fail('Verification requires bounded structured commands');
|
|
730
794
|
const commands = request.commands.map((command) => validateCommand(context, command));
|
|
731
|
-
|
|
795
|
+
const unverifiedRepositories = context.repositories.filter((repo) => !commands.some((command) => command.repositoryId === repo.id)).map((repo) => repo.id);
|
|
796
|
+
if (request.kind === 'full' && !unverified && unverifiedRepositories.length)
|
|
732
797
|
fail('Full verification must cover every selected repository');
|
|
733
|
-
return { request, commands };
|
|
798
|
+
return { request, commands, unverifiedRepositories: request.kind === 'full' && unverified ? unverifiedRepositories : [] };
|
|
799
|
+
}
|
|
800
|
+
/** Validate a check plan without creating state or executing commands. */
|
|
801
|
+
export function validateVerificationRequest(contextInput, raw) {
|
|
802
|
+
verificationPlan(validatePipelineContext(contextInput), raw);
|
|
734
803
|
}
|
|
735
804
|
export function preparePreview(contextInput, raw) {
|
|
736
805
|
const context = validatePipelineContext(contextInput);
|