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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@ Headless-движок исполнения инженерных поручени
5
5
  интеграция с принятой веткой через compare-and-swap и машинный результат с SHA
6
6
  и доказательствами.
7
7
 
8
- > Статус: `0.1.0-alpha.12` — предварительный выпуск (alpha). Договоры
8
+ > Статус: `0.1.0-alpha.13` — предварительный выпуск (alpha). Договоры
9
9
  > `v1alpha1` могут измениться между alpha-версиями; закрепляйте точную версию.
10
10
  > Файл → ревью плана → кандидат → интеграция → принятие → результат JSON, с
11
11
  > ограниченным восстановлением, повторным использованием кандидата, бюджетами,
@@ -28,7 +28,7 @@ Headless-движок исполнения инженерных поручени
28
28
  Из npm — точная версия (воспроизводимо) или последняя alpha по тегу:
29
29
 
30
30
  ```sh
31
- npm install --save-exact devcontour-harness@0.1.0-alpha.12 # точная версия
31
+ npm install --save-exact devcontour-harness@0.1.0-alpha.13 # точная версия
32
32
  # или: npm install --save-exact devcontour-harness@alpha # текущая alpha
33
33
  npx harness --version
34
34
  ```
@@ -123,6 +123,19 @@ PASS» нет. Адаптеры хоста реализуют порты `Execut
123
123
  `devcontour-harness/schemas/<имя>.schema.json`. Декларации используют типы
124
124
  Node.js: TypeScript-потребителю нужен `@types/node` (или `skipLibCheck`).
125
125
 
126
+ ## Выпуск 0.1.0-alpha.13
127
+
128
+ Controller mode для задач Relay и установка без лишних вопросов. Подробно —
129
+ [заметки выпуска](docs/release-notes/0.1.0-alpha.13.md).
130
+
131
+ - `tasks[].context` и `contextDir` коннектора: локальные пакеты контекста
132
+ (оверлей controller-mode) закрепляются в снимке задач Relay.
133
+ - Skill `harness-setup` решает рутинные выборы сам и спрашивает только на
134
+ развилках (вендоры writer/reviewer, недостающие факты Relay, изоляция,
135
+ Docker, конфликты с правилами проекта, коммит, запись в Relay, живые
136
+ вызовы); маршрут для pnpm/Turborepo-монорепо (Turbo Starter): свой
137
+ профиль, gates из команд проекта, controller mode.
138
+
126
139
  ## Выпуск 0.1.0-alpha.12
127
140
 
128
141
  Skill установки и обновления. Подробно —
@@ -109,14 +109,40 @@ CLI: 0 — успех, 2 — неверный вход, 3 — отказ/бло
109
109
  | `projectId` | Ожидаемая идентичность проекта backend; несовпадение блокирует чтение снимка и любые мутации |
110
110
  | `actor` | Автор всех записей коннектора |
111
111
  | `stateDir` | Каталог состояния коннектора (относительно файла); без него — `--state` |
112
+ | `contextDir` | Каталог локальных пакетов контекста `tasks[].context` (относительно файла); по умолчанию `work/context` |
112
113
  | `repository` | ID репозитория политики (и путь для сверки) |
113
114
  | `tasks[]` | Задачи: `ref` (постоянный ID или ключ), роль, `writeScope`, `gates`, сопоставление критериев, обязательные документы |
114
115
  | `tasks[].criteria[]` | Критерий (`id` или точный `title`) → `gate` и `testId` JUnit; `evidence: "gate"` — достаточно прошедшего gate (явный выбор) |
116
+ | `tasks[].context` | Локальные пакеты контекста поручения — каталоги `<contextDir>/<id>` (например, `controller-mode`); закрепляются в снимке |
115
117
  | `completion` | `complete` — закрыть по правилам backend (по умолчанию в `done`); `review-only` — перевести в `review`/`column` и оставить человеку; `historicalComment` — при конфликте постановки записать явно исторический комментарий |
116
118
  | `eligibility.columns` | Колонки, из которых задача допускается к запуску (`done`/`cancelled` — никогда) |
117
119
  | `limits` | Пределы документов и постановки, число повторных чтений, срок владения доставкой |
118
120
  | `history` | Запись истории работы (корневая и дочерние сессии) |
119
121
 
122
+ ### Локальные пакеты контекста
123
+
124
+ `tasks[].context` добавляет к поручению задачи пакеты из `<contextDir>/<id>` —
125
+ те же каталоги, что у файлового графа: файлы вкладываются в запросы writer и
126
+ reviewer, `references/` закрепляется digest без вложения. Так к задачам Relay
127
+ подключается оверлей controller-mode
128
+ (`templates/controller-mode/`): `"context": ["controller-mode"]`.
129
+
130
+ - Пакеты закрепляются в `context.json` снимка; поручение получает
131
+ `context: [<контекст задачи>, ...пакеты]`. `source-snapshot` и manifest
132
+ называют их в `localContext` (ID, digest, число файлов и справок, байты).
133
+ - Digest пакетов и их распределение по задачам (какие у какой, в каком
134
+ порядке) входят в ревизию набора задач: изменённый, новый или перенесённый
135
+ пакет — новый `source-snapshot`, ревью плана и согласование (`run --source`
136
+ иначе отказывает `APPROVAL_STALE`). Semantic digest задачи от пакетов не
137
+ зависит: включение поля или изменение пакета не делает доставку уже
138
+ принятого результата конфликтом.
139
+ - Нет каталога или пакета — `NOT_READY` до чтения backend; пакет через
140
+ symlink — `INVALID_INPUT`. `source-check` называет то и другое в `problems`.
141
+ - Пределы контекста (файл, сумма, не больше 30 источников вместе с
142
+ контекстами задач) проверяются при снимке: превышение — `LIMIT_EXCEEDED`, и
143
+ задача не допускается к запуску.
144
+ - Без поля снимок, ревизия и digest задачи те же, что в прежних версиях.
145
+
120
146
  ### Идентичность и снимок
121
147
 
122
148
  - Идентичность задачи — `<provider>:<projectId>:<постоянный ID>`; ID поручения
@@ -13,7 +13,7 @@
13
13
  ```sh
14
14
  mkdir harness-demo && cd harness-demo
15
15
  npm init -y >/dev/null
16
- npm install --save-exact devcontour-harness@0.1.0-alpha.12
16
+ npm install --save-exact devcontour-harness@0.1.0-alpha.13
17
17
  npx harness --version
18
18
  npx harness doctor
