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.
Files changed (51) hide show
  1. package/README.md +12 -2
  2. package/docs/architecture.md +48 -0
  3. package/docs/contracts.md +3 -0
  4. package/docs/quickstart.md +1 -1
  5. package/docs/release-notes/0.1.0-alpha.19.md +96 -0
  6. package/docs/upgrade.md +17 -16
  7. package/lib/cli.js +7 -1
  8. package/lib/contracts/cli.d.ts +30 -2
  9. package/lib/contracts/events.d.ts +21 -0
  10. package/lib/contracts/events.js +7 -0
  11. package/lib/contracts/execution.d.ts +24 -3
  12. package/lib/contracts/execution.js +14 -2
  13. package/lib/contracts/planning.js +2 -2
  14. package/lib/contracts/policy.d.ts +1 -1
  15. package/lib/contracts/ports.d.ts +6 -0
  16. package/lib/contracts/primitives.d.ts +13 -0
  17. package/lib/contracts/primitives.js +15 -0
  18. package/lib/contracts/profiles.js +2 -2
  19. package/lib/contracts/published.d.ts +23 -2
  20. package/lib/contracts/review.d.ts +2 -2
  21. package/lib/contracts/review.js +2 -2
  22. package/lib/contracts/work.js +3 -3
  23. package/lib/control/base-move.js +2 -2
  24. package/lib/control/harness.d.ts +1 -1
  25. package/lib/control/harness.js +7 -2
  26. package/lib/control/workflow.js +6 -0
  27. package/lib/engine/budget.d.ts +1 -1
  28. package/lib/engine/engine.d.ts +14 -2
  29. package/lib/engine/engine.js +40 -1
  30. package/lib/engine/runner.d.ts +11 -1
  31. package/lib/engine/runner.js +82 -11
  32. package/lib/environment/git-trust.d.ts +36 -0
  33. package/lib/environment/git-trust.js +174 -0
  34. package/lib/environment/isolation.d.ts +103 -1
  35. package/lib/environment/isolation.js +340 -5
  36. package/lib/environment/process.d.ts +5 -0
  37. package/lib/environment/process.js +20 -1
  38. package/lib/index.d.ts +2 -0
  39. package/lib/index.js +4 -0
  40. package/lib/integrations/connector/config.js +2 -2
  41. package/lib/storage/document.d.ts +10 -2
  42. package/lib/verification/confirm.js +2 -2
  43. package/lib/verification/gates.d.ts +12 -1
  44. package/lib/verification/gates.js +81 -8
  45. package/lib/verification/requirements.js +6 -1
  46. package/package.json +1 -1
  47. package/schemas/execution-document.schema.json +27 -5
  48. package/schemas/plan-draft.schema.json +2 -2
  49. package/schemas/run-event.schema.json +22 -0
  50. package/schemas/work-graph.schema.json +2 -2
  51. package/schemas/work-order.schema.json +2 -2
@@ -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(100).default([]),
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";
@@ -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: z.array(z.string().min(1).max(200)).min(1).max(20),
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({
@@ -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: z.array(gateRefSchema).min(1).max(20),
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: z.array(gateRefSchema).max(20).optional(),
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(),
@@ -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', ['-c', 'core.hooksPath=/dev/null', 'update-ref', target, to, from], {
14
+ execFileSync('git', hostGitArgs(repoPath, ['update-ref', target, to, from]), {
15
15
  cwd: repoPath,
16
16
  timeout: timeoutMs,
17
17
  stdio: 'pipe',
@@ -681,7 +681,7 @@ export declare class Harness {
681
681
  private repair;
682
682
  status(graphId?: string): {
683
683
  budgets: {
684
- onUnknown: "stop" | "continue";
684
+ onUnknown: "continue" | "stop";
685
685
  budgets: {
686
686
  reason?: string | undefined;
687
687
  scope: import("../engine/budget.ts").BudgetScope;
@@ -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', ['merge-base', '--is-ancestor', p.resultSha, repo.targetRef], {
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
  });
@@ -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;
@@ -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: "stop" | "continue";
325
+ onUnknown: "continue" | "stop";
326
326
  budgets: {
327
327
  reason?: string | undefined;
328
328
  scope: BudgetScope;
@@ -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: "stop" | "continue";
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: "unknown" | "missing" | "schema" | "duplicate" | "not-open" | "approved-with-blocking";
561
+ reason: "schema" | "unknown" | "missing" | "duplicate" | "not-open" | "approved-with-blocking";
550
562
  message: string;
551
563
  }[] | undefined;
552
564
  transitions?: {
@@ -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.attempts())
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
  'Истёк срок владения. Повторите явно.';
@@ -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 { Engine } from './engine.ts';
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. Проверка потребителя исполняет
@@ -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.attempts().filter((a) => a.status === 'active' && a.leaseUntil <= now));
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
- : `${phase}-test:${gate.id}`;
1056
- await this.timed(attempt, stage, () => this.runOneGate(attempt, cwd, sha, phase, gate, artifacts, signal, execution, consumer));
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
- async runOneGate(attempt, cwd, sha, phase, gate, artifacts, signal, execution, consumer) {
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
- : join(artifacts, phase),
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.gate(attempt, cwd, sha, 'integration', gate, artifacts, signal, execution);
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', ['-c', 'core.hooksPath=/dev/null', 'update-ref', repo.targetRef, sha, base], { cwd: repoPath, stdio: 'pipe', timeout: Runner.refTimeoutMs });
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;