openplanr 1.14.4 → 1.15.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 (75) hide show
  1. package/dist/cli/commands/operate.d.ts.map +1 -1
  2. package/dist/cli/commands/operate.js +54 -87
  3. package/dist/cli/commands/operate.js.map +1 -1
  4. package/dist/services/operate/config.d.ts +4 -1
  5. package/dist/services/operate/config.d.ts.map +1 -1
  6. package/dist/services/operate/config.js +56 -2
  7. package/dist/services/operate/config.js.map +1 -1
  8. package/dist/services/operate/doctor.d.ts.map +1 -1
  9. package/dist/services/operate/doctor.js +15 -0
  10. package/dist/services/operate/doctor.js.map +1 -1
  11. package/dist/services/operate/evidence-classifications.d.ts +59 -0
  12. package/dist/services/operate/evidence-classifications.d.ts.map +1 -0
  13. package/dist/services/operate/evidence-classifications.js +132 -0
  14. package/dist/services/operate/evidence-classifications.js.map +1 -0
  15. package/dist/services/operate/evidence-diagnostics.d.ts +23 -0
  16. package/dist/services/operate/evidence-diagnostics.d.ts.map +1 -0
  17. package/dist/services/operate/evidence-diagnostics.js +152 -0
  18. package/dist/services/operate/evidence-diagnostics.js.map +1 -0
  19. package/dist/services/operate/evidence.d.ts.map +1 -1
  20. package/dist/services/operate/evidence.js +54 -7
  21. package/dist/services/operate/evidence.js.map +1 -1
  22. package/dist/services/operate/index.d.ts.map +1 -1
  23. package/dist/services/operate/index.js +459 -62
  24. package/dist/services/operate/index.js.map +1 -1
  25. package/dist/services/operate/interaction/action-service.d.ts +17 -0
  26. package/dist/services/operate/interaction/action-service.d.ts.map +1 -0
  27. package/dist/services/operate/interaction/action-service.js +84 -0
  28. package/dist/services/operate/interaction/action-service.js.map +1 -0
  29. package/dist/services/operate/interaction/answer-service.d.ts +33 -0
  30. package/dist/services/operate/interaction/answer-service.d.ts.map +1 -0
  31. package/dist/services/operate/interaction/answer-service.js +239 -0
  32. package/dist/services/operate/interaction/answer-service.js.map +1 -0
  33. package/dist/services/operate/interaction/charter-suggestions.d.ts +29 -0
  34. package/dist/services/operate/interaction/charter-suggestions.d.ts.map +1 -0
  35. package/dist/services/operate/interaction/charter-suggestions.js +104 -0
  36. package/dist/services/operate/interaction/charter-suggestions.js.map +1 -0
  37. package/dist/services/operate/interaction/confirmation-service.d.ts +30 -0
  38. package/dist/services/operate/interaction/confirmation-service.d.ts.map +1 -0
  39. package/dist/services/operate/interaction/confirmation-service.js +78 -0
  40. package/dist/services/operate/interaction/confirmation-service.js.map +1 -0
  41. package/dist/services/operate/interaction/question-engine.d.ts +39 -0
  42. package/dist/services/operate/interaction/question-engine.d.ts.map +1 -0
  43. package/dist/services/operate/interaction/question-engine.js +277 -0
  44. package/dist/services/operate/interaction/question-engine.js.map +1 -0
  45. package/dist/services/operate/interaction/question-registry.d.ts +16 -0
  46. package/dist/services/operate/interaction/question-registry.d.ts.map +1 -0
  47. package/dist/services/operate/interaction/question-registry.js +190 -0
  48. package/dist/services/operate/interaction/question-registry.js.map +1 -0
  49. package/dist/services/operate/interaction/session-service.d.ts +55 -0
  50. package/dist/services/operate/interaction/session-service.d.ts.map +1 -0
  51. package/dist/services/operate/interaction/session-service.js +272 -0
  52. package/dist/services/operate/interaction/session-service.js.map +1 -0
  53. package/dist/services/operate/interaction/terminal-renderer.d.ts +8 -0
  54. package/dist/services/operate/interaction/terminal-renderer.d.ts.map +1 -0
  55. package/dist/services/operate/interaction/terminal-renderer.js +103 -0
  56. package/dist/services/operate/interaction/terminal-renderer.js.map +1 -0
  57. package/dist/services/operate/maintenance.d.ts.map +1 -1
  58. package/dist/services/operate/maintenance.js +40 -3
  59. package/dist/services/operate/maintenance.js.map +1 -1
  60. package/dist/services/operate/types.d.ts +194 -1
  61. package/dist/services/operate/types.d.ts.map +1 -1
  62. package/dist/services/operate/types.js.map +1 -1
  63. package/dist/services/operate/workspace.d.ts +1 -0
  64. package/dist/services/operate/workspace.d.ts.map +1 -1
  65. package/dist/services/operate/workspace.js +2 -0
  66. package/dist/services/operate/workspace.js.map +1 -1
  67. package/dist/services/pipeline-package-service.d.ts +14 -0
  68. package/dist/services/pipeline-package-service.d.ts.map +1 -1
  69. package/dist/services/pipeline-package-service.js +45 -1
  70. package/dist/services/pipeline-package-service.js.map +1 -1
  71. package/dist/services/runtime-manager-service.d.ts +3 -0
  72. package/dist/services/runtime-manager-service.d.ts.map +1 -1
  73. package/dist/services/runtime-manager-service.js +26 -2
  74. package/dist/services/runtime-manager-service.js.map +1 -1
  75. package/package.json +3 -2
