@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, "
|
|
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 =
|
|
409
|
+
const currentLevel = pi.getThinkingLevel()
|
|
401
410
|
if (currentLevel !== normalized) {
|
|
402
|
-
|
|
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),
|
|
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)
|
|
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,
|
|
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
|
@@ -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
|
|