pi-usereq 0.11.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 (50) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +6 -6
  3. package/package.json +1 -1
  4. package/pi-usereq/docs/REFERENCES.md +818 -691
  5. package/pi-usereq/docs/REQUIREMENTS.md +131 -77
  6. package/pi-usereq/docs/WORKFLOW.md +185 -51
  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/extension-status.ts +69 -12
  11. package/src/core/pi-notify.ts +5 -5
  12. package/src/core/pi-usereq-tools.ts +4 -2
  13. package/src/core/prompt-command-catalog.ts +4 -5
  14. package/src/core/prompt-command-runtime.ts +183 -44
  15. package/src/core/prompts.ts +0 -2
  16. package/src/core/req-references-command.ts +175 -0
  17. package/src/core/req-reset-command.ts +323 -0
  18. package/src/core/resources.ts +6 -23
  19. package/src/core/settings-menu.ts +85 -28
  20. package/src/core/tool-runner.ts +26 -6
  21. package/src/index.ts +523 -85
  22. package/tests/attended-results-scenarios.ts +5 -5
  23. package/tests/cli-command-option-parity.test.ts +25 -25
  24. package/tests/debug-extension-harness.test.ts +1 -1
  25. package/tests/extension-registration.test.ts +1029 -82
  26. package/tests/oracle-project.test.ts +4 -4
  27. package/tests/oracle-standalone.test.ts +5 -5
  28. package/src/core/reference-payload.ts +0 -752
  29. package/src/resources/prompts/references.md +0 -64
  30. /package/tests/fixtures_attended_results/project/{references.json → summarize.json} +0 -0
  31. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_c.c.json +0 -0
  32. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_cpp.cpp.json +0 -0
  33. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_csharp.cs.json +0 -0
  34. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_elixir.ex.json +0 -0
  35. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_go.go.json +0 -0
  36. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_haskell.hs.json +0 -0
  37. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_java.java.json +0 -0
  38. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_javascript.js.json +0 -0
  39. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_kotlin.kt.json +0 -0
  40. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_lua.lua.json +0 -0
  41. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_perl.pl.json +0 -0
  42. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_php.php.json +0 -0
  43. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_python.py.json +0 -0
  44. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_ruby.rb.json +0 -0
  45. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_rust.rs.json +0 -0
  46. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_scala.scala.json +0 -0
  47. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_shell.sh.json +0 -0
  48. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_swift.swift.json +0 -0
  49. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_typescript.ts.json +0 -0
  50. /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
 
