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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { advisorResponseContractDetails, assertAdvisorOutputMatchesBrief, buildAdvisorOperatingContext, createNativeOperatingRoleResult, createOperatingAdvisorPack, } from './advisors.js';
|
|
4
|
+
import { advisorResponseContractDetails, assertAdvisorOutputMatchesBrief, assertOperatingAdvisorPackWithinBudget, buildAdvisorOperatingContext, createNativeMissionOperatingRoleResult, createNativeOperatingRoleResult, createOperatingAdvisorPack, } from './advisors.js';
|
|
5
5
|
import { canonicalDigest, canonicalize, sha256Digest } from './canonical.js';
|
|
6
|
-
import { operatingProjectKey } from './config.js';
|
|
7
|
-
import { buildChairEvidence } from './engine.js';
|
|
6
|
+
import { operatingProjectKey, readOperatingAdapterLeaseDurationMs, readOperatingDispatchModeOverrides, } from './config.js';
|
|
7
|
+
import { buildChairEvidence, buildOperatingMissionPackets, gateRecordedProposalCitations, } from './engine.js';
|
|
8
8
|
import { OperatingEventStore } from './event-store.js';
|
|
9
9
|
import { OperatingEvidenceCache } from './evidence-cache.js';
|
|
10
10
|
import { purgeStaleEvidenceClassifications } from './evidence-classifications.js';
|
|
@@ -13,12 +13,22 @@ import { evaluateEvidenceReadiness } from './evidence-readiness.js';
|
|
|
13
13
|
import { guidedSessionStatus, purgeGuidedSessions } from './interaction/session-service.js';
|
|
14
14
|
import { assertCommittedOperatingView, recoverOperatingTransactions } from './journal.js';
|
|
15
15
|
import { withOperatingLock } from './lock-service.js';
|
|
16
|
+
import { narrowMissionRootsToCeiling, operatingRegistryDispatchMode, operatingRuntimeEnforcesBoundedReadOnly, resolveOperatingDispatchMode, } from './mission-dispatch.js';
|
|
16
17
|
import { assertOperatingArtifact, loadOperatingProtocol } from './protocol.js';
|
|
17
18
|
import { containsSecret, redactSensitiveText } from './redaction.js';
|
|
18
19
|
import { OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
|
|
19
|
-
import { resolveContainedPath, resolveOperatingPaths } from './workspace.js';
|
|
20
|
+
import { readOperatingConfig, refreshOperatingWorkspaceManifest, resolveContainedPath, resolveOperatingPaths, } from './workspace.js';
|
|
21
|
+
/**
|
|
22
|
+
* Whether any bound role of this session resolved to native mission dispatch, so
|
|
23
|
+
* both adapter-handoff call sites can pass `protocolVersion: '1.3.0'` and the
|
|
24
|
+
* pipeline emits the v1.3 mission record action (`dispatch.agent` +
|
|
25
|
+
* `dispatch.missionPacketPointer`) instead of the v1.2 `rolePackPointer`.
|
|
26
|
+
*/
|
|
27
|
+
function sessionHasMissionRole(session) {
|
|
28
|
+
return Boolean(session.roleMissionPackets && Object.keys(session.roleMissionPackets).length > 0);
|
|
29
|
+
}
|
|
20
30
|
function adapterSessionSummary(session) {
|
|
21
|
-
const { roleBriefs: _roleBriefs, rolePacks: _rolePacks, ...summary } = session;
|
|
31
|
+
const { roleBriefs: _roleBriefs, rolePacks: _rolePacks, roleMissionPackets: _roleMissionPackets, ...summary } = session;
|
|
22
32
|
return summary;
|
|
23
33
|
}
|
|
24
34
|
async function adapterHandoff(session) {
|
|
@@ -32,6 +42,10 @@ async function adapterHandoff(session) {
|
|
|
32
42
|
: 'finalize-required';
|
|
33
43
|
const protocol = await loadOperatingProtocol();
|
|
34
44
|
const handoff = protocol.createOperatingAdapterHandoff({
|
|
45
|
+
// v1.3 when any bound role resolved mission, so the record action names the
|
|
46
|
+
// generated lens agent and points at its mission packet; v1.2 (pack) default
|
|
47
|
+
// otherwise, byte-compatible with the pack-mode handoff.
|
|
48
|
+
...(sessionHasMissionRole(session) ? { protocolVersion: '1.3.0' } : {}),
|
|
35
49
|
phase: session.phase,
|
|
36
50
|
state,
|
|
37
51
|
cycleId: session.cycleId,
|
|
@@ -55,8 +69,66 @@ function sameRoleSet(left, right) {
|
|
|
55
69
|
return (left.length === right.length &&
|
|
56
70
|
[...left].sort().every((role, index) => role === [...right].sort()[index]));
|
|
57
71
|
}
|
|
58
|
-
function sessionExpired(session) {
|
|
59
|
-
return Date.parse(session.expiresAt) <=
|
|
72
|
+
function sessionExpired(session, nowMs = Date.now()) {
|
|
73
|
+
return Date.parse(session.expiresAt) <= nowMs;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* FR4: the board's identity is the hash of its event-chain genesis event (the
|
|
77
|
+
* event whose `previousEventHash` is null). It is immutable for a board
|
|
78
|
+
* generation — appending events never rewrites the genesis — and a board
|
|
79
|
+
* re-inited at the same path re-genesises the chain, so the identity changes.
|
|
80
|
+
* Machine-local adapter sessions are bound to it so a session from a superseded
|
|
81
|
+
* generation is never matched or reused by the current board. Returns '' when
|
|
82
|
+
* no committed chain exists yet, or if the chain cannot be read/verified (that
|
|
83
|
+
* failure is surfaced by the dedicated event-replay diagnostics, not here).
|
|
84
|
+
*/
|
|
85
|
+
async function committedBoardIdentity(store) {
|
|
86
|
+
try {
|
|
87
|
+
const { events } = await store.replay();
|
|
88
|
+
const genesis = events.find((event) => event.previousEventHash === null) ?? events[0];
|
|
89
|
+
return genesis?.eventHash ?? '';
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function removeMachineLocalCacheDir(target) {
|
|
96
|
+
const entries = await readdir(target, { withFileTypes: true }).catch(() => []);
|
|
97
|
+
const removed = entries.filter((entry) => entry.isFile() && entry.name.endsWith('.json')).length;
|
|
98
|
+
await rm(target, { recursive: true, force: true });
|
|
99
|
+
return removed;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* FR4: purge the board's machine-local advisor sessions (`<localRoot>/advisors/`)
|
|
103
|
+
* and incremental evidence baselines (`<localRoot>/evidence/incremental/`). A
|
|
104
|
+
* committed `operate init` apply calls this so a board re-inited at the same path
|
|
105
|
+
* never inherits a prior generation's sessions or cached baselines, and
|
|
106
|
+
* `operate cache purge` calls it so the doctor's staleness diagnostics have a
|
|
107
|
+
* scoped fix command. Both surfaces are rebuildable machine-local caches, never
|
|
108
|
+
* committed protocol artifacts.
|
|
109
|
+
*/
|
|
110
|
+
export async function purgeBoardMachineLocalCaches(input) {
|
|
111
|
+
const paths = resolveOperatingPaths(input.projectRoot, { localRoot: input.localRoot });
|
|
112
|
+
const removedAdvisorSessions = await removeMachineLocalCacheDir(paths.advisors);
|
|
113
|
+
const removedIncrementalBaselines = await removeMachineLocalCacheDir(path.join(paths.evidence, 'incremental'));
|
|
114
|
+
return { removedAdvisorSessions, removedIncrementalBaselines };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Surface the adapter session lease ergonomically (FR10 / T-008): the absolute
|
|
118
|
+
* `expiresAt` plus the remaining time relative to the resolved clock. Included in
|
|
119
|
+
* every adapter lifecycle response so a native runtime can see, at a glance, how
|
|
120
|
+
* long its prepared session is still valid and when it must resume or re-run —
|
|
121
|
+
* rather than parsing `expiresAt` against wall-clock itself. `remainingMs` is
|
|
122
|
+
* floored at zero so an already-lapsed lease reads as `expired`, never negative.
|
|
123
|
+
*/
|
|
124
|
+
function adapterLeaseStatus(session, nowMs) {
|
|
125
|
+
const remainingMs = Math.max(0, Date.parse(session.expiresAt) - nowMs);
|
|
126
|
+
return {
|
|
127
|
+
expiresAt: session.expiresAt,
|
|
128
|
+
remainingMs,
|
|
129
|
+
remainingSeconds: Math.floor(remainingMs / 1_000),
|
|
130
|
+
expired: remainingMs <= 0,
|
|
131
|
+
};
|
|
60
132
|
}
|
|
61
133
|
function retryRunCommand(session) {
|
|
62
134
|
return `planr operate run --cycle-id ${session.cycleId} --runtime ${session.runtime} --json`;
|
|
@@ -148,16 +220,36 @@ export async function operatingCacheAction(input) {
|
|
|
148
220
|
localRoot: input.localRoot,
|
|
149
221
|
purge: true,
|
|
150
222
|
});
|
|
223
|
+
// FR4/FR11: clearing machine-local caches also drops board-bound adapter
|
|
224
|
+
// sessions and incremental evidence baselines, so this is the scoped fix the
|
|
225
|
+
// doctor names for stale adapter sessions and stale incremental baselines.
|
|
226
|
+
const board = await purgeBoardMachineLocalCaches({
|
|
227
|
+
projectRoot: input.projectRoot,
|
|
228
|
+
localRoot: input.localRoot,
|
|
229
|
+
});
|
|
151
230
|
return {
|
|
152
|
-
removed: removed.length +
|
|
231
|
+
removed: removed.length +
|
|
232
|
+
sessions.removed +
|
|
233
|
+
classifications.purged +
|
|
234
|
+
board.removedAdvisorSessions +
|
|
235
|
+
board.removedIncrementalBaselines,
|
|
153
236
|
evidence: { removed: removed.length, entries: removed },
|
|
154
237
|
sessions,
|
|
155
238
|
classifications,
|
|
239
|
+
adapterSessions: { removed: board.removedAdvisorSessions },
|
|
240
|
+
incrementalBaselines: { removed: board.removedIncrementalBaselines },
|
|
156
241
|
};
|
|
157
242
|
}
|
|
158
243
|
function integrityKeyPath(projectRoot, localRoot) {
|
|
159
244
|
return path.join(resolveOperatingPaths(projectRoot, { localRoot }).localRoot, 'integrity.key');
|
|
160
245
|
}
|
|
246
|
+
// The committed checkpoint moved under `.state/` in Protocol v1.3; derive its
|
|
247
|
+
// project-relative display path from the resolver so these results never drift
|
|
248
|
+
// from the on-disk layout again.
|
|
249
|
+
function operatingCheckpointRelativePath(projectRoot, localRoot) {
|
|
250
|
+
const paths = resolveOperatingPaths(projectRoot, { localRoot });
|
|
251
|
+
return path.relative(projectRoot, paths.checkpoint).split(path.sep).join('/');
|
|
252
|
+
}
|
|
161
253
|
async function loadIntegrityKey(projectRoot, localRoot) {
|
|
162
254
|
return readFile(integrityKeyPath(projectRoot, localRoot)).catch(() => null);
|
|
163
255
|
}
|
|
@@ -201,7 +293,7 @@ export async function operatingIntegrityAction(input) {
|
|
|
201
293
|
return {
|
|
202
294
|
status: checkpoint.integrity.status,
|
|
203
295
|
keyId: checkpoint.integrity.status === 'signed' ? checkpoint.integrity.signature.keyId : null,
|
|
204
|
-
checkpoint:
|
|
296
|
+
checkpoint: operatingCheckpointRelativePath(input.projectRoot, input.localRoot),
|
|
205
297
|
};
|
|
206
298
|
});
|
|
207
299
|
}
|
|
@@ -467,7 +559,7 @@ export async function repairOperatingSecurity(input) {
|
|
|
467
559
|
},
|
|
468
560
|
checkpoint: {
|
|
469
561
|
integrity: checkpoint.integrity,
|
|
470
|
-
path:
|
|
562
|
+
path: operatingCheckpointRelativePath(input.projectRoot, input.localRoot),
|
|
471
563
|
},
|
|
472
564
|
guidance,
|
|
473
565
|
};
|
|
@@ -552,7 +644,7 @@ export async function readPersistedOperatingRoleResults(store, cycleId) {
|
|
|
552
644
|
}
|
|
553
645
|
return [...byRole.values()].sort((left, right) => left.roleId.localeCompare(right.roleId));
|
|
554
646
|
}
|
|
555
|
-
async function readAdapterSession(projectRoot, cycleId, localRoot) {
|
|
647
|
+
async function readAdapterSession(projectRoot, cycleId, localRoot, nowMs = Date.now()) {
|
|
556
648
|
const parsed = JSON.parse(await readFile(adapterSessionPath(projectRoot, cycleId, localRoot), 'utf8'));
|
|
557
649
|
const session = {
|
|
558
650
|
...parsed,
|
|
@@ -562,7 +654,10 @@ async function readAdapterSession(projectRoot, cycleId, localRoot) {
|
|
|
562
654
|
if (session.implementation !== 'openplanr-operate-adapter') {
|
|
563
655
|
throw new OperateError('E_OPERATE_ADVISOR_FAILED', 'Adapter session is invalid.');
|
|
564
656
|
}
|
|
565
|
-
|
|
657
|
+
// Expiry is evaluated against the resolved clock (an injected clock in tests,
|
|
658
|
+
// wall-clock in production) so a lease that lapsed after its refresh window is
|
|
659
|
+
// still rejected here even when the caller supplies a deterministic clock.
|
|
660
|
+
if (sessionExpired(session, nowMs)) {
|
|
566
661
|
throw new OperateError('E_OPERATE_ADVISOR_FAILED', 'Adapter session expired.', {
|
|
567
662
|
cycleId,
|
|
568
663
|
recoveryCommand: retryRunCommand(session),
|
|
@@ -593,6 +688,81 @@ async function assertAdapterCycleBinding(input, session) {
|
|
|
593
688
|
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Adapter evidence no longer matches the committed cycle snapshot.');
|
|
594
689
|
}
|
|
595
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* The machine-local sensitivity ceiling (default `internal`), read the same way
|
|
693
|
+
* the evidence cache and cycle engine read it. Mission packets and their tool
|
|
694
|
+
* grants are narrowed to this ceiling.
|
|
695
|
+
*/
|
|
696
|
+
async function readAdapterSensitivityCeiling(projectRoot, localRoot) {
|
|
697
|
+
const paths = resolveOperatingPaths(projectRoot, { localRoot });
|
|
698
|
+
return readFile(path.join(paths.localRoot, 'preferences.json'), 'utf8')
|
|
699
|
+
.then((raw) => JSON.parse(raw).sensitivityCeiling ??
|
|
700
|
+
'internal')
|
|
701
|
+
.catch(() => 'internal');
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* FR2: deny-list every declared read root that contains an above-ceiling evidence
|
|
705
|
+
* item BEFORE the mission packet is built, so no above-ceiling file is reachable
|
|
706
|
+
* even inside a granted root. Uses `mission-dispatch.ts`'s
|
|
707
|
+
* `narrowMissionRootsToCeiling` over the FULL (pre-ceiling-filter) evidence, then
|
|
708
|
+
* drops the items in the denied roots. `buildOperatingMissionPackets` then derives
|
|
709
|
+
* the packet's `declaredRoots` (and thus its tool grant) from only the narrowed
|
|
710
|
+
* roots. This is the belt to the bounded reader's read-time-ceiling suspenders.
|
|
711
|
+
*/
|
|
712
|
+
function narrowEvidenceToMissionCeiling(evidence, ceiling) {
|
|
713
|
+
const topSegment = (location) => {
|
|
714
|
+
const [top] = location.split('/');
|
|
715
|
+
return top && top.length > 0 ? top : null;
|
|
716
|
+
};
|
|
717
|
+
const declaredRoots = [
|
|
718
|
+
...new Set(evidence.items
|
|
719
|
+
.map((item) => topSegment(item.location))
|
|
720
|
+
.filter((segment) => Boolean(segment))),
|
|
721
|
+
];
|
|
722
|
+
const evidenceIndex = evidence.items.map((item) => ({
|
|
723
|
+
path: item.location,
|
|
724
|
+
sensitivity: item.sensitivity,
|
|
725
|
+
}));
|
|
726
|
+
const allowedRoots = new Set(narrowMissionRootsToCeiling({ declaredRoots, evidenceIndex, ceiling }));
|
|
727
|
+
return {
|
|
728
|
+
...evidence,
|
|
729
|
+
items: evidence.items.filter((item) => {
|
|
730
|
+
const top = topSegment(item.location);
|
|
731
|
+
return top === null || allowedRoots.has(top);
|
|
732
|
+
}),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Resolve every requested role's effective dispatch mode for THIS runtime (FR1):
|
|
737
|
+
* the v1.3 registry default, overridden by the machine-local
|
|
738
|
+
* `dispatchModeOverrides`, reconciled against whether the runtime natively
|
|
739
|
+
* enforces the bounded read-only boundary. The adapter lifecycle IS the native
|
|
740
|
+
* runtime path, so the hosting adapter is native-capable by construction; the
|
|
741
|
+
* runtime's own enforceability then decides whether a role receives a native
|
|
742
|
+
* bounded mission lens (`resolution.native`) or falls closed to the v1.2 pack.
|
|
743
|
+
*/
|
|
744
|
+
async function resolveBoundRoleDispatchModes(input) {
|
|
745
|
+
const protocol = await loadOperatingProtocol();
|
|
746
|
+
const registryDefaults = new Map(protocol.listOperatingRoles().map((role) => [
|
|
747
|
+
role.id,
|
|
748
|
+
operatingRegistryDispatchMode(role),
|
|
749
|
+
]));
|
|
750
|
+
const overrides = await readOperatingDispatchModeOverrides(input.projectRoot, {
|
|
751
|
+
localRoot: input.localRoot,
|
|
752
|
+
});
|
|
753
|
+
const runtimeEnforcesBoundedReadOnly = await operatingRuntimeEnforcesBoundedReadOnly(input.runtime);
|
|
754
|
+
const resolved = new Map();
|
|
755
|
+
for (const roleId of input.roles) {
|
|
756
|
+
resolved.set(roleId, resolveOperatingDispatchMode({
|
|
757
|
+
roleId: roleId,
|
|
758
|
+
registryDefault: registryDefaults.get(roleId) ?? 'mission',
|
|
759
|
+
override: overrides[roleId],
|
|
760
|
+
runtimeEnforcesBoundedReadOnly,
|
|
761
|
+
adapterNativeCapable: true,
|
|
762
|
+
}));
|
|
763
|
+
}
|
|
764
|
+
return resolved;
|
|
765
|
+
}
|
|
596
766
|
export async function createOperatingAdapterStartHandoff(input) {
|
|
597
767
|
const target = adapterSessionPath(input.projectRoot, input.cycleId, input.localRoot);
|
|
598
768
|
const existing = await readFile(target, 'utf8')
|
|
@@ -618,7 +788,19 @@ export async function createOperatingAdapterStartHandoff(input) {
|
|
|
618
788
|
}
|
|
619
789
|
}
|
|
620
790
|
const protocol = await loadOperatingProtocol();
|
|
791
|
+
// FR1 call site 2 (the start / prepare-required handoff): stamp v1.3 whenever
|
|
792
|
+
// any bound role would resolve to native mission dispatch on this runtime, so
|
|
793
|
+
// the whole lifecycle — including the record actions built after prepare —
|
|
794
|
+
// carries the mission protocol version rather than defaulting to pack.
|
|
795
|
+
const dispatchModes = await resolveBoundRoleDispatchModes({
|
|
796
|
+
projectRoot: input.projectRoot,
|
|
797
|
+
localRoot: input.localRoot,
|
|
798
|
+
runtime: input.runtime,
|
|
799
|
+
roles: input.roles,
|
|
800
|
+
});
|
|
801
|
+
const anyMission = [...dispatchModes.values()].some((resolution) => resolution.native);
|
|
621
802
|
const handoff = protocol.createOperatingAdapterHandoff({
|
|
803
|
+
...(anyMission ? { protocolVersion: '1.3.0' } : {}),
|
|
622
804
|
phase: input.phase,
|
|
623
805
|
state: 'prepare-required',
|
|
624
806
|
cycleId: input.cycleId,
|
|
@@ -639,6 +821,13 @@ export async function operateAdapterLifecycle(input) {
|
|
|
639
821
|
if (!input.cycleId || !input.idempotencyKey) {
|
|
640
822
|
throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Adapter calls require --cycle-id and --idempotency-key.');
|
|
641
823
|
}
|
|
824
|
+
const nowMs = (input.now?.() ?? new Date()).getTime();
|
|
825
|
+
// The lease window is a machine-local preference (default 15 minutes). Resolved
|
|
826
|
+
// once so both the fresh `prepare` expiry and the per-`record` refresh use the
|
|
827
|
+
// same configured duration.
|
|
828
|
+
const leaseDurationMs = await readOperatingAdapterLeaseDurationMs(input.projectRoot, {
|
|
829
|
+
localRoot: input.localRoot,
|
|
830
|
+
});
|
|
642
831
|
const target = adapterSessionPath(input.projectRoot, input.cycleId, input.localRoot);
|
|
643
832
|
if (input.action === 'prepare') {
|
|
644
833
|
if (!input.evidenceDigest?.startsWith('sha256:')) {
|
|
@@ -648,6 +837,9 @@ export async function operateAdapterLifecycle(input) {
|
|
|
648
837
|
localRoot: input.localRoot,
|
|
649
838
|
});
|
|
650
839
|
const state = await store.state();
|
|
840
|
+
// FR4: the identity of the board that owns this prepare. A machine-local
|
|
841
|
+
// session bound to any other genesis belongs to a superseded generation.
|
|
842
|
+
const boardIdentity = await committedBoardIdentity(store);
|
|
651
843
|
const cycle = state.cycles.find((record) => record.id === input.cycleId);
|
|
652
844
|
if (!cycle || !['advising', 'blocked'].includes(cycle.state)) {
|
|
653
845
|
throw new OperateError('E_OPERATE_STATE_INVALID', 'Adapter prepare requires an advising or blocked cycle.');
|
|
@@ -686,36 +878,63 @@ export async function operateAdapterLifecycle(input) {
|
|
|
686
878
|
const existing = await readFile(target, 'utf8')
|
|
687
879
|
.then((raw) => JSON.parse(raw))
|
|
688
880
|
.catch(() => null);
|
|
881
|
+
// Recovery semantics (FR10 / T-008, behavior unchanged — documented here so
|
|
882
|
+
// the lease ergonomics read coherently): when a prior session for this cycle
|
|
883
|
+
// exists and its binding is an *exact* match (same cycle, evidence digest,
|
|
884
|
+
// runtime, phase, and role set) but it has lapsed or was cancelled, its
|
|
885
|
+
// deterministic role packs are reused rather than rebuilt. The reused packs
|
|
886
|
+
// carry the same per-role `inputDigest`, so any machine-local role result that
|
|
887
|
+
// still matches that digest is re-adopted below as already-recorded work — the
|
|
888
|
+
// lease is reissued fresh (new token + refreshed `expiresAt`) while the proven,
|
|
889
|
+
// digest-bound advisory output is preserved. A non-exact prior binding is never
|
|
890
|
+
// recovered; it fails closed above with `E_OPERATE_ADVISOR_ISOLATION`.
|
|
689
891
|
let recoverableSession = null;
|
|
690
892
|
if (existing) {
|
|
691
893
|
const normalized = {
|
|
692
894
|
...existing,
|
|
693
895
|
phase: existing.phase ?? adapterPhase(existing.roles ?? []),
|
|
694
896
|
runtime: existing.runtime ?? 'auto',
|
|
897
|
+
boardIdentity: existing.boardIdentity ?? '',
|
|
695
898
|
};
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
899
|
+
// FR4: a session bound to a superseded board generation (its genesis no
|
|
900
|
+
// longer matches the committed event chain) never blocks, matches, or is
|
|
901
|
+
// reused by the current board — it is silently superseded by the fresh
|
|
902
|
+
// session written below. This is what lets a board re-inited at the same
|
|
903
|
+
// path — whose CYCLE-NNN ordinal collides with a prior generation's
|
|
904
|
+
// finalized session — prepare cleanly instead of dead-ending on
|
|
905
|
+
// `E_OPERATE_ADVISOR_ISOLATION` / a whole-cycle cancel.
|
|
906
|
+
const sessionMatchesBoard = normalized.boardIdentity === boardIdentity;
|
|
907
|
+
if (sessionMatchesBoard) {
|
|
908
|
+
const exact = normalized.cycleId === input.cycleId &&
|
|
909
|
+
normalized.evidenceDigest === input.evidenceDigest &&
|
|
910
|
+
normalized.runtime === runtime &&
|
|
911
|
+
normalized.phase === phase &&
|
|
912
|
+
sameRoleSet(normalized.roles, roles);
|
|
913
|
+
if (normalized.idempotencyKey === input.idempotencyKey) {
|
|
914
|
+
if (!exact) {
|
|
915
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Idempotent adapter prepare does not match its original cycle, evidence, runtime, phase, or role binding.');
|
|
916
|
+
}
|
|
917
|
+
if (sessionExpired(normalized) || normalized.state === 'cancelled') {
|
|
918
|
+
throw new OperateError('E_OPERATE_ADVISOR_FAILED', 'The prepared adapter session is expired or cancelled; request a fresh CLI-owned handoff.', { recoveryCommand: retryRunCommand(normalized) });
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
...normalized,
|
|
922
|
+
leaseStatus: adapterLeaseStatus(normalized, nowMs),
|
|
923
|
+
handoff: await adapterHandoff(normalized),
|
|
924
|
+
};
|
|
704
925
|
}
|
|
705
|
-
if (sessionExpired(normalized)
|
|
706
|
-
throw new OperateError('
|
|
926
|
+
if (!sessionExpired(normalized) && ['prepared', 'recording'].includes(normalized.state)) {
|
|
927
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Cycle ${input.cycleId} already has an active adapter session with another binding.`, { recoveryCommand: retryRunCommand(normalized) });
|
|
928
|
+
}
|
|
929
|
+
// FR4: a finalized/expired/cancelled same-board session no longer forces
|
|
930
|
+
// a whole-cycle-cancelling error on a new compatible prepare. The
|
|
931
|
+
// advisors→chair continuation and a same-phase re-prepare both fall
|
|
932
|
+
// through and supersede the dead session; an exact dead session still
|
|
933
|
+
// recovers its already-built packs so recorded advisory work is reused
|
|
934
|
+
// rather than recomputed.
|
|
935
|
+
if (exact && (sessionExpired(normalized) || normalized.state === 'cancelled')) {
|
|
936
|
+
recoverableSession = normalized;
|
|
707
937
|
}
|
|
708
|
-
return { ...normalized, handoff: await adapterHandoff(normalized) };
|
|
709
|
-
}
|
|
710
|
-
if (!sessionExpired(normalized) && ['prepared', 'recording'].includes(normalized.state)) {
|
|
711
|
-
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Cycle ${input.cycleId} already has an active adapter session with another binding.`, { recoveryCommand: retryRunCommand(normalized) });
|
|
712
|
-
}
|
|
713
|
-
if (normalized.state === 'finalized' &&
|
|
714
|
-
!(normalized.phase === 'advisors' && phase === 'chair')) {
|
|
715
|
-
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Cycle ${input.cycleId} already finalized its ${normalized.phase} adapter phase.`, { recoveryCommand: retryRunCommand(normalized) });
|
|
716
|
-
}
|
|
717
|
-
if (exact && (sessionExpired(normalized) || normalized.state === 'cancelled')) {
|
|
718
|
-
recoverableSession = normalized;
|
|
719
938
|
}
|
|
720
939
|
}
|
|
721
940
|
const roleEvidence = roles[0] === 'chair'
|
|
@@ -741,8 +960,22 @@ export async function operateAdapterLifecycle(input) {
|
|
|
741
960
|
state,
|
|
742
961
|
cycleId: input.cycleId,
|
|
743
962
|
});
|
|
963
|
+
// FR1: resolve each requested role's dispatch mode for THIS runtime. A role
|
|
964
|
+
// that resolves to a native bounded lens (`resolution.native`) gets a
|
|
965
|
+
// body-free v1.3 mission packet; every other role keeps the v1.2 role pack.
|
|
966
|
+
// A recovered dead-but-exact session reuses its stored packs/packets verbatim.
|
|
967
|
+
const dispatchModes = await resolveBoundRoleDispatchModes({
|
|
968
|
+
projectRoot: input.projectRoot,
|
|
969
|
+
localRoot: input.localRoot,
|
|
970
|
+
runtime,
|
|
971
|
+
roles,
|
|
972
|
+
});
|
|
973
|
+
const missionRoleIds = readiness.roles
|
|
974
|
+
.map((role) => role.roleId)
|
|
975
|
+
.filter((roleId) => dispatchModes.get(roleId)?.native);
|
|
976
|
+
const packReadinessRoles = readiness.roles.filter((role) => !dispatchModes.get(role.roleId)?.native);
|
|
744
977
|
const rolePacks = recoverableSession?.rolePacks ??
|
|
745
|
-
Object.fromEntries(await Promise.all(
|
|
978
|
+
Object.fromEntries(await Promise.all(packReadinessRoles.map(async (role) => [
|
|
746
979
|
role.roleId,
|
|
747
980
|
await createOperatingAdvisorPack({
|
|
748
981
|
cycleId: input.cycleId,
|
|
@@ -751,7 +984,66 @@ export async function operateAdapterLifecycle(input) {
|
|
|
751
984
|
context,
|
|
752
985
|
}),
|
|
753
986
|
])));
|
|
754
|
-
|
|
987
|
+
// FR1: build the natively-dispatched roles' mission packets from live
|
|
988
|
+
// cycle/workspace state. Only reached when a role actually resolved mission,
|
|
989
|
+
// so a pure pack-mode prepare never loads config/workspace and stays
|
|
990
|
+
// byte-compatible with the v1.2 path. The evidence is ceiling-narrowed by
|
|
991
|
+
// `narrowMissionRootsToCeiling` first, so a declared read root can never reach
|
|
992
|
+
// an above-ceiling file even before the bounded reader's read-time check.
|
|
993
|
+
const roleMissionPackets = recoverableSession?.roleMissionPackets ??
|
|
994
|
+
(missionRoleIds.length > 0
|
|
995
|
+
? await (async () => {
|
|
996
|
+
const [config, workspace, sensitivityCeiling] = await Promise.all([
|
|
997
|
+
readOperatingConfig(input.projectRoot, { localRoot: input.localRoot }),
|
|
998
|
+
refreshOperatingWorkspaceManifest(input.projectRoot, { localRoot: input.localRoot }),
|
|
999
|
+
readAdapterSensitivityCeiling(input.projectRoot, input.localRoot),
|
|
1000
|
+
]);
|
|
1001
|
+
const packets = await buildOperatingMissionPackets({
|
|
1002
|
+
cycleId: input.cycleId,
|
|
1003
|
+
config,
|
|
1004
|
+
workspace,
|
|
1005
|
+
context,
|
|
1006
|
+
evidence: narrowEvidenceToMissionCeiling(roleEvidence, sensitivityCeiling),
|
|
1007
|
+
roleIds: missionRoleIds,
|
|
1008
|
+
sensitivityCeiling,
|
|
1009
|
+
});
|
|
1010
|
+
return Object.fromEntries(packets.map((packet) => [packet.roleId, packet]));
|
|
1011
|
+
})()
|
|
1012
|
+
: {});
|
|
1013
|
+
// FR2: fail closed before a native adapter session is assembled. A freshly
|
|
1014
|
+
// built pack is already measured inside `createOperatingAdvisorPack`, but the
|
|
1015
|
+
// session may instead be RECOVERED from disk (`recoverableSession`) — possibly
|
|
1016
|
+
// written before pack-budget enforcement existed. Re-measure every pack that
|
|
1017
|
+
// will back this session against its role's published v1.2 `maxInputBytes`, so
|
|
1018
|
+
// an oversized pack can never reach the adapter from either source instead of
|
|
1019
|
+
// silently constructing an oversized session. (Mission packets are measured
|
|
1020
|
+
// against their derived mission budget inside `buildOperatingMissionPackets`.)
|
|
1021
|
+
const packInputBudgets = new Map(protocol.listOperatingRoles().map((role) => {
|
|
1022
|
+
const maxInputBytes = role.budgets
|
|
1023
|
+
?.maxInputBytes;
|
|
1024
|
+
return [role.id, typeof maxInputBytes === 'number' ? maxInputBytes : 262_144];
|
|
1025
|
+
}));
|
|
1026
|
+
for (const [roleId, pack] of Object.entries(rolePacks)) {
|
|
1027
|
+
assertOperatingAdvisorPackWithinBudget(pack, packInputBudgets.get(roleId) ?? 262_144);
|
|
1028
|
+
}
|
|
1029
|
+
// A mission role's brief facet is the same registry-derived brief pack mode
|
|
1030
|
+
// uses, so the record path's brief-bound machinery (output limits, contract
|
|
1031
|
+
// details, output-vs-brief checks) applies uniformly across both modes.
|
|
1032
|
+
const roleBriefs = {
|
|
1033
|
+
...Object.fromEntries(Object.entries(rolePacks).map(([role, pack]) => [role, pack.roleBrief])),
|
|
1034
|
+
...Object.fromEntries(Object.keys(roleMissionPackets).map((role) => [
|
|
1035
|
+
role,
|
|
1036
|
+
protocol.createOperatingAdvisorBrief(role),
|
|
1037
|
+
])),
|
|
1038
|
+
};
|
|
1039
|
+
// A role's committed input digest is its pack's `inputDigest` (pack mode) or
|
|
1040
|
+
// its mission packet's `packetDigest` (mission mode). The record path binds a
|
|
1041
|
+
// recorded result to exactly this value, and a recovered machine-local result
|
|
1042
|
+
// is re-adopted only when it still matches it.
|
|
1043
|
+
const roleInputDigests = Object.fromEntries(roles.map((role) => {
|
|
1044
|
+
const digest = roleMissionPackets[role]?.packetDigest ?? rolePacks[role]?.inputDigest;
|
|
1045
|
+
return [role, digest];
|
|
1046
|
+
}));
|
|
755
1047
|
const validRecordedRoles = [];
|
|
756
1048
|
for (const role of roles) {
|
|
757
1049
|
const prior = await readFile(path.join(path.dirname(target), `${input.cycleId}.${role}.json`), 'utf8')
|
|
@@ -760,7 +1052,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
760
1052
|
if (prior &&
|
|
761
1053
|
prior.cycleId === input.cycleId &&
|
|
762
1054
|
prior.roleId === role &&
|
|
763
|
-
prior.inputDigest ===
|
|
1055
|
+
prior.inputDigest === roleInputDigests[role]) {
|
|
764
1056
|
try {
|
|
765
1057
|
await assertOperatingArtifact('operating-role-result', prior);
|
|
766
1058
|
protocol.validateOperatingRoleResultDigest(prior);
|
|
@@ -774,6 +1066,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
774
1066
|
}
|
|
775
1067
|
const session = {
|
|
776
1068
|
implementation: 'openplanr-operate-adapter',
|
|
1069
|
+
boardIdentity,
|
|
777
1070
|
cycleId: input.cycleId,
|
|
778
1071
|
evidenceDigest: input.evidenceDigest,
|
|
779
1072
|
phase,
|
|
@@ -781,20 +1074,31 @@ export async function operateAdapterLifecycle(input) {
|
|
|
781
1074
|
lease: randomBytes(32).toString('base64url'),
|
|
782
1075
|
idempotencyKey: input.idempotencyKey,
|
|
783
1076
|
state: validRecordedRoles.length > 0 ? 'recording' : 'prepared',
|
|
784
|
-
expiresAt: new Date(
|
|
1077
|
+
expiresAt: new Date(nowMs + leaseDurationMs).toISOString(),
|
|
785
1078
|
roles,
|
|
786
1079
|
recordedRoles: validRecordedRoles.sort(),
|
|
787
1080
|
roleBriefs,
|
|
788
1081
|
rolePacks,
|
|
789
|
-
|
|
1082
|
+
// Omitted entirely for a pure pack-mode session so its on-disk shape stays
|
|
1083
|
+
// byte-compatible with the v1.2 path.
|
|
1084
|
+
...(Object.keys(roleMissionPackets).length > 0 ? { roleMissionPackets } : {}),
|
|
1085
|
+
roleInputDigests,
|
|
790
1086
|
};
|
|
791
1087
|
await atomicPrivateWrite(target, session);
|
|
792
|
-
return {
|
|
1088
|
+
return {
|
|
1089
|
+
...session,
|
|
1090
|
+
// Expose the mission packets at `/data/missionPackets/<role>` so the v1.3
|
|
1091
|
+
// record action's `dispatch.missionPacketPointer` resolves against the
|
|
1092
|
+
// prepare result; pack roles keep resolving at `/data/rolePacks/<role>`.
|
|
1093
|
+
...(Object.keys(roleMissionPackets).length > 0 ? { missionPackets: roleMissionPackets } : {}),
|
|
1094
|
+
leaseStatus: adapterLeaseStatus(session, nowMs),
|
|
1095
|
+
handoff: await adapterHandoff(session),
|
|
1096
|
+
};
|
|
793
1097
|
}
|
|
794
1098
|
if (!input.lease) {
|
|
795
1099
|
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Adapter lease is required.');
|
|
796
1100
|
}
|
|
797
|
-
const session = await readAdapterSession(input.projectRoot, input.cycleId, input.localRoot);
|
|
1101
|
+
const session = await readAdapterSession(input.projectRoot, input.cycleId, input.localRoot, nowMs);
|
|
798
1102
|
assertAdapterBinding(session, input.lease, input.idempotencyKey, input.evidenceDigest);
|
|
799
1103
|
await assertAdapterCycleBinding({
|
|
800
1104
|
projectRoot: input.projectRoot,
|
|
@@ -805,7 +1109,11 @@ export async function operateAdapterLifecycle(input) {
|
|
|
805
1109
|
if (!['prepared', 'recording'].includes(session.state)) {
|
|
806
1110
|
throw new OperateError('E_OPERATE_STATE_INVALID', `Adapter resume is not valid after the session is ${session.state}.`, { recoveryCommand: retryRunCommand(session) });
|
|
807
1111
|
}
|
|
808
|
-
return {
|
|
1112
|
+
return {
|
|
1113
|
+
...session,
|
|
1114
|
+
leaseStatus: adapterLeaseStatus(session, nowMs),
|
|
1115
|
+
handoff: await adapterHandoff(session),
|
|
1116
|
+
};
|
|
809
1117
|
}
|
|
810
1118
|
if (input.action === 'cancel') {
|
|
811
1119
|
if (session.state === 'finalized') {
|
|
@@ -814,6 +1122,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
814
1122
|
if (session.state === 'cancelled') {
|
|
815
1123
|
return {
|
|
816
1124
|
session: adapterSessionSummary(session),
|
|
1125
|
+
leaseStatus: adapterLeaseStatus(session, nowMs),
|
|
817
1126
|
handoff: await adapterHandoff(session),
|
|
818
1127
|
};
|
|
819
1128
|
}
|
|
@@ -821,6 +1130,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
821
1130
|
await atomicPrivateWrite(target, cancelled);
|
|
822
1131
|
return {
|
|
823
1132
|
session: adapterSessionSummary(cancelled),
|
|
1133
|
+
leaseStatus: adapterLeaseStatus(cancelled, nowMs),
|
|
824
1134
|
handoff: await adapterHandoff(cancelled),
|
|
825
1135
|
};
|
|
826
1136
|
}
|
|
@@ -863,6 +1173,9 @@ export async function operateAdapterLifecycle(input) {
|
|
|
863
1173
|
}
|
|
864
1174
|
const submittedRecord = submitted && typeof submitted === 'object' ? submitted : {};
|
|
865
1175
|
let result;
|
|
1176
|
+
// FR1: unresolvable-citation gaps opened while resolving a mission response's
|
|
1177
|
+
// citations, surfaced in the record response for observability.
|
|
1178
|
+
let recordGaps = [];
|
|
866
1179
|
if (submittedRecord.kind === 'operating-role-result') {
|
|
867
1180
|
throw new OperateError('E_OPERATE_ADVISOR_FAILED', 'Native advisors must submit only the compact response contract; OpenPlanr owns canonical result metadata.', {
|
|
868
1181
|
...advisorResponseContractDetails(session.roleBriefs[input.role]),
|
|
@@ -891,12 +1204,50 @@ export async function operateAdapterLifecycle(input) {
|
|
|
891
1204
|
throw new OperateError('E_OPERATE_SECRET_DETECTED', `Native advisor response contains a secret at ${field.location}; nothing was persisted.`, { roleId: input.role, field: field.location });
|
|
892
1205
|
}
|
|
893
1206
|
}
|
|
1207
|
+
const missionPacket = session.roleMissionPackets?.[input.role];
|
|
894
1208
|
try {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1209
|
+
if (missionPacket) {
|
|
1210
|
+
// FR1: a mission-mode role submits the v1.3 citation-bearing response.
|
|
1211
|
+
// Its citations flow into the engine's already-live citation gate,
|
|
1212
|
+
// which resolves them against the cycle's pinned revision and mints the
|
|
1213
|
+
// evidenceRefs that make the committed result v1.2-valid — the pack
|
|
1214
|
+
// path is untouched.
|
|
1215
|
+
const gated = await createNativeMissionOperatingRoleResult({
|
|
1216
|
+
packet: missionPacket,
|
|
1217
|
+
response: submitted,
|
|
1218
|
+
runtime: session.runtime,
|
|
1219
|
+
resolveCitations: async (roleResults) => {
|
|
1220
|
+
const [workspace, sensitivityCeiling, config] = await Promise.all([
|
|
1221
|
+
refreshOperatingWorkspaceManifest(input.projectRoot, {
|
|
1222
|
+
localRoot: input.localRoot,
|
|
1223
|
+
}),
|
|
1224
|
+
readAdapterSensitivityCeiling(input.projectRoot, input.localRoot),
|
|
1225
|
+
readOperatingConfig(input.projectRoot, { localRoot: input.localRoot }).catch(() => null),
|
|
1226
|
+
]);
|
|
1227
|
+
return gateRecordedProposalCitations({
|
|
1228
|
+
roleResults,
|
|
1229
|
+
context: {
|
|
1230
|
+
projectRoot: input.projectRoot,
|
|
1231
|
+
cycleId: input.cycleId,
|
|
1232
|
+
descriptor: workspace.controlRepository,
|
|
1233
|
+
cache: new OperatingEvidenceCache(resolveOperatingPaths(input.projectRoot, {
|
|
1234
|
+
localRoot: input.localRoot,
|
|
1235
|
+
}).evidence, sensitivityCeiling),
|
|
1236
|
+
owner: config?.decisionOwner,
|
|
1237
|
+
},
|
|
1238
|
+
});
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1241
|
+
result = gated.result;
|
|
1242
|
+
recordGaps = gated.gaps;
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
result = await createNativeOperatingRoleResult({
|
|
1246
|
+
pack: session.rolePacks[input.role],
|
|
1247
|
+
response: submitted,
|
|
1248
|
+
runtime: session.runtime,
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
900
1251
|
}
|
|
901
1252
|
catch (error) {
|
|
902
1253
|
if (error instanceof OperateError && error.code === 'E_OPERATE_ADVISOR_FAILED') {
|
|
@@ -958,16 +1309,23 @@ export async function operateAdapterLifecycle(input) {
|
|
|
958
1309
|
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Role ${input.role} already recorded a different result.`);
|
|
959
1310
|
}
|
|
960
1311
|
await atomicPrivateWrite(path.join(path.dirname(target), `${input.cycleId}.${input.role}.json`), result);
|
|
1312
|
+
// A successful record refreshes the lease forward from now (FR10 / T-008):
|
|
1313
|
+
// an advisor making steady progress across a multi-role dispatch keeps its
|
|
1314
|
+
// session alive without a separate keep-alive call, while a session that goes
|
|
1315
|
+
// idle past the refreshed window still lapses and is rejected on the next call.
|
|
961
1316
|
const updated = {
|
|
962
1317
|
...session,
|
|
963
1318
|
state: 'recording',
|
|
964
1319
|
recordedRoles: [...new Set([...session.recordedRoles, input.role])].sort(),
|
|
1320
|
+
expiresAt: new Date(nowMs + leaseDurationMs).toISOString(),
|
|
965
1321
|
};
|
|
966
1322
|
await atomicPrivateWrite(target, updated);
|
|
967
1323
|
return {
|
|
968
1324
|
recorded: input.role,
|
|
969
1325
|
result,
|
|
1326
|
+
...(recordGaps.length > 0 ? { citationGaps: recordGaps } : {}),
|
|
970
1327
|
session: adapterSessionSummary(updated),
|
|
1328
|
+
leaseStatus: adapterLeaseStatus(updated, nowMs),
|
|
971
1329
|
handoff: await adapterHandoff(updated),
|
|
972
1330
|
};
|
|
973
1331
|
}
|
|
@@ -987,6 +1345,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
987
1345
|
return {
|
|
988
1346
|
session: adapterSessionSummary(session),
|
|
989
1347
|
results: summaries,
|
|
1348
|
+
leaseStatus: adapterLeaseStatus(session, nowMs),
|
|
990
1349
|
handoff: await adapterHandoff(session),
|
|
991
1350
|
};
|
|
992
1351
|
}
|
|
@@ -1058,6 +1417,7 @@ export async function operateAdapterLifecycle(input) {
|
|
|
1058
1417
|
inputDigest: result.inputDigest,
|
|
1059
1418
|
resultDigest: result.resultDigest,
|
|
1060
1419
|
})),
|
|
1420
|
+
leaseStatus: adapterLeaseStatus(finalized, nowMs),
|
|
1061
1421
|
handoff: await adapterHandoff(finalized),
|
|
1062
1422
|
};
|
|
1063
1423
|
}
|