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.
Files changed (145) 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 +144 -166
  53. package/dist/services/operate/advisors.d.ts.map +1 -1
  54. package/dist/services/operate/advisors.js +178 -543
  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 +18 -52
  60. package/dist/services/operate/config.d.ts.map +1 -1
  61. package/dist/services/operate/config.js +24 -206
  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 -78
  68. package/dist/services/operate/engine.d.ts.map +1 -1
  69. package/dist/services/operate/engine.js +269 -202
  70. package/dist/services/operate/engine.js.map +1 -1
  71. package/dist/services/operate/evidence.d.ts +9 -107
  72. package/dist/services/operate/evidence.d.ts.map +1 -1
  73. package/dist/services/operate/evidence.js +0 -1403
  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 +10 -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/maintenance.d.ts +1 -11
  100. package/dist/services/operate/maintenance.d.ts.map +1 -1
  101. package/dist/services/operate/maintenance.js +94 -342
  102. package/dist/services/operate/maintenance.js.map +1 -1
  103. package/dist/services/operate/mission-dispatch.d.ts +73 -55
  104. package/dist/services/operate/mission-dispatch.d.ts.map +1 -1
  105. package/dist/services/operate/mission-dispatch.js +91 -67
  106. package/dist/services/operate/mission-dispatch.js.map +1 -1
  107. package/dist/services/operate/projection-persistence.d.ts.map +1 -1
  108. package/dist/services/operate/projection-persistence.js +22 -2
  109. package/dist/services/operate/projection-persistence.js.map +1 -1
  110. package/dist/services/operate/projection.d.ts.map +1 -1
  111. package/dist/services/operate/projection.js +10 -3
  112. package/dist/services/operate/projection.js.map +1 -1
  113. package/dist/services/operate/protocol.d.ts +1 -41
  114. package/dist/services/operate/protocol.d.ts.map +1 -1
  115. package/dist/services/operate/protocol.js +7 -45
  116. package/dist/services/operate/protocol.js.map +1 -1
  117. package/dist/services/operate/redaction.d.ts +3 -3
  118. package/dist/services/operate/redaction.js +4 -4
  119. package/dist/services/operate/redaction.js.map +1 -1
  120. package/dist/services/operate/reports.d.ts +11 -1
  121. package/dist/services/operate/reports.d.ts.map +1 -1
  122. package/dist/services/operate/reports.js +88 -3
  123. package/dist/services/operate/reports.js.map +1 -1
  124. package/dist/services/operate/types.d.ts +42 -128
  125. package/dist/services/operate/types.d.ts.map +1 -1
  126. package/dist/services/operate/types.js.map +1 -1
  127. package/dist/services/operate/workspace.d.ts +0 -1
  128. package/dist/services/operate/workspace.d.ts.map +1 -1
  129. package/dist/services/operate/workspace.js +0 -2
  130. package/dist/services/operate/workspace.js.map +1 -1
  131. package/dist/services/runtime-manager-service.js +2 -2
  132. package/dist/services/runtime-manager-service.js.map +1 -1
  133. package/package.json +3 -3
  134. package/dist/services/operate/evidence-classifications.d.ts +0 -59
  135. package/dist/services/operate/evidence-classifications.d.ts.map +0 -1
  136. package/dist/services/operate/evidence-classifications.js +0 -132
  137. package/dist/services/operate/evidence-classifications.js.map +0 -1
  138. package/dist/services/operate/evidence-diagnostics.d.ts +0 -23
  139. package/dist/services/operate/evidence-diagnostics.d.ts.map +0 -1
  140. package/dist/services/operate/evidence-diagnostics.js +0 -152
  141. package/dist/services/operate/evidence-diagnostics.js.map +0 -1
  142. package/dist/services/operate/evidence-readiness.d.ts +0 -18
  143. package/dist/services/operate/evidence-readiness.d.ts.map +0 -1
  144. package/dist/services/operate/evidence-readiness.js +0 -94
  145. package/dist/services/operate/evidence-readiness.js.map +0 -1
