gsd-pi 2.38.0-dev.63ad7e5 → 2.38.0-dev.7209774

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 (76) hide show
  1. package/dist/resource-loader.js +34 -1
  2. package/dist/resources/extensions/browser-tools/index.js +3 -1
  3. package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
  4. package/dist/resources/extensions/github-sync/cli.js +284 -0
  5. package/dist/resources/extensions/github-sync/index.js +73 -0
  6. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  7. package/dist/resources/extensions/github-sync/sync.js +424 -0
  8. package/dist/resources/extensions/github-sync/templates.js +118 -0
  9. package/dist/resources/extensions/github-sync/types.js +7 -0
  10. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  11. package/dist/resources/extensions/gsd/auto-loop.js +593 -516
  12. package/dist/resources/extensions/gsd/auto-post-unit.js +28 -3
  13. package/dist/resources/extensions/gsd/auto-prompts.js +26 -15
  14. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  15. package/dist/resources/extensions/gsd/commands.js +2 -1
  16. package/dist/resources/extensions/gsd/doctor.js +20 -1
  17. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  18. package/dist/resources/extensions/gsd/files.js +4 -0
  19. package/dist/resources/extensions/gsd/git-service.js +30 -12
  20. package/dist/resources/extensions/gsd/guided-flow.js +82 -32
  21. package/dist/resources/extensions/gsd/index.js +22 -19
  22. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  23. package/dist/resources/extensions/gsd/paths.js +3 -0
  24. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  25. package/dist/resources/extensions/gsd/preferences-validation.js +58 -0
  26. package/dist/resources/extensions/gsd/preferences.js +3 -0
  27. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
  28. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
  29. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  30. package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
  31. package/dist/resources/extensions/mcp-client/index.js +14 -1
  32. package/package.json +1 -1
  33. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  34. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  35. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  36. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  37. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  38. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  39. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  40. package/src/resources/extensions/browser-tools/index.ts +3 -0
  41. package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
  42. package/src/resources/extensions/github-sync/cli.ts +364 -0
  43. package/src/resources/extensions/github-sync/index.ts +93 -0
  44. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  45. package/src/resources/extensions/github-sync/sync.ts +556 -0
  46. package/src/resources/extensions/github-sync/templates.ts +183 -0
  47. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  48. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  49. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  50. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  51. package/src/resources/extensions/github-sync/types.ts +47 -0
  52. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  53. package/src/resources/extensions/gsd/auto-loop.ts +472 -434
  54. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  55. package/src/resources/extensions/gsd/auto-prompts.ts +29 -15
  56. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  57. package/src/resources/extensions/gsd/commands.ts +2 -2
  58. package/src/resources/extensions/gsd/doctor.ts +22 -1
  59. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  60. package/src/resources/extensions/gsd/files.ts +3 -1
  61. package/src/resources/extensions/gsd/git-service.ts +44 -10
  62. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  63. package/src/resources/extensions/gsd/index.ts +21 -16
  64. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  65. package/src/resources/extensions/gsd/paths.ts +4 -0
  66. package/src/resources/extensions/gsd/preferences-types.ts +4 -0
  67. package/src/resources/extensions/gsd/preferences-validation.ts +50 -0
  68. package/src/resources/extensions/gsd/preferences.ts +3 -0
  69. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
  70. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
  71. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  72. package/src/resources/extensions/gsd/templates/runtime.md +21 -0
  73. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +111 -37
  74. package/src/resources/extensions/gsd/types.ts +8 -0
  75. package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
  76. package/src/resources/extensions/mcp-client/index.ts +17 -1
@@ -7,6 +7,7 @@ import {
7
7
  resolveAgentEnd,
8
8
  runUnit,
9
9
  autoLoop,
10
+ detectStuck,
10
11
  _resetPendingResolve,
11
12
  _setActiveSession,
12
13
  isSessionSwitchInFlight,
@@ -104,7 +105,6 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
104
105
  "task",
105
106
  "T01",
106
107
  "do stuff",
107
- undefined,
108
108
  );
