openplanr 1.17.0 → 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 (63) 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 +55 -7
  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 +37 -1
  10. package/dist/services/operate/advisors.d.ts.map +1 -1
  11. package/dist/services/operate/advisors.js +351 -10
  12. package/dist/services/operate/advisors.js.map +1 -1
  13. package/dist/services/operate/config.d.ts +26 -0
  14. package/dist/services/operate/config.d.ts.map +1 -1
  15. package/dist/services/operate/config.js +41 -0
  16. package/dist/services/operate/config.js.map +1 -1
  17. package/dist/services/operate/doctor.d.ts.map +1 -1
  18. package/dist/services/operate/doctor.js +121 -1
  19. package/dist/services/operate/doctor.js.map +1 -1
  20. package/dist/services/operate/evidence.d.ts.map +1 -1
  21. package/dist/services/operate/evidence.js +29 -0
  22. package/dist/services/operate/evidence.js.map +1 -1
  23. package/dist/services/operate/index.d.ts +2 -0
  24. package/dist/services/operate/index.d.ts.map +1 -1
  25. package/dist/services/operate/index.js +237 -41
  26. package/dist/services/operate/index.js.map +1 -1
  27. package/dist/services/operate/interaction/answer-service.d.ts +17 -0
  28. package/dist/services/operate/interaction/answer-service.d.ts.map +1 -1
  29. package/dist/services/operate/interaction/answer-service.js +49 -7
  30. package/dist/services/operate/interaction/answer-service.js.map +1 -1
  31. package/dist/services/operate/interaction/question-engine.d.ts.map +1 -1
  32. package/dist/services/operate/interaction/question-engine.js +9 -3
  33. package/dist/services/operate/interaction/question-engine.js.map +1 -1
  34. package/dist/services/operate/interaction/question-registry.d.ts +13 -0
  35. package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
  36. package/dist/services/operate/interaction/question-registry.js +116 -26
  37. package/dist/services/operate/interaction/question-registry.js.map +1 -1
  38. package/dist/services/operate/interaction/terminal-renderer.d.ts +12 -1
  39. package/dist/services/operate/interaction/terminal-renderer.d.ts.map +1 -1
  40. package/dist/services/operate/interaction/terminal-renderer.js +26 -15
  41. package/dist/services/operate/interaction/terminal-renderer.js.map +1 -1
  42. package/dist/services/operate/lifecycle.d.ts +8 -0
  43. package/dist/services/operate/lifecycle.d.ts.map +1 -1
  44. package/dist/services/operate/lifecycle.js +19 -1
  45. package/dist/services/operate/lifecycle.js.map +1 -1
  46. package/dist/services/operate/maintenance.d.ts +22 -0
  47. package/dist/services/operate/maintenance.d.ts.map +1 -1
  48. package/dist/services/operate/maintenance.js +397 -44
  49. package/dist/services/operate/maintenance.js.map +1 -1
  50. package/dist/services/operate/protocol.d.ts +4 -1
  51. package/dist/services/operate/protocol.d.ts.map +1 -1
  52. package/dist/services/operate/protocol.js.map +1 -1
  53. package/dist/services/operate/reports.d.ts +9 -0
  54. package/dist/services/operate/reports.d.ts.map +1 -1
  55. package/dist/services/operate/reports.js +9 -0
  56. package/dist/services/operate/reports.js.map +1 -1
  57. package/dist/services/operate/types.d.ts +19 -0
  58. package/dist/services/operate/types.d.ts.map +1 -1
  59. package/dist/services/operate/types.js.map +1 -1
  60. package/dist/services/runtime-manager-service.d.ts.map +1 -1
  61. package/dist/services/runtime-manager-service.js +25 -5
  62. package/dist/services/runtime-manager-service.js.map +1 -1
  63. package/package.json +2 -2
@@ -1,5 +1,7 @@
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';
3
5
  import { computeNextDueAt } from './cadence.js';
4
6
  import { canonicalDigest, sha256Digest } from './canonical.js';
