devcontour-harness 0.1.0-alpha.13 → 0.1.0-alpha.18

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 (73) hide show
  1. package/README.md +48 -2
  2. package/docs/architecture.md +77 -3
  3. package/docs/contracts.md +100 -0
  4. package/docs/quickstart.md +1 -1
  5. package/docs/release-notes/0.1.0-alpha.14.md +69 -0
  6. package/docs/release-notes/0.1.0-alpha.15.md +62 -0
  7. package/docs/release-notes/0.1.0-alpha.16.md +36 -0
  8. package/docs/release-notes/0.1.0-alpha.17.md +86 -0
  9. package/docs/release-notes/0.1.0-alpha.18.md +48 -0
  10. package/docs/review-probes.md +9 -3
  11. package/docs/review-protocol.md +9 -0
  12. package/docs/upgrade.md +16 -14
  13. package/lib/agents/cli-adapter.js +14 -10
  14. package/lib/agents/review-probe.d.ts +9 -0
  15. package/lib/agents/review-probe.js +23 -1
  16. package/lib/agents/schemas.d.ts +206 -0
  17. package/lib/agents/schemas.js +37 -1
  18. package/lib/agents/usage.d.ts +22 -0
  19. package/lib/agents/usage.js +112 -6
  20. package/lib/contracts/checks.d.ts +119 -0
  21. package/lib/contracts/checks.js +64 -1
  22. package/lib/contracts/cli.d.ts +30 -0
  23. package/lib/contracts/diagnosis.d.ts +3 -1
  24. package/lib/contracts/diagnosis.js +2 -0
  25. package/lib/contracts/events.d.ts +12 -0
  26. package/lib/contracts/execution.d.ts +109 -5
  27. package/lib/contracts/execution.js +58 -0
  28. package/lib/contracts/failure.d.ts +17 -3
  29. package/lib/contracts/failure.js +4 -2
  30. package/lib/contracts/published.d.ts +67 -1
  31. package/lib/contracts/repair-policy.d.ts +7 -0
  32. package/lib/contracts/repair-policy.js +17 -0
  33. package/lib/contracts/review.d.ts +12 -0
  34. package/lib/contracts/review.js +19 -0
  35. package/lib/control/harness.d.ts +2 -2
  36. package/lib/control/harness.js +2 -1
  37. package/lib/control/recovery.d.ts +26 -1
  38. package/lib/engine/engine.d.ts +41 -14
  39. package/lib/engine/engine.js +92 -8
  40. package/lib/engine/failure.d.ts +1 -1
  41. package/lib/engine/failure.js +1 -1
  42. package/lib/engine/property-recovery.d.ts +1 -0
  43. package/lib/engine/property-recovery.js +4 -1
  44. package/lib/engine/repair.d.ts +26 -0
  45. package/lib/engine/repair.js +83 -1
  46. package/lib/engine/runner.d.ts +7 -0
  47. package/lib/engine/runner.js +207 -34
  48. package/lib/explain/state.js +4 -2
  49. package/lib/index.d.ts +6 -4
  50. package/lib/index.js +6 -4
  51. package/lib/projections/stop.d.ts +1 -1
  52. package/lib/projections/stop.js +16 -0
  53. package/lib/storage/document.d.ts +48 -8
  54. package/lib/verification/acceptance.js +2 -1
  55. package/lib/verification/already-satisfied.d.ts +44 -0
  56. package/lib/verification/already-satisfied.js +49 -0
  57. package/lib/verification/checks.d.ts +28 -0
  58. package/lib/verification/checks.js +270 -22
  59. package/lib/verification/corpus.d.ts +50 -0
  60. package/lib/verification/gate-scope.d.ts +36 -0
  61. package/lib/verification/gate-scope.js +97 -0
  62. package/lib/verification/gates-at.d.ts +67 -0
  63. package/lib/verification/gates-at.js +173 -0
  64. package/lib/verification/gates.d.ts +5 -0
  65. package/lib/verification/gates.js +20 -5
  66. package/lib/verification/property.d.ts +9 -0
  67. package/lib/verification/property.js +11 -2
  68. package/lib/verification/self-check.js +12 -12
  69. package/package.json +1 -1
  70. package/schemas/contract-document.schema.json +161 -0
  71. package/schemas/execution-document.schema.json +134 -0
  72. package/schemas/run-event.schema.json +4 -0
  73. package/schemas/stop-diagnosis.schema.json +2 -0