@@ -1,9 +1,17 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { applyOperatingInitialization, getOperatingProfile, listOperatingProfiles, normalizeCustomOperatingProfile, prepareOperatingInitialization, validateOperatingConfiguration, } from './config.js';
3
+ import { canonicalDigest, sha256Digest } from './canonical.js';
4
+ import { applyOperatingInitialization, getOperatingProfile, listOperatingProfiles, normalizeCustomOperatingProfile, normalizeOperatingInitializationAnswers, prepareOperatingInitialization, validateOperatingConfiguration, } from './config.js';
4
5
  import { runOperatingCycle } from './engine.js';
5
6
  import { OperatingEventStore } from './event-store.js';
7
+ import { classifyEvidenceDiagnostic } from './evidence-classifications.js';
8
+ import { listEvidenceDiagnostics, readEvidenceDiagnostic } from './evidence-diagnostics.js';
6
9
  import { parseStrictJson, readImportedEvidenceFile } from './evidence-import.js';
10
+ import { createOperatingAction } from './interaction/action-service.js';
11
+ import { persistableOperatingInitAnswers, resumeGuidedSession, submitGuidedAnswers, } from './interaction/answer-service.js';
12
+ import { assertOperatingConfirmation } from './interaction/confirmation-service.js';
13
+ import { createOperatingInitQuestionnaire, evaluateOperatingInitQuestions, operatingInitAnswersFromOptions, } from './interaction/question-engine.js';
14
+ import { cancelGuidedSession, createGuidedSession, createGuidedSessionId, currentGuidedSessionBindings, updateGuidedSession, } from './interaction/session-service.js';
7
15
  import { assertCommittedOperatingView } from './journal.js';
8
16
  import { applyOperatingMigration, inspectOperatingMigration, rollbackOperatingMigration, } from './legacy-import-service.js';
9
17
  import { answerOperatingGap, applyOrRollbackRoute, decideOperatingDecision, governOperatingFinding, readOperatingCollection, readOperatingReview, transitionOperatingCycle, verifyOperatingGap, } from './lifecycle.js';
