devcontour-harness 0.1.0-alpha.16 → 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 (85) hide show
  1. package/README.md +28 -2
  2. package/docs/architecture.md +119 -3
  3. package/docs/contracts.md +3 -0
  4. package/docs/quickstart.md +1 -1
  5. package/docs/release-notes/0.1.0-alpha.17.md +86 -0
  6. package/docs/release-notes/0.1.0-alpha.18.md +48 -0
  7. package/docs/release-notes/0.1.0-alpha.19.md +96 -0
  8. package/docs/review-probes.md +9 -3
  9. package/docs/review-protocol.md +9 -0
  10. package/docs/upgrade.md +17 -15
  11. package/lib/agents/cli-adapter.js +14 -10
  12. package/lib/agents/review-probe.d.ts +9 -0
  13. package/lib/agents/review-probe.js +23 -1
  14. package/lib/agents/schemas.d.ts +206 -0
  15. package/lib/agents/schemas.js +37 -1
  16. package/lib/agents/usage.d.ts +22 -0
  17. package/lib/agents/usage.js +112 -6
  18. package/lib/cli.js +7 -1
  19. package/lib/contracts/cli.d.ts +60 -2
  20. package/lib/contracts/diagnosis.d.ts +3 -1
  21. package/lib/contracts/diagnosis.js +2 -0
  22. package/lib/contracts/events.d.ts +33 -0
  23. package/lib/contracts/events.js +7 -0
  24. package/lib/contracts/execution.d.ts +105 -8
  25. package/lib/contracts/execution.js +71 -1
  26. package/lib/contracts/failure.d.ts +17 -3
  27. package/lib/contracts/failure.js +4 -2
  28. package/lib/contracts/planning.js +2 -2
  29. package/lib/contracts/policy.d.ts +1 -1
  30. package/lib/contracts/ports.d.ts +6 -0
  31. package/lib/contracts/primitives.d.ts +13 -0
  32. package/lib/contracts/primitives.js +15 -0
  33. package/lib/contracts/profiles.js +2 -2
  34. package/lib/contracts/published.d.ts +62 -3
  35. package/lib/contracts/repair-policy.d.ts +7 -0
  36. package/lib/contracts/repair-policy.js +17 -0
  37. package/lib/contracts/review.d.ts +14 -2
  38. package/lib/contracts/review.js +20 -1
  39. package/lib/contracts/work.js +3 -3
  40. package/lib/control/base-move.js +2 -2
  41. package/lib/control/harness.d.ts +3 -3
  42. package/lib/control/harness.js +7 -2
  43. package/lib/control/recovery.d.ts +1 -1
  44. package/lib/control/workflow.js +6 -0
  45. package/lib/engine/budget.d.ts +1 -1
  46. package/lib/engine/engine.d.ts +53 -14
  47. package/lib/engine/engine.js +132 -9
  48. package/lib/engine/failure.d.ts +1 -1
  49. package/lib/engine/failure.js +1 -1
  50. package/lib/engine/property-recovery.d.ts +1 -0
  51. package/lib/engine/property-recovery.js +4 -1
  52. package/lib/engine/repair.d.ts +26 -0
  53. package/lib/engine/repair.js +83 -1
  54. package/lib/engine/runner.d.ts +18 -1
  55. package/lib/engine/runner.js +261 -42
  56. package/lib/environment/git-trust.d.ts +36 -0
  57. package/lib/environment/git-trust.js +174 -0
  58. package/lib/environment/isolation.d.ts +103 -1
  59. package/lib/environment/isolation.js +340 -5
  60. package/lib/environment/process.d.ts +5 -0
  61. package/lib/environment/process.js +20 -1
  62. package/lib/explain/state.js +4 -2
  63. package/lib/index.d.ts +6 -2
  64. package/lib/index.js +8 -2
  65. package/lib/integrations/connector/config.js +2 -2
  66. package/lib/projections/stop.d.ts +1 -1
  67. package/lib/projections/stop.js +16 -0
  68. package/lib/storage/document.d.ts +58 -10
  69. package/lib/verification/already-satisfied.d.ts +44 -0
  70. package/lib/verification/already-satisfied.js +49 -0
  71. package/lib/verification/confirm.js +2 -2
  72. package/lib/verification/gate-scope.d.ts +36 -0
  73. package/lib/verification/gate-scope.js +97 -0
  74. package/lib/verification/gates-at.d.ts +67 -0
  75. package/lib/verification/gates-at.js +173 -0
  76. package/lib/verification/gates.d.ts +17 -1
  77. package/lib/verification/gates.js +87 -9
  78. package/lib/verification/requirements.js +6 -1
  79. package/package.json +1 -1
  80. package/schemas/execution-document.schema.json +160 -4
  81. package/schemas/plan-draft.schema.json +2 -2
  82. package/schemas/run-event.schema.json +26 -0
  83. package/schemas/stop-diagnosis.schema.json +2 -0
  84. package/schemas/work-graph.schema.json +2 -2
  85. package/schemas/work-order.schema.json +2 -2
