gsd-pi 2.41.0-dev.cac69f9 → 2.42.0-dev.1df898f

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 (263) hide show
  1. package/README.md +23 -0
  2. package/dist/cli.js +18 -3
  3. package/dist/loader.js +3 -1
  4. package/dist/resource-loader.js +39 -6
  5. package/dist/resources/extensions/async-jobs/async-bash-tool.js +52 -4
  6. package/dist/resources/extensions/async-jobs/await-tool.js +5 -0
  7. package/dist/resources/extensions/async-jobs/index.js +2 -0
  8. package/dist/resources/extensions/gsd/auto/loop.js +80 -0
  9. package/dist/resources/extensions/gsd/auto/phases.js +3 -5
  10. package/dist/resources/extensions/gsd/auto/session.js +6 -0
  11. package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
  12. package/dist/resources/extensions/gsd/auto-prompts.js +3 -16
  13. package/dist/resources/extensions/gsd/auto-start.js +8 -11
  14. package/dist/resources/extensions/gsd/auto.js +28 -1
  15. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +11 -5
  16. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
  17. package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
  18. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
  19. package/dist/resources/extensions/gsd/context-injector.js +74 -0
  20. package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
  21. package/dist/resources/extensions/gsd/custom-verification.js +145 -0
  22. package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
  23. package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
  24. package/dist/resources/extensions/gsd/definition-loader.js +352 -0
  25. package/dist/resources/extensions/gsd/detection.js +19 -0
  26. package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
  27. package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
  28. package/dist/resources/extensions/gsd/doctor-checks.js +31 -1
  29. package/dist/resources/extensions/gsd/doctor-providers.js +10 -0
  30. package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
  31. package/dist/resources/extensions/gsd/engine-types.js +8 -0
  32. package/dist/resources/extensions/gsd/execution-policy.js +8 -0
  33. package/dist/resources/extensions/gsd/forensics.js +84 -0
  34. package/dist/resources/extensions/gsd/git-constants.js +1 -0
  35. package/dist/resources/extensions/gsd/git-service.js +1 -1
  36. package/dist/resources/extensions/gsd/graph.js +225 -0
  37. package/dist/resources/extensions/gsd/native-git-bridge.js +1 -0
  38. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  39. package/dist/resources/extensions/gsd/preferences.js +59 -8
  40. package/dist/resources/extensions/gsd/prompts/forensics.md +12 -5
  41. package/dist/resources/extensions/gsd/repo-identity.js +46 -5
  42. package/dist/resources/extensions/gsd/run-manager.js +134 -0
  43. package/dist/resources/extensions/gsd/service-tier.js +13 -4
  44. package/dist/resources/extensions/gsd/session-lock.js +2 -2
  45. package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
  46. package/dist/resources/extensions/gsd/worktree-resolver.js +2 -2
  47. package/dist/resources/extensions/gsd/worktree.js +2 -2
  48. package/dist/resources/extensions/mcp-client/index.js +2 -1
  49. package/dist/resources/extensions/search-the-web/tool-search.js +3 -3
  50. package/dist/resources/skills/create-workflow/SKILL.md +103 -0
  51. package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  52. package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
  53. package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  54. package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  55. package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  56. package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  57. package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  58. package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  59. package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  60. package/dist/web/standalone/.next/BUILD_ID +1 -1
  61. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  62. package/dist/web/standalone/.next/build-manifest.json +2 -2
  63. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  64. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  65. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/index.html +1 -1
  82. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  89. package/dist/web/standalone/.next/server/chunks/229.js +2 -2
  90. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  91. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  92. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  93. package/dist/web-mode.d.ts +2 -0
  94. package/dist/web-mode.js +40 -4
  95. package/package.json +1 -1
  96. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  97. package/packages/pi-agent-core/dist/agent.js +2 -0
  98. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  99. package/packages/pi-agent-core/dist/types.d.ts +6 -0
  100. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  101. package/packages/pi-agent-core/dist/types.js.map +1 -1
  102. package/packages/pi-agent-core/src/agent.test.ts +53 -0
  103. package/packages/pi-agent-core/src/agent.ts +3 -0
  104. package/packages/pi-agent-core/src/types.ts +6 -0
  105. package/packages/pi-agent-core/tsconfig.json +1 -1
  106. package/packages/pi-ai/dist/models.d.ts +5 -3
  107. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/models.generated.d.ts +801 -1468
  109. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  110. package/packages/pi-ai/dist/models.generated.js +1135 -1588
  111. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  112. package/packages/pi-ai/dist/models.js.map +1 -1
  113. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  114. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +60 -2
  115. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  116. package/packages/pi-ai/scripts/generate-models.ts +1543 -0
  117. package/packages/pi-ai/src/models.generated.ts +1140 -1593
  118. package/packages/pi-ai/src/models.ts +7 -4
  119. package/packages/pi-ai/src/utils/oauth/github-copilot.ts +74 -2
  120. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -1
  122. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +7 -0
  124. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/auth-storage.js +29 -2
  126. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +60 -0
  128. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/extensions/loader.js +18 -0
  131. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/lsp/client.js +23 -0
  134. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/model-registry.js +2 -0
  137. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/core/package-manager.d.ts +6 -0
  139. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  140. package/packages/pi-coding-agent/dist/core/package-manager.js +63 -11
  141. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  142. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +9 -0
  143. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  144. package/packages/pi-coding-agent/dist/core/resource-loader.js +20 -6
  145. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  146. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -5
  148. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  150. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js +3 -0
  151. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js.map +1 -1
  152. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  153. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +9 -6
  154. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  155. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  156. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +30 -10
  157. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  158. package/packages/pi-coding-agent/package.json +1 -1
  159. package/packages/pi-coding-agent/src/core/agent-session.ts +7 -1
  160. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +68 -0
  161. package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -2
  162. package/packages/pi-coding-agent/src/core/extensions/loader.ts +18 -0
  163. package/packages/pi-coding-agent/src/core/lsp/client.ts +29 -0
  164. package/packages/pi-coding-agent/src/core/model-registry.ts +3 -0
  165. package/packages/pi-coding-agent/src/core/package-manager.ts +99 -58
  166. package/packages/pi-coding-agent/src/core/resource-loader.ts +24 -6
  167. package/packages/pi-coding-agent/src/core/system-prompt.ts +6 -5
  168. package/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts +3 -0
  169. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +10 -6
  170. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +31 -11
  171. package/pkg/package.json +1 -1
  172. package/src/resources/extensions/async-jobs/async-bash-timeout.test.ts +122 -0
  173. package/src/resources/extensions/async-jobs/async-bash-tool.ts +40 -4
  174. package/src/resources/extensions/async-jobs/await-tool.test.ts +47 -0
  175. package/src/resources/extensions/async-jobs/await-tool.ts +5 -0
  176. package/src/resources/extensions/async-jobs/index.ts +1 -0
  177. package/src/resources/extensions/async-jobs/job-manager.ts +2 -0
  178. package/src/resources/extensions/gsd/auto/loop-deps.ts +0 -1
  179. package/src/resources/extensions/gsd/auto/loop.ts +91 -0
  180. package/src/resources/extensions/gsd/auto/phases.ts +3 -5
  181. package/src/resources/extensions/gsd/auto/session.ts +6 -0
  182. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  183. package/src/resources/extensions/gsd/auto-prompts.ts +2 -18
  184. package/src/resources/extensions/gsd/auto-start.ts +7 -10
  185. package/src/resources/extensions/gsd/auto.ts +31 -1
  186. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -5
  187. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
  188. package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
  189. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
  190. package/src/resources/extensions/gsd/context-injector.ts +100 -0
  191. package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
  192. package/src/resources/extensions/gsd/custom-verification.ts +180 -0
  193. package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
  194. package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
  195. package/src/resources/extensions/gsd/definition-loader.ts +462 -0
  196. package/src/resources/extensions/gsd/detection.ts +19 -0
  197. package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
  198. package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
  199. package/src/resources/extensions/gsd/doctor-checks.ts +32 -1
  200. package/src/resources/extensions/gsd/doctor-providers.ts +13 -0
  201. package/src/resources/extensions/gsd/doctor-types.ts +1 -0
  202. package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
  203. package/src/resources/extensions/gsd/engine-types.ts +71 -0
  204. package/src/resources/extensions/gsd/execution-policy.ts +43 -0
  205. package/src/resources/extensions/gsd/forensics.ts +92 -0
  206. package/src/resources/extensions/gsd/git-constants.ts +1 -0
  207. package/src/resources/extensions/gsd/git-service.ts +0 -1
  208. package/src/resources/extensions/gsd/gitignore.ts +1 -1
  209. package/src/resources/extensions/gsd/graph.ts +312 -0
  210. package/src/resources/extensions/gsd/native-git-bridge.ts +1 -0
  211. package/src/resources/extensions/gsd/preferences-types.ts +3 -0
  212. package/src/resources/extensions/gsd/preferences.ts +62 -6
  213. package/src/resources/extensions/gsd/prompts/forensics.md +12 -5
  214. package/src/resources/extensions/gsd/repo-identity.ts +48 -5
  215. package/src/resources/extensions/gsd/run-manager.ts +180 -0
  216. package/src/resources/extensions/gsd/service-tier.ts +17 -4
  217. package/src/resources/extensions/gsd/session-lock.ts +2 -2
  218. package/src/resources/extensions/gsd/tests/activity-log.test.ts +31 -69
  219. package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
  220. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
  221. package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
  222. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
  223. package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
  224. package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
  225. package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
  226. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
  227. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
  228. package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
  229. package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
  230. package/src/resources/extensions/gsd/tests/forensics-dedup.test.ts +48 -0
  231. package/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts +43 -0
  232. package/src/resources/extensions/gsd/tests/git-locale.test.ts +133 -0
  233. package/src/resources/extensions/gsd/tests/git-service.test.ts +44 -0
  234. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
  235. package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
  236. package/src/resources/extensions/gsd/tests/journal.test.ts +82 -127
  237. package/src/resources/extensions/gsd/tests/manifest-status.test.ts +73 -82
  238. package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
  239. package/src/resources/extensions/gsd/tests/service-tier.test.ts +30 -1
  240. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +56 -3
  241. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +151 -0
  242. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
  243. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +156 -263
  244. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +35 -78
  245. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +81 -74
  246. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +1 -2
  247. package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
  248. package/src/resources/extensions/gsd/worktree-resolver.ts +2 -3
  249. package/src/resources/extensions/gsd/worktree.ts +2 -2
  250. package/src/resources/extensions/mcp-client/index.ts +5 -1
  251. package/src/resources/extensions/search-the-web/tool-search.ts +3 -3
  252. package/src/resources/skills/create-workflow/SKILL.md +103 -0
  253. package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  254. package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
  255. package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  256. package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  257. package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  258. package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  259. package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  260. package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  261. package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  262. /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → qw8qDHXOTLUXBq1vEknSz}/_buildManifest.js +0 -0
  263. /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → qw8qDHXOTLUXBq1vEknSz}/_ssgManifest.js +0 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * run-manager.test.ts — Tests for run directory creation and listing.