@@ -122,11 +130,16 @@ const OPERATE_EXIT_CODES = {
122
130
  E_OPERATE_CHARTER_INCOMPLETE: 2,
123
131
  E_OPERATE_CONFIG_INVALID: 2,
124
132
  E_OPERATE_INPUT_TOO_LARGE: 2,
133
+ E_OPERATE_QUESTIONNAIRE_INVALID: 2,
125
134
  E_OPERATE_PATH_ESCAPE: 2,
126
135
  E_OPERATE_NOT_INITIALIZED: 3,
127
136
  E_OPERATE_PROJECT_REQUIRED: 3,
128
137
  E_PIPELINE_NOT_INSTALLED: 3,
138
+ E_PIPELINE_VERSION_INCOMPATIBLE: 3,
129
139
  E_OPERATE_AUTHORITY_REQUIRED: 4,
140
+ E_OPERATE_INPUT_REQUIRED: 4,
141
+ E_OPERATE_SESSION_CANCELLED: 4,
142
+ E_OPERATE_SESSION_EXPIRED: 4,
130
143
  E_OPERATE_ROUTE_CONFIRMATION_REQUIRED: 4,
131
144
  E_OPERATE_CHECKPOINT_INVALID: 5,
132
145
  E_OPERATE_CYCLE_ACTIVE: 5,
@@ -142,6 +155,8 @@ const OPERATE_EXIT_CODES = {
142
155
  E_OPERATE_STALE_LOCK_UNSAFE: 5,
143
156
  E_OPERATE_STATE_INVALID: 5,
144
157
  E_OPERATE_TRANSACTION_INVALID: 5,
158
+ E_OPERATE_SESSION_REPLAY_CONFLICT: 5,
159
+ E_OPERATE_SESSION_STALE: 5,
145
160
  E_OPERATE_ADVISOR_FAILED: 6,
146
161
  E_OPERATE_ADVISOR_ISOLATION: 6,
147
162
  E_OPERATE_CAP_EXCEEDED: 6,
@@ -151,41 +166,46 @@ const OPERATE_EXIT_CODES = {
151
166
  E_OPERATE_EVIDENCE_REJECTED: 6,
152
167
  E_OPERATE_PROVIDER_READ_ONLY: 6,
153
168
  E_OPERATE_SECRET_DETECTED: 6,
169
+ E_OPERATE_SESSION_INVALID: 2,
154
170
  };
155
171
  function operateExitCode(code) {
156
172
  return OPERATE_EXIT_CODES[code];
157
173
  }
158
174
  function failure(action, error) {
159
175
  const code = error instanceof OperateError ? error.code : 'E_OPERATE_INTERNAL';
160
- const nextActions = code === 'E_PIPELINE_NOT_INSTALLED'
161
- ? ['npm install -g openplanr@latest', 'planr setup --scope user', 'planr operate inspect']
162
- : code === 'E_OPERATE_NOT_INITIALIZED'
163
- ? ['planr operate init']
164
- : code === 'E_OPERATE_PROJECT_REQUIRED'
165
- ? ['cd /path/to/your/project', 'planr init', 'planr operate inspect']
166
- : code === 'E_OPERATE_CONFIG_INVALID' || code === 'E_OPERATE_CHARTER_INCOMPLETE'
167
- ? ['planr operate config validate']
168
- : code === 'E_OPERATE_ROUTE_CONFIRMATION_REQUIRED'
169
- ? ['planr operate routes apply <route-id> --preview --json']
170
- : code === 'E_OPERATE_MIGRATION_CONFLICT'
171
- ? ['planr operate migrate inspect', 'planr operate migrations list']
172
- : code === 'E_OPERATE_HEAD_DIVERGED' ||
173
- code === 'E_OPERATE_CHECKPOINT_INVALID' ||
174
- code === 'E_OPERATE_PROJECTION_DRIFT'
175
- ? ['planr operate integrity status', 'planr operate cycles recover <cycle-id>']
176
- : code === 'E_OPERATE_CYCLE_ACTIVE' || code === 'E_OPERATE_CYCLE_INPUT_CONFLICT'
177
- ? ['planr operate status', 'planr operate cycles resume <cycle-id>']
178
- : code === 'E_OPERATE_CYCLE_NOT_DISPOSED'
179
- ? [
180
- 'planr operate findings list',
181
- 'planr operate decisions list',
182
- 'planr operate cycles close <cycle-id>',
183
- ]
184
- : code.startsWith('E_OPERATE_EVIDENCE') ||
185
- code.startsWith('E_OPERATE_PROVIDER') ||
186
- code.startsWith('E_OPERATE_ADVISOR')
187
- ? ['planr operate sources list', 'planr operate run --preview --json']
188
- : ['planr operate diagnostics export'];
176
+ const explicitRecovery = error instanceof OperateError && typeof error.details?.recoveryCommand === 'string'
177
+ ? [error.details.recoveryCommand]
178
+ : null;
179
+ const nextActions = explicitRecovery ??
180
+ (code === 'E_PIPELINE_NOT_INSTALLED'
181
+ ? ['npm install -g openplanr@latest', 'planr setup --scope user', 'planr operate inspect']
182
+ : code === 'E_OPERATE_NOT_INITIALIZED'
183
+ ? ['planr operate init']
184
+ : code === 'E_OPERATE_PROJECT_REQUIRED'
185
+ ? ['cd /path/to/your/project', 'planr init', 'planr operate inspect']
186
+ : code === 'E_OPERATE_CONFIG_INVALID' || code === 'E_OPERATE_CHARTER_INCOMPLETE'
187
+ ? ['planr operate config validate']
188
+ : code === 'E_OPERATE_ROUTE_CONFIRMATION_REQUIRED'
189
+ ? ['planr operate routes apply <route-id> --preview --json']
190
+ : code === 'E_OPERATE_MIGRATION_CONFLICT'
191
+ ? ['planr operate migrate inspect', 'planr operate migrations list']
192
+ : code === 'E_OPERATE_HEAD_DIVERGED' ||
193
+ code === 'E_OPERATE_CHECKPOINT_INVALID' ||
194
+ code === 'E_OPERATE_PROJECTION_DRIFT'
195
+ ? ['planr operate integrity status', 'planr operate cycles recover <cycle-id>']
196
+ : code === 'E_OPERATE_CYCLE_ACTIVE' || code === 'E_OPERATE_CYCLE_INPUT_CONFLICT'
197
+ ? ['planr operate status', 'planr operate cycles resume <cycle-id>']
198
+ : code === 'E_OPERATE_CYCLE_NOT_DISPOSED'
199
+ ? [
200
+ 'planr operate findings list',
201
+ 'planr operate decisions list',
202
+ 'planr operate cycles close <cycle-id>',
203
+ ]
204
+ : code.startsWith('E_OPERATE_EVIDENCE') ||
205
+ code.startsWith('E_OPERATE_PROVIDER') ||
206
+ code.startsWith('E_OPERATE_ADVISOR')
207
+ ? ['planr operate sources list', 'planr operate run --preview --json']
208
+ : ['planr operate diagnostics export']);
189
209
  return {
190
210
  schemaVersion: '1.0.0',
191
211
  protocolVersion: '1.2.0',
@@ -205,6 +225,100 @@ function failure(action, error) {
205
225
  exitCode: operateExitCode(code),
206
226
  };
207
227
  }
228
+ function publicActionCommands(nextActions) {
229
+ return [
230
+ ...new Set(nextActions
231
+ .flatMap((value) => value.split(/\s+&&\s+/))
232
+ .map((value) => value
233
+ .trim()
234
+ .replace(/\s+--yes(?=\s|$)/g, '')
235
+ .replace(/\s+--confirm\s+\S+/g, '')
236
+ .replace(/\s+--preview-digest\s+\S+/g, ''))
237
+ .filter((value) => /^planr\s+/.test(value))),
238
+ ];
239
+ }
240
+ function actionEffect(command) {
241
+ if (/\b(?:inspect|status|brief|review|list|show)\b/.test(command) ||
242
+ /\bconfig validate\b/.test(command) ||
243
+ /\bprofiles validate\b/.test(command) ||
244
+ /\bsources test\b/.test(command) ||
245
+ /\brun\b.*\b--preview\b/.test(command) ||
246
+ /\bmigrate inspect\b/.test(command) ||
247
+ /\bcache status\b/.test(command) ||
248
+ /\bintegrity status\b/.test(command)) {
249
+ return 'read-only';
250
+ }
251
+ if (/\boperate run\b/.test(command) && !/\b--offline\b/.test(command)) {
252
+ return 'provider-call';
253
+ }
254
+ if (/\b(?:adapter (?:prepare|record|resume|finalize|cancel)|cache purge|diagnostics export)\b/.test(command)) {
255
+ return 'machine-local-write';
256
+ }
257
+ if (/\bpipeline (?:plan|ship)\b/.test(command))
258
+ return 'external-effect';
259
+ return 'project-write';
260
+ }
261
+ async function attachStructuredActions(request, result) {
262
+ if (result.actions?.length || result.action === 'input_required')
263
+ return result;
264
+ const commands = publicActionCommands(result.nextActions);
265
+ if (commands.length === 0)
266
+ return result;
267
+ // Planning-only installations intentionally omit the portable pipeline and
268
+ // therefore its Protocol v1.2 action validators. Provider-free inspection
269
+ // and demonstration must still work; their legacy nextActions remain the
270
+ // compatible fallback until the full package is installed.
271
+ if (!resolveOperatingPipelineRoot())
272
+ return result;
273
+ const bindings = await currentGuidedSessionBindings(request.projectRoot);
274
+ const eventHead = await new OperatingEventStore(request.projectRoot)
275
+ .replay()
276
+ .then((value) => (value.eventHead.hash ? value.eventHead : null))
277
+ .catch(() => null);
278
+ const requestDigest = canonicalDigest({
279
+ action: request.action,
280
+ arguments: request.arguments ?? {},
281
+ options: Object.fromEntries(Object.entries(request.options)
282
+ .filter(([key]) => !['yes', 'confirm', 'stdin'].includes(key))
283
+ .map(([key, value]) => [key, canonicalDigest({ value })])),
284
+ });
285
+ const sessionId = `GIS-action-${requestDigest.slice('sha256:'.length, 'sha256:'.length + 24)}`;
286
+ const actions = [];
287
+ for (const [index, command] of commands.entries()) {
288
+ const effect = actionEffect(command);
289
+ const id = `operate.next.${canonicalDigest({ command }).slice('sha256:'.length, 'sha256:'.length + 20)}`.toLowerCase();
290
+ const created = await createOperatingAction({
291
+ id,
292
+ label: (() => {
293
+ const publicLabel = command.replace(/^planr\s+/, '');
294
+ return publicLabel.length <= 160
295
+ ? publicLabel
296
+ : `${publicLabel.slice(0, 157).trimEnd()}...`;
297
+ })(),
298
+ description: `Run the named ${effect} action returned by ${request.action}.`,
299
+ command,
300
+ effect,
301
+ recommended: index === 0,
302
+ ...(effect === 'read-only'
303
+ ? {}
304
+ : {
305
+ confirmation: {
306
+ sessionId,
307
+ confirmationScope: `${request.action}:${index + 1}`,
308
+ projectIdentity: bindings.projectIdentity,
309
+ projectHead: bindings.projectHead,
310
+ configHead: bindings.configHead,
311
+ eventHead,
312
+ arguments: [requestDigest],
313
+ destinations: [],
314
+ writes: [],
315
+ },
316
+ }),
317
+ });
318
+ actions.push(created.action);
319
+ }
320
+ return { ...result, actions };
321
+ }
208
322
  async function inspect(request) {
209
323
  const pipelineRoot = resolveOperatingPipelineRoot();
210
324
  const paths = resolveOperatingPaths(request.projectRoot);
@@ -324,75 +438,284 @@ function demo(request) {
324
438
  next: ['planr operate init'],
325
439
  });
326
440
  }
441
+ async function initializationApplyAction(input) {
442
+ const sessionId = input.sessionId ??
443
+ `GIS-init-${input.preview.previewDigest.slice('sha256:'.length, 'sha256:'.length + 24)}`;
444
+ const command = input.sessionId
445
+ ? `planr operate init --resume ${input.sessionId}`
446
+ : `planr operate init --preview-created-at ${input.preview.workspace.capturedAt}`;
447
+ return createOperatingAction({
448
+ id: 'operate.init.apply',
449
+ label: 'Apply Operating Board configuration',
450
+ description: 'Write only the reviewed charter, workspace, configuration, and machine-local preferences.',
451
+ command,
452
+ effect: 'project-write',
453
+ recommended: true,
454
+ confirmation: {
455
+ sessionId,
456
+ confirmationScope: 'operate.init.apply',
457
+ projectIdentity: input.bindings.projectIdentity,
458
+ projectHead: input.bindings.projectHead,
459
+ configHead: input.bindings.configHead,
460
+ eventHead: input.preview.expectedEventHead.hash ? input.preview.expectedEventHead : null,
461
+ arguments: [
462
+ `answers=${canonicalDigest(input.answers)}`,
463
+ `preview=${input.preview.previewDigest}`,
464
+ ],
465
+ destinations: [
466
+ ...input.preview.changedPaths,
467
+ ...(input.preview.preferencesChanged ? ['~/.planr/operate/preferences.json'] : []),
468
+ ],
469
+ writes: input.preview.writes.map((write) => `${write.relativePath}:${write.operation ?? 'replace'}:${sha256Digest(write.content)}`),
470
+ },
471
+ });
472
+ }
327
473
  async function initialize(request) {
328
474
  await loadOperatingProtocol();
329
- if (!request.interactive) {
330
- const missing = ['profile', 'decisionOwner', 'planningEngine'].filter((name) => !Object.hasOwn(request.options, name) ||
331
- String(request.options[name] ?? '').trim().length === 0);
332
- if (missing.length > 0) {
333
- throw new OperateError('E_OPERATE_CONFIG_INVALID', `Non-interactive initialization requires explicit ${missing
334
- .map((name) => `--${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`)
335
- .join(', ')}.`, { missing });
475
+ let supplied = normalizeOperatingInitializationAnswers(operatingInitAnswersFromOptions(request.options));
476
+ const resumeId = option(request, 'resume', undefined);
477
+ const localRoot = option(request, 'localRoot', undefined);
478
+ if (option(request, 'cancelSession', false)) {
479
+ if (!resumeId) {
480
+ throw new OperateError('E_OPERATE_SESSION_INVALID', '--cancel-session requires --resume <session-id>.');
481
+ }
482
+ return success(request.action, {
483
+ message: 'Guided initialization session cancelled.',
484
+ state: 'cancelled',
485
+ data: await cancelGuidedSession({
486
+ projectRoot: request.projectRoot,
487
+ sessionId: resumeId,
488
+ localRoot,
489
+ }),
490
+ next: ['planr operate init --json'],
491
+ });
492
+ }
493
+ if (request.stdin !== undefined && !resumeId) {
494
+ throw new OperateError('E_OPERATE_SESSION_INVALID', '--stdin requires --resume <session-id>.');
495
+ }
496
+ const bindings = await currentGuidedSessionBindings(request.projectRoot);
497
+ let resumedSession = null;
498
+ if (resumeId) {
499
+ resumedSession =
500
+ request.stdin === undefined
501
+ ? await resumeGuidedSession({
502
+ projectRoot: request.projectRoot,
503
+ sessionId: resumeId,
504
+ bindings,
505
+ localRoot,
506
+ })
507
+ : await submitGuidedAnswers({
508
+ projectRoot: request.projectRoot,
509
+ sessionId: resumeId,
510
+ raw: request.stdin,
511
+ bindings,
512
+ localRoot,
513
+ });
514
+ supplied = normalizeOperatingInitializationAnswers(resumedSession.answers);
515
+ if (resumedSession.status === 'input-required') {
516
+ return {
517
+ schemaVersion: '1.0.0',
518
+ protocolVersion: '1.2.0',
519
+ ok: false,
520
+ action: 'input_required',
521
+ code: 'E_OPERATE_INPUT_REQUIRED',
522
+ message: 'Operating Board initialization needs explicit human input.',
523
+ state: resumedSession.session.state,
524
+ paths: {},
525
+ counts: {},
526
+ warnings: [],
527
+ nextActions: [],
528
+ next: [],
529
+ questionnaire: resumedSession.questionnaire,
530
+ exitCode: operateExitCode('E_OPERATE_INPUT_REQUIRED'),
531
+ };
336
532
  }
337
533
  }
338
- const profile = option(request, 'profile', 'saas');
339
534
  let customProfile;
340
- const profileFile = option(request, 'profileFile', undefined);
341
- if (profile === 'custom') {
342
- if (!profileFile) {
343
- throw new OperateError('E_OPERATE_CONFIG_INVALID', 'The custom profile requires --profile-file inside the project.');
344
- }
345
- customProfile = await readCustomOperatingProfile(request.projectRoot, profileFile);
535
+ if (supplied.profile === 'custom' && supplied.profileFile) {
536
+ customProfile = await readCustomOperatingProfile(request.projectRoot, supplied.profileFile);
346
537
  }
347
- else if (profileFile) {
538
+ else if (supplied.profile !== undefined &&
539
+ supplied.profile !== 'custom' &&
540
+ supplied.profileFile) {
348
541
  throw new OperateError('E_OPERATE_CONFIG_INVALID', '--profile-file is valid only with --profile custom.');
349
542
  }
350
- const decisionOwner = option(request, 'decisionOwner', '').trim();
351
- const planningEngine = option(request, 'planningEngine', 'openplanr');
352
- const rawCharter = option(request, 'charter', {});
543
+ const context = {
544
+ projectRoot: request.projectRoot,
545
+ ...bindings,
546
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
547
+ availableSources: ['repository', 'planr', 'git', 'file-import'],
548
+ runtime: option(request, 'runtime', 'unknown'),
549
+ interaction: request.interactive ? 'terminal' : 'none',
550
+ };
551
+ const questionState = await evaluateOperatingInitQuestions({
552
+ answers: supplied,
553
+ context,
554
+ requireCharter: true,
555
+ });
556
+ if (questionState.status === 'input-required') {
557
+ const sessionId = createGuidedSessionId();
558
+ const questionnaire = await createOperatingInitQuestionnaire({
559
+ context,
560
+ questions: questionState.questions,
561
+ stage: questionState.stage,
562
+ sessionId,
563
+ });
564
+ await createGuidedSession({
565
+ projectRoot: request.projectRoot,
566
+ questionnaire,
567
+ persistedAnswers: persistableOperatingInitAnswers(questionState.answers, context),
568
+ localRoot,
569
+ });
570
+ return {
571
+ schemaVersion: '1.0.0',
572
+ protocolVersion: '1.2.0',
573
+ ok: false,
574
+ action: 'input_required',
575
+ code: 'E_OPERATE_INPUT_REQUIRED',
576
+ message: 'Operating Board initialization needs explicit human input.',
577
+ state: null,
578
+ paths: {},
579
+ counts: {},
580
+ warnings: [],
581
+ nextActions: [],
582
+ next: [],
583
+ questionnaire,
584
+ exitCode: operateExitCode('E_OPERATE_INPUT_REQUIRED'),
585
+ };
586
+ }
587
+ const profile = supplied.profile;
588
+ const decisionOwner = supplied.decisionOwner ?? option(request, 'decisionOwner', '').trim();
589
+ const planningEngine = supplied.planningEngine ??
590
+ option(request, 'planningEngine', 'openplanr');
591
+ const rawCharter = supplied.charter ?? option(request, 'charter', {});
353
592
  const preview = await prepareOperatingInitialization({
354
593
  projectRoot: request.projectRoot,
355
594
  profile,
356
595
  decisionOwner,
357
596
  planningEngine,
358
- runtime: option(request, 'runtime', 'auto'),
359
- cadence: option(request, 'cadence', 'manual'),
360
- timezone: option(request, 'timezone', Intl.DateTimeFormat().resolvedOptions().timeZone),
361
- sensitivityCeiling: option(request, 'sensitivityCeiling', 'internal'),
362
- enabledProviders: stringList(request.options.sources).length
363
- ? stringList(request.options.sources)
597
+ runtime: supplied.runtime ?? option(request, 'runtime', 'auto'),
598
+ cadence: supplied.cadence ?? option(request, 'cadence', 'manual'),
599
+ timezone: supplied.timezone ??
600
+ option(request, 'timezone', Intl.DateTimeFormat().resolvedOptions().timeZone),
601
+ sensitivityCeiling: supplied.sensitivityCeiling ?? option(request, 'sensitivityCeiling', 'internal'),
602
+ enabledProviders: (supplied.sources ?? stringList(request.options.sources)).length
603
+ ? (supplied.sources ?? stringList(request.options.sources))
364
604
  : undefined,
365
- evidenceFiles: stringList(request.options.evidenceFile),
605
+ evidenceFiles: supplied.evidenceFiles ?? stringList(request.options.evidenceFile),
366
606
  charter: rawCharter,
367
607
  customProfile,
368
- componentRoots: stringList(request.options.components),
608
+ componentRoots: supplied.componentRoots ?? stringList(request.options.components),
609
+ localRoot,
610
+ now: resumedSession?.session.createdAt ??
611
+ option(request, 'previewCreatedAt', undefined),
369
612
  });
370
613
  const legacyMigration = await inspectOperatingMigration({
371
614
  projectRoot: request.projectRoot,
372
615
  });
373
616
  const previewData = {
374
617
  previewDigest: preview.previewDigest,
618
+ expectedEventHead: preview.expectedEventHead,
619
+ resultingEventHead: preview.resultingEventHead,
375
620
  changedPaths: preview.changedPaths,
376
621
  localPreferencesChanged: preview.preferencesChanged,
622
+ previewCreatedAt: preview.workspace.capturedAt,
377
623
  workspaceDigest: preview.workspace.workspaceDigest,
378
624
  config: preview.config,
379
625
  legacyMigration,
626
+ ...(resumedSession ? { sessionId: resumedSession.session.sessionId } : {}),
380
627
  };
628
+ const { action: applyAction, confirmation } = await initializationApplyAction({
629
+ request,
630
+ preview,
631
+ bindings,
632
+ sessionId: resumedSession?.session.sessionId,
633
+ answers: questionState.answers,
634
+ });
635
+ const requestedConfirmation = option(request, 'confirm', undefined);
636
+ const confirmed = option(request, 'yes', false);
637
+ if (resumedSession) {
638
+ await updateGuidedSession({
639
+ projectRoot: request.projectRoot,
640
+ localRoot,
641
+ session: {
642
+ ...resumedSession.session,
643
+ state: 'preview-ready',
644
+ previewDigest: preview.previewDigest,
645
+ confirmationDigest: confirmation?.confirmationDigest,
646
+ updatedAt: new Date().toISOString(),
647
+ },
648
+ });
649
+ if (!confirmed || !requestedConfirmation) {
650
+ return success(request.action, {
651
+ message: 'Operating Board initialization preview is ready; no state was written.',
652
+ state: 'preview-ready',
653
+ preview: { ...previewData, confirmation },
654
+ actions: [applyAction],
655
+ next: [
656
+ `planr operate init --resume ${resumedSession.session.sessionId} --confirm ${confirmation?.confirmationDigest} --yes --json`,
657
+ ],
658
+ });
659
+ }
660
+ }
381
661
  if (option(request, 'preview', false) || option(request, 'dryRun', false)) {
382
662
  return success(request.action, {
383
663
  message: 'Operating Board initialization preview is ready; no state was written.',
384
- preview: previewData,
385
- next: ['planr operate init --yes'],
664
+ state: 'preview-ready',
665
+ preview: { ...previewData, confirmation },
666
+ actions: [applyAction],
667
+ next: [
668
+ `planr operate init --preview-created-at ${preview.workspace.capturedAt} --confirm ${confirmation?.confirmationDigest} --yes`,
669
+ ],
386
670
  });
387
671
  }
388
- if (!request.interactive && !option(request, 'yes', false)) {
389
- throw new OperateError('E_OPERATE_ROUTE_CONFIRMATION_REQUIRED', 'Non-interactive initialization requires --yes after reviewing --preview.', { preview: previewData });
672
+ if (!confirmed || !requestedConfirmation || !confirmation) {
673
+ throw new OperateError('E_OPERATE_ROUTE_CONFIRMATION_REQUIRED', 'Initialization requires the exact named confirmation returned by its preview.', {
674
+ preview: { ...previewData, confirmation },
675
+ action: applyAction,
676
+ });
677
+ }
678
+ const accepted = assertOperatingConfirmation({
679
+ expected: confirmation,
680
+ actionId: applyAction.id,
681
+ confirmationDigest: requestedConfirmation,
682
+ confirmed,
683
+ });
684
+ if (resumedSession) {
685
+ await updateGuidedSession({
686
+ projectRoot: request.projectRoot,
687
+ localRoot,
688
+ session: {
689
+ ...resumedSession.session,
690
+ state: 'confirmed',
691
+ previewDigest: preview.previewDigest,
692
+ confirmationDigest: accepted.confirmationDigest,
693
+ confirmedAt: accepted.confirmedAt,
694
+ updatedAt: accepted.confirmedAt ?? new Date().toISOString(),
695
+ },
696
+ });
390
697
  }
391
698
  const applied = await applyOperatingInitialization({
392
699
  projectRoot: request.projectRoot,
393
700
  preview,
394
701
  confirmationDigest: preview.previewDigest,
395
702
  });
703
+ if (resumedSession) {
704
+ const appliedAt = new Date().toISOString();
705
+ await updateGuidedSession({
706
+ projectRoot: request.projectRoot,
707
+ localRoot,
708
+ session: {
709
+ ...resumedSession.session,
710
+ state: 'applied',
711
+ previewDigest: preview.previewDigest,
712
+ confirmationDigest: accepted.confirmationDigest,
713
+ confirmedAt: accepted.confirmedAt,
714
+ appliedAt,
715
+ updatedAt: appliedAt,
716
+ },
717
+ });
718
+ }
396
719
  return success(request.action, {
397
720
  message: applied.initialized
398
721
  ? 'Operating Board initialized.'
@@ -539,6 +862,7 @@ async function status(request) {
539
862
  });
540
863
  }
541
864
  async function run(request) {
865
+ await validateOperatingConfiguration(request.projectRoot);
542
866
  const requestedRuntime = option(request, 'runtime', 'auto');
543
867
  const deferAdvisors = !option(request, 'offline', false) &&
544
868
  !option(request, 'dryRun', false) &&
@@ -635,6 +959,71 @@ async function collections(request) {
635
959
  }
636
960
  return success(request.action, { data });
637
961
  }
962
+ async function evidenceRecovery(request) {
963
+ const candidateId = argument(request, 'candidateId');
964
+ if (request.action === 'evidence.diagnose') {
965
+ const data = candidateId
966
+ ? await readEvidenceDiagnostic({
967
+ projectRoot: request.projectRoot,
968
+ candidateId,
969
+ localRoot: option(request, 'localRoot', undefined),
970
+ })
971
+ : await listEvidenceDiagnostics({
972
+ projectRoot: request.projectRoot,
973
+ localRoot: option(request, 'localRoot', undefined),
974
+ });
975
+ const diagnostic = Array.isArray(data) ? null : data;
976
+ return success(request.action, {
977
+ data: {
978
+ diagnostic: data,
979
+ valueDisclosed: false,
980
+ recovery: diagnostic
981
+ ? {
982
+ repairOrRemove: diagnostic.location
983
+ ? `Repair or remove the candidate at ${diagnostic.location}${diagnostic.line ? `:${diagnostic.line}` : ''}, then rerun.`
984
+ : 'Repair or remove the candidate in the identified component, then rerun.',
985
+ rotateCredential: 'If this is a real credential, rotate it before removing it from source history.',
986
+ eligibleExclusion: 'Exclude only the exact eligible source path through operating source policy; broad scanner bypasses are not supported.',
987
+ falsePositive: `planr operate evidence classify ${diagnostic.candidateId} --status false-positive --reason "<reason>" --json`,
988
+ rerun: 'planr operate run --offline',
989
+ }
990
+ : null,
991
+ },
992
+ actions: diagnostic?.actions,
993
+ next: diagnostic
994
+ ? [
995
+ `planr operate evidence classify ${diagnostic.candidateId} --status false-positive --reason "<reason>" --json`,
996
+ 'planr operate run --offline',
997
+ ]
998
+ : [],
999
+ });
1000
+ }
1001
+ if (!candidateId) {
1002
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Evidence candidate ID is required.');
1003
+ }
1004
+ const status = option(request, 'status', '');
1005
+ if (status !== 'false-positive' && status !== 'confirmed-secret') {
1006
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Evidence classification status must be false-positive or confirmed-secret.');
1007
+ }
1008
+ const config = await validateOperatingConfiguration(request.projectRoot);
1009
+ const data = await classifyEvidenceDiagnostic({
1010
+ projectRoot: request.projectRoot,
1011
+ candidateId,
1012
+ status,
1013
+ reason: option(request, 'reason', ''),
1014
+ classifiedBy: config.decisionOwner,
1015
+ confirmationDigest: option(request, 'confirm', undefined),
1016
+ confirmed: option(request, 'yes', false),
1017
+ localRoot: option(request, 'localRoot', undefined),
1018
+ });
1019
+ return success(request.action, {
1020
+ data,
1021
+ actions: data.state === 'preview' && data.action ? [data.action] : undefined,
1022
+ message: data.state === 'classified'
1023
+ ? `Evidence candidate ${candidateId} was classified without storing or exposing its value.`
1024
+ : 'Review and confirm the exact evidence classification action.',
1025
+ });
1026
+ }
638
1027
  async function cycleMutation(request) {
639
1028
  const cycleId = argument(request, 'cycleId');
640
1029
  if (!cycleId)
@@ -736,6 +1125,7 @@ async function maintenance(request) {
736
1125
  projectRoot: request.projectRoot,
737
1126
  action: request.action.endsWith('purge') ? 'purge' : 'status',
738
1127
  confirmed: option(request, 'yes', false),
1128
+ localRoot: option(request, 'localRoot', undefined),
739
1129
  }),
740
1130
  });
741
1131
  }
@@ -844,6 +1234,8 @@ const HANDLERS = {
844
1234
  'gaps.verify': answerMutation,
845
1235
  'evidence.list': collections,
846
1236
  'evidence.show': collections,
1237
+ 'evidence.diagnose': evidenceRecovery,
1238
+ 'evidence.classify': evidenceRecovery,
847
1239
  'migrate.inspect': maintenance,
848
1240
  'migrate.apply': maintenance,
849
1241
  'migrations.list': collections,
@@ -872,10 +1264,15 @@ export async function executeOperateAction(request) {
872
1264
  if (!handler) {
873
1265
  throw new OperateError('E_OPERATE_ACTION_UNKNOWN', `Unknown Operating Board action: ${request.action}.`);
874
1266
  }
875
- return await handler(request);
1267
+ return await attachStructuredActions(request, await handler(request));
876
1268
  }
877
1269
  catch (error) {
878
- return failure(request.action, error);
1270
+ try {
1271
+ return await attachStructuredActions(request, failure(request.action, error));
1272
+ }
1273
+ catch {
1274
+ return failure(request.action, error);
1275
+ }
879
1276
  }
880
1277
  }
881
1278
  //# sourceMappingURL=index.js.map