@@ -1,35 +1,21 @@
1
1
  import { createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
2
  import { mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { advisorResponseContractDetails, assertAdvisorOutputMatchesBrief, assertOperatingAdvisorPackWithinBudget, buildAdvisorOperatingContext, createNativeMissionOperatingRoleResult, createNativeOperatingRoleResult, createOperatingAdvisorPack, } from './advisors.js';
4
+ import { advisorResponseContractDetails, assertAdvisorOutputMatchesBrief, buildOperatingMandate, createNativeMissionOperatingRoleResult, } from './advisors.js';
5
5
  import { canonicalDigest, canonicalize, sha256Digest } from './canonical.js';
6
- import { operatingProjectKey, readOperatingAdapterLeaseDurationMs, readOperatingDispatchModeOverrides, } from './config.js';
7
- import { buildChairEvidence, buildOperatingMissionPackets, gateRecordedProposalCitations, } from './engine.js';
6
+ import { operatingProjectKey, readOperatingAdapterLeaseDurationMs } from './config.js';
7
+ import { gateRecordedProposalCitations } from './engine.js';
8
8
  import { OperatingEventStore } from './event-store.js';
9
- import { buildOperatingEvidenceIndex, starvedRoleEvidenceGaps } from './evidence.js';
10
9
  import { OperatingEvidenceCache } from './evidence-cache.js';
11
- import { purgeStaleEvidenceClassifications } from './evidence-classifications.js';
12
- import { listEvidenceDiagnostics } from './evidence-diagnostics.js';
13
- import { evaluateEvidenceReadiness } from './evidence-readiness.js';
14
10
  import { guidedSessionStatus, purgeGuidedSessions } from './interaction/session-service.js';
15
11
  import { assertCommittedOperatingView, recoverOperatingTransactions } from './journal.js';
16
12
  import { withOperatingLock } from './lock-service.js';
17
- import { narrowMissionRootsToCeiling, operatingRegistryDispatchMode, operatingRuntimeEnforcesBoundedReadOnly, resolveOperatingDispatchMode, } from './mission-dispatch.js';
18
13
  import { assertOperatingArtifact, loadOperatingProtocol } from './protocol.js';
19
- import { compareSensitivity, containsSecret, redactSensitiveText } from './redaction.js';
14
+ import { containsSecret, redactSensitiveText } from './redaction.js';
20
15
  import { OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
21
16
  import { readOperatingConfig, refreshOperatingWorkspaceManifest, resolveContainedPath, resolveOperatingPaths, } from './workspace.js';
22
- /**
23
- * Whether any bound role of this session resolved to native mission dispatch, so
24
- * both adapter-handoff call sites can pass `protocolVersion: '1.3.0'` and the
25
- * pipeline emits the v1.3 mission record action (`dispatch.agent` +
26
- * `dispatch.missionPacketPointer`) instead of the v1.2 `rolePackPointer`.
27
- */
28
- function sessionHasMissionRole(session) {
29
- return Boolean(session.roleMissionPackets && Object.keys(session.roleMissionPackets).length > 0);
30
- }
31
17
  function adapterSessionSummary(session) {
32
- const { roleBriefs: _roleBriefs, rolePacks: _rolePacks, roleMissionPackets: _roleMissionPackets, ...summary } = session;
18
+ const { roleBriefs: _roleBriefs, roleMandates: _roleMandates, ...summary } = session;
33
19
  return summary;
34
20
  }
35
21
  async function adapterHandoff(session) {
@@ -43,10 +29,7 @@ async function adapterHandoff(session) {
43
29
  : 'finalize-required';
44
30
  const protocol = await loadOperatingProtocol();
45
31
  const handoff = protocol.createOperatingAdapterHandoff({
46
- // v1.3 when any bound role resolved mission, so the record action names the
47
- // generated lens agent and points at its mission packet; v1.2 (pack) default
48
- // otherwise, byte-compatible with the pack-mode handoff.
49
- ...(sessionHasMissionRole(session) ? { protocolVersion: '1.3.0' } : {}),
32
+ protocolVersion: '1.3.0',
50
33
  phase: session.phase,
51
34
  state,
52
35
  cycleId: session.cycleId,
@@ -187,25 +170,12 @@ export async function operatingCacheAction(input) {
187
170
  .catch(() => ({ sensitivityCeiling: 'internal' }));
188
171
  const cache = new OperatingEvidenceCache(paths.evidence, preferences.sensitivityCeiling ?? 'internal');
189
172
  if (input.action === 'status') {
190
- const diagnostics = await listEvidenceDiagnostics({
191
- projectRoot: input.projectRoot,
192
- localRoot: input.localRoot,
193
- });
194
- const classifications = await purgeStaleEvidenceClassifications({
195
- projectRoot: input.projectRoot,
196
- localRoot: input.localRoot,
197
- });
198
173
  return {
199
174
  evidence: await cache.status(),
200
175
  sessions: await guidedSessionStatus({
201
176
  projectRoot: input.projectRoot,
202
177
  localRoot: input.localRoot,
203
178
  }),
204
- diagnostics: {
205
- candidates: diagnostics.length,
206
- classified: diagnostics.filter((entry) => entry.classification).length,
207
- staleClassifications: classifications.stale,
208
- },
209
179
  };
210
180
  }
211
181
  if (!input.confirmed) {
@@ -216,11 +186,6 @@ export async function operatingCacheAction(input) {
216
186
  projectRoot: input.projectRoot,
217
187
  localRoot: input.localRoot,
218
188
  });
219
- const classifications = await purgeStaleEvidenceClassifications({
220
- projectRoot: input.projectRoot,
221
- localRoot: input.localRoot,
222
- purge: true,
223
- });
224
189
  // FR4/FR11: clearing machine-local caches also drops board-bound adapter
225
190
  // sessions and incremental evidence baselines, so this is the scoped fix the
226
191
  // doctor names for stale adapter sessions and stale incremental baselines.
@@ -231,12 +196,10 @@ export async function operatingCacheAction(input) {
231
196
  return {
232
197
  removed: removed.length +
233
198
  sessions.removed +
234
- classifications.purged +
235
199
  board.removedAdvisorSessions +
236
200
  board.removedIncrementalBaselines,
237
201
  evidence: { removed: removed.length, entries: removed },
238
202
  sessions,
239
- classifications,
240
203
  adapterSessions: { removed: board.removedAdvisorSessions },
241
204
  incrementalBaselines: { removed: board.removedIncrementalBaselines },
242
205
  };
@@ -701,76 +664,31 @@ async function readAdapterSensitivityCeiling(projectRoot, localRoot) {
701
664
  'internal')
702
665
  .catch(() => 'internal');
703
666
  }
667
+ // The mandate `boundaries.roots` items must satisfy this pattern (leading dot
668
+ // permitted, no `..` traversal), so `.planr` is a valid declared root.
669
+ const MANDATE_ROOT_PATTERN = /^(?!.*\.\.)[A-Za-z0-9._][A-Za-z0-9._/-]*$/;
704
670
  /**
705
- * FR2: deny-list every declared read root that contains an above-ceiling evidence
706
- * item BEFORE the mission packet is built, so no above-ceiling file is reachable
707
- * even inside a granted root. Uses `mission-dispatch.ts`'s
708
- * `narrowMissionRootsToCeiling` over the FULL (pre-ceiling-filter) evidence, then
709
- * drops the items in the denied roots. `buildOperatingMissionPackets` then derives
710
- * the packet's `declaredRoots` (and thus its tool grant) from only the narrowed
711
- * roots. This is the belt to the bounded reader's read-time-ceiling suspenders.
671
+ * FR1/FR2: derive a mandate's declared read roots directly from the project
672
+ * working tree — every top-level directory the agent may traverse — rather than
673
+ * from a collected evidence index. `.planr/` is ALWAYS declared, whether or not
674
+ * it exists yet and REGARDLESS of git tracking, because the mission tool surface
675
+ * (`mission-dispatch.ts`) walks the filesystem directly rather than `git
676
+ * ls-files`, so a gitignored `.planr/` tree is fully readable (finding 2's
677
+ * tracked-file gap cannot reproduce here). `.git`/`node_modules` are never
678
+ * declared, and every root is filtered to the mandate's schema pattern.
712
679
  */
713
- export function narrowEvidenceToMissionCeiling(evidence, ceiling) {
714
- // FR2: scope the deny-list to the individual above-ceiling ITEMS, not every
715
- // item under the same top-level root. A compliant sibling under a root that
716
- // also holds one above-ceiling file stays reachable; only the offending item
717
- // is removed. `narrowMissionRootsToCeiling` still runs over the already
718
- // item-narrowed set (the pipeline-owned root guard, not reimplemented), so the
719
- // derived read roots never widen past the surviving items — the read-time
720
- // ceiling check on the bounded reader remains the suspenders to this belt.
721
- const compliantItems = evidence.items.filter((item) => compareSensitivity(item.sensitivity, ceiling) <= 0);
722
- const topSegment = (location) => {
723
- const [top] = location.split('/');
724
- return top && top.length > 0 ? top : null;
725
- };
726
- const declaredRoots = [
727
- ...new Set(compliantItems
728
- .map((item) => topSegment(item.location))
729
- .filter((segment) => Boolean(segment))),
730
- ];
731
- const evidenceIndex = compliantItems.map((item) => ({
732
- path: item.location,
733
- sensitivity: item.sensitivity,
734
- }));
735
- const allowedRoots = new Set(narrowMissionRootsToCeiling({ declaredRoots, evidenceIndex, ceiling }));
736
- return {
737
- ...evidence,
738
- items: compliantItems.filter((item) => {
739
- const top = topSegment(item.location);
740
- return top === null || allowedRoots.has(top);
741
- }),
742
- };
743
- }
744
- /**
745
- * Resolve every requested role's effective dispatch mode for THIS runtime (FR1):
746
- * the v1.3 registry default, overridden by the machine-local
747
- * `dispatchModeOverrides`, reconciled against whether the runtime natively
748
- * enforces the bounded read-only boundary. The adapter lifecycle IS the native
749
- * runtime path, so the hosting adapter is native-capable by construction; the
750
- * runtime's own enforceability then decides whether a role receives a native
751
- * bounded mission lens (`resolution.native`) or falls closed to the v1.2 pack.
752
- */
753
- async function resolveBoundRoleDispatchModes(input) {
754
- const protocol = await loadOperatingProtocol();
755
- const registryDefaults = new Map(protocol.listOperatingRoles().map((role) => [
756
- role.id,
757
- operatingRegistryDispatchMode(role),
758
- ]));
759
- const overrides = await readOperatingDispatchModeOverrides(input.projectRoot, {
760
- localRoot: input.localRoot,
761
- });
762
- const runtimeEnforcesBoundedReadOnly = await operatingRuntimeEnforcesBoundedReadOnly(input.runtime);
763
- const resolved = new Map();
764
- for (const roleId of input.roles) {
765
- resolved.set(roleId, resolveOperatingDispatchMode({
766
- roleId: roleId,
767
- registryDefault: registryDefaults.get(roleId) ?? 'mission',
768
- override: overrides[roleId],
769
- runtimeEnforcesBoundedReadOnly,
770
- adapterNativeCapable: true,
771
- }));
680
+ async function deriveOperatingMandateRoots(projectRoot) {
681
+ const entries = await readdir(projectRoot, { withFileTypes: true }).catch(() => []);
682
+ const roots = new Set(['.planr']);
683
+ for (const entry of entries) {
684
+ if (!entry.isDirectory())
685
+ continue;
686
+ if (entry.name === '.git' || entry.name === 'node_modules')
687
+ continue;
688
+ if (MANDATE_ROOT_PATTERN.test(entry.name))
689
+ roots.add(entry.name);
772
690
  }
773
- return resolved;
691
+ return [...roots].sort();
774
692
  }
775
693
  export async function createOperatingAdapterStartHandoff(input) {
776
694
  const target = adapterSessionPath(input.projectRoot, input.cycleId, input.localRoot);
@@ -797,19 +715,8 @@ export async function createOperatingAdapterStartHandoff(input) {
797
715
  }
798
716
  }
799
717
  const protocol = await loadOperatingProtocol();
800
- // FR1 call site 2 (the start / prepare-required handoff): stamp v1.3 whenever
801
- // any bound role would resolve to native mission dispatch on this runtime, so
802
- // the whole lifecycle — including the record actions built after prepare —
803
- // carries the mission protocol version rather than defaulting to pack.
804
- const dispatchModes = await resolveBoundRoleDispatchModes({
805
- projectRoot: input.projectRoot,
806
- localRoot: input.localRoot,
807
- runtime: input.runtime,
808
- roles: input.roles,
809
- });
810
- const anyMission = [...dispatchModes.values()].some((resolution) => resolution.native);
811
718
  const handoff = protocol.createOperatingAdapterHandoff({
812
- ...(anyMission ? { protocolVersion: '1.3.0' } : {}),
719
+ protocolVersion: '1.3.0',
813
720
  phase: input.phase,
814
721
  state: 'prepare-required',
815
722
  cycleId: input.cycleId,
@@ -938,171 +845,38 @@ export async function operateAdapterLifecycle(input) {
938
845
  // FR4: a finalized/expired/cancelled same-board session no longer forces
939
846
  // a whole-cycle-cancelling error on a new compatible prepare. The
940
847
  // advisors→chair continuation and a same-phase re-prepare both fall
941
- // through and supersede the dead session; an exact dead session still
942
- // recovers its already-built packs so recorded advisory work is reused
943
- // rather than recomputed.
848
+ // through and supersede the dead session; an exact dead session reuses
849
+ // its already-built mandates so recorded advisory work is not recomputed.
944
850
  if (exact && (sessionExpired(normalized) || normalized.state === 'cancelled')) {
945
851
  recoverableSession = normalized;
946
852
  }
947
853
  }
948
854
  }
949
- const roleEvidence = roles[0] === 'chair'
950
- ? buildChairEvidence(baseEvidence, await readPersistedOperatingRoleResults(store, input.cycleId), new Date().toISOString())
951
- : baseEvidence;
952
- const readiness = await evaluateEvidenceReadiness({
953
- cycleId: input.cycleId,
954
- evidence: roleEvidence,
955
- enabledRoles: roles,
956
- now: new Date(),
957
- });
958
- const unready = readiness.roles.filter((role) => !role.modelCallAllowed);
959
- if (unready.length > 0) {
960
- throw new OperateError('E_OPERATE_EVIDENCE_NOT_READY', `Native advisor preparation is blocked for: ${unready
961
- .map((role) => role.roleId)
962
- .sort()
963
- .join(', ')}.`);
964
- }
965
- const context = await buildAdvisorOperatingContext({
966
- charterPath: resolveOperatingPaths(input.projectRoot, {
967
- localRoot: input.localRoot,
968
- }).charter,
969
- state,
970
- cycleId: input.cycleId,
971
- });
972
- // FR1: resolve each requested role's dispatch mode for THIS runtime. A role
973
- // that resolves to a native bounded lens (`resolution.native`) gets a
974
- // body-free v1.3 mission packet; every other role keeps the v1.2 role pack.
975
- // A recovered dead-but-exact session reuses its stored packs/packets verbatim.
976
- const dispatchModes = await resolveBoundRoleDispatchModes({
977
- projectRoot: input.projectRoot,
978
- localRoot: input.localRoot,
979
- runtime,
980
- roles,
981
- });
982
- const missionRoleIds = readiness.roles
983
- .map((role) => role.roleId)
984
- .filter((roleId) => dispatchModes.get(roleId)?.native);
985
- const packReadinessRoles = readiness.roles.filter((role) => !dispatchModes.get(role.roleId)?.native);
986
- const rolePacks = recoverableSession?.rolePacks ??
987
- Object.fromEntries(await Promise.all(packReadinessRoles.map(async (role) => [
988
- role.roleId,
989
- await createOperatingAdvisorPack({
990
- cycleId: input.cycleId,
991
- role,
992
- evidence: roleEvidence,
993
- context,
994
- }),
995
- ])));
996
- // FR1: build the natively-dispatched roles' mission packets from live
997
- // cycle/workspace state. Only reached when a role actually resolved mission,
998
- // so a pure pack-mode prepare never loads config/workspace and stays
999
- // byte-compatible with the v1.2 path. The evidence is ceiling-narrowed by
1000
- // `narrowMissionRootsToCeiling` first, so a declared read root can never reach
1001
- // an above-ceiling file even before the bounded reader's read-time check.
1002
- // FR2: after narrowing, the same POST-index gate the readiness evaluator now
1003
- // understands runs here — a mission role whose repository items were all
1004
- // dropped by the dot-prefixed/pattern filter retains zero repository index
1005
- // items, is gated not-ready with a governed gap, and is dropped from
1006
- // `missionRoleIds` instead of dispatching on an empty evidence base. The
1007
- // other ready roles still dispatch.
1008
- let starvedMissionRoleIds = [];
1009
- let evidenceGaps = [];
1010
- // FR4: index truncations the published pipeline performed for over-cap roles,
1011
- // surfaced onto the prepare result so the drop reaches the cycle as a warning.
1012
- let missionEvidenceTruncations = [];
1013
- const roleMissionPackets = recoverableSession?.roleMissionPackets ??
1014
- (missionRoleIds.length > 0
1015
- ? await (async () => {
1016
- const [config, workspace, sensitivityCeiling] = await Promise.all([
1017
- readOperatingConfig(input.projectRoot, { localRoot: input.localRoot }),
1018
- refreshOperatingWorkspaceManifest(input.projectRoot, { localRoot: input.localRoot }),
1019
- readAdapterSensitivityCeiling(input.projectRoot, input.localRoot),
1020
- ]);
1021
- const narrowedEvidence = narrowEvidenceToMissionCeiling(roleEvidence, sensitivityCeiling);
1022
- const missionEvidenceIndex = buildOperatingEvidenceIndex(narrowedEvidence, {
1023
- sensitivityCeiling,
1024
- });
1025
- const postIndexReadiness = await evaluateEvidenceReadiness({
1026
- cycleId: input.cycleId,
1027
- evidence: narrowedEvidence,
1028
- enabledRoles: missionRoleIds,
1029
- now: new Date(),
1030
- missionEvidenceIndex,
1031
- });
1032
- starvedMissionRoleIds = postIndexReadiness.roles
1033
- .filter((role) => !role.modelCallAllowed)
1034
- .map((role) => role.roleId);
1035
- const dispatchableMissionRoleIds = missionRoleIds.filter((roleId) => !starvedMissionRoleIds.includes(roleId));
1036
- if (starvedMissionRoleIds.length > 0) {
1037
- evidenceGaps = await starvedRoleEvidenceGaps({
1038
- cycleId: input.cycleId,
1039
- roleIds: starvedMissionRoleIds,
1040
- owner: config.decisionOwner,
1041
- now: new Date().toISOString(),
1042
- });
1043
- }
1044
- if (dispatchableMissionRoleIds.length === 0)
1045
- return {};
1046
- // FR4: pass the configured item budget as the upper bound; each role's
1047
- // effective cap is the smaller of it and the role's registry-sized
1048
- // default. A monorepo-scale index is truncated (highest-priority items
1049
- // kept) so its packet fits the derived budget instead of failing
1050
- // closed; the truncation record is captured for the cycle warnings.
1051
- const { packets, truncations } = await buildOperatingMissionPackets({
1052
- cycleId: input.cycleId,
1053
- config,
1054
- workspace,
1055
- context,
1056
- evidence: narrowedEvidence,
1057
- roleIds: dispatchableMissionRoleIds,
1058
- sensitivityCeiling,
1059
- maxEvidenceItems: config.budgets.maxItems,
1060
- });
1061
- missionEvidenceTruncations = truncations;
1062
- return Object.fromEntries(packets.map((packet) => [packet.roleId, packet]));
1063
- })()
1064
- : {});
1065
- // FR2: the session's bound role set drops any starved mission role, so its
1066
- // handoff, briefs, and input digests never reference a role that will not
1067
- // dispatch. When nothing is starved this is exactly `roles`, keeping the
1068
- // pack-mode / healthy-mission prepare byte-identical.
1069
- const dispatchedRoles = starvedMissionRoleIds.length > 0
1070
- ? roles.filter((role) => !starvedMissionRoleIds.includes(role))
1071
- : roles;
1072
- // FR2: fail closed before a native adapter session is assembled. A freshly
1073
- // built pack is already measured inside `createOperatingAdvisorPack`, but the
1074
- // session may instead be RECOVERED from disk (`recoverableSession`) — possibly
1075
- // written before pack-budget enforcement existed. Re-measure every pack that
1076
- // will back this session against its role's published v1.2 `maxInputBytes`, so
1077
- // an oversized pack can never reach the adapter from either source instead of
1078
- // silently constructing an oversized session. (Mission packets are measured
1079
- // against their derived mission budget inside `buildOperatingMissionPackets`.)
1080
- const packInputBudgets = new Map(protocol.listOperatingRoles().map((role) => {
1081
- const maxInputBytes = role.budgets
1082
- ?.maxInputBytes;
1083
- return [role.id, typeof maxInputBytes === 'number' ? maxInputBytes : 262_144];
1084
- }));
1085
- for (const [roleId, pack] of Object.entries(rolePacks)) {
1086
- assertOperatingAdvisorPackWithinBudget(pack, packInputBudgets.get(roleId) ?? 262_144);
1087
- }
1088
- // A mission role's brief facet is the same registry-derived brief pack mode
1089
- // uses, so the record path's brief-bound machinery (output limits, contract
1090
- // details, output-vs-brief checks) applies uniformly across both modes.
1091
- const roleBriefs = {
1092
- ...Object.fromEntries(Object.entries(rolePacks).map(([role, pack]) => [role, pack.roleBrief])),
1093
- ...Object.fromEntries(Object.keys(roleMissionPackets).map((role) => [
1094
- role,
1095
- protocol.createOperatingAdvisorBrief(role),
1096
- ])),
1097
- };
1098
- // A role's committed input digest is its pack's `inputDigest` (pack mode) or
1099
- // its mission packet's `packetDigest` (mission mode). The record path binds a
1100
- // recorded result to exactly this value, and a recovered machine-local result
1101
- // is re-adopted only when it still matches it.
1102
- const roleInputDigests = Object.fromEntries(dispatchedRoles.map((role) => {
1103
- const digest = roleMissionPackets[role]?.packetDigest ?? rolePacks[role]?.inputDigest;
1104
- return [role, digest];
1105
- }));
855
+ // FR1/FR2: every requested role — the Chair included — dispatches an operating
856
+ // MANDATE. The role-dependent pack/mission split is retired, so there is no
857
+ // pre-dispatch readiness gate against collected evidence and no v1.2 role pack
858
+ // is built. A mandate carries the lens question, the investigation mandate, the
859
+ // declared read boundaries (the granted workspace roots — including a
860
+ // gitignored `.planr/` tree — the registry sensitivity ceiling, and any
861
+ // forbidden paths), the response schema, and the citation requirement. It
862
+ // carries NO evidence body and NO evidence index: nothing is collected,
863
+ // budgeted, packed, or pre-filtered. A response that grounds nothing is
864
+ // post-gated at record time by the universal citation gate
865
+ // (`gateRecordedProposalCitations`). A recovered dead-but-exact session reuses
866
+ // its stored mandates verbatim.
867
+ const roleMandates = recoverableSession?.roleMandates && Object.keys(recoverableSession.roleMandates).length > 0
868
+ ? recoverableSession.roleMandates
869
+ : await (async () => {
870
+ const roots = await deriveOperatingMandateRoots(input.projectRoot);
871
+ const built = await Promise.all(roles.map(async (roleId) => [roleId, await buildOperatingMandate({ roleId, roots })]));
872
+ return Object.fromEntries(built);
873
+ })();
874
+ // Every requested role dispatches; the bound role set is exactly `roles`.
875
+ const dispatchedRoles = roles;
876
+ const roleBriefs = Object.fromEntries(Object.keys(roleMandates).map((role) => [role, protocol.createOperatingAdvisorBrief(role)]));
877
+ // A role's committed input digest is its mandate digest. The record path
878
+ // binds a recorded result to exactly this value.
879
+ const roleInputDigests = Object.fromEntries(dispatchedRoles.map((role) => [role, roleMandates[role].mandateDigest]));
1106
880
  const validRecordedRoles = [];
1107
881
  for (const role of dispatchedRoles) {
1108
882
  const prior = await readFile(path.join(path.dirname(target), `${input.cycleId}.${role}.json`), 'utf8')
@@ -1137,27 +911,13 @@ export async function operateAdapterLifecycle(input) {
1137
911
  roles: dispatchedRoles,
1138
912
  recordedRoles: validRecordedRoles.sort(),
1139
913
  roleBriefs,
1140
- rolePacks,
1141
- // Omitted entirely for a pure pack-mode session so its on-disk shape stays
1142
- // byte-compatible with the v1.2 path.
1143
- ...(Object.keys(roleMissionPackets).length > 0 ? { roleMissionPackets } : {}),
914
+ roleMandates,
1144
915
  roleInputDigests,
1145
916
  };
1146
917
  await atomicPrivateWrite(target, session);
1147
918
  return {
1148
919
  ...session,
1149
- // Expose the mission packets at `/data/missionPackets/<role>` so the v1.3
1150
- // record action's `dispatch.missionPacketPointer` resolves against the
1151
- // prepare result; pack roles keep resolving at `/data/rolePacks/<role>`.
1152
- ...(Object.keys(roleMissionPackets).length > 0 ? { missionPackets: roleMissionPackets } : {}),
1153
- // FR2: surface the governed gaps for any starved mission role that was
1154
- // dropped from dispatch. Omitted when nothing was starved so a healthy
1155
- // prepare result stays byte-identical.
1156
- ...(evidenceGaps.length > 0 ? { evidenceGaps } : {}),
1157
- // FR4: surface the evidence-index truncation warnings for any over-cap
1158
- // mission role so the drop reaches the cycle, never a silent drop. Omitted
1159
- // when nothing was truncated so a healthy prepare result stays byte-identical.
1160
- ...(missionEvidenceTruncations.length > 0 ? { missionEvidenceTruncations } : {}),
920
+ mandates: roleMandates,
1161
921
  leaseStatus: adapterLeaseStatus(session, nowMs),
1162
922
  handoff: await adapterHandoff(session),
1163
923
  };
@@ -1240,9 +1000,12 @@ export async function operateAdapterLifecycle(input) {
1240
1000
  }
1241
1001
  const submittedRecord = submitted && typeof submitted === 'object' ? submitted : {};
1242
1002
  let result;
1243
- // FR1: unresolvable-citation gaps opened while resolving a mission response's
1244
- // citations, surfaced in the record response for observability.
1003
+ // FR1/FR2: unresolvable-citation and empty-grounding gaps opened while
1004
+ // resolving a mandate response's citations, surfaced in the record response
1005
+ // for observability. `recordNotEvaluated` marks a role whose citations
1006
+ // grounded zero evidence (committed as a quiet result plus the governed gap).
1245
1007
  let recordGaps = [];
1008
+ let recordNotEvaluated = false;
1246
1009
  if (submittedRecord.kind === 'operating-role-result') {
1247
1010
  throw new OperateError('E_OPERATE_ADVISOR_FAILED', 'Native advisors must submit only the compact response contract; OpenPlanr owns canonical result metadata.', {
1248
1011
  ...advisorResponseContractDetails(session.roleBriefs[input.role]),
@@ -1271,50 +1034,38 @@ export async function operateAdapterLifecycle(input) {
1271
1034
  throw new OperateError('E_OPERATE_SECRET_DETECTED', `Native advisor response contains a secret at ${field.location}; nothing was persisted.`, { roleId: input.role, field: field.location });
1272
1035
  }
1273
1036
  }
1274
- const missionPacket = session.roleMissionPackets?.[input.role];
1037
+ const mandate = session.roleMandates[input.role];
1275
1038
  try {
1276
- if (missionPacket) {
1277
- // FR1: a mission-mode role submits the v1.3 citation-bearing response.
1278
- // Its citations flow into the engine's already-live citation gate,
1279
- // which resolves them against the cycle's pinned revision and mints the
1280
- // evidenceRefs that make the committed result v1.2-valid — the pack
1281
- // path is untouched.
1282
- const gated = await createNativeMissionOperatingRoleResult({
1283
- packet: missionPacket,
1284
- response: submitted,
1285
- runtime: session.runtime,
1286
- resolveCitations: async (roleResults) => {
1287
- const [workspace, sensitivityCeiling, config] = await Promise.all([
1288
- refreshOperatingWorkspaceManifest(input.projectRoot, {
1039
+ const gated = await createNativeMissionOperatingRoleResult({
1040
+ mandate,
1041
+ cycleId: input.cycleId,
1042
+ response: submitted,
1043
+ runtime: session.runtime,
1044
+ resolveCitations: async (roleResults) => {
1045
+ const [workspace, sensitivityCeiling, config] = await Promise.all([
1046
+ refreshOperatingWorkspaceManifest(input.projectRoot, {
1047
+ localRoot: input.localRoot,
1048
+ }),
1049
+ readAdapterSensitivityCeiling(input.projectRoot, input.localRoot),
1050
+ readOperatingConfig(input.projectRoot, { localRoot: input.localRoot }).catch(() => null),
1051
+ ]);
1052
+ return gateRecordedProposalCitations({
1053
+ roleResults,
1054
+ context: {
1055
+ projectRoot: input.projectRoot,
1056
+ cycleId: input.cycleId,
1057
+ descriptor: workspace.controlRepository,
1058
+ cache: new OperatingEvidenceCache(resolveOperatingPaths(input.projectRoot, {
1289
1059
  localRoot: input.localRoot,
1290
- }),
1291
- readAdapterSensitivityCeiling(input.projectRoot, input.localRoot),
1292
- readOperatingConfig(input.projectRoot, { localRoot: input.localRoot }).catch(() => null),
1293
- ]);
1294
- return gateRecordedProposalCitations({
1295
- roleResults,
1296
- context: {
1297
- projectRoot: input.projectRoot,
1298
- cycleId: input.cycleId,
1299
- descriptor: workspace.controlRepository,
1300
- cache: new OperatingEvidenceCache(resolveOperatingPaths(input.projectRoot, {
1301
- localRoot: input.localRoot,
1302
- }).evidence, sensitivityCeiling),
1303
- owner: config?.decisionOwner,
1304
- },
1305
- });
1306
- },
1307
- });
1308
- result = gated.result;
1309
- recordGaps = gated.gaps;
1310
- }
1311
- else {
1312
- result = await createNativeOperatingRoleResult({
1313
- pack: session.rolePacks[input.role],
1314
- response: submitted,
1315
- runtime: session.runtime,
1316
- });
1317
- }
1060
+ }).evidence, sensitivityCeiling),
1061
+ owner: config?.decisionOwner,
1062
+ },
1063
+ });
1064
+ },
1065
+ });
1066
+ result = gated.result;
1067
+ recordGaps = gated.gaps;
1068
+ recordNotEvaluated = gated.notEvaluated;
1318
1069
  }
1319
1070
  catch (error) {
1320
1071
  if (error instanceof OperateError && error.code === 'E_OPERATE_ADVISOR_FAILED') {
@@ -1391,6 +1142,7 @@ export async function operateAdapterLifecycle(input) {
1391
1142
  recorded: input.role,
1392
1143
  result,
1393
1144
  ...(recordGaps.length > 0 ? { citationGaps: recordGaps } : {}),
1145
+ ...(recordNotEvaluated ? { notEvaluated: true } : {}),
1394
1146
  session: adapterSessionSummary(updated),
1395
1147
  leaseStatus: adapterLeaseStatus(updated, nowMs),
1396
1148
  handoff: await adapterHandoff(updated),