gsd-pi 2.28.0-dev.e19bf89 → 2.29.0-dev.49d972f

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 (116) hide show
  1. package/dist/cli.js +15 -9
  2. package/dist/resource-loader.js +80 -8
  3. package/dist/resources/extensions/gsd/auto-post-unit.ts +9 -4
  4. package/dist/resources/extensions/gsd/auto-recovery.ts +33 -23
  5. package/dist/resources/extensions/gsd/auto-start.ts +25 -10
  6. package/dist/resources/extensions/gsd/auto-verification.ts +41 -7
  7. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
  8. package/dist/resources/extensions/gsd/auto.ts +67 -22
  9. package/dist/resources/extensions/gsd/commands-handlers.ts +3 -11
  10. package/dist/resources/extensions/gsd/commands-logs.ts +536 -0
  11. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +46 -33
  12. package/dist/resources/extensions/gsd/commands.ts +22 -28
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +2 -1
  14. package/dist/resources/extensions/gsd/doctor-types.ts +13 -0
  15. package/dist/resources/extensions/gsd/doctor.ts +2 -6
  16. package/dist/resources/extensions/gsd/export.ts +28 -2
  17. package/dist/resources/extensions/gsd/gsd-db.ts +19 -0
  18. package/dist/resources/extensions/gsd/index.ts +2 -1
  19. package/dist/resources/extensions/gsd/json-persistence.ts +67 -0
  20. package/dist/resources/extensions/gsd/metrics.ts +17 -31
  21. package/dist/resources/extensions/gsd/paths.ts +0 -8
  22. package/dist/resources/extensions/gsd/queue-order.ts +10 -11
  23. package/dist/resources/extensions/gsd/routing-history.ts +13 -17
  24. package/dist/resources/extensions/gsd/session-lock.ts +284 -0
  25. package/dist/resources/extensions/gsd/session-status-io.ts +23 -41
  26. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  27. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  28. package/dist/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
  29. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
  30. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  31. package/dist/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
  32. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
  33. package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
  34. package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
  35. package/dist/resources/extensions/gsd/types.ts +1 -0
  36. package/dist/resources/extensions/gsd/unit-runtime.ts +16 -13
  37. package/dist/resources/extensions/gsd/verification-evidence.ts +2 -0
  38. package/dist/resources/extensions/gsd/verification-gate.ts +13 -2
  39. package/dist/resources/extensions/remote-questions/discord-adapter.ts +9 -20
  40. package/dist/resources/extensions/remote-questions/http-client.ts +76 -0
  41. package/dist/resources/extensions/remote-questions/notify.ts +1 -2
  42. package/dist/resources/extensions/remote-questions/slack-adapter.ts +11 -18
  43. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
  44. package/dist/resources/extensions/remote-questions/types.ts +3 -0
  45. package/dist/resources/extensions/shared/mod.ts +3 -0
  46. package/package.json +6 -3
  47. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  48. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  49. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  50. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  51. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/system-prompt.js +10 -0
  53. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  55. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -1
  56. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  57. package/packages/pi-coding-agent/package.json +1 -1
  58. package/packages/pi-coding-agent/scripts/copy-assets.cjs +39 -8
  59. package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
  60. package/packages/pi-coding-agent/src/core/system-prompt.ts +11 -0
  61. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -1
  62. package/packages/pi-tui/dist/autocomplete.d.ts +3 -0
  63. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  64. package/packages/pi-tui/dist/autocomplete.js +14 -0
  65. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  66. package/packages/pi-tui/src/autocomplete.ts +19 -1
  67. package/pkg/package.json +1 -1
  68. package/src/resources/extensions/gsd/auto-post-unit.ts +9 -4
  69. package/src/resources/extensions/gsd/auto-recovery.ts +33 -23
  70. package/src/resources/extensions/gsd/auto-start.ts +25 -10
  71. package/src/resources/extensions/gsd/auto-verification.ts +41 -7
  72. package/src/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
  73. package/src/resources/extensions/gsd/auto.ts +67 -22
  74. package/src/resources/extensions/gsd/commands-handlers.ts +3 -11
  75. package/src/resources/extensions/gsd/commands-logs.ts +536 -0
  76. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +46 -33
  77. package/src/resources/extensions/gsd/commands.ts +22 -28
  78. package/src/resources/extensions/gsd/dashboard-overlay.ts +2 -1
  79. package/src/resources/extensions/gsd/doctor-types.ts +13 -0
  80. package/src/resources/extensions/gsd/doctor.ts +2 -6
  81. package/src/resources/extensions/gsd/export.ts +28 -2
  82. package/src/resources/extensions/gsd/gsd-db.ts +19 -0
  83. package/src/resources/extensions/gsd/index.ts +2 -1
  84. package/src/resources/extensions/gsd/json-persistence.ts +67 -0
  85. package/src/resources/extensions/gsd/metrics.ts +17 -31
  86. package/src/resources/extensions/gsd/paths.ts +0 -8
  87. package/src/resources/extensions/gsd/queue-order.ts +10 -11
  88. package/src/resources/extensions/gsd/routing-history.ts +13 -17
  89. package/src/resources/extensions/gsd/session-lock.ts +284 -0
  90. package/src/resources/extensions/gsd/session-status-io.ts +23 -41
  91. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  92. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  93. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
  94. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
  95. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  96. package/src/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
  97. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
  98. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
  99. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
  100. package/src/resources/extensions/gsd/types.ts +1 -0
  101. package/src/resources/extensions/gsd/unit-runtime.ts +16 -13
  102. package/src/resources/extensions/gsd/verification-evidence.ts +2 -0
  103. package/src/resources/extensions/gsd/verification-gate.ts +13 -2
  104. package/src/resources/extensions/remote-questions/discord-adapter.ts +9 -20
  105. package/src/resources/extensions/remote-questions/http-client.ts +76 -0
  106. package/src/resources/extensions/remote-questions/notify.ts +1 -2
  107. package/src/resources/extensions/remote-questions/slack-adapter.ts +11 -18
  108. package/src/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
  109. package/src/resources/extensions/remote-questions/types.ts +3 -0
  110. package/src/resources/extensions/shared/mod.ts +3 -0
  111. package/dist/resources/extensions/gsd/preferences-hooks.ts +0 -10
  112. package/dist/resources/extensions/shared/progress-widget.ts +0 -282
  113. package/dist/resources/extensions/shared/thinking-widget.ts +0 -107
  114. package/src/resources/extensions/gsd/preferences-hooks.ts +0 -10
  115. package/src/resources/extensions/shared/progress-widget.ts +0 -282
  116. package/src/resources/extensions/shared/thinking-widget.ts +0 -107
