devcontour-harness 0.1.0-alpha.18 → 0.1.0-alpha.19
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 +12 -2
- package/docs/architecture.md +48 -0
- package/docs/contracts.md +3 -0
- package/docs/quickstart.md +1 -1
- package/docs/release-notes/0.1.0-alpha.19.md +96 -0
- package/docs/upgrade.md +17 -16
- package/lib/cli.js +7 -1
- package/lib/contracts/cli.d.ts +30 -2
- package/lib/contracts/events.d.ts +21 -0
- package/lib/contracts/events.js +7 -0
- package/lib/contracts/execution.d.ts +24 -3
- package/lib/contracts/execution.js +14 -2
- package/lib/contracts/planning.js +2 -2
- package/lib/contracts/policy.d.ts +1 -1
- package/lib/contracts/ports.d.ts +6 -0
- package/lib/contracts/primitives.d.ts +13 -0
- package/lib/contracts/primitives.js +15 -0
- package/lib/contracts/profiles.js +2 -2
- package/lib/contracts/published.d.ts +23 -2
- package/lib/contracts/review.d.ts +2 -2
- package/lib/contracts/review.js +2 -2
- package/lib/contracts/work.js +3 -3
- package/lib/control/base-move.js +2 -2
- package/lib/control/harness.d.ts +1 -1
- package/lib/control/harness.js +7 -2
- package/lib/control/workflow.js +6 -0
- package/lib/engine/budget.d.ts +1 -1
- package/lib/engine/engine.d.ts +14 -2
- package/lib/engine/engine.js +40 -1
- package/lib/engine/runner.d.ts +11 -1
- package/lib/engine/runner.js +82 -11
- package/lib/environment/git-trust.d.ts +36 -0
- package/lib/environment/git-trust.js +174 -0
- package/lib/environment/isolation.d.ts +103 -1
- package/lib/environment/isolation.js +340 -5
- package/lib/environment/process.d.ts +5 -0
- package/lib/environment/process.js +20 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +4 -0
- package/lib/integrations/connector/config.js +2 -2
- package/lib/storage/document.d.ts +10 -2
- package/lib/verification/confirm.js +2 -2
- package/lib/verification/gates.d.ts +12 -1
- package/lib/verification/gates.js +81 -8
- package/lib/verification/requirements.js +6 -1
- package/package.json +1 -1
- package/schemas/execution-document.schema.json +27 -5
- package/schemas/plan-draft.schema.json +2 -2
- package/schemas/run-event.schema.json +22 -0
- package/schemas/work-graph.schema.json +2 -2
- package/schemas/work-order.schema.json +2 -2
package/lib/contracts/ports.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ export interface ExecutionView {
|
|
|
16
16
|
works(graphId?: string): WorkState[];
|
|
17
17
|
attempt(id: string): AttemptState | undefined;
|
|
18
18
|
attempts(workId?: string): AttemptState[];
|
|
19
|
+
/**
|
|
20
|
+
* Только активные попытки. Необязательный быстрый путь: такт очереди ищет
|
|
21
|
+
* истёкшие среди активных, и адаптеру с длинной историей не нужно строить
|
|
22
|
+
* все прежние попытки. Без метода — `attempts()` с фильтром по статусу.
|
|
23
|
+
*/
|
|
24
|
+
activeAttempts?(): AttemptState[];
|
|
19
25
|
verification(id: string): JointVerificationState | undefined;
|
|
20
26
|
verifications(): JointVerificationState[];
|
|
21
27
|
/**
|
|
@@ -3,6 +3,19 @@ import { z } from 'zod';
|
|
|
3
3
|
export declare const idSchema: z.ZodString;
|
|
4
4
|
/** Ссылка на gate или на именованную группу gates (`@имя`). */
|
|
5
5
|
export declare const gateRefSchema: z.ZodString;
|
|
6
|
+
/**
|
|
7
|
+
* Предел ссылок на gates в одной области (задача, поручение, потребитель,
|
|
8
|
+
* основание ревью). Он равен пределу gates профиля стека: область задачи
|
|
9
|
+
* может назвать весь профиль, и включение каждого gate в задачу выполнимо
|
|
10
|
+
* одной задачей.
|
|
11
|
+
*/
|
|
12
|
+
export declare const GATE_SCOPE_LIMIT = 100;
|
|
13
|
+
/** Превышение предела области gates — понятный отказ с величиной и исправлением. */
|
|
14
|
+
export declare const gateScopeTooBig: (issue: {
|
|
15
|
+
input?: unknown;
|
|
16
|
+
}) => string;
|
|
17
|
+
/** Ссылки на gates одной области: не больше `GATE_SCOPE_LIMIT`. */
|
|
18
|
+
export declare const gateScopeSchema: <T extends z.ZodType<string>>(item: T) => z.ZodArray<T>;
|
|
6
19
|
export declare const roleIdSchema: z.ZodString;
|
|
7
20
|
/** Полный SHA объекта Git (SHA-1 или SHA-256). */
|
|
8
21
|
export declare const gitShaSchema: z.ZodString;
|
|
@@ -5,6 +5,21 @@ export const idSchema = z
|
|
|
5
5
|
.regex(/^[A-Za-z0-9_-]{1,80}$/, 'Ожидается ID [A-Za-z0-9_-]{1,80}');
|
|
6
6
|
/** Ссылка на gate или на именованную группу gates (`@имя`). */
|
|
7
7
|
export const gateRefSchema = z.string().regex(/^@?[A-Za-z0-9_-]{1,80}$/);
|
|
8
|
+
/**
|
|
9
|
+
* Предел ссылок на gates в одной области (задача, поручение, потребитель,
|
|
10
|
+
* основание ревью). Он равен пределу gates профиля стека: область задачи
|
|
11
|
+
* может назвать весь профиль, и включение каждого gate в задачу выполнимо
|
|
12
|
+
* одной задачей.
|
|
13
|
+
*/
|
|
14
|
+
export const GATE_SCOPE_LIMIT = 100;
|
|
15
|
+
/** Превышение предела области gates — понятный отказ с величиной и исправлением. */
|
|
16
|
+
export const gateScopeTooBig = (issue) => {
|
|
17
|
+
const actual = Array.isArray(issue.input) ? issue.input.length : GATE_SCOPE_LIMIT + 1;
|
|
18
|
+
return (`область gates: ${actual} при пределе ${GATE_SCOPE_LIMIT}, лишних ${actual - GATE_SCOPE_LIMIT}. ` +
|
|
19
|
+
'Назовите группу gates (@имя) или уберите повторы');
|
|
20
|
+
};
|
|
21
|
+
/** Ссылки на gates одной области: не больше `GATE_SCOPE_LIMIT`. */
|
|
22
|
+
export const gateScopeSchema = (item) => z.array(item).max(GATE_SCOPE_LIMIT, { error: gateScopeTooBig });
|
|
8
23
|
export const roleIdSchema = z
|
|
9
24
|
.string()
|
|
10
25
|
.regex(/^[a-z][a-z0-9-]{1,30}$/, 'Идентификатор роли: строчные латинские буквы, цифры и дефис');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { idSchema, relativePathSchema, roleIdSchema } from "./primitives.js";
|
|
2
|
+
import { GATE_SCOPE_LIMIT, idSchema, relativePathSchema, roleIdSchema } from "./primitives.js";
|
|
3
3
|
import { environmentSchema, gateSchema, lifecycleSchema, stepSchema } from "./policy.js";
|
|
4
4
|
import { toolchainSchema } from "./toolchain.js";
|
|
5
5
|
/**
|
|
@@ -62,7 +62,7 @@ export const stackProfileSchema = z.strictObject({
|
|
|
62
62
|
/** Встроенный ID или относительный `./file.json` внутри репозитория. */
|
|
63
63
|
extends: z.array(z.string().min(1)).max(PROFILE_DEPTH_LIMIT).default([]),
|
|
64
64
|
capabilities: z.array(z.string().min(1).max(100)).max(100).default([]),
|
|
65
|
-
gates: z.array(profileGateSchema).max(
|
|
65
|
+
gates: z.array(profileGateSchema).max(GATE_SCOPE_LIMIT).default([]),
|
|
66
66
|
prepare: z.array(stepSchema).max(100).default([]),
|
|
67
67
|
environment: environmentSchema.optional(),
|
|
68
68
|
lifecycle: lifecycleSchema.optional(),
|
|
@@ -628,8 +628,8 @@ export declare const publishedSchemas: {
|
|
|
628
628
|
timeMs: z.ZodNumber;
|
|
629
629
|
}, z.core.$strict>]>>;
|
|
630
630
|
onUnknown: z.ZodDefault<z.ZodEnum<{
|
|
631
|
-
stop: "stop";
|
|
632
631
|
continue: "continue";
|
|
632
|
+
stop: "stop";
|
|
633
633
|
}>>;
|
|
634
634
|
}, z.core.$strict>>;
|
|
635
635
|
prices: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -1700,6 +1700,13 @@ export declare const publishedSchemas: {
|
|
|
1700
1700
|
passed: z.ZodBoolean;
|
|
1701
1701
|
evidenceId: z.ZodString;
|
|
1702
1702
|
consumer: z.ZodOptional<z.ZodString>;
|
|
1703
|
+
recheck: z.ZodOptional<z.ZodObject<{
|
|
1704
|
+
of: z.ZodString;
|
|
1705
|
+
outcome: z.ZodEnum<{
|
|
1706
|
+
flaky: "flaky";
|
|
1707
|
+
confirmed: "confirmed";
|
|
1708
|
+
}>;
|
|
1709
|
+
}, z.core.$strict>>;
|
|
1703
1710
|
}, z.core.$strict> | z.ZodObject<{
|
|
1704
1711
|
phase: z.ZodEnum<{
|
|
1705
1712
|
candidate: "candidate";
|
|
@@ -1955,6 +1962,13 @@ export declare const publishedSchemas: {
|
|
|
1955
1962
|
passed: z.ZodBoolean;
|
|
1956
1963
|
evidenceId: z.ZodString;
|
|
1957
1964
|
consumer: z.ZodOptional<z.ZodString>;
|
|
1965
|
+
recheck: z.ZodOptional<z.ZodObject<{
|
|
1966
|
+
of: z.ZodString;
|
|
1967
|
+
outcome: z.ZodEnum<{
|
|
1968
|
+
flaky: "flaky";
|
|
1969
|
+
confirmed: "confirmed";
|
|
1970
|
+
}>;
|
|
1971
|
+
}, z.core.$strict>>;
|
|
1958
1972
|
}, z.core.$strict> | z.ZodObject<{
|
|
1959
1973
|
phase: z.ZodEnum<{
|
|
1960
1974
|
candidate: "candidate";
|
|
@@ -2857,6 +2871,13 @@ export declare const publishedSchemas: {
|
|
|
2857
2871
|
digest: z.ZodString;
|
|
2858
2872
|
summary: z.ZodString;
|
|
2859
2873
|
consumer: z.ZodOptional<z.ZodString>;
|
|
2874
|
+
recheck: z.ZodOptional<z.ZodObject<{
|
|
2875
|
+
of: z.ZodString;
|
|
2876
|
+
outcome: z.ZodEnum<{
|
|
2877
|
+
flaky: "flaky";
|
|
2878
|
+
confirmed: "confirmed";
|
|
2879
|
+
}>;
|
|
2880
|
+
}, z.core.$strict>>;
|
|
2860
2881
|
inspection: z.ZodOptional<z.ZodObject<{
|
|
2861
2882
|
mode: z.ZodEnum<{
|
|
2862
2883
|
commands: "commands";
|
|
@@ -2948,9 +2969,9 @@ export declare const publishedSchemas: {
|
|
|
2948
2969
|
issues: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
2949
2970
|
path: z.ZodString;
|
|
2950
2971
|
reason: z.ZodEnum<{
|
|
2972
|
+
schema: "schema";
|
|
2951
2973
|
unknown: "unknown";
|
|
2952
2974
|
missing: "missing";
|
|
2953
|
-
schema: "schema";
|
|
2954
2975
|
duplicate: "duplicate";
|
|
2955
2976
|
"not-open": "not-open";
|
|
2956
2977
|
"approved-with-blocking": "approved-with-blocking";
|
|
@@ -239,9 +239,9 @@ export type ProtocolIssueReason = (typeof protocolIssueReasons)[number];
|
|
|
239
239
|
export declare const protocolIssueSchema: z.ZodObject<{
|
|
240
240
|
path: z.ZodString;
|
|
241
241
|
reason: z.ZodEnum<{
|
|
242
|
+
schema: "schema";
|
|
242
243
|
unknown: "unknown";
|
|
243
244
|
missing: "missing";
|
|
244
|
-
schema: "schema";
|
|
245
245
|
duplicate: "duplicate";
|
|
246
246
|
"not-open": "not-open";
|
|
247
247
|
"approved-with-blocking": "approved-with-blocking";
|
|
@@ -269,9 +269,9 @@ export declare const reviewRoundSchema: z.ZodObject<{
|
|
|
269
269
|
issues: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
270
270
|
path: z.ZodString;
|
|
271
271
|
reason: z.ZodEnum<{
|
|
272
|
+
schema: "schema";
|
|
272
273
|
unknown: "unknown";
|
|
273
274
|
missing: "missing";
|
|
274
|
-
schema: "schema";
|
|
275
275
|
duplicate: "duplicate";
|
|
276
276
|
"not-open": "not-open";
|
|
277
277
|
"approved-with-blocking": "approved-with-blocking";
|
package/lib/contracts/review.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { authorSchema, digestSchema, idSchema, isoTimeSchema, relativePathSchema, runtimeNameSchema, } from "./primitives.js";
|
|
2
|
+
import { authorSchema, digestSchema, idSchema, isoTimeSchema, relativePathSchema, runtimeNameSchema, gateScopeSchema, } from "./primitives.js";
|
|
3
3
|
import { digestOf } from "./digest.js";
|
|
4
4
|
/**
|
|
5
5
|
* Ответы исполнителей. Runtime возвращает сырой текст/JSON (`RuntimeResponse.output`
|
|
@@ -71,7 +71,7 @@ export const findingTransitionSchema = z.strictObject({
|
|
|
71
71
|
*/
|
|
72
72
|
/** Основание «приёмка уже выполнена»: названные gates и что их evidence показывает. */
|
|
73
73
|
export const alreadySatisfiedSchema = z.strictObject({
|
|
74
|
-
gates:
|
|
74
|
+
gates: gateScopeSchema(z.string().min(1).max(200)).min(1),
|
|
75
75
|
evidence: z.string().min(1).max(4000),
|
|
76
76
|
});
|
|
77
77
|
export const reviewResultSchema = z.object({
|
package/lib/contracts/work.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { API_VERSION } from "./version.js";
|
|
3
|
-
import { digestSchema, gateRefSchema, gitShaSchema, idSchema, relativePathSchema, roleIdSchema, trimmedText, } from "./primitives.js";
|
|
3
|
+
import { digestSchema, gateRefSchema, gitShaSchema, idSchema, relativePathSchema, roleIdSchema, trimmedText, gateScopeSchema, } from "./primitives.js";
|
|
4
4
|
import { digestOf } from "./digest.js";
|
|
5
5
|
/**
|
|
6
6
|
* Откуда пришло поручение: коннектор, ключ записи в нём и её редакция.
|
|
@@ -31,7 +31,7 @@ export const contractRefSchema = z.strictObject({ id: idSchema, digest: digestSc
|
|
|
31
31
|
/** Проверка потребителя: компонент и его gates против integration результата. */
|
|
32
32
|
export const consumerSchema = z.strictObject({
|
|
33
33
|
repositoryId: idSchema,
|
|
34
|
-
gates:
|
|
34
|
+
gates: gateScopeSchema(gateRefSchema).min(1),
|
|
35
35
|
});
|
|
36
36
|
/**
|
|
37
37
|
* Исполняемое поручение. Это снимок постановки, а не редактируемая задача:
|
|
@@ -63,7 +63,7 @@ export const workOrderSchema = z.strictObject({
|
|
|
63
63
|
}),
|
|
64
64
|
dependsOn: z.array(idSchema).max(100).default([]),
|
|
65
65
|
/** Область доказательства: подмножество gates/групп политики. Нет поля — все gates репозитория. */
|
|
66
|
-
gates:
|
|
66
|
+
gates: gateScopeSchema(gateRefSchema).optional(),
|
|
67
67
|
consumers: z.array(consumerSchema).max(10).optional(),
|
|
68
68
|
/** Свойства договора проверки, которые поручение подтверждает сейчас. */
|
|
69
69
|
properties: z.array(propertyIdSchema).max(30).optional(),
|
package/lib/control/base-move.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
-
import { git } from "../environment/process.js";
|
|
3
|
+
import { git, hostGitArgs } from "../environment/process.js";
|
|
4
4
|
/**
|
|
5
5
|
* Сдвиг ref внутри транзакции проверки владения операцией: проверка и
|
|
6
6
|
* `git update-ref` неразделимы — отмена или перехват операции либо видны
|
|
@@ -11,7 +11,7 @@ import { git } from "../environment/process.js";
|
|
|
11
11
|
export function fencedRefMove(store, fence, timeoutMs = fencedRefMove.timeoutMs) {
|
|
12
12
|
return (repoPath, target, to, from) => store.transact('base.moved', (tx) => {
|
|
13
13
|
fence(tx);
|
|
14
|
-
execFileSync('git',
|
|
14
|
+
execFileSync('git', hostGitArgs(repoPath, ['update-ref', target, to, from]), {
|
|
15
15
|
cwd: repoPath,
|
|
16
16
|
timeout: timeoutMs,
|
|
17
17
|
stdio: 'pipe',
|
package/lib/control/harness.d.ts
CHANGED
|
@@ -681,7 +681,7 @@ export declare class Harness {
|
|
|
681
681
|
private repair;
|
|
682
682
|
status(graphId?: string): {
|
|
683
683
|
budgets: {
|
|
684
|
-
onUnknown: "
|
|
684
|
+
onUnknown: "continue" | "stop";
|
|
685
685
|
budgets: {
|
|
686
686
|
reason?: string | undefined;
|
|
687
687
|
scope: import("../engine/budget.ts").BudgetScope;
|
package/lib/control/harness.js
CHANGED
|
@@ -16,7 +16,7 @@ import { WorkflowQueue } from "./workflow.js";
|
|
|
16
16
|
import { technicalReview } from "./review.js";
|
|
17
17
|
import { policySourceChanged } from "./provenance.js";
|
|
18
18
|
import { JointRunner } from "../engine/joint-runner.js";
|
|
19
|
-
import { git } from "../environment/process.js";
|
|
19
|
+
import { git, hostGitArgs } from "../environment/process.js";
|
|
20
20
|
import { analyzeRecovery, applyCases } from "./recovery.js";
|
|
21
21
|
import { declarationProblems, referenceNote } from "../verification/checks.js";
|
|
22
22
|
import { spikeOf, spikesOf } from "../verification/spike.js";
|
|
@@ -341,7 +341,12 @@ export class Harness {
|
|
|
341
341
|
return false;
|
|
342
342
|
try {
|
|
343
343
|
// Синхронная проверка: предпосылка доступна в целевой ветке.
|
|
344
|
-
execFileSync('git',
|
|
344
|
+
execFileSync('git', hostGitArgs(this.repoPath(repo.path), [
|
|
345
|
+
'merge-base',
|
|
346
|
+
'--is-ancestor',
|
|
347
|
+
p.resultSha,
|
|
348
|
+
repo.targetRef,
|
|
349
|
+
]), {
|
|
345
350
|
cwd: this.repoPath(repo.path),
|
|
346
351
|
stdio: 'ignore',
|
|
347
352
|
});
|
package/lib/control/workflow.js
CHANGED
|
@@ -217,6 +217,12 @@ export class WorkflowQueue {
|
|
|
217
217
|
next.status = next.stage >= stages ? 'completed' : 'queued';
|
|
218
218
|
if (next.status === 'completed')
|
|
219
219
|
next.finishedAt = iso(this.now());
|
|
220
|
+
// Счётчик попыток — бюджет текущей стадии, и следующая стадия начинает
|
|
221
|
+
// его с нуля. У завершённого задания следующей стадии нет: счётчик
|
|
222
|
+
// показывает, сколько попыток стадий задание израсходовало всего (захваты
|
|
223
|
+
// `running` в истории; захват стадии, ответившей «жду», из неё снят).
|
|
224
|
+
if (next.status === 'completed')
|
|
225
|
+
next.attempts = next.history.filter((h) => h.event === 'running').length;
|
|
220
226
|
delete next.token;
|
|
221
227
|
delete next.leaseUntil;
|
|
222
228
|
delete next.error;
|
package/lib/engine/budget.d.ts
CHANGED
|
@@ -322,7 +322,7 @@ export declare function budgetView(b: BudgetStatus): {
|
|
|
322
322
|
};
|
|
323
323
|
/** Бюджеты установки: общий, группы с незавершённой работой, незавершённые поручения. */
|
|
324
324
|
export declare function budgetOverview(c: BudgetContext, view: ExecutionView, records?: CallRecord[]): {
|
|
325
|
-
onUnknown: "
|
|
325
|
+
onUnknown: "continue" | "stop";
|
|
326
326
|
budgets: {
|
|
327
327
|
reason?: string | undefined;
|
|
328
328
|
scope: BudgetScope;
|
package/lib/engine/engine.d.ts
CHANGED
|
@@ -34,6 +34,14 @@ export interface EngineOptions {
|
|
|
34
34
|
export type Phase = 'running' | 'verifying' | 'reviewing' | 'integrating';
|
|
35
35
|
/** Поручение исчерпало предел незасчитанных отказов: повтор — только со сбросом. */
|
|
36
36
|
export declare const neutralExhausted: (w: Pick<WorkState, "neutralFailures">) => boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Провал, названный свойством или случаем корпуса, — найденный контрпример, а
|
|
39
|
+
* не сбой среды: генеративная проверка недетерминирована, и удачный повтор
|
|
40
|
+
* не отменяет найденного входа. Такой провал не повторяется.
|
|
41
|
+
*/
|
|
42
|
+
export declare function namesFailingCases(e: Pick<EvidenceRecord, 'property'>): boolean;
|
|
43
|
+
/** Активные попытки снимка: быстрый путь адаптера или фильтр всех. */
|
|
44
|
+
export declare function activeOf(v: ExecutionView): AttemptState[];
|
|
37
45
|
export declare class Engine {
|
|
38
46
|
readonly host: ExecutionHost;
|
|
39
47
|
readonly policy: ExecutionPolicy;
|
|
@@ -372,7 +380,7 @@ export declare class Engine {
|
|
|
372
380
|
pinLegacyCalls(): number;
|
|
373
381
|
/** Обзор бюджетов для статуса; `undefined` — бюджеты не заданы. */
|
|
374
382
|
budgets(): {
|
|
375
|
-
onUnknown: "
|
|
383
|
+
onUnknown: "continue" | "stop";
|
|
376
384
|
budgets: {
|
|
377
385
|
reason?: string | undefined;
|
|
378
386
|
scope: import("./budget.ts").BudgetScope;
|
|
@@ -492,6 +500,10 @@ export declare class Engine {
|
|
|
492
500
|
digest: string;
|
|
493
501
|
summary: string;
|
|
494
502
|
consumer?: string | undefined;
|
|
503
|
+
recheck?: {
|
|
504
|
+
of: string;
|
|
505
|
+
outcome: "flaky" | "confirmed";
|
|
506
|
+
} | undefined;
|
|
495
507
|
inspection?: {
|
|
496
508
|
mode: "commands" | "diff-only";
|
|
497
509
|
source: "codex-events" | "unavailable" | "controller-probes" | "runtime-and-controller";
|
|
@@ -546,7 +558,7 @@ export declare class Engine {
|
|
|
546
558
|
expected: string[];
|
|
547
559
|
issues?: {
|
|
548
560
|
path: string;
|
|
549
|
-
reason: "
|
|
561
|
+
reason: "schema" | "unknown" | "missing" | "duplicate" | "not-open" | "approved-with-blocking";
|
|
550
562
|
message: string;
|
|
551
563
|
}[] | undefined;
|
|
552
564
|
transitions?: {
|
package/lib/engine/engine.js
CHANGED
|
@@ -33,6 +33,21 @@ const contentDigest = (order) => digestOf({ ...order, source: { connector: order
|
|
|
33
33
|
const uncounted = (kind, blocked) => blocked || kind === 'environment' || kind === 'provider-auth';
|
|
34
34
|
/** Поручение исчерпало предел незасчитанных отказов: повтор — только со сбросом. */
|
|
35
35
|
export const neutralExhausted = (w) => (w.neutralFailures ?? 0) >= MAX_NEUTRAL_FAILURES;
|
|
36
|
+
/**
|
|
37
|
+
* Провал, названный свойством или случаем корпуса, — найденный контрпример, а
|
|
38
|
+
* не сбой среды: генеративная проверка недетерминирована, и удачный повтор
|
|
39
|
+
* не отменяет найденного входа. Такой провал не повторяется.
|
|
40
|
+
*/
|
|
41
|
+
export function namesFailingCases(e) {
|
|
42
|
+
const report = e.property?.report;
|
|
43
|
+
return (!!report &&
|
|
44
|
+
(report.properties.some((p) => p.status === 'failed') ||
|
|
45
|
+
(report.corpus?.failed.length ?? 0) > 0));
|
|
46
|
+
}
|
|
47
|
+
/** Активные попытки снимка: быстрый путь адаптера или фильтр всех. */
|
|
48
|
+
export function activeOf(v) {
|
|
49
|
+
return v.activeAttempts ? v.activeAttempts() : v.attempts().filter((a) => a.status === 'active');
|
|
50
|
+
}
|
|
36
51
|
export class Engine {
|
|
37
52
|
host;
|
|
38
53
|
policy;
|
|
@@ -1136,6 +1151,25 @@ export class Engine {
|
|
|
1136
1151
|
// доказывала бы его (и названный testcase).
|
|
1137
1152
|
if ((evidence.kind === 'review') !== (evidence.gate === 'independent-review'))
|
|
1138
1153
|
throw new HarnessError('INVALID_INPUT', `Evidence вида ${evidence.kind} не может относиться к gate ${evidence.gate}`);
|
|
1154
|
+
// Повтор ссылается на провал того же gate этой попытки на том же SHA, и
|
|
1155
|
+
// попытка повторяет не больше одного провала.
|
|
1156
|
+
if (evidence.recheck) {
|
|
1157
|
+
const of = evidence.recheck.of;
|
|
1158
|
+
const first = a.evidence.find((e) => e.id === of);
|
|
1159
|
+
if (evidence.kind !== 'test' ||
|
|
1160
|
+
!first ||
|
|
1161
|
+
first.passed ||
|
|
1162
|
+
first.kind !== 'test' ||
|
|
1163
|
+
first.gate !== evidence.gate ||
|
|
1164
|
+
first.phase !== evidence.phase ||
|
|
1165
|
+
first.sha !== evidence.sha ||
|
|
1166
|
+
first.consumer !== evidence.consumer ||
|
|
1167
|
+
namesFailingCases(first) ||
|
|
1168
|
+
a.evidence.some((e) => e.recheck))
|
|
1169
|
+
throw new HarnessError('INVALID_INPUT', 'Повтор gate не относится к провалу этой попытки');
|
|
1170
|
+
if (evidence.recheck.outcome !== (evidence.passed ? 'flaky' : 'confirmed'))
|
|
1171
|
+
throw new HarnessError('INVALID_INPUT', 'Исход повтора gate не согласован с записью');
|
|
1172
|
+
}
|
|
1139
1173
|
const record = {
|
|
1140
1174
|
...evidence,
|
|
1141
1175
|
...(a.checks ? { checks: a.checks.effective } : {}),
|
|
@@ -1170,6 +1204,7 @@ export class Engine {
|
|
|
1170
1204
|
passed: record.passed,
|
|
1171
1205
|
evidenceId: record.id,
|
|
1172
1206
|
...(record.consumer ? { consumer: record.consumer } : {}),
|
|
1207
|
+
...(record.recheck ? { recheck: record.recheck } : {}),
|
|
1173
1208
|
},
|
|
1174
1209
|
});
|
|
1175
1210
|
return record;
|
|
@@ -2215,9 +2250,13 @@ export class Engine {
|
|
|
2215
2250
|
* момент в будущем, когда снимает всё владение восстановленной копии.
|
|
2216
2251
|
*/
|
|
2217
2252
|
expire(at = this.now()) {
|
|
2253
|
+
// Такт очереди вызывает истечение постоянно: без истёкших попыток
|
|
2254
|
+
// транзакции записи (и её снимка состояния) нет.
|
|
2255
|
+
if (!this.read((v) => activeOf(v).some((a) => a.leaseUntil <= at)))
|
|
2256
|
+
return [];
|
|
2218
2257
|
return this.tx('runs.expired', (tx) => {
|
|
2219
2258
|
const ids = [];
|
|
2220
|
-
for (const a of tx
|
|
2259
|
+
for (const a of activeOf(tx))
|
|
2221
2260
|
if (a.status === 'active' && a.leaseUntil <= at) {
|
|
2222
2261
|
const error = (a.error?.startsWith(HEARTBEAT_FAILED) ? a.error + '. ' : '') +
|
|
2223
2262
|
'Истёк срок владения. Повторите явно.';
|
package/lib/engine/runner.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ImplementationResult, type AttemptState, type ContextSnapshot, type ExecutionPolicy, type RuntimeAdapter, type RuntimeName, type WorkState } from '../contracts/index.ts';
|
|
2
2
|
import { type FailureKind } from './failure.ts';
|
|
3
|
-
import type
|
|
3
|
+
import { type Engine } from './engine.ts';
|
|
4
4
|
import { CorpusStore } from '../verification/corpus.ts';
|
|
5
5
|
type Repo = ExecutionPolicy['repositories'][number];
|
|
6
6
|
export interface RunnerOptions {
|
|
@@ -197,6 +197,16 @@ export declare class Runner {
|
|
|
197
197
|
receipts: string[];
|
|
198
198
|
}>;
|
|
199
199
|
private gate;
|
|
200
|
+
/**
|
|
201
|
+
* Gate интеграции с одним повтором на кандидата. Провал собственного gate на
|
|
202
|
+
* integration, когда тот же gate прошёл на кандидате, часто вызван средой
|
|
203
|
+
* (бюджет времени под конкуренцией за CPU), а не кодом; без повтора такой
|
|
204
|
+
* провал оплачивался новой реализацией. Повтор — тот же gate на том же
|
|
205
|
+
* integration SHA, один раз за интеграцию кандидата; обе записи остаются,
|
|
206
|
+
* у повтора — `recheck` со ссылкой на первую и исходом (`flaky` — повтор
|
|
207
|
+
* прошёл, `confirmed` — провал подтвердился и засчитывается как прежде).
|
|
208
|
+
*/
|
|
209
|
+
private integrationGate;
|
|
200
210
|
private runOneGate;
|
|
201
211
|
/**
|
|
202
212
|
* Ожидаемая проверка генеративного gate. Проверка потребителя исполняет
|
package/lib/engine/runner.js
CHANGED
|
@@ -2,9 +2,10 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { roleBinding, selectReviewer, mcpEnvironment, HarnessError, digestOf, contextDigest, implementationResultSchema, codeReviewHistory, codeReviewScope, checkCodeReview, logicalReview, DEFAULT_REVIEW_PROTOCOL_RETRIES, reviewResultSchema, sha256, } from "../contracts/index.js";
|
|
7
|
-
import { git } from "../environment/process.js";
|
|
7
|
+
import { git, hostGitArgs } from "../environment/process.js";
|
|
8
|
+
import { protectedChanges, protectedSnapshot } from "../environment/isolation.js";
|
|
8
9
|
import { executionEnvironment, runSteps, withEnvironment } from "../environment/lifecycle.js";
|
|
9
10
|
import { redactDeep } from "../environment/redaction.js";
|
|
10
11
|
import { alreadySatisfiedReviewOutputSchema, implementationOutputSchema, reviewOutputSchema, } from "../agents/schemas.js";
|
|
@@ -20,6 +21,7 @@ import { defaultResourceDatabase, withResources } from "../environment/resources
|
|
|
20
21
|
import { REQUIREMENT_GATE, assertRequirements } from "../verification/requirements.js";
|
|
21
22
|
import { BlockedError, classifyFailure } from "./classify.js";
|
|
22
23
|
import { TaskFailure } from "./failure.js";
|
|
24
|
+
import { activeOf, namesFailingCases } from "./engine.js";
|
|
23
25
|
import { prompt } from "./prompt.js";
|
|
24
26
|
import { contractDocuments, orderCheckManifest, referenceCodeNote, referenceSetFiles, setFileReference, } from "../verification/checks.js";
|
|
25
27
|
import { CorpusStore, localRoots } from "../verification/corpus.js";
|
|
@@ -271,7 +273,7 @@ export class Runner {
|
|
|
271
273
|
this.engine.pinLegacyCalls();
|
|
272
274
|
}
|
|
273
275
|
const now = this.engine.clock();
|
|
274
|
-
const stale = this.engine.read((v) => v
|
|
276
|
+
const stale = this.engine.read((v) => activeOf(v).filter((a) => a.leaseUntil <= now));
|
|
275
277
|
for (const a of stale) {
|
|
276
278
|
if (!a.integrationSha)
|
|
277
279
|
continue;
|
|
@@ -761,6 +763,10 @@ export class Runner {
|
|
|
761
763
|
if (context)
|
|
762
764
|
await writeFile(join(dir, 'context.json'), JSON.stringify(context, null, 2));
|
|
763
765
|
await this.prepareSelfCheck(attempt, repo, join(artifacts, 'self-check'));
|
|
766
|
+
// Защищённые пути до работы исполнителя: перенос каталога,
|
|
767
|
+
// собранного там, где запрет песочницы не действует, виден
|
|
768
|
+
// только сверкой.
|
|
769
|
+
const guarded = this.policy.isolation.mode === 'os' ? protectedSnapshot(cwd) : undefined;
|
|
764
770
|
const response = await this.call(this.adapter(attempt.runtime), {
|
|
765
771
|
task: 'implement',
|
|
766
772
|
attempt,
|
|
@@ -798,6 +804,13 @@ export class Runner {
|
|
|
798
804
|
result.data.summary.slice(0, 500));
|
|
799
805
|
this.engine.heartbeat(attempt.id, attempt.token);
|
|
800
806
|
signal.throwIfAborted();
|
|
807
|
+
const planted = guarded ? protectedChanges(guarded, cwd) : [];
|
|
808
|
+
if (planted.length)
|
|
809
|
+
throw new TaskFailure('scope-violation', 'Исполнитель создал или изменил защищённые пути (конфигурация IDE, shell, git или Claude): ' +
|
|
810
|
+
planted
|
|
811
|
+
.slice(0, 5)
|
|
812
|
+
.map((p) => relative(cwd, p))
|
|
813
|
+
.join(', '));
|
|
801
814
|
if ((await git(cwd, 'rev-parse', 'HEAD')) !== base)
|
|
802
815
|
throw new TaskFailure('scope-violation', 'Исполнитель изменил HEAD; интеграцией владеет harness');
|
|
803
816
|
await git(cwd, 'add', '-A');
|
|
@@ -1047,15 +1060,63 @@ export class Runner {
|
|
|
1047
1060
|
JSON.parse(await readFile(file, 'utf8')).errors.join('; '));
|
|
1048
1061
|
return { status: 'cleaned', receipts: cleaned };
|
|
1049
1062
|
}
|
|
1050
|
-
async gate(attempt, cwd, sha, phase, gate, artifacts, signal, execution, consumer
|
|
1063
|
+
async gate(attempt, cwd, sha, phase, gate, artifacts, signal, execution, consumer,
|
|
1064
|
+
/** ID записи провала, которую повторяет этот запуск (`recheckIntegration`). */
|
|
1065
|
+
recheck) {
|
|
1051
1066
|
// Время проверок потребителя — отдельная стадия: в метриках области оно
|
|
1052
|
-
// не смешивается с собственными gates поручения.
|
|
1067
|
+
// не смешивается с собственными gates поручения. Повтор — тоже своя
|
|
1068
|
+
// стадия: оценка длительности gate не удваивается из-за него.
|
|
1053
1069
|
const stage = consumer
|
|
1054
1070
|
? `${phase}-consumer:${consumer.repositoryId}:${gate.id}`
|
|
1055
|
-
:
|
|
1056
|
-
|
|
1071
|
+
: recheck
|
|
1072
|
+
? `${phase}-recheck:${gate.id}`
|
|
1073
|
+
: `${phase}-test:${gate.id}`;
|
|
1074
|
+
await this.timed(attempt, stage, () => this.runOneGate(attempt, cwd, sha, phase, gate, artifacts, signal, execution, consumer, recheck));
|
|
1057
1075
|
}
|
|
1058
|
-
|
|
1076
|
+
/**
|
|
1077
|
+
* Gate интеграции с одним повтором на кандидата. Провал собственного gate на
|
|
1078
|
+
* integration, когда тот же gate прошёл на кандидате, часто вызван средой
|
|
1079
|
+
* (бюджет времени под конкуренцией за CPU), а не кодом; без повтора такой
|
|
1080
|
+
* провал оплачивался новой реализацией. Повтор — тот же gate на том же
|
|
1081
|
+
* integration SHA, один раз за интеграцию кандидата; обе записи остаются,
|
|
1082
|
+
* у повтора — `recheck` со ссылкой на первую и исходом (`flaky` — повтор
|
|
1083
|
+
* прошёл, `confirmed` — провал подтвердился и засчитывается как прежде).
|
|
1084
|
+
*/
|
|
1085
|
+
async integrationGate(attempt, cwd, sha, gate, artifacts, signal, execution, budget) {
|
|
1086
|
+
try {
|
|
1087
|
+
await this.gate(attempt, cwd, sha, 'integration', gate, artifacts, signal, execution);
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
if (!budget.recheck || signal.aborted || !(error instanceof TaskFailure))
|
|
1091
|
+
throw error;
|
|
1092
|
+
if (error.kind !== 'gate')
|
|
1093
|
+
throw error;
|
|
1094
|
+
const { failed, candidatePassed, spent } = this.engine.read((v) => {
|
|
1095
|
+
const evidence = v.attempt(attempt.id)?.evidence ?? [];
|
|
1096
|
+
const own = (e, phase, at) => e.kind === 'test' &&
|
|
1097
|
+
e.gate === gate.id &&
|
|
1098
|
+
e.phase === phase &&
|
|
1099
|
+
e.sha === at &&
|
|
1100
|
+
!e.consumer;
|
|
1101
|
+
return {
|
|
1102
|
+
failed: evidence.findLast((e) => own(e, 'integration', sha)),
|
|
1103
|
+
candidatePassed: evidence.findLast((e) => own(e, 'candidate', attempt.candidateSha))?.passed === true,
|
|
1104
|
+
// Один повтор на кандидата: переиспользованный следующей попыткой
|
|
1105
|
+
// кандидат второго не получает.
|
|
1106
|
+
spent: v
|
|
1107
|
+
.attempts(attempt.workId)
|
|
1108
|
+
.some((x) => x.candidateSha === attempt.candidateSha && x.evidence.some((e) => e.recheck)),
|
|
1109
|
+
};
|
|
1110
|
+
});
|
|
1111
|
+
// Контрпример свойства или провал случая корпуса не повторяется: удачный
|
|
1112
|
+
// второй запуск недетерминированной проверки не отменяет найденного входа.
|
|
1113
|
+
if (!failed || failed.passed || namesFailingCases(failed) || !candidatePassed || spent)
|
|
1114
|
+
throw error;
|
|
1115
|
+
budget.recheck = false;
|
|
1116
|
+
await this.gate(attempt, cwd, sha, 'integration', gate, artifacts, signal, execution, undefined, failed.id);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
async runOneGate(attempt, cwd, sha, phase, gate, artifacts, signal, execution, consumer, recheck) {
|
|
1059
1120
|
// Генеративный gate — всегда; gate эталонных наборов — когда манифест
|
|
1060
1121
|
// поручения ждёт от него отчёта (без договора — прежний уровень).
|
|
1061
1122
|
const run = this.propertyRun(attempt, gate, consumer);
|
|
@@ -1067,9 +1128,14 @@ export class Runner {
|
|
|
1067
1128
|
sha,
|
|
1068
1129
|
phase,
|
|
1069
1130
|
gate,
|
|
1131
|
+
// Лог и отчёт повтора — в своём каталоге: первая запись ссылается на
|
|
1132
|
+
// свой лог по digest, и повтор его не перезаписывает.
|
|
1070
1133
|
artifactDir: consumer
|
|
1071
1134
|
? join(artifacts, phase, 'consumer', consumer.repositoryId)
|
|
1072
|
-
:
|
|
1135
|
+
: recheck
|
|
1136
|
+
? join(artifacts, phase, 'recheck')
|
|
1137
|
+
: join(artifacts, phase),
|
|
1138
|
+
...(recheck ? { recheck } : {}),
|
|
1073
1139
|
signal,
|
|
1074
1140
|
execution,
|
|
1075
1141
|
boundary: this.boundary(consumer?.dependencies ?? attempt.dependencies),
|
|
@@ -1893,8 +1959,9 @@ export class Runner {
|
|
|
1893
1959
|
const execution = this.environment(attempt, 'integration', cwd);
|
|
1894
1960
|
await withEnvironment(repo.lifecycle ?? this.policy.lifecycle, cwd, join(artifacts, 'integration', 'environment'), execution, signal, async () => {
|
|
1895
1961
|
await runSteps(repo.prepare, cwd, join(artifacts, 'integration', 'prepare'), execution, signal);
|
|
1962
|
+
const budget = { recheck: true };
|
|
1896
1963
|
for (const gate of gates)
|
|
1897
|
-
await this.
|
|
1964
|
+
await this.integrationGate(attempt, cwd, sha, gate, artifacts, signal, execution, budget);
|
|
1898
1965
|
await this.consumers(attempt, cwd, sha, artifacts, signal);
|
|
1899
1966
|
this.requirements(attempt, work, repoPath, cwd, sha, 'integration');
|
|
1900
1967
|
rearm();
|
|
@@ -1920,7 +1987,11 @@ export class Runner {
|
|
|
1920
1987
|
assertRequirements(work.order, repoPath);
|
|
1921
1988
|
// Внутри транзакции: без hooks репозитория (они исполнили бы чужой код
|
|
1922
1989
|
// под блокировкой записи) и с конечным пределом, короче lease.
|
|
1923
|
-
execFileSync('git',
|
|
1990
|
+
execFileSync('git', hostGitArgs(repoPath, ['update-ref', repo.targetRef, sha, base]), {
|
|
1991
|
+
cwd: repoPath,
|
|
1992
|
+
stdio: 'pipe',
|
|
1993
|
+
timeout: Runner.refTimeoutMs,
|
|
1994
|
+
});
|
|
1924
1995
|
});
|
|
1925
1996
|
}
|
|
1926
1997
|
catch (error) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git на хосте над деревом, в котором исполнялся чужой код.
|
|
3
|
+
*
|
|
4
|
+
* Исполнитель и проверки пишут в checkout. Git, запущенный потом на хосте,
|
|
5
|
+
* читает конфигурацию того репозитория, который найдёт от рабочего каталога,
|
|
6
|
+
* а она способна запускать программы (`core.fsmonitor`, hooks, драйверы
|
|
7
|
+
* фильтров и diff). Подменённый файл `.git` (`gitdir: <checkout>/evil`),
|
|
8
|
+
* `.git/commondir` в обычном checkout или вложенный `.git` уводили git в
|
|
9
|
+
* конфигурацию, которую написал чужой код, — и она исполнялась вне песочницы.
|
|
10
|
+
*
|
|
11
|
+
* Поэтому каждый вызов git хоста сначала сверяет, куда git придёт: файл
|
|
12
|
+
* `.git` рабочего дерева, созданного через `worktree add`, должен остаться
|
|
13
|
+
* тем же; gitdir и общий каталог репозитория лежат вне дерева и согласованы
|
|
14
|
+
* между собой; общий каталог — уже известный хосту репозиторий, если он
|
|
15
|
+
* известен; вложенный репозиторий внутри известного дерева и перенаправление
|
|
16
|
+
* обычного `.git` — отказ. Флаги `gitHardening` поверх этого снимают
|
|
17
|
+
* исполняемые настройки, которые git читает сам.
|
|
18
|
+
*/
|
|
19
|
+
export declare const gitHardening: string[];
|
|
20
|
+
export declare class UntrustedCheckoutError extends Error {
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Сверяет репозиторий, который git найдёт от `cwd`. Возвращает его общий
|
|
25
|
+
* каталог или `undefined`, если репозитория нет (git откажет сам).
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertTrustedCheckout(cwd: string): string | undefined;
|
|
28
|
+
/** Рабочее дерево создано хостом: дальше его `.git` обязан остаться прежним. */
|
|
29
|
+
export declare function rememberWorktree(path: string): void;
|
|
30
|
+
/** Ключ дерева — настоящий путь (/var → /private/var). */
|
|
31
|
+
export declare const worktreeKey: (path: string) => string;
|
|
32
|
+
export declare function forgetWorktree(key: string): void;
|
|
33
|
+
/** Путь, который `git worktree add` получит как каталог дерева. */
|
|
34
|
+
export declare function worktreeAddPath(args: string[]): string | undefined;
|
|
35
|
+
/** Только для тестов: забыть, что известно процессу. */
|
|
36
|
+
export declare function resetGitTrust(): void;
|