openplanr 1.16.2 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/cli/commands/operate.d.ts +10 -1
  2. package/dist/cli/commands/operate.d.ts.map +1 -1
  3. package/dist/cli/commands/operate.js +240 -13
  4. package/dist/cli/commands/operate.js.map +1 -1
  5. package/dist/services/ai-service.d.ts +25 -0
  6. package/dist/services/ai-service.d.ts.map +1 -1
  7. package/dist/services/ai-service.js +38 -0
  8. package/dist/services/ai-service.js.map +1 -1
  9. package/dist/services/operate/advisors.d.ts +82 -1
  10. package/dist/services/operate/advisors.d.ts.map +1 -1
  11. package/dist/services/operate/advisors.js +469 -17
  12. package/dist/services/operate/advisors.js.map +1 -1
  13. package/dist/services/operate/artifacts.d.ts +27 -1
  14. package/dist/services/operate/artifacts.d.ts.map +1 -1
  15. package/dist/services/operate/artifacts.js +103 -1
  16. package/dist/services/operate/artifacts.js.map +1 -1
  17. package/dist/services/operate/cadence.d.ts +50 -0
  18. package/dist/services/operate/cadence.d.ts.map +1 -0
  19. package/dist/services/operate/cadence.js +56 -0
  20. package/dist/services/operate/cadence.js.map +1 -0
  21. package/dist/services/operate/citation-resolution.d.ts +101 -0
  22. package/dist/services/operate/citation-resolution.d.ts.map +1 -0
  23. package/dist/services/operate/citation-resolution.js +308 -0
  24. package/dist/services/operate/citation-resolution.js.map +1 -0
  25. package/dist/services/operate/config.d.ts +93 -1
  26. package/dist/services/operate/config.d.ts.map +1 -1
  27. package/dist/services/operate/config.js +199 -9
  28. package/dist/services/operate/config.js.map +1 -1
  29. package/dist/services/operate/decision-brief.d.ts +148 -0
  30. package/dist/services/operate/decision-brief.d.ts.map +1 -0
  31. package/dist/services/operate/decision-brief.js +194 -0
  32. package/dist/services/operate/decision-brief.js.map +1 -0
  33. package/dist/services/operate/doctor.d.ts.map +1 -1
  34. package/dist/services/operate/doctor.js +271 -3
  35. package/dist/services/operate/doctor.js.map +1 -1
  36. package/dist/services/operate/engine.d.ts +66 -2
  37. package/dist/services/operate/engine.d.ts.map +1 -1
  38. package/dist/services/operate/engine.js +219 -7
  39. package/dist/services/operate/engine.js.map +1 -1
  40. package/dist/services/operate/event-store.d.ts +35 -1
  41. package/dist/services/operate/event-store.d.ts.map +1 -1
  42. package/dist/services/operate/event-store.js +142 -29
  43. package/dist/services/operate/event-store.js.map +1 -1
  44. package/dist/services/operate/evidence-cache.d.ts +25 -1
  45. package/dist/services/operate/evidence-cache.d.ts.map +1 -1
  46. package/dist/services/operate/evidence-cache.js +62 -1
  47. package/dist/services/operate/evidence-cache.js.map +1 -1
  48. package/dist/services/operate/evidence.d.ts +20 -1
  49. package/dist/services/operate/evidence.d.ts.map +1 -1
  50. package/dist/services/operate/evidence.js +148 -0
  51. package/dist/services/operate/evidence.js.map +1 -1
  52. package/dist/services/operate/index.d.ts +2 -0
  53. package/dist/services/operate/index.d.ts.map +1 -1
  54. package/dist/services/operate/index.js +309 -46
  55. package/dist/services/operate/index.js.map +1 -1
  56. package/dist/services/operate/interaction/action-service.d.ts +17 -0
  57. package/dist/services/operate/interaction/action-service.d.ts.map +1 -1
  58. package/dist/services/operate/interaction/action-service.js +19 -0
  59. package/dist/services/operate/interaction/action-service.js.map +1 -1
  60. package/dist/services/operate/interaction/answer-service.d.ts +17 -0
  61. package/dist/services/operate/interaction/answer-service.d.ts.map +1 -1
  62. package/dist/services/operate/interaction/answer-service.js +49 -7
  63. package/dist/services/operate/interaction/answer-service.js.map +1 -1
  64. package/dist/services/operate/interaction/question-engine.d.ts.map +1 -1
  65. package/dist/services/operate/interaction/question-engine.js +9 -3
  66. package/dist/services/operate/interaction/question-engine.js.map +1 -1
  67. package/dist/services/operate/interaction/question-registry.d.ts +13 -0
  68. package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
  69. package/dist/services/operate/interaction/question-registry.js +116 -26
  70. package/dist/services/operate/interaction/question-registry.js.map +1 -1
  71. package/dist/services/operate/interaction/terminal-renderer.d.ts +12 -1
  72. package/dist/services/operate/interaction/terminal-renderer.d.ts.map +1 -1
  73. package/dist/services/operate/interaction/terminal-renderer.js +26 -15
  74. package/dist/services/operate/interaction/terminal-renderer.js.map +1 -1
  75. package/dist/services/operate/journal.d.ts.map +1 -1
  76. package/dist/services/operate/journal.js +11 -5
  77. package/dist/services/operate/journal.js.map +1 -1
  78. package/dist/services/operate/lifecycle.d.ts +8 -0
  79. package/dist/services/operate/lifecycle.d.ts.map +1 -1
  80. package/dist/services/operate/lifecycle.js +19 -1
  81. package/dist/services/operate/lifecycle.js.map +1 -1
  82. package/dist/services/operate/maintenance.d.ts +22 -0
  83. package/dist/services/operate/maintenance.d.ts.map +1 -1
  84. package/dist/services/operate/maintenance.js +406 -46
  85. package/dist/services/operate/maintenance.js.map +1 -1
  86. package/dist/services/operate/migration.d.ts +68 -0
  87. package/dist/services/operate/migration.d.ts.map +1 -0
  88. package/dist/services/operate/migration.js +235 -0
  89. package/dist/services/operate/migration.js.map +1 -0
  90. package/dist/services/operate/mission-dispatch.d.ts +193 -0
  91. package/dist/services/operate/mission-dispatch.d.ts.map +1 -0
  92. package/dist/services/operate/mission-dispatch.js +444 -0
  93. package/dist/services/operate/mission-dispatch.js.map +1 -0
  94. package/dist/services/operate/projection-persistence.d.ts +6 -0
  95. package/dist/services/operate/projection-persistence.d.ts.map +1 -1
  96. package/dist/services/operate/projection-persistence.js +87 -11
  97. package/dist/services/operate/projection-persistence.js.map +1 -1
  98. package/dist/services/operate/projection.d.ts +26 -1
  99. package/dist/services/operate/projection.d.ts.map +1 -1
  100. package/dist/services/operate/projection.js +62 -0
  101. package/dist/services/operate/projection.js.map +1 -1
  102. package/dist/services/operate/protocol.d.ts +50 -3
  103. package/dist/services/operate/protocol.d.ts.map +1 -1
  104. package/dist/services/operate/protocol.js +57 -5
  105. package/dist/services/operate/protocol.js.map +1 -1
  106. package/dist/services/operate/read-only-providers.d.ts +37 -0
  107. package/dist/services/operate/read-only-providers.d.ts.map +1 -1
  108. package/dist/services/operate/read-only-providers.js +130 -0
  109. package/dist/services/operate/read-only-providers.js.map +1 -1
  110. package/dist/services/operate/reports.d.ts +22 -0
  111. package/dist/services/operate/reports.d.ts.map +1 -1
  112. package/dist/services/operate/reports.js +226 -1
  113. package/dist/services/operate/reports.js.map +1 -1
  114. package/dist/services/operate/routes.d.ts.map +1 -1
  115. package/dist/services/operate/routes.js +79 -5
  116. package/dist/services/operate/routes.js.map +1 -1
  117. package/dist/services/operate/types.d.ts +139 -4
  118. package/dist/services/operate/types.d.ts.map +1 -1
  119. package/dist/services/operate/types.js +13 -0
  120. package/dist/services/operate/types.js.map +1 -1
  121. package/dist/services/operate/workspace.d.ts +7 -0
  122. package/dist/services/operate/workspace.d.ts.map +1 -1
  123. package/dist/services/operate/workspace.js +14 -5
  124. package/dist/services/operate/workspace.js.map +1 -1
  125. package/dist/services/runtime-manager-service.d.ts.map +1 -1
  126. package/dist/services/runtime-manager-service.js +25 -5
  127. package/dist/services/runtime-manager-service.js.map +1 -1
  128. package/package.json +2 -2
