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.
Files changed (52) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +6 -6
  3. package/package.json +1 -1
  4. package/pi-usereq/docs/REFERENCES.md +861 -704
  5. package/pi-usereq/docs/REQUIREMENTS.md +152 -95
  6. package/pi-usereq/docs/WORKFLOW.md +228 -65
  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/debug-runtime.ts +2 -2
  11. package/src/core/extension-status.ts +98 -36
  12. package/src/core/pi-notify.ts +5 -5
  13. package/src/core/pi-usereq-tools.ts +4 -2
  14. package/src/core/prompt-command-catalog.ts +4 -5
  15. package/src/core/prompt-command-runtime.ts +347 -42
  16. package/src/core/prompts.ts +0 -2
  17. package/src/core/req-references-command.ts +175 -0
  18. package/src/core/req-reset-command.ts +323 -0
  19. package/src/core/resources.ts +6 -23
  20. package/src/core/runtime-project-paths.ts +21 -1
  21. package/src/core/settings-menu.ts +85 -28
  22. package/src/core/tool-runner.ts +26 -6
  23. package/src/index.ts +530 -104
  24. package/tests/attended-results-scenarios.ts +5 -5
  25. package/tests/cli-command-option-parity.test.ts +25 -25
  26. package/tests/debug-extension-harness.test.ts +8 -2
  27. package/tests/extension-registration.test.ts +1109 -76
  28. package/tests/oracle-project.test.ts +4 -4
  29. package/tests/oracle-standalone.test.ts +5 -5
  30. package/src/core/reference-payload.ts +0 -752
  31. package/src/resources/prompts/references.md +0 -64
  32. /package/tests/fixtures_attended_results/project/{references.json → summarize.json} +0 -0
  33. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_c.c.json +0 -0
  34. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_cpp.cpp.json +0 -0
  35. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_csharp.cs.json +0 -0
  36. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_elixir.ex.json +0 -0
  37. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_go.go.json +0 -0
  38. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_haskell.hs.json +0 -0
  39. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_java.java.json +0 -0
  40. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_javascript.js.json +0 -0
  41. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_kotlin.kt.json +0 -0
  42. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_lua.lua.json +0 -0
  43. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_perl.pl.json +0 -0
  44. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_php.php.json +0 -0
  45. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_python.py.json +0 -0
  46. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_ruby.rb.json +0 -0
  47. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_rust.rs.json +0 -0
  48. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_scala.scala.json +0 -0
  49. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_shell.sh.json +0 -0
  50. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_swift.swift.json +0 -0
  51. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_typescript.ts.json +0 -0
  52. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_zig.zig.json +0 -0
@@ -5,6 +5,13 @@ import fs from "node:fs";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import piUsereqExtension from "../src/index.ts";
8
+ import {
9
+ REQ_REFERENCES_COMMAND_DESCRIPTION,
10
+ REQ_REFERENCES_COMMIT_MESSAGE,
11
+ } from "../src/core/req-references-command.js";
12
+ import {
13
+ REQ_RESET_COMMAND_DESCRIPTION,
14
+ } from "../src/core/req-reset-command.js";
8
15
  import {
9
16
  createStaticCheckLanguageConfig,
10
17
  DEFAULT_DOCS_DIR,
@@ -45,9 +52,11 @@ import {
45
52
  } from "../src/core/pi-usereq-tools.js";
46
53
  import {
47
54
  createPiUsereqStatusController,
55
+ renderPiUsereqStatus,
48
56
  setPiUsereqStatusConfig,
49
57
  setPiUsereqWorkflowState,
50
58
  } from "../src/core/extension-status.js";
59
+ import { showPiUsereqSettingsMenu } from "../src/core/settings-menu.js";
51
60
  import { getSupportedStaticCheckLanguageSupport } from "../src/core/static-check.js";
52
61
  import { createTempDir, initFixtureRepo } from "./helpers.js";
53
62
 
@@ -406,9 +415,30 @@ function formatFakeThemeBackgroundFromForeground(color: string, text: string): s
406
415
  return `<bg-from-fg-${color}>${text}</bg-from-fg-${color}>`;
407
416
  }
408
417
 
418
+ /**
419
+ * @brief Resolves the expected fake branch payload for status assertions.
420
+ * @details Reads `git branch --show-current` from the supplied base path and falls back to `unknown` when the path is outside a repository or HEAD has no branch name. Runtime is dominated by git execution. Side effects include subprocess creation.
421
+ * @param[in] basePath {string | undefined} Base path represented by the fake context.
422
+ * @return {string} Expected branch name or `unknown` when unavailable.
423
+ */
424
+ function buildExpectedFakeBranchValue(basePath: string | undefined): string {
425
+ if (!basePath || !fs.existsSync(basePath)) {
426
+ return "unknown";
427
+ }
428
+ const result = spawnSync("git", ["branch", "--show-current"], {
429
+ cwd: basePath,
430
+ encoding: "utf8",
431
+ });
432
+ if (result.error || result.status !== 0) {
433
+ return "unknown";
434
+ }
435
+ const branchName = result.stdout.trim();
436
+ return branchName === "" ? "unknown" : branchName;
437
+ }
438
+
409
439
  /**
410
440
  * @brief Builds the expected fake context-gauge payload for assertions.
411
- * @details Resolves the documented icon thresholds for `0-90%`, emits the blinking warning full icon for percent values above `90` through `100`, and emits the blinking red overflow icon for percent values above `100`. Runtime is O(1). No external state is mutated.
441
+ * @details Resolves the documented icon thresholds for `0`, `>0-<25`, `>=25-<50`, `>=50-<75`, and `>=75-<90`, renders those non-error bands with the same `warning` token used by the `status` value, emits the non-blinking error full icon for `>=90-<100`, and emits the blinking error full icon for `>=100`. Runtime is O(1). No external state is mutated.
412
442
  * @param[in] options {{ filledCells: number; percent?: number | null }} Expected context-gauge facts.
413
443
  * @return {string} Encoded context-gauge string.
414
444
  */
