projscan 4.1.0 → 4.2.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.
@@ -45,6 +45,7 @@ export async function computeStartReport(rootPath, options = {}) {
45
45
  fixFirst,
46
46
  adoptionGaps,
47
47
  coordinationHints,
48
+ riskSources,
48
49
  });
49
50
  const nextActions = dedupeActions([
50
51
  missionControl.primaryAction,
@@ -267,12 +268,50 @@ function buildMissionControl(input) {
267
268
  const proofCommands = missionProofCommands(input.mode, input.workplan, guardrails, actionPlan);
268
269
  const successCriteria = missionSuccessCriteria(input.mode, routed, actionPlan, input.workplan);
269
270
  const unresolvedInputs = missionUnresolvedInputs(actionPlan);
271
+ const executionPlan = buildMissionExecutionPlan({
272
+ primaryAction,
273
+ actionPlan,
274
+ readyActions,
275
+ unresolvedInputs,
276
+ successCriteria,
277
+ proofCommands,
278
+ });
279
+ const resume = missionResume(executionPlan);
280
+ const reviewProof = buildMissionReviewProof(resume, proofCommands);
270
281
  const whyNow = routed
271
282
  ? routedWhyNow(routed, actionPlan)
272
283
  : input.fixFirst
273
284
  ? `Top evidence points to "${input.fixFirst.title}" as the first useful move.`
274
285
  : `The ${input.mode} workflow is the shortest path from orientation to verified action.`;
275
- const commandText = primaryAction.command ?? primaryAction.tool ?? 'projscan start --format json';
286
+ const reviewGate = buildMissionReviewGate({
287
+ status,
288
+ doneWhen: successCriteria,
289
+ proof: reviewProof,
290
+ currentWorktree: input.riskSources.currentWorktree,
291
+ });
292
+ const handoffPrompt = missionHandoffPrompt(resume, successCriteria, whyNow, unresolvedInputs, proofCommands, reviewGate);
293
+ const runbook = buildMissionRunbook({
294
+ intent: input.intent,
295
+ status,
296
+ primaryAction,
297
+ readyActions,
298
+ unresolvedInputs,
299
+ successCriteria,
300
+ proofCommands,
301
+ executionPlan,
302
+ resume,
303
+ handoffPrompt,
304
+ reviewGate,
305
+ });
306
+ const taskCard = buildMissionTaskCard({
307
+ intent: input.intent,
308
+ status,
309
+ currentStep: executionPlan.cursor,
310
+ resume,
311
+ successCriteria,
312
+ handoffPrompt,
313
+ reviewGate,
314
+ });
276
315
  return {
277
316
  ...(input.intent ? { intent: input.intent } : {}),
278
317
  status,
@@ -288,27 +327,1021 @@ function buildMissionControl(input) {
288
327
  successCriteria,
289
328
  proofSummary: READY_PROOF_SUMMARY,
290
329
  proofCommands,
291
- handoff: missionHandoff(primaryAction, readyActions, unresolvedInputs, successCriteria, proofCommands),
292
- handoffPrompt: missionHandoffPrompt(commandText, successCriteria, whyNow, unresolvedInputs, proofCommands),
330
+ resume,
331
+ handoff: missionHandoff(executionPlan.cursor, resume, primaryAction, readyActions, unresolvedInputs, successCriteria, proofCommands, reviewGate),
332
+ executionPlan,
333
+ runbook,
334
+ reviewGate,
335
+ taskCard,
336
+ handoffPrompt,
337
+ };
338
+ }
339
+ function buildMissionReviewGate(input) {
340
+ const checklist = [
341
+ 'Complete this task card and remaining proof.',
342
+ 'Capture `git status --short`.',
343
+ 'Capture `git diff --stat`.',
344
+ 'Stop and ask for approval before starting another slice, release, publish, or deploy.',
345
+ ];
346
+ const commands = ['git status --short', 'git diff --stat'];
347
+ const doneWhen = input.doneWhen.slice();
348
+ const policy = buildMissionReviewPolicy();
349
+ const decisions = buildMissionReviewDecisions();
350
+ const worktree = buildMissionReviewWorktree(input.currentWorktree);
351
+ const stopCondition = 'Stop after the current Mission Control checklist and proof are complete.';
352
+ const reviewPrompt = `Review the completed mission, proof output, and working-tree summary before approving another slice, release, publish, or deploy. ${input.proof.summary}`;
353
+ return {
354
+ title: 'Mission Review Gate',
355
+ required: true,
356
+ status: input.status,
357
+ stopCondition,
358
+ reviewPrompt,
359
+ checklist,
360
+ doneWhen,
361
+ policy,
362
+ decisions,
363
+ commands,
364
+ worktree,
365
+ proof: input.proof,
366
+ markdown: renderMissionReviewGateMarkdown({
367
+ status: input.status,
368
+ stopCondition,
369
+ reviewPrompt,
370
+ checklist,
371
+ doneWhen,
372
+ policy,
373
+ decisions,
374
+ commands,
375
+ worktree,
376
+ proof: input.proof,
377
+ }),
293
378
  };
294
379
  }
295
- function missionHandoff(nextAction, readyActions, needsInput, doneWhen, proofCommands) {
380
+ function buildMissionReviewPolicy() {
296
381
  return {
382
+ approvalRequired: true,
383
+ blockedActions: ['next_slice', 'release', 'publish', 'deploy', 'push', 'merge', 'version_bump'],
384
+ summary: 'Explicit reviewer approval is required before another slice, release, publish, deploy, push, merge, or version bump.',
385
+ };
386
+ }
387
+ function buildMissionReviewDecisions() {
388
+ return [
389
+ {
390
+ id: 'approve_next_slice',
391
+ label: 'Approve next slice',
392
+ description: 'The agent may start another bounded implementation slice.',
393
+ consequence: 'No release, publish, deploy, or version bump is allowed unless the reviewer asks for it.',
394
+ reply: 'Approved: start one more bounded implementation slice. Do not release, publish, deploy, push, merge, or bump the version.',
395
+ },
396
+ {
397
+ id: 'request_changes',
398
+ label: 'Request changes',
399
+ description: 'The agent must address review feedback before starting more scope.',
400
+ consequence: 'The current mission stays open until feedback and proof are updated.',
401
+ reply: 'Changes requested: address the review feedback first, update proof, then stop for another review.',
402
+ },
403
+ {
404
+ id: 'review_version_candidate',
405
+ label: 'Review version candidate',
406
+ description: 'The agent may prepare release notes, version rationale, and remaining gates for review.',
407
+ consequence: 'Publishing still requires a separate explicit approval.',
408
+ reply: 'Prepare a version-candidate review only. Do not publish, deploy, push, merge, or bump the version.',
409
+ },
410
+ ];
411
+ }
412
+ function buildMissionReviewProof(resume, proofCommands) {
413
+ const commands = resume.remainingProofCommands ?? proofCommands;
414
+ const toolCalls = resume.remainingProofToolCalls ?? [];
415
+ const items = resume.remainingProofItems ?? [];
416
+ return {
417
+ summary: READY_PROOF_SUMMARY,
418
+ commands,
419
+ ...(toolCalls.length > 0 ? { toolCalls } : {}),
420
+ ...(items.length > 0 ? { items } : {}),
421
+ };
422
+ }
423
+ function buildMissionReviewWorktree(currentWorktree) {
424
+ if (!currentWorktree.available) {
425
+ const reason = currentWorktree.reason ?? 'unknown';
426
+ return {
427
+ available: false,
428
+ clean: false,
429
+ changedFileCount: 0,
430
+ files: [],
431
+ baseRef: currentWorktree.baseRef,
432
+ summary: `Current worktree evidence is unavailable: ${reason}.`,
433
+ reason,
434
+ };
435
+ }
436
+ const changedFileCount = currentWorktree.count;
437
+ const baseRef = currentWorktree.baseRef;
438
+ return {
439
+ available: true,
440
+ clean: changedFileCount === 0,
441
+ changedFileCount,
442
+ files: currentWorktree.files,
443
+ baseRef,
444
+ summary: changedFileCount === 0
445
+ ? 'Current worktree evidence sees no changed files.'
446
+ : `Current worktree evidence sees ${changedFileCount} changed file(s)${baseRef ? ` against ${baseRef}` : ''}.`,
447
+ };
448
+ }
449
+ function renderMissionReviewGateMarkdown(input) {
450
+ const lines = [
451
+ '# Mission Review Gate',
452
+ '',
453
+ `Status: ${input.status}`,
454
+ `Stop condition: ${input.stopCondition}`,
455
+ '',
456
+ '## Checklist',
457
+ ...input.checklist.map((item) => `- [ ] ${item}`),
458
+ '',
459
+ '## Review Policy',
460
+ `Approval required: ${input.policy.approvalRequired ? 'yes' : 'no'}`,
461
+ input.policy.summary,
462
+ 'Blocked until approval:',
463
+ ...input.policy.blockedActions.map(formatMissionReviewBlockedAction),
464
+ '',
465
+ '## Done When',
466
+ ...(input.doneWhen.length > 0
467
+ ? input.doneWhen.map((criterion) => `- [ ] ${criterion}`)
468
+ : ['- [ ] The current mission is complete and verified.']),
469
+ '',
470
+ '## Reviewer Decision',
471
+ ...input.decisions.map(formatMissionReviewDecision),
472
+ '',
473
+ ...renderMissionReviewProofLines(input.proof),
474
+ '## Evidence Commands',
475
+ ...input.commands.map((command) => `- \`${command}\``),
476
+ '',
477
+ '## Worktree Evidence',
478
+ input.worktree.summary,
479
+ ...input.worktree.files.slice(0, 8).map((file) => `- \`${file}\``),
480
+ '',
481
+ '## Review Prompt',
482
+ input.reviewPrompt,
483
+ ];
484
+ return `${lines.join('\n').trimEnd()}\n`;
485
+ }
486
+ function formatMissionReviewDecision(decision) {
487
+ return `- [ ] ${decision.label}: ${decision.description} Consequence: ${decision.consequence} Reply: "${decision.reply}"`;
488
+ }
489
+ function formatMissionReviewBlockedAction(action) {
490
+ const labels = {
491
+ next_slice: 'Start another implementation slice',
492
+ release: 'Release',
493
+ publish: 'Publish',
494
+ deploy: 'Deploy',
495
+ push: 'Push',
496
+ merge: 'Merge',
497
+ version_bump: 'Version bump',
498
+ };
499
+ return `- ${labels[action]} (\`${action}\`)`;
500
+ }
501
+ function renderMissionReviewProofLines(proof) {
502
+ const lines = ['## Proof Queue', proof.summary];
503
+ if (proof.items && proof.items.length > 0) {
504
+ return [...lines, ...proof.items.map(formatMissionReviewProofItem), ''];
505
+ }
506
+ if (proof.commands.length > 0) {
507
+ return [...lines, ...proof.commands.map((command) => `- \`${command}\``), ''];
508
+ }
509
+ return [...lines, 'No proof commands are ready yet.', ''];
510
+ }
511
+ function formatMissionReviewProofItem(item) {
512
+ const annotation = item.toolCall
513
+ ? ` (MCP: ${formatMissionReviewToolCall(item.toolCall)})`
514
+ : ' (CLI only)';
515
+ return `- \`${item.command}\`${annotation}`;
516
+ }
517
+ function formatMissionReviewToolCall(toolCall) {
518
+ return typeof toolCall.args !== 'undefined'
519
+ ? `${toolCall.tool} ${JSON.stringify(toolCall.args)}`
520
+ : toolCall.tool;
521
+ }
522
+ function buildMissionRunbook(input) {
523
+ const readyCommands = uniqueStrings(input.readyActions
524
+ .map((action) => action.command ?? '')
525
+ .filter(isRunnableCommand));
526
+ const readyCommandBlock = readyCommands.join('\n');
527
+ const blockedInputSummary = input.unresolvedInputs.length > 0
528
+ ? `Needs input: ${input.unresolvedInputs.map((item) => `${item.name}=${item.placeholder}`).join(', ')}.`
529
+ : undefined;
530
+ return {
531
+ title: `Runbook: ${input.primaryAction.label}`,
532
+ status: input.status,
533
+ currentPhase: input.executionPlan.currentPhase,
534
+ currentStep: input.executionPlan.cursor,
535
+ resume: input.resume,
536
+ readyCommandBlock,
537
+ ...(blockedInputSummary ? { blockedInputSummary } : {}),
538
+ markdown: renderMissionRunbookMarkdown({
539
+ intent: input.intent,
540
+ status: input.status,
541
+ currentPhase: input.executionPlan.currentPhase,
542
+ currentStep: input.executionPlan.cursor,
543
+ resume: input.resume,
544
+ primaryAction: input.primaryAction,
545
+ readyCommands,
546
+ unresolvedInputs: input.unresolvedInputs,
547
+ proofCommands: input.proofCommands,
548
+ successCriteria: input.successCriteria,
549
+ handoffPrompt: input.handoffPrompt,
550
+ reviewGate: input.reviewGate,
551
+ }),
552
+ };
553
+ }
554
+ function renderMissionRunbookMarkdown(input) {
555
+ const lines = [
556
+ '# Mission Runbook',
557
+ '',
558
+ ...(input.intent ? [`Intent: ${input.intent}`] : []),
559
+ `Status: ${input.status}`,
560
+ `Current phase: ${input.currentPhase}`,
561
+ `Next action: ${input.primaryAction.command ? `\`${input.primaryAction.command}\`` : input.primaryAction.label}`,
562
+ '',
563
+ ...renderRunbookCursorLines(input.currentStep),
564
+ '',
565
+ ...renderRunbookResumeLines(input.resume),
566
+ '',
567
+ '## Handoff Prompt',
568
+ input.handoffPrompt,
569
+ '',
570
+ '## Review Gate',
571
+ ...input.reviewGate.checklist.map((item) => `- [ ] ${item}`),
572
+ '',
573
+ '## Reviewer Decision',
574
+ ...input.reviewGate.decisions.map(formatMissionReviewDecision),
575
+ '',
576
+ input.reviewGate.reviewPrompt,
577
+ '',
578
+ '## Ready Commands',
579
+ ...(input.readyCommands.length > 0 ? input.readyCommands.map((command) => `- \`${command}\``) : ['- None yet. Resolve blocked inputs first.']),
580
+ '',
581
+ ...(input.unresolvedInputs.length > 0
582
+ ? [
583
+ '## Blocked Inputs',
584
+ ...input.unresolvedInputs.map((item) => `- ${item.name}: ${item.instruction}`),
585
+ '',
586
+ ]
587
+ : []),
588
+ '## Proof Commands',
589
+ ...(input.proofCommands.length > 0 ? input.proofCommands.map((command) => `- \`${command}\``) : ['- No proof commands available yet.']),
590
+ '',
591
+ '## Done When',
592
+ ...(input.successCriteria.length > 0 ? input.successCriteria.map((criterion) => `- ${criterion}`) : ['- The next action is complete and verified.']),
593
+ ];
594
+ return `${lines.join('\n')}\n`;
595
+ }
596
+ function buildMissionTaskCard(input) {
597
+ return {
598
+ title: 'Mission Task Card',
599
+ status: input.status,
600
+ currentPhase: input.currentStep.phaseId,
601
+ currentStep: input.currentStep,
602
+ markdown: renderMissionTaskCardMarkdown(input),
603
+ };
604
+ }
605
+ function renderMissionTaskCardMarkdown(input) {
606
+ const lines = [
607
+ '# Mission Task Card',
608
+ '',
609
+ ...(input.intent ? [`Intent: ${input.intent}`] : []),
610
+ `Status: ${input.status}`,
611
+ `Current step: ${input.currentStep.stepId} in ${input.currentStep.phaseId}`,
612
+ '',
613
+ '## Do Next',
614
+ ...missionTaskCardActionLines(input.resume),
615
+ '',
616
+ '## Proof',
617
+ ...missionTaskCardProofLines(input.resume),
618
+ '',
619
+ '## Done When',
620
+ ...(input.successCriteria.length > 0
621
+ ? input.successCriteria.map((criterion) => `- [ ] ${criterion}`)
622
+ : ['- [ ] The next action is complete and verified.']),
623
+ '',
624
+ '## Review Gate',
625
+ ...input.reviewGate.checklist.map((item) => `- [ ] ${item}`),
626
+ '',
627
+ '## Reviewer Decision',
628
+ ...input.reviewGate.decisions.map(formatMissionReviewDecision),
629
+ '',
630
+ '## Handoff Prompt',
631
+ input.handoffPrompt,
632
+ ];
633
+ return `${lines.join('\n').trimEnd()}\n`;
634
+ }
635
+ function missionTaskCardActionLines(resume) {
636
+ const checklist = resume.checklist ?? [];
637
+ const actionLines = checklist
638
+ .filter((item) => item.kind !== 'run_proof' && item.kind !== 'confirm_done')
639
+ .map(formatTaskCardChecklistItem);
640
+ return actionLines.length > 0 ? actionLines : ['- [ ] Continue from the current Mission Control cursor.'];
641
+ }
642
+ function missionTaskCardProofLines(resume) {
643
+ const proofItems = resume.remainingProofItems ?? [];
644
+ const proofLines = proofItems.map(formatTaskCardProofItem);
645
+ if (proofLines.length > 0)
646
+ return proofLines;
647
+ const commands = resume.remainingProofCommands ?? [];
648
+ return commands.length > 0
649
+ ? commands.map((command) => `- [ ] \`${command}\``)
650
+ : ['- [ ] No proof commands are ready yet.'];
651
+ }
652
+ function formatTaskCardChecklistItem(item) {
653
+ if (item.kind === 'resolve_input') {
654
+ const label = item.label ? ` (\`${item.label}\`)` : '';
655
+ const instruction = item.instruction ?? item.label;
656
+ return `- [ ] Resolve \`${item.stepId}\`${label}: ${instruction}`;
657
+ }
658
+ if (item.kind === 'run_follow_up' && item.command) {
659
+ const prefix = item.status === 'blocked' ? 'After inputs, run' : 'Then run';
660
+ return `- [ ] ${prefix} \`${item.command}\`${formatTaskCardChecklistAnnotation(item)}`;
661
+ }
662
+ if (item.command) {
663
+ return `- [ ] Run \`${item.command}\`${formatTaskCardChecklistAnnotation(item)}`;
664
+ }
665
+ return `- [ ] ${item.instruction ?? item.label}`;
666
+ }
667
+ function formatTaskCardChecklistAnnotation(item) {
668
+ if (!item.tool)
669
+ return '';
670
+ return ` (MCP: ${formatTaskCardToolCall({ tool: item.tool, ...(typeof item.args !== 'undefined' ? { args: item.args } : {}) })})`;
671
+ }
672
+ function formatTaskCardProofItem(item) {
673
+ const annotation = item.toolCall
674
+ ? ` (MCP: ${formatTaskCardToolCall(item.toolCall)})`
675
+ : ' (CLI only)';
676
+ return `- [ ] \`${item.command}\`${annotation}`;
677
+ }
678
+ function formatTaskCardToolCall(toolCall) {
679
+ return typeof toolCall.args !== 'undefined'
680
+ ? `${toolCall.tool} ${JSON.stringify(toolCall.args)}`
681
+ : toolCall.tool;
682
+ }
683
+ function missionResume(plan) {
684
+ const cursor = plan.cursor;
685
+ const commandBlock = cursor.command && isRunnableCommand(cursor.command) ? cursor.command : undefined;
686
+ const toolCall = resumeToolCall(plan, cursor);
687
+ const followUps = resumeFollowUps(plan, cursor);
688
+ const inputBindings = resumeInputBindings(plan, cursor);
689
+ const checklist = resumeChecklist(plan, cursor, inputBindings, followUps);
690
+ const remainingProofItems = resumeRemainingProofItems(checklist);
691
+ const remainingProofCommands = resumeRemainingProofCommands(checklist);
692
+ const remainingProofToolCalls = resumeRemainingProofToolCalls(checklist);
693
+ const unlocks = resolveResumeReferences(plan, cursor.unlocks);
694
+ const blockedBy = resolveResumeReferences(plan, cursor.blockedBy);
695
+ const instruction = commandBlock
696
+ ? `Run ${commandBlock}.`
697
+ : cursor.instruction
698
+ ? `Resolve ${cursor.label}: ${cursor.instruction}`
699
+ : `Continue with ${cursor.label}.`;
700
+ const prompt = commandBlock
701
+ ? `Resume at ${cursor.stepId} in ${cursor.phaseId}: run \`${commandBlock}\`.${resumeUnlocksSentence(unlocks, cursor.unlocks)}`
702
+ : `Resume at ${cursor.stepId} in ${cursor.phaseId}: ${instruction}${resumeBlockersSentence(blockedBy, cursor.blockedBy)}`;
703
+ return {
704
+ currentStep: cursor,
705
+ status: cursor.status,
706
+ instruction,
707
+ prompt,
708
+ ...(commandBlock ? { commandBlock } : {}),
709
+ ...(toolCall ? { toolCall } : {}),
710
+ ...(followUps.length > 0 ? { followUps } : {}),
711
+ ...(inputBindings.length > 0 ? { inputBindings } : {}),
712
+ ...(checklist.length > 0 ? { checklist } : {}),
713
+ ...(remainingProofItems.length > 0 ? { remainingProofItems } : {}),
714
+ ...(remainingProofCommands.length > 0 ? { remainingProofCommands } : {}),
715
+ ...(remainingProofToolCalls.length > 0 ? { remainingProofToolCalls } : {}),
716
+ ...(unlocks.length > 0 ? { unlocks } : {}),
717
+ ...(blockedBy.length > 0 ? { blockedBy } : {}),
718
+ };
719
+ }
720
+ function resumeRemainingProofCommands(checklist) {
721
+ return checklist
722
+ .filter((item) => item.kind === 'run_proof' && typeof item.command === 'string')
723
+ .map((item) => item.command);
724
+ }
725
+ function resumeRemainingProofItems(checklist) {
726
+ return checklist.flatMap((item) => {
727
+ if (item.kind !== 'run_proof' || typeof item.command !== 'string')
728
+ return [];
729
+ const toolCall = proofChecklistToolCall(item);
730
+ return [{
731
+ stepId: item.stepId,
732
+ status: item.status,
733
+ label: item.label,
734
+ command: item.command,
735
+ ...(toolCall ? { toolCall } : {}),
736
+ }];
737
+ });
738
+ }
739
+ function resumeRemainingProofToolCalls(checklist) {
740
+ return checklist.flatMap((item) => {
741
+ if (item.kind !== 'run_proof' || typeof item.command !== 'string')
742
+ return [];
743
+ const toolCall = proofChecklistToolCall(item);
744
+ return toolCall ? [{ stepId: item.stepId, command: item.command, ...toolCall }] : [];
745
+ });
746
+ }
747
+ function proofChecklistToolCall(item) {
748
+ if (item.tool) {
749
+ return {
750
+ tool: item.tool,
751
+ ...(typeof item.args !== 'undefined' ? { args: item.args } : {}),
752
+ };
753
+ }
754
+ return typeof item.command === 'string' ? proofCommandToolCall(item.command) : undefined;
755
+ }
756
+ function proofCommandToolCall(command) {
757
+ const preflightMatch = /^projscan preflight(?: --mode ([a-z_]+))? --format json$/.exec(command);
758
+ if (preflightMatch) {
759
+ return {
760
+ tool: 'projscan_preflight',
761
+ args: preflightMatch[1] ? { mode: preflightMatch[1] } : {},
762
+ };
763
+ }
764
+ const understandMatch = /^projscan understand --view ([a-z_]+)(?: --intent "((?:\\.|[^"\\])*)")? --format json$/.exec(command);
765
+ if (understandMatch) {
766
+ return {
767
+ tool: 'projscan_understand',
768
+ args: {
769
+ view: understandMatch[1],
770
+ ...(understandMatch[2] ? { intent: unescapeDoubleQuoted(understandMatch[2]) } : {}),
771
+ },
772
+ };
773
+ }
774
+ if (command === 'projscan session touched --format json') {
775
+ return {
776
+ tool: 'projscan_session',
777
+ args: { action: 'touched' },
778
+ };
779
+ }
780
+ return undefined;
781
+ }
782
+ function unescapeDoubleQuoted(value) {
783
+ return value.replace(/\\(["\\$`])/g, '$1');
784
+ }
785
+ function resumeChecklist(plan, cursor, inputBindings, followUps) {
786
+ const current = findStepInPlan(plan, cursor.stepId);
787
+ const currentItem = current
788
+ ? resumeChecklistItemFromStep(current.phase, current.step, currentChecklistKind(current.step), cursor.stepId)
789
+ : undefined;
790
+ const includedStepIds = new Set(currentItem ? [currentItem.stepId] : []);
791
+ const inputItems = inputBindings.flatMap((binding) => {
792
+ if (includedStepIds.has(binding.inputId))
793
+ return [];
794
+ const found = findStepInPlan(plan, binding.inputId);
795
+ if (!found)
796
+ return [];
797
+ includedStepIds.add(found.step.id);
798
+ return [{
799
+ id: `resume-${found.step.id}`,
800
+ kind: 'resolve_input',
801
+ phaseId: found.phase.id,
802
+ stepId: found.step.id,
803
+ status: found.step.status,
804
+ label: binding.label,
805
+ placeholder: binding.placeholder,
806
+ instruction: binding.instruction,
807
+ followUpIds: binding.followUpIds,
808
+ ...(found.step.dependsOn && found.step.dependsOn.length > 0 ? { dependsOn: found.step.dependsOn } : {}),
809
+ ...(found.step.unlocks && found.step.unlocks.length > 0 ? { unlocks: found.step.unlocks } : {}),
810
+ }];
811
+ });
812
+ const followUpItems = followUps.flatMap((followUp) => {
813
+ if (includedStepIds.has(followUp.id))
814
+ return [];
815
+ includedStepIds.add(followUp.id);
816
+ return [{
817
+ id: `resume-${followUp.id}`,
818
+ kind: 'run_follow_up',
819
+ phaseId: followUp.phaseId,
820
+ stepId: followUp.id,
821
+ status: followUp.status,
822
+ label: followUp.label,
823
+ ...(followUp.command ? { command: followUp.command } : {}),
824
+ ...(followUp.tool ? { tool: followUp.tool } : {}),
825
+ ...(followUp.args ? { args: followUp.args } : {}),
826
+ ...(followUp.blockedBy && followUp.blockedBy.length > 0 ? { blockedBy: followUp.blockedBy } : {}),
827
+ ...(followUp.dependsOn && followUp.dependsOn.length > 0 ? { dependsOn: followUp.dependsOn } : {}),
828
+ }];
829
+ });
830
+ const currentCommand = current?.step.command;
831
+ const proofItems = stepsForPhase(plan, 'proof')
832
+ .filter(({ step }) => step.command && step.command !== currentCommand)
833
+ .map(({ phase, step }) => resumeChecklistItemFromStep(phase, step, 'run_proof', step.id));
834
+ const doneItems = stepsForPhase(plan, 'done_when')
835
+ .map(({ phase, step }) => resumeChecklistItemFromStep(phase, step, 'confirm_done', step.id));
836
+ return [
837
+ ...(currentItem ? [currentItem] : []),
838
+ ...inputItems,
839
+ ...followUpItems,
840
+ ...proofItems,
841
+ ...doneItems,
842
+ ];
843
+ }
844
+ function resumeChecklistItemFromStep(phase, step, kind, stepId) {
845
+ return {
846
+ id: `resume-${stepId}`,
847
+ kind,
848
+ phaseId: phase.id,
849
+ stepId,
850
+ status: step.status,
851
+ label: step.label,
852
+ ...(step.command ? { command: step.command } : {}),
853
+ ...(step.tool ? { tool: step.tool } : {}),
854
+ ...(step.args ? { args: step.args } : {}),
855
+ ...(step.placeholder ? { placeholder: step.placeholder } : {}),
856
+ ...(step.instruction ? { instruction: step.instruction } : {}),
857
+ ...(step.blockedBy && step.blockedBy.length > 0 ? { blockedBy: step.blockedBy } : {}),
858
+ ...(step.dependsOn && step.dependsOn.length > 0 ? { dependsOn: step.dependsOn } : {}),
859
+ ...(step.unlocks && step.unlocks.length > 0 ? { unlocks: step.unlocks } : {}),
860
+ };
861
+ }
862
+ function currentChecklistKind(step) {
863
+ if (step.kind === 'input')
864
+ return 'resolve_input';
865
+ if (step.kind === 'proof')
866
+ return 'run_proof';
867
+ if (step.kind === 'criterion')
868
+ return 'confirm_done';
869
+ return 'run_current';
870
+ }
871
+ function resumeInputBindings(plan, cursor) {
872
+ const ids = uniqueStrings([
873
+ ...(cursor.kind === 'input' ? [cursor.stepId] : []),
874
+ ...(cursor.unlocks ?? []),
875
+ ]);
876
+ return ids.flatMap((id) => {
877
+ const found = findStepInPlan(plan, id);
878
+ if (!found || found.step.kind !== 'input' || !found.step.placeholder || !found.step.instruction)
879
+ return [];
880
+ const followUpIds = (found.step.unlocks ?? []).filter((unlockedId) => findStepInPlan(plan, unlockedId)?.phase.id === 'follow_up');
881
+ return [{
882
+ inputId: found.step.id,
883
+ label: found.step.label,
884
+ placeholder: found.step.placeholder,
885
+ instruction: found.step.instruction,
886
+ followUpIds,
887
+ }];
888
+ });
889
+ }
890
+ function resumeFollowUps(plan, cursor) {
891
+ const followUpIds = new Set();
892
+ for (const id of cursor.unlocks ?? []) {
893
+ const found = findStepInPlan(plan, id);
894
+ if (!found)
895
+ continue;
896
+ if (found.phase.id === 'follow_up')
897
+ followUpIds.add(found.step.id);
898
+ for (const unlockedId of found.step.unlocks ?? []) {
899
+ const unlocked = findStepInPlan(plan, unlockedId);
900
+ if (unlocked?.phase.id === 'follow_up')
901
+ followUpIds.add(unlocked.step.id);
902
+ }
903
+ }
904
+ return Array.from(followUpIds).flatMap((id) => {
905
+ const found = findStepInPlan(plan, id);
906
+ if (!found)
907
+ return [];
908
+ return [{
909
+ id: found.step.id,
910
+ phaseId: found.phase.id,
911
+ kind: found.step.kind,
912
+ status: found.step.status,
913
+ label: found.step.label,
914
+ ...(found.step.command ? { command: found.step.command } : {}),
915
+ ...(found.step.tool ? { tool: found.step.tool } : {}),
916
+ ...(found.step.args ? { args: found.step.args } : {}),
917
+ ...(found.step.blockedBy && found.step.blockedBy.length > 0 ? { blockedBy: found.step.blockedBy } : {}),
918
+ ...(found.step.dependsOn && found.step.dependsOn.length > 0 ? { dependsOn: found.step.dependsOn } : {}),
919
+ }];
920
+ });
921
+ }
922
+ function resumeToolCall(plan, cursor) {
923
+ const found = findStepInPlan(plan, cursor.stepId);
924
+ if (!found?.step.tool || !argsAreReady(found.step.args))
925
+ return undefined;
926
+ return {
927
+ tool: found.step.tool,
928
+ ...(typeof found.step.args !== 'undefined' ? { args: found.step.args } : {}),
929
+ };
930
+ }
931
+ function resolveResumeReferences(plan, ids) {
932
+ if (!ids || ids.length === 0)
933
+ return [];
934
+ const references = [];
935
+ for (const id of ids) {
936
+ const found = findStepInPlan(plan, id);
937
+ if (!found)
938
+ continue;
939
+ references.push({
940
+ id: found.step.id,
941
+ phaseId: found.phase.id,
942
+ kind: found.step.kind,
943
+ status: found.step.status,
944
+ label: found.step.label,
945
+ ...(found.step.instruction ? { instruction: found.step.instruction } : {}),
946
+ ...(found.step.command ? { command: found.step.command } : {}),
947
+ ...(found.step.placeholder ? { placeholder: found.step.placeholder } : {}),
948
+ });
949
+ }
950
+ return references;
951
+ }
952
+ function findStepInPlan(plan, id) {
953
+ for (const phase of plan.phases) {
954
+ for (const step of phase.steps) {
955
+ if (step.id === id)
956
+ return { phase, step };
957
+ }
958
+ }
959
+ return undefined;
960
+ }
961
+ function stepsForPhase(plan, phaseId) {
962
+ const phase = plan.phases.find((item) => item.id === phaseId);
963
+ return phase ? phase.steps.map((step) => ({ phase, step })) : [];
964
+ }
965
+ function resumeUnlocksSentence(unlocks, rawIds) {
966
+ if (unlocks.length > 0)
967
+ return ` This can unlock ${unlocks.map(formatResumeReferenceLabel).join(', ')}.`;
968
+ return rawIds && rawIds.length > 0 ? ` This can unlock ${rawIds.join(', ')}.` : '';
969
+ }
970
+ function resumeBlockersSentence(blockedBy, rawIds) {
971
+ if (blockedBy.length > 0)
972
+ return ` Blocked by ${blockedBy.map(formatResumeReferenceLabel).join(', ')}.`;
973
+ return rawIds && rawIds.length > 0 ? ` Blocked by ${rawIds.join(', ')}.` : '';
974
+ }
975
+ function formatResumeReferenceLabel(reference) {
976
+ return `${reference.id} (${reference.label})`;
977
+ }
978
+ function renderRunbookResumeLines(resume) {
979
+ const lines = ['## Resume'];
980
+ if (resume.commandBlock) {
981
+ lines.push('Run now:', '```sh', resume.commandBlock, '```');
982
+ }
983
+ else {
984
+ lines.push(`Do now: ${resume.instruction}`);
985
+ }
986
+ if (resume.toolCall) {
987
+ lines.push(`MCP call: ${formatRunbookToolCall(resume.toolCall)}`);
988
+ }
989
+ if (resume.unlocks && resume.unlocks.length > 0) {
990
+ lines.push('After running, resolve:', ...resume.unlocks.map((reference) => `- ${formatRunbookResumeReference(reference)}`));
991
+ }
992
+ if (resume.inputBindings && resume.inputBindings.length > 0) {
993
+ lines.push('Template inputs:', ...resume.inputBindings.map((binding) => `- ${formatRunbookInputBinding(binding)}`));
994
+ }
995
+ if (resume.checklist && resume.checklist.length > 0) {
996
+ lines.push('Resume checklist:', ...resume.checklist.map((item) => `- ${formatRunbookChecklistItem(item)}`));
997
+ }
998
+ if (resume.remainingProofItems && resume.remainingProofItems.length > 0) {
999
+ lines.push('Proof queue:', ...resume.remainingProofItems.map((item) => `- ${formatRunbookProofItem(item)}`));
1000
+ }
1001
+ if (resume.remainingProofCommands && resume.remainingProofCommands.length > 0) {
1002
+ lines.push('Remaining proof:', ...resume.remainingProofCommands.map((command) => `- \`${command}\``));
1003
+ }
1004
+ if (resume.remainingProofToolCalls && resume.remainingProofToolCalls.length > 0) {
1005
+ lines.push('MCP proof calls:', ...resume.remainingProofToolCalls.map((toolCall) => `- ${formatRunbookProofToolCall(toolCall)}`));
1006
+ }
1007
+ if (resume.followUps && resume.followUps.length > 0) {
1008
+ lines.push('Then use:', ...resume.followUps.map((followUp) => `- ${formatRunbookFollowUp(followUp)}`));
1009
+ }
1010
+ if (resume.blockedBy && resume.blockedBy.length > 0) {
1011
+ lines.push('Blocked by:', ...resume.blockedBy.map((reference) => `- ${formatRunbookResumeReference(reference)}`));
1012
+ }
1013
+ lines.push(`Prompt: ${resume.prompt}`);
1014
+ return lines;
1015
+ }
1016
+ function formatRunbookResumeReference(reference) {
1017
+ const detail = reference.instruction ?? reference.command ?? reference.label;
1018
+ return `${reference.id} (${reference.label}): ${detail}`;
1019
+ }
1020
+ function formatRunbookInputBinding(binding) {
1021
+ return `${binding.placeholder} -> ${binding.inputId} (${binding.label}): ${binding.instruction}`;
1022
+ }
1023
+ function formatRunbookChecklistItem(item) {
1024
+ const action = item.command
1025
+ ?? (item.placeholder && item.instruction ? `${item.placeholder} -> ${item.instruction}` : undefined)
1026
+ ?? item.instruction
1027
+ ?? item.label;
1028
+ return `[${item.status}] ${item.kind} ${item.stepId}: ${action}${formatRunbookChecklistAnnotation(item)}`;
1029
+ }
1030
+ function formatRunbookChecklistAnnotation(item) {
1031
+ if (item.tool) {
1032
+ return ` (MCP: ${formatRunbookToolCall({ tool: item.tool, ...(typeof item.args !== 'undefined' ? { args: item.args } : {}) })})`;
1033
+ }
1034
+ if (item.kind === 'run_proof' && item.command)
1035
+ return ' (CLI only)';
1036
+ return '';
1037
+ }
1038
+ function formatRunbookToolCall(toolCall) {
1039
+ return typeof toolCall.args !== 'undefined'
1040
+ ? `${toolCall.tool} ${JSON.stringify(toolCall.args)}`
1041
+ : toolCall.tool;
1042
+ }
1043
+ function formatRunbookProofToolCall(toolCall) {
1044
+ return `${toolCall.stepId}: ${formatRunbookToolCall(toolCall)}`;
1045
+ }
1046
+ function formatRunbookProofItem(item) {
1047
+ const proofAction = item.toolCall ? `MCP: ${formatRunbookToolCall(item.toolCall)}` : 'CLI only';
1048
+ return `${item.stepId}: \`${item.command}\` (${proofAction})`;
1049
+ }
1050
+ function formatRunbookFollowUp(followUp) {
1051
+ const action = followUp.command
1052
+ ?? (followUp.tool ? formatRunbookToolCall({ tool: followUp.tool, ...(typeof followUp.args !== 'undefined' ? { args: followUp.args } : {}) }) : followUp.label);
1053
+ return `${followUp.id} (${followUp.label}): ${action}`;
1054
+ }
1055
+ function renderRunbookCursorLines(cursor) {
1056
+ const lines = [
1057
+ '## Current Cursor',
1058
+ `- Step: ${cursor.stepId} in ${cursor.phaseId}`,
1059
+ ];
1060
+ if (cursor.command) {
1061
+ lines.push(`- Command: \`${cursor.command}\``);
1062
+ }
1063
+ else if (cursor.instruction) {
1064
+ lines.push(`- Input: ${cursor.instruction}`);
1065
+ }
1066
+ else {
1067
+ lines.push(`- Label: ${cursor.label}`);
1068
+ }
1069
+ if (cursor.tool) {
1070
+ lines.push(`- MCP call: ${formatRunbookToolCall({ tool: cursor.tool, ...(typeof cursor.args !== 'undefined' ? { args: cursor.args } : {}) })}`);
1071
+ }
1072
+ if (cursor.blockedBy && cursor.blockedBy.length > 0) {
1073
+ lines.push(`- Blocked by: ${cursor.blockedBy.join(', ')}`);
1074
+ }
1075
+ if (cursor.unlocks && cursor.unlocks.length > 0) {
1076
+ lines.push(`- Unlocks: ${cursor.unlocks.join(', ')}`);
1077
+ }
1078
+ lines.push(`- Why: ${cursor.reason}`);
1079
+ return lines;
1080
+ }
1081
+ function buildMissionExecutionPlan(input) {
1082
+ const phases = [];
1083
+ const readyStepIds = input.readyActions.map((_, index) => `ready-${index + 1}`);
1084
+ const inputStepIdsByPlaceholder = new Map(input.unresolvedInputs.map((item, index) => [item.placeholder, `input-${index + 1}`]));
1085
+ const nextActionStep = actionToExecutionStep('next-action-1', input.primaryAction);
1086
+ phases.push({
1087
+ id: 'next_action',
1088
+ title: 'Next Action',
1089
+ status: nextActionStep.status,
1090
+ steps: [nextActionStep],
1091
+ });
1092
+ if (input.readyActions.length > 0) {
1093
+ phases.push({
1094
+ id: 'ready_now',
1095
+ title: 'Ready Commands',
1096
+ status: 'ready',
1097
+ steps: input.readyActions.map((action, index) => {
1098
+ const step = actionToExecutionStep(`ready-${index + 1}`, action);
1099
+ const unlockedInputs = Array.from(inputStepIdsByPlaceholder.values());
1100
+ if (index === 0 && unlockedInputs.length > 0)
1101
+ step.unlocks = unlockedInputs;
1102
+ return step;
1103
+ }),
1104
+ });
1105
+ }
1106
+ if (input.unresolvedInputs.length > 0) {
1107
+ phases.push({
1108
+ id: 'resolve_inputs',
1109
+ title: 'Resolve Inputs',
1110
+ status: 'blocked',
1111
+ steps: input.unresolvedInputs.map((item, index) => {
1112
+ const id = `input-${index + 1}`;
1113
+ const followUps = followUpIdsForPlaceholder(input.actionPlan, item.placeholder);
1114
+ return {
1115
+ id,
1116
+ kind: 'input',
1117
+ status: 'blocked',
1118
+ label: item.name,
1119
+ ...(readyStepIds[0] ? { dependsOn: [readyStepIds[0]] } : {}),
1120
+ ...(followUps.length > 0 ? { unlocks: followUps } : {}),
1121
+ placeholder: item.placeholder,
1122
+ instruction: item.instruction,
1123
+ };
1124
+ }),
1125
+ });
1126
+ }
1127
+ const pendingActions = input.actionPlan.filter((action) => !isReadyAction(action));
1128
+ if (pendingActions.length > 0) {
1129
+ phases.push({
1130
+ id: 'follow_up',
1131
+ title: 'Follow Up',
1132
+ status: 'pending',
1133
+ steps: pendingActions.map((action, index) => {
1134
+ const step = actionToExecutionStep(`follow-up-${index + 1}`, action);
1135
+ const blockedBy = placeholdersInAction(action)
1136
+ .map((placeholder) => inputStepIdsByPlaceholder.get(placeholder))
1137
+ .filter((id) => typeof id === 'string');
1138
+ if (blockedBy.length > 0) {
1139
+ step.blockedBy = blockedBy;
1140
+ step.dependsOn = uniqueStrings([readyStepIds[0] ?? '', ...blockedBy].filter(Boolean));
1141
+ }
1142
+ return step;
1143
+ }),
1144
+ });
1145
+ }
1146
+ if (input.proofCommands.length > 0) {
1147
+ phases.push({
1148
+ id: 'proof',
1149
+ title: 'Proof',
1150
+ status: 'ready',
1151
+ steps: input.proofCommands.map((command, index) => {
1152
+ const toolCall = proofCommandToolCall(command);
1153
+ return {
1154
+ id: `proof-${index + 1}`,
1155
+ kind: 'proof',
1156
+ status: 'ready',
1157
+ label: command,
1158
+ command,
1159
+ ...(toolCall ? {
1160
+ tool: toolCall.tool,
1161
+ ...(typeof toolCall.args !== 'undefined' ? { args: toolCall.args } : {}),
1162
+ } : {}),
1163
+ };
1164
+ }),
1165
+ });
1166
+ }
1167
+ phases.push({
1168
+ id: 'done_when',
1169
+ title: 'Done When',
1170
+ status: 'pending',
1171
+ steps: input.successCriteria.map((criterion, index) => ({
1172
+ id: `criterion-${index + 1}`,
1173
+ kind: 'criterion',
1174
+ status: 'pending',
1175
+ label: criterion,
1176
+ })),
1177
+ });
1178
+ const cursor = executionCursor(phases);
1179
+ return {
1180
+ summary: executionPlanSummary(input.readyActions.length, input.unresolvedInputs.length, input.proofCommands.length),
1181
+ currentPhase: cursor.phaseId,
1182
+ cursor,
1183
+ phases,
1184
+ };
1185
+ }
1186
+ function actionToExecutionStep(id, action) {
1187
+ const step = {
1188
+ id,
1189
+ kind: 'tool',
1190
+ status: executionStatusForAction(action),
1191
+ label: action.label,
1192
+ };
1193
+ if (typeof action.command === 'string')
1194
+ step.command = action.command;
1195
+ if (typeof action.tool === 'string')
1196
+ step.tool = action.tool;
1197
+ if (action.args)
1198
+ step.args = action.args;
1199
+ return step;
1200
+ }
1201
+ function followUpIdsForPlaceholder(actionPlan, placeholder) {
1202
+ return actionPlan
1203
+ .filter((action) => !isReadyAction(action))
1204
+ .map((action, index) => ({ action, id: `follow-up-${index + 1}` }))
1205
+ .filter(({ action }) => placeholdersInAction(action).includes(placeholder))
1206
+ .map(({ id }) => id);
1207
+ }
1208
+ function placeholdersInAction(action) {
1209
+ const placeholders = new Set();
1210
+ if (typeof action.command === 'string') {
1211
+ for (const match of action.command.matchAll(/<[^<>]+>/g))
1212
+ placeholders.add(match[0]);
1213
+ }
1214
+ collectPlaceholdersFromValue(action.args, placeholders);
1215
+ return Array.from(placeholders);
1216
+ }
1217
+ function collectPlaceholdersFromValue(value, placeholders) {
1218
+ if (typeof value === 'string') {
1219
+ if (isPlaceholder(value))
1220
+ placeholders.add(value);
1221
+ return;
1222
+ }
1223
+ if (Array.isArray(value)) {
1224
+ for (const item of value)
1225
+ collectPlaceholdersFromValue(item, placeholders);
1226
+ return;
1227
+ }
1228
+ if (value && typeof value === 'object') {
1229
+ for (const item of Object.values(value))
1230
+ collectPlaceholdersFromValue(item, placeholders);
1231
+ }
1232
+ }
1233
+ function executionStatusForAction(action) {
1234
+ if (isReadyAction(action))
1235
+ return 'ready';
1236
+ if ((typeof action.command === 'string' && !isRunnableCommand(action.command)) || !argsAreReady(action.args)) {
1237
+ return 'blocked';
1238
+ }
1239
+ return 'pending';
1240
+ }
1241
+ function executionCursor(phases) {
1242
+ const selected = findExecutionStep(phases, (phase, step) => phase.id === 'ready_now' && step.status === 'ready' && typeof step.command === 'string')
1243
+ ?? findExecutionStep(phases, (phase, step) => phase.id === 'resolve_inputs' && step.status === 'blocked')
1244
+ ?? findExecutionStep(phases, (phase, step) => phase.id === 'proof' && step.status === 'ready')
1245
+ ?? findExecutionStep(phases, (phase) => phase.id === 'done_when')
1246
+ ?? findExecutionStep(phases, (phase) => phase.id === 'next_action');
1247
+ if (!selected) {
1248
+ return {
1249
+ phaseId: 'done_when',
1250
+ stepId: 'criterion-1',
1251
+ status: 'pending',
1252
+ kind: 'criterion',
1253
+ label: 'The next action is complete and verified.',
1254
+ reason: 'Use this criterion to decide when the task is complete.',
1255
+ };
1256
+ }
1257
+ return {
1258
+ phaseId: selected.phase.id,
1259
+ stepId: selected.step.id,
1260
+ status: selected.step.status,
1261
+ kind: selected.step.kind,
1262
+ label: selected.step.label,
1263
+ ...(selected.step.command ? { command: selected.step.command } : {}),
1264
+ ...(selected.step.tool ? { tool: selected.step.tool } : {}),
1265
+ ...(typeof selected.step.args !== 'undefined' ? { args: selected.step.args } : {}),
1266
+ ...(selected.step.instruction ? { instruction: selected.step.instruction } : {}),
1267
+ ...(selected.step.placeholder ? { placeholder: selected.step.placeholder } : {}),
1268
+ ...(selected.step.blockedBy && selected.step.blockedBy.length > 0 ? { blockedBy: selected.step.blockedBy } : {}),
1269
+ ...(selected.step.unlocks && selected.step.unlocks.length > 0 ? { unlocks: selected.step.unlocks } : {}),
1270
+ reason: executionCursorReason(selected.step),
1271
+ };
1272
+ }
1273
+ function findExecutionStep(phases, predicate) {
1274
+ for (const phase of phases) {
1275
+ for (const step of phase.steps) {
1276
+ if (predicate(phase, step))
1277
+ return { phase, step };
1278
+ }
1279
+ }
1280
+ return undefined;
1281
+ }
1282
+ function executionCursorReason(step) {
1283
+ if (step.status === 'ready' && step.kind === 'tool') {
1284
+ return step.unlocks && step.unlocks.length > 0
1285
+ ? 'Run this ready command next; it can unlock later inputs or follow-up steps.'
1286
+ : 'Run this ready command next.';
1287
+ }
1288
+ if (step.status === 'blocked' && step.kind === 'input') {
1289
+ return 'Resolve this blocked input before running dependent follow-up steps.';
1290
+ }
1291
+ if (step.status === 'ready' && step.kind === 'proof') {
1292
+ return 'Run this proof command when action steps are complete.';
1293
+ }
1294
+ if (step.kind === 'criterion') {
1295
+ return 'Use this criterion to decide when the task is complete.';
1296
+ }
1297
+ return 'Use this step as the current execution pointer.';
1298
+ }
1299
+ function executionPlanSummary(readyCount, inputCount, proofCount) {
1300
+ const pieces = [`Run ${readyCount} ready ${pluralize(readyCount, 'step')}`];
1301
+ if (inputCount > 0)
1302
+ pieces.push(`resolve ${inputCount} input(s)`);
1303
+ if (proofCount > 0)
1304
+ pieces.push(`then gather ${proofCount} proof command(s)`);
1305
+ return `${pieces.join(', ')}.`;
1306
+ }
1307
+ function pluralize(count, singular) {
1308
+ return count === 1 ? singular : `${singular}s`;
1309
+ }
1310
+ function missionHandoff(currentStep, resume, nextAction, readyActions, needsInput, doneWhen, proofCommands, reviewGate) {
1311
+ const readyProofCommands = resume.remainingProofCommands ?? proofCommands;
1312
+ const readyProofToolCalls = resume.remainingProofToolCalls;
1313
+ const readyProofItems = resume.remainingProofItems;
1314
+ return {
1315
+ currentStep,
1316
+ resume,
1317
+ reviewGate,
297
1318
  nextAction,
298
1319
  readyActions,
299
1320
  needsInput,
300
1321
  doneWhen,
301
1322
  readyProof: {
302
1323
  summary: READY_PROOF_SUMMARY,
303
- commands: proofCommands,
1324
+ commands: readyProofCommands,
1325
+ ...(readyProofToolCalls && readyProofToolCalls.length > 0 ? { toolCalls: readyProofToolCalls } : {}),
1326
+ ...(readyProofItems && readyProofItems.length > 0 ? { items: readyProofItems } : {}),
304
1327
  },
305
1328
  };
306
1329
  }
307
- function missionHandoffPrompt(commandText, successCriteria, whyNow, unresolvedInputs, proofCommands) {
1330
+ function missionHandoffPrompt(resume, successCriteria, whyNow, unresolvedInputs, proofCommands, reviewGate) {
308
1331
  const needsInput = unresolvedInputs.length > 0
309
1332
  ? ` Needs input: ${unresolvedInputs.map((input) => `${input.name}=${input.placeholder}`).join(', ')}.`
310
1333
  : '';
311
- return `Next: ${commandText}. Done when: ${trimTrailingPunctuation(successCriteria[0] ?? 'The proof commands pass')}.${needsInput} Why: ${whyNow} Ready proof: ${READY_PROOF_SUMMARY} ${proofCommands.slice(0, 3).join(' && ')}.`;
1334
+ const proofCommandText = (resume.remainingProofCommands ?? proofCommands).slice(0, 3).join(' && ');
1335
+ const readyProof = proofCommandText.length > 0
1336
+ ? ` Ready proof: ${READY_PROOF_SUMMARY} ${proofCommandText}.`
1337
+ : ` Ready proof: ${READY_PROOF_SUMMARY}.`;
1338
+ return `Resume: ${trimTrailingPunctuation(resume.prompt)}. Done when: ${trimTrailingPunctuation(successCriteria[0] ?? 'The proof commands pass')}.${needsInput} Why: ${whyNow}${readyProof}${handoffReviewGatePrompt(reviewGate)}`;
1339
+ }
1340
+ function handoffReviewGatePrompt(reviewGate) {
1341
+ const decisions = reviewGate.decisions
1342
+ .map((decision) => `${decision.label} => ${decision.reply}`)
1343
+ .join('; ');
1344
+ return ` Review gate: ${trimTrailingPunctuation(reviewGate.stopCondition)}. Reviewer replies: ${decisions}`;
312
1345
  }
313
1346
  function trimTrailingPunctuation(value) {
314
1347
  return value.replace(/[.!?]+$/g, '');
@@ -1816,7 +2849,11 @@ function normalizePackageName(target) {
1816
2849
  return target.toLowerCase();
1817
2850
  }
1818
2851
  function escapeDoubleQuoted(value) {
1819
- return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
2852
+ return value
2853
+ .replace(/\\/g, '\\\\')
2854
+ .replace(/"/g, '\\"')
2855
+ .replace(/\$/g, '\\$')
2856
+ .replace(/`/g, '\\`');
1820
2857
  }
1821
2858
  function quoteShellArg(value) {
1822
2859
  return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `"${escapeDoubleQuoted(value)}"`;