@leing2021/super-pi 0.23.2 → 0.23.3

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.
@@ -310,6 +310,7 @@ const contextHandoffParams = Type.Object({
310
310
  Type.Literal("load"),
311
311
  Type.Literal("latest"),
312
312
  Type.Literal("status"),
313
+ Type.Literal("validate"),
313
314
  ], { description: "Handoff operation" }),
314
315
  repoRoot: Type.String({ description: "Repository root" }),
315
316
  currentStage: Type.Optional(Type.String({ description: "Current pipeline stage (e.g. 02-plan)" })),
@@ -393,18 +394,21 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
393
394
  if (thinkingStrategy) {
394
395
  const targetThinking = thinkingStrategy[stageKey] ?? thinkingStrategy.default
395
396
  if (targetThinking) {
396
- const levelMap: Record<string, "low" | "medium" | "high"> = {
397
+ const levelMap: Record<string, ReturnType<ExtensionAPI["getThinkingLevel"]>> = {
398
+ off: "off",
399
+ minimal: "minimal",
397
400
  low: "low",
398
401
  medium: "medium",
399
402
  high: "high",
403
+ xhigh: "xhigh",
400
404
  "0": "low",
401
405
  "1": "medium",
402
406
  "2": "high",
403
407
  }
404
408
  const normalized = levelMap[targetThinking.toLowerCase()] ?? "medium"
405
- const currentLevel = ctx.getThinkingLevel?.() ?? "medium"
409
+ const currentLevel = pi.getThinkingLevel()
406
410
  if (currentLevel !== normalized) {
407
- ctx.setThinkingLevel?.(normalized)
411
+ pi.setThinkingLevel(normalized)
408
412
  if (ctx.hasUI) {
409
413
  ctx.ui.notify(`Switched thinking level for ${stageKey}: ${normalized}`, "info")
410
414
  }
@@ -748,7 +752,7 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
748
752
  pi.registerTool({
749
753
  name: contextHandoff.name,
750
754
  label: "Context Handoff",
751
- description: "Manage cross-stage context handoffs with evidence-first templates. Supports save (write handoff + state), load (read handoff + state), latest (read latest dated handoff), and status (read current state).",
755
+ description: "Manage cross-stage context handoffs with evidence-first templates. Supports save (write handoff + state), load (read handoff + state), latest (read latest dated handoff), status (read current state), and validate (check continuation readiness with deterministic probes).",
752
756
  parameters: contextHandoffParams,
753
757
  async execute(_toolCallId, params) {
754
758
  const result = await contextHandoff.execute({
@@ -5,8 +5,23 @@ import { normalizeSlug } from "../utils/name-utils"
5
5
 
6
6
  export type ContextHealth = "good" | "watch" | "heavy" | "critical"
7
7
 
8
+ export type ContextHandoffRecommendedAction = "continue" | "save_handoff" | "fill_required_context"
9
+
10
+ export interface ContextHandoffValidationCheck {
11
+ name: string
12
+ passed: boolean
13
+ reason: string
14
+ }
15
+
16
+ export interface ContextHandoffValidationProbes {
17
+ recall: boolean
18
+ continuation: boolean
19
+ artifact: boolean
20
+ decision: boolean
21
+ }
22
+
8
23
  export interface ContextHandoffInput {
9
- operation: "save" | "load" | "latest" | "status"
24
+ operation: "save" | "load" | "latest" | "status" | "validate"
10
25
  repoRoot: string
11
26
  currentStage?: string
12
27
  nextStage?: string
@@ -63,6 +78,13 @@ export interface ContextHandoffResult {
63
78
  recentlyAccessedFiles?: string[]
64
79
  compressionRisk?: string[]
65
80
  updatedAt?: string
81
+ // Validation fields
82
+ ok?: boolean
83
+ probes?: ContextHandoffValidationProbes
84
+ checks?: ContextHandoffValidationCheck[]
85
+ missing?: string[]
86
+ warnings?: string[]
87
+ recommendedAction?: ContextHandoffRecommendedAction
66
88
  }
67
89
 
68
90
  function ceDir(repoRoot: string): string {
@@ -268,6 +290,8 @@ export function createContextHandoffTool() {
268
290
  return latest(input)
269
291
  case "status":
270
292
  return status(input)
293
+ case "validate":
294
+ return validate(input)
271
295
  default:
272
296
  throw new Error(`Unknown operation: ${input.operation}`)
273
297
  }
@@ -434,6 +458,272 @@ async function latest(input: ContextHandoffInput): Promise<ContextHandoffResult>
434
458
  }
435
459
  }
436
460
 
461
+ const PLACEHOLDER_VALUES = new Set(["n/a", "na", "not run", "none", "", "-"])
462
+ const PLACEHOLDER_PREFIXES = ["- n/a", "- na", "- not run", "- none", "- "]
463
+
464
+ function isPlaceholder(text: string): boolean {
465
+ const trimmed = text.trim().toLowerCase()
466
+ if (PLACEHOLDER_VALUES.has(trimmed)) return true
467
+ for (const prefix of PLACEHOLDER_PREFIXES) {
468
+ if (trimmed === prefix) return true
469
+ }
470
+ return false
471
+ }
472
+
473
+ function isMeaningfulText(value?: string): boolean {
474
+ return Boolean(value && !isPlaceholder(value))
475
+ }
476
+
477
+ function toPublicHandoffPath(repoRoot: string, filePath: string): string {
478
+ return path.isAbsolute(filePath) ? toRepoRelative(repoRoot, filePath) : filePath
479
+ }
480
+
481
+ function extractSection(markdown: string, heading: string): string {
482
+ const lines = markdown.split("\n")
483
+ const sectionLines: string[] = []
484
+ let inSection = false
485
+
486
+ for (const line of lines) {
487
+ if (line.startsWith("## ")) {
488
+ if (inSection) break
489
+ if (line.slice(3).trim() === heading) {
490
+ inSection = true
491
+ }
492
+ continue
493
+ }
494
+ if (inSection) {
495
+ sectionLines.push(line)
496
+ }
497
+ }
498
+
499
+ return sectionLines.join("\n").trim()
500
+ }
501
+
502
+ function sectionHasMeaningfulContent(markdown: string, heading: string): boolean {
503
+ const section = extractSection(markdown, heading)
504
+ if (!section) return false
505
+
506
+ const lines = section.split("\n")
507
+ for (const line of lines) {
508
+ const trimmed = line.trim()
509
+ if (!trimmed) continue
510
+ if (!isPlaceholder(trimmed)) return true
511
+ }
512
+ return false
513
+ }
514
+
515
+ function hasMeaningfulArray(arr: string[]): boolean {
516
+ return arr.some(item => isMeaningfulText(item))
517
+ }
518
+
519
+ function hasMeaningfulRecord(rec: Record<string, string | undefined>): boolean {
520
+ return Object.values(rec).some(isMeaningfulText)
521
+ }
522
+
523
+ function isMeaningfulStage(value?: string): boolean {
524
+ return Boolean(value && value.trim().length > 0 && value !== "unknown" && !isPlaceholder(value))
525
+ }
526
+
527
+ async function validate(input: ContextHandoffInput): Promise<ContextHandoffResult> {
528
+ const checks: ContextHandoffValidationCheck[] = []
529
+ const missing: string[] = []
530
+ const warnings: string[] = []
531
+
532
+ // Read state
533
+ const state = await readState(input.repoRoot)
534
+ const hasState = state !== null
535
+
536
+ // Read markdown
537
+ let markdown = ""
538
+ let hasMarkdown = false
539
+ let handoffFile = ""
540
+
541
+ if (input.handoffPath) {
542
+ const absolutePath = resolveRepoPath(input.repoRoot, input.handoffPath)
543
+ if (existsSync(absolutePath)) {
544
+ markdown = await readFile(absolutePath, "utf8")
545
+ hasMarkdown = markdown.trim().length > 0
546
+ handoffFile = toPublicHandoffPath(input.repoRoot, input.handoffPath)
547
+ }
548
+ }
549
+
550
+ if (!hasMarkdown && state?.latestHandoffPath) {
551
+ const absolutePath = resolveRepoPath(input.repoRoot, state.latestHandoffPath)
552
+ if (existsSync(absolutePath)) {
553
+ markdown = await readFile(absolutePath, "utf8")
554
+ hasMarkdown = markdown.trim().length > 0
555
+ handoffFile = toPublicHandoffPath(input.repoRoot, state.latestHandoffPath)
556
+ }
557
+ }
558
+
559
+ if (!hasMarkdown) {
560
+ const latestPath = latestHandoffPath(input.repoRoot)
561
+ if (existsSync(latestPath)) {
562
+ markdown = await readFile(latestPath, "utf8")
563
+ hasMarkdown = markdown.trim().length > 0
564
+ handoffFile = toRepoRelative(input.repoRoot, latestPath)
565
+ }
566
+ }
567
+
568
+ const found = hasState || hasMarkdown
569
+
570
+ checks.push({
571
+ name: "state_exists",
572
+ passed: hasState,
573
+ reason: hasState ? "Found context-state.json" : "No context-state.json found",
574
+ })
575
+
576
+ checks.push({
577
+ name: "handoff_exists",
578
+ passed: hasMarkdown,
579
+ reason: hasMarkdown ? "Found handoff markdown" : "No handoff markdown found",
580
+ })
581
+
582
+ // --- Recall probe ---
583
+ const hasCurrentTruth = state ? hasMeaningfulArray(state.currentTruth) : false
584
+ const hasCurrentStage = state ? isMeaningfulStage(state.currentStage) : false
585
+ const hasCurrentTaskSection = hasMarkdown && sectionHasMeaningfulContent(markdown, "Current Task")
586
+
587
+ const recallPass = hasCurrentTruth || (hasCurrentStage && (hasMarkdown || (state?.nextStage !== undefined))) || hasCurrentTaskSection
588
+
589
+ checks.push({
590
+ name: "recall_current_truth",
591
+ passed: hasCurrentTruth,
592
+ reason: hasCurrentTruth ? `Found ${state!.currentTruth.length} current truth entries` : "No current truth entries",
593
+ })
594
+
595
+ checks.push({
596
+ name: "recall_current_task",
597
+ passed: hasCurrentTaskSection,
598
+ reason: hasCurrentTaskSection ? "Current Task section has meaningful content" : "Current Task section is missing or placeholder",
599
+ })
600
+
601
+ checks.push({
602
+ name: "recall_current_stage",
603
+ passed: hasCurrentStage,
604
+ reason: hasCurrentStage ? `Current stage: ${state!.currentStage}` : "No meaningful current stage",
605
+ })
606
+
607
+ if (!recallPass) {
608
+ missing.push("recall: current task or goal evidence")
609
+ }
610
+
611
+ // --- Continuation probe ---
612
+ // Tightened: only actionable next-step evidence passes
613
+ const hasNextStage = state ? isMeaningfulStage(state.nextStage) : false
614
+ const hasNextMinimalStep = hasMarkdown && sectionHasMeaningfulContent(markdown, "Next Minimal Step")
615
+
616
+ const continuationPass = hasNextMinimalStep || hasNextStage
617
+
618
+ checks.push({
619
+ name: "continuation_next_stage",
620
+ passed: hasNextStage,
621
+ reason: hasNextStage ? `Next stage: ${state!.nextStage}` : "No meaningful next stage",
622
+ })
623
+
624
+ checks.push({
625
+ name: "continuation_next_minimal_step",
626
+ passed: hasNextMinimalStep,
627
+ reason: hasNextMinimalStep ? "Next Minimal Step has meaningful content" : "Next Minimal Step is missing or placeholder",
628
+ })
629
+
630
+ // Diagnostic checks (do not affect continuation pass)
631
+ const hasBlocker = state ? Boolean(state.blocker && !isPlaceholder(state.blocker)) : false
632
+ const hasVerification = state ? Boolean(state.verification && !isPlaceholder(state.verification)) : false
633
+ const hasVerificationSection = hasMarkdown && sectionHasMeaningfulContent(markdown, "Verification")
634
+
635
+ checks.push({
636
+ name: "continuation_blocker",
637
+ passed: hasBlocker,
638
+ reason: hasBlocker ? `Blocker: ${state!.blocker}` : "No blocker information",
639
+ })
640
+
641
+ checks.push({
642
+ name: "continuation_verification",
643
+ passed: hasVerification || hasVerificationSection,
644
+ reason: (hasVerification || hasVerificationSection) ? "Verification evidence present" : "No verification information",
645
+ })
646
+
647
+ if (!continuationPass) {
648
+ missing.push("continuation: next minimal step or next stage evidence")
649
+ }
650
+
651
+ // --- Artifact probe ---
652
+ const hasActiveFiles = state ? hasMeaningfulArray(state.activeFiles) : false
653
+ const hasRecentlyAccessed = state ? hasMeaningfulArray(state.recentlyAccessedFiles) : false
654
+ const hasArtifacts = state ? hasMeaningfulRecord(state.artifacts) : false
655
+ const hasActiveFilesSection = hasMarkdown && sectionHasMeaningfulContent(markdown, "Active Files")
656
+ const hasRecentlyAccessedSection = hasMarkdown && sectionHasMeaningfulContent(markdown, "Recently Accessed Files")
657
+ const hasArtifactsSection = hasMarkdown && sectionHasMeaningfulContent(markdown, "Artifacts")
658
+
659
+ const artifactPass = hasActiveFiles || hasRecentlyAccessed || hasArtifacts
660
+ || hasActiveFilesSection || hasRecentlyAccessedSection || hasArtifactsSection
661
+
662
+ checks.push({
663
+ name: "artifact_active_files",
664
+ passed: hasActiveFiles || hasActiveFilesSection,
665
+ reason: (hasActiveFiles || hasActiveFilesSection) ? "Active files present" : "No active files",
666
+ })
667
+
668
+ if (!artifactPass) {
669
+ warnings.push("artifact: active files or artifacts are missing")
670
+ }
671
+
672
+ // --- Decision probe ---
673
+ const hasOpenDecisions = state ? hasMeaningfulArray(state.openDecisions) : false
674
+ const hasInvalidatedAssumptions = state ? hasMeaningfulArray(state.invalidatedAssumptions) : false
675
+ const hasOpenDecisionsSection = hasMarkdown && sectionHasMeaningfulContent(markdown, "Open Decisions")
676
+ const hasInvalidatedSection = hasMarkdown && sectionHasMeaningfulContent(markdown, "Invalidated Assumptions")
677
+ const hasCurrentTruthSection = hasMarkdown && sectionHasMeaningfulContent(markdown, "Current Truth")
678
+
679
+ const decisionPass = hasOpenDecisions || hasInvalidatedAssumptions || hasCurrentTruth
680
+ || hasOpenDecisionsSection || hasInvalidatedSection || hasCurrentTruthSection
681
+
682
+ checks.push({
683
+ name: "decision_open_decisions",
684
+ passed: hasOpenDecisions || hasOpenDecisionsSection,
685
+ reason: (hasOpenDecisions || hasOpenDecisionsSection) ? "Open decisions present" : "No open decisions",
686
+ })
687
+
688
+ if (!decisionPass) {
689
+ warnings.push("decision: decisions or invalidated assumptions are missing")
690
+ }
691
+
692
+ // --- ok derivation ---
693
+ const ok = recallPass && continuationPass
694
+
695
+ // --- recommended action ---
696
+ let recommendedAction: ContextHandoffRecommendedAction
697
+ if (!found) {
698
+ recommendedAction = "save_handoff"
699
+ } else if (!ok) {
700
+ recommendedAction = "fill_required_context"
701
+ } else {
702
+ recommendedAction = "continue"
703
+ }
704
+
705
+ return {
706
+ operation: "validate",
707
+ found,
708
+ ok,
709
+ path: handoffFile || undefined,
710
+ probes: {
711
+ recall: recallPass,
712
+ continuation: continuationPass,
713
+ artifact: artifactPass,
714
+ decision: decisionPass,
715
+ },
716
+ checks,
717
+ missing,
718
+ warnings,
719
+ recommendedAction,
720
+ currentStage: state?.currentStage,
721
+ nextStage: state?.nextStage,
722
+ contextHealth: state?.contextHealth,
723
+ updatedAt: state?.updatedAt,
724
+ }
725
+ }
726
+
437
727
  async function status(input: ContextHandoffInput): Promise<ContextHandoffResult> {
438
728
  const state = await readState(input.repoRoot)
439
729
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leing2021/super-pi",
3
- "version": "0.23.2",
3
+ "version": "0.23.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Pi-native Compound Engineering package for iterative development workflows",