pi-usereq 0.11.0 → 0.13.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +6 -6
  3. package/package.json +1 -1
  4. package/pi-usereq/docs/REFERENCES.md +1135 -834
  5. package/pi-usereq/docs/REQUIREMENTS.md +177 -110
  6. package/pi-usereq/docs/WORKFLOW.md +244 -79
  7. package/scripts/lib/extension-debug-harness.ts +2 -2
  8. package/scripts/tool-args-to-params.ts +2 -2
  9. package/src/cli.ts +12 -12
  10. package/src/core/config.ts +541 -180
  11. package/src/core/extension-status.ts +69 -12
  12. package/src/core/path-context.ts +19 -4
  13. package/src/core/pi-notify.ts +5 -5
  14. package/src/core/pi-usereq-tools.ts +4 -2
  15. package/src/core/prompt-command-catalog.ts +4 -5
  16. package/src/core/prompt-command-runtime.ts +183 -44
  17. package/src/core/prompts.ts +0 -2
  18. package/src/core/req-references-command.ts +175 -0
  19. package/src/core/req-reset-command.ts +323 -0
  20. package/src/core/resources.ts +6 -23
  21. package/src/core/settings-menu.ts +85 -28
  22. package/src/core/tool-runner.ts +26 -6
  23. package/src/index.ts +601 -116
  24. package/tests/attended-results-scenarios.ts +15 -9
  25. package/tests/cli-command-option-parity.test.ts +53 -35
  26. package/tests/debug-extension-harness.test.ts +8 -10
  27. package/tests/extension-registration.test.ts +1204 -205
  28. package/tests/helpers.ts +29 -6
  29. package/tests/oracle-project.test.ts +4 -4
  30. package/tests/oracle-standalone.test.ts +5 -5
  31. package/src/core/reference-payload.ts +0 -752
  32. package/src/resources/prompts/references.md +0 -64
  33. /package/tests/fixtures_attended_results/project/{references.json → summarize.json} +0 -0
  34. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_c.c.json +0 -0
  35. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_cpp.cpp.json +0 -0
  36. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_csharp.cs.json +0 -0
  37. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_elixir.ex.json +0 -0
  38. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_go.go.json +0 -0
  39. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_haskell.hs.json +0 -0
  40. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_java.java.json +0 -0
  41. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_javascript.js.json +0 -0
  42. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_kotlin.kt.json +0 -0
  43. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_lua.lua.json +0 -0
  44. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_perl.pl.json +0 -0
  45. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_php.php.json +0 -0
  46. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_python.py.json +0 -0
  47. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_ruby.rb.json +0 -0
  48. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_rust.rs.json +0 -0
  49. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_scala.scala.json +0 -0
  50. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_shell.sh.json +0 -0
  51. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_swift.swift.json +0 -0
  52. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_typescript.ts.json +0 -0
  53. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_zig.zig.json +0 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @file