19
19
  ```
@@ -0,0 +1,91 @@
1
+ # devcontour-harness 0.1.0-alpha.13
2
+
3
+ Controller mode для задач Relay и маршрут установки, который спрашивает только
4
+ на развилках. Gates, `maxAttempts`, бюджеты попыток и повторов, правила
5
+ находок, изоляция, принятие target ref и формат состояния не менялись.
6
+
7
+ ## Что нового
8
+
9
+ - **`tasks[].context` и `contextDir` коннектора.** Задача Relay получает,
10
+ кроме своего контекста, локальные пакеты из `<contextDir>/<id>` (по
11
+ умолчанию `work/context` рядом с `connector.json`) — например, оверлей
12
+ controller-mode из `templates/controller-mode/`. Пакет закрепляется в
13
+ `context.json` снимка теми же правилами, что файловый контекст графа
14
+ (`references/` — справки без вложения). Digest пакетов и их распределение
15
+ по задачам входят в ревизию набора задач: изменённый пакет требует нового `source-snapshot` и
16
+ согласования (`run --source` иначе отказывает `APPROVAL_STALE`). Semantic
17
+ digest задачи от пакетов не зависит: включение поля или изменение пакета не
18
+ делает доставку принятого результата конфликтом. Нет каталога или пакета —
19
+ `NOT_READY` до чтения backend; `source-check` называет его в `problems`
20
+ ([connectors](../connectors.md#локальные-пакеты-контекста)).
21
+ - **Пределы контекста при снимке.** `source-snapshot` проверяет пределы
22
+ снимка контекста (размер файла, сумма, не больше 30 источников) и сообщает
23
+ превышение как `LIMIT_EXCEEDED`, а не только отказом при запуске;
24
+ `source-check` проверяет пределы локальных пакетов. Вывод
25
+ `source-snapshot` и manifest называют закреплённые пакеты (`localContext`).
26
+ - **`controllerContext`** (`examples/turbo-starter/setup.mjs`) пересобирает
27
+ пакет с пустого каталога и не берёт в справки `.harness/` и сам каталог
28
+ пакета: повторная сборка внутри репозитория не вкладывает копии пакета и
29
+ не оставляет справки удалённых AGENTS.md. `upstream: null` — сборка для
30
+ обычного репозитория проекта.
31
+ - **Skill `harness-setup`:**
32
+ - таблица «решить или спросить»: рутинные выборы (ветка, каталог
33
+ установки, источник пакета, профиль, gates, согласование, completion)
34
+ агент делает сам и объясняет в отчёте; спрашивает о вендорах
35
+ writer/reviewer, ревью тем же вендором, controller mode (оверлей
36
+ главнее правил продукта), пропуске проверки проекта или CI, недостающих
37
+ фактах Relay, изоляции, Docker, конфликтах с правилами проекта, коммите,
38
+ записи в Relay и живых вызовах;
39
+ - pnpm/Turborepo-монорепо (Turbo Starter): локальный `.harness/profile.json`
40
+ из команд проекта (`pnpm --filter`), test-gates с JUnit без новых
41
+ зависимостей или приёмочные проверки в `.harness/acceptance/`, gate
42
+ детерминизма генерации, защищённые пути, среда с Docker только по
43
+ согласию;
44
+ - controller mode: пакет собирается `controllerContext`, в Git не
45
+ коммитится (`.git/info/exclude`, пересборка в новом клоне) и
46
+ подключается к файловым графам и к задачам Relay (`tasks[].context`);
47
+ - при обновлении — предложить новые возможности выпуска, не применяя их.
48
+
49
+ ## Изменения договора
50
+
51
+ Добавления в конфигурацию коннектора: `contextDir` (строка) и
52
+ `tasks[].context` (1–10 ID); в manifest снимка и ответ `source-snapshot` —
53
+ `localContext` (только при пакетах). Без них снимок, ревизия набора, semantic
54
+ digest задачи, digest графа и контекста те же, что в alpha.12. Пакет с именем
55
+ вида ID контекста задачи (`c` + 20 hex) — `INVALID_INPUT`.
56
+
57
+ Изменение отказа: корень источника контекста через symlink (в файловом
58
+ графе и в пакетах коннектора) — `INVALID_INPUT` «Контекст не читается через
59
+ symlink» вместо `NOT_READY` «не найден»; symlink внутри источника и раньше
60
+ давал `INVALID_INPUT`.
61
+
62
+ ## Совместимость
63
+
64
+ Формат состояния не менялся: alpha.13 читает всё, что читает alpha.12, а
65
+ alpha.12 — всё, что пишет alpha.13. Конфигурация коннектора с новыми полями
66
+ alpha.12 не принимает (строгая схема). Обновление и откат — по
67
+ [upgrade](../upgrade.md).
68
+
69
+ ## Проверка выпуска
70
+
71
+ `npm run check` (lint, typecheck, schemas, build, весь набор тестов, package
72
+ smoke из установленного архива) на macOS arm64, Node.js 24.3. Тесты
73
+ коннектора на подделке backend (`tests/connector-context.test.ts`):
74
+
75
+ - пакет закреплён в снимке и поручении, его байты в контексте попытки
76
+ writer; поручение принято и доставлено;
77
+ - без поля ревизия набора, digest задачи, графа и контекста совпадают со
78
+ значениями alpha.12 (golden-тест, сверен с кодом тега);
79
+ - изменённый пакет — `APPROVAL_STALE` до запуска; перенос пакета между
80
+ задачами и смена порядка — новая ревизия; больше 30 источников —
81
+ `LIMIT_EXCEEDED`; доставка принятого
82
+ результата после изменения пакета и после включения поля — не конфликт;
83
+ - нет каталога — отказ без обращений к backend; нет пакета, свой
84
+ `contextDir`, конфликт имени, symlink-пакет, превышение пределов в снимке
85
+ и `source-check`, `localContext` в manifest.
86
+
87
+ Пересборка `controllerContext` после коммита пакета и удаления AGENTS.md —
88
+ `tests/starter-setup.test.ts`. Тест skill сверяет команды, флаги,
89
+ обязательные аргументы и пути `<pkg>/…` с поставкой. Живые модели и
90
+ настоящие Relay не вызывались; W01 на настоящих Relay для alpha.13 не
91
+ повторялся.
@@ -224,7 +224,8 @@ controller. Оверлей `templates/controller-mode/AGENTS.controller.md` v1
224
224
  читает живые файлы своего worktree;
225
225
  - `context/controller-mode/provenance.json` содержит версию и sha256
226
226
  оверлея, baseline SHA и blob каждого product AGENTS.md;
227
- - в каждом поручении указан `"context": ["controller-mode"]`.
227
+ - в каждом поручении указан `"context": ["controller-mode"]`; для задач
228
+ Relay — в `tasks[].context` коннектора ([connectors](connectors.md#локальные-пакеты-контекста)).
228
229
 
229
230
  Оверлей перечисляет правила, делегированные controller-у: worktree, commit,
230
231
  merge, история Relay, `pnpm run setup`, вложенные оркестраторы и `harness`.
package/docs/upgrade.md CHANGED
@@ -21,20 +21,20 @@
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 | Пишет поверх | Примечание |
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
- | неизвестная `stateVersion` | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет | `STATE_VERSION_UNSUPPORTED`, exit 4 |
36
-
37
- Столбец 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.7. Каждая следующая версия добавляет сюда свой столбец. Отказ неизвестного
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 | Пишет поверх | Примечание |
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
+ | неизвестная `stateVersion` | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет, отказ до записи | нет | `STATE_VERSION_UNSUPPORTED`, exit 4 |
36
+
37
+ Столбец 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. Каждая следующая версия добавляет сюда свой столбец. Отказ неизвестного
38
38
  формата до записи проверяют тесты совместимости пакета
39
39
  <!-- repo-only -->(`tests/compatibility.test.ts`)<!-- /repo-only -->.
40
40
 
@@ -34,7 +34,7 @@ import {
34
34
  writeFileSync,
35
35
  } from 'node:fs';
36
36
  import { tmpdir } from 'node:os';
37
- import { dirname, join, relative, resolve } from 'node:path';
37
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
38
38
  import { fileURLToPath } from 'node:url';
39
39
  import { parseArgs } from 'node:util';
40
40
 
@@ -257,10 +257,17 @@ export function changeFiles(fixture, patch) {
257
257
  */
258
258
  export function controllerContext(fixture, dir) {
259
259
  const target = join(dir, 'controller-mode');
260
+ // Пересборка начинается с пустого каталога: справки удалённых AGENTS.md не
261
+ // остаются, а закоммиченный пакет и .harness/ не попадают в свои же справки.
262
+ rmSync(target, { recursive: true, force: true });
260
263
  mkdirSync(join(target, 'references'), { recursive: true });
261
264
  const overlay = readFileSync(join(CONTROLLER_TEMPLATE, 'AGENTS.controller.md'));
262
265
  writeFileSync(join(target, 'AGENTS.controller.md'), overlay);
263
- const product = git(fixture.repo, ['ls-files', '--', 'AGENTS.md', '**/AGENTS.md'])
266
+ const inside = relative(fixture.repo, resolve(target));
267
+ const exclude = [':(exclude).harness/**'];
268
+ if (inside && !inside.startsWith('..') && !isAbsolute(inside))
269
+ exclude.push(`:(exclude)${inside.split(sep).join('/')}/**`);
270
+ const product = git(fixture.repo, ['ls-files', '--', 'AGENTS.md', '**/AGENTS.md', ...exclude])
264
271
  .split('\n')
265
272
  .filter(Boolean);
