pi-bar 0.3.27 → 0.3.29

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
@@ -9,9 +9,9 @@
9
9
  claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ Inspecting package structure ❯ plan:active
10
10
  ```
11
11
 
12
- ![pi-bar with low context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.27/assets/screenshot-green.png)
13
- ![pi-bar with medium context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.27/assets/screenshot-yellow.png)
14
- ![pi-bar with high context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.27/assets/screenshot-red.png)
12
+ ![pi-bar with low context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.29/assets/screenshot-green.png)
13
+ ![pi-bar with medium context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.29/assets/screenshot-yellow.png)
14
+ ![pi-bar with high context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.29/assets/screenshot-red.png)
15
15
 
16
16
  ## Why use it?
17
17
 
@@ -1008,21 +1008,139 @@ class FooterTldrEngine {
1008
1008
  }
1009
1009
  }
1010
1010
 
1011
+ // First-word allow/ban lists are duplicated between the prompt and the
1012
+ // sanitizer fallback. They live here so both stay aligned automatically when
1013
+ // new verbs get added or moved.
1014
+ const ALLOWED_FIRST_WORDS_PROGRESSIVE = [
1015
+ "Reviewing",
1016
+ "Investigating",
1017
+ "Exploring",
1018
+ "Updating",
1019
+ "Refining",
1020
+ "Fixing",
1021
+ "Implementing",
1022
+ "Wrapping up",
1023
+ "Bumping",
1024
+ "Releasing",
1025
+ "Preparing",
1026
+ "Drafting",
1027
+ "Resuming",
1028
+ "Pulling",
1029
+ "Surveying",
1030
+ "Recording",
1031
+ ] as const;
1032
+ const ALLOWED_FIRST_WORDS_PAST = [
1033
+ "Reviewed",
1034
+ "Investigated",
1035
+ "Explored",
1036
+ "Updated",
1037
+ "Refined",
1038
+ "Fixed",
1039
+ "Implemented",
1040
+ "Wrapped up",
1041
+ "Bumped",
1042
+ "Released",
1043
+ "Prepared",
1044
+ "Drafted",
1045
+ "Resumed",
1046
+ "Pulled",
1047
+ "Surveyed",
1048
+ "Recorded",
1049
+ ] as const;
1050
+ const BANNED_FIRST_WORDS = [
1051
+ "Read",
1052
+ "Reading",
1053
+ "Grep",
1054
+ "Grepping",
1055
+ "Listing",
1056
+ "List",
1057
+ "Counting",
1058
+ "Counted",
1059
+ "Extracting",
1060
+ "Extracted",
1061
+ "Displaying",
1062
+ "Displayed",
1063
+ "Editing",
1064
+ "Edited",
1065
+ "Writing",
1066
+ "Wrote",
1067
+ "Running",
1068
+ "Ran",
1069
+ "Publishing",
1070
+ "Published",
1071
+ "Capturing",
1072
+ "Captured",
1073
+ "Verifying",
1074
+ "Verified",
1075
+ "Verify",
1076
+ "Validating",
1077
+ "Validated",
1078
+ "Validate",
1079
+ "Checking",
1080
+ "Checked",
1081
+ "Check",
1082
+ "Confirming",
1083
+ "Confirmed",
1084
+ "Confirm",
1085
+ "Searching",
1086
+ "Searched",
1087
+ "Search",
1088
+ "Finding",
1089
+ "Found",
1090
+ "Find",
1091
+ ] as const;
1092
+
1011
1093
  function checkpointSystemPrompt(job: TldrCheckpointJob): string {
1012
- const tenseInstruction =
1013
- job.displayPriority === "final"
1014
- ? "Start with a past-tense verb."
1015
- : "Start with a present-tense -ing verb.";
1094
+ // Tense + exemplars vary by priority. Final TLDRs describe completed turns;
1095
+ // using -ing examples for final caused all final TLDRs to slip back into
1096
+ // present-progressive even though the literal instruction said past tense.
1097
+ // Immediate TLDRs cover a brand-new user request; they need a rephrase
1098
+ // directive instead of generic filler like "Continuing with task".
1099
+ let tenseInstruction: string;
1100
+ let goodExamples: string;
1101
+ let allowedFirstWords: readonly string[];
1102
+ if (job.displayPriority === "final") {
1103
+ tenseInstruction = "Start with a past-tense verb describing what was completed.";
1104
+ goodExamples = [
1105
+ "- Updated footer summary behavior",
1106
+ "- Investigated live TLDR regressions",
1107
+ "- Refined sanitizer for stray prefixes",
1108
+ "- Wrapped up extension release",
1109
+ ].join("\n");
1110
+ allowedFirstWords = ALLOWED_FIRST_WORDS_PAST;
1111
+ } else if (job.displayPriority === "immediate") {
1112
+ tenseInstruction =
1113
+ "Rephrase the user's new request as a concise present-progressive task clause. If the request is opaque (e.g. \"continue\", \"go\", \"ok\"), name the carry-over task with a noun (e.g. \"Resuming refactor work\"), not generic filler.";
1114
+ goodExamples = [
1115
+ "- Reviewing footer summary behavior",
1116
+ "- Investigating live TLDR regressions",
1117
+ "- Refining sanitizer for stray prefixes",
1118
+ "- Preparing extension release",
1119
+ "- Resuming refactor work",
1120
+ ].join("\n");
1121
+ allowedFirstWords = ALLOWED_FIRST_WORDS_PROGRESSIVE;
1122
+ } else {
1123
+ tenseInstruction = "Start with a present-tense -ing verb describing current work.";
1124
+ goodExamples = [
1125
+ "- Reviewing footer summary behavior",
1126
+ "- Investigating live TLDR regressions",
1127
+ "- Refining sanitizer for stray prefixes",
1128
+ "- Wrapping up extension release",
1129
+ ].join("\n");
1130
+ allowedFirstWords = ALLOWED_FIRST_WORDS_PROGRESSIVE;
1131
+ }
1132
+
1016
1133
  // Frame the model as a human developer describing visible work, not an agent
1017
1134
  // summarizing its own mechanics. The banned phrases and few-shot examples
1018
1135
  // fix concrete regressions observed in backtests: tool-name verbs, success
1019
- // suffixes, file path leaks, and markdown formatting.
1136
+ // suffixes, file path leaks, and markdown formatting. The HARD CONSTRAINTS
1137
+ // block sits at the tail of the prompt because instruction-following models
1138
+ // give the most weight to the most-recent directives (recency bias).
1020
1139
  return `Write one plain-English TLDR for a Pi coding agent.
