projscan 4.1.0 → 4.3.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.
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
1
3
  import chalk from 'chalk';
2
4
  import { assertFormatSupported, getRootPath, maybeCompactBanner, program, setupLogLevel, } from '../_shared.js';
3
5
  import { computeStartReport } from '../../core/start.js';
@@ -8,26 +10,150 @@ export function registerStart() {
8
10
  .description('Orient an engineer or agent with the next best workflow for this repo')
9
11
  .option('--mode <mode>', 'before_edit, before_commit, before_merge, refactor, release, bug_hunt, or hardening')
10
12
  .option('--intent <text>', 'plain-language goal to route into the next best action')
13
+ .option('--mission <dir>', 'read an existing Mission Control bundle and include proof outcome')
11
14
  .option('--max-tasks <count>', 'maximum workplan tasks to inspect', parsePositiveInt)
12
15
  .option('--max-risks <count>', 'maximum start risks to return', parsePositiveInt)
13
16
  .option('--include-handoff', 'include a compact handoff payload')
17
+ .option('--handoff-prompt', 'print only the concise Mission Control handoff prompt')
18
+ .option('--next-command', 'print only the current Mission Control cursor command')
19
+ .option('--next-tool-call', 'print only the current Mission Control cursor MCP tool call as JSON')
20
+ .option('--ready-tool-calls', 'print all currently ready Mission Control MCP tool calls as compact JSON')
21
+ .option('--proof-commands', 'print only ready Mission Control proof commands')
22
+ .option('--checklist', 'print only the Mission Control resume checklist')
23
+ .option('--resume-json', 'print only the Mission Control resume object as compact JSON')
24
+ .option('--handoff-json', 'print only the Mission Control handoff object as compact JSON')
25
+ .option('--save-mission <dir>', 'write the Mission Control bundle to this directory')
26
+ .option('--runbook', 'print only the Mission Control Markdown runbook')
27
+ .option('--task-card', 'print only the Mission Control Markdown task card')
28
+ .option('--review-gate', 'print only the Mission Control review gate')
29
+ .option('--review-gate-json', 'print only the Mission Control review gate as JSON')
30
+ .option('--review-policy', 'print only the Mission Control review policy as JSON')
31
+ .option('--review-replies', 'print only the Mission Control reviewer reply choices')
32
+ .option('--mission-script', 'print the Mission Control shell script')
33
+ .option('--shortcuts', 'print the Mission Control shortcut command index')
34
+ .option('--shortcuts-json', 'print the Mission Control shortcut command index as JSON')
14
35
  .action(async (cmdOpts) => {
15
36
  setupLogLevel();
16
37
  maybeCompactBanner();
17
38
  const format = assertFormatSupported('start');
18
39
  const mode = parseMode(cmdOpts.mode);
40
+ const rootPath = getRootPath();
19
41
  try {
20
- const report = await computeStartReport(getRootPath(), {
42
+ const report = await computeStartReport(rootPath, {
21
43
  mode,
22
44
  intent: typeof cmdOpts.intent === 'string' ? cmdOpts.intent : undefined,
45
+ missionDir: typeof cmdOpts.mission === 'string' ? cmdOpts.mission : undefined,
23
46
  maxTasks: cmdOpts.maxTasks,
24
47
  maxRisks: cmdOpts.maxRisks,
25
48
  includeHandoff: cmdOpts.includeHandoff === true,
26
49
  });
50
+ if (typeof cmdOpts.saveMission === 'string' && cmdOpts.saveMission.length > 0) {
51
+ const missionBundle = await writeMissionBundle(rootPath, cmdOpts.saveMission, report);
52
+ if (format === 'json') {
53
+ console.log(JSON.stringify({ missionBundle }, null, 2));
54
+ return;
55
+ }
56
+ printMissionBundle(missionBundle);
57
+ return;
58
+ }
27
59
  if (format === 'json') {
28
60
  console.log(JSON.stringify(report, null, 2));
29
61
  return;
30
62
  }
63
+ if (cmdOpts.nextCommand === true) {
64
+ const command = report.missionControl.executionPlan.cursor.command;
65
+ if (!command) {
66
+ console.error(chalk.red('No runnable Mission Control cursor command is available.'));
67
+ process.exit(1);
68
+ }
69
+ console.log(command);
70
+ return;
71
+ }
72
+ if (cmdOpts.nextToolCall === true) {
73
+ const toolCall = nextToolCall(report);
74
+ if (!toolCall) {
75
+ console.error(chalk.red('No MCP-callable Mission Control cursor tool call is available.'));
76
+ process.exit(1);
77
+ }
78
+ console.log(JSON.stringify(toolCall));
79
+ return;
80
+ }
81
+ if (cmdOpts.readyToolCalls === true) {
82
+ const toolCalls = readyToolCalls(report);
83
+ if (toolCalls.length === 0) {
84
+ console.error(chalk.red('No ready Mission Control MCP tool calls are available.'));
85
+ process.exit(1);
86
+ }
87
+ console.log(JSON.stringify(toolCalls));
88
+ return;
89
+ }
90
+ if (cmdOpts.proofCommands === true) {
91
+ const commands = readyProofCommands(report);
92
+ if (commands.length === 0) {
93
+ console.error(chalk.red('No ready Mission Control proof commands are available.'));
94
+ process.exit(1);
95
+ }
96
+ console.log(commands.join('\n'));
97
+ return;
98
+ }
99
+ if (cmdOpts.checklist === true) {
100
+ printChecklistOnly(report);
101
+ return;
102
+ }
103
+ if (cmdOpts.resumeJson === true) {
104
+ console.log(JSON.stringify(report.missionControl.resume));
105
+ return;
106
+ }
107
+ if (cmdOpts.handoffJson === true) {
108
+ console.log(JSON.stringify(report.missionControl.handoff));
109
+ return;
110
+ }
111
+ if (cmdOpts.runbook === true) {
112
+ printRunbookOnly(report);
113
+ return;
114
+ }
115
+ if (cmdOpts.taskCard === true) {
116
+ printTaskCardOnly(report);
117
+ return;
118
+ }
119
+ if (cmdOpts.reviewGate === true) {
120
+ printReviewGateOnly(report);
121
+ return;
122
+ }
123
+ if (cmdOpts.reviewGateJson === true) {
124
+ printReviewGateJsonOnly(report);
125
+ return;
126
+ }
127
+ if (cmdOpts.reviewPolicy === true) {
128
+ printReviewPolicyOnly(report);
129
+ return;
130
+ }
131
+ if (cmdOpts.reviewReplies === true) {
132
+ printReviewRepliesOnly(report);
133
+ return;
134
+ }
135
+ if (cmdOpts.missionScript === true) {
136
+ printMissionScriptOnly(report);
137
+ return;
138
+ }
139
+ if (cmdOpts.shortcuts === true) {
140
+ printShortcutsOnly(report, {
141
+ intent: typeof cmdOpts.intent === 'string' ? cmdOpts.intent : undefined,
142
+ mode,
143
+ });
144
+ return;
145
+ }
146
+ if (cmdOpts.shortcutsJson === true) {
147
+ printShortcutsJsonOnly(report, {
148
+ intent: typeof cmdOpts.intent === 'string' ? cmdOpts.intent : undefined,
149
+ mode,
150
+ });
151
+ return;
152
+ }
153
+ if (cmdOpts.handoffPrompt === true) {
154
+ console.log(report.missionControl.handoffPrompt);
155
+ return;
156
+ }
31
157
  printStart(report);
32
158
  }
33
159
  catch (err) {
@@ -61,6 +187,10 @@ function printStart(report) {
61
187
  console.log(`Workflow: ${report.recommendedWorkflow.name}`);
62
188
  console.log('');
63
189
  printMissionControl(report);
190
+ if (report.handoff) {
191
+ console.log('');
192
+ printAgentRunbook(report);
193
+ }
64
194
  console.log('');
65
195
  console.log(chalk.bold('First 10 Minutes'));
66
196
  for (const step of report.firstTenMinutes.commands) {
@@ -96,6 +226,10 @@ function printStart(report) {
96
226
  }
97
227
  }
98
228
  }
229
+ function printAgentRunbook(report) {
230
+ console.log(chalk.bold('Agent Runbook'));
231
+ console.log(report.missionControl.runbook.markdown.trimEnd());
232
+ }
99
233
  function modeSourceLabel(source) {
100
234
  if (source === 'intent')
101
235
  return 'inferred from intent';
@@ -116,6 +250,11 @@ function printMissionControl(report) {
116
250
  if (mission.primaryAction.command)
117
251
  console.log(chalk.cyan(mission.primaryAction.command));
118
252
  console.log(chalk.dim(mission.whyNow));
253
+ printExecutionPlan(report);
254
+ printResumeChecklist(report);
255
+ printHandoffPrompt(report);
256
+ printMissionOutcome(report);
257
+ printReviewGate(report);
119
258
  if (mission.actionPlan.length > 0) {
120
259
  console.log(chalk.bold('Action Plan'));
121
260
  for (const action of mission.actionPlan.slice(0, 4)) {
@@ -151,10 +290,909 @@ function printMissionControl(report) {
151
290
  if (mission.proofCommands.length > 0) {
152
291
  console.log(chalk.bold('Ready Proof'));
153
292
  console.log(chalk.dim(mission.proofSummary));
154
- for (const command of mission.proofCommands.slice(0, 3)) {
293
+ const proofCommands = readyProofCommands(report);
294
+ for (const command of proofCommands.slice(0, 3)) {
155
295
  console.log(chalk.dim(`- ${command}`));
156
296
  }
297
+ const proofItems = mission.handoff.readyProof.items ?? [];
298
+ if (proofItems.length > 0) {
299
+ console.log(chalk.bold('Proof Queue'));
300
+ for (const item of proofItems) {
301
+ console.log(chalk.dim(`- ${formatConsoleProofItem(item)}`));
302
+ }
303
+ }
304
+ }
305
+ }
306
+ function printMissionOutcome(report) {
307
+ const outcome = report.missionControl.outcome;
308
+ if (!outcome)
309
+ return;
310
+ console.log(chalk.bold('Mission Outcome'));
311
+ console.log(`Status: ${outcome.status}`);
312
+ if (!outcome.available && outcome.reason)
313
+ console.log(`Reason: ${outcome.reason}`);
314
+ for (const item of outcome.whatChanged)
315
+ console.log(`- Changed: ${item}`);
316
+ for (const item of outcome.whatRemains)
317
+ console.log(`- Remains: ${item}`);
318
+ console.log(`Version candidate: ${outcome.versionCandidate.recommendation}`);
319
+ console.log(chalk.dim(outcome.versionCandidate.summary));
320
+ }
321
+ function nextToolCall(report) {
322
+ const resumeToolCall = report.missionControl.resume.toolCall;
323
+ if (resumeToolCall)
324
+ return resumeToolCall;
325
+ const cursor = report.missionControl.executionPlan.cursor;
326
+ if (!cursor.tool)
327
+ return undefined;
328
+ return {
329
+ tool: cursor.tool,
330
+ ...(typeof cursor.args !== 'undefined' ? { args: cursor.args } : {}),
331
+ };
332
+ }
333
+ function readyToolCalls(report) {
334
+ const calls = [];
335
+ const current = nextToolCall(report);
336
+ if (current)
337
+ calls.push(compactToolCall(current));
338
+ for (const proofCall of report.missionControl.handoff.readyProof.toolCalls ?? []) {
339
+ calls.push(compactToolCall(proofCall));
340
+ }
341
+ return dedupeToolCalls(calls);
342
+ }
343
+ function compactToolCall(toolCall) {
344
+ return {
345
+ tool: toolCall.tool,
346
+ ...(typeof toolCall.args !== 'undefined' ? { args: toolCall.args } : {}),
347
+ };
348
+ }
349
+ function dedupeToolCalls(toolCalls) {
350
+ const seen = new Set();
351
+ const out = [];
352
+ for (const toolCall of toolCalls) {
353
+ const key = JSON.stringify(toolCall);
354
+ if (seen.has(key))
355
+ continue;
356
+ seen.add(key);
357
+ out.push(toolCall);
358
+ }
359
+ return out;
360
+ }
361
+ function readyProofCommands(report) {
362
+ const mission = report.missionControl;
363
+ return mission.handoff.readyProof.commands.length > 0
364
+ ? mission.handoff.readyProof.commands
365
+ : mission.proofCommands;
366
+ }
367
+ async function writeMissionBundle(rootPath, bundleDir, report) {
368
+ const targetDir = path.resolve(rootPath, bundleDir);
369
+ const files = missionBundleFiles(targetDir);
370
+ const shortcutOptions = missionShortcutOptions(report);
371
+ const manifest = {
372
+ schemaVersion: 1,
373
+ kind: 'projscan.mission-bundle',
374
+ directory: targetDir,
375
+ ...(report.missionControl.intent ? { intent: report.missionControl.intent } : {}),
376
+ mode: report.mode,
377
+ status: report.missionControl.status,
378
+ currentStep: missionBundleCurrentStep(report),
379
+ quickCommands: missionBundleQuickCommands(),
380
+ files,
381
+ };
382
+ await fs.mkdir(targetDir, { recursive: true });
383
+ await fs.writeFile(path.join(targetDir, 'README.md'), missionBundleReadme(report, files), 'utf-8');
384
+ await fs.writeFile(path.join(targetDir, 'next-command.txt'), missionBundleNextCommand(report), 'utf-8');
385
+ await fs.writeFile(path.join(targetDir, 'next-tool-call.json'), JSON.stringify(nextToolCall(report) ?? null) + '\n', 'utf-8');
386
+ await fs.writeFile(path.join(targetDir, 'handoff-prompt.txt'), report.missionControl.handoffPrompt.trimEnd() + '\n', 'utf-8');
387
+ await fs.writeFile(path.join(targetDir, 'resume-prompt.txt'), report.missionControl.resume.prompt.trimEnd() + '\n', 'utf-8');
388
+ await fs.writeFile(path.join(targetDir, 'task-card.md'), report.missionControl.taskCard.markdown, 'utf-8');
389
+ await fs.writeFile(path.join(targetDir, 'review-gate.md'), report.missionControl.reviewGate.markdown, 'utf-8');
390
+ await fs.writeFile(path.join(targetDir, 'review-gate.json'), JSON.stringify(report.missionControl.reviewGate, null, 2) + '\n', 'utf-8');
391
+ await fs.writeFile(path.join(targetDir, 'review-policy.json'), JSON.stringify(report.missionControl.reviewGate.policy, null, 2) + '\n', 'utf-8');
392
+ await fs.writeFile(path.join(targetDir, 'review-replies.txt'), missionReviewReplyLines(report).join('\n') + '\n', 'utf-8');
393
+ await fs.writeFile(path.join(targetDir, 'runbook.md'), report.missionControl.runbook.markdown.trimEnd() + '\n', 'utf-8');
394
+ await fs.writeFile(path.join(targetDir, 'handoff.json'), JSON.stringify(report.missionControl.handoff, null, 2) + '\n', 'utf-8');
395
+ await fs.writeFile(path.join(targetDir, 'resume.json'), JSON.stringify(report.missionControl.resume, null, 2) + '\n', 'utf-8');
396
+ await fs.writeFile(path.join(targetDir, 'ready-tool-calls.json'), JSON.stringify(readyToolCalls(report), null, 2) + '\n', 'utf-8');
397
+ await fs.writeFile(path.join(targetDir, 'shortcuts.json'), JSON.stringify(buildShortcutIndex(report, shortcutOptions), null, 2) + '\n', 'utf-8');
398
+ await fs.mkdir(path.join(targetDir, 'proof-logs'), { recursive: true });
399
+ await fs.writeFile(path.join(targetDir, 'proof-logs', 'README.md'), missionProofLogsReadme(report), 'utf-8');
400
+ await fs.writeFile(path.join(targetDir, 'proof-logs', 'status.jsonl'), '', 'utf-8');
401
+ await fs.writeFile(path.join(targetDir, 'proof-logs', 'run-report.md'), missionInitialRunReport(), 'utf-8');
402
+ await fs.writeFile(path.join(targetDir, 'proof-logs', 'summary.json'), JSON.stringify(missionInitialRunSummary(), null, 2) + '\n', 'utf-8');
403
+ const missionScriptPath = path.join(targetDir, 'mission.sh');
404
+ await fs.writeFile(missionScriptPath, buildMissionScript(report, { proofLogs: true }), 'utf-8');
405
+ await fs.chmod(missionScriptPath, 0o755).catch(() => undefined);
406
+ const statusScriptPath = path.join(targetDir, 'status.sh');
407
+ await fs.writeFile(statusScriptPath, buildMissionStatusScript(), 'utf-8');
408
+ await fs.chmod(statusScriptPath, 0o755).catch(() => undefined);
409
+ const reviewScriptPath = path.join(targetDir, 'review.sh');
410
+ await fs.writeFile(reviewScriptPath, buildMissionReviewScript(report), 'utf-8');
411
+ await fs.chmod(reviewScriptPath, 0o755).catch(() => undefined);
412
+ await fs.writeFile(path.join(targetDir, 'proof-commands.txt'), readyProofCommands(report).join('\n') + '\n', 'utf-8');
413
+ await fs.writeFile(path.join(targetDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
414
+ return manifest;
415
+ }
416
+ function missionBundleQuickCommands() {
417
+ return [
418
+ {
419
+ id: 'run',
420
+ command: './mission.sh',
421
+ description: 'Run the current command and remaining proof.',
422
+ },
423
+ {
424
+ id: 'status',
425
+ command: './status.sh',
426
+ description: 'Print the latest mission state and next action.',
427
+ },
428
+ {
429
+ id: 'review',
430
+ command: './review.sh',
431
+ description: 'Print the review packet for approval.',
432
+ },
433
+ ];
434
+ }
435
+ function missionBundleFiles(targetDir) {
436
+ return [
437
+ {
438
+ name: 'README.md',
439
+ path: path.join(targetDir, 'README.md'),
440
+ description: 'Quickstart for humans opening the bundle.',
441
+ },
442
+ {
443
+ name: 'next-command.txt',
444
+ path: path.join(targetDir, 'next-command.txt'),
445
+ description: 'Current shell command or resume instruction.',
446
+ },
447
+ {
448
+ name: 'next-tool-call.json',
449
+ path: path.join(targetDir, 'next-tool-call.json'),
450
+ description: 'Current MCP tool call, or null when no mapped call exists.',
451
+ },
452
+ {
453
+ name: 'handoff-prompt.txt',
454
+ path: path.join(targetDir, 'handoff-prompt.txt'),
455
+ description: 'Copyable prompt for handing this mission to another agent.',
456
+ },
457
+ {
458
+ name: 'resume-prompt.txt',
459
+ path: path.join(targetDir, 'resume-prompt.txt'),
460
+ description: 'Focused prompt for resuming the current cursor.',
461
+ },
462
+ {
463
+ name: 'task-card.md',
464
+ path: path.join(targetDir, 'task-card.md'),
465
+ description: 'Paste-ready Markdown task card for PRs, issues, and handoffs.',
466
+ },
467
+ {
468
+ name: 'review-gate.md',
469
+ path: path.join(targetDir, 'review-gate.md'),
470
+ description: 'Stop-and-review gate for approving another slice, release, publish, or deploy.',
471
+ },
472
+ {
473
+ name: 'review-gate.json',
474
+ path: path.join(targetDir, 'review-gate.json'),
475
+ description: 'Machine-readable review gate with policy, proof, decisions, and worktree evidence.',
476
+ },
477
+ {
478
+ name: 'review-policy.json',
479
+ path: path.join(targetDir, 'review-policy.json'),
480
+ description: 'Machine-readable review approval boundary and blocked actions.',
481
+ },
482
+ {
483
+ name: 'review-replies.txt',
484
+ path: path.join(targetDir, 'review-replies.txt'),
485
+ description: 'Copy-only reviewer reply choices for approving or redirecting the stopped mission.',
486
+ },
487
+ {
488
+ name: 'runbook.md',
489
+ path: path.join(targetDir, 'runbook.md'),
490
+ description: 'Human-readable Mission Control runbook.',
491
+ },
492
+ {
493
+ name: 'handoff.json',
494
+ path: path.join(targetDir, 'handoff.json'),
495
+ description: 'Structured Mission Control handoff object.',
496
+ },
497
+ {
498
+ name: 'resume.json',
499
+ path: path.join(targetDir, 'resume.json'),
500
+ description: 'Focused resume object for the current cursor.',
501
+ },
502
+ {
503
+ name: 'ready-tool-calls.json',
504
+ path: path.join(targetDir, 'ready-tool-calls.json'),
505
+ description: 'Current cursor MCP call followed by remaining MCP-callable proof.',
506
+ },
507
+ {
508
+ name: 'shortcuts.json',
509
+ path: path.join(targetDir, 'shortcuts.json'),
510
+ description: 'Machine-readable Mission Control shortcut command index.',
511
+ },
512
+ {
513
+ name: 'mission.sh',
514
+ path: path.join(targetDir, 'mission.sh'),
515
+ description: 'Shell script that runs the current cursor command and remaining proof queue.',
516
+ },
517
+ {
518
+ name: 'status.sh',
519
+ path: path.join(targetDir, 'status.sh'),
520
+ description: 'Shell script that prints the latest mission run state from summary.json.',
521
+ },
522
+ {
523
+ name: 'review.sh',
524
+ path: path.join(targetDir, 'review.sh'),
525
+ description: 'Shell script that prints status, review evidence, run report, and reviewer replies.',
526
+ },
527
+ {
528
+ name: 'proof-logs/README.md',
529
+ path: path.join(targetDir, 'proof-logs', 'README.md'),
530
+ description: 'Proof-log index for output written by mission.sh.',
531
+ },
532
+ {
533
+ name: 'proof-logs/status.jsonl',
534
+ path: path.join(targetDir, 'proof-logs', 'status.jsonl'),
535
+ description: 'Runtime status rows written by mission.sh.',
536
+ },
537
+ {
538
+ name: 'proof-logs/run-report.md',
539
+ path: path.join(targetDir, 'proof-logs', 'run-report.md'),
540
+ description: 'Human-readable run report refreshed by mission.sh.',
541
+ },
542
+ {
543
+ name: 'proof-logs/summary.json',
544
+ path: path.join(targetDir, 'proof-logs', 'summary.json'),
545
+ description: 'Machine-readable mission run state refreshed by mission.sh.',
546
+ },
547
+ {
548
+ name: 'proof-commands.txt',
549
+ path: path.join(targetDir, 'proof-commands.txt'),
550
+ description: 'Remaining ready proof commands, one per line.',
551
+ },
552
+ {
553
+ name: 'manifest.json',
554
+ path: path.join(targetDir, 'manifest.json'),
555
+ description: 'Bundle index with mode, status, current step, and file paths.',
556
+ },
557
+ ];
558
+ }
559
+ function missionBundleReadme(report, files) {
560
+ const mission = report.missionControl;
561
+ const cursor = mission.executionPlan.cursor;
562
+ const lines = [
563
+ '# Mission Bundle',
564
+ '',
565
+ ...(mission.intent ? [`Intent: ${mission.intent}`] : []),
566
+ `Mode: ${report.mode}`,
567
+ `Status: ${mission.status}`,
568
+ `Current step: ${cursor.stepId} in ${cursor.phaseId}`,
569
+ '',
570
+ '## Quick Commands',
571
+ '',
572
+ '```sh',
573
+ './mission.sh',
574
+ './status.sh',
575
+ './review.sh',
576
+ '```',
577
+ '',
578
+ '- `./mission.sh` runs the current command and remaining proof.',
579
+ '- `./status.sh` prints the latest mission state and next action.',
580
+ '- `./review.sh` prints the review packet for approval.',
581
+ '',
582
+ '## Run Next',
583
+ '',
584
+ ];
585
+ if (cursor.command) {
586
+ lines.push('```sh', cursor.command, '```');
587
+ }
588
+ else {
589
+ lines.push(mission.resume.instruction);
590
+ }
591
+ const toolCall = nextToolCall(report);
592
+ if (toolCall) {
593
+ lines.push('', `MCP call: \`${toolCall.tool} ${JSON.stringify(toolCall.args ?? {})}\``);
594
+ }
595
+ const reviewReplyLines = missionReviewReplyLines(report);
596
+ if (reviewReplyLines.length > 0) {
597
+ lines.push('', '## Reviewer Replies', '', ...reviewReplyLines);
598
+ }
599
+ lines.push('', '## Files');
600
+ for (const file of files) {
601
+ lines.push(`- \`${file.name}\`: ${file.description}`);
602
+ }
603
+ return lines.join('\n').trimEnd() + '\n';
604
+ }
605
+ function missionBundleNextCommand(report) {
606
+ return `${report.missionControl.executionPlan.cursor.command ?? report.missionControl.resume.instruction}\n`;
607
+ }
608
+ function missionBundleCurrentStep(report) {
609
+ const cursor = report.missionControl.executionPlan.cursor;
610
+ return {
611
+ phaseId: cursor.phaseId,
612
+ stepId: cursor.stepId,
613
+ ...(cursor.command ? { command: cursor.command } : {}),
614
+ ...(cursor.tool ? { toolCall: { tool: cursor.tool, ...(typeof cursor.args !== 'undefined' ? { args: cursor.args } : {}) } } : {}),
615
+ };
616
+ }
617
+ function printMissionBundle(manifest) {
618
+ console.log(chalk.green(`Wrote Mission Control bundle to ${manifest.directory}`));
619
+ for (const file of manifest.files) {
620
+ console.log(`- ${file.name}`);
621
+ }
622
+ }
623
+ function printChecklistOnly(report) {
624
+ const checklist = report.missionControl.resume.checklist ?? [];
625
+ if (checklist.length === 0) {
626
+ console.error(chalk.red('No Mission Control resume checklist is available.'));
627
+ process.exit(1);
628
+ }
629
+ for (const item of checklist) {
630
+ console.log(`- ${formatConsoleChecklistItem(item)}`);
631
+ }
632
+ }
633
+ function printRunbookOnly(report) {
634
+ const runbook = report.missionControl.runbook.markdown.trimEnd();
635
+ if (runbook.length === 0) {
636
+ console.error(chalk.red('No Mission Control runbook is available.'));
637
+ process.exit(1);
638
+ }
639
+ console.log(runbook);
640
+ }
641
+ function printTaskCardOnly(report) {
642
+ const taskCard = report.missionControl.taskCard.markdown.trimEnd();
643
+ if (taskCard.length === 0) {
644
+ console.error(chalk.red('No Mission Control task card is available.'));
645
+ process.exit(1);
646
+ }
647
+ console.log(taskCard);
648
+ }
649
+ function printReviewGateOnly(report) {
650
+ const reviewGate = report.missionControl.reviewGate.markdown.trimEnd();
651
+ if (reviewGate.length === 0) {
652
+ console.error(chalk.red('No Mission Control review gate is available.'));
653
+ process.exit(1);
654
+ }
655
+ console.log(reviewGate);
656
+ }
657
+ function printReviewGateJsonOnly(report) {
658
+ console.log(JSON.stringify(report.missionControl.reviewGate));
659
+ }
660
+ function printReviewPolicyOnly(report) {
661
+ console.log(JSON.stringify(report.missionControl.reviewGate.policy));
662
+ }
663
+ function printReviewRepliesOnly(report) {
664
+ const replies = missionReviewReplyLines(report);
665
+ if (replies.length === 0) {
666
+ console.error(chalk.red('No Mission Control reviewer replies are available.'));
667
+ process.exit(1);
668
+ }
669
+ console.log(replies.join('\n'));
670
+ }
671
+ function printMissionScriptOnly(report) {
672
+ console.log(buildMissionScript(report).trimEnd());
673
+ }
674
+ function buildMissionStatusScript() {
675
+ return [
676
+ '#!/usr/bin/env sh',
677
+ 'set -eu',
678
+ '',
679
+ 'MISSION_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd)',
680
+ 'SUMMARY_FILE="${MISSION_DIR}/proof-logs/summary.json"',
681
+ '',
682
+ 'if ! command -v node >/dev/null 2>&1; then',
683
+ ` ${scriptPrintError('Node.js is required to read proof-logs/summary.json.')}`,
684
+ ' exit 2',
685
+ 'fi',
686
+ '',
687
+ 'node - "$SUMMARY_FILE" <<\'NODE\'',
688
+ 'const fs = require("node:fs");',
689
+ 'const summaryPath = process.argv[2];',
690
+ 'let summary;',
691
+ 'try {',
692
+ ' summary = JSON.parse(fs.readFileSync(summaryPath, "utf8"));',
693
+ '} catch (error) {',
694
+ ' console.error(`Unable to read ${summaryPath}: ${error.message}`);',
695
+ ' process.exit(2);',
696
+ '}',
697
+ 'const status = typeof summary.status === "string" ? summary.status : "unknown";',
698
+ 'console.log(`Mission status: ${status}`);',
699
+ 'if (summary.report) console.log(`Report: ${summary.report}`);',
700
+ 'if (summary.statusRows) console.log(`Status rows: ${summary.statusRows}`);',
701
+ 'if (summary.totalCommands !== undefined) console.log(`Total commands: ${summary.totalCommands}`);',
702
+ 'if (summary.failedStep) console.log(`Failed step: ${summary.failedStep}`);',
703
+ 'if (summary.exitCode !== undefined) console.log(`Exit code: ${summary.exitCode}`);',
704
+ 'if (summary.log) console.log(`Log: ${summary.log}`);',
705
+ `const nextActions = ${JSON.stringify(missionRunNextActions)};`,
706
+ 'const nextAction = typeof summary.nextAction === "string" ? summary.nextAction : nextActions[status] ?? "inspect proof-logs/summary.json.";',
707
+ 'console.log(`Next action: ${nextAction}`);',
708
+ 'process.exitCode = status === "passed" ? 0 : status === "failed" ? 1 : 2;',
709
+ 'NODE',
710
+ '',
711
+ ].join('\n');
712
+ }
713
+ function buildMissionReviewScript(report) {
714
+ const evidenceCommands = report.missionControl.reviewGate.commands;
715
+ return [
716
+ '#!/usr/bin/env sh',
717
+ 'set -eu',
718
+ '',
719
+ 'MISSION_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd)',
720
+ 'status_code=2',
721
+ '',
722
+ scriptPrint('Mission Review'),
723
+ scriptPrint(''),
724
+ 'if [ -x "${MISSION_DIR}/status.sh" ]; then',
725
+ ' set +e',
726
+ ' "${MISSION_DIR}/status.sh"',
727
+ ' status_code=$?',
728
+ ' set -e',
729
+ 'else',
730
+ ` ${scriptPrintError('Missing status.sh; run projscan start --save-mission again.')}`,
731
+ 'fi',
732
+ '',
733
+ scriptPrint(''),
734
+ scriptPrint('Review gate: review-gate.md'),
735
+ 'if [ -f "${MISSION_DIR}/review-gate.md" ]; then',
736
+ " sed -n '1,220p' \"${MISSION_DIR}/review-gate.md\"",
737
+ 'else',
738
+ ` ${scriptPrintError('Missing review-gate.md.')}`,
739
+ 'fi',
740
+ '',
741
+ scriptPrint(''),
742
+ scriptPrint('Run report: proof-logs/run-report.md'),
743
+ 'if [ -f "${MISSION_DIR}/proof-logs/run-report.md" ]; then',
744
+ " sed -n '1,220p' \"${MISSION_DIR}/proof-logs/run-report.md\"",
745
+ 'else',
746
+ ` ${scriptPrintError('Missing proof-logs/run-report.md. Run ./mission.sh to create proof output.')}`,
747
+ 'fi',
748
+ '',
749
+ scriptPrint(''),
750
+ scriptPrint('Evidence commands'),
751
+ ...evidenceCommands.map((command) => scriptPrint(`- ${command}`)),
752
+ '',
753
+ scriptPrint(''),
754
+ scriptPrint('Reviewer replies:'),
755
+ 'if [ -f "${MISSION_DIR}/review-replies.txt" ]; then',
756
+ ' cat "${MISSION_DIR}/review-replies.txt"',
757
+ 'else',
758
+ ` ${scriptPrintError('Missing review-replies.txt.')}`,
759
+ 'fi',
760
+ '',
761
+ 'exit "$status_code"',
762
+ '',
763
+ ].join('\n');
764
+ }
765
+ function buildMissionScript(report, options = {}) {
766
+ const mission = report.missionControl;
767
+ const cursor = mission.executionPlan.cursor;
768
+ const proofCommands = readyProofCommands(report);
769
+ const totalLoggedCommands = typeof cursor.command === 'string' ? 1 + proofCommands.length : proofCommands.length;
770
+ const unsafeCommand = [cursor.command, ...proofCommands]
771
+ .filter((command) => typeof command === 'string')
772
+ .find(commandHasShellExpansionSyntax);
773
+ const proofLogs = options.proofLogs === true && !unsafeCommand && typeof cursor.command === 'string';
774
+ const lines = [
775
+ '#!/usr/bin/env sh',
776
+ 'set -eu',
777
+ '',
778
+ scriptPrint('projscan Mission Control'),
779
+ ...(mission.intent && !unsafeCommand ? [scriptPrint(`Intent: ${mission.intent}`)] : []),
780
+ scriptPrint(`Mode: ${report.mode}`),
781
+ scriptPrint(`Status: ${mission.status}`),
782
+ scriptPrint(`Current step: ${cursor.stepId} in ${cursor.phaseId}`),
783
+ scriptPrint(''),
784
+ ];
785
+ if (unsafeCommand) {
786
+ lines.push(scriptPrintError('Blocked: mission command contains shell expansion syntax; inspect --next-command before running it.'), 'exit 2');
787
+ return lines.join('\n') + '\n';
788
+ }
789
+ if (proofLogs) {
790
+ lines.push('MISSION_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd)', 'PROOF_LOG_DIR="${MISSION_DIR}/proof-logs"', 'PROOF_STATUS_FILE="${PROOF_LOG_DIR}/status.jsonl"', 'PROOF_REPORT_FILE="${PROOF_LOG_DIR}/run-report.md"', 'PROOF_SUMMARY_FILE="${PROOF_LOG_DIR}/summary.json"', 'mkdir -p "$PROOF_LOG_DIR"', ': > "$PROOF_STATUS_FILE"', ': > "$PROOF_REPORT_FILE"', ...scriptInitRunReport(report), scriptWriteSummaryJson('running'), scriptPrintExpanded('Proof logs: ${PROOF_LOG_DIR}'), scriptPrintExpanded('Run report: ${PROOF_REPORT_FILE}'), scriptPrintExpanded('Summary: ${PROOF_SUMMARY_FILE}'), scriptPrint(''));
791
+ }
792
+ if (!cursor.command) {
793
+ lines.push(scriptPrintError(`Blocked: ${cursor.instruction ?? cursor.label}`), 'exit 2');
794
+ return lines.join('\n') + '\n';
795
+ }
796
+ lines.push(...scriptCommandBlock('Run current command', cursor.command, proofLogs ? { id: `current-${cursor.stepId}`, logName: `current-${cursor.stepId}.log` } : undefined));
797
+ if (proofCommands.length > 0) {
798
+ lines.push(scriptPrint(''), scriptPrint('Run remaining proof'));
799
+ for (const [index, command] of proofCommands.entries()) {
800
+ lines.push(...scriptCommandBlock(`Proof ${index + 1}`, command, proofLogs ? { id: `proof-${index + 1}`, logName: `proof-${index + 1}.log` } : undefined));
801
+ }
802
+ }
803
+ if (proofLogs) {
804
+ lines.push(...scriptAppendRunReportResult('passed'), scriptWriteSummaryJson('passed', { totalCommands: totalLoggedCommands }), ...scriptAppendRunReportReviewGate(mission.reviewGate.stopCondition, mission.reviewGate.commands));
805
+ }
806
+ lines.push(scriptPrint(''), scriptPrint('Review gate'), scriptPrint(mission.reviewGate.stopCondition), ...mission.reviewGate.commands.map((command) => scriptPrint(`Capture: ${command}`)));
807
+ return lines.join('\n') + '\n';
808
+ }
809
+ function missionProofLogsReadme(report) {
810
+ const entries = missionProofLogEntries(report);
811
+ const lines = [
812
+ '# Mission Proof Logs',
813
+ '',
814
+ 'Run `./mission.sh` from this bundle to write command output here.',
815
+ 'Read `run-report.md` first for pass/fail proof after `mission.sh` runs.',
816
+ 'Read `summary.json` for the latest not_run, running, passed, or failed state.',
817
+ 'Read `status.jsonl` for command exit codes after `mission.sh` runs.',
818
+ '',
819
+ '## Expected Logs',
820
+ '',
821
+ ];
822
+ if (entries.length === 0) {
823
+ lines.push('No runnable proof logs are planned for this mission.');
824
+ }
825
+ else {
826
+ for (const entry of entries) {
827
+ lines.push(`- \`${entry.name}\`: \`${entry.command}\``);
828
+ }
829
+ }
830
+ return lines.join('\n').trimEnd() + '\n';
831
+ }
832
+ function missionInitialRunReport() {
833
+ return [
834
+ '# Mission Run Report',
835
+ '',
836
+ 'Run `./mission.sh` to refresh this report with command exit codes and log links.',
837
+ 'Review `status.jsonl` for machine-readable status rows.',
838
+ '',
839
+ ].join('\n');
840
+ }
841
+ function missionInitialRunSummary() {
842
+ return {
843
+ schemaVersion: 1,
844
+ status: 'not_run',
845
+ nextAction: missionRunNextActions.not_run,
846
+ report: 'proof-logs/run-report.md',
847
+ statusRows: 'proof-logs/status.jsonl',
848
+ };
849
+ }
850
+ function missionProofLogEntries(report) {
851
+ const cursor = report.missionControl.executionPlan.cursor;
852
+ const proofCommands = readyProofCommands(report);
853
+ const unsafeCommand = [cursor.command, ...proofCommands]
854
+ .filter((command) => typeof command === 'string')
855
+ .find(commandHasShellExpansionSyntax);
856
+ if (unsafeCommand || !cursor.command)
857
+ return [];
858
+ return [
859
+ { name: `current-${cursor.stepId}.log`, command: cursor.command },
860
+ ...proofCommands.map((command, index) => ({ name: `proof-${index + 1}.log`, command })),
861
+ ];
862
+ }
863
+ function scriptCommandBlock(label, command, logTarget) {
864
+ if (!logTarget)
865
+ return [scriptPrint(label), command];
866
+ return [
867
+ scriptPrint(label),
868
+ scriptPrint(`Writing proof-logs/${logTarget.logName}`),
869
+ 'set +e',
870
+ '{',
871
+ ` ${command}`,
872
+ `} > "$PROOF_LOG_DIR/${logTarget.logName}" 2>&1`,
873
+ 'status=$?',
874
+ 'set -e',
875
+ scriptAppendStatusJsonl(logTarget.id, label, logTarget.logName, command),
876
+ scriptAppendReportRow(logTarget.id, label, logTarget.logName),
877
+ 'if [ "$status" -ne 0 ]; then',
878
+ ` ${scriptPrintError(`Command failed. See proof-logs/${logTarget.logName}.`)}`,
879
+ ...scriptAppendReportFailure(logTarget.id, logTarget.logName),
880
+ ` ${scriptWriteSummaryJson('failed', { failedStep: logTarget.id, logName: logTarget.logName })}`,
881
+ ` ${scriptPrintExpanded('Run report: ${PROOF_REPORT_FILE}')}`,
882
+ ` ${scriptPrintExpanded('Summary: ${PROOF_SUMMARY_FILE}')}`,
883
+ ' exit "$status"',
884
+ 'fi',
885
+ ];
886
+ }
887
+ function scriptPrint(value) {
888
+ return `printf '%s\\n' ${shellQuote(value)}`;
889
+ }
890
+ function scriptPrintExpanded(value) {
891
+ return `printf '%s\\n' "${value.replace(/(["\\])/g, '\\$1')}"`;
892
+ }
893
+ function scriptAppendStatusJsonl(id, label, logName, command) {
894
+ const prefix = JSON.stringify({
895
+ id,
896
+ label,
897
+ log: logName,
898
+ command,
899
+ }).replace(/}$/, ',"exitCode":');
900
+ return `printf '%s%s%s\\n' ${shellQuote(prefix)} "$status" '}' >> "$PROOF_STATUS_FILE"`;
901
+ }
902
+ function scriptInitRunReport(report) {
903
+ const mission = report.missionControl;
904
+ return [
905
+ '{',
906
+ ` ${scriptPrint('# Mission Run Report')}`,
907
+ ` ${scriptPrint('')}`,
908
+ ...(mission.intent ? [` ${scriptPrint(`Intent: ${mission.intent}`)}`] : []),
909
+ ` ${scriptPrint(`Mode: ${report.mode}`)}`,
910
+ ` ${scriptPrint(`Status: ${mission.status}`)}`,
911
+ ` ${scriptPrint(`Current step: ${mission.executionPlan.cursor.stepId} in ${mission.executionPlan.cursor.phaseId}`)}`,
912
+ ` ${scriptPrint('')}`,
913
+ ` ${scriptPrint('| Step | Label | Exit | Log |')}`,
914
+ ` ${scriptPrint('| --- | --- | ---: | --- |')}`,
915
+ '} >> "$PROOF_REPORT_FILE"',
916
+ ];
917
+ }
918
+ function scriptAppendReportRow(id, label, logName) {
919
+ return `printf '| %s | %s | %s | %s |\\n' ${shellQuote(id)} ${shellQuote(label)} "$status" ${shellQuote(`proof-logs/${logName}`)} >> "$PROOF_REPORT_FILE"`;
920
+ }
921
+ function scriptAppendReportFailure(id, logName) {
922
+ return [
923
+ ...scriptAppendRunReportResult('failed'),
924
+ ' {',
925
+ ` ${scriptPrint(`Failed step: ${id}`)}`,
926
+ ` ${scriptPrint(`Log: proof-logs/${logName}`)}`,
927
+ ' } >> "$PROOF_REPORT_FILE"',
928
+ ];
929
+ }
930
+ const missionRunNextActions = {
931
+ not_run: 'run ./mission.sh to generate proof.',
932
+ running: 'wait for ./mission.sh to finish, or inspect proof-logs/status.jsonl.',
933
+ failed: 'inspect the failed log, fix the issue, then rerun ./mission.sh.',
934
+ passed: 'run ./review.sh and choose a reviewer reply.',
935
+ };
936
+ function scriptWriteSummaryJson(status, options = {}) {
937
+ const base = {
938
+ schemaVersion: 1,
939
+ status,
940
+ nextAction: missionRunNextActions[status],
941
+ report: 'proof-logs/run-report.md',
942
+ statusRows: 'proof-logs/status.jsonl',
943
+ ...(typeof options.totalCommands === 'number' ? { totalCommands: options.totalCommands } : {}),
944
+ ...(options.failedStep ? { failedStep: options.failedStep } : {}),
945
+ ...(options.logName ? { log: `proof-logs/${options.logName}` } : {}),
946
+ };
947
+ if (status !== 'failed') {
948
+ return `printf '%s\\n' ${shellQuote(JSON.stringify(base))} > "$PROOF_SUMMARY_FILE"`;
949
+ }
950
+ const prefix = JSON.stringify(base).replace(/}$/, ',"exitCode":');
951
+ return `printf '%s%s%s\\n' ${shellQuote(prefix)} "$status" '}' > "$PROOF_SUMMARY_FILE"`;
952
+ }
953
+ function scriptAppendRunReportResult(status) {
954
+ const message = status === 'passed'
955
+ ? 'All current and proof commands exited 0.'
956
+ : 'Mission stopped before completion.';
957
+ return [
958
+ '{',
959
+ ` ${scriptPrint('')}`,
960
+ ` ${scriptPrint('## Result')}`,
961
+ ` ${scriptPrint(message)}`,
962
+ '} >> "$PROOF_REPORT_FILE"',
963
+ ];
964
+ }
965
+ function scriptAppendRunReportReviewGate(stopCondition, commands) {
966
+ return [
967
+ '{',
968
+ ` ${scriptPrint('')}`,
969
+ ` ${scriptPrint('## Review Gate')}`,
970
+ ` ${scriptPrint(stopCondition)}`,
971
+ ...commands.map((command) => ` ${scriptPrint(`- ${command}`)}`),
972
+ '} >> "$PROOF_REPORT_FILE"',
973
+ ];
974
+ }
975
+ function scriptPrintError(value) {
976
+ return `${scriptPrint(value)} >&2`;
977
+ }
978
+ function commandHasShellExpansionSyntax(command) {
979
+ let backslashCount = 0;
980
+ for (const char of command) {
981
+ if (char === '\\') {
982
+ backslashCount += 1;
983
+ continue;
984
+ }
985
+ const escaped = backslashCount % 2 === 1;
986
+ if ((char === '$' || char === '`') && !escaped)
987
+ return true;
988
+ backslashCount = 0;
989
+ }
990
+ return false;
991
+ }
992
+ function printShortcutsOnly(report, options) {
993
+ const shortcutIndex = buildShortcutIndex(report, options);
994
+ console.log(chalk.bold('Mission Shortcuts'));
995
+ if (shortcutIndex.currentCommand) {
996
+ console.log('Current command:');
997
+ console.log(shortcutIndex.currentCommand);
998
+ console.log('');
999
+ }
1000
+ if (shortcutIndex.currentToolCall) {
1001
+ console.log('Current MCP tool call:');
1002
+ console.log(JSON.stringify(shortcutIndex.currentToolCall));
1003
+ console.log('');
1004
+ }
1005
+ console.log('Copy from here:');
1006
+ for (const shortcut of shortcutIndex.shortcuts)
1007
+ console.log(shortcut.command);
1008
+ }
1009
+ function printShortcutsJsonOnly(report, options) {
1010
+ console.log(JSON.stringify(buildShortcutIndex(report, options)));
1011
+ }
1012
+ function buildShortcutIndex(report, options) {
1013
+ const command = report.missionControl.executionPlan.cursor.command;
1014
+ const toolCall = nextToolCall(report);
1015
+ const entries = [
1016
+ shortcutEntry('next-command', 'Current shell command', '--next-command', 'Print only the current Mission Control cursor command.', options),
1017
+ shortcutEntry('next-tool-call', 'Current MCP tool call', '--next-tool-call', 'Print only the current Mission Control cursor MCP tool call as compact JSON.', options),
1018
+ shortcutEntry('ready-tool-calls', 'Ready MCP calls', '--ready-tool-calls', 'Print the current cursor and remaining MCP-callable proof queue as compact JSON.', options),
1019
+ shortcutEntry('proof-commands', 'Ready proof commands', '--proof-commands', 'Print only ready Mission Control proof commands.', options),
1020
+ shortcutEntry('checklist', 'Resume checklist', '--checklist', 'Print only the Mission Control resume checklist.', options),
1021
+ shortcutEntry('resume-json', 'Resume JSON', '--resume-json', 'Print only the structured Mission Control resume object.', options),
1022
+ shortcutEntry('handoff-json', 'Handoff JSON', '--handoff-json', 'Print only the structured Mission Control handoff object.', options),
1023
+ shortcutEntry('mission-script', 'Mission script', '--mission-script', 'Print the Mission Control shell script.', options),
1024
+ shortcutEntry('save-mission', 'Save mission bundle', '--save-mission .projscan/mission', 'Write the Mission Control bundle to .projscan/mission.', options),
1025
+ shortcutEntry('task-card', 'Task card', '--task-card', 'Print only the Mission Control Markdown task card.', options),
1026
+ shortcutEntry('review-gate', 'Review gate Markdown', '--review-gate', 'Print only the Mission Control stop-and-review gate.', options),
1027
+ shortcutEntry('review-gate-json', 'Review gate JSON', '--review-gate-json', 'Print only the Mission Control review gate as JSON.', options),
1028
+ shortcutEntry('review-policy', 'Review policy JSON', '--review-policy', 'Print only the Mission Control review policy as JSON.', options),
1029
+ shortcutEntry('review-replies', 'Reviewer replies', '--review-replies', 'Print only copyable Mission Control reviewer replies.', options),
1030
+ shortcutEntry('runbook', 'Mission runbook', '--runbook', 'Print only the Mission Control Markdown runbook.', options),
1031
+ shortcutEntry('handoff-prompt', 'Handoff prompt', '--handoff-prompt', 'Print only the concise Mission Control handoff prompt.', options),
1032
+ {
1033
+ id: 'start',
1034
+ label: 'Full start report',
1035
+ command: startBaseCommand(options),
1036
+ description: 'Print the full Mission Control start report.',
1037
+ },
1038
+ ];
1039
+ return {
1040
+ schemaVersion: 1,
1041
+ kind: 'projscan.start-shortcuts',
1042
+ ...(command ? { currentCommand: command } : {}),
1043
+ ...(toolCall ? { currentToolCall: toolCall } : {}),
1044
+ baseCommand: startBaseCommand(options),
1045
+ shortcuts: entries,
1046
+ };
1047
+ }
1048
+ function shortcutEntry(id, label, flag, description, options) {
1049
+ return {
1050
+ id,
1051
+ label,
1052
+ command: shortcutCommand(flag, options),
1053
+ description,
1054
+ };
1055
+ }
1056
+ function missionShortcutOptions(report) {
1057
+ return {
1058
+ ...(report.modeSource === 'explicit' ? { mode: report.mode } : {}),
1059
+ ...(report.missionControl.intent ? { intent: report.missionControl.intent } : {}),
1060
+ };
1061
+ }
1062
+ function shortcutCommand(flag, options) {
1063
+ return ['projscan start', flag, ...startCommandOptionArgs(options)].join(' ');
1064
+ }
1065
+ function startBaseCommand(options) {
1066
+ return ['projscan start', ...startCommandOptionArgs(options)].join(' ');
1067
+ }
1068
+ function startCommandOptionArgs(options) {
1069
+ const args = [];
1070
+ if (options.mode)
1071
+ args.push('--mode', shellQuote(options.mode));
1072
+ if (options.intent)
1073
+ args.push('--intent', shellQuote(options.intent));
1074
+ return args;
1075
+ }
1076
+ function shellQuote(value) {
1077
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1078
+ }
1079
+ function printHandoffPrompt(report) {
1080
+ const prompt = report.missionControl.handoffPrompt;
1081
+ if (prompt.length === 0)
1082
+ return;
1083
+ console.log(chalk.bold('Handoff Prompt'));
1084
+ console.log(chalk.dim(prompt));
1085
+ }
1086
+ function printReviewGate(report) {
1087
+ const gate = report.missionControl.reviewGate;
1088
+ console.log(chalk.bold('Review Gate'));
1089
+ console.log(gate.stopCondition);
1090
+ for (const command of gate.commands)
1091
+ console.log(`- ${command}`);
1092
+ console.log(gate.worktree.summary);
1093
+ printReviewReplies(report);
1094
+ const stopLine = gate.checklist.find((item) => item.startsWith('Stop and ask'));
1095
+ if (stopLine)
1096
+ console.log(chalk.dim(stopLine));
1097
+ }
1098
+ function missionReviewReplyLines(report) {
1099
+ return report.missionControl.reviewGate.decisions.map((decision) => `- ${decision.label}: ${decision.reply}`);
1100
+ }
1101
+ function printReviewReplies(report) {
1102
+ const replies = missionReviewReplyLines(report);
1103
+ if (replies.length === 0)
1104
+ return;
1105
+ console.log(chalk.bold('Reviewer Replies'));
1106
+ for (const reply of replies)
1107
+ console.log(reply);
1108
+ }
1109
+ function printExecutionPlan(report) {
1110
+ const plan = report.missionControl.executionPlan;
1111
+ console.log(chalk.bold('Execution Plan'));
1112
+ console.log(chalk.dim(plan.summary));
1113
+ for (const phase of plan.phases.slice(0, 6)) {
1114
+ console.log(`- [${phase.status}] ${phase.title}`);
1115
+ for (const step of phase.steps.slice(0, 4)) {
1116
+ if (step.kind === 'input' && step.instruction) {
1117
+ console.log(` - ${step.label}: ${step.instruction}`);
1118
+ }
1119
+ else if (step.kind === 'criterion') {
1120
+ console.log(` - ${step.label}`);
1121
+ }
1122
+ else if (step.command && step.kind === 'proof') {
1123
+ console.log(` - ${step.command}`);
1124
+ }
1125
+ else if (step.command) {
1126
+ console.log(` - ${step.label}: ${step.command}`);
1127
+ }
1128
+ else {
1129
+ console.log(` - ${step.label}`);
1130
+ }
1131
+ if (step.blockedBy && step.blockedBy.length > 0) {
1132
+ console.log(` blocked by: ${step.blockedBy.join(', ')}`);
1133
+ }
1134
+ }
1135
+ }
1136
+ printExecutionCursor(report);
1137
+ }
1138
+ function printExecutionCursor(report) {
1139
+ const plan = report.missionControl.executionPlan;
1140
+ const cursor = plan.cursor;
1141
+ const phaseTitle = plan.phases.find((phase) => phase.id === cursor.phaseId)?.title ?? cursor.phaseId;
1142
+ console.log(chalk.bold('Run Cursor'));
1143
+ console.log(`next: ${cursor.stepId} in ${phaseTitle}`);
1144
+ if (cursor.command) {
1145
+ console.log(`command: ${cursor.command}`);
1146
+ }
1147
+ else if (cursor.instruction) {
1148
+ console.log(`input: ${cursor.instruction}`);
1149
+ }
1150
+ else {
1151
+ console.log(`step: ${cursor.label}`);
1152
+ }
1153
+ if (cursor.tool) {
1154
+ console.log(`MCP call: ${formatConsoleToolCall({ tool: cursor.tool, ...(typeof cursor.args !== 'undefined' ? { args: cursor.args } : {}) })}`);
157
1155
  }
1156
+ if (cursor.blockedBy && cursor.blockedBy.length > 0) {
1157
+ console.log(`blocked by: ${cursor.blockedBy.join(', ')}`);
1158
+ }
1159
+ if (cursor.unlocks && cursor.unlocks.length > 0) {
1160
+ console.log(`unlocks: ${cursor.unlocks.join(', ')}`);
1161
+ }
1162
+ console.log(chalk.dim(cursor.reason));
1163
+ }
1164
+ function printResumeChecklist(report) {
1165
+ const checklist = report.missionControl.resume.checklist ?? [];
1166
+ if (checklist.length === 0)
1167
+ return;
1168
+ console.log(chalk.bold('Resume Checklist'));
1169
+ for (const item of checklist) {
1170
+ console.log(chalk.dim(`- ${formatConsoleChecklistItem(item)}`));
1171
+ }
1172
+ }
1173
+ function formatConsoleChecklistItem(item) {
1174
+ const action = item.command
1175
+ ?? (item.placeholder && item.instruction ? `${item.placeholder} -> ${item.instruction}` : undefined)
1176
+ ?? item.instruction
1177
+ ?? item.label;
1178
+ return `[${item.status}] ${item.kind} ${item.stepId}: ${action}${formatConsoleChecklistAnnotation(item)}`;
1179
+ }
1180
+ function formatConsoleChecklistAnnotation(item) {
1181
+ if (item.tool) {
1182
+ return ` (MCP: ${formatConsoleToolCall({ tool: item.tool, ...(typeof item.args !== 'undefined' ? { args: item.args } : {}) })})`;
1183
+ }
1184
+ if (item.kind === 'run_proof' && item.command)
1185
+ return ' (CLI only)';
1186
+ return '';
1187
+ }
1188
+ function formatConsoleProofItem(item) {
1189
+ const proofAction = item.toolCall ? `MCP: ${formatConsoleToolCall(item.toolCall)}` : 'CLI only';
1190
+ return `${item.stepId}: ${item.command} (${proofAction})`;
1191
+ }
1192
+ function formatConsoleToolCall(toolCall) {
1193
+ return typeof toolCall.args !== 'undefined'
1194
+ ? `${toolCall.tool} ${JSON.stringify(toolCall.args)}`
1195
+ : toolCall.tool;
158
1196
  }
159
1197
  function routeEvidence(route) {
160
1198
  if (!route)