@@ -581,7 +581,7 @@ test("formatFailureContext: formats a single failure with command, exit code, st
581
581
  const result: import("../types.ts").VerificationResult = {
582
582
  passed: false,
583
583
  checks: [
584
- { command: "npm run lint", exitCode: 1, stdout: "", stderr: "error: unused var", durationMs: 500 },
584
+ { command: "npm run lint", exitCode: 1, stdout: "", stderr: "error: unused var", durationMs: 500, blocking: true },
585
585
  ],
586
586
  discoverySource: "preference",
587
587
  timestamp: Date.now(),
@@ -598,9 +598,9 @@ test("formatFailureContext: formats multiple failures", () => {
598
598
  const result: import("../types.ts").VerificationResult = {
599
599
  passed: false,
600
600
  checks: [
601
- { command: "npm run lint", exitCode: 1, stdout: "", stderr: "lint error", durationMs: 100 },
602
- { command: "npm run test", exitCode: 2, stdout: "", stderr: "test failure", durationMs: 200 },
603
- { command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 50 },
601
+ { command: "npm run lint", exitCode: 1, stdout: "", stderr: "lint error", durationMs: 100, blocking: true },
602
+ { command: "npm run test", exitCode: 2, stdout: "", stderr: "test failure", durationMs: 200, blocking: true },
603
+ { command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 50, blocking: true },
604
604
  ],
605
605
  discoverySource: "preference",
606
606
  timestamp: Date.now(),
@@ -619,7 +619,7 @@ test("formatFailureContext: truncates stderr longer than 2000 chars", () => {
619
619
  const result: import("../types.ts").VerificationResult = {
620
620
  passed: false,
621
621
  checks: [
622
- { command: "big-err", exitCode: 1, stdout: "", stderr: longStderr, durationMs: 100 },
622
+ { command: "big-err", exitCode: 1, stdout: "", stderr: longStderr, durationMs: 100, blocking: true },
623
623
  ],
624
624
  discoverySource: "preference",
625
625
  timestamp: Date.now(),
@@ -634,8 +634,8 @@ test("formatFailureContext: returns empty string when all checks pass", () => {
634
634
  const result: import("../types.ts").VerificationResult = {
635
635
  passed: true,
636
636
  checks: [
637
- { command: "npm run lint", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100 },
638
- { command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 200 },
637
+ { command: "npm run lint", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100, blocking: true },
638
+ { command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 200, blocking: true },
639
639
  ],
640
640
  discoverySource: "preference",
641
641
  timestamp: Date.now(),
@@ -663,6 +663,7 @@ test("formatFailureContext: caps total output at 10,000 chars", () => {
663
663
  stdout: "",
664
664
  stderr: "e".repeat(1000), // 1000 chars each, 20 * ~1050 (with formatting) > 10,000
665
665
  durationMs: 100,
666
+ blocking: true,
666
667
  });
667
668
  }
668
669
  const result: import("../types.ts").VerificationResult = {
@@ -1077,3 +1078,131 @@ test("dependency-audit: subdirectory package.json does not trigger audit", () =>
1077
1078
  assert.equal(npmAuditCalled, false, "subdirectory dependency files should not trigger audit");
1078
1079
  assert.deepStrictEqual(result, []);
1079
1080
  });
1081
+
1082
+ // ─── Non-Blocking Discovery Tests ────────────────────────────────────────────
1083
+
1084
+ test("non-blocking: package-json discovered commands failing → result.passed is still true", () => {
1085
+ const tmp = makeTempDir("vg-nb-pkg-fail");
1086
+ try {
1087
+ writeFileSync(
1088
+ join(tmp, "package.json"),
1089
+ JSON.stringify({ scripts: { lint: "eslint .", test: "vitest" } }),
1090
+ );
1091
+ // These commands will fail because eslint/vitest don't exist in the temp dir
1092
+ const result = runVerificationGate({
1093
+ basePath: tmp,
1094
+ unitId: "T01",
1095
+ cwd: tmp,
1096
+ // No preference commands — discovery falls through to package.json
1097
+ });
1098
+ assert.equal(result.discoverySource, "package-json");
1099
+ assert.ok(result.checks.length > 0, "should have discovered package.json checks");
1100
+ assert.equal(result.passed, true, "package-json failures should not block the gate");
1101
+ for (const check of result.checks) {
1102
+ assert.equal(check.blocking, false, "package-json checks should be non-blocking");
1103
+ }
1104
+ } finally {
1105
+ rmSync(tmp, { recursive: true, force: true });
1106
+ }
1107
+ });
1108
+
1109
+ test("non-blocking: preference commands failing → result.passed is false", () => {
1110
+ const tmp = makeTempDir("vg-nb-pref-fail");
1111
+ try {
1112
+ const result = runVerificationGate({
1113
+ basePath: tmp,
1114
+ unitId: "T01",
1115
+ cwd: tmp,
1116
+ preferenceCommands: ["sh -c 'exit 1'"],
1117
+ });
1118
+ assert.equal(result.discoverySource, "preference");
1119
+ assert.equal(result.passed, false, "preference failures should block the gate");
1120
+ assert.equal(result.checks[0].blocking, true, "preference checks should be blocking");
1121
+ } finally {
1122
+ rmSync(tmp, { recursive: true, force: true });
1123
+ }
1124
+ });
1125
+
1126
+ test("non-blocking: task-plan commands failing → result.passed is false", () => {
1127
+ const tmp = makeTempDir("vg-nb-tp-fail");
1128
+ try {
1129
+ const result = runVerificationGate({
1130
+ basePath: tmp,
1131
+ unitId: "T01",
1132
+ cwd: tmp,
1133
+ taskPlanVerify: "sh -c 'exit 1'",
1134
+ });
1135
+ assert.equal(result.discoverySource, "task-plan");
1136
+ assert.equal(result.passed, false, "task-plan failures should block the gate");
1137
+ assert.equal(result.checks[0].blocking, true, "task-plan checks should be blocking");
1138
+ } finally {
1139
+ rmSync(tmp, { recursive: true, force: true });
1140
+ }
1141
+ });
1142
+
1143
+ test("non-blocking: blocking field is set correctly based on discovery source", () => {
1144
+ const tmp = makeTempDir("vg-nb-field");
1145
+ try {
1146
+ // preference → blocking
1147
+ const prefResult = runVerificationGate({
1148
+ basePath: tmp,
1149
+ unitId: "T01",
1150
+ cwd: tmp,
1151
+ preferenceCommands: ["echo ok"],
1152
+ });
1153
+ assert.equal(prefResult.checks[0].blocking, true);
1154
+
1155
+ // task-plan → blocking
1156
+ const tpResult = runVerificationGate({
1157
+ basePath: tmp,
1158
+ unitId: "T01",
1159
+ cwd: tmp,
1160
+ taskPlanVerify: "echo ok",
1161
+ });
1162
+ assert.equal(tpResult.checks[0].blocking, true);
1163
+
1164
+ // package-json → non-blocking
1165
+ writeFileSync(
1166
+ join(tmp, "package.json"),
1167
+ JSON.stringify({ scripts: { test: "echo ok" } }),
1168
+ );
1169
+ const pkgResult = runVerificationGate({
1170
+ basePath: tmp,
1171
+ unitId: "T01",
1172
+ cwd: tmp,
1173
+ });
1174
+ assert.equal(pkgResult.checks[0].blocking, false);
1175
+ } finally {
1176
+ rmSync(tmp, { recursive: true, force: true });
1177
+ }
1178
+ });
1179
+
1180
+ test("non-blocking: formatFailureContext only includes blocking failures", () => {
1181
+ const result: import("../types.ts").VerificationResult = {
1182
+ passed: true,
1183
+ checks: [
1184
+ { command: "npm run lint", exitCode: 1, stdout: "", stderr: "lint warning", durationMs: 100, blocking: false },
1185
+ { command: "npm run test", exitCode: 1, stdout: "", stderr: "test error", durationMs: 200, blocking: true },
1186
+ { command: "npm run typecheck", exitCode: 1, stdout: "", stderr: "type error", durationMs: 50, blocking: false },
1187
+ ],
1188
+ discoverySource: "preference",
1189
+ timestamp: Date.now(),
1190
+ };
1191
+ const output = formatFailureContext(result);
1192
+ assert.ok(output.includes("`npm run test`"), "should include blocking failure");
1193
+ assert.ok(!output.includes("npm run lint"), "should not include non-blocking failure");
1194
+ assert.ok(!output.includes("npm run typecheck"), "should not include non-blocking failure");
1195
+ });
1196
+
1197
+ test("non-blocking: formatFailureContext returns empty when only non-blocking failures exist", () => {
1198
+ const result: import("../types.ts").VerificationResult = {
1199
+ passed: true,
1200
+ checks: [
1201
+ { command: "npm run lint", exitCode: 1, stdout: "", stderr: "lint warning", durationMs: 100, blocking: false },
1202
+ { command: "npm run test", exitCode: 1, stdout: "", stderr: "test warning", durationMs: 200, blocking: false },
1203
+ ],
1204
+ discoverySource: "package-json",
1205
+ timestamp: Date.now(),
1206
+ };
1207
+ assert.equal(formatFailureContext(result), "", "should return empty when only non-blocking failures");
1208
+ });
@@ -55,6 +55,7 @@ export interface VerificationCheck {
55
55
  stdout: string;
56
56
  stderr: string;
57
57
  durationMs: number;
58
+ blocking: boolean; // true for preference/task-plan sources, false for package-json (advisory only)
58
59
  }
59
60
 
60
61
  /** A runtime error captured from bg-shell processes or browser console */
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import {
4
4
  gsdRoot,
@@ -8,6 +8,7 @@ import {
8
8
  resolveTaskFile,
9
9
  } from "./paths.js";
10
10
  import { loadFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
11
+ import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
11
12
 
12
13
  export type UnitRuntimePhase =
13
14
  | "dispatched"
@@ -46,13 +47,23 @@ export interface AutoUnitRuntimeRecord {
46
47
  lastRecoveryReason?: "idle" | "hard";
47
48
  }
48
49
 
50
+ function isAutoUnitRuntimeRecord(data: unknown): data is AutoUnitRuntimeRecord {
51
+ return (
52
+ typeof data === "object" &&
53
+ data !== null &&
54
+ (data as AutoUnitRuntimeRecord).version === 1 &&
55
+ typeof (data as AutoUnitRuntimeRecord).unitType === "string" &&
56
+ typeof (data as AutoUnitRuntimeRecord).unitId === "string"
57
+ );
58
+ }
59
+
49
60
  function runtimeDir(basePath: string): string {
50
61
  return join(gsdRoot(basePath), "runtime", "units");
51
62
  }
52
63
 
53
64
  function runtimePath(basePath: string, unitType: string, unitId: string): string {
54
- const sanitizedUnitType = unitType.replace(/[\/]/g, "-");
55
- const sanitizedUnitId = unitId.replace(/[\/]/g, "-");
65
+ const sanitizedUnitType = unitType.replace(/[^a-zA-Z0-9._-]+/g, "-");
66
+ const sanitizedUnitId = unitId.replace(/[^a-zA-Z0-9._-]+/g, "-");
56
67
  return join(runtimeDir(basePath), `${sanitizedUnitType}-${sanitizedUnitId}.json`);
57
68
  }
58
69
 
@@ -63,8 +74,6 @@ export function writeUnitRuntimeRecord(
63
74
  startedAt: number,
64
75
  updates: Partial<AutoUnitRuntimeRecord> = {},
65
76
  ): AutoUnitRuntimeRecord {
66
- const dir = runtimeDir(basePath);
67
- mkdirSync(dir, { recursive: true });
68
77
  const path = runtimePath(basePath, unitType, unitId);
69
78
  const prev = readUnitRuntimeRecord(basePath, unitType, unitId);
70
79
  const next: AutoUnitRuntimeRecord = {
@@ -84,18 +93,12 @@ export function writeUnitRuntimeRecord(
84
93
  recoveryAttempts: updates.recoveryAttempts ?? prev?.recoveryAttempts ?? 0,
85
94
  lastRecoveryReason: updates.lastRecoveryReason ?? prev?.lastRecoveryReason,
86
95
  };
87
- writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
96
+ saveJsonFile(path, next);
88
97
  return next;
89
98
  }
90
99
 
91
100
  export function readUnitRuntimeRecord(basePath: string, unitType: string, unitId: string): AutoUnitRuntimeRecord | null {
92
- const path = runtimePath(basePath, unitType, unitId);
93
- if (!existsSync(path)) return null;
94
- try {
95
- return JSON.parse(readFileSync(path, "utf-8")) as AutoUnitRuntimeRecord;
96
- } catch {
97
- return null;
98
- }
101
+ return loadJsonFileOrNull(runtimePath(basePath, unitType, unitId), isAutoUnitRuntimeRecord);
99
102
  }
100
103
 
101
104
  export function clearUnitRuntimeRecord(basePath: string, unitType: string, unitId: string): void {
@@ -20,6 +20,7 @@ export interface EvidenceCheckJSON {
20
20
  exitCode: number;
21
21
  durationMs: number;
22
22
  verdict: "pass" | "fail";
23
+ blocking: boolean;
23
24
  }
24
25
 
25
26
  export interface RuntimeErrorJSON {
@@ -80,6 +81,7 @@ export function writeVerificationJSON(
80
81
  exitCode: check.exitCode,
81
82
  durationMs: check.durationMs,
82
83
  verdict: check.exitCode === 0 ? "pass" : "fail",
84
+ blocking: check.blocking,
83
85
  })),
84
86
  ...(retryAttempt !== undefined ? { retryAttempt } : {}),
85
87
  ...(maxRetries !== undefined ? { maxRetries } : {}),
@@ -112,7 +112,9 @@ const MAX_FAILURE_CONTEXT_CHARS = 10_000;
112
112
  * Returns an empty string when all checks pass or the checks array is empty.
113
113
  */
114
114
  export function formatFailureContext(result: VerificationResult): string {
115
- const failures = result.checks.filter((c) => c.exitCode !== 0);
115
+ // Only include blocking failures in retry context non-blocking (advisory) failures
116
+ // should not be injected into retry prompts to avoid noise pollution.
117
+ const failures = result.checks.filter((c) => c.exitCode !== 0 && c.blocking);
116
118
  if (failures.length === 0) return "";
117
119
 
118
120
  const blocks: string[] = [];
@@ -256,6 +258,10 @@ export function runVerificationGate(options: RunVerificationGateOptions): Verifi
256
258
  };
257
259
  }
258
260
 
261
+ // Commands from preference and task-plan sources are blocking;
262
+ // package-json discovered commands are advisory (non-blocking).
263
+ const blocking = source === "preference" || source === "task-plan";
264
+
259
265
  const checks: VerificationCheck[] = [];
260
266
 
261
267
  for (const command of commands) {
@@ -291,11 +297,16 @@ export function runVerificationGate(options: RunVerificationGateOptions): Verifi
291
297
  stdout: truncate(result.stdout, MAX_OUTPUT_BYTES),
292
298
  stderr,
293
299
  durationMs,
300
+ blocking,
294
301
  });
295
302
  }
296
303
 
304
+ // Gate passes if all blocking checks pass (non-blocking failures are advisory)
305
+ const blockingChecks = checks.filter(c => c.blocking);
306
+ const passed = blockingChecks.length === 0 || blockingChecks.every(c => c.exitCode === 0);
307
+
297
308
  return {
298
- passed: checks.every(c => c.exitCode === 0),
309
+ passed,
299
310
  checks,
300
311
  discoverySource: source,
301
312
  timestamp,
@@ -2,11 +2,12 @@
2
2
  * Remote Questions — Discord adapter
3
3
  */
4
4
 
5
- import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
5
+ import { type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
6
6
  import { formatForDiscord, parseDiscordResponse, DISCORD_NUMBER_EMOJIS } from "./format.js";
7
+ import { apiRequest } from "./http-client.js";
7
8
 
8
9
  const DISCORD_API = "https://discord.com/api/v10";
9
- const PER_REQUEST_TIMEOUT_MS = 15_000;
10
+
10
11
  export class DiscordAdapter implements ChannelAdapter {
11
12
  readonly name = "discord" as const;
12
13
  private botUserId: string | null = null;
@@ -137,23 +138,11 @@ export class DiscordAdapter implements ChannelAdapter {
137
138
  return parseDiscordResponse([], String(replies[0].content), prompt.questions);
138
139
  }
139
140
 
140
- private async discordApi(method: string, path: string, body?: unknown): Promise<any> {
141
- const headers: Record<string, string> = { Authorization: `Bot ${this.token}` };
142
- const init: RequestInit = { method, headers };
143
- if (body) {
144
- headers["Content-Type"] = "application/json";
145
- init.body = JSON.stringify(body);
146
- }
147
-
148
- init.signal = AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS);
149
- const response = await fetch(`${DISCORD_API}${path}`, init);
150
- if (response.status === 204) return {};
151
- if (!response.ok) {
152
- const text = await response.text().catch(() => "");
153
- // Limit error body length to avoid leaking verbose Discord error responses
154
- const safeText = text.length > 200 ? text.slice(0, 200) + "…" : text;
155
- throw new Error(`Discord API HTTP ${response.status}: ${safeText}`);
156
- }
157
- return response.json();
141
+ private async discordApi(method: "GET" | "POST" | "PUT" | "DELETE", path: string, body?: unknown): Promise<any> {
142
+ return apiRequest(`${DISCORD_API}${path}`, method, body, {
143
+ authScheme: "Bot",
144
+ authToken: this.token,
145
+ errorLabel: "Discord API",
146
+ });
158
147
  }
159
148
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Remote Questions — shared HTTP client
3
+ *
4
+ * Centralizes timeout, error handling, and JSON serialization logic
5
+ * used by all channel adapters (Discord, Slack, Telegram).
6
+ */
7
+
8
+ import { PER_REQUEST_TIMEOUT_MS } from "./types.js";
9
+
10
+ export interface ApiRequestOptions {
11
+ /** Authorization header scheme. Omit to skip the Authorization header entirely. */
12
+ authScheme?: "Bearer" | "Bot";
13
+ /** Token for the Authorization header. Ignored when authScheme is omitted. */
14
+ authToken?: string;
15
+ /** Max chars of error body to include in thrown Error. Default 200. */
16
+ safeErrorLength?: number;
17
+ /** Label used in error messages (e.g. "Discord API", "Slack API"). Default "HTTP". */
18
+ errorLabel?: string;
19
+ /** Content-Type override. Default "application/json" when body is present. */
20
+ contentType?: string;
21
+ }
22
+
23
+ /**
24
+ * Makes an HTTP request with standardized timeout, error handling, and JSON
25
+ * serialization.
26
+ *
27
+ * - Sets `AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS)` on every request.
28
+ * - Serializes `body` as JSON and sets Content-Type when provided.
29
+ * - Returns `{}` for 204 No Content responses.
30
+ * - Truncates error response bodies to `safeErrorLength` chars (default 200).
31
+ */
32
+ export async function apiRequest(
33
+ url: string,
34
+ method: "GET" | "POST" | "PUT" | "DELETE",
35
+ body?: unknown,
36
+ options: ApiRequestOptions = {},
37
+ ): Promise<any> {
38
+ const {
39
+ authScheme,
40
+ authToken,
41
+ safeErrorLength = 200,
42
+ errorLabel = "HTTP",
43
+ contentType,
44
+ } = options;
45
+
46
+ const headers: Record<string, string> = {};
47
+ if (authScheme && authToken) {
48
+ headers["Authorization"] = `${authScheme} ${authToken}`;
49
+ }
50
+
51
+ const init: RequestInit = {
52
+ method,
53
+ headers,
54
+ signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
55
+ };
56
+
57
+ if (body !== undefined) {
58
+ headers["Content-Type"] = contentType ?? "application/json";
59
+ init.body = JSON.stringify(body);
60
+ }
61
+
62
+ const response = await fetch(url, init);
63
+
64
+ if (response.status === 204) return {};
65
+
66
+ if (!response.ok) {
67
+ const text = await response.text().catch(() => "");
68
+ const safeText =
69
+ text.length > safeErrorLength
70
+ ? text.slice(0, safeErrorLength) + "\u2026"
71
+ : text;
72
+ throw new Error(`${errorLabel} HTTP ${response.status}: ${safeText}`);
73
+ }
74
+
75
+ return response.json();
76
+ }
@@ -9,8 +9,7 @@
9
9
 
10
10
  import { resolveRemoteConfig } from "./config.js";
11
11
  import type { ResolvedConfig } from "./config.js";
12
-
13
- const PER_REQUEST_TIMEOUT_MS = 15_000;
12
+ import { PER_REQUEST_TIMEOUT_MS } from "./types.js";
14
13
 
15
14
  /**
16
15
  * Send a one-way notification to the configured remote channel.
@@ -2,11 +2,11 @@
2
2
  * Remote Questions — Slack adapter
3
3
  */
4
4
 
5
- import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
5
+ import { type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
6
6
  import { formatForSlack, parseSlackReply, parseSlackReactionResponse, SLACK_NUMBER_REACTION_NAMES } from "./format.js";
7
+ import { apiRequest } from "./http-client.js";
7
8
 
8
9
  const SLACK_API = "https://slack.com/api";
9
- const PER_REQUEST_TIMEOUT_MS = 15_000;
10
10
  const SLACK_ACK_REACTION = "white_check_mark";
11
11
 
12
12
  export class SlackAdapter implements ChannelAdapter {
@@ -123,26 +123,19 @@ export class SlackAdapter implements ChannelAdapter {
123
123
  }
124
124
 
125
125
  private async slackApi(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> {
126
- const url = `${SLACK_API}/${method}`;
127
126
  const isGet = method === "conversations.replies" || method === "auth.test" || method === "reactions.get";
127
+ const opts = { authScheme: "Bearer" as const, authToken: this.token, errorLabel: "Slack API" };
128
128
 
129
- let response: Response;
130
129
  if (isGet) {
131
- const qs = new URLSearchParams(Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)]))).toString();
132
- response = await fetch(`${url}?${qs}`, { method: "GET", headers: { Authorization: `Bearer ${this.token}` }, signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS) });
133
- } else {
134
- response = await fetch(url, {
135
- method: "POST",
136
- headers: {
137
- Authorization: `Bearer ${this.token}`,
138
- "Content-Type": "application/json; charset=utf-8",
139
- },
140
- body: JSON.stringify(params),
141
- signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
142
- });
130
+ const qs = new URLSearchParams(
131
+ Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)])),
132
+ ).toString();
133
+ return apiRequest(`${SLACK_API}/${method}?${qs}`, "GET", undefined, opts);
143
134
  }
144
135
 
145
- if (!response.ok) throw new Error(`Slack API HTTP ${response.status}: ${response.statusText}`);
146
- return (await response.json()) as Record<string, unknown>;
136
+ return apiRequest(`${SLACK_API}/${method}`, "POST", params, {
137
+ ...opts,
138
+ contentType: "application/json; charset=utf-8",
139
+ });
147
140
  }
148
141
  }
@@ -2,11 +2,11 @@
2
2
  * Remote Questions — Telegram adapter
3
3
  */
4
4
 
5
- import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
5
+ import { type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
6
6
  import { formatForTelegram, parseTelegramResponse } from "./format.js";
7
+ import { apiRequest } from "./http-client.js";
7
8
 
8
9
  const TELEGRAM_API = "https://api.telegram.org";
9
- const PER_REQUEST_TIMEOUT_MS = 15_000;
10
10
 
11
11
  export class TelegramAdapter implements ChannelAdapter {
12
12
  readonly name = "telegram" as const;
@@ -139,23 +139,11 @@ export class TelegramAdapter implements ChannelAdapter {
139
139
  }
140
140
 
141
141
  private async telegramApi(method: string, params?: Record<string, unknown>): Promise<any> {
142
- const url = `${TELEGRAM_API}/bot${this.token}/${method}`;
143
- const init: RequestInit = {
144
- method: "POST",
145
- headers: { "Content-Type": "application/json" },
146
- signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
147
- };
148
-
149
- if (params) {
150
- init.body = JSON.stringify(params);
151
- }
152
-
153
- const response = await fetch(url, init);
154
- if (!response.ok) {
155
- const text = await response.text().catch(() => "");
156
- const safeText = text.length > 200 ? text.slice(0, 200) + "…" : text;
157
- throw new Error(`Telegram API HTTP ${response.status}: ${safeText}`);
158
- }
159
- return response.json();
142
+ return apiRequest(
143
+ `${TELEGRAM_API}/bot${this.token}/${method}`,
144
+ "POST",
145
+ params,
146
+ { errorLabel: "Telegram API" },
147
+ );
160
148
  }
161
149
  }
@@ -2,6 +2,9 @@
2
2
  * Remote Questions — shared types
3
3
  */
4
4
 
5
+ /** Timeout applied to every outbound HTTP request across all channel adapters. */
6
+ export const PER_REQUEST_TIMEOUT_MS = 15_000;
7
+
5
8
  export type RemoteChannel = "slack" | "discord" | "telegram";
6
9
 
7
10
  export interface RemoteQuestionOption {
@@ -28,3 +28,6 @@ export { showInterviewRound } from "./interview-ui.js";
28
28
  export type { Question, QuestionOption, RoundResult } from "./interview-ui.js";
29
29
  export { showNextAction } from "./next-action-ui.js";
30
30
  export { showConfirm } from "./confirm-ui.js";
31
+ export { sanitizeError } from "./sanitize.js";
32
+ export { formatDateShort, truncateWithEllipsis } from "./format-utils.js";
33
+ export { splitFrontmatter, parseFrontmatterMap } from "./frontmatter.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.28.0-dev.e19bf89",
3
+ "version": "2.29.0-dev.49d972f",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "configDir": ".gsd"
36
36
  },
37
37
  "engines": {
38
- "node": ">=20.6.0"
38
+ "node": ">=22.0.0"
39
39
  },
40
40
  "packageManager": "npm@10.9.3",
41
41
  "scripts": {
@@ -73,6 +73,9 @@
73
73
  "validate-pack": "node scripts/validate-pack.js",
74
74
  "typecheck:extensions": "tsc --noEmit --project tsconfig.extensions.json",
75
75
  "pipeline:version-stamp": "node scripts/version-stamp.mjs",
76
+ "release:changelog": "node scripts/generate-changelog.mjs",
77
+ "release:bump": "node scripts/bump-version.mjs",
78
+ "release:update-changelog": "node scripts/update-changelog.mjs",
76
79
  "docker:build-runtime": "docker build --target runtime -t ghcr.io/gsd-build/gsd-pi .",
77
80
  "docker:build-builder": "docker build --target builder -t ghcr.io/gsd-build/gsd-ci-builder .",
78
81
  "prepublishOnly": "npm run sync-pkg-version && npm run sync-platform-versions && ([ \"$CI\" = 'true' ] || git diff --exit-code || (echo 'ERROR: version sync changed files — commit them before publishing' && exit 1)) && npm run build && npm run typecheck:extensions && npm run validate-pack"
@@ -117,7 +120,7 @@
117
120
  "zod-to-json-schema": "^3.24.6"
118
121
  },
119
122
  "devDependencies": {
120
- "@types/node": "^22.0.0",
123
+ "@types/node": "^24.12.0",
121
124
  "@types/picomatch": "^4.0.2",
122
125
  "c8": "^11.0.0",
123
126
  "jiti": "^2.6.1",
@@ -112,6 +112,7 @@ export interface Settings {
112
112
  editorPaddingX?: number;
113
113
  autocompleteMaxVisible?: number;
114
114
  respectGitignoreInPicker?: boolean;
115
+ searchExcludeDirs?: string[];
115
116
  showHardwareCursor?: boolean;
116
117
  markdown?: MarkdownSettings;
117
118
  memory?: MemorySettings;
@@ -281,6 +282,8 @@ export declare class SettingsManager {
281
282
  setAutocompleteMaxVisible(maxVisible: number): void;
282
283
  getRespectGitignoreInPicker(): boolean;
283
284
  setRespectGitignoreInPicker(value: boolean): void;
285
+ getSearchExcludeDirs(): string[];
286
+ setSearchExcludeDirs(dirs: string[]): void;
284
287
  getCodeBlockIndent(): string;
285
288
  getMemorySettings(): {
286
289
  enabled: boolean;