openplanr 1.18.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.
Files changed (153) hide show
  1. package/dist/cli/commands/backlog.d.ts.map +1 -1
  2. package/dist/cli/commands/backlog.js +3 -0
  3. package/dist/cli/commands/backlog.js.map +1 -1
  4. package/dist/cli/commands/config.d.ts.map +1 -1
  5. package/dist/cli/commands/config.js +3 -0
  6. package/dist/cli/commands/config.js.map +1 -1
  7. package/dist/cli/commands/epic.d.ts.map +1 -1
  8. package/dist/cli/commands/epic.js +2 -0
  9. package/dist/cli/commands/epic.js.map +1 -1
  10. package/dist/cli/commands/feature.d.ts.map +1 -1
  11. package/dist/cli/commands/feature.js +2 -0
  12. package/dist/cli/commands/feature.js.map +1 -1
  13. package/dist/cli/commands/operate.d.ts +5 -2
  14. package/dist/cli/commands/operate.d.ts.map +1 -1
  15. package/dist/cli/commands/operate.js +10 -101
  16. package/dist/cli/commands/operate.js.map +1 -1
  17. package/dist/cli/commands/plan.d.ts.map +1 -1
  18. package/dist/cli/commands/plan.js +2 -0
  19. package/dist/cli/commands/plan.js.map +1 -1
  20. package/dist/cli/commands/quick.d.ts.map +1 -1
  21. package/dist/cli/commands/quick.js +2 -0
  22. package/dist/cli/commands/quick.js.map +1 -1
  23. package/dist/cli/commands/refine.d.ts.map +1 -1
  24. package/dist/cli/commands/refine.js +2 -0
  25. package/dist/cli/commands/refine.js.map +1 -1
  26. package/dist/cli/commands/revise.d.ts.map +1 -1
  27. package/dist/cli/commands/revise.js +2 -0
  28. package/dist/cli/commands/revise.js.map +1 -1
  29. package/dist/cli/commands/spec.d.ts.map +1 -1
  30. package/dist/cli/commands/spec.js +2 -0
  31. package/dist/cli/commands/spec.js.map +1 -1
  32. package/dist/cli/commands/sprint.d.ts.map +1 -1
  33. package/dist/cli/commands/sprint.js +2 -0
  34. package/dist/cli/commands/sprint.js.map +1 -1
  35. package/dist/cli/commands/story.d.ts.map +1 -1
  36. package/dist/cli/commands/story.js +3 -0
  37. package/dist/cli/commands/story.js.map +1 -1
  38. package/dist/cli/commands/task.d.ts.map +1 -1
  39. package/dist/cli/commands/task.js +3 -0
  40. package/dist/cli/commands/task.js.map +1 -1
  41. package/dist/services/ai-service.d.ts +3 -5
  42. package/dist/services/ai-service.d.ts.map +1 -1
  43. package/dist/services/ai-service.js +7 -1
  44. package/dist/services/ai-service.js.map +1 -1
  45. package/dist/services/credentials-service.d.ts.map +1 -1
  46. package/dist/services/credentials-service.js +2 -0
  47. package/dist/services/credentials-service.js.map +1 -1
  48. package/dist/services/deprecation-notices.d.ts +5 -0
  49. package/dist/services/deprecation-notices.d.ts.map +1 -0
  50. package/dist/services/deprecation-notices.js +6 -0
  51. package/dist/services/deprecation-notices.js.map +1 -0
  52. package/dist/services/operate/advisors.d.ts +146 -108
  53. package/dist/services/operate/advisors.d.ts.map +1 -1
  54. package/dist/services/operate/advisors.js +178 -448
  55. package/dist/services/operate/advisors.js.map +1 -1
  56. package/dist/services/operate/citation-resolution.d.ts.map +1 -1
  57. package/dist/services/operate/citation-resolution.js +50 -1
  58. package/dist/services/operate/citation-resolution.js.map +1 -1
  59. package/dist/services/operate/config.d.ts +28 -52
  60. package/dist/services/operate/config.d.ts.map +1 -1
  61. package/dist/services/operate/config.js +68 -203
  62. package/dist/services/operate/config.js.map +1 -1
  63. package/dist/services/operate/doctor.d.ts +15 -0
  64. package/dist/services/operate/doctor.d.ts.map +1 -1
  65. package/dist/services/operate/doctor.js +142 -32
  66. package/dist/services/operate/doctor.js.map +1 -1
  67. package/dist/services/operate/engine.d.ts +22 -62
  68. package/dist/services/operate/engine.d.ts.map +1 -1
  69. package/dist/services/operate/engine.js +269 -191
  70. package/dist/services/operate/engine.js.map +1 -1
  71. package/dist/services/operate/evidence.d.ts +9 -78
  72. package/dist/services/operate/evidence.d.ts.map +1 -1
  73. package/dist/services/operate/evidence.js +0 -1194
  74. package/dist/services/operate/evidence.js.map +1 -1
  75. package/dist/services/operate/index.d.ts.map +1 -1
  76. package/dist/services/operate/index.js +15 -231
  77. package/dist/services/operate/index.js.map +1 -1
  78. package/dist/services/operate/integrity.d.ts +81 -0
  79. package/dist/services/operate/integrity.d.ts.map +1 -0
  80. package/dist/services/operate/integrity.js +152 -0
  81. package/dist/services/operate/integrity.js.map +1 -0
  82. package/dist/services/operate/interaction/answer-service.d.ts +0 -10
  83. package/dist/services/operate/interaction/answer-service.d.ts.map +1 -1
  84. package/dist/services/operate/interaction/answer-service.js +63 -43
  85. package/dist/services/operate/interaction/answer-service.js.map +1 -1
  86. package/dist/services/operate/interaction/question-engine.d.ts.map +1 -1
  87. package/dist/services/operate/interaction/question-engine.js +105 -21
  88. package/dist/services/operate/interaction/question-engine.js.map +1 -1
  89. package/dist/services/operate/interaction/question-registry.d.ts +16 -1
  90. package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
  91. package/dist/services/operate/interaction/question-registry.js +45 -39
  92. package/dist/services/operate/interaction/question-registry.js.map +1 -1
  93. package/dist/services/operate/interaction/session-service.d.ts.map +1 -1
  94. package/dist/services/operate/interaction/session-service.js +16 -11
  95. package/dist/services/operate/interaction/session-service.js.map +1 -1
  96. package/dist/services/operate/interaction/terminal-renderer.d.ts.map +1 -1
  97. package/dist/services/operate/interaction/terminal-renderer.js +16 -6
  98. package/dist/services/operate/interaction/terminal-renderer.js.map +1 -1
  99. package/dist/services/operate/lifecycle.d.ts.map +1 -1
  100. package/dist/services/operate/lifecycle.js +17 -1
  101. package/dist/services/operate/lifecycle.js.map +1 -1
  102. package/dist/services/operate/maintenance.d.ts.map +1 -1
  103. package/dist/services/operate/maintenance.js +95 -276
  104. package/dist/services/operate/maintenance.js.map +1 -1
  105. package/dist/services/operate/mission-dispatch.d.ts +73 -55
  106. package/dist/services/operate/mission-dispatch.d.ts.map +1 -1
  107. package/dist/services/operate/mission-dispatch.js +91 -67
  108. package/dist/services/operate/mission-dispatch.js.map +1 -1
  109. package/dist/services/operate/projection-persistence.d.ts +35 -7
  110. package/dist/services/operate/projection-persistence.d.ts.map +1 -1
  111. package/dist/services/operate/projection-persistence.js +142 -53
  112. package/dist/services/operate/projection-persistence.js.map +1 -1
  113. package/dist/services/operate/projection.d.ts +9 -1
  114. package/dist/services/operate/projection.d.ts.map +1 -1
  115. package/dist/services/operate/projection.js +22 -12
  116. package/dist/services/operate/projection.js.map +1 -1
  117. package/dist/services/operate/protocol.d.ts +1 -41
  118. package/dist/services/operate/protocol.d.ts.map +1 -1
  119. package/dist/services/operate/protocol.js +7 -45
  120. package/dist/services/operate/protocol.js.map +1 -1
  121. package/dist/services/operate/redaction.d.ts +3 -3
  122. package/dist/services/operate/redaction.js +4 -4
  123. package/dist/services/operate/redaction.js.map +1 -1
  124. package/dist/services/operate/reports.d.ts +20 -1
  125. package/dist/services/operate/reports.d.ts.map +1 -1
  126. package/dist/services/operate/reports.js +108 -4
  127. package/dist/services/operate/reports.js.map +1 -1
  128. package/dist/services/operate/routes.d.ts +84 -0
  129. package/dist/services/operate/routes.d.ts.map +1 -1
  130. package/dist/services/operate/routes.js +519 -10
  131. package/dist/services/operate/routes.js.map +1 -1
  132. package/dist/services/operate/types.d.ts +48 -133
  133. package/dist/services/operate/types.d.ts.map +1 -1
  134. package/dist/services/operate/types.js.map +1 -1
  135. package/dist/services/operate/workspace.d.ts +0 -1
  136. package/dist/services/operate/workspace.d.ts.map +1 -1
  137. package/dist/services/operate/workspace.js +0 -2
  138. package/dist/services/operate/workspace.js.map +1 -1
  139. package/dist/services/runtime-manager-service.js +2 -2
  140. package/dist/services/runtime-manager-service.js.map +1 -1
  141. package/package.json +3 -3
  142. package/dist/services/operate/evidence-classifications.d.ts +0 -59
  143. package/dist/services/operate/evidence-classifications.d.ts.map +0 -1
  144. package/dist/services/operate/evidence-classifications.js +0 -132
  145. package/dist/services/operate/evidence-classifications.js.map +0 -1
  146. package/dist/services/operate/evidence-diagnostics.d.ts +0 -23
  147. package/dist/services/operate/evidence-diagnostics.d.ts.map +0 -1
  148. package/dist/services/operate/evidence-diagnostics.js +0 -152
  149. package/dist/services/operate/evidence-diagnostics.js.map +0 -1
  150. package/dist/services/operate/evidence-readiness.d.ts +0 -8
  151. package/dist/services/operate/evidence-readiness.d.ts.map +0 -1
  152. package/dist/services/operate/evidence-readiness.js +0 -84
  153. 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, buildOperatingMissionPacket, configuredAdvisorProviderPolicy, createConfiguredStructuredAdapter, createOfflineAdvisorAdapter, dispatchOperatingAdvisors, ensureOperatingProviderConsent, } from './advisors.js';
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, readOperatingDispatchModeOverrides, validateOperatingConfiguration, } from './config.js';
9
+ import { operatingProjectKey, validateOperatingConfiguration } from './config.js';
10
10
  import { consolidateOperatingResults } from './consolidation.js';