package/docs/upgrade.md CHANGED
@@ -21,21 +21,23 @@
21
21
 
22
22
  ## Матрица форматов
23
23
 
24
- | Состояние, записанное… | Читает 0.1.0-alpha.1 | Читает 0.1.0-alpha.2 | Читает 0.1.0-alpha.6 | Читает 0.1.0-alpha.7 | Читает 0.1.0-alpha.8 | Читает 0.1.0-alpha.9 | Читает 0.1.0-alpha.10 | Читает 0.1.0-alpha.11 | Читает 0.1.0-alpha.12 | Читает 0.1.0-alpha.13 | Читает 0.1.0-alpha.14 | Читает 0.1.0-alpha.15 | Читает 0.1.0-alpha.16 | Пишет поверх | Примечание |
25
- | ------------------------------------------------------------------------------ | ------------------------ | ------------------------ | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | --------------------- | --------------------- | --------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
26
- | standalone `stateVersion: 1` версией alpha.1 | да | да | да | да | да | да | да | да | да | да | да | да | да | да | одна база SQLite: документ, события, локальные записи |
27
- | хост, центральная база | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да | данные не переносятся; новые поля необязательны |
28
- | хост, базы компонентов | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да | нужна копия всех баз при остановленных writers |
29
- | alpha.2 после штатной остановки controller | да | да | да | да | да | да | да | да | да | да | да | да | да | да | роль controller снята — нового поля в состоянии нет |
30
- | alpha.2 с живой или брошенной ролью controller | **нет**, отказ до записи | да | да | да | да | да | да | да | да | да | да | да | да | — | роль хранит `engineVersion`; строгая схема alpha.1 его не знает → откат через копию |
31
- | alpha.4 с совместной проверкой | **нет**, отказ до записи | **нет**, отказ до записи | да | да | да | да | да | да | да | да | да | да | да | — | проверка хранит `engineVersion`; прежние версии его не знают → откат через копию |
32
- | alpha.6 standalone с записями ревью нового протокола или доставкой `uncertain` | **нет**, отказ до записи | **нет**, отказ до записи | да | да | да | да | да | да | да | да | да | да | да | — | `reviewRound`, `findings[].id/status`, статус доставки `uncertain`; alpha.4/alpha.5 отказывают `STATE_VERSION_UNSUPPORTED` до записи → откат через копию |
33
- | alpha.6, хост DevContour (адаптер alpha.4) | — | — | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да | адаптер DevContour alpha.4 открывает такое состояние без отказа и пишет роль controller и события; новые поля сохраняются → для отката всё равно копия |
34
- | alpha.7 с новым `inspection` ревью кода | не проверялось | не проверялось | **нет** (строгая схема) | да | да | да | да | да | да | да | да | да | да | — | новые значения `source`, `exitCode: null`, `origins`/`context`/`excluded`; строгая схема alpha.6 их не принимает, поведение прежних версий при открытии не проверялось → откат через копию |
35
- | alpha.14 с договором проверки `reference` или `gates` обязательства | не проверялось | не проверялось | не проверялось | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | да | да | да | — | контракт хранится строгой схемой (`contract-documents`): прежние версии новых полей `gates`, `verification`, `reference` не знают → откат через копию |
36
- | неизвестная `stateVersion` | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет | `STATE_VERSION_UNSUPPORTED`, exit 4 |
37
-
38
- Столбец alpha.2 относится и к alpha.3; alpha.4 и alpha.5 читают все строки, кроме неизвестной `stateVersion` и строк alpha.6 с новыми полями (standalone — отказ до записи) и строки alpha.7. alpha.8, alpha.9, alpha.10, alpha.11, alpha.12 и alpha.13 формат состояния не меняют и читают всё, что читает alpha.7; alpha.14 и alpha.15 (локальные, не опубликованы) и alpha.16 читают всё, что читает alpha.13, и добавляют необязательные поля договора проверки (строка alpha.14). Каждая следующая версия добавляет сюда свой столбец. Отказ неизвестного
24
+ | Состояние, записанное… | Читает 0.1.0-alpha.1 | Читает 0.1.0-alpha.2 | Читает 0.1.0-alpha.6 | Читает 0.1.0-alpha.7 | Читает 0.1.0-alpha.8 | Читает 0.1.0-alpha.9 | Читает 0.1.0-alpha.10 | Читает 0.1.0-alpha.11 | Читает 0.1.0-alpha.12 | Читает 0.1.0-alpha.13 | Читает 0.1.0-alpha.14 | Читает 0.1.0-alpha.15 | Читает 0.1.0-alpha.16 | Читает 0.1.0-alpha.17 | Читает 0.1.0-alpha.18 | Читает 0.1.0-alpha.19 | Пишет поверх | Примечание |
25
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ------------------------ | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | ----------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
+ | standalone `stateVersion: 1` версией alpha.1 | да | да | да | да | да | да | да | да | да | да | да | да | да | да | да | да | да | одна база SQLite: документ, события, локальные записи |
27
+ | хост, центральная база | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да | данные не переносятся; новые поля необязательны |
28
+ | хост, базы компонентов | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да | нужна копия всех баз при остановленных writers |
29
+ | alpha.2 после штатной остановки controller | да | да | да | да | да | да | да | да | да | да | да | да | да | да | да | да | да | роль controller снята — нового поля в состоянии нет |
30
+ | alpha.2 с живой или брошенной ролью controller | **нет**, отказ до записи | да | да | да | да | да | да | да | да | да | да | да | да | да | да | да | — | роль хранит `engineVersion`; строгая схема alpha.1 его не знает → откат через копию |
31
+ | alpha.4 с совместной проверкой | **нет**, отказ до записи | **нет**, отказ до записи | да | да | да | да | да | да | да | да | да | да | да | да | да | да | — | проверка хранит `engineVersion`; прежние версии его не знают → откат через копию |
32
+ | alpha.6 standalone с записями ревью нового протокола или доставкой `uncertain` | **нет**, отказ до записи | **нет**, отказ до записи | да | да | да | да | да | да | да | да | да | да | да | да | да | да | — | `reviewRound`, `findings[].id/status`, статус доставки `uncertain`; alpha.4/alpha.5 отказывают `STATE_VERSION_UNSUPPORTED` до записи → откат через копию |
33
+ | alpha.6, хост DevContour (адаптер alpha.4) | — | — | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да (через адаптер) | да | адаптер DevContour alpha.4 открывает такое состояние без отказа и пишет роль controller и события; новые поля сохраняются → для отката всё равно копия |
34
+ | alpha.7 с новым `inspection` ревью кода | не проверялось | не проверялось | **нет** (строгая схема) | да | да | да | да | да | да | да | да | да | да | да | да | да | — | новые значения `source`, `exitCode: null`, `origins`/`context`/`excluded`; строгая схема alpha.6 их не принимает, поведение прежних версий при открытии не проверялось → откат через копию |
35
+ | alpha.14 с договором проверки `reference` или `gates` обязательства | не проверялось | не проверялось | не проверялось | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | да | да | да | да | да | да | — | контракт хранится строгой схемой (`contract-documents`): прежние версии новых полей `gates`, `verification`, `reference` не знают → откат через копию |
36
+ | alpha.17 с `interrupted`, `inputClasses`, `classified`, `neutralFailures`, `alreadySatisfied` в evidence ревью или классом отказа `unfinished`/`needs-lead` | не проверялось | не проверялось | не проверялось | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | да | да | да | — | попытка (`interrupted`, `inputClasses`, `classified`), поручение (`neutralFailures`), `alreadySatisfied` evidence ревью и классы отказа `unfinished`, `needs-lead` — новые значения строгих схем; прежние версии их отклоняют → откат через копию |
37
+ | alpha.19 с `recheck` в evidence gate или областью gates длиннее 20 | не проверялось | не проверялось | не проверялось | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | **нет** (строгая схема) | да | — | evidence gate и событие `attempt.gate` (`recheck`), области gates поручения, задачи, потребителя и `alreadySatisfied` до 100 — новые значения строгих схем; прежние версии их отклоняют → откат через копию |
38
+ | неизвестная `stateVersion` | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет | `STATE_VERSION_UNSUPPORTED`, exit 4 |
39
+
40
+ Столбец alpha.2 относится и к alpha.3; alpha.4 и alpha.5 читают все строки, кроме неизвестной `stateVersion` и строк alpha.6 с новыми полями (standalone — отказ до записи) и строки alpha.7. alpha.8, alpha.9, alpha.10, alpha.11, alpha.12 и alpha.13 формат состояния не меняют и читают всё, что читает alpha.7; alpha.14 и alpha.15 (локальные, не опубликованы) и alpha.16 читают всё, что читает alpha.13, и добавляют необязательные поля договора проверки (строка alpha.14). alpha.17 читает всё, что читает alpha.16, и добавляет необязательные поля попытки, поручения и evidence ревью и классы отказа `unfinished` и `needs-lead` (строка alpha.17); alpha.18 формат состояния не меняет и читает всё, что читает alpha.17. alpha.19 читает всё, что читает alpha.18, и добавляет необязательное поле `recheck` evidence gate и области gates до 100 (строка alpha.19). Каждая следующая версия добавляет сюда свой столбец. Отказ неизвестного
39
41
  формата до записи проверяют тесты совместимости пакета
