@leing2021/super-pi 0.23.1 → 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)" })),
@@ -326,6 +327,11 @@ const contextHandoffParams = Type.Object({
326
327
  artifacts: Type.Optional(Type.Record(Type.String(), Type.Optional(Type.String()), { description: "Artifact paths (requirements, plan, checkpoint, proof)" })),
327
328
  handoffMarkdown: Type.Optional(Type.String({ description: "Custom handoff markdown content" })),
328
329
  handoffPath: Type.Optional(Type.String({ description: "Specific handoff file path to load" })),
330
+ currentTruth: Type.Optional(Type.Array(Type.String(), { description: "Known true statements validated during session" })),
331
+ invalidatedAssumptions: Type.Optional(Type.Array(Type.String(), { description: "Assumptions proven wrong during session" })),
332
+ openDecisions: Type.Optional(Type.Array(Type.String(), { description: "Pending decisions that affect next steps" })),
333
+ recentlyAccessedFiles: Type.Optional(Type.Array(Type.String(), { description: "Files recently read or edited (defaults to activeFiles)" })),
334
+ compressionRisk: Type.Optional(Type.Array(Type.String(), { description: "Context compression risks to watch for" })),
329
335
  })
330
336
 
331
337
  const patternExtractorParams = Type.Object({
@@ -388,18 +394,21 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
388
394
  if (thinkingStrategy) {
389
395
  const targetThinking = thinkingStrategy[stageKey] ?? thinkingStrategy.default
390
396
  if (targetThinking) {
391
- const levelMap: Record<string, "low" | "medium" | "high"> = {
397
+ const levelMap: Record<string, ReturnType<ExtensionAPI["getThinkingLevel"]>> = {
398
+ off: "off",
399
+ minimal: "minimal",
392
400
  low: "low",
393
401
  medium: "medium",
394
402
  high: "high",
403
+ xhigh: "xhigh",
395
404
  "0": "low",
396
405
  "1": "medium",
397
406
  "2": "high",
398
407
  }
399
408
  const normalized = levelMap[targetThinking.toLowerCase()] ?? "medium"
400
- const currentLevel = ctx.getThinkingLevel?.() ?? "medium"
409
+ const currentLevel = pi.getThinkingLevel()
401
410
  if (currentLevel !== normalized) {
402
- ctx.setThinkingLevel?.(normalized)
411
+ pi.setThinkingLevel(normalized)
403
412
  if (ctx.hasUI) {
404
413
  ctx.ui.notify(`Switched thinking level for ${stageKey}: ${normalized}`, "info")
405
414
  }
@@ -743,7 +752,7 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
743
752
  pi.registerTool({
744
753
  name: contextHandoff.name,
745
754
  label: "Context Handoff",
746
- 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).",
747
756
  parameters: contextHandoffParams,
748
757
  async execute(_toolCallId, params) {
749
758
  const result = await contextHandoff.execute({
@@ -758,6 +767,11 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
758
767
  artifacts: params.artifacts as Record<string, string | undefined> | undefined,
759
768
  handoffMarkdown: params.handoffMarkdown,
760
769
  handoffPath: params.handoffPath,
770
+ currentTruth: params.currentTruth,
771
+ invalidatedAssumptions: params.invalidatedAssumptions,
772
+ openDecisions: params.openDecisions,
773
+ recentlyAccessedFiles: params.recentlyAccessedFiles,
774
+ compressionRisk: params.compressionRisk,
761
775
  })
762
776
 
763
777
  return {
@@ -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
@@ -17,6 +32,11 @@ export interface ContextHandoffInput {
17
32
  artifacts?: Record<string, string | undefined>
18
33
  handoffMarkdown?: string
19
34
  handoffPath?: string
35
+ currentTruth?: string[]
36
+ invalidatedAssumptions?: string[]
37
+ openDecisions?: string[]
38
+ recentlyAccessedFiles?: string[]
39
+ compressionRisk?: string[]
20
40
  }
21
41
 
22
42
  export interface ContextStateEntry {
@@ -29,6 +49,11 @@ export interface ContextStateEntry {
29
49
  blocker?: string
30
50
  verification?: string
31
51
  artifacts: Record<string, string | undefined>
52
+ currentTruth: string[]
53
+ invalidatedAssumptions: string[]
54
+ openDecisions: string[]
55
+ recentlyAccessedFiles: string[]
56
+ compressionRisk: string[]
32
57
  recommendNewSession: boolean
33
58
  updatedAt: string
34
59
  }
@@ -47,7 +72,19 @@ export interface ContextHandoffResult {
47
72
  artifacts?: Record<string, string | undefined>
48
73
  recommendNewSession?: boolean
49
74
  handoffMarkdown?: string
75
+ currentTruth?: string[]
76
+ invalidatedAssumptions?: string[]
77
+ openDecisions?: string[]
78
+ recentlyAccessedFiles?: string[]
79
+ compressionRisk?: string[]
50
80
  updatedAt?: string
81
+ // Validation fields
82
+ ok?: boolean
83
+ probes?: ContextHandoffValidationProbes
84
+ checks?: ContextHandoffValidationCheck[]
85
+ missing?: string[]
86
+ warnings?: string[]
87
+ recommendedAction?: ContextHandoffRecommendedAction
51
88
  }
52
89
 
53
90
  function ceDir(repoRoot: string): string {
@@ -112,6 +149,11 @@ function buildDefaultHandoffMarkdown(input: {
112
149
  artifacts: Record<string, string | undefined>
113
150
  blocker?: string
114
151
  verification?: string
152
+ currentTruth: string[]
153
+ invalidatedAssumptions: string[]
154
+ openDecisions: string[]
155
+ recentlyAccessedFiles: string[]
156
+ compressionRisk: string[]
115
157
  }): string {
116
158
  const currentTask = input.nextStage
117
159
  ? `Continue from ${input.currentStage} to ${input.nextStage}.`
@@ -137,12 +179,24 @@ function buildDefaultHandoffMarkdown(input: {
137
179
  "## Hot Context",
138
180
  hotContext,
139
181
  "",
182
+ "## Current Truth",
183
+ formatBullets(input.currentTruth),
184
+ "",
140
185
  "## Verified Facts",
141
186
  verifiedFacts,
142
187
  "",
188
+ "## Invalidated Assumptions",
189
+ formatBullets(input.invalidatedAssumptions),
190
+ "",
191
+ "## Open Decisions",
192
+ formatBullets(input.openDecisions),
193
+ "",
143
194
  "## Active Files",
144
195
  activeFiles,
145
196
  "",
197
+ "## Recently Accessed Files",
198
+ formatBullets(input.recentlyAccessedFiles),
199
+ "",
146
200
  "## Artifacts",
147
201
  artifacts,
148
202
  "",
@@ -152,6 +206,9 @@ function buildDefaultHandoffMarkdown(input: {
152
206
  "## Verification",
153
207
  `- ${verification}`,
154
208
  "",
209
+ "## Compression Risk",
210
+ formatBullets(input.compressionRisk),
211
+ "",
155
212
  "## Do Not Repeat",
156
213
  "- Do not reload full history unless the handoff lacks required evidence.",
157
214
  "",
@@ -161,13 +218,54 @@ function buildDefaultHandoffMarkdown(input: {
161
218
  ].join("\n")
162
219
  }
163
220
 
221
+ function toStringArray(value: unknown): string[] {
222
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []
223
+ }
224
+
225
+ function normalizeStateEntry(raw: unknown): ContextStateEntry | null {
226
+ if (!raw || typeof raw !== "object") return null
227
+
228
+ const state = raw as Record<string, unknown>
229
+ const activeFiles = toStringArray(state.activeFiles)
230
+
231
+ return {
232
+ currentStage: typeof state.currentStage === "string" ? state.currentStage : "unknown",
233
+ nextStage: typeof state.nextStage === "string" ? state.nextStage : undefined,
234
+ contextHealth: isContextHealth(state.contextHealth) ? state.contextHealth : "watch",
235
+ latestHandoffPath: typeof state.latestHandoffPath === "string" ? state.latestHandoffPath : undefined,
236
+ latestDatedHandoffPath: typeof state.latestDatedHandoffPath === "string" ? state.latestDatedHandoffPath : undefined,
237
+ activeFiles,
238
+ blocker: typeof state.blocker === "string" ? state.blocker : undefined,
239
+ verification: typeof state.verification === "string" ? state.verification : undefined,
240
+ artifacts: isStringRecord(state.artifacts) ? state.artifacts : {},
241
+ currentTruth: toStringArray(state.currentTruth),
242
+ invalidatedAssumptions: toStringArray(state.invalidatedAssumptions),
243
+ openDecisions: toStringArray(state.openDecisions),
244
+ recentlyAccessedFiles: toStringArray(state.recentlyAccessedFiles).length > 0
245
+ ? toStringArray(state.recentlyAccessedFiles)
246
+ : activeFiles.slice(0, 5),
247
+ compressionRisk: toStringArray(state.compressionRisk),
248
+ recommendNewSession: typeof state.recommendNewSession === "boolean" ? state.recommendNewSession : false,
249
+ updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : new Date(0).toISOString(),
250
+ }
251
+ }
252
+
253
+ function isContextHealth(value: unknown): value is ContextHealth {
254
+ return value === "good" || value === "watch" || value === "heavy" || value === "critical"
255
+ }
256
+
257
+ function isStringRecord(value: unknown): value is Record<string, string | undefined> {
258
+ if (!value || typeof value !== "object") return false
259
+ return Object.values(value).every(item => item === undefined || typeof item === "string")
260
+ }
261
+
164
262
  async function readState(repoRoot: string): Promise<ContextStateEntry | null> {
165
263
  const filePath = stateFilePath(repoRoot)
166
264
  if (!existsSync(filePath)) return null
167
265
 
168
266
  try {
169
267
  const content = await readFile(filePath, "utf8")
170
- return JSON.parse(content) as ContextStateEntry
268
+ return normalizeStateEntry(JSON.parse(content))
171
269
  } catch {
172
270
  return null
173
271
  }
@@ -192,6 +290,8 @@ export function createContextHandoffTool() {
192
290
  return latest(input)
193
291
  case "status":
194
292
  return status(input)
293
+ case "validate":
294
+ return validate(input)
195
295
  default:
196
296
  throw new Error(`Unknown operation: ${input.operation}`)
197
297
  }
@@ -207,6 +307,14 @@ async function save(input: ContextHandoffInput): Promise<ContextHandoffResult> {
207
307
  const blocker = input.blocker
208
308
  const verification = input.verification
209
309
  const artifacts = input.artifacts ?? {}
310
+ const currentTruth = input.currentTruth ?? []
311
+ const invalidatedAssumptions = input.invalidatedAssumptions ?? []
312
+ const openDecisions = input.openDecisions ?? []
313
+ const recentlyAccessedFiles = input.recentlyAccessedFiles?.length
314
+ ? input.recentlyAccessedFiles
315
+ : activeFiles.slice(0, 5)
316
+ const compressionRisk = input.compressionRisk ?? []
317
+
210
318
  const handoffMarkdown = input.handoffMarkdown?.trim().length
211
319
  ? input.handoffMarkdown
212
320
  : buildDefaultHandoffMarkdown({
@@ -216,6 +324,11 @@ async function save(input: ContextHandoffInput): Promise<ContextHandoffResult> {
216
324
  artifacts,
217
325
  blocker,
218
326
  verification,
327
+ currentTruth,
328
+ invalidatedAssumptions,
329
+ openDecisions,
330
+ recentlyAccessedFiles,
331
+ compressionRisk,
219
332
  })
220
333
 
221
334
  const recommendNewSession = computeRecommendNewSession(currentStage, nextStage, contextHealth)
@@ -238,6 +351,11 @@ async function save(input: ContextHandoffInput): Promise<ContextHandoffResult> {
238
351
  blocker,
239
352
  verification,
240
353
  artifacts,
354
+ currentTruth,
355
+ invalidatedAssumptions,
356
+ openDecisions,
357
+ recentlyAccessedFiles,
358
+ compressionRisk,
241
359
  recommendNewSession,
242
360
  updatedAt: new Date().toISOString(),
243
361
  }
@@ -256,6 +374,11 @@ async function save(input: ContextHandoffInput): Promise<ContextHandoffResult> {
256
374
  blocker,
257
375
  verification,
258
376
  artifacts,
377
+ currentTruth,
378
+ invalidatedAssumptions,
379
+ openDecisions,
380
+ recentlyAccessedFiles,
381
+ compressionRisk,
259
382
  recommendNewSession,
260
383
  updatedAt: state.updatedAt,
261
384
  }
@@ -291,6 +414,11 @@ async function load(input: ContextHandoffInput): Promise<ContextHandoffResult> {
291
414
  blocker: state.blocker,
292
415
  verification: state.verification,
293
416
  artifacts: state.artifacts,
417
+ currentTruth: state.currentTruth,
418
+ invalidatedAssumptions: state.invalidatedAssumptions,
419
+ openDecisions: state.openDecisions,
420
+ recentlyAccessedFiles: state.recentlyAccessedFiles,
421
+ compressionRisk: state.compressionRisk,
294
422
  recommendNewSession: state.recommendNewSession,
295
423
  handoffMarkdown: markdown,
296
424
  updatedAt: state.updatedAt,
@@ -320,11 +448,282 @@ async function latest(input: ContextHandoffInput): Promise<ContextHandoffResult>
320
448
  blocker: state.blocker,
321
449
  verification: state.verification,
322
450
  artifacts: state.artifacts,
451
+ currentTruth: state.currentTruth,
452
+ invalidatedAssumptions: state.invalidatedAssumptions,
453
+ openDecisions: state.openDecisions,
454
+ recentlyAccessedFiles: state.recentlyAccessedFiles,
455
+ compressionRisk: state.compressionRisk,
323
456
  recommendNewSession: state.recommendNewSession,
324
457
  updatedAt: state.updatedAt,
325
458
  }
326
459
  }
327
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
+
328
727
  async function status(input: ContextHandoffInput): Promise<ContextHandoffResult> {
329
728
  const state = await readState(input.repoRoot)
330
729
 
@@ -351,6 +750,11 @@ async function status(input: ContextHandoffInput): Promise<ContextHandoffResult>
351
750
  blocker: state.blocker,
352
751
  verification: state.verification,
353
752
  artifacts: state.artifacts,
753
+ currentTruth: state.currentTruth,
754
+ invalidatedAssumptions: state.invalidatedAssumptions,
755
+ openDecisions: state.openDecisions,
756
+ recentlyAccessedFiles: state.recentlyAccessedFiles,
757
+ compressionRisk: state.compressionRisk,
354
758
  recommendNewSession: state.recommendNewSession,
355
759
  updatedAt: state.updatedAt,
356
760
  }
@@ -1,4 +1,4 @@
1
- import { readdirSync, statSync, existsSync } from "node:fs"
1
+ import { readdirSync, existsSync, readFileSync } from "node:fs"
2
2
  import path from "node:path"
3
3
 
4
4
  export interface WorkflowCategoryState {
@@ -6,6 +6,25 @@ export interface WorkflowCategoryState {
6
6
  latest: string | null
7
7
  }
8
8
 
9
+ export interface WorkflowContextState {
10
+ found: boolean
11
+ currentStage?: string
12
+ nextStage?: string
13
+ contextHealth?: string
14
+ latestHandoffPath?: string
15
+ latestDatedHandoffPath?: string
16
+ activeFiles: string[]
17
+ recentlyAccessedFiles: string[]
18
+ blocker?: string
19
+ verification?: string
20
+ currentTruth: string[]
21
+ invalidatedAssumptions: string[]
22
+ openDecisions: string[]
23
+ compressionRisk: string[]
24
+ recommendNewSession?: boolean
25
+ updatedAt?: string
26
+ }
27
+
9
28
  export interface WorkflowStateInput {
10
29
  repoRoot: string
11
30
  }
@@ -15,6 +34,7 @@ export interface WorkflowStateResult {
15
34
  plans: WorkflowCategoryState
16
35
  solutions: WorkflowCategoryState
17
36
  runs: WorkflowCategoryState
37
+ context: WorkflowContextState
18
38
  }
19
39
 
20
40
  function emptyCategory(): WorkflowCategoryState {
@@ -63,6 +83,52 @@ function collectFiles(dirPath: string): string[] {
63
83
  return results
64
84
  }
65
85
 
86
+ function emptyContext(): WorkflowContextState {
87
+ return {
88
+ found: false,
89
+ activeFiles: [],
90
+ recentlyAccessedFiles: [],
91
+ currentTruth: [],
92
+ invalidatedAssumptions: [],
93
+ openDecisions: [],
94
+ compressionRisk: [],
95
+ }
96
+ }
97
+
98
+ function toStringArray(value: unknown): string[] {
99
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []
100
+ }
101
+
102
+ function readContextState(repoRoot: string): WorkflowContextState {
103
+ const statePath = path.join(repoRoot, ".context", "compound-engineering", "context-state.json")
104
+ if (!existsSync(statePath)) return emptyContext()
105
+
106
+ try {
107
+ const raw = readFileSync(statePath, "utf8")
108
+ const state = JSON.parse(raw) as Record<string, unknown>
109
+ return {
110
+ found: true,
111
+ currentStage: typeof state.currentStage === "string" ? state.currentStage : undefined,
112
+ nextStage: typeof state.nextStage === "string" ? state.nextStage : undefined,
113
+ contextHealth: typeof state.contextHealth === "string" ? state.contextHealth : undefined,
114
+ latestHandoffPath: typeof state.latestHandoffPath === "string" ? state.latestHandoffPath : undefined,
115
+ latestDatedHandoffPath: typeof state.latestDatedHandoffPath === "string" ? state.latestDatedHandoffPath : undefined,
116
+ activeFiles: toStringArray(state.activeFiles),
117
+ recentlyAccessedFiles: toStringArray(state.recentlyAccessedFiles),
118
+ blocker: typeof state.blocker === "string" ? state.blocker : undefined,
119
+ verification: typeof state.verification === "string" ? state.verification : undefined,
120
+ currentTruth: toStringArray(state.currentTruth),
121
+ invalidatedAssumptions: toStringArray(state.invalidatedAssumptions),
122
+ openDecisions: toStringArray(state.openDecisions),
123
+ compressionRisk: toStringArray(state.compressionRisk),
124
+ recommendNewSession: typeof state.recommendNewSession === "boolean" ? state.recommendNewSession : undefined,
125
+ updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : undefined,
126
+ }
127
+ } catch {
128
+ return emptyContext()
129
+ }
130
+ }
131
+
66
132
  export function createWorkflowStateTool() {
67
133
  return {
68
134
  name: "workflow_state",
@@ -74,6 +140,7 @@ export function createWorkflowStateTool() {
74
140
  plans: scanDir(path.join(repoRoot, "docs", "plans")),
75
141
  solutions: scanDir(path.join(repoRoot, "docs", "solutions")),
76
142
  runs: scanDir(path.join(repoRoot, ".context", "compound-engineering")),
143
+ context: readContextState(repoRoot),
77
144
  }
78
145
  },
79
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leing2021/super-pi",
3
- "version": "0.23.1",
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",
@@ -55,12 +55,24 @@ When a stage produces or updates handoff-lite, use this evidence-first structure
55
55
  ## Hot Context
56
56
  - 1-5 must-know facts for the next step
57
57
 
58
+ ## Current Truth
59
+ - validated truths that must survive compression
60
+
58
61
  ## Verified Facts
59
62
  - already validated facts (do not re-prove)
60
63
 
64
+ ## Invalidated Assumptions
65
+ - assumptions proven wrong this session
66
+
67
+ ## Open Decisions
68
+ - pending decisions that affect next steps
69
+
61
70
  ## Active Files
62
71
  - 1-5 file paths only
63
72
 
73
+ ## Recently Accessed Files
74
+ - files recently read or edited
75
+
64
76
  ## Artifacts
65
77
  - requirements: <path or N/A>
66
78
  - plan: <path or N/A>
@@ -73,6 +85,9 @@ When a stage produces or updates handoff-lite, use this evidence-first structure
73
85
  ## Verification
74
86
  - <latest command + result or Not run>
75
87
 
88
+ ## Compression Risk
89
+ - context compression risks to watch for
90
+
76
91
  ## Do Not Repeat
77
92
  - what should not be re-read/re-run unless needed
78
93