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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,6 +40,7 @@ export declare const taskMappingSchema: z.ZodObject<{
40
40
  }>>;
41
41
  }, z.core.$strict>>>;
42
42
  requiredDocuments: z.ZodDefault<z.ZodArray<z.ZodString>>;
43
+ context: z.ZodOptional<z.ZodArray<z.ZodString>>;
43
44
  }, z.core.$strict>;
44
45
  export type TaskMapping = z.infer<typeof taskMappingSchema>;
45
46
  export declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -51,6 +52,7 @@ export declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject
51
52
  projectId: z.ZodString;
52
53
  actor: z.ZodString;
53
54
  stateDir: z.ZodOptional<z.ZodString>;
55
+ contextDir: z.ZodOptional<z.ZodString>;
54
56
  repository: z.ZodObject<{
55
57
  id: z.ZodString;
56
58
  path: z.ZodOptional<z.ZodString>;
@@ -72,6 +74,7 @@ export declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject
72
74
  }>>;
73
75
  }, z.core.$strict>>>;
74
76
  requiredDocuments: z.ZodDefault<z.ZodArray<z.ZodString>>;
77
+ context: z.ZodOptional<z.ZodArray<z.ZodString>>;
75
78
  }, z.core.$strict>>;
76
79
  completion: z.ZodDefault<z.ZodObject<{
77
80
  mode: z.ZodDefault<z.ZodEnum<{
@@ -122,6 +125,7 @@ export declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject
122
125
  projectId: z.ZodString;
123
126
  actor: z.ZodString;
124
127
  stateDir: z.ZodOptional<z.ZodString>;
128
+ contextDir: z.ZodOptional<z.ZodString>;
125
129
  repository: z.ZodObject<{
126
130
  id: z.ZodString;
127
131
  path: z.ZodOptional<z.ZodString>;
@@ -143,6 +147,7 @@ export declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject
143
147
  }>>;
144
148
  }, z.core.$strict>>>;
145
149
  requiredDocuments: z.ZodDefault<z.ZodArray<z.ZodString>>;
150
+ context: z.ZodOptional<z.ZodArray<z.ZodString>>;
146
151
  }, z.core.$strict>>;
