pi-usereq 0.59.0 → 0.61.0

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
@@ -1,4 +1,4 @@
1
- # PI-useReq/pi-usereq (0.59.0)
1
+ # PI-useReq/pi-usereq (0.61.0)
2
2
 
3
3
  <p align="center">
4
4
  <img src="https://img.shields.io/badge/python-3.11%2B-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python 3.11+">
@@ -51,7 +51,7 @@ in sync with the repository. All capabilities are exposed as slash commands and
51
51
  - **Git repository** - every `req-*` command runs slash-command-owned git validation: the project must be inside a git work tree, the tracked working tree must be clean, and `HEAD` must resolve (a detached `HEAD` is tolerated; a working branch is recommended because the branch name is embedded in generated worktree names).
52
52
  - **Requirements documentation** - the configured `docs-dir` (default `pi-usereq/docs`) must contain the canonical documents required by each command (`REQUIREMENTS.md`, `WORKFLOW.md`, `REFERENCES.md`); commands such as `/req-write`, `/req-create`, and `/req-workflow` are the entry points that generate them.
53
53
  - **Static code checkers** - the bundled checkers (`pyright`, `ruff`, `eslint`) install automatically through the `postinstall` script; the native C/C++ checkers (`cppcheck`, `clang-format`) require a one-line system install (see [Install](#install)). Default configured languages: C, C++, JavaScript, Python, TypeScript.
54
- - **Models** - model selection and model configuration are managed by the pi client, not by pi-usereq: the extension bundles no model files and exposes no model-related configuration or management surface (custom providers and models are configured in the pi client, e.g. `~/.pi/agent/models.json`, and selected through its model picker).
54
+ - **Models** - model selection and model configuration are managed by the pi client, not by pi-usereq: the extension bundles no model files and exposes no model-related configuration or management surface (custom providers and models are configured in the pi client, e.g. `~/.pi/agent/models.json`, and selected through its model picker). The tool is tested with the OpenRouter model `~deepseek/deepseek-v4-flash-latest`; the extension is model-agnostic and also works with other models exposed by the pi client.
55
55
 
56
56
 
57
57
  ## Feature Highlights
@@ -79,6 +79,12 @@ creating a worktree.
79
79
  > session with a clean context (run `/new`, pi's `Start a new session` action) before each new `/req-*` command, unless you
80
80
  > actually need to keep the previous context (for example when the next command builds on the outputs of the previous one).
81
81
 
82
+ > **Recommended context for analysis** — for the most effective analysis it is recommended to place the requirements,
83
+ > reference, and workflow documents in the prompt context by enabling the `Context Files` toggles for `REQUIREMENTS.md`,
84
+ > `REFERENCES.md`, and `WORKFLOW.md` (the settings-menu row then reads `Context Files requirements:on • references:on • workflow:on`).
85
+ > The enabled canonical documents are injected into every bundled prompt through `%%CONTEXT_FILES%%`, so the agent analyzes
86
+ > the requirements, the reference index, and the workflow documentation together with the source code.
87
+
82
88
  | Command | Description | Required docs |
83
89
  | --- | --- | --- |
84
90
  | `/req-write` | Produce a *SRS* draft based on the User Request description | none |
@@ -275,7 +281,7 @@ The interactive configuration menu exposes every user-facing setting; changes ar
275
281
  - **Documentation directory** — `docs-dir` (default `pi-usereq/docs`) used for the canonical documents.
276
282
  - **Unit tests directory** — `tests-dir` (default `tests`).
277
283
  - **Source directories** — `src-dir` (default `["src"]`) used by the analysis tools.
278
- - **Context Files** — toggles to inject `REQUIREMENTS.md`, `WORKFLOW.md`, and `REFERENCES.md` into the prompt context through `%%CONTEXT_FILES%%`.
284
+ - **Context Files** — toggles to inject `REQUIREMENTS.md`, `WORKFLOW.md`, and `REFERENCES.md` into the prompt context through `%%CONTEXT_FILES%%`. It is recommended to keep all three toggles enabled (`requirements:on • references:on • workflow:on`) so the analysis commands receive the canonical documents in their context.
279
285
  - **Auto git commit** — `enable` (default) injects git commit instructions into every prompt; `disable` forces read-only git behavior (`git_read-only.md`) and turns worktree orchestration off.
280
286
  - **Git worktree** / **Worktree prefix** — enable/disable prompt-command worktree isolation and set the name prefix (default `PI-useReq-`).
281
287
  - **Language static code checkers** — per-language `enable`/`disable` flags and the global `Command`-module checker definitions (view/remove/reset with confirmation).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-usereq",
3
- "version": "0.59.0",
3
+ "version": "0.61.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Ogekuri/PI-useReq.git"
@@ -32,9 +32,9 @@
32
32
  ]
33
33
  },
34
34
  "peerDependencies": {
35
- "@earendil-works/pi-ai": "^0.80.4",
36
- "@earendil-works/pi-coding-agent": "^0.87.1",
37
- "@earendil-works/pi-tui": "^0.80.4",
35
+ "@earendil-works/pi-ai": "^1.0.0",
36
+ "@earendil-works/pi-coding-agent": "^1.0.0",
37
+ "@earendil-works/pi-tui": "^1.0.0",
38
38
  "@sinclair/typebox": "^0.34.49"
39
39
  },
40
40
  "dependencies": {
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @file
3
+ * @brief Measures runtime character and token sizes of the canonical context files.
4
+ * @details Provides reusable helpers that compute character and `cl100k_base` token estimates for `REQUIREMENTS.md`, `REFERENCES.md`, and `WORKFLOW.md` under `<base-path>/<docs-dir>` so configuration menus and command summaries share one measurement contract. Runtime is O(n) in aggregate context-file size. Side effects are limited to filesystem reads.
5
+ */
6
+
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { DEFAULT_DOCS_DIR, type UseReqConfig } from "./config.js";
10
+ import { normalizeRelativeDirContract } from "./path-context.js";
11
+ import { countFileMetrics } from "./token-counter.js";
12
+
13
+ /**
14
+ * @brief Stores the canonical context-file names in their documented injection order.
15
+ * @details The tuple is the single ordering source reused by measurement helpers and size renderers so every caller reports `REQUIREMENTS.md`, `REFERENCES.md`, and `WORKFLOW.md` consistently. Lookup complexity is O(1).
16
+ * @satisfies REQ-373
17
+ */
18
+ export const CONTEXT_FILE_NAMES = ["REQUIREMENTS.md", "REFERENCES.md", "WORKFLOW.md"] as const;
19
+
20
+ /**
21
+ * @brief Narrows context-file identifiers to the canonical measured document set.
22
+ * @details Compile-time alias derived from `CONTEXT_FILE_NAMES` and reused by the measured-facts record keys. The alias introduces no runtime cost.
23
+ */
24
+ export type ContextFileName = (typeof CONTEXT_FILE_NAMES)[number];
25
+
26
+ /**
27
+ * @brief Describes the measured size facts of one canonical context file.
28
+ * @details Stores the filesystem existence flag plus the exact character count and the `cl100k_base` token estimate computed from the file content. The interface is compile-time only and introduces no runtime cost.
29
+ * @satisfies REQ-374
30
+ */
31
+ export interface ContextFileSizeFacts {
32
+ readonly exists: boolean;
33
+ readonly chars: number;
34
+ readonly tokens: number;
35
+ }
36
+
37
+ /**
38
+ * @brief Stores the canonical missing-file size facts.
39
+ * @details Reused as the deterministic result for missing, non-file, and unreadable targets so callers never receive `undefined` facts. Lookup complexity is O(1).
40
+ * @satisfies REQ-374
41
+ */
42
+ export const MISSING_CONTEXT_FILE_SIZE: ContextFileSizeFacts = Object.freeze({ exists: false, chars: 0, tokens: 0 });
43
+
44
+ /**
45
+ * @brief Resolves one canonical context-file absolute path for one project base.
46
+ * @details Joins the project base with the trailing-slash-free configured `docs-dir` (falling back to `DEFAULT_DOCS_DIR`) and the supplied canonical file name so menus, summaries, and the `%%CONTEXT_FILES%%` renderer address identical targets. Runtime is O(p) in path length. No external state is mutated.
47
+ * @param[in] projectBase {string} Absolute project root path.
48
+ * @param[in] config {UseReqConfig} Effective project configuration supplying the docs directory.
49
+ * @param[in] fileName {ContextFileName} Canonical context-file name.
50
+ * @return {string} Absolute context-file path.
51
+ * @satisfies REQ-373
52
+ */
53
+ export function resolveContextFilePath(
54
+ projectBase: string,
55
+ config: UseReqConfig,
56
+ fileName: ContextFileName,
57
+ ): string {
58
+ const normalizedDocsDir = normalizeRelativeDirContract(config["docs-dir"]) || DEFAULT_DOCS_DIR;
59
+ return path.join(projectBase, normalizedDocsDir, fileName);
60
+ }
61
+
62
+ /**
63
+ * @brief Measures one context file into deterministic size facts.
64
+ * @details Probes filesystem existence and file kind, reads UTF-8 content, and reuses the shared `countFileMetrics` tokenizer for the `cl100k_base` token estimate. Missing, non-file, and unreadable targets return `MISSING_CONTEXT_FILE_SIZE` facts without throwing. Runtime is O(n) in file size. Side effects are limited to filesystem reads.
65
+ * @param[in] filePath {string} Absolute context-file path to measure.
66
+ * @return {ContextFileSizeFacts} Measured size facts for the target.
67
+ * @satisfies REQ-373, REQ-374
68
+ */
69
+ export function measureContextFileSize(filePath: string): ContextFileSizeFacts {
70
+ try {
71
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
72
+ return MISSING_CONTEXT_FILE_SIZE;
73
+ }
74
+ const metrics = countFileMetrics(fs.readFileSync(filePath, "utf8"));
75
+ return { exists: true, chars: metrics.chars, tokens: metrics.tokens };
76
+ } catch {
77
+ return MISSING_CONTEXT_FILE_SIZE;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * @brief Measures every canonical context file for one project base.
83
+ * @details Iterates `CONTEXT_FILE_NAMES` in documented order, resolves each configured `<base-path>/<docs-dir>` target through `resolveContextFilePath`, and returns the keyed facts record consumed by configuration menus and command summaries. Runtime is O(n) in aggregate context-file size. Side effects are limited to filesystem reads.
84
+ * @param[in] projectBase {string} Absolute project root path.
85
+ * @param[in] config {UseReqConfig} Effective project configuration supplying the docs directory.
86
+ * @return {Record<ContextFileName, ContextFileSizeFacts>} Measured size facts keyed by canonical file name.
87
+ * @satisfies REQ-373, REQ-374
88
+ */
89
+ export function measureContextFileSizes(
90
+ projectBase: string,
91
+ config: UseReqConfig,
92
+ ): Record<ContextFileName, ContextFileSizeFacts> {
93
+ const facts = {} as Record<ContextFileName, ContextFileSizeFacts>;
94
+ for (const fileName of CONTEXT_FILE_NAMES) {
95
+ facts[fileName] = measureContextFileSize(resolveContextFilePath(projectBase, config, fileName));
96
+ }
97
+ return facts;
98
+ }
99
+
100
+ /**
101
+ * @brief Formats one measured context-file size as a compact character and token estimate.
102
+ * @details Emits the deterministic `<chars>c/<tokens>t` shape reused by menu rows, the top-level summary, and the command invocation summary so every surface exposes identical size facts. Runtime is O(n) in rendered length. No external state is mutated.
103
+ * @param[in] facts {ContextFileSizeFacts} Measured context-file size facts.
104
+ * @return {string} Compact `<chars>c/<tokens>t` size string.
105
+ * @satisfies REQ-375, REQ-376, REQ-377
106
+ */
107
+ export function formatContextFileSize(facts: ContextFileSizeFacts): string {
108
+ return `${facts.chars}c/${facts.tokens}t`;
109
+ }
@@ -63,6 +63,126 @@ export interface PromptCommandExecutionPlan {
63
63
  worktreeDir?: string;
64
64
  worktreePath?: string;
65
65
  worktreeRootPath?: string;
66
+ originalModel?: PromptCommandModelSelection;
67
+ originalThinkingLevel?: string;
68
+ }
69
+
70
+ /**
71
+ * @brief Describes one serializable model-selection fact captured at prompt-command preflight.
72
+ * @details Stores the active model provider and identifier as plain strings so the selection can travel inside the prompt execution plan and process-scoped persistence across session replacement without retaining live SDK model objects, and MUST NOT be persisted into pi-usereq configuration files per CTN-022. The interface is compile-time only and introduces no runtime cost.
73
+ * @satisfies REQ-366, CTN-022
74
+ */
75
+ export interface PromptCommandModelSelection {
76
+ provider: string;
77
+ modelId: string;
78
+ }
79
+
80
+ /**
81
+ * @brief Describes the loosely-typed runtime surface required to re-apply one captured selection.
82
+ * @details Narrows the pi runtime to the model registry lookup, the active model plus thinking-level probes used by the duplicate-skip guard, and the session-scoped `setModel`/`setThinkingLevel` appliers that leave the configured default model untouched. All members stay optional so legacy hosts and offline replays degrade to a documented warning instead of aborting orchestration. The interface is compile-time only and introduces no runtime cost.
83
+ * @satisfies REQ-367, REQ-370, REQ-371
84
+ */
85
+ export interface PromptCommandModelSelectionSurface {
86
+ modelRegistry?: { find?: (provider: unknown, modelId: unknown) => unknown };
87
+ model?: unknown;
88
+ thinkingLevel?: unknown;
89
+ setModel?: (model: unknown) => Promise<boolean>;
90
+ setThinkingLevel?: (level: unknown) => void;
91
+ }
92
+
93
+ /**
94
+ * @brief Describes the re-apply request consumed by `reapplyPromptCommandSessionSelection`.
95
+ * @details Carries the captured selection plus thinking level, the runtime surface, and the optional stale-context-safe warning callback so callers keep notification ownership inside `src/index.ts` while the helper stays independent from extension UI types per DES-012. The interface is compile-time only and introduces no runtime cost.
96
+ */
97
+ export interface PromptCommandModelSelectionReapplyOptions {
98
+ selection: PromptCommandModelSelection | undefined;
99
+ thinkingLevel: string | undefined;
100
+ surface: PromptCommandModelSelectionSurface | undefined;
101
+ notifyWarning?: (message: string) => void;
102
+ }
103
+
104
+ /**
105
+ * @brief Describes the deterministic outcome facts of one guarded selection re-application.
106
+ * @details Distinguishes not-attempted skips (missing selection, missing surface, or post-switch state already matching the captured selection) from attempted applications, and carries the normalized warning message emitted when resolution, authentication, or the runtime applier fails. The interface is compile-time only and introduces no runtime cost.
107
+ * @satisfies REQ-370, REQ-372
108
+ */
109
+ export interface PromptCommandModelSelectionReapplyResult {
110
+ attempted: boolean;
111
+ applied: boolean;
112
+ skipped: boolean;
113
+ warningMessage?: string;
114
+ }
115
+
116
+ /**
117
+ * @brief Re-applies one captured model selection onto the active post-switch session.
118
+ * @details Resolves the captured provider plus identifier through the surface model registry, skips the whole operation when the selection or surface is unavailable and when the active model and thinking level already match the captured selection so no duplicate `model_change` session entry is emitted, otherwise awaits the session-scoped `setModel` applier, then re-applies the captured thinking level when the active level differs. Every resolution, authentication, and applier failure degrades into one warning notification through the supplied callback without throwing, so prompt orchestration and worktree closure continue unchanged. Runtime is O(1) plus one awaited model mutation. No external state is mutated beyond the applied session selection.
119
+ * @param[in] options {PromptCommandModelSelectionReapplyOptions} Captured selection, runtime surface, and warning callback.
120
+ * @return {Promise<PromptCommandModelSelectionReapplyResult>} Attempted, applied, skipped, and warning facts for the re-application.
121
+ * @satisfies REQ-366, REQ-367, REQ-368, REQ-369, REQ-370, REQ-371, REQ-372, DES-021
122
+ */
123
+ export async function reapplyPromptCommandSessionSelection(
124
+ options: PromptCommandModelSelectionReapplyOptions,
125
+ ): Promise<PromptCommandModelSelectionReapplyResult> {
126
+ const selection = options.selection;
127
+ const surface = options.surface;
128
+ if (!selection || !surface) {
129
+ return { attempted: false, applied: false, skipped: true };
130
+ }
131
+ const notifyWarning = options.notifyWarning;
132
+ const registryFind = surface.modelRegistry?.find;
133
+ const resolvedCandidate = typeof registryFind === "function"
134
+ ? registryFind.call(surface.modelRegistry, selection.provider, selection.modelId)
135
+ : undefined;
136
+ const resolved = resolvedCandidate as { provider?: unknown; id?: unknown } | null | undefined;
137
+ if (!resolved || typeof resolved.provider !== "string" || typeof resolved.id !== "string") {
138
+ const message = `WARNING: Could not re-apply model ${selection.provider}/${selection.modelId}: model is not available in the active model registry.`;
139
+ notifyWarning?.(message);
140
+ return { attempted: true, applied: false, skipped: false, warningMessage: message };
141
+ }
142
+ const currentModel = surface.model as { provider?: unknown; id?: unknown } | null | undefined;
143
+ const modelMatches = currentModel !== null
144
+ && currentModel !== undefined
145
+ && currentModel.provider === selection.provider
146
+ && currentModel.id === selection.modelId;
147
+ const levelMatches = typeof options.thinkingLevel !== "string"
148
+ || surface.thinkingLevel === options.thinkingLevel;
149
+ if (modelMatches && levelMatches) {
150
+ return { attempted: false, applied: false, skipped: true };
151
+ }
152
+ if (!modelMatches) {
153
+ const setModelFn = surface.setModel;
154
+ if (typeof setModelFn !== "function") {
155
+ const message = `WARNING: Could not re-apply model ${selection.provider}/${selection.modelId}: the runtime does not expose setModel.`;
156
+ notifyWarning?.(message);
157
+ return { attempted: true, applied: false, skipped: false, warningMessage: message };
158
+ }
159
+ let setModelResult: unknown;
160
+ try {
161
+ setModelResult = await setModelFn.call(surface, resolved);
162
+ } catch (error) {
163
+ const message = `WARNING: Could not re-apply model ${selection.provider}/${selection.modelId}: ${error instanceof Error ? error.message : String(error)}.`;
164
+ notifyWarning?.(message);
165
+ return { attempted: true, applied: false, skipped: false, warningMessage: message };
166
+ }
167
+ if (setModelResult === false) {
168
+ const message = `WARNING: Could not re-apply model ${selection.provider}/${selection.modelId}: no API key configured for ${selection.provider}.`;
169
+ notifyWarning?.(message);
170
+ return { attempted: true, applied: false, skipped: false, warningMessage: message };
171
+ }
172
+ }
173
+ const setThinkingLevelFn = surface.setThinkingLevel;
174
+ if (
175
+ typeof options.thinkingLevel === "string"
176
+ && surface.thinkingLevel !== options.thinkingLevel
177
+ && typeof setThinkingLevelFn === "function"
178
+ ) {
179
+ try {
180
+ setThinkingLevelFn.call(surface, options.thinkingLevel);
181
+ } catch {
182
+ // REQ-372: thinking-level failures are advisory-only and MUST NOT abort orchestration.
183
+ }
184
+ }
185
+ return { attempted: true, applied: true, skipped: false };
66
186
  }
67
187
 
68
188
  /**
@@ -109,11 +229,11 @@ interface PromptCommandSessionSwitchOptions {
109
229
 
110
230
  /**
111
231
  * @brief Describes the minimal session-bound surface available after session replacement.
112
- * @details Extends the shared prompt-command context with `sendUserMessage(...)` so prompt dispatch can target the replacement session without reusing stale pre-switch runtime objects. The interface is compile-time only and introduces no runtime cost.
232
+ * @details Extends the shared prompt-command context with a string-only `sendUserMessage(...)` probe so prompt dispatch can target the replacement session without reusing stale pre-switch runtime objects. The string content parameter mirrors the prompt-delivery contract consumed by `deliverPromptCommand(...)` while remaining structurally compatible with the pi 1.0.0 replacement-session delivery signatures. The interface is compile-time only and introduces no runtime cost.
113
233
  */
114
234
  interface PromptCommandActiveContext extends PromptCommandSessionContext {
115
235
  sendUserMessage?: (
116
- content: string | Array<{ type: string; text?: string }>,
236
+ content: string,
117
237
  options?: PromptCommandSessionMessageOptions,
118
238
  ) => Promise<void> | void;
119
239
  }
@@ -1584,9 +1704,11 @@ export function validatePromptRequiredDocs(
1584
1704
  * @param[in] currentSessionDir {string | undefined} Active session directory reused when the execution session is forked.
1585
1705
  * @param[in] currentSessionBranch {PromptCommandSessionEntry[] | undefined} Active in-memory session branch copied when the origin session file is not flushed yet.
1586
1706
  * @param[in] debugOptions {PromptCommandDebugOptions | undefined} Optional prompt debug logging context.
1707
+ * @param[in] originalModel {PromptCommandModelSelection | undefined} Active model provider plus identifier captured by the command handler for later re-application.
1708
+ * @param[in] originalThinkingLevel {string | undefined} Active thinking level captured by the command handler for later re-application.
1587
1709
  * @return {PromptCommandExecutionPlan} Prepared execution plan.
1588
1710
  * @throws {ReqError} Throws when repository validation, required-doc validation, worktree creation, or session preparation fails.
1589
- * @satisfies REQ-200, REQ-203, REQ-206, REQ-207, REQ-215, REQ-219, REQ-220, REQ-245, REQ-256, REQ-271
1711
+ * @satisfies REQ-200, REQ-203, REQ-206, REQ-207, REQ-215, REQ-219, REQ-220, REQ-245, REQ-256, REQ-271, REQ-366
1590
1712
  */
1591
1713
  export function preparePromptCommandExecution(
1592
1714
  promptName: PromptCommandName,
@@ -1597,6 +1719,8 @@ export function preparePromptCommandExecution(
1597
1719
  currentSessionDir: string | undefined,
1598
1720
  currentSessionBranch: PromptCommandSessionEntry[] | undefined,
1599
1721
  debugOptions?: PromptCommandDebugOptions,
1722
+ originalModel?: PromptCommandModelSelection,
1723
+ originalThinkingLevel?: string,
1600
1724
  ): PromptCommandExecutionPlan {
1601
1725
  const gitPath = validatePromptGitState(projectBase, config);
1602
1726
  const reuseCurrentSessionFile = isUsablePromptSessionFile(currentSessionFile, projectBase);
@@ -1624,6 +1748,8 @@ export function preparePromptCommandExecution(
1624
1748
  baseDir: staticWorktreePaths.baseDir,
1625
1749
  originalSessionFile,
1626
1750
  executionSessionFile: originalSessionFile,
1751
+ originalModel,
1752
+ originalThinkingLevel,
1627
1753
  };
1628
1754
  }
1629
1755
  const worktreeDir = buildPromptWorktreeName(gitPath, config);
@@ -1663,6 +1789,8 @@ export function preparePromptCommandExecution(
1663
1789
  worktreeDir: worktreePaths.worktreeDir,
1664
1790
  worktreePath: worktreePaths.worktreePath,
1665
1791
  worktreeRootPath: worktreePaths.worktreeRootPath,
1792
+ originalModel,
1793
+ originalThinkingLevel,
1666
1794
  };
1667
1795
  }
1668
1796
 
@@ -17,6 +17,7 @@ import {
17
17
  comparePiUsereqStartupToolNames,
18
18
  type PiUsereqStartupToolName,
19
19
  } from "./pi-usereq-tools.js";
20
+ import { formatContextFileSize, type ContextFileSizeFacts } from "./context-file-size.js";
20
21
  import { formatRuntimePathForDisplay, normalizeRelativeDirContract } from "./path-context.js";
21
22
  import type {
22
23
  PromptCommandExecutionPlan,
@@ -374,21 +375,27 @@ export const PROMPT_COMMAND_SUMMARY_CUSTOM_TYPE = "pi-usereq-prompt-command";
374
375
 
375
376
  /**
376
377
  * @brief Builds the on-screen command invocation summary for one bundled prompt-backed `req-<prompt>` command.
377
- * @details Renders the command name without the `req-` prefix in uppercase, the user request arguments, and the active configuration fields (`docs-dir`, `src-dir`, `tests-dir`, enabled context files, `AUTO_GIT_COMMIT`, effective `GIT_WORKTREE_ENABLED`, `GIT_WORKTREE_PREFIX`, enabled static-check languages, and `enabled-tools`) so the TUI shows only a compact summary while the full rendered prompt is delivered hidden to the LLM agent. Static-check languages are emitted in canonical `DEFAULT_STATIC_CHECK_LANGUAGES` order; enabled tools are emitted in documented menu order via `comparePiUsereqStartupToolNames`. The `context files`, `static code checks`, and `enabled tools` fields render the literal `none` placeholder whenever their respective enabled-item list is empty so the summary never shows a blank value. Sections are emitted in the order `Command:`, `Configuration:` with its bullet list, then `User's Request:`, each separated by one blank line. Runtime is O(l + t log t) where l is language count and t is enabled-tool count. No external state is mutated.
378
+ * @details Renders the command name without the `req-` prefix in uppercase, the user request arguments, and the active configuration fields (`docs-dir`, `src-dir`, `tests-dir`, enabled context files, `AUTO_GIT_COMMIT`, effective `GIT_WORKTREE_ENABLED`, `GIT_WORKTREE_PREFIX`, enabled static-check languages, and `enabled-tools`) so the TUI shows only a compact summary while the full rendered prompt is delivered hidden to the LLM agent. When measured context-file size facts are supplied, every enabled existing context file in the `context files` field is rendered as `name(<chars>c/<tokens>t)`; without facts, or for enabled missing files, the plain `name` form is rendered. Static-check languages are emitted in canonical `DEFAULT_STATIC_CHECK_LANGUAGES` order; enabled tools are emitted in documented menu order via `comparePiUsereqStartupToolNames`. The `context files`, `static code checks`, and `enabled tools` fields render the literal `none` placeholder whenever their respective enabled-item list is empty so the summary never shows a blank value. Sections are emitted in the order `Command:`, `Configuration:` with its bullet list, then `User's Request:`, each separated by one blank line. Runtime is O(l + t log t) where l is language count and t is enabled-tool count. No external state is mutated.
378
379
  * @param[in] promptName {string} Bundled prompt name without the `req-` prefix.
379
380
  * @param[in] args {string} User request arguments passed to the slash command.
380
381
  * @param[in] config {UseReqConfig} Effective project configuration supplying directory, git, static-check, and tool fields.
382
+ * @param[in] contextFileSizes {Record<string, ContextFileSizeFacts> | undefined} Optional measured context-file size facts keyed by canonical file name, consumed for `context files` size suffixes.
381
383
  * @return {string} Multi-line command invocation summary text.
382
- * @satisfies REQ-335, REQ-336, REQ-337, REQ-338, REQ-353
384
+ * @satisfies REQ-335, REQ-336, REQ-337, REQ-338, REQ-353, REQ-377
383
385
  */
384
386
  export function renderPromptCommandSummary(
385
387
  promptName: string,
386
388
  args: string,
387
389
  config: UseReqConfig,
390
+ contextFileSizes?: Record<string, ContextFileSizeFacts>,
388
391
  ): string {
389
392
  const contextFiles = CONTEXT_FILE_DESCRIPTORS
390
393
  .filter((descriptor) => config[descriptor.flagKey])
391
- .map((descriptor) => descriptor.fileName.replace(/\.md$/, "").toLowerCase())
394
+ .map((descriptor) => {
395
+ const baseName = descriptor.fileName.replace(/\.md$/, "").toLowerCase();
396
+ const facts = contextFileSizes?.[descriptor.fileName];
397
+ return facts?.exists ? `${baseName}(${formatContextFileSize(facts)})` : baseName;
398
+ })
392
399
  .join(", ") || "none";
393
400
  const enabledLanguages = DEFAULT_STATIC_CHECK_LANGUAGES
394
401
  .filter((language) => config["static-check"][language]?.enabled === "enable")
@@ -404,7 +404,7 @@ export class StaticCheckCommand extends StaticCheckBase {
404
404
  throw new ReqError(`Error: external command '${cmd}' not found on PATH.`, 1);
405
405
  }
406
406
  super(inputs, extraArgs, failOnly);
407
- this.cmd = cmd.includes("%%INSTALLATION_PATH%%") ? resolvedCmd : cmd;
407
+ this.cmd = resolvedCmd;
408
408
  this.label = `Command[${cmd}]`;
409
409
  }
410
410
 
package/src/index.ts CHANGED
@@ -83,6 +83,12 @@ import {
83
83
  normalizeEnabledPiUsereqTools,
84
84
  type PiUsereqStartupToolName,
85
85
  } from "./core/pi-usereq-tools.js";
86
+ import {
87
+ formatContextFileSize,
88
+ measureContextFileSizes,
89
+ type ContextFileName,
90
+ type ContextFileSizeFacts,
91
+ } from "./core/context-file-size.js";
86
92
  import {
87
93
  PROMPT_COMMAND_SUMMARY_CUSTOM_TYPE,
88
94
  renderPrompt,
@@ -95,8 +101,11 @@ import {
95
101
  finalizePromptCommandExecution,
96
102
  getPromptCommandErrorContext,
97
103
  preparePromptCommandExecution,
104
+ reapplyPromptCommandSessionSelection,
98
105
  restorePromptCommandExecution,
99
106
  type PromptCommandExecutionPlan,
107
+ type PromptCommandModelSelection,
108
+ type PromptCommandModelSelectionSurface,
100
109
  } from "./core/prompt-command-runtime.js";
101
110
  import {
102
111
  REQ_REFERENCES_COMMAND_DESCRIPTION,
@@ -1301,13 +1310,15 @@ function isPiAgentSettledEventSupported(
1301
1310
  /**
1302
1311
  * @brief Finalizes a matched successful worktree-backed prompt at the current lifecycle point.
1303
1312
  * @details Executes the deferred stash-assisted merge, transcript preservation, base-path restore, and worktree plus branch deletion through `finalizePromptCommandExecution(...)`, surfaces `error` or warning-only notifications, clears the pending finalization outcome plus prompt state, and transitions workflow state through `merging` to `idle`. Reused by the `agent_settled` handler on 0.80.4+ hosts and directly by the `agent_end` fallback on legacy hosts that never emit `agent_settled`. Runtime is dominated by git finalization. Side effects include branch merges, worktree deletion, notifications, and workflow-state transitions.
1313
+ * @param[in] pi {ExtensionAPI} Active extension API instance supplying the model re-apply appliers.
1304
1314
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller whose pending and active prompt state is cleared.
1305
1315
  * @param[in] promptRequest {PiUsereqPromptRequest} Matched successful worktree-backed prompt execution plan.
1306
1316
  * @param[in] ctx {ExtensionContext} Active extension context used for finalization and notifications.
1307
1317
  * @return {Promise<void>} Promise resolved when finalization and state transitions complete.
1308
- * @satisfies REQ-208, REQ-228, REQ-229, REQ-230, REQ-282, REQ-291, REQ-292, REQ-354, REQ-355
1318
+ * @satisfies REQ-208, REQ-228, REQ-229, REQ-230, REQ-282, REQ-291, REQ-292, REQ-354, REQ-355, REQ-368, REQ-370, REQ-371, REQ-372
1309
1319
  */
1310
1320
  async function finalizeMatchedPromptSuccess(
1321
+ pi: ExtensionAPI,
1311
1322
  statusController: PiUsereqStatusController,
1312
1323
  promptRequest: PiUsereqPromptRequest,
1313
1324
  ctx: ExtensionContext,
@@ -1333,6 +1344,14 @@ async function finalizeMatchedPromptSuccess(
1333
1344
  : undefined,
1334
1345
  );
1335
1346
  promptContext = (finalization.activeContext ?? promptContext) as typeof ctx;
1347
+ if (finalization.cleanupSucceeded && !finalization.errorMessage) {
1348
+ await reapplyCapturedPromptModelSelection(
1349
+ promptRequest.originalModel,
1350
+ promptRequest.originalThinkingLevel,
1351
+ pi,
1352
+ promptContext,
1353
+ );
1354
+ }
1336
1355
  } catch (error) {
1337
1356
  promptContext = (getPromptCommandErrorContext(error) ?? promptContext) as typeof ctx;
1338
1357
  let errorMessage = error instanceof Error ? error.message : String(error);
@@ -1346,6 +1365,12 @@ async function finalizeMatchedPromptSuccess(
1346
1365
  : undefined,
1347
1366
  ) ?? promptContext) as typeof ctx;
1348
1367
  cleanupSucceeded = true;
1368
+ await reapplyCapturedPromptModelSelection(
1369
+ promptRequest.originalModel,
1370
+ promptRequest.originalThinkingLevel,
1371
+ pi,
1372
+ promptContext,
1373
+ );
1349
1374
  } catch (restoreError) {
1350
1375
  promptContext = (getPromptCommandErrorContext(restoreError) ?? promptContext) as typeof ctx;
1351
1376
  errorMessage = restoreError instanceof Error ? restoreError.message : String(restoreError);
@@ -1451,6 +1476,12 @@ async function handleExtensionStatusEvent(
1451
1476
  const requestForActivation = activePromptRequest ?? pendingPromptRequest;
1452
1477
  if (requestForActivation !== undefined) {
1453
1478
  await activatePromptCommandExecution(requestForActivation, ctx);
1479
+ await reapplyCapturedPromptModelSelection(
1480
+ requestForActivation.originalModel,
1481
+ requestForActivation.originalThinkingLevel,
1482
+ pi,
1483
+ ctx,
1484
+ );
1454
1485
  }
1455
1486
  }
1456
1487
  const notifyRequest: PiNotifyEventRequest | undefined = hookName === "agent_end"
@@ -1555,6 +1586,7 @@ async function handleExtensionStatusEvent(
1555
1586
  // directly at `agent_end` to avoid parking in `merging` forever.
1556
1587
  statusController.state.pendingFinalizationOutcome = outcome;
1557
1588
  await finalizeMatchedPromptSuccess(
1589
+ pi,
1558
1590
  statusController,
1559
1591
  activePromptRequest,
1560
1592
  promptContext,
@@ -1635,7 +1667,7 @@ async function handleExtensionStatusEvent(
1635
1667
  && settledPromptRequest.worktreeDir !== undefined
1636
1668
  && pendingOutcome === "completed"
1637
1669
  ) {
1638
- await finalizeMatchedPromptSuccess(statusController, settledPromptRequest, ctx);
1670
+ await finalizeMatchedPromptSuccess(pi, statusController, settledPromptRequest, ctx);
1639
1671
  }
1640
1672
  }
1641
1673
  if (hookName === "session_shutdown") {
@@ -3041,6 +3073,139 @@ function registerPiNotifyShortcut(
3041
3073
  });
3042
3074
  }
3043
3075
 
3076
+ /**
3077
+ * @brief Describes the loosely-typed pi extension API surface required by model re-application.
3078
+ * @details Narrows the runtime API to the model appliers and thinking-level probe accessed defensively so legacy hosts and offline harnesses without `setModel`, `setThinkingLevel`, or `getThinkingLevel` degrade to a documented warning instead of a runtime exception. The alias is compile-time only and introduces no runtime cost.
3079
+ */
3080
+ type PromptModelSelectionApiSurface = {
3081
+ setModel?: (model: unknown) => unknown;
3082
+ setThinkingLevel?: (level: unknown) => unknown;
3083
+ getThinkingLevel?: () => unknown;
3084
+ };
3085
+
3086
+ /**
3087
+ * @brief Describes the loosely-typed context surface required by model re-application.
3088
+ * @details Narrows extension contexts to the active model, thinking level, and model registry probe used to capture the pre-switch selection and to evaluate the duplicate-skip guard after session replacement. The alias is compile-time only and introduces no runtime cost.
3089
+ */
3090
+ type PromptModelSelectionContextSurface = {
3091
+ model?: unknown;
3092
+ thinkingLevel?: unknown;
3093
+ modelRegistry?: { find?: (provider: unknown, modelId: unknown) => unknown };
3094
+ };
3095
+
3096
+ /**
3097
+ * @brief Captures the active model provider and identifier from one loosely-typed context model.
3098
+ * @details Validates that the runtime model object exposes non-empty string provider and identifier fields and emits the serializable selection fact stored inside the prompt execution plan, returning undefined when the runtime exposes no usable model so preflight capture never throws. Runtime is O(1). No external state is mutated.
3099
+ * @param[in] model {unknown} Active runtime model object from `ctx.model`.
3100
+ * @return {PromptCommandModelSelection | undefined} Serializable provider plus identifier fact, or undefined when unavailable.
3101
+ * @satisfies REQ-366
3102
+ */
3103
+ function resolveCurrentModelSelection(
3104
+ model: unknown,
3105
+ ): PromptCommandModelSelection | undefined {
3106
+ const candidate = model as { provider?: unknown; id?: unknown } | null | undefined;
3107
+ if (candidate === null || candidate === undefined) {
3108
+ return undefined;
3109
+ }
3110
+ if (
3111
+ typeof candidate.provider !== "string"
3112
+ || typeof candidate.id !== "string"
3113
+ || candidate.provider === ""
3114
+ || candidate.id === ""
3115
+ ) {
3116
+ return undefined;
3117
+ }
3118
+ return { provider: candidate.provider, modelId: candidate.id };
3119
+ }
3120
+
3121
+ /**
3122
+ * @brief Captures the active thinking level from the runtime API or fallback context.
3123
+ * @details Prefers `pi.getThinkingLevel()` when the host exposes it and falls back to the context `thinkingLevel` probe, returning undefined for legacy hosts or offline harnesses that provide neither so capture stays advisory. Runtime is O(1). No external state is mutated.
3124
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
3125
+ * @param[in] ctx {unknown} Active extension or command context used as fallback probe.
3126
+ * @return {string | undefined} Active thinking level, or undefined when unavailable.
3127
+ * @satisfies REQ-366, REQ-371
3128
+ */
3129
+ function resolveCurrentThinkingLevel(pi: ExtensionAPI, ctx: unknown): string | undefined {
3130
+ const api = pi as unknown as PromptModelSelectionApiSurface;
3131
+ if (typeof api.getThinkingLevel === "function") {
3132
+ const level = api.getThinkingLevel();
3133
+ if (typeof level === "string" && level !== "") {
3134
+ return level;
3135
+ }
3136
+ }
3137
+ const context = ctx as PromptModelSelectionContextSurface | null | undefined;
3138
+ if (
3139
+ context !== null
3140
+ && context !== undefined
3141
+ && typeof context.thinkingLevel === "string"
3142
+ && context.thinkingLevel !== ""
3143
+ ) {
3144
+ return context.thinkingLevel;
3145
+ }
3146
+ return undefined;
3147
+ }
3148
+
3149
+ /**
3150
+ * @brief Builds the loosely-typed model-selection surface from the runtime API plus context.
3151
+ * @details Wraps `setModel` and `setThinkingLevel` with defensive function probes so the surface only exposes appliers the running host actually implements, and copies the context model, thinking level, and model registry used by the guarded re-apply comparison. Returns undefined when no context is available so re-application degrades to a not-attempted skip. Runtime is O(1). No external state is mutated.
3152
+ * @param[in] pi {ExtensionAPI} Active extension API instance supplying the session-scoped model appliers.
3153
+ * @param[in] ctx {unknown} Active extension or command context supplying model probes.
3154
+ * @return {PromptCommandModelSelectionSurface | undefined} Re-apply surface, or undefined when no context exists.
3155
+ * @satisfies REQ-367, REQ-370, REQ-371
3156
+ */
3157
+ function resolvePromptCommandModelSurface(
3158
+ pi: ExtensionAPI,
3159
+ ctx: unknown,
3160
+ ): PromptCommandModelSelectionSurface | undefined {
3161
+ const context = ctx as PromptModelSelectionContextSurface | null | undefined;
3162
+ if (context === null || context === undefined) {
3163
+ return undefined;
3164
+ }
3165
+ const api = pi as unknown as PromptModelSelectionApiSurface;
3166
+ const setModelFn = api.setModel;
3167
+ const setThinkingLevelFn = api.setThinkingLevel;
3168
+ return {
3169
+ modelRegistry: context.modelRegistry,
3170
+ model: context.model,
3171
+ thinkingLevel: context.thinkingLevel,
3172
+ setModel: typeof setModelFn === "function"
3173
+ ? (model: unknown) => setModelFn.call(pi, model) as Promise<boolean>
3174
+ : undefined,
3175
+ setThinkingLevel: typeof setThinkingLevelFn === "function"
3176
+ ? (level: unknown) => {
3177
+ setThinkingLevelFn.call(pi, level);
3178
+ }
3179
+ : undefined,
3180
+ };
3181
+ }
3182
+
3183
+ /**
3184
+ * @brief Re-applies one captured model selection onto the active post-switch session.
3185
+ * @details Builds the guarded re-apply surface from the runtime API plus context, forwards the captured selection plus thinking level into `reapplyPromptCommandSessionSelection(...)`, and routes every warning through `notifyContextSafely(...)` so stale replacement contexts after session replacement never abort orchestration. Runtime is O(1) plus one awaited model mutation. Side effects include session-scoped model and thinking-level mutation plus stale-safe warning notifications.
3186
+ * @param[in] selection {PromptCommandModelSelection | undefined} Captured model provider plus identifier.
3187
+ * @param[in] thinkingLevel {string | undefined} Captured thinking level.
3188
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
3189
+ * @param[in] ctx {unknown} Active extension or command context used for probes and warning delivery.
3190
+ * @return {Promise<void>} Promise resolved once the guarded re-application completed.
3191
+ * @satisfies REQ-367, REQ-368, REQ-369, REQ-370, REQ-371, REQ-372
3192
+ */
3193
+ async function reapplyCapturedPromptModelSelection(
3194
+ selection: PromptCommandModelSelection | undefined,
3195
+ thinkingLevel: string | undefined,
3196
+ pi: ExtensionAPI,
3197
+ ctx: unknown,
3198
+ ): Promise<void> {
3199
+ await reapplyPromptCommandSessionSelection({
3200
+ selection,
3201
+ thinkingLevel,
3202
+ surface: resolvePromptCommandModelSurface(pi, ctx),
3203
+ notifyWarning: (message) => {
3204
+ notifyContextSafely(ctx as ExtensionContext, message, "error");
3205
+ },
3206
+ });
3207
+ }
3208
+
3044
3209
  /**
3045
3210
  * @brief Resolves the prompt execution plan targeted by `req-reset` recovery.
3046
3211
  * @details Prefers the current in-memory active request, then the current in-memory pending request, then the process-scoped persisted prompt runtime state so the dedicated reset command can recover from same-host unclean prompt termination after session replacement. Runtime is O(1). No external state is mutated.
@@ -3091,6 +3256,10 @@ function registerReqResetCommand(
3091
3256
  description: REQ_RESET_COMMAND_DESCRIPTION,
3092
3257
  handler: async (_args, ctx) => {
3093
3258
  const promptRequest = resolveReqResetPromptRequest(statusController);
3259
+ const resetModelSelection = promptRequest?.originalModel
3260
+ ?? resolveCurrentModelSelection((ctx as unknown as PromptModelSelectionContextSurface).model);
3261
+ const resetThinkingLevel = promptRequest?.originalThinkingLevel
3262
+ ?? resolveCurrentThinkingLevel(pi, ctx);
3094
3263
  const commandCwd = resolveLiveBootstrapCwd(ctx.cwd);
3095
3264
  syncContextCwdMirror(ctx, commandCwd);
3096
3265
  const projectBase = path.resolve(promptRequest?.basePath ?? getProjectBase(commandCwd));
@@ -3109,6 +3278,12 @@ function registerReqResetCommand(
3109
3278
  executionPlan = prepareReqResetCommandExecution(projectBase, config, promptRequest);
3110
3279
  executionResult = await executeReqResetCommandExecution(executionPlan, ctx);
3111
3280
  resetContext = (executionResult.activeContext ?? resetContext) as typeof ctx;
3281
+ await reapplyCapturedPromptModelSelection(
3282
+ resetModelSelection,
3283
+ resetThinkingLevel,
3284
+ pi,
3285
+ resetContext,
3286
+ );
3112
3287
  } catch (error) {
3113
3288
  const message = error instanceof Error ? error.message : String(error);
3114
3289
  setPiUsereqWorkflowState(statusController, "error", resetContext);
@@ -3197,7 +3372,7 @@ function registerReqReferencesCommand(
3197
3372
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3198
3373
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
3199
3374
  * @return {void} No return value.
3200
- * @satisfies REQ-004, REQ-067, REQ-068, REQ-169, REQ-200, REQ-201, REQ-202, REQ-203, REQ-206, REQ-207, REQ-219, REQ-220, REQ-221, REQ-224, REQ-225, REQ-226, REQ-227, REQ-245, REQ-246, REQ-247, REQ-277, REQ-281
3375
+ * @satisfies REQ-004, REQ-067, REQ-068, REQ-169, REQ-200, REQ-201, REQ-202, REQ-203, REQ-206, REQ-207, REQ-219, REQ-220, REQ-221, REQ-224, REQ-225, REQ-226, REQ-227, REQ-245, REQ-246, REQ-247, REQ-277, REQ-281, REQ-377
3201
3376
  */
3202
3377
  function registerPromptCommands(
3203
3378
  pi: ExtensionAPI,
@@ -3228,6 +3403,10 @@ function registerPromptCommands(
3228
3403
  ensureBundledResourcesAccessible();
3229
3404
  let executionPlan: PromptCommandExecutionPlan | undefined;
3230
3405
  let promptContext = ctx;
3406
+ const originalModel = resolveCurrentModelSelection(
3407
+ (ctx as unknown as PromptModelSelectionContextSurface).model,
3408
+ );
3409
+ const originalThinkingLevel = resolveCurrentThinkingLevel(pi, ctx);
3231
3410
  try {
3232
3411
  executionPlan = preparePromptCommandExecution(
3233
3412
  promptName,
@@ -3241,6 +3420,8 @@ function registerPromptCommands(
3241
3420
  config,
3242
3421
  workflowState: statusController.state.workflowState,
3243
3422
  },
3423
+ originalModel,
3424
+ originalThinkingLevel,
3244
3425
  );
3245
3426
  const content = renderPrompt(
3246
3427
  promptName,
@@ -3256,6 +3437,12 @@ function registerPromptCommands(
3256
3437
  activePromptRequest: statusController.state.activePromptRequest,
3257
3438
  });
3258
3439
  promptContext = (await activatePromptCommandExecution(executionPlan, ctx) ?? ctx) as typeof ctx;
3440
+ await reapplyCapturedPromptModelSelection(
3441
+ executionPlan.originalModel,
3442
+ executionPlan.originalThinkingLevel,
3443
+ pi,
3444
+ promptContext,
3445
+ );
3259
3446
  logPromptWorkflowEvent(
3260
3447
  projectBase,
3261
3448
  config,
@@ -3277,6 +3464,7 @@ function registerPromptCommands(
3277
3464
  promptName,
3278
3465
  args,
3279
3466
  config,
3467
+ measureContextFileSizes(projectBase, config),
3280
3468
  );
3281
3469
  const promptDelivery = deliverPromptCommand(pi, content, commandSummary, promptContext);
3282
3470
  transitionPromptWorkflowState(
@@ -4352,46 +4540,68 @@ async function configureStaticCheckMenu(
4352
4540
  }
4353
4541
 
4354
4542
  /**
4355
- * @brief Summarizes the `Context Files` flag state for the top-level menu value column.
4356
- * @details Renders the three context-file flags as compact `name:on|off` segments in the documented order so the top-level row reflects the current injection configuration. Runtime is O(1). No external state is mutated.
4543
+ * @brief Summarizes the `Context Files` flag state with measured sizes for the top-level menu value column.
4544
+ * @details Renders the three context-file flags as compact `name:on|off` segments in the documented order, appending `(<chars>c/<tokens>t)` measured size facts to every enabled segment so the top-level row exposes a runtime estimate of the injected context payload while disabled segments stay plain `name:off`. Runtime is O(1) in segment count. No external state is mutated.
4357
4545
  * @param[in] config {UseReqConfig} Effective project configuration.
4546
+ * @param[in] sizes {Record<ContextFileName, ContextFileSizeFacts>} Measured context-file size facts keyed by canonical file name.
4358
4547
  * @return {string} Compact `Context Files` summary string.
4359
- * @satisfies REQ-327
4548
+ * @satisfies REQ-327, REQ-376
4360
4549
  */
4361
- function formatContextFilesSummary(config: UseReqConfig): string {
4362
- return `requirements:${config["context-files-requirements"] ? "on" : "off"} \u2022 references:${config["context-files-references"] ? "on" : "off"} \u2022 workflow:${config["context-files-workflow"] ? "on" : "off"}`;
4550
+ function formatContextFilesSummary(
4551
+ config: UseReqConfig,
4552
+ sizes: Record<ContextFileName, ContextFileSizeFacts>,
4553
+ ): string {
4554
+ const segments = [
4555
+ `requirements:${config["context-files-requirements"] ? `on(${formatContextFileSize(sizes["REQUIREMENTS.md"])})` : "off"}`,
4556
+ `references:${config["context-files-references"] ? `on(${formatContextFileSize(sizes["REFERENCES.md"])})` : "off"}`,
4557
+ `workflow:${config["context-files-workflow"] ? `on(${formatContextFileSize(sizes["WORKFLOW.md"])})` : "off"}`,
4558
+ ];
4559
+ return segments.join(" \u2022 ");
4363
4560
  }
4364
4561
 
4365
4562
  /**
4366
4563
  * @brief Builds the shared settings-menu choices for the `Context Files` submenu.
4367
- * @details Exposes one inline on|off toggle row per context file in the documented `REQUIREMENTS.md`, `REFERENCES.md`, `WORKFLOW.md` order plus a value-less subtree-local `Reset defaults` row. Runtime is O(1). No external state is mutated.
4564
+ * @details Exposes one inline toggle row per context file in the documented `REQUIREMENTS.md`, `REFERENCES.md`, `WORKFLOW.md` order whose value renders `on|off • <chars>c/<tokens>t` measured size facts plus a value-less subtree-local `Reset defaults` row. Cycle values embed the same measured facts so inline toggling keeps the size estimate visible while persisting the on|off state. Runtime is O(1) in row count. No external state is mutated.
4368
4565
  * @param[in] config {UseReqConfig} Effective project configuration.
4566
+ * @param[in] sizes {Record<ContextFileName, ContextFileSizeFacts>} Measured context-file size facts keyed by canonical file name.
4369
4567
  * @return {PiUsereqSettingsMenuChoice[]} Ordered `Context Files` submenu choices.
4370
- * @satisfies REQ-327, REQ-328, REQ-333
4568
+ * @satisfies REQ-327, REQ-328, REQ-333, REQ-375
4371
4569
  */
4372
- function buildContextFilesMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
4570
+ function buildContextFilesMenuChoices(
4571
+ config: UseReqConfig,
4572
+ sizes: Record<ContextFileName, ContextFileSizeFacts>,
4573
+ ): PiUsereqSettingsMenuChoice[] {
4574
+ const buildRow = (
4575
+ id: "context-files-requirements" | "context-files-references" | "context-files-workflow",
4576
+ fileName: ContextFileName,
4577
+ description: string,
4578
+ ): PiUsereqSettingsMenuChoice => {
4579
+ const sizedOn = `on \u2022 ${formatContextFileSize(sizes[fileName])}`;
4580
+ const sizedOff = `off \u2022 ${formatContextFileSize(sizes[fileName])}`;
4581
+ return {
4582
+ id,
4583
+ label: fileName,
4584
+ value: config[id] ? sizedOn : sizedOff,
4585
+ values: [sizedOn, sizedOff],
4586
+ description,
4587
+ };
4588
+ };
4373
4589
  return [
4374
- {
4375
- id: "context-files-requirements",
4376
- label: "REQUIREMENTS.md",
4377
- value: config["context-files-requirements"] ? "on" : "off",
4378
- values: ["on", "off"],
4379
- description: "Toggle injection of REQUIREMENTS.md into the prompt context through `%%CONTEXT_FILES%%`.",
4380
- },
4381
- {
4382
- id: "context-files-references",
4383
- label: "REFERENCES.md",
4384
- value: config["context-files-references"] ? "on" : "off",
4385
- values: ["on", "off"],
4386
- description: "Toggle injection of REFERENCES.md into the prompt context through `%%CONTEXT_FILES%%`.",
4387
- },
4388
- {
4389
- id: "context-files-workflow",
4390
- label: "WORKFLOW.md",
4391
- value: config["context-files-workflow"] ? "on" : "off",
4392
- values: ["on", "off"],
4393
- description: "Toggle injection of WORKFLOW.md into the prompt context through `%%CONTEXT_FILES%%`.",
4394
- },
4590
+ buildRow(
4591
+ "context-files-requirements",
4592
+ "REQUIREMENTS.md",
4593
+ "Toggle injection of REQUIREMENTS.md into the prompt context through `%%CONTEXT_FILES%%`.",
4594
+ ),
4595
+ buildRow(
4596
+ "context-files-references",
4597
+ "REFERENCES.md",
4598
+ "Toggle injection of REFERENCES.md into the prompt context through `%%CONTEXT_FILES%%`.",
4599
+ ),
4600
+ buildRow(
4601
+ "context-files-workflow",
4602
+ "WORKFLOW.md",
4603
+ "Toggle injection of WORKFLOW.md into the prompt context through `%%CONTEXT_FILES%%`.",
4604
+ ),
4395
4605
  ...buildTerminalSettingsMenuChoices({
4396
4606
  resetDefaultsDescription: "Restore the default Context Files configuration (all three files enabled).",
4397
4607
  }),
@@ -4412,6 +4622,7 @@ async function configureContextFilesMenu(
4412
4622
  config: UseReqConfig,
4413
4623
  onConfigChange: () => void,
4414
4624
  ): Promise<void> {
4625
+ const contextFileSizes = measureContextFileSizes(getProjectBase(ctx.cwd), config);
4415
4626
  const setFlag = (flagKey: "context-files-requirements" | "context-files-references" | "context-files-workflow", enabled: boolean): void => {
4416
4627
  config[flagKey] = enabled;
4417
4628
  onConfigChange();
@@ -4419,20 +4630,20 @@ async function configureContextFilesMenu(
4419
4630
  };
4420
4631
  let focusedChoiceId: string | undefined;
4421
4632
  while (true) {
4422
- const choice = await showPiUsereqSettingsMenu(ctx, "Context Files", buildContextFilesMenuChoices(config), {
4633
+ const choice = await showPiUsereqSettingsMenu(ctx, "Context Files", buildContextFilesMenuChoices(config, contextFileSizes), {
4423
4634
  initialSelectedId: focusedChoiceId,
4424
- getChoices: () => buildContextFilesMenuChoices(config),
4635
+ getChoices: () => buildContextFilesMenuChoices(config, contextFileSizes),
4425
4636
  onChange: (choiceId, newValue) => {
4426
4637
  if (choiceId === "context-files-requirements") {
4427
- setFlag("context-files-requirements", newValue === "on");
4638
+ setFlag("context-files-requirements", newValue.startsWith("on"));
4428
4639
  return;
4429
4640
  }
4430
4641
  if (choiceId === "context-files-references") {
4431
- setFlag("context-files-references", newValue === "on");
4642
+ setFlag("context-files-references", newValue.startsWith("on"));
4432
4643
  return;
4433
4644
  }
4434
4645
  if (choiceId === "context-files-workflow") {
4435
- setFlag("context-files-workflow", newValue === "on");
4646
+ setFlag("context-files-workflow", newValue.startsWith("on"));
4436
4647
  }
4437
4648
  },
4438
4649
  });
@@ -4473,7 +4684,7 @@ async function configureContextFilesMenu(
4473
4684
  * @param[in] cwd {string} Current working directory.
4474
4685
  * @param[in] config {UseReqConfig} Effective project configuration.
4475
4686
  * @return {PiUsereqSettingsMenuChoice[]} Ordered top-level menu choices.
4476
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162, REQ-190, REQ-191, REQ-197, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-314, REQ-318, REQ-319, REQ-320, REQ-326
4687
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162, REQ-190, REQ-191, REQ-197, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-314, REQ-318, REQ-319, REQ-320, REQ-326, REQ-376
4477
4688
  */
4478
4689
  function buildPiUsereqMenuChoices(
4479
4690
  cwd: string,
@@ -4506,7 +4717,7 @@ function buildPiUsereqMenuChoices(
4506
4717
  {
4507
4718
  id: "context-files",
4508
4719
  label: "Context Files",
4509
- value: formatContextFilesSummary(config),
4720
+ value: formatContextFilesSummary(config, measureContextFileSizes(cwd, config)),
4510
4721
  description: "Toggle injection of REQUIREMENTS.md, REFERENCES.md, and WORKFLOW.md into prompt context through `%%CONTEXT_FILES%%`.",
4511
4722
  },
4512
4723
  {
@@ -4863,7 +5074,7 @@ async function configurePiUsereq(
4863
5074
  { label: "Document directory", previousValue: config["docs-dir"], nextValue: defaultConfig["docs-dir"] },
4864
5075
  { label: "Source-code directories", previousValue: config["src-dir"].join(", "), nextValue: defaultConfig["src-dir"].join(", ") },
4865
5076
  { label: "Unit tests directory", previousValue: config["tests-dir"], nextValue: defaultConfig["tests-dir"] },
4866
- { label: "Context Files", previousValue: formatContextFilesSummary(config), nextValue: formatContextFilesSummary(defaultConfig) },
5077
+ { label: "Context Files", previousValue: formatContextFilesSummary(config, measureContextFileSizes(ctx.cwd, config)), nextValue: formatContextFilesSummary(defaultConfig, measureContextFileSizes(ctx.cwd, defaultConfig)) },
4867
5078
  { label: "Auto git commit", previousValue: config.AUTO_GIT_COMMIT, nextValue: defaultConfig.AUTO_GIT_COMMIT },
4868
5079
  { label: "Git worktree", previousValue: config.GIT_WORKTREE_ENABLED, nextValue: defaultConfig.GIT_WORKTREE_ENABLED },
4869
5080
  { label: "Worktree prefix", previousValue: config.GIT_WORKTREE_PREFIX, nextValue: defaultConfig.GIT_WORKTREE_PREFIX },