109
109
 
110
110
  // Give the microtask queue a tick so runUnit reaches the await
@@ -136,7 +136,7 @@ test("double resolveAgentEnd only resolves once (second is dropped)", async () =
136
136
  const event1 = makeEvent([{ id: 1 }]);
137
137
  const event2 = makeEvent([{ id: 2 }]);
138
138
 
139
- const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt", undefined);
139
+ const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");
140
140
 
141
141
  await new Promise((r) => setTimeout(r, 10));
142
142
 
@@ -161,7 +161,7 @@ test("runUnit returns cancelled when session creation fails", async () => {
161
161
  const pi = makeMockPi();
162
162
  const s = makeMockSession({ newSessionThrows: "connection refused" });
163
163
 
164
- const result = await runUnit(ctx, pi, s, "task", "T01", "prompt", undefined);
164
+ const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
165
165
 
166
166
  assert.equal(result.status, "cancelled");
167
167
  assert.equal(result.event, undefined);
@@ -177,7 +177,7 @@ test("runUnit returns cancelled when session creation times out", async () => {
177
177
  // Session returns cancelled: true (simulates the timeout race outcome)
178
178
  const s = makeMockSession({ newSessionResult: { cancelled: true } });
179
179
 
180
- const result = await runUnit(ctx, pi, s, "task", "T01", "prompt", undefined);
180
+ const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
181
181
 
182
182
  assert.equal(result.status, "cancelled");
183
183
  assert.equal(result.event, undefined);
@@ -192,7 +192,7 @@ test("runUnit returns cancelled when s.active is false before sendMessage", asyn
192
192
  const s = makeMockSession();
193
193
  s.active = false;
194
194
 
195
- const result = await runUnit(ctx, pi, s, "task", "T01", "prompt", undefined);
195
+ const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
196
196
 
197
197
  assert.equal(result.status, "cancelled");
198
198
  assert.equal(pi.calls.length, 0);
@@ -212,7 +212,7 @@ test("runUnit only arms resolve after newSession completes", async () => {
212
212
  },
213
213
  });
214
214
 
215
- const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt", undefined);
215
+ const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");
216
216
 
217
217
  await new Promise((r) => setTimeout(r, 30));
218
218
 
@@ -1043,7 +1043,7 @@ test("handleAgentEnd in auto.ts is a thin wrapper calling resolveAgentEnd", () =
1043
1043
 
1044
1044
  // ── Stuck counter tests ──────────────────────────────────────────────────────
1045
1045
 
1046
- test("stuck counter: stops when deriveState returns same unit 5 consecutive times", async () => {
1046
+ test("stuck detection: stops when sliding window detects same unit 3 consecutive times", async () => {
1047
1047
  _resetPendingResolve();
1048
1048
 
1049
1049
  const ctx = makeMockCtx();
@@ -1078,20 +1078,15 @@ test("stuck counter: stops when deriveState returns same unit 5 consecutive time
1078
1078
 
1079
1079
  const loopPromise = autoLoop(ctx, pi, s, deps);
1080
1080
 
1081
- // The loop will dispatch the same unit each iteration. On iteration 1, sameUnitCount
1082
- // starts at 0 and the unit key is set. On iterations 2-5, sameUnitCount increments.
1083
- // At sameUnitCount=5 (iteration 6), stopAuto is called.
1084
- // Each iteration requires resolving an agent_end event.
1085
- // But the stuck counter fires BEFORE runUnit, so we only need to resolve 4 times
1086
- // (iterations 1-4 each run a unit, iteration 5 increments to 5 and stops).
1081
+ // Sliding window: iteration 1 pushes [A], iteration 2 pushes [A,A],
1082
+ // iteration 3 pushes [A,A,A] Rule 2 fires (3 consecutive) Level 1 recovery.
1083
+ // Level 1 invalidates caches and continues. Iteration 4 pushes [A,A,A,A] →
1084
+ // Rule 2 fires again Level 2 hard stop.
1085
+ // Iterations 1-3 each run a unit (3 resolves needed). Iteration 3 triggers
1086
+ // Level 1 (cache invalidation + continue). Iteration 4 triggers Level 2 (stop
1087
+ // before runUnit), so no 4th resolve needed.
1087
1088
 
1088
- // Actually: iteration 1 sets lastDerivedUnit (sameUnitCount=0).
1089
- // Iteration 2: derivedKey === lastDerivedUnit → sameUnitCount=1.
1090
- // Iteration 3: sameUnitCount=2. Iteration 4: sameUnitCount=3.
1091
- // Iteration 5: sameUnitCount=4. Iteration 6: sameUnitCount=5 → stop.
1092
- // So we need to resolve 5 agent_end events (iterations 1-5 each run a unit).
1093
-
1094
- for (let i = 0; i < 5; i++) {
1089
+ for (let i = 0; i < 3; i++) {
1095
1090
  await new Promise((r) => setTimeout(r, 30));
1096
1091
  resolveAgentEnd(makeEvent());
1097
1092
  }
@@ -1106,17 +1101,13 @@ test("stuck counter: stops when deriveState returns same unit 5 consecutive time
1106
1101
  stopReason.includes("Stuck"),
1107
1102
  `stop reason should mention 'Stuck', got: ${stopReason}`,
1108
1103
  );
1109
- assert.ok(
1110
- stopReason.includes("execute-task"),
1111
- "stop reason should include unitType",
1112
- );
1113
1104
  assert.ok(
1114
1105
  stopReason.includes("M001/S01/T01"),
1115
1106
  "stop reason should include unitId",
1116
1107
  );
1117
1108
  });
1118
1109
 
1119
- test("stuck counter: resets when deriveState returns a different unit", async () => {
1110
+ test("stuck detection: window resets recovery when deriveState returns a different unit", async () => {
1120
1111
  _resetPendingResolve();
1121
1112
 
1122
1113
  const ctx = makeMockCtx();
@@ -1177,10 +1168,11 @@ test("stuck counter: resets when deriveState returns a different unit", async ()
1177
1168
 
1178
1169
  await loopPromise;
1179
1170
 
1180
- // The counter should have reset when T02 was derived no stuck stop
1171
+ // Level 1 recovery fires on iteration 3 (cache invalidation + continue),
1172
+ // then iteration 4 derives T02 — no Level 2 hard stop.
1181
1173
  assert.ok(
1182
1174
  !stopCalled,
1183
- "stopAuto should NOT have been called — counter reset on unit change",
1175
+ "stopAuto should NOT have been called — different unit broke stuck pattern",
1184
1176
  );
1185
1177
  assert.ok(
1186
1178
  deriveCallCount >= 4,
@@ -1188,7 +1180,7 @@ test("stuck counter: resets when deriveState returns a different unit", async ()
1188
1180
  );
1189
1181
  });
1190
1182
 
1191
- test("stuck counter: does not increment during verification retry", async () => {
1183
+ test("stuck detection: does not push to window during verification retry", async () => {
1192
1184
  _resetPendingResolve();
1193
1185
 
1194
1186
  const ctx = makeMockCtx();
@@ -1250,10 +1242,10 @@ test("stuck counter: does not increment during verification retry", async () =>
1250
1242
  await loopPromise;
1251
1243
 
1252
1244
  // Even though same unit was derived 4 times, verification retries should
1253
- // not count, so stuck counter should not have fired
1245
+ // not push to the sliding window, so stuck detection should not have fired
1254
1246
  assert.ok(
1255
1247
  !stopReason.includes("Stuck"),
1256
- `stuck counter should not fire during verification retries, got: ${stopReason}`,
1248
+ `stuck detection should not fire during verification retries, got: ${stopReason}`,
1257
1249
  );
1258
1250
  assert.equal(
1259
1251
  verifyCallCount,
@@ -1262,24 +1254,106 @@ test("stuck counter: does not increment during verification retry", async () =>
1262
1254
  );
1263
1255
  });
1264
1256
 
1265
- test("stuck counter: logs debug output with stuck-detected phase", () => {
1266
- // Structural test: verify the auto-loop.ts source contains both
1267
- // stuck-detected and stuck-counter-reset debug log phases
1257
+ // ── detectStuck unit tests ────────────────────────────────────────────────────
1258
+
1259
+ test("detectStuck: returns null for fewer than 2 entries", () => {
1260
+ assert.equal(detectStuck([]), null);
1261
+ assert.equal(detectStuck([{ key: "A" }]), null);
1262
+ });
1263
+
1264
+ test("detectStuck: Rule 1 — same error twice in a row", () => {
1265
+ const result = detectStuck([
1266
+ { key: "A", error: "ENOENT: file not found" },
1267
+ { key: "A", error: "ENOENT: file not found" },
1268
+ ]);
1269
+ assert.ok(result?.stuck, "should detect same error repeated");
1270
+ assert.ok(result?.reason.includes("Same error repeated"));
1271
+ });
1272
+
1273
+ test("detectStuck: Rule 1 — different errors do not trigger", () => {
1274
+ const result = detectStuck([
1275
+ { key: "A", error: "ENOENT: file not found" },
1276
+ { key: "A", error: "EACCES: permission denied" },
1277
+ ]);
1278
+ assert.equal(result, null);
1279
+ });
1280
+
1281
+ test("detectStuck: Rule 2 — same unit 3 consecutive times", () => {
1282
+ const result = detectStuck([
1283
+ { key: "execute-task/M001/S01/T01" },
1284
+ { key: "execute-task/M001/S01/T01" },
1285
+ { key: "execute-task/M001/S01/T01" },
1286
+ ]);
1287
+ assert.ok(result?.stuck);
1288
+ assert.ok(result?.reason.includes("3 consecutive times"));
1289
+ });
1290
+
1291
+ test("detectStuck: Rule 2 — 2 consecutive does not trigger", () => {
1292
+ assert.equal(detectStuck([
1293
+ { key: "A" },
1294
+ { key: "A" },
1295
+ ]), null);
1296
+ });
1297
+
1298
+ test("detectStuck: Rule 3 — oscillation A→B→A→B", () => {
1299
+ const result = detectStuck([
1300
+ { key: "A" },
1301
+ { key: "B" },
1302
+ { key: "A" },
1303
+ { key: "B" },
1304
+ ]);
1305
+ assert.ok(result?.stuck);
1306
+ assert.ok(result?.reason.includes("Oscillation"));
1307
+ });
1308
+
1309
+ test("detectStuck: Rule 3 — non-oscillation pattern A→B→C→B", () => {
1310
+ assert.equal(detectStuck([
1311
+ { key: "A" },
1312
+ { key: "B" },
1313
+ { key: "C" },
1314
+ { key: "B" },
1315
+ ]), null);
1316
+ });
1317
+
1318
+ test("detectStuck: Rule 1 takes priority over Rule 2 when both match", () => {
1319
+ const result = detectStuck([
1320
+ { key: "A", error: "test error" },
1321
+ { key: "A", error: "test error" },
1322
+ { key: "A", error: "test error" },
1323
+ ]);
1324
+ assert.ok(result?.stuck);
1325
+ // Rule 1 fires first
1326
+ assert.ok(result?.reason.includes("Same error repeated"));
1327
+ });
1328
+
1329
+ test("detectStuck: truncates long error strings", () => {
1330
+ const longError = "x".repeat(500);
1331
+ const result = detectStuck([
1332
+ { key: "A", error: longError },
1333
+ { key: "A", error: longError },
1334
+ ]);
1335
+ assert.ok(result?.stuck);
1336
+ assert.ok(result!.reason.length < 300, "reason should be truncated");
1337
+ });
1338
+
1339
+ test("stuck detection: logs debug output with stuck-detected phase", () => {
1340
+ // Structural test: verify the auto-loop.ts source contains
1341
+ // stuck-detected and stuck-counter-reset debug log phases, plus detectStuck
1268
1342
  const src = readFileSync(
1269
1343
  resolve(import.meta.dirname, "..", "auto-loop.ts"),
1270
1344
  "utf-8",
1271
1345
  );
1272
1346
  assert.ok(
1273
1347
  src.includes('"stuck-detected"'),
1274
- "auto-loop.ts must log phase: 'stuck-detected' when stuck counter fires",
1348
+ "auto-loop.ts must log phase: 'stuck-detected' when stuck detection fires",
1275
1349
  );
1276
1350
  assert.ok(
1277
1351
  src.includes('"stuck-counter-reset"'),
1278
- "auto-loop.ts must log phase: 'stuck-counter-reset' when counter resets on new unit",
1352
+ "auto-loop.ts must log phase: 'stuck-counter-reset' when recovery resets on new unit",
1279
1353
  );
1280
1354
  assert.ok(
1281
- src.includes("sameUnitCount"),
1282
- "auto-loop.ts must track sameUnitCount for stuck detection",
1355
+ src.includes("detectStuck"),
1356
+ "auto-loop.ts must use detectStuck for sliding window analysis",
1283
1357
  );
1284
1358
  });
1285
1359
 
@@ -478,3 +478,11 @@ export interface ReactiveExecutionState {
478
478
  };
479
479
  updatedAt: string;
480
480
  }
481
+
482
+ export interface BrowserFlowResult {
483
+ url: string;
484
+ passed: boolean;
485
+ checksTotal: number;
486
+ checksPassed: number;
487
+ duration: number;
488
+ }
@@ -37,6 +37,21 @@ export interface AuditWarningJSON {
37
37
  fixAvailable: boolean;
38
38
  }
39
39
 
40
+ export interface BrowserEvidenceCheckJSON {
41
+ description: string;
42
+ passed: boolean;
43
+ actual?: string;
44
+ evidence?: string;
45
+ error?: string;
46
+ }
47
+
48
+ export interface BrowserEvidenceJSON {
49
+ url: string;
50
+ passed: boolean;
51
+ checks: BrowserEvidenceCheckJSON[];
52
+ duration: number;
53
+ }
54
+
40
55
  export interface EvidenceJSON {
41
56
  schemaVersion: 1;
42
57
  taskId: string;
@@ -49,6 +64,7 @@ export interface EvidenceJSON {
49
64
  maxRetries?: number;
50
65
  runtimeErrors?: RuntimeErrorJSON[];
51
66
  auditWarnings?: AuditWarningJSON[];
67
+ browser?: BrowserEvidenceJSON;
52
68
  }
53
69
 
54
70
  /**
@@ -114,6 +114,22 @@ function getServerConfig(name: string): McpServerConfig | undefined {
114
114
  return readConfigs().find((s) => s.name === name);
115
115
  }
116
116
 
117
+ /** Resolve ${VAR} references in env values against process.env. */
118
+ function resolveEnv(env: Record<string, string>): Record<string, string> {
119
+ const resolved: Record<string, string> = {};
120
+ for (const [key, value] of Object.entries(env)) {
121
+ if (typeof value === "string") {
122
+ resolved[key] = value.replace(
123
+ /\$\{([^}]+)\}/g,
124
+ (_match, varName) => process.env[varName] ?? "",
125
+ );
126
+ } else {
127
+ resolved[key] = value;
128
+ }
129
+ }
130
+ return resolved;
131
+ }
132
+
117
133
  async function getOrConnect(name: string, signal?: AbortSignal): Promise<Client> {
118
134
  const existing = connections.get(name);
119
135
  if (existing) return existing.client;
@@ -128,7 +144,7 @@ async function getOrConnect(name: string, signal?: AbortSignal): Promise<Client>
128
144
  transport = new StdioClientTransport({
129
145
  command: config.command,
130
146
  args: config.args,
131
- env: config.env ? { ...process.env, ...config.env } as Record<string, string> : undefined,
147
+ env: config.env ? { ...process.env, ...resolveEnv(config.env) } as Record<string, string> : undefined,
132
148
  cwd: config.cwd,
133
149
  stderr: "pipe",
134
150
  });