@leing2021/super-pi 0.19.2 → 0.19.4

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 CHANGED
@@ -35,7 +35,7 @@ Super Pi's answers:
35
35
 
36
36
  Each step has a dedicated skill + tool pair. Not just prompts — structured toolchains.
37
37
 
38
- ### New: Stage model routing + optional auto-continue
38
+ ### New: Stage model routing
39
39
 
40
40
  Configure once in `.pi/settings.json`:
41
41
 
@@ -48,24 +48,22 @@ Configure once in `.pi/settings.json`:
48
48
  "04-review": "claude-sonnet-4-20250514",
49
49
  "05-learn": "claude-haiku-4-20250414",
50
50
  "default": "claude-sonnet-4-20250514"
51
- },
52
- "pipeline": {
53
- "autoContinue": false
54
51
  }
55
52
  }
56
53
  ```
57
54
 
58
55
  How it works:
59
- - Each stage picks `modelStrategy[stage]`, or `modelStrategy.default` as fallback.
56
+ - Model switching is handled automatically by the ce-core extension `input` hook — no manual `/model` needed.
57
+ - When you type `/skill:01-brainstorm` through `/skill:05-learn`, the extension reads `modelStrategy[stage]` (or `modelStrategy.default`) and switches before the skill runs.
58
+ - Supported formats: full reference (`"anthropic/claude-opus-4-1"`) or bare model id (`"claude-opus-4-1"`, reuses current provider).
60
59
  - Every stage prints a `📊 Pipeline Status` block with `Current / Output / Next`.
61
- - If `pipeline.autoContinue=true`, super-pi can trigger the next stage automatically.
62
- - Auto-continue is gate-aware: it will NOT skip required approvals (brainstorm approval, plan/review A/B/C choices).
60
+ - A `Switched model for <stage>: <provider>/<model>` notification appears when the model changes.
63
61
 
64
62
  Quick example:
65
- 1. Run `/skill:01-brainstorm`
63
+ 1. Run `/skill:01-brainstorm` — model auto-switches to the configured brainstorm model
66
64
  2. Approve the design
67
- 3. If `autoContinue=true`, it moves to `/skill:02-plan` automatically
68
- 4. If `autoContinue=false`, it stops after status output and waits for your command
65
+ 3. Run `/skill:02-plan` — model auto-switches to the configured plan model
66
+ 4. Continue through each stage model switches automatically at each step
69
67
 
70
68
  ---
71
69
 
@@ -180,7 +178,7 @@ Single `npm install` output filtered once pays for the entire overhead. Full eva
180
178
 
181
179
  ## Code Scale
182
180
 
183
- ~2800 lines of TypeScript implementing 16 tools, 22 Markdown reference files + 78 rule files driving 10 skills, 170 tests covering all tool logic.
181
+ ~2800 lines of TypeScript implementing 16 tools, 22 Markdown reference files + 78 rule files driving 10 skills, 175 tests covering all tool logic.
184
182
 
185
183
  Not a heavy framework. Each tool has a single responsibility, each skill works independently, and together they form a complete workflow.
186
184
 
@@ -383,6 +381,20 @@ Not a fork. Not a wrapper. Methodologies extracted and rebuilt with Pi's native
383
381
 
384
382
  ## Changelog
385
383
 
384
+ ### 0.19.4 — Read output filter markdown truncation fix
385
+ - Fixed `read-output-filter` over-truncating markdown files: raised markdown threshold from 2KB → 8KB.
386
+ - Improved `filterMarkdown()` to fully preserve list items (`-`, `*`, numbered) and keep first 3 lines of paragraphs (was 1).
387
+ - Filter notice now includes actual file path in actionable guidance (`bash cat <path>`).
388
+ - Added 5 new tests covering list preservation, markdown threshold gate, and path-in-notice.
389
+ - 175 tests passing.
390
+
391
+ ### 0.19.3 — Terminate fix + runtime model routing + autoContinue removal
392
+ - Fixed 6 ce-core tools (`brainstorm_dialog`, `workflow_state`, `review_router`, `session_checkpoint`, `session_history`, `pattern_extractor`) incorrectly returning `terminate: true`, which caused agent turns to end prematurely (brainstorm questions not shown, "type continue to proceed" interruptions).
393
+ - Implemented runtime stage model routing via ce-core extension `input` hook: reads `.pi/settings.json` `modelStrategy`, auto-switches model before skill execution. Supports full reference (`anthropic/claude-opus-4-1`) and bare model id (`claude-opus-4-1`).
394
+ - Removed `pipeline.autoContinue` configuration (never had runtime implementation; Pi lacks `skill_end` event for auto-continue).
395
+ - Updated `skills/references/pipeline-config.md`, `README.md`, `README_CN.md` to reflect runtime model routing behavior.
396
+ - Added 4 new tests covering terminate regression, input hook model routing, and bare model id parsing.
397
+
386
398
  ### 0.19.2 — Evidence-first handoff-lite + docs tracking rule
387
399
  - Added `context_handoff` with evidence-first default handoff-lite generation when markdown is omitted.
388
400
  - Standardized the shared handoff-lite template across 01-05 workflow handoffs via `skills/references/pipeline-config.md`.
@@ -391,8 +403,7 @@ Not a fork. Not a wrapper. Methodologies extracted and rebuilt with Pi's native
391
403
 
392
404
  ### 0.19.1 — Pipeline config + typecheck baseline fix
393
405
  - Added shared pipeline config (`skills/references/pipeline-config.md`) for stage model routing via `.pi/settings.json`.
394
- - Added gated auto-continue rules (`pipeline.autoContinue`) so automation does not skip required approval/review-choice steps.
395
- - Added explicit README usage examples for `modelStrategy` and `pipeline.autoContinue`.
406
+ - Added runtime stage model routing via ce-core extension `input` hook (reads `modelStrategy` from `.pi/settings.json`, auto-switches model before skill execution).
396
407
  - Fixed TypeScript baseline issues so `bunx tsc --noEmit` passes.
397
408
 
398
409
  ### 0.19.0 — 0.69.0 alignment + learn rename
@@ -1,3 +1,5 @@
1
+ import { readFile } from "node:fs/promises"
2
+ import path from "node:path"
1
3
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
2
4
  import { Type } from "typebox"
3
5
  import { createArtifactHelperTool, type ArtifactType } from "./tools/artifact-helper"
@@ -20,6 +22,65 @@ import { AsyncMutex } from "./tools/async-mutex"
20
22
  import type { SubagentExecOptions, SubagentRunner } from "./tools/subagent"
21
23
 
22
24
  const _subagentEnvMutex = new AsyncMutex()
25
+ const PIPELINE_STAGE_KEYS = new Set([
26
+ "01-brainstorm",
27
+ "02-plan",
28
+ "03-work",
29
+ "04-review",
30
+ "05-learn",
31
+ ])
32
+
33
+ interface ModelStrategySettings {
34
+ modelStrategy?: Record<string, string>
35
+ }
36
+
37
+ async function readProjectSettings(cwd: string): Promise<ModelStrategySettings | null> {
38
+ const settingsPath = path.join(cwd, ".pi", "settings.json")
39
+ try {
40
+ const content = await readFile(settingsPath, "utf8")
41
+ return JSON.parse(content) as ModelStrategySettings
42
+ } catch {
43
+ return null
44
+ }
45
+ }
46
+
47
+ function parseStageSkillName(text: string): string | null {
48
+ const trimmed = text.trim()
49
+ const match = trimmed.match(/^\/skill:([^\s]+)/)
50
+ if (!match) {
51
+ return null
52
+ }
53
+
54
+ const skillName = match[1]
55
+ return PIPELINE_STAGE_KEYS.has(skillName) ? skillName : null
56
+ }
57
+
58
+ function parseModelRef(
59
+ modelRef: string,
60
+ currentProvider?: string,
61
+ ): { provider: string, id: string } | null {
62
+ const trimmed = modelRef.trim()
63
+ if (!trimmed) {
64
+ return null
65
+ }
66
+
67
+ const slashIndex = trimmed.indexOf("/")
68
+ if (slashIndex > 0 && slashIndex < trimmed.length - 1) {
69
+ return {
70
+ provider: trimmed.slice(0, slashIndex),
71
+ id: trimmed.slice(slashIndex + 1),
72
+ }
73
+ }
74
+
75
+ if (!currentProvider) {
76
+ return null
77
+ }
78
+
79
+ return {
80
+ provider: currentProvider,
81
+ id: trimmed,
82
+ }
83
+ }
23
84
 
24
85
  /**
25
86
  * Create a subagent runner that handles env injection with mutex protection
@@ -223,6 +284,62 @@ const patternExtractorParams = Type.Object({
223
284
  })
224
285
 
225
286
  export default function ceCoreExtension(pi: ExtensionAPI) {
287
+ pi.on("input", async (event, ctx) => {
288
+ if (event.source === "extension") {
289
+ return { action: "continue" as const }
290
+ }
291
+
292
+ const stageKey = parseStageSkillName(event.text)
293
+ if (!stageKey) {
294
+ return { action: "continue" as const }
295
+ }
296
+
297
+ const settings = await readProjectSettings(ctx.cwd)
298
+ const modelStrategy = settings?.modelStrategy
299
+ if (!modelStrategy) {
300
+ return { action: "continue" as const }
301
+ }
302
+
303
+ const targetModelRef = modelStrategy[stageKey] ?? modelStrategy.default
304
+ if (!targetModelRef) {
305
+ return { action: "continue" as const }
306
+ }
307
+
308
+ const parsed = parseModelRef(targetModelRef, ctx.model?.provider)
309
+ if (!parsed) {
310
+ if (ctx.hasUI) {
311
+ ctx.ui.notify(`Invalid modelStrategy entry for ${stageKey}: ${targetModelRef}`, "warning")
312
+ }
313
+ return { action: "continue" as const }
314
+ }
315
+
316
+ if (ctx.model?.provider === parsed.provider && ctx.model?.id === parsed.id) {
317
+ return { action: "continue" as const }
318
+ }
319
+
320
+ const model = ctx.modelRegistry.find(parsed.provider, parsed.id)
321
+ if (!model) {
322
+ if (ctx.hasUI) {
323
+ ctx.ui.notify(`Configured model not found for ${stageKey}: ${targetModelRef}`, "warning")
324
+ }
325
+ return { action: "continue" as const }
326
+ }
327
+
328
+ const switched = await pi.setModel(model)
329
+ if (switched) {
330
+ if (ctx.hasUI) {
331
+ ctx.ui.notify(`Switched model for ${stageKey}: ${model.provider}/${model.id}`, "info")
332
+ }
333
+ return { action: "continue" as const }
334
+ }
335
+
336
+ if (ctx.hasUI) {
337
+ ctx.ui.notify(`No API key available for configured model: ${model.provider}/${model.id}`, "warning")
338
+ }
339
+
340
+ return { action: "continue" as const }
341
+ })
342
+
226
343
  const artifactHelper = createArtifactHelperTool()
227
344
  const askUserQuestion = createAskUserQuestionTool()
228
345
  const subagent = createSubagentTool()
@@ -338,7 +455,6 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
338
455
  return {
339
456
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
340
457
  details: result,
341
- terminate: true,
342
458
  }
343
459
  },
344
460
  })
@@ -393,7 +509,6 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
393
509
  return {
394
510
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
395
511
  details: result,
396
- terminate: true,
397
512
  }
398
513
  },
399
514
  })
@@ -449,7 +564,6 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
449
564
  return {
450
565
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
451
566
  details: result,
452
- terminate: true,
453
567
  }
454
568
  },
455
569
  })
@@ -489,7 +603,6 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
489
603
  return {
490
604
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
491
605
  details: result,
492
- terminate: true,
493
606
  }
494
607
  },
495
608
  })
@@ -531,7 +644,6 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
531
644
  return {
532
645
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
533
646
  details: result,
534
- terminate: true,
535
647
  }
536
648
  },
537
649
  })
@@ -553,7 +665,6 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
553
665
  return {
554
666
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
555
667
  details: result,
556
- terminate: true,
557
668
  }
558
669
  },
559
670
  })
@@ -9,7 +9,7 @@
9
9
  // 1. Lock / generated files → extreme compression (summary only)
10
10
  // 2. package.json → keep scripts + dep summary, drop full version lists
11
11
  // 3. Large code files → keep signatures/structure, collapse bodies
12
- // 4. Large markdown → keep headings + first paragraph per section
12
+ // 4. Large markdown → keep headings + code blocks + lists + expanded paragraphs per section
13
13
  // 5. Generic → head/tail truncation for very large files
14
14
 
15
15
  // ============================================================================
@@ -93,6 +93,9 @@ function classifyFile(path: string): FileCategory {
93
93
  /** Minimum output size (bytes) to trigger any filtering */
94
94
  const MIN_FILTER_THRESHOLD = 2048 // 2KB
95
95
 
96
+ /** Minimum output size (bytes) to trigger markdown filtering */
97
+ const MARKDOWN_FILTER_THRESHOLD = 8192 // 8KB — markdown docs are dense, don't filter small ones
98
+
96
99
  /** Maximum size for structural code compression trigger */
97
100
  const CODE_COMPRESS_THRESHOLD = 8192 // 8KB
98
101
 
@@ -308,6 +311,9 @@ function filterMarkdown(output: string): string {
308
311
  let inParagraph = false
309
312
  let paragraphLineCount = 0
310
313
  let inCodeBlock = false
314
+ let omitMarkerPlaced = false
315
+
316
+ const MAX_PARAGRAPH_LINES = 3 // Keep first N lines of each paragraph (vs 1 before)
311
317
 
312
318
  for (const line of lines) {
313
319
  const trimmed = line.trim()
@@ -322,6 +328,7 @@ function filterMarkdown(output: string): string {
322
328
  // Keep all lines inside code blocks
323
329
  if (inCodeBlock) {
324
330
  kept.push(line)
331
+ omitMarkerPlaced = false
325
332
  continue
326
333
  }
327
334
 
@@ -330,27 +337,43 @@ function filterMarkdown(output: string): string {
330
337
  kept.push(line)
331
338
  inParagraph = false
332
339
  paragraphLineCount = 0
340
+ omitMarkerPlaced = false
333
341
  continue
334
342
  }
335
343
 
336
- // Empty lines
344
+ // Empty lines — reset paragraph state
337
345
  if (trimmed === "") {
338
346
  kept.push(line)
339
347
  inParagraph = false
340
348
  paragraphLineCount = 0
349
+ omitMarkerPlaced = false
350
+ continue
351
+ }
352
+
353
+ // Always keep list items (-, *, numbered) — they often contain key definitions
354
+ if (/^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) {
355
+ kept.push(line)
356
+ omitMarkerPlaced = false
357
+ // List items don't count as paragraph continuation
341
358
  continue
342
359
  }
343
360
 
344
- // Keep first paragraph line after heading, skip the rest
361
+ // Keep first N lines of each paragraph
345
362
  if (!inParagraph) {
346
363
  kept.push(line)
347
364
  inParagraph = true
348
365
  paragraphLineCount = 1
366
+ omitMarkerPlaced = false
349
367
  } else {
350
368
  paragraphLineCount++
351
- // Skip subsequent paragraph lines
352
- if (paragraphLineCount === 2) {
353
- kept.push(` [... additional paragraph content omitted]`)
369
+ if (paragraphLineCount <= MAX_PARAGRAPH_LINES) {
370
+ kept.push(line)
371
+ } else {
372
+ // Place omit marker once per paragraph
373
+ if (!omitMarkerPlaced) {
374
+ kept.push(` [... additional paragraph content omitted]`)
375
+ omitMarkerPlaced = true
376
+ }
354
377
  }
355
378
  }
356
379
  }
@@ -400,9 +423,11 @@ function appendFilterNotice(
400
423
  originalBytes: number,
401
424
  filteredBytes: number,
402
425
  strategy: string,
426
+ path?: string,
403
427
  ): string {
404
428
  const saved = formatBytes(originalBytes - filteredBytes)
405
- return `${output}\n\n[Read output filtered: ${formatBytes(originalBytes)} ${formatBytes(filteredBytes)} (${saved} saved, strategy: ${strategy})]`
429
+ const pathHint = path ? `: bash cat ${path}` : ": bash cat <path>"
430
+ return `${output}\n\n[Read output filtered: ${formatBytes(originalBytes)} → ${formatBytes(filteredBytes)} (${saved} saved, strategy: ${strategy}). If you need the full content, use${pathHint}]`
406
431
  }
407
432
 
408
433
  // ============================================================================
@@ -464,8 +489,12 @@ export function filterReadOutput(input: ReadOutputFilterInput): ReadOutputFilter
464
489
  break
465
490
 
466
491
  case "markdown":
467
- filtered = filterMarkdown(output)
468
- strategyName = "markdown (headings + first paragraph)"
492
+ if (originalBytes >= MARKDOWN_FILTER_THRESHOLD) {
493
+ filtered = filterMarkdown(output)
494
+ strategyName = "markdown (headings + code + lists + expanded paragraphs)"
495
+ } else {
496
+ return { output, filtered: false, originalBytes, filteredBytes: originalBytes, strategy: "none (markdown below 8KB threshold)" }
497
+ }
469
498
  break
470
499
 
471
500
  case "config":
@@ -497,7 +526,7 @@ export function filterReadOutput(input: ReadOutputFilterInput): ReadOutputFilter
497
526
  }
498
527
 
499
528
  return {
500
- output: appendFilterNotice(filtered, originalBytes, filteredBytes, strategyName),
529
+ output: appendFilterNotice(filtered, originalBytes, filteredBytes, strategyName, path),
501
530
  filtered: true,
502
531
  originalBytes,
503
532
  filteredBytes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leing2021/super-pi",
3
- "version": "0.19.2",
3
+ "version": "0.19.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Pi-native Compound Engineering package for iterative development workflows",
@@ -4,21 +4,21 @@ Use these rules in all Phase 1 skills: `01-brainstorm` → `02-plan` → `03-wor
4
4
 
