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.
- package/README.md +173 -25
- package/dist/cli/commands/start.js +1022 -2
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/core/start.js +1045 -8
- package/dist/core/start.js.map +1 -1
- package/dist/projscan-sbom.cdx.json +6 -6
- package/dist/tool-manifest.json +2 -2
- package/dist/types.d.ts +192 -0
- package/docs/GUIDE.md +3 -1
- package/docs/demos/projscan-4-1-demo.html +94 -65
- package/docs/projscan-mission-control.png +0 -0
- package/docs/projscan-proof-router.png +0 -0
- package/package.json +1 -1
- package/scripts/capture-readme-assets.mjs +1 -1
package/dist/core/start.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
292
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
|
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)}"`;
|