5
7
  import { applyOperatingInitialization, getOperatingProfile, listOperatingProfiles, normalizeCustomOperatingProfile, normalizeOperatingInitializationAnswers, parseOperatingDispatchModeOverrideFlags, prepareOperatingInitialization, readOperatingLastRunAt, validateOperatingConfiguration, } from './config.js';
@@ -9,7 +11,7 @@ import { classifyEvidenceDiagnostic } from './evidence-classifications.js';
9
11
  import { listEvidenceDiagnostics, readEvidenceDiagnostic } from './evidence-diagnostics.js';
10
12
  import { parseStrictJson, readImportedEvidenceFile } from './evidence-import.js';
11
13
  import { createOperatingAction } from './interaction/action-service.js';
12
- import { persistableOperatingInitAnswers, resumeGuidedSession, submitGuidedAnswers, } from './interaction/answer-service.js';
14
+ import { persistableOperatingInitAnswers, probeAvailableEvidenceSources, probeGitUserName, probePipelineInstalled, resumeGuidedSession, submitGuidedAnswers, } from './interaction/answer-service.js';
13
15
  import { assertOperatingConfirmation } from './interaction/confirmation-service.js';
14
16
  import { decodeOperatingInitializationReplay, encodeOperatingInitializationReplay, } from './interaction/initialization-replay.js';
15
17
  import { createOperatingInitQuestionnaire, evaluateOperatingInitQuestions, operatingInitAnswersFromOptions, } from './interaction/question-engine.js';
@@ -17,7 +19,7 @@ import { cancelGuidedSession, createGuidedSession, createGuidedSessionId, curren
17
19
  import { assertCommittedOperatingView } from './journal.js';
18
20
  import { applyOperatingMigration, inspectOperatingMigration, rollbackOperatingMigration, } from './legacy-import-service.js';
19
21
  import { answerOperatingGap, applyOrRollbackRoute, decideOperatingDecision, governOperatingFinding, readOperatingCollection, readOperatingReview, transitionOperatingCycle, verifyOperatingGap, } from './lifecycle.js';
20
- import { createOperatingAdapterStartHandoff, exportOperatingDiagnostics, operateAdapterLifecycle, operatingCacheAction, operatingIntegrityAction, repairOperatingSecurity, } from './maintenance.js';
22
+ import { createOperatingAdapterStartHandoff, exportOperatingDiagnostics, operateAdapterLifecycle, operatingCacheAction, operatingIntegrityAction, purgeBoardMachineLocalCaches, repairOperatingSecurity, } from './maintenance.js';
21
23
  import { migrateOperatingStorageLayoutOnOpen } from './migration.js';
22
24
  import { renderOperatingBrief } from './projection.js';
23
25
  import { loadOperatingProtocol, resolveOperatingPipelineRoot } from './protocol.js';
@@ -50,17 +52,43 @@ function stringList(value) {
50
52
  .filter(Boolean);
51
53
  return [];
52
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
+ }
53
74
  async function resolvedOperatingRuntime(projectRoot, requested) {
54
75
  if (requested !== 'auto')
55
76
  return requested;
56
- return readFile(path.join(resolveOperatingPaths(projectRoot).localRoot, 'preferences.json'), 'utf8')
77
+ const persisted = await readFile(path.join(resolveOperatingPaths(projectRoot).localRoot, 'preferences.json'), 'utf8')
57
78
  .then((raw) => {
58
79
  const runtime = JSON.parse(raw).runtime;
59
80
  return typeof runtime === 'string' && runtime ? runtime : 'auto';
60
81
  })
61
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';
62
90
  }
63
- async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
91
+ export async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
64
92
  const runtime = await resolvedOperatingRuntime(projectRoot, requestedRuntime);
65
93
  const adapterId = runtime === 'claude' ? 'claude-code' : runtime;
66
94
  if (adapterId === 'auto')
@@ -74,7 +102,10 @@ async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
74
102
  const adapter = registry.adapters?.find((entry) => entry.id === adapterId);
75
103
  if (adapter?.capabilities?.operatingBoard !== true)
76
104
  return false;
77
- 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 ?? '')) {
78
109
  return true;
79
110
  }