@@ -429,7 +438,7 @@ function buildExpectedFakeBranchValue(basePath: string | undefined): string {
429
438
 
430
439
  /**
431
440
  * @brief Builds the expected fake context-gauge payload for assertions.
432
- * @details Resolves the documented icon thresholds for `0`, `>0-<25`, `>=25-<50`, `>=50-<75`, and `>=75-<90`, 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.
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.
433
442
  * @param[in] options {{ filledCells: number; percent?: number | null }} Expected context-gauge facts.
434
443
  * @return {string} Encoded context-gauge string.
435
444
  */
@@ -439,7 +448,7 @@ function buildExpectedFakeContextBar(options: {
439
448
  }): string {
440
449
  const percent = options.percent;
441
450
  if (percent === undefined || percent === null || percent <= 0) {
442
- return "▕_▏";
451
+ return formatFakeThemeForeground("warning", "▕_▏");
443
452
  }
444
453
  if (percent >= 100) {
445
454
  return formatFakeThemeForeground("error", "\u001b[5m▕█▏\u001b[25m");
@@ -448,15 +457,15 @@ function buildExpectedFakeContextBar(options: {
448
457
  return formatFakeThemeForeground("error", "▕█▏");
449
458
  }
450
459
  if (percent < 25) {
451
- return "▕▂▏";
460
+ return formatFakeThemeForeground("warning", "▕▂▏");
452
461
  }
453
462
  if (percent < 50) {
454
- return "▕▄▏";
463
+ return formatFakeThemeForeground("warning", "▕▄▏");
455
464
  }
456
465
  if (percent < 75) {
457
- return "▕▆▏";
466
+ return formatFakeThemeForeground("warning", "▕▆▏");
458
467
  }
459
- return "▕█▏";
468
+ return formatFakeThemeForeground("warning", "▕█▏");
460
469
  }
461
470
 
462
471
  /**
@@ -785,6 +794,7 @@ test("extension registers prompt and config commands while exposing tool capabil
785
794
  "req-recreate",
786
795
  "req-refactor",
787
796
  "req-references",
797
+ "req-reset",
788
798
  "req-renumber",
789
799
  "req-workflow",
790
800
  "req-write",
@@ -795,9 +805,10 @@ test("extension registers prompt and config commands while exposing tool capabil
795
805
 
796
806
  for (const name of [
797
807
  "files-tokens",
798
- "files-references",
808
+ "files-summarize",
799
809
  "files-compress",
800
810
  "files-search",
811
+ "summarize",
801
812
  "references",
802
813
  "compress",
803
814
  "search",
@@ -813,9 +824,10 @@ test("extension registers prompt and config commands while exposing tool capabil
813
824
  const toolNames = pi.tools.map((tool: RegisteredTool) => tool.name).sort();
814
825
  for (const name of [
815
826
  "files-tokens",
816
- "files-references",
827
+ "files-summarize",
817
828
  "files-compress",
818
829
  "files-search",
830
+ "summarize",
819
831
  "references",
820
832
  "compress",
821
833
  "search",
@@ -827,6 +839,46 @@ test("extension registers prompt and config commands while exposing tool capabil
827
839
  }
828
840
  });
829
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
+
830
882
  test("token tools register agent-oriented descriptions and schema details", () => {
831
883
  const pi = createFakePi();
832
884
  piUsereqExtension(pi);
@@ -845,24 +897,37 @@ test("token tools register agent-oriented descriptions and schema details", () =
845
897
  assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic pack-summary text/);
846
898
  });
847
899
 
848
- 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", () => {
849
921
  const pi = createFakePi();
850
922
  piUsereqExtension(pi);
851
923
 
852
- const filesReferences = pi.tools.find((tool: RegisteredTool) => tool.name === "files-references");
853
924
  const references = pi.tools.find((tool: RegisteredTool) => tool.name === "references");
854
- assert.ok(filesReferences, "missing files-references tool");
855
925
  assert.ok(references, "missing references tool");
856
926
 
857
- assert.match(filesReferences.description ?? "", /monolithic references markdown report/);
858
- assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("monolithic markdown")));
859
- assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("Formatting contract:")));
860
- assert.match(String((filesReferences.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
861
-
862
- assert.match(references.description ?? "", /configured project source directories/);
863
- assert.ok(references.promptGuidelines?.some((line) => line.includes("file-structure markdown block")));
864
- assert.ok(references.promptGuidelines?.some((line) => line.includes("Configuration contract:")));
865
- 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>`/);
866
931
  });
867
932
 
868
933
  test("search tools register agent-oriented descriptions and schema details", () => {
@@ -913,9 +978,10 @@ test("agent tools define custom renderResult with compact and expanded structure
913
978
  piUsereqExtension(pi);
914
979
  const toolNames = [
915
980
  "files-tokens",
916
- "files-references",
981
+ "files-summarize",
917
982
  "files-compress",
918
983
  "files-search",
984
+ "summarize",
919
985
  "references",
920
986
  "compress",
921
987
  "search",
@@ -963,7 +1029,7 @@ test("files-tokens returns monolithic token summary text with execution diagnost
963
1029
  docs: {
964
1030
  "REQUIREMENTS.md": "---\ntitle: Requirements\n---\n# Requirements\nAlpha\nBeta\n",
965
1031
  "WORKFLOW.md": "# Workflow\nStep one\n",
966
- "REFERENCES.md": "# References\nEntry one\n",
1032
+ "REFERENCES.md": "# Summarize\nEntry one\n",
967
1033
  },
968
1034
  });
969
1035
  try {
@@ -1011,7 +1077,7 @@ test("files-tokens defers js-tiktoken loading until execution and returns monoli
1011
1077
  }
1012
1078
  });
1013
1079
 
1014
- test("files-references returns monolithic references markdown", async () => {
1080
+ test("files-summarize returns monolithic summary markdown", async () => {
1015
1081
  const { projectBase } = initFixtureRepo({ fixtures: [] });
1016
1082
  const samplePath = path.join(projectBase, "src", "sample.ts");
1017
1083
  fs.writeFileSync(samplePath, `/**
@@ -1032,7 +1098,7 @@ export function buildSample(input: string): string {
1032
1098
  try {
1033
1099
  const pi = createFakePi();
1034
1100
  piUsereqExtension(pi);
1035
- const result = await executeRegisteredTool(pi, "files-references", projectBase, {
1101
+ const result = await executeRegisteredTool(pi, "files-summarize", projectBase, {
1036
1102
  files: ["src/sample.ts", "src/missing.ts"],
1037
1103
  }) as {
1038
1104
  content?: Array<{ type: string; text?: string }>;
@@ -1051,7 +1117,7 @@ export function buildSample(input: string): string {
1051
1117
  }
1052
1118
  });
1053
1119
 
1054
- test("references returns monolithic project references markdown", async () => {
1120
+ test("summarize returns monolithic project summary markdown", async () => {
1055
1121
  const { projectBase } = initFixtureRepo({ fixtures: [] });
1056
1122
  fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
1057
1123
  fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
@@ -1059,7 +1125,7 @@ test("references returns monolithic project references markdown", async () => {
1059
1125
  try {
1060
1126
  const pi = createFakePi();
1061
1127
  piUsereqExtension(pi);
1062
- const result = await executeRegisteredTool(pi, "references", projectBase, {}) as {
1128
+ const result = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
1063
1129
  content?: Array<{ type: string; text?: string }>;
1064
1130
  details?: { execution: { code: number; stderr_lines?: string[] } };
1065
1131
  };
@@ -1077,6 +1143,85 @@ test("references returns monolithic project references markdown", async () => {
1077
1143
  }
1078
1144
  });
1079
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
+
1080
1225
  test("files-compress returns monolithic compression markdown", async () => {
1081
1226
  const { projectBase } = initFixtureRepo({ fixtures: [] });
1082
1227
  const samplePath = path.join(projectBase, "src", "sample.ts");
@@ -1264,21 +1409,21 @@ func (s *Server) Start(ctx context.Context) error {
1264
1409
  const pi = createFakePi();
1265
1410
  piUsereqExtension(pi);
1266
1411
 
1267
- const filesReferencesResult = await executeRegisteredTool(pi, "files-references", projectBase, {
1412
+ const filesSummarizeResult = await executeRegisteredTool(pi, "files-summarize", projectBase, {
1268
1413
  files: ["src/sample.go"],
1269
1414
  }) as {
1270
1415
  content?: Array<{ type: string; text?: string }>;
1271
1416
  details?: { execution: { code: number } };
1272
1417
  };
1273
- assert.match(filesReferencesResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
1274
- 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);
1275
1420
 
1276
- const referencesResult = await executeRegisteredTool(pi, "references", projectBase, {}) as {
1421
+ const summarizeResult = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
1277
1422
  content?: Array<{ type: string; text?: string }>;
1278
1423
  details?: { execution: { code: number } };
1279
1424
  };
1280
- assert.match(referencesResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
1281
- 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);
1282
1427
 
1283
1428
  const filesCompressResult = await executeRegisteredTool(pi, "files-compress", projectBase, {
1284
1429
  files: ["src/sample.go"],
@@ -1501,12 +1646,12 @@ test("configuration menus expose show-config ordering and omit overview or notif
1501
1646
  assert.ok(renderedMenu.includes("disable"));
1502
1647
  assert.doesNotMatch(renderedMenu, /beep:/);
1503
1648
  assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
1504
- assert.match(renderedNotificationsMenu, /<dim>off<\/dim>/);
1649
+ assert.match(renderedNotificationsMenu, /<dim>configure user\/token keys first<\/dim>/);
1505
1650
  assert.deepEqual(ctx.__state.selectCalls[1]?.items ?? [], [
1506
1651
  "Enable notification",
1507
1652
  "Notification events",
1508
1653
  "Notify command",
1509
- "Enable sound",
1654
+ "Enable sound (boot value)",
1510
1655
  "Sound events",
1511
1656
  "Sound toggle hotkey bind",
1512
1657
  "Sound command (low vol.)",
@@ -1556,7 +1701,7 @@ test("descendant configuration menus end with Reset defaults only", async () =>
1556
1701
  const ctx = createFakeCtx(cwd, {
1557
1702
  selects: [
1558
1703
  "notifications",
1559
- "Enable sound",
1704
+ "Enable sound (boot value)",
1560
1705
  "Save and close",
1561
1706
  "Pushover priority",
1562
1707
  "Save and close",
@@ -1599,14 +1744,14 @@ test("descendant configuration menus end with Reset defaults only", async () =>
1599
1744
  return (ctx.__state.customRenderLines[index] ?? []).join("\n");
1600
1745
  };
1601
1746
 
1602
- assert.deepEqual(getTerminalRows("Enable sound", "high"), ["Reset defaults"]);
1747
+ assert.deepEqual(getTerminalRows("Enable sound (boot value)", "high"), ["Reset defaults"]);
1603
1748
  assert.deepEqual(getTerminalRows("Pushover priority", "High"), ["Reset defaults"]);
1604
1749
  assert.deepEqual(getTerminalRows("Enable tools", "static-check"), ["Reset defaults"]);
1605
1750
  assert.deepEqual(getTerminalRows("Debug", "Log on status"), ["Reset defaults"]);
1606
1751
  assert.deepEqual(getTerminalRows("static-check language", "Python"), ["Reset defaults"]);
1607
1752
  assert.deepEqual(getTerminalRows("Remove static code checker"), ["Reset defaults"]);
1608
1753
  assert.deepEqual(getTerminalRows("Remove source-code directory", "src"), ["Reset defaults"]);
1609
- assert.doesNotMatch(getRenderedMenu("Enable sound", "high"), /Reset defaults.*none/);
1754
+ assert.doesNotMatch(getRenderedMenu("Enable sound (boot value)", "high"), /Reset defaults.*none/);
1610
1755
  assert.doesNotMatch(getRenderedMenu("Pushover priority", "High"), /Reset defaults.*Normal/);
1611
1756
  assert.doesNotMatch(getRenderedMenu("Enable tools", "static-check"), /Reset defaults.*defaults/);
1612
1757
  assert.doesNotMatch(getRenderedMenu("Debug", "Log on status"), /Reset defaults.*running/);
@@ -1917,6 +2062,101 @@ test("notifications menu preserves focus on toggled and edited rows", async () =
1917
2062
  assert.equal(notificationEventCalls[1]?.selectedChoiceId, "notify-on-failed");
1918
2063
  });
1919
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
+
1920
2160
  test("notifications reset defaults preserves non-notification settings", async () => {
1921
2161
  const cwd = createTempDir("pi-usereq-menu-reset-notifications-");
1922
2162
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
@@ -1929,7 +2169,7 @@ test("notifications reset defaults preserves non-notification settings", async (
1929
2169
  "docs-dir",
1930
2170
  "notifications",
1931
2171
  "Enable notification",
1932
- "Enable sound",
2172
+ "Enable sound (boot value)",
1933
2173
  "high",
1934
2174
  "Reset defaults",
1935
2175
  "Approve reset",
@@ -2033,6 +2273,376 @@ test("prompt commands use the current session by default", async () => {
2033
2273
  }
2034
2274
  });
2035
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
+
2036
2646
  test("prompt commands skip worktree creation when auto git commit is disabled", async () => {
2037
2647
  const { projectBase } = initFixtureRepo({ fixtures: [] });
2038
2648
  try {
@@ -2060,35 +2670,102 @@ test("prompt commands skip worktree creation when auto git commit is disabled",
2060
2670
  }
2061
2671
  });
2062
2672
 
2063
- test("prompt commands capture checking before handoff and running at prompt injection", async () => {
2673
+ test("prompt commands capture checking before handoff and running at prompt injection", async () => {
2674
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
2675
+ const previousCwd = process.cwd();
2676
+ let checkingStatus = "";
2677
+ let handoffStatus = "";
2678
+ try {
2679
+ const pi = createFakePi();
2680
+ piUsereqExtension(pi);
2681
+ const ctx = createFakeCtx(projectBase);
2682
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2683
+ const originalSendUserMessage = pi.sendUserMessage.bind(pi);
2684
+ pi.sendUserMessage = (content: unknown, options?: unknown) => {
2685
+ handoffStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
2686
+ originalSendUserMessage(content, options);
2687
+ };
2688
+ setPromptCommandPostCreateHookForTests(() => {
2689
+ checkingStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
2690
+ });
2691
+
2692
+ await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
2693
+
2694
+ const promptText = String(pi.sentUserMessages[0]?.content ?? "");
2695
+ const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
2696
+ assert.ok(worktreeMatch, promptText);
2697
+ const executionBasePath = worktreeMatch?.[2] ?? "";
2698
+ assert.equal(
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 () => {
2064
2755
  const { projectBase } = initFixtureRepo({ fixtures: [] });
2065
2756
  const previousCwd = process.cwd();
2066
- let checkingStatus = "";
2067
- let handoffStatus = "";
2068
2757
  try {
2758
+ writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
2069
2759
  const pi = createFakePi();
2070
2760
  piUsereqExtension(pi);
2071
2761
  const ctx = createFakeCtx(projectBase);
2072
2762
  await pi.emit("session_start", { reason: "startup" }, ctx);
2073
- const originalSendUserMessage = pi.sendUserMessage.bind(pi);
2074
- pi.sendUserMessage = (content: unknown, options?: unknown) => {
2075
- handoffStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
2076
- originalSendUserMessage(content, options);
2077
- };
2078
- setPromptCommandPostCreateHookForTests(() => {
2079
- checkingStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
2080
- });
2081
2763
 
2082
2764
  await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
2083
-
2084
- const promptText = String(pi.sentUserMessages[0]?.content ?? "");
2085
- const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
2086
- assert.ok(worktreeMatch, promptText);
2087
- const executionBasePath = worktreeMatch?.[2] ?? "";
2088
2765
  assert.equal(
2089
- checkingStatus,
2766
+ ctx.__state.statuses.get("pi-usereq"),
2090
2767
  buildExpectedFakeStatusText({
2091
- workflowState: "checking",
2768
+ workflowState: "running",
2092
2769
  basePath: buildExpectedFakeBasePath(projectBase),
2093
2770
  docsDir: DEFAULT_DOCS_DIR,
2094
2771
  testsDir: "tests",
@@ -2097,11 +2774,21 @@ test("prompt commands capture checking before handoff and running at prompt inje
2097
2774
  et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2098
2775
  }),
2099
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);
2100
2783
  assert.equal(
2101
- 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"),
2102
2789
  buildExpectedFakeStatusText({
2103
- workflowState: "checking",
2104
- basePath: buildExpectedFakeBasePath(executionBasePath),
2790
+ workflowState: "error",
2791
+ basePath: buildExpectedFakeBasePath(projectBase),
2105
2792
  docsDir: DEFAULT_DOCS_DIR,
2106
2793
  testsDir: "tests",
2107
2794
  srcDir: ["src"],
@@ -2109,11 +2796,21 @@ test("prompt commands capture checking before handoff and running at prompt inje
2109
2796
  et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
2110
2797
  }),
2111
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
+ );
2112
2809
  assert.equal(
2113
2810
  ctx.__state.statuses.get("pi-usereq"),
2114
2811
  buildExpectedFakeStatusText({
2115
- workflowState: "running",
2116
- basePath: buildExpectedFakeBasePath(executionBasePath),
2812
+ workflowState: "error",
2813
+ basePath: buildExpectedFakeBasePath(projectBase),
2117
2814
  docsDir: DEFAULT_DOCS_DIR,
2118
2815
  testsDir: "tests",
2119
2816
  srcDir: ["src"],
@@ -2122,33 +2819,64 @@ test("prompt commands capture checking before handoff and running at prompt inje
2122
2819
  }),
2123
2820
  );
2124
2821
 
2125
- await pi.emit("before_agent_start", {}, ctx);
2126
2822
  await pi.emit("agent_start", {}, ctx);
2127
2823
  await pi.emit("agent_end", {
2128
2824
  messages: [{ role: "assistant", stopReason: "stop", content: [] }],
2129
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
+ );
2130
2838
  } finally {
2131
- setPromptCommandPostCreateHookForTests(undefined);
2132
2839
  process.chdir(previousCwd);
2133
2840
  fs.rmSync(projectBase, { recursive: true, force: true });
2134
2841
  }
2135
2842
  });
2136
2843
 
2137
- 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 () => {
2138
2852
  const { projectBase } = initFixtureRepo({ fixtures: [] });
2853
+ const previousCwd = process.cwd();
2139
2854
  try {
2140
- writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
2141
2855
  const pi = createFakePi();
2142
2856
  piUsereqExtension(pi);
2143
2857
  const ctx = createFakeCtx(projectBase);
2858
+
2144
2859
  await pi.emit("session_start", { reason: "startup" }, ctx);
2860
+ const originalSessionFile = ctx.sessionManager.getSessionFile() ?? "";
2861
+ assert.notEqual(originalSessionFile, "");
2145
2862
 
2146
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);
2147
2875
  assert.equal(
2148
2876
  ctx.__state.statuses.get("pi-usereq"),
2149
2877
  buildExpectedFakeStatusText({
2150
2878
  workflowState: "running",
2151
- basePath: buildExpectedFakeBasePath(projectBase),
2879
+ basePath: buildExpectedFakeBasePath(executionBasePath),
2152
2880
  docsDir: DEFAULT_DOCS_DIR,
2153
2881
  testsDir: "tests",
2154
2882
  srcDir: ["src"],
@@ -2161,13 +2889,16 @@ test("prompt commands reject non-idle workflow state before starting a new orche
2161
2889
  pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx),
2162
2890
  /expected idle/,
2163
2891
  );
2164
- assert.equal(pi.sentUserMessages.length, 1);
2165
- 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
+ );
2166
2897
  assert.equal(
2167
2898
  ctx.__state.statuses.get("pi-usereq"),
2168
2899
  buildExpectedFakeStatusText({
2169
- workflowState: "running",
2170
- basePath: buildExpectedFakeBasePath(projectBase),
2900
+ workflowState: "error",
2901
+ basePath: buildExpectedFakeBasePath(executionBasePath),
2171
2902
  docsDir: DEFAULT_DOCS_DIR,
2172
2903
  testsDir: "tests",
2173
2904
  srcDir: ["src"],
@@ -2176,10 +2907,30 @@ test("prompt commands reject non-idle workflow state before starting a new orche
2176
2907
  }),
2177
2908
  );
2178
2909
 
2910
+ await pi.emit("before_agent_start", {}, ctx);
2179
2911
  await pi.emit("agent_start", {}, ctx);
2180
2912
  await pi.emit("agent_end", {
2181
2913
  messages: [{ role: "assistant", stopReason: "stop", content: [] }],
2182
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
+ );
2183
2934
  assert.equal(
2184
2935
  ctx.__state.statuses.get("pi-usereq"),
2185
2936
  buildExpectedFakeStatusText({
@@ -2193,6 +2944,7 @@ test("prompt commands reject non-idle workflow state before starting a new orche
2193
2944
  }),
2194
2945
  );
2195
2946
  } finally {
2947
+ process.chdir(previousCwd);
2196
2948
  fs.rmSync(projectBase, { recursive: true, force: true });
2197
2949
  }
2198
2950
  });
@@ -2343,6 +3095,110 @@ test("worktree-enabled prompt commands merge successful runs, restore base-path,
2343
3095
  }
2344
3096
  });
2345
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
+ );
3196
+ } finally {
3197
+ process.chdir(previousCwd);
3198
+ fs.rmSync(projectBase, { recursive: true, force: true });
3199
+ }
3200
+ });
3201
+
2346
3202
  /**
2347
3203
  * @brief Verifies prompt-command bootstrap ignores stale deleted-worktree session metadata on later commands.
2348
3204
  * @details Simulates the `/tmp/debug.json` follow-up failure mode: one successful worktree-backed `req-change` run restores `base-path` and deletes its worktree, then a later `req-change` invocation receives a stale command context whose `cwd` and session getters still point at the removed execution worktree. The test proves command bootstrap MUST discard deleted-worktree cwd/session residue, prepare a fresh worktree with a new identifier, and complete the second successful run without reusing the removed execution path.
@@ -3996,6 +4852,58 @@ test("session_start renders the 0-percent context icon when context usage is emp
3996
4852
  );
3997
4853
  });
3998
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
+
3999
4907
  /**
4000
4908
  * @brief Verifies stale replacement contexts do not break workflow-state rendering.
4001
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.
@@ -4092,12 +5000,12 @@ test("context hook refreshes context usage and rounds progress cells upward", as
4092
5000
 
4093
5001
  /**
4094
5002
  * @brief Verifies context-gauge icon cutovers below the error range.
4095
- * @details Replays deterministic context percentages at documented threshold boundaries below `90%` so the status bar proves the `▕▂▏`, `▕▄▏`, `▕▆▏`, and `▕█▏` transitions while keeping the default-color contract intact. Runtime is O(n) in case count. Side effects are limited to in-memory status updates.
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.
4096
5004
  * @return {Promise<void>} Promise resolved after all threshold assertions complete.
4097
- * @throws {AssertionError} Throws when any threshold renders the wrong icon.
4098
- * @satisfies TST-096
5005
+ * @throws {AssertionError} Throws when any threshold renders the wrong icon or low-band color.
5006
+ * @satisfies REQ-122, REQ-126, TST-096
4099
5007
  */
4100
- test("context hook applies quarter-threshold gauge icons below the error range", async () => {
5008
+ test("context hook applies warning-colored quarter-threshold gauge icons below the error range", async () => {
4101
5009
  const cwd = createTempDir("pi-usereq-context-threshold-status-");
4102
5010
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
4103
5011
  const contextUsage = { tokens: 0, contextWindow: 1000, percent: 0 };
@@ -4476,11 +5384,12 @@ test("configuration menu orders tool toggles and can enable embedded builtin too
4476
5384
  "references",
4477
5385
  "search",
4478
5386
  "static-check",
5387
+ "summarize",
4479
5388
  "tokens",
4480
5389
  "files-compress",
4481
- "files-references",
4482
5390
  "files-search",
4483
5391
  "files-static-check",
5392
+ "files-summarize",
4484
5393
  "files-tokens",
4485
5394
  "bash",
4486
5395
  "edit",
@@ -4515,7 +5424,7 @@ test("configuration menu can disable configurable active tools", async () => {
4515
5424
  assert.deepEqual(pi.getActiveTools().filter((toolName: string) => PI_USEREQ_STARTUP_TOOL_NAMES.includes(toolName as never)), []);
4516
5425
  });
4517
5426
 
4518
- 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 () => {
4519
5428
  const cwd = createTempDir("pi-usereq-menu-notify-");
4520
5429
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
4521
5430
  const pi = createFakePi();
@@ -4529,7 +5438,7 @@ test("configuration menu can persist notify and sound settings", async () => {
4529
5438
  "Notification events",
4530
5439
  "Prompt failed",
4531
5440
  "Save and close",
4532
- "Enable sound",
5441
+ "Enable sound (boot value)",
4533
5442
  "high",
4534
5443
  "Sound events",
4535
5444
  "Prompt interrupted",
@@ -4551,6 +5460,7 @@ test("configuration menu can persist notify and sound settings", async () => {
4551
5460
  ],
4552
5461
  });
4553
5462
 
5463
+ await pi.emit("session_start", { reason: "startup" }, ctx);
4554
5464
  await command!.handler("", ctx);
4555
5465
 
4556
5466
  const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
@@ -4576,7 +5486,7 @@ test("configuration menu can persist notify and sound settings", async () => {
4576
5486
  srcDir: ["src"],
4577
5487
  contextFilledCells: 0,
4578
5488
  et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
4579
- sound: "high",
5489
+ sound: "none",
4580
5490
  }),
4581
5491
  );
4582
5492
  });
@@ -4662,7 +5572,7 @@ test("notifications menu escapes pushover text and locks pushover enablement unt
4662
5572
  const renderedNotificationsMenu = (ctx.__state.customRenderLines[2] ?? []).join("\n");
4663
5573
  assert.match(renderedNotificationsMenu, /first line\\nsecond line/);
4664
5574
  assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
4665
- assert.match(renderedNotificationsMenu, /<dim>off<\/dim>/);
5575
+ assert.match(renderedNotificationsMenu, /<dim>configure user\/token keys first<\/dim>/);
4666
5576
  });
4667
5577
 
4668
5578
  test("prompt-end pushover requests honor global enable, event toggles, credentials, priority, and templates", async () => {
@@ -5125,9 +6035,11 @@ test("sound routing honors the selected sound state and per-event toggles", asyn
5125
6035
  }
5126
6036
  });
5127
6037
 
5128
- 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 () => {
5129
6039
  const cwd = createTempDir("pi-usereq-shortcut-notify-");
5130
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");
5131
6043
  const previousCwd = process.cwd();
5132
6044
  const pi = createFakePi();
5133
6045
  process.chdir(cwd);
@@ -5144,8 +6056,7 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
5144
6056
 
5145
6057
  for (const expectedSound of ["low", "mid", "high", "none"]) {
5146
6058
  await shortcut!.handler(ctx);
5147
- const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
5148
- assert.equal(config["notify-sound"], expectedSound);
6059
+ assert.equal(fs.readFileSync(getProjectConfigPath(cwd), "utf8"), persistedConfigText);
5149
6060
  assert.equal(
5150
6061
  ctx.__state.statuses.get("pi-usereq"),
5151
6062
  buildExpectedFakeStatusText({
@@ -5161,6 +6072,42 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
5161
6072
  }
5162
6073
  });
5163
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
+
5164
6111
  test("static-check exposes the supported programming languages", () => {
5165
6112
  const supported = getSupportedStaticCheckLanguageSupport();
5166
6113
  assert.equal(supported.length, 20);