@@ -1,14 +1,17 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { resolveAIProviderReadiness } from '../ai-service.js';
4
+ import { loadConfig } from '../config-service.js';
5
+ import { computeNextDueAt } from './cadence.js';
3
6
  import { canonicalDigest, sha256Digest } from './canonical.js';
4
- import { applyOperatingInitialization, getOperatingProfile, listOperatingProfiles, normalizeCustomOperatingProfile, normalizeOperatingInitializationAnswers, prepareOperatingInitialization, validateOperatingConfiguration, } from './config.js';
7
+ import { applyOperatingInitialization, getOperatingProfile, listOperatingProfiles, normalizeCustomOperatingProfile, normalizeOperatingInitializationAnswers, parseOperatingDispatchModeOverrideFlags, prepareOperatingInitialization, readOperatingLastRunAt, validateOperatingConfiguration, } from './config.js';
5
8
  import { runOperatingCycle } from './engine.js';
6
9
  import { OperatingEventStore } from './event-store.js';
7
10
  import { classifyEvidenceDiagnostic } from './evidence-classifications.js';
8
11
  import { listEvidenceDiagnostics, readEvidenceDiagnostic } from './evidence-diagnostics.js';
9
12
  import { parseStrictJson, readImportedEvidenceFile } from './evidence-import.js';
10
13
  import { createOperatingAction } from './interaction/action-service.js';