11
11
  import { OperatingEventStore } from './event-store.js';
12
- import { buildOperatingEvidenceIndex, collectOperatingEvidence, deriveOperatingDeclaredRoots, evidenceProjectionSources, unqualifiedCommercialEvidenceGaps, } from './evidence.js';
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,110 +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
114
- * single-digit-KiB mission budget and fails closed with
115
- * `E_OPERATE_MISSION_PACKET_BUDGET` naming the role before any dispatch. Pack
116
- * mode (`createOperatingAdvisorPack`) is untouched.
117
- */
118
- export async function buildOperatingMissionPackets(input) {
119
- const evidenceIndex = buildOperatingEvidenceIndex(input.evidence, {
120
- sensitivityCeiling: input.sensitivityCeiling,
121
- });
122
- const state = sourceOperatingMissionPacketState({
123
- cycleId: input.cycleId,
124
- config: input.config,
125
- workspace: input.workspace,
126
- context: input.context,
127
- evidenceIndex,
128
- });
129
- const roleIds = input.roleIds ?? input.config.enabledRoles;
130
- const packets = [];
131
- for (const roleId of roleIds) {
132
- packets.push(await buildOperatingMissionPacket({
133
- roleId,
134
- ...state,
135
- evidenceIndex,
136
- maxEvidenceItems: input.maxEvidenceItems,
137
- }));
138
- }
139
- return packets;
140
- }
141
36
  async function snapshotWorkspace(projectRoot, localRoot) {
142
37
  return refreshOperatingWorkspaceManifest(projectRoot, { localRoot });
143
38
  }
