specrails-core 6.4.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/capabilities.js +22 -5
- package/dist/agent-runtime/cli-executor.js +8 -3
- package/dist/agent-runtime/cli.d.ts +4 -4
- package/dist/agent-runtime/cli.js +50 -3
- package/dist/agent-runtime/compact/environment.d.ts +50 -4
- package/dist/agent-runtime/compact/environment.js +183 -6
- 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.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +64 -22
- package/dist/agent-runtime/engine/pieces/shared.d.ts +2 -2
- package/dist/agent-runtime/engine/pieces/verify.js +46 -13
- 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/application/active-session.d.ts +176 -0
- package/dist/agent-runtime/session/application/active-session.js +701 -0
- package/dist/agent-runtime/session/application/delegation.d.ts +73 -0
- package/dist/agent-runtime/session/application/delegation.js +171 -0
- package/dist/agent-runtime/session/application/output-buffer.d.ts +43 -0
- package/dist/agent-runtime/session/application/output-buffer.js +65 -0
- package/dist/agent-runtime/session/application/session-service.d.ts +121 -0
- package/dist/agent-runtime/session/application/session-service.js +256 -0
- package/dist/agent-runtime/session/application/timers.d.ts +15 -0
- package/dist/agent-runtime/session/application/timers.js +35 -0
- package/dist/agent-runtime/session/domain/descriptor.d.ts +6 -0
- package/dist/agent-runtime/session/domain/descriptor.js +32 -0
- package/dist/agent-runtime/session/domain/errors.d.ts +9 -0
- package/dist/agent-runtime/session/domain/errors.js +16 -0
- package/dist/agent-runtime/session/domain/events.d.ts +147 -0
- package/dist/agent-runtime/session/domain/events.js +24 -0
- package/dist/agent-runtime/session/domain/fingerprint.d.ts +2 -0
- package/dist/agent-runtime/session/domain/fingerprint.js +10 -0
- package/dist/agent-runtime/session/domain/interruption.d.ts +13 -0
- package/dist/agent-runtime/session/domain/interruption.js +39 -0
- package/dist/agent-runtime/session/domain/policy.d.ts +42 -0
- package/dist/agent-runtime/session/domain/policy.js +104 -0
- package/dist/agent-runtime/session/domain/snapshot.d.ts +100 -0
- package/dist/agent-runtime/session/domain/snapshot.js +234 -0
- package/dist/agent-runtime/session/domain/state-machines.d.ts +21 -0
- package/dist/agent-runtime/session/domain/state-machines.js +41 -0
- package/dist/agent-runtime/session/domain/types.d.ts +130 -0
- package/dist/agent-runtime/session/domain/types.js +15 -0
- package/dist/agent-runtime/session/domain/usage.d.ts +54 -0
- package/dist/agent-runtime/session/domain/usage.js +86 -0
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/argv.js +67 -0
- package/dist/agent-runtime/session/drivers/claude/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/driver.js +134 -0
- package/dist/agent-runtime/session/drivers/claude/translator.d.ts +40 -0
- package/dist/agent-runtime/session/drivers/claude/translator.js +247 -0
- package/dist/agent-runtime/session/drivers/codex/argv.d.ts +12 -0
- package/dist/agent-runtime/session/drivers/codex/argv.js +76 -0
- package/dist/agent-runtime/session/drivers/codex/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/codex/driver.js +193 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.d.ts +28 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.js +81 -0
- package/dist/agent-runtime/session/drivers/codex/translator.d.ts +53 -0
- package/dist/agent-runtime/session/drivers/codex/translator.js +267 -0
- package/dist/agent-runtime/session/drivers/common/process.d.ts +37 -0
- package/dist/agent-runtime/session/drivers/common/process.js +116 -0
- package/dist/agent-runtime/session/drivers/executor/driver.d.ts +22 -0
- package/dist/agent-runtime/session/drivers/executor/driver.js +163 -0
- package/dist/agent-runtime/session/drivers/registry.d.ts +32 -0
- package/dist/agent-runtime/session/drivers/registry.js +38 -0
- package/dist/agent-runtime/session/host/protocol.d.ts +18 -0
- package/dist/agent-runtime/session/host/protocol.js +88 -0
- package/dist/agent-runtime/session/host/server.d.ts +57 -0
- package/dist/agent-runtime/session/host/server.js +229 -0
- package/dist/agent-runtime/session/index.d.ts +45 -0
- package/dist/agent-runtime/session/index.js +74 -0
- package/dist/agent-runtime/session/journal/schema.d.ts +7 -0
- package/dist/agent-runtime/session/journal/schema.js +44 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.d.ts +70 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.js +279 -0
- package/dist/agent-runtime/session/ports.d.ts +192 -0
- package/dist/agent-runtime/session/ports.js +1 -0
- package/dist/agent-runtime/session/testing/driver-conformance.d.ts +21 -0
- package/dist/agent-runtime/session/testing/driver-conformance.js +97 -0
- package/dist/agent-runtime/session/testing/fake-clock.d.ts +20 -0
- package/dist/agent-runtime/session/testing/fake-clock.js +34 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.d.ts +41 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.js +172 -0
- package/dist/agent-runtime/session/testing/index.d.ts +9 -0
- package/dist/agent-runtime/session/testing/index.js +9 -0
- package/dist/agent-runtime/session/testing/memory-journal.d.ts +27 -0
- package/dist/agent-runtime/session/testing/memory-journal.js +70 -0
- package/dist/agent-runtime/session/testing/scripted-driver.d.ts +30 -0
- package/dist/agent-runtime/session/testing/scripted-driver.js +77 -0
- package/dist/agent-runtime/verification-proposals.d.ts +9 -0
- package/dist/agent-runtime/verification-proposals.js +69 -0
- 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/installer/util/registry.js +3 -3
- package/dist/pipeline/pipeline-state.d.ts +42 -11
- package/dist/pipeline/pipeline-state.js +47 -8
- package/dist/shared/specrails-home.d.ts +21 -0
- package/dist/shared/specrails-home.js +36 -0
- package/integration-contract.json +42 -1
- package/package.json +23 -11
- package/schemas/agent-runtime.schema.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ProcessSpawner, ProcessSpec } from '../drivers/common/process.js';
|
|
2
|
+
/** One recorded transcript row (see the spike harness in the OpenSpec change). */
|
|
3
|
+
export interface FixtureRow {
|
|
4
|
+
t: number;
|
|
5
|
+
dir: 'in' | 'out' | 'ctl';
|
|
6
|
+
json?: Record<string, unknown> | null;
|
|
7
|
+
raw?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function loadFixture(file: string): FixtureRow[];
|
|
10
|
+
export interface ReplaySession {
|
|
11
|
+
readonly spec: ProcessSpec;
|
|
12
|
+
/** Lines the driver wrote to stdin, parsed. */
|
|
13
|
+
readonly written: Array<Record<string, unknown>>;
|
|
14
|
+
readonly inputEnded: boolean;
|
|
15
|
+
readonly terminated: boolean;
|
|
16
|
+
/** Resolves when the replay is blocked waiting for the driver, or finished. */
|
|
17
|
+
idle(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Replays a recorded provider transcript as a ProcessSpawner:
|
|
21
|
+
* - `out` rows are emitted in order (time compressed);
|
|
22
|
+
* - before passing an `in` row, replay waits until the driver wrote a line,
|
|
23
|
+
* then maps recorded ids/uuids to the ones the driver used;
|
|
24
|
+
* - a `ctl kill` row waits for `terminate()` (output after it is what the
|
|
25
|
+
* provider printed while stopping); `closeStdin` waits for `endInput()`;
|
|
26
|
+
* - after the last row the process stays alive until terminated, like a
|
|
27
|
+
* resident provider, unless the transcript recorded a clean exit.
|
|
28
|
+
*/
|
|
29
|
+
export declare class FixtureReplayer {
|
|
30
|
+
private readonly rows;
|
|
31
|
+
private readonly options;
|
|
32
|
+
readonly sessions: ReplaySession[];
|
|
33
|
+
constructor(rows: FixtureRow[], options?: {
|
|
34
|
+
exitAtEnd?: boolean;
|
|
35
|
+
});
|
|
36
|
+
static fromFile(file: string, options?: {
|
|
37
|
+
exitAtEnd?: boolean;
|
|
38
|
+
}): FixtureReplayer;
|
|
39
|
+
get last(): ReplaySession;
|
|
40
|
+
readonly spawner: ProcessSpawner;
|
|
41
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
export function loadFixture(file) {
|
|
3
|
+
return readFileSync(file, 'utf8').split('\n').filter((line) => line.trim()).map((line) => JSON.parse(line));
|
|
4
|
+
}
|
|
5
|
+
/** Fields whose recorded values are replaced by the values the driver actually wrote. */
|
|
6
|
+
const REMAPPED = ['id', 'uuid', 'command_uuid'];
|
|
7
|
+
/**
|
|
8
|
+
* Replays a recorded provider transcript as a ProcessSpawner:
|
|
9
|
+
* - `out` rows are emitted in order (time compressed);
|
|
10
|
+
* - before passing an `in` row, replay waits until the driver wrote a line,
|
|
11
|
+
* then maps recorded ids/uuids to the ones the driver used;
|
|
12
|
+
* - a `ctl kill` row waits for `terminate()` (output after it is what the
|
|
13
|
+
* provider printed while stopping); `closeStdin` waits for `endInput()`;
|
|
14
|
+
* - after the last row the process stays alive until terminated, like a
|
|
15
|
+
* resident provider, unless the transcript recorded a clean exit.
|
|
16
|
+
*/
|
|
17
|
+
export class FixtureReplayer {
|
|
18
|
+
rows;
|
|
19
|
+
options;
|
|
20
|
+
sessions = [];
|
|
21
|
+
constructor(rows, options = {}) {
|
|
22
|
+
this.rows = rows;
|
|
23
|
+
this.options = options;
|
|
24
|
+
}
|
|
25
|
+
static fromFile(file, options) {
|
|
26
|
+
return new FixtureReplayer(loadFixture(file), options);
|
|
27
|
+
}
|
|
28
|
+
get last() {
|
|
29
|
+
const session = this.sessions.at(-1);
|
|
30
|
+
if (!session)
|
|
31
|
+
throw new Error('fixture replayer: nothing spawned');
|
|
32
|
+
return session;
|
|
33
|
+
}
|
|
34
|
+
spawner = (spec, handlers) => {
|
|
35
|
+
const rows = this.rows;
|
|
36
|
+
const ids = new Map();
|
|
37
|
+
const written = [];
|
|
38
|
+
let inputsSeen = 0;
|
|
39
|
+
let index = 0;
|
|
40
|
+
let inputOpen = true;
|
|
41
|
+
let terminated = false;
|
|
42
|
+
let exit = null;
|
|
43
|
+
let resolveExit;
|
|
44
|
+
const exited = new Promise((resolve) => { resolveExit = resolve; });
|
|
45
|
+
let idleWaiters = [];
|
|
46
|
+
let blocked = false;
|
|
47
|
+
const exitAtEnd = this.options.exitAtEnd ?? false;
|
|
48
|
+
const remap = (value) => {
|
|
49
|
+
if (Array.isArray(value))
|
|
50
|
+
return value.map(remap);
|
|
51
|
+
if (value && typeof value === 'object') {
|
|
52
|
+
return Object.fromEntries(Object.entries(value).map(([key, inner]) => [key,
|
|
53
|
+
REMAPPED.includes(key) && ids.has(JSON.stringify(inner)) ? ids.get(JSON.stringify(inner)) : remap(inner)]));
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
const finish = (result) => {
|
|
58
|
+
if (exit)
|
|
59
|
+
return;
|
|
60
|
+
exit = result;
|
|
61
|
+
blocked = true;
|
|
62
|
+
resolveExit(result);
|
|
63
|
+
flushIdle();
|
|
64
|
+
};
|
|
65
|
+
const flushIdle = () => { const waiters = idleWaiters; idleWaiters = []; for (const waiter of waiters)
|
|
66
|
+
waiter(); };
|
|
67
|
+
const step = () => {
|
|
68
|
+
if (exit)
|
|
69
|
+
return;
|
|
70
|
+
blocked = false;
|
|
71
|
+
while (index < rows.length) {
|
|
72
|
+
const row = rows[index];
|
|
73
|
+
if (row.dir === 'in') {
|
|
74
|
+
if (written.length <= inputsSeen) {
|
|
75
|
+
blocked = true;
|
|
76
|
+
flushIdle();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const actual = written[inputsSeen];
|
|
80
|
+
for (const key of REMAPPED) {
|
|
81
|
+
const recorded = row.json?.[key];
|
|
82
|
+
if (recorded !== undefined && actual[key] !== undefined)
|
|
83
|
+
ids.set(JSON.stringify(recorded), actual[key]);
|
|
84
|
+
}
|
|
85
|
+
inputsSeen += 1;
|
|
86
|
+
index += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (row.dir === 'ctl') {
|
|
90
|
+
const control = row.json ?? {};
|
|
91
|
+
if ('kill' in control) {
|
|
92
|
+
if (!terminated) {
|
|
93
|
+
blocked = true;
|
|
94
|
+
flushIdle();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
index += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if ('closeStdin' in control) {
|
|
101
|
+
if (inputOpen) {
|
|
102
|
+
blocked = true;
|
|
103
|
+
flushIdle();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if ('end' in control) {
|
|
110
|
+
const match = /exit code=(\S+) sig=(\S+)/.exec(String(control.end));
|
|
111
|
+
const code = match && match[1] !== 'null' ? Number(match[1]) : null;
|
|
112
|
+
const signal = match && match[2] !== 'null' ? match[2] : null;
|
|
113
|
+
// A harness timeout means the real process was still alive: stay resident.
|
|
114
|
+
if (terminated || exitAtEnd || (signal !== 'SIGKILL' && signal !== 'SIGTERM')) {
|
|
115
|
+
finish({ exitCode: code, signal });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
index += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
index += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
index += 1;
|
|
125
|
+
if (row.json)
|
|
126
|
+
handlers.onLine(JSON.stringify(remap(row.json)));
|
|
127
|
+
else if (row.raw)
|
|
128
|
+
handlers.onLine(row.raw);
|
|
129
|
+
setImmediate(step);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (terminated || exitAtEnd)
|
|
133
|
+
finish({ exitCode: terminated ? null : 0, signal: terminated ? 'SIGTERM' : null });
|
|
134
|
+
else {
|
|
135
|
+
blocked = true;
|
|
136
|
+
flushIdle();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const session = {
|
|
140
|
+
spec,
|
|
141
|
+
written,
|
|
142
|
+
get inputEnded() { return !inputOpen; },
|
|
143
|
+
get terminated() { return terminated; },
|
|
144
|
+
idle: () => (blocked || exit ? Promise.resolve() : new Promise((resolve) => { idleWaiters.push(resolve); })),
|
|
145
|
+
};
|
|
146
|
+
this.sessions.push(session);
|
|
147
|
+
setImmediate(step);
|
|
148
|
+
return {
|
|
149
|
+
pid: 4242,
|
|
150
|
+
exited,
|
|
151
|
+
write(line) {
|
|
152
|
+
if (!inputOpen || exit)
|
|
153
|
+
return false;
|
|
154
|
+
written.push(JSON.parse(line));
|
|
155
|
+
if (blocked)
|
|
156
|
+
setImmediate(step);
|
|
157
|
+
return true;
|
|
158
|
+
},
|
|
159
|
+
endInput() {
|
|
160
|
+
inputOpen = false;
|
|
161
|
+
if (blocked)
|
|
162
|
+
setImmediate(step);
|
|
163
|
+
},
|
|
164
|
+
terminate() {
|
|
165
|
+
terminated = true;
|
|
166
|
+
if (blocked)
|
|
167
|
+
setImmediate(step);
|
|
168
|
+
return exited;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test kit for hosts and driver authors (package export `./agent-runtime/session/testing`).
|
|
3
|
+
* Framework-free: usable from any test runner.
|
|
4
|
+
*/
|
|
5
|
+
export { DRIVER_CONFORMANCE, type ConformanceCheck, type ConformanceHarness } from './driver-conformance.js';
|
|
6
|
+
export { FixtureReplayer, loadFixture, type FixtureRow, type ReplaySession } from './fixture-replayer.js';
|
|
7
|
+
export { MemoryJournal } from './memory-journal.js';
|
|
8
|
+
export { FakeClock, SequentialIds } from './fake-clock.js';
|
|
9
|
+
export { ScriptedDriverFactory, ScriptedDriverSession, catalogOf, descriptor } from './scripted-driver.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test kit for hosts and driver authors (package export `./agent-runtime/session/testing`).
|
|
3
|
+
* Framework-free: usable from any test runner.
|
|
4
|
+
*/
|
|
5
|
+
export { DRIVER_CONFORMANCE } from './driver-conformance.js';
|
|
6
|
+
export { FixtureReplayer, loadFixture } from './fixture-replayer.js';
|
|
7
|
+
export { MemoryJournal } from './memory-journal.js';
|
|
8
|
+
export { FakeClock, SequentialIds } from './fake-clock.js';
|
|
9
|
+
export { ScriptedDriverFactory, ScriptedDriverSession, catalogOf, descriptor } from './scripted-driver.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SessionEvent, SessionEventEnvelope } from '../domain/events.js';
|
|
2
|
+
import { type SessionSnapshot } from '../domain/snapshot.js';
|
|
3
|
+
import type { UsageBaseline } from '../domain/usage.js';
|
|
4
|
+
import type { AppendOptions, SessionJournal, SessionRecord, SessionSummary } from '../ports.js';
|
|
5
|
+
/**
|
|
6
|
+
* In-memory SessionJournal with the same contract as the SQLite adapter:
|
|
7
|
+
* gap-free sequences, atomic appends, baselines stored with their events.
|
|
8
|
+
* `failNextAppend` simulates a failed transaction (nothing becomes visible).
|
|
9
|
+
*/
|
|
10
|
+
export declare class MemoryJournal implements SessionJournal {
|
|
11
|
+
private readonly sessions;
|
|
12
|
+
private readonly baselines;
|
|
13
|
+
failNextAppend: Error | null;
|
|
14
|
+
createSession(record: SessionRecord): void;
|
|
15
|
+
getSession(sessionId: string): SessionRecord | null;
|
|
16
|
+
append(sessionId: string, events: SessionEvent[], options?: AppendOptions): SessionEventEnvelope[];
|
|
17
|
+
read(sessionId: string, afterSeq: number, limit: number): {
|
|
18
|
+
events: SessionEventEnvelope[];
|
|
19
|
+
hasMore: boolean;
|
|
20
|
+
};
|
|
21
|
+
snapshot(sessionId: string): SessionSnapshot | null;
|
|
22
|
+
list(filter: 'open' | 'closed' | 'all'): SessionSummary[];
|
|
23
|
+
baseline(providerRef: string): UsageBaseline | null;
|
|
24
|
+
/** Test helper: every committed event of a session. */
|
|
25
|
+
events(sessionId: string): SessionEventEnvelope[];
|
|
26
|
+
private require;
|
|
27
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { emptySnapshot, foldEvents } from '../domain/snapshot.js';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory SessionJournal with the same contract as the SQLite adapter:
|
|
4
|
+
* gap-free sequences, atomic appends, baselines stored with their events.
|
|
5
|
+
* `failNextAppend` simulates a failed transaction (nothing becomes visible).
|
|
6
|
+
*/
|
|
7
|
+
export class MemoryJournal {
|
|
8
|
+
sessions = new Map();
|
|
9
|
+
baselines = new Map();
|
|
10
|
+
failNextAppend = null;
|
|
11
|
+
createSession(record) {
|
|
12
|
+
if (this.sessions.has(record.sessionId))
|
|
13
|
+
throw new Error(`Session ${record.sessionId} exists`);
|
|
14
|
+
this.sessions.set(record.sessionId, { record: { ...record }, events: [], snapshot: emptySnapshot(record.sessionId) });
|
|
15
|
+
}
|
|
16
|
+
getSession(sessionId) {
|
|
17
|
+
return this.sessions.get(sessionId)?.record ?? null;
|
|
18
|
+
}
|
|
19
|
+
append(sessionId, events, options) {
|
|
20
|
+
const entry = this.require(sessionId);
|
|
21
|
+
if (this.failNextAppend) {
|
|
22
|
+
const error = this.failNextAppend;
|
|
23
|
+
this.failNextAppend = null;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
const envelopes = events.map((event, index) => ({ sessionId, seq: entry.snapshot.lastSeq + index + 1, event: structuredClone(event) }));
|
|
27
|
+
const snapshot = foldEvents(entry.snapshot, envelopes);
|
|
28
|
+
entry.events.push(...envelopes);
|
|
29
|
+
entry.snapshot = snapshot;
|
|
30
|
+
if (options?.baseline)
|
|
31
|
+
this.baselines.set(options.baseline.providerRef, { ...options.baseline.value });
|
|
32
|
+
return envelopes.map((envelope) => structuredClone(envelope));
|
|
33
|
+
}
|
|
34
|
+
read(sessionId, afterSeq, limit) {
|
|
35
|
+
const all = this.require(sessionId).events.filter((envelope) => envelope.seq > afterSeq);
|
|
36
|
+
return { events: structuredClone(all.slice(0, limit)), hasMore: all.length > limit };
|
|
37
|
+
}
|
|
38
|
+
snapshot(sessionId) {
|
|
39
|
+
const entry = this.sessions.get(sessionId);
|
|
40
|
+
return entry ? structuredClone(entry.snapshot) : null;
|
|
41
|
+
}
|
|
42
|
+
list(filter) {
|
|
43
|
+
return [...this.sessions.values()]
|
|
44
|
+
.filter(({ snapshot }) => filter === 'all' || snapshot.status === filter)
|
|
45
|
+
.map(({ record, snapshot }) => ({
|
|
46
|
+
sessionId: record.sessionId,
|
|
47
|
+
driver: record.driver,
|
|
48
|
+
model: snapshot.model,
|
|
49
|
+
status: snapshot.status,
|
|
50
|
+
phase: snapshot.phase,
|
|
51
|
+
lastSeq: snapshot.lastSeq,
|
|
52
|
+
createdAt: record.createdAt,
|
|
53
|
+
metadata: { ...record.metadata },
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
baseline(providerRef) {
|
|
57
|
+
const value = this.baselines.get(providerRef);
|
|
58
|
+
return value ? { ...value } : null;
|
|
59
|
+
}
|
|
60
|
+
/** Test helper: every committed event of a session. */
|
|
61
|
+
events(sessionId) {
|
|
62
|
+
return structuredClone(this.require(sessionId).events);
|
|
63
|
+
}
|
|
64
|
+
require(sessionId) {
|
|
65
|
+
const entry = this.sessions.get(sessionId);
|
|
66
|
+
if (!entry)
|
|
67
|
+
throw new Error(`Unknown session ${sessionId}`);
|
|
68
|
+
return entry;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DriverCapabilities, DriverDescriptor } from '../domain/types.js';
|
|
2
|
+
import type { CloseReason, DriverCatalog, DriverEvent, DriverEventSink, DriverFactory, DriverInput, DriverOpenSpec, DriverSession } from '../ports.js';
|
|
3
|
+
export declare function descriptor(id: string, capabilities?: Partial<DriverCapabilities>): DriverDescriptor;
|
|
4
|
+
/** One opened provider process, driven by the test through `emit`. */
|
|
5
|
+
export declare class ScriptedDriverSession implements DriverSession {
|
|
6
|
+
readonly spec: DriverOpenSpec;
|
|
7
|
+
private readonly sink;
|
|
8
|
+
readonly sent: DriverInput[];
|
|
9
|
+
readonly calls: string[];
|
|
10
|
+
closed: CloseReason | null;
|
|
11
|
+
/** What `stopSubagents` resolves with. */
|
|
12
|
+
stopResult: string[] | 'process';
|
|
13
|
+
/** Events emitted while closing (e.g. Claude's stopped/killed notifications on SIGTERM). */
|
|
14
|
+
onClose: DriverEvent[];
|
|
15
|
+
constructor(spec: DriverOpenSpec, sink: DriverEventSink);
|
|
16
|
+
emit(...events: DriverEvent[]): void;
|
|
17
|
+
send(input: DriverInput): Promise<void>;
|
|
18
|
+
interrupt(): Promise<void>;
|
|
19
|
+
stopSubagents(ids?: string[]): Promise<string[] | 'process'>;
|
|
20
|
+
close(reason: CloseReason): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export declare class ScriptedDriverFactory implements DriverFactory {
|
|
23
|
+
readonly descriptor: DriverDescriptor;
|
|
24
|
+
readonly sessions: ScriptedDriverSession[];
|
|
25
|
+
openError: Error | null;
|
|
26
|
+
constructor(descriptor: DriverDescriptor);
|
|
27
|
+
open(spec: DriverOpenSpec, sink: DriverEventSink): Promise<DriverSession>;
|
|
28
|
+
get last(): ScriptedDriverSession;
|
|
29
|
+
}
|
|
30
|
+
export declare function catalogOf(...factories: DriverFactory[]): DriverCatalog;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export function descriptor(id, capabilities = {}) {
|
|
2
|
+
return {
|
|
3
|
+
id,
|
|
4
|
+
displayName: id,
|
|
5
|
+
capabilities: {
|
|
6
|
+
resident: true,
|
|
7
|
+
nativeInputQueue: true,
|
|
8
|
+
subagents: 'supported',
|
|
9
|
+
subagentDisable: true,
|
|
10
|
+
subagentModel: true,
|
|
11
|
+
subagentEffort: true,
|
|
12
|
+
autonomousContinuation: true,
|
|
13
|
+
steer: true,
|
|
14
|
+
toolFiltering: true,
|
|
15
|
+
usage: { costUsd: 'session-cumulative', tokens: 'per-turn' },
|
|
16
|
+
...capabilities,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** One opened provider process, driven by the test through `emit`. */
|
|
21
|
+
export class ScriptedDriverSession {
|
|
22
|
+
spec;
|
|
23
|
+
sink;
|
|
24
|
+
sent = [];
|
|
25
|
+
calls = [];
|
|
26
|
+
closed = null;
|
|
27
|
+
/** What `stopSubagents` resolves with. */
|
|
28
|
+
stopResult = 'process';
|
|
29
|
+
/** Events emitted while closing (e.g. Claude's stopped/killed notifications on SIGTERM). */
|
|
30
|
+
onClose = [];
|
|
31
|
+
constructor(spec, sink) {
|
|
32
|
+
this.spec = spec;
|
|
33
|
+
this.sink = sink;
|
|
34
|
+
}
|
|
35
|
+
emit(...events) {
|
|
36
|
+
if (this.closed)
|
|
37
|
+
throw new Error('scripted driver: emit after close');
|
|
38
|
+
for (const event of events)
|
|
39
|
+
this.sink(event);
|
|
40
|
+
}
|
|
41
|
+
async send(input) { this.calls.push(`send:${input.inputId}`); this.sent.push(input); }
|
|
42
|
+
async interrupt() { this.calls.push('interrupt'); }
|
|
43
|
+
async stopSubagents(ids) { this.calls.push(`stopSubagents:${ids?.join(',') ?? '*'}`); return this.stopResult; }
|
|
44
|
+
async close(reason) {
|
|
45
|
+
if (this.closed)
|
|
46
|
+
return;
|
|
47
|
+
this.calls.push(`close:${reason}`);
|
|
48
|
+
for (const event of this.onClose)
|
|
49
|
+
this.sink(event);
|
|
50
|
+
this.closed = reason;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export class ScriptedDriverFactory {
|
|
54
|
+
descriptor;
|
|
55
|
+
sessions = [];
|
|
56
|
+
openError = null;
|
|
57
|
+
constructor(descriptor) {
|
|
58
|
+
this.descriptor = descriptor;
|
|
59
|
+
}
|
|
60
|
+
async open(spec, sink) {
|
|
61
|
+
if (this.openError)
|
|
62
|
+
throw this.openError;
|
|
63
|
+
const session = new ScriptedDriverSession(spec, sink);
|
|
64
|
+
this.sessions.push(session);
|
|
65
|
+
return session;
|
|
66
|
+
}
|
|
67
|
+
get last() {
|
|
68
|
+
const session = this.sessions.at(-1);
|
|
69
|
+
if (!session)
|
|
70
|
+
throw new Error('scripted driver: no session opened');
|
|
71
|
+
return session;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function catalogOf(...factories) {
|
|
75
|
+
const map = new Map(factories.map((factory) => [factory.descriptor.id, factory]));
|
|
76
|
+
return { get: (id) => map.get(id), descriptors: () => factories.map((factory) => factory.descriptor) };
|
|
77
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PipelineContext, VerificationCommand } from '../pipeline/pipeline-state.js';
|
|
2
|
+
/** The package script a command runs (`npm test`, `npm run x`, `pnpm [run] x`, `yarn [run] x`), when it runs exactly one with no extra arguments. */
|
|
3
|
+
export declare function packageScriptOf(command: Pick<VerificationCommand, 'command' | 'args'>): string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* The proposals to append to `planned` (already scope-defaulted). `normalize`
|
|
6
|
+
* applies the caller's scope default; proposals are expected to be
|
|
7
|
+
* structurally valid.
|
|
8
|
+
*/
|
|
9
|
+
export declare function admitVerificationProposals(context: PipelineContext, planned: readonly VerificationCommand[], proposals: readonly VerificationCommand[], normalize: (command: VerificationCommand) => VerificationCommand): VerificationCommand[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Which agent-proposed verification commands the host adds to its plan.
|
|
2
|
+
//
|
|
3
|
+
// A repository without configured checks takes the proposals as its checks
|
|
4
|
+
// (validated like any configured command). A repository with configured
|
|
5
|
+
// checks keeps them and may gain only runs of a package script the
|
|
6
|
+
// repository itself declares, such as `npm run test:e2e`: a change whose
|
|
7
|
+
// acceptance needs browser or integration tests then gets host evidence for
|
|
8
|
+
// them, without the agent introducing arbitrary commands next to the
|
|
9
|
+
// configured ones.
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
/** The package script a command runs (`npm test`, `npm run x`, `pnpm [run] x`, `yarn [run] x`), when it runs exactly one with no extra arguments. */
|
|
13
|
+
export function packageScriptOf(command) {
|
|
14
|
+
const [first, second, ...rest] = command.args;
|
|
15
|
+
if (command.command === 'npm') {
|
|
16
|
+
if (first === 'test' && second === undefined)
|
|
17
|
+
return 'test';
|
|
18
|
+
if ((first === 'run' || first === 'run-script') && second && !rest.length)
|
|
19
|
+
return second;
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (command.command === 'pnpm' || command.command === 'yarn') {
|
|
23
|
+
if (first === 'run' && second && !rest.length)
|
|
24
|
+
return second;
|
|
25
|
+
if (first && second === undefined && !first.startsWith('-'))
|
|
26
|
+
return first;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
function declaresScript(directory, script) {
|
|
31
|
+
const file = path.join(directory, 'package.json');
|
|
32
|
+
if (!existsSync(file))
|
|
33
|
+
return false;
|
|
34
|
+
try {
|
|
35
|
+
const scripts = JSON.parse(readFileSync(file, 'utf8')).scripts;
|
|
36
|
+
return !!scripts && typeof scripts[script] === 'string';
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const planKey = (command) => JSON.stringify([command.repositoryId, command.cwd ?? '', command.command, command.args]);
|
|
43
|
+
/**
|
|
44
|
+
* The proposals to append to `planned` (already scope-defaulted). `normalize`
|
|
45
|
+
* applies the caller's scope default; proposals are expected to be
|
|
46
|
+
* structurally valid.
|
|
47
|
+
*/
|
|
48
|
+
export function admitVerificationProposals(context, planned, proposals, normalize) {
|
|
49
|
+
const configured = new Set(planned.map(command => command.repositoryId));
|
|
50
|
+
const seen = new Set(planned.map(planKey));
|
|
51
|
+
const admitted = [];
|
|
52
|
+
for (const raw of proposals) {
|
|
53
|
+
const repository = context.repositories.find(repo => repo.id === raw.repositoryId);
|
|
54
|
+
if (!repository)
|
|
55
|
+
continue;
|
|
56
|
+
const command = normalize(raw);
|
|
57
|
+
if (configured.has(raw.repositoryId)) {
|
|
58
|
+
const script = packageScriptOf(command);
|
|
59
|
+
if (!script || !declaresScript(path.resolve(repository.path, command.cwd ?? '.'), script))
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const key = planKey(command);
|
|
63
|
+
if (seen.has(key))
|
|
64
|
+
continue;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
admitted.push(command);
|
|
67
|
+
}
|
|
68
|
+
return admitted;
|
|
69
|
+
}
|
|
@@ -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)
|