3
+ *
4
+ * Uses real temp directories with actual definition YAML files and
5
+ * GRAPH.yaml persistence — no mocks.
6
+ */
7
+
8
+ import { describe, it, afterEach } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import {
11
+ mkdtempSync,
12
+ rmSync,
13
+ mkdirSync,
14
+ writeFileSync,
15
+ readFileSync,
16
+ existsSync,
17
+ readdirSync,
18
+ } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { tmpdir } from "node:os";
21
+ import { parse } from "yaml";
22
+
23
+ import { createRun, listRuns } from "../run-manager.ts";
24
+
25
+ // ─── Helpers ─────────────────────────────────────────────────────────────
26
+
27
+ const tmpDirs: string[] = [];
28
+
29
+ function makeTmpBase(): string {
30
+ const dir = mkdtempSync(join(tmpdir(), "run-mgr-test-"));
31
+ tmpDirs.push(dir);
32
+ return dir;
33
+ }
34
+
35
+ afterEach(() => {
36
+ for (const d of tmpDirs) {
37
+ try { rmSync(d, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM */ }
38
+ }
39
+ tmpDirs.length = 0;
40
+ });
41
+
42
+ /** Write a minimal valid workflow definition YAML to the expected location. */
43
+ function writeDefinition(
44
+ basePath: string,
45
+ name: string,
46
+ content: string,
47
+ ): void {
48
+ const defsDir = join(basePath, ".gsd", "workflow-defs");
49
+ mkdirSync(defsDir, { recursive: true });
50
+ writeFileSync(join(defsDir, `${name}.yaml`), content, "utf-8");
51
+ }
52
+
53
+ const SIMPLE_DEF = `
54
+ version: 1
55
+ name: test-workflow
56
+ description: A test workflow
57
+ steps:
58
+ - id: step-1
59
+ name: First Step
60
+ prompt: Do step 1
61
+ requires: []
62
+ produces: []
63
+ - id: step-2
64
+ name: Second Step
65
+ prompt: Do step 2
66
+ requires:
67
+ - step-1
68
+ produces: []
69
+ `;
70
+
71
+ const PARAMETERIZED_DEF = `
72
+ version: 1
73
+ name: param-workflow
74
+ description: A parameterized workflow
75
+ params:
76
+ target: default-target
77
+ steps:
78
+ - id: step-1
79
+ name: Build
80
+ prompt: "Build {{target}}"
81
+ requires: []
82
+ produces: []
83
+ `;
84
+
85
+ // ─── createRun ───────────────────────────────────────────────────────────
86
+
87
+ describe("createRun", () => {
88
+ it("creates directory structure with DEFINITION.yaml and GRAPH.yaml", () => {
89
+ const base = makeTmpBase();
90
+ writeDefinition(base, "test-workflow", SIMPLE_DEF);
91
+
92
+ const runDir = createRun(base, "test-workflow");
93
+
94
+ // Run directory exists
95
+ assert.ok(existsSync(runDir), "run directory should exist");
96
+
97
+ // DEFINITION.yaml exists and contains the definition
98
+ const defPath = join(runDir, "DEFINITION.yaml");
99
+ assert.ok(existsSync(defPath), "DEFINITION.yaml should exist");
100
+ const defContent = parse(readFileSync(defPath, "utf-8"));
101
+ assert.equal(defContent.name, "test-workflow");
102
+ assert.equal(defContent.steps.length, 2);
103
+
104
+ // GRAPH.yaml exists with all steps pending
105
+ const graphPath = join(runDir, "GRAPH.yaml");
106
+ assert.ok(existsSync(graphPath), "GRAPH.yaml should exist");
107
+ const graphContent = parse(readFileSync(graphPath, "utf-8"));
108
+ assert.equal(graphContent.steps.length, 2);
109
+ assert.equal(graphContent.steps[0].status, "pending");
110
+ assert.equal(graphContent.steps[1].status, "pending");
111
+ assert.equal(graphContent.metadata.name, "test-workflow");
112
+
113
+ // No PARAMS.json without overrides
114
+ assert.ok(!existsSync(join(runDir, "PARAMS.json")), "PARAMS.json should not exist without overrides");
115
+
116
+ // Run directory path matches convention
117
+ assert.ok(runDir.includes(join(".gsd", "workflow-runs", "test-workflow")), "path should follow convention");
118
+ });
119
+
120
+ it("writes PARAMS.json and substituted prompts when overrides provided", () => {
121
+ const base = makeTmpBase();
122
+ writeDefinition(base, "param-workflow", PARAMETERIZED_DEF);
123
+
124
+ const runDir = createRun(base, "param-workflow", { target: "my-app" });
125
+
126
+ // PARAMS.json exists with overrides
127
+ const paramsPath = join(runDir, "PARAMS.json");
128
+ assert.ok(existsSync(paramsPath), "PARAMS.json should exist");
129
+ const params = JSON.parse(readFileSync(paramsPath, "utf-8"));
130
+ assert.deepStrictEqual(params, { target: "my-app" });
131
+
132
+ // DEFINITION.yaml has substituted prompts
133
+ const defPath = join(runDir, "DEFINITION.yaml");
134
+ const defContent = parse(readFileSync(defPath, "utf-8"));
135
+ assert.equal(defContent.steps[0].prompt, "Build my-app");
136
+
137
+ // GRAPH.yaml also has substituted prompts
138
+ const graphPath = join(runDir, "GRAPH.yaml");
139
+ const graphContent = parse(readFileSync(graphPath, "utf-8"));
140
+ assert.equal(graphContent.steps[0].prompt, "Build my-app");
141
+ });
142
+
143
+ it("throws for unknown definition", () => {
144
+ const base = makeTmpBase();
145
+ // Don't write any definition file
146
+
147
+ assert.throws(
148
+ () => createRun(base, "nonexistent"),
149
+ (err: Error) => err.message.includes("not found"),
150
+ );
151
+ });
152
+
153
+ it("uses filesystem-safe timestamp directory names", () => {
154
+ const base = makeTmpBase();
155
+ writeDefinition(base, "test-workflow", SIMPLE_DEF);
156
+
157
+ const runDir = createRun(base, "test-workflow");
158
+
159
+ // Extract the timestamp directory name (use path.sep for cross-platform)
160
+ const timestamp = runDir.split(/[/\\]/).pop()!;
161
+
162
+ // Should not contain colons (filesystem-unsafe on Windows)
163
+ assert.ok(!timestamp.includes(":"), `timestamp should not contain colons: ${timestamp}`);
164
+ // Should match YYYY-MM-DDTHH-MM-SS pattern
165
+ assert.match(timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/);
166
+ });
167
+ });
168
+
169
+ // ─── listRuns ────────────────────────────────────────────────────────────
170
+
171
+ describe("listRuns", () => {
172
+ it("returns empty array when no runs exist", () => {
173
+ const base = makeTmpBase();
174
+ const runs = listRuns(base);
175
+ assert.deepStrictEqual(runs, []);
176
+ });
177
+
178
+ it("returns correct metadata for existing runs", () => {
179
+ const base = makeTmpBase();
180
+ writeDefinition(base, "test-workflow", SIMPLE_DEF);
181
+
182
+ // Create a run
183
+ const runDir = createRun(base, "test-workflow");
184
+
185
+ const runs = listRuns(base);
186
+ assert.equal(runs.length, 1);
187
+ assert.equal(runs[0].name, "test-workflow");
188
+ assert.equal(runs[0].runDir, runDir);
189
+ assert.equal(runs[0].steps.total, 2);
190
+ assert.equal(runs[0].steps.completed, 0);
191
+ assert.equal(runs[0].steps.pending, 2);
192
+ assert.equal(runs[0].steps.active, 0);
193
+ assert.equal(runs[0].status, "pending");
194
+ });
195
+
196
+ it("filters by definition name", () => {
197
+ const base = makeTmpBase();
198
+ writeDefinition(base, "test-workflow", SIMPLE_DEF);
199
+ writeDefinition(base, "param-workflow", PARAMETERIZED_DEF);
200
+
201
+ createRun(base, "test-workflow");
202
+ createRun(base, "param-workflow", { target: "app" });
203
+
204
+ const allRuns = listRuns(base);
205
+ assert.equal(allRuns.length, 2);
206
+
207
+ const filtered = listRuns(base, "test-workflow");
208
+ assert.equal(filtered.length, 1);
209
+ assert.equal(filtered[0].name, "test-workflow");
210
+ });
211
+
212
+ it("returns newest-first within same definition", () => {
213
+ const base = makeTmpBase();
214
+ writeDefinition(base, "test-workflow", SIMPLE_DEF);
215
+
216
+ const run1 = createRun(base, "test-workflow");
217
+ // Ensure different timestamp by creating run dir manually with earlier timestamp
218
+ const earlyDir = join(base, ".gsd", "workflow-runs", "test-workflow", "2020-01-01T00-00-00");
219
+ mkdirSync(earlyDir, { recursive: true });
220
+ // Copy GRAPH.yaml to make it a valid run
221
+ const graphContent = readFileSync(join(run1, "GRAPH.yaml"), "utf-8");
222
+ writeFileSync(join(earlyDir, "GRAPH.yaml"), graphContent, "utf-8");
223
+
224
+ const runs = listRuns(base, "test-workflow");
225
+ assert.equal(runs.length, 2);
226
+ // First should be the newer one (the one we just created)
227
+ assert.ok(runs[0].timestamp > runs[1].timestamp, "should be sorted newest-first");
228
+ });
229
+ });
@@ -4,8 +4,8 @@ import assert from "node:assert/strict";
4
4
  import {
5
5
  supportsServiceTier,
6
6
  formatServiceTierStatus,
7
+ formatServiceTierFooterStatus,
7
8
  resolveServiceTierIcon,
8
- type ServiceTierSetting,
9
9
  } from "../service-tier.ts";
10
10
 
11
11
  // ─── supportsServiceTier ─────────────────────────────────────────────────────
@@ -27,6 +27,14 @@ describe("supportsServiceTier", () => {
27
27
  assert.equal(supportsServiceTier("openai/gpt-5.4"), true);
28
28
  });
29
29
 
30
+ test("returns true for vibeproxy-openai/gpt-5.4 (proxy provider-prefixed)", () => {
31
+ assert.equal(supportsServiceTier("vibeproxy-openai/gpt-5.4"), true);
32
+ });
33
+
34
+ test("returns false for provider-only identifier without gpt-5.4 model suffix", () => {
35
+ assert.equal(supportsServiceTier("vibeproxy-openai"), false);
36
+ });
37
+
30
38
  test("returns false for claude-opus-4-6", () => {
31
39
  assert.equal(supportsServiceTier("claude-opus-4-6"), false);
32
40
  });
@@ -52,6 +60,11 @@ describe("formatServiceTierStatus", () => {
52
60
  assert.ok(output.includes("disabled"), `Expected 'disabled' in: ${output}`);
53
61
  });
54
62
 
63
+ test("mentions provider-agnostic model gating", () => {
64
+ const output = formatServiceTierStatus("priority");
65
+ assert.ok(output.includes("regardless of provider"), `Expected provider note in: ${output}`);
66
+ });
67
+
55
68
  test("shows priority when set to priority", () => {
56
69
  const output = formatServiceTierStatus("priority");
57
70
  assert.ok(output.includes("priority"), `Expected 'priority' in: ${output}`);
@@ -63,6 +76,22 @@ describe("formatServiceTierStatus", () => {
63
76
  });
64
77
  });
65
78
 
79
+ // ─── formatServiceTierFooterStatus ───────────────────────────────────────────
80
+
81
+ describe("formatServiceTierFooterStatus", () => {
82
+ test("returns priority footer status for supported model", () => {
83
+ assert.equal(formatServiceTierFooterStatus("priority", "vibeproxy-openai/gpt-5.4"), "fast: ⚡ priority");
84
+ });
85
+
86
+ test("returns undefined for unsupported model", () => {
87
+ assert.equal(formatServiceTierFooterStatus("priority", "claude-opus-4-6"), undefined);
88
+ });
89
+
90
+ test("returns undefined when tier is disabled", () => {
91
+ assert.equal(formatServiceTierFooterStatus(undefined, "gpt-5.4"), undefined);
92
+ });
93
+ });
94
+
66
95
  // ─── resolveServiceTierIcon ──────────────────────────────────────────────────
67
96
 
68
97
  describe("resolveServiceTierIcon", () => {
@@ -39,7 +39,7 @@ function buildBlock(
39
39
  });
40
40
  }
41
41
 
42
- test("buildSkillActivationBlock matches installed skills from task context", () => {
42
+ test("buildSkillActivationBlock does not auto-activate skills via broad context heuristic", () => {
43
43
  const base = makeTempBase();
44
44
  try {
45
45
  writeSkill(base, "react", "Use for React components, hooks, JSX, and frontend UI work.");
@@ -52,7 +52,29 @@ test("buildSkillActivationBlock matches installed skills from task context", ()
52
52
  taskTitle: "Implement React settings panel",
53
53
  });
54
54
 
55
- assert.match(result, /<skill_activation>/);
55
+ // Skills should not be activated just because their name appears in task context.
56
+ // Activation requires explicit preference sources (always_use, skill_rules, prefer_skills, skills_used).
57
+ assert.equal(result, "");
58
+ } finally {
59
+ cleanup(base);
60
+ }
61
+ });
62
+
63
+ test("buildSkillActivationBlock activates skills via prefer_skills when context matches", () => {
64
+ const base = makeTempBase();
65
+ try {
66
+ writeSkill(base, "react", "Use for React components, hooks, JSX, and frontend UI work.");
67
+ writeSkill(base, "swiftui", "Use for SwiftUI views, iOS layout, and Apple platform UI work.");
68
+ loadOnlyTestSkills(base);
69
+
70
+ const result = buildBlock(base, {
71
+ sliceTitle: "Build React dashboard",
72
+ taskId: "T01",
73
+ taskTitle: "Implement React settings panel",
74
+ }, {
75
+ prefer_skills: ["react"],
76
+ });
77
+
56
78
  assert.match(result, /Call Skill\('react'\)/);
57
79
  assert.doesNotMatch(result, /swiftui/);
58
80
  } finally {
@@ -105,7 +127,7 @@ test("buildSkillActivationBlock includes skill_rules matches and task-plan skill
105
127
  }
106
128
  });
107
129
 
108
- test("buildSkillActivationBlock honors avoid_skills", () => {
130
+ test("buildSkillActivationBlock honors avoid_skills against always_use_skills", () => {
109
131
  const base = makeTempBase();
110
132
  try {
111
133
  writeSkill(base, "react", "Use for React components and frontend UI work.");
@@ -114,6 +136,7 @@ test("buildSkillActivationBlock honors avoid_skills", () => {
114
136
  const result = buildBlock(base, {
115
137
  taskTitle: "Implement React settings panel",
116
138
  }, {
139
+ always_use_skills: ["react"],
117
140
  avoid_skills: ["react"],
118
141
  });
119
142
 
@@ -138,3 +161,33 @@ test("buildSkillActivationBlock falls back cleanly when nothing matches", () =>
138
161
  cleanup(base);
139
162
  }
140
163
  });
164
+
165
+ test("buildSkillActivationBlock does not activate skills from extraContext or taskPlanContent body", () => {
166
+ const base = makeTempBase();
167
+ try {
168
+ writeSkill(base, "xcode-build", "Use for Xcode build workflows and iOS compilation.");
169
+ writeSkill(base, "ableton-lom", "Use for Ableton Live Object Model scripting.");
170
+ writeSkill(base, "frontend-design", "Use for frontend design systems and UI components.");
171
+ loadOnlyTestSkills(base);
172
+
173
+ const taskPlan = [
174
+ "---",
175
+ "skills_used: []",
176
+ "---",
177
+ "# T01: Build the API endpoint",
178
+ "Use xcode-build patterns and frontend-design tokens.",
179
+ ].join("\n");
180
+
181
+ const result = buildBlock(base, {
182
+ taskTitle: "Build REST API",
183
+ extraContext: ["Build workflow for iOS and Ableton integration testing"],
184
+ taskPlanContent: taskPlan,
185
+ });
186
+
187
+ // None of these skills should activate — extraContext and taskPlanContent body
188
+ // must not be used for heuristic matching.
189
+ assert.equal(result, "");
190
+ } finally {
191
+ cleanup(base);
192
+ }
193
+ });
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Tests for macOS numbered symlink variant cleanup (#2205).
3
+ *
4
+ * macOS can rename `.gsd` to `.gsd 2`, `.gsd 3`, etc. when a directory
5
+ * already exists at the target path. ensureGsdSymlink() must detect and
6
+ * remove these numbered variants so the real `.gsd` symlink is always
7
+ * the one in use.
8
+ */
9
+
10
+ import {
11
+ mkdtempSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ existsSync,
15
+ lstatSync,
16
+ realpathSync,
17
+ mkdirSync,
18
+ symlinkSync,
19
+ readlinkSync,
20
+ } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { tmpdir } from "node:os";
23
+ import { execSync } from "node:child_process";
24
+
25
+ import { ensureGsdSymlink, externalGsdRoot } from "../repo-identity.ts";
26
+ import { createTestContext } from "./test-helpers.ts";
27
+
28
+ const { assertEq, assertTrue, report } = createTestContext();
29
+
30
+ function run(command: string, cwd: string): string {
31
+ return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
32
+ }
33
+
34
+ async function main(): Promise<void> {
35
+ const base = realpathSync(mkdtempSync(join(tmpdir(), "gsd-symlink-variants-")));
36
+ const stateDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-state-variants-")));
37
+
38
+ try {
39
+ process.env.GSD_STATE_DIR = stateDir;
40
+
41
+ // Set up a minimal git repo
42
+ run("git init -b main", base);
43
+ run('git config user.name "Pi Test"', base);
44
+ run('git config user.email "pi@example.com"', base);
45
+ run('git remote add origin git@github.com:example/repo.git', base);
46
+ writeFileSync(join(base, "README.md"), "# Test Repo\n", "utf-8");
47
+ run("git add README.md", base);
48
+ run('git commit -m "chore: init"', base);
49
+
50
+ const externalPath = externalGsdRoot(base);
51
+
52
+ // ── Test: numbered variant directories are cleaned up ──────────────
53
+ console.log("\n=== ensureGsdSymlink removes numbered .gsd variants (#2205) ===");
54
+ {
55
+ // Simulate macOS creating numbered variants: ".gsd 2", ".gsd 3"
56
+ mkdirSync(join(base, ".gsd 2"), { recursive: true });
57
+ mkdirSync(join(base, ".gsd 3"), { recursive: true });
58
+ mkdirSync(join(base, ".gsd 4"), { recursive: true });
59
+
60
+ const result = ensureGsdSymlink(base);
61
+ assertEq(result, externalPath, "ensureGsdSymlink returns external path");
62
+ assertTrue(existsSync(join(base, ".gsd")), ".gsd exists after ensureGsdSymlink");
63
+ assertTrue(lstatSync(join(base, ".gsd")).isSymbolicLink(), ".gsd is a symlink");
64
+
65
+ // The numbered variants must have been removed
66
+ assertTrue(!existsSync(join(base, ".gsd 2")), '".gsd 2" directory was cleaned up');
67
+ assertTrue(!existsSync(join(base, ".gsd 3")), '".gsd 3" directory was cleaned up');
68
+ assertTrue(!existsSync(join(base, ".gsd 4")), '".gsd 4" directory was cleaned up');
69
+ }
70
+
71
+ // ── Test: numbered variant symlinks are cleaned up ─────────────────
72
+ console.log("\n=== ensureGsdSymlink removes numbered symlink variants ===");
73
+ {
74
+ // Clean slate
75
+ rmSync(join(base, ".gsd"), { recursive: true, force: true });
76
+
77
+ // Simulate: ".gsd 2" is a symlink to the correct target (the real .gsd)
78
+ // and ".gsd" doesn't exist — this is the actual macOS scenario
79
+ const staleTarget = join(stateDir, "projects", "stale-target");
80
+ mkdirSync(staleTarget, { recursive: true });
81
+ symlinkSync(externalPath, join(base, ".gsd 2"), "junction");
82
+ symlinkSync(staleTarget, join(base, ".gsd 3"), "junction");
83
+
84
+ const result = ensureGsdSymlink(base);
85
+ assertEq(result, externalPath, "ensureGsdSymlink returns external path when variants exist");
86
+ assertTrue(existsSync(join(base, ".gsd")), ".gsd exists");
87
+ assertTrue(lstatSync(join(base, ".gsd")).isSymbolicLink(), ".gsd is a symlink");
88
+
89
+ assertTrue(!existsSync(join(base, ".gsd 2")), '".gsd 2" symlink variant was cleaned up');
90
+ assertTrue(!existsSync(join(base, ".gsd 3")), '".gsd 3" symlink variant was cleaned up');
91
+ }
92
+
93
+ // ── Test: real .gsd directory blocks symlink, but variants still cleaned ──
94
+ console.log("\n=== ensureGsdSymlink cleans variants even when .gsd is a real directory ===");
95
+ {
96
+ // Clean slate
97
+ rmSync(join(base, ".gsd"), { recursive: true, force: true });
98
+
99
+ // .gsd is a real directory (git-tracked) and numbered variants exist
100
+ mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
101
+ writeFileSync(join(base, ".gsd", "milestones", "M001.md"), "# M001\n", "utf-8");
102
+ mkdirSync(join(base, ".gsd 2"), { recursive: true });
103
+ mkdirSync(join(base, ".gsd 3"), { recursive: true });
104
+
105
+ const result = ensureGsdSymlink(base);
106
+ // When .gsd is a real directory, ensureGsdSymlink preserves it
107
+ assertEq(result, join(base, ".gsd"), "real .gsd directory preserved");
108
+ assertTrue(lstatSync(join(base, ".gsd")).isDirectory(), ".gsd remains a directory");
109
+
110
+ // But the numbered variants should still be cleaned up
111
+ assertTrue(!existsSync(join(base, ".gsd 2")), '".gsd 2" cleaned even when .gsd is a directory');
112
+ assertTrue(!existsSync(join(base, ".gsd 3")), '".gsd 3" cleaned even when .gsd is a directory');
113
+ }
114
+
115
+ // ── Test: only numeric-suffixed variants are removed ───────────────
116
+ console.log("\n=== ensureGsdSymlink only removes .gsd + space + digit variants ===");
117
+ {
118
+ rmSync(join(base, ".gsd"), { recursive: true, force: true });
119
+
120
+ // These should NOT be touched
121
+ mkdirSync(join(base, ".gsd-backup"), { recursive: true });
122
+ mkdirSync(join(base, ".gsd_old"), { recursive: true });
123
+
124
+ // These SHOULD be removed (macOS collision pattern)
125
+ mkdirSync(join(base, ".gsd 2"), { recursive: true });
126
+ mkdirSync(join(base, ".gsd 10"), { recursive: true });
127
+
128
+ ensureGsdSymlink(base);
129
+
130
+ assertTrue(existsSync(join(base, ".gsd-backup")), ".gsd-backup is NOT removed");
131
+ assertTrue(existsSync(join(base, ".gsd_old")), ".gsd_old is NOT removed");
132
+ assertTrue(!existsSync(join(base, ".gsd 2")), '".gsd 2" removed');
133
+ assertTrue(!existsSync(join(base, ".gsd 10")), '".gsd 10" removed');
134
+
135
+ // Cleanup non-variant dirs
136
+ rmSync(join(base, ".gsd-backup"), { recursive: true, force: true });
137
+ rmSync(join(base, ".gsd_old"), { recursive: true, force: true });
138
+ }
139
+
140
+ } finally {
141
+ delete process.env.GSD_STATE_DIR;
142
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* ignore */ }
143
+ try { rmSync(stateDir, { recursive: true, force: true }); } catch { /* ignore */ }
144
+ report();
145
+ }
146
+ }
147
+
148
+ main().catch((error) => {
149
+ console.error(error);
150
+ process.exit(1);
151
+ });
@@ -118,6 +118,51 @@ console.log('\n── Loop guard: arg order is normalized ──');
118
118
  assertEq(getToolCallLoopCount(), 2, 'Should detect as same call regardless of key order');
119
119
  }
120
120
 
121
+ // ═══════════════════════════════════════════════════════════════════════════
122
+ // Nested/array arguments produce distinct hashes
123
+ // ═══════════════════════════════════════════════════════════════════════════
124
+
125
+ console.log('\n── Loop guard: nested args are not stripped ──');
126
+
127
+ {
128
+ resetToolCallLoopGuard();
129
+
130
+ // Simulate ask_user_questions-style calls with different nested content
131
+ for (let i = 1; i <= 5; i++) {
132
+ const result = checkToolCallLoop('ask_user_questions', {
133
+ questions: [{ id: `q${i}`, question: `Question ${i}?` }],
134
+ });
135
+ assertTrue(result.block === false, `Nested call ${i} with unique content should be allowed`);
136
+ assertEq(getToolCallLoopCount(), 1, `Each unique nested call should reset count to 1`);
137
+ }
138
+
139
+ // Truly identical nested calls should still be detected
140
+ resetToolCallLoopGuard();
141
+ for (let i = 1; i <= 4; i++) {
142
+ checkToolCallLoop('ask_user_questions', {
143
+ questions: [{ id: 'same', question: 'Same?' }],
144
+ });
145
+ }
146
+ const blocked = checkToolCallLoop('ask_user_questions', {
147
+ questions: [{ id: 'same', question: 'Same?' }],
148
+ });
149
+ assertTrue(blocked.block === true, 'Identical nested calls should still be blocked');
150
+ }
151
+
152
+ // ═══════════════════════════════════════════════════════════════════════════
153
+ // Nested object key order is normalized
154
+ // ═══════════════════════════════════════════════════════════════════════════
155
+
156
+ console.log('\n── Loop guard: nested key order is normalized ──');
157
+
158
+ {
159
+ resetToolCallLoopGuard();
160
+
161
+ checkToolCallLoop('tool', { outer: { b: 2, a: 1 } });
162
+ const result = checkToolCallLoop('tool', { outer: { a: 1, b: 2 } });
163
+ assertEq(getToolCallLoopCount(), 2, 'Same nested args in different key order should match');
164
+ }
165
+
121
166
  // ═══════════════════════════════════════════════════════════════════════════
122
167
 
123
168
  report();