@@ -420,30 +450,33 @@ function buildExpectedFakeContextBar(options: {
420
450
  if (percent === undefined || percent === null || percent <= 0) {
421
451
  return formatFakeThemeForeground("warning", "▕_▏");
422
452
  }
423
- if (percent > 100) {
453
+ if (percent >= 100) {
424
454
  return formatFakeThemeForeground("error", "\u001b[5m▕█▏\u001b[25m");
425
455
  }
426
- if (percent <= 25) {
456
+ if (percent >= 90) {
457
+ return formatFakeThemeForeground("error", "▕█▏");
458
+ }
459
+ if (percent < 25) {
427
460
  return formatFakeThemeForeground("warning", "▕▂▏");
428
461
  }
429
- if (percent <= 50) {
462
+ if (percent < 50) {
430
463
  return formatFakeThemeForeground("warning", "▕▄▏");
431
464
  }
432
- if (percent <= 90) {
465
+ if (percent < 75) {
433
466
  return formatFakeThemeForeground("warning", "▕▆▏");
434
467
  }
435
- return formatFakeThemeForeground("warning", "\u001b[5m▕█▏\u001b[25m");
468
+ return formatFakeThemeForeground("warning", "▕█▏");
436
469
  }
437
470
 
438
471
  /**
439
472
  * @brief Builds the expected fake pi-usereq status-bar string for assertions.
440
- * @details Reconstructs the field order, workflow-state highlighting, separators, icon-based context gauge, consolidated elapsed field, and sound field emitted by the extension using deterministic fake theme markers. Runtime is O(1). No external state is mutated.
441
- * @param[in] options {{ workflowState?: string; basePath: string; contextFilledCells: number; contextPercent?: number | null; et: string; sound?: string }} Expected status facts where `basePath` carries the rendered `current-path` value.
473
+ * @details Reconstructs the field order, workflow-state highlighting, branch field, icon-based context gauge, consolidated elapsed field, and sound field emitted by the extension using deterministic fake theme markers. Runtime is O(1) plus optional git execution for branch discovery. No external state is mutated.
474
+ * @param[in] options {{ workflowState?: string; basePath?: string; contextFilledCells: number; contextPercent?: number | null; et: string; sound?: string }} Expected status facts for rendered `status`, `branch`, `context`, `elapsed`, and `sound` fields.
442
475
  * @return {string} Encoded status-bar string.
443
476
  */
444
477
  function buildExpectedFakeStatusText(options: {
445
478
  workflowState?: string;
446
- basePath: string;
479
+ basePath?: string;
447
480
  docsDir?: string;
448
481
  testsDir?: string;
449
482
  srcDir?: string[];
@@ -458,13 +491,14 @@ function buildExpectedFakeStatusText(options: {
458
491
  const workflowStateValue = workflowStateText === "error"
459
492
  ? formatFakeThemeForeground("error", "\u001b[5merror\u001b[25m")
460
493
  : formatFakeThemeForeground("warning", workflowStateText);
494
+ const branchValue = buildExpectedFakeBranchValue(options.basePath);
461
495
  const contextBar = buildExpectedFakeContextBar({
462
496
  filledCells: options.contextFilledCells,
463
497
  percent: options.contextPercent,
464
498
  });
465
499
  return [
466
500
  `${formatFakeThemeForeground("accent", "status:")}${workflowStateValue}`,
467
- buildField("current-path", options.basePath),
501
+ buildField("branch", branchValue),
468
502
  `${formatFakeThemeForeground("accent", "context:")}${contextBar}`,
469
503
  buildField("elapsed", options.et),
470
504
  buildField("sound", options.sound ?? "none"),
@@ -472,10 +506,11 @@ function buildExpectedFakeStatusText(options: {
472
506
  }
473
507
 
474
508
  /**
475
- * @brief Formats one expected absolute base path for fake status assertions.
509
+ * @brief Formats one expected absolute base path for prompt and status assertions.
476
510
  * @details Resolves the supplied cwd and normalizes path separators to `/` so
477
- * fake status comparisons stay stable across operating systems. Runtime is O(p)
478
- * in path length. No external state is mutated.
511
+ * prompt-text comparisons and legacy status-call-site inputs stay stable across
512
+ * operating systems. Runtime is O(p) in path length. No external state is
513
+ * mutated.
479
514
  * @param[in] cwd {string} Runtime working directory.
480
515
  * @return {string} Slash-normalized absolute base path.
481
516
  */
@@ -759,6 +794,7 @@ test("extension registers prompt and config commands while exposing tool capabil
759
794
  "req-recreate",
760
795
  "req-refactor",
761
796
  "req-references",
797
+ "req-reset",
762
798
  "req-renumber",
763
799
  "req-workflow",
764
800
  "req-write",
@@ -769,9 +805,10 @@ test("extension registers prompt and config commands while exposing tool capabil
769
805
 
770
806
  for (const name of [
771
807
  "files-tokens",
772
- "files-references",
808
+ "files-summarize",
773
809
  "files-compress",
774
810
  "files-search",
811
+ "summarize",
775
812
  "references",
776
813
  "compress",
777
814
  "search",
@@ -787,9 +824,10 @@ test("extension registers prompt and config commands while exposing tool capabil
787
824
  const toolNames = pi.tools.map((tool: RegisteredTool) => tool.name).sort();
788
825
  for (const name of [
789
826
  "files-tokens",
790
- "files-references",
827
+ "files-summarize",
791
828
  "files-compress",
792
829
  "files-search",
830
+ "summarize",
793
831
  "references",
794
832
  "compress",
795
833
  "search",
@@ -801,6 +839,46 @@ test("extension registers prompt and config commands while exposing tool capabil
801
839
  }
802
840
  });
803
841
 
842
+ /**
843
+ * @brief Verifies `req-references` registration is independent from bundled prompt inventory.
844
+ * @details Confirms `references` is absent from `PROMPT_COMMAND_NAMES` while extension activation still registers `req-references` with the fixed dedicated description. Runtime is O(1). Side effects are limited to extension registration.
845
+ * @return {void} No return value.
846
+ * @throws {AssertionError} Throws when `references` remains in the bundled prompt inventory or the command description diverges.
847
+ * @satisfies TST-102
848
+ */
849
+ test("req-references registers outside the bundled prompt inventory and keeps its dedicated description", () => {
850
+ const bundledPromptNames = PROMPT_COMMAND_NAMES as readonly string[];
851
+ assert.equal(bundledPromptNames.includes("references"), false);
852
+
853
+ const pi = createFakePi();
854
+ piUsereqExtension(pi);
855
+
856
+ assert.equal(
857
+ pi.commands.get("req-references")?.description,
858
+ REQ_REFERENCES_COMMAND_DESCRIPTION,
859
+ );
860
+ });
861
+
862
+ /**
863
+ * @brief Verifies `req-reset` registration is independent from bundled prompt inventory.
864
+ * @details Confirms `reset` is absent from `PROMPT_COMMAND_NAMES` while extension activation still registers `req-reset` with the fixed dedicated description. Runtime is O(1). Side effects are limited to extension registration.
865
+ * @return {void} No return value.
866
+ * @throws {AssertionError} Throws when `reset` remains in the bundled prompt inventory or the command description diverges.
867
+ * @satisfies TST-105
868
+ */
869
+ test("req-reset registers outside the bundled prompt inventory and keeps its dedicated description", () => {
870
+ const bundledPromptNames = PROMPT_COMMAND_NAMES as readonly string[];
871
+ assert.equal(bundledPromptNames.includes("reset"), false);
872
+
873
+ const pi = createFakePi();
874
+ piUsereqExtension(pi);
875
+
876
+ assert.equal(
877
+ pi.commands.get("req-reset")?.description,
878
+ REQ_RESET_COMMAND_DESCRIPTION,
879
+ );
880
+ });
881
+
804
882
  test("token tools register agent-oriented descriptions and schema details", () => {
805
883
  const pi = createFakePi();
806
884
  piUsereqExtension(pi);
@@ -819,24 +897,37 @@ test("token tools register agent-oriented descriptions and schema details", () =
819
897
  assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic pack-summary text/);
820
898
  });
821
899
 
822
- test("reference tools register agent-oriented descriptions and schema details", () => {
900
+ test("summary tools register agent-oriented descriptions and schema details", () => {
901
+ const pi = createFakePi();
902
+ piUsereqExtension(pi);
903
+
904
+ const filesSummarize = pi.tools.find((tool: RegisteredTool) => tool.name === "files-summarize");
905
+ const summarize = pi.tools.find((tool: RegisteredTool) => tool.name === "summarize");
906
+ assert.ok(filesSummarize, "missing files-summarize tool");
907
+ assert.ok(summarize, "missing summarize tool");
908
+
909
+ assert.match(filesSummarize.description ?? "", /monolithic summary markdown report/);
910
+ assert.ok(filesSummarize.promptGuidelines?.some((line) => line.includes("monolithic markdown")));
911
+ assert.ok(filesSummarize.promptGuidelines?.some((line) => line.includes("Formatting contract:")));
912
+ assert.match(String((filesSummarize.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
913
+
914
+ assert.match(summarize.description ?? "", /configured project source directories/);
915
+ assert.ok(summarize.promptGuidelines?.some((line) => line.includes("file-structure markdown block")));
916
+ assert.ok(summarize.promptGuidelines?.some((line) => line.includes("Configuration contract:")));
917
+ assert.match(String((summarize.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
918
+ });
919
+
920
+ test("references tool registers agent-oriented descriptions and schema details", () => {
823
921
  const pi = createFakePi();
824
922
  piUsereqExtension(pi);
825
923
 
826
- const filesReferences = pi.tools.find((tool: RegisteredTool) => tool.name === "files-references");
827
924
  const references = pi.tools.find((tool: RegisteredTool) => tool.name === "references");
828
- assert.ok(filesReferences, "missing files-references tool");
829
925
  assert.ok(references, "missing references tool");
830
926
 
831
- assert.match(filesReferences.description ?? "", /monolithic references markdown report/);
832
- assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("monolithic markdown")));
833
- assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("Formatting contract:")));
834
- assert.match(String((filesReferences.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
835
-
836
- assert.match(references.description ?? "", /configured project source directories/);
837
- assert.ok(references.promptGuidelines?.some((line) => line.includes("file-structure markdown block")));
838
- assert.ok(references.promptGuidelines?.some((line) => line.includes("Configuration contract:")));
839
- assert.match(String((references.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
927
+ assert.match(references.description ?? "", /configured project source directories and configured docs-dir/);
928
+ assert.ok(references.promptGuidelines?.some((line) => line.includes("overwrite `<docs-dir>/REFERENCES.md`")));
929
+ assert.ok(references.promptGuidelines?.some((line) => line.includes("generated markdown is never returned")));
930
+ assert.match(String((references.parameters as { description?: string } | undefined)?.description ?? ""), /content\[0\]\.text returns only `success` or `error: <diagnostic>`/);
840
931
  });
841
932
 
842
933
  test("search tools register agent-oriented descriptions and schema details", () => {
@@ -887,9 +978,10 @@ test("agent tools define custom renderResult with compact and expanded structure
887
978
  piUsereqExtension(pi);
888
979
  const toolNames = [
889
980
  "files-tokens",
890
- "files-references",
981
+ "files-summarize",
891
982
  "files-compress",
892
983
  "files-search",
984
+ "summarize",
893
985
  "references",
894
986
  "compress",
895
987
  "search",
@@ -937,7 +1029,7 @@ test("files-tokens returns monolithic token summary text with execution diagnost
937
1029
  docs: {
938
1030
  "REQUIREMENTS.md": "---\ntitle: Requirements\n---\n# Requirements\nAlpha\nBeta\n",
939
1031
  "WORKFLOW.md": "# Workflow\nStep one\n",
940
- "REFERENCES.md": "# References\nEntry one\n",
1032
+ "REFERENCES.md": "# Summarize\nEntry one\n",
941
1033
  },
942
1034
  });
943
1035
  try {
@@ -985,7 +1077,7 @@ test("files-tokens defers js-tiktoken loading until execution and returns monoli
985
1077
  }
986
1078
  });
987
1079
 
988
- test("files-references returns monolithic references markdown", async () => {
1080
+ test("files-summarize returns monolithic summary markdown", async () => {
989
1081
  const { projectBase } = initFixtureRepo({ fixtures: [] });
990
1082
  const samplePath = path.join(projectBase, "src", "sample.ts");
991
1083
  fs.writeFileSync(samplePath, `/**
@@ -1006,7 +1098,7 @@ export function buildSample(input: string): string {
1006
1098
  try {
1007
1099
  const pi = createFakePi();
1008
1100
  piUsereqExtension(pi);
1009
- const result = await executeRegisteredTool(pi, "files-references", projectBase, {
1101
+ const result = await executeRegisteredTool(pi, "files-summarize", projectBase, {
1010
1102
  files: ["src/sample.ts", "src/missing.ts"],
1011
1103
  }) as {
1012
1104
  content?: Array<{ type: string; text?: string }>;
@@ -1025,7 +1117,7 @@ export function buildSample(input: string): string {
1025
1117
  }
1026
1118
  });
1027
1119
 
1028
- test("references returns monolithic project references markdown", async () => {
1120
+ test("summarize returns monolithic project summary markdown", async () => {
1029
1121
  const { projectBase } = initFixtureRepo({ fixtures: [] });
1030
1122
  fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
1031
1123
  fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
@@ -1033,7 +1125,7 @@ test("references returns monolithic project references markdown", async () => {
1033
1125
  try {
1034
1126
  const pi = createFakePi();
1035
1127
  piUsereqExtension(pi);
1036
- const result = await executeRegisteredTool(pi, "references", projectBase, {}) as {
1128
+ const result = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
1037
1129
  content?: Array<{ type: string; text?: string }>;
1038
1130
  details?: { execution: { code: number; stderr_lines?: string[] } };
1039
1131
  };
@@ -1051,6 +1143,85 @@ test("references returns monolithic project references markdown", async () => {
1051
1143
  }
1052
1144
  });
1053
1145
 
1146
+ test("references overwrites the configured REFERENCES.md and returns status-only success", async () => {
1147
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
1148
+ fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
1149
+ fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
1150
+ fs.writeFileSync(path.join(projectBase, "src", "nested", "beta.ts"), "export function beta(): number {\n return 2;\n}\n", "utf8");
1151
+ const docsDir = path.join(projectBase, "docs", "custom");
1152
+ fs.mkdirSync(docsDir, { recursive: true });
1153
+ fs.writeFileSync(path.join(docsDir, "REFERENCES.md"), "stale\n", "utf8");
1154
+ const configPath = getProjectConfigPath(projectBase);
1155
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
1156
+ config["docs-dir"] = "docs/custom";
1157
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
1158
+
1159
+ try {
1160
+ const pi = createFakePi();
1161
+ piUsereqExtension(pi);
1162
+ const referencesResult = await executeRegisteredTool(pi, "references", projectBase, {}) as {
1163
+ content?: Array<{ type: string; text?: string }>;
1164
+ details?: { execution: { code: number; stderr_lines?: string[]; stdout_lines?: string[] } };
1165
+ };
1166
+ const summarizeResult = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
1167
+ content?: Array<{ type: string; text?: string }>;
1168
+ };
1169
+
1170
+ const referencesPath = path.join(docsDir, "REFERENCES.md");
1171
+ const writtenText = fs.readFileSync(referencesPath, "utf8");
1172
+ assert.equal(referencesResult.content?.[0]?.text ?? "", "success");
1173
+ assert.equal(writtenText.trimEnd(), summarizeResult.content?.[0]?.text ?? "");
1174
+ assert.doesNotMatch(writtenText, /^stale$/m);
1175
+ assert.equal(referencesResult.details?.execution.code, 0);
1176
+ assert.deepEqual(referencesResult.details?.execution.stderr_lines, undefined);
1177
+ assert.deepEqual(referencesResult.details?.execution.stdout_lines, undefined);
1178
+ } finally {
1179
+ fs.rmSync(projectBase, { recursive: true, force: true });
1180
+ }
1181
+ });
1182
+
1183
+ test("references returns status-only errors for source-discovery and write failures", async () => {
1184
+ const sourceFailure = initFixtureRepo({ fixtures: [] });
1185
+ try {
1186
+ const pi = createFakePi();
1187
+ piUsereqExtension(pi);
1188
+ const result = await executeRegisteredTool(pi, "references", sourceFailure.projectBase, {}) as {
1189
+ content?: Array<{ type: string; text?: string }>;
1190
+ details?: { execution: { code: number; stderr_lines?: string[]; stdout_lines?: string[] } };
1191
+ };
1192
+
1193
+ assert.equal(result.content?.[0]?.text ?? "", "error: no source files found in configured directories.");
1194
+ assert.equal(result.details?.execution.code, 1);
1195
+ assert.deepEqual(result.details?.execution.stdout_lines, undefined);
1196
+ assert.ok(result.details?.execution.stderr_lines?.some((line) => line.includes("no source files found in configured directories.")));
1197
+ } finally {
1198
+ fs.rmSync(sourceFailure.projectBase, { recursive: true, force: true });
1199
+ }
1200
+
1201
+ const writeFailure = initFixtureRepo({ fixtures: [] });
1202
+ fs.writeFileSync(path.join(writeFailure.projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
1203
+ const writeFailureConfigPath = getProjectConfigPath(writeFailure.projectBase);
1204
+ const writeFailureConfig = JSON.parse(fs.readFileSync(writeFailureConfigPath, "utf8")) as Record<string, unknown>;
1205
+ writeFailureConfig["docs-dir"] = "docs/missing/custom";
1206
+ fs.writeFileSync(writeFailureConfigPath, `${JSON.stringify(writeFailureConfig, null, 2)}\n`, "utf8");
1207
+
1208
+ try {
1209
+ const pi = createFakePi();
1210
+ piUsereqExtension(pi);
1211
+ const result = await executeRegisteredTool(pi, "references", writeFailure.projectBase, {}) as {
1212
+ content?: Array<{ type: string; text?: string }>;
1213
+ details?: { execution: { code: number; stderr_lines?: string[]; stdout_lines?: string[] } };
1214
+ };
1215
+
1216
+ assert.match(result.content?.[0]?.text ?? "", /^error: ENOENT:/);
1217
+ assert.equal(result.details?.execution.code, 1);
1218
+ assert.deepEqual(result.details?.execution.stdout_lines, undefined);
1219
+ assert.ok(result.details?.execution.stderr_lines?.some((line) => /^error: ENOENT:/.test(line)));
1220
+ } finally {
1221
+ fs.rmSync(writeFailure.projectBase, { recursive: true, force: true });
1222
+ }
1223
+ });
1224
+
1054
1225
  test("files-compress returns monolithic compression markdown", async () => {
1055
1226
  const { projectBase } = initFixtureRepo({ fixtures: [] });
1056
1227
  const samplePath = path.join(projectBase, "src", "sample.ts");
@@ -1238,21 +1409,21 @@ func (s *Server) Start(ctx context.Context) error {
1238
1409
  const pi = createFakePi();
1239
1410
  piUsereqExtension(pi);
1240
1411
 
1241
- const filesReferencesResult = await executeRegisteredTool(pi, "files-references", projectBase, {
1412
+ const filesSummarizeResult = await executeRegisteredTool(pi, "files-summarize", projectBase, {
1242
1413
  files: ["src/sample.go"],
1243
1414
  }) as {
1244
1415
  content?: Array<{ type: string; text?: string }>;
1245
1416
  details?: { execution: { code: number } };
1246
1417
  };
1247
- assert.match(filesReferencesResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
1248
- assert.equal(filesReferencesResult.details?.execution.code, 0);
1418
+ assert.match(filesSummarizeResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
1419
+ assert.equal(filesSummarizeResult.details?.execution.code, 0);
1249
1420
 
1250
- const referencesResult = await executeRegisteredTool(pi, "references", projectBase, {}) as {
1421
+ const summarizeResult = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
1251
1422
  content?: Array<{ type: string; text?: string }>;
1252
1423
  details?: { execution: { code: number } };
1253
1424
  };
1254
- assert.match(referencesResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
1255
- assert.equal(referencesResult.details?.execution.code, 0);
1425
+ assert.match(summarizeResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
1426
+ assert.equal(summarizeResult.details?.execution.code, 0);
1256
1427
 
1257
1428
  const filesCompressResult = await executeRegisteredTool(pi, "files-compress", projectBase, {
1258
1429
  files: ["src/sample.go"],
@@ -1475,12 +1646,12 @@ test("configuration menus expose show-config ordering and omit overview or notif
1475
1646
  assert.ok(renderedMenu.includes("disable"));
1476
1647
  assert.doesNotMatch(renderedMenu, /beep:/);
1477
1648
  assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
1478
- assert.match(renderedNotificationsMenu, /<dim>off<\/dim>/);
1649
+ assert.match(renderedNotificationsMenu, /<dim>configure user\/token keys first<\/dim>/);
1479
1650
  assert.deepEqual(ctx.__state.selectCalls[1]?.items ?? [], [
1480
1651
  "Enable notification",
1481
1652
  "Notification events",
1482
1653
  "Notify command",
1483
- "Enable sound",
1654
+ "Enable sound (boot value)",
1484
1655
  "Sound events",
1485
1656
  "Sound toggle hotkey bind",
1486
1657
  "Sound command (low vol.)",
@@ -1530,7 +1701,7 @@ test("descendant configuration menus end with Reset defaults only", async () =>
1530
1701
  const ctx = createFakeCtx(cwd, {
1531
1702
  selects: [
1532
1703
  "notifications",
1533
- "Enable sound",
1704
+ "Enable sound (boot value)",
1534
1705
  "Save and close",
1535
1706
  "Pushover priority",
1536
1707
  "Save and close",
@@ -1557,19 +1728,34 @@ test("descendant configuration menus end with Reset defaults only", async () =>
1557
1728
 
1558
1729
  await command!.handler("", ctx);
1559
1730
 
1731
+ const getMenuIndex = (title: string, marker?: string): number => {
1732
+ const index = ctx.__state.selectCalls.findIndex(
1733
+ (entry) => entry.title === title && (marker === undefined || entry.items.includes(marker)),
1734
+ );
1735
+ assert.notEqual(index, -1, `missing menu ${title}`);
1736
+ return index;
1737
+ };
1560
1738
  const getTerminalRows = (title: string, marker?: string): string[] => {
1561
- const call = ctx.__state.selectCalls.find((entry) => entry.title === title && (marker === undefined || entry.items.includes(marker)));
1562
- assert.ok(call, `missing menu ${title}`);
1563
- return call.items.slice(-1);
1739
+ const index = getMenuIndex(title, marker);
1740
+ return ctx.__state.selectCalls[index]!.items.slice(-1);
1741
+ };
1742
+ const getRenderedMenu = (title: string, marker?: string): string => {
1743
+ const index = getMenuIndex(title, marker);
1744
+ return (ctx.__state.customRenderLines[index] ?? []).join("\n");
1564
1745
  };
1565
1746
 
1566
- assert.deepEqual(getTerminalRows("Enable sound", "high"), ["Reset defaults"]);
1747
+ assert.deepEqual(getTerminalRows("Enable sound (boot value)", "high"), ["Reset defaults"]);
1567
1748
  assert.deepEqual(getTerminalRows("Pushover priority", "High"), ["Reset defaults"]);
1568
1749
  assert.deepEqual(getTerminalRows("Enable tools", "static-check"), ["Reset defaults"]);
1569
1750
  assert.deepEqual(getTerminalRows("Debug", "Log on status"), ["Reset defaults"]);
1570
1751
  assert.deepEqual(getTerminalRows("static-check language", "Python"), ["Reset defaults"]);
1571
1752
  assert.deepEqual(getTerminalRows("Remove static code checker"), ["Reset defaults"]);
1572
1753
  assert.deepEqual(getTerminalRows("Remove source-code directory", "src"), ["Reset defaults"]);
1754
+ assert.doesNotMatch(getRenderedMenu("Enable sound (boot value)", "high"), /Reset defaults.*none/);
1755
+ assert.doesNotMatch(getRenderedMenu("Pushover priority", "High"), /Reset defaults.*Normal/);
1756
+ assert.doesNotMatch(getRenderedMenu("Enable tools", "static-check"), /Reset defaults.*defaults/);
1757
+ assert.doesNotMatch(getRenderedMenu("Debug", "Log on status"), /Reset defaults.*running/);
1758
+ assert.doesNotMatch(getRenderedMenu("Remove source-code directory", "src"), /Reset defaults.*src/);
1573
1759
  });
1574
1760
 
1575
1761
  test("debug menu dims locked rows and persists debug settings with focus-preserving re-renders", async () => {
@@ -1876,6 +2062,101 @@ test("notifications menu preserves focus on toggled and edited rows", async () =
1876
2062
  assert.equal(notificationEventCalls[1]?.selectedChoiceId, "notify-on-failed");
1877
2063
  });
1878
2064
 
2065
+ /**
2066
+ * @brief Verifies inline toggle rows stay open and preserve selection for confirm keys.
2067
+ * @details Builds one shared settings-menu instance with a primary inline toggle and a dependent secondary row whose disabled styling changes after each toggle. The test proves `space` and `enter` must mutate the selected row in place, rebuild dependent rows without closing the menu, and preserve focus on the toggled row until explicit cancel.
2068
+ * @return {Promise<void>} Promise resolved after the raw-input interaction sequence completes.
2069
+ * @throws {AssertionError} Throws when the menu closes early, loses focus, or fails to rebuild dependent row styling after a toggle.
2070
+ * @satisfies REQ-192, TST-052
2071
+ */
2072
+ test("shared settings menu keeps inline toggles focused and open for space and enter", async () => {
2073
+ let primaryEnabled = true;
2074
+ let component:
2075
+ | {
2076
+ handleInput?: (data: string) => void;
2077
+ render: (width: number) => string[];
2078
+ }
2079
+ | undefined;
2080
+ let resolved = false;
2081
+ const changes: string[] = [];
2082
+ const theme = {
2083
+ fg: (color: string, text: string) => formatFakeThemeForeground(color, text),
2084
+ bgFromFg: (color: string, text: string) => formatFakeThemeBackgroundFromForeground(color, text),
2085
+ bold: (text: string) => `<bold>${text}</bold>`,
2086
+ };
2087
+ const ctx = {
2088
+ ui: {
2089
+ async custom<T>(factory: (tui: { requestRender: () => void }, customTheme: typeof theme, keybindings: Record<string, never>, done: (value?: T) => void) => unknown) {
2090
+ let resolveResult!: (value: T | undefined) => void;
2091
+ const resultPromise = new Promise<T | undefined>((resolve) => {
2092
+ resolveResult = (value?: T) => {
2093
+ resolved = true;
2094
+ resolve(value);
2095
+ };
2096
+ });
2097
+ component = factory(
2098
+ { requestRender: () => undefined },
2099
+ theme,
2100
+ {},
2101
+ (value?: T) => resolveResult(value),
2102
+ ) as typeof component;
2103
+ return resultPromise;
2104
+ },
2105
+ },
2106
+ } as any;
2107
+ const buildChoices = () => [
2108
+ {
2109
+ id: "primary",
2110
+ label: "Primary",
2111
+ value: primaryEnabled ? "enable" : "disable",
2112
+ values: ["enable", "disable"],
2113
+ description: "Toggle the primary row in place.",
2114
+ },
2115
+ {
2116
+ id: "secondary",
2117
+ label: "Secondary",
2118
+ labelTone: primaryEnabled ? undefined : "dim",
2119
+ value: primaryEnabled ? "editable" : "locked",
2120
+ valueTone: primaryEnabled ? undefined : "dim",
2121
+ disabled: !primaryEnabled,
2122
+ description: "Dependent row that must rebuild without closing the menu.",
2123
+ },
2124
+ ] as any;
2125
+
2126
+ const resultPromise = showPiUsereqSettingsMenu(
2127
+ ctx,
2128
+ "Toggle menu",
2129
+ buildChoices(),
2130
+ {
2131
+ getChoices: buildChoices,
2132
+ onChange: (choiceId: string, newValue: string) => {
2133
+ if (choiceId === "primary") {
2134
+ primaryEnabled = newValue === "enable";
2135
+ changes.push(newValue);
2136
+ }
2137
+ },
2138
+ } as any,
2139
+ );
2140
+
2141
+ assert.ok(component);
2142
+ assert.doesNotMatch(component.render(120).join("\n"), /<dim>Secondary/);
2143
+
2144
+ component.handleInput?.(" ");
2145
+ await Promise.resolve();
2146
+ assert.deepEqual(changes, ["disable"]);
2147
+ assert.equal(resolved, false);
2148
+ assert.match(component.render(120).join("\n"), /<dim>Secondary/);
2149
+
2150
+ component.handleInput?.("\r");
2151
+ await Promise.resolve();
2152
+ assert.deepEqual(changes, ["disable", "enable"]);
2153
+ assert.equal(resolved, false);
2154
+ assert.doesNotMatch(component.render(120).join("\n"), /<dim>Secondary/);
2155
+
2156
+ component.handleInput?.("\x1b");
2157
+ assert.equal(await resultPromise, undefined);
2158
+ });
2159
+
1879
2160
  test("notifications reset defaults preserves non-notification settings", async () => {
1880
2161
  const cwd = createTempDir("pi-usereq-menu-reset-notifications-");
1881
2162
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
@@ -1888,7 +2169,7 @@ test("notifications reset defaults preserves non-notification settings", async (
1888
2169
  "docs-dir",
1889
2170
  "notifications",
1890
2171
  "Enable notification",
1891
- "Enable sound",
2172
+ "Enable sound (boot value)",
1892
2173
  "high",
1893
2174
  "Reset defaults",
1894
2175
  "Approve reset",
@@ -1992,6 +2273,376 @@ test("prompt commands use the current session by default", async () => {
1992
2273
  }
1993
2274
  });
1994
2275
 
2276
+ /**
2277
+ * @brief Verifies successful `req-references` completion uses the specialized direct commit workflow.
2278
+ * @details Confirms the command regenerates configured `REFERENCES.md`, creates no worktree, sends no LLM user message, stages only the target file, commits the fixed message, and restores workflow state `idle`. Runtime is dominated by temporary git execution and reference generation. Side effects are limited to temporary repository mutation and command-handler notifications.
2279
+ * @return {Promise<void>} Promise resolved after success-path assertions complete.
2280
+ * @throws {AssertionError} Throws when `req-references` creates a worktree, starts a session handoff, skips the fixed commit, or leaves the repository dirty.
2281
+ * @satisfies TST-103
2282
+ */
2283
+ test("req-references updates REFERENCES.md with a direct commit workflow and no LLM session", async () => {
2284
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
2285
+ const previousCwd = process.cwd();
2286
+ try {
2287
+ fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
2288
+ fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
2289
+ fs.writeFileSync(path.join(projectBase, "src", "nested", "beta.ts"), "export function beta(): number {\n return 2;\n}\n", "utf8");
2290
+ const docsDir = path.join(projectBase, "docs", "custom");
2291
+ fs.mkdirSync(docsDir, { recursive: true });
2292
+ fs.writeFileSync(path.join(docsDir, "REFERENCES.md"), "stale\n", "utf8");
2293
+ writeProjectConfigOverrides(projectBase, { "docs-dir": "docs/custom" });
2294
+ assert.equal(spawnSync("git", ["add", "src/alpha.ts", "src/nested/beta.ts", "docs/custom/REFERENCES.md"], {
2295
+ cwd: projectBase,
2296
+ encoding: "utf8",
2297
+ }).status, 0);
2298
+ const fixtureCommit = spawnSync("git", ["commit", "-m", "fixture sources"], {
2299
+ cwd: projectBase,
2300
+ encoding: "utf8",
2301
+ });
2302
+ assert.equal(fixtureCommit.status, 0, fixtureCommit.stderr);
2303
+
2304
+ const worktreeParentPath = path.dirname(projectBase);
2305
+ const initialEntries = new Set(fs.readdirSync(worktreeParentPath));
2306
+ const pi = createFakePi();
2307
+ piUsereqExtension(pi);
2308
+ const ctx = createFakeCtx(projectBase);
2309
+ const recordedStatuses: string[] = [];
2310
+ let switchSessionCalls = 0;
2311
+ const originalSetStatus = ctx.ui.setStatus.bind(ctx.ui);
2312
+ const originalSwitchSession = ctx.switchSession.bind(ctx);
2313
+ ctx.ui.setStatus = (key: string, value?: string) => {
2314
+ if (key === "pi-usereq" && value !== undefined) {
2315
+ recordedStatuses.push(value);
2316
+ }
2317
+ originalSetStatus(key, value);
2318
+ };
2319
+ ctx.switchSession = async (sessionPath: string) => {
2320
+ switchSessionCalls += 1;
2321
+ return originalSwitchSession(sessionPath);
2322
+ };
2323
+
2324
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2325
+ await pi.commands.get("req-references")!.handler("", ctx);
2326
+
2327
+ const summarizeResult = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
2328
+ content?: Array<{ type?: string; text?: string }>;
2329
+ };
2330
+ const referencesPath = path.join(docsDir, "REFERENCES.md");
2331
+ const writtenText = fs.readFileSync(referencesPath, "utf8");
2332
+ const lastCommitMessage = spawnSync("git", ["log", "-1", "--pretty=%s"], {
2333
+ cwd: projectBase,
2334
+ encoding: "utf8",
2335
+ }).stdout.trim();
2336
+ const committedPaths = spawnSync("git", ["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"], {
2337
+ cwd: projectBase,
2338
+ encoding: "utf8",
2339
+ }).stdout.trim().split(/\r?\n/).filter((line) => line !== "");
2340
+ const createdWorktreeEntries = fs.readdirSync(worktreeParentPath)
2341
+ .filter((entry) => !initialEntries.has(entry))
2342
+ .filter((entry) => /^PI-useReq-/.test(entry));
2343
+ const finalStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
2344
+
2345
+ assert.equal(pi.sentUserMessages.length, 0);
2346
+ assert.equal(switchSessionCalls, 0);
2347
+ assert.deepEqual(createdWorktreeEntries, []);
2348
+ assert.equal(writtenText.trimEnd(), summarizeResult.content?.[0]?.text ?? "");
2349
+ assert.doesNotMatch(writtenText, /^stale$/m);
2350
+ assert.equal(lastCommitMessage, REQ_REFERENCES_COMMIT_MESSAGE);
2351
+ assert.deepEqual(committedPaths, ["docs/custom/REFERENCES.md"]);
2352
+ assert.equal(
2353
+ spawnSync("git", ["status", "--porcelain"], { cwd: projectBase, encoding: "utf8" }).stdout.trim(),
2354
+ "",
2355
+ );
2356
+ assert.ok(ctx.__state.notifications.some((entry) => /SUCCESS: Updated .*REFERENCES\.md/.test(entry.message)));
2357
+ assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>running<\/warning>/.test(status)));
2358
+ assert.ok(recordedStatuses.every((status) => !/<accent>status:<\/accent><warning>merging<\/warning>/.test(status)));
2359
+ assert.equal(
2360
+ finalStatus,
2361
+ buildExpectedFakeStatusText({
2362
+ workflowState: "idle",
2363
+ basePath: buildExpectedFakeBasePath(projectBase),
2364
+ docsDir: "docs/custom",
2365
+ testsDir: "tests",
2366
+ srcDir: ["src"],
2367
+ contextFilledCells: 0,
2368
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2369
+ }),
2370
+ );
2371
+ } finally {
2372
+ process.chdir(previousCwd);
2373
+ fs.rmSync(projectBase, { recursive: true, force: true });
2374
+ }
2375
+ });
2376
+
2377
+ /**
2378
+ * @brief Verifies failing `req-references` runs report errors without session handoff.
2379
+ * @details Covers both git-preflight failure and reference-generation failure, proving the command creates no worktree, sends no LLM message, transitions workflow state to `error`, and preserves the previous git commit when execution aborts. Runtime is dominated by temporary repository setup plus failing command invocations. Side effects are limited to temporary filesystem and git mutation inside fixture repositories.
2380
+ * @return {Promise<void>} Promise resolved after failure-path assertions complete.
2381
+ * @throws {AssertionError} Throws when `req-references` hides failures, mutates history, or attempts prompt-session orchestration.
2382
+ * @satisfies TST-104
2383
+ */
2384
+ test("req-references surfaces git-validation and reference-generation failures without session handoff", async () => {
2385
+ const gitFailure = initFixtureRepo({ fixtures: [] });
2386
+ const previousCwd = process.cwd();
2387
+ try {
2388
+ fs.writeFileSync(path.join(gitFailure.projectBase, "DIRTY.txt"), "dirty\n", "utf8");
2389
+ const worktreeParentPath = path.dirname(gitFailure.projectBase);
2390
+ const initialEntries = new Set(fs.readdirSync(worktreeParentPath));
2391
+ const pi = createFakePi();
2392
+ piUsereqExtension(pi);
2393
+ const ctx = createFakeCtx(gitFailure.projectBase);
2394
+
2395
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2396
+ await assert.rejects(
2397
+ pi.commands.get("req-references")!.handler("", ctx),
2398
+ /ERROR: Git status unclear!/,
2399
+ );
2400
+
2401
+ const createdWorktreeEntries = fs.readdirSync(worktreeParentPath)
2402
+ .filter((entry) => !initialEntries.has(entry))
2403
+ .filter((entry) => /^PI-useReq-/.test(entry));
2404
+ assert.equal(pi.sentUserMessages.length, 0);
2405
+ assert.deepEqual(createdWorktreeEntries, []);
2406
+ assert.ok(ctx.__state.notifications.some((entry) => /ERROR: Git status unclear!/.test(entry.message)));
2407
+ assert.equal(
2408
+ ctx.__state.statuses.get("pi-usereq"),
2409
+ buildExpectedFakeStatusText({
2410
+ workflowState: "error",
2411
+ basePath: buildExpectedFakeBasePath(gitFailure.projectBase),
2412
+ docsDir: DEFAULT_DOCS_DIR,
2413
+ testsDir: "tests",
2414
+ srcDir: ["src"],
2415
+ contextFilledCells: 0,
2416
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2417
+ }),
2418
+ );
2419
+ assert.equal(
2420
+ spawnSync("git", ["log", "-1", "--pretty=%s"], {
2421
+ cwd: gitFailure.projectBase,
2422
+ encoding: "utf8",
2423
+ }).stdout.trim(),
2424
+ "init",
2425
+ );
2426
+ } finally {
2427
+ process.chdir(previousCwd);
2428
+ fs.rmSync(gitFailure.projectBase, { recursive: true, force: true });
2429
+ }
2430
+
2431
+ const sourceFailure = initFixtureRepo({ fixtures: [] });
2432
+ try {
2433
+ const worktreeParentPath = path.dirname(sourceFailure.projectBase);
2434
+ const initialEntries = new Set(fs.readdirSync(worktreeParentPath));
2435
+ const pi = createFakePi();
2436
+ piUsereqExtension(pi);
2437
+ const ctx = createFakeCtx(sourceFailure.projectBase);
2438
+ const recordedStatuses: string[] = [];
2439
+ const originalSetStatus = ctx.ui.setStatus.bind(ctx.ui);
2440
+ ctx.ui.setStatus = (key: string, value?: string) => {
2441
+ if (key === "pi-usereq" && value !== undefined) {
2442
+ recordedStatuses.push(value);
2443
+ }
2444
+ originalSetStatus(key, value);
2445
+ };
2446
+
2447
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2448
+ await assert.rejects(
2449
+ pi.commands.get("req-references")!.handler("", ctx),
2450
+ /no source files found in configured directories/,
2451
+ );
2452
+
2453
+ const createdWorktreeEntries = fs.readdirSync(worktreeParentPath)
2454
+ .filter((entry) => !initialEntries.has(entry))
2455
+ .filter((entry) => /^PI-useReq-/.test(entry));
2456
+ assert.equal(pi.sentUserMessages.length, 0);
2457
+ assert.deepEqual(createdWorktreeEntries, []);
2458
+ assert.ok(ctx.__state.notifications.some((entry) => /no source files found in configured directories/.test(entry.message)));
2459
+ assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>running<\/warning>/.test(status)));
2460
+ assert.equal(
2461
+ ctx.__state.statuses.get("pi-usereq"),
2462
+ buildExpectedFakeStatusText({
2463
+ workflowState: "error",
2464
+ basePath: buildExpectedFakeBasePath(sourceFailure.projectBase),
2465
+ docsDir: DEFAULT_DOCS_DIR,
2466
+ testsDir: "tests",
2467
+ srcDir: ["src"],
2468
+ contextFilledCells: 0,
2469
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2470
+ }),
2471
+ );
2472
+ assert.equal(
2473
+ spawnSync("git", ["log", "-1", "--pretty=%s"], {
2474
+ cwd: sourceFailure.projectBase,
2475
+ encoding: "utf8",
2476
+ }).stdout.trim(),
2477
+ "init",
2478
+ );
2479
+ } finally {
2480
+ process.chdir(previousCwd);
2481
+ fs.rmSync(sourceFailure.projectBase, { recursive: true, force: true });
2482
+ }
2483
+ });
2484
+
2485
+ /**
2486
+ * @brief Verifies successful `req-reset` recovers from a non-`idle` worktree-backed prompt state.
2487
+ * @details Starts one worktree-backed `req-change` run until the fake command context remains attached to the execution worktree with workflow state `running`, appends one unique execution-session record, invokes `req-reset` from that non-`idle` state, and verifies transcript preservation, base-path restoration, generated worktree plus branch deletion, unchanged LLM prompt count, success notification, and final workflow state `idle`. Runtime is dominated by temporary git worktree setup plus session-file inspection. Side effects are limited to temporary repository mutation and fake UI notifications.
2488
+ * @return {Promise<void>} Promise resolved after recovery-path assertions complete.
2489
+ * @throws {AssertionError} Throws when `req-reset` rejects the non-`idle` state, loses the execution transcript, leaves generated cleanup artifacts behind, or dispatches an additional LLM prompt.
2490
+ * @satisfies TST-106
2491
+ */
2492
+ test("req-reset accepts non-idle workflow state, preserves transcript, and removes generated worktrees", async () => {
2493
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
2494
+ const previousCwd = process.cwd();
2495
+ try {
2496
+ const pi = createFakePi();
2497
+ piUsereqExtension(pi);
2498
+ const ctx = createFakeCtx(projectBase);
2499
+
2500
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2501
+ const originalSessionFile = ctx.sessionManager.getSessionFile() ?? "";
2502
+ assert.notEqual(originalSessionFile, "");
2503
+
2504
+ await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
2505
+
2506
+ const sentMessageCountBeforeReset = pi.sentUserMessages.length;
2507
+ const promptText = String(pi.sentUserMessages[0]?.content ?? "");
2508
+ const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
2509
+ assert.ok(worktreeMatch, promptText);
2510
+ const worktreeName = worktreeMatch?.[1] ?? "";
2511
+ const executionBasePath = worktreeMatch?.[2] ?? "";
2512
+ const executionSessionFile = ctx.sessionManager.getSessionFile() ?? "";
2513
+ const appendedEntryId = "req-reset-transcript-entry";
2514
+
2515
+ assert.notEqual(executionSessionFile, "");
2516
+ assert.notEqual(executionSessionFile, originalSessionFile);
2517
+ assert.equal(ctx.cwd, executionBasePath);
2518
+ assert.equal(process.cwd(), executionBasePath);
2519
+ assert.equal(fs.existsSync(executionBasePath), true);
2520
+ assert.match(
2521
+ ctx.__state.statuses.get("pi-usereq") ?? "",
2522
+ /<accent>status:<\/accent><warning>running<\/warning>/,
2523
+ );
2524
+
2525
+ fs.appendFileSync(
2526
+ executionSessionFile,
2527
+ `${JSON.stringify({
2528
+ type: "custom",
2529
+ id: appendedEntryId,
2530
+ parentId: null,
2531
+ timestamp: new Date(0).toISOString(),
2532
+ customType: "req-reset-test",
2533
+ data: { recovered: true },
2534
+ })}\n`,
2535
+ "utf8",
2536
+ );
2537
+
2538
+ await pi.commands.get("req-reset")!.handler("", ctx);
2539
+
2540
+ assert.equal(pi.sentUserMessages.length, sentMessageCountBeforeReset);
2541
+ assert.equal(ctx.cwd, projectBase);
2542
+ assert.equal(process.cwd(), projectBase);
2543
+ assert.equal(ctx.sessionManager.getSessionFile(), originalSessionFile);
2544
+ assert.equal(fs.existsSync(executionBasePath), false);
2545
+ assert.equal(
2546
+ spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
2547
+ cwd: projectBase,
2548
+ encoding: "utf8",
2549
+ }).status,
2550
+ 1,
2551
+ );
2552
+ assert.match(fs.readFileSync(originalSessionFile, "utf8"), new RegExp(appendedEntryId));
2553
+ assert.ok(
2554
+ ctx.__state.notifications.some((entry) => /SUCCESS: req-reset/.test(entry.message)),
2555
+ );
2556
+ assert.equal(
2557
+ ctx.__state.statuses.get("pi-usereq"),
2558
+ buildExpectedFakeStatusText({
2559
+ workflowState: "idle",
2560
+ basePath: buildExpectedFakeBasePath(projectBase),
2561
+ docsDir: DEFAULT_DOCS_DIR,
2562
+ testsDir: "tests",
2563
+ srcDir: ["src"],
2564
+ contextFilledCells: 0,
2565
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2566
+ }),
2567
+ );
2568
+ } finally {
2569
+ process.chdir(previousCwd);
2570
+ fs.rmSync(projectBase, { recursive: true, force: true });
2571
+ }
2572
+ });
2573
+
2574
+ /**
2575
+ * @brief Verifies failing `req-reset` surfaces restoration errors without starting new worktree or LLM execution.
2576
+ * @details Starts one worktree-backed `req-change` run until the fake command context remains attached to the execution worktree, forces the subsequent base-path restoration switch to return `cancelled`, invokes `req-reset` from that non-`idle` state, and verifies the command reports the restoration failure, leaves the generated worktree plus branch in place for manual recovery, sends no additional LLM prompt, and transitions workflow state to `error`. Runtime is dominated by temporary git worktree setup. Side effects are limited to temporary repository mutation and fake UI notifications.
2577
+ * @return {Promise<void>} Promise resolved after failure-path assertions complete.
2578
+ * @throws {AssertionError} Throws when `req-reset` hides restoration failures, creates new cleanup artifacts, or clears the generated worktree plus branch despite the failed restore.
2579
+ * @satisfies TST-107
2580
+ */
2581
+ test("req-reset surfaces restoration failures and transitions workflow state to error", async () => {
2582
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
2583
+ const previousCwd = process.cwd();
2584
+ try {
2585
+ const pi = createFakePi();
2586
+ piUsereqExtension(pi);
2587
+ const ctx = createFakeCtx(projectBase);
2588
+
2589
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2590
+ const originalSessionFile = ctx.sessionManager.getSessionFile() ?? "";
2591
+ assert.notEqual(originalSessionFile, "");
2592
+
2593
+ await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
2594
+
2595
+ const sentMessageCountBeforeReset = pi.sentUserMessages.length;
2596
+ const promptText = String(pi.sentUserMessages[0]?.content ?? "");
2597
+ const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
2598
+ assert.ok(worktreeMatch, promptText);
2599
+ const worktreeName = worktreeMatch?.[1] ?? "";
2600
+ const executionBasePath = worktreeMatch?.[2] ?? "";
2601
+
2602
+ assert.match(
2603
+ ctx.__state.statuses.get("pi-usereq") ?? "",
2604
+ /<accent>status:<\/accent><warning>running<\/warning>/,
2605
+ );
2606
+
2607
+ ctx.switchSession = async (_sessionPath: string) => ({ cancelled: true });
2608
+
2609
+ await assert.rejects(
2610
+ pi.commands.get("req-reset")!.handler("", ctx),
2611
+ /Session switch cancelled/,
2612
+ );
2613
+
2614
+ assert.equal(pi.sentUserMessages.length, sentMessageCountBeforeReset);
2615
+ assert.equal(ctx.cwd, executionBasePath);
2616
+ assert.equal(process.cwd(), executionBasePath);
2617
+ assert.equal(fs.existsSync(executionBasePath), true);
2618
+ assert.equal(
2619
+ spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
2620
+ cwd: projectBase,
2621
+ encoding: "utf8",
2622
+ }).status,
2623
+ 0,
2624
+ );
2625
+ assert.ok(
2626
+ ctx.__state.notifications.some((entry) => /Session switch cancelled/.test(entry.message)),
2627
+ );
2628
+ assert.equal(
2629
+ ctx.__state.statuses.get("pi-usereq"),
2630
+ buildExpectedFakeStatusText({
2631
+ workflowState: "error",
2632
+ basePath: buildExpectedFakeBasePath(executionBasePath),
2633
+ docsDir: DEFAULT_DOCS_DIR,
2634
+ testsDir: "tests",
2635
+ srcDir: ["src"],
2636
+ contextFilledCells: 0,
2637
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2638
+ }),
2639
+ );
2640
+ } finally {
2641
+ process.chdir(previousCwd);
2642
+ fs.rmSync(projectBase, { recursive: true, force: true });
2643
+ }
2644
+ });
2645
+
1995
2646
  test("prompt commands skip worktree creation when auto git commit is disabled", async () => {
1996
2647
  const { projectBase } = initFixtureRepo({ fixtures: [] });
1997
2648
  try {
@@ -2045,9 +2696,76 @@ test("prompt commands capture checking before handoff and running at prompt inje
2045
2696
  assert.ok(worktreeMatch, promptText);
2046
2697
  const executionBasePath = worktreeMatch?.[2] ?? "";
2047
2698
  assert.equal(
2048
- checkingStatus,
2699
+ checkingStatus,
2700
+ buildExpectedFakeStatusText({
2701
+ workflowState: "checking",
2702
+ basePath: buildExpectedFakeBasePath(projectBase),
2703
+ docsDir: DEFAULT_DOCS_DIR,
2704
+ testsDir: "tests",
2705
+ srcDir: ["src"],
2706
+ contextFilledCells: 0,
2707
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2708
+ }),
2709
+ );
2710
+ assert.equal(
2711
+ handoffStatus,
2712
+ buildExpectedFakeStatusText({
2713
+ workflowState: "checking",
2714
+ basePath: buildExpectedFakeBasePath(executionBasePath),
2715
+ docsDir: DEFAULT_DOCS_DIR,
2716
+ testsDir: "tests",
2717
+ srcDir: ["src"],
2718
+ contextFilledCells: 0,
2719
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2720
+ }),
2721
+ );
2722
+ assert.equal(
2723
+ ctx.__state.statuses.get("pi-usereq"),
2724
+ buildExpectedFakeStatusText({
2725
+ workflowState: "running",
2726
+ basePath: buildExpectedFakeBasePath(executionBasePath),
2727
+ docsDir: DEFAULT_DOCS_DIR,
2728
+ testsDir: "tests",
2729
+ srcDir: ["src"],
2730
+ contextFilledCells: 0,
2731
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2732
+ }),
2733
+ );
2734
+
2735
+ await pi.emit("before_agent_start", {}, ctx);
2736
+ await pi.emit("agent_start", {}, ctx);
2737
+ await pi.emit("agent_end", {
2738
+ messages: [{ role: "assistant", stopReason: "stop", content: [] }],
2739
+ }, ctx);
2740
+ } finally {
2741
+ setPromptCommandPostCreateHookForTests(undefined);
2742
+ process.chdir(previousCwd);
2743
+ fs.rmSync(projectBase, { recursive: true, force: true });
2744
+ }
2745
+ });
2746
+
2747
+ /**
2748
+ * @brief Verifies every req command except `req-reset` rejects non-`idle` workflow state and transitions to `error`.
2749
+ * @details Starts one no-worktree `req-change` run until workflow state becomes `running`, invokes `req-analyze` and `req-references` while orchestration remains active, and verifies both handlers reject with deterministic diagnostics, emit error notifications, send no additional LLM prompt, and move the shared workflow state to `error` before `agent_end` restores `idle`. Runtime is dominated by temporary project setup plus fake lifecycle dispatch. Side effects are limited to temporary filesystem mutation and fake UI notifications.
2750
+ * @return {Promise<void>} Promise resolved after rejection-path assertions complete.
2751
+ * @throws {AssertionError} Throws when a busy req command starts new orchestration, skips the error transition, or prevents the active no-worktree prompt from returning to `idle`.
2752
+ * @satisfies TST-068
2753
+ */
2754
+ test("req commands except req-reset reject non-idle workflow state and transition to error", async () => {
2755
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
2756
+ const previousCwd = process.cwd();
2757
+ try {
2758
+ writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
2759
+ const pi = createFakePi();
2760
+ piUsereqExtension(pi);
2761
+ const ctx = createFakeCtx(projectBase);
2762
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2763
+
2764
+ await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
2765
+ assert.equal(
2766
+ ctx.__state.statuses.get("pi-usereq"),
2049
2767
  buildExpectedFakeStatusText({
2050
- workflowState: "checking",
2768
+ workflowState: "running",
2051
2769
  basePath: buildExpectedFakeBasePath(projectBase),
2052
2770
  docsDir: DEFAULT_DOCS_DIR,
2053
2771
  testsDir: "tests",
@@ -2056,11 +2774,21 @@ test("prompt commands capture checking before handoff and running at prompt inje
2056
2774
  et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2057
2775
  }),
2058
2776
  );
2777
+
2778
+ await assert.rejects(
2779
+ pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx),
2780
+ /expected idle/,
2781
+ );
2782
+ assert.equal(pi.sentUserMessages.length, 1);
2059
2783
  assert.equal(
2060
- handoffStatus,
2784
+ ctx.__state.notifications.filter((entry) => /expected idle/.test(entry.message)).length,
2785
+ 1,
2786
+ );
2787
+ assert.equal(
2788
+ ctx.__state.statuses.get("pi-usereq"),
2061
2789
  buildExpectedFakeStatusText({
2062
- workflowState: "checking",
2063
- basePath: buildExpectedFakeBasePath(executionBasePath),
2790
+ workflowState: "error",
2791
+ basePath: buildExpectedFakeBasePath(projectBase),
2064
2792
  docsDir: DEFAULT_DOCS_DIR,
2065
2793
  testsDir: "tests",
2066
2794
  srcDir: ["src"],
@@ -2068,11 +2796,21 @@ test("prompt commands capture checking before handoff and running at prompt inje
2068
2796
  et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2069
2797
  }),
2070
2798
  );
2799
+
2800
+ await assert.rejects(
2801
+ pi.commands.get("req-references")!.handler("", ctx),
2802
+ /expected idle/,
2803
+ );
2804
+ assert.equal(pi.sentUserMessages.length, 1);
2805
+ assert.equal(
2806
+ ctx.__state.notifications.filter((entry) => /expected idle/.test(entry.message)).length,
2807
+ 2,
2808
+ );
2071
2809
  assert.equal(
2072
2810
  ctx.__state.statuses.get("pi-usereq"),
2073
2811
  buildExpectedFakeStatusText({
2074
- workflowState: "running",
2075
- basePath: buildExpectedFakeBasePath(executionBasePath),
2812
+ workflowState: "error",
2813
+ basePath: buildExpectedFakeBasePath(projectBase),
2076
2814
  docsDir: DEFAULT_DOCS_DIR,
2077
2815
  testsDir: "tests",
2078
2816
  srcDir: ["src"],
@@ -2081,33 +2819,64 @@ test("prompt commands capture checking before handoff and running at prompt inje
2081
2819
  }),
2082
2820
  );
2083
2821
 
2084
- await pi.emit("before_agent_start", {}, ctx);
2085
2822
  await pi.emit("agent_start", {}, ctx);
2086
2823
  await pi.emit("agent_end", {
2087
2824
  messages: [{ role: "assistant", stopReason: "stop", content: [] }],
2088
2825
  }, ctx);
2826
+ assert.equal(
2827
+ ctx.__state.statuses.get("pi-usereq"),
2828
+ buildExpectedFakeStatusText({
2829
+ workflowState: "idle",
2830
+ basePath: buildExpectedFakeBasePath(projectBase),
2831
+ docsDir: DEFAULT_DOCS_DIR,
2832
+ testsDir: "tests",
2833
+ srcDir: ["src"],
2834
+ contextFilledCells: 0,
2835
+ et: "⏱︎ --:-- ⚑ 0:00 ⌛︎0:00",
2836
+ }),
2837
+ );
2089
2838
  } finally {
2090
- setPromptCommandPostCreateHookForTests(undefined);
2091
2839
  process.chdir(previousCwd);
2092
2840
  fs.rmSync(projectBase, { recursive: true, force: true });
2093
2841
  }
2094
2842
  });
2095
2843
 
2096
- test("prompt commands reject non-idle workflow state before starting a new orchestration", async () => {
2844
+ /**
2845
+ * @brief Verifies a worktree-backed prompt still finalizes after a busy req-command rejection moves workflow state to `error`.
2846
+ * @details Starts one worktree-backed `req-change` run until the fake command context remains attached to the execution worktree with workflow state `running`, invokes `req-analyze` while the active run is still in progress so the busy-command guard forces workflow state to `error`, then emits the matched lifecycle completion and verifies `base-path` restoration, worktree plus branch deletion, unchanged prompt count, and final workflow state `idle`. Runtime is dominated by temporary git worktree setup plus fake lifecycle dispatch. Side effects are limited to temporary repository mutation and fake UI notifications.
2847
+ * @return {Promise<void>} Promise resolved after finalization-path assertions complete.
2848
+ * @throws {AssertionError} Throws when busy-command rejection orphaned the successful worktree-backed run or prevented cleanup.
2849
+ * @satisfies TST-068, TST-082
2850
+ */
2851
+ test("worktree-backed prompt finalization still succeeds after a busy-command rejection", async () => {
2097
2852
  const { projectBase } = initFixtureRepo({ fixtures: [] });
2853
+ const previousCwd = process.cwd();
2098
2854
  try {
2099
- writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
2100
2855
  const pi = createFakePi();
2101
2856
  piUsereqExtension(pi);
2102
2857
  const ctx = createFakeCtx(projectBase);
2858
+
2103
2859
  await pi.emit("session_start", { reason: "startup" }, ctx);
2860
+ const originalSessionFile = ctx.sessionManager.getSessionFile() ?? "";
2861
+ assert.notEqual(originalSessionFile, "");
2104
2862
 
2105
2863
  await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
2864
+
2865
+ const sentMessageCount = pi.sentUserMessages.length;
2866
+ const promptText = String(pi.sentUserMessages[0]?.content ?? "");
2867
+ const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
2868
+ assert.ok(worktreeMatch, promptText);
2869
+ const worktreeName = worktreeMatch?.[1] ?? "";
2870
+ const executionBasePath = worktreeMatch?.[2] ?? "";
2871
+
2872
+ assert.equal(ctx.cwd, executionBasePath);
2873
+ assert.equal(process.cwd(), executionBasePath);
2874
+ assert.equal(fs.existsSync(executionBasePath), true);
2106
2875
  assert.equal(
2107
2876
  ctx.__state.statuses.get("pi-usereq"),
2108
2877
  buildExpectedFakeStatusText({
2109
2878
  workflowState: "running",
2110
- basePath: buildExpectedFakeBasePath(projectBase),
2879
+ basePath: buildExpectedFakeBasePath(executionBasePath),
2111
2880
  docsDir: DEFAULT_DOCS_DIR,
2112
2881
  testsDir: "tests",
2113
2882
  srcDir: ["src"],
@@ -2120,13 +2889,16 @@ test("prompt commands reject non-idle workflow state before starting a new orche
2120
2889
  pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx),
2121
2890
  /expected idle/,
2122
2891
  );
2123
- assert.equal(pi.sentUserMessages.length, 1);
2124
- assert.ok(ctx.__state.notifications.some((entry) => /expected idle/.test(entry.message)));
2892
+ assert.equal(pi.sentUserMessages.length, sentMessageCount);
2893
+ assert.equal(
2894
+ ctx.__state.notifications.filter((entry) => /expected idle/.test(entry.message)).length,
2895
+ 1,
2896
+ );
2125
2897
  assert.equal(
2126
2898
  ctx.__state.statuses.get("pi-usereq"),
2127
2899
  buildExpectedFakeStatusText({
2128
- workflowState: "running",
2129
- basePath: buildExpectedFakeBasePath(projectBase),
2900
+ workflowState: "error",
2901
+ basePath: buildExpectedFakeBasePath(executionBasePath),
2130
2902
  docsDir: DEFAULT_DOCS_DIR,
2131
2903
  testsDir: "tests",
2132
2904
  srcDir: ["src"],
@@ -2135,10 +2907,30 @@ test("prompt commands reject non-idle workflow state before starting a new orche
2135
2907
  }),
2136
2908
  );
2137
2909
 
2910
+ await pi.emit("before_agent_start", {}, ctx);
2138
2911
  await pi.emit("agent_start", {}, ctx);
2139
2912
  await pi.emit("agent_end", {
2140
2913
  messages: [{ role: "assistant", stopReason: "stop", content: [] }],
2141
2914
  }, ctx);
2915
+
2916
+ assert.equal(ctx.cwd, projectBase);
2917
+ assert.equal(process.cwd(), projectBase);
2918
+ assert.equal(ctx.sessionManager.getSessionFile(), originalSessionFile);
2919
+ assert.equal(fs.existsSync(executionBasePath), false);
2920
+ assert.notEqual(
2921
+ spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
2922
+ cwd: projectBase,
2923
+ encoding: "utf8",
2924
+ }).status,
2925
+ 0,
2926
+ );
2927
+ assert.equal(
2928
+ spawnSync("git", ["status", "--porcelain"], {
2929
+ cwd: projectBase,
2930
+ encoding: "utf8",
2931
+ }).stdout.trim(),
2932
+ "",
2933
+ );
2142
2934
  assert.equal(
2143
2935
  ctx.__state.statuses.get("pi-usereq"),
2144
2936
  buildExpectedFakeStatusText({
@@ -2152,6 +2944,7 @@ test("prompt commands reject non-idle workflow state before starting a new orche
2152
2944
  }),
2153
2945
  );
2154
2946
  } finally {
2947
+ process.chdir(previousCwd);
2155
2948
  fs.rmSync(projectBase, { recursive: true, force: true });
2156
2949
  }
2157
2950
  });
@@ -2238,6 +3031,7 @@ test("worktree-enabled prompt commands merge successful runs, restore base-path,
2238
3031
  };
2239
3032
  await pi.emit("session_start", { reason: "startup" }, ctx);
2240
3033
 
3034
+ const originalBranchName = buildExpectedFakeBranchValue(projectBase);
2241
3035
  await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
2242
3036
  const promptText = String(pi.sentUserMessages[0]?.content ?? "");
2243
3037
  const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
@@ -2256,6 +3050,7 @@ test("worktree-enabled prompt commands merge successful runs, restore base-path,
2256
3050
  assert.equal(process.cwd(), executionBasePath);
2257
3051
  assert.equal(ctx.cwd, executionBasePath);
2258
3052
  assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>running<\/warning>/.test(status)));
3053
+ assert.ok(recordedStatuses.some((status) => status.includes(`${formatFakeThemeForeground("accent", "branch:")}${formatFakeThemeForeground("warning", worktreeName)}`)));
2259
3054
 
2260
3055
  await pi.emit("before_agent_start", {}, ctx);
2261
3056
  assert.equal(process.cwd(), executionBasePath);
@@ -2290,7 +3085,114 @@ test("worktree-enabled prompt commands merge successful runs, restore base-path,
2290
3085
  );
2291
3086
  assert.equal(ctx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
2292
3087
  assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>merging<\/warning>/.test(status)));
2293
- assert.match(ctx.__state.statuses.get("pi-usereq") ?? "", /<accent>status:<\/accent><warning>idle<\/warning>/);
3088
+ assert.ok(recordedStatuses.some((status) => status.includes(`${formatFakeThemeForeground("accent", "branch:")}${formatFakeThemeForeground("warning", originalBranchName)}`)));
3089
+ const finalStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
3090
+ assert.match(finalStatus, /<accent>status:<\/accent><warning>idle<\/warning>/);
3091
+ assert.ok(finalStatus.includes(`${formatFakeThemeForeground("accent", "branch:")}${formatFakeThemeForeground("warning", originalBranchName)}`));
3092
+ } finally {
3093
+ process.chdir(previousCwd);
3094
+ fs.rmSync(projectBase, { recursive: true, force: true });
3095
+ }
3096
+ });
3097
+
3098
+ /**
3099
+ * @brief Verifies successful closure stashes tracked `base-path` changes before merge and restores them afterward.
3100
+ * @details Creates tracked `base-path` files, dirties one in the index and one in the worktree after worktree handoff, commits a worktree change, and proves prompt finalization runs `git stash`/merge/`git stash pop` semantics by merging successfully, restoring the local `base-path` changes, deleting the worktree, and surfacing a warning-only notification about the dirty restored base path.
3101
+ * @return {Promise<void>} Promise resolved after stash-assisted merge assertions complete.
3102
+ * @throws {AssertionError} Throws when finalization drops local `base-path` changes, skips the merge, or reports an error instead of a warning.
3103
+ * @satisfies REQ-208, REQ-291, REQ-292
3104
+ */
3105
+ test("worktree-backed successful closure stashes tracked base-path changes, merges, restores changes, and warns about dirty base-path", async () => {
3106
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
3107
+ const previousCwd = process.cwd();
3108
+ try {
3109
+ const stagedBasePath = path.join(projectBase, "staged-base.txt");
3110
+ const worktreeBasePath = path.join(projectBase, "worktree-base.txt");
3111
+ fs.writeFileSync(stagedBasePath, "staged base clean\n", "utf8");
3112
+ fs.writeFileSync(worktreeBasePath, "worktree base clean\n", "utf8");
3113
+ assert.equal(
3114
+ spawnSync("git", ["add", "staged-base.txt", "worktree-base.txt"], {
3115
+ cwd: projectBase,
3116
+ encoding: "utf8",
3117
+ }).status,
3118
+ 0,
3119
+ );
3120
+ const baseCommit = spawnSync("git", ["commit", "-m", "tracked base files"], {
3121
+ cwd: projectBase,
3122
+ encoding: "utf8",
3123
+ });
3124
+ assert.equal(baseCommit.status, 0, baseCommit.stderr);
3125
+
3126
+ const pi = createFakePi();
3127
+ piUsereqExtension(pi);
3128
+ const ctx = createFakeCtx(projectBase);
3129
+ await pi.emit("session_start", { reason: "startup" }, ctx);
3130
+
3131
+ await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
3132
+ const promptText = String(pi.sentUserMessages[0]?.content ?? "");
3133
+ const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
3134
+ assert.ok(worktreeMatch, promptText);
3135
+ const worktreeName = worktreeMatch?.[1] ?? "";
3136
+ const executionBasePath = worktreeMatch?.[2] ?? "";
3137
+
3138
+ await pi.emit("before_agent_start", {}, ctx);
3139
+ await pi.emit("agent_start", {}, ctx);
3140
+ fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
3141
+ fs.writeFileSync(path.join(executionBasePath, "src", "stash-assisted.ts"), "export const STASH_ASSISTED = 1;\n", "utf8");
3142
+ assert.equal(
3143
+ spawnSync("git", ["add", "src/stash-assisted.ts"], {
3144
+ cwd: executionBasePath,
3145
+ encoding: "utf8",
3146
+ }).status,
3147
+ 0,
3148
+ );
3149
+ const worktreeCommit = spawnSync("git", ["commit", "-m", "stash assisted change"], {
3150
+ cwd: executionBasePath,
3151
+ encoding: "utf8",
3152
+ });
3153
+ assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
3154
+
3155
+ fs.writeFileSync(stagedBasePath, "staged base dirty\n", "utf8");
3156
+ assert.equal(
3157
+ spawnSync("git", ["add", "staged-base.txt"], {
3158
+ cwd: projectBase,
3159
+ encoding: "utf8",
3160
+ }).status,
3161
+ 0,
3162
+ );
3163
+ fs.writeFileSync(worktreeBasePath, "worktree base dirty\n", "utf8");
3164
+
3165
+ await pi.emit("agent_end", {
3166
+ messages: [{ role: "assistant", stopReason: "stop", content: [] }],
3167
+ }, ctx);
3168
+
3169
+ const statusResult = spawnSync("git", ["status", "--porcelain"], {
3170
+ cwd: projectBase,
3171
+ encoding: "utf8",
3172
+ });
3173
+ assert.equal(statusResult.status, 0, statusResult.stderr);
3174
+ assert.equal(process.cwd(), projectBase);
3175
+ assert.equal(ctx.cwd, projectBase);
3176
+ assert.ok(fs.existsSync(path.join(projectBase, "src", "stash-assisted.ts")));
3177
+ assert.equal(fs.readFileSync(stagedBasePath, "utf8"), "staged base dirty\n");
3178
+ assert.equal(fs.readFileSync(worktreeBasePath, "utf8"), "worktree base dirty\n");
3179
+ assert.match(statusResult.stdout, /staged-base\.txt/);
3180
+ assert.match(statusResult.stdout, /worktree-base\.txt/);
3181
+ assert.equal(fs.existsSync(executionBasePath), false);
3182
+ assert.notEqual(
3183
+ spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
3184
+ cwd: projectBase,
3185
+ encoding: "utf8",
3186
+ }).status,
3187
+ 0,
3188
+ );
3189
+ assert.equal(ctx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
3190
+ assert.ok(
3191
+ ctx.__state.notifications.some(
3192
+ (entry) => entry.level === "info"
3193
+ && /WARNING: Restored base-path changes after merge; base-path is not clean\./.test(entry.message),
3194
+ ),
3195
+ );
2294
3196
  } finally {
2295
3197
  process.chdir(previousCwd);
2296
3198
  fs.rmSync(projectBase, { recursive: true, force: true });
@@ -3950,6 +4852,58 @@ test("session_start renders the 0-percent context icon when context usage is emp
3950
4852
  );
3951
4853
  });
3952
4854
 
4855
+ /**
4856
+ * @brief Verifies direct status renders refresh context usage before drawing the footer.
4857
+ * @details Exercises `renderPiUsereqStatus(...)` without going through an intercepted lifecycle hook, then mutates the fake `getContextUsage()` result to simulate a post-compaction context reset. The test proves direct rerender call sites must fetch fresh context usage so the `context:` gauge updates immediately instead of reusing stale controller state.
4858
+ * @return {void} No return value.
4859
+ * @throws {AssertionError} Throws when a direct render ignores the live `getContextUsage()` snapshot or fails to clear the gauge after context reset.
4860
+ * @satisfies REQ-118, REQ-119, REQ-122, REQ-284
4861
+ */
4862
+ test("direct status rerenders refresh the context gauge after context reset", () => {
4863
+ const cwd = createTempDir("pi-usereq-context-direct-render-");
4864
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
4865
+ const controller = createPiUsereqStatusController();
4866
+ setPiUsereqStatusConfig(controller, getDefaultConfig(cwd));
4867
+ const contextUsage: { tokens: number | null; contextWindow: number; percent: number | null } = {
4868
+ tokens: 910,
4869
+ contextWindow: 1000,
4870
+ percent: 91,
4871
+ };
4872
+ const ctx = createFakeCtx(cwd, { selects: [] }, {
4873
+ getContextUsage: () => ({ ...contextUsage }),
4874
+ });
4875
+
4876
+ renderPiUsereqStatus(controller, ctx);
4877
+ assert.equal(
4878
+ ctx.__state.statuses.get("pi-usereq"),
4879
+ buildExpectedFakeStatusText({
4880
+ basePath: buildExpectedFakeBasePath(cwd),
4881
+ docsDir: DEFAULT_DOCS_DIR,
4882
+ testsDir: "tests",
4883
+ srcDir: ["src"],
4884
+ contextFilledCells: 10,
4885
+ contextPercent: 91,
4886
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
4887
+ }),
4888
+ );
4889
+
4890
+ contextUsage.tokens = null;
4891
+ contextUsage.percent = null;
4892
+ renderPiUsereqStatus(controller, ctx);
4893
+ assert.equal(
4894
+ ctx.__state.statuses.get("pi-usereq"),
4895
+ buildExpectedFakeStatusText({
4896
+ basePath: buildExpectedFakeBasePath(cwd),
4897
+ docsDir: DEFAULT_DOCS_DIR,
4898
+ testsDir: "tests",
4899
+ srcDir: ["src"],
4900
+ contextFilledCells: 0,
4901
+ contextPercent: null,
4902
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
4903
+ }),
4904
+ );
4905
+ });
4906
+
3953
4907
  /**
3954
4908
  * @brief Verifies stale replacement contexts do not break workflow-state rendering.
3955
4909
  * @details Simulates the guarded pi runtime error thrown when an extension context is accessed after session replacement or reload. The test proves workflow-state updates must preserve internal state while suppressing stale-context render failures instead of surfacing extension errors.
@@ -4044,7 +4998,47 @@ test("context hook refreshes context usage and rounds progress cells upward", as
4044
4998
  );
4045
4999
  });
4046
5000
 
4047
- test("context hook renders the full context icon when usage exceeds ninety percent", async () => {
5001
+ /**
5002
+ * @brief Verifies context-gauge icon cutovers below the error range.
5003
+ * @details Replays deterministic context percentages at documented threshold boundaries below `90%` so the status bar proves the `▕▂▏`, `▕▄▏`, `▕▆▏`, and `▕█▏` transitions while rendering every non-error band with the same warning-token color used by the `status` value. Runtime is O(n) in case count. Side effects are limited to in-memory status updates.
5004
+ * @return {Promise<void>} Promise resolved after all threshold assertions complete.
5005
+ * @throws {AssertionError} Throws when any threshold renders the wrong icon or low-band color.
5006
+ * @satisfies REQ-122, REQ-126, TST-096
5007
+ */
5008
+ test("context hook applies warning-colored quarter-threshold gauge icons below the error range", async () => {
5009
+ const cwd = createTempDir("pi-usereq-context-threshold-status-");
5010
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
5011
+ const contextUsage = { tokens: 0, contextWindow: 1000, percent: 0 };
5012
+ const pi = createFakePi();
5013
+ piUsereqExtension(pi);
5014
+ const ctx = createFakeCtx(cwd, { selects: [] }, {
5015
+ getContextUsage: () => ({ ...contextUsage }),
5016
+ });
5017
+
5018
+ await pi.emit("session_start", { reason: "startup" }, ctx);
5019
+
5020
+ for (const percent of [24.9, 25, 49.9, 50, 74.9, 75, 89.9]) {
5021
+ contextUsage.tokens = Math.round(percent * 10);
5022
+ contextUsage.percent = percent;
5023
+ await pi.emit("context", { messages: [] }, ctx);
5024
+
5025
+ assert.equal(
5026
+ ctx.__state.statuses.get("pi-usereq"),
5027
+ buildExpectedFakeStatusText({
5028
+ basePath: buildExpectedFakeBasePath(cwd),
5029
+ docsDir: DEFAULT_DOCS_DIR,
5030
+ testsDir: "tests",
5031
+ srcDir: ["src"],
5032
+ contextFilledCells: Math.ceil(percent / 10),
5033
+ contextPercent: percent,
5034
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
5035
+ }),
5036
+ `unexpected context icon at ${percent}%`,
5037
+ );
5038
+ }
5039
+ });
5040
+
5041
+ test("context hook renders the error full context icon when usage exceeds ninety percent", async () => {
4048
5042
  const cwd = createTempDir("pi-usereq-context-full-status-");
4049
5043
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
4050
5044
  const contextUsage = { tokens: 910, contextWindow: 1000, percent: 91 };
@@ -4071,14 +5065,14 @@ test("context hook renders the full context icon when usage exceeds ninety perce
4071
5065
  );
4072
5066
  });
4073
5067
 
4074
- test("status overflow context icon uses only CLI-supported theme tokens", async () => {
5068
+ test("status overflow-threshold context icon uses only CLI-supported theme tokens", async () => {
4075
5069
  const cwd = createTempDir("pi-usereq-context-theme-contract-");
4076
5070
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
4077
5071
  const usedColors = new Set<string>();
4078
5072
  const pi = createFakePi();
4079
5073
  piUsereqExtension(pi);
4080
5074
  const ctx = createFakeCtx(cwd, { selects: [] }, {
4081
- getContextUsage: () => ({ tokens: 1250, contextWindow: 1000, percent: 125 }),
5075
+ getContextUsage: () => ({ tokens: 1000, contextWindow: 1000, percent: 100 }),
4082
5076
  theme: {
4083
5077
  fg(color: string, text: string) {
4084
5078
  assert.ok(["accent", "warning", "dim", "error"].includes(color));
@@ -4107,7 +5101,7 @@ test("status overflow context icon uses only CLI-supported theme tokens", async
4107
5101
  testsDir: "tests",
4108
5102
  srcDir: ["src"],
4109
5103
  contextFilledCells: 10,
4110
- contextPercent: 125,
5104
+ contextPercent: 100,
4111
5105
  et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
4112
5106
  }),
4113
5107
  );
@@ -4390,11 +5384,12 @@ test("configuration menu orders tool toggles and can enable embedded builtin too
4390
5384
  "references",
4391
5385
  "search",
4392
5386
  "static-check",
5387
+ "summarize",
4393
5388
  "tokens",
4394
5389
  "files-compress",
4395
- "files-references",
4396
5390
  "files-search",
4397
5391
  "files-static-check",
5392
+ "files-summarize",
4398
5393
  "files-tokens",
4399
5394
  "bash",
4400
5395
  "edit",
@@ -4429,7 +5424,7 @@ test("configuration menu can disable configurable active tools", async () => {
4429
5424
  assert.deepEqual(pi.getActiveTools().filter((toolName: string) => PI_USEREQ_STARTUP_TOOL_NAMES.includes(toolName as never)), []);
4430
5425
  });
4431
5426
 
4432
- test("configuration menu can persist notify and sound settings", async () => {
5427
+ test("configuration menu can persist notify and boot-sound settings without changing runtime sound", async () => {
4433
5428
  const cwd = createTempDir("pi-usereq-menu-notify-");
4434
5429
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
4435
5430
  const pi = createFakePi();
@@ -4443,7 +5438,7 @@ test("configuration menu can persist notify and sound settings", async () => {
4443
5438
  "Notification events",
4444
5439
  "Prompt failed",
4445
5440
  "Save and close",
4446
- "Enable sound",
5441
+ "Enable sound (boot value)",
4447
5442
  "high",
4448
5443
  "Sound events",
4449
5444
  "Prompt interrupted",
@@ -4465,6 +5460,7 @@ test("configuration menu can persist notify and sound settings", async () => {
4465
5460
  ],
4466
5461
  });
4467
5462
 
5463
+ await pi.emit("session_start", { reason: "startup" }, ctx);
4468
5464
  await command!.handler("", ctx);
4469
5465
 
4470
5466
  const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
@@ -4490,7 +5486,7 @@ test("configuration menu can persist notify and sound settings", async () => {
4490
5486
  srcDir: ["src"],
4491
5487
  contextFilledCells: 0,
4492
5488
  et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
4493
- sound: "high",
5489
+ sound: "none",
4494
5490
  }),
4495
5491
  );
4496
5492
  });
@@ -4576,7 +5572,7 @@ test("notifications menu escapes pushover text and locks pushover enablement unt
4576
5572
  const renderedNotificationsMenu = (ctx.__state.customRenderLines[2] ?? []).join("\n");
4577
5573
  assert.match(renderedNotificationsMenu, /first line\\nsecond line/);
4578
5574
  assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
4579
- assert.match(renderedNotificationsMenu, /<dim>off<\/dim>/);
5575
+ assert.match(renderedNotificationsMenu, /<dim>configure user\/token keys first<\/dim>/);
4580
5576
  });
4581
5577
 
4582
5578
  test("prompt-end pushover requests honor global enable, event toggles, credentials, priority, and templates", async () => {
@@ -5039,9 +6035,11 @@ test("sound routing honors the selected sound state and per-event toggles", asyn
5039
6035
  }
5040
6036
  });
5041
6037
 
5042
- test("sound toggle shortcut cycles persisted pi-notify sound levels", async () => {
6038
+ test("sound toggle shortcut cycles active runtime pi-notify sound levels without persisting boot config", async () => {
5043
6039
  const cwd = createTempDir("pi-usereq-shortcut-notify-");
5044
6040
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
6041
+ const persistedConfigText = `${JSON.stringify(getDefaultConfig(cwd), null, 2)}\n`;
6042
+ fs.writeFileSync(getProjectConfigPath(cwd), persistedConfigText, "utf8");
5045
6043
  const previousCwd = process.cwd();
5046
6044
  const pi = createFakePi();
5047
6045
  process.chdir(cwd);
@@ -5058,8 +6056,7 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
5058
6056
 
5059
6057
  for (const expectedSound of ["low", "mid", "high", "none"]) {
5060
6058
  await shortcut!.handler(ctx);
5061
- const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
5062
- assert.equal(config["notify-sound"], expectedSound);
6059
+ assert.equal(fs.readFileSync(getProjectConfigPath(cwd), "utf8"), persistedConfigText);
5063
6060
  assert.equal(
5064
6061
  ctx.__state.statuses.get("pi-usereq"),
5065
6062
  buildExpectedFakeStatusText({
@@ -5075,6 +6072,42 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
5075
6072
  }
5076
6073
  });
5077
6074
 
6075
+ test("session_start loads the active runtime sound from persisted boot config", async () => {
6076
+ const cwd = createTempDir("pi-usereq-session-start-sound-");
6077
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
6078
+ const persistedConfig = getDefaultConfig(cwd);
6079
+ persistedConfig["notify-sound"] = "mid";
6080
+ fs.writeFileSync(
6081
+ getProjectConfigPath(cwd),
6082
+ `${JSON.stringify(persistedConfig, null, 2)}\n`,
6083
+ "utf8",
6084
+ );
6085
+ const previousCwd = process.cwd();
6086
+ const pi = createFakePi();
6087
+ process.chdir(cwd);
6088
+ try {
6089
+ piUsereqExtension(pi);
6090
+ } finally {
6091
+ process.chdir(previousCwd);
6092
+ }
6093
+
6094
+ const ctx = createFakeCtx(cwd);
6095
+ await pi.emit("session_start", { reason: "startup" }, ctx);
6096
+
6097
+ assert.equal(
6098
+ ctx.__state.statuses.get("pi-usereq"),
6099
+ buildExpectedFakeStatusText({
6100
+ basePath: buildExpectedFakeBasePath(cwd),
6101
+ docsDir: DEFAULT_DOCS_DIR,
6102
+ testsDir: "tests",
6103
+ srcDir: ["src"],
6104
+ contextFilledCells: 0,
6105
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
6106
+ sound: "mid",
6107
+ }),
6108
+ );
6109
+ });
6110
+
5078
6111
  test("static-check exposes the supported programming languages", () => {
5079
6112
  const supported = getSupportedStaticCheckLanguageSupport();
5080
6113
  assert.equal(supported.length, 20);