1021
1140
  Describe the work progress as if a human developer were doing it.
1022
1141
  Focus on the task activity and current outcome, not agent mechanics.
1023
1142
  Do not mention tools, tool calls, prompts, messages, model output, or implementation details.
1024
- Do not start with tool-name verbs such as Read, Reading, Grep, Listing, Counting, Extracting, Displaying, Editing, Writing, Running, Publishing.
1025
- Use human-developer verbs instead: Reviewing, Investigating, Exploring, Updating, Refining, Fixing, Implementing, Wrapping up.
1143
+ Use human-developer verbs instead of tool-narration verbs.
1026
1144
  Do not use file paths, file extensions, code identifiers, package names, or version strings.
1027
1145
  Do not use backticks, asterisks, underscores, quotes, or any markdown formatting.
1028
1146
  Do not append filler suffixes such as "with success", "successfully", or "completed successfully".
@@ -1039,18 +1157,20 @@ Output only the status fragment itself. No prefixes, labels, bullets, or quotes.
1039
1157
  Plain text only; no markdown, JSON, code, file paths, or tool names.
1040
1158
 
1041
1159
  Good examples:
1042
- - Reviewing footer summary behavior
1043
- - Investigating live TLDR regressions
1044
- - Refining sanitizer for stray prefixes
1045
- - Wrapping up extension release
1160
+ ${goodExamples}
1046
1161
 
