pi-usereq 0.64.0 → 0.65.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 +1 -1
- package/package.json +1 -1
- package/scripts/pi-cli-update-src-docs.sh +370 -0
- package/scripts/pi-prompts-update.sh +113 -15
- package/src/core/context-file-size.ts +204 -7
- package/src/core/extension-status.ts +16 -0
- package/src/core/prompt-command-runtime.ts +7 -3
- package/src/core/prompts.ts +12 -5
- package/src/core/token-counter.ts +26 -6
- package/src/index.ts +41 -18
- package/src/resources/pi-prompts-version.txt +1 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# @file scripts/pi-cli-update-src-docs.sh
|
|
3
|
+
# @brief Updates read-only pi CLI reference sources and docs from the latest pi CLI release.
|
|
4
|
+
# @details Clones the official pi CLI repository (https://github.com/earendil-works/pi),
|
|
5
|
+
# resolves the newest release tag reachable from the upstream default branch, and
|
|
6
|
+
# performs a full one-way synchronization of the upstream repository root into the
|
|
7
|
+
# local read-only `pi.dev-src/pi` target. Every dot-prefixed upstream entry
|
|
8
|
+
# (`.git`, `.github`, `.gitignore`, and any other file or directory whose name
|
|
9
|
+
# starts with `.`) is excluded at every tree level. Full synchronization
|
|
10
|
+
# overwrites every present file and removes every file that no longer exists
|
|
11
|
+
# upstream by replacing each target directory with a freshly staged copy.
|
|
12
|
+
# Upstream file access and modification timestamps are preserved through `cp -p`.
|
|
13
|
+
# After the source synchronization completes, three documentation targets are
|
|
14
|
+
# refreshed from the synchronized sources: `docs/pi.dev/coding-agent-docs`
|
|
15
|
+
# from `pi.dev-src/pi/packages/coding-agent/docs`,
|
|
16
|
+
# `docs/pi.dev/coding-agent-examples` from
|
|
17
|
+
# `pi.dev-src/pi/packages/coding-agent/examples`, and `docs/pi.dev/durable-docs`
|
|
18
|
+
# from `pi.dev-src/pi/packages/durable/docs`. Existing target files whose
|
|
19
|
+
# bytes differ from the upstream candidate only in carriage-return
|
|
20
|
+
# line-ending characters are preserved with their committed bytes and
|
|
21
|
+
# timestamps, so repeated updates against an unchanged upstream release
|
|
22
|
+
# leave the git working tree clean. Before any modification, the
|
|
23
|
+
# updater prints the resolved latest version and the stored versions read from
|
|
24
|
+
# `docs/pi.dev/pi-cli-version.txt` and `pi.dev-src/pi-cli-version.txt`
|
|
25
|
+
# (missing or empty files report `unknown`), then requires an explicit `Y`
|
|
26
|
+
# confirmation; any other input aborts without modifications. On success, the
|
|
27
|
+
# resolved version is written into both version marker files. Runtime is
|
|
28
|
+
# dominated by one git clone plus one tag checkout and per-file copies.
|
|
29
|
+
# Side effects include temporary-directory creation, git subprocesses, and
|
|
30
|
+
# replacement of the four target directories plus the two version marker
|
|
31
|
+
# files. No other repository content is modified.
|
|
32
|
+
# @satisfies DES-023, REQ-385, REQ-386, REQ-387, REQ-388, REQ-389, REQ-390, REQ-391, REQ-392, REQ-393, REQ-394, REQ-395
|
|
33
|
+
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
|
|
36
|
+
PI_REPO_URL_PRIMARY="https://github.com/earendil-works/pi.git"
|
|
37
|
+
PI_REPO_URL_FALLBACK="https://github.com/earendil-works/pi"
|
|
38
|
+
|
|
39
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
40
|
+
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
41
|
+
|
|
42
|
+
## @brief Stores the resolved read-only pi CLI source synchronization target.
|
|
43
|
+
## @details The updater replaces the whole content of `${REPO_ROOT}/${SRC_TARGET_REL}`
|
|
44
|
+
## with the non-dot upstream repository tree. The constant is immutable at
|
|
45
|
+
## runtime and costs O(1) per access.
|
|
46
|
+
SRC_TARGET_REL="pi.dev-src/pi"
|
|
47
|
+
|
|
48
|
+
## @brief Stores the ordered `source-relative:target-relative` documentation synchronization pairs.
|
|
49
|
+
## @details Each entry maps one directory inside the synchronized `${SRC_TARGET_REL}`
|
|
50
|
+
## tree to one `docs/pi.dev` target directory synchronized in declaration order
|
|
51
|
+
## after the source synchronization completes. The constant is immutable at
|
|
52
|
+
## runtime and costs O(1) per element access.
|
|
53
|
+
DOC_SYNC_SPECS=(
|
|
54
|
+
"pi.dev-src/pi/packages/coding-agent/docs:docs/pi.dev/coding-agent-docs"
|
|
55
|
+
"pi.dev-src/pi/packages/coding-agent/examples:docs/pi.dev/coding-agent-examples"
|
|
56
|
+
"pi.dev-src/pi/packages/durable/docs:docs/pi.dev/durable-docs"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
## @brief Stores the documentation version marker path relative to the repository root.
|
|
60
|
+
## @details Read before the update and overwritten with the resolved version after
|
|
61
|
+
## successful synchronization. The constant is immutable at runtime.
|
|
62
|
+
DOC_VERSION_REL="docs/pi.dev/pi-cli-version.txt"
|
|
63
|
+
|
|
64
|
+
## @brief Stores the source version marker path relative to the repository root.
|
|
65
|
+
## @details Read before the update and overwritten with the resolved version after
|
|
66
|
+
## successful synchronization. The constant is immutable at runtime.
|
|
67
|
+
SRC_VERSION_REL="pi.dev-src/pi-cli-version.txt"
|
|
68
|
+
|
|
69
|
+
WORK_DIR=""
|
|
70
|
+
STAGING_DIR=""
|
|
71
|
+
|
|
72
|
+
## @brief Writes one progress message to stdout.
|
|
73
|
+
## @details Emits the supplied message fragments joined by single spaces followed
|
|
74
|
+
## by one newline. Runtime is O(n) in message length. Side effect: writes to
|
|
75
|
+
## `stdout`.
|
|
76
|
+
## @param[in] ... {string} Progress message fragments.
|
|
77
|
+
## @return {void} No return value.
|
|
78
|
+
log() {
|
|
79
|
+
printf '%s\n' "$*"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
## @brief Aborts the updater with one deterministic diagnostic.
|
|
83
|
+
## @details Writes the supplied diagnostic prefixed with `ERROR: ` to `stderr`
|
|
84
|
+
## and terminates the process with exit code `1`. Runtime is O(n) in message
|
|
85
|
+
## length. Side effects include `stderr` output and process exit; the `EXIT`
|
|
86
|
+
## trap still removes updater temporary directories.
|
|
87
|
+
## @param[in] message {string} Failure diagnostic.
|
|
88
|
+
## @return {never} Never returns.
|
|
89
|
+
fail() {
|
|
90
|
+
printf 'ERROR: %s\n' "$*" >&2
|
|
91
|
+
exit 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
## @brief Removes updater temporary directories.
|
|
95
|
+
## @details Deletes the cloned upstream working tree and any pending staging
|
|
96
|
+
## directory registered in `WORK_DIR` plus `STAGING_DIR`. Invoked through the
|
|
97
|
+
## `EXIT` trap on every exit path. Runtime is O(1) plus filesystem removal
|
|
98
|
+
## cost. Side effects include directory removal.
|
|
99
|
+
## @return {void} No return value.
|
|
100
|
+
cleanup() {
|
|
101
|
+
if [ -n "${STAGING_DIR}" ]; then
|
|
102
|
+
rm -rf "${STAGING_DIR}"
|
|
103
|
+
fi
|
|
104
|
+
if [ -n "${WORK_DIR}" ]; then
|
|
105
|
+
rm -rf "${WORK_DIR}"
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
## @brief Verifies that one required external tool is available.
|
|
110
|
+
## @details Probes the executable search path for the requested tool name and
|
|
111
|
+
## aborts through `fail(...)` when the probe fails. Runtime is O(1) plus one
|
|
112
|
+
## `PATH` lookup. No external state is mutated.
|
|
113
|
+
## @param[in] tool {string} Executable name to probe.
|
|
114
|
+
## @return {void} No return value.
|
|
115
|
+
require_tool() {
|
|
116
|
+
command -v "$1" >/dev/null 2>&1 || fail "Required tool not found: $1"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
## @brief Reads one stored pi CLI version marker file.
|
|
120
|
+
## @details Returns the first line of the marker file with all surrounding
|
|
121
|
+
## whitespace removed when the file exists and is non-empty; otherwise
|
|
122
|
+
## returns the literal `unknown` so missing or empty version markers are
|
|
123
|
+
## handled deterministically. Runtime is O(n) in file size. Side effects are
|
|
124
|
+
## limited to filesystem reads.
|
|
125
|
+
## @param[in] marker_path {string} Absolute version marker file path.
|
|
126
|
+
## @return {string} Trimmed stored version or `unknown` on `stdout`.
|
|
127
|
+
read_version_file() {
|
|
128
|
+
local marker_path="$1"
|
|
129
|
+
local version=""
|
|
130
|
+
if [ -f "${marker_path}" ] && [ -s "${marker_path}" ]; then
|
|
131
|
+
version="$(head -n 1 "${marker_path}" | tr -d '[:space:]' || true)"
|
|
132
|
+
fi
|
|
133
|
+
if [ -z "${version}" ]; then
|
|
134
|
+
version="unknown"
|
|
135
|
+
fi
|
|
136
|
+
printf '%s\n' "${version}"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
## @brief Clones the upstream pi CLI repository default branch into one target directory.
|
|
140
|
+
## @details Attempts a quiet, no-checkout clone from `${PI_REPO_URL_PRIMARY}`,
|
|
141
|
+
## removes the partial destination on failure, and retries once with
|
|
142
|
+
## `${PI_REPO_URL_FALLBACK}`. Prints the resolved upstream URL on `stdout` so
|
|
143
|
+
## callers can capture provenance. Runtime is dominated by git subprocess
|
|
144
|
+
## execution. Side effects include destination-directory creation and removal.
|
|
145
|
+
## @param[in] destination {string} Target directory path for the clone.
|
|
146
|
+
## @return {string} Resolved upstream repository URL on stdout.
|
|
147
|
+
## @throws Exits through `fail(...)` when both upstream URLs are unreachable.
|
|
148
|
+
clone_upstream() {
|
|
149
|
+
local destination="$1"
|
|
150
|
+
rm -rf "${destination}"
|
|
151
|
+
if git clone --quiet --no-checkout "${PI_REPO_URL_PRIMARY}" "${destination}" 2>/dev/null; then
|
|
152
|
+
printf '%s\n' "${PI_REPO_URL_PRIMARY}"
|
|
153
|
+
return 0
|
|
154
|
+
fi
|
|
155
|
+
log "Primary clone failed, retrying with the fallback repository URL ..."
|
|
156
|
+
rm -rf "${destination}"
|
|
157
|
+
if git clone --quiet --no-checkout "${PI_REPO_URL_FALLBACK}" "${destination}" 2>/dev/null; then
|
|
158
|
+
printf '%s\n' "${PI_REPO_URL_FALLBACK}"
|
|
159
|
+
return 0
|
|
160
|
+
fi
|
|
161
|
+
fail "Unable to clone pi CLI from ${PI_REPO_URL_PRIMARY} or ${PI_REPO_URL_FALLBACK}."
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
## @brief Resolves the latest released pi CLI tag reachable from the default branch.
|
|
165
|
+
## @details Prefers the default branch reported by `refs/remotes/origin/HEAD` and
|
|
166
|
+
## falls back to `origin/master` then `origin/main` when the symbolic ref is
|
|
167
|
+
## absent. Selects the newest version-sorted tag matching `v[0-9]*` that is
|
|
168
|
+
## merged into the resolved default ref, which represents the latest release
|
|
169
|
+
## reachable from that branch. Prints the resolved tag on `stdout`. Runtime is
|
|
170
|
+
## dominated by git ref enumeration. No external state is mutated.
|
|
171
|
+
## @param[in] repository {string} Cloned upstream repository path.
|
|
172
|
+
## @return {string} Latest release tag name on stdout.
|
|
173
|
+
## @throws Exits through `fail(...)` when no release tag is resolvable.
|
|
174
|
+
resolve_release_tag() {
|
|
175
|
+
local repository="$1"
|
|
176
|
+
local default_ref=""
|
|
177
|
+
local release_tag=""
|
|
178
|
+
|
|
179
|
+
default_ref="$(git -C "${repository}" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || true)"
|
|
180
|
+
if [ -z "${default_ref}" ]; then
|
|
181
|
+
if git -C "${repository}" rev-parse --verify --quiet refs/remotes/origin/master >/dev/null 2>&1; then
|
|
182
|
+
default_ref="origin/master"
|
|
183
|
+
else
|
|
184
|
+
default_ref="origin/main"
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
release_tag="$(git -C "${repository}" tag --list 'v[0-9]*' --sort=-v:refname --merged "${default_ref}" 2>/dev/null | head -n 1 || true)"
|
|
189
|
+
if [ -z "${release_tag}" ]; then
|
|
190
|
+
fail "Unable to resolve the latest pi CLI release tag from ${default_ref}."
|
|
191
|
+
fi
|
|
192
|
+
printf '%s\n' "${release_tag}"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
## @brief Decides whether one staged upstream file must replace the existing target file.
|
|
196
|
+
## @details Returns success (replace) when the target file does not exist or when
|
|
197
|
+
## its bytes differ from the staged upstream file beyond line-ending
|
|
198
|
+
## characters. Returns failure (preserve) when both files are byte-equal or
|
|
199
|
+
## when both files are equal after removing every carriage-return byte, so
|
|
200
|
+
## committed reference bytes and timestamps stay stable when upstream
|
|
201
|
+
## releases change only line endings. Runtime is O(n) in compared file size
|
|
202
|
+
## plus at most three subprocesses. Side effects are limited to subprocess
|
|
203
|
+
## execution and stdin pipes; no file is mutated.
|
|
204
|
+
## @param[in] source_file {string} Absolute staged upstream candidate file path.
|
|
205
|
+
## @param[in] target_file {string} Absolute existing target file path.
|
|
206
|
+
## @return {integer} `0` when the staged file must replace the target; `1` when
|
|
207
|
+
## the existing target bytes must be preserved.
|
|
208
|
+
staged_copy_replaces_existing() {
|
|
209
|
+
local source_file="$1"
|
|
210
|
+
local target_file="$2"
|
|
211
|
+
local source_normalized=""
|
|
212
|
+
local target_normalized=""
|
|
213
|
+
|
|
214
|
+
if [ ! -f "${target_file}" ]; then
|
|
215
|
+
return 0
|
|
216
|
+
fi
|
|
217
|
+
if cmp -s -- "${source_file}" "${target_file}"; then
|
|
218
|
+
return 0
|
|
219
|
+
fi
|
|
220
|
+
source_normalized="$(tr -d '\r' < "${source_file}" | git hash-object --stdin)" || return 0
|
|
221
|
+
target_normalized="$(tr -d '\r' < "${target_file}" | git hash-object --stdin)" || return 0
|
|
222
|
+
[ "${source_normalized}" != "${target_normalized}" ]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
## @brief Fully synchronizes one upstream directory into one target directory.
|
|
226
|
+
## @details Stages every upstream entry below `source_dir` into a fresh sibling
|
|
227
|
+
## staging directory using timestamp-preserving copies, excluding every
|
|
228
|
+
## dot-prefixed entry and its whole subtree at every tree level, then
|
|
229
|
+
## replaces `target_dir` with the staged tree through a backup-and-swap
|
|
230
|
+
## sequence so files removed upstream disappear and existing files are
|
|
231
|
+
## overwritten with an exact file-set match. Existing target files
|
|
232
|
+
## whose bytes differ from the staged upstream file only in carriage-return
|
|
233
|
+
## line-ending characters are preserved through
|
|
234
|
+
## `staged_copy_replaces_existing(...)` so unchanged upstream releases do
|
|
235
|
+
## not dirty the git working tree. Runtime is dominated by per-file copy
|
|
236
|
+
## cost. Side effects include staging plus backup directory creation and
|
|
237
|
+
## removal, target-directory replacement, and one progress line on `stdout`.
|
|
238
|
+
## @param[in] source_dir {string} Absolute upstream source directory path.
|
|
239
|
+
## @param[in] target_dir {string} Absolute read-only target directory path.
|
|
240
|
+
## @return {void} No return value.
|
|
241
|
+
## @throws Exits through `fail(...)` when `source_dir` is not a directory.
|
|
242
|
+
sync_directory() {
|
|
243
|
+
local source_dir="$1"
|
|
244
|
+
local target_dir="$2"
|
|
245
|
+
|
|
246
|
+
[ -d "${source_dir}" ] || fail "Upstream directory not found: ${source_dir}"
|
|
247
|
+
|
|
248
|
+
STAGING_DIR="${target_dir}.staging.$$"
|
|
249
|
+
rm -rf "${STAGING_DIR}"
|
|
250
|
+
mkdir -p "${STAGING_DIR}"
|
|
251
|
+
|
|
252
|
+
local entry destination parent_dir copied=0
|
|
253
|
+
while IFS= read -r -d '' entry; do
|
|
254
|
+
entry="${entry#./}"
|
|
255
|
+
[ -n "${entry}" ] || continue
|
|
256
|
+
destination="${STAGING_DIR}/${entry}"
|
|
257
|
+
if [ -d "${source_dir}/${entry}" ]; then
|
|
258
|
+
mkdir -p "${destination}"
|
|
259
|
+
else
|
|
260
|
+
parent_dir="$(dirname "${destination}")"
|
|
261
|
+
mkdir -p "${parent_dir}"
|
|
262
|
+
if staged_copy_replaces_existing "${source_dir}/${entry}" "${target_dir}/${entry}"; then
|
|
263
|
+
cp -p "${source_dir}/${entry}" "${destination}"
|
|
264
|
+
else
|
|
265
|
+
cp -p "${target_dir}/${entry}" "${destination}"
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
268
|
+
copied=$((copied + 1))
|
|
269
|
+
done < <(cd "${source_dir}" && find . -mindepth 1 \( -name '.*' -prune \) -o -print0)
|
|
270
|
+
|
|
271
|
+
local backup_dir="${target_dir}.backup.$$"
|
|
272
|
+
rm -rf "${backup_dir}"
|
|
273
|
+
if [ -d "${target_dir}" ]; then
|
|
274
|
+
mv "${target_dir}" "${backup_dir}"
|
|
275
|
+
fi
|
|
276
|
+
mv "${STAGING_DIR}" "${target_dir}"
|
|
277
|
+
STAGING_DIR=""
|
|
278
|
+
rm -rf "${backup_dir}"
|
|
279
|
+
|
|
280
|
+
log "Synchronized ${copied} file(s) into ${target_dir#"${REPO_ROOT}"/}"
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
## @brief Requests explicit user confirmation before any modification.
|
|
284
|
+
## @details Prints the versions plus one English confirmation sentence, reads one
|
|
285
|
+
## line from `stdin`, and returns success only when the confirmation input is
|
|
286
|
+
## exactly `Y`; any other input, including end-of-stream input, returns
|
|
287
|
+
## failure so callers abort without modifications. Runtime is O(1) plus one
|
|
288
|
+
## user interaction. Side effects include `stdout` and `stderr` writes.
|
|
289
|
+
## @param[in] latest_version {string} Resolved upstream release version.
|
|
290
|
+
## @param[in] doc_version {string} Stored version of `${DOC_VERSION_REL}`.
|
|
291
|
+
## @param[in] src_version {string} Stored version of `${SRC_VERSION_REL}`.
|
|
292
|
+
## @return {integer} `0` when the user confirmed with exactly `Y`; `1` otherwise.
|
|
293
|
+
confirm_update() {
|
|
294
|
+
local latest_version="$1"
|
|
295
|
+
local doc_version="$2"
|
|
296
|
+
local src_version="$3"
|
|
297
|
+
local answer=""
|
|
298
|
+
|
|
299
|
+
log "Latest pi CLI release : ${latest_version}"
|
|
300
|
+
log "Stored version (${DOC_VERSION_REL}): ${doc_version}"
|
|
301
|
+
log "Stored version (${SRC_VERSION_REL}): ${src_version}"
|
|
302
|
+
log "This update replaces the read-only reference sources in ${SRC_TARGET_REL} and the documentation in docs/pi.dev with the content of the latest pi CLI release."
|
|
303
|
+
printf 'Proceed with the update? [Y/n] '
|
|
304
|
+
read -r answer || answer=""
|
|
305
|
+
[ "${answer}" = "Y" ]
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
## @brief Executes one full pi CLI reference update run.
|
|
309
|
+
## @details Validates required tools, allocates the temporary upstream working
|
|
310
|
+
## tree, clones the pi CLI default branch, resolves and checks out the newest
|
|
311
|
+
## release tag, prints the resolved plus stored versions, requires an explicit
|
|
312
|
+
## `Y` confirmation, synchronizes the upstream repository root into the
|
|
313
|
+
## read-only source target excluding every dot-prefixed entry, synchronizes
|
|
314
|
+
## every ordered documentation pair declared in `DOC_SYNC_SPECS`, and writes
|
|
315
|
+
## the resolved version into both version marker files. Runtime is dominated
|
|
316
|
+
## by the upstream clone plus the four directory synchronizations. Side
|
|
317
|
+
## effects include temporary-directory creation and removal, git subprocesses,
|
|
318
|
+
## and replacement of the four target directories plus the two version marker
|
|
319
|
+
## files.
|
|
320
|
+
## @return {void} No return value.
|
|
321
|
+
## @throws Exits through `fail(...)` when any required tool, clone, tag
|
|
322
|
+
## resolution, or synchronization step fails.
|
|
323
|
+
main() {
|
|
324
|
+
require_tool git
|
|
325
|
+
require_tool find
|
|
326
|
+
require_tool mkdir
|
|
327
|
+
require_tool cp
|
|
328
|
+
require_tool cmp
|
|
329
|
+
|
|
330
|
+
local doc_version src_version
|
|
331
|
+
doc_version="$(read_version_file "${REPO_ROOT}/${DOC_VERSION_REL}")"
|
|
332
|
+
src_version="$(read_version_file "${REPO_ROOT}/${SRC_VERSION_REL}")"
|
|
333
|
+
|
|
334
|
+
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/pi-cli-update-src-docs.XXXXXX")" \
|
|
335
|
+
|| fail "Unable to create the temporary working directory."
|
|
336
|
+
local repository="${WORK_DIR}/pi"
|
|
337
|
+
|
|
338
|
+
local upstream_url
|
|
339
|
+
upstream_url="$(clone_upstream "${repository}")"
|
|
340
|
+
log "Upstream repository: ${upstream_url}"
|
|
341
|
+
|
|
342
|
+
local release_tag
|
|
343
|
+
release_tag="$(resolve_release_tag "${repository}")"
|
|
344
|
+
|
|
345
|
+
if ! confirm_update "${release_tag}" "${doc_version}" "${src_version}"; then
|
|
346
|
+
log "Update aborted; no repository content was modified."
|
|
347
|
+
return 0
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
git -C "${repository}" checkout --quiet --detach "${release_tag}" \
|
|
351
|
+
|| fail "Unable to check out ${release_tag}."
|
|
352
|
+
log "Synchronizing from release: ${release_tag}"
|
|
353
|
+
|
|
354
|
+
sync_directory "${repository}" "${REPO_ROOT}/${SRC_TARGET_REL}"
|
|
355
|
+
|
|
356
|
+
local spec source_rel target_rel
|
|
357
|
+
for spec in "${DOC_SYNC_SPECS[@]}"; do
|
|
358
|
+
source_rel="${spec%%:*}"
|
|
359
|
+
target_rel="${spec#*:}"
|
|
360
|
+
sync_directory "${REPO_ROOT}/${source_rel}" "${REPO_ROOT}/${target_rel}"
|
|
361
|
+
done
|
|
362
|
+
|
|
363
|
+
printf '%s\n' "${release_tag}" > "${REPO_ROOT}/${DOC_VERSION_REL}"
|
|
364
|
+
printf '%s\n' "${release_tag}" > "${REPO_ROOT}/${SRC_VERSION_REL}"
|
|
365
|
+
|
|
366
|
+
log "pi CLI reference sources and documentation updated to ${release_tag}."
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
trap cleanup EXIT
|
|
370
|
+
main "$@"
|
|
@@ -8,13 +8,19 @@
|
|
|
8
8
|
# `src/resources/templates` targets. Full synchronization overwrites every
|
|
9
9
|
# present file and removes every file that no longer exists upstream by
|
|
10
10
|
# replacing each target directory with a freshly staged copy. Upstream file
|
|
11
|
-
# access and modification timestamps are preserved through `cp -p`.
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
11
|
+
# access and modification timestamps are preserved through `cp -p`. Before
|
|
12
|
+
# any modification, the updater prints the resolved latest PI-Prompts
|
|
13
|
+
# release version and the stored version read from
|
|
14
|
+
# `src/resources/pi-prompts-version.txt` (missing or empty files report
|
|
15
|
+
# `unknown`), then requires an explicit `Y` confirmation; any other input
|
|
16
|
+
# aborts without modifications. On success, the resolved version is written
|
|
17
|
+
# into `src/resources/pi-prompts-version.txt`. Runtime is dominated by one
|
|
18
|
+
# git clone plus one ref checkout and per-file copies. Side effects include
|
|
19
|
+
# temporary-directory creation, git subprocesses, one optional GitHub
|
|
20
|
+
# Releases API request, replacement of the three target resource
|
|
21
|
+
# directories, and overwrite of the one version marker file. No other
|
|
22
|
+
# repository content is modified.
|
|
23
|
+
# @satisfies DES-022, REQ-379, REQ-380, REQ-381, REQ-382, REQ-383, REQ-384, REQ-396, REQ-397, REQ-398, REQ-399
|
|
18
24
|
|
|
19
25
|
set -euo pipefail
|
|
20
26
|
|
|
@@ -27,6 +33,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
27
33
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
28
34
|
TARGET_ROOT="${REPO_ROOT}/src/resources"
|
|
29
35
|
|
|
36
|
+
## @brief Stores the PI-Prompts version marker path relative to the repository root.
|
|
37
|
+
## @details Read before the update and overwritten with the resolved version after
|
|
38
|
+
## successful synchronization. The constant is immutable at runtime.
|
|
39
|
+
## @satisfies REQ-396, REQ-399
|
|
40
|
+
PROMPTS_VERSION_REL="src/resources/pi-prompts-version.txt"
|
|
41
|
+
|
|
30
42
|
## @brief Stores the ordered `upstream-relative:target-relative` synchronization pairs.
|
|
31
43
|
## @details Each entry maps one upstream repository directory to one bundled
|
|
32
44
|
## `src/resources` target directory synchronized in declaration order. The
|
|
@@ -87,6 +99,73 @@ require_tool() {
|
|
|
87
99
|
command -v "$1" >/dev/null 2>&1 || fail "Required tool not found: $1"
|
|
88
100
|
}
|
|
89
101
|
|
|
102
|
+
## @brief Reads one stored PI-Prompts version marker file.
|
|
103
|
+
## @details Returns the first line of the marker file with all surrounding
|
|
104
|
+
## whitespace removed when the file exists and is non-empty; otherwise
|
|
105
|
+
## returns the literal `unknown` so missing or empty version markers are
|
|
106
|
+
## handled deterministically. Runtime is O(n) in file size. Side effects are
|
|
107
|
+
## limited to filesystem reads.
|
|
108
|
+
## @param[in] marker_path {string} Absolute version marker file path.
|
|
109
|
+
## @return {string} Trimmed stored version or `unknown` on `stdout`.
|
|
110
|
+
## @satisfies REQ-396
|
|
111
|
+
read_version_file() {
|
|
112
|
+
local marker_path="$1"
|
|
113
|
+
local version=""
|
|
114
|
+
if [ -f "${marker_path}" ] && [ -s "${marker_path}" ]; then
|
|
115
|
+
version="$(head -n 1 "${marker_path}" | tr -d '[:space:]' || true)"
|
|
116
|
+
fi
|
|
117
|
+
if [ -z "${version}" ]; then
|
|
118
|
+
version="unknown"
|
|
119
|
+
fi
|
|
120
|
+
printf '%s\n' "${version}"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
## @brief Derives the release version from one resolved upstream synchronization ref.
|
|
124
|
+
## @details Strips the `refs/tags/` prefix from a release-tag ref and the
|
|
125
|
+
## `refs/heads/` prefix from a branch-head ref so the printed version and
|
|
126
|
+
## the stored version marker share one deterministic release identity.
|
|
127
|
+
## Runtime is O(n) in ref length. No external state is mutated.
|
|
128
|
+
## @param[in] release_ref {string} Resolved `refs/tags/<tag>` or `refs/heads/<branch>` ref.
|
|
129
|
+
## @return {string} Release version on `stdout`.
|
|
130
|
+
resolve_release_version() {
|
|
131
|
+
local release_ref="$1"
|
|
132
|
+
case "${release_ref}" in
|
|
133
|
+
refs/tags/*)
|
|
134
|
+
printf '%s\n' "${release_ref#refs/tags/}"
|
|
135
|
+
;;
|
|
136
|
+
refs/heads/*)
|
|
137
|
+
printf '%s\n' "${release_ref#refs/heads/}"
|
|
138
|
+
;;
|
|
139
|
+
*)
|
|
140
|
+
printf '%s\n' "${release_ref}"
|
|
141
|
+
;;
|
|
142
|
+
esac
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
## @brief Requests explicit user confirmation before any modification.
|
|
146
|
+
## @details Prints the resolved latest PI-Prompts release version plus the stored
|
|
147
|
+
## version and one English confirmation sentence, reads one line from
|
|
148
|
+
## `stdin`, and returns success only when the confirmation input is exactly
|
|
149
|
+
## `Y`; any other input, including end-of-stream input, returns failure so
|
|
150
|
+
## callers abort without modifications. Runtime is O(1) plus one user
|
|
151
|
+
## interaction. Side effects include `stdout` and `stderr` writes.
|
|
152
|
+
## @param[in] latest_version {string} Resolved upstream release version.
|
|
153
|
+
## @param[in] stored_version {string} Stored version of `${PROMPTS_VERSION_REL}`.
|
|
154
|
+
## @return {integer} `0` when the user confirmed with exactly `Y`; `1` otherwise.
|
|
155
|
+
## @satisfies REQ-397, REQ-398
|
|
156
|
+
confirm_update() {
|
|
157
|
+
local latest_version="$1"
|
|
158
|
+
local stored_version="$2"
|
|
159
|
+
local answer=""
|
|
160
|
+
|
|
161
|
+
log "Latest PI-Prompts release : ${latest_version}"
|
|
162
|
+
log "Stored version (${PROMPTS_VERSION_REL}): ${stored_version}"
|
|
163
|
+
log "This update replaces the bundled prompt resources in src/resources/instructions, src/resources/prompts, and src/resources/templates with the content of the latest PI-Prompts release."
|
|
164
|
+
printf 'Proceed with the update? [Y/n] '
|
|
165
|
+
read -r answer || answer=""
|
|
166
|
+
[ "${answer}" = "Y" ]
|
|
167
|
+
}
|
|
168
|
+
|
|
90
169
|
## @brief Clones the upstream PI-Prompts master branch into one target directory.
|
|
91
170
|
## @details Attempts a quiet, no-checkout clone of `${PROMPTS_BRANCH}` from
|
|
92
171
|
## `${PROMPTS_REPO_URL_PRIMARY}`, removes the partial destination on failure,
|
|
@@ -194,22 +273,30 @@ sync_directory() {
|
|
|
194
273
|
}
|
|
195
274
|
|
|
196
275
|
## @brief Executes one full bundled-resource update run.
|
|
197
|
-
## @details Validates required tools,
|
|
198
|
-
## tree, clones the PI-Prompts
|
|
199
|
-
## newest release ref
|
|
200
|
-
##
|
|
201
|
-
##
|
|
202
|
-
##
|
|
203
|
-
##
|
|
276
|
+
## @details Validates required tools, reads the stored PI-Prompts version marker,
|
|
277
|
+
## allocates the temporary upstream working tree, clones the PI-Prompts
|
|
278
|
+
## master branch, resolves the newest release ref plus its release version,
|
|
279
|
+
## prints the resolved plus stored versions and requires an explicit `Y`
|
|
280
|
+
## confirmation, checks out the resolved release ref, synchronizes every
|
|
281
|
+
## ordered pair declared in `SYNC_SPECS`, and writes the resolved version
|
|
282
|
+
## into the version marker file. Runtime is dominated by the upstream clone
|
|
283
|
+
## plus the three directory synchronizations. Side effects include
|
|
284
|
+
## temporary-directory creation and removal, git subprocesses, one optional
|
|
285
|
+
## GitHub Releases API request, replacement of the three bundled target
|
|
286
|
+
## directories, and overwrite of the version marker file.
|
|
204
287
|
## @return {void} No return value.
|
|
205
288
|
## @throws Exits through `fail(...)` when any required tool, clone, checkout, or
|
|
206
289
|
## synchronization step fails.
|
|
290
|
+
## @satisfies REQ-396, REQ-397, REQ-398, REQ-399
|
|
207
291
|
main() {
|
|
208
292
|
require_tool git
|
|
209
293
|
require_tool find
|
|
210
294
|
require_tool mkdir
|
|
211
295
|
require_tool cp
|
|
212
296
|
|
|
297
|
+
local prompts_version
|
|
298
|
+
prompts_version="$(read_version_file "${REPO_ROOT}/${PROMPTS_VERSION_REL}")"
|
|
299
|
+
|
|
213
300
|
mkdir -p "${TARGET_ROOT}"
|
|
214
301
|
|
|
215
302
|
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/pi-prompts-update.XXXXXX")" \
|
|
@@ -222,6 +309,15 @@ main() {
|
|
|
222
309
|
|
|
223
310
|
local release_ref
|
|
224
311
|
release_ref="$(resolve_release_ref "${repository}")"
|
|
312
|
+
|
|
313
|
+
local release_version
|
|
314
|
+
release_version="$(resolve_release_version "${release_ref}")"
|
|
315
|
+
|
|
316
|
+
if ! confirm_update "${release_version}" "${prompts_version}"; then
|
|
317
|
+
log "Update aborted; no repository content was modified."
|
|
318
|
+
return 0
|
|
319
|
+
fi
|
|
320
|
+
|
|
225
321
|
git -C "${repository}" checkout --quiet --detach "${release_ref}" \
|
|
226
322
|
|| fail "Unable to check out ${release_ref}."
|
|
227
323
|
log "Synchronizing from ref: ${release_ref}"
|
|
@@ -233,7 +329,9 @@ main() {
|
|
|
233
329
|
sync_directory "${repository}/${source_rel}" "${REPO_ROOT}/${target_rel}"
|
|
234
330
|
done
|
|
235
331
|
|
|
236
|
-
|
|
332
|
+
printf '%s\n' "${release_version}" > "${REPO_ROOT}/${PROMPTS_VERSION_REL}"
|
|
333
|
+
|
|
334
|
+
log "Bundled prompt resources updated to ${release_version}."
|
|
237
335
|
}
|
|
238
336
|
|
|
239
337
|
trap cleanup EXIT
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file
|
|
3
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)
|
|
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. Measurements are memoized per resolved path against the file `mtimeMs` plus `size` signature and reuses the process-cached shared tokenizer, so repeated menu renders resolve unchanged files through filesystem stats only. Runtime is O(n) on first measurement per content revision and O(1) per unchanged remeasure. Side effects are limited to filesystem reads and module-local cache mutation.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import fs from "node:fs";
|
|
@@ -9,6 +9,7 @@ import path from "node:path";
|
|
|
9
9
|
import { DEFAULT_DOCS_DIR, type UseReqConfig } from "./config.js";
|
|
10
10
|
import { normalizeRelativeDirContract } from "./path-context.js";
|
|
11
11
|
import { countFileMetrics } from "./token-counter.js";
|
|
12
|
+
import { ReqError } from "./errors.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @brief Stores the canonical context-file names in their documented injection order.
|
|
@@ -41,6 +42,27 @@ export interface ContextFileSizeFacts {
|
|
|
41
42
|
*/
|
|
42
43
|
export const MISSING_CONTEXT_FILE_SIZE: ContextFileSizeFacts = Object.freeze({ exists: false, chars: 0, tokens: 0 });
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* @brief Stores one memoized context-file measurement with its staleness signature.
|
|
47
|
+
* @details Pairs the measured facts with the `mtimeMs` plus `size` filesystem signature observed at measurement time so later probes can decide, through one stat call, whether the cached facts still describe the current file content. The interface is compile-time only and introduces no runtime cost.
|
|
48
|
+
*/
|
|
49
|
+
interface CachedContextFileSizeEntry {
|
|
50
|
+
readonly statSignature: string;
|
|
51
|
+
readonly facts: ContextFileSizeFacts;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @brief Bounds the module-local context-file measurement cache.
|
|
56
|
+
* @details Worktree-backed prompt runs measure three context files per execution path, so unbounded caching would accumulate one entry triplet per generated worktree in long-lived pi hosts. Exceeding the bound clears the cache completely, which only costs one re-measurement per live path. Lookup complexity is O(1).
|
|
57
|
+
*/
|
|
58
|
+
const CONTEXT_FILE_SIZE_CACHE_LIMIT = 64;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @brief Stores the module-local context-file measurement cache keyed by resolved path.
|
|
62
|
+
* @details Maps each measured absolute context-file path to its stat-signed facts so repeated configuration-menu renders and command summaries reuse identical measurements while any content revision invalidates only the affected entry. Cache state is module-local and never serialized into configuration.
|
|
63
|
+
*/
|
|
64
|
+
const contextFileSizeCache = new Map<string, CachedContextFileSizeEntry>();
|
|
65
|
+
|
|
44
66
|
/**
|
|
45
67
|
* @brief Resolves one canonical context-file absolute path for one project base.
|
|
46
68
|
* @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.
|
|
@@ -61,18 +83,34 @@ export function resolveContextFilePath(
|
|
|
61
83
|
|
|
62
84
|
/**
|
|
63
85
|
* @brief Measures one context file into deterministic size facts.
|
|
64
|
-
* @details Probes
|
|
86
|
+
* @details Probes the target with one `stat` call, returns the memoized facts when the observed `mtimeMs` plus `size` signature matches the cached entry, and otherwise reads UTF-8 content and reuses the process-cached shared tokenizer behind `countFileMetrics` for the `cl100k_base` token estimate before storing the fresh facts in the bounded cache. Missing, non-file, and unreadable targets return `MISSING_CONTEXT_FILE_SIZE` facts without throwing and without caching. Runtime is O(1) for unchanged remeasures and O(n) in file size on first measurement per content revision. Side effects are limited to filesystem reads and module-local cache mutation.
|
|
65
87
|
* @param[in] filePath {string} Absolute context-file path to measure.
|
|
66
88
|
* @return {ContextFileSizeFacts} Measured size facts for the target.
|
|
67
89
|
* @satisfies REQ-373, REQ-374
|
|
68
90
|
*/
|
|
69
91
|
export function measureContextFileSize(filePath: string): ContextFileSizeFacts {
|
|
92
|
+
let stat: fs.Stats | undefined;
|
|
93
|
+
try {
|
|
94
|
+
stat = fs.statSync(filePath);
|
|
95
|
+
} catch {
|
|
96
|
+
stat = undefined;
|
|
97
|
+
}
|
|
98
|
+
if (!stat || !stat.isFile()) {
|
|
99
|
+
return MISSING_CONTEXT_FILE_SIZE;
|
|
100
|
+
}
|
|
101
|
+
const statSignature = `${stat.mtimeMs}:${stat.size}`;
|
|
102
|
+
const cachedEntry = contextFileSizeCache.get(filePath);
|
|
103
|
+
if (cachedEntry && cachedEntry.statSignature === statSignature) {
|
|
104
|
+
return cachedEntry.facts;
|
|
105
|
+
}
|
|
70
106
|
try {
|
|
71
|
-
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
72
|
-
return MISSING_CONTEXT_FILE_SIZE;
|
|
73
|
-
}
|
|
74
107
|
const metrics = countFileMetrics(fs.readFileSync(filePath, "utf8"));
|
|
75
|
-
|
|
108
|
+
const facts: ContextFileSizeFacts = { exists: true, chars: metrics.chars, tokens: metrics.tokens };
|
|
109
|
+
if (contextFileSizeCache.size >= CONTEXT_FILE_SIZE_CACHE_LIMIT) {
|
|
110
|
+
contextFileSizeCache.clear();
|
|
111
|
+
}
|
|
112
|
+
contextFileSizeCache.set(filePath, { statSignature, facts });
|
|
113
|
+
return facts;
|
|
76
114
|
} catch {
|
|
77
115
|
return MISSING_CONTEXT_FILE_SIZE;
|
|
78
116
|
}
|
|
@@ -80,7 +118,7 @@ export function measureContextFileSize(filePath: string): ContextFileSizeFacts {
|
|
|
80
118
|
|
|
81
119
|
/**
|
|
82
120
|
* @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.
|
|
121
|
+
* @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. Repeated invocations with unchanged files resolve through the stat-signed measurement cache. Runtime is O(n) in aggregate context-file size on first measurement per content revision and O(1) per unchanged remeasure. Side effects are limited to filesystem reads and module-local cache mutation.
|
|
84
122
|
* @param[in] projectBase {string} Absolute project root path.
|
|
85
123
|
* @param[in] config {UseReqConfig} Effective project configuration supplying the docs directory.
|
|
86
124
|
* @return {Record<ContextFileName, ContextFileSizeFacts>} Measured size facts keyed by canonical file name.
|
|
@@ -107,3 +145,162 @@ export function measureContextFileSizes(
|
|
|
107
145
|
export function formatContextFileSize(facts: ContextFileSizeFacts): string {
|
|
108
146
|
return `${facts.chars}c/${facts.tokens}t`;
|
|
109
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @brief Stores the documented fallback context-window token budget.
|
|
151
|
+
* @details Used as the occupancy computation basis and as the early-check limit whenever the selected model max input context is unknown, and always renders behind the `*` marker so consumers can detect the assumed basis. Lookup complexity is O(1).
|
|
152
|
+
* @satisfies REQ-403
|
|
153
|
+
*/
|
|
154
|
+
export const CONTEXT_OCCUPANCY_FALLBACK_CONTEXT_WINDOW_TOKENS = 1_000_000;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @brief Maps every canonical context file to its persisted configuration flag key.
|
|
158
|
+
* @details Single mapping source shared by the token summation and segment renderers so enabled-file selection stays identical across every occupancy surface. Lookup complexity is O(1). No external state is mutated.
|
|
159
|
+
*/
|
|
160
|
+
const CONTEXT_FILE_FLAG_KEYS: Record<ContextFileName, "context-files-requirements" | "context-files-references" | "context-files-workflow"> = {
|
|
161
|
+
"REQUIREMENTS.md": "context-files-requirements",
|
|
162
|
+
"REFERENCES.md": "context-files-references",
|
|
163
|
+
"WORKFLOW.md": "context-files-workflow",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @brief Describes the computed context-occupancy facts of one enabled context-file set.
|
|
168
|
+
* @details Stores the aggregate token total, the resolved selected-model max input context (undefined when unknown), the effective basis tokens used for the percentage, the rendered percentage label, the rendered max-context label, and the final bracketed occupancy suffix. The interface is compile-time only and introduces no runtime cost.
|
|
169
|
+
* @satisfies REQ-400
|
|
170
|
+
*/
|
|
171
|
+
export interface ContextOccupancyFacts {
|
|
172
|
+
readonly totalTokens: number;
|
|
173
|
+
readonly maxContextTokens: number | undefined;
|
|
174
|
+
readonly basisTokens: number;
|
|
175
|
+
readonly percentLabel: string;
|
|
176
|
+
readonly maxLabel: string;
|
|
177
|
+
readonly suffix: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @brief Sums the `cl100k_base` token estimates of every enabled existing context file.
|
|
182
|
+
* @details Iterates `CONTEXT_FILE_NAMES` in documented order, skips disabled flags and missing or unreadable files, and accumulates the measured token estimates so occupancy surfaces and the prompt-dispatch early check share one total. Runtime is O(1) in file count. No external state is mutated.
|
|
183
|
+
* @param[in] config {Pick<UseReqConfig, "context-files-requirements" | "context-files-references" | "context-files-workflow">} Effective configuration supplying the three context-file flags.
|
|
184
|
+
* @param[in] sizes {Record<string, ContextFileSizeFacts>} Measured context-file size facts keyed by canonical file name.
|
|
185
|
+
* @return {number} Aggregate token estimate of enabled existing context files.
|
|
186
|
+
* @satisfies REQ-400
|
|
187
|
+
*/
|
|
188
|
+
export function sumEnabledContextFileTokens(
|
|
189
|
+
config: Pick<UseReqConfig, "context-files-requirements" | "context-files-references" | "context-files-workflow">,
|
|
190
|
+
sizes: Record<string, ContextFileSizeFacts>,
|
|
191
|
+
): number {
|
|
192
|
+
let totalTokens = 0;
|
|
193
|
+
for (const fileName of CONTEXT_FILE_NAMES) {
|
|
194
|
+
const facts = sizes[fileName];
|
|
195
|
+
if (!config[CONTEXT_FILE_FLAG_KEYS[fileName]] || !facts || !facts.exists) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
totalTokens += facts.tokens;
|
|
199
|
+
}
|
|
200
|
+
return totalTokens;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @brief Formats one context-window token count as a compact human-readable label.
|
|
205
|
+
* @details Renders token counts of one million or more as `<x.x>M`, counts of one thousand or more as `<x.x>K`, and smaller counts as rounded integers so the documented `1.0M` fallback shape stays deterministic. Runtime is O(1). No external state is mutated.
|
|
206
|
+
* @param[in] tokens {number} Context-window token count to label.
|
|
207
|
+
* @return {string} Compact context-window label such as `1.0M`, `200.0K`, or `512`.
|
|
208
|
+
* @satisfies REQ-403
|
|
209
|
+
*/
|
|
210
|
+
export function formatContextWindowTokensLabel(tokens: number): string {
|
|
211
|
+
if (tokens >= 1_000_000) {
|
|
212
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
213
|
+
}
|
|
214
|
+
if (tokens >= 1_000) {
|
|
215
|
+
return `${(tokens / 1_000).toFixed(1)}K`;
|
|
216
|
+
}
|
|
217
|
+
return String(Math.round(tokens));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @brief Formats one occupancy percentage as a compact label with one decimal place.
|
|
222
|
+
* @details Rounds the percentage to one decimal place and removes a trailing `.0` so whole percentages render as `36%` and fractional values keep their decimal digit. Runtime is O(1). No external state is mutated.
|
|
223
|
+
* @param[in] percent {number} Occupancy percentage value.
|
|
224
|
+
* @return {string} Percentage label such as `36%`, `0.4%`, or `0%`.
|
|
225
|
+
* @satisfies REQ-402
|
|
226
|
+
*/
|
|
227
|
+
export function formatContextOccupancyPercentLabel(percent: number): string {
|
|
228
|
+
const fixed = percent.toFixed(1);
|
|
229
|
+
return fixed.endsWith(".0") ? fixed.slice(0, -2) : fixed;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @brief Computes the context-occupancy facts of one enabled context-file token total.
|
|
234
|
+
* @details Resolves the effective basis from the supplied max input context or the documented 1,000,000-token fallback, derives the percentage label, derives the max label with the `*` marker only for the unknown fallback, and renders `[<percent>% context]` for a known max or `[<percent>%/1.0M* context]` for the fallback basis. Runtime is O(1). No external state is mutated.
|
|
235
|
+
* @param[in] totalTokens {number} Aggregate enabled existing context-file token estimate.
|
|
236
|
+
* @param[in] maxContextTokens {number | undefined} Selected model max input context tokens; undefined selects the fallback basis.
|
|
237
|
+
* @return {ContextOccupancyFacts} Computed occupancy facts including the rendered suffix.
|
|
238
|
+
* @satisfies REQ-400, REQ-402, REQ-403
|
|
239
|
+
*/
|
|
240
|
+
export function computeContextOccupancyFacts(
|
|
241
|
+
totalTokens: number,
|
|
242
|
+
maxContextTokens: number | undefined,
|
|
243
|
+
): ContextOccupancyFacts {
|
|
244
|
+
const knownMax = typeof maxContextTokens === "number" && Number.isFinite(maxContextTokens) && maxContextTokens > 0
|
|
245
|
+
? maxContextTokens
|
|
246
|
+
: undefined;
|
|
247
|
+
const basisTokens = knownMax ?? CONTEXT_OCCUPANCY_FALLBACK_CONTEXT_WINDOW_TOKENS;
|
|
248
|
+
const percentLabel = formatContextOccupancyPercentLabel((totalTokens / basisTokens) * 100);
|
|
249
|
+
const maxLabel = knownMax === undefined
|
|
250
|
+
? `${formatContextWindowTokensLabel(basisTokens)}*`
|
|
251
|
+
: formatContextWindowTokensLabel(knownMax);
|
|
252
|
+
const suffix = knownMax === undefined
|
|
253
|
+
? `[${percentLabel}%/${maxLabel} context]`
|
|
254
|
+
: `[${percentLabel}% context]`;
|
|
255
|
+
return { totalTokens, maxContextTokens: knownMax, basisTokens, percentLabel, maxLabel, suffix };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @brief Renders every enabled existing context file as a `name(<chars>c/<tokens>t)` segment list.
|
|
260
|
+
* @details Iterates `CONTEXT_FILE_NAMES` in documented order, skips disabled flags and missing or unreadable files, and joins the remaining segments with the documented ` • ` separator so early-check diagnostics reuse the same segment shape as the configuration summary. Runtime is O(1) in file count. No external state is mutated.
|
|
261
|
+
* @param[in] config {Pick<UseReqConfig, "context-files-requirements" | "context-files-references" | "context-files-workflow">} Effective configuration supplying the three context-file flags.
|
|
262
|
+
* @param[in] sizes {Record<string, ContextFileSizeFacts>} Measured context-file size facts keyed by canonical file name.
|
|
263
|
+
* @return {string} Joined segment list, or the empty string when no enabled existing context file contributes.
|
|
264
|
+
* @satisfies REQ-410
|
|
265
|
+
*/
|
|
266
|
+
export function formatEnabledContextFileSegments(
|
|
267
|
+
config: Pick<UseReqConfig, "context-files-requirements" | "context-files-references" | "context-files-workflow">,
|
|
268
|
+
sizes: Record<string, ContextFileSizeFacts>,
|
|
269
|
+
): string {
|
|
270
|
+
const segments: string[] = [];
|
|
271
|
+
for (const fileName of CONTEXT_FILE_NAMES) {
|
|
272
|
+
const facts = sizes[fileName];
|
|
273
|
+
if (!config[CONTEXT_FILE_FLAG_KEYS[fileName]] || !facts || !facts.exists) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
segments.push(`${fileName}(${formatContextFileSize(facts)})`);
|
|
277
|
+
}
|
|
278
|
+
return segments.join(" • ");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @brief Enforces the context-occupancy early check for one prepared prompt command.
|
|
283
|
+
* @details Sums the enabled existing context-file tokens, computes the occupancy facts against the supplied max input context or the documented fallback, and throws one deterministic ReqError diagnostic listing every enabled existing file segment, the aggregate token total, and the occupancy suffix whenever the total strictly exceeds the basis. Runtime is O(1) in file count. No external state is mutated.
|
|
284
|
+
* @param[in] config {Pick<UseReqConfig, "context-files-requirements" | "context-files-references" | "context-files-workflow">} Effective configuration supplying the three context-file flags.
|
|
285
|
+
* @param[in] sizes {Record<string, ContextFileSizeFacts>} Measured context-file size facts keyed by canonical file name.
|
|
286
|
+
* @param[in] maxContextTokens {number | undefined} Selected model max input context tokens; undefined selects the fallback basis.
|
|
287
|
+
* @return {ContextOccupancyFacts} Computed occupancy facts when the check passes.
|
|
288
|
+
* @throws {ReqError} Throws when the enabled token total strictly exceeds the effective basis.
|
|
289
|
+
* @satisfies REQ-408, REQ-409, REQ-410
|
|
290
|
+
*/
|
|
291
|
+
export function enforceContextOccupancyLimit(
|
|
292
|
+
config: Pick<UseReqConfig, "context-files-requirements" | "context-files-references" | "context-files-workflow">,
|
|
293
|
+
sizes: Record<string, ContextFileSizeFacts>,
|
|
294
|
+
maxContextTokens: number | undefined,
|
|
295
|
+
): ContextOccupancyFacts {
|
|
296
|
+
const facts = computeContextOccupancyFacts(sumEnabledContextFileTokens(config, sizes), maxContextTokens);
|
|
297
|
+
const segments = formatEnabledContextFileSegments(config, sizes);
|
|
298
|
+
if (segments.length > 0 && facts.totalTokens > facts.basisTokens) {
|
|
299
|
+
throw new ReqError(
|
|
300
|
+
`Context occupancy early check failed: enabled context files total ${facts.totalTokens}t exceed ` +
|
|
301
|
+
`${facts.maxLabel} selected model input context. ${segments} ${facts.suffix}`,
|
|
302
|
+
1,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return facts;
|
|
306
|
+
}
|
|
@@ -364,6 +364,22 @@ function normalizeContextUsage(
|
|
|
364
364
|
};
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
/**
|
|
368
|
+
* @brief Resolves the selected model max input context tokens from one normalized context-usage snapshot.
|
|
369
|
+
* @details Accepts only finite positive `contextWindow` numbers so missing, non-numeric, and non-positive runtime values degrade to `undefined`, which every context-occupancy consumer treats as the documented unknown basis. Runtime is O(1). No external state is mutated.
|
|
370
|
+
* @param[in] contextUsage {ContextUsage | undefined} Normalized context-usage snapshot from the status controller state.
|
|
371
|
+
* @return {number | undefined} Selected model max input context tokens, or undefined when unknown.
|
|
372
|
+
* @satisfies REQ-401
|
|
373
|
+
*/
|
|
374
|
+
export function resolveModelContextWindowTokens(
|
|
375
|
+
contextUsage: ContextUsage | undefined,
|
|
376
|
+
): number | undefined {
|
|
377
|
+
const contextWindow = contextUsage?.contextWindow;
|
|
378
|
+
return typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0
|
|
379
|
+
? contextWindow
|
|
380
|
+
: undefined;
|
|
381
|
+
}
|
|
382
|
+
|
|
367
383
|
/**
|
|
368
384
|
* @brief Refreshes the stored context-usage snapshot from the active extension context.
|
|
369
385
|
* @details Calls `ctx.getContextUsage()` on every intercepted event so the
|
|
@@ -8,6 +8,7 @@ import fs from "node:fs";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { spawnSync, type SpawnSyncReturns } from "node:child_process";
|
|
10
10
|
import { ReqError } from "./errors.js";
|
|
11
|
+
import { enforceContextOccupancyLimit, measureContextFileSizes } from "./context-file-size.js";
|
|
11
12
|
import { classifyPiNotifyOutcome, type PiNotifyOutcome } from "./pi-notify.js";
|
|
12
13
|
import {
|
|
13
14
|
normalizeGitWorktreePrefix,
|
|
@@ -1695,7 +1696,7 @@ export function validatePromptRequiredDocs(
|
|
|
1695
1696
|
|
|
1696
1697
|
/**
|
|
1697
1698
|
* @brief Prepares prompt-command execution for one bundled prompt.
|
|
1698
|
-
* @details Runs slash-command-owned git validation, enforces the prompt-specific required-doc matrix, resolves persisted origin and execution session files, applies the effective worktree policy, generates and verifies a dedicated worktree when enabled, and returns the execution plan consumed by prompt rendering plus lifecycle hooks. Worktree-backed execution reuses the active session directory for the forked session file. Runtime is dominated by git subprocesses, worktree creation, and optional session-file cloning. Side effects include worktree creation, session-file creation, filesystem reads, and optional prompt debug-log writes.
|
|
1699
|
+
* @details Runs slash-command-owned git validation, enforces the context-occupancy early check against the selected model max input context, enforces the prompt-specific required-doc matrix, resolves persisted origin and execution session files, applies the effective worktree policy, generates and verifies a dedicated worktree when enabled, and returns the execution plan consumed by prompt rendering plus lifecycle hooks. Worktree-backed execution reuses the active session directory for the forked session file. Runtime is dominated by git subprocesses, worktree creation, and optional session-file cloning. Side effects include worktree creation, session-file creation, filesystem reads, and optional prompt debug-log writes.
|
|
1699
1700
|
* @param[in] promptName {PromptCommandName} Bundled prompt identifier.
|
|
1700
1701
|
* @param[in] promptArgs {string} Raw prompt argument string.
|
|
1701
1702
|
* @param[in] projectBase {string} Absolute current project base.
|
|
@@ -1706,9 +1707,10 @@ export function validatePromptRequiredDocs(
|
|
|
1706
1707
|
* @param[in] debugOptions {PromptCommandDebugOptions | undefined} Optional prompt debug logging context.
|
|
1707
1708
|
* @param[in] originalModel {PromptCommandModelSelection | undefined} Active model provider plus identifier captured by the command handler for later re-application.
|
|
1708
1709
|
* @param[in] originalThinkingLevel {string | undefined} Active thinking level captured by the command handler for later re-application.
|
|
1710
|
+
* @param[in] maxContextTokens {number | undefined} Selected model max input context tokens consumed by the early occupancy check; undefined selects the documented 1,000,000-token fallback.
|
|
1709
1711
|
* @return {PromptCommandExecutionPlan} Prepared execution plan.
|
|
1710
|
-
* @throws {ReqError} Throws when repository validation, required-doc validation, worktree creation, or session preparation fails.
|
|
1711
|
-
* @satisfies REQ-200, REQ-203, REQ-206, REQ-207, REQ-215, REQ-219, REQ-220, REQ-245, REQ-256, REQ-271, REQ-366
|
|
1712
|
+
* @throws {ReqError} Throws when repository validation, the context-occupancy early check, required-doc validation, worktree creation, or session preparation fails.
|
|
1713
|
+
* @satisfies REQ-200, REQ-203, REQ-206, REQ-207, REQ-215, REQ-219, REQ-220, REQ-245, REQ-256, REQ-271, REQ-366, REQ-408, REQ-409, REQ-410, REQ-411
|
|
1712
1714
|
*/
|
|
1713
1715
|
export function preparePromptCommandExecution(
|
|
1714
1716
|
promptName: PromptCommandName,
|
|
@@ -1721,8 +1723,10 @@ export function preparePromptCommandExecution(
|
|
|
1721
1723
|
debugOptions?: PromptCommandDebugOptions,
|
|
1722
1724
|
originalModel?: PromptCommandModelSelection,
|
|
1723
1725
|
originalThinkingLevel?: string,
|
|
1726
|
+
maxContextTokens?: number | undefined,
|
|
1724
1727
|
): PromptCommandExecutionPlan {
|
|
1725
1728
|
const gitPath = validatePromptGitState(projectBase, config);
|
|
1729
|
+
enforceContextOccupancyLimit(config, measureContextFileSizes(projectBase, config), maxContextTokens);
|
|
1726
1730
|
const reuseCurrentSessionFile = isUsablePromptSessionFile(currentSessionFile, projectBase);
|
|
1727
1731
|
const originalSessionFile = resolvePromptSessionFile(
|
|
1728
1732
|
reuseCurrentSessionFile ? currentSessionFile : undefined,
|
package/src/core/prompts.ts
CHANGED
|
@@ -17,7 +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
|
+
import { computeContextOccupancyFacts, formatContextFileSize, sumEnabledContextFileTokens, type ContextFileSizeFacts } from "./context-file-size.js";
|
|
21
21
|
import { formatRuntimePathForDisplay, normalizeRelativeDirContract } from "./path-context.js";
|
|
22
22
|
import type {
|
|
23
23
|
PromptCommandExecutionPlan,
|
|
@@ -387,28 +387,35 @@ export const PROMPT_COMMAND_SUMMARY_CUSTOM_TYPE = "pi-usereq-prompt-command";
|
|
|
387
387
|
|
|
388
388
|
/**
|
|
389
389
|
* @brief Builds the on-screen command invocation summary for one bundled prompt-backed `req-<prompt>` command.
|
|
390
|
-
* @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.
|
|
390
|
+
* @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)`; when the enabled existing token total contributes tokens, the field additionally appends the `[<percent>% context]` occupancy suffix against the selected model max input context (or `[<percent>%/1.0M* context]` with the documented fallback); 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.
|
|
391
391
|
* @param[in] promptName {string} Bundled prompt name without the `req-` prefix.
|
|
392
392
|
* @param[in] args {string} User request arguments passed to the slash command.
|
|
393
393
|
* @param[in] config {UseReqConfig} Effective project configuration supplying directory, git, static-check, and tool fields.
|
|
394
394
|
* @param[in] contextFileSizes {Record<string, ContextFileSizeFacts> | undefined} Optional measured context-file size facts keyed by canonical file name, consumed for `context files` size suffixes.
|
|
395
|
+
* @param[in] maxContextTokens {number | undefined} Selected model max input context tokens consumed by the occupancy suffix; undefined selects the documented 1,000,000-token fallback.
|
|
395
396
|
* @return {string} Multi-line command invocation summary text.
|
|
396
|
-
* @satisfies REQ-335, REQ-336, REQ-337, REQ-338, REQ-353, REQ-377
|
|
397
|
+
* @satisfies REQ-335, REQ-336, REQ-337, REQ-338, REQ-353, REQ-377, REQ-402, REQ-403, REQ-404, REQ-407
|
|
397
398
|
*/
|
|
398
399
|
export function renderPromptCommandSummary(
|
|
399
400
|
promptName: string,
|
|
400
401
|
args: string,
|
|
401
402
|
config: UseReqConfig,
|
|
402
403
|
contextFileSizes?: Record<string, ContextFileSizeFacts>,
|
|
404
|
+
maxContextTokens?: number | undefined,
|
|
403
405
|
): string {
|
|
404
|
-
const
|
|
406
|
+
const contextSegments = CONTEXT_FILE_DESCRIPTORS
|
|
405
407
|
.filter((descriptor) => config[descriptor.flagKey])
|
|
406
408
|
.map((descriptor) => {
|
|
407
409
|
const baseName = descriptor.fileName.replace(/\.md$/, "").toLowerCase();
|
|
408
410
|
const facts = contextFileSizes?.[descriptor.fileName];
|
|
409
411
|
return facts?.exists ? `${baseName}(${formatContextFileSize(facts)})` : baseName;
|
|
410
412
|
})
|
|
411
|
-
.join(", ")
|
|
413
|
+
.join(", ");
|
|
414
|
+
const totalContextTokens = contextFileSizes ? sumEnabledContextFileTokens(config, contextFileSizes) : 0;
|
|
415
|
+
const contextFiles = (contextSegments || "none")
|
|
416
|
+
+ (contextSegments && totalContextTokens > 0
|
|
417
|
+
? ` ${computeContextOccupancyFacts(totalContextTokens, maxContextTokens).suffix}`
|
|
418
|
+
: "");
|
|
412
419
|
const enabledLanguages = DEFAULT_STATIC_CHECK_LANGUAGES
|
|
413
420
|
.filter((language) => config["static-check"][language]?.enabled === "enable")
|
|
414
421
|
.join(", ") || "none";
|
|
@@ -196,14 +196,34 @@ function defaultJsTiktokenModuleLoader(): JsTiktokenModule {
|
|
|
196
196
|
|
|
197
197
|
let jsTiktokenModuleLoader: () => JsTiktokenModule = defaultJsTiktokenModuleLoader;
|
|
198
198
|
|
|
199
|
+
const cachedTokenCounterEncodings = new Map<string, TokenCounterEncoding>();
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @brief Resolves one shared tokenizer encoding for a `TokenCounter` construction.
|
|
203
|
+
* @details Checks the process-scoped per-encoding cache first and constructs the `js-tiktoken` encoding only on the first request per encoding name, so repeated `TokenCounter` construction across the `files-tokens` runner, canonical-doc counting, and context-file measurement reuses one parsed encoder instead of re-parsing the bundled BPE ranks each time. Runtime is O(1) after the first construction for the encoding and dominated by the single deferred `getEncoding` construction otherwise. Side effect: mutates the module-local encoding cache on first resolution per encoding name.
|
|
204
|
+
* @param[in] encodingName {string} `js-tiktoken` encoding identifier.
|
|
205
|
+
* @return {TokenCounterEncoding} Cached or newly constructed tokenizer encoding.
|
|
206
|
+
* @throws {ReqError} Propagates the deterministic unavailable-dependency failure when `js-tiktoken` cannot be loaded.
|
|
207
|
+
*/
|
|
208
|
+
function resolveTokenCounterEncoding(encodingName: string): TokenCounterEncoding {
|
|
209
|
+
const cachedEncoding = cachedTokenCounterEncodings.get(encodingName);
|
|
210
|
+
if (cachedEncoding) {
|
|
211
|
+
return cachedEncoding;
|
|
212
|
+
}
|
|
213
|
+
const encoding = loadJsTiktokenModule().getEncoding(encodingName);
|
|
214
|
+
cachedTokenCounterEncodings.set(encodingName, encoding);
|
|
215
|
+
return encoding;
|
|
216
|
+
}
|
|
217
|
+
|
|
199
218
|
/**
|
|
200
219
|
* @brief Overrides the `js-tiktoken` loader for tests.
|
|
201
|
-
* @details Enables deterministic dependency-failure tests without mutating repository dependencies on disk. Runtime is O(
|
|
220
|
+
* @details Enables deterministic dependency-failure tests without mutating repository dependencies on disk. Clears the process-scoped encoding cache so every loader swap observes fresh module behavior instead of encoders constructed by the previous loader. Runtime is O(e) in cached encoding count. Side effects are limited to module-local test state and cache reset.
|
|
202
221
|
* @param[in] loader {(() => JsTiktokenModule) | undefined} Replacement loader, or `undefined` to restore the default loader.
|
|
203
222
|
* @return {void} No return value.
|
|
204
223
|
*/
|
|
205
224
|
export function setJsTiktokenModuleLoaderForTests(loader?: () => JsTiktokenModule): void {
|
|
206
225
|
jsTiktokenModuleLoader = loader ?? defaultJsTiktokenModuleLoader;
|
|
226
|
+
cachedTokenCounterEncodings.clear();
|
|
207
227
|
}
|
|
208
228
|
|
|
209
229
|
/**
|
|
@@ -233,23 +253,23 @@ function loadJsTiktokenModule(): JsTiktokenModule {
|
|
|
233
253
|
|
|
234
254
|
/**
|
|
235
255
|
* @brief Encapsulates one tokenizer instance for repeated token counting.
|
|
236
|
-
* @details
|
|
256
|
+
* @details Resolves the process-scoped shared encoder for the requested encoding so multiple documents and multiple counter instances are counted without repeated encoding construction; the first construction per encoding name parses the BPE ranks once and every later `TokenCounter` reuses it through the module-local cache. Counting cost is O(n) in content length. The class mutates only instance state during construction.
|
|
237
257
|
*/
|
|
238
258
|
export class TokenCounter {
|
|
239
259
|
/**
|
|
240
260
|
* @brief Stores the tokenizer implementation used for subsequent counts.
|
|
241
|
-
* @details
|
|
261
|
+
* @details Holds the cached encoder resolved through the per-encoding process cache. Access complexity is O(1); construction is O(1) after the first resolution for the encoding name.
|
|
242
262
|
*/
|
|
243
263
|
private encoding: TokenCounterEncoding;
|
|
244
264
|
|
|
245
265
|
/**
|
|
246
266
|
* @brief Initializes a token counter for one encoding family.
|
|
247
|
-
* @details Resolves the
|
|
267
|
+
* @details Resolves the shared cached tokenizer for the encoding name, constructing the underlying `js-tiktoken` encoding at most once per encoding name per process so repeated constructions from token tools and context-file measurement are constant-time cache hits. Side effects are limited to instance initialization plus the optional first-time encoding cache mutation.
|
|
248
268
|
* @param[in] encodingName {string} `js-tiktoken` encoding identifier. Defaults to `cl100k_base`.
|
|
249
269
|
* @return {TokenCounter} New token counter instance.
|
|
250
270
|
*/
|
|
251
271
|
constructor(encodingName = TOKEN_COUNTER_ENCODING) {
|
|
252
|
-
this.encoding =
|
|
272
|
+
this.encoding = resolveTokenCounterEncoding(encodingName);
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
/**
|
|
@@ -427,7 +447,7 @@ function buildCountFileMetricsResult(filePath: string, content: string, counter:
|
|
|
427
447
|
|
|
428
448
|
/**
|
|
429
449
|
* @brief Counts tokens, characters, bytes, and lines for one in-memory content string.
|
|
430
|
-
* @details
|
|
450
|
+
* @details Resolves the process-cached shared `TokenCounter` encoder for the encoding, tokenizes the supplied text once, and pairs the result with raw character length, UTF-8 byte size, and logical line count. Runtime is O(n) after the one-time per-encoding encoder construction. No filesystem I/O occurs.
|
|
431
451
|
* @param[in] content {string} Text payload to measure.
|
|
432
452
|
* @param[in] encodingName {string} Tokenizer identifier. Defaults to `cl100k_base`.
|
|
433
453
|
* @return {{ tokens: number; chars: number; bytes: number; lines: number }} Aggregate metrics for the supplied content.
|
package/src/index.ts
CHANGED
|
@@ -84,8 +84,10 @@ import {
|
|
|
84
84
|
type PiUsereqStartupToolName,
|
|
85
85
|
} from "./core/pi-usereq-tools.js";
|
|
86
86
|
import {
|
|
87
|
+
computeContextOccupancyFacts,
|
|
87
88
|
formatContextFileSize,
|
|
88
89
|
measureContextFileSizes,
|
|
90
|
+
sumEnabledContextFileTokens,
|
|
89
91
|
type ContextFileName,
|
|
90
92
|
type ContextFileSizeFacts,
|
|
91
93
|
} from "./core/context-file-size.js";
|
|
@@ -160,6 +162,7 @@ import {
|
|
|
160
162
|
getPiUsereqRuntimeSoundLevel,
|
|
161
163
|
isStaleExtensionContextError,
|
|
162
164
|
renderPiUsereqStatus,
|
|
165
|
+
resolveModelContextWindowTokens,
|
|
163
166
|
setPiUsereqRuntimeSoundLevel,
|
|
164
167
|
setPiUsereqStatusConfig,
|
|
165
168
|
setPiUsereqWorkflowState,
|
|
@@ -3397,6 +3400,7 @@ function registerPromptCommands(
|
|
|
3397
3400
|
});
|
|
3398
3401
|
const projectBase = getProjectBase(commandCwd);
|
|
3399
3402
|
const config = loadProjectConfig(commandCwd);
|
|
3403
|
+
const maxContextTokens = resolveModelContextWindowTokens(statusController.state.contextUsage);
|
|
3400
3404
|
transitionPromptWorkflowState(
|
|
3401
3405
|
statusController,
|
|
3402
3406
|
ctx,
|
|
@@ -3427,6 +3431,7 @@ function registerPromptCommands(
|
|
|
3427
3431
|
},
|
|
3428
3432
|
originalModel,
|
|
3429
3433
|
originalThinkingLevel,
|
|
3434
|
+
maxContextTokens,
|
|
3430
3435
|
);
|
|
3431
3436
|
const content = renderPrompt(
|
|
3432
3437
|
promptName,
|
|
@@ -3470,6 +3475,7 @@ function registerPromptCommands(
|
|
|
3470
3475
|
args,
|
|
3471
3476
|
config,
|
|
3472
3477
|
measureContextFileSizes(projectBase, config),
|
|
3478
|
+
maxContextTokens,
|
|
3473
3479
|
);
|
|
3474
3480
|
const promptDelivery = deliverPromptCommand(pi, content, commandSummary, promptContext);
|
|
3475
3481
|
transitionPromptWorkflowState(
|
|
@@ -4545,44 +4551,56 @@ async function configureStaticCheckMenu(
|
|
|
4545
4551
|
}
|
|
4546
4552
|
|
|
4547
4553
|
/**
|
|
4548
|
-
* @brief Summarizes the `Context Files` flag state with measured sizes for the top-level menu value column.
|
|
4549
|
-
* @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
|
|
4554
|
+
* @brief Summarizes the `Context Files` flag state with measured sizes and context occupancy for the top-level menu value column.
|
|
4555
|
+
* @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, then appends the occupancy suffix computed from the enabled existing token total against the selected model max input context (`[<percent>% context]`, or `[<percent>%/1.0M* context]` with the documented fallback) whenever that total contributes tokens, while disabled segments stay plain `name:off`. Runtime is O(1) in segment count. No external state is mutated.
|
|
4550
4556
|
* @param[in] config {UseReqConfig} Effective project configuration.
|
|
4551
4557
|
* @param[in] sizes {Record<ContextFileName, ContextFileSizeFacts>} Measured context-file size facts keyed by canonical file name.
|
|
4552
|
-
* @
|
|
4553
|
-
* @
|
|
4558
|
+
* @param[in] maxContextTokens {number | undefined} Selected model max input context tokens; undefined selects the documented 1,000,000-token fallback.
|
|
4559
|
+
* @return {string} Compact `Context Files` summary string with the occupancy suffix.
|
|
4560
|
+
* @satisfies REQ-327, REQ-376, REQ-402, REQ-403, REQ-404, REQ-405
|
|
4554
4561
|
*/
|
|
4555
4562
|
function formatContextFilesSummary(
|
|
4556
4563
|
config: UseReqConfig,
|
|
4557
4564
|
sizes: Record<ContextFileName, ContextFileSizeFacts>,
|
|
4565
|
+
maxContextTokens?: number | undefined,
|
|
4558
4566
|
): string {
|
|
4559
4567
|
const segments = [
|
|
4560
4568
|
`requirements:${config["context-files-requirements"] ? `on(${formatContextFileSize(sizes["REQUIREMENTS.md"])})` : "off"}`,
|
|
4561
4569
|
`references:${config["context-files-references"] ? `on(${formatContextFileSize(sizes["REFERENCES.md"])})` : "off"}`,
|
|
4562
4570
|
`workflow:${config["context-files-workflow"] ? `on(${formatContextFileSize(sizes["WORKFLOW.md"])})` : "off"}`,
|
|
4563
4571
|
];
|
|
4564
|
-
|
|
4572
|
+
const summary = segments.join(" \u2022 ");
|
|
4573
|
+
const totalTokens = sumEnabledContextFileTokens(config, sizes);
|
|
4574
|
+
return totalTokens > 0
|
|
4575
|
+
? `${summary} ${computeContextOccupancyFacts(totalTokens, maxContextTokens).suffix}`
|
|
4576
|
+
: summary;
|
|
4565
4577
|
}
|
|
4566
4578
|
|
|
4567
4579
|
/**
|
|
4568
4580
|
* @brief Builds the shared settings-menu choices for the `Context Files` submenu.
|
|
4569
|
-
* @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.
|
|
4581
|
+
* @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 the context-occupancy suffix computed from the enabled existing token total against the selected model max input context, followed by a value-less subtree-local `Reset defaults` row. Cycle values embed the same measured facts and occupancy suffix 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.
|
|
4570
4582
|
* @param[in] config {UseReqConfig} Effective project configuration.
|
|
4571
4583
|
* @param[in] sizes {Record<ContextFileName, ContextFileSizeFacts>} Measured context-file size facts keyed by canonical file name.
|
|
4584
|
+
* @param[in] maxContextTokens {number | undefined} Selected model max input context tokens; undefined selects the documented 1,000,000-token fallback.
|
|
4572
4585
|
* @return {PiUsereqSettingsMenuChoice[]} Ordered `Context Files` submenu choices.
|
|
4573
|
-
* @satisfies REQ-327, REQ-328, REQ-333, REQ-375
|
|
4586
|
+
* @satisfies REQ-327, REQ-328, REQ-333, REQ-375, REQ-403, REQ-404, REQ-406
|
|
4574
4587
|
*/
|
|
4575
4588
|
function buildContextFilesMenuChoices(
|
|
4576
4589
|
config: UseReqConfig,
|
|
4577
4590
|
sizes: Record<ContextFileName, ContextFileSizeFacts>,
|
|
4591
|
+
maxContextTokens?: number | undefined,
|
|
4578
4592
|
): PiUsereqSettingsMenuChoice[] {
|
|
4593
|
+
const totalTokens = sumEnabledContextFileTokens(config, sizes);
|
|
4594
|
+
const occupancySuffix = totalTokens > 0
|
|
4595
|
+
? ` ${computeContextOccupancyFacts(totalTokens, maxContextTokens).suffix}`
|
|
4596
|
+
: "";
|
|
4579
4597
|
const buildRow = (
|
|
4580
4598
|
id: "context-files-requirements" | "context-files-references" | "context-files-workflow",
|
|
4581
4599
|
fileName: ContextFileName,
|
|
4582
4600
|
description: string,
|
|
4583
4601
|
): PiUsereqSettingsMenuChoice => {
|
|
4584
|
-
const sizedOn = `on \u2022 ${formatContextFileSize(sizes[fileName])}`;
|
|
4585
|
-
const sizedOff = `off \u2022 ${formatContextFileSize(sizes[fileName])}`;
|
|
4602
|
+
const sizedOn = `on \u2022 ${formatContextFileSize(sizes[fileName])}${occupancySuffix}`;
|
|
4603
|
+
const sizedOff = `off \u2022 ${formatContextFileSize(sizes[fileName])}${occupancySuffix}`;
|
|
4586
4604
|
return {
|
|
4587
4605
|
id,
|
|
4588
4606
|
label: fileName,
|
|
@@ -4619,13 +4637,15 @@ function buildContextFilesMenuChoices(
|
|
|
4619
4637
|
* @param[in] ctx {ExtensionCommandContext} Active command context.
|
|
4620
4638
|
* @param[in,out] config {UseReqConfig} Mutable effective project configuration.
|
|
4621
4639
|
* @param[in] onConfigChange {() => void} Shared persistence-plus-status callback.
|
|
4640
|
+
* @param[in] maxContextTokens {number | undefined} Selected model max input context tokens used by the submenu occupancy suffix; undefined selects the documented 1,000,000-token fallback.
|
|
4622
4641
|
* @return {Promise<void>} Promise resolved when the submenu closes.
|
|
4623
|
-
* @satisfies REQ-327, REQ-328, REQ-333
|
|
4642
|
+
* @satisfies REQ-327, REQ-328, REQ-333, REQ-406
|
|
4624
4643
|
*/
|
|
4625
4644
|
async function configureContextFilesMenu(
|
|
4626
4645
|
ctx: ExtensionCommandContext,
|
|
4627
4646
|
config: UseReqConfig,
|
|
4628
4647
|
onConfigChange: () => void,
|
|
4648
|
+
maxContextTokens?: number | undefined,
|
|
4629
4649
|
): Promise<void> {
|
|
4630
4650
|
const contextFileSizes = measureContextFileSizes(getProjectBase(ctx.cwd), config);
|
|
4631
4651
|
const setFlag = (flagKey: "context-files-requirements" | "context-files-references" | "context-files-workflow", enabled: boolean): void => {
|
|
@@ -4635,9 +4655,9 @@ async function configureContextFilesMenu(
|
|
|
4635
4655
|
};
|
|
4636
4656
|
let focusedChoiceId: string | undefined;
|
|
4637
4657
|
while (true) {
|
|
4638
|
-
const choice = await showPiUsereqSettingsMenu(ctx, "Context Files", buildContextFilesMenuChoices(config, contextFileSizes), {
|
|
4658
|
+
const choice = await showPiUsereqSettingsMenu(ctx, "Context Files", buildContextFilesMenuChoices(config, contextFileSizes, maxContextTokens), {
|
|
4639
4659
|
initialSelectedId: focusedChoiceId,
|
|
4640
|
-
getChoices: () => buildContextFilesMenuChoices(config, contextFileSizes),
|
|
4660
|
+
getChoices: () => buildContextFilesMenuChoices(config, contextFileSizes, maxContextTokens),
|
|
4641
4661
|
onChange: (choiceId, newValue) => {
|
|
4642
4662
|
if (choiceId === "context-files-requirements") {
|
|
4643
4663
|
setFlag("context-files-requirements", newValue.startsWith("on"));
|
|
@@ -4688,12 +4708,14 @@ async function configureContextFilesMenu(
|
|
|
4688
4708
|
* @details Serializes primary configuration actions into right-valued menu rows consumed by the shared settings-menu renderer, including the `Context Files` injection toggles, automatic git-commit mode, effective prompt-command worktree state, notification summary, debug summary, locked worktree rows when automatic git commit is disabled, and display-only local plus global config paths. Runtime is O(s) in source-directory count. No external state is mutated.
|
|
4689
4709
|
* @param[in] cwd {string} Current working directory.
|
|
4690
4710
|
* @param[in] config {UseReqConfig} Effective project configuration.
|
|
4711
|
+
* @param[in] maxContextTokens {number | undefined} Selected model max input context tokens used by the `Context Files` occupancy suffix; undefined selects the documented 1,000,000-token fallback.
|
|
4691
4712
|
* @return {PiUsereqSettingsMenuChoice[]} Ordered top-level menu choices.
|
|
4692
|
-
* @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
|
|
4713
|
+
* @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, REQ-405
|
|
4693
4714
|
*/
|
|
4694
4715
|
function buildPiUsereqMenuChoices(
|
|
4695
4716
|
cwd: string,
|
|
4696
4717
|
config: UseReqConfig,
|
|
4718
|
+
maxContextTokens?: number | undefined,
|
|
4697
4719
|
): PiUsereqSettingsMenuChoice[] {
|
|
4698
4720
|
const autoGitCommitDisabled = config.AUTO_GIT_COMMIT === "disable";
|
|
4699
4721
|
const effectiveGitWorktreeEnabled = resolveEffectiveGitWorktreeEnabled(
|
|
@@ -4722,7 +4744,7 @@ function buildPiUsereqMenuChoices(
|
|
|
4722
4744
|
{
|
|
4723
4745
|
id: "context-files",
|
|
4724
4746
|
label: "Context Files",
|
|
4725
|
-
value: formatContextFilesSummary(config, measureContextFileSizes(cwd, config)),
|
|
4747
|
+
value: formatContextFilesSummary(config, measureContextFileSizes(cwd, config), maxContextTokens),
|
|
4726
4748
|
description: "Toggle injection of REQUIREMENTS.md, REFERENCES.md, and WORKFLOW.md into prompt context through `%%CONTEXT_FILES%%`.",
|
|
4727
4749
|
},
|
|
4728
4750
|
{
|
|
@@ -4865,6 +4887,7 @@ async function configurePiUsereq(
|
|
|
4865
4887
|
});
|
|
4866
4888
|
let config = loadProjectConfig(ctx.cwd);
|
|
4867
4889
|
const projectBase = getProjectBase(ctx.cwd);
|
|
4890
|
+
const maxContextTokens = resolveModelContextWindowTokens(statusController.state.contextUsage);
|
|
4868
4891
|
const initialShortcut = config["notify-sound-toggle-shortcut"];
|
|
4869
4892
|
const persistConfigChange = () => {
|
|
4870
4893
|
Object.assign(config, normalizeConfigPaths(projectBase, config));
|
|
@@ -4878,10 +4901,10 @@ async function configurePiUsereq(
|
|
|
4878
4901
|
const choice = await showPiUsereqSettingsMenu(
|
|
4879
4902
|
ctx,
|
|
4880
4903
|
"pi-usereq",
|
|
4881
|
-
buildPiUsereqMenuChoices(ctx.cwd, config),
|
|
4904
|
+
buildPiUsereqMenuChoices(ctx.cwd, config, maxContextTokens),
|
|
4882
4905
|
{
|
|
4883
4906
|
initialSelectedId: focusedChoiceId,
|
|
4884
|
-
getChoices: () => buildPiUsereqMenuChoices(ctx.cwd, config),
|
|
4907
|
+
getChoices: () => buildPiUsereqMenuChoices(ctx.cwd, config, maxContextTokens),
|
|
4885
4908
|
onChange: (choiceId, newValue) => {
|
|
4886
4909
|
if (choiceId === "auto-git-commit") {
|
|
4887
4910
|
config.AUTO_GIT_COMMIT = newValue === "enable" ? "enable" : "disable";
|
|
@@ -4934,7 +4957,7 @@ async function configurePiUsereq(
|
|
|
4934
4957
|
continue;
|
|
4935
4958
|
}
|
|
4936
4959
|
if (choice === "context-files") {
|
|
4937
|
-
await configureContextFilesMenu(ctx, config, persistConfigChange);
|
|
4960
|
+
await configureContextFilesMenu(ctx, config, persistConfigChange, maxContextTokens);
|
|
4938
4961
|
continue;
|
|
4939
4962
|
}
|
|
4940
4963
|
if (choice === "auto-git-commit") {
|
|
@@ -5079,7 +5102,7 @@ async function configurePiUsereq(
|
|
|
5079
5102
|
{ label: "Document directory", previousValue: config["docs-dir"], nextValue: defaultConfig["docs-dir"] },
|
|
5080
5103
|
{ label: "Source-code directories", previousValue: config["src-dir"].join(", "), nextValue: defaultConfig["src-dir"].join(", ") },
|
|
5081
5104
|
{ label: "Unit tests directory", previousValue: config["tests-dir"], nextValue: defaultConfig["tests-dir"] },
|
|
5082
|
-
{ label: "Context Files", previousValue: formatContextFilesSummary(config, measureContextFileSizes(ctx.cwd, config)), nextValue: formatContextFilesSummary(defaultConfig, measureContextFileSizes(ctx.cwd, defaultConfig)) },
|
|
5105
|
+
{ label: "Context Files", previousValue: formatContextFilesSummary(config, measureContextFileSizes(ctx.cwd, config), maxContextTokens), nextValue: formatContextFilesSummary(defaultConfig, measureContextFileSizes(ctx.cwd, defaultConfig), maxContextTokens) },
|
|
5083
5106
|
{ label: "Auto git commit", previousValue: config.AUTO_GIT_COMMIT, nextValue: defaultConfig.AUTO_GIT_COMMIT },
|
|
5084
5107
|
{ label: "Git worktree", previousValue: config.GIT_WORKTREE_ENABLED, nextValue: defaultConfig.GIT_WORKTREE_ENABLED },
|
|
5085
5108
|
{ label: "Worktree prefix", previousValue: config.GIT_WORKTREE_PREFIX, nextValue: defaultConfig.GIT_WORKTREE_PREFIX },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v0.8.0
|