5
5
  ## Start of skill: model routing
6
6
 
7
- Run this checklist before normal skill workflow:
8
-
9
- 1. Read `.pi/settings.json` from the project root.
10
- 2. Parse `modelStrategy` (if missing, skip switching).
11
- 3. Resolve current stage key:
12
- - `01-brainstorm`
13
- - `02-plan`
14
- - `03-work`
15
- - `04-review`
16
- - `05-learn`
17
- 4. Pick `targetModel = modelStrategy[stageKey] ?? modelStrategy.default`.
18
- 5. If `targetModel` exists and differs from the current model, run `/model <targetModel>`.
19
- 6. If switching fails, continue with current model and mention the failure once.
20
-
21
- ## End of skill: status + context + optional auto-continue
7
+ Model routing is handled automatically by the ce-core extension's `input` hook.
8
+ When a user types `/skill:01-brainstorm` through `/skill:05-learn`, the extension:
9
+
10
+ 1. Reads `.pi/settings.json` from the project root.
11
+ 2. Parses `modelStrategy[stageKey]` or falls back to `modelStrategy.default`.
12
+ 3. If the target model differs from the current model, calls `pi.setModel()`.
13
+ 4. If switching fails, notifies the user and continues with the current model.
14
+
15
+ No manual `/model` command is needed. The skill itself does not need to handle model switching.
16
+
17
+ Supported `modelStrategy` formats in `.pi/settings.json`:
18
+ - Full reference: `"02-plan": "anthropic/claude-opus-4-1"`
19
+ - Bare model id (reuses current provider): `"02-plan": "claude-opus-4-1"`
20
+
21
+ ## End of skill: status + context
22
22
 
23
23
  Before final completion, always output these blocks (replace placeholders with real values, never output angle-bracket placeholders literally):
24
24
 
@@ -45,16 +45,6 @@ Next step mapping:
45
45
  - `04-review` → `/skill:05-learn`
46
46
  - `05-learn` → `Completed`
47
47
 
48
- Then read `.pi/settings.json` → `pipeline.autoContinue`:
49
- - If `false` or missing, stop after the status block and wait for user input.
50
- - If current stage is `05-learn`, stop after status block.
51
- - If `true` and current stage is not `05-learn`, auto-continue is allowed only after stage-specific gates are satisfied:
52
- - `01-brainstorm`: user has explicitly approved the design handoff.
53
- - `02-plan`: review choice is resolved (A/B/C flow completed) and user confirmed to proceed.
54
- - `04-review`: optional QA choice is resolved (A/B/C flow completed) and user confirmed to proceed.
55
- - Any unclear/ambiguous state: do not auto-continue; stop and ask.
56
- - When gates are satisfied, automatically trigger the mapped next skill command.
57
-
58
48
  ### Handoff-lite template
59
49
 
60
50
  When a stage produces or updates handoff-lite, use this evidence-first structure and keep it concise (target <= 1500 tokens):