80
111
  // Compatibility with Protocol v1.2 registries published before the
@@ -123,6 +154,47 @@ function success(action, value = {}) {
123
154
  nextActions,
124
155
  };
125
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
+ }
126
198
  /**
127
199
  * Stable process exit classes for automation.
128
200
  *
@@ -135,6 +207,15 @@ function success(action, value = {}) {
135
207
  *
136
208
  * Keep this exhaustive so adding a public Operate error cannot silently fall
137
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.
138
219
  */
139
220
  const OPERATE_EXIT_CODES = {
140
221
  E_OPERATE_INTERNAL: 1,
@@ -187,8 +268,16 @@ const OPERATE_EXIT_CODES = {
187
268
  function operateExitCode(code) {
188
269
  return OPERATE_EXIT_CODES[code];
189
270
  }
190
- function failure(action, error) {
271
+ export function failure(action, error) {
191
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;
192
281
  const confirmationAction = error instanceof OperateError &&
193
282
  error.details?.action &&
194
283
  typeof error.details.action === 'object' &&
@@ -248,11 +337,29 @@ function failure(action, error) {
248
337
  counts: {},
249
338
  warnings: [],
250
339
  nextActions,
251
- data: error instanceof OperateError ? error.details : undefined,
340
+ data: error instanceof OperateError
341
+ ? error.details
342
+ : internalErrorClass
343
+ ? { errorClass: internalErrorClass }
344
+ : undefined,
252
345
  next: nextActions,
253
346
  exitCode: operateExitCode(code),
254
347
  };
255
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
+ */
256
363
  function publicActionCommands(nextActions) {
257
364
  return [
258
365
  ...new Set(nextActions
@@ -265,6 +372,17 @@ function publicActionCommands(nextActions) {
265
372
  .filter((value) => /^planr\s+/.test(value))),
266
373
  ];
267
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
+ }
268
386
  function hasFlag(command, flag) {
269
387
  return new RegExp(`(?:^|\\s)${flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|$)`).test(command);
270
388
  }
@@ -300,8 +418,9 @@ async function actionEffect(request, command) {
300
418
  return 'project-write';
301
419
  }
302
420
  async function attachStructuredActions(request, result) {
303
- if (result.actions?.length || result.action === 'input_required')
421
+ if (result.actions?.length || result.action === 'input_required' || result.flow === 'handoff') {
304
422
  return result;
423
+ }
305
424
  const commands = publicActionCommands(result.nextActions);
306
425
  if (commands.length === 0)
307
426
  return result;
@@ -327,6 +446,7 @@ async function attachStructuredActions(request, result) {
327
446
  const actions = [];
328
447
  for (const [index, command] of commands.entries()) {
329
448
  const effect = await actionEffect(request, command);
449
+ const confirmFlag = commandConfirmFlag(command);
330
450
  const id = `operate.next.${canonicalDigest({ command }).slice('sha256:'.length, 'sha256:'.length + 20)}`.toLowerCase();
331
451
  const created = await createOperatingAction({
332
452
  id,
@@ -356,8 +476,29 @@ async function attachStructuredActions(request, result) {
356
476
  },
357
477
  }),
358
478
  });
359
- 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);
360
499
  }
500
+ if (actions.length === 0)
501
+ return result;
361
502
  return { ...result, actions };
362
503
  }
363
504
  async function inspect(request) {
@@ -563,22 +704,14 @@ async function initialize(request) {
563
704
  });
564
705
  supplied = normalizeOperatingInitializationAnswers(resumedSession.answers);
565
706
  if (resumedSession.status === 'input-required') {
566
- return {
567
- schemaVersion: '1.0.0',
568
- protocolVersion: '1.2.0',
569
- ok: false,
570
- action: 'input_required',
571
- 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', {
572
711
  message: 'Operating Board initialization needs explicit human input.',
573
712
  state: resumedSession.session.state,
574
- paths: {},
575
- counts: {},
576
- warnings: [],
577
- nextActions: [],
578
- next: [],
579
713
  questionnaire: resumedSession.questionnaire,
580
- exitCode: operateExitCode('E_OPERATE_INPUT_REQUIRED'),
581
- };
714
+ });
582
715
  }
583
716
  }