@@ -196,6 +91,10 @@ function readinessGaps(readiness, decisionOwner, now) {
196
91
  updatedAt: now,
197
92
  }));
198
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)
199
98
  function requiredOpenEvidenceRefs(state) {
200
99
  const records = [
201
100
  ...state.findings.filter((finding) => !['done', 'rejected', 'superseded'].includes(String(finding.status))),
@@ -267,6 +166,121 @@ export function buildChairEvidence(evidence, results, now) {
267
166
  }),
268
167
  };
269
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
+ }
270
284
  const SENSITIVE_ROUTE_LANGUAGE = /\b(?:payment|billing|security|privacy|legal|tenant|isolation|destructive|delete|purge|credential|secret)\b/i;
271
285
  function containsSensitiveRouteProposal(results) {
272
286
  return results.some((result) => result.proposals.some((proposal) => SENSITIVE_ROUTE_LANGUAGE.test(`${proposal.type} ${proposal.title} ${proposal.problem} ${proposal.proposal}`)));
@@ -430,50 +444,104 @@ async function persistedCycleRoleResults(store, events, cycleId) {
430
444
  * dry-run never enter the mutating branch.
431
445
  */
432
446
  /**
433
- * FR3 / E-003 citation gate, invoked immediately before consolidation.
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).
434
482
  *
435
483
  * Every recorded proposal that carries citations is resolved against the cycle's
436
- * pinned revision through T-004's `enforceRecordedProposalCitations`. A proposal
437
- * with ANY unresolvable citation is DROPPED — it never reaches
438
- * `consolidateOperatingResults` — and exactly one `unresolvable-citation` gap is
439
- * opened in its place. A proposal whose citations all resolve keeps its minted
440
- * evidence IDs. Proposals carrying no citations (the v1.2 evidence-ref path) pass
441
- * through untouched, so this is a byte-for-byte no-op for every non-citation
442
- * cycle: the same `roleResults` reference is returned and no gap is emitted.
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.
443
498
  */
444
499
  export async function gateRecordedProposalCitations(input) {
445
500
  const bearing = [];
501
+ const citationBearingProposalKeys = new Set();
446
502
  for (const result of input.roleResults) {
447
503
  for (const proposal of result.proposals) {
448
504
  const citations = proposal.citations;
449
505
  if (Array.isArray(citations) && citations.length > 0) {
450
506
  bearing.push({ proposalKey: proposal.proposalKey, citations });
507
+ citationBearingProposalKeys.add(proposal.proposalKey);
451
508
  }
452
509
  }
453
510
  }
454
- if (bearing.length === 0)
455
- return { roleResults: input.roleResults, gaps: [] };
456
511
  const enforcement = await enforceRecordedProposalCitations(bearing, input.context);
457
512
  const rejectedKeys = new Set(enforcement.rejected.map((entry) => entry.proposalKey));
458
513
  const evidenceByKey = new Map(enforcement.accepted.map((entry) => [entry.proposal.proposalKey, entry.evidenceRefs]));
459
- const roleResults = input.roleResults.map((result) => ({
460
- ...result,
461
- proposals: result.proposals
462
- .filter((proposal) => !rejectedKeys.has(proposal.proposalKey))
463
- .map((proposal) => {
464
- const minted = evidenceByKey.get(proposal.proposalKey);
465
- return minted && minted.length > 0
466
- ? {
467
- ...proposal,
468
- evidenceRefs: [...new Set([...proposal.evidenceRefs, ...minted])].sort(),
469
- }
470
- : proposal;
471
- }),
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;
472
543
  }));
473
- // The unresolvable-citation gaps are Protocol v1.3 data gaps (they validate
474
- // against the v1.3 operating-data-gap schema, which admits `category`). They
475
- // are persisted alongside the cycle's other gaps by the committed path.
476
- return { roleResults, gaps: enforcement.gaps };
544
+ return { roleResults, gaps, notEvaluatedRoleIds };
477
545
  }
478
546
  export async function runOperatingCycle(input) {
479
547
  const projectRoot = await assertOperatingProject(input.projectRoot);
@@ -482,12 +550,6 @@ export async function runOperatingCycle(input) {
482
550
  const initializedWorkspace = await loadJson(paths.workspace);
483
551
  await assertOperatingArtifact('operating-workspace-manifest', initializedWorkspace);
484
552
  const preferences = await readPreferences(projectRoot, input.localRoot);
485
- // FR4 / E-004 fold-in: the live dispatch call site reads the persisted
486
- // per-project dispatch-mode overrides once and threads them into every advisor
487
- // dispatch below, so a persisted override reaches dispatch provenance.
488
- const dispatchModeOverrides = await readOperatingDispatchModeOverrides(projectRoot, {
489
- localRoot: input.localRoot,
490
- });
491
553
  const store = new OperatingEventStore(projectRoot, { localRoot: input.localRoot });
492
554
  const now = input.now ?? new Date();
493
555
  const deadlineReconciliation = !input.preview && !input.dryRun
@@ -634,36 +696,36 @@ export async function runOperatingCycle(input) {
634
696
  state: initialState,
635
697
  cycleId: cycle.id,
636
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
+ });
637
714
  const evaluate = async (persist) => {
638
715
  await assertCycleWorkspace();
639
716
  const evidence = existingEvidence ??
640
- (await collectOperatingEvidence({
641
- projectRoot,
717
+ (await buildPinnedRevisionEvidenceSeed({
642
718
  cycleId: cycle.id,
643
- workspace,
644
- providers: config.enabledProviders,
645
- sensitivityCeiling: preferences.sensitivityCeiling,
646
- budgets: {
647
- ...config.budgets,
648
- maxItemBytes: Math.min(config.budgets.maxBytes, 256 * 1024),
649
- },
650
- localRoot: input.localRoot,
651
- now,
652
- incremental: depth === 'standard',
653
- persistIncremental: persist && depth === 'standard',
654
- requiredEvidenceRefs: requiredOpenEvidenceRefs(initialState),
719
+ pinnedRevision: workspace.controlRepository.pinnedRevision,
720
+ collectedAt: timestamp,
655
721
  }));
656
- const evidenceGaps = await unqualifiedCommercialEvidenceGaps({
657
- cycleId: cycle.id,
658
- evidence,
659
- owner: config.decisionOwner,
660
- now: timestamp,
661
- });
722
+ const evidenceGaps = [];
662
723
  const nonChair = config.enabledRoles.filter((role) => role !== 'chair');
663
- let readiness = await evaluateEvidenceReadiness({
724
+ let readiness = await buildDispatchReadiness({
664
725
  cycleId: cycle.id,
665
- evidence,
666
- enabledRoles: nonChair,
726
+ roles: nonChair,
727
+ pinnedRevision: workspace.controlRepository.pinnedRevision,
728
+ sensitivityCeiling: preferences.sensitivityCeiling,
667
729
  now,
668
730
  });
669
731
  readiness = await remapReadinessGaps(readiness, initialState);
@@ -731,13 +793,14 @@ export async function runOperatingCycle(input) {
731
793
  const first = runnableMissingNonChair.length > 0
732
794
  ? await dispatchOperatingAdvisors({
733
795
  cycleId: cycle.id,
734
- evidence,
796
+ projectRoot,
797
+ pinnedRevision: workspace.controlRepository.pinnedRevision,
735
798
  readiness: { ...readiness, roles: missingNonChair },
736
799
  context: advisorContext,
737
800
  adapter: await resolveAdapter(),
738
801
  depth,
739
802
  runtime: resolvedRuntime,
740
- dispatchModeOverrides,
803
+ resolveCitations,
741
804
  })
742
805
  : {
743
806
  results: [],
@@ -746,6 +809,7 @@ export async function runOperatingCycle(input) {
746
809
  blocked: false,
747
810
  skipped: skippedMissingNonChair,
748
811
  failed: [],
812
+ gaps: [],
749
813
  };
750
814
  let roleResults = [
751
815
  ...existingRoleResults.filter((result) => result.roleId !== 'chair'),
@@ -756,14 +820,19 @@ export async function runOperatingCycle(input) {
756
820
  const dispatchProvenance = [...first.provenance];
757
821
  const skipped = [...first.skipped];
758
822
  const failed = [...first.failed];
759
- let consolidationEvidence = evidence;
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];
760
827
  if (config.enabledRoles.includes('chair')) {
761
- const chairEvidence = buildChairEvidence(evidence, roleResults, timestamp);
762
- consolidationEvidence = chairEvidence;
763
- let chairReadiness = await evaluateEvidenceReadiness({
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({
764
832
  cycleId: cycle.id,
765
- evidence: chairEvidence,
766
- enabledRoles: ['chair'],
833
+ roles: ['chair'],
834
+ pinnedRevision: workspace.controlRepository.pinnedRevision,
835
+ sensitivityCeiling: preferences.sensitivityCeiling,
767
836
  now,
768
837
  });
769
838
  chairReadiness = await remapReadinessGaps(chairReadiness, {
@@ -814,47 +883,52 @@ export async function runOperatingCycle(input) {
814
883
  blocked: false,
815
884
  skipped: [],
816
885
  failed: [],
886
+ gaps: [],
817
887
  }
818
888
  : await dispatchOperatingAdvisors({
819
889
  cycleId: cycle.id,
820
- evidence: chairEvidence,
890
+ projectRoot,
891
+ pinnedRevision: workspace.controlRepository.pinnedRevision,
821
892
  readiness: chairReadiness,
822
893
  context: advisorContext,
823
894
  adapter: await resolveAdapter(),
824
895
  depth,
825
896
  runtime: resolvedRuntime,
826
- dispatchModeOverrides,
897
+ resolveCitations,
827
898
  });
828
899
  roleResults = [...roleResults, ...chair.results];
829
900
  modelCalls += chair.modelCalls;
830
901
  dispatchProvenance.push(...chair.provenance);
831
902
  skipped.push(...chair.skipped);
832
903
  failed.push(...chair.failed);
904
+ dispatchGaps.push(...chair.gaps);
833
905
  blocked ||= chair.blocked || chairReadiness.roles.some((role) => !role.modelCallAllowed);
834
906
  }
835
907
  const technologyRiskReady = readiness.roles.some((role) => role.roleId === 'technology-risk' && role.modelCallAllowed);
836
908
  const technologyRiskFailed = failed.some((entry) => entry.roleId === 'technology-risk');
837
909
  const sensitiveRouteBlocked = containsSensitiveRouteProposal(roleResults) && (!technologyRiskReady || technologyRiskFailed);
838
910
  blocked ||= sensitiveRouteBlocked;
839
- // FR3 / E-003: resolve every recorded proposal's citations against the cycle's
840
- // pinned revision before consolidation. A proposal built on a fabricated,
841
- // drifted, or uncommitted citation is dropped here and can never reach
842
- // consolidation; its place is taken by a single unresolvable-citation gap.
843
- const citationGate = await gateRecordedProposalCitations({
844
- roleResults,
845
- context: {
846
- projectRoot,
847
- cycleId: cycle.id,
848
- descriptor: workspace.controlRepository,
849
- cache: new OperatingEvidenceCache(resolveOperatingPaths(projectRoot, { localRoot: input.localRoot }).evidence, preferences.sensitivityCeiling),
850
- owner: config.decisionOwner,
851
- now,
852
- },
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,
853
928
  });
854
929
  // Only the gated proposals feed consolidation, so a proposal with an
855
- // unresolvable citation can never become a finding/route. The raw advisor
856
- // result stays intact as the audit record; the rejection is recorded as the
857
- // 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.
858
932
  const consolidated = await consolidateOperatingResults({
859
933
  cycleId: cycle.id,
860
934
  results: citationGate.roleResults,
@@ -925,9 +999,13 @@ export async function runOperatingCycle(input) {
925
999
  decisions: remapped.decisions,
926
1000
  // Citation gaps bypass remapConsolidation (their IDs are digest-derived and
927
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).
928
1005
  gaps: [
929
1006
  ...readinessGaps(readiness, config.decisionOwner, timestamp),
930
1007
  ...remapped.gaps,
1008
+ ...dispatchGaps,
931
1009
  ...citationGate.gaps,
932
1010
  ],
933
1011
  routes: [],