openplanr 1.14.4 → 1.15.1

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