openplanr 1.21.2 → 1.22.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/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +11 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/operate.d.ts.map +1 -1
- package/dist/cli/commands/operate.js +91 -2
- package/dist/cli/commands/operate.js.map +1 -1
- package/dist/services/operate/advisors.d.ts +63 -0
- package/dist/services/operate/advisors.d.ts.map +1 -1
- package/dist/services/operate/advisors.js +68 -4
- package/dist/services/operate/advisors.js.map +1 -1
- package/dist/services/operate/artifacts.d.ts +9 -3
- package/dist/services/operate/artifacts.d.ts.map +1 -1
- package/dist/services/operate/artifacts.js +126 -5
- package/dist/services/operate/artifacts.js.map +1 -1
- package/dist/services/operate/citation-resolution.d.ts +14 -0
- package/dist/services/operate/citation-resolution.d.ts.map +1 -1
- package/dist/services/operate/citation-resolution.js +9 -0
- package/dist/services/operate/citation-resolution.js.map +1 -1
- package/dist/services/operate/completion.d.ts +80 -0
- package/dist/services/operate/completion.d.ts.map +1 -0
- package/dist/services/operate/completion.js +279 -0
- package/dist/services/operate/completion.js.map +1 -0
- package/dist/services/operate/config.d.ts +7 -0
- package/dist/services/operate/config.d.ts.map +1 -1
- package/dist/services/operate/config.js +7 -0
- package/dist/services/operate/config.js.map +1 -1
- package/dist/services/operate/context-research.d.ts +43 -0
- package/dist/services/operate/context-research.d.ts.map +1 -1
- package/dist/services/operate/context-research.js +219 -1
- package/dist/services/operate/context-research.js.map +1 -1
- package/dist/services/operate/decision-brief.d.ts +90 -1
- package/dist/services/operate/decision-brief.d.ts.map +1 -1
- package/dist/services/operate/decision-brief.js +175 -1
- package/dist/services/operate/decision-brief.js.map +1 -1
- package/dist/services/operate/doctor.d.ts +58 -0
- package/dist/services/operate/doctor.d.ts.map +1 -1
- package/dist/services/operate/doctor.js +395 -21
- package/dist/services/operate/doctor.js.map +1 -1
- package/dist/services/operate/engine.d.ts +75 -2
- package/dist/services/operate/engine.d.ts.map +1 -1
- package/dist/services/operate/engine.js +504 -33
- package/dist/services/operate/engine.js.map +1 -1
- package/dist/services/operate/event-store.d.ts +22 -0
- package/dist/services/operate/event-store.d.ts.map +1 -1
- package/dist/services/operate/event-store.js +35 -0
- package/dist/services/operate/event-store.js.map +1 -1
- package/dist/services/operate/evidence-cache.d.ts.map +1 -1
- package/dist/services/operate/evidence-cache.js +41 -5
- package/dist/services/operate/evidence-cache.js.map +1 -1
- package/dist/services/operate/index.d.ts.map +1 -1
- package/dist/services/operate/index.js +98 -1
- package/dist/services/operate/index.js.map +1 -1
- package/dist/services/operate/interaction/question-engine.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-engine.js +36 -8
- package/dist/services/operate/interaction/question-engine.js.map +1 -1
- package/dist/services/operate/interaction/question-registry.d.ts +8 -0
- package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-registry.js +21 -2
- package/dist/services/operate/interaction/question-registry.js.map +1 -1
- package/dist/services/operate/lifecycle-driver.d.ts +309 -0
- package/dist/services/operate/lifecycle-driver.d.ts.map +1 -0
- package/dist/services/operate/lifecycle-driver.js +434 -0
- package/dist/services/operate/lifecycle-driver.js.map +1 -0
- package/dist/services/operate/maintenance.d.ts +65 -1
- package/dist/services/operate/maintenance.d.ts.map +1 -1
- package/dist/services/operate/maintenance.js +718 -24
- package/dist/services/operate/maintenance.js.map +1 -1
- package/dist/services/operate/mission-dispatch.d.ts +61 -1
- package/dist/services/operate/mission-dispatch.d.ts.map +1 -1
- package/dist/services/operate/mission-dispatch.js +78 -5
- package/dist/services/operate/mission-dispatch.js.map +1 -1
- package/dist/services/operate/profile-migration.d.ts +69 -0
- package/dist/services/operate/profile-migration.d.ts.map +1 -0
- package/dist/services/operate/profile-migration.js +462 -0
- package/dist/services/operate/profile-migration.js.map +1 -0
- package/dist/services/operate/projection-persistence.d.ts.map +1 -1
- package/dist/services/operate/projection-persistence.js +24 -5
- 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 +9 -0
- package/dist/services/operate/projection.js.map +1 -1
- package/dist/services/operate/protocol.d.ts +1 -0
- package/dist/services/operate/protocol.d.ts.map +1 -1
- package/dist/services/operate/protocol.js.map +1 -1
- package/dist/services/operate/scratch.d.ts +81 -0
- package/dist/services/operate/scratch.d.ts.map +1 -0
- package/dist/services/operate/scratch.js +190 -0
- package/dist/services/operate/scratch.js.map +1 -0
- package/dist/services/operate/types.d.ts +3 -2
- 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 +1 -0
- package/dist/services/operate/workspace.d.ts.map +1 -1
- package/dist/services/operate/workspace.js +2 -0
- package/dist/services/operate/workspace.js.map +1 -1
- package/dist/services/runtime-manager-service.d.ts +23 -0
- package/dist/services/runtime-manager-service.d.ts.map +1 -1
- package/dist/services/runtime-manager-service.js +30 -0
- package/dist/services/runtime-manager-service.js.map +1 -1
- package/package.json +2 -2
|
@@ -3,18 +3,21 @@ import { mkdir, readFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { OPENPLANR_VERSION } from '../../utils/package-version.js';
|
|
5
5
|
import { loadConfig } from '../config-service.js';
|
|
6
|
-
import { advisorFailureGaps, buildAdvisorOperatingContext, configuredAdvisorProviderPolicy, createConfiguredStructuredAdapter, createOfflineAdvisorAdapter, dispatchOperatingAdvisors, ensureOperatingProviderConsent, } from './advisors.js';
|
|
6
|
+
import { advisorFailureGaps, buildAdvisorOperatingContext, configuredAdvisorProviderPolicy, createConfiguredStructuredAdapter, createOfflineAdvisorAdapter, DEFAULT_OPERATING_DISPATCH_CONCURRENCY, dispatchOperatingAdvisors, ensureOperatingProviderConsent, } from './advisors.js';
|
|
7
7
|
import { recordOperatingCadenceRun } from './cadence.js';
|
|
8
8
|
import { canonicalDigest, canonicalize } from './canonical.js';
|
|
9
9
|
import { operatingProjectKey, validateOperatingConfiguration } from './config.js';
|
|
10
10
|
import { consolidateOperatingResults } from './consolidation.js';
|
|
11
|
-
import {
|
|
11
|
+
import { chairBoardGapLines, classifyChairRoleContributions, } from './decision-brief.js';
|
|
12
|
+
import { OperatingEventStore, toPersistedDataGap, } from './event-store.js';
|
|
12
13
|
import { evidenceProjectionSources } from './evidence.js';
|
|
13
14
|
import { OperatingEvidenceCache } from './evidence-cache.js';
|
|
14
15
|
import { nextCycleId } from './ids.js';
|
|
16
|
+
import { buildOperatingIntegritySummary, } from './integrity.js';
|
|
15
17
|
import { enforceRecordedProposalCitations } from './interaction/action-service.js';
|
|
16
18
|
import { applyJournalTransaction, prepareJournalTransaction, readJournal, rollbackJournalTransaction, } from './journal.js';
|
|
17
19
|
import { reconcileOperatingDecisionDeadlines } from './lifecycle.js';
|
|
20
|
+
import { AdvisorLifecycleDriver, } from './lifecycle-driver.js';
|
|
18
21
|
import { withOperatingLock } from './lock-service.js';
|
|
19
22
|
import { reconcileOperatingOutcomeFiles } from './outcomes.js';
|
|
20
23
|
import { persistOperatingProjections } from './projection-persistence.js';
|
|
@@ -107,11 +110,28 @@ function requiredOpenEvidenceRefs(state) {
|
|
|
107
110
|
: [])),
|
|
108
111
|
].sort();
|
|
109
112
|
}
|
|
110
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The Chair's grounded input assembled from the board (FR13). Each recorded role
|
|
115
|
+
* contributes its real analysis as synthetic advisor-result evidence, labelled
|
|
116
|
+
* with its six-way outcome. Citation-rejected proposals are EXCLUDED from the
|
|
117
|
+
* grounded input (never silently dropped — they are named in the gap list), so a
|
|
118
|
+
* proposal Chair could not have grounded can never reach its synthesis. Roles
|
|
119
|
+
* with no valid analysis (not-evaluated, failed, still-running, all-rejected)
|
|
120
|
+
* contribute no proposal item: their absence is surfaced as a gap, never as a
|
|
121
|
+
* fabricated conclusion.
|
|
122
|
+
*/
|
|
123
|
+
export function buildChairEvidence(evidence, results, now, context = {}) {
|
|
124
|
+
const contributions = classifyChairRoleContributions(results, context);
|
|
125
|
+
const outcomeByRole = new Map(contributions.map((entry) => [entry.roleId, entry.outcome]));
|
|
126
|
+
const excludedByRole = new Map(contributions.map((entry) => [entry.roleId, new Set(entry.excludedProposalKeys)]));
|
|
111
127
|
const evidenceById = new Map(evidence.items.map((item) => [item.id, item]));
|
|
112
128
|
const synthetic = results.flatMap((result) => {
|
|
129
|
+
const excluded = excludedByRole.get(result.roleId) ?? new Set();
|
|
130
|
+
// Grounded proposals only: a citation-rejected proposal is excluded here so it
|
|
131
|
+
// never becomes Chair-citable evidence, while `chairBoardGapLines` names it.
|
|
132
|
+
const groundedProposals = result.proposals.filter((proposal) => !excluded.has(proposal.proposalKey));
|
|
113
133
|
const evidenceRefs = [
|
|
114
|
-
...new Set(
|
|
134
|
+
...new Set(groundedProposals.flatMap((proposal) => proposal.evidenceRefs)),
|
|
115
135
|
].sort();
|
|
116
136
|
const missingRefs = evidenceRefs.filter((reference) => !evidenceById.has(reference));
|
|
117
137
|
if (missingRefs.length > 0) {
|
|
@@ -127,12 +147,13 @@ export function buildChairEvidence(evidence, results, now) {
|
|
|
127
147
|
location: `${result.roleId}/context`,
|
|
128
148
|
body: {
|
|
129
149
|
roleId: result.roleId,
|
|
130
|
-
outcome: result.outcome,
|
|
150
|
+
outcome: outcomeByRole.get(result.roleId) ?? result.outcome,
|
|
131
151
|
gaps: [...new Set(result.gaps)].sort(),
|
|
132
152
|
conflicts: [...new Set(result.conflicts)].sort(),
|
|
153
|
+
...(excluded.size > 0 ? { citationRejectedProposals: [...excluded].sort() } : {}),
|
|
133
154
|
},
|
|
134
155
|
},
|
|
135
|
-
...
|
|
156
|
+
...groundedProposals.map((proposal) => ({
|
|
136
157
|
id: `EVD-advisor-results-${result.roleId}-${proposal.proposalKey}`,
|
|
137
158
|
location: `${result.roleId}/proposals/${proposal.proposalKey}`,
|
|
138
159
|
body: {
|
|
@@ -166,6 +187,21 @@ export function buildChairEvidence(evidence, results, now) {
|
|
|
166
187
|
}),
|
|
167
188
|
};
|
|
168
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Assemble the Chair's input from a possibly-partial board (FR13). This is the
|
|
192
|
+
* single entry point that ties the grounded evidence to the honest per-role
|
|
193
|
+
* ledger and the named-gap list: one missing perspective can never destroy an
|
|
194
|
+
* otherwise-useful board, and the Chair is handed a structural record of what did
|
|
195
|
+
* NOT evaluate rather than a silent absence it might synthesize around.
|
|
196
|
+
*/
|
|
197
|
+
export function assembleChairBoardInput(evidence, results, now, context = {}) {
|
|
198
|
+
const contributions = classifyChairRoleContributions(results, context);
|
|
199
|
+
return {
|
|
200
|
+
evidence: buildChairEvidence(evidence, results, now, context),
|
|
201
|
+
contributions,
|
|
202
|
+
gaps: chairBoardGapLines(contributions),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
169
205
|
/**
|
|
170
206
|
* FR1/FR2 (isolation rebind): the cycle's pinned-revision evidence seed. Evidence
|
|
171
207
|
* is an OUTPUT now, so a fresh cycle no longer collects a body-bearing snapshot
|
|
@@ -511,7 +547,39 @@ export async function gateRecordedProposalCitations(input) {
|
|
|
511
547
|
const enforcement = await enforceRecordedProposalCitations(bearing, input.context);
|
|
512
548
|
const rejectedKeys = new Set(enforcement.rejected.map((entry) => entry.proposalKey));
|
|
513
549
|
const evidenceByKey = new Map(enforcement.accepted.map((entry) => [entry.proposal.proposalKey, entry.evidenceRefs]));
|
|
514
|
-
|
|
550
|
+
// FR13 (T-017): name the role whose proposal each unresolvable-citation gap
|
|
551
|
+
// rejected, so the integrity summary links the citation rejection to that role.
|
|
552
|
+
// Without it the gap carries no `affectedRoles`, the Chair board cannot tell a
|
|
553
|
+
// fully-rejected role's `citation-rejected` outcome apart from a plain
|
|
554
|
+
// not-evaluated absence, and the integrity report's "Affected:" line is blank.
|
|
555
|
+
const roleByProposalKey = new Map();
|
|
556
|
+
for (const result of input.roleResults) {
|
|
557
|
+
for (const proposal of result.proposals) {
|
|
558
|
+
roleByProposalKey.set(proposal.proposalKey, result.roleId);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const rolesByGapId = new Map();
|
|
562
|
+
for (const entry of enforcement.rejected) {
|
|
563
|
+
const roleId = roleByProposalKey.get(entry.proposalKey);
|
|
564
|
+
if (!roleId)
|
|
565
|
+
continue;
|
|
566
|
+
const roles = rolesByGapId.get(entry.gapId) ?? new Set();
|
|
567
|
+
roles.add(roleId);
|
|
568
|
+
rolesByGapId.set(entry.gapId, roles);
|
|
569
|
+
}
|
|
570
|
+
const gaps = enforcement.gaps.map((gap) => {
|
|
571
|
+
const roles = rolesByGapId.get(String(gap.id));
|
|
572
|
+
if (!roles || roles.size === 0)
|
|
573
|
+
return gap;
|
|
574
|
+
const record = gap;
|
|
575
|
+
const existing = Array.isArray(record.affectedRoles)
|
|
576
|
+
? record.affectedRoles
|
|
577
|
+
: [];
|
|
578
|
+
return {
|
|
579
|
+
...record,
|
|
580
|
+
affectedRoles: [...new Set([...existing, ...roles])].sort(),
|
|
581
|
+
};
|
|
582
|
+
});
|
|
515
583
|
const notEvaluatedRoleIds = [];
|
|
516
584
|
const now = input.context.now ?? new Date();
|
|
517
585
|
const roleResults = await Promise.all(input.roleResults.map(async (result) => {
|
|
@@ -543,6 +611,39 @@ export async function gateRecordedProposalCitations(input) {
|
|
|
543
611
|
}));
|
|
544
612
|
return { roleResults, gaps, notEvaluatedRoleIds };
|
|
545
613
|
}
|
|
614
|
+
/**
|
|
615
|
+
* FR3/FR13 (SPEC-005 T-015): mint the governed data gap persisted for a lens the
|
|
616
|
+
* lifecycle driver terminated `not_evaluated` — a stalled lens that exhausted its
|
|
617
|
+
* bounded retry budget. It carries the driver's REAL governed reason and links
|
|
618
|
+
* the role so `status`/`report` render a governed gap rather than a silent
|
|
619
|
+
* absence. It is shaped as a v1.2.0 `operating-data-gap` (no `category`) so it
|
|
620
|
+
* validates as a `data-gap` record, exactly as `readinessGaps` gaps do; the
|
|
621
|
+
* Chair board's `not-evaluated` CLASSIFICATION is sourced separately, directly
|
|
622
|
+
* from the driver's in-memory governed gaps, so it needs no v1.3 category here.
|
|
623
|
+
* The digest-derived id keeps it canonical and idempotent across resume.
|
|
624
|
+
*/
|
|
625
|
+
function driverNotEvaluatedGap(gap, cycleId, owner, now) {
|
|
626
|
+
return {
|
|
627
|
+
kind: 'operating-data-gap',
|
|
628
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
629
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
630
|
+
id: `GAP-${canonicalDigest({
|
|
631
|
+
roleId: gap.roleId,
|
|
632
|
+
cycleId,
|
|
633
|
+
reason: 'lifecycle-not-evaluated',
|
|
634
|
+
}).slice('sha256:'.length)}`,
|
|
635
|
+
cycleId,
|
|
636
|
+
question: `Why did ${gap.roleId} not evaluate this cycle?`,
|
|
637
|
+
reason: gap.reason,
|
|
638
|
+
unblocks: [],
|
|
639
|
+
affectedRoles: [gap.roleId],
|
|
640
|
+
status: 'open',
|
|
641
|
+
owner: owner && owner.length > 0 ? owner : 'chair',
|
|
642
|
+
evidenceRefs: [],
|
|
643
|
+
createdAt: now,
|
|
644
|
+
updatedAt: now,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
546
647
|
export async function runOperatingCycle(input) {
|
|
547
648
|
const projectRoot = await assertOperatingProject(input.projectRoot);
|
|
548
649
|
const config = await validateOperatingConfiguration(projectRoot);
|
|
@@ -763,7 +864,23 @@ export async function runOperatingCycle(input) {
|
|
|
763
864
|
};
|
|
764
865
|
const existingByRole = new Map(existingRoleResults.map((result) => [result.roleId, result]));
|
|
765
866
|
const missingNonChair = readiness.roles.filter((role) => role.roleId !== 'chair' && !existingByRole.has(role.roleId));
|
|
766
|
-
|
|
867
|
+
// SPEC-005 T-020 (FR13): a non-chair lens with NO committed result that is
|
|
868
|
+
// nonetheless named by an open governed cycle gap was terminated
|
|
869
|
+
// `not_evaluated` — a runtime `harness abandon`, or the operator escape after a
|
|
870
|
+
// stall. It is terminal, not runnable: exclude it so a genuinely stalled lens
|
|
871
|
+
// no longer strands the cycle at `phase: advisors` forever, exactly as a
|
|
872
|
+
// recorded lens does not block it. On the adapter-lifecycle path the only
|
|
873
|
+
// committed cycle gaps that name a lens with no result are these terminal ones
|
|
874
|
+
// (the citation gate's gaps always co-occur with a recorded result, handled by
|
|
875
|
+
// `existingByRole`; readiness gaps are never persisted on this path), so this
|
|
876
|
+
// never suppresses a still-pending lens — a pending lens has no naming gap yet.
|
|
877
|
+
const terminatedNonChair = new Set(missingNonChair
|
|
878
|
+
.filter((role) => initialState.dataGaps.some((gap) => (gap.cycleId === undefined || gap.cycleId === cycle.id) &&
|
|
879
|
+
!['closed', 'superseded'].includes(String(gap.status)) &&
|
|
880
|
+
Array.isArray(gap.affectedRoles) &&
|
|
881
|
+
gap.affectedRoles.includes(role.roleId)))
|
|
882
|
+
.map((role) => role.roleId));
|
|
883
|
+
const runnableMissingNonChair = missingNonChair.filter((role) => role.modelCallAllowed && !terminatedNonChair.has(role.roleId));
|
|
767
884
|
const skippedMissingNonChair = missingNonChair
|
|
768
885
|
.filter((role) => !role.modelCallAllowed || role.readiness === 'not_evaluated')
|
|
769
886
|
.map((role) => ({
|
|
@@ -795,27 +912,182 @@ export async function runOperatingCycle(input) {
|
|
|
795
912
|
};
|
|
796
913
|
}
|
|
797
914
|
await assertCycleWorkspace();
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
915
|
+
// FR3 (SPEC-005 T-015): drive the non-chair advisor fan-out through the
|
|
916
|
+
// AdvisorLifecycleDriver (T-003) rather than a single blocking dispatch. The
|
|
917
|
+
// driver owns the per-role lifecycle around the runtime dispatch — bounded
|
|
918
|
+
// retry, per-attempt timeout, and automatic lease heartbeat — and never
|
|
919
|
+
// dispatches an agent itself: its `dispatch` callback IS the runtime dispatch
|
|
920
|
+
// (a single-role `dispatchOperatingAdvisors`) and `recordRole` commits the
|
|
921
|
+
// role's built result. A stalled lens is retried within its budget then
|
|
922
|
+
// terminated `not_evaluated` with a governed gap while its siblings keep
|
|
923
|
+
// recording and the lease keeps renewing — none of which the pre-driver
|
|
924
|
+
// single-call fan-out could survive, because one hung role deadlocked the
|
|
925
|
+
// whole `Promise.all`. A healthy board records identically to before.
|
|
926
|
+
const lifecycleTuning = input.advisorLifecycle ?? {};
|
|
927
|
+
const driveNonChairFanOut = async () => {
|
|
928
|
+
if (runnableMissingNonChair.length === 0) {
|
|
929
|
+
return {
|
|
930
|
+
dispatch: {
|
|
931
|
+
results: [],
|
|
932
|
+
provenance: [],
|
|
933
|
+
modelCalls: 0,
|
|
934
|
+
blocked: false,
|
|
935
|
+
skipped: skippedMissingNonChair,
|
|
936
|
+
failed: [],
|
|
937
|
+
gaps: [],
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
const adapter = await resolveAdapter();
|
|
942
|
+
const protocol = await loadOperatingProtocol();
|
|
943
|
+
// Registry order so the aggregated per-role results are byte-identical to
|
|
944
|
+
// the single-call dispatch's canonically-ordered result set (FR4).
|
|
945
|
+
const registryOrder = new Map(protocol.listOperatingRoles().map((role, index) => [
|
|
946
|
+
role.id,
|
|
947
|
+
index,
|
|
948
|
+
]));
|
|
949
|
+
const readinessByRole = new Map(readiness.roles.map((role) => [role.roleId, role]));
|
|
950
|
+
const recordedResults = new Map();
|
|
951
|
+
const provenanceByRole = new Map();
|
|
952
|
+
const aggregatedGaps = [];
|
|
953
|
+
// Roles whose single-role dispatch produced a genuine FAILURE (an adapter
|
|
954
|
+
// error), as distinct from a STALL (a per-attempt timeout with no dispatch
|
|
955
|
+
// error). A genuine failure is preserved as `failed` — the pre-driver
|
|
956
|
+
// semantics: an `X failed` warning plus an `advisorFailureGaps` gap — while
|
|
957
|
+
// only a true stall becomes a governed not_evaluated gap (FR3).
|
|
958
|
+
const failedEntries = [];
|
|
959
|
+
let aggregatedModelCalls = 0;
|
|
960
|
+
let aggregatedBlocked = false;
|
|
961
|
+
const clock = lifecycleTuning.now ?? (() => Date.now());
|
|
962
|
+
// The engine-managed cycle lease window (FR2). Defaults to the documented
|
|
963
|
+
// 15-minute session lease; the driver's heartbeat renews it independent of
|
|
964
|
+
// any role completing, so a slow lens cannot expire recorded work.
|
|
965
|
+
const leaseWindowMs = lifecycleTuning.leaseWindowMs ?? 900_000;
|
|
966
|
+
let leaseExpiresAtMs = clock() + leaseWindowMs;
|
|
967
|
+
const plans = runnableMissingNonChair.map((role) => ({
|
|
968
|
+
roleId: role.roleId,
|
|
969
|
+
// Every advisory lens is structurally OPTIONAL here: a stalled lens
|
|
970
|
+
// terminates not_evaluated with a governed gap and the cycle continues to
|
|
971
|
+
// Chair (FR3/FR13); nothing in the board is structurally required.
|
|
972
|
+
retryBudget: lifecycleTuning.retryBudget ?? 0,
|
|
973
|
+
}));
|
|
974
|
+
const driver = new AdvisorLifecycleDriver({
|
|
975
|
+
roles: plans,
|
|
976
|
+
now: clock,
|
|
977
|
+
...(lifecycleTuning.setTimer ? { setTimer: lifecycleTuning.setTimer } : {}),
|
|
978
|
+
...(lifecycleTuning.clearTimer ? { clearTimer: lifecycleTuning.clearTimer } : {}),
|
|
979
|
+
...(lifecycleTuning.roleTimeoutMs !== undefined
|
|
980
|
+
? { roleTimeoutMs: lifecycleTuning.roleTimeoutMs }
|
|
981
|
+
: {}),
|
|
982
|
+
...(lifecycleTuning.heartbeatLeadMs !== undefined
|
|
983
|
+
? { heartbeatLeadMs: lifecycleTuning.heartbeatLeadMs }
|
|
984
|
+
: {}),
|
|
985
|
+
leaseExpiresAtMs,
|
|
986
|
+
concurrency: DEFAULT_OPERATING_DISPATCH_CONCURRENCY,
|
|
987
|
+
heartbeat: async () => {
|
|
988
|
+
leaseExpiresAtMs = clock() + leaseWindowMs;
|
|
989
|
+
return { leaseExpiresAtMs };
|
|
990
|
+
},
|
|
991
|
+
recordRole: async ({ roleId, output }) => {
|
|
992
|
+
recordedResults.set(roleId, output);
|
|
993
|
+
leaseExpiresAtMs = clock() + leaseWindowMs;
|
|
994
|
+
return { recorded: true, leaseExpiresAtMs };
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
const dispatchOne = async (roleId) => {
|
|
998
|
+
const roleReadiness = readinessByRole.get(roleId);
|
|
999
|
+
if (!roleReadiness) {
|
|
1000
|
+
throw new OperateError('E_OPERATE_STATE_INVALID', `No dispatch readiness for advisor role ${roleId}.`);
|
|
1001
|
+
}
|
|
1002
|
+
const single = await dispatchOperatingAdvisors({
|
|
1003
|
+
cycleId: cycle.id,
|
|
1004
|
+
projectRoot,
|
|
1005
|
+
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
1006
|
+
readiness: { ...readiness, roles: [roleReadiness] },
|
|
1007
|
+
context: advisorContext,
|
|
1008
|
+
adapter,
|
|
1009
|
+
depth,
|
|
1010
|
+
runtime: resolvedRuntime,
|
|
1011
|
+
resolveCitations,
|
|
1012
|
+
});
|
|
1013
|
+
aggregatedModelCalls += single.modelCalls;
|
|
1014
|
+
for (const entry of single.provenance)
|
|
1015
|
+
provenanceByRole.set(entry.roleId, entry);
|
|
1016
|
+
aggregatedGaps.push(...single.gaps);
|
|
1017
|
+
if (single.blocked)
|
|
1018
|
+
aggregatedBlocked = true;
|
|
1019
|
+
const built = single.results.find((result) => result.roleId === roleId);
|
|
1020
|
+
if (built)
|
|
1021
|
+
return built;
|
|
1022
|
+
// The lens dispatch errored. Record it as a genuine failure (preserved as
|
|
1023
|
+
// `failed` below) and surface it to the driver, which terminates the role.
|
|
1024
|
+
// A HANG instead never reaches here: the driver's per-attempt timeout
|
|
1025
|
+
// rejects `dispatchOne` from outside, so a timeout is classified a stall
|
|
1026
|
+
// (not a failure) and becomes a governed not_evaluated gap. The driver
|
|
1027
|
+
// dispatches nothing itself — this callback IS the dispatch.
|
|
1028
|
+
const failure = single.failed.find((entry) => entry.roleId === roleId);
|
|
1029
|
+
failedEntries.push({
|
|
1030
|
+
roleId,
|
|
1031
|
+
message: failure?.message ?? `Advisor role ${roleId} produced no committed result.`,
|
|
1032
|
+
});
|
|
1033
|
+
throw new OperateError('E_OPERATE_ADVISOR_FAILED', failure?.message ?? `Advisor role ${roleId} produced no committed result.`);
|
|
1034
|
+
};
|
|
1035
|
+
const heartbeatIntervalMs = lifecycleTuning.heartbeatIntervalMs ?? 60_000;
|
|
1036
|
+
const stopHeartbeat = driver.scheduleHeartbeat(heartbeatIntervalMs);
|
|
1037
|
+
try {
|
|
1038
|
+
await driver.run({
|
|
1039
|
+
parallel: Boolean(adapter.parallelDispatch),
|
|
1040
|
+
dispatch: (roleId) => dispatchOne(roleId),
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
finally {
|
|
1044
|
+
stopHeartbeat();
|
|
1045
|
+
}
|
|
1046
|
+
const governedGaps = driver.governedGaps();
|
|
1047
|
+
const failedRoleIds = new Set(failedEntries.map((entry) => entry.roleId));
|
|
1048
|
+
// Only a TRUE stall (a timeout, never a dispatch failure) becomes a governed
|
|
1049
|
+
// not_evaluated gap; a role the driver terminated after a genuine dispatch
|
|
1050
|
+
// error is preserved as `failed` and drops out of the stall set.
|
|
1051
|
+
const stallGaps = governedGaps.filter((gap) => gap.outcome === 'not_evaluated' && !failedRoleIds.has(gap.roleId));
|
|
1052
|
+
const results = [...recordedResults.values()].sort((left, right) => (registryOrder.get(left.roleId) ?? Number.MAX_SAFE_INTEGER) -
|
|
1053
|
+
(registryOrder.get(right.roleId) ?? Number.MAX_SAFE_INTEGER));
|
|
1054
|
+
const provenance = results
|
|
1055
|
+
.map((result) => provenanceByRole.get(result.roleId))
|
|
1056
|
+
.filter((entry) => entry !== undefined);
|
|
1057
|
+
// Genuine dispatch failures preserved with the pre-driver semantics, plus any
|
|
1058
|
+
// structurally-required lens the driver terminated `failed` (none on this
|
|
1059
|
+
// path, where every lens is optional).
|
|
1060
|
+
const failed = [
|
|
1061
|
+
...failedEntries,
|
|
1062
|
+
...governedGaps
|
|
1063
|
+
.filter((gap) => gap.outcome === 'failed')
|
|
1064
|
+
.map((gap) => ({ roleId: gap.roleId, message: gap.reason })),
|
|
1065
|
+
];
|
|
1066
|
+
return {
|
|
1067
|
+
dispatch: {
|
|
1068
|
+
results,
|
|
1069
|
+
provenance,
|
|
1070
|
+
modelCalls: aggregatedModelCalls,
|
|
1071
|
+
blocked: aggregatedBlocked,
|
|
1072
|
+
skipped: skippedMissingNonChair,
|
|
1073
|
+
failed,
|
|
1074
|
+
gaps: [...aggregatedGaps],
|
|
1075
|
+
},
|
|
1076
|
+
lifecycle: {
|
|
1077
|
+
snapshot: driver.snapshot(),
|
|
1078
|
+
// Only the genuine stalls — a failed lens is surfaced through `failed`,
|
|
1079
|
+
// not as a not_evaluated governed gap.
|
|
1080
|
+
gaps: stallGaps,
|
|
1081
|
+
chairReady: driver.chairReady(),
|
|
1082
|
+
heartbeats: driver.heartbeatsIssued(),
|
|
1083
|
+
recorded: results.map((result) => result.roleId),
|
|
1084
|
+
notEvaluated: stallGaps.map((gap) => gap.roleId),
|
|
1085
|
+
},
|
|
818
1086
|
};
|
|
1087
|
+
};
|
|
1088
|
+
const fanOut = await driveNonChairFanOut();
|
|
1089
|
+
const first = fanOut.dispatch;
|
|
1090
|
+
const advisorLifecycleSummary = fanOut.lifecycle;
|
|
819
1091
|
let roleResults = [
|
|
820
1092
|
...existingRoleResults.filter((result) => result.roleId !== 'chair'),
|
|
821
1093
|
...first.results,
|
|
@@ -829,6 +1101,7 @@ export async function runOperatingCycle(input) {
|
|
|
829
1101
|
// citations (unresolvable-citation, empty-grounding) are recorded alongside
|
|
830
1102
|
// the cycle's other gaps.
|
|
831
1103
|
const dispatchGaps = [...first.gaps];
|
|
1104
|
+
let chairBoard;
|
|
832
1105
|
if (config.enabledRoles.includes('chair')) {
|
|
833
1106
|
// FR1: the Chair dispatches its own mandate and investigates independently —
|
|
834
1107
|
// it no longer receives a synthesized advisor-results evidence body. Its
|
|
@@ -880,6 +1153,174 @@ export async function runOperatingCycle(input) {
|
|
|
880
1153
|
},
|
|
881
1154
|
};
|
|
882
1155
|
}
|
|
1156
|
+
// FR13 (SPEC-005 T-015): assemble the Chair's grounded input from the
|
|
1157
|
+
// partial, honestly-labelled non-chair board with T-004's assembler, then
|
|
1158
|
+
// feed its contributions and named-gap lines into the Chair mandate. The
|
|
1159
|
+
// integrity summary (built from the cycle's committed + dispatch gaps), the
|
|
1160
|
+
// dispatch `failed` list, and the citation gate's rejected proposal keys are
|
|
1161
|
+
// threaded in so every not-evaluated / failed / still-running /
|
|
1162
|
+
// citation-rejected lens is surfaced to the Chair as a gap it must NOT
|
|
1163
|
+
// synthesize around. Absent lenses contribute ZERO proposal items to the
|
|
1164
|
+
// grounded evidence by construction — the assembler only synthesizes from
|
|
1165
|
+
// roles that recorded a result.
|
|
1166
|
+
const nonChairResults = roleResults.filter((result) => result.roleId !== 'chair');
|
|
1167
|
+
// Citation rejections / boundary refusals come from the cycle's committed +
|
|
1168
|
+
// dispatch gaps (category-bearing v1.3 gaps read in-memory). The stalled
|
|
1169
|
+
// lenses' not_evaluated classification is sourced directly from the driver's
|
|
1170
|
+
// in-memory governed gaps — its authoritative record of what did NOT
|
|
1171
|
+
// evaluate and WHY — rather than from a persisted category, so the Chair
|
|
1172
|
+
// sees every absent perspective with its real reason.
|
|
1173
|
+
const baseIntegrity = buildOperatingIntegritySummary({
|
|
1174
|
+
...initialState,
|
|
1175
|
+
dataGaps: [...initialState.dataGaps, ...dispatchGaps],
|
|
1176
|
+
}, cycle.id);
|
|
1177
|
+
const stallNotEvaluatedRoles = (advisorLifecycleSummary?.gaps ?? []).map((gap) => ({
|
|
1178
|
+
roleId: gap.roleId,
|
|
1179
|
+
gapId: `GAP-lifecycle-${gap.roleId}`,
|
|
1180
|
+
reason: gap.reason,
|
|
1181
|
+
detail: gap.reason,
|
|
1182
|
+
}));
|
|
1183
|
+
// SPEC-005 T-019 (FR5/FR13): reconstruct the citation-rejection signal for the
|
|
1184
|
+
// agent-native adapter-lifecycle path. On that path a fully citation-rejected
|
|
1185
|
+
// lens commits a quiet result in a PRIOR `run` (via `operateAdapterLifecycle`'s
|
|
1186
|
+
// record step), which persists the citation gate's governed gaps
|
|
1187
|
+
// (`maintenance.ts`). On THIS consolidation continuation the role is already
|
|
1188
|
+
// recorded, so it is never re-dispatched and its category-bearing v1.3 gaps are
|
|
1189
|
+
// never rebuilt in memory — `dispatchGaps` is empty. The persisted gaps carry
|
|
1190
|
+
// no `category` (the v1.2 append-log projection T-017 uses drops it, and the
|
|
1191
|
+
// reducer projects it away regardless), so `buildOperatingIntegritySummary`
|
|
1192
|
+
// cannot re-derive the signal from committed state, and the lens would fall
|
|
1193
|
+
// through to a false-clean `recorded-quiet`.
|
|
1194
|
+
//
|
|
1195
|
+
// Carry T-017's signal across that boundary using the SAME co-occurring
|
|
1196
|
+
// structural fact its classifier keys on — never the gap's prose: a lens that
|
|
1197
|
+
// recorded a result grounding ZERO proposals AND is named by a governed cycle
|
|
1198
|
+
// gap was citation-rejected (the citation gate is the only producer of a
|
|
1199
|
+
// role-affecting gap at record time on this path). Layer those as
|
|
1200
|
+
// `citationRejections` onto the Chair integrity exactly as the stall
|
|
1201
|
+
// not_evaluated roles are layered above, skipping any role `baseIntegrity`
|
|
1202
|
+
// already classified (so the inline path — whose in-memory `dispatchGaps` still
|
|
1203
|
+
// carry `category` — is untouched and T-017's inline proof stays green).
|
|
1204
|
+
const recordedZeroGroundedRoles = new Set(nonChairResults
|
|
1205
|
+
.filter((result) => result.proposals.length === 0)
|
|
1206
|
+
.map((result) => result.roleId));
|
|
1207
|
+
const alreadyClassifiedRoles = new Set([
|
|
1208
|
+
...baseIntegrity.citationRejections.flatMap((entry) => entry.affectedRoles),
|
|
1209
|
+
...baseIntegrity.boundaryRefusals.flatMap((entry) => entry.affectedRoles),
|
|
1210
|
+
...baseIntegrity.notEvaluatedRoles.map((role) => role.roleId),
|
|
1211
|
+
...stallNotEvaluatedRoles.map((role) => role.roleId),
|
|
1212
|
+
]);
|
|
1213
|
+
const nativeCitationRejections = [];
|
|
1214
|
+
for (const roleId of [...recordedZeroGroundedRoles].sort()) {
|
|
1215
|
+
if (alreadyClassifiedRoles.has(roleId))
|
|
1216
|
+
continue;
|
|
1217
|
+
const namingGap = initialState.dataGaps
|
|
1218
|
+
.filter((gap) => (gap.cycleId === undefined || gap.cycleId === cycle.id) &&
|
|
1219
|
+
Array.isArray(gap.affectedRoles) &&
|
|
1220
|
+
gap.affectedRoles.includes(roleId))
|
|
1221
|
+
.sort((left, right) => String(left.id).localeCompare(String(right.id)))[0];
|
|
1222
|
+
if (!namingGap)
|
|
1223
|
+
continue;
|
|
1224
|
+
const reason = String(namingGap.reason ?? '').trim();
|
|
1225
|
+
const detail = String(namingGap.question ?? '').trim();
|
|
1226
|
+
nativeCitationRejections.push({
|
|
1227
|
+
gapId: String(namingGap.id),
|
|
1228
|
+
reason: reason || 'A cited location could not be resolved to evidence.',
|
|
1229
|
+
detail: detail || 'Every citation this role returned failed to resolve to evidence.',
|
|
1230
|
+
affectedRoles: [roleId],
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
// SPEC-005 T-020 (FR13): a lens dispatched but that never returned — a true
|
|
1234
|
+
// stall, terminated via `harness abandon` (runtime) or the operator escape —
|
|
1235
|
+
// commits NO result at all, only a governed terminal gap (`maintenance.ts`
|
|
1236
|
+
// `governTerminalNotEvaluated`). It is the inverse of T-019's citation-rejected
|
|
1237
|
+
// case (which DID record a zero-grounded result), so it is reconstructed the
|
|
1238
|
+
// same structural way, keyed on the same committed-state fact and never gap
|
|
1239
|
+
// prose: a non-chair enabled lens with NO committed result that is named by an
|
|
1240
|
+
// open governed cycle gap is `not_evaluated`, carrying that gap's reason and
|
|
1241
|
+
// id. This keeps it a distinct `not-evaluated` outcome — never citation-rejected
|
|
1242
|
+
// (which requires a recorded result), never a fabricated conclusion — so the
|
|
1243
|
+
// Chair names it as an explicit gap it must not synthesize around, and it
|
|
1244
|
+
// contributes zero grounded proposal items by construction (it recorded none).
|
|
1245
|
+
const recordedNonChairRoles = new Set(nonChairResults.map((result) => result.roleId));
|
|
1246
|
+
const nativeNotEvaluatedRoles = [];
|
|
1247
|
+
for (const roleId of [...nonChair].sort()) {
|
|
1248
|
+
if (recordedNonChairRoles.has(roleId))
|
|
1249
|
+
continue;
|
|
1250
|
+
if (alreadyClassifiedRoles.has(roleId))
|
|
1251
|
+
continue;
|
|
1252
|
+
const namingGap = initialState.dataGaps
|
|
1253
|
+
.filter((gap) => (gap.cycleId === undefined || gap.cycleId === cycle.id) &&
|
|
1254
|
+
!['closed', 'superseded'].includes(String(gap.status)) &&
|
|
1255
|
+
Array.isArray(gap.affectedRoles) &&
|
|
1256
|
+
gap.affectedRoles.includes(roleId))
|
|
1257
|
+
.sort((left, right) => String(left.id).localeCompare(String(right.id)))[0];
|
|
1258
|
+
if (!namingGap)
|
|
1259
|
+
continue;
|
|
1260
|
+
const reason = String(namingGap.reason ?? '').trim();
|
|
1261
|
+
const detail = String(namingGap.question ?? '').trim();
|
|
1262
|
+
nativeNotEvaluatedRoles.push({
|
|
1263
|
+
roleId,
|
|
1264
|
+
gapId: String(namingGap.id),
|
|
1265
|
+
reason: reason || 'The lens was dispatched but never recorded a result.',
|
|
1266
|
+
detail: detail ||
|
|
1267
|
+
'The lens was dispatched but never recorded a result; it is recorded not_evaluated.',
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
const chairIntegrity = {
|
|
1271
|
+
...baseIntegrity,
|
|
1272
|
+
citationRejections: [...baseIntegrity.citationRejections, ...nativeCitationRejections],
|
|
1273
|
+
notEvaluatedRoles: [
|
|
1274
|
+
...baseIntegrity.notEvaluatedRoles,
|
|
1275
|
+
...stallNotEvaluatedRoles,
|
|
1276
|
+
...nativeNotEvaluatedRoles,
|
|
1277
|
+
],
|
|
1278
|
+
hasConcerns: baseIntegrity.hasConcerns ||
|
|
1279
|
+
stallNotEvaluatedRoles.length > 0 ||
|
|
1280
|
+
nativeCitationRejections.length > 0 ||
|
|
1281
|
+
nativeNotEvaluatedRoles.length > 0,
|
|
1282
|
+
};
|
|
1283
|
+
// The grounded evidence-of-record the recorded proposals cite: read back
|
|
1284
|
+
// from the citation snapshots the dispatch-time gate minted, so
|
|
1285
|
+
// `buildChairEvidence` resolves every recorded proposal's evidenceRefs
|
|
1286
|
+
// instead of the body-free pinned-revision seed (whose `items` are empty).
|
|
1287
|
+
const chairGroundedEvidence = await assembleCitationEvidence({
|
|
1288
|
+
cycleId: cycle.id,
|
|
1289
|
+
roleResults: nonChairResults,
|
|
1290
|
+
cache: citationCache,
|
|
1291
|
+
baseFingerprint: evidence.fingerprint,
|
|
1292
|
+
now: timestamp,
|
|
1293
|
+
});
|
|
1294
|
+
chairBoard = assembleChairBoardInput(chairGroundedEvidence, nonChairResults, timestamp, {
|
|
1295
|
+
expectedRoles: nonChair,
|
|
1296
|
+
integrity: chairIntegrity,
|
|
1297
|
+
failedReasons: Object.fromEntries(failed.map((entry) => [entry.roleId, entry.message])),
|
|
1298
|
+
// Recorded results are already citation-gated at dispatch time, so no
|
|
1299
|
+
// recorded proposal is outstanding-rejected here; a role whose proposals
|
|
1300
|
+
// were all rejected committed not_evaluated and is classified via the
|
|
1301
|
+
// integrity summary's missing-evidence gaps above.
|
|
1302
|
+
rejectedProposalKeys: {},
|
|
1303
|
+
});
|
|
1304
|
+
// Layer the named-gap lines onto the Chair's context so a real runtime agent
|
|
1305
|
+
// sees each absent perspective explicitly. The offline/test adapter ignores
|
|
1306
|
+
// context, so the machine-checked proof of the wiring is the exposed
|
|
1307
|
+
// `chairBoard`; the Chair still investigates independently and receives no
|
|
1308
|
+
// synthesized advisor conclusions — only the honest record of what did NOT
|
|
1309
|
+
// evaluate, which it must surface as a gap rather than fabricate around.
|
|
1310
|
+
const chairAdvisorContext = {
|
|
1311
|
+
...advisorContext,
|
|
1312
|
+
openGaps: [
|
|
1313
|
+
...advisorContext.openGaps,
|
|
1314
|
+
...chairBoard.gaps.map((line, index) => ({
|
|
1315
|
+
id: `BOARD-GAP-${String(index + 1).padStart(3, '0')}`,
|
|
1316
|
+
status: 'open',
|
|
1317
|
+
summary: line,
|
|
1318
|
+
owner: null,
|
|
1319
|
+
evidenceRefs: [],
|
|
1320
|
+
affectedRoles: ['chair'],
|
|
1321
|
+
})),
|
|
1322
|
+
],
|
|
1323
|
+
};
|
|
883
1324
|
const chair = persistedChair
|
|
884
1325
|
? {
|
|
885
1326
|
results: [persistedChair],
|
|
@@ -895,7 +1336,7 @@ export async function runOperatingCycle(input) {
|
|
|
895
1336
|
projectRoot,
|
|
896
1337
|
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
897
1338
|
readiness: chairReadiness,
|
|
898
|
-
context:
|
|
1339
|
+
context: chairAdvisorContext,
|
|
899
1340
|
adapter: await resolveAdapter(),
|
|
900
1341
|
depth,
|
|
901
1342
|
runtime: resolvedRuntime,
|
|
@@ -949,6 +1390,12 @@ export async function runOperatingCycle(input) {
|
|
|
949
1390
|
owner: config.decisionOwner,
|
|
950
1391
|
now: timestamp,
|
|
951
1392
|
});
|
|
1393
|
+
// FR3 (SPEC-005 T-015): the governed data gaps for lenses the lifecycle driver
|
|
1394
|
+
// terminated not_evaluated (a stall past the retry budget). They join the
|
|
1395
|
+
// consolidation gap set so `remapConsolidation` mints their canonical `GAP-NNN`
|
|
1396
|
+
// ids alongside every other cycle gap, then persist and render like any other
|
|
1397
|
+
// governed gap — so `status`/`report` show a stalled lens honestly.
|
|
1398
|
+
const lifecycleNotEvaluatedGaps = (advisorLifecycleSummary?.gaps ?? []).map((gap) => driverNotEvaluatedGap(gap, cycle.id, config.decisionOwner, timestamp));
|
|
952
1399
|
const surfacedDedup = suppressRepeatedFindings(initialState, consolidated.findings, cycle.id);
|
|
953
1400
|
const overflowDedup = suppressRepeatedFindings(initialState, consolidated.criticalOverflow, cycle.id);
|
|
954
1401
|
const parkedDedup = suppressRepeatedFindings(initialState, consolidated.parked, cycle.id);
|
|
@@ -963,7 +1410,7 @@ export async function runOperatingCycle(input) {
|
|
|
963
1410
|
.filter((role) => role.gapId)
|
|
964
1411
|
.map((role) => ({ id: role.gapId, status: 'open' })),
|
|
965
1412
|
],
|
|
966
|
-
}, [...surfacedDedup.findings, ...overflowDedup.findings], parkedDedup.findings, consolidated.decisions, [...evidenceGaps, ...consolidated.gaps, ...failureGaps]);
|
|
1413
|
+
}, [...surfacedDedup.findings, ...overflowDedup.findings], parkedDedup.findings, consolidated.decisions, [...evidenceGaps, ...consolidated.gaps, ...failureGaps, ...lifecycleNotEvaluatedGaps]);
|
|
967
1414
|
const warnings = [
|
|
968
1415
|
...evidence.warnings,
|
|
969
1416
|
...(evidenceGaps.length > 0
|
|
@@ -1018,6 +1465,8 @@ export async function runOperatingCycle(input) {
|
|
|
1018
1465
|
modelCalls,
|
|
1019
1466
|
warnings,
|
|
1020
1467
|
dispatchProvenance,
|
|
1468
|
+
...(advisorLifecycleSummary ? { advisorLifecycle: advisorLifecycleSummary } : {}),
|
|
1469
|
+
...(chairBoard ? { chairBoard } : {}),
|
|
1021
1470
|
};
|
|
1022
1471
|
};
|
|
1023
1472
|
if (input.dryRun)
|
|
@@ -1102,7 +1551,19 @@ export async function runOperatingCycle(input) {
|
|
|
1102
1551
|
state,
|
|
1103
1552
|
revalidateEventHead: async () => (await store.replay()).eventHead,
|
|
1104
1553
|
});
|
|
1105
|
-
|
|
1554
|
+
// FR5 (SPEC-005 T-002): the status-facing summary this native handoff
|
|
1555
|
+
// returns reflects LIVE persisted role results — every lens the harness
|
|
1556
|
+
// has already committed to the canonical event log, re-read here from the
|
|
1557
|
+
// freshest replay rather than the call-entry snapshot. Combined with the
|
|
1558
|
+
// harness recording each role's event the moment it validates (not only
|
|
1559
|
+
// at finalize), a `run` re-entered mid fan-out reports exactly the roles
|
|
1560
|
+
// that are durably recorded, never a stale "nothing recorded yet" view.
|
|
1561
|
+
const livePersistedRoleResults = await persistedCycleRoleResults(store, (await store.replay()).events, cycle.id);
|
|
1562
|
+
return {
|
|
1563
|
+
...evaluated,
|
|
1564
|
+
roleResults: livePersistedRoleResults.filter((result) => result.roleId !== 'chair'),
|
|
1565
|
+
state,
|
|
1566
|
+
};
|
|
1106
1567
|
}
|
|
1107
1568
|
if (phase === 'advising') {
|
|
1108
1569
|
const recordedRoles = new Set(existingRoleResults.map((result) => result.roleId));
|
|
@@ -1124,8 +1585,18 @@ export async function runOperatingCycle(input) {
|
|
|
1124
1585
|
head = appended.head;
|
|
1125
1586
|
}
|
|
1126
1587
|
const existingGapIds = new Set(initialState.dataGaps.filter((gap) => gap.cycleId === cycle.id).map((gap) => gap.id));
|
|
1127
|
-
|
|
1128
|
-
|
|
1588
|
+
// FR2/FR13 (T-017): a citation-rejection / empty-grounding gap is a
|
|
1589
|
+
// Protocol v1.3 gap in memory (category + digest id), where the Chair
|
|
1590
|
+
// board reads its category to classify the role. The append log and the
|
|
1591
|
+
// gap.open/operating-record schemas only admit a v1.2 data gap, so
|
|
1592
|
+
// persist each gap in its v1.2 projection (canonical `GAP-NNN` id, no
|
|
1593
|
+
// v1.3-only fields) — lossless for replayed state, which drops category
|
|
1594
|
+
// regardless. Seed the ordinal past every canonical id already present in
|
|
1595
|
+
// committed state and this batch so a minted id never collides.
|
|
1596
|
+
let gapOrdinal = maximumOrdinal([...initialState.dataGaps, ...(evaluated.gaps ?? [])], 'GAP');
|
|
1597
|
+
for (const rawGap of evaluated.gaps ?? []) {
|
|
1598
|
+
const gap = toPersistedDataGap(rawGap, () => `GAP-${String(++gapOrdinal).padStart(3, '0')}`);
|
|
1599
|
+
if (existingGapIds.has(rawGap.id) || existingGapIds.has(gap.id))
|
|
1129
1600
|
continue;
|
|
1130
1601
|
await assertOperatingArtifact('operating-data-gap', gap);
|
|
1131
1602
|
await store.putRecord('data-gap', recordContent(gap), {
|