1047
1162
  Bad examples:
1048
1163
  - Editing extensions/status-footer.ts with success.
1049
1164
  - Reading status-footer file completed successfully.
1050
1165
  - Publishing \`pi-bar@0.3.3\` to npm.
1051
1166
  - Grepping for sanitizeTldrText callers.
1167
+ - Verifying repository status after commit.
1168
+ - Investigating user input responses.
1052
1169
 
1053
- ${tenseInstruction}`;
1170
+ HARD CONSTRAINTS (apply last; override anything above that conflicts):
1171
+ - First word MUST be one of: ${allowedFirstWords.join(", ")}.
1172
+ - First word MUST NOT be: ${BANNED_FIRST_WORDS.join(", ")}.
1173
+ - ${tenseInstruction}`;
1054
1174
  }
1055
1175
 
1056
1176
  function extractStopReason(message: unknown): string | undefined {
@@ -1236,6 +1356,95 @@ function stripIdentifierLeaks(text: string): string {
1236
1356
  .trim();
1237
1357
  }
1238
1358
 
1359
+ // After identifier removal the sentence often has a dangling preposition like
1360
+ // "Bumping version to for publishing" (was "to 0.3.2 for publishing") or a
1361
+ // trailing "to"/"version". Run in a fix-point loop so chained leftovers (e.g.
1362
+ // "to version") collapse cleanly.
1363
+ const DANGLING_TRAILING_PREP_PATTERN =
1364
+ /\s+(?:to|at|as|of|by|for|in|on|with|from|version|v)\s*[.!?,;:]?\s*$/i;
1365
+ const DANGLING_PREP_CHAIN_PATTERN =
1366
+ /\b(to|at|as|of|by|from|version|v)\s+(for|in|on|with|from|after|before|during|to|at|as|of|by|and|but|or)\b/gi;
1367
+ // File-path / identifier strip can leave a verb directly followed by a
1368
+ // preposition: "Reviewing README for image update" -> after path strip ->
1369
+ // "Reviewing for image update". Collapse the bare verb+prep into just the verb.
1370
+ const VERB_BARE_PREP_PATTERN =
1371
+ /\b(Reviewing|Investigating|Updating|Refining|Exploring|Fixing|Implementing|Bumping|Releasing|Preparing|Drafting|Resuming|Pulling|Surveying|Recording)\s+(?:for|in|on|with|after|before|to|at|as|of|by|from)\s+/gi;
1372
+
1373
+ function stripDanglingPrepositions(text: string): string {
1374
+ let cleaned = text;
1375
+ let previous: string;
1376
+ do {
1377
+ previous = cleaned;
1378
+ cleaned = cleaned.replace(VERB_BARE_PREP_PATTERN, "$1 ");
1379
+ cleaned = cleaned.replace(DANGLING_PREP_CHAIN_PATTERN, "$2");
1380
+ cleaned = cleaned.replace(DANGLING_TRAILING_PREP_PATTERN, "").trim();
1381
+ } while (cleaned !== previous && cleaned.length > 0);
1382
+ return cleaned;
1383
+ }
1384
+
1385
+ // Last-resort fallback when the model ignored the allow-list and started the
1386
+ // TLDR with a banned tool-narration verb. The map preserves tense (present
1387
+ // vs past). Case of the first letter follows the original word.
1388
+ const BANNED_FIRST_WORD_REWRITES: Record<string, string> = {
1389
+ Reading: "Reviewing",
1390
+ Read: "Reviewed",
1391
+ Grepping: "Investigating",
1392
+ Grep: "Investigated",
1393
+ Listing: "Reviewing",
1394
+ List: "Reviewed",
1395
+ Counting: "Surveying",
1396
+ Counted: "Surveyed",
1397
+ Extracting: "Pulling",
1398
+ Extracted: "Pulled",
1399
+ Displaying: "Reviewing",
1400
+ Displayed: "Reviewed",
1401
+ Editing: "Updating",
1402
+ Edited: "Updated",
1403
+ Writing: "Drafting",
1404
+ Wrote: "Drafted",
1405
+ Running: "Working on",
1406
+ Ran: "Worked on",
1407
+ Publishing: "Releasing",
1408
+ Published: "Released",
1409
+ Capturing: "Recording",
1410
+ Captured: "Recorded",
1411
+ Verifying: "Reviewing",
1412
+ Verified: "Reviewed",
1413
+ Verify: "Review",
1414
+ Validating: "Reviewing",
1415
+ Validated: "Reviewed",
1416
+ Validate: "Review",
1417
+ Checking: "Reviewing",
1418
+ Checked: "Reviewed",
1419
+ Check: "Review",
1420
+ Confirming: "Reviewing",
1421
+ Confirmed: "Reviewed",
1422
+ Confirm: "Review",
1423
+ Searching: "Investigating",
1424
+ Searched: "Investigated",
1425
+ Search: "Investigate",
1426
+ Finding: "Investigating",
1427
+ Found: "Investigated",
1428
+ Find: "Investigate",
1429
+ };
1430
+ const BANNED_FIRST_WORD_PATTERN = new RegExp(
1431
+ `^(${Object.keys(BANNED_FIRST_WORD_REWRITES).join("|")})\\b`,
1432
+ );
1433
+
1434
+ function rewriteBannedFirstWord(text: string): string {
1435
+ const match = BANNED_FIRST_WORD_PATTERN.exec(text);
1436
+ if (!match) return text;
1437
+ const original = match[1];
1438
+ const replacement = BANNED_FIRST_WORD_REWRITES[original];
1439
+ if (!replacement) return text;
1440
+ // Preserve lowercase-start TLDRs (rare but possible) by matching original case.
1441
+ const cased =
1442
+ original[0] === original[0].toLowerCase()
1443
+ ? replacement[0].toLowerCase() + replacement.slice(1)
1444
+ : replacement;
1445
+ return cased + text.slice(original.length);
1446
+ }
1447
+
1239
1448
  // Trailing filler suffixes observed in backtests:
1240
1449
  // "... with success.", "... completed successfully.", "... successfully."
1241
1450
  const SUCCESS_SUFFIX_PATTERN =
@@ -1265,8 +1474,10 @@ function sanitizeTldrText(
1265
1474
  const withoutMarkdown = stripMarkdownFormatting(stripped);
1266
1475
  const withoutScaffolding = stripLeakedScaffolding(withoutMarkdown) || withoutMarkdown;
1267
1476
  const withoutLeaks = stripIdentifierLeaks(withoutScaffolding) || withoutScaffolding;
1268
- const withoutSuccess = stripSuccessSuffix(withoutLeaks) || withoutLeaks;
1269
- return truncateText(withoutSuccess, maxChars);
1477
+ const withoutDangling = stripDanglingPrepositions(withoutLeaks) || withoutLeaks;
1478
+ const withoutSuccess = stripSuccessSuffix(withoutDangling) || withoutDangling;
1479
+ const rewritten = rewriteBannedFirstWord(withoutSuccess);
1480
+ return truncateText(rewritten, maxChars);
1270
1481
  }
1271
1482
 
1272
1483
  function shouldShowStatus(key: string, filter: StatusFilter): boolean {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bar",
3
- "version": "0.3.27",
3
+ "version": "0.3.29",
4
4
  "description": "Pi footer/statusline showing active model, thinking level, context pressure, live TLDR, and extension statuses.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -51,6 +51,6 @@
51
51
  "extensions": [
52
52
  "./extensions/status-footer.ts"
53
53
  ],
54
- "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.27/assets/screenshot-yellow.png"
54
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.29/assets/screenshot-yellow.png"
55
55
  }
56
56
  }