584
717
  let customProfile;
@@ -590,13 +723,36 @@ async function initialize(request) {
590
723
  supplied.profileFile) {
591
724
  throw new OperateError('E_OPERATE_CONFIG_INVALID', '--profile-file is valid only with --profile custom.');
592
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
+ ]);
593
740
  const context = {
594
741
  projectRoot: request.projectRoot,
595
742
  ...bindings,
743
+ ...(detectedHostRuntime ? { detectedRuntime: detectedHostRuntime } : {}),
744
+ ...(gitUserName ? { gitUserName } : {}),
596
745
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
597
- availableSources: ['repository', 'planr', 'git', 'file-import'],
598
- runtime: option(request, 'runtime', 'unknown'),
599
- 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',
600
756
  };
601
757
  const questionState = await evaluateOperatingInitQuestions({
602
758
  answers: supplied,
@@ -617,22 +773,12 @@ async function initialize(request) {
617
773
  persistedAnswers: persistableOperatingInitAnswers(questionState.answers, context),
618
774
  localRoot,
619
775
  });
620
- return {
621
- schemaVersion: '1.0.0',
622
- protocolVersion: '1.2.0',
623
- ok: false,
624
- action: 'input_required',
625
- code: 'E_OPERATE_INPUT_REQUIRED',
626
- message: 'Operating Board initialization needs explicit human input.',
627
- state: null,
628
- paths: {},
629
- counts: {},
630
- warnings: [],
631
- nextActions: [],
632
- 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', {
633
780
  questionnaire,
634
- exitCode: operateExitCode('E_OPERATE_INPUT_REQUIRED'),
635
- };
781
+ });
636
782
  }
637
783
  const profile = supplied.profile;
638
784
  const decisionOwner = supplied.decisionOwner ?? option(request, 'decisionOwner', '').trim();
@@ -755,6 +901,11 @@ async function initialize(request) {
755
901
  preview,
756
902
  confirmationDigest: preview.previewDigest,
757
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 });
758
909
  if (resumedSession) {
759
910
  const appliedAt = new Date().toISOString();
760
911
  await updateGuidedSession({
@@ -980,6 +1131,27 @@ async function run(request) {
980
1131
  !option(request, 'dryRun', false) &&
981
1132
  !option(request, 'reviewOnly', false) &&
982
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
+ }
983
1155
  const result = await runOperatingCycle({
984
1156
  projectRoot: request.projectRoot,
985
1157
  cycleId: option(request, 'cycleId', undefined),
@@ -1041,7 +1213,13 @@ async function run(request) {
1041
1213
  },
1042
1214
  counts,
1043
1215
  handoff,
1044
- 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
+ ],
1045
1223
  nextActions,
1046
1224
  data: result,
1047
1225
  next: nextActions,
@@ -1049,10 +1227,16 @@ async function run(request) {
1049
1227
  }
1050
1228
  async function reviewOrBrief(request) {
1051
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);
1052
1234
  const data = await readOperatingReview({
1053
1235
  projectRoot: request.projectRoot,
1054
1236
  cycleId,
1055
1237
  brief: request.action === 'brief',
1238
+ human,
1239
+ localRoot: option(request, 'localRoot', undefined),
1056
1240
  });
1057
1241
  return success(request.action, {
1058
1242
  data,
@@ -1469,6 +1653,18 @@ export async function executeOperateAction(request) {
1469
1653
  return await attachStructuredActions(request, await handler(request));
1470
1654
  }
1471
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
+ }
1472
1668
  try {
1473
1669
  return await attachStructuredActions(request, failure(request.action, error));
1474
1670
  }