devcontour-harness 0.1.0-alpha.5 → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +24 -2
  2. package/docs/compatibility-versions.md +56 -0
  3. package/docs/compatibility.md +2 -2
  4. package/docs/connectors.md +323 -0
  5. package/docs/contracts.md +33 -5
  6. package/docs/integrations/hindsight.md +148 -0
  7. package/docs/quickstart.md +1 -1
  8. package/docs/release-notes/0.1.0-alpha.6.md +238 -0
  9. package/docs/review-probes.md +120 -0
  10. package/docs/review-protocol.md +251 -0
  11. package/docs/toolchains.md +167 -0
  12. package/docs/turbo-starter.md +269 -0
  13. package/docs/upgrade.md +48 -12
  14. package/examples/README.md +1 -0
  15. package/examples/toolchains/README.md +32 -0
  16. package/examples/toolchains/go/bad/label/label.go +9 -0
  17. package/examples/toolchains/go/good/label/label.go +9 -0
  18. package/examples/toolchains/go/policy.json +79 -0
  19. package/examples/toolchains/go/repo/REQUIREMENTS.md +9 -0
  20. package/examples/toolchains/go/repo/_gitignore +1 -0
  21. package/examples/toolchains/go/repo/go.mod +5 -0
  22. package/examples/toolchains/go/repo/go.sum +2 -0
  23. package/examples/toolchains/go/repo/label/label.go +9 -0
  24. package/examples/toolchains/go/repo/label/label_test.go +27 -0
  25. package/examples/toolchains/go/repo/label/version.go +9 -0
  26. package/examples/toolchains/go/task.json +25 -0
  27. package/examples/toolchains/mixed-go/bad/backend/label/label.go +9 -0
  28. package/examples/toolchains/mixed-go/bad/frontend/src/label.js +4 -0
  29. package/examples/toolchains/mixed-go/good/backend/label/label.go +9 -0
  30. package/examples/toolchains/mixed-go/good/frontend/src/label.js +4 -0
  31. package/examples/toolchains/mixed-go/policy.json +126 -0
  32. package/examples/toolchains/mixed-go/repo/REQUIREMENTS.md +9 -0
  33. package/examples/toolchains/mixed-go/repo/_gitignore +6 -0
  34. package/examples/toolchains/mixed-go/repo/backend/go.mod +3 -0
  35. package/examples/toolchains/mixed-go/repo/backend/label/label.go +9 -0
  36. package/examples/toolchains/mixed-go/repo/backend/label/label_test.go +30 -0
  37. package/examples/toolchains/mixed-go/repo/contract/check.mjs +17 -0
  38. package/examples/toolchains/mixed-go/repo/contract/label.json +9 -0
  39. package/examples/toolchains/mixed-go/repo/frontend/package.json +15 -0
  40. package/examples/toolchains/mixed-go/repo/frontend/pnpm-lock.yaml +22 -0
  41. package/examples/toolchains/mixed-go/repo/frontend/src/label.js +4 -0
  42. package/examples/toolchains/mixed-go/repo/frontend/test/label.test.js +11 -0
  43. package/examples/toolchains/mixed-go/task.json +25 -0
  44. package/examples/toolchains/mixed-python/bad/backend/src/fx_backend/__init__.py +6 -0
  45. package/examples/toolchains/mixed-python/bad/frontend/src/label.js +4 -0
  46. package/examples/toolchains/mixed-python/good/backend/src/fx_backend/__init__.py +6 -0
  47. package/examples/toolchains/mixed-python/good/frontend/src/label.js +4 -0
  48. package/examples/toolchains/mixed-python/policy.json +121 -0
  49. package/examples/toolchains/mixed-python/repo/REQUIREMENTS.md +9 -0
  50. package/examples/toolchains/mixed-python/repo/_gitignore +6 -0
  51. package/examples/toolchains/mixed-python/repo/backend/.python-version +1 -0
  52. package/examples/toolchains/mixed-python/repo/backend/pyproject.toml +19 -0
  53. package/examples/toolchains/mixed-python/repo/backend/src/fx_backend/__init__.py +6 -0
  54. package/examples/toolchains/mixed-python/repo/backend/tests/test_label.py +11 -0
  55. package/examples/toolchains/mixed-python/repo/backend/uv.lock +108 -0
  56. package/examples/toolchains/mixed-python/repo/contract/check.mjs +17 -0
  57. package/examples/toolchains/mixed-python/repo/contract/label.json +9 -0
  58. package/examples/toolchains/mixed-python/repo/frontend/package.json +15 -0
  59. package/examples/toolchains/mixed-python/repo/frontend/pnpm-lock.yaml +22 -0
  60. package/examples/toolchains/mixed-python/repo/frontend/src/label.js +4 -0
  61. package/examples/toolchains/mixed-python/repo/frontend/test/label.test.js +11 -0
  62. package/examples/toolchains/mixed-python/task.json +25 -0
  63. package/examples/toolchains/npm/bad/src/label.js +4 -0
  64. package/examples/toolchains/npm/good/src/label.js +4 -0
  65. package/examples/toolchains/npm/policy.json +82 -0
  66. package/examples/toolchains/npm/repo/REQUIREMENTS.md +9 -0
  67. package/examples/toolchains/npm/repo/_gitignore +4 -0
  68. package/examples/toolchains/npm/repo/package-lock.json +22 -0
  69. package/examples/toolchains/npm/repo/package.json +13 -0
  70. package/examples/toolchains/npm/repo/src/label.js +4 -0
  71. package/examples/toolchains/npm/repo/test/label.test.js +13 -0
  72. package/examples/toolchains/npm/task.json +25 -0
  73. package/examples/toolchains/pnpm/bad/packages/label/src/label.js +4 -0
  74. package/examples/toolchains/pnpm/good/packages/label/src/label.js +4 -0
  75. package/examples/toolchains/pnpm/policy.json +85 -0
  76. package/examples/toolchains/pnpm/repo/REQUIREMENTS.md +9 -0
  77. package/examples/toolchains/pnpm/repo/_gitignore +4 -0
  78. package/examples/toolchains/pnpm/repo/package.json +10 -0
  79. package/examples/toolchains/pnpm/repo/packages/checker/check.mjs +5 -0
  80. package/examples/toolchains/pnpm/repo/packages/checker/package.json +9 -0
  81. package/examples/toolchains/pnpm/repo/packages/label/package.json +14 -0
  82. package/examples/toolchains/pnpm/repo/packages/label/src/label.js +4 -0
  83. package/examples/toolchains/pnpm/repo/packages/label/test/label.test.js +13 -0
  84. package/examples/toolchains/pnpm/repo/pnpm-lock.yaml +29 -0
  85. package/examples/toolchains/pnpm/repo/pnpm-workspace.yaml +2 -0
  86. package/examples/toolchains/pnpm/task.json +25 -0
  87. package/examples/toolchains/python/bad/src/fx_label/__init__.py +6 -0
  88. package/examples/toolchains/python/good/src/fx_label/__init__.py +6 -0
  89. package/examples/toolchains/python/policy.json +80 -0
  90. package/examples/toolchains/python/repo/.python-version +1 -0
  91. package/examples/toolchains/python/repo/REQUIREMENTS.md +9 -0
  92. package/examples/toolchains/python/repo/_gitignore +5 -0
  93. package/examples/toolchains/python/repo/pyproject.toml +19 -0
  94. package/examples/toolchains/python/repo/src/fx_label/__init__.py +6 -0
  95. package/examples/toolchains/python/repo/tests/test_label.py +13 -0
  96. package/examples/toolchains/python/repo/uv.lock +108 -0
  97. package/examples/toolchains/python/task.json +25 -0
  98. package/examples/toolchains/turbo/bad/packages/label/src/label.js +4 -0
  99. package/examples/toolchains/turbo/good/packages/label/src/label.js +4 -0
  100. package/examples/toolchains/turbo/policy.json +110 -0
  101. package/examples/toolchains/turbo/repo/REQUIREMENTS.md +9 -0
  102. package/examples/toolchains/turbo/repo/_gitignore +4 -0
  103. package/examples/toolchains/turbo/repo/package.json +13 -0
  104. package/examples/toolchains/turbo/repo/packages/checker/check.mjs +5 -0
  105. package/examples/toolchains/turbo/repo/packages/checker/package.json +9 -0
  106. package/examples/toolchains/turbo/repo/packages/label/package.json +14 -0
  107. package/examples/toolchains/turbo/repo/packages/label/src/label.js +4 -0
  108. package/examples/toolchains/turbo/repo/packages/label/test/label.test.js +13 -0
  109. package/examples/toolchains/turbo/repo/pnpm-lock.yaml +94 -0
  110. package/examples/toolchains/turbo/repo/pnpm-workspace.yaml +2 -0
  111. package/examples/toolchains/turbo/repo/turbo.json +7 -0
  112. package/examples/toolchains/turbo/task.json +25 -0
  113. package/examples/turbo-starter/README.md +16 -0
  114. package/examples/turbo-starter/changes/issued-at-consumer-only.patch +12 -0
  115. package/examples/turbo-starter/changes/issued-at.patch +130 -0
  116. package/examples/turbo-starter/cleanup.mjs +58 -0
  117. package/examples/turbo-starter/graph.json +32 -0
  118. package/examples/turbo-starter/harness/acceptance/db-isolation.test.mjs +97 -0
  119. package/examples/turbo-starter/harness/acceptance/env-probe.test.mjs +75 -0
  120. package/examples/turbo-starter/harness/acceptance/generation.test.mjs +54 -0
  121. package/examples/turbo-starter/harness/acceptance/issued-at.test.mjs +43 -0
  122. package/examples/turbo-starter/harness/acceptance/lifecycle-probe.test.mjs +47 -0
  123. package/examples/turbo-starter/harness/compose-bridge.mjs +593 -0
  124. package/examples/turbo-starter/harness/compose.acceptance.yml +23 -0
  125. package/examples/turbo-starter/harness/env/backend.env.template +7 -0
  126. package/examples/turbo-starter/harness/env/compose.env.template +6 -0
  127. package/examples/turbo-starter/policy.json +109 -0
  128. package/examples/turbo-starter/policy.lifecycle.json +114 -0
  129. package/examples/turbo-starter/setup.mjs +352 -0
  130. package/lib/agents/cli-adapter.js +2 -0
  131. package/lib/agents/review-probe.d.ts +179 -7
  132. package/lib/agents/review-probe.js +648 -55
  133. package/lib/agents/schemas.d.ts +3 -1
  134. package/lib/agents/schemas.js +7 -3
  135. package/lib/cli.js +167 -7
  136. package/lib/contracts/cli.d.ts +820 -11
  137. package/lib/contracts/cli.js +243 -4
  138. package/lib/contracts/errors.d.ts +3 -1
  139. package/lib/contracts/errors.js +2 -0
  140. package/lib/contracts/events.d.ts +37 -5
  141. package/lib/contracts/events.js +9 -0
  142. package/lib/contracts/execution.d.ts +176 -8
  143. package/lib/contracts/execution.js +9 -1
  144. package/lib/contracts/failure.js +5 -0
  145. package/lib/contracts/index.d.ts +1 -0
  146. package/lib/contracts/index.js +1 -0
  147. package/lib/contracts/policy.d.ts +15 -0
  148. package/lib/contracts/policy.js +10 -0
  149. package/lib/contracts/ports.d.ts +50 -0
  150. package/lib/contracts/profiles.d.ts +14 -0
  151. package/lib/contracts/profiles.js +4 -0
  152. package/lib/contracts/published.d.ts +230 -7
  153. package/lib/contracts/result.d.ts +171 -0
  154. package/lib/contracts/result.js +39 -1
  155. package/lib/contracts/review.d.ts +231 -0
  156. package/lib/contracts/review.js +326 -2
  157. package/lib/contracts/toolchain.d.ts +96 -0
  158. package/lib/contracts/toolchain.js +89 -0
  159. package/lib/control/doctor.d.ts +16 -1
  160. package/lib/control/doctor.js +45 -3
  161. package/lib/control/harness.d.ts +52 -6
  162. package/lib/control/harness.js +28 -1
  163. package/lib/control/recovery.d.ts +3 -1
  164. package/lib/control/review.js +14 -6
  165. package/lib/engine/classify.js +1 -0
  166. package/lib/engine/engine.d.ts +39 -15
  167. package/lib/engine/prompt.d.ts +29 -1
  168. package/lib/engine/prompt.js +57 -0
  169. package/lib/engine/property-recovery.js +2 -0
  170. package/lib/engine/result.js +52 -0
  171. package/lib/engine/reuse.js +6 -1
  172. package/lib/engine/runner.d.ts +25 -0
  173. package/lib/engine/runner.js +491 -122
  174. package/lib/environment/lifecycle.js +6 -0
  175. package/lib/environment/live-calls.js +3 -2
  176. package/lib/environment/os-tools.d.ts +16 -0
  177. package/lib/environment/os-tools.js +35 -0
  178. package/lib/environment/process.js +5 -4
  179. package/lib/integrations/connector/commands.d.ts +358 -0
  180. package/lib/integrations/connector/commands.js +428 -0
  181. package/lib/integrations/connector/config.d.ts +215 -0
  182. package/lib/integrations/connector/config.js +214 -0
  183. package/lib/integrations/connector/delivery.d.ts +107 -0
  184. package/lib/integrations/connector/delivery.js +591 -0
  185. package/lib/integrations/connector/fake.d.ts +291 -0
  186. package/lib/integrations/connector/fake.js +786 -0
  187. package/lib/integrations/connector/history.d.ts +124 -0
  188. package/lib/integrations/connector/history.js +497 -0
  189. package/lib/integrations/connector/index.d.ts +16 -0
  190. package/lib/integrations/connector/index.js +16 -0
  191. package/lib/integrations/connector/redact.d.ts +19 -0
  192. package/lib/integrations/connector/redact.js +91 -0
  193. package/lib/integrations/connector/registry.d.ts +15 -0
  194. package/lib/integrations/connector/registry.js +43 -0
  195. package/lib/integrations/connector/snapshot.d.ts +131 -0
  196. package/lib/integrations/connector/snapshot.js +422 -0
  197. package/lib/integrations/connector/source.d.ts +37 -0
  198. package/lib/integrations/connector/source.js +73 -0
  199. package/lib/integrations/connector/steps.d.ts +71 -0
  200. package/lib/integrations/connector/steps.js +188 -0
  201. package/lib/integrations/connector/store.d.ts +140 -0
  202. package/lib/integrations/connector/store.js +277 -0
  203. package/lib/integrations/connector/summary.d.ts +38 -0
  204. package/lib/integrations/connector/summary.js +59 -0
  205. package/lib/integrations/connector/testing-worker.d.ts +1 -0
  206. package/lib/integrations/connector/testing-worker.js +81 -0
  207. package/lib/integrations/connector/testing.d.ts +205 -0
  208. package/lib/integrations/connector/testing.js +1615 -0
  209. package/lib/integrations/connector/transport.d.ts +240 -0
  210. package/lib/integrations/connector/transport.js +28 -0
  211. package/lib/integrations/memory/context.d.ts +120 -0
  212. package/lib/integrations/memory/context.js +239 -0
  213. package/lib/integrations/memory/index.d.ts +8 -0
  214. package/lib/integrations/memory/index.js +8 -0
  215. package/lib/integrations/memory/provider.d.ts +79 -0
  216. package/lib/integrations/memory/provider.js +22 -0
  217. package/lib/integrations/memory/sink.d.ts +56 -0
  218. package/lib/integrations/memory/sink.js +116 -0
  219. package/lib/integrations/memory/testing.d.ts +44 -0
  220. package/lib/integrations/memory/testing.js +61 -0
  221. package/lib/integrations/relay-v1/index.d.ts +122 -0
  222. package/lib/integrations/relay-v1/index.js +860 -0
  223. package/lib/integrations/relay-v2/index.d.ts +145 -0
  224. package/lib/integrations/relay-v2/index.js +752 -0
  225. package/lib/integrations/relay-v2/process.d.ts +32 -0
  226. package/lib/integrations/relay-v2/process.js +112 -0
  227. package/lib/integrations/relay-v2/protocol.d.ts +172 -0
  228. package/lib/integrations/relay-v2/protocol.js +142 -0
  229. package/lib/profiles/resolve.d.ts +5 -1
  230. package/lib/profiles/resolve.js +31 -0
  231. package/lib/storage/document.d.ts +60 -12
  232. package/lib/toolchains/index.d.ts +9 -0
  233. package/lib/toolchains/index.js +9 -0
  234. package/lib/toolchains/inspect.d.ts +105 -0
  235. package/lib/toolchains/inspect.js +831 -0
  236. package/lib/toolchains/manifests.d.ts +67 -0
  237. package/lib/toolchains/manifests.js +238 -0
  238. package/lib/toolchains/probe.d.ts +28 -0
  239. package/lib/toolchains/probe.js +54 -0
  240. package/lib/toolchains/recipes.d.ts +33 -0
  241. package/lib/toolchains/recipes.js +51 -0
  242. package/lib/verification/acceptance.d.ts +16 -0
  243. package/lib/verification/acceptance.js +16 -0
  244. package/lib/verification/gates.d.ts +16 -0
  245. package/lib/verification/gates.js +44 -3
  246. package/package.json +22 -1
  247. package/profiles/go-gotestsum.json +21 -0
  248. package/profiles/npm-ci.json +22 -0
  249. package/profiles/pnpm-frozen.json +24 -0
  250. package/profiles/python-uv.json +26 -0
  251. package/schemas/execution-document.schema.json +155 -0
  252. package/schemas/execution-policy.schema.json +48 -0
  253. package/schemas/result-envelope.schema.json +213 -1
  254. package/schemas/run-event.schema.json +93 -0
  255. package/schemas/run-result.schema.json +207 -0
  256. package/schemas/stack-profile.schema.json +43 -0
  257. package/templates/controller-mode/AGENTS.controller.md +58 -0
  258. package/templates/controller-mode/README.md +44 -0
