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.
- package/LICENSE +21 -0
- package/README.md +161 -2
- package/docs/architecture.md +473 -0
- package/docs/compatibility.md +192 -0
- package/docs/contracts.md +575 -0
- package/docs/quickstart.md +182 -0
- package/docs/upgrade.md +166 -0
- package/examples/README.md +26 -0
- package/examples/external-connector/README.md +19 -0
- package/examples/external-connector/connector.mjs +55 -0
- package/examples/files-only/README.md +28 -0
- package/examples/files-only/context/norms/rules.md +4 -0
- package/examples/files-only/graph.json +23 -0
- package/examples/files-only/policy.json +30 -0
- package/examples/one-vendor/README.md +19 -0
- package/examples/one-vendor/policy.json +27 -0
- package/examples/single-task/README.md +18 -0
- package/examples/single-task/graph.json +27 -0
- package/examples/single-task/policy-same-vendor.json +55 -0
- package/examples/single-task/policy.json +52 -0
- package/examples/single-task/src/.gitkeep +0 -0
- package/examples/single-task/test/run.js +19 -0
- package/examples/two-vendors/policy.json +30 -0
- package/lib/agents/cli-adapter.d.ts +48 -0
- package/lib/agents/cli-adapter.js +384 -0
- package/lib/agents/inspect.d.ts +6 -0
- package/lib/agents/inspect.js +46 -0
- package/lib/agents/probe-server.d.ts +2 -0
- package/lib/agents/probe-server.js +13 -0
- package/lib/agents/review-probe.d.ts +123 -0
- package/lib/agents/review-probe.js +279 -0
- package/lib/agents/runtime-events.d.ts +11 -0
- package/lib/agents/runtime-events.js +21 -0
- package/lib/agents/schemas.d.ts +215 -0
- package/lib/agents/schemas.js +120 -0
- package/lib/agents/usage.d.ts +8 -0
- package/lib/agents/usage.js +83 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +331 -0
- package/lib/connectors/files.d.ts +108 -0
- package/lib/connectors/files.js +133 -0
- package/lib/contracts/checks.d.ts +307 -0
- package/lib/contracts/checks.js +181 -0
- package/lib/contracts/cli.d.ts +3086 -0
- package/lib/contracts/cli.js +425 -0
- package/lib/contracts/context.d.ts +135 -0
- package/lib/contracts/context.js +129 -0
- package/lib/contracts/digest.d.ts +9 -0
- package/lib/contracts/digest.js +22 -0
- package/lib/contracts/errors.d.ts +79 -0
- package/lib/contracts/errors.js +68 -0
- package/lib/contracts/events.d.ts +795 -0
- package/lib/contracts/events.js +249 -0
- package/lib/contracts/execution.d.ts +3048 -0
- package/lib/contracts/execution.js +864 -0
- package/lib/contracts/failure.d.ts +67 -0
- package/lib/contracts/failure.js +122 -0
- package/lib/contracts/index.d.ts +17 -0
- package/lib/contracts/index.js +17 -0
- package/lib/contracts/policy.d.ts +1065 -0
- package/lib/contracts/policy.js +534 -0
- package/lib/contracts/ports.d.ts +367 -0
- package/lib/contracts/ports.js +1 -0
- package/lib/contracts/primitives.d.ts +63 -0
- package/lib/contracts/primitives.js +73 -0
- package/lib/contracts/profiles.d.ts +322 -0
- package/lib/contracts/profiles.js +105 -0
- package/lib/contracts/published.d.ts +3014 -0
- package/lib/contracts/published.js +34 -0
- package/lib/contracts/result.d.ts +1109 -0
- package/lib/contracts/result.js +273 -0
- package/lib/contracts/review.d.ts +210 -0
- package/lib/contracts/review.js +153 -0
- package/lib/contracts/version.d.ts +17 -0
- package/lib/contracts/version.js +17 -0
- package/lib/contracts/work.d.ts +173 -0
- package/lib/contracts/work.js +178 -0
- package/lib/control/base-move.d.ts +44 -0
- package/lib/control/base-move.js +90 -0
- package/lib/control/doctor.d.ts +35 -0
- package/lib/control/doctor.js +71 -0
- package/lib/control/harness.d.ts +1079 -0
- package/lib/control/harness.js +714 -0
- package/lib/control/provenance.d.ts +17 -0
- package/lib/control/provenance.js +25 -0
- package/lib/control/recovery.d.ts +235 -0
- package/lib/control/recovery.js +236 -0
- package/lib/control/review.d.ts +62 -0
- package/lib/control/review.js +170 -0
- package/lib/control/workflow.d.ts +247 -0
- package/lib/control/workflow.js +259 -0
- package/lib/engine/budget.d.ts +345 -0
- package/lib/engine/budget.js +278 -0
- package/lib/engine/classify.d.ts +8 -0
- package/lib/engine/classify.js +31 -0
- package/lib/engine/engine.d.ts +1503 -0
- package/lib/engine/engine.js +2259 -0
- package/lib/engine/failure.d.ts +1 -0
- package/lib/engine/failure.js +3 -0
- package/lib/engine/joint-runner.d.ts +257 -0
- package/lib/engine/joint-runner.js +254 -0
- package/lib/engine/measured.d.ts +15 -0
- package/lib/engine/measured.js +82 -0
- package/lib/engine/prompt.d.ts +38 -0
- package/lib/engine/prompt.js +74 -0
- package/lib/engine/property-recovery.d.ts +50 -0
- package/lib/engine/property-recovery.js +169 -0
- package/lib/engine/repair.d.ts +33 -0
- package/lib/engine/repair.js +82 -0
- package/lib/engine/result.d.ts +9 -0
- package/lib/engine/result.js +166 -0
- package/lib/engine/reuse.d.ts +80 -0
- package/lib/engine/reuse.js +97 -0
- package/lib/engine/runner.d.ts +335 -0
- package/lib/engine/runner.js +1467 -0
- package/lib/engine/standalone.d.ts +6 -0
- package/lib/engine/standalone.js +48 -0
- package/lib/engine/usage.d.ts +87 -0
- package/lib/engine/usage.js +165 -0
- package/lib/environment/isolation.d.ts +110 -0
- package/lib/environment/isolation.js +238 -0
- package/lib/environment/lifecycle.d.ts +40 -0
- package/lib/environment/lifecycle.js +191 -0
- package/lib/environment/live-calls.d.ts +14 -0
- package/lib/environment/live-calls.js +72 -0
- package/lib/environment/process.d.ts +32 -0
- package/lib/environment/process.js +368 -0
- package/lib/environment/redaction.d.ts +22 -0
- package/lib/environment/redaction.js +102 -0
- package/lib/environment/resources.d.ts +31 -0
- package/lib/environment/resources.js +120 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.js +67 -0
- package/lib/package.d.ts +2 -0
- package/lib/package.js +3 -0
- package/lib/profiles/resolve.d.ts +186 -0
- package/lib/profiles/resolve.js +242 -0
- package/lib/storage/document.d.ts +1186 -0
- package/lib/storage/document.js +80 -0
- package/lib/storage/sqlite.d.ts +35 -0
- package/lib/storage/sqlite.js +260 -0
- package/lib/testing/index.d.ts +5 -0
- package/lib/testing/index.js +5 -0
- package/lib/testing/store-contract.d.ts +8 -0
- package/lib/testing/store-contract.js +167 -0
- package/lib/validate.d.ts +36 -0
- package/lib/validate.js +175 -0
- package/lib/verification/acceptance.d.ts +268 -0
- package/lib/verification/acceptance.js +654 -0
- package/lib/verification/checks.d.ts +72 -0
- package/lib/verification/checks.js +190 -0
- package/lib/verification/confirm.d.ts +125 -0
- package/lib/verification/confirm.js +411 -0
- package/lib/verification/corpus.d.ts +1303 -0
- package/lib/verification/corpus.js +744 -0
- package/lib/verification/dependencies.d.ts +66 -0
- package/lib/verification/dependencies.js +144 -0
- package/lib/verification/gates.d.ts +144 -0
- package/lib/verification/gates.js +559 -0
- package/lib/verification/joint.d.ts +63 -0
- package/lib/verification/joint.js +121 -0
- package/lib/verification/proof.d.ts +54 -0
- package/lib/verification/proof.js +96 -0
- package/lib/verification/property.d.ts +86 -0
- package/lib/verification/property.js +214 -0
- package/lib/verification/requirements.d.ts +25 -0
- package/lib/verification/requirements.js +69 -0
- package/lib/verification/self-check.d.ts +29 -0
- package/lib/verification/self-check.js +99 -0
- package/lib/verification/spike.d.ts +79 -0
- package/lib/verification/spike.js +124 -0
- package/package.json +79 -4
- package/profiles/expo-mobile.json +15 -0
- package/profiles/go-api.json +15 -0
- package/profiles/mobile-maestro.json +29 -0
- package/profiles/nest-api.json +26 -0
- package/profiles/next-product.json +72 -0
- package/profiles/node-checks.json +77 -0
- package/profiles/python-api.json +32 -0
- package/profiles/react-native.json +50 -0
- package/profiles/react-vite-admin.json +71 -0
- package/schemas/context-snapshot.schema.json +231 -0
- package/schemas/contract-document.schema.json +385 -0
- package/schemas/execution-document.schema.json +3540 -0
- package/schemas/execution-policy.schema.json +2148 -0
- package/schemas/joint-verification.schema.json +617 -0
- package/schemas/result-envelope.schema.json +1440 -0
- package/schemas/review-record.schema.json +177 -0
- package/schemas/run-event.schema.json +2365 -0
- package/schemas/run-result.schema.json +1363 -0
- package/schemas/stack-profile.schema.json +492 -0
- package/schemas/work-graph.schema.json +381 -0
- package/schemas/work-order.schema.json +256 -0
- package/skills/harness/SKILL.md +64 -0
- package/templates/AGENTS.md +13 -0
- package/templates/graph.json +22 -0
- package/templates/policy.json +28 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ExecutionPolicy, ProbeServer } from '../contracts/index.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Пробные проверки reviewer под пределами harness.
|
|
5
|
+
*
|
|
6
|
+
* Reviewer, запускавший проверки своим shell, зависал на проверяемом коде:
|
|
7
|
+
* срок команды задавал сам CLI модели, и одна зависшая команда съедала весь
|
|
8
|
+
* срок ревью — готовый кандидат падал без вердикта. Здесь срок задаёт
|
|
9
|
+
* controller: команда исполняется тем же исполнителем, что и gates (песочница
|
|
10
|
+
* ОС, исходники только для чтения, остановка потомков), с пределом на
|
|
11
|
+
* команду, общим бюджетом проверок и резервом времени на заключение.
|
|
12
|
+
* Превышение — результат, который получает модель, а не зависание.
|
|
13
|
+
*/
|
|
14
|
+
export declare const probeServer = "devcontour_probe";
|
|
15
|
+
export declare const probeTool = "run_check";
|
|
16
|
+
export declare const probeSpec: z.ZodObject<{
|
|
17
|
+
cwd: z.ZodString;
|
|
18
|
+
controller: z.ZodArray<z.ZodString>;
|
|
19
|
+
readable: z.ZodArray<z.ZodString>;
|
|
20
|
+
isolation: z.ZodObject<{
|
|
21
|
+
mode: z.ZodEnum<{
|
|
22
|
+
os: "os";
|
|
23
|
+
none: "none";
|
|
24
|
+
}>;
|
|
25
|
+
domains: z.ZodArray<z.ZodString>;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
commandTimeoutMs: z.ZodNumber;
|
|
28
|
+
budgetMs: z.ZodNumber;
|
|
29
|
+
deadline: z.ZodNumber;
|
|
30
|
+
env: z.ZodArray<z.ZodString>;
|
|
31
|
+
secrets: z.ZodArray<z.ZodString>;
|
|
32
|
+
settingsDir: z.ZodString;
|
|
33
|
+
log: z.ZodString;
|
|
34
|
+
}, z.core.$strip>;
|
|
35
|
+
export type ProbeSpec = z.infer<typeof probeSpec>;
|
|
36
|
+
export declare const probeInput: z.ZodObject<{
|
|
37
|
+
argv: z.ZodArray<z.ZodString>;
|
|
38
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
39
|
+
}, z.core.$strip>;
|
|
40
|
+
export type ProbeResult = {
|
|
41
|
+
/**
|
|
42
|
+
* Id запуска. Reviewer ссылается на него в воспроизведении, и «наблюдено»
|
|
43
|
+
* ставится по журналу controller, а не по тексту команды: замаскированные
|
|
44
|
+
* аргументы разных команд могут совпадать.
|
|
45
|
+
*/
|
|
46
|
+
runId: string;
|
|
47
|
+
argv: string[];
|
|
48
|
+
/**
|
|
49
|
+
* `timeout` — команда не уложилась в предел и остановлена вместе с
|
|
50
|
+
* потомками; это наблюдение о команде на данном входе, а не доказанный
|
|
51
|
+
* дефект. `budget-exhausted` — команда не запускалась: бюджет проверок или
|
|
52
|
+
* время до заключения исчерпаны. `error` — команда не запустилась.
|
|
53
|
+
*/
|
|
54
|
+
status: 'passed' | 'failed' | 'timeout' | 'budget-exhausted' | 'error';
|
|
55
|
+
exitCode: number | null;
|
|
56
|
+
durationMs: number;
|
|
57
|
+
limitMs: number;
|
|
58
|
+
budgetLeftMs: number;
|
|
59
|
+
stdout: string;
|
|
60
|
+
stderr: string;
|
|
61
|
+
note?: string;
|
|
62
|
+
};
|
|
63
|
+
export declare class ReviewProbes {
|
|
64
|
+
readonly spec: ProbeSpec;
|
|
65
|
+
private readonly now;
|
|
66
|
+
private readonly environment;
|
|
67
|
+
private spent;
|
|
68
|
+
constructor(spec: ProbeSpec, now?: () => number, environment?: NodeJS.ProcessEnv);
|
|
69
|
+
/** Сколько может длиться следующая команда: меньшее из трёх пределов. */
|
|
70
|
+
limit(requested?: number): number;
|
|
71
|
+
run(input: z.infer<typeof probeInput>, signal?: AbortSignal): Promise<ProbeResult>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Stdio MCP-сервер с одним инструментом — его запускает CLI reviewer.
|
|
75
|
+
* Протокол — построчный JSON-RPC 2.0 (`initialize`, `tools/list`,
|
|
76
|
+
* `tools/call`, `ping`); вызовы исполняются одновременно, бюджет общий —
|
|
77
|
+
* один экземпляр `ReviewProbes` на сервер.
|
|
78
|
+
*/
|
|
79
|
+
export declare function serveReviewProbes(probes: ReviewProbes, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): {
|
|
80
|
+
lines: import("readline").Interface;
|
|
81
|
+
pending: Set<Promise<unknown>>;
|
|
82
|
+
};
|
|
83
|
+
/** Пределы проверок ревью, выведенные из срока фазы. */
|
|
84
|
+
export declare function probeLimits(policy: Pick<ExecutionPolicy, 'runTimeoutMs' | 'reviewProbes'>): {
|
|
85
|
+
commandTimeoutMs: number;
|
|
86
|
+
budgetMs: number;
|
|
87
|
+
reserveMs: number;
|
|
88
|
+
};
|
|
89
|
+
/** Срок вызова инструмента в CLI — с запасом сверх предела команды controller. */
|
|
90
|
+
export declare const probeCallMs: (probe: ProbeServer) => number;
|
|
91
|
+
/**
|
|
92
|
+
* Команда, которой CLI reviewer запускает сервер проверок — этот же пакет.
|
|
93
|
+
* CLI стартует её из проверяемого каталога, поэтому загрузчик исходников
|
|
94
|
+
* указан абсолютным путём: из worktree он бы не разрешился.
|
|
95
|
+
*/
|
|
96
|
+
export declare function probeCommand(specPath: string): {
|
|
97
|
+
command: string;
|
|
98
|
+
args: string[];
|
|
99
|
+
};
|
|
100
|
+
/** Запуск из журнала controller в том виде, в каком он входит в review.json. */
|
|
101
|
+
export interface ProbeRecord {
|
|
102
|
+
runId: string;
|
|
103
|
+
argv: string[];
|
|
104
|
+
status: ProbeResult['status'];
|
|
105
|
+
durationMs: number;
|
|
106
|
+
limitMs: number;
|
|
107
|
+
}
|
|
108
|
+
/** Журнал проверок reviewer: по записи на запуск; нечитаемая строка — не запуск. */
|
|
109
|
+
export declare function probeRecords(log: string): Promise<ProbeRecord[]>;
|
|
110
|
+
/**
|
|
111
|
+
* Воспроизведения находок, сверенные с журналом controller. «Наблюдено» —
|
|
112
|
+
* только по запуску, на который сослался reviewer: он состоялся (исчерпанный
|
|
113
|
+
* бюджет и отказ до старта — не наблюдение), совпадение единственное, и
|
|
114
|
+
* команда в записи — та, что действительно запускалась. Команда из текста
|
|
115
|
+
* находки не исполняется. Ответ reviewer мог прийти уже замаскированным, и
|
|
116
|
+
* короткий секрет задевает символы id: id журнала сравниваются и в той же маске.
|
|
117
|
+
*/
|
|
118
|
+
export declare function observeReproductions<F extends {
|
|
119
|
+
reproduction?: {
|
|
120
|
+
command: string[] | null;
|
|
121
|
+
run?: string | null;
|
|
122
|
+
} | null;
|
|
123
|
+
}>(findings: F[], probes: ProbeRecord[] | undefined, redact: (text: string) => string): F[];
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { appendFile, readFile } from 'node:fs/promises';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { redactor } from "../environment/redaction.js";
|
|
7
|
+
import { runCheck } from "../verification/gates.js";
|
|
8
|
+
/**
|
|
9
|
+
* Пробные проверки reviewer под пределами harness.
|
|
10
|
+
*
|
|
11
|
+
* Reviewer, запускавший проверки своим shell, зависал на проверяемом коде:
|
|
12
|
+
* срок команды задавал сам CLI модели, и одна зависшая команда съедала весь
|
|
13
|
+
* срок ревью — готовый кандидат падал без вердикта. Здесь срок задаёт
|
|
14
|
+
* controller: команда исполняется тем же исполнителем, что и gates (песочница
|
|
15
|
+
* ОС, исходники только для чтения, остановка потомков), с пределом на
|
|
16
|
+
* команду, общим бюджетом проверок и резервом времени на заключение.
|
|
17
|
+
* Превышение — результат, который получает модель, а не зависание.
|
|
18
|
+
*/
|
|
19
|
+
export const probeServer = 'devcontour_probe';
|
|
20
|
+
export const probeTool = 'run_check';
|
|
21
|
+
export const probeSpec = z.object({
|
|
22
|
+
cwd: z.string().min(1),
|
|
23
|
+
/** Закрытое проверке и открытое на чтение — граница reviewer. */
|
|
24
|
+
controller: z.array(z.string()),
|
|
25
|
+
readable: z.array(z.string()),
|
|
26
|
+
isolation: z.object({ mode: z.enum(['os', 'none']), domains: z.array(z.string()) }),
|
|
27
|
+
commandTimeoutMs: z.number().int().min(1000),
|
|
28
|
+
budgetMs: z.number().int().min(1000),
|
|
29
|
+
/** Момент, к которому ревью должно быть готово дать заключение (epoch ms). */
|
|
30
|
+
deadline: z.number().int(),
|
|
31
|
+
/** Переменные окружения, передаваемые по имени; значения на диск не пишутся. */
|
|
32
|
+
env: z.array(z.string()),
|
|
33
|
+
secrets: z.array(z.string()),
|
|
34
|
+
settingsDir: z.string().min(1),
|
|
35
|
+
log: z.string().min(1),
|
|
36
|
+
});
|
|
37
|
+
export const probeInput = z.object({
|
|
38
|
+
argv: z.array(z.string().min(1)).min(1).max(64),
|
|
39
|
+
timeoutMs: z.number().int().min(1).optional(),
|
|
40
|
+
});
|
|
41
|
+
const tail = (text) => (text.length > 8000 ? '…' + text.slice(-8000) : text);
|
|
42
|
+
export class ReviewProbes {
|
|
43
|
+
spec;
|
|
44
|
+
now;
|
|
45
|
+
environment;
|
|
46
|
+
spent = 0;
|
|
47
|
+
constructor(spec, now = () => Date.now(), environment = process.env) {
|
|
48
|
+
this.spec = spec;
|
|
49
|
+
this.now = now;
|
|
50
|
+
this.environment = environment;
|
|
51
|
+
}
|
|
52
|
+
/** Сколько может длиться следующая команда: меньшее из трёх пределов. */
|
|
53
|
+
limit(requested) {
|
|
54
|
+
return Math.max(0, Math.min(requested ?? this.spec.commandTimeoutMs, this.spec.commandTimeoutMs, this.spec.budgetMs - this.spent, this.spec.deadline - this.now()));
|
|
55
|
+
}
|
|
56
|
+
async run(input, signal = new AbortController().signal) {
|
|
57
|
+
const source = this.environment;
|
|
58
|
+
const env = Object.fromEntries(this.spec.env.flatMap((name) => (source[name] === undefined ? [] : [[name, source[name]]])));
|
|
59
|
+
const redact = redactor(this.spec.secrets.flatMap((name) => (source[name] ? [source[name]] : [])));
|
|
60
|
+
const limitMs = this.limit(input.timeoutMs);
|
|
61
|
+
// Аргументы — тоже сохраняемое поле: секрет в них попадал в результат и журнал.
|
|
62
|
+
const base = {
|
|
63
|
+
runId: randomUUID(),
|
|
64
|
+
argv: input.argv.map((part) => redact(part)),
|
|
65
|
+
limitMs,
|
|
66
|
+
stdout: '',
|
|
67
|
+
stderr: '',
|
|
68
|
+
};
|
|
69
|
+
let result;
|
|
70
|
+
// Меньше секунды на команду — это уже не проверка, а гарантированный
|
|
71
|
+
// таймаут: честнее сказать, что время кончилось.
|
|
72
|
+
if (limitMs < 1000)
|
|
73
|
+
result = {
|
|
74
|
+
...base,
|
|
75
|
+
status: 'budget-exhausted',
|
|
76
|
+
exitCode: null,
|
|
77
|
+
durationMs: 0,
|
|
78
|
+
budgetLeftMs: Math.max(0, this.spec.budgetMs - this.spent),
|
|
79
|
+
note: 'Бюджет проверок ревью или время до заключения исчерпаны; дай заключение по уже собранному.',
|
|
80
|
+
};
|
|
81
|
+
else {
|
|
82
|
+
const started = this.now();
|
|
83
|
+
// Предел команды резервируется до запуска, в том же синхронном шаге,
|
|
84
|
+
// что и расчёт: одновременные вызовы иначе видели один остаток и
|
|
85
|
+
// вместе превышали бюджет. После завершения резерв заменяется
|
|
86
|
+
// фактической длительностью.
|
|
87
|
+
this.spent += limitMs;
|
|
88
|
+
try {
|
|
89
|
+
const r = await runCheck({ config: { isolation: this.spec.isolation } }, {
|
|
90
|
+
argv: input.argv,
|
|
91
|
+
cwd: this.spec.cwd,
|
|
92
|
+
env: { ...env, CI: '1' },
|
|
93
|
+
redact,
|
|
94
|
+
timeoutMs: limitMs,
|
|
95
|
+
signal,
|
|
96
|
+
// Проверяемые исходники — только чтение; пишет проверка во
|
|
97
|
+
// временный каталог, который runCheck создаёт и убирает сам.
|
|
98
|
+
write: [],
|
|
99
|
+
readable: this.spec.readable,
|
|
100
|
+
controller: this.spec.controller,
|
|
101
|
+
settingsDir: this.spec.settingsDir,
|
|
102
|
+
});
|
|
103
|
+
const durationMs = this.now() - started;
|
|
104
|
+
this.spent += durationMs - limitMs;
|
|
105
|
+
result = {
|
|
106
|
+
...base,
|
|
107
|
+
status: r.timedOut ? 'timeout' : r.code === 0 ? 'passed' : 'failed',
|
|
108
|
+
exitCode: r.timedOut ? null : r.code,
|
|
109
|
+
durationMs,
|
|
110
|
+
budgetLeftMs: Math.max(0, this.spec.budgetMs - this.spent),
|
|
111
|
+
stdout: tail(r.stdout),
|
|
112
|
+
stderr: tail(r.stderr),
|
|
113
|
+
...(r.timedOut
|
|
114
|
+
? {
|
|
115
|
+
note: `Команда не завершилась за ${limitMs} мс и остановлена вместе с потомками. Это наблюдение о команде на этом входе, а не доказанный дефект: назови вход и предел, если делаешь из него замечание.`,
|
|
116
|
+
}
|
|
117
|
+
: {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const durationMs = this.now() - started;
|
|
122
|
+
this.spent += durationMs - limitMs;
|
|
123
|
+
result = {
|
|
124
|
+
...base,
|
|
125
|
+
status: 'error',
|
|
126
|
+
exitCode: null,
|
|
127
|
+
durationMs,
|
|
128
|
+
budgetLeftMs: Math.max(0, this.spec.budgetMs - this.spent),
|
|
129
|
+
note: 'Команда не запустилась — это отказ среды, а не результат проверки: ' +
|
|
130
|
+
redact(error instanceof Error ? error.message : String(error)),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Журнал — наблюдение controller, а не самоотчёт модели.
|
|
135
|
+
await appendFile(this.spec.log, JSON.stringify({ at: new Date().toISOString(), ...result }) + '\n').catch(() => undefined);
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Stdio MCP-сервер с одним инструментом — его запускает CLI reviewer.
|
|
141
|
+
* Протокол — построчный JSON-RPC 2.0 (`initialize`, `tools/list`,
|
|
142
|
+
* `tools/call`, `ping`); вызовы исполняются одновременно, бюджет общий —
|
|
143
|
+
* один экземпляр `ReviewProbes` на сервер.
|
|
144
|
+
*/
|
|
145
|
+
export function serveReviewProbes(probes, input = process.stdin, output = process.stdout) {
|
|
146
|
+
const send = (message) => output.write(JSON.stringify(message) + '\n');
|
|
147
|
+
const tool = {
|
|
148
|
+
name: probeTool,
|
|
149
|
+
description: `Run a check command (argv, no shell) in the candidate's directory with read-only sources. The controller stops it after ${probes.spec.commandTimeoutMs} ms per command and a total probe budget of ${probes.spec.budgetMs} ms; a timeout is returned as a result. The result carries a runId: cite it in reproduction.run when a finding rests on that run.`,
|
|
150
|
+
inputSchema: z.toJSONSchema(probeInput, { target: 'draft-7' }),
|
|
151
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
152
|
+
};
|
|
153
|
+
const pending = new Set();
|
|
154
|
+
const lines = createInterface({ input });
|
|
155
|
+
lines.on('line', (line) => {
|
|
156
|
+
if (!line.trim())
|
|
157
|
+
return;
|
|
158
|
+
let message;
|
|
159
|
+
try {
|
|
160
|
+
message = JSON.parse(line);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const { id, method, params } = message;
|
|
167
|
+
// Уведомления (без id) ответа не требуют.
|
|
168
|
+
if (id === undefined)
|
|
169
|
+
return;
|
|
170
|
+
const reply = (result) => send({ jsonrpc: '2.0', id, result });
|
|
171
|
+
const fail = (code, text) => send({ jsonrpc: '2.0', id, error: { code, message: text } });
|
|
172
|
+
if (method === 'initialize')
|
|
173
|
+
return reply({
|
|
174
|
+
protocolVersion: typeof params?.protocolVersion === 'string' ? params.protocolVersion : '2025-06-18',
|
|
175
|
+
capabilities: { tools: {} },
|
|
176
|
+
serverInfo: { name: probeServer, version: '1' },
|
|
177
|
+
});
|
|
178
|
+
if (method === 'ping')
|
|
179
|
+
return reply({});
|
|
180
|
+
if (method === 'tools/list')
|
|
181
|
+
return reply({ tools: [tool] });
|
|
182
|
+
if (method === 'tools/call') {
|
|
183
|
+
if (params?.name !== probeTool)
|
|
184
|
+
return fail(-32602, 'Неизвестный инструмент');
|
|
185
|
+
const parsed = probeInput.safeParse(params.arguments);
|
|
186
|
+
if (!parsed.success)
|
|
187
|
+
return fail(-32602, 'Неверные аргументы: ' + parsed.error.message);
|
|
188
|
+
const call = probes
|
|
189
|
+
.run(parsed.data)
|
|
190
|
+
.then((result) => reply({
|
|
191
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
192
|
+
structuredContent: result,
|
|
193
|
+
}))
|
|
194
|
+
.catch((error) => fail(-32603, String(error)))
|
|
195
|
+
.finally(() => pending.delete(call));
|
|
196
|
+
pending.add(call);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
fail(-32601, 'Метод не поддерживается: ' + String(method));
|
|
200
|
+
});
|
|
201
|
+
return { lines, pending };
|
|
202
|
+
}
|
|
203
|
+
/** Пределы проверок ревью, выведенные из срока фазы. */
|
|
204
|
+
export function probeLimits(policy) {
|
|
205
|
+
const reserveMs = Math.max(policy.reviewProbes.reserveMs, Math.round(policy.runTimeoutMs * 0.2));
|
|
206
|
+
return {
|
|
207
|
+
commandTimeoutMs: Math.min(policy.reviewProbes.commandTimeoutMs, policy.runTimeoutMs),
|
|
208
|
+
budgetMs: Math.max(1000, Math.min(policy.reviewProbes.budgetMs, policy.runTimeoutMs - reserveMs)),
|
|
209
|
+
reserveMs,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/** Срок вызова инструмента в CLI — с запасом сверх предела команды controller. */
|
|
213
|
+
export const probeCallMs = (probe) => probe.commandTimeoutMs + 30000;
|
|
214
|
+
/**
|
|
215
|
+
* Команда, которой CLI reviewer запускает сервер проверок — этот же пакет.
|
|
216
|
+
* CLI стартует её из проверяемого каталога, поэтому загрузчик исходников
|
|
217
|
+
* указан абсолютным путём: из worktree он бы не разрешился.
|
|
218
|
+
*/
|
|
219
|
+
export function probeCommand(specPath) {
|
|
220
|
+
const source = import.meta.url.endsWith('.ts');
|
|
221
|
+
const entry = fileURLToPath(new URL(source ? './probe-server.ts' : './probe-server.js', import.meta.url));
|
|
222
|
+
return {
|
|
223
|
+
command: process.execPath,
|
|
224
|
+
args: [...(source ? ['--import', import.meta.resolve('tsx')] : []), entry, '--spec', specPath],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** Журнал проверок reviewer: по записи на запуск; нечитаемая строка — не запуск. */
|
|
228
|
+
export async function probeRecords(log) {
|
|
229
|
+
const text = await readFile(log, 'utf8').catch(() => '');
|
|
230
|
+
return text
|
|
231
|
+
.split('\n')
|
|
232
|
+
.filter(Boolean)
|
|
233
|
+
.flatMap((line) => {
|
|
234
|
+
try {
|
|
235
|
+
const p = JSON.parse(line);
|
|
236
|
+
return [
|
|
237
|
+
{
|
|
238
|
+
runId: String(p.runId),
|
|
239
|
+
argv: p.argv,
|
|
240
|
+
status: p.status,
|
|
241
|
+
durationMs: p.durationMs,
|
|
242
|
+
limitMs: p.limitMs,
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Воспроизведения находок, сверенные с журналом controller. «Наблюдено» —
|
|
253
|
+
* только по запуску, на который сослался reviewer: он состоялся (исчерпанный
|
|
254
|
+
* бюджет и отказ до старта — не наблюдение), совпадение единственное, и
|
|
255
|
+
* команда в записи — та, что действительно запускалась. Команда из текста
|
|
256
|
+
* находки не исполняется. Ответ reviewer мог прийти уже замаскированным, и
|
|
257
|
+
* короткий секрет задевает символы id: id журнала сравниваются и в той же маске.
|
|
258
|
+
*/
|
|
259
|
+
export function observeReproductions(findings, probes, redact) {
|
|
260
|
+
return findings.map((f) => {
|
|
261
|
+
const r = f.reproduction;
|
|
262
|
+
if (!r)
|
|
263
|
+
return f;
|
|
264
|
+
const matches = r.run
|
|
265
|
+
? (probes ?? []).filter((p) => p.runId === r.run || redact(p.runId) === r.run)
|
|
266
|
+
: [];
|
|
267
|
+
const cited = matches.length === 1 ? matches[0] : undefined;
|
|
268
|
+
const ran = !!cited && ['passed', 'failed', 'timeout'].includes(cited.status);
|
|
269
|
+
return {
|
|
270
|
+
...f,
|
|
271
|
+
reproduction: {
|
|
272
|
+
...r,
|
|
273
|
+
command: ran ? cited.argv : r.command,
|
|
274
|
+
run: ran ? cited.runId : null,
|
|
275
|
+
observed: ran,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare function runtimeEvents(output: string): {
|
|
3
|
+
events: Record<string, unknown>[];
|
|
4
|
+
malformed: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare const claudeResult: z.ZodObject<{
|
|
7
|
+
type: z.ZodLiteral<"result">;
|
|
8
|
+
is_error: z.ZodOptional<z.ZodBoolean>;
|
|
9
|
+
result: z.ZodOptional<z.ZodString>;
|
|
10
|
+
structured_output: z.ZodUnknown;
|
|
11
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const object = z.record(z.string(), z.unknown());
|
|
3
|
+
export function runtimeEvents(output) {
|
|
4
|
+
const events = [];
|
|
5
|
+
let malformed = false;
|
|
6
|
+
for (const line of output.split('\n').filter((s) => s.trim())) {
|
|
7
|
+
try {
|
|
8
|
+
events.push(object.parse(JSON.parse(line)));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
malformed = true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return { events, malformed };
|
|
15
|
+
}
|
|
16
|
+
export const claudeResult = z.object({
|
|
17
|
+
type: z.literal('result'),
|
|
18
|
+
is_error: z.boolean().optional(),
|
|
19
|
+
result: z.string().optional(),
|
|
20
|
+
structured_output: z.unknown(),
|
|
21
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export declare const implementationOutputSchema: {
|
|
2
|
+
type: string;
|
|
3
|
+
additionalProperties: boolean;
|
|
4
|
+
properties: {
|
|
5
|
+
completed: {
|
|
6
|
+
type: string;
|
|
7
|
+
};
|
|
8
|
+
summary: {
|
|
9
|
+
type: string;
|
|
10
|
+
};
|
|
11
|
+
discoveries: {
|
|
12
|
+
type: string;
|
|
13
|
+
maxItems: number;
|
|
14
|
+
items: {
|
|
15
|
+
type: string;
|
|
16
|
+
additionalProperties: boolean;
|
|
17
|
+
properties: {
|
|
18
|
+
kind: {
|
|
19
|
+
type: string;
|
|
20
|
+
enum: string[];
|
|
21
|
+
};
|
|
22
|
+
title: {
|
|
23
|
+
type: string;
|
|
24
|
+
};
|
|
25
|
+
path: {
|
|
26
|
+
type: string;
|
|
27
|
+
};
|
|
28
|
+
line: {
|
|
29
|
+
type: string;
|
|
30
|
+
minimum: number;
|
|
31
|
+
};
|
|
32
|
+
observation: {
|
|
33
|
+
type: string;
|
|
34
|
+
};
|
|
35
|
+
consequence: {
|
|
36
|
+
type: string;
|
|
37
|
+
};
|
|
38
|
+
reproduction: {
|
|
39
|
+
type: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
required: string[];
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
required: string[];
|
|
47
|
+
};
|
|
48
|
+
export declare const reviewOutputSchema: {
|
|
49
|
+
type: string;
|
|
50
|
+
additionalProperties: boolean;
|
|
51
|
+
properties: {
|
|
52
|
+
execution: {
|
|
53
|
+
type: string;
|
|
54
|
+
additionalProperties: boolean;
|
|
55
|
+
properties: {
|
|
56
|
+
commands: {
|
|
57
|
+
type: string;
|
|
58
|
+
maxItems: number;
|
|
59
|
+
items: {
|
|
60
|
+
type: string;
|
|
61
|
+
additionalProperties: boolean;
|
|
62
|
+
properties: {
|
|
63
|
+
command: {
|
|
64
|
+
type: string;
|
|
65
|
+
};
|
|
66
|
+
exitCode: {
|
|
67
|
+
type: string;
|
|
68
|
+
};
|
|
69
|
+
summary: {
|
|
70
|
+
type: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
required: string[];
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
noCommandsReason: {
|
|
77
|
+
type: string[];
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
required: string[];
|
|
81
|
+
};
|
|
82
|
+
approved: {
|
|
83
|
+
type: string;
|
|
84
|
+
};
|
|
85
|
+
summary: {
|
|
86
|
+
type: string;
|
|
87
|
+
};
|
|
88
|
+
discoveries: {
|
|
89
|
+
type: string;
|
|
90
|
+
maxItems: number;
|
|
91
|
+
items: {
|
|
92
|
+
type: string;
|
|
93
|
+
additionalProperties: boolean;
|
|
94
|
+
properties: {
|
|
95
|
+
kind: {
|
|
96
|
+
type: string;
|
|
97
|
+
enum: string[];
|
|
98
|
+
};
|
|
99
|
+
title: {
|
|
100
|
+
type: string;
|
|
101
|
+
};
|
|
102
|
+
path: {
|
|
103
|
+
type: string;
|
|
104
|
+
};
|
|
105
|
+
line: {
|
|
106
|
+
type: string;
|
|
107
|
+
minimum: number;
|
|
108
|
+
};
|
|
109
|
+
observation: {
|
|
110
|
+
type: string;
|
|
111
|
+
};
|
|
112
|
+
consequence: {
|
|
113
|
+
type: string;
|
|
114
|
+
};
|
|
115
|
+
reproduction: {
|
|
116
|
+
type: string;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
required: string[];
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
findings: {
|
|
123
|
+
type: string;
|
|
124
|
+
items: {
|
|
125
|
+
type: string;
|
|
126
|
+
additionalProperties: boolean;
|
|
127
|
+
properties: {
|
|
128
|
+
severity: {
|
|
129
|
+
type: string;
|
|
130
|
+
enum: string[];
|
|
131
|
+
};
|
|
132
|
+
message: {
|
|
133
|
+
type: string;
|
|
134
|
+
};
|
|
135
|
+
path: {
|
|
136
|
+
type: string[];
|
|
137
|
+
};
|
|
138
|
+
line: {
|
|
139
|
+
type: string[];
|
|
140
|
+
minimum: number;
|
|
141
|
+
};
|
|
142
|
+
rule: {
|
|
143
|
+
type: string[];
|
|
144
|
+
};
|
|
145
|
+
consequence: {
|
|
146
|
+
type: string[];
|
|
147
|
+
};
|
|
148
|
+
evidence: {
|
|
149
|
+
type: string[];
|
|
150
|
+
};
|
|
151
|
+
reproduction: {
|
|
152
|
+
anyOf: ({
|
|
153
|
+
type: string;
|
|
154
|
+
additionalProperties?: undefined;
|
|
155
|
+
properties?: undefined;
|
|
156
|
+
required?: undefined;
|
|
157
|
+
} | {
|
|
158
|
+
type: string;
|
|
159
|
+
additionalProperties: boolean;
|
|
160
|
+
properties: {
|
|
161
|
+
property: {
|
|
162
|
+
type: string[];
|
|
163
|
+
};
|
|
164
|
+
input: {
|
|
165
|
+
type: string;
|
|
166
|
+
};
|
|
167
|
+
expected: {
|
|
168
|
+
type: string[];
|
|
169
|
+
};
|
|
170
|
+
actual: {
|
|
171
|
+
type: string[];
|
|
172
|
+
};
|
|
173
|
+
command: {
|
|
174
|
+
type: string[];
|
|
175
|
+
items: {
|
|
176
|
+
type: string;
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
executed: {
|
|
180
|
+
type: string;
|
|
181
|
+
};
|
|
182
|
+
run: {
|
|
183
|
+
type: string[];
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
required: string[];
|
|
187
|
+
})[];
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
required: string[];
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
previous: {
|
|
194
|
+
type: string;
|
|
195
|
+
items: {
|
|
196
|
+
type: string;
|
|
197
|
+
additionalProperties: boolean;
|
|
198
|
+
properties: {
|
|
199
|
+
id: {
|
|
200
|
+
type: string;
|
|
201
|
+
};
|
|
202
|
+
status: {
|
|
203
|
+
type: string;
|
|
204
|
+
enum: string[];
|
|
205
|
+
};
|
|
206
|
+
basis: {
|
|
207
|
+
type: string;
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
required: string[];
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
required: string[];
|
|
215
|
+
};
|