11
- import { persistableOperatingInitAnswers, resumeGuidedSession, submitGuidedAnswers, } from './interaction/answer-service.js';
14
+ import { persistableOperatingInitAnswers, probeAvailableEvidenceSources, probeGitUserName, probePipelineInstalled, resumeGuidedSession, submitGuidedAnswers, } from './interaction/answer-service.js';
12
15
  import { assertOperatingConfirmation } from './interaction/confirmation-service.js';
13
16
  import { decodeOperatingInitializationReplay, encodeOperatingInitializationReplay, } from './interaction/initialization-replay.js';
14
17
  import { createOperatingInitQuestionnaire, evaluateOperatingInitQuestions, operatingInitAnswersFromOptions, } from './interaction/question-engine.js';
@@ -16,7 +19,8 @@ import { cancelGuidedSession, createGuidedSession, createGuidedSessionId, curren
16
19
  import { assertCommittedOperatingView } from './journal.js';
17
20
  import { applyOperatingMigration, inspectOperatingMigration, rollbackOperatingMigration, } from './legacy-import-service.js';
18
21
  import { answerOperatingGap, applyOrRollbackRoute, decideOperatingDecision, governOperatingFinding, readOperatingCollection, readOperatingReview, transitionOperatingCycle, verifyOperatingGap, } from './lifecycle.js';
19
- import { createOperatingAdapterStartHandoff, exportOperatingDiagnostics, operateAdapterLifecycle, operatingCacheAction, operatingIntegrityAction, repairOperatingSecurity, } from './maintenance.js';
22
+ import { createOperatingAdapterStartHandoff, exportOperatingDiagnostics, operateAdapterLifecycle, operatingCacheAction, operatingIntegrityAction, purgeBoardMachineLocalCaches, repairOperatingSecurity, } from './maintenance.js';
23
+ import { migrateOperatingStorageLayoutOnOpen } from './migration.js';
20
24
  import { renderOperatingBrief } from './projection.js';
21
25
  import { loadOperatingProtocol, resolveOperatingPipelineRoot } from './protocol.js';
22
26
  import { executeGitHubReadOnly, executeGitReadOnly } from './read-only-providers.js';
@@ -48,17 +52,43 @@ function stringList(value) {
48
52
  .filter(Boolean);
49
53
  return [];
50
54
  }
55
+ /**
56
+ * Probe the coding-runtime identity of the host process from the environment
57
+ * markers set by the agent that launched the CLI. Mirrors the compatible-runtime
58
+ * detection in terminal-renderer.ts's `detectOperatingQuestionContext`, but keys
59
+ * off the launcher's env markers so a non-interactive/JSON invocation can stamp a
60
+ * truthful adapter block and resolve `auto` instead of stamping `unknown`/`none`
61
+ * or silently disabling native dispatch.
62
+ */
63
+ function detectOperatingHostRuntime() {
64
+ const env = process.env;
65
+ const marker = (value) => (value ?? '').length > 0;
66
+ if (marker(env.CLAUDECODE) || marker(env.CLAUDE_CODE_ENTRYPOINT))
67
+ return 'claude';
68
+ if (marker(env.CURSOR_TRACE_ID) || marker(env.CURSOR_AGENT))
69
+ return 'cursor';
70
+ if (marker(env.CODEX_SANDBOX) || marker(env.CODEX_HOME))
71
+ return 'codex';
72
+ return undefined;
73
+ }
51
74
  async function resolvedOperatingRuntime(projectRoot, requested) {
52
75
  if (requested !== 'auto')
53
76
  return requested;
54
- return readFile(path.join(resolveOperatingPaths(projectRoot).localRoot, 'preferences.json'), 'utf8')
77
+ const persisted = await readFile(path.join(resolveOperatingPaths(projectRoot).localRoot, 'preferences.json'), 'utf8')
55
78
  .then((raw) => {
56
79
  const runtime = JSON.parse(raw).runtime;
57
80
  return typeof runtime === 'string' && runtime ? runtime : 'auto';
58
81
  })
59
82
  .catch(() => 'auto');
83
+ if (persisted !== 'auto')
84
+ return persisted;
85
+ // A persisted (or absent) `auto` preference must never silently disable native
86
+ // dispatch inside a capable host: resolve it to the detected runtime identity
87
+ // so `usesNativeOperatingAdvisors` evaluates the real host, not the `auto`
88
+ // placeholder that always returns false.
89
+ return detectOperatingHostRuntime() ?? 'auto';
60
90
  }
61
- async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
91
+ export async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
62
92
  const runtime = await resolvedOperatingRuntime(projectRoot, requestedRuntime);
63
93
  const adapterId = runtime === 'claude' ? 'claude-code' : runtime;
64
94
  if (adapterId === 'auto')
@@ -72,7 +102,10 @@ async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
72
102
  const adapter = registry.adapters?.find((entry) => entry.id === adapterId);
73
103
  if (adapter?.capabilities?.operatingBoard !== true)
74
104
  return false;
75
- if (['native-isolated', 'native-bounded'].includes(adapter.capabilities.operatingAdvisorDispatch ?? '')) {
105
+ if (
106
+ // `native-read-only` is the US-001/T-001 adapters-registry capability name that
107
+ // supersedes the earlier isolation labels; recognize it alongside them.
108
+ ['native-isolated', 'native-bounded', 'native-read-only'].includes(adapter.capabilities.operatingAdvisorDispatch ?? '')) {
76
109
  return true;
77
110
  }
78
111
  // Compatibility with Protocol v1.2 registries published before the
@@ -121,6 +154,47 @@ function success(action, value = {}) {
121
154
  nextActions,
122
155
  };
123
156
  }
157
+ /**
158
+ * A healthy continuation (FR7/E-007): a guided-stage advance
159
+ * (`E_OPERATE_INPUT_REQUIRED`) or first-use provider consent
160
+ * (`E_OPERATE_AUTHORITY_REQUIRED`) is not a failure. It is returned as an
161
+ * `ok: true` handoff carrying a machine-readable `flow: 'handoff'` discriminator
162
+ * (and the originating `code`), mirroring `run`'s adapter-handoff shape so a
163
+ * harness reads the pause without treating it as a red exit. No `exitCode` is
164
+ * set: the CLI leaves the process exit code at 0 for `ok: true` results.
165
+ */
166
+ function handoffContinuation(action, code, value = {}) {
167
+ return { ...success(action, value), code, flow: 'handoff' };
168
+ }
169
+ /**
170
+ * First-use / renewal provider consent is disclosed by
171
+ * `ensureOperatingProviderConsent` (advisors.ts) as an
172
+ * `E_OPERATE_AUTHORITY_REQUIRED` carrying the full policy disclosure
173
+ * (`endpoint`, `permittedDataClasses`, `policyDigest`). That specific
174
+ * disclosure is a continuation, not a refusal — unlike every other
175
+ * `E_OPERATE_AUTHORITY_REQUIRED` (a mutation attempted without `--yes`), which
176
+ * stays an `ok: false` exit-4 failure so the authority model is unchanged.
177
+ */
178
+ function isProviderConsentHandoff(error) {
179
+ if (!(error instanceof OperateError) || error.code !== 'E_OPERATE_AUTHORITY_REQUIRED') {
180
+ return false;
181
+ }
182
+ const details = error.details;
183
+ return (typeof details === 'object' &&
184
+ details !== null &&
185
+ 'policyDigest' in details &&
186
+ 'endpoint' in details &&
187
+ 'permittedDataClasses' in details);
188
+ }
189
+ function providerConsentContinuation(action, error) {
190
+ const retry = `planr operate ${action} --yes`;
191
+ return handoffContinuation(action, 'E_OPERATE_AUTHORITY_REQUIRED', {
192
+ message: error.message,
193
+ data: error.details,
194
+ nextActions: [retry],
195
+ next: [retry],
196
+ });
197
+ }
124
198
  /**
125
199
  * Stable process exit classes for automation.
126
200
  *
@@ -133,6 +207,15 @@ function success(action, value = {}) {
133
207
  *
134
208
  * Keep this exhaustive so adding a public Operate error cannot silently fall
135
209
  * through to the internal-error class.
210
+ *
211
+ * FR7/E-007 continuation note: `E_OPERATE_INPUT_REQUIRED` and
212
+ * `E_OPERATE_AUTHORITY_REQUIRED` keep this class-4 mapping for the cases that
213
+ * are genuine refusals — a mutation attempted without `--yes` still fails with
214
+ * `ok: false` and exit 4. But a *healthy continuation* — a guided-stage advance
215
+ * or first-use provider consent — is not a failure: it is returned as an
216
+ * `ok: true` handoff (`flow: 'handoff'`) with no failure exit code, mirroring
217
+ * `run`'s adapter handoff, so a harness never paints the happy path red. The
218
+ * numeric class below is therefore only consulted for the failure branch.
136
219
  */
137
220
  const OPERATE_EXIT_CODES = {
138
221
  E_OPERATE_INTERNAL: 1,
@@ -178,13 +261,23 @@ const OPERATE_EXIT_CODES = {
178
261
  E_OPERATE_EVIDENCE_REJECTED: 6,
179
262
  E_OPERATE_PROVIDER_READ_ONLY: 6,
180
263
  E_OPERATE_SECRET_DETECTED: 6,
264
+ E_OPERATE_MISSION_PACKET_BUDGET: 6,
265
+ E_OPERATE_MISSION_UNAVAILABLE: 3,
181
266
  E_OPERATE_SESSION_INVALID: 2,
182
267
  };
183
268
  function operateExitCode(code) {
184
269
  return OPERATE_EXIT_CODES[code];
185
270
  }
186
- function failure(action, error) {
271
+ export function failure(action, error) {
187
272
  const code = error instanceof OperateError ? error.code : 'E_OPERATE_INTERNAL';
273
+ // E_OPERATE_INTERNAL must never be zero-information: record the redacted error
274
+ // class/name (no message or stack, which can carry paths or secrets) so the
275
+ // diagnostics export and automation callers can classify the internal failure.
276
+ const internalErrorClass = code === 'E_OPERATE_INTERNAL'
277
+ ? error instanceof Error && typeof error.name === 'string' && error.name.length > 0
278
+ ? error.name
279
+ : 'Error'
280
+ : undefined;
188
281
  const confirmationAction = error instanceof OperateError &&
189
282
  error.details?.action &&
190
283
  typeof error.details.action === 'object' &&
@@ -244,11 +337,29 @@ function failure(action, error) {
244
337
  counts: {},
245
338
  warnings: [],
246
339
  nextActions,
247
- data: error instanceof OperateError ? error.details : undefined,
340
+ data: error instanceof OperateError
341
+ ? error.details
342
+ : internalErrorClass
343
+ ? { errorClass: internalErrorClass }
344
+ : undefined,
248
345
  next: nextActions,
249
346
  exitCode: operateExitCode(code),
250
347
  };
251
348
  }
349
+ /**
350
+ * Reduce recovery `nextActions` to the set of public `planr operate` commands
351
+ * that back the structured actions. The digest-bound authority flags
352
+ * (`--yes`, `--confirm <digest>`, `--preview-digest <digest>`) are stripped from
353
+ * the *command* string — a raw sha256 digest must never enter a structured
354
+ * command (it would trip the sensitive-data guard in `assertSafeCommand`).
355
+ *
356
+ * FR8/E-008: stripping them here no longer strands a runner. For a
357
+ * digest-confirmable command the exact, ready-to-run argv (including the real
358
+ * `--confirm <digest> --yes` token) is re-surfaced on the structured action as
359
+ * `confirmArgv` in `attachStructuredActions`, so the runner never has to
360
+ * re-synthesize a confirmation token it was handed. A command whose only
361
+ * authority is `--yes` is never given a confirmationDigest at all.
362
+ */
252
363
  function publicActionCommands(nextActions) {
253
364
  return [
254
365
  ...new Set(nextActions
@@ -261,6 +372,17 @@ function publicActionCommands(nextActions) {
261
372
  .filter((value) => /^planr\s+/.test(value))),
262
373
  ];
263
374
  }
375
+ /**
376
+ * The digest-bound confirmation flag a public command's CLI actually accepts, or
377
+ * `null` when its only authority is `--yes`. A confirmationDigest is meaningful
378
+ * only for a command that can consume it via `--confirm`; a `--yes`-only command
379
+ * (e.g. `operate run`) must never be handed one (FR8/E-008).
380
+ */
381
+ function commandConfirmFlag(command) {
382
+ return /\boperate\s+init\b/.test(command) || /\bevidence\s+classify\b/.test(command)
383
+ ? '--confirm'
384
+ : null;
385
+ }
264
386
  function hasFlag(command, flag) {
265
387
  return new RegExp(`(?:^|\\s)${flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|$)`).test(command);
266
388
  }
@@ -296,8 +418,9 @@ async function actionEffect(request, command) {
296
418
  return 'project-write';
297
419
  }
298
420
  async function attachStructuredActions(request, result) {
299
- if (result.actions?.length || result.action === 'input_required')
421
+ if (result.actions?.length || result.action === 'input_required' || result.flow === 'handoff') {
300
422
  return result;
423
+ }
301
424
  const commands = publicActionCommands(result.nextActions);
302
425
  if (commands.length === 0)
303
426
  return result;
@@ -323,6 +446,7 @@ async function attachStructuredActions(request, result) {
323
446
  const actions = [];
324
447
  for (const [index, command] of commands.entries()) {
325
448
  const effect = await actionEffect(request, command);
449
+ const confirmFlag = commandConfirmFlag(command);
326
450
  const id = `operate.next.${canonicalDigest({ command }).slice('sha256:'.length, 'sha256:'.length + 20)}`.toLowerCase();
327
451
  const created = await createOperatingAction({
328
452
  id,
@@ -352,8 +476,29 @@ async function attachStructuredActions(request, result) {
352
476
  },
353
477
  }),
354
478
  });
355
- actions.push(created.action);
479
+ let action = created.action;
480
+ // FR8/E-008: `operate run` authorizes with `--yes` alone — its CLI accepts no
481
+ // `--confirm` flag — so it must never carry a confirmationDigest a runner
482
+ // could never pass. It still appears as a structured action (mirroring the
483
+ // handoff `run` continuation) but with its digest binding cleared.
484
+ if (/\boperate\s+run\b/.test(command)) {
485
+ action = {
486
+ ...action,
487
+ requiresConfirmation: false,
488
+ confirmationScope: null,
489
+ confirmationDigest: null,
490
+ };
491
+ }
492
+ // A digest-confirmable action (`--confirm <digest>`) carries its exact,
493
+ // ready-to-run argv so a runner never has to re-synthesize the confirmation
494
+ // token it was already handed.
495
+ const confirmArgv = confirmFlag && action.confirmationDigest
496
+ ? [...command.split(/\s+/), confirmFlag, action.confirmationDigest, '--yes']
497
+ : undefined;
498
+ actions.push(confirmArgv ? { ...action, confirmArgv } : action);
356
499
  }
500
+ if (actions.length === 0)
501
+ return result;
357
502
  return { ...result, actions };
358
503
  }
359
504
  async function inspect(request) {
@@ -559,22 +704,14 @@ async function initialize(request) {
559
704
  });
560
705
  supplied = normalizeOperatingInitializationAnswers(resumedSession.answers);
561
706
  if (resumedSession.status === 'input-required') {
562
- return {
563
- schemaVersion: '1.0.0',
564
- protocolVersion: '1.2.0',
565
- ok: false,
566
- action: 'input_required',
567
- code: 'E_OPERATE_INPUT_REQUIRED',
707
+ // FR7/E-007: a guided-stage advance is a healthy continuation, not a
708
+ // failure. Report it as an `ok: true` handoff (`flow: 'handoff'`) carrying
709
+ // the next questionnaire, mirroring `run`'s adapter handoff.
710
+ return handoffContinuation('input_required', 'E_OPERATE_INPUT_REQUIRED', {
568
711
  message: 'Operating Board initialization needs explicit human input.',
569
712
  state: resumedSession.session.state,
570
- paths: {},
571
- counts: {},
572
- warnings: [],
573
- nextActions: [],
574
- next: [],
575
713
  questionnaire: resumedSession.questionnaire,
576
- exitCode: operateExitCode('E_OPERATE_INPUT_REQUIRED'),
577
- };
714
+ });
578
715
  }
579
716
  }
580
717
  let customProfile;
@@ -586,13 +723,36 @@ async function initialize(request) {
586
723
  supplied.profileFile) {
587
724
  throw new OperateError('E_OPERATE_CONFIG_INVALID', '--profile-file is valid only with --profile custom.');
588
725
  }
726
+ // Probe the real host runtime instead of stamping `unknown`/`none`: an explicit
727
+ // --runtime flag wins, otherwise the launcher's env markers name the host so the
728
+ // questionnaire's adapter block is truthful (and the runtime question can be a
729
+ // detect-don't-ask suggestion rather than a required prompt).
730
+ const detectedHostRuntime = detectOperatingHostRuntime();
731
+ const requestedRuntimeOption = option(request, 'runtime', 'auto');
732
+ // Probe the same signals the terminal path does so the JSON/native init path is
733
+ // equally truthful: the decision-owner suggestion reaches this path (gitUserName),
734
+ // the "locally available" source claim is real (availableSources), and the
735
+ // planning-engine detects pipeline-po when a compatible pipeline is installed.
736
+ const [gitUserName, availableSources] = await Promise.all([
737
+ probeGitUserName(request.projectRoot),
738
+ probeAvailableEvidenceSources(request.projectRoot),
739
+ ]);
589
740
  const context = {
590
741
  projectRoot: request.projectRoot,
591
742
  ...bindings,
743
+ ...(detectedHostRuntime ? { detectedRuntime: detectedHostRuntime } : {}),
744
+ ...(gitUserName ? { gitUserName } : {}),
592
745
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
593
- availableSources: ['repository', 'planr', 'git', 'file-import'],
594
- runtime: option(request, 'runtime', 'unknown'),
595
- interaction: request.interactive ? 'terminal' : 'none',
746
+ availableSources,
747
+ pipelineInstalled: probePipelineInstalled(),
748
+ runtime: requestedRuntimeOption && requestedRuntimeOption !== 'auto'
749
+ ? requestedRuntimeOption
750
+ : (detectedHostRuntime ?? (request.interactive ? 'terminal' : 'unknown')),
751
+ interaction: detectedHostRuntime
752
+ ? 'native'
753
+ : request.interactive
754
+ ? 'terminal'
755
+ : 'none',
596
756
  };
597
757
  const questionState = await evaluateOperatingInitQuestions({
598
758
  answers: supplied,
@@ -613,28 +773,24 @@ async function initialize(request) {
613
773
  persistedAnswers: persistableOperatingInitAnswers(questionState.answers, context),
614
774
  localRoot,
615
775
  });
616
- return {
617
- schemaVersion: '1.0.0',
618
- protocolVersion: '1.2.0',
619
- ok: false,
620
- action: 'input_required',
621
- code: 'E_OPERATE_INPUT_REQUIRED',
622
- message: 'Operating Board initialization needs explicit human input.',
623
- state: null,
624
- paths: {},
625
- counts: {},
626
- warnings: [],
627
- nextActions: [],
628
- next: [],
776
+ // FR7/E-007: the first guided-stage prompt is a healthy continuation — an
777
+ // `ok: true` handoff (`flow: 'handoff'`) carrying the questionnaire, not an
778
+ // exit-4 failure.
779
+ return handoffContinuation('input_required', 'E_OPERATE_INPUT_REQUIRED', {
629
780
  questionnaire,
630
- exitCode: operateExitCode('E_OPERATE_INPUT_REQUIRED'),
631
- };
781
+ });
632
782
  }
633
783
  const profile = supplied.profile;
634
784
  const decisionOwner = supplied.decisionOwner ?? option(request, 'decisionOwner', '').trim();
635
785
  const planningEngine = supplied.planningEngine ??
636
786
  option(request, 'planningEngine', 'openplanr');
637
787
  const rawCharter = supplied.charter ?? option(request, 'charter', {});
788
+ // FR4 / E-004 fold-in: forward the CLI-validated per-project dispatch-mode
789
+ // overrides into initialization so they are part of the committed machine-local
790
+ // preferences (and thus the preview digest), rather than a separate post-apply
791
+ // patch. Omitted entirely when no override flag was supplied, keeping the
792
+ // no-override preview digest byte-identical.
793
+ const dispatchModeOverrides = parseOperatingDispatchModeOverrideFlags(stringList(request.options.dispatchModeOverride));
638
794
  const preview = await prepareOperatingInitialization({
639
795
  projectRoot: request.projectRoot,
640
796
  profile,
@@ -642,6 +798,7 @@ async function initialize(request) {
642
798
  planningEngine,
643
799
  runtime: supplied.runtime ?? option(request, 'runtime', 'auto'),
644
800
  cadence: supplied.cadence ?? option(request, 'cadence', 'manual'),
801
+ ...(Object.keys(dispatchModeOverrides).length > 0 ? { dispatchModeOverrides } : {}),
645
802
  timezone: supplied.timezone ??
646
803
  option(request, 'timezone', Intl.DateTimeFormat().resolvedOptions().timeZone),
647
804
  sensitivityCeiling: supplied.sensitivityCeiling ?? option(request, 'sensitivityCeiling', 'internal'),
@@ -744,6 +901,11 @@ async function initialize(request) {
744
901
  preview,
745
902
  confirmationDigest: preview.previewDigest,
746
903
  });
904
+ // FR4: a committed init apply is a fresh (re-genesised) board. Purge the
905
+ // machine-local advisor sessions and incremental evidence baselines a prior
906
+ // generation left at this path so the new board never inherits a stale
907
+ // session or a baseline bound to a superseded workspace/board identity.
908
+ await purgeBoardMachineLocalCaches({ projectRoot: request.projectRoot, localRoot });
747
909
  if (resumedSession) {
748
910
  const appliedAt = new Date().toISOString();
749
911
  await updateGuidedSession({
@@ -935,16 +1097,24 @@ async function testOperatingSource(request, provider) {
935
1097
  return { provider, healthy: true, observation, writeBoundary: 'none' };
936
1098
  }
937
1099
  async function status(request) {
938
- await validateOperatingConfiguration(request.projectRoot);
939
- await assertCommittedOperatingView(request.projectRoot);
940
- const store = new OperatingEventStore(request.projectRoot);
1100
+ const config = await validateOperatingConfiguration(request.projectRoot);
1101
+ const localRoot = option(request, 'localRoot', undefined);
1102
+ await assertCommittedOperatingView(request.projectRoot, localRoot ? { localRoot } : {});
1103
+ const store = new OperatingEventStore(request.projectRoot, localRoot ? { localRoot } : {});
941
1104
  const state = await store.state();
942
1105
  const activeCycle = [...state.cycles]
943
1106
  .filter((cycle) => !['closed', 'cancelled'].includes(cycle.state))
944
1107
  .sort((left, right) => left.id.localeCompare(right.id))
945
1108
  .at(-1);
1109
+ // FR8 / E-008: surface `nextDueAt` via the pipeline's pure calculator with an
1110
+ // INJECTED clock. This CLI boundary supplies `now` (an explicit option or the
1111
+ // wall clock); the calculator itself reads no `Date.now()`. `manual` → null;
1112
+ // `weekly` / `monthly` → the computed due date from the persisted `lastRunAt`.
1113
+ const now = option(request, 'now', undefined) ?? new Date().toISOString();
1114
+ const lastRunAt = await readOperatingLastRunAt(request.projectRoot, localRoot ? { localRoot } : {});
1115
+ const nextDueAt = await computeNextDueAt(config.cadence, lastRunAt, now);
946
1116
  return success(request.action, {
947
- data: state,
1117
+ data: { ...state, cadence: { mode: config.cadence, lastRunAt, nextDueAt } },
948
1118
  message: activeCycle?.state === 'blocked'
949
1119
  ? `Operating Board is blocked on ${state.summary.openGaps} evidence or advisor readiness gap(s).`
950
1120
  : state.summary.quiet
@@ -961,6 +1131,27 @@ async function run(request) {
961
1131
  !option(request, 'dryRun', false) &&
962
1132
  !option(request, 'reviewOnly', false) &&
963
1133
  nativeAdvisors;
1134
+ // Preflight the structured-provider key on a non-offline, non-native preview so
1135
+ // `run --preview` names a missing key before any cycle starts, rather than
1136
+ // surfacing it only when a real cycle reaches the provider path. Native and
1137
+ // offline runs never need the structured key, so they skip the check.
1138
+ const previewProviderWarnings = [];
1139
+ if (option(request, 'preview', false) &&
1140
+ !option(request, 'offline', false) &&
1141
+ !option(request, 'reviewOnly', false) &&
1142
+ !nativeAdvisors) {
1143
+ const openPlanrConfig = await loadConfig(request.projectRoot).catch(() => null);
1144
+ const readiness = openPlanrConfig
1145
+ ? await resolveAIProviderReadiness(openPlanrConfig)
1146
+ : {
1147
+ configured: false,
1148
+ keyResolvable: false,
1149
+ remedy: 'No AI provider is configured. Run `planr config set-provider <name>` then `planr config set-key <provider>`, or run offline with --offline.',
1150
+ };
1151
+ if (!readiness.keyResolvable && readiness.remedy) {
1152
+ previewProviderWarnings.push(readiness.remedy);
1153
+ }
1154
+ }
964
1155
  const result = await runOperatingCycle({
965
1156
  projectRoot: request.projectRoot,
966
1157
  cycleId: option(request, 'cycleId', undefined),
@@ -1022,7 +1213,13 @@ async function run(request) {
1022
1213
  },
1023
1214
  counts,
1024
1215
  handoff,
1025
- warnings: [...new Set([...(result.cycle.warnings ?? []), ...stringList(projected?.warnings)])],
1216
+ warnings: [
1217
+ ...new Set([
1218
+ ...(result.cycle.warnings ?? []),
1219
+ ...stringList(projected?.warnings),
1220
+ ...previewProviderWarnings,
1221
+ ]),
1222
+ ],
1026
1223
  nextActions,
1027
1224
  data: result,
1028
1225
  next: nextActions,
@@ -1030,10 +1227,16 @@ async function run(request) {
1030
1227
  }
1031
1228
  async function reviewOrBrief(request) {
1032
1229
  const cycleId = argument(request, 'cycleId');
1230
+ // FR3/E-003: the human review gate renders report Markdown (brief + per-role
1231
+ // lens reports + exact next actions), never a raw `JSON.stringify` of the
1232
+ // state. `--json` keeps returning the exact raw state object, byte-unchanged.
1233
+ const human = request.action === 'review' && !option(request, 'json', false);
1033
1234
  const data = await readOperatingReview({
1034
1235
  projectRoot: request.projectRoot,
1035
1236
  cycleId,
1036
1237
  brief: request.action === 'brief',
1238
+ human,
1239
+ localRoot: option(request, 'localRoot', undefined),
1037
1240
  });
1038
1241
  return success(request.action, {
1039
1242
  data,
@@ -1388,6 +1591,47 @@ const HANDLERS = {
1388
1591
  'adapter.finalize': maintenance,
1389
1592
  'adapter.cancel': maintenance,
1390
1593
  };
1594
+ /**
1595
+ * Actions that only read committed state. A SPEC-002-layout project stays
1596
+ * readable through these without being migrated; only a mutating action opening
1597
+ * such a project triggers the automatic, journal-safe v1.3 migration (FR5/E-005).
1598
+ */
1599
+ const OPERATE_READ_ONLY_ACTIONS = new Set([
1600
+ 'inspect',
1601
+ 'demo',
1602
+ 'config.show',
1603
+ 'config.validate',
1604
+ 'config.edit',
1605
+ 'profiles.list',
1606
+ 'profiles.show',
1607
+ 'profiles.validate',
1608
+ 'sources.list',
1609
+ 'sources.show',
1610
+ 'sources.test',
1611
+ 'status',
1612
+ 'review',
1613
+ 'brief',
1614
+ 'report',
1615
+ 'cycles.list',
1616
+ 'cycles.show',
1617
+ 'findings.list',
1618
+ 'findings.show',
1619
+ 'routes.list',
1620
+ 'routes.show',
1621
+ 'decisions.list',
1622
+ 'decisions.show',
1623
+ 'gaps.list',
1624
+ 'gaps.show',
1625
+ 'evidence.list',
1626
+ 'evidence.show',
1627
+ 'evidence.diagnose',
1628
+ 'migrate.inspect',
1629
+ 'migrations.list',
1630
+ 'migrations.show',
1631
+ 'cache.status',
1632
+ 'integrity.status',
1633
+ 'adapter.resume',
1634
+ ]);
1391
1635
  /**
1392
1636
  * Stable runtime-neutral Operating Board facade. Public CLI adapters only parse
1393
1637
  * arguments and render this structured result; all state and security semantics
@@ -1399,9 +1643,28 @@ export async function executeOperateAction(request) {
1399
1643
  if (!handler) {
1400
1644
  throw new OperateError('E_OPERATE_ACTION_UNKNOWN', `Unknown Operating Board action: ${request.action}.`);
1401
1645
  }
1646
+ // Any mutating action that opens a SPEC-002-layout project migrates it to the
1647
+ // v1.3 storage layout, through the write-ahead journal, before proceeding.
1648
+ if (!OPERATE_READ_ONLY_ACTIONS.has(request.action)) {
1649
+ await migrateOperatingStorageLayoutOnOpen(request.projectRoot, {
1650
+ localRoot: option(request, 'localRoot', undefined),
1651
+ });
1652
+ }
1402
1653
  return await attachStructuredActions(request, await handler(request));
1403
1654
  }
1404
1655
  catch (error) {
1656
+ // FR7/E-007: first-use provider consent is a healthy continuation, not a
1657
+ // failure — return the `ok: true` handoff shape instead of an exit-4 error.
1658
+ // Every other authority/error stays a genuine failure.
1659
+ if (isProviderConsentHandoff(error)) {
1660
+ const continuation = providerConsentContinuation(request.action, error);
1661
+ try {
1662
+ return await attachStructuredActions(request, continuation);
1663
+ }
1664
+ catch {
1665
+ return continuation;
1666
+ }
1667
+ }
1405
1668
  try {
1406
1669
  return await attachStructuredActions(request, failure(request.action, error));
1407
1670
  }