3
+ * @brief Implements the specialized `req-references` slash-command workflow.
4
+ * @details Performs slash-command-owned git validation reuse, reference-file generation, targeted staging, fixed-message commit creation, and post-commit cleanliness verification without creating a worktree or starting an LLM session. Runtime is dominated by git subprocess execution plus source-summary generation and one documentation write. Side effects include filesystem writes and git index/history mutation.
5
+ */
6
+
7
+ import { spawnSync } from "node:child_process";
8
+ import path from "node:path";
9
+ import type { UseReqConfig } from "./config.js";
10
+ import { ReqError } from "./errors.js";
11
+ import { validatePromptGitState } from "./prompt-command-runtime.js";
12
+ import { runReferences } from "./tool-runner.js";
13
+
14
+ /**
15
+ * @brief Declares the fixed slash-command description for `req-references`.
16
+ * @details Preserves the legacy human-facing command label while the runtime implementation no longer depends on a bundled prompt Markdown file. Access complexity is O(1).
17
+ */
18
+ export const REQ_REFERENCES_COMMAND_DESCRIPTION = "Write a REFERENCES.md using the project's source code";
19
+
20
+ /**
21
+ * @brief Declares the fixed git commit message used by `req-references`.
22
+ * @details Keeps the commit payload deterministic so downstream tooling and tests can assert the exact commit contract without parsing prompt templates. Access complexity is O(1).
23
+ */
24
+ export const REQ_REFERENCES_COMMIT_MESSAGE = "docs(references): Update REFERENCES.md document. [useReq]";
25
+
26
+ /**
27
+ * @brief Describes the prepared execution facts for one `req-references` run.
28
+ * @details Stores the validated project base, resolved git root, target references path, and fixed commit message needed by the specialized direct-write workflow. The interface is compile-time only and introduces no runtime cost.
29
+ */
30
+ export interface ReqReferencesCommandPlan {
31
+ basePath: string;
32
+ gitPath: string;
33
+ referencesPath: string;
34
+ commitMessage: string;
35
+ }
36
+
37
+ /**
38
+ * @brief Executes one synchronous subprocess and captures UTF-8 output.
39
+ * @details Delegates to `spawnSync(...)`, preserves the supplied working directory, and returns the raw result so callers can interpret git exit status plus diagnostics deterministically. Runtime is dominated by external process execution. Side effects include subprocess creation.
40
+ * @param[in] command {string[]} Executable plus argument vector.
41
+ * @param[in] cwd {string} Working directory for the subprocess.
42
+ * @return {ReturnType<typeof spawnSync>} Captured subprocess result.
43
+ */
44
+ function runCapture(command: string[], cwd: string): ReturnType<typeof spawnSync> {
45
+ return spawnSync(command[0]!, command.slice(1), {
46
+ cwd,
47
+ encoding: "utf8",
48
+ });
49
+ }
50
+
51
+ /**
52
+ * @brief Builds the set of git-status paths ignored for cleanliness checks.
53
+ * @details Reuses the configured debug-log path exception already honored by prompt-command git validation so extension-owned debug artifacts do not block `req-references` execution or post-commit cleanliness verification. Runtime is O(p) in path length. No external state is mutated.
54
+ * @param[in] projectBase {string} Absolute project base path.
55
+ * @param[in] gitRoot {string} Absolute git root path.
56
+ * @param[in] config {UseReqConfig} Effective project configuration.
57
+ * @return {Set<string>} Slash-normalized relative paths ignored during git-status evaluation.
58
+ */
59
+ function buildIgnoredGitStatusPaths(
60
+ projectBase: string,
61
+ gitRoot: string,
62
+ config: UseReqConfig,
63
+ ): Set<string> {
64
+ const ignoredStatusPaths = new Set<string>();
65
+ const configuredLogPath = path.isAbsolute(config.DEBUG_LOG_FILE)
66
+ ? path.normalize(config.DEBUG_LOG_FILE)
67
+ : path.resolve(projectBase, config.DEBUG_LOG_FILE);
68
+ const relativeLogPath = path.relative(gitRoot, configuredLogPath);
69
+ if (relativeLogPath !== "" && !relativeLogPath.startsWith("..") && !path.isAbsolute(relativeLogPath)) {
70
+ ignoredStatusPaths.add(relativeLogPath.split(path.sep).join("/"));
71
+ }
72
+ return ignoredStatusPaths;
73
+ }
74
+
75
+ /**
76
+ * @brief Lists residual git-status rows after ignored extension-owned paths are filtered out.
77
+ * @details Executes `git status --porcelain`, drops the configured debug-log path when present inside the active repository, and returns all remaining staged or unstaged rows used for post-commit cleanliness verification. Runtime is dominated by one git subprocess plus O(n) parsing in status-line count. Side effects include subprocess creation.
78
+ * @param[in] projectBase {string} Absolute project base path.
79
+ * @param[in] gitRoot {string} Absolute git root path.
80
+ * @param[in] config {UseReqConfig} Effective project configuration.
81
+ * @return {string[]} Residual status rows after ignored paths are removed.
82
+ * @throws {ReqError} Throws when git status cannot be inspected.
83
+ */
84
+ function listResidualGitStatusLines(
85
+ projectBase: string,
86
+ gitRoot: string,
87
+ config: UseReqConfig,
88
+ ): string[] {
89
+ const statusResult = runCapture(["git", "status", "--porcelain"], gitRoot);
90
+ if (statusResult.error || statusResult.status !== 0) {
91
+ throw new ReqError("ERROR: Unable to inspect git repository cleanliness after req-references commit.", 1);
92
+ }
93
+ const ignoredStatusPaths = buildIgnoredGitStatusPaths(projectBase, gitRoot, config);
94
+ return statusResult.stdout
95
+ .split(/\r?\n/)
96
+ .map((line) => line.trimEnd())
97
+ .filter((line) => line !== "")
98
+ .filter((line) => {
99
+ const statusPath = line.slice(3).split(" -> ").at(-1)?.split(path.sep).join("/") ?? "";
100
+ return !ignoredStatusPaths.has(statusPath);
101
+ });
102
+ }
103
+
104
+ /**
105
+ * @brief Converts one absolute repository path into the preferred git-add target syntax.
106
+ * @details Emits a slash-normalized relative path when the target is inside the git root and falls back to the absolute path otherwise, preserving deterministic add semantics across nested project-base layouts. Runtime is O(p) in path length. No external state is mutated.
107
+ * @param[in] gitRoot {string} Absolute git root path.
108
+ * @param[in] absolutePath {string} Absolute path to stage.
109
+ * @return {string} Relative or absolute git-add target path.
110
+ */
111
+ function getGitAddTargetPath(gitRoot: string, absolutePath: string): string {
112
+ const relativePath = path.relative(gitRoot, absolutePath);
113
+ if (relativePath === "" || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
114
+ return absolutePath;
115
+ }
116
+ return relativePath.split(path.sep).join("/");
117
+ }
118
+
119
+ /**
120
+ * @brief Prepares the specialized `req-references` execution plan.
121
+ * @details Reuses slash-command-owned git validation, resolves the configured references document path, and returns the fixed commit metadata consumed by the direct-write workflow. Runtime is dominated by git validation subprocesses. Side effects include subprocess creation delegated through `validatePromptGitState(...)`.
122
+ * @param[in] projectBase {string} Absolute project base path.
123
+ * @param[in] config {UseReqConfig} Effective project configuration.
124
+ * @return {ReqReferencesCommandPlan} Prepared execution plan for direct references regeneration.
125
+ * @throws {ReqError} Throws when git validation fails.
126
+ * @satisfies REQ-200, REQ-299
127
+ */
128
+ export function prepareReqReferencesCommandExecution(
129
+ projectBase: string,
130
+ config: UseReqConfig,
131
+ ): ReqReferencesCommandPlan {
132
+ const basePath = path.resolve(projectBase);
133
+ const gitPath = validatePromptGitState(basePath, config);
134
+ const docsDir = config["docs-dir"].replace(/[/\\]+$/, "");
135
+ return {
136
+ basePath,
137
+ gitPath,
138
+ referencesPath: path.join(basePath, docsDir, "REFERENCES.md"),
139
+ commitMessage: REQ_REFERENCES_COMMIT_MESSAGE,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * @brief Executes the specialized `req-references` direct-write workflow.
145
+ * @details Regenerates `REFERENCES.md` through the same source-summary path used by the `references` tool, stages only the target file, creates the fixed-message commit, and verifies that no residual git-status rows remain after ignored extension-owned debug artifacts are filtered out. Runtime is dominated by summary generation plus three git subprocesses. Side effects include documentation writes, index mutation, commit creation, and subprocess creation.
146
+ * @param[in] plan {ReqReferencesCommandPlan} Prepared direct-write execution plan.
147
+ * @param[in] config {UseReqConfig} Effective project configuration.
148
+ * @return {void} No return value.
149
+ * @throws {ReqError} Throws when reference generation, staging, commit creation, or cleanliness verification fails.
150
+ * @satisfies REQ-300, REQ-301, REQ-302, REQ-303
151
+ */
152
+ export function executeReqReferencesCommandExecution(
153
+ plan: ReqReferencesCommandPlan,
154
+ config: UseReqConfig,
155
+ ): void {
156
+ runReferences(plan.basePath, config);
157
+ const addTargetPath = getGitAddTargetPath(plan.gitPath, plan.referencesPath);
158
+ const addResult = runCapture(["git", "add", "--", addTargetPath], plan.gitPath);
159
+ if (addResult.error || addResult.status !== 0) {
160
+ const diagnostic = addResult.stderr.trim() || addResult.error?.message || "unknown error";
161
+ throw new ReqError(`ERROR: git add failed for ${addTargetPath}: ${diagnostic}`, 1);
162
+ }
163
+ const commitResult = runCapture(["git", "commit", "-m", plan.commitMessage], plan.gitPath);
164
+ if (commitResult.error || commitResult.status !== 0) {
165
+ const diagnostic = commitResult.stderr.trim()
166
+ || commitResult.stdout.trim()
167
+ || commitResult.error?.message
168
+ || "unknown error";
169
+ throw new ReqError(`ERROR: git commit failed: ${diagnostic}`, 1);
170
+ }
171
+ const residualStatusLines = listResidualGitStatusLines(plan.basePath, plan.gitPath, config);
172
+ if (residualStatusLines.length > 0) {
173
+ throw new ReqError("ERROR: Git repository is not clean after req-references commit.", 1);
174
+ }
175
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * @file
3
+ * @brief Implements the specialized `req-reset` slash-command workflow.
4
+ * @details Performs non-agentic prompt-orchestration recovery by preserving the current execution-session transcript when available, restoring the original session-backed `base-path`, force-removing every generated sibling worktree and matching branch, and returning deterministic cleanup facts to the extension command handler. 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 removal.
5
+ */
6
+
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { spawnSync } from "node:child_process";
10
+ import {
11
+ normalizeGitWorktreePrefix,
12
+ type UseReqConfig,
13
+ } from "./config.js";
14
+ import { ReqError } from "./errors.js";
15
+ import {
16
+ deletePromptWorktree,
17
+ getPromptCommandErrorContext,
18
+ preservePromptCommandExecutionTranscript,
19
+ restorePromptCommandExecution,
20
+ type PromptCommandExecutionPlan,
21
+ } from "./prompt-command-runtime.js";
22
+ import { resolveRuntimeGitPath } from "./runtime-project-paths.js";
23
+
24
+ /**
25
+ * @brief Declares the fixed slash-command description for `req-reset`.
26
+ * @details Preserves a deterministic human-facing label for the dedicated non-agentic recovery command while keeping the implementation independent from bundled prompt Markdown files. Access complexity is O(1).
27
+ */
28
+ export const REQ_RESET_COMMAND_DESCRIPTION = "Reset req workflow state, restore base-path, and remove generated worktrees";
29
+
30
+ /**
31
+ * @brief Describes the prepared execution facts for one `req-reset` run.
32
+ * @details Stores the validated project base, resolved git root, sibling-worktree parent directory, generated-name matcher, and optional persisted prompt execution plan used for transcript preservation plus base-path restoration. The interface is compile-time only and introduces no runtime cost.
33
+ */
34
+ export interface ReqResetCommandPlan {
35
+ basePath: string;
36
+ gitPath: string;
37
+ parentPath: string;
38
+ worktreeNamePattern: RegExp;
39
+ promptRequest: PromptCommandExecutionPlan | undefined;
40
+ }
41
+
42
+ /**
43
+ * @brief Describes the outcome of one `req-reset` execution attempt.
44
+ * @details Captures the last valid session-bound context, transcript-preservation and base-path-restoration facts, removed generated worktree and branch names, and one aggregated failure string when any recovery step fails. The interface is compile-time only and introduces no runtime cost.
45
+ */
46
+ export interface ReqResetCommandExecutionResult {
47
+ activeContext: ReqResetCommandContext | undefined;
48
+ transcriptPreserved: boolean;
49
+ restoredBasePath: boolean;
50
+ removedWorktreeDirs: string[];
51
+ removedBranchNames: string[];
52
+ errorMessage?: string;
53
+ }
54
+
55
+ /**
56
+ * @brief Describes the session-bound context surface reused during `req-reset` recovery.
57
+ * @details Reuses the session-switching contract already accepted by `restorePromptCommandExecution(...)` so the dedicated reset command can restore the original session without depending on concrete pi runtime classes. The alias is compile-time only and introduces no runtime cost.
58
+ */
59
+ type ReqResetCommandContext = Parameters<typeof restorePromptCommandExecution>[1];
60
+
61
+ /**
62
+ * @brief Executes one synchronous subprocess and captures UTF-8 output.
63
+ * @details Delegates to `spawnSync(...)`, preserves the supplied working directory, and returns the raw result so callers can interpret git exit status plus diagnostics deterministically. Runtime is dominated by external process execution. Side effects include subprocess creation.
64
+ * @param[in] command {string[]} Executable plus argument vector.
65
+ * @param[in] cwd {string} Working directory for the subprocess.
66
+ * @return {ReturnType<typeof spawnSync>} Captured subprocess result.
67
+ */
68
+ function runCapture(command: string[], cwd: string): ReturnType<typeof spawnSync> {
69
+ return spawnSync(command[0]!, command.slice(1), {
70
+ cwd,
71
+ encoding: "utf8",
72
+ });
73
+ }
74
+
75
+ /**
76
+ * @brief Escapes one literal string for safe JavaScript regular-expression reuse.
77
+ * @details Prefixes every regular-expression metacharacter with `\\` so generated worktree-name patterns can embed persisted prefixes and repository basenames without introducing unintended matcher semantics. Runtime is O(n) in string length. No external state is mutated.
78
+ * @param[in] text {string} Literal text fragment.
79
+ * @return {string} Regular-expression-safe literal fragment.
80
+ */
81
+ function escapeReqResetRegExpLiteral(text: string): string {
82
+ return text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
83
+ }
84
+
85
+ /**
86
+ * @brief Builds the generated-worktree name matcher used by `req-reset` cleanup.
87
+ * @details Reuses the configured worktree prefix plus repository basename, accepts any sanitized branch token between those fixed segments and the final execution identifier, and constrains the timestamp suffix to the documented `YYYYMMDDHHMMSS` shape. Runtime is O(p) in combined prefix and project-name length. No external state is mutated.
88
+ * @param[in] gitRoot {string} Absolute runtime git root.
89
+ * @param[in] config {UseReqConfig} Effective project configuration.
90
+ * @return {RegExp} Matcher for generated prompt-command worktree and branch names.
91
+ * @satisfies REQ-309, REQ-310, REQ-311
92
+ */
93
+ function buildReqResetWorktreeNamePattern(gitRoot: string, config: UseReqConfig): RegExp {
94
+ const worktreePrefix = normalizeGitWorktreePrefix(config.GIT_WORKTREE_PREFIX);
95
+ const projectName = path.basename(gitRoot);
96
+ return new RegExp(
97
+ `^${escapeReqResetRegExpLiteral(worktreePrefix)}${escapeReqResetRegExpLiteral(projectName)}-.+-\\d{14}$`,
98
+ "u",
99
+ );
100
+ }
101
+
102
+ /**
103
+ * @brief Lists every registered git worktree root for one repository.
104
+ * @details Executes `git worktree list --porcelain`, extracts each `worktree <path>` record, resolves every listed path to an absolute form, and returns the ordered list used by generated-worktree cleanup. Runtime is dominated by one git subprocess plus O(n) parsing in listed worktree count. Side effects include subprocess creation.
105
+ * @param[in] gitRoot {string} Absolute runtime git root.
106
+ * @return {string[]} Absolute registered worktree-root paths.
107
+ * @throws {ReqError} Throws when git worktree enumeration fails.
108
+ */
109
+ function listReqResetRegisteredWorktreeRoots(gitRoot: string): string[] {
110
+ const listResult = runCapture(["git", "worktree", "list", "--porcelain"], gitRoot);
111
+ if (listResult.error || listResult.status !== 0) {
112
+ throw new ReqError("ERROR: Unable to enumerate git worktrees for req-reset.", 1);
113
+ }
114
+ return listResult.stdout
115
+ .split(/\r?\n/u)
116
+ .filter((line) => line.startsWith("worktree "))
117
+ .map((line) => path.resolve(line.slice("worktree ".length)));
118
+ }
119
+
120
+ /**
121
+ * @brief Lists sibling directories whose names match the generated-worktree contract.
122
+ * @details Reads the repository parent directory, keeps only direct child directories whose basenames match the supplied generated-name pattern, and resolves each candidate to an absolute path so `req-reset` can remove unregistered leftover directories as well as registered git worktrees. Runtime is dominated by directory enumeration plus O(n) matcher cost. Side effects are limited to filesystem reads.
123
+ * @param[in] parentPath {string} Absolute directory containing sibling worktree roots.
124
+ * @param[in] worktreeNamePattern {RegExp} Generated-worktree name matcher.
125
+ * @return {string[]} Absolute sibling directory paths whose basenames match the generated-name contract.
126
+ * @throws {ReqError} Throws when directory enumeration fails.
127
+ */
128
+ function listReqResetSiblingWorktreeRoots(
129
+ parentPath: string,
130
+ worktreeNamePattern: RegExp,
131
+ ): string[] {
132
+ const normalizedParentPath = path.resolve(parentPath);
133
+ let siblingEntries: fs.Dirent[];
134
+ try {
135
+ siblingEntries = fs.readdirSync(normalizedParentPath, { withFileTypes: true });
136
+ } catch (error) {
137
+ const errorMessage = error instanceof Error ? error.message : String(error);
138
+ throw new ReqError(
139
+ `ERROR: Unable to inspect sibling worktrees in ${normalizedParentPath}: ${errorMessage}.`,
140
+ 1,
141
+ );
142
+ }
143
+ return siblingEntries
144
+ .filter((entry) => entry.isDirectory() && worktreeNamePattern.test(entry.name))
145
+ .map((entry) => path.join(normalizedParentPath, entry.name));
146
+ }
147
+
148
+ /**
149
+ * @brief Lists every generated sibling worktree candidate targeted by `req-reset`.
150
+ * @details Unions registered git-worktree roots with matching sibling directories so cleanup covers both registered worktrees and unregistered leftover directories, then sorts the canonical absolute paths for deterministic deletion order. Runtime is dominated by git worktree enumeration plus sibling-directory scanning. Side effects are limited to subprocess creation and filesystem reads.
151
+ * @param[in] parentPath {string} Absolute directory containing sibling worktree roots.
152
+ * @param[in] gitRoot {string} Absolute runtime git root.
153
+ * @param[in] worktreeNamePattern {RegExp} Generated-worktree name matcher.
154
+ * @return {string[]} Sorted absolute worktree-root paths targeted for deletion.
155
+ * @throws {ReqError} Throws when git worktree or sibling-directory enumeration fails.
156
+ */
157
+ function listReqResetMatchingWorktreeRoots(
158
+ parentPath: string,
159
+ gitRoot: string,
160
+ worktreeNamePattern: RegExp,
161
+ ): string[] {
162
+ const matchingRoots = new Set<string>();
163
+ for (const worktreeRootPath of listReqResetRegisteredWorktreeRoots(gitRoot)) {
164
+ if (worktreeNamePattern.test(path.basename(worktreeRootPath))) {
165
+ matchingRoots.add(path.resolve(worktreeRootPath));
166
+ }
167
+ }
168
+ for (const siblingRootPath of listReqResetSiblingWorktreeRoots(parentPath, worktreeNamePattern)) {
169
+ matchingRoots.add(path.resolve(siblingRootPath));
170
+ }
171
+ return [...matchingRoots].sort((left, right) => left.localeCompare(right));
172
+ }
173
+
174
+ /**
175
+ * @brief Lists every matching generated branch targeted by `req-reset`.
176
+ * @details Executes `git branch --list --format=%(refname:short)`, filters the local branch inventory through the generated-name matcher, and returns a sorted list so later forced branch deletion remains deterministic. Runtime is dominated by one git subprocess plus O(n) parsing in listed branch count. Side effects include subprocess creation.
177
+ * @param[in] gitRoot {string} Absolute runtime git root.
178
+ * @param[in] worktreeNamePattern {RegExp} Generated-worktree name matcher.
179
+ * @return {string[]} Sorted local branch names targeted for deletion.
180
+ * @throws {ReqError} Throws when local branch enumeration fails.
181
+ */
182
+ function listReqResetMatchingBranchNames(
183
+ gitRoot: string,
184
+ worktreeNamePattern: RegExp,
185
+ ): string[] {
186
+ const branchResult = runCapture(["git", "branch", "--list", "--format=%(refname:short)"], gitRoot);
187
+ if (branchResult.error || branchResult.status !== 0) {
188
+ throw new ReqError("ERROR: Unable to enumerate git branches for req-reset.", 1);
189
+ }
190
+ return branchResult.stdout
191
+ .split(/\r?\n/u)
192
+ .map((line) => line.trim())
193
+ .filter((line) => line !== "" && worktreeNamePattern.test(line))
194
+ .sort((left, right) => left.localeCompare(right));
195
+ }
196
+
197
+ /**
198
+ * @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.
200
+ * @param[in] projectBase {string} Absolute project base path.
201
+ * @param[in] config {UseReqConfig} Effective project configuration.
202
+ * @param[in] promptRequest {PromptCommandExecutionPlan | undefined} Pending or active prompt execution plan when available.
203
+ * @return {ReqResetCommandPlan} Prepared recovery and cleanup plan.
204
+ * @throws {ReqError} Throws when the repository root cannot be resolved.
205
+ * @satisfies REQ-306, REQ-309, REQ-310, REQ-311
206
+ */
207
+ export function prepareReqResetCommandExecution(
208
+ projectBase: string,
209
+ config: UseReqConfig,
210
+ promptRequest?: PromptCommandExecutionPlan,
211
+ ): ReqResetCommandPlan {
212
+ const basePath = path.resolve(projectBase);
213
+ const gitPath = resolveRuntimeGitPath(basePath);
214
+ if (!gitPath) {
215
+ throw new ReqError("ERROR: Unable to resolve git repository for req-reset.", 1);
216
+ }
217
+ const normalizedGitPath = path.resolve(gitPath);
218
+ const resetPromptRequest = promptRequest?.worktreeDir
219
+ && promptRequest.worktreeRootPath
220
+ && promptRequest.worktreePath
221
+ ? promptRequest
222
+ : undefined;
223
+ return {
224
+ basePath,
225
+ gitPath: normalizedGitPath,
226
+ parentPath: path.resolve(normalizedGitPath, ".."),
227
+ worktreeNamePattern: buildReqResetWorktreeNamePattern(normalizedGitPath, config),
228
+ promptRequest: resetPromptRequest,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * @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.
235
+ * @param[in] plan {ReqResetCommandPlan} Prepared recovery and cleanup plan.
236
+ * @param[in] ctx {ReqResetCommandContext | undefined} Optional session-bound command context.
237
+ * @return {Promise<ReqResetCommandExecutionResult>} Recovery and cleanup outcome facts.
238
+ * @satisfies REQ-305, REQ-307, REQ-308, REQ-309, REQ-310, REQ-313
239
+ */
240
+ export async function executeReqResetCommandExecution(
241
+ plan: ReqResetCommandPlan,
242
+ ctx?: ReqResetCommandContext,
243
+ ): Promise<ReqResetCommandExecutionResult> {
244
+ let activeContext = ctx;
245
+ let transcriptPreserved = plan.promptRequest === undefined;
246
+ let restoredBasePath = plan.promptRequest === undefined;
247
+ const removedWorktreeDirs: string[] = [];
248
+ const removedBranchNames: string[] = [];
249
+ const errorMessages: string[] = [];
250
+
251
+ if (plan.promptRequest !== undefined) {
252
+ try {
253
+ preservePromptCommandExecutionTranscript(plan.promptRequest);
254
+ transcriptPreserved = true;
255
+ } catch (error) {
256
+ transcriptPreserved = false;
257
+ errorMessages.push(error instanceof Error ? error.message : String(error));
258
+ }
259
+ try {
260
+ activeContext = await restorePromptCommandExecution(plan.promptRequest, activeContext);
261
+ restoredBasePath = true;
262
+ } catch (error) {
263
+ return {
264
+ activeContext: (getPromptCommandErrorContext(error) ?? activeContext) as ReqResetCommandContext | undefined,
265
+ transcriptPreserved,
266
+ restoredBasePath: false,
267
+ removedWorktreeDirs,
268
+ removedBranchNames,
269
+ errorMessage: error instanceof Error ? error.message : String(error),
270
+ };
271
+ }
272
+ }
273
+
274
+ let matchingWorktreeRoots: string[] = [];
275
+ try {
276
+ matchingWorktreeRoots = listReqResetMatchingWorktreeRoots(
277
+ plan.parentPath,
278
+ plan.gitPath,
279
+ plan.worktreeNamePattern,
280
+ );
281
+ } catch (error) {
282
+ errorMessages.push(error instanceof Error ? error.message : String(error));
283
+ }
284
+
285
+ for (const worktreeRootPath of matchingWorktreeRoots) {
286
+ const worktreeDir = path.basename(worktreeRootPath);
287
+ try {
288
+ deletePromptWorktree(plan.basePath, worktreeDir, worktreeRootPath);
289
+ removedWorktreeDirs.push(worktreeDir);
290
+ } catch (error) {
291
+ errorMessages.push(error instanceof Error ? error.message : String(error));
292
+ }
293
+ }
294
+
295
+ try {
296
+ const matchingBranchNames = listReqResetMatchingBranchNames(plan.gitPath, plan.worktreeNamePattern);
297
+ for (const branchName of matchingBranchNames) {
298
+ const deleteResult = runCapture(["git", "branch", "-D", branchName], plan.gitPath);
299
+ if (deleteResult.error || deleteResult.status !== 0) {
300
+ const diagnostic = deleteResult.stderr.trim()
301
+ || deleteResult.stdout.trim()
302
+ || deleteResult.error?.message
303
+ || `Unable to remove branch ${branchName}.`;
304
+ errorMessages.push(`ERROR: git branch -D failed for ${branchName}: ${diagnostic}`);
305
+ continue;
306
+ }
307
+ removedBranchNames.push(branchName);
308
+ }
309
+ } catch (error) {
310
+ errorMessages.push(error instanceof Error ? error.message : String(error));
311
+ }
312
+
313
+ return {
314
+ activeContext,
315
+ transcriptPreserved,
316
+ restoredBasePath,
317
+ removedWorktreeDirs,
318
+ removedBranchNames,
319
+ errorMessage: errorMessages.length > 0
320
+ ? errorMessages.join(" ")
321
+ : undefined,
322
+ };
323
+ }
@@ -65,33 +65,16 @@ export function readBundledPrompt(promptName: string): string {
65
65
  }
66
66
 
67
67
  /**
68
- * @brief Extracts the YAML-front-matter `description` field from one bundled prompt.
69
- * @details Parses only the leading front-matter block, resolves the first scalar `description` entry, strips one matching pair of wrapping quotes, and unescapes quoted apostrophe or quote characters used in prompt metadata. Runtime is O(n) in prompt length. Side effects are limited to filesystem reads delegated through `readBundledPrompt(...)`.
68
+ * @brief Extracts the first Markdown level-one heading from one bundled prompt.
69
+ * @details Removes one optional leading YAML front-matter block, scans the remaining markdown body for the first line that begins with `# `, and returns the heading payload without the marker or surrounding whitespace. Runtime is O(n) in prompt length. Side effects are limited to filesystem reads delegated through `readBundledPrompt(...)`.
70
70
  * @param[in] promptName {string} Prompt identifier without the `.md` suffix.
71
- * @return {string} Normalized prompt description or the empty string when the front matter does not declare one.
71
+ * @return {string} First `# ` heading payload, or the empty string when no level-one heading exists.
72
72
  */
73
73
  export function readBundledPromptDescription(promptName: string): string {
74
74
  const promptText = readBundledPrompt(promptName);
75
- const frontMatterMatch = promptText.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
76
- if (!frontMatterMatch) {
77
- return "";
78
- }
79
- const descriptionLine = frontMatterMatch[1]
80
- .split(/\r?\n/)
81
- .find((line) => line.startsWith("description:"));
82
- if (!descriptionLine) {
83
- return "";
84
- }
85
- const rawValue = descriptionLine.slice("description:".length).trim();
86
- const unquotedValue = rawValue.length >= 2
87
- && ((rawValue.startsWith('"') && rawValue.endsWith('"'))
88
- || (rawValue.startsWith("'") && rawValue.endsWith("'")))
89
- ? rawValue.slice(1, -1)
90
- : rawValue;
91
- return unquotedValue
92
- .replace(/\\"/g, '"')
93
- .replace(/\\'/g, "'")
94
- .trim();
75
+ const promptBody = promptText.replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, "");
76
+ const headingMatch = promptBody.match(/^# (.+?)\s*$/m);
77
+ return headingMatch?.[1]?.trim() ?? "";
95
78
  }
96
79
 
97
80
  /**