openplanr 1.16.2 → 1.18.0
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/dist/cli/commands/operate.d.ts +10 -1
- package/dist/cli/commands/operate.d.ts.map +1 -1
- package/dist/cli/commands/operate.js +240 -13
- package/dist/cli/commands/operate.js.map +1 -1
- package/dist/services/ai-service.d.ts +25 -0
- package/dist/services/ai-service.d.ts.map +1 -1
- package/dist/services/ai-service.js +38 -0
- package/dist/services/ai-service.js.map +1 -1
- package/dist/services/operate/advisors.d.ts +82 -1
- package/dist/services/operate/advisors.d.ts.map +1 -1
- package/dist/services/operate/advisors.js +469 -17
- package/dist/services/operate/advisors.js.map +1 -1
- package/dist/services/operate/artifacts.d.ts +27 -1
- package/dist/services/operate/artifacts.d.ts.map +1 -1
- package/dist/services/operate/artifacts.js +103 -1
- package/dist/services/operate/artifacts.js.map +1 -1
- package/dist/services/operate/cadence.d.ts +50 -0
- package/dist/services/operate/cadence.d.ts.map +1 -0
- package/dist/services/operate/cadence.js +56 -0
- package/dist/services/operate/cadence.js.map +1 -0
- package/dist/services/operate/citation-resolution.d.ts +101 -0
- package/dist/services/operate/citation-resolution.d.ts.map +1 -0
- package/dist/services/operate/citation-resolution.js +308 -0
- package/dist/services/operate/citation-resolution.js.map +1 -0
- package/dist/services/operate/config.d.ts +93 -1
- package/dist/services/operate/config.d.ts.map +1 -1
- package/dist/services/operate/config.js +199 -9
- package/dist/services/operate/config.js.map +1 -1
- package/dist/services/operate/decision-brief.d.ts +148 -0
- package/dist/services/operate/decision-brief.d.ts.map +1 -0
- package/dist/services/operate/decision-brief.js +194 -0
- package/dist/services/operate/decision-brief.js.map +1 -0
- package/dist/services/operate/doctor.d.ts.map +1 -1
- package/dist/services/operate/doctor.js +271 -3
- package/dist/services/operate/doctor.js.map +1 -1
- package/dist/services/operate/engine.d.ts +66 -2
- package/dist/services/operate/engine.d.ts.map +1 -1
- package/dist/services/operate/engine.js +219 -7
- package/dist/services/operate/engine.js.map +1 -1
- package/dist/services/operate/event-store.d.ts +35 -1
- package/dist/services/operate/event-store.d.ts.map +1 -1
- package/dist/services/operate/event-store.js +142 -29
- package/dist/services/operate/event-store.js.map +1 -1
- package/dist/services/operate/evidence-cache.d.ts +25 -1
- package/dist/services/operate/evidence-cache.d.ts.map +1 -1
- package/dist/services/operate/evidence-cache.js +62 -1
- package/dist/services/operate/evidence-cache.js.map +1 -1
- package/dist/services/operate/evidence.d.ts +20 -1
- package/dist/services/operate/evidence.d.ts.map +1 -1
- package/dist/services/operate/evidence.js +148 -0
- package/dist/services/operate/evidence.js.map +1 -1
- package/dist/services/operate/index.d.ts +2 -0
- package/dist/services/operate/index.d.ts.map +1 -1
- package/dist/services/operate/index.js +309 -46
- package/dist/services/operate/index.js.map +1 -1
- package/dist/services/operate/interaction/action-service.d.ts +17 -0
- package/dist/services/operate/interaction/action-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/action-service.js +19 -0
- package/dist/services/operate/interaction/action-service.js.map +1 -1
- package/dist/services/operate/interaction/answer-service.d.ts +17 -0
- package/dist/services/operate/interaction/answer-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/answer-service.js +49 -7
- package/dist/services/operate/interaction/answer-service.js.map +1 -1
- package/dist/services/operate/interaction/question-engine.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-engine.js +9 -3
- package/dist/services/operate/interaction/question-engine.js.map +1 -1
- package/dist/services/operate/interaction/question-registry.d.ts +13 -0
- package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-registry.js +116 -26
- package/dist/services/operate/interaction/question-registry.js.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.d.ts +12 -1
- package/dist/services/operate/interaction/terminal-renderer.d.ts.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.js +26 -15
- package/dist/services/operate/interaction/terminal-renderer.js.map +1 -1
- package/dist/services/operate/journal.d.ts.map +1 -1
- package/dist/services/operate/journal.js +11 -5
- package/dist/services/operate/journal.js.map +1 -1
- package/dist/services/operate/lifecycle.d.ts +8 -0
- package/dist/services/operate/lifecycle.d.ts.map +1 -1
- package/dist/services/operate/lifecycle.js +19 -1
- package/dist/services/operate/lifecycle.js.map +1 -1
- package/dist/services/operate/maintenance.d.ts +22 -0
- package/dist/services/operate/maintenance.d.ts.map +1 -1
- package/dist/services/operate/maintenance.js +406 -46
- package/dist/services/operate/maintenance.js.map +1 -1
- package/dist/services/operate/migration.d.ts +68 -0
- package/dist/services/operate/migration.d.ts.map +1 -0
- package/dist/services/operate/migration.js +235 -0
- package/dist/services/operate/migration.js.map +1 -0
- package/dist/services/operate/mission-dispatch.d.ts +193 -0
- package/dist/services/operate/mission-dispatch.d.ts.map +1 -0
- package/dist/services/operate/mission-dispatch.js +444 -0
- package/dist/services/operate/mission-dispatch.js.map +1 -0
- package/dist/services/operate/projection-persistence.d.ts +6 -0
- package/dist/services/operate/projection-persistence.d.ts.map +1 -1
- package/dist/services/operate/projection-persistence.js +87 -11
- package/dist/services/operate/projection-persistence.js.map +1 -1
- package/dist/services/operate/projection.d.ts +26 -1
- package/dist/services/operate/projection.d.ts.map +1 -1
- package/dist/services/operate/projection.js +62 -0
- package/dist/services/operate/projection.js.map +1 -1
- package/dist/services/operate/protocol.d.ts +50 -3
- package/dist/services/operate/protocol.d.ts.map +1 -1
- package/dist/services/operate/protocol.js +57 -5
- package/dist/services/operate/protocol.js.map +1 -1
- package/dist/services/operate/read-only-providers.d.ts +37 -0
- package/dist/services/operate/read-only-providers.d.ts.map +1 -1
- package/dist/services/operate/read-only-providers.js +130 -0
- package/dist/services/operate/read-only-providers.js.map +1 -1
- package/dist/services/operate/reports.d.ts +22 -0
- package/dist/services/operate/reports.d.ts.map +1 -1
- package/dist/services/operate/reports.js +226 -1
- package/dist/services/operate/reports.js.map +1 -1
- package/dist/services/operate/routes.d.ts.map +1 -1
- package/dist/services/operate/routes.js +79 -5
- package/dist/services/operate/routes.js.map +1 -1
- package/dist/services/operate/types.d.ts +139 -4
- package/dist/services/operate/types.d.ts.map +1 -1
- package/dist/services/operate/types.js +13 -0
- package/dist/services/operate/types.js.map +1 -1
- package/dist/services/operate/workspace.d.ts +7 -0
- package/dist/services/operate/workspace.d.ts.map +1 -1
- package/dist/services/operate/workspace.js +14 -5
- package/dist/services/operate/workspace.js.map +1 -1
- package/dist/services/runtime-manager-service.d.ts.map +1 -1
- package/dist/services/runtime-manager-service.js +25 -5
- package/dist/services/runtime-manager-service.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,14 +2,16 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { AIError } from '../../ai/errors.js';
|
|
5
6
|
import { DEFAULT_MODELS } from '../../ai/types.js';
|
|
6
7
|
import { OPENPLANR_VERSION } from '../../utils/package-version.js';
|
|
7
8
|
import { generateJSON, getAIProvider, isAIConfigured } from '../ai-service.js';
|
|
8
9
|
import { loadConfig } from '../config-service.js';
|
|
9
10
|
import { canonicalDigest, canonicalize } from './canonical.js';
|
|
10
|
-
import {
|
|
11
|
+
import { createMissionToolset, MISSION_READ_ONLY_TOOLS, narrowMissionRootsToCeiling, operatingRegistryDispatchMode, operatingRuntimeEnforcesBoundedReadOnly, resolveOperatingDispatchMode, runMissionDispatchFanOut, } from './mission-dispatch.js';
|
|
12
|
+
import { assertOperatingArtifact, loadOperatingMissionApi, loadOperatingProtocol, } from './protocol.js';
|
|
11
13
|
import { prepareAdvisorEvidenceText, sanitizeGeneratedPlainText } from './redaction.js';
|
|
12
|
-
import { OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
|
|
14
|
+
import { OPERATE_MISSION_PROTOCOL_VERSION, OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
|
|
13
15
|
const proposalSchema = z
|
|
14
16
|
.object({
|
|
15
17
|
proposalKey: z.string().regex(/^[A-Za-z0-9._-]+$/),
|
|
@@ -38,6 +40,68 @@ const advisorOutputSchema = z
|
|
|
38
40
|
conflicts: z.array(z.string()),
|
|
39
41
|
})
|
|
40
42
|
.strict();
|
|
43
|
+
// The Protocol v1.3 mission (`operating-advisor-response@1.3.0`) proposal shape:
|
|
44
|
+
// each proposal carries `citations` (repository path / git revision / planr
|
|
45
|
+
// artifact, each bound to the cycle's frozen `pinnedRevision`) INSTEAD of the
|
|
46
|
+
// v1.2 `evidenceRefs`. The pipeline snapshots each citation after the lens
|
|
47
|
+
// returns; OpenPlanr never widens the set. Kept in lockstep with the installed
|
|
48
|
+
// `schemas/v1.3.0/operating-citation.schema.json` so a locally parsed response
|
|
49
|
+
// and the pipeline-validated one cannot drift.
|
|
50
|
+
const missionCitationSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
citationKey: z
|
|
53
|
+
.string()
|
|
54
|
+
.regex(/^[A-Za-z0-9._-]+$/)
|
|
55
|
+
.max(128)
|
|
56
|
+
.optional(),
|
|
57
|
+
repositoryPath: z
|
|
58
|
+
.string()
|
|
59
|
+
.max(1024)
|
|
60
|
+
.regex(/^(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/-]*$/)
|
|
61
|
+
.optional(),
|
|
62
|
+
lineRange: z
|
|
63
|
+
.object({ start: z.number().int().min(1), end: z.number().int().min(1) })
|
|
64
|
+
.strict()
|
|
65
|
+
.optional(),
|
|
66
|
+
gitRevision: z
|
|
67
|
+
.string()
|
|
68
|
+
.regex(/^[a-f0-9]{7,64}$/)
|
|
69
|
+
.optional(),
|
|
70
|
+
planrArtifactId: z
|
|
71
|
+
.string()
|
|
72
|
+
.regex(/^(?:EPIC|FEAT|US|SPEC|TASK|ADR|DEC|FND|GAP|OUT)-[A-Za-z0-9._-]+$/)
|
|
73
|
+
.optional(),
|
|
74
|
+
pinnedRevision: z.string().regex(/^[a-f0-9]{7,64}$/),
|
|
75
|
+
})
|
|
76
|
+
.strict();
|
|
77
|
+
const missionProposalSchema = z
|
|
78
|
+
.object({
|
|
79
|
+
proposalKey: z.string().regex(/^[A-Za-z0-9._-]+$/),
|
|
80
|
+
type: z.enum(['finding', 'decision', 'data-gap', 'merge', 'sequence']),
|
|
81
|
+
title: z.string().min(1),
|
|
82
|
+
problem: z.string().min(1),
|
|
83
|
+
proposal: z.string().min(1),
|
|
84
|
+
impact: z.number().int().min(1).max(5),
|
|
85
|
+
confidence: z.number().int().min(1).max(5),
|
|
86
|
+
ease: z.number().int().min(1).max(5),
|
|
87
|
+
severity: z.enum(['low', 'medium', 'high', 'critical']),
|
|
88
|
+
citations: z.array(missionCitationSchema).min(1).max(50),
|
|
89
|
+
dependsOnProposalKeys: z.array(z.string().regex(/^[A-Za-z0-9._-]+$/)).optional(),
|
|
90
|
+
conflictsWithProposalKeys: z.array(z.string().regex(/^[A-Za-z0-9._-]+$/)).optional(),
|
|
91
|
+
sequenceProposalKeys: z
|
|
92
|
+
.array(z.string().regex(/^[A-Za-z0-9._-]+$/))
|
|
93
|
+
.min(2)
|
|
94
|
+
.optional(),
|
|
95
|
+
})
|
|
96
|
+
.strict();
|
|
97
|
+
const missionAdvisorOutputSchema = z
|
|
98
|
+
.object({
|
|
99
|
+
outcome: z.enum(['proposals', 'quiet']),
|
|
100
|
+
proposals: z.array(missionProposalSchema).max(20),
|
|
101
|
+
gaps: z.array(z.string()),
|
|
102
|
+
conflicts: z.array(z.string()),
|
|
103
|
+
})
|
|
104
|
+
.strict();
|
|
41
105
|
export function advisorResponseContractDetails(brief) {
|
|
42
106
|
const examples = brief.output.jsonSchema?.examples;
|
|
43
107
|
return {
|
|
@@ -282,7 +346,8 @@ export async function createOperatingAdvisorPack(input) {
|
|
|
282
346
|
})),
|
|
283
347
|
}),
|
|
284
348
|
};
|
|
285
|
-
const
|
|
349
|
+
const protocol = await loadOperatingProtocol();
|
|
350
|
+
const roleBrief = protocol.createOperatingAdvisorBrief(input.role.roleId);
|
|
286
351
|
const inputDigest = canonicalDigest({
|
|
287
352
|
cycleId: input.cycleId,
|
|
288
353
|
roleId: input.role.roleId,
|
|
@@ -291,7 +356,7 @@ export async function createOperatingAdvisorPack(input) {
|
|
|
291
356
|
evidenceRefs: roleItems.map((item) => item.id).sort(),
|
|
292
357
|
context,
|
|
293
358
|
});
|
|
294
|
-
|
|
359
|
+
const pack = {
|
|
295
360
|
implementation: 'openplanr-operating-advisor-pack',
|
|
296
361
|
cycleId: input.cycleId,
|
|
297
362
|
roleId: input.role.roleId,
|
|
@@ -300,6 +365,121 @@ export async function createOperatingAdvisorPack(input) {
|
|
|
300
365
|
context,
|
|
301
366
|
inputDigest,
|
|
302
367
|
};
|
|
368
|
+
// FR2: measure the canonicalized v1.2 pack against the role's published
|
|
369
|
+
// `maxInputBytes` and fail closed BEFORE returning it. Redaction quarantines
|
|
370
|
+
// a single oversized excerpt (its 16 KiB per-item gate) but never bounds the
|
|
371
|
+
// AGGREGATE pack, so a role carrying many in-gate excerpts can still exceed
|
|
372
|
+
// its input budget — the field incident shipped a 2,736,185-byte pack against
|
|
373
|
+
// a 393,216-byte role budget with nothing catching it. The pack is never
|
|
374
|
+
// truncated to fit; the role fails closed instead, mirroring the mission
|
|
375
|
+
// budget's `E_OPERATE_MISSION_PACKET_BUDGET` semantics with the existing
|
|
376
|
+
// `E_OPERATE_EVIDENCE_BUDGET` code (no new OperateErrorCode is minted).
|
|
377
|
+
assertOperatingAdvisorPackWithinBudget(pack, resolveOperatingPackBudget(protocol, input.role.roleId));
|
|
378
|
+
return pack;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* A role's v1.2 pack input budget, read from the pipeline's published role
|
|
382
|
+
* registry (the same authoritative source `deriveOperatingMissionBudgets` reads).
|
|
383
|
+
* A registry entry that omits `budgets.maxInputBytes` falls back to the same
|
|
384
|
+
* 256 KiB default the mission-budget derivation uses, so an unpublished budget
|
|
385
|
+
* still fails closed rather than admitting an unbounded pack.
|
|
386
|
+
*/
|
|
387
|
+
function resolveOperatingPackBudget(protocol, roleId) {
|
|
388
|
+
const role = protocol.listOperatingRoles().find((candidate) => candidate.id === roleId);
|
|
389
|
+
const maxInputBytes = role?.budgets?.maxInputBytes;
|
|
390
|
+
return typeof maxInputBytes === 'number' ? maxInputBytes : 262_144;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Measure a canonicalized advisor pack and fail closed when it exceeds the
|
|
394
|
+
* role's v1.2 `maxInputBytes`. Shared by `createOperatingAdvisorPack` (fresh
|
|
395
|
+
* construction) and `operateAdapterLifecycle`'s prepare branch (which also
|
|
396
|
+
* guards packs restored from an on-disk session that may predate this check),
|
|
397
|
+
* so an oversized pack can never reach a provider or native adapter from either
|
|
398
|
+
* call site. Reuses the existing `E_OPERATE_EVIDENCE_BUDGET` code.
|
|
399
|
+
*/
|
|
400
|
+
export function assertOperatingAdvisorPackWithinBudget(pack, maxInputBytes) {
|
|
401
|
+
const actualBytes = Buffer.byteLength(canonicalize(pack), 'utf8');
|
|
402
|
+
if (actualBytes > maxInputBytes) {
|
|
403
|
+
throw new OperateError('E_OPERATE_EVIDENCE_BUDGET', `Advisor pack for role ${pack.roleId} is ${actualBytes} bytes, exceeding its ` +
|
|
404
|
+
`${maxInputBytes}-byte v1.2 pack input budget; the pack is not truncated to fit.`, { roleId: pack.roleId, actualBytes, maxInputBytes });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Derive a role's mission-mode input budget from the pipeline's published pack
|
|
409
|
+
* budget. Mission packets carry only an evidence INDEX (no bodies), so their
|
|
410
|
+
* budget is a single-digit-KiB fraction of the role's v1.2 pack budget, clamped
|
|
411
|
+
* to `[1, 9]` KiB. This DERIVES a new value; it never mutates the frozen v1.2
|
|
412
|
+
* `maxInputBytes`. Enforcing that pack-mode budget is a separate concern handled
|
|
413
|
+
* by `assertOperatingAdvisorPackWithinBudget` at pack construction, not here.
|
|
414
|
+
*/
|
|
415
|
+
export function deriveOperatingMissionBudget(packMaxInputBytes) {
|
|
416
|
+
const kib = Math.min(9, Math.max(1, Math.round(packMaxInputBytes / (32 * 1024))));
|
|
417
|
+
return kib * 1024;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* The per-role mission input budget registry, derived once from the pipeline's
|
|
421
|
+
* role registry. Every value is single-digit KiB.
|
|
422
|
+
*/
|
|
423
|
+
export async function deriveOperatingMissionBudgets() {
|
|
424
|
+
const roles = (await loadOperatingProtocol()).listOperatingRoles();
|
|
425
|
+
return Object.fromEntries(roles.map((role) => {
|
|
426
|
+
const budgets = role.budgets;
|
|
427
|
+
const packMax = typeof budgets?.maxInputBytes === 'number' ? budgets.maxInputBytes : 262_144;
|
|
428
|
+
return [role.id, deriveOperatingMissionBudget(packMax)];
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
function isMissionBudgetError(error) {
|
|
432
|
+
const code = error?.code;
|
|
433
|
+
if (code === 'E_OPERATE_MISSION_PACKET_BUDGET')
|
|
434
|
+
return true;
|
|
435
|
+
return error instanceof Error && error.message.includes('E_OPERATE_MISSION_PACKET_BUDGET');
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Build a digest-bound Protocol v1.3 mission packet (FR1) by calling the
|
|
439
|
+
* pipeline's `createOperatingMissionPacket` with the live non-evidence payload
|
|
440
|
+
* and the index-only (body-free) evidence. The packet is measured against the
|
|
441
|
+
* role's DERIVED single-digit-KiB mission budget; when it would exceed that
|
|
442
|
+
* budget the assembler fails closed with `E_OPERATE_MISSION_PACKET_BUDGET`
|
|
443
|
+
* naming the role, before any dispatch call is made — the packet is never
|
|
444
|
+
* truncated to fit. This is a separate construction path from
|
|
445
|
+
* `createOperatingAdvisorPack`; pack-mode role-filtered body content is
|
|
446
|
+
* unaffected.
|
|
447
|
+
*/
|
|
448
|
+
export async function buildOperatingMissionPacket(input) {
|
|
449
|
+
const mission = await loadOperatingMissionApi();
|
|
450
|
+
const protocol = await loadOperatingProtocol();
|
|
451
|
+
const role = protocol.listOperatingRoles().find((candidate) => candidate.id === input.roleId);
|
|
452
|
+
if (!role) {
|
|
453
|
+
throw new OperateError('E_OPERATE_ADVISOR_FAILED', `Mission packet requested for unknown role ${input.roleId}.`);
|
|
454
|
+
}
|
|
455
|
+
const packBudget = role.budgets?.maxInputBytes;
|
|
456
|
+
const derivedBudget = deriveOperatingMissionBudget(typeof packBudget === 'number' ? packBudget : 262_144);
|
|
457
|
+
let packet;
|
|
458
|
+
try {
|
|
459
|
+
packet = mission.createOperatingMissionPacket(input.roleId, input.evidenceIndex, {
|
|
460
|
+
protocolVersion: OPERATE_MISSION_PROTOCOL_VERSION,
|
|
461
|
+
cycleId: input.cycleId,
|
|
462
|
+
pinnedRevision: input.pinnedRevision,
|
|
463
|
+
charter: input.charter,
|
|
464
|
+
priorCycleSummary: input.priorCycleSummary,
|
|
465
|
+
planningStatus: input.planningStatus,
|
|
466
|
+
declaredRoots: input.declaredRoots,
|
|
467
|
+
maxEvidenceItems: input.maxEvidenceItems,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
if (isMissionBudgetError(error)) {
|
|
472
|
+
throw new OperateError('E_OPERATE_MISSION_PACKET_BUDGET', `Mission packet for role ${input.roleId} exceeds its mission input budget; ` +
|
|
473
|
+
'the evidence index is not truncated to fit.', { roleId: input.roleId });
|
|
474
|
+
}
|
|
475
|
+
throw new OperateError('E_OPERATE_ADVISOR_FAILED', `Mission packet construction failed for role ${input.roleId}: ${error instanceof Error ? error.message : String(error)}`, { roleId: input.roleId });
|
|
476
|
+
}
|
|
477
|
+
const actualBytes = Buffer.byteLength(canonicalize(packet), 'utf8');
|
|
478
|
+
if (actualBytes > derivedBudget) {
|
|
479
|
+
throw new OperateError('E_OPERATE_MISSION_PACKET_BUDGET', `Mission packet for role ${input.roleId} is ${actualBytes} bytes, exceeding its ` +
|
|
480
|
+
`derived ${derivedBudget}-byte mission budget; the evidence index is not truncated to fit.`, { roleId: input.roleId, actualBytes, maxInputBytes: derivedBudget });
|
|
481
|
+
}
|
|
482
|
+
return packet;
|
|
303
483
|
}
|
|
304
484
|
export function operatingAdvisorMessages(input) {
|
|
305
485
|
return [
|
|
@@ -423,6 +603,156 @@ export async function createNativeOperatingRoleResult(input) {
|
|
|
423
603
|
protocol.validateOperatingRoleResultDigest(result);
|
|
424
604
|
return result;
|
|
425
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* Sanitize a v1.3 mission advisor response's free text exactly as the v1.2
|
|
608
|
+
* `sanitizeOutput` does, but PRESERVE each proposal's structured `citations`
|
|
609
|
+
* verbatim: they are schema-pattern-bounded locators (repository path, git
|
|
610
|
+
* revision, or planr artifact — never free prose), which the engine resolves and
|
|
611
|
+
* snapshots after the lens returns. Dropping them would silence every proposal.
|
|
612
|
+
*/
|
|
613
|
+
function sanitizeMissionOutput(output) {
|
|
614
|
+
return {
|
|
615
|
+
outcome: output.outcome,
|
|
616
|
+
proposals: output.proposals
|
|
617
|
+
.map((proposal) => ({
|
|
618
|
+
...proposal,
|
|
619
|
+
title: sanitizeGeneratedPlainText(proposal.title).replace(/\s+/g, ' ').trim(),
|
|
620
|
+
problem: sanitizeGeneratedPlainText(proposal.problem).replace(/\s+/g, ' ').trim(),
|
|
621
|
+
proposal: sanitizeGeneratedPlainText(proposal.proposal).replace(/\s+/g, ' ').trim(),
|
|
622
|
+
citations: [...proposal.citations],
|
|
623
|
+
...(proposal.dependsOnProposalKeys
|
|
624
|
+
? { dependsOnProposalKeys: [...new Set(proposal.dependsOnProposalKeys)].sort() }
|
|
625
|
+
: {}),
|
|
626
|
+
...(proposal.conflictsWithProposalKeys
|
|
627
|
+
? { conflictsWithProposalKeys: [...new Set(proposal.conflictsWithProposalKeys)].sort() }
|
|
628
|
+
: {}),
|
|
629
|
+
...(proposal.sequenceProposalKeys
|
|
630
|
+
? { sequenceProposalKeys: [...proposal.sequenceProposalKeys] }
|
|
631
|
+
: {}),
|
|
632
|
+
}))
|
|
633
|
+
.sort((left, right) => left.proposalKey.localeCompare(right.proposalKey) ||
|
|
634
|
+
left.type.localeCompare(right.type) ||
|
|
635
|
+
left.problem.localeCompare(right.problem)),
|
|
636
|
+
gaps: [...new Set(output.gaps.map(sanitizeGeneratedPlainText))].sort(),
|
|
637
|
+
conflicts: [...new Set(output.conflicts.map(sanitizeGeneratedPlainText))].sort(),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* A mission packet's `role.output` facet mirrors the v1.2 brief's output
|
|
642
|
+
* contract (allowed proposal types, maxima), so a v1.3 response is validated
|
|
643
|
+
* against exactly the same invariants a pack response is — reusing the pipeline's
|
|
644
|
+
* registry-derived brief as the single source of truth.
|
|
645
|
+
*/
|
|
646
|
+
export async function createNativeMissionOperatingRoleResult(input) {
|
|
647
|
+
const protocol = await loadOperatingProtocol();
|
|
648
|
+
// Validate against the INSTALLED v1.3 schema explicitly — the compact response
|
|
649
|
+
// carries no protocol envelope, so the pipeline additively resolves to v1.2
|
|
650
|
+
// unless the version is passed.
|
|
651
|
+
const contractIssues = protocol.validateProtocolArtifact('operating-advisor-response', input.response, { protocolVersion: '1.3.0' });
|
|
652
|
+
if (contractIssues.length > 0) {
|
|
653
|
+
throw new OperateError('E_OPERATE_ADVISOR_FAILED', `Native ${input.packet.roleId} response does not match operating-advisor-response@1.3.0.`, { issues: contractIssues.slice(0, 8) });
|
|
654
|
+
}
|
|
655
|
+
const parsed = missionAdvisorOutputSchema.safeParse(input.response);
|
|
656
|
+
if (!parsed.success) {
|
|
657
|
+
throw new OperateError('E_OPERATE_INTERNAL', 'Protocol and OpenPlanr disagree on the v1.3 mission advisor response contract.', {
|
|
658
|
+
issues: parsed.error.issues.slice(0, 8).map((issue) => ({
|
|
659
|
+
path: issue.path.join('.'),
|
|
660
|
+
code: issue.code,
|
|
661
|
+
})),
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
const output = sanitizeMissionOutput(parsed.data);
|
|
665
|
+
const brief = protocol.createOperatingAdvisorBrief(input.packet.roleId);
|
|
666
|
+
assertAdvisorOutputMatchesBrief(brief, output);
|
|
667
|
+
const capability = (input.packet.role.capabilityTier ??
|
|
668
|
+
brief.role.capabilityTier);
|
|
669
|
+
// The intermediate result: proposals carry their v1.3 citations and an empty
|
|
670
|
+
// evidenceRefs set. It is deliberately NOT yet a v1.2-valid committed
|
|
671
|
+
// operating-role-result — the citation gate mints the evidenceRefs that make
|
|
672
|
+
// it one. `inputDigest` is the packet's digest, so the record path's
|
|
673
|
+
// input-digest binding (prepare stored the same packet digest) holds.
|
|
674
|
+
const intermediate = {
|
|
675
|
+
kind: 'operating-role-result',
|
|
676
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
677
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
678
|
+
cycleId: input.packet.cycleId,
|
|
679
|
+
roleId: input.packet.roleId,
|
|
680
|
+
inputDigest: input.packet.packetDigest,
|
|
681
|
+
resultDigest: input.packet.packetDigest,
|
|
682
|
+
outcome: output.outcome,
|
|
683
|
+
proposals: output.proposals.map((proposal) => ({
|
|
684
|
+
proposalKey: proposal.proposalKey,
|
|
685
|
+
type: proposal.type,
|
|
686
|
+
title: proposal.title,
|
|
687
|
+
problem: proposal.problem,
|
|
688
|
+
proposal: proposal.proposal,
|
|
689
|
+
impact: proposal.impact,
|
|
690
|
+
confidence: proposal.confidence,
|
|
691
|
+
ease: proposal.ease,
|
|
692
|
+
severity: proposal.severity,
|
|
693
|
+
evidenceRefs: [],
|
|
694
|
+
...(proposal.dependsOnProposalKeys
|
|
695
|
+
? { dependsOnProposalKeys: proposal.dependsOnProposalKeys }
|
|
696
|
+
: {}),
|
|
697
|
+
...(proposal.conflictsWithProposalKeys
|
|
698
|
+
? { conflictsWithProposalKeys: proposal.conflictsWithProposalKeys }
|
|
699
|
+
: {}),
|
|
700
|
+
...(proposal.sequenceProposalKeys
|
|
701
|
+
? { sequenceProposalKeys: proposal.sequenceProposalKeys }
|
|
702
|
+
: {}),
|
|
703
|
+
citations: proposal.citations,
|
|
704
|
+
})),
|
|
705
|
+
gaps: output.gaps,
|
|
706
|
+
conflicts: output.conflicts,
|
|
707
|
+
producer: {
|
|
708
|
+
product: 'openplanr',
|
|
709
|
+
version: OPENPLANR_VERSION,
|
|
710
|
+
runtime: input.runtime,
|
|
711
|
+
capability,
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
// A quiet response has no proposals/citations, so it never touches the gate; a
|
|
715
|
+
// proposals response threads its citations through the already-live gate.
|
|
716
|
+
const gated = output.proposals.length > 0
|
|
717
|
+
? await input.resolveCitations([intermediate])
|
|
718
|
+
: { roleResults: [intermediate], gaps: [] };
|
|
719
|
+
const resolved = gated.roleResults[0] ?? intermediate;
|
|
720
|
+
// Finalize into a v1.2-valid committed operating-role-result: strip the
|
|
721
|
+
// now-resolved citations, keep the minted evidenceRefs, and let the surviving
|
|
722
|
+
// proposal count set the honest outcome (an all-unresolvable response commits
|
|
723
|
+
// as quiet, its citations preserved only as the opened gaps).
|
|
724
|
+
const survivingProposals = resolved.proposals
|
|
725
|
+
.map((proposal) => {
|
|
726
|
+
const { citations: _citations, ...rest } = proposal;
|
|
727
|
+
return rest;
|
|
728
|
+
})
|
|
729
|
+
.filter((proposal) => proposal.evidenceRefs.length > 0);
|
|
730
|
+
const unsigned = {
|
|
731
|
+
kind: 'operating-role-result',
|
|
732
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
733
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
734
|
+
cycleId: input.packet.cycleId,
|
|
735
|
+
roleId: input.packet.roleId,
|
|
736
|
+
inputDigest: input.packet.packetDigest,
|
|
737
|
+
outcome: survivingProposals.length > 0 ? 'proposals' : 'quiet',
|
|
738
|
+
proposals: survivingProposals,
|
|
739
|
+
gaps: output.gaps,
|
|
740
|
+
conflicts: output.conflicts,
|
|
741
|
+
producer: {
|
|
742
|
+
product: 'openplanr',
|
|
743
|
+
version: OPENPLANR_VERSION,
|
|
744
|
+
runtime: input.runtime,
|
|
745
|
+
capability,
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
const result = {
|
|
749
|
+
...unsigned,
|
|
750
|
+
resultDigest: protocol.computeOperatingRoleResultDigest(unsigned),
|
|
751
|
+
};
|
|
752
|
+
await assertOperatingArtifact('operating-role-result', result);
|
|
753
|
+
protocol.validateOperatingRoleResultDigest(result);
|
|
754
|
+
return { result, gaps: gated.gaps };
|
|
755
|
+
}
|
|
426
756
|
function safeFailureMessage(error) {
|
|
427
757
|
try {
|
|
428
758
|
return sanitizeGeneratedPlainText(error instanceof Error ? error.message : String(error));
|
|
@@ -491,6 +821,37 @@ class OpenPlanrStructuredAdapter {
|
|
|
491
821
|
})).result;
|
|
492
822
|
}
|
|
493
823
|
}
|
|
824
|
+
/** Redacted error class label for diagnostics — no message or stack, ever. */
|
|
825
|
+
function redactedProviderErrorClass(error) {
|
|
826
|
+
if (error instanceof AIError)
|
|
827
|
+
return `AIError:${error.code}`;
|
|
828
|
+
if (error instanceof Error && typeof error.name === 'string' && error.name.length > 0) {
|
|
829
|
+
return error.name;
|
|
830
|
+
}
|
|
831
|
+
return 'UnknownError';
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Build the actionable remedy for a failed structured-provider bootstrap,
|
|
835
|
+
* preserving the underlying provider guidance (e.g. the `planr config set-key`
|
|
836
|
+
* block an AIError already carries) and always naming the `--offline` escape.
|
|
837
|
+
*/
|
|
838
|
+
function structuredProviderBootstrapRemedy(error, provider) {
|
|
839
|
+
const detail = error instanceof AIError
|
|
840
|
+
? error.userMessage
|
|
841
|
+
: error instanceof Error
|
|
842
|
+
? error.message
|
|
843
|
+
: String(error);
|
|
844
|
+
const trimmed = detail.trim();
|
|
845
|
+
const suffixParts = [];
|
|
846
|
+
if (!/config set-key/.test(trimmed)) {
|
|
847
|
+
suffixParts.push(`Configure a key with \`planr config set-key ${provider ?? '<provider>'}\``);
|
|
848
|
+
}
|
|
849
|
+
if (!/--offline/.test(trimmed)) {
|
|
850
|
+
suffixParts.push('or run the cycle offline with --offline');
|
|
851
|
+
}
|
|
852
|
+
const suffix = suffixParts.length > 0 ? ` ${suffixParts.join(' ')}.` : '';
|
|
853
|
+
return `Structured AI provider bootstrap failed: ${trimmed}${suffix}`;
|
|
854
|
+
}
|
|
494
855
|
export async function createConfiguredStructuredAdapter(projectRoot, options = {}) {
|
|
495
856
|
// `planr operate init` writes .planr/operate/config.json, not the project-wide
|
|
496
857
|
// .planr/config.json that loadConfig requires. A project that ran only the
|
|
@@ -503,13 +864,45 @@ export async function createConfiguredStructuredAdapter(projectRoot, options = {
|
|
|
503
864
|
if (!config || !isAIConfigured(config)) {
|
|
504
865
|
throw new OperateError('E_OPERATE_ADVISOR_FAILED', 'No structured AI provider is configured; use --offline or configure OpenPlanr AI.');
|
|
505
866
|
}
|
|
506
|
-
|
|
867
|
+
// A named provider whose key cannot be resolved in this (possibly sandboxed)
|
|
868
|
+
// subprocess environment makes getAIProvider throw a raw AIError. Left
|
|
869
|
+
// unguarded it reaches index.ts's failure() as E_OPERATE_INTERNAL — the exact
|
|
870
|
+
// masked crash the audit reproduced. Convert any provider-bootstrap failure
|
|
871
|
+
// into a typed E_OPERATE_ADVISOR_FAILED that preserves the actionable remedy
|
|
872
|
+
// (`planr config set-key …` / `--offline`) and records a redacted error class.
|
|
873
|
+
let provider;
|
|
874
|
+
try {
|
|
875
|
+
provider = await getAIProvider(config);
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
throw new OperateError('E_OPERATE_ADVISOR_FAILED', structuredProviderBootstrapRemedy(error, config.ai?.provider), { errorClass: redactedProviderErrorClass(error) });
|
|
879
|
+
}
|
|
880
|
+
return new OpenPlanrStructuredAdapter(provider, config.ai?.provider ?? 'ai', options.quiet ?? false);
|
|
507
881
|
}
|
|
508
882
|
export async function dispatchOperatingAdvisors(input) {
|
|
509
883
|
assertAdvisorIsolation(input.adapter);
|
|
510
884
|
const roleRegistry = (await loadOperatingProtocol()).listOperatingRoles();
|
|
511
885
|
const protocol = await loadOperatingProtocol();
|
|
512
886
|
const capabilityByRole = new Map(roleRegistry.map((role) => [role.id, role.capabilityTier ?? 'analysis-high']));
|
|
887
|
+
// Canonical registry order so results and provenance are byte-identical across
|
|
888
|
+
// parallel/sequential dispatch and across the order roles arrive in (FR4).
|
|
889
|
+
const roleOrder = new Map(roleRegistry.map((role, index) => [role.id, index]));
|
|
890
|
+
const registryDefaultMode = new Map(roleRegistry.map((role) => [role.id, operatingRegistryDispatchMode(role)]));
|
|
891
|
+
// The runtime's ability to enforce the bounded read-only boundary is resolved
|
|
892
|
+
// once per dispatch and fails closed: a runtime whose isolation cannot be
|
|
893
|
+
// verified never receives a native lens (FR2).
|
|
894
|
+
const runtimeEnforcesBoundedReadOnly = await operatingRuntimeEnforcesBoundedReadOnly(input.runtime);
|
|
895
|
+
// A structured adapter can never host a native lens; only a native-isolated
|
|
896
|
+
// adapter (whose isolation `assertAdvisorIsolation` has already proven to be
|
|
897
|
+
// enforced) is native-capable.
|
|
898
|
+
const adapterNativeCapable = input.adapter.mode === 'native-isolated';
|
|
899
|
+
const resolveMode = (roleId) => resolveOperatingDispatchMode({
|
|
900
|
+
roleId,
|
|
901
|
+
registryDefault: registryDefaultMode.get(roleId) ?? 'mission',
|
|
902
|
+
override: input.dispatchModeOverrides?.[roleId],
|
|
903
|
+
runtimeEnforcesBoundedReadOnly,
|
|
904
|
+
adapterNativeCapable,
|
|
905
|
+
});
|
|
513
906
|
const skipped = [];
|
|
514
907
|
const runnable = [];
|
|
515
908
|
for (const role of input.readiness.roles) {
|
|
@@ -524,6 +917,18 @@ export async function dispatchOperatingAdvisors(input) {
|
|
|
524
917
|
runnable.push(role);
|
|
525
918
|
}
|
|
526
919
|
async function dispatchRole(role) {
|
|
920
|
+
// Resolve THIS role's dispatch mode once and derive provenance from what is
|
|
921
|
+
// actually dispatched below — never re-derived after the fact. A role that
|
|
922
|
+
// resolves to a native bounded lens has its read-only tool grant enforced
|
|
923
|
+
// before the lens runs (below); every other role fails closed to the pack
|
|
924
|
+
// path, so `isolation` can only read `enforced-read-only-bounded` when the
|
|
925
|
+
// bounded grant was genuinely enforced, never as a bare label over a pack.
|
|
926
|
+
const resolution = resolveMode(role.roleId);
|
|
927
|
+
const dispatch = {
|
|
928
|
+
dispatchMode: resolution.mode,
|
|
929
|
+
isolation: resolution.isolation,
|
|
930
|
+
reconciliation: resolution.reconciliation,
|
|
931
|
+
};
|
|
527
932
|
let pack;
|
|
528
933
|
try {
|
|
529
934
|
pack = await createOperatingAdvisorPack({
|
|
@@ -539,8 +944,28 @@ export async function dispatchOperatingAdvisors(input) {
|
|
|
539
944
|
roleId: role.roleId,
|
|
540
945
|
message: safeFailureMessage(error),
|
|
541
946
|
modelCalls: 0,
|
|
947
|
+
dispatch,
|
|
542
948
|
};
|
|
543
949
|
}
|
|
950
|
+
if (resolution.native) {
|
|
951
|
+
// Route through mission-dispatch.ts's granted-tool-set enforcement: the
|
|
952
|
+
// native lens is confined to the bounded read-only toolset over its
|
|
953
|
+
// sensitivity-ceiling-narrowed declared roots. Constructing the toolset is
|
|
954
|
+
// what makes `enforced-read-only-bounded` true; a callable outside the
|
|
955
|
+
// read-only grant simply does not exist on the surface it hands the lens.
|
|
956
|
+
const ceiling = pack.roleBrief.evidence.sensitivityCeiling;
|
|
957
|
+
const declaredRoots = [
|
|
958
|
+
...new Set(pack.evidence.items
|
|
959
|
+
.map((item) => item.location.split('/')[0])
|
|
960
|
+
.filter((segment) => Boolean(segment))),
|
|
961
|
+
].sort();
|
|
962
|
+
const roots = narrowMissionRootsToCeiling({ declaredRoots, evidenceIndex: [], ceiling });
|
|
963
|
+
const toolset = createMissionToolset({ roots, ceiling });
|
|
964
|
+
const grantedTools = Object.keys(toolset);
|
|
965
|
+
if (grantedTools.some((tool) => !MISSION_READ_ONLY_TOOLS.includes(tool))) {
|
|
966
|
+
throw new OperateError('E_OPERATE_PROVIDER_READ_ONLY', `Mission dispatch for ${role.roleId} assembled a tool outside the bounded read-only grant.`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
544
969
|
const permittedEvidenceRefs = new Set(role.evidenceRefs);
|
|
545
970
|
let output;
|
|
546
971
|
let lastError;
|
|
@@ -571,6 +996,7 @@ export async function dispatchOperatingAdvisors(input) {
|
|
|
571
996
|
roleId: role.roleId,
|
|
572
997
|
message: safeFailureMessage(lastError),
|
|
573
998
|
modelCalls: roleModelCalls,
|
|
999
|
+
dispatch,
|
|
574
1000
|
};
|
|
575
1001
|
}
|
|
576
1002
|
const unsigned = {
|
|
@@ -597,27 +1023,53 @@ export async function dispatchOperatingAdvisors(input) {
|
|
|
597
1023
|
};
|
|
598
1024
|
await assertOperatingArtifact('operating-role-result', result);
|
|
599
1025
|
protocol.validateOperatingRoleResultDigest(result);
|
|
600
|
-
return { ok: true, result, modelCalls: roleModelCalls };
|
|
1026
|
+
return { ok: true, result, modelCalls: roleModelCalls, dispatch };
|
|
1027
|
+
}
|
|
1028
|
+
// Fan the per-role dispatch out in parallel where the adapter reports it,
|
|
1029
|
+
// sequentially otherwise. The orchestrator returns results in `runnable` order
|
|
1030
|
+
// regardless of dispatch style, and results are sorted into canonical registry
|
|
1031
|
+
// order below so the reduced events are byte-identical across parallel and
|
|
1032
|
+
// sequential dispatch and across dispatch order (FR4/E-004).
|
|
1033
|
+
const dispatched = await runMissionDispatchFanOut({
|
|
1034
|
+
items: runnable,
|
|
1035
|
+
parallel: Boolean(input.adapter.parallelDispatch),
|
|
1036
|
+
run: (role) => dispatchRole(role),
|
|
1037
|
+
});
|
|
1038
|
+
// The per-role dispatch descriptor captured inside `dispatchRole` — provenance
|
|
1039
|
+
// reads it rather than re-resolving, so it can only report the isolation the
|
|
1040
|
+
// role was actually dispatched under.
|
|
1041
|
+
const dispatchByRole = new Map();
|
|
1042
|
+
for (const entry of dispatched) {
|
|
1043
|
+
dispatchByRole.set(entry.ok ? entry.result.roleId : entry.roleId, entry.dispatch);
|
|
601
1044
|
}
|
|
602
|
-
const dispatched = input.adapter.parallelDispatch
|
|
603
|
-
? await Promise.all(runnable.map((role) => dispatchRole(role)))
|
|
604
|
-
: await runnable.reduce(async (pending, role) => [...(await pending), await dispatchRole(role)], Promise.resolve([]));
|
|
605
1045
|
const results = dispatched
|
|
606
1046
|
.filter((entry) => entry.ok)
|
|
607
|
-
.map((entry) => entry.result)
|
|
1047
|
+
.map((entry) => entry.result)
|
|
1048
|
+
.sort((left, right) => (roleOrder.get(left.roleId) ?? Number.MAX_SAFE_INTEGER) -
|
|
1049
|
+
(roleOrder.get(right.roleId) ?? Number.MAX_SAFE_INTEGER));
|
|
608
1050
|
const failed = dispatched
|
|
609
1051
|
.filter((entry) => !entry.ok)
|
|
610
1052
|
.map(({ roleId, message }) => ({ roleId, message }));
|
|
611
1053
|
const modelCalls = dispatched.reduce((total, entry) => total + entry.modelCalls, 0);
|
|
612
1054
|
return {
|
|
613
1055
|
results,
|
|
614
|
-
provenance: results.map((result) =>
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
1056
|
+
provenance: results.map((result) => {
|
|
1057
|
+
const dispatchProvenance = dispatchByRole.get(result.roleId) ?? {
|
|
1058
|
+
dispatchMode: resolveMode(result.roleId).mode,
|
|
1059
|
+
isolation: resolveMode(result.roleId).isolation,
|
|
1060
|
+
reconciliation: resolveMode(result.roleId).reconciliation,
|
|
1061
|
+
};
|
|
1062
|
+
return {
|
|
1063
|
+
roleId: result.roleId,
|
|
1064
|
+
runtime: input.runtime ?? input.adapter.id,
|
|
1065
|
+
adapterId: input.adapter.id,
|
|
1066
|
+
capability: input.adapter.capability,
|
|
1067
|
+
dispatch: input.adapter.parallelDispatch ? 'parallel' : 'sequential',
|
|
1068
|
+
dispatchMode: dispatchProvenance.dispatchMode,
|
|
1069
|
+
isolation: dispatchProvenance.isolation,
|
|
1070
|
+
reconciliation: dispatchProvenance.reconciliation,
|
|
1071
|
+
};
|
|
1072
|
+
}),
|
|
621
1073
|
skipped,
|
|
622
1074
|
failed,
|
|
623
1075
|
blocked: failed.some((entry) => entry.roleId === 'chair') ||
|