pi-usereq 0.57.0 → 0.58.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.57.0)
1
+ # PI-useReq/pi-usereq (0.58.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+">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-usereq",
3
- "version": "0.57.0",
3
+ "version": "0.58.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Ogekuri/PI-useReq.git"
@@ -194,9 +194,132 @@ function listReqResetMatchingBranchNames(
194
194
  .sort((left, right) => left.localeCompare(right));
195
195
  }
196
196
 
197
+ /**
198
+ * @brief Resolves the main repository worktree root for one git path.
199
+ * @details Runs `git rev-parse --path-format=absolute --git-common-dir` from the supplied path, treats a common-directory result whose basename is `.git` as proof that its parent is the main repository worktree root, and falls back to the supplied root when git probing fails or the repository layout is non-standard. This keeps all `req-reset` cleanup probes anchored to the main repository even when the live pi session cwd is inside a linked worktree. Runtime is dominated by one git subprocess plus O(1) path math. Side effects include subprocess creation.
200
+ * @param[in] gitRoot {string} Absolute git worktree root used as probing cwd.
201
+ * @return {string} Absolute main repository worktree root.
202
+ */
203
+ function resolveReqResetMainWorktreeRoot(gitRoot: string): string {
204
+ const normalizedGitRoot = path.resolve(gitRoot);
205
+ const commonDirResult = runCapture(
206
+ ["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
207
+ normalizedGitRoot,
208
+ );
209
+ if (commonDirResult.error || commonDirResult.status !== 0) {
210
+ return normalizedGitRoot;
211
+ }
212
+ const commonDirPath = path.resolve(commonDirResult.stdout.trim());
213
+ return path.basename(commonDirPath) === ".git"
214
+ ? path.dirname(commonDirPath)
215
+ : normalizedGitRoot;
216
+ }
217
+
218
+ /**
219
+ * @brief Resolves the repository main branch name used by `req-reset` cleanup.
220
+ * @details Prefers the local branch referenced by `refs/remotes/origin/HEAD` with the leading remote `origin/` prefix stripped, then conventional local `main`, then local `master`, and returns `undefined` when no candidate exists locally so callers skip branch switching without losing cleanup facts. Runtime is dominated by up to three git subprocesses. Side effects include subprocess creation.
221
+ * @param[in] gitRoot {string} Absolute main repository worktree root.
222
+ * @return {string | undefined} Local main branch name or `undefined` when unresolvable.
223
+ */
224
+ function resolveReqResetMainBranchName(gitRoot: string): string | undefined {
225
+ const symbolicHeadResult = runCapture(
226
+ ["git", "symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
227
+ gitRoot,
228
+ );
229
+ const originCandidate = symbolicHeadResult.error || symbolicHeadResult.status !== 0
230
+ ? undefined
231
+ : symbolicHeadResult.stdout.trim();
232
+ for (const candidate of [
233
+ originCandidate?.startsWith("origin/") === true
234
+ ? originCandidate.slice("origin/".length)
235
+ : originCandidate,
236
+ "main",
237
+ "master",
238
+ ]) {
239
+ if (candidate === undefined || candidate === "" || candidate === "." || candidate === "..") {
240
+ continue;
241
+ }
242
+ const verifyResult = runCapture(
243
+ ["git", "show-ref", "--verify", "--quiet", `refs/heads/${candidate}`],
244
+ gitRoot,
245
+ );
246
+ if (!verifyResult.error && verifyResult.status === 0) {
247
+ return candidate;
248
+ }
249
+ }
250
+ return undefined;
251
+ }
252
+
253
+ /**
254
+ * @brief Ensures the main repository HEAD stays on the main branch before cleanup.
255
+ * @details Reads the current local branch of the main repository; when HEAD is checked out on a generated prompt-command branch that matches the cleanup matcher, resolves the repository main branch and switches to it so later forced branch deletion cannot fail with git's checked-out-worktree guard. Normal branches, detached HEAD, unresolvable main branches, and probing failures skip the switch defensively. Runtime is dominated by up to two git subprocesses. Side effects include main-worktree branch switching.
256
+ * @param[in] gitRoot {string} Absolute main repository worktree root.
257
+ * @param[in] worktreeNamePattern {RegExp} Generated-worktree name matcher.
258
+ * @return {void} No return value.
259
+ * @throws {ReqError} Throws when the main worktree cannot switch off one generated branch.
260
+ * @satisfies REQ-309, REQ-310
261
+ */
262
+ function ensureReqResetMainBranch(gitRoot: string, worktreeNamePattern: RegExp): void {
263
+ const currentBranchResult = runCapture(["git", "branch", "--show-current"], gitRoot);
264
+ if (currentBranchResult.error || currentBranchResult.status !== 0) {
265
+ return;
266
+ }
267
+ const currentBranch = currentBranchResult.stdout.trim();
268
+ if (currentBranch === "" || !worktreeNamePattern.test(currentBranch)) {
269
+ return;
270
+ }
271
+ const mainBranchName = resolveReqResetMainBranchName(gitRoot);
272
+ if (mainBranchName === undefined || mainBranchName === currentBranch) {
273
+ return;
274
+ }
275
+ const switchResult = runCapture(["git", "switch", mainBranchName], gitRoot);
276
+ if (switchResult.error || switchResult.status !== 0) {
277
+ const diagnostic = switchResult.stderr.trim()
278
+ || switchResult.stdout.trim()
279
+ || switchResult.error?.message
280
+ || `Unable to switch ${mainBranchName}.`;
281
+ throw new ReqError(`ERROR: git switch failed for ${mainBranchName}: ${diagnostic}`, 1);
282
+ }
283
+ }
284
+
285
+ /**
286
+ * @brief Restores live process and context cwd surfaces after worktree removal.
287
+ * @details Best-effort re-points `process.cwd()` and the supplied context `cwd` mirror to the main repository base path when the previously live cwd was removed by `req-reset` cleanup, so subsequent status rendering and notifications never probe deleted worktree paths. Runtime is O(1) plus bounded filesystem probes. Side effects include process cwd mutation and optional context mirror mutation.
288
+ * @param[in] basePath {string} Absolute main repository base path.
289
+ * @param[in,out] activeContext {ReqResetCommandContext | undefined} Mutated session context mirror.
290
+ * @return {void} No return value.
291
+ */
292
+ function restoreReqResetExecutionCwd(basePath: string, activeContext: ReqResetCommandContext | undefined): void {
293
+ const normalizedBasePath = path.resolve(basePath);
294
+ let processCwd = "";
295
+ try {
296
+ processCwd = path.resolve(process.cwd());
297
+ } catch (error) {
298
+ // Deleted process cwd after worktree removal.
299
+ }
300
+ if (processCwd === "" || !fs.existsSync(processCwd)) {
301
+ try {
302
+ process.chdir(normalizedBasePath);
303
+ } catch (error) {
304
+ // Best-effort; cleanup facts remain authoritative.
305
+ }
306
+ }
307
+ if (activeContext === undefined) {
308
+ return;
309
+ }
310
+ try {
311
+ const contextCwd = typeof activeContext.cwd === "string" ? path.resolve(activeContext.cwd) : "";
312
+ if (contextCwd !== normalizedBasePath && (contextCwd === "" || !fs.existsSync(contextCwd))) {
313
+ Reflect.set(activeContext, "cwd", normalizedBasePath);
314
+ }
315
+ } catch (error) {
316
+ // Best-effort context mirror mutation.
317
+ }
318
+ }
319
+
197
320
  /**
198
321
  * @brief Prepares the specialized `req-reset` execution plan.
199
- * @details Resolves the active project base into a runtime git root, derives the sibling-worktree parent directory and generated-name matcher from the same prefix plus repository-basename contract used by prompt-command worktree generation, and keeps only worktree-backed persisted prompt execution plans for transcript-preserving base-path restoration. Runtime is O(p) in path length. No external state is mutated.
322
+ * @details Resolves the active project base into a runtime git root, normalizes that root to the main repository worktree so cleanup and branch checks never run from inside a linked worktree, derives the sibling-worktree parent directory and generated-name matcher from the same prefix plus repository-basename contract used by prompt-command worktree generation, and keeps only worktree-backed persisted prompt execution plans for transcript-preserving base-path restoration. Runtime is O(p) in path length plus one git subprocess. No external state is mutated.
200
323
  * @param[in] projectBase {string} Absolute project base path.
201
324
  * @param[in] config {UseReqConfig} Effective project configuration.
202
325
  * @param[in] promptRequest {PromptCommandExecutionPlan | undefined} Pending or active prompt execution plan when available.
@@ -215,23 +338,25 @@ export function prepareReqResetCommandExecution(
215
338
  throw new ReqError("ERROR: Unable to resolve git repository for req-reset.", 1);
216
339
  }
217
340
  const normalizedGitPath = path.resolve(gitPath);
341
+ const mainGitPath = resolveReqResetMainWorktreeRoot(normalizedGitPath);
342
+ const mainBasePath = path.join(mainGitPath, path.relative(normalizedGitPath, basePath));
218
343
  const resetPromptRequest = promptRequest?.worktreeDir
219
344
  && promptRequest.worktreeRootPath
220
345
  && promptRequest.worktreePath
221
346
  ? promptRequest
222
347
  : undefined;
223
348
  return {
224
- basePath,
225
- gitPath: normalizedGitPath,
226
- parentPath: path.resolve(normalizedGitPath, ".."),
227
- worktreeNamePattern: buildReqResetWorktreeNamePattern(normalizedGitPath, config),
349
+ basePath: mainBasePath,
350
+ gitPath: mainGitPath,
351
+ parentPath: path.resolve(mainGitPath, ".."),
352
+ worktreeNamePattern: buildReqResetWorktreeNamePattern(mainGitPath, config),
228
353
  promptRequest: resetPromptRequest,
229
354
  };
230
355
  }
231
356
 
232
357
  /**
233
358
  * @brief Executes the specialized `req-reset` recovery and cleanup workflow.
234
- * @details Preserves the execution-session transcript into the original session file when a worktree-backed prompt execution plan is still available, restores the original session-backed `base-path` through the shared prompt-command restoration helper, force-removes every matching sibling worktree directory, force-removes every remaining matching local branch, and aggregates any failure diagnostics without rolling back successful cleanup steps. Runtime is dominated by session switching plus git subprocess execution. Side effects include session-file reads and writes, active-session replacement, host-process cwd mutation, worktree deletion, branch deletion, and filesystem reads.
359
+ * @details Preserves the execution-session transcript into the original session file when a worktree-backed prompt execution plan is still available, restores the original session-backed `base-path` through the shared prompt-command restoration helper, ensures the main repository HEAD is on the main branch so generated branch deletion cannot hit git's checked-out-worktree guard, force-removes every matching sibling worktree directory, force-removes every remaining matching local branch, restores deleted live cwd surfaces, and aggregates any failure diagnostics without rolling back successful cleanup steps. Runtime is dominated by session switching plus git subprocess execution. Side effects include session-file reads and writes, active-session replacement, host-process cwd mutation, branch switching, worktree deletion, branch deletion, and filesystem reads.
235
360
  * @param[in] plan {ReqResetCommandPlan} Prepared recovery and cleanup plan.
236
361
  * @param[in] ctx {ReqResetCommandContext | undefined} Optional session-bound command context.
237
362
  * @return {Promise<ReqResetCommandExecutionResult>} Recovery and cleanup outcome facts.
@@ -271,6 +396,12 @@ export async function executeReqResetCommandExecution(
271
396
  }
272
397
  }
273
398
 
399
+ try {
400
+ ensureReqResetMainBranch(plan.gitPath, plan.worktreeNamePattern);
401
+ } catch (error) {
402
+ errorMessages.push(error instanceof Error ? error.message : String(error));
403
+ }
404
+
274
405
  let matchingWorktreeRoots: string[] = [];
275
406
  try {
276
407
  matchingWorktreeRoots = listReqResetMatchingWorktreeRoots(
@@ -310,6 +441,8 @@ export async function executeReqResetCommandExecution(
310
441
  errorMessages.push(error instanceof Error ? error.message : String(error));
311
442
  }
312
443
 
444
+ restoreReqResetExecutionCwd(plan.basePath, activeContext);
445
+
313
446
  return {
314
447
  activeContext,
315
448
  transcriptPreserved,