147
152
  completion: z.ZodDefault<z.ZodObject<{
148
153
  mode: z.ZodDefault<z.ZodEnum<{
@@ -57,6 +57,13 @@ export const taskMappingSchema = z.strictObject({
57
57
  criteria: z.array(criterionMappingSchema).max(100).default([]),
58
58
  /** Документы, без которых запуск блокируется (ID, ключ или имя). Остальные — дополнительные. */
59
59
  requiredDocuments: z.array(z.string().min(1).max(200)).max(30).default([]),
60
+ /**
61
+ * Локальные пакеты контекста поручения — каталоги `<contextDir>/<id>` (например,
62
+ * оверлей controller-mode). Закрепляются в снимке вместе с контекстом задачи;
63
+ * изменение набора или содержимого пакетов — новый снимок и новое
64
+ * согласование; semantic digest задачи от них не зависит.
65
+ */
66
+ context: z.array(idSchema).min(1).max(10).optional(),
60
67
  });
61
68
  const relaySettings = z.record(z.string(), z.unknown());
62
69
  const common = {
@@ -69,6 +76,8 @@ const common = {
69
76
  * для процессов, разделяющих этот каталог.
70
77
  */
71
78
  stateDir: z.string().min(1).max(1000).optional(),
79
+ /** Каталог пакетов `tasks[].context` (относительно файла конфигурации); по умолчанию `work/context`. */
80
+ contextDir: z.string().min(1).max(1000).optional(),
72
81
  repository: z.strictObject({
73
82
  id: idSchema,
74
83
  /** Путь репозитория для сверки с политикой (относительно файла конфигурации). */
@@ -161,6 +170,12 @@ export function parseConfig(value, baseDir, file) {
161
170
  const dup = refs.find((r, i) => refs.indexOf(r) !== i);
162
171
  if (dup)
163
172
  throw new HarnessError('INVALID_INPUT', 'source: задача указана дважды: ' + dup);
173
+ // ID контекста задачи — `c` + 20 hex (contextIdOf): локальный пакет не может его занять.
174
+ const taken = parsed.data.tasks
175
+ .flatMap((t) => t.context ?? [])
176
+ .find((id) => /^c[0-9a-f]{20}$/.test(id));
177
+ if (taken)
178
+ throw new HarnessError('INVALID_INPUT', `source: пакет контекста ${taken} совпадает по форме с ID контекста задачи; выберите другое имя`);
164
179
  return { config: parsed.data, baseDir, ...(file ? { file } : {}) };
165
180
  }
166
181
  export function loadConfig(path) {
@@ -204,7 +219,9 @@ export const contextIdOf = (orderId) => 'c' + orderId.slice(1);
204
219
  export const graphIdOf = (provider, projectId, taskIds) => 'g' + short([provider, projectId, ...[...taskIds].sort()].join('\n'));
205
220
  /** Digest сопоставления задачи: входит в semantic digest — смена mapping требует нового согласования. */
206
221
  export const mappingDigest = (config, mapping) => digestOf({
207
- mapping: { ...mapping, ref: undefined },
222
+ // Пакеты контекста — не постановка задачи: их ID и digest входят в ревизию
223
+ // набора (новое согласование), но доставку принятого результата не меняют.
224
+ mapping: { ...mapping, ref: undefined, context: undefined },
208
225
  repository: config.repository.id,
209
226
  completion: config.completion,
210
227
  });
@@ -6,7 +6,7 @@
6
6
  export * from './transport.ts';
7
7
  export { connectorConfigSchema, criterionMappingSchema, taskMappingSchema, loadConfig, parseConfig, connectorIdOf, taskIdentity, parseIdentity, orderIdOf, graphIdOf, contextIdOf, mappingDigest, completionColumn, storeFileOf, type ConnectorConfig, type ConnectorConfigInput, type CriterionMapping, type TaskMapping, type LoadedConfig, } from './config.ts';
8
8
  export { ConnectorStore, LeaseLost, type OperationRecord, type OperationStatus, type StepRecord, type StepStatus, type LeaseRecord, type ReceiptRecord, type SnapshotRecord, } from './store.ts';
9
- export { buildSnapshot, checkIdentity, readStable, semanticDigest, takeSnapshot, assertEligible, type BuiltSnapshot, type SnapshotManifest, type SnapshotProblem, type TaskManifest, } from './snapshot.ts';
9
+ export { buildSnapshot, checkIdentity, readStable, semanticDigest, contextLimitIssues, localContext, takeSnapshot, assertEligible, type BuiltSnapshot, type SnapshotManifest, type SnapshotProblem, type TaskManifest, } from './snapshot.ts';
10
10
  export { RelayWorkSource } from './source.ts';
11
11
  export { RelaySink, operationKey, type ReconcileDecision, type SinkOptions } from './delivery.ts';
12
12
  export { HistoryRecorder, type SessionLink } from './history.ts';
@@ -6,7 +6,7 @@
6
6
  export * from "./transport.js";
7
7
  export { connectorConfigSchema, criterionMappingSchema, taskMappingSchema, loadConfig, parseConfig, connectorIdOf, taskIdentity, parseIdentity, orderIdOf, graphIdOf, contextIdOf, mappingDigest, completionColumn, storeFileOf, } from "./config.js";
8
8
  export { ConnectorStore, LeaseLost, } from "./store.js";
9
- export { buildSnapshot, checkIdentity, readStable, semanticDigest, takeSnapshot, assertEligible, } from "./snapshot.js";
9
+ export { buildSnapshot, checkIdentity, readStable, semanticDigest, contextLimitIssues, localContext, takeSnapshot, assertEligible, } from "./snapshot.js";
10
10
  export { RelayWorkSource } from "./source.js";
11
11
  export { RelaySink, operationKey } from "./delivery.js";
12
12
  export { HistoryRecorder } from "./history.js";
@@ -1,4 +1,4 @@
1
- import { HarnessError, type ContextSnapshot, type WorkGraph } from '../../contracts/index.ts';
1
+ import { HarnessError, type ContextSnapshot, type ContextSourceEntry, type WorkGraph } from '../../contracts/index.ts';
2
2
  import { type ConnectorConfig, type TaskMapping } from './config.ts';
3
3
  import type { ReceiptRecord } from './store.ts';
4
4
  import { type CriterionRecord, type RelayTransport, type TaskRead, type TransportIdentity } from './transport.ts';
@@ -84,6 +84,15 @@ export interface SnapshotManifest {
84
84
  tasks: TaskManifest[];
85
85
  truncated: string[];
86
86
  problems: SnapshotProblem[];
87
+ /** Закреплённые локальные пакеты `tasks[].context`; без них поля нет. */
88
+ localContext?: LocalContextManifest[];
89
+ }
90
+ export interface LocalContextManifest {
91
+ id: string;
92
+ digest: string;
93
+ files: number;
94
+ references: number;
95
+ bytes: number;
87
96
  }
88
97
  export interface BuiltSnapshot {
89
98
  graph: WorkGraph;
@@ -119,12 +128,24 @@ export declare function buildSnapshot(input: {
119
128
  rounds: number;
120
129
  /** Доверенный receipt этого коннектора для задачи-зависимости вне графа. */
121
130
  receiptOf: (orderId: string) => ReceiptRecord | undefined;
131
+ /** Закреплённые локальные пакеты `tasks[].context` (localContext). */
132
+ local?: ContextSourceEntry[];
122
133
  }): BuiltSnapshot;
134
+ /** Нарушения пределов снимка контекста: число источников, размер файла и сумма. */
135
+ export declare function contextLimitIssues(context: ContextSnapshot): string[];
136
+ /**
137
+ * Локальные пакеты `tasks[].context` из `contextDir`: те же правила, что у
138
+ * файлового контекста графа (каталог `<contextDir>/<id>`, `references/` —
139
+ * справки). Нет каталога или пакета — NOT_READY до чтения backend.
140
+ */
141
+ export declare function localContext(config: ConnectorConfig, contextDir: string | undefined): Promise<ContextSourceEntry[]>;
123
142
  /** Прочитать, собрать и проверить идентичность: общий путь `source-snapshot` и `run --source`. */
124
143
  export declare function takeSnapshot(input: {
125
144
  config: ConnectorConfig;
126
145
  transport: RelayTransport;
127
146
  receiptOf: (orderId: string) => ReceiptRecord | undefined;
147
+ /** Абсолютный каталог пакетов `tasks[].context`. */
148
+ contextDir?: string;
128
149
  signal?: AbortSignal;
129
150
  }): Promise<BuiltSnapshot>;
130
151
  /** Отказ по eligibility/limits: причина и предел в сообщении, без запуска. */
@@ -1,6 +1,8 @@
1
- import { API_VERSION, HarnessError, contextDigest, digestOf, sha256, sourceDigest, workGraphDigest, workGraphSchema, } from "../../contracts/index.js";
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { API_VERSION, HarnessError, CONTEXT_LIMITS, contextDigest, contextIssues, digestOf, sha256, sourceDigest, workGraphDigest, workGraphSchema, } from "../../contracts/index.js";
2
3
  import { connectorIdOf, contextIdOf, graphIdOf, mappingDigest, orderIdOf, taskIdentity, } from "./config.js";
3
4
  import { TransportError, } from "./transport.js";
5
+ import { FileContextSource } from "../../connectors/files.js";
4
6
  /** Безопасная причина отказа транспорта при чтении: сеть/CLI — не пустой список. */
5
7
  export function readFailure(error, what) {
6
8
  if (error instanceof HarnessError)
@@ -124,6 +126,8 @@ export function buildSnapshot(input) {
124
126
  const missing = [];
125
127
  const prerequisites = new Map();
126
128
  const tasks = [];
129
+ /** Какие локальные пакеты у какой задачи (по порядку): часть ревизии набора. */
130
+ const packsByTask = [];
127
131
  const truncated = [];
128
132
  let totalBytes = 0;
129
133
  const limits = config.limits;
@@ -331,8 +335,9 @@ export function buildSnapshot(input) {
331
335
  ...(mapping.gates ? { gates: mapping.gates } : {}),
332
336
  requirements: [],
333
337
  contracts: [],
334
- context: [entry.id],
338
+ context: [entry.id, ...(mapping.context ?? [])],
335
339
  });
340
+ packsByTask.push([identityOf, mapping.context ?? []]);
336
341
  tasks.push({
337
342
  ref: mapping.ref,
338
343
  id: task.id,
@@ -367,6 +372,15 @@ export function buildSnapshot(input) {
367
372
  connector,
368
373
  tasks: tasks.map((t) => [t.identity, t.semanticDigest]),
369
374
  prerequisites: prereqs,
375
+ // Без локальных пакетов ревизия та же, что до их появления. С ними —
376
+ // содержимое пакетов и их распределение по задачам: перенос пакета между
377
+ // задачами или смена порядка — тоже новое согласование.
378
+ ...(input.local?.length || packsByTask.some(([, ids]) => ids.length)
379
+ ? {
380
+ context: (input.local ?? []).map((s) => [s.id, s.digest]),
381
+ contextBy: [...packsByTask].sort(([a], [b]) => (a < b ? -1 : 1)),
382
+ }
383
+ : {}),
370
384
  });
371
385
  const joined = `${config.provider}:${config.projectId}:${taskIds.join('+')}`;
372
386
  const graph = workGraphSchema.parse({
@@ -380,7 +394,15 @@ export function buildSnapshot(input) {
380
394
  orders,
381
395
  prerequisites: prereqs,
382
396
  });
383
- const context = { apiVersion: API_VERSION, sources, missing };
397
+ const context = {
398
+ apiVersion: API_VERSION,
399
+ sources: [...sources, ...(input.local ?? [])],
400
+ missing,
401
+ };
402
+ // Пределы снимка контекста проверяются здесь, а не только при запуске:
403
+ // source-snapshot и source-check называют их как отказ eligibility.
404
+ for (const issue of contextLimitIssues(context))
405
+ problems.push({ code: 'LIMIT_EXCEEDED', taskId: '', key: 'context', message: issue });
384
406
  const manifest = {
385
407
  version: 1,
386
408
  provider: config.provider,
@@ -398,11 +420,45 @@ export function buildSnapshot(input) {
398
420
  tasks,
399
421
  truncated,
400
422
  problems,
423
+ ...(input.local?.length ? { localContext: input.local.map(localManifest) } : {}),
401
424
  };
402
425
  return { graph, context, manifest, revision, problems };
403
426
  }
427
+ const CONTEXT_SOURCES_MAX = 30;
428
+ /** Нарушения пределов снимка контекста: число источников, размер файла и сумма. */
429
+ export function contextLimitIssues(context) {
430
+ const issues = contextIssues(context);
431
+ if (context.sources.length > CONTEXT_SOURCES_MAX)
432
+ issues.push(`Источников контекста ${context.sources.length} при пределе ${CONTEXT_SOURCES_MAX} (задачи и локальные пакеты)`);
433
+ return issues.map((i) => /больше предела/.test(i)
434
+ ? `${i} (fileBytes=${CONTEXT_LIMITS.fileBytes}, totalBytes=${CONTEXT_LIMITS.totalBytes})`
435
+ : i);
436
+ }
437
+ const localManifest = (s) => ({
438
+ id: s.id,
439
+ digest: s.digest,
440
+ files: s.files.length,
441
+ references: s.references.length,
442
+ bytes: [...s.files, ...s.references].reduce((n, f) => n + f.bytes, 0),
443
+ });
444
+ /**
445
+ * Локальные пакеты `tasks[].context` из `contextDir`: те же правила, что у
446
+ * файлового контекста графа (каталог `<contextDir>/<id>`, `references/` —
447
+ * справки). Нет каталога или пакета — NOT_READY до чтения backend.
448
+ */
449
+ export async function localContext(config, contextDir) {
450
+ const ids = [...new Set(config.tasks.flatMap((t) => t.context ?? []))].sort();
451
+ if (!ids.length)
452
+ return [];
453
+ if (!contextDir)
454
+ throw new HarnessError('INVALID_INPUT', 'source: tasks[].context требует каталог пакетов (contextDir)');
455
+ if (!existsSync(contextDir) || !statSync(contextDir).isDirectory())
456
+ throw new HarnessError('NOT_READY', `Каталог пакетов контекста ${contextDir} не найден (tasks[].context: ${ids.join(', ')})`);
457
+ return (await new FileContextSource(contextDir).snapshot(ids)).sources;
458
+ }
404
459
  /** Прочитать, собрать и проверить идентичность: общий путь `source-snapshot` и `run --source`. */
405
460
  export async function takeSnapshot(input) {
461
+ const local = await localContext(input.config, input.contextDir);
406
462
  const identity = await checkIdentity(input.transport, input.config, input.signal);
407
463
  const { reads, rounds } = await readStable(input.transport, input.config.tasks.map((t) => t.ref), input.config.limits.rereads, input.signal);
408
464
  return buildSnapshot({
@@ -411,6 +467,7 @@ export async function takeSnapshot(input) {
411
467
  reads,
412
468
  rounds,
413
469
  receiptOf: input.receiptOf,
470
+ ...(local.length ? { local } : {}),
414
471
  });
415
472
  }
416
473
  /** Отказ по eligibility/limits: причина и предел в сообщении, без запуска. */
@@ -19,12 +19,16 @@ export declare class RelayWorkSource implements WorkSource {
19
19
  config: ConnectorConfig;
20
20
  transport: RelayTransport;
21
21
  store: ConnectorStore;
22
+ /** Абсолютный каталог пакетов `tasks[].context`. */
23
+ contextDir?: string;
22
24
  };
23
25
  readonly id: string;
24
26
  constructor(options: {
25
27
  config: ConnectorConfig;
26
28
  transport: RelayTransport;
27
29
  store: ConnectorStore;
30
+ /** Абсолютный каталог пакетов `tasks[].context`. */
31
+ contextDir?: string;
28
32
  });
29
33
  /** Снимок без проверки eligibility (для предпросмотра и диагностики). */
30
34
  snapshot(signal?: AbortSignal): Promise<BuiltSnapshot>;
@@ -26,6 +26,7 @@ export class RelayWorkSource {
26
26
  config: this.options.config,
27
27
  transport: this.options.transport,
28
28
  receiptOf: (orderId) => this.options.store.receipts(orderId).at(-1),
29
+ ...(this.options.contextDir ? { contextDir: this.options.contextDir } : {}),
29
30
  ...(signal ? { signal } : {}),
30
31
  });
31
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devcontour-harness",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.13",
4
4
  "description": "Headless execution harness: work graph, isolated attempts, gates, independent reviews and accepted results.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,8 +39,10 @@ shasum -a 256 <file.tgz>
39
39
  ```
40
40
 
41
41
  Use the exact tarball (or exact registry version) you were given and record
42
- its sha256. Do not install it into the project: the project `package.json`
43
- and lockfile stay untouched.
42
+ its sha256. Do not install it into the project's dependencies: the project
43
+ `package.json` and lockfile stay untouched. `<tooldir>` is outside the
44
+ repository or `<repo>/.harness-tools`; the `harness-setup` skill of the
45
+ package covers both, the version pin and upgrades.
44
46
 
45
47
  ### 2. Check the machine
46
48
 
@@ -65,19 +67,19 @@ Schema of input and response, and the byte limits. Read only.
65
67
  interruption it resumes from the saved manifest decisions and writes only
66
68
  what is missing.
67
69
 
68
- | Conflict or refusal | Meaning | Action |
69
- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
70
- | `symlink` | the path or an ancestor (`conflictPath`) is a symlink | never write through it; the user replaces the link |
71
- | `ancestor-not-directory` | an ancestor of the path is a file | ask the user; never delete their files |
72
- | `not-regular-file` | the destination exists and is not a regular file | ask the user |
73
- | `outside-root` | the destination resolves outside the repository | ask the user |
74
- | `content-differs` on `.harness/project.json` | an explicit `--writer` or `--profile` differs from the saved manifest | keep the saved decision, or the user confirms the change explicitly |
75
- | `content-differs` on `.harness/policy.json` | the saved policy's writer differs from the manifest or explicit `--writer` | keep the saved decision, or the user confirms the change explicitly |
76
- | `INVALID_INPUT` with a path | not a Git root, invalid manifest or policy (also a truncated write), `--state` inside the repository | read the issue path; a broken file is removed by a human, not overwritten |
77
- | `NOT_READY` `no-commits` | the repository has no commits | the user commits first |
78
- | `NOT_READY` `stack-ambiguous` / `stack-unsupported` | both lockfiles, or none | pass `--profile`; when profile gates do not fit, write `.harness/profile.json` and pass `--profile ./.harness/profile.json` |
79
- | `NOT_READY` toolchain problems | missing scripts, wrong tool versions | fix the project or hand it to the user; never weaken the policy |
80
- | `CONTROLLER_BUSY` | the repository is already owned by another controller | stop: a second controller and state are not created |
70
+ | Conflict or refusal | Meaning | Action |
71
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
72
+ | `symlink` | the path or an ancestor (`conflictPath`) is a symlink | never write through it; the user replaces the link |
73
+ | `ancestor-not-directory` | an ancestor of the path is a file | ask the user; never delete their files |
74
+ | `not-regular-file` | the destination exists and is not a regular file | ask the user |
75
+ | `outside-root` | the destination resolves outside the repository | ask the user |
76
+ | `content-differs` on `.harness/project.json` | an explicit `--writer` or `--profile` differs from the saved manifest; after an upgrade or a move, also a changed built-in profile, root or API version | keep the saved decision, or the user confirms the change explicitly |
77
+ | `content-differs` on `.harness/policy.json` | the saved policy's writer differs from the manifest or explicit `--writer` | keep the saved decision, or the user confirms the change explicitly |
78
+ | `INVALID_INPUT` with a path | not a Git root, invalid manifest or policy (also a truncated write), `--state` inside the repository | read the issue path; a broken file is removed by a human, not overwritten |
79
+ | `NOT_READY` `no-commits` | the repository has no commits | the user commits first |
80
+ | `NOT_READY` `stack-ambiguous` / `stack-unsupported` | both lockfiles, or none | pass `--profile`; when profile gates do not fit, write `.harness/profile.json` and pass `--profile ./.harness/profile.json` |
81
+ | `NOT_READY` toolchain problems | missing scripts, wrong tool versions | fix the project or hand it to the user; never weaken the policy |
82
+ | `CONTROLLER_BUSY` | the repository is already owned by another controller | stop: a second controller and state are not created |
81
83
 
82
84
  ### 4. Plan the graph
83
85