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 +15 -2
- package/docs/connectors.md +26 -0
- package/docs/quickstart.md +1 -1
- package/docs/release-notes/0.1.0-alpha.13.md +91 -0
- package/docs/turbo-starter.md +2 -1
- package/docs/upgrade.md +14 -14
- package/examples/turbo-starter/setup.mjs +9 -2
- package/lib/connectors/files.js +2 -0
- package/lib/integrations/connector/commands.d.ts +11 -4
- package/lib/integrations/connector/commands.js +16 -2
- package/lib/integrations/connector/config.d.ts +5 -0
- package/lib/integrations/connector/config.js +18 -1
- package/lib/integrations/connector/index.d.ts +1 -1
- package/lib/integrations/connector/index.js +1 -1
- package/lib/integrations/connector/snapshot.d.ts +22 -1
- package/lib/integrations/connector/snapshot.js +60 -3
- package/lib/integrations/connector/source.d.ts +4 -0
- package/lib/integrations/connector/source.js +1 -0
- package/package.json +1 -1
- package/skills/harness-setup/SKILL.md +191 -69
- package/templates/controller-mode/README.md +5 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Headless-движок исполнения инженерных поручени
|
|
|
5
5
|
интеграция с принятой веткой через compare-and-swap и машинный результат с SHA
|
|
6
6
|
и доказательствами.
|
|
7
7
|
|
|
8
|
-
> Статус: `0.1.0-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.
|
|
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 установки и обновления. Подробно —
|
package/docs/connectors.md
CHANGED
|
@@ -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 поручения
|
package/docs/quickstart.md
CHANGED
|
@@ -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
|
+
повторялся.
|
package/docs/turbo-starter.md
CHANGED
|
@@ -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.
|
|
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
|
|
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) => {
|
package/lib/connectors/files.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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 = {
|
|
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: harness-setup
|
|
3
|
-
description: Install, configure or upgrade devcontour-harness for a new or existing npm/pnpm Git repository — ask the
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
66
|
-
|
|
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`).
|
|
79
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
###
|
|
242
|
+
### 8. Runtimes, approval, isolation
|
|
188
243
|
|
|
189
|
-
Edit `.harness/policy.json` only as the
|
|
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
|
-
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
|
230
|
-
|
|
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;
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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
|
-
###
|
|
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/
|
|
265
|
-
|
|
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
|
-
###
|
|
385
|
+
### 12. Report and hand over
|
|
271
386
|
|
|
272
|
-
Report: installed version (and the tarball sha256), `doctor`,
|
|
273
|
-
of the
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
`
|
|
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
|
|
345
|
-
|
|
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.
|