openplanr 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/backlog.d.ts.map +1 -1
- package/dist/cli/commands/backlog.js +3 -0
- package/dist/cli/commands/backlog.js.map +1 -1
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/config.js +3 -0
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/epic.d.ts.map +1 -1
- package/dist/cli/commands/epic.js +2 -0
- package/dist/cli/commands/epic.js.map +1 -1
- package/dist/cli/commands/feature.d.ts.map +1 -1
- package/dist/cli/commands/feature.js +2 -0
- package/dist/cli/commands/feature.js.map +1 -1
- package/dist/cli/commands/operate.d.ts +5 -2
- package/dist/cli/commands/operate.d.ts.map +1 -1
- package/dist/cli/commands/operate.js +10 -101
- package/dist/cli/commands/operate.js.map +1 -1
- package/dist/cli/commands/plan.d.ts.map +1 -1
- package/dist/cli/commands/plan.js +2 -0
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/quick.d.ts.map +1 -1
- package/dist/cli/commands/quick.js +2 -0
- package/dist/cli/commands/quick.js.map +1 -1
- package/dist/cli/commands/refine.d.ts.map +1 -1
- package/dist/cli/commands/refine.js +2 -0
- package/dist/cli/commands/refine.js.map +1 -1
- package/dist/cli/commands/revise.d.ts.map +1 -1
- package/dist/cli/commands/revise.js +2 -0
- package/dist/cli/commands/revise.js.map +1 -1
- package/dist/cli/commands/spec.d.ts.map +1 -1
- package/dist/cli/commands/spec.js +2 -0
- package/dist/cli/commands/spec.js.map +1 -1
- package/dist/cli/commands/sprint.d.ts.map +1 -1
- package/dist/cli/commands/sprint.js +2 -0
- package/dist/cli/commands/sprint.js.map +1 -1
- package/dist/cli/commands/story.d.ts.map +1 -1
- package/dist/cli/commands/story.js +3 -0
- package/dist/cli/commands/story.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +3 -0
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/services/ai-service.d.ts +3 -5
- package/dist/services/ai-service.d.ts.map +1 -1
- package/dist/services/ai-service.js +7 -1
- package/dist/services/ai-service.js.map +1 -1
- package/dist/services/credentials-service.d.ts.map +1 -1
- package/dist/services/credentials-service.js +2 -0
- package/dist/services/credentials-service.js.map +1 -1
- package/dist/services/deprecation-notices.d.ts +5 -0
- package/dist/services/deprecation-notices.d.ts.map +1 -0
- package/dist/services/deprecation-notices.js +6 -0
- package/dist/services/deprecation-notices.js.map +1 -0
- package/dist/services/operate/advisors.d.ts +144 -166
- package/dist/services/operate/advisors.d.ts.map +1 -1
- package/dist/services/operate/advisors.js +178 -543
- package/dist/services/operate/advisors.js.map +1 -1
- package/dist/services/operate/citation-resolution.d.ts.map +1 -1
- package/dist/services/operate/citation-resolution.js +50 -1
- package/dist/services/operate/citation-resolution.js.map +1 -1
- package/dist/services/operate/config.d.ts +18 -52
- package/dist/services/operate/config.d.ts.map +1 -1
- package/dist/services/operate/config.js +24 -206
- package/dist/services/operate/config.js.map +1 -1
- package/dist/services/operate/doctor.d.ts +15 -0
- package/dist/services/operate/doctor.d.ts.map +1 -1
- package/dist/services/operate/doctor.js +142 -32
- package/dist/services/operate/doctor.js.map +1 -1
- package/dist/services/operate/engine.d.ts +22 -78
- package/dist/services/operate/engine.d.ts.map +1 -1
- package/dist/services/operate/engine.js +269 -202
- package/dist/services/operate/engine.js.map +1 -1
- package/dist/services/operate/evidence.d.ts +9 -107
- package/dist/services/operate/evidence.d.ts.map +1 -1
- package/dist/services/operate/evidence.js +0 -1403
- package/dist/services/operate/evidence.js.map +1 -1
- package/dist/services/operate/index.d.ts.map +1 -1
- package/dist/services/operate/index.js +10 -231
- package/dist/services/operate/index.js.map +1 -1
- package/dist/services/operate/integrity.d.ts +81 -0
- package/dist/services/operate/integrity.d.ts.map +1 -0
- package/dist/services/operate/integrity.js +152 -0
- package/dist/services/operate/integrity.js.map +1 -0
- package/dist/services/operate/interaction/answer-service.d.ts +0 -10
- package/dist/services/operate/interaction/answer-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/answer-service.js +63 -43
- package/dist/services/operate/interaction/answer-service.js.map +1 -1
- package/dist/services/operate/interaction/question-engine.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-engine.js +105 -21
- package/dist/services/operate/interaction/question-engine.js.map +1 -1
- package/dist/services/operate/interaction/question-registry.d.ts +16 -1
- package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-registry.js +45 -39
- package/dist/services/operate/interaction/question-registry.js.map +1 -1
- package/dist/services/operate/interaction/session-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/session-service.js +16 -11
- package/dist/services/operate/interaction/session-service.js.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.d.ts.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.js +16 -6
- package/dist/services/operate/interaction/terminal-renderer.js.map +1 -1
- package/dist/services/operate/maintenance.d.ts +1 -11
- package/dist/services/operate/maintenance.d.ts.map +1 -1
- package/dist/services/operate/maintenance.js +94 -342
- package/dist/services/operate/maintenance.js.map +1 -1
- package/dist/services/operate/mission-dispatch.d.ts +73 -55
- package/dist/services/operate/mission-dispatch.d.ts.map +1 -1
- package/dist/services/operate/mission-dispatch.js +91 -67
- package/dist/services/operate/mission-dispatch.js.map +1 -1
- package/dist/services/operate/projection-persistence.d.ts.map +1 -1
- package/dist/services/operate/projection-persistence.js +22 -2
- package/dist/services/operate/projection-persistence.js.map +1 -1
- package/dist/services/operate/projection.d.ts.map +1 -1
- package/dist/services/operate/projection.js +10 -3
- package/dist/services/operate/projection.js.map +1 -1
- package/dist/services/operate/protocol.d.ts +1 -41
- package/dist/services/operate/protocol.d.ts.map +1 -1
- package/dist/services/operate/protocol.js +7 -45
- package/dist/services/operate/protocol.js.map +1 -1
- package/dist/services/operate/redaction.d.ts +3 -3
- package/dist/services/operate/redaction.js +4 -4
- package/dist/services/operate/redaction.js.map +1 -1
- package/dist/services/operate/reports.d.ts +11 -1
- package/dist/services/operate/reports.d.ts.map +1 -1
- package/dist/services/operate/reports.js +88 -3
- package/dist/services/operate/reports.js.map +1 -1
- package/dist/services/operate/types.d.ts +42 -128
- package/dist/services/operate/types.d.ts.map +1 -1
- package/dist/services/operate/types.js.map +1 -1
- package/dist/services/operate/workspace.d.ts +0 -1
- package/dist/services/operate/workspace.d.ts.map +1 -1
- package/dist/services/operate/workspace.js +0 -2
- package/dist/services/operate/workspace.js.map +1 -1
- package/dist/services/runtime-manager-service.js +2 -2
- package/dist/services/runtime-manager-service.js.map +1 -1
- package/package.json +3 -3
- package/dist/services/operate/evidence-classifications.d.ts +0 -59
- package/dist/services/operate/evidence-classifications.d.ts.map +0 -1
- package/dist/services/operate/evidence-classifications.js +0 -132
- package/dist/services/operate/evidence-classifications.js.map +0 -1
- package/dist/services/operate/evidence-diagnostics.d.ts +0 -23
- package/dist/services/operate/evidence-diagnostics.d.ts.map +0 -1
- package/dist/services/operate/evidence-diagnostics.js +0 -152
- package/dist/services/operate/evidence-diagnostics.js.map +0 -1
- package/dist/services/operate/evidence-readiness.d.ts +0 -18
- package/dist/services/operate/evidence-readiness.d.ts.map +0 -1
- package/dist/services/operate/evidence-readiness.js +0 -94
- package/dist/services/operate/evidence-readiness.js.map +0 -1
|
@@ -3,15 +3,14 @@ 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,
|
|
6
|
+
import { advisorFailureGaps, buildAdvisorOperatingContext, configuredAdvisorProviderPolicy, createConfiguredStructuredAdapter, createOfflineAdvisorAdapter, dispatchOperatingAdvisors, ensureOperatingProviderConsent, } from './advisors.js';
|
|
7
7
|
import { recordOperatingCadenceRun } from './cadence.js';
|
|
8
8
|
import { canonicalDigest, canonicalize } from './canonical.js';
|
|
9
|
-
import { operatingProjectKey,
|
|
9
|
+
import { operatingProjectKey, validateOperatingConfiguration } from './config.js';
|
|
10
10
|
import { consolidateOperatingResults } from './consolidation.js';
|
|
11
11
|
import { OperatingEventStore } from './event-store.js';
|
|
12
|
-
import {
|
|
12
|
+
import { evidenceProjectionSources } from './evidence.js';
|
|
13
13
|
import { OperatingEvidenceCache } from './evidence-cache.js';
|
|
14
|
-
import { evaluateEvidenceReadiness } from './evidence-readiness.js';
|
|
15
14
|
import { nextCycleId } from './ids.js';
|
|
16
15
|
import { enforceRecordedProposalCitations } from './interaction/action-service.js';
|
|
17
16
|
import { applyJournalTransaction, prepareJournalTransaction, readJournal, rollbackJournalTransaction, } from './journal.js';
|
|
@@ -34,121 +33,6 @@ function normalizeFocus(focus) {
|
|
|
34
33
|
function loadJson(target) {
|
|
35
34
|
return readFile(target, 'utf8').then((raw) => JSON.parse(raw));
|
|
36
35
|
}
|
|
37
|
-
const MISSION_REVISION_PATTERN = /^[a-f0-9]{7,64}$/;
|
|
38
|
-
const MISSION_CYCLE_PATTERN = /^CYCLE-[0-9]{3,}$/;
|
|
39
|
-
const MISSION_DECISION_PATTERN = /^DEC-[0-9]{3,}$/;
|
|
40
|
-
const MISSION_GAP_PATTERN = /^GAP-[A-Za-z0-9._-]+$/;
|
|
41
|
-
const MISSION_OUTCOME_PATTERN = /^OUT-[0-9]{3,}$/;
|
|
42
|
-
function boundedMissionText(value, maximum, fallback) {
|
|
43
|
-
const text = value.replace(/\s+/g, ' ').trim().slice(0, maximum);
|
|
44
|
-
return text || fallback;
|
|
45
|
-
}
|
|
46
|
-
function missionOpenItemIds(items, pattern) {
|
|
47
|
-
return [...new Set(items.map((item) => item.id).filter((id) => pattern.test(id)))].sort();
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Source a mission packet's non-evidence payload (FR1/E-001) from live cycle
|
|
51
|
-
* and workspace state at the point mission packets are constructed: the product
|
|
52
|
-
* charter and current goals, a prior-cycle summary with its open
|
|
53
|
-
* decisions/gaps/pending outcomes, planning and delivery status, the read roots
|
|
54
|
-
* declared for the index, and the revision pinned once at cycle start (recorded
|
|
55
|
-
* in the workspace manifest's control repository and threaded identically into
|
|
56
|
-
* every mission packet). Nothing here is invented: every value is read from the
|
|
57
|
-
* parsed charter context, the reduced state, the operating config, and the
|
|
58
|
-
* captured workspace manifest.
|
|
59
|
-
*/
|
|
60
|
-
export function sourceOperatingMissionPacketState(input) {
|
|
61
|
-
const { context } = input;
|
|
62
|
-
const charter = context.charter;
|
|
63
|
-
const charterLine = [
|
|
64
|
-
charter.purpose && `Purpose: ${charter.purpose}`,
|
|
65
|
-
charter.stage && `Stage: ${charter.stage}`,
|
|
66
|
-
charter.businessModel && `Business model: ${charter.businessModel}`,
|
|
67
|
-
charter.idealCustomer && `Ideal customer: ${charter.idealCustomer}`,
|
|
68
|
-
]
|
|
69
|
-
.filter(Boolean)
|
|
70
|
-
.join('. ');
|
|
71
|
-
const currentGoals = [...new Set(charter.goals.map((goal) => goal.trim()).filter(Boolean))]
|
|
72
|
-
.map((goal) => goal.slice(0, 512))
|
|
73
|
-
.sort();
|
|
74
|
-
const prior = context.priorCycle;
|
|
75
|
-
const priorSummaryText = prior
|
|
76
|
-
? `Prior cycle ${prior.id} (${prior.state}, health ${prior.health ?? 'unknown'}) closed with ` +
|
|
77
|
-
`${prior.findings} finding(s), ${prior.decisions} decision(s), ${prior.gaps} gap(s), and ` +
|
|
78
|
-
`${prior.pendingOutcomes} pending outcome(s).`
|
|
79
|
-
: 'No prior operating cycle.';
|
|
80
|
-
const priorCycleSummary = {
|
|
81
|
-
...(prior && MISSION_CYCLE_PATTERN.test(prior.id) ? { cycleId: prior.id } : {}),
|
|
82
|
-
summary: boundedMissionText(priorSummaryText, 8192, 'No prior operating cycle.'),
|
|
83
|
-
openDecisions: missionOpenItemIds(context.openDecisions, MISSION_DECISION_PATTERN),
|
|
84
|
-
openGaps: missionOpenItemIds(context.openGaps, MISSION_GAP_PATTERN),
|
|
85
|
-
pendingOutcomes: missionOpenItemIds(context.pendingOutcomes, MISSION_OUTCOME_PATTERN),
|
|
86
|
-
};
|
|
87
|
-
const planning = boundedMissionText(`${context.openDecisions.length} open decision(s); ${context.openGaps.length} open gap(s); ` +
|
|
88
|
-
`${context.pendingOutcomes.length} pending outcome(s).`, 2048, 'No open planning items.');
|
|
89
|
-
const delivery = boundedMissionText(prior
|
|
90
|
-
? `Most recent cycle ${prior.id} is ${prior.state}.`
|
|
91
|
-
: 'No delivery cycle has completed yet.', 2048, 'No delivery status recorded.');
|
|
92
|
-
const pinnedRevision = input.workspace.controlRepository.pinnedRevision;
|
|
93
|
-
return {
|
|
94
|
-
cycleId: input.cycleId,
|
|
95
|
-
...(pinnedRevision && MISSION_REVISION_PATTERN.test(pinnedRevision) ? { pinnedRevision } : {}),
|
|
96
|
-
charter: {
|
|
97
|
-
productCharter: boundedMissionText(charterLine, 8192, 'No product charter recorded.'),
|
|
98
|
-
currentGoals,
|
|
99
|
-
},
|
|
100
|
-
priorCycleSummary,
|
|
101
|
-
planningStatus: {
|
|
102
|
-
planningEngine: input.config.planningEngine,
|
|
103
|
-
planning,
|
|
104
|
-
delivery,
|
|
105
|
-
},
|
|
106
|
-
declaredRoots: deriveOperatingDeclaredRoots(input.evidenceIndex),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Build the Protocol v1.3 mission packets (FR1/E-001) for a cycle from live
|
|
111
|
-
* state: derive the body-free evidence index from the collected snapshot,
|
|
112
|
-
* source the non-evidence payload once, and construct one digest-bound packet
|
|
113
|
-
* per requested role. Each packet is measured against its role's derived mission
|
|
114
|
-
* budget and fails closed with `E_OPERATE_MISSION_PACKET_BUDGET` naming the role
|
|
115
|
-
* before any dispatch. Pack mode (`createOperatingAdvisorPack`) is untouched.
|
|
116
|
-
*
|
|
117
|
-
* `maxEvidenceItems` is the caller's upper bound (`config.budgets.maxItems`); the
|
|
118
|
-
* effective per-role cap is the smaller of it and the role's registry-sized
|
|
119
|
-
* default (`deriveOperatingMissionEvidenceCap`), so a large-budget lens keeps
|
|
120
|
-
* more items than a small-budget one and a healthy repository under the cap is
|
|
121
|
-
* never truncated. When a role's index exceeds its cap the pipeline keeps the
|
|
122
|
-
* highest-priority items and the drop is returned in `truncations`.
|
|
123
|
-
*/
|
|
124
|
-
export async function buildOperatingMissionPackets(input) {
|
|
125
|
-
const evidenceIndex = buildOperatingEvidenceIndex(input.evidence, {
|
|
126
|
-
sensitivityCeiling: input.sensitivityCeiling,
|
|
127
|
-
});
|
|
128
|
-
const state = sourceOperatingMissionPacketState({
|
|
129
|
-
cycleId: input.cycleId,
|
|
130
|
-
config: input.config,
|
|
131
|
-
workspace: input.workspace,
|
|
132
|
-
context: input.context,
|
|
133
|
-
evidenceIndex,
|
|
134
|
-
});
|
|
135
|
-
const roleIds = input.roleIds ?? input.config.enabledRoles;
|
|
136
|
-
const evidenceCaps = await deriveOperatingMissionEvidenceCaps(input.maxEvidenceItems);
|
|
137
|
-
const packets = [];
|
|
138
|
-
const truncations = [];
|
|
139
|
-
for (const roleId of roleIds) {
|
|
140
|
-
const { packet, truncation } = await buildOperatingMissionPacket({
|
|
141
|
-
roleId,
|
|
142
|
-
...state,
|
|
143
|
-
evidenceIndex,
|
|
144
|
-
maxEvidenceItems: evidenceCaps[roleId],
|
|
145
|
-
});
|
|
146
|
-
packets.push(packet);
|
|
147
|
-
if (truncation)
|
|
148
|
-
truncations.push(truncation);
|
|
149
|
-
}
|
|
150
|
-
return { packets, truncations };
|
|
151
|
-
}
|
|
152
36
|
async function snapshotWorkspace(projectRoot, localRoot) {
|
|
153
37
|
return refreshOperatingWorkspaceManifest(projectRoot, { localRoot });
|
|
154
38
|
}
|
|
@@ -207,6 +91,10 @@ function readinessGaps(readiness, decisionOwner, now) {
|
|
|
207
91
|
updatedAt: now,
|
|
208
92
|
}));
|
|
209
93
|
}
|
|
94
|
+
// Retained callerless: this fed the collector's `requiredEvidenceRefs` input,
|
|
95
|
+
// which the pinned-revision seed no longer takes. The collector and its dedicated
|
|
96
|
+
// feeders are retired wholesale in T-009; nothing is deleted here.
|
|
97
|
+
// biome-ignore lint/correctness/noUnusedVariables: retained pending collector retirement (T-009)
|
|
210
98
|
function requiredOpenEvidenceRefs(state) {
|
|
211
99
|
const records = [
|
|
212
100
|
...state.findings.filter((finding) => !['done', 'rejected', 'superseded'].includes(String(finding.status))),
|
|
@@ -278,6 +166,121 @@ export function buildChairEvidence(evidence, results, now) {
|
|
|
278
166
|
}),
|
|
279
167
|
};
|
|
280
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* FR1/FR2 (isolation rebind): the cycle's pinned-revision evidence seed. Evidence
|
|
171
|
+
* is an OUTPUT now, so a fresh cycle no longer collects a body-bearing snapshot
|
|
172
|
+
* before anyone has investigated; it seeds an empty evidence-of-record whose
|
|
173
|
+
* fingerprint binds the cycle to its control-repository pinned revision. This
|
|
174
|
+
* seed is what the adapter isolation binds to (`persistedAdapterEvidence`'s
|
|
175
|
+
* fingerprint === the prepared session's `evidenceDigest`), so a tampered or
|
|
176
|
+
* revision-mismatched session still fails closed with `E_OPERATE_ADVISOR_ISOLATION`
|
|
177
|
+
* — the guarantee is preserved, only its input is the pinned revision rather than
|
|
178
|
+
* a collected-evidence digest. Nothing is guessed, budgeted, or packed.
|
|
179
|
+
*/
|
|
180
|
+
async function buildPinnedRevisionEvidenceSeed(input) {
|
|
181
|
+
return assertOperatingArtifact('operating-evidence', {
|
|
182
|
+
kind: 'operating-evidence',
|
|
183
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
184
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
185
|
+
cycleId: input.cycleId,
|
|
186
|
+
fingerprint: canonicalDigest({
|
|
187
|
+
cycleId: input.cycleId,
|
|
188
|
+
pinnedRevision: input.pinnedRevision,
|
|
189
|
+
}),
|
|
190
|
+
collectedAt: input.collectedAt,
|
|
191
|
+
truncated: false,
|
|
192
|
+
items: [],
|
|
193
|
+
sources: [],
|
|
194
|
+
warnings: [],
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* FR2: dispatch is no longer pre-gated on collected evidence — every enabled role
|
|
199
|
+
* is dispatched with a mandate and post-gated on what its response actually cites.
|
|
200
|
+
* This builds the "all roles runnable" readiness the dispatch consumes, replacing
|
|
201
|
+
* the collector-era evidence-readiness evaluation.
|
|
202
|
+
*/
|
|
203
|
+
async function buildDispatchReadiness(input) {
|
|
204
|
+
const hasPinnedRevision = !/^0+$/.test(input.pinnedRevision);
|
|
205
|
+
return assertOperatingArtifact('operating-evidence-readiness', {
|
|
206
|
+
kind: 'operating-evidence-readiness',
|
|
207
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
208
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
209
|
+
cycleId: input.cycleId,
|
|
210
|
+
inputDigest: canonicalDigest({ cycleId: input.cycleId, roles: [...input.roles].sort() }),
|
|
211
|
+
evaluatedAt: input.now.toISOString(),
|
|
212
|
+
roles: input.roles.map((roleId, index) => ({
|
|
213
|
+
roleId,
|
|
214
|
+
readiness: 'ready',
|
|
215
|
+
requirements: [
|
|
216
|
+
{
|
|
217
|
+
source: 'mandate',
|
|
218
|
+
claimTypes: ['resolvable-citation-required'],
|
|
219
|
+
minimumItems: 1,
|
|
220
|
+
observedItems: hasPinnedRevision ? 1 : 0,
|
|
221
|
+
maxAgeHours: 8760,
|
|
222
|
+
oldestAgeHours: hasPinnedRevision ? 0 : null,
|
|
223
|
+
observationWindow: 'current-cycle',
|
|
224
|
+
sensitivityCeiling: input.sensitivityCeiling,
|
|
225
|
+
satisfied: hasPinnedRevision,
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
missingEvidence: hasPinnedRevision
|
|
229
|
+
? []
|
|
230
|
+
: ['mandate:resolvable-citation-required (0/1; no pinned revision)'],
|
|
231
|
+
evidenceRefs: [],
|
|
232
|
+
modelCallAllowed: hasPinnedRevision,
|
|
233
|
+
gapId: hasPinnedRevision ? null : `GAP-${String(index + 1).padStart(3, '0')}`,
|
|
234
|
+
})),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* FR2 (evidence-as-output): assemble the cycle's evidence-of-record for
|
|
239
|
+
* consolidation from the resolved-citation snapshots the universal gate minted.
|
|
240
|
+
* Every evidenceRef a gated proposal carries was snapshotted to machine-local
|
|
241
|
+
* evidence (`putCitationSnapshot`); this reads each back (`getCitationSnapshot`)
|
|
242
|
+
* so downstream confidence/sensitivity/freshness scoring resolves against what
|
|
243
|
+
* was actually cited and verified rather than a pre-collected guess. A ref whose
|
|
244
|
+
* snapshot is unreadable still materializes a minimal item so consolidation never
|
|
245
|
+
* rejects a legitimately-minted reference.
|
|
246
|
+
*/
|
|
247
|
+
async function assembleCitationEvidence(input) {
|
|
248
|
+
const refs = [
|
|
249
|
+
...new Set(input.roleResults.flatMap((result) => result.proposals.flatMap((proposal) => proposal.evidenceRefs))),
|
|
250
|
+
].sort();
|
|
251
|
+
const items = [];
|
|
252
|
+
for (const ref of refs) {
|
|
253
|
+
const snapshot = await input.cache.getCitationSnapshot(ref);
|
|
254
|
+
items.push({
|
|
255
|
+
id: ref,
|
|
256
|
+
source: 'citation',
|
|
257
|
+
location: snapshot?.sourceLocation ?? ref,
|
|
258
|
+
digest: canonicalDigest(snapshot?.content ?? ref),
|
|
259
|
+
collectedAt: input.now,
|
|
260
|
+
observedFrom: null,
|
|
261
|
+
observedTo: null,
|
|
262
|
+
freshness: 'fresh',
|
|
263
|
+
sensitivity: snapshot?.sensitivity ?? 'internal',
|
|
264
|
+
claimTypes: ['verified-citation'],
|
|
265
|
+
...(snapshot ? { summary: snapshot.content.slice(0, 512) } : {}),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return assertOperatingArtifact('operating-evidence', {
|
|
269
|
+
kind: 'operating-evidence',
|
|
270
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
271
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
272
|
+
cycleId: input.cycleId,
|
|
273
|
+
fingerprint: canonicalDigest({
|
|
274
|
+
source: input.baseFingerprint,
|
|
275
|
+
citations: items.map((item) => ({ id: item.id, digest: item.digest })),
|
|
276
|
+
}),
|
|
277
|
+
collectedAt: input.now,
|
|
278
|
+
truncated: false,
|
|
279
|
+
items,
|
|
280
|
+
sources: [],
|
|
281
|
+
warnings: [],
|
|
282
|
+
});
|
|
283
|
+
}
|
|
281
284
|
const SENSITIVE_ROUTE_LANGUAGE = /\b(?:payment|billing|security|privacy|legal|tenant|isolation|destructive|delete|purge|credential|secret)\b/i;
|
|
282
285
|
function containsSensitiveRouteProposal(results) {
|
|
283
286
|
return results.some((result) => result.proposals.some((proposal) => SENSITIVE_ROUTE_LANGUAGE.test(`${proposal.type} ${proposal.title} ${proposal.problem} ${proposal.proposal}`)));
|
|
@@ -441,50 +444,104 @@ async function persistedCycleRoleResults(store, events, cycleId) {
|
|
|
441
444
|
* dry-run never enter the mutating branch.
|
|
442
445
|
*/
|
|
443
446
|
/**
|
|
444
|
-
*
|
|
447
|
+
* The single governed gap opened for a role whose citation-bearing response
|
|
448
|
+
* grounded zero evidence (FR2). It names the role and its empty grounding so the
|
|
449
|
+
* integrity surface can render the role `not_evaluated` with a real reason rather
|
|
450
|
+
* than a silent absence or a "quiet" that pretends the lens evaluated.
|
|
451
|
+
*/
|
|
452
|
+
function roleEmptyGroundingGap(roleId, context, now) {
|
|
453
|
+
const createdAt = now.toISOString();
|
|
454
|
+
return {
|
|
455
|
+
kind: 'operating-data-gap',
|
|
456
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
457
|
+
protocolVersion: OPERATE_MISSION_PROTOCOL_VERSION,
|
|
458
|
+
id: `GAP-${canonicalDigest({
|
|
459
|
+
roleId,
|
|
460
|
+
cycleId: context.cycleId,
|
|
461
|
+
reason: 'empty-grounding',
|
|
462
|
+
}).slice('sha256:'.length)}`,
|
|
463
|
+
cycleId: context.cycleId,
|
|
464
|
+
category: 'missing-evidence',
|
|
465
|
+
question: `What evidence can ${roleId} cite? Its response resolved zero citations to evidence.`,
|
|
466
|
+
reason: `Every citation ${roleId} returned failed to resolve at the pinned revision, so the role ` +
|
|
467
|
+
'grounded no evidence and is recorded not_evaluated instead of dispatching a proposal on an ' +
|
|
468
|
+
'empty base.',
|
|
469
|
+
unblocks: [],
|
|
470
|
+
affectedRoles: [roleId],
|
|
471
|
+
status: 'open',
|
|
472
|
+
owner: context.owner && context.owner.length > 0 ? context.owner : 'chair',
|
|
473
|
+
evidenceRefs: [],
|
|
474
|
+
createdAt,
|
|
475
|
+
updatedAt: createdAt,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* FR2 universal citation gate — the PRIMARY, unconditional mechanism, run on
|
|
480
|
+
* every dispatch path and every evidence source (no `bearing.length === 0`
|
|
481
|
+
* bypass remains, so it is entered even for a citation-free result).
|
|
445
482
|
*
|
|
446
483
|
* Every recorded proposal that carries citations is resolved against the cycle's
|
|
447
|
-
* pinned revision through
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
484
|
+
* pinned revision through `enforceRecordedProposalCitations`. A proposal with ANY
|
|
485
|
+
* unresolvable citation is DROPPED — it never reaches `consolidateOperatingResults`
|
|
486
|
+
* — and exactly one `unresolvable-citation` gap is opened in its place. A proposal
|
|
487
|
+
* whose citations all resolve keeps its minted evidence IDs.
|
|
488
|
+
*
|
|
489
|
+
* On top of that per-proposal gate, the FR2 role-level rule: a role result whose
|
|
490
|
+
* proposals carried citations but whose accepted citations resolve to ZERO
|
|
491
|
+
* evidence IDs is `not_evaluated` — every one of its proposals is dropped, its id
|
|
492
|
+
* is returned in `notEvaluatedRoleIds`, and one governed `missing-evidence` gap
|
|
493
|
+
* naming the role and its empty grounding is opened. This replaces SPEC-003's
|
|
494
|
+
* mission-only, repository-only starvation gate with one rule that holds whatever
|
|
495
|
+
* the dispatch path or evidence source. A v1.2 pack result (evidenceRefs, no
|
|
496
|
+
* citations) carries no citation-bearing proposal, so it is never demoted — the
|
|
497
|
+
* pack path stays functional until T-003 removes it.
|
|
454
498
|
*/
|
|
455
499
|
export async function gateRecordedProposalCitations(input) {
|
|
456
500
|
const bearing = [];
|
|
501
|
+
const citationBearingProposalKeys = new Set();
|
|
457
502
|
for (const result of input.roleResults) {
|
|
458
503
|
for (const proposal of result.proposals) {
|
|
459
504
|
const citations = proposal.citations;
|
|
460
505
|
if (Array.isArray(citations) && citations.length > 0) {
|
|
461
506
|
bearing.push({ proposalKey: proposal.proposalKey, citations });
|
|
507
|
+
citationBearingProposalKeys.add(proposal.proposalKey);
|
|
462
508
|
}
|
|
463
509
|
}
|
|
464
510
|
}
|
|
465
|
-
if (bearing.length === 0)
|
|
466
|
-
return { roleResults: input.roleResults, gaps: [] };
|
|
467
511
|
const enforcement = await enforceRecordedProposalCitations(bearing, input.context);
|
|
468
512
|
const rejectedKeys = new Set(enforcement.rejected.map((entry) => entry.proposalKey));
|
|
469
513
|
const evidenceByKey = new Map(enforcement.accepted.map((entry) => [entry.proposal.proposalKey, entry.evidenceRefs]));
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
514
|
+
const gaps = [...enforcement.gaps];
|
|
515
|
+
const notEvaluatedRoleIds = [];
|
|
516
|
+
const now = input.context.now ?? new Date();
|
|
517
|
+
const roleResults = await Promise.all(input.roleResults.map(async (result) => {
|
|
518
|
+
const gated = {
|
|
519
|
+
...result,
|
|
520
|
+
proposals: result.proposals
|
|
521
|
+
.filter((proposal) => !rejectedKeys.has(proposal.proposalKey))
|
|
522
|
+
.map((proposal) => {
|
|
523
|
+
const minted = evidenceByKey.get(proposal.proposalKey);
|
|
524
|
+
return minted && minted.length > 0
|
|
525
|
+
? {
|
|
526
|
+
...proposal,
|
|
527
|
+
evidenceRefs: [...new Set([...proposal.evidenceRefs, ...minted])].sort(),
|
|
528
|
+
}
|
|
529
|
+
: proposal;
|
|
530
|
+
}),
|
|
531
|
+
};
|
|
532
|
+
// FR2 role-level rule: a role whose proposals were citation-bearing but
|
|
533
|
+
// grounded ZERO evidence after resolution is not_evaluated. Drop its
|
|
534
|
+
// surviving (evidence-free) proposals and open one governed gap.
|
|
535
|
+
const wasCitationBearing = result.proposals.some((proposal) => citationBearingProposalKeys.has(proposal.proposalKey));
|
|
536
|
+
const mintedAny = gated.proposals.some((proposal) => proposal.evidenceRefs.length > 0);
|
|
537
|
+
if (wasCitationBearing && !mintedAny) {
|
|
538
|
+
notEvaluatedRoleIds.push(result.roleId);
|
|
539
|
+
gaps.push(await assertOperatingArtifact('operating-data-gap', roleEmptyGroundingGap(result.roleId, input.context, now)));
|
|
540
|
+
return { ...gated, proposals: [] };
|
|
541
|
+
}
|
|
542
|
+
return gated;
|
|
483
543
|
}));
|
|
484
|
-
|
|
485
|
-
// against the v1.3 operating-data-gap schema, which admits `category`). They
|
|
486
|
-
// are persisted alongside the cycle's other gaps by the committed path.
|
|
487
|
-
return { roleResults, gaps: enforcement.gaps };
|
|
544
|
+
return { roleResults, gaps, notEvaluatedRoleIds };
|
|
488
545
|
}
|
|
489
546
|
export async function runOperatingCycle(input) {
|
|
490
547
|
const projectRoot = await assertOperatingProject(input.projectRoot);
|
|
@@ -493,12 +550,6 @@ export async function runOperatingCycle(input) {
|
|
|
493
550
|
const initializedWorkspace = await loadJson(paths.workspace);
|
|
494
551
|
await assertOperatingArtifact('operating-workspace-manifest', initializedWorkspace);
|
|
495
552
|
const preferences = await readPreferences(projectRoot, input.localRoot);
|
|
496
|
-
// FR4 / E-004 fold-in: the live dispatch call site reads the persisted
|
|
497
|
-
// per-project dispatch-mode overrides once and threads them into every advisor
|
|
498
|
-
// dispatch below, so a persisted override reaches dispatch provenance.
|
|
499
|
-
const dispatchModeOverrides = await readOperatingDispatchModeOverrides(projectRoot, {
|
|
500
|
-
localRoot: input.localRoot,
|
|
501
|
-
});
|
|
502
553
|
const store = new OperatingEventStore(projectRoot, { localRoot: input.localRoot });
|
|
503
554
|
const now = input.now ?? new Date();
|
|
504
555
|
const deadlineReconciliation = !input.preview && !input.dryRun
|
|
@@ -645,36 +696,36 @@ export async function runOperatingCycle(input) {
|
|
|
645
696
|
state: initialState,
|
|
646
697
|
cycleId: cycle.id,
|
|
647
698
|
});
|
|
699
|
+
const citationCache = new OperatingEvidenceCache(resolveOperatingPaths(projectRoot, { localRoot: input.localRoot }).evidence, preferences.sensitivityCeiling);
|
|
700
|
+
// FR2 universal gate, bound to the cycle's pinned revision. Injected into
|
|
701
|
+
// dispatch so advisors.ts never imports engine.ts, and reused by the
|
|
702
|
+
// consolidation-time gate below so both paths resolve citations identically.
|
|
703
|
+
const resolveCitations = (roleResults) => gateRecordedProposalCitations({
|
|
704
|
+
roleResults,
|
|
705
|
+
context: {
|
|
706
|
+
projectRoot,
|
|
707
|
+
cycleId: cycle.id,
|
|
708
|
+
descriptor: workspace.controlRepository,
|
|
709
|
+
cache: citationCache,
|
|
710
|
+
owner: config.decisionOwner,
|
|
711
|
+
now,
|
|
712
|
+
},
|
|
713
|
+
});
|
|
648
714
|
const evaluate = async (persist) => {
|
|
649
715
|
await assertCycleWorkspace();
|
|
650
716
|
const evidence = existingEvidence ??
|
|
651
|
-
(await
|
|
652
|
-
projectRoot,
|
|
717
|
+
(await buildPinnedRevisionEvidenceSeed({
|
|
653
718
|
cycleId: cycle.id,
|
|
654
|
-
workspace,
|
|
655
|
-
|
|
656
|
-
sensitivityCeiling: preferences.sensitivityCeiling,
|
|
657
|
-
budgets: {
|
|
658
|
-
...config.budgets,
|
|
659
|
-
maxItemBytes: Math.min(config.budgets.maxBytes, 256 * 1024),
|
|
660
|
-
},
|
|
661
|
-
localRoot: input.localRoot,
|
|
662
|
-
now,
|
|
663
|
-
incremental: depth === 'standard',
|
|
664
|
-
persistIncremental: persist && depth === 'standard',
|
|
665
|
-
requiredEvidenceRefs: requiredOpenEvidenceRefs(initialState),
|
|
719
|
+
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
720
|
+
collectedAt: timestamp,
|
|
666
721
|
}));
|
|
667
|
-
const evidenceGaps =
|
|
668
|
-
cycleId: cycle.id,
|
|
669
|
-
evidence,
|
|
670
|
-
owner: config.decisionOwner,
|
|
671
|
-
now: timestamp,
|
|
672
|
-
});
|
|
722
|
+
const evidenceGaps = [];
|
|
673
723
|
const nonChair = config.enabledRoles.filter((role) => role !== 'chair');
|
|
674
|
-
let readiness = await
|
|
724
|
+
let readiness = await buildDispatchReadiness({
|
|
675
725
|
cycleId: cycle.id,
|
|
676
|
-
|
|
677
|
-
|
|
726
|
+
roles: nonChair,
|
|
727
|
+
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
728
|
+
sensitivityCeiling: preferences.sensitivityCeiling,
|
|
678
729
|
now,
|
|
679
730
|
});
|
|
680
731
|
readiness = await remapReadinessGaps(readiness, initialState);
|
|
@@ -742,13 +793,14 @@ export async function runOperatingCycle(input) {
|
|
|
742
793
|
const first = runnableMissingNonChair.length > 0
|
|
743
794
|
? await dispatchOperatingAdvisors({
|
|
744
795
|
cycleId: cycle.id,
|
|
745
|
-
|
|
796
|
+
projectRoot,
|
|
797
|
+
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
746
798
|
readiness: { ...readiness, roles: missingNonChair },
|
|
747
799
|
context: advisorContext,
|
|
748
800
|
adapter: await resolveAdapter(),
|
|
749
801
|
depth,
|
|
750
802
|
runtime: resolvedRuntime,
|
|
751
|
-
|
|
803
|
+
resolveCitations,
|
|
752
804
|
})
|
|
753
805
|
: {
|
|
754
806
|
results: [],
|
|
@@ -757,6 +809,7 @@ export async function runOperatingCycle(input) {
|
|
|
757
809
|
blocked: false,
|
|
758
810
|
skipped: skippedMissingNonChair,
|
|
759
811
|
failed: [],
|
|
812
|
+
gaps: [],
|
|
760
813
|
};
|
|
761
814
|
let roleResults = [
|
|
762
815
|
...existingRoleResults.filter((result) => result.roleId !== 'chair'),
|
|
@@ -767,14 +820,19 @@ export async function runOperatingCycle(input) {
|
|
|
767
820
|
const dispatchProvenance = [...first.provenance];
|
|
768
821
|
const skipped = [...first.skipped];
|
|
769
822
|
const failed = [...first.failed];
|
|
770
|
-
|
|
823
|
+
// FR2: the governed gaps opened while resolving each dispatched role's
|
|
824
|
+
// citations (unresolvable-citation, empty-grounding) are recorded alongside
|
|
825
|
+
// the cycle's other gaps.
|
|
826
|
+
const dispatchGaps = [...first.gaps];
|
|
771
827
|
if (config.enabledRoles.includes('chair')) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
828
|
+
// FR1: the Chair dispatches its own mandate and investigates independently —
|
|
829
|
+
// it no longer receives a synthesized advisor-results evidence body. Its
|
|
830
|
+
// proposals cite resolvable content the same way every other lens does.
|
|
831
|
+
let chairReadiness = await buildDispatchReadiness({
|
|
775
832
|
cycleId: cycle.id,
|
|
776
|
-
|
|
777
|
-
|
|
833
|
+
roles: ['chair'],
|
|
834
|
+
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
835
|
+
sensitivityCeiling: preferences.sensitivityCeiling,
|
|
778
836
|
now,
|
|
779
837
|
});
|
|
780
838
|
chairReadiness = await remapReadinessGaps(chairReadiness, {
|
|
@@ -825,47 +883,52 @@ export async function runOperatingCycle(input) {
|
|
|
825
883
|
blocked: false,
|
|
826
884
|
skipped: [],
|
|
827
885
|
failed: [],
|
|
886
|
+
gaps: [],
|
|
828
887
|
}
|
|
829
888
|
: await dispatchOperatingAdvisors({
|
|
830
889
|
cycleId: cycle.id,
|
|
831
|
-
|
|
890
|
+
projectRoot,
|
|
891
|
+
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
832
892
|
readiness: chairReadiness,
|
|
833
893
|
context: advisorContext,
|
|
834
894
|
adapter: await resolveAdapter(),
|
|
835
895
|
depth,
|
|
836
896
|
runtime: resolvedRuntime,
|
|
837
|
-
|
|
897
|
+
resolveCitations,
|
|
838
898
|
});
|
|
839
899
|
roleResults = [...roleResults, ...chair.results];
|
|
840
900
|
modelCalls += chair.modelCalls;
|
|
841
901
|
dispatchProvenance.push(...chair.provenance);
|
|
842
902
|
skipped.push(...chair.skipped);
|
|
843
903
|
failed.push(...chair.failed);
|
|
904
|
+
dispatchGaps.push(...chair.gaps);
|
|
844
905
|
blocked ||= chair.blocked || chairReadiness.roles.some((role) => !role.modelCallAllowed);
|
|
845
906
|
}
|
|
846
907
|
const technologyRiskReady = readiness.roles.some((role) => role.roleId === 'technology-risk' && role.modelCallAllowed);
|
|
847
908
|
const technologyRiskFailed = failed.some((entry) => entry.roleId === 'technology-risk');
|
|
848
909
|
const sensitiveRouteBlocked = containsSensitiveRouteProposal(roleResults) && (!technologyRiskReady || technologyRiskFailed);
|
|
849
910
|
blocked ||= sensitiveRouteBlocked;
|
|
850
|
-
// FR3 / E-003:
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
911
|
+
// FR2/FR3 / E-003: the universal citation gate. Every dispatched role already
|
|
912
|
+
// resolved its citations at dispatch time (the committed result is v1.2-valid
|
|
913
|
+
// with minted evidenceRefs, no raw citations), so re-running the gate over the
|
|
914
|
+
// full role set — including any natively-committed existing results — is a
|
|
915
|
+
// no-op that provides defence in depth and a single point where consolidation
|
|
916
|
+
// input is proven citation-clean.
|
|
917
|
+
const citationGate = await resolveCitations(roleResults);
|
|
918
|
+
// FR2 (evidence-as-output): the cycle's evidence-of-record is assembled from
|
|
919
|
+
// the resolved-citation snapshots, so confidence/sensitivity/freshness scoring
|
|
920
|
+
// resolves against what was actually cited and verified — never a pre-collected
|
|
921
|
+
// guess. This replaces the retired collected-evidence consolidation input.
|
|
922
|
+
const consolidationEvidence = await assembleCitationEvidence({
|
|
923
|
+
cycleId: cycle.id,
|
|
924
|
+
roleResults: citationGate.roleResults,
|
|
925
|
+
cache: citationCache,
|
|
926
|
+
baseFingerprint: evidence.fingerprint,
|
|
927
|
+
now: timestamp,
|
|
864
928
|
});
|
|
865
929
|
// Only the gated proposals feed consolidation, so a proposal with an
|
|
866
|
-
// unresolvable citation can never become a finding/route. The
|
|
867
|
-
//
|
|
868
|
-
// opened unresolvable-citation gap threaded into `gaps` below.
|
|
930
|
+
// unresolvable citation can never become a finding/route. The rejection is
|
|
931
|
+
// recorded as the opened unresolvable-citation gap threaded into `gaps` below.
|
|
869
932
|
const consolidated = await consolidateOperatingResults({
|
|
870
933
|
cycleId: cycle.id,
|
|
871
934
|
results: citationGate.roleResults,
|
|
@@ -936,9 +999,13 @@ export async function runOperatingCycle(input) {
|
|
|
936
999
|
decisions: remapped.decisions,
|
|
937
1000
|
// Citation gaps bypass remapConsolidation (their IDs are digest-derived and
|
|
938
1001
|
// already canonical) and are persisted alongside the cycle's other gaps.
|
|
1002
|
+
// `dispatchGaps` are the unresolvable-citation/empty-grounding gaps opened
|
|
1003
|
+
// while resolving each role's citations at dispatch time; `citationGate.gaps`
|
|
1004
|
+
// covers the no-op consolidation-time re-gate (empty in practice).
|
|
939
1005
|
gaps: [
|
|
940
1006
|
...readinessGaps(readiness, config.decisionOwner, timestamp),
|
|
941
1007
|
...remapped.gaps,
|
|
1008
|
+
...dispatchGaps,
|
|
942
1009
|
...citationGate.gaps,
|
|
943
1010
|
],
|
|
944
1011
|
routes: [],
|