266
273
  const references = product.map((path) => {
@@ -71,6 +71,8 @@ export class FileContextSource {
71
71
  const base = realpathSync(this.dir);
72
72
  const sources = [...new Set(ids)].map((id) => {
73
73
  const root = resolve(base, id);
74
+ if (root.startsWith(base + sep) && existsSync(root) && lstatSync(root).isSymbolicLink())
75
+ throw new HarnessError('INVALID_INPUT', `Контекст не читается через symlink: ${id}`);
74
76
  if (!root.startsWith(base + sep) || !existsSync(root) || !lstatSync(root).isDirectory())
75
77
  throw new HarnessError('NOT_READY', `Источник контекста ${id} не найден в ${this.dir}`);
76
78
  const pin = (path) => {
@@ -63,6 +63,7 @@ export declare class Connector {
63
63
  requiredDocuments: string[];
64
64
  repositoryId?: string | undefined;
65
65
  gates?: string[] | undefined;
66
+ context?: string[] | undefined;
66
67
  }[];
67
68
  completion: {
68
69
  mode: "complete" | "review-only";
@@ -85,6 +86,7 @@ export declare class Connector {
85
86
  client: string;
86
87
  };
87
88
  stateDir?: string | undefined;
89
+ contextDir?: string | undefined;
88
90
  } | {
89
91
  provider: "relay-v2-cli";
90
92
  relay: Record<string, unknown> & {
@@ -115,6 +117,7 @@ export declare class Connector {
115
117
  requiredDocuments: string[];
116
118
  repositoryId?: string | undefined;
117
119
  gates?: string[] | undefined;
120
+ context?: string[] | undefined;
118
121
  }[];
119
122
  completion: {
120
123
  mode: "complete" | "review-only";
@@ -137,7 +140,10 @@ export declare class Connector {
137
140
  client: string;
138
141
  };
139
142
  stateDir?: string | undefined;
143
+ contextDir?: string | undefined;
140
144
  };
145
+ /** Каталог пакетов `tasks[].context`: `contextDir` конфигурации, иначе `work/context` рядом с ней. */
146
+ get contextDir(): string;
141
147
  /** Состояние коннектора (создаётся при первой записи). */
142
148
  get store(): ConnectorStore;
143
149
  /** Только чтение: без создания файла; нет состояния — `undefined`. */
@@ -186,10 +192,6 @@ export declare class Connector {
186
192
  * semantic digest: повтор неизменной постановки ничего не меняет.
187
193
  */
188
194
  snapshot(outDir?: string): Promise<{
189
- connector: string;
190
- graphId: string;
191
- graphDigest: string;
192
- contextDigest: string;
193
195
  revision: string;
194
196
  created: boolean;
195
197
  eligible: boolean;
@@ -209,6 +211,11 @@ export declare class Connector {
209
211
  manifest: string;
210
212
  };
211
213
  snapshot: import("./snapshot.ts").BuiltSnapshot;
214
+ localContext?: import("./snapshot.ts").LocalContextManifest[] | undefined;
215
+ connector: string;
216
+ graphId: string;
217
+ graphDigest: string;
218
+ contextDigest: string;
212
219
  }>;
213
220
  /**
214
221
  * Перед запуском — свежее чтение: semantic digest обязан совпасть с
@@ -1,12 +1,12 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
- import { HarnessError, } from "../../contracts/index.js";
3
+ import { API_VERSION, HarnessError, } from "../../contracts/index.js";
4
4
  import { redactor } from "../../environment/redaction.js";
5
5
  import { connectorIdOf, loadConfig, orderIdOf, resolveFrom, storeFileOf, } from "./config.js";
6
6
  import { RelaySink } from "./delivery.js";
7
7
  import { HistoryRecorder } from "./history.js";
8
8
  import { createTransportFor } from "./registry.js";
9
- import { assertEligible, checkIdentity, matchCriterion, readStable, semanticDigest, } from "./snapshot.js";
9
+ import { assertEligible, checkIdentity, contextLimitIssues, localContext, matchCriterion, readStable, semanticDigest, } from "./snapshot.js";
10
10
  import { RelayWorkSource } from "./source.js";
11
11
  import { redactingTransport, settingsSecrets } from "./redact.js";
12
12
  import { ConnectorStore } from "./store.js";
@@ -46,6 +46,10 @@ export class Connector {
46
46
  get config() {
47
47
  return this.loaded.config;
48
48
  }
49
+ /** Каталог пакетов `tasks[].context`: `contextDir` конфигурации, иначе `work/context` рядом с ней. */
50
+ get contextDir() {
51
+ return resolveFrom(this.loaded.baseDir, this.config.contextDir ?? 'work/context');
52
+ }
49
53
  /** Состояние коннектора (создаётся при первой записи). */
50
54
  get store() {
51
55
  this.storeInstance ??= new ConnectorStore(this.storeFile, { provider: this.config.provider, projectId: this.config.projectId }, this.options.onStoreWrite ? { onWrite: this.options.onStoreWrite } : {});
@@ -64,6 +68,7 @@ export class Connector {
64
68
  config: this.config,
65
69
  transport: this.transport,
66
70
  store: this.store,
71
+ contextDir: this.contextDir,
67
72
  });
68
73
  }
69
74
  get history() {
@@ -114,6 +119,14 @@ export class Connector {
114
119
  return this.checkReport(undefined, false, [message], []);
115
120
  }
116
121
  const mapping = [];
122
+ try {
123
+ const local = await localContext(config, this.contextDir);
124
+ if (local.length)
125
+ problems.push(...contextLimitIssues({ apiVersion: API_VERSION, sources: local, missing: [] }));
126
+ }
127
+ catch (error) {
128
+ problems.push(this.redact(error instanceof Error ? error.message : String(error)));
129
+ }
117
130
  try {
118
131
  const { reads } = await readStable(this.transport, config.tasks.map((t) => t.ref), config.limits.rereads);
119
132
  reads.forEach((read, i) => {
@@ -202,6 +215,7 @@ export class Connector {
202
215
  graphId: snapshot.graph.id,
203
216
  graphDigest: snapshot.manifest.graphDigest,
204
217
  contextDigest: snapshot.manifest.contextDigest,
218
+ ...(snapshot.manifest.localContext ? { localContext: snapshot.manifest.localContext } : {}),
205
219
  revision: snapshot.revision,
206
220
  created,
207
221
  eligible: snapshot.problems.length === 0,
@@ -40,6 +40,7 @@ export declare const taskMappingSchema: z.ZodObject<{
40
40
  }>>;
41
41
  }, z.core.$strict>>>;
42
42
  requiredDocuments: z.ZodDefault<z.ZodArray<z.ZodString>>;
43
+ context: z.ZodOptional<z.ZodArray<z.ZodString>>;
43
44
  }, z.core.$strict>;
44
45
  export type TaskMapping = z.infer<typeof taskMappingSchema>;
45
46
  export declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -51,6 +52,7 @@ export declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject
51
52
  projectId: z.ZodString;
52
53
  actor: z.ZodString;
53
54
  stateDir: z.ZodOptional<z.ZodString>;
55
+ contextDir: z.ZodOptional<z.ZodString>;
54
56
  repository: z.ZodObject<{
55
57
  id: z.ZodString;
56
58
  path: z.ZodOptional<z.ZodString>;
@@ -72,6 +74,7 @@ export declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject
72
74
  }>>;
73
75
  }, z.core.$strict>>>;
74
76
  requiredDocuments: z.ZodDefault<z.ZodArray<z.ZodString>>;
77
+ context: z.ZodOptional<z.ZodArray<z.ZodString>>;
75
78
  }, z.core.$strict>>;
76
79
  completion: z.ZodDefault<z.ZodObject<{
77
80
  mode: z.ZodDefault<z.ZodEnum<{
@@ -122,6 +125,7 @@ export declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject
122
125
  projectId: z.ZodString;
123
126
  actor: z.ZodString;
124
127
  stateDir: z.ZodOptional<z.ZodString>;
128
+ contextDir: z.ZodOptional<z.ZodString>;
125
129
  repository: z.ZodObject<{
126
130
  id: z.ZodString;
127
131
  path: z.ZodOptional<z.ZodString>;
@@ -143,6 +147,7 @@ export declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject
143
147
  }>>;
144
148
  }, z.core.$strict>>>;
145
149
  requiredDocuments: z.ZodDefault<z.ZodArray<z.ZodString>>;
150
+ context: z.ZodOptional<z.ZodArray<z.ZodString>>;
146
151
  }, z.core.$strict>>;
147
152
  completion: z.ZodDefault<z.ZodObject<{
148
153
  mode: z.ZodDefault<z.ZodEnum<{
@@ -57,6 +57,13 @@ export const taskMappingSchema = z.strictObject({
57
57
  criteria: z.array(criterionMappingSchema).max(100).default([]),
58
58
  /** Документы, без которых запуск блокируется (ID, ключ или имя). Остальные — дополнительные. */
59
59
  requiredDocuments: z.array(z.string().min(1).max(200)).max(30).default([]),
60
+ /**
61
+ * Локальные пакеты контекста поручения — каталоги `<contextDir>/<id>` (например,
62
+ * оверлей controller-mode). Закрепляются в снимке вместе с контекстом задачи;
63
+ * изменение набора или содержимого пакетов — новый снимок и новое
64
+ * согласование; semantic digest задачи от них не зависит.
65
+ */
66
+ context: z.array(idSchema).min(1).max(10).optional(),
60
67
  });
61
68
  const relaySettings = z.record(z.string(), z.unknown());
62
69
  const common = {
@@ -69,6 +76,8 @@ const common = {
69
76
  * для процессов, разделяющих этот каталог.
70
77
  */
71
78
  stateDir: z.string().min(1).max(1000).optional(),
79
+ /** Каталог пакетов `tasks[].context` (относительно файла конфигурации); по умолчанию `work/context`. */
80
+ contextDir: z.string().min(1).max(1000).optional(),
72
81
  repository: z.strictObject({
73
82
  id: idSchema,
74
83
  /** Путь репозитория для сверки с политикой (относительно файла конфигурации). */
@@ -161,6 +170,12 @@ export function parseConfig(value, baseDir, file) {
161
170
  const dup = refs.find((r, i) => refs.indexOf(r) !== i);
162
171
  if (dup)
163
172
  throw new HarnessError('INVALID_INPUT', 'source: задача указана дважды: ' + dup);
173
+ // ID контекста задачи — `c` + 20 hex (contextIdOf): локальный пакет не может его занять.
174
+ const taken = parsed.data.tasks
175
+ .flatMap((t) => t.context ?? [])
176
+ .find((id) => /^c[0-9a-f]{20}$/.test(id));
177
+ if (taken)
178
+ throw new HarnessError('INVALID_INPUT', `source: пакет контекста ${taken} совпадает по форме с ID контекста задачи; выберите другое имя`);
164
179
  return { config: parsed.data, baseDir, ...(file ? { file } : {}) };
165
180
  }
166
181
  export function loadConfig(path) {
@@ -204,7 +219,9 @@ export const contextIdOf = (orderId) => 'c' + orderId.slice(1);
204
219
  export const graphIdOf = (provider, projectId, taskIds) => 'g' + short([provider, projectId, ...[...taskIds].sort()].join('\n'));
205
220
  /** Digest сопоставления задачи: входит в semantic digest — смена mapping требует нового согласования. */
206
221
  export const mappingDigest = (config, mapping) => digestOf({
207
- mapping: { ...mapping, ref: undefined },
222
+ // Пакеты контекста — не постановка задачи: их ID и digest входят в ревизию
223
+ // набора (новое согласование), но доставку принятого результата не меняют.
224
+ mapping: { ...mapping, ref: undefined, context: undefined },
208
225
  repository: config.repository.id,
209
226
  completion: config.completion,
210
227
  });
@@ -6,7 +6,7 @@
6
6
  export * from './transport.ts';
7
7
  export { connectorConfigSchema, criterionMappingSchema, taskMappingSchema, loadConfig, parseConfig, connectorIdOf, taskIdentity, parseIdentity, orderIdOf, graphIdOf, contextIdOf, mappingDigest, completionColumn, storeFileOf, type ConnectorConfig, type ConnectorConfigInput, type CriterionMapping, type TaskMapping, type LoadedConfig, } from './config.ts';
8
8
  export { ConnectorStore, LeaseLost, type OperationRecord, type OperationStatus, type StepRecord, type StepStatus, type LeaseRecord, type ReceiptRecord, type SnapshotRecord, } from './store.ts';
9
- export { buildSnapshot, checkIdentity, readStable, semanticDigest, takeSnapshot, assertEligible, type BuiltSnapshot, type SnapshotManifest, type SnapshotProblem, type TaskManifest, } from './snapshot.ts';
9
+ export { buildSnapshot, checkIdentity, readStable, semanticDigest, contextLimitIssues, localContext, takeSnapshot, assertEligible, type BuiltSnapshot, type SnapshotManifest, type SnapshotProblem, type TaskManifest, } from './snapshot.ts';
10
10
  export { RelayWorkSource } from './source.ts';
11
11
  export { RelaySink, operationKey, type ReconcileDecision, type SinkOptions } from './delivery.ts';
12
12
  export { HistoryRecorder, type SessionLink } from './history.ts';
@@ -6,7 +6,7 @@
6
6
  export * from "./transport.js";
7
7
  export { connectorConfigSchema, criterionMappingSchema, taskMappingSchema, loadConfig, parseConfig, connectorIdOf, taskIdentity, parseIdentity, orderIdOf, graphIdOf, contextIdOf, mappingDigest, completionColumn, storeFileOf, } from "./config.js";
8
8
  export { ConnectorStore, LeaseLost, } from "./store.js";
9
- export { buildSnapshot, checkIdentity, readStable, semanticDigest, takeSnapshot, assertEligible, } from "./snapshot.js";
9
+ export { buildSnapshot, checkIdentity, readStable, semanticDigest, contextLimitIssues, localContext, takeSnapshot, assertEligible, } from "./snapshot.js";
10
10
  export { RelayWorkSource } from "./source.js";
11
11
  export { RelaySink, operationKey } from "./delivery.js";
12
12
  export { HistoryRecorder } from "./history.js";
@@ -1,4 +1,4 @@
1
- import { HarnessError, type ContextSnapshot, type WorkGraph } from '../../contracts/index.ts';
1
+ import { HarnessError, type ContextSnapshot, type ContextSourceEntry, type WorkGraph } from '../../contracts/index.ts';
2
2
  import { type ConnectorConfig, type TaskMapping } from './config.ts';
3
3
  import type { ReceiptRecord } from './store.ts';
4
4
  import { type CriterionRecord, type RelayTransport, type TaskRead, type TransportIdentity } from './transport.ts';
@@ -84,6 +84,15 @@ export interface SnapshotManifest {
84
84
  tasks: TaskManifest[];
85
85
  truncated: string[];
86
86
  problems: SnapshotProblem[];
87
+ /** Закреплённые локальные пакеты `tasks[].context`; без них поля нет. */
88
+ localContext?: LocalContextManifest[];
89
+ }
90
+ export interface LocalContextManifest {
91
+ id: string;
92
+ digest: string;
93
+ files: number;
94
+ references: number;
95
+ bytes: number;
87
96
  }
88
97
  export interface BuiltSnapshot {
89
98
  graph: WorkGraph;
@@ -119,12 +128,24 @@ export declare function buildSnapshot(input: {
119
128
  rounds: number;
120
129
  /** Доверенный receipt этого коннектора для задачи-зависимости вне графа. */
121
130
  receiptOf: (orderId: string) => ReceiptRecord | undefined;
131
+ /** Закреплённые локальные пакеты `tasks[].context` (localContext). */
132
+ local?: ContextSourceEntry[];
122
133
  }): BuiltSnapshot;
134
+ /** Нарушения пределов снимка контекста: число источников, размер файла и сумма. */
135
+ export declare function contextLimitIssues(context: ContextSnapshot): string[];
136
+ /**
137
+ * Локальные пакеты `tasks[].context` из `contextDir`: те же правила, что у
138
+ * файлового контекста графа (каталог `<contextDir>/<id>`, `references/` —
139
+ * справки). Нет каталога или пакета — NOT_READY до чтения backend.
140
+ */
141
+ export declare function localContext(config: ConnectorConfig, contextDir: string | undefined): Promise<ContextSourceEntry[]>;
123
142
  /** Прочитать, собрать и проверить идентичность: общий путь `source-snapshot` и `run --source`. */
124
143
  export declare function takeSnapshot(input: {
125
144
  config: ConnectorConfig;
126
145
  transport: RelayTransport;
127
146
  receiptOf: (orderId: string) => ReceiptRecord | undefined;
147
+ /** Абсолютный каталог пакетов `tasks[].context`. */
148
+ contextDir?: string;
128
149
  signal?: AbortSignal;
129
150
  }): Promise<BuiltSnapshot>;
130
151
  /** Отказ по eligibility/limits: причина и предел в сообщении, без запуска. */
@@ -1,6 +1,8 @@
1
- import { API_VERSION, HarnessError, contextDigest, digestOf, sha256, sourceDigest, workGraphDigest, workGraphSchema, } from "../../contracts/index.js";
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { API_VERSION, HarnessError, CONTEXT_LIMITS, contextDigest, contextIssues, digestOf, sha256, sourceDigest, workGraphDigest, workGraphSchema, } from "../../contracts/index.js";
2
3
  import { connectorIdOf, contextIdOf, graphIdOf, mappingDigest, orderIdOf, taskIdentity, } from "./config.js";
3
4
  import { TransportError, } from "./transport.js";
5
+ import { FileContextSource } from "../../connectors/files.js";
4
6
  /** Безопасная причина отказа транспорта при чтении: сеть/CLI — не пустой список. */
5
7
  export function readFailure(error, what) {
6
8
  if (error instanceof HarnessError)
@@ -124,6 +126,8 @@ export function buildSnapshot(input) {
124
126
  const missing = [];
125
127
  const prerequisites = new Map();
126
128
  const tasks = [];
129
+ /** Какие локальные пакеты у какой задачи (по порядку): часть ревизии набора. */
130
+ const packsByTask = [];
127
131
  const truncated = [];
128
132
  let totalBytes = 0;
129
133
  const limits = config.limits;
@@ -331,8 +335,9 @@ export function buildSnapshot(input) {
331
335
  ...(mapping.gates ? { gates: mapping.gates } : {}),
332
336
  requirements: [],
333
337
  contracts: [],
334
- context: [entry.id],
338
+ context: [entry.id, ...(mapping.context ?? [])],
335
339
  });
340
+ packsByTask.push([identityOf, mapping.context ?? []]);
336
341
  tasks.push({
337
342
  ref: mapping.ref,
338
343
  id: task.id,
@@ -367,6 +372,15 @@ export function buildSnapshot(input) {
367
372
  connector,
368
373
  tasks: tasks.map((t) => [t.identity, t.semanticDigest]),
369
374
  prerequisites: prereqs,
375
+ // Без локальных пакетов ревизия та же, что до их появления. С ними —
376
+ // содержимое пакетов и их распределение по задачам: перенос пакета между
377
+ // задачами или смена порядка — тоже новое согласование.
378
+ ...(input.local?.length || packsByTask.some(([, ids]) => ids.length)
379
+ ? {
380
+ context: (input.local ?? []).map((s) => [s.id, s.digest]),
381
+ contextBy: [...packsByTask].sort(([a], [b]) => (a < b ? -1 : 1)),
382
+ }
383
+ : {}),
370
384
  });
371
385
  const joined = `${config.provider}:${config.projectId}:${taskIds.join('+')}`;
372
386
  const graph = workGraphSchema.parse({
@@ -380,7 +394,15 @@ export function buildSnapshot(input) {
380
394
  orders,
381
395
  prerequisites: prereqs,
382
396
  });
383
- const context = { apiVersion: API_VERSION, sources, missing };
397
+ const context = {
398
+ apiVersion: API_VERSION,
399
+ sources: [...sources, ...(input.local ?? [])],
400
+ missing,
401
+ };
402
+ // Пределы снимка контекста проверяются здесь, а не только при запуске:
403
+ // source-snapshot и source-check называют их как отказ eligibility.
404
+ for (const issue of contextLimitIssues(context))
405
+ problems.push({ code: 'LIMIT_EXCEEDED', taskId: '', key: 'context', message: issue });
384
406
  const manifest = {
385
407
  version: 1,
386
408
  provider: config.provider,
@@ -398,11 +420,45 @@ export function buildSnapshot(input) {
398
420
  tasks,
399
421
  truncated,
400
422
  problems,
423
+ ...(input.local?.length ? { localContext: input.local.map(localManifest) } : {}),
401
424
  };
402
425
  return { graph, context, manifest, revision, problems };
403
426
  }
427
+ const CONTEXT_SOURCES_MAX = 30;
428
+ /** Нарушения пределов снимка контекста: число источников, размер файла и сумма. */
429
+ export function contextLimitIssues(context) {
430
+ const issues = contextIssues(context);
431
+ if (context.sources.length > CONTEXT_SOURCES_MAX)
432
+ issues.push(`Источников контекста ${context.sources.length} при пределе ${CONTEXT_SOURCES_MAX} (задачи и локальные пакеты)`);
433
+ return issues.map((i) => /больше предела/.test(i)
434
+ ? `${i} (fileBytes=${CONTEXT_LIMITS.fileBytes}, totalBytes=${CONTEXT_LIMITS.totalBytes})`
435
+ : i);
436
+ }
437
+ const localManifest = (s) => ({
438
+ id: s.id,
439
+ digest: s.digest,
440
+ files: s.files.length,
441
+ references: s.references.length,
442
+ bytes: [...s.files, ...s.references].reduce((n, f) => n + f.bytes, 0),
443
+ });
444
+ /**
445
+ * Локальные пакеты `tasks[].context` из `contextDir`: те же правила, что у
446
+ * файлового контекста графа (каталог `<contextDir>/<id>`, `references/` —
447
+ * справки). Нет каталога или пакета — NOT_READY до чтения backend.
448
+ */
449
+ export async function localContext(config, contextDir) {
450
+ const ids = [...new Set(config.tasks.flatMap((t) => t.context ?? []))].sort();
451
+ if (!ids.length)
452
+ return [];
453
+ if (!contextDir)
454
+ throw new HarnessError('INVALID_INPUT', 'source: tasks[].context требует каталог пакетов (contextDir)');
455
+ if (!existsSync(contextDir) || !statSync(contextDir).isDirectory())
456
+ throw new HarnessError('NOT_READY', `Каталог пакетов контекста ${contextDir} не найден (tasks[].context: ${ids.join(', ')})`);
457
+ return (await new FileContextSource(contextDir).snapshot(ids)).sources;
458
+ }
404
459
  /** Прочитать, собрать и проверить идентичность: общий путь `source-snapshot` и `run --source`. */
405
460
  export async function takeSnapshot(input) {
461
+ const local = await localContext(input.config, input.contextDir);
406
462
  const identity = await checkIdentity(input.transport, input.config, input.signal);
407
463
  const { reads, rounds } = await readStable(input.transport, input.config.tasks.map((t) => t.ref), input.config.limits.rereads, input.signal);
408
464
  return buildSnapshot({
@@ -411,6 +467,7 @@ export async function takeSnapshot(input) {
411
467
  reads,
412
468
  rounds,
413
469
  receiptOf: input.receiptOf,
470
+ ...(local.length ? { local } : {}),
414
471
  });
415
472
  }
416
473
  /** Отказ по eligibility/limits: причина и предел в сообщении, без запуска. */
@@ -19,12 +19,16 @@ export declare class RelayWorkSource implements WorkSource {
19
19
  config: ConnectorConfig;
20
20
  transport: RelayTransport;
21
21
  store: ConnectorStore;
22
+ /** Абсолютный каталог пакетов `tasks[].context`. */
23
+ contextDir?: string;
22
24
  };
23
25
  readonly id: string;
24
26
  constructor(options: {
25
27
  config: ConnectorConfig;
26
28
  transport: RelayTransport;
27
29
  store: ConnectorStore;
30
+ /** Абсолютный каталог пакетов `tasks[].context`. */
31
+ contextDir?: string;
28
32
  });
29
33
  /** Снимок без проверки eligibility (для предпросмотра и диагностики). */
30
34
  snapshot(signal?: AbortSignal): Promise<BuiltSnapshot>;
@@ -26,6 +26,7 @@ export class RelayWorkSource {
26
26
  config: this.options.config,
27
27
  transport: this.options.transport,
28
28
  receiptOf: (orderId) => this.options.store.receipts(orderId).at(-1),
29
+ ...(this.options.contextDir ? { contextDir: this.options.contextDir } : {}),
29
30
  ...(signal ? { signal } : {}),
30
31
  });
31
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devcontour-harness",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.13",
4
4
  "description": "Headless execution harness: work graph, isolated attempts, gates, independent reviews and accepted results.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: harness-setup
3
- description: Install, configure or upgrade devcontour-harness for a new or existing npm/pnpm Git repository — ask the setup questions once (project, branch, install location, package source, writer and reviewer models, approval, task source such as files or Relay), install the exact version inside the project (`.harness-tools/`) or outside it, run `harness init`, record the decisions and hand over to the `harness` skill; upgrade an installed version with a state copy and a compatibility check.
3
+ description: Install, configure or upgrade devcontour-harness for a new or existing npm/pnpm Git repository, including pnpm/Turborepo monorepos such as Turbo Starter and task sources such as Relay — decide every routine choice yourself from the repository, ask the user only at real forks (which vendor writes and which accepts, a missing Relay fact, isolation or Docker, rule conflicts, commits, writes to Relay, live model calls), install the exact version in `.harness-tools/`, run `harness init`, and hand over to the `harness` skill; upgrade an installed version with a state copy and a compatibility check.
4
4
  ---
5
5
 
6
6
  # Harness setup
@@ -18,52 +18,51 @@ replaces or relaxes them.
18
18
  Every `harness` command prints one JSON envelope on stdout (`ok` with `data`,
19
19
  or `error`); exit codes: 0 success, 2 invalid input, 3 refusal or block,
20
20
  4 infrastructure. Below `$H` is the installed CLI:
21
- `H=<tooldir>/node_modules/.bin/harness`.
21
+ `H=<tooldir>/node_modules/.bin/harness`, and `<pkg>` is
22
+ `<tooldir>/node_modules/devcontour-harness`.
22
23
 
23
24
  Modes: **install** (first setup), **upgrade** (change the installed version),
24
25
  **check** (report what is installed; read only). Pick the mode from the
25
26
  request; when the repository already has `.harness/` and a pinned or
26
27
  installed `$H`, it is upgrade or check, not install.
27
28
 
28
- ## Install
29
+ ## Decide or ask
30
+
31
+ Decide everything you can from the repository and the defaults below, and
32
+ report each decision with its reason at the end; the user overrides any of
33
+ them by saying so. Ask only at a fork — a choice that changes who is trusted,
34
+ what leaves the machine, what is spent or which project rule gives way — and
35
+ ask all open forks of a step in one round (multiple choice where the host
36
+ supports it).
37
+
38
+ | Choice | Default (decide yourself) | Ask when |
39
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | Writer and reviewer vendor | — | always: which of `claude` / `codex` writes; the other one reviews and accepts |
41
+ | Same vendor for review | never | only one runtime is installed, or the user asked for it |
42
+ | Models and effort | runtime defaults | the user named models |
43
+ | Branch | new branch `harness-setup` from the current branch; an existing `harness/accepted` is reported and left alone | the working tree is not clean |
44
+ | Install location | `<repo>/.harness-tools` | the user asked for a directory outside the repository |
45
+ | Package source | npm `@alpha` resolved to the exact version | the user gave a version or a tarball |
46
+ | Stack profile | built-in by lockfile; a local `.harness/profile.json` when the project does not have its scripts (below) | — |
47
+ | Gates | derived from the project's own commands (step 6), each reported with its command | a check the project or its CI runs would be left out, or no command can prove behaviour without new dependencies |
48
+ | Plan approval | `agent` (`init` default) | the user wants to approve plans themselves (`operator`) |
49
+ | Task source | Relay when the repository or the request names it, otherwise graph files | both are plausible |
50
+ | Relay connection | discovered from the repository and environment | a fact is missing: endpoint or binary/config, `projectId`, which tasks |
51
+ | Relay completion | `review-only` (a human closes the task) | the user wants `complete` |
52
+ | Controller mode | — | the product AGENTS.md tells agents to manage worktrees, commits, merges or Relay history: the overlay then takes precedence over those rules (step 9) |
53
+ | OS isolation | on | prepare or gates fail inside the sandbox (store, network, Docker socket) |
54
+ | Docker environment | none | gates need a database or other service |
55
+ | Project rule conflicts | — | a project rule forbids what setup creates (for example new `.md` files: `.harness/README.md`) |
56
+ | Commit, Relay writes, runs | — | always confirm: committing the setup, `run --source` / `source-deliver` / `source-reconcile`, any live `run` or `review-plan` |
29
57
 
30
- ### 0. Ask once
31
-
32
- Ask everything in one round (a multiple-choice question per item where the
33
- host supports it); offer the default first. Do not ask what the repository
34
- already answers: detect the lockfile, `packageManager`, an existing
35
- `.harness/`, the current branch and which of `claude` / `codex` are installed
36
- first. Whether a runtime is signed in is not detectable without a model call:
37
- the user confirms it.
38
-
39
- | # | Question | Options (default first) | Becomes |
40
- | --- | ------------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
41
- | 1 | Repository | existing path / new project at a path | step 2 |
42
- | 2 | Branch for the setup | new branch `harness-setup` / current branch | step 3; never the name `harness` (it clashes with `harness/accepted`) |
43
- | 3 | Install location | inside the project `.harness-tools/` / outside, in a tool directory | `<tooldir>` of step 4 |
44
- | 4 | Package source | npm `@alpha` resolved to an exact version / npm exact version / local `.tgz` with its sha256 | step 4 |
45
- | 5 | Writer runtime | `claude` / `codex` | `init --writer`; the reviewer is the other vendor |
46
- | 6 | Models and effort | runtime defaults / explicit `model` (and `effort` for codex) for writer and reviewer | step 6 |
47
- | 7 | Reviewer of the writer's vendor | no (`different-vendor`) / yes for code and contract review, stated explicitly | step 6 |
48
- | 8 | Plan approval | `agent` (the reviewer approves) / `operator` (a human runs `approve`) | step 6 |
49
- | 9 | Task source | graph files in `.harness/work/` / Relay v1 (HTTP) / Relay-2 (CLI) | step 7 |
50
- | 10 | Commit the setup | commit on the setup branch / leave uncommitted | step 8 |
51
-
52
- For Relay also ask: the v1 endpoint, or the absolute Relay-2 binary, the
53
- absolute `relay.config.json` / `relay.workspace.json` and the workspace
54
- project; `projectId`; `actor` (default `harness-controller`); the task refs
55
- and, per task, its write scope when it is narrower than the whole
56
- repository; the columns a task may start from; `completion.mode`
57
- (`review-only` for the first run, or `complete`).
58
-
59
- The answers are the user's decisions. Do not change them later without
60
- asking.
58
+ ## Install
61
59
 
62
60
  ### 1. Preconditions
63
61
 
64
62
  Node.js ≥ 24.3, `git`, `npm` (or `pnpm` for a pnpm project) and the runtime
65
- CLIs the answers name (`claude --version`, `codex --version`). A missing or
66
- signed-out runtime is the user's to fix; a live run is impossible without it.
63
+ CLIs (`claude --version`, `codex --version`). Whether a runtime is signed in
64
+ is not detectable without a model call: the user confirms it. A missing or
65
+ signed-out runtime is the user's to fix.
67
66
 
68
67
  The package manager version is checked exactly: the built-in profiles pin
69
68
  one (`npm-ci` pins npm `11.4.2`), and `package.json#packageManager`
@@ -75,9 +74,8 @@ project is the user's decision.
75
74
 
76
75
  **Existing.** It must be the top level of a Git repository with at least one
77
76
  commit and exactly one lockfile: `package-lock.json` (profile `npm-ci`) or
78
- `pnpm-lock.yaml` (profile `pnpm-frozen`). Both or none is decided by the user
79
- (`--profile`, or a local `.harness/profile.json` passed as
80
- `--profile ./.harness/profile.json`).
77
+ `pnpm-lock.yaml` (profile `pnpm-frozen`). With both or none, write a local
78
+ profile (step 6).
81
79
 
82
80
  **New.** Create it only with the user's consent, at the path they gave:
83
81
 
@@ -106,11 +104,11 @@ a gate.
106
104
  ### 3. Branch
107
105
 
108
106
  `git status` must be clean (ask the user about local changes; never stash or
109
- discard them yourself). Then `git switch -c <branch>`.
107
+ discard them yourself). Then `git switch -c harness-setup`.
110
108
 
111
109
  `run` creates `refs/heads/harness/accepted` from the `HEAD` of the main
112
110
  checkout when that ref does not exist yet. Keep the setup branch checked out,
113
- with the setup committed (step 8), until the first `run`: otherwise
111
+ with the setup committed (step 11), until the first `run`: otherwise
114
112
  `harness/accepted` starts from a commit without it. When `harness/accepted`
115
113
  already exists, report its SHA and leave it alone. Accepted work lands in
116
114
  `harness/accepted`, not in the setup branch; merging it anywhere is the
@@ -169,14 +167,71 @@ $H doctor
169
167
  Never install the package into the project's own dependencies as part of this
170
168
  route, and never put the execution state inside the repository.
171
169
 
172
- ### 5. `harness init`
170
+ ### 5. Read the project
171
+
172
+ Before choosing gates read: the root and package `package.json` scripts, the
173
+ lockfile, `pnpm-workspace.yaml`, `turbo.json`, every `AGENTS.md`, CI
174
+ workflows (they show which commands the project trusts), code generators
175
+ (OpenAPI, SDK, Prisma) and Relay traces (`relay.config.json`,
176
+ `relay.workspace.json`, `.artifacts/relay/`, Relay commands in AGENTS.md).
177
+
178
+ ### 6. Stack profile and gates
179
+
180
+ **A built-in profile fits** when the project already has the scripts it
181
+ calls (`npm-ci` / `pnpm-frozen`: `check`, and `test` writing JUnit to
182
+ `.reports/junit.xml`). Then skip to step 7.
183
+
184
+ **Otherwise write `.harness/profile.json`** (`<pkg>/schemas/stack-profile.schema.json`)
185
+ and pass `--profile ./.harness/profile.json` to `init`. This is the usual
186
+ case for a pnpm/Turborepo monorepo; Turbo Starter is the worked example
187
+ (`<pkg>/docs/turbo-starter.md`, `<pkg>/examples/turbo-starter/policy.json`):
188
+
189
+ - `prepare`: `pnpm install --frozen-lockfile`.
190
+ - check gates (`kind: check`): the project's own type and build commands per
191
+ package, called directly — `pnpm --filter <package> run check-types`,
192
+ `… run build`, with `dependsOn` where one needs another's output. Prefer
193
+ the per-package commands a root script runs when the root script also
194
+ does work that cannot run in a worktree (for example an environment check
195
+ that fails on the pinned Node); that keeps every check the root script
196
+ makes. Leaving out a check the project or its CI runs is the user's
197
+ decision. Never call `turbo run` without `--force` (a cache hit proves
198
+ nothing), and `pnpm exec turbo` only with an exactly pinned `turbo`.
199
+ - test gates (`kind: test`, at least one, with
200
+ `report: { type: junit, path: .artifacts/junit/<gate>.xml }`): an existing
201
+ test command that can write JUnit without new dependencies (Node's test
202
+ runner `--test-reporter=junit --test-reporter-destination=…`, Vitest's
203
+ built-in `junit` reporter). When the project has none, or its AGENTS.md
204
+ forbids tests in product code, put acceptance tests in
205
+ `.harness/acceptance/*.test.mjs` run by `node --test`, as the example does.
206
+ When no command can prove behaviour without adding a dependency, ask.
207
+ - code generation (OpenAPI, SDK): a test gate that runs the project's pinned
208
+ generator and fails on any difference in the generated paths
209
+ (`<pkg>/examples/turbo-starter/harness/acceptance/generation.test.mjs`).
210
+ Generated outputs are not `generatedPaths` (that list means "protected");
211
+ orders that change generated code list the outputs in their write scope.
212
+ `generatedPaths` is only for ignored outputs nobody edits (Prisma client).
213
+ - `protectedPaths`: lockfile, every `package.json`, `pnpm-workspace.yaml`,
214
+ `turbo.json`, `.npmrc`, `scripts/`, `infra/`, CI, every `AGENTS.md`,
215
+ generator inputs and configuration, `.harness/`, `.harness-tools/`.
216
+ - `toolchain`: the package manager version from `packageManager`, and
217
+ `node: ">=24.3"`.
218
+ - `environment.values` for variables the commands need to stay local
219
+ (for example `DOTENV_CONFIG_PATH: /dev/null`, `CHECKPOINT_DISABLE: 1`);
220
+ secrets only by name in `environment.secrets`, never their values.
221
+
222
+ A gate that is red on the current base stays: fixing the base is a separate
223
+ first work order. List the gates with the command each one runs in the
224
+ report.
225
+
226
+ ### 7. `harness init`
173
227
 
174
228
  ```sh
175
229
  $H init --repository <repo> --writer <claude|codex> --plan
176
230
  $H init --repository <repo> --writer <claude|codex>
177
231
  ```
178
232
 
179
- The first call writes nothing: read `data.files`, `data.conflicts`,
233
+ Add `--profile ./.harness/profile.json` with a local profile. The first call
234
+ writes nothing: read `data.files`, `data.conflicts`,
180
235
  `data.toolchain.problems`, `data.facts` and `data.manifest.paths.state`, and
181
236
  resolve every conflict or problem with the table of the `harness` skill
182
237
  (step 3) before the second call. `init` is create-only: `.harness/.gitignore`,
@@ -184,9 +239,9 @@ resolve every conflict or problem with the table of the `harness` skill
184
239
  `.harness/policy.json`. The state lives outside the repository
185
240
  (`HARNESS_STATE_HOME`, `XDG_STATE_HOME` or `~/.local/state/devcontour-harness`).
186
241
 
187
- ### 6. Runtimes and approval
242
+ ### 8. Runtimes, approval, isolation
188
243
 
189
- Edit `.harness/policy.json` only as the answers require:
244
+ Edit `.harness/policy.json` only as the decisions require:
190
245
 
191
246
  - models: `model` on `roles.<role>.writer` and on `review.code.reviewer`,
192
247
  `review.plan.reviewer`, `review.contract.reviewer`;
@@ -200,9 +255,13 @@ Edit `.harness/policy.json` only as the answers require:
200
255
  `content-differs` on `policy.json`. When the user wants it anyway, say so
201
256
  and let them decide;
202
257
  - operator approval: top-level `"approvalMode": "operator"`;
203
- - with the version pin committed inside the project: add `.harness-tools/` to
204
- `repositories[0].protectedPaths`, so no work order can change the tool it
205
- is checked by.
258
+ - isolation: keep `isolation.mode: os`. When `doctor` or a first `prepare`
259
+ shows the sandbox blocking the package store, the network or the Docker
260
+ socket, show the diagnosis and ask between `isolation.domains` and
261
+ `isolation.mode: none`;
262
+ - with the version pin committed inside the project: `.harness-tools/` in
263
+ `repositories[0].protectedPaths` (already there when the local profile
264
+ lists it).
206
265
 
207
266
  Never remove or weaken anything `init` wrote. Keep the writer runtime as
208
267
  `init` wrote it: a different writer vendor is a new `init --writer` decision.
@@ -221,18 +280,67 @@ non-codex runtime, the reviewer vendor rule, unknown gates) run in
221
280
  for the first graph before any review or run, and fix the policy, not the
222
281
  check, when it fails.
223
282
 
224
- ### 7. Task source
283
+ **Services.** When gates need a database or another service, ask before
284
+ using Docker. With a yes, copy `<pkg>/examples/turbo-starter/harness/`
285
+ (`compose-bridge.mjs`, `compose.acceptance.yml`, `env/`) into `.harness/`,
286
+ adapt the compose model to the project and add `repositories[0].lifecycle`
287
+ (setup, ready, teardown) and the secret names as in
288
+ `<pkg>/docs/turbo-starter.md`; gates that need the service run through
289
+ `node .harness/compose-bridge.mjs exec -- <command>`.
290
+
291
+ ### 9. Controller mode
292
+
293
+ When the product AGENTS.md tells an agent to create worktrees, commit, merge,
294
+ move tasks or keep Relay history itself, the harness controller does those
295
+ operations instead, and the worker needs the controller-mode overlay
296
+ (`<pkg>/templates/controller-mode/README.md`). The overlay takes precedence
297
+ over those product rules, so ask first: show the rules it overrides and say
298
+ that the overlay text names Turbo Starter commands (`pnpm worktree:create`,
299
+ `pnpm rest-api-sdk:generate`) as examples. Without a yes, keep controller
300
+ mode off; the harness still refuses a worker commit (`scope-violation`).
301
+ With a yes, build the pack from the committed AGENTS.md files:
302
+
303
+ ```sh
304
+ node --input-type=module -e "
305
+ import { controllerContext } from '<pkg>/examples/turbo-starter/setup.mjs';
306
+ import { execFileSync } from 'node:child_process';
307
+ const repo = '<repo>';
308
+ const baselineSha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo }).toString().trim();
309
+ controllerContext({ repo, baselineSha, upstream: null }, repo + '/.harness/work/context');
310
+ "
311
+ ```
312
+
313
+ It writes `.harness/work/context/controller-mode/` (the unchanged overlay,
314
+ `references/` with the product AGENTS.md files, `provenance.json`); a rebuild
315
+ starts from an empty directory and never lists `.harness/`. The pack is not
316
+ committed to the repository (some projects forbid new `.md` files): keep it
317
+ out of Git and rebuild it in a fresh clone with the same command. Its
318
+ `provenance.json` records the `HEAD` it was built on, so a rebuild on another
319
+ commit is a new pack digest and the next Relay snapshot needs a new approval
320
+ even when no AGENTS.md changed.
321
+
322
+ ```sh
323
+ grep -qxF '.harness/work/context/controller-mode/' <repo>/.git/info/exclude \
324
+ || echo '.harness/work/context/controller-mode/' >> <repo>/.git/info/exclude
325
+ ```
326
+
327
+ File graphs list `"context": ["controller-mode"]` on every order; Relay tasks
328
+ list it in `tasks[].context` of the connector (step 10). A changed AGENTS.md
329
+ or overlay means rebuilding the pack, and the next snapshot needs a new
330
+ approval; a result already accepted is still delivered.
331
+
332
+ ### 10. Task source
225
333
 
226
334
  **Graph files.** Nothing more to configure; the `harness` skill plans the
227
335
  first graph.
228
336
 
229
- **Relay.** Write `.harness/connector.json` from `docs/connectors.md` of the
230
- installed package ("Конфигурация"):
337
+ **Relay.** Write `.harness/connector.json` from `<pkg>/docs/connectors.md`
338
+ ("Конфигурация"):
231
339
 
232
340
  - `"version": 1` and `"provider": "relay-v1-http"` or `"relay-v2-cli"`,
233
341
  always explicit — never guessed from a binary named `relay`;
234
- - `projectId` and `actor` at the top level; `repository.id` is the policy
235
- repository id (`app` after `init`);
342
+ - `projectId` and `actor` (`harness-controller`) at the top level;
343
+ `repository.id` is the policy repository id (`app` after `init`);
236
344
  - adapter settings nested under `relay`: v1 `relay.endpoint`; Relay-2
237
345
  absolute `relay.binary` and `relay.config`, `relay.project` for a workspace,
238
346
  `relay.env` that may set `RELAY_HOME` (and on macOS `DYLD_LIBRARY_PATH` to
@@ -240,9 +348,12 @@ installed package ("Конфигурация"):
240
348
  - `tasks[]`: `ref`; `role` and `gates` only from `data.facts` of `init`;
241
349
  `writeScope` (required) = the role's `writePaths` from the facts, or, when
242
350
  the role has none, `["."]` (the whole repository) or the narrower paths the
243
- user named; `criteria` mapped to a gate and a JUnit `testId`;
244
- - `completion: { "mode": "review-only" }` (or `"complete"`) and
245
- `eligibility.columns` from the answers;
351
+ task names (with generated outputs when it changes them); `criteria` mapped
352
+ to a gate and a JUnit `testId`; `"context": ["controller-mode"]` with
353
+ controller mode (step 9) — the pack is read from `contextDir`, by default
354
+ `work/context` next to `connector.json`;
355
+ - `completion: { "mode": "review-only" }` unless the user chose `complete`;
356
+ `eligibility.columns` from the project's board; `history.enabled: true`;
246
357
  - no `stateDir` (pass `--state <manifest.paths.state>`), no secret values.
247
358
 
248
359
  Checks that do not write to Relay:
@@ -250,30 +361,36 @@ Checks that do not write to Relay:
250
361
  ```sh
251
362
  $H source-check --source <repo>/.harness/connector.json --policy <repo>/.harness/policy.json --state <state>
252
363
  $H source-snapshot --source <repo>/.harness/connector.json --policy <repo>/.harness/policy.json --state <state> --out <dir outside the repo>
364
+ $H validate --graph <dir outside the repo>/graph.json --policy <repo>/.harness/policy.json --context <dir outside the repo>/context.json
253
365
  ```
254
366
 
255
367
  `source-check` is read only. `source-snapshot` reads Relay but records the
256
368
  snapshot in the connector state under `--state` and writes `graph.json`,
257
- `context.json` and `manifest.json` to `--out`. `run --source`,
369
+ `context.json` and `manifest.json` to `--out`; its output names the pinned
370
+ local packs (`localContext`) and a context over the limits as
371
+ `LIMIT_EXCEEDED`. `run --source`,
258
372
  `source-deliver` and `source-reconcile` write to Relay: run them only after
259
373
  the user says so.
260
374
 
261
- ### 8. Commit
375
+ ### 11. Commit
262
376
 
263
377
  With the user's yes: commit on the setup branch `.harness/.gitignore`,
264
- `.harness/README.md`, `.harness/policy.json`, `.harness/connector.json` (when
265
- present) and, for an npm source inside the project,
378
+ `.harness/README.md`, `.harness/policy.json`, `.harness/profile.json`,
379
+ `.harness/connector.json`, `.harness/acceptance/` and the bridge files when
380
+ present, and, for an npm source inside the project,
266
381
  `.harness-tools/.gitignore`, `.harness-tools/package.json`,
267
382
  `.harness-tools/package-lock.json`. Never `project.json`, `node_modules/` or
268
- the state. Do not push or merge.
383
+ the state or the controller-mode pack. Do not push or merge.
269
384
 
270
- ### 9. Report and hand over
385
+ ### 12. Report and hand over
271
386
 
272
- Report: installed version (and the tarball sha256), `doctor`, the decisions
273
- of the manifest (profile, writer, reviewer, state path), the policy edits,
274
- the connector check, the commit SHA. Then continue with the `harness` skill:
275
- brief → `plan-request` → draft → `plan-check` → `validate` → `run`. A live
276
- `run` or `review-plan` calls paid models: start it when the user asks.
387
+ Report: installed version (and the tarball sha256), `doctor`, every decision
388
+ of the table with its reason, the gates with their commands, the policy
389
+ edits, controller mode, the connector check and snapshot (`graphId`,
390
+ `graphDigest`, eligibility per task), the commit SHA. Then continue with the
391
+ `harness` skill: brief → `plan-request` → draft → `plan-check` → `validate`
392
+ → `run` (for Relay: `review-plan` and `run --source`). A live `run` or
393
+ `review-plan` calls paid models: start it when the user asks.
277
394
 
278
395
  ## Upgrade
279
396
 
@@ -320,6 +437,9 @@ otherwise in step 7.
320
437
  graphs may need plan review or approval again (`explain` names it).
321
438
  - `$H validate --graph <graph> --policy <repo>/.harness/policy.json` for
322
439
  every `.harness/work/*.json` graph.
440
+ - Read the release notes for new options the setup should use (for
441
+ example `tasks[].context` of the connector for controller mode) and
442
+ propose them; do not apply them unasked.
323
443
  8. **Resume.** `$H resume --state <state>`, then `run` as before. Attempts that
324
444
  were active at the stop are not continued with old tokens; recovery either
325
445
  accepts a saved integration SHA or ends the attempt.
@@ -341,8 +461,10 @@ and `$H --version`, or a missing `node_modules/` (restore with `npm ci`, step
341
461
 
342
462
  ## Don'ts
343
463
 
344
- - Do not choose for the user: location, source, runtimes, models, the vendor
345
- rule, approval mode, task source and commits are their answers.
464
+ - Do not ask what the repository answers, and do not decide a fork of the
465
+ table for the user: the vendors, a reviewer of the writer's vendor,
466
+ controller mode, a left-out project check, isolation, Docker, project rule
467
+ conflicts, commits, writes to Relay and live runs are theirs.
346
468
  - Do not install into the project's dependencies, edit its `package.json` or
347
469
  lockfile (except when bootstrapping a new project with consent), or put the
348
470
  state inside the repository.
@@ -27,6 +27,11 @@ not a file committed to the target repository. Some projects forbid new
27
27
  4. Add `"context": ["controller-mode"]` to every order of the graph. Writer
28
28
  and reviewers then see the same pinned bytes, and a changed overlay
29
29
  requires a new plan approval.
30
+ For tasks read from Relay, list the pack in the connector instead:
31
+ `"context": ["controller-mode"]` on each entry of `tasks[]`, with the
32
+ directory at `<contextDir>/controller-mode` (`contextDir` defaults to
33
+ `work/context` next to `connector.json`). The snapshot pins it the same
34
+ way, and a changed overlay needs a new snapshot and approval.
30
35
  5. Protect the product AGENTS.md files and the harness setup through the
31
36
  policy's `protectedPaths`. The worker cannot change the rules it runs
32
37
  under.