pi-usereq 0.10.0 → 0.12.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/CHANGELOG.md +66 -0
- package/README.md +6 -6
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +861 -704
- package/pi-usereq/docs/REQUIREMENTS.md +152 -95
- package/pi-usereq/docs/WORKFLOW.md +228 -65
- package/scripts/lib/extension-debug-harness.ts +2 -2
- package/scripts/tool-args-to-params.ts +2 -2
- package/src/cli.ts +12 -12
- package/src/core/debug-runtime.ts +2 -2
- package/src/core/extension-status.ts +98 -36
- package/src/core/pi-notify.ts +5 -5
- package/src/core/pi-usereq-tools.ts +4 -2
- package/src/core/prompt-command-catalog.ts +4 -5
- package/src/core/prompt-command-runtime.ts +347 -42
- package/src/core/prompts.ts +0 -2
- package/src/core/req-references-command.ts +175 -0
- package/src/core/req-reset-command.ts +323 -0
- package/src/core/resources.ts +6 -23
- package/src/core/runtime-project-paths.ts +21 -1
- package/src/core/settings-menu.ts +85 -28
- package/src/core/tool-runner.ts +26 -6
- package/src/index.ts +530 -104
- package/tests/attended-results-scenarios.ts +5 -5
- package/tests/cli-command-option-parity.test.ts +25 -25
- package/tests/debug-extension-harness.test.ts +8 -2
- package/tests/extension-registration.test.ts +1109 -76
- package/tests/oracle-project.test.ts +4 -4
- package/tests/oracle-standalone.test.ts +5 -5
- package/src/core/reference-payload.ts +0 -752
- package/src/resources/prompts/references.md +0 -64
- /package/tests/fixtures_attended_results/project/{references.json → summarize.json} +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_c.c.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_cpp.cpp.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_csharp.cs.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_elixir.ex.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_go.go.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_haskell.hs.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_java.java.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_javascript.js.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_kotlin.kt.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_lua.lua.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_perl.pl.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_php.php.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_python.py.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_ruby.rb.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_rust.rs.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_scala.scala.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_shell.sh.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_swift.swift.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_typescript.ts.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_zig.zig.json +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file
|
|
3
|
-
* @brief Implements prompt-command preflight and worktree orchestration.
|
|
4
|
-
* @details Centralizes `req-<prompt>` repository validation, prompt-specific required-document checks, slash-command-owned worktree naming and lifecycle handling,
|
|
3
|
+
* @brief Implements bundled prompt-command preflight and worktree orchestration.
|
|
4
|
+
* @details Centralizes prompt-template-backed `req-<prompt>` repository validation, prompt-specific required-document checks, slash-command-owned worktree naming and lifecycle handling, reusable transcript-preservation plus session-restoration helpers, persisted replacement-session context reuse for non-command lifecycle handlers, matched-success stash-assisted fast-forward merge finalization, and command-side abort cleanup. Runtime is dominated by git subprocess execution plus bounded filesystem and session-file metadata checks. Side effects include active-session replacement, worktree creation and deletion, branch merges, stash-stack mutation, and filesystem reads and writes.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import fs from "node:fs";
|
|
@@ -19,10 +19,7 @@ import {
|
|
|
19
19
|
logDebugPromptWorkflowEvent,
|
|
20
20
|
type DebugWorkflowState,
|
|
21
21
|
} from "./debug-runtime.js";
|
|
22
|
-
import {
|
|
23
|
-
PROMPT_COMMAND_NAMES,
|
|
24
|
-
type PromptCommandName,
|
|
25
|
-
} from "./prompt-command-catalog.js";
|
|
22
|
+
import { type PromptCommandName } from "./prompt-command-catalog.js";
|
|
26
23
|
import {
|
|
27
24
|
isSameOrAncestorPath,
|
|
28
25
|
normalizeRelativeDirContract,
|
|
@@ -421,6 +418,169 @@ function readPromptSessionFileCwd(sessionFile: string): string | undefined {
|
|
|
421
418
|
return typeof headerCwd === "string" ? headerCwd : undefined;
|
|
422
419
|
}
|
|
423
420
|
|
|
421
|
+
/**
|
|
422
|
+
* @brief Reads one persisted session file as ordered parsed JSONL records.
|
|
423
|
+
* @details Loads the raw session file, preserves every non-empty serialized line verbatim, parses each line as one JSON object, and rejects unreadable or structurally invalid files so prompt-closure helpers can replay exact execution-session transcript records into the restored base session without reserialization drift. Runtime is O(n) in session-file size. No external state is mutated.
|
|
424
|
+
* @param[in] sessionFile {string} Absolute session-file path.
|
|
425
|
+
* @return {Array<{ rawLine: string; parsed: Record<string, unknown> }>} Parsed non-empty JSONL lines in file order.
|
|
426
|
+
* @throws {ReqError} Throws when the file cannot be read, when it contains no JSONL records, when any record is not a JSON object, or when the header record is missing.
|
|
427
|
+
*/
|
|
428
|
+
function readPromptSessionJsonLines(
|
|
429
|
+
sessionFile: string,
|
|
430
|
+
): Array<{ rawLine: string; parsed: Record<string, unknown> }> {
|
|
431
|
+
const normalizedSessionFile = path.resolve(sessionFile);
|
|
432
|
+
let raw: string;
|
|
433
|
+
try {
|
|
434
|
+
raw = fs.readFileSync(normalizedSessionFile, "utf8");
|
|
435
|
+
} catch (error) {
|
|
436
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
437
|
+
throw new ReqError(
|
|
438
|
+
`ERROR: Unable to read session file ${normalizedSessionFile}: ${errorMessage}.`,
|
|
439
|
+
1,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
const parsedLines: Array<{ rawLine: string; parsed: Record<string, unknown> }> = [];
|
|
443
|
+
for (const [index, rawLine] of raw.split(/\r?\n/u).entries()) {
|
|
444
|
+
if (rawLine.trim() === "") {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
let parsed: unknown;
|
|
448
|
+
try {
|
|
449
|
+
parsed = JSON.parse(rawLine);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
452
|
+
throw new ReqError(
|
|
453
|
+
`ERROR: Unable to parse session file ${normalizedSessionFile} line ${index + 1}: ${errorMessage}.`,
|
|
454
|
+
1,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
458
|
+
throw new ReqError(
|
|
459
|
+
`ERROR: Session file ${normalizedSessionFile} line ${index + 1} is not a JSON object.`,
|
|
460
|
+
1,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
parsedLines.push({ rawLine, parsed: parsed as Record<string, unknown> });
|
|
464
|
+
}
|
|
465
|
+
if (parsedLines.length === 0) {
|
|
466
|
+
throw new ReqError(`ERROR: Session file ${normalizedSessionFile} is empty.`, 1);
|
|
467
|
+
}
|
|
468
|
+
if (parsedLines[0]?.parsed.type !== "session") {
|
|
469
|
+
throw new ReqError(
|
|
470
|
+
`ERROR: Session file ${normalizedSessionFile} is missing a valid session header.`,
|
|
471
|
+
1,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return parsedLines;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @brief Copies successful execution-session transcript records into the restored base session file.
|
|
479
|
+
* @details Reads the execution session JSONL file, preserves the original base-session header when it already exists, materializes a restored base-session header when the reserved original session file is still pending persistence, appends any execution-session records missing from the original session in original execution order, and re-reads the restored file to verify both `base-path` cwd and copied entry identifiers. Runtime is O(n) in combined session-file size. Side effects include session-file creation or append operations for the restored base session.
|
|
480
|
+
* @param[in] plan {PromptCommandExecutionPlan} Prompt execution plan whose original and execution session files must be synchronized.
|
|
481
|
+
* @return {void} No return value.
|
|
482
|
+
* @throws {ReqError} Throws when either session file is unreadable or when appended execution records are not persisted to the original session file.
|
|
483
|
+
* @satisfies REQ-208, REQ-307
|
|
484
|
+
*/
|
|
485
|
+
export function preservePromptCommandExecutionTranscript(plan: PromptCommandExecutionPlan): void {
|
|
486
|
+
const normalizedOriginalSessionFile = path.resolve(plan.originalSessionFile);
|
|
487
|
+
const normalizedExecutionSessionFile = path.resolve(plan.executionSessionFile);
|
|
488
|
+
if (normalizedOriginalSessionFile === normalizedExecutionSessionFile) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const executionLines = readPromptSessionJsonLines(normalizedExecutionSessionFile);
|
|
492
|
+
const executionEntryIds = executionLines
|
|
493
|
+
.slice(1)
|
|
494
|
+
.map((line) => line.parsed.id)
|
|
495
|
+
.filter((entryId): entryId is string => typeof entryId === "string" && entryId !== "");
|
|
496
|
+
if (!fs.existsSync(normalizedOriginalSessionFile)) {
|
|
497
|
+
const rewrittenHeader: Record<string, unknown> = {
|
|
498
|
+
...executionLines[0]!.parsed,
|
|
499
|
+
cwd: plan.basePath,
|
|
500
|
+
};
|
|
501
|
+
if (
|
|
502
|
+
typeof rewrittenHeader.parentSession === "string"
|
|
503
|
+
&& path.resolve(rewrittenHeader.parentSession) === normalizedOriginalSessionFile
|
|
504
|
+
) {
|
|
505
|
+
delete rewrittenHeader.parentSession;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
fs.mkdirSync(path.dirname(normalizedOriginalSessionFile), { recursive: true });
|
|
509
|
+
const serializedLines = [
|
|
510
|
+
JSON.stringify(rewrittenHeader),
|
|
511
|
+
...executionLines.slice(1).map((line) => line.rawLine),
|
|
512
|
+
];
|
|
513
|
+
fs.writeFileSync(
|
|
514
|
+
normalizedOriginalSessionFile,
|
|
515
|
+
`${serializedLines.join("\n")}\n`,
|
|
516
|
+
"utf8",
|
|
517
|
+
);
|
|
518
|
+
} catch (error) {
|
|
519
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
520
|
+
throw new ReqError(
|
|
521
|
+
`ERROR: Unable to materialize restored session transcript in ${normalizedOriginalSessionFile}: ${errorMessage}.`,
|
|
522
|
+
1,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
const originalLines = readPromptSessionJsonLines(normalizedOriginalSessionFile);
|
|
527
|
+
const existingEntryIds = new Set<string>();
|
|
528
|
+
for (const line of originalLines.slice(1)) {
|
|
529
|
+
const entryId = line.parsed.id;
|
|
530
|
+
if (typeof entryId === "string" && entryId !== "") {
|
|
531
|
+
existingEntryIds.add(entryId);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const missingExecutionLines = executionLines.slice(1).filter((line) => {
|
|
535
|
+
const entryId = line.parsed.id;
|
|
536
|
+
return typeof entryId === "string"
|
|
537
|
+
&& entryId !== ""
|
|
538
|
+
&& !existingEntryIds.has(entryId);
|
|
539
|
+
});
|
|
540
|
+
if (missingExecutionLines.length > 0) {
|
|
541
|
+
const originalRaw = fs.readFileSync(normalizedOriginalSessionFile, "utf8");
|
|
542
|
+
const leadingSeparator = originalRaw === "" || originalRaw.endsWith("\n") ? "" : "\n";
|
|
543
|
+
try {
|
|
544
|
+
fs.appendFileSync(
|
|
545
|
+
normalizedOriginalSessionFile,
|
|
546
|
+
`${leadingSeparator}${missingExecutionLines.map((line) => line.rawLine).join("\n")}\n`,
|
|
547
|
+
"utf8",
|
|
548
|
+
);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
551
|
+
throw new ReqError(
|
|
552
|
+
`ERROR: Unable to preserve execution transcript in ${normalizedOriginalSessionFile}: ${errorMessage}.`,
|
|
553
|
+
1,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const synchronizedSessionCwd = readPromptSessionFileCwd(normalizedOriginalSessionFile);
|
|
559
|
+
if (
|
|
560
|
+
typeof synchronizedSessionCwd !== "string"
|
|
561
|
+
|| path.resolve(synchronizedSessionCwd) !== path.resolve(plan.basePath)
|
|
562
|
+
) {
|
|
563
|
+
throw new ReqError(
|
|
564
|
+
`ERROR: Restored session transcript expected cwd ${path.resolve(plan.basePath)} but observed ${synchronizedSessionCwd ?? "missing"}.`,
|
|
565
|
+
1,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
const synchronizedEntryIds = new Set<string>();
|
|
569
|
+
for (const line of readPromptSessionJsonLines(normalizedOriginalSessionFile).slice(1)) {
|
|
570
|
+
const entryId = line.parsed.id;
|
|
571
|
+
if (typeof entryId === "string" && entryId !== "") {
|
|
572
|
+
synchronizedEntryIds.add(entryId);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const missingSynchronizedIds = executionEntryIds.filter((entryId) => !synchronizedEntryIds.has(entryId));
|
|
576
|
+
if (missingSynchronizedIds.length > 0) {
|
|
577
|
+
throw new ReqError(
|
|
578
|
+
`ERROR: Unable to verify execution transcript preservation for ${normalizedOriginalSessionFile}: missing entries ${missingSynchronizedIds.join(", ")}.`,
|
|
579
|
+
1,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
424
584
|
/**
|
|
425
585
|
* @brief Verifies that the active session file and cwd surfaces match one expected prompt-orchestration target.
|
|
426
586
|
* @details Re-reads the persisted session-file header when present plus the host `process.cwd()` and throws on the first mismatch so prompt commands abort before prompt dispatch or prompt-end handling whenever session switching leaves execution attached to the wrong cwd. A missing persisted session file is treated as a non-fatal lazy-persistence state because pi's `SessionManager` writes session files on first assistant flush rather than eagerly during `ctx.switchSession(sessionPath)`; when the file is absent, pi aligns its internal session cwd to the live `process.cwd()`, so verifying `process.cwd()` alone is authoritative in that state. Reads of `ctx.cwd`, `ctx.sessionManager.getCwd()`, and `ctx.sessionManager.getSessionFile()` are advisory only because the pi `ctx.switchSession(sessionPath)` SDK contract does not mutate the handler-scoped `ctx` object, so those probes stay bound to the pre-switch session and a divergent value alone never triggers abort; they only surface a mismatch when they disagree with both the persisted header cwd and the live `process.cwd()`. Runtime is O(p) in aggregate path length plus one session-file header read. No external state is mutated.
|
|
@@ -666,9 +826,6 @@ const PROMPT_REQUIRED_DOCS: Record<PromptCommandName, readonly PromptRequiredDoc
|
|
|
666
826
|
{ fileName: "WORKFLOW.md", promptCommand: "/req-workflow" },
|
|
667
827
|
{ fileName: "REFERENCES.md", promptCommand: "/req-references" },
|
|
668
828
|
],
|
|
669
|
-
references: [
|
|
670
|
-
{ fileName: "REQUIREMENTS.md", promptCommand: "/req-write" },
|
|
671
|
-
],
|
|
672
829
|
renumber: [
|
|
673
830
|
{ fileName: "REQUIREMENTS.md", promptCommand: "/req-write" },
|
|
674
831
|
{ fileName: "WORKFLOW.md", promptCommand: "/req-workflow" },
|
|
@@ -692,6 +849,166 @@ function runCapture(command: string[], cwd: string): ReturnType<typeof spawnSync
|
|
|
692
849
|
});
|
|
693
850
|
}
|
|
694
851
|
|
|
852
|
+
/**
|
|
853
|
+
* @brief Lists tracked `base-path` status rows that require stash-assisted merge handling.
|
|
854
|
+
* @details Executes `git status --porcelain`, retains only tracked rows whose index or worktree slot reports a change, and excludes untracked or ignored rows because the required `git stash` command does not preserve them. Runtime is dominated by one git subprocess plus O(n) parsing in status-line count. Side effects include process spawning.
|
|
855
|
+
* @param[in] basePath {string} Restored project base path.
|
|
856
|
+
* @return {string[]} Tracked status rows requiring stash-assisted merge handling.
|
|
857
|
+
* @throws {ReqError} Throws when git status cannot be read from `basePath`.
|
|
858
|
+
* @satisfies REQ-291
|
|
859
|
+
*/
|
|
860
|
+
function listPromptTrackedBasePathChanges(basePath: string): string[] {
|
|
861
|
+
const statusResult = runCapture(["git", "status", "--porcelain"], basePath);
|
|
862
|
+
if (statusResult.error || statusResult.status !== 0) {
|
|
863
|
+
throw new ReqError("ERROR: Unable to inspect base-path changes before merge.", 1);
|
|
864
|
+
}
|
|
865
|
+
return statusResult.stdout
|
|
866
|
+
.split(/\r?\n/)
|
|
867
|
+
.map((line) => line.trimEnd())
|
|
868
|
+
.filter((line) => line !== "")
|
|
869
|
+
.filter((line) => {
|
|
870
|
+
const statusCode = line.slice(0, 2);
|
|
871
|
+
if (statusCode === "??" || statusCode === "!!") {
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
const indexStatus = statusCode[0] ?? " ";
|
|
875
|
+
const worktreeStatus = statusCode[1] ?? " ";
|
|
876
|
+
return indexStatus !== " " || worktreeStatus !== " ";
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* @brief Executes the successful-closure merge sequence from restored `base-path`.
|
|
882
|
+
* @details Detects tracked staged or unstaged `base-path` changes, wraps the existing fast-forward merge in `git stash` and `git stash pop` when required, preserves the direct merge path when no tracked changes exist, emits a warning-only result after successful local-change restoration, and writes one merge-finalization debug entry when enabled. Runtime is dominated by up to four git subprocesses plus O(n) status parsing. Side effects include stash-stack mutation, branch merge attempts, and optional debug-log writes.
|
|
883
|
+
* @param[in] plan {PromptCommandExecutionPlan} Prompt execution plan whose branch should be merged.
|
|
884
|
+
* @param[in] debugOptions {PromptCommandDebugOptions | undefined} Optional prompt debug logging context.
|
|
885
|
+
* @return {{ mergeAttempted: boolean; mergeSucceeded: boolean; errorMessage?: string; warningMessage?: string }} Merge-attempt facts plus optional warning text.
|
|
886
|
+
* @satisfies REQ-208, REQ-245, REQ-291, REQ-292
|
|
887
|
+
*/
|
|
888
|
+
function finalizePromptCommandMerge(
|
|
889
|
+
plan: PromptCommandExecutionPlan,
|
|
890
|
+
debugOptions?: PromptCommandDebugOptions,
|
|
891
|
+
): {
|
|
892
|
+
mergeAttempted: boolean;
|
|
893
|
+
mergeSucceeded: boolean;
|
|
894
|
+
errorMessage?: string;
|
|
895
|
+
warningMessage?: string;
|
|
896
|
+
} {
|
|
897
|
+
const worktreeDir = plan.worktreeDir ?? plan.branchName;
|
|
898
|
+
const mergeLogInput = {
|
|
899
|
+
worktree_dir: worktreeDir,
|
|
900
|
+
branch_name: plan.branchName,
|
|
901
|
+
};
|
|
902
|
+
let trackedStatusLines: string[];
|
|
903
|
+
try {
|
|
904
|
+
trackedStatusLines = listPromptTrackedBasePathChanges(plan.basePath);
|
|
905
|
+
} catch (error) {
|
|
906
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
907
|
+
if (debugOptions) {
|
|
908
|
+
logDebugPromptEvent(
|
|
909
|
+
plan.basePath,
|
|
910
|
+
debugOptions.config,
|
|
911
|
+
debugOptions.workflowState,
|
|
912
|
+
plan.promptName,
|
|
913
|
+
"merge",
|
|
914
|
+
mergeLogInput,
|
|
915
|
+
{
|
|
916
|
+
success: false,
|
|
917
|
+
merge_attempted: false,
|
|
918
|
+
error: errorMessage,
|
|
919
|
+
},
|
|
920
|
+
true,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
mergeAttempted: false,
|
|
925
|
+
mergeSucceeded: false,
|
|
926
|
+
errorMessage,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
const usedStash = trackedStatusLines.length > 0;
|
|
930
|
+
let stashStatus: number | null | undefined;
|
|
931
|
+
if (usedStash) {
|
|
932
|
+
const stashResult = runCapture(["git", "stash"], plan.basePath);
|
|
933
|
+
stashStatus = stashResult.status;
|
|
934
|
+
if (stashResult.error || stashResult.status !== 0) {
|
|
935
|
+
const errorMessage = `ERROR: Unable to stash base-path changes before merge for worktree ${worktreeDir}.`;
|
|
936
|
+
if (debugOptions) {
|
|
937
|
+
logDebugPromptEvent(
|
|
938
|
+
plan.basePath,
|
|
939
|
+
debugOptions.config,
|
|
940
|
+
debugOptions.workflowState,
|
|
941
|
+
plan.promptName,
|
|
942
|
+
"merge",
|
|
943
|
+
{
|
|
944
|
+
...mergeLogInput,
|
|
945
|
+
used_stash: true,
|
|
946
|
+
tracked_change_count: trackedStatusLines.length,
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
success: false,
|
|
950
|
+
merge_attempted: false,
|
|
951
|
+
error: errorMessage,
|
|
952
|
+
stash_status: stashStatus,
|
|
953
|
+
},
|
|
954
|
+
true,
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
return {
|
|
958
|
+
mergeAttempted: false,
|
|
959
|
+
mergeSucceeded: false,
|
|
960
|
+
errorMessage,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const mergeResult = runCapture(
|
|
965
|
+
["git", "merge", "--ff-only", plan.branchName],
|
|
966
|
+
plan.basePath,
|
|
967
|
+
);
|
|
968
|
+
const mergeSucceeded = !mergeResult.error && mergeResult.status === 0;
|
|
969
|
+
const errorMessage = mergeSucceeded
|
|
970
|
+
? undefined
|
|
971
|
+
: `ERROR: Fast-forward merge failed for worktree ${worktreeDir}.`;
|
|
972
|
+
let stashPopStatus: number | null | undefined;
|
|
973
|
+
let warningMessage: string | undefined;
|
|
974
|
+
if (usedStash) {
|
|
975
|
+
const stashPopResult = runCapture(["git", "stash", "pop"], plan.basePath);
|
|
976
|
+
stashPopStatus = stashPopResult.status;
|
|
977
|
+
if (mergeSucceeded) {
|
|
978
|
+
warningMessage = "WARNING: Restored base-path changes after merge; base-path is not clean.";
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (debugOptions) {
|
|
982
|
+
logDebugPromptEvent(
|
|
983
|
+
plan.basePath,
|
|
984
|
+
debugOptions.config,
|
|
985
|
+
debugOptions.workflowState,
|
|
986
|
+
plan.promptName,
|
|
987
|
+
"merge",
|
|
988
|
+
{
|
|
989
|
+
...mergeLogInput,
|
|
990
|
+
used_stash: usedStash,
|
|
991
|
+
tracked_change_count: trackedStatusLines.length,
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
success: mergeSucceeded,
|
|
995
|
+
merge_attempted: true,
|
|
996
|
+
error: errorMessage,
|
|
997
|
+
warning: warningMessage,
|
|
998
|
+
stash_status: stashStatus,
|
|
999
|
+
stash_pop_status: stashPopStatus,
|
|
1000
|
+
},
|
|
1001
|
+
!mergeSucceeded,
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
mergeAttempted: true,
|
|
1006
|
+
mergeSucceeded,
|
|
1007
|
+
errorMessage,
|
|
1008
|
+
warningMessage,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
695
1012
|
/**
|
|
696
1013
|
* @brief Stores or clears the prompt-command post-create test hook.
|
|
697
1014
|
* @details Enables deterministic simulation of post-create worktree verification failures without altering production control flow. Runtime is O(1). Side effect: mutates module-local test state.
|
|
@@ -787,15 +1104,15 @@ function throwPromptGitStatusError(): never {
|
|
|
787
1104
|
}
|
|
788
1105
|
|
|
789
1106
|
/**
|
|
790
|
-
* @brief Runs
|
|
791
|
-
* @details Validates work-tree membership, porcelain cleanliness, and symbolic or detached `HEAD` presence without invoking extension custom-tool executors. Runtime is dominated by git subprocess execution. Side effects include process spawning.
|
|
1107
|
+
* @brief Runs slash-command-owned git validation and returns the runtime git root.
|
|
1108
|
+
* @details Validates work-tree membership, porcelain cleanliness, and symbolic or detached `HEAD` presence for bundled prompt commands and `req-references` without invoking extension custom-tool executors. Runtime is dominated by git subprocess execution. Side effects include process spawning.
|
|
792
1109
|
* @param[in] projectBase {string} Absolute current project base.
|
|
793
1110
|
* @param[in] config {UseReqConfig | undefined} Optional effective project configuration used to ignore extension-owned debug-log artifacts.
|
|
794
1111
|
* @return {string} Absolute runtime git root.
|
|
795
1112
|
* @throws {ReqError} Throws the canonical prompt-command git-preflight error on any validation failure.
|
|
796
1113
|
* @satisfies REQ-200, REQ-220
|
|
797
1114
|
*/
|
|
798
|
-
function validatePromptGitState(projectBase: string, config?: UseReqConfig): string {
|
|
1115
|
+
export function validatePromptGitState(projectBase: string, config?: UseReqConfig): string {
|
|
799
1116
|
const gitPath = resolveRuntimeGitPath(projectBase);
|
|
800
1117
|
if (!gitPath) {
|
|
801
1118
|
throwPromptGitStatusError();
|
|
@@ -1117,9 +1434,9 @@ function createPromptWorktree(
|
|
|
1117
1434
|
* @param[in] debugOptions {PromptCommandDebugOptions | undefined} Optional prompt debug logging context.
|
|
1118
1435
|
* @return {void} No return value.
|
|
1119
1436
|
* @throws {ReqError} Throws when cleanup cannot remove the worktree and branch fully.
|
|
1120
|
-
* @satisfies REQ-208, REQ-220, REQ-245
|
|
1437
|
+
* @satisfies REQ-208, REQ-220, REQ-245, REQ-309
|
|
1121
1438
|
*/
|
|
1122
|
-
function deletePromptWorktree(
|
|
1439
|
+
export function deletePromptWorktree(
|
|
1123
1440
|
basePath: string,
|
|
1124
1441
|
worktreeDir: string,
|
|
1125
1442
|
worktreeRootPath: string,
|
|
@@ -1528,12 +1845,12 @@ export async function abortPromptCommandExecution(
|
|
|
1528
1845
|
|
|
1529
1846
|
/**
|
|
1530
1847
|
* @brief Finalizes one matched successful worktree-backed prompt execution.
|
|
1531
|
-
* @details Re-verifies persisted execution-session metadata plus worktree artifacts, restores the original session-backed `base-path`, fast-forward
|
|
1848
|
+
* @details Re-verifies persisted execution-session metadata plus worktree artifacts, copies any execution-session transcript records missing from the original session file, restores the original session-backed `base-path`, executes the stash-assisted fast-forward merge sequence from `base-path`, deletes the worktree after merge success, and preserves the restored base session across closure failures. Closure intentionally treats `base-path` restoration as authoritative even when pi CLI has already started end-of-session session replacement or other housekeeping that moved the live runtime away from `worktree-path`. Runtime is dominated by session switching plus git subprocess execution. Side effects include session-file appends, active-session replacement, branch merges, stash-stack mutation, worktree deletion, and optional debug-log writes.
|
|
1532
1849
|
* @param[in] plan {PromptCommandExecutionPlan} Prompt execution plan.
|
|
1533
1850
|
* @param[in] ctx {PromptCommandSessionContext | undefined} Optional prompt-command context.
|
|
1534
1851
|
* @param[in] debugOptions {PromptCommandDebugOptions | undefined} Optional prompt debug logging context.
|
|
1535
|
-
* @return {Promise<{ mergeAttempted: boolean; mergeSucceeded: boolean; cleanupSucceeded: boolean; errorMessage?: string; activeContext?: PromptCommandSessionContext }>} Finalization facts plus the last valid active prompt-command context.
|
|
1536
|
-
* @satisfies REQ-208, REQ-209, REQ-220, REQ-245, REQ-282
|
|
1852
|
+
* @return {Promise<{ mergeAttempted: boolean; mergeSucceeded: boolean; cleanupSucceeded: boolean; errorMessage?: string; warningMessage?: string; activeContext?: PromptCommandSessionContext }>} Finalization facts plus the last valid active prompt-command context.
|
|
1853
|
+
* @satisfies REQ-208, REQ-209, REQ-220, REQ-245, REQ-282, REQ-291, REQ-292
|
|
1537
1854
|
*/
|
|
1538
1855
|
export async function finalizePromptCommandExecution(
|
|
1539
1856
|
plan: PromptCommandExecutionPlan,
|
|
@@ -1544,12 +1861,16 @@ export async function finalizePromptCommandExecution(
|
|
|
1544
1861
|
mergeSucceeded: boolean;
|
|
1545
1862
|
cleanupSucceeded: boolean;
|
|
1546
1863
|
errorMessage?: string;
|
|
1864
|
+
warningMessage?: string;
|
|
1547
1865
|
activeContext?: PromptCommandSessionContext;
|
|
1548
1866
|
}> {
|
|
1549
1867
|
let activeContext = ctx;
|
|
1550
1868
|
let verificationErrorMessage: string | undefined;
|
|
1551
1869
|
try {
|
|
1552
1870
|
verifyPromptCommandClosureArtifacts(plan);
|
|
1871
|
+
if (plan.worktreePath && plan.worktreeDir && plan.worktreeRootPath) {
|
|
1872
|
+
preservePromptCommandExecutionTranscript(plan);
|
|
1873
|
+
}
|
|
1553
1874
|
} catch (error) {
|
|
1554
1875
|
verificationErrorMessage = error instanceof Error ? error.message : String(error);
|
|
1555
1876
|
}
|
|
@@ -1583,32 +1904,14 @@ export async function finalizePromptCommandExecution(
|
|
|
1583
1904
|
activeContext,
|
|
1584
1905
|
};
|
|
1585
1906
|
}
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1588
|
-
plan.basePath,
|
|
1589
|
-
);
|
|
1590
|
-
const mergeSucceeded = !mergeResult.error && mergeResult.status === 0;
|
|
1591
|
-
const errorMessage = mergeSucceeded
|
|
1592
|
-
? undefined
|
|
1593
|
-
: `ERROR: Fast-forward merge failed for worktree ${plan.worktreeDir}.`;
|
|
1594
|
-
if (debugOptions) {
|
|
1595
|
-
logDebugPromptEvent(
|
|
1596
|
-
plan.basePath,
|
|
1597
|
-
debugOptions.config,
|
|
1598
|
-
debugOptions.workflowState,
|
|
1599
|
-
plan.promptName,
|
|
1600
|
-
"merge",
|
|
1601
|
-
{ worktree_dir: plan.worktreeDir, branch_name: plan.branchName },
|
|
1602
|
-
{ success: mergeSucceeded, error: errorMessage },
|
|
1603
|
-
!mergeSucceeded,
|
|
1604
|
-
);
|
|
1605
|
-
}
|
|
1606
|
-
if (!mergeSucceeded) {
|
|
1907
|
+
const mergeFinalization = finalizePromptCommandMerge(plan, debugOptions);
|
|
1908
|
+
if (!mergeFinalization.mergeSucceeded) {
|
|
1607
1909
|
return {
|
|
1608
|
-
mergeAttempted:
|
|
1910
|
+
mergeAttempted: mergeFinalization.mergeAttempted,
|
|
1609
1911
|
mergeSucceeded: false,
|
|
1610
1912
|
cleanupSucceeded: true,
|
|
1611
|
-
errorMessage,
|
|
1913
|
+
errorMessage: mergeFinalization.errorMessage,
|
|
1914
|
+
warningMessage: mergeFinalization.warningMessage,
|
|
1612
1915
|
activeContext,
|
|
1613
1916
|
};
|
|
1614
1917
|
}
|
|
@@ -1622,17 +1925,19 @@ export async function finalizePromptCommandExecution(
|
|
|
1622
1925
|
);
|
|
1623
1926
|
} catch {
|
|
1624
1927
|
return {
|
|
1625
|
-
mergeAttempted:
|
|
1928
|
+
mergeAttempted: mergeFinalization.mergeAttempted,
|
|
1626
1929
|
mergeSucceeded: true,
|
|
1627
1930
|
cleanupSucceeded: false,
|
|
1628
1931
|
errorMessage: `ERROR: Unable to remove worktree or branch ${plan.worktreeDir}.`,
|
|
1932
|
+
warningMessage: mergeFinalization.warningMessage,
|
|
1629
1933
|
activeContext,
|
|
1630
1934
|
};
|
|
1631
1935
|
}
|
|
1632
1936
|
return {
|
|
1633
|
-
mergeAttempted:
|
|
1937
|
+
mergeAttempted: mergeFinalization.mergeAttempted,
|
|
1634
1938
|
mergeSucceeded: true,
|
|
1635
1939
|
cleanupSucceeded: true,
|
|
1940
|
+
warningMessage: mergeFinalization.warningMessage,
|
|
1636
1941
|
activeContext,
|
|
1637
1942
|
};
|
|
1638
1943
|
}
|
package/src/core/prompts.ts
CHANGED
|
@@ -22,14 +22,12 @@ import { readBundledInstruction, readBundledPrompt } from "./resources.js";
|
|
|
22
22
|
const TOOL_REFERENCE_REPLACEMENTS: Array<[RegExp, string]> = [
|
|
23
23
|
[/`req --find`/g, "`search` tool"],
|
|
24
24
|
[/`req --files-find`/g, "`files-search` tool"],
|
|
25
|
-
[/`req --references`/g, "`references` tool"],
|
|
26
25
|
[/`req --compress`/g, "`compress` tool"],
|
|
27
26
|
[/`req --tokens`/g, "`tokens` tool"],
|
|
28
27
|
[/`req --static-check`/g, "`static-check` tool"],
|
|
29
28
|
[/`req --files-static-check`/g, "`files-static-check` tool"],
|
|
30
29
|
[/\breq --find\b/g, "search tool"],
|
|
31
30
|
[/\breq --files-find\b/g, "files-search tool"],
|
|
32
|
-
[/\breq --references\b/g, "references tool"],
|
|
33
31
|
[/\breq --compress\b/g, "compress tool"],
|
|
34
32
|
[/\breq --tokens\b/g, "tokens tool"],
|
|
35
33
|
[/\breq --static-check\b/g, "static-check tool"],
|