devcontour-harness 0.0.0-stage → 0.1.0-alpha.5

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 (197) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -2
  3. package/docs/architecture.md +473 -0
  4. package/docs/compatibility.md +192 -0
  5. package/docs/contracts.md +575 -0
  6. package/docs/quickstart.md +182 -0
  7. package/docs/upgrade.md +166 -0
  8. package/examples/README.md +26 -0
  9. package/examples/external-connector/README.md +19 -0
  10. package/examples/external-connector/connector.mjs +55 -0
  11. package/examples/files-only/README.md +28 -0
  12. package/examples/files-only/context/norms/rules.md +4 -0
  13. package/examples/files-only/graph.json +23 -0
  14. package/examples/files-only/policy.json +30 -0
  15. package/examples/one-vendor/README.md +19 -0
  16. package/examples/one-vendor/policy.json +27 -0
  17. package/examples/single-task/README.md +18 -0
  18. package/examples/single-task/graph.json +27 -0
  19. package/examples/single-task/policy-same-vendor.json +55 -0
  20. package/examples/single-task/policy.json +52 -0
  21. package/examples/single-task/src/.gitkeep +0 -0
  22. package/examples/single-task/test/run.js +19 -0
  23. package/examples/two-vendors/policy.json +30 -0
  24. package/lib/agents/cli-adapter.d.ts +48 -0
  25. package/lib/agents/cli-adapter.js +384 -0
  26. package/lib/agents/inspect.d.ts +6 -0
  27. package/lib/agents/inspect.js +46 -0
  28. package/lib/agents/probe-server.d.ts +2 -0
  29. package/lib/agents/probe-server.js +13 -0
  30. package/lib/agents/review-probe.d.ts +123 -0
  31. package/lib/agents/review-probe.js +279 -0
  32. package/lib/agents/runtime-events.d.ts +11 -0
  33. package/lib/agents/runtime-events.js +21 -0
  34. package/lib/agents/schemas.d.ts +215 -0
  35. package/lib/agents/schemas.js +120 -0
  36. package/lib/agents/usage.d.ts +8 -0
  37. package/lib/agents/usage.js +83 -0
  38. package/lib/cli.d.ts +2 -0
  39. package/lib/cli.js +331 -0
  40. package/lib/connectors/files.d.ts +108 -0
  41. package/lib/connectors/files.js +133 -0
  42. package/lib/contracts/checks.d.ts +307 -0
  43. package/lib/contracts/checks.js +181 -0
  44. package/lib/contracts/cli.d.ts +3086 -0
  45. package/lib/contracts/cli.js +425 -0
  46. package/lib/contracts/context.d.ts +135 -0
  47. package/lib/contracts/context.js +129 -0
  48. package/lib/contracts/digest.d.ts +9 -0
  49. package/lib/contracts/digest.js +22 -0
  50. package/lib/contracts/errors.d.ts +79 -0
  51. package/lib/contracts/errors.js +68 -0
  52. package/lib/contracts/events.d.ts +795 -0
  53. package/lib/contracts/events.js +249 -0
  54. package/lib/contracts/execution.d.ts +3048 -0
  55. package/lib/contracts/execution.js +864 -0
  56. package/lib/contracts/failure.d.ts +67 -0
  57. package/lib/contracts/failure.js +122 -0
  58. package/lib/contracts/index.d.ts +17 -0
  59. package/lib/contracts/index.js +17 -0
  60. package/lib/contracts/policy.d.ts +1065 -0
  61. package/lib/contracts/policy.js +534 -0
  62. package/lib/contracts/ports.d.ts +367 -0
  63. package/lib/contracts/ports.js +1 -0
  64. package/lib/contracts/primitives.d.ts +63 -0
  65. package/lib/contracts/primitives.js +73 -0
  66. package/lib/contracts/profiles.d.ts +322 -0
  67. package/lib/contracts/profiles.js +105 -0
  68. package/lib/contracts/published.d.ts +3014 -0
  69. package/lib/contracts/published.js +34 -0
  70. package/lib/contracts/result.d.ts +1109 -0
  71. package/lib/contracts/result.js +273 -0
  72. package/lib/contracts/review.d.ts +210 -0
  73. package/lib/contracts/review.js +153 -0
  74. package/lib/contracts/version.d.ts +17 -0
  75. package/lib/contracts/version.js +17 -0
  76. package/lib/contracts/work.d.ts +173 -0
  77. package/lib/contracts/work.js +178 -0
  78. package/lib/control/base-move.d.ts +44 -0
  79. package/lib/control/base-move.js +90 -0
  80. package/lib/control/doctor.d.ts +35 -0
  81. package/lib/control/doctor.js +71 -0
  82. package/lib/control/harness.d.ts +1079 -0
  83. package/lib/control/harness.js +714 -0
  84. package/lib/control/provenance.d.ts +17 -0
  85. package/lib/control/provenance.js +25 -0
  86. package/lib/control/recovery.d.ts +235 -0
  87. package/lib/control/recovery.js +236 -0
  88. package/lib/control/review.d.ts +62 -0
  89. package/lib/control/review.js +170 -0
  90. package/lib/control/workflow.d.ts +247 -0
  91. package/lib/control/workflow.js +259 -0
  92. package/lib/engine/budget.d.ts +345 -0
  93. package/lib/engine/budget.js +278 -0
  94. package/lib/engine/classify.d.ts +8 -0
  95. package/lib/engine/classify.js +31 -0
  96. package/lib/engine/engine.d.ts +1503 -0
  97. package/lib/engine/engine.js +2259 -0
  98. package/lib/engine/failure.d.ts +1 -0
  99. package/lib/engine/failure.js +3 -0
  100. package/lib/engine/joint-runner.d.ts +257 -0
  101. package/lib/engine/joint-runner.js +254 -0
  102. package/lib/engine/measured.d.ts +15 -0
  103. package/lib/engine/measured.js +82 -0
  104. package/lib/engine/prompt.d.ts +38 -0
  105. package/lib/engine/prompt.js +74 -0
  106. package/lib/engine/property-recovery.d.ts +50 -0
  107. package/lib/engine/property-recovery.js +169 -0
  108. package/lib/engine/repair.d.ts +33 -0
  109. package/lib/engine/repair.js +82 -0
  110. package/lib/engine/result.d.ts +9 -0
  111. package/lib/engine/result.js +166 -0
  112. package/lib/engine/reuse.d.ts +80 -0
  113. package/lib/engine/reuse.js +97 -0
  114. package/lib/engine/runner.d.ts +335 -0
  115. package/lib/engine/runner.js +1467 -0
  116. package/lib/engine/standalone.d.ts +6 -0
  117. package/lib/engine/standalone.js +48 -0
  118. package/lib/engine/usage.d.ts +87 -0
  119. package/lib/engine/usage.js +165 -0
  120. package/lib/environment/isolation.d.ts +110 -0
  121. package/lib/environment/isolation.js +238 -0
  122. package/lib/environment/lifecycle.d.ts +40 -0
  123. package/lib/environment/lifecycle.js +191 -0
  124. package/lib/environment/live-calls.d.ts +14 -0
  125. package/lib/environment/live-calls.js +72 -0
  126. package/lib/environment/process.d.ts +32 -0
  127. package/lib/environment/process.js +368 -0
  128. package/lib/environment/redaction.d.ts +22 -0
  129. package/lib/environment/redaction.js +102 -0
  130. package/lib/environment/resources.d.ts +31 -0
  131. package/lib/environment/resources.js +120 -0
  132. package/lib/index.d.ts +65 -0
  133. package/lib/index.js +67 -0
  134. package/lib/package.d.ts +2 -0
  135. package/lib/package.js +3 -0
  136. package/lib/profiles/resolve.d.ts +186 -0
  137. package/lib/profiles/resolve.js +242 -0
  138. package/lib/storage/document.d.ts +1186 -0
  139. package/lib/storage/document.js +80 -0
  140. package/lib/storage/sqlite.d.ts +35 -0
  141. package/lib/storage/sqlite.js +260 -0
  142. package/lib/testing/index.d.ts +5 -0
  143. package/lib/testing/index.js +5 -0
  144. package/lib/testing/store-contract.d.ts +8 -0
  145. package/lib/testing/store-contract.js +167 -0
  146. package/lib/validate.d.ts +36 -0
  147. package/lib/validate.js +175 -0
  148. package/lib/verification/acceptance.d.ts +268 -0
  149. package/lib/verification/acceptance.js +654 -0
  150. package/lib/verification/checks.d.ts +72 -0
  151. package/lib/verification/checks.js +190 -0
  152. package/lib/verification/confirm.d.ts +125 -0
  153. package/lib/verification/confirm.js +411 -0
  154. package/lib/verification/corpus.d.ts +1303 -0
  155. package/lib/verification/corpus.js +744 -0
  156. package/lib/verification/dependencies.d.ts +66 -0
  157. package/lib/verification/dependencies.js +144 -0
  158. package/lib/verification/gates.d.ts +144 -0
  159. package/lib/verification/gates.js +559 -0
  160. package/lib/verification/joint.d.ts +63 -0
  161. package/lib/verification/joint.js +121 -0
  162. package/lib/verification/proof.d.ts +54 -0
  163. package/lib/verification/proof.js +96 -0
  164. package/lib/verification/property.d.ts +86 -0
  165. package/lib/verification/property.js +214 -0
  166. package/lib/verification/requirements.d.ts +25 -0
  167. package/lib/verification/requirements.js +69 -0
  168. package/lib/verification/self-check.d.ts +29 -0
  169. package/lib/verification/self-check.js +99 -0
  170. package/lib/verification/spike.d.ts +79 -0
  171. package/lib/verification/spike.js +124 -0
  172. package/package.json +79 -4
  173. package/profiles/expo-mobile.json +15 -0
  174. package/profiles/go-api.json +15 -0
  175. package/profiles/mobile-maestro.json +29 -0
  176. package/profiles/nest-api.json +26 -0
  177. package/profiles/next-product.json +72 -0
  178. package/profiles/node-checks.json +77 -0
  179. package/profiles/python-api.json +32 -0
  180. package/profiles/react-native.json +50 -0
  181. package/profiles/react-vite-admin.json +71 -0
  182. package/schemas/context-snapshot.schema.json +231 -0
  183. package/schemas/contract-document.schema.json +385 -0
  184. package/schemas/execution-document.schema.json +3540 -0
  185. package/schemas/execution-policy.schema.json +2148 -0
  186. package/schemas/joint-verification.schema.json +617 -0
  187. package/schemas/result-envelope.schema.json +1440 -0
  188. package/schemas/review-record.schema.json +177 -0
  189. package/schemas/run-event.schema.json +2365 -0
  190. package/schemas/run-result.schema.json +1363 -0
  191. package/schemas/stack-profile.schema.json +492 -0
  192. package/schemas/work-graph.schema.json +381 -0
  193. package/schemas/work-order.schema.json +256 -0
  194. package/skills/harness/SKILL.md +64 -0
  195. package/templates/AGENTS.md +13 -0
  196. package/templates/graph.json +22 -0
  197. package/templates/policy.json +28 -0
