openplanr 1.19.0 → 1.20.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/backlog.d.ts.map +1 -1
- package/dist/cli/commands/backlog.js +3 -0
- package/dist/cli/commands/backlog.js.map +1 -1
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/config.js +3 -0
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/epic.d.ts.map +1 -1
- package/dist/cli/commands/epic.js +2 -0
- package/dist/cli/commands/epic.js.map +1 -1
- package/dist/cli/commands/feature.d.ts.map +1 -1
- package/dist/cli/commands/feature.js +2 -0
- package/dist/cli/commands/feature.js.map +1 -1
- package/dist/cli/commands/operate.d.ts +5 -2
- package/dist/cli/commands/operate.d.ts.map +1 -1
- package/dist/cli/commands/operate.js +10 -101
- package/dist/cli/commands/operate.js.map +1 -1
- package/dist/cli/commands/plan.d.ts.map +1 -1
- package/dist/cli/commands/plan.js +2 -0
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/quick.d.ts.map +1 -1
- package/dist/cli/commands/quick.js +2 -0
- package/dist/cli/commands/quick.js.map +1 -1
- package/dist/cli/commands/refine.d.ts.map +1 -1
- package/dist/cli/commands/refine.js +2 -0
- package/dist/cli/commands/refine.js.map +1 -1
- package/dist/cli/commands/revise.d.ts.map +1 -1
- package/dist/cli/commands/revise.js +2 -0
- package/dist/cli/commands/revise.js.map +1 -1
- package/dist/cli/commands/spec.d.ts.map +1 -1
- package/dist/cli/commands/spec.js +2 -0
- package/dist/cli/commands/spec.js.map +1 -1
- package/dist/cli/commands/sprint.d.ts.map +1 -1
- package/dist/cli/commands/sprint.js +2 -0
- package/dist/cli/commands/sprint.js.map +1 -1
- package/dist/cli/commands/story.d.ts.map +1 -1
- package/dist/cli/commands/story.js +3 -0
- package/dist/cli/commands/story.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +3 -0
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/services/ai-service.d.ts +3 -5
- package/dist/services/ai-service.d.ts.map +1 -1
- package/dist/services/ai-service.js +7 -1
- package/dist/services/ai-service.js.map +1 -1
- package/dist/services/credentials-service.d.ts.map +1 -1
- package/dist/services/credentials-service.js +2 -0
- package/dist/services/credentials-service.js.map +1 -1
- package/dist/services/deprecation-notices.d.ts +5 -0
- package/dist/services/deprecation-notices.d.ts.map +1 -0
- package/dist/services/deprecation-notices.js +6 -0
- package/dist/services/deprecation-notices.js.map +1 -0
- package/dist/services/operate/advisors.d.ts +144 -166
- package/dist/services/operate/advisors.d.ts.map +1 -1
- package/dist/services/operate/advisors.js +178 -543
- package/dist/services/operate/advisors.js.map +1 -1
- package/dist/services/operate/citation-resolution.d.ts.map +1 -1
- package/dist/services/operate/citation-resolution.js +50 -1
- package/dist/services/operate/citation-resolution.js.map +1 -1
- package/dist/services/operate/config.d.ts +18 -52
- package/dist/services/operate/config.d.ts.map +1 -1
- package/dist/services/operate/config.js +24 -206
- package/dist/services/operate/config.js.map +1 -1
- package/dist/services/operate/doctor.d.ts +15 -0
- package/dist/services/operate/doctor.d.ts.map +1 -1
- package/dist/services/operate/doctor.js +142 -32
- package/dist/services/operate/doctor.js.map +1 -1
- package/dist/services/operate/engine.d.ts +22 -78
- package/dist/services/operate/engine.d.ts.map +1 -1
- package/dist/services/operate/engine.js +269 -202
- package/dist/services/operate/engine.js.map +1 -1
- package/dist/services/operate/evidence.d.ts +9 -107
- package/dist/services/operate/evidence.d.ts.map +1 -1
- package/dist/services/operate/evidence.js +0 -1403
- package/dist/services/operate/evidence.js.map +1 -1
- package/dist/services/operate/index.d.ts.map +1 -1
- package/dist/services/operate/index.js +10 -231
- package/dist/services/operate/index.js.map +1 -1
- package/dist/services/operate/integrity.d.ts +81 -0
- package/dist/services/operate/integrity.d.ts.map +1 -0
- package/dist/services/operate/integrity.js +152 -0
- package/dist/services/operate/integrity.js.map +1 -0
- package/dist/services/operate/interaction/answer-service.d.ts +0 -10
- package/dist/services/operate/interaction/answer-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/answer-service.js +63 -43
- 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 +105 -21
- package/dist/services/operate/interaction/question-engine.js.map +1 -1
- package/dist/services/operate/interaction/question-registry.d.ts +16 -1
- package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-registry.js +45 -39
- package/dist/services/operate/interaction/question-registry.js.map +1 -1
- package/dist/services/operate/interaction/session-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/session-service.js +16 -11
- package/dist/services/operate/interaction/session-service.js.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.d.ts.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.js +16 -6
- package/dist/services/operate/interaction/terminal-renderer.js.map +1 -1
- package/dist/services/operate/maintenance.d.ts +1 -11
- package/dist/services/operate/maintenance.d.ts.map +1 -1
- package/dist/services/operate/maintenance.js +94 -342
- package/dist/services/operate/maintenance.js.map +1 -1
- package/dist/services/operate/mission-dispatch.d.ts +73 -55
- package/dist/services/operate/mission-dispatch.d.ts.map +1 -1
- package/dist/services/operate/mission-dispatch.js +91 -67
- package/dist/services/operate/mission-dispatch.js.map +1 -1
- package/dist/services/operate/projection-persistence.d.ts.map +1 -1
- package/dist/services/operate/projection-persistence.js +22 -2
- package/dist/services/operate/projection-persistence.js.map +1 -1
- package/dist/services/operate/projection.d.ts.map +1 -1
- package/dist/services/operate/projection.js +10 -3
- package/dist/services/operate/projection.js.map +1 -1
- package/dist/services/operate/protocol.d.ts +1 -41
- package/dist/services/operate/protocol.d.ts.map +1 -1
- package/dist/services/operate/protocol.js +7 -45
- package/dist/services/operate/protocol.js.map +1 -1
- package/dist/services/operate/redaction.d.ts +3 -3
- package/dist/services/operate/redaction.js +4 -4
- package/dist/services/operate/redaction.js.map +1 -1
- package/dist/services/operate/reports.d.ts +11 -1
- package/dist/services/operate/reports.d.ts.map +1 -1
- package/dist/services/operate/reports.js +88 -3
- package/dist/services/operate/reports.js.map +1 -1
- package/dist/services/operate/types.d.ts +42 -128
- package/dist/services/operate/types.d.ts.map +1 -1
- package/dist/services/operate/types.js.map +1 -1
- package/dist/services/operate/workspace.d.ts +0 -1
- package/dist/services/operate/workspace.d.ts.map +1 -1
- package/dist/services/operate/workspace.js +0 -2
- package/dist/services/operate/workspace.js.map +1 -1
- package/dist/services/runtime-manager-service.js +2 -2
- package/dist/services/runtime-manager-service.js.map +1 -1
- package/package.json +3 -3
- package/dist/services/operate/evidence-classifications.d.ts +0 -59
- package/dist/services/operate/evidence-classifications.d.ts.map +0 -1
- package/dist/services/operate/evidence-classifications.js +0 -132
- package/dist/services/operate/evidence-classifications.js.map +0 -1
- package/dist/services/operate/evidence-diagnostics.d.ts +0 -23
- package/dist/services/operate/evidence-diagnostics.d.ts.map +0 -1
- package/dist/services/operate/evidence-diagnostics.js +0 -152
- package/dist/services/operate/evidence-diagnostics.js.map +0 -1
- package/dist/services/operate/evidence-readiness.d.ts +0 -18
- package/dist/services/operate/evidence-readiness.d.ts.map +0 -1
- package/dist/services/operate/evidence-readiness.js +0 -94
- package/dist/services/operate/evidence-readiness.js.map +0 -1
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
import { AIError } from '../../ai/errors.js';
|
|
6
7
|
import { DEFAULT_MODELS } from '../../ai/types.js';
|
|
7
8
|
import { OPENPLANR_VERSION } from '../../utils/package-version.js';
|
|
8
|
-
import {
|
|
9
|
+
import { getAIProvider, isAIConfigured } from '../ai-service.js';
|
|
9
10
|
import { loadConfig } from '../config-service.js';
|
|
10
11
|
import { canonicalDigest, canonicalize } from './canonical.js';
|
|
11
|
-
import { createMissionToolset, MISSION_READ_ONLY_TOOLS, narrowMissionRootsToCeiling,
|
|
12
|
-
import { assertOperatingArtifact,
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { createMissionToolset, MISSION_READ_ONLY_TOOLS, narrowMissionRootsToCeiling, operatingRuntimeEnforcesBoundedReadOnly, resolveOperatingDispatchIsolation, runMissionDispatchFanOut, } from './mission-dispatch.js';
|
|
13
|
+
import { assertOperatingArtifact, loadOperatingProtocol, resolveOperatingPipelineRoot, } from './protocol.js';
|
|
14
|
+
import { sanitizeGeneratedPlainText } from './redaction.js';
|
|
15
|
+
import { OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
|
|
15
16
|
const proposalSchema = z
|
|
16
17
|
.object({
|
|
17
18
|
proposalKey: z.string().regex(/^[A-Za-z0-9._-]+$/),
|
|
@@ -240,10 +241,7 @@ function openItem(record, summaryKeys) {
|
|
|
240
241
|
: {}),
|
|
241
242
|
};
|
|
242
243
|
}
|
|
243
|
-
/**
|
|
244
|
-
* Build the immutable non-advisor context shared by all independent lenses.
|
|
245
|
-
* Role filtering is applied later, after the evidence permission set is known.
|
|
246
|
-
*/
|
|
244
|
+
/** Build the immutable non-advisor context shared by all independent lenses. */
|
|
247
245
|
export async function buildAdvisorOperatingContext(input) {
|
|
248
246
|
const charter = parseOperatingCharter(await readFile(input.charterPath, 'utf8'));
|
|
249
247
|
const prior = [...input.state.cycles]
|
|
@@ -273,13 +271,7 @@ export async function buildAdvisorOperatingContext(input) {
|
|
|
273
271
|
.filter((outcome) => ['pending', 'observing', 'inconclusive'].includes(outcome.status))
|
|
274
272
|
.map((outcome) => openItem(outcome, ['metric', 'queryIdentity']))
|
|
275
273
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
276
|
-
const unsigned = {
|
|
277
|
-
charter,
|
|
278
|
-
priorCycle,
|
|
279
|
-
openDecisions,
|
|
280
|
-
openGaps,
|
|
281
|
-
pendingOutcomes,
|
|
282
|
-
};
|
|
274
|
+
const unsigned = { charter, priorCycle, openDecisions, openGaps, pendingOutcomes };
|
|
283
275
|
return { ...unsigned, snapshotDigest: canonicalDigest(unsigned) };
|
|
284
276
|
}
|
|
285
277
|
function roleContext(context, roleId, permittedEvidenceRefs) {
|
|
@@ -300,411 +292,70 @@ function roleContext(context, roleId, permittedEvidenceRefs) {
|
|
|
300
292
|
openGaps: context.openGaps.filter(visible),
|
|
301
293
|
pendingOutcomes: context.pendingOutcomes.filter(visible),
|
|
302
294
|
};
|
|
303
|
-
return {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}));
|
|
320
|
-
const quarantined = preparedItems.filter(({ prepared }) => prepared.quarantined);
|
|
321
|
-
if (quarantined.length > 0) {
|
|
322
|
-
throw new OperateError('E_OPERATE_EVIDENCE_REJECTED', 'Evidence quarantined before advisor dispatch: ' +
|
|
323
|
-
quarantined
|
|
324
|
-
.map(({ item, prepared }) => `${item.id} (${prepared.reason ?? 'unsafe untrusted content'})`)
|
|
325
|
-
.sort()
|
|
326
|
-
.join('; '));
|
|
327
|
-
}
|
|
328
|
-
const roleItems = preparedItems.map(({ item, prepared }) => ({
|
|
329
|
-
...item,
|
|
330
|
-
summary: prepared.value,
|
|
331
|
-
}));
|
|
332
|
-
const roleSources = input.evidence.sources.filter((source) => roleItems.some((item) => item.source === source.id));
|
|
333
|
-
const context = roleContext(input.context, input.role.roleId, permittedEvidenceRefs);
|
|
334
|
-
const evidence = {
|
|
335
|
-
...input.evidence,
|
|
336
|
-
items: roleItems,
|
|
337
|
-
sources: roleSources,
|
|
338
|
-
fingerprint: canonicalDigest({
|
|
339
|
-
roleId: input.role.roleId,
|
|
340
|
-
sourceFingerprint: input.evidence.fingerprint,
|
|
341
|
-
evidenceRefs: roleItems.map((item) => item.id).sort(),
|
|
342
|
-
requirements: input.role.requirements,
|
|
343
|
-
framedEvidence: roleItems.map((item) => ({
|
|
344
|
-
id: item.id,
|
|
345
|
-
summary: item.summary,
|
|
346
|
-
})),
|
|
347
|
-
}),
|
|
348
|
-
};
|
|
349
|
-
const protocol = await loadOperatingProtocol();
|
|
350
|
-
const roleBrief = protocol.createOperatingAdvisorBrief(input.role.roleId);
|
|
351
|
-
const inputDigest = canonicalDigest({
|
|
352
|
-
cycleId: input.cycleId,
|
|
353
|
-
roleId: input.role.roleId,
|
|
354
|
-
roleBriefDigest: roleBrief.briefDigest,
|
|
355
|
-
evidenceFingerprint: evidence.fingerprint,
|
|
356
|
-
evidenceRefs: roleItems.map((item) => item.id).sort(),
|
|
357
|
-
context,
|
|
358
|
-
});
|
|
359
|
-
const pack = {
|
|
360
|
-
implementation: 'openplanr-operating-advisor-pack',
|
|
361
|
-
cycleId: input.cycleId,
|
|
362
|
-
roleId: input.role.roleId,
|
|
363
|
-
roleBrief,
|
|
364
|
-
evidence,
|
|
365
|
-
context,
|
|
366
|
-
inputDigest,
|
|
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 fraction of the role's v1.2 pack budget — `packMaxInputBytes / 32
|
|
411
|
-
* KiB`, rounded — clamped to the registry's real spread `[1, 32]` KiB.
|
|
412
|
-
*
|
|
413
|
-
* The prior `[1, 9]` KiB ceiling was arbitrary: it capped every repository-reading
|
|
414
|
-
* lens (512 KiB → 16 KiB) and `technology-risk` (640 KiB → 20 KiB) down to 9 KiB.
|
|
415
|
-
* At ~300 canonical bytes per index item that admitted only ~25 items, so a real
|
|
416
|
-
* monorepo's packet overflowed the derived budget and mission dispatch failed
|
|
417
|
-
* closed on a healthy repository. The ceiling now tracks the schema's own
|
|
418
|
-
* `maxInputBytes` maximum (1 MiB → 32 KiB), so a large registry budget maps to a
|
|
419
|
-
* proportional mission budget instead of an arbitrary cut. This DERIVES a new
|
|
420
|
-
* value; it never mutates the frozen v1.2 `maxInputBytes`. Enforcing that
|
|
421
|
-
* pack-mode budget is a separate concern handled by
|
|
422
|
-
* `assertOperatingAdvisorPackWithinBudget` at pack construction, not here.
|
|
423
|
-
*/
|
|
424
|
-
export function deriveOperatingMissionBudget(packMaxInputBytes) {
|
|
425
|
-
const kib = Math.min(32, Math.max(1, Math.round(packMaxInputBytes / (32 * 1024))));
|
|
426
|
-
return kib * 1024;
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Conservative upper bound on one evidence index item's canonical JSON size. A
|
|
430
|
-
* v1.3 index item is a fixed-shape pointer (id, path/revision, contentHash,
|
|
431
|
-
* source, classification, freshness, sensitivity, signals[]); measured against
|
|
432
|
-
* real indexes it lands near ~300 bytes even with a long repository path. Using a
|
|
433
|
-
* value at the top of that range as the divisor guarantees the derived item cap
|
|
434
|
-
* keeps a truncated packet within budget rather than merely on average.
|
|
435
|
-
*/
|
|
436
|
-
const MISSION_EVIDENCE_INDEX_ITEM_BYTES = 320;
|
|
437
|
-
/**
|
|
438
|
-
* The non-evidence portion of a mission packet (charter, prior-cycle summary,
|
|
439
|
-
* planning status, role brief, tool grant, envelope) reserved out of the derived
|
|
440
|
-
* byte budget before the remainder is divided into index slots. Sized to roughly
|
|
441
|
-
* twice the measured empty-packet overhead (~1.9 KiB) so a moderately large
|
|
442
|
-
* charter/summary still leaves the derived cap safely inside budget.
|
|
443
|
-
*/
|
|
444
|
-
const MISSION_PACKET_OVERHEAD_RESERVE_BYTES = 4096;
|
|
445
|
-
/**
|
|
446
|
-
* Derive how many evidence index items a role's mission packet can carry before
|
|
447
|
-
* the published pipeline must truncate (the `maxEvidenceItems` the assembler
|
|
448
|
-
* enforces). The cap is the number of `MISSION_EVIDENCE_INDEX_ITEM_BYTES`-sized
|
|
449
|
-
* slots that fit in the role's derived byte budget after reserving
|
|
450
|
-
* `MISSION_PACKET_OVERHEAD_RESERVE_BYTES` for the non-evidence payload,
|
|
451
|
-
* intersected with any caller-supplied upper bound (`config.budgets.maxItems`).
|
|
452
|
-
*
|
|
453
|
-
* This is the "per-role registry-sized default": `technology-risk`'s 640-KiB
|
|
454
|
-
* pack budget yields a larger cap than a 512-KiB lens, and the chair's 192-KiB
|
|
455
|
-
* budget a smaller one — instead of one flat number that starves large-budget
|
|
456
|
-
* roles. Because `slots * itemBytes + reserve <= derivedBudget` holds by
|
|
457
|
-
* construction and the real per-item/overhead costs sit under those bounds, a
|
|
458
|
-
* packet truncated to this cap fits the derived budget with margin.
|
|
459
|
-
*/
|
|
460
|
-
export function deriveOperatingMissionEvidenceCap(packMaxInputBytes, upperBound) {
|
|
461
|
-
const derivedBudget = deriveOperatingMissionBudget(packMaxInputBytes);
|
|
462
|
-
const indexBudget = Math.max(0, derivedBudget - MISSION_PACKET_OVERHEAD_RESERVE_BYTES);
|
|
463
|
-
const registrySizedDefault = Math.max(1, Math.floor(indexBudget / MISSION_EVIDENCE_INDEX_ITEM_BYTES));
|
|
464
|
-
return typeof upperBound === 'number' && upperBound > 0
|
|
465
|
-
? Math.min(upperBound, registrySizedDefault)
|
|
466
|
-
: registrySizedDefault;
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* The per-role mission evidence-item cap registry, derived once from the
|
|
470
|
-
* pipeline's role registry. Mirrors `deriveOperatingMissionBudgets` for the
|
|
471
|
-
* `maxEvidenceItems` dimension so `buildOperatingMissionPackets` can pass each
|
|
472
|
-
* role its own byte-fitting cap instead of one flat value.
|
|
473
|
-
*/
|
|
474
|
-
export async function deriveOperatingMissionEvidenceCaps(upperBound) {
|
|
475
|
-
const roles = (await loadOperatingProtocol()).listOperatingRoles();
|
|
476
|
-
return Object.fromEntries(roles.map((role) => {
|
|
477
|
-
const budgets = role.budgets;
|
|
478
|
-
const packMax = typeof budgets?.maxInputBytes === 'number' ? budgets.maxInputBytes : 262_144;
|
|
479
|
-
return [role.id, deriveOperatingMissionEvidenceCap(packMax, upperBound)];
|
|
480
|
-
}));
|
|
481
|
-
}
|
|
482
|
-
/**
|
|
483
|
-
* The per-role mission input budget registry, derived once from the pipeline's
|
|
484
|
-
* role registry. Every value is single-digit KiB.
|
|
485
|
-
*/
|
|
486
|
-
export async function deriveOperatingMissionBudgets() {
|
|
487
|
-
const roles = (await loadOperatingProtocol()).listOperatingRoles();
|
|
488
|
-
return Object.fromEntries(roles.map((role) => {
|
|
489
|
-
const budgets = role.budgets;
|
|
490
|
-
const packMax = typeof budgets?.maxInputBytes === 'number' ? budgets.maxInputBytes : 262_144;
|
|
491
|
-
return [role.id, deriveOperatingMissionBudget(packMax)];
|
|
492
|
-
}));
|
|
493
|
-
}
|
|
494
|
-
function isMissionBudgetError(error) {
|
|
495
|
-
const code = error?.code;
|
|
496
|
-
if (code === 'E_OPERATE_MISSION_PACKET_BUDGET')
|
|
497
|
-
return true;
|
|
498
|
-
return error instanceof Error && error.message.includes('E_OPERATE_MISSION_PACKET_BUDGET');
|
|
295
|
+
return { ...filtered, snapshotDigest: canonicalDigest(filtered) };
|
|
296
|
+
}
|
|
297
|
+
let cachedMandateApi = null;
|
|
298
|
+
async function loadOperatingMandateApi() {
|
|
299
|
+
cachedMandateApi ??= (async () => {
|
|
300
|
+
const root = resolveOperatingPipelineRoot({ requireMission: true });
|
|
301
|
+
if (!root) {
|
|
302
|
+
throw new OperateError('E_OPERATE_MISSION_UNAVAILABLE', 'Mandate dispatch requires the pipeline package with Protocol v1.3 (operating mandate).');
|
|
303
|
+
}
|
|
304
|
+
const loaded = (await import(pathToFileURL(path.join(root, 'lib', 'operate', 'mandate.mjs')).href));
|
|
305
|
+
if (typeof loaded.createOperatingMandate !== 'function') {
|
|
306
|
+
throw new OperateError('E_PIPELINE_VERSION_INCOMPATIBLE', 'Installed planr-pipeline does not export the Protocol v1.3 operating mandate builder.');
|
|
307
|
+
}
|
|
308
|
+
return loaded;
|
|
309
|
+
})();
|
|
310
|
+
return cachedMandateApi;
|
|
499
311
|
}
|
|
500
312
|
/**
|
|
501
|
-
*
|
|
502
|
-
*
|
|
313
|
+
* Build the Protocol v1.3 operating mandate for a role (FR1). The mandate's
|
|
314
|
+
* boundaries are declared directly from the caller's granted workspace roots —
|
|
315
|
+
* never an evidence-index-derived subset — so a gitignored `.planr/` tree is
|
|
316
|
+
* fully readable when the caller declares it. The registry supplies the lens
|
|
317
|
+
* question, investigation mandate, and sensitivity ceiling; this function only
|
|
318
|
+
* threads the declared boundaries through.
|
|
503
319
|
*/
|
|
504
|
-
export function
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
320
|
+
export async function buildOperatingMandate(input) {
|
|
321
|
+
const api = await loadOperatingMandateApi();
|
|
322
|
+
return api.createOperatingMandate(input.roleId, {
|
|
323
|
+
roots: [...input.roots],
|
|
324
|
+
forbiddenPaths: [...(input.forbiddenPaths ?? [])],
|
|
325
|
+
});
|
|
508
326
|
}
|
|
327
|
+
// A mandate's `boundaries.roots` items must satisfy this pattern (leading dot
|
|
328
|
+
// permitted, no `..` traversal), so `.planr` is a valid declared root.
|
|
329
|
+
const INLINE_MANDATE_ROOT_PATTERN = /^(?!.*\.\.)[A-Za-z0-9._][A-Za-z0-9._/-]*$/;
|
|
509
330
|
/**
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
331
|
+
* FR1/FR2: derive a mandate's declared read roots directly from the project
|
|
332
|
+
* working tree — every top-level directory the agent may traverse — for the
|
|
333
|
+
* inline dispatch path. `.planr/` is ALWAYS declared, whether or not it exists
|
|
334
|
+
* yet and REGARDLESS of git tracking, because the mission tool surface walks the
|
|
335
|
+
* filesystem directly rather than a tracked-file inventory, so a gitignored `.planr/` tree
|
|
336
|
+
* is fully readable (finding 2's tracked-file gap cannot reproduce here).
|
|
337
|
+
* `.git`/`node_modules` are never declared, and every root is filtered to the
|
|
338
|
+
* mandate's schema pattern. Mirrors the native adapter path's derivation so both
|
|
339
|
+
* dispatch paths declare identical boundaries.
|
|
519
340
|
*/
|
|
520
|
-
export async function
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
try {
|
|
531
|
-
packet = mission.createOperatingMissionPacket(input.roleId, input.evidenceIndex, {
|
|
532
|
-
protocolVersion: OPERATE_MISSION_PROTOCOL_VERSION,
|
|
533
|
-
cycleId: input.cycleId,
|
|
534
|
-
pinnedRevision: input.pinnedRevision,
|
|
535
|
-
charter: input.charter,
|
|
536
|
-
priorCycleSummary: input.priorCycleSummary,
|
|
537
|
-
planningStatus: input.planningStatus,
|
|
538
|
-
declaredRoots: input.declaredRoots,
|
|
539
|
-
maxEvidenceItems: input.maxEvidenceItems,
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
catch (error) {
|
|
543
|
-
if (isMissionBudgetError(error)) {
|
|
544
|
-
throw new OperateError('E_OPERATE_MISSION_PACKET_BUDGET', `Mission packet for role ${input.roleId} exceeds its mission input budget; ` +
|
|
545
|
-
'the evidence index is not truncated to fit.', { roleId: input.roleId });
|
|
546
|
-
}
|
|
547
|
-
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 });
|
|
548
|
-
}
|
|
549
|
-
const actualBytes = Buffer.byteLength(canonicalize(packet), 'utf8');
|
|
550
|
-
if (actualBytes > derivedBudget) {
|
|
551
|
-
throw new OperateError('E_OPERATE_MISSION_PACKET_BUDGET', `Mission packet for role ${input.roleId} is ${actualBytes} bytes, exceeding its ` +
|
|
552
|
-
`derived ${derivedBudget}-byte mission budget; the evidence index is not truncated to fit.`, { roleId: input.roleId, actualBytes, maxInputBytes: derivedBudget });
|
|
553
|
-
}
|
|
554
|
-
// Read the published pipeline's (0.35.0+) enforced-truncation record off the
|
|
555
|
-
// signed packet's budgets. When the caller-prioritized index exceeded
|
|
556
|
-
// `maxEvidenceItems`, the assembler kept the highest-priority items and stamped
|
|
557
|
-
// `truncatedEvidenceItems`/`evidenceItemsBeforeTruncation` into the packet.
|
|
558
|
-
// Surface that as a structured record so the caller reports the drop instead of
|
|
559
|
-
// silently returning a smaller-than-requested index. (Narrowed locally: the
|
|
560
|
-
// frozen `OperatingMissionPacket.budgets` type does not carry these optional
|
|
561
|
-
// v1.3 provenance fields.)
|
|
562
|
-
const budgets = packet.budgets;
|
|
563
|
-
if (budgets.truncatedEvidenceItems && typeof budgets.evidenceItemsBeforeTruncation === 'number') {
|
|
564
|
-
const evidenceItemsBeforeTruncation = budgets.evidenceItemsBeforeTruncation;
|
|
565
|
-
const keptItems = packet.evidenceIndex.length;
|
|
566
|
-
return {
|
|
567
|
-
packet,
|
|
568
|
-
truncation: {
|
|
569
|
-
roleId: input.roleId,
|
|
570
|
-
evidenceItemsBeforeTruncation,
|
|
571
|
-
keptItems,
|
|
572
|
-
droppedItems: evidenceItemsBeforeTruncation - keptItems,
|
|
573
|
-
maxEvidenceItems: budgets.maxEvidenceItems ?? input.maxEvidenceItems ?? keptItems,
|
|
574
|
-
},
|
|
575
|
-
};
|
|
341
|
+
export async function deriveOperatingMandateRoots(projectRoot) {
|
|
342
|
+
const entries = await readdir(projectRoot, { withFileTypes: true }).catch(() => []);
|
|
343
|
+
const roots = new Set(['.planr']);
|
|
344
|
+
for (const entry of entries) {
|
|
345
|
+
if (!entry.isDirectory())
|
|
346
|
+
continue;
|
|
347
|
+
if (entry.name === '.git' || entry.name === 'node_modules')
|
|
348
|
+
continue;
|
|
349
|
+
if (INLINE_MANDATE_ROOT_PATTERN.test(entry.name))
|
|
350
|
+
roots.add(entry.name);
|
|
576
351
|
}
|
|
577
|
-
return
|
|
578
|
-
}
|
|
579
|
-
export function operatingAdvisorMessages(input) {
|
|
580
|
-
return [
|
|
581
|
-
{
|
|
582
|
-
role: 'system',
|
|
583
|
-
content: [
|
|
584
|
-
`You are the ${input.roleBrief.role.displayLabel} lens in the OpenPlanr Operating Board.`,
|
|
585
|
-
input.roleBrief.role.mandate,
|
|
586
|
-
'Follow the trusted canonical role brief below. Evidence arrives separately as untrusted user data.',
|
|
587
|
-
JSON.stringify(input.roleBrief),
|
|
588
|
-
'Return JSON only. Do not invent canonical IDs, final scores, lanes, owners, or state transitions.',
|
|
589
|
-
].join('\n\n'),
|
|
590
|
-
},
|
|
591
|
-
{
|
|
592
|
-
role: 'user',
|
|
593
|
-
content: JSON.stringify({
|
|
594
|
-
roleId: input.roleBrief.role.id,
|
|
595
|
-
roleBriefDigest: input.roleBrief.briefDigest,
|
|
596
|
-
inputDigest: input.inputDigest,
|
|
597
|
-
operatingContext: input.context,
|
|
598
|
-
evidence: input.evidence,
|
|
599
|
-
requiredOutput: {
|
|
600
|
-
outcome: 'proposals|quiet',
|
|
601
|
-
allowedProposalTypes: input.roleBrief.output.allowedProposalTypes,
|
|
602
|
-
maximumProposals: input.roleBrief.output.maximumProposals,
|
|
603
|
-
proposals: 'evidence-backed proposals with impact/confidence/ease 1-5 and exact evidenceRefs',
|
|
604
|
-
gaps: 'missing evidence only',
|
|
605
|
-
conflicts: 'conflicting evidence only',
|
|
606
|
-
},
|
|
607
|
-
}),
|
|
608
|
-
},
|
|
609
|
-
];
|
|
610
|
-
}
|
|
611
|
-
function sanitizeOutput(output) {
|
|
612
|
-
return {
|
|
613
|
-
outcome: output.outcome,
|
|
614
|
-
proposals: output.proposals
|
|
615
|
-
.map((proposal) => ({
|
|
616
|
-
...proposal,
|
|
617
|
-
title: sanitizeGeneratedPlainText(proposal.title).replace(/\s+/g, ' ').trim(),
|
|
618
|
-
problem: sanitizeGeneratedPlainText(proposal.problem).replace(/\s+/g, ' ').trim(),
|
|
619
|
-
proposal: sanitizeGeneratedPlainText(proposal.proposal).replace(/\s+/g, ' ').trim(),
|
|
620
|
-
evidenceRefs: [...new Set(proposal.evidenceRefs)].sort(),
|
|
621
|
-
...(proposal.dependsOnProposalKeys
|
|
622
|
-
? {
|
|
623
|
-
dependsOnProposalKeys: [...new Set(proposal.dependsOnProposalKeys)].sort(),
|
|
624
|
-
}
|
|
625
|
-
: {}),
|
|
626
|
-
...(proposal.conflictsWithProposalKeys
|
|
627
|
-
? {
|
|
628
|
-
conflictsWithProposalKeys: [...new Set(proposal.conflictsWithProposalKeys)].sort(),
|
|
629
|
-
}
|
|
630
|
-
: {}),
|
|
631
|
-
...(proposal.sequenceProposalKeys
|
|
632
|
-
? { sequenceProposalKeys: [...proposal.sequenceProposalKeys] }
|
|
633
|
-
: {}),
|
|
634
|
-
}))
|
|
635
|
-
.sort((left, right) => left.proposalKey.localeCompare(right.proposalKey) ||
|
|
636
|
-
left.type.localeCompare(right.type) ||
|
|
637
|
-
left.problem.localeCompare(right.problem)),
|
|
638
|
-
gaps: [...new Set(output.gaps.map(sanitizeGeneratedPlainText))].sort(),
|
|
639
|
-
conflicts: [...new Set(output.conflicts.map(sanitizeGeneratedPlainText))].sort(),
|
|
640
|
-
};
|
|
352
|
+
return [...roots].sort();
|
|
641
353
|
}
|
|
642
354
|
/**
|
|
643
355
|
* Convert a compact native-harness response into the canonical, digest-bound
|
|
644
356
|
* Protocol result. Runtime adapters should not manufacture protocol metadata,
|
|
645
357
|
* producer fields, or JCS digests themselves.
|
|
646
358
|
*/
|
|
647
|
-
export async function createNativeOperatingRoleResult(input) {
|
|
648
|
-
const contractIssues = (await loadOperatingProtocol()).validateProtocolArtifact('operating-advisor-response', input.response);
|
|
649
|
-
if (contractIssues.length > 0) {
|
|
650
|
-
throw new OperateError('E_OPERATE_ADVISOR_FAILED', `Native ${input.pack.roleId} response does not match operating-advisor-response@1.2.0.`, {
|
|
651
|
-
...advisorResponseContractDetails(input.pack.roleBrief),
|
|
652
|
-
issues: contractIssues.slice(0, 8),
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
const parsed = advisorOutputSchema.safeParse(input.response);
|
|
656
|
-
if (!parsed.success) {
|
|
657
|
-
throw new OperateError('E_OPERATE_INTERNAL', 'Protocol and OpenPlanr disagree on the compact advisor response contract.', {
|
|
658
|
-
...advisorResponseContractDetails(input.pack.roleBrief),
|
|
659
|
-
issues: parsed.error.issues.slice(0, 8).map((issue) => ({
|
|
660
|
-
path: issue.path.join('.'),
|
|
661
|
-
code: issue.code,
|
|
662
|
-
})),
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
const output = sanitizeOutput(parsed.data);
|
|
666
|
-
assertAdvisorOutputMatchesBrief(input.pack.roleBrief, output);
|
|
667
|
-
const permittedEvidenceRefs = new Set(input.pack.evidence.items.map((item) => item.id));
|
|
668
|
-
const outsideRoleView = output.proposals
|
|
669
|
-
.flatMap((proposal) => proposal.evidenceRefs)
|
|
670
|
-
.filter((reference) => !permittedEvidenceRefs.has(reference));
|
|
671
|
-
if (outsideRoleView.length > 0) {
|
|
672
|
-
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Native ${input.pack.roleId} response cites evidence outside its role-filtered pack.`, { evidenceRefs: [...new Set(outsideRoleView)].sort() });
|
|
673
|
-
}
|
|
674
|
-
const protocol = await loadOperatingProtocol();
|
|
675
|
-
const unsigned = {
|
|
676
|
-
kind: 'operating-role-result',
|
|
677
|
-
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
678
|
-
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
679
|
-
cycleId: input.pack.cycleId,
|
|
680
|
-
roleId: input.pack.roleId,
|
|
681
|
-
inputDigest: input.pack.inputDigest,
|
|
682
|
-
outcome: output.outcome,
|
|
683
|
-
proposals: output.proposals,
|
|
684
|
-
gaps: output.gaps,
|
|
685
|
-
conflicts: output.conflicts,
|
|
686
|
-
producer: {
|
|
687
|
-
product: 'openplanr',
|
|
688
|
-
version: OPENPLANR_VERSION,
|
|
689
|
-
runtime: input.runtime,
|
|
690
|
-
capability: input.pack.roleBrief.role.capabilityTier,
|
|
691
|
-
},
|
|
692
|
-
};
|
|
693
|
-
const result = {
|
|
694
|
-
...unsigned,
|
|
695
|
-
resultDigest: protocol.computeOperatingRoleResultDigest(unsigned),
|
|
696
|
-
};
|
|
697
|
-
await assertOperatingArtifact('operating-role-result', result);
|
|
698
|
-
protocol.validateOperatingRoleResultDigest(result);
|
|
699
|
-
return result;
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Sanitize a v1.3 mission advisor response's free text exactly as the v1.2
|
|
703
|
-
* `sanitizeOutput` does, but PRESERVE each proposal's structured `citations`
|
|
704
|
-
* verbatim: they are schema-pattern-bounded locators (repository path, git
|
|
705
|
-
* revision, or planr artifact — never free prose), which the engine resolves and
|
|
706
|
-
* snapshots after the lens returns. Dropping them would silence every proposal.
|
|
707
|
-
*/
|
|
708
359
|
function sanitizeMissionOutput(output) {
|
|
709
360
|
return {
|
|
710
361
|
outcome: output.outcome,
|
|
@@ -733,10 +384,18 @@ function sanitizeMissionOutput(output) {
|
|
|
733
384
|
};
|
|
734
385
|
}
|
|
735
386
|
/**
|
|
736
|
-
*
|
|
737
|
-
* contract (allowed proposal types,
|
|
738
|
-
*
|
|
739
|
-
* registry-derived brief as the single
|
|
387
|
+
* The mandate's response contract is `operating-advisor-response@1.3.0`, whose
|
|
388
|
+
* output facet mirrors the v1.2 brief's contract (allowed proposal types,
|
|
389
|
+
* maxima), so a v1.3 response is validated against exactly the same invariants a
|
|
390
|
+
* pack response is — reusing the pipeline's registry-derived brief as the single
|
|
391
|
+
* source of truth.
|
|
392
|
+
*
|
|
393
|
+
* FR2: when the response's citations resolve to ZERO evidence IDs across every
|
|
394
|
+
* proposal, the role commits `not_evaluated` — a schema-legal `quiet` result
|
|
395
|
+
* (the frozen v1.2 role-result schema admits no `not_evaluated` outcome; the
|
|
396
|
+
* not_evaluated state lives in the governed gap + the integrity surface) — and
|
|
397
|
+
* the governed empty-grounding gap the citation gate opened is returned, with
|
|
398
|
+
* `notEvaluated: true`, rather than a `quiet` that pretends the lens evaluated.
|
|
740
399
|
*/
|
|
741
400
|
export async function createNativeMissionOperatingRoleResult(input) {
|
|
742
401
|
const protocol = await loadOperatingProtocol();
|
|
@@ -745,7 +404,7 @@ export async function createNativeMissionOperatingRoleResult(input) {
|
|
|
745
404
|
// unless the version is passed.
|
|
746
405
|
const contractIssues = protocol.validateProtocolArtifact('operating-advisor-response', input.response, { protocolVersion: '1.3.0' });
|
|
747
406
|
if (contractIssues.length > 0) {
|
|
748
|
-
throw new OperateError('E_OPERATE_ADVISOR_FAILED', `Native ${input.
|
|
407
|
+
throw new OperateError('E_OPERATE_ADVISOR_FAILED', `Native ${input.mandate.roleId} response does not match operating-advisor-response@1.3.0.`, { issues: contractIssues.slice(0, 8) });
|
|
749
408
|
}
|
|
750
409
|
const parsed = missionAdvisorOutputSchema.safeParse(input.response);
|
|
751
410
|
if (!parsed.success) {
|
|
@@ -757,23 +416,22 @@ export async function createNativeMissionOperatingRoleResult(input) {
|
|
|
757
416
|
});
|
|
758
417
|
}
|
|
759
418
|
const output = sanitizeMissionOutput(parsed.data);
|
|
760
|
-
const brief = protocol.createOperatingAdvisorBrief(input.
|
|
419
|
+
const brief = protocol.createOperatingAdvisorBrief(input.mandate.roleId);
|
|
761
420
|
assertAdvisorOutputMatchesBrief(brief, output);
|
|
762
|
-
const capability =
|
|
763
|
-
brief.role.capabilityTier);
|
|
421
|
+
const capability = brief.role.capabilityTier;
|
|
764
422
|
// The intermediate result: proposals carry their v1.3 citations and an empty
|
|
765
423
|
// evidenceRefs set. It is deliberately NOT yet a v1.2-valid committed
|
|
766
424
|
// operating-role-result — the citation gate mints the evidenceRefs that make
|
|
767
|
-
// it one. `inputDigest` is the
|
|
768
|
-
// input-digest binding (prepare stored the same
|
|
425
|
+
// it one. `inputDigest` is the mandate's digest, so the record path's
|
|
426
|
+
// input-digest binding (prepare stored the same mandate digest) holds.
|
|
769
427
|
const intermediate = {
|
|
770
428
|
kind: 'operating-role-result',
|
|
771
429
|
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
772
430
|
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
773
|
-
cycleId: input.
|
|
774
|
-
roleId: input.
|
|
775
|
-
inputDigest: input.
|
|
776
|
-
resultDigest: input.
|
|
431
|
+
cycleId: input.cycleId,
|
|
432
|
+
roleId: input.mandate.roleId,
|
|
433
|
+
inputDigest: input.mandate.mandateDigest,
|
|
434
|
+
resultDigest: input.mandate.mandateDigest,
|
|
777
435
|
outcome: output.outcome,
|
|
778
436
|
proposals: output.proposals.map((proposal) => ({
|
|
779
437
|
proposalKey: proposal.proposalKey,
|
|
@@ -810,25 +468,37 @@ export async function createNativeMissionOperatingRoleResult(input) {
|
|
|
810
468
|
// proposals response threads its citations through the already-live gate.
|
|
811
469
|
const gated = output.proposals.length > 0
|
|
812
470
|
? await input.resolveCitations([intermediate])
|
|
813
|
-
: {
|
|
471
|
+
: {
|
|
472
|
+
roleResults: [intermediate],
|
|
473
|
+
gaps: [],
|
|
474
|
+
notEvaluatedRoleIds: [],
|
|
475
|
+
};
|
|
814
476
|
const resolved = gated.roleResults[0] ?? intermediate;
|
|
477
|
+
// FR2: a proposals response whose citations resolved to ZERO evidence is
|
|
478
|
+
// not_evaluated — the gate returned this role in `notEvaluatedRoleIds` and
|
|
479
|
+
// opened the governed empty-grounding gap. The committed artifact is a
|
|
480
|
+
// schema-legal `quiet` result (no proposals); the not_evaluated state is
|
|
481
|
+
// carried by the gap and the returned flag.
|
|
482
|
+
const notEvaluated = gated.notEvaluatedRoleIds.includes(input.mandate.roleId);
|
|
815
483
|
// Finalize into a v1.2-valid committed operating-role-result: strip the
|
|
816
484
|
// now-resolved citations, keep the minted evidenceRefs, and let the surviving
|
|
817
|
-
// proposal count set the honest outcome (
|
|
818
|
-
//
|
|
819
|
-
const survivingProposals =
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
485
|
+
// proposal count set the honest outcome (a not_evaluated response commits as
|
|
486
|
+
// quiet, its grounding failure preserved only as the opened gaps).
|
|
487
|
+
const survivingProposals = notEvaluated
|
|
488
|
+
? []
|
|
489
|
+
: resolved.proposals
|
|
490
|
+
.map((proposal) => {
|
|
491
|
+
const { citations: _citations, ...rest } = proposal;
|
|
492
|
+
return rest;
|
|
493
|
+
})
|
|
494
|
+
.filter((proposal) => proposal.evidenceRefs.length > 0);
|
|
825
495
|
const unsigned = {
|
|
826
496
|
kind: 'operating-role-result',
|
|
827
497
|
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
828
498
|
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
829
|
-
cycleId: input.
|
|
830
|
-
roleId: input.
|
|
831
|
-
inputDigest: input.
|
|
499
|
+
cycleId: input.cycleId,
|
|
500
|
+
roleId: input.mandate.roleId,
|
|
501
|
+
inputDigest: input.mandate.mandateDigest,
|
|
832
502
|
outcome: survivingProposals.length > 0 ? 'proposals' : 'quiet',
|
|
833
503
|
proposals: survivingProposals,
|
|
834
504
|
gaps: output.gaps,
|
|
@@ -846,7 +516,7 @@ export async function createNativeMissionOperatingRoleResult(input) {
|
|
|
846
516
|
};
|
|
847
517
|
await assertOperatingArtifact('operating-role-result', result);
|
|
848
518
|
protocol.validateOperatingRoleResultDigest(result);
|
|
849
|
-
return { result, gaps: gated.gaps };
|
|
519
|
+
return { result, gaps: gated.gaps, notEvaluated };
|
|
850
520
|
}
|
|
851
521
|
function safeFailureMessage(error) {
|
|
852
522
|
try {
|
|
@@ -882,38 +552,17 @@ export function createOfflineAdvisorAdapter(fixture) {
|
|
|
882
552
|
};
|
|
883
553
|
}
|
|
884
554
|
class OpenPlanrStructuredAdapter {
|
|
885
|
-
provider;
|
|
886
|
-
quiet;
|
|
887
555
|
id;
|
|
888
556
|
mode = 'structured';
|
|
889
557
|
toolIsolation = 'not-applicable';
|
|
890
558
|
capability = 'analysis-high';
|
|
891
559
|
parallelDispatch = false;
|
|
892
|
-
constructor(
|
|
893
|
-
this.provider = provider;
|
|
894
|
-
this.quiet = quiet;
|
|
560
|
+
constructor(providerName) {
|
|
895
561
|
this.id = `openplanr-${providerName}`;
|
|
896
562
|
}
|
|
897
563
|
async invoke(input) {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
source: item.source,
|
|
901
|
-
location: item.location,
|
|
902
|
-
freshness: item.freshness,
|
|
903
|
-
claimTypes: item.claimTypes,
|
|
904
|
-
summary: item.summary,
|
|
905
|
-
}));
|
|
906
|
-
const messages = operatingAdvisorMessages({
|
|
907
|
-
roleBrief: input.roleBrief,
|
|
908
|
-
evidence,
|
|
909
|
-
context: input.context,
|
|
910
|
-
inputDigest: input.inputDigest,
|
|
911
|
-
});
|
|
912
|
-
return (await generateJSON(this.provider, messages, advisorOutputSchema, {
|
|
913
|
-
temperature: 0.2,
|
|
914
|
-
maxTokens: 4_096,
|
|
915
|
-
quiet: this.quiet,
|
|
916
|
-
})).result;
|
|
564
|
+
void input;
|
|
565
|
+
throw new OperateError('E_OPERATE_PROVIDER_DEPRECATED', 'The structured-provider advisor path is deprecated; dispatch now runs through the native Protocol v1.3 mandate harness. See https://openplanr.dev/docs/operate/agent-harness. Scheduled for removal in OpenPlanr 2.0.0.');
|
|
917
566
|
}
|
|
918
567
|
}
|
|
919
568
|
/** Redacted error class label for diagnostics — no message or stack, ever. */
|
|
@@ -967,22 +616,22 @@ export async function createConfiguredStructuredAdapter(projectRoot, options = {
|
|
|
967
616
|
// (`planr config set-key …` / `--offline`) and records a redacted error class.
|
|
968
617
|
let provider;
|
|
969
618
|
try {
|
|
970
|
-
provider = await getAIProvider(config);
|
|
619
|
+
provider = await getAIProvider(config, { surface: 'operate-structured-provider' });
|
|
971
620
|
}
|
|
972
621
|
catch (error) {
|
|
973
622
|
throw new OperateError('E_OPERATE_ADVISOR_FAILED', structuredProviderBootstrapRemedy(error, config.ai?.provider), { errorClass: redactedProviderErrorClass(error) });
|
|
974
623
|
}
|
|
975
|
-
|
|
624
|
+
void provider;
|
|
625
|
+
void options;
|
|
626
|
+
return new OpenPlanrStructuredAdapter(config.ai?.provider ?? 'ai');
|
|
976
627
|
}
|
|
977
628
|
export async function dispatchOperatingAdvisors(input) {
|
|
978
629
|
assertAdvisorIsolation(input.adapter);
|
|
979
|
-
const roleRegistry = (await loadOperatingProtocol()).listOperatingRoles();
|
|
980
630
|
const protocol = await loadOperatingProtocol();
|
|
981
|
-
const
|
|
631
|
+
const roleRegistry = protocol.listOperatingRoles();
|
|
982
632
|
// Canonical registry order so results and provenance are byte-identical across
|
|
983
633
|
// parallel/sequential dispatch and across the order roles arrive in (FR4).
|
|
984
634
|
const roleOrder = new Map(roleRegistry.map((role, index) => [role.id, index]));
|
|
985
|
-
const registryDefaultMode = new Map(roleRegistry.map((role) => [role.id, operatingRegistryDispatchMode(role)]));
|
|
986
635
|
// The runtime's ability to enforce the bounded read-only boundary is resolved
|
|
987
636
|
// once per dispatch and fails closed: a runtime whose isolation cannot be
|
|
988
637
|
// verified never receives a native lens (FR2).
|
|
@@ -991,13 +640,15 @@ export async function dispatchOperatingAdvisors(input) {
|
|
|
991
640
|
// adapter (whose isolation `assertAdvisorIsolation` has already proven to be
|
|
992
641
|
// enforced) is native-capable.
|
|
993
642
|
const adapterNativeCapable = input.adapter.mode === 'native-isolated';
|
|
994
|
-
const
|
|
643
|
+
const resolveIsolation = (roleId) => resolveOperatingDispatchIsolation({
|
|
995
644
|
roleId,
|
|
996
|
-
registryDefault: registryDefaultMode.get(roleId) ?? 'mission',
|
|
997
|
-
override: input.dispatchModeOverrides?.[roleId],
|
|
998
645
|
runtimeEnforcesBoundedReadOnly,
|
|
999
646
|
adapterNativeCapable,
|
|
1000
647
|
});
|
|
648
|
+
// FR1/FR2: the mandate's declared read roots are the whole granted workspace —
|
|
649
|
+
// including a gitignored `.planr/` — never an evidence-index subset. Derived
|
|
650
|
+
// once and shared by every role's mandate on this path.
|
|
651
|
+
const roots = await deriveOperatingMandateRoots(input.projectRoot);
|
|
1001
652
|
const skipped = [];
|
|
1002
653
|
const runnable = [];
|
|
1003
654
|
for (const role of input.readiness.roles) {
|
|
@@ -1015,23 +666,18 @@ export async function dispatchOperatingAdvisors(input) {
|
|
|
1015
666
|
// Resolve THIS role's dispatch mode once and derive provenance from what is
|
|
1016
667
|
// actually dispatched below — never re-derived after the fact. A role that
|
|
1017
668
|
// resolves to a native bounded lens has its read-only tool grant enforced
|
|
1018
|
-
// before the lens runs (below);
|
|
1019
|
-
//
|
|
1020
|
-
|
|
1021
|
-
const resolution = resolveMode(role.roleId);
|
|
669
|
+
// before the lens runs (below); `isolation` can only read
|
|
670
|
+
// `enforced-read-only-bounded` when the bounded grant was genuinely enforced.
|
|
671
|
+
const resolution = resolveIsolation(role.roleId);
|
|
1022
672
|
const dispatch = {
|
|
1023
|
-
dispatchMode: resolution.mode,
|
|
1024
673
|
isolation: resolution.isolation,
|
|
1025
674
|
reconciliation: resolution.reconciliation,
|
|
1026
675
|
};
|
|
1027
|
-
let
|
|
676
|
+
let mandate;
|
|
1028
677
|
try {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
evidence: input.evidence,
|
|
1033
|
-
context: input.context,
|
|
1034
|
-
});
|
|
678
|
+
// FR1: a body-free operating mandate — the lens question, declared read
|
|
679
|
+
// boundaries, and citation requirement — replaces the curated evidence pack.
|
|
680
|
+
mandate = await buildOperatingMandate({ roleId: role.roleId, roots });
|
|
1035
681
|
}
|
|
1036
682
|
catch (error) {
|
|
1037
683
|
return {
|
|
@@ -1042,50 +688,58 @@ export async function dispatchOperatingAdvisors(input) {
|
|
|
1042
688
|
dispatch,
|
|
1043
689
|
};
|
|
1044
690
|
}
|
|
691
|
+
const roleBrief = protocol.createOperatingAdvisorBrief(role.roleId);
|
|
692
|
+
const context = roleContext(input.context, role.roleId, new Set());
|
|
1045
693
|
if (resolution.native) {
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
const toolset = createMissionToolset({ roots, ceiling });
|
|
694
|
+
// Confine the native lens to the bounded read-only toolset over the
|
|
695
|
+
// mandate's declared roots. Constructing the toolset is what makes
|
|
696
|
+
// `enforced-read-only-bounded` true; a callable outside the read-only grant
|
|
697
|
+
// simply does not exist on the surface it hands the lens.
|
|
698
|
+
const grantedRoots = narrowMissionRootsToCeiling({
|
|
699
|
+
declaredRoots: mandate.boundaries.roots,
|
|
700
|
+
forbiddenPaths: mandate.boundaries.forbiddenPaths,
|
|
701
|
+
});
|
|
702
|
+
const toolset = createMissionToolset({
|
|
703
|
+
roots: grantedRoots,
|
|
704
|
+
ceiling: mandate.boundaries.sensitivityCeiling,
|
|
705
|
+
});
|
|
1059
706
|
const grantedTools = Object.keys(toolset);
|
|
1060
707
|
if (grantedTools.some((tool) => !MISSION_READ_ONLY_TOOLS.includes(tool))) {
|
|
1061
708
|
throw new OperateError('E_OPERATE_PROVIDER_READ_ONLY', `Mission dispatch for ${role.roleId} assembled a tool outside the bounded read-only grant.`);
|
|
1062
709
|
}
|
|
1063
710
|
}
|
|
1064
|
-
|
|
1065
|
-
let output;
|
|
711
|
+
let built;
|
|
1066
712
|
let lastError;
|
|
1067
713
|
let roleModelCalls = 0;
|
|
1068
714
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1069
715
|
try {
|
|
1070
716
|
roleModelCalls += 1;
|
|
1071
|
-
|
|
717
|
+
const response = await input.adapter.invoke({
|
|
1072
718
|
roleId: role.roleId,
|
|
1073
|
-
roleBrief
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
719
|
+
roleBrief,
|
|
720
|
+
mandate,
|
|
721
|
+
pinnedRevision: input.pinnedRevision,
|
|
722
|
+
context,
|
|
723
|
+
inputDigest: mandate.mandateDigest,
|
|
724
|
+
});
|
|
725
|
+
// The mandate response's citations flow into the injected universal gate,
|
|
726
|
+
// which mints the evidenceRefs that make the committed result v1.2-valid;
|
|
727
|
+
// a response that grounds zero evidence commits not_evaluated (quiet) with
|
|
728
|
+
// a governed gap. Identical finalization to the native adapter record path.
|
|
729
|
+
built = await createNativeMissionOperatingRoleResult({
|
|
730
|
+
mandate,
|
|
731
|
+
cycleId: input.cycleId,
|
|
732
|
+
response,
|
|
733
|
+
runtime: input.runtime ?? input.adapter.id,
|
|
734
|
+
resolveCitations: input.resolveCitations,
|
|
735
|
+
});
|
|
1082
736
|
break;
|
|
1083
737
|
}
|
|
1084
738
|
catch (error) {
|
|
1085
739
|
lastError = error;
|
|
1086
740
|
}
|
|
1087
741
|
}
|
|
1088
|
-
if (!
|
|
742
|
+
if (!built) {
|
|
1089
743
|
return {
|
|
1090
744
|
ok: false,
|
|
1091
745
|
roleId: role.roleId,
|
|
@@ -1094,54 +748,36 @@ export async function dispatchOperatingAdvisors(input) {
|
|
|
1094
748
|
dispatch,
|
|
1095
749
|
};
|
|
1096
750
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
inputDigest: pack.inputDigest,
|
|
1104
|
-
outcome: output.outcome,
|
|
1105
|
-
proposals: output.proposals,
|
|
1106
|
-
gaps: output.gaps,
|
|
1107
|
-
conflicts: output.conflicts,
|
|
1108
|
-
producer: {
|
|
1109
|
-
product: 'openplanr',
|
|
1110
|
-
version: OPENPLANR_VERSION,
|
|
1111
|
-
runtime: input.runtime ?? input.adapter.id,
|
|
1112
|
-
capability: capabilityByRole.get(role.roleId) ?? 'analysis-high',
|
|
1113
|
-
},
|
|
1114
|
-
};
|
|
1115
|
-
const result = {
|
|
1116
|
-
...unsigned,
|
|
1117
|
-
resultDigest: protocol.computeOperatingRoleResultDigest(unsigned),
|
|
751
|
+
return {
|
|
752
|
+
ok: true,
|
|
753
|
+
result: built.result,
|
|
754
|
+
gaps: built.gaps,
|
|
755
|
+
modelCalls: roleModelCalls,
|
|
756
|
+
dispatch,
|
|
1118
757
|
};
|
|
1119
|
-
await assertOperatingArtifact('operating-role-result', result);
|
|
1120
|
-
protocol.validateOperatingRoleResultDigest(result);
|
|
1121
|
-
return { ok: true, result, modelCalls: roleModelCalls, dispatch };
|
|
1122
758
|
}
|
|
1123
759
|
// Fan the per-role dispatch out in parallel where the adapter reports it,
|
|
1124
|
-
// sequentially otherwise.
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1127
|
-
// sequential dispatch and across dispatch order (FR4/E-004).
|
|
760
|
+
// sequentially otherwise. Results are sorted into canonical registry order
|
|
761
|
+
// below so the reduced events are byte-identical across parallel and sequential
|
|
762
|
+
// dispatch and across dispatch order (FR4/E-004).
|
|
1128
763
|
const dispatched = await runMissionDispatchFanOut({
|
|
1129
764
|
items: runnable,
|
|
1130
765
|
parallel: Boolean(input.adapter.parallelDispatch),
|
|
1131
766
|
run: (role) => dispatchRole(role),
|
|
1132
767
|
});
|
|
1133
|
-
// The per-role dispatch descriptor captured inside `dispatchRole` — provenance
|
|
1134
|
-
// reads it rather than re-resolving, so it can only report the isolation the
|
|
1135
|
-
// role was actually dispatched under.
|
|
1136
768
|
const dispatchByRole = new Map();
|
|
1137
769
|
for (const entry of dispatched) {
|
|
1138
770
|
dispatchByRole.set(entry.ok ? entry.result.roleId : entry.roleId, entry.dispatch);
|
|
1139
771
|
}
|
|
1140
|
-
const
|
|
1141
|
-
|
|
772
|
+
const okEntries = dispatched.filter((entry) => entry.ok);
|
|
773
|
+
const results = okEntries
|
|
1142
774
|
.map((entry) => entry.result)
|
|
1143
775
|
.sort((left, right) => (roleOrder.get(left.roleId) ?? Number.MAX_SAFE_INTEGER) -
|
|
1144
776
|
(roleOrder.get(right.roleId) ?? Number.MAX_SAFE_INTEGER));
|
|
777
|
+
// The governed gaps opened while resolving each role's citations (unresolvable
|
|
778
|
+
// citations, empty grounding) are threaded back so the engine records them
|
|
779
|
+
// alongside the cycle's other gaps.
|
|
780
|
+
const gaps = okEntries.flatMap((entry) => entry.gaps);
|
|
1145
781
|
const failed = dispatched
|
|
1146
782
|
.filter((entry) => !entry.ok)
|
|
1147
783
|
.map(({ roleId, message }) => ({ roleId, message }));
|
|
@@ -1150,9 +786,8 @@ export async function dispatchOperatingAdvisors(input) {
|
|
|
1150
786
|
results,
|
|
1151
787
|
provenance: results.map((result) => {
|
|
1152
788
|
const dispatchProvenance = dispatchByRole.get(result.roleId) ?? {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
reconciliation: resolveMode(result.roleId).reconciliation,
|
|
789
|
+
isolation: resolveIsolation(result.roleId).isolation,
|
|
790
|
+
reconciliation: resolveIsolation(result.roleId).reconciliation,
|
|
1156
791
|
};
|
|
1157
792
|
return {
|
|
1158
793
|
roleId: result.roleId,
|
|
@@ -1160,13 +795,13 @@ export async function dispatchOperatingAdvisors(input) {
|
|
|
1160
795
|
adapterId: input.adapter.id,
|
|
1161
796
|
capability: input.adapter.capability,
|
|
1162
797
|
dispatch: input.adapter.parallelDispatch ? 'parallel' : 'sequential',
|
|
1163
|
-
dispatchMode: dispatchProvenance.dispatchMode,
|
|
1164
798
|
isolation: dispatchProvenance.isolation,
|
|
1165
799
|
reconciliation: dispatchProvenance.reconciliation,
|
|
1166
800
|
};
|
|
1167
801
|
}),
|
|
1168
802
|
skipped,
|
|
1169
803
|
failed,
|
|
804
|
+
gaps,
|
|
1170
805
|
blocked: failed.some((entry) => entry.roleId === 'chair') ||
|
|
1171
806
|
(input.depth === 'deep' && failed.length > 0),
|
|
1172
807
|
modelCalls,
|