@@ -7,11 +7,13 @@ import { roleBinding, selectReviewer, mcpEnvironment, HarnessError, digestOf, co
7
7
  import { git } from "../environment/process.js";
8
8
  import { executionEnvironment, runSteps, withEnvironment } from "../environment/lifecycle.js";
9
9
  import { redactDeep } from "../environment/redaction.js";
10
- import { implementationOutputSchema, reviewOutputSchema } from "../agents/schemas.js";
10
+ import { alreadySatisfiedReviewOutputSchema, implementationOutputSchema, reviewOutputSchema, } from "../agents/schemas.js";
11
11
  import { reviewInspection } from "../agents/inspect.js";
12
12
  import { reviewerRunsChecks } from "../agents/cli-adapter.js";
13
13
  import { gateProbePrompt, observeReproductions, probeCommand, probeGates, probeLimits, probeRecord, readProbeJournal, recoverProbeLog, probeTool, } from "../agents/review-probe.js";
14
14
  import { runGate } from "../verification/gates.js";
15
+ import { orderedScope } from "../verification/gate-scope.js";
16
+ import { alreadySatisfiedVerdict, contractCheckGates, withinWriteScope, } from "../verification/already-satisfied.js";
15
17
  import { assertDependencies, dependencyFreshness, removeSnapshots, snapshotDependencies, } from "../verification/dependencies.js";
16
18
  import { CleanupFailure, cleanupEnvironment } from "../environment/lifecycle.js";
17
19
  import { defaultResourceDatabase, withResources } from "../environment/resources.js";
@@ -19,31 +21,20 @@ import { REQUIREMENT_GATE, assertRequirements } from "../verification/requiremen
19
21
  import { BlockedError, classifyFailure } from "./classify.js";
20
22
  import { TaskFailure } from "./failure.js";
21
23
  import { prompt } from "./prompt.js";
22
- import { contractDocuments, orderCheckManifest } from "../verification/checks.js";
24
+ import { contractDocuments, orderCheckManifest, referenceCodeNote, referenceSetFiles, setFileReference, } from "../verification/checks.js";
23
25
  import { CorpusStore, localRoots } from "../verification/corpus.js";
24
26
  import { submitCase } from "../verification/confirm.js";
25
27
  import { runSpike } from "../verification/spike.js";
26
- import { propertySeed } from "../verification/property.js";
28
+ import { expectsReference, propertySeed } from "../verification/property.js";
27
29
  import { selfCheckNote, writeSelfCheck } from "../verification/self-check.js";
28
30
  import { measuredInvoke } from "./measured.js";
29
31
  import { continuableDraft, implementationBasis } from "./reuse.js";
30
- const ordered = (gates, ids) => {
31
- const byId = new Map(gates.map((g) => [g.id, g]));
32
- const out = [];
33
- const seen = new Set();
34
- const visit = (id) => {
35
- // Псевдо-gate (актуальность требований) проверяется отдельно, а не командой.
36
- if (seen.has(id) || !byId.has(id))
37
- return;
38
- seen.add(id);
39
- for (const dep of byId.get(id)?.dependsOn ?? [])
40
- if (ids.includes(dep))
41
- visit(dep);
42
- out.push(byId.get(id));
43
- };
44
- ids.forEach(visit);
45
- return out;
46
- };
32
+ /** Честный путь исполнителя, когда работу уже сделала зависимость. */
33
+ const ALREADY_SATISFIED_WRITER_NOTE = 'Implement the task. Only if the work is verifiably already present on the base commit — a dependency of this task already changed the files of its write scope, or the approved check contract of this task already passes — leave the tree unchanged and report completed: true, naming the commit and the files in the summary. An unchanged tree is never accepted on a generic passing test alone: without such a basis it stops for the lead, and a wrong claim is an empty result.';
34
+ /** Указание reviewer попытки без изменений (кандидат — база). */
35
+ const ALREADY_SATISFIED_REVIEW_NOTE = 'The writer made no changes: the candidate is the base commit itself. Approve only if the acceptance criteria are already satisfied on it (for example, by a dependency). Then set "alreadySatisfied" to the ids of the gates of this task whose passing evidence on this SHA shows it — at least one test gate and every gate of the approved check contracts — and "evidence" to what in that evidence and in the code proves each criterion. Otherwise set approved: false and alreadySatisfied: null; the attempt is then an empty result.';
36
+ /** Порядок области gates — общий с проверкой на SHA вне попытки (`orderedScope`). */
37
+ const ordered = orderedScope;
47
38
  const within = (path, prefixes) => prefixes.some((p) => p === '.' || path === p || path.startsWith(p.endsWith('/') ? p : p + '/'));
48
39
  /**
49
40
  * Исполнитель очереди. Один действующий controller; попытка проходит
@@ -254,8 +245,12 @@ export class Runner {
254
245
  /** Штатная остановка: пауза `shutdown`, прерывание попыток, освобождение роли controller. */
255
246
  async stop() {
256
247
  this.engine.pause('shutdown');
257
- for (const job of this.jobs.values())
248
+ // Прерванная остановкой попытка не упала: поручение вернётся в очередь
249
+ // и продолжится с кандидата или черновика (см. catch в `execute`).
250
+ for (const job of this.jobs.values()) {
251
+ job.shutdown = true;
258
252
  job.controller.abort();
253
+ }
259
254
  await Promise.all([...this.jobs.values()].map((j) => j.promise));
260
255
  this.release();
261
256
  }
@@ -371,7 +366,7 @@ export class Runner {
371
366
  binding,
372
367
  cwd: input.cwd,
373
368
  prompt: input.prompt,
374
- outputSchema: review ? reviewOutputSchema : implementationOutputSchema,
369
+ outputSchema: input.outputSchema ?? (review ? reviewOutputSchema : implementationOutputSchema),
375
370
  session: { kind: 'fresh' },
376
371
  sandbox: {
377
372
  mode: this.policy.isolation.mode,
@@ -465,7 +460,9 @@ export class Runner {
465
460
  return adapter;
466
461
  }
467
462
  /** Область записи кандидата: защищённые пути, пустой результат, выход за область поручения и роли. */
468
- async assertScope(work, repo, cwd, context, range) {
463
+ async assertScope(work, repo, cwd, context, range,
464
+ /** Пустой результат допустим (возможное «приёмка уже выполнена»): вернуть true вместо отказа. */
465
+ allowEmpty = false) {
469
466
  const changed = (await (range
470
467
  ? git(cwd, 'diff', '--no-renames', '--name-only', '-z', ...range)
471
468
  : git(cwd, 'diff', '--cached', '--no-renames', '--name-only', '-z')))
@@ -491,12 +488,34 @@ export class Runner {
491
488
  const forbidden = changed.filter((p) => within(p, protectedList));
492
489
  if (forbidden.length)
493
490
  throw new TaskFailure('scope-violation', `Изменены защищённые файлы: ${forbidden.join(', ')}`);
494
- if (!changed.length)
491
+ // Ответы эталонных наборов лежат в рабочем дереве: изменённый код,
492
+ // который называет файл набора строковым литералом, читает эталон, а не
493
+ // решает. Тесты исполнителя (не артефакты контракта) проверяются так же:
494
+ // через них ответы тоже можно подать реализации.
495
+ const sets = this.engine.read((v) => referenceSetFiles(contractDocuments(work.order, v), repo.id));
496
+ if (sets.length) {
497
+ const named = [];
498
+ for (const path of changed) {
499
+ const text = await git(cwd, 'show', `${range ? range[1] : ''}:${path}`).catch(() => '');
500
+ if (text.length > 4 * 1024 * 1024)
501
+ continue;
502
+ const hit = setFileReference(path, text, sets);
503
+ if (hit)
504
+ named.push(`${path} → ${hit}`);
505
+ }
506
+ if (named.length)
507
+ throw new TaskFailure('scope-violation', `Реализация ссылается на файлы эталонных наборов: ${named.join(', ')}`);
508
+ }
509
+ if (!changed.length) {
510
+ if (allowEmpty)
511
+ return true;
495
512
  throw new TaskFailure('empty-result', 'Изменений нет: поручение отчиталось о выполнении пустым коммитом');
513
+ }
496
514
  const role = roleBinding(this.policy, work.order.role, work.order.repositoryId)?.writePaths;
497
515
  const outside = changed.filter((p) => !within(p, work.order.writeScope) || (role && !within(p, role)));
498
516
  if (outside.length)
499
517
  throw new TaskFailure('scope-violation', 'Изменены файлы вне области поручения/роли: ' + outside.join(', '));
518
+ return false;
500
519
  }
501
520
  /** Стадия попытки с замером: начало, конец и исход записываются под fencing. */
502
521
  async timed(attempt, stage, fn) {
@@ -757,6 +776,7 @@ export class Runner {
757
776
  notes: [
758
777
  ...this.notes(attempt, false),
759
778
  ...(await this.hostNotes(work, attempt, false)),
779
+ ALREADY_SATISFIED_WRITER_NOTE,
760
780
  ],
761
781
  // Открытые находки ревью с выданными ID — из истории
762
782
  // поручения; только когда reviewer ведёт протокол
@@ -771,14 +791,28 @@ export class Runner {
771
791
  const result = implementationResultSchema.safeParse(response.output);
772
792
  if (!result.success)
773
793
  throw new Error('Ответ исполнителя вне схемы: ' + result.error.issues[0]?.message);
794
+ // Не доделано — свой класс: повтор продолжит с черновика,
795
+ // а не остановится как неизвестный отказ.
774
796
  if (!result.data.completed)
775
- throw new Error('Исполнитель сообщил о незавершённой работе');
797
+ throw new TaskFailure('unfinished', 'Исполнитель сообщил о незавершённой работе: ' +
798
+ result.data.summary.slice(0, 500));
776
799
  this.engine.heartbeat(attempt.id, attempt.token);
777
800
  signal.throwIfAborted();
778
801
  if ((await git(cwd, 'rev-parse', 'HEAD')) !== base)
779
802
  throw new TaskFailure('scope-violation', 'Исполнитель изменил HEAD; интеграцией владеет harness');
780
803
  await git(cwd, 'add', '-A');
781
- await this.assertScope(work, repo, cwd, context);
804
+ // Пустой результат — не отказ сразу: приёмку могла уже
805
+ // выполнить зависимость. Кандидат — сама база (без
806
+ // коммита); gates задачи на ней и подтверждение reviewer
807
+ // (`alreadySatisfied`) решают, принять ли его.
808
+ if (await this.assertScope(work, repo, cwd, context, undefined, true)) {
809
+ await this.options.hooks?.implemented?.({
810
+ attempt,
811
+ output: result.data,
812
+ sha: base,
813
+ });
814
+ return base;
815
+ }
782
816
  await git(cwd, '-c', 'core.hooksPath=/dev/null', 'commit', '--no-gpg-sign', '-q', '-m', `${work.id}: ${work.order.title}`);
783
817
  const committed = await git(cwd, 'rev-parse', 'HEAD');
784
818
  await this.options.hooks?.implemented?.({
@@ -791,7 +825,13 @@ export class Runner {
791
825
  rearm();
792
826
  this.engine.phase(attempt.id, attempt.token, 'verifying', { candidateSha: sha });
793
827
  for (const gate of gates)
794
- await this.gate(attempt, cwd, sha, 'candidate', gate, artifacts, signal, execution);
828
+ await this.gate(attempt, cwd, sha, 'candidate', gate, artifacts, signal, execution).catch((error) => {
829
+ // Без изменений непрошедший gate значит одно: работа не
830
+ // сделана, — это пустой результат, а не дефект кода.
831
+ if (sha !== base || !(error instanceof TaskFailure))
832
+ throw error;
833
+ throw new TaskFailure('empty-result', 'Изменений нет, а проверки задачи на базе не проходят: ' + error.message);
834
+ });
795
835
  this.requirements(attempt, work, repoPath, cwd, sha, 'candidate');
796
836
  rearm();
797
837
  this.engine.phase(attempt.id, attempt.token, 'reviewing');
@@ -825,6 +865,32 @@ export class Runner {
825
865
  : error instanceof Error
826
866
  ? error.message
827
867
  : String(error);
868
+ // Штатная остановка оборвала попытку: что бы ни выбросила прерванная
869
+ // операция, это не отказ работы. Черновик сохраняется, поручение
870
+ // возвращается в очередь, бюджет попыток не тратится.
871
+ const shutdown = controller.signal.aborted && this.jobs.get(attempt.id)?.shutdown === true;
872
+ if (shutdown)
873
+ try {
874
+ await this.keepDraft(attempt, await this.workOf(attempt), artifacts);
875
+ const message = 'Попытку прервала штатная остановка controller; поручение возвращено в очередь и продолжится с кандидата или черновика';
876
+ const failed = this.engine.fail(attempt.id, attempt.token, message, {
877
+ kind: 'environment',
878
+ interrupted: true,
879
+ });
880
+ // Интегрированную попытку движок в очередь не вернул: это обычный
881
+ // отказ, и хост называет его причину.
882
+ if (!failed.interrupted)
883
+ await this.options.hooks?.failed?.({
884
+ attempt,
885
+ kind: 'environment',
886
+ message: 'Штатная остановка прервала интегрированную попытку; нужна проверка recovery',
887
+ });
888
+ return;
889
+ }
890
+ catch {
891
+ // Потерявшая владение попытка не публикует поздний отказ.
892
+ return;
893
+ }
828
894
  try {
829
895
  const kind = expired && !(error instanceof TaskFailure) ? 'timeout' : classifyFailure(error, message);
830
896
  // Незакоммиченная работа сохраняется при любом отказе до кандидата:
@@ -990,6 +1056,10 @@ export class Runner {
990
1056
  await this.timed(attempt, stage, () => this.runOneGate(attempt, cwd, sha, phase, gate, artifacts, signal, execution, consumer));
991
1057
  }
992
1058
  async runOneGate(attempt, cwd, sha, phase, gate, artifacts, signal, execution, consumer) {
1059
+ // Генеративный gate — всегда; gate эталонных наборов — когда манифест
1060
+ // поручения ждёт от него отчёта (без договора — прежний уровень).
1061
+ const run = this.propertyRun(attempt, gate, consumer);
1062
+ const property = gate.property || expectsReference(run.expected) ? run : undefined;
993
1063
  await runGate({
994
1064
  policy: this.policy,
995
1065
  record: (e) => this.engine.evidence(attempt.id, attempt.token, e),
@@ -1004,7 +1074,7 @@ export class Runner {
1004
1074
  execution,
1005
1075
  boundary: this.boundary(consumer?.dependencies ?? attempt.dependencies),
1006
1076
  ...(consumer ? { consumer: consumer.repositoryId } : {}),
1007
- ...(gate.property ? { property: this.propertyRun(attempt, gate, consumer) } : {}),
1077
+ ...(property ? { property } : {}),
1008
1078
  });
1009
1079
  }
1010
1080
  /**
@@ -1223,6 +1293,50 @@ export class Runner {
1223
1293
  * команды — засчитывается всем резервом, записанным до запуска, а его
1224
1294
  * пережившие процессы останавливаются (R3-F01).
1225
1295
  */
1296
+ /**
1297
+ * Решение по подтверждению «приёмка уже выполнена» (`alreadySatisfiedVerdict`).
1298
+ * Основание по зависимости controller ищет сам: зависимость поручения,
1299
+ * чей принятый результат входит в проверяемый SHA, а её собственный
1300
+ * кандидат (база → кандидат) изменил файлы области записи поручения.
1301
+ */
1302
+ async alreadySatisfied(work, repoPath, sha, gates, claim) {
1303
+ const { contractGates, dependencies } = this.engine.read((v) => ({
1304
+ contractGates: contractCheckGates(contractDocuments(work.order, v)),
1305
+ dependencies: work.order.dependsOn.flatMap((id) => {
1306
+ const w = v.work(id);
1307
+ const a = v
1308
+ .attempts(id)
1309
+ .findLast((x) => x.status === 'succeeded' && x.integrationSha === w?.resultSha);
1310
+ return w?.resultSha && a?.baseSha && a.candidateSha && a.candidateSha !== a.baseSha
1311
+ ? [{ workId: id, resultSha: w.resultSha, base: a.baseSha, candidate: a.candidateSha }]
1312
+ : [];
1313
+ }),
1314
+ }));
1315
+ let dependency;
1316
+ for (const d of dependencies) {
1317
+ const inBase = await git(repoPath, 'merge-base', '--is-ancestor', d.resultSha, sha)
1318
+ .then(() => true)
1319
+ .catch(() => false);
1320
+ if (!inBase)
1321
+ continue;
1322
+ const paths = (await git(repoPath, 'diff', '--no-renames', '--name-only', '-z', d.base, d.candidate))
1323
+ .split('\0')
1324
+ .filter((p) => p && withinWriteScope(p, work.order.writeScope))
1325
+ .slice(0, 50);
1326
+ if (paths.length) {
1327
+ dependency = { kind: 'dependency', workId: d.workId, sha: d.candidate, paths };
1328
+ break;
1329
+ }
1330
+ }
1331
+ return alreadySatisfiedVerdict({
1332
+ scope: gates,
1333
+ contractGates,
1334
+ writeScope: work.order.writeScope,
1335
+ dependsOn: work.order.dependsOn,
1336
+ claim,
1337
+ ...(dependency ? { dependency } : {}),
1338
+ });
1339
+ }
1226
1340
  async probeSpent(runs, repositoryId, phase) {
1227
1341
  let spent = 0;
1228
1342
  for (const runId of runs) {
@@ -1275,9 +1389,13 @@ export class Runner {
1275
1389
  const round = history.round + 1;
1276
1390
  // Запрос одинаков для всех вызовов логического ревью: меняется только
1277
1391
  // диагностика ошибки протокола.
1392
+ // Попытка без изменений: кандидат — сама база. Принять её можно только
1393
+ // по явному подтверждению reviewer, что приёмка уже выполнена.
1394
+ const unchanged = phase === 'candidate' ? base === sha : attempt.candidateSha === attempt.baseSha;
1278
1395
  const notes = [
1279
1396
  ...this.notes(attempt, true, sha),
1280
1397
  ...(await this.hostNotes(work, attempt, true)),
1398
+ ...(unchanged ? [ALREADY_SATISFIED_REVIEW_NOTE] : []),
1281
1399
  ];
1282
1400
  let issues = lastIssues;
1283
1401
  const stage = phase + '-review';
@@ -1340,6 +1458,7 @@ export class Runner {
1340
1458
  ...(probe ? { probe: probe.server } : {}),
1341
1459
  ...(legacy ? {} : { findings: requestFindings(history) }),
1342
1460
  timeoutMs: remaining,
1461
+ ...(unchanged ? { outputSchema: alreadySatisfiedReviewOutputSchema } : {}),
1343
1462
  }, signal);
1344
1463
  // Reviewer не меняет закреплённые зависимости, как и своё дерево.
1345
1464
  await assertDependencies(attempt.dependencies ?? []);
@@ -1481,6 +1600,7 @@ export class Runner {
1481
1600
  {
1482
1601
  property: r.property,
1483
1602
  input: r.input,
1603
+ ...(r.inputClass?.trim() ? { inputClass: r.inputClass.trim().slice(0, 500) } : {}),
1484
1604
  expected: r.expected,
1485
1605
  actual: r.actual,
1486
1606
  command: r.command,
@@ -1493,7 +1613,14 @@ export class Runner {
1493
1613
  });
1494
1614
  if (reproductions.length)
1495
1615
  this.engine.recordReproductions(attempt.id, attempt.token, sha, reproductions);
1496
- const passed = verdict.result.approved;
1616
+ // Без изменений одобрение засчитывается только с подтверждением
1617
+ // `alreadySatisfied`, которое опирается на прошедшее evidence этой фазы.
1618
+ const claim = verdict.result.alreadySatisfied ?? undefined;
1619
+ const decision = unchanged && verdict.result.approved
1620
+ ? await this.alreadySatisfied(work, this.repoPath(repo), sha, gates, claim)
1621
+ : undefined;
1622
+ const unconfirmed = decision && !decision.ok ? decision.problem : undefined;
1623
+ const passed = verdict.result.approved && !unconfirmed;
1497
1624
  // Вердикт, новые находки с выданными ID и переходы прежних — одна запись
1498
1625
  // под fencing: попытка, утратившая владение, их не сохраняет.
1499
1626
  this.engine.evidence(attempt.id, attempt.token, {
@@ -1507,7 +1634,17 @@ export class Runner {
1507
1634
  exitCode: passed ? 0 : 1,
1508
1635
  log,
1509
1636
  digest,
1510
- summary: redact(`[${inspection.mode}] ${record.summary}`).slice(0, 4000),
1637
+ summary: redact(`[${inspection.mode}] ${record.summary}` +
1638
+ (unconfirmed ? ` (не подтверждено, что приёмка уже выполнена: ${unconfirmed})` : '')).slice(0, 4000),
1639
+ ...(unchanged && passed && claim && decision?.ok
1640
+ ? {
1641
+ alreadySatisfied: {
1642
+ gates: claim.gates,
1643
+ evidence: redact(claim.evidence).slice(0, 4000),
1644
+ basis: decision.basis,
1645
+ },
1646
+ }
1647
+ : {}),
1511
1648
  findings: (record.findings ?? []).map((f, i) => ({
1512
1649
  severity: f.severity,
1513
1650
  message: f.message,
@@ -1532,10 +1669,21 @@ export class Runner {
1532
1669
  .map((f) => f.reproduction?.property)
1533
1670
  .filter((p) => !!p);
1534
1671
  const declared = new Set(work.order.properties ?? []);
1535
- throw new TaskFailure('review', 'Независимое ревью отклонило результат: ' + redact(record.summary), named.length
1672
+ // Без изменений отказ ревью — пустой результат: работа не сделана.
1673
+ if (unchanged)
1674
+ throw new TaskFailure(decision && !decision.ok ? decision.kind : 'empty-result', unconfirmed
1675
+ ? 'Изменений нет, и ревью не подтвердило, что приёмка уже выполнена: ' + unconfirmed
1676
+ : 'Изменений нет, и ревью не подтвердило, что приёмка уже выполнена: ' +
1677
+ redact(record.summary));
1678
+ // Класс входов — из замаскированной записи: он попадает в состояние.
1679
+ const classes = inputClassesOf(record.findings ?? []);
1680
+ const pairs = classifiedOf(record.findings ?? []);
1681
+ throw new TaskFailure('review', 'Независимое ревью отклонило результат: ' + redact(record.summary), named.length || classes.length
1536
1682
  ? {
1537
1683
  bound: [...new Set(named.filter((p) => declared.has(p)))],
1538
1684
  unbound: [...new Set(named.filter((p) => !declared.has(p)))],
1685
+ ...(classes.length ? { classes } : {}),
1686
+ ...(pairs.length ? { pairs } : {}),
1539
1687
  }
1540
1688
  : undefined);
1541
1689
  }
@@ -1588,12 +1736,13 @@ export class Runner {
1588
1736
  // Воспроизведения прошлых ревью — входы, на которых поручение уже
1589
1737
  // ломалось. Диагностика: gate они не расширяют.
1590
1738
  if (reproductions.length)
1591
- out.push('Reproductions recorded by earlier reviews of this work order (diagnostic, not a gate; "observed" means the controller saw the reviewer run the command). Check the change against each input: ' +
1739
+ out.push('Reproductions recorded by earlier reviews of this work order (diagnostic, not a gate; "observed" means the controller saw the reviewer run the command). Check the change against each input, and fix the whole inputClass it names rather than the single instance: ' +
1592
1740
  JSON.stringify(reproductions
1593
1741
  .slice(-20)
1594
- .map(({ property, input, expected, actual, command, observed, runs }) => ({
1742
+ .map(({ property, input, inputClass, expected, actual, command, observed, runs }) => ({
1595
1743
  property,
1596
1744
  input,
1745
+ ...(inputClass ? { inputClass } : {}),
1597
1746
  expected,
1598
1747
  actual,
1599
1748
  command,
@@ -1602,11 +1751,14 @@ export class Runner {
1602
1751
  }))));
1603
1752
  // Согласованные контракты поручения — их текстом, по закреплённому digest.
1604
1753
  const contracts = contractDocuments(v.work(attempt.workId).order, v);
1754
+ const sets = referenceCodeNote(referenceSetFiles(contracts), review);
1755
+ if (sets)
1756
+ out.push(sets);
1605
1757
  if (contracts.length)
1606
1758
  out.push('Approved contracts: ' +
1607
1759
  JSON.stringify(contracts.map((c) => ({ id: c.id, title: c.title, content: c.content }))));
1608
1760
  if (review)
1609
- out.push('When a finding fails on a concrete input, fill reproduction: property (the testId or acceptance criterion it violates), input (the exact input as JSON text), expected and actual, and command (argv) that shows it. If you ran it through the controller check tool, set run to the runId it returned and executed=true; the controller confirms the run from its own log and records the command that actually ran. Otherwise run=null and it is a hypothesis. Use null for findings without such an input.');
1761
+ out.push('When a finding fails on a concrete input, fill reproduction: property (the testId or acceptance criterion it violates; required for a blocking finding), input (the exact input as JSON text), inputClass (the class of inputs this one belongs to: the generator or family that produces such failures, e.g. "ties between equal optima with more than 6 items", not the instance itself; required for a blocking finding), expected and actual, and command (argv) that shows it. If you ran it through the controller check tool, set run to the runId it returned and executed=true; the controller confirms the run from its own log and records the command that actually ran. Otherwise run=null and it is a hypothesis. Use null for findings without such an input.');
1610
1762
  // Reviewer видит, что controller сам записал о проверяемом SHA: gates и
1611
1763
  // их итоги, а не пересказ исполнителя.
1612
1764
  if (review && sha) {
@@ -1842,3 +1994,24 @@ function startedCalls(view, repositoryId, runId, stage) {
1842
1994
  (r.outcome === 'active' ? Math.max(0, Date.now() - Date.parse(r.startedAt)) : 0)), 0),
1843
1995
  };
1844
1996
  }
1997
+ /** Свойство и класс одной блокирующей находки: оба названы. */
1998
+ function classifiedOf(findings) {
1999
+ const out = new Map();
2000
+ for (const f of findings) {
2001
+ const property = f.reproduction?.property?.trim();
2002
+ const inputClass = f.reproduction?.inputClass?.trim();
2003
+ if (f.severity !== 'blocking' || !property || !inputClass)
2004
+ continue;
2005
+ const pair = { property: property.slice(0, 200), inputClass: inputClass.slice(0, 500) };
2006
+ out.set(JSON.stringify(pair), pair);
2007
+ }
2008
+ return [...out.values()].slice(0, 20);
2009
+ }
2010
+ /** Классы входов блокирующих находок: без пустых и повторов, в пределах записи попытки. */
2011
+ function inputClassesOf(findings) {
2012
+ const classes = findings.flatMap((f) => {
2013
+ const c = f.severity === 'blocking' ? f.reproduction?.inputClass?.trim() : undefined;
2014
+ return c ? [c.slice(0, 500)] : [];
2015
+ });
2016
+ return [...new Set(classes)].slice(0, 20);
2017
+ }
@@ -1,6 +1,6 @@
1
1
  import { HarnessError } from "../contracts/errors.js";
2
- import { repairPolicy } from "../contracts/repair-policy.js";
3
- import { repairDecision } from "../engine/repair.js";
2
+ import { MAX_NEUTRAL_FAILURES, repairPolicy } from "../contracts/repair-policy.js";
3
+ import { acceptanceGap, repairDecision } from "../engine/repair.js";
4
4
  const phaseOf = {
5
5
  running: 'implementation',
6
6
  verifying: 'gates',
@@ -295,6 +295,8 @@ function outlookOf(v, graphId, repairsUsed, options, control, budgetHeld) {
295
295
  : rule.repeats + 1;
296
296
  const retry = rule.action === 'retry' &&
297
297
  identical - 1 < rule.repeats &&
298
+ !acceptanceGap(v, w) &&
299
+ (w.neutralFailures ?? 0) < MAX_NEUTRAL_FAILURES &&
298
300
  w.attempt < options.maxAttempts &&
299
301
  used + i < options.repairBudget;
300
302
  if (!retry)
package/lib/index.d.ts CHANGED
@@ -31,8 +31,8 @@ export { callDurationMs, callRecords, usageTotals } from './engine/usage.ts';
31
31
  * ожидаемая проверка поручения, сверка отчёта свойств, закреплённые версии
32
32
  * корпуса, подтверждение примера эталоном и переносимая форма корпуса.
33
33
  */
34
- export { contractDocuments, declarationProblems, corpusBindingHolds, effectiveChecks, orderCheckManifest, orderPropertyProblems, suiteBasis, } from './verification/checks.ts';
35
- export { corpusWitness, manifestDigest, propertyCoverage, propertyFailure, propertySeed, readPropertyReport, type CheckManifest, } from './verification/property.ts';
34
+ export { contractDocuments, declarationProblems, corpusBindingHolds, effectiveChecks, isReference, obligationGates, orderCheckManifest, orderPropertyProblems, referenceCodeNote, referenceNote, referenceSetFiles, setFileReference, suiteBasis, } from './verification/checks.ts';
35
+ export { corpusWitness, expectsReference, manifestDigest, propertyCoverage, propertyFailure, propertySeed, readPropertyReport, type CheckManifest, } from './verification/property.ts';
36
36
  export { CorpusStore, canonicalJson, caseId, corpusFile, parseCanonical, versionOf, portableSuite, scrubLocalPaths, scrubPortable, verifySuite, type PortableSuite, } from './verification/corpus.ts';
37
37
  export { caseHooks, caseInputSchema, staleArtifacts, submitCase, type CaseRequest, type ConfirmContext, } from './verification/confirm.ts';
38
38
  export { assertResult, pinnedCorpus, propertyProofProblem, resultProblems, type ReceiverContext, type ReceiverPolicy, } from './verification/acceptance.ts';
@@ -51,18 +51,20 @@ export { builtinProfiles, builtinProfilesRoot, profilePin, profileRoles, reposit
51
51
  * маскирование и ограниченный разбор по свойству.
52
52
  */
53
53
  export { WorkflowQueue, GRAPH_STAGES, type WorkflowSubjects } from './control/workflow.ts';
54
- export { repairDecision, type RepairDecision } from './engine/repair.ts';
54
+ export { acceptanceGap, repairDecision, type AcceptanceGap, type RepairDecision, } from './engine/repair.ts';
55
55
  export { technicalReview, type TechnicalReviewInput } from './control/review.ts';
56
56
  export { JointRunner, type JointRunnerOptions } from './engine/joint-runner.ts';
57
57
  export { moveBase, fencedRefMove, type BaseMoved, type RefMove } from './control/base-move.ts';
58
58
  export { junitSummary, prepareReportPath, runCheck, runGate, sandboxBlockedHosts, type GateRun, } from './verification/gates.ts';
59
+ export { runGatesAt, type GatesAtContext, type GatesAtInput, type GatesAtResult, } from './verification/gates-at.ts';
60
+ export { explicitPrerequisites, gatePrerequisites, orderedScope, standaloneScope, } from './verification/gate-scope.ts';
59
61
  export { requirementProof, uncoveredGates, unprovenReason } from './verification/proof.ts';
60
62
  export { ReviewProbes, probeLimits, observeReproductions } from './agents/review-probe.ts';
61
63
  export { composeRedactors, outputRedactor, redactDeep, redactError, redactor, type Redactor, } from './environment/redaction.ts';
62
64
  export { closeIfBasisChanged } from './engine/property-recovery.ts';
63
65
  export { analyzeRecovery, applyCases, recoveryBrief, recoveryOutputSchema, recoveryResultSchema, } from './control/recovery.ts';
64
66
  export { BlockedError, classifyFailure, providerRefusal } from './engine/classify.ts';
65
- export { TaskFailure, failureFingerprint, repairPolicy, type RepairRule, } from './engine/failure.ts';
67
+ export { TaskFailure, failureFingerprint, repairPolicy, MAX_NEUTRAL_FAILURES, type RepairRule, } from './engine/failure.ts';
66
68
  export * from './projections/index.ts';
67
69
  export { inspectPath, checkDestination, listRegularFiles, planFileWrites, applyFileWrites, type FileConflict, type FileIntent, type FileWriteInput, type FilePlanEntry, type FilePlan, } from './setup/files.ts';
68
70
  export { initProject, readProjectManifest, defaultStateDir, projectIdOf, type InitOptions, type InitData, } from './init/project.ts';
package/lib/index.js CHANGED
@@ -31,8 +31,8 @@ export { callDurationMs, callRecords, usageTotals } from "./engine/usage.js";
31
31
  * ожидаемая проверка поручения, сверка отчёта свойств, закреплённые версии
32
32
  * корпуса, подтверждение примера эталоном и переносимая форма корпуса.
33
33
  */
34
- export { contractDocuments, declarationProblems, corpusBindingHolds, effectiveChecks, orderCheckManifest, orderPropertyProblems, suiteBasis, } from "./verification/checks.js";
35
- export { corpusWitness, manifestDigest, propertyCoverage, propertyFailure, propertySeed, readPropertyReport, } from "./verification/property.js";
34
+ export { contractDocuments, declarationProblems, corpusBindingHolds, effectiveChecks, isReference, obligationGates, orderCheckManifest, orderPropertyProblems, referenceCodeNote, referenceNote, referenceSetFiles, setFileReference, suiteBasis, } from "./verification/checks.js";
35
+ export { corpusWitness, expectsReference, manifestDigest, propertyCoverage, propertyFailure, propertySeed, readPropertyReport, } from "./verification/property.js";
36
36
  export { CorpusStore, canonicalJson, caseId, corpusFile, parseCanonical, versionOf, portableSuite, scrubLocalPaths, scrubPortable, verifySuite, } from "./verification/corpus.js";
37
37
  export { caseHooks, caseInputSchema, staleArtifacts, submitCase, } from "./verification/confirm.js";
38
38
  export { assertResult, pinnedCorpus, propertyProofProblem, resultProblems, } from "./verification/acceptance.js";
@@ -51,11 +51,13 @@ export { builtinProfiles, builtinProfilesRoot, profilePin, profileRoles, reposit
51
51
  * маскирование и ограниченный разбор по свойству.
52
52
  */
53
53
  export { WorkflowQueue, GRAPH_STAGES } from "./control/workflow.js";
54
- export { repairDecision } from "./engine/repair.js";
54
+ export { acceptanceGap, repairDecision, } from "./engine/repair.js";
55
55
  export { technicalReview } from "./control/review.js";
56
56
  export { JointRunner } from "./engine/joint-runner.js";
57
57
  export { moveBase, fencedRefMove } from "./control/base-move.js";
58
58
  export { junitSummary, prepareReportPath, runCheck, runGate, sandboxBlockedHosts, } from "./verification/gates.js";
59
+ export { runGatesAt, } from "./verification/gates-at.js";
60
+ export { explicitPrerequisites, gatePrerequisites, orderedScope, standaloneScope, } from "./verification/gate-scope.js";
59
61
  export { requirementProof, uncoveredGates, unprovenReason } from "./verification/proof.js";
60
62
  export { ReviewProbes, probeLimits, observeReproductions } from "./agents/review-probe.js";
61
63
  export { composeRedactors, outputRedactor, redactDeep, redactError, redactor, } from "./environment/redaction.js";
@@ -64,7 +66,7 @@ export { analyzeRecovery, applyCases, recoveryBrief, recoveryOutputSchema, recov
64
66
  // Классы отказов и правила отказов: хост бросает их из своих хуков и
65
67
  // читает класс отказа попытки теми же правилами, что и движок.
66
68
  export { BlockedError, classifyFailure, providerRefusal } from "./engine/classify.js";
67
- export { TaskFailure, failureFingerprint, repairPolicy, } from "./engine/failure.js";
69
+ export { TaskFailure, failureFingerprint, repairPolicy, MAX_NEUTRAL_FAILURES, } from "./engine/failure.js";
68
70
  export * from "./projections/index.js";
69
71
  export { inspectPath, checkDestination, listRegularFiles, planFileWrites, applyFileWrites, } from "./setup/files.js";
70
72
  export { initProject, readProjectManifest, defaultStateDir, projectIdOf, } from "./init/project.js";
@@ -6,7 +6,7 @@ import type { Actor, FailureKind, NextAction, PauseReason, WorkStatus } from './
6
6
  * (его ставит classifyFailure в точке отказа), текст здесь не разбирается.
7
7
  */
8
8
  export declare function stopCodeOf(kind: FailureKind | undefined): ErrorCode;
9
- export declare const stopCauses: readonly ["provider-auth", "environment", "timeout", "stale-base", "empty-result", "scope-violation", "gate", "review", "review-protocol", "merge", "cancelled", "budget", "unknown", "runtime-missing", "controller-busy", "operator-pause", "approval-required", "not-ready"];
9
+ export declare const stopCauses: readonly ["provider-auth", "environment", "timeout", "stale-base", "empty-result", "unfinished", "scope-violation", "gate", "review", "review-protocol", "merge", "cancelled", "budget", "needs-lead", "unknown", "runtime-missing", "controller-busy", "operator-pause", "approval-required", "not-ready"];
10
10
  export type StopCause = (typeof stopCauses)[number];
11
11
  export declare const stopPhases: readonly ["preflight", "claim", "implementation", "gates", "review", "integration", "delivery"];
12
12
  export type StopPhase = (typeof stopPhases)[number];
@@ -13,6 +13,8 @@ export function stopCodeOf(kind) {
13
13
  return 'GATE_FAILED';
14
14
  case 'review':
15
15
  return 'REVIEW_REJECTED';
16
+ case 'needs-lead':
17
+ return 'APPROVAL_REQUIRED';
16
18
  case 'review-protocol':
17
19
  return 'REVIEW_PROTOCOL_INVALID';
18
20
  case 'scope-violation':
@@ -38,6 +40,7 @@ export const stopCauses = [
38
40
  'timeout',
39
41
  'stale-base',
40
42
  'empty-result',
43
+ 'unfinished',
41
44
  'scope-violation',
42
45
  'gate',
43
46
  'review',
@@ -45,6 +48,7 @@ export const stopCauses = [
45
48
  'merge',
46
49
  'cancelled',
47
50
  'budget',
51
+ 'needs-lead',
48
52
  'unknown',
49
53
  'runtime-missing',
50
54
  'controller-busy',
@@ -68,6 +72,7 @@ const causeText = {
68
72
  timeout: 'Попытка исчерпала отведённое время.',
69
73
  'stale-base': 'База прогонов отстала от рабочей ветки.',
70
74
  'empty-result': 'Исполнитель отчитался о выполнении, не изменив ни одного файла.',
75
+ unfinished: 'Исполнитель не доделал работу и сообщил об этом.',
71
76
  'scope-violation': 'Изменения вышли за область поручения или роли.',
72
77
  gate: 'Настроенная проверка не прошла.',
73
78
  review: 'Независимое ревью отклонило результат.',
@@ -75,6 +80,7 @@ const causeText = {
75
80
  merge: 'Слияние с веткой интеграции не сошлось.',
76
81
  cancelled: 'Работа отменена человеком.',
77
82
  budget: 'Бюджет расходов или времени исчерпан либо не определён.',
83
+ 'needs-lead': 'Пустой результат без механического основания считать работу сделанной: решение ведущего.',
78
84
  unknown: 'Класс отказа не определён.',
79
85
  'runtime-missing': 'Назначенный политикой runtime не найден или не отвечает на --version.',
80
86
  'controller-busy': 'Очередь ведёт другой процесс controller.',
@@ -89,6 +95,8 @@ const failurePhase = {
89
95
  timeout: 'implementation',
90
96
  unknown: 'implementation',
91
97
  'empty-result': 'implementation',
98
+ unfinished: 'implementation',
99
+ 'needs-lead': 'review',
92
100
  gate: 'gates',
93
101
  review: 'review',
94
102
  'review-protocol': 'review',
@@ -262,6 +270,14 @@ function workOutcome(input, retry) {
262
270
  actor: 'user',
263
271
  action: 'fix-environment',
264
272
  };
273
+ case 'needs-lead':
274
+ return {
275
+ code: stopCodeOf(kind),
276
+ cause: kind,
277
+ phase,
278
+ actor: 'lead',
279
+ action: 'diagnose-failure',
280
+ };
265
281
  case 'review-protocol':
266
282
  case 'unknown':
267
283
  return {