@@ -0,0 +1,40 @@
1
+ import type { z } from 'zod';
2
+ import { environmentSchema, lifecycleSchema, stepSchema, type ExecutionPolicy } from '../contracts/index.ts';
3
+ export { redactor } from './redaction.ts';
4
+ type Environment = z.infer<typeof environmentSchema>;
5
+ type Lifecycle = z.infer<typeof lifecycleSchema>;
6
+ type Step = z.infer<typeof stepSchema>;
7
+ /** Отказ обязательной очистки окружения: приёмка с ним не происходит. */
8
+ export declare class CleanupFailure extends Error {
9
+ name: string;
10
+ }
11
+ /** Жив ли процесс (сигнал 0 не доставляется, только проверяется). */
12
+ export declare function processAlive(pid: number): boolean;
13
+ export declare function executionEnvironment(profiles: (Environment | undefined)[], devcontour?: NodeJS.ProcessEnv, source?: NodeJS.ProcessEnv): {
14
+ env: NodeJS.ProcessEnv;
15
+ redact: import("./redaction.ts").Redactor;
16
+ };
17
+ /**
18
+ * Маска по тем секретам и наследуемым значениям, что заданы, — даже когда
19
+ * окружение целиком собрать нельзя. Незаданный секрет — причина отказа, но не
20
+ * повод снимать маску с остальных: известный секрет иначе сохранялся бы в
21
+ * команде и итоге проверки.
22
+ */
23
+ export declare function availableRedactor(profiles: (Environment | undefined)[], source?: NodeJS.ProcessEnv): import("./redaction.ts").Redactor;
24
+ export declare function runSteps(steps: Step[], cwd: string, dir: string, execution: ReturnType<typeof executionEnvironment>, signal: AbortSignal): Promise<void>;
25
+ export declare function withEnvironment<T>(lifecycle: Lifecycle | undefined, cwd: string, dir: string, execution: ReturnType<typeof executionEnvironment>, signal: AbortSignal, action: () => Promise<T>, measure?: <R>(stage: string, action: () => Promise<R>) => Promise<R>): Promise<T>;
26
+ /**
27
+ * Завершить очистку окружения, прерванную падением процесса: по сохранённому
28
+ * receipt, только внутри разрешённых корней, только при той же политике
29
+ * очистки и только когда процесс-владелец уже не существует (или это он сам,
30
+ * `options.owner`, после окончания попытки).
31
+ */
32
+ export declare function cleanupEnvironment(receiptPath: string, policy: ExecutionPolicy, allowedRoots: string[],
33
+ /** `owner` — процесс, который вправе повторить очистку своего окружения сам. */
34
+ options?: {
35
+ owner?: number;
36
+ }): Promise<{
37
+ status: "already-cleaned";
38
+ } | {
39
+ status: "cleaned";
40
+ }>;
@@ -0,0 +1,191 @@
1
+ import { mkdir, writeFile, readFile, realpath } from 'node:fs/promises';
2
+ import { join, sep } from 'node:path';
3
+ import { digestOf, } from "../contracts/index.js";
4
+ import { command } from "./process.js";
5
+ import { redactor } from "./redaction.js";
6
+ export { redactor } from "./redaction.js";
7
+ /** Отказ обязательной очистки окружения: приёмка с ним не происходит. */
8
+ export class CleanupFailure extends Error {
9
+ name = 'CleanupFailure';
10
+ }
11
+ /** Жив ли процесс (сигнал 0 не доставляется, только проверяется). */
12
+ export function processAlive(pid) {
13
+ try {
14
+ process.kill(pid, 0);
15
+ return true;
16
+ }
17
+ catch (error) {
18
+ return error.code === 'EPERM';
19
+ }
20
+ }
21
+ export function executionEnvironment(profiles, devcontour = {}, source = process.env) {
22
+ // Окружение прогона — минимальное: исполнитель не должен наследовать всё,
23
+ // что оказалось у оператора. Но учётные данные runtime живут в системном
24
+ // хранилище, а найти их там нельзя, не зная имени учётной записи: без USER
25
+ // claude на macOS отвечает «Not logged in», и единственным выходом остаётся
26
+ // передать API-ключ — то есть платить за то, что уже оплачено подпиской.
27
+ // USER и LOGNAME — не секреты, а адрес в хранилище.
28
+ const env = {
29
+ PATH: source.PATH,
30
+ HOME: source.HOME,
31
+ TMPDIR: source.TMPDIR,
32
+ USER: source.USER,
33
+ LOGNAME: source.LOGNAME ?? source.USER,
34
+ CI: '1',
35
+ };
36
+ const secretValues = [];
37
+ for (const profile of profiles) {
38
+ if (!profile)
39
+ continue;
40
+ for (const key of profile.inherit) {
41
+ if (source[key] === undefined)
42
+ throw new Error(`Не задана переменная окружения: ${key}`);
43
+ env[key] = source[key];
44
+ // Inherited values are also redacted: callers need not classify every provider token.
45
+ if (source[key])
46
+ secretValues.push(source[key]);
47
+ }
48
+ Object.assign(env, profile.values);
49
+ for (const [key, from] of Object.entries(profile.secrets)) {
50
+ if (!source[from])
51
+ throw new Error(`Не задан секрет окружения: ${from} (для ${key})`);
52
+ env[key] = source[from];
53
+ secretValues.push(source[from]);
54
+ }
55
+ }
56
+ // Переменные контура: исторические DEVCONTOUR_* — часть договора проверок
57
+ // пользователей — и их псевдонимы HARNESS_* с теми же значениями.
58
+ for (const [key, value] of Object.entries(devcontour)) {
59
+ env[key] = value;
60
+ if (key.startsWith('DEVCONTOUR_'))
61
+ env['HARNESS_' + key.slice('DEVCONTOUR_'.length)] = value;
62
+ }
63
+ return { env, redact: redactor(secretValues) };
64
+ }
65
+ /**
66
+ * Маска по тем секретам и наследуемым значениям, что заданы, — даже когда
67
+ * окружение целиком собрать нельзя. Незаданный секрет — причина отказа, но не
68
+ * повод снимать маску с остальных: известный секрет иначе сохранялся бы в
69
+ * команде и итоге проверки.
70
+ */
71
+ export function availableRedactor(profiles, source = process.env) {
72
+ const values = [];
73
+ for (const profile of profiles) {
74
+ if (!profile)
75
+ continue;
76
+ for (const key of profile.inherit)
77
+ if (source[key])
78
+ values.push(source[key]);
79
+ for (const from of Object.values(profile.secrets))
80
+ if (source[from])
81
+ values.push(source[from]);
82
+ }
83
+ return redactor(values);
84
+ }
85
+ export async function runSteps(steps, cwd, dir, execution, signal) {
86
+ await mkdir(dir, { recursive: true });
87
+ for (const step of steps) {
88
+ signal.throwIfAborted();
89
+ const result = await command(step.command, cwd, {
90
+ signal,
91
+ timeoutMs: step.timeoutMs,
92
+ env: execution.env,
93
+ redact: execution.redact,
94
+ });
95
+ await writeFile(join(dir, step.id + '.log'), result.stdout + '\n' + result.stderr);
96
+ if (result.code || result.timedOut || signal.aborted)
97
+ throw new Error(`Шаг ${step.id}: exit=${result.code}; timeout=${result.timedOut}`);
98
+ }
99
+ }
100
+ // Teardown uses a fresh deadline: cancellation must not cancel cleanup itself.
101
+ // The receipt is retained after crashes and can be explicitly resumed with environment-cleanup.
102
+ export async function withEnvironment(lifecycle, cwd, dir, execution, signal, action, measure = (_stage, action) => action()) {
103
+ if (!lifecycle)
104
+ return action();
105
+ await mkdir(dir, { recursive: true });
106
+ const receiptPath = join(dir, 'environment.json');
107
+ const receipt = {
108
+ version: 1,
109
+ ownerPid: process.pid,
110
+ devcontourEnv: Object.fromEntries(Object.entries(execution.env).filter(([key]) => key.startsWith('DEVCONTOUR_'))),
111
+ cwd,
112
+ lifecycle,
113
+ policyDigest: digestOf(lifecycle),
114
+ status: 'active',
115
+ startedAt: new Date().toISOString(),
116
+ runId: execution.env.DEVCONTOUR_RUN_ID,
117
+ };
118
+ await writeFile(receiptPath, JSON.stringify(receipt, null, 2));
119
+ let result, failure;
120
+ try {
121
+ await measure('setup', () => runSteps(lifecycle.setup, cwd, join(dir, 'setup'), execution, signal));
122
+ await measure('ready', () => runSteps(lifecycle.ready, cwd, join(dir, 'ready'), execution, signal));
123
+ result = await action();
124
+ }
125
+ catch (error) {
126
+ // A thrown undefined/false must still fail the attempt after cleanup.
127
+ failure =
128
+ error instanceof Error
129
+ ? error
130
+ : new Error('Операция окружения завершилась ошибкой', { cause: error });
131
+ }
132
+ try {
133
+ await measure('cleanup', () => teardown(lifecycle, cwd, dir, execution));
134
+ receipt.status = 'cleaned';
135
+ }
136
+ catch (error) {
137
+ receipt.status = 'cleanup-failed';
138
+ failure = new CleanupFailure(`${failure instanceof Error ? failure.message + '; ' : ''}Очистка окружения не завершена: ${error instanceof Error ? error.message : String(error)}`);
139
+ }
140
+ await writeFile(receiptPath, JSON.stringify(receipt, null, 2));
141
+ if (failure)
142
+ throw failure;
143
+ return result;
144
+ }
145
+ async function teardown(lifecycle, cwd, dir, execution) {
146
+ const failures = [];
147
+ for (const step of lifecycle.teardown) {
148
+ try {
149
+ await runSteps([step], cwd, join(dir, 'teardown'), execution, AbortSignal.timeout(step.timeoutMs + 2000));
150
+ }
151
+ catch (e) {
152
+ failures.push(e instanceof Error ? e.message : String(e));
153
+ }
154
+ }
155
+ if (failures.length)
156
+ throw new Error(failures.join('; '));
157
+ }
158
+ /**
159
+ * Завершить очистку окружения, прерванную падением процесса: по сохранённому
160
+ * receipt, только внутри разрешённых корней, только при той же политике
161
+ * очистки и только когда процесс-владелец уже не существует (или это он сам,
162
+ * `options.owner`, после окончания попытки).
163
+ */
164
+ export async function cleanupEnvironment(receiptPath, policy, allowedRoots,
165
+ /** `owner` — процесс, который вправе повторить очистку своего окружения сам. */
166
+ options = {}) {
167
+ const receipt = JSON.parse(await readFile(receiptPath, 'utf8'));
168
+ const cwd = await realpath(receipt.cwd);
169
+ if (!allowedRoots.some((root) => cwd.startsWith(root + sep)))
170
+ throw new Error('Окружение находится вне рабочих каталогов');
171
+ const repositoryId = receipt.devcontourEnv?.DEVCONTOUR_REPOSITORY_ID;
172
+ const repo = policy.repositories.find((r) => r.id === repositoryId);
173
+ if (repositoryId && !repo)
174
+ throw new Error('Неизвестный репозиторий сохранённого окружения');
175
+ const lifecycle = [repo?.lifecycle, policy.lifecycle]
176
+ .filter(Boolean)
177
+ .find((l) => digestOf(l) === receipt.policyDigest);
178
+ if (!lifecycle)
179
+ throw new Error('Политика очистки изменилась; сверьте сохранённое окружение вручную');
180
+ if (receipt.status === 'cleaned')
181
+ return { status: 'already-cleaned' };
182
+ if (receipt.ownerPid && receipt.ownerPid !== options.owner && processAlive(receipt.ownerPid))
183
+ throw new Error('Процесс владельца окружения ещё существует; сначала остановите его');
184
+ const execution = executionEnvironment([policy.environment, repo?.environment], {
185
+ ...receipt.devcontourEnv,
186
+ ...(receipt.runId ? { DEVCONTOUR_RUN_ID: receipt.runId } : {}),
187
+ });
188
+ await teardown(lifecycle, cwd, join(receiptPath, '..'), execution);
189
+ await writeFile(receiptPath, JSON.stringify({ ...receipt, status: 'cleaned' }, null, 2));
190
+ return { status: 'cleaned' };
191
+ }
@@ -0,0 +1,14 @@
1
+ import { type CallOwner } from '../engine/usage.ts';
2
+ /** Момент старта процесса по данным ОС, мс; `undefined` — узнать нельзя. */
3
+ export declare function processStartedAt(pid: number): number | undefined;
4
+ /**
5
+ * Этот процесс: по нему восстановление и бюджет отличают свои идущие вызовы.
6
+ * Момент старта отличает процесс от другого, получившего тот же pid.
7
+ */
8
+ export declare const CURRENT_PROCESS: CallOwner;
9
+ /**
10
+ * Идентификатор текущей загрузки ОС: Linux сообщает его прямо, macOS — момент
11
+ * загрузки. `undefined` — узнать нельзя, и монотонные часы ОС между
12
+ * процессами не сравниваются.
13
+ */
14
+ export declare const BOOT_ID: string | undefined;
@@ -0,0 +1,72 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
4
+ import { registerMonotonic, registerProcess } from "../engine/usage.js";
5
+ import { processAlive } from "./lifecycle.js";
6
+ /** Момент старта процесса по данным ОС, мс; `undefined` — узнать нельзя. */
7
+ export function processStartedAt(pid) {
8
+ try {
9
+ const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
10
+ encoding: 'utf8',
11
+ env: { ...process.env, LC_ALL: 'C' },
12
+ stdio: ['ignore', 'pipe', 'ignore'],
13
+ timeout: 2000,
14
+ }).trim();
15
+ const at = Date.parse(out);
16
+ return Number.isFinite(at) ? at : undefined;
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ /**
23
+ * Этот процесс: по нему восстановление и бюджет отличают свои идущие вызовы.
24
+ * Момент старта отличает процесс от другого, получившего тот же pid.
25
+ */
26
+ export const CURRENT_PROCESS = {
27
+ id: randomUUID(),
28
+ pid: process.pid,
29
+ ...((at) => (at === undefined ? {} : { startedAt: at }))(processStartedAt(process.pid)),
30
+ };
31
+ /** Тот же ли это процесс: pid существует и момент старта совпадает. */
32
+ const sameProcess = (owner) => {
33
+ if (owner.startedAt === undefined || !processAlive(owner.pid))
34
+ return false;
35
+ // Оба момента — из одного источника (ОС) с одним округлением: у того же
36
+ // процесса они равны, любое расхождение — другой процесс.
37
+ return processStartedAt(owner.pid) === owner.startedAt;
38
+ };
39
+ /** Процесса нет или pid достался другому процессу; неизвестное — не «ушёл». */
40
+ const gone = (owner) => {
41
+ if (!processAlive(owner.pid))
42
+ return true;
43
+ if (owner.startedAt === undefined)
44
+ return false;
45
+ const started = processStartedAt(owner.pid);
46
+ return started !== undefined && started !== owner.startedAt;
47
+ };
48
+ /**
49
+ * Идентификатор текущей загрузки ОС: Linux сообщает его прямо, macOS — момент
50
+ * загрузки. `undefined` — узнать нельзя, и монотонные часы ОС между
51
+ * процессами не сравниваются.
52
+ */
53
+ export const BOOT_ID = (() => {
54
+ try {
55
+ return readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
56
+ }
57
+ catch {
58
+ try {
59
+ const out = execFileSync('sysctl', ['-n', 'kern.boottime'], {
60
+ encoding: 'utf8',
61
+ stdio: ['ignore', 'pipe', 'ignore'],
62
+ timeout: 2000,
63
+ });
64
+ return /sec = \d+, usec = \d+/.exec(out)?.[0];
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
70
+ })();
71
+ registerProcess(CURRENT_PROCESS, sameProcess, gone);
72
+ registerMonotonic(() => process.hrtime.bigint(), BOOT_ID);
@@ -0,0 +1,32 @@
1
+ import { type Redactor } from './redaction.ts';
2
+ import type { RuntimeDiagnostics } from '../contracts/index.ts';
3
+ export interface CommandResult {
4
+ code: number;
5
+ stdout: string;
6
+ stderr: string;
7
+ timedOut: boolean;
8
+ diagnostics: RuntimeDiagnostics;
9
+ }
10
+ export declare function command(argv: string[], cwd: string, options?: {
11
+ signal?: AbortSignal;
12
+ timeoutMs?: number;
13
+ input?: string;
14
+ env?: NodeJS.ProcessEnv;
15
+ redact?: Redactor;
16
+ onOutput?: (stream: 'stdout' | 'stderr', text: string) => void;
17
+ /**
18
+ * Владеть всеми потомками, включая сменивших группу или сессию: после
19
+ * завершения они останавливаются, и результат не возвращается, пока
20
+ * хотя бы один жив. Сигнал группе не достаёт отсоединившегося потомка.
21
+ *
22
+ * Потомок узнаётся по любому из трёх признаков: метка окружения (её
23
+ * наследуют все, кто не очистил окружение), замеченное опросом родство с
24
+ * командой и текущий каталог внутри `dirs` — каталогов, которые принадлежат
25
+ * только этой команде (worktree, scratch). Ни один признак сам по себе не
26
+ * полон: окружение можно очистить, опрос можно опередить, каталог сменить.
27
+ */
28
+ contain?: boolean | {
29
+ dirs: string[];
30
+ };
31
+ }): Promise<CommandResult>;
32
+ export declare function git(cwd: string, ...args: string[]): Promise<string>;