@@ -0,0 +1,752 @@
1
+ /**
2
+ * Opt-in entrypoint `devcontour-harness/integrations/relay-v2`: транспорт
3
+ * Relay-2 через настроенный CLI binary (subprocess, argv без shell).
4
+ *
5
+ * Адаптер не импортирует Bun-зависимый `@relay/core` и не открывает SQLite
6
+ * получателя: всё идёт через `relay … --format json`. Каждый вызов адресует
7
+ * ровно настроенный проект (`--config` с абсолютным путём, `--project` для
8
+ * workspace); cwd процесса на выбор проекта не влияет. Мутации выполняются
9
+ * один раз, без повторов; перед каждой сверяется projectId хранилища.
10
+ */
11
+ import { createHash } from 'node:crypto';
12
+ import { createReadStream } from 'node:fs';
13
+ import { isAbsolute } from 'node:path';
14
+ import { z } from 'zod';
15
+ import { HarnessError } from "../../contracts/errors.js";
16
+ import { TransportError, } from "../connector/transport.js";
17
+ import { runCli } from "./process.js";
18
+ import { checkpointSchema, classifyCode, commentSchema, doctorSchema, documentSchema, failureSchema, itemsSchema, pageMetaSchema, progressSchema, projectSchema, sessionSchema, storageStatusSchema, successSchema, taskSchema, } from "./protocol.js";
19
+ export const PROVIDER = 'relay-v2-cli';
20
+ /** Переменные, которые выбирают проект/автора в обход явных настроек. */
21
+ const RESERVED_ENV = ['RELAY_CONFIG', 'RELAY_ACTOR'];
22
+ export const relayV2SettingsSchema = z
23
+ .object({
24
+ /** Абсолютный путь к собранному binary Relay-2 (не имя из PATH). */
25
+ binary: z.string().refine(isAbsolute, 'нужен абсолютный путь'),
26
+ /** Абсолютный путь к `relay.config.json` проекта или `relay.workspace.json`. */
27
+ config: z.string().refine(isAbsolute, 'нужен абсолютный путь'),
28
+ /** Имя проекта в workspace (`--project`). */
29
+ project: z.string().min(1).optional(),
30
+ /** Ожидаемый постоянный ID проекта; расхождение блокирует чтение и мутации. */
31
+ projectId: z.string().min(1),
32
+ /** Автор записей (`--actor`). */
33
+ actor: z.string().min(1),
34
+ /**
35
+ * Явное окружение процесса: `RELAY_HOME`, на macOS — `DYLD_LIBRARY_PATH`
36
+ * к SQLite >= 3.44 и т. п. Наследуются только PATH и HOME.
37
+ */
38
+ env: z
39
+ .record(z.string(), z.string())
40
+ .refine((env) => !RESERVED_ENV.some((k) => k in env), {
41
+ message: `${RESERVED_ENV.join('/')} задаются только через config/actor`,
42
+ })
43
+ .optional(),
44
+ timeoutMs: z.number().int().positive().max(600_000).default(30_000),
45
+ maxOutputBytes: z
46
+ .number()
47
+ .int()
48
+ .positive()
49
+ .max(256 * 1024 * 1024)
50
+ .default(16 * 1024 * 1024),
51
+ /** Ожидаемая идентичность сборки: sha256 binary проверяется, commit записывается. */
52
+ build: z
53
+ .object({
54
+ sha256: z
55
+ .string()
56
+ .regex(/^[0-9a-f]{64}$/)
57
+ .optional(),
58
+ commit: z.string().min(1).optional(),
59
+ })
60
+ .strict()
61
+ .optional(),
62
+ })
63
+ .strict();
64
+ /** Capabilities, выводимые пробами чтения несуществующей записи. */
65
+ export const RELAY_V2_CAPABILITIES = {
66
+ 'relay-2.task.read': ['task', 'get'],
67
+ 'relay-2.task.children': ['task', 'children'],
68
+ 'relay-2.task.progress': ['task', 'progress'],
69
+ 'relay-2.task.criterion': ['task', 'criterion', 'list'],
70
+ 'relay-2.task.comment': ['task', 'comment', 'list'],
71
+ 'relay-2.document.target': ['document', 'list', '--target'],
72
+ 'relay-2.history.session': ['session', 'get'],
73
+ 'relay-2.history.checkpoint': ['checkpoint', 'get'],
74
+ 'relay-2.history.list': ['session', 'history'],
75
+ };
76
+ const PROBE_REF = '00000000-0000-0000-0000-000000000000';
77
+ /** Ref/ID в позиционном аргументе: ключ, alias или UUID; не может начинаться с `-`. */
78
+ const REF = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$/;
79
+ export function createTransport(settings, options = {}) {
80
+ const parsed = relayV2SettingsSchema.safeParse(settings);
81
+ if (!parsed.success)
82
+ throw new HarnessError('INVALID_INPUT', 'Неверные настройки relay-v2-cli', parsed.error.issues.map((i) => ({
83
+ path: i.path.filter((p) => typeof p !== 'symbol'),
84
+ message: i.message,
85
+ })));
86
+ return new RelayV2Transport(parsed.data, options);
87
+ }
88
+ export class RelayV2Transport {
89
+ settings;
90
+ options;
91
+ provider = PROVIDER;
92
+ verified;
93
+ cwd;
94
+ constructor(settings, options = {}) {
95
+ this.settings = settings;
96
+ this.options = options;
97
+ this.cwd = options.cwd ?? process.cwd();
98
+ }
99
+ /* ---------- процесс и протокол ---------- */
100
+ env() {
101
+ const env = {};
102
+ for (const key of ['PATH', 'HOME']) {
103
+ const value = process.env[key];
104
+ if (value !== undefined)
105
+ env[key] = value;
106
+ }
107
+ // Унаследованные RELAY_CONFIG/RELAY_ACTOR/RELAY_USER_* сюда не попадают:
108
+ // окружение собирается заново, а не копируется.
109
+ return { ...env, ...this.settings.env };
110
+ }
111
+ argv(args, mutation) {
112
+ return [
113
+ ...args,
114
+ `--config=${this.settings.config}`,
115
+ '--format=json',
116
+ ...(this.settings.project ? [`--project=${this.settings.project}`] : []),
117
+ ...(mutation ? [`--actor=${this.settings.actor}`] : []),
118
+ ];
119
+ }
120
+ fail(kind, operation, message, extra = {}) {
121
+ return new TransportError(kind, operation, `relay-v2 ${operation}: ${message}`, {
122
+ provider: PROVIDER,
123
+ projectId: this.settings.projectId,
124
+ ...extra,
125
+ });
126
+ }
127
+ log(operation, argv, mutation, run, outcome, code) {
128
+ if (!this.options.onCall)
129
+ return;
130
+ this.options.onCall({
131
+ operation,
132
+ argv: [this.settings.binary, ...argv.map(sanitizeArg)],
133
+ cwd: this.cwd,
134
+ mutation,
135
+ exitCode: run?.code ?? null,
136
+ signal: run?.signal ?? null,
137
+ durationMs: run?.durationMs ?? 0,
138
+ ...(code ? { code } : {}),
139
+ outcome,
140
+ });
141
+ }
142
+ /** Один вызов CLI → `{data, meta}` успешного envelope или TransportError. */
143
+ async call(operation, args, { mutation, signal }) {
144
+ const argv = this.argv(args, mutation);
145
+ const run = await runCli(this.settings.binary, argv, {
146
+ cwd: this.cwd,
147
+ env: this.env(),
148
+ timeoutMs: this.settings.timeoutMs,
149
+ maxOutputBytes: this.settings.maxOutputBytes,
150
+ killGraceMs: this.options.killGraceMs ?? 1000,
151
+ ...(signal ? { signal } : {}),
152
+ });
153
+ const flag = { mutation };
154
+ const done = (kind, code) => this.log(operation, argv, mutation, run, kind, code);
155
+ if (run.spawnError || !run.started) {
156
+ done('unavailable', run.spawnError?.code);
157
+ const reason = run.spawnError
158
+ ? `binary не запущен (${run.spawnError.code ?? 'spawn error'})`
159
+ : 'отменено до запуска';
160
+ throw this.fail('unavailable', operation, reason, {
161
+ ...flag,
162
+ ...(run.spawnError?.code ? { code: run.spawnError.code } : {}),
163
+ });
164
+ }
165
+ if (run.timedOut || run.aborted) {
166
+ const kind = mutation ? 'uncertain' : 'unavailable';
167
+ done(kind);
168
+ throw this.fail(kind, operation, `${run.timedOut ? `таймаут ${this.settings.timeoutMs} мс` : 'отменено'} после запуска процесса${mutation ? '; мутация могла примениться' : ''}`, flag);
169
+ }
170
+ if (run.oversized) {
171
+ done('protocol');
172
+ throw this.fail('protocol', operation, `вывод больше ${this.settings.maxOutputBytes} байт`, flag);
173
+ }
174
+ if (run.code === 0) {
175
+ const body = parseLine(run.stdout);
176
+ const ok = successSchema.safeParse(body);
177
+ if (!ok.success) {
178
+ done('protocol');
179
+ throw this.fail('protocol', operation, 'stdout не является envelope {ok:true,data}', flag);
180
+ }
181
+ done('ok');
182
+ return { data: ok.data.data, meta: ok.data.meta };
183
+ }
184
+ const failure = failureSchema.safeParse(parseLine(run.stderr));
185
+ if (!failure.success) {
186
+ // Ненулевой exit без структурной ошибки: процесс убит сигналом или
187
+ // вывод не по договору. Для мутации исход неизвестен.
188
+ const kind = run.code === null && mutation ? 'uncertain' : 'protocol';
189
+ done(kind);
190
+ throw this.fail(kind, operation, run.code === null
191
+ ? `процесс завершён сигналом ${run.signal ?? '?'}`
192
+ : `exit ${run.code} без JSON-ошибки в stderr`, flag);
193
+ }
194
+ const { code, message, details } = failure.data.error;
195
+ const kind = classifyCode(code, run.code, mutation);
196
+ const actual = details?.actualRevision;
197
+ done(kind, code);
198
+ throw this.fail(kind, operation, `${code}: ${message.slice(0, 300)}`, {
199
+ ...flag,
200
+ code,
201
+ ...(typeof actual === 'number' ? { actualRevision: actual } : {}),
202
+ });
203
+ }
204
+ shape(operation, schema, value, mutation) {
205
+ const parsed = schema.safeParse(value);
206
+ if (!parsed.success)
207
+ throw this.fail('protocol', operation, `неожиданная форма ответа: ${parsed.error.issues[0]?.path.join('.') ?? ''} ${parsed.error.issues[0]?.message ?? ''}`.trim(), { mutation });
208
+ return parsed.data;
209
+ }
210
+ /** Полный список по `meta.page.nextCursor`; при SNAPSHOT_CONFLICT — заново, ограниченно. */
211
+ async list(operation, args, item, signal) {
212
+ const limit = Math.min(100, Math.max(1, this.options.pageLimit ?? 100));
213
+ const maxPages = this.options.maxPages ?? 100;
214
+ const restarts = this.options.snapshotRestarts ?? 3;
215
+ for (let attempt = 0; attempt <= restarts; attempt++) {
216
+ const items = [];
217
+ let cursor = null;
218
+ let restart = false;
219
+ for (let page = 0;; page++) {
220
+ if (page >= maxPages)
221
+ throw this.fail('protocol', operation, `больше ${maxPages} страниц`, { mutation: false });
222
+ let res;
223
+ try {
224
+ // nextCommand не исполняется: argv собирается заново с тем же запросом.
225
+ res = await this.call(operation, cursor ? [...args, `--cursor=${cursor}`] : [...args, `--limit=${limit}`], { mutation: false, signal });
226
+ }
227
+ catch (error) {
228
+ if (error instanceof TransportError && error.details.code === 'SNAPSHOT_CONFLICT') {
229
+ restart = true;
230
+ break;
231
+ }
232
+ throw error;
233
+ }
234
+ const data = this.shape(operation, itemsSchema, res.data, false);
235
+ const meta = this.shape(operation, pageMetaSchema, res.meta, false);
236
+ for (const raw of data.items)
237
+ items.push(this.shape(operation, item, raw, false));
238
+ cursor = meta.page.nextCursor;
239
+ if (!cursor)
240
+ break;
241
+ }
242
+ if (!restart)
243
+ return items;
244
+ }
245
+ throw this.fail('conflict', operation, `данные менялись во время чтения ${restarts + 1} раз подряд`, {
246
+ code: 'SNAPSHOT_CONFLICT',
247
+ mutation: false,
248
+ });
249
+ }
250
+ ref(operation, value, mutation) {
251
+ if (!REF.test(value))
252
+ throw this.fail('refused', operation, 'недопустимый ref (ожидается ключ или UUID)', {
253
+ mutation,
254
+ code: 'INVALID_REF',
255
+ });
256
+ return value;
257
+ }
258
+ /* ---------- идентичность и защита проекта ---------- */
259
+ async status(operation, signal, mutation) {
260
+ const res = await this.call(`${operation}:storage-status`, ['storage', 'status'], {
261
+ mutation: false,
262
+ signal,
263
+ }).catch((error) => {
264
+ // Отказ проверки перед мутацией: мутация не отправлена.
265
+ if (mutation && error instanceof TransportError && error.kind === 'uncertain')
266
+ throw this.fail('unavailable', operation, error.message, { mutation });
267
+ throw error;
268
+ });
269
+ const status = this.shape(operation, storageStatusSchema, res.data, false);
270
+ if (status.projectId !== this.settings.projectId)
271
+ throw this.fail('refused', operation, `projectId ${status.projectId} не совпадает с настроенным`, {
272
+ code: 'PROJECT_ID_MISMATCH',
273
+ mutation,
274
+ });
275
+ if (this.verified && status.databasePath !== this.verified.databasePath)
276
+ throw this.fail('refused', operation, 'путь базы проекта изменился после проверки идентичности', {
277
+ code: 'PROJECT_ID_MISMATCH',
278
+ mutation,
279
+ });
280
+ return status;
281
+ }
282
+ async ensure(operation, signal, mutation, ...caps) {
283
+ if (!this.verified)
284
+ await this.identity(signal);
285
+ const have = this.verified.identity.capabilities;
286
+ const missing = caps.filter((c) => !have.includes(c));
287
+ if (missing.length)
288
+ throw this.fail('unsupported', operation, `сборка не поддерживает ${missing.join(', ')}`, {
289
+ mutation,
290
+ });
291
+ }
292
+ /** Перед мутацией: идентичность сборки/проекта и свежая сверка projectId. */
293
+ async guard(operation, signal, ...caps) {
294
+ await this.ensure(operation, signal, true, ...caps);
295
+ await this.status(operation, signal, true);
296
+ }
297
+ async identity(signal) {
298
+ const op = 'identity';
299
+ const build = {};
300
+ let sha256;
301
+ try {
302
+ sha256 = await hashFile(this.settings.binary);
303
+ }
304
+ catch (error) {
305
+ throw this.fail('unavailable', op, `binary недоступен (${error.code ?? 'read error'})`, { mutation: false });
306
+ }
307
+ build.sha256 = sha256;
308
+ if (this.settings.build?.commit)
309
+ build.commit = this.settings.build.commit;
310
+ if (this.settings.build?.sha256) {
311
+ if (this.settings.build.sha256 !== sha256)
312
+ throw this.fail('unsupported', op, `sha256 binary ${sha256} не совпадает с настроенной сборкой`, {
313
+ code: 'BUILD_MISMATCH',
314
+ mutation: false,
315
+ });
316
+ build.sha256Verified = 'true';
317
+ }
318
+ else
319
+ build.sha256Verified = 'false';
320
+ // Пока проверка не прошла заново, мутации заблокированы.
321
+ this.verified = undefined;
322
+ const status = await this.status(op, signal, false);
323
+ // project get читает сущности: падает на SQLite < 3.44 (STORAGE_ERROR).
324
+ const project = this.shape(op, projectSchema, (await this.call(`${op}:project-get`, ['project', 'get'], { mutation: false, signal })).data, false);
325
+ if (project.id !== this.settings.projectId)
326
+ throw this.fail('refused', op, `project get вернул ${project.id}`, {
327
+ code: 'PROJECT_ID_MISMATCH',
328
+ mutation: false,
329
+ });
330
+ const doctor = this.shape(op, doctorSchema, (await this.call(`${op}:doctor`, ['doctor', 'check'], { mutation: false, signal })).data, false);
331
+ if (doctor.projectId !== this.settings.projectId)
332
+ throw this.fail('refused', op, `doctor check вернул ${doctor.projectId}`, {
333
+ code: 'PROJECT_ID_MISMATCH',
334
+ mutation: false,
335
+ });
336
+ if (!doctor.valid)
337
+ throw this.fail('refused', op, `doctor check: ${doctor.issues.length} нарушений целостности`, { code: 'INTEGRITY_INVALID', mutation: false });
338
+ const capabilities = ['relay-2.envelope.json-v1'];
339
+ const probes = await Promise.all(Object.entries(RELAY_V2_CAPABILITIES).map(async ([cap, args]) => {
340
+ const argv = args[args.length - 1] === '--target'
341
+ ? [...args.slice(0, -1), `--target=${PROBE_REF}`]
342
+ : [...args, PROBE_REF];
343
+ try {
344
+ await this.call(`${op}:probe`, argv, { mutation: false, signal });
345
+ return cap;
346
+ }
347
+ catch (error) {
348
+ if (!(error instanceof TransportError))
349
+ throw error;
350
+ // Команда есть: несуществующая запись даёт NOT_FOUND.
351
+ if (error.details.code === 'NOT_FOUND')
352
+ return cap;
353
+ // Нет команды/параметра: ошибка разбора argv.
354
+ if (error.details.code === 'INVALID_ARGUMENT')
355
+ return undefined;
356
+ throw error;
357
+ }
358
+ }));
359
+ for (const cap of probes)
360
+ if (cap)
361
+ capabilities.push(cap);
362
+ build.generation = String(status.generation);
363
+ const identity = {
364
+ provider: PROVIDER,
365
+ projectId: status.projectId,
366
+ capabilities,
367
+ build,
368
+ };
369
+ this.verified = { identity, databasePath: status.databasePath };
370
+ return identity;
371
+ }
372
+ /* ---------- чтение ---------- */
373
+ async readTask(ref, signal) {
374
+ const op = 'task.read';
375
+ this.ref(op, ref, false);
376
+ await this.ensure(op, signal, false, 'relay-2.task.read', 'relay-2.task.children', 'relay-2.task.progress', 'relay-2.document.target');
377
+ const before = await this.status(op, signal, false);
378
+ const task = await this.getTask(op, ref, signal);
379
+ const children = await this.list(op, ['task', 'children', task.id], taskSchema, signal);
380
+ const childSummaries = [];
381
+ for (const child of children)
382
+ childSummaries.push(await this.summary(op, child, signal));
383
+ const dependencies = [];
384
+ for (const id of task.data.dependencies)
385
+ dependencies.push(await this.summary(op, await this.getTask(op, this.ref(op, id, false), signal), signal));
386
+ const listed = await this.list(op, ['document', 'list', `--target=${task.id}`], documentSchema, signal);
387
+ const documents = [];
388
+ for (const doc of listed) {
389
+ const full = this.shape(op, documentSchema, (await this.call(op, ['document', 'get', this.ref(op, doc.id, false)], {
390
+ mutation: false,
391
+ signal,
392
+ })).data, false);
393
+ documents.push({
394
+ id: full.id,
395
+ key: full.key,
396
+ name: full.data.name,
397
+ body: full.data.body,
398
+ revision: full.revision,
399
+ });
400
+ }
401
+ const after = await this.status(op, signal, false);
402
+ return {
403
+ task: taskRecord(task),
404
+ children: childSummaries,
405
+ dependencies,
406
+ documents,
407
+ consistency: { before: String(before.generation), after: String(after.generation) },
408
+ };
409
+ }
410
+ async getTask(op, ref, signal) {
411
+ return this.shape(op, taskSchema, (await this.call(op, ['task', 'get', ref], { mutation: false, signal })).data, false);
412
+ }
413
+ async summary(op, task, signal) {
414
+ const progress = this.shape(op, progressSchema, (await this.call(op, ['task', 'progress', this.ref(op, task.id, false)], {
415
+ mutation: false,
416
+ signal,
417
+ })).data, false);
418
+ return {
419
+ id: task.id,
420
+ key: task.key,
421
+ title: task.data.title,
422
+ column: task.data.column,
423
+ revision: task.revision,
424
+ finished: progress.completed,
425
+ };
426
+ }
427
+ async listComments(ref, signal) {
428
+ const op = 'task.comments';
429
+ this.ref(op, ref, false);
430
+ await this.ensure(op, signal, false, 'relay-2.task.comment');
431
+ const items = await this.list(op, ['task', 'comment', 'list', ref], commentSchema, signal);
432
+ return items.map((c) => ({
433
+ id: c.id,
434
+ title: c.title,
435
+ body: c.body,
436
+ actor: c.createdBy,
437
+ createdAt: c.createdAt,
438
+ }));
439
+ }
440
+ /* ---------- мутации задачи ---------- */
441
+ async addComment(ref, input, signal) {
442
+ const op = 'task.comment';
443
+ this.ref(op, ref, true);
444
+ await this.guard(op, signal, 'relay-2.task.comment');
445
+ // Create-mode: без --if-revision. Комментарий append-only, ревизию задачи не меняет.
446
+ const res = await this.call(op, [
447
+ 'task',
448
+ 'comment',
449
+ 'add',
450
+ ref,
451
+ ...(input.title ? [`--title=${input.title}`] : []),
452
+ `--body=${input.body}`,
453
+ ], { mutation: true, signal });
454
+ const task = this.shape(op, taskSchema, res.data, true);
455
+ const last = task.data.comments?.at(-1);
456
+ if (!last || last.body !== input.body)
457
+ throw this.fail('protocol', op, 'ответ не содержит добавленный комментарий', {
458
+ mutation: true,
459
+ });
460
+ return { commentId: last.id };
461
+ }
462
+ async setCriterion(ref, input, signal) {
463
+ const op = 'task.criterion';
464
+ this.ref(op, ref, true);
465
+ this.ref(op, input.criterionId, true);
466
+ await this.guard(op, signal, 'relay-2.task.criterion');
467
+ const res = await this.call(op, [
468
+ 'task',
469
+ 'criterion',
470
+ input.completed ? 'complete' : 'reopen',
471
+ ref,
472
+ input.criterionId,
473
+ `--if-revision=${revision(input.ifRevision)}`,
474
+ ], { mutation: true, signal });
475
+ const task = this.shape(op, taskSchema, res.data, true);
476
+ const criterion = task.data.acceptanceCriteria.find((c) => c.id === input.criterionId);
477
+ if (!criterion || criterion.completed !== input.completed)
478
+ throw this.fail('protocol', op, 'ответ не отражает изменение критерия', { mutation: true });
479
+ return { revision: task.revision };
480
+ }
481
+ async move(ref, input, signal) {
482
+ const op = 'task.move';
483
+ this.ref(op, ref, true);
484
+ await this.guard(op, signal, 'relay-2.task.read');
485
+ const res = await this.call(op, [
486
+ 'task',
487
+ 'move',
488
+ ref,
489
+ `--column=${input.column}`,
490
+ `--if-revision=${revision(input.ifRevision)}`,
491
+ ], { mutation: true, signal });
492
+ const task = this.shape(op, taskSchema, res.data, true);
493
+ if (task.data.column !== input.column)
494
+ throw this.fail('protocol', op, 'ответ не отражает перенос', { mutation: true });
495
+ return { revision: task.revision };
496
+ }
497
+ /* ---------- история ---------- */
498
+ async startSession(input, signal) {
499
+ const op = 'history.start';
500
+ await this.guard(op, signal, 'relay-2.history.session');
501
+ const res = await this.call(op, [
502
+ 'session',
503
+ 'start',
504
+ `--title=${input.title}`,
505
+ ...(input.description ? [`--description=${input.description}`] : []),
506
+ `--client=${input.client}`,
507
+ ...(input.sourceSessionId ? [`--source-session-id=${input.sourceSessionId}`] : []),
508
+ ], { mutation: true, signal });
509
+ return this.sessionMutation(op, res.data);
510
+ }
511
+ async getSession(ref, signal) {
512
+ const op = 'history.get';
513
+ this.ref(op, ref, false);
514
+ await this.ensure(op, signal, false, 'relay-2.history.session');
515
+ return sessionRecord(await this.readSession(op, ref, signal, false));
516
+ }
517
+ async readSession(op, ref, signal, mutation) {
518
+ try {
519
+ return this.shape(op, sessionSchema, (await this.call(op, ['session', 'get', ref], { mutation: false, signal })).data, false);
520
+ }
521
+ catch (error) {
522
+ if (!mutation)
523
+ throw error;
524
+ // Мутация уже подтверждена ответом CLI; не удалось только дочитать
525
+ // сессию. Исход для ядра — неизвестен до read-back.
526
+ const reason = error instanceof Error ? error.message : String(error);
527
+ throw this.fail('uncertain', op, `мутация применена, но сессия не дочитана: ${reason}`, {
528
+ mutation: true,
529
+ code: 'READBACK_FAILED',
530
+ });
531
+ }
532
+ }
533
+ async findSessions(marker, signal) {
534
+ const op = 'history.find';
535
+ if (!marker.trim())
536
+ throw this.fail('refused', op, 'пустой marker', { mutation: false });
537
+ await this.ensure(op, signal, false, 'relay-2.history.session');
538
+ // --q ищет по подстроке, но точность поиска — дело backend: фильтруем сами.
539
+ const items = await this.list(op, ['session', 'list', `--q=${marker}`], sessionSchema, signal);
540
+ return items
541
+ .filter((s) => s.data.title.includes(marker) || s.data.description.includes(marker))
542
+ .map(sessionRecord);
543
+ }
544
+ async checkpoints(sessionRef, signal) {
545
+ const op = 'history.checkpoints';
546
+ this.ref(op, sessionRef, false);
547
+ await this.ensure(op, signal, false, 'relay-2.history.list', 'relay-2.history.session');
548
+ const session = await this.readSession(op, sessionRef, signal, false);
549
+ const items = await this.list(op, ['session', 'history', session.id], checkpointSchema, signal);
550
+ // История ветки может включать потомков: оставляем только эту сессию.
551
+ return items.filter((c) => c.data.sessionId === session.id).map(checkpointRecord);
552
+ }
553
+ async openIntent(sessionRef, input, signal) {
554
+ const op = 'history.intent';
555
+ this.ref(op, sessionRef, true);
556
+ return this.writeCheckpoint(op, ['checkpoint', 'open', sessionRef], 'intent', input, signal);
557
+ }
558
+ async checkpoint(intentRef, kind, input, signal) {
559
+ const op = `history.${kind}`;
560
+ this.ref(op, intentRef, true);
561
+ const verb = { progress: 'save', result: 'complete', cancel: 'cancel' }[kind];
562
+ return this.writeCheckpoint(op, ['checkpoint', verb, intentRef], kind, input, signal);
563
+ }
564
+ async writeCheckpoint(op, head, kind, input, signal) {
565
+ this.ref(op, input.runId, true);
566
+ if (input.acceptedSessionIds?.length && kind !== 'result')
567
+ throw this.fail('refused', op, 'acceptedSessionIds допустимы только в checkpoint результата', { mutation: true });
568
+ for (const id of [...(input.references ?? []), ...(input.acceptedSessionIds ?? [])])
569
+ this.ref(op, id, true);
570
+ await this.guard(op, signal, 'relay-2.history.checkpoint', 'relay-2.history.session');
571
+ // Каждый checkpoint — новая запись (не no-op), даже с тем же содержанием.
572
+ const res = await this.call(op, [
573
+ ...head,
574
+ `--run=${input.runId}`,
575
+ `--title=${input.title}`,
576
+ `--body=${input.body}`,
577
+ ...(input.summary ? [`--summary=${input.summary}`] : []),
578
+ ...(input.checks ? [`--checks=${input.checks}`] : []),
579
+ ...(input.references ?? []).map((r) => `--references=${r}`),
580
+ ...(input.acceptedSessionIds ?? []).map((s) => `--accept=${s}`),
581
+ `--if-revision=${revision(input.ifRevision)}`,
582
+ ], { mutation: true, signal });
583
+ const cp = this.shape(op, checkpointSchema, res.data, true);
584
+ if (cp.data.kind !== kind)
585
+ throw this.fail('protocol', op, `ожидался checkpoint ${kind}, получен ${cp.data.kind}`, {
586
+ mutation: true,
587
+ });
588
+ const session = await this.readSession(op, cp.data.sessionId, signal, true);
589
+ return {
590
+ session: sessionRecord(session),
591
+ runId: input.runId,
592
+ checkpoint: checkpointRecord(cp),
593
+ };
594
+ }
595
+ async fork(intentRef, input, signal) {
596
+ const op = 'history.fork';
597
+ this.ref(op, intentRef, true);
598
+ this.ref(op, input.runId, true);
599
+ await this.guard(op, signal, 'relay-2.history.session', 'relay-2.history.checkpoint');
600
+ const res = await this.call(op, [
601
+ 'session',
602
+ 'fork',
603
+ intentRef,
604
+ `--run=${input.runId}`,
605
+ `--title=${input.title}`,
606
+ ...(input.description ? [`--description=${input.description}`] : []),
607
+ `--if-revision=${revision(input.ifRevision)}`,
608
+ ], { mutation: true, signal });
609
+ const child = this.shape(op, sessionSchema, res.data, true);
610
+ if (!child.data.parentSessionId)
611
+ throw this.fail('protocol', op, 'дочерняя сессия без parentSessionId', { mutation: true });
612
+ const parent = await this.readSession(op, child.data.parentSessionId, signal, true);
613
+ return {
614
+ session: sessionRecord(parent),
615
+ runId: input.runId,
616
+ childSession: sessionRecord(child),
617
+ };
618
+ }
619
+ async attach(sessionRef, input, signal) {
620
+ const op = 'history.attach';
621
+ this.ref(op, sessionRef, true);
622
+ await this.guard(op, signal, 'relay-2.history.session');
623
+ const res = await this.call(op, [
624
+ 'session',
625
+ 'attach',
626
+ sessionRef,
627
+ `--client=${input.client}`,
628
+ ...(input.sourceSessionId ? [`--source-session-id=${input.sourceSessionId}`] : []),
629
+ `--if-revision=${revision(input.ifRevision)}`,
630
+ ], { mutation: true, signal });
631
+ return this.sessionMutation(op, res.data);
632
+ }
633
+ async takeover(sessionRef, input, signal) {
634
+ const op = 'history.takeover';
635
+ this.ref(op, sessionRef, true);
636
+ await this.guard(op, signal, 'relay-2.history.session');
637
+ const res = await this.call(op, [
638
+ 'session',
639
+ 'takeover',
640
+ sessionRef,
641
+ `--reason=${input.reason}`,
642
+ `--client=${input.client}`,
643
+ `--if-revision=${revision(input.ifRevision)}`,
644
+ ], { mutation: true, signal });
645
+ return this.sessionMutation(op, res.data);
646
+ }
647
+ async finishSession(sessionRef, outcome, input, signal) {
648
+ const op = `history.${outcome}`;
649
+ this.ref(op, sessionRef, true);
650
+ if (input.runId)
651
+ this.ref(op, input.runId, true);
652
+ await this.guard(op, signal, 'relay-2.history.session');
653
+ const res = await this.call(op, [
654
+ 'session',
655
+ outcome,
656
+ sessionRef,
657
+ ...(input.runId ? [`--run=${input.runId}`] : []),
658
+ `--result=${input.result}`,
659
+ `--if-revision=${revision(input.ifRevision)}`,
660
+ ], { mutation: true, signal });
661
+ const session = this.shape(op, sessionSchema, res.data, true);
662
+ const expected = outcome === 'complete' ? 'completed' : 'cancelled';
663
+ if (session.data.status !== expected)
664
+ throw this.fail('protocol', op, `ожидался статус ${expected}, получен ${session.data.status}`, { mutation: true });
665
+ return { session: sessionRecord(session), ...(input.runId ? { runId: input.runId } : {}) };
666
+ }
667
+ sessionMutation(op, data) {
668
+ const session = this.shape(op, sessionSchema, data, true);
669
+ if (!session.data.currentRunId)
670
+ throw this.fail('protocol', op, 'сессия без активного run после записи', { mutation: true });
671
+ return { session: sessionRecord(session), runId: session.data.currentRunId };
672
+ }
673
+ }
674
+ /* ---------- преобразования ---------- */
675
+ function taskRecord(task) {
676
+ return {
677
+ id: task.id,
678
+ key: task.key,
679
+ title: task.data.title,
680
+ description: task.data.description,
681
+ column: task.data.column,
682
+ revision: task.revision,
683
+ parentId: task.data.parentId,
684
+ dependencies: [...task.data.dependencies],
685
+ criteria: task.data.acceptanceCriteria.map((c) => ({
686
+ id: c.id,
687
+ title: c.title,
688
+ summary: c.summary,
689
+ description: c.description,
690
+ completed: c.completed,
691
+ })),
692
+ };
693
+ }
694
+ function sessionRecord(s) {
695
+ return {
696
+ id: s.id,
697
+ key: s.key,
698
+ status: s.data.status,
699
+ revision: s.revision,
700
+ currentRunId: s.data.currentRunId,
701
+ parentSessionId: s.data.parentSessionId,
702
+ title: s.data.title,
703
+ description: s.data.description,
704
+ result: s.data.result,
705
+ };
706
+ }
707
+ function checkpointRecord(c) {
708
+ return {
709
+ id: c.id,
710
+ key: c.key,
711
+ sessionId: c.data.sessionId,
712
+ runId: c.data.runId,
713
+ kind: c.data.kind,
714
+ intentId: c.data.intentId,
715
+ title: c.data.title,
716
+ body: c.data.body,
717
+ };
718
+ }
719
+ function revision(value) {
720
+ if (!Number.isSafeInteger(value) || value < 1)
721
+ throw new TransportError('refused', 'revision', `relay-v2: недопустимая ревизия ${value}`, {
722
+ provider: PROVIDER,
723
+ mutation: true,
724
+ });
725
+ return value;
726
+ }
727
+ /** Ровно одна JSON-строка (допускается завершающий перевод строки). */
728
+ function parseLine(text) {
729
+ const trimmed = text.trim();
730
+ if (!trimmed || trimmed.includes('\n'))
731
+ return undefined;
732
+ try {
733
+ return JSON.parse(trimmed);
734
+ }
735
+ catch {
736
+ return undefined;
737
+ }
738
+ }
739
+ function sanitizeArg(arg) {
740
+ if (arg.startsWith('--cursor='))
741
+ return '--cursor=<cursor>';
742
+ return arg.length > 240 ? `${arg.slice(0, 240)}…(${arg.length})` : arg;
743
+ }
744
+ function hashFile(path) {
745
+ return new Promise((resolve, reject) => {
746
+ const hash = createHash('sha256');
747
+ createReadStream(path)
748
+ .on('error', reject)
749
+ .on('data', (d) => hash.update(d))
750
+ .on('end', () => resolve(hash.digest('hex')));
751
+ });
752
+ }