40
42
  <!-- repo-only -->(`tests/compatibility.test.ts`)<!-- /repo-only -->.
41
43
 
@@ -8,7 +8,7 @@ import { redactor } from "../environment/redaction.js";
8
8
  import { inspectReview } from "./inspect.js";
9
9
  import { probeCallMs, probeServer, probeTool } from "./review-probe.js";
10
10
  import { claudeResult, runtimeEvents } from "./runtime-events.js";
11
- import { parseUsage, reportedModel, runtimeVersion, unknownUsage } from "./usage.js";
11
+ import { runtimeVersion, unknownUsage, usageStream } from "./usage.js";
12
12
  /** Встроенные инструменты исполнителя claude, когда профиль их не называет; shell — в песочнице ОС. */
13
13
  export const claudeWriterTools = ['Read', 'Glob', 'Grep', 'Edit', 'Write', 'Bash'];
14
14
  const writerTools = (profile) => profile?.claudeTools ?? claudeWriterTools;
@@ -283,12 +283,17 @@ export function cliAdapter(runtime, options = {}) {
283
283
  // короткий секрет портит `item.completed`), текст полей маскируется
284
284
  // при разборе; сырой поток только в памяти и не сохраняется.
285
285
  let raw = '';
286
+ // Телеметрия — по всему потоку, независимо от пределов журнала и
287
+ // хвоста stdout: итоговое событие долгого вызова лежит за ними.
288
+ const telemetry = usageStream(runtime);
286
289
  try {
287
290
  result = await command(argv, r.cwd, {
288
291
  contain: { dirs: review ? [] : r.sandbox.writable },
289
- ...(review
290
- ? { onRawStdout: (text) => void (raw = (raw + text).slice(-2_000_000)) }
291
- : {}),
292
+ onRawStdout: (text) => {
293
+ telemetry.push(text);
294
+ if (review)
295
+ raw = (raw + text).slice(-2_000_000);
296
+ },
292
297
  onOutput: (stream, text) => {
293
298
  if (logged >= 10_000_000)
294
299
  return;
@@ -312,12 +317,11 @@ export function cliAdapter(runtime, options = {}) {
312
317
  return unavailable(`${runtime}: runtime не запускается: ${String(error)}`);
313
318
  throw error;
314
319
  }
315
- await writeFile(join(dir, 'runtime.json'), JSON.stringify(result.diagnostics, null, 2));
316
- const usage = parseUsage(runtime, result.stdout, result.code === 0 &&
317
- !result.timedOut &&
318
- !signal.aborted &&
319
- result.stdout.length < 2_000_000);
320
- const model = reportedModel(result.stdout);
320
+ const usage = telemetry.usage(result.code === 0 && !result.timedOut && !signal.aborted);
321
+ const model = telemetry.model();
322
+ // Сообщённые runtime счётчики и стоимость — и в диагностике вызова:
323
+ // runtime.log ограничен и итоговой строки может не содержать.
324
+ await writeFile(join(dir, 'runtime.json'), JSON.stringify({ ...result.diagnostics, usage }, null, 2));
321
325
  const bookkeeping = ['system', 'result', 'error', 'session.created', 'thread.started'];
322
326
  const started = result.timedOut ||
323
327
  runtimeEvents(result.stdout).events.some((e) => !bookkeeping.includes(String(e.type)));
@@ -118,6 +118,15 @@ export type ProbeGateRun = {
118
118
  /** Имена переменных, которые выставил controller (значения не пишутся). */
119
119
  controllerEnv: string[];
120
120
  report?: ProbeReport;
121
+ /**
122
+ * Копия отчёта свойств, если проверка записала его по пути
123
+ * `DEVCONTOUR_PROPERTY_REPORT` в scratch. Только evidence probe: движок
124
+ * этот отчёт не читает, и свойства по нему не засчитываются.
125
+ */
126
+ propertyReport?: {
127
+ path: string;
128
+ scope: 'probe-only';
129
+ };
121
130
  /** Каталог evidence запуска в каталоге ревью controller. */
122
131
  artifacts?: string;
123
132
  /** Временный каталог отчёта; убран после сохранения evidence. */
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  import { z } from 'zod';
10
10
  import { idSchema, relativePathSchema, sha256, } from "../contracts/index.js";
11
11
  import { redactor } from "../environment/redaction.js";
12
- import { completedCheck, freshnessEnv, junitSummary, runCheck, sandboxBlockedHosts, } from "../verification/gates.js";
12
+ import { completedCheck, freshnessEnv, propertyReportEnv, junitSummary, runCheck, sandboxBlockedHosts, } from "../verification/gates.js";
13
13
  /**
14
14
  * Пробные проверки reviewer под пределами harness.
15
15
  *
@@ -112,6 +112,21 @@ export function fixedReportArgument(argv, cwd, report) {
112
112
  }
113
113
  return undefined;
114
114
  }
115
+ /**
116
+ * Копия отчёта свойств из scratch в evidence probe. Как и JUnit: symlink,
117
+ * жёсткая ссылка, файл вне scratch и слишком большой файл не читаются.
118
+ */
119
+ async function savePropertyReport(path, scratch, runDir, id, redact) {
120
+ const stat = await lstat(path).catch(() => undefined);
121
+ if (!stat?.isFile() || stat.nlink > 1 || stat.size > REPORT_LIMIT)
122
+ return undefined;
123
+ const real = await realpath(path);
124
+ if (!within(dirname(real), scratch))
125
+ return undefined;
126
+ const saved = join(runDir, `${id}.property.json`);
127
+ await writeFile(saved, redact(await readFile(real, 'utf8')), { flag: 'wx' });
128
+ return { path: saved, scope: 'probe-only' };
129
+ }
115
130
  /**
116
131
  * Отчёт запуска gate в scratch controller. Symlink, жёсткая ссылка и путь вне
117
132
  * scratch не читаются: так отчёт подменялся бы чужим файлом — прежним
@@ -458,12 +473,18 @@ export class ReviewProbes {
458
473
  }
459
474
  meta.scratch = scratch;
460
475
  const reportPath = join(scratch, `${gate.id}.xml`);
476
+ // Отчёт свойств — тоже в scratch, под тем же именем переменной, что и у
477
+ // gate движка (`propertyReportEnv`): проверка, пишущая отчёт свойств по
478
+ // прежнему пути в дереве, под probe получала бы EPERM. Seed, селектор и
479
+ // корпус probe не выставляет: генеративные gate он не исполняет.
480
+ const propertyPath = join(scratch, `${gate.id}.property.json`);
461
481
  const controllerEnv = {
462
482
  CI: '1',
463
483
  ...freshnessEnv(gate.kind, env),
464
484
  ...(gate.report
465
485
  ? { DEVCONTOUR_REPORT_PATH: reportPath, HARNESS_REPORT_PATH: reportPath }
466
486
  : {}),
487
+ [propertyReportEnv]: propertyPath,
467
488
  };
468
489
  meta.controllerEnv = Object.keys(controllerEnv);
469
490
  const started = Date.now();
@@ -493,6 +514,7 @@ export class ReviewProbes {
493
514
  const read = gate.report
494
515
  ? await inspectReport(reportPath, scratch, started, redact)
495
516
  : undefined;
517
+ meta.propertyReport = await savePropertyReport(propertyPath, scratch, runDir, gate.id, redact);
496
518
  if (read) {
497
519
  meta.report = read.report;
498
520
  if (read.xml !== undefined) {
@@ -164,6 +164,9 @@ export declare const reviewOutputSchema: {
164
164
  input: {
165
165
  type: string;
166
166
  };
167
+ inputClass: {
168
+ type: string[];
169
+ };
167
170
  expected: {
168
171
  type: string[];
169
172
  };
@@ -215,3 +218,206 @@ export declare const reviewOutputSchema: {
215
218
  };
216
219
  required: string[];
217
220
  };
221
+ /**
222
+ * Ответ reviewer попытки без изменений: тот же протокол и обязательное
223
+ * `alreadySatisfied` (`null` — подтверждения нет). Обычное ревью этого поля
224
+ * не получает: его вид ответа не меняется.
225
+ */
226
+ export declare const alreadySatisfiedReviewOutputSchema: {
227
+ properties: {
228
+ alreadySatisfied: {
229
+ anyOf: ({
230
+ type: string;
231
+ additionalProperties?: undefined;
232
+ properties?: undefined;
233
+ required?: undefined;
234
+ } | {
235
+ type: string;
236
+ additionalProperties: boolean;
237
+ properties: {
238
+ gates: {
239
+ type: string;
240
+ minItems: number;
241
+ maxItems: number;
242
+ items: {
243
+ type: string;
244
+ };
245
+ };
246
+ evidence: {
247
+ type: string;
248
+ };
249
+ };
250
+ required: string[];
251
+ })[];
252
+ };
253
+ execution: {
254
+ type: string;
255
+ additionalProperties: boolean;
256
+ properties: {
257
+ commands: {
258
+ type: string;
259
+ maxItems: number;
260
+ items: {
261
+ type: string;
262
+ additionalProperties: boolean;
263
+ properties: {
264
+ command: {
265
+ type: string;
266
+ };
267
+ exitCode: {
268
+ type: string;
269
+ };
270
+ summary: {
271
+ type: string;
272
+ };
273
+ };
274
+ required: string[];
275
+ };
276
+ };
277
+ noCommandsReason: {
278
+ type: string[];
279
+ };
280
+ };
281
+ required: string[];
282
+ };
283
+ approved: {
284
+ type: string;
285
+ };
286
+ summary: {
287
+ type: string;
288
+ };
289
+ discoveries: {
290
+ type: string;
291
+ maxItems: number;
292
+ items: {
293
+ type: string;
294
+ additionalProperties: boolean;
295
+ properties: {
296
+ kind: {
297
+ type: string;
298
+ enum: string[];
299
+ };
300
+ title: {
301
+ type: string;
302
+ };
303
+ path: {
304
+ type: string;
305
+ };
306
+ line: {
307
+ type: string;
308
+ minimum: number;
309
+ };
310
+ observation: {
311
+ type: string;
312
+ };
313
+ consequence: {
314
+ type: string;
315
+ };
316
+ reproduction: {
317
+ type: string;
318
+ };
319
+ };
320
+ required: string[];
321
+ };
322
+ };
323
+ findings: {
324
+ type: string;
325
+ items: {
326
+ type: string;
327
+ additionalProperties: boolean;
328
+ properties: {
329
+ severity: {
330
+ type: string;
331
+ enum: string[];
332
+ };
333
+ message: {
334
+ type: string;
335
+ };
336
+ path: {
337
+ type: string[];
338
+ };
339
+ line: {
340
+ type: string[];
341
+ minimum: number;
342
+ };
343
+ rule: {
344
+ type: string[];
345
+ };
346
+ consequence: {
347
+ type: string[];
348
+ };
349
+ evidence: {
350
+ type: string[];
351
+ };
352
+ reproduction: {
353
+ anyOf: ({
354
+ type: string;
355
+ additionalProperties?: undefined;
356
+ properties?: undefined;
357
+ required?: undefined;
358
+ } | {
359
+ type: string;
360
+ additionalProperties: boolean;
361
+ properties: {
362
+ property: {
363
+ type: string[];
364
+ };
365
+ input: {
366
+ type: string;
367
+ };
368
+ inputClass: {
369
+ type: string[];
370
+ };
371
+ expected: {
372
+ type: string[];
373
+ };
374
+ actual: {
375
+ type: string[];
376
+ };
377
+ command: {
378
+ type: string[];
379
+ items: {
380
+ type: string;
381
+ };
382
+ };
383
+ executed: {
384
+ type: string;
385
+ };
386
+ run: {
387
+ type: string[];
388
+ };
389
+ };
390
+ required: string[];
391
+ })[];
392
+ };
393
+ };
394
+ required: string[];
395
+ };
396
+ };
397
+ previous: {
398
+ type: string;
399
+ items: {
400
+ type: string;
401
+ additionalProperties: boolean;
402
+ properties: {
403
+ id: {
404
+ type: string;
405
+ pattern: string;
406
+ };
407
+ status: {
408
+ type: string;
409
+ enum: ("open" | "closed" | "withdrawn")[];
410
+ };
411
+ basis: {
412
+ type: string;
413
+ pattern: string;
414
+ };
415
+ };
416
+ required: string[];
417
+ };
418
+ };
419
+ };
420
+ required: string[];
421
+ type: string;
422
+ additionalProperties: boolean;
423
+ };
@@ -80,13 +80,23 @@ export const reviewOutputSchema = {
80
80
  properties: {
81
81
  property: { type: ['string', 'null'] },
82
82
  input: { type: 'string' },
83
+ inputClass: { type: ['string', 'null'] },
83
84
  expected: { type: ['string', 'null'] },
84
85
  actual: { type: ['string', 'null'] },
85
86
  command: { type: ['array', 'null'], items: { type: 'string' } },
86
87
  executed: { type: 'boolean' },
87
88
  run: { type: ['string', 'null'] },
88
89
  },
89
- required: ['property', 'input', 'expected', 'actual', 'command', 'executed', 'run'],
90
+ required: [
91
+ 'property',
92
+ 'input',
93
+ 'inputClass',
94
+ 'expected',
95
+ 'actual',
96
+ 'command',
97
+ 'executed',
98
+ 'run',
99
+ ],
90
100
  },
91
101
  ],
92
102
  },
@@ -122,3 +132,29 @@ export const reviewOutputSchema = {
122
132
  },
123
133
  required: ['approved', 'summary', 'findings', 'discoveries', 'execution', 'previous'],
124
134
  };
135
+ /**
136
+ * Ответ reviewer попытки без изменений: тот же протокол и обязательное
137
+ * `alreadySatisfied` (`null` — подтверждения нет). Обычное ревью этого поля
138
+ * не получает: его вид ответа не меняется.
139
+ */
140
+ export const alreadySatisfiedReviewOutputSchema = {
141
+ ...reviewOutputSchema,
142
+ properties: {
143
+ ...reviewOutputSchema.properties,
144
+ alreadySatisfied: {
145
+ anyOf: [
146
+ { type: 'null' },
147
+ {
148
+ type: 'object',
149
+ additionalProperties: false,
150
+ properties: {
151
+ gates: { type: 'array', minItems: 1, maxItems: 20, items: { type: 'string' } },
152
+ evidence: { type: 'string' },
153
+ },
154
+ required: ['gates', 'evidence'],
155
+ },
156
+ ],
157
+ },
158
+ },
159
+ required: [...reviewOutputSchema.required, 'alreadySatisfied'],
160
+ };
@@ -6,3 +6,25 @@ export declare function parseUsage(runtime: 'codex' | 'claude', stdout: string,
6
6
  export declare function runtimeVersion(executable: string, env?: NodeJS.ProcessEnv): Promise<string | null>;
7
7
  /** Модель, которую runtime выбрал на самом деле: событие system у claude, ключи modelUsage у codex. */
8
8
  export declare function reportedModel(stdout: string): string | null;
9
+ /** Строка потока длиннее — не разбирается и делает телеметрию неполной. */
10
+ export declare const LINE_LIMIT = 8000000;
11
+ /**
12
+ * Телеметрия, собранная по ходу потока runtime, а не из хвоста вывода. Хвост
13
+ * stdout и журнал вызова ограничены, и у долгого вызова итоговое событие
14
+ * `result` (стоимость и счётчики claude) или ранние `turn.completed` codex
15
+ * оказывались за пределом: стоимость, сообщённая провайдером, терялась или
16
+ * учитывалась неполной. Здесь каждая строка разбирается сразу; хранятся
17
+ * только поля телеметрии — последнее событие `result`, события
18
+ * `turn.completed` и первая сообщённая модель, — а не текст вывода.
19
+ */
20
+ export declare function usageStream(runtime: 'codex' | 'claude'): {
21
+ /**
22
+ * Очередной фрагмент stdout runtime (без маски: берутся только числа).
23
+ * Вызывается из обработчика вывода процесса и не бросает: сбой разбора —
24
+ * неполная телеметрия, а не падение controller.
25
+ */
26
+ push(text: string): void;
27
+ /** Телеметрия всего потока; `completed` — вызов завершился штатно. */
28
+ usage(completed: boolean): Usage;
29
+ model: () => string | null;
30
+ };
@@ -69,15 +69,121 @@ export function runtimeVersion(executable, env) {
69
69
  .catch(() => null));
70
70
  return versions.get(key);
71
71
  }
72
+ /** Модель события runtime: событие system у claude, ключи modelUsage у codex. */
73
+ function eventModel(event) {
74
+ if (typeof event.model === 'string' && event.model && event.model !== '<synthetic>')
75
+ return event.model;
76
+ const usage = event.modelUsage;
77
+ const [name] = Object.keys(usage ?? {});
78
+ return name ?? null;
79
+ }
72
80
  /** Модель, которую runtime выбрал на самом деле: событие system у claude, ключи modelUsage у codex. */
73
81
  export function reportedModel(stdout) {
74
82
  for (const event of runtimeEvents(stdout).events) {
75
- if (typeof event.model === 'string' && event.model && event.model !== '<synthetic>')
76
- return event.model;
77
- const usage = event.modelUsage;
78
- const [name] = Object.keys(usage ?? {});
79
- if (name)
80
- return name;
83
+ const model = eventModel(event);
84
+ if (model)
85
+ return model;
81
86
  }
82
87
  return null;
83
88
  }
89
+ /** Строка потока длиннее — не разбирается и делает телеметрию неполной. */
90
+ export const LINE_LIMIT = 8_000_000;
91
+ /**
92
+ * Только числовые поля верхнего уровня `usage`: вложенная структура любой
93
+ * глубины не хранится и не сериализуется (глубокая вложенность роняла бы
94
+ * `JSON.stringify` в обработчике вывода процесса).
95
+ */
96
+ const flatUsage = (usage) => usage && typeof usage === 'object' && !Array.isArray(usage)
97
+ ? Object.fromEntries(Object.entries(usage).filter(([, v]) => typeof v === 'number' || v === null))
98
+ : usage === undefined
99
+ ? undefined
100
+ : {};
101
+ /**
102
+ * Телеметрия, собранная по ходу потока runtime, а не из хвоста вывода. Хвост
103
+ * stdout и журнал вызова ограничены, и у долгого вызова итоговое событие
104
+ * `result` (стоимость и счётчики claude) или ранние `turn.completed` codex
105
+ * оказывались за пределом: стоимость, сообщённая провайдером, терялась или
106
+ * учитывалась неполной. Здесь каждая строка разбирается сразу; хранятся
107
+ * только поля телеметрии — последнее событие `result`, события
108
+ * `turn.completed` и первая сообщённая модель, — а не текст вывода.
109
+ */
110
+ export function usageStream(runtime) {
111
+ let partial = '', overflow = false, malformed = false, model = null, result;
112
+ const turns = [];
113
+ const line = (text) => {
114
+ if (!text.trim())
115
+ return;
116
+ let event;
117
+ try {
118
+ event = JSON.parse(text);
119
+ }
120
+ catch {
121
+ malformed = true;
122
+ return;
123
+ }
124
+ if (!event || typeof event !== 'object' || Array.isArray(event)) {
125
+ malformed = true;
126
+ return;
127
+ }
128
+ const e = event;
129
+ model ??= eventModel(e);
130
+ if (runtime === 'claude' && e.type === 'result')
131
+ result = JSON.stringify({
132
+ type: e.type,
133
+ usage: flatUsage(e.usage),
134
+ total_cost_usd: typeof e.total_cost_usd === 'number' ? e.total_cost_usd : null,
135
+ });
136
+ if (runtime === 'codex' && e.type === 'turn.completed')
137
+ turns.push(JSON.stringify({ type: e.type, usage: flatUsage(e.usage) }));
138
+ };
139
+ const flush = () => {
140
+ if (overflow)
141
+ malformed = true;
142
+ else
143
+ line(partial);
144
+ partial = '';
145
+ overflow = false;
146
+ };
147
+ const append = (piece) => {
148
+ if (overflow)
149
+ return;
150
+ if (partial.length + piece.length > LINE_LIMIT) {
151
+ partial = '';
152
+ overflow = true;
153
+ }
154
+ else
155
+ partial += piece;
156
+ };
157
+ const consume = (text) => {
158
+ let start = 0;
159
+ for (let end = text.indexOf('\n'); end >= 0; end = text.indexOf('\n', start)) {
160
+ append(text.slice(start, end));
161
+ flush();
162
+ start = end + 1;
163
+ }
164
+ append(text.slice(start));
165
+ };
166
+ return {
167
+ /**
168
+ * Очередной фрагмент stdout runtime (без маски: берутся только числа).
169
+ * Вызывается из обработчика вывода процесса и не бросает: сбой разбора —
170
+ * неполная телеметрия, а не падение controller.
171
+ */
172
+ push(text) {
173
+ try {
174
+ consume(text);
175
+ }
176
+ catch {
177
+ malformed = true;
178
+ partial = '';
179
+ overflow = true;
180
+ }
181
+ },
182
+ /** Телеметрия всего потока; `completed` — вызов завершился штатно. */
183
+ usage(completed) {
184
+ flush();
185
+ return parseUsage(runtime, [...turns, ...(result ? [result] : [])].join('\n'), completed && !malformed);
186
+ },
187
